@diagrammo/dgmo 0.8.20 → 0.8.22
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 +2 -1
- package/README.md +1 -0
- package/dist/cli.cjs +142 -90
- package/dist/editor.cjs +30 -4
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +30 -4
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +25 -3
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +25 -3
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +21201 -12886
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +646 -89
- package/dist/index.d.ts +646 -89
- package/dist/index.js +21178 -12889
- package/dist/index.js.map +1 -1
- package/docs/guide/chart-mindmap.md +198 -0
- package/docs/guide/chart-sequence.md +23 -1
- package/docs/guide/chart-sitemap.md +18 -1
- package/docs/guide/chart-tech-radar.md +219 -0
- package/docs/guide/chart-wireframe.md +100 -0
- package/docs/guide/index.md +8 -0
- package/docs/guide/registry.json +1 -0
- package/docs/language-reference.md +249 -4
- package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
- package/gallery/fixtures/c4-full.dgmo +2 -2
- package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
- package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
- package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
- package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
- package/gallery/fixtures/gantt-full.dgmo +2 -2
- package/gallery/fixtures/gantt.dgmo +2 -2
- package/gallery/fixtures/infra-full.dgmo +2 -2
- package/gallery/fixtures/infra.dgmo +1 -1
- package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
- package/gallery/fixtures/sequence-tags.dgmo +2 -2
- package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
- package/gallery/fixtures/tech-radar.dgmo +36 -0
- package/gallery/fixtures/timeline.dgmo +1 -1
- package/package.json +1 -1
- package/src/boxes-and-lines/collapse.ts +21 -3
- package/src/boxes-and-lines/layout.ts +360 -42
- package/src/boxes-and-lines/parser.ts +94 -11
- package/src/boxes-and-lines/renderer.ts +371 -114
- package/src/boxes-and-lines/types.ts +2 -1
- package/src/c4/layout.ts +8 -8
- package/src/c4/parser.ts +35 -2
- package/src/c4/renderer.ts +19 -3
- package/src/c4/types.ts +1 -0
- package/src/chart.ts +14 -7
- package/src/completion.ts +253 -0
- package/src/cycle/layout.ts +732 -0
- package/src/cycle/parser.ts +352 -0
- package/src/cycle/renderer.ts +539 -0
- package/src/cycle/types.ts +77 -0
- package/src/d3.ts +240 -40
- package/src/dgmo-router.ts +15 -0
- package/src/echarts.ts +7 -4
- package/src/editor/dgmo.grammar +5 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/editor/keywords.ts +26 -0
- package/src/gantt/parser.ts +2 -8
- package/src/graph/flowchart-parser.ts +15 -21
- package/src/graph/layout.ts +73 -9
- package/src/graph/state-collapse.ts +78 -0
- package/src/graph/state-parser.ts +5 -10
- package/src/graph/state-renderer.ts +139 -34
- package/src/index.ts +78 -0
- package/src/infra/layout.ts +218 -74
- package/src/infra/parser.ts +30 -6
- package/src/infra/renderer.ts +14 -8
- package/src/infra/types.ts +10 -3
- package/src/journey-map/layout.ts +386 -0
- package/src/journey-map/parser.ts +540 -0
- package/src/journey-map/renderer.ts +1456 -0
- package/src/journey-map/types.ts +47 -0
- package/src/kanban/parser.ts +3 -10
- package/src/kanban/renderer.ts +325 -63
- package/src/mindmap/collapse.ts +88 -0
- package/src/mindmap/layout.ts +605 -0
- package/src/mindmap/parser.ts +373 -0
- package/src/mindmap/renderer.ts +544 -0
- package/src/mindmap/text-wrap.ts +217 -0
- package/src/mindmap/types.ts +55 -0
- package/src/org/parser.ts +2 -6
- package/src/render.ts +18 -21
- package/src/sequence/renderer.ts +273 -56
- package/src/sharing.ts +3 -0
- package/src/sitemap/layout.ts +56 -18
- package/src/sitemap/parser.ts +26 -17
- package/src/sitemap/renderer.ts +34 -0
- package/src/sitemap/types.ts +1 -0
- package/src/tech-radar/index.ts +14 -0
- package/src/tech-radar/interactive.ts +1058 -0
- package/src/tech-radar/layout.ts +190 -0
- package/src/tech-radar/parser.ts +385 -0
- package/src/tech-radar/renderer.ts +1159 -0
- package/src/tech-radar/shared.ts +187 -0
- package/src/tech-radar/types.ts +81 -0
- package/src/utils/description-helpers.ts +33 -0
- package/src/utils/export-container.ts +3 -2
- package/src/utils/legend-d3.ts +1 -0
- package/src/utils/legend-layout.ts +5 -3
- package/src/utils/parsing.ts +48 -7
- package/src/utils/tag-groups.ts +46 -60
- package/src/wireframe/layout.ts +460 -0
- package/src/wireframe/parser.ts +956 -0
- package/src/wireframe/renderer.ts +1293 -0
- package/src/wireframe/types.ts +110 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Wireframe Diagram Layout Engine (Document Flow)
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
WireframeElement,
|
|
7
|
+
ParsedWireframe,
|
|
8
|
+
WireframeFormFactor,
|
|
9
|
+
} from './types';
|
|
10
|
+
|
|
11
|
+
// ============================================================
|
|
12
|
+
// Constants
|
|
13
|
+
// ============================================================
|
|
14
|
+
|
|
15
|
+
const DESKTOP_WIDTH = 1200;
|
|
16
|
+
const MOBILE_WIDTH = 375;
|
|
17
|
+
|
|
18
|
+
const CHAR_WIDTH = 7.5;
|
|
19
|
+
const LABEL_PADDING = 16;
|
|
20
|
+
|
|
21
|
+
/** Element type-specific heights */
|
|
22
|
+
const ELEMENT_HEIGHTS: Record<string, number> = {
|
|
23
|
+
textInput: 36,
|
|
24
|
+
button: 40,
|
|
25
|
+
dropdown: 36,
|
|
26
|
+
checkbox: 24,
|
|
27
|
+
radio: 24,
|
|
28
|
+
heading: 48,
|
|
29
|
+
subheading: 36,
|
|
30
|
+
divider: 24,
|
|
31
|
+
text: 22,
|
|
32
|
+
listItem: 24,
|
|
33
|
+
image: 120,
|
|
34
|
+
progress: 28,
|
|
35
|
+
chart: 160,
|
|
36
|
+
alert: 40,
|
|
37
|
+
nav: 44,
|
|
38
|
+
tabs: 44,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Spacing after specific element types */
|
|
42
|
+
const SPACING_AFTER: Record<string, number> = {
|
|
43
|
+
heading: 16,
|
|
44
|
+
subheading: 12,
|
|
45
|
+
divider: 12,
|
|
46
|
+
textInput: 8,
|
|
47
|
+
dropdown: 8,
|
|
48
|
+
checkbox: 8,
|
|
49
|
+
radio: 8,
|
|
50
|
+
button: 8,
|
|
51
|
+
text: 12,
|
|
52
|
+
listItem: 4,
|
|
53
|
+
group: 16,
|
|
54
|
+
image: 12,
|
|
55
|
+
progress: 12,
|
|
56
|
+
chart: 16,
|
|
57
|
+
alert: 12,
|
|
58
|
+
nav: 12,
|
|
59
|
+
tabs: 12,
|
|
60
|
+
table: 16,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const GROUP_PADDING_TOP_WITH_LABEL = 32; // label + top padding
|
|
64
|
+
const GROUP_PADDING_TOP_NO_LABEL = 10; // just content padding
|
|
65
|
+
const GROUP_PADDING_BOTTOM = 12;
|
|
66
|
+
|
|
67
|
+
/** Resolved at layout time based on showGroupLabels option */
|
|
68
|
+
let GROUP_PADDING_TOP = GROUP_PADDING_TOP_WITH_LABEL;
|
|
69
|
+
const GROUP_PADDING_X = 12;
|
|
70
|
+
const FRAME_PADDING = 20;
|
|
71
|
+
const TITLE_HEIGHT = 40;
|
|
72
|
+
|
|
73
|
+
/** Smart sizing: recognized region name prefixes → width fraction */
|
|
74
|
+
const REGION_SIZES: Record<string, number> = {
|
|
75
|
+
sidebar: 0.25,
|
|
76
|
+
side: 0.25,
|
|
77
|
+
left: 0.25,
|
|
78
|
+
right: 0.25,
|
|
79
|
+
main: 0, // fill remaining
|
|
80
|
+
content: 0, // fill remaining
|
|
81
|
+
center: 0, // fill remaining
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Full-width regions (thin strips) */
|
|
85
|
+
const FULL_WIDTH_REGIONS = new Set(['header', 'top', 'footer', 'bottom']);
|
|
86
|
+
const HEADER_REGION_HEIGHT = 60;
|
|
87
|
+
const FOOTER_REGION_HEIGHT = 40;
|
|
88
|
+
|
|
89
|
+
// ============================================================
|
|
90
|
+
// Layout Types
|
|
91
|
+
// ============================================================
|
|
92
|
+
|
|
93
|
+
export interface WireframeLayoutNode {
|
|
94
|
+
id: string;
|
|
95
|
+
x: number;
|
|
96
|
+
y: number;
|
|
97
|
+
width: number;
|
|
98
|
+
height: number;
|
|
99
|
+
element: WireframeElement;
|
|
100
|
+
children: WireframeLayoutNode[];
|
|
101
|
+
/** For label-field pairs: the x offset where fields align */
|
|
102
|
+
fieldAlignX?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface WireframeLayout {
|
|
106
|
+
width: number;
|
|
107
|
+
height: number;
|
|
108
|
+
titleHeight: number;
|
|
109
|
+
nodes: WireframeLayoutNode[];
|
|
110
|
+
modalNodes: WireframeLayoutNode[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================
|
|
114
|
+
// Layout Engine
|
|
115
|
+
// ============================================================
|
|
116
|
+
|
|
117
|
+
export function layoutWireframe(
|
|
118
|
+
parsed: ParsedWireframe,
|
|
119
|
+
_options?: Record<string, string>,
|
|
120
|
+
overrideWidth?: number,
|
|
121
|
+
showGroupLabels = true
|
|
122
|
+
): WireframeLayout {
|
|
123
|
+
GROUP_PADDING_TOP = showGroupLabels
|
|
124
|
+
? GROUP_PADDING_TOP_WITH_LABEL
|
|
125
|
+
: GROUP_PADDING_TOP_NO_LABEL;
|
|
126
|
+
const defaultWidth =
|
|
127
|
+
parsed.formFactor === 'mobile' ? MOBILE_WIDTH : DESKTOP_WIDTH;
|
|
128
|
+
const frameWidth = overrideWidth ?? defaultWidth;
|
|
129
|
+
const titleHeight = parsed.title ? TITLE_HEIGHT : 0;
|
|
130
|
+
|
|
131
|
+
const contentWidth = frameWidth - FRAME_PADDING * 2;
|
|
132
|
+
|
|
133
|
+
// Layout top-level roots
|
|
134
|
+
const nodes = layoutTopLevel(parsed.roots, contentWidth, parsed.formFactor);
|
|
135
|
+
|
|
136
|
+
// Calculate total height from positioned nodes
|
|
137
|
+
let maxY = 0;
|
|
138
|
+
for (const n of nodes) {
|
|
139
|
+
maxY = Math.max(maxY, n.y + n.height);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Layout modals below main content
|
|
143
|
+
const modalNodes: WireframeLayoutNode[] = [];
|
|
144
|
+
let modalY = maxY + 24;
|
|
145
|
+
for (const modal of parsed.modals) {
|
|
146
|
+
const modalWidth = Math.min(contentWidth * 0.7, 600);
|
|
147
|
+
const modalX = (contentWidth - modalWidth) / 2;
|
|
148
|
+
const node = layoutElement(modal, modalX, modalY, modalWidth);
|
|
149
|
+
modalNodes.push(node);
|
|
150
|
+
modalY += node.height + 24;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const totalHeight =
|
|
154
|
+
(modalNodes.length > 0
|
|
155
|
+
? modalNodes[modalNodes.length - 1].y +
|
|
156
|
+
modalNodes[modalNodes.length - 1].height
|
|
157
|
+
: maxY) +
|
|
158
|
+
FRAME_PADDING * 2 +
|
|
159
|
+
titleHeight;
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
width: frameWidth,
|
|
163
|
+
height: totalHeight,
|
|
164
|
+
titleHeight,
|
|
165
|
+
nodes,
|
|
166
|
+
modalNodes,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Layout top-level roots: separate into full-width regions (header/footer)
|
|
172
|
+
* and horizontal siblings, then stack vertically.
|
|
173
|
+
*/
|
|
174
|
+
function layoutTopLevel(
|
|
175
|
+
roots: WireframeElement[],
|
|
176
|
+
contentWidth: number,
|
|
177
|
+
formFactor: WireframeFormFactor
|
|
178
|
+
): WireframeLayoutNode[] {
|
|
179
|
+
const result: WireframeLayoutNode[] = [];
|
|
180
|
+
let y = 0;
|
|
181
|
+
|
|
182
|
+
// Classify roots into rows
|
|
183
|
+
const rows: WireframeElement[][] = [];
|
|
184
|
+
let currentRow: WireframeElement[] = [];
|
|
185
|
+
|
|
186
|
+
for (const root of roots) {
|
|
187
|
+
const regionName = root.label.toLowerCase().split(/\s+/)[0];
|
|
188
|
+
if (FULL_WIDTH_REGIONS.has(regionName) || formFactor === 'mobile') {
|
|
189
|
+
// Full-width: flush current row first, then add as its own row
|
|
190
|
+
if (currentRow.length > 0) {
|
|
191
|
+
rows.push(currentRow);
|
|
192
|
+
currentRow = [];
|
|
193
|
+
}
|
|
194
|
+
rows.push([root]);
|
|
195
|
+
} else {
|
|
196
|
+
currentRow.push(root);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (currentRow.length > 0) rows.push(currentRow);
|
|
200
|
+
|
|
201
|
+
// Layout each row
|
|
202
|
+
for (const row of rows) {
|
|
203
|
+
if (row.length === 1 && formFactor !== 'mobile') {
|
|
204
|
+
const el = row[0];
|
|
205
|
+
const regionName = el.label.toLowerCase().split(/\s+/)[0];
|
|
206
|
+
|
|
207
|
+
if (FULL_WIDTH_REGIONS.has(regionName)) {
|
|
208
|
+
// Full-width strip
|
|
209
|
+
const node = layoutElement(el, 0, y, contentWidth);
|
|
210
|
+
// Enforce min height for header/footer
|
|
211
|
+
if (regionName === 'header' || regionName === 'top') {
|
|
212
|
+
node.height = Math.max(node.height, HEADER_REGION_HEIGHT);
|
|
213
|
+
} else {
|
|
214
|
+
node.height = Math.max(node.height, FOOTER_REGION_HEIGHT);
|
|
215
|
+
}
|
|
216
|
+
result.push(node);
|
|
217
|
+
y += node.height + 16;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (formFactor === 'mobile' || row.length === 1) {
|
|
223
|
+
// Stack vertically
|
|
224
|
+
for (const el of row) {
|
|
225
|
+
const node = layoutElement(el, 0, y, contentWidth);
|
|
226
|
+
result.push(node);
|
|
227
|
+
y += node.height + 16;
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
// Horizontal siblings — smart sizing
|
|
231
|
+
const allocated = allocateHorizontalWidths(row, contentWidth);
|
|
232
|
+
let x = 0;
|
|
233
|
+
let maxHeight = 0;
|
|
234
|
+
const rowNodes: WireframeLayoutNode[] = [];
|
|
235
|
+
|
|
236
|
+
for (let i = 0; i < row.length; i++) {
|
|
237
|
+
const w = allocated[i];
|
|
238
|
+
const node = layoutElement(row[i], x, y, w);
|
|
239
|
+
rowNodes.push(node);
|
|
240
|
+
maxHeight = Math.max(maxHeight, node.height);
|
|
241
|
+
x += w + 12; // gap between horizontal siblings
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Equalize heights (tallest wins, F4)
|
|
245
|
+
for (const n of rowNodes) {
|
|
246
|
+
n.height = maxHeight;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
result.push(...rowNodes);
|
|
250
|
+
y += maxHeight + 16;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Allocate horizontal widths for side-by-side regions using smart sizing.
|
|
259
|
+
*/
|
|
260
|
+
function allocateHorizontalWidths(
|
|
261
|
+
elements: WireframeElement[],
|
|
262
|
+
totalWidth: number
|
|
263
|
+
): number[] {
|
|
264
|
+
const gap = 12;
|
|
265
|
+
const totalGaps = (elements.length - 1) * gap;
|
|
266
|
+
const available = totalWidth - totalGaps;
|
|
267
|
+
|
|
268
|
+
const widths: number[] = new Array(elements.length).fill(0);
|
|
269
|
+
let allocated = 0;
|
|
270
|
+
let fillCount = 0;
|
|
271
|
+
|
|
272
|
+
for (let i = 0; i < elements.length; i++) {
|
|
273
|
+
const name = elements[i].label.toLowerCase().split(/\s+/)[0];
|
|
274
|
+
const fraction = REGION_SIZES[name];
|
|
275
|
+
|
|
276
|
+
if (fraction !== undefined && fraction > 0) {
|
|
277
|
+
widths[i] = available * fraction;
|
|
278
|
+
allocated += widths[i];
|
|
279
|
+
} else if (fraction === 0) {
|
|
280
|
+
// Fill remaining
|
|
281
|
+
fillCount++;
|
|
282
|
+
} else {
|
|
283
|
+
// Unknown — will be equal-split
|
|
284
|
+
fillCount++;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Distribute remaining to fill/unknown regions
|
|
289
|
+
if (fillCount > 0) {
|
|
290
|
+
const remaining = Math.max(0, available - allocated);
|
|
291
|
+
const each = remaining / fillCount;
|
|
292
|
+
for (let i = 0; i < widths.length; i++) {
|
|
293
|
+
if (widths[i] === 0) widths[i] = Math.max(40, each);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Clamp all widths to minimum
|
|
298
|
+
for (let i = 0; i < widths.length; i++) {
|
|
299
|
+
widths[i] = Math.max(40, widths[i]);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return widths;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Layout a single element and its children recursively.
|
|
307
|
+
*/
|
|
308
|
+
function layoutElement(
|
|
309
|
+
el: WireframeElement,
|
|
310
|
+
x: number,
|
|
311
|
+
y: number,
|
|
312
|
+
width: number
|
|
313
|
+
): WireframeLayoutNode {
|
|
314
|
+
const node: WireframeLayoutNode = {
|
|
315
|
+
id: el.id,
|
|
316
|
+
x,
|
|
317
|
+
y,
|
|
318
|
+
width,
|
|
319
|
+
height: 0,
|
|
320
|
+
element: el,
|
|
321
|
+
children: [],
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// Nav and tabs render children internally as a horizontal bar — treat as leaf for layout
|
|
325
|
+
const selfRendered = el.type === 'nav' || el.type === 'tabs';
|
|
326
|
+
|
|
327
|
+
if (!el.isContainer || el.children.length === 0 || selfRendered) {
|
|
328
|
+
// Leaf element — use type-specific height
|
|
329
|
+
node.height = getElementHeight(el);
|
|
330
|
+
return node;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Container — layout children
|
|
334
|
+
const isInlineRow =
|
|
335
|
+
el.metadata._inlineRow === 'true' || el.metadata._labelField === 'true';
|
|
336
|
+
const padTop = isInlineRow ? 0 : GROUP_PADDING_TOP;
|
|
337
|
+
const padBottom = isInlineRow ? 0 : GROUP_PADDING_BOTTOM;
|
|
338
|
+
const padX = isInlineRow ? 0 : GROUP_PADDING_X;
|
|
339
|
+
const innerWidth = width - padX * 2;
|
|
340
|
+
const innerX = padX;
|
|
341
|
+
|
|
342
|
+
if (el.orientation === 'horizontal') {
|
|
343
|
+
// Horizontal layout: children in a row
|
|
344
|
+
const childWidths = allocateEqualWidths(el.children, innerWidth);
|
|
345
|
+
let cx = innerX;
|
|
346
|
+
let maxChildHeight = 0;
|
|
347
|
+
|
|
348
|
+
for (let i = 0; i < el.children.length; i++) {
|
|
349
|
+
const child = el.children[i];
|
|
350
|
+
const cw = childWidths[i];
|
|
351
|
+
const childNode = layoutElement(child, cx, padTop, cw);
|
|
352
|
+
node.children.push(childNode);
|
|
353
|
+
maxChildHeight = Math.max(maxChildHeight, childNode.height);
|
|
354
|
+
cx += cw + 8;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Equalize child heights
|
|
358
|
+
for (const cn of node.children) {
|
|
359
|
+
cn.height = maxChildHeight;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
node.height = padTop + maxChildHeight + padBottom;
|
|
363
|
+
} else {
|
|
364
|
+
// Vertical layout: stack children
|
|
365
|
+
let cy = padTop;
|
|
366
|
+
|
|
367
|
+
// Check for label-field auto-alignment (ADR-4)
|
|
368
|
+
const fieldAlignX = computeFieldAlignX(el.children);
|
|
369
|
+
if (fieldAlignX > 0) {
|
|
370
|
+
node.fieldAlignX = fieldAlignX;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (const child of el.children) {
|
|
374
|
+
const childNode = layoutElement(child, innerX, cy, innerWidth);
|
|
375
|
+
node.children.push(childNode);
|
|
376
|
+
cy += childNode.height + getSpacingAfter(child);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
node.height = cy + padBottom;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Collapsed state: only show header
|
|
383
|
+
if (el.states.includes('collapsed')) {
|
|
384
|
+
node.height = GROUP_PADDING_TOP;
|
|
385
|
+
node.children = [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return node;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function allocateEqualWidths(
|
|
392
|
+
children: WireframeElement[],
|
|
393
|
+
totalWidth: number
|
|
394
|
+
): number[] {
|
|
395
|
+
const gap = 8;
|
|
396
|
+
const totalGaps = (children.length - 1) * gap;
|
|
397
|
+
const each = Math.max(40, (totalWidth - totalGaps) / children.length);
|
|
398
|
+
return children.map(() => each);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function getElementHeight(el: WireframeElement): number {
|
|
402
|
+
if (el.type === 'heading') {
|
|
403
|
+
return el.headingLevel === 2
|
|
404
|
+
? (ELEMENT_HEIGHTS.subheading ?? 36)
|
|
405
|
+
: (ELEMENT_HEIGHTS.heading ?? 48);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (el.type === 'textInput' && el.fieldVariant === 'textarea') {
|
|
409
|
+
return 80;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (el.type === 'table') {
|
|
413
|
+
const headerH = 32;
|
|
414
|
+
const rowH = 28;
|
|
415
|
+
const rows = el.tableData?.length || el.tableRows || 0;
|
|
416
|
+
return headerH + rows * rowH + 8;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Image hints affect height
|
|
420
|
+
if (el.type === 'image') {
|
|
421
|
+
if (el.imageHint === 'round') return 80;
|
|
422
|
+
if (el.imageHint === 'wide') return 80;
|
|
423
|
+
return ELEMENT_HEIGHTS.image ?? 120;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Label-field wrapper
|
|
427
|
+
if (el.metadata._labelField === 'true') {
|
|
428
|
+
return 36; // input height
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return ELEMENT_HEIGHTS[el.type] ?? 24;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function getSpacingAfter(el: WireframeElement): number {
|
|
435
|
+
if (el.type === 'heading' && el.headingLevel === 2) {
|
|
436
|
+
return SPACING_AFTER.subheading ?? 12;
|
|
437
|
+
}
|
|
438
|
+
return SPACING_AFTER[el.type] ?? 8;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Compute auto-alignment X offset for label-field pairs in a group (ADR-4, EC8).
|
|
443
|
+
* Returns 0 if no label-field pattern detected.
|
|
444
|
+
*/
|
|
445
|
+
function computeFieldAlignX(children: WireframeElement[]): number {
|
|
446
|
+
let maxLabelWidth = 0;
|
|
447
|
+
let labelFieldCount = 0;
|
|
448
|
+
|
|
449
|
+
for (const child of children) {
|
|
450
|
+
if (child.metadata._labelField === 'true' && child.children.length >= 2) {
|
|
451
|
+
const labelEl = child.children[0];
|
|
452
|
+
const labelWidth = labelEl.label.length * CHAR_WIDTH;
|
|
453
|
+
maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
|
|
454
|
+
labelFieldCount++;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (labelFieldCount < 2) return 0;
|
|
459
|
+
return maxLabelWidth + LABEL_PADDING;
|
|
460
|
+
}
|