@1agh/maude 0.39.1 → 0.41.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 +2 -2
- package/apps/studio/annotations-context-toolbar.tsx +44 -1
- package/apps/studio/annotations-layer.tsx +306 -5
- package/apps/studio/annotations-model.ts +107 -6
- package/apps/studio/api.ts +1316 -67
- package/apps/studio/artboard-marquee.tsx +1 -1
- package/apps/studio/bin/_canvas-rects-playwright.mjs +55 -0
- package/apps/studio/bin/_canvas-rects-static.mjs +166 -0
- package/apps/studio/bin/_fetch-asset.mjs +556 -0
- package/apps/studio/bin/_html-playwright.mjs +9 -1
- package/apps/studio/bin/_pdf-playwright.mjs +8 -1
- package/apps/studio/bin/_png-playwright.mjs +8 -1
- package/apps/studio/bin/_pw-launch.mjs +54 -0
- package/apps/studio/bin/_svg-playwright.mjs +8 -1
- package/apps/studio/bin/_video-playwright.mjs +452 -0
- package/apps/studio/bin/annotate.mjs +576 -34
- package/apps/studio/bin/canvas-rects.sh +152 -0
- package/apps/studio/bin/fetch-asset.sh +34 -0
- package/apps/studio/bin/prep.sh +8 -1
- package/apps/studio/bin/read-annotations.mjs +138 -7
- package/apps/studio/bin/smoke.sh +42 -6
- package/apps/studio/build.ts +21 -0
- package/apps/studio/canvas-comment-mount.tsx +138 -4
- package/apps/studio/canvas-edit.ts +2625 -111
- package/apps/studio/canvas-lib.tsx +238 -2
- package/apps/studio/canvas-list-watch.ts +6 -2
- package/apps/studio/canvas-shell.tsx +514 -20
- package/apps/studio/client/app.jsx +2480 -52
- package/apps/studio/client/comments-overlay.css +130 -126
- package/apps/studio/client/github.js +15 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +860 -0
- package/apps/studio/client/panels/timeline-parse.js +229 -0
- package/apps/studio/client/panels/timeline-snap.js +55 -0
- package/apps/studio/client/styles/3-shell-maude.css +155 -0
- package/apps/studio/comments-overlay.tsx +148 -41
- package/apps/studio/config.schema.json +14 -0
- package/apps/studio/context-menu.tsx +16 -6
- package/apps/studio/context.ts +113 -1
- package/apps/studio/contextual-toolbar.tsx +262 -4
- package/apps/studio/cursors-overlay.tsx +4 -4
- package/apps/studio/dist/client.bundle.js +89 -17
- package/apps/studio/dist/comment-mount.js +59 -1
- package/apps/studio/dist/runtime/.min-sizes.json +11 -1
- package/apps/studio/dist/runtime/@remotion_media.js +491 -0
- package/apps/studio/dist/runtime/@remotion_player.js +56 -0
- package/apps/studio/dist/runtime/@remotion_transitions.js +300 -0
- package/apps/studio/dist/runtime/@remotion_transitions_clock-wipe.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_fade.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_flip.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_none.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_slide.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_wipe.js +1 -0
- package/apps/studio/dist/runtime/REMOTION-LICENSE.md +28 -0
- package/apps/studio/dist/runtime/remotion.js +42 -0
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +127 -1
- package/apps/studio/drag-state.ts +24 -0
- package/apps/studio/equal-spacing-detector.ts +205 -0
- package/apps/studio/export-dialog.tsx +1 -1
- package/apps/studio/exporters/_browser-bundles.ts +117 -0
- package/apps/studio/exporters/_runtime.ts +69 -0
- package/apps/studio/exporters/html.ts +4 -3
- package/apps/studio/exporters/index.ts +28 -2
- package/apps/studio/exporters/pdf.ts +4 -3
- package/apps/studio/exporters/png.ts +5 -3
- package/apps/studio/exporters/pptx.ts +6 -4
- package/apps/studio/exporters/svg.ts +9 -5
- package/apps/studio/exporters/video-encode-lib.ts +200 -0
- package/apps/studio/exporters/video-render-lib.ts +108 -0
- package/apps/studio/exporters/video.ts +184 -0
- package/apps/studio/history.ts +47 -1
- package/apps/studio/http.ts +758 -27
- package/apps/studio/input-router.tsx +19 -1
- package/apps/studio/marquee-overlay.tsx +1 -1
- package/apps/studio/measure-overlay.tsx +241 -0
- package/apps/studio/participants-chrome.tsx +3 -3
- package/apps/studio/runtime-bundle.ts +30 -0
- package/apps/studio/server.ts +34 -9
- package/apps/studio/sizing-mode.ts +117 -0
- package/apps/studio/spacing-handles.ts +166 -0
- package/apps/studio/test/annotate-write.test.ts +890 -0
- package/apps/studio/test/annotations-roundtrip.test.ts +47 -0
- package/apps/studio/test/camera-reveal.test.tsx +173 -0
- package/apps/studio/test/canvas-create-api.test.ts +76 -0
- package/apps/studio/test/canvas-edit.test.ts +140 -0
- package/apps/studio/test/canvas-list-watch.test.ts +49 -0
- package/apps/studio/test/canvas-media-drop.test.ts +30 -1
- package/apps/studio/test/canvas-origin-gate.test.ts +54 -0
- package/apps/studio/test/canvas-rects.test.ts +198 -0
- package/apps/studio/test/clip-addressing.test.ts +732 -0
- package/apps/studio/test/comments-overlay.test.ts +117 -0
- package/apps/studio/test/config-reload.test.ts +230 -0
- package/apps/studio/test/dns-rebinding-guard.test.ts +74 -0
- package/apps/studio/test/dom-selection.test.ts +130 -0
- package/apps/studio/test/edit-css-occurrence.test.ts +81 -0
- package/apps/studio/test/edit-persistence.test.ts +91 -0
- package/apps/studio/test/edit-scope-api.test.ts +115 -0
- package/apps/studio/test/element-resize.test.ts +136 -0
- package/apps/studio/test/element-structural-api.test.ts +360 -0
- package/apps/studio/test/element-structural-edit.test.ts +233 -0
- package/apps/studio/test/equal-spacing-detector.test.ts +165 -1
- package/apps/studio/test/exporters/runtime.test.ts +59 -0
- package/apps/studio/test/file-lock.test.ts +84 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +82 -0
- package/apps/studio/test/history-rollback.test.ts +26 -0
- package/apps/studio/test/knob-props-authored.test.ts +87 -0
- package/apps/studio/test/read-annotations.test.ts +154 -0
- package/apps/studio/test/sizing-mode.test.ts +102 -0
- package/apps/studio/test/spacing-handles.test.ts +138 -0
- package/apps/studio/test/specimen-select.test.ts +88 -0
- package/apps/studio/test/timeline-parse.test.ts +127 -0
- package/apps/studio/test/timeline-snap.test.ts +85 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +84 -0
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +211 -0
- package/apps/studio/test/video-asset.test.ts +163 -0
- package/apps/studio/test/video-comp-fixture.test.ts +50 -0
- package/apps/studio/test/video-comp.test.ts +168 -0
- package/apps/studio/test/video-render-bridge.test.ts +149 -0
- package/apps/studio/tool-palette.tsx +122 -2
- package/apps/studio/undo-hud.tsx +2 -2
- package/apps/studio/use-annotation-resize.tsx +6 -3
- package/apps/studio/use-canvas-media-drop.tsx +66 -4
- package/apps/studio/use-element-resize.tsx +732 -0
- package/apps/studio/use-keyboard-discipline.tsx +205 -15
- package/apps/studio/use-selection-set.tsx +14 -0
- package/apps/studio/use-spacing-handles.tsx +388 -0
- package/apps/studio/video-comp.tsx +444 -0
- package/apps/studio/whats-new.json +55 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/commands/design.mjs +6 -1
- package/cli/commands/design.test.mjs +49 -1
- package/cli/lib/fetch-asset.test.mjs +213 -0
- package/package.json +8 -8
- package/plugins/design/dependencies.json +10 -2
- package/plugins/design/templates/_shell.html +25 -2
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +1 -1
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
selectorIndex,
|
|
77
77
|
shortText,
|
|
78
78
|
} from './dom-selection.ts';
|
|
79
|
+
import { setElementDragActive } from './drag-state.ts';
|
|
79
80
|
import { EqualSpacingHandles } from './equal-spacing-handles.tsx';
|
|
80
81
|
import { ExportDialogProvider } from './export-dialog.tsx';
|
|
81
82
|
import {
|
|
@@ -85,6 +86,7 @@ import {
|
|
|
85
86
|
useInputRouter,
|
|
86
87
|
} from './input-router.tsx';
|
|
87
88
|
import { ElementMarqueeOverlay } from './marquee-overlay.tsx';
|
|
89
|
+
import { MeasureOverlay } from './measure-overlay.tsx';
|
|
88
90
|
import { ParticipantsChrome } from './participants-chrome.tsx';
|
|
89
91
|
import { ToolPalette } from './tool-palette.tsx';
|
|
90
92
|
import { UndoHud } from './undo-hud.tsx';
|
|
@@ -94,15 +96,18 @@ import {
|
|
|
94
96
|
useAnnotationSelectionOptional,
|
|
95
97
|
} from './use-annotation-selection.tsx';
|
|
96
98
|
import { AnnotationsVisibilityProvider } from './use-annotations-visibility.tsx';
|
|
99
|
+
import { showCanvasToast } from './use-canvas-media-drop.tsx';
|
|
97
100
|
import { ChromeVisibilityProvider, useChromeVisibility } from './use-chrome-visibility.tsx';
|
|
98
101
|
import { useCollab } from './use-collab.tsx';
|
|
99
102
|
import { useCursorModifiers } from './use-cursor-modifiers.tsx';
|
|
103
|
+
import { ElementResizeOverlay } from './use-element-resize.tsx';
|
|
100
104
|
import { useKeyboardDiscipline } from './use-keyboard-discipline.tsx';
|
|
101
105
|
import {
|
|
102
106
|
MaybeSelectionSetProvider,
|
|
103
107
|
type Selection,
|
|
104
108
|
useSelectionSet,
|
|
105
109
|
} from './use-selection-set.tsx';
|
|
110
|
+
import { SpacingHandlesOverlay } from './use-spacing-handles.tsx';
|
|
106
111
|
import { useToolMode } from './use-tool-mode.tsx';
|
|
107
112
|
import { useUndoSinks, useUndoStack } from './use-undo-stack.tsx';
|
|
108
113
|
|
|
@@ -381,6 +386,11 @@ interface LayerNode {
|
|
|
381
386
|
/** Occurrence index of this id within the artboard — matches the resolver's
|
|
382
387
|
* `querySelectorAll([data-dc-screen=…] [data-cd-id=…])[index]`. */
|
|
383
388
|
index: number;
|
|
389
|
+
/** Authored `style.display === 'none'` — the Layers-panel eye icon's source
|
|
390
|
+
* of truth. The panel's toggle persists through `/_api/edit-css` (same
|
|
391
|
+
* route CssKnobs uses), so this must reflect the SOURCE value, not a
|
|
392
|
+
* client-only preview flag (see app.jsx `toggleVisibility`). */
|
|
393
|
+
hidden: boolean;
|
|
384
394
|
children: LayerNode[];
|
|
385
395
|
}
|
|
386
396
|
|
|
@@ -422,6 +432,73 @@ function layerType(el: Element): string {
|
|
|
422
432
|
return 'box';
|
|
423
433
|
}
|
|
424
434
|
|
|
435
|
+
// Task L6 — the stamped-ancestor chain from an artboard body down to `el`
|
|
436
|
+
// (outermost first), mirroring `resolveHoverTarget`'s "top mode" climb
|
|
437
|
+
// (input-router.tsx) but returning every rung instead of only the outermost —
|
|
438
|
+
// deep-select needs the whole ladder to know what "one level deeper" means.
|
|
439
|
+
function stampedChainToBody(el: Element): Element[] {
|
|
440
|
+
const bodyEl = el.closest('.dc-artboard-body');
|
|
441
|
+
const chain: Element[] = [];
|
|
442
|
+
let cur: Element | null = el;
|
|
443
|
+
while (cur && cur !== bodyEl) {
|
|
444
|
+
if (cur.hasAttribute('data-cd-id')) chain.push(cur);
|
|
445
|
+
cur = cur.parentElement;
|
|
446
|
+
}
|
|
447
|
+
return chain.reverse();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Task L6 — "Select layer" submenu: every DISTINCT `[data-cd-id]` ancestor in
|
|
451
|
+
// the full hit-stack under the cursor (topmost/frontmost first, matching
|
|
452
|
+
// Layers-panel order), so overlapping/deeply-nested elements a single
|
|
453
|
+
// elementFromPoint pick can't reach become individually selectable.
|
|
454
|
+
// `elementsFromPoint` (unlike `elementFromPoint`) walks the WHOLE z-order
|
|
455
|
+
// stack — including non-ancestor siblings visually underneath the frontmost
|
|
456
|
+
// hit — so this also disambiguates true overlaps, not just nesting depth.
|
|
457
|
+
function buildSelectLayerSubmenu(
|
|
458
|
+
target: ContextTarget,
|
|
459
|
+
selSet: { replace: (s: Selection | Selection[]) => void }
|
|
460
|
+
): MenuItem[] {
|
|
461
|
+
if (typeof document === 'undefined' || typeof document.elementsFromPoint !== 'function') {
|
|
462
|
+
return [];
|
|
463
|
+
}
|
|
464
|
+
const hits = document.elementsFromPoint(target.clientX, target.clientY);
|
|
465
|
+
const seen = new Set<Element>();
|
|
466
|
+
const candidates: Element[] = [];
|
|
467
|
+
for (const hit of hits) {
|
|
468
|
+
const stamped = hit.closest?.('[data-cd-id]') ?? null;
|
|
469
|
+
if (!stamped || seen.has(stamped)) continue;
|
|
470
|
+
seen.add(stamped);
|
|
471
|
+
candidates.push(stamped);
|
|
472
|
+
}
|
|
473
|
+
if (candidates.length === 0) {
|
|
474
|
+
return [
|
|
475
|
+
{
|
|
476
|
+
id: 'select-layer-empty',
|
|
477
|
+
label: 'No stamped elements here',
|
|
478
|
+
disabled: true,
|
|
479
|
+
onSelect: () => {},
|
|
480
|
+
},
|
|
481
|
+
];
|
|
482
|
+
}
|
|
483
|
+
return candidates.map((el, i) => {
|
|
484
|
+
const cdId = el.getAttribute('data-cd-id') ?? '';
|
|
485
|
+
const artboardId =
|
|
486
|
+
(el.closest('[data-dc-screen]') as HTMLElement | null)?.getAttribute('data-dc-screen') ??
|
|
487
|
+
null;
|
|
488
|
+
return {
|
|
489
|
+
id: `select-layer-${i}`,
|
|
490
|
+
label: `${layerLabel(el)} · ${layerType(el)}`,
|
|
491
|
+
onSelect: () => {
|
|
492
|
+
// `hoverTargetToSelection` computes the occurrence index itself
|
|
493
|
+
// (`selectorIndex` against the scoped selector) — no need to pass one.
|
|
494
|
+
selSet.replace(
|
|
495
|
+
hoverTargetToSelection({ el, cdId: cdId || null, artboardId } as HoverTarget)
|
|
496
|
+
);
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
425
502
|
function serializeArtboardTree(root: Element): LayerNode[] {
|
|
426
503
|
const seen = new Map<string, number>();
|
|
427
504
|
function walk(el: Element): LayerNode[] {
|
|
@@ -439,6 +516,7 @@ function serializeArtboardTree(root: Element): LayerNode[] {
|
|
|
439
516
|
label: layerLabel(child),
|
|
440
517
|
type: layerType(child),
|
|
441
518
|
index: idx,
|
|
519
|
+
hidden: (child as HTMLElement).style.display === 'none',
|
|
442
520
|
children: walk(child),
|
|
443
521
|
});
|
|
444
522
|
} else {
|
|
@@ -1362,9 +1440,168 @@ function buildRegistry(deps: {
|
|
|
1362
1440
|
}
|
|
1363
1441
|
},
|
|
1364
1442
|
},
|
|
1443
|
+
{
|
|
1444
|
+
id: 'select-layer',
|
|
1445
|
+
label: 'Select layer',
|
|
1446
|
+
// Task L6 — every DISTINCT stamped ancestor in the cursor's full hit
|
|
1447
|
+
// stack (elementsFromPoint sees through overlapping/nested elements a
|
|
1448
|
+
// single elementFromPoint pick can't reach), Layers-order (topmost
|
|
1449
|
+
// first), so overlapping/deeply-nested TSX becomes grabbable. A
|
|
1450
|
+
// function submenu — resolved per-click against `target`, since the
|
|
1451
|
+
// candidate set depends on where the user right-clicked.
|
|
1452
|
+
//
|
|
1453
|
+
// v1 scope: no live Stage-H local/shared badge on each candidate —
|
|
1454
|
+
// that resolver is server-side/main-origin-only (`GET /_api/edit-scope`,
|
|
1455
|
+
// canvas-edit.ts), unreachable from this iframe without a new
|
|
1456
|
+
// request/reply round-trip; deferred rather than blocking this task.
|
|
1457
|
+
onSelect: () => {
|
|
1458
|
+
/* parent of a submenu — never invoked directly */
|
|
1459
|
+
},
|
|
1460
|
+
submenu: (t) => buildSelectLayerSubmenu(t, selSet),
|
|
1461
|
+
},
|
|
1462
|
+
],
|
|
1463
|
+
[
|
|
1464
|
+
{
|
|
1465
|
+
id: 'insert-element',
|
|
1466
|
+
label: 'Insert',
|
|
1467
|
+
// feature-element-editing-robustness Stage I3 — insert a synthesized
|
|
1468
|
+
// element AFTER the right-clicked one (main-origin write: request it
|
|
1469
|
+
// from the parent shell). Image insert needs an asset src (the Stage-F
|
|
1470
|
+
// AssetPicker), so only Div/Text are offered here for now.
|
|
1471
|
+
submenu: [
|
|
1472
|
+
{
|
|
1473
|
+
id: 'insert-div',
|
|
1474
|
+
label: 'Div',
|
|
1475
|
+
onSelect: (target) => {
|
|
1476
|
+
if (!target.cdId) return;
|
|
1477
|
+
try {
|
|
1478
|
+
window.parent.postMessage(
|
|
1479
|
+
{ dgn: 'insert-request', refId: target.cdId, position: 'after', kind: 'div' },
|
|
1480
|
+
'*'
|
|
1481
|
+
);
|
|
1482
|
+
} catch {
|
|
1483
|
+
/* detached */
|
|
1484
|
+
}
|
|
1485
|
+
},
|
|
1486
|
+
},
|
|
1487
|
+
{
|
|
1488
|
+
id: 'insert-text',
|
|
1489
|
+
label: 'Text',
|
|
1490
|
+
onSelect: (target) => {
|
|
1491
|
+
if (!target.cdId) return;
|
|
1492
|
+
try {
|
|
1493
|
+
window.parent.postMessage(
|
|
1494
|
+
{ dgn: 'insert-request', refId: target.cdId, position: 'after', kind: 'text' },
|
|
1495
|
+
'*'
|
|
1496
|
+
);
|
|
1497
|
+
} catch {
|
|
1498
|
+
/* detached */
|
|
1499
|
+
}
|
|
1500
|
+
},
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
id: 'insert-image',
|
|
1504
|
+
label: 'Image…',
|
|
1505
|
+
// Image needs a contained asset src — the shell opens the AssetPicker.
|
|
1506
|
+
onSelect: (target) => {
|
|
1507
|
+
if (!target.cdId) return;
|
|
1508
|
+
try {
|
|
1509
|
+
window.parent.postMessage(
|
|
1510
|
+
{ dgn: 'insert-image-request', refId: target.cdId, position: 'after' },
|
|
1511
|
+
'*'
|
|
1512
|
+
);
|
|
1513
|
+
} catch {
|
|
1514
|
+
/* detached */
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
},
|
|
1518
|
+
],
|
|
1519
|
+
onSelect: () => {},
|
|
1520
|
+
},
|
|
1521
|
+
{
|
|
1522
|
+
id: 'replace-media',
|
|
1523
|
+
label: 'Replace image…',
|
|
1524
|
+
// Stage F2 — re-point an authored <img>/<video> src. Only meaningful on
|
|
1525
|
+
// a media element; on anything else it's a no-op (the shell's edit-attr
|
|
1526
|
+
// will refuse a non-media / expression src). The Media inspector section
|
|
1527
|
+
// carries the primary, always-gated "Replace…" button.
|
|
1528
|
+
onSelect: (target) => {
|
|
1529
|
+
const tag = target.el?.tagName?.toLowerCase();
|
|
1530
|
+
if (!target.cdId || (tag !== 'img' && tag !== 'video')) return;
|
|
1531
|
+
// Read the authored src straight off the element so the shell has the
|
|
1532
|
+
// undo before-value (getAttribute is the literal content attr, not the
|
|
1533
|
+
// resolved URL).
|
|
1534
|
+
const before = target.el?.getAttribute('src') ?? null;
|
|
1535
|
+
try {
|
|
1536
|
+
window.parent.postMessage(
|
|
1537
|
+
{ dgn: 'replace-media-request', id: target.cdId, before },
|
|
1538
|
+
'*'
|
|
1539
|
+
);
|
|
1540
|
+
} catch {
|
|
1541
|
+
/* detached */
|
|
1542
|
+
}
|
|
1543
|
+
},
|
|
1544
|
+
},
|
|
1365
1545
|
],
|
|
1366
1546
|
[exportItem('export-selection', 'Export selection…', 'selection', '⌘E')],
|
|
1367
1547
|
[
|
|
1548
|
+
{
|
|
1549
|
+
id: 'duplicate-element',
|
|
1550
|
+
label: 'Duplicate',
|
|
1551
|
+
shortcut: '⌘D',
|
|
1552
|
+
// Task L3 — main-origin source duplicate; post to the parent shell.
|
|
1553
|
+
onSelect: (target) => {
|
|
1554
|
+
if (!target.cdId) return;
|
|
1555
|
+
try {
|
|
1556
|
+
window.parent.postMessage({ dgn: 'duplicate-request', id: target.cdId }, '*');
|
|
1557
|
+
} catch {
|
|
1558
|
+
/* detached / cross-origin */
|
|
1559
|
+
}
|
|
1560
|
+
},
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
id: 'copy-style',
|
|
1564
|
+
label: 'Copy style',
|
|
1565
|
+
shortcut: '⌘⌥C',
|
|
1566
|
+
// Task L4 — the shell captures the current selection's authored style.
|
|
1567
|
+
onSelect: () => {
|
|
1568
|
+
try {
|
|
1569
|
+
window.parent.postMessage({ dgn: 'copy-style' }, '*');
|
|
1570
|
+
} catch {
|
|
1571
|
+
/* detached / cross-origin */
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
id: 'paste-style',
|
|
1577
|
+
label: 'Paste style',
|
|
1578
|
+
shortcut: '⌘⌥V',
|
|
1579
|
+
onSelect: (target) => {
|
|
1580
|
+
if (!target.cdId) return;
|
|
1581
|
+
try {
|
|
1582
|
+
window.parent.postMessage({ dgn: 'paste-style', id: target.cdId }, '*');
|
|
1583
|
+
} catch {
|
|
1584
|
+
/* detached / cross-origin */
|
|
1585
|
+
}
|
|
1586
|
+
},
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
id: 'delete-element',
|
|
1590
|
+
label: 'Delete',
|
|
1591
|
+
shortcut: '⌫',
|
|
1592
|
+
destructive: true,
|
|
1593
|
+
// feature-element-editing-robustness Stage I — request a source delete.
|
|
1594
|
+
// The write is main-origin-only (DDR-054): post to the parent shell,
|
|
1595
|
+
// which performs it (pinned to the active canvas) + records undo.
|
|
1596
|
+
onSelect: (target) => {
|
|
1597
|
+
if (!target.cdId) return;
|
|
1598
|
+
try {
|
|
1599
|
+
window.parent.postMessage({ dgn: 'delete-request', id: target.cdId }, '*');
|
|
1600
|
+
} catch {
|
|
1601
|
+
/* detached / cross-origin */
|
|
1602
|
+
}
|
|
1603
|
+
},
|
|
1604
|
+
},
|
|
1368
1605
|
{
|
|
1369
1606
|
id: 'hide',
|
|
1370
1607
|
label: 'Hide',
|
|
@@ -1454,6 +1691,28 @@ function buildRegistry(deps: {
|
|
|
1454
1691
|
},
|
|
1455
1692
|
],
|
|
1456
1693
|
[themeItem],
|
|
1694
|
+
[
|
|
1695
|
+
{
|
|
1696
|
+
id: 'delete-artboard',
|
|
1697
|
+
label: 'Delete artboard',
|
|
1698
|
+
shortcut: '⌫',
|
|
1699
|
+
destructive: true,
|
|
1700
|
+
// Main-origin-only source delete (DDR-054): post to the parent shell,
|
|
1701
|
+
// which performs it (pinned to the active canvas) + records undo. The
|
|
1702
|
+
// engine refuses to delete the last artboard.
|
|
1703
|
+
onSelect: (target) => {
|
|
1704
|
+
if (!target.artboardId) return;
|
|
1705
|
+
try {
|
|
1706
|
+
window.parent.postMessage(
|
|
1707
|
+
{ dgn: 'delete-artboard-request', artboardId: target.artboardId },
|
|
1708
|
+
'*'
|
|
1709
|
+
);
|
|
1710
|
+
} catch {
|
|
1711
|
+
/* detached / cross-origin */
|
|
1712
|
+
}
|
|
1713
|
+
},
|
|
1714
|
+
},
|
|
1715
|
+
],
|
|
1457
1716
|
[exportItem('export-artboard', 'Export this artboard…', 'artboard')],
|
|
1458
1717
|
],
|
|
1459
1718
|
world: [
|
|
@@ -1477,6 +1736,63 @@ function boundsOf(el: HTMLElement) {
|
|
|
1477
1736
|
};
|
|
1478
1737
|
}
|
|
1479
1738
|
|
|
1739
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1740
|
+
// Task A1 (feature-element-editing-robustness) — camera-aware reveal.
|
|
1741
|
+
//
|
|
1742
|
+
// The `select-by-id` channel (Layers-panel click, restored/echoed selection,
|
|
1743
|
+
// post-edit reselect) used to `scrollIntoView({block:'nearest'})` to reveal the
|
|
1744
|
+
// selected element. But `.dc-canvas` / `.dc-artboard-body` are `overflow:hidden`
|
|
1745
|
+
// engine clip layers, and the camera is the `.dc-world` transform — so a host
|
|
1746
|
+
// scroll shifted the layout out from under the camera model (a phantom right
|
|
1747
|
+
// strip, and the pan reset after an absolute-element move: Bugs A + B). Reveal
|
|
1748
|
+
// THROUGH the camera instead: measure the element's live screen box and, only
|
|
1749
|
+
// when it's off-screen, pan the world transform the minimal amount to bring the
|
|
1750
|
+
// nearest out-of-view edge in. Screen-space deltas → `controller.panBy` (which
|
|
1751
|
+
// reads the exact live viewport), so it's self-correcting regardless of any
|
|
1752
|
+
// model lag and never touches host scroll.
|
|
1753
|
+
|
|
1754
|
+
/** Distance (screen px) a canvas element is allowed to sit from the viewport
|
|
1755
|
+
* edge after a reveal — a small inset so a just-revealed element isn't flush
|
|
1756
|
+
* against the frame. */
|
|
1757
|
+
export const REVEAL_MARGIN_PX = 40;
|
|
1758
|
+
|
|
1759
|
+
/** Minimal 1-axis delta (screen px) to bring `[elStart, elEnd]` inside
|
|
1760
|
+
* `[viewStart + margin, viewEnd - margin]`. Returns 0 when already visible OR
|
|
1761
|
+
* when the element spans the viewport (bigger than the view — don't move,
|
|
1762
|
+
* matching `scrollIntoView({block:'nearest'})`). Sign matches `panBy`: a
|
|
1763
|
+
* positive delta pans the world content toward the end (down / right). */
|
|
1764
|
+
export function revealAxisDelta(
|
|
1765
|
+
elStart: number,
|
|
1766
|
+
elEnd: number,
|
|
1767
|
+
viewStart: number,
|
|
1768
|
+
viewEnd: number,
|
|
1769
|
+
margin: number
|
|
1770
|
+
): number {
|
|
1771
|
+
const vs = viewStart + margin;
|
|
1772
|
+
const ve = viewEnd - margin;
|
|
1773
|
+
if (elStart >= vs && elEnd <= ve) return 0; // fully visible
|
|
1774
|
+
if (elStart <= vs && elEnd >= ve) return 0; // spans the viewport — leave it
|
|
1775
|
+
if (elStart < vs) return vs - elStart; // off the start edge → pan toward end
|
|
1776
|
+
if (elEnd > ve) return ve - elEnd; // off the end edge → pan toward start
|
|
1777
|
+
return 0;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/** Pan the camera the minimal amount so `target` is visible inside `host`,
|
|
1781
|
+
* entirely through the world transform (never host scroll). No-op when the
|
|
1782
|
+
* element is already on-screen. Exported for the Task A3 regression test. */
|
|
1783
|
+
export function revealElementViaCamera(
|
|
1784
|
+
host: HTMLElement,
|
|
1785
|
+
target: HTMLElement,
|
|
1786
|
+
controller: ViewportControllerHandle
|
|
1787
|
+
): void {
|
|
1788
|
+
const hostRect = host.getBoundingClientRect();
|
|
1789
|
+
const r = target.getBoundingClientRect();
|
|
1790
|
+
if (r.width <= 0 && r.height <= 0) return; // detached / display:none — nothing to reveal
|
|
1791
|
+
const dx = revealAxisDelta(r.left, r.right, hostRect.left, hostRect.right, REVEAL_MARGIN_PX);
|
|
1792
|
+
const dy = revealAxisDelta(r.top, r.bottom, hostRect.top, hostRect.bottom, REVEAL_MARGIN_PX);
|
|
1793
|
+
if (dx !== 0 || dy !== 0) controller.panBy(dx, dy);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1480
1796
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1481
1797
|
// Router wire-up
|
|
1482
1798
|
|
|
@@ -1541,6 +1857,36 @@ function CanvasRouter({
|
|
|
1541
1857
|
// consumes.
|
|
1542
1858
|
const chromeCtx = useChromeVisibility();
|
|
1543
1859
|
|
|
1860
|
+
// Task A2 (feature-element-editing-robustness) — host-scroll-0 invariant.
|
|
1861
|
+
// `.dc-canvas` (overflow:hidden) and each `.dc-artboard-body` are the engine's
|
|
1862
|
+
// clip layers; the camera lives entirely in the `.dc-world` transform, and
|
|
1863
|
+
// both `writeTransform` and `__maudeCanvasRects` assume these hosts sit at
|
|
1864
|
+
// scroll 0. A stray programmatic scroll (a residual scrollIntoView, focus()
|
|
1865
|
+
// auto-scroll of an overflow:hidden ancestor) would shift the layout and
|
|
1866
|
+
// desync the camera model — the Bug A/B class. Clamp any scroll on an ENGINE
|
|
1867
|
+
// clip layer straight back to 0. Author scroll containers deeper inside a mock
|
|
1868
|
+
// are left alone (the target check excludes them). `scroll` events don't
|
|
1869
|
+
// bubble, so capture:true is what lets the host observe a descendant clip's
|
|
1870
|
+
// scroll without also intercepting author-content scrolls (those fail the
|
|
1871
|
+
// engine-clip test and pass through).
|
|
1872
|
+
useEffect(() => {
|
|
1873
|
+
const host = hostRef.current;
|
|
1874
|
+
if (!host) return;
|
|
1875
|
+
const clamp = (e: Event) => {
|
|
1876
|
+
const t = e.target;
|
|
1877
|
+
if (!(t instanceof HTMLElement)) return;
|
|
1878
|
+
const isEngineClip =
|
|
1879
|
+
t === host ||
|
|
1880
|
+
t.classList.contains('dc-artboard-body') ||
|
|
1881
|
+
t.classList.contains('dc-artboard');
|
|
1882
|
+
if (!isEngineClip) return;
|
|
1883
|
+
if (t.scrollLeft !== 0) t.scrollLeft = 0;
|
|
1884
|
+
if (t.scrollTop !== 0) t.scrollTop = 0;
|
|
1885
|
+
};
|
|
1886
|
+
host.addEventListener('scroll', clamp, true);
|
|
1887
|
+
return () => host.removeEventListener('scroll', clamp, true);
|
|
1888
|
+
}, [hostRef]);
|
|
1889
|
+
|
|
1544
1890
|
// Hover state drives the floating .dc-cv-halo--hover overlay. The overlay
|
|
1545
1891
|
// itself reads getBoundingClientRect on every rAF tick to follow pan/zoom.
|
|
1546
1892
|
const [hoverEl, setHoverEl] = useState<Element | null>(null);
|
|
@@ -1624,7 +1970,15 @@ function CanvasRouter({
|
|
|
1624
1970
|
artboardId: mm.artboardId ?? null,
|
|
1625
1971
|
} as HoverTarget)
|
|
1626
1972
|
);
|
|
1627
|
-
|
|
1973
|
+
// Task A1 — reveal THROUGH the camera, not host scroll. The old
|
|
1974
|
+
// `scrollIntoView` scrolled the overflow:hidden `.dc-canvas` /
|
|
1975
|
+
// `.dc-artboard-body` clip layers, desyncing the `.dc-world`
|
|
1976
|
+
// transform (Bugs A + B). `revealElementViaCamera` pans the world
|
|
1977
|
+
// the minimal amount only when the element is off-screen.
|
|
1978
|
+
const host = hostRef.current;
|
|
1979
|
+
if (host && zoomController) {
|
|
1980
|
+
revealElementViaCamera(host, target as HTMLElement, zoomController);
|
|
1981
|
+
}
|
|
1628
1982
|
}
|
|
1629
1983
|
}
|
|
1630
1984
|
return;
|
|
@@ -1766,7 +2120,7 @@ function CanvasRouter({
|
|
|
1766
2120
|
};
|
|
1767
2121
|
window.addEventListener('message', onMessage);
|
|
1768
2122
|
return () => window.removeEventListener('message', onMessage);
|
|
1769
|
-
}, [selSet, annotSel, setTool, undoStack, zoomController, chromeCtx]);
|
|
2123
|
+
}, [selSet, annotSel, setTool, undoStack, zoomController, chromeCtx, hostRef]);
|
|
1770
2124
|
|
|
1771
2125
|
// Phase 12 (DDR-103) — double-click a LEAF-TEXT element (children all text
|
|
1772
2126
|
// nodes) to edit its copy in place. Commit (blur / Enter) posts `dgn:edit-text`
|
|
@@ -1780,6 +2134,11 @@ function CanvasRouter({
|
|
|
1780
2134
|
if (!host) return;
|
|
1781
2135
|
let editing: HTMLElement | null = null;
|
|
1782
2136
|
let original = '';
|
|
2137
|
+
// DDR-150 P1 — the committed-but-unconfirmed edit, so a shell rejection
|
|
2138
|
+
// (dynamic/mixed content the engine refuses) can revert the optimistic
|
|
2139
|
+
// contenteditable text instead of letting it silently vanish on the next
|
|
2140
|
+
// reload. Mirrors the reorder-failed revert below. Cleared on revert.
|
|
2141
|
+
let lastTextCommit: { el: HTMLElement; original: string } | null = null;
|
|
1783
2142
|
const onBlur = (): void => commitEdit(true);
|
|
1784
2143
|
const onKey = (e: KeyboardEvent): void => {
|
|
1785
2144
|
if (e.key === 'Escape') {
|
|
@@ -1815,6 +2174,8 @@ function CanvasRouter({
|
|
|
1815
2174
|
} catch {
|
|
1816
2175
|
/* detached / cross-origin */
|
|
1817
2176
|
}
|
|
2177
|
+
// Stash the optimistic edit so a shell rejection can revert it (DDR-150 P1).
|
|
2178
|
+
lastTextCommit = { el: elx, original };
|
|
1818
2179
|
// Record onto the in-canvas undo stack so Cmd+Z reverts the rewrite. The
|
|
1819
2180
|
// edit already posted above (record, don't re-run do()). before/after are
|
|
1820
2181
|
// the trimmed bodies the edit-text endpoint persists.
|
|
@@ -1835,7 +2196,37 @@ function CanvasRouter({
|
|
|
1835
2196
|
const stamped = (t?.closest?.('[data-cd-id]') as HTMLElement | null) ?? null;
|
|
1836
2197
|
if (!stamped) return;
|
|
1837
2198
|
const kids = Array.from(stamped.childNodes);
|
|
1838
|
-
|
|
2199
|
+
const isLeafText = kids.length > 0 && kids.every((n) => n.nodeType === 3);
|
|
2200
|
+
if (!isLeafText) {
|
|
2201
|
+
// Task L6 — deep-select: a container (anything that isn't leaf-text)
|
|
2202
|
+
// double-clicked here used to just bail — this is the exact gap the
|
|
2203
|
+
// plan's deep-select task targets. Drill ONE level past whatever's
|
|
2204
|
+
// currently selected along this click's own stamped-ancestor chain
|
|
2205
|
+
// (Figma parity: repeated double-clicks walk progressively deeper);
|
|
2206
|
+
// default to the outermost rung when nothing in the chain is
|
|
2207
|
+
// currently selected. Mutually exclusive with text-edit below by
|
|
2208
|
+
// construction — same capture-phase listener, same branch gate.
|
|
2209
|
+
const chain = stampedChainToBody(stamped);
|
|
2210
|
+
if (chain.length > 0) {
|
|
2211
|
+
const sel = selSet.selected;
|
|
2212
|
+
const currentId = sel.length === 1 ? sel[0]?.id : undefined;
|
|
2213
|
+
const curIdx = currentId
|
|
2214
|
+
? chain.findIndex((n) => n.getAttribute('data-cd-id') === currentId)
|
|
2215
|
+
: -1;
|
|
2216
|
+
const next = curIdx >= 0 && curIdx + 1 < chain.length ? chain[curIdx + 1] : chain[0];
|
|
2217
|
+
if (next) {
|
|
2218
|
+
e.preventDefault();
|
|
2219
|
+
e.stopPropagation();
|
|
2220
|
+
const cdId = next.getAttribute('data-cd-id');
|
|
2221
|
+
const artboardId =
|
|
2222
|
+
(next.closest('[data-dc-screen]') as HTMLElement | null)?.getAttribute(
|
|
2223
|
+
'data-dc-screen'
|
|
2224
|
+
) ?? null;
|
|
2225
|
+
selSet.replace(hoverTargetToSelection({ el: next, cdId, artboardId } as HoverTarget));
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
1839
2230
|
e.preventDefault();
|
|
1840
2231
|
e.stopPropagation();
|
|
1841
2232
|
editing = stamped;
|
|
@@ -1855,13 +2246,32 @@ function CanvasRouter({
|
|
|
1855
2246
|
/* selection API unavailable */
|
|
1856
2247
|
}
|
|
1857
2248
|
};
|
|
2249
|
+
// The shell rejected the last inline text edit — revert the optimistic
|
|
2250
|
+
// contenteditable text so the canvas reflects the true (unsaved) source
|
|
2251
|
+
// rather than a change that silently disappears on the next reload (DDR-150 P1).
|
|
2252
|
+
const onEditReverted = (e: MessageEvent): void => {
|
|
2253
|
+
if (e.source !== window.parent) return;
|
|
2254
|
+
const m = e.data as { dgn?: string; op?: string; reason?: string } | null;
|
|
2255
|
+
if (m?.dgn !== 'edit-reverted' || m.op !== 'text' || !lastTextCommit) return;
|
|
2256
|
+
try {
|
|
2257
|
+
lastTextCommit.el.textContent = lastTextCommit.original;
|
|
2258
|
+
} catch {
|
|
2259
|
+
/* detached — a subsequent reload re-syncs */
|
|
2260
|
+
}
|
|
2261
|
+
showCanvasToast(
|
|
2262
|
+
`Couldn't save that text — ${m.reason || 'not editable inline'}. Edit it via chat.`
|
|
2263
|
+
);
|
|
2264
|
+
lastTextCommit = null;
|
|
2265
|
+
};
|
|
1858
2266
|
// Capture phase so we beat the fit-to-view dblclick handler.
|
|
1859
2267
|
host.addEventListener('dblclick', onDbl, true);
|
|
2268
|
+
window.addEventListener('message', onEditReverted);
|
|
1860
2269
|
return () => {
|
|
1861
2270
|
host.removeEventListener('dblclick', onDbl, true);
|
|
2271
|
+
window.removeEventListener('message', onEditReverted);
|
|
1862
2272
|
if (editing) teardown(editing);
|
|
1863
2273
|
};
|
|
1864
|
-
}, [hostRef]);
|
|
2274
|
+
}, [hostRef, selSet]);
|
|
1865
2275
|
|
|
1866
2276
|
// Cleanup any pending rAF on unmount.
|
|
1867
2277
|
useEffect(
|
|
@@ -1955,10 +2365,13 @@ function CanvasRouter({
|
|
|
1955
2365
|
<ElementMarqueeOverlay />
|
|
1956
2366
|
<HoverHalo el={hoverEl} />
|
|
1957
2367
|
<SelectionHalos />
|
|
2368
|
+
<ElementResizeOverlay />
|
|
2369
|
+
<SpacingHandlesOverlay />
|
|
1958
2370
|
<ReorderDrag />
|
|
1959
2371
|
<LayersLiveSync />
|
|
1960
2372
|
<GroupBbox />
|
|
1961
2373
|
<EqualSpacingHandles />
|
|
2374
|
+
<MeasureOverlay />
|
|
1962
2375
|
<ContextualToolbar />
|
|
1963
2376
|
<MultiArtboardToolbar
|
|
1964
2377
|
distributeArtboards={distributeArtboards}
|
|
@@ -2500,6 +2913,15 @@ function ReorderDrag() {
|
|
|
2500
2913
|
/** Stamped [data-cd-id] nodes in their ORIGINAL order — for computing a
|
|
2501
2914
|
* reused instance's occurrence index even after the DOM reflowed. */
|
|
2502
2915
|
snapshot: Element[] | null;
|
|
2916
|
+
/** True when the dragged element is `position: absolute|fixed` — reorder
|
|
2917
|
+
* (sibling order) never changes where an out-of-flow element paints, so
|
|
2918
|
+
* these drags commit `left`/`top` instead of a tree move (see the
|
|
2919
|
+
* "absolute element snaps back" RCA). */
|
|
2920
|
+
outOfFlow: boolean;
|
|
2921
|
+
/** Pre-drag `left`/`top` (px, resolved via computed style) — only
|
|
2922
|
+
* meaningful when `outOfFlow`. */
|
|
2923
|
+
originLeft: number;
|
|
2924
|
+
originTop: number;
|
|
2503
2925
|
};
|
|
2504
2926
|
// Hover a stable spot this long and the layout reflows LIVE (the node moves
|
|
2505
2927
|
// there while still floating with the cursor) — you see the result before
|
|
@@ -2511,6 +2933,9 @@ function ReorderDrag() {
|
|
|
2511
2933
|
// The last committed drop, kept so a shell 'reorder-failed' can undo the
|
|
2512
2934
|
// optimistic DOM move (the write was rejected → nothing persisted).
|
|
2513
2935
|
let lastCommit: { el: HTMLElement; parent: HTMLElement; next: Element | null } | null = null;
|
|
2936
|
+
// The last committed out-of-flow reposition, kept so a shell
|
|
2937
|
+
// 'reposition-failed' can restore the pre-drag inline style.
|
|
2938
|
+
let lastPosition: { el: HTMLElement; prevStyle: string | null } | null = null;
|
|
2514
2939
|
|
|
2515
2940
|
const setHighlight = (el: HTMLElement | null) => {
|
|
2516
2941
|
if (highlightEl === el) return;
|
|
@@ -2742,6 +3167,7 @@ function ReorderDrag() {
|
|
|
2742
3167
|
hideDivider();
|
|
2743
3168
|
suppressNextCanvasClick();
|
|
2744
3169
|
drag = null;
|
|
3170
|
+
setElementDragActive(false);
|
|
2745
3171
|
};
|
|
2746
3172
|
|
|
2747
3173
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
@@ -2764,6 +3190,7 @@ function ReorderDrag() {
|
|
|
2764
3190
|
if (!(t instanceof Node) || !el.contains(t)) return;
|
|
2765
3191
|
// Candidate — do NOT claim yet (a plain click must still work). We only
|
|
2766
3192
|
// become a drag once the pointer crosses the threshold in onMove.
|
|
3193
|
+
const outOfFlowPos = getComputedStyle(el).position;
|
|
2767
3194
|
drag = {
|
|
2768
3195
|
pointerId: e.pointerId,
|
|
2769
3196
|
startX: e.clientX,
|
|
@@ -2783,6 +3210,9 @@ function ReorderDrag() {
|
|
|
2783
3210
|
settleKey: null,
|
|
2784
3211
|
applied: null,
|
|
2785
3212
|
snapshot: null,
|
|
3213
|
+
outOfFlow: outOfFlowPos === 'absolute' || outOfFlowPos === 'fixed',
|
|
3214
|
+
originLeft: 0,
|
|
3215
|
+
originTop: 0,
|
|
2786
3216
|
};
|
|
2787
3217
|
};
|
|
2788
3218
|
|
|
@@ -2792,6 +3222,7 @@ function ReorderDrag() {
|
|
|
2792
3222
|
if (!d.dragging) {
|
|
2793
3223
|
if (!crossedDragThreshold(d.startX, d.startY, e.clientX, e.clientY)) return;
|
|
2794
3224
|
d.dragging = true;
|
|
3225
|
+
setElementDragActive(true);
|
|
2795
3226
|
// Start floating: the element tracks the cursor via `transform`, so its
|
|
2796
3227
|
// layout box stays reserved (origin shows empty space) and it stays in
|
|
2797
3228
|
// the zoomed world (styling + scale intact). Divide the screen delta by
|
|
@@ -2807,7 +3238,18 @@ function ReorderDrag() {
|
|
|
2807
3238
|
// reflect the pre-drag layout, not the reflowed one.
|
|
2808
3239
|
d.snapshot = [...document.querySelectorAll('[data-cd-id]')];
|
|
2809
3240
|
d.prevStyle = d.el.getAttribute('style');
|
|
2810
|
-
if (
|
|
3241
|
+
if (d.outOfFlow) {
|
|
3242
|
+
// Resolved (used) value in px — matches the unit space `left`/`top`
|
|
3243
|
+
// are written back in, regardless of an ancestor's zoom transform
|
|
3244
|
+
// (a CSS `transform: scale()` on the world doesn't change a
|
|
3245
|
+
// descendant's own box-model values). `auto` (e.g. positioned via
|
|
3246
|
+
// `right`/`bottom` instead) falls back to 0 — a known v1 limit.
|
|
3247
|
+
const cs = getComputedStyle(d.el);
|
|
3248
|
+
d.originLeft = Number.parseFloat(cs.left) || 0;
|
|
3249
|
+
d.originTop = Number.parseFloat(cs.top) || 0;
|
|
3250
|
+
} else if (getComputedStyle(d.el).position === 'static') {
|
|
3251
|
+
d.el.style.position = 'relative';
|
|
3252
|
+
}
|
|
2811
3253
|
d.el.style.zIndex = '9990';
|
|
2812
3254
|
d.el.style.pointerEvents = 'none'; // so elementFromPoint sees what's beneath
|
|
2813
3255
|
d.el.style.opacity = '0.9';
|
|
@@ -2821,6 +3263,11 @@ function ReorderDrag() {
|
|
|
2821
3263
|
const tx = (e.clientX - d.startX) / d.zoom;
|
|
2822
3264
|
const ty = (e.clientY - d.startY) / d.zoom;
|
|
2823
3265
|
d.el.style.transform = `translate(${tx}px, ${ty}px)`;
|
|
3266
|
+
// Out-of-flow elements free-move with the cursor — there's no sibling
|
|
3267
|
+
// "before/after" to hunt for (reordering an absolute/fixed element's
|
|
3268
|
+
// JSX siblings wouldn't change where it paints), so skip the reorder
|
|
3269
|
+
// target/divider/settle machinery entirely; onUp commits `left`/`top`.
|
|
3270
|
+
if (d.outOfFlow) return;
|
|
2824
3271
|
// Indicator + settle scheduling only — the DOM moves when a target stays
|
|
2825
3272
|
// stable for SETTLE_MS (the live-reflow preview), never per-mousemove.
|
|
2826
3273
|
const target = computeTarget(e.clientX, e.clientY, d);
|
|
@@ -2853,11 +3300,59 @@ function ReorderDrag() {
|
|
|
2853
3300
|
const onUp = (e: PointerEvent) => {
|
|
2854
3301
|
const d = drag;
|
|
2855
3302
|
drag = null;
|
|
3303
|
+
setElementDragActive(false);
|
|
2856
3304
|
setHighlight(null);
|
|
2857
3305
|
hideDivider();
|
|
2858
3306
|
if (!d || e.pointerId !== d.pointerId) return;
|
|
2859
3307
|
if (!d.dragging) return; // a plain click — leave it to native handlers
|
|
2860
3308
|
clearSettle(d);
|
|
3309
|
+
// Occurrence index of a node among its like-id siblings, read from the
|
|
3310
|
+
// PRE-drag snapshot (the live DOM already reflowed for an in-flow drop). For
|
|
3311
|
+
// a reused component (many DOM nodes share one internal id) the server maps
|
|
3312
|
+
// this to the parent's distinct <Component> USAGE; 0 for a normal element.
|
|
3313
|
+
// Shared by the reposition (Stage H3 — instance move stays local) + reorder
|
|
3314
|
+
// posts below, so both anchor to the same instance the user dragged.
|
|
3315
|
+
const occ = (node: Element | null): number => {
|
|
3316
|
+
if (!node || !d.snapshot) return 0;
|
|
3317
|
+
const nid = node.getAttribute('data-cd-id');
|
|
3318
|
+
const at = d.snapshot.indexOf(node);
|
|
3319
|
+
if (!nid || at < 0) return 0;
|
|
3320
|
+
let c = 0;
|
|
3321
|
+
for (let k = 0; k < at; k++) {
|
|
3322
|
+
if (d.snapshot[k]?.getAttribute('data-cd-id') === nid) c++;
|
|
3323
|
+
}
|
|
3324
|
+
return c;
|
|
3325
|
+
};
|
|
3326
|
+
if (d.outOfFlow) {
|
|
3327
|
+
// Un-float: restore the pre-drag inline style (drops the drag-only
|
|
3328
|
+
// props AND the cursor-follow transform), then overlay the new
|
|
3329
|
+
// left/top — same "unfloat" step as reorder, but the position
|
|
3330
|
+
// sticks instead of reverting to `prevStyle`'s original left/top.
|
|
3331
|
+
if (d.prevStyle == null) d.el.removeAttribute('style');
|
|
3332
|
+
else d.el.setAttribute('style', d.prevStyle);
|
|
3333
|
+
const dx = (d.lastX - d.startX) / d.zoom;
|
|
3334
|
+
const dy = (d.lastY - d.startY) / d.zoom;
|
|
3335
|
+
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) return; // negligible — nothing to persist
|
|
3336
|
+
const left = Math.round(d.originLeft + dx);
|
|
3337
|
+
const top = Math.round(d.originTop + dy);
|
|
3338
|
+
d.el.style.left = `${left}px`;
|
|
3339
|
+
d.el.style.top = `${top}px`;
|
|
3340
|
+
lastPosition = { el: d.el, prevStyle: d.prevStyle };
|
|
3341
|
+
suppressNextCanvasClick();
|
|
3342
|
+
window.parent.postMessage(
|
|
3343
|
+
{
|
|
3344
|
+
dgn: 'reposition-request',
|
|
3345
|
+
id: d.cdId,
|
|
3346
|
+
left,
|
|
3347
|
+
top,
|
|
3348
|
+
beforeLeft: Math.round(d.originLeft),
|
|
3349
|
+
beforeTop: Math.round(d.originTop),
|
|
3350
|
+
idIndex: occ(d.el),
|
|
3351
|
+
},
|
|
3352
|
+
'*'
|
|
3353
|
+
);
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
2861
3356
|
// Un-float: restore the element's exact original inline style.
|
|
2862
3357
|
if (d.prevStyle == null) d.el.removeAttribute('style');
|
|
2863
3358
|
else d.el.setAttribute('style', d.prevStyle);
|
|
@@ -2876,21 +3371,6 @@ function ReorderDrag() {
|
|
|
2876
3371
|
// never persisted doesn't linger until the next canvas switch.
|
|
2877
3372
|
lastCommit = d.origin ? { el: d.el, parent: d.origin.parent, next: d.origin.next } : null;
|
|
2878
3373
|
suppressNextCanvasClick();
|
|
2879
|
-
// Occurrence index of each node among its like-id siblings, read from the
|
|
2880
|
-
// PRE-drag snapshot (the live DOM already reflowed). For a reused component
|
|
2881
|
-
// (many DOM nodes share one internal id) the server maps this to the
|
|
2882
|
-
// parent's distinct <Component> usage; 0 for a normal element.
|
|
2883
|
-
const occ = (node: Element | null): number => {
|
|
2884
|
-
if (!node || !d.snapshot) return 0;
|
|
2885
|
-
const id = node.getAttribute('data-cd-id');
|
|
2886
|
-
const at = d.snapshot.indexOf(node);
|
|
2887
|
-
if (!id || at < 0) return 0;
|
|
2888
|
-
let c = 0;
|
|
2889
|
-
for (let k = 0; k < at; k++) {
|
|
2890
|
-
if (d.snapshot[k]?.getAttribute('data-cd-id') === id) c++;
|
|
2891
|
-
}
|
|
2892
|
-
return c;
|
|
2893
|
-
};
|
|
2894
3374
|
const refNode = d.target?.kind === 'inside' ? d.target.container : (d.target?.el ?? null);
|
|
2895
3375
|
window.parent.postMessage(
|
|
2896
3376
|
{
|
|
@@ -2918,15 +3398,29 @@ function ReorderDrag() {
|
|
|
2918
3398
|
lastCommit = null;
|
|
2919
3399
|
};
|
|
2920
3400
|
|
|
3401
|
+
// The shell rejected the last reposition write — revert the optimistic
|
|
3402
|
+
// left/top back to the pre-drag inline style.
|
|
3403
|
+
const onRepositionFailed = (e: MessageEvent) => {
|
|
3404
|
+
if (e.source !== window.parent) return;
|
|
3405
|
+
const m = e.data as { dgn?: string } | null;
|
|
3406
|
+
if (m?.dgn !== 'reposition-failed' || !lastPosition) return;
|
|
3407
|
+
if (lastPosition.prevStyle == null) lastPosition.el.removeAttribute('style');
|
|
3408
|
+
else lastPosition.el.setAttribute('style', lastPosition.prevStyle);
|
|
3409
|
+
lastPosition = null;
|
|
3410
|
+
};
|
|
3411
|
+
|
|
2921
3412
|
document.addEventListener('pointerdown', onDown, true);
|
|
2922
3413
|
window.addEventListener('pointermove', onMove);
|
|
2923
3414
|
window.addEventListener('pointerup', onUp);
|
|
2924
3415
|
window.addEventListener('keydown', onKeyDown, true);
|
|
2925
3416
|
window.addEventListener('message', onReorderFailed);
|
|
3417
|
+
window.addEventListener('message', onRepositionFailed);
|
|
2926
3418
|
return () => {
|
|
2927
3419
|
if (drag) clearSettle(drag);
|
|
2928
3420
|
setHighlight(null);
|
|
3421
|
+
setElementDragActive(false);
|
|
2929
3422
|
window.removeEventListener('message', onReorderFailed);
|
|
3423
|
+
window.removeEventListener('message', onRepositionFailed);
|
|
2930
3424
|
if (dividerEl) {
|
|
2931
3425
|
dividerEl.remove();
|
|
2932
3426
|
dividerEl = null;
|