Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit dcfd2cb6d116b2e73b01f796c5e1972414c06f93
parent a077a89c6a9ae7dafe1839190d55a390b3011875
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 30 Sep 2026 22:15:32 -0400

deck S5: On-screen section — errors beside the settings, switch follows the click, folded table when off

The validator's sentences sit above the settings form when the panel is on
and at the top when it is off. The switch shows the pressed state while its
write is in flight and falls back if refused. With the panel off the table
is folded under a summary counting the rows already written. Re-render
on-screen is disabled until the cut has been built, since it works over the
segments a build left. The true-still caption separator is fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mumtool/components/projects/OnscreenSection.tsx | 290+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
1 file changed, 164 insertions(+), 126 deletions(-)

diff --git a/umtool/components/projects/OnscreenSection.tsx b/umtool/components/projects/OnscreenSection.tsx @@ -436,6 +436,9 @@ export default function OnscreenSection({ const [job, setJob] = useState<JobView | null>(null); const [jobError, setJobError] = useState<string | null>(null); const [finalV, setFinalV] = useState<number | null | "none">(null); + // The switch as pressed, while its write is in flight: the box follows the + // hand at once and falls back to the manifest's answer if the write fails. + const [switching, setSwitching] = useState<boolean | null>(null); // One token for the manifest, shared by both writers here: a settings save // moves it, and the table's next save must carry the moved one. A ref, as // in the clip bench, because two saves can be in flight before a render. @@ -591,9 +594,14 @@ export default function OnscreenSection({ ) return; } + setSwitching(on); const ok = await putChrome(on ? { engine: "hyperframes", layout: "deck", deck: {} } : null); - if (!ok) return; + if (!ok) { + setSwitching(null); + return; + } const c = await loadChrome(); + setSwitching(null); await loadRows(true); if (c?.deckOn) await recompose(); else setPreview(null); @@ -775,6 +783,134 @@ export default function OnscreenSection({ const on = doc.deckOn; const over = (s: string) => s.trim().length > maxChars; + // The validator's sentences, one per problem. Beside the settings when the + // panel is on (that is the form they are about); at the top when it is off, + // where a refused switch is. + const errorList = errors.length > 0 && ( + <ul + data-testid="onscreen-errors" + className="space-y-0.5 rounded border border-[var(--color-bad)] px-2 py-1 text-[11px] text-[var(--color-bad)]" + > + {errors.map((e) => ( + <li key={e}>{e}</li> + ))} + </ul> + ); + + const words = ( + <div className="space-y-1.5"> + <div className="flex flex-wrap items-center gap-2"> + <span className="micro">titles and subtitles — {rows.length} entries</span> + <span className="text-[11px] text-[var(--color-dim)]"> + empty uses the grey text; the counter warns past {maxChars}, it does not refuse + </span> + <button + type="button" + data-testid="onscreen-save" + className={`${buttonVariants({ variant: "primary", size: "sm" })} ml-auto`} + disabled={!!busy || dirtyIds.length === 0} + onClick={() => void saveRows()} + > + {dirtyIds.length ? `save ${dirtyIds.length} change${dirtyIds.length === 1 ? "" : "s"}` : "saved"} + </button> + {dirtyIds.length > 0 && ( + <button + type="button" + data-testid="onscreen-discard" + className={buttonVariants({ size: "sm" })} + disabled={!!busy} + onClick={() => setDrafts(Object.fromEntries(rows.map((r) => [r.id, draftOf(r.onscreen)])))} + > + discard + </button> + )} + </div> + <div className="overflow-x-auto"> + <table data-testid="onscreen-table" className="w-full border-collapse text-[12px]"> + <thead> + <tr className="micro text-left"> + <th className="w-16 px-1.5 py-0.5">entry</th> + <th className="px-1.5 py-0.5">title</th> + <th className="w-14 px-1.5 py-0.5 text-right">chars</th> + <th className="px-1.5 py-0.5">subtitle</th> + </tr> + </thead> + <tbody> + {rows.map((row) => { + const d = drafts[row.id] ?? draftOf(row.onscreen); + const dirty = !sameDraft(d, draftOf(row.onscreen)); + const shown = d.title.trim() || row.auto.title; + const isCurrent = current?.id === row.id; + const set = (k: keyof Draft, v: string) => + setDrafts((prev) => ({ ...prev, [row.id]: { ...(prev[row.id] ?? draftOf(row.onscreen)), [k]: v } })); + return ( + <tr + key={row.id} + data-testid="onscreen-row" + data-entry={row.id} + data-dirty={dirty ? "1" : "0"} + data-current={isCurrent ? "1" : "0"} + className={`border-t border-[var(--color-line)] align-middle ${isCurrent ? "bg-[var(--color-panel-2)]" : ""}`} + > + <td className="px-1.5 py-1"> + <button + type="button" + onClick={() => jump(row.id)} + className={`font-mono text-[11px] hover:underline ${isCurrent ? "text-[var(--color-sel)]" : "text-[var(--color-text)]"}`} + title="show this entry in the preview" + > + {row.id} + </button> + <span className="micro ml-1">{row.type === "clip" ? "" : row.type}</span> + {dirty && <span className="ml-1 text-[var(--color-dirty)]" title="unsaved">●</span>} + </td> + <td className="px-1.5 py-1"> + <input + type="text" + data-testid="onscreen-title" + name={`title-${row.id}`} + value={d.title} + maxLength={200} + placeholder={row.auto.title || "(no title — the source line takes its place)"} + onFocus={() => jump(row.id)} + onChange={(e) => set("title", e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void saveRows(); + }} + className={`${field} w-full ${over(shown) ? "border-[var(--color-dirty)]" : "border-[var(--color-line)]"}`} + /> + </td> + <td + className={`num px-1.5 py-1 text-right font-mono text-[11px] ${over(shown) ? "text-[var(--color-dirty)]" : "text-[var(--color-dim)]"}`} + data-testid="onscreen-count" + data-over={over(shown) ? "1" : "0"} + > + {shown.trim().length}/{maxChars} + </td> + <td className="px-1.5 py-1"> + <input + type="text" + data-testid="onscreen-subtitle" + name={`subtitle-${row.id}`} + value={d.subtitle} + maxLength={200} + placeholder={row.auto.subtitle || "(no archive record here — the build reads the fetched file)"} + onFocus={() => jump(row.id)} + onChange={(e) => set("subtitle", e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void saveRows(); + }} + className={`${input} w-full`} + /> + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + </div> + ); return ( <section @@ -789,8 +925,8 @@ export default function OnscreenSection({ <input type="checkbox" data-testid="onscreen-toggle" - checked={on} - disabled={!!busy} + checked={switching ?? on} + disabled={!!busy || switching !== null} onChange={(e) => void toggle(e.target.checked)} /> a panel under the footage: timeline, title, source, QR @@ -838,13 +974,7 @@ export default function OnscreenSection({ </p> )} - {errors.length > 0 && ( - <ul data-testid="onscreen-errors" className="space-y-0.5 rounded border border-[var(--color-bad)] px-2 py-1 text-[11px] text-[var(--color-bad)]"> - {errors.map((e) => ( - <li key={e}>{e}</li> - ))} - </ul> - )} + {!on && errorList} {on && ( <div className="grid gap-3 xl:grid-cols-[minmax(0,1fr)_minmax(20rem,26rem)]"> @@ -974,9 +1104,14 @@ export default function OnscreenSection({ type="button" data-testid="onscreen-rerender" className={buttonVariants({ variant: "primary", size: "sm" })} - disabled={job?.state === "running"} + // Over the segments a build left, so there must have been one. + disabled={job?.state === "running" || finalV === "none"} onClick={() => void rerender()} - title="Render the panel over the segments already built and re-concat. No segment is rebuilt." + title={ + typeof finalV === "number" + ? "Render the panel over the segments already built and re-concat. No segment is rebuilt." + : "Build the cut first: a re-render works over the segments a build left" + } > re-render on-screen </button> @@ -1004,7 +1139,7 @@ export default function OnscreenSection({ /> <figcaption className="num text-[11px] text-[var(--color-dim)]"> true still at {clock(still.at)} - {still.estimated ? " · estimated timing" : ""} — what is saved, drawn by the render&rsquo;s browser + {still.estimated ? " · estimated timing" : ""} · what is saved, drawn by the render&rsquo;s browser </figcaption> </figure> )} @@ -1069,6 +1204,7 @@ export default function OnscreenSection({ defaults </button> </div> + {errorList} <div className="grid grid-cols-[max-content_minmax(0,1fr)] items-center gap-x-2 gap-y-1 text-[11px]"> {GROUPS.map((group) => ( <div key={group.name} className="contents"> @@ -1149,119 +1285,21 @@ export default function OnscreenSection({ </div> )} - {/* ================= the words ================= */} - <div className="space-y-1.5"> - <div className="flex flex-wrap items-center gap-2"> - <span className="micro">titles and subtitles — {rows.length} entries</span> - <span className="text-[11px] text-[var(--color-dim)]"> - empty uses the grey text; the counter warns past {maxChars}, it does not refuse - </span> - <button - type="button" - data-testid="onscreen-save" - className={`${buttonVariants({ variant: "primary", size: "sm" })} ml-auto`} - disabled={!!busy || dirtyIds.length === 0} - onClick={() => void saveRows()} - > - {dirtyIds.length ? `save ${dirtyIds.length} change${dirtyIds.length === 1 ? "" : "s"}` : "saved"} - </button> - {dirtyIds.length > 0 && ( - <button - type="button" - data-testid="onscreen-discard" - className={buttonVariants({ size: "sm" })} - disabled={!!busy} - onClick={() => setDrafts(Object.fromEntries(rows.map((r) => [r.id, draftOf(r.onscreen)])))} - > - discard - </button> - )} - </div> - <div className="overflow-x-auto"> - <table data-testid="onscreen-table" className="w-full border-collapse text-[12px]"> - <thead> - <tr className="micro text-left"> - <th className="w-16 px-1.5 py-0.5">entry</th> - <th className="px-1.5 py-0.5">title</th> - <th className="w-14 px-1.5 py-0.5 text-right">chars</th> - <th className="px-1.5 py-0.5">subtitle</th> - </tr> - </thead> - <tbody> - {rows.map((row) => { - const d = drafts[row.id] ?? draftOf(row.onscreen); - const dirty = !sameDraft(d, draftOf(row.onscreen)); - const shown = d.title.trim() || row.auto.title; - const isCurrent = current?.id === row.id; - const set = (k: keyof Draft, v: string) => - setDrafts((prev) => ({ ...prev, [row.id]: { ...(prev[row.id] ?? draftOf(row.onscreen)), [k]: v } })); - return ( - <tr - key={row.id} - data-testid="onscreen-row" - data-entry={row.id} - data-dirty={dirty ? "1" : "0"} - data-current={isCurrent ? "1" : "0"} - className={`border-t border-[var(--color-line)] align-middle ${isCurrent ? "bg-[var(--color-panel-2)]" : ""}`} - > - <td className="px-1.5 py-1"> - <button - type="button" - onClick={() => jump(row.id)} - className={`font-mono text-[11px] hover:underline ${isCurrent ? "text-[var(--color-sel)]" : "text-[var(--color-text)]"}`} - title="show this entry in the preview" - > - {row.id} - </button> - <span className="micro ml-1">{row.type === "clip" ? "" : row.type}</span> - {dirty && <span className="ml-1 text-[var(--color-dirty)]" title="unsaved">●</span>} - </td> - <td className="px-1.5 py-1"> - <input - type="text" - data-testid="onscreen-title" - name={`title-${row.id}`} - value={d.title} - maxLength={200} - placeholder={row.auto.title || "(no title — the source line takes its place)"} - onFocus={() => jump(row.id)} - onChange={(e) => set("title", e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void saveRows(); - }} - className={`${field} w-full ${over(shown) ? "border-[var(--color-dirty)]" : "border-[var(--color-line)]"}`} - /> - </td> - <td - className={`num px-1.5 py-1 text-right font-mono text-[11px] ${over(shown) ? "text-[var(--color-dirty)]" : "text-[var(--color-dim)]"}`} - data-testid="onscreen-count" - data-over={over(shown) ? "1" : "0"} - > - {shown.trim().length}/{maxChars} - </td> - <td className="px-1.5 py-1"> - <input - type="text" - data-testid="onscreen-subtitle" - name={`subtitle-${row.id}`} - value={d.subtitle} - maxLength={200} - placeholder={row.auto.subtitle || "(no archive record here — the build reads the fetched file)"} - onFocus={() => jump(row.id)} - onChange={(e) => set("subtitle", e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void saveRows(); - }} - className={`${input} w-full`} - /> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - </div> + {/* ================= the words ================= + Open when the panel is on. Folded when it is off: a report that + never uses the panel should not carry a nineteen-row form, and one + that will can have its titles written before the switch. */} + {on ? ( + words + ) : ( + <details data-testid="onscreen-words-folded"> + <summary className="cursor-pointer text-[11px] text-[var(--color-dim)]"> + titles and subtitles — {rows.length} entries + {rows.some((r) => r.onscreen) ? ` · ${rows.filter((r) => r.onscreen).length} written` : ""} + </summary> + <div className="mt-1.5">{words}</div> + </details> + )} </section> ); }