orcasvn-react-diagrams 0.2.11 → 0.2.13

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 (55) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +15 -7
  3. package/ai/api-contract.json +90 -1
  4. package/ai/invariants.json +6 -2
  5. package/ai/manifest.json +1 -1
  6. package/dist/cjs/examples.js +916 -331
  7. package/dist/cjs/index.js +406 -97
  8. package/dist/cjs/types/api/createDiagramEditor.d.ts +3 -1
  9. package/dist/cjs/types/api/types.d.ts +64 -0
  10. package/dist/cjs/types/displaybox/demos/LinkPortCreationDemoTab.d.ts +3 -0
  11. package/dist/cjs/types/displaybox/demos/deletionEventsDemo.d.ts +2 -0
  12. package/dist/cjs/types/displaybox/demos/focusElementDemo.d.ts +4 -0
  13. package/dist/cjs/types/displaybox/demos/linkLabelsDemo.d.ts +4 -0
  14. package/dist/cjs/types/displaybox/types.d.ts +3 -0
  15. package/dist/cjs/types/displaybox/useDemoEditor.d.ts +3 -2
  16. package/dist/cjs/types/engine/DiagramEngine.d.ts +11 -1
  17. package/dist/cjs/types/engine/TextLayoutService.d.ts +1 -0
  18. package/dist/cjs/types/examples/index.d.ts +1 -1
  19. package/dist/cjs/types/renderer/konva/KonvaInteraction.d.ts +10 -1
  20. package/dist/esm/examples.js +916 -331
  21. package/dist/esm/examples.js.map +1 -1
  22. package/dist/esm/index.js +406 -97
  23. package/dist/esm/index.js.map +1 -1
  24. package/dist/esm/types/api/createDiagramEditor.d.ts +3 -1
  25. package/dist/esm/types/api/types.d.ts +64 -0
  26. package/dist/esm/types/displaybox/demos/LinkPortCreationDemoTab.d.ts +3 -0
  27. package/dist/esm/types/displaybox/demos/deletionEventsDemo.d.ts +2 -0
  28. package/dist/esm/types/displaybox/demos/focusElementDemo.d.ts +4 -0
  29. package/dist/esm/types/displaybox/demos/linkLabelsDemo.d.ts +4 -0
  30. package/dist/esm/types/displaybox/types.d.ts +3 -0
  31. package/dist/esm/types/displaybox/useDemoEditor.d.ts +3 -2
  32. package/dist/esm/types/engine/DiagramEngine.d.ts +11 -1
  33. package/dist/esm/types/engine/TextLayoutService.d.ts +1 -0
  34. package/dist/esm/types/examples/index.d.ts +1 -1
  35. package/dist/esm/types/renderer/konva/KonvaInteraction.d.ts +10 -1
  36. package/dist/examples.d.ts +55 -1
  37. package/dist/index.d.ts +67 -1
  38. package/docs/API_CONTRACT.md +88 -1
  39. package/docs/ARCHITECTURE.md +9 -7
  40. package/docs/CAPABILITIES.md +5 -0
  41. package/docs/COMMANDS_EVENTS.md +16 -6
  42. package/docs/DOCUMENTATION_WORKFLOW.md +5 -1
  43. package/docs/INTEGRATION_PLAYBOOK.md +8 -0
  44. package/docs/PORTING_CHECKLIST.md +4 -1
  45. package/docs/STATE_INVARIANTS.md +3 -0
  46. package/package.json +1 -1
  47. package/src/displaybox/demos/DeletionEventsDemoTab.tsx +167 -9
  48. package/src/displaybox/demos/LinkPortCreationDemoTab.tsx +98 -0
  49. package/src/displaybox/demos/deletionEventsDemo.ts +2 -2
  50. package/src/displaybox/demos/focusElementDemo.ts +91 -0
  51. package/src/displaybox/demos/index.tsx +124 -221
  52. package/src/displaybox/demos/linkLabelsDemo.ts +164 -0
  53. package/src/displaybox/demos/linkPortCreationDemo.ts +7 -7
  54. package/src/displaybox/types.ts +21 -11
  55. package/src/examples/index.ts +1 -0
@@ -26,6 +26,7 @@ Primary definitions:
26
26
  - `elementShapeHoverControls?: ElementShapeHoverControls`
27
27
  - `onElementShapeHoverControlInteraction?: (event: ElementShapeHoverControlInteractionEvent) => void`
28
28
  - `onElementShapeHoverControlActivated?: (event: ElementShapeHoverControlActivationEvent) => void` (deprecated callback contract)
29
+ - `onCreateLinkTargetPort?: (context: LinkTargetPortCreationContext) => Partial<LinkTargetPortDraft> | void`
29
30
  - `onChange?: (event: EngineChangeEvent) => void`
30
31
  - `onSelection?: (event: EngineSelectionEvent) => void`
31
32
  - `linkRouteRefreshPolicy?: LinkRouteRefreshPolicy`
@@ -104,6 +105,12 @@ Defaults:
104
105
  - non-finite values fall back to defaults
105
106
  - `minZoom`/`maxZoom` are clamped to positive values and normalized into ascending order
106
107
 
108
+ ### `FocusElementOptions`
109
+ - Optional `zoom?: number`
110
+ - Semantics:
111
+ - omitted `zoom` preserves the current viewport zoom
112
+ - provided `zoom` is clamped to the same normalized min/max range used by viewport fit helpers
113
+
107
114
  ### `TextInteractionPolicy`
108
115
  - Optional `movable?: boolean` (default behavior: `true`)
109
116
  - Optional `editable?: boolean` (default behavior: `true`)
@@ -138,12 +145,40 @@ Defaults:
138
145
  - `anchorCenter`: `true`
139
146
  - `orientToHostBorder`: `true`
140
147
 
148
+ ### `LinkTargetPortCreationPhase`
149
+ - `'preview' | 'commit'`
150
+
151
+ ### `LinkTargetPortDraft`
152
+ - Alias of `PortData` without `id` and `elementId`
153
+ - `position` uses target-element-local coordinates
154
+ - Returned overrides are normalized against the target element before placeholder render or commit
155
+
156
+ ### `LinkTargetPortCreationContext`
157
+ - Required: `phase`, `pointer`, `sourcePortId`, `sourceElementId`, `targetElementId`, `sourcePort`, `targetElement`, `defaultPort`
158
+ - `phase` indicates whether the callback is shaping the placeholder draft (`'preview'`) or the commit draft (`'commit'`)
159
+ - `pointer` is the current world-space pointer/drop coordinate
160
+ - `sourcePort` is a snapshot of the source port data used to derive the destination-port defaults
161
+ - `targetElement` is a snapshot of the target element receiving the created port
162
+ - `defaultPort` is the pre-normalization default draft before host overrides are applied
163
+
164
+ ### `LinkTargetPortCreationResult`
165
+ - `Partial<LinkTargetPortDraft> | void`
166
+
167
+ ### `LinkTargetPortCreationHandler`
168
+ - Signature: `(context: LinkTargetPortCreationContext) => LinkTargetPortCreationResult`
169
+ - Applies only to link-to-element completion flows
170
+ - Does not cancel creation; use `elementLinkConnecting.cancel()` for host veto
171
+
141
172
  ### `LinkData`
142
173
  - Required: `id`, `sourcePortId`, `targetPortId`, `points`
143
174
  - Optional: `routing`, `style`, `textIds`
144
175
  - Defaults at runtime/model:
145
176
  - `routing`: `'auto'`
146
177
  - `textIds`: `[]`
178
+ - Link-label contract:
179
+ - link labels are represented as ordinary `TextData`
180
+ - a link-owned label uses `TextData.ownerId = link.id`
181
+ - `LinkData.textIds` is the owner-side index only; there is no separate `LinkData.label` or `LinkData.labels` field
147
182
 
148
183
  ### `LinkRouteRefreshPolicy`
149
184
  - Optional `mode?: 'mutation-only' | 'redraw-two-endpoint-change'` (default: `'mutation-only'`)
@@ -168,12 +203,47 @@ Defaults:
168
203
  - `beforeRows` / `afterRows`: arrays of `{ childIds: string[] }` snapshots describing derived row membership before and after the topology change
169
204
  - Emitted only when enabled grid-child width resizing causes a real row-topology change
170
205
 
206
+ ### `ElementDoubleClickEvent`
207
+ - Required: `elementId`, `pointer`, `isMulti`
208
+ - Emitted for eligible element-body double click / double tap
209
+ - Observational only; does not cancel or replace the normal click-selection flow
210
+
211
+ ### `DeletionTrigger`
212
+ - `'direct' | 'selection' | 'cascade'`
213
+
214
+ ### `DeletionRootRef`
215
+ - Required: `entity`, `id`
216
+ - `entity`: `'element' | 'port' | 'link' | 'text'`
217
+ - `id` is the original requested delete root for the active delete pass
218
+
219
+ ### `ElementDeletingEvent`
220
+ - Required: `elementId`, `trigger`, `root`, `cancel`, `cancelled`
221
+ - Emitted before an element deletion commits, including descendant element cascades
222
+
223
+ ### `PortDeletingEvent`
224
+ - Required: `portId`, `elementId`, `trigger`, `root`, `cancel`, `cancelled`
225
+ - Emitted before a port deletion commits, including cascades caused by parent element removal
226
+
227
+ ### `LinkDeletingEvent`
228
+ - Required: `linkId`, `sourcePortId`, `targetPortId`, `trigger`, `root`, `cancel`, `cancelled`
229
+ - Emitted before a link deletion commits, including cascades caused by element or port removal
230
+
231
+ ### `TextDeletingEvent`
232
+ - Required: `textId`, `trigger`, `root`, `cancel`, `cancelled`
233
+ - Optional: `ownerId?: string | null`
234
+ - Emitted before a text deletion commits, including cascades caused by owner deletion
235
+
171
236
  ### `TextData`
172
237
  - Required: `id`, `content`, `position`
173
238
  - Optional: `size`, `style`, `ownerId`, `layout`, `interaction`, `displayContent`, `displayOffset`, `displayClipSize`
174
239
  - Defaults at runtime/model:
175
240
  - `ownerId`: `null`
176
241
  - `displayContent`: mirrors `content` at creation
242
+ - Ownership semantics:
243
+ - element-owned text stores owner-relative local position
244
+ - port-owned text stores owner-relative local position
245
+ - link-owned text stores local offset from the owning link's current geometric midpoint
246
+ - when a link reroutes or endpoints move, link-owned text keeps the same stored local offset and resolves a new world position from the recomputed midpoint
177
247
 
178
248
  ### `TextLayout`
179
249
  - Optional `boundsMode?: 'owner-width' | 'owner-box' | 'fixed'`
@@ -217,6 +287,7 @@ Defaults:
217
287
  - `completeLinkToElement(targetElementId, pointer)`
218
288
  - `cancelLink()`
219
289
  - `zoomToFitElements(options?)`
290
+ - `focusElement(elementId, options?)`
220
291
  - `exportImage(options?)`
221
292
  - `resize(width, height)`
222
293
  - `setElementShapeHoverControls(controls?)`
@@ -256,13 +327,18 @@ Defaults:
256
327
  - `portMoved: PortMovedEvent`
257
328
  - `portSelected: PortSelectedEvent`
258
329
  - `elementClick: ElementPointerEvent`
330
+ - `elementDoubleClick: ElementDoubleClickEvent`
259
331
  - `elementDragged: ElementDropEvent`
260
332
  - `elementMoved: ElementMovedEvent`
261
333
  - `elementResized: ElementResizedEvent`
262
334
  - `gridLayoutChanged: GridLayoutChangedEvent`
335
+ - `elementDeleting: ElementDeletingEvent`
263
336
  - `elementDeleted: ElementDeletedEvent`
337
+ - `portDeleting: PortDeletingEvent`
264
338
  - `portDeleted: PortDeletedEvent`
339
+ - `linkDeleting: LinkDeletingEvent`
265
340
  - `linkDeleted: LinkDeletedEvent`
341
+ - `textDeleting: TextDeletingEvent`
266
342
  - `textDeleted: TextDeletedEvent`
267
343
  - `textUpdated: TextUpdatedEvent`
268
344
  - `elementSelected: ElementSelectedEvent`
@@ -278,14 +354,25 @@ Defaults:
278
354
  - `zoomToFitElements` computes bounds from `elements[]` only, ignores ports/links/texts for fit expansion, and no-ops when no elements exist.
279
355
  - `zoomToFitElements` ignores hidden elements when computing fit bounds.
280
356
  - `zoomToFitElements` applies its result through `setViewport`, so hosts observe the standard viewport patch/change path.
357
+ - `focusElement(elementId, options?)` centers the target element bounds in the current editor viewport and preserves the current zoom unless `options.zoom` is provided.
358
+ - `focusElement(...)` no-ops when the target id does not resolve to an element world position.
281
359
  - `exportImage({ fitToContent })` derives crop bounds from visible scene entities only.
282
360
  - Built-in empty-paper panning now starts from plain primary-button drag and no longer requires `panKey`; marquee selection uses `Shift + primary drag`.
361
+ - `elementDoubleClick` is observational; normal click selection still runs, and text double click keeps built-in text-edit precedence instead of also emitting `elementDoubleClick`.
283
362
  - `clientToWorld` uses: `world = (client - containerRect - pan) / zoom`, with zoom fallback to `1` when zoom is `0`.
284
363
  - `rerouteLinks(ids)` skips manual links unless `options.includeManual === true`.
285
364
  - `linkRouteRefreshPolicy.mode` defaults to `'mutation-only'`; `'redraw-two-endpoint-change'` enables redraw-cycle reroute checks for links whose two endpoints changed.
286
365
  - `updateLinkPoints` always marks the link as manual routing.
287
366
  - `linkColorPoolPolicy` is opt-in; it applies to newly created links when enabled, and it may also fill loaded links with missing strokes when `assignOnLoadWhenMissingStroke` is enabled. Explicit non-empty `style.stroke` values are preserved.
367
+ - `onCreateLinkTargetPort` applies only when a link completes onto an element body; direct port-to-port linking and direct `addPortToElement(...)` behavior are unchanged.
368
+ - `onCreateLinkTargetPort` runs during placeholder preview and again during commit, and both phases use the same post-callback target-port normalization path.
369
+ - `onCreateLinkTargetPort` shapes the destination-port draft only; host cancellation remains exclusively on `elementLinkConnecting`.
370
+ - `completeLinkToElement(...)` uses the same target-port callback and cancellation lifecycle as pointer-driven link drag.
371
+ - Link labels use the existing text lifecycle and event surface: `addText(...)`, `moveTextTo(...)`, `updateText(...)`, `removeText(...)`, `textUpdated`, and `textDeleted`.
372
+ - Link-owned text defaults to unconstrained layout unless `TextLayout.boundsMode` is explicitly set; element-only owner-width defaults do not apply to link labels.
288
373
  - `gridLayoutChanged` is additive and does not replace standard `change` / `elementResized` flows.
374
+ - Deletion mutators emit cancellable `elementDeleting` / `portDeleting` / `linkDeleting` / `textDeleting` before commit and matching `...Deleted` events after successful removal.
375
+ - Canceling any dependent `...Deleting` event aborts the current root deletion request; `deleteSelection()` still continues evaluating unrelated selected roots.
289
376
  - `ElementData.visible = false` hides only the element body from built-in renderer sync, hit testing, marquee selection, persisted selection, and fit/export helpers.
290
377
  - Descendant element visibility is not suppressed by hidden ancestors; direct-owned parent ports/texts still follow the hidden parent, and links remain visible whenever both endpoint ports are visible.
291
378
  - `ElementData.selectable = false` blocks built-in element-body selection while still allowing visible owned ports to participate in their normal interaction flows.
@@ -294,7 +381,7 @@ Defaults:
294
381
  - `TextData.interaction.movable = false` blocks built-in drag only; selection still works and programmatic movement remains allowed.
295
382
  - `TextData.interaction.editable = false` blocks built-in textarea editing only; selection still works and programmatic text updates remain allowed.
296
383
  - Text layout/overflow resolution updates `displayContent` and emits `textUpdated` when `updateText` is called.
297
- - Deletion mutators emit cascading `portDeleted`/`linkDeleted`/`textDeleted` for removed dependent entities.
384
+ - Successful cascade removal still emits the matching dependent `...Deleted` events for removed entities.
298
385
 
299
386
  ## Verification References
300
387
  - `src/api/__tests__/createDiagramEditor.test.ts`
@@ -3,8 +3,8 @@
3
3
  > Sections: [Module Boundaries](#module-boundaries) | [Runtime Flow](#runtime-flow) | [Integration Boundary](#integration-boundary) | [Key Sources](#key-sources)
4
4
 
5
5
  ## Module Boundaries
6
- - `src/api/`: public entry points and type contracts.
7
- - `src/engine/`: command execution, event bus, selection, viewport, routing/snap integration.
6
+ - `src/api/`: public entry points, type contracts, and editor-only helpers such as `zoomToFitElements(...)` and `focusElement(...)`.
7
+ - `src/engine/`: command execution, event bus, selection, viewport, routing/snap integration, and delete-lifecycle orchestration.
8
8
  - `src/engine/`: `DiagramEngine` orchestration plus extracted services (`TextLayoutService`, `AutoLayoutService`, `LinkRoutingService`, `MutationPipeline`).
9
9
  - `src/models/`: in-memory state graph for elements/ports/links/texts.
10
10
  - `src/renderer/`: renderer interface and shape registry contracts.
@@ -14,15 +14,17 @@
14
14
  - `src/utils/`: geometry, patch helpers, guards, ids.
15
15
 
16
16
  ## Runtime Flow
17
- 1. Host calls API mutator.
18
- 2. Engine executes command(s) against model.
19
- 3. Engine emits patches + latest state via `change`.
20
- 4. Scheduler requests render.
21
- 5. Renderer syncs model to view nodes.
17
+ 1. Host calls API mutator or editor helper.
18
+ 2. Interaction or API helper resolves target intent and any required lifecycle hooks.
19
+ 3. Engine executes command(s) against model or applies viewport state updates.
20
+ 4. Engine emits patches + latest state via `change`.
21
+ 5. Scheduler requests render.
22
+ 6. Renderer syncs model to view nodes.
22
23
 
23
24
  ## Integration Boundary
24
25
  - Engine core does not require Konva if you use `createDiagramEngine` with a custom `Renderer`.
25
26
  - `createDiagramEditor` wires Konva stage, hit testing, and interaction defaults.
27
+ - The Konva-backed editor surface also owns editor-only navigation helpers such as `focusElement(...)`.
26
28
 
27
29
  ## Key Sources
28
30
  - `src/api/createDiagramEditor.ts`
@@ -18,7 +18,12 @@
18
18
  - Optional grid-child width topology editing with `gridLayoutChanged` host events.
19
19
  - Parent-owned direct-child drag suppression through `ElementData.childElementInteraction`.
20
20
  - Element-level visibility/selectability policy through `ElementData.visible` and `ElementData.selectable`, with element-body-only hiding, direct-owned port/text suppression, and visible descendants that keep their own links/rendering behavior.
21
+ - Host-callable viewport focus through `focusElement(elementId, options?)`.
21
22
  - Element shape hover controls with edge/vertex/midpoint targets and interaction callbacks.
23
+ - Observational `elementDoubleClick` event for element-body double click / double tap.
24
+ - Cancellable delete lifecycle events (`elementDeleting`, `portDeleting`, `linkDeleting`, `textDeleting`) before direct, selection, and cascade removals commit.
25
+ - Link labels through link-owned `TextData`, including midpoint-relative attachment that follows reroute and endpoint movement.
26
+ - Optional host callback for shaping auto-created destination ports during link-to-element preview and commit.
22
27
  - Optional random link color assignment from configurable pools during link creation, plus opt-in load-time fill for links missing `style.stroke`.
23
28
  - Per-text interaction policy for suppressing built-in text drag and/or built-in text editing.
24
29
  - Overlay shapes for external drag ghosting and previews.
@@ -6,7 +6,7 @@
6
6
  - Engine mutators route through command factories in `src/engine/EngineCommands.ts`.
7
7
  - Each command applies model mutation and returns one or more patches.
8
8
  - `emitChange` publishes `{ patches, state }` on `change`.
9
- - Editor helpers such as `zoomToFitElements(options?)` compose existing engine commands rather than introducing a separate patch type.
9
+ - Editor helpers such as `zoomToFitElements(options?)` and `focusElement(elementId, options?)` compose existing viewport updates rather than introducing separate patch types.
10
10
 
11
11
  ## Patch Entities
12
12
  - `element`
@@ -20,7 +20,7 @@
20
20
  ## Mutation To Events Mapping
21
21
  - `add/move/resize/remove element`
22
22
  - Always `change`
23
- - Plus `elementMoved`, `elementResized`, or `elementDeleted` when applicable
23
+ - Plus `elementMoved`, `elementResized`, `elementDeleting`, or `elementDeleted` when applicable
24
24
  - `resize element` with enabled grid-child width topology editing
25
25
  - Standard `change`
26
26
  - Standard `elementResized` when size changes
@@ -28,22 +28,26 @@
28
28
  - `add/move/remove port`
29
29
  - Always `change`
30
30
  - Plus `portMoved` on effective movement
31
- - Plus `portDeleted` on removal (including cascade removal)
31
+ - Plus `portDeleting` before removal and `portDeleted` on successful removal (including cascade removal)
32
32
  - `add/update/remove link`
33
33
  - Always `change`
34
- - Plus `linkDeleted` on removal (including cascade removal)
34
+ - Plus `linkDeleting` before removal and `linkDeleted` on successful removal (including cascade removal)
35
35
  - `add/update/move/remove text`
36
36
  - Always `change`
37
37
  - Plus `textUpdated` when `updateText` resolves display layout
38
- - Plus `textDeleted` on removal (including cascade removal)
38
+ - Plus `textDeleting` before removal and `textDeleted` on successful removal (including cascade removal)
39
39
  - `setSelection/toggleSelection/deleteSelection`
40
40
  - `selection` plus derived selected entity events
41
+ - `deleteSelection` evaluates each selected root independently, so a vetoed root can be skipped while unrelated roots still delete
41
42
  - hidden element bodies, direct-owned hidden artifacts, and `selectable:false` element IDs are filtered out before selection persists/emits
42
43
  - `setViewport`
43
44
  - `change` with viewport patch (`render: false`)
44
45
  - `zoomToFitElements`
45
46
  - delegates to `setViewport` when at least one visible element exists
46
47
  - emits the same viewport `change` patch path
48
+ - `focusElement`
49
+ - delegates to `setViewport` when the target element resolves
50
+ - preserves the current zoom unless `options.zoom` is supplied
47
51
  - `setRouting/setSnapping/registerShape`
48
52
  - `config` events
49
53
 
@@ -53,19 +57,25 @@
53
57
  - legacy `element-drop`
54
58
  - `elementDragged`
55
59
  - Click paper: `paperClick`
60
+ - Double click/tap element body:
61
+ - `elementDoubleClick`
56
62
  - Linking:
57
63
  - `elementLinkStarted`
64
+ - optional `onCreateLinkTargetPort(...)` preview/commit shaping for link-to-element completion
58
65
  - `elementLinkConnecting` (cancelable via `cancel()`)
59
66
  - `elementLinkEnded`
60
67
 
61
68
  ## Preconditions
62
- - Events are emitted only after engine state transition logic.
69
+ - Events are emitted only after engine state transition logic resolves a valid action.
70
+ - Cancellable `...Deleting` events emit before the corresponding mutation commits.
63
71
  - Derived events require selected IDs to resolve to known entity types.
64
72
 
65
73
  ## Failure Modes
66
74
  - Unknown IDs do not emit entity-specific movement/selection events.
67
75
  - Canceled link creation emits `elementLinkEnded` with `cancelled=true` and no link creation.
76
+ - `focusElement(...)` emits no viewport change when the target element id does not resolve.
68
77
  - Built-in element-body selection emits no persisted element selection when the target element is hidden or sets `selectable = false`.
78
+ - Text double click continues text editing precedence and does not also emit `elementDoubleClick`.
69
79
  - Built-in drag on a direct child element emits no move/drop mutation when its parent sets `childElementInteraction.movable = false`.
70
80
  - `textUpdated` emits only when target text exists; missing IDs remain no-op.
71
81
 
@@ -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-06-20
62
+ - Last updated: 2026-07-07
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)
@@ -91,3 +91,7 @@ When resuming later:
91
91
  - 2026-05-27: Completed release-doc pass for `v0.2.10`; refreshed release highlights, local-body visibility semantics, state/event integration notes, and machine-readable contract metadata.
92
92
  - 2026-06-20: Completed release-doc pass for `v0.2.11`; refreshed release highlights, link color pool load-time fill contract text, integration guidance, and machine-readable contract metadata.
93
93
  - 2026-06-20: Ran release validations: `npm run typecheck`, `npm test -- --watchAll=false`, `npm run build`, `npm run rollup-build-lib`, and `npm pack --dry-run`.
94
+ - 2026-06-26: Completed release-doc pass for `v0.2.12`; refreshed release highlights, link-created target-port callback contract text, integration guidance, and machine-readable contract metadata.
95
+ - 2026-06-26: Ran release validations: `npm run typecheck`, `npm test -- --watchAll=false`, `npm run build`, `npm run rollup-build-lib`, and `npm pack --dry-run`.
96
+ - 2026-07-07: Completed release-doc pass for `v0.2.13`; refreshed release highlights, focus/navigation and delete-lifecycle API/event docs, and machine-readable contract metadata.
97
+ - 2026-07-07: Ran release validations: `npm run typecheck`, `npm test -- --watchAll=false`, `npm run build`, `npm run rollup-build-lib`, and `npm pack --dry-run`.
@@ -21,6 +21,10 @@ Embed this library into another diagram host while preserving deterministic stat
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
22
  9. Set `ElementData.visible` and `ElementData.selectable` when hosts need hidden-but-retained nodes or visible-but-nonselectable element bodies; hidden elements hide only their own body while direct-owned ports/texts still follow the hidden parent and visible descendants remain available to built-in view helpers.
23
23
  10. Set `linkColorPoolPolicy` when hosts want random link colors on creation and optional load-time backfill for loaded links whose `style.stroke` is missing or blank.
24
+ 11. Create link labels with ordinary `TextData` by setting `ownerId` to the target link id; the stored `position` is midpoint-relative for link-owned text rather than a free world-space coordinate.
25
+ 12. Set `onCreateLinkTargetPort` when hosts need to customize the auto-created destination port for link-to-element drops; keep using `elementLinkConnecting` when the host needs to cancel the drop entirely.
26
+ 13. Use `focusElement(elementId, options?)` when the host needs to center one editor element in response to navigation, search, or external selection.
27
+ 14. Subscribe to `elementDoubleClick` and cancellable `...Deleting` events when the host needs double-click intent or pre-delete confirmation/guard logic.
24
28
 
25
29
  ## Path B: Engine-Only Adapter
26
30
  1. Implement `Renderer`:
@@ -37,10 +41,14 @@ Embed this library into another diagram host while preserving deterministic stat
37
41
  - host port/anchor -> `PortData`
38
42
  - host edge -> `LinkData`
39
43
  - host label -> `TextData`
44
+ - host edge label -> `TextData` with `ownerId = link.id`
40
45
  - Event mapping:
41
46
  - host click -> `setSelection` / `emitElementClick`-equivalent behavior
47
+ - host double click / double tap -> `elementDoubleClick`
42
48
  - host drag end -> `emitElementDrop`-equivalent payload
43
49
  - host link gesture -> `elementLinkStarted/Connecting/Ended` lifecycle
50
+ - optional host target-port shaping -> `onCreateLinkTargetPort(...)` for link-to-element preview/commit only
51
+ - host delete confirmation / veto surface -> cancellable `elementDeleting` / `portDeleting` / `linkDeleting` / `textDeleting`
44
52
 
45
53
  ## Behavioral Checklist Per Integration Step
46
54
  - Preconditions
@@ -32,14 +32,17 @@
32
32
  - Ensure host can emulate:
33
33
  - `elementDrop` + legacy `element-drop`
34
34
  - `paperClick`
35
+ - `elementDoubleClick`
35
36
  - `elementLinkStarted/Connecting/Ended`
36
37
  - `portMouseDown/portMouseUp`
37
- - deletion and text lifecycle events (`portDeleted`, `linkDeleted`, `textDeleted`, `textUpdated`)
38
+ - cancellable delete lifecycle events (`elementDeleting`, `portDeleting`, `linkDeleting`, `textDeleting`)
39
+ - post-delete and text lifecycle events (`elementDeleted`, `portDeleted`, `linkDeleted`, `textDeleted`, `textUpdated`)
38
40
 
39
41
  ## 6. Verify Cascading Deletes
40
42
  - Deleting element should remove descendant ports, connected links, owned texts.
41
43
  - Deleting port should remove connected links and owned texts.
42
44
  - Deleting link should remove owned texts.
45
+ - A veto from any `...Deleting` handler should abort that root delete request before mutation commit.
43
46
 
44
47
  ## 7. Run Fixture-Based Validation
45
48
  - Load fixtures from `fixtures/*.json`.
@@ -39,12 +39,15 @@
39
39
  - Manual routing preserves interior bends on endpoint movement.
40
40
  - Links with unresolved source/target world positions are skipped by reroute/update paths.
41
41
  - Link visible-state requires both endpoint ports to remain visible.
42
+ - Deleting a link cascades to any link-owned text labels.
42
43
 
43
44
  ## Text
44
45
  - `ownerId` can reference element, port, or link.
45
46
  - Owned text position is stored owner-relative.
47
+ - Link-owned text stores owner-relative position as a local offset from the owning link midpoint.
46
48
  - Text with missing owner behaves as standalone world-position text.
47
49
  - Owned text visible-state follows its owner visibility when the owner resolves to an element, port, or link.
50
+ - Link-owned text recomputes world position from the current link midpoint plus stored local offset.
48
51
  - Text interaction policy is persisted separately from style/layout metadata.
49
52
  - `interaction.movable = false` suppresses built-in drag without blocking selection or programmatic movement.
50
53
  - `interaction.editable = false` suppresses built-in editing without blocking selection or programmatic content updates.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orcasvn-react-diagrams",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "dependencies": {
5
5
  "eventemitter3": "^5.0.1",
6
6
  "flatten-js": "^0.6.9",
@@ -1,8 +1,13 @@
1
1
  import React, { useEffect, useMemo, useState } from 'react';
2
2
  import type {
3
+ ElementDeletingEvent,
4
+ ElementDoubleClickEvent,
3
5
  ElementDeletedEvent,
6
+ LinkDeletingEvent,
4
7
  LinkDeletedEvent,
8
+ PortDeletingEvent,
5
9
  PortDeletedEvent,
10
+ TextDeletingEvent,
6
11
  TextDeletedEvent,
7
12
  } from '../../api';
8
13
  import DisplayBoxControls from '../DisplayBoxControls';
@@ -14,17 +19,41 @@ import type { DemoActionHelpers } from '../types';
14
19
  import { deletionEventsDemoConfig } from './deletionEventsDemo';
15
20
 
16
21
  type EventPayloads = {
22
+ elementDeleting: ElementDeletingEvent | null;
17
23
  elementDeleted: ElementDeletedEvent | null;
24
+ portDeleting: PortDeletingEvent | null;
18
25
  portDeleted: PortDeletedEvent | null;
26
+ linkDeleting: LinkDeletingEvent | null;
19
27
  linkDeleted: LinkDeletedEvent | null;
28
+ textDeleting: TextDeletingEvent | null;
20
29
  textDeleted: TextDeletedEvent | null;
30
+ elementDoubleClick: ElementDoubleClickEvent | null;
21
31
  };
22
32
 
23
33
  const initialPayloads: EventPayloads = {
34
+ elementDeleting: null,
24
35
  elementDeleted: null,
36
+ portDeleting: null,
25
37
  portDeleted: null,
38
+ linkDeleting: null,
26
39
  linkDeleted: null,
40
+ textDeleting: null,
27
41
  textDeleted: null,
42
+ elementDoubleClick: null,
43
+ };
44
+
45
+ type CancelFlags = {
46
+ element: boolean;
47
+ port: boolean;
48
+ link: boolean;
49
+ text: boolean;
50
+ };
51
+
52
+ const initialCancelFlags: CancelFlags = {
53
+ element: false,
54
+ port: false,
55
+ link: false,
56
+ text: false,
28
57
  };
29
58
 
30
59
  const eventBoxStyle: React.CSSProperties = {
@@ -61,21 +90,79 @@ const DeletionEventsDemo = () => {
61
90
  });
62
91
 
63
92
  const [payloads, setPayloads] = useState<EventPayloads>(initialPayloads);
93
+ const [cancelFlags, setCancelFlags] = useState<CancelFlags>(initialCancelFlags);
94
+ const [eventLog, setEventLog] = useState<string[]>([]);
95
+
96
+ const appendLog = React.useCallback((eventName: string, payload: unknown) => {
97
+ setEventLog((prev) => [
98
+ `${eventName}: ${JSON.stringify(payload)}`,
99
+ ...prev,
100
+ ].slice(0, 18));
101
+ }, []);
64
102
 
65
103
  useEffect(() => {
66
104
  const editor = editorRef.current;
67
105
  if (!editor) return undefined;
68
106
  setPayloads(initialPayloads);
107
+ setEventLog([]);
69
108
  const unsubs = [
70
- editor.on('elementDeleted', (payload) => setPayloads((prev) => ({ ...prev, elementDeleted: payload }))),
71
- editor.on('portDeleted', (payload) => setPayloads((prev) => ({ ...prev, portDeleted: payload }))),
72
- editor.on('linkDeleted', (payload) => setPayloads((prev) => ({ ...prev, linkDeleted: payload }))),
73
- editor.on('textDeleted', (payload) => setPayloads((prev) => ({ ...prev, textDeleted: payload }))),
109
+ editor.on('elementDeleting', (payload) => {
110
+ if (cancelFlags.element) {
111
+ payload.cancel();
112
+ }
113
+ setPayloads((prev) => ({ ...prev, elementDeleting: payload }));
114
+ appendLog('elementDeleting', payload);
115
+ }),
116
+ editor.on('elementDeleted', (payload) => {
117
+ setPayloads((prev) => ({ ...prev, elementDeleted: payload }));
118
+ appendLog('elementDeleted', payload);
119
+ }),
120
+ editor.on('portDeleting', (payload) => {
121
+ if (cancelFlags.port) {
122
+ payload.cancel();
123
+ }
124
+ setPayloads((prev) => ({ ...prev, portDeleting: payload }));
125
+ appendLog('portDeleting', payload);
126
+ }),
127
+ editor.on('portDeleted', (payload) => {
128
+ setPayloads((prev) => ({ ...prev, portDeleted: payload }));
129
+ appendLog('portDeleted', payload);
130
+ }),
131
+ editor.on('linkDeleting', (payload) => {
132
+ if (cancelFlags.link) {
133
+ payload.cancel();
134
+ }
135
+ setPayloads((prev) => ({ ...prev, linkDeleting: payload }));
136
+ appendLog('linkDeleting', payload);
137
+ }),
138
+ editor.on('linkDeleted', (payload) => {
139
+ setPayloads((prev) => ({ ...prev, linkDeleted: payload }));
140
+ appendLog('linkDeleted', payload);
141
+ }),
142
+ editor.on('textDeleting', (payload) => {
143
+ if (cancelFlags.text) {
144
+ payload.cancel();
145
+ }
146
+ setPayloads((prev) => ({ ...prev, textDeleting: payload }));
147
+ appendLog('textDeleting', payload);
148
+ }),
149
+ editor.on('textDeleted', (payload) => {
150
+ setPayloads((prev) => ({ ...prev, textDeleted: payload }));
151
+ appendLog('textDeleted', payload);
152
+ }),
153
+ editor.on('elementDoubleClick', (payload) => {
154
+ setPayloads((prev) => ({ ...prev, elementDoubleClick: payload }));
155
+ appendLog('elementDoubleClick', payload);
156
+ }),
74
157
  ];
75
158
  return () => {
76
- unsubs.forEach((unsub) => unsub());
159
+ unsubs.forEach((unsub) => {
160
+ if (typeof unsub === 'function') {
161
+ unsub();
162
+ }
163
+ });
77
164
  };
78
- }, [editorRef]);
165
+ }, [appendLog, cancelFlags, editorRef]);
79
166
 
80
167
  const hasPort = (diagramState?.ports ?? []).some((port) => port.id === 'delete-source-free-port');
81
168
  const hasLink = (diagramState?.links ?? []).some((link) => link.id === 'delete-link');
@@ -87,8 +174,10 @@ const DeletionEventsDemo = () => {
87
174
  <div style={{ marginBottom: 12 }}>
88
175
  <h2 style={{ marginTop: 0, marginBottom: 4 }}>{demo.title}</h2>
89
176
  <p style={{ marginTop: 0 }}>
90
- Use direct delete buttons for port/link/text, then delete <code>delete-source</code> to trigger cascade
91
- removals. Latest payloads are shown below for integration debugging.
177
+ Use the direct delete buttons for port/link/text, then delete <code>delete-source</code> to trigger
178
+ cascades. Turn cancellation toggles on to veto deletes before commit, double click an element body to log
179
+ <code> elementDoubleClick</code>, and use the batch-selection helper before pressing Delete to verify
180
+ partial success.
92
181
  </p>
93
182
  </div>
94
183
  <DisplayBoxControls
@@ -123,24 +212,93 @@ const DeletionEventsDemo = () => {
123
212
  <button type="button" onClick={() => editorRef.current?.removeElement('delete-source')} disabled={!hasCascadeElement}>
124
213
  Delete Element (Cascade)
125
214
  </button>
215
+ <button
216
+ type="button"
217
+ onClick={() => editorRef.current?.setSelection(['delete-free-text', 'delete-target'])}
218
+ disabled={!hasText}
219
+ >
220
+ Prepare Batch Delete Selection
221
+ </button>
222
+ </div>
223
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
224
+ <label>
225
+ <input
226
+ type="checkbox"
227
+ checked={cancelFlags.element}
228
+ onChange={(event) => setCancelFlags((prev) => ({ ...prev, element: event.target.checked }))}
229
+ />
230
+ {' '}
231
+ Cancel elementDeleting
232
+ </label>
233
+ <label>
234
+ <input
235
+ type="checkbox"
236
+ checked={cancelFlags.port}
237
+ onChange={(event) => setCancelFlags((prev) => ({ ...prev, port: event.target.checked }))}
238
+ />
239
+ {' '}
240
+ Cancel portDeleting
241
+ </label>
242
+ <label>
243
+ <input
244
+ type="checkbox"
245
+ checked={cancelFlags.link}
246
+ onChange={(event) => setCancelFlags((prev) => ({ ...prev, link: event.target.checked }))}
247
+ />
248
+ {' '}
249
+ Cancel linkDeleting
250
+ </label>
251
+ <label>
252
+ <input
253
+ type="checkbox"
254
+ checked={cancelFlags.text}
255
+ onChange={(event) => setCancelFlags((prev) => ({ ...prev, text: event.target.checked }))}
256
+ />
257
+ {' '}
258
+ Cancel textDeleting
259
+ </label>
126
260
  </div>
127
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 12 }}>
261
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12 }}>
262
+ <div>
263
+ <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>elementDeleting</label>
264
+ <textarea readOnly value={payloads.elementDeleting ? JSON.stringify(payloads.elementDeleting, null, 2) : ''} style={eventBoxStyle} />
265
+ </div>
128
266
  <div>
129
267
  <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>elementDeleted</label>
130
268
  <textarea readOnly value={payloads.elementDeleted ? JSON.stringify(payloads.elementDeleted, null, 2) : ''} style={eventBoxStyle} />
131
269
  </div>
270
+ <div>
271
+ <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>portDeleting</label>
272
+ <textarea readOnly value={payloads.portDeleting ? JSON.stringify(payloads.portDeleting, null, 2) : ''} style={eventBoxStyle} />
273
+ </div>
132
274
  <div>
133
275
  <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>portDeleted</label>
134
276
  <textarea readOnly value={payloads.portDeleted ? JSON.stringify(payloads.portDeleted, null, 2) : ''} style={eventBoxStyle} />
135
277
  </div>
278
+ <div>
279
+ <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>linkDeleting</label>
280
+ <textarea readOnly value={payloads.linkDeleting ? JSON.stringify(payloads.linkDeleting, null, 2) : ''} style={eventBoxStyle} />
281
+ </div>
136
282
  <div>
137
283
  <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>linkDeleted</label>
138
284
  <textarea readOnly value={payloads.linkDeleted ? JSON.stringify(payloads.linkDeleted, null, 2) : ''} style={eventBoxStyle} />
139
285
  </div>
286
+ <div>
287
+ <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>textDeleting</label>
288
+ <textarea readOnly value={payloads.textDeleting ? JSON.stringify(payloads.textDeleting, null, 2) : ''} style={eventBoxStyle} />
289
+ </div>
140
290
  <div>
141
291
  <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>textDeleted</label>
142
292
  <textarea readOnly value={payloads.textDeleted ? JSON.stringify(payloads.textDeleted, null, 2) : ''} style={eventBoxStyle} />
143
293
  </div>
294
+ <div>
295
+ <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>elementDoubleClick</label>
296
+ <textarea readOnly value={payloads.elementDoubleClick ? JSON.stringify(payloads.elementDoubleClick, null, 2) : ''} style={eventBoxStyle} />
297
+ </div>
298
+ </div>
299
+ <div>
300
+ <label style={{ display: 'block', fontWeight: 600, marginBottom: 6 }}>Event Log</label>
301
+ <textarea readOnly value={eventLog.join('\n')} style={{ ...eventBoxStyle, minHeight: 180 }} />
144
302
  </div>
145
303
  </div>
146
304
  <DisplayBoxStage containerRef={containerRef} />