@1agh/maude 0.60.5 → 0.60.7

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.
@@ -956,7 +956,7 @@ function scaleStrokeInGroup(
956
956
  export function AnnotationsLayer() {
957
957
  countRender('annotationRenders');
958
958
  ensureAnnotStyles();
959
- const { tool, setTool, sticky, tools, shapeKind } = useToolMode();
959
+ const { tool, setTool, resetTool, sticky, tools, shapeKind } = useToolMode();
960
960
  const theme = useCanvasChromeTheme();
961
961
  const controller = useViewportControllerContext();
962
962
  const vp = controller?.viewport ?? null;
@@ -2008,7 +2008,9 @@ export function AnnotationsLayer() {
2008
2008
  setEditCaretPoint({ x: e.clientX, y: e.clientY });
2009
2009
  setEditingId(strokeId);
2010
2010
  if (annotSel) annotSel.replace(strokeId);
2011
- setTool('move');
2011
+ // DDR-223 — back to the MODE's resting tool (move in edit, browse in
2012
+ // preview): editing a text annotation shouldn't exit preview.
2013
+ resetTool();
2012
2014
  return true;
2013
2015
  }
2014
2016
  const editableTarget = findEditableElementAt(e.clientX, e.clientY);
@@ -2020,7 +2022,7 @@ export function AnnotationsLayer() {
2020
2022
  showCanvasToast(
2021
2023
  'This text is filled in from code (a variable) — edit it via chat or /design:edit.'
2022
2024
  );
2023
- setTool('move');
2025
+ resetTool();
2024
2026
  return true;
2025
2027
  }
2026
2028
  document.dispatchEvent(
@@ -2028,6 +2030,9 @@ export function AnnotationsLayer() {
2028
2030
  detail: { el: editableTarget, clientX: e.clientX, clientY: e.clientY },
2029
2031
  })
2030
2032
  );
2033
+ // Deliberately `move`, not `resetTool` — editing ARTBOARD content is
2034
+ // an edit-mode activity, so this one flips to edit even from preview
2035
+ // (DDR-223 mode⇄tool sync).
2031
2036
  setTool('move');
2032
2037
  return true;
2033
2038
  }
@@ -2037,7 +2042,7 @@ export function AnnotationsLayer() {
2037
2042
  setPendingText({ x: wx, y: wy });
2038
2043
  if (annotSel) annotSel.clear();
2039
2044
  const stickyOnText = sticky.locked && sticky.tool === 'text';
2040
- if (!stickyOnText) setTool('move');
2045
+ if (!stickyOnText) resetTool();
2041
2046
  return true;
2042
2047
  }
2043
2048
  return true;
@@ -2061,6 +2066,7 @@ export function AnnotationsLayer() {
2061
2066
  annotSel,
2062
2067
  sticky,
2063
2068
  setTool,
2069
+ resetTool,
2064
2070
  ]
2065
2071
  );
2066
2072
 
@@ -2209,21 +2215,24 @@ export function AnnotationsLayer() {
2209
2215
  showOnceHint('chain', '⌘Enter commits and creates the next one beside it.');
2210
2216
  }
2211
2217
  }
2212
- // T18 / T19 — flip the tool back to Move after every commit UNLESS sticky
2213
- // mode is locked on this tool. Sticky lets the user draw many shapes in a
2214
- // row (canonical pattern: tldraw double-click to lock). Eraser stays
2215
- // armed by default — that tool is destructive, not constructive.
2216
- // Map a highlighter pen (a 'pen' stroke with the flag) back to the
2217
- // 'highlighter' tool id so its sticky-lock check matches the active tool.
2218
+ // T18 / T19 — flip the tool back to the MODE's resting tool after every
2219
+ // commit UNLESS sticky mode is locked on this tool (DDR-223: move in edit
2220
+ // — byte-identical to the old hardcode — browse in preview, so drawing an
2221
+ // annotation never silently exits the alive posture). Sticky lets the user
2222
+ // draw many shapes in a row (canonical pattern: tldraw double-click to
2223
+ // lock). Eraser stays armed by default — that tool is destructive, not
2224
+ // constructive. Map a highlighter pen (a 'pen' stroke with the flag) back
2225
+ // to the 'highlighter' tool id so its sticky-lock check matches the
2226
+ // active tool.
2218
2227
  const toolJustUsed = cur.tool === 'pen' && cur.highlighter ? 'highlighter' : cur.tool;
2219
2228
  if (toolJustUsed !== 'eraser') {
2220
2229
  const stickyOnThis = sticky.locked && sticky.tool === toolJustUsed;
2221
- if (!stickyOnThis) setTool('move');
2230
+ if (!stickyOnThis) resetTool();
2222
2231
  }
2223
2232
  drawAnchorRef.current = null;
2224
2233
  lastDrawPointRef.current = null;
2225
2234
  setDrawing(null);
2226
- }, [isActive, isErase, visible, commitStrokes, annotSel, setTool, sticky]);
2235
+ }, [isActive, isErase, visible, commitStrokes, annotSel, resetTool, sticky]);
2227
2236
 
2228
2237
  // T21 — abort a mid-stroke draw without committing. Dispatched by the
2229
2238
  // canvas-shell Esc handler (`maude:cancel-stroke`). Safe to call when
@@ -496,7 +496,13 @@ export interface MountCanvasOptions {
496
496
  function buildCanvasTree(Canvas: ComponentType, file: string | undefined): ReactNode {
497
497
  return createElement(
498
498
  MaybeToolProvider,
499
- null,
499
+ // DDR-223 — this layer's provider instance keeps the DDR-187 posture:
500
+ // bare DS specimens stay ALIVE with native cursors (there is no palette,
501
+ // no editing, nothing to boot into edit for). On a UI canvas the inner
502
+ // canvas-lib ToolProvider (a separate bundle, separate context) owns the
503
+ // real boot posture (edit/`move`); this ancestor instance only carries the
504
+ // comment tool + claim layer and converges on the first tool keydown.
505
+ { initial: 'browse' as const },
500
506
  createElement(
501
507
  MaybeSelectionSetProvider,
502
508
  null,
@@ -52,6 +52,31 @@ export function IconBrowse(props: IconProps) {
52
52
  );
53
53
  }
54
54
 
55
+ // DDR-223 addendum 2 — the Preview/Edit/Present toggle glyphs (owner steer
56
+ // 2026-08-15): lucide `eye` + `pencil-ruler` (ISC), redrawn in the house
57
+ // 24 × 24 / 1.75-stroke language. Mode segments deliberately do NOT reuse tool
58
+ // glyphs — a mode is a way of looking at the canvas, not a recalled tool.
59
+ export function IconEye(props: IconProps) {
60
+ return (
61
+ <Svg {...props}>
62
+ <path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
63
+ <circle cx="12" cy="12" r="3" />
64
+ </Svg>
65
+ );
66
+ }
67
+ export function IconPencilRuler(props: IconProps) {
68
+ return (
69
+ <Svg {...props}>
70
+ <path d="M13 7 8.7 2.7a2.41 2.41 0 0 0-3.4 0L2.7 5.3a2.41 2.41 0 0 0 0 3.4L7 13" />
71
+ <path d="m8 6 2-2" />
72
+ <path d="m18 16 2-2" />
73
+ <path d="m17 11 4.3 4.3c.94.94.94 2.46 0 3.4l-2.6 2.6c-.94.94-2.46.94-3.4 0L11 17" />
74
+ <path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" />
75
+ <path d="m15 5 4 4" />
76
+ </Svg>
77
+ );
78
+ }
79
+
55
80
  export function IconHand(props: IconProps) {
56
81
  return (
57
82
  <Svg {...props}>
@@ -146,11 +171,14 @@ export function IconLink(props: IconProps) {
146
171
  );
147
172
  }
148
173
 
174
+ // DDR-223 addendum 2 — redrawn to the lucide `presentation` glyph so the three
175
+ // mode-segment icons (eye / pencil-ruler / presentation) read as one family.
149
176
  export function IconPresentation(props: IconProps) {
150
177
  return (
151
178
  <Svg {...props}>
152
- <rect x="3" y="4" width="18" height="12" rx="1.5" />
153
- <path d="M9 21l3-5 3 5" />
179
+ <path d="M2 3h20" />
180
+ <path d="M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3" />
181
+ <path d="m7 21 5-5 5 5" />
154
182
  </Svg>
155
183
  );
156
184
  }
@@ -917,6 +917,21 @@ export interface ViewportControllerHandle {
917
917
  isInteracting: boolean;
918
918
  }
919
919
 
920
+ // Issue #94 — wheel pan used raw WheelEvent deltas as CSS pixels, but the
921
+ // Wheel Events spec lets a browser report deltas in three different units
922
+ // (`deltaMode`): pixel (0, trackpads and most Chromium mice), line (1,
923
+ // Firefox's default for physical mice — one notch is a small integer like
924
+ // 3), or page (2). Consuming a line-mode `3` as 3px of pan per notch is why
925
+ // wheel-driven panning read as "very slow" while trackpad panning (already
926
+ // pixel-mode) felt fine. LINES_TO_PIXELS mirrors the pixels-per-line browsers
927
+ // themselves use for native line-scrolling at default font size.
928
+ const LINES_TO_PIXELS = 16;
929
+ export function wheelDeltaToPixels(delta: number, deltaMode: number, pageSize: number): number {
930
+ if (deltaMode === 1) return delta * LINES_TO_PIXELS; // DOM_DELTA_LINE
931
+ if (deltaMode === 2) return delta * pageSize; // DOM_DELTA_PAGE
932
+ return delta; // DOM_DELTA_PIXEL (0) — already pixels
933
+ }
934
+
920
935
  // Exported (like `revealAxisDelta` in canvas-shell.tsx) so a pure-logic unit
921
936
  // test can pin the zoom floor without booting the DOM/host-dependent
922
937
  // viewport controller.
@@ -1412,6 +1427,13 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
1412
1427
  const rect = host.getBoundingClientRect();
1413
1428
  const cx = e.clientX - rect.left;
1414
1429
  const cy = e.clientY - rect.top;
1430
+ // Normalize line/page-mode deltas to pixels before anything below
1431
+ // reads them — see `wheelDeltaToPixels` (issue #94). Zoom's own
1432
+ // [-50, 50] clamp already absorbed this for the pinch/ctrl+wheel path,
1433
+ // but pan had no such clamp so a line-mode mouse wheel panned at only
1434
+ // a few px per notch.
1435
+ const dx = wheelDeltaToPixels(e.deltaX, e.deltaMode, host.clientWidth);
1436
+ const dy = wheelDeltaToPixels(e.deltaY, e.deltaMode, host.clientHeight);
1415
1437
  // Mac trackpad pinch fires wheel with ctrlKey:true automatically, even
1416
1438
  // without a physical Ctrl press — so the same branch covers both
1417
1439
  // Ctrl+wheel (mouse) and pinch-zoom (trackpad).
@@ -1422,7 +1444,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
1422
1444
  // notch = ±100) onto the same perceived-speed curve. Mouse-wheel
1423
1445
  // users still get smooth zoom (clamped notches accumulate at the
1424
1446
  // same exp rate), trackpad-pinch users no longer outpace them.
1425
- const clamped = Math.max(-50, Math.min(50, e.deltaY));
1447
+ const clamped = Math.max(-50, Math.min(50, dy));
1426
1448
  const factor = Math.exp(-clamped * WHEEL_ZOOM_K);
1427
1449
  zoomAt(factor, cx, cy);
1428
1450
  return;
@@ -1432,14 +1454,14 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
1432
1454
  // doesn't, Safari sometimes does); some don't. Read whichever axis
1433
1455
  // actually carries energy so the gesture lands horizontally either way.
1434
1456
  if (e.shiftKey) {
1435
- const d = Math.abs(e.deltaY) >= Math.abs(e.deltaX) ? e.deltaY : e.deltaX;
1457
+ const d = Math.abs(dy) >= Math.abs(dx) ? dy : dx;
1436
1458
  panBy(-d, 0);
1437
1459
  return;
1438
1460
  }
1439
1461
  // Default: trackpad two-finger scroll → 2D pan. The negation keeps the
1440
1462
  // "content follows your fingers" mapping (Mac natural scroll). Mouse
1441
1463
  // wheels with only deltaY pan vertically.
1442
- panBy(-e.deltaX, -e.deltaY);
1464
+ panBy(-dx, -dy);
1443
1465
  };
1444
1466
 
1445
1467
  // Safari/WKWebView-only trackpad pinch. WebKit exposes the native pinch via
@@ -2433,7 +2433,7 @@ function CanvasRouter({
2433
2433
  distributeArtboards: (axis: 'x' | 'y') => void;
2434
2434
  alignArtboards: (mode: 'left' | 'right' | 'center-x' | 'top' | 'bottom' | 'center-y') => void;
2435
2435
  }) {
2436
- const { tool, setTool, clearSticky } = useToolMode();
2436
+ const { tool, setTool, resetTool, clearSticky } = useToolMode();
2437
2437
  const selSet = useSelectionSet();
2438
2438
  const annotSel = useAnnotationSelection();
2439
2439
  const ctxMenu = useContextMenu();
@@ -3269,10 +3269,11 @@ function CanvasRouter({
3269
3269
  } catch {
3270
3270
  /* non-DOM env */
3271
3271
  }
3272
- // T19 — clear sticky-tool lock + flip back to Move. Esc is the
3273
- // canonical "back to default state" gesture across canvas apps.
3272
+ // T19 — clear sticky-tool lock + flip back to the mode's resting tool
3273
+ // (DDR-223: move in edit, browse in preview). Esc is the canonical
3274
+ // "back to default state" gesture across canvas apps.
3274
3275
  clearSticky();
3275
- if (tool !== 'move') setTool('move');
3276
+ resetTool();
3276
3277
  ctxMenu.close();
3277
3278
  selSet.clear();
3278
3279
  annotSel.clear();
@@ -370,16 +370,20 @@ function shellToast(message, ok = false, action) {
370
370
  );
371
371
  }
372
372
 
373
- // feature-4 (browse/move split, DDR-187) — one-time first-run teaching hint.
374
- // The boot default is `browse` (the mock is ALIVE — buttons click), so the
375
- // Figma muscle-memory gesture (plain click = select) does nothing until V is
376
- // pressed. Surface that ONCE, gated by a localStorage marker (same family as
377
- // `mdcc-whatsnew-seen`). Auto-dismisses; a V press (learned) clears it early.
378
- const BROWSE_HINT_SEEN = 'maude-browse-hint-seen';
373
+ // DDR-223 (issue #93, supersedes DDR-187's boot half) — one-time first-run
374
+ // teaching hint. Authoring canvases now boot into EDIT (`move`/V armed): click
375
+ // selects like Figma, and the mock is inert until the Preview toggle flips the
376
+ // alive posture back on. Teach exactly that ONCE, gated by a NEW localStorage
377
+ // marker (`maude-mode-hint-seen` — deliberately not the old
378
+ // `maude-browse-hint-seen`, which taught the opposite posture; every existing
379
+ // user should see the new hint once). Read-only viewers still boot preview
380
+ // (alive) and keep the DDR-187 wording. Auto-dismisses; the taught gesture
381
+ // (V in read-only) clears it early.
382
+ const MODE_HINT_SEEN = 'maude-mode-hint-seen';
379
383
  function browseFirstRunHint(readOnly = false) {
380
384
  if (typeof document === 'undefined' || typeof localStorage === 'undefined') return;
381
385
  try {
382
- if (localStorage.getItem(BROWSE_HINT_SEEN) === '1') return;
386
+ if (localStorage.getItem(MODE_HINT_SEEN) === '1') return;
383
387
  } catch {
384
388
  return;
385
389
  }
@@ -393,18 +397,21 @@ function browseFirstRunHint(readOnly = false) {
393
397
  'font:12px/1.5 var(--font-ui,system-ui,sans-serif);background:var(--surface-2,#1b1e24);' +
394
398
  'color:var(--text-1,#e7eaf0);border:1px solid var(--border-1,#333a45);' +
395
399
  'box-shadow:0 10px 34px rgba(0,0,0,.42);opacity:0;transition:opacity 160ms ease;';
400
+ const kbd =
401
+ '<kbd style="padding:1px 6px;border-radius:5px;border:1px solid var(--border-1,#333a45);' +
402
+ 'background:var(--surface-3,#262b33);font-family:var(--font-mono,monospace)">V</kbd>';
396
403
  el.innerHTML =
397
- '<span>Your mock is <strong>live</strong> — click things to try it. ' +
398
- 'Press <kbd style="padding:1px 6px;border-radius:5px;border:1px solid var(--border-1,#333a45);' +
399
- 'background:var(--surface-3,#262b33);font-family:var(--font-mono,monospace)">V</kbd> ' +
400
404
  // Cloud Phase 25 C2 — a viewer selects to inspect, never to edit; the
401
405
  // hint must not promise an editor the role doesn't have.
402
- (readOnly ? 'to select &amp; inspect.</span>' : 'to select &amp; edit like Figma.</span>') +
406
+ (readOnly
407
+ ? `<span>Your mock is <strong>live</strong> — click things to try it. Press ${kbd} to select &amp; inspect.</span>`
408
+ : '<span>You’re in <strong>Edit</strong> — click selects, like Figma. ' +
409
+ 'Switch to <strong>Preview</strong> in the toolbar to use the live mock.</span>') +
403
410
  '<button type="button" aria-label="Dismiss" style="background:none;border:none;color:inherit;' +
404
411
  'cursor:pointer;font-size:15px;line-height:1;opacity:.65;padding:2px">×</button>';
405
412
  const dismiss = () => {
406
413
  try {
407
- localStorage.setItem(BROWSE_HINT_SEEN, '1');
414
+ localStorage.setItem(MODE_HINT_SEEN, '1');
408
415
  } catch {
409
416
  /* private mode */
410
417
  }
@@ -416,7 +423,7 @@ function browseFirstRunHint(readOnly = false) {
416
423
  if ((e.key === 'v' || e.key === 'V') && !e.metaKey && !e.ctrlKey && !e.altKey) dismiss();
417
424
  };
418
425
  el.querySelector('button')?.addEventListener('click', dismiss);
419
- document.addEventListener('keydown', onV, true);
426
+ if (readOnly) document.addEventListener('keydown', onV, true);
420
427
  document.body.appendChild(el);
421
428
  requestAnimationFrame(() => {
422
429
  el.style.opacity = '1';