@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,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file input-router.tsx — canvas pointer/keyboard classifier + hook
|
|
3
|
+
* @scope plugins/design/dev-server/input-router.tsx
|
|
4
|
+
* @purpose Owned by canvas-lib's DesignCanvas. Classifies the NON-WHEEL
|
|
5
|
+
* subset of pointer + key events into discrete router actions.
|
|
6
|
+
* `useViewportController` keeps owning wheel + middle-mouse +
|
|
7
|
+
* space-pan + Cmd+0/1/+/- — the two stacks coexist without a
|
|
8
|
+
* listener race (DDR-026).
|
|
9
|
+
*
|
|
10
|
+
* Event ownership (read this before adding handlers):
|
|
11
|
+
*
|
|
12
|
+
* ┌──────────────────────────────────┬─────────────────────────┐
|
|
13
|
+
* │ Event │ Owner │
|
|
14
|
+
* ├──────────────────────────────────┼─────────────────────────┤
|
|
15
|
+
* │ wheel / shift-wheel / cmd-wheel │ useViewportController │
|
|
16
|
+
* │ pointerdown btn=1 / space-held │ useViewportController │
|
|
17
|
+
* │ keydown Space / Cmd+0/1/+/- │ useViewportController │
|
|
18
|
+
* │ pointermove (hover) │ input-router │
|
|
19
|
+
* │ pointerdown btn=0 (select) │ input-router │
|
|
20
|
+
* │ pointerdown btn=2 (right-click) │ input-router │
|
|
21
|
+
* │ keydown V / H / C / Esc │ input-router │
|
|
22
|
+
* └──────────────────────────────────┴─────────────────────────┘
|
|
23
|
+
*
|
|
24
|
+
* The router does no DOM work itself — `classify()` is pure (testable without
|
|
25
|
+
* a DOM) and `useInputRouter()` attaches listeners that dispatch through the
|
|
26
|
+
* caller-supplied callbacks. Hover-target resolution + selection persistence
|
|
27
|
+
* live in the consumer (DesignCanvas).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { type RefObject, useEffect } from 'react';
|
|
31
|
+
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
// Types
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tool union. Phase 4.1 shipped V/H/C; Phase 5 adds the draw set
|
|
37
|
+
* (pen / rect / arrow / eraser). Draw-tool pointer events are owned by
|
|
38
|
+
* `AnnotationsLayer` — the router classifies their letter shortcuts but
|
|
39
|
+
* returns `no-op` for the corresponding pointer events so the SVG overlay
|
|
40
|
+
* can grab them natively.
|
|
41
|
+
*/
|
|
42
|
+
export type Tool = 'move' | 'hand' | 'comment' | 'pen' | 'rect' | 'ellipse' | 'arrow' | 'eraser';
|
|
43
|
+
|
|
44
|
+
const ANNOTATION_TOOLS = new Set<Tool>(['pen', 'rect', 'ellipse', 'arrow', 'eraser']);
|
|
45
|
+
|
|
46
|
+
export function isAnnotationTool(t: Tool): boolean {
|
|
47
|
+
return ANNOTATION_TOOLS.has(t);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type RouterAction =
|
|
51
|
+
| { kind: 'no-op' }
|
|
52
|
+
| { kind: 'hover'; deep: boolean; clientX: number; clientY: number }
|
|
53
|
+
| {
|
|
54
|
+
kind: 'select';
|
|
55
|
+
/** `replace` swaps the selection set, `add` merges into it. */
|
|
56
|
+
mode: 'replace' | 'add';
|
|
57
|
+
/**
|
|
58
|
+
* `true` resolves to the deepest descendant under the cursor (Cmd-held
|
|
59
|
+
* mode). `false` resolves to the topmost interesting ancestor (top mode).
|
|
60
|
+
* Phase 4.1 Move-tool selection always uses deep=true — bare clicks
|
|
61
|
+
* are passthrough (no select), and the only entry points are Cmd
|
|
62
|
+
* (replace deep) and Cmd+Shift (add deep).
|
|
63
|
+
*/
|
|
64
|
+
deep: boolean;
|
|
65
|
+
clientX: number;
|
|
66
|
+
clientY: number;
|
|
67
|
+
}
|
|
68
|
+
| { kind: 'drop-comment'; clientX: number; clientY: number }
|
|
69
|
+
| { kind: 'context-menu'; clientX: number; clientY: number }
|
|
70
|
+
| { kind: 'tool'; tool: Tool }
|
|
71
|
+
| { kind: 'escape' };
|
|
72
|
+
|
|
73
|
+
export interface ClassifyInput {
|
|
74
|
+
type: 'pointermove' | 'pointerdown' | 'contextmenu' | 'keydown';
|
|
75
|
+
/** PointerEvent.button: 0 = left, 1 = middle, 2 = right. */
|
|
76
|
+
button?: number;
|
|
77
|
+
metaKey?: boolean;
|
|
78
|
+
ctrlKey?: boolean;
|
|
79
|
+
shiftKey?: boolean;
|
|
80
|
+
altKey?: boolean;
|
|
81
|
+
key?: string;
|
|
82
|
+
clientX?: number;
|
|
83
|
+
clientY?: number;
|
|
84
|
+
/** Spacebar held — shared signal with `useViewportController`'s pan-drag. */
|
|
85
|
+
spaceHeld?: boolean;
|
|
86
|
+
/** Event target is editable (input/textarea/contentEditable) — caller computes. */
|
|
87
|
+
isEditable?: boolean;
|
|
88
|
+
activeTool: Tool;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
92
|
+
// classify — pure function. All branching lives here so unit tests cover every
|
|
93
|
+
// row of the dispatch table without spinning up a DOM.
|
|
94
|
+
|
|
95
|
+
const metaOrCtrl = (i: ClassifyInput): boolean => !!(i.metaKey || i.ctrlKey);
|
|
96
|
+
|
|
97
|
+
export function classify(input: ClassifyInput): RouterAction {
|
|
98
|
+
if (input.type === 'keydown') {
|
|
99
|
+
if (input.isEditable) return { kind: 'no-op' };
|
|
100
|
+
// Tool letters are bare keys — Cmd/Ctrl/Alt+letter belongs to shell / browser.
|
|
101
|
+
if (input.metaKey || input.ctrlKey || input.altKey) {
|
|
102
|
+
// Esc with modifiers still dismisses.
|
|
103
|
+
if (input.key === 'Escape') return { kind: 'escape' };
|
|
104
|
+
return { kind: 'no-op' };
|
|
105
|
+
}
|
|
106
|
+
const k = (input.key || '').toLowerCase();
|
|
107
|
+
if (k === 'v') return { kind: 'tool', tool: 'move' };
|
|
108
|
+
if (k === 'h') return { kind: 'tool', tool: 'hand' };
|
|
109
|
+
if (k === 'c') return { kind: 'tool', tool: 'comment' };
|
|
110
|
+
if (k === 'b') return { kind: 'tool', tool: 'pen' };
|
|
111
|
+
if (k === 'r') return { kind: 'tool', tool: 'rect' };
|
|
112
|
+
if (k === 'o') return { kind: 'tool', tool: 'ellipse' };
|
|
113
|
+
if (k === 'a') return { kind: 'tool', tool: 'arrow' };
|
|
114
|
+
if (k === 'e') return { kind: 'tool', tool: 'eraser' };
|
|
115
|
+
if (input.key === 'Escape') return { kind: 'escape' };
|
|
116
|
+
return { kind: 'no-op' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (input.type === 'contextmenu') {
|
|
120
|
+
return {
|
|
121
|
+
kind: 'context-menu',
|
|
122
|
+
clientX: input.clientX ?? 0,
|
|
123
|
+
clientY: input.clientY ?? 0,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (input.type === 'pointermove') {
|
|
128
|
+
// Phase 5 draw tools: pen / rect / arrow / eraser own all their pointer
|
|
129
|
+
// events through `AnnotationsLayer`. The router never paints a hover halo
|
|
130
|
+
// while drawing — that affordance is reserved for select / comment.
|
|
131
|
+
if (isAnnotationTool(input.activeTool)) return { kind: 'no-op' };
|
|
132
|
+
// Hand tool: drag pan is owned by useViewportController; no hover paint.
|
|
133
|
+
if (input.activeTool === 'hand') return { kind: 'no-op' };
|
|
134
|
+
// Comment tool: always paint a preview halo on the deepest element under
|
|
135
|
+
// cursor — that's the element the user is about to comment on. Comment
|
|
136
|
+
// pin attachment is to the same element they were hovering.
|
|
137
|
+
if (input.activeTool === 'comment') {
|
|
138
|
+
return {
|
|
139
|
+
kind: 'hover',
|
|
140
|
+
deep: true,
|
|
141
|
+
clientX: input.clientX ?? 0,
|
|
142
|
+
clientY: input.clientY ?? 0,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Move tool: bare hover does nothing (native interactions pass through);
|
|
146
|
+
// Cmd-held hover paints a halo on the deepest element (preview).
|
|
147
|
+
if (!metaOrCtrl(input)) return { kind: 'no-op' };
|
|
148
|
+
return {
|
|
149
|
+
kind: 'hover',
|
|
150
|
+
deep: true,
|
|
151
|
+
clientX: input.clientX ?? 0,
|
|
152
|
+
clientY: input.clientY ?? 0,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (input.type === 'pointerdown') {
|
|
157
|
+
if (input.button === 2) {
|
|
158
|
+
return {
|
|
159
|
+
kind: 'context-menu',
|
|
160
|
+
clientX: input.clientX ?? 0,
|
|
161
|
+
clientY: input.clientY ?? 0,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (input.button === 1 || input.spaceHeld) return { kind: 'no-op' };
|
|
165
|
+
if (input.button !== 0) return { kind: 'no-op' };
|
|
166
|
+
|
|
167
|
+
// Phase 5 draw tools own bare left-clicks; the router returns no-op so
|
|
168
|
+
// the SVG layer's own listeners (no preventDefault) fire normally. Cmd-
|
|
169
|
+
// modified clicks still flow into the move-tool select path below — that
|
|
170
|
+
// stays available as an escape hatch even while a draw tool is active.
|
|
171
|
+
if (isAnnotationTool(input.activeTool) && !metaOrCtrl(input)) {
|
|
172
|
+
return { kind: 'no-op' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (input.activeTool === 'comment') {
|
|
176
|
+
// Comment tool: bare click drops a pin. Cmd / Shift modifiers reserved
|
|
177
|
+
// for future "scope comment to deepest" variants — for now they fall
|
|
178
|
+
// through to the same drop.
|
|
179
|
+
return {
|
|
180
|
+
kind: 'drop-comment',
|
|
181
|
+
clientX: input.clientX ?? 0,
|
|
182
|
+
clientY: input.clientY ?? 0,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Hand tool: pan is owned by useViewportController via `isPanDragActive`.
|
|
187
|
+
// Router returns no-op so it doesn't preventDefault or stopPropagation —
|
|
188
|
+
// the controller's pointerdown listener on the same host claims the drag.
|
|
189
|
+
if (input.activeTool === 'hand') return { kind: 'no-op' };
|
|
190
|
+
|
|
191
|
+
// Move tool. Selection ONLY fires with Cmd / Cmd+Shift. Bare clicks and
|
|
192
|
+
// Shift-without-Cmd pass through so native canvas interactions (button
|
|
193
|
+
// presses, link clicks, input focus) still work — exactly the same as
|
|
194
|
+
// pre-Phase-4.1 behavior for everything except Cmd-modified gestures.
|
|
195
|
+
const cmd = metaOrCtrl(input);
|
|
196
|
+
if (!cmd) return { kind: 'no-op' };
|
|
197
|
+
const shift = !!input.shiftKey;
|
|
198
|
+
return {
|
|
199
|
+
kind: 'select',
|
|
200
|
+
mode: shift ? 'add' : 'replace',
|
|
201
|
+
deep: true,
|
|
202
|
+
clientX: input.clientX ?? 0,
|
|
203
|
+
clientY: input.clientY ?? 0,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { kind: 'no-op' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
211
|
+
// useInputRouter — attach listeners scoped to `hostRef.current`. Dispatches
|
|
212
|
+
// through `callbacks`. Returns nothing; cleans up on unmount.
|
|
213
|
+
|
|
214
|
+
export interface RouterCallbacks {
|
|
215
|
+
onHover?: (a: Extract<RouterAction, { kind: 'hover' }>) => void;
|
|
216
|
+
onSelect?: (a: Extract<RouterAction, { kind: 'select' }>) => void;
|
|
217
|
+
onDropComment?: (a: Extract<RouterAction, { kind: 'drop-comment' }>) => void;
|
|
218
|
+
onContextMenu?: (a: Extract<RouterAction, { kind: 'context-menu' }>) => void;
|
|
219
|
+
onTool?: (a: Extract<RouterAction, { kind: 'tool' }>) => void;
|
|
220
|
+
onEscape?: () => void;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface UseInputRouterOptions {
|
|
224
|
+
hostRef: RefObject<HTMLElement | null>;
|
|
225
|
+
/** Latest active tool — read at event time, not captured. */
|
|
226
|
+
getActiveTool: () => Tool;
|
|
227
|
+
/** Optional spacebar-held signal shared with useViewportController. */
|
|
228
|
+
isSpaceHeld?: () => boolean;
|
|
229
|
+
callbacks: RouterCallbacks;
|
|
230
|
+
/** When false, listeners are not attached. Defaults to true. */
|
|
231
|
+
enabled?: boolean;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function isEditableTarget(t: EventTarget | null): boolean {
|
|
235
|
+
if (!t || !(t as HTMLElement).tagName) return false;
|
|
236
|
+
const el = t as HTMLElement;
|
|
237
|
+
const tag = el.tagName;
|
|
238
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
239
|
+
if (el.isContentEditable) return true;
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function useInputRouter(opts: UseInputRouterOptions): void {
|
|
244
|
+
const { hostRef, getActiveTool, isSpaceHeld, callbacks, enabled = true } = opts;
|
|
245
|
+
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (!enabled) return;
|
|
248
|
+
const host = hostRef.current;
|
|
249
|
+
if (!host) return;
|
|
250
|
+
|
|
251
|
+
const dispatch = (action: RouterAction): void => {
|
|
252
|
+
switch (action.kind) {
|
|
253
|
+
case 'hover':
|
|
254
|
+
callbacks.onHover?.(action);
|
|
255
|
+
break;
|
|
256
|
+
case 'select':
|
|
257
|
+
callbacks.onSelect?.(action);
|
|
258
|
+
break;
|
|
259
|
+
case 'drop-comment':
|
|
260
|
+
callbacks.onDropComment?.(action);
|
|
261
|
+
break;
|
|
262
|
+
case 'context-menu':
|
|
263
|
+
callbacks.onContextMenu?.(action);
|
|
264
|
+
break;
|
|
265
|
+
case 'tool':
|
|
266
|
+
callbacks.onTool?.(action);
|
|
267
|
+
break;
|
|
268
|
+
case 'escape':
|
|
269
|
+
callbacks.onEscape?.();
|
|
270
|
+
break;
|
|
271
|
+
case 'no-op':
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const onPointerMove = (e: PointerEvent): void => {
|
|
277
|
+
const action = classify({
|
|
278
|
+
type: 'pointermove',
|
|
279
|
+
button: e.button,
|
|
280
|
+
metaKey: e.metaKey,
|
|
281
|
+
ctrlKey: e.ctrlKey,
|
|
282
|
+
shiftKey: e.shiftKey,
|
|
283
|
+
altKey: e.altKey,
|
|
284
|
+
clientX: e.clientX,
|
|
285
|
+
clientY: e.clientY,
|
|
286
|
+
spaceHeld: isSpaceHeld?.() ?? false,
|
|
287
|
+
activeTool: getActiveTool(),
|
|
288
|
+
});
|
|
289
|
+
dispatch(action);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const onPointerDown = (e: PointerEvent): void => {
|
|
293
|
+
const action = classify({
|
|
294
|
+
type: 'pointerdown',
|
|
295
|
+
button: e.button,
|
|
296
|
+
metaKey: e.metaKey,
|
|
297
|
+
ctrlKey: e.ctrlKey,
|
|
298
|
+
shiftKey: e.shiftKey,
|
|
299
|
+
altKey: e.altKey,
|
|
300
|
+
clientX: e.clientX,
|
|
301
|
+
clientY: e.clientY,
|
|
302
|
+
spaceHeld: isSpaceHeld?.() ?? false,
|
|
303
|
+
activeTool: getActiveTool(),
|
|
304
|
+
});
|
|
305
|
+
if (action.kind !== 'no-op') {
|
|
306
|
+
// Suppress native behavior on every event the router claims —
|
|
307
|
+
// button presses don't fire, inputs don't focus, the canvas
|
|
308
|
+
// content's own click handlers don't run. The router lives in
|
|
309
|
+
// capture phase so this fires before descendants.
|
|
310
|
+
e.preventDefault();
|
|
311
|
+
e.stopImmediatePropagation();
|
|
312
|
+
}
|
|
313
|
+
dispatch(action);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Paired mousedown listener — preventDefault on pointerdown does NOT
|
|
318
|
+
* suppress the mousedown event that browsers fire alongside, and
|
|
319
|
+
* `<input>` / `<button>` focus is driven by mousedown's default behavior.
|
|
320
|
+
* We mirror the same gate as pointerdown so suppressed pointerdowns also
|
|
321
|
+
* stop their twin mousedown.
|
|
322
|
+
*/
|
|
323
|
+
const onMouseDown = (e: MouseEvent): void => {
|
|
324
|
+
const action = classify({
|
|
325
|
+
type: 'pointerdown',
|
|
326
|
+
button: e.button,
|
|
327
|
+
metaKey: e.metaKey,
|
|
328
|
+
ctrlKey: e.ctrlKey,
|
|
329
|
+
shiftKey: e.shiftKey,
|
|
330
|
+
altKey: e.altKey,
|
|
331
|
+
clientX: e.clientX,
|
|
332
|
+
clientY: e.clientY,
|
|
333
|
+
spaceHeld: isSpaceHeld?.() ?? false,
|
|
334
|
+
activeTool: getActiveTool(),
|
|
335
|
+
});
|
|
336
|
+
if (action.kind !== 'no-op') {
|
|
337
|
+
e.preventDefault();
|
|
338
|
+
e.stopImmediatePropagation();
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Click listener — fires AFTER pointerdown+pointerup. Even with
|
|
344
|
+
* preventDefault on mousedown, the click event still synthesizes for
|
|
345
|
+
* non-form elements. We suppress it whenever the router claimed the
|
|
346
|
+
* matching pointerdown (re-classify with the same modifiers).
|
|
347
|
+
*/
|
|
348
|
+
const onClick = (e: MouseEvent): void => {
|
|
349
|
+
const tool = getActiveTool();
|
|
350
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
351
|
+
const wouldRoute =
|
|
352
|
+
tool === 'comment' || (tool === 'move' && mod && e.button === 0) || e.button === 2;
|
|
353
|
+
if (wouldRoute) {
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
e.stopImmediatePropagation();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const onContextMenu = (e: MouseEvent): void => {
|
|
360
|
+
e.preventDefault();
|
|
361
|
+
e.stopImmediatePropagation();
|
|
362
|
+
const action = classify({
|
|
363
|
+
type: 'contextmenu',
|
|
364
|
+
clientX: e.clientX,
|
|
365
|
+
clientY: e.clientY,
|
|
366
|
+
metaKey: e.metaKey,
|
|
367
|
+
ctrlKey: e.ctrlKey,
|
|
368
|
+
shiftKey: e.shiftKey,
|
|
369
|
+
altKey: e.altKey,
|
|
370
|
+
activeTool: getActiveTool(),
|
|
371
|
+
});
|
|
372
|
+
dispatch(action);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const onKeyDown = (e: KeyboardEvent): void => {
|
|
376
|
+
const action = classify({
|
|
377
|
+
type: 'keydown',
|
|
378
|
+
key: e.key,
|
|
379
|
+
metaKey: e.metaKey,
|
|
380
|
+
ctrlKey: e.ctrlKey,
|
|
381
|
+
shiftKey: e.shiftKey,
|
|
382
|
+
altKey: e.altKey,
|
|
383
|
+
isEditable: isEditableTarget(e.target),
|
|
384
|
+
activeTool: getActiveTool(),
|
|
385
|
+
});
|
|
386
|
+
if (action.kind === 'tool' || action.kind === 'escape') {
|
|
387
|
+
e.preventDefault();
|
|
388
|
+
}
|
|
389
|
+
dispatch(action);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Capture phase for pointer/mouse/click events — router runs BEFORE
|
|
393
|
+
// descendants (buttons, inputs, canvas content listeners). For events the
|
|
394
|
+
// classifier claims, we preventDefault + stopImmediatePropagation so the
|
|
395
|
+
// descendants never see them.
|
|
396
|
+
host.addEventListener('pointermove', onPointerMove, { passive: true });
|
|
397
|
+
host.addEventListener('pointerdown', onPointerDown, { capture: true });
|
|
398
|
+
host.addEventListener('mousedown', onMouseDown, { capture: true });
|
|
399
|
+
host.addEventListener('click', onClick, { capture: true });
|
|
400
|
+
host.addEventListener('contextmenu', onContextMenu, { capture: true });
|
|
401
|
+
// Key events: attach on document so focus inside any descendant is OK;
|
|
402
|
+
// the editable-target gate handles the "user is typing" case.
|
|
403
|
+
const doc = host.ownerDocument ?? document;
|
|
404
|
+
doc.addEventListener('keydown', onKeyDown, true);
|
|
405
|
+
|
|
406
|
+
return () => {
|
|
407
|
+
host.removeEventListener('pointermove', onPointerMove);
|
|
408
|
+
host.removeEventListener('pointerdown', onPointerDown, {
|
|
409
|
+
capture: true,
|
|
410
|
+
} as EventListenerOptions);
|
|
411
|
+
host.removeEventListener('mousedown', onMouseDown, { capture: true } as EventListenerOptions);
|
|
412
|
+
host.removeEventListener('click', onClick, { capture: true } as EventListenerOptions);
|
|
413
|
+
host.removeEventListener('contextmenu', onContextMenu, {
|
|
414
|
+
capture: true,
|
|
415
|
+
} as EventListenerOptions);
|
|
416
|
+
doc.removeEventListener('keydown', onKeyDown, true);
|
|
417
|
+
};
|
|
418
|
+
}, [enabled, hostRef, getActiveTool, isSpaceHeld, callbacks]);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
422
|
+
// resolveHoverTarget — walks from a clientX/clientY pair to the canvas element
|
|
423
|
+
// of interest. Default = topmost `[data-cd-id]` ancestor (the stable
|
|
424
|
+
// pipeline-stamped anchor). `deep = true` returns the deepest descendant
|
|
425
|
+
// (Cmd-hover behavior).
|
|
426
|
+
|
|
427
|
+
export interface HoverTarget {
|
|
428
|
+
el: Element;
|
|
429
|
+
cdId: string | null;
|
|
430
|
+
artboardId: string | null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function resolveHoverTarget(
|
|
434
|
+
doc: Document,
|
|
435
|
+
clientX: number,
|
|
436
|
+
clientY: number,
|
|
437
|
+
opts: { deep: boolean }
|
|
438
|
+
): HoverTarget | null {
|
|
439
|
+
const hit = doc.elementFromPoint(clientX, clientY);
|
|
440
|
+
if (!hit) return null;
|
|
441
|
+
// Skip the floating chrome (MiniMap / ZoomToolbar / ToolPalette / ContextMenu)
|
|
442
|
+
// AND the canvas/world frame itself — the user is never asking to "select
|
|
443
|
+
// the entire canvas viewport," that's a UI accident from climbing too high.
|
|
444
|
+
if (hit.closest?.('.dc-mm, .dc-zoom-tb, .dc-tool-palette, .dc-context-menu, .dc-cv-group-bbox')) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const artboardEl = hit.closest?.('[data-dc-screen]') ?? null;
|
|
449
|
+
const artboardId = artboardEl?.getAttribute('data-dc-screen') ?? null;
|
|
450
|
+
|
|
451
|
+
// Hover-target hard ceiling = `.dc-artboard-body`. The artboard chrome
|
|
452
|
+
// (article root + label button + body wrapper) is never a selection
|
|
453
|
+
// candidate — those carry `data-cd-id` from the pipeline pass but visually
|
|
454
|
+
// selecting them makes the WHOLE artboard outline, which looks like the
|
|
455
|
+
// canvas viewport is selected. If the user landed on chrome (the label
|
|
456
|
+
// button or empty body padding), bail.
|
|
457
|
+
const bodyEl = hit.closest?.('.dc-artboard-body') ?? null;
|
|
458
|
+
if (!bodyEl) return null;
|
|
459
|
+
if (hit === bodyEl) {
|
|
460
|
+
// Clicked exactly on the body wrapper, no inner element under cursor.
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (opts.deep) {
|
|
465
|
+
// Deepest mode — the hit element IS the target. Use its OWN data-cd-id
|
|
466
|
+
// when present; never climb to an ancestor's id (climbing was the cause
|
|
467
|
+
// of "Cmd-click on a deep span selects the whole artboard root"). When
|
|
468
|
+
// the hit lacks a stamped id, consumers fall back to a CSS-path selector.
|
|
469
|
+
const cdId = hit.getAttribute?.('data-cd-id') ?? null;
|
|
470
|
+
return { el: hit, cdId, artboardId };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Top mode — climb to the topmost descendant of the artboard body that
|
|
474
|
+
// still carries a data-cd-id. Hard ceiling is bodyEl itself (never select
|
|
475
|
+
// the body wrapper or higher).
|
|
476
|
+
let cur: Element | null = hit;
|
|
477
|
+
let topCdEl: Element | null = null;
|
|
478
|
+
while (cur && cur !== bodyEl) {
|
|
479
|
+
if (cur.hasAttribute?.('data-cd-id')) topCdEl = cur;
|
|
480
|
+
cur = cur.parentElement;
|
|
481
|
+
}
|
|
482
|
+
const el = topCdEl ?? hit;
|
|
483
|
+
const cdId = el.getAttribute?.('data-cd-id') ?? null;
|
|
484
|
+
return { el, cdId, artboardId };
|
|
485
|
+
}
|