@1agh/maude 0.29.0 → 0.30.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.
Files changed (99) hide show
  1. package/README.md +1 -1
  2. package/apps/studio/ai-banner.tsx +0 -1
  3. package/apps/studio/annotations-align.ts +149 -0
  4. package/apps/studio/annotations-bindings.ts +197 -0
  5. package/apps/studio/annotations-context-toolbar.tsx +436 -186
  6. package/apps/studio/annotations-groups.ts +270 -0
  7. package/apps/studio/annotations-layer.tsx +1708 -1660
  8. package/apps/studio/annotations-model.ts +2077 -0
  9. package/apps/studio/annotations-snap.ts +125 -0
  10. package/apps/studio/api.ts +208 -116
  11. package/apps/studio/bin/_png-playwright.mjs +1 -1
  12. package/apps/studio/bin/annotate.mjs +732 -0
  13. package/apps/studio/bin/annotate.sh +17 -0
  14. package/apps/studio/bin/read-annotations.mjs +152 -17
  15. package/apps/studio/build.ts +1 -1
  16. package/apps/studio/canvas-arrowheads.ts +78 -9
  17. package/apps/studio/canvas-cursors.ts +2 -0
  18. package/apps/studio/canvas-edit.ts +257 -7
  19. package/apps/studio/canvas-icons.tsx +105 -0
  20. package/apps/studio/canvas-shell.tsx +305 -8
  21. package/apps/studio/client/app.jsx +2812 -238
  22. package/apps/studio/client/index.html +1 -1
  23. package/apps/studio/client/styles/3-shell-maude.css +713 -11
  24. package/apps/studio/client/styles/4-components.css +39 -0
  25. package/apps/studio/client/styles/5-maude-overrides.css +16 -2
  26. package/apps/studio/client/tour/overlay.jsx +105 -17
  27. package/apps/studio/client/tour/usage-tour.js +23 -10
  28. package/apps/studio/commands/annotation-strokes-command.ts +1 -1
  29. package/apps/studio/commands/move-artboards-command.ts +1 -1
  30. package/apps/studio/comments-overlay.tsx +7 -5
  31. package/apps/studio/context-menu.tsx +4 -3
  32. package/apps/studio/contextual-toolbar.tsx +14 -0
  33. package/apps/studio/cursors-overlay.tsx +20 -20
  34. package/apps/studio/dist/client.bundle.js +3322 -602
  35. package/apps/studio/dist/comment-mount.js +95 -9
  36. package/apps/studio/dist/styles.css +2490 -167
  37. package/apps/studio/dom-selection.ts +115 -0
  38. package/apps/studio/export-dialog.tsx +3 -3
  39. package/apps/studio/history.ts +20 -3
  40. package/apps/studio/http.ts +124 -5
  41. package/apps/studio/input-router.tsx +7 -0
  42. package/apps/studio/inspect.ts +37 -8
  43. package/apps/studio/participants-chrome.tsx +1 -7
  44. package/apps/studio/server.ts +5 -2
  45. package/apps/studio/sync/agent.ts +231 -57
  46. package/apps/studio/sync/codec.ts +45 -0
  47. package/apps/studio/sync/cold-start.ts +158 -0
  48. package/apps/studio/sync/connection-state.ts +58 -2
  49. package/apps/studio/sync/index.ts +563 -221
  50. package/apps/studio/sync/journal.ts +190 -0
  51. package/apps/studio/sync/migrate-seed.ts +202 -44
  52. package/apps/studio/sync/projection.ts +22 -3
  53. package/apps/studio/sync/status.ts +15 -4
  54. package/apps/studio/test/activity.test.ts +1 -7
  55. package/apps/studio/test/annotate-write.test.ts +184 -0
  56. package/apps/studio/test/annotations-align.test.ts +88 -0
  57. package/apps/studio/test/annotations-bindings.test.ts +124 -0
  58. package/apps/studio/test/annotations-groups.test.ts +231 -0
  59. package/apps/studio/test/annotations-snap.test.ts +79 -0
  60. package/apps/studio/test/canvas-edit.test.ts +181 -1
  61. package/apps/studio/test/canvas-origin-gate.test.ts +8 -0
  62. package/apps/studio/test/collab-bridge.test.ts +0 -2
  63. package/apps/studio/test/collab-room.test.ts +2 -7
  64. package/apps/studio/test/csp-canvas-shell.test.ts +5 -0
  65. package/apps/studio/test/csrf-write-guard.test.ts +52 -0
  66. package/apps/studio/test/figjam-v3-model.test.ts +342 -0
  67. package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
  68. package/apps/studio/test/shared-doc-migrate.test.ts +224 -14
  69. package/apps/studio/test/sync-agent.test.ts +272 -2
  70. package/apps/studio/test/sync-codec.test.ts +65 -0
  71. package/apps/studio/test/sync-cold-start.test.ts +244 -0
  72. package/apps/studio/test/sync-connection-state.test.ts +68 -0
  73. package/apps/studio/test/sync-hardening.test.ts +0 -8
  74. package/apps/studio/test/sync-hubs-config.test.ts +0 -1
  75. package/apps/studio/test/sync-incident-replay.test.ts +211 -0
  76. package/apps/studio/test/sync-journal.test.ts +176 -0
  77. package/apps/studio/test/sync-runtime.test.ts +360 -5
  78. package/apps/studio/test/sync-status.test.ts +66 -0
  79. package/apps/studio/test/use-annotation-resize.test.ts +159 -0
  80. package/apps/studio/test/use-artboard-drag.test.ts +0 -1
  81. package/apps/studio/test/use-collab.test.ts +0 -0
  82. package/apps/studio/test/use-tool-mode.test.tsx +5 -2
  83. package/apps/studio/tool-palette.tsx +12 -2
  84. package/apps/studio/undo-hud.tsx +0 -1
  85. package/apps/studio/use-agent-presence.tsx +4 -2
  86. package/apps/studio/use-annotation-resize.tsx +484 -54
  87. package/apps/studio/use-annotation-selection.tsx +16 -4
  88. package/apps/studio/use-canvas-activity.tsx +2 -2
  89. package/apps/studio/use-collab.tsx +20 -15
  90. package/apps/studio/use-selection-set.tsx +9 -0
  91. package/apps/studio/use-tool-mode.tsx +5 -1
  92. package/apps/studio/whats-new.json +135 -10
  93. package/apps/studio/whats-new.schema.json +18 -2
  94. package/cli/commands/design-link.test.mjs +84 -0
  95. package/cli/commands/design.mjs +2 -1
  96. package/cli/commands/design.test.mjs +5 -1
  97. package/cli/lib/design-link.mjs +51 -1
  98. package/package.json +8 -8
  99. package/plugins/design/templates/_shell.html +28 -4
@@ -302,6 +302,14 @@ const HALO_CSS = `
302
302
  -webkit-font-smoothing: subpixel-antialiased;
303
303
  font-smooth: always;
304
304
  }
305
+ /* Phase 12 (DDR-103) — inline text editing: the element being edited carries an
306
+ accent ring + caret. plaintext-only contenteditable; commit writes to source. */
307
+ [contenteditable].dc-text-editing {
308
+ outline: 2px solid var(--maude-hud-accent, #0d99ff);
309
+ outline-offset: 1px;
310
+ border-radius: 1px;
311
+ cursor: text;
312
+ }
305
313
  `.trim();
306
314
 
307
315
  function ensureHaloStyles(): void {
@@ -322,6 +330,104 @@ function ensureHaloStyles(): void {
322
330
  document.head.appendChild(s);
323
331
  }
324
332
 
333
+ // ─────────────────────────────────────────────────────────────────────────────
334
+ // Layers tree (Phase 12 Task 4, DDR-103). The shell's Layers tab is a browsable
335
+ // tree of the active artboard's stamped (`data-cd-id`) elements. We walk the
336
+ // artboard subtree in the iframe, serialize a nested tree, and post it to the
337
+ // shell; the shell renders it + posts back `select-by-id` / `highlight`.
338
+
339
+ interface LayerNode {
340
+ id: string;
341
+ tag: string;
342
+ label: string;
343
+ type: string;
344
+ /** Occurrence index of this id within the artboard — matches the resolver's
345
+ * `querySelectorAll([data-dc-screen=…] [data-cd-id=…])[index]`. */
346
+ index: number;
347
+ children: LayerNode[];
348
+ }
349
+
350
+ function kebabToTitle(s: string): string {
351
+ return s
352
+ .split('-')
353
+ .filter(Boolean)
354
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
355
+ .join(' ');
356
+ }
357
+
358
+ // Human label: data-dc-element (Title Case) → aria-label → role → tag(.class).
359
+ function layerLabel(el: Element): string {
360
+ const dc = el.getAttribute('data-dc-element');
361
+ if (dc) return kebabToTitle(dc);
362
+ const aria = el.getAttribute('aria-label');
363
+ if (aria) return aria.slice(0, 40);
364
+ const role = el.getAttribute('role');
365
+ if (role) return role;
366
+ const tag = el.tagName.toLowerCase();
367
+ const cls = realClasses(el).split(/\s+/).filter(Boolean)[0];
368
+ return cls ? `${tag}.${cls}` : tag;
369
+ }
370
+
371
+ function layerType(el: Element): string {
372
+ const tag = el.tagName.toLowerCase();
373
+ const role = el.getAttribute('role') ?? '';
374
+ const dc = el.getAttribute('data-dc-element') ?? '';
375
+ if (tag === 'button' || role === 'button' || /(^|-)(btn|cta|button)(-|$)/.test(dc))
376
+ return 'button';
377
+ if (/^h[1-6]$/.test(tag) || role === 'heading') return 'heading';
378
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') return 'input';
379
+ if (tag === 'img' || tag === 'svg' || tag === 'picture' || tag === 'video') return 'image';
380
+ if (tag === 'a') return 'link';
381
+ if (tag === 'ul' || tag === 'ol' || tag === 'li') return 'list';
382
+ if (tag === 'nav') return 'nav';
383
+ if (tag === 'form') return 'form';
384
+ if (tag === 'p' || tag === 'span' || tag === 'label') return 'text';
385
+ return 'box';
386
+ }
387
+
388
+ function serializeArtboardTree(root: Element): LayerNode[] {
389
+ const seen = new Map<string, number>();
390
+ function walk(el: Element): LayerNode[] {
391
+ const out: LayerNode[] = [];
392
+ for (const child of Array.from(el.children)) {
393
+ // Skip dev-server overlay chrome (pins, halos) that lives inside the artboard.
394
+ if (child.closest('.dgn-pin, .dc-cv-halo, .dc-cv-group-bbox')) continue;
395
+ const cd = child.getAttribute('data-cd-id');
396
+ if (cd) {
397
+ const idx = seen.get(cd) ?? 0;
398
+ seen.set(cd, idx + 1);
399
+ out.push({
400
+ id: cd,
401
+ tag: child.tagName.toLowerCase(),
402
+ label: layerLabel(child),
403
+ type: layerType(child),
404
+ index: idx,
405
+ children: walk(child),
406
+ });
407
+ } else {
408
+ // Unstamped wrapper — descend so its stamped descendants still surface.
409
+ out.push(...walk(child));
410
+ }
411
+ }
412
+ return out;
413
+ }
414
+ return walk(root);
415
+ }
416
+
417
+ function postLayersTree(artboardId: string | null | undefined): void {
418
+ if (typeof document === 'undefined' || !artboardId) return;
419
+ const root = document.querySelector(`[data-dc-screen="${CSS.escape(artboardId)}"]`);
420
+ if (!root) return;
421
+ try {
422
+ window.parent.postMessage(
423
+ { dgn: 'layers-tree', artboardId, tree: serializeArtboardTree(root) },
424
+ '*'
425
+ );
426
+ } catch {
427
+ /* detached / cross-origin */
428
+ }
429
+ }
430
+
325
431
  // ─────────────────────────────────────────────────────────────────────────────
326
432
  // Shell
327
433
 
@@ -434,7 +540,7 @@ function CanvasCore({
434
540
  if (!host) return;
435
541
  const onDbl = (e: MouseEvent) => {
436
542
  const t = e.target as Element | null;
437
- if (!t || !t.closest) return;
543
+ if (!t?.closest) return;
438
544
  // Floating chrome / overlays / drawn user content / any artboard
439
545
  // surface → leave alone. Only dblclick that lands on the canvas
440
546
  // background outside every artboard triggers `fit()`.
@@ -517,7 +623,7 @@ function CanvasCore({
517
623
  useEffect(() => {
518
624
  if (!collab) return;
519
625
  const first = selSet.selected[0];
520
- if (!first || !first.selector) {
626
+ if (!first?.selector) {
521
627
  collab.publishAwareness({ selection: null });
522
628
  return;
523
629
  }
@@ -947,7 +1053,7 @@ function applyArtboardFollowers(): void {
947
1053
  function buildRegistry(deps: {
948
1054
  controller: ViewportControllerHandle | null;
949
1055
  clearSelection: () => void;
950
- selSet: { selected: Selection[] };
1056
+ selSet: { selected: Selection[]; replace: (s: Selection | Selection[]) => void };
951
1057
  distributeArtboards: (axis: 'x' | 'y') => void;
952
1058
  alignArtboards: (mode: 'left' | 'right' | 'center-x' | 'top' | 'bottom' | 'center-y') => void;
953
1059
  focusArtboard: (artboardId: string) => void;
@@ -1109,9 +1215,24 @@ function buildRegistry(deps: {
1109
1215
  id: 'inspect',
1110
1216
  label: 'Inspect',
1111
1217
  shortcut: '⌥I',
1112
- disabled: true,
1113
- onSelect: () => {
1114
- console.warn('[context-menu] TODO: tweaks panel for TSX canvases');
1218
+ onSelect: (target) => {
1219
+ // Phase 12 — select the right-clicked element + open the shell's
1220
+ // right Inspector panel (Inspect / Layers / CSS). Was a disabled TODO
1221
+ // before the inspector shipped.
1222
+ if (target.el) {
1223
+ selSet.replace(
1224
+ hoverTargetToSelection({
1225
+ el: target.el,
1226
+ cdId: target.cdId ?? null,
1227
+ artboardId: target.artboardId ?? null,
1228
+ } as HoverTarget)
1229
+ );
1230
+ }
1231
+ try {
1232
+ window.parent.postMessage({ dgn: 'open-inspector' }, '*');
1233
+ } catch {
1234
+ /* detached / cross-origin */
1235
+ }
1115
1236
  },
1116
1237
  },
1117
1238
  ],
@@ -1248,6 +1369,8 @@ function CanvasRouter({
1248
1369
  const annotSel = useAnnotationSelection();
1249
1370
  const ctxMenu = useContextMenu();
1250
1371
  const undoStack = useUndoStack();
1372
+ // Shell View-menu zoom bridge (dgn:'zoom') — same controller the zoom pill uses.
1373
+ const zoomController = useViewportControllerContext();
1251
1374
 
1252
1375
  // Hover state drives the floating .dc-cv-halo--hover overlay. The overlay
1253
1376
  // itself reads getBoundingClientRect on every rAF tick to follow pan/zoom.
@@ -1312,6 +1435,87 @@ function CanvasRouter({
1312
1435
  void undoStack.redo();
1313
1436
  return;
1314
1437
  }
1438
+ // Phase 12 Task 4 (DDR-103) — Layers-tree round-trip. select-by-id resolves
1439
+ // the node + replaces the selection (which posts select-set back, updating
1440
+ // the Inspect/CSS tabs + halos); highlight shows the transient hover halo;
1441
+ // request-layers re-walks + posts the tree for an artboard.
1442
+ if (m.dgn === 'select-by-id') {
1443
+ const mm = m as { id?: string; artboardId?: string | null; index?: number };
1444
+ if (mm.id) {
1445
+ const target = resolveSelectionEl(document, {
1446
+ id: mm.id,
1447
+ artboardId: mm.artboardId,
1448
+ index: mm.index,
1449
+ });
1450
+ if (target) {
1451
+ selSet.replace(
1452
+ hoverTargetToSelection({
1453
+ el: target,
1454
+ cdId: mm.id,
1455
+ artboardId: mm.artboardId ?? null,
1456
+ } as HoverTarget)
1457
+ );
1458
+ (target as HTMLElement).scrollIntoView?.({ block: 'nearest', behavior: 'smooth' });
1459
+ }
1460
+ }
1461
+ return;
1462
+ }
1463
+ if (m.dgn === 'highlight') {
1464
+ const mm = m as { id?: string; artboardId?: string | null; index?: number };
1465
+ setHoverEl(
1466
+ mm.id
1467
+ ? resolveSelectionEl(document, {
1468
+ id: mm.id,
1469
+ artboardId: mm.artboardId,
1470
+ index: mm.index,
1471
+ })
1472
+ : null
1473
+ );
1474
+ return;
1475
+ }
1476
+ // Phase 12.3 (W1.1) — optimistic inline-style apply. The CSS panel posts
1477
+ // this on commit so the selected element updates INSTANTLY, before the
1478
+ // edit-css → file-watcher HMR reload re-renders from source. `value` null/
1479
+ // empty removes the property (the reset path). Purely a live-DOM preview;
1480
+ // the authoritative value is whatever the reload renders from source.
1481
+ // SECURITY (ethical-hacker A1/A2) — accept apply-style ONLY from the parent
1482
+ // shell, never from a canvas self-post. The canvas iframe is untrusted
1483
+ // (DDR-054); without this gate hostile canvas JS could repaint elements AND
1484
+ // (paired with the reload echo-guard) suppress reloads to desync view/source.
1485
+ // The reload-suppression timestamp now lives in _shell.html's closure (NOT a
1486
+ // canvas-writable window global) — set there from the same parent-gated msg.
1487
+ if (m.dgn === 'apply-style') {
1488
+ if (e.source !== window.parent) return;
1489
+ const mm = m as {
1490
+ id?: string;
1491
+ artboardId?: string | null;
1492
+ index?: number;
1493
+ prop?: string;
1494
+ value?: string | null;
1495
+ };
1496
+ if (mm.id && mm.prop) {
1497
+ const target = resolveSelectionEl(document, {
1498
+ id: mm.id,
1499
+ artboardId: mm.artboardId,
1500
+ index: mm.index,
1501
+ });
1502
+ if (target) {
1503
+ const style = (target as HTMLElement).style;
1504
+ try {
1505
+ // Live-DOM preview only; the reload renders the authoritative value.
1506
+ if (mm.value == null || mm.value === '') style.removeProperty(mm.prop);
1507
+ else style.setProperty(mm.prop, mm.value);
1508
+ } catch {
1509
+ /* invalid prop/value — ignore; the reload is the source of truth */
1510
+ }
1511
+ }
1512
+ }
1513
+ return;
1514
+ }
1515
+ if (m.dgn === 'request-layers') {
1516
+ postLayersTree((m as { artboardId?: string }).artboardId ?? null);
1517
+ return;
1518
+ }
1315
1519
  // D9 — canvas-shell chrome follows the Maude chrome theme. The chrome's
1316
1520
  // `--maude-chrome-*` token family is keyed by `data-maude-theme` on the
1317
1521
  // iframe documentElement (see HUD_TOKENS_CSS). This attribute is
@@ -1325,10 +1529,101 @@ function CanvasRouter({
1325
1529
  }
1326
1530
  return;
1327
1531
  }
1532
+ // Shell View-menu zoom items — route to the live viewport controller
1533
+ // (the same methods the in-canvas zoom pill calls).
1534
+ if (m.dgn === 'zoom' && zoomController) {
1535
+ const op = (m as { op?: string }).op;
1536
+ if (op === 'in') zoomController.zoomIn();
1537
+ else if (op === 'out') zoomController.zoomOut();
1538
+ else if (op === 'fit') zoomController.fit();
1539
+ else if (op === 'actual') zoomController.reset();
1540
+ return;
1541
+ }
1328
1542
  };
1329
1543
  window.addEventListener('message', onMessage);
1330
1544
  return () => window.removeEventListener('message', onMessage);
1331
- }, [selSet, annotSel, setTool, undoStack]);
1545
+ }, [selSet, annotSel, setTool, undoStack, zoomController]);
1546
+
1547
+ // Phase 12 (DDR-103) — double-click a LEAF-TEXT element (children all text
1548
+ // nodes) to edit its copy in place. Commit (blur / Enter) posts `dgn:edit-text`
1549
+ // to the shell, which calls the main-origin /_api/edit-text → editText writes
1550
+ // the escaped JSXText to source; the file-watcher HMR reload re-renders from
1551
+ // the persisted source. Esc restores the original. Mixed/expression elements
1552
+ // are skipped (the engine would refuse them anyway — honest no-op).
1553
+ useEffect(() => {
1554
+ if (typeof document === 'undefined') return;
1555
+ const host = hostRef.current;
1556
+ if (!host) return;
1557
+ let editing: HTMLElement | null = null;
1558
+ let original = '';
1559
+ const onBlur = (): void => commitEdit(true);
1560
+ const onKey = (e: KeyboardEvent): void => {
1561
+ if (e.key === 'Escape') {
1562
+ e.preventDefault();
1563
+ commitEdit(false);
1564
+ } else if (e.key === 'Enter' && !e.shiftKey) {
1565
+ e.preventDefault();
1566
+ (e.currentTarget as HTMLElement | null)?.blur?.();
1567
+ }
1568
+ };
1569
+ function teardown(elx: HTMLElement): void {
1570
+ elx.removeAttribute('contenteditable');
1571
+ elx.classList.remove('dc-text-editing');
1572
+ elx.removeEventListener('blur', onBlur, true);
1573
+ elx.removeEventListener('keydown', onKey, true);
1574
+ }
1575
+ function commitEdit(commit: boolean): void {
1576
+ const elx = editing;
1577
+ if (!elx) return;
1578
+ editing = null;
1579
+ const text = (elx.textContent ?? '').trim();
1580
+ teardown(elx);
1581
+ if (!commit) {
1582
+ elx.textContent = original;
1583
+ return;
1584
+ }
1585
+ if (text === original.trim()) return;
1586
+ const cdId = elx.getAttribute('data-cd-id');
1587
+ if (!cdId) return;
1588
+ try {
1589
+ window.parent.postMessage({ dgn: 'edit-text', id: cdId, file: deriveFile(), text }, '*');
1590
+ } catch {
1591
+ /* detached / cross-origin */
1592
+ }
1593
+ }
1594
+ const onDbl = (e: MouseEvent): void => {
1595
+ if (editing) return;
1596
+ const t = e.target as HTMLElement | null;
1597
+ const stamped = (t?.closest?.('[data-cd-id]') as HTMLElement | null) ?? null;
1598
+ if (!stamped) return;
1599
+ const kids = Array.from(stamped.childNodes);
1600
+ if (kids.length === 0 || !kids.every((n) => n.nodeType === 3)) return; // leaf-text only
1601
+ e.preventDefault();
1602
+ e.stopPropagation();
1603
+ editing = stamped;
1604
+ original = stamped.textContent ?? '';
1605
+ stamped.setAttribute('contenteditable', 'plaintext-only');
1606
+ stamped.classList.add('dc-text-editing');
1607
+ stamped.addEventListener('blur', onBlur, true);
1608
+ stamped.addEventListener('keydown', onKey, true);
1609
+ stamped.focus();
1610
+ try {
1611
+ const range = document.createRange();
1612
+ range.selectNodeContents(stamped);
1613
+ const sel = window.getSelection();
1614
+ sel?.removeAllRanges();
1615
+ sel?.addRange(range);
1616
+ } catch {
1617
+ /* selection API unavailable */
1618
+ }
1619
+ };
1620
+ // Capture phase so we beat the fit-to-view dblclick handler.
1621
+ host.addEventListener('dblclick', onDbl, true);
1622
+ return () => {
1623
+ host.removeEventListener('dblclick', onDbl, true);
1624
+ if (editing) teardown(editing);
1625
+ };
1626
+ }, [hostRef]);
1332
1627
 
1333
1628
  // Cleanup any pending rAF on unmount.
1334
1629
  useEffect(
@@ -1363,6 +1658,8 @@ function CanvasRouter({
1363
1658
  const sel = hoverTargetToSelection(target);
1364
1659
  if (mode === 'replace') selSet.replace(sel);
1365
1660
  else selSet.add(sel);
1661
+ // Phase 12 Task 4 — refresh the Layers tree for the selected artboard.
1662
+ postLayersTree(target.artboardId);
1366
1663
  },
1367
1664
  onContextMenu: ({ clientX, clientY }) => {
1368
1665
  const target = resolveHoverTarget(document, clientX, clientY, { deep: true });
@@ -1754,7 +2051,7 @@ function HoverHalo({ el }: { el: Element | null }) {
1754
2051
  rafRef.current = null;
1755
2052
  const div = ref.current;
1756
2053
  const t = targetRef.current;
1757
- if (!div || !t || !t.isConnected) {
2054
+ if (!div || !t?.isConnected) {
1758
2055
  if (div) div.style.display = 'none';
1759
2056
  return;
1760
2057
  }