plotlink-ows 1.2.97 → 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/web/components/CartoonWorkflowNav.tsx +3 -7
- package/app/web/components/CutListPanel.tsx +252 -177
- package/app/web/components/CutOverlayPreview.tsx +9 -1
- package/app/web/components/EpisodesPage.tsx +10 -4
- package/app/web/components/LetteringEditor.tsx +81 -37
- package/app/web/components/PreviewPanel.tsx +145 -154
- package/app/web/components/StoriesPage.tsx +10 -17
- package/app/web/components/StoryBrowser.tsx +53 -19
- package/app/web/components/StoryProgressPanel.tsx +12 -5
- package/app/web/dist/assets/{export-cut-Cj-cOtan.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-CCeEYE7p.css +0 -32
- package/app/web/dist/assets/index-CF7pE09m.js +0 -141
|
@@ -124,6 +124,22 @@ export function StoryBrowser({ authFetch, selectedStory, selectedFile, onSelectF
|
|
|
124
124
|
return files[0]?.file ?? null;
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
+
const isEpisodeFile = (file: string) =>
|
|
128
|
+
file === "genesis.md" || /^plot-\d+\.md$/.test(file);
|
|
129
|
+
|
|
130
|
+
const cartoonEpisodeMeta = (file: string): { label: string; sort: number } | null => {
|
|
131
|
+
if (file === "genesis.md") {
|
|
132
|
+
return { label: "epi-01 (Genesis)", sort: 1 };
|
|
133
|
+
}
|
|
134
|
+
const m = file.match(/^plot-(\d+)\.md$/);
|
|
135
|
+
if (!m) return null;
|
|
136
|
+
const n = parseInt(m[1], 10) + 1;
|
|
137
|
+
return {
|
|
138
|
+
label: `epi-${String(n).padStart(2, "0")}`,
|
|
139
|
+
sort: n,
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
127
143
|
const handleStoryClick = (story: StoryInfo) => {
|
|
128
144
|
toggleExpand(story.name);
|
|
129
145
|
// Cartoon: a root-row click opens the story-level progress overview (#418) on
|
|
@@ -139,8 +155,18 @@ export function StoryBrowser({ authFetch, selectedStory, selectedFile, onSelectF
|
|
|
139
155
|
}
|
|
140
156
|
};
|
|
141
157
|
|
|
142
|
-
// Sort files: structure first, genesis, then plots in order
|
|
143
|
-
|
|
158
|
+
// Sort files: structure first, genesis, then plots in order. Cartoon stories
|
|
159
|
+
// show only episode files to avoid leaking the markdown implementation model.
|
|
160
|
+
const sortFiles = (files: FileStatus[], contentType?: "fiction" | "cartoon") => {
|
|
161
|
+
if (contentType === "cartoon") {
|
|
162
|
+
return [...files]
|
|
163
|
+
.filter((f) => isEpisodeFile(f.file))
|
|
164
|
+
.sort((a, b) => {
|
|
165
|
+
const aa = cartoonEpisodeMeta(a.file)?.sort ?? 999;
|
|
166
|
+
const bb = cartoonEpisodeMeta(b.file)?.sort ?? 999;
|
|
167
|
+
return aa - bb;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
144
170
|
const order = (f: string) => {
|
|
145
171
|
if (f === "structure.md") return 0;
|
|
146
172
|
if (f === "genesis.md") return 1;
|
|
@@ -238,27 +264,35 @@ export function StoryBrowser({ authFetch, selectedStory, selectedFile, onSelectF
|
|
|
238
264
|
{story.contentType === "cartoon" && (
|
|
239
265
|
<span className="bg-accent/10 text-accent rounded px-1.5 py-0.5 text-[10px] font-medium flex-shrink-0">Cartoon</span>
|
|
240
266
|
)}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
267
|
+
<span className="ml-auto flex-shrink-0 text-xs text-muted">
|
|
268
|
+
{story.contentType === "cartoon"
|
|
269
|
+
? `${sortFiles(story.files, "cartoon").length} ep`
|
|
270
|
+
: `${story.publishedCount}/${story.files.length}`}
|
|
271
|
+
</span>
|
|
272
|
+
</button>
|
|
273
|
+
{expanded.has(story.name) && (
|
|
274
|
+
<div className="pl-4">
|
|
275
|
+
{sortFiles(story.files, story.contentType).map((f) => {
|
|
276
|
+
const isSelected = selectedStory === story.name && selectedFile === f.file;
|
|
277
|
+
const cartoonMeta =
|
|
278
|
+
story.contentType === "cartoon"
|
|
279
|
+
? cartoonEpisodeMeta(f.file)
|
|
280
|
+
: null;
|
|
281
|
+
return (
|
|
282
|
+
<button
|
|
283
|
+
key={f.file}
|
|
252
284
|
onClick={() => onSelectFile(story.name, f.file)}
|
|
253
285
|
className={`w-full px-3 py-1.5 text-left flex items-center gap-2 text-xs hover:bg-surface ${
|
|
254
286
|
isSelected ? "bg-surface font-medium" : ""
|
|
255
287
|
}`}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
288
|
+
>
|
|
289
|
+
<span className={STATUS_COLOR[f.status]}>{STATUS_ICON[f.status]}</span>
|
|
290
|
+
<span className={story.contentType === "cartoon" ? "truncate" : "truncate font-mono"}>
|
|
291
|
+
{cartoonMeta?.label ?? f.file}
|
|
292
|
+
</span>
|
|
293
|
+
</button>
|
|
294
|
+
);
|
|
295
|
+
})}
|
|
262
296
|
</div>
|
|
263
297
|
)}
|
|
264
298
|
</div>
|
|
@@ -137,7 +137,7 @@ function Section({
|
|
|
137
137
|
title: string;
|
|
138
138
|
status: SectionStatus;
|
|
139
139
|
items: ChecklistItem[];
|
|
140
|
-
/**
|
|
140
|
+
/** Secondary text, shown small. */
|
|
141
141
|
fileName?: string | null;
|
|
142
142
|
/** Called to open the section's underlying file, or undefined for no navigation. */
|
|
143
143
|
openFile?: () => void;
|
|
@@ -171,6 +171,14 @@ function episodeStatus(ep: EpisodeProgress, isActive: boolean): SectionStatus {
|
|
|
171
171
|
return "needs-action";
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
function episodeDisplayLabel(ep: EpisodeProgress): string {
|
|
175
|
+
if (ep.file === "genesis.md") return "epi-01 (Genesis)";
|
|
176
|
+
const m = ep.file.match(/^plot-(\d+)\.md$/);
|
|
177
|
+
if (!m) return ep.label;
|
|
178
|
+
const episodeNumber = parseInt(m[1], 10) + 1;
|
|
179
|
+
return `epi-${String(episodeNumber).padStart(2, "0")}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
174
182
|
/** Build the rendered checklist for a cartoon episode from its production checklist. */
|
|
175
183
|
function episodeItems(ep: EpisodeProgress, openingDone = true): ChecklistItem[] {
|
|
176
184
|
const steps = ep.checklist ?? [];
|
|
@@ -243,9 +251,8 @@ function CartoonWorkflowMap({
|
|
|
243
251
|
|
|
244
252
|
<Section
|
|
245
253
|
index={++idx}
|
|
246
|
-
title="Story
|
|
254
|
+
title="Story Bible"
|
|
247
255
|
status={whitepaperStatus}
|
|
248
|
-
fileName="structure.md"
|
|
249
256
|
openFile={hasStructure ? () => onOpenFile(storyName, "structure.md") : undefined}
|
|
250
257
|
items={[{ label: "Planning document", status: hasStructure ? "done" : "todo", detail: hasStructure ? null : "Not written yet" }]}
|
|
251
258
|
/>
|
|
@@ -302,12 +309,12 @@ function EpisodeSection({
|
|
|
302
309
|
markdownReady: ep.state === "ready" || ep.published,
|
|
303
310
|
published: ep.published,
|
|
304
311
|
});
|
|
305
|
-
const
|
|
312
|
+
const label = episodeDisplayLabel(ep);
|
|
313
|
+
const title = ep.title ? `${label} · ${ep.title}` : label;
|
|
306
314
|
const heading = (
|
|
307
315
|
<div className="flex items-center gap-2 min-w-0">
|
|
308
316
|
<span className={`flex-shrink-0 ${SECTION_TONE[status]}`} aria-hidden>{SECTION_ICON[status]}</span>
|
|
309
317
|
<span className="text-xs font-medium text-foreground truncate">{index}. {title}</span>
|
|
310
|
-
<span className="text-[10px] text-muted truncate">{ep.file}</span>
|
|
311
318
|
<span className={`ml-auto text-[10px] font-medium ${SECTION_TONE[status]} flex-shrink-0`}>{SECTION_LABEL[status]}</span>
|
|
312
319
|
</div>
|
|
313
320
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{M as w,v as O,t as A,c as M,b as R,s as H,l as I,a as B,d as F}from"./index-
|
|
1
|
+
import{M as w,v as O,t as A,c as M,b as R,s as H,l as I,a as B,d as F}from"./index-CoG6WKyb.js";const z=w;async function G(e){if(typeof document>"u"||!document.fonts||typeof document.fonts.load!="function")return{ready:!0,missing:[]};const n=[];for(const s of e)try{const t=await document.fonts.load(`16px "${s}"`);!t||t.length===0?n.push(s):document.fonts.check(`16px "${s}"`)||n.push(s)}catch{n.push(s)}return{ready:n.length===0,missing:n}}function C(e){return new Promise((n,s)=>{const t=new Image;t.crossOrigin="anonymous",t.onload=()=>n(t),t.onerror=()=>s(new Error("Failed to load image")),t.src=e})}const W="rgba(255, 255, 255, 0.95)",_="#1a1a1a",L="rgba(244, 239, 230, 0.94)",N="rgba(26, 26, 26, 0.55)";function Y(e){return Math.max(2,e*.004)}function K(e,n,s,t,c,d,f){e.beginPath();for(const o of F(n,s,t,c,d,f))o.k==="M"?e.moveTo(o.x,o.y):o.k==="L"?e.lineTo(o.x,o.y):e.arcTo(o.cornerX,o.cornerY,o.x,o.y,o.r);e.closePath()}function P(e,n,s,t,c,d){var f;for(const o of n){const l=o.x*s,i=o.y*t,r=o.width*s,a=o.height*t,y=Y(t);if(o.type==="speech"){const u=R(o,r,a),k=o.tailAnchor?H(l,i,r,a,o.tailAnchor,u):null;K(e,l,i,r,a,k,u),e.fillStyle=W,e.fill(),e.strokeStyle=_,e.lineWidth=y,e.lineJoin="round",e.stroke()}else if(o.type==="narration"){const u=Math.min(r,a)*.12;e.beginPath(),e.roundRect(l,i,r,a,u),e.fillStyle=L,e.fill(),e.strokeStyle=N,e.lineWidth=Math.max(1.5,y*.75),e.lineJoin="round",e.stroke()}const g=o.type==="sfx"?d:c,T=o.type!=="sfx"&&!!o.speaker,h=I((u,k,E=400)=>(e.font=`${E} ${k}px ${g}`,e.measureText(u).width),o.text,r,a,B(o,t,r,a));e.textAlign="center",e.textBaseline="middle";const p=l+r/2,S=T?h.speakerFontSize*1.2:0;T&&(e.fillStyle="#3a3a3a",e.font=`700 ${h.speakerFontSize}px ${g}`,e.fillText(o.speaker,p,i+S/2+a*.04,r-6));const v=i+S,b=a-S,$=h.lines.length*h.lineHeight;let m=v+b/2-$/2+h.lineHeight/2;e.font=`${((f=o.textStyle)==null?void 0:f.fontWeight)??400} ${h.fontSize}px ${g}`;for(const u of h.lines)o.type==="sfx"?(e.fillStyle="#000",e.strokeStyle="#fff",e.lineWidth=3,e.strokeText(u,p,m),e.fillText(u,p,m)):(e.fillStyle="#1a1a1a",e.fillText(u,p,m)),m+=h.lineHeight}}function X(e,n,s,t,c){const d=Math.max(14,Math.min(t*.05,28));e.font=`${d}px ${c}`,e.fillStyle="#1a1a1a",e.textAlign="center",e.textBaseline="middle";const f=[];if(n.dialogue)for(const i of n.dialogue)f.push(`${i.speaker}: ${i.text}`);n.narration&&f.push(n.narration);const o=d*1.6,l=t/2-(f.length-1)*o/2;for(let i=0;i<f.length;i++)e.fillText(f[i],s/2,l+i*o,s-40)}async function Z(e,n,s,t,c,d){const f=O(n);if(!f.valid)throw new Error(f.error??"Overlay geometry is invalid");let o=800,l=600,i=null;if(e)i=await C(e),o=i.naturalWidth,l=i.naturalHeight;else if(d){const y=A(d.aspectRatio);y&&(o=y.width,l=y.height)}const r=document.createElement("canvas");r.width=o,r.height=l;const a=r.getContext("2d");return i?a.drawImage(i,0,0,o,l):(a.fillStyle=(d==null?void 0:d.background)||"#ffffff",a.fillRect(0,0,o,l)),P(a,n,o,l,s,t),c&&n.length===0&&!i&&X(a,c,o,l,s),M(r)}function j(e){return e.size>z?{valid:!1,error:`Image is ${(e.size/1024).toFixed(0)}KB, exceeds 1MB limit`}:{valid:!0}}export{z as MAX_SIZE,G as ensureFontsReady,Z as exportCut,P as renderOverlays,A as textPanelDimensions,j as validateExportSize};
|