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,386 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import type { TextLayout } from '../../api';
3
+ import DisplayBoxControls from '../DisplayBoxControls';
4
+ import DisplayBoxStage from '../DisplayBoxStage';
5
+ import useDemoControls from '../useDemoControls';
6
+ import useDemoEditor from '../useDemoEditor';
7
+ import useOffsetSequence from '../useOffsetSequence';
8
+ import type { DemoActionHelpers } from '../types';
9
+ import {
10
+ textDemoConfig,
11
+ textDemoFloatingTextId,
12
+ textDemoLongSample,
13
+ textDemoOwnedTextId,
14
+ textDemoOwnerId,
15
+ } from './textDemo';
16
+
17
+ type TextTarget = 'owned' | 'floating';
18
+
19
+ const TextLayoutDemo = () => {
20
+ const demo = textDemoConfig;
21
+ const { containerRef, editorRef, diagramState, selection, snapEnabled, setSnapEnabled } = useDemoEditor({
22
+ createState: demo.createState,
23
+ elementShapes: demo.elementShapes,
24
+ portShapes: demo.portShapes,
25
+ });
26
+
27
+ const nextOffset = useOffsetSequence();
28
+ const actionHelpers: DemoActionHelpers = useMemo(() => ({ nextOffset }), [nextOffset]);
29
+ const controls = useDemoControls({
30
+ demo,
31
+ editorRef,
32
+ diagramState,
33
+ selection,
34
+ snapEnabled,
35
+ setSnapEnabled,
36
+ actionHelpers,
37
+ });
38
+
39
+ const [target, setTarget] = useState<TextTarget>('owned');
40
+ const [wrap, setWrap] = useState<TextLayout['wrap']>('none');
41
+ const [overflow, setOverflow] = useState<TextLayout['overflow']>('ellipsis-middle');
42
+ const [padding, setPadding] = useState<number>(12);
43
+ const [boundsMode, setBoundsMode] = useState<TextLayout['boundsMode']>('owner-width');
44
+ const [maxLines, setMaxLines] = useState<number>(3);
45
+ const [fixedWidth, setFixedWidth] = useState<number>(240);
46
+ const [fixedHeight, setFixedHeight] = useState<number>(90);
47
+ const [ownerWidth, setOwnerWidth] = useState<number>(260);
48
+ const [ownerHeight, setOwnerHeight] = useState<number>(140);
49
+
50
+ useEffect(() => {
51
+ const owner = diagramState?.elements.find((element) => element.id === textDemoOwnerId);
52
+ if (!owner) return;
53
+ setOwnerWidth(owner.size.width);
54
+ setOwnerHeight(owner.size.height);
55
+ }, [diagramState]);
56
+
57
+ const selectedTextId = target === 'owned' ? textDemoOwnedTextId : textDemoFloatingTextId;
58
+ const selectedText = diagramState?.texts.find((text) => text.id === selectedTextId);
59
+ const selectedDisplayContent = selectedText?.displayContent ?? selectedText?.content ?? '';
60
+ const ownerElement = diagramState?.elements.find((element) => element.id === textDemoOwnerId);
61
+
62
+ const applyLayoutToTarget = (nextTarget: TextTarget, nextLayout: TextLayout) => {
63
+ const editor = editorRef.current;
64
+ if (!editor) return;
65
+ if (nextTarget === 'owned') {
66
+ editor.removeText(textDemoOwnedTextId);
67
+ editor.addText({
68
+ id: textDemoOwnedTextId,
69
+ content: textDemoLongSample,
70
+ ownerId: textDemoOwnerId,
71
+ position: { x: 12, y: 12 },
72
+ layout: nextLayout,
73
+ });
74
+ editor.setSelection([textDemoOwnedTextId]);
75
+ return;
76
+ }
77
+
78
+ editor.removeText(textDemoFloatingTextId);
79
+ editor.addText({
80
+ id: textDemoFloatingTextId,
81
+ content: textDemoLongSample,
82
+ position: { x: 470, y: 120 },
83
+ layout: nextLayout,
84
+ });
85
+ editor.setSelection([textDemoFloatingTextId]);
86
+ };
87
+
88
+ const applyTextLayout = () => {
89
+ const layout: TextLayout = {
90
+ boundsMode,
91
+ wrap,
92
+ overflow,
93
+ padding,
94
+ maxLines,
95
+ ...(boundsMode === 'fixed' ? { fixedSize: { width: fixedWidth, height: fixedHeight } } : {}),
96
+ };
97
+ applyLayoutToTarget(target, layout);
98
+ };
99
+
100
+ const applyOwnerPaddingVerification = (nextPadding: number) => {
101
+ setTarget('owned');
102
+ setBoundsMode('owner-box');
103
+ setWrap('word');
104
+ setOverflow('ellipsis-end');
105
+ setPadding(nextPadding);
106
+ setMaxLines(4);
107
+ applyLayoutToTarget('owned', {
108
+ boundsMode: 'owner-box',
109
+ wrap: 'word',
110
+ overflow: 'ellipsis-end',
111
+ padding: nextPadding,
112
+ maxLines: 4,
113
+ });
114
+ };
115
+
116
+ const applyZeroPaddingOverflowPreset = () => {
117
+ const editor = editorRef.current;
118
+ if (!editor) return;
119
+ const presetWidth = 180;
120
+ const presetHeight = 58;
121
+ setTarget('owned');
122
+ setBoundsMode('owner-box');
123
+ setWrap('char');
124
+ setOverflow('clip');
125
+ setPadding(0);
126
+ setMaxLines(2);
127
+ setOwnerWidth(presetWidth);
128
+ setOwnerHeight(presetHeight);
129
+ editor.resizeElement(textDemoOwnerId, presetWidth, presetHeight);
130
+ applyLayoutToTarget('owned', {
131
+ boundsMode: 'owner-box',
132
+ wrap: 'char',
133
+ overflow: 'clip',
134
+ padding: 0,
135
+ maxLines: 2,
136
+ });
137
+ };
138
+
139
+ const applyOwnerSize = () => {
140
+ const editor = editorRef.current;
141
+ if (!editor) return;
142
+ editor.resizeElement(textDemoOwnerId, ownerWidth, ownerHeight);
143
+ editor.setSelection([textDemoOwnerId]);
144
+ };
145
+
146
+ const ownerPadding = target === 'owned' ? Math.max(0, padding) : 0;
147
+ const resolvedOwnerContentWidth = ownerElement ? Math.max(0, ownerElement.size.width - ownerPadding * 2) : null;
148
+ const resolvedOwnerContentHeight = ownerElement ? Math.max(0, ownerElement.size.height - ownerPadding * 2) : null;
149
+ const selectedDisplayOffset = selectedText?.displayOffset ?? { x: 0, y: 0 };
150
+ const selectedClip = selectedText?.displayClipSize;
151
+ const selectedTextSize = selectedText?.size;
152
+ const selectedOwner = selectedText?.ownerId
153
+ ? diagramState?.elements.find((element) => element.id === selectedText.ownerId)
154
+ : null;
155
+ const selectedWorldX = (selectedOwner?.position.x ?? 0) + (selectedText?.position.x ?? 0) + selectedDisplayOffset.x;
156
+ const selectedWorldY = (selectedOwner?.position.y ?? 0) + (selectedText?.position.y ?? 0) + selectedDisplayOffset.y;
157
+
158
+ return (
159
+ <section>
160
+ <div style={{ marginBottom: 12 }}>
161
+ <h2 style={{ marginTop: 0, marginBottom: 4 }}>{demo.title}</h2>
162
+ <p style={{ marginTop: 0 }}>
163
+ Full text content is preserved in state. Overflow and ellipsis settings only change visual output
164
+ (`displayContent`) for rendering.
165
+ </p>
166
+ </div>
167
+ <DisplayBoxControls
168
+ actions={demo.actions}
169
+ snapEnabled={controls.snapEnabled}
170
+ selectedLinkRouting={controls.selectedLinkRouting}
171
+ canToggleLinkRouting={controls.canToggleLinkRouting}
172
+ onReload={controls.handleReload}
173
+ onZoomIn={controls.handleZoomIn}
174
+ onZoomOut={controls.handleZoomOut}
175
+ onResetViewport={controls.handleResetViewport}
176
+ onToggleSnap={controls.handleToggleSnap}
177
+ onManualRender={controls.handleManualRender}
178
+ onToggleLinkRouting={controls.handleToggleLinkRouting}
179
+ onAction={controls.handleAction}
180
+ />
181
+
182
+ <div style={{ display: 'grid', gap: 10, marginBottom: 12 }}>
183
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'center' }}>
184
+ <label htmlFor="text-target" style={{ fontWeight: 600 }}>
185
+ Target
186
+ </label>
187
+ <select
188
+ id="text-target"
189
+ value={target}
190
+ onChange={(event) => {
191
+ const next = event.target.value as TextTarget;
192
+ setTarget(next);
193
+ if (next === 'owned') {
194
+ setBoundsMode('owner-width');
195
+ setWrap('none');
196
+ setOverflow('ellipsis-middle');
197
+ setPadding(12);
198
+ setMaxLines(2);
199
+ } else {
200
+ setBoundsMode('fixed');
201
+ setWrap('word');
202
+ setOverflow('ellipsis-end');
203
+ setPadding(8);
204
+ setMaxLines(3);
205
+ }
206
+ }}
207
+ style={{ padding: '6px 8px' }}
208
+ >
209
+ <option value="owned">Owned text (owner constrained)</option>
210
+ <option value="floating">Floating text (fixed bounds)</option>
211
+ </select>
212
+
213
+ <label htmlFor="text-bounds" style={{ fontWeight: 600 }}>
214
+ Bounds
215
+ </label>
216
+ <select
217
+ id="text-bounds"
218
+ value={boundsMode}
219
+ onChange={(event) => setBoundsMode(event.target.value as TextLayout['boundsMode'])}
220
+ style={{ padding: '6px 8px' }}
221
+ >
222
+ <option value="owner-width">owner-width</option>
223
+ <option value="owner-box">owner-box</option>
224
+ <option value="fixed">fixed</option>
225
+ </select>
226
+
227
+ <label htmlFor="text-wrap" style={{ fontWeight: 600 }}>
228
+ Wrap
229
+ </label>
230
+ <select
231
+ id="text-wrap"
232
+ value={wrap}
233
+ onChange={(event) => setWrap(event.target.value as TextLayout['wrap'])}
234
+ style={{ padding: '6px 8px' }}
235
+ >
236
+ <option value="none">none</option>
237
+ <option value="word">word</option>
238
+ <option value="char">char</option>
239
+ </select>
240
+
241
+ <label htmlFor="text-overflow" style={{ fontWeight: 600 }}>
242
+ Overflow
243
+ </label>
244
+ <select
245
+ id="text-overflow"
246
+ value={overflow}
247
+ onChange={(event) => setOverflow(event.target.value as TextLayout['overflow'])}
248
+ style={{ padding: '6px 8px' }}
249
+ >
250
+ <option value="clip">clip</option>
251
+ <option value="ellipsis-end">ellipsis-end</option>
252
+ <option value="ellipsis-middle">ellipsis-middle</option>
253
+ <option value="ellipsis-start">ellipsis-start</option>
254
+ </select>
255
+
256
+ <label htmlFor="text-padding" style={{ fontWeight: 600 }}>
257
+ Padding
258
+ </label>
259
+ <input
260
+ id="text-padding"
261
+ type="number"
262
+ value={padding}
263
+ min={0}
264
+ onChange={(event) => setPadding(Math.max(0, Number(event.target.value) || 0))}
265
+ style={{ width: 72, padding: '6px 8px' }}
266
+ />
267
+
268
+ <label htmlFor="text-max-lines" style={{ fontWeight: 600 }}>
269
+ Max lines
270
+ </label>
271
+ <input
272
+ id="text-max-lines"
273
+ type="number"
274
+ value={maxLines}
275
+ min={1}
276
+ onChange={(event) => setMaxLines(Math.max(1, Number(event.target.value) || 1))}
277
+ style={{ width: 72, padding: '6px 8px' }}
278
+ />
279
+
280
+ <button type="button" onClick={applyTextLayout} style={{ padding: '6px 12px', fontWeight: 600 }}>
281
+ Apply text layout
282
+ </button>
283
+ </div>
284
+
285
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'center' }}>
286
+ <strong>Owner resize (reflow):</strong>
287
+ <label htmlFor="owner-width-input">Width</label>
288
+ <input
289
+ id="owner-width-input"
290
+ type="number"
291
+ value={ownerWidth}
292
+ min={60}
293
+ onChange={(event) => setOwnerWidth(Math.max(60, Number(event.target.value) || 60))}
294
+ style={{ width: 80, padding: '6px 8px' }}
295
+ />
296
+ <label htmlFor="owner-height-input">Height</label>
297
+ <input
298
+ id="owner-height-input"
299
+ type="number"
300
+ value={ownerHeight}
301
+ min={40}
302
+ onChange={(event) => setOwnerHeight(Math.max(40, Number(event.target.value) || 40))}
303
+ style={{ width: 80, padding: '6px 8px' }}
304
+ />
305
+ <button type="button" onClick={applyOwnerSize} style={{ padding: '6px 12px' }}>
306
+ Apply owner size
307
+ </button>
308
+ {boundsMode === 'fixed' && (
309
+ <>
310
+ <label htmlFor="fixed-width-input">Fixed W</label>
311
+ <input
312
+ id="fixed-width-input"
313
+ type="number"
314
+ value={fixedWidth}
315
+ min={40}
316
+ onChange={(event) => setFixedWidth(Math.max(40, Number(event.target.value) || 40))}
317
+ style={{ width: 80, padding: '6px 8px' }}
318
+ />
319
+ <label htmlFor="fixed-height-input">Fixed H</label>
320
+ <input
321
+ id="fixed-height-input"
322
+ type="number"
323
+ value={fixedHeight}
324
+ min={24}
325
+ onChange={(event) => setFixedHeight(Math.max(24, Number(event.target.value) || 24))}
326
+ style={{ width: 80, padding: '6px 8px' }}
327
+ />
328
+ </>
329
+ )}
330
+ </div>
331
+
332
+ <div style={{ border: '1px solid #d3dbe8', borderRadius: 8, padding: 10, display: 'grid', gap: 8 }}>
333
+ <strong>Padding verification (owner constrained)</strong>
334
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
335
+ <button type="button" onClick={() => applyOwnerPaddingVerification(0)} style={{ padding: '6px 10px' }}>
336
+ Verify padding: 0
337
+ </button>
338
+ <button type="button" onClick={() => applyOwnerPaddingVerification(18)} style={{ padding: '6px 10px' }}>
339
+ Verify padding: 18
340
+ </button>
341
+ <button type="button" onClick={applyZeroPaddingOverflowPreset} style={{ padding: '6px 10px' }}>
342
+ Zero-padding no-bleed preset
343
+ </button>
344
+ </div>
345
+ <div style={{ fontSize: 12 }}>
346
+ Owner bounds: {ownerElement ? `${ownerElement.size.width} x ${ownerElement.size.height}` : 'n/a'} | Padding:{' '}
347
+ {ownerPadding}
348
+ </div>
349
+ <div style={{ fontSize: 12 }}>
350
+ Resolved content box: {resolvedOwnerContentWidth ?? 'n/a'} x {resolvedOwnerContentHeight ?? 'n/a'}
351
+ </div>
352
+ <div style={{ fontSize: 12 }}>
353
+ Quick check: padding 0 should show no edge bleed; increasing padding should shift inset and truncation together.
354
+ </div>
355
+ </div>
356
+
357
+ <div style={{ border: '1px solid #d3dbe8', borderRadius: 8, padding: 10, display: 'grid', gap: 6, fontSize: 12 }}>
358
+ <strong>Resolved render bounds (selected text)</strong>
359
+ <div>
360
+ size: {selectedTextSize ? `${selectedTextSize.width} x ${selectedTextSize.height}` : 'n/a'} | offset:{' '}
361
+ {`${selectedDisplayOffset.x}, ${selectedDisplayOffset.y}`}
362
+ </div>
363
+ <div>
364
+ clip: {selectedClip ? `${selectedClip.width} x ${selectedClip.height}` : 'none'} | world origin:{' '}
365
+ {Number.isFinite(selectedWorldX) && Number.isFinite(selectedWorldY)
366
+ ? `${selectedWorldX.toFixed(1)}, ${selectedWorldY.toFixed(1)}`
367
+ : 'n/a'}
368
+ </div>
369
+ </div>
370
+
371
+ <div style={{ display: 'grid', gap: 6 }}>
372
+ <div style={{ fontSize: 12 }}>
373
+ <strong>Canonical content:</strong> {textDemoLongSample}
374
+ </div>
375
+ <div style={{ fontSize: 12 }}>
376
+ <strong>Rendered displayContent:</strong> <code>{selectedDisplayContent}</code>
377
+ </div>
378
+ </div>
379
+ </div>
380
+
381
+ <DisplayBoxStage containerRef={containerRef} />
382
+ </section>
383
+ );
384
+ };
385
+
386
+ export default TextLayoutDemo;
@@ -0,0 +1,111 @@
1
+ import type { DiagramState } from '../../api';
2
+ import type { DemoConfig } from '../types';
3
+ import { baseElementShapes, basePortShapes } from './shared';
4
+
5
+ const createAutoLayoutState = (): DiagramState => ({
6
+ elements: [
7
+ {
8
+ id: 'layout-row',
9
+ position: { x: 60, y: 80 },
10
+ size: { width: 180, height: 96 },
11
+ shapeId: 'panel',
12
+ layout: { mode: 'horizontal', padding: { x: 14, y: 12 }, gap: 14, align: 'center' },
13
+ },
14
+ { id: 'row-a', position: { x: 0, y: 0 }, size: { width: 62, height: 44 }, shapeId: 'default', parentId: 'layout-row' },
15
+ { id: 'row-b', position: { x: 0, y: 0 }, size: { width: 96, height: 32 }, shapeId: 'default', parentId: 'layout-row' },
16
+ { id: 'row-c', position: { x: 0, y: 0 }, size: { width: 54, height: 56 }, shapeId: 'default', parentId: 'layout-row' },
17
+ {
18
+ id: 'row-nested-owner',
19
+ position: { x: 0, y: 0 },
20
+ size: { width: 82, height: 82 },
21
+ shapeId: 'default',
22
+ parentId: 'layout-row',
23
+ layout: { mode: 'vertical', padding: { x: 8, y: 6 }, gap: 6, align: 'start' },
24
+ },
25
+ { id: 'row-nested-child-a', position: { x: 0, y: 0 }, size: { width: 60, height: 20 }, shapeId: 'default', parentId: 'row-nested-owner' },
26
+ { id: 'row-nested-child-b', position: { x: 0, y: 0 }, size: { width: 76, height: 22 }, shapeId: 'default', parentId: 'row-nested-owner' },
27
+
28
+ {
29
+ id: 'layout-column',
30
+ position: { x: 400, y: 70 },
31
+ size: { width: 120, height: 140 },
32
+ shapeId: 'panel',
33
+ layout: { mode: 'vertical', padding: { x: 12, y: 10 }, gap: 16, align: 'end' },
34
+ },
35
+ { id: 'col-a', position: { x: 0, y: 0 }, size: { width: 44, height: 28 }, shapeId: 'default', parentId: 'layout-column' },
36
+ { id: 'col-b', position: { x: 0, y: 0 }, size: { width: 70, height: 36 }, shapeId: 'default', parentId: 'layout-column' },
37
+ { id: 'col-c', position: { x: 0, y: 0 }, size: { width: 92, height: 44 }, shapeId: 'default', parentId: 'layout-column' },
38
+
39
+ {
40
+ id: 'layout-nested',
41
+ position: { x: 120, y: 260 },
42
+ size: { width: 190, height: 130 },
43
+ shapeId: 'panel',
44
+ layout: { mode: 'horizontal', padding: { x: 12, y: 12 }, gap: 12, align: 'start' },
45
+ },
46
+ {
47
+ id: 'nested-stack',
48
+ position: { x: 0, y: 0 },
49
+ size: { width: 90, height: 92 },
50
+ shapeId: 'default',
51
+ parentId: 'layout-nested',
52
+ layout: { mode: 'vertical', padding: { x: 8, y: 8 }, gap: 8, align: 'center' },
53
+ },
54
+ { id: 'stack-a', position: { x: 0, y: 0 }, size: { width: 60, height: 24 }, shapeId: 'default', parentId: 'nested-stack' },
55
+ { id: 'stack-b', position: { x: 0, y: 0 }, size: { width: 90, height: 30 }, shapeId: 'default', parentId: 'nested-stack' },
56
+ {
57
+ id: 'stack-overflow',
58
+ position: { x: 0, y: 0 },
59
+ size: { width: 110, height: 26 },
60
+ shapeId: 'default',
61
+ parentId: 'nested-stack',
62
+ },
63
+ { id: 'nested-peer', position: { x: 0, y: 0 }, size: { width: 50, height: 90 }, shapeId: 'default', parentId: 'layout-nested' },
64
+
65
+ {
66
+ id: 'layout-manual',
67
+ position: { x: 420, y: 260 },
68
+ size: { width: 190, height: 120 },
69
+ shapeId: 'panel',
70
+ },
71
+ { id: 'manual-a', position: { x: 12, y: 18 }, size: { width: 60, height: 30 }, shapeId: 'default', parentId: 'layout-manual' },
72
+ { id: 'manual-b', position: { x: 90, y: 60 }, size: { width: 68, height: 32 }, shapeId: 'default', parentId: 'layout-manual' },
73
+ ],
74
+ ports: [],
75
+ links: [],
76
+ texts: [
77
+ { id: 'label-layout-row', content: 'Row parent', position: { x: 8, y: -18 }, ownerId: 'layout-row' },
78
+ { id: 'label-row-a', content: 'row-a', position: { x: 6, y: -14 }, ownerId: 'row-a' },
79
+ { id: 'label-row-b', content: 'row-b', position: { x: 6, y: -14 }, ownerId: 'row-b' },
80
+ { id: 'label-row-c', content: 'row-c', position: { x: 6, y: -14 }, ownerId: 'row-c' },
81
+ { id: 'label-row-nested-owner', content: 'owns children', position: { x: 6, y: -16 }, ownerId: 'row-nested-owner' },
82
+
83
+ { id: 'label-layout-column', content: 'Column parent', position: { x: 8, y: -18 }, ownerId: 'layout-column' },
84
+ { id: 'label-col-a', content: 'col-a', position: { x: 6, y: -14 }, ownerId: 'col-a' },
85
+ { id: 'label-col-b', content: 'col-b', position: { x: 6, y: -14 }, ownerId: 'col-b' },
86
+ { id: 'label-col-c', content: 'col-c', position: { x: 6, y: -14 }, ownerId: 'col-c' },
87
+
88
+ { id: 'label-layout-nested', content: 'Row + nested column', position: { x: 6, y: -18 }, ownerId: 'layout-nested' },
89
+ { id: 'label-nested-stack', content: 'nested stack', position: { x: 6, y: -16 }, ownerId: 'nested-stack' },
90
+ { id: 'label-stack-a', content: 'stack-a', position: { x: 6, y: -14 }, ownerId: 'stack-a' },
91
+ { id: 'label-stack-b', content: 'stack-b', position: { x: 6, y: -14 }, ownerId: 'stack-b' },
92
+ { id: 'label-stack-overflow', content: 'stack-overflow', position: { x: 6, y: -14 }, ownerId: 'stack-overflow' },
93
+ { id: 'label-nested-peer', content: 'nested-peer', position: { x: 6, y: -14 }, ownerId: 'nested-peer' },
94
+
95
+ { id: 'label-layout-manual', content: 'Manual (no layout)', position: { x: 6, y: -18 }, ownerId: 'layout-manual' },
96
+ { id: 'label-manual-a', content: 'manual-a', position: { x: 6, y: -14 }, ownerId: 'manual-a' },
97
+ { id: 'label-manual-b', content: 'manual-b', position: { x: 6, y: -14 }, ownerId: 'manual-b' },
98
+ ],
99
+ });
100
+
101
+ export const autoLayoutDemoConfig: DemoConfig = ({
102
+ id: 'auto-layout',
103
+ title: 'Element Auto-Layout',
104
+ description: 'Arrange children with padding/alignment plus fit options (main-axis distribute, cross-axis stretch).',
105
+ createState: createAutoLayoutState,
106
+ elementShapes: baseElementShapes,
107
+ portShapes: basePortShapes,
108
+ defaultElementShapeId: 'default',
109
+ defaultPortShapeId: 'port-circle',
110
+ actions: [],
111
+ });
@@ -0,0 +1,131 @@
1
+ import type { DiagramState } from '../../api';
2
+ import { createId } from '../../utils/ids';
3
+ import type { DemoConfig } from '../types';
4
+ import { baseElementShapes, basePortShapes } from './shared';
5
+
6
+ const createBasicState = (): DiagramState => ({
7
+ elements: [
8
+ {
9
+ id: 'element-a',
10
+ position: { x: 120, y: 120 },
11
+ size: { width: 220, height: 140 },
12
+ shapeId: 'default',
13
+ },
14
+ {
15
+ id: 'element-b',
16
+ position: { x: 430, y: 260 },
17
+ size: { width: 240, height: 150 },
18
+ shapeId: 'default',
19
+ },
20
+ ],
21
+ ports: [
22
+ {
23
+ id: 'port-a',
24
+ elementId: 'element-a',
25
+ position: { x: 40, y: 40 },
26
+ shapeId: 'port-circle',
27
+ anchorCenter: true,
28
+ },
29
+ {
30
+ id: 'port-b',
31
+ elementId: 'element-b',
32
+ position: { x: 60, y: 60 },
33
+ shapeId: 'port-circle',
34
+ anchorCenter: true,
35
+ },
36
+ ],
37
+ links: [
38
+ {
39
+ id: 'link-ab',
40
+ sourcePortId: 'port-a',
41
+ targetPortId: 'port-b',
42
+ points: [],
43
+ },
44
+ ],
45
+ texts: [
46
+ {
47
+ id: 'text-a',
48
+ content: 'Element A',
49
+ position: { x: 10, y: -10 },
50
+ ownerId: 'element-a',
51
+ style: { fontSize: 14, fontFamily: 'sans-serif' },
52
+ },
53
+ {
54
+ id: 'text-b',
55
+ content: 'Element B',
56
+ position: { x: 10, y: -10 },
57
+ ownerId: 'element-b',
58
+ style: { fontSize: 14, fontFamily: 'sans-serif' },
59
+ },
60
+ ],
61
+ });
62
+
63
+ export const basicDemoConfig: DemoConfig = ({
64
+ id: 'basic',
65
+ title: 'Basic Diagram',
66
+ description: 'Elements, ports, link, and labels.',
67
+ createState: createBasicState,
68
+ elementShapes: baseElementShapes,
69
+ portShapes: basePortShapes,
70
+ defaultElementShapeId: 'default',
71
+ defaultPortShapeId: 'port-circle',
72
+ actions: [
73
+ {
74
+ id: 'add-element',
75
+ label: 'Add element',
76
+ run: (editor, _state, demo, helpers) => {
77
+ const offset = helpers.nextOffset();
78
+ const id = `element-${createId()}`;
79
+ editor.addElement({
80
+ id,
81
+ position: { x: 140 + offset, y: 140 + offset },
82
+ size: { width: 200, height: 120 },
83
+ shapeId: demo.defaultElementShapeId ?? 'default',
84
+ });
85
+ },
86
+ },
87
+ {
88
+ id: 'add-port',
89
+ label: 'Add port',
90
+ run: (editor, state, demo, helpers) => {
91
+ const element = state.elements[0];
92
+ if (!element) return;
93
+ const offset = helpers.nextOffset();
94
+ editor.addPortToElement(element.id, {
95
+ id: `port-${createId()}`,
96
+ elementId: element.id,
97
+ position: { x: 20 + offset, y: 20 + offset },
98
+ shapeId: demo.defaultPortShapeId ?? 'port-circle',
99
+ });
100
+ },
101
+ },
102
+ {
103
+ id: 'add-link',
104
+ label: 'Add link',
105
+ run: (editor, state) => {
106
+ if (state.ports.length < 2) return;
107
+ const [first, second] = state.ports;
108
+ editor.addLink({
109
+ id: `link-${createId()}`,
110
+ sourcePortId: first.id,
111
+ targetPortId: second.id,
112
+ points: [],
113
+ });
114
+ },
115
+ },
116
+ {
117
+ id: 'add-text',
118
+ label: 'Add text',
119
+ run: (editor, state) => {
120
+ const element = state.elements[0];
121
+ if (!element) return;
122
+ editor.addText({
123
+ id: `text-${createId()}`,
124
+ content: 'New label',
125
+ position: { x: 12, y: -12 },
126
+ ownerId: element.id,
127
+ });
128
+ },
129
+ },
130
+ ],
131
+ });
@@ -0,0 +1,65 @@
1
+ import type { DiagramState } from '../../api';
2
+ import type { DemoConfig } from '../types';
3
+ import { baseElementShapes, basePortShapes } from './shared';
4
+
5
+ const createChildConstraintState = (): DiagramState => ({
6
+ elements: [
7
+ {
8
+ id: 'parent-box',
9
+ position: { x: 120, y: 120 },
10
+ size: { width: 320, height: 220 },
11
+ shapeId: 'panel',
12
+ },
13
+ {
14
+ id: 'child-inside',
15
+ position: { x: 30, y: 40 },
16
+ size: { width: 120, height: 80 },
17
+ shapeId: 'default',
18
+ parentId: 'parent-box',
19
+ moveMode: 'inside',
20
+ },
21
+ {
22
+ id: 'child-border',
23
+ position: { x: 160, y: 0 },
24
+ size: { width: 120, height: 80 },
25
+ shapeId: 'panel',
26
+ parentId: 'parent-box',
27
+ moveMode: 'border',
28
+ anchorCenter: true,
29
+ },
30
+ ],
31
+ ports: [],
32
+ links: [],
33
+ texts: [
34
+ { id: 'parent-label', content: 'Parent', position: { x: 12, y: -16 }, ownerId: 'parent-box' },
35
+ { id: 'child-inside-label', content: 'Inside', position: { x: 8, y: -14 }, ownerId: 'child-inside' },
36
+ { id: 'child-border-label', content: 'Border (center)', position: { x: 8, y: -14 }, ownerId: 'child-border' },
37
+ ],
38
+ });
39
+
40
+ export const childConstraintsDemoConfig: DemoConfig = ({
41
+ id: 'child-constraints',
42
+ title: 'Child Movement Constraints',
43
+ description: 'Child elements constrained inside or along the parent border (anchor-center on border child).',
44
+ createState: createChildConstraintState,
45
+ elementShapes: baseElementShapes,
46
+ portShapes: basePortShapes,
47
+ defaultElementShapeId: 'default',
48
+ defaultPortShapeId: 'port-circle',
49
+ actions: [
50
+ {
51
+ id: 'nudge-border-child',
52
+ label: 'Nudge border child',
53
+ run: (editor) => {
54
+ editor.moveElementTo('child-border', 260, 220);
55
+ },
56
+ },
57
+ {
58
+ id: 'nudge-inside-child',
59
+ label: 'Nudge inside child',
60
+ run: (editor) => {
61
+ editor.moveElementTo('child-inside', 60, 140);
62
+ },
63
+ },
64
+ ],
65
+ });