@1agh/maude 0.60.4 → 0.60.6

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 (34) hide show
  1. package/apps/studio/annotations-layer.tsx +21 -12
  2. package/apps/studio/api.ts +54 -5
  3. package/apps/studio/build.ts +10 -0
  4. package/apps/studio/canvas-comment-mount.tsx +7 -1
  5. package/apps/studio/canvas-icons.tsx +30 -2
  6. package/apps/studio/canvas-lib.tsx +41 -6
  7. package/apps/studio/canvas-shell.tsx +39 -43
  8. package/apps/studio/client/app.jsx +20 -13
  9. package/apps/studio/dist/client.bundle.js +1 -1
  10. package/apps/studio/dist/comment-mount.js +2 -2
  11. package/apps/studio/dom-selection.ts +67 -0
  12. package/apps/studio/hmr-broadcast.ts +40 -2
  13. package/apps/studio/input-router.tsx +26 -2
  14. package/apps/studio/sync/agent.ts +64 -13
  15. package/apps/studio/sync/codec.ts +47 -0
  16. package/apps/studio/sync/cold-start.ts +96 -0
  17. package/apps/studio/sync/migrate-seed.ts +52 -7
  18. package/apps/studio/sync/projection.ts +10 -1
  19. package/apps/studio/test/canvas-meta-api.test.ts +114 -3
  20. package/apps/studio/test/canvas-wheel-delta.test.ts +42 -0
  21. package/apps/studio/test/canvas-zoom-floor.test.ts +73 -0
  22. package/apps/studio/test/comment-mount.test.ts +10 -1
  23. package/apps/studio/test/dom-selection.test.ts +69 -1
  24. package/apps/studio/test/hmr-broadcast.test.ts +6 -4
  25. package/apps/studio/test/hmr-classify.test.ts +18 -1
  26. package/apps/studio/test/input-router.test.ts +69 -0
  27. package/apps/studio/test/mode-toggle.test.tsx +226 -0
  28. package/apps/studio/test/sync-annotations-cold-start.test.ts +302 -0
  29. package/apps/studio/test/use-tool-mode.test.tsx +33 -3
  30. package/apps/studio/tool-palette.tsx +86 -24
  31. package/apps/studio/use-tool-mode.tsx +113 -14
  32. package/apps/studio/whats-new.json +45 -0
  33. package/package.json +8 -8
  34. package/plugins/design/templates/_shell.html +31 -0
@@ -7,6 +7,7 @@ import { renderToStaticMarkup } from 'react-dom/server';
7
7
  import {
8
8
  DEFAULT_TOOLS,
9
9
  filterToolsForReadOnly,
10
+ MODE_DEFAULT_TOOL,
10
11
  ToolProvider,
11
12
  useToolMode,
12
13
  useToolModeOptional,
@@ -49,6 +50,11 @@ describe('use-tool-mode / static', () => {
49
50
  expect(Object.isFrozen(DEFAULT_TOOLS)).toBe(true);
50
51
  });
51
52
 
53
+ test('MODE_DEFAULT_TOOL — DDR-223 resting tools: preview→browse, edit→move', () => {
54
+ expect(MODE_DEFAULT_TOOL).toEqual({ preview: 'browse', edit: 'move' });
55
+ expect(Object.isFrozen(MODE_DEFAULT_TOOL)).toBe(true);
56
+ });
57
+
52
58
  test('cursors per tool (Phase 24 — ONE Kenney library for every tool + native fallback)', () => {
53
59
  const byId = Object.fromEntries(DEFAULT_TOOLS.map((t) => [t.id, t.cursor]));
54
60
  // Phase 24 — EVERY tool (incl. move) ships a Kenney data-URI SVG cursor that
@@ -97,16 +103,40 @@ describe('use-tool-mode / useToolModeOptional', () => {
97
103
  describe('use-tool-mode / SSR render', () => {
98
104
  test('ToolProvider with consumer renders without throwing', () => {
99
105
  function Consumer() {
100
- const { tool } = useToolMode();
101
- return <span data-tool={tool}>{tool}</span>;
106
+ const { tool, mode } = useToolMode();
107
+ return (
108
+ <span data-tool={tool} data-mode={mode}>
109
+ {tool}
110
+ </span>
111
+ );
102
112
  }
103
113
  const html = renderToStaticMarkup(
104
114
  <ToolProvider>
105
115
  <Consumer />
106
116
  </ToolProvider>
107
117
  );
108
- // feature-4 — boot default is browse (the mock is alive until V is pressed).
118
+ // DDR-223 (issue #93) — boot posture is EDIT with the Move (select) tool
119
+ // armed; Preview (the DDR-187 alive posture) is one toggle away.
120
+ expect(html).toContain('data-tool="move"');
121
+ expect(html).toContain('data-mode="edit"');
122
+ });
123
+
124
+ test('ToolProvider initial="browse" boots the preview mode (comment-mount / specimens)', () => {
125
+ function Consumer() {
126
+ const { tool, mode } = useToolMode();
127
+ return (
128
+ <span data-tool={tool} data-mode={mode}>
129
+ {tool}
130
+ </span>
131
+ );
132
+ }
133
+ const html = renderToStaticMarkup(
134
+ <ToolProvider initial="browse">
135
+ <Consumer />
136
+ </ToolProvider>
137
+ );
109
138
  expect(html).toContain('data-tool="browse"');
139
+ expect(html).toContain('data-mode="preview"');
110
140
  });
111
141
 
112
142
  test('ToolProvider honors initial tool', () => {
@@ -16,7 +16,9 @@ import { useEffect, useRef, useState } from 'react';
16
16
 
17
17
  import {
18
18
  IconChevronDown,
19
+ IconEye,
19
20
  IconLetterA,
21
+ IconPencilRuler,
20
22
  IconPresentation,
21
23
  IconSquare,
22
24
  SHAPE_KIND_ICONS,
@@ -217,6 +219,10 @@ const PALETTE_CSS = `
217
219
  background: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 14%, transparent);
218
220
  color: var(--maude-hud-accent, #d63b1f);
219
221
  }
222
+ /* DDR-223 — Preview/Edit/Present segmented mode toggle at the head of the
223
+ palette (owner steer 2026-08-15: icon segments — lucide eye / pencil-ruler /
224
+ presentation). Standard 32 × 32 buttons; active state reuses the
225
+ aria-pressed tint + underbar. */
220
226
  /* Stage I3 tail — "+ Element" insert popover. A vertical labeled list (not the
221
227
  shape popover's icon grid): Div/Text/Image have no recognizable glyph on
222
228
  their own, and this is a rare, must-be-legible action, not a recalled tool. */
@@ -257,9 +263,12 @@ function ensurePaletteStyles(): void {
257
263
  document.head.appendChild(s);
258
264
  }
259
265
 
260
- // feature-4 (browse/move split) — Browse leads the nav group (it's the boot
261
- // default), then Move (the select tool, V), Hand, Comment.
262
- const NAV_TOOLS = ['browse', 'move', 'hand', 'comment'] as const;
266
+ // DDR-223 — the Preview/Edit segments replace BOTH resting-tool buttons in
267
+ // the nav group: `browse` is armed by the Preview segment, `move` by the Edit
268
+ // segment (clicking the active segment re-arms it; V and Esc still arm move
269
+ // directly). Annotation tools render in BOTH modes (issue #93: preview keeps
270
+ // annotations).
271
+ const NAV_TOOLS = ['hand', 'comment'] as const;
263
272
  // Phase 24 — the two rect/ellipse buttons collapse into one Shape tool (with a
264
273
  // kind popover); sticky/arrow/text/eraser keep their order. Wave H — section
265
274
  // joins the row (it was ⇧S-only before; users couldn't discover it).
@@ -324,7 +333,18 @@ export function resolveInsertAnchor(
324
333
 
325
334
  export function ToolPalette() {
326
335
  ensurePaletteStyles();
327
- const { tool, setTool, tools, sticky, toggleSticky, shapeKind, setShapeKind } = useToolMode();
336
+ const {
337
+ tool,
338
+ setTool,
339
+ tools,
340
+ mode,
341
+ setMode,
342
+ resetTool,
343
+ sticky,
344
+ toggleSticky,
345
+ shapeKind,
346
+ setShapeKind,
347
+ } = useToolMode();
328
348
  const chrome = useChromeVisibility();
329
349
  const [mounted, setMounted] = useState(false);
330
350
  const [shapeOpen, setShapeOpen] = useState(false);
@@ -484,8 +504,65 @@ export function ToolPalette() {
484
504
  );
485
505
  };
486
506
 
507
+ // DDR-223 — the mode toggle, three segments (owner steer 2026-08-15, round
508
+ // 2): Preview (eye), Edit (pencil-ruler), Present (presentation) — icon-only,
509
+ // the words live in aria-label + title. Clicking the ACTIVE Preview/Edit
510
+ // segment re-arms that mode's resting tool (the "get me out of the pen tool"
511
+ // gesture — especially in preview, where browse has no palette button of its
512
+ // own). Present is the existing SHELL-level state (enter-only — the palette
513
+ // is hidden while presenting; exit is Esc or the floating pill); it moved
514
+ // here from the palette's right end so all three "ways of looking at the
515
+ // canvas" sit in one control.
516
+ const renderModeToggle = () => (
517
+ // biome-ignore lint/a11y/useSemanticElements: segmented control of buttons, not a form — fieldset semantics (and its default box styling) don't apply
518
+ <div className="dc-tp-group dc-tp-mode" role="group" aria-label="Canvas mode">
519
+ <button
520
+ type="button"
521
+ data-testid="palette-mode-preview"
522
+ aria-label="Preview mode — the mock is live; annotation tools stay on"
523
+ aria-pressed={mode === 'preview'}
524
+ title="Preview — the mock is live (buttons click); annotation tools stay on"
525
+ onClick={() => (mode === 'preview' ? resetTool() : setMode('preview'))}
526
+ >
527
+ <IconEye />
528
+ </button>
529
+ <button
530
+ type="button"
531
+ data-testid="palette-mode-edit"
532
+ aria-label="Edit mode — click selects & edits, like Figma (V)"
533
+ aria-pressed={mode === 'edit'}
534
+ title="Edit — click selects & edits, like Figma (V)"
535
+ onClick={() => (mode === 'edit' ? resetTool() : setMode('edit'))}
536
+ >
537
+ <IconPencilRuler />
538
+ </button>
539
+ <button
540
+ type="button"
541
+ data-testid="palette-mode-present"
542
+ aria-label="Presentation mode — hide all chrome (Esc to exit)"
543
+ aria-pressed={chrome?.present ?? false}
544
+ title="Presentation mode — hide all chrome (Esc to exit)"
545
+ onClick={() => {
546
+ // Present Mode is a SHELL-level state (it hides the menubar /
547
+ // sidebar / panels too), so the canvas iframe requests it from the
548
+ // parent shell, which flips on `.is-present` + broadcasts
549
+ // dgn:'view-chrome' back down.
550
+ try {
551
+ window.parent.postMessage({ dgn: 'present-enter' }, '*');
552
+ } catch {
553
+ /* detached / cross-origin */
554
+ }
555
+ }}
556
+ >
557
+ <IconPresentation />
558
+ </button>
559
+ </div>
560
+ );
561
+
487
562
  return (
488
563
  <div ref={containerRef} className="dc-tool-palette" role="toolbar" aria-label="Canvas tools">
564
+ {renderModeToggle()}
565
+ <div className="dc-tp-sep" />
489
566
  <div className="dc-tp-group">
490
567
  {navList.map((t) => (t ? renderToolButton(t.id, t.label, t.shortcut) : null))}
491
568
  </div>
@@ -505,7 +582,9 @@ export function ToolPalette() {
505
582
  ) : null}
506
583
  <div className="dc-tp-sep" />
507
584
  <div className="dc-tp-group">
508
- {!readOnly && (
585
+ {/* DDR-223 — "+ Element" is a JSX WRITE, an edit-mode affordance;
586
+ stickers/export/present stay in both modes. */}
587
+ {!readOnly && mode === 'edit' && (
509
588
  <span className="dc-tp-insert">
510
589
  <button
511
590
  type="button"
@@ -575,25 +654,8 @@ export function ToolPalette() {
575
654
  ⬇
576
655
  </span>
577
656
  </button>
578
- <button
579
- type="button"
580
- aria-label="Presentation mode — hide all chrome"
581
- title="Presentation mode — hide all chrome (Esc to exit)"
582
- onClick={() => {
583
- // Present Mode is a SHELL-level state (it hides the menubar /
584
- // sidebar / panels too), so the canvas iframe requests it from the
585
- // parent shell, which flips on `.is-present` + broadcasts
586
- // dgn:'view-chrome' back down. Enter-only — the palette is hidden
587
- // while presenting; exit is Esc or the floating pill.
588
- try {
589
- window.parent.postMessage({ dgn: 'present-enter' }, '*');
590
- } catch {
591
- /* detached / cross-origin */
592
- }
593
- }}
594
- >
595
- <IconPresentation />
596
- </button>
657
+ {/* DDR-223 addendum 2 — the Presentation button moved into the mode
658
+ toggle at the palette's head (third segment). */}
597
659
  </div>
598
660
  </div>
599
661
  );
@@ -44,14 +44,42 @@ export interface ToolDescriptor {
44
44
  */
45
45
  export type ShapeKind = 'square' | 'rounded' | 'circle' | 'diamond' | 'triangle' | 'triangle-down';
46
46
 
47
+ /**
48
+ * DDR-223 (issue #93) — the binary canvas mode over the tool set. `preview`
49
+ * rests on `browse` (pure pass-through, the mock is alive; annotation tools
50
+ * stay usable); `edit` rests on `move` (the Figma select ladder). The mode is
51
+ * a store-level layer — `classify()` and every `tool === 'move'` gate are
52
+ * untouched (DDR-187's decomposition stands).
53
+ */
54
+ export type CanvasMode = 'preview' | 'edit';
55
+
56
+ /** The resting tool each mode arms (and returns to via `resetTool`). */
57
+ export const MODE_DEFAULT_TOOL: Readonly<Record<CanvasMode, Tool>> = Object.freeze({
58
+ preview: 'browse',
59
+ edit: 'move',
60
+ });
61
+
62
+ /**
63
+ * Mode implied by arming a mode-exclusive resting tool. `browse` and `move`
64
+ * are the only mode-carrying tools; everything else (comment, draw set) is
65
+ * mode-NEUTRAL — arming pen in preview keeps you in preview (the issue-#93
66
+ * "annotations work in preview" contract).
67
+ */
68
+ function modeForTool(t: Tool): CanvasMode | null {
69
+ if (t === 'browse') return 'preview';
70
+ if (t === 'move') return 'edit';
71
+ return null;
72
+ }
73
+
47
74
  // Phase 21 — every tool ships a custom 32×32 SVG cursor (canvas-cursors.ts)
48
75
  // with a white outline halo so the glyph reads on any background. The native
49
76
  // crosshair/text/cell were thin + tiny ("pen almost invisible"); these mirror
50
77
  // the tool-palette icons. `move` keeps the system arrow on purpose.
51
78
  export const DEFAULT_TOOLS: readonly ToolDescriptor[] = Object.freeze([
52
- // feature-4 — Browse is the BOOT default: the mock is alive (buttons click).
53
- // Press V for the Move (select) tool. No letter shortcut — it's a deliberate
54
- // "I'm done editing" choice via the palette / Esc-from-a-draw-tool.
79
+ // feature-4 — Browse is the pure pass-through tool: the mock is alive
80
+ // (buttons click). Since DDR-223 it is the PREVIEW mode's resting tool (the
81
+ // boot default moved to edit/`move`); no letter shortcut — it's armed via
82
+ // the Preview toggle / Esc-in-preview.
55
83
  { id: 'browse', label: 'Browse', shortcut: '', cursor: TOOL_CURSORS.browse },
56
84
  { id: 'move', label: 'Select', shortcut: 'V', cursor: TOOL_CURSORS.move },
57
85
  { id: 'hand', label: 'Hand', shortcut: 'H', cursor: TOOL_CURSORS.hand },
@@ -91,6 +119,14 @@ interface ToolContextValue {
91
119
  tool: Tool;
92
120
  setTool: (t: Tool) => void;
93
121
  tools: readonly ToolDescriptor[];
122
+ /** DDR-223 — the binary canvas mode. Kept coherent with `tool` (arming
123
+ * `move`/`browse` moves the mode; `setMode` arms the mode's resting tool). */
124
+ mode: CanvasMode;
125
+ setMode: (m: CanvasMode) => void;
126
+ /** Arm the current mode's resting tool — the Esc / post-draw-commit flip.
127
+ * `move` in edit (byte-identical to the pre-DDR-223 hardcode), `browse` in
128
+ * preview (drawing an annotation never silently exits the alive posture). */
129
+ resetTool: () => void;
94
130
  /** T19 — sticky-tool double-click lock. When `sticky.locked === true` AND
95
131
  * `sticky.tool === tool`, draw tools stay armed after each shape commit
96
132
  * (T18 auto-flip is suppressed). Single-click on any other tool clears
@@ -111,23 +147,33 @@ const ToolContext = createContext<ToolContextValue | null>(null);
111
147
  export function ToolProvider({
112
148
  children,
113
149
  tools: toolsProp = DEFAULT_TOOLS,
114
- // feature-4 (browse/move split, DDR-187) — boot into `browse` so a freshly
115
- // opened mock is ALIVE (native pass-through). V flips to the Move (select)
116
- // tool. This is the user-decided boot posture (2026-07-15), zero regression
117
- // for existing canvases + every native-input surface.
118
- initial = 'browse',
150
+ // DDR-223 (issue #93, supersedes DDR-187's boot half) — authoring surfaces
151
+ // boot into EDIT with `move` (V) armed: click-selects from the first frame,
152
+ // Preview (the alive pass-through posture) is one always-visible toggle
153
+ // away. Read-only canvases (cloud viewers) keep the DDR-187 posture and
154
+ // boot preview/`browse` — a viewer's job is to use the live mock. The
155
+ // comment-mount layer passes `initial='browse'` explicitly so bare DS
156
+ // specimens stay alive too.
157
+ initial,
158
+ initialMode,
119
159
  }: {
120
160
  children: ReactNode;
121
161
  tools?: readonly ToolDescriptor[];
122
162
  initial?: Tool;
163
+ initialMode?: CanvasMode;
123
164
  }) {
124
165
  // Cloud Phase 25 C2 — a read-only canvas keeps only navigate/inspect tools.
125
166
  // Filtering HERE covers every consumer at once: the palette renders from
126
167
  // `tools`, the input router's letter shortcuts resolve against `tools`, and
127
168
  // `setTool` below refuses anything outside the list (the shell `tool-set`
128
169
  // postMessage lane included).
129
- const tools = useMemo(() => filterToolsForReadOnly(toolsProp, isReadOnlyCanvas()), [toolsProp]);
130
- const [tool, setToolState] = useState<Tool>(initial);
170
+ const readOnly = isReadOnlyCanvas();
171
+ const tools = useMemo(() => filterToolsForReadOnly(toolsProp, readOnly), [toolsProp, readOnly]);
172
+ const bootTool: Tool = initial ?? (readOnly ? 'browse' : 'move');
173
+ const bootMode: CanvasMode =
174
+ initialMode ?? modeForTool(bootTool) ?? (readOnly ? 'preview' : 'edit');
175
+ const [tool, setToolState] = useState<Tool>(bootTool);
176
+ const [mode, setModeState] = useState<CanvasMode>(bootMode);
131
177
  const [sticky, setSticky] = useState<{ tool: Tool | null; locked: boolean }>(() => ({
132
178
  tool: null,
133
179
  locked: false,
@@ -138,12 +184,39 @@ export function ToolProvider({
138
184
  // shortcut or postMessage can't arm a tool the palette doesn't show.
139
185
  if (!tools.some((d) => d.id === t)) return;
140
186
  setToolState(t);
187
+ // DDR-223 — mode⇄tool invariant: arming a resting tool moves the mode
188
+ // with it, so V, the menubar `tool-set` lane, and the Cmd+click-in-
189
+ // browse escape hatch all land in EDIT coherently (and arming browse
190
+ // lands in PREVIEW). Annotation tools leave the mode alone. This is
191
+ // also what keeps `move` structurally unreachable inside preview — the
192
+ // moment it arms, the mode is edit, so no `tool === 'move'` gate can
193
+ // ever fire while the UI claims preview.
194
+ const m = modeForTool(t);
195
+ if (m) setModeState(m);
141
196
  // Single-click on a different tool clears any sticky lock — sticky is
142
197
  // a per-tool flag, not global.
143
198
  setSticky((prev) => (prev.locked && prev.tool === t ? prev : { tool: null, locked: false }));
144
199
  },
145
200
  [tools]
146
201
  );
202
+ const setMode = useCallback(
203
+ (m: CanvasMode) => {
204
+ // Switching mode arms its resting tool (Figma-style: the toggle IS the
205
+ // posture). Read-only keeps both browse+move on its allowlist, so the
206
+ // guard below never fires there; it exists for exotic custom `tools`.
207
+ const rest = MODE_DEFAULT_TOOL[m];
208
+ if (!tools.some((d) => d.id === rest)) return;
209
+ setModeState(m);
210
+ setToolState(rest);
211
+ setSticky((prev) =>
212
+ prev.locked && prev.tool === rest ? prev : { tool: null, locked: false }
213
+ );
214
+ },
215
+ [tools]
216
+ );
217
+ const resetTool = useCallback(() => {
218
+ setTool(MODE_DEFAULT_TOOL[mode]);
219
+ }, [mode, setTool]);
147
220
  const toggleSticky = useCallback((t: Tool) => {
148
221
  setSticky((prev) => {
149
222
  if (prev.locked && prev.tool === t) return { tool: null, locked: false };
@@ -227,8 +300,20 @@ export function ToolProvider({
227
300
  }, [tool, tools]);
228
301
 
229
302
  const value = useMemo<ToolContextValue>(
230
- () => ({ tool, setTool, tools, sticky, toggleSticky, clearSticky, shapeKind, setShapeKind }),
231
- [tool, setTool, tools, sticky, toggleSticky, clearSticky, shapeKind]
303
+ () => ({
304
+ tool,
305
+ setTool,
306
+ tools,
307
+ mode,
308
+ setMode,
309
+ resetTool,
310
+ sticky,
311
+ toggleSticky,
312
+ clearSticky,
313
+ shapeKind,
314
+ setShapeKind,
315
+ }),
316
+ [tool, setTool, tools, mode, setMode, resetTool, sticky, toggleSticky, clearSticky, shapeKind]
232
317
  );
233
318
 
234
319
  return <ToolContext.Provider value={value}>{children}</ToolContext.Provider>;
@@ -240,10 +325,24 @@ export function ToolProvider({
240
325
  * `DesignCanvas` consumes that instance instead of double-mounting. The hook
241
326
  * is called unconditionally; only the returned tree branches (hook rules).
242
327
  */
243
- export function MaybeToolProvider({ children }: { children: ReactNode }) {
328
+ export function MaybeToolProvider({
329
+ children,
330
+ initial,
331
+ initialMode,
332
+ }: {
333
+ children: ReactNode;
334
+ /** DDR-223 — forwarded so the comment-mount layer can pin bare DS
335
+ * specimens to the preview/`browse` posture (see canvas-comment-mount). */
336
+ initial?: Tool;
337
+ initialMode?: CanvasMode;
338
+ }) {
244
339
  const outer = useContext(ToolContext);
245
340
  if (outer) return <>{children}</>;
246
- return <ToolProvider>{children}</ToolProvider>;
341
+ return (
342
+ <ToolProvider initial={initial} initialMode={initialMode}>
343
+ {children}
344
+ </ToolProvider>
345
+ );
247
346
  }
248
347
 
249
348
  // ─────────────────────────────────────────────────────────────────────────────
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "$schema": "./whats-new.schema.json",
3
3
  "entries": [
4
+ {
5
+ "id": "assets-arrive-and-heal",
6
+ "version": "0.60.6",
7
+ "date": "2026-08-15",
8
+ "kind": "fix",
9
+ "title": "Dropped images reach every machine, live",
10
+ "summary": "A picture added on one machine now arrives on your others while you watch: the cloud serves it the moment it has it, and a broken-image frame heals in place when the file lands — no reload needed.",
11
+ "surface": "design-ui"
12
+ },
13
+ {
14
+ "id": "preview-edit-mode-toggle",
15
+ "version": "0.60.6",
16
+ "date": "2026-08-15",
17
+ "kind": "feature",
18
+ "title": "Preview, Edit and Present in one switch",
19
+ "summary": "Canvases open in Edit — clicking selects right away, like Figma. A new three-way switch in the toolbar flips to Preview (the mock is live and clickable; comments and drawing still work) or straight into Presentation.",
20
+ "surface": "design-ui"
21
+ },
22
+ {
23
+ "id": "desktop-splash-recovers-itself",
24
+ "version": "0.60.6",
25
+ "date": "2026-08-15",
26
+ "kind": "fix",
27
+ "title": "No more getting stuck on “Starting…”",
28
+ "summary": "The desktop app could go black and sit on the startup screen for good — often right after a command finished — with a force-quit as the only way out. It now finds its way back on its own, in about a second, and keeps your session instead of losing it.",
29
+ "surface": "desktop"
30
+ },
31
+ {
32
+ "id": "annotations-survive-cold-start",
33
+ "version": "0.60.5",
34
+ "date": "2026-08-14",
35
+ "kind": "fix",
36
+ "title": "Your drawings survive a cloud restart",
37
+ "summary": "Stickers, drawings and dropped images on the canvas no longer vanish after the cloud restarts, and a freshly added image now reaches your other machines instead of showing a broken frame. Annotations keep their own edit time now, and an empty record can never silently replace real strokes.",
38
+ "surface": "design-ui"
39
+ },
40
+ {
41
+ "id": "self-contained-asset-sweep",
42
+ "version": "0.60.5",
43
+ "date": "2026-08-14",
44
+ "kind": "fix",
45
+ "title": "Asset upload works on a clean machine",
46
+ "summary": "The desktop app uploads your project images with its own bundled runtime, so the upload no longer fails with an executable-not-found error on machines without developer tools installed.",
47
+ "surface": "design-ui"
48
+ },
4
49
  {
5
50
  "id": "continuous-sync-discovery",
6
51
  "version": "0.60.3",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1agh/maude",
3
- "version": "0.60.4",
3
+ "version": "0.60.6",
4
4
  "description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -54,13 +54,13 @@
54
54
  "test:e2e:desktop:parity:cloud": "pnpm --filter @maude/desktop-e2e e2e:parity:cloud"
55
55
  },
56
56
  "optionalDependencies": {
57
- "@1agh/maude-darwin-arm64": "0.60.4",
58
- "@1agh/maude-darwin-x64": "0.60.4",
59
- "@1agh/maude-linux-arm64": "0.60.4",
60
- "@1agh/maude-linux-arm64-musl": "0.60.4",
61
- "@1agh/maude-linux-x64": "0.60.4",
62
- "@1agh/maude-linux-x64-musl": "0.60.4",
63
- "@1agh/maude-win32-x64": "0.60.4"
57
+ "@1agh/maude-darwin-arm64": "0.60.6",
58
+ "@1agh/maude-darwin-x64": "0.60.6",
59
+ "@1agh/maude-linux-arm64": "0.60.6",
60
+ "@1agh/maude-linux-arm64-musl": "0.60.6",
61
+ "@1agh/maude-linux-x64": "0.60.6",
62
+ "@1agh/maude-linux-x64-musl": "0.60.6",
63
+ "@1agh/maude-win32-x64": "0.60.6"
64
64
  },
65
65
  "files": [
66
66
  "cli",
@@ -463,6 +463,37 @@
463
463
  .catch(() => {
464
464
  /* network blip — next interaction will retry */
465
465
  });
466
+ } else if (msg.mode === 'asset') {
467
+ // 2026-08-15 annotations-assets RCA — a synced-down media file
468
+ // just landed on disk. Any <img> / SVG <image> that already
469
+ // 404'd on it shows the broken glyph FOREVER (browsers never
470
+ // retry a failed load), so the heal was invisible: the bytes
471
+ // arrived, the canvas stayed broken until a manual reload.
472
+ // Re-point matching, still-broken elements at a cache-busted
473
+ // URL — no reload, no React state loss. A later React render
474
+ // restoring the clean URL refetches it fresh (the file exists
475
+ // now), so both URLs converge on the good bytes.
476
+ const assetName = (msg.file || '').split('/').pop();
477
+ if (!assetName) return;
478
+ const v = msg.version || Date.now();
479
+ const nameMatches = function (u) {
480
+ if (!u) return false;
481
+ return u.indexOf(assetName) !== -1 || u.indexOf(encodeURIComponent(assetName)) !== -1;
482
+ };
483
+ for (const img of document.querySelectorAll('img')) {
484
+ const src = img.getAttribute('src');
485
+ if (!nameMatches(src)) continue;
486
+ // Already rendering fine — don't churn a working image.
487
+ if (img.complete && img.naturalWidth > 0) continue;
488
+ img.setAttribute('src', src.split('?')[0] + '?v=' + v);
489
+ }
490
+ for (const el of document.querySelectorAll('image')) {
491
+ const href = el.getAttribute('href') || el.getAttribute('xlink:href');
492
+ if (!nameMatches(href)) continue;
493
+ // SVG <image> exposes no broken-state flag — re-pointing a
494
+ // working one is a cheap refetch of now-cached bytes.
495
+ el.setAttribute('href', href.split('?')[0] + '?v=' + v);
496
+ }
466
497
  }
467
498
  } catch (e) {
468
499
  console.warn('[canvas-shell] HMR message parse error', e);