@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,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file use-snap-guides.tsx — Phase 4.2 snap math + guide-line shapes
|
|
3
|
+
* @scope plugins/design/dev-server/use-snap-guides.tsx
|
|
4
|
+
* @purpose Pure function `computeSnap` that returns a snapped (x, y) for a
|
|
5
|
+
* proposed artboard rect plus the guide lines that should render
|
|
6
|
+
* as visual feedback. No React state; the drag controller calls
|
|
7
|
+
* this on every pointermove tick.
|
|
8
|
+
*
|
|
9
|
+
* Two snap kinds:
|
|
10
|
+
*
|
|
11
|
+
* Grid — every `gridSize` world-units. Applied to top-left corner of
|
|
12
|
+
* `proposed` (left edge for X, top edge for Y).
|
|
13
|
+
* Sibling — alignment with other artboard rects: left↔left, right↔right,
|
|
14
|
+
* left↔right, right↔left, centerX↔centerX on the X-axis (and
|
|
15
|
+
* the matching top/bottom/centerY set on the Y-axis).
|
|
16
|
+
*
|
|
17
|
+
* Tolerance is in world units, not screen pixels (DDR-028) — snap feel stays
|
|
18
|
+
* consistent across zoom levels.
|
|
19
|
+
*
|
|
20
|
+
* The X-axis and Y-axis are independent: a proposed rect can snap X to a
|
|
21
|
+
* sibling edge and Y to a grid line simultaneously. Both guides render.
|
|
22
|
+
*
|
|
23
|
+
* Closest candidate wins per axis (smallest |delta|). When multiple sibling
|
|
24
|
+
* candidates land on the exact same `pos` (e.g. two artboards' right edges
|
|
25
|
+
* stacked at the same x), we render one merged guide whose from/to spans the
|
|
26
|
+
* union of every aligned rect's perpendicular extent.
|
|
27
|
+
*
|
|
28
|
+
* `disabled: true` (Alt held) short-circuits with `{ x, y, guides: [] }`.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export interface Rect {
|
|
32
|
+
/** Optional rect id — used by the drag controller to filter self / followers
|
|
33
|
+
* out of the snap candidate set. `computeSnap` itself ignores it. */
|
|
34
|
+
id?: string;
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
w: number;
|
|
38
|
+
h: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type SnapAxis = 'x' | 'y';
|
|
42
|
+
|
|
43
|
+
export interface SnapGuide {
|
|
44
|
+
/** `"x"` → vertical line (snapping X coord). Line sits at `pos` on X,
|
|
45
|
+
* spans `from..to` on Y. `"y"` is the dual: horizontal line at `pos` on Y,
|
|
46
|
+
* spans `from..to` on X. */
|
|
47
|
+
axis: SnapAxis;
|
|
48
|
+
pos: number;
|
|
49
|
+
from: number;
|
|
50
|
+
to: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SnapResult {
|
|
54
|
+
/** Possibly-snapped top-left X of the proposed rect (in world coords). */
|
|
55
|
+
x: number;
|
|
56
|
+
/** Possibly-snapped top-left Y. */
|
|
57
|
+
y: number;
|
|
58
|
+
/** Guides to render. 0..2 entries in the common case (one per axis). */
|
|
59
|
+
guides: SnapGuide[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SnapOptions {
|
|
63
|
+
/** World-units between grid lines. Default 40. */
|
|
64
|
+
gridSize: number;
|
|
65
|
+
/** Max world-unit distance at which a candidate is considered "close". */
|
|
66
|
+
tolerance: number;
|
|
67
|
+
/** Skip all snap math + return proposed unchanged (Alt-held bypass). */
|
|
68
|
+
disabled: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface AxisCandidate {
|
|
72
|
+
/** Shift to apply to proposed on this axis. */
|
|
73
|
+
delta: number;
|
|
74
|
+
/** Position of the guide line in world coords (on the snapped axis). */
|
|
75
|
+
pos: number;
|
|
76
|
+
/** Perpendicular extent — `from..to` of the would-be guide. */
|
|
77
|
+
from: number;
|
|
78
|
+
to: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function nearestGridDelta(coord: number, gridSize: number, tolerance: number): number | null {
|
|
82
|
+
if (!Number.isFinite(coord) || gridSize <= 0) return null;
|
|
83
|
+
const nearest = Math.round(coord / gridSize) * gridSize;
|
|
84
|
+
const delta = nearest - coord;
|
|
85
|
+
return Math.abs(delta) <= tolerance ? delta : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pickClosest(cands: AxisCandidate[]): AxisCandidate | null {
|
|
89
|
+
if (cands.length === 0) return null;
|
|
90
|
+
let best = cands[0] as AxisCandidate;
|
|
91
|
+
let bestAbs = Math.abs(best.delta);
|
|
92
|
+
for (let i = 1; i < cands.length; i++) {
|
|
93
|
+
const c = cands[i] as AxisCandidate;
|
|
94
|
+
const a = Math.abs(c.delta);
|
|
95
|
+
if (a < bestAbs) {
|
|
96
|
+
best = c;
|
|
97
|
+
bestAbs = a;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return best;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Among `cands`, find every entry whose `pos` matches `winner.pos` (within a
|
|
105
|
+
* tiny epsilon to absorb floating-point noise) and union their from/to into
|
|
106
|
+
* one guide. Returns the merged guide.
|
|
107
|
+
*/
|
|
108
|
+
function mergeAtPos(axis: SnapAxis, winner: AxisCandidate, cands: AxisCandidate[]): SnapGuide {
|
|
109
|
+
let from = winner.from;
|
|
110
|
+
let to = winner.to;
|
|
111
|
+
for (const c of cands) {
|
|
112
|
+
if (Math.abs(c.pos - winner.pos) > 0.001) continue;
|
|
113
|
+
if (c.from < from) from = c.from;
|
|
114
|
+
if (c.to > to) to = c.to;
|
|
115
|
+
}
|
|
116
|
+
return { axis, pos: winner.pos, from, to };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function computeSnap(proposed: Rect, others: Rect[], opts: SnapOptions): SnapResult {
|
|
120
|
+
if (opts.disabled) {
|
|
121
|
+
return { x: proposed.x, y: proposed.y, guides: [] };
|
|
122
|
+
}
|
|
123
|
+
const { gridSize, tolerance } = opts;
|
|
124
|
+
const propLeft = proposed.x;
|
|
125
|
+
const propRight = proposed.x + proposed.w;
|
|
126
|
+
const propCenterX = proposed.x + proposed.w / 2;
|
|
127
|
+
const propTop = proposed.y;
|
|
128
|
+
const propBottom = proposed.y + proposed.h;
|
|
129
|
+
const propCenterY = proposed.y + proposed.h / 2;
|
|
130
|
+
|
|
131
|
+
const xCands: AxisCandidate[] = [];
|
|
132
|
+
const yCands: AxisCandidate[] = [];
|
|
133
|
+
|
|
134
|
+
// Grid candidates — left edge for X, top edge for Y. Perpendicular extent
|
|
135
|
+
// is just the proposed rect's own range (no sibling involved).
|
|
136
|
+
const gridX = nearestGridDelta(propLeft, gridSize, tolerance);
|
|
137
|
+
if (gridX !== null) {
|
|
138
|
+
xCands.push({
|
|
139
|
+
delta: gridX,
|
|
140
|
+
pos: propLeft + gridX,
|
|
141
|
+
from: propTop,
|
|
142
|
+
to: propBottom,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const gridY = nearestGridDelta(propTop, gridSize, tolerance);
|
|
146
|
+
if (gridY !== null) {
|
|
147
|
+
yCands.push({
|
|
148
|
+
delta: gridY,
|
|
149
|
+
pos: propTop + gridY,
|
|
150
|
+
from: propLeft,
|
|
151
|
+
to: propRight,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Sibling candidates.
|
|
156
|
+
for (const other of others) {
|
|
157
|
+
const oLeft = other.x;
|
|
158
|
+
const oRight = other.x + other.w;
|
|
159
|
+
const oCenterX = other.x + other.w / 2;
|
|
160
|
+
const oTop = other.y;
|
|
161
|
+
const oBottom = other.y + other.h;
|
|
162
|
+
const oCenterY = other.y + other.h / 2;
|
|
163
|
+
|
|
164
|
+
// X-axis pairs: (propCoord, otherCoord).
|
|
165
|
+
const xPairs: Array<[number, number]> = [
|
|
166
|
+
[propLeft, oLeft],
|
|
167
|
+
[propRight, oRight],
|
|
168
|
+
[propLeft, oRight],
|
|
169
|
+
[propRight, oLeft],
|
|
170
|
+
[propCenterX, oCenterX],
|
|
171
|
+
];
|
|
172
|
+
for (const [propCoord, otherCoord] of xPairs) {
|
|
173
|
+
const delta = otherCoord - propCoord;
|
|
174
|
+
if (Math.abs(delta) > tolerance) continue;
|
|
175
|
+
xCands.push({
|
|
176
|
+
delta,
|
|
177
|
+
pos: otherCoord,
|
|
178
|
+
from: Math.min(propTop, oTop),
|
|
179
|
+
to: Math.max(propBottom, oBottom),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Y-axis pairs.
|
|
184
|
+
const yPairs: Array<[number, number]> = [
|
|
185
|
+
[propTop, oTop],
|
|
186
|
+
[propBottom, oBottom],
|
|
187
|
+
[propTop, oBottom],
|
|
188
|
+
[propBottom, oTop],
|
|
189
|
+
[propCenterY, oCenterY],
|
|
190
|
+
];
|
|
191
|
+
for (const [propCoord, otherCoord] of yPairs) {
|
|
192
|
+
const delta = otherCoord - propCoord;
|
|
193
|
+
if (Math.abs(delta) > tolerance) continue;
|
|
194
|
+
yCands.push({
|
|
195
|
+
delta,
|
|
196
|
+
pos: otherCoord,
|
|
197
|
+
from: Math.min(propLeft, oLeft),
|
|
198
|
+
to: Math.max(propRight, oRight),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const winX = pickClosest(xCands);
|
|
204
|
+
const winY = pickClosest(yCands);
|
|
205
|
+
|
|
206
|
+
const guides: SnapGuide[] = [];
|
|
207
|
+
if (winX) guides.push(mergeAtPos('x', winX, xCands));
|
|
208
|
+
if (winY) guides.push(mergeAtPos('y', winY, yCands));
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
x: proposed.x + (winX ? winX.delta : 0),
|
|
212
|
+
y: proposed.y + (winY ? winY.delta : 0),
|
|
213
|
+
guides,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file use-tool-mode.tsx — Phase 4.1 tool-mode store
|
|
3
|
+
* @scope plugins/design/dev-server/use-tool-mode.tsx
|
|
4
|
+
* @purpose Context + hook for the active canvas tool. Wired into
|
|
5
|
+
* DesignCanvas. Phase 5 will
|
|
6
|
+
* register additional tools (pen, circle, arrow, eraser) via
|
|
7
|
+
* the same provider — the API is intentionally open.
|
|
8
|
+
*
|
|
9
|
+
* The router's `onTool` callback (input-router.tsx) writes into this store.
|
|
10
|
+
* The ToolPalette + cursor sync read from it. Selecting a tool also mutates
|
|
11
|
+
* `document.body.style.cursor` so the affordance matches across the iframe.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
type ReactNode,
|
|
16
|
+
createContext,
|
|
17
|
+
useCallback,
|
|
18
|
+
useContext,
|
|
19
|
+
useEffect,
|
|
20
|
+
useMemo,
|
|
21
|
+
useState,
|
|
22
|
+
} from 'react';
|
|
23
|
+
|
|
24
|
+
import type { Tool } from './input-router.tsx';
|
|
25
|
+
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// Types
|
|
28
|
+
|
|
29
|
+
export interface ToolDescriptor {
|
|
30
|
+
id: Tool;
|
|
31
|
+
label: string;
|
|
32
|
+
/** Letter-key shortcut shown in the palette tooltip. */
|
|
33
|
+
shortcut: string;
|
|
34
|
+
/** CSS cursor value applied to <body> when this tool is active. */
|
|
35
|
+
cursor: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const DEFAULT_TOOLS: readonly ToolDescriptor[] = Object.freeze([
|
|
39
|
+
{ id: 'move', label: 'Move', shortcut: 'V', cursor: 'default' },
|
|
40
|
+
{ id: 'hand', label: 'Hand', shortcut: 'H', cursor: 'grab' },
|
|
41
|
+
{ id: 'comment', label: 'Comment', shortcut: 'C', cursor: 'crosshair' },
|
|
42
|
+
// Phase 5 — draw / annotation tools. Cursors stay as `crosshair` for pen /
|
|
43
|
+
// rect / arrow (the pen-tip glyph is reserved for the system text caret).
|
|
44
|
+
// Eraser uses `cell` as the closest cross-browser substitute for a rubber
|
|
45
|
+
// affordance (no native rubber cursor exists).
|
|
46
|
+
{ id: 'pen', label: 'Pen', shortcut: 'B', cursor: 'crosshair' },
|
|
47
|
+
{ id: 'rect', label: 'Rect', shortcut: 'R', cursor: 'crosshair' },
|
|
48
|
+
{ id: 'ellipse', label: 'Ellipse', shortcut: 'O', cursor: 'crosshair' },
|
|
49
|
+
{ id: 'arrow', label: 'Arrow', shortcut: 'A', cursor: 'crosshair' },
|
|
50
|
+
{ id: 'eraser', label: 'Eraser', shortcut: 'E', cursor: 'cell' },
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
interface ToolContextValue {
|
|
54
|
+
tool: Tool;
|
|
55
|
+
setTool: (t: Tool) => void;
|
|
56
|
+
tools: readonly ToolDescriptor[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ToolContext = createContext<ToolContextValue | null>(null);
|
|
60
|
+
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
// Provider
|
|
63
|
+
|
|
64
|
+
export function ToolProvider({
|
|
65
|
+
children,
|
|
66
|
+
tools = DEFAULT_TOOLS,
|
|
67
|
+
initial = 'move',
|
|
68
|
+
}: {
|
|
69
|
+
children: ReactNode;
|
|
70
|
+
tools?: readonly ToolDescriptor[];
|
|
71
|
+
initial?: Tool;
|
|
72
|
+
}) {
|
|
73
|
+
const [tool, setToolState] = useState<Tool>(initial);
|
|
74
|
+
const setTool = useCallback((t: Tool) => setToolState(t), []);
|
|
75
|
+
|
|
76
|
+
// Body cursor sync — applied to the canvas iframe's body (this hook runs
|
|
77
|
+
// inside the iframe context). The viewport-controller still owns the
|
|
78
|
+
// grabbing/grab cursor swap during space-pan; this only sets the resting
|
|
79
|
+
// cursor for the active tool.
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (typeof document === 'undefined') return;
|
|
82
|
+
const desc = tools.find((t) => t.id === tool);
|
|
83
|
+
if (!desc) return;
|
|
84
|
+
const prev = document.body.style.cursor;
|
|
85
|
+
document.body.style.cursor = desc.cursor;
|
|
86
|
+
return () => {
|
|
87
|
+
document.body.style.cursor = prev;
|
|
88
|
+
};
|
|
89
|
+
}, [tool, tools]);
|
|
90
|
+
|
|
91
|
+
const value = useMemo<ToolContextValue>(() => ({ tool, setTool, tools }), [tool, setTool, tools]);
|
|
92
|
+
|
|
93
|
+
return <ToolContext.Provider value={value}>{children}</ToolContext.Provider>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
// Hook
|
|
98
|
+
|
|
99
|
+
export function useToolMode(): ToolContextValue {
|
|
100
|
+
const ctx = useContext(ToolContext);
|
|
101
|
+
if (!ctx) {
|
|
102
|
+
throw new Error('useToolMode must be used inside <ToolProvider>');
|
|
103
|
+
}
|
|
104
|
+
return ctx;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Read-only variant — returns `null` when no provider mounted. Used by
|
|
109
|
+
* components that can render outside a ToolProvider tree (the input
|
|
110
|
+
* router's optional path).
|
|
111
|
+
*/
|
|
112
|
+
export function useToolModeOptional(): ToolContextValue | null {
|
|
113
|
+
return useContext(ToolContext);
|
|
114
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Bun.serve native WebSocket handlers — replaces server.mjs's hand-rolled
|
|
2
|
+
// RFC-6455 upgrade. Per-connection state lives on ws.data (Bun's typed slot).
|
|
3
|
+
|
|
4
|
+
import type { ServerWebSocket, WebSocketHandler } from 'bun';
|
|
5
|
+
|
|
6
|
+
import type { Api } from './api.ts';
|
|
7
|
+
import type { Context } from './context.ts';
|
|
8
|
+
import { createHmrBroadcaster } from './hmr-broadcast.ts';
|
|
9
|
+
import type { Inspect } from './inspect.ts';
|
|
10
|
+
|
|
11
|
+
export interface WsData {
|
|
12
|
+
id: string;
|
|
13
|
+
remote: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Ws {
|
|
17
|
+
handler: WebSocketHandler<WsData>;
|
|
18
|
+
broadcast(payload: unknown): void;
|
|
19
|
+
clientCount(): number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createWs(ctx: Context, api: Api, inspect: Inspect): Ws {
|
|
23
|
+
const clients = new Set<ServerWebSocket<WsData>>();
|
|
24
|
+
|
|
25
|
+
function send(ws: ServerWebSocket<WsData>, payload: unknown) {
|
|
26
|
+
try {
|
|
27
|
+
ws.send(typeof payload === 'string' ? payload : JSON.stringify(payload));
|
|
28
|
+
} catch {
|
|
29
|
+
/* dead socket — close handler will clean up */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function broadcast(payload: unknown) {
|
|
34
|
+
const msg = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
35
|
+
for (const ws of clients) send(ws, msg);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Wire bus -> WS broadcasts. inspect.ts emits 'selected' / 'active' after every
|
|
39
|
+
// state write; fs-watch.ts emits 'fs:*' on every save.
|
|
40
|
+
ctx.bus.on('selected', (sel) => broadcast({ type: 'selected', selected: sel }));
|
|
41
|
+
ctx.bus.on('active', (file) => broadcast({ type: 'active', file }));
|
|
42
|
+
ctx.bus.on('fs:html', (file) => broadcast({ type: 'fs:html', file }));
|
|
43
|
+
ctx.bus.on('fs:css', (file) => broadcast({ type: 'fs:css', file }));
|
|
44
|
+
ctx.bus.on('fs:json', (file) => broadcast({ type: 'fs:json', file }));
|
|
45
|
+
ctx.bus.on('comments', ({ file, comments }: { file: string; comments: unknown[] }) =>
|
|
46
|
+
broadcast({ type: 'comments', file, comments })
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// HMR broadcaster — turns fs:any change events into `canvas-hmr` messages.
|
|
50
|
+
// The iframe-side client (in _shell.html) decides reload strategy from `mode`.
|
|
51
|
+
createHmrBroadcaster(ctx, (msg) => broadcast(msg));
|
|
52
|
+
|
|
53
|
+
const handler: WebSocketHandler<WsData> = {
|
|
54
|
+
open(ws) {
|
|
55
|
+
clients.add(ws);
|
|
56
|
+
send(ws, { type: 'snapshot', state: inspect.state });
|
|
57
|
+
},
|
|
58
|
+
close(ws) {
|
|
59
|
+
clients.delete(ws);
|
|
60
|
+
},
|
|
61
|
+
async message(ws, raw) {
|
|
62
|
+
// biome-ignore lint/suspicious/noExplicitAny: JSON.parse result; narrowed by runtime discriminator checks below.
|
|
63
|
+
let msg: any;
|
|
64
|
+
try {
|
|
65
|
+
msg = JSON.parse(typeof raw === 'string' ? raw : new TextDecoder().decode(raw));
|
|
66
|
+
} catch {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!msg || typeof msg !== 'object') return;
|
|
70
|
+
try {
|
|
71
|
+
if (msg.type === 'active' && typeof msg.file === 'string') inspect.setActive(msg.file);
|
|
72
|
+
else if (msg.type === 'tabs' && Array.isArray(msg.tabs)) inspect.setOpenTabs(msg.tabs);
|
|
73
|
+
else if (msg.type === 'select' && msg.selection) inspect.setSelected(msg.selection);
|
|
74
|
+
else if (msg.type === 'clear-select') inspect.setSelected(null);
|
|
75
|
+
else if (msg.type === 'comments-add' && msg.payload) await api.commentsAdd(msg.payload);
|
|
76
|
+
else if (msg.type === 'comments-patch' && msg.id)
|
|
77
|
+
await api.commentsPatch(msg.id, msg.patch || {});
|
|
78
|
+
else if (msg.type === 'comments-delete' && msg.id) await api.commentsDelete(msg.id);
|
|
79
|
+
else if (msg.type === 'comments-request' && typeof msg.file === 'string') {
|
|
80
|
+
const comments = await api.loadCommentsForFile(msg.file);
|
|
81
|
+
send(ws, { type: 'comments', file: msg.file, comments });
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error('[ws] message handler threw:', err);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return { handler, broadcast, clientCount: () => clients.size };
|
|
90
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!--
|
|
3
|
+
_shell.html — shared canvas mount harness (DDR-019, Phase 3.6).
|
|
4
|
+
|
|
5
|
+
How it works:
|
|
6
|
+
1. The dev-server's TSX-canvas iframe loads this file with
|
|
7
|
+
?canvas=<path-relative-to-designRoot>
|
|
8
|
+
(e.g. ?canvas=ui/Docs%20Site.tsx).
|
|
9
|
+
2. The importmap below resolves "react", "react-dom/client",
|
|
10
|
+
"react/jsx-runtime", "react/jsx-dev-runtime" against the four
|
|
11
|
+
/_canvas-runtime/*.js bundles served by the dev-server. The browser
|
|
12
|
+
only fetches each bundle once — every canvas in the session shares them.
|
|
13
|
+
3. The canvas TSX module is fetched from the same path under designRoot.
|
|
14
|
+
The dev-server's /<designRel>/ui/<slug>.tsx route runs canvas-pipeline
|
|
15
|
+
(data-cd-id injection) then Bun.build (browser-loadable ESM) and serves
|
|
16
|
+
the result with Content-Type application/javascript.
|
|
17
|
+
4. We grab the default export, wrap it in React's createRoot, and render
|
|
18
|
+
into <div id="canvas-root">. The token + component CSS files load
|
|
19
|
+
cooperatively — same href as for legacy .html canvases, so the visual
|
|
20
|
+
contract is identical.
|
|
21
|
+
|
|
22
|
+
This file is part of the plugin distribution; the dev-server serves it
|
|
23
|
+
directly from plugins/design/templates/. It is intentionally NOT copied
|
|
24
|
+
into <designRoot> — the dev-server is its single source of truth.
|
|
25
|
+
-->
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="utf-8" />
|
|
29
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
30
|
+
<title>Canvas</title>
|
|
31
|
+
<style>
|
|
32
|
+
:root {
|
|
33
|
+
color-scheme: light dark;
|
|
34
|
+
}
|
|
35
|
+
html, body, #canvas-root { height: 100%; margin: 0; }
|
|
36
|
+
body { background: var(--u-bg-0, #fff); }
|
|
37
|
+
#canvas-mount-error {
|
|
38
|
+
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
39
|
+
padding: 16px 20px;
|
|
40
|
+
color: #b91c1c;
|
|
41
|
+
background: #fff5f5;
|
|
42
|
+
white-space: pre-wrap;
|
|
43
|
+
}
|
|
44
|
+
#canvas-mount-error:empty { display: none; }
|
|
45
|
+
</style>
|
|
46
|
+
<!-- Tokens + DS component CSS — loaded by the host app via the canvas's
|
|
47
|
+
.meta.json (set later via window.parent postMessage, OR by the dev-
|
|
48
|
+
server when it knows the active DS). For now we load both common
|
|
49
|
+
paths; one will 404 silently per DS depending on layout. -->
|
|
50
|
+
<link id="canvas-tokens" rel="stylesheet" href="" data-canvas-css="tokens" />
|
|
51
|
+
<link id="canvas-components" rel="stylesheet" href="" data-canvas-css="components" />
|
|
52
|
+
<script type="importmap">
|
|
53
|
+
{
|
|
54
|
+
"imports": {
|
|
55
|
+
"react": "/_canvas-runtime/react.js",
|
|
56
|
+
"react-dom": "/_canvas-runtime/react-dom.js",
|
|
57
|
+
"react-dom/client": "/_canvas-runtime/react-dom_client.js",
|
|
58
|
+
"react/jsx-runtime": "/_canvas-runtime/react_jsx-runtime.js",
|
|
59
|
+
"react/jsx-dev-runtime": "/_canvas-runtime/react_jsx-dev-runtime.js",
|
|
60
|
+
"pixi.js": "/_canvas-runtime/pixi-js.js"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<div id="canvas-root"></div>
|
|
67
|
+
<pre id="canvas-mount-error"></pre>
|
|
68
|
+
|
|
69
|
+
<script type="module">
|
|
70
|
+
const params = new URLSearchParams(location.search);
|
|
71
|
+
const canvasRel = params.get('canvas');
|
|
72
|
+
const designRel = params.get('designRel') || '.design';
|
|
73
|
+
const tokensRel = params.get('tokens') || '';
|
|
74
|
+
const componentsRel = params.get('components') || '';
|
|
75
|
+
const layoutRel = params.get('layout') || '';
|
|
76
|
+
|
|
77
|
+
function showError(msg) {
|
|
78
|
+
const el = document.getElementById('canvas-mount-error');
|
|
79
|
+
if (el) el.textContent = msg;
|
|
80
|
+
console.error('[canvas-shell]', msg);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------
|
|
84
|
+
// HMR client — listens for `canvas-hmr` messages on /_ws. Three modes:
|
|
85
|
+
// - css: swap <link> href with ?v=<version> cache bust.
|
|
86
|
+
// - module: location.reload() — the canvas module re-fetches.
|
|
87
|
+
// - hard: location.reload() — _lib/* changed, everything re-bundles.
|
|
88
|
+
// The WS connection retries on close (dev-server restart resilience).
|
|
89
|
+
function connectHmr() {
|
|
90
|
+
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
91
|
+
const ws = new WebSocket(proto + '://' + location.host + '/_ws');
|
|
92
|
+
ws.addEventListener('message', (ev) => {
|
|
93
|
+
try {
|
|
94
|
+
const msg = JSON.parse(ev.data);
|
|
95
|
+
if (!msg || msg.type !== 'canvas-hmr') return;
|
|
96
|
+
if (msg.mode === 'css') {
|
|
97
|
+
const v = msg.version || Date.now();
|
|
98
|
+
// Match by exact filename when we have one; otherwise refresh all.
|
|
99
|
+
const targetFile = (msg.file || '').split('/').pop();
|
|
100
|
+
for (const link of document.querySelectorAll('link[rel="stylesheet"]')) {
|
|
101
|
+
const href = link.getAttribute('href') || '';
|
|
102
|
+
if (!targetFile || href.includes(targetFile)) {
|
|
103
|
+
const base = href.split('?')[0];
|
|
104
|
+
link.setAttribute('href', base + '?v=' + v);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} else if (msg.mode === 'module' || msg.mode === 'hard') {
|
|
108
|
+
// Only reload when the change touches *this* canvas (or `_lib`).
|
|
109
|
+
if (msg.mode === 'hard' || !msg.file || (canvasRel && msg.file === canvasRel)) {
|
|
110
|
+
location.reload();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.warn('[canvas-shell] HMR message parse error', e);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
ws.addEventListener('close', () => {
|
|
118
|
+
// Reconnect after a beat — dev-server may have restarted.
|
|
119
|
+
setTimeout(connectHmr, 750);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
connectHmr();
|
|
123
|
+
|
|
124
|
+
if (!canvasRel) {
|
|
125
|
+
showError('Missing ?canvas= query parameter. Add ?canvas=ui/<slug>.tsx.');
|
|
126
|
+
} else {
|
|
127
|
+
if (tokensRel) document.getElementById('canvas-tokens').href = '/' + designRel + '/' + tokensRel;
|
|
128
|
+
if (componentsRel) document.getElementById('canvas-components').href = '/' + designRel + '/' + componentsRel;
|
|
129
|
+
if (layoutRel) {
|
|
130
|
+
// Inject a third <link> for the DS layout chrome (specimens load this).
|
|
131
|
+
const layoutLink = document.createElement('link');
|
|
132
|
+
layoutLink.rel = 'stylesheet';
|
|
133
|
+
layoutLink.href = '/' + designRel + '/' + layoutRel;
|
|
134
|
+
layoutLink.dataset.canvasCss = 'layout';
|
|
135
|
+
document.head.appendChild(layoutLink);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const canvasUrl = '/' + designRel + '/' + canvasRel;
|
|
139
|
+
// Phase 4 T5 — read the canvas's sibling .meta.json and stash it on
|
|
140
|
+
// window so DesignCanvas can seed `layout` + `viewport` synchronously.
|
|
141
|
+
// The fetch runs in parallel with imports; failure is non-fatal (the
|
|
142
|
+
// canvas falls through to default grid + fit-to-screen).
|
|
143
|
+
const metaFileParam = encodeURIComponent(designRel + '/' + canvasRel);
|
|
144
|
+
const metaFetch = fetch('/_api/canvas-meta?file=' + metaFileParam, {
|
|
145
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
146
|
+
})
|
|
147
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
148
|
+
.then((j) => {
|
|
149
|
+
window.__canvas_meta__ = (j && typeof j === 'object') ? j : {};
|
|
150
|
+
window.__canvas_meta_file__ = designRel + '/' + canvasRel;
|
|
151
|
+
})
|
|
152
|
+
.catch(() => {
|
|
153
|
+
window.__canvas_meta__ = {};
|
|
154
|
+
window.__canvas_meta_file__ = designRel + '/' + canvasRel;
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
const [{ createRoot }, mod, react] = await Promise.all([
|
|
158
|
+
import('react-dom/client'),
|
|
159
|
+
import(canvasUrl),
|
|
160
|
+
import('react'),
|
|
161
|
+
metaFetch,
|
|
162
|
+
]);
|
|
163
|
+
const Canvas = mod.default;
|
|
164
|
+
if (typeof Canvas !== 'function') {
|
|
165
|
+
throw new Error(
|
|
166
|
+
'Canvas module at ' + canvasUrl + ' has no default export (or it is not a component).'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const root = createRoot(document.getElementById('canvas-root'));
|
|
170
|
+
root.render(react.createElement(Canvas));
|
|
171
|
+
} catch (err) {
|
|
172
|
+
showError((err && err.stack) || String(err));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
</script>
|
|
176
|
+
</body>
|
|
177
|
+
</html>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @canvas {{NAME}} — {{SUBTITLE}}
|
|
3
|
+
* @ds {{DS_NAME}}
|
|
4
|
+
* @platform {{PLATFORM}}
|
|
5
|
+
* @opt_out {{OPT_OUT_SCOPE}}
|
|
6
|
+
* @artboards {{ARTBOARDS}}
|
|
7
|
+
* @brief {{BRIEF}}
|
|
8
|
+
* @stack React 19 · TSX · Bun.build · css_mode={{CSS_MODE}}
|
|
9
|
+
* @history {{HISTORY_DIR}}
|
|
10
|
+
* @handoff bunx shadcn add file://./{{NAME}}.registry.json
|
|
11
|
+
*
|
|
12
|
+
* Authored under {{DS_NAME}}. Tokens + shared component classes load via the
|
|
13
|
+
* dev-server's _shell.html harness — link tags arrive automatically from the
|
|
14
|
+
* iframe's ?tokens= / ?components= query. Class names match the DS's
|
|
15
|
+
* `_components.css` (`.btn`, `.tile`, `.sku`, `.seg`, ...). Bespoke classes go
|
|
16
|
+
* into a sibling .module.css iff `css_mode === "modules"`; for `inline` (the
|
|
17
|
+
* MDCC-DSN/01 default) prefer existing classes + `style={{}}` for arbitrary
|
|
18
|
+
* one-off values.
|
|
19
|
+
*
|
|
20
|
+
* The envelope primitives (`DesignCanvas`, `DCSection`, `DCArtboard`) and any
|
|
21
|
+
* specimen helpers come from the dev-server-bundled canvas library via the
|
|
22
|
+
* virtual specifier — the dev-server resolves it to
|
|
23
|
+
* `plugins/design/dev-server/canvas-lib.tsx` (single source, ships with the
|
|
24
|
+
* dev-server install per DDR-025), and `/design:handoff` AST-inlines the used
|
|
25
|
+
* exports so the registry-item drop is self-contained. Read that file for the
|
|
26
|
+
* full export surface (helpers + hooks) before reaching for new components.
|
|
27
|
+
*
|
|
28
|
+
* `data-cd-id="<8 hex>"` attributes are injected at request time by the dev
|
|
29
|
+
* server (canvas-pipeline.ts pass 1). Don't author them by hand — they'd be
|
|
30
|
+
* stripped + replaced. `/design:handoff` strips them again before the registry
|
|
31
|
+
* sidecar leaves dev (production has no business with dev-time scaffolding).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { useState } from "react";
|
|
35
|
+
|
|
36
|
+
import { DesignCanvas, DCSection, DCArtboard } from "@maude/canvas-lib";
|
|
37
|
+
|
|
38
|
+
export default function {{COMPONENT_NAME}}() {
|
|
39
|
+
return (
|
|
40
|
+
<DesignCanvas>
|
|
41
|
+
<DCSection id="overview" title="{{SUBTITLE}}">
|
|
42
|
+
<DCArtboard id="primary" label="A · primary" width={1280} height={800}>
|
|
43
|
+
<div className="mdcc" data-theme="{{THEME_DEFAULT}}">
|
|
44
|
+
{/* Replace this scaffold with the actual canvas content. The
|
|
45
|
+
envelope this template was rendered against is in
|
|
46
|
+
{{HISTORY_DIR}}/000-envelope.md. */}
|
|
47
|
+
<h1 className="sku">{{NAME}}</h1>
|
|
48
|
+
<p>{{BRIEF}}</p>
|
|
49
|
+
</div>
|
|
50
|
+
</DCArtboard>
|
|
51
|
+
</DCSection>
|
|
52
|
+
</DesignCanvas>
|
|
53
|
+
);
|
|
54
|
+
}
|