orcasvn-react-diagrams 0.2.4 → 0.2.6

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +42 -4
  2. package/README.md +15 -32
  3. package/ai/api-contract.json +88 -3
  4. package/ai/manifest.json +1 -1
  5. package/dist/cjs/examples.js +6519 -4423
  6. package/dist/cjs/index.js +1513 -120
  7. package/dist/cjs/types/api/createDiagramEditor.d.ts +4 -1
  8. package/dist/cjs/types/api/types.d.ts +43 -1
  9. package/dist/cjs/types/displaybox/demos/LinkColorPoolDemoTab.d.ts +3 -0
  10. package/dist/cjs/types/displaybox/demos/OffsetAnchorAvoidanceDemoTab.d.ts +3 -0
  11. package/dist/cjs/types/displaybox/demos/SimpleDemo.d.ts +4 -1
  12. package/dist/cjs/types/displaybox/demos/ZoomToFitElementsDemoTab.d.ts +3 -0
  13. package/dist/cjs/types/displaybox/demos/linkColorPoolDemo.d.ts +2 -0
  14. package/dist/cjs/types/displaybox/demos/offsetAnchorAvoidanceDemo.d.ts +4 -0
  15. package/dist/cjs/types/displaybox/demos/textDemo.d.ts +3 -0
  16. package/dist/cjs/types/displaybox/demos/zoomToFitElementsDemo.d.ts +4 -0
  17. package/dist/cjs/types/displaybox/types.d.ts +3 -1
  18. package/dist/cjs/types/displaybox/useDemoEditor.d.ts +4 -2
  19. package/dist/cjs/types/engine/AutoLayoutService.d.ts +59 -2
  20. package/dist/cjs/types/engine/DiagramEngine.d.ts +20 -1
  21. package/dist/cjs/types/engine/LinkRoutingService.d.ts +9 -0
  22. package/dist/cjs/types/models/TextModel.d.ts +1 -0
  23. package/dist/cjs/types/renderer/konva/KonvaInteraction.d.ts +6 -0
  24. package/dist/cjs/types/strategies/ObstacleRouter.d.ts +2 -0
  25. package/dist/cjs/types/strategies/RouterStrategy.d.ts +10 -0
  26. package/dist/esm/examples.js +6519 -4423
  27. package/dist/esm/examples.js.map +1 -1
  28. package/dist/esm/index.js +1513 -120
  29. package/dist/esm/index.js.map +1 -1
  30. package/dist/esm/types/api/createDiagramEditor.d.ts +4 -1
  31. package/dist/esm/types/api/types.d.ts +43 -1
  32. package/dist/esm/types/displaybox/demos/LinkColorPoolDemoTab.d.ts +3 -0
  33. package/dist/esm/types/displaybox/demos/OffsetAnchorAvoidanceDemoTab.d.ts +3 -0
  34. package/dist/esm/types/displaybox/demos/SimpleDemo.d.ts +4 -1
  35. package/dist/esm/types/displaybox/demos/ZoomToFitElementsDemoTab.d.ts +3 -0
  36. package/dist/esm/types/displaybox/demos/linkColorPoolDemo.d.ts +2 -0
  37. package/dist/esm/types/displaybox/demos/offsetAnchorAvoidanceDemo.d.ts +4 -0
  38. package/dist/esm/types/displaybox/demos/textDemo.d.ts +3 -0
  39. package/dist/esm/types/displaybox/demos/zoomToFitElementsDemo.d.ts +4 -0
  40. package/dist/esm/types/displaybox/types.d.ts +3 -1
  41. package/dist/esm/types/displaybox/useDemoEditor.d.ts +4 -2
  42. package/dist/esm/types/engine/AutoLayoutService.d.ts +59 -2
  43. package/dist/esm/types/engine/DiagramEngine.d.ts +20 -1
  44. package/dist/esm/types/engine/LinkRoutingService.d.ts +9 -0
  45. package/dist/esm/types/models/TextModel.d.ts +1 -0
  46. package/dist/esm/types/renderer/konva/KonvaInteraction.d.ts +6 -0
  47. package/dist/esm/types/strategies/ObstacleRouter.d.ts +2 -0
  48. package/dist/esm/types/strategies/RouterStrategy.d.ts +10 -0
  49. package/dist/examples.d.ts +56 -1
  50. package/dist/index.d.ts +60 -2
  51. package/docs/API_CONTRACT.md +73 -3
  52. package/docs/CAPABILITIES.md +8 -0
  53. package/docs/COMMANDS_EVENTS.md +8 -0
  54. package/docs/DOCUMENTATION_WORKFLOW.md +4 -1
  55. package/docs/INTEGRATION_PLAYBOOK.md +3 -0
  56. package/docs/STATE_INVARIANTS.md +4 -0
  57. package/package.json +1 -1
  58. package/src/displaybox/demos/AutoLayoutDemoTab.tsx +515 -381
  59. package/src/displaybox/demos/EngineEventsDemoTab.tsx +12 -11
  60. package/src/displaybox/demos/LinkColorPoolDemoTab.tsx +49 -0
  61. package/src/displaybox/demos/ObstacleRoutingDemoTab.tsx +52 -31
  62. package/src/displaybox/demos/OffsetAnchorAvoidanceDemoTab.tsx +30 -0
  63. package/src/displaybox/demos/SimpleDemo.tsx +18 -13
  64. package/src/displaybox/demos/TextLayoutDemoTab.tsx +18 -1
  65. package/src/displaybox/demos/ZoomToFitElementsDemoTab.tsx +29 -0
  66. package/src/displaybox/demos/autoLayoutDemo.ts +62 -26
  67. package/src/displaybox/demos/index.tsx +81 -57
  68. package/src/displaybox/demos/linkColorPoolDemo.ts +122 -0
  69. package/src/displaybox/demos/obstacleRoutingDemo.ts +77 -1
  70. package/src/displaybox/demos/offsetAnchorAvoidanceDemo.ts +211 -0
  71. package/src/displaybox/demos/routingDemo.ts +5 -5
  72. package/src/displaybox/demos/shared.ts +17 -12
  73. package/src/displaybox/demos/textDemo.ts +6 -2
  74. package/src/displaybox/demos/zoomToFitElementsDemo.ts +83 -0
  75. package/src/displaybox/types.ts +10 -8
@@ -1,131 +1,175 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
2
- import type { ElementLayout, ElementLayoutLabelReservedSpaceMode, ElementLayoutMode } from '../../api';
3
- import { createId } from '../../utils/ids';
4
- import DisplayBoxControls from '../DisplayBoxControls';
5
- import DisplayBoxStage from '../DisplayBoxStage';
6
- import useDemoControls from '../useDemoControls';
7
- import useDemoEditor from '../useDemoEditor';
8
- import useOffsetSequence from '../useOffsetSequence';
9
- import type { DemoActionHelpers } from '../types';
10
- import { autoLayoutDemoConfig } from './autoLayoutDemo';
11
- import { gridStageStyle } from './shared';
12
-
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import type {
3
+ ElementLayout,
4
+ ElementLayoutAutoResizeMode,
5
+ ElementLayoutGridRowTemplate,
6
+ ElementLayoutLabelReservedSpaceMode,
7
+ ElementLayoutMode,
8
+ } from '../../api';
9
+ import { createId } from '../../utils/ids';
10
+ import DisplayBoxControls from '../DisplayBoxControls';
11
+ import DisplayBoxStage from '../DisplayBoxStage';
12
+ import useDemoControls from '../useDemoControls';
13
+ import useDemoEditor from '../useDemoEditor';
14
+ import useOffsetSequence from '../useOffsetSequence';
15
+ import type { DemoActionHelpers } from '../types';
16
+ import { autoLayoutDemoConfig } from './autoLayoutDemo';
17
+ import { gridStageStyle } from './shared';
18
+
13
19
  const parentOptions = [
14
20
  { id: 'layout-row', label: 'Horizontal layout' },
15
21
  { id: 'layout-column', label: 'Vertical layout' },
22
+ { id: 'layout-grid', label: 'Grid layout' },
16
23
  { id: 'layout-nested', label: 'Nested layout' },
17
24
  { id: 'layout-manual', label: 'Manual (compare)' },
18
25
  ];
19
26
  const shortLabel = 'Parent label lane demo';
20
27
  const longLabel =
21
28
  'Parent label lane demo with longer content to increase flexible reserved space and push children downward.';
22
-
23
- const AutoLayoutDemo = () => {
24
- const demo = autoLayoutDemoConfig;
25
- const { containerRef, editorRef, diagramState, selection, snapEnabled, setSnapEnabled } = useDemoEditor({
26
- createState: demo.createState,
27
- elementShapes: demo.elementShapes,
28
- portShapes: demo.portShapes,
29
- });
30
-
31
- const nextOffset = useOffsetSequence();
32
- const actionHelpers: DemoActionHelpers = useMemo(() => ({ nextOffset }), [nextOffset]);
33
-
34
- const controls = useDemoControls({
35
- demo,
36
- editorRef,
37
- diagramState,
38
- selection,
39
- snapEnabled,
40
- setSnapEnabled,
41
- actionHelpers,
42
- });
43
-
44
- const [targetId, setTargetId] = useState<string>(parentOptions[0].id);
45
- const [mode, setMode] = useState<ElementLayoutMode>('horizontal');
29
+ const defaultGridTemplateText = '[12,4,8]';
30
+
31
+ const serializeGridTemplate = (template?: ElementLayoutGridRowTemplate[]): string =>
32
+ Array.isArray(template) && template.length > 0 ? JSON.stringify(template) : defaultGridTemplateText;
33
+
34
+ const parseGridTemplate = (value: string): ElementLayoutGridRowTemplate[] | undefined | null => {
35
+ const trimmed = value.trim();
36
+ if (!trimmed) return undefined;
37
+ let parsed: unknown;
38
+ try {
39
+ parsed = JSON.parse(trimmed);
40
+ } catch {
41
+ return null;
42
+ }
43
+ if (!Array.isArray(parsed)) return null;
44
+ const normalized: ElementLayoutGridRowTemplate[] = [];
45
+ for (let i = 0; i < parsed.length; i += 1) {
46
+ const entry = parsed[i];
47
+ if (typeof entry === 'number') {
48
+ if (!Number.isFinite(entry) || entry <= 0) return null;
49
+ normalized.push(Math.floor(entry));
50
+ continue;
51
+ }
52
+ if (!Array.isArray(entry) || entry.length === 0) return null;
53
+ for (let j = 0; j < entry.length; j += 1) {
54
+ const cell = entry[j];
55
+ if (typeof cell !== 'number' || !Number.isFinite(cell) || cell <= 0) return null;
56
+ normalized.push(Math.floor(cell));
57
+ }
58
+ }
59
+ return normalized;
60
+ };
61
+
62
+ const AutoLayoutDemo = () => {
63
+ const demo = autoLayoutDemoConfig;
64
+ const { containerRef, editorRef, diagramState, selection, snapEnabled, setSnapEnabled } = useDemoEditor({
65
+ createState: demo.createState,
66
+ elementShapes: demo.elementShapes,
67
+ portShapes: demo.portShapes,
68
+ });
69
+
70
+ const nextOffset = useOffsetSequence();
71
+ const actionHelpers: DemoActionHelpers = useMemo(() => ({ nextOffset }), [nextOffset]);
72
+
73
+ const controls = useDemoControls({
74
+ demo,
75
+ editorRef,
76
+ diagramState,
77
+ selection,
78
+ snapEnabled,
79
+ setSnapEnabled,
80
+ actionHelpers,
81
+ });
82
+
83
+ const [targetId, setTargetId] = useState<string>(parentOptions[0].id);
84
+ const [mode, setMode] = useState<ElementLayoutMode>('horizontal');
46
85
  const [align, setAlign] = useState<ElementLayout['align']>('center');
86
+ const [autoResize, setAutoResize] = useState<ElementLayoutAutoResizeMode>('grow');
47
87
  const [padding, setPadding] = useState<number>(12);
48
88
  const [gap, setGap] = useState<number>(12);
49
89
  const [childFitMainAxis, setChildFitMainAxis] = useState<ElementLayout['childFitMainAxis']>('none');
50
90
  const [childFitCrossAxis, setChildFitCrossAxis] = useState<ElementLayout['childFitCrossAxis']>('none');
51
- const [childFitMinWidth, setChildFitMinWidth] = useState<number | ''>('');
52
- const [childFitMinHeight, setChildFitMinHeight] = useState<number | ''>('');
53
- const [childFitMaxWidth, setChildFitMaxWidth] = useState<number | ''>('');
54
- const [childFitMaxHeight, setChildFitMaxHeight] = useState<number | ''>('');
91
+ const [childMinWidth, setChildMinWidth] = useState<number | ''>('');
92
+ const [childMinHeight, setChildMinHeight] = useState<number | ''>('');
93
+ const [childMaxWidth, setChildMaxWidth] = useState<number | ''>('');
94
+ const [childMaxHeight, setChildMaxHeight] = useState<number | ''>('');
95
+ const [gridTemplateText, setGridTemplateText] = useState<string>(defaultGridTemplateText);
96
+ const [gridChildWidthResizeEnabled, setGridChildWidthResizeEnabled] = useState<boolean>(false);
55
97
  const [labelReservedMode, setLabelReservedMode] = useState<ElementLayoutLabelReservedSpaceMode>('none');
56
98
  const [labelReservedFixedSize, setLabelReservedFixedSize] = useState<number>(32);
57
99
  const [labelReservedMinSize, setLabelReservedMinSize] = useState<number | ''>('');
58
100
  const [labelReservedMaxSize, setLabelReservedMaxSize] = useState<number | ''>('');
59
101
  const [lastTrigger, setLastTrigger] = useState<string>('None yet');
60
-
61
- useEffect(() => {
62
- const editor = editorRef.current;
63
- if (!editor) return undefined;
64
- const disposers = [
65
- editor.on('elementMoved', () => setLastTrigger('child moved')),
66
- editor.on('elementResized', () => setLastTrigger('child resized')),
67
- editor.on('elementDeleted', () => setLastTrigger('child removed')),
68
- editor.on('change', () => setLastTrigger((prev) => prev)),
69
- ];
70
- return () => disposers.forEach((off) => off && off());
71
- }, [editorRef]);
72
-
73
- useEffect(() => {
74
- if (!diagramState) return;
75
- if (selection.length === 1) {
76
- const selectedId = selection[0];
77
- const exists = diagramState.elements.some((el) => el.id === selectedId);
78
- if (exists && selectedId !== targetId) {
79
- setTargetId(selectedId);
80
- }
81
- }
82
- }, [selection, diagramState, targetId]);
83
-
84
- useEffect(() => {
85
- if (!diagramState) return;
86
- const exists = diagramState.elements.some((el) => el.id === targetId);
87
- if (!exists) {
88
- setTargetId(parentOptions[0].id);
89
- }
90
- }, [diagramState, targetId]);
91
-
92
- const targetElement = useMemo(
93
- () => diagramState?.elements.find((el) => el.id === targetId),
94
- [diagramState, targetId],
95
- );
96
-
97
- const targetOptions = useMemo(() => {
98
- const seen = new Set<string>();
99
- const options: Array<{ id: string; label: string }> = [];
100
- parentOptions.forEach((option) => {
101
- if (!seen.has(option.id)) {
102
- options.push(option);
103
- seen.add(option.id);
104
- }
105
- });
106
- if (diagramState) {
107
- diagramState.elements
108
- .filter((el) => (el.layout && el.layout.mode !== 'manual') || el.parentId)
109
- .forEach((el) => {
110
- if (!seen.has(el.id)) {
111
- const modeLabel = el.layout?.mode ?? 'manual';
112
- options.push({ id: el.id, label: `${el.id} (${modeLabel})` });
113
- seen.add(el.id);
114
- }
115
- });
116
- }
117
- if (targetElement && !seen.has(targetElement.id)) {
118
- options.push({ id: targetElement.id, label: `${targetElement.id} (selected)` });
119
- }
120
- return options;
121
- }, [diagramState, targetElement]);
122
-
123
- useEffect(() => {
124
- const parent = diagramState?.elements.find((el) => el.id === targetId);
125
- if (!parent) return;
126
- const layout = parent.layout;
127
- setMode(layout?.mode ?? 'manual');
128
- setAlign(layout?.align ?? 'center');
102
+
103
+ useEffect(() => {
104
+ const editor = editorRef.current;
105
+ if (!editor) return undefined;
106
+ const disposers = [
107
+ editor.on('elementMoved', () => setLastTrigger('child moved')),
108
+ editor.on('elementResized', () => setLastTrigger('child resized')),
109
+ editor.on('gridLayoutChanged', (event) => setLastTrigger(`grid layout changed (${event.reason})`)),
110
+ editor.on('elementDeleted', () => setLastTrigger('child removed')),
111
+ editor.on('change', () => setLastTrigger((prev) => prev)),
112
+ ];
113
+ return () => disposers.forEach((off) => off && off());
114
+ }, [editorRef]);
115
+
116
+ useEffect(() => {
117
+ if (!diagramState) return;
118
+ if (selection.length === 1) {
119
+ const selectedId = selection[0];
120
+ const exists = diagramState.elements.some((el) => el.id === selectedId);
121
+ if (exists && selectedId !== targetId) {
122
+ setTargetId(selectedId);
123
+ }
124
+ }
125
+ }, [selection, diagramState, targetId]);
126
+
127
+ useEffect(() => {
128
+ if (!diagramState) return;
129
+ const exists = diagramState.elements.some((el) => el.id === targetId);
130
+ if (!exists) {
131
+ setTargetId(parentOptions[0].id);
132
+ }
133
+ }, [diagramState, targetId]);
134
+
135
+ const targetElement = useMemo(
136
+ () => diagramState?.elements.find((el) => el.id === targetId),
137
+ [diagramState, targetId],
138
+ );
139
+
140
+ const targetOptions = useMemo(() => {
141
+ const seen = new Set<string>();
142
+ const options: Array<{ id: string; label: string }> = [];
143
+ parentOptions.forEach((option) => {
144
+ if (!seen.has(option.id)) {
145
+ options.push(option);
146
+ seen.add(option.id);
147
+ }
148
+ });
149
+ if (diagramState) {
150
+ diagramState.elements
151
+ .filter((el) => (el.layout && el.layout.mode !== 'manual') || el.parentId)
152
+ .forEach((el) => {
153
+ if (!seen.has(el.id)) {
154
+ const modeLabel = el.layout?.mode ?? 'manual';
155
+ options.push({ id: el.id, label: `${el.id} (${modeLabel})` });
156
+ seen.add(el.id);
157
+ }
158
+ });
159
+ }
160
+ if (targetElement && !seen.has(targetElement.id)) {
161
+ options.push({ id: targetElement.id, label: `${targetElement.id} (selected)` });
162
+ }
163
+ return options;
164
+ }, [diagramState, targetElement]);
165
+
166
+ useEffect(() => {
167
+ const parent = diagramState?.elements.find((el) => el.id === targetId);
168
+ if (!parent) return;
169
+ const layout = parent.layout;
170
+ setMode(layout?.mode ?? 'manual');
171
+ setAlign(layout?.align ?? 'center');
172
+ setAutoResize(layout?.autoResize ?? 'grow');
129
173
  const paddingValue =
130
174
  typeof layout?.padding === 'number'
131
175
  ? layout.padding
@@ -136,60 +180,61 @@ const AutoLayoutDemo = () => {
136
180
  setGap(layout?.gap ?? 12);
137
181
  setChildFitMainAxis(layout?.childFitMainAxis ?? 'none');
138
182
  setChildFitCrossAxis(layout?.childFitCrossAxis ?? 'none');
139
- setChildFitMinWidth(layout?.childFitMinSize?.width ?? '');
140
- setChildFitMinHeight(layout?.childFitMinSize?.height ?? '');
141
- setChildFitMaxWidth(layout?.childFitMaxSize?.width ?? '');
142
- setChildFitMaxHeight(layout?.childFitMaxSize?.height ?? '');
183
+ setChildMinWidth(layout?.childMinWidth ?? layout?.childFitMinSize?.width ?? '');
184
+ setChildMinHeight(layout?.childMinHeight ?? layout?.childFitMinSize?.height ?? '');
185
+ setChildMaxWidth(layout?.childMaxWidth ?? layout?.childFitMaxSize?.width ?? '');
186
+ setChildMaxHeight(layout?.childMaxHeight ?? layout?.childFitMaxSize?.height ?? '');
187
+ setGridTemplateText(serializeGridTemplate(layout?.gridTemplate));
188
+ setGridChildWidthResizeEnabled(Boolean(layout?.gridChildWidthResizeEnabled));
143
189
  setLabelReservedMode(layout?.labelReservedSpace?.mode ?? 'none');
144
190
  setLabelReservedFixedSize(layout?.labelReservedSpace?.size ?? 32);
145
191
  setLabelReservedMinSize(layout?.labelReservedSpace?.minSize ?? '');
146
192
  setLabelReservedMaxSize(layout?.labelReservedSpace?.maxSize ?? '');
147
193
  }, [diagramState, targetId]);
148
-
149
- const childOrder = useMemo(() => {
150
- if (!diagramState) return [];
151
- const children = diagramState.elements.filter((el) => el.parentId === targetId);
152
- const parent = diagramState.elements.find((el) => el.id === targetId);
153
- const axis = parent?.layout?.mode === 'vertical' ? 'y' : 'x';
154
- return [...children].sort((a, b) => {
155
- const aPos = axis === 'y' ? a.position.y : a.position.x;
156
- const bPos = axis === 'y' ? b.position.y : b.position.x;
157
- if (aPos === bPos) return a.id.localeCompare(b.id);
158
- return aPos - bPos;
159
- });
160
- }, [diagramState, targetId]);
161
-
162
- const parentSummaries = useMemo(() => {
163
- return parentOptions.map((option) => {
164
- const parent = diagramState?.elements.find((el) => el.id === option.id);
165
- const childCount = diagramState?.elements.filter((el) => el.parentId === option.id).length ?? 0;
166
- return {
167
- ...option,
168
- layoutMode: parent?.layout?.mode ?? 'manual',
169
- size: parent?.size ?? { width: 0, height: 0 },
170
- childCount,
171
- };
172
- });
173
- }, [diagramState]);
174
-
194
+
195
+ const childOrder = useMemo(() => {
196
+ if (!diagramState) return [];
197
+ const children = diagramState.elements.filter((el) => el.parentId === targetId);
198
+ const parent = diagramState.elements.find((el) => el.id === targetId);
199
+ const layoutMode = parent?.layout?.mode;
200
+ return [...children].sort((a, b) => {
201
+ if (layoutMode === 'vertical' || layoutMode === 'grid') {
202
+ if (a.position.y === b.position.y) {
203
+ if (a.position.x === b.position.x) return a.id.localeCompare(b.id);
204
+ return a.position.x - b.position.x;
205
+ }
206
+ return a.position.y - b.position.y;
207
+ }
208
+ if (a.position.x === b.position.x) {
209
+ if (a.position.y === b.position.y) return a.id.localeCompare(b.id);
210
+ return a.position.y - b.position.y;
211
+ }
212
+ return a.position.x - b.position.x;
213
+ });
214
+ }, [diagramState, targetId]);
215
+
216
+ const parentSummaries = useMemo(() => {
217
+ return parentOptions.map((option) => {
218
+ const parent = diagramState?.elements.find((el) => el.id === option.id);
219
+ const childCount = diagramState?.elements.filter((el) => el.parentId === option.id).length ?? 0;
220
+ return {
221
+ ...option,
222
+ layoutMode: parent?.layout?.mode ?? 'manual',
223
+ size: parent?.size ?? { width: 0, height: 0 },
224
+ childCount,
225
+ };
226
+ });
227
+ }, [diagramState]);
228
+
175
229
  const handleApplyLayout = () => {
176
- const editor = editorRef.current;
230
+ const editor = editorRef.current;
177
231
  if (!editor) return;
178
232
  if (!targetElement) return;
179
- const childFitMinSize =
180
- childFitMinWidth === '' && childFitMinHeight === ''
181
- ? undefined
182
- : {
183
- ...(childFitMinWidth === '' ? {} : { width: childFitMinWidth }),
184
- ...(childFitMinHeight === '' ? {} : { height: childFitMinHeight }),
185
- };
186
- const childFitMaxSize =
187
- childFitMaxWidth === '' && childFitMaxHeight === ''
188
- ? undefined
189
- : {
190
- ...(childFitMaxWidth === '' ? {} : { width: childFitMaxWidth }),
191
- ...(childFitMaxHeight === '' ? {} : { height: childFitMaxHeight }),
192
- };
233
+ const parsedGridTemplate = mode === 'grid' ? parseGridTemplate(gridTemplateText) : undefined;
234
+ if (mode === 'grid' && parsedGridTemplate === null) {
235
+ setLastTrigger('invalid grid template JSON');
236
+ return;
237
+ }
193
238
  const labelReservedSpace =
194
239
  mode === 'manual'
195
240
  ? undefined
@@ -205,13 +250,18 @@ const AutoLayoutDemo = () => {
205
250
  ? { mode: 'manual' as const }
206
251
  : {
207
252
  mode,
253
+ autoResize,
208
254
  padding,
209
255
  gap,
210
256
  align: align ?? 'center',
211
257
  childFitMainAxis,
212
258
  childFitCrossAxis,
213
- childFitMinSize,
214
- childFitMaxSize,
259
+ ...(childMinWidth === '' ? {} : { childMinWidth }),
260
+ ...(childMinHeight === '' ? {} : { childMinHeight }),
261
+ ...(childMaxWidth === '' ? {} : { childMaxWidth }),
262
+ ...(childMaxHeight === '' ? {} : { childMaxHeight }),
263
+ ...(mode === 'grid' && parsedGridTemplate ? { gridTemplate: parsedGridTemplate } : {}),
264
+ ...(mode === 'grid' ? { gridChildWidthResizeEnabled } : {}),
215
265
  labelReservedSpace,
216
266
  };
217
267
  editor.setElementLayout(targetElement.id, layout);
@@ -226,139 +276,179 @@ const AutoLayoutDemo = () => {
226
276
  editor.updateText(label.id, content);
227
277
  setLastTrigger('label updated');
228
278
  };
229
-
230
- const handleAddChild = () => {
231
- const editor = editorRef.current;
232
- if (!editor) return;
233
- if (!targetElement) return;
234
- const offset = nextOffset();
235
- const sizeBump = (offset % 3) * 8;
236
- editor.addElement({
237
- id: `auto-child-${createId()}`,
238
- position: { x: 16 + offset, y: 16 + offset },
239
- size: { width: 50 + sizeBump, height: 30 + (offset % 2) * 6 },
240
- shapeId: demo.defaultElementShapeId ?? 'default',
241
- parentId: targetElement.id,
242
- });
243
- setLastTrigger('child added');
244
- };
245
-
246
- const handleRemoveChild = () => {
247
- const editor = editorRef.current;
248
- if (!editor) return;
249
- const children = childOrder;
250
- const target = children[children.length - 1];
251
- if (target) {
252
- editor.removeElement(target.id);
253
- setLastTrigger('child removed');
254
- }
255
- };
256
-
257
- const alignmentLabel =
258
- mode === 'horizontal' ? 'Vertical align (top/center/bottom)' : 'Horizontal align (left/center/right)';
259
-
260
- return (
261
- <section>
262
- <div style={{ marginBottom: 12 }}>
263
- <h2 style={{ marginTop: 0, marginBottom: 4 }}>{demo.title}</h2>
279
+
280
+ const handleAddChild = () => {
281
+ const editor = editorRef.current;
282
+ if (!editor) return;
283
+ if (!targetElement) return;
284
+ const offset = nextOffset();
285
+ const sizeBump = (offset % 3) * 8;
286
+ editor.addElement({
287
+ id: `auto-child-${createId()}`,
288
+ position: { x: 16 + offset, y: 16 + offset },
289
+ size: { width: 50 + sizeBump, height: 30 + (offset % 2) * 6 },
290
+ shapeId: demo.defaultElementShapeId ?? 'default',
291
+ parentId: targetElement.id,
292
+ });
293
+ setLastTrigger('child added');
294
+ };
295
+
296
+ const handleRemoveChild = () => {
297
+ const editor = editorRef.current;
298
+ if (!editor) return;
299
+ const children = childOrder;
300
+ const target = children[children.length - 1];
301
+ if (target) {
302
+ editor.removeElement(target.id);
303
+ setLastTrigger('child removed');
304
+ }
305
+ };
306
+
307
+ const handleFocusResizeLockChild = () => {
308
+ const editor = editorRef.current;
309
+ if (!editor) return;
310
+ setTargetId('layout-grid');
311
+ editor.setSelection(['grid-b']);
312
+ setLastTrigger('focus resize-lock child');
313
+ };
314
+
315
+ const handleFocusResizeLockParent = () => {
316
+ const editor = editorRef.current;
317
+ if (!editor) return;
318
+ setTargetId('layout-grid');
319
+ editor.setSelection(['layout-grid']);
320
+ setLastTrigger('focus resize-lock parent');
321
+ };
322
+
323
+ const alignmentLabel =
324
+ mode === 'horizontal'
325
+ ? 'Vertical align (top/center/bottom)'
326
+ : mode === 'vertical'
327
+ ? 'Horizontal align (left/center/right)'
328
+ : 'Cell align (start/center/end)';
329
+
330
+ return (
331
+ <section>
332
+ <div style={{ marginBottom: 12 }}>
333
+ <h2 style={{ marginTop: 0, marginBottom: 4 }}>{demo.title}</h2>
264
334
  <p style={{ marginTop: 0 }}>
265
- Try horizontal, vertical, nested, and manual layout containers. Select a layout parent directly on the canvas
266
- or pick it from the list; the controls below always act on the current selection. Drag, resize, add, or remove
267
- children to see automatic reflow. Use fit controls to distribute/stretch child sizes with optional min/max
268
- guards. Nested column shows fit propagation boundaries to its parent row.
335
+ Try horizontal, vertical, grid, nested, and manual layout containers. Select a layout parent directly on the
336
+ canvas or pick it from the list; the controls below always act on the current selection. Drag, resize, add, or
337
+ remove children to see automatic reflow. Use fit controls, explicit child min/max constraints, auto-resize
338
+ policy, 12-unit grid templates, and the optional grid child width-resize capability to validate current behavior.
339
+ </p>
340
+ <p style={{ marginTop: 0, marginBottom: 0, fontSize: 13, color: '#333' }}>
341
+ Grid child width resize is disabled by default. With the capability off, select <code>grid-b</code> and drag a
342
+ corner mostly downward: width stays stable while height changes. Turn the capability on to let horizontal drag
343
+ wrap rows automatically from a flat 12-unit slot array, then resize children in 1/12 steps. Any corner can drive width changes, width always grows or shrinks on the right, siblings keep their own widths, and dragging can swap grid item order.
269
344
  </p>
270
345
  </div>
271
- <DisplayBoxControls
272
- actions={demo.actions}
273
- snapEnabled={controls.snapEnabled}
274
- selectedLinkRouting={controls.selectedLinkRouting}
275
- canToggleLinkRouting={controls.canToggleLinkRouting}
276
- onReload={controls.handleReload}
277
- onZoomIn={controls.handleZoomIn}
278
- onZoomOut={controls.handleZoomOut}
279
- onResetViewport={controls.handleResetViewport}
280
- onToggleSnap={controls.handleToggleSnap}
281
- onManualRender={controls.handleManualRender}
282
- onToggleLinkRouting={controls.handleToggleLinkRouting}
283
- onAction={controls.handleAction}
284
- onExportImage={controls.handleExportImage}
285
- onClearExportPreview={controls.handleClearExportPreview}
286
- exportPreviewDataUrl={controls.exportPreviewDataUrl}
287
- exportError={controls.exportError}
288
- />
289
-
290
- <div style={{ display: 'grid', gap: 12, marginBottom: 12 }}>
291
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'center' }}>
292
- <label htmlFor="parent-select" style={{ fontWeight: 600 }}>
293
- Target element (follows selection)
294
- </label>
295
- <select
296
- id="parent-select"
297
- value={targetId}
298
- onChange={(event) => {
299
- const nextId = event.target.value;
300
- setTargetId(nextId);
301
- if (editorRef.current) {
302
- editorRef.current.setSelection([nextId]);
303
- }
304
- }}
305
- style={{ padding: '6px 10px', minWidth: 180 }}
306
- >
307
- {targetOptions.map((option) => (
308
- <option key={option.id} value={option.id}>
309
- {option.label}
310
- </option>
311
- ))}
312
- </select>
313
-
314
- <label htmlFor="mode-select" style={{ fontWeight: 600 }}>
315
- Layout
316
- </label>
317
- <select
318
- id="mode-select"
319
- value={mode}
320
- onChange={(event) => setMode(event.target.value as ElementLayoutMode)}
321
- style={{ padding: '6px 10px', minWidth: 140 }}
322
- >
323
- <option value="manual">Manual</option>
324
- <option value="horizontal">Horizontal</option>
325
- <option value="vertical">Vertical</option>
326
- </select>
327
-
328
- <label htmlFor="align-select" style={{ fontWeight: 600 }}>
329
- {alignmentLabel}
330
- </label>
331
- <select
332
- id="align-select"
333
- value={align ?? 'center'}
334
- onChange={(event) => setAlign(event.target.value as ElementLayout['align'])}
335
- style={{ padding: '6px 10px', minWidth: 140 }}
336
- >
337
- <option value="start">{mode === 'horizontal' ? 'Top' : 'Left'}</option>
338
- <option value="center">Center</option>
339
- <option value="end">{mode === 'horizontal' ? 'Bottom' : 'Right'}</option>
340
- </select>
341
-
342
- <label htmlFor="padding-input" style={{ fontWeight: 600 }}>
343
- Padding
344
- </label>
345
- <input
346
- id="padding-input"
347
- type="number"
348
- value={padding}
349
- onChange={(event) => setPadding(Number(event.target.value) || 0)}
350
- style={{ width: 72, padding: '6px 8px' }}
351
- min={0}
352
- />
353
-
346
+ <DisplayBoxControls
347
+ actions={demo.actions}
348
+ snapEnabled={controls.snapEnabled}
349
+ selectedLinkRouting={controls.selectedLinkRouting}
350
+ canToggleLinkRouting={controls.canToggleLinkRouting}
351
+ onReload={controls.handleReload}
352
+ onZoomIn={controls.handleZoomIn}
353
+ onZoomOut={controls.handleZoomOut}
354
+ onResetViewport={controls.handleResetViewport}
355
+ onToggleSnap={controls.handleToggleSnap}
356
+ onManualRender={controls.handleManualRender}
357
+ onToggleLinkRouting={controls.handleToggleLinkRouting}
358
+ onAction={controls.handleAction}
359
+ onExportImage={controls.handleExportImage}
360
+ onClearExportPreview={controls.handleClearExportPreview}
361
+ exportPreviewDataUrl={controls.exportPreviewDataUrl}
362
+ exportError={controls.exportError}
363
+ />
364
+
365
+ <div style={{ display: 'grid', gap: 12, marginBottom: 12 }}>
366
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'center' }}>
367
+ <label htmlFor="parent-select" style={{ fontWeight: 600 }}>
368
+ Target element (follows selection)
369
+ </label>
370
+ <select
371
+ id="parent-select"
372
+ value={targetId}
373
+ onChange={(event) => {
374
+ const nextId = event.target.value;
375
+ setTargetId(nextId);
376
+ if (editorRef.current) {
377
+ editorRef.current.setSelection([nextId]);
378
+ }
379
+ }}
380
+ style={{ padding: '6px 10px', minWidth: 180 }}
381
+ >
382
+ {targetOptions.map((option) => (
383
+ <option key={option.id} value={option.id}>
384
+ {option.label}
385
+ </option>
386
+ ))}
387
+ </select>
388
+
389
+ <label htmlFor="mode-select" style={{ fontWeight: 600 }}>
390
+ Layout
391
+ </label>
392
+ <select
393
+ id="mode-select"
394
+ value={mode}
395
+ onChange={(event) => setMode(event.target.value as ElementLayoutMode)}
396
+ style={{ padding: '6px 10px', minWidth: 140 }}
397
+ >
398
+ <option value="manual">Manual</option>
399
+ <option value="horizontal">Horizontal</option>
400
+ <option value="vertical">Vertical</option>
401
+ <option value="grid">Grid</option>
402
+ </select>
403
+
404
+ <label htmlFor="align-select" style={{ fontWeight: 600 }}>
405
+ {alignmentLabel}
406
+ </label>
407
+ <select
408
+ id="align-select"
409
+ value={align ?? 'center'}
410
+ onChange={(event) => setAlign(event.target.value as ElementLayout['align'])}
411
+ style={{ padding: '6px 10px', minWidth: 140 }}
412
+ >
413
+ <option value="start">{mode === 'horizontal' ? 'Top' : mode === 'vertical' ? 'Left' : 'Start'}</option>
414
+ <option value="center">Center</option>
415
+ <option value="end">{mode === 'horizontal' ? 'Bottom' : mode === 'vertical' ? 'Right' : 'End'}</option>
416
+ </select>
417
+
418
+ <label htmlFor="auto-resize-select" style={{ fontWeight: 600 }}>
419
+ Auto resize
420
+ </label>
421
+ <select
422
+ id="auto-resize-select"
423
+ value={autoResize}
424
+ onChange={(event) => setAutoResize(event.target.value as ElementLayoutAutoResizeMode)}
425
+ style={{ padding: '6px 10px', minWidth: 130 }}
426
+ disabled={mode === 'manual'}
427
+ >
428
+ <option value="grow">Grow only</option>
429
+ <option value="grow-shrink">Grow + shrink</option>
430
+ </select>
431
+
432
+ <label htmlFor="padding-input" style={{ fontWeight: 600 }}>
433
+ Padding
434
+ </label>
435
+ <input
436
+ id="padding-input"
437
+ type="number"
438
+ value={padding}
439
+ onChange={(event) => setPadding(Number(event.target.value) || 0)}
440
+ style={{ width: 72, padding: '6px 8px' }}
441
+ min={0}
442
+ />
443
+
354
444
  <label htmlFor="gap-input" style={{ fontWeight: 600 }}>
355
445
  Gap
356
446
  </label>
357
447
  <input
358
448
  id="gap-input"
359
- type="number"
360
- value={gap}
361
- onChange={(event) => setGap(Number(event.target.value) || 0)}
449
+ type="number"
450
+ value={gap}
451
+ onChange={(event) => setGap(Number(event.target.value) || 0)}
362
452
  style={{ width: 72, padding: '6px 8px' }}
363
453
  min={0}
364
454
  />
@@ -371,6 +461,7 @@ const AutoLayoutDemo = () => {
371
461
  value={childFitMainAxis ?? 'none'}
372
462
  onChange={(event) => setChildFitMainAxis(event.target.value as ElementLayout['childFitMainAxis'])}
373
463
  style={{ padding: '6px 10px', minWidth: 130 }}
464
+ disabled={mode === 'manual' || mode === 'grid'}
374
465
  >
375
466
  <option value="none">Off</option>
376
467
  <option value="distribute">Distribute</option>
@@ -384,28 +475,29 @@ const AutoLayoutDemo = () => {
384
475
  value={childFitCrossAxis ?? 'none'}
385
476
  onChange={(event) => setChildFitCrossAxis(event.target.value as ElementLayout['childFitCrossAxis'])}
386
477
  style={{ padding: '6px 10px', minWidth: 130 }}
478
+ disabled={mode === 'manual'}
387
479
  >
388
480
  <option value="none">Off</option>
389
481
  <option value="stretch">Stretch</option>
390
482
  </select>
391
483
 
392
484
  <label htmlFor="fit-min-width-input" style={{ fontWeight: 600 }}>
393
- Min W/H
485
+ Child min W/H
394
486
  </label>
395
487
  <div style={{ display: 'inline-flex', gap: 6 }}>
396
488
  <input
397
489
  id="fit-min-width-input"
398
490
  type="number"
399
- value={childFitMinWidth}
400
- onChange={(event) => setChildFitMinWidth(event.target.value === '' ? '' : Math.max(0, Number(event.target.value) || 0))}
491
+ value={childMinWidth}
492
+ onChange={(event) => setChildMinWidth(event.target.value === '' ? '' : Math.max(0, Number(event.target.value) || 0))}
401
493
  style={{ width: 64, padding: '6px 8px' }}
402
494
  min={0}
403
495
  placeholder="w"
404
496
  />
405
497
  <input
406
498
  type="number"
407
- value={childFitMinHeight}
408
- onChange={(event) => setChildFitMinHeight(event.target.value === '' ? '' : Math.max(0, Number(event.target.value) || 0))}
499
+ value={childMinHeight}
500
+ onChange={(event) => setChildMinHeight(event.target.value === '' ? '' : Math.max(0, Number(event.target.value) || 0))}
409
501
  style={{ width: 64, padding: '6px 8px' }}
410
502
  min={0}
411
503
  placeholder="h"
@@ -413,28 +505,55 @@ const AutoLayoutDemo = () => {
413
505
  </div>
414
506
 
415
507
  <label htmlFor="fit-max-width-input" style={{ fontWeight: 600 }}>
416
- Max W/H
508
+ Child max W/H
417
509
  </label>
418
510
  <div style={{ display: 'inline-flex', gap: 6 }}>
419
511
  <input
420
512
  id="fit-max-width-input"
421
513
  type="number"
422
- value={childFitMaxWidth}
423
- onChange={(event) => setChildFitMaxWidth(event.target.value === '' ? '' : Math.max(0, Number(event.target.value) || 0))}
514
+ value={childMaxWidth}
515
+ onChange={(event) => setChildMaxWidth(event.target.value === '' ? '' : Math.max(0, Number(event.target.value) || 0))}
424
516
  style={{ width: 64, padding: '6px 8px' }}
425
517
  min={0}
426
518
  placeholder="w"
427
519
  />
428
520
  <input
429
521
  type="number"
430
- value={childFitMaxHeight}
431
- onChange={(event) => setChildFitMaxHeight(event.target.value === '' ? '' : Math.max(0, Number(event.target.value) || 0))}
522
+ value={childMaxHeight}
523
+ onChange={(event) => setChildMaxHeight(event.target.value === '' ? '' : Math.max(0, Number(event.target.value) || 0))}
432
524
  style={{ width: 64, padding: '6px 8px' }}
433
525
  min={0}
434
526
  placeholder="h"
435
527
  />
436
528
  </div>
437
529
 
530
+ <label htmlFor="grid-template-input" style={{ fontWeight: 600 }}>
531
+ Grid template
532
+ </label>
533
+ <input
534
+ id="grid-template-input"
535
+ type="text"
536
+ value={gridTemplateText}
537
+ onChange={(event) => setGridTemplateText(event.target.value)}
538
+ style={{ minWidth: 220, padding: '6px 8px' }}
539
+ placeholder="[12,4,8] or [3,3,6,9]"
540
+ disabled={mode !== 'grid'}
541
+ />
542
+
543
+ <label htmlFor="grid-child-width-resize-enabled" style={{ fontWeight: 600 }}>
544
+ Grid child width resize
545
+ </label>
546
+ <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
547
+ <input
548
+ id="grid-child-width-resize-enabled"
549
+ type="checkbox"
550
+ checked={gridChildWidthResizeEnabled}
551
+ onChange={(event) => setGridChildWidthResizeEnabled(event.target.checked)}
552
+ disabled={mode !== 'grid'}
553
+ />
554
+ Enable 12-unit resize on child width drag
555
+ </label>
556
+
438
557
  <label htmlFor="label-reserved-mode-select" style={{ fontWeight: 600 }}>
439
558
  Label lane
440
559
  </label>
@@ -487,26 +606,26 @@ const AutoLayoutDemo = () => {
487
606
  <button type="button" onClick={handleApplyLayout} style={{ padding: '6px 12px', fontWeight: 600 }}>
488
607
  Apply layout
489
608
  </button>
490
- <button type="button" onClick={handleAddChild} style={{ padding: '6px 10px' }} disabled={!targetElement}>
491
- Add child
492
- </button>
493
- <button type="button" onClick={handleRemoveChild} style={{ padding: '6px 10px' }} disabled={!targetElement}>
494
- Remove last child
495
- </button>
496
- <span
497
- style={{
498
- padding: '6px 10px',
499
- borderRadius: 8,
500
- background: '#f7f7f8',
501
- border: '1px solid #e0e0e0',
502
- display: 'inline-flex',
503
- gap: 6,
504
- alignItems: 'center',
505
- }}
506
- >
609
+ <button type="button" onClick={handleAddChild} style={{ padding: '6px 10px' }} disabled={!targetElement}>
610
+ Add child
611
+ </button>
612
+ <button type="button" onClick={handleRemoveChild} style={{ padding: '6px 10px' }} disabled={!targetElement}>
613
+ Remove last child
614
+ </button>
615
+ <span
616
+ style={{
617
+ padding: '6px 10px',
618
+ borderRadius: 8,
619
+ background: '#f7f7f8',
620
+ border: '1px solid #e0e0e0',
621
+ display: 'inline-flex',
622
+ gap: 6,
623
+ alignItems: 'center',
624
+ }}
625
+ >
507
626
  <span style={{ fontWeight: 600 }}>Target:</span> {targetElement ? targetElement.id : '—'}
508
- <span style={{ width: 6, height: 6, borderRadius: '50%', background: '#1f4d99', display: 'inline-block' }} />
509
- Last trigger: <strong>{lastTrigger}</strong>
627
+ <span style={{ width: 6, height: 6, borderRadius: '50%', background: '#1f4d99', display: 'inline-block' }} />
628
+ Last trigger: <strong>{lastTrigger}</strong>
510
629
  </span>
511
630
  <button
512
631
  type="button"
@@ -524,80 +643,95 @@ const AutoLayoutDemo = () => {
524
643
  >
525
644
  Label long
526
645
  </button>
646
+ <button type="button" onClick={handleFocusResizeLockChild} style={{ padding: '6px 10px' }}>
647
+ Focus lock child
648
+ </button>
649
+ <button type="button" onClick={handleFocusResizeLockParent} style={{ padding: '6px 10px' }}>
650
+ Focus lock parent
651
+ </button>
527
652
  </div>
528
-
529
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
530
- <div>
531
- <div style={{ fontWeight: 600, marginBottom: 6 }}>Child order (based on layout axis)</div>
532
- <ol style={{ marginTop: 0, paddingLeft: 18 }}>
533
- {childOrder.map((child) => (
534
- <li key={child.id} style={{ marginBottom: 4, fontSize: 13 }}>
535
- {child.id} — position ({Math.round(child.position.x)}, {Math.round(child.position.y)}) size (
536
- {child.size.width}×{child.size.height})
537
- </li>
538
- ))}
539
- {childOrder.length === 0 && <li style={{ color: '#555' }}>No children.</li>}
540
- </ol>
541
- <p style={{ fontSize: 12, color: '#555', marginTop: 4 }}>
542
- Drag a child before/after its sibling or resize it. The order above reflects the reflow sequence.
543
- </p>
544
- </div>
545
- <div>
546
- <div style={{ fontWeight: 600, marginBottom: 6 }}>Scenario tips</div>
547
- <ul style={{ marginTop: 0, paddingLeft: 18, fontSize: 13 }}>
653
+
654
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
655
+ <div>
656
+ <div style={{ fontWeight: 600, marginBottom: 6 }}>Child order (axis or grid row-major)</div>
657
+ <ol style={{ marginTop: 0, paddingLeft: 18 }}>
658
+ {childOrder.map((child) => (
659
+ <li key={child.id} style={{ marginBottom: 4, fontSize: 13 }}>
660
+ {child.id} — position ({Math.round(child.position.x)}, {Math.round(child.position.y)}) size (
661
+ {child.size.width}×{child.size.height})
662
+ </li>
663
+ ))}
664
+ {childOrder.length === 0 && <li style={{ color: '#555' }}>No children.</li>}
665
+ </ol>
666
+ <p style={{ fontSize: 12, color: '#555', marginTop: 4 }}>
667
+ Drag a child before/after its sibling or resize it. The order above reflects the reflow sequence.
668
+ </p>
669
+ </div>
670
+ <div>
671
+ <div style={{ fontWeight: 600, marginBottom: 6 }}>Scenario tips</div>
672
+ <ul style={{ marginTop: 0, paddingLeft: 18, fontSize: 13 }}>
673
+ <li>Resize lock: with grid child width resize off, select grid-b and drag diagonally; width should stay fixed.</li>
674
+ <li>Enable grid child width resize, then shrink a full-width grid child; it should snap smaller without flashing, and later siblings should keep their own widths.</li>
675
+ <li>Enable grid child width resize, then grow a child; siblings on the right should keep their own widths and only move down when they no longer fit.</li>
676
+ <li>Try dragging a left resize handle on the first child in a row; the child should still grow, but only to the right.</li>
677
+ <li>Try dragging a grid child across a sibling; the row-major order should swap to match the drag position.</li>
678
+ <li>Whenever enabled child width resize changes row allocation, the last-trigger badge should report grid layout changed.</li>
679
+ <li>Parent resize: select layout-grid, drag corner; parent width and height should both track drag.</li>
548
680
  <li>Horizontal: try center vs bottom alignment and larger padding.</li>
549
681
  <li>Vertical: see parent grow taller as children stack.</li>
682
+ <li>Grid: apply templates like [12,4,8] and [3,3,6,9]. Rows wrap automatically when the next item cannot fit.</li>
683
+ <li>Auto resize: compare grow-only vs grow-shrink while adding/removing children.</li>
550
684
  <li>Label lane: compare none/fixed/flexible and observe children start below the reserved lane.</li>
551
685
  <li>Fit main-axis distribute fills inner layout space across siblings.</li>
552
686
  <li>Fit cross-axis stretch extends children across inner cross axis.</li>
553
- <li>Min/max fit guards cap distributed or stretched child sizes.</li>
687
+ <li>Child min/max guards cap distributed, stretched, and grid cell sizes.</li>
554
688
  <li>Nested: the left column owns its own children and expands the row.</li>
555
- <li>Deep bounds: stack-overflow widens the nested column and the outer row.</li>
556
- <li>Re-order: drag along the layout axis then apply layout to keep the new order.</li>
557
- <li>Select any container on the canvas to target it instantly—controls follow your selection.</li>
558
- <li>Manual: switch to manual to compare with auto layout.</li>
559
- </ul>
560
- </div>
561
- </div>
562
-
563
- <div>
564
- <div style={{ fontWeight: 600, marginBottom: 6 }}>Parent bounds snapshot</div>
565
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 8 }}>
566
- {parentSummaries.map((parent) => (
567
- <div
568
- key={parent.id}
569
- style={{
570
- padding: 10,
571
- borderRadius: 8,
572
- border: '1px solid #e0e0e0',
573
- background: '#fafafa',
574
- boxShadow: '0 1px 0 rgba(0,0,0,0.04)',
575
- }}
576
- >
577
- <div style={{ fontWeight: 700 }}>{parent.label}</div>
578
- <div style={{ fontSize: 12, color: '#444', marginTop: 2 }}>
579
- Layout: <code>{parent.layoutMode}</code> · {parent.childCount} children
580
- </div>
581
- <div
582
- style={{
583
- marginTop: 4,
584
- fontFamily:
585
- 'ui-monospace, SFMono-Regular, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace',
586
- fontSize: 12,
587
- }}
588
- >
589
- {Math.round(parent.size.width)} × {Math.round(parent.size.height)} px
590
- </div>
591
- </div>
592
- ))}
593
- </div>
594
- </div>
595
- </div>
596
-
597
- <DisplayBoxStage containerRef={containerRef} stageStyle={gridStageStyle} />
598
- </section>
599
- );
600
- };
601
-
602
- export default AutoLayoutDemo;
603
-
689
+ <li>Deep bounds: stack-overflow widens the nested column and the outer row.</li>
690
+ <li>Re-order: drag along the layout axis then apply layout to keep the new order.</li>
691
+ <li>Select any container on the canvas to target it instantly—controls follow your selection.</li>
692
+ <li>Manual: switch to manual to compare with auto layout.</li>
693
+ </ul>
694
+ </div>
695
+ </div>
696
+
697
+ <div>
698
+ <div style={{ fontWeight: 600, marginBottom: 6 }}>Parent bounds snapshot</div>
699
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 8 }}>
700
+ {parentSummaries.map((parent) => (
701
+ <div
702
+ key={parent.id}
703
+ style={{
704
+ padding: 10,
705
+ borderRadius: 8,
706
+ border: '1px solid #e0e0e0',
707
+ background: '#fafafa',
708
+ boxShadow: '0 1px 0 rgba(0,0,0,0.04)',
709
+ }}
710
+ >
711
+ <div style={{ fontWeight: 700 }}>{parent.label}</div>
712
+ <div style={{ fontSize: 12, color: '#444', marginTop: 2 }}>
713
+ Layout: <code>{parent.layoutMode}</code> · {parent.childCount} children
714
+ </div>
715
+ <div
716
+ style={{
717
+ marginTop: 4,
718
+ fontFamily:
719
+ 'ui-monospace, SFMono-Regular, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace',
720
+ fontSize: 12,
721
+ }}
722
+ >
723
+ {Math.round(parent.size.width)} × {Math.round(parent.size.height)} px
724
+ </div>
725
+ </div>
726
+ ))}
727
+ </div>
728
+ </div>
729
+ </div>
730
+
731
+ <DisplayBoxStage containerRef={containerRef} stageStyle={gridStageStyle} />
732
+ </section>
733
+ );
734
+ };
735
+
736
+ export default AutoLayoutDemo;
737
+