@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.
- package/README.md +1 -1
- package/apps/studio/ai-banner.tsx +0 -1
- package/apps/studio/annotations-align.ts +149 -0
- package/apps/studio/annotations-bindings.ts +197 -0
- package/apps/studio/annotations-context-toolbar.tsx +436 -186
- package/apps/studio/annotations-groups.ts +270 -0
- package/apps/studio/annotations-layer.tsx +1708 -1660
- package/apps/studio/annotations-model.ts +2077 -0
- package/apps/studio/annotations-snap.ts +125 -0
- package/apps/studio/api.ts +208 -116
- package/apps/studio/bin/_png-playwright.mjs +1 -1
- package/apps/studio/bin/annotate.mjs +732 -0
- package/apps/studio/bin/annotate.sh +17 -0
- package/apps/studio/bin/read-annotations.mjs +152 -17
- package/apps/studio/build.ts +1 -1
- package/apps/studio/canvas-arrowheads.ts +78 -9
- package/apps/studio/canvas-cursors.ts +2 -0
- package/apps/studio/canvas-edit.ts +257 -7
- package/apps/studio/canvas-icons.tsx +105 -0
- package/apps/studio/canvas-shell.tsx +305 -8
- package/apps/studio/client/app.jsx +2812 -238
- package/apps/studio/client/index.html +1 -1
- package/apps/studio/client/styles/3-shell-maude.css +713 -11
- package/apps/studio/client/styles/4-components.css +39 -0
- package/apps/studio/client/styles/5-maude-overrides.css +16 -2
- package/apps/studio/client/tour/overlay.jsx +105 -17
- package/apps/studio/client/tour/usage-tour.js +23 -10
- package/apps/studio/commands/annotation-strokes-command.ts +1 -1
- package/apps/studio/commands/move-artboards-command.ts +1 -1
- package/apps/studio/comments-overlay.tsx +7 -5
- package/apps/studio/context-menu.tsx +4 -3
- package/apps/studio/contextual-toolbar.tsx +14 -0
- package/apps/studio/cursors-overlay.tsx +20 -20
- package/apps/studio/dist/client.bundle.js +3322 -602
- package/apps/studio/dist/comment-mount.js +95 -9
- package/apps/studio/dist/styles.css +2490 -167
- package/apps/studio/dom-selection.ts +115 -0
- package/apps/studio/export-dialog.tsx +3 -3
- package/apps/studio/history.ts +20 -3
- package/apps/studio/http.ts +124 -5
- package/apps/studio/input-router.tsx +7 -0
- package/apps/studio/inspect.ts +37 -8
- package/apps/studio/participants-chrome.tsx +1 -7
- package/apps/studio/server.ts +5 -2
- package/apps/studio/sync/agent.ts +231 -57
- package/apps/studio/sync/codec.ts +45 -0
- package/apps/studio/sync/cold-start.ts +158 -0
- package/apps/studio/sync/connection-state.ts +58 -2
- package/apps/studio/sync/index.ts +563 -221
- package/apps/studio/sync/journal.ts +190 -0
- package/apps/studio/sync/migrate-seed.ts +202 -44
- package/apps/studio/sync/projection.ts +22 -3
- package/apps/studio/sync/status.ts +15 -4
- package/apps/studio/test/activity.test.ts +1 -7
- package/apps/studio/test/annotate-write.test.ts +184 -0
- package/apps/studio/test/annotations-align.test.ts +88 -0
- package/apps/studio/test/annotations-bindings.test.ts +124 -0
- package/apps/studio/test/annotations-groups.test.ts +231 -0
- package/apps/studio/test/annotations-snap.test.ts +79 -0
- package/apps/studio/test/canvas-edit.test.ts +181 -1
- package/apps/studio/test/canvas-origin-gate.test.ts +8 -0
- package/apps/studio/test/collab-bridge.test.ts +0 -2
- package/apps/studio/test/collab-room.test.ts +2 -7
- package/apps/studio/test/csp-canvas-shell.test.ts +5 -0
- package/apps/studio/test/csrf-write-guard.test.ts +52 -0
- package/apps/studio/test/figjam-v3-model.test.ts +342 -0
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
- package/apps/studio/test/shared-doc-migrate.test.ts +224 -14
- package/apps/studio/test/sync-agent.test.ts +272 -2
- package/apps/studio/test/sync-codec.test.ts +65 -0
- package/apps/studio/test/sync-cold-start.test.ts +244 -0
- package/apps/studio/test/sync-connection-state.test.ts +68 -0
- package/apps/studio/test/sync-hardening.test.ts +0 -8
- package/apps/studio/test/sync-hubs-config.test.ts +0 -1
- package/apps/studio/test/sync-incident-replay.test.ts +211 -0
- package/apps/studio/test/sync-journal.test.ts +176 -0
- package/apps/studio/test/sync-runtime.test.ts +360 -5
- package/apps/studio/test/sync-status.test.ts +66 -0
- package/apps/studio/test/use-annotation-resize.test.ts +159 -0
- package/apps/studio/test/use-artboard-drag.test.ts +0 -1
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-tool-mode.test.tsx +5 -2
- package/apps/studio/tool-palette.tsx +12 -2
- package/apps/studio/undo-hud.tsx +0 -1
- package/apps/studio/use-agent-presence.tsx +4 -2
- package/apps/studio/use-annotation-resize.tsx +484 -54
- package/apps/studio/use-annotation-selection.tsx +16 -4
- package/apps/studio/use-canvas-activity.tsx +2 -2
- package/apps/studio/use-collab.tsx +20 -15
- package/apps/studio/use-selection-set.tsx +9 -0
- package/apps/studio/use-tool-mode.tsx +5 -1
- package/apps/studio/whats-new.json +135 -10
- package/apps/studio/whats-new.schema.json +18 -2
- package/cli/commands/design-link.test.mjs +84 -0
- package/cli/commands/design.mjs +2 -1
- package/cli/commands/design.test.mjs +5 -1
- package/cli/lib/design-link.mjs +51 -1
- package/package.json +8 -8
- 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
|
|
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
|
|
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
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
|
2054
|
+
if (!div || !t?.isConnected) {
|
|
1758
2055
|
if (div) div.style.display = 'none';
|
|
1759
2056
|
return;
|
|
1760
2057
|
}
|