From 6dcc7cbecb0682e2ac6e5d3c514365101fbb48d3 Mon Sep 17 00:00:00 2001 From: Project7 Date: Mon, 8 Jun 2026 02:56:06 +0000 Subject: [PATCH] feat: simplify focused lettering editor --- app/web/components/CutListPanel.test.tsx | 4 +- app/web/components/CutListPanel.tsx | 4 +- app/web/components/LetteringEditor.test.tsx | 28 ++ app/web/components/LetteringEditor.tsx | 289 ++++++++++++-------- 4 files changed, 214 insertions(+), 111 deletions(-) diff --git a/app/web/components/CutListPanel.test.tsx b/app/web/components/CutListPanel.test.tsx index e33338a..546459e 100644 --- a/app/web/components/CutListPanel.test.tsx +++ b/app/web/components/CutListPanel.test.tsx @@ -2337,7 +2337,9 @@ describe("CutListPanel asset diagnostics + Refresh assets (#427)", () => { expect( await screen.findByTestId("lettering-review-state-1"), ).toHaveTextContent("No draft"); - expect(screen.getByTestId("ai-draft-1")).toBeInTheDocument(); + expect(screen.getByTestId("ai-draft-1")).toHaveTextContent( + "AI draft bubbles", + ); fireEvent.click(screen.getByTestId("ai-draft-1")); expect( diff --git a/app/web/components/CutListPanel.tsx b/app/web/components/CutListPanel.tsx index 8f08490..8035f54 100644 --- a/app/web/components/CutListPanel.tsx +++ b/app/web/components/CutListPanel.tsx @@ -413,7 +413,9 @@ function CutRow({ !cut.uploadedUrl && canDraftLettering(cut); const aiDraftLabel = - (cut.overlays?.length ?? 0) > 0 ? "Re-draft with AI" : "AI draft lettering"; + (cut.overlays?.length ?? 0) > 0 + ? "Re-draft bubbles with AI" + : "AI draft bubbles"; const primary: { label: string; onClick: () => void; testid: string } | null = board.key === "convert" diff --git a/app/web/components/LetteringEditor.test.tsx b/app/web/components/LetteringEditor.test.tsx index 12c367a..802a505 100644 --- a/app/web/components/LetteringEditor.test.tsx +++ b/app/web/components/LetteringEditor.test.tsx @@ -853,6 +853,34 @@ describe("LetteringEditor", () => { expect(onClose).toHaveBeenCalled(); }); + it("keeps the focused editor toolbar compact and moves guide copy behind Help (#501)", async () => { + render( + , + ); + + const toolbar = screen.getByTestId("lettering-toolbar"); + expect(toolbar).toHaveTextContent("Focused lettering editor"); + expect(toolbar).not.toHaveTextContent( + /Place bubbles, captions, SFX, or between-scene card text/i, + ); + expect(screen.queryByTestId("lettering-help-panel")).not.toBeInTheDocument(); + + fireEvent.click(screen.getByTestId("lettering-help-toggle")); + expect(screen.getByTestId("lettering-help-panel")).toHaveTextContent( + /Add or select a bubble/i, + ); + }); + // #310: the editor preview must wrap bubble dialogue into multiple lines // (shared layout with the export), not a single truncated label. it("renders wrapped multi-line bubble text in the preview (WYSIWYG)", async () => { diff --git a/app/web/components/LetteringEditor.tsx b/app/web/components/LetteringEditor.tsx index ed2f440..16484fb 100644 --- a/app/web/components/LetteringEditor.tsx +++ b/app/web/components/LetteringEditor.tsx @@ -224,6 +224,7 @@ export function LetteringEditor({ const [exporting, setExporting] = useState(false); const [exportError, setExportError] = useState(null); const [saveError, setSaveError] = useState(null); + const [showHelp, setShowHelp] = useState(false); const [imageBounds, setImageBounds] = useState({ x: 0, y: 0, @@ -681,6 +682,23 @@ export function LetteringEditor({ displayFontFamily, ]); const warningCount = Object.keys(overlayWarnings).length; + const checklistChips: Array<{ + key: string; + label: string; + done: boolean; + }> = [ + { key: "clean-image", label: "Clean", done: checklist.hasCleanImage }, + { key: "script-text", label: "Script", done: checklist.hasScriptText }, + { + key: "bubbles", + label: checklist.bubblesPlaced + ? `Bubbles ${checklist.bubblesPlaced}` + : "Bubbles", + done: checklist.bubblesPlaced > 0, + }, + { key: "exported", label: "Exported", done: checklist.exported }, + { key: "uploaded", label: "Uploaded", done: checklist.uploaded }, + ]; const isTextPanel = cut.kind === "text"; const isNarrationCut = !cut.cleanImagePath; @@ -708,42 +726,81 @@ export function LetteringEditor({ data-testid="focused-lettering-editor" > {/* Toolbar */} -
-
-
- - Focused lettering editor - - - {targetLabel ?? `Cut #${cut.id}`} - -
-

- Place bubbles, captions, SFX, or between-scene card text, then save - back to the full cut review. -

- - {overlays.length} overlays - -
-
+
+
+ + Focused lettering editor + + + {targetLabel ?? `Cut #${cut.id}`} + + + {overlays.length} overlays + + {checklistChips.map((chip) => ( + + {chip.done ? "✓ " : "○ "} + {chip.label} + + ))} + {cut.aiDraft?.status === "generated" && ( + + AI draft ready + + )} + {staleExport && ( + + Re-export needed + + )} +
+
{onToggleWorkspaceVisible && ( )} -
+ +
{exportError && ( - {exportError} + + {exportError} + )} {saveError && ( - {saveError} + + {saveError} + )}
+ {showHelp && ( +
+ Add or select a bubble, edit it in the inspector, then Save to return + to cut review. Use Export after the overlay layout is ready. Text cards + use narration overlays on the canvas. +
+ )} + {invalidOverlayCount > 0 && !acknowledgedInvalid ? (
)} - {/* Per-cut lettering checklist (#336): shows how far this cut has come so - the writer can finish it from the editor without inspecting cuts.json. */} -
- {( - [ - ["clean-image", "Clean image", checklist.hasCleanImage], - ["script-text", "Script text", checklist.hasScriptText], - [ - "bubbles", - `Bubbles placed${checklist.bubblesPlaced ? ` (${checklist.bubblesPlaced})` : ""}`, - checklist.bubblesPlaced > 0, - ], - ["exported", "Final exported", checklist.exported], - ["uploaded", "Uploaded", checklist.uploaded], - ] as [string, string, boolean][] - ).map(([key, label, done]) => ( - - {done ? "✓" : "○"} - {label} - - ))} -
- {/* Stale-export warning (#336, re1): bubbles changed since the recorded export/upload, so the final image/uploaded URL are out of date. The - checklist already marks export/upload incomplete; this says why. */} + compact toolbar chips already mark export/upload incomplete; this says why. */} {staleExport && (
- {cut.aiDraft?.status === "generated" && ( -
-

- AI draft ready -

-

- These first-pass overlays came from the cut script. Review and - tune them here before exporting the final panel. -

-
- )} - {/* Insert-from-script (#336): drop the cut's planned dialogue/narration/ - SFX straight into a prefilled overlay — no copy/paste out of JSON. */} - {scriptLines.length > 0 && ( -
- - From script - -
- {scriptLines.map((line) => ( - - ))} -
-
- )} {selectedOverlay ? (
-

- {TYPE_LABEL[selectedOverlay.type]} -

+
+

+ {TYPE_LABEL[selectedOverlay.type]} +

+ + #{overlays.findIndex((o) => o.id === selectedOverlay.id) + 1} + +
{selectedOverlay.speaker !== undefined && (
) : ( -

- Select an overlay to inspect. -

+
+

+ Select or add an overlay to inspect it. +

+ {cut.aiDraft?.status === "generated" && ( +
+ AI drafted overlays are editable here before export. +
+ )} + {/* Insert-from-script (#336): drop the cut's planned dialogue/narration/ + SFX straight into a prefilled overlay — no copy/paste out of JSON. */} + {scriptLines.length > 0 && ( +
+ + Add from script + +
+ {scriptLines.map((line) => ( + + ))} +
+
+ )} +
)}