six defects, and a typecheck the repo could not pass
Found by reviewing the plugin rather than running it. Nothing was live — the table does not exist and the plugin is not installed — so all of these were waiting for the first person to use it. ── The second unnamed design could not be saved ── `uniqueIndex(userId, title)`, and every design starts life called "Untitled": `titleOf` returns it for a document with no title and the API defaults to it. So the first saved, the second collided, and the insert threw under a panel that could only say "Changes are not being saved." Renaming one design to match another did the same. Two designs may share a name, as files and notes and dashboards do. Nothing looks a design up by title, so uniqueness bought nothing. The index is now ordinary and on the columns the list query actually uses. The lesson from `dashboards.id` that the schema comment cites is that a key must be scoped to its owner. `userId` on the row does that; making the pair unique as well imported the collision it was citing, one account at a time. ── New did not start a new design ── It cleared `?doc=`, and no parameter already means "open what I had last", answered with `/documents/latest`. So New handed back the design on screen and pointed the next save at its id. There is a `new` sentinel now, in its own module because both panels read it and neither owns it. ── Two ways the load effect raced the debounced save ── Changing `?doc=` does not unmount the panel, so the unmount flush never ran for it: an edit made within the debounce and navigated away from was written after `latest.current` had been replaced, which is to say never. And the first save of a new design puts its id in the URL, which lands in the same effect — re-reading the row overwrote in-memory state with the copy the server had just been given, discarding whatever was drawn during the round trip. The effect now flushes what is pending before it reads, and does not re-read a document it already holds. The editor is keyed on a load generation rather than on `row?.id`, which could not express "blank again" and left the previous design on screen with `rowId` cleared underneath it. ── The typecheck ── 354 of the 355 errors were upstream's tree measured against this platform's settings, which it was never written for. `bunx tsgo` is how this repo is kept clean and it could not be clean while this plugin existed. `exclude` does not help: adapter.ts imports the tree back in. So vendor.sh marks each vendored file unchecked in the same deterministic pass that rewrites its imports. Types still resolve; only the errors inside those files are silenced, which is what the adapter already says the seam is for. Whole repo: 0. ── PROVENANCE.md understated what is done to the copy ── It said "the one edit vendor.sh makes" and described the import rewrite. It also runs apply-host.py, which places our own lib/host.ts and wires three export paths. Four files are not upstream and the document whose job is making the copy trustworthy did not say so. It lists all four now. Also: web/host.ts imported a path only correct at its destination, so the file could never typecheck where it is edited — apply-host.py rewrites that line when placing it, and re-marks the placed copy, which it overwrites after vendor.sh has marked it. PUT is no longer full-replace: omitting `title` renamed a design to "Untitled" and omitting `ui` nulled it, a trap set for the second caller. And a document is bounded at 4 MB, measured in bytes, where the title was capped at 200 and the document at nothing.
This commit is contained in:
+37
-2
@@ -71,6 +71,27 @@ router.get('/documents/:id', async (ctx) => {
|
|||||||
|
|
||||||
type Body = { title?: string; doc?: unknown; ui?: unknown };
|
type Body = { title?: string; doc?: unknown; ui?: unknown };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How large one design may be.
|
||||||
|
*
|
||||||
|
* The title was capped at 200 and the document at nothing at all, which is the wrong way round: the
|
||||||
|
* document is the part that grows without anybody deciding to make it grow, and it is stored per user
|
||||||
|
* with no quota above it. A real design with several screens of parts measures in the tens of
|
||||||
|
* kilobytes, so this is far above anything the editor produces and far below anything that would hurt.
|
||||||
|
*
|
||||||
|
* Measured on the serialised form rather than guessed at from the object, because that is the thing
|
||||||
|
* the column actually holds.
|
||||||
|
*/
|
||||||
|
const MAX_DOC_BYTES = 4 * 1024 * 1024;
|
||||||
|
|
||||||
|
/** Null when it fits, a message when it does not. */
|
||||||
|
const tooLarge = (doc: unknown, ui: unknown): string | null => {
|
||||||
|
// Bytes, not string length: `.length` counts UTF-16 units, and a design full of non-Latin labels
|
||||||
|
// would measure well under what it actually stores.
|
||||||
|
const size = Buffer.byteLength(JSON.stringify({ doc, ui }), 'utf8');
|
||||||
|
return size > MAX_DOC_BYTES ? `design is too large (${Math.round(size / 1024)} KB)` : null;
|
||||||
|
};
|
||||||
|
|
||||||
router.post('/documents', async (ctx) => {
|
router.post('/documents', async (ctx) => {
|
||||||
const userId = userIdOf(ctx);
|
const userId = userIdOf(ctx);
|
||||||
if (!userId) return ctx.json({ error: 'unauthenticated' }, 401);
|
if (!userId) return ctx.json({ error: 'unauthenticated' }, 401);
|
||||||
@@ -78,6 +99,9 @@ router.post('/documents', async (ctx) => {
|
|||||||
const body = (await ctx.req.json().catch(() => null)) as Body | null;
|
const body = (await ctx.req.json().catch(() => null)) as Body | null;
|
||||||
if (!body?.doc) return ctx.json({ error: 'doc is required' }, 400);
|
if (!body?.doc) return ctx.json({ error: 'doc is required' }, 400);
|
||||||
|
|
||||||
|
const oversize = tooLarge(body.doc, body.ui);
|
||||||
|
if (oversize) return ctx.json({ error: oversize }, 413);
|
||||||
|
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.insert(m3eCanvasDocuments)
|
.insert(m3eCanvasDocuments)
|
||||||
.values({
|
.values({
|
||||||
@@ -91,6 +115,14 @@ router.post('/documents', async (ctx) => {
|
|||||||
return ctx.json(row, 201);
|
return ctx.json(row, 201);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A save. Only what is SENT is written.
|
||||||
|
*
|
||||||
|
* `title` and `ui` were unconditional — omitting either renamed the design to "Untitled" and nulled
|
||||||
|
* the editor state. The only caller today sends both every time, so nothing was losing anything; it
|
||||||
|
* was a trap set for the second caller, and the panel's own comment calls `ui` the cheaper of the two
|
||||||
|
* things to lose, which is a reason to be careful with it rather than a reason not to be.
|
||||||
|
*/
|
||||||
router.put('/documents/:id', async (ctx) => {
|
router.put('/documents/:id', async (ctx) => {
|
||||||
const userId = userIdOf(ctx);
|
const userId = userIdOf(ctx);
|
||||||
if (!userId) return ctx.json({ error: 'unauthenticated' }, 401);
|
if (!userId) return ctx.json({ error: 'unauthenticated' }, 401);
|
||||||
@@ -101,12 +133,15 @@ router.put('/documents/:id', async (ctx) => {
|
|||||||
const body = (await ctx.req.json().catch(() => null)) as Body | null;
|
const body = (await ctx.req.json().catch(() => null)) as Body | null;
|
||||||
if (!body?.doc) return ctx.json({ error: 'doc is required' }, 400);
|
if (!body?.doc) return ctx.json({ error: 'doc is required' }, 400);
|
||||||
|
|
||||||
|
const oversize = tooLarge(body.doc, body.ui);
|
||||||
|
if (oversize) return ctx.json({ error: oversize }, 413);
|
||||||
|
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.update(m3eCanvasDocuments)
|
.update(m3eCanvasDocuments)
|
||||||
.set({
|
.set({
|
||||||
title: (body.title ?? 'Untitled').slice(0, 200),
|
...(body.title === undefined ? {} : { title: body.title.slice(0, 200) }),
|
||||||
|
...(body.ui === undefined ? {} : { ui: body.ui }),
|
||||||
doc: body.doc,
|
doc: body.doc,
|
||||||
ui: body.ui ?? null,
|
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(and(eq(m3eCanvasDocuments.id, id), eq(m3eCanvasDocuments.userId, userId)))
|
.where(and(eq(m3eCanvasDocuments.id, id), eq(m3eCanvasDocuments.userId, userId)))
|
||||||
|
|||||||
+21
-4
@@ -1,4 +1,4 @@
|
|||||||
import { pgTable, serial, text, integer, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, text, integer, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A design, and the editor state that goes with it.
|
* A design, and the editor state that goes with it.
|
||||||
@@ -44,9 +44,26 @@ export const m3eCanvasDocuments = pgTable(
|
|||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
},
|
},
|
||||||
(table) => ({
|
(table) => ({
|
||||||
// Composite via uniqueIndex, never `unique().on(...)` — drizzle-kit mis-diffs named composite
|
/*
|
||||||
// unique CONSTRAINTS and re-creates them on every push. `databases/CLAUDE.md`, "Composite keys".
|
Ordinary, NOT unique — and it used to be `uniqueIndex(userId, title)`, which could not work.
|
||||||
byUser: uniqueIndex('m3e_canvas_documents_user_title').on(table.userId, table.title),
|
|
||||||
|
Every design starts life called "Untitled": `titleOf` returns it for a document with no title of
|
||||||
|
its own, and the API defaults to it. So the first design saved, the second one collided, and the
|
||||||
|
insert threw under a panel that could only say "Changes are not being saved." Renaming one design
|
||||||
|
to match another did the same thing.
|
||||||
|
|
||||||
|
Two designs may share a name. That is true of files, of notes and of dashboards, and the
|
||||||
|
alternative — uniquifying the title on save — renames somebody's work behind their back to
|
||||||
|
satisfy an index. Nothing here looks a design up BY title, so uniqueness bought nothing and cost
|
||||||
|
the ordinary case.
|
||||||
|
|
||||||
|
The lesson from `dashboards.id` that the note above cites is that a key must be scoped to its
|
||||||
|
owner. `userId` on the row does that. Making the pair unique as well imported the collision it
|
||||||
|
was citing, one account at a time.
|
||||||
|
|
||||||
|
The columns are the list query: filter by user, newest first.
|
||||||
|
*/
|
||||||
|
byUser: index('m3e_canvas_documents_user_updated').on(table.userId, table.updatedAt),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+16
-1
@@ -222,7 +222,22 @@ def main() -> int:
|
|||||||
source = VENDOR.parent / "host.ts"
|
source = VENDOR.parent / "host.ts"
|
||||||
if not source.exists():
|
if not source.exists():
|
||||||
raise SystemExit("web/host.ts is missing — it is the source for the vendored lib/host.ts.")
|
raise SystemExit("web/host.ts is missing — it is the source for the vendored lib/host.ts.")
|
||||||
(VENDOR / "lib" / "host.ts").write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
|
# One line is rewritten on the way in. `web/host.ts` is edited where it lives, so it imports
|
||||||
|
# `./vendor/lib/tokens` — which is correct there and wrong once placed a directory deeper.
|
||||||
|
# Deterministic, like vendor.sh own import rewrite, and it keeps the canonical copy typecheckable
|
||||||
|
# instead of permanently carrying an unresolvable import for the sake of its destination.
|
||||||
|
placed = source.read_text(encoding="utf-8").replace('"./vendor/lib/tokens"', '"./tokens"')
|
||||||
|
if '"./tokens"' not in placed:
|
||||||
|
fail("the tokens import to rewrite", "web/host.ts")
|
||||||
|
|
||||||
|
# Marked like the rest of the vendored tree, because this step runs AFTER vendor.sh has marked it
|
||||||
|
# and writes the file whole — so without this line the mark is lost on every re-vendor and this one
|
||||||
|
# file starts failing the typecheck again. The canonical copy at web/host.ts stays checked; only
|
||||||
|
# the placed copy is exempt, for the same reason its neighbours are.
|
||||||
|
if not placed.startswith("// @ts-nocheck"):
|
||||||
|
placed = "// @ts-nocheck — vendored, see PROVENANCE.md\n" + placed
|
||||||
|
|
||||||
|
(VENDOR / "lib" / "host.ts").write_text(placed, encoding="utf-8")
|
||||||
print(" placed lib/host.ts (ours)")
|
print(" placed lib/host.ts (ours)")
|
||||||
for rel, fn in (
|
for rel, fn in (
|
||||||
("Canvas.tsx", canvas),
|
("Canvas.tsx", canvas),
|
||||||
|
|||||||
+47
-7
@@ -15,8 +15,11 @@
|
|||||||
# which is what keeps the copy reviewable: `git diff` after a re-vendor shows upstream's real changes,
|
# which is what keeps the copy reviewable: `git diff` after a re-vendor shows upstream's real changes,
|
||||||
# because the rewrite produces the same output for the same input every time.
|
# because the rewrite produces the same output for the same input every time.
|
||||||
#
|
#
|
||||||
# Nothing else is edited. Officer's own changes live outside `web/vendor/`, reached through
|
# The other edit is `apply-host.py`, run at the end: it places our own `lib/host.ts` and wires the host
|
||||||
# `web/adapter.ts`.
|
# hooks into the three export paths. Nothing else is touched, and PROVENANCE.md lists all four files so
|
||||||
|
# somebody diffing against upstream can account for every difference.
|
||||||
|
#
|
||||||
|
# Officer's own code lives outside `web/vendor/`, reached through `web/adapter.ts`.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REF="${1:-main}"
|
REF="${1:-main}"
|
||||||
@@ -61,8 +64,27 @@ find "$DEST" \( -name '*.test.ts' -o -name '*.test.tsx' -o -name '*.spec.ts' -o
|
|||||||
# ── the mechanical rewrite ──
|
# ── the mechanical rewrite ──
|
||||||
# `@/lib/x` -> relative path to vendor/lib/x
|
# `@/lib/x` -> relative path to vendor/lib/x
|
||||||
# `@/components/x` -> relative path to vendor/components/x
|
# `@/components/x` -> relative path to vendor/components/x
|
||||||
|
# and `// @ts-nocheck` at the top of every file — see the long note below.
|
||||||
python3 - "$DEST" <<'PY'
|
python3 - "$DEST" <<'PY'
|
||||||
import os, re, sys
|
import os, re, sys
|
||||||
|
|
||||||
|
# Upstream is a Next.js app and is typechecked as one. This platform is stricter — verbatimModuleSyntax,
|
||||||
|
# strictNullChecks, noImplicitAny — and against those settings upstream's tree reports 354 errors it was
|
||||||
|
# never written to satisfy: type-only imports, possibly-null, implicit any.
|
||||||
|
#
|
||||||
|
# That is not a bug in upstream and it is not ours to fix. It is a bug in what it does to everyone else:
|
||||||
|
# `bunx tsgo` is how this repo is kept clean, and it cannot be clean while these files are in it, so the
|
||||||
|
# check stops being a gate and becomes a thing people filter.
|
||||||
|
#
|
||||||
|
# `exclude` in tsconfig does not work here. It drops files from the initial set, but `web/adapter.ts`
|
||||||
|
# imports the tree and TypeScript pulls every one of them back in.
|
||||||
|
#
|
||||||
|
# So each file is marked unchecked, in this same deterministic pass. Types still RESOLVE — the adapter
|
||||||
|
# goes on getting `Doc`, `Frame`, `buildPrompt` and their real signatures — only the errors inside these
|
||||||
|
# files are silenced. That matches what adapter.ts already says the seam is for: when upstream renames
|
||||||
|
# something, the breakage surfaces at the adapter, which is ours and is checked.
|
||||||
|
MARK = "// @ts-nocheck — vendored, see PROVENANCE.md\n"
|
||||||
|
|
||||||
dest = sys.argv[1]
|
dest = sys.argv[1]
|
||||||
for root, _, files in os.walk(dest):
|
for root, _, files in os.walk(dest):
|
||||||
for name in files:
|
for name in files:
|
||||||
@@ -73,6 +95,10 @@ for root, _, files in os.walk(dest):
|
|||||||
prefix = "./" if depth == "." else "../" * len(depth.split(os.sep))
|
prefix = "./" if depth == "." else "../" * len(depth.split(os.sep))
|
||||||
src = open(path, encoding="utf-8").read()
|
src = open(path, encoding="utf-8").read()
|
||||||
out = re.sub(r'(["\'])@/(lib|components)/', lambda m: f"{m.group(1)}{prefix}{m.group(2)}/", src)
|
out = re.sub(r'(["\'])@/(lib|components)/', lambda m: f"{m.group(1)}{prefix}{m.group(2)}/", src)
|
||||||
|
# First line, before any "use client" — a directive only has to precede STATEMENTS, and
|
||||||
|
# @ts-nocheck only counts if it is in the file's opening comment block.
|
||||||
|
if not out.startswith("// @ts-nocheck"):
|
||||||
|
out = MARK + out
|
||||||
if out != src:
|
if out != src:
|
||||||
open(path, "w", encoding="utf-8").write(out)
|
open(path, "w", encoding="utf-8").write(out)
|
||||||
PY
|
PY
|
||||||
@@ -109,13 +135,27 @@ keep model-provider API keys in \`localStorage\`, which is not how anything here
|
|||||||
faithful copy carrying an unused module beats a doctored one that cannot be re-vendored. They are
|
faithful copy carrying an unused module beats a doctored one that cannot be re-vendored. They are
|
||||||
inert without a key, and the shell does not surface them.
|
inert without a key, and the shell does not surface them.
|
||||||
|
|
||||||
## The one edit \`vendor.sh\` makes
|
## What is NOT upstream in this directory
|
||||||
|
|
||||||
Every \`@/lib/…\` and \`@/components/…\` import is rewritten to a relative path. This platform already
|
Four files differ from the clone, and this list is the whole of it. Anything else that does not
|
||||||
binds \`@/*\` to \`src/apps/officer-web\` and \`@/components/*\` to its own shared components, so an
|
match upstream is a mistake.
|
||||||
unrewritten import resolves to the WRONG module rather than failing — silent breakage.
|
|
||||||
|
|
||||||
The rewrite is deterministic, so a re-vendor produces a readable diff of upstream's real changes.
|
**Every \`.ts\`/\`.tsx\` file — the import rewrite.** Every \`@/lib/…\` and \`@/components/…\` import
|
||||||
|
becomes a relative path. This platform already binds \`@/*\` to \`src/apps/officer-web\` and
|
||||||
|
\`@/components/*\` to its own shared components, so an unrewritten import resolves to the WRONG
|
||||||
|
module rather than failing — silent breakage.
|
||||||
|
|
||||||
|
**\`lib/host.ts\` — ours, not upstream's.** Upstream reviewed the host-adapter contract on
|
||||||
|
lnkiai/m3e-canvas#56 and declined to carry it for now, so it is written here and placed by
|
||||||
|
\`scripts/apply-host.py\`. The canonical copy is \`web/host.ts\`; this one is a copy of it.
|
||||||
|
|
||||||
|
**\`Canvas.tsx\`, \`components/PromptPanel.tsx\`, \`components/Inspector.tsx\` — host hooks wired in.**
|
||||||
|
\`apply-host.py\` matches on structure rather than context so upstream can reformat freely, and it
|
||||||
|
stops loudly when an anchor genuinely disappears. All three export paths are wired, which is what
|
||||||
|
upstream said a complete version needs.
|
||||||
|
|
||||||
|
Both transformations are deterministic, so a re-vendor produces a readable diff of upstream's real
|
||||||
|
changes rather than of the edits above.
|
||||||
PROV
|
PROV
|
||||||
|
|
||||||
echo "vendored $SHA ($WHEN) -> web/vendor/"
|
echo "vendored $SHA ($WHEN) -> web/vendor/"
|
||||||
|
|||||||
+65
-6
@@ -1,7 +1,8 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useSearchParams } from "react-router";
|
import { useSearchParams } from "react-router";
|
||||||
import { useClient } from "hooks/useClient";
|
import { useClient } from "hooks/useClient";
|
||||||
import { Canvas, HostProvider, effectivePrompt, titleOf, type Doc, type HostAdapter } from "./adapter";
|
import { Canvas, HostProvider, titleOf, type Doc, type HostAdapter } from "./adapter";
|
||||||
|
import { NEW_DOC } from "./doc-param";
|
||||||
import "./vendor/globals.css";
|
import "./vendor/globals.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,30 +38,86 @@ export const CanvasPanel = () => {
|
|||||||
const [params, setParams] = useSearchParams();
|
const [params, setParams] = useSearchParams();
|
||||||
const requested = params.get("doc");
|
const requested = params.get("doc");
|
||||||
|
|
||||||
const [row, setRow] = useState<DocumentRow | null>(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
|
/*
|
||||||
|
What the editor is keyed on: one count of how many times a document has been ESTABLISHED here.
|
||||||
|
|
||||||
|
It was `row?.id ?? 'new'`, which cannot express the case it most needed to. After the first save
|
||||||
|
the panel deliberately does not re-read the row, so `row` stays null and the key stays `'new'` —
|
||||||
|
and clicking New a second time then changed nothing, leaving the previous design on screen with
|
||||||
|
`rowId` cleared underneath it, so the next save wrote a copy of it.
|
||||||
|
|
||||||
|
Bumped only where a document is actually put in place, never on the already-open path, so a save
|
||||||
|
does not remount the editor mid-edit.
|
||||||
|
*/
|
||||||
|
const [generation, setGeneration] = useState(0);
|
||||||
|
|
||||||
/** The newest document, outside React state — see the note above on why. */
|
/** The newest document, outside React state — see the note above on why. */
|
||||||
const latest = useRef<{ doc: Partial<Doc> | null; ui: unknown }>({ doc: null, ui: null });
|
const latest = useRef<{ doc: Partial<Doc> | null; ui: unknown }>({ doc: null, ui: null });
|
||||||
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const rowId = useRef<number | null>(null);
|
const rowId = useRef<number | null>(null);
|
||||||
|
|
||||||
|
/*
|
||||||
|
`flush` is defined below and the load effect above needs it.
|
||||||
|
|
||||||
|
Through a ref, not the dependency list: `flush` changes identity when `setParams` does, and a load
|
||||||
|
effect that re-runs on that would re-read the document mid-edit — the exact thing the guard inside
|
||||||
|
it exists to prevent.
|
||||||
|
*/
|
||||||
|
const flushRef = useRef<() => Promise<void>>(async () => {});
|
||||||
|
|
||||||
// ── load, or start a new one ────────────────────────────────────────────────────────────────────
|
// ── load, or start a new one ────────────────────────────────────────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
|
/*
|
||||||
|
── Two things have to happen before this reads anything ──
|
||||||
|
|
||||||
|
WRITE OUT what is still in the timer. Changing `?doc=` does not unmount the panel, so the
|
||||||
|
unmount flush below never ran for it: an edit made within the debounce and then navigated away
|
||||||
|
from was written after `latest.current` had already been replaced with the next document, which
|
||||||
|
is to say never. Up to a full debounce of somebody's work, lost silently and only sometimes.
|
||||||
|
|
||||||
|
Then DO NOT re-read a document already held. The first save of a new design puts its id in the
|
||||||
|
URL, which lands right here — and re-reading overwrote `latest.current` with the copy the server
|
||||||
|
had just been given, discarding whatever was drawn during the round trip, and changed the
|
||||||
|
canvas `key` mid-edit so the editor remounted on top of it.
|
||||||
|
*/
|
||||||
|
if (saveTimer.current) {
|
||||||
|
clearTimeout(saveTimer.current);
|
||||||
|
saveTimer.current = null;
|
||||||
|
void flushRef.current();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already open, and `latest.current` is newer than anything a read would return.
|
||||||
|
if (requested && rowId.current !== null && requested === String(rowId.current) && latest.current.doc) {
|
||||||
|
setReady(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setReady(false);
|
setReady(false);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const found = requested
|
/*
|
||||||
? await clientRef.current.get<DocumentRow>(`/m3e-canvas/documents/${requested}`)
|
`new` is a design that does not exist yet, and it is NOT the same as no parameter at all.
|
||||||
|
|
||||||
|
No parameter means "whatever I had open", answered with `/documents/latest`. Asking for a
|
||||||
|
blank canvas that way returned the most recently updated design instead, and pointed the
|
||||||
|
next save at its id — so New reopened the thing you were already on and then edited it.
|
||||||
|
*/
|
||||||
|
const found =
|
||||||
|
requested === NEW_DOC ? null
|
||||||
|
: requested ? await clientRef.current.get<DocumentRow>(`/m3e-canvas/documents/${requested}`)
|
||||||
: await clientRef.current.get<DocumentRow | null>("/m3e-canvas/documents/latest");
|
: await clientRef.current.get<DocumentRow | null>("/m3e-canvas/documents/latest");
|
||||||
|
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
rowId.current = found?.id ?? null;
|
rowId.current = found?.id ?? null;
|
||||||
latest.current = { doc: found?.doc ?? null, ui: found?.ui ?? null };
|
latest.current = { doc: found?.doc ?? null, ui: found?.ui ?? null };
|
||||||
setRow(found ?? null);
|
// The editor reads the document once, at mount. This is what makes it mount again.
|
||||||
|
setGeneration((n) => n + 1);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setError("Could not open that design.");
|
if (!cancelled) setError("Could not open that design.");
|
||||||
@@ -97,6 +154,8 @@ export const CanvasPanel = () => {
|
|||||||
}
|
}
|
||||||
}, [setParams]);
|
}, [setParams]);
|
||||||
|
|
||||||
|
flushRef.current = flush;
|
||||||
|
|
||||||
const schedule = useCallback(() => {
|
const schedule = useCallback(() => {
|
||||||
if (saveTimer.current) clearTimeout(saveTimer.current);
|
if (saveTimer.current) clearTimeout(saveTimer.current);
|
||||||
saveTimer.current = setTimeout(() => void flush(), SAVE_DEBOUNCE_MS);
|
saveTimer.current = setTimeout(() => void flush(), SAVE_DEBOUNCE_MS);
|
||||||
@@ -151,7 +210,7 @@ export const CanvasPanel = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<HostProvider value={host}>
|
<HostProvider value={host}>
|
||||||
<Canvas key={row?.id ?? "new"} />
|
<Canvas key={generation} />
|
||||||
</HostProvider>
|
</HostProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+12
-4
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|||||||
import { useSearchParams } from "react-router";
|
import { useSearchParams } from "react-router";
|
||||||
import { FilePlus2, PenTool, RefreshCw, Trash2 } from "lucide-react";
|
import { FilePlus2, PenTool, RefreshCw, Trash2 } from "lucide-react";
|
||||||
import { useClient } from "hooks/useClient";
|
import { useClient } from "hooks/useClient";
|
||||||
|
import { NEW_DOC } from "./doc-param";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of designs — the one concept upstream does not have.
|
* The list of designs — the one concept upstream does not have.
|
||||||
@@ -40,10 +41,17 @@ export const DesignsPanel = () => {
|
|||||||
void load();
|
void load();
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
// The canvas creates a row on its first save, so this only has to clear the selection — a design
|
/*
|
||||||
// with nothing drawn in it should not exist, and creating one here would leave empties behind
|
The canvas creates the row on its first save, so New only has to ask for a blank one — a design
|
||||||
// every time somebody clicked New and changed their mind.
|
with nothing drawn in it should not exist, and creating one here would leave empties behind every
|
||||||
const startNew = () => setParams((p) => { p.delete("doc"); return p; }, { replace: true });
|
time somebody clicked New and changed their mind.
|
||||||
|
|
||||||
|
`?doc=new`, and NOT clearing the parameter, which is what this did. No parameter means "open what
|
||||||
|
I was last working on", which the canvas answers with `/documents/latest` — so New handed back the
|
||||||
|
design already on screen and pointed the next save at it. The sentinel says the thing the absence
|
||||||
|
could not: an empty one, on purpose.
|
||||||
|
*/
|
||||||
|
const startNew = () => setParams((p) => { p.set("doc", NEW_DOC); return p; }, { replace: true });
|
||||||
|
|
||||||
const remove = async (id: number, title: string) => {
|
const remove = async (id: number, title: string) => {
|
||||||
if (!confirm(`Delete "${title}"? This cannot be undone.`)) return;
|
if (!confirm(`Delete "${title}"? This cannot be undone.`)) return;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* `?doc=` — which design is on screen, and the one value that is not an id.
|
||||||
|
*
|
||||||
|
* Here rather than in either panel because both read it and neither owns it: the list writes it, the
|
||||||
|
* canvas answers it, and a constant that lives in one of them makes the other import a component to
|
||||||
|
* get at a string.
|
||||||
|
*
|
||||||
|
* ── Why a sentinel and not an absent parameter ──
|
||||||
|
*
|
||||||
|
* Absent already means something: "open whatever I had last", which the canvas answers with
|
||||||
|
* `/documents/latest`. A blank canvas is a different request, and expressing it by removing the
|
||||||
|
* parameter made New hand back the design already on screen — then point the next save at its id.
|
||||||
|
*
|
||||||
|
* `new` is not a valid id, so nothing can collide with it: `Number('new')` is NaN and every document
|
||||||
|
* route rejects that before it reaches the database.
|
||||||
|
*/
|
||||||
|
export const NEW_DOC = 'new';
|
||||||
+4
-1
@@ -1,7 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
import type { Doc } from "./tokens";
|
// `./vendor/lib/tokens` HERE, `./tokens` once placed. apply-host.py rewrites this one line when it
|
||||||
|
// copies the file into web/vendor/lib/ — the same deterministic-edit trick vendor.sh uses on
|
||||||
|
// upstream's `@/` imports, and what lets this file typecheck where it is edited.
|
||||||
|
import type { Doc } from "./vendor/lib/tokens";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks for an application that embeds the editor.
|
* Hooks for an application that embeds the editor.
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
Vendored
+19
-5
@@ -25,10 +25,24 @@ keep model-provider API keys in `localStorage`, which is not how anything here s
|
|||||||
faithful copy carrying an unused module beats a doctored one that cannot be re-vendored. They are
|
faithful copy carrying an unused module beats a doctored one that cannot be re-vendored. They are
|
||||||
inert without a key, and the shell does not surface them.
|
inert without a key, and the shell does not surface them.
|
||||||
|
|
||||||
## The one edit `vendor.sh` makes
|
## What is NOT upstream in this directory
|
||||||
|
|
||||||
Every `@/lib/…` and `@/components/…` import is rewritten to a relative path. This platform already
|
Four files differ from the clone, and this list is the whole of it. Anything else that does not
|
||||||
binds `@/*` to `src/apps/officer-web` and `@/components/*` to its own shared components, so an
|
match upstream is a mistake.
|
||||||
unrewritten import resolves to the WRONG module rather than failing — silent breakage.
|
|
||||||
|
|
||||||
The rewrite is deterministic, so a re-vendor produces a readable diff of upstream's real changes.
|
**Every `.ts`/`.tsx` file — the import rewrite.** Every `@/lib/…` and `@/components/…` import
|
||||||
|
becomes a relative path. This platform already binds `@/*` to `src/apps/officer-web` and
|
||||||
|
`@/components/*` to its own shared components, so an unrewritten import resolves to the WRONG
|
||||||
|
module rather than failing — silent breakage.
|
||||||
|
|
||||||
|
**`lib/host.ts` — ours, not upstream's.** Upstream reviewed the host-adapter contract on
|
||||||
|
lnkiai/m3e-canvas#56 and declined to carry it for now, so it is written here and placed by
|
||||||
|
`scripts/apply-host.py`. The canonical copy is `web/host.ts`; this one is a copy of it.
|
||||||
|
|
||||||
|
**`Canvas.tsx`, `components/PromptPanel.tsx`, `components/Inspector.tsx` — host hooks wired in.**
|
||||||
|
`apply-host.py` matches on structure rather than context so upstream can reformat freely, and it
|
||||||
|
stops loudly when an anchor genuinely disappears. All three export paths are wired, which is what
|
||||||
|
upstream said a complete version needs.
|
||||||
|
|
||||||
|
Both transformations are deterministic, so a re-vendor produces a readable diff of upstream's real
|
||||||
|
changes rather than of the edits above.
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ReactNode, useMemo, useState } from "react";
|
import { ReactNode, useMemo, useState } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
/** The app mark: one frozen frame of the M3 Expressive loading indicator with the layers glyph inside. */
|
/** The app mark: one frozen frame of the M3 Expressive loading indicator with the layers glyph inside. */
|
||||||
const COOKIE =
|
const COOKIE =
|
||||||
"M56.3679 6.02002C57.1442 5.3783 57.5324 5.05744 57.8867 4.78656C66.2354 -1.59552 77.7646 -1.59552 86.1133 4.78656C86.4676 5.05744 86.8558 5.3783 87.6321 6.02002C87.9786 6.30648 88.1519 6.44971 88.3233 6.58606C92.2522 9.71203 97.0693 11.4835 102.068 11.6405C102.286 11.6473 102.51 11.6501 102.957 11.6558C103.96 11.6684 104.462 11.6747 104.906 11.6973C115.361 12.2303 124.193 19.7179 126.528 30.0289C126.627 30.4666 126.721 30.9644 126.907 31.9602C126.99 32.4047 127.032 32.627 127.076 32.8427C128.097 37.789 130.661 42.2744 134.39 45.6409C134.552 45.7878 134.722 45.9353 135.062 46.2304C135.822 46.8914 136.202 47.2219 136.528 47.5275C144.198 54.7262 146.2 66.1979 141.429 75.6132C141.227 76.0128 140.981 76.4547 140.49 77.3386C140.271 77.7332 140.162 77.9304 140.059 78.1246C137.694 82.5768 136.804 87.6775 137.519 92.6782C137.55 92.8964 137.586 93.1196 137.658 93.5661C137.82 94.5662 137.901 95.0663 137.956 95.5117C139.252 106.008 133.488 116.096 123.843 120.21C123.434 120.385 122.965 120.564 122.026 120.922C121.608 121.082 121.398 121.162 121.196 121.244C116.552 123.119 112.625 126.448 109.991 130.743C109.876 130.93 109.762 131.125 109.533 131.514C109.021 132.385 108.765 132.821 108.523 133.198C102.839 142.08 92.0048 146.064 81.9992 142.952C81.5745 142.82 81.1011 142.652 80.1544 142.318C79.7318 142.168 79.5205 142.094 79.3133 142.025C74.5631 140.445 69.4369 140.445 64.6867 142.025C64.4795 142.094 64.2682 142.168 63.8456 142.318C62.8989 142.652 62.4255 142.82 62.0008 142.952C51.9952 146.064 41.1613 142.08 35.4766 133.198C35.2353 132.821 34.9791 132.385 34.4669 131.514C34.2382 131.125 34.1239 130.93 34.009 130.743C31.3752 126.448 27.4482 123.119 22.8044 121.244C22.6018 121.162 22.3924 121.082 21.9736 120.922C21.0354 120.564 20.5663 120.385 20.1569 120.21C10.5122 116.096 4.74763 106.008 6.04367 95.5117C6.09868 95.0663 6.17963 94.5662 6.34151 93.5661C6.41377 93.1196 6.4499 92.8964 6.48109 92.6783C7.19603 87.6775 6.30587 82.5768 3.94121 78.1246C3.83807 77.9304 3.72855 77.7332 3.50951 77.3386C3.01883 76.4547 2.77349 76.0128 2.57099 75.6132C-2.19995 66.1979 -0.197931 54.7262 7.47248 47.5275C7.79804 47.2219 8.17819 46.8914 8.93848 46.2304C9.27787 45.9353 9.44757 45.7878 9.61023 45.6409C13.3394 42.2744 15.9025 37.789 16.9235 32.8427C16.9681 32.627 17.0097 32.4047 17.0929 31.9602C17.2793 30.9644 17.3725 30.4666 17.4717 30.0289C19.8069 19.7179 28.6387 12.2303 39.0944 11.6973C39.5382 11.6747 40.0397 11.6684 41.0426 11.6558C41.4903 11.6501 41.7142 11.6473 41.9322 11.6405C46.9307 11.4835 51.7478 9.71203 55.6767 6.58606C55.8481 6.44971 56.0214 6.30648 56.3679 6.02002Z";
|
"M56.3679 6.02002C57.1442 5.3783 57.5324 5.05744 57.8867 4.78656C66.2354 -1.59552 77.7646 -1.59552 86.1133 4.78656C86.4676 5.05744 86.8558 5.3783 87.6321 6.02002C87.9786 6.30648 88.1519 6.44971 88.3233 6.58606C92.2522 9.71203 97.0693 11.4835 102.068 11.6405C102.286 11.6473 102.51 11.6501 102.957 11.6558C103.96 11.6684 104.462 11.6747 104.906 11.6973C115.361 12.2303 124.193 19.7179 126.528 30.0289C126.627 30.4666 126.721 30.9644 126.907 31.9602C126.99 32.4047 127.032 32.627 127.076 32.8427C128.097 37.789 130.661 42.2744 134.39 45.6409C134.552 45.7878 134.722 45.9353 135.062 46.2304C135.822 46.8914 136.202 47.2219 136.528 47.5275C144.198 54.7262 146.2 66.1979 141.429 75.6132C141.227 76.0128 140.981 76.4547 140.49 77.3386C140.271 77.7332 140.162 77.9304 140.059 78.1246C137.694 82.5768 136.804 87.6775 137.519 92.6782C137.55 92.8964 137.586 93.1196 137.658 93.5661C137.82 94.5662 137.901 95.0663 137.956 95.5117C139.252 106.008 133.488 116.096 123.843 120.21C123.434 120.385 122.965 120.564 122.026 120.922C121.608 121.082 121.398 121.162 121.196 121.244C116.552 123.119 112.625 126.448 109.991 130.743C109.876 130.93 109.762 131.125 109.533 131.514C109.021 132.385 108.765 132.821 108.523 133.198C102.839 142.08 92.0048 146.064 81.9992 142.952C81.5745 142.82 81.1011 142.652 80.1544 142.318C79.7318 142.168 79.5205 142.094 79.3133 142.025C74.5631 140.445 69.4369 140.445 64.6867 142.025C64.4795 142.094 64.2682 142.168 63.8456 142.318C62.8989 142.652 62.4255 142.82 62.0008 142.952C51.9952 146.064 41.1613 142.08 35.4766 133.198C35.2353 132.821 34.9791 132.385 34.4669 131.514C34.2382 131.125 34.1239 130.93 34.009 130.743C31.3752 126.448 27.4482 123.119 22.8044 121.244C22.6018 121.162 22.3924 121.082 21.9736 120.922C21.0354 120.564 20.5663 120.385 20.1569 120.21C10.5122 116.096 4.74763 106.008 6.04367 95.5117C6.09868 95.0663 6.17963 94.5662 6.34151 93.5661C6.41377 93.1196 6.4499 92.8964 6.48109 92.6783C7.19603 87.6775 6.30587 82.5768 3.94121 78.1246C3.83807 77.9304 3.72855 77.7332 3.50951 77.3386C3.01883 76.4547 2.77349 76.0128 2.57099 75.6132C-2.19995 66.1979 -0.197931 54.7262 7.47248 47.5275C7.79804 47.2219 8.17819 46.8914 8.93848 46.2304C9.27787 45.9353 9.44757 45.7878 9.61023 45.6409C13.3394 42.2744 15.9025 37.789 16.9235 32.8427C16.9681 32.627 17.0097 32.4047 17.0929 31.9602C17.2793 30.9644 17.3725 30.4666 17.4717 30.0289C19.8069 19.7179 28.6387 12.2303 39.0944 11.6973C39.5382 11.6747 40.0397 11.6684 41.0426 11.6558C41.4903 11.6501 41.7142 11.6473 41.9322 11.6405C46.9307 11.4835 51.7478 9.71203 55.6767 6.58606C55.8481 6.44971 56.0214 6.30648 56.3679 6.02002Z";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|||||||
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|||||||
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
import { Doc, Frame, Group, Item, frameOfGroup } from "./tokens";
|
import { Doc, Frame, Group, Item, frameOfGroup } from "./tokens";
|
||||||
import { buildPrompt } from "./prompt";
|
import { buildPrompt } from "./prompt";
|
||||||
import { isProject } from "./project";
|
import { isProject } from "./project";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
/**
|
/**
|
||||||
* A small Material-style scheme generator: one seed color becomes a light
|
* A small Material-style scheme generator: one seed color becomes a light
|
||||||
* color scheme by placing each role at a fixed tone (CIE L*) with the seed's
|
* color scheme by placing each role at a fixed tone (CIE L*) with the seed's
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
import { Doc, KIND_ORDER, Kind, VARIANTS, isPlatform } from "./tokens";
|
import { Doc, KIND_ORDER, Kind, VARIANTS, isPlatform } from "./tokens";
|
||||||
|
|
||||||
/* A project file is the Doc as JSON, nothing more. Reading one back only checks
|
/* A project file is the Doc as JSON, nothing more. Reading one back only checks
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
import { KIND_TEXT, Lang, SWIPE_TEXT, TRANSITION_TEXT, getLang } from "./i18n";
|
import { KIND_TEXT, Lang, SWIPE_TEXT, TRANSITION_TEXT, getLang } from "./i18n";
|
||||||
import {
|
import {
|
||||||
Platform,
|
Platform,
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
/**
|
/**
|
||||||
* Material 3 Expressive loading-indicator shapes.
|
* Material 3 Expressive loading-indicator shapes.
|
||||||
*
|
*
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
import { Doc } from "./tokens";
|
import { Doc } from "./tokens";
|
||||||
import { isProject } from "./project";
|
import { isProject } from "./project";
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
import { CONTENT_W, FULL_WIDTH, Frame, Group, Item, KIND_SPEC, Kind, PHONE_MARGIN, RAIL_W, canJoin, isPhoneFrame, scaleR, carryItemSize, connectSpecOf, frameOfGroup, frameRect, frameSizeOf, groupBounds, isExpanded } from "./tokens";
|
import { CONTENT_W, FULL_WIDTH, Frame, Group, Item, KIND_SPEC, Kind, PHONE_MARGIN, RAIL_W, canJoin, isPhoneFrame, scaleR, carryItemSize, connectSpecOf, frameOfGroup, frameRect, frameSizeOf, groupBounds, isExpanded } from "./tokens";
|
||||||
|
|
||||||
/* Rule-based layout for one screen. Nothing here is guessed by a model.
|
/* Rule-based layout for one screen. Nothing here is guessed by a model.
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck — vendored, see PROVENANCE.md
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { FAB_MENU_TABS, KIND_TEXT, Lang, NAV_TABS, TAB_LABELS, getLang, t } from "./i18n";
|
import { FAB_MENU_TABS, KIND_TEXT, Lang, NAV_TABS, TAB_LABELS, getLang, t } from "./i18n";
|
||||||
import { Contrast, schemeFromSeed } from "./color";
|
import { Contrast, schemeFromSeed } from "./color";
|
||||||
|
|||||||
Reference in New Issue
Block a user