orcasvn-react-diagrams 0.2.7 → 0.2.9

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 (37) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +7 -12
  3. package/ai/api-contract.json +16 -0
  4. package/ai/invariants.json +8 -0
  5. package/ai/manifest.json +1 -1
  6. package/dist/cjs/examples.js +604 -39
  7. package/dist/cjs/index.js +227 -28
  8. package/dist/cjs/types/api/types.d.ts +2 -0
  9. package/dist/cjs/types/displaybox/demos/elementVisibilitySelectionDemo.d.ts +4 -0
  10. package/dist/cjs/types/engine/DiagramEngine.d.ts +8 -0
  11. package/dist/cjs/types/models/DiagramModel.d.ts +5 -0
  12. package/dist/cjs/types/models/ElementModel.d.ts +2 -0
  13. package/dist/esm/examples.js +604 -39
  14. package/dist/esm/examples.js.map +1 -1
  15. package/dist/esm/index.js +227 -28
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/types/api/types.d.ts +2 -0
  18. package/dist/esm/types/displaybox/demos/elementVisibilitySelectionDemo.d.ts +4 -0
  19. package/dist/esm/types/engine/DiagramEngine.d.ts +8 -0
  20. package/dist/esm/types/models/DiagramModel.d.ts +5 -0
  21. package/dist/esm/types/models/ElementModel.d.ts +2 -0
  22. package/dist/examples.d.ts +2 -0
  23. package/dist/index.d.ts +9 -0
  24. package/docs/API_CONTRACT.md +16 -1
  25. package/docs/CAPABILITIES.md +1 -0
  26. package/docs/COMMANDS_EVENTS.md +3 -1
  27. package/docs/DOCUMENTATION_WORKFLOW.md +3 -1
  28. package/docs/INTEGRATION_PLAYBOOK.md +1 -0
  29. package/docs/PORTING_CHECKLIST.md +1 -0
  30. package/docs/STATE_INVARIANTS.md +9 -1
  31. package/package.json +1 -1
  32. package/src/displaybox/demos/AutoLayoutDemoTab.tsx +11 -5
  33. package/src/displaybox/demos/autoLayoutDemo.ts +233 -0
  34. package/src/displaybox/demos/basicDemo.ts +6 -6
  35. package/src/displaybox/demos/elementVisibilitySelectionDemo.ts +128 -0
  36. package/src/displaybox/demos/index.tsx +14 -7
  37. package/src/displaybox/demos/selectionDemo.ts +12 -12
@@ -269,6 +269,8 @@ export type ElementData = {
269
269
  style?: Record<string, unknown>;
270
270
  portIds?: string[];
271
271
  textIds?: string[];
272
+ visible?: boolean;
273
+ selectable?: boolean;
272
274
  parentId?: string | null;
273
275
  moveMode?: MoveConstraint;
274
276
  anchorCenter?: boolean;
@@ -0,0 +1,4 @@
1
+ import type { DiagramState } from '../../api';
2
+ import type { DemoConfig } from '../types';
3
+ export declare const createElementVisibilitySelectionState: () => DiagramState;
4
+ export declare const elementVisibilitySelectionDemoConfig: DemoConfig;
@@ -76,6 +76,11 @@ export default class DiagramEngine {
76
76
  setSelection(ids: string[]): void;
77
77
  toggleSelection(id: string): void;
78
78
  getSelection(): string[];
79
+ isElementVisible(id: string): boolean;
80
+ isElementSelectable(id: string): boolean;
81
+ isPortVisible(id: string): boolean;
82
+ isLinkVisible(id: string): boolean;
83
+ isTextVisible(id: string): boolean;
79
84
  getElementWorldPosition(id: string): Point | null;
80
85
  getElementRotation(id: string): number;
81
86
  normalizeElementResize(id: string, proposal: {
@@ -146,6 +151,9 @@ export default class DiagramEngine {
146
151
  private resolveTextPresentation;
147
152
  private resolveAllTextPresentationPatches;
148
153
  private emitSelection;
154
+ private normalizeSelectionIds;
155
+ private isSelectableId;
156
+ private syncSelectionToPolicies;
149
157
  private applyLayoutForParent;
150
158
  private applyLayoutCascade;
151
159
  private applyAllLayouts;
@@ -13,14 +13,19 @@ export default class DiagramModel {
13
13
  load(state: DiagramState): void;
14
14
  toState(): DiagramState;
15
15
  getElement(id: string): ElementModel | undefined;
16
+ isElementVisible(id: string): boolean;
17
+ isElementSelectable(id: string): boolean;
16
18
  setElementLayout(id: string, layout: ElementData['layout']): void;
17
19
  getChildren(parentId: string): ElementModel[];
18
20
  getElementWorldPosition(id: string): Point | null;
19
21
  getPort(id: string): PortModel | undefined;
22
+ isPortVisible(id: string): boolean;
20
23
  getPortWorldPosition(id: string): Point | null;
21
24
  getLink(id: string): LinkModel | undefined;
25
+ isLinkVisible(id: string): boolean;
22
26
  getLinkMidpoint(id: string): Point | null;
23
27
  getText(id: string): TextModel | undefined;
28
+ isTextVisible(id: string): boolean;
24
29
  getTextWorldPosition(id: string): Point | null;
25
30
  addElement(data: ElementData): ElementModel;
26
31
  moveElement(id: string, position: Point): void;
@@ -7,6 +7,8 @@ export default class ElementModel {
7
7
  style?: Record<string, unknown>;
8
8
  portIds: string[];
9
9
  textIds: string[];
10
+ visible?: boolean;
11
+ selectable?: boolean;
10
12
  parentId: string | null;
11
13
  moveMode?: MoveConstraint;
12
14
  anchorCenter?: boolean;
@@ -242,6 +242,8 @@ type ElementData = {
242
242
  style?: Record<string, unknown>;
243
243
  portIds?: string[];
244
244
  textIds?: string[];
245
+ visible?: boolean;
246
+ selectable?: boolean;
245
247
  parentId?: string | null;
246
248
  moveMode?: MoveConstraint;
247
249
  anchorCenter?: boolean;
package/dist/index.d.ts CHANGED
@@ -269,6 +269,8 @@ type ElementData = {
269
269
  style?: Record<string, unknown>;
270
270
  portIds?: string[];
271
271
  textIds?: string[];
272
+ visible?: boolean;
273
+ selectable?: boolean;
272
274
  parentId?: string | null;
273
275
  moveMode?: MoveConstraint;
274
276
  anchorCenter?: boolean;
@@ -543,6 +545,8 @@ declare class ElementModel {
543
545
  style?: Record<string, unknown>;
544
546
  portIds: string[];
545
547
  textIds: string[];
548
+ visible?: boolean;
549
+ selectable?: boolean;
546
550
  parentId: string | null;
547
551
  moveMode?: MoveConstraint;
548
552
  anchorCenter?: boolean;
@@ -640,14 +644,19 @@ declare class DiagramModel {
640
644
  load(state: DiagramState): void;
641
645
  toState(): DiagramState;
642
646
  getElement(id: string): ElementModel | undefined;
647
+ isElementVisible(id: string): boolean;
648
+ isElementSelectable(id: string): boolean;
643
649
  setElementLayout(id: string, layout: ElementData['layout']): void;
644
650
  getChildren(parentId: string): ElementModel[];
645
651
  getElementWorldPosition(id: string): Point | null;
646
652
  getPort(id: string): PortModel | undefined;
653
+ isPortVisible(id: string): boolean;
647
654
  getPortWorldPosition(id: string): Point | null;
648
655
  getLink(id: string): LinkModel | undefined;
656
+ isLinkVisible(id: string): boolean;
649
657
  getLinkMidpoint(id: string): Point | null;
650
658
  getText(id: string): TextModel | undefined;
659
+ isTextVisible(id: string): boolean;
651
660
  getTextWorldPosition(id: string): Point | null;
652
661
  addElement(data: ElementData): ElementModel;
653
662
  moveElement(id: string, position: Point): void;
@@ -62,12 +62,22 @@ Defaults:
62
62
 
63
63
  ### `ElementData`
64
64
  - Required: `id`, `position`, `size`, `shapeId`
65
- - Optional: `style`, `portIds`, `textIds`, `parentId`, `moveMode`, `anchorCenter`, `layout`, `childElementInteraction`, `portMovement`
65
+ - Optional: `style`, `portIds`, `textIds`, `visible`, `selectable`, `parentId`, `moveMode`, `anchorCenter`, `layout`, `childElementInteraction`, `portMovement`
66
66
  - Defaults at runtime/model:
67
67
  - `portIds`: `[]`
68
68
  - `textIds`: `[]`
69
69
  - `parentId`: `null`
70
70
 
71
+ ### `ElementData.visible` and `ElementData.selectable`
72
+ - Optional `visible?: boolean` (default behavior: `true`)
73
+ - Optional `selectable?: boolean` (default behavior: `true`)
74
+ - Semantics:
75
+ - `visible: false` removes the element body from built-in render, hit testing, marquee selection, persisted selection, `zoomToFitElements`, and `exportImage({ fitToContent })`
76
+ - hidden-scene propagation also hides owned ports, owned texts, and links whose source/target ports belong to hidden elements
77
+ - `selectable: false` prevents built-in element-body click selection, marquee selection, and programmatic `setSelection(...)` / `toggleSelection(...)` from retaining that element id
78
+ - `selectable: false` does not hide the element
79
+ - visible ports on a visible-but-unselectable element remain interactable/selectable unless separately constrained
80
+
71
81
  ### `ElementLayout`
72
82
  - Required: `mode: 'manual' | 'horizontal' | 'vertical' | 'grid'`
73
83
  - Optional: `padding?: number | { x: number; y: number }`, `gap?: number`, `align?: 'start' | 'center' | 'end'`
@@ -261,7 +271,9 @@ Defaults:
261
271
  - Missing IDs in most mutators are no-op (non-throwing).
262
272
  - `setViewport` emits a `change` patch with `entity: 'viewport'` and skips immediate render scheduling.
263
273
  - `zoomToFitElements` computes bounds from `elements[]` only, ignores ports/links/texts for fit expansion, and no-ops when no elements exist.
274
+ - `zoomToFitElements` ignores hidden elements when computing fit bounds.
264
275
  - `zoomToFitElements` applies its result through `setViewport`, so hosts observe the standard viewport patch/change path.
276
+ - `exportImage({ fitToContent })` derives crop bounds from visible scene entities only.
265
277
  - Built-in empty-paper panning now starts from plain primary-button drag and no longer requires `panKey`; marquee selection uses `Shift + primary drag`.
266
278
  - `clientToWorld` uses: `world = (client - containerRect - pan) / zoom`, with zoom fallback to `1` when zoom is `0`.
267
279
  - `rerouteLinks(ids)` skips manual links unless `options.includeManual === true`.
@@ -269,6 +281,9 @@ Defaults:
269
281
  - `updateLinkPoints` always marks the link as manual routing.
270
282
  - `linkColorPoolPolicy` is opt-in and applies only to newly created links; explicitly provided non-empty `style.stroke` values are preserved.
271
283
  - `gridLayoutChanged` is additive and does not replace standard `change` / `elementResized` flows.
284
+ - `ElementData.visible = false` hides the element plus owned ports/texts and endpoint-dependent links from built-in renderer sync, hit testing, marquee selection, persisted selection, and fit/export helpers.
285
+ - `ElementData.selectable = false` blocks built-in element-body selection while still allowing visible owned ports to participate in their normal interaction flows.
286
+ - `setSelection(...)` and `toggleSelection(...)` normalize away unknown IDs, hidden scene members, and unselectable elements before emitting `selection`.
272
287
  - `ElementData.childElementInteraction.movable = false` suppresses built-in drag only for direct child elements; programmatic `moveElementTo(...)` and layout/ancestor-driven repositioning remain allowed.
273
288
  - `TextData.interaction.movable = false` blocks built-in drag only; selection still works and programmatic movement remains allowed.
274
289
  - `TextData.interaction.editable = false` blocks built-in textarea editing only; selection still works and programmatic text updates remain allowed.
@@ -17,6 +17,7 @@
17
17
  - Auto-layout parent resize policy (`grow` / `grow-shrink`) and child size constraints.
18
18
  - Optional grid-child width topology editing with `gridLayoutChanged` host events.
19
19
  - Parent-owned direct-child drag suppression through `ElementData.childElementInteraction`.
20
+ - Element-level visibility/selectability policy through `ElementData.visible` and `ElementData.selectable`, including hidden-scene propagation to owned ports/texts and endpoint-dependent links.
20
21
  - Element shape hover controls with edge/vertex/midpoint targets and interaction callbacks.
21
22
  - Optional random link color assignment from configurable pools during link creation.
22
23
  - Per-text interaction policy for suppressing built-in text drag and/or built-in text editing.
@@ -38,10 +38,11 @@
38
38
  - Plus `textDeleted` on removal (including cascade removal)
39
39
  - `setSelection/toggleSelection/deleteSelection`
40
40
  - `selection` plus derived selected entity events
41
+ - hidden scene members and `selectable:false` element IDs are filtered out before selection persists/emits
41
42
  - `setViewport`
42
43
  - `change` with viewport patch (`render: false`)
43
44
  - `zoomToFitElements`
44
- - delegates to `setViewport` when at least one element exists
45
+ - delegates to `setViewport` when at least one visible element exists
45
46
  - emits the same viewport `change` patch path
46
47
  - `setRouting/setSnapping/registerShape`
47
48
  - `config` events
@@ -64,6 +65,7 @@
64
65
  ## Failure Modes
65
66
  - Unknown IDs do not emit entity-specific movement/selection events.
66
67
  - Canceled link creation emits `elementLinkEnded` with `cancelled=true` and no link creation.
68
+ - Built-in element-body selection emits no persisted element selection when the target element is hidden or sets `selectable = false`.
67
69
  - Built-in drag on a direct child element emits no move/drop mutation when its parent sets `childElementInteraction.movable = false`.
68
70
  - `textUpdated` emits only when target text exists; missing IDs remain no-op.
69
71
 
@@ -59,7 +59,7 @@ When resuming later:
59
59
  5. Re-run packaging verification.
60
60
 
61
61
  ## 6. Current Status
62
- - Last updated: 2026-05-18
62
+ - Last updated: 2026-05-27
63
63
  - Last completed step: 10
64
64
  - Next step: 7 (optional additional fixture coverage for deeper nested/manual-route scenarios)
65
65
  - Owner: Codex (with repository maintainers)
@@ -86,3 +86,5 @@ When resuming later:
86
86
  - 2026-05-10: Ran release validations: `npm run typecheck`, `npm test -- --watchAll=false`, `npm run build`, `npm run rollup-build-lib`, and `npm pack --dry-run`.
87
87
  - 2026-05-14: Completed release-doc pass for `v0.2.6`; refreshed release highlights, API contract updates (`zoomToFitElements`, `ViewportFitOptions`, `gridChildWidthResizeEnabled`, `gridLayoutChanged`, `TextInteractionPolicy`), and machine-readable contract metadata.
88
88
  - 2026-05-18: Completed release-doc pass for `v0.2.7`; refreshed release highlights, API contract updates (`ElementChildInteractionPolicy`, `childElementInteraction`, and built-in pan gesture behavior), and machine-readable contract metadata.
89
+ - 2026-05-24: Completed release-doc pass for `v0.2.8`; refreshed release highlights and publish-facing workflow metadata for the nested grid slot-preservation fix.
90
+ - 2026-05-27: Completed release-doc pass for `v0.2.9`; refreshed release highlights, visibility/selectability API docs, state/event integration notes, and machine-readable contract metadata.
@@ -19,6 +19,7 @@ Embed this library into another diagram host while preserving deterministic stat
19
19
  6. Use `zoomToFitElements(options?)` when the host needs library-owned fit-to-elements navigation instead of recomputing viewport math externally.
20
20
  7. Set `TextData.interaction` when some labels must remain read-only or fixed in place while still using library selection behavior.
21
21
  8. Set `ElementData.childElementInteraction` on parent elements when direct child nodes should stay selectable but not be draggable through built-in interaction.
22
+ 9. Set `ElementData.visible` and `ElementData.selectable` when hosts need hidden-but-retained nodes or visible-but-nonselectable element bodies; hidden elements also suppress owned ports/texts and endpoint-dependent links from built-in view helpers.
22
23
 
23
24
  ## Path B: Engine-Only Adapter
24
25
  1. Implement `Renderer`:
@@ -8,6 +8,7 @@
8
8
  - `PortData`
9
9
  - `LinkData`
10
10
  - `TextData`
11
+ - Confirm host mapping for `ElementData.visible` / `ElementData.selectable` if hidden or non-selectable nodes must retain state.
11
12
  - Confirm owner-relative semantics for ports/texts.
12
13
 
13
14
  ## 2. Match Coordinate Semantics
@@ -12,6 +12,9 @@
12
12
  - Position is local to parent if `parentId` exists; otherwise world.
13
13
  - World position resolves by summing ancestor positions.
14
14
  - If `anchorCenter=true`, world top-left is shifted by half size.
15
+ - `visible` and `selectable` behave as enabled when omitted.
16
+ - `visible=false` excludes the element from the visible scene, and ancestor hidden state propagates to descendant elements.
17
+ - `selectable=false` prevents the element ID from persisting in engine selection state.
15
18
  - Deleting an element cascades to:
16
19
  - child ports
17
20
  - links connected through removed ports
@@ -21,6 +24,7 @@
21
24
  - Port position is local to owner element.
22
25
  - Default `anchorCenter=true` in model constructor.
23
26
  - Default `orientToHostBorder=true` in model constructor.
27
+ - Port visible-state is derived from owning element visibility.
24
28
  - Moving ports can be constrained by:
25
29
  - `moveMode: inside|border`
26
30
  - element-level `portMovement.moveMode: free|inside|border|anchors`
@@ -34,11 +38,13 @@
34
38
  - Auto routing recomputes from router strategy and normalized endpoints.
35
39
  - Manual routing preserves interior bends on endpoint movement.
36
40
  - Links with unresolved source/target world positions are skipped by reroute/update paths.
41
+ - Link visible-state requires both endpoint ports to remain visible.
37
42
 
38
43
  ## Text
39
44
  - `ownerId` can reference element, port, or link.
40
45
  - Owned text position is stored owner-relative.
41
46
  - Text with missing owner behaves as standalone world-position text.
47
+ - Owned text visible-state follows its owner visibility when the owner resolves to an element, port, or link.
42
48
  - Text interaction policy is persisted separately from style/layout metadata.
43
49
  - `interaction.movable = false` suppresses built-in drag without blocking selection or programmatic movement.
44
50
  - `interaction.editable = false` suppresses built-in editing without blocking selection or programmatic content updates.
@@ -49,8 +55,10 @@
49
55
  - `setSelection` emits:
50
56
  - `selection`
51
57
  - plus derived `elementSelected`/`portSelected`/`textSelected`
58
+ - `setSelection` and `toggleSelection` normalize IDs against current visibility/selectability rules before persisting or emitting selection.
52
59
  - `setViewport` emits `change` with `entity=viewport` and does not force render directly.
53
- - `zoomToFitElements` derives viewport fit from `elements[]` bounds only and applies the result through `setViewport`.
60
+ - `zoomToFitElements` derives viewport fit from visible `elements[]` bounds only and applies the result through `setViewport`.
61
+ - `exportImage({ fitToContent })` derives crop bounds from visible scene entities only.
54
62
 
55
63
  ## Failure Modes
56
64
  - Non-existent target IDs: operation is no-op.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orcasvn-react-diagrams",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "dependencies": {
5
5
  "eventemitter3": "^5.0.1",
6
6
  "flatten-js": "^0.6.9",
@@ -22,6 +22,9 @@ const parentOptions = [
22
22
  { id: 'layout-grid', label: 'Grid layout' },
23
23
  { id: 'layout-nested', label: 'Nested layout' },
24
24
  { id: 'layout-grid-deep', label: 'Nested grid' },
25
+ { id: 'layout-grid-empty-compare', label: 'Grid empty vs filled' },
26
+ { id: 'layout-grid-mixed-modes', label: 'Grid mixed child modes' },
27
+ { id: 'layout-column-grid-tree', label: 'Non-grid with grid descendants' },
25
28
  { id: 'layout-lock-compare', label: 'Child lock compare' },
26
29
  { id: 'layout-manual', label: 'Manual (compare)' },
27
30
  ];
@@ -391,11 +394,11 @@ const AutoLayoutDemo = () => {
391
394
  stable while height changes.
392
395
  </p>
393
396
  <p style={{ marginTop: 0, marginBottom: 0, fontSize: 13, color: '#333' }}>
394
- The <code>layout-grid-deep</code> scenario now keeps the outer container focused on grid behavior with three direct
395
- grid children, including one nested grid parent that also follows the enabled 12-unit resize path. The child drag
396
- lock comparison lives beside it in <code> layout-lock-compare</code>; toggle the lock, try dragging
397
- <code> deep-lock-child-a</code> versus <code> deep-free-child-a</code>, then use the API move shortcut to confirm
398
- the policy suppresses built-in drag without freezing geometry.
397
+ The seeded scenarios now cover empty nested grids, mixed child layout modes under one grid owner, and grid
398
+ descendants inside a non-grid tree. Use <code>layout-grid-deep</code> for enabled 12-unit nested-child resize,
399
+ <code>layout-grid-empty-compare</code> for empty-versus-filled nested grid comparison, <code>layout-grid-mixed-modes</code>
400
+ for direct-child layout mixing, and <code>layout-column-grid-tree</code> for a non-grid owner that still contains
401
+ grid-layout descendants. The child drag lock comparison lives beside them in <code>layout-lock-compare</code>.
399
402
  </p>
400
403
  </div>
401
404
  <DisplayBoxControls
@@ -747,6 +750,9 @@ const AutoLayoutDemo = () => {
747
750
  <ul style={{ marginTop: 0, paddingLeft: 18, fontSize: 13 }}>
748
751
  <li>Nested grid: target layout-grid-deep to inspect an outer grid where all three direct children, including the nested grid parent, use enabled 12-unit resize.</li>
749
752
  <li>Select deep-grid-nested-parent and drag a horizontal resize handle; it should snap in grid units and then reflow inner children inside the resized container.</li>
753
+ <li>Empty nested grid: target layout-grid-empty-compare and compare empty-grid-slot against filled-grid-slot. The empty nested grid should still occupy its assigned outer slot.</li>
754
+ <li>Mixed grid children: target layout-grid-mixed-modes to inspect one grid owner whose direct children use grid, vertical, and horizontal auto-layout modes side by side.</li>
755
+ <li>Non-grid with grid descendants: target layout-column-grid-tree to inspect a vertical parent that still contains grid-layout containers deeper in the subtree.</li>
750
756
  <li>Lock comparison: target layout-lock-compare, toggle Lock comparison child drag, then compare deep-lock-child-a and deep-free-child-a.</li>
751
757
  <li>Use Move lock target via API after locking; the child should still reposition because the policy only suppresses built-in drag.</li>
752
758
  <li>Resize lock: with grid child width resize off, select grid-b and drag diagonally; width should stay fixed.</li>
@@ -139,6 +139,195 @@ export const createAutoLayoutState = (lockComparisonChildren = true): DiagramSta
139
139
  { id: 'deep-grid-c', position: { x: 0, y: 0 }, size: { width: 52, height: 24 }, shapeId: 'default', parentId: 'deep-grid-nested-parent' },
140
140
  { id: 'deep-grid-d', position: { x: 0, y: 0 }, size: { width: 28, height: 30 }, shapeId: 'default', parentId: 'deep-grid-nested-parent' },
141
141
 
142
+ {
143
+ id: 'layout-grid-empty-compare',
144
+ position: { x: 930, y: 70 },
145
+ size: { width: 280, height: 190 },
146
+ shapeId: 'panel',
147
+ layout: {
148
+ mode: 'grid',
149
+ padding: { x: 12, y: 12 },
150
+ gap: 12,
151
+ align: 'center',
152
+ autoResize: 'grow-shrink',
153
+ gridTemplate: [4, 4, 4],
154
+ gridChildWidthResizeEnabled: true,
155
+ childMinWidth: 44,
156
+ childMinHeight: 52,
157
+ },
158
+ },
159
+ {
160
+ id: 'empty-grid-slot',
161
+ position: { x: 0, y: 0 },
162
+ size: { width: 28, height: 18 },
163
+ shapeId: 'default',
164
+ parentId: 'layout-grid-empty-compare',
165
+ layout: {
166
+ mode: 'grid',
167
+ padding: { x: 8, y: 8 },
168
+ gap: 8,
169
+ align: 'center',
170
+ autoResize: 'grow-shrink',
171
+ gridTemplate: [6, 6],
172
+ gridChildWidthResizeEnabled: true,
173
+ },
174
+ },
175
+ {
176
+ id: 'filled-grid-slot',
177
+ position: { x: 0, y: 0 },
178
+ size: { width: 92, height: 88 },
179
+ shapeId: 'default',
180
+ parentId: 'layout-grid-empty-compare',
181
+ layout: {
182
+ mode: 'grid',
183
+ padding: { x: 8, y: 8 },
184
+ gap: 8,
185
+ align: 'center',
186
+ autoResize: 'grow-shrink',
187
+ gridTemplate: [6, 6],
188
+ gridChildWidthResizeEnabled: true,
189
+ childMinWidth: 18,
190
+ childMinHeight: 18,
191
+ },
192
+ },
193
+ {
194
+ id: 'empty-grid-compare-leaf',
195
+ position: { x: 0, y: 0 },
196
+ size: { width: 60, height: 70 },
197
+ shapeId: 'default',
198
+ parentId: 'layout-grid-empty-compare',
199
+ },
200
+ { id: 'filled-grid-slot-a', position: { x: 0, y: 0 }, size: { width: 34, height: 22 }, shapeId: 'default', parentId: 'filled-grid-slot' },
201
+ { id: 'filled-grid-slot-b', position: { x: 0, y: 0 }, size: { width: 40, height: 24 }, shapeId: 'default', parentId: 'filled-grid-slot' },
202
+ { id: 'filled-grid-slot-c', position: { x: 0, y: 0 }, size: { width: 48, height: 22 }, shapeId: 'default', parentId: 'filled-grid-slot' },
203
+
204
+ {
205
+ id: 'layout-grid-mixed-modes',
206
+ position: { x: 930, y: 320 },
207
+ size: { width: 340, height: 240 },
208
+ shapeId: 'panel',
209
+ layout: {
210
+ mode: 'grid',
211
+ padding: { x: 12, y: 12 },
212
+ gap: 12,
213
+ align: 'center',
214
+ autoResize: 'grow-shrink',
215
+ gridTemplate: [4, 4, 4],
216
+ gridChildWidthResizeEnabled: true,
217
+ childMinWidth: 52,
218
+ childMinHeight: 64,
219
+ },
220
+ },
221
+ {
222
+ id: 'mixed-grid-child',
223
+ position: { x: 0, y: 0 },
224
+ size: { width: 88, height: 88 },
225
+ shapeId: 'default',
226
+ parentId: 'layout-grid-mixed-modes',
227
+ layout: {
228
+ mode: 'grid',
229
+ padding: { x: 8, y: 8 },
230
+ gap: 8,
231
+ align: 'center',
232
+ autoResize: 'grow-shrink',
233
+ gridTemplate: [6, 6],
234
+ gridChildWidthResizeEnabled: true,
235
+ childMinWidth: 18,
236
+ childMinHeight: 18,
237
+ },
238
+ },
239
+ {
240
+ id: 'mixed-column-child',
241
+ position: { x: 0, y: 0 },
242
+ size: { width: 92, height: 96 },
243
+ shapeId: 'default',
244
+ parentId: 'layout-grid-mixed-modes',
245
+ layout: {
246
+ mode: 'vertical',
247
+ padding: { x: 8, y: 8 },
248
+ gap: 8,
249
+ align: 'center',
250
+ autoResize: 'grow-shrink',
251
+ },
252
+ },
253
+ {
254
+ id: 'mixed-row-child',
255
+ position: { x: 0, y: 0 },
256
+ size: { width: 104, height: 84 },
257
+ shapeId: 'default',
258
+ parentId: 'layout-grid-mixed-modes',
259
+ layout: {
260
+ mode: 'horizontal',
261
+ padding: { x: 8, y: 8 },
262
+ gap: 8,
263
+ align: 'center',
264
+ autoResize: 'grow-shrink',
265
+ },
266
+ },
267
+ { id: 'mixed-grid-a', position: { x: 0, y: 0 }, size: { width: 34, height: 22 }, shapeId: 'default', parentId: 'mixed-grid-child' },
268
+ { id: 'mixed-grid-b', position: { x: 0, y: 0 }, size: { width: 44, height: 24 }, shapeId: 'default', parentId: 'mixed-grid-child' },
269
+ { id: 'mixed-column-a', position: { x: 0, y: 0 }, size: { width: 60, height: 20 }, shapeId: 'default', parentId: 'mixed-column-child' },
270
+ { id: 'mixed-column-b', position: { x: 0, y: 0 }, size: { width: 68, height: 24 }, shapeId: 'default', parentId: 'mixed-column-child' },
271
+ { id: 'mixed-row-a', position: { x: 0, y: 0 }, size: { width: 32, height: 28 }, shapeId: 'default', parentId: 'mixed-row-child' },
272
+ { id: 'mixed-row-b', position: { x: 0, y: 0 }, size: { width: 44, height: 24 }, shapeId: 'default', parentId: 'mixed-row-child' },
273
+
274
+ {
275
+ id: 'layout-column-grid-tree',
276
+ position: { x: 40, y: 520 },
277
+ size: { width: 340, height: 220 },
278
+ shapeId: 'panel',
279
+ layout: { mode: 'vertical', padding: { x: 12, y: 12 }, gap: 12, align: 'start', autoResize: 'grow-shrink' },
280
+ },
281
+ {
282
+ id: 'column-grid-tree-top',
283
+ position: { x: 0, y: 0 },
284
+ size: { width: 180, height: 88 },
285
+ shapeId: 'default',
286
+ parentId: 'layout-column-grid-tree',
287
+ layout: {
288
+ mode: 'grid',
289
+ padding: { x: 8, y: 8 },
290
+ gap: 8,
291
+ align: 'center',
292
+ autoResize: 'grow-shrink',
293
+ gridTemplate: [6, 6],
294
+ gridChildWidthResizeEnabled: true,
295
+ childMinWidth: 18,
296
+ childMinHeight: 18,
297
+ },
298
+ },
299
+ {
300
+ id: 'column-grid-tree-branch',
301
+ position: { x: 0, y: 0 },
302
+ size: { width: 220, height: 96 },
303
+ shapeId: 'default',
304
+ parentId: 'layout-column-grid-tree',
305
+ layout: { mode: 'horizontal', padding: { x: 8, y: 8 }, gap: 8, align: 'center', autoResize: 'grow-shrink' },
306
+ },
307
+ { id: 'column-grid-top-a', position: { x: 0, y: 0 }, size: { width: 52, height: 22 }, shapeId: 'default', parentId: 'column-grid-tree-top' },
308
+ { id: 'column-grid-top-b', position: { x: 0, y: 0 }, size: { width: 44, height: 24 }, shapeId: 'default', parentId: 'column-grid-tree-top' },
309
+ {
310
+ id: 'column-grid-tree-inner-grid',
311
+ position: { x: 0, y: 0 },
312
+ size: { width: 120, height: 84 },
313
+ shapeId: 'default',
314
+ parentId: 'column-grid-tree-branch',
315
+ layout: {
316
+ mode: 'grid',
317
+ padding: { x: 8, y: 8 },
318
+ gap: 8,
319
+ align: 'center',
320
+ autoResize: 'grow-shrink',
321
+ gridTemplate: [6, 6],
322
+ gridChildWidthResizeEnabled: true,
323
+ childMinWidth: 18,
324
+ childMinHeight: 18,
325
+ },
326
+ },
327
+ { id: 'column-grid-tree-leaf', position: { x: 0, y: 0 }, size: { width: 58, height: 62 }, shapeId: 'default', parentId: 'column-grid-tree-branch' },
328
+ { id: 'column-grid-inner-a', position: { x: 0, y: 0 }, size: { width: 30, height: 22 }, shapeId: 'default', parentId: 'column-grid-tree-inner-grid' },
329
+ { id: 'column-grid-inner-b', position: { x: 0, y: 0 }, size: { width: 42, height: 24 }, shapeId: 'default', parentId: 'column-grid-tree-inner-grid' },
330
+
142
331
  {
143
332
  id: 'layout-lock-compare',
144
333
  position: { x: 420, y: 410 },
@@ -241,6 +430,50 @@ export const createAutoLayoutState = (lockComparisonChildren = true): DiagramSta
241
430
  { id: 'label-deep-grid-b', content: 'grid-b', position: { x: 6, y: -14 }, ownerId: 'deep-grid-b' },
242
431
  { id: 'label-deep-grid-c', content: 'grid-c', position: { x: 6, y: -14 }, ownerId: 'deep-grid-c' },
243
432
  { id: 'label-deep-grid-d', content: 'grid-d', position: { x: 6, y: -14 }, ownerId: 'deep-grid-d' },
433
+ {
434
+ id: 'label-layout-grid-empty-compare',
435
+ content: 'Grid owner: empty vs filled nested grids',
436
+ position: { x: 8, y: 6 },
437
+ ownerId: 'layout-grid-empty-compare',
438
+ layout: { boundsMode: 'owner-width', wrap: 'word', overflow: 'clip', padding: 0 },
439
+ },
440
+ { id: 'label-empty-grid-slot', content: 'empty nested grid', position: { x: 6, y: -16 }, ownerId: 'empty-grid-slot' },
441
+ { id: 'label-filled-grid-slot', content: 'nested grid with children', position: { x: 6, y: -16 }, ownerId: 'filled-grid-slot' },
442
+ { id: 'label-empty-grid-compare-leaf', content: 'leaf sibling', position: { x: 6, y: -16 }, ownerId: 'empty-grid-compare-leaf' },
443
+ { id: 'label-filled-grid-slot-a', content: 'slot-a', position: { x: 6, y: -14 }, ownerId: 'filled-grid-slot-a' },
444
+ { id: 'label-filled-grid-slot-b', content: 'slot-b', position: { x: 6, y: -14 }, ownerId: 'filled-grid-slot-b' },
445
+ { id: 'label-filled-grid-slot-c', content: 'slot-c', position: { x: 6, y: -14 }, ownerId: 'filled-grid-slot-c' },
446
+ {
447
+ id: 'label-layout-grid-mixed-modes',
448
+ content: 'Grid owner: mixed child layout modes',
449
+ position: { x: 8, y: 6 },
450
+ ownerId: 'layout-grid-mixed-modes',
451
+ layout: { boundsMode: 'owner-width', wrap: 'word', overflow: 'clip', padding: 0 },
452
+ },
453
+ { id: 'label-mixed-grid-child', content: 'grid child', position: { x: 6, y: -16 }, ownerId: 'mixed-grid-child' },
454
+ { id: 'label-mixed-column-child', content: 'vertical child', position: { x: 6, y: -16 }, ownerId: 'mixed-column-child' },
455
+ { id: 'label-mixed-row-child', content: 'horizontal child', position: { x: 6, y: -16 }, ownerId: 'mixed-row-child' },
456
+ { id: 'label-mixed-grid-a', content: 'grid-a', position: { x: 6, y: -14 }, ownerId: 'mixed-grid-a' },
457
+ { id: 'label-mixed-grid-b', content: 'grid-b', position: { x: 6, y: -14 }, ownerId: 'mixed-grid-b' },
458
+ { id: 'label-mixed-column-a', content: 'col-a', position: { x: 6, y: -14 }, ownerId: 'mixed-column-a' },
459
+ { id: 'label-mixed-column-b', content: 'col-b', position: { x: 6, y: -14 }, ownerId: 'mixed-column-b' },
460
+ { id: 'label-mixed-row-a', content: 'row-a', position: { x: 6, y: -14 }, ownerId: 'mixed-row-a' },
461
+ { id: 'label-mixed-row-b', content: 'row-b', position: { x: 6, y: -14 }, ownerId: 'mixed-row-b' },
462
+ {
463
+ id: 'label-layout-column-grid-tree',
464
+ content: 'Vertical owner with grid descendants',
465
+ position: { x: 8, y: 6 },
466
+ ownerId: 'layout-column-grid-tree',
467
+ layout: { boundsMode: 'owner-width', wrap: 'word', overflow: 'clip', padding: 0 },
468
+ },
469
+ { id: 'label-column-grid-tree-top', content: 'top-level grid child', position: { x: 6, y: -16 }, ownerId: 'column-grid-tree-top' },
470
+ { id: 'label-column-grid-tree-branch', content: 'row branch with inner grid', position: { x: 6, y: -16 }, ownerId: 'column-grid-tree-branch' },
471
+ { id: 'label-column-grid-top-a', content: 'top-a', position: { x: 6, y: -14 }, ownerId: 'column-grid-top-a' },
472
+ { id: 'label-column-grid-top-b', content: 'top-b', position: { x: 6, y: -14 }, ownerId: 'column-grid-top-b' },
473
+ { id: 'label-column-grid-tree-inner-grid', content: 'nested grid descendant', position: { x: 6, y: -16 }, ownerId: 'column-grid-tree-inner-grid' },
474
+ { id: 'label-column-grid-tree-leaf', content: 'branch leaf', position: { x: 6, y: -14 }, ownerId: 'column-grid-tree-leaf' },
475
+ { id: 'label-column-grid-inner-a', content: 'inner-a', position: { x: 6, y: -14 }, ownerId: 'column-grid-inner-a' },
476
+ { id: 'label-column-grid-inner-b', content: 'inner-b', position: { x: 6, y: -14 }, ownerId: 'column-grid-inner-b' },
244
477
  {
245
478
  id: 'label-layout-lock-compare',
246
479
  content: 'Child drag lock comparison',
@@ -60,12 +60,12 @@ const createBasicState = (): DiagramState => ({
60
60
  ],
61
61
  });
62
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,
63
+ export const basicDemoConfig: DemoConfig = ({
64
+ id: 'basic',
65
+ title: 'Basic Diagram',
66
+ description: 'Elements, ports, link, and labels. Drag empty paper to pan; use Shift + drag for marquee selection.',
67
+ createState: createBasicState,
68
+ elementShapes: baseElementShapes,
69
69
  portShapes: basePortShapes,
70
70
  defaultElementShapeId: 'default',
71
71
  defaultPortShapeId: 'port-circle',