orcasvn-react-diagrams 0.2.6 → 0.2.7

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.
@@ -29,6 +29,9 @@ export type TextInteractionPolicy = {
29
29
  movable?: boolean;
30
30
  editable?: boolean;
31
31
  };
32
+ export type ElementChildInteractionPolicy = {
33
+ movable?: boolean;
34
+ };
32
35
  export type BorderSide = 'left' | 'right' | 'top' | 'bottom';
33
36
  export type HostAnchorPreset = 'vertices' | 'cardinal';
34
37
  export type PortLinkAttachMode = 'external' | 'internal';
@@ -270,6 +273,7 @@ export type ElementData = {
270
273
  moveMode?: MoveConstraint;
271
274
  anchorCenter?: boolean;
272
275
  layout?: ElementLayout;
276
+ childElementInteraction?: ElementChildInteractionPolicy;
273
277
  portMovement?: ElementPortMovementPolicy;
274
278
  };
275
279
  export type PortData = {
@@ -1,2 +1,4 @@
1
+ import type { DiagramState } from '../../api';
1
2
  import type { DemoConfig } from '../types';
3
+ export declare const createAutoLayoutState: (lockComparisonChildren?: boolean) => DiagramState;
2
4
  export declare const autoLayoutDemoConfig: DemoConfig;
@@ -94,6 +94,8 @@ export default class DiagramEngine {
94
94
  private shouldRestrictAutoLayoutChildRightGrowth;
95
95
  private resolveSnappedGridChildWidth;
96
96
  private resolveGridChildWidthResizeTopologyChange;
97
+ private isLeafGridChildResizeCandidate;
98
+ private isEnabledGridChildWidthResizeCandidate;
97
99
  getPortWorldPosition(id: string): Point | null;
98
100
  getPortLinkWorldPosition(id: string, options?: {
99
101
  oppositePortId?: string;
@@ -11,6 +11,7 @@ export default class ElementModel {
11
11
  moveMode?: MoveConstraint;
12
12
  anchorCenter?: boolean;
13
13
  layout?: ElementData['layout'];
14
+ childElementInteraction?: ElementData['childElementInteraction'];
14
15
  portMovement?: ElementData['portMovement'];
15
16
  constructor(data: ElementData);
16
17
  setPosition(position: Point): void;
@@ -74,7 +74,6 @@ export default class KonvaInteraction {
74
74
  private readonly occupiedVertexTolerance;
75
75
  private readonly nestedHoverElementPadding;
76
76
  private emittingElementLinkEnded;
77
- private panKey;
78
77
  constructor(engine: DiagramEngine, config?: InteractionConfig);
79
78
  setShapeHoverControls(controls?: ElementShapeHoverControls): void;
80
79
  startLinkFromPort(sourcePortId: string, pointer?: Point): void;
@@ -147,6 +146,7 @@ export default class KonvaInteraction {
147
146
  private getPortById;
148
147
  private getTextById;
149
148
  private isTextMovable;
149
+ private isElementMovable;
150
150
  private isTextEditable;
151
151
  private getLinkById;
152
152
  private resolveTextScreenPosition;
@@ -154,5 +154,4 @@ export default class KonvaInteraction {
154
154
  private applyStageTransform;
155
155
  private cancelLinkDrag;
156
156
  private isEditableTarget;
157
- private isPanKeyPressed;
158
157
  }
@@ -31,6 +31,9 @@ type TextInteractionPolicy = {
31
31
  movable?: boolean;
32
32
  editable?: boolean;
33
33
  };
34
+ type ElementChildInteractionPolicy = {
35
+ movable?: boolean;
36
+ };
34
37
  type BorderSide = 'left' | 'right' | 'top' | 'bottom';
35
38
  type HostAnchorPreset = 'vertices' | 'cardinal';
36
39
  type PortLinkAttachMode = 'external' | 'internal';
@@ -243,6 +246,7 @@ type ElementData = {
243
246
  moveMode?: MoveConstraint;
244
247
  anchorCenter?: boolean;
245
248
  layout?: ElementLayout;
249
+ childElementInteraction?: ElementChildInteractionPolicy;
246
250
  portMovement?: ElementPortMovementPolicy;
247
251
  };
248
252
  type PortData = {
package/dist/index.d.ts CHANGED
@@ -29,6 +29,9 @@ type TextInteractionPolicy = {
29
29
  movable?: boolean;
30
30
  editable?: boolean;
31
31
  };
32
+ type ElementChildInteractionPolicy = {
33
+ movable?: boolean;
34
+ };
32
35
  type BorderSide$1 = 'left' | 'right' | 'top' | 'bottom';
33
36
  type HostAnchorPreset = 'vertices' | 'cardinal';
34
37
  type PortLinkAttachMode = 'external' | 'internal';
@@ -270,6 +273,7 @@ type ElementData = {
270
273
  moveMode?: MoveConstraint;
271
274
  anchorCenter?: boolean;
272
275
  layout?: ElementLayout;
276
+ childElementInteraction?: ElementChildInteractionPolicy;
273
277
  portMovement?: ElementPortMovementPolicy;
274
278
  };
275
279
  type PortData = {
@@ -543,6 +547,7 @@ declare class ElementModel {
543
547
  moveMode?: MoveConstraint;
544
548
  anchorCenter?: boolean;
545
549
  layout?: ElementData['layout'];
550
+ childElementInteraction?: ElementData['childElementInteraction'];
546
551
  portMovement?: ElementData['portMovement'];
547
552
  constructor(data: ElementData);
548
553
  setPosition(position: Point): void;
@@ -834,4 +839,4 @@ type DiagramEditorHandle = DiagramEngineHandle & {
834
839
  };
835
840
  declare const createDiagramEditor: (config: DiagramEditorConfig) => DiagramEditorHandle;
836
841
 
837
- export { type AnchorReference, type BorderSide$1 as BorderSide, type ClientRectLike, type DiagramContainer, type DiagramEditorConfig, type DiagramEditorHandle, type DiagramEngineHandle, type DiagramImageExportOptions, type DiagramPatch, type DiagramState, type EdgeHoverControl, type ElementData, type ElementDeletedEvent, type ElementDropEvent, type ElementLayout, type ElementLayoutAlign, type ElementLayoutAutoResizeMode, type ElementLayoutChildFitCrossAxis, type ElementLayoutChildFitMainAxis, type ElementLayoutGridRowTemplate, type ElementLayoutLabelReservedSpace, type ElementLayoutLabelReservedSpaceMode, type ElementLayoutLabelReservedSpacePlacement, type ElementLayoutMode, type ElementLinkConnectingEvent, type ElementLinkEndedEvent, type ElementLinkStartedEvent, type ElementMovedEvent, type ElementPointerEvent, type ElementPortMovementPolicy, type ElementResizedEvent, type ElementSelectedEvent, type ElementShapeControlDragEvent, type ElementShapeControlEventType, type ElementShapeHoverControlActivationEvent, type ElementShapeHoverControlInteractionEvent, type ElementShapeHoverControls, EllipseMidPoint, type EngineChangeEvent, type EngineConfigEvent, type EngineEventMap, type EnginePointerInfo, type EngineSelectionEvent, type GridLayoutChangedEvent, type GridLayoutChangedReason, type GridRowSnapshot, type HostAnchorPreset, type HoverControlIcon, type LinkColorPoolPolicy, type LinkData, type LinkDeletedEvent, type LinkRouteRefreshMode, type LinkRouteRefreshPolicy, type LinkRoutingMode, type MoveConstraint, type OverlayShapeConfig, type OverlayShapeHandle, type PaperClickEvent, type Point, type PortAnchor, type PortAnchorConstraint, type PortBorderTransformContext, type PortBorderTransformResult, type PortData, type PortDeletedEvent, type PortLinkAttachMode, type PortMouseEvent, type PortMovedEvent, type PortPositionLimits, type PortSelectedEvent, type PositionAxisLimit, type Rect, type RerouteLinksOptions, type ResolvePortAnchorsOptions, type ShapeControlDefinition, type ShapeControlTargetKind, type ShapeControlVisibilityTrigger, type ShapeDrawContext, type ShapeEdgeTarget, type ShapeEllipseMidPointTarget, type ShapeHoverGeometry, type ShapeVertexTarget, type SimpleShape, type Size, type TextData, type TextDeletedEvent, type TextInteractionPolicy, type TextLayout, type TextLayoutBoundsMode, type TextLayoutOverflowMode, type TextLayoutWrapMode, type TextSelectedEvent, type TextUpdatedEvent, type VertexHoverControl, type ViewportFitOptions, createDiagramEditor, createDiagramEngine };
842
+ export { type AnchorReference, type BorderSide$1 as BorderSide, type ClientRectLike, type DiagramContainer, type DiagramEditorConfig, type DiagramEditorHandle, type DiagramEngineHandle, type DiagramImageExportOptions, type DiagramPatch, type DiagramState, type EdgeHoverControl, type ElementChildInteractionPolicy, type ElementData, type ElementDeletedEvent, type ElementDropEvent, type ElementLayout, type ElementLayoutAlign, type ElementLayoutAutoResizeMode, type ElementLayoutChildFitCrossAxis, type ElementLayoutChildFitMainAxis, type ElementLayoutGridRowTemplate, type ElementLayoutLabelReservedSpace, type ElementLayoutLabelReservedSpaceMode, type ElementLayoutLabelReservedSpacePlacement, type ElementLayoutMode, type ElementLinkConnectingEvent, type ElementLinkEndedEvent, type ElementLinkStartedEvent, type ElementMovedEvent, type ElementPointerEvent, type ElementPortMovementPolicy, type ElementResizedEvent, type ElementSelectedEvent, type ElementShapeControlDragEvent, type ElementShapeControlEventType, type ElementShapeHoverControlActivationEvent, type ElementShapeHoverControlInteractionEvent, type ElementShapeHoverControls, EllipseMidPoint, type EngineChangeEvent, type EngineConfigEvent, type EngineEventMap, type EnginePointerInfo, type EngineSelectionEvent, type GridLayoutChangedEvent, type GridLayoutChangedReason, type GridRowSnapshot, type HostAnchorPreset, type HoverControlIcon, type LinkColorPoolPolicy, type LinkData, type LinkDeletedEvent, type LinkRouteRefreshMode, type LinkRouteRefreshPolicy, type LinkRoutingMode, type MoveConstraint, type OverlayShapeConfig, type OverlayShapeHandle, type PaperClickEvent, type Point, type PortAnchor, type PortAnchorConstraint, type PortBorderTransformContext, type PortBorderTransformResult, type PortData, type PortDeletedEvent, type PortLinkAttachMode, type PortMouseEvent, type PortMovedEvent, type PortPositionLimits, type PortSelectedEvent, type PositionAxisLimit, type Rect, type RerouteLinksOptions, type ResolvePortAnchorsOptions, type ShapeControlDefinition, type ShapeControlTargetKind, type ShapeControlVisibilityTrigger, type ShapeDrawContext, type ShapeEdgeTarget, type ShapeEllipseMidPointTarget, type ShapeHoverGeometry, type ShapeVertexTarget, type SimpleShape, type Size, type TextData, type TextDeletedEvent, type TextInteractionPolicy, type TextLayout, type TextLayoutBoundsMode, type TextLayoutOverflowMode, type TextLayoutWrapMode, type TextSelectedEvent, type TextUpdatedEvent, type VertexHoverControl, type ViewportFitOptions, createDiagramEditor, createDiagramEngine };
@@ -22,7 +22,7 @@ Primary definitions:
22
22
  - `initialState?: DiagramState`
23
23
  - `elementShapes?: SimpleShape[]`
24
24
  - `portShapes?: SimpleShape[]`
25
- - `panKey?: 'Control' | 'Shift' | 'Alt' | 'Meta'` (default: `'Control'`)
25
+ - `panKey?: 'Control' | 'Shift' | 'Alt' | 'Meta'` (default: `'Control'`; accepted for compatibility, but built-in empty-paper panning no longer depends on it)
26
26
  - `elementShapeHoverControls?: ElementShapeHoverControls`
27
27
  - `onElementShapeHoverControlInteraction?: (event: ElementShapeHoverControlInteractionEvent) => void`
28
28
  - `onElementShapeHoverControlActivated?: (event: ElementShapeHoverControlActivationEvent) => void` (deprecated callback contract)
@@ -62,7 +62,7 @@ Defaults:
62
62
 
63
63
  ### `ElementData`
64
64
  - Required: `id`, `position`, `size`, `shapeId`
65
- - Optional: `style`, `portIds`, `textIds`, `parentId`, `moveMode`, `anchorCenter`, `layout`, `portMovement`
65
+ - Optional: `style`, `portIds`, `textIds`, `parentId`, `moveMode`, `anchorCenter`, `layout`, `childElementInteraction`, `portMovement`
66
66
  - Defaults at runtime/model:
67
67
  - `portIds`: `[]`
68
68
  - `textIds`: `[]`
@@ -74,6 +74,7 @@ Defaults:
74
74
  - Optional: `autoResize?: 'grow' | 'grow-shrink'` (default: `'grow'`)
75
75
  - Optional grid controls: `gridTemplate?: number[]`
76
76
  - Optional grid interaction policy: `gridChildWidthResizeEnabled?: boolean` (default: `false`)
77
+ - Enabled grid-child width topology semantics apply to all direct children of the grid parent, including nested layout-container children.
77
78
  - Optional fit controls: `childFitMainAxis?: 'none' | 'distribute'`, `childFitCrossAxis?: 'none' | 'stretch'`, `childFitMinSize?: Partial<Size>`, `childFitMaxSize?: Partial<Size>`
78
79
  - Optional child size aliases: `childMinWidth?: number`, `childMaxWidth?: number`, `childMinHeight?: number`, `childMaxHeight?: number`
79
80
  - Optional label reservation: `labelReservedSpace?: ElementLayoutLabelReservedSpace`
@@ -100,6 +101,14 @@ Defaults:
100
101
  - omitted fields preserve existing built-in behavior
101
102
  - programmatic `moveTextTo(...)` and `updateText(...)` remain allowed
102
103
 
104
+ ### `ElementChildInteractionPolicy`
105
+ - Optional `movable?: boolean` (default behavior: `true`)
106
+ - Semantics:
107
+ - configured on a parent element through `ElementData.childElementInteraction`
108
+ - applies to direct child elements only
109
+ - `movable: false` suppresses built-in pointer drag for those direct child elements
110
+ - selection, linking, resize, and programmatic/layout-driven movement remain available
111
+
103
112
  ### `ElementPortMovementPolicy`
104
113
  - `moveMode: 'free' | 'inside' | 'border' | 'anchors'`
105
114
  - Optional `anchorConstraint?: PortAnchorConstraint`
@@ -253,12 +262,14 @@ Defaults:
253
262
  - `setViewport` emits a `change` patch with `entity: 'viewport'` and skips immediate render scheduling.
254
263
  - `zoomToFitElements` computes bounds from `elements[]` only, ignores ports/links/texts for fit expansion, and no-ops when no elements exist.
255
264
  - `zoomToFitElements` applies its result through `setViewport`, so hosts observe the standard viewport patch/change path.
265
+ - Built-in empty-paper panning now starts from plain primary-button drag and no longer requires `panKey`; marquee selection uses `Shift + primary drag`.
256
266
  - `clientToWorld` uses: `world = (client - containerRect - pan) / zoom`, with zoom fallback to `1` when zoom is `0`.
257
267
  - `rerouteLinks(ids)` skips manual links unless `options.includeManual === true`.
258
268
  - `linkRouteRefreshPolicy.mode` defaults to `'mutation-only'`; `'redraw-two-endpoint-change'` enables redraw-cycle reroute checks for links whose two endpoints changed.
259
269
  - `updateLinkPoints` always marks the link as manual routing.
260
270
  - `linkColorPoolPolicy` is opt-in and applies only to newly created links; explicitly provided non-empty `style.stroke` values are preserved.
261
271
  - `gridLayoutChanged` is additive and does not replace standard `change` / `elementResized` flows.
272
+ - `ElementData.childElementInteraction.movable = false` suppresses built-in drag only for direct child elements; programmatic `moveElementTo(...)` and layout/ancestor-driven repositioning remain allowed.
262
273
  - `TextData.interaction.movable = false` blocks built-in drag only; selection still works and programmatic movement remains allowed.
263
274
  - `TextData.interaction.editable = false` blocks built-in textarea editing only; selection still works and programmatic text updates remain allowed.
264
275
  - Text layout/overflow resolution updates `displayContent` and emits `textUpdated` when `updateText` is called.
@@ -16,6 +16,7 @@
16
16
  - Auto-layout modes: `manual`, `horizontal`, `vertical`, and `grid` with template rows/columns.
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
+ - Parent-owned direct-child drag suppression through `ElementData.childElementInteraction`.
19
20
  - Element shape hover controls with edge/vertex/midpoint targets and interaction callbacks.
20
21
  - Optional random link color assignment from configurable pools during link creation.
21
22
  - Per-text interaction policy for suppressing built-in text drag and/or built-in text editing.
@@ -64,6 +64,7 @@
64
64
  ## Failure Modes
65
65
  - Unknown IDs do not emit entity-specific movement/selection events.
66
66
  - Canceled link creation emits `elementLinkEnded` with `cancelled=true` and no link creation.
67
+ - Built-in drag on a direct child element emits no move/drop mutation when its parent sets `childElementInteraction.movable = false`.
67
68
  - `textUpdated` emits only when target text exists; missing IDs remain no-op.
68
69
 
69
70
  ## Verification References
@@ -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-14
62
+ - Last updated: 2026-05-18
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)
@@ -85,3 +85,4 @@ When resuming later:
85
85
  - 2026-05-10: Completed release-doc pass for `v0.2.5`; refreshed release highlights, API contract updates (`linkRouteRefreshPolicy`, `linkColorPoolPolicy`, grid auto-layout fields), and machine-readable contract metadata.
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
+ - 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.
@@ -18,6 +18,7 @@ Embed this library into another diagram host while preserving deterministic stat
18
18
  5. For shape controls, pass `elementShapeHoverControls` and handle `onElementShapeHoverControlInteraction`.
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
+ 8. Set `ElementData.childElementInteraction` on parent elements when direct child nodes should stay selectable but not be draggable through built-in interaction.
21
22
 
22
23
  ## Path B: Engine-Only Adapter
23
24
  1. Implement `Renderer`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orcasvn-react-diagrams",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "dependencies": {
5
5
  "eventemitter3": "^5.0.1",
6
6
  "flatten-js": "^0.6.9",
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import type {
3
3
  ElementLayout,
4
4
  ElementLayoutAutoResizeMode,
@@ -13,7 +13,7 @@ import useDemoControls from '../useDemoControls';
13
13
  import useDemoEditor from '../useDemoEditor';
14
14
  import useOffsetSequence from '../useOffsetSequence';
15
15
  import type { DemoActionHelpers } from '../types';
16
- import { autoLayoutDemoConfig } from './autoLayoutDemo';
16
+ import { autoLayoutDemoConfig, createAutoLayoutState } from './autoLayoutDemo';
17
17
  import { gridStageStyle } from './shared';
18
18
 
19
19
  const parentOptions = [
@@ -21,12 +21,17 @@ const parentOptions = [
21
21
  { id: 'layout-column', label: 'Vertical layout' },
22
22
  { id: 'layout-grid', label: 'Grid layout' },
23
23
  { id: 'layout-nested', label: 'Nested layout' },
24
+ { id: 'layout-grid-deep', label: 'Nested grid' },
25
+ { id: 'layout-lock-compare', label: 'Child lock compare' },
24
26
  { id: 'layout-manual', label: 'Manual (compare)' },
25
27
  ];
26
28
  const shortLabel = 'Parent label lane demo';
27
29
  const longLabel =
28
30
  'Parent label lane demo with longer content to increase flexible reserved space and push children downward.';
29
31
  const defaultGridTemplateText = '[12,4,8]';
32
+ const lockComparisonContainerId = 'layout-lock-compare';
33
+ const lockParentId = 'deep-lock-parent';
34
+ const lockTargetId = 'deep-lock-child-a';
30
35
 
31
36
  const serializeGridTemplate = (template?: ElementLayoutGridRowTemplate[]): string =>
32
37
  Array.isArray(template) && template.length > 0 ? JSON.stringify(template) : defaultGridTemplateText;
@@ -61,8 +66,13 @@ const parseGridTemplate = (value: string): ElementLayoutGridRowTemplate[] | unde
61
66
 
62
67
  const AutoLayoutDemo = () => {
63
68
  const demo = autoLayoutDemoConfig;
69
+ const [lockComparisonChildren, setLockComparisonChildren] = useState<boolean>(true);
70
+ const createState = useCallback(
71
+ () => createAutoLayoutState(lockComparisonChildren),
72
+ [lockComparisonChildren],
73
+ );
64
74
  const { containerRef, editorRef, diagramState, selection, snapEnabled, setSnapEnabled } = useDemoEditor({
65
- createState: demo.createState,
75
+ createState,
66
76
  elementShapes: demo.elementShapes,
67
77
  portShapes: demo.portShapes,
68
78
  });
@@ -136,6 +146,34 @@ const AutoLayoutDemo = () => {
136
146
  () => diagramState?.elements.find((el) => el.id === targetId),
137
147
  [diagramState, targetId],
138
148
  );
149
+ const resolveElementWorldPosition = useCallback(
150
+ (elementId: string) => {
151
+ if (!diagramState) return null;
152
+ const byId = new Map(diagramState.elements.map((element) => [element.id, element]));
153
+ let current = byId.get(elementId);
154
+ if (!current) return null;
155
+ let x = 0;
156
+ let y = 0;
157
+ while (current) {
158
+ x += current.position.x;
159
+ y += current.position.y;
160
+ current = current.parentId ? byId.get(current.parentId) : undefined;
161
+ }
162
+ return { x, y };
163
+ },
164
+ [diagramState],
165
+ );
166
+ const lockDemoContext = useMemo(() => {
167
+ if (!diagramState) return null;
168
+ const outer = diagramState.elements.find((el) => el.id === lockComparisonContainerId);
169
+ const parent = diagramState.elements.find((el) => el.id === lockParentId);
170
+ if (!outer || !parent) return null;
171
+ return {
172
+ outer,
173
+ parent,
174
+ isLocked: parent.childElementInteraction?.movable === false,
175
+ };
176
+ }, [diagramState]);
139
177
 
140
178
  const targetOptions = useMemo(() => {
141
179
  const seen = new Set<string>();
@@ -304,20 +342,28 @@ const AutoLayoutDemo = () => {
304
342
  }
305
343
  };
306
344
 
307
- const handleFocusResizeLockChild = () => {
345
+ const handleFocusLockTarget = () => {
308
346
  const editor = editorRef.current;
309
347
  if (!editor) return;
310
- setTargetId('layout-grid');
311
- editor.setSelection(['grid-b']);
312
- setLastTrigger('focus resize-lock child');
348
+ setTargetId(lockParentId);
349
+ editor.setSelection([lockTargetId]);
350
+ setLastTrigger(lockComparisonChildren ? 'focus locked child' : 'focus unlocked comparison child');
313
351
  };
314
352
 
315
- const handleFocusResizeLockParent = () => {
353
+ const handleFocusLockParent = () => {
316
354
  const editor = editorRef.current;
317
355
  if (!editor) return;
318
- setTargetId('layout-grid');
319
- editor.setSelection(['layout-grid']);
320
- setLastTrigger('focus resize-lock parent');
356
+ setTargetId(lockParentId);
357
+ editor.setSelection([lockParentId]);
358
+ setLastTrigger('focus lock parent');
359
+ };
360
+
361
+ const handleMoveLockTargetViaApi = () => {
362
+ const editor = editorRef.current;
363
+ const parentWorld = resolveElementWorldPosition(lockParentId);
364
+ if (!editor || !lockDemoContext || !parentWorld) return;
365
+ editor.moveElementTo(lockTargetId, parentWorld.x + 56, parentWorld.y + 18);
366
+ setLastTrigger(lockDemoContext.isLocked ? 'locked child moved by api' : 'comparison child moved by api');
321
367
  };
322
368
 
323
369
  const alignmentLabel =
@@ -337,10 +383,19 @@ const AutoLayoutDemo = () => {
337
383
  remove children to see automatic reflow. Use fit controls, explicit child min/max constraints, auto-resize
338
384
  policy, 12-unit grid templates, and the optional grid child width-resize capability to validate current behavior.
339
385
  </p>
386
+ <p style={{ marginTop: 0, fontSize: 13, color: '#333' }}>
387
+ Grid child width resize starts enabled for the seeded grid parents in this demo. With the capability on, horizontal
388
+ drag can wrap rows automatically from a flat 12-unit slot array, resize direct grid children in 1/12 steps, and
389
+ reorder peers by drag position. That now includes nested layout parents as well as leaf children. Turn the checkbox
390
+ off on a selected grid parent to compare the older locked-width behavior where a mostly downward drag keeps width
391
+ stable while height changes.
392
+ </p>
340
393
  <p style={{ marginTop: 0, marginBottom: 0, fontSize: 13, color: '#333' }}>
341
- Grid child width resize is disabled by default. With the capability off, select <code>grid-b</code> and drag a
342
- corner mostly downward: width stays stable while height changes. Turn the capability on to let horizontal drag
343
- wrap rows automatically from a flat 12-unit slot array, then resize children in 1/12 steps. Any corner can drive width changes, width always grows or shrinks on the right, siblings keep their own widths, and dragging can swap grid item order.
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.
344
399
  </p>
345
400
  </div>
346
401
  <DisplayBoxControls
@@ -554,6 +609,23 @@ const AutoLayoutDemo = () => {
554
609
  Enable 12-unit resize on child width drag
555
610
  </label>
556
611
 
612
+ <label htmlFor="lock-comparison-child-drag" style={{ fontWeight: 600 }}>
613
+ Lock comparison child drag
614
+ </label>
615
+ <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
616
+ <input
617
+ id="lock-comparison-child-drag"
618
+ type="checkbox"
619
+ checked={lockComparisonChildren}
620
+ onChange={(event) => {
621
+ setLockComparisonChildren(event.target.checked);
622
+ setTargetId(lockComparisonContainerId);
623
+ setLastTrigger(event.target.checked ? 'comparison child drag locked' : 'comparison child drag unlocked');
624
+ }}
625
+ />
626
+ Reload parent-owned lock demo
627
+ </label>
628
+
557
629
  <label htmlFor="label-reserved-mode-select" style={{ fontWeight: 600 }}>
558
630
  Label lane
559
631
  </label>
@@ -643,12 +715,15 @@ const AutoLayoutDemo = () => {
643
715
  >
644
716
  Label long
645
717
  </button>
646
- <button type="button" onClick={handleFocusResizeLockChild} style={{ padding: '6px 10px' }}>
647
- Focus lock child
718
+ <button type="button" onClick={handleFocusLockTarget} style={{ padding: '6px 10px' }}>
719
+ Focus lock target
648
720
  </button>
649
- <button type="button" onClick={handleFocusResizeLockParent} style={{ padding: '6px 10px' }}>
721
+ <button type="button" onClick={handleFocusLockParent} style={{ padding: '6px 10px' }}>
650
722
  Focus lock parent
651
723
  </button>
724
+ <button type="button" onClick={handleMoveLockTargetViaApi} style={{ padding: '6px 10px' }}>
725
+ Move lock target via API
726
+ </button>
652
727
  </div>
653
728
 
654
729
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
@@ -670,6 +745,10 @@ const AutoLayoutDemo = () => {
670
745
  <div>
671
746
  <div style={{ fontWeight: 600, marginBottom: 6 }}>Scenario tips</div>
672
747
  <ul style={{ marginTop: 0, paddingLeft: 18, fontSize: 13 }}>
748
+ <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
+ <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>
750
+ <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
+ <li>Use Move lock target via API after locking; the child should still reposition because the policy only suppresses built-in drag.</li>
673
752
  <li>Resize lock: with grid child width resize off, select grid-b and drag diagonally; width should stay fixed.</li>
674
753
  <li>Enable grid child width resize, then shrink a full-width grid child; it should snap smaller without flashing, and later siblings should keep their own widths.</li>
675
754
  <li>Enable grid child width resize, then grow a child; siblings on the right should keep their own widths and only move down when they no longer fit.</li>
@@ -734,4 +813,3 @@ const AutoLayoutDemo = () => {
734
813
  };
735
814
 
736
815
  export default AutoLayoutDemo;
737
-
@@ -1,9 +1,9 @@
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: [
1
+ import type { DiagramState } from '../../api';
2
+ import type { DemoConfig } from '../types';
3
+ import { baseElementShapes, basePortShapes } from './shared';
4
+
5
+ export const createAutoLayoutState = (lockComparisonChildren = true): DiagramState => ({
6
+ elements: [
7
7
  {
8
8
  id: 'layout-row',
9
9
  position: { x: 60, y: 80 },
@@ -44,14 +44,14 @@ const createAutoLayoutState = (): DiagramState => ({
44
44
  layout: {
45
45
  mode: 'grid',
46
46
  padding: { x: 10, y: 10 },
47
- gap: 10,
48
- align: 'center',
49
- autoResize: 'grow-shrink',
50
- gridTemplate: [12, 4, 8],
51
- gridChildWidthResizeEnabled: false,
52
- childMinWidth: 24,
53
- childMinHeight: 18,
54
- },
47
+ gap: 10,
48
+ align: 'center',
49
+ autoResize: 'grow-shrink',
50
+ gridTemplate: [12, 4, 8],
51
+ gridChildWidthResizeEnabled: true,
52
+ childMinWidth: 24,
53
+ childMinHeight: 18,
54
+ },
55
55
  },
56
56
  { id: 'grid-a', position: { x: 0, y: 0 }, size: { width: 36, height: 26 }, shapeId: 'default', parentId: 'layout-grid' },
57
57
  { id: 'grid-b', position: { x: 0, y: 0 }, size: { width: 44, height: 24 }, shapeId: 'default', parentId: 'layout-grid' },
@@ -82,14 +82,95 @@ const createAutoLayoutState = (): DiagramState => ({
82
82
  size: { width: 110, height: 26 },
83
83
  shapeId: 'default',
84
84
  parentId: 'nested-stack',
85
- },
86
- { id: 'nested-peer', position: { x: 0, y: 0 }, size: { width: 50, height: 90 }, shapeId: 'default', parentId: 'layout-nested' },
87
-
88
- {
89
- id: 'layout-manual',
90
- position: { x: 420, y: 260 },
91
- size: { width: 190, height: 120 },
92
- shapeId: 'panel',
85
+ },
86
+ { id: 'nested-peer', position: { x: 0, y: 0 }, size: { width: 50, height: 90 }, shapeId: 'default', parentId: 'layout-nested' },
87
+
88
+ {
89
+ id: 'layout-grid-deep',
90
+ position: { x: 640, y: 250 },
91
+ size: { width: 250, height: 180 },
92
+ shapeId: 'panel',
93
+ layout: {
94
+ mode: 'grid',
95
+ padding: { x: 12, y: 12 },
96
+ gap: 12,
97
+ align: 'center',
98
+ autoResize: 'grow-shrink',
99
+ gridTemplate: [4, 4, 4],
100
+ gridChildWidthResizeEnabled: true,
101
+ childMinWidth: 48,
102
+ childMinHeight: 60,
103
+ },
104
+ },
105
+ {
106
+ id: 'deep-grid-leaf-left',
107
+ position: { x: 0, y: 0 },
108
+ size: { width: 58, height: 72 },
109
+ shapeId: 'default',
110
+ parentId: 'layout-grid-deep',
111
+ },
112
+ {
113
+ id: 'deep-grid-nested-parent',
114
+ position: { x: 0, y: 0 },
115
+ size: { width: 108, height: 90 },
116
+ shapeId: 'default',
117
+ parentId: 'layout-grid-deep',
118
+ layout: {
119
+ mode: 'grid',
120
+ padding: { x: 8, y: 8 },
121
+ gap: 8,
122
+ align: 'center',
123
+ autoResize: 'grow-shrink',
124
+ gridTemplate: [6, 6],
125
+ gridChildWidthResizeEnabled: true,
126
+ childMinWidth: 18,
127
+ childMinHeight: 18,
128
+ },
129
+ },
130
+ {
131
+ id: 'deep-grid-leaf-right',
132
+ position: { x: 0, y: 0 },
133
+ size: { width: 52, height: 78 },
134
+ shapeId: 'default',
135
+ parentId: 'layout-grid-deep',
136
+ },
137
+ { id: 'deep-grid-a', position: { x: 0, y: 0 }, size: { width: 34, height: 24 }, shapeId: 'default', parentId: 'deep-grid-nested-parent' },
138
+ { id: 'deep-grid-b', position: { x: 0, y: 0 }, size: { width: 40, height: 28 }, shapeId: 'default', parentId: 'deep-grid-nested-parent' },
139
+ { id: 'deep-grid-c', position: { x: 0, y: 0 }, size: { width: 52, height: 24 }, shapeId: 'default', parentId: 'deep-grid-nested-parent' },
140
+ { id: 'deep-grid-d', position: { x: 0, y: 0 }, size: { width: 28, height: 30 }, shapeId: 'default', parentId: 'deep-grid-nested-parent' },
141
+
142
+ {
143
+ id: 'layout-lock-compare',
144
+ position: { x: 420, y: 410 },
145
+ size: { width: 240, height: 160 },
146
+ shapeId: 'panel',
147
+ layout: { mode: 'horizontal', padding: { x: 12, y: 12 }, gap: 12, align: 'start' },
148
+ },
149
+ {
150
+ id: 'deep-lock-parent',
151
+ position: { x: 0, y: 0 },
152
+ size: { width: 110, height: 92 },
153
+ shapeId: 'default',
154
+ parentId: 'layout-lock-compare',
155
+ childElementInteraction: lockComparisonChildren ? { movable: false } : undefined,
156
+ },
157
+ { id: 'deep-lock-child-a', position: { x: 12, y: 24 }, size: { width: 38, height: 24 }, shapeId: 'default', parentId: 'deep-lock-parent' },
158
+ { id: 'deep-lock-child-b', position: { x: 56, y: 54 }, size: { width: 42, height: 24 }, shapeId: 'default', parentId: 'deep-lock-parent' },
159
+ {
160
+ id: 'deep-free-parent',
161
+ position: { x: 0, y: 0 },
162
+ size: { width: 110, height: 92 },
163
+ shapeId: 'default',
164
+ parentId: 'layout-lock-compare',
165
+ },
166
+ { id: 'deep-free-child-a', position: { x: 12, y: 24 }, size: { width: 38, height: 24 }, shapeId: 'default', parentId: 'deep-free-parent' },
167
+ { id: 'deep-free-child-b', position: { x: 56, y: 54 }, size: { width: 42, height: 24 }, shapeId: 'default', parentId: 'deep-free-parent' },
168
+
169
+ {
170
+ id: 'layout-manual',
171
+ position: { x: 420, y: 260 },
172
+ size: { width: 190, height: 120 },
173
+ shapeId: 'panel',
93
174
  },
94
175
  { id: 'manual-a', position: { x: 12, y: 18 }, size: { width: 60, height: 30 }, shapeId: 'default', parentId: 'layout-manual' },
95
176
  { id: 'manual-b', position: { x: 90, y: 60 }, size: { width: 68, height: 32 }, shapeId: 'default', parentId: 'layout-manual' },
@@ -142,15 +223,53 @@ const createAutoLayoutState = (): DiagramState => ({
142
223
  },
143
224
  { id: 'label-nested-stack', content: 'nested stack', position: { x: 6, y: -16 }, ownerId: 'nested-stack' },
144
225
  { id: 'label-stack-a', content: 'stack-a', position: { x: 6, y: -14 }, ownerId: 'stack-a' },
145
- { id: 'label-stack-b', content: 'stack-b', position: { x: 6, y: -14 }, ownerId: 'stack-b' },
146
- { id: 'label-stack-overflow', content: 'stack-overflow', position: { x: 6, y: -14 }, ownerId: 'stack-overflow' },
147
- { id: 'label-nested-peer', content: 'nested-peer', position: { x: 6, y: -14 }, ownerId: 'nested-peer' },
148
-
149
- { id: 'label-layout-manual', content: 'Manual (no layout)', position: { x: 6, y: -18 }, ownerId: 'layout-manual' },
150
- { id: 'label-manual-a', content: 'manual-a', position: { x: 6, y: -14 }, ownerId: 'manual-a' },
151
- { id: 'label-manual-b', content: 'manual-b', position: { x: 6, y: -14 }, ownerId: 'manual-b' },
152
- ],
153
- });
226
+ { id: 'label-stack-b', content: 'stack-b', position: { x: 6, y: -14 }, ownerId: 'stack-b' },
227
+ { id: 'label-stack-overflow', content: 'stack-overflow', position: { x: 6, y: -14 }, ownerId: 'stack-overflow' },
228
+ { id: 'label-nested-peer', content: 'nested-peer', position: { x: 6, y: -14 }, ownerId: 'nested-peer' },
229
+
230
+ {
231
+ id: 'label-layout-grid-deep',
232
+ content: 'Outer grid: all direct children snap',
233
+ position: { x: 8, y: 6 },
234
+ ownerId: 'layout-grid-deep',
235
+ layout: { boundsMode: 'owner-width', wrap: 'word', overflow: 'clip', padding: 0 },
236
+ },
237
+ { id: 'label-deep-grid-leaf-left', content: 'leaf resize/reorder', position: { x: 6, y: -16 }, ownerId: 'deep-grid-leaf-left' },
238
+ { id: 'label-deep-grid-nested-parent', content: 'nested grid 12-unit child', position: { x: 6, y: -16 }, ownerId: 'deep-grid-nested-parent' },
239
+ { id: 'label-deep-grid-leaf-right', content: 'leaf resize/reorder', position: { x: 6, y: -16 }, ownerId: 'deep-grid-leaf-right' },
240
+ { id: 'label-deep-grid-a', content: 'grid-a', position: { x: 6, y: -14 }, ownerId: 'deep-grid-a' },
241
+ { id: 'label-deep-grid-b', content: 'grid-b', position: { x: 6, y: -14 }, ownerId: 'deep-grid-b' },
242
+ { id: 'label-deep-grid-c', content: 'grid-c', position: { x: 6, y: -14 }, ownerId: 'deep-grid-c' },
243
+ { id: 'label-deep-grid-d', content: 'grid-d', position: { x: 6, y: -14 }, ownerId: 'deep-grid-d' },
244
+ {
245
+ id: 'label-layout-lock-compare',
246
+ content: 'Child drag lock comparison',
247
+ position: { x: 8, y: 6 },
248
+ ownerId: 'layout-lock-compare',
249
+ layout: { boundsMode: 'owner-width', wrap: 'word', overflow: 'clip', padding: 0 },
250
+ },
251
+ {
252
+ id: 'label-deep-lock-parent',
253
+ content: lockComparisonChildren ? 'parent lock: on' : 'parent lock: off',
254
+ position: { x: 6, y: -16 },
255
+ ownerId: 'deep-lock-parent',
256
+ },
257
+ {
258
+ id: 'label-deep-lock-child-a',
259
+ content: lockComparisonChildren ? 'drag target (locked)' : 'drag target (unlocked)',
260
+ position: { x: 4, y: -14 },
261
+ ownerId: 'deep-lock-child-a',
262
+ },
263
+ { id: 'label-deep-lock-child-b', content: 'api move still works', position: { x: 4, y: -14 }, ownerId: 'deep-lock-child-b' },
264
+ { id: 'label-deep-free-parent', content: 'movable reference', position: { x: 6, y: -16 }, ownerId: 'deep-free-parent' },
265
+ { id: 'label-deep-free-child-a', content: 'drag target', position: { x: 4, y: -14 }, ownerId: 'deep-free-child-a' },
266
+ { id: 'label-deep-free-child-b', content: 'free sibling', position: { x: 4, y: -14 }, ownerId: 'deep-free-child-b' },
267
+
268
+ { id: 'label-layout-manual', content: 'Manual (no layout)', position: { x: 6, y: -18 }, ownerId: 'layout-manual' },
269
+ { id: 'label-manual-a', content: 'manual-a', position: { x: 6, y: -14 }, ownerId: 'manual-a' },
270
+ { id: 'label-manual-b', content: 'manual-b', position: { x: 6, y: -14 }, ownerId: 'manual-b' },
271
+ ],
272
+ });
154
273
 
155
274
  export const autoLayoutDemoConfig: DemoConfig = ({
156
275
  id: 'auto-layout',