konpeki 0.3.0 → 0.4.0

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.
Files changed (162) hide show
  1. package/AGENTS.md +21 -28
  2. package/AUTHORING.md +68 -229
  3. package/CONTRIBUTING.md +10 -23
  4. package/README.md +96 -87
  5. package/SETUP.md +50 -114
  6. package/docs/development.md +43 -248
  7. package/docs/workflow.md +6 -108
  8. package/html/README.md +140 -0
  9. package/html/browser.ts +103 -0
  10. package/html/document.ts +38 -0
  11. package/html/floor.ts +34 -0
  12. package/html/index.html +5 -0
  13. package/html/inspect.ts +313 -0
  14. package/html/preview.css +62 -0
  15. package/html/preview.tsx +463 -0
  16. package/html/review-hints.ts +63 -0
  17. package/html/server.ts +99 -0
  18. package/html/source.ts +104 -0
  19. package/html/starter.ts +6 -0
  20. package/html/theme-authoring.md +176 -0
  21. package/html/theme.md +61 -0
  22. package/index.html +9 -9
  23. package/package.json +21 -35
  24. package/plugin.json +1 -1
  25. package/public/og.png +0 -0
  26. package/runtime/browser-B-29TH1a.mjs +595 -0
  27. package/runtime/floor-Cmk7G3pU.mjs +41 -0
  28. package/runtime/konpeki.mjs +90 -1410
  29. package/runtime/server-BAqC_5WD.mjs +2 -0
  30. package/runtime/server-DfRpfcY9.mjs +205 -0
  31. package/runtime/source-B_Ui9tMp.mjs +141 -0
  32. package/runtime/source-CZQq9GUO.mjs +2 -0
  33. package/skills/konpeki/SKILL.md +88 -172
  34. package/skills/konpeki/assets/blank.html +17 -0
  35. package/skills/konpeki/floor.md +76 -0
  36. package/skills/konpeki/references/cover.md +27 -0
  37. package/skills/konpeki/references/long-document.md +40 -0
  38. package/skills/konpeki/references/one-pager.md +27 -0
  39. package/skills/konpeki/references/patterns.md +118 -0
  40. package/skills/konpeki/references/resume.md +28 -0
  41. package/skills/konpeki/references/slides.md +28 -0
  42. package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
  43. package/skills/konpeki/scripts/prepare-document.mjs +25 -14
  44. package/src/components/PageBoard.tsx +156 -0
  45. package/src/lib/alignment.ts +21 -0
  46. package/src/lib/page-board.ts +25 -0
  47. package/src/lib/review-position.ts +19 -0
  48. package/src/styles/base.css +4 -6
  49. package/src/styles/feedback.css +97 -61
  50. package/src/styles/shell.css +95 -323
  51. package/theme-base.css +90 -0
  52. package/theme.css +56 -0
  53. package/vite.config.ts +2 -5
  54. package/composition/README.md +0 -156
  55. package/composition/compile.ts +0 -227
  56. package/composition/document.ts +0 -600
  57. package/composition/schema.json +0 -3001
  58. package/composition/schema.ts +0 -437
  59. package/composition/theme-tokens.ts +0 -16
  60. package/composition/types.ts +0 -269
  61. package/composition/validate.ts +0 -226
  62. package/composition/vector.ts +0 -143
  63. package/composition/visualizations.ts +0 -319
  64. package/design/README.md +0 -17
  65. package/design/palettes/README.md +0 -14
  66. package/design/palettes/base.ts +0 -14
  67. package/design/palettes/candidates.ts +0 -19
  68. package/design/palettes/index.ts +0 -78
  69. package/design/review/color-theme.md +0 -44
  70. package/design/review/layout.md +0 -16
  71. package/design/review/text.md +0 -18
  72. package/design/review/typography.md +0 -15
  73. package/design/review/visuals.md +0 -31
  74. package/design/semantic-patterns.md +0 -43
  75. package/design/themes/README.md +0 -40
  76. package/design/themes/index.ts +0 -24
  77. package/design/visual-languages/technical-product.md +0 -17
  78. package/design/visual-review.md +0 -88
  79. package/lib/assets.d.ts +0 -8
  80. package/lib/charts.ts +0 -18
  81. package/lib/contrast.ts +0 -16
  82. package/lib/layouts.ts +0 -50
  83. package/lib/slide.tsx +0 -42
  84. package/lib/taste.ts +0 -17
  85. package/lib/text.tsx +0 -89
  86. package/lib/typeface.ts +0 -44
  87. package/scripts/migrate-react-page.ts +0 -120
  88. package/skills/konpeki/assets/blank.json +0 -23
  89. package/slides/README.md +0 -153
  90. package/slides/architecture/PROMPT.md +0 -31
  91. package/slides/architecture/index.tsx +0 -102
  92. package/slides/article-brief/PROMPT.md +0 -35
  93. package/slides/article-brief/index.tsx +0 -71
  94. package/slides/bar-chart/PROMPT.md +0 -39
  95. package/slides/bar-chart/index.tsx +0 -97
  96. package/slides/comparison/PROMPT.md +0 -29
  97. package/slides/comparison/index.tsx +0 -95
  98. package/slides/decision-memo/PROMPT.md +0 -34
  99. package/slides/decision-memo/index.tsx +0 -85
  100. package/slides/delivery-plan/PROMPT.md +0 -45
  101. package/slides/delivery-plan/index.tsx +0 -105
  102. package/slides/experiment/PROMPT.md +0 -44
  103. package/slides/experiment/index.tsx +0 -127
  104. package/slides/incident-workflow/PROMPT.md +0 -57
  105. package/slides/incident-workflow/index.tsx +0 -78
  106. package/slides/introducing-konpeki/PROMPT.md +0 -40
  107. package/slides/introducing-konpeki/README.md +0 -76
  108. package/slides/introducing-konpeki/SOURCE.md +0 -26
  109. package/slides/introducing-konpeki/author.ts +0 -165
  110. package/slides/introducing-konpeki/composition.json +0 -3270
  111. package/slides/line-chart/PROMPT.md +0 -40
  112. package/slides/line-chart/index.tsx +0 -72
  113. package/slides/migration/PROMPT.md +0 -38
  114. package/slides/migration/index.tsx +0 -89
  115. package/slides/og-images/PROMPT.md +0 -21
  116. package/slides/og-images/index.tsx +0 -76
  117. package/slides/product-introduction/PROMPT.md +0 -24
  118. package/slides/product-introduction/index.tsx +0 -105
  119. package/slides/research-brief/PROMPT.md +0 -40
  120. package/slides/research-brief/index.tsx +0 -104
  121. package/slides/results-explanation/PROMPT.md +0 -32
  122. package/slides/results-explanation/index.tsx +0 -96
  123. package/slides/retrospective/PROMPT.md +0 -43
  124. package/slides/retrospective/index.tsx +0 -105
  125. package/slides/sankey/PROMPT.md +0 -11
  126. package/slides/sankey/index.tsx +0 -93
  127. package/slides/teaching/PROMPT.md +0 -45
  128. package/slides/teaching/index.tsx +0 -124
  129. package/slides/vertical-bar-charts/PROMPT.md +0 -13
  130. package/slides/vertical-bar-charts/index.tsx +0 -97
  131. package/src/app/App.tsx +0 -820
  132. package/src/components/BuildOrb.tsx +0 -40
  133. package/src/components/Canvas.tsx +0 -1185
  134. package/src/components/DiagramTypeIcon.tsx +0 -78
  135. package/src/components/InspectorPanel.tsx +0 -773
  136. package/src/components/LeftPanel.tsx +0 -120
  137. package/src/components/PageSizePicker.tsx +0 -30
  138. package/src/components/Presentation.tsx +0 -105
  139. package/src/components/RevisionNotes.tsx +0 -56
  140. package/src/components/RightPanel.tsx +0 -201
  141. package/src/components/VectorOverflowWarning.tsx +0 -46
  142. package/src/components/WorkspaceChrome.tsx +0 -288
  143. package/src/components/ui.tsx +0 -53
  144. package/src/lib/examples/react-page-migration.json +0 -1295
  145. package/src/lib/examples.ts +0 -42
  146. package/src/lib/export-png.ts +0 -104
  147. package/src/lib/file-session.ts +0 -87
  148. package/src/lib/history.ts +0 -53
  149. package/src/lib/model.ts +0 -188
  150. package/src/lib/page-size.ts +0 -24
  151. package/src/lib/presentation.ts +0 -17
  152. package/src/lib/review.ts +0 -26
  153. package/src/lib/storage.ts +0 -71
  154. package/src/lib/theme.ts +0 -25
  155. package/src/lib/use-file-session.ts +0 -227
  156. package/src/main.tsx +0 -29
  157. package/src/styles/canvas.css +0 -299
  158. package/src/styles/chrome.css +0 -384
  159. package/src/styles/component-previews.css +0 -386
  160. package/src/styles/left-panel.css +0 -166
  161. package/src/styles/presentation.css +0 -72
  162. package/src/styles/right-panel.css +0 -1215
@@ -1,600 +0,0 @@
1
- import {
2
- canvasPadding,
3
- canvasSize,
4
- compositionSchema,
5
- type CanvasSize,
6
- type CompositionComponent,
7
- type CompositionDocument,
8
- type CompositionSlide,
9
- type ContentSlot,
10
- type Rect,
11
- } from "./types.ts";
12
- import { validateComposition } from "./validate.ts";
13
- import { appearanceOptions } from "./schema.ts";
14
- import { diagramDefinition } from "./visualizations.ts";
15
-
16
- export type Draft = CompositionDocument;
17
- export type StoredDraftResult =
18
- { ok: true; draft: Draft } | { ok: false };
19
- export type ParsedComposition =
20
- | { ok: true; document: CompositionDocument }
21
- | { ok: false; message: string };
22
- export const componentLabels = {
23
- "text-block": "Text block",
24
- chart: "Chart",
25
- diagram: "Diagram",
26
- image: "Image",
27
- table: "Table",
28
- };
29
- export function defaultAppearance(kind: CompositionComponent["kind"]) {
30
- const appearance = Object.fromEntries(
31
- Object.entries(appearanceOptions[kind]).map(([key, options]) => [
32
- key,
33
- options[0],
34
- ]),
35
- );
36
- if (kind === "text-block") {
37
- appearance.role = "body";
38
- appearance.titleStyle = "plain";
39
- appearance.purpose = "narrative";
40
- appearance.treatment = "plain";
41
- }
42
- if (kind === "chart") {
43
- appearance.orientation = "vertical";
44
- appearance.colorScheme = "accent-with-muted-context";
45
- appearance.emphasis = "primary";
46
- appearance.legend = "top";
47
- }
48
- if (kind === "diagram") {
49
- appearance.type = "process";
50
- }
51
- if (kind === "table") delete appearance.colorScheme;
52
- return appearance;
53
- }
54
- const defaultRects: Record<CompositionComponent["kind"], Rect> = {
55
- "text-block": { x: canvasPadding.left, y: 280, width: 560, height: 420 },
56
- chart: { x: canvasPadding.left, y: 280, width: 1080, height: 530 },
57
- diagram: {
58
- x: canvasPadding.left,
59
- y: 320,
60
- width: canvasSize.width - canvasPadding.left - canvasPadding.right,
61
- height: 420,
62
- },
63
- image: { x: canvasPadding.left, y: 260, width: 880, height: 540 },
64
- table: { x: canvasPadding.left, y: 280, width: 1080, height: 530 },
65
- };
66
- const instructions = {
67
- "text-block": "Explain the supporting idea in concise language.",
68
- chart: "Show the data evidence that supports the takeaway.",
69
- diagram: "Show the entities or sequence and their relationships.",
70
- image: "Describe the image to use and its purpose.",
71
- table: "Describe the comparison, exact cell values, and reading order for the table.",
72
- };
73
- function isFootnoteText(component: CompositionComponent) {
74
- return component.kind === "text-block" && component.appearance.role === "footnote";
75
- }
76
- function clamp(value: number, minimum: number, maximum: number) {
77
- return Math.min(maximum, Math.max(minimum, value));
78
- }
79
- export function createComponent(
80
- kind: CompositionComponent["kind"],
81
- index: number,
82
- at?: { x: number; y: number },
83
- size: CanvasSize = canvasSize,
84
- ): CompositionComponent {
85
- const id = `${kind}-${index}`;
86
- const original = defaultRects[kind];
87
- const scale = Math.min(1, size.width / canvasSize.width, size.height / canvasSize.height);
88
- const rect = { x: original.x * scale, y: original.y * scale, width: original.width * scale, height: original.height * scale };
89
- const preferredRect = at
90
- ? {
91
- ...rect,
92
- x: clamp(at.x - rect.width / 2, 0, size.width - rect.width),
93
- y: clamp(at.y - 60, 0, size.height - rect.height),
94
- }
95
- : { ...rect };
96
- const appearance = defaultAppearance(kind);
97
- return {
98
- id,
99
- kind,
100
- preferredRect,
101
- slotIds: [`${id}-content`],
102
- intent: instructions[kind],
103
- ...(kind === "text-block" ? { content: "Text", textStyle: { size: 36, weight: 400, lineHeight: 1.4, color: "ink", font: "body" } } : {}),
104
- appearance,
105
- } as CompositionComponent;
106
- }
107
- function createStarterSlide(index = 1): CompositionSlide {
108
- const components = (["text-block", "chart", "text-block", "text-block"] as const)
109
- .map((kind, index) => createComponent(kind, index + 1));
110
- const title = components[0];
111
- if (title.kind === "text-block") {
112
- title.preferredRect = {
113
- x: canvasPadding.left,
114
- y: canvasPadding.top,
115
- width: canvasSize.width - canvasPadding.left - canvasPadding.right,
116
- height: 88,
117
- };
118
- title.intent = "State the decision or takeaway.";
119
- title.appearance = {
120
- ...title.appearance,
121
- role: "title",
122
- titleStyle: "prominent",
123
- rule: "bottom",
124
- };
125
- }
126
- const emphasis = components[2];
127
- if (emphasis.kind === "text-block") {
128
- emphasis.preferredRect.x = 1248;
129
- emphasis.intent = "Explain the implication or recommended action.";
130
- emphasis.appearance = {
131
- ...emphasis.appearance,
132
- purpose: "emphasis",
133
- treatment: "strong",
134
- };
135
- }
136
- const footnote = components[3];
137
- if (footnote.kind === "text-block") {
138
- footnote.preferredRect = {
139
- x: canvasPadding.left,
140
- y: canvasSize.height - canvasPadding.bottom - 88,
141
- width: canvasSize.width - canvasPadding.left - canvasPadding.right,
142
- height: 88,
143
- };
144
- footnote.intent = "Add the source, scope, and any important caveat.";
145
- footnote.appearance = {
146
- ...footnote.appearance,
147
- role: "footnote",
148
- rule: "top",
149
- };
150
- }
151
- const contentSlots = components.flatMap(createContentSlots).map((slot) =>
152
- slot.role === "source"
153
- ? {
154
- ...slot,
155
- targets: components
156
- .filter((component) => !isFootnoteText(component))
157
- .map((component) => component.id),
158
- }
159
- : slot,
160
- );
161
- return {
162
- id: `slide-${index}`,
163
- name: `Slide ${String(index).padStart(2, "0")}`,
164
- canvas: canvasSize,
165
- innerPadding: canvasPadding,
166
- pageNumber: { style: "none", color: "muted" },
167
- audience: "Decision makers",
168
- question: "What should the audience understand or decide?",
169
- intendedViewingSize: "presentation",
170
- contentSlots,
171
- components,
172
- groups: [],
173
- readingOrder: components.map((component) => ({
174
- kind: "component",
175
- id: component.id,
176
- })),
177
- paintOrder: components.map((component) => component.id),
178
- relationships: [],
179
- };
180
- }
181
- export function createBlankSlide(index = 1, size: CanvasSize = canvasSize): CompositionSlide {
182
- return {
183
- id: `slide-${index}`,
184
- name: `Page ${String(index).padStart(2, "0")}`,
185
- canvas: { width: size.width, height: size.height },
186
- innerPadding: canvasPadding,
187
- pageNumber: { style: "none", color: "muted" },
188
- audience: "Decision makers",
189
- question: "What should the audience understand or decide?",
190
- intendedViewingSize: "presentation",
191
- contentSlots: [],
192
- components: [],
193
- groups: [],
194
- readingOrder: [],
195
- paintOrder: [],
196
- relationships: [],
197
- };
198
- }
199
- export function initialDraft(empty = false): Draft {
200
- return {
201
- schema: compositionSchema,
202
- title: "Untitled composition",
203
- theme: { id: "plex", mode: "paper" },
204
- slides: [empty ? createBlankSlide() : createStarterSlide()],
205
- };
206
- }
207
- export function getSlide(draft: Draft, id = draft.slides[0]?.id) {
208
- const slide = draft.slides.find((item) => item.id === id);
209
- if (!slide) throw new Error(`Unknown slide: ${id}`);
210
- return slide;
211
- }
212
- export function addSlide(draft: Draft): { draft: Draft; slideId: string } {
213
- const used = new Set(draft.slides.map((slide) => slide.id));
214
- let index = draft.slides.length + 1;
215
- while (used.has(`slide-${index}`)) index += 1;
216
- const slide = createBlankSlide(index);
217
- const next = { ...draft, slides: [...draft.slides, slide] };
218
- const validated = validMutation(draft, next);
219
- if (validated === draft) return { draft, slideId: "" };
220
- return {
221
- draft: validated,
222
- slideId: slide.id,
223
- };
224
- }
225
- export function removeSlide(draft: Draft, slideId: string): Draft {
226
- if (draft.slides.length === 1) return draft;
227
- const next = {
228
- ...draft,
229
- slides: draft.slides.filter((slide) => slide.id !== slideId),
230
- };
231
- if (next.slides.length === draft.slides.length) return draft;
232
- return validMutation(draft, next);
233
- }
234
- export function createContentSlots(
235
- component: CompositionComponent,
236
- ): ContentSlot[] {
237
- return component.slotIds.map((id, index): ContentSlot => {
238
- const base = {
239
- id,
240
- label: componentLabels[component.kind] + (index ? ` ${index + 1}` : ""),
241
- required: true,
242
- instruction: component.intent?.trim() || instructions[component.kind],
243
- };
244
- if (isFootnoteText(component))
245
- return { ...base, role: "source", targets: [] };
246
- const roles = {
247
- chart: "evidence",
248
- diagram:
249
- component.kind === "diagram"
250
- ? diagramDefinition(component.appearance.type).slotRole
251
- : "entity",
252
- "text-block": component.kind === "text-block" && component.appearance.role === "title"
253
- ? "takeaway"
254
- : "body",
255
- image: "image",
256
- table: "table",
257
- } as const;
258
- return { ...base, role: roles[component.kind] };
259
- });
260
- }
261
- function uniqueId(base: string, used: Set<string>) {
262
- if (!used.has(base)) return base;
263
- let index = 2;
264
- while (used.has(`${base}-${index}`)) index += 1;
265
- return `${base}-${index}`;
266
- }
267
- function offsetRect(rect: Rect, size: CanvasSize) {
268
- const maximumX = size.width - rect.width;
269
- const maximumY = size.height - rect.height;
270
- const x = rect.x + 48 <= maximumX ? rect.x + 48 : Math.max(0, rect.x - 48);
271
- const y = rect.y + 48 <= maximumY ? rect.y + 48 : Math.max(0, rect.y - 48);
272
- return { ...rect, x, y };
273
- }
274
- export function transformComponentRect(
275
- component: CompositionComponent,
276
- previousRect: Rect,
277
- nextRect: Rect,
278
- ): CompositionComponent {
279
- const next = structuredClone(component);
280
- next.preferredRect = { ...nextRect };
281
- if ((next.kind !== "diagram" && next.kind !== "chart") || !next.topology) return next;
282
- const scaleX = nextRect.width / previousRect.width;
283
- const scaleY = nextRect.height / previousRect.height;
284
- next.topology.nodes = next.topology.nodes.map((node) => {
285
- if (!node.preferredRect) return node;
286
- return {
287
- ...node,
288
- preferredRect: {
289
- x: nextRect.x + (node.preferredRect.x - previousRect.x) * scaleX,
290
- y: nextRect.y + (node.preferredRect.y - previousRect.y) * scaleY,
291
- width: node.preferredRect.width * scaleX,
292
- height: node.preferredRect.height * scaleY,
293
- },
294
- };
295
- });
296
- return next;
297
- }
298
- function validMutation(draft: Draft, next: Draft) {
299
- const candidate = next.title.trim()
300
- ? next
301
- : { ...next, title: "Untitled composition" };
302
- return validateComposition(candidate).ok ? next : draft;
303
- }
304
- export function addComponent(
305
- draft: Draft,
306
- kind: CompositionComponent["kind"],
307
- at?: { x: number; y: number },
308
- slideId?: string,
309
- ): Draft {
310
- const slide = getSlide(draft, slideId);
311
- let index =
312
- Math.max(
313
- 0,
314
- ...slide.components.map((c) =>
315
- Number(c.id.match(/-(\d+)$/)?.[1] ?? 0),
316
- ),
317
- ) + 1;
318
- const usedIds = new Set([
319
- ...slide.components.map((component) => component.id),
320
- ...slide.contentSlots.map((slot) => slot.id),
321
- ...slide.groups.map((group) => group.id),
322
- ]);
323
- while (
324
- usedIds.has(`${kind}-${index}`) ||
325
- usedIds.has(`${kind}-${index}-content`)
326
- )
327
- index += 1;
328
- const component = createComponent(kind, index, at, slide.canvas);
329
- const previous = [...slide.components]
330
- .reverse()
331
- .find((item) =>
332
- item.kind === kind &&
333
- (item.kind !== "text-block" ||
334
- component.kind !== "text-block" ||
335
- item.appearance.role === component.appearance.role),
336
- );
337
- if (!at && previous)
338
- component.preferredRect = offsetRect(previous.preferredRect, slide.canvas);
339
- const readingOrder = [...slide.readingOrder];
340
- readingOrder.push({ kind: "component", id: component.id });
341
- const contentSlots = [
342
- ...slide.contentSlots,
343
- ...createContentSlots(component).map((slot) =>
344
- slot.role === "source"
345
- ? {
346
- ...slot,
347
- targets: slide.components
348
- .filter((item) => !isFootnoteText(item))
349
- .map((item) => item.id),
350
- }
351
- : slot,
352
- ),
353
- ];
354
- const next = {
355
- ...draft,
356
- slides: draft.slides.map((item) =>
357
- item.id === slide.id
358
- ? {
359
- ...item,
360
- contentSlots,
361
- components: [...item.components, component],
362
- readingOrder,
363
- paintOrder: [...item.paintOrder, component.id],
364
- }
365
- : item,
366
- ),
367
- };
368
- return validMutation(draft, next);
369
- }
370
- export function componentRemovalIssue(draft: Draft, id: string, slideId?: string) {
371
- const slide = getSlide(draft, slideId);
372
- const removed = slide.components.find((component) => component.id === id);
373
- if (!removed) return "Component not found.";
374
- const survivingOwners = new Set(
375
- slide.components
376
- .filter((component) => component.id !== id)
377
- .flatMap((component) => component.slotIds),
378
- );
379
- const orphanedSlots = new Set(
380
- removed.slotIds.filter((slotId) => !survivingOwners.has(slotId)),
381
- );
382
- const blockedReference = slide.contentSlots.find(
383
- (slot) =>
384
- "targets" in slot &&
385
- !orphanedSlots.has(slot.id) &&
386
- slot.targets.filter(
387
- (target) => target !== id && !orphanedSlots.has(target),
388
- ).length === 0,
389
- );
390
- return blockedReference
391
- ? `Cannot delete while “${blockedReference.label}” only targets this component.`
392
- : undefined;
393
- }
394
- export function removeComponent(draft: Draft, id: string, slideId?: string): Draft {
395
- const slide = getSlide(draft, slideId);
396
- const removed = slide.components.find((component) => component.id === id);
397
- if (!removed || componentRemovalIssue(draft, id, slide.id)) return draft;
398
- const survivingSlots = new Set(
399
- slide.components
400
- .filter((component) => component.id !== id)
401
- .flatMap((component) => component.slotIds),
402
- );
403
- const removedSlots = new Set(
404
- removed.slotIds.filter((slotId) => !survivingSlots.has(slotId)),
405
- );
406
- const groups = slide.groups
407
- .map((group) => ({
408
- ...group,
409
- childIds: group.childIds.filter((childId) => childId !== id),
410
- }))
411
- .filter((group) => group.childIds.length);
412
- const groupIds = new Set(groups.map((group) => group.id));
413
- const next = {
414
- ...draft,
415
- slides: draft.slides.map((item) =>
416
- item.id === slide.id
417
- ? {
418
- ...item,
419
- contentSlots: item.contentSlots
420
- .filter((slot) => !removedSlots.has(slot.id))
421
- .map((slot) =>
422
- "targets" in slot
423
- ? {
424
- ...slot,
425
- targets: slot.targets.filter(
426
- (target) =>
427
- target !== id && !removedSlots.has(target),
428
- ),
429
- }
430
- : slot,
431
- ),
432
- components: item.components.filter((c) => c.id !== id),
433
- groups,
434
- readingOrder: item.readingOrder.filter((entry) =>
435
- entry.kind === "component"
436
- ? entry.id !== id
437
- : groupIds.has(entry.id),
438
- ),
439
- paintOrder: item.paintOrder.filter((item) => item !== id),
440
- relationships: item.relationships.filter(
441
- (link) => link.from.nodeId !== id && link.to.nodeId !== id,
442
- ),
443
- }
444
- : item,
445
- ),
446
- };
447
- return validMutation(draft, next);
448
- }
449
- export function reorderPaintOrder(
450
- draft: Draft,
451
- paintOrder: string[],
452
- slideId?: string,
453
- ): Draft {
454
- const slide = getSlide(draft, slideId);
455
- if (
456
- paintOrder.length !== slide.components.length ||
457
- new Set(paintOrder).size !== paintOrder.length ||
458
- paintOrder.some((id) => !slide.components.some((component) => component.id === id))
459
- )
460
- return draft;
461
- return validMutation(draft, {
462
- ...draft,
463
- slides: draft.slides.map((item) =>
464
- item.id === slide.id ? { ...item, paintOrder: [...paintOrder] } : item,
465
- ),
466
- });
467
- }
468
- export function duplicateComponent(draft: Draft, id: string, slideId?: string): Draft {
469
- const slide = getSlide(draft, slideId);
470
- const source = slide.components.find((component) => component.id === id);
471
- if (!source) return draft;
472
- const used = new Set([
473
- ...slide.components.map((component) => component.id),
474
- ...slide.contentSlots.map((slot) => slot.id),
475
- ...slide.groups.map((group) => group.id),
476
- ]);
477
- const componentId = uniqueId(`${source.id}-copy`, used);
478
- used.add(componentId);
479
- const slotIds = source.slotIds.map((_, index) => {
480
- const slotId = uniqueId(`${componentId}-content${index ? `-${index + 1}` : ""}`, used);
481
- used.add(slotId);
482
- return slotId;
483
- });
484
- const slotMap = new Map(source.slotIds.map((slotId, index) => [slotId, slotIds[index]]));
485
- const preferredRect = offsetRect(source.preferredRect, slide.canvas);
486
- const component = transformComponentRect(
487
- source,
488
- source.preferredRect,
489
- preferredRect,
490
- );
491
- component.id = componentId;
492
- component.slotIds = slotIds;
493
- if ((component.kind === "diagram" || component.kind === "chart") && component.topology)
494
- component.topology.nodes = component.topology.nodes.map((node) => ({
495
- ...node,
496
- slotId: slotMap.get(node.slotId) ?? node.slotId,
497
- }));
498
- const sourceSlots = new Map(
499
- slide.contentSlots.map((slot) => [slot.id, slot]),
500
- );
501
- const contentSlots = source.slotIds.map((slotId, index) => {
502
- const slot = structuredClone(sourceSlots.get(slotId)!);
503
- slot.id = slotIds[index];
504
- if ("targets" in slot)
505
- slot.targets = slot.targets.map((target) =>
506
- target === source.id ? componentId : slotMap.get(target) ?? target,
507
- );
508
- return slot;
509
- });
510
- const allSlots = slide.contentSlots.map((slot) =>
511
- slot.role === "source" && slot.targets.includes(source.id)
512
- ? {
513
- ...slot,
514
- targets: slot.targets.flatMap((target) =>
515
- target === source.id ? [target, componentId] : [target],
516
- ),
517
- }
518
- : slot,
519
- );
520
- const slotInsert = Math.max(
521
- ...source.slotIds.map((slotId) => allSlots.findIndex((slot) => slot.id === slotId)),
522
- );
523
- allSlots.splice(slotInsert + 1, 0, ...contentSlots);
524
- const components = [...slide.components];
525
- components.splice(components.indexOf(source) + 1, 0, component);
526
- const groups = slide.groups.map((group) => {
527
- const sourceIndex = group.childIds.indexOf(id);
528
- if (sourceIndex < 0) return group;
529
- const childIds = [...group.childIds];
530
- childIds.splice(sourceIndex + 1, 0, componentId);
531
- return { ...group, childIds };
532
- });
533
- const readingOrder = [...slide.readingOrder];
534
- const sourceIndex = readingOrder.findIndex(
535
- (entry) => entry.kind === "component" && entry.id === id,
536
- );
537
- if (sourceIndex >= 0) {
538
- readingOrder.splice(sourceIndex + 1, 0, {
539
- kind: "component",
540
- id: componentId,
541
- });
542
- }
543
- const paintOrder = [...slide.paintOrder];
544
- paintOrder.splice(paintOrder.indexOf(id) + 1, 0, componentId);
545
- const next = {
546
- ...draft,
547
- slides: draft.slides.map((item) =>
548
- item.id === slide.id
549
- ? {
550
- ...item,
551
- contentSlots: allSlots,
552
- components,
553
- groups,
554
- readingOrder,
555
- paintOrder,
556
- }
557
- : item,
558
- ),
559
- };
560
- return validMutation(draft, next);
561
- }
562
- export function serializeDraft(draft: Draft) {
563
- return JSON.stringify({ version: 2, document: draft });
564
- }
565
- export function parseStoredDraft(raw: string): StoredDraftResult {
566
- try {
567
- const parsed = JSON.parse(raw);
568
- if (parsed?.version === 2) {
569
- const document = parsed.document as CompositionDocument;
570
- if (typeof document?.title !== "string") return { ok: false };
571
- const candidate = {
572
- ...document,
573
- title: document.title.trim() || "Untitled composition",
574
- };
575
- const result = validateComposition(candidate);
576
- if (!result.ok) return { ok: false };
577
- return {
578
- ok: true,
579
- draft: { ...result.document, title: document.title },
580
- };
581
- }
582
- return { ok: false };
583
- } catch {
584
- return { ok: false };
585
- }
586
- }
587
- export function parseCompositionJSON(raw: string): ParsedComposition {
588
- try {
589
- const input: unknown = JSON.parse(raw);
590
- const result = validateComposition(input);
591
- if (result.ok) return { ok: true, document: result.document };
592
- const issue = result.issues[0];
593
- return {
594
- ok: false,
595
- message: `${issue?.path || "Document"}: ${issue?.message || "Invalid composition"}`,
596
- };
597
- } catch {
598
- return { ok: false, message: "The selected file is not valid JSON." };
599
- }
600
- }