@1agh/maude 0.45.1 → 0.46.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/bridge.ts +628 -74
- package/apps/studio/acp/index.ts +172 -23
- package/apps/studio/acp/probe.ts +31 -7
- package/apps/studio/acp/transcript.ts +91 -5
- package/apps/studio/annotations-snap.ts +50 -10
- package/apps/studio/api.ts +344 -14
- package/apps/studio/artboard-guides-overlay.tsx +270 -0
- package/apps/studio/bin/_import-asset.mjs +35 -5
- 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/smart-frames.sh +30 -0
- package/apps/studio/canvas-edit.ts +302 -3
- package/apps/studio/canvas-lib.tsx +203 -11
- package/apps/studio/canvas-shell.tsx +167 -28
- package/apps/studio/client/app.jsx +678 -176
- package/apps/studio/client/panels/CapabilityBar.jsx +101 -0
- package/apps/studio/client/panels/ChatPanel.jsx +846 -133
- package/apps/studio/client/panels/ElicitationPrompt.jsx +429 -0
- package/apps/studio/client/panels/PermissionPrompt.jsx +119 -0
- package/apps/studio/client/panels/ReadinessList.jsx +17 -3
- package/apps/studio/client/panels/SettingsPanel.jsx +211 -0
- package/apps/studio/client/panels/ToolGroup.jsx +79 -0
- package/apps/studio/client/panels/acp-capabilities.js +82 -0
- package/apps/studio/client/panels/acp-elicitation.js +194 -0
- package/apps/studio/client/panels/acp-runtime.js +248 -9
- package/apps/studio/client/panels/acp-usage.js +65 -0
- package/apps/studio/client/panels/transcript-view.js +36 -0
- package/apps/studio/client/styles/6-acp-chat.css +689 -11
- package/apps/studio/dist/client.bundle.js +1738 -1736
- 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/generation/whisper-models.test.ts +28 -1
- package/apps/studio/generation/whisper-models.ts +22 -7
- package/apps/studio/http.ts +246 -9
- 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-bridge.test.ts +11 -6
- package/apps/studio/test/acp-capabilities.test.ts +123 -0
- package/apps/studio/test/acp-caps-bridge.test.ts +274 -0
- package/apps/studio/test/acp-elicitation-bridge.test.ts +475 -0
- package/apps/studio/test/acp-elicitation.test.ts +251 -0
- package/apps/studio/test/acp-mode-banner.test.ts +45 -0
- package/apps/studio/test/acp-permission-prompt.test.ts +77 -0
- package/apps/studio/test/acp-permission.test.ts +262 -0
- package/apps/studio/test/acp-session-allowed-tools.test.ts +91 -0
- package/apps/studio/test/acp-toolgroup.test.ts +76 -0
- package/apps/studio/test/acp-transcript-view.test.ts +71 -0
- package/apps/studio/test/acp-transcript.test.ts +75 -2
- package/apps/studio/test/acp-usage-bridge.test.ts +136 -0
- package/apps/studio/test/acp-usage.test.ts +143 -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/canvas-meta-api.test.ts +167 -0
- package/apps/studio/test/element-structural-edit.test.ts +250 -0
- package/apps/studio/test/exporters/png.test.ts +49 -1
- package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +158 -0
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +46 -0
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +42 -0
- package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +63 -0
- package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +51 -0
- package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +50 -0
- package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +56 -0
- package/apps/studio/test/import-asset.test.ts +31 -3
- package/apps/studio/test/pdf-print-boxes.test.ts +272 -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/use-chrome-visibility.tsx +19 -0
- package/apps/studio/use-element-resize.tsx +21 -2
- package/apps/studio/use-snap-guides.tsx +73 -5
- package/apps/studio/use-spacing-handles.tsx +9 -5
- package/apps/studio/whats-new.json +62 -0
- package/cli/commands/design.mjs +116 -3
- package/cli/lib/pkg-root.mjs +42 -10
- package/cli/lib/pkg-root.test.mjs +33 -1
- package/package.json +11 -9
- package/plugins/design/dependencies.json +35 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// artboard-guides-overlay — feature-1-artboard-kinds-foundation, T4.
|
|
2
|
+
// Registry contract + generic guides rendering. Pure renderToStaticMarkup —
|
|
3
|
+
// this component has no hooks/context, so (unlike DCArtboard's own overlays)
|
|
4
|
+
// it renders fully outside a live DesignCanvas.
|
|
5
|
+
|
|
6
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
7
|
+
|
|
8
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
__resetKindOverlayRegistryForTests,
|
|
12
|
+
ArtboardGuidesOverlay,
|
|
13
|
+
registerKindOverlay,
|
|
14
|
+
} from '../artboard-guides-overlay.tsx';
|
|
15
|
+
|
|
16
|
+
const RECT = { x: 10, y: 20, w: 400, h: 200 };
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
__resetKindOverlayRegistryForTests();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('ArtboardGuidesOverlay — generic guides', () => {
|
|
23
|
+
test('renders nothing with no guides and no registered kind content', () => {
|
|
24
|
+
const html = renderToStaticMarkup(
|
|
25
|
+
<ArtboardGuidesOverlay rect={RECT} kind="digital" visibility={{}} />
|
|
26
|
+
);
|
|
27
|
+
expect(html).toBe('');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('positions the overlay at the artboard world rect', () => {
|
|
31
|
+
const html = renderToStaticMarkup(
|
|
32
|
+
<ArtboardGuidesOverlay
|
|
33
|
+
rect={RECT}
|
|
34
|
+
kind="print"
|
|
35
|
+
guides={{ grid: { size: 40 } }}
|
|
36
|
+
visibility={{ guides: true }}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
expect(html).toMatch(/left:\s*10px/);
|
|
40
|
+
expect(html).toMatch(/top:\s*20px/);
|
|
41
|
+
expect(html).toMatch(/width:\s*400px/);
|
|
42
|
+
expect(html).toMatch(/height:\s*200px/);
|
|
43
|
+
expect(html).toContain('aria-hidden="true"');
|
|
44
|
+
expect(html).toContain('pointer-events:none');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('visibility.guides === false suppresses the generic guides', () => {
|
|
48
|
+
const html = renderToStaticMarkup(
|
|
49
|
+
<ArtboardGuidesOverlay
|
|
50
|
+
rect={RECT}
|
|
51
|
+
kind="print"
|
|
52
|
+
guides={{ grid: { size: 40 } }}
|
|
53
|
+
visibility={{ guides: false }}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
expect(html).toBe('');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('columns render as one band per column, respecting margin/gutter', () => {
|
|
60
|
+
const html = renderToStaticMarkup(
|
|
61
|
+
<ArtboardGuidesOverlay
|
|
62
|
+
rect={RECT}
|
|
63
|
+
kind="web"
|
|
64
|
+
guides={{ columns: { count: 4, gutter: 10, margin: 20 } }}
|
|
65
|
+
visibility={{ guides: true }}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
// usable = 400 - 2*20 = 360; (360 - 3*10) / 4 = 82.5px per column, 4 bands.
|
|
69
|
+
const bandCount = (html.match(/width:82\.5px/g) || []).length;
|
|
70
|
+
expect(bandCount).toBe(4);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Security review (defender + adversarial pass, /flow:done on 0e001da7) —
|
|
74
|
+
// columns/rows `count` had no cap, unlike the grid path: a malicious/
|
|
75
|
+
// poisoned canvas (guides is an authored JSX prop, part of the DDR-054
|
|
76
|
+
// untrusted-canvas surface) could set `columns.count` to a huge number and
|
|
77
|
+
// mount one <div> per band, freezing the viewer's tab. Regression-tests the
|
|
78
|
+
// MAX_BANDS_PER_AXIS clamp both independent reviews confirmed as the fix.
|
|
79
|
+
test('a pathologically large column count is clamped, never a runaway render', () => {
|
|
80
|
+
const html = renderToStaticMarkup(
|
|
81
|
+
<ArtboardGuidesOverlay
|
|
82
|
+
rect={RECT}
|
|
83
|
+
kind="web"
|
|
84
|
+
guides={{ columns: { count: 1_000_000_000, gutter: 0, margin: 0 } }}
|
|
85
|
+
visibility={{ guides: true }}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
const bandCount = (html.match(/position:absolute/g) || []).length - 1; // minus the wrapper div
|
|
89
|
+
expect(bandCount).toBeLessThanOrEqual(64);
|
|
90
|
+
expect(bandCount).toBeGreaterThan(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('a pathologically large row count is clamped the same way', () => {
|
|
94
|
+
const html = renderToStaticMarkup(
|
|
95
|
+
<ArtboardGuidesOverlay
|
|
96
|
+
rect={RECT}
|
|
97
|
+
kind="web"
|
|
98
|
+
guides={{ rows: { count: 999_999_999, gutter: 0, margin: 0 } }}
|
|
99
|
+
visibility={{ guides: true }}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
const bandCount = (html.match(/position:absolute/g) || []).length - 1;
|
|
103
|
+
expect(bandCount).toBeLessThanOrEqual(64);
|
|
104
|
+
expect(bandCount).toBeGreaterThan(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('grid lines are capped, never runaway for a degenerate small size', () => {
|
|
108
|
+
const html = renderToStaticMarkup(
|
|
109
|
+
<ArtboardGuidesOverlay
|
|
110
|
+
rect={{ x: 0, y: 0, w: 5000, h: 5000 }}
|
|
111
|
+
kind="digital"
|
|
112
|
+
guides={{ grid: { size: 2 } }}
|
|
113
|
+
visibility={{ guides: true }}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
const lineCount = (html.match(/width:1px/g) || []).length;
|
|
117
|
+
expect(lineCount).toBeLessThanOrEqual(400);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('ArtboardGuidesOverlay — per-kind registry', () => {
|
|
122
|
+
test('registered kind renderer is invoked with rect/kind/guides/visibility', () => {
|
|
123
|
+
let seen: unknown;
|
|
124
|
+
registerKindOverlay('print', (props) => {
|
|
125
|
+
seen = props;
|
|
126
|
+
return <div data-testid="print-bleed" />;
|
|
127
|
+
});
|
|
128
|
+
const html = renderToStaticMarkup(
|
|
129
|
+
<ArtboardGuidesOverlay rect={RECT} kind="print" visibility={{ bleed: true }} />
|
|
130
|
+
);
|
|
131
|
+
expect(html).toContain('data-testid="print-bleed"');
|
|
132
|
+
expect(seen).toMatchObject({ rect: RECT, kind: 'print', visibility: { bleed: true } });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('a renderer registered for one kind does not fire for another', () => {
|
|
136
|
+
registerKindOverlay('web', () => <div data-testid="breakpoint-band" />);
|
|
137
|
+
const html = renderToStaticMarkup(
|
|
138
|
+
<ArtboardGuidesOverlay rect={RECT} kind="print" visibility={{}} />
|
|
139
|
+
);
|
|
140
|
+
expect(html).not.toContain('breakpoint-band');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('re-registering the same kind replaces the previous renderer (HMR-safe)', () => {
|
|
144
|
+
registerKindOverlay('print', () => <div data-testid="v1" />);
|
|
145
|
+
registerKindOverlay('print', () => <div data-testid="v2" />);
|
|
146
|
+
const html = renderToStaticMarkup(
|
|
147
|
+
<ArtboardGuidesOverlay rect={RECT} kind="print" visibility={{}} />
|
|
148
|
+
);
|
|
149
|
+
expect(html).not.toContain('v1');
|
|
150
|
+
expect(html).toContain('v2');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// artboard-kinds — feature-1-artboard-kinds-foundation, T1/T2.
|
|
2
|
+
//
|
|
3
|
+
// DCArtboard rendered OUTSIDE a DesignCanvas (no WorldContext, no rect) takes
|
|
4
|
+
// the specimen/legacy early-return branch — it still computes + emits the
|
|
5
|
+
// resolved `data-dc-kind` attribute (readBackAttrs is built before the
|
|
6
|
+
// `!ctx || !rect` check), so that resolution logic is testable via
|
|
7
|
+
// renderToStaticMarkup without a live DOM/layout pass. The label-strip kind
|
|
8
|
+
// chip (T3) only renders in the full DesignCanvas layout path (like the
|
|
9
|
+
// video corner badge it sits beside) — covered by /design:smoke, not here
|
|
10
|
+
// (see use-canvas-activity.test.tsx's header comment for the same split).
|
|
11
|
+
|
|
12
|
+
import { describe, expect, test } from 'bun:test';
|
|
13
|
+
|
|
14
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
15
|
+
|
|
16
|
+
import { DCArtboard } from '../canvas-lib.tsx';
|
|
17
|
+
|
|
18
|
+
// Minimal displayName-only stand-in for VideoComp — subtreeHasVideoComp
|
|
19
|
+
// identity-matches OR falls back to `displayName === 'VideoComp'` (the same
|
|
20
|
+
// fallback minified builds rely on), so this exercises the fallback path
|
|
21
|
+
// without pulling in the real VideoComp's rendering/registration machinery.
|
|
22
|
+
function FakeVideoComp() {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
FakeVideoComp.displayName = 'VideoComp';
|
|
26
|
+
|
|
27
|
+
describe('DCArtboard kind resolution (data-dc-kind readback)', () => {
|
|
28
|
+
test('absent kind, no video subtree ⇒ digital', () => {
|
|
29
|
+
const html = renderToStaticMarkup(
|
|
30
|
+
<DCArtboard id="a" label="A" width={100} height={100}>
|
|
31
|
+
hi
|
|
32
|
+
</DCArtboard>
|
|
33
|
+
);
|
|
34
|
+
expect(html).toContain('data-dc-kind="digital"');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('explicit kind="print" is emitted verbatim', () => {
|
|
38
|
+
const html = renderToStaticMarkup(
|
|
39
|
+
<DCArtboard id="a" label="A" width={100} height={100} kind="print">
|
|
40
|
+
hi
|
|
41
|
+
</DCArtboard>
|
|
42
|
+
);
|
|
43
|
+
expect(html).toContain('data-dc-kind="print"');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('explicit kind="web" is emitted verbatim', () => {
|
|
47
|
+
const html = renderToStaticMarkup(
|
|
48
|
+
<DCArtboard id="a" label="A" width={100} height={100} kind="web">
|
|
49
|
+
hi
|
|
50
|
+
</DCArtboard>
|
|
51
|
+
);
|
|
52
|
+
expect(html).toContain('data-dc-kind="web"');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('absent kind + VideoComp subtree ⇒ video fallback (Design Decision 1)', () => {
|
|
56
|
+
const html = renderToStaticMarkup(
|
|
57
|
+
<DCArtboard id="a" label="A" width={100} height={100}>
|
|
58
|
+
<FakeVideoComp />
|
|
59
|
+
</DCArtboard>
|
|
60
|
+
);
|
|
61
|
+
expect(html).toContain('data-dc-kind="video"');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('explicit kind="digital" supersedes a VideoComp subtree', () => {
|
|
65
|
+
const html = renderToStaticMarkup(
|
|
66
|
+
<DCArtboard id="a" label="A" width={100} height={100} kind="digital">
|
|
67
|
+
<FakeVideoComp />
|
|
68
|
+
</DCArtboard>
|
|
69
|
+
);
|
|
70
|
+
expect(html).toContain('data-dc-kind="digital"');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('digital kind (default) carries no other resolved-kind attribute value', () => {
|
|
74
|
+
const html = renderToStaticMarkup(
|
|
75
|
+
<DCArtboard id="a" label="A" width={100} height={100}>
|
|
76
|
+
hi
|
|
77
|
+
</DCArtboard>
|
|
78
|
+
);
|
|
79
|
+
expect(html).not.toContain('data-dc-kind="print"');
|
|
80
|
+
expect(html).not.toContain('data-dc-kind="web"');
|
|
81
|
+
expect(html).not.toContain('data-dc-kind="video"');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Dogfood follow-up (print artboards) — whole-ARTBOARD selections must carry
|
|
2
|
+
// the DCArtboard readBack attrs (`data-dc-kind`, `data-dc-print`, `data-dc-
|
|
3
|
+
// fixed`, `data-dc-bg`, …) in `Selection.attrs`, because the Inspector's
|
|
4
|
+
// ArtboardKnobs panel pre-fills its Kind picker / paper preset / Hug toggle /
|
|
5
|
+
// Style fields from exactly these. ATTR_SKIP deliberately hides `data-dc-*`
|
|
6
|
+
// from the generic custom-attributes surface on ORDINARY elements — the bug
|
|
7
|
+
// was that the blanket skip also applied to the artboard element itself, so
|
|
8
|
+
// every readback silently fell back to its default ('digital', screen
|
|
9
|
+
// presets, Hug, empty Bg) no matter what the artboard actually was.
|
|
10
|
+
//
|
|
11
|
+
// Same happy-dom + hoverTargetToSelection harness as knob-props-authored.
|
|
12
|
+
|
|
13
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
14
|
+
import { GlobalRegistrator } from '@happy-dom/global-registrator';
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
GlobalRegistrator.register();
|
|
18
|
+
});
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
GlobalRegistrator.unregister();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
import { hoverTargetToSelection } from '../dom-selection.ts';
|
|
24
|
+
import type { HoverTarget } from '../input-router.tsx';
|
|
25
|
+
|
|
26
|
+
function artboardSelection(attrsHtml: string) {
|
|
27
|
+
document.body.innerHTML = `<article class="dc-artboard" data-dc-screen="ab-1" ${attrsHtml}><div>content</div></article>`;
|
|
28
|
+
const el = document.querySelector('[data-dc-screen]') as HTMLElement;
|
|
29
|
+
const target: HoverTarget = { el, cdId: null, artboardId: 'ab-1' };
|
|
30
|
+
return hoverTargetToSelection(target);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('whole-artboard selection — data-dc-* readback attrs', () => {
|
|
34
|
+
test('data-dc-kind + data-dc-print reach Selection.attrs (the ArtboardKnobs payload)', () => {
|
|
35
|
+
const sel = artboardSelection(
|
|
36
|
+
'data-dc-kind="print" data-dc-print=\'{"paper":"a4","bleedMm":3}\''
|
|
37
|
+
);
|
|
38
|
+
expect(sel.attrs?.['data-dc-kind']).toBe('print');
|
|
39
|
+
expect(sel.attrs?.['data-dc-print']).toBe('{"paper":"a4","bleedMm":3}');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('the other readback attrs come through too (fixed/bg/padding/layout/gap)', () => {
|
|
43
|
+
const sel = artboardSelection(
|
|
44
|
+
'data-dc-kind="digital" data-dc-fixed="true" data-dc-bg="var(--bg-1)" data-dc-padding="16" data-dc-layout="flex-col" data-dc-gap="8"'
|
|
45
|
+
);
|
|
46
|
+
expect(sel.attrs?.['data-dc-fixed']).toBe('true');
|
|
47
|
+
expect(sel.attrs?.['data-dc-bg']).toBe('var(--bg-1)');
|
|
48
|
+
expect(sel.attrs?.['data-dc-padding']).toBe('16');
|
|
49
|
+
expect(sel.attrs?.['data-dc-layout']).toBe('flex-col');
|
|
50
|
+
expect(sel.attrs?.['data-dc-gap']).toBe('8');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('an ORDINARY element (no data-dc-screen) keeps the data-dc-* filter', () => {
|
|
54
|
+
document.body.innerHTML =
|
|
55
|
+
'<div data-cd-id="bbbbbbbb" data-dc-kind="print" data-custom="yes">x</div>';
|
|
56
|
+
const el = document.querySelector('[data-cd-id]') as HTMLElement;
|
|
57
|
+
const target: HoverTarget = { el, cdId: 'bbbbbbbb', artboardId: null };
|
|
58
|
+
const sel = hoverTargetToSelection(target);
|
|
59
|
+
// Engine plumbing stays hidden from the generic custom-attributes panel…
|
|
60
|
+
expect(sel.attrs?.['data-dc-kind']).toBeUndefined();
|
|
61
|
+
// …while genuinely custom attributes still surface.
|
|
62
|
+
expect(sel.attrs?.['data-custom']).toBe('yes');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('data-dc-screen itself is included for the artboard (harmless, self-describing)', () => {
|
|
66
|
+
const sel = artboardSelection('data-dc-kind="print"');
|
|
67
|
+
expect(sel.attrs?.['data-dc-screen']).toBe('ab-1');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -357,3 +357,170 @@ 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
|
+
});
|