orcasvn-react-diagrams 0.2.0 → 0.2.2

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 (133) hide show
  1. package/README.md +22 -1
  2. package/ai/api-contract.json +57 -5
  3. package/ai/invariants.json +5 -3
  4. package/ai/manifest.json +1 -1
  5. package/dist/cjs/examples.js +11775 -0
  6. package/dist/cjs/index.js +3889 -1112
  7. package/dist/cjs/types/api/createDiagramEditor.d.ts +7 -2
  8. package/dist/cjs/types/api/types.d.ts +178 -0
  9. package/dist/cjs/types/displaybox/demos/DeletionEventsDemoTab.d.ts +3 -0
  10. package/dist/cjs/types/displaybox/demos/ShapeHoverControlsDemoTab.d.ts +3 -0
  11. package/dist/cjs/types/displaybox/demos/TextLayoutDemoTab.d.ts +3 -0
  12. package/dist/cjs/types/displaybox/demos/deletionEventsDemo.d.ts +2 -0
  13. package/dist/cjs/types/displaybox/demos/rotatedCreationDemo.d.ts +2 -0
  14. package/dist/cjs/types/displaybox/demos/roundedRectRadiusDemo.d.ts +2 -0
  15. package/dist/cjs/types/displaybox/demos/shapeBorderMovementDemo.d.ts +2 -0
  16. package/dist/cjs/types/displaybox/demos/shapeHoverControlsDemo.d.ts +10 -0
  17. package/dist/cjs/types/displaybox/demos/textDemo.d.ts +4 -0
  18. package/dist/cjs/types/displaybox/useDemoEditor.d.ts +5 -2
  19. package/dist/cjs/types/engine/AutoLayoutService.d.ts +24 -0
  20. package/dist/cjs/types/engine/DiagramEngine.d.ts +32 -14
  21. package/dist/cjs/types/engine/EngineCommands.d.ts +4 -1
  22. package/dist/cjs/types/engine/LinkRoutingService.d.ts +35 -0
  23. package/dist/cjs/types/engine/MutationPipeline.d.ts +23 -0
  24. package/dist/cjs/types/engine/TextLayoutService.d.ts +40 -0
  25. package/dist/cjs/types/examples/index.d.ts +2 -0
  26. package/dist/cjs/types/measure/textStyleDefaults.d.ts +9 -0
  27. package/dist/cjs/types/models/DiagramModel.d.ts +1 -0
  28. package/dist/cjs/types/models/ElementModel.d.ts +1 -0
  29. package/dist/cjs/types/models/PortModel.d.ts +3 -0
  30. package/dist/cjs/types/models/TextModel.d.ts +8 -0
  31. package/dist/cjs/types/renderer/RenderTypes.d.ts +34 -1
  32. package/dist/cjs/types/renderer/konva/KonvaHitTester.d.ts +1 -1
  33. package/dist/cjs/types/renderer/konva/KonvaInteraction.d.ts +53 -3
  34. package/dist/cjs/types/renderer/konva/KonvaNodeFactory.d.ts +18 -1
  35. package/dist/cjs/types/renderer/konva/KonvaRenderer.d.ts +49 -2
  36. package/dist/cjs/types/shapes/BuiltInShapes.d.ts +107 -0
  37. package/dist/cjs/types/shapes/__tests__/BuiltInShapes.test.d.ts +1 -0
  38. package/dist/cjs/types/shapes/index.d.ts +1 -0
  39. package/dist/cjs/types/utils/__tests__/borderGeometry.test.d.ts +1 -0
  40. package/dist/cjs/types/utils/borderGeometry.d.ts +6 -0
  41. package/dist/cjs/types/utils/geometry.d.ts +22 -0
  42. package/dist/esm/examples.js +11767 -0
  43. package/dist/esm/examples.js.map +1 -0
  44. package/dist/esm/index.js +3890 -1113
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/types/api/createDiagramEditor.d.ts +7 -2
  47. package/dist/esm/types/api/types.d.ts +178 -0
  48. package/dist/esm/types/displaybox/demos/DeletionEventsDemoTab.d.ts +3 -0
  49. package/dist/esm/types/displaybox/demos/ShapeHoverControlsDemoTab.d.ts +3 -0
  50. package/dist/esm/types/displaybox/demos/TextLayoutDemoTab.d.ts +3 -0
  51. package/dist/esm/types/displaybox/demos/deletionEventsDemo.d.ts +2 -0
  52. package/dist/esm/types/displaybox/demos/rotatedCreationDemo.d.ts +2 -0
  53. package/dist/esm/types/displaybox/demos/roundedRectRadiusDemo.d.ts +2 -0
  54. package/dist/esm/types/displaybox/demos/shapeBorderMovementDemo.d.ts +2 -0
  55. package/dist/esm/types/displaybox/demos/shapeHoverControlsDemo.d.ts +10 -0
  56. package/dist/esm/types/displaybox/demos/textDemo.d.ts +4 -0
  57. package/dist/esm/types/displaybox/useDemoEditor.d.ts +5 -2
  58. package/dist/esm/types/engine/AutoLayoutService.d.ts +24 -0
  59. package/dist/esm/types/engine/DiagramEngine.d.ts +32 -14
  60. package/dist/esm/types/engine/EngineCommands.d.ts +4 -1
  61. package/dist/esm/types/engine/LinkRoutingService.d.ts +35 -0
  62. package/dist/esm/types/engine/MutationPipeline.d.ts +23 -0
  63. package/dist/esm/types/engine/TextLayoutService.d.ts +40 -0
  64. package/dist/esm/types/examples/index.d.ts +2 -0
  65. package/dist/esm/types/measure/textStyleDefaults.d.ts +9 -0
  66. package/dist/esm/types/models/DiagramModel.d.ts +1 -0
  67. package/dist/esm/types/models/ElementModel.d.ts +1 -0
  68. package/dist/esm/types/models/PortModel.d.ts +3 -0
  69. package/dist/esm/types/models/TextModel.d.ts +8 -0
  70. package/dist/esm/types/renderer/RenderTypes.d.ts +34 -1
  71. package/dist/esm/types/renderer/konva/KonvaHitTester.d.ts +1 -1
  72. package/dist/esm/types/renderer/konva/KonvaInteraction.d.ts +53 -3
  73. package/dist/esm/types/renderer/konva/KonvaNodeFactory.d.ts +18 -1
  74. package/dist/esm/types/renderer/konva/KonvaRenderer.d.ts +49 -2
  75. package/dist/esm/types/shapes/BuiltInShapes.d.ts +107 -0
  76. package/dist/esm/types/shapes/__tests__/BuiltInShapes.test.d.ts +1 -0
  77. package/dist/esm/types/shapes/index.d.ts +1 -0
  78. package/dist/esm/types/utils/__tests__/borderGeometry.test.d.ts +1 -0
  79. package/dist/esm/types/utils/borderGeometry.d.ts +6 -0
  80. package/dist/esm/types/utils/geometry.d.ts +22 -0
  81. package/dist/examples.d.ts +532 -0
  82. package/dist/index.d.ts +233 -2
  83. package/docs/API_CONTRACT.md +59 -3
  84. package/docs/ARCHITECTURE.md +1 -0
  85. package/docs/CAPABILITIES.md +3 -1
  86. package/docs/COMMANDS_EVENTS.md +5 -0
  87. package/docs/DOCUMENTATION_WORKFLOW.md +6 -8
  88. package/docs/INTEGRATION_PLAYBOOK.md +2 -0
  89. package/docs/PORTING_CHECKLIST.md +1 -0
  90. package/docs/STATE_INVARIANTS.md +4 -0
  91. package/package.json +20 -10
  92. package/src/displaybox/demos/AutoLayoutDemoTab.tsx +501 -0
  93. package/src/displaybox/demos/DeletionEventsDemoTab.tsx +147 -0
  94. package/src/displaybox/demos/EngineEventsDemoTab.tsx +151 -0
  95. package/src/displaybox/demos/EventHandlersDemoTab.tsx +110 -0
  96. package/src/displaybox/demos/ExternalDragDropDemoTab.tsx +261 -0
  97. package/src/displaybox/demos/LinkCancelDemoTab.tsx +238 -0
  98. package/src/displaybox/demos/ObstacleRoutingDemoTab.tsx +30 -0
  99. package/src/displaybox/demos/ShapeHoverControlsDemoTab.tsx +558 -0
  100. package/src/displaybox/demos/SimpleDemo.tsx +73 -0
  101. package/src/displaybox/demos/SvgPathDemoTab.tsx +327 -0
  102. package/src/displaybox/demos/TextLayoutDemoTab.tsx +386 -0
  103. package/src/displaybox/demos/autoLayoutDemo.ts +111 -0
  104. package/src/displaybox/demos/basicDemo.ts +131 -0
  105. package/src/displaybox/demos/childConstraintsDemo.ts +65 -0
  106. package/src/displaybox/demos/customDemo.ts +59 -0
  107. package/src/displaybox/demos/deletionEventsDemo.ts +91 -0
  108. package/src/displaybox/demos/engineEventsDemo.ts +64 -0
  109. package/src/displaybox/demos/eventHandlersDemo.ts +41 -0
  110. package/src/displaybox/demos/externalDragDropDemo.ts +28 -0
  111. package/src/displaybox/demos/gridOverlayDemo.ts +50 -0
  112. package/src/displaybox/demos/index.tsx +217 -0
  113. package/src/displaybox/demos/linkBendHandlesDemo.ts +143 -0
  114. package/src/displaybox/demos/linkCancelDemo.ts +56 -0
  115. package/src/displaybox/demos/linkPortCreationDemo.ts +46 -0
  116. package/src/displaybox/demos/multiLevelTreeDemo.ts +120 -0
  117. package/src/displaybox/demos/multipleElementsDemo.ts +62 -0
  118. package/src/displaybox/demos/nestedDemo.ts +78 -0
  119. package/src/displaybox/demos/obstacleRoutingDemo.ts +176 -0
  120. package/src/displaybox/demos/portBorderDemo.ts +98 -0
  121. package/src/displaybox/demos/portConstraintsDemo.ts +175 -0
  122. package/src/displaybox/demos/rotatedCreationDemo.ts +185 -0
  123. package/src/displaybox/demos/roundedRectRadiusDemo.ts +93 -0
  124. package/src/displaybox/demos/routingDemo.ts +57 -0
  125. package/src/displaybox/demos/selectionDemo.ts +49 -0
  126. package/src/displaybox/demos/shapeBorderMovementDemo.ts +126 -0
  127. package/src/displaybox/demos/shapeGalleryDemo.ts +73 -0
  128. package/src/displaybox/demos/shapeHoverControlsDemo.ts +172 -0
  129. package/src/displaybox/demos/shared.ts +161 -0
  130. package/src/displaybox/demos/svgPathDemo.ts +71 -0
  131. package/src/displaybox/demos/textDemo.ts +62 -0
  132. package/src/displaybox/types.ts +66 -0
  133. package/src/examples/index.ts +21 -0
@@ -0,0 +1,501 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import type { ElementLayout, 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
+
13
+ const parentOptions = [
14
+ { id: 'layout-row', label: 'Horizontal layout' },
15
+ { id: 'layout-column', label: 'Vertical layout' },
16
+ { id: 'layout-nested', label: 'Nested layout' },
17
+ { id: 'layout-manual', label: 'Manual (compare)' },
18
+ ];
19
+
20
+ const AutoLayoutDemo = () => {
21
+ const demo = autoLayoutDemoConfig;
22
+ const { containerRef, editorRef, diagramState, selection, snapEnabled, setSnapEnabled } = useDemoEditor({
23
+ createState: demo.createState,
24
+ elementShapes: demo.elementShapes,
25
+ portShapes: demo.portShapes,
26
+ });
27
+
28
+ const nextOffset = useOffsetSequence();
29
+ const actionHelpers: DemoActionHelpers = useMemo(() => ({ nextOffset }), [nextOffset]);
30
+
31
+ const controls = useDemoControls({
32
+ demo,
33
+ editorRef,
34
+ diagramState,
35
+ selection,
36
+ snapEnabled,
37
+ setSnapEnabled,
38
+ actionHelpers,
39
+ });
40
+
41
+ const [targetId, setTargetId] = useState<string>(parentOptions[0].id);
42
+ const [mode, setMode] = useState<ElementLayoutMode>('horizontal');
43
+ const [align, setAlign] = useState<ElementLayout['align']>('center');
44
+ const [padding, setPadding] = useState<number>(12);
45
+ const [gap, setGap] = useState<number>(12);
46
+ const [childFitMainAxis, setChildFitMainAxis] = useState<ElementLayout['childFitMainAxis']>('none');
47
+ const [childFitCrossAxis, setChildFitCrossAxis] = useState<ElementLayout['childFitCrossAxis']>('none');
48
+ const [childFitMinWidth, setChildFitMinWidth] = useState<number | ''>('');
49
+ const [childFitMinHeight, setChildFitMinHeight] = useState<number | ''>('');
50
+ const [childFitMaxWidth, setChildFitMaxWidth] = useState<number | ''>('');
51
+ const [childFitMaxHeight, setChildFitMaxHeight] = useState<number | ''>('');
52
+ const [lastTrigger, setLastTrigger] = useState<string>('None yet');
53
+
54
+ useEffect(() => {
55
+ const editor = editorRef.current;
56
+ if (!editor) return undefined;
57
+ const disposers = [
58
+ editor.on('elementMoved', () => setLastTrigger('child moved')),
59
+ editor.on('elementResized', () => setLastTrigger('child resized')),
60
+ editor.on('elementDeleted', () => setLastTrigger('child removed')),
61
+ editor.on('change', () => setLastTrigger((prev) => prev)),
62
+ ];
63
+ return () => disposers.forEach((off) => off && off());
64
+ }, [editorRef]);
65
+
66
+ useEffect(() => {
67
+ if (!diagramState) return;
68
+ if (selection.length === 1) {
69
+ const selectedId = selection[0];
70
+ const exists = diagramState.elements.some((el) => el.id === selectedId);
71
+ if (exists && selectedId !== targetId) {
72
+ setTargetId(selectedId);
73
+ }
74
+ }
75
+ }, [selection, diagramState, targetId]);
76
+
77
+ useEffect(() => {
78
+ if (!diagramState) return;
79
+ const exists = diagramState.elements.some((el) => el.id === targetId);
80
+ if (!exists) {
81
+ setTargetId(parentOptions[0].id);
82
+ }
83
+ }, [diagramState, targetId]);
84
+
85
+ const targetElement = useMemo(
86
+ () => diagramState?.elements.find((el) => el.id === targetId),
87
+ [diagramState, targetId],
88
+ );
89
+
90
+ const targetOptions = useMemo(() => {
91
+ const seen = new Set<string>();
92
+ const options: Array<{ id: string; label: string }> = [];
93
+ parentOptions.forEach((option) => {
94
+ if (!seen.has(option.id)) {
95
+ options.push(option);
96
+ seen.add(option.id);
97
+ }
98
+ });
99
+ if (diagramState) {
100
+ diagramState.elements
101
+ .filter((el) => (el.layout && el.layout.mode !== 'manual') || el.parentId)
102
+ .forEach((el) => {
103
+ if (!seen.has(el.id)) {
104
+ const modeLabel = el.layout?.mode ?? 'manual';
105
+ options.push({ id: el.id, label: `${el.id} (${modeLabel})` });
106
+ seen.add(el.id);
107
+ }
108
+ });
109
+ }
110
+ if (targetElement && !seen.has(targetElement.id)) {
111
+ options.push({ id: targetElement.id, label: `${targetElement.id} (selected)` });
112
+ }
113
+ return options;
114
+ }, [diagramState, targetElement]);
115
+
116
+ useEffect(() => {
117
+ const parent = diagramState?.elements.find((el) => el.id === targetId);
118
+ if (!parent) return;
119
+ const layout = parent.layout;
120
+ setMode(layout?.mode ?? 'manual');
121
+ setAlign(layout?.align ?? 'center');
122
+ const paddingValue =
123
+ typeof layout?.padding === 'number'
124
+ ? layout.padding
125
+ : layout?.padding
126
+ ? Math.max(layout.padding.x ?? 0, layout.padding.y ?? 0)
127
+ : 12;
128
+ setPadding(paddingValue ?? 12);
129
+ setGap(layout?.gap ?? 12);
130
+ setChildFitMainAxis(layout?.childFitMainAxis ?? 'none');
131
+ setChildFitCrossAxis(layout?.childFitCrossAxis ?? 'none');
132
+ setChildFitMinWidth(layout?.childFitMinSize?.width ?? '');
133
+ setChildFitMinHeight(layout?.childFitMinSize?.height ?? '');
134
+ setChildFitMaxWidth(layout?.childFitMaxSize?.width ?? '');
135
+ setChildFitMaxHeight(layout?.childFitMaxSize?.height ?? '');
136
+ }, [diagramState, targetId]);
137
+
138
+ const childOrder = useMemo(() => {
139
+ if (!diagramState) return [];
140
+ const children = diagramState.elements.filter((el) => el.parentId === targetId);
141
+ const parent = diagramState.elements.find((el) => el.id === targetId);
142
+ const axis = parent?.layout?.mode === 'vertical' ? 'y' : 'x';
143
+ return [...children].sort((a, b) => {
144
+ const aPos = axis === 'y' ? a.position.y : a.position.x;
145
+ const bPos = axis === 'y' ? b.position.y : b.position.x;
146
+ if (aPos === bPos) return a.id.localeCompare(b.id);
147
+ return aPos - bPos;
148
+ });
149
+ }, [diagramState, targetId]);
150
+
151
+ const parentSummaries = useMemo(() => {
152
+ return parentOptions.map((option) => {
153
+ const parent = diagramState?.elements.find((el) => el.id === option.id);
154
+ const childCount = diagramState?.elements.filter((el) => el.parentId === option.id).length ?? 0;
155
+ return {
156
+ ...option,
157
+ layoutMode: parent?.layout?.mode ?? 'manual',
158
+ size: parent?.size ?? { width: 0, height: 0 },
159
+ childCount,
160
+ };
161
+ });
162
+ }, [diagramState]);
163
+
164
+ const handleApplyLayout = () => {
165
+ const editor = editorRef.current;
166
+ if (!editor) return;
167
+ if (!targetElement) return;
168
+ const childFitMinSize =
169
+ childFitMinWidth === '' && childFitMinHeight === ''
170
+ ? undefined
171
+ : {
172
+ ...(childFitMinWidth === '' ? {} : { width: childFitMinWidth }),
173
+ ...(childFitMinHeight === '' ? {} : { height: childFitMinHeight }),
174
+ };
175
+ const childFitMaxSize =
176
+ childFitMaxWidth === '' && childFitMaxHeight === ''
177
+ ? undefined
178
+ : {
179
+ ...(childFitMaxWidth === '' ? {} : { width: childFitMaxWidth }),
180
+ ...(childFitMaxHeight === '' ? {} : { height: childFitMaxHeight }),
181
+ };
182
+ const layout =
183
+ mode === 'manual'
184
+ ? { mode: 'manual' as const }
185
+ : {
186
+ mode,
187
+ padding,
188
+ gap,
189
+ align: align ?? 'center',
190
+ childFitMainAxis,
191
+ childFitCrossAxis,
192
+ childFitMinSize,
193
+ childFitMaxSize,
194
+ };
195
+ editor.setElementLayout(targetElement.id, layout);
196
+ setLastTrigger(`layout applied (${mode})`);
197
+ };
198
+
199
+ const handleAddChild = () => {
200
+ const editor = editorRef.current;
201
+ if (!editor) return;
202
+ if (!targetElement) return;
203
+ const offset = nextOffset();
204
+ const sizeBump = (offset % 3) * 8;
205
+ editor.addElement({
206
+ id: `auto-child-${createId()}`,
207
+ position: { x: 16 + offset, y: 16 + offset },
208
+ size: { width: 50 + sizeBump, height: 30 + (offset % 2) * 6 },
209
+ shapeId: demo.defaultElementShapeId ?? 'default',
210
+ parentId: targetElement.id,
211
+ });
212
+ setLastTrigger('child added');
213
+ };
214
+
215
+ const handleRemoveChild = () => {
216
+ const editor = editorRef.current;
217
+ if (!editor) return;
218
+ const children = childOrder;
219
+ const target = children[children.length - 1];
220
+ if (target) {
221
+ editor.removeElement(target.id);
222
+ setLastTrigger('child removed');
223
+ }
224
+ };
225
+
226
+ const alignmentLabel =
227
+ mode === 'horizontal' ? 'Vertical align (top/center/bottom)' : 'Horizontal align (left/center/right)';
228
+
229
+ return (
230
+ <section>
231
+ <div style={{ marginBottom: 12 }}>
232
+ <h2 style={{ marginTop: 0, marginBottom: 4 }}>{demo.title}</h2>
233
+ <p style={{ marginTop: 0 }}>
234
+ Try horizontal, vertical, nested, and manual layout containers. Select a layout parent directly on the canvas
235
+ or pick it from the list; the controls below always act on the current selection. Drag, resize, add, or remove
236
+ children to see automatic reflow. Use fit controls to distribute/stretch child sizes with optional min/max
237
+ guards. Nested column shows fit propagation boundaries to its parent row.
238
+ </p>
239
+ </div>
240
+ <DisplayBoxControls
241
+ actions={demo.actions}
242
+ snapEnabled={controls.snapEnabled}
243
+ selectedLinkRouting={controls.selectedLinkRouting}
244
+ canToggleLinkRouting={controls.canToggleLinkRouting}
245
+ onReload={controls.handleReload}
246
+ onZoomIn={controls.handleZoomIn}
247
+ onZoomOut={controls.handleZoomOut}
248
+ onResetViewport={controls.handleResetViewport}
249
+ onToggleSnap={controls.handleToggleSnap}
250
+ onManualRender={controls.handleManualRender}
251
+ onToggleLinkRouting={controls.handleToggleLinkRouting}
252
+ onAction={controls.handleAction}
253
+ />
254
+
255
+ <div style={{ display: 'grid', gap: 12, marginBottom: 12 }}>
256
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'center' }}>
257
+ <label htmlFor="parent-select" style={{ fontWeight: 600 }}>
258
+ Target element (follows selection)
259
+ </label>
260
+ <select
261
+ id="parent-select"
262
+ value={targetId}
263
+ onChange={(event) => {
264
+ const nextId = event.target.value;
265
+ setTargetId(nextId);
266
+ if (editorRef.current) {
267
+ editorRef.current.setSelection([nextId]);
268
+ }
269
+ }}
270
+ style={{ padding: '6px 10px', minWidth: 180 }}
271
+ >
272
+ {targetOptions.map((option) => (
273
+ <option key={option.id} value={option.id}>
274
+ {option.label}
275
+ </option>
276
+ ))}
277
+ </select>
278
+
279
+ <label htmlFor="mode-select" style={{ fontWeight: 600 }}>
280
+ Layout
281
+ </label>
282
+ <select
283
+ id="mode-select"
284
+ value={mode}
285
+ onChange={(event) => setMode(event.target.value as ElementLayoutMode)}
286
+ style={{ padding: '6px 10px', minWidth: 140 }}
287
+ >
288
+ <option value="manual">Manual</option>
289
+ <option value="horizontal">Horizontal</option>
290
+ <option value="vertical">Vertical</option>
291
+ </select>
292
+
293
+ <label htmlFor="align-select" style={{ fontWeight: 600 }}>
294
+ {alignmentLabel}
295
+ </label>
296
+ <select
297
+ id="align-select"
298
+ value={align ?? 'center'}
299
+ onChange={(event) => setAlign(event.target.value as ElementLayout['align'])}
300
+ style={{ padding: '6px 10px', minWidth: 140 }}
301
+ >
302
+ <option value="start">{mode === 'horizontal' ? 'Top' : 'Left'}</option>
303
+ <option value="center">Center</option>
304
+ <option value="end">{mode === 'horizontal' ? 'Bottom' : 'Right'}</option>
305
+ </select>
306
+
307
+ <label htmlFor="padding-input" style={{ fontWeight: 600 }}>
308
+ Padding
309
+ </label>
310
+ <input
311
+ id="padding-input"
312
+ type="number"
313
+ value={padding}
314
+ onChange={(event) => setPadding(Number(event.target.value) || 0)}
315
+ style={{ width: 72, padding: '6px 8px' }}
316
+ min={0}
317
+ />
318
+
319
+ <label htmlFor="gap-input" style={{ fontWeight: 600 }}>
320
+ Gap
321
+ </label>
322
+ <input
323
+ id="gap-input"
324
+ type="number"
325
+ value={gap}
326
+ onChange={(event) => setGap(Number(event.target.value) || 0)}
327
+ style={{ width: 72, padding: '6px 8px' }}
328
+ min={0}
329
+ />
330
+
331
+ <label htmlFor="fit-main-select" style={{ fontWeight: 600 }}>
332
+ Main-axis fit
333
+ </label>
334
+ <select
335
+ id="fit-main-select"
336
+ value={childFitMainAxis ?? 'none'}
337
+ onChange={(event) => setChildFitMainAxis(event.target.value as ElementLayout['childFitMainAxis'])}
338
+ style={{ padding: '6px 10px', minWidth: 130 }}
339
+ >
340
+ <option value="none">Off</option>
341
+ <option value="distribute">Distribute</option>
342
+ </select>
343
+
344
+ <label htmlFor="fit-cross-select" style={{ fontWeight: 600 }}>
345
+ Cross-axis fit
346
+ </label>
347
+ <select
348
+ id="fit-cross-select"
349
+ value={childFitCrossAxis ?? 'none'}
350
+ onChange={(event) => setChildFitCrossAxis(event.target.value as ElementLayout['childFitCrossAxis'])}
351
+ style={{ padding: '6px 10px', minWidth: 130 }}
352
+ >
353
+ <option value="none">Off</option>
354
+ <option value="stretch">Stretch</option>
355
+ </select>
356
+
357
+ <label htmlFor="fit-min-width-input" style={{ fontWeight: 600 }}>
358
+ Min W/H
359
+ </label>
360
+ <div style={{ display: 'inline-flex', gap: 6 }}>
361
+ <input
362
+ id="fit-min-width-input"
363
+ type="number"
364
+ value={childFitMinWidth}
365
+ onChange={(event) => setChildFitMinWidth(event.target.value === '' ? '' : Math.max(0, Number(event.target.value) || 0))}
366
+ style={{ width: 64, padding: '6px 8px' }}
367
+ min={0}
368
+ placeholder="w"
369
+ />
370
+ <input
371
+ type="number"
372
+ value={childFitMinHeight}
373
+ onChange={(event) => setChildFitMinHeight(event.target.value === '' ? '' : Math.max(0, Number(event.target.value) || 0))}
374
+ style={{ width: 64, padding: '6px 8px' }}
375
+ min={0}
376
+ placeholder="h"
377
+ />
378
+ </div>
379
+
380
+ <label htmlFor="fit-max-width-input" style={{ fontWeight: 600 }}>
381
+ Max W/H
382
+ </label>
383
+ <div style={{ display: 'inline-flex', gap: 6 }}>
384
+ <input
385
+ id="fit-max-width-input"
386
+ type="number"
387
+ value={childFitMaxWidth}
388
+ onChange={(event) => setChildFitMaxWidth(event.target.value === '' ? '' : Math.max(0, Number(event.target.value) || 0))}
389
+ style={{ width: 64, padding: '6px 8px' }}
390
+ min={0}
391
+ placeholder="w"
392
+ />
393
+ <input
394
+ type="number"
395
+ value={childFitMaxHeight}
396
+ onChange={(event) => setChildFitMaxHeight(event.target.value === '' ? '' : Math.max(0, Number(event.target.value) || 0))}
397
+ style={{ width: 64, padding: '6px 8px' }}
398
+ min={0}
399
+ placeholder="h"
400
+ />
401
+ </div>
402
+
403
+ <button type="button" onClick={handleApplyLayout} style={{ padding: '6px 12px', fontWeight: 600 }}>
404
+ Apply layout
405
+ </button>
406
+ <button type="button" onClick={handleAddChild} style={{ padding: '6px 10px' }} disabled={!targetElement}>
407
+ Add child
408
+ </button>
409
+ <button type="button" onClick={handleRemoveChild} style={{ padding: '6px 10px' }} disabled={!targetElement}>
410
+ Remove last child
411
+ </button>
412
+ <span
413
+ style={{
414
+ padding: '6px 10px',
415
+ borderRadius: 8,
416
+ background: '#f7f7f8',
417
+ border: '1px solid #e0e0e0',
418
+ display: 'inline-flex',
419
+ gap: 6,
420
+ alignItems: 'center',
421
+ }}
422
+ >
423
+ <span style={{ fontWeight: 600 }}>Target:</span> {targetElement ? targetElement.id : '—'}
424
+ <span style={{ width: 6, height: 6, borderRadius: '50%', background: '#1f4d99', display: 'inline-block' }} />
425
+ Last trigger: <strong>{lastTrigger}</strong>
426
+ </span>
427
+ </div>
428
+
429
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
430
+ <div>
431
+ <div style={{ fontWeight: 600, marginBottom: 6 }}>Child order (based on layout axis)</div>
432
+ <ol style={{ marginTop: 0, paddingLeft: 18 }}>
433
+ {childOrder.map((child) => (
434
+ <li key={child.id} style={{ marginBottom: 4, fontSize: 13 }}>
435
+ {child.id} — position ({Math.round(child.position.x)}, {Math.round(child.position.y)}) size (
436
+ {child.size.width}×{child.size.height})
437
+ </li>
438
+ ))}
439
+ {childOrder.length === 0 && <li style={{ color: '#555' }}>No children.</li>}
440
+ </ol>
441
+ <p style={{ fontSize: 12, color: '#555', marginTop: 4 }}>
442
+ Drag a child before/after its sibling or resize it. The order above reflects the reflow sequence.
443
+ </p>
444
+ </div>
445
+ <div>
446
+ <div style={{ fontWeight: 600, marginBottom: 6 }}>Scenario tips</div>
447
+ <ul style={{ marginTop: 0, paddingLeft: 18, fontSize: 13 }}>
448
+ <li>Horizontal: try center vs bottom alignment and larger padding.</li>
449
+ <li>Vertical: see parent grow taller as children stack.</li>
450
+ <li>Fit main-axis distribute fills inner layout space across siblings.</li>
451
+ <li>Fit cross-axis stretch extends children across inner cross axis.</li>
452
+ <li>Min/max fit guards cap distributed or stretched child sizes.</li>
453
+ <li>Nested: the left column owns its own children and expands the row.</li>
454
+ <li>Deep bounds: stack-overflow widens the nested column and the outer row.</li>
455
+ <li>Re-order: drag along the layout axis then apply layout to keep the new order.</li>
456
+ <li>Select any container on the canvas to target it instantly—controls follow your selection.</li>
457
+ <li>Manual: switch to manual to compare with auto layout.</li>
458
+ </ul>
459
+ </div>
460
+ </div>
461
+
462
+ <div>
463
+ <div style={{ fontWeight: 600, marginBottom: 6 }}>Parent bounds snapshot</div>
464
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 8 }}>
465
+ {parentSummaries.map((parent) => (
466
+ <div
467
+ key={parent.id}
468
+ style={{
469
+ padding: 10,
470
+ borderRadius: 8,
471
+ border: '1px solid #e0e0e0',
472
+ background: '#fafafa',
473
+ boxShadow: '0 1px 0 rgba(0,0,0,0.04)',
474
+ }}
475
+ >
476
+ <div style={{ fontWeight: 700 }}>{parent.label}</div>
477
+ <div style={{ fontSize: 12, color: '#444', marginTop: 2 }}>
478
+ Layout: <code>{parent.layoutMode}</code> · {parent.childCount} children
479
+ </div>
480
+ <div
481
+ style={{
482
+ marginTop: 4,
483
+ fontFamily:
484
+ 'ui-monospace, SFMono-Regular, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace',
485
+ fontSize: 12,
486
+ }}
487
+ >
488
+ {Math.round(parent.size.width)} × {Math.round(parent.size.height)} px
489
+ </div>
490
+ </div>
491
+ ))}
492
+ </div>
493
+ </div>
494
+ </div>
495
+
496
+ <DisplayBoxStage containerRef={containerRef} stageStyle={gridStageStyle} />
497
+ </section>
498
+ );
499
+ };
500
+
501
+ export default AutoLayoutDemo;
@@ -0,0 +1,147 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import type {
3
+ ElementDeletedEvent,
4
+ LinkDeletedEvent,
5
+ PortDeletedEvent,
6
+ TextDeletedEvent,
7
+ } from '../../api';
8
+ import DisplayBoxControls from '../DisplayBoxControls';
9
+ import DisplayBoxStage from '../DisplayBoxStage';
10
+ import useDemoControls from '../useDemoControls';
11
+ import useDemoEditor from '../useDemoEditor';
12
+ import useOffsetSequence from '../useOffsetSequence';
13
+ import type { DemoActionHelpers } from '../types';
14
+ import { deletionEventsDemoConfig } from './deletionEventsDemo';
15
+
16
+ type EventPayloads = {
17
+ elementDeleted: ElementDeletedEvent | null;
18
+ portDeleted: PortDeletedEvent | null;
19
+ linkDeleted: LinkDeletedEvent | null;
20
+ textDeleted: TextDeletedEvent | null;
21
+ };
22
+
23
+ const initialPayloads: EventPayloads = {
24
+ elementDeleted: null,
25
+ portDeleted: null,
26
+ linkDeleted: null,
27
+ textDeleted: null,
28
+ };
29
+
30
+ const eventBoxStyle: React.CSSProperties = {
31
+ width: '100%',
32
+ minHeight: 130,
33
+ padding: '8px 10px',
34
+ borderRadius: 8,
35
+ border: '1px solid #d0d5dd',
36
+ background: '#fff',
37
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Courier New", monospace',
38
+ fontSize: 12,
39
+ resize: 'vertical',
40
+ };
41
+
42
+ const DeletionEventsDemo = () => {
43
+ const demo = deletionEventsDemoConfig;
44
+ const { containerRef, editorRef, diagramState, selection, snapEnabled, setSnapEnabled } = useDemoEditor({
45
+ createState: demo.createState,
46
+ elementShapes: demo.elementShapes,
47
+ portShapes: demo.portShapes,
48
+ });
49
+
50
+ const nextOffset = useOffsetSequence();
51
+ const actionHelpers: DemoActionHelpers = useMemo(() => ({ nextOffset }), [nextOffset]);
52
+
53
+ const controls = useDemoControls({
54
+ demo,
55
+ editorRef,
56
+ diagramState,
57
+ selection,
58
+ snapEnabled,
59
+ setSnapEnabled,
60
+ actionHelpers,
61
+ });
62
+
63
+ const [payloads, setPayloads] = useState<EventPayloads>(initialPayloads);
64
+
65
+ useEffect(() => {
66
+ const editor = editorRef.current;
67
+ if (!editor) return undefined;
68
+ setPayloads(initialPayloads);
69
+ const unsubs = [
70
+ editor.on('elementDeleted', (payload) => setPayloads((prev) => ({ ...prev, elementDeleted: payload }))),
71
+ editor.on('portDeleted', (payload) => setPayloads((prev) => ({ ...prev, portDeleted: payload }))),
72
+ editor.on('linkDeleted', (payload) => setPayloads((prev) => ({ ...prev, linkDeleted: payload }))),
73
+ editor.on('textDeleted', (payload) => setPayloads((prev) => ({ ...prev, textDeleted: payload }))),
74
+ ];
75
+ return () => {
76
+ unsubs.forEach((unsub) => unsub());
77
+ };
78
+ }, [editorRef]);
79
+
80
+ const hasPort = (diagramState?.ports ?? []).some((port) => port.id === 'delete-source-free-port');
81
+ const hasLink = (diagramState?.links ?? []).some((link) => link.id === 'delete-link');
82
+ const hasText = (diagramState?.texts ?? []).some((text) => text.id === 'delete-free-text');
83
+ const hasCascadeElement = (diagramState?.elements ?? []).some((element) => element.id === 'delete-source');
84
+
85
+ return (
86
+ <section>
87
+ <div style={{ marginBottom: 12 }}>
88
+ <h2 style={{ marginTop: 0, marginBottom: 4 }}>{demo.title}</h2>
89
+ <p style={{ marginTop: 0 }}>
90
+ Use direct delete buttons for port/link/text, then delete <code>delete-source</code> to trigger cascade
91
+ removals. Latest payloads are shown below for integration debugging.
92
+ </p>
93
+ </div>
94
+ <DisplayBoxControls
95
+ actions={demo.actions}
96
+ snapEnabled={controls.snapEnabled}
97
+ selectedLinkRouting={controls.selectedLinkRouting}
98
+ canToggleLinkRouting={controls.canToggleLinkRouting}
99
+ onReload={controls.handleReload}
100
+ onZoomIn={controls.handleZoomIn}
101
+ onZoomOut={controls.handleZoomOut}
102
+ onResetViewport={controls.handleResetViewport}
103
+ onToggleSnap={controls.handleToggleSnap}
104
+ onManualRender={controls.handleManualRender}
105
+ onToggleLinkRouting={controls.handleToggleLinkRouting}
106
+ onAction={controls.handleAction}
107
+ />
108
+ <div style={{ display: 'grid', gap: 12, marginBottom: 12 }}>
109
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
110
+ <button type="button" onClick={() => editorRef.current?.removePort('delete-source-free-port')} disabled={!hasPort}>
111
+ Delete Port
112
+ </button>
113
+ <button type="button" onClick={() => editorRef.current?.removeLink('delete-link')} disabled={!hasLink}>
114
+ Delete Link
115
+ </button>
116
+ <button type="button" onClick={() => editorRef.current?.removeText('delete-free-text')} disabled={!hasText}>
117
+ Delete Text
118
+ </button>
119
+ <button type="button" onClick={() => editorRef.current?.removeElement('delete-source')} disabled={!hasCascadeElement}>
120
+ Delete Element (Cascade)
121
+ </button>
122
+ </div>
123
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12 }}>
124
+ <div>
125
+ <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>elementDeleted</label>
126
+ <textarea readOnly value={payloads.elementDeleted ? JSON.stringify(payloads.elementDeleted, null, 2) : ''} style={eventBoxStyle} />
127
+ </div>
128
+ <div>
129
+ <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>portDeleted</label>
130
+ <textarea readOnly value={payloads.portDeleted ? JSON.stringify(payloads.portDeleted, null, 2) : ''} style={eventBoxStyle} />
131
+ </div>
132
+ <div>
133
+ <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>linkDeleted</label>
134
+ <textarea readOnly value={payloads.linkDeleted ? JSON.stringify(payloads.linkDeleted, null, 2) : ''} style={eventBoxStyle} />
135
+ </div>
136
+ <div>
137
+ <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>textDeleted</label>
138
+ <textarea readOnly value={payloads.textDeleted ? JSON.stringify(payloads.textDeleted, null, 2) : ''} style={eventBoxStyle} />
139
+ </div>
140
+ </div>
141
+ </div>
142
+ <DisplayBoxStage containerRef={containerRef} />
143
+ </section>
144
+ );
145
+ };
146
+
147
+ export default DeletionEventsDemo;