konpeki 0.1.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.
- package/.agents/skills/authoring-visuals/SKILL.md +82 -0
- package/AGENTS.md +33 -0
- package/AUTHORING.md +233 -0
- package/LICENSE +201 -0
- package/README.md +70 -0
- package/SETUP.md +86 -0
- package/composition/README.md +166 -0
- package/composition/compile.ts +227 -0
- package/composition/document.ts +709 -0
- package/composition/schema.json +2992 -0
- package/composition/schema.ts +435 -0
- package/composition/theme-tokens.ts +16 -0
- package/composition/types.ts +286 -0
- package/composition/validate.ts +684 -0
- package/composition/vector.ts +143 -0
- package/composition/visualizations.ts +335 -0
- package/design/README.md +17 -0
- package/design/palettes/README.md +14 -0
- package/design/palettes/base.ts +14 -0
- package/design/palettes/candidates.ts +19 -0
- package/design/palettes/index.ts +78 -0
- package/design/review/color-theme.md +44 -0
- package/design/review/layout.md +16 -0
- package/design/review/text.md +18 -0
- package/design/review/typography.md +15 -0
- package/design/review/visuals.md +31 -0
- package/design/semantic-patterns.md +43 -0
- package/design/themes/README.md +22 -0
- package/design/themes/index.ts +13 -0
- package/design/visual-languages/technical-product.md +17 -0
- package/design/visual-review.md +88 -0
- package/docs/development.md +140 -0
- package/docs/workflow.md +61 -0
- package/index.html +17 -0
- package/lib/assets.d.ts +8 -0
- package/lib/charts.ts +18 -0
- package/lib/contrast.ts +16 -0
- package/lib/layouts.ts +50 -0
- package/lib/slide.tsx +42 -0
- package/lib/taste.ts +17 -0
- package/lib/text.tsx +89 -0
- package/lib/typeface.ts +44 -0
- package/package.json +85 -0
- package/runtime/konpeki.mjs +1685 -0
- package/scripts/migrate-react-page.ts +120 -0
- package/slides/README.md +153 -0
- package/slides/architecture/PROMPT.md +31 -0
- package/slides/architecture/index.tsx +102 -0
- package/slides/article-brief/PROMPT.md +35 -0
- package/slides/article-brief/index.tsx +71 -0
- package/slides/bar-chart/PROMPT.md +39 -0
- package/slides/bar-chart/index.tsx +97 -0
- package/slides/comparison/PROMPT.md +29 -0
- package/slides/comparison/index.tsx +95 -0
- package/slides/decision-memo/PROMPT.md +34 -0
- package/slides/decision-memo/index.tsx +85 -0
- package/slides/delivery-plan/PROMPT.md +45 -0
- package/slides/delivery-plan/index.tsx +105 -0
- package/slides/experiment/PROMPT.md +44 -0
- package/slides/experiment/index.tsx +127 -0
- package/slides/incident-workflow/PROMPT.md +57 -0
- package/slides/incident-workflow/index.tsx +78 -0
- package/slides/introducing-konpeki/PROMPT.md +19 -0
- package/slides/introducing-konpeki/README.md +54 -0
- package/slides/introducing-konpeki/SOURCE.md +20 -0
- package/slides/introducing-konpeki/author.ts +163 -0
- package/slides/introducing-konpeki/composition.json +3265 -0
- package/slides/line-chart/PROMPT.md +40 -0
- package/slides/line-chart/index.tsx +72 -0
- package/slides/migration/PROMPT.md +38 -0
- package/slides/migration/index.tsx +89 -0
- package/slides/og-images/PROMPT.md +21 -0
- package/slides/og-images/index.tsx +76 -0
- package/slides/product-introduction/PROMPT.md +24 -0
- package/slides/product-introduction/index.tsx +105 -0
- package/slides/research-brief/PROMPT.md +40 -0
- package/slides/research-brief/index.tsx +104 -0
- package/slides/results-explanation/PROMPT.md +32 -0
- package/slides/results-explanation/index.tsx +96 -0
- package/slides/retrospective/PROMPT.md +43 -0
- package/slides/retrospective/index.tsx +105 -0
- package/slides/sankey/PROMPT.md +11 -0
- package/slides/sankey/index.tsx +93 -0
- package/slides/teaching/PROMPT.md +45 -0
- package/slides/teaching/index.tsx +124 -0
- package/slides/vertical-bar-charts/PROMPT.md +13 -0
- package/slides/vertical-bar-charts/index.tsx +97 -0
- package/src/app/App.tsx +694 -0
- package/src/assets/konpeki-mark.png +0 -0
- package/src/components/Canvas.tsx +1168 -0
- package/src/components/DiagramTypeIcon.tsx +78 -0
- package/src/components/InspectorPanel.tsx +687 -0
- package/src/components/LeftPanel.tsx +107 -0
- package/src/components/PageSizePicker.tsx +30 -0
- package/src/components/Presentation.tsx +105 -0
- package/src/components/RightPanel.tsx +201 -0
- package/src/components/VectorOverflowWarning.tsx +46 -0
- package/src/components/WorkspaceChrome.tsx +199 -0
- package/src/components/ui.tsx +53 -0
- package/src/lib/examples/react-page-migration.json +1295 -0
- package/src/lib/examples.ts +42 -0
- package/src/lib/export-png.ts +101 -0
- package/src/lib/file-session.ts +74 -0
- package/src/lib/history.ts +53 -0
- package/src/lib/model.ts +188 -0
- package/src/lib/page-size.ts +24 -0
- package/src/lib/presentation.ts +17 -0
- package/src/lib/storage.ts +43 -0
- package/src/lib/theme.ts +25 -0
- package/src/lib/use-file-session.ts +162 -0
- package/src/main.tsx +26 -0
- package/src/styles/base.css +105 -0
- package/src/styles/canvas.css +268 -0
- package/src/styles/chrome.css +214 -0
- package/src/styles/component-previews.css +386 -0
- package/src/styles/feedback.css +71 -0
- package/src/styles/left-panel.css +125 -0
- package/src/styles/presentation.css +72 -0
- package/src/styles/right-panel.css +1172 -0
- package/src/styles/shell.css +247 -0
- package/vite.config.ts +6 -0
|
@@ -0,0 +1,1685 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createServer } from "vite";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
9
|
+
import "@xmldom/xmldom";
|
|
10
|
+
//#region composition/types.ts
|
|
11
|
+
var compositionSchema = "konpeki-composition/v18";
|
|
12
|
+
var legacyCompositionSchemas = [
|
|
13
|
+
"konpeki-composition/v1",
|
|
14
|
+
"konpeki-composition/v2",
|
|
15
|
+
"konpeki-composition/v3",
|
|
16
|
+
"konpeki-composition/v4",
|
|
17
|
+
"konpeki-composition/v5",
|
|
18
|
+
"konpeki-composition/v6",
|
|
19
|
+
"konpeki-composition/v7",
|
|
20
|
+
"konpeki-composition/v8",
|
|
21
|
+
"konpeki-composition/v9",
|
|
22
|
+
"konpeki-composition/v10",
|
|
23
|
+
"konpeki-composition/v11",
|
|
24
|
+
"konpeki-composition/v12",
|
|
25
|
+
"konpeki-composition/v13",
|
|
26
|
+
"konpeki-composition/v14",
|
|
27
|
+
"konpeki-composition/v15",
|
|
28
|
+
"konpeki-composition/v16",
|
|
29
|
+
"konpeki-composition/v17"
|
|
30
|
+
];
|
|
31
|
+
var canvasSize = {
|
|
32
|
+
width: 1920,
|
|
33
|
+
height: 1080
|
|
34
|
+
};
|
|
35
|
+
var canvasPadding = {
|
|
36
|
+
top: 72,
|
|
37
|
+
right: 112,
|
|
38
|
+
bottom: 0,
|
|
39
|
+
left: 112
|
|
40
|
+
};
|
|
41
|
+
var authoringModes = ["default", "dynamic"];
|
|
42
|
+
var themeIds = [
|
|
43
|
+
"plex",
|
|
44
|
+
"precision",
|
|
45
|
+
"editorial",
|
|
46
|
+
"blue-cyan",
|
|
47
|
+
"orange-coral",
|
|
48
|
+
"yellow",
|
|
49
|
+
"green",
|
|
50
|
+
"graphite"
|
|
51
|
+
];
|
|
52
|
+
var themeModes = ["paper", "night"];
|
|
53
|
+
var vectorElementKinds = [
|
|
54
|
+
"g",
|
|
55
|
+
"rect",
|
|
56
|
+
"circle",
|
|
57
|
+
"ellipse",
|
|
58
|
+
"line",
|
|
59
|
+
"polyline",
|
|
60
|
+
"polygon",
|
|
61
|
+
"path",
|
|
62
|
+
"text",
|
|
63
|
+
"tspan"
|
|
64
|
+
];
|
|
65
|
+
var componentKinds = [
|
|
66
|
+
"text-block",
|
|
67
|
+
"chart",
|
|
68
|
+
"diagram",
|
|
69
|
+
"image",
|
|
70
|
+
"table"
|
|
71
|
+
];
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region composition/visualizations.ts
|
|
74
|
+
var diagramDefinitions = {
|
|
75
|
+
architecture: {
|
|
76
|
+
label: "Architecture",
|
|
77
|
+
category: "Systems & technical structure",
|
|
78
|
+
layout: "hub-and-spoke",
|
|
79
|
+
slotRole: "entity",
|
|
80
|
+
expression: "Show system boundaries, responsibilities, interfaces, and directional communication without implying physical deployment."
|
|
81
|
+
},
|
|
82
|
+
"it-current-state": {
|
|
83
|
+
label: "IT current state",
|
|
84
|
+
category: "Systems & technical structure",
|
|
85
|
+
layout: "hub-and-spoke",
|
|
86
|
+
slotRole: "entity",
|
|
87
|
+
expression: "Show the systems that exist today, their ownership, integration paths, friction, duplication, and known constraints."
|
|
88
|
+
},
|
|
89
|
+
"high-level": {
|
|
90
|
+
label: "High-level overview",
|
|
91
|
+
category: "Systems & technical structure",
|
|
92
|
+
layout: "layered",
|
|
93
|
+
slotRole: "entity",
|
|
94
|
+
expression: "Reduce the system to a small number of named capabilities and major relationships; suppress implementation detail."
|
|
95
|
+
},
|
|
96
|
+
deployment: {
|
|
97
|
+
label: "Deployment",
|
|
98
|
+
category: "Systems & technical structure",
|
|
99
|
+
layout: "layered",
|
|
100
|
+
slotRole: "entity",
|
|
101
|
+
expression: "Place software artifacts inside the environments, hosts, zones, or runtimes where they execute and show cross-zone communication."
|
|
102
|
+
},
|
|
103
|
+
flowchart: {
|
|
104
|
+
label: "Flowchart",
|
|
105
|
+
category: "Flow, interaction & behavior",
|
|
106
|
+
layout: "branching",
|
|
107
|
+
slotRole: "process-step",
|
|
108
|
+
expression: "Distinguish actions, decisions, merge points, and start or end states; label decision branches explicitly."
|
|
109
|
+
},
|
|
110
|
+
sequence: {
|
|
111
|
+
label: "Sequence",
|
|
112
|
+
category: "Flow, interaction & behavior",
|
|
113
|
+
layout: "request-flow",
|
|
114
|
+
slotRole: "entity",
|
|
115
|
+
expression: "Order actors horizontally and messages vertically through time; distinguish calls, returns, asynchronous messages, and branches."
|
|
116
|
+
},
|
|
117
|
+
"state-machine": {
|
|
118
|
+
label: "State machine",
|
|
119
|
+
category: "Flow, interaction & behavior",
|
|
120
|
+
layout: "branching",
|
|
121
|
+
slotRole: "entity",
|
|
122
|
+
expression: "Treat nodes as legal states and edges as guarded transitions; preserve initial, terminal, retry, and invalid transition semantics."
|
|
123
|
+
},
|
|
124
|
+
swimlane: {
|
|
125
|
+
label: "Swimlane",
|
|
126
|
+
category: "Flow, interaction & behavior",
|
|
127
|
+
layout: "layered",
|
|
128
|
+
slotRole: "process-step",
|
|
129
|
+
expression: "Partition steps by responsible actor or team and make every cross-lane handoff visible."
|
|
130
|
+
},
|
|
131
|
+
process: {
|
|
132
|
+
label: "Process",
|
|
133
|
+
category: "Flow, interaction & behavior",
|
|
134
|
+
layout: "linear",
|
|
135
|
+
slotRole: "process-step",
|
|
136
|
+
expression: "Show ordered work from trigger to outcome, including branches, loops, owners, and exceptional paths when supplied."
|
|
137
|
+
},
|
|
138
|
+
"data-flow": {
|
|
139
|
+
label: "Data flow",
|
|
140
|
+
category: "Flow, interaction & behavior",
|
|
141
|
+
layout: "hub-and-spoke",
|
|
142
|
+
slotRole: "entity",
|
|
143
|
+
expression: "Show data producers, transformations, stores, and consumers; label payloads and direction independently from control flow."
|
|
144
|
+
},
|
|
145
|
+
"dp-integration": {
|
|
146
|
+
label: "Data-product integration",
|
|
147
|
+
category: "Flow, interaction & behavior",
|
|
148
|
+
layout: "request-flow",
|
|
149
|
+
slotRole: "entity",
|
|
150
|
+
expression: "Show producer and consumer responsibilities, contracts, integration boundaries, and exchange direction for each data product."
|
|
151
|
+
},
|
|
152
|
+
"er-data-model": {
|
|
153
|
+
label: "ER / data model",
|
|
154
|
+
category: "Data & software modeling",
|
|
155
|
+
layout: "hub-and-spoke",
|
|
156
|
+
slotRole: "entity",
|
|
157
|
+
expression: "Represent entities with key attributes and label relationship cardinality and optionality at both endpoints."
|
|
158
|
+
},
|
|
159
|
+
medallion: {
|
|
160
|
+
label: "Medallion",
|
|
161
|
+
category: "Data & software modeling",
|
|
162
|
+
layout: "layered",
|
|
163
|
+
slotRole: "entity",
|
|
164
|
+
expression: "Arrange data products by progressive quality tier and show transformations from raw through refined to consumption-ready data."
|
|
165
|
+
},
|
|
166
|
+
"dp-security-matrix": {
|
|
167
|
+
label: "Data-product security matrix",
|
|
168
|
+
category: "Data & software modeling",
|
|
169
|
+
layout: "layered",
|
|
170
|
+
slotRole: "entity",
|
|
171
|
+
expression: "Use rows and columns as explicit subject and resource axes; encode permissions or controls at their intersections."
|
|
172
|
+
},
|
|
173
|
+
"dependency-graph": {
|
|
174
|
+
label: "Dependency graph",
|
|
175
|
+
category: "Data & software modeling",
|
|
176
|
+
layout: "branching",
|
|
177
|
+
slotRole: "entity",
|
|
178
|
+
expression: "Preserve many-parent fan-in, shared dependencies, and cycles; orient every dependency edge consistently."
|
|
179
|
+
},
|
|
180
|
+
"uml-class": {
|
|
181
|
+
label: "UML class",
|
|
182
|
+
category: "Data & software modeling",
|
|
183
|
+
layout: "hub-and-spoke",
|
|
184
|
+
slotRole: "entity",
|
|
185
|
+
expression: "Keep class names, attributes, and operations in separate compartments and preserve inheritance, aggregation, and composition notation."
|
|
186
|
+
},
|
|
187
|
+
"database-schema": {
|
|
188
|
+
label: "Database schema",
|
|
189
|
+
category: "Data & software modeling",
|
|
190
|
+
layout: "hub-and-spoke",
|
|
191
|
+
slotRole: "entity",
|
|
192
|
+
expression: "Show tables with typed columns, keys, constraints, and column-level foreign-key endpoints."
|
|
193
|
+
},
|
|
194
|
+
nested: {
|
|
195
|
+
label: "Nested containment",
|
|
196
|
+
category: "Hierarchy, containment & organization",
|
|
197
|
+
layout: "layered",
|
|
198
|
+
slotRole: "entity",
|
|
199
|
+
expression: "Use enclosure to express scope and ownership; containment must not be replaced by proximity alone."
|
|
200
|
+
},
|
|
201
|
+
tree: {
|
|
202
|
+
label: "Tree",
|
|
203
|
+
category: "Hierarchy, containment & organization",
|
|
204
|
+
layout: "branching",
|
|
205
|
+
slotRole: "entity",
|
|
206
|
+
expression: "Show a strict one-parent hierarchy with consistent depth and sibling ordering."
|
|
207
|
+
},
|
|
208
|
+
"org-chart": {
|
|
209
|
+
label: "Org chart",
|
|
210
|
+
category: "Hierarchy, containment & organization",
|
|
211
|
+
layout: "branching",
|
|
212
|
+
slotRole: "entity",
|
|
213
|
+
expression: "Show reporting structure, role ownership, and escalation paths; do not imply technical dependency."
|
|
214
|
+
},
|
|
215
|
+
"layer-stack": {
|
|
216
|
+
label: "Layer stack",
|
|
217
|
+
category: "Hierarchy, containment & organization",
|
|
218
|
+
layout: "layered",
|
|
219
|
+
slotRole: "entity",
|
|
220
|
+
expression: "Order abstraction or responsibility layers consistently and show only legitimate cross-layer dependencies."
|
|
221
|
+
},
|
|
222
|
+
"pyramid-funnel": {
|
|
223
|
+
label: "Pyramid / funnel",
|
|
224
|
+
category: "Hierarchy, containment & organization",
|
|
225
|
+
layout: "layered",
|
|
226
|
+
slotRole: "entity",
|
|
227
|
+
expression: "Use width and vertical order to encode rank, concentration, or drop-off; state which meaning applies."
|
|
228
|
+
},
|
|
229
|
+
timeline: {
|
|
230
|
+
label: "Timeline",
|
|
231
|
+
category: "Time & work",
|
|
232
|
+
layout: "linear",
|
|
233
|
+
slotRole: "process-step",
|
|
234
|
+
expression: "Place dated events on an honest temporal axis and preserve meaningful interval differences."
|
|
235
|
+
},
|
|
236
|
+
gantt: {
|
|
237
|
+
label: "Gantt",
|
|
238
|
+
category: "Time & work",
|
|
239
|
+
layout: "layered",
|
|
240
|
+
slotRole: "process-step",
|
|
241
|
+
expression: "Encode task start, duration, overlap, milestones, dependencies, and ownership against a shared time scale."
|
|
242
|
+
},
|
|
243
|
+
kanban: {
|
|
244
|
+
label: "Kanban",
|
|
245
|
+
category: "Time & work",
|
|
246
|
+
layout: "layered",
|
|
247
|
+
slotRole: "entity",
|
|
248
|
+
expression: "Use columns as workflow states, preserve card ownership and priority, and show WIP limits when supplied."
|
|
249
|
+
},
|
|
250
|
+
"user-journey": {
|
|
251
|
+
label: "User journey",
|
|
252
|
+
category: "Time & work",
|
|
253
|
+
layout: "linear",
|
|
254
|
+
slotRole: "process-step",
|
|
255
|
+
expression: "Order experience stages and align actions, touchpoints, pain points, and sentiment to each stage."
|
|
256
|
+
},
|
|
257
|
+
"story-map": {
|
|
258
|
+
label: "Story map",
|
|
259
|
+
category: "Time & work",
|
|
260
|
+
layout: "layered",
|
|
261
|
+
slotRole: "entity",
|
|
262
|
+
expression: "Use columns for narrative activity order and rows for release slices; preserve the release-cut line."
|
|
263
|
+
},
|
|
264
|
+
quadrant: {
|
|
265
|
+
label: "Quadrant",
|
|
266
|
+
category: "Quantitative & analytical",
|
|
267
|
+
layout: "layered",
|
|
268
|
+
slotRole: "entity",
|
|
269
|
+
expression: "Position items against two named axes with meaningful scales and make quadrant interpretation explicit."
|
|
270
|
+
},
|
|
271
|
+
"loop-flywheel": {
|
|
272
|
+
label: "Loop / flywheel",
|
|
273
|
+
category: "Conceptual & strategic",
|
|
274
|
+
layout: "cyclical",
|
|
275
|
+
slotRole: "process-step",
|
|
276
|
+
expression: "Show a reinforcing cycle with explicit direction and explain how each stage increases momentum or feeds the next."
|
|
277
|
+
},
|
|
278
|
+
venn: {
|
|
279
|
+
label: "Venn",
|
|
280
|
+
category: "Conceptual & strategic",
|
|
281
|
+
layout: "hub-and-spoke",
|
|
282
|
+
slotRole: "entity",
|
|
283
|
+
expression: "Use overlapping regions to encode set membership and label every meaningful intersection."
|
|
284
|
+
},
|
|
285
|
+
fishbone: {
|
|
286
|
+
label: "Fishbone",
|
|
287
|
+
category: "Conceptual & strategic",
|
|
288
|
+
layout: "branching",
|
|
289
|
+
slotRole: "entity",
|
|
290
|
+
expression: "Group candidate causes by category along branches that converge on one clearly stated effect."
|
|
291
|
+
},
|
|
292
|
+
"wardley-map": {
|
|
293
|
+
label: "Wardley map",
|
|
294
|
+
category: "Conceptual & strategic",
|
|
295
|
+
layout: "layered",
|
|
296
|
+
slotRole: "entity",
|
|
297
|
+
expression: "Position components by user visibility and evolution, then show value-chain dependencies and movement without treating the map as runtime architecture."
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
var diagramTypes = Object.keys(diagramDefinitions);
|
|
301
|
+
var chartTemplates = Object.keys({
|
|
302
|
+
"grouped-bar": {
|
|
303
|
+
label: "Grouped bar",
|
|
304
|
+
expression: "Compare quantitative values across categories with a shared zero baseline; use grouped series only when each category has comparable values.",
|
|
305
|
+
source: "diagram-design"
|
|
306
|
+
},
|
|
307
|
+
line: {
|
|
308
|
+
label: "Line",
|
|
309
|
+
expression: "Encode values on a shared scale and connect observations in meaningful order, usually time; preserve intervals and do not imply missing measurements.",
|
|
310
|
+
source: "diagram-design"
|
|
311
|
+
},
|
|
312
|
+
pie: {
|
|
313
|
+
label: "Pie",
|
|
314
|
+
expression: "Show a small set of mutually exclusive parts of one whole; preserve the total and label values directly when comparison by angle is difficult.",
|
|
315
|
+
source: "konpeki"
|
|
316
|
+
},
|
|
317
|
+
"annotated-detail": {
|
|
318
|
+
label: "Annotated detail",
|
|
319
|
+
expression: "Focus on one visual detail and connect concise annotations to exact evidence without obscuring the underlying image or chart.",
|
|
320
|
+
source: "konpeki"
|
|
321
|
+
},
|
|
322
|
+
radar: {
|
|
323
|
+
label: "Radar",
|
|
324
|
+
expression: "Compare multivariate profiles on consistently normalized radial axes; preserve axis order, scale, and direction across every series.",
|
|
325
|
+
source: "diagram-design"
|
|
326
|
+
},
|
|
327
|
+
treemap: {
|
|
328
|
+
label: "Treemap",
|
|
329
|
+
expression: "Encode part-to-whole magnitude by rectangle area within explicit hierarchy; preserve parent totals and avoid implying rank from position.",
|
|
330
|
+
source: "diagram-design"
|
|
331
|
+
},
|
|
332
|
+
scatter: {
|
|
333
|
+
label: "Scatter",
|
|
334
|
+
expression: "Position observations by two measured quantitative variables; preserve axis scales, units, outliers, and uncertainty about correlation.",
|
|
335
|
+
source: "diagram-design"
|
|
336
|
+
},
|
|
337
|
+
sankey: {
|
|
338
|
+
label: "Sankey",
|
|
339
|
+
expression: "Encode conserved quantity through splits and merges using proportional ribbon width; preserve node totals, flow direction, and units.",
|
|
340
|
+
source: "diagram-design"
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
Array.from(new Set(Object.values(diagramDefinitions).map((definition) => definition.category)));
|
|
344
|
+
function migrateLegacyDiagramType(type, layout) {
|
|
345
|
+
if (typeof type !== "string" || typeof layout !== "string") return void 0;
|
|
346
|
+
return {
|
|
347
|
+
"process/linear": "process",
|
|
348
|
+
"process/branching": "flowchart",
|
|
349
|
+
"process/cyclical": "loop-flywheel",
|
|
350
|
+
"system/layered": "layer-stack",
|
|
351
|
+
"system/request-flow": "sequence",
|
|
352
|
+
"system/hub-and-spoke": "architecture"
|
|
353
|
+
}[`${type}/${layout}`];
|
|
354
|
+
}
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region composition/theme-tokens.ts
|
|
357
|
+
/** Explicit opt-in bindings. Literal SVG attributes always remain fixed. */
|
|
358
|
+
var colorTokens = [
|
|
359
|
+
"ink",
|
|
360
|
+
"muted",
|
|
361
|
+
"background",
|
|
362
|
+
"surface",
|
|
363
|
+
"divider",
|
|
364
|
+
"accent",
|
|
365
|
+
"on-accent",
|
|
366
|
+
"wash"
|
|
367
|
+
];
|
|
368
|
+
var fontTokens = ["heading-font", "body-font"];
|
|
369
|
+
function tokensForAttribute(name) {
|
|
370
|
+
return name === "font-family" ? fontTokens : [
|
|
371
|
+
"fill",
|
|
372
|
+
"stroke",
|
|
373
|
+
"color"
|
|
374
|
+
].includes(name) ? colorTokens : [];
|
|
375
|
+
}
|
|
376
|
+
function validThemeBinding(name, value) {
|
|
377
|
+
return typeof value !== "string" || !value.startsWith("theme:") || tokensForAttribute(name).some((token) => value === `theme:${token}`);
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region composition/schema.ts
|
|
381
|
+
var literalVectorAttribute = { oneOf: [{
|
|
382
|
+
type: "string",
|
|
383
|
+
maxLength: 1e4,
|
|
384
|
+
pattern: "^(?!theme:)"
|
|
385
|
+
}, { type: "number" }] };
|
|
386
|
+
var alignment = [
|
|
387
|
+
"start",
|
|
388
|
+
"center",
|
|
389
|
+
"end"
|
|
390
|
+
];
|
|
391
|
+
var titleStyle = ["plain", "prominent"];
|
|
392
|
+
var border = [
|
|
393
|
+
"none",
|
|
394
|
+
"outline",
|
|
395
|
+
"filled"
|
|
396
|
+
];
|
|
397
|
+
var colorScheme = [
|
|
398
|
+
"monochrome",
|
|
399
|
+
"accent-with-muted-context",
|
|
400
|
+
"categorical",
|
|
401
|
+
"sequential",
|
|
402
|
+
"status"
|
|
403
|
+
];
|
|
404
|
+
var emphasis = [
|
|
405
|
+
"none",
|
|
406
|
+
"primary",
|
|
407
|
+
"latest-series",
|
|
408
|
+
"exception"
|
|
409
|
+
];
|
|
410
|
+
var density = ["sparse", "standard"];
|
|
411
|
+
var appearanceOptions = {
|
|
412
|
+
"text-block": {
|
|
413
|
+
role: [
|
|
414
|
+
"title",
|
|
415
|
+
"subtitle",
|
|
416
|
+
"body",
|
|
417
|
+
"caption",
|
|
418
|
+
"footnote"
|
|
419
|
+
],
|
|
420
|
+
alignment,
|
|
421
|
+
border,
|
|
422
|
+
layout: [
|
|
423
|
+
"single",
|
|
424
|
+
"two-column",
|
|
425
|
+
"three-column",
|
|
426
|
+
"two-plus-two",
|
|
427
|
+
"four-column",
|
|
428
|
+
"one-plus-three",
|
|
429
|
+
"three-plus-one"
|
|
430
|
+
],
|
|
431
|
+
orientation: ["horizontal", "vertical"],
|
|
432
|
+
purpose: [
|
|
433
|
+
"narrative",
|
|
434
|
+
"comparison",
|
|
435
|
+
"emphasis"
|
|
436
|
+
],
|
|
437
|
+
treatment: [
|
|
438
|
+
"plain",
|
|
439
|
+
"subtle",
|
|
440
|
+
"strong"
|
|
441
|
+
],
|
|
442
|
+
logicalOrder: [
|
|
443
|
+
"none",
|
|
444
|
+
"parallel",
|
|
445
|
+
"progressive",
|
|
446
|
+
"cyclical",
|
|
447
|
+
"general-to-specific",
|
|
448
|
+
"hierarchical"
|
|
449
|
+
],
|
|
450
|
+
titleStyle,
|
|
451
|
+
rule: [
|
|
452
|
+
"none",
|
|
453
|
+
"top",
|
|
454
|
+
"bottom"
|
|
455
|
+
]
|
|
456
|
+
},
|
|
457
|
+
chart: {
|
|
458
|
+
template: chartTemplates,
|
|
459
|
+
selection: ["auto", "explicit"],
|
|
460
|
+
border,
|
|
461
|
+
colorScheme,
|
|
462
|
+
orientation: ["horizontal", "vertical"],
|
|
463
|
+
emphasis,
|
|
464
|
+
density,
|
|
465
|
+
legend: [
|
|
466
|
+
"none",
|
|
467
|
+
"top",
|
|
468
|
+
"right",
|
|
469
|
+
"bottom"
|
|
470
|
+
]
|
|
471
|
+
},
|
|
472
|
+
diagram: {
|
|
473
|
+
type: diagramTypes,
|
|
474
|
+
selection: ["auto", "explicit"],
|
|
475
|
+
border,
|
|
476
|
+
colorScheme,
|
|
477
|
+
emphasis,
|
|
478
|
+
density
|
|
479
|
+
},
|
|
480
|
+
image: {
|
|
481
|
+
border,
|
|
482
|
+
fit: ["cover", "contain"]
|
|
483
|
+
},
|
|
484
|
+
table: {
|
|
485
|
+
header: [
|
|
486
|
+
"row",
|
|
487
|
+
"none",
|
|
488
|
+
"column",
|
|
489
|
+
"both"
|
|
490
|
+
],
|
|
491
|
+
grid: [
|
|
492
|
+
"none",
|
|
493
|
+
"rows",
|
|
494
|
+
"all"
|
|
495
|
+
],
|
|
496
|
+
border,
|
|
497
|
+
colorScheme,
|
|
498
|
+
density
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
var text = {
|
|
502
|
+
type: "string",
|
|
503
|
+
minLength: 1,
|
|
504
|
+
pattern: "\\S"
|
|
505
|
+
};
|
|
506
|
+
var string = { type: "string" };
|
|
507
|
+
var enumeration = (values) => ({
|
|
508
|
+
type: "string",
|
|
509
|
+
enum: values
|
|
510
|
+
});
|
|
511
|
+
var object = (properties, required = Object.keys(properties)) => ({
|
|
512
|
+
type: "object",
|
|
513
|
+
properties,
|
|
514
|
+
required,
|
|
515
|
+
additionalProperties: false
|
|
516
|
+
});
|
|
517
|
+
var array = (items, minItems = 0) => ({
|
|
518
|
+
type: "array",
|
|
519
|
+
items,
|
|
520
|
+
minItems
|
|
521
|
+
});
|
|
522
|
+
var ids = {
|
|
523
|
+
...array(text, 1),
|
|
524
|
+
uniqueItems: true
|
|
525
|
+
};
|
|
526
|
+
var endpoint = object({
|
|
527
|
+
nodeId: text,
|
|
528
|
+
slotId: text
|
|
529
|
+
}, ["nodeId"]);
|
|
530
|
+
var rect = object({
|
|
531
|
+
x: {
|
|
532
|
+
type: "number",
|
|
533
|
+
minimum: 0,
|
|
534
|
+
maximum: 4096
|
|
535
|
+
},
|
|
536
|
+
y: {
|
|
537
|
+
type: "number",
|
|
538
|
+
minimum: 0,
|
|
539
|
+
maximum: 4096
|
|
540
|
+
},
|
|
541
|
+
width: {
|
|
542
|
+
type: "number",
|
|
543
|
+
exclusiveMinimum: 0,
|
|
544
|
+
maximum: 4096
|
|
545
|
+
},
|
|
546
|
+
height: {
|
|
547
|
+
type: "number",
|
|
548
|
+
exclusiveMinimum: 0,
|
|
549
|
+
maximum: 4096
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
var visualBase = {
|
|
553
|
+
viewBox: object({
|
|
554
|
+
x: { type: "number" },
|
|
555
|
+
y: { type: "number" },
|
|
556
|
+
width: {
|
|
557
|
+
type: "number",
|
|
558
|
+
exclusiveMinimum: 0,
|
|
559
|
+
maximum: 1e5
|
|
560
|
+
},
|
|
561
|
+
height: {
|
|
562
|
+
type: "number",
|
|
563
|
+
exclusiveMinimum: 0,
|
|
564
|
+
maximum: 1e5
|
|
565
|
+
}
|
|
566
|
+
}),
|
|
567
|
+
description: {
|
|
568
|
+
...text,
|
|
569
|
+
maxLength: 1e3
|
|
570
|
+
},
|
|
571
|
+
fit: enumeration([
|
|
572
|
+
"contain",
|
|
573
|
+
"cover",
|
|
574
|
+
"stretch"
|
|
575
|
+
])
|
|
576
|
+
};
|
|
577
|
+
var customVisual = { oneOf: [object({
|
|
578
|
+
format: { const: "vector" },
|
|
579
|
+
elements: {
|
|
580
|
+
...array(object({
|
|
581
|
+
id: text,
|
|
582
|
+
kind: enumeration(vectorElementKinds),
|
|
583
|
+
parentId: text,
|
|
584
|
+
attributes: {
|
|
585
|
+
type: "object",
|
|
586
|
+
maxProperties: 40,
|
|
587
|
+
properties: Object.fromEntries([
|
|
588
|
+
"fill",
|
|
589
|
+
"stroke",
|
|
590
|
+
"color",
|
|
591
|
+
"font-family"
|
|
592
|
+
].map((name) => [name, { anyOf: [literalVectorAttribute, { enum: tokensForAttribute(name).map((token) => `theme:${token}`) }] }])),
|
|
593
|
+
additionalProperties: literalVectorAttribute
|
|
594
|
+
},
|
|
595
|
+
text: {
|
|
596
|
+
type: "string",
|
|
597
|
+
maxLength: 1e4
|
|
598
|
+
}
|
|
599
|
+
}, [
|
|
600
|
+
"id",
|
|
601
|
+
"kind",
|
|
602
|
+
"attributes"
|
|
603
|
+
]), 0),
|
|
604
|
+
maxItems: 2e3
|
|
605
|
+
},
|
|
606
|
+
...visualBase
|
|
607
|
+
}, [
|
|
608
|
+
"format",
|
|
609
|
+
"elements",
|
|
610
|
+
"viewBox",
|
|
611
|
+
"description"
|
|
612
|
+
]), object({
|
|
613
|
+
format: { const: "svg" },
|
|
614
|
+
source: {
|
|
615
|
+
type: "string",
|
|
616
|
+
minLength: 1,
|
|
617
|
+
maxLength: 5e5,
|
|
618
|
+
pattern: "^\\s*<svg(?:\\s|>)"
|
|
619
|
+
},
|
|
620
|
+
...visualBase
|
|
621
|
+
}, [
|
|
622
|
+
"format",
|
|
623
|
+
"source",
|
|
624
|
+
"viewBox",
|
|
625
|
+
"description"
|
|
626
|
+
])] };
|
|
627
|
+
var topology = object({
|
|
628
|
+
kind: { const: "explicit" },
|
|
629
|
+
nodes: {
|
|
630
|
+
...array(object({
|
|
631
|
+
id: text,
|
|
632
|
+
slotId: text,
|
|
633
|
+
visible: { type: "boolean" },
|
|
634
|
+
preferredRect: rect,
|
|
635
|
+
primitive: { oneOf: [object({
|
|
636
|
+
kind: { const: "shape" },
|
|
637
|
+
shape: enumeration([
|
|
638
|
+
"rectangle",
|
|
639
|
+
"rounded-rectangle",
|
|
640
|
+
"circle",
|
|
641
|
+
"line"
|
|
642
|
+
]),
|
|
643
|
+
color: enumeration([
|
|
644
|
+
"ink",
|
|
645
|
+
"muted",
|
|
646
|
+
"accent"
|
|
647
|
+
]),
|
|
648
|
+
fill: enumeration([
|
|
649
|
+
"none",
|
|
650
|
+
"solid",
|
|
651
|
+
"wash"
|
|
652
|
+
])
|
|
653
|
+
}, ["kind", "shape"]), object({
|
|
654
|
+
kind: { const: "icon" },
|
|
655
|
+
name: text,
|
|
656
|
+
style: enumeration(["outline", "filled"]),
|
|
657
|
+
color: enumeration([
|
|
658
|
+
"ink",
|
|
659
|
+
"muted",
|
|
660
|
+
"accent"
|
|
661
|
+
])
|
|
662
|
+
}, ["kind"])] }
|
|
663
|
+
}, ["id", "slotId"]), 1),
|
|
664
|
+
maxItems: 24
|
|
665
|
+
},
|
|
666
|
+
edges: {
|
|
667
|
+
...array(object({
|
|
668
|
+
from: text,
|
|
669
|
+
to: text,
|
|
670
|
+
label: {
|
|
671
|
+
...text,
|
|
672
|
+
maxLength: 120
|
|
673
|
+
}
|
|
674
|
+
}, ["from", "to"]), 0),
|
|
675
|
+
maxItems: 48
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
var components = componentKinds.map((kind) => {
|
|
679
|
+
const options = appearanceOptions[kind];
|
|
680
|
+
const requiredAppearance = [
|
|
681
|
+
...kind === "chart" ? ["template"] : [],
|
|
682
|
+
...kind === "text-block" ? ["role"] : [],
|
|
683
|
+
...kind === "diagram" ? ["type"] : []
|
|
684
|
+
];
|
|
685
|
+
const component = object({
|
|
686
|
+
id: text,
|
|
687
|
+
kind: { const: kind },
|
|
688
|
+
preferredRect: rect,
|
|
689
|
+
slotIds: ids,
|
|
690
|
+
intent: string,
|
|
691
|
+
customVisual,
|
|
692
|
+
...kind === "text-block" ? {
|
|
693
|
+
content: {
|
|
694
|
+
type: "string",
|
|
695
|
+
maxLength: 5e4
|
|
696
|
+
},
|
|
697
|
+
textStyle: object({
|
|
698
|
+
size: {
|
|
699
|
+
type: "number",
|
|
700
|
+
minimum: 8,
|
|
701
|
+
maximum: 240
|
|
702
|
+
},
|
|
703
|
+
weight: { enum: [
|
|
704
|
+
400,
|
|
705
|
+
500,
|
|
706
|
+
600
|
|
707
|
+
] },
|
|
708
|
+
lineHeight: {
|
|
709
|
+
type: "number",
|
|
710
|
+
minimum: 1,
|
|
711
|
+
maximum: 3
|
|
712
|
+
},
|
|
713
|
+
color: enumeration([
|
|
714
|
+
"ink",
|
|
715
|
+
"muted",
|
|
716
|
+
"accent"
|
|
717
|
+
]),
|
|
718
|
+
font: enumeration(["heading", "body"])
|
|
719
|
+
}, [])
|
|
720
|
+
} : {},
|
|
721
|
+
appearance: object(Object.fromEntries(Object.entries(options).map(([key, values]) => [key, enumeration(values)])), requiredAppearance),
|
|
722
|
+
...kind === "diagram" || kind === "chart" ? { topology } : {}
|
|
723
|
+
}, [
|
|
724
|
+
"id",
|
|
725
|
+
"kind",
|
|
726
|
+
"preferredRect",
|
|
727
|
+
"slotIds",
|
|
728
|
+
...requiredAppearance.length ? ["appearance"] : []
|
|
729
|
+
]);
|
|
730
|
+
return kind === "chart" ? {
|
|
731
|
+
...component,
|
|
732
|
+
allOf: [{
|
|
733
|
+
if: { required: ["topology"] },
|
|
734
|
+
then: { properties: { appearance: { properties: { template: { const: "sankey" } } } } }
|
|
735
|
+
}]
|
|
736
|
+
} : component;
|
|
737
|
+
});
|
|
738
|
+
var schema = {
|
|
739
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
740
|
+
$id: "https://vcfgdev.github.io/konpeki/composition/v18/schema.json",
|
|
741
|
+
title: compositionSchema,
|
|
742
|
+
...object({
|
|
743
|
+
schema: { const: compositionSchema },
|
|
744
|
+
title: text,
|
|
745
|
+
authoringMode: enumeration(authoringModes),
|
|
746
|
+
theme: object({
|
|
747
|
+
id: enumeration(themeIds),
|
|
748
|
+
mode: enumeration(themeModes)
|
|
749
|
+
}),
|
|
750
|
+
slides: array(object({
|
|
751
|
+
id: text,
|
|
752
|
+
name: text,
|
|
753
|
+
canvas: object({
|
|
754
|
+
width: {
|
|
755
|
+
type: "integer",
|
|
756
|
+
minimum: 256,
|
|
757
|
+
maximum: 4096
|
|
758
|
+
},
|
|
759
|
+
height: {
|
|
760
|
+
type: "integer",
|
|
761
|
+
minimum: 256,
|
|
762
|
+
maximum: 4096
|
|
763
|
+
}
|
|
764
|
+
}),
|
|
765
|
+
innerPadding: object({
|
|
766
|
+
top: {
|
|
767
|
+
type: "number",
|
|
768
|
+
minimum: 0
|
|
769
|
+
},
|
|
770
|
+
right: {
|
|
771
|
+
type: "number",
|
|
772
|
+
minimum: 0
|
|
773
|
+
},
|
|
774
|
+
bottom: {
|
|
775
|
+
type: "number",
|
|
776
|
+
minimum: 0
|
|
777
|
+
},
|
|
778
|
+
left: {
|
|
779
|
+
type: "number",
|
|
780
|
+
minimum: 0
|
|
781
|
+
}
|
|
782
|
+
}),
|
|
783
|
+
pageNumber: object({
|
|
784
|
+
style: enumeration([
|
|
785
|
+
"none",
|
|
786
|
+
"01",
|
|
787
|
+
"01/02"
|
|
788
|
+
]),
|
|
789
|
+
color: enumeration([
|
|
790
|
+
"ink",
|
|
791
|
+
"muted",
|
|
792
|
+
"accent"
|
|
793
|
+
])
|
|
794
|
+
}),
|
|
795
|
+
audience: text,
|
|
796
|
+
question: text,
|
|
797
|
+
intendedViewingSize: enumeration([
|
|
798
|
+
"presentation",
|
|
799
|
+
"social",
|
|
800
|
+
"article",
|
|
801
|
+
"custom"
|
|
802
|
+
]),
|
|
803
|
+
contentSlots: array({ oneOf: [object({
|
|
804
|
+
id: text,
|
|
805
|
+
label: text,
|
|
806
|
+
required: { type: "boolean" },
|
|
807
|
+
instruction: text,
|
|
808
|
+
role: enumeration([
|
|
809
|
+
"takeaway",
|
|
810
|
+
"body",
|
|
811
|
+
"evidence",
|
|
812
|
+
"comparison-item",
|
|
813
|
+
"process-step",
|
|
814
|
+
"entity",
|
|
815
|
+
"image",
|
|
816
|
+
"table"
|
|
817
|
+
])
|
|
818
|
+
}), object({
|
|
819
|
+
id: text,
|
|
820
|
+
label: text,
|
|
821
|
+
required: { type: "boolean" },
|
|
822
|
+
instruction: text,
|
|
823
|
+
role: enumeration(["qualification", "source"]),
|
|
824
|
+
targets: ids
|
|
825
|
+
})] }, 0),
|
|
826
|
+
components: array({ oneOf: components }, 0),
|
|
827
|
+
groups: array(object({
|
|
828
|
+
id: text,
|
|
829
|
+
label: string,
|
|
830
|
+
childIds: ids
|
|
831
|
+
}, ["id", "childIds"])),
|
|
832
|
+
readingOrder: {
|
|
833
|
+
...array(object({
|
|
834
|
+
kind: enumeration(["component", "group"]),
|
|
835
|
+
id: text
|
|
836
|
+
}), 0),
|
|
837
|
+
uniqueItems: true
|
|
838
|
+
},
|
|
839
|
+
paintOrder: {
|
|
840
|
+
...array(text, 0),
|
|
841
|
+
uniqueItems: true
|
|
842
|
+
},
|
|
843
|
+
relationships: array(object({
|
|
844
|
+
id: text,
|
|
845
|
+
kind: enumeration([
|
|
846
|
+
"flows-to",
|
|
847
|
+
"qualifies",
|
|
848
|
+
"compares-with",
|
|
849
|
+
"depends-on",
|
|
850
|
+
"connects-to"
|
|
851
|
+
]),
|
|
852
|
+
direction: enumeration([
|
|
853
|
+
"forward",
|
|
854
|
+
"bidirectional",
|
|
855
|
+
"undirected"
|
|
856
|
+
]),
|
|
857
|
+
from: endpoint,
|
|
858
|
+
to: endpoint,
|
|
859
|
+
label: string
|
|
860
|
+
}, [
|
|
861
|
+
"id",
|
|
862
|
+
"kind",
|
|
863
|
+
"direction",
|
|
864
|
+
"from",
|
|
865
|
+
"to"
|
|
866
|
+
]))
|
|
867
|
+
}, [
|
|
868
|
+
"id",
|
|
869
|
+
"name",
|
|
870
|
+
"canvas",
|
|
871
|
+
"audience",
|
|
872
|
+
"question",
|
|
873
|
+
"intendedViewingSize",
|
|
874
|
+
"contentSlots",
|
|
875
|
+
"components",
|
|
876
|
+
"groups",
|
|
877
|
+
"readingOrder",
|
|
878
|
+
"paintOrder",
|
|
879
|
+
"relationships"
|
|
880
|
+
]), 1)
|
|
881
|
+
}, [
|
|
882
|
+
"schema",
|
|
883
|
+
"title",
|
|
884
|
+
"slides"
|
|
885
|
+
])
|
|
886
|
+
};
|
|
887
|
+
//#endregion
|
|
888
|
+
//#region composition/vector.ts
|
|
889
|
+
var vectorAttributeNames = /* @__PURE__ */ new Set([
|
|
890
|
+
"x",
|
|
891
|
+
"y",
|
|
892
|
+
"dx",
|
|
893
|
+
"dy",
|
|
894
|
+
"x1",
|
|
895
|
+
"y1",
|
|
896
|
+
"x2",
|
|
897
|
+
"y2",
|
|
898
|
+
"width",
|
|
899
|
+
"height",
|
|
900
|
+
"rx",
|
|
901
|
+
"ry",
|
|
902
|
+
"cx",
|
|
903
|
+
"cy",
|
|
904
|
+
"r",
|
|
905
|
+
"d",
|
|
906
|
+
"points",
|
|
907
|
+
"transform",
|
|
908
|
+
"fill",
|
|
909
|
+
"fill-opacity",
|
|
910
|
+
"stroke",
|
|
911
|
+
"stroke-width",
|
|
912
|
+
"stroke-opacity",
|
|
913
|
+
"stroke-linecap",
|
|
914
|
+
"stroke-linejoin",
|
|
915
|
+
"stroke-dasharray",
|
|
916
|
+
"opacity",
|
|
917
|
+
"font-family",
|
|
918
|
+
"font-size",
|
|
919
|
+
"font-weight",
|
|
920
|
+
"font-style",
|
|
921
|
+
"text-anchor",
|
|
922
|
+
"dominant-baseline",
|
|
923
|
+
"vector-effect"
|
|
924
|
+
]);
|
|
925
|
+
//#endregion
|
|
926
|
+
//#region composition/validate.ts
|
|
927
|
+
var structural = new Ajv2020({
|
|
928
|
+
allErrors: true,
|
|
929
|
+
strict: false
|
|
930
|
+
}).compile(schema);
|
|
931
|
+
function migratePreV5Document(input) {
|
|
932
|
+
const slide = input.slide;
|
|
933
|
+
const components = Array.isArray(slide?.components) ? slide.components.map((value) => {
|
|
934
|
+
if (!value || typeof value !== "object") return value;
|
|
935
|
+
const component = value;
|
|
936
|
+
if (component.kind !== "callout" && component.kind !== "comparison" && component.kind !== "text-block") return component;
|
|
937
|
+
const previous = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
|
|
938
|
+
const common = Object.fromEntries([
|
|
939
|
+
"alignment",
|
|
940
|
+
"border",
|
|
941
|
+
"titleStyle",
|
|
942
|
+
"rule"
|
|
943
|
+
].flatMap((key) => key in previous ? [[key, previous[key]]] : []));
|
|
944
|
+
const appearance = component.kind === "callout" ? {
|
|
945
|
+
...common,
|
|
946
|
+
layout: "single",
|
|
947
|
+
orientation: "horizontal",
|
|
948
|
+
purpose: "emphasis",
|
|
949
|
+
treatment: "strong"
|
|
950
|
+
} : component.kind === "comparison" ? {
|
|
951
|
+
...common,
|
|
952
|
+
layout: "two-column",
|
|
953
|
+
orientation: "horizontal",
|
|
954
|
+
purpose: "comparison",
|
|
955
|
+
treatment: "plain"
|
|
956
|
+
} : {
|
|
957
|
+
...previous,
|
|
958
|
+
purpose: previous.purpose ?? "narrative",
|
|
959
|
+
treatment: previous.treatment ?? "plain"
|
|
960
|
+
};
|
|
961
|
+
return {
|
|
962
|
+
...component,
|
|
963
|
+
kind: "text-block",
|
|
964
|
+
appearance
|
|
965
|
+
};
|
|
966
|
+
}) : slide?.components;
|
|
967
|
+
return {
|
|
968
|
+
...Object.fromEntries(Object.entries(input).filter(([key]) => key !== "target" && key !== "slide")),
|
|
969
|
+
schema: "konpeki-composition/v5",
|
|
970
|
+
...slide ? { slides: [{
|
|
971
|
+
...slide,
|
|
972
|
+
name: "Slide 01",
|
|
973
|
+
components
|
|
974
|
+
}] } : {}
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function migrateV5Document(input) {
|
|
978
|
+
const slides = Array.isArray(input.slides) ? input.slides.map((value) => {
|
|
979
|
+
if (!value || typeof value !== "object") return value;
|
|
980
|
+
const slide = value;
|
|
981
|
+
const previousComponents = Array.isArray(slide.components) ? slide.components : [];
|
|
982
|
+
const pageNumber = previousComponents.find((component) => component.kind === "page-number");
|
|
983
|
+
const pageAppearance = pageNumber?.appearance && typeof pageNumber.appearance === "object" ? pageNumber.appearance : {};
|
|
984
|
+
const removedIds = new Set(previousComponents.filter((component) => component.kind === "page-number").map((component) => String(component.id)));
|
|
985
|
+
const removedSlots = new Set(previousComponents.filter((component) => component.kind === "page-number").flatMap((component) => Array.isArray(component.slotIds) ? component.slotIds.map(String) : []));
|
|
986
|
+
const components = previousComponents.flatMap((component) => {
|
|
987
|
+
const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
|
|
988
|
+
if (component.kind === "page-number") return [];
|
|
989
|
+
if (component.kind === "evidence") return [{
|
|
990
|
+
...component,
|
|
991
|
+
kind: "visual"
|
|
992
|
+
}];
|
|
993
|
+
if (component.kind === "process") return [{
|
|
994
|
+
...component,
|
|
995
|
+
kind: "diagram",
|
|
996
|
+
appearance: {
|
|
997
|
+
...appearance,
|
|
998
|
+
type: "process"
|
|
999
|
+
}
|
|
1000
|
+
}];
|
|
1001
|
+
if (component.kind === "system-map") return [{
|
|
1002
|
+
...component,
|
|
1003
|
+
kind: "diagram",
|
|
1004
|
+
appearance: {
|
|
1005
|
+
...appearance,
|
|
1006
|
+
type: "system"
|
|
1007
|
+
}
|
|
1008
|
+
}];
|
|
1009
|
+
return [component];
|
|
1010
|
+
});
|
|
1011
|
+
const componentIds = new Set(components.map((component) => String(component.id)));
|
|
1012
|
+
const groups = Array.isArray(slide.groups) ? slide.groups.map((group) => ({
|
|
1013
|
+
...group,
|
|
1014
|
+
childIds: Array.isArray(group.childIds) ? group.childIds.filter((id) => !removedIds.has(String(id))) : group.childIds
|
|
1015
|
+
})).filter((group) => !Array.isArray(group.childIds) || group.childIds.length > 0) : slide.groups;
|
|
1016
|
+
const groupIds = new Set(Array.isArray(groups) ? groups.map((group) => String(group.id)) : []);
|
|
1017
|
+
const contentSlots = Array.isArray(slide.contentSlots) ? slide.contentSlots.filter((slot) => !removedSlots.has(String(slot.id))).map((slot) => {
|
|
1018
|
+
if (!Array.isArray(slot.targets)) return slot;
|
|
1019
|
+
const targets = slot.targets.filter((target) => !removedIds.has(String(target)) && !removedSlots.has(String(target)));
|
|
1020
|
+
return {
|
|
1021
|
+
...slot,
|
|
1022
|
+
targets: targets.length ? targets : [String(components[0]?.id ?? "")]
|
|
1023
|
+
};
|
|
1024
|
+
}) : slide.contentSlots;
|
|
1025
|
+
return {
|
|
1026
|
+
...slide,
|
|
1027
|
+
pageNumber: {
|
|
1028
|
+
style: pageNumber ? pageAppearance.style ?? "01" : "none",
|
|
1029
|
+
color: pageAppearance.color ?? "muted"
|
|
1030
|
+
},
|
|
1031
|
+
components,
|
|
1032
|
+
contentSlots,
|
|
1033
|
+
groups,
|
|
1034
|
+
readingOrder: Array.isArray(slide.readingOrder) ? slide.readingOrder.filter((entry) => entry.kind === "group" ? groupIds.has(String(entry.id)) : componentIds.has(String(entry.id))) : slide.readingOrder,
|
|
1035
|
+
paintOrder: Array.isArray(slide.paintOrder) ? slide.paintOrder.filter((id) => !removedIds.has(String(id))) : slide.paintOrder,
|
|
1036
|
+
relationships: Array.isArray(slide.relationships) ? slide.relationships.filter((relationship) => {
|
|
1037
|
+
const from = relationship.from;
|
|
1038
|
+
const to = relationship.to;
|
|
1039
|
+
return !removedIds.has(String(from?.nodeId)) && !removedIds.has(String(to?.nodeId));
|
|
1040
|
+
}) : slide.relationships
|
|
1041
|
+
};
|
|
1042
|
+
}) : input.slides;
|
|
1043
|
+
return {
|
|
1044
|
+
...input,
|
|
1045
|
+
schema: compositionSchema,
|
|
1046
|
+
slides
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
function migrateV7Document(input) {
|
|
1050
|
+
const slides = Array.isArray(input.slides) ? input.slides.map((value) => {
|
|
1051
|
+
if (!value || typeof value !== "object") return value;
|
|
1052
|
+
const slide = value;
|
|
1053
|
+
const components = Array.isArray(slide.components) ? slide.components.map((value) => {
|
|
1054
|
+
if (!value || typeof value !== "object") return value;
|
|
1055
|
+
const component = value;
|
|
1056
|
+
const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
|
|
1057
|
+
if (component.kind !== "visual" || appearance.template !== "image") return component;
|
|
1058
|
+
return {
|
|
1059
|
+
...component,
|
|
1060
|
+
kind: "image",
|
|
1061
|
+
appearance: {
|
|
1062
|
+
...appearance.border ? { border: appearance.border } : {},
|
|
1063
|
+
fit: "contain"
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
}) : slide.components;
|
|
1067
|
+
return {
|
|
1068
|
+
...slide,
|
|
1069
|
+
components
|
|
1070
|
+
};
|
|
1071
|
+
}) : input.slides;
|
|
1072
|
+
return {
|
|
1073
|
+
...input,
|
|
1074
|
+
schema: compositionSchema,
|
|
1075
|
+
slides
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function migrateV10Document(input) {
|
|
1079
|
+
const slides = Array.isArray(input.slides) ? input.slides.map((value) => {
|
|
1080
|
+
if (!value || typeof value !== "object") return value;
|
|
1081
|
+
const slide = value;
|
|
1082
|
+
const components = Array.isArray(slide.components) ? slide.components.map((value) => {
|
|
1083
|
+
if (!value || typeof value !== "object") return value;
|
|
1084
|
+
const component = value;
|
|
1085
|
+
const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
|
|
1086
|
+
if (component.kind === "headline") return {
|
|
1087
|
+
...component,
|
|
1088
|
+
kind: "text-block",
|
|
1089
|
+
appearance: {
|
|
1090
|
+
...appearance,
|
|
1091
|
+
role: "title",
|
|
1092
|
+
layout: "single",
|
|
1093
|
+
orientation: "horizontal",
|
|
1094
|
+
purpose: "narrative",
|
|
1095
|
+
treatment: "plain",
|
|
1096
|
+
logicalOrder: "none"
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
if (component.kind === "footnote") return {
|
|
1100
|
+
...component,
|
|
1101
|
+
kind: "text-block",
|
|
1102
|
+
appearance: {
|
|
1103
|
+
...appearance,
|
|
1104
|
+
role: "footnote",
|
|
1105
|
+
layout: "single",
|
|
1106
|
+
orientation: "horizontal",
|
|
1107
|
+
purpose: "narrative",
|
|
1108
|
+
treatment: "plain",
|
|
1109
|
+
logicalOrder: "none",
|
|
1110
|
+
titleStyle: "plain"
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
if (component.kind === "text-block") return {
|
|
1114
|
+
...component,
|
|
1115
|
+
appearance: {
|
|
1116
|
+
role: "body",
|
|
1117
|
+
...appearance
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
if (component.kind === "visual") return {
|
|
1121
|
+
...component,
|
|
1122
|
+
kind: "chart"
|
|
1123
|
+
};
|
|
1124
|
+
if (component.kind === "diagram") {
|
|
1125
|
+
const { template, ...rest } = appearance;
|
|
1126
|
+
return {
|
|
1127
|
+
...component,
|
|
1128
|
+
appearance: {
|
|
1129
|
+
...rest,
|
|
1130
|
+
layout: template ?? appearance.layout ?? "linear"
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
if (component.kind === "icon" || component.kind === "shape") {
|
|
1135
|
+
const slotIds = Array.isArray(component.slotIds) ? component.slotIds.map(String) : [];
|
|
1136
|
+
const slotId = slotIds[0] ?? `${String(component.id)}-content`;
|
|
1137
|
+
const primitive = component.kind === "icon" ? {
|
|
1138
|
+
kind: "icon",
|
|
1139
|
+
...typeof component.intent === "string" && component.intent.trim() ? { name: component.intent.trim() } : {},
|
|
1140
|
+
...appearance.style ? { style: appearance.style } : {},
|
|
1141
|
+
...appearance.color ? { color: appearance.color } : {}
|
|
1142
|
+
} : {
|
|
1143
|
+
kind: "shape",
|
|
1144
|
+
shape: appearance.shape ?? "rectangle",
|
|
1145
|
+
...appearance.color ? { color: appearance.color } : {},
|
|
1146
|
+
...appearance.fill ? { fill: appearance.fill } : {}
|
|
1147
|
+
};
|
|
1148
|
+
return {
|
|
1149
|
+
...component,
|
|
1150
|
+
kind: "diagram",
|
|
1151
|
+
slotIds: slotIds.length ? slotIds : [slotId],
|
|
1152
|
+
appearance: {
|
|
1153
|
+
type: "system",
|
|
1154
|
+
layout: "hub-and-spoke",
|
|
1155
|
+
border: appearance.border ?? "none",
|
|
1156
|
+
colorScheme: "accent-with-muted-context",
|
|
1157
|
+
emphasis: "primary",
|
|
1158
|
+
density: "sparse"
|
|
1159
|
+
},
|
|
1160
|
+
topology: {
|
|
1161
|
+
kind: "explicit",
|
|
1162
|
+
nodes: (slotIds.length ? slotIds : [slotId]).map((currentSlotId, index) => ({
|
|
1163
|
+
id: index ? `${String(component.id)}-slot-${index + 1}` : String(component.id),
|
|
1164
|
+
slotId: currentSlotId,
|
|
1165
|
+
...index ? { visible: false } : { primitive },
|
|
1166
|
+
...component.preferredRect && typeof component.preferredRect === "object" ? { preferredRect: component.preferredRect } : {}
|
|
1167
|
+
})),
|
|
1168
|
+
edges: []
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
return component;
|
|
1173
|
+
}) : slide.components;
|
|
1174
|
+
const contentSlots = Array.isArray(slide.contentSlots) ? slide.contentSlots.map((value) => {
|
|
1175
|
+
if (!value || typeof value !== "object") return value;
|
|
1176
|
+
const slot = value;
|
|
1177
|
+
return slot.role === "icon" || slot.role === "shape" ? {
|
|
1178
|
+
...slot,
|
|
1179
|
+
role: "entity"
|
|
1180
|
+
} : slot;
|
|
1181
|
+
}) : slide.contentSlots;
|
|
1182
|
+
return {
|
|
1183
|
+
...slide,
|
|
1184
|
+
components,
|
|
1185
|
+
contentSlots
|
|
1186
|
+
};
|
|
1187
|
+
}) : input.slides;
|
|
1188
|
+
return {
|
|
1189
|
+
...input,
|
|
1190
|
+
schema: "konpeki-composition/v11",
|
|
1191
|
+
slides
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
function migrateV11Document(input) {
|
|
1195
|
+
const slides = Array.isArray(input.slides) ? input.slides.map((value) => {
|
|
1196
|
+
if (!value || typeof value !== "object") return value;
|
|
1197
|
+
const slide = value;
|
|
1198
|
+
const components = Array.isArray(slide.components) ? slide.components.map((value) => {
|
|
1199
|
+
if (!value || typeof value !== "object") return value;
|
|
1200
|
+
const component = value;
|
|
1201
|
+
if (component.kind !== "diagram") return component;
|
|
1202
|
+
const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
|
|
1203
|
+
const migratedType = migrateLegacyDiagramType(appearance.type, appearance.layout);
|
|
1204
|
+
const { layout, ...rest } = appearance;
|
|
1205
|
+
return {
|
|
1206
|
+
...component,
|
|
1207
|
+
appearance: {
|
|
1208
|
+
...rest,
|
|
1209
|
+
type: migratedType ?? null
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
}) : slide.components;
|
|
1213
|
+
return {
|
|
1214
|
+
...slide,
|
|
1215
|
+
components
|
|
1216
|
+
};
|
|
1217
|
+
}) : input.slides;
|
|
1218
|
+
return {
|
|
1219
|
+
...input,
|
|
1220
|
+
schema: compositionSchema,
|
|
1221
|
+
slides
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
function migrateInterimV12Sankey(input) {
|
|
1225
|
+
if (!input || typeof input !== "object") return input;
|
|
1226
|
+
const document = input;
|
|
1227
|
+
if (document.schema !== "konpeki-composition/v18" || !Array.isArray(document.slides)) return input;
|
|
1228
|
+
const slides = document.slides.map((value) => {
|
|
1229
|
+
if (!value || typeof value !== "object") return value;
|
|
1230
|
+
const slide = value;
|
|
1231
|
+
if (!Array.isArray(slide.components)) return value;
|
|
1232
|
+
const components = slide.components.map((componentValue) => {
|
|
1233
|
+
if (!componentValue || typeof componentValue !== "object") return componentValue;
|
|
1234
|
+
const component = componentValue;
|
|
1235
|
+
const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
|
|
1236
|
+
if (component.kind !== "diagram" || appearance.type !== "sankey") return componentValue;
|
|
1237
|
+
const { type: _type, ...sharedAppearance } = appearance;
|
|
1238
|
+
return {
|
|
1239
|
+
...component,
|
|
1240
|
+
kind: "chart",
|
|
1241
|
+
appearance: {
|
|
1242
|
+
...sharedAppearance,
|
|
1243
|
+
template: "sankey"
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
});
|
|
1247
|
+
return {
|
|
1248
|
+
...slide,
|
|
1249
|
+
components
|
|
1250
|
+
};
|
|
1251
|
+
});
|
|
1252
|
+
return {
|
|
1253
|
+
...document,
|
|
1254
|
+
slides
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
function preserveLegacyVisualizationChoices(input) {
|
|
1258
|
+
if (!input || typeof input !== "object") return input;
|
|
1259
|
+
const document = input;
|
|
1260
|
+
if (!Array.isArray(document.slides)) return input;
|
|
1261
|
+
return {
|
|
1262
|
+
...document,
|
|
1263
|
+
slides: document.slides.map((value) => {
|
|
1264
|
+
if (!value || typeof value !== "object") return value;
|
|
1265
|
+
const slide = value;
|
|
1266
|
+
if (!Array.isArray(slide.components)) return value;
|
|
1267
|
+
return {
|
|
1268
|
+
...slide,
|
|
1269
|
+
components: slide.components.map((value) => {
|
|
1270
|
+
if (!value || typeof value !== "object") return value;
|
|
1271
|
+
const component = value;
|
|
1272
|
+
if (component.kind !== "diagram" && component.kind !== "chart" || !component.appearance || typeof component.appearance !== "object") return value;
|
|
1273
|
+
return {
|
|
1274
|
+
...component,
|
|
1275
|
+
appearance: {
|
|
1276
|
+
...component.appearance,
|
|
1277
|
+
selection: "explicit"
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
})
|
|
1281
|
+
};
|
|
1282
|
+
})
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
function validateComposition(input) {
|
|
1286
|
+
const legacy = Boolean(input && typeof input === "object" && "schema" in input && legacyCompositionSchemas.includes(input.schema));
|
|
1287
|
+
const legacyInput = input;
|
|
1288
|
+
const normalized = migrateInterimV12Sankey(legacy ? [
|
|
1289
|
+
"konpeki-composition/v12",
|
|
1290
|
+
"konpeki-composition/v13",
|
|
1291
|
+
"konpeki-composition/v14",
|
|
1292
|
+
"konpeki-composition/v15",
|
|
1293
|
+
"konpeki-composition/v16",
|
|
1294
|
+
"konpeki-composition/v17"
|
|
1295
|
+
].includes(String(legacyInput.schema)) ? {
|
|
1296
|
+
...legacyInput,
|
|
1297
|
+
schema: compositionSchema
|
|
1298
|
+
} : migrateV11Document(legacyInput.schema === "konpeki-composition/v11" ? legacyInput : migrateV10Document([
|
|
1299
|
+
"konpeki-composition/v6",
|
|
1300
|
+
"konpeki-composition/v7",
|
|
1301
|
+
"konpeki-composition/v8",
|
|
1302
|
+
"konpeki-composition/v9",
|
|
1303
|
+
"konpeki-composition/v10"
|
|
1304
|
+
].includes(String(legacyInput.schema)) ? migrateV7Document(legacyInput) : migrateV7Document(migrateV5Document(legacyInput.schema === "konpeki-composition/v5" ? legacyInput : migratePreV5Document(legacyInput))))) : input);
|
|
1305
|
+
const candidate = legacy ? preserveLegacyVisualizationChoices(normalized) : normalized;
|
|
1306
|
+
if (!structural(candidate)) return {
|
|
1307
|
+
ok: false,
|
|
1308
|
+
issues: (structural.errors ?? []).map((error) => ({
|
|
1309
|
+
path: error.instancePath,
|
|
1310
|
+
message: error.message ?? "Invalid value"
|
|
1311
|
+
}))
|
|
1312
|
+
};
|
|
1313
|
+
const issues = [];
|
|
1314
|
+
const fail = (path, message) => issues.push({
|
|
1315
|
+
path,
|
|
1316
|
+
message
|
|
1317
|
+
});
|
|
1318
|
+
const unique = (ids, path) => {
|
|
1319
|
+
if (new Set(ids).size !== ids.length) fail(path, "IDs must be unique");
|
|
1320
|
+
};
|
|
1321
|
+
unique(candidate.slides.map((slide) => slide.id), "/slides");
|
|
1322
|
+
candidate.slides.forEach((slide, slideIndex) => {
|
|
1323
|
+
const base = `/slides/${slideIndex}`;
|
|
1324
|
+
const padding = slide.innerPadding;
|
|
1325
|
+
if (padding && (padding.left + padding.right >= slide.canvas.width || padding.top + padding.bottom >= slide.canvas.height)) fail(`${base}/innerPadding`, "Padding leaves no page content area");
|
|
1326
|
+
const components = new Map(slide.components.map((item) => [item.id, item]));
|
|
1327
|
+
const slots = new Set(slide.contentSlots.map((item) => item.id));
|
|
1328
|
+
const groups = new Map(slide.groups.map((item) => [item.id, item]));
|
|
1329
|
+
unique([...components.keys(), ...groups.keys()], `${base}/groups`);
|
|
1330
|
+
unique(slide.components.map((item) => item.id), `${base}/components`);
|
|
1331
|
+
unique(slide.contentSlots.map((item) => item.id), `${base}/contentSlots`);
|
|
1332
|
+
unique(slide.groups.map((item) => item.id), `${base}/groups`);
|
|
1333
|
+
unique(slide.relationships.map((item) => item.id), `${base}/relationships`);
|
|
1334
|
+
const assigned = [];
|
|
1335
|
+
for (const component of slide.components) {
|
|
1336
|
+
const path = `${base}/components/${component.id}`;
|
|
1337
|
+
const r = component.preferredRect;
|
|
1338
|
+
if (r.x + r.width > slide.canvas.width || r.y + r.height > slide.canvas.height) fail(path, "Rectangle exceeds canvas");
|
|
1339
|
+
assigned.push(...component.slotIds);
|
|
1340
|
+
for (const id of component.slotIds) if (!slots.has(id)) fail(path, `Unknown slot: ${id}`);
|
|
1341
|
+
if (component.customVisual) {
|
|
1342
|
+
if (component.customVisual.format === "svg") {
|
|
1343
|
+
const source = component.customVisual.source;
|
|
1344
|
+
if (/<script\b|<foreignObject\b|<\?xml-stylesheet\b|<!DOCTYPE\b|\son[a-z]+\s*=|(?:href|src)\s*=\s*["'](?!data:|#)|url\(\s*["']?(?!data:|#)/i.test(source)) fail(path, "Custom SVG must be self-contained and cannot include scripts, foreign objects, event handlers, document declarations, or external resources");
|
|
1345
|
+
const sourceViewBox = source.match(/<svg\b[^>]*\bviewBox\s*=\s*(["'])([^"']+)\1/i)?.[2]?.trim().split(/[\s,]+/).map(Number);
|
|
1346
|
+
const declaredViewBox = component.customVisual.viewBox;
|
|
1347
|
+
if (sourceViewBox?.length !== 4 || sourceViewBox.some((value) => !Number.isFinite(value)) || sourceViewBox[0] !== declaredViewBox.x || sourceViewBox[1] !== declaredViewBox.y || sourceViewBox[2] !== declaredViewBox.width || sourceViewBox[3] !== declaredViewBox.height) fail(path, "Custom SVG root viewBox must match the declared local viewport");
|
|
1348
|
+
} else {
|
|
1349
|
+
const known = /* @__PURE__ */ new Set();
|
|
1350
|
+
const kinds = new Map(component.customVisual.elements.map((element) => [element.id, element.kind]));
|
|
1351
|
+
for (const element of component.customVisual.elements) {
|
|
1352
|
+
if (known.has(element.id)) fail(path, `Duplicate vector element ID: ${element.id}`);
|
|
1353
|
+
if (element.parentId && !known.has(element.parentId)) fail(path, `Vector parent must precede its child: ${element.parentId}`);
|
|
1354
|
+
const parentKind = element.parentId ? kinds.get(element.parentId) : void 0;
|
|
1355
|
+
if (element.parentId && parentKind !== "g" && !(element.kind === "tspan" && (parentKind === "text" || parentKind === "tspan"))) fail(path, "Vector children require a group or a text/tspan parent for tspans");
|
|
1356
|
+
if (element.kind === "tspan" && parentKind !== "text" && parentKind !== "tspan") fail(path, "Vector tspans require a text or tspan parent");
|
|
1357
|
+
if (element.text !== void 0 && element.kind !== "text" && element.kind !== "tspan") fail(path, "Only vector text and tspans may contain text");
|
|
1358
|
+
known.add(element.id);
|
|
1359
|
+
for (const [name, value] of Object.entries(element.attributes)) {
|
|
1360
|
+
if (!validThemeBinding(name, value)) fail(path, `Invalid theme binding for vector attribute: ${name}`);
|
|
1361
|
+
if (!vectorAttributeNames.has(name)) fail(path, `Unsupported vector attribute: ${name}`);
|
|
1362
|
+
if (typeof value === "string" && /url\s*\(|javascript:|data:/i.test(value)) fail(path, `Unsafe vector attribute value: ${name}`);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (component.kind === "chart" && component.topology && component.appearance.template !== "sankey") fail(path, "Only Sankey charts may define topology");
|
|
1368
|
+
if ((component.kind === "diagram" || component.kind === "chart") && component.topology) {
|
|
1369
|
+
const { nodes, edges } = component.topology;
|
|
1370
|
+
unique(nodes.map((node) => node.id), path);
|
|
1371
|
+
unique(nodes.map((node) => node.slotId), path);
|
|
1372
|
+
unique(edges.map((edge) => JSON.stringify([
|
|
1373
|
+
edge.from,
|
|
1374
|
+
edge.to,
|
|
1375
|
+
edge.label ?? ""
|
|
1376
|
+
])), path);
|
|
1377
|
+
const known = new Set(nodes.map((node) => node.id));
|
|
1378
|
+
if (nodes.length !== component.slotIds.length) fail(path, "Topology must represent every component slot");
|
|
1379
|
+
for (const node of nodes) {
|
|
1380
|
+
if (!component.slotIds.includes(node.slotId)) fail(path, "Topology slot must belong to its component");
|
|
1381
|
+
const nodeRect = node.preferredRect;
|
|
1382
|
+
if (nodeRect && (nodeRect.x < r.x || nodeRect.y < r.y || nodeRect.x + nodeRect.width > r.x + r.width || nodeRect.y + nodeRect.height > r.y + r.height)) fail(path, "Topology node rectangle must stay within its component");
|
|
1383
|
+
}
|
|
1384
|
+
for (const edge of edges) {
|
|
1385
|
+
if (!known.has(edge.from) || !known.has(edge.to)) fail(path, "Unknown topology endpoint");
|
|
1386
|
+
if (edge.from === edge.to) fail(path, "Topology self-edges are not supported");
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
for (const id of slots) if (!assigned.includes(id)) fail(`${base}/contentSlots`, `Unassigned content slot: ${id}`);
|
|
1391
|
+
const grouped = [];
|
|
1392
|
+
for (const group of slide.groups) for (const id of group.childIds) {
|
|
1393
|
+
grouped.push(id);
|
|
1394
|
+
if (!components.has(id)) fail(`${base}/groups`, "Groups may contain only components");
|
|
1395
|
+
}
|
|
1396
|
+
unique(grouped, `${base}/groups`);
|
|
1397
|
+
const reading = slide.readingOrder.flatMap((entry) => {
|
|
1398
|
+
if (entry.kind === "component") {
|
|
1399
|
+
if (!components.has(entry.id)) fail(`${base}/readingOrder`, "Unknown component");
|
|
1400
|
+
return [entry.id];
|
|
1401
|
+
}
|
|
1402
|
+
const group = groups.get(entry.id);
|
|
1403
|
+
if (!group) fail(`${base}/readingOrder`, "Unknown group");
|
|
1404
|
+
return group?.childIds ?? [];
|
|
1405
|
+
});
|
|
1406
|
+
for (const [path, order] of [["readingOrder", reading], ["paintOrder", slide.paintOrder]]) if (order.length !== components.size || new Set(order).size !== components.size || order.some((id) => !components.has(id))) fail(`${base}/${path}`, "Must contain every component exactly once");
|
|
1407
|
+
for (const slot of slide.contentSlots) if ("targets" in slot) {
|
|
1408
|
+
for (const id of slot.targets) if (!components.has(id) && !slots.has(id)) fail(`${base}/contentSlots`, "Unknown qualification/source target");
|
|
1409
|
+
}
|
|
1410
|
+
for (const relationship of slide.relationships) for (const endpoint of [relationship.from, relationship.to]) {
|
|
1411
|
+
const component = components.get(endpoint.nodeId);
|
|
1412
|
+
if (!component) fail(`${base}/relationships`, "Unknown relationship node");
|
|
1413
|
+
if (endpoint.slotId && !component?.slotIds.includes(endpoint.slotId)) fail(`${base}/relationships`, "Relationship slot must belong to its endpoint");
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
return issues.length ? {
|
|
1417
|
+
ok: false,
|
|
1418
|
+
issues
|
|
1419
|
+
} : {
|
|
1420
|
+
ok: true,
|
|
1421
|
+
document: legacy ? {
|
|
1422
|
+
...candidate,
|
|
1423
|
+
slides: candidate.slides.map((slide) => ({
|
|
1424
|
+
...slide,
|
|
1425
|
+
components: slide.components.map((component) => component.kind === "text-block" && !component.customVisual && component.content === void 0 ? {
|
|
1426
|
+
...component,
|
|
1427
|
+
content: component.intent ?? ""
|
|
1428
|
+
} : component)
|
|
1429
|
+
}))
|
|
1430
|
+
} : candidate
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
//#endregion
|
|
1434
|
+
//#region composition/compile.ts
|
|
1435
|
+
function canonicalJSON(input) {
|
|
1436
|
+
const sort = (value) => Array.isArray(value) ? value.map(sort) : value && typeof value === "object" ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => [key, sort(item)])) : value;
|
|
1437
|
+
return JSON.stringify(sort(input), null, 2);
|
|
1438
|
+
}
|
|
1439
|
+
canvasPadding.left, canvasPadding.left, canvasPadding.left, canvasSize.width - canvasPadding.left - canvasPadding.right, canvasPadding.left, canvasPadding.left;
|
|
1440
|
+
function parseCompositionJSON(raw) {
|
|
1441
|
+
try {
|
|
1442
|
+
const result = validateComposition(JSON.parse(raw));
|
|
1443
|
+
if (result.ok) return {
|
|
1444
|
+
ok: true,
|
|
1445
|
+
document: result.document
|
|
1446
|
+
};
|
|
1447
|
+
const issue = result.issues[0];
|
|
1448
|
+
return {
|
|
1449
|
+
ok: false,
|
|
1450
|
+
message: `${issue?.path || "Document"}: ${issue?.message || "Invalid composition"}`
|
|
1451
|
+
};
|
|
1452
|
+
} catch {
|
|
1453
|
+
return {
|
|
1454
|
+
ok: false,
|
|
1455
|
+
message: "The selected file is not valid JSON."
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
//#endregion
|
|
1460
|
+
//#region bin/session-store.ts
|
|
1461
|
+
function revisionFor(value) {
|
|
1462
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1463
|
+
}
|
|
1464
|
+
function requestFileFor(compositionPath) {
|
|
1465
|
+
const key = revisionFor(resolve(compositionPath));
|
|
1466
|
+
return join(tmpdir(), "konpeki", `${key}.request.json`);
|
|
1467
|
+
}
|
|
1468
|
+
async function readCompositionFile(compositionPath) {
|
|
1469
|
+
const raw = await readFile(compositionPath, "utf8");
|
|
1470
|
+
const parsed = parseCompositionJSON(raw);
|
|
1471
|
+
if (!parsed.ok) throw new Error(parsed.message);
|
|
1472
|
+
return {
|
|
1473
|
+
document: parsed.document,
|
|
1474
|
+
revision: revisionFor(raw),
|
|
1475
|
+
name: basename(compositionPath)
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
async function saveCompositionFile(compositionPath, expectedRevision, document) {
|
|
1479
|
+
const validation = validateComposition(document);
|
|
1480
|
+
if (!validation.ok) {
|
|
1481
|
+
const issue = validation.issues[0];
|
|
1482
|
+
throw Object.assign(/* @__PURE__ */ new Error(`${issue?.path || "document"} ${issue?.message || "is invalid"}`), { code: "INVALID_COMPOSITION" });
|
|
1483
|
+
}
|
|
1484
|
+
const currentRevision = revisionFor(await readFile(compositionPath, "utf8"));
|
|
1485
|
+
if (currentRevision !== expectedRevision) throw Object.assign(/* @__PURE__ */ new Error("The composition changed outside this browser."), {
|
|
1486
|
+
code: "REVISION_CONFLICT",
|
|
1487
|
+
revision: currentRevision
|
|
1488
|
+
});
|
|
1489
|
+
const value = `${canonicalJSON(document)}\n`;
|
|
1490
|
+
const temporary = join(dirname(compositionPath), `.${basename(compositionPath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
1491
|
+
await writeFile(temporary, value, {
|
|
1492
|
+
encoding: "utf8",
|
|
1493
|
+
flag: "wx"
|
|
1494
|
+
});
|
|
1495
|
+
try {
|
|
1496
|
+
await rename(temporary, compositionPath);
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
await rm(temporary, { force: true });
|
|
1499
|
+
throw error;
|
|
1500
|
+
}
|
|
1501
|
+
return { revision: revisionFor(value) };
|
|
1502
|
+
}
|
|
1503
|
+
async function writeBuildRequest(compositionPath, request) {
|
|
1504
|
+
const current = await readCompositionFile(compositionPath);
|
|
1505
|
+
if (current.revision !== request.revision) throw Object.assign(/* @__PURE__ */ new Error("Save the latest composition before requesting a build."), {
|
|
1506
|
+
code: "REVISION_CONFLICT",
|
|
1507
|
+
revision: current.revision
|
|
1508
|
+
});
|
|
1509
|
+
const output = requestFileFor(compositionPath);
|
|
1510
|
+
await mkdir(dirname(output), { recursive: true });
|
|
1511
|
+
const value = {
|
|
1512
|
+
schema: "konpeki-build-request/v1",
|
|
1513
|
+
compositionPath: resolve(compositionPath),
|
|
1514
|
+
revision: request.revision,
|
|
1515
|
+
instruction: request.instruction.trim(),
|
|
1516
|
+
slideId: request.slideId,
|
|
1517
|
+
...request.componentId ? { componentId: request.componentId } : {},
|
|
1518
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1519
|
+
};
|
|
1520
|
+
await writeFile(output, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
1521
|
+
return {
|
|
1522
|
+
output,
|
|
1523
|
+
request: value
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
//#endregion
|
|
1527
|
+
//#region bin/session-plugin.ts
|
|
1528
|
+
var maximumBodyBytes = 16777216;
|
|
1529
|
+
var route = "/__konpeki/session";
|
|
1530
|
+
function sendJSON(response, status, value) {
|
|
1531
|
+
response.writeHead(status, {
|
|
1532
|
+
"content-type": "application/json; charset=utf-8",
|
|
1533
|
+
"cache-control": "no-store"
|
|
1534
|
+
});
|
|
1535
|
+
response.end(JSON.stringify(value));
|
|
1536
|
+
}
|
|
1537
|
+
async function readJSON(request) {
|
|
1538
|
+
let size = 0;
|
|
1539
|
+
const chunks = [];
|
|
1540
|
+
for await (const chunk of request) {
|
|
1541
|
+
size += chunk.length;
|
|
1542
|
+
if (size > maximumBodyBytes) throw Object.assign(/* @__PURE__ */ new Error("Request body is too large."), { code: "BODY_TOO_LARGE" });
|
|
1543
|
+
chunks.push(chunk);
|
|
1544
|
+
}
|
|
1545
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1546
|
+
}
|
|
1547
|
+
function errorResponse(error) {
|
|
1548
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
1549
|
+
return {
|
|
1550
|
+
status: code === "REVISION_CONFLICT" ? 409 : code === "INVALID_COMPOSITION" ? 422 : code === "BODY_TOO_LARGE" ? 413 : error instanceof SyntaxError ? 400 : 500,
|
|
1551
|
+
body: {
|
|
1552
|
+
error: error instanceof Error ? error.message : "File-session request failed.",
|
|
1553
|
+
...error && typeof error === "object" && "revision" in error ? { revision: error.revision } : {}
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
function fileSessionPlugin({ compositionPath, token, onBuildRequest }) {
|
|
1558
|
+
return {
|
|
1559
|
+
name: "konpeki-file-session",
|
|
1560
|
+
enforce: "pre",
|
|
1561
|
+
configureServer(server) {
|
|
1562
|
+
server.middlewares.use(async (request, response, next) => {
|
|
1563
|
+
const url = new URL(request.url || "/", "http://localhost");
|
|
1564
|
+
if (!url.pathname.startsWith(route)) return next();
|
|
1565
|
+
if (request.headers["x-konpeki-session"] !== token) {
|
|
1566
|
+
sendJSON(response, 403, { error: "Invalid session token." });
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
try {
|
|
1570
|
+
if (url.pathname === route && request.method === "GET") {
|
|
1571
|
+
sendJSON(response, 200, await readCompositionFile(compositionPath));
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (url.pathname === route && request.method === "PUT") {
|
|
1575
|
+
const body = await readJSON(request);
|
|
1576
|
+
sendJSON(response, 200, await saveCompositionFile(compositionPath, body.revision, body.document));
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (url.pathname === `${route}/build` && request.method === "POST") {
|
|
1580
|
+
const body = await readJSON(request);
|
|
1581
|
+
if (typeof body.instruction !== "string" || !body.instruction.trim()) {
|
|
1582
|
+
sendJSON(response, 400, { error: "Describe what the agent should build." });
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
const result = await writeBuildRequest(compositionPath, body);
|
|
1586
|
+
onBuildRequest?.(result);
|
|
1587
|
+
sendJSON(response, 201, { ok: true });
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
sendJSON(response, 404, { error: "Unknown file-session endpoint." });
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
const responseError = errorResponse(error);
|
|
1593
|
+
sendJSON(response, responseError.status, responseError.body);
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
//#endregion
|
|
1600
|
+
//#region bin/konpeki.mjs
|
|
1601
|
+
var root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
1602
|
+
function usage() {
|
|
1603
|
+
console.error(`Usage:
|
|
1604
|
+
konpeki preview <composition.json> [--host <host>] [--port <port>]
|
|
1605
|
+
konpeki validate <composition.json>
|
|
1606
|
+
konpeki wait <composition.json>`);
|
|
1607
|
+
}
|
|
1608
|
+
function option(name, fallback) {
|
|
1609
|
+
const index = process.argv.indexOf(name);
|
|
1610
|
+
return index === -1 ? fallback : process.argv[index + 1];
|
|
1611
|
+
}
|
|
1612
|
+
async function preview(input) {
|
|
1613
|
+
const compositionPath = resolve(input);
|
|
1614
|
+
await readCompositionFile(compositionPath);
|
|
1615
|
+
const token = randomBytes(24).toString("base64url");
|
|
1616
|
+
const host = option("--host", "127.0.0.1");
|
|
1617
|
+
const port = Number(option("--port", "4318"));
|
|
1618
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Port must be an integer from 1 to 65535.");
|
|
1619
|
+
await rm(requestFileFor(compositionPath), { force: true });
|
|
1620
|
+
const server = await createServer({
|
|
1621
|
+
root,
|
|
1622
|
+
configFile: resolve(root, "vite.config.ts"),
|
|
1623
|
+
server: {
|
|
1624
|
+
host,
|
|
1625
|
+
port,
|
|
1626
|
+
strictPort: true
|
|
1627
|
+
},
|
|
1628
|
+
plugins: [fileSessionPlugin({
|
|
1629
|
+
compositionPath,
|
|
1630
|
+
token,
|
|
1631
|
+
onBuildRequest: ({ request }) => {
|
|
1632
|
+
console.log(`\nBuild requested for ${request.compositionPath}`);
|
|
1633
|
+
console.log(`Revision: ${request.revision}`);
|
|
1634
|
+
console.log(`Instruction: ${request.instruction}`);
|
|
1635
|
+
console.log(`Run: konpeki wait ${JSON.stringify(request.compositionPath)}\n`);
|
|
1636
|
+
}
|
|
1637
|
+
})]
|
|
1638
|
+
});
|
|
1639
|
+
await server.listen();
|
|
1640
|
+
const address = server.httpServer?.address();
|
|
1641
|
+
const actualPort = address && typeof address === "object" ? address.port : port;
|
|
1642
|
+
const displayHost = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|
|
1643
|
+
console.log(`Konpeki is editing ${compositionPath}`);
|
|
1644
|
+
console.log(`http://${displayHost}:${actualPort}/?session=${encodeURIComponent(token)}`);
|
|
1645
|
+
console.log(`Waiting for edits and Build it requests. Press Ctrl+C to stop.`);
|
|
1646
|
+
}
|
|
1647
|
+
async function validate(input) {
|
|
1648
|
+
const result = await readCompositionFile(resolve(input));
|
|
1649
|
+
console.log(`${result.name}: valid (${result.revision})`);
|
|
1650
|
+
}
|
|
1651
|
+
async function waitForRequest(input) {
|
|
1652
|
+
const requestPath = requestFileFor(resolve(input));
|
|
1653
|
+
console.log(`Waiting for a Build it request for ${resolve(input)}…`);
|
|
1654
|
+
for (;;) {
|
|
1655
|
+
try {
|
|
1656
|
+
await access(requestPath);
|
|
1657
|
+
const value = await readFile(requestPath, "utf8");
|
|
1658
|
+
await rm(requestPath, { force: true });
|
|
1659
|
+
process.stdout.write(value);
|
|
1660
|
+
return;
|
|
1661
|
+
} catch (error) {
|
|
1662
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
|
|
1663
|
+
}
|
|
1664
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 250));
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
var command = process.argv[2];
|
|
1668
|
+
var input = process.argv[3];
|
|
1669
|
+
if (!command || !input) {
|
|
1670
|
+
usage();
|
|
1671
|
+
process.exitCode = 1;
|
|
1672
|
+
} else try {
|
|
1673
|
+
if (command === "preview") await preview(input);
|
|
1674
|
+
else if (command === "validate") await validate(input);
|
|
1675
|
+
else if (command === "wait") await waitForRequest(input);
|
|
1676
|
+
else {
|
|
1677
|
+
usage();
|
|
1678
|
+
process.exitCode = 1;
|
|
1679
|
+
}
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
console.error(error instanceof Error ? error.message : error);
|
|
1682
|
+
process.exitCode = 1;
|
|
1683
|
+
}
|
|
1684
|
+
//#endregion
|
|
1685
|
+
export {};
|