@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.
Files changed (67) hide show
  1. package/apps/studio/annotations-layer.tsx +49 -15
  2. package/apps/studio/bin/_import-asset.mjs +18 -0
  3. package/apps/studio/bin/_import-figma.mjs +868 -214
  4. package/apps/studio/bin/_perf-probe-safari.mjs +332 -0
  5. package/apps/studio/bin/_perf-probe.mjs +228 -0
  6. package/apps/studio/bin/_perf-shared.mjs +345 -0
  7. package/apps/studio/bin/_video-playwright.mjs +17 -4
  8. package/apps/studio/bin/import-figma.sh +10 -1
  9. package/apps/studio/bin/perf.sh +228 -0
  10. package/apps/studio/bin/smoke.sh +49 -5
  11. package/apps/studio/canvas-lib.tsx +148 -6
  12. package/apps/studio/client/app.jsx +152 -37
  13. package/apps/studio/client/panels/SyncPanel.jsx +229 -0
  14. package/apps/studio/client/panels/TimelinePanel.jsx +29 -1
  15. package/apps/studio/client/panels/timeline-comp-target.js +101 -0
  16. package/apps/studio/client/styles/3-shell-maude.css +30 -0
  17. package/apps/studio/client/styles/4-components.css +4 -4
  18. package/apps/studio/dist/client.bundle.js +772 -772
  19. package/apps/studio/dist/styles.css +1 -1
  20. package/apps/studio/exporters/video-encode-lib.ts +8 -5
  21. package/apps/studio/exporters/video.ts +10 -0
  22. package/apps/studio/figma/assets.test.ts +92 -0
  23. package/apps/studio/figma/assets.ts +63 -9
  24. package/apps/studio/figma/codegen-client.test.ts +276 -0
  25. package/apps/studio/figma/codegen-client.ts +509 -0
  26. package/apps/studio/figma/codegen-fonts.test.ts +103 -0
  27. package/apps/studio/figma/codegen-fonts.ts +195 -0
  28. package/apps/studio/figma/codegen-values.test.ts +179 -0
  29. package/apps/studio/figma/codegen-values.ts +270 -0
  30. package/apps/studio/figma/endpoints.ts +73 -0
  31. package/apps/studio/figma/fig-decode.test.ts +702 -0
  32. package/apps/studio/figma/fig-decode.ts +617 -0
  33. package/apps/studio/figma/fig-kiwi.ts +410 -0
  34. package/apps/studio/figma/fig-zip.ts +270 -0
  35. package/apps/studio/figma/from-codegen.test.ts +408 -0
  36. package/apps/studio/figma/from-codegen.ts +1103 -0
  37. package/apps/studio/figma/sanitize.test.ts +69 -0
  38. package/apps/studio/figma/sanitize.ts +139 -47
  39. package/apps/studio/figma/tailwind-map.test.ts +142 -0
  40. package/apps/studio/figma/tailwind-map.ts +545 -0
  41. package/apps/studio/figma/to-render.ts +25 -3
  42. package/apps/studio/figma/types.ts +6 -1
  43. package/apps/studio/http.ts +47 -0
  44. package/apps/studio/sync/asset-push.ts +346 -38
  45. package/apps/studio/sync/connection-state.ts +71 -3
  46. package/apps/studio/sync/index.ts +10 -1
  47. package/apps/studio/sync/status.ts +18 -0
  48. package/apps/studio/test/canvas-origin-gate.test.ts +4 -0
  49. package/apps/studio/test/figma-explode.test.ts +438 -0
  50. package/apps/studio/test/fixtures/perf-canvas.mjs +201 -0
  51. package/apps/studio/test/import-figma.test.ts +192 -4
  52. package/apps/studio/test/sync-asset-push.test.ts +490 -47
  53. package/apps/studio/test/sync-connection-state.test.ts +66 -0
  54. package/apps/studio/test/sync-panel-surface.test.ts +90 -0
  55. package/apps/studio/test/sync-status.test.ts +28 -0
  56. package/apps/studio/test/timeline-comp-target.test.ts +139 -0
  57. package/apps/studio/test/video-comp.test.ts +81 -1
  58. package/apps/studio/test/video-encode-lib.test.ts +63 -0
  59. package/apps/studio/use-artboard-drag.tsx +37 -3
  60. package/apps/studio/video-comp.tsx +51 -0
  61. package/apps/studio/whats-new.json +71 -0
  62. package/cli/commands/design.mjs +7 -0
  63. package/cli/commands/kg.mjs +8 -1
  64. package/cli/commands/kg.test.mjs +24 -0
  65. package/cli/lib/figma-codegen-reachability.test.mjs +104 -0
  66. package/cli/lib/figma-import-controls.test.mjs +70 -0
  67. 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
- expect(imageCalls.length).toBe(1);
456
- expect(new URL(imageCalls[0]).searchParams.get('ids')).toBe('2:1');
457
- expect(new URL(imageCalls[0]).searchParams.get('format')).toBe('svg');
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