@1agh/maude 0.19.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/cli/bin/maude.mjs +5 -1
- package/cli/commands/design-link.test.mjs +207 -0
- package/cli/commands/design.mjs +42 -12
- package/cli/commands/doctor.mjs +350 -0
- package/cli/commands/doctor.test.mjs +185 -0
- package/cli/commands/help.mjs +24 -0
- package/cli/commands/hub.mjs +231 -0
- package/cli/commands/hub.test.mjs +87 -0
- package/cli/lib/config-lint.mjs +141 -0
- package/cli/lib/config-lint.test.mjs +117 -0
- package/cli/lib/design-link.mjs +216 -0
- package/cli/lib/hubs-config.mjs +123 -0
- package/cli/lib/hubs-config.test.mjs +100 -0
- package/cli/lib/preflight.mjs +232 -0
- package/cli/lib/stack-detect.mjs +344 -0
- package/cli/lib/stack-detect.test.mjs +121 -0
- package/package.json +17 -9
- package/plugins/design/dependencies.json +147 -0
- package/plugins/design/dependencies.schema.json +107 -0
- package/plugins/design/dev-server/ai-banner.tsx +188 -0
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +115 -41
- package/plugins/design/dev-server/annotations-layer.tsx +256 -107
- package/plugins/design/dev-server/api.ts +17 -1
- package/plugins/design/dev-server/artboard-marquee.tsx +10 -6
- package/plugins/design/dev-server/bin/asset-sweep.sh +180 -0
- package/plugins/design/dev-server/bin/preflight.sh +32 -0
- package/plugins/design/dev-server/bin/runtime-health.sh +169 -0
- package/plugins/design/dev-server/bin/visual-sanity.sh +221 -0
- package/plugins/design/dev-server/canvas-lib.tsx +534 -32
- package/plugins/design/dev-server/canvas-shell.tsx +479 -29
- package/plugins/design/dev-server/client/app.jsx +72 -0
- package/plugins/design/dev-server/client/hmr.mjs +28 -0
- package/plugins/design/dev-server/collab/ai-activity.ts +127 -0
- package/plugins/design/dev-server/collab/git-lifecycle.ts +124 -0
- package/plugins/design/dev-server/collab/index.ts +47 -0
- package/plugins/design/dev-server/collab/persistence.ts +123 -0
- package/plugins/design/dev-server/collab/protocol.ts +108 -0
- package/plugins/design/dev-server/collab/registry.ts +110 -0
- package/plugins/design/dev-server/collab/room.ts +215 -0
- package/plugins/design/dev-server/commands/annotation-strokes-command.ts +127 -0
- package/plugins/design/dev-server/commands/equal-spacing-command.ts +28 -0
- package/plugins/design/dev-server/commands/move-artboards-command.ts +158 -0
- package/plugins/design/dev-server/comments-overlay.tsx +41 -4
- package/plugins/design/dev-server/contextual-toolbar.tsx +241 -0
- package/plugins/design/dev-server/cursors-overlay.tsx +296 -0
- package/plugins/design/dev-server/dist/client.bundle.js +75 -3
- package/plugins/design/dev-server/dist/runtime/motion.js +165 -0
- package/plugins/design/dev-server/dist/runtime/motion_react.js +409 -0
- package/plugins/design/dev-server/dist/runtime/y-protocols_awareness.js +587 -0
- package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +257 -0
- package/plugins/design/dev-server/dist/runtime/yjs.js +7107 -0
- package/plugins/design/dev-server/equal-spacing-detector.ts +98 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +289 -0
- package/plugins/design/dev-server/export-dialog.tsx +1 -1
- package/plugins/design/dev-server/handoff.ts +24 -0
- package/plugins/design/dev-server/hmr-broadcast.ts +15 -2
- package/plugins/design/dev-server/http.ts +91 -1
- package/plugins/design/dev-server/input-router.tsx +52 -2
- package/plugins/design/dev-server/marquee-overlay.tsx +282 -0
- package/plugins/design/dev-server/participants-chrome.tsx +261 -0
- package/plugins/design/dev-server/runtime-bundle.ts +15 -0
- package/plugins/design/dev-server/server.ts +78 -11
- package/plugins/design/dev-server/test/ai-activity.test.ts +113 -0
- package/plugins/design/dev-server/test/annotation-strokes-command.test.ts +149 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +44 -0
- package/plugins/design/dev-server/test/canvas-lib-motion.test.ts +131 -0
- package/plugins/design/dev-server/test/collab-annotations-bridge.test.ts +55 -0
- package/plugins/design/dev-server/test/collab-bridge.test.ts +81 -0
- package/plugins/design/dev-server/test/collab-loopback.test.ts +63 -0
- package/plugins/design/dev-server/test/collab-protocol.test.ts +146 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +182 -0
- package/plugins/design/dev-server/test/collab-stress.test.ts +121 -0
- package/plugins/design/dev-server/test/equal-spacing-detector.test.ts +93 -0
- package/plugins/design/dev-server/test/git-lifecycle.test.ts +101 -0
- package/plugins/design/dev-server/test/input-router.test.ts +87 -1
- package/plugins/design/dev-server/test/marquee-overlay.test.ts +94 -0
- package/plugins/design/dev-server/test/move-artboards-command.test.ts +108 -0
- package/plugins/design/dev-server/test/participants-chrome.test.ts +30 -0
- package/plugins/design/dev-server/test/undo-stack.test.ts +211 -0
- package/plugins/design/dev-server/test/use-collab.test.ts +71 -0
- package/plugins/design/dev-server/test/use-cursor-modifiers.test.ts +59 -0
- package/plugins/design/dev-server/test/use-keyboard-discipline.test.ts +27 -0
- package/plugins/design/dev-server/test/use-undo-stack.test.tsx +193 -0
- package/plugins/design/dev-server/tool-palette.tsx +7 -7
- package/plugins/design/dev-server/undo-hud.tsx +95 -0
- package/plugins/design/dev-server/undo-stack.ts +240 -0
- package/plugins/design/dev-server/use-annotation-resize.tsx +1 -1
- package/plugins/design/dev-server/use-artboard-drag.tsx +6 -3
- package/plugins/design/dev-server/use-collab.tsx +478 -0
- package/plugins/design/dev-server/use-cursor-modifiers.tsx +122 -0
- package/plugins/design/dev-server/use-keyboard-discipline.tsx +125 -0
- package/plugins/design/dev-server/use-undo-stack.tsx +355 -0
- package/plugins/design/dev-server/ws.ts +123 -7
- package/plugins/design/templates/_shell.html +51 -6
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +245 -0
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +2 -2
- package/plugins/design/templates/design-system-inspiration/core/preview/_components.css.tpl +129 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +63 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/motion.css.tpl +106 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/motion.tsx.tpl +208 -0
- package/plugins/flow/.claude-plugin/config.schema.json +12 -0
- package/plugins/flow/dependencies.json +143 -0
- package/plugins/flow/dependencies.schema.json +107 -0
- /package/plugins/design/templates/design-system-inspiration/core/preview/{motion.html → .archive/motion.html} +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file undo-stack.ts — per-canvas in-memory command stack
|
|
3
|
+
* @scope plugins/design/dev-server/undo-stack.ts
|
|
4
|
+
* @purpose Pure state + reducer for canvas Cmd+Z / Cmd+Shift+Z. No React,
|
|
5
|
+
* no DOM, no fetch — `EditCommand.do()` / `.undo()` are caller-
|
|
6
|
+
* supplied side-effects so this file stays trivially testable
|
|
7
|
+
* under `bun:test`.
|
|
8
|
+
*
|
|
9
|
+
* Scope (DDR-050 rev 2):
|
|
10
|
+
* - **Persistent per-canvas, in-memory, session-scoped.** The stack lives
|
|
11
|
+
* in `window.top.__maude_undo_stacks` keyed by canvas file path so it
|
|
12
|
+
* survives canvas switches (close Foo.tsx, open Bar.tsx, come back to
|
|
13
|
+
* Foo.tsx → history still there). Reload destroys it. The original
|
|
14
|
+
* "per-iframe-ephemeral" rule from rev 1 was UX-wrong.
|
|
15
|
+
* - Stack stores SERIALIZABLE `CommandRecord`s (kind + label + payload),
|
|
16
|
+
* NOT EditCommand closures. Closures captured by patchFn/putFn point
|
|
17
|
+
* to the iframe's React state — when that iframe unmounts (canvas
|
|
18
|
+
* switch), the closures become invalid. Rebuilding from the record
|
|
19
|
+
* using the fresh iframe's sinks side-steps the lifecycle issue.
|
|
20
|
+
* - Command-pattern semantics intact: each kind ships its own builder
|
|
21
|
+
* that turns a payload + sinks into a runnable EditCommand.
|
|
22
|
+
* - Depth cap = 50 per canvas. Future-discarded on push. Viewport +
|
|
23
|
+
* selection NOT undoable (Figma convention).
|
|
24
|
+
*
|
|
25
|
+
* Async note. `cmd.do()` and `cmd.undo()` may return a Promise (server
|
|
26
|
+
* PATCH/PUT). The reducer itself is synchronous and never awaits — the
|
|
27
|
+
* runner inside `use-undo-stack.tsx` awaits the side-effect BEFORE
|
|
28
|
+
* dispatching the state transition. Reducer-pure keeps reasoning + tests
|
|
29
|
+
* trivial.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
// Runtime command shape
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A single reversible edit, ready to execute. Built on demand from a
|
|
37
|
+
* `CommandRecord` + `CommandSinks` via the registry below. Holds closures
|
|
38
|
+
* bound to the CURRENT iframe's React state — never persist this; persist
|
|
39
|
+
* the `CommandRecord` instead.
|
|
40
|
+
*/
|
|
41
|
+
export interface EditCommand {
|
|
42
|
+
readonly kind: string;
|
|
43
|
+
readonly label: string;
|
|
44
|
+
do(): Promise<void> | void;
|
|
45
|
+
undo(): Promise<void> | void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
// Persistent record shape — what the stack actually stores
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Serializable description of a reversible edit. Each `kind` corresponds
|
|
53
|
+
* to a registered builder (see `registerCommand` below). Payload shape is
|
|
54
|
+
* the kind's contract — see `commands/*-command.ts` for definitions.
|
|
55
|
+
*/
|
|
56
|
+
export interface CommandRecord<P = unknown> {
|
|
57
|
+
readonly kind: string;
|
|
58
|
+
readonly label: string;
|
|
59
|
+
readonly payload: P;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Per-iframe side-effect surface. Populated by descendants of the
|
|
64
|
+
* `UndoStackProvider` via `useUndoSinks().setSinks(...)`. Each consumer
|
|
65
|
+
* provides only the sink it owns; the provider merges. Unbound sinks are
|
|
66
|
+
* `undefined` — builders fail gracefully (returning `null` makes the
|
|
67
|
+
* runner skip the entry with a warning rather than crash).
|
|
68
|
+
*/
|
|
69
|
+
export interface CommandSinks {
|
|
70
|
+
/** Wired by canvas-lib `DesignCanvasInner`. */
|
|
71
|
+
layoutPatchFn?: unknown;
|
|
72
|
+
/** Wired by `AnnotationsLayer`. */
|
|
73
|
+
strokesPutFn?: unknown;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type CommandBuilder<P = unknown> = (
|
|
77
|
+
record: CommandRecord<P>,
|
|
78
|
+
sinks: CommandSinks
|
|
79
|
+
) => EditCommand | null;
|
|
80
|
+
|
|
81
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
82
|
+
// Reducer state + actions
|
|
83
|
+
|
|
84
|
+
export interface UndoStackState {
|
|
85
|
+
past: readonly CommandRecord[];
|
|
86
|
+
future: readonly CommandRecord[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type UndoAction =
|
|
90
|
+
| { type: 'push'; record: CommandRecord }
|
|
91
|
+
| { type: 'undo' }
|
|
92
|
+
| { type: 'redo' }
|
|
93
|
+
| { type: 'clear' }
|
|
94
|
+
| { type: 'hydrate'; state: UndoStackState };
|
|
95
|
+
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
// Constants
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Ring cap per canvas. When `past.length === MAX_DEPTH` and a new record
|
|
101
|
+
* arrives, the oldest entry is dropped. 50 ≈ 3–5 minutes of intense
|
|
102
|
+
* iteration before the user can no longer undo to the start.
|
|
103
|
+
*/
|
|
104
|
+
export const MAX_DEPTH = 50;
|
|
105
|
+
|
|
106
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
// Builder registry
|
|
108
|
+
|
|
109
|
+
const BUILDERS = new Map<string, CommandBuilder>();
|
|
110
|
+
|
|
111
|
+
/** Register a builder for a command kind. Idempotent. */
|
|
112
|
+
export function registerCommand<P>(kind: string, builder: CommandBuilder<P>): void {
|
|
113
|
+
BUILDERS.set(kind, builder as CommandBuilder);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Rebuild a runnable `EditCommand` from a persisted record + the iframe's
|
|
118
|
+
* current sinks. Returns `null` when the kind isn't registered or the
|
|
119
|
+
* required sink isn't bound — the runner treats `null` as "skip this entry,
|
|
120
|
+
* something's misconfigured" rather than crashing.
|
|
121
|
+
*/
|
|
122
|
+
export function rebuildCommand(record: CommandRecord, sinks: CommandSinks): EditCommand | null {
|
|
123
|
+
const builder = BUILDERS.get(record.kind);
|
|
124
|
+
if (!builder) return null;
|
|
125
|
+
return builder(record, sinks);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Test seam — reset the registry between bun:test cases. */
|
|
129
|
+
export function _clearBuilderRegistry(): void {
|
|
130
|
+
BUILDERS.clear();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
134
|
+
// Factory + reducer
|
|
135
|
+
|
|
136
|
+
export function createUndoStackState(): UndoStackState {
|
|
137
|
+
return { past: [], future: [] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function undoReducer(state: UndoStackState, action: UndoAction): UndoStackState {
|
|
141
|
+
switch (action.type) {
|
|
142
|
+
case 'push': {
|
|
143
|
+
const next = state.past.length >= MAX_DEPTH ? state.past.slice(1) : state.past;
|
|
144
|
+
return { past: [...next, action.record], future: [] };
|
|
145
|
+
}
|
|
146
|
+
case 'undo': {
|
|
147
|
+
if (state.past.length === 0) return state;
|
|
148
|
+
const top = state.past[state.past.length - 1];
|
|
149
|
+
if (!top) return state;
|
|
150
|
+
return {
|
|
151
|
+
past: state.past.slice(0, -1),
|
|
152
|
+
future: [...state.future, top],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
case 'redo': {
|
|
156
|
+
if (state.future.length === 0) return state;
|
|
157
|
+
const top = state.future[state.future.length - 1];
|
|
158
|
+
if (!top) return state;
|
|
159
|
+
return {
|
|
160
|
+
past: [...state.past, top],
|
|
161
|
+
future: state.future.slice(0, -1),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
case 'clear':
|
|
165
|
+
return createUndoStackState();
|
|
166
|
+
case 'hydrate':
|
|
167
|
+
return action.state;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
172
|
+
// Selectors — keep state-shape inspection out of consumers.
|
|
173
|
+
|
|
174
|
+
export function canUndo(state: UndoStackState): boolean {
|
|
175
|
+
return state.past.length > 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function canRedo(state: UndoStackState): boolean {
|
|
179
|
+
return state.future.length > 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Top of `past` — the record an `undo` will invert. */
|
|
183
|
+
export function peekUndo(state: UndoStackState): CommandRecord | null {
|
|
184
|
+
return state.past[state.past.length - 1] ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Top of `future` — the record a `redo` will re-apply. */
|
|
188
|
+
export function peekRedo(state: UndoStackState): CommandRecord | null {
|
|
189
|
+
return state.future[state.future.length - 1] ?? null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
193
|
+
// Cross-iframe persistent store (DDR-050 rev 2)
|
|
194
|
+
//
|
|
195
|
+
// Lives on the topmost window of the dev-server (same-origin — all canvas
|
|
196
|
+
// iframes are children of /index.html). The map is keyed by canvas file path,
|
|
197
|
+
// so switching from Foo.tsx → Bar.tsx → Foo.tsx replays the original Foo
|
|
198
|
+
// history into the freshly-mounted iframe.
|
|
199
|
+
|
|
200
|
+
interface StoreHost {
|
|
201
|
+
__maude_undo_stacks?: Map<string, UndoStackState>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Prefer `window.top` so all canvas iframes (children of the dev-server
|
|
206
|
+
* shell) read + write the same Map. Falls back to `window` (when top is
|
|
207
|
+
* cross-origin — shouldn't happen in our same-origin setup), then to
|
|
208
|
+
* `globalThis` (Node / Bun test runtime where window is absent).
|
|
209
|
+
*/
|
|
210
|
+
function getStoreHost(): StoreHost {
|
|
211
|
+
if (typeof window !== 'undefined') {
|
|
212
|
+
try {
|
|
213
|
+
return (window.top ?? window) as unknown as StoreHost;
|
|
214
|
+
} catch {
|
|
215
|
+
return window as unknown as StoreHost;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return globalThis as unknown as StoreHost;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function ensureStoreMap(): Map<string, UndoStackState> {
|
|
222
|
+
const host = getStoreHost();
|
|
223
|
+
if (!host.__maude_undo_stacks) host.__maude_undo_stacks = new Map();
|
|
224
|
+
return host.__maude_undo_stacks;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Load saved state for a canvas file. Returns empty state on miss. */
|
|
228
|
+
export function loadStackState(canvasFile: string): UndoStackState {
|
|
229
|
+
return ensureStoreMap().get(canvasFile) ?? createUndoStackState();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Persist state for a canvas file. */
|
|
233
|
+
export function saveStackState(canvasFile: string, state: UndoStackState): void {
|
|
234
|
+
ensureStoreMap().set(canvasFile, state);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Test seam — wipe the cross-iframe store between bun:test cases. */
|
|
238
|
+
export function _clearStackStore(): void {
|
|
239
|
+
getStoreHost().__maude_undo_stacks = new Map();
|
|
240
|
+
}
|
|
@@ -34,7 +34,7 @@ const RESIZE_CSS = `
|
|
|
34
34
|
position: fixed;
|
|
35
35
|
width: 8px;
|
|
36
36
|
height: 8px;
|
|
37
|
-
background: var(--accent, #d63b1f);
|
|
37
|
+
background: var(--maude-hud-accent, #d63b1f);
|
|
38
38
|
border: 1px solid var(--bg-0, #ffffff);
|
|
39
39
|
border-radius: 1px;
|
|
40
40
|
box-shadow: 0 0 0 0.5px color-mix(in oklab, var(--fg-0, #1c1917) 30%, transparent);
|
|
@@ -36,14 +36,17 @@ import {
|
|
|
36
36
|
useState,
|
|
37
37
|
} from 'react';
|
|
38
38
|
|
|
39
|
+
import { DRAG_THRESHOLD_PX as INPUT_DRAG_THRESHOLD_PX } from './input-router.tsx';
|
|
39
40
|
import type { Selection } from './use-selection-set.tsx';
|
|
40
41
|
import { type Rect, type SnapResult, computeSnap } from './use-snap-guides.tsx';
|
|
41
42
|
|
|
42
43
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
44
|
// Constants
|
|
44
45
|
|
|
45
|
-
/** Screen-pixel distance the cursor must travel before pending → dragging.
|
|
46
|
-
export
|
|
46
|
+
/** Screen-pixel distance the cursor must travel before pending → dragging.
|
|
47
|
+
* Re-export from `input-router` so artboard-drag, marquees, and annotation
|
|
48
|
+
* drag-vs-tap share the same canonical value (T25). */
|
|
49
|
+
export const DRAG_THRESHOLD_PX = INPUT_DRAG_THRESHOLD_PX;
|
|
47
50
|
|
|
48
51
|
/** Default grid + tolerance (world units). Documented in DDR-028. */
|
|
49
52
|
export const DEFAULT_GRID_SIZE = 40;
|
|
@@ -136,7 +139,7 @@ export function dragReducer(state: DragState, ev: DragEvent): DragState {
|
|
|
136
139
|
const dxClient = ev.clientX - state.startClientX;
|
|
137
140
|
const dyClient = ev.clientY - state.startClientY;
|
|
138
141
|
if (state.kind === 'pending') {
|
|
139
|
-
if (Math.
|
|
142
|
+
if (Math.hypot(dxClient, dyClient) < DRAG_THRESHOLD_PX) {
|
|
140
143
|
return state;
|
|
141
144
|
}
|
|
142
145
|
}
|