synthesisui 0.16.82 → 0.16.84

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.
@@ -0,0 +1,535 @@
1
+ /**
2
+ * CSS MODULES - the largest source of style in a real repo, and the one nothing read.
3
+ *
4
+ * Enumerated before a line of this was written, which is the rule that produced it
5
+ * (dono, 01/08). In `apps/web-dashboard`:
6
+ *
7
+ * 530 module stylesheets, 32,947 lines
8
+ * 3,978 simple class selectors each one a part, under the author's own name
9
+ * 5,777 real CSS declarations against 0.2 per component read before this
10
+ * 1,623 (28%) point at var(--their-token)
11
+ * 51 dark rules via [data-theme], all in one stylesheet
12
+ * 154 keyframes
13
+ * 47 descendant selectors
14
+ * 45 media queries
15
+ * 29 states
16
+ * 0 `composes:` - no inheritance to resolve
17
+ *
18
+ * 239 of 317 components (75%) style this way, and they arrived with ZERO declarations.
19
+ * A person importing that app would get 283 empty components and conclude the product
20
+ * is broken, which is a reasonable conclusion to reach.
21
+ *
22
+ * IT IS MORE RELIABLE THAN READING TAILWIND, not less. There is no utility table to
23
+ * maintain and nothing is inferred from a name: the declaration is already CSS, and the
24
+ * part name is the word the author typed. The mapping is arithmetic:
25
+ *
26
+ * Loader.tsx import styles from './Chat.module.css'
27
+ * <div className={styles.loader}> → the base
28
+ * <div className={styles.spinner}> → part "spinner"
29
+ * <p className={styles.loadingMessage}> → part "loading-message"
30
+ *
31
+ * Chat.module.css .loader { display:flex; gap:8px }
32
+ * .spinner { width:20px; animation:spinPulse 4s }
33
+ * .loadingMessage { … }
34
+ *
35
+ * THE INTERPOLATED TEMPLATE IS THE SAME FAMILY, which the enumeration also settled. Of
36
+ * 200 real `className={`…`}` occurrences, 165 are a literal plus `${styles.x}` and 85
37
+ * are `${cond ? styles.a : styles.b}` - a variant, with the condition naming the prop
38
+ * and both branches naming a class in this file. Exactly one is a ternary between two
39
+ * string literals. So this reader covers a family I had written off as impossible.
40
+ */
41
+ /** A CSS pseudo-class or attribute, mapped to the state name the contract uses. */
42
+ const STATE_OF = [
43
+ [/:hover\b/, "hover"],
44
+ [/:focus-visible\b/, "focusVisible"],
45
+ [/:focus-within\b/, "focus"],
46
+ [/:focus\b/, "focus"],
47
+ [/:active\b/, "active"],
48
+ [/:disabled\b|\[disabled\]/, "disabled"],
49
+ [/:checked\b|\[data-checked\]|\[aria-checked="?true"?\]/, "checked"],
50
+ [/\[aria-selected="?true"?\]|\[data-selected\]/, "selected"],
51
+ [/\[open\]|\[aria-expanded="?true"?\]|\[data-state="?open"?\]/, "open"],
52
+ [/:invalid\b|\[aria-invalid="?true"?\]/, "invalid"],
53
+ [/:read-only\b|\[readonly\]/, "readOnly"],
54
+ [/::placeholder\b/, "placeholder"],
55
+ [/:nth-child\(even\)/, "even"],
56
+ [/:nth-child\(odd\)/, "odd"],
57
+ [/:first-child\b/, "first"],
58
+ [/:last-child\b/, "last"],
59
+ [/:empty\b/, "empty"],
60
+ ];
61
+ /** `[data-theme="dark"]`, `.dark`, `[data-scheme=dark]` - how a project says dark. */
62
+ const DARK = /\[data-theme=["']?dark["']?\]|\[data-scheme=["']?dark["']?\]|(^|\s)\.dark(\s|$)/;
63
+ /** `kebab-case` → `camelCase`, which is the only spelling the contract accepts. */
64
+ export function camel(prop) {
65
+ return prop.trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase());
66
+ }
67
+ /** `.loadingMessage` → `loading-message`. THEIR word, only re-spelled for a class. */
68
+ export function partName(authored) {
69
+ return authored
70
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
71
+ .toLowerCase()
72
+ .replace(/[^a-z0-9-]/g, "-")
73
+ .replace(/-+/g, "-")
74
+ .replace(/^-|-$/g, "");
75
+ }
76
+ /** Comments out, so a `{` inside one cannot end a block early. */
77
+ function stripCssComments(css) {
78
+ return css.replace(/\/\*[\s\S]*?\*\//g, "");
79
+ }
80
+ function rules(css, media = null, out = []) {
81
+ let i = 0;
82
+ while (i < css.length) {
83
+ const open = css.indexOf("{", i);
84
+ if (open === -1)
85
+ break;
86
+ const head = css.slice(i, open).trim();
87
+ // Balance to the matching close, because an at-rule contains whole rules.
88
+ let depth = 0;
89
+ let close = -1;
90
+ for (let j = open; j < css.length; j++) {
91
+ if (css[j] === "{")
92
+ depth += 1;
93
+ else if (css[j] === "}") {
94
+ depth -= 1;
95
+ if (depth === 0) {
96
+ close = j;
97
+ break;
98
+ }
99
+ }
100
+ }
101
+ if (close === -1)
102
+ break;
103
+ const body = css.slice(open + 1, close);
104
+ if (head.startsWith("@")) {
105
+ const at = /^@media\b/.test(head) ? head : null;
106
+ if (/^@(?:media|supports|layer)\b/.test(head)) {
107
+ rules(body, at ?? media, out);
108
+ }
109
+ else {
110
+ // `@keyframes` and friends: the caller reads them whole.
111
+ out.push({ selector: head, body, media });
112
+ }
113
+ }
114
+ else if (head) {
115
+ out.push({ selector: head, body, media });
116
+ }
117
+ i = close + 1;
118
+ }
119
+ return out;
120
+ }
121
+ /** `prop: value;` pairs, in the camelCase the contract uses. */
122
+ function declarations(body) {
123
+ const out = {};
124
+ // Only the top level: a nested rule's declarations belong to that rule.
125
+ const flat = body.replace(/\{[^{}]*\}/g, "").replace(/[^;]*\{/g, "");
126
+ for (const piece of flat.split(";")) {
127
+ const colon = piece.indexOf(":");
128
+ if (colon === -1)
129
+ continue;
130
+ const prop = piece.slice(0, colon).trim();
131
+ const value = piece.slice(colon + 1).trim();
132
+ if (!/^-?[a-z][a-z-]*$/.test(prop) || !value)
133
+ continue;
134
+ // A value that could break out of the stylesheet is not a value we carry.
135
+ if (/[<>{}@]/.test(value))
136
+ continue;
137
+ out[camel(prop)] = value;
138
+ }
139
+ return out;
140
+ }
141
+ /** `min-width: 768px` out of a media query, or null for anything else. */
142
+ function minWidthOf(media) {
143
+ if (!media)
144
+ return null;
145
+ const m = /min-width\s*:\s*([\d.]+(?:px|rem|em))/i.exec(media);
146
+ return m ? m[1] : null;
147
+ }
148
+ const CLASS_TOKEN = /\.(-?[_a-zA-Z][\w-]*)/g;
149
+ /** Every class the selector names, in order - the last one is the target. */
150
+ function classesIn(selector) {
151
+ CLASS_TOKEN.lastIndex = 0;
152
+ return [...selector.matchAll(CLASS_TOKEN)].map((m) => m[1]);
153
+ }
154
+ function emptyClass() {
155
+ return {
156
+ base: {},
157
+ states: {},
158
+ dark: {},
159
+ at: {},
160
+ darkStates: {},
161
+ children: [],
162
+ };
163
+ }
164
+ /**
165
+ * One module stylesheet, read.
166
+ *
167
+ * `declared` resolves a `var(--their-token)` to the ref for the token they named, the
168
+ * same way the utility reader does - so the value travels as `{radius.md}` rather than
169
+ * as a literal, and the recipe re-themes.
170
+ */
171
+ export function readModuleCss(css) {
172
+ const out = { classes: {}, keyframes: {}, unslotted: [] };
173
+ const clean = stripCssComments(css);
174
+ for (const rule of rules(clean)) {
175
+ if (rule.selector.startsWith("@keyframes")) {
176
+ const name = rule.selector.replace(/^@keyframes\s+/, "").trim();
177
+ if (name)
178
+ out.keyframes[name] = rule.body.trim();
179
+ continue;
180
+ }
181
+ if (rule.selector.startsWith("@")) {
182
+ out.unslotted.push(rule.selector);
183
+ continue;
184
+ }
185
+ // A comma-separated selector is several rules with one body.
186
+ for (const one of rule.selector.split(",")) {
187
+ const selector = one.trim();
188
+ if (!selector)
189
+ continue;
190
+ const names = classesIn(selector);
191
+ if (names.length === 0) {
192
+ // An element or `:root` selector styles something this reader cannot attach to
193
+ // a component. Said out loud rather than dropped.
194
+ out.unslotted.push(selector);
195
+ continue;
196
+ }
197
+ const target = names[names.length - 1];
198
+ const entry = out.classes[target] ?? emptyClass();
199
+ out.classes[target] = entry;
200
+ // A descendant selector records the RELATION as well as the styles: `.panel
201
+ // .title` says title lives inside panel, which is the anatomy for free.
202
+ if (names.length > 1) {
203
+ const parent = names[names.length - 2];
204
+ const above = out.classes[parent] ?? emptyClass();
205
+ if (!above.children.includes(target))
206
+ above.children.push(target);
207
+ out.classes[parent] = above;
208
+ }
209
+ const block = declarations(rule.body);
210
+ if (Object.keys(block).length === 0)
211
+ continue;
212
+ const isDark = DARK.test(selector);
213
+ const state = STATE_OF.find(([re]) => re.test(selector))?.[1] ?? null;
214
+ const width = minWidthOf(rule.media);
215
+ if (width) {
216
+ // A breakpoint layer wins over the state split: `@media … { .x:hover }` is rare
217
+ // enough that carrying it as the breakpoint's block is the honest simplification,
218
+ // and the selector is reported when it is neither.
219
+ entry.at[width] = { ...entry.at[width], ...block };
220
+ continue;
221
+ }
222
+ if (isDark && state) {
223
+ entry.darkStates[state] = { ...entry.darkStates[state], ...block };
224
+ continue;
225
+ }
226
+ if (isDark) {
227
+ entry.dark = { ...entry.dark, ...block };
228
+ continue;
229
+ }
230
+ if (state) {
231
+ entry.states[state] = { ...entry.states[state], ...block };
232
+ continue;
233
+ }
234
+ entry.base = { ...entry.base, ...block };
235
+ }
236
+ }
237
+ return out;
238
+ }
239
+ // ---------------------------------------------------------------------------
240
+ // The other half: which element wears which class
241
+ // ---------------------------------------------------------------------------
242
+ /** `import styles from "./X.module.css"` - the local name and the file. */
243
+ const MODULE_IMPORT = /import\s+([A-Za-z_$][\w$]*)\s+from\s+["']([^"']*\.module\.(?:css|scss|sass|less))["']/g;
244
+ export function moduleImports(source) {
245
+ MODULE_IMPORT.lastIndex = 0;
246
+ return [...source.matchAll(MODULE_IMPORT)].map((m) => ({
247
+ local: m[1],
248
+ specifier: m[2],
249
+ }));
250
+ }
251
+ /**
252
+ * A generic in type position, not an element.
253
+ *
254
+ * The same discriminator the utility reader uses, for the same reason: JSX follows `(`,
255
+ * `{`, `>`, a newline, a keyword or nothing, while a type argument follows an
256
+ * identifier. `return <Foo />` is the keyword case, so the keyword list is here too.
257
+ */
258
+ const JSX_BEFORE = /\b(return|case|default|await|yield|typeof|in|of|do|else|new)$/;
259
+ function looksLikeTypeArg(source, at) {
260
+ for (let i = at - 1; i >= 0; i--) {
261
+ const c = source[i];
262
+ if (c === " " || c === "\t")
263
+ continue;
264
+ if (!/[A-Za-z0-9_)\]]/.test(c))
265
+ return false;
266
+ return !JSX_BEFORE.test(source.slice(Math.max(0, i - 11), i + 1));
267
+ }
268
+ return false;
269
+ }
270
+ function scanTags(source) {
271
+ const out = [];
272
+ for (let i = 0; i < source.length; i++) {
273
+ if (source[i] !== "<")
274
+ continue;
275
+ const closing = source[i + 1] === "/";
276
+ const nameAt = i + (closing ? 2 : 1);
277
+ const name = /^[A-Za-z][\w.]*/.exec(source.slice(nameAt, nameAt + 64))?.[0];
278
+ if (!name)
279
+ continue;
280
+ if (!closing && looksLikeTypeArg(source, i))
281
+ continue;
282
+ let depth = 0;
283
+ let quote = null;
284
+ let end = -1;
285
+ for (let j = nameAt + name.length; j < source.length; j++) {
286
+ const ch = source[j];
287
+ if (quote) {
288
+ if (ch === "\\")
289
+ j += 1;
290
+ else if (ch === quote)
291
+ quote = null;
292
+ continue;
293
+ }
294
+ if (ch === '"' || ch === "'" || ch === "`") {
295
+ quote = ch;
296
+ continue;
297
+ }
298
+ if (ch === "{")
299
+ depth += 1;
300
+ else if (ch === "}")
301
+ depth -= 1;
302
+ else if (ch === ">" && depth === 0) {
303
+ end = j;
304
+ break;
305
+ }
306
+ }
307
+ if (end === -1)
308
+ continue;
309
+ const body = source.slice(nameAt + name.length, end);
310
+ if (closing) {
311
+ out.push({ at: i, kind: "close", tag: name, body: "" });
312
+ }
313
+ else {
314
+ out.push({ at: i, kind: "open", tag: name, body });
315
+ // Self-closing opens and closes at once.
316
+ if (body.trimEnd().endsWith("/")) {
317
+ out.push({ at: i + 1, kind: "close", tag: name, body: "" });
318
+ }
319
+ }
320
+ i = end;
321
+ }
322
+ return out;
323
+ }
324
+ /**
325
+ * Which class each element wears, and how deep it sits.
326
+ *
327
+ * The FIRST element carrying a module class is the component's own surface; everything
328
+ * below it is a part. That is the same rule the utility reader follows with
329
+ * `rootClasses`, applied to a different way of spelling a class.
330
+ */
331
+ export function readModuleUsage(source, local) {
332
+ const out = [];
333
+ const ref = new RegExp(`\\b${local}\\.([A-Za-z_$][\\w$]*)`, "g");
334
+ const bracket = new RegExp(`\\b${local}\\[`, "g");
335
+ // Depth by counting opens and closes as they appear, which is enough for well-formed
336
+ // JSX and degrades to a flat list rather than to a wrong tree.
337
+ const events = scanTags(source);
338
+ let depth = 0;
339
+ for (const event of events) {
340
+ if (event.kind === "close") {
341
+ depth = Math.max(0, depth - 1);
342
+ continue;
343
+ }
344
+ const body = event.body;
345
+ if (bracket.test(body)) {
346
+ // `styles[key]` picks a class at runtime, and the class is not in the source.
347
+ // Impossible rather than hard, so it is not attempted.
348
+ bracket.lastIndex = 0;
349
+ }
350
+ ref.lastIndex = 0;
351
+ const named = [...body.matchAll(ref)].map((m) => m[1]);
352
+ if (named.length > 0) {
353
+ /**
354
+ * THE TERNARY IS A VARIANT, and the enumeration is why this is here: 85 of 200
355
+ * real interpolated templates are `${cond ? styles.a : styles.b}`. The condition
356
+ * names the prop; each branch names a class in this file.
357
+ */
358
+ const variants = [];
359
+ const TERNARY = new RegExp(`([!A-Za-z_$][\\w$.!]*)\\s*\\?\\s*(?:${local}\\.([\\w$]+)|["'][^"']*["'])\\s*:\\s*(?:${local}\\.([\\w$]+)|["'][^"']*["'])`, "g");
360
+ for (const t of body.matchAll(TERNARY)) {
361
+ const prop = t[1].replace(/^!/, "").split(".").pop() ?? "";
362
+ if (!prop)
363
+ continue;
364
+ variants.push({
365
+ prop,
366
+ whenTrue: t[2] ?? null,
367
+ whenFalse: t[3] ?? null,
368
+ });
369
+ }
370
+ // The resting class is the first one that is not a ternary branch.
371
+ const inTernary = new Set(variants.flatMap((v) => [v.whenTrue, v.whenFalse].filter(Boolean)));
372
+ const resting = named.find((n) => !inTernary.has(n)) ?? named[0];
373
+ out.push({ className: resting, depth, tag: event.tag, variants });
374
+ }
375
+ depth += 1;
376
+ }
377
+ return out;
378
+ }
379
+ // ---------------------------------------------------------------------------
380
+ // The bridge: a module read + its usage, as one Transcription
381
+ // ---------------------------------------------------------------------------
382
+ /**
383
+ * ONE FUNNEL, so a module-styled component arrives in exactly the shape a
384
+ * utility-styled one does.
385
+ *
386
+ * Everything downstream - the anatomy tree, the floor, the compiler, the vitrine -
387
+ * already knows how to read a `Transcription`. Adding a second shape beside it would
388
+ * mean every consumer learning a second one, and a consumer that learns half is how
389
+ * "valid and unread" happens.
390
+ *
391
+ * `declared` resolves `var(--their-token)` to the ref for the token they named, the same
392
+ * way the utility reader does, so the value re-themes instead of being frozen.
393
+ */
394
+ export function transcribeModule(read, usage, declared) {
395
+ const resolve = (block) => {
396
+ const out = {};
397
+ for (const [prop, value] of Object.entries(block)) {
398
+ out[prop] = value.replace(/var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)/g, (whole, name) => declared.has(name) ? tokenRefFor(name) : whole);
399
+ }
400
+ return out;
401
+ };
402
+ const sorted = [...usage].sort((a, b) => a.depth - b.depth);
403
+ const root = sorted[0];
404
+ const parts = {};
405
+ const layers = [];
406
+ let count = 0;
407
+ const take = (className) => {
408
+ const c = read.classes[className];
409
+ if (!c)
410
+ return null;
411
+ const base = resolve(c.base);
412
+ const dark = resolve(c.dark);
413
+ const states = {};
414
+ for (const [state, block] of Object.entries(c.states)) {
415
+ states[state] = resolve(block);
416
+ }
417
+ count +=
418
+ Object.keys(base).length +
419
+ Object.keys(dark).length +
420
+ Object.values(states).reduce((n, b) => n + Object.keys(b).length, 0);
421
+ return { base, dark, states, own: c };
422
+ };
423
+ const rootTaken = root ? take(root.className) : null;
424
+ for (const u of sorted) {
425
+ const taken = u === root ? rootTaken : take(u.className);
426
+ if (!taken)
427
+ continue;
428
+ if (u !== root) {
429
+ parts[partName(u.className)] = {
430
+ base: taken.base,
431
+ dark: taken.dark,
432
+ states: taken.states,
433
+ };
434
+ }
435
+ // A breakpoint block and a dark state are LAYERS, because neither the flat map nor
436
+ // the dark half can hold a condition of two halves.
437
+ for (const [width, block] of Object.entries(taken.own.at)) {
438
+ const style = resolve(block);
439
+ if (Object.keys(style).length === 0)
440
+ continue;
441
+ count += Object.keys(style).length;
442
+ layers.push({ at: width, style });
443
+ }
444
+ for (const [state, block] of Object.entries(taken.own.darkStates)) {
445
+ const style = resolve(block);
446
+ if (Object.keys(style).length === 0)
447
+ continue;
448
+ count += Object.keys(style).length;
449
+ layers.push({ when: { state, scheme: "dark" }, style });
450
+ }
451
+ /**
452
+ * `${selected ? styles.selected : styles.notSelected}` - a variant, read from the
453
+ * markup. Both branches name a class in this file, so both become a layer under the
454
+ * prop the condition names.
455
+ */
456
+ for (const v of u.variants) {
457
+ for (const [option, className] of [
458
+ ["true", v.whenTrue],
459
+ ["false", v.whenFalse],
460
+ ]) {
461
+ if (!className)
462
+ continue;
463
+ const branch = read.classes[className];
464
+ if (!branch)
465
+ continue;
466
+ const style = resolve(branch.base);
467
+ if (Object.keys(style).length === 0)
468
+ continue;
469
+ count += Object.keys(style).length;
470
+ layers.push({ when: { variant: { [v.prop]: option } }, style });
471
+ }
472
+ }
473
+ }
474
+ /**
475
+ * The tree, from the depth the markup put each element at. `children` of the root are
476
+ * everything one level down; deeper than that is folded up rather than guessed, because
477
+ * the depth counter degrades to a flat list on malformed JSX and a wrong tree is worse
478
+ * than a shallow one.
479
+ */
480
+ const tree = sorted
481
+ .filter((u) => u !== root)
482
+ .map((u) => ({ as: formFor(u.tag), part: partName(u.className) }));
483
+ return {
484
+ base: rootTaken?.base ?? {},
485
+ dark: rootTaken?.dark ?? {},
486
+ states: rootTaken?.states ?? {},
487
+ layers,
488
+ parts,
489
+ tree,
490
+ keyframes: read.keyframes,
491
+ read: count,
492
+ };
493
+ }
494
+ /** The html tag → the preview form. The same nine the anatomy contract accepts. */
495
+ function formFor(tag) {
496
+ if (/^(img|picture|video|canvas|svg)$/.test(tag))
497
+ return "image";
498
+ if (/^h[1-6]$/.test(tag))
499
+ return "heading";
500
+ if (/^(button|a)$/.test(tag))
501
+ return "button";
502
+ if (/^(input|textarea|select)$/.test(tag))
503
+ return "field";
504
+ if (/^(span|p|label|strong|em|small|li|td|th|dt|dd)$/.test(tag))
505
+ return "text";
506
+ if (/^(ul|ol|table|tbody|thead|tr|section|article|nav|aside|header|footer|form|main|div)$/.test(tag)) {
507
+ return "stack";
508
+ }
509
+ // A capitalised tag is a component of theirs, and the frontier reader decides what it
510
+ // reaches - not this.
511
+ return /^[A-Z]/.test(tag) ? "component" : "stack";
512
+ }
513
+ /**
514
+ * A declared custom property as a token ref in the document's namespace. Their name is
515
+ * the path, which is "your names travel unchanged" one level deeper.
516
+ */
517
+ function tokenRefFor(name) {
518
+ const bare = name.replace(/^--/, "");
519
+ if (bare.startsWith("color-")) {
520
+ const rest = bare.slice("color-".length);
521
+ const m = /^(.*)-(\d{2,4})$/.exec(rest);
522
+ return m ? `{color.${m[1]}.${m[2]}}` : `{color.${rest}}`;
523
+ }
524
+ if (bare.startsWith("radius-"))
525
+ return `{radius.${bare.slice(7)}}`;
526
+ if (bare.startsWith("spacing-"))
527
+ return `{spacing.${bare.slice(8)}}`;
528
+ if (bare.startsWith("shadow-"))
529
+ return `{shadow.${bare.slice(7)}}`;
530
+ if (bare.startsWith("text-"))
531
+ return `{typography.scale.${bare.slice(5)}.fontSize}`;
532
+ if (bare.startsWith("font-"))
533
+ return `{typography.families.${bare.slice(5)}}`;
534
+ return `var(${name})`;
535
+ }
@@ -235,6 +235,20 @@ export function readUtility(utility, declared) {
235
235
  const keyword = TYPE_KEYWORD[core];
236
236
  if (keyword)
237
237
  return keyword;
238
+ /**
239
+ * BORDER WIDTH - `border`, `border-2`, `border-b`, `border-t-4`.
240
+ *
241
+ * `border-b border-lightgray-300` on a real table row gave up the colour and lost the
242
+ * WIDTH, so the row reproduced its border colour and no border (spec, 01/08). Tailwind
243
+ * splits the two across separate utilities and only one of them was in a table.
244
+ */
245
+ const width = BORDER_WIDTH.exec(core);
246
+ if (width) {
247
+ const side = width[1] ? BORDER_SIDE[width[1]] : "";
248
+ const property = side ? `border${side}Width` : "borderWidth";
249
+ const value = width[2] ? `${width[2]}px` : "1px";
250
+ return { property, value };
251
+ }
238
252
  /**
239
253
  * AN ARBITRARY VALUE POINTING AT ONE OF THEIR OWN TOKENS.
240
254
  *
@@ -258,8 +272,22 @@ export function readUtility(utility, declared) {
258
272
  if (bracket !== -1 && core.endsWith("]")) {
259
273
  const head = core.slice(0, bracket);
260
274
  const inner = core.slice(bracket + 2, -1);
261
- const property = COLOR_PROPERTY[head] ?? SPACING_PROPERTY[head] ?? BRACKET_PROPERTY[head];
262
275
  const value = resolveArbitrary(inner, declared);
276
+ /**
277
+ * `text-[…]` IS AMBIGUOUS AND THE VALUE DECIDES.
278
+ *
279
+ * `text-[1.8em]` is a font size and `text-[#fff]` is a colour, and the head alone
280
+ * cannot tell them apart. Reading the head first put `fontSize: 1.8em` into `color`
281
+ * on a real `Text` component - a wrong value, which is worse than a missing one
282
+ * (probe, 01/08). The same ambiguity the scale branch below already resolves.
283
+ */
284
+ const property = head === "text"
285
+ ? value && LOOKS_LIKE_LENGTH.test(value.value)
286
+ ? "fontSize"
287
+ : COLOR_PROPERTY.text
288
+ : (COLOR_PROPERTY[head] ??
289
+ SPACING_PROPERTY[head] ??
290
+ BRACKET_PROPERTY[head]);
263
291
  if (property && value) {
264
292
  return { property, value: value.value, token: value.token };
265
293
  }
@@ -380,6 +408,19 @@ export function readUtility(utility, declared) {
380
408
  * `rounded-[…]` is a radius, `shadow-[…]` an elevation, `text-[…]` ambiguous by design
381
409
  * (a colour or a size) and resolved by what the value looks like.
382
410
  */
411
+ /** `border`, `border-2`, `border-b`, `border-b-2` - a width, never a colour. The colour
412
+ * is a separate utility (`border-ocean-500`) and the colour table already reads it. */
413
+ const BORDER_WIDTH = /^border(?:-([xytrbl]))?(?:-(\d+))?$/;
414
+ const BORDER_SIDE = {
415
+ t: "Top",
416
+ r: "Right",
417
+ b: "Bottom",
418
+ l: "Left",
419
+ x: "Inline",
420
+ y: "Block",
421
+ };
422
+ /** A length or a token ref to one - not a colour. `1.8em`, `20px`, `{typography.…}`. */
423
+ const LOOKS_LIKE_LENGTH = /^-?[\d.]+(?:px|rem|em|ch|ex|vw|vh|vmin|vmax|%|pt|cm|mm|in)$|^\{(?:typography|spacing|radius)\.|^(?:calc|clamp|min|max)\(/;
383
424
  const BRACKET_PROPERTY = {
384
425
  rounded: "borderRadius",
385
426
  shadow: "boxShadow",
@@ -454,6 +495,51 @@ function refFor(name) {
454
495
  * `group-data-[checked]:` value written into the resting state would be a look
455
496
  * the component never has.
456
497
  */
498
+ /**
499
+ * `style={{ … }}` - 7 of 35 components in a real library and 73 of 317 in a real app.
500
+ *
501
+ * A literal, so it re-themes nowhere - and that is exactly why it travels. v1 is a
502
+ * MIRROR and v2 is where a literal becomes a token they approve, which is the rule this
503
+ * pipeline already runs on everywhere else. The alternative is the component arriving
504
+ * empty, and an empty component is not more honest than a literal one (dono, 01/08).
505
+ *
506
+ * Only the FIRST inline style in the file, matching what `rootClasses` does: it is the
507
+ * element the component returns, and everything deeper is a part this reader does not
508
+ * attempt.
509
+ */
510
+ const INLINE_STYLE = /style=\{\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}\}/;
511
+ export function readInlineStyle(source) {
512
+ const m = INLINE_STYLE.exec(source);
513
+ if (!m)
514
+ return {};
515
+ const out = {};
516
+ for (const piece of m[1].split(",")) {
517
+ const colon = piece.indexOf(":");
518
+ if (colon === -1)
519
+ continue;
520
+ const prop = piece
521
+ .slice(0, colon)
522
+ .trim()
523
+ .replace(/^["']|["']$/g, "");
524
+ let value = piece.slice(colon + 1).trim();
525
+ // A value computed at runtime is not in the source, so there is nothing to read.
526
+ if (!value || /[`$]|\?|=>|\(/.test(value))
527
+ continue;
528
+ value = value.replace(/^["']|["']$/g, "");
529
+ if (!/^[a-zA-Z][\w]*$/.test(prop))
530
+ continue;
531
+ if (/[<>{}@;]/.test(value))
532
+ continue;
533
+ // React accepts a bare number for a length property and means pixels.
534
+ if (/^-?\d+(\.\d+)?$/.test(value) && !UNITLESS.test(prop)) {
535
+ value = `${value}px`;
536
+ }
537
+ out[prop] = value;
538
+ }
539
+ return out;
540
+ }
541
+ /** Properties React leaves unitless, so a bare number is not pixels. */
542
+ const UNITLESS = /^(opacity|zIndex|flex|flexGrow|flexShrink|order|lineHeight|fontWeight|zoom|columnCount|fillOpacity|strokeOpacity|animationIterationCount)$/;
457
543
  export function transcribe(classes, declared) {
458
544
  const out = {
459
545
  base: {},