@1agh/maude 0.19.0 → 0.20.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 (66) hide show
  1. package/package.json +9 -9
  2. package/plugins/design/dev-server/annotations-context-toolbar.tsx +126 -44
  3. package/plugins/design/dev-server/annotations-layer.tsx +343 -112
  4. package/plugins/design/dev-server/api.ts +70 -17
  5. package/plugins/design/dev-server/artboard-marquee.tsx +174 -0
  6. package/plugins/design/dev-server/bin/asset-sweep.sh +180 -0
  7. package/plugins/design/dev-server/bin/visual-sanity.sh +221 -0
  8. package/plugins/design/dev-server/canvas-lib.tsx +691 -119
  9. package/plugins/design/dev-server/canvas-shell.tsx +818 -42
  10. package/plugins/design/dev-server/client/app.jsx +173 -52
  11. package/plugins/design/dev-server/client/hmr.mjs +28 -0
  12. package/plugins/design/dev-server/client/styles/1-tokens.css +15 -8
  13. package/plugins/design/dev-server/client/styles/4-components.css +32 -2
  14. package/plugins/design/dev-server/commands/annotation-strokes-command.ts +127 -0
  15. package/plugins/design/dev-server/commands/equal-spacing-command.ts +28 -0
  16. package/plugins/design/dev-server/commands/move-artboards-command.ts +158 -0
  17. package/plugins/design/dev-server/comments-overlay.tsx +12 -4
  18. package/plugins/design/dev-server/config.schema.json +31 -5
  19. package/plugins/design/dev-server/context-menu.tsx +3 -3
  20. package/plugins/design/dev-server/context.ts +37 -3
  21. package/plugins/design/dev-server/contextual-toolbar.tsx +241 -0
  22. package/plugins/design/dev-server/dist/client.bundle.js +212 -103
  23. package/plugins/design/dev-server/dist/runtime/motion.js +165 -0
  24. package/plugins/design/dev-server/dist/runtime/motion_react.js +409 -0
  25. package/plugins/design/dev-server/dist/styles.css +38 -2
  26. package/plugins/design/dev-server/equal-spacing-detector.ts +98 -0
  27. package/plugins/design/dev-server/equal-spacing-handles.tsx +289 -0
  28. package/plugins/design/dev-server/export-dialog.tsx +3 -3
  29. package/plugins/design/dev-server/handoff.ts +24 -0
  30. package/plugins/design/dev-server/http.ts +41 -2
  31. package/plugins/design/dev-server/input-router.tsx +74 -10
  32. package/plugins/design/dev-server/marquee-overlay.tsx +282 -0
  33. package/plugins/design/dev-server/runtime-bundle.ts +7 -0
  34. package/plugins/design/dev-server/server.mjs +84 -18
  35. package/plugins/design/dev-server/test/annotation-strokes-command.test.ts +149 -0
  36. package/plugins/design/dev-server/test/annotations-layer.test.ts +44 -0
  37. package/plugins/design/dev-server/test/canvas-lib-motion.test.ts +131 -0
  38. package/plugins/design/dev-server/test/context-resolve-tokens.test.ts +97 -0
  39. package/plugins/design/dev-server/test/equal-spacing-detector.test.ts +93 -0
  40. package/plugins/design/dev-server/test/input-router.test.ts +87 -1
  41. package/plugins/design/dev-server/test/marquee-overlay.test.ts +94 -0
  42. package/plugins/design/dev-server/test/move-artboards-command.test.ts +108 -0
  43. package/plugins/design/dev-server/test/snap-distance-pill.test.ts +68 -0
  44. package/plugins/design/dev-server/test/system-endpoint.test.ts +144 -0
  45. package/plugins/design/dev-server/test/undo-stack.test.ts +211 -0
  46. package/plugins/design/dev-server/test/use-cursor-modifiers.test.ts +59 -0
  47. package/plugins/design/dev-server/test/use-keyboard-discipline.test.ts +27 -0
  48. package/plugins/design/dev-server/test/use-undo-stack.test.tsx +193 -0
  49. package/plugins/design/dev-server/tool-palette.tsx +71 -15
  50. package/plugins/design/dev-server/undo-hud.tsx +95 -0
  51. package/plugins/design/dev-server/undo-stack.ts +240 -0
  52. package/plugins/design/dev-server/use-annotation-resize.tsx +295 -0
  53. package/plugins/design/dev-server/use-artboard-drag.tsx +6 -3
  54. package/plugins/design/dev-server/use-cursor-modifiers.tsx +122 -0
  55. package/plugins/design/dev-server/use-keyboard-discipline.tsx +125 -0
  56. package/plugins/design/dev-server/use-snap-guides.tsx +25 -1
  57. package/plugins/design/dev-server/use-tool-mode.tsx +31 -2
  58. package/plugins/design/dev-server/use-undo-stack.tsx +355 -0
  59. package/plugins/design/templates/_shell.html +17 -6
  60. package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +245 -0
  61. package/plugins/design/templates/design-system-inspiration/_MAPPING.md +2 -2
  62. package/plugins/design/templates/design-system-inspiration/core/preview/_components.css.tpl +129 -0
  63. package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +63 -0
  64. package/plugins/design/templates/design-system-inspiration/core/preview/motion.css.tpl +106 -0
  65. package/plugins/design/templates/design-system-inspiration/core/preview/motion.tsx.tpl +208 -0
  66. /package/plugins/design/templates/design-system-inspiration/core/preview/{motion.html → .archive/motion.html} +0 -0
@@ -54,6 +54,13 @@ interface ToolContextValue {
54
54
  tool: Tool;
55
55
  setTool: (t: Tool) => void;
56
56
  tools: readonly ToolDescriptor[];
57
+ /** T19 — sticky-tool double-click lock. When `sticky.locked === true` AND
58
+ * `sticky.tool === tool`, draw tools stay armed after each shape commit
59
+ * (T18 auto-flip is suppressed). Single-click on any other tool clears
60
+ * sticky; Esc clears + flips to Move. */
61
+ sticky: { tool: Tool | null; locked: boolean };
62
+ toggleSticky: (t: Tool) => void;
63
+ clearSticky: () => void;
57
64
  }
58
65
 
59
66
  const ToolContext = createContext<ToolContextValue | null>(null);
@@ -71,7 +78,26 @@ export function ToolProvider({
71
78
  initial?: Tool;
72
79
  }) {
73
80
  const [tool, setToolState] = useState<Tool>(initial);
74
- const setTool = useCallback((t: Tool) => setToolState(t), []);
81
+ const [sticky, setSticky] = useState<{ tool: Tool | null; locked: boolean }>(() => ({
82
+ tool: null,
83
+ locked: false,
84
+ }));
85
+ const setTool = useCallback((t: Tool) => {
86
+ setToolState(t);
87
+ // Single-click on a different tool clears any sticky lock — sticky is
88
+ // a per-tool flag, not global.
89
+ setSticky((prev) => (prev.locked && prev.tool === t ? prev : { tool: null, locked: false }));
90
+ }, []);
91
+ const toggleSticky = useCallback((t: Tool) => {
92
+ setSticky((prev) => {
93
+ if (prev.locked && prev.tool === t) return { tool: null, locked: false };
94
+ return { tool: t, locked: true };
95
+ });
96
+ setToolState(t);
97
+ }, []);
98
+ const clearSticky = useCallback(() => {
99
+ setSticky({ tool: null, locked: false });
100
+ }, []);
75
101
 
76
102
  // Body cursor sync — applied to the canvas iframe's body (this hook runs
77
103
  // inside the iframe context). The viewport-controller still owns the
@@ -88,7 +114,10 @@ export function ToolProvider({
88
114
  };
89
115
  }, [tool, tools]);
90
116
 
91
- const value = useMemo<ToolContextValue>(() => ({ tool, setTool, tools }), [tool, setTool, tools]);
117
+ const value = useMemo<ToolContextValue>(
118
+ () => ({ tool, setTool, tools, sticky, toggleSticky, clearSticky }),
119
+ [tool, setTool, tools, sticky, toggleSticky, clearSticky]
120
+ );
92
121
 
93
122
  return <ToolContext.Provider value={value}>{children}</ToolContext.Provider>;
94
123
  }
@@ -0,0 +1,355 @@
1
+ /**
2
+ * @file use-undo-stack.tsx — React Context + Provider for the undo stack
3
+ * @scope plugins/design/dev-server/use-undo-stack.tsx
4
+ * @purpose Wraps the pure `undoReducer` (undo-stack.ts) in React state,
5
+ * owns the async runner that awaits side-effects before applying
6
+ * the next state transition, exposes the `lastLabel` HUD signal,
7
+ * AND persists the stack across canvas switches via
8
+ * `window.top.__maude_undo_stacks` keyed by canvas file path
9
+ * (DDR-050 rev 2).
10
+ *
11
+ * Two roles in one provider:
12
+ * 1. **State authority** — ref-as-store synchronously between awaited
13
+ * side-effects (Cmd+Z at 30 Hz key-repeat needs to see its own writes).
14
+ * 2. **Sinks registry** — descendants (DesignCanvasInner for layout,
15
+ * AnnotationsLayer for strokes) register their side-effect functions
16
+ * via `useUndoSinks().setSink(name, fn)`. The provider merges them
17
+ * and uses them when rebuilding commands from records.
18
+ */
19
+
20
+ import {
21
+ type ReactNode,
22
+ createContext,
23
+ useCallback,
24
+ useContext,
25
+ useEffect,
26
+ useMemo,
27
+ useRef,
28
+ useState,
29
+ } from 'react';
30
+
31
+ import {
32
+ type CommandRecord,
33
+ type CommandSinks,
34
+ type EditCommand,
35
+ type UndoStackState,
36
+ canRedo as canRedoOf,
37
+ canUndo as canUndoOf,
38
+ loadStackState,
39
+ rebuildCommand,
40
+ saveStackState,
41
+ undoReducer,
42
+ } from './undo-stack.ts';
43
+
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+ // Context shape
46
+
47
+ export interface UndoStackValue {
48
+ /**
49
+ * Push a fresh command. The runner calls the rebuilt command's `do()` —
50
+ * the new state is committed only after the side-effect resolves.
51
+ * Awaiting the returned promise is optional.
52
+ */
53
+ push: (record: CommandRecord) => Promise<void>;
54
+ /** Undo the top of `past`. No-op when empty. */
55
+ undo: () => Promise<void>;
56
+ /** Redo the top of `future`. No-op when empty. */
57
+ redo: () => Promise<void>;
58
+ /** Drop both stacks (external edit). */
59
+ clear: (reason?: string) => void;
60
+ canUndo: boolean;
61
+ canRedo: boolean;
62
+ /** Most recent operation label. HUD reads this. */
63
+ lastLabel: string | null;
64
+ /** Monotonic counter — HUD subscribes to bump dismiss timer per op. */
65
+ lastTick: number;
66
+ }
67
+
68
+ const UndoStackContext = createContext<UndoStackValue | null>(null);
69
+
70
+ const NOOP_VALUE: UndoStackValue = {
71
+ push: () => Promise.resolve(),
72
+ undo: () => Promise.resolve(),
73
+ redo: () => Promise.resolve(),
74
+ clear: () => {},
75
+ canUndo: false,
76
+ canRedo: false,
77
+ lastLabel: null,
78
+ lastTick: 0,
79
+ };
80
+
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+ // Sinks context — descendants register side-effect functions here.
83
+
84
+ export interface UndoSinksValue {
85
+ /** Register or replace a sink. Pass `undefined` to unregister. */
86
+ setSink: <K extends keyof CommandSinks>(key: K, fn: CommandSinks[K]) => void;
87
+ }
88
+
89
+ const UndoSinksContext = createContext<UndoSinksValue | null>(null);
90
+
91
+ const NOOP_SINKS: UndoSinksValue = { setSink: () => {} };
92
+
93
+ // ─────────────────────────────────────────────────────────────────────────────
94
+ // Provider
95
+
96
+ export interface UndoStackProviderProps {
97
+ children: ReactNode;
98
+ /**
99
+ * Canvas file path (e.g. `.design/ui/Foo.tsx` or repo-relative). Required
100
+ * for cross-canvas persistence — the stack is keyed by this string in the
101
+ * `window.top.__maude_undo_stacks` Map. Two iframes with the same canvas
102
+ * file share one stack; switching canvases gets a fresh stack keyed under
103
+ * a different path.
104
+ *
105
+ * Optional only because some test harnesses mount the provider standalone;
106
+ * an empty / undefined value disables persistence (stack is ephemeral).
107
+ */
108
+ canvasFile?: string;
109
+ /**
110
+ * Optional async-failure hook. Fires when a command's `do()` / `undo()`
111
+ * throws or rejects. The reducer never commits the transition for a failed
112
+ * side-effect (push: cmd not appended; undo: stays in past; redo: stays in
113
+ * future). Defaults to a console.warn.
114
+ */
115
+ onCommandError?: (err: unknown, op: 'do' | 'undo', record: CommandRecord) => void;
116
+ }
117
+
118
+ export function UndoStackProvider({
119
+ children,
120
+ canvasFile,
121
+ onCommandError,
122
+ }: UndoStackProviderProps) {
123
+ // Ref is the authoritative store — the async runner reads + writes it
124
+ // synchronously between awaited side-effects.
125
+ const stateRef = useRef<UndoStackState>(
126
+ canvasFile ? loadStackState(canvasFile) : { past: [], future: [] }
127
+ );
128
+ const [, setRenderToken] = useState(0);
129
+
130
+ // Sinks live in a ref so writes don't churn re-renders; the runner reads
131
+ // them on every push/undo/redo.
132
+ const sinksRef = useRef<CommandSinks>({});
133
+
134
+ // canvasFile ref tracks the current key — used for persistence + for
135
+ // re-hydration when the consumer remounts under a different file.
136
+ const fileRef = useRef<string | undefined>(canvasFile);
137
+
138
+ // Re-hydrate state when canvasFile changes (e.g. parent shell remounts
139
+ // the provider for a new canvas). For the typical iframe lifecycle the
140
+ // provider unmounts entirely on canvas switch so this branch is rarely
141
+ // hit — it's a safety net for test harnesses that swap files in place.
142
+ useEffect(() => {
143
+ if (canvasFile === fileRef.current) return;
144
+ fileRef.current = canvasFile;
145
+ stateRef.current = canvasFile ? loadStackState(canvasFile) : { past: [], future: [] };
146
+ setRenderToken((t) => t + 1);
147
+ }, [canvasFile]);
148
+
149
+ const writeState = useCallback((next: UndoStackState) => {
150
+ stateRef.current = next;
151
+ if (fileRef.current) saveStackState(fileRef.current, next);
152
+ setRenderToken((t) => t + 1);
153
+ }, []);
154
+
155
+ const [lastLabel, setLastLabel] = useState<string | null>(null);
156
+ const [lastTick, setLastTick] = useState(0);
157
+
158
+ // Serialize concurrent ops. Cmd+Z held down can repeat at ~30 Hz; the
159
+ // runner awaits each side-effect before applying state.
160
+ const inFlightRef = useRef<Promise<void>>(Promise.resolve());
161
+
162
+ const reportError = useCallback(
163
+ (err: unknown, op: 'do' | 'undo', record: CommandRecord) => {
164
+ if (onCommandError) {
165
+ onCommandError(err, op, record);
166
+ return;
167
+ }
168
+ console.warn(`[undo-stack] ${op} failed for "${record.label}":`, err);
169
+ },
170
+ [onCommandError]
171
+ );
172
+
173
+ const bumpLabel = useCallback((label: string | null) => {
174
+ setLastLabel(label);
175
+ setLastTick((t) => t + 1);
176
+ }, []);
177
+
178
+ const enqueue = useCallback((task: () => Promise<void>): Promise<void> => {
179
+ const next = inFlightRef.current.then(task, task);
180
+ inFlightRef.current = next.catch(() => {
181
+ /* swallow — per-op error already reported */
182
+ });
183
+ return next;
184
+ }, []);
185
+
186
+ /**
187
+ * Build a runnable EditCommand from a record using the iframe's current
188
+ * sinks. Returns `null` when the kind isn't registered or its required
189
+ * sink isn't bound — the runner skips the entry and bumps a label so the
190
+ * user sees `"(skipped: …)"` in the HUD instead of silent failure.
191
+ */
192
+ const build = useCallback((record: CommandRecord): EditCommand | null => {
193
+ return rebuildCommand(record, sinksRef.current);
194
+ }, []);
195
+
196
+ const push = useCallback(
197
+ (record: CommandRecord): Promise<void> =>
198
+ enqueue(async () => {
199
+ const cmd = build(record);
200
+ if (!cmd) {
201
+ bumpLabel(`(skipped: ${record.kind})`);
202
+ return;
203
+ }
204
+ try {
205
+ await cmd.do();
206
+ } catch (err) {
207
+ reportError(err, 'do', record);
208
+ return;
209
+ }
210
+ writeState(undoReducer(stateRef.current, { type: 'push', record }));
211
+ bumpLabel(record.label);
212
+ }),
213
+ [enqueue, build, reportError, bumpLabel, writeState]
214
+ );
215
+
216
+ const undo = useCallback(
217
+ (): Promise<void> =>
218
+ enqueue(async () => {
219
+ const cur = stateRef.current;
220
+ if (!canUndoOf(cur)) return;
221
+ const top = cur.past[cur.past.length - 1];
222
+ if (!top) return;
223
+ const cmd = build(top);
224
+ if (!cmd) {
225
+ bumpLabel(`(skipped: ${top.kind})`);
226
+ return;
227
+ }
228
+ try {
229
+ await cmd.undo();
230
+ } catch (err) {
231
+ reportError(err, 'undo', top);
232
+ return;
233
+ }
234
+ writeState(undoReducer(stateRef.current, { type: 'undo' }));
235
+ bumpLabel(`Undo: ${top.label}`);
236
+ }),
237
+ [enqueue, build, reportError, bumpLabel, writeState]
238
+ );
239
+
240
+ const redo = useCallback(
241
+ (): Promise<void> =>
242
+ enqueue(async () => {
243
+ const cur = stateRef.current;
244
+ if (!canRedoOf(cur)) return;
245
+ const top = cur.future[cur.future.length - 1];
246
+ if (!top) return;
247
+ const cmd = build(top);
248
+ if (!cmd) {
249
+ bumpLabel(`(skipped: ${top.kind})`);
250
+ return;
251
+ }
252
+ try {
253
+ await cmd.do();
254
+ } catch (err) {
255
+ reportError(err, 'do', top);
256
+ return;
257
+ }
258
+ writeState(undoReducer(stateRef.current, { type: 'redo' }));
259
+ bumpLabel(`Redo: ${top.label}`);
260
+ }),
261
+ [enqueue, build, reportError, bumpLabel, writeState]
262
+ );
263
+
264
+ const clear = useCallback(
265
+ (reason?: string) => {
266
+ writeState({ past: [], future: [] });
267
+ bumpLabel(reason ?? null);
268
+ },
269
+ [bumpLabel, writeState]
270
+ );
271
+
272
+ // External-edit invalidation. Listen for an explicit window event
273
+ // dispatched by client/hmr.mjs when an external .meta.json change arrives.
274
+ // The self-echo guard ignores our own PATCH bouncing back through fs-watch
275
+ // (see canvas-lib's `__maude_last_meta_self_write_at`).
276
+ useEffect(() => {
277
+ if (typeof window === 'undefined') return;
278
+ const onInvalidate = (ev: Event) => {
279
+ const w = window as unknown as { __maude_last_meta_self_write_at?: number };
280
+ const last = w.__maude_last_meta_self_write_at ?? 0;
281
+ if (Date.now() - last < 500) return;
282
+ const reason =
283
+ (ev as CustomEvent<{ reason?: string }>).detail?.reason ?? 'External edit detected';
284
+ writeState({ past: [], future: [] });
285
+ bumpLabel(reason);
286
+ };
287
+ window.addEventListener('maude:invalidate-undo', onInvalidate);
288
+ return () => window.removeEventListener('maude:invalidate-undo', onInvalidate);
289
+ }, [writeState, bumpLabel]);
290
+
291
+ // Sinks API exposed via a sibling context — descendants register their
292
+ // own side-effect functions without prop-drilling.
293
+ const sinksApi = useMemo<UndoSinksValue>(
294
+ () => ({
295
+ setSink: (key, fn) => {
296
+ if (fn === undefined) {
297
+ delete sinksRef.current[key];
298
+ } else {
299
+ (sinksRef.current as Record<string, unknown>)[key] = fn as unknown;
300
+ }
301
+ },
302
+ }),
303
+ []
304
+ );
305
+
306
+ const value = useMemo<UndoStackValue>(
307
+ () => ({
308
+ push,
309
+ undo,
310
+ redo,
311
+ clear,
312
+ canUndo: canUndoOf(stateRef.current),
313
+ canRedo: canRedoOf(stateRef.current),
314
+ lastLabel,
315
+ lastTick,
316
+ }),
317
+ // `stateRef.current` doesn't trigger memo invalidation; we depend on
318
+ // `lastTick` (bumped by every push/undo/redo/clear) as the proxy so the
319
+ // memoized canUndo/canRedo readouts re-evaluate on each transition.
320
+ // eslint-disable-next-line react-hooks/exhaustive-deps
321
+ [push, undo, redo, clear, lastLabel, lastTick]
322
+ );
323
+
324
+ return (
325
+ <UndoStackContext.Provider value={value}>
326
+ <UndoSinksContext.Provider value={sinksApi}>{children}</UndoSinksContext.Provider>
327
+ </UndoStackContext.Provider>
328
+ );
329
+ }
330
+
331
+ // ─────────────────────────────────────────────────────────────────────────────
332
+ // Hooks
333
+
334
+ export function useUndoStack(): UndoStackValue {
335
+ const ctx = useContext(UndoStackContext);
336
+ if (!ctx) throw new Error('useUndoStack must be used inside <UndoStackProvider>');
337
+ return ctx;
338
+ }
339
+
340
+ export function useUndoStackOptional(): UndoStackValue {
341
+ return useContext(UndoStackContext) ?? NOOP_VALUE;
342
+ }
343
+
344
+ /**
345
+ * Sinks-registration hook. Use a setSink call inside a useEffect — register
346
+ * on mount, unregister on unmount.
347
+ */
348
+ export function useUndoSinks(): UndoSinksValue {
349
+ return useContext(UndoSinksContext) ?? NOOP_SINKS;
350
+ }
351
+
352
+ export { UndoStackContext, UndoSinksContext };
353
+
354
+ // Re-export the EditCommand type for ergonomic single-import in consumers.
355
+ export type { CommandRecord, EditCommand } from './undo-stack.ts';
@@ -83,7 +83,9 @@
83
83
  "react-dom/client": "/_canvas-runtime/react-dom_client.js",
84
84
  "react/jsx-runtime": "/_canvas-runtime/react_jsx-runtime.js",
85
85
  "react/jsx-dev-runtime": "/_canvas-runtime/react_jsx-dev-runtime.js",
86
- "pixi.js": "/_canvas-runtime/pixi-js.js"
86
+ "pixi.js": "/_canvas-runtime/pixi-js.js",
87
+ "motion": "/_canvas-runtime/motion.js",
88
+ "motion/react": "/_canvas-runtime/motion_react.js"
87
89
  }
88
90
  }
89
91
  </script>
@@ -94,11 +96,20 @@
94
96
 
95
97
  <script type="module">
96
98
  const params = new URLSearchParams(location.search);
97
- const canvasRel = params.get('canvas');
98
- const designRel = params.get('designRel') || '.design';
99
- const tokensRel = params.get('tokens') || '';
100
- const componentsRel = params.get('components') || '';
101
- const layoutRel = params.get('layout') || '';
99
+ const designRel = (params.get('designRel') || '.design').replace(/^\/+|\/+$/g, '');
100
+ // Callers may pass paths either relative to designRoot (`ui/Foo.tsx` the
101
+ // canonical contract) OR repo-relative (`.design/ui/Foo.tsx` the form
102
+ // `_active.json` stores). Normalize to the canonical form so we never
103
+ // emit `/.design/.design/...` import URLs and 404 silently.
104
+ const stripDesignPrefix = (p) => {
105
+ if (!p) return p;
106
+ const s = p.replace(/^\/+/, '');
107
+ return s.startsWith(designRel + '/') ? s.slice(designRel.length + 1) : s;
108
+ };
109
+ const canvasRel = stripDesignPrefix(params.get('canvas'));
110
+ const tokensRel = stripDesignPrefix(params.get('tokens') || '');
111
+ const componentsRel = stripDesignPrefix(params.get('components') || '');
112
+ const layoutRel = stripDesignPrefix(params.get('layout') || '');
102
113
  // Phase 6.5 — exporters pass ?hide-chrome=1 to flip the export-mode
103
114
  // stylesheet from `media="not all"` to `media="all"`, hiding the
104
115
  // dev-server overlays during capture. See `<style id="canvas-hide-chrome">`.