orcasvn-react-diagrams 0.2.12 → 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 (35) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +15 -11
  3. package/ai/api-contract.json +47 -0
  4. package/ai/invariants.json +6 -2
  5. package/ai/manifest.json +1 -1
  6. package/dist/cjs/examples.js +427 -48
  7. package/dist/cjs/index.js +226 -36
  8. package/dist/cjs/types/api/createDiagramEditor.d.ts +2 -1
  9. package/dist/cjs/types/api/types.d.ts +50 -0
  10. package/dist/cjs/types/displaybox/demos/deletionEventsDemo.d.ts +2 -0
  11. package/dist/cjs/types/displaybox/demos/focusElementDemo.d.ts +4 -0
  12. package/dist/cjs/types/engine/DiagramEngine.d.ts +9 -0
  13. package/dist/esm/examples.js +427 -48
  14. package/dist/esm/examples.js.map +1 -1
  15. package/dist/esm/index.js +226 -36
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/types/api/createDiagramEditor.d.ts +2 -1
  18. package/dist/esm/types/api/types.d.ts +50 -0
  19. package/dist/esm/types/displaybox/demos/deletionEventsDemo.d.ts +2 -0
  20. package/dist/esm/types/displaybox/demos/focusElementDemo.d.ts +4 -0
  21. package/dist/esm/types/engine/DiagramEngine.d.ts +9 -0
  22. package/dist/examples.d.ts +51 -0
  23. package/dist/index.d.ts +52 -1
  24. package/docs/API_CONTRACT.md +48 -1
  25. package/docs/ARCHITECTURE.md +9 -7
  26. package/docs/CAPABILITIES.md +3 -0
  27. package/docs/COMMANDS_EVENTS.md +15 -6
  28. package/docs/DOCUMENTATION_WORKFLOW.md +3 -1
  29. package/docs/INTEGRATION_PLAYBOOK.md +4 -0
  30. package/docs/PORTING_CHECKLIST.md +4 -1
  31. package/package.json +1 -1
  32. package/src/displaybox/demos/DeletionEventsDemoTab.tsx +167 -9
  33. package/src/displaybox/demos/deletionEventsDemo.ts +2 -2
  34. package/src/displaybox/demos/focusElementDemo.ts +91 -0
  35. package/src/displaybox/demos/index.tsx +2 -0
@@ -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} />
@@ -2,7 +2,7 @@ import type { DiagramState } from '../../api';
2
2
  import type { DemoConfig } from '../types';
3
3
  import { baseElementShapes, basePortShapes } from './shared';
4
4
 
5
- const createDeletionEventsState = (): DiagramState => ({
5
+ export const createDeletionEventsState = (): DiagramState => ({
6
6
  elements: [
7
7
  {
8
8
  id: 'delete-source',
@@ -81,7 +81,7 @@ const createDeletionEventsState = (): DiagramState => ({
81
81
  export const deletionEventsDemoConfig: DemoConfig = {
82
82
  id: 'deletion-events',
83
83
  title: 'Deletion Events',
84
- description: 'Inspect elementDeleted, portDeleted, linkDeleted, and textDeleted payloads for direct and cascade removals.',
84
+ description: 'Inspect cancellable delete lifecycle events, post-delete payloads, and element double click in one integration-focused scenario.',
85
85
  createState: createDeletionEventsState,
86
86
  elementShapes: baseElementShapes,
87
87
  portShapes: basePortShapes,
@@ -0,0 +1,91 @@
1
+ import type { DiagramState } from '../../api';
2
+ import type { DemoAction, DemoConfig } from '../types';
3
+ import { baseElementShapes, basePortShapes } from './shared';
4
+
5
+ export const createFocusElementState = (): DiagramState => ({
6
+ elements: [
7
+ {
8
+ id: 'focus-left',
9
+ position: { x: 40, y: 80 },
10
+ size: { width: 140, height: 90 },
11
+ shapeId: 'default',
12
+ style: { fill: '#e0f2fe', stroke: '#0369a1', strokeWidth: 2 },
13
+ },
14
+ {
15
+ id: 'focus-right',
16
+ position: { x: 820, y: 120 },
17
+ size: { width: 180, height: 110 },
18
+ shapeId: 'panel',
19
+ style: { fill: '#fef3c7', stroke: '#b45309', strokeWidth: 2 },
20
+ },
21
+ {
22
+ id: 'focus-parent',
23
+ position: { x: 520, y: 420 },
24
+ size: { width: 220, height: 160 },
25
+ shapeId: 'default',
26
+ style: { fill: '#ecfccb', stroke: '#4d7c0f', strokeWidth: 2 },
27
+ },
28
+ {
29
+ id: 'focus-child',
30
+ parentId: 'focus-parent',
31
+ position: { x: 110, y: 60 },
32
+ size: { width: 70, height: 50 },
33
+ shapeId: 'default',
34
+ anchorCenter: true,
35
+ style: { fill: '#dcfce7', stroke: '#15803d', strokeWidth: 2 },
36
+ },
37
+ ],
38
+ ports: [],
39
+ links: [],
40
+ texts: [
41
+ {
42
+ id: 'focus-copy',
43
+ content: 'Compare explicit element focus against zoom-to-fit.',
44
+ position: { x: 40, y: 26 },
45
+ },
46
+ ],
47
+ });
48
+
49
+ const focusLeftAction: DemoAction = {
50
+ id: 'focus-left-default',
51
+ label: 'Focus Left (Keep Zoom)',
52
+ run: (editor) => {
53
+ editor.focusElement('focus-left');
54
+ },
55
+ };
56
+
57
+ const focusRightZoomAction: DemoAction = {
58
+ id: 'focus-right-zoom',
59
+ label: 'Focus Right (Zoom 2.2)',
60
+ run: (editor) => {
61
+ editor.focusElement('focus-right', { zoom: 2.2 });
62
+ },
63
+ };
64
+
65
+ const focusChildZoomAction: DemoAction = {
66
+ id: 'focus-child-zoom',
67
+ label: 'Focus Nested Child (Zoom 1.8)',
68
+ run: (editor) => {
69
+ editor.focusElement('focus-child', { zoom: 1.8 });
70
+ },
71
+ };
72
+
73
+ const fitAllAction: DemoAction = {
74
+ id: 'focus-fit-all',
75
+ label: 'Zoom-To-Fit For Comparison',
76
+ run: (editor) => {
77
+ editor.zoomToFitElements({ padding: 40 });
78
+ },
79
+ };
80
+
81
+ export const focusElementDemoConfig: DemoConfig = {
82
+ id: 'focus-element',
83
+ title: 'Focus Element',
84
+ description: 'Centers one explicit element, optionally with a chosen zoom, unlike zoom-to-fit which frames the whole scene.',
85
+ createState: createFocusElementState,
86
+ elementShapes: baseElementShapes,
87
+ portShapes: basePortShapes,
88
+ defaultElementShapeId: 'default',
89
+ defaultPortShapeId: 'port-circle',
90
+ actions: [focusLeftAction, focusRightZoomAction, focusChildZoomAction, fitAllAction],
91
+ };
@@ -53,6 +53,7 @@ import AsymmetricPortMultiAnchorDemo from './AsymmetricPortMultiAnchorDemoTab';
53
53
  import VertexControlLinkSessionDemo from './VertexControlLinkSessionDemoTab';
54
54
  import { offsetAnchorAvoidanceDemoConfig } from './offsetAnchorAvoidanceDemo';
55
55
  import { zoomToFitElementsDemoConfig } from './zoomToFitElementsDemo';
56
+ import { focusElementDemoConfig } from './focusElementDemo';
56
57
 
57
58
  export {
58
59
  baseElementShapes,
@@ -120,6 +121,7 @@ const basics = [
120
121
  registerDemo(selectionDemoConfig, wrapSimpleDemo(selectionDemoConfig), 'basics', 4),
121
122
  registerDemo(elementVisibilitySelectionDemoConfig, wrapSimpleDemo(elementVisibilitySelectionDemoConfig), 'basics', 5),
122
123
  registerDemo(zoomToFitElementsDemoConfig, ZoomToFitElementsDemo, 'basics', 6),
124
+ registerDemo(focusElementDemoConfig, wrapSimpleDemo(focusElementDemoConfig), 'basics', 7),
123
125
  ];
124
126
 
125
127
  const layoutAndText = [