@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.
Files changed (105) hide show
  1. package/README.md +7 -0
  2. package/cli/bin/maude.mjs +5 -1
  3. package/cli/commands/design-link.test.mjs +207 -0
  4. package/cli/commands/design.mjs +42 -12
  5. package/cli/commands/doctor.mjs +350 -0
  6. package/cli/commands/doctor.test.mjs +185 -0
  7. package/cli/commands/help.mjs +24 -0
  8. package/cli/commands/hub.mjs +231 -0
  9. package/cli/commands/hub.test.mjs +87 -0
  10. package/cli/lib/config-lint.mjs +141 -0
  11. package/cli/lib/config-lint.test.mjs +117 -0
  12. package/cli/lib/design-link.mjs +216 -0
  13. package/cli/lib/hubs-config.mjs +123 -0
  14. package/cli/lib/hubs-config.test.mjs +100 -0
  15. package/cli/lib/preflight.mjs +232 -0
  16. package/cli/lib/stack-detect.mjs +344 -0
  17. package/cli/lib/stack-detect.test.mjs +121 -0
  18. package/package.json +17 -9
  19. package/plugins/design/dependencies.json +147 -0
  20. package/plugins/design/dependencies.schema.json +107 -0
  21. package/plugins/design/dev-server/ai-banner.tsx +188 -0
  22. package/plugins/design/dev-server/annotations-context-toolbar.tsx +115 -41
  23. package/plugins/design/dev-server/annotations-layer.tsx +256 -107
  24. package/plugins/design/dev-server/api.ts +17 -1
  25. package/plugins/design/dev-server/artboard-marquee.tsx +10 -6
  26. package/plugins/design/dev-server/bin/asset-sweep.sh +180 -0
  27. package/plugins/design/dev-server/bin/preflight.sh +32 -0
  28. package/plugins/design/dev-server/bin/runtime-health.sh +169 -0
  29. package/plugins/design/dev-server/bin/visual-sanity.sh +221 -0
  30. package/plugins/design/dev-server/canvas-lib.tsx +534 -32
  31. package/plugins/design/dev-server/canvas-shell.tsx +479 -29
  32. package/plugins/design/dev-server/client/app.jsx +72 -0
  33. package/plugins/design/dev-server/client/hmr.mjs +28 -0
  34. package/plugins/design/dev-server/collab/ai-activity.ts +127 -0
  35. package/plugins/design/dev-server/collab/git-lifecycle.ts +124 -0
  36. package/plugins/design/dev-server/collab/index.ts +47 -0
  37. package/plugins/design/dev-server/collab/persistence.ts +123 -0
  38. package/plugins/design/dev-server/collab/protocol.ts +108 -0
  39. package/plugins/design/dev-server/collab/registry.ts +110 -0
  40. package/plugins/design/dev-server/collab/room.ts +215 -0
  41. package/plugins/design/dev-server/commands/annotation-strokes-command.ts +127 -0
  42. package/plugins/design/dev-server/commands/equal-spacing-command.ts +28 -0
  43. package/plugins/design/dev-server/commands/move-artboards-command.ts +158 -0
  44. package/plugins/design/dev-server/comments-overlay.tsx +41 -4
  45. package/plugins/design/dev-server/contextual-toolbar.tsx +241 -0
  46. package/plugins/design/dev-server/cursors-overlay.tsx +296 -0
  47. package/plugins/design/dev-server/dist/client.bundle.js +75 -3
  48. package/plugins/design/dev-server/dist/runtime/motion.js +165 -0
  49. package/plugins/design/dev-server/dist/runtime/motion_react.js +409 -0
  50. package/plugins/design/dev-server/dist/runtime/y-protocols_awareness.js +587 -0
  51. package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +257 -0
  52. package/plugins/design/dev-server/dist/runtime/yjs.js +7107 -0
  53. package/plugins/design/dev-server/equal-spacing-detector.ts +98 -0
  54. package/plugins/design/dev-server/equal-spacing-handles.tsx +289 -0
  55. package/plugins/design/dev-server/export-dialog.tsx +1 -1
  56. package/plugins/design/dev-server/handoff.ts +24 -0
  57. package/plugins/design/dev-server/hmr-broadcast.ts +15 -2
  58. package/plugins/design/dev-server/http.ts +91 -1
  59. package/plugins/design/dev-server/input-router.tsx +52 -2
  60. package/plugins/design/dev-server/marquee-overlay.tsx +282 -0
  61. package/plugins/design/dev-server/participants-chrome.tsx +261 -0
  62. package/plugins/design/dev-server/runtime-bundle.ts +15 -0
  63. package/plugins/design/dev-server/server.ts +78 -11
  64. package/plugins/design/dev-server/test/ai-activity.test.ts +113 -0
  65. package/plugins/design/dev-server/test/annotation-strokes-command.test.ts +149 -0
  66. package/plugins/design/dev-server/test/annotations-layer.test.ts +44 -0
  67. package/plugins/design/dev-server/test/canvas-lib-motion.test.ts +131 -0
  68. package/plugins/design/dev-server/test/collab-annotations-bridge.test.ts +55 -0
  69. package/plugins/design/dev-server/test/collab-bridge.test.ts +81 -0
  70. package/plugins/design/dev-server/test/collab-loopback.test.ts +63 -0
  71. package/plugins/design/dev-server/test/collab-protocol.test.ts +146 -0
  72. package/plugins/design/dev-server/test/collab-room.test.ts +182 -0
  73. package/plugins/design/dev-server/test/collab-stress.test.ts +121 -0
  74. package/plugins/design/dev-server/test/equal-spacing-detector.test.ts +93 -0
  75. package/plugins/design/dev-server/test/git-lifecycle.test.ts +101 -0
  76. package/plugins/design/dev-server/test/input-router.test.ts +87 -1
  77. package/plugins/design/dev-server/test/marquee-overlay.test.ts +94 -0
  78. package/plugins/design/dev-server/test/move-artboards-command.test.ts +108 -0
  79. package/plugins/design/dev-server/test/participants-chrome.test.ts +30 -0
  80. package/plugins/design/dev-server/test/undo-stack.test.ts +211 -0
  81. package/plugins/design/dev-server/test/use-collab.test.ts +71 -0
  82. package/plugins/design/dev-server/test/use-cursor-modifiers.test.ts +59 -0
  83. package/plugins/design/dev-server/test/use-keyboard-discipline.test.ts +27 -0
  84. package/plugins/design/dev-server/test/use-undo-stack.test.tsx +193 -0
  85. package/plugins/design/dev-server/tool-palette.tsx +7 -7
  86. package/plugins/design/dev-server/undo-hud.tsx +95 -0
  87. package/plugins/design/dev-server/undo-stack.ts +240 -0
  88. package/plugins/design/dev-server/use-annotation-resize.tsx +1 -1
  89. package/plugins/design/dev-server/use-artboard-drag.tsx +6 -3
  90. package/plugins/design/dev-server/use-collab.tsx +478 -0
  91. package/plugins/design/dev-server/use-cursor-modifiers.tsx +122 -0
  92. package/plugins/design/dev-server/use-keyboard-discipline.tsx +125 -0
  93. package/plugins/design/dev-server/use-undo-stack.tsx +355 -0
  94. package/plugins/design/dev-server/ws.ts +123 -7
  95. package/plugins/design/templates/_shell.html +51 -6
  96. package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +245 -0
  97. package/plugins/design/templates/design-system-inspiration/_MAPPING.md +2 -2
  98. package/plugins/design/templates/design-system-inspiration/core/preview/_components.css.tpl +129 -0
  99. package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +63 -0
  100. package/plugins/design/templates/design-system-inspiration/core/preview/motion.css.tpl +106 -0
  101. package/plugins/design/templates/design-system-inspiration/core/preview/motion.tsx.tpl +208 -0
  102. package/plugins/flow/.claude-plugin/config.schema.json +12 -0
  103. package/plugins/flow/dependencies.json +143 -0
  104. package/plugins/flow/dependencies.schema.json +107 -0
  105. /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 const DRAG_THRESHOLD_PX = 4;
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.abs(dxClient) < DRAG_THRESHOLD_PX && Math.abs(dyClient) < DRAG_THRESHOLD_PX) {
142
+ if (Math.hypot(dxClient, dyClient) < DRAG_THRESHOLD_PX) {
140
143
  return state;
141
144
  }
142
145
  }