@1agh/maude 0.15.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/LICENSE +21 -0
- package/README.md +166 -0
- package/cli/bin/maude.exe +15 -0
- package/cli/bin/maude.mjs +45 -0
- package/cli/bin/mdcc.exe +10 -0
- package/cli/bin/mdcc.mjs +7 -0
- package/cli/cli-wrapper.cjs +67 -0
- package/cli/commands/config.mjs +94 -0
- package/cli/commands/design.mjs +386 -0
- package/cli/commands/help.mjs +57 -0
- package/cli/commands/init.mjs +178 -0
- package/cli/commands/version.mjs +7 -0
- package/cli/install.cjs +113 -0
- package/cli/lib/argv.mjs +37 -0
- package/cli/lib/argv.test.mjs +46 -0
- package/cli/lib/copy-tree.mjs +78 -0
- package/package.json +94 -0
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +397 -0
- package/plugins/design/dev-server/annotations-layer.tsx +1717 -0
- package/plugins/design/dev-server/api.ts +674 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +50 -0
- package/plugins/design/dev-server/bin/bootstrap-check.sh +83 -0
- package/plugins/design/dev-server/bin/canvas-edit.sh +48 -0
- package/plugins/design/dev-server/bin/handoff.sh +27 -0
- package/plugins/design/dev-server/bin/screenshot.sh +232 -0
- package/plugins/design/dev-server/bin/server-up.sh +135 -0
- package/plugins/design/dev-server/bin/slug.sh +22 -0
- package/plugins/design/dev-server/bin/smoke.sh +272 -0
- package/plugins/design/dev-server/build.ts +267 -0
- package/plugins/design/dev-server/canvas-build.ts +219 -0
- package/plugins/design/dev-server/canvas-edit.ts +388 -0
- package/plugins/design/dev-server/canvas-header.ts +165 -0
- package/plugins/design/dev-server/canvas-icons.tsx +131 -0
- package/plugins/design/dev-server/canvas-lib-inline.ts +260 -0
- package/plugins/design/dev-server/canvas-lib-resolver.ts +85 -0
- package/plugins/design/dev-server/canvas-lib.tsx +1995 -0
- package/plugins/design/dev-server/canvas-meta.schema.json +181 -0
- package/plugins/design/dev-server/canvas-pipeline.ts +270 -0
- package/plugins/design/dev-server/canvas-shell.tsx +813 -0
- package/plugins/design/dev-server/client/app.jsx +2027 -0
- package/plugins/design/dev-server/client/hmr.mjs +85 -0
- package/plugins/design/dev-server/client/iframe-lazy.mjs +121 -0
- package/plugins/design/dev-server/client/index.html +15 -0
- package/plugins/design/dev-server/client/styles/0-reset.css +18 -0
- package/plugins/design/dev-server/client/styles/1-tokens.css +297 -0
- package/plugins/design/dev-server/client/styles/2-layout.css +35 -0
- package/plugins/design/dev-server/client/styles/3-shell.css +906 -0
- package/plugins/design/dev-server/client/styles/4-components.css +1268 -0
- package/plugins/design/dev-server/client/styles/5-utilities.css +4 -0
- package/plugins/design/dev-server/client/styles/_index.css +24 -0
- package/plugins/design/dev-server/client/styles.css +1419 -0
- package/plugins/design/dev-server/config.schema.json +147 -0
- package/plugins/design/dev-server/context-menu.tsx +343 -0
- package/plugins/design/dev-server/context.ts +173 -0
- package/plugins/design/dev-server/dist/client.bundle.js +20323 -0
- package/plugins/design/dev-server/dist/styles.css +2875 -0
- package/plugins/design/dev-server/examples/README.md +9 -0
- package/plugins/design/dev-server/examples/perf-100-artboards.tsx +113 -0
- package/plugins/design/dev-server/fs-watch.ts +63 -0
- package/plugins/design/dev-server/handoff.ts +721 -0
- package/plugins/design/dev-server/history.ts +125 -0
- package/plugins/design/dev-server/hmr-broadcast.ts +114 -0
- package/plugins/design/dev-server/http.ts +413 -0
- package/plugins/design/dev-server/input-router.tsx +485 -0
- package/plugins/design/dev-server/inspect.ts +365 -0
- package/plugins/design/dev-server/locator.ts +159 -0
- package/plugins/design/dev-server/mem.ts +97 -0
- package/plugins/design/dev-server/runtime-bundle.ts +235 -0
- package/plugins/design/dev-server/server.mjs +1246 -0
- package/plugins/design/dev-server/server.ts +131 -0
- package/plugins/design/dev-server/test/_helpers.ts +81 -0
- package/plugins/design/dev-server/test/active-state.test.ts +145 -0
- package/plugins/design/dev-server/test/annotations-api.test.ts +146 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +419 -0
- package/plugins/design/dev-server/test/binary-smoke.test.ts +47 -0
- package/plugins/design/dev-server/test/bundle-smoke.test.ts +29 -0
- package/plugins/design/dev-server/test/canvas-build.test.ts +78 -0
- package/plugins/design/dev-server/test/canvas-edit.test.ts +139 -0
- package/plugins/design/dev-server/test/canvas-header.test.ts +127 -0
- package/plugins/design/dev-server/test/canvas-lib-inline.test.ts +146 -0
- package/plugins/design/dev-server/test/canvas-lib-resolver.test.ts +112 -0
- package/plugins/design/dev-server/test/canvas-meta-api.test.ts +236 -0
- package/plugins/design/dev-server/test/canvas-pipeline.test.ts +180 -0
- package/plugins/design/dev-server/test/canvas-route.test.ts +176 -0
- package/plugins/design/dev-server/test/fs-watch.test.ts +41 -0
- package/plugins/design/dev-server/test/handoff-static-frames.test.ts +215 -0
- package/plugins/design/dev-server/test/handoff.test.ts +281 -0
- package/plugins/design/dev-server/test/history-rollback.test.ts +62 -0
- package/plugins/design/dev-server/test/hmr-broadcast.test.ts +108 -0
- package/plugins/design/dev-server/test/input-router.test.ts +316 -0
- package/plugins/design/dev-server/test/locator.test.ts +214 -0
- package/plugins/design/dev-server/test/perf-harness.ts +193 -0
- package/plugins/design/dev-server/test/phase-3.6-smoke.test.ts +77 -0
- package/plugins/design/dev-server/test/runtime-bundle.test.ts +69 -0
- package/plugins/design/dev-server/test/server-lifecycle.test.ts +28 -0
- package/plugins/design/dev-server/test/tool-palette.test.tsx +55 -0
- package/plugins/design/dev-server/test/use-annotation-selection.test.tsx +77 -0
- package/plugins/design/dev-server/test/use-artboard-drag.test.ts +325 -0
- package/plugins/design/dev-server/test/use-selection-set.test.tsx +166 -0
- package/plugins/design/dev-server/test/use-snap-guides.test.ts +190 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +93 -0
- package/plugins/design/dev-server/test/ws-handshake.test.ts +33 -0
- package/plugins/design/dev-server/tool-palette.tsx +278 -0
- package/plugins/design/dev-server/tsconfig.json +26 -0
- package/plugins/design/dev-server/use-annotation-selection.tsx +92 -0
- package/plugins/design/dev-server/use-annotations-visibility.tsx +43 -0
- package/plugins/design/dev-server/use-artboard-drag.tsx +445 -0
- package/plugins/design/dev-server/use-selection-set.tsx +224 -0
- package/plugins/design/dev-server/use-snap-guides.tsx +215 -0
- package/plugins/design/dev-server/use-tool-mode.tsx +114 -0
- package/plugins/design/dev-server/ws.ts +90 -0
- package/plugins/design/templates/_shell.html +177 -0
- package/plugins/design/templates/canvas.tsx.template +54 -0
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +277 -0
- package/plugins/design/templates/design-system-inspiration/_README.md +71 -0
- package/plugins/design/templates/design-system-inspiration/audience-consumer/components-banner.html +68 -0
- package/plugins/design/templates/design-system-inspiration/audience-consumer/components-empty-state-generous.html +39 -0
- package/plugins/design/templates/design-system-inspiration/audience-consumer/components-feature-grid.html +62 -0
- package/plugins/design/templates/design-system-inspiration/audience-consumer/components-marketing-card.html +63 -0
- package/plugins/design/templates/design-system-inspiration/audience-consumer/components-testimonial.html +80 -0
- package/plugins/design/templates/design-system-inspiration/audience-developer/components-code-block.html +71 -0
- package/plugins/design/templates/design-system-inspiration/audience-developer/components-diff-view.html +65 -0
- package/plugins/design/templates/design-system-inspiration/audience-developer/components-log-stream.html +62 -0
- package/plugins/design/templates/design-system-inspiration/audience-developer/components-monospace-table.html +57 -0
- package/plugins/design/templates/design-system-inspiration/audience-developer/components-terminal-pane.html +67 -0
- package/plugins/design/templates/design-system-inspiration/audience-developer/type-mono.html +57 -0
- package/plugins/design/templates/design-system-inspiration/audience-pro/colors-presence.html +74 -0
- package/plugins/design/templates/design-system-inspiration/audience-pro/components-command-palette.html +90 -0
- package/plugins/design/templates/design-system-inspiration/audience-pro/components-keyboard.html +51 -0
- package/plugins/design/templates/design-system-inspiration/audience-pro/components-list.html +89 -0
- package/plugins/design/templates/design-system-inspiration/audience-pro/components-shortcuts-overlay.html +85 -0
- package/plugins/design/templates/design-system-inspiration/audience-pro/components-toast-menu.html +80 -0
- package/plugins/design/templates/design-system-inspiration/core/INDEX.md.tpl +25 -0
- package/plugins/design/templates/design-system-inspiration/core/README.orchestration.md.tpl +71 -0
- package/plugins/design/templates/design-system-inspiration/core/README.philosophy.md.tpl +77 -0
- package/plugins/design/templates/design-system-inspiration/core/SKILL.md.tpl +50 -0
- package/plugins/design/templates/design-system-inspiration/core/colors_and_type.css.tpl +111 -0
- package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +28 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/_layout.css +113 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/colors-accent.html +48 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/colors-surfaces.html +49 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/colors-text.html +52 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/components-buttons.html +83 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/components-cards.html +79 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/components-inputs.html +82 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/motion.html +66 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/spacing-scale.html +53 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/type-scale.html +62 -0
- package/plugins/design/templates/design-system-inspiration/foundations/borders.html +39 -0
- package/plugins/design/templates/design-system-inspiration/foundations/elevation.html +46 -0
- package/plugins/design/templates/design-system-inspiration/foundations/focus.html +48 -0
- package/plugins/design/templates/design-system-inspiration/foundations/grid.html +51 -0
- package/plugins/design/templates/design-system-inspiration/foundations/iconography.html +73 -0
- package/plugins/design/templates/design-system-inspiration/foundations/opacity.html +45 -0
- package/plugins/design/templates/design-system-inspiration/foundations/radii.html +40 -0
- package/plugins/design/templates/design-system-inspiration/foundations/selection.html +45 -0
- package/plugins/design/templates/design-system-inspiration/meta/accessibility.html +62 -0
- package/plugins/design/templates/design-system-inspiration/meta/i18n.html +73 -0
- package/plugins/design/templates/design-system-inspiration/meta/presence-multiplayer.html +71 -0
- package/plugins/design/templates/design-system-inspiration/meta/tokens-index.html +80 -0
- package/plugins/design/templates/design-system-inspiration/patterns/patterns-auth.html +78 -0
- package/plugins/design/templates/design-system-inspiration/patterns/patterns-data-density.html +61 -0
- package/plugins/design/templates/design-system-inspiration/patterns/patterns-error-pages.html +70 -0
- package/plugins/design/templates/design-system-inspiration/patterns/patterns-form-layouts.html +70 -0
- package/plugins/design/templates/design-system-inspiration/patterns/patterns-onboarding.html +71 -0
- package/plugins/design/templates/design-system-inspiration/patterns/patterns-pricing.html +83 -0
- package/plugins/design/templates/design-system-inspiration/platform-desktop/components-resize-panels.html +63 -0
- package/plugins/design/templates/design-system-inspiration/platform-desktop/ui_kits-desktop-index.html +55 -0
- package/plugins/design/templates/design-system-inspiration/platform-desktop/ui_kits-desktop-showcase.html +302 -0
- package/plugins/design/templates/design-system-inspiration/platform-mobile/components-bottom-sheet.html +63 -0
- package/plugins/design/templates/design-system-inspiration/platform-mobile/components-pull-to-refresh.html +74 -0
- package/plugins/design/templates/design-system-inspiration/platform-mobile/components-segmented-control.html +51 -0
- package/plugins/design/templates/design-system-inspiration/platform-mobile/components-tab-bar.html +57 -0
- package/plugins/design/templates/design-system-inspiration/platform-mobile/ui_kits-mobile-index.html +58 -0
- package/plugins/design/templates/design-system-inspiration/platform-mobile/ui_kits-mobile-showcase.html +237 -0
- package/plugins/design/templates/design-system-inspiration/status/colors-status.html +49 -0
- package/plugins/design/templates/design-system-inspiration/status/components-status.html +63 -0
- package/plugins/design/templates/design-system-inspiration/status/skeletons.html +74 -0
- package/plugins/design/templates/design-system-inspiration/theme-both/colors-themes-side-by-side.html +59 -0
- package/plugins/design/templates/design-system-inspiration/universal/components-callout.html +74 -0
- package/plugins/design/templates/design-system-inspiration/universal/components-dialogs.html +81 -0
- package/plugins/design/templates/design-system-inspiration/universal/components-tables.html +101 -0
- package/plugins/design/templates/design-system-inspiration/universal/components-toggles.html +74 -0
- package/plugins/design/templates/design-system-inspiration/universal/components-tooltips.html +74 -0
- package/plugins/design/templates/design-system-inspiration/universal/empty-state.html.tpl +34 -0
- package/plugins/design/templates/design-system-inspiration/universal/logo.html +42 -0
- package/plugins/design/templates/ds-specimen.tsx.template +59 -0
- package/plugins/flow/.claude-plugin/config.schema.json +398 -0
- package/plugins/flow/README.md +45 -0
- package/plugins/flow/templates/ai-skeleton/INDEX.md +50 -0
- package/plugins/flow/templates/ai-skeleton/README.md +46 -0
- package/plugins/flow/templates/ai-skeleton/browser/har/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/browser/snapshots/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/business/README.md +12 -0
- package/plugins/flow/templates/ai-skeleton/context/README.md +12 -0
- package/plugins/flow/templates/ai-skeleton/decisions/README.md +20 -0
- package/plugins/flow/templates/ai-skeleton/design-import/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/dev-logs/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/device/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/docs/README.md +5 -0
- package/plugins/flow/templates/ai-skeleton/logs/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/logs/README.md +13 -0
- package/plugins/flow/templates/ai-skeleton/plans/README.md +16 -0
- package/plugins/flow/templates/ai-skeleton/plans/archive/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/release-guide.md +35 -0
- package/plugins/flow/templates/ai-skeleton/reviews/README.md +15 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/README.md +26 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/_lib/.gitkeep +0 -0
- package/plugins/flow/templates/ai-skeleton/state/STATE.md +25 -0
- package/plugins/flow/templates/ai-skeleton/templates/HANDOFF.md +32 -0
- package/plugins/flow/templates/ai-skeleton/workflows.config.json +74 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file canvas-shell.tsx — universal input-grammar wrapper for TSX canvases
|
|
3
|
+
* @scope plugins/design/dev-server/canvas-shell.tsx
|
|
4
|
+
* @purpose Mounted by `DesignCanvas` for every canvas. Stacks
|
|
5
|
+
* SelectionSetProvider + ContextMenuProvider, wires the input
|
|
6
|
+
* router to provider actions, and renders the floating chrome
|
|
7
|
+
* (ToolPalette, hover halo, selection halos, group bbox).
|
|
8
|
+
*
|
|
9
|
+
* Input grammar (V0.16+ universal — there's no opt-out flag):
|
|
10
|
+
*
|
|
11
|
+
* Move tool (V)
|
|
12
|
+
* bare hover / click → passes through (native interactions work)
|
|
13
|
+
* Cmd + hover → preview halo on deepest element under cursor
|
|
14
|
+
* Cmd + click → replace selection with deepest element
|
|
15
|
+
* Cmd + Shift + click → add deepest to selection (multi)
|
|
16
|
+
* right-click → context menu
|
|
17
|
+
*
|
|
18
|
+
* Hand tool (H) pan-on-drag, no Space required; no selection
|
|
19
|
+
* Comment tool (C) hover paints halo, click drops comment pin;
|
|
20
|
+
* native interactions on artboard children fully
|
|
21
|
+
* suppressed via capture-phase preventDefault
|
|
22
|
+
*
|
|
23
|
+
* keydown V / H / C / Esc → tool switch (Esc also clears selection + menu)
|
|
24
|
+
*
|
|
25
|
+
* Wheel / pinch / space-pan / Cmd+0/1/+/- stay with `useViewportController`
|
|
26
|
+
* (canvas-lib.tsx). The router consumes a strict non-overlapping subset.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
type ReactNode,
|
|
31
|
+
type RefObject,
|
|
32
|
+
useCallback,
|
|
33
|
+
useEffect,
|
|
34
|
+
useMemo,
|
|
35
|
+
useRef,
|
|
36
|
+
useState,
|
|
37
|
+
} from 'react';
|
|
38
|
+
|
|
39
|
+
import { AnnotationsLayer } from './annotations-layer.tsx';
|
|
40
|
+
import {
|
|
41
|
+
SnapGuideOverlay,
|
|
42
|
+
type ViewportControllerHandle,
|
|
43
|
+
useViewportControllerContext,
|
|
44
|
+
} from './canvas-lib.tsx';
|
|
45
|
+
import {
|
|
46
|
+
ContextMenuProvider,
|
|
47
|
+
type ContextRegistry,
|
|
48
|
+
type ContextTarget,
|
|
49
|
+
type ContextTargetKind,
|
|
50
|
+
type MenuItem,
|
|
51
|
+
useContextMenu,
|
|
52
|
+
} from './context-menu.tsx';
|
|
53
|
+
import { type HoverTarget, resolveHoverTarget, useInputRouter } from './input-router.tsx';
|
|
54
|
+
import { ToolPalette } from './tool-palette.tsx';
|
|
55
|
+
import {
|
|
56
|
+
AnnotationSelectionProvider,
|
|
57
|
+
useAnnotationSelection,
|
|
58
|
+
} from './use-annotation-selection.tsx';
|
|
59
|
+
import { AnnotationsVisibilityProvider } from './use-annotations-visibility.tsx';
|
|
60
|
+
import { type Selection, SelectionSetProvider, useSelectionSet } from './use-selection-set.tsx';
|
|
61
|
+
import { useToolMode } from './use-tool-mode.tsx';
|
|
62
|
+
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
// Styles — halos render as `position: fixed` siblings of the canvas. Reading
|
|
65
|
+
// element bounds via getBoundingClientRect (screen coords) keeps the 2 px
|
|
66
|
+
// border thickness consistent across zoom levels — CSS `zoom` on the world
|
|
67
|
+
// plane would otherwise scale a 2 px outline to 0.84 px at 42 % zoom (subpixel
|
|
68
|
+
// = invisible). No per-element class stamping is used.
|
|
69
|
+
|
|
70
|
+
const HALO_CSS = `
|
|
71
|
+
.dc-cv-halo {
|
|
72
|
+
position: fixed;
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
z-index: 5;
|
|
75
|
+
border: 2px solid var(--accent, #d63b1f);
|
|
76
|
+
box-sizing: border-box;
|
|
77
|
+
border-radius: 2px;
|
|
78
|
+
transition: opacity 60ms linear;
|
|
79
|
+
}
|
|
80
|
+
.dc-cv-halo--hover {
|
|
81
|
+
border-width: 2px;
|
|
82
|
+
opacity: 0.85;
|
|
83
|
+
}
|
|
84
|
+
.dc-cv-halo--selected {
|
|
85
|
+
border-width: 2px;
|
|
86
|
+
box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent, #d63b1f) 18%, transparent);
|
|
87
|
+
}
|
|
88
|
+
.dc-cv-group-bbox {
|
|
89
|
+
position: fixed;
|
|
90
|
+
pointer-events: none;
|
|
91
|
+
z-index: 5;
|
|
92
|
+
border: 1px dashed var(--accent, #d63b1f);
|
|
93
|
+
border-radius: 2px;
|
|
94
|
+
opacity: 0.85;
|
|
95
|
+
}
|
|
96
|
+
/*
|
|
97
|
+
* Active-artboard indicator — the artboard whose center sits closest to the
|
|
98
|
+
* viewport midpoint after pan settles is "active" (DesignCanvas tracks this
|
|
99
|
+
* for keyboard jumps + the /design:edit context anchor). Phase 4 shipped a
|
|
100
|
+
* 2 px accent ring there; that's too loud next to selection halos. Keep it
|
|
101
|
+
* subtle: 1 px outline at low opacity. The drop-shadow stays untouched.
|
|
102
|
+
*/
|
|
103
|
+
.dc-canvas .dc-artboard[aria-current="true"] {
|
|
104
|
+
box-shadow:
|
|
105
|
+
6px 6px 0 var(--fg-0, #2a2520),
|
|
106
|
+
0 0 0 1px color-mix(in oklab, var(--accent, #d63b1f) 40%, transparent);
|
|
107
|
+
}
|
|
108
|
+
/*
|
|
109
|
+
* Force tool cursor across the canvas tree in comment / hand modes. Without
|
|
110
|
+
* !important on every descendant, buttons and links with their own cursor
|
|
111
|
+
* declaration would flip the cursor away from the tool affordance the moment
|
|
112
|
+
* the user hovers an interactive element — wrong signal when native
|
|
113
|
+
* interactions are suppressed by the router anyway.
|
|
114
|
+
*/
|
|
115
|
+
.dc-canvas[data-active-tool="comment"],
|
|
116
|
+
.dc-canvas[data-active-tool="comment"] *,
|
|
117
|
+
.dc-canvas[data-active-tool="pen"],
|
|
118
|
+
.dc-canvas[data-active-tool="pen"] *,
|
|
119
|
+
.dc-canvas[data-active-tool="rect"],
|
|
120
|
+
.dc-canvas[data-active-tool="rect"] *,
|
|
121
|
+
.dc-canvas[data-active-tool="ellipse"],
|
|
122
|
+
.dc-canvas[data-active-tool="ellipse"] *,
|
|
123
|
+
.dc-canvas[data-active-tool="arrow"],
|
|
124
|
+
.dc-canvas[data-active-tool="arrow"] * {
|
|
125
|
+
cursor: crosshair !important;
|
|
126
|
+
}
|
|
127
|
+
.dc-canvas[data-active-tool="hand"],
|
|
128
|
+
.dc-canvas[data-active-tool="hand"] * {
|
|
129
|
+
cursor: grab !important;
|
|
130
|
+
}
|
|
131
|
+
.dc-canvas[data-active-tool="eraser"],
|
|
132
|
+
.dc-canvas[data-active-tool="eraser"] * {
|
|
133
|
+
cursor: cell !important;
|
|
134
|
+
}
|
|
135
|
+
`.trim();
|
|
136
|
+
|
|
137
|
+
function ensureHaloStyles(): void {
|
|
138
|
+
if (typeof document === 'undefined') return;
|
|
139
|
+
if (document.getElementById('dc-cv-halo-css')) return;
|
|
140
|
+
const s = document.createElement('style');
|
|
141
|
+
s.id = 'dc-cv-halo-css';
|
|
142
|
+
s.textContent = HALO_CSS;
|
|
143
|
+
document.head.appendChild(s);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
// Shell
|
|
148
|
+
|
|
149
|
+
export function CanvasShell({
|
|
150
|
+
hostRef,
|
|
151
|
+
children,
|
|
152
|
+
}: {
|
|
153
|
+
hostRef: RefObject<HTMLDivElement | null>;
|
|
154
|
+
children: ReactNode;
|
|
155
|
+
}) {
|
|
156
|
+
ensureHaloStyles();
|
|
157
|
+
// ToolProvider is mounted by DesignCanvas one level up (so the viewport
|
|
158
|
+
// controller's `isPanDragActive` predicate can read the live tool state).
|
|
159
|
+
return (
|
|
160
|
+
<SelectionSetProvider>
|
|
161
|
+
<AnnotationSelectionProvider>
|
|
162
|
+
<AnnotationsVisibilityProvider>
|
|
163
|
+
<CanvasCore hostRef={hostRef}>{children}</CanvasCore>
|
|
164
|
+
</AnnotationsVisibilityProvider>
|
|
165
|
+
</AnnotationSelectionProvider>
|
|
166
|
+
</SelectionSetProvider>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
// CanvasCore — sits inside SelectionSetProvider, builds the menu registry
|
|
172
|
+
// against the live viewport controller + selection set, then mounts
|
|
173
|
+
// ContextMenuProvider + CanvasRouter.
|
|
174
|
+
|
|
175
|
+
function CanvasCore({
|
|
176
|
+
hostRef,
|
|
177
|
+
children,
|
|
178
|
+
}: {
|
|
179
|
+
hostRef: RefObject<HTMLDivElement | null>;
|
|
180
|
+
children: ReactNode;
|
|
181
|
+
}) {
|
|
182
|
+
const controller = useViewportControllerContext();
|
|
183
|
+
const selSet = useSelectionSet();
|
|
184
|
+
const { tool } = useToolMode();
|
|
185
|
+
|
|
186
|
+
// Project active tool to `.dc-canvas[data-active-tool]` so the cursor
|
|
187
|
+
// override CSS rules (HALO_CSS) can force the tool cursor across every
|
|
188
|
+
// descendant — buttons / links with their own cursor declaration get
|
|
189
|
+
// overridden when in comment / hand modes.
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const host = hostRef.current;
|
|
192
|
+
if (!host) return;
|
|
193
|
+
host.setAttribute('data-active-tool', tool);
|
|
194
|
+
return () => {
|
|
195
|
+
host.removeAttribute('data-active-tool');
|
|
196
|
+
};
|
|
197
|
+
}, [hostRef, tool]);
|
|
198
|
+
|
|
199
|
+
const registry = useMemo<ContextRegistry>(
|
|
200
|
+
() => buildRegistry({ controller, clearSelection: selSet.clear }),
|
|
201
|
+
[controller, selSet.clear]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<ContextMenuProvider registry={registry}>
|
|
206
|
+
<CanvasRouter hostRef={hostRef}>{children}</CanvasRouter>
|
|
207
|
+
</ContextMenuProvider>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
212
|
+
// Registry builder — closes over controller + clear callback.
|
|
213
|
+
|
|
214
|
+
function buildRegistry(deps: {
|
|
215
|
+
controller: ViewportControllerHandle | null;
|
|
216
|
+
clearSelection: () => void;
|
|
217
|
+
}): ContextRegistry {
|
|
218
|
+
const { controller, clearSelection } = deps;
|
|
219
|
+
|
|
220
|
+
const copy = (text: string): void => {
|
|
221
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) return;
|
|
222
|
+
void navigator.clipboard.writeText(text).catch(() => {
|
|
223
|
+
/* clipboard blocked */
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const postComposeForTarget = (target: ContextTarget): void => {
|
|
228
|
+
if (typeof window === 'undefined') return;
|
|
229
|
+
const sel: Selection | null = target.el
|
|
230
|
+
? {
|
|
231
|
+
file: deriveFile(),
|
|
232
|
+
id: target.cdId ?? undefined,
|
|
233
|
+
selector: target.cdId ? `[data-cd-id="${target.cdId}"]` : cssPath(target.el),
|
|
234
|
+
artboardId: target.artboardId,
|
|
235
|
+
tag: target.el.tagName.toLowerCase(),
|
|
236
|
+
classes: realClasses(target.el),
|
|
237
|
+
text: shortText(target.el, 240),
|
|
238
|
+
dom_path: domPath(target.el),
|
|
239
|
+
bounds: (target.el as HTMLElement).getBoundingClientRect
|
|
240
|
+
? boundsOf(target.el as HTMLElement)
|
|
241
|
+
: null,
|
|
242
|
+
html: (target.el.outerHTML ?? '').slice(0, 4000),
|
|
243
|
+
}
|
|
244
|
+
: null;
|
|
245
|
+
try {
|
|
246
|
+
window.parent.postMessage({ dgn: 'comment-compose', selection: sel }, '*');
|
|
247
|
+
} catch {
|
|
248
|
+
/* ignore */
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const fitItem: MenuItem = {
|
|
253
|
+
id: 'fit-view',
|
|
254
|
+
label: 'Fit to view',
|
|
255
|
+
shortcut: '1',
|
|
256
|
+
onSelect: () => controller?.fit(),
|
|
257
|
+
};
|
|
258
|
+
const resetItem: MenuItem = {
|
|
259
|
+
id: 'reset-view',
|
|
260
|
+
label: 'Reset view',
|
|
261
|
+
shortcut: '⌘0',
|
|
262
|
+
onSelect: () => controller?.reset(),
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
element: [
|
|
267
|
+
[
|
|
268
|
+
{
|
|
269
|
+
id: 'add-comment',
|
|
270
|
+
label: 'Add comment',
|
|
271
|
+
shortcut: 'C',
|
|
272
|
+
onSelect: postComposeForTarget,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: 'copy-css',
|
|
276
|
+
label: 'Copy CSS',
|
|
277
|
+
shortcut: '⌘⇧C',
|
|
278
|
+
onSelect: (target) => {
|
|
279
|
+
if (!target.el) return;
|
|
280
|
+
copy(cssPath(target.el));
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: 'copy-id',
|
|
285
|
+
label: 'Copy data-cd-id',
|
|
286
|
+
onSelect: (target) => {
|
|
287
|
+
if (target.cdId) copy(target.cdId);
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
id: 'inspect',
|
|
292
|
+
label: 'Inspect',
|
|
293
|
+
shortcut: '⌥I',
|
|
294
|
+
disabled: true,
|
|
295
|
+
onSelect: () => {
|
|
296
|
+
console.warn('[context-menu] TODO: tweaks panel for TSX canvases');
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
[
|
|
301
|
+
{
|
|
302
|
+
id: 'hide',
|
|
303
|
+
label: 'Hide',
|
|
304
|
+
shortcut: '⌘⇧H',
|
|
305
|
+
onSelect: (target) => {
|
|
306
|
+
if (target.el) (target.el as HTMLElement).style.visibility = 'hidden';
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: 'deselect',
|
|
311
|
+
label: 'Deselect',
|
|
312
|
+
shortcut: 'Esc',
|
|
313
|
+
onSelect: () => clearSelection(),
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
],
|
|
317
|
+
'artboard-chrome': [
|
|
318
|
+
[
|
|
319
|
+
{
|
|
320
|
+
id: 'fit-one',
|
|
321
|
+
label: 'Fit just this artboard',
|
|
322
|
+
onSelect: (target) => {
|
|
323
|
+
if (!controller || !target.artboardId) return;
|
|
324
|
+
// controller exposes jumpTo(rect) — DCArtboard.onFocus uses the
|
|
325
|
+
// same pattern. The artboard label button already wires to
|
|
326
|
+
// onFocus; dispatch a synthetic click as the simplest bridge.
|
|
327
|
+
const btn = (target.el ?? document)
|
|
328
|
+
.closest?.('[data-dc-screen]')
|
|
329
|
+
?.querySelector('button.dc-artboard-label');
|
|
330
|
+
(btn as HTMLButtonElement | null)?.click();
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
fitItem,
|
|
334
|
+
resetItem,
|
|
335
|
+
],
|
|
336
|
+
],
|
|
337
|
+
world: [[fitItem, resetItem]],
|
|
338
|
+
overlay: [],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function boundsOf(el: HTMLElement) {
|
|
343
|
+
const r = el.getBoundingClientRect();
|
|
344
|
+
return {
|
|
345
|
+
x: Math.round(r.left),
|
|
346
|
+
y: Math.round(r.top),
|
|
347
|
+
w: Math.round(r.width),
|
|
348
|
+
h: Math.round(r.height),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
353
|
+
// Router wire-up
|
|
354
|
+
|
|
355
|
+
function CanvasRouter({
|
|
356
|
+
hostRef,
|
|
357
|
+
children,
|
|
358
|
+
}: {
|
|
359
|
+
hostRef: RefObject<HTMLDivElement | null>;
|
|
360
|
+
children: ReactNode;
|
|
361
|
+
}) {
|
|
362
|
+
const { tool, setTool } = useToolMode();
|
|
363
|
+
const selSet = useSelectionSet();
|
|
364
|
+
const annotSel = useAnnotationSelection();
|
|
365
|
+
const ctxMenu = useContextMenu();
|
|
366
|
+
|
|
367
|
+
// Hover state drives the floating .dc-cv-halo--hover overlay. The overlay
|
|
368
|
+
// itself reads getBoundingClientRect on every rAF tick to follow pan/zoom.
|
|
369
|
+
const [hoverEl, setHoverEl] = useState<Element | null>(null);
|
|
370
|
+
|
|
371
|
+
// rAF-coalesced hover dispatcher. `pointermove` fires hundreds of times/sec
|
|
372
|
+
// under trackpad input — collapse to one elementFromPoint per frame.
|
|
373
|
+
const pendingHoverRef = useRef<{ deep: boolean; x: number; y: number } | null>(null);
|
|
374
|
+
const hoverRafRef = useRef<number | null>(null);
|
|
375
|
+
|
|
376
|
+
const getActiveTool = useCallback(() => tool, [tool]);
|
|
377
|
+
|
|
378
|
+
const applyHover = useCallback(() => {
|
|
379
|
+
hoverRafRef.current = null;
|
|
380
|
+
const pending = pendingHoverRef.current;
|
|
381
|
+
pendingHoverRef.current = null;
|
|
382
|
+
if (!pending) return;
|
|
383
|
+
const target = resolveHoverTarget(document, pending.x, pending.y, {
|
|
384
|
+
deep: pending.deep,
|
|
385
|
+
});
|
|
386
|
+
const nextEl = target?.el ?? null;
|
|
387
|
+
setHoverEl((prev) => (prev === nextEl ? prev : nextEl));
|
|
388
|
+
}, []);
|
|
389
|
+
|
|
390
|
+
// Clear hover when switching to hand mode mid-stream.
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
if (tool === 'hand') setHoverEl(null);
|
|
393
|
+
}, [tool]);
|
|
394
|
+
|
|
395
|
+
// Listen for `dgn: 'force-clear'` from the shell — the comment composer
|
|
396
|
+
// posts it on submit / cancel / Esc so the selection halo clears when the
|
|
397
|
+
// user closes the composer.
|
|
398
|
+
useEffect(() => {
|
|
399
|
+
if (typeof window === 'undefined') return;
|
|
400
|
+
const onMessage = (e: MessageEvent) => {
|
|
401
|
+
const m = e.data as { dgn?: string } | null;
|
|
402
|
+
if (!m || typeof m !== 'object' || !m.dgn) return;
|
|
403
|
+
if (m.dgn === 'force-clear' || m.dgn === 'select-clear' || m.dgn === 'selection-clear') {
|
|
404
|
+
selSet.clear();
|
|
405
|
+
annotSel.clear();
|
|
406
|
+
setHoverEl(null);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (m.dgn === 'tool-set') {
|
|
410
|
+
const t = (m as { tool?: string }).tool;
|
|
411
|
+
if (typeof t === 'string') setTool(t as never);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
window.addEventListener('message', onMessage);
|
|
416
|
+
return () => window.removeEventListener('message', onMessage);
|
|
417
|
+
}, [selSet, annotSel, setTool]);
|
|
418
|
+
|
|
419
|
+
// Cleanup any pending rAF on unmount.
|
|
420
|
+
useEffect(
|
|
421
|
+
() => () => {
|
|
422
|
+
if (hoverRafRef.current != null && typeof cancelAnimationFrame !== 'undefined') {
|
|
423
|
+
cancelAnimationFrame(hoverRafRef.current);
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
[]
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
useInputRouter({
|
|
430
|
+
hostRef,
|
|
431
|
+
getActiveTool,
|
|
432
|
+
callbacks: {
|
|
433
|
+
onHover: ({ deep, clientX, clientY }) => {
|
|
434
|
+
pendingHoverRef.current = { deep, x: clientX, y: clientY };
|
|
435
|
+
if (hoverRafRef.current == null && typeof requestAnimationFrame !== 'undefined') {
|
|
436
|
+
hoverRafRef.current = requestAnimationFrame(applyHover);
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
onSelect: ({ mode, deep, clientX, clientY }) => {
|
|
440
|
+
const target = resolveHoverTarget(document, clientX, clientY, { deep });
|
|
441
|
+
if (!target) {
|
|
442
|
+
// Cmd-click on dead space (canvas chrome, label strip, empty world):
|
|
443
|
+
// clear for `replace`, leave the set alone for `add`.
|
|
444
|
+
if (mode === 'replace') selSet.clear();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const sel = hoverTargetToSelection(target);
|
|
448
|
+
if (mode === 'replace') selSet.replace(sel);
|
|
449
|
+
else selSet.add(sel);
|
|
450
|
+
},
|
|
451
|
+
onContextMenu: ({ clientX, clientY }) => {
|
|
452
|
+
const target = resolveHoverTarget(document, clientX, clientY, { deep: true });
|
|
453
|
+
const kind = classifyContextKind(target);
|
|
454
|
+
const ctxTarget: ContextTarget = {
|
|
455
|
+
kind,
|
|
456
|
+
el: target?.el ?? null,
|
|
457
|
+
cdId: target?.cdId ?? null,
|
|
458
|
+
artboardId: target?.artboardId ?? null,
|
|
459
|
+
clientX,
|
|
460
|
+
clientY,
|
|
461
|
+
};
|
|
462
|
+
ctxMenu.open(ctxTarget);
|
|
463
|
+
},
|
|
464
|
+
onTool: ({ tool: t }) => setTool(t),
|
|
465
|
+
onEscape: () => {
|
|
466
|
+
ctxMenu.close();
|
|
467
|
+
selSet.clear();
|
|
468
|
+
annotSel.clear();
|
|
469
|
+
setHoverEl(null);
|
|
470
|
+
},
|
|
471
|
+
onDropComment: ({ clientX, clientY }) => {
|
|
472
|
+
const target = resolveHoverTarget(document, clientX, clientY, { deep: true });
|
|
473
|
+
if (!target) return;
|
|
474
|
+
const sel = hoverTargetToSelection(target);
|
|
475
|
+
// Commit the target to the selection set so the halo persists while
|
|
476
|
+
// the shell-side composer is open. The user clears by:
|
|
477
|
+
// - submit / cancel on the composer (shell posts `force-clear`)
|
|
478
|
+
// - pressing Esc inside the canvas (router's onEscape → clear)
|
|
479
|
+
// - clicking another element in comment mode (this handler runs
|
|
480
|
+
// again and replaces)
|
|
481
|
+
selSet.replace(sel);
|
|
482
|
+
if (typeof window === 'undefined') return;
|
|
483
|
+
try {
|
|
484
|
+
window.parent.postMessage({ dgn: 'comment-compose', selection: sel }, '*');
|
|
485
|
+
} catch {
|
|
486
|
+
/* parent detached */
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<>
|
|
494
|
+
{children}
|
|
495
|
+
<AnnotationsLayer />
|
|
496
|
+
<ToolPalette />
|
|
497
|
+
<HoverHalo el={hoverEl} />
|
|
498
|
+
<SelectionHalos />
|
|
499
|
+
<GroupBbox />
|
|
500
|
+
<SnapGuideOverlay />
|
|
501
|
+
</>
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
506
|
+
// HoverHalo — single floating overlay tracking the hovered element's screen
|
|
507
|
+
// bounds. Updates on every animation frame while mounted. Position: fixed so
|
|
508
|
+
// CSS `zoom` on the world plane never affects the 2 px border thickness.
|
|
509
|
+
|
|
510
|
+
function HoverHalo({ el }: { el: Element | null }) {
|
|
511
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
512
|
+
const rafRef = useRef<number | null>(null);
|
|
513
|
+
const targetRef = useRef<Element | null>(el);
|
|
514
|
+
targetRef.current = el;
|
|
515
|
+
|
|
516
|
+
useEffect(() => {
|
|
517
|
+
if (!el) {
|
|
518
|
+
if (ref.current) ref.current.style.display = 'none';
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const tick = () => {
|
|
522
|
+
rafRef.current = null;
|
|
523
|
+
const div = ref.current;
|
|
524
|
+
const t = targetRef.current;
|
|
525
|
+
if (!div || !t || !t.isConnected) {
|
|
526
|
+
if (div) div.style.display = 'none';
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const r = (t as HTMLElement).getBoundingClientRect();
|
|
530
|
+
if (r.width === 0 && r.height === 0) {
|
|
531
|
+
div.style.display = 'none';
|
|
532
|
+
} else {
|
|
533
|
+
div.style.display = 'block';
|
|
534
|
+
div.style.left = `${Math.round(r.left)}px`;
|
|
535
|
+
div.style.top = `${Math.round(r.top)}px`;
|
|
536
|
+
div.style.width = `${Math.round(r.width)}px`;
|
|
537
|
+
div.style.height = `${Math.round(r.height)}px`;
|
|
538
|
+
}
|
|
539
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
540
|
+
};
|
|
541
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
542
|
+
return () => {
|
|
543
|
+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
|
544
|
+
};
|
|
545
|
+
}, [el]);
|
|
546
|
+
|
|
547
|
+
if (!el) return null;
|
|
548
|
+
return <div ref={ref} className="dc-cv-halo dc-cv-halo--hover" aria-hidden="true" />;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
552
|
+
// SelectionHalos — N floating overlays, one per selected element. Resolves
|
|
553
|
+
// elements by `data-cd-id` when present, falling back to the selector path.
|
|
554
|
+
|
|
555
|
+
function SelectionHalos() {
|
|
556
|
+
const { selected } = useSelectionSet();
|
|
557
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
558
|
+
const rafRef = useRef<number | null>(null);
|
|
559
|
+
|
|
560
|
+
useEffect(() => {
|
|
561
|
+
if (selected.length === 0) {
|
|
562
|
+
const c = containerRef.current;
|
|
563
|
+
if (c) c.innerHTML = '';
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const tick = () => {
|
|
567
|
+
rafRef.current = null;
|
|
568
|
+
const c = containerRef.current;
|
|
569
|
+
if (!c) return;
|
|
570
|
+
// Match the rendered halo count to selected.length; reuse DOM nodes.
|
|
571
|
+
while (c.children.length < selected.length) {
|
|
572
|
+
const child = document.createElement('div');
|
|
573
|
+
child.className = 'dc-cv-halo dc-cv-halo--selected';
|
|
574
|
+
child.setAttribute('aria-hidden', 'true');
|
|
575
|
+
c.appendChild(child);
|
|
576
|
+
}
|
|
577
|
+
while (c.children.length > selected.length) {
|
|
578
|
+
c.removeChild(c.lastChild as Node);
|
|
579
|
+
}
|
|
580
|
+
for (let i = 0; i < selected.length; i++) {
|
|
581
|
+
const sel = selected[i];
|
|
582
|
+
const child = c.children[i] as HTMLDivElement;
|
|
583
|
+
const el = sel?.id
|
|
584
|
+
? document.querySelector(`[data-cd-id="${cssEscape(sel.id)}"]`)
|
|
585
|
+
: sel
|
|
586
|
+
? safeQuery(sel.selector)
|
|
587
|
+
: null;
|
|
588
|
+
if (!el) {
|
|
589
|
+
child.style.display = 'none';
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const r = (el as HTMLElement).getBoundingClientRect();
|
|
593
|
+
if (r.width === 0 && r.height === 0) {
|
|
594
|
+
child.style.display = 'none';
|
|
595
|
+
} else {
|
|
596
|
+
child.style.display = 'block';
|
|
597
|
+
child.style.left = `${Math.round(r.left)}px`;
|
|
598
|
+
child.style.top = `${Math.round(r.top)}px`;
|
|
599
|
+
child.style.width = `${Math.round(r.width)}px`;
|
|
600
|
+
child.style.height = `${Math.round(r.height)}px`;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
604
|
+
};
|
|
605
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
606
|
+
return () => {
|
|
607
|
+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
|
608
|
+
};
|
|
609
|
+
}, [selected]);
|
|
610
|
+
|
|
611
|
+
return <div ref={containerRef} aria-hidden="true" />;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
615
|
+
// GroupBbox — dashed outline around the union of selected elements when N > 1.
|
|
616
|
+
|
|
617
|
+
function GroupBbox() {
|
|
618
|
+
const { selected } = useSelectionSet();
|
|
619
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
620
|
+
const rafRef = useRef<number | null>(null);
|
|
621
|
+
|
|
622
|
+
useEffect(() => {
|
|
623
|
+
if (selected.length < 2) {
|
|
624
|
+
if (ref.current) ref.current.style.display = 'none';
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const tick = () => {
|
|
628
|
+
rafRef.current = null;
|
|
629
|
+
const div = ref.current;
|
|
630
|
+
if (!div) return;
|
|
631
|
+
let xMin = Number.POSITIVE_INFINITY;
|
|
632
|
+
let yMin = Number.POSITIVE_INFINITY;
|
|
633
|
+
let xMax = Number.NEGATIVE_INFINITY;
|
|
634
|
+
let yMax = Number.NEGATIVE_INFINITY;
|
|
635
|
+
let anyHit = false;
|
|
636
|
+
for (const sel of selected) {
|
|
637
|
+
const el = sel.id
|
|
638
|
+
? document.querySelector(`[data-cd-id="${cssEscape(sel.id)}"]`)
|
|
639
|
+
: safeQuery(sel.selector);
|
|
640
|
+
if (!el) continue;
|
|
641
|
+
const r = (el as HTMLElement).getBoundingClientRect();
|
|
642
|
+
if (r.width === 0 && r.height === 0) continue;
|
|
643
|
+
anyHit = true;
|
|
644
|
+
if (r.left < xMin) xMin = r.left;
|
|
645
|
+
if (r.top < yMin) yMin = r.top;
|
|
646
|
+
if (r.right > xMax) xMax = r.right;
|
|
647
|
+
if (r.bottom > yMax) yMax = r.bottom;
|
|
648
|
+
}
|
|
649
|
+
if (!anyHit) {
|
|
650
|
+
div.style.display = 'none';
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const pad = 4;
|
|
654
|
+
div.style.display = 'block';
|
|
655
|
+
div.style.left = `${Math.round(xMin - pad)}px`;
|
|
656
|
+
div.style.top = `${Math.round(yMin - pad)}px`;
|
|
657
|
+
div.style.width = `${Math.round(xMax - xMin + pad * 2)}px`;
|
|
658
|
+
div.style.height = `${Math.round(yMax - yMin + pad * 2)}px`;
|
|
659
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
660
|
+
};
|
|
661
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
662
|
+
return () => {
|
|
663
|
+
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
|
664
|
+
};
|
|
665
|
+
}, [selected]);
|
|
666
|
+
|
|
667
|
+
if (selected.length < 2) return null;
|
|
668
|
+
return <div ref={ref} className="dc-cv-group-bbox" aria-hidden="true" />;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
672
|
+
// Helpers
|
|
673
|
+
|
|
674
|
+
function classifyContextKind(target: HoverTarget | null): ContextTargetKind {
|
|
675
|
+
if (!target) return 'world';
|
|
676
|
+
const el = target.el;
|
|
677
|
+
if (!el) return 'world';
|
|
678
|
+
if (el.closest?.('.dc-mm, .dc-zoom-tb, .dc-tool-palette, .dc-context-menu')) {
|
|
679
|
+
return 'overlay';
|
|
680
|
+
}
|
|
681
|
+
if (target.cdId) return 'element';
|
|
682
|
+
if (target.artboardId) return 'artboard-chrome';
|
|
683
|
+
return 'world';
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function hoverTargetToSelection(target: HoverTarget): Selection {
|
|
687
|
+
const el = target.el;
|
|
688
|
+
const rect =
|
|
689
|
+
el && (el as HTMLElement).getBoundingClientRect
|
|
690
|
+
? (el as HTMLElement).getBoundingClientRect()
|
|
691
|
+
: null;
|
|
692
|
+
// `cdId` is the hit element's OWN data-cd-id (deep mode); resolver never
|
|
693
|
+
// climbs to an ancestor. Falls back to cssPath of the hit when no stable
|
|
694
|
+
// anchor exists.
|
|
695
|
+
const cdId = target.cdId;
|
|
696
|
+
return {
|
|
697
|
+
file: typeof window !== 'undefined' ? deriveFile() : undefined,
|
|
698
|
+
id: cdId ?? undefined,
|
|
699
|
+
selector: cdId ? `[data-cd-id="${cdId}"]` : cssPath(el),
|
|
700
|
+
artboardId: target.artboardId,
|
|
701
|
+
tag: el?.tagName.toLowerCase() ?? '',
|
|
702
|
+
classes: realClasses(el),
|
|
703
|
+
text: shortText(el, 240),
|
|
704
|
+
dom_path: domPath(el),
|
|
705
|
+
bounds: rect
|
|
706
|
+
? {
|
|
707
|
+
x: Math.round(rect.left),
|
|
708
|
+
y: Math.round(rect.top),
|
|
709
|
+
w: Math.round(rect.width),
|
|
710
|
+
h: Math.round(rect.height),
|
|
711
|
+
}
|
|
712
|
+
: null,
|
|
713
|
+
html: el ? (el.outerHTML ?? '').slice(0, 4000) : '',
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function deriveFile(): string | undefined {
|
|
718
|
+
try {
|
|
719
|
+
const p = window.location.pathname;
|
|
720
|
+
if (p === '/_canvas-shell.html' || p === '/_canvas-shell') {
|
|
721
|
+
const qs = new URLSearchParams(window.location.search);
|
|
722
|
+
const canvas = qs.get('canvas') ?? '';
|
|
723
|
+
const designRel = (qs.get('designRel') ?? '.design').replace(/^\/+|\/+$/g, '');
|
|
724
|
+
return `${designRel}/${canvas}`;
|
|
725
|
+
}
|
|
726
|
+
return decodeURIComponent(p).replace(/^\//, '');
|
|
727
|
+
} catch {
|
|
728
|
+
return undefined;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function realClasses(el: Element | null): string {
|
|
733
|
+
if (!el) return '';
|
|
734
|
+
return (el.getAttribute('class') ?? '')
|
|
735
|
+
.trim()
|
|
736
|
+
.split(/\s+/)
|
|
737
|
+
.filter((c) => c && !c.startsWith('dgn-') && !c.startsWith('dc-cv-'))
|
|
738
|
+
.join(' ');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function shortText(el: Element | null, max: number): string {
|
|
742
|
+
if (!el) return '';
|
|
743
|
+
const t = ((el as HTMLElement).innerText || el.textContent || '').replace(/\s+/g, ' ').trim();
|
|
744
|
+
return t.length > max ? `${t.slice(0, max - 1)}…` : t;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function cssPath(el: Element | null): string {
|
|
748
|
+
if (!el) return '';
|
|
749
|
+
const path: string[] = [];
|
|
750
|
+
let cur: Element | null = el;
|
|
751
|
+
while (cur && cur.nodeType === 1 && path.length < 8) {
|
|
752
|
+
const dscEl = cur.getAttribute?.('data-dc-element');
|
|
753
|
+
if (dscEl) {
|
|
754
|
+
path.unshift(`[data-dc-element="${dscEl}"]`);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
const dscSc = cur.getAttribute?.('data-dc-screen');
|
|
758
|
+
if (dscSc) {
|
|
759
|
+
path.unshift(`[data-dc-screen="${dscSc}"]`);
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
let sel = cur.nodeName.toLowerCase();
|
|
763
|
+
if (cur.id) {
|
|
764
|
+
sel = `#${cur.id}`;
|
|
765
|
+
path.unshift(sel);
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
const cls = realClasses(cur).split(/\s+/).filter(Boolean).slice(0, 2);
|
|
769
|
+
if (cls.length) sel += `.${cls.join('.')}`;
|
|
770
|
+
let sib = 1;
|
|
771
|
+
let n: Element | null = cur.previousElementSibling;
|
|
772
|
+
while (n) {
|
|
773
|
+
sib++;
|
|
774
|
+
n = n.previousElementSibling;
|
|
775
|
+
}
|
|
776
|
+
sel += `:nth-child(${sib})`;
|
|
777
|
+
path.unshift(sel);
|
|
778
|
+
cur = cur.parentElement;
|
|
779
|
+
}
|
|
780
|
+
return path.join(' > ');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function domPath(el: Element | null): string[] {
|
|
784
|
+
const hops: string[] = [];
|
|
785
|
+
let cur = el;
|
|
786
|
+
while (cur && cur.nodeType === 1 && hops.length < 8) {
|
|
787
|
+
let label = cur.nodeName.toLowerCase();
|
|
788
|
+
const dEl = cur.getAttribute?.('data-dc-element');
|
|
789
|
+
const dSc = cur.getAttribute?.('data-dc-screen');
|
|
790
|
+
if (dEl) label += `[data-dc-element="${dEl}"]`;
|
|
791
|
+
else if (dSc) label += `[data-dc-screen="${dSc}"]`;
|
|
792
|
+
else if (cur.id) label += `#${cur.id}`;
|
|
793
|
+
const cls = realClasses(cur).split(/\s+/).filter(Boolean).slice(0, 2);
|
|
794
|
+
if (cls.length && !dEl && !dSc) label += `.${cls.join('.')}`;
|
|
795
|
+
hops.unshift(label);
|
|
796
|
+
cur = cur.parentElement;
|
|
797
|
+
}
|
|
798
|
+
return hops;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function cssEscape(s: string): string {
|
|
802
|
+
// Minimal CSS.escape polyfill — only handles chars actually present in
|
|
803
|
+
// pipeline-stamped IDs (alphanumerics + `-` + `_`).
|
|
804
|
+
return s.replace(/[^a-zA-Z0-9_-]/g, (c) => `\\${c}`);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function safeQuery(selector: string): Element | null {
|
|
808
|
+
try {
|
|
809
|
+
return document.querySelector(selector);
|
|
810
|
+
} catch {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
}
|