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.
@@ -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
- const sortFiles = (files: FileStatus[]) => {
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
- <span className="ml-auto flex-shrink-0 text-xs text-muted">
242
- {story.publishedCount}/{story.files.length}
243
- </span>
244
- </button>
245
- {expanded.has(story.name) && (
246
- <div className="pl-4">
247
- {sortFiles(story.files).map((f) => {
248
- const isSelected = selectedStory === story.name && selectedFile === f.file;
249
- return (
250
- <button
251
- key={f.file}
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
- <span className={STATUS_COLOR[f.status]}>{STATUS_ICON[f.status]}</span>
258
- <span className="truncate font-mono">{f.file}</span>
259
- </button>
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
- /** Power-user secondary text (real file name), shown small. */
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 Whitepaper"
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 title = ep.title ? `${ep.label} · ${ep.title}` : ep.label;
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-CF7pE09m.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};
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};