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,269 +0,0 @@
1
- import type { ChartTemplate, DiagramType } from "./visualizations.ts";
2
- export type { ChartTemplate, DiagramType } from "./visualizations.ts";
3
-
4
- export const compositionSchema = "konpeki-composition/v1" as const;
5
- export type CanvasSize = { width: number; height: number };
6
- export const canvasSize = { width: 1920, height: 1080 } as const;
7
- export const canvasPadding = {
8
- top: 72,
9
- right: 112,
10
- bottom: 0,
11
- left: 112,
12
- } as const;
13
- export const authoringModes = ["default", "dynamic"] as const;
14
- export type AuthoringMode = (typeof authoringModes)[number];
15
- export const themeIds = [
16
- "plex",
17
- "precision",
18
- "editorial",
19
- "blue-cyan",
20
- "orange-coral",
21
- "yellow",
22
- "green",
23
- "graphite",
24
- ] as const;
25
- export type ThemeId = (typeof themeIds)[number];
26
- export const themeModes = ["paper", "night"] as const;
27
- export type ThemeMode = (typeof themeModes)[number];
28
- export const typographyIds = ["plex-sans", "noto-sans", "plex-serif", "hanken-grotesk"] as const;
29
- export type TypographyId = (typeof typographyIds)[number];
30
- export type Rect = { x: number; y: number; width: number; height: number };
31
- export type Alignment = "start" | "center" | "end";
32
- export type TitleStyle = "plain" | "prominent";
33
- export type BorderTreatment = "none" | "outline" | "filled";
34
- export type TextColor = "ink" | "muted" | "accent";
35
- export type TextLayout =
36
- | "single"
37
- | "two-column"
38
- | "three-column"
39
- | "two-plus-two"
40
- | "four-column"
41
- | "one-plus-three"
42
- | "three-plus-one";
43
- export type TextPurpose = "narrative" | "comparison" | "emphasis";
44
- export type TextTreatment = "plain" | "subtle" | "strong";
45
- export type TextLogicalOrder =
46
- | "none"
47
- | "parallel"
48
- | "progressive"
49
- | "cyclical"
50
- | "general-to-specific"
51
- | "hierarchical";
52
- export type TextRole = "title" | "subtitle" | "body" | "caption" | "footnote";
53
- export type ColorScheme =
54
- | "monochrome"
55
- | "accent-with-muted-context"
56
- | "categorical"
57
- | "sequential"
58
- | "status";
59
- export type Density = "sparse" | "standard";
60
- export type Emphasis = "none" | "primary" | "latest-series" | "exception";
61
- export const vectorElementKinds = [
62
- "g",
63
- "rect",
64
- "circle",
65
- "ellipse",
66
- "line",
67
- "polyline",
68
- "polygon",
69
- "path",
70
- "text",
71
- "tspan",
72
- ] as const;
73
- export type VectorElementKind = (typeof vectorElementKinds)[number];
74
- export type VectorElement = {
75
- id: string;
76
- kind: VectorElementKind;
77
- parentId?: string;
78
- attributes: Record<string, string | number>;
79
- text?: string;
80
- };
81
- type VisualBase = {
82
- viewBox: { x: number; y: number; width: number; height: number };
83
- description: string;
84
- fit?: "contain" | "cover" | "stretch";
85
- };
86
- export type CustomVisual = VisualBase & (
87
- | { format: "vector"; elements: VectorElement[] }
88
- | { format: "svg"; source: string }
89
- );
90
- type ComponentBase = {
91
- id: string;
92
- preferredRect: Rect;
93
- slotIds: string[];
94
- intent?: string;
95
- customVisual?: CustomVisual;
96
- };
97
- export type TextBlockComponent = ComponentBase & {
98
- kind: "text-block";
99
- content?: string;
100
- textStyle?: {
101
- size?: number;
102
- weight?: 400 | 500 | 600;
103
- lineHeight?: number;
104
- color?: "ink" | "muted" | "accent";
105
- font?: "heading" | "body";
106
- };
107
- appearance: {
108
- role: TextRole;
109
- alignment?: Alignment;
110
- border?: BorderTreatment;
111
- layout?: TextLayout;
112
- orientation?: "horizontal" | "vertical";
113
- purpose?: TextPurpose;
114
- treatment?: TextTreatment;
115
- logicalOrder?: TextLogicalOrder;
116
- titleStyle?: TitleStyle;
117
- rule?: "none" | "top" | "bottom";
118
- };
119
- };
120
- type ChartAppearance = {
121
- selection?: "auto" | "explicit";
122
- border?: BorderTreatment;
123
- colorScheme?: ColorScheme;
124
- orientation?: "horizontal" | "vertical";
125
- emphasis?: Emphasis;
126
- density?: Density;
127
- legend?: "none" | "top" | "right" | "bottom";
128
- };
129
- export type ChartComponent = ComponentBase & ({
130
- kind: "chart";
131
- appearance: ChartAppearance & { template: "sankey" };
132
- topology?: ExplicitTopology;
133
- } | {
134
- kind: "chart";
135
- appearance: ChartAppearance & {
136
- template: Exclude<ChartTemplate, "sankey">;
137
- };
138
- topology?: never;
139
- });
140
- export type ExplicitTopology = {
141
- kind: "explicit";
142
- nodes: {
143
- id: string;
144
- slotId: string;
145
- visible?: boolean;
146
- preferredRect?: Rect;
147
- primitive?:
148
- | {
149
- kind: "shape";
150
- shape: "rectangle" | "rounded-rectangle" | "circle" | "line";
151
- color?: TextColor;
152
- fill?: "none" | "solid" | "wash";
153
- }
154
- | {
155
- kind: "icon";
156
- name?: string;
157
- style?: "outline" | "filled";
158
- color?: TextColor;
159
- };
160
- }[];
161
- edges: { from: string; to: string; label?: string }[];
162
- };
163
- export type DiagramComponent = ComponentBase & {
164
- kind: "diagram";
165
- appearance: {
166
- type: DiagramType;
167
- selection?: "auto" | "explicit";
168
- border?: BorderTreatment;
169
- colorScheme?: ColorScheme;
170
- emphasis?: Emphasis;
171
- density?: Density;
172
- };
173
- topology?: ExplicitTopology;
174
- };
175
- export type ImageComponent = ComponentBase & {
176
- kind: "image";
177
- appearance?: {
178
- border?: BorderTreatment;
179
- fit?: "cover" | "contain";
180
- };
181
- };
182
- export type TableComponent = ComponentBase & {
183
- kind: "table";
184
- appearance?: {
185
- header?: "none" | "row" | "column" | "both";
186
- grid?: "none" | "rows" | "all";
187
- border?: BorderTreatment;
188
- colorScheme?: ColorScheme;
189
- density?: Density;
190
- };
191
- };
192
- export type CompositionComponent =
193
- | TextBlockComponent
194
- | ChartComponent
195
- | DiagramComponent
196
- | ImageComponent
197
- | TableComponent;
198
- export const componentKinds = [
199
- "text-block",
200
- "chart",
201
- "diagram",
202
- "image",
203
- "table",
204
- ] as const satisfies readonly CompositionComponent["kind"][];
205
- export type ContentSlotRole =
206
- | "takeaway"
207
- | "body"
208
- | "evidence"
209
- | "comparison-item"
210
- | "process-step"
211
- | "entity"
212
- | "qualification"
213
- | "source"
214
- | "image"
215
- | "table";
216
- type ContentSlotBase = {
217
- id: string;
218
- label: string;
219
- required: boolean;
220
- instruction: string;
221
- };
222
- export type ContentSlot =
223
- | (ContentSlotBase & {
224
- role: Exclude<ContentSlotRole, "qualification" | "source">;
225
- })
226
- | (ContentSlotBase & { role: "qualification" | "source"; targets: string[] });
227
- export type ManipulationGroup = {
228
- id: string;
229
- label?: string;
230
- childIds: string[];
231
- };
232
- export type ReadingOrderEntry =
233
- { kind: "component"; id: string } | { kind: "group"; id: string };
234
- export type RelationshipEndpoint = { nodeId: string; slotId?: string };
235
- export type SemanticRelationship = {
236
- id: string;
237
- kind:
238
- "flows-to" | "qualifies" | "compares-with" | "depends-on" | "connects-to";
239
- direction: "forward" | "bidirectional" | "undirected";
240
- from: RelationshipEndpoint;
241
- to: RelationshipEndpoint;
242
- label?: string;
243
- };
244
- export type CompositionSlide = {
245
- id: string;
246
- name: string;
247
- canvas: CanvasSize;
248
- innerPadding?: { top: number; right: number; bottom: number; left: number };
249
- pageNumber?: {
250
- style: "none" | "01" | "01/02";
251
- color: TextColor;
252
- };
253
- audience: string;
254
- question: string;
255
- intendedViewingSize: "presentation" | "social" | "article" | "custom";
256
- contentSlots: ContentSlot[];
257
- components: CompositionComponent[];
258
- groups: ManipulationGroup[];
259
- readingOrder: ReadingOrderEntry[];
260
- paintOrder: string[];
261
- relationships: SemanticRelationship[];
262
- };
263
- export type CompositionDocument = {
264
- schema: typeof compositionSchema;
265
- title: string;
266
- authoringMode?: AuthoringMode;
267
- theme?: { id: ThemeId; mode: ThemeMode; typography?: TypographyId };
268
- slides: CompositionSlide[];
269
- };
@@ -1,226 +0,0 @@
1
- import { Ajv2020 } from "ajv/dist/2020.js";
2
- import { schema } from "./schema.ts";
3
- import {
4
- type CompositionDocument,
5
- } from "./types.ts";
6
- import { vectorAttributeNames } from "./vector.ts";
7
- import { validThemeBinding } from "./theme-tokens.ts";
8
-
9
- export type ValidationIssue = { path: string; message: string };
10
- export type ValidationResult =
11
- | { ok: true; document: CompositionDocument }
12
- | { ok: false; issues: ValidationIssue[] };
13
- const structural = new Ajv2020({
14
- allErrors: true,
15
- strict: false,
16
- }).compile<CompositionDocument>(schema);
17
- export function validateComposition(input: unknown): ValidationResult {
18
- if (!structural(input))
19
- return {
20
- ok: false,
21
- issues: (structural.errors ?? []).map((error) => ({
22
- path: error.instancePath,
23
- message: error.message ?? "Invalid value",
24
- })),
25
- };
26
- const candidate = input;
27
- const issues: ValidationIssue[] = [];
28
- const fail = (path: string, message: string) =>
29
- issues.push({ path, message });
30
- const unique = (ids: string[], path: string) => {
31
- if (new Set(ids).size !== ids.length) fail(path, "IDs must be unique");
32
- };
33
- unique(
34
- candidate.slides.map((slide) => slide.id),
35
- "/slides",
36
- );
37
- candidate.slides.forEach((slide, slideIndex) => {
38
- const base = `/slides/${slideIndex}`;
39
- const padding = slide.innerPadding;
40
- if (padding && (padding.left + padding.right >= slide.canvas.width || padding.top + padding.bottom >= slide.canvas.height))
41
- fail(`${base}/innerPadding`, "Padding leaves no page content area");
42
- const components = new Map(slide.components.map((item) => [item.id, item]));
43
- const slots = new Set(slide.contentSlots.map((item) => item.id));
44
- const groups = new Map(slide.groups.map((item) => [item.id, item]));
45
- unique([...components.keys(), ...groups.keys()], `${base}/groups`);
46
- unique(
47
- slide.components.map((item) => item.id),
48
- `${base}/components`,
49
- );
50
- unique(
51
- slide.contentSlots.map((item) => item.id),
52
- `${base}/contentSlots`,
53
- );
54
- unique(
55
- slide.groups.map((item) => item.id),
56
- `${base}/groups`,
57
- );
58
- unique(
59
- slide.relationships.map((item) => item.id),
60
- `${base}/relationships`,
61
- );
62
- const assigned: string[] = [];
63
- for (const component of slide.components) {
64
- const path = `${base}/components/${component.id}`;
65
- const r = component.preferredRect;
66
- if (r.x + r.width > slide.canvas.width || r.y + r.height > slide.canvas.height)
67
- fail(path, "Rectangle exceeds canvas");
68
- assigned.push(...component.slotIds);
69
- for (const id of component.slotIds)
70
- if (!slots.has(id)) fail(path, `Unknown slot: ${id}`);
71
- if (component.customVisual) {
72
- if (component.customVisual.format === "svg") {
73
- const source = component.customVisual.source;
74
- if (
75
- /<script\b|<foreignObject\b|<\?xml-stylesheet\b|<!DOCTYPE\b|\son[a-z]+\s*=|(?:href|src)\s*=\s*["'](?!data:|#)|url\(\s*["']?(?!data:|#)/i.test(
76
- source,
77
- )
78
- )
79
- fail(path, "Custom SVG must be self-contained and cannot include scripts, foreign objects, event handlers, document declarations, or external resources");
80
- const sourceViewBox = source
81
- .match(/<svg\b[^>]*\bviewBox\s*=\s*(["'])([^"']+)\1/i)?.[2]
82
- ?.trim()
83
- .split(/[\s,]+/)
84
- .map(Number);
85
- const declaredViewBox = component.customVisual.viewBox;
86
- if (
87
- sourceViewBox?.length !== 4 ||
88
- sourceViewBox.some((value) => !Number.isFinite(value)) ||
89
- sourceViewBox[0] !== declaredViewBox.x ||
90
- sourceViewBox[1] !== declaredViewBox.y ||
91
- sourceViewBox[2] !== declaredViewBox.width ||
92
- sourceViewBox[3] !== declaredViewBox.height
93
- )
94
- fail(path, "Custom SVG root viewBox must match the declared local viewport");
95
- } else {
96
- const known = new Set<string>();
97
- const kinds = new Map(component.customVisual.elements.map((element) => [element.id, element.kind]));
98
- for (const element of component.customVisual.elements) {
99
- if (known.has(element.id)) fail(path, `Duplicate vector element ID: ${element.id}`);
100
- if (element.parentId && !known.has(element.parentId))
101
- fail(path, `Vector parent must precede its child: ${element.parentId}`);
102
- const parentKind = element.parentId ? kinds.get(element.parentId) : undefined;
103
- if (element.parentId && parentKind !== "g" &&
104
- !(element.kind === "tspan" && (parentKind === "text" || parentKind === "tspan")))
105
- fail(path, "Vector children require a group or a text/tspan parent for tspans");
106
- if (element.kind === "tspan" && parentKind !== "text" && parentKind !== "tspan")
107
- fail(path, "Vector tspans require a text or tspan parent");
108
- if (element.text !== undefined && element.kind !== "text" && element.kind !== "tspan")
109
- fail(path, "Only vector text and tspans may contain text");
110
- known.add(element.id);
111
- for (const [name, value] of Object.entries(element.attributes)) {
112
- if (!validThemeBinding(name, value))
113
- fail(path, `Invalid theme binding for vector attribute: ${name}`);
114
- if (!vectorAttributeNames.has(name))
115
- fail(path, `Unsupported vector attribute: ${name}`);
116
- if (typeof value === "string" && /url\s*\(|javascript:|data:/i.test(value))
117
- fail(path, `Unsafe vector attribute value: ${name}`);
118
- }
119
- }
120
- }
121
- }
122
- if (component.kind === "chart" && component.topology && component.appearance.template !== "sankey")
123
- fail(path, "Only Sankey charts may define topology");
124
- if ((component.kind === "diagram" || component.kind === "chart") && component.topology) {
125
- const { nodes, edges } = component.topology;
126
- unique(
127
- nodes.map((node) => node.id),
128
- path,
129
- );
130
- unique(
131
- nodes.map((node) => node.slotId),
132
- path,
133
- );
134
- unique(
135
- edges.map((edge) =>
136
- JSON.stringify([edge.from, edge.to, edge.label ?? ""]),
137
- ),
138
- path,
139
- );
140
- const known = new Set(nodes.map((node) => node.id));
141
- if (nodes.length !== component.slotIds.length)
142
- fail(path, "Topology must represent every component slot");
143
- for (const node of nodes) {
144
- if (!component.slotIds.includes(node.slotId))
145
- fail(path, "Topology slot must belong to its component");
146
- const nodeRect = node.preferredRect;
147
- if (
148
- nodeRect &&
149
- (nodeRect.x < r.x ||
150
- nodeRect.y < r.y ||
151
- nodeRect.x + nodeRect.width > r.x + r.width ||
152
- nodeRect.y + nodeRect.height > r.y + r.height)
153
- )
154
- fail(path, "Topology node rectangle must stay within its component");
155
- }
156
- for (const edge of edges) {
157
- if (!known.has(edge.from) || !known.has(edge.to))
158
- fail(path, "Unknown topology endpoint");
159
- if (edge.from === edge.to)
160
- fail(path, "Topology self-edges are not supported");
161
- }
162
- }
163
- }
164
- for (const id of slots)
165
- if (!assigned.includes(id))
166
- fail(`${base}/contentSlots`, `Unassigned content slot: ${id}`);
167
- const grouped: string[] = [];
168
- for (const group of slide.groups)
169
- for (const id of group.childIds) {
170
- grouped.push(id);
171
- if (!components.has(id))
172
- fail(`${base}/groups`, "Groups may contain only components");
173
- }
174
- unique(grouped, `${base}/groups`);
175
- const reading = slide.readingOrder.flatMap((entry) => {
176
- if (entry.kind === "component") {
177
- if (!components.has(entry.id))
178
- fail(`${base}/readingOrder`, "Unknown component");
179
- return [entry.id];
180
- }
181
- const group = groups.get(entry.id);
182
- if (!group) fail(`${base}/readingOrder`, "Unknown group");
183
- return group?.childIds ?? [];
184
- });
185
- for (const [path, order] of [
186
- ["readingOrder", reading],
187
- ["paintOrder", slide.paintOrder],
188
- ] as const) {
189
- if (
190
- order.length !== components.size ||
191
- new Set(order).size !== components.size ||
192
- order.some((id) => !components.has(id))
193
- )
194
- fail(`${base}/${path}`, "Must contain every component exactly once");
195
- }
196
- for (const slot of slide.contentSlots)
197
- if ("targets" in slot)
198
- for (const id of slot.targets) {
199
- if (!components.has(id) && !slots.has(id))
200
- fail(`${base}/contentSlots`, "Unknown qualification/source target");
201
- }
202
- for (const relationship of slide.relationships)
203
- for (const endpoint of [relationship.from, relationship.to]) {
204
- const component = components.get(endpoint.nodeId);
205
- if (!component) fail(`${base}/relationships`, "Unknown relationship node");
206
- if (endpoint.slotId && !component?.slotIds.includes(endpoint.slotId))
207
- fail(
208
- `${base}/relationships`,
209
- "Relationship slot must belong to its endpoint",
210
- );
211
- }
212
- });
213
- return issues.length
214
- ? { ok: false, issues }
215
- : { ok: true, document: input };
216
- }
217
- export function assertComposition(input: unknown): CompositionDocument {
218
- const result = validateComposition(input);
219
- if (!result.ok)
220
- throw new Error(
221
- result.issues
222
- .map((issue) => `${issue.path}: ${issue.message}`)
223
- .join("\n"),
224
- );
225
- return result.document;
226
- }
@@ -1,143 +0,0 @@
1
- import { DOMParser } from "@xmldom/xmldom";
2
- import {
3
- vectorElementKinds,
4
- type VectorElement,
5
- type VectorElementKind,
6
- } from "./types.ts";
7
-
8
- export const vectorAttributeNames = new Set([
9
- "x", "y", "dx", "dy", "x1", "y1", "x2", "y2", "width", "height", "rx", "ry",
10
- "cx", "cy", "r", "d", "points", "transform", "fill", "fill-opacity",
11
- "stroke", "stroke-width", "stroke-opacity", "stroke-linecap", "stroke-linejoin",
12
- "stroke-dasharray", "opacity", "font-family", "font-size", "font-weight",
13
- "font-style", "text-anchor", "dominant-baseline", "vector-effect",
14
- ]);
15
-
16
- /** Remove a subtree, never leaving orphaned descendants. */
17
- export function removeVectorElement(elements: VectorElement[], id: string) {
18
- const removed = new Set([id]);
19
- for (const element of elements) {
20
- if (element.parentId && removed.has(element.parentId)) removed.add(element.id);
21
- }
22
- return elements.filter((element) => !removed.has(element.id));
23
- }
24
-
25
- /** Swap sibling subtrees while retaining parent-before-child serialization. */
26
- export function moveVectorElement(elements: VectorElement[], id: string, direction: -1 | 1) {
27
- const selected = elements.find((element) => element.id === id);
28
- if (!selected) return elements;
29
- const children = new Map<string | undefined, VectorElement[]>();
30
- for (const element of elements) {
31
- const peers = children.get(element.parentId) ?? [];
32
- peers.push(element);
33
- children.set(element.parentId, peers);
34
- }
35
- const siblings = children.get(selected.parentId)!;
36
- const index = siblings.indexOf(selected);
37
- const other = siblings[index + direction];
38
- if (!other) return elements;
39
- [siblings[index], siblings[index + direction]] = [other, selected];
40
- const result: VectorElement[] = [];
41
- function visit(parentId?: string) {
42
- for (const child of children.get(parentId) ?? []) {
43
- result.push(child);
44
- visit(child.id);
45
- }
46
- }
47
- visit();
48
- return result;
49
- }
50
-
51
- function uniqueId(preferred: string, used: Set<string>, index: number) {
52
- const base = preferred.trim().replace(/[^a-zA-Z0-9_-]+/g, "-") || `vector-${index}`;
53
- if (!used.has(base)) return base;
54
- let suffix = 2;
55
- while (used.has(`${base}-${suffix}`)) suffix += 1;
56
- return `${base}-${suffix}`;
57
- }
58
-
59
- function attributesOf(element: Element) {
60
- const attributes: Record<string, string | number> = {};
61
- for (let index = 0; index < element.attributes.length; index += 1) {
62
- const attribute = element.attributes.item(index);
63
- if (!attribute || attribute.name === "id") continue;
64
- if (attribute.name === "style") {
65
- for (const declaration of attribute.value.split(";")) {
66
- const separator = declaration.indexOf(":");
67
- if (separator < 0) continue;
68
- const name = declaration.slice(0, separator).trim();
69
- const value = declaration.slice(separator + 1).trim();
70
- if (vectorAttributeNames.has(name) && value && !/url\s*\(/i.test(value))
71
- attributes[name] = value;
72
- }
73
- continue;
74
- }
75
- if (vectorAttributeNames.has(attribute.name)) attributes[attribute.name] = attribute.value;
76
- }
77
- return attributes;
78
- }
79
-
80
- export function parseEditableSvg(source: string) {
81
- const document = new DOMParser().parseFromString(source, "image/svg+xml");
82
- const root = document.documentElement;
83
- if (!root || root.localName !== "svg") throw new Error("The visual must have an SVG root.");
84
- const values = root.getAttribute("viewBox")?.trim().split(/[\s,]+/).map(Number);
85
- if (!values || values.length !== 4 || values.some((value) => !Number.isFinite(value)))
86
- throw new Error("The SVG root needs a numeric viewBox.");
87
- const [x, y, width, height] = values;
88
- if (width <= 0 || height <= 0) throw new Error("The SVG viewBox must be positive.");
89
-
90
- const elements: VectorElement[] = [];
91
- const used = new Set<string>();
92
- function visit(element: Element, parentId?: string) {
93
- const kind = element.localName as VectorElementKind;
94
- if (!vectorElementKinds.includes(kind))
95
- throw new Error(`Unsupported editable SVG element: ${element.localName}.`);
96
- const id = uniqueId(element.getAttribute("id") ?? "", used, elements.length + 1);
97
- used.add(id);
98
- const mixedText = (kind === "text" || kind === "tspan") &&
99
- Array.from({ length: element.childNodes.length }, (_, index) =>
100
- element.childNodes.item(index),
101
- ).some((node) => node?.nodeType === 1);
102
- const text = [...Array.from({ length: element.childNodes.length }, (_, index) =>
103
- element.childNodes.item(index),
104
- )]
105
- .filter((node) => node?.nodeType === 3)
106
- .map((node) => node?.nodeValue ?? "")
107
- .join("");
108
- elements.push({
109
- id,
110
- kind,
111
- ...(parentId ? { parentId } : {}),
112
- attributes: attributesOf(element),
113
- ...(!mixedText && text && (kind === "text" || kind === "tspan") ? { text } : {}),
114
- });
115
- for (let index = 0; index < element.childNodes.length; index += 1) {
116
- const child = element.childNodes.item(index);
117
- if (mixedText && child?.nodeType === 3 && child.nodeValue) {
118
- const textId = uniqueId("", used, elements.length + 1);
119
- used.add(textId);
120
- elements.push({ id: textId, kind: "tspan", parentId: id, attributes: {}, text: child.nodeValue });
121
- }
122
- if (child?.nodeType !== 1) continue;
123
- visit(child as unknown as Element, id);
124
- }
125
- }
126
-
127
- const rootAttributes = attributesOf(root as unknown as Element);
128
- let parentId: string | undefined;
129
- if (Object.keys(rootAttributes).length) {
130
- parentId = "vector-root";
131
- used.add(parentId);
132
- elements.push({ id: parentId, kind: "g", attributes: rootAttributes });
133
- }
134
- for (let index = 0; index < root.childNodes.length; index += 1) {
135
- const child = root.childNodes.item(index);
136
- if (child?.nodeType !== 1) continue;
137
- const name = (child as unknown as Element).localName;
138
- if (name === "title" || name === "desc") continue;
139
- visit(child as unknown as Element, parentId);
140
- }
141
- if (!elements.length) throw new Error("The SVG contains no editable elements.");
142
- return { viewBox: { x, y, width, height }, elements };
143
- }