@dr2rai/raid-canvas 0.1.0 → 0.3.0

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.
@@ -0,0 +1,748 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * @file RaidCanvas.tsx
4
+ * @description Reusable React component wrapping AntV X6 and RaiBridge for
5
+ * AOAIM (Activity-Object-AI Model) interactive visual editing, Manhattan orthogonal
6
+ * connector routing, drag-and-drop shape stencils, semantic anti-entropy wiring,
7
+ * and aim-* SVG synchronization.
8
+ *
9
+ * Honors Alan Kay's Dynabook vision of dynamic, malleable visual objects
10
+ * and Rainer Burkhardt's C++ GrafObj graphical hierarchy contracts.
11
+ */
12
+ import React, { useEffect, useRef, useState, useCallback, useImperativeHandle, } from 'react';
13
+ import { Graph, Shape } from '@antv/x6';
14
+ import { RaiBridge } from './RaiBridge.js';
15
+ import { registerAimShapes, configureAimGraph, createAimNode, applyEdgeRouting, CascaisPalette, getDefaultNodeBounds, getDefaultNodeName, } from './X6Shapes.js';
16
+ import { validateSemanticConnection, getSemanticEdgeKind, getSemanticEdgeStereotype, } from './semanticRules.js';
17
+ /**
18
+ * Declarative drop-in React component for rendering and interacting with
19
+ * AOAIM diagrams conforming to the ontological aim-* SVG contract.
20
+ */
21
+ export const RaidCanvas = React.forwardRef(function RaidCanvas({ svg, svgContent, readOnly = false, defaultRouting = 'manhattan', className, style, onChange, onSave, onSelect, onSelectionChange, onDropStencil, showToolbar = true, }, ref) {
22
+ const containerRef = useRef(null);
23
+ const graphRef = useRef(null);
24
+ const bridgeRef = useRef(new RaiBridge());
25
+ const lastSerializedSvgRef = useRef('');
26
+ const isHydratingRef = useRef(false);
27
+ const debounceTimerRef = useRef(null);
28
+ const selectedCellIdRef = useRef(null);
29
+ const clearEdgeToolsRef = useRef(null);
30
+ const routingModeRef = useRef(defaultRouting);
31
+ routingModeRef.current = defaultRouting;
32
+ const [zoomLevel, setZoomLevel] = useState(100);
33
+ const [isDragOver, setIsDragOver] = useState(false);
34
+ // Unify svg vs svgContent props
35
+ const activeSvg = svgContent ?? svg ?? '';
36
+ // Callback refs to maintain stable graph event listeners
37
+ const onChangeRef = useRef(onChange);
38
+ onChangeRef.current = onChange;
39
+ const onSaveRef = useRef(onSave);
40
+ onSaveRef.current = onSave;
41
+ const onSelectRef = useRef(onSelect);
42
+ onSelectRef.current = onSelect;
43
+ const onSelectionChangeRef = useRef(onSelectionChange);
44
+ onSelectionChangeRef.current = onSelectionChange;
45
+ const onDropStencilRef = useRef(onDropStencil);
46
+ onDropStencilRef.current = onDropStencil;
47
+ const svgPropRef = useRef(activeSvg);
48
+ svgPropRef.current = activeSvg;
49
+ // Helper: Debounced Serialization and notification
50
+ const triggerDebouncedChange = useCallback(() => {
51
+ if (isHydratingRef.current)
52
+ return;
53
+ if (debounceTimerRef.current) {
54
+ clearTimeout(debounceTimerRef.current);
55
+ }
56
+ debounceTimerRef.current = setTimeout(() => {
57
+ if (!graphRef.current)
58
+ return;
59
+ const currentBaseSvg = svgPropRef.current;
60
+ const updatedSvg = bridgeRef.current.serializeToSvg(graphRef.current, currentBaseSvg);
61
+ lastSerializedSvgRef.current = updatedSvg;
62
+ onChangeRef.current?.(updatedSvg);
63
+ onSaveRef.current?.(updatedSvg);
64
+ }, 100);
65
+ }, []);
66
+ // --------------------------------------------------------------------------
67
+ // Toolbar Actions
68
+ // --------------------------------------------------------------------------
69
+ const handleZoomIn = useCallback(() => {
70
+ const graph = graphRef.current;
71
+ if (!graph)
72
+ return;
73
+ graph.zoom(0.2);
74
+ setZoomLevel(Math.round(graph.zoom() * 100));
75
+ }, []);
76
+ const handleZoomOut = useCallback(() => {
77
+ const graph = graphRef.current;
78
+ if (!graph)
79
+ return;
80
+ graph.zoom(-0.2);
81
+ setZoomLevel(Math.round(graph.zoom() * 100));
82
+ }, []);
83
+ const handleFitToContent = useCallback(() => {
84
+ const graph = graphRef.current;
85
+ if (!graph)
86
+ return;
87
+ graph.zoomToFit({ padding: 32, maxScale: 1.5 });
88
+ setZoomLevel(Math.round(graph.zoom() * 100));
89
+ }, []);
90
+ const handleCenter = useCallback(() => {
91
+ const graph = graphRef.current;
92
+ if (!graph)
93
+ return;
94
+ graph.centerContent();
95
+ }, []);
96
+ const handleResetView = useCallback(() => {
97
+ const graph = graphRef.current;
98
+ if (!graph)
99
+ return;
100
+ graph.zoom(1, { absolute: true });
101
+ graph.centerContent();
102
+ setZoomLevel(100);
103
+ }, []);
104
+ // --------------------------------------------------------------------------
105
+ // Imperative Ref Handle API
106
+ // --------------------------------------------------------------------------
107
+ useImperativeHandle(ref, () => ({
108
+ getGraph: () => graphRef.current,
109
+ getSvg: () => {
110
+ if (!graphRef.current)
111
+ return svgPropRef.current;
112
+ return bridgeRef.current.serializeToSvg(graphRef.current, svgPropRef.current);
113
+ },
114
+ addNode: (kind, x, y, customData) => {
115
+ const graph = graphRef.current;
116
+ if (!graph)
117
+ return '';
118
+ const defaultBounds = getDefaultNodeBounds(kind, x ?? 200, y ?? 150);
119
+ const defaultName = getDefaultNodeName(kind);
120
+ const id = `${kind.toUpperCase()}_${Date.now().toString(36).slice(-4)}`;
121
+ const nodeData = {
122
+ id,
123
+ kind,
124
+ displayName: customData?.displayName ?? defaultName,
125
+ ...(customData?.stereotype !== undefined ? { stereotype: customData.stereotype } : {}),
126
+ ...(customData?.attributes !== undefined ? { attributes: customData.attributes } : {}),
127
+ ...(customData?.methods !== undefined ? { methods: customData.methods } : {}),
128
+ bounds: {
129
+ ...defaultBounds,
130
+ ...(customData?.bounds ?? {}),
131
+ },
132
+ };
133
+ const nodeMeta = createAimNode(nodeData);
134
+ graph.addNode(nodeMeta);
135
+ selectedCellIdRef.current = id;
136
+ onSelectRef.current?.({ id, kind, label: nodeData.displayName });
137
+ onSelectionChangeRef.current?.([id]);
138
+ triggerDebouncedChange();
139
+ return id;
140
+ },
141
+ updateNode: (id, updates) => {
142
+ const graph = graphRef.current;
143
+ if (!graph)
144
+ return;
145
+ const node = graph.getCellById(id);
146
+ if (!node || !node.isNode())
147
+ return;
148
+ const currentData = (node.getData() ?? {});
149
+ const nextData = {
150
+ ...currentData,
151
+ ...updates,
152
+ id: updates.id ?? currentData.id ?? id,
153
+ kind: updates.kind ?? currentData.kind ?? 'act',
154
+ displayName: updates.displayName ?? currentData.displayName ?? id,
155
+ bounds: {
156
+ ...(currentData.bounds ?? { x: 0, y: 0, width: 140, height: 60 }),
157
+ ...(updates.bounds ?? {}),
158
+ },
159
+ };
160
+ node.setData(nextData);
161
+ const displayName = nextData.displayName;
162
+ const stereotype = nextData.stereotype;
163
+ const labelText = stereotype ? `${stereotype}\n${displayName}` : displayName;
164
+ if (nextData.kind === 'cls') {
165
+ node.setAttrByPath('title/text', displayName);
166
+ if (nextData.attributes !== undefined) {
167
+ node.setAttrByPath('attributes/text', nextData.attributes.join('\n'));
168
+ }
169
+ if (nextData.methods !== undefined) {
170
+ node.setAttrByPath('methods/text', nextData.methods.join('\n'));
171
+ }
172
+ }
173
+ else {
174
+ node.setAttrByPath('label/text', labelText);
175
+ }
176
+ if (updates.bounds) {
177
+ node.setPosition(updates.bounds.x, updates.bounds.y);
178
+ node.setSize(updates.bounds.width, updates.bounds.height);
179
+ }
180
+ triggerDebouncedChange();
181
+ },
182
+ updateEdge: (id, updates) => {
183
+ const graph = graphRef.current;
184
+ if (!graph)
185
+ return;
186
+ const edge = graph.getCellById(id);
187
+ if (!edge || !edge.isEdge())
188
+ return;
189
+ const currentData = (edge.getData() ?? {});
190
+ const nextData = { ...currentData, ...updates };
191
+ edge.setData(nextData);
192
+ if (updates.routing !== undefined) {
193
+ applyEdgeRouting(edge, updates.routing);
194
+ }
195
+ if (updates.sourcePort !== undefined) {
196
+ const currentSource = edge.getSource();
197
+ if (currentSource.cell) {
198
+ edge.setSource({ cell: currentSource.cell, port: updates.sourcePort });
199
+ }
200
+ }
201
+ if (updates.targetPort !== undefined) {
202
+ const currentTarget = edge.getTarget();
203
+ if (currentTarget.cell) {
204
+ edge.setTarget({ cell: currentTarget.cell, port: updates.targetPort });
205
+ }
206
+ }
207
+ if (updates.label !== undefined || updates.stereotype !== undefined) {
208
+ const text = updates.label ?? updates.stereotype ?? '';
209
+ edge.setLabels(text
210
+ ? [
211
+ {
212
+ attrs: {
213
+ text: {
214
+ text,
215
+ fill: CascaisPalette.TextSecondary,
216
+ fontSize: 11,
217
+ },
218
+ },
219
+ position: 0.5,
220
+ },
221
+ ]
222
+ : []);
223
+ }
224
+ triggerDebouncedChange();
225
+ },
226
+ setRoutingMode: (mode, applyToAll = true) => {
227
+ routingModeRef.current = mode;
228
+ if (applyToAll && graphRef.current) {
229
+ for (const edge of graphRef.current.getEdges()) {
230
+ applyEdgeRouting(edge, mode);
231
+ const currentData = (edge.getData() ?? {});
232
+ edge.setData({ ...currentData, routing: mode });
233
+ }
234
+ triggerDebouncedChange();
235
+ }
236
+ },
237
+ getRoutingMode: () => routingModeRef.current,
238
+ deleteSelection: () => {
239
+ const graph = graphRef.current;
240
+ if (!graph || readOnly)
241
+ return;
242
+ if (selectedCellIdRef.current) {
243
+ const cell = graph.getCellById(selectedCellIdRef.current);
244
+ if (cell) {
245
+ clearEdgeToolsRef.current?.();
246
+ graph.removeCell(cell);
247
+ selectedCellIdRef.current = null;
248
+ onSelectRef.current?.(null);
249
+ onSelectionChangeRef.current?.([]);
250
+ triggerDebouncedChange();
251
+ }
252
+ }
253
+ },
254
+ clear: () => {
255
+ const graph = graphRef.current;
256
+ if (!graph || readOnly)
257
+ return;
258
+ graph.clearCells();
259
+ selectedCellIdRef.current = null;
260
+ onSelectRef.current?.(null);
261
+ onSelectionChangeRef.current?.([]);
262
+ triggerDebouncedChange();
263
+ },
264
+ center: handleCenter,
265
+ zoomToFit: handleFitToContent,
266
+ resetView: handleResetView,
267
+ zoomIn: handleZoomIn,
268
+ zoomOut: handleZoomOut,
269
+ }), [
270
+ handleCenter,
271
+ handleFitToContent,
272
+ handleResetView,
273
+ handleZoomIn,
274
+ handleZoomOut,
275
+ readOnly,
276
+ triggerDebouncedChange,
277
+ ]);
278
+ // --------------------------------------------------------------------------
279
+ // 1. Mount Graph & Event Listeners
280
+ // --------------------------------------------------------------------------
281
+ useEffect(() => {
282
+ if (!containerRef.current)
283
+ return;
284
+ registerAimShapes();
285
+ const container = containerRef.current;
286
+ const initialWidth = container.clientWidth || 800;
287
+ const initialHeight = container.clientHeight || 600;
288
+ const graph = new Graph({
289
+ container,
290
+ width: initialWidth,
291
+ height: initialHeight,
292
+ autoResize: false,
293
+ grid: {
294
+ visible: true,
295
+ type: 'dot',
296
+ args: {
297
+ color: CascaisPalette.SilverLine,
298
+ thickness: 1,
299
+ },
300
+ },
301
+ highlighting: {
302
+ magnetAvailable: {
303
+ name: 'stroke',
304
+ args: {
305
+ padding: 3,
306
+ attrs: {
307
+ stroke: CascaisPalette.HeraldicGreen,
308
+ strokeWidth: 2,
309
+ },
310
+ },
311
+ },
312
+ magnetAdsorbed: {
313
+ name: 'stroke',
314
+ args: {
315
+ padding: 4,
316
+ attrs: {
317
+ stroke: CascaisPalette.HeraldicGreen,
318
+ strokeWidth: 3,
319
+ fill: CascaisPalette.HeraldicGreen,
320
+ },
321
+ },
322
+ },
323
+ },
324
+ interacting: {
325
+ nodeMovable: !readOnly,
326
+ edgeMovable: !readOnly,
327
+ edgeLabelMovable: !readOnly,
328
+ arrowheadMovable: !readOnly,
329
+ vertexMovable: !readOnly,
330
+ vertexAddable: !readOnly,
331
+ vertexDeletable: !readOnly,
332
+ },
333
+ panning: {
334
+ enabled: true,
335
+ eventTypes: readOnly
336
+ ? ['leftMouseDown', 'rightMouseDown']
337
+ : ['rightMouseDown', 'mouseWheel'],
338
+ },
339
+ mousewheel: {
340
+ enabled: true,
341
+ modifiers: ['ctrl', 'meta'],
342
+ },
343
+ connecting: {
344
+ router: 'manhattan',
345
+ connector: { name: 'rounded', args: { radius: 8 } },
346
+ allowBlank: false,
347
+ allowLoop: false,
348
+ allowNode: true,
349
+ allowPort: true,
350
+ allowEdge: false,
351
+ highlight: true,
352
+ validateConnection({ sourceCell, targetCell }) {
353
+ if (!sourceCell || !targetCell)
354
+ return false;
355
+ if (sourceCell === targetCell)
356
+ return false;
357
+ if (!sourceCell.isNode() || !targetCell.isNode())
358
+ return false;
359
+ const sourceKind = (sourceCell.getData()?.kind ?? 'act');
360
+ const targetKind = (targetCell.getData()?.kind ?? 'act');
361
+ return validateSemanticConnection(sourceKind, targetKind);
362
+ },
363
+ createEdge() {
364
+ const edge = new Shape.Edge({
365
+ shape: 'aim-edge',
366
+ });
367
+ applyEdgeRouting(edge, routingModeRef.current);
368
+ return edge;
369
+ },
370
+ },
371
+ });
372
+ configureAimGraph(graph);
373
+ graphRef.current = graph;
374
+ let activeToolEdge = null;
375
+ const clearEdgeTools = () => {
376
+ if (activeToolEdge) {
377
+ try {
378
+ activeToolEdge.removeTools();
379
+ }
380
+ catch {
381
+ // Ignore if cell was already removed
382
+ }
383
+ activeToolEdge = null;
384
+ }
385
+ };
386
+ clearEdgeToolsRef.current = clearEdgeTools;
387
+ const setEdgeTools = (edge) => {
388
+ clearEdgeTools();
389
+ if (readOnly)
390
+ return;
391
+ activeToolEdge = edge;
392
+ try {
393
+ edge.addTools([
394
+ {
395
+ name: 'source-arrowhead',
396
+ args: {
397
+ attrs: {
398
+ fill: CascaisPalette.NetGold,
399
+ stroke: '#FFFFFF',
400
+ strokeWidth: 2,
401
+ cursor: 'grab',
402
+ },
403
+ },
404
+ },
405
+ {
406
+ name: 'target-arrowhead',
407
+ args: {
408
+ attrs: {
409
+ fill: CascaisPalette.NetGold,
410
+ stroke: '#FFFFFF',
411
+ strokeWidth: 2,
412
+ cursor: 'grab',
413
+ },
414
+ },
415
+ },
416
+ {
417
+ name: 'vertices',
418
+ args: {
419
+ attrs: {
420
+ fill: CascaisPalette.WarmGraphite,
421
+ stroke: '#FFFFFF',
422
+ strokeWidth: 1.5,
423
+ },
424
+ },
425
+ },
426
+ ]);
427
+ }
428
+ catch {
429
+ // Fallback if tools fail to attach
430
+ }
431
+ };
432
+ // Handle edge connections and re-connections with automatic semantic wiring
433
+ graph.on('edge:connected', ({ edge, isNew }) => {
434
+ const sourceCell = edge.getSourceCell();
435
+ const targetCell = edge.getTargetCell();
436
+ if (sourceCell?.isNode() && targetCell?.isNode()) {
437
+ const sourceKind = (sourceCell.getData()?.kind ?? 'act');
438
+ const targetKind = (targetCell.getData()?.kind ?? 'act');
439
+ const edgeKind = getSemanticEdgeKind(sourceKind, targetKind);
440
+ const currentData = (edge.getData() ?? {});
441
+ const stereotype = currentData.stereotype ?? getSemanticEdgeStereotype(sourceKind, targetKind);
442
+ edge.setData({
443
+ ...currentData,
444
+ id: edge.id,
445
+ kind: edgeKind,
446
+ sourceId: sourceCell.id,
447
+ targetId: targetCell.id,
448
+ sourcePort: edge.getSourcePortId(),
449
+ targetPort: edge.getTargetPortId(),
450
+ label: currentData.label ?? stereotype,
451
+ stereotype,
452
+ routing: currentData.routing ?? routingModeRef.current,
453
+ bendPoints: currentData.bendPoints ?? [],
454
+ });
455
+ if (isNew) {
456
+ applyEdgeRouting(edge, routingModeRef.current);
457
+ if (stereotype) {
458
+ edge.setLabels([
459
+ {
460
+ attrs: {
461
+ text: {
462
+ text: stereotype,
463
+ fill: CascaisPalette.TextSecondary,
464
+ fontSize: 11,
465
+ },
466
+ },
467
+ position: 0.5,
468
+ },
469
+ ]);
470
+ }
471
+ }
472
+ else {
473
+ // Re-anchored: if edge was selected, re-attach tools
474
+ if (selectedCellIdRef.current === edge.id) {
475
+ setEdgeTools(edge);
476
+ }
477
+ }
478
+ }
479
+ triggerDebouncedChange();
480
+ });
481
+ // Canvas change events
482
+ graph.on('node:change:position', triggerDebouncedChange);
483
+ graph.on('node:change:size', triggerDebouncedChange);
484
+ graph.on('edge:change:vertices', triggerDebouncedChange);
485
+ graph.on('edge:change:source', triggerDebouncedChange);
486
+ graph.on('edge:change:target', triggerDebouncedChange);
487
+ graph.on('cell:added', triggerDebouncedChange);
488
+ graph.on('cell:removed', ({ cell }) => {
489
+ if (cell === activeToolEdge) {
490
+ activeToolEdge = null;
491
+ }
492
+ triggerDebouncedChange();
493
+ });
494
+ // Zoom listener to keep toolbar indicator accurate
495
+ graph.on('scale', () => {
496
+ setZoomLevel(Math.round(graph.zoom() * 100));
497
+ });
498
+ // Selection listeners
499
+ graph.on('cell:click', ({ cell }) => {
500
+ const id = String(cell.id);
501
+ selectedCellIdRef.current = id;
502
+ if (cell.isNode()) {
503
+ clearEdgeTools();
504
+ const data = (cell.getData() ?? {});
505
+ const kind = (data.kind ?? 'act');
506
+ const label = data.displayName ??
507
+ cell.getAttrByPath('label/text') ??
508
+ cell.getAttrByPath('title/text') ??
509
+ id;
510
+ onSelectRef.current?.({ id, kind, label });
511
+ }
512
+ else if (cell.isEdge()) {
513
+ setEdgeTools(cell);
514
+ const data = (cell.getData() ?? {});
515
+ const label = data.label ??
516
+ cell.getLabels()?.[0]?.attrs?.['text']?.['text'] ??
517
+ '';
518
+ onSelectRef.current?.({ id, kind: 'act', label });
519
+ }
520
+ onSelectionChangeRef.current?.([id]);
521
+ });
522
+ graph.on('blank:click', () => {
523
+ clearEdgeTools();
524
+ selectedCellIdRef.current = null;
525
+ onSelectRef.current?.(null);
526
+ onSelectionChangeRef.current?.([]);
527
+ });
528
+ // Keyboard listener for deletion
529
+ const handleKeyDown = (e) => {
530
+ if (readOnly)
531
+ return;
532
+ const activeTag = (document.activeElement?.tagName ?? '').toLowerCase();
533
+ if (activeTag === 'input' ||
534
+ activeTag === 'textarea' ||
535
+ document.activeElement?.isContentEditable) {
536
+ return;
537
+ }
538
+ if (e.key === 'Delete' || e.key === 'Backspace') {
539
+ if (selectedCellIdRef.current && graphRef.current) {
540
+ const cell = graphRef.current.getCellById(selectedCellIdRef.current);
541
+ if (cell) {
542
+ e.preventDefault();
543
+ clearEdgeTools();
544
+ graphRef.current.removeCell(cell);
545
+ selectedCellIdRef.current = null;
546
+ onSelectRef.current?.(null);
547
+ onSelectionChangeRef.current?.([]);
548
+ triggerDebouncedChange();
549
+ }
550
+ }
551
+ }
552
+ };
553
+ window.addEventListener('keydown', handleKeyDown);
554
+ // Auto-resize observer for fluid layouts
555
+ const resizeObserver = new ResizeObserver((entries) => {
556
+ for (const entry of entries) {
557
+ const { width, height } = entry.contentRect;
558
+ if (width > 0 && height > 0 && graphRef.current) {
559
+ graphRef.current.resize(width, height);
560
+ }
561
+ }
562
+ });
563
+ resizeObserver.observe(container);
564
+ // Initial Hydration
565
+ if (svgPropRef.current) {
566
+ isHydratingRef.current = true;
567
+ try {
568
+ bridgeRef.current.hydrateFromSvg(svgPropRef.current, graph);
569
+ lastSerializedSvgRef.current = svgPropRef.current;
570
+ graph.centerContent();
571
+ }
572
+ catch (err) {
573
+ console.error('RaidCanvas hydration error:', err);
574
+ }
575
+ finally {
576
+ isHydratingRef.current = false;
577
+ }
578
+ }
579
+ // Cleanup
580
+ return () => {
581
+ if (debounceTimerRef.current) {
582
+ clearTimeout(debounceTimerRef.current);
583
+ }
584
+ window.removeEventListener('keydown', handleKeyDown);
585
+ resizeObserver.disconnect();
586
+ clearEdgeToolsRef.current = null;
587
+ graph.dispose();
588
+ graphRef.current = null;
589
+ };
590
+ }, [readOnly, triggerDebouncedChange]);
591
+ // --------------------------------------------------------------------------
592
+ // 2. React to External SVG Changes
593
+ // --------------------------------------------------------------------------
594
+ useEffect(() => {
595
+ const graph = graphRef.current;
596
+ if (!graph || !activeSvg)
597
+ return;
598
+ // Ignore if this change originated from our own serialization
599
+ if (activeSvg === lastSerializedSvgRef.current) {
600
+ return;
601
+ }
602
+ isHydratingRef.current = true;
603
+ try {
604
+ bridgeRef.current.hydrateFromSvg(activeSvg, graph);
605
+ lastSerializedSvgRef.current = activeSvg;
606
+ graph.centerContent();
607
+ }
608
+ catch (err) {
609
+ console.error('RaidCanvas re-hydration error:', err);
610
+ }
611
+ finally {
612
+ isHydratingRef.current = false;
613
+ }
614
+ }, [activeSvg]);
615
+ // --------------------------------------------------------------------------
616
+ // 3. HTML5 Drag-and-Drop Stencil Dropzone
617
+ // --------------------------------------------------------------------------
618
+ const handleDragOver = (e) => {
619
+ if (readOnly)
620
+ return;
621
+ const hasStencil = e.dataTransfer.types.includes('application/aoaim-kind') ||
622
+ e.dataTransfer.types.includes('text/plain');
623
+ if (hasStencil) {
624
+ e.preventDefault();
625
+ e.dataTransfer.dropEffect = 'copy';
626
+ if (!isDragOver)
627
+ setIsDragOver(true);
628
+ }
629
+ };
630
+ const handleDragLeave = (e) => {
631
+ if (readOnly)
632
+ return;
633
+ if (e.currentTarget.contains(e.relatedTarget))
634
+ return;
635
+ setIsDragOver(false);
636
+ };
637
+ const handleDrop = (e) => {
638
+ if (readOnly)
639
+ return;
640
+ e.preventDefault();
641
+ setIsDragOver(false);
642
+ const kind = (e.dataTransfer.getData('application/aoaim-kind') ||
643
+ e.dataTransfer.getData('text/plain'));
644
+ if (!kind || !graphRef.current || !containerRef.current)
645
+ return;
646
+ const rect = containerRef.current.getBoundingClientRect();
647
+ const clientX = e.clientX - rect.left;
648
+ const clientY = e.clientY - rect.top;
649
+ const graph = graphRef.current;
650
+ const localPos = graph.clientToLocal({ x: clientX, y: clientY });
651
+ const defaultBounds = getDefaultNodeBounds(kind, localPos.x, localPos.y);
652
+ const bounds = {
653
+ ...defaultBounds,
654
+ x: Math.round(localPos.x - defaultBounds.width / 2),
655
+ y: Math.round(localPos.y - defaultBounds.height / 2),
656
+ };
657
+ const defaultName = getDefaultNodeName(kind);
658
+ const id = `${kind.toUpperCase()}_${Date.now().toString(36).slice(-4)}`;
659
+ const nodeData = {
660
+ id,
661
+ kind,
662
+ displayName: defaultName,
663
+ bounds,
664
+ };
665
+ const nodeMeta = createAimNode(nodeData);
666
+ graph.addNode(nodeMeta);
667
+ selectedCellIdRef.current = id;
668
+ onSelectRef.current?.({ id, kind, label: defaultName });
669
+ onSelectionChangeRef.current?.([id]);
670
+ onDropStencilRef.current?.(kind, { x: bounds.x, y: bounds.y });
671
+ triggerDebouncedChange();
672
+ };
673
+ return (_jsxs("div", { className: `raid-canvas-wrapper ${className ?? ''}`.trim(), onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, style: {
674
+ position: 'relative',
675
+ width: '100%',
676
+ height: '100%',
677
+ minHeight: 300,
678
+ overflow: 'hidden',
679
+ background: isDragOver ? '#F0FDF4' : CascaisPalette.ChalkWhite,
680
+ outline: isDragOver ? `2px dashed ${CascaisPalette.HeraldicGreen}` : 'none',
681
+ outlineOffset: -4,
682
+ transition: 'background-color 0.2s ease, outline 0.2s ease',
683
+ ...style,
684
+ }, children: [_jsx("div", { ref: containerRef, style: {
685
+ width: '100%',
686
+ height: '100%',
687
+ position: 'absolute',
688
+ inset: 0,
689
+ } }), isDragOver && (_jsx("div", { style: {
690
+ position: 'absolute',
691
+ top: 16,
692
+ left: '50%',
693
+ transform: 'translateX(-50%)',
694
+ zIndex: 20,
695
+ padding: '6px 16px',
696
+ background: '#10B981',
697
+ color: '#FFFFFF',
698
+ borderRadius: 20,
699
+ fontSize: 12,
700
+ fontWeight: 600,
701
+ boxShadow: '0 4px 12px rgba(16, 185, 129, 0.3)',
702
+ pointerEvents: 'none',
703
+ }, children: "Drop to Instantiate AOAIM Node" })), showToolbar && (_jsxs("div", { className: "raid-canvas-toolbar", style: {
704
+ position: 'absolute',
705
+ bottom: 16,
706
+ left: 16,
707
+ zIndex: 10,
708
+ display: 'inline-flex',
709
+ alignItems: 'center',
710
+ gap: 2,
711
+ padding: '4px 6px',
712
+ background: 'rgba(255, 255, 255, 0.92)',
713
+ backdropFilter: 'blur(8px)',
714
+ border: `1px solid ${CascaisPalette.SilverLineDark}`,
715
+ borderRadius: 8,
716
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
717
+ fontFamily: 'Inter, system-ui, sans-serif',
718
+ userSelect: 'none',
719
+ }, children: [_jsx("button", { type: "button", title: "Zoom In", "aria-label": "Zoom In", onClick: handleZoomIn, style: toolbarBtnStyle, children: "\u2795" }), _jsx("button", { type: "button", title: "Zoom Out", "aria-label": "Zoom Out", onClick: handleZoomOut, style: toolbarBtnStyle, children: "\u2796" }), _jsxs("span", { style: {
720
+ padding: '0 6px',
721
+ fontSize: 11,
722
+ fontWeight: 600,
723
+ color: CascaisPalette.TextSecondary,
724
+ minWidth: 42,
725
+ textAlign: 'center',
726
+ }, children: [zoomLevel, "%"] }), _jsx("div", { style: {
727
+ width: 1,
728
+ height: 16,
729
+ background: CascaisPalette.SilverLine,
730
+ margin: '0 2px',
731
+ } }), _jsx("button", { type: "button", title: "Fit to Content", "aria-label": "Fit to Content", onClick: handleFitToContent, style: toolbarBtnStyle, children: "\u26F6" }), _jsx("button", { type: "button", title: "Center Content", "aria-label": "Center Content", onClick: handleCenter, style: toolbarBtnStyle, children: "\uD83C\uDFAF" }), _jsx("button", { type: "button", title: "Reset View (100%)", "aria-label": "Reset View", onClick: handleResetView, style: toolbarBtnStyle, children: "\u21BA" })] }))] }));
732
+ });
733
+ const toolbarBtnStyle = {
734
+ display: 'inline-flex',
735
+ alignItems: 'center',
736
+ justifyContent: 'center',
737
+ width: 28,
738
+ height: 28,
739
+ padding: 0,
740
+ border: 'none',
741
+ background: 'transparent',
742
+ borderRadius: 6,
743
+ cursor: 'pointer',
744
+ fontSize: 12,
745
+ color: CascaisPalette.WarmGraphite,
746
+ transition: 'background-color 0.15s ease',
747
+ };
748
+ //# sourceMappingURL=RaidCanvas.js.map