plotlink-ows 1.2.96 → 1.2.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/lib/cartoon-production-status.ts +77 -0
- package/app/web/components/CartoonNextAction.tsx +154 -36
- package/app/web/components/CartoonProductionStatus.tsx +142 -0
- package/app/web/components/CartoonPublishPage.tsx +34 -25
- package/app/web/components/CartoonWorkflowNav.tsx +3 -7
- package/app/web/components/CutListPanel.tsx +372 -243
- package/app/web/components/CutOverlayPreview.tsx +314 -0
- package/app/web/components/EpisodesPage.tsx +34 -4
- package/app/web/components/FinishEpisodePanel.tsx +21 -108
- package/app/web/components/LetteringEditor.tsx +246 -131
- package/app/web/components/PreviewPanel.tsx +200 -214
- package/app/web/components/StoriesPage.tsx +105 -91
- package/app/web/components/StoryBrowser.tsx +53 -19
- package/app/web/components/StoryInfoPage.tsx +31 -14
- package/app/web/components/StoryProgressPanel.tsx +31 -28
- package/app/web/dist/assets/{export-cut-BqZI0-Rv.js → export-cut-DVpOZ5AO.js} +1 -1
- package/app/web/dist/assets/index-CoG6WKyb.js +141 -0
- package/app/web/dist/assets/index-H5_FM885.css +32 -0
- package/app/web/dist/index.html +2 -2
- package/package.json +1 -1
- package/app/web/dist/assets/index-C43toXVm.js +0 -141
- package/app/web/dist/assets/index-CcfChGEK.css +0 -32
|
@@ -4,11 +4,8 @@ import remarkBreaks from "remark-breaks";
|
|
|
4
4
|
import remarkGfm from "remark-gfm";
|
|
5
5
|
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
|
6
6
|
import { GENRES, LANGUAGES, canonicalizeGenre } from "../../../lib/genres";
|
|
7
|
-
import { CartoonPreview } from "./CartoonPreview";
|
|
8
|
-
import { CartoonPublishPreview } from "./CartoonPublishPreview";
|
|
9
|
-
import { CutListPanel } from "./CutListPanel";
|
|
10
|
-
import { WorkflowCoach } from "./WorkflowCoach";
|
|
11
7
|
import type { CoachUiAction } from "@app-lib/cartoon-coach";
|
|
8
|
+
import { CutListPanel } from "./CutListPanel";
|
|
12
9
|
import {
|
|
13
10
|
classifyCartoonReadiness,
|
|
14
11
|
cartoonGenesisReadiness,
|
|
@@ -120,6 +117,11 @@ interface PreviewPanelProps {
|
|
|
120
117
|
onFocusedLetteringModeChange?: (active: boolean) => void;
|
|
121
118
|
/** Restore/fold the wider app work area while staying in the editor. */
|
|
122
119
|
onFocusedLetteringWorkspaceVisibleChange?: (visible: boolean) => void;
|
|
120
|
+
workflowActionRequest?: {
|
|
121
|
+
action: CoachUiAction;
|
|
122
|
+
seq: number;
|
|
123
|
+
} | null;
|
|
124
|
+
onWorkflowActionHandled?: (seq: number) => void;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
interface FileData {
|
|
@@ -136,6 +138,44 @@ interface FileData {
|
|
|
136
138
|
|
|
137
139
|
type Tab = "preview" | "edit";
|
|
138
140
|
|
|
141
|
+
function workflowActionNeedsCuts(action: CoachUiAction | null | undefined): boolean {
|
|
142
|
+
return action === "open-cuts"
|
|
143
|
+
|| action === "open-lettering"
|
|
144
|
+
|| action === "upload"
|
|
145
|
+
|| action === "refresh-assets";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function cartoonEpisodeLabel(fileName: string | null): string {
|
|
149
|
+
if (fileName === "genesis.md") return "epi-01 (Genesis)";
|
|
150
|
+
const m = fileName?.match(/^plot-(\d+)\.md$/);
|
|
151
|
+
if (!m) return fileName ?? "";
|
|
152
|
+
const episodeNumber = parseInt(m[1], 10) + 1;
|
|
153
|
+
return `epi-${String(episodeNumber).padStart(2, "0")}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function extractMarkdownTitle(content: string | null | undefined): string | null {
|
|
157
|
+
const title = content?.match(/^#\s+(.+?)\s*$/m)?.[1]?.trim();
|
|
158
|
+
return title || null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function cartoonEpisodeNumberLabel(fileName: string | null): string | null {
|
|
162
|
+
if (fileName === "genesis.md") return "Episode 1";
|
|
163
|
+
const m = fileName?.match(/^plot-(\d+)\.md$/);
|
|
164
|
+
if (!m) return null;
|
|
165
|
+
return `Episode ${parseInt(m[1], 10) + 1}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function cartoonEpisodeHeaderTitle(
|
|
169
|
+
fileName: string | null,
|
|
170
|
+
title: string | null,
|
|
171
|
+
): string | null {
|
|
172
|
+
const trimmedTitle = title?.trim();
|
|
173
|
+
if (!trimmedTitle) return null;
|
|
174
|
+
if (/^Episode\s+\d+\s*[—-]\s*/i.test(trimmedTitle)) return trimmedTitle;
|
|
175
|
+
const episodeLabel = cartoonEpisodeNumberLabel(fileName);
|
|
176
|
+
return episodeLabel ? `${episodeLabel} — ${trimmedTitle}` : trimmedTitle;
|
|
177
|
+
}
|
|
178
|
+
|
|
139
179
|
export function PreviewPanel({
|
|
140
180
|
storyName,
|
|
141
181
|
fileName,
|
|
@@ -155,25 +195,12 @@ export function PreviewPanel({
|
|
|
155
195
|
focusedLetteringWorkspaceVisible = false,
|
|
156
196
|
onFocusedLetteringModeChange,
|
|
157
197
|
onFocusedLetteringWorkspaceVisibleChange,
|
|
198
|
+
workflowActionRequest = null,
|
|
199
|
+
onWorkflowActionHandled,
|
|
158
200
|
}: PreviewPanelProps) {
|
|
159
201
|
const [fileData, setFileData] = useState<FileData | null>(null);
|
|
160
202
|
const [loading, setLoading] = useState(false);
|
|
161
203
|
const [activeTab, setActiveTab] = useState<Tab>("preview");
|
|
162
|
-
// Cartoon preview sub-mode: "publish" = exact PlotLink-bound markdown;
|
|
163
|
-
// "inspect" = cuts.json planning inspector. Kept distinct so planning prose
|
|
164
|
-
// does not masquerade as publish content (#289).
|
|
165
|
-
const [cartoonPreviewMode, setCartoonPreviewMode] = useState<
|
|
166
|
-
"publish" | "inspect"
|
|
167
|
-
>("publish");
|
|
168
|
-
// Cartoon Genesis is a hybrid (a prose opening + its own genesis.cuts.json image
|
|
169
|
-
// cuts), so its Edit tab offers two sub-views: the opening-text editor and the
|
|
170
|
-
// cut workspace (#429). Plots use the cut workspace directly; fiction never sees
|
|
171
|
-
// this. Defaults to "text" so opening Edit on Genesis is unchanged; the workflow
|
|
172
|
-
// coach's cut actions switch it to "cuts" so lettering/upload/refresh land on a
|
|
173
|
-
// real, actionable workspace instead of the markdown editor.
|
|
174
|
-
const [genesisEditMode, setGenesisEditMode] = useState<"text" | "cuts">(
|
|
175
|
-
"text",
|
|
176
|
-
);
|
|
177
204
|
// #371: a deep-link request from the Cut Inspector's per-cut CTA into the Edit
|
|
178
205
|
// tab for that exact cut. `seq` makes repeated clicks (even on the same cut)
|
|
179
206
|
// re-trigger the focus/expand effect in CutListPanel; it is cleared once
|
|
@@ -183,10 +210,6 @@ export function PreviewPanel({
|
|
|
183
210
|
openEditor: boolean;
|
|
184
211
|
seq: number;
|
|
185
212
|
} | null>(null);
|
|
186
|
-
const handleEditCut = useCallback((cutId: number, openEditor: boolean) => {
|
|
187
|
-
setActiveTab("edit");
|
|
188
|
-
setCutFocus((prev) => ({ cutId, openEditor, seq: (prev?.seq ?? 0) + 1 }));
|
|
189
|
-
}, []);
|
|
190
213
|
const [editContent, setEditContent] = useState("");
|
|
191
214
|
const [saving, setSaving] = useState(false);
|
|
192
215
|
const [dirty, setDirty] = useState(false);
|
|
@@ -269,6 +292,11 @@ export function PreviewPanel({
|
|
|
269
292
|
const illustrationInputRef = useRef<HTMLInputElement>(null);
|
|
270
293
|
|
|
271
294
|
const prevFileRef = useRef<string | null>(null);
|
|
295
|
+
const appliedWorkflowSeqRef = useRef(0);
|
|
296
|
+
const pendingWorkflowCutsRef = useRef(false);
|
|
297
|
+
pendingWorkflowCutsRef.current = workflowActionNeedsCuts(
|
|
298
|
+
workflowActionRequest?.action,
|
|
299
|
+
);
|
|
272
300
|
|
|
273
301
|
const loadFile = useCallback(async () => {
|
|
274
302
|
if (!storyName || !fileName) {
|
|
@@ -384,9 +412,9 @@ export function PreviewPanel({
|
|
|
384
412
|
setCartoonTotalCuts(result.totalCuts);
|
|
385
413
|
setCartoonCutProgress(summarizeCutProgress(cuts));
|
|
386
414
|
// Cut plan's episode title for the publish-title display (#358).
|
|
387
|
-
|
|
388
|
-
typeof cutsData.title === "string" ? cutsData.title :
|
|
389
|
-
);
|
|
415
|
+
const cutsTitle =
|
|
416
|
+
typeof cutsData.title === "string" ? cutsData.title.trim() : "";
|
|
417
|
+
setCartoonEpisodeTitle(cutsTitle || extractMarkdownTitle(content));
|
|
390
418
|
}
|
|
391
419
|
} catch {
|
|
392
420
|
if (!cancelled) {
|
|
@@ -533,43 +561,30 @@ export function PreviewPanel({
|
|
|
533
561
|
}
|
|
534
562
|
}, [storyName, fileName, authFetch, loadFile]);
|
|
535
563
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
(action: CoachUiAction, episodeFile: string | null) => {
|
|
544
|
-
if (action === "view-progress") {
|
|
564
|
+
useEffect(() => {
|
|
565
|
+
if (!workflowActionRequest) return;
|
|
566
|
+
if (workflowActionRequest.seq === appliedWorkflowSeqRef.current) return;
|
|
567
|
+
appliedWorkflowSeqRef.current = workflowActionRequest.seq;
|
|
568
|
+
|
|
569
|
+
switch (workflowActionRequest.action) {
|
|
570
|
+
case "view-progress":
|
|
545
571
|
onViewProgress?.();
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
break;
|
|
563
|
-
case "generate-markdown":
|
|
564
|
-
handleGenerateMarkdown();
|
|
565
|
-
break;
|
|
566
|
-
case "publish":
|
|
567
|
-
setActiveTab("preview");
|
|
568
|
-
break;
|
|
569
|
-
}
|
|
570
|
-
},
|
|
571
|
-
[fileName, onViewProgress, onOpenFile, handleGenerateMarkdown],
|
|
572
|
-
);
|
|
572
|
+
break;
|
|
573
|
+
case "open-cuts":
|
|
574
|
+
case "open-lettering":
|
|
575
|
+
case "upload":
|
|
576
|
+
case "refresh-assets":
|
|
577
|
+
setActiveTab("edit");
|
|
578
|
+
break;
|
|
579
|
+
case "generate-markdown":
|
|
580
|
+
handleGenerateMarkdown();
|
|
581
|
+
break;
|
|
582
|
+
case "publish":
|
|
583
|
+
setActiveTab("preview");
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
onWorkflowActionHandled?.(workflowActionRequest.seq);
|
|
587
|
+
}, [workflowActionRequest, onViewProgress, handleGenerateMarkdown, onWorkflowActionHandled]);
|
|
573
588
|
|
|
574
589
|
// Handle cover image selection
|
|
575
590
|
const handleCoverSelect = useCallback(
|
|
@@ -811,7 +826,7 @@ export function PreviewPanel({
|
|
|
811
826
|
setDetectedCoverWarning(null);
|
|
812
827
|
setCoverStatus("unknown");
|
|
813
828
|
coverUserTouchedRef.current = false;
|
|
814
|
-
|
|
829
|
+
if (pendingWorkflowCutsRef.current) setActiveTab("edit");
|
|
815
830
|
}, [storyName, fileName]);
|
|
816
831
|
|
|
817
832
|
// Auto-detect an agent-created cover (assets/cover.webp|jpg) for an UNPUBLISHED
|
|
@@ -994,6 +1009,13 @@ export function PreviewPanel({
|
|
|
994
1009
|
// readiness block — those move to the Publish tab — and show only the opening
|
|
995
1010
|
// content, production next-step guidance, and a compact "Review publish" CTA.
|
|
996
1011
|
const isCartoonEpisode = isCartoonGenesis || isCartoonPlot;
|
|
1012
|
+
const cartoonEpisodeDisplayTitle =
|
|
1013
|
+
isCartoonEpisode
|
|
1014
|
+
? cartoonEpisodeHeaderTitle(
|
|
1015
|
+
fileName,
|
|
1016
|
+
cartoonEpisodeTitle || extractMarkdownTitle(fileData?.content),
|
|
1017
|
+
)
|
|
1018
|
+
: null;
|
|
997
1019
|
const isPublished =
|
|
998
1020
|
fileData?.status === "published" ||
|
|
999
1021
|
fileData?.status === "published-not-indexed";
|
|
@@ -1213,7 +1235,8 @@ export function PreviewPanel({
|
|
|
1213
1235
|
// Plain prose editor (fiction files + the Genesis "Opening text" sub-view).
|
|
1214
1236
|
const proseEditor = (
|
|
1215
1237
|
<div
|
|
1216
|
-
className="flex-1 min-h-0 flex flex-col"
|
|
1238
|
+
className="flex-1 min-h-0 flex flex-col overflow-hidden"
|
|
1239
|
+
data-testid="prose-editor-shell"
|
|
1217
1240
|
style={{ background: "var(--paper-bg)" }}
|
|
1218
1241
|
>
|
|
1219
1242
|
<textarea
|
|
@@ -1231,8 +1254,12 @@ export function PreviewPanel({
|
|
|
1231
1254
|
color: "var(--text)",
|
|
1232
1255
|
}}
|
|
1233
1256
|
spellCheck={false}
|
|
1257
|
+
data-testid="prose-editor-textarea"
|
|
1234
1258
|
/>
|
|
1235
|
-
<div
|
|
1259
|
+
<div
|
|
1260
|
+
className="shrink-0 px-3 py-1.5 border-t border-border flex items-center justify-between bg-surface/95"
|
|
1261
|
+
data-testid="prose-editor-savebar"
|
|
1262
|
+
>
|
|
1236
1263
|
<span className="text-xs text-muted">
|
|
1237
1264
|
{dirty ? "Unsaved changes" : "No changes"}
|
|
1238
1265
|
</span>
|
|
@@ -1248,24 +1275,38 @@ export function PreviewPanel({
|
|
|
1248
1275
|
);
|
|
1249
1276
|
|
|
1250
1277
|
return (
|
|
1251
|
-
<div className="h-full flex flex-col">
|
|
1278
|
+
<div className="h-full min-h-0 flex flex-col">
|
|
1252
1279
|
{/* Header with file path + tabs */}
|
|
1253
1280
|
{!hideFocusedEditorChrome && (
|
|
1254
1281
|
<div className="border-b border-border">
|
|
1255
|
-
<div
|
|
1256
|
-
|
|
1282
|
+
<div
|
|
1283
|
+
className={
|
|
1284
|
+
isCartoonEpisode
|
|
1285
|
+
? "px-3 py-1.5 flex items-center justify-between gap-3"
|
|
1286
|
+
: "px-3 py-1.5 flex items-center justify-between"
|
|
1287
|
+
}
|
|
1288
|
+
>
|
|
1289
|
+
<div className="flex items-center gap-2 text-xs text-muted min-w-0">
|
|
1257
1290
|
{onViewProgress && (
|
|
1258
1291
|
<button
|
|
1259
1292
|
onClick={onViewProgress}
|
|
1260
1293
|
data-testid="view-progress-btn"
|
|
1261
|
-
className="text-accent hover:underline
|
|
1294
|
+
className="text-accent hover:underline"
|
|
1262
1295
|
title="Story progress overview"
|
|
1263
1296
|
>
|
|
1264
1297
|
← Progress
|
|
1265
1298
|
</button>
|
|
1266
1299
|
)}
|
|
1267
|
-
<span
|
|
1268
|
-
{
|
|
1300
|
+
<span
|
|
1301
|
+
className={
|
|
1302
|
+
isCartoonEpisode
|
|
1303
|
+
? "font-medium text-foreground truncate"
|
|
1304
|
+
: "font-mono"
|
|
1305
|
+
}
|
|
1306
|
+
>
|
|
1307
|
+
{isCartoonEpisode
|
|
1308
|
+
? `${cartoonEpisodeLabel(fileName)}${cartoonEpisodeDisplayTitle ? ` · ${cartoonEpisodeDisplayTitle}` : ""}`
|
|
1309
|
+
: `${storyName}/${fileName}`}
|
|
1269
1310
|
</span>
|
|
1270
1311
|
{fileData?.status === "published" && (
|
|
1271
1312
|
<span className="text-green-700 font-medium">Published</span>
|
|
@@ -1282,108 +1323,95 @@ export function PreviewPanel({
|
|
|
1282
1323
|
<span className="text-amber-700 font-medium">Pending</span>
|
|
1283
1324
|
)}
|
|
1284
1325
|
</div>
|
|
1285
|
-
|
|
1286
|
-
<
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1326
|
+
{isCartoonEpisode ? (
|
|
1327
|
+
<div className="flex items-center gap-1 rounded border border-border bg-surface/40 p-0.5">
|
|
1328
|
+
<button
|
|
1329
|
+
onClick={() => setActiveTab("preview")}
|
|
1330
|
+
className={`px-2.5 py-0.5 text-[11px] font-medium rounded transition-colors ${
|
|
1331
|
+
activeTab === "preview"
|
|
1332
|
+
? "bg-accent text-white"
|
|
1333
|
+
: "text-muted hover:text-foreground"
|
|
1334
|
+
}`}
|
|
1335
|
+
>
|
|
1336
|
+
Preview
|
|
1337
|
+
</button>
|
|
1338
|
+
<button
|
|
1339
|
+
onClick={() => setActiveTab("edit")}
|
|
1340
|
+
className={`px-2.5 py-0.5 text-[11px] font-medium rounded transition-colors ${
|
|
1341
|
+
activeTab === "edit"
|
|
1342
|
+
? "bg-accent text-white"
|
|
1343
|
+
: "text-muted hover:text-foreground"
|
|
1344
|
+
}`}
|
|
1345
|
+
>
|
|
1346
|
+
Edit
|
|
1347
|
+
{dirty && <span className="ml-1 text-amber-200">*</span>}
|
|
1348
|
+
</button>
|
|
1349
|
+
</div>
|
|
1350
|
+
) : (
|
|
1351
|
+
<div className="flex items-center gap-2">
|
|
1352
|
+
<span
|
|
1353
|
+
className={`text-xs font-mono ${overLimit ? "text-error font-medium" : "text-muted"}`}
|
|
1354
|
+
>
|
|
1355
|
+
{charCount.toLocaleString()}
|
|
1356
|
+
{charLimit !== null
|
|
1357
|
+
? `/${charLimit.toLocaleString()}`
|
|
1358
|
+
: " chars"}
|
|
1297
1359
|
</span>
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
onClick={() => setActiveTab("preview")}
|
|
1306
|
-
className={`px-3 py-1 text-xs font-medium border-b-2 transition-colors ${
|
|
1307
|
-
activeTab === "preview"
|
|
1308
|
-
? "border-accent text-accent"
|
|
1309
|
-
: "border-transparent text-muted hover:text-foreground"
|
|
1310
|
-
}`}
|
|
1311
|
-
>
|
|
1312
|
-
Preview
|
|
1313
|
-
</button>
|
|
1314
|
-
<button
|
|
1315
|
-
onClick={() => setActiveTab("edit")}
|
|
1316
|
-
className={`px-3 py-1 text-xs font-medium border-b-2 transition-colors ${
|
|
1317
|
-
activeTab === "edit"
|
|
1318
|
-
? "border-accent text-accent"
|
|
1319
|
-
: "border-transparent text-muted hover:text-foreground"
|
|
1320
|
-
}`}
|
|
1321
|
-
>
|
|
1322
|
-
Edit
|
|
1323
|
-
{dirty && <span className="ml-1 text-amber-600">*</span>}
|
|
1324
|
-
</button>
|
|
1360
|
+
{overLimit && (
|
|
1361
|
+
<span className="text-error text-xs font-medium">
|
|
1362
|
+
{(charCount - charLimit).toLocaleString()} over limit
|
|
1363
|
+
</span>
|
|
1364
|
+
)}
|
|
1365
|
+
</div>
|
|
1366
|
+
)}
|
|
1325
1367
|
</div>
|
|
1368
|
+
{!isCartoonEpisode && (
|
|
1369
|
+
<div className="flex px-3 gap-1">
|
|
1370
|
+
<button
|
|
1371
|
+
onClick={() => setActiveTab("preview")}
|
|
1372
|
+
className={`px-3 py-1 text-xs font-medium border-b-2 transition-colors ${
|
|
1373
|
+
activeTab === "preview"
|
|
1374
|
+
? "border-accent text-accent"
|
|
1375
|
+
: "border-transparent text-muted hover:text-foreground"
|
|
1376
|
+
}`}
|
|
1377
|
+
>
|
|
1378
|
+
Preview
|
|
1379
|
+
</button>
|
|
1380
|
+
<button
|
|
1381
|
+
onClick={() => setActiveTab("edit")}
|
|
1382
|
+
className={`px-3 py-1 text-xs font-medium border-b-2 transition-colors ${
|
|
1383
|
+
activeTab === "edit"
|
|
1384
|
+
? "border-accent text-accent"
|
|
1385
|
+
: "border-transparent text-muted hover:text-foreground"
|
|
1386
|
+
}`}
|
|
1387
|
+
>
|
|
1388
|
+
Edit
|
|
1389
|
+
{dirty && <span className="ml-1 text-amber-600">*</span>}
|
|
1390
|
+
</button>
|
|
1391
|
+
</div>
|
|
1392
|
+
)}
|
|
1326
1393
|
</div>
|
|
1327
1394
|
)}
|
|
1328
1395
|
|
|
1329
|
-
{/* Persistent cartoon workflow coach (#429): one clear next action across
|
|
1330
|
-
every cartoon file view, derived from real story/episode state. Sits
|
|
1331
|
-
above the content so it stays visible on both the Preview and Edit
|
|
1332
|
-
tabs. Fiction renders nothing (the coach is null), so fiction views are
|
|
1333
|
-
unchanged. */}
|
|
1334
|
-
{!hideFocusedEditorChrome &&
|
|
1335
|
-
contentType === "cartoon" &&
|
|
1336
|
-
storyName &&
|
|
1337
|
-
fileName && (
|
|
1338
|
-
<WorkflowCoach
|
|
1339
|
-
storyName={storyName}
|
|
1340
|
-
fileName={fileName}
|
|
1341
|
-
authFetch={authFetch}
|
|
1342
|
-
refreshKey={cutsRefreshKey}
|
|
1343
|
-
onAction={handleCoachAction}
|
|
1344
|
-
showEmptyState
|
|
1345
|
-
/>
|
|
1346
|
-
)}
|
|
1347
|
-
|
|
1348
1396
|
{/* Content area */}
|
|
1349
1397
|
{activeTab === "preview" ? (
|
|
1350
|
-
|
|
1398
|
+
isCartoonEpisode ? (
|
|
1351
1399
|
<div
|
|
1352
1400
|
className="flex-1 min-h-0 flex flex-col"
|
|
1353
1401
|
style={{ background: "var(--paper-bg)" }}
|
|
1354
1402
|
>
|
|
1355
|
-
{/* Two explicit modes: Publish Preview (exact PlotLink markdown) vs
|
|
1356
|
-
Cut Inspector (cuts.json planning metadata) — see #289. */}
|
|
1357
|
-
<div className="flex gap-1 px-3 py-1 border-b border-border">
|
|
1358
|
-
<button
|
|
1359
|
-
data-testid="cartoon-mode-publish"
|
|
1360
|
-
onClick={() => setCartoonPreviewMode("publish")}
|
|
1361
|
-
className={`px-2 py-0.5 text-[11px] rounded ${cartoonPreviewMode === "publish" ? "bg-accent text-white" : "text-muted hover:text-foreground"}`}
|
|
1362
|
-
>
|
|
1363
|
-
Publish Preview
|
|
1364
|
-
</button>
|
|
1365
|
-
<button
|
|
1366
|
-
data-testid="cartoon-mode-inspect"
|
|
1367
|
-
onClick={() => setCartoonPreviewMode("inspect")}
|
|
1368
|
-
className={`px-2 py-0.5 text-[11px] rounded ${cartoonPreviewMode === "inspect" ? "bg-accent text-white" : "text-muted hover:text-foreground"}`}
|
|
1369
|
-
>
|
|
1370
|
-
Cut Inspector
|
|
1371
|
-
</button>
|
|
1372
|
-
</div>
|
|
1373
1403
|
<div className="flex-1 min-h-0">
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
/>
|
|
1386
|
-
)}
|
|
1404
|
+
<CutListPanel
|
|
1405
|
+
storyName={storyName!}
|
|
1406
|
+
fileName={fileName!}
|
|
1407
|
+
authFetch={authFetch}
|
|
1408
|
+
language={language}
|
|
1409
|
+
mode="preview"
|
|
1410
|
+
onCutsChanged={() => setCutsRefreshKey((k) => k + 1)}
|
|
1411
|
+
focusRequest={cutFocus}
|
|
1412
|
+
onFocusHandled={() => setCutFocus(null)}
|
|
1413
|
+
compactEpisodeChrome
|
|
1414
|
+
/>
|
|
1387
1415
|
</div>
|
|
1388
1416
|
</div>
|
|
1389
1417
|
) : (
|
|
@@ -1405,7 +1433,7 @@ export function PreviewPanel({
|
|
|
1405
1433
|
)}
|
|
1406
1434
|
</div>
|
|
1407
1435
|
)
|
|
1408
|
-
) :
|
|
1436
|
+
) : isCartoonEpisode ? (
|
|
1409
1437
|
<div
|
|
1410
1438
|
className="flex-1 min-h-[22rem] overflow-hidden"
|
|
1411
1439
|
style={{ background: "var(--paper-bg)" }}
|
|
@@ -1415,67 +1443,27 @@ export function PreviewPanel({
|
|
|
1415
1443
|
fileName={fileName!}
|
|
1416
1444
|
authFetch={authFetch}
|
|
1417
1445
|
language={language}
|
|
1446
|
+
mode="edit"
|
|
1418
1447
|
onCutsChanged={() => setCutsRefreshKey((k) => k + 1)}
|
|
1419
1448
|
focusRequest={cutFocus}
|
|
1420
1449
|
onFocusHandled={() => setCutFocus(null)}
|
|
1421
1450
|
onFocusedLetteringModeChange={onFocusedLetteringModeChange}
|
|
1422
1451
|
workspaceVisible={focusedLetteringWorkspaceVisible}
|
|
1423
1452
|
onWorkspaceVisibleChange={onFocusedLetteringWorkspaceVisibleChange}
|
|
1453
|
+
onExitFocusedEditor={() => setActiveTab("preview")}
|
|
1454
|
+
compactEpisodeChrome
|
|
1424
1455
|
/>
|
|
1425
1456
|
</div>
|
|
1426
|
-
) : isCartoonGenesis ? (
|
|
1427
|
-
// Genesis Edit tab: opening-text editor vs. its cut workspace (#429), so
|
|
1428
|
-
// the coach's lettering/upload/refresh actions for Episode 1 are actionable
|
|
1429
|
-
// and Genesis cuts get the same workspace as plots — without losing the
|
|
1430
|
-
// hand-written opening prose editor.
|
|
1431
|
-
<div
|
|
1432
|
-
className="flex-1 min-h-0 flex flex-col"
|
|
1433
|
-
style={{ background: "var(--paper-bg)" }}
|
|
1434
|
-
>
|
|
1435
|
-
<div className="flex gap-1 px-3 py-1 border-b border-border">
|
|
1436
|
-
<button
|
|
1437
|
-
data-testid="genesis-edit-mode-text"
|
|
1438
|
-
onClick={() => setGenesisEditMode("text")}
|
|
1439
|
-
className={`px-2 py-0.5 text-[11px] rounded ${genesisEditMode === "text" ? "bg-accent text-white" : "text-muted hover:text-foreground"}`}
|
|
1440
|
-
>
|
|
1441
|
-
Opening text
|
|
1442
|
-
</button>
|
|
1443
|
-
<button
|
|
1444
|
-
data-testid="genesis-edit-mode-cuts"
|
|
1445
|
-
onClick={() => setGenesisEditMode("cuts")}
|
|
1446
|
-
className={`px-2 py-0.5 text-[11px] rounded ${genesisEditMode === "cuts" ? "bg-accent text-white" : "text-muted hover:text-foreground"}`}
|
|
1447
|
-
>
|
|
1448
|
-
Cuts
|
|
1449
|
-
</button>
|
|
1450
|
-
</div>
|
|
1451
|
-
<div className="flex-1 min-h-0">
|
|
1452
|
-
{genesisEditMode === "cuts" ? (
|
|
1453
|
-
<CutListPanel
|
|
1454
|
-
storyName={storyName!}
|
|
1455
|
-
fileName={fileName!}
|
|
1456
|
-
authFetch={authFetch}
|
|
1457
|
-
language={language}
|
|
1458
|
-
onCutsChanged={() => setCutsRefreshKey((k) => k + 1)}
|
|
1459
|
-
focusRequest={cutFocus}
|
|
1460
|
-
onFocusHandled={() => setCutFocus(null)}
|
|
1461
|
-
onFocusedLetteringModeChange={onFocusedLetteringModeChange}
|
|
1462
|
-
workspaceVisible={focusedLetteringWorkspaceVisible}
|
|
1463
|
-
onWorkspaceVisibleChange={
|
|
1464
|
-
onFocusedLetteringWorkspaceVisibleChange
|
|
1465
|
-
}
|
|
1466
|
-
/>
|
|
1467
|
-
) : (
|
|
1468
|
-
proseEditor
|
|
1469
|
-
)}
|
|
1470
|
-
</div>
|
|
1471
|
-
</div>
|
|
1472
1457
|
) : (
|
|
1473
1458
|
proseEditor
|
|
1474
1459
|
)}
|
|
1475
1460
|
|
|
1476
1461
|
{/* Action bar */}
|
|
1477
|
-
{!hideFocusedEditorChrome && (
|
|
1478
|
-
<div
|
|
1462
|
+
{!hideFocusedEditorChrome && !isCartoonEpisode && (
|
|
1463
|
+
<div
|
|
1464
|
+
className="shrink-0 px-3 py-2 border-t border-border flex items-center justify-between bg-surface/95"
|
|
1465
|
+
data-testid="preview-panel-footer"
|
|
1466
|
+
>
|
|
1479
1467
|
{fileName === "structure.md" ? (
|
|
1480
1468
|
<p
|
|
1481
1469
|
className="text-muted text-xs italic"
|
|
@@ -1944,9 +1932,7 @@ export function PreviewPanel({
|
|
|
1944
1932
|
cut/lettering editor gets the height — the cover stays available
|
|
1945
1933
|
in the Opening-text/Preview view, Story Info, and the Publish page,
|
|
1946
1934
|
and the auto-detect effect still loads it for publish. */}
|
|
1947
|
-
{isGenesis &&
|
|
1948
|
-
contentType !== "cartoon" &&
|
|
1949
|
-
!(activeTab === "edit" && genesisEditMode === "cuts") && (
|
|
1935
|
+
{isGenesis && contentType !== "cartoon" && (
|
|
1950
1936
|
<div
|
|
1951
1937
|
className="flex flex-col gap-1.5"
|
|
1952
1938
|
data-testid="prepublish-cover"
|
|
@@ -2082,7 +2068,7 @@ export function PreviewPanel({
|
|
|
2082
2068
|
</div>
|
|
2083
2069
|
</div>
|
|
2084
2070
|
</div>
|
|
2085
|
-
|
|
2071
|
+
)}
|
|
2086
2072
|
{/* Public title shown + validated before publish (#358). #461: moved
|
|
2087
2073
|
to the Publish tab for cartoon — fiction keeps it inline. */}
|
|
2088
2074
|
{!isCartoonEpisode && renderPublishTitle()}
|