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
|
@@ -90,6 +90,11 @@ interface LetteringEditorProps {
|
|
|
90
90
|
workspaceVisible?: boolean;
|
|
91
91
|
/** Toggle the surrounding app work area while staying in the editor. */
|
|
92
92
|
onToggleWorkspaceVisible?: () => void;
|
|
93
|
+
/** Move to adjacent cuts while staying in the focused editor. */
|
|
94
|
+
onPreviousCut?: () => void;
|
|
95
|
+
onNextCut?: () => void;
|
|
96
|
+
hasPreviousCut?: boolean;
|
|
97
|
+
hasNextCut?: boolean;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
const TYPE_LABEL: Record<OverlayType, string> = {
|
|
@@ -138,6 +143,10 @@ export function LetteringEditor({
|
|
|
138
143
|
returnOnSave = false,
|
|
139
144
|
workspaceVisible = false,
|
|
140
145
|
onToggleWorkspaceVisible,
|
|
146
|
+
onPreviousCut,
|
|
147
|
+
onNextCut,
|
|
148
|
+
hasPreviousCut = false,
|
|
149
|
+
hasNextCut = false,
|
|
141
150
|
}: LetteringEditorProps) {
|
|
142
151
|
const bodyFont = getDefaultFont(language);
|
|
143
152
|
const displayFont = getDisplayFont();
|
|
@@ -224,6 +233,7 @@ export function LetteringEditor({
|
|
|
224
233
|
const [exporting, setExporting] = useState(false);
|
|
225
234
|
const [exportError, setExportError] = useState<string | null>(null);
|
|
226
235
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
236
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
227
237
|
const [imageBounds, setImageBounds] = useState({
|
|
228
238
|
x: 0,
|
|
229
239
|
y: 0,
|
|
@@ -243,6 +253,17 @@ export function LetteringEditor({
|
|
|
243
253
|
origH: number;
|
|
244
254
|
} | null>(null);
|
|
245
255
|
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
const nextOverlays = overlayNormalization.overlays as Overlay[];
|
|
258
|
+
setOverlays(nextOverlays);
|
|
259
|
+
setSelectedId(null);
|
|
260
|
+
setAcknowledgedInvalid(false);
|
|
261
|
+
setConfirmDelete(false);
|
|
262
|
+
setExportError(null);
|
|
263
|
+
setSaveError(null);
|
|
264
|
+
setExportBaselineSig(overlaysSignature(nextOverlays));
|
|
265
|
+
}, [cut.id, overlayNormalization]);
|
|
266
|
+
|
|
246
267
|
const updateImageBounds = useCallback(() => {
|
|
247
268
|
const container = containerRef.current;
|
|
248
269
|
if (!container) return;
|
|
@@ -681,6 +702,23 @@ export function LetteringEditor({
|
|
|
681
702
|
displayFontFamily,
|
|
682
703
|
]);
|
|
683
704
|
const warningCount = Object.keys(overlayWarnings).length;
|
|
705
|
+
const checklistChips: Array<{
|
|
706
|
+
key: string;
|
|
707
|
+
label: string;
|
|
708
|
+
done: boolean;
|
|
709
|
+
}> = [
|
|
710
|
+
{ key: "clean-image", label: "Clean", done: checklist.hasCleanImage },
|
|
711
|
+
{ key: "script-text", label: "Script", done: checklist.hasScriptText },
|
|
712
|
+
{
|
|
713
|
+
key: "bubbles",
|
|
714
|
+
label: checklist.bubblesPlaced
|
|
715
|
+
? `Bubbles ${checklist.bubblesPlaced}`
|
|
716
|
+
: "Bubbles",
|
|
717
|
+
done: checklist.bubblesPlaced > 0,
|
|
718
|
+
},
|
|
719
|
+
{ key: "exported", label: "Exported", done: checklist.exported },
|
|
720
|
+
{ key: "uploaded", label: "Uploaded", done: checklist.uploaded },
|
|
721
|
+
];
|
|
684
722
|
|
|
685
723
|
const isTextPanel = cut.kind === "text";
|
|
686
724
|
const isNarrationCut = !cut.cleanImagePath;
|
|
@@ -708,74 +746,114 @@ export function LetteringEditor({
|
|
|
708
746
|
data-testid="focused-lettering-editor"
|
|
709
747
|
>
|
|
710
748
|
{/* Toolbar */}
|
|
711
|
-
<div
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
749
|
+
<div
|
|
750
|
+
className="px-3 py-1.5 border-b border-border bg-surface/55 grid grid-cols-[minmax(14rem,1fr)_auto_minmax(12rem,1fr)] items-center gap-2"
|
|
751
|
+
data-testid="lettering-toolbar"
|
|
752
|
+
>
|
|
753
|
+
<div className="flex items-center gap-1.5 min-w-0 overflow-hidden">
|
|
754
|
+
<button
|
|
755
|
+
onClick={onClose}
|
|
756
|
+
className="px-2.5 py-1 text-[11px] border border-border rounded text-muted hover:text-foreground"
|
|
757
|
+
data-testid="return-to-cut-review-btn"
|
|
758
|
+
>
|
|
759
|
+
Cut review
|
|
760
|
+
</button>
|
|
761
|
+
<span className="rounded-full border border-accent/30 bg-accent/10 px-2 py-0.5 text-[10px] font-bold uppercase tracking-[0.14em] text-accent whitespace-nowrap">
|
|
762
|
+
Lettering
|
|
763
|
+
</span>
|
|
764
|
+
<span className="text-[11px] font-mono text-muted">
|
|
765
|
+
{targetLabel ?? `Cut #${cut.id}`}
|
|
766
|
+
</span>
|
|
767
|
+
<span
|
|
768
|
+
className="rounded-full border border-border bg-background px-2 py-0.5 text-[10px] text-muted"
|
|
769
|
+
data-testid="overlay-count"
|
|
770
|
+
>
|
|
726
771
|
{overlays.length} overlays
|
|
727
772
|
</span>
|
|
773
|
+
{checklistChips.map((chip) => (
|
|
774
|
+
<span
|
|
775
|
+
key={chip.key}
|
|
776
|
+
data-testid={`lettering-check-${chip.key}`}
|
|
777
|
+
data-done={chip.done ? "true" : "false"}
|
|
778
|
+
className={`rounded-full border px-2 py-0.5 text-[10px] ${
|
|
779
|
+
chip.key === "exported" || chip.key === "uploaded"
|
|
780
|
+
? "hidden xl:inline-flex"
|
|
781
|
+
: ""
|
|
782
|
+
} ${
|
|
783
|
+
chip.done
|
|
784
|
+
? "border-green-700/30 bg-green-700/10 text-green-700"
|
|
785
|
+
: "border-border bg-background text-muted"
|
|
786
|
+
}`}
|
|
787
|
+
>
|
|
788
|
+
{chip.done ? "✓ " : "○ "}
|
|
789
|
+
{chip.label}
|
|
790
|
+
</span>
|
|
791
|
+
))}
|
|
792
|
+
<span className="sr-only">Focused lettering editor</span>
|
|
793
|
+
{cut.aiDraft?.status === "generated" && (
|
|
794
|
+
<span
|
|
795
|
+
className="rounded-full border border-accent/30 bg-accent/10 px-2 py-0.5 text-[10px] text-accent"
|
|
796
|
+
data-testid="ai-draft-current-target"
|
|
797
|
+
>
|
|
798
|
+
AI draft ready
|
|
799
|
+
</span>
|
|
800
|
+
)}
|
|
728
801
|
</div>
|
|
729
|
-
<div className="flex items-center gap-
|
|
802
|
+
<div className="flex items-center justify-center gap-1 rounded border border-border bg-background px-1 py-0.5">
|
|
730
803
|
<button
|
|
731
|
-
onClick={
|
|
732
|
-
className="px-
|
|
733
|
-
data-testid="
|
|
804
|
+
onClick={() => addOverlay("speech")}
|
|
805
|
+
className="px-2.5 py-1 text-[11px] rounded hover:bg-accent/10 hover:text-accent"
|
|
806
|
+
data-testid="add-speech"
|
|
807
|
+
>
|
|
808
|
+
Speech
|
|
809
|
+
</button>
|
|
810
|
+
<button
|
|
811
|
+
onClick={() => addOverlay("narration")}
|
|
812
|
+
className="px-2.5 py-1 text-[11px] rounded hover:bg-accent/10 hover:text-accent"
|
|
813
|
+
data-testid="add-narration"
|
|
814
|
+
>
|
|
815
|
+
Narration
|
|
816
|
+
</button>
|
|
817
|
+
<button
|
|
818
|
+
onClick={() => addOverlay("sfx")}
|
|
819
|
+
className="px-2.5 py-1 text-[11px] rounded hover:bg-accent/10 hover:text-accent"
|
|
820
|
+
data-testid="add-sfx"
|
|
734
821
|
>
|
|
735
|
-
|
|
822
|
+
SFX
|
|
736
823
|
</button>
|
|
824
|
+
</div>
|
|
825
|
+
<div className="flex items-center gap-1.5 justify-end min-w-0">
|
|
737
826
|
{onToggleWorkspaceVisible && (
|
|
738
827
|
<button
|
|
739
828
|
onClick={onToggleWorkspaceVisible}
|
|
740
|
-
className="px-
|
|
829
|
+
className="px-2.5 py-1 text-[11px] border border-border rounded text-muted hover:border-accent hover:text-accent"
|
|
741
830
|
data-testid="toggle-work-area-btn"
|
|
742
831
|
>
|
|
743
832
|
{workspaceVisible ? "Hide work area" : "Show work area"}
|
|
744
833
|
</button>
|
|
745
834
|
)}
|
|
746
|
-
<
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
<button
|
|
755
|
-
onClick={() => addOverlay("narration")}
|
|
756
|
-
className="px-2 py-0.5 text-[10px] border border-border rounded hover:border-accent hover:bg-accent/5"
|
|
757
|
-
data-testid="add-narration"
|
|
758
|
-
>
|
|
759
|
-
Narration
|
|
760
|
-
</button>
|
|
761
|
-
<button
|
|
762
|
-
onClick={() => addOverlay("sfx")}
|
|
763
|
-
className="px-2 py-0.5 text-[10px] border border-border rounded hover:border-accent hover:bg-accent/5"
|
|
764
|
-
data-testid="add-sfx"
|
|
765
|
-
>
|
|
766
|
-
SFX
|
|
767
|
-
</button>
|
|
768
|
-
</div>
|
|
835
|
+
<button
|
|
836
|
+
type="button"
|
|
837
|
+
onClick={() => setShowHelp((prev) => !prev)}
|
|
838
|
+
className="px-2.5 py-1 text-[11px] border border-border rounded text-muted hover:border-accent hover:text-accent"
|
|
839
|
+
data-testid="lettering-help-toggle"
|
|
840
|
+
>
|
|
841
|
+
{showHelp ? "Hide help" : "Help"}
|
|
842
|
+
</button>
|
|
769
843
|
{exportError && (
|
|
770
|
-
<span className="text-[10px] text-error">
|
|
844
|
+
<span className="text-[10px] text-error max-w-[18rem]">
|
|
845
|
+
{exportError}
|
|
846
|
+
</span>
|
|
771
847
|
)}
|
|
772
848
|
{saveError && (
|
|
773
|
-
<span className="text-[10px] text-error">
|
|
849
|
+
<span className="text-[10px] text-error max-w-[18rem]">
|
|
850
|
+
{saveError}
|
|
851
|
+
</span>
|
|
774
852
|
)}
|
|
775
853
|
<button
|
|
776
854
|
onClick={handleExport}
|
|
777
855
|
disabled={exporting}
|
|
778
|
-
className="px-
|
|
856
|
+
className="px-2.5 py-1 text-[11px] border border-accent text-accent rounded hover:bg-accent/5 disabled:opacity-50"
|
|
779
857
|
data-testid="export-btn"
|
|
780
858
|
>
|
|
781
859
|
{exporting ? "Exporting..." : "Export"}
|
|
@@ -784,14 +862,14 @@ export function LetteringEditor({
|
|
|
784
862
|
onClick={() => {
|
|
785
863
|
void handleSave();
|
|
786
864
|
}}
|
|
787
|
-
className="px-
|
|
865
|
+
className="px-2.5 py-1 text-[11px] bg-accent text-white rounded hover:bg-accent-dim"
|
|
788
866
|
data-testid="save-lettering-btn"
|
|
789
867
|
>
|
|
790
868
|
Save
|
|
791
869
|
</button>
|
|
792
870
|
<button
|
|
793
871
|
onClick={onClose}
|
|
794
|
-
className="px-
|
|
872
|
+
className="px-2.5 py-1 text-[11px] text-muted hover:text-foreground border border-border rounded"
|
|
795
873
|
data-testid="cancel-lettering-btn"
|
|
796
874
|
>
|
|
797
875
|
Cancel
|
|
@@ -799,6 +877,17 @@ export function LetteringEditor({
|
|
|
799
877
|
</div>
|
|
800
878
|
</div>
|
|
801
879
|
|
|
880
|
+
{showHelp && (
|
|
881
|
+
<div
|
|
882
|
+
className="px-3 py-1.5 border-b border-border bg-background text-[10px] text-muted"
|
|
883
|
+
data-testid="lettering-help-panel"
|
|
884
|
+
>
|
|
885
|
+
Add or select a bubble, edit it in the inspector, then Save to return
|
|
886
|
+
to cut review. Use Export after the overlay layout is ready. Text cards
|
|
887
|
+
use narration overlays on the canvas.
|
|
888
|
+
</div>
|
|
889
|
+
)}
|
|
890
|
+
|
|
802
891
|
{invalidOverlayCount > 0 && !acknowledgedInvalid ? (
|
|
803
892
|
<div
|
|
804
893
|
className="px-3 py-1 border-b border-border bg-error/10 text-[10px] text-error flex items-center gap-2 flex-wrap"
|
|
@@ -856,40 +945,9 @@ export function LetteringEditor({
|
|
|
856
945
|
</div>
|
|
857
946
|
)}
|
|
858
947
|
|
|
859
|
-
{/* Per-cut lettering checklist (#336): shows how far this cut has come so
|
|
860
|
-
the writer can finish it from the editor without inspecting cuts.json. */}
|
|
861
|
-
<div
|
|
862
|
-
className="px-3 py-1 border-b border-border flex items-center gap-3 flex-wrap text-[10px] text-muted"
|
|
863
|
-
data-testid="lettering-checklist"
|
|
864
|
-
>
|
|
865
|
-
{(
|
|
866
|
-
[
|
|
867
|
-
["clean-image", "Clean image", checklist.hasCleanImage],
|
|
868
|
-
["script-text", "Script text", checklist.hasScriptText],
|
|
869
|
-
[
|
|
870
|
-
"bubbles",
|
|
871
|
-
`Bubbles placed${checklist.bubblesPlaced ? ` (${checklist.bubblesPlaced})` : ""}`,
|
|
872
|
-
checklist.bubblesPlaced > 0,
|
|
873
|
-
],
|
|
874
|
-
["exported", "Final exported", checklist.exported],
|
|
875
|
-
["uploaded", "Uploaded", checklist.uploaded],
|
|
876
|
-
] as [string, string, boolean][]
|
|
877
|
-
).map(([key, label, done]) => (
|
|
878
|
-
<span
|
|
879
|
-
key={key}
|
|
880
|
-
data-testid={`lettering-check-${key}`}
|
|
881
|
-
data-done={done ? "true" : "false"}
|
|
882
|
-
className={`flex items-center gap-1 ${done ? "text-green-700" : "text-muted/70"}`}
|
|
883
|
-
>
|
|
884
|
-
<span aria-hidden>{done ? "✓" : "○"}</span>
|
|
885
|
-
{label}
|
|
886
|
-
</span>
|
|
887
|
-
))}
|
|
888
|
-
</div>
|
|
889
|
-
|
|
890
948
|
{/* Stale-export warning (#336, re1): bubbles changed since the recorded
|
|
891
949
|
export/upload, so the final image/uploaded URL are out of date. The
|
|
892
|
-
|
|
950
|
+
compact toolbar chips already mark export/upload incomplete; this says why. */}
|
|
893
951
|
{staleExport && (
|
|
894
952
|
<div
|
|
895
953
|
className="px-3 py-1 border-b border-border bg-amber-500/10 text-[10px] text-amber-700"
|
|
@@ -995,6 +1053,33 @@ export function LetteringEditor({
|
|
|
995
1053
|
</div>
|
|
996
1054
|
)}
|
|
997
1055
|
|
|
1056
|
+
{(onPreviousCut || onNextCut) && (
|
|
1057
|
+
<>
|
|
1058
|
+
<button
|
|
1059
|
+
type="button"
|
|
1060
|
+
onClick={onPreviousCut}
|
|
1061
|
+
disabled={!hasPreviousCut}
|
|
1062
|
+
className="absolute left-3 top-1/2 z-20 flex h-12 w-8 -translate-y-1/2 items-center justify-center rounded border border-border bg-background/85 text-2xl text-accent shadow-sm hover:bg-background disabled:opacity-30 disabled:hover:bg-background/85"
|
|
1063
|
+
data-testid="previous-cut-btn"
|
|
1064
|
+
aria-label="Previous cut"
|
|
1065
|
+
>
|
|
1066
|
+
<span aria-hidden>‹</span>
|
|
1067
|
+
<span className="sr-only">Previous cut</span>
|
|
1068
|
+
</button>
|
|
1069
|
+
<button
|
|
1070
|
+
type="button"
|
|
1071
|
+
onClick={onNextCut}
|
|
1072
|
+
disabled={!hasNextCut}
|
|
1073
|
+
className="absolute right-3 top-1/2 z-20 flex h-12 w-8 -translate-y-1/2 items-center justify-center rounded border border-border bg-background/85 text-2xl text-accent shadow-sm hover:bg-background disabled:opacity-30 disabled:hover:bg-background/85"
|
|
1074
|
+
data-testid="next-cut-btn"
|
|
1075
|
+
aria-label="Next cut"
|
|
1076
|
+
>
|
|
1077
|
+
<span aria-hidden>›</span>
|
|
1078
|
+
<span className="sr-only">Next cut</span>
|
|
1079
|
+
</button>
|
|
1080
|
+
</>
|
|
1081
|
+
)}
|
|
1082
|
+
|
|
998
1083
|
{/* Speech balloons, drawn under the overlay boxes (which carry the
|
|
999
1084
|
text + drag/resize handles) so the box sits on top of the fill.
|
|
1000
1085
|
Body + tail are ONE integrated <path> per bubble (#327), mirroring
|
|
@@ -1175,55 +1260,16 @@ export function LetteringEditor({
|
|
|
1175
1260
|
|
|
1176
1261
|
{/* Inspector panel */}
|
|
1177
1262
|
<div className="w-64 border-l border-border p-3 overflow-y-auto flex-shrink-0">
|
|
1178
|
-
{cut.aiDraft?.status === "generated" && (
|
|
1179
|
-
<div
|
|
1180
|
-
className="mb-3 rounded border border-accent/30 bg-accent/5 p-2 space-y-1"
|
|
1181
|
-
data-testid="ai-draft-current-target"
|
|
1182
|
-
>
|
|
1183
|
-
<p className="text-[10px] font-bold uppercase tracking-[0.14em] text-accent">
|
|
1184
|
-
AI draft ready
|
|
1185
|
-
</p>
|
|
1186
|
-
<p className="text-[11px] text-muted">
|
|
1187
|
-
These first-pass overlays came from the cut script. Review and
|
|
1188
|
-
tune them here before exporting the final panel.
|
|
1189
|
-
</p>
|
|
1190
|
-
</div>
|
|
1191
|
-
)}
|
|
1192
|
-
{/* Insert-from-script (#336): drop the cut's planned dialogue/narration/
|
|
1193
|
-
SFX straight into a prefilled overlay — no copy/paste out of JSON. */}
|
|
1194
|
-
{scriptLines.length > 0 && (
|
|
1195
|
-
<div className="mb-3 space-y-1.5" data-testid="script-insert-panel">
|
|
1196
|
-
<span className="text-[10px] font-medium text-muted">
|
|
1197
|
-
From script
|
|
1198
|
-
</span>
|
|
1199
|
-
<div className="flex flex-col gap-1">
|
|
1200
|
-
{scriptLines.map((line) => (
|
|
1201
|
-
<button
|
|
1202
|
-
key={line.key}
|
|
1203
|
-
onClick={() => addScriptLine(line)}
|
|
1204
|
-
data-testid={`script-insert-${line.key}`}
|
|
1205
|
-
title={`Add ${line.type} overlay with this text`}
|
|
1206
|
-
className="text-left px-2 py-1 text-[10px] border border-border rounded hover:border-accent hover:bg-accent/5"
|
|
1207
|
-
>
|
|
1208
|
-
<span className="font-medium text-accent">
|
|
1209
|
-
+ {TYPE_LABEL[line.type]}
|
|
1210
|
-
</span>{" "}
|
|
1211
|
-
<span className="text-muted">
|
|
1212
|
-
{line.speaker ? `${line.speaker}: ` : ""}
|
|
1213
|
-
{line.text.length > 32
|
|
1214
|
-
? `${line.text.slice(0, 32)}…`
|
|
1215
|
-
: line.text}
|
|
1216
|
-
</span>
|
|
1217
|
-
</button>
|
|
1218
|
-
))}
|
|
1219
|
-
</div>
|
|
1220
|
-
</div>
|
|
1221
|
-
)}
|
|
1222
1263
|
{selectedOverlay ? (
|
|
1223
1264
|
<div className="space-y-3">
|
|
1224
|
-
<
|
|
1225
|
-
|
|
1226
|
-
|
|
1265
|
+
<div className="flex items-center justify-between gap-2">
|
|
1266
|
+
<p className="text-xs font-medium text-foreground">
|
|
1267
|
+
{TYPE_LABEL[selectedOverlay.type]}
|
|
1268
|
+
</p>
|
|
1269
|
+
<span className="text-[10px] text-muted">
|
|
1270
|
+
#{overlays.findIndex((o) => o.id === selectedOverlay.id) + 1}
|
|
1271
|
+
</span>
|
|
1272
|
+
</div>
|
|
1227
1273
|
|
|
1228
1274
|
{selectedOverlay.speaker !== undefined && (
|
|
1229
1275
|
<label className="block space-y-1">
|
|
@@ -1629,11 +1675,80 @@ export function LetteringEditor({
|
|
|
1629
1675
|
>
|
|
1630
1676
|
{confirmDelete ? "Click again to delete" : "Delete"}
|
|
1631
1677
|
</button>
|
|
1678
|
+
|
|
1679
|
+
{scriptLines.length > 0 && (
|
|
1680
|
+
<div
|
|
1681
|
+
className="space-y-1.5 border-t border-border pt-3"
|
|
1682
|
+
data-testid="script-insert-panel"
|
|
1683
|
+
>
|
|
1684
|
+
<span className="text-[10px] font-medium text-muted">
|
|
1685
|
+
Add from script
|
|
1686
|
+
</span>
|
|
1687
|
+
<div className="flex flex-col gap-1">
|
|
1688
|
+
{scriptLines.map((line) => (
|
|
1689
|
+
<button
|
|
1690
|
+
key={line.key}
|
|
1691
|
+
onClick={() => addScriptLine(line)}
|
|
1692
|
+
data-testid={`script-insert-${line.key}`}
|
|
1693
|
+
title={`Add ${line.type} overlay with this text`}
|
|
1694
|
+
className="text-left px-2 py-1 text-[10px] border border-border rounded hover:border-accent hover:bg-accent/5"
|
|
1695
|
+
>
|
|
1696
|
+
<span className="font-medium text-accent">
|
|
1697
|
+
+ {TYPE_LABEL[line.type]}
|
|
1698
|
+
</span>{" "}
|
|
1699
|
+
<span className="text-muted">
|
|
1700
|
+
{line.speaker ? `${line.speaker}: ` : ""}
|
|
1701
|
+
{line.text.length > 32
|
|
1702
|
+
? `${line.text.slice(0, 32)}…`
|
|
1703
|
+
: line.text}
|
|
1704
|
+
</span>
|
|
1705
|
+
</button>
|
|
1706
|
+
))}
|
|
1707
|
+
</div>
|
|
1708
|
+
</div>
|
|
1709
|
+
)}
|
|
1632
1710
|
</div>
|
|
1633
1711
|
) : (
|
|
1634
|
-
<
|
|
1635
|
-
|
|
1636
|
-
|
|
1712
|
+
<div className="space-y-3">
|
|
1713
|
+
<p className="text-xs text-muted" data-testid="inspector-empty">
|
|
1714
|
+
Select or add an overlay to inspect it.
|
|
1715
|
+
</p>
|
|
1716
|
+
{cut.aiDraft?.status === "generated" && (
|
|
1717
|
+
<div className="rounded border border-accent/30 bg-accent/5 p-2 text-[10px] text-muted">
|
|
1718
|
+
AI drafted overlays are editable here before export.
|
|
1719
|
+
</div>
|
|
1720
|
+
)}
|
|
1721
|
+
{/* Insert-from-script (#336): drop the cut's planned dialogue/narration/
|
|
1722
|
+
SFX straight into a prefilled overlay — no copy/paste out of JSON. */}
|
|
1723
|
+
{scriptLines.length > 0 && (
|
|
1724
|
+
<div className="space-y-1.5" data-testid="script-insert-panel">
|
|
1725
|
+
<span className="text-[10px] font-medium text-muted">
|
|
1726
|
+
Add from script
|
|
1727
|
+
</span>
|
|
1728
|
+
<div className="flex flex-col gap-1">
|
|
1729
|
+
{scriptLines.map((line) => (
|
|
1730
|
+
<button
|
|
1731
|
+
key={line.key}
|
|
1732
|
+
onClick={() => addScriptLine(line)}
|
|
1733
|
+
data-testid={`script-insert-${line.key}`}
|
|
1734
|
+
title={`Add ${line.type} overlay with this text`}
|
|
1735
|
+
className="text-left px-2 py-1 text-[10px] border border-border rounded hover:border-accent hover:bg-accent/5"
|
|
1736
|
+
>
|
|
1737
|
+
<span className="font-medium text-accent">
|
|
1738
|
+
+ {TYPE_LABEL[line.type]}
|
|
1739
|
+
</span>{" "}
|
|
1740
|
+
<span className="text-muted">
|
|
1741
|
+
{line.speaker ? `${line.speaker}: ` : ""}
|
|
1742
|
+
{line.text.length > 32
|
|
1743
|
+
? `${line.text.slice(0, 32)}…`
|
|
1744
|
+
: line.text}
|
|
1745
|
+
</span>
|
|
1746
|
+
</button>
|
|
1747
|
+
))}
|
|
1748
|
+
</div>
|
|
1749
|
+
</div>
|
|
1750
|
+
)}
|
|
1751
|
+
</div>
|
|
1637
1752
|
)}
|
|
1638
1753
|
</div>
|
|
1639
1754
|
</div>
|