@1agh/maude 0.58.3 → 0.59.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/annotations-layer.tsx +49 -15
- package/apps/studio/bin/_import-asset.mjs +18 -0
- package/apps/studio/bin/_import-figma.mjs +868 -214
- package/apps/studio/bin/_perf-probe-safari.mjs +332 -0
- package/apps/studio/bin/_perf-probe.mjs +228 -0
- package/apps/studio/bin/_perf-shared.mjs +345 -0
- package/apps/studio/bin/_video-playwright.mjs +17 -4
- package/apps/studio/bin/import-figma.sh +10 -1
- package/apps/studio/bin/perf.sh +228 -0
- package/apps/studio/bin/smoke.sh +49 -5
- package/apps/studio/canvas-lib.tsx +148 -6
- package/apps/studio/client/app.jsx +152 -37
- package/apps/studio/client/panels/SyncPanel.jsx +229 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +29 -1
- package/apps/studio/client/panels/timeline-comp-target.js +101 -0
- package/apps/studio/client/styles/3-shell-maude.css +30 -0
- package/apps/studio/client/styles/4-components.css +4 -4
- package/apps/studio/dist/client.bundle.js +772 -772
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/exporters/video-encode-lib.ts +8 -5
- package/apps/studio/exporters/video.ts +10 -0
- package/apps/studio/figma/assets.test.ts +92 -0
- package/apps/studio/figma/assets.ts +63 -9
- package/apps/studio/figma/codegen-client.test.ts +276 -0
- package/apps/studio/figma/codegen-client.ts +509 -0
- package/apps/studio/figma/codegen-fonts.test.ts +103 -0
- package/apps/studio/figma/codegen-fonts.ts +195 -0
- package/apps/studio/figma/codegen-values.test.ts +179 -0
- package/apps/studio/figma/codegen-values.ts +270 -0
- package/apps/studio/figma/endpoints.ts +73 -0
- package/apps/studio/figma/fig-decode.test.ts +702 -0
- package/apps/studio/figma/fig-decode.ts +617 -0
- package/apps/studio/figma/fig-kiwi.ts +410 -0
- package/apps/studio/figma/fig-zip.ts +270 -0
- package/apps/studio/figma/from-codegen.test.ts +408 -0
- package/apps/studio/figma/from-codegen.ts +1103 -0
- package/apps/studio/figma/sanitize.test.ts +69 -0
- package/apps/studio/figma/sanitize.ts +139 -47
- package/apps/studio/figma/tailwind-map.test.ts +142 -0
- package/apps/studio/figma/tailwind-map.ts +545 -0
- package/apps/studio/figma/to-render.ts +25 -3
- package/apps/studio/figma/types.ts +6 -1
- package/apps/studio/http.ts +47 -0
- package/apps/studio/sync/asset-push.ts +346 -38
- package/apps/studio/sync/connection-state.ts +71 -3
- package/apps/studio/sync/index.ts +10 -1
- package/apps/studio/sync/status.ts +18 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +4 -0
- package/apps/studio/test/figma-explode.test.ts +438 -0
- package/apps/studio/test/fixtures/perf-canvas.mjs +201 -0
- package/apps/studio/test/import-figma.test.ts +192 -4
- package/apps/studio/test/sync-asset-push.test.ts +490 -47
- package/apps/studio/test/sync-connection-state.test.ts +66 -0
- package/apps/studio/test/sync-panel-surface.test.ts +90 -0
- package/apps/studio/test/sync-status.test.ts +28 -0
- package/apps/studio/test/timeline-comp-target.test.ts +139 -0
- package/apps/studio/test/video-comp.test.ts +81 -1
- package/apps/studio/test/video-encode-lib.test.ts +63 -0
- package/apps/studio/use-artboard-drag.tsx +37 -3
- package/apps/studio/video-comp.tsx +51 -0
- package/apps/studio/whats-new.json +71 -0
- package/cli/commands/design.mjs +7 -0
- package/cli/commands/kg.mjs +8 -1
- package/cli/commands/kg.test.mjs +24 -0
- package/cli/lib/figma-codegen-reachability.test.mjs +104 -0
- package/cli/lib/figma-import-controls.test.mjs +70 -0
- package/package.json +8 -8
|
@@ -30,7 +30,9 @@ import {
|
|
|
30
30
|
ImportFigmaError,
|
|
31
31
|
importBoard,
|
|
32
32
|
importFrames,
|
|
33
|
+
importPages,
|
|
33
34
|
importTokens,
|
|
35
|
+
withSansFallback,
|
|
34
36
|
} from '../bin/_import-figma.mjs';
|
|
35
37
|
|
|
36
38
|
const TOKEN = 'figd_VERBCANARY_0123456789abcdef';
|
|
@@ -107,6 +109,140 @@ afterEach(() => {
|
|
|
107
109
|
delete process.env.MAUDE_GEN_KEYS_PATH;
|
|
108
110
|
});
|
|
109
111
|
|
|
112
|
+
describe('a rendered SVG never falls back to a SERIF (live-migration report)', () => {
|
|
113
|
+
// Measured on the StudyFi cover page: Figma renders text as
|
|
114
|
+
// `font-family="Inter"` and nothing else. An SVG behind `<img src>` renders
|
|
115
|
+
// in an isolated document — the page CSS, its @font-face rules and the DS
|
|
116
|
+
// webfonts do not reach inside — so an uninstalled family lands on the
|
|
117
|
+
// browser default, which is a serif. A sans-serif product design arrived in
|
|
118
|
+
// Times, and every count-based check called it a success.
|
|
119
|
+
test('a bare family gains a generic sans fallback', () => {
|
|
120
|
+
expect(withSansFallback('<text font-family="Inter">x</text>')).toContain(
|
|
121
|
+
'font-family="Inter, sans-serif"'
|
|
122
|
+
);
|
|
123
|
+
expect(withSansFallback('<text style="font-family: SF Pro Text">x</text>')).toContain(
|
|
124
|
+
'font-family:SF Pro Text, sans-serif'
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('a stack that already ends in a generic is left alone', () => {
|
|
129
|
+
for (const already of [
|
|
130
|
+
'<text font-family="Inter, sans-serif">x</text>',
|
|
131
|
+
'<text font-family="Georgia, serif">x</text>',
|
|
132
|
+
'<text font-family="ui-monospace, monospace">x</text>',
|
|
133
|
+
'<text font-family="system-ui">x</text>',
|
|
134
|
+
]) {
|
|
135
|
+
expect(withSansFallback(already)).toBe(already);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('it is a FALLBACK, not a substitution — the requested family still wins', () => {
|
|
140
|
+
const out = withSansFallback('<text font-family="Hanken Grotesk">x</text>');
|
|
141
|
+
expect(out.indexOf('Hanken Grotesk')).toBeLessThan(out.indexOf('sans-serif'));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('it rewrites nothing but font-family', () => {
|
|
145
|
+
const svg = '<svg><rect fill="#fff"/><text font-size="12" font-family="Inter">hi</text></svg>';
|
|
146
|
+
const out = withSansFallback(svg);
|
|
147
|
+
expect(out).toContain('fill="#fff"');
|
|
148
|
+
expect(out).toContain('font-size="12"');
|
|
149
|
+
expect(out).toContain('>hi<');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('an unterminated / oversized attribute cannot run away', () => {
|
|
153
|
+
const huge = `<text font-family="${'A'.repeat(500)}">x</text>`;
|
|
154
|
+
expect(withSansFallback(huge)).toBe(huge);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('--pages contains a page failure instead of losing the rest of the file', () => {
|
|
159
|
+
// Measured on the first live migration: a fault entering page 4 of 6 cost
|
|
160
|
+
// pages 4, 5 and 6 — twice in a row — because the loop caught `too_large`,
|
|
161
|
+
// empty pages and comment failures but NOT a page fetch failure. There is no
|
|
162
|
+
// resume, so the retry re-fetched and re-rendered the three that had already
|
|
163
|
+
// succeeded.
|
|
164
|
+
const PAGES = {
|
|
165
|
+
document: {
|
|
166
|
+
id: '0:0',
|
|
167
|
+
type: 'DOCUMENT',
|
|
168
|
+
children: [
|
|
169
|
+
{ id: '1:1', name: 'One', type: 'CANVAS' },
|
|
170
|
+
{ id: '2:2', name: 'Two', type: 'CANVAS' },
|
|
171
|
+
{ id: '3:3', name: 'Three', type: 'CANVAS' },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
const PAGE_BODY = (id: string) => ({
|
|
176
|
+
nodes: {
|
|
177
|
+
[id]: {
|
|
178
|
+
document: {
|
|
179
|
+
id,
|
|
180
|
+
name: 'P',
|
|
181
|
+
type: 'CANVAS',
|
|
182
|
+
children: [
|
|
183
|
+
{
|
|
184
|
+
id: `${id}0`,
|
|
185
|
+
name: 'Frame',
|
|
186
|
+
type: 'FRAME',
|
|
187
|
+
visible: true,
|
|
188
|
+
absoluteBoundingBox: box(0, 0, 100, 100),
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/** Page two's document fetch drops the connection; one and three are fine. */
|
|
197
|
+
function stubWithPageTwoDown() {
|
|
198
|
+
globalThis.fetch = mock(async (input: unknown) => {
|
|
199
|
+
const url = String(input);
|
|
200
|
+
if (url.includes('/images'))
|
|
201
|
+
return new Response(JSON.stringify({ images: {} }), { status: 200 });
|
|
202
|
+
if (url.includes('/comments'))
|
|
203
|
+
return new Response(JSON.stringify({ comments: [] }), { status: 200 });
|
|
204
|
+
if (
|
|
205
|
+
url.includes('depth=1') ||
|
|
206
|
+
/\/files\/[^/]+\?/.test(url) ||
|
|
207
|
+
url.endsWith('/files/2H6a9YUgPAu0AGdEiwP895')
|
|
208
|
+
) {
|
|
209
|
+
return new Response(JSON.stringify(PAGES), { status: 200 });
|
|
210
|
+
}
|
|
211
|
+
const m = /ids=([^&]+)/.exec(url);
|
|
212
|
+
const id = m ? decodeURIComponent(m[1]) : '';
|
|
213
|
+
if (id.startsWith('2:2')) throw new TypeError('fetch failed');
|
|
214
|
+
if (id) return new Response(JSON.stringify(PAGE_BODY(id)), { status: 200 });
|
|
215
|
+
return new Response(JSON.stringify(PAGES), { status: 200 });
|
|
216
|
+
}) as unknown as typeof fetch;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
test('the pages either side of the fault still import, and the fault is REPORTED', async () => {
|
|
220
|
+
stubWithPageTwoDown();
|
|
221
|
+
const r = await importPages({
|
|
222
|
+
url: 'https://www.figma.com/design/2H6a9YUgPAu0AGdEiwP895/x',
|
|
223
|
+
root: sandbox,
|
|
224
|
+
folder: 'f',
|
|
225
|
+
});
|
|
226
|
+
// Page three is the one the old behaviour lost: it comes AFTER the fault.
|
|
227
|
+
expect(r.written.length).toBeGreaterThanOrEqual(2);
|
|
228
|
+
const failed = r.skipped.find((x: { page?: string }) => x.page === '2:2');
|
|
229
|
+
expect(failed).toBeDefined();
|
|
230
|
+
// Reported by node id + a code-owned reason, never document text (D10).
|
|
231
|
+
expect(String(failed?.why)).toMatch(/^(network|bad_response|failed \(\w+\))$/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('a MISCONFIGURED request stays fatal — a partial folder must not look complete', async () => {
|
|
235
|
+
stubWithPageTwoDown();
|
|
236
|
+
await expect(
|
|
237
|
+
importPages({
|
|
238
|
+
url: 'https://www.figma.com/design/2H6a9YUgPAu0AGdEiwP895/x',
|
|
239
|
+
root: sandbox,
|
|
240
|
+
folder: 'NOT A SLUG',
|
|
241
|
+
})
|
|
242
|
+
).rejects.toBeInstanceOf(ImportFigmaError);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
110
246
|
describe('--board writes a sanitized annotation layer', () => {
|
|
111
247
|
test('lands at a code-computed slug under the design root', async () => {
|
|
112
248
|
const result = await importBoard({ url: BOARD_URL, root: sandbox });
|
|
@@ -139,6 +275,46 @@ describe('--board writes a sanitized annotation layer', () => {
|
|
|
139
275
|
expect(svg).not.toContain('you cannot see me');
|
|
140
276
|
});
|
|
141
277
|
|
|
278
|
+
// A FigJam board is not a screen, and framing it as one was wrong twice: the
|
|
279
|
+
// artboard drew screen chrome around whiteboard content, and it took the DS
|
|
280
|
+
// surface colour — which paints a white board near-black on a dark-default
|
|
281
|
+
// design system. Reported from a live migration into `studyfi-design`.
|
|
282
|
+
test('the host canvas has NO artboard — the board is not a screen', async () => {
|
|
283
|
+
const result = await importBoard({ url: BOARD_URL, root: sandbox });
|
|
284
|
+
const tsx = readFileSync(join(sandbox, '.design', result.canvas as string), 'utf8');
|
|
285
|
+
expect(tsx).not.toContain('<DCArtboard');
|
|
286
|
+
expect(tsx).not.toContain('DCArtboard,');
|
|
287
|
+
expect(tsx).toContain('<DesignCanvas />');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('the board gets an OPAQUE light paper — a section is only a 6% tint', async () => {
|
|
291
|
+
// `annotations-model.ts` paints a section at a hardcoded fill-opacity of
|
|
292
|
+
// 0.06, so it cannot be the ground: white-at-6% over a dark-default DS is
|
|
293
|
+
// still dark. Measured on the first migration into `studyfi-design`.
|
|
294
|
+
const result = await importBoard({ url: BOARD_URL, root: sandbox });
|
|
295
|
+
const svg = readFileSync(result.path as string, 'utf8');
|
|
296
|
+
expect(svg).toContain('data-id="figma-board-paper"');
|
|
297
|
+
expect(svg.toLowerCase()).toContain('fill="#ffffff"');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('the region is a labelled SECTION, on the annotation layer', async () => {
|
|
301
|
+
const result = await importBoard({ url: BOARD_URL, root: sandbox });
|
|
302
|
+
const svg = readFileSync(result.path as string, 'utf8');
|
|
303
|
+
expect(svg).toContain('data-tool="section"');
|
|
304
|
+
expect(svg).toContain('data-id="figma-board-region"');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('paint order is paper -> region -> content; either one later would veil the board', async () => {
|
|
308
|
+
const result = await importBoard({ url: BOARD_URL, root: sandbox });
|
|
309
|
+
const svg = readFileSync(result.path as string, 'utf8');
|
|
310
|
+
const paper = svg.indexOf('data-id="figma-board-paper"');
|
|
311
|
+
const region = svg.indexOf('data-id="figma-board-region"');
|
|
312
|
+
const firstContent = svg.search(/data-tool="(sticky|image|text|ellipse|arrow)"/);
|
|
313
|
+
expect(paper).toBeGreaterThan(-1);
|
|
314
|
+
expect(paper).toBeLessThan(region);
|
|
315
|
+
if (firstContent > -1) expect(region).toBeLessThan(firstContent);
|
|
316
|
+
});
|
|
317
|
+
|
|
142
318
|
test('an explicit --slug is honoured when it is a valid slug', async () => {
|
|
143
319
|
const result = await importBoard({ url: BOARD_URL, root: sandbox, slug: 'retro-q3' });
|
|
144
320
|
// `--slug` names the CANVAS; the annotation layer follows from it.
|
|
@@ -450,11 +626,23 @@ describe('the asset pipeline is actually WIRED into --frames', () => {
|
|
|
450
626
|
|
|
451
627
|
const r = await importFrames({ url: URL_D, root: sandbox });
|
|
452
628
|
const imageCalls = seen.filter((u) => u.includes('/v1/images/'));
|
|
629
|
+
const byFormat = (f: string) =>
|
|
630
|
+
imageCalls.filter((u) => new URL(u).searchParams.get('format') === f);
|
|
631
|
+
|
|
453
632
|
// ONE call for the whole cluster — the collapse is what keeps this at 1
|
|
454
|
-
// instead of 2 (or, on a real logo, 14).
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
633
|
+
// instead of 2 (or, on a real logo, 14). Asserted PER FORMAT, because this
|
|
634
|
+
// stub returns `{ '2:1': null }`, i.e. Figma DECLINING to render, which the
|
|
635
|
+
// later SVG→PNG degradation path answers with a second, legitimate request.
|
|
636
|
+
// The original `imageCalls.length === 1` conflated "the collapse batched the
|
|
637
|
+
// cluster" with "nothing retried", and went red in main when the degradation
|
|
638
|
+
// landed — the same incomplete change that left `asset-degraded` outside the
|
|
639
|
+
// Disposition union (DDR-219 D9). The collapse claim is what this test owns.
|
|
640
|
+
expect(byFormat('svg')).toHaveLength(1);
|
|
641
|
+
expect(new URL(byFormat('svg')[0]).searchParams.get('ids')).toBe('2:1');
|
|
642
|
+
// The degradation is intended behaviour, so it is asserted rather than
|
|
643
|
+
// tolerated: one retry, for the same collapsed id, never one per leaf.
|
|
644
|
+
expect(byFormat('png')).toHaveLength(1);
|
|
645
|
+
expect(new URL(byFormat('png')[0]).searchParams.get('ids')).toBe('2:1');
|
|
458
646
|
expect(r.pendingExports).toBe(1);
|
|
459
647
|
});
|
|
460
648
|
|