@1agh/maude 0.45.2 → 0.47.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.
- package/apps/studio/acp/bootstrap-brief.ts +8 -0
- package/apps/studio/acp/bridge.ts +151 -1
- package/apps/studio/annotations-layer.tsx +42 -0
- package/apps/studio/annotations-snap.ts +50 -10
- package/apps/studio/api.ts +760 -15
- package/apps/studio/artboard-guides-overlay.tsx +270 -0
- package/apps/studio/bin/_agent-browser-safe-config.json +1 -0
- package/apps/studio/bin/_agent-browser-safe.mjs +228 -0
- package/apps/studio/bin/_agent-browser-safe.test.mjs +165 -0
- package/apps/studio/bin/_curl-local.mjs +349 -0
- package/apps/studio/bin/_curl-local.test.mjs +280 -0
- package/apps/studio/bin/_pdf-playwright.mjs +35 -4
- package/apps/studio/bin/_png-playwright.mjs +38 -4
- package/apps/studio/bin/_pw-launch.mjs +52 -0
- package/apps/studio/bin/_pw-launch.test.mjs +90 -0
- package/apps/studio/bin/_smart-frames.mjs +419 -0
- package/apps/studio/bin/_smart-frames.test.mjs +140 -0
- package/apps/studio/bin/agent-browser-safe.sh +29 -0
- package/apps/studio/bin/curl-local.sh +28 -0
- package/apps/studio/bin/smart-frames.sh +30 -0
- package/apps/studio/canvas-cursors.ts +6 -0
- package/apps/studio/canvas-edit.ts +972 -6
- package/apps/studio/canvas-icons.tsx +13 -0
- package/apps/studio/canvas-lib.tsx +206 -11
- package/apps/studio/canvas-shell.tsx +809 -50
- package/apps/studio/client/app.jsx +1554 -206
- package/apps/studio/client/panels/ChatPanel.jsx +92 -3
- package/apps/studio/client/panels/SettingsPanel.jsx +211 -0
- package/apps/studio/client/panels/acp-capabilities.js +11 -0
- package/apps/studio/client/styles/3-shell-maude.css +39 -0
- package/apps/studio/client/styles/6-acp-chat.css +41 -0
- package/apps/studio/contextual-toolbar.tsx +5 -3
- package/apps/studio/dist/client.bundle.js +1627 -1627
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +20 -0
- package/apps/studio/export-dialog.tsx +138 -18
- package/apps/studio/exporters/pdf.ts +332 -18
- package/apps/studio/exporters/png.ts +45 -3
- package/apps/studio/footage/schema.ts +17 -1
- package/apps/studio/generation/gemma-models.ts +224 -0
- package/apps/studio/generation/prefs.ts +43 -0
- package/apps/studio/grid-track-handles.ts +179 -0
- package/apps/studio/handoff.ts +35 -0
- package/apps/studio/http.ts +331 -7
- package/apps/studio/input-router.tsx +73 -17
- package/apps/studio/print/marks.ts +113 -0
- package/apps/studio/print/units.ts +269 -0
- package/apps/studio/print-overlay-content.tsx +132 -0
- package/apps/studio/test/acp-mode-banner.test.ts +45 -0
- package/apps/studio/test/acp-session-allowed-tools.test.ts +187 -0
- package/apps/studio/test/annotations-snap.test.ts +56 -0
- package/apps/studio/test/artboard-guides-overlay.test.tsx +152 -0
- package/apps/studio/test/artboard-kinds.test.tsx +83 -0
- package/apps/studio/test/artboard-selection-attrs.test.ts +69 -0
- package/apps/studio/test/browse-posture.test.tsx +107 -0
- package/apps/studio/test/canvas-hide-chrome.test.ts +58 -0
- package/apps/studio/test/canvas-meta-api.test.ts +237 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +4 -0
- package/apps/studio/test/comment-mount.test.ts +2 -1
- package/apps/studio/test/component-map.test.ts +48 -0
- package/apps/studio/test/convert-to-absolute.test.ts +333 -0
- package/apps/studio/test/detach-component.test.ts +94 -0
- package/apps/studio/test/edit-scope-api.test.ts +8 -4
- package/apps/studio/test/element-structural-api.test.ts +74 -0
- package/apps/studio/test/element-structural-edit.test.ts +363 -0
- package/apps/studio/test/exporters/png.test.ts +49 -1
- package/apps/studio/test/grid-track-handles.test.ts +160 -0
- package/apps/studio/test/handoff.test.ts +48 -0
- package/apps/studio/test/input-router.test.ts +82 -8
- package/apps/studio/test/layers-synthetic-groups.test.ts +96 -0
- package/apps/studio/test/pdf-print-boxes.test.ts +326 -0
- package/apps/studio/test/print-marks.test.ts +113 -0
- package/apps/studio/test/print-units.test.ts +173 -0
- package/apps/studio/test/use-snap-guides.test.ts +81 -0
- package/apps/studio/test/use-tool-mode.test.tsx +10 -2
- package/apps/studio/tool-palette.tsx +3 -1
- package/apps/studio/use-canvas-media-drop.tsx +126 -0
- package/apps/studio/use-chrome-visibility.tsx +19 -0
- package/apps/studio/use-element-resize.tsx +24 -3
- package/apps/studio/use-grid-track-handles.tsx +364 -0
- package/apps/studio/use-keyboard-discipline.tsx +15 -0
- package/apps/studio/use-snap-guides.tsx +73 -5
- package/apps/studio/use-spacing-handles.tsx +9 -5
- package/apps/studio/use-tool-mode.tsx +30 -3
- package/apps/studio/web-overlay-content.tsx +52 -0
- package/apps/studio/whats-new.json +55 -0
- package/cli/commands/design.mjs +25 -1
- package/package.json +8 -8
- package/plugins/design/dependencies.json +35 -0
- package/plugins/design/templates/_shell.html +4 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// canvas-hide-chrome.test.ts — regression guard for RCA
|
|
2
|
+
// issue-pdf-print-export-marks-missing (Bug A). `_shell.html`'s
|
|
3
|
+
// `#canvas-hide-chrome` style block is the sanctioned "never leaks into an
|
|
4
|
+
// export" list every exporter relies on (`?hide-chrome=1` flips it from
|
|
5
|
+
// `media="not all"` to `media="all"` — see exporters/index.ts
|
|
6
|
+
// canvasShellUrl()). `.dc-artboard-guides` (the print bleed/trim/margin guide
|
|
7
|
+
// overlay, artboard-guides-overlay.tsx) was never added to this list when
|
|
8
|
+
// feature-2-print-artboards shipped, so it rendered straight into every PDF/
|
|
9
|
+
// PNG/SVG capture whenever a canvas had it toggled on. Grep-based, mirrors
|
|
10
|
+
// shell-importmap.test.ts's own style for the same file.
|
|
11
|
+
|
|
12
|
+
import { describe, expect, test } from 'bun:test';
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { DEV_SERVER_ROOT } from '../paths.ts';
|
|
17
|
+
|
|
18
|
+
const SHELL_HTML_PATH = join(
|
|
19
|
+
DEV_SERVER_ROOT,
|
|
20
|
+
'..',
|
|
21
|
+
'..',
|
|
22
|
+
'plugins',
|
|
23
|
+
'design',
|
|
24
|
+
'templates',
|
|
25
|
+
'_shell.html'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
describe('_shell.html #canvas-hide-chrome covers every overlay mount class', () => {
|
|
29
|
+
const shellHtml = readFileSync(SHELL_HTML_PATH, 'utf8');
|
|
30
|
+
const blockMatch = shellHtml.match(/<style id="canvas-hide-chrome"[^>]*>([\s\S]*?)<\/style>/);
|
|
31
|
+
|
|
32
|
+
test('the #canvas-hide-chrome style block exists', () => {
|
|
33
|
+
expect(blockMatch).not.toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const block = blockMatch?.[1] ?? '';
|
|
37
|
+
|
|
38
|
+
// Every editor-chrome overlay class that must never leak into a capture
|
|
39
|
+
// render. `.dc-artboard-guides` is the one this RCA adds; the rest already
|
|
40
|
+
// guard against the same class of regression for their own overlays.
|
|
41
|
+
const REQUIRED_SELECTORS = [
|
|
42
|
+
'.dc-mm',
|
|
43
|
+
'.dc-tool-palette',
|
|
44
|
+
'.dc-participants',
|
|
45
|
+
'.dc-cv-halo',
|
|
46
|
+
'.dc-snap-guide',
|
|
47
|
+
'.dc-annot-svg',
|
|
48
|
+
'.cm-layer',
|
|
49
|
+
'.dc-artboard-label',
|
|
50
|
+
'.dc-artboard-guides',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
for (const selector of REQUIRED_SELECTORS) {
|
|
54
|
+
test(`"${selector}" is hidden during capture`, () => {
|
|
55
|
+
expect(block.includes(selector)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
@@ -357,3 +357,240 @@ describe('/_api/canvas-meta — GET/PATCH (DDR-115 camera split)', () => {
|
|
|
357
357
|
}
|
|
358
358
|
});
|
|
359
359
|
});
|
|
360
|
+
|
|
361
|
+
// feature-1-artboard-kinds-foundation, T6 — per-user overlay-visibility lane.
|
|
362
|
+
// Same camera-split contract as `viewport` above: `overlays` lives ONLY in the
|
|
363
|
+
// gitignored per-machine view file, never the versioned `.meta.json`.
|
|
364
|
+
describe('/_api/canvas-meta — overlays lane (T6)', () => {
|
|
365
|
+
test('PATCH overlays leaves meta byte-unchanged + writes the view file', async () => {
|
|
366
|
+
const { root, designRoot } = makeSandbox();
|
|
367
|
+
const port = nextPort();
|
|
368
|
+
const proc = await bootServer(root, port);
|
|
369
|
+
try {
|
|
370
|
+
mkdirSync(join(designRoot, 'ui'), { recursive: true });
|
|
371
|
+
const tsxAbs = join(designRoot, 'ui', 'Overlays.tsx');
|
|
372
|
+
writeFileSync(tsxAbs, 'export default function O(){return <main/>}\n');
|
|
373
|
+
const metaAbs = tsxAbs.replace(/\.tsx$/, '.meta.json');
|
|
374
|
+
writeFileSync(metaAbs, JSON.stringify({ title: 'Overlays', sections: [] }));
|
|
375
|
+
const before = readFileSync(metaAbs, 'utf8');
|
|
376
|
+
const file = repoRel(designRoot, tsxAbs);
|
|
377
|
+
|
|
378
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas-meta`, {
|
|
379
|
+
method: 'PATCH',
|
|
380
|
+
headers: { 'content-type': 'application/json' },
|
|
381
|
+
body: JSON.stringify({ file, patch: { overlays: { guides: true } } }),
|
|
382
|
+
});
|
|
383
|
+
expect(r.status).toBe(200);
|
|
384
|
+
const merged = (await r.json()) as MetaShape;
|
|
385
|
+
expect(merged.overlays).toEqual({ guides: true });
|
|
386
|
+
|
|
387
|
+
// KEYSTONE — versioned meta is byte-identical; no last_modified bump.
|
|
388
|
+
expect(readFileSync(metaAbs, 'utf8')).toBe(before);
|
|
389
|
+
|
|
390
|
+
const vAbs = viewPath(designRoot, 'ui-overlays');
|
|
391
|
+
expect(existsSync(vAbs)).toBe(true);
|
|
392
|
+
const view = JSON.parse(readFileSync(vAbs, 'utf8'));
|
|
393
|
+
expect(view.overlays).toEqual({ guides: true });
|
|
394
|
+
} finally {
|
|
395
|
+
await killProc(proc);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('absent overlays key resolves to hidden (T6 Gotcha — old view files default off)', async () => {
|
|
400
|
+
const { root, designRoot } = makeSandbox();
|
|
401
|
+
const port = nextPort();
|
|
402
|
+
const proc = await bootServer(root, port);
|
|
403
|
+
try {
|
|
404
|
+
mkdirSync(join(designRoot, 'ui'), { recursive: true });
|
|
405
|
+
const tsxAbs = join(designRoot, 'ui', 'NoOverlays.tsx');
|
|
406
|
+
writeFileSync(tsxAbs, 'export default function N(){return <main/>}\n');
|
|
407
|
+
writeFileSync(tsxAbs.replace(/\.tsx$/, '.meta.json'), JSON.stringify({ title: 'N' }));
|
|
408
|
+
const file = repoRel(designRoot, tsxAbs);
|
|
409
|
+
|
|
410
|
+
const r = await fetch(
|
|
411
|
+
`http://localhost:${port}/_api/canvas-meta?file=${encodeURIComponent(file)}`
|
|
412
|
+
);
|
|
413
|
+
const m = (await r.json()) as MetaShape;
|
|
414
|
+
expect(m.overlays).toBeUndefined();
|
|
415
|
+
} finally {
|
|
416
|
+
await killProc(proc);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('PATCH overlays merges with an existing viewport in the SAME view file', async () => {
|
|
421
|
+
const { root, designRoot } = makeSandbox();
|
|
422
|
+
const port = nextPort();
|
|
423
|
+
const proc = await bootServer(root, port);
|
|
424
|
+
try {
|
|
425
|
+
mkdirSync(join(designRoot, 'ui'), { recursive: true });
|
|
426
|
+
const tsxAbs = join(designRoot, 'ui', 'Both.tsx');
|
|
427
|
+
writeFileSync(tsxAbs, 'export default function B(){return <main/>}\n');
|
|
428
|
+
writeFileSync(tsxAbs.replace(/\.tsx$/, '.meta.json'), JSON.stringify({ title: 'B' }));
|
|
429
|
+
const file = repoRel(designRoot, tsxAbs);
|
|
430
|
+
|
|
431
|
+
await fetch(`http://localhost:${port}/_api/canvas-meta`, {
|
|
432
|
+
method: 'PATCH',
|
|
433
|
+
headers: { 'content-type': 'application/json' },
|
|
434
|
+
body: JSON.stringify({ file, patch: { viewport: { x: 3, y: 4, zoom: 1 } } }),
|
|
435
|
+
});
|
|
436
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas-meta`, {
|
|
437
|
+
method: 'PATCH',
|
|
438
|
+
headers: { 'content-type': 'application/json' },
|
|
439
|
+
body: JSON.stringify({ file, patch: { overlays: { guides: true } } }),
|
|
440
|
+
});
|
|
441
|
+
const merged = (await r.json()) as MetaShape;
|
|
442
|
+
// Both lanes present — writing overlays didn't clobber the viewport.
|
|
443
|
+
expect(merged.viewport).toEqual({ x: 3, y: 4, zoom: 1 });
|
|
444
|
+
expect(merged.overlays).toEqual({ guides: true });
|
|
445
|
+
|
|
446
|
+
const view = JSON.parse(readFileSync(viewPath(designRoot, 'ui-both'), 'utf8'));
|
|
447
|
+
expect(view.viewport).toEqual({ x: 3, y: 4, zoom: 1 });
|
|
448
|
+
expect(view.overlays).toEqual({ guides: true });
|
|
449
|
+
} finally {
|
|
450
|
+
await killProc(proc);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test('PATCH overlays shallow-merges keys — a later patch keeps an earlier key', async () => {
|
|
455
|
+
const { root, designRoot } = makeSandbox();
|
|
456
|
+
const port = nextPort();
|
|
457
|
+
const proc = await bootServer(root, port);
|
|
458
|
+
try {
|
|
459
|
+
mkdirSync(join(designRoot, 'ui'), { recursive: true });
|
|
460
|
+
const tsxAbs = join(designRoot, 'ui', 'Merge2.tsx');
|
|
461
|
+
writeFileSync(tsxAbs, 'export default function M(){return <main/>}\n');
|
|
462
|
+
writeFileSync(tsxAbs.replace(/\.tsx$/, '.meta.json'), JSON.stringify({ title: 'M' }));
|
|
463
|
+
const file = repoRel(designRoot, tsxAbs);
|
|
464
|
+
|
|
465
|
+
await fetch(`http://localhost:${port}/_api/canvas-meta`, {
|
|
466
|
+
method: 'PATCH',
|
|
467
|
+
headers: { 'content-type': 'application/json' },
|
|
468
|
+
body: JSON.stringify({ file, patch: { overlays: { guides: true } } }),
|
|
469
|
+
});
|
|
470
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas-meta`, {
|
|
471
|
+
method: 'PATCH',
|
|
472
|
+
headers: { 'content-type': 'application/json' },
|
|
473
|
+
// A downstream plan (print) toggling ITS OWN key shouldn't erase `guides`.
|
|
474
|
+
body: JSON.stringify({ file, patch: { overlays: { bleed: true } } }),
|
|
475
|
+
});
|
|
476
|
+
const merged = (await r.json()) as MetaShape;
|
|
477
|
+
expect(merged.overlays).toEqual({ guides: true, bleed: true });
|
|
478
|
+
} finally {
|
|
479
|
+
await killProc(proc);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('PATCH ignores a malformed overlays bag (non-boolean value) — silent no-op', async () => {
|
|
484
|
+
const { root, designRoot } = makeSandbox();
|
|
485
|
+
const port = nextPort();
|
|
486
|
+
const proc = await bootServer(root, port);
|
|
487
|
+
try {
|
|
488
|
+
mkdirSync(join(designRoot, 'ui'), { recursive: true });
|
|
489
|
+
const tsxAbs = join(designRoot, 'ui', 'Malformed.tsx');
|
|
490
|
+
writeFileSync(tsxAbs, 'export default function X(){return <main/>}\n');
|
|
491
|
+
writeFileSync(tsxAbs.replace(/\.tsx$/, '.meta.json'), JSON.stringify({ title: 'X' }));
|
|
492
|
+
const file = repoRel(designRoot, tsxAbs);
|
|
493
|
+
|
|
494
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas-meta`, {
|
|
495
|
+
method: 'PATCH',
|
|
496
|
+
headers: { 'content-type': 'application/json' },
|
|
497
|
+
body: JSON.stringify({ file, patch: { overlays: { guides: 'yes' } } }),
|
|
498
|
+
});
|
|
499
|
+
expect(r.status).toBe(200);
|
|
500
|
+
const merged = (await r.json()) as MetaShape;
|
|
501
|
+
expect(merged.overlays).toBeUndefined();
|
|
502
|
+
expect(existsSync(viewPath(designRoot, 'ui-malformed'))).toBe(false);
|
|
503
|
+
} finally {
|
|
504
|
+
await killProc(proc);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('PATCH overlays on a non-existent canvas is refused (404), mints no file', async () => {
|
|
509
|
+
const { root, designRoot } = makeSandbox();
|
|
510
|
+
const port = nextPort();
|
|
511
|
+
const proc = await bootServer(root, port);
|
|
512
|
+
try {
|
|
513
|
+
mkdirSync(join(designRoot, 'ui'), { recursive: true });
|
|
514
|
+
const file = '.design/ui/GhostOverlays.tsx';
|
|
515
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas-meta`, {
|
|
516
|
+
method: 'PATCH',
|
|
517
|
+
headers: { 'content-type': 'application/json' },
|
|
518
|
+
body: JSON.stringify({ file, patch: { overlays: { guides: true } } }),
|
|
519
|
+
});
|
|
520
|
+
expect(r.status).toBe(404);
|
|
521
|
+
expect(existsSync(viewPath(designRoot, 'ui-ghostoverlays'))).toBe(false);
|
|
522
|
+
} finally {
|
|
523
|
+
await killProc(proc);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// feature-4 T7b — `locked` (per-user locked layer keys) is the third view-file
|
|
529
|
+
// lane: same split contract as viewport/overlays (never the versioned meta),
|
|
530
|
+
// REPLACE semantics (the client sends the full set), bounded shape.
|
|
531
|
+
describe('/_api/canvas-meta — locked lane (feature-4 T7b)', () => {
|
|
532
|
+
test('PATCH locked leaves meta byte-unchanged + writes the view file; GET merges it back', async () => {
|
|
533
|
+
const { root, designRoot } = makeSandbox();
|
|
534
|
+
const port = nextPort();
|
|
535
|
+
const proc = await bootServer(root, port);
|
|
536
|
+
try {
|
|
537
|
+
mkdirSync(join(designRoot, 'ui'), { recursive: true });
|
|
538
|
+
const tsxAbs = join(designRoot, 'ui', 'Locked.tsx');
|
|
539
|
+
writeFileSync(tsxAbs, 'export default function L(){return <main/>}\n');
|
|
540
|
+
const metaAbs = tsxAbs.replace(/\.tsx$/, '.meta.json');
|
|
541
|
+
writeFileSync(metaAbs, JSON.stringify({ title: 'Locked', sections: [] }));
|
|
542
|
+
const before = readFileSync(metaAbs, 'utf8');
|
|
543
|
+
const file = repoRel(designRoot, tsxAbs);
|
|
544
|
+
|
|
545
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas-meta`, {
|
|
546
|
+
method: 'PATCH',
|
|
547
|
+
headers: { 'content-type': 'application/json' },
|
|
548
|
+
body: JSON.stringify({ file, patch: { locked: ['aabbccdd:0', 'deadbeef:2'] } }),
|
|
549
|
+
});
|
|
550
|
+
expect(r.status).toBe(200);
|
|
551
|
+
const merged = (await r.json()) as MetaShape;
|
|
552
|
+
expect(merged.locked).toEqual(['aabbccdd:0', 'deadbeef:2']);
|
|
553
|
+
// Versioned meta byte-identical (runtime state never lands there).
|
|
554
|
+
expect(readFileSync(metaAbs, 'utf8')).toBe(before);
|
|
555
|
+
const view = JSON.parse(readFileSync(viewPath(designRoot, 'ui-locked'), 'utf8'));
|
|
556
|
+
expect(view.locked).toEqual(['aabbccdd:0', 'deadbeef:2']);
|
|
557
|
+
|
|
558
|
+
// REPLACE semantics — a smaller set unlocks the dropped key.
|
|
559
|
+
await fetch(`http://localhost:${port}/_api/canvas-meta`, {
|
|
560
|
+
method: 'PATCH',
|
|
561
|
+
headers: { 'content-type': 'application/json' },
|
|
562
|
+
body: JSON.stringify({ file, patch: { locked: ['aabbccdd:0'] } }),
|
|
563
|
+
});
|
|
564
|
+
const g = await fetch(
|
|
565
|
+
`http://localhost:${port}/_api/canvas-meta?file=${encodeURIComponent(file)}`
|
|
566
|
+
);
|
|
567
|
+
expect(((await g.json()) as MetaShape).locked).toEqual(['aabbccdd:0']);
|
|
568
|
+
} finally {
|
|
569
|
+
await killProc(proc);
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test('malformed locked keys are a silent no-op (shape-gated)', async () => {
|
|
574
|
+
const { root, designRoot } = makeSandbox();
|
|
575
|
+
const port = nextPort();
|
|
576
|
+
const proc = await bootServer(root, port);
|
|
577
|
+
try {
|
|
578
|
+
mkdirSync(join(designRoot, 'ui'), { recursive: true });
|
|
579
|
+
const tsxAbs = join(designRoot, 'ui', 'BadLock.tsx');
|
|
580
|
+
writeFileSync(tsxAbs, 'export default function B(){return <main/>}\n');
|
|
581
|
+
writeFileSync(tsxAbs.replace(/\.tsx$/, '.meta.json'), JSON.stringify({ title: 'B' }));
|
|
582
|
+
const file = repoRel(designRoot, tsxAbs);
|
|
583
|
+
await fetch(`http://localhost:${port}/_api/canvas-meta`, {
|
|
584
|
+
method: 'PATCH',
|
|
585
|
+
headers: { 'content-type': 'application/json' },
|
|
586
|
+
body: JSON.stringify({ file, patch: { locked: ['../../etc/passwd', 'not-a-key'] } }),
|
|
587
|
+
});
|
|
588
|
+
const g = await fetch(
|
|
589
|
+
`http://localhost:${port}/_api/canvas-meta?file=${encodeURIComponent(file)}`
|
|
590
|
+
);
|
|
591
|
+
expect(((await g.json()) as MetaShape).locked).toBeUndefined();
|
|
592
|
+
} finally {
|
|
593
|
+
await killProc(proc);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
});
|
|
@@ -111,6 +111,10 @@ describe('canvas-origin gate — A1/A2 traversal + privilege containment', () =>
|
|
|
111
111
|
'/_api/insert-artboard',
|
|
112
112
|
'/_api/resize-artboard',
|
|
113
113
|
'/_api/delete-artboard',
|
|
114
|
+
// feature-3-web-artboards T3 — "Duplicate at width…" is a source-write,
|
|
115
|
+
// same MAIN-ORIGIN-ONLY posture as the other artboard structural ops
|
|
116
|
+
// right above.
|
|
117
|
+
'/_api/duplicate-artboard',
|
|
114
118
|
// Stage F1 — the AssetPicker's asset-listing GET is a shell (main-origin)
|
|
115
119
|
// concern; the untrusted canvas iframe must not enumerate project media.
|
|
116
120
|
'/_api/assets',
|
|
@@ -39,7 +39,8 @@ describe('comment-mount / MaybeToolProvider dedup', () => {
|
|
|
39
39
|
const html = renderToStaticMarkup(
|
|
40
40
|
createElement(MaybeToolProvider, null, createElement(ToolReader))
|
|
41
41
|
);
|
|
42
|
-
|
|
42
|
+
// feature-4 (browse/move split) — the default boot tool is `browse`.
|
|
43
|
+
expect(html).toContain('data-tool="browse"');
|
|
43
44
|
});
|
|
44
45
|
});
|
|
45
46
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// component-map — feature-4 T7a. The Layers panel's purple-instance source:
|
|
2
|
+
// `componentMapForCanvas` reports every element that renders through an
|
|
3
|
+
// INSTANTIATED component (`<Card/>` referenced as JSX in the file), keyed by
|
|
4
|
+
// cd-id. The top-level canvas component (exported/mounted, never referenced as
|
|
5
|
+
// JSX here) must NOT mark its elements as instances.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
|
|
9
|
+
import { componentMapForCanvas } from '../canvas-edit.ts';
|
|
10
|
+
import { transpileCanvasSource } from '../canvas-pipeline.ts';
|
|
11
|
+
|
|
12
|
+
const CANVAS = '/abs/Canvas.tsx';
|
|
13
|
+
|
|
14
|
+
function idsOf(source: string): Record<string, string> {
|
|
15
|
+
const { withIds } = transpileCanvasSource(CANVAS, source);
|
|
16
|
+
const out: Record<string, string> = {};
|
|
17
|
+
for (const m of withIds.matchAll(/<(\w+)([^>]*?)data-cd-id="([0-9a-f]{8})"/g)) {
|
|
18
|
+
if (!out[m[1] as string]) out[m[1] as string] = m[3] as string;
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('canvas-edit / componentMapForCanvas', () => {
|
|
24
|
+
test('elements of an instantiated component are instances; the canvas root is not', () => {
|
|
25
|
+
const src = `
|
|
26
|
+
function Card() { return <article className="card"><h2>Hi</h2></article>; }
|
|
27
|
+
function Demo() { return <div><Card /><Card /><p>plain</p></div>; }
|
|
28
|
+
`;
|
|
29
|
+
const ids = idsOf(src);
|
|
30
|
+
const map = componentMapForCanvas(CANVAS, src);
|
|
31
|
+
// Card's own elements → instances of Card (2 usages); article is the root.
|
|
32
|
+
expect(map[ids.article as string]).toEqual({ component: 'Card', root: true, usages: 2 });
|
|
33
|
+
expect(map[ids.h2 as string]).toEqual({ component: 'Card', root: false, usages: 2 });
|
|
34
|
+
// Demo (the canvas component, never referenced as JSX) → NOT in the map.
|
|
35
|
+
expect(map[ids.div as string]).toBeUndefined();
|
|
36
|
+
expect(map[ids.p as string]).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('no components → empty map', () => {
|
|
40
|
+
const src = `function Demo() { return <div><h1>T</h1></div>; }`;
|
|
41
|
+
const map = componentMapForCanvas(CANVAS, src);
|
|
42
|
+
expect(Object.keys(map)).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('unparsable source → empty map (no throw)', () => {
|
|
46
|
+
expect(componentMapForCanvas(CANVAS, 'function {{{')).toEqual({});
|
|
47
|
+
});
|
|
48
|
+
});
|