@1agh/maude 0.58.3 → 0.60.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 (81) 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 +1180 -242
  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 +320 -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 +40 -0
  17. package/apps/studio/client/styles/4-components.css +4 -4
  18. package/apps/studio/context.ts +4 -0
  19. package/apps/studio/dist/client.bundle.js +772 -772
  20. package/apps/studio/dist/styles.css +1 -1
  21. package/apps/studio/exporters/video-encode-lib.ts +8 -5
  22. package/apps/studio/exporters/video.ts +10 -0
  23. package/apps/studio/figma/assets.test.ts +92 -0
  24. package/apps/studio/figma/assets.ts +63 -9
  25. package/apps/studio/figma/codegen-client.test.ts +276 -0
  26. package/apps/studio/figma/codegen-client.ts +509 -0
  27. package/apps/studio/figma/codegen-fonts.test.ts +103 -0
  28. package/apps/studio/figma/codegen-fonts.ts +195 -0
  29. package/apps/studio/figma/codegen-values.test.ts +179 -0
  30. package/apps/studio/figma/codegen-values.ts +270 -0
  31. package/apps/studio/figma/endpoints.ts +73 -0
  32. package/apps/studio/figma/fig-decode.test.ts +788 -0
  33. package/apps/studio/figma/fig-decode.ts +839 -0
  34. package/apps/studio/figma/fig-differential.test.ts +182 -0
  35. package/apps/studio/figma/fig-kiwi.ts +410 -0
  36. package/apps/studio/figma/fig-translator.test.ts +192 -0
  37. package/apps/studio/figma/fig-vector.test.ts +113 -0
  38. package/apps/studio/figma/fig-vector.ts +145 -0
  39. package/apps/studio/figma/fig-zip.ts +270 -0
  40. package/apps/studio/figma/from-codegen.test.ts +408 -0
  41. package/apps/studio/figma/from-codegen.ts +1103 -0
  42. package/apps/studio/figma/sanitize.test.ts +69 -0
  43. package/apps/studio/figma/sanitize.ts +146 -47
  44. package/apps/studio/figma/tailwind-map.test.ts +142 -0
  45. package/apps/studio/figma/tailwind-map.ts +545 -0
  46. package/apps/studio/figma/to-artboard.ts +41 -1
  47. package/apps/studio/figma/to-render.ts +25 -3
  48. package/apps/studio/figma/types.ts +6 -1
  49. package/apps/studio/http.ts +94 -0
  50. package/apps/studio/sync/asset-push-worker.ts +84 -0
  51. package/apps/studio/sync/asset-push.ts +441 -39
  52. package/apps/studio/sync/asset-sweep.ts +262 -0
  53. package/apps/studio/sync/connection-state.ts +71 -3
  54. package/apps/studio/sync/index.ts +39 -6
  55. package/apps/studio/sync/presentation.ts +21 -0
  56. package/apps/studio/sync/status.ts +18 -0
  57. package/apps/studio/sync/supervisor.ts +20 -0
  58. package/apps/studio/test/canvas-origin-gate.test.ts +13 -0
  59. package/apps/studio/test/figma-explode.test.ts +438 -0
  60. package/apps/studio/test/fixtures/perf-canvas.mjs +201 -0
  61. package/apps/studio/test/import-figma.test.ts +192 -4
  62. package/apps/studio/test/sync-asset-push-worker.test.ts +183 -0
  63. package/apps/studio/test/sync-asset-push.test.ts +639 -47
  64. package/apps/studio/test/sync-asset-sweep.test.ts +243 -0
  65. package/apps/studio/test/sync-connection-state.test.ts +66 -0
  66. package/apps/studio/test/sync-panel-surface.test.ts +123 -0
  67. package/apps/studio/test/sync-resync-routes.test.ts +125 -0
  68. package/apps/studio/test/sync-status.test.ts +28 -0
  69. package/apps/studio/test/sync-supervisor.test.ts +46 -0
  70. package/apps/studio/test/timeline-comp-target.test.ts +139 -0
  71. package/apps/studio/test/video-comp.test.ts +81 -1
  72. package/apps/studio/test/video-encode-lib.test.ts +63 -0
  73. package/apps/studio/use-artboard-drag.tsx +37 -3
  74. package/apps/studio/video-comp.tsx +51 -0
  75. package/apps/studio/whats-new.json +87 -0
  76. package/cli/commands/design.mjs +7 -0
  77. package/cli/commands/kg.mjs +8 -1
  78. package/cli/commands/kg.test.mjs +24 -0
  79. package/cli/lib/figma-codegen-reachability.test.mjs +104 -0
  80. package/cli/lib/figma-import-controls.test.mjs +70 -0
  81. package/package.json +8 -8
@@ -7,16 +7,20 @@
7
7
 
8
8
  import { describe, expect, test } from 'bun:test';
9
9
 
10
+ import { ASSET_DISPOSITIONS } from './assets.ts';
10
11
  import {
11
12
  attrValue,
12
13
  clampIntoBounds,
13
14
  cleanText,
14
15
  contrastRatio,
16
+ DISPOSITIONS,
15
17
  ensureContrast,
16
18
  ensureFontSize,
17
19
  hexToRgb01,
18
20
  ImportReport,
19
21
  identifierFromNodeId,
22
+ isCodeOwnedDetail,
23
+ isDisposition,
20
24
  jsxStringLiteral,
21
25
  MIN_FONT_SIZE,
22
26
  rgb01ToHex,
@@ -254,3 +258,68 @@ describe('D6a covers blank-glyph LETTERS, not just format characters (review F10
254
258
  expect(out.strippedHidden).toBe(true);
255
259
  });
256
260
  });
261
+
262
+ // ── DDR-219 D9 — the fixed-enum promise is a CONTROL, not a comment ──────────
263
+ //
264
+ // `Disposition` was a bare union, i.e. a compile-time promise, in a repo that
265
+ // runs no `tsc` gate. `assets.ts` shipped an off-enum `'asset-degraded'` to
266
+ // main — onto the wire, into verb stdout an agent reads (DDR-216 D10) and into
267
+ // the panel. These tests are what makes the promise true at runtime.
268
+
269
+ describe('DDR-219 D9 — disposition enum is enforced, not asserted', () => {
270
+ test('every disposition assets.ts declares is a member', () => {
271
+ // The drift that shipped: ASSET_DISPOSITIONS and the union disagreed, and
272
+ // nothing compared them.
273
+ for (const d of ASSET_DISPOSITIONS) expect(isDisposition(d)).toBe(true);
274
+ });
275
+
276
+ test('asset-degraded is a member — the specific drift found in main', () => {
277
+ expect(isDisposition('asset-degraded')).toBe(true);
278
+ expect(() => new ImportReport().add('1:2', 'ASSET', 'asset-degraded')).not.toThrow();
279
+ });
280
+
281
+ test('an invented disposition throws rather than reaching the wire', () => {
282
+ const r = new ImportReport();
283
+ // @ts-expect-error — the runtime backstop for exactly what tsc would catch
284
+ expect(() => r.add('1:2', 'ASSET', 'asset-vanished')).toThrow(/unknown disposition/);
285
+ expect(r.entries).toHaveLength(0);
286
+ });
287
+
288
+ test('DISPOSITIONS is frozen — a caller cannot widen the set at runtime', () => {
289
+ expect(Object.isFrozen(DISPOSITIONS)).toBe(true);
290
+ });
291
+
292
+ test('every real detail in the codebase satisfies the code-owned rule', () => {
293
+ // Non-ASCII is legitimate here — real notes carry em-dash and arrow. What
294
+ // separates a code-owned note from interpolated node text is length and the
295
+ // absence of a zero-glyph payload.
296
+ for (const d of [
297
+ 'vector unavailable — rasterized',
298
+ 'group → groupIds tag',
299
+ 'page has no frames — rendered whole',
300
+ 'loose content wrapped in one artboard',
301
+ ])
302
+ expect(isCodeOwnedDetail(d)).toBe(true);
303
+ });
304
+
305
+ test('detail rejects an interpolated upstream string', () => {
306
+ const r = new ImportReport();
307
+ // A Figma layer name is exactly what must never ride out in `detail`.
308
+ const layerName = 'V4 — Airy / light — Property 1=Cisty-a-zeleny-kraj 1 — Úprava 3d modelu';
309
+ expect(() => r.add('1:2', 'FRAME', 'imported', layerName)).toThrow(/code-owned/);
310
+ });
311
+
312
+ test('detail rejects a zero-glyph payload even when short', () => {
313
+ const r = new ImportReport();
314
+ expect(() => r.add('1:2', 'FRAME', 'imported', `ok\u{E0041}`)).toThrow(/code-owned/);
315
+ });
316
+
317
+ test('the zero-glyph predicate is not stateful across calls', () => {
318
+ // ZERO_GLYPH_RE carries `g`, so `.test()` on THAT instance alternates
319
+ // true/false via lastIndex. The one-shot predicate must not share it.
320
+ const dirty = 'a​b';
321
+ expect(isCodeOwnedDetail(dirty)).toBe(false);
322
+ expect(isCodeOwnedDetail(dirty)).toBe(false);
323
+ expect(isCodeOwnedDetail(dirty)).toBe(false);
324
+ });
325
+ });
@@ -39,28 +39,88 @@
39
39
  * @invariant DEPENDENCY-FREE — pure string/number work.
40
40
  */
41
41
 
42
- /** Every disposition this feature can report. A FIXED enum: the per-import
42
+ /** Every disposition this feature can report. A FIXED set: the per-import
43
43
  * summary's "never silently dropped" promise is only true because a caller
44
- * cannot invent a reason string (DDR-216 D5/D7). */
45
- export type Disposition =
46
- | 'imported'
47
- | 'hidden-chars-dropped'
48
- | 'hidden-node-skipped'
49
- | 'text-normalized'
50
- | 'geometry-clamped'
51
- | 'truncated-text'
52
- | 'truncated-attr'
53
- | 'unmappable-type'
54
- | 'unmappable-shape'
44
+ * cannot invent a reason string (DDR-216 D5/D7).
45
+ *
46
+ * A frozen ARRAY, with the type derived from it, rather than a bare union —
47
+ * because a union alone is a compile-time promise and this repo runs **no
48
+ * `tsc` gate** (CLAUDE.md states the omission deliberately: `quality` is
49
+ * lint/tests/build/parity/tarball/tokens/site-content, no typecheck). That gap
50
+ * is not hypothetical: `assets.ts` shipped `'asset-degraded'` — absent from the
51
+ * union — to `main`, onto the wire (`http.ts` `/_api/figma/import`), into verb
52
+ * stdout that an agent reads (D10), and into `FigmaImportPanel`. Found by the
53
+ * DDR-219 design-stage review, fixed here with `asset-degraded` admitted as the
54
+ * legitimate outcome it always was. See DDR-219 D9. */
55
+ export const DISPOSITIONS = Object.freeze([
56
+ 'imported',
57
+ 'hidden-chars-dropped',
58
+ 'hidden-node-skipped',
59
+ 'text-normalized',
60
+ 'geometry-clamped',
61
+ 'truncated-text',
62
+ 'truncated-attr',
63
+ 'unmappable-type',
64
+ 'unmappable-shape',
55
65
  /** A comment whose pinned node no longer exists in the file — no position to place it at. */
56
- | 'comment-target-deleted'
57
- | 'bind-degraded-to-bbox'
58
- | 'bind-dropped-self-connector'
59
- | 'asset-pending'
60
- | 'asset-skipped'
61
- | 'asset-cap-reached'
62
- | 'jsx-cap-reached'
63
- | 'value-rejected';
66
+ 'comment-target-deleted',
67
+ 'bind-degraded-to-bbox',
68
+ 'bind-dropped-self-connector',
69
+ 'asset-pending',
70
+ 'asset-skipped',
71
+ 'asset-cap-reached',
72
+ /** A vector Figma declined to render as SVG, re-requested as PNG. */
73
+ 'asset-degraded',
74
+ /**
75
+ * The LOCAL `.fig` door (DDR-221) cannot produce this asset: a vector cluster
76
+ * is rendered by Figma's servers and is simply absent from an export. The
77
+ * archive carries image FILLS, never renders. Distinct from `asset-skipped`
78
+ * ("we tried and it failed") — nothing was attempted and nothing could be.
79
+ */
80
+ 'asset-unavailable-offline',
81
+ 'jsx-cap-reached',
82
+ 'value-rejected',
83
+ // ── The codegen route (DDR-219 D9). Three dispositions, one rule: what makes
84
+ // them safe is that `detail` carries a BOUNDED token plus a count, never the
85
+ // upstream family or utility name verbatim — `detail` is the one field on the
86
+ // wire no sanitizer touches, and it reaches verb stdout (D10), the HTTP route
87
+ // and the panel.
88
+ /** A Tailwind utility the mapper does not know, or whose value failed its grammar. */
89
+ 'codegen-utility-unmapped',
90
+ /** A font family that did not survive the copy — a CSS fallback is NOT a report. */
91
+ 'font-substituted',
92
+ /** Dev Mode unreachable / no seat / wrong document. The COMMON case, not an error state. */
93
+ 'codegen-unavailable',
94
+ /** The converter itself could not be LOADED. Contract: REFUSE, never "let a model do it". */
95
+ 'codegen-converter-unavailable',
96
+ /**
97
+ * The converter ran and REFUSED this frame — a parse error, an element outside
98
+ * the allowlist, a construct it does not understand (DDR-219 D5 rule 4).
99
+ *
100
+ * Deliberately NOT folded into `codegen-unavailable`: "Figma was unreachable"
101
+ * and "we looked at what Figma sent and would not emit it" are different
102
+ * facts, and conflating outcomes is how this feature reported success three
103
+ * times while losing content.
104
+ */
105
+ 'codegen-frame-refused',
106
+ ] as const);
107
+
108
+ export type Disposition = (typeof DISPOSITIONS)[number];
109
+
110
+ const DISPOSITION_SET: ReadonlySet<string> = new Set(DISPOSITIONS);
111
+
112
+ export function isDisposition(v: string): v is Disposition {
113
+ return DISPOSITION_SET.has(v);
114
+ }
115
+
116
+ /** `detail` is a code-owned note, and the only field on the wire that no
117
+ * sanitizer touches — so it is the one place an upstream string would ride out
118
+ * to a model unescaped (DDR-219 D9). It is NOT charset-restricted to ASCII:
119
+ * real notes carry `—` and `→` legitimately. What distinguishes a code-owned
120
+ * note from interpolated node text is that it is SHORT and carries no
121
+ * zero-glyph payload, so those are what get asserted. Longest real note today
122
+ * is 36 chars. */
123
+ export const MAX_DETAIL_LEN = 64;
64
124
 
65
125
  export interface ReportEntry {
66
126
  /** `^[0-9]+:[0-9]+$` — an identifier, never node text (DDR-216 D7). */
@@ -76,6 +136,17 @@ export class ImportReport {
76
136
  readonly entries: ReportEntry[] = [];
77
137
 
78
138
  add(nodeId: string, type: string, disposition: Disposition, detail?: string): void {
139
+ // The backstop the missing `tsc` gate cannot provide. A disposition outside
140
+ // the set is a programming error, never bad input — so it throws rather than
141
+ // degrading: silent acceptance is exactly how `asset-degraded` reached main.
142
+ if (!DISPOSITION_SET.has(disposition)) {
143
+ throw new Error(`ImportReport: unknown disposition ${JSON.stringify(disposition)}`);
144
+ }
145
+ if (detail !== undefined && !isCodeOwnedDetail(detail)) {
146
+ throw new Error(
147
+ `ImportReport: detail must be a short code-owned note (<=${MAX_DETAIL_LEN} chars, no zero-glyph); got ${detail.length} chars`
148
+ );
149
+ }
79
150
  this.entries.push(
80
151
  detail ? { nodeId, type, disposition, detail } : { nodeId, type, disposition }
81
152
  );
@@ -86,6 +157,11 @@ export class ImportReport {
86
157
  }
87
158
  }
88
159
 
160
+ /** True when `detail` looks like the code-owned note it is contracted to be. */
161
+ export function isCodeOwnedDetail(detail: string): boolean {
162
+ return detail.length <= MAX_DETAIL_LEN && !ZERO_GLYPH_ONESHOT_RE.test(detail);
163
+ }
164
+
89
165
  // ── D6a — character classes ─────────────────────────────────────────────────
90
166
 
91
167
  /**
@@ -111,33 +187,37 @@ export class ImportReport {
111
187
  * *letters or symbols* by category but render blank. A category rule cannot rot
112
188
  * the way an enumeration does.
113
189
  */
114
- const ZERO_GLYPH_RE = new RegExp(
115
- [
116
- // Format / control / private-use / unassigned — the categories that cover
117
- // every bidi control, every zero-width, the Tags block, and anything Unicode
118
- // adds later without this list needing an edit.
119
- '[\\p{Cf}\\p{Cc}\\p{Co}\\p{Cn}]',
120
- // Blank-rendering code points that are LETTERS or SYMBOLS by category, so no
121
- // category rule catches them. Listed one per alternative rather than as a
122
- // character class: `\u034F` is a COMBINING character, and a class mixing it
123
- // with base characters is the `noMisleadingCharacterClass` footgun.
124
- '\\u00AD', // soft hyphen
125
- '\\u034F', // combining grapheme joiner
126
- '\\u061C', // arabic letter mark (Trojan Source)
127
- '\\u115F', // hangul choseong filler
128
- '\\u1160', // hangul jungseong filler
129
- '\\u17B4', // khmer inherent AQ
130
- '\\u17B5', // khmer inherent AA
131
- '[\\u180B-\\u180E]', // mongolian selectors + vowel separator
132
- '\\u2800', // braille pattern blank
133
- '\\u3164', // HANGUL FILLER — category Lo, i.e. a LETTER
134
- '[\\uFE00-\\uFE0F]', // variation selectors
135
- '\\uFFA0', // halfwidth hangul filler
136
- '[\\uFFF9-\\uFFFB]', // interlinear annotation
137
- '[\\u{E0100}-\\u{E01EF}]', // variation selectors supplement
138
- ].join('|'),
139
- 'gu'
140
- );
190
+ const ZERO_GLYPH_SOURCE = [
191
+ // Format / control / private-use / unassigned — the categories that cover
192
+ // every bidi control, every zero-width, the Tags block, and anything Unicode
193
+ // adds later without this list needing an edit.
194
+ '[\\p{Cf}\\p{Cc}\\p{Co}\\p{Cn}]',
195
+ // Blank-rendering code points that are LETTERS or SYMBOLS by category, so no
196
+ // category rule catches them. Listed one per alternative rather than as a
197
+ // character class: `\u034F` is a COMBINING character, and a class mixing it
198
+ // with base characters is the `noMisleadingCharacterClass` footgun.
199
+ '\\u00AD', // soft hyphen
200
+ '\\u034F', // combining grapheme joiner
201
+ '\\u061C', // arabic letter mark (Trojan Source)
202
+ '\\u115F', // hangul choseong filler
203
+ '\\u1160', // hangul jungseong filler
204
+ '\\u17B4', // khmer inherent AQ
205
+ '\\u17B5', // khmer inherent AA
206
+ '[\\u180B-\\u180E]', // mongolian selectors + vowel separator
207
+ '\\u2800', // braille pattern blank
208
+ '\\u3164', // HANGUL FILLER — category Lo, i.e. a LETTER
209
+ '[\\uFE00-\\uFE0F]', // variation selectors
210
+ '\\uFFA0', // halfwidth hangul filler
211
+ '[\\uFFF9-\\uFFFB]', // interlinear annotation
212
+ '[\\u{E0100}-\\u{E01EF}]', // variation selectors supplement
213
+ ].join('|');
214
+
215
+ const ZERO_GLYPH_RE = new RegExp(ZERO_GLYPH_SOURCE, 'gu');
216
+
217
+ /** The same rule without `g`. `RegExp.prototype.test` on a GLOBAL regex advances
218
+ * `lastIndex` and so alternates true/false across calls on the same instance —
219
+ * a one-shot predicate must never share the `g` instance. */
220
+ const ZERO_GLYPH_ONESHOT_RE = new RegExp(ZERO_GLYPH_SOURCE, 'u');
141
221
 
142
222
  /** Tab / newline / carriage return are legitimate content — never stripped. */
143
223
  const KEEP_WHITESPACE_RE = /[\t\n\r]/;
@@ -184,6 +264,25 @@ export function attrValue(raw: string, maxLen = 64): string {
184
264
  .slice(0, maxLen);
185
265
  }
186
266
 
267
+ /**
268
+ * A report token for `detail` — strict, and deliberately NOT `attrValue`.
269
+ *
270
+ * `attrValue` rewrites rejected characters to SPACES, which is right for a label
271
+ * and wrong here: a class token like `ignore.all.prior.instructions.and` comes
272
+ * back as that sentence, and `detail` reaches verb stdout (which D10 declares
273
+ * entirely code-owned), the HTTP summary and the panel. Charset-bounding is a
274
+ * markup control; it was never a semantic one (post-implementation review F3).
275
+ *
276
+ * So: a Tailwind FAMILY shape only — lowercase, digits, hyphens, NO SPACES ever
277
+ * — and anything else collapses to a fixed word. `unrecognized x7` is less
278
+ * informative than the raw token and cannot be read as an instruction, which is
279
+ * the right trade for a field an agent reads.
280
+ */
281
+ export function reportToken(raw: string): string {
282
+ const head = /^[a-z][a-z0-9-]{0,23}/.exec(raw.trim().toLowerCase());
283
+ return head ? head[0] : 'unrecognized';
284
+ }
285
+
187
286
  /**
188
287
  * A JSX-safe identifier derived from a NODE ID — never from a Figma string.
189
288
  * `2:17` → `Node_2_17`. This is the whole identifier story: there is no Figma
@@ -0,0 +1,142 @@
1
+ // figma/tailwind-map.ts — Tailwind utility → JSX style object.
2
+ //
3
+ // The coverage claim is a MEASUREMENT, not an aspiration: 129 distinct classes
4
+ // across 64 families were counted on a real 375×812 screen (DDR-219 § Spike),
5
+ // and the census below is that exact list. If a future Figma release emits
6
+ // something new, this test is where it shows up — as an `unmapped` entry, which
7
+ // is the reportable outcome the acceptance criteria demand, not a silent drop.
8
+
9
+ import { describe, expect, test } from 'bun:test';
10
+
11
+ import { MAX_CLASSES_PER_ELEMENT, mapClassName } from './tailwind-map.ts';
12
+
13
+ const DS_FONTS = [{ name: '--font-body', value: "'hanken grotesk','inter',sans-serif" }];
14
+
15
+ /** Every distinct class observed on `425:2939`. */
16
+ const MEASURED_CLASSES = `relative shrink-0 flex content-stretch items-center flex-col absolute
17
+ justify-center size-full max-w-none block inset-0 [word-break:break-word] not-italic leading-[0]
18
+ whitespace-nowrap size-[24px] w-full text-black items-start border-solid leading-[normal] gap-[8px]
19
+ text-[12px] font-normal border bg-[var(--black-10,rgba(15,22,30,0.1))] py-[8px] px-[12px]
20
+ rounded-[8px] overflow-clip leading-[20px] font-bold tracking-[0.288px] text-[14px] justify-between
21
+ font-[family-name:var(--font,'Inter:Regular')] font-[family-name:var(--font,'Inter:Bold')] p-[10px]
22
+ border-black w-[185px] rounded-[9px] min-w-px flex-[1_0_0] text-center self-stretch rounded-[999px]
23
+ w-[40px] text-[color:var(--black,#0f161e)] size-[21.5px] rounded-[99px] p-px h-[32px] gap-px
24
+ font-['Inter:Regular'] border-[var(--black,#0f161e)] row-1 py-[16px] px-[24px] col-1 w-[34.018px]
25
+ w-[16px] text-[10px] text-[#0f161e] size-[32px] px-[10px] mt-0 h-[12px] border-b
26
+ border-[var(--black-10,rgba(15,22,30,0.1))] -translate-y-1/2 -translate-x-1/2 w-[54px] w-[375px]
27
+ w-[364px] w-[358px] w-[341.871px] w-[327px] w-[25px] w-[18px] w-0 tracking-[0.24px] tracking-[-0.3px]
28
+ top-[695.49px] top-[25.01px] top-[191px] top-1/2 text-[16px] size-[16px] size-[14.018px]
29
+ rounded-[16px] py-[12px] px-[20px] place-items-start pb-[8px] mt-px ml-[50px] ml-[26px] ml-0
30
+ left-[calc(50%-32.5px)] left-[calc(50%-0.02px)] left-[6.35px] left-1/2 leading-[12px] justify-end
31
+ inset-[37.5%_18.75%_26.56%_18.75%] inset-[10.42%_16.67%_8.33%_16.67%] inset-[0_4.17%] inset-[0_-0.5px]
32
+ inset-[0.49%_0.06%_-0.36%_0.07%] inset-[-6.64%_-7.07%_-7.49%_-7.07%] inline-grid h-full h-[768px]
33
+ h-[58.766px] h-[480px] h-[296px] h-[14.454px] h-[14.018px] h-[10px] grid-rows-[max-content]
34
+ grid-cols-[max-content] gap-[7px] gap-[4px] font-['SF_Pro_Display:Semibold'] font-['SF_Pro:Bold']
35
+ flex-row border-[red] bg-white`
36
+ .trim()
37
+ .split(/\s+/);
38
+
39
+ describe('the measured surface', () => {
40
+ test('every class from the real screen maps — 129/129, zero unmapped', () => {
41
+ const unmapped: string[] = [];
42
+ for (const cls of MEASURED_CLASSES) {
43
+ const r = mapClassName(cls, { fontTokens: DS_FONTS });
44
+ if (r.unmapped.length > 0) unmapped.push(cls);
45
+ }
46
+ expect(unmapped).toEqual([]);
47
+ expect(MEASURED_CLASSES.length).toBe(129);
48
+ });
49
+ });
50
+
51
+ describe('the disambiguations that are easy to get wrong', () => {
52
+ test('text-[16px] is a font size and text-[#hex] is a colour', () => {
53
+ expect(mapClassName('text-[16px]').declarations).toEqual({ fontSize: '16px' });
54
+ expect(mapClassName('text-[#0f161e]').declarations).toEqual({ color: '#0f161e' });
55
+ });
56
+
57
+ test("Tailwind's own color: hint wins", () => {
58
+ expect(mapClassName('text-[color:var(--black,#0f161e)]').declarations).toEqual({
59
+ color: 'var(--black,#0f161e)',
60
+ });
61
+ });
62
+
63
+ test('border-[red] is a colour, border-[2px] is a width', () => {
64
+ expect(mapClassName('border-[red]').declarations).toEqual({ borderColor: 'red' });
65
+ expect(mapClassName('border-[2px]').declarations).toEqual({
66
+ borderWidth: '2px',
67
+ borderStyle: 'solid',
68
+ });
69
+ });
70
+
71
+ test('a negative utility carries its sign on the FAMILY, not the value', () => {
72
+ expect(mapClassName('-ml-[26px]').declarations).toEqual({ marginLeft: '-26px' });
73
+ });
74
+
75
+ test('transforms compose into ONE declaration', () => {
76
+ expect(mapClassName('-translate-x-1/2 -translate-y-1/2').declarations.transform).toBe(
77
+ 'translateX(-50%) translateY(-50%)'
78
+ );
79
+ });
80
+
81
+ test('leading-[0] becomes normal — a zero line box hides the text', () => {
82
+ // D6b's rule in miniature: the translator can GUARANTEE visibility, so it
83
+ // does, rather than faithfully reproducing a collapsed line and reporting
84
+ // success on an empty-looking screen.
85
+ expect(mapClassName('leading-[0]').declarations).toEqual({ lineHeight: 'normal' });
86
+ expect(mapClassName('leading-[20px]').declarations).toEqual({ lineHeight: '20px' });
87
+ });
88
+
89
+ test('size-[N] sets both axes', () => {
90
+ expect(mapClassName('size-[16px]').declarations).toEqual({ width: '16px', height: '16px' });
91
+ });
92
+ });
93
+
94
+ describe('what must NOT get through', () => {
95
+ test.each([
96
+ 'bg-[url(https://attacker.example/beacon.png)]',
97
+ '[behavior:url(#x)]',
98
+ '[content:"pwned"]',
99
+ 'w-[99999999px]',
100
+ 'text-[expression(alert(1))]',
101
+ 'bg-[#fff;position:fixed]',
102
+ 'grid-cols-[repeat(999999,1fr)]',
103
+ ])('%s is reported, never emitted', (cls) => {
104
+ const r = mapClassName(cls);
105
+ expect(r.declarations).toEqual({});
106
+ expect(r.unmapped.length).toBeGreaterThan(0);
107
+ });
108
+
109
+ test('an unknown utility is REPORTED — "never silently dropped" is the criterion', () => {
110
+ const r = mapClassName('flex definitely-not-a-tailwind-class');
111
+ expect(r.declarations).toEqual({ display: 'flex' });
112
+ expect(r.unmapped).toEqual(['definitely-not-a-tailwind-class']);
113
+ });
114
+
115
+ test('a class list long enough to be a payload is capped, and the cap is reported', () => {
116
+ const many = Array.from({ length: MAX_CLASSES_PER_ELEMENT + 10 }, () => 'flex').join(' ');
117
+ const r = mapClassName(many);
118
+ expect(r.unmapped).toContain('class-cap-reached');
119
+ });
120
+
121
+ test('an oversized single token never reaches a grammar', () => {
122
+ const r = mapClassName(`w-[${'1'.repeat(500)}px]`);
123
+ expect(r.declarations).toEqual({});
124
+ expect(r.unmapped).toEqual(['oversized']);
125
+ });
126
+ });
127
+
128
+ describe('DS tokens', () => {
129
+ test('a bare hex snaps onto a near DS token', () => {
130
+ const r = mapClassName('bg-[#0f161e]', {
131
+ tokens: [{ name: '--fg-0', hex: '#0f161e' }],
132
+ });
133
+ expect(r.declarations.background).toBe('var(--fg-0)');
134
+ });
135
+
136
+ test('a var() Figma already resolved is NOT re-snapped — that would be a downgrade', () => {
137
+ const r = mapClassName('bg-[var(--black,#0f161e)]', {
138
+ tokens: [{ name: '--fg-0', hex: '#0f161e' }],
139
+ });
140
+ expect(r.declarations.background).toBe('var(--black,#0f161e)');
141
+ });
142
+ });