konpeki 0.1.1 → 0.3.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.md +1 -1
- package/CONTRIBUTING.md +36 -0
- package/README.md +57 -21
- package/SECURITY.md +22 -0
- package/SETUP.md +112 -51
- package/composition/README.md +42 -52
- package/composition/compile.ts +2 -2
- package/composition/document.ts +2 -111
- package/composition/schema.json +12 -3
- package/composition/schema.ts +4 -2
- package/composition/types.ts +4 -21
- package/composition/validate.ts +3 -461
- package/composition/visualizations.ts +0 -16
- package/design/themes/README.md +25 -7
- package/design/themes/index.ts +15 -4
- package/docs/development.md +75 -11
- package/docs/workflow.md +60 -11
- package/index.html +17 -1
- package/package.json +10 -2
- package/plugin.json +22 -0
- package/public/og.png +0 -0
- package/runtime/konpeki.mjs +223 -473
- package/skills/konpeki/SKILL.md +186 -0
- package/skills/konpeki/assets/blank.json +23 -0
- package/skills/konpeki/scripts/ensure-runtime.mjs +69 -0
- package/skills/konpeki/scripts/prepare-document.mjs +40 -0
- package/slides/introducing-konpeki/PROMPT.md +21 -0
- package/slides/introducing-konpeki/README.md +52 -30
- package/slides/introducing-konpeki/SOURCE.md +16 -10
- package/slides/introducing-konpeki/author.ts +44 -42
- package/slides/introducing-konpeki/composition.json +1748 -1743
- package/src/app/App.tsx +179 -37
- package/src/components/BuildOrb.tsx +40 -0
- package/src/components/Canvas.tsx +18 -1
- package/src/components/InspectorPanel.tsx +130 -43
- package/src/components/LeftPanel.tsx +16 -3
- package/src/components/RevisionNotes.tsx +56 -0
- package/src/components/WorkspaceChrome.tsx +98 -11
- package/src/lib/examples/react-page-migration.json +1 -1
- package/src/lib/export-png.ts +4 -1
- package/src/lib/file-session.ts +14 -1
- package/src/lib/review.ts +26 -0
- package/src/lib/storage.ts +38 -10
- package/src/lib/use-file-session.ts +49 -13
- package/src/main.tsx +3 -0
- package/src/styles/base.css +16 -0
- package/src/styles/canvas.css +43 -12
- package/src/styles/chrome.css +174 -52
- package/src/styles/feedback.css +39 -4
- package/src/styles/left-panel.css +48 -7
- package/src/styles/right-panel.css +90 -47
- package/src/styles/shell.css +99 -53
- package/.agents/skills/authoring-visuals/SKILL.md +0 -82
package/composition/validate.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
2
|
import { schema } from "./schema.ts";
|
|
3
|
-
import { migrateLegacyDiagramType } from "./visualizations.ts";
|
|
4
3
|
import {
|
|
5
|
-
compositionSchema,
|
|
6
|
-
legacyCompositionSchemas,
|
|
7
4
|
type CompositionDocument,
|
|
8
5
|
} from "./types.ts";
|
|
9
6
|
import { vectorAttributeNames } from "./vector.ts";
|
|
@@ -17,461 +14,8 @@ const structural = new Ajv2020({
|
|
|
17
14
|
allErrors: true,
|
|
18
15
|
strict: false,
|
|
19
16
|
}).compile<CompositionDocument>(schema);
|
|
20
|
-
function migratePreV5Document(input: Record<string, unknown>) {
|
|
21
|
-
const slide = input.slide as Record<string, unknown> | undefined;
|
|
22
|
-
const components = Array.isArray(slide?.components)
|
|
23
|
-
? slide.components.map((value: unknown) => {
|
|
24
|
-
if (!value || typeof value !== "object") return value;
|
|
25
|
-
const component = value as Record<string, unknown>;
|
|
26
|
-
if (
|
|
27
|
-
component.kind !== "callout" &&
|
|
28
|
-
component.kind !== "comparison" &&
|
|
29
|
-
component.kind !== "text-block"
|
|
30
|
-
)
|
|
31
|
-
return component;
|
|
32
|
-
const previous =
|
|
33
|
-
component.appearance && typeof component.appearance === "object"
|
|
34
|
-
? (component.appearance as Record<string, unknown>)
|
|
35
|
-
: {};
|
|
36
|
-
const common = Object.fromEntries(
|
|
37
|
-
["alignment", "border", "titleStyle", "rule"].flatMap((key) =>
|
|
38
|
-
key in previous ? [[key, previous[key]]] : [],
|
|
39
|
-
),
|
|
40
|
-
);
|
|
41
|
-
const appearance =
|
|
42
|
-
component.kind === "callout"
|
|
43
|
-
? {
|
|
44
|
-
...common,
|
|
45
|
-
layout: "single",
|
|
46
|
-
orientation: "horizontal",
|
|
47
|
-
purpose: "emphasis",
|
|
48
|
-
treatment: "strong",
|
|
49
|
-
}
|
|
50
|
-
: component.kind === "comparison"
|
|
51
|
-
? {
|
|
52
|
-
...common,
|
|
53
|
-
layout: "two-column",
|
|
54
|
-
orientation: "horizontal",
|
|
55
|
-
purpose: "comparison",
|
|
56
|
-
treatment: "plain",
|
|
57
|
-
}
|
|
58
|
-
: {
|
|
59
|
-
...previous,
|
|
60
|
-
purpose: previous.purpose ?? "narrative",
|
|
61
|
-
treatment: previous.treatment ?? "plain",
|
|
62
|
-
};
|
|
63
|
-
return { ...component, kind: "text-block", appearance };
|
|
64
|
-
})
|
|
65
|
-
: slide?.components;
|
|
66
|
-
return {
|
|
67
|
-
...Object.fromEntries(
|
|
68
|
-
Object.entries(input).filter(
|
|
69
|
-
([key]) => key !== "target" && key !== "slide",
|
|
70
|
-
),
|
|
71
|
-
),
|
|
72
|
-
schema: "konpeki-composition/v5",
|
|
73
|
-
...(slide
|
|
74
|
-
? { slides: [{ ...slide, name: "Slide 01", components }] }
|
|
75
|
-
: {}),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
function migrateV5Document(input: Record<string, unknown>) {
|
|
79
|
-
const slides = Array.isArray(input.slides)
|
|
80
|
-
? input.slides.map((value: unknown) => {
|
|
81
|
-
if (!value || typeof value !== "object") return value;
|
|
82
|
-
const slide = value as Record<string, unknown>;
|
|
83
|
-
const previousComponents = Array.isArray(slide.components)
|
|
84
|
-
? (slide.components as Record<string, unknown>[])
|
|
85
|
-
: [];
|
|
86
|
-
const pageNumber = previousComponents.find(
|
|
87
|
-
(component) => component.kind === "page-number",
|
|
88
|
-
);
|
|
89
|
-
const pageAppearance =
|
|
90
|
-
pageNumber?.appearance && typeof pageNumber.appearance === "object"
|
|
91
|
-
? (pageNumber.appearance as Record<string, unknown>)
|
|
92
|
-
: {};
|
|
93
|
-
const removedIds = new Set(
|
|
94
|
-
previousComponents
|
|
95
|
-
.filter((component) => component.kind === "page-number")
|
|
96
|
-
.map((component) => String(component.id)),
|
|
97
|
-
);
|
|
98
|
-
const removedSlots = new Set(
|
|
99
|
-
previousComponents
|
|
100
|
-
.filter((component) => component.kind === "page-number")
|
|
101
|
-
.flatMap((component) =>
|
|
102
|
-
Array.isArray(component.slotIds)
|
|
103
|
-
? component.slotIds.map(String)
|
|
104
|
-
: [],
|
|
105
|
-
),
|
|
106
|
-
);
|
|
107
|
-
const components = previousComponents.flatMap((component) => {
|
|
108
|
-
const appearance =
|
|
109
|
-
component.appearance && typeof component.appearance === "object"
|
|
110
|
-
? (component.appearance as Record<string, unknown>)
|
|
111
|
-
: {};
|
|
112
|
-
if (component.kind === "page-number") return [];
|
|
113
|
-
if (component.kind === "evidence")
|
|
114
|
-
return [{ ...component, kind: "visual" }];
|
|
115
|
-
if (component.kind === "process")
|
|
116
|
-
return [
|
|
117
|
-
{
|
|
118
|
-
...component,
|
|
119
|
-
kind: "diagram",
|
|
120
|
-
appearance: { ...appearance, type: "process" },
|
|
121
|
-
},
|
|
122
|
-
];
|
|
123
|
-
if (component.kind === "system-map")
|
|
124
|
-
return [
|
|
125
|
-
{
|
|
126
|
-
...component,
|
|
127
|
-
kind: "diagram",
|
|
128
|
-
appearance: { ...appearance, type: "system" },
|
|
129
|
-
},
|
|
130
|
-
];
|
|
131
|
-
return [component];
|
|
132
|
-
});
|
|
133
|
-
const componentIds = new Set(components.map((component) => String(component.id)));
|
|
134
|
-
const groups = Array.isArray(slide.groups)
|
|
135
|
-
? (slide.groups as Record<string, unknown>[])
|
|
136
|
-
.map((group) => ({
|
|
137
|
-
...group,
|
|
138
|
-
childIds: Array.isArray(group.childIds)
|
|
139
|
-
? group.childIds.filter((id) => !removedIds.has(String(id)))
|
|
140
|
-
: group.childIds,
|
|
141
|
-
}))
|
|
142
|
-
.filter(
|
|
143
|
-
(group) =>
|
|
144
|
-
!Array.isArray(group.childIds) || group.childIds.length > 0,
|
|
145
|
-
)
|
|
146
|
-
: slide.groups;
|
|
147
|
-
const groupIds = new Set(
|
|
148
|
-
Array.isArray(groups)
|
|
149
|
-
? groups.map((group) => String(group.id))
|
|
150
|
-
: [],
|
|
151
|
-
);
|
|
152
|
-
const contentSlots = Array.isArray(slide.contentSlots)
|
|
153
|
-
? (slide.contentSlots as Record<string, unknown>[])
|
|
154
|
-
.filter((slot) => !removedSlots.has(String(slot.id)))
|
|
155
|
-
.map((slot) => {
|
|
156
|
-
if (!Array.isArray(slot.targets)) return slot;
|
|
157
|
-
const targets = slot.targets.filter(
|
|
158
|
-
(target) =>
|
|
159
|
-
!removedIds.has(String(target)) &&
|
|
160
|
-
!removedSlots.has(String(target)),
|
|
161
|
-
);
|
|
162
|
-
return {
|
|
163
|
-
...slot,
|
|
164
|
-
targets: targets.length
|
|
165
|
-
? targets
|
|
166
|
-
: [String(components[0]?.id ?? "")],
|
|
167
|
-
};
|
|
168
|
-
})
|
|
169
|
-
: slide.contentSlots;
|
|
170
|
-
return {
|
|
171
|
-
...slide,
|
|
172
|
-
pageNumber: {
|
|
173
|
-
style: pageNumber ? (pageAppearance.style ?? "01") : "none",
|
|
174
|
-
color: pageAppearance.color ?? "muted",
|
|
175
|
-
},
|
|
176
|
-
components,
|
|
177
|
-
contentSlots,
|
|
178
|
-
groups,
|
|
179
|
-
readingOrder: Array.isArray(slide.readingOrder)
|
|
180
|
-
? (slide.readingOrder as Record<string, unknown>[]).filter((entry) =>
|
|
181
|
-
entry.kind === "group"
|
|
182
|
-
? groupIds.has(String(entry.id))
|
|
183
|
-
: componentIds.has(String(entry.id)),
|
|
184
|
-
)
|
|
185
|
-
: slide.readingOrder,
|
|
186
|
-
paintOrder: Array.isArray(slide.paintOrder)
|
|
187
|
-
? slide.paintOrder.filter((id) => !removedIds.has(String(id)))
|
|
188
|
-
: slide.paintOrder,
|
|
189
|
-
relationships: Array.isArray(slide.relationships)
|
|
190
|
-
? (slide.relationships as Record<string, unknown>[]).filter(
|
|
191
|
-
(relationship) => {
|
|
192
|
-
const from = relationship.from as Record<string, unknown> | undefined;
|
|
193
|
-
const to = relationship.to as Record<string, unknown> | undefined;
|
|
194
|
-
return (
|
|
195
|
-
!removedIds.has(String(from?.nodeId)) &&
|
|
196
|
-
!removedIds.has(String(to?.nodeId))
|
|
197
|
-
);
|
|
198
|
-
},
|
|
199
|
-
)
|
|
200
|
-
: slide.relationships,
|
|
201
|
-
};
|
|
202
|
-
})
|
|
203
|
-
: input.slides;
|
|
204
|
-
return { ...input, schema: compositionSchema, slides };
|
|
205
|
-
}
|
|
206
|
-
function migrateV7Document(input: Record<string, unknown>) {
|
|
207
|
-
const slides = Array.isArray(input.slides)
|
|
208
|
-
? input.slides.map((value: unknown) => {
|
|
209
|
-
if (!value || typeof value !== "object") return value;
|
|
210
|
-
const slide = value as Record<string, unknown>;
|
|
211
|
-
const components = Array.isArray(slide.components)
|
|
212
|
-
? slide.components.map((value: unknown) => {
|
|
213
|
-
if (!value || typeof value !== "object") return value;
|
|
214
|
-
const component = value as Record<string, unknown>;
|
|
215
|
-
const appearance =
|
|
216
|
-
component.appearance && typeof component.appearance === "object"
|
|
217
|
-
? (component.appearance as Record<string, unknown>)
|
|
218
|
-
: {};
|
|
219
|
-
if (component.kind !== "visual" || appearance.template !== "image")
|
|
220
|
-
return component;
|
|
221
|
-
return {
|
|
222
|
-
...component,
|
|
223
|
-
kind: "image",
|
|
224
|
-
appearance: {
|
|
225
|
-
...(appearance.border ? { border: appearance.border } : {}),
|
|
226
|
-
fit: "contain",
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
})
|
|
230
|
-
: slide.components;
|
|
231
|
-
return { ...slide, components };
|
|
232
|
-
})
|
|
233
|
-
: input.slides;
|
|
234
|
-
return { ...input, schema: compositionSchema, slides };
|
|
235
|
-
}
|
|
236
|
-
function migrateV10Document(input: Record<string, unknown>) {
|
|
237
|
-
const slides = Array.isArray(input.slides)
|
|
238
|
-
? input.slides.map((value: unknown) => {
|
|
239
|
-
if (!value || typeof value !== "object") return value;
|
|
240
|
-
const slide = value as Record<string, unknown>;
|
|
241
|
-
const components = Array.isArray(slide.components)
|
|
242
|
-
? slide.components.map((value: unknown) => {
|
|
243
|
-
if (!value || typeof value !== "object") return value;
|
|
244
|
-
const component = value as Record<string, unknown>;
|
|
245
|
-
const appearance =
|
|
246
|
-
component.appearance && typeof component.appearance === "object"
|
|
247
|
-
? (component.appearance as Record<string, unknown>)
|
|
248
|
-
: {};
|
|
249
|
-
if (component.kind === "headline")
|
|
250
|
-
return {
|
|
251
|
-
...component,
|
|
252
|
-
kind: "text-block",
|
|
253
|
-
appearance: {
|
|
254
|
-
...appearance,
|
|
255
|
-
role: "title",
|
|
256
|
-
layout: "single",
|
|
257
|
-
orientation: "horizontal",
|
|
258
|
-
purpose: "narrative",
|
|
259
|
-
treatment: "plain",
|
|
260
|
-
logicalOrder: "none",
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
if (component.kind === "footnote")
|
|
264
|
-
return {
|
|
265
|
-
...component,
|
|
266
|
-
kind: "text-block",
|
|
267
|
-
appearance: {
|
|
268
|
-
...appearance,
|
|
269
|
-
role: "footnote",
|
|
270
|
-
layout: "single",
|
|
271
|
-
orientation: "horizontal",
|
|
272
|
-
purpose: "narrative",
|
|
273
|
-
treatment: "plain",
|
|
274
|
-
logicalOrder: "none",
|
|
275
|
-
titleStyle: "plain",
|
|
276
|
-
},
|
|
277
|
-
};
|
|
278
|
-
if (component.kind === "text-block")
|
|
279
|
-
return {
|
|
280
|
-
...component,
|
|
281
|
-
appearance: { role: "body", ...appearance },
|
|
282
|
-
};
|
|
283
|
-
if (component.kind === "visual")
|
|
284
|
-
return { ...component, kind: "chart" };
|
|
285
|
-
if (component.kind === "diagram") {
|
|
286
|
-
const { template, ...rest } = appearance;
|
|
287
|
-
return {
|
|
288
|
-
...component,
|
|
289
|
-
appearance: {
|
|
290
|
-
...rest,
|
|
291
|
-
layout: template ?? appearance.layout ?? "linear",
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
if (component.kind === "icon" || component.kind === "shape") {
|
|
296
|
-
const slotIds = Array.isArray(component.slotIds)
|
|
297
|
-
? component.slotIds.map(String)
|
|
298
|
-
: [];
|
|
299
|
-
const slotId = slotIds[0] ?? `${String(component.id)}-content`;
|
|
300
|
-
const primitive = component.kind === "icon"
|
|
301
|
-
? {
|
|
302
|
-
kind: "icon" as const,
|
|
303
|
-
...(typeof component.intent === "string" && component.intent.trim()
|
|
304
|
-
? { name: component.intent.trim() }
|
|
305
|
-
: {}),
|
|
306
|
-
...(appearance.style ? { style: appearance.style } : {}),
|
|
307
|
-
...(appearance.color ? { color: appearance.color } : {}),
|
|
308
|
-
}
|
|
309
|
-
: {
|
|
310
|
-
kind: "shape" as const,
|
|
311
|
-
shape: appearance.shape ?? "rectangle",
|
|
312
|
-
...(appearance.color ? { color: appearance.color } : {}),
|
|
313
|
-
...(appearance.fill ? { fill: appearance.fill } : {}),
|
|
314
|
-
};
|
|
315
|
-
return {
|
|
316
|
-
...component,
|
|
317
|
-
kind: "diagram",
|
|
318
|
-
slotIds: slotIds.length ? slotIds : [slotId],
|
|
319
|
-
appearance: {
|
|
320
|
-
type: "system",
|
|
321
|
-
layout: "hub-and-spoke",
|
|
322
|
-
border: appearance.border ?? "none",
|
|
323
|
-
colorScheme: "accent-with-muted-context",
|
|
324
|
-
emphasis: "primary",
|
|
325
|
-
density: "sparse",
|
|
326
|
-
},
|
|
327
|
-
topology: {
|
|
328
|
-
kind: "explicit",
|
|
329
|
-
nodes: (slotIds.length ? slotIds : [slotId]).map(
|
|
330
|
-
(currentSlotId, index) => ({
|
|
331
|
-
id: index
|
|
332
|
-
? `${String(component.id)}-slot-${index + 1}`
|
|
333
|
-
: String(component.id),
|
|
334
|
-
slotId: currentSlotId,
|
|
335
|
-
...(index ? { visible: false } : { primitive }),
|
|
336
|
-
...(component.preferredRect && typeof component.preferredRect === "object"
|
|
337
|
-
? { preferredRect: component.preferredRect }
|
|
338
|
-
: {}),
|
|
339
|
-
}),
|
|
340
|
-
),
|
|
341
|
-
edges: [],
|
|
342
|
-
},
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
return component;
|
|
346
|
-
})
|
|
347
|
-
: slide.components;
|
|
348
|
-
const contentSlots = Array.isArray(slide.contentSlots)
|
|
349
|
-
? slide.contentSlots.map((value: unknown) => {
|
|
350
|
-
if (!value || typeof value !== "object") return value;
|
|
351
|
-
const slot = value as Record<string, unknown>;
|
|
352
|
-
return slot.role === "icon" || slot.role === "shape"
|
|
353
|
-
? { ...slot, role: "entity" }
|
|
354
|
-
: slot;
|
|
355
|
-
})
|
|
356
|
-
: slide.contentSlots;
|
|
357
|
-
return { ...slide, components, contentSlots };
|
|
358
|
-
})
|
|
359
|
-
: input.slides;
|
|
360
|
-
return { ...input, schema: "konpeki-composition/v11", slides };
|
|
361
|
-
}
|
|
362
|
-
function migrateV11Document(input: Record<string, unknown>) {
|
|
363
|
-
const slides = Array.isArray(input.slides)
|
|
364
|
-
? input.slides.map((value: unknown) => {
|
|
365
|
-
if (!value || typeof value !== "object") return value;
|
|
366
|
-
const slide = value as Record<string, unknown>;
|
|
367
|
-
const components = Array.isArray(slide.components)
|
|
368
|
-
? slide.components.map((value: unknown) => {
|
|
369
|
-
if (!value || typeof value !== "object") return value;
|
|
370
|
-
const component = value as Record<string, unknown>;
|
|
371
|
-
if (component.kind !== "diagram") return component;
|
|
372
|
-
const appearance =
|
|
373
|
-
component.appearance && typeof component.appearance === "object"
|
|
374
|
-
? (component.appearance as Record<string, unknown>)
|
|
375
|
-
: {};
|
|
376
|
-
const migratedType = migrateLegacyDiagramType(
|
|
377
|
-
appearance.type,
|
|
378
|
-
appearance.layout,
|
|
379
|
-
);
|
|
380
|
-
const { layout, ...rest } = appearance;
|
|
381
|
-
return {
|
|
382
|
-
...component,
|
|
383
|
-
appearance: {
|
|
384
|
-
...rest,
|
|
385
|
-
type: migratedType ?? null,
|
|
386
|
-
},
|
|
387
|
-
};
|
|
388
|
-
})
|
|
389
|
-
: slide.components;
|
|
390
|
-
return { ...slide, components };
|
|
391
|
-
})
|
|
392
|
-
: input.slides;
|
|
393
|
-
return { ...input, schema: compositionSchema, slides };
|
|
394
|
-
}
|
|
395
|
-
function migrateInterimV12Sankey(input: unknown) {
|
|
396
|
-
if (!input || typeof input !== "object") return input;
|
|
397
|
-
const document = input as Record<string, unknown>;
|
|
398
|
-
if (document.schema !== compositionSchema || !Array.isArray(document.slides))
|
|
399
|
-
return input;
|
|
400
|
-
const slides = document.slides.map((value: unknown) => {
|
|
401
|
-
if (!value || typeof value !== "object") return value;
|
|
402
|
-
const slide = value as Record<string, unknown>;
|
|
403
|
-
if (!Array.isArray(slide.components)) return value;
|
|
404
|
-
const components = slide.components.map((componentValue: unknown) => {
|
|
405
|
-
if (!componentValue || typeof componentValue !== "object") return componentValue;
|
|
406
|
-
const component = componentValue as Record<string, unknown>;
|
|
407
|
-
const appearance = component.appearance && typeof component.appearance === "object"
|
|
408
|
-
? component.appearance as Record<string, unknown>
|
|
409
|
-
: {};
|
|
410
|
-
if (component.kind !== "diagram" || appearance.type !== "sankey")
|
|
411
|
-
return componentValue;
|
|
412
|
-
const { type: _type, ...sharedAppearance } = appearance;
|
|
413
|
-
return {
|
|
414
|
-
...component,
|
|
415
|
-
kind: "chart",
|
|
416
|
-
appearance: { ...sharedAppearance, template: "sankey" },
|
|
417
|
-
};
|
|
418
|
-
});
|
|
419
|
-
return { ...slide, components };
|
|
420
|
-
});
|
|
421
|
-
return { ...document, slides };
|
|
422
|
-
}
|
|
423
|
-
function preserveLegacyVisualizationChoices(input: unknown) {
|
|
424
|
-
if (!input || typeof input !== "object") return input;
|
|
425
|
-
const document = input as Record<string, unknown>;
|
|
426
|
-
if (!Array.isArray(document.slides)) return input;
|
|
427
|
-
return { ...document, slides: document.slides.map((value: unknown) => {
|
|
428
|
-
if (!value || typeof value !== "object") return value;
|
|
429
|
-
const slide = value as Record<string, unknown>;
|
|
430
|
-
if (!Array.isArray(slide.components)) return value;
|
|
431
|
-
return { ...slide, components: slide.components.map((value: unknown) => {
|
|
432
|
-
if (!value || typeof value !== "object") return value;
|
|
433
|
-
const component = value as Record<string, unknown>;
|
|
434
|
-
if ((component.kind !== "diagram" && component.kind !== "chart") || !component.appearance || typeof component.appearance !== "object") return value;
|
|
435
|
-
return { ...component, appearance: { ...component.appearance, selection: "explicit" } };
|
|
436
|
-
}) };
|
|
437
|
-
}) };
|
|
438
|
-
}
|
|
439
17
|
export function validateComposition(input: unknown): ValidationResult {
|
|
440
|
-
|
|
441
|
-
input &&
|
|
442
|
-
typeof input === "object" &&
|
|
443
|
-
"schema" in input &&
|
|
444
|
-
legacyCompositionSchemas.includes(
|
|
445
|
-
input.schema as (typeof legacyCompositionSchemas)[number],
|
|
446
|
-
),
|
|
447
|
-
);
|
|
448
|
-
const legacyInput = input as Record<string, unknown>;
|
|
449
|
-
const migrated = legacy
|
|
450
|
-
? ["konpeki-composition/v12", "konpeki-composition/v13", "konpeki-composition/v14", "konpeki-composition/v15", "konpeki-composition/v16", "konpeki-composition/v17"].includes(
|
|
451
|
-
String(legacyInput.schema),
|
|
452
|
-
)
|
|
453
|
-
? { ...legacyInput, schema: compositionSchema }
|
|
454
|
-
: migrateV11Document(
|
|
455
|
-
legacyInput.schema === "konpeki-composition/v11"
|
|
456
|
-
? legacyInput
|
|
457
|
-
: migrateV10Document(
|
|
458
|
-
["konpeki-composition/v6", "konpeki-composition/v7", "konpeki-composition/v8", "konpeki-composition/v9", "konpeki-composition/v10"].includes(
|
|
459
|
-
String(legacyInput.schema),
|
|
460
|
-
)
|
|
461
|
-
? migrateV7Document(legacyInput)
|
|
462
|
-
: migrateV7Document(
|
|
463
|
-
migrateV5Document(
|
|
464
|
-
legacyInput.schema === "konpeki-composition/v5"
|
|
465
|
-
? legacyInput
|
|
466
|
-
: migratePreV5Document(legacyInput),
|
|
467
|
-
),
|
|
468
|
-
),
|
|
469
|
-
),
|
|
470
|
-
)
|
|
471
|
-
: input;
|
|
472
|
-
const normalized = migrateInterimV12Sankey(migrated);
|
|
473
|
-
const candidate = legacy ? preserveLegacyVisualizationChoices(normalized) : normalized;
|
|
474
|
-
if (!structural(candidate))
|
|
18
|
+
if (!structural(input))
|
|
475
19
|
return {
|
|
476
20
|
ok: false,
|
|
477
21
|
issues: (structural.errors ?? []).map((error) => ({
|
|
@@ -479,6 +23,7 @@ export function validateComposition(input: unknown): ValidationResult {
|
|
|
479
23
|
message: error.message ?? "Invalid value",
|
|
480
24
|
})),
|
|
481
25
|
};
|
|
26
|
+
const candidate = input;
|
|
482
27
|
const issues: ValidationIssue[] = [];
|
|
483
28
|
const fail = (path: string, message: string) =>
|
|
484
29
|
issues.push({ path, message });
|
|
@@ -667,10 +212,7 @@ export function validateComposition(input: unknown): ValidationResult {
|
|
|
667
212
|
});
|
|
668
213
|
return issues.length
|
|
669
214
|
? { ok: false, issues }
|
|
670
|
-
: { ok: true, document:
|
|
671
|
-
...slide, components: slide.components.map((component) => component.kind === "text-block" && !component.customVisual && component.content === undefined
|
|
672
|
-
? { ...component, content: component.intent ?? "" } : component),
|
|
673
|
-
})) } : candidate };
|
|
215
|
+
: { ok: true, document: input };
|
|
674
216
|
}
|
|
675
217
|
export function assertComposition(input: unknown): CompositionDocument {
|
|
676
218
|
const result = validateComposition(input);
|
|
@@ -317,19 +317,3 @@ export const diagramCategories = Array.from(
|
|
|
317
317
|
export function diagramDefinition(type: DiagramType): DiagramDefinition {
|
|
318
318
|
return diagramDefinitions[type];
|
|
319
319
|
}
|
|
320
|
-
|
|
321
|
-
export function migrateLegacyDiagramType(
|
|
322
|
-
type: unknown,
|
|
323
|
-
layout: unknown,
|
|
324
|
-
): DiagramType | undefined {
|
|
325
|
-
if (typeof type !== "string" || typeof layout !== "string") return undefined;
|
|
326
|
-
const legacyTypes: Record<string, DiagramType> = {
|
|
327
|
-
"process/linear": "process",
|
|
328
|
-
"process/branching": "flowchart",
|
|
329
|
-
"process/cyclical": "loop-flywheel",
|
|
330
|
-
"system/layered": "layer-stack",
|
|
331
|
-
"system/request-flow": "sequence",
|
|
332
|
-
"system/hub-and-spoke": "architecture",
|
|
333
|
-
};
|
|
334
|
-
return legacyTypes[`${type}/${layout}`];
|
|
335
|
-
}
|
package/design/themes/README.md
CHANGED
|
@@ -1,21 +1,39 @@
|
|
|
1
1
|
# Themes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Document appearance has independent **Typography**, **Color palette**, and
|
|
4
|
+
**Background** controls. These do not choose a layout or change font sizes.
|
|
4
5
|
|
|
5
6
|
Use [authoring guidance](../../AUTHORING.md#taste-and-creative-freedom) for theme
|
|
6
7
|
selection, font defaults and deck-wide consistency.
|
|
7
8
|
|
|
8
9
|
```ts
|
|
9
10
|
import { getTheme } from './design/themes/index.ts';
|
|
10
|
-
const theme = getTheme('
|
|
11
|
+
const theme = getTheme('Green', 'paper', 'noto-sans');
|
|
11
12
|
// theme.body, theme.headline, theme.typography, theme.palette
|
|
12
13
|
```
|
|
13
14
|
|
|
14
|
-
Eight
|
|
15
|
-
Yellow, Green and Graphite.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
Eight palettes retain their existing names: Plex, Precision, Editorial,
|
|
16
|
+
Blue–cyan, Orange–coral, Yellow, Green and Graphite. Each supports Paper and Night.
|
|
17
|
+
Choose any palette with any of these typography presets:
|
|
18
|
+
|
|
19
|
+
| Typography ID | Headings | Body |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| `plex-sans` | IBM Plex Sans | IBM Plex Sans |
|
|
22
|
+
| `noto-sans` | Noto Sans | Noto Sans |
|
|
23
|
+
| `plex-serif` | IBM Plex Serif | IBM Plex Sans |
|
|
24
|
+
| `hanken-grotesk` | Hanken Grotesk | Hanken Grotesk |
|
|
25
|
+
|
|
26
|
+
Composition JSON stores the optional choice in `theme.typography`, independently
|
|
27
|
+
of `theme.id` (palette) and `theme.mode` (background). Omitted typography preserves
|
|
28
|
+
legacy appearance: Precision uses Noto Sans, Editorial uses Plex Serif headings
|
|
29
|
+
with Plex Sans body, and all other palettes use Plex Sans. Changing the palette
|
|
30
|
+
in the editor records the current typography so colors no longer change fonts.
|
|
31
|
+
Two-argument `getTheme` callers retain those legacy pairings too.
|
|
32
|
+
|
|
33
|
+
Font loading remains the author's responsibility; load the required Fontsource
|
|
34
|
+
Latin weights before measuring or rendering text. Theme-linked native text and
|
|
35
|
+
vectors follow the selection; literal font/color overrides remain fixed.
|
|
36
|
+
Different fonts can change wrapping or overflow, so inspect the result.
|
|
19
37
|
|
|
20
38
|
Mineral and Botanical are light-only palette candidates in
|
|
21
39
|
[palettes/candidates.ts](../palettes/candidates.ts).
|
package/design/themes/index.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { typography } from '../../lib/taste.ts';
|
|
2
2
|
import { paletteNames, resolvePalette, type PaletteName, type PaletteMode } from '../palettes/index.ts';
|
|
3
|
+
import type { TypographyId } from '../../composition/types.ts';
|
|
3
4
|
|
|
4
5
|
export const themeNames = paletteNames;
|
|
5
6
|
export type ThemeName = PaletteName;
|
|
6
7
|
export type ThemeMode = PaletteMode;
|
|
8
|
+
export const typographyLabels: Record<TypographyId, string> = {
|
|
9
|
+
'plex-sans': 'IBM Plex Sans',
|
|
10
|
+
'noto-sans': 'Noto Sans',
|
|
11
|
+
'plex-serif': 'Plex Serif + Plex Sans',
|
|
12
|
+
'hanken-grotesk': 'Hanken Grotesk',
|
|
13
|
+
};
|
|
7
14
|
|
|
8
15
|
// Themes combine typography with a palette; they do not choose a composition.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
// Omitted typography retains the original pairing for older documents/callers.
|
|
17
|
+
export function getTheme(name: ThemeName, mode: ThemeMode,
|
|
18
|
+
typographyId: TypographyId = name === 'Precision' ? 'noto-sans' : name === 'Editorial' ? 'plex-serif' : 'plex-sans',
|
|
19
|
+
) {
|
|
20
|
+
const body = typographyId === 'noto-sans' ? '"Noto Sans", sans-serif'
|
|
21
|
+
: typographyId === 'hanken-grotesk' ? '"Hanken Grotesk", sans-serif' : typography.family;
|
|
22
|
+
const headline = typographyId === 'plex-serif' ? '"IBM Plex Serif", serif' : body;
|
|
23
|
+
return { name, typographyId, body, headline, typography: { ...typography, family: body }, palette: resolvePalette(name, mode) };
|
|
13
24
|
}
|