@1agh/maude 0.60.5 → 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.
- package/apps/studio/annotations-layer.tsx +21 -12
- package/apps/studio/canvas-comment-mount.tsx +7 -1
- package/apps/studio/canvas-icons.tsx +30 -2
- package/apps/studio/canvas-lib.tsx +25 -3
- package/apps/studio/canvas-shell.tsx +5 -4
- package/apps/studio/client/app.jsx +20 -13
- package/apps/studio/dist/client.bundle.js +1 -1
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/hmr-broadcast.ts +40 -2
- package/apps/studio/test/canvas-wheel-delta.test.ts +42 -0
- package/apps/studio/test/comment-mount.test.ts +10 -1
- package/apps/studio/test/hmr-broadcast.test.ts +6 -4
- package/apps/studio/test/hmr-classify.test.ts +18 -1
- package/apps/studio/test/mode-toggle.test.tsx +226 -0
- package/apps/studio/test/use-tool-mode.test.tsx +33 -3
- package/apps/studio/tool-palette.tsx +86 -24
- package/apps/studio/use-tool-mode.tsx +113 -14
- package/apps/studio/whats-new.json +27 -0
- package/package.json +8 -8
- package/plugins/design/templates/_shell.html +31 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
2213
|
-
// mode is locked on this tool
|
|
2214
|
-
//
|
|
2215
|
-
//
|
|
2216
|
-
//
|
|
2217
|
-
//
|
|
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)
|
|
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,
|
|
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
|
-
|
|
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
|
-
<
|
|
153
|
-
<path d="
|
|
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,
|
|
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(
|
|
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(-
|
|
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
|
|
3273
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
374
|
-
//
|
|
375
|
-
// Figma
|
|
376
|
-
//
|
|
377
|
-
// `
|
|
378
|
-
|
|
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(
|
|
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
|
|
406
|
+
(readOnly
|
|
407
|
+
? `<span>Your mock is <strong>live</strong> — click things to try it. Press ${kbd} to select & 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(
|
|
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';
|