@1agh/maude 0.60.4 → 0.60.5
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/api.ts +54 -5
- package/apps/studio/build.ts +10 -0
- package/apps/studio/canvas-lib.tsx +16 -3
- package/apps/studio/canvas-shell.tsx +34 -39
- package/apps/studio/dist/client.bundle.js +1 -1
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dom-selection.ts +67 -0
- package/apps/studio/input-router.tsx +26 -2
- package/apps/studio/sync/agent.ts +64 -13
- package/apps/studio/sync/codec.ts +47 -0
- package/apps/studio/sync/cold-start.ts +96 -0
- package/apps/studio/sync/migrate-seed.ts +52 -7
- package/apps/studio/sync/projection.ts +10 -1
- package/apps/studio/test/canvas-meta-api.test.ts +114 -3
- package/apps/studio/test/canvas-zoom-floor.test.ts +73 -0
- package/apps/studio/test/dom-selection.test.ts +69 -1
- package/apps/studio/test/input-router.test.ts +69 -0
- package/apps/studio/test/sync-annotations-cold-start.test.ts +302 -0
- package/apps/studio/whats-new.json +18 -0
- package/package.json +8 -8
package/apps/studio/api.ts
CHANGED
|
@@ -1499,11 +1499,14 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1499
1499
|
// shared `.meta.json` document. It lives in `_canvas-state/<slug>.view.json`
|
|
1500
1500
|
// ({ viewport }) — gitignored, swept on delete, export-excluded. DISTINCT from
|
|
1501
1501
|
// the legacy `_canvas-state/<slug>.json` ({ sections, viewport:{x,y,scale} })
|
|
1502
|
-
// store: that uses `scale` clamped 0.05–8, this uses `zoom` clamped 0.
|
|
1502
|
+
// store: that uses `scale` clamped 0.05–8, this uses `zoom` clamped 0.02–4, so
|
|
1503
1503
|
// overloading one file would let the two writers clobber each other's shape.
|
|
1504
1504
|
|
|
1505
|
-
/** Validate a candidate viewport — finite x/y, zoom clamped [0.
|
|
1506
|
-
* Phase 4 rule).
|
|
1505
|
+
/** Validate a candidate viewport — finite x/y, zoom clamped [0.02, 4] (the
|
|
1506
|
+
* Phase 4 rule). Mirrors `ZOOM_MIN` in canvas-lib.tsx (issue #91) — keep the
|
|
1507
|
+
* two floors in lockstep or a save/reload round-trip silently re-clamps a
|
|
1508
|
+
* zoom the client just let the user reach. Returns the normalized viewport,
|
|
1509
|
+
* or null when invalid. */
|
|
1507
1510
|
function normalizeViewport(v: unknown): { x: number; y: number; zoom: number } | null {
|
|
1508
1511
|
if (!v || typeof v !== 'object' || Array.isArray(v)) return null;
|
|
1509
1512
|
const vv = v as { x?: unknown; y?: unknown; zoom?: unknown };
|
|
@@ -1512,7 +1515,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1512
1515
|
Number.isFinite(vv.y as number) &&
|
|
1513
1516
|
Number.isFinite(vv.zoom as number)
|
|
1514
1517
|
) {
|
|
1515
|
-
const zoom = Math.min(4, Math.max(0.
|
|
1518
|
+
const zoom = Math.min(4, Math.max(0.02, vv.zoom as number));
|
|
1516
1519
|
return { x: vv.x as number, y: vv.y as number, zoom };
|
|
1517
1520
|
}
|
|
1518
1521
|
return null;
|
|
@@ -1571,6 +1574,41 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1571
1574
|
return [...new Set(out)];
|
|
1572
1575
|
}
|
|
1573
1576
|
|
|
1577
|
+
// Issue #91 security follow-up — `layout.artboards[]` (DDR-027 position-only
|
|
1578
|
+
// entries) is reachable from the untrusted canvas origin (DDR-054) via the
|
|
1579
|
+
// same PATCH lane as overlays/locked above, but previously had no count or
|
|
1580
|
+
// magnitude bound. `fit()`/`computeFit` (canvas-lib.tsx) now correctly frames
|
|
1581
|
+
// whatever bounding box this describes — before the zoom-floor fix, a hard
|
|
1582
|
+
// 0.1 zoom clamp accidentally masked an unbounded layout into a cropped,
|
|
1583
|
+
// mismatched view; after it, an oversized synced layout can legitimately be
|
|
1584
|
+
// framed whole, promoting every artboard in it to its own GPU layer at once
|
|
1585
|
+
// (canvas-lib.tsx `content-visibility` comment). Cap count + coordinate
|
|
1586
|
+
// magnitude the same way `normalizeOverlays`/`normalizeLocked` cap theirs.
|
|
1587
|
+
const MAX_LAYOUT_ARTBOARDS = 2000;
|
|
1588
|
+
const MAX_LAYOUT_COORD = 1_000_000;
|
|
1589
|
+
|
|
1590
|
+
function normalizeLayoutArtboards(
|
|
1591
|
+
v: unknown
|
|
1592
|
+
): Array<{ id: string; x: number; y: number }> | null {
|
|
1593
|
+
if (!Array.isArray(v) || v.length > MAX_LAYOUT_ARTBOARDS) return null;
|
|
1594
|
+
const out: Array<{ id: string; x: number; y: number }> = [];
|
|
1595
|
+
for (const entry of v) {
|
|
1596
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null;
|
|
1597
|
+
const e = entry as { id?: unknown; x?: unknown; y?: unknown };
|
|
1598
|
+
if (typeof e.id !== 'string' || e.id.length === 0 || e.id.length > 128) return null;
|
|
1599
|
+
if (
|
|
1600
|
+
!Number.isFinite(e.x as number) ||
|
|
1601
|
+
!Number.isFinite(e.y as number) ||
|
|
1602
|
+
Math.abs(e.x as number) > MAX_LAYOUT_COORD ||
|
|
1603
|
+
Math.abs(e.y as number) > MAX_LAYOUT_COORD
|
|
1604
|
+
) {
|
|
1605
|
+
return null;
|
|
1606
|
+
}
|
|
1607
|
+
out.push({ id: e.id, x: e.x as number, y: e.y as number });
|
|
1608
|
+
}
|
|
1609
|
+
return out;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1574
1612
|
/** Raw view-file contents, tolerant of a missing/corrupt file (→ `{}`) — the
|
|
1575
1613
|
* read half of the read-modify-write both `saveCanvasView` and
|
|
1576
1614
|
* `saveCanvasOverlays` need so writing one lane never clobbers the other. */
|
|
@@ -1819,7 +1857,18 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1819
1857
|
if (patch.layout === null) {
|
|
1820
1858
|
next.layout = undefined;
|
|
1821
1859
|
} else if (typeof patch.layout === 'object' && !Array.isArray(patch.layout)) {
|
|
1822
|
-
|
|
1860
|
+
const rawLayout = patch.layout as Record<string, unknown>;
|
|
1861
|
+
if ('artboards' in rawLayout) {
|
|
1862
|
+
// Bounded — see normalizeLayoutArtboards. An oversized/malformed
|
|
1863
|
+
// `artboards` array is a silent no-op (leaves the prior persisted
|
|
1864
|
+
// layout in place), matching the overlays/locked lanes' convention.
|
|
1865
|
+
const artboards = normalizeLayoutArtboards(rawLayout.artboards);
|
|
1866
|
+
if (artboards !== null) {
|
|
1867
|
+
next.layout = { ...rawLayout, artboards };
|
|
1868
|
+
}
|
|
1869
|
+
} else {
|
|
1870
|
+
next.layout = patch.layout;
|
|
1871
|
+
}
|
|
1823
1872
|
}
|
|
1824
1873
|
// Defensive: a stale inline viewport must never persist in the versioned
|
|
1825
1874
|
// file (the camera lane owns it now). JSON.stringify drops undefined keys.
|
package/apps/studio/build.ts
CHANGED
|
@@ -353,6 +353,16 @@ export function writeCompileEntry(target: PlatformTarget): string {
|
|
|
353
353
|
// Side-effect module: must be imported BEFORE any oxc-parser usage.
|
|
354
354
|
import bindingPath from ${JSON.stringify(bindingSpec)} with { type: 'file' };
|
|
355
355
|
process.env.NAPI_RS_NATIVE_LIBRARY_PATH = bindingPath;
|
|
356
|
+
// DDR-177 — a compiled binary must be self-contained: every runtime-spawned
|
|
357
|
+
// child (canvas-build worker, asset-sweep worker) resolves its JS runtime via
|
|
358
|
+
// resolveBunPath() = MAUDE_BUN_PATH || 'bun'. The desktop app launches this
|
|
359
|
+
// binary with a GUI PATH that has no 'bun', and NOTHING set MAUDE_BUN_PATH —
|
|
360
|
+
// so the asset sweep died with "Executable not found in $PATH" on machines
|
|
361
|
+
// without a user-installed bun. Default it to THIS binary: workerEnv() adds
|
|
362
|
+
// BUN_BE_BUN=1 when MAUDE_BUN_PATH === process.execPath, so children re-enter
|
|
363
|
+
// the sidecar as a plain JS runtime. An explicit launcher-set value (the
|
|
364
|
+
// cell's studio-child.mjs) still wins.
|
|
365
|
+
process.env.MAUDE_BUN_PATH ||= process.execPath;
|
|
356
366
|
`;
|
|
357
367
|
writeFileSync(initPath, initContent);
|
|
358
368
|
|
|
@@ -214,7 +214,15 @@ export function AIPlaceholder({
|
|
|
214
214
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
215
215
|
// Module constants
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
// Issue #91 — was 0.1. A board spanning more world units than ~10x the
|
|
218
|
+
// viewport could never be framed whole: `fit()` and manual zoom-out both
|
|
219
|
+
// funnel through `clampZoom` (below), so the old floor blocked "fit to
|
|
220
|
+
// screen" from ever reaching the zoom a wide board actually needs. 0.02
|
|
221
|
+
// matches the interactive floor other canvas tools (Figma, tldraw) ship.
|
|
222
|
+
// Keep in lockstep with the mirror clamp in `api.ts`'s `normalizeViewport`
|
|
223
|
+
// (the per-machine camera persisted to `_canvas-state/<slug>.view.json`,
|
|
224
|
+
// DDR-115) — that one re-clamps on save/reload independently of this file.
|
|
225
|
+
const ZOOM_MIN = 0.02;
|
|
218
226
|
const ZOOM_MAX = 4.0;
|
|
219
227
|
const ZOOM_STEP_IN = 1.2;
|
|
220
228
|
const ZOOM_STEP_OUT = 1 / 1.2;
|
|
@@ -685,7 +693,9 @@ function synthDefaultGrid(seeds: ArtboardSeed[]): ArtboardRect[] {
|
|
|
685
693
|
});
|
|
686
694
|
}
|
|
687
695
|
|
|
688
|
-
|
|
696
|
+
// Exported for the same reason as `clampZoom` above — lets a unit test prove
|
|
697
|
+
// the raw fit computation for a wide board isn't what's clamping the zoom.
|
|
698
|
+
export function computeFit(rects: ArtboardRect[], hostEl: HTMLElement, pad = 24): ViewportState {
|
|
689
699
|
if (rects.length === 0) return { x: 0, y: 0, zoom: 1 };
|
|
690
700
|
let xMin = Number.POSITIVE_INFINITY;
|
|
691
701
|
let yMin = Number.POSITIVE_INFINITY;
|
|
@@ -907,7 +917,10 @@ export interface ViewportControllerHandle {
|
|
|
907
917
|
isInteracting: boolean;
|
|
908
918
|
}
|
|
909
919
|
|
|
910
|
-
|
|
920
|
+
// Exported (like `revealAxisDelta` in canvas-shell.tsx) so a pure-logic unit
|
|
921
|
+
// test can pin the zoom floor without booting the DOM/host-dependent
|
|
922
|
+
// viewport controller.
|
|
923
|
+
export function clampZoom(z: number): number {
|
|
911
924
|
if (!Number.isFinite(z)) return 1;
|
|
912
925
|
return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, z));
|
|
913
926
|
}
|
|
@@ -67,15 +67,13 @@ import {
|
|
|
67
67
|
import { ContextualToolbar } from './contextual-toolbar.tsx';
|
|
68
68
|
import { CursorsOverlay } from './cursors-overlay.tsx';
|
|
69
69
|
import {
|
|
70
|
+
buildComposeSelection,
|
|
70
71
|
cssPath,
|
|
71
72
|
deriveFile,
|
|
72
|
-
domPath,
|
|
73
73
|
hoverTargetToSelection,
|
|
74
|
+
openCommentComposer,
|
|
74
75
|
realClasses,
|
|
75
76
|
resolveSelectionEl,
|
|
76
|
-
scopedCdSelector,
|
|
77
|
-
selectorIndex,
|
|
78
|
-
shortText,
|
|
79
77
|
} from './dom-selection.ts';
|
|
80
78
|
import { setElementDragActive } from './drag-state.ts';
|
|
81
79
|
import { EqualSpacingHandles } from './equal-spacing-handles.tsx';
|
|
@@ -1378,33 +1376,17 @@ function buildRegistry(deps: {
|
|
|
1378
1376
|
});
|
|
1379
1377
|
};
|
|
1380
1378
|
|
|
1379
|
+
// issue-90 — right-click "Add comment" (element / artboard-chrome / world)
|
|
1380
|
+
// used to only postMessage the parent shell, which merely mirrors the
|
|
1381
|
+
// target for the StatusBar/sidebar; it never dispatched `cm:open-composer`,
|
|
1382
|
+
// the event `comments-overlay.tsx` actually listens on to open the
|
|
1383
|
+
// composer. Selecting the menu item did nothing. `openCommentComposer`
|
|
1384
|
+
// sends both, matching the working comment-tool-click path
|
|
1385
|
+
// (canvas-comment-mount.tsx's `dropComment`).
|
|
1381
1386
|
const postComposeForTarget = (target: ContextTarget): void => {
|
|
1382
1387
|
if (typeof window === 'undefined') return;
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
: cssPath(target.el);
|
|
1386
|
-
const sel: Selection | null = target.el
|
|
1387
|
-
? {
|
|
1388
|
-
file: deriveFile(),
|
|
1389
|
-
id: target.cdId ?? undefined,
|
|
1390
|
-
selector: composeSelector,
|
|
1391
|
-
artboardId: target.artboardId,
|
|
1392
|
-
index: target.cdId ? selectorIndex(document, composeSelector, target.el) : 0,
|
|
1393
|
-
tag: target.el.tagName.toLowerCase(),
|
|
1394
|
-
classes: realClasses(target.el),
|
|
1395
|
-
text: shortText(target.el, 240),
|
|
1396
|
-
dom_path: domPath(target.el),
|
|
1397
|
-
bounds: (target.el as HTMLElement).getBoundingClientRect
|
|
1398
|
-
? boundsOf(target.el as HTMLElement)
|
|
1399
|
-
: null,
|
|
1400
|
-
html: (target.el.outerHTML ?? '').slice(0, 4000),
|
|
1401
|
-
}
|
|
1402
|
-
: null;
|
|
1403
|
-
try {
|
|
1404
|
-
window.parent.postMessage({ dgn: 'comment-compose', selection: sel }, '*');
|
|
1405
|
-
} catch {
|
|
1406
|
-
/* ignore */
|
|
1407
|
-
}
|
|
1388
|
+
const sel = buildComposeSelection(target, target.clientX, target.clientY);
|
|
1389
|
+
openCommentComposer(sel, target.clientX, target.clientY);
|
|
1408
1390
|
};
|
|
1409
1391
|
|
|
1410
1392
|
const fitItem: MenuItem = {
|
|
@@ -2141,6 +2123,18 @@ function buildRegistry(deps: {
|
|
|
2141
2123
|
],
|
|
2142
2124
|
],
|
|
2143
2125
|
'artboard-chrome': [
|
|
2126
|
+
[
|
|
2127
|
+
// issue-90 — right-click on an artboard's own label/border chrome
|
|
2128
|
+
// (outside its body, where `data-cd-id` never resolves) had no
|
|
2129
|
+
// "Add comment" entry at all; only a hit inside the body counted as
|
|
2130
|
+
// `element`. Anchors at the click point (no element under it).
|
|
2131
|
+
{
|
|
2132
|
+
id: 'add-comment',
|
|
2133
|
+
label: 'Add comment',
|
|
2134
|
+
shortcut: 'C',
|
|
2135
|
+
onSelect: postComposeForTarget,
|
|
2136
|
+
},
|
|
2137
|
+
],
|
|
2144
2138
|
[
|
|
2145
2139
|
{
|
|
2146
2140
|
id: 'fit-one',
|
|
@@ -2273,6 +2267,17 @@ function buildRegistry(deps: {
|
|
|
2273
2267
|
[exportItem('export-artboard', 'Export this artboard…', 'artboard')],
|
|
2274
2268
|
],
|
|
2275
2269
|
world: [
|
|
2270
|
+
// issue-90 — right-click on empty canvas background (no artboard, no
|
|
2271
|
+
// element) had no "Add comment" entry at all. Anchors at the click
|
|
2272
|
+
// point, same as the comment tool's own floating-pin fallback.
|
|
2273
|
+
[
|
|
2274
|
+
{
|
|
2275
|
+
id: 'add-comment',
|
|
2276
|
+
label: 'Add comment',
|
|
2277
|
+
shortcut: 'C',
|
|
2278
|
+
onSelect: postComposeForTarget,
|
|
2279
|
+
},
|
|
2280
|
+
],
|
|
2276
2281
|
[fitItem, resetItem],
|
|
2277
2282
|
[
|
|
2278
2283
|
exportItem('export-canvas', 'Export canvas as separate…', 'canvas-as-separate'),
|
|
@@ -2320,16 +2325,6 @@ function filterRegistryForReadOnly(registry: ContextRegistry): ContextRegistry {
|
|
|
2320
2325
|
return out;
|
|
2321
2326
|
}
|
|
2322
2327
|
|
|
2323
|
-
function boundsOf(el: HTMLElement) {
|
|
2324
|
-
const r = el.getBoundingClientRect();
|
|
2325
|
-
return {
|
|
2326
|
-
x: Math.round(r.left),
|
|
2327
|
-
y: Math.round(r.top),
|
|
2328
|
-
w: Math.round(r.width),
|
|
2329
|
-
h: Math.round(r.height),
|
|
2330
|
-
};
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
2328
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2334
2329
|
// Task A1 (feature-element-editing-robustness) — camera-aware reveal.
|
|
2335
2330
|
//
|