@velumo/renderer-dom 0.1.0-beta.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/dist/background-renderer.d.ts +16 -0
- package/dist/background-renderer.d.ts.map +1 -0
- package/dist/background-renderer.js +41 -0
- package/dist/background-renderer.js.map +1 -0
- package/dist/camera-adapter.d.ts +16 -0
- package/dist/camera-adapter.d.ts.map +1 -0
- package/dist/camera-adapter.js +13 -0
- package/dist/camera-adapter.js.map +1 -0
- package/dist/default-theme.d.ts +5 -0
- package/dist/default-theme.d.ts.map +1 -0
- package/dist/default-theme.js +579 -0
- package/dist/default-theme.js.map +1 -0
- package/dist/dom-renderer.d.ts +62 -0
- package/dist/dom-renderer.d.ts.map +1 -0
- package/dist/dom-renderer.js +306 -0
- package/dist/dom-renderer.js.map +1 -0
- package/dist/html-sanitize.d.ts +18 -0
- package/dist/html-sanitize.d.ts.map +1 -0
- package/dist/html-sanitize.js +379 -0
- package/dist/html-sanitize.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/layout-renderer.d.ts +16 -0
- package/dist/layout-renderer.d.ts.map +1 -0
- package/dist/layout-renderer.js +450 -0
- package/dist/layout-renderer.js.map +1 -0
- package/dist/slide-thumbnail.d.ts +63 -0
- package/dist/slide-thumbnail.d.ts.map +1 -0
- package/dist/slide-thumbnail.js +93 -0
- package/dist/slide-thumbnail.js.map +1 -0
- package/dist/spatial-renderer.d.ts +18 -0
- package/dist/spatial-renderer.d.ts.map +1 -0
- package/dist/spatial-renderer.js +36 -0
- package/dist/spatial-renderer.js.map +1 -0
- package/dist/theme-fonts.d.ts +8 -0
- package/dist/theme-fonts.d.ts.map +1 -0
- package/dist/theme-fonts.js +68 -0
- package/dist/theme-fonts.js.map +1 -0
- package/dist/theme-tokens.d.ts +3 -0
- package/dist/theme-tokens.d.ts.map +1 -0
- package/dist/theme-tokens.js +43 -0
- package/dist/theme-tokens.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +20 -0
- package/src/background-renderer.ts +77 -0
- package/src/camera-adapter.ts +21 -0
- package/src/default-theme.ts +582 -0
- package/src/dom-renderer.ts +486 -0
- package/src/html-sanitize.ts +446 -0
- package/src/index.ts +25 -0
- package/src/layout-renderer.ts +721 -0
- package/src/slide-thumbnail.ts +146 -0
- package/src/spatial-renderer.ts +62 -0
- package/src/theme-fonts.ts +75 -0
- package/src/theme-tokens.ts +66 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
export interface LayoutRendererElement {
|
|
2
|
+
setAttribute(name: string, value: string): void;
|
|
3
|
+
appendChild(child: LayoutRendererElement): void;
|
|
4
|
+
readonly style?: { setProperty(name: string, value: string): void };
|
|
5
|
+
textContent: string;
|
|
6
|
+
parentNode?: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LayoutRendererFactories {
|
|
10
|
+
createElement(tagName: string): LayoutRendererElement;
|
|
11
|
+
createElementNS?(namespace: string, tagName: string): LayoutRendererElement;
|
|
12
|
+
setStyleProperty(
|
|
13
|
+
element: LayoutRendererElement,
|
|
14
|
+
property: string,
|
|
15
|
+
value: string,
|
|
16
|
+
): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function renderLayoutNode(
|
|
20
|
+
node: unknown,
|
|
21
|
+
factories: LayoutRendererFactories,
|
|
22
|
+
): LayoutRendererElement {
|
|
23
|
+
if (!isRecord(node) || typeof node.kind !== "string") {
|
|
24
|
+
return unknownLayoutElement(factories);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
switch (node.kind) {
|
|
28
|
+
case "stack":
|
|
29
|
+
return renderLayoutContainer(node, "stack", factories);
|
|
30
|
+
case "grid":
|
|
31
|
+
return renderGridLayout(node, factories);
|
|
32
|
+
case "split":
|
|
33
|
+
return renderSplitLayout(node, factories);
|
|
34
|
+
case "hero":
|
|
35
|
+
return renderLayoutContainer(node, "hero", factories);
|
|
36
|
+
case "frame":
|
|
37
|
+
return renderFrameLayout(node, factories);
|
|
38
|
+
case "heading":
|
|
39
|
+
return renderHeadingElement(node, factories);
|
|
40
|
+
case "text":
|
|
41
|
+
return renderTextElement(node, factories);
|
|
42
|
+
case "media":
|
|
43
|
+
return renderMediaElement(node, factories);
|
|
44
|
+
case "callout":
|
|
45
|
+
return renderCalloutElement(node, factories);
|
|
46
|
+
case "canvas":
|
|
47
|
+
return renderCanvasLayout(node, factories);
|
|
48
|
+
case "animate":
|
|
49
|
+
return renderAnimateLayout(node, factories);
|
|
50
|
+
case "code":
|
|
51
|
+
return renderCodeElement(node, factories);
|
|
52
|
+
case "icon":
|
|
53
|
+
return renderIconElement(node, factories);
|
|
54
|
+
case "badge":
|
|
55
|
+
return renderBadgeElement(node, factories);
|
|
56
|
+
case "rule":
|
|
57
|
+
return renderRuleElement(node, factories);
|
|
58
|
+
case "table":
|
|
59
|
+
return renderTableLayout(node, factories);
|
|
60
|
+
case "connector":
|
|
61
|
+
return renderConnectorElement(node, factories);
|
|
62
|
+
default:
|
|
63
|
+
return unknownLayoutElement(factories);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderLayoutContainer(
|
|
68
|
+
node: {
|
|
69
|
+
readonly children?: unknown;
|
|
70
|
+
readonly justify?: unknown;
|
|
71
|
+
readonly align?: unknown;
|
|
72
|
+
readonly direction?: unknown;
|
|
73
|
+
},
|
|
74
|
+
kind: string,
|
|
75
|
+
factories: LayoutRendererFactories,
|
|
76
|
+
): LayoutRendererElement {
|
|
77
|
+
const element = factories.createElement("div");
|
|
78
|
+
element.setAttribute("data-velumo-layout", kind);
|
|
79
|
+
if (
|
|
80
|
+
typeof (node as { readonly direction?: unknown }).direction === "string"
|
|
81
|
+
) {
|
|
82
|
+
element.setAttribute(
|
|
83
|
+
"data-velumo-stack-direction",
|
|
84
|
+
(node as { readonly direction: string }).direction,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (typeof (node as { readonly justify?: unknown }).justify === "string") {
|
|
88
|
+
element.setAttribute(
|
|
89
|
+
"data-velumo-stack-justify",
|
|
90
|
+
(node as { readonly justify: string }).justify,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (typeof (node as { readonly align?: unknown }).align === "string") {
|
|
94
|
+
element.setAttribute(
|
|
95
|
+
"data-velumo-stack-align",
|
|
96
|
+
(node as { readonly align: string }).align,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
appendLayoutChildren(element, node.children, factories);
|
|
100
|
+
return element;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderGridLayout(
|
|
104
|
+
node: {
|
|
105
|
+
readonly children?: unknown;
|
|
106
|
+
readonly columns?: unknown;
|
|
107
|
+
readonly rows?: unknown;
|
|
108
|
+
},
|
|
109
|
+
factories: LayoutRendererFactories,
|
|
110
|
+
): LayoutRendererElement {
|
|
111
|
+
const element = renderLayoutContainer(node, "grid", factories);
|
|
112
|
+
|
|
113
|
+
if (typeof node.columns === "number") {
|
|
114
|
+
factories.setStyleProperty(
|
|
115
|
+
element,
|
|
116
|
+
"--velumo-layout-columns",
|
|
117
|
+
String(node.columns),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof node.rows === "number") {
|
|
122
|
+
factories.setStyleProperty(
|
|
123
|
+
element,
|
|
124
|
+
"--velumo-layout-rows",
|
|
125
|
+
String(node.rows),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return element;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function renderSplitLayout(
|
|
133
|
+
node: {
|
|
134
|
+
readonly children?: unknown;
|
|
135
|
+
readonly weights?: unknown;
|
|
136
|
+
},
|
|
137
|
+
factories: LayoutRendererFactories,
|
|
138
|
+
): LayoutRendererElement {
|
|
139
|
+
const element = renderLayoutContainer(node, "split", factories);
|
|
140
|
+
|
|
141
|
+
if (Array.isArray(node.weights)) {
|
|
142
|
+
factories.setStyleProperty(
|
|
143
|
+
element,
|
|
144
|
+
"--velumo-layout-weights",
|
|
145
|
+
node.weights.join(" "),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return element;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderFrameLayout(
|
|
153
|
+
node: {
|
|
154
|
+
readonly children?: unknown;
|
|
155
|
+
readonly meta?: unknown;
|
|
156
|
+
readonly tone?: unknown;
|
|
157
|
+
},
|
|
158
|
+
factories: LayoutRendererFactories,
|
|
159
|
+
): LayoutRendererElement {
|
|
160
|
+
const element = renderLayoutContainer(node, "frame", factories);
|
|
161
|
+
|
|
162
|
+
if (typeof node.tone === "string") {
|
|
163
|
+
element.setAttribute("data-velumo-frame-tone", node.tone);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (isRecord(node.meta)) {
|
|
167
|
+
if (typeof node.meta.safeArea === "string") {
|
|
168
|
+
element.setAttribute("data-velumo-safe-area", node.meta.safeArea);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (typeof node.meta.cameraHint === "string") {
|
|
172
|
+
element.setAttribute("data-velumo-camera-hint", node.meta.cameraHint);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (typeof node.meta.depth === "number") {
|
|
176
|
+
factories.setStyleProperty(
|
|
177
|
+
element,
|
|
178
|
+
"--velumo-layout-depth",
|
|
179
|
+
String(node.meta.depth),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return element;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function applyTextStyleAttributes(
|
|
188
|
+
element: LayoutRendererElement,
|
|
189
|
+
node: {
|
|
190
|
+
readonly transform?: unknown;
|
|
191
|
+
readonly decoration?: unknown;
|
|
192
|
+
readonly glow?: unknown;
|
|
193
|
+
readonly tone?: unknown;
|
|
194
|
+
},
|
|
195
|
+
): void {
|
|
196
|
+
if (typeof node.transform === "string") {
|
|
197
|
+
element.setAttribute("data-velumo-text-transform", node.transform);
|
|
198
|
+
}
|
|
199
|
+
if (typeof node.decoration === "string") {
|
|
200
|
+
element.setAttribute("data-velumo-text-decoration", node.decoration);
|
|
201
|
+
}
|
|
202
|
+
if (node.glow === true) {
|
|
203
|
+
element.setAttribute("data-velumo-text-glow", "");
|
|
204
|
+
}
|
|
205
|
+
if (typeof node.tone === "string") {
|
|
206
|
+
element.setAttribute("data-velumo-text-tone", node.tone);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function renderHeadingElement(
|
|
211
|
+
node: {
|
|
212
|
+
readonly text?: unknown;
|
|
213
|
+
readonly size?: unknown;
|
|
214
|
+
readonly level?: unknown;
|
|
215
|
+
readonly transform?: unknown;
|
|
216
|
+
readonly decoration?: unknown;
|
|
217
|
+
readonly glow?: unknown;
|
|
218
|
+
readonly tone?: unknown;
|
|
219
|
+
},
|
|
220
|
+
factories: LayoutRendererFactories,
|
|
221
|
+
): LayoutRendererElement {
|
|
222
|
+
const level =
|
|
223
|
+
typeof node.level === "number" &&
|
|
224
|
+
Number.isInteger(node.level) &&
|
|
225
|
+
node.level >= 1 &&
|
|
226
|
+
node.level <= 6
|
|
227
|
+
? node.level
|
|
228
|
+
: 1;
|
|
229
|
+
const element = factories.createElement(`h${level}`);
|
|
230
|
+
element.textContent = String(node.text ?? "");
|
|
231
|
+
element.setAttribute("data-velumo-element", "heading");
|
|
232
|
+
if (typeof node.size === "string") {
|
|
233
|
+
element.setAttribute("data-velumo-heading-size", node.size);
|
|
234
|
+
}
|
|
235
|
+
applyTextStyleAttributes(element, node);
|
|
236
|
+
return element;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function renderTextElement(
|
|
240
|
+
node: {
|
|
241
|
+
readonly text?: unknown;
|
|
242
|
+
readonly variant?: unknown;
|
|
243
|
+
readonly transform?: unknown;
|
|
244
|
+
readonly decoration?: unknown;
|
|
245
|
+
readonly glow?: unknown;
|
|
246
|
+
readonly tone?: unknown;
|
|
247
|
+
},
|
|
248
|
+
factories: LayoutRendererFactories,
|
|
249
|
+
): LayoutRendererElement {
|
|
250
|
+
const element = factories.createElement("p");
|
|
251
|
+
element.textContent = String(node.text ?? "");
|
|
252
|
+
element.setAttribute("data-velumo-element", "text");
|
|
253
|
+
|
|
254
|
+
if (typeof node.variant === "string") {
|
|
255
|
+
element.setAttribute("data-velumo-text-variant", node.variant);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
applyTextStyleAttributes(element, node);
|
|
259
|
+
return element;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function renderMediaElement(
|
|
263
|
+
node: {
|
|
264
|
+
readonly src?: unknown;
|
|
265
|
+
readonly alt?: unknown;
|
|
266
|
+
readonly fit?: unknown;
|
|
267
|
+
},
|
|
268
|
+
factories: LayoutRendererFactories,
|
|
269
|
+
): LayoutRendererElement {
|
|
270
|
+
const element = factories.createElement("img");
|
|
271
|
+
element.setAttribute("data-velumo-element", "media");
|
|
272
|
+
element.setAttribute("src", String(node.src ?? ""));
|
|
273
|
+
element.setAttribute("alt", String(node.alt ?? ""));
|
|
274
|
+
|
|
275
|
+
if (typeof node.fit === "string") {
|
|
276
|
+
element.setAttribute("data-velumo-media-fit", node.fit);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return element;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderCalloutElement(
|
|
283
|
+
node: { readonly children?: unknown; readonly tone?: unknown },
|
|
284
|
+
factories: LayoutRendererFactories,
|
|
285
|
+
): LayoutRendererElement {
|
|
286
|
+
const element = factories.createElement("aside");
|
|
287
|
+
element.setAttribute("data-velumo-element", "callout");
|
|
288
|
+
|
|
289
|
+
if (typeof node.tone === "string") {
|
|
290
|
+
element.setAttribute("data-velumo-callout-tone", node.tone);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
appendLayoutChildren(element, node.children, factories);
|
|
294
|
+
return element;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function appendLayoutChildren(
|
|
298
|
+
parent: LayoutRendererElement,
|
|
299
|
+
children: unknown,
|
|
300
|
+
factories: LayoutRendererFactories,
|
|
301
|
+
): void {
|
|
302
|
+
if (!Array.isArray(children)) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const child of children) {
|
|
307
|
+
parent.appendChild(renderLayoutNode(child, factories));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function renderCanvasLayout(
|
|
312
|
+
node: { readonly items?: unknown },
|
|
313
|
+
factories: LayoutRendererFactories,
|
|
314
|
+
): LayoutRendererElement {
|
|
315
|
+
const element = factories.createElement("div");
|
|
316
|
+
element.setAttribute("data-velumo-layout", "canvas");
|
|
317
|
+
factories.setStyleProperty(element, "position", "relative");
|
|
318
|
+
factories.setStyleProperty(element, "flex", "1");
|
|
319
|
+
factories.setStyleProperty(element, "min-height", "0");
|
|
320
|
+
factories.setStyleProperty(element, "width", "100%");
|
|
321
|
+
// height:100% fills any definite-height parent (grid item, spatial world);
|
|
322
|
+
// in a flex column flex-basis:0% from flex:1 overrides height so the canvas
|
|
323
|
+
// still fills remaining space — no overlap regression since it is in-flow.
|
|
324
|
+
factories.setStyleProperty(element, "height", "100%");
|
|
325
|
+
|
|
326
|
+
if (Array.isArray(node.items)) {
|
|
327
|
+
for (const item of node.items) {
|
|
328
|
+
if (!isRecord(item)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const slot = factories.createElement("div");
|
|
332
|
+
slot.setAttribute("data-velumo-canvas-item", "");
|
|
333
|
+
factories.setStyleProperty(slot, "position", "absolute");
|
|
334
|
+
if (typeof item.x === "number") {
|
|
335
|
+
factories.setStyleProperty(slot, "left", `${item.x}%`);
|
|
336
|
+
}
|
|
337
|
+
if (typeof item.y === "number") {
|
|
338
|
+
factories.setStyleProperty(slot, "top", `${item.y}%`);
|
|
339
|
+
}
|
|
340
|
+
if (typeof item.width === "number") {
|
|
341
|
+
factories.setStyleProperty(slot, "width", `${item.width}%`);
|
|
342
|
+
}
|
|
343
|
+
if (typeof item.height === "number") {
|
|
344
|
+
factories.setStyleProperty(slot, "height", `${item.height}%`);
|
|
345
|
+
}
|
|
346
|
+
slot.appendChild(renderLayoutNode(item.child, factories));
|
|
347
|
+
element.appendChild(slot);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return element;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function renderAnimateLayout(
|
|
355
|
+
node: {
|
|
356
|
+
readonly name?: unknown;
|
|
357
|
+
readonly delay?: unknown;
|
|
358
|
+
readonly duration?: unknown;
|
|
359
|
+
readonly easing?: unknown;
|
|
360
|
+
readonly child?: unknown;
|
|
361
|
+
},
|
|
362
|
+
factories: LayoutRendererFactories,
|
|
363
|
+
): LayoutRendererElement {
|
|
364
|
+
const element = factories.createElement("div");
|
|
365
|
+
element.setAttribute("data-velumo-layout", "animate");
|
|
366
|
+
if (typeof node.name === "string") {
|
|
367
|
+
element.setAttribute("data-velumo-animation", node.name);
|
|
368
|
+
}
|
|
369
|
+
if (typeof node.delay === "number") {
|
|
370
|
+
factories.setStyleProperty(
|
|
371
|
+
element,
|
|
372
|
+
"--velumo-animation-delay",
|
|
373
|
+
`${node.delay}s`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (typeof node.duration === "number") {
|
|
377
|
+
factories.setStyleProperty(
|
|
378
|
+
element,
|
|
379
|
+
"--velumo-animation-duration",
|
|
380
|
+
`${node.duration}s`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (typeof node.easing === "string") {
|
|
384
|
+
factories.setStyleProperty(
|
|
385
|
+
element,
|
|
386
|
+
"--velumo-animation-easing",
|
|
387
|
+
node.easing,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
element.appendChild(renderLayoutNode(node.child, factories));
|
|
391
|
+
return element;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function renderCodeElement(
|
|
395
|
+
node: { readonly lines?: unknown },
|
|
396
|
+
factories: LayoutRendererFactories,
|
|
397
|
+
): LayoutRendererElement {
|
|
398
|
+
const element = factories.createElement("pre");
|
|
399
|
+
element.setAttribute("data-velumo-element", "code");
|
|
400
|
+
const lines = Array.isArray(node.lines) ? node.lines : [];
|
|
401
|
+
lines.forEach((line, lineIndex) => {
|
|
402
|
+
if (Array.isArray(line)) {
|
|
403
|
+
for (const tok of line) {
|
|
404
|
+
if (!isRecord(tok)) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const span = factories.createElement("span");
|
|
408
|
+
span.setAttribute(
|
|
409
|
+
"data-velumo-token",
|
|
410
|
+
typeof tok.kind === "string" ? tok.kind : "plain",
|
|
411
|
+
);
|
|
412
|
+
span.textContent = String(tok.text ?? "");
|
|
413
|
+
element.appendChild(span);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (lineIndex < lines.length - 1) {
|
|
417
|
+
const newline = factories.createElement("span");
|
|
418
|
+
newline.textContent = "\n";
|
|
419
|
+
element.appendChild(newline);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
return element;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function renderIconElement(
|
|
426
|
+
node: {
|
|
427
|
+
readonly name?: unknown;
|
|
428
|
+
readonly tone?: unknown;
|
|
429
|
+
readonly label?: unknown;
|
|
430
|
+
},
|
|
431
|
+
factories: LayoutRendererFactories,
|
|
432
|
+
): LayoutRendererElement {
|
|
433
|
+
const element = factories.createElement("span");
|
|
434
|
+
element.setAttribute("data-velumo-element", "icon");
|
|
435
|
+
element.setAttribute(
|
|
436
|
+
"data-velumo-icon",
|
|
437
|
+
typeof node.name === "string" ? node.name : "dot",
|
|
438
|
+
);
|
|
439
|
+
if (typeof node.tone === "string") {
|
|
440
|
+
element.setAttribute("data-velumo-text-tone", node.tone);
|
|
441
|
+
}
|
|
442
|
+
if (typeof node.label === "string" && node.label.length > 0) {
|
|
443
|
+
element.setAttribute("role", "img");
|
|
444
|
+
element.setAttribute("aria-label", node.label);
|
|
445
|
+
} else {
|
|
446
|
+
element.setAttribute("aria-hidden", "true");
|
|
447
|
+
}
|
|
448
|
+
return element;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function renderBadgeElement(
|
|
452
|
+
node: {
|
|
453
|
+
readonly text?: unknown;
|
|
454
|
+
readonly tone?: unknown;
|
|
455
|
+
readonly variant?: unknown;
|
|
456
|
+
readonly dot?: unknown;
|
|
457
|
+
readonly mono?: unknown;
|
|
458
|
+
},
|
|
459
|
+
factories: LayoutRendererFactories,
|
|
460
|
+
): LayoutRendererElement {
|
|
461
|
+
const element = factories.createElement("span");
|
|
462
|
+
element.setAttribute("data-velumo-element", "badge");
|
|
463
|
+
if (typeof node.tone === "string") {
|
|
464
|
+
element.setAttribute("data-velumo-badge-tone", node.tone);
|
|
465
|
+
}
|
|
466
|
+
if (typeof node.variant === "string") {
|
|
467
|
+
element.setAttribute("data-velumo-badge-variant", node.variant);
|
|
468
|
+
}
|
|
469
|
+
if (node.mono === true) {
|
|
470
|
+
element.setAttribute("data-velumo-badge-mono", "");
|
|
471
|
+
}
|
|
472
|
+
if (typeof node.dot === "string") {
|
|
473
|
+
const dot = factories.createElement("span");
|
|
474
|
+
dot.setAttribute("data-velumo-badge-dot", node.dot);
|
|
475
|
+
element.appendChild(dot);
|
|
476
|
+
}
|
|
477
|
+
const label = factories.createElement("span");
|
|
478
|
+
label.textContent = String(node.text ?? "");
|
|
479
|
+
element.appendChild(label);
|
|
480
|
+
return element;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function renderRuleElement(
|
|
484
|
+
node: {
|
|
485
|
+
readonly length?: unknown;
|
|
486
|
+
readonly thickness?: unknown;
|
|
487
|
+
readonly tone?: unknown;
|
|
488
|
+
readonly gradient?: unknown;
|
|
489
|
+
readonly radius?: unknown;
|
|
490
|
+
},
|
|
491
|
+
factories: LayoutRendererFactories,
|
|
492
|
+
): LayoutRendererElement {
|
|
493
|
+
const element = factories.createElement("span");
|
|
494
|
+
element.setAttribute("data-velumo-element", "rule");
|
|
495
|
+
const width =
|
|
496
|
+
typeof node.length === "number"
|
|
497
|
+
? `${node.length}px`
|
|
498
|
+
: typeof node.length === "string"
|
|
499
|
+
? node.length
|
|
500
|
+
: "100%";
|
|
501
|
+
const height =
|
|
502
|
+
typeof node.thickness === "number" ? `${node.thickness}px` : "2px";
|
|
503
|
+
const radius = typeof node.radius === "number" ? `${node.radius}px` : "999px";
|
|
504
|
+
factories.setStyleProperty(element, "width", width);
|
|
505
|
+
factories.setStyleProperty(element, "height", height);
|
|
506
|
+
factories.setStyleProperty(element, "border-radius", radius);
|
|
507
|
+
if (Array.isArray(node.gradient) && node.gradient.length === 2) {
|
|
508
|
+
const [from, to] = node.gradient;
|
|
509
|
+
element.setAttribute(
|
|
510
|
+
"data-velumo-rule-gradient",
|
|
511
|
+
`${String(from)} ${String(to)}`,
|
|
512
|
+
);
|
|
513
|
+
factories.setStyleProperty(
|
|
514
|
+
element,
|
|
515
|
+
"--velumo-rule-from",
|
|
516
|
+
`var(--velumo-tone-${String(from)})`,
|
|
517
|
+
);
|
|
518
|
+
factories.setStyleProperty(
|
|
519
|
+
element,
|
|
520
|
+
"--velumo-rule-to",
|
|
521
|
+
`var(--velumo-tone-${String(to)})`,
|
|
522
|
+
);
|
|
523
|
+
} else if (typeof node.tone === "string") {
|
|
524
|
+
element.setAttribute("data-velumo-rule-tone", node.tone);
|
|
525
|
+
}
|
|
526
|
+
return element;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function renderTableLayout(
|
|
530
|
+
node: { readonly columns?: unknown; readonly rows?: unknown },
|
|
531
|
+
factories: LayoutRendererFactories,
|
|
532
|
+
): LayoutRendererElement {
|
|
533
|
+
const columns = Array.isArray(node.columns) ? node.columns : [];
|
|
534
|
+
const element = factories.createElement("div");
|
|
535
|
+
element.setAttribute("data-velumo-layout", "table");
|
|
536
|
+
element.setAttribute("role", "table");
|
|
537
|
+
const weights = columns
|
|
538
|
+
.map((c) => (isRecord(c) && typeof c.weight === "number" ? c.weight : 1))
|
|
539
|
+
.join("fr ");
|
|
540
|
+
factories.setStyleProperty(
|
|
541
|
+
element,
|
|
542
|
+
"--velumo-table-columns",
|
|
543
|
+
columns.length > 0 ? `${weights}fr` : "1fr",
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const applyColAttrs = (
|
|
547
|
+
cell: LayoutRendererElement,
|
|
548
|
+
column: unknown,
|
|
549
|
+
): void => {
|
|
550
|
+
if (!isRecord(column)) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (column.emphasis === true) {
|
|
554
|
+
cell.setAttribute("data-velumo-col-emphasis", "");
|
|
555
|
+
}
|
|
556
|
+
if (typeof column.align === "string") {
|
|
557
|
+
cell.setAttribute("data-velumo-col-align", column.align);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const hasHeader = columns.some((c) => isRecord(c) && c.header !== undefined);
|
|
562
|
+
if (hasHeader) {
|
|
563
|
+
const headRow = factories.createElement("div");
|
|
564
|
+
headRow.setAttribute("role", "row");
|
|
565
|
+
headRow.setAttribute("data-velumo-table-head", "");
|
|
566
|
+
columns.forEach((column) => {
|
|
567
|
+
const cell = factories.createElement("div");
|
|
568
|
+
cell.setAttribute("role", "columnheader");
|
|
569
|
+
applyColAttrs(cell, column);
|
|
570
|
+
if (isRecord(column) && column.header !== undefined) {
|
|
571
|
+
cell.appendChild(renderLayoutNode(column.header, factories));
|
|
572
|
+
}
|
|
573
|
+
headRow.appendChild(cell);
|
|
574
|
+
});
|
|
575
|
+
element.appendChild(headRow);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const body = factories.createElement("div");
|
|
579
|
+
body.setAttribute("role", "rowgroup");
|
|
580
|
+
const rows = Array.isArray(node.rows) ? node.rows : [];
|
|
581
|
+
for (const row of rows) {
|
|
582
|
+
const rowEl = factories.createElement("div");
|
|
583
|
+
rowEl.setAttribute("role", "row");
|
|
584
|
+
const cells = Array.isArray(row) ? row.slice(0, columns.length) : [];
|
|
585
|
+
cells.forEach((cellNode, cellIndex) => {
|
|
586
|
+
const cell = factories.createElement("div");
|
|
587
|
+
cell.setAttribute("role", "cell");
|
|
588
|
+
applyColAttrs(cell, columns[cellIndex]);
|
|
589
|
+
cell.appendChild(renderLayoutNode(cellNode, factories));
|
|
590
|
+
rowEl.appendChild(cell);
|
|
591
|
+
});
|
|
592
|
+
body.appendChild(rowEl);
|
|
593
|
+
}
|
|
594
|
+
element.appendChild(body);
|
|
595
|
+
return element;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
599
|
+
|
|
600
|
+
function connectorMarkerId(node: {
|
|
601
|
+
readonly from?: unknown;
|
|
602
|
+
readonly to?: unknown;
|
|
603
|
+
readonly control?: unknown;
|
|
604
|
+
readonly arrow?: unknown;
|
|
605
|
+
readonly dashed?: unknown;
|
|
606
|
+
readonly tone?: unknown;
|
|
607
|
+
}): string {
|
|
608
|
+
// Deterministic, collision-resistant id from all style-bearing fields so that
|
|
609
|
+
// two connectors sharing endpoints but differing in tone/arrow/dashed/control
|
|
610
|
+
// receive distinct marker ids (inline-SVG ids are document-global).
|
|
611
|
+
const pt = (p: unknown): string =>
|
|
612
|
+
isRecord(p) ? `${String(p.x)}_${String(p.y)}` : "x";
|
|
613
|
+
const parts = [
|
|
614
|
+
pt(node.from),
|
|
615
|
+
pt(node.to),
|
|
616
|
+
isRecord(node.control) ? pt(node.control) : "noctl",
|
|
617
|
+
typeof node.arrow === "string" ? node.arrow : "none",
|
|
618
|
+
node.dashed === true ? "d" : "s",
|
|
619
|
+
typeof node.tone === "string" ? node.tone : "default",
|
|
620
|
+
].join("-");
|
|
621
|
+
return `velumo-arrow-${parts}`.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function renderConnectorElement(
|
|
625
|
+
node: {
|
|
626
|
+
readonly from?: unknown;
|
|
627
|
+
readonly to?: unknown;
|
|
628
|
+
readonly control?: unknown;
|
|
629
|
+
readonly arrow?: unknown;
|
|
630
|
+
readonly dashed?: unknown;
|
|
631
|
+
readonly tone?: unknown;
|
|
632
|
+
},
|
|
633
|
+
factories: LayoutRendererFactories,
|
|
634
|
+
): LayoutRendererElement {
|
|
635
|
+
const create = (tag: string): LayoutRendererElement =>
|
|
636
|
+
factories.createElementNS
|
|
637
|
+
? factories.createElementNS(SVG_NAMESPACE, tag)
|
|
638
|
+
: factories.createElement(tag);
|
|
639
|
+
|
|
640
|
+
const svg = create("svg");
|
|
641
|
+
svg.setAttribute("data-velumo-element", "connector");
|
|
642
|
+
svg.setAttribute("viewBox", "0 0 100 100");
|
|
643
|
+
svg.setAttribute("preserveAspectRatio", "none");
|
|
644
|
+
svg.setAttribute("width", "100%");
|
|
645
|
+
svg.setAttribute("height", "100%");
|
|
646
|
+
svg.setAttribute("aria-hidden", "true");
|
|
647
|
+
if (typeof node.tone === "string") {
|
|
648
|
+
svg.setAttribute("data-velumo-text-tone", node.tone);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Guard against non-finite coordinates (e.g. NaN from non-numeric input) so
|
|
652
|
+
// the path data attribute is always well-formed.
|
|
653
|
+
const num = (v: unknown): number => {
|
|
654
|
+
const n = Number(v);
|
|
655
|
+
return Number.isFinite(n) ? n : 0;
|
|
656
|
+
};
|
|
657
|
+
const from = isRecord(node.from) ? node.from : { x: 0, y: 0 };
|
|
658
|
+
const to = isRecord(node.to) ? node.to : { x: 0, y: 0 };
|
|
659
|
+
const fx = num(from.x ?? 0);
|
|
660
|
+
const fy = num(from.y ?? 0);
|
|
661
|
+
const tx = num(to.x ?? 0);
|
|
662
|
+
const ty = num(to.y ?? 0);
|
|
663
|
+
const d = isRecord(node.control)
|
|
664
|
+
? `M ${fx} ${fy} Q ${num(node.control.x ?? 0)} ${num(node.control.y ?? 0)} ${tx} ${ty}`
|
|
665
|
+
: `M ${fx} ${fy} L ${tx} ${ty}`;
|
|
666
|
+
|
|
667
|
+
const arrow = typeof node.arrow === "string" ? node.arrow : "none";
|
|
668
|
+
const markerId = connectorMarkerId(node);
|
|
669
|
+
|
|
670
|
+
// Append the connection path first so depth-first tree walkers encounter it
|
|
671
|
+
// before the arrowhead path nested inside <defs>/<marker>.
|
|
672
|
+
const path = create("path");
|
|
673
|
+
path.setAttribute("d", d);
|
|
674
|
+
path.setAttribute("fill", "none");
|
|
675
|
+
path.setAttribute("stroke", "currentColor");
|
|
676
|
+
path.setAttribute("stroke-width", "2.5");
|
|
677
|
+
path.setAttribute("vector-effect", "non-scaling-stroke");
|
|
678
|
+
path.setAttribute("stroke-linecap", "round");
|
|
679
|
+
if (node.dashed === true) {
|
|
680
|
+
path.setAttribute("stroke-dasharray", "4 4");
|
|
681
|
+
}
|
|
682
|
+
if (arrow === "end" || arrow === "both") {
|
|
683
|
+
path.setAttribute("marker-end", `url(#${markerId})`);
|
|
684
|
+
}
|
|
685
|
+
if (arrow === "start" || arrow === "both") {
|
|
686
|
+
path.setAttribute("marker-start", `url(#${markerId})`);
|
|
687
|
+
}
|
|
688
|
+
svg.appendChild(path);
|
|
689
|
+
|
|
690
|
+
if (arrow === "end" || arrow === "start" || arrow === "both") {
|
|
691
|
+
const defs = create("defs");
|
|
692
|
+
const marker = create("marker");
|
|
693
|
+
marker.setAttribute("id", markerId);
|
|
694
|
+
marker.setAttribute("markerWidth", "2.6");
|
|
695
|
+
marker.setAttribute("markerHeight", "2.6");
|
|
696
|
+
marker.setAttribute("refX", "2.2");
|
|
697
|
+
marker.setAttribute("refY", "1.3");
|
|
698
|
+
marker.setAttribute("orient", "auto");
|
|
699
|
+
marker.setAttribute("markerUnits", "userSpaceOnUse");
|
|
700
|
+
const head = create("path");
|
|
701
|
+
head.setAttribute("d", "M 0 0 L 2.6 1.3 L 0 2.6 z");
|
|
702
|
+
head.setAttribute("fill", "currentColor");
|
|
703
|
+
marker.appendChild(head);
|
|
704
|
+
defs.appendChild(marker);
|
|
705
|
+
svg.appendChild(defs);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return svg;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function unknownLayoutElement(
|
|
712
|
+
factories: LayoutRendererFactories,
|
|
713
|
+
): LayoutRendererElement {
|
|
714
|
+
const element = factories.createElement("div");
|
|
715
|
+
element.setAttribute("data-velumo-layout", "unknown");
|
|
716
|
+
return element;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
720
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
721
|
+
}
|