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