synthesisui 0.16.285 → 0.16.289

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.
@@ -38,6 +38,7 @@
38
38
  * and both branches naming a class in this file. Exactly one is a ternary between two
39
39
  * string literals. So this reader covers a family I had written off as impossible.
40
40
  */
41
+ import { tokenRefFor } from "../token-ref.js";
41
42
  /** A CSS pseudo-class or attribute, mapped to the state name the contract uses. */
42
43
  const STATE_OF = [
43
44
  [/:hover\b/, "hover"],
@@ -58,8 +59,19 @@ const STATE_OF = [
58
59
  [/:last-child\b/, "last"],
59
60
  [/:empty\b/, "empty"],
60
61
  ];
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|$)/;
62
+ /**
63
+ * `[data-theme="dark"]`, `.dark`, `html.dark`, `[data-scheme=dark]` - como um projeto diz escuro.
64
+ *
65
+ * O QUE O CLIENTE PERDIA: o esquema escuro escrito na folha dele. O padrão exigia que `.dark`
66
+ * viesse no início do seletor ou depois de um espaço, e ele escreve `html.dark .stroke-text` - a
67
+ * classe colada na tag, que é a forma que o Tailwind com `darkMode: "class"` produz quando o
68
+ * atributo vive no `<html>`. Cinco classes da folha dele caíam nisso, entre elas o `stroke-text` e o
69
+ * `holo-card`, e a regra escura delas era lida como se fosse a regra de repouso: o contorno claro do
70
+ * texto vinha pintado com a cor do tema escuro.
71
+ *
72
+ * `(?![\w-])` para não casar `.darkroom`, que é uma classe de alguém e não um esquema.
73
+ */
74
+ const DARK = /\[data-theme=["']?dark["']?\]|\[data-scheme=["']?dark["']?\]|(?:^|[\s>+~]|[a-zA-Z0-9\]])\.dark(?![\w-])/;
63
75
  /** `kebab-case` → `camelCase`, which is the only spelling the contract accepts. */
64
76
  export function camel(prop) {
65
77
  return prop.trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -274,7 +286,21 @@ export function readModuleCss(css) {
274
286
  const selector = one.trim();
275
287
  if (!selector)
276
288
  continue;
277
- const names = classesIn(selector);
289
+ /**
290
+ * O SELETOR DE ESQUEMA NÃO É UM ANCESTRAL DE ANATOMIA - e era registrado como um.
291
+ *
292
+ * `html.dark .stroke-text` tem duas classes, e a leitura tomava `dark` como PAI de
293
+ * `stroke-text`. Duas consequências, as duas na tela dele: a classe entrava na lista de
294
+ * "filho que só existe num contexto" e era descartada inteira - `stroke-text`,
295
+ * `stroke-text-soft`, `stroke-text-grad`, `stage-3d` e `holo-card`, cinco das 24 da folha
296
+ * dele -, e a árvore ganhava um nó chamado `dark` que não existe em marcação nenhuma.
297
+ *
298
+ * `.dark` é COMO o projeto diz escuro, não onde um elemento mora. Tirado dos nomes antes de
299
+ * decidir alvo e pai; o `isDark` abaixo continua lendo o seletor inteiro e mandando o bloco
300
+ * para `dark`, que é onde ele pertence.
301
+ */
302
+ const scheme = DARK.test(selector);
303
+ const names = classesIn(selector).filter((n) => !(scheme && (n === "dark" || n === "light")));
278
304
  if (names.length === 0) {
279
305
  // An element or `:root` selector styles something this reader cannot attach to
280
306
  // a component. Said out loud rather than dropped.
@@ -283,6 +309,9 @@ export function readModuleCss(css) {
283
309
  }
284
310
  const target = names[names.length - 1];
285
311
  const entry = out.classes[target] ?? emptyClass();
312
+ /** Alvo sozinho no seletor: é regra dela, e não regra dentro de outra. Ver `ModuleClass.own`. */
313
+ if (names.length === 1)
314
+ entry.own = true;
286
315
  out.classes[target] = entry;
287
316
  // A descendant selector records the RELATION as well as the styles: `.panel
288
317
  // .title` says title lives inside panel, which is the anatomy for free.
@@ -296,7 +325,7 @@ export function readModuleCss(css) {
296
325
  const block = declarations(rule.body);
297
326
  if (Object.keys(block).length === 0)
298
327
  continue;
299
- const isDark = DARK.test(selector);
328
+ const isDark = scheme;
300
329
  const state = STATE_OF.find(([re]) => re.test(selector))?.[1] ?? null;
301
330
  const width = minWidthOf(rule.media);
302
331
  if (width) {
@@ -526,7 +555,7 @@ export function transcribeModule(read, usage, declared) {
526
555
  const resolve = (block) => {
527
556
  const out = {};
528
557
  for (const [prop, value] of Object.entries(block)) {
529
- out[prop] = value.replace(/var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)/g, (whole, name) => declared.has(name) ? tokenRefFor(name) : whole);
558
+ out[prop] = value.replace(/var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)/g, (whole, name) => declared.has(name) ? tokenRefFor(name, declared) : whole);
530
559
  }
531
560
  return out;
532
561
  };
@@ -645,44 +674,6 @@ function formFor(tag) {
645
674
  * A declared custom property as a token ref in the document's namespace. Their name is
646
675
  * the path, which is "your names travel unchanged" one level deeper.
647
676
  */
648
- function tokenRefFor(name) {
649
- const bare = name.replace(/^--/, "");
650
- if (bare.startsWith("color-")) {
651
- const rest = bare.slice("color-".length);
652
- const m = /^(.*)-(\d{2,4})$/.exec(rest);
653
- return m ? `{color.${m[1]}.${m[2]}}` : `{color.${rest}}`;
654
- }
655
- if (bare.startsWith("radius-"))
656
- return `{radius.${bare.slice(7)}}`;
657
- if (bare.startsWith("spacing-"))
658
- return `{spacing.${bare.slice(8)}}`;
659
- if (bare.startsWith("shadow-"))
660
- return `{shadow.${bare.slice(7)}}`;
661
- if (bare.startsWith("background-image-gradient-")) {
662
- return `{gradients.${bare.slice("background-image-gradient-".length)}}`;
663
- }
664
- if (bare.startsWith("gradient-"))
665
- return `{gradients.${bare.slice(9)}}`;
666
- if (bare.startsWith("text-")) {
667
- // The companion token: `--text-body-s--line-height` names the line-height OF a
668
- // step, and a double dash inside a ref is a grammar nothing accepts (test13).
669
- const companion = /^text-(.+?)--(line-height|letter-spacing|font-weight)$/.exec(bare);
670
- if (companion) {
671
- const prop = {
672
- "line-height": "lineHeight",
673
- "letter-spacing": "letterSpacing",
674
- "font-weight": "weight",
675
- }[companion[2]];
676
- return `{typography.scale.${companion[1]}.${prop}}`;
677
- }
678
- if (bare.slice(5).includes("--"))
679
- return `var(${name})`;
680
- return `{typography.scale.${bare.slice(5)}.fontSize}`;
681
- }
682
- if (bare.startsWith("font-"))
683
- return `{typography.families.${bare.slice(5)}}`;
684
- return `var(${name})`;
685
- }
686
677
  /**
687
678
  * AS REGRAS DE CLASSE DE UMA FOLHA GLOBAL, prontas para o componente que as veste.
688
679
  *
@@ -700,21 +691,60 @@ function tokenRefFor(name) {
700
691
  * descendente (`.a .b` - o contexto é do pai) e seletor composto, que `readModuleCss`
701
692
  * já reporta em `unslotted`.
702
693
  */
694
+ /**
695
+ * `@utility x { … }` LIDA COMO `.x { … }` - a porta que o Tailwind v4 abriu, e nada mais.
696
+ *
697
+ * O QUE O CLIENTE PERDIA: as utilities ASSINATURA dele. No `codelevel-ui` são 23, e entre elas
698
+ * `text-grad`, `text-grad-fire`, `text-grad-xp` e `text-grad-gold` - os gradientes de texto que dão
699
+ * a cara do produto. O componente que escreve `text-grad` viajava sem o gradiente e o título dele
700
+ * chegava em cor lisa; das 198 declarações do `globals.css` que não alcançavam receita nenhuma, é
701
+ * aqui que a maior parte morava.
702
+ *
703
+ * É NORMALIZAÇÃO DE SINTAXE, E NÃO MOTOR NOVO. Uma `@utility` é uma classe com outro nome de porta:
704
+ * mesmo corpo, mesmas declarações, mesmo cascade. A máquina que resolve o `var()` dele para o token
705
+ * que ELE nomeou, que lê estado e `dark`, e que veste a classe na raiz do componente já existe
706
+ * inteira - só não conhecia a porta.
707
+ *
708
+ * MEDIDO EM TRÊS POPULAÇÕES: `codelevel-ui` 23 em 6 folhas, `synthesisui/web` 7 em 1 folha
709
+ * (`bg-ember`, `text-ember`, `mask-fade-x`…), `frontend-hub` 0 em 722 - ele é CSS Modules e não usa
710
+ * a sintaxe. Duas populações independentes com a forma, e uma sem, que é o que prova que a régua não
711
+ * assume o formato de um repositório só.
712
+ *
713
+ * A FUNCIONAL FICA DE FORA, e é o único caso: `@utility tab-* { tab-size: --value(integer) }` tem
714
+ * molde no lugar do nome, então não há classe fixa que uma marcação possa vestir. Nenhuma das 30
715
+ * medidas é funcional, e quando uma for, ela cai nas sobras com arquivo e linha, que é a resposta
716
+ * honesta.
717
+ */
718
+ function asClasses(body) {
719
+ return body.replace(/@utility\s+([a-zA-Z][\w-]*)(\s*\{)/g, (_whole, name, brace) => `.${name}${brace}`);
720
+ }
703
721
  export function readGlobalClasses(globals, declared) {
704
722
  const resolve = (block) => {
705
723
  const out = {};
706
724
  for (const [prop, value] of Object.entries(block)) {
707
- out[prop] = value.replace(/var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)/g, (whole, name) => declared.has(name) ? tokenRefFor(name) : whole);
725
+ out[prop] = value.replace(/var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)/g, (whole, name) => declared.has(name) ? tokenRefFor(name, declared) : whole);
708
726
  }
709
727
  return out;
710
728
  };
711
729
  const out = new Map();
712
730
  for (const sheet of globals) {
713
- const read = readModuleCss(sheet.body);
731
+ const read = readModuleCss(asClasses(sheet.body));
714
732
  const dependent = new Set(Object.values(read.classes).flatMap((c) => c.children));
715
733
  for (const [name, cls] of Object.entries(read.classes)) {
716
- /** O filho de `.a .b` só existe naquele contexto - vesti-lo solto pintaria errado. */
717
- if (dependent.has(name))
734
+ /**
735
+ * O filho de `.a .b` só existe naquele contexto - vesti-lo solto pintaria errado. MAS uma
736
+ * classe que também tem REGRA PRÓPRIA não é um filho: é uma classe com variação contextual, e
737
+ * descartá-la joga fora a regra base junto.
738
+ *
739
+ * O QUE ISSO CUSTAVA: cinco das 24 classes da folha dele, entre elas `stroke-text` e
740
+ * `holo-card` - o contorno de texto e a borda holográfica. A causa era uma linha de tema,
741
+ * `html.dark .stroke-text { … }`, que fazia a classe aparecer como filha em algum lugar.
742
+ *
743
+ * O PADRÃO É UNIVERSAL, e é isso que torna o conserto necessário e não específico: `html.dark
744
+ * .x` é como se escreve tema em CSS e `.parent:hover .child` é como se escreve estado. Medido
745
+ * em duas folhas independentes - 5 de 24 na dele, 2 de 22 na nossa.
746
+ */
747
+ if (dependent.has(name) && !cls.own)
718
748
  continue;
719
749
  const states = {};
720
750
  for (const [state, block] of Object.entries(cls.states))
@@ -51,7 +51,50 @@ export function tierOf(file) {
51
51
  return undefined;
52
52
  }
53
53
  /** `export function X`, `export default function X`, `export const X =`. */
54
- const EXPORTED = /export\s+(?:default\s+)?(?:async\s+)?(?:function\s+([A-Z][A-Za-z0-9_]*)|const\s+([A-Z][A-Za-z0-9_]*)\s*[:=])/g;
54
+ const EXPORTED = /export\s+(?:default\s+)?(?:async\s+)?(?:function\s+([A-Z][A-Za-z0-9_]*)|const\s+([A-Z][A-Za-z0-9_]*)\s*[:=]|class\s+([A-Z][A-Za-z0-9_]*))/g;
55
+ /**
56
+ * O `export default` QUE NOMEIA A PEÇA EM OUTRA LINHA - e é assim que metade da indústria escreve.
57
+ *
58
+ * O QUE O CLIENTE PERDIA: 108 componentes do dashboard dele, o kit de mídia inteiro entre eles
59
+ * (`Media`, `MediaHeader`, `MediaTitle`, `MediaFooter`, `MediaDescription`), mais o `SelectItem` e o
60
+ * `CurateContentSection`. Nenhum no censo, nenhum aviso: o leitor exigia a palavra `export` na MESMA
61
+ * linha da declaração. `export default function Media()` era lido, e
62
+ * `function Media() {…}` + `export default Media;` no fim do arquivo, não - a mesma peça, com a
63
+ * exportação numa linha própria.
64
+ *
65
+ * MEDIDO EM QUATRO POPULAÇÕES, e é o que decide o que cobrir:
66
+ *
67
+ * forma codelevel fh/ui fh/dashboard synthesisui
68
+ * export default function Name 0 0 519 73
69
+ * export default Name; 0 0 110 0
70
+ * export default memo/forwardRef(…) 0 0 1 0
71
+ * export { Name as default } 0 0 0 0
72
+ * export default class Name 0 0 0 0
73
+ * export default () => / function() 0 0 0 0
74
+ *
75
+ * As formas com zero entram de propósito: são o que a LINGUAGEM oferece, e uma régua que só cobre a
76
+ * forma do repositório que eu tenho na mão é uma régua que quebra no próximo cliente.
77
+ *
78
+ * O ANÔNIMO FICA DE FORA, declarado: em `export default () => …` não existe nome para ler. O único
79
+ * candidato seria o nome do arquivo, e batizar peça alheia é supor - o que este censo não faz.
80
+ */
81
+ const DEFAULT_NAMED = [
82
+ /** `export default Media;` e `export default memo(forwardRef(Media))`, com ou sem `React.` */
83
+ /export\s+default\s+(?:(?:React\.)?(?:memo|forwardRef)\s*\(\s*)*([A-Z][A-Za-z0-9_]*)/g,
84
+ /** `export { Media as default }` - a forma canônica do ES, e ela aceita vizinhos na mesma chave */
85
+ /export\s*\{[^}]*?\b([A-Z][A-Za-z0-9_]*)\s+as\s+default\b/g,
86
+ ];
87
+ /**
88
+ * O NOME PRECISA SER DECLARADO AQUI, e é isto que separa uma definição de um repasse.
89
+ *
90
+ * `import Button from "@acme/ui"; export default Button;` é um re-export: a peça é de outra pessoa e
91
+ * reivindicá-la faria o censo listar como dele um componente que ele só encaminha. A pergunta é se
92
+ * ESTE arquivo constrói o nome - `function X`, `const X =`, `class X`.
93
+ */
94
+ function declaresName(source, name) {
95
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
96
+ return new RegExp(`(?:^|\\n)\\s*(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?(?:function|class|const|let|var)\\s+${escaped}\\b`).test(source);
97
+ }
55
98
  /** `type ButtonProps = { … }` or `interface ButtonProps { … }`, to its closing
56
99
  * brace, wherever it sits.
57
100
  *
@@ -102,8 +145,21 @@ export function scanDefinitions(file, source) {
102
145
  const out = [];
103
146
  const seen = new Set();
104
147
  EXPORTED.lastIndex = 0;
105
- for (const m of source.matchAll(EXPORTED)) {
106
- const name = m[1] ?? m[2];
148
+ /**
149
+ * A EXPORTAÇÃO EM LINHA PRÓPRIA ENTRA NA MESMA FILA - ver `DEFAULT_NAMED`.
150
+ *
151
+ * Reunidas antes do laço para que a deduplicação por `seen` valha para as duas origens: um
152
+ * arquivo que escreve `export default function Card()` casa nos dois padrões e a peça é uma só.
153
+ */
154
+ const names = [...source.matchAll(EXPORTED)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
155
+ for (const rx of DEFAULT_NAMED) {
156
+ rx.lastIndex = 0;
157
+ for (const m of source.matchAll(rx)) {
158
+ if (m[1] && declaresName(source, m[1]))
159
+ names.push(m[1]);
160
+ }
161
+ }
162
+ for (const name of names) {
107
163
  if (!name || seen.has(name))
108
164
  continue;
109
165
  seen.add(name);
@@ -42,6 +42,7 @@
42
42
  * hover:shadow-md → states.hover
43
43
  * data-[checked]:bg-ocean-50 → states.checked
44
44
  */
45
+ import { tokenRefFor } from "../token-ref.js";
45
46
  /**
46
47
  * Utility prefix → the property name THE DOCUMENT USES, which is camelCase.
47
48
  *
@@ -357,10 +358,29 @@ const FONT_WEIGHT = {
357
358
  black: "900",
358
359
  };
359
360
  /** The three family slots the document actually has. Nothing else may be a ref. */
361
+ /**
362
+ * O NOME DELE JÁ É O SLOT, e por isso esta tabela tem seis linhas e não três.
363
+ *
364
+ * O QUE O CLIENTE VIA: `--font-display: "Bricolage Grotesque"` declarado no `@theme`, a utility
365
+ * `font-display` escrita no `ModalHeader` e no `Typography` inteiro, e a leitura devolvendo nada -
366
+ * então o título dele chega no Studio na fonte do corpo (`codelevel-ui`, 23/08).
367
+ *
368
+ * As três primeiras traduzem o vocabulário da TAILWIND para o do documento (`font-sans` é o corpo).
369
+ * As duas últimas são o vocabulário do documento dito por ELE: quem declara `--font-display` não
370
+ * está pedindo tradução nenhuma, está nomeando o slot. Recusar isso era exigir que ele chamasse a
371
+ * fonte de display de `serif` para ser ouvido.
372
+ *
373
+ * E SÓ OS NOMES LITERAIS DO SLOT - `heading` fica de fora, e a tentação de incluí-lo é o erro que
374
+ * esta tabela evita. Um projeto pode declarar `--font-heading` E `--font-display` com fontes
375
+ * diferentes; mapear sinônimo colapsaria as duas em uma e apagaria uma decisão que ele escreveu.
376
+ * `font-heading` continua não lida, com o motivo no relatório, que é a resposta honesta.
377
+ */
360
378
  const FAMILY_SLOT = {
361
379
  sans: "body",
362
380
  serif: "display",
363
381
  mono: "mono",
382
+ display: "display",
383
+ body: "body",
364
384
  };
365
385
  /** Type utilities with no scale behind them - a fact, not a decision deferred. */
366
386
  const TYPE_KEYWORD = {
@@ -1170,8 +1190,67 @@ function readUtilityCore(utility, declared) {
1170
1190
  if (RADIUS[rest])
1171
1191
  return { property: "borderRadius", value: RADIUS[rest] };
1172
1192
  }
1193
+ /**
1194
+ * A UTILITY QUE O `@theme` DELE CRIOU - o último recurso, e é mecanismo, não caso.
1195
+ *
1196
+ * O QUE O CLIENTE VÊ SEM ISTO: o título do `ModalHeader` dele usa `font-display` e
1197
+ * `tracking-tightest`, os dois declarados no `@theme` como `--font-display: "Bricolage
1198
+ * Grotesque"` e `--tracking-tightest: -0.06em`. A leitura devolvia "não resolve nada" para
1199
+ * ambos, então o título chega no Studio na fonte do corpo e sem o aperto de letra que ele
1200
+ * escolheu - e o relatório afirma que a decisão não existe, com ela escrita no código
1201
+ * (`codelevel-ui`, 23/08).
1202
+ *
1203
+ * NO TAILWIND V4 TODO TOKEN DE UM NAMESPACE DO `@theme` CRIA A UTILITY DE MESMO NOME. As tabelas
1204
+ * acima conhecem as escalas que a Tailwind PUBLICA - `tracking-tight`, `text-2xl` -, então toda
1205
+ * escala própria de todo cliente caía neste buraco. A régua aqui não cita um nome de token: ela
1206
+ * pergunta se ELE declarou aquele nome.
1207
+ *
1208
+ * DEPOIS DE TUDO, e não antes: `tracking-tight` continua vindo da tabela fixa, e nenhuma leitura
1209
+ * que já funcionava muda de resposta. Medido nas duas populações: 2 classes resgatadas de 163
1210
+ * não lidas no `codelevel-ui`, 0 de 837 no `frontend-hub`, que não declara namespace próprio.
1211
+ *
1212
+ * SÓ OS NAMESPACES DE ATRIBUIÇÃO DIRETA. `--blur-*` e `--perspective-*` viram
1213
+ * `filter: blur(…)`/`transform`, e escrever a propriedade crua com o valor dentro produziria CSS
1214
+ * inválido - pior que não ler. Ver `tokenRefFor` para como o valor viaja: com ref de documento
1215
+ * onde existe grammar (`{typography.families.display}`) e como `var()` onde não existe, que é
1216
+ * CSS correto contra a folha dele.
1217
+ */
1218
+ const themeProperty = THEME_NAMESPACE[prefix];
1219
+ if (themeProperty && rest) {
1220
+ const own = `--${prefix}-${rest}`;
1221
+ if (declared.has(own)) {
1222
+ return {
1223
+ property: themeProperty,
1224
+ value: tokenRefFor(own, declared),
1225
+ token: own,
1226
+ };
1227
+ }
1228
+ }
1173
1229
  return null;
1174
1230
  }
1231
+ /**
1232
+ * Namespace do `@theme` → a propriedade CSS que a utility de mesmo nome escreve.
1233
+ *
1234
+ * Só atribuição direta: cada uma destas escreve `<propriedade>: var(--<namespace>-<nome>)` e nada
1235
+ * mais. `--blur-*` e `--perspective-*` ficam fora porque viram `filter: blur(…)`/`transform`, e a
1236
+ * propriedade crua com o valor dentro seria CSS inválido - pior que não ler.
1237
+ *
1238
+ * QUATRO LINHAS, E NÃO SETE. `text`, `shadow` e `animate` estavam aqui e foram REMOVIDOS depois de
1239
+ * medidos: os ramos acima já os resolvem, então a linha nunca era alcançada. Uma entrada que nada
1240
+ * alcança é a mesma dívida que os specs de alcance existem para impedir - conferido com
1241
+ * `--text-hero`, `--shadow-lifted` e `--animate-drift`, que continuam lidos sem elas.
1242
+ */
1243
+ const THEME_NAMESPACE = {
1244
+ /**
1245
+ * `font` NÃO ENTRA AQUI de propósito: o ramo de `font` acima decide sozinho e retorna, então uma
1246
+ * linha para ele seria código que nada alcança. A família dele é resolvida por `FAMILY_SLOT`,
1247
+ * que é onde o slot do documento mora.
1248
+ */
1249
+ tracking: "letterSpacing",
1250
+ leading: "lineHeight",
1251
+ ease: "transitionTimingFunction",
1252
+ aspect: "aspectRatio",
1253
+ };
1175
1254
  /**
1176
1255
  * A colour token ref in the document's own spelling.
1177
1256
  *
@@ -1236,7 +1315,7 @@ function resolveArbitrary(inner, declared) {
1236
1315
  if (raw.startsWith("--")) {
1237
1316
  const name = raw.split(",")[0].trim();
1238
1317
  return declared.has(name)
1239
- ? { value: tokenRefFor(name), token: name }
1318
+ ? { value: tokenRefFor(name, declared), token: name }
1240
1319
  : null;
1241
1320
  }
1242
1321
  // A literal in brackets is still a real value somebody typed.
@@ -1244,7 +1323,7 @@ function resolveArbitrary(inner, declared) {
1244
1323
  }
1245
1324
  const name = v[1];
1246
1325
  if (declared.has(name))
1247
- return { value: tokenRefFor(name), token: name };
1326
+ return { value: tokenRefFor(name, declared), token: name };
1248
1327
  /**
1249
1328
  * A `var()` THEY NEVER DECLARED, WHEN THEY DECLARED IT UNDER ITS NAMESPACE.
1250
1329
  *
@@ -1264,7 +1343,7 @@ function resolveArbitrary(inner, declared) {
1264
1343
  */
1265
1344
  const meant = MEANT_NAMESPACES.map((ns) => `--${ns}-${name.slice(2)}`).find((candidate) => declared.has(candidate));
1266
1345
  if (meant)
1267
- return { value: tokenRefFor(meant), token: meant };
1346
+ return { value: tokenRefFor(meant, declared), token: meant };
1268
1347
  return { value: raw };
1269
1348
  }
1270
1349
  /**
@@ -1288,53 +1367,6 @@ const MEANT_NAMESPACES = [
1288
1367
  * `--text-body-s` → `{typography.scale.body-s.fontSize}`. Their name is the path,
1289
1368
  * which is the whole "your names travel unchanged" promise applied one level deeper.
1290
1369
  */
1291
- function tokenRefFor(name) {
1292
- const bare = name.replace(/^--/, "");
1293
- if (bare.startsWith("color-"))
1294
- return refFor(bare.slice("color-".length));
1295
- if (bare.startsWith("radius-"))
1296
- return `{radius.${bare.slice(7)}}`;
1297
- if (bare.startsWith("spacing-"))
1298
- return `{spacing.${bare.slice(8)}}`;
1299
- if (bare.startsWith("shadow-"))
1300
- return `{shadow.${bare.slice(7)}}`;
1301
- // `--gradient-ui` and `--background-image-gradient-ui` are one token in two
1302
- // spellings - Tailwind v4's utility namespace wraps the first. Both reach
1303
- // `{gradients.ui}`, which is where the census files them.
1304
- if (bare.startsWith("background-image-gradient-")) {
1305
- return `{gradients.${bare.slice("background-image-gradient-".length)}}`;
1306
- }
1307
- if (bare.startsWith("gradient-"))
1308
- return `{gradients.${bare.slice(9)}}`;
1309
- if (bare.startsWith("text-")) {
1310
- /**
1311
- * THE COMPANION TOKEN. Tailwind v4 spells "the line-height OF text-body-s" as
1312
- * `--text-body-s--line-height` - a double dash inside one name. Read as a step name
1313
- * it produced `{typography.scale.body-s--line-height.fontSize}`, whose double dash
1314
- * no ref grammar accepts, and the whole recipe was REFUSED at validation (test13,
1315
- * 01/08). The suffix names the property; the middle names the step.
1316
- */
1317
- const companion = /^text-(.+?)--(line-height|letter-spacing|font-weight)$/.exec(bare);
1318
- if (companion) {
1319
- const prop = {
1320
- "line-height": "lineHeight",
1321
- "letter-spacing": "letterSpacing",
1322
- "font-weight": "weight",
1323
- }[companion[2]];
1324
- return `{typography.scale.${companion[1]}.${prop}}`;
1325
- }
1326
- // Any other double dash is a name this grammar cannot hold - the literal var()
1327
- // still resolves against their own stylesheet, and an invalid ref helps nobody.
1328
- if (bare.slice(5).includes("--"))
1329
- return `var(${name})`;
1330
- return `{typography.scale.${bare.slice(5)}.fontSize}`;
1331
- }
1332
- if (bare.startsWith("font-"))
1333
- return `{typography.families.${bare.slice(5)}}`;
1334
- // A namespace we do not model. The literal `var()` is still correct CSS against
1335
- // their own stylesheet, and inventing a ref would point at nothing.
1336
- return `var(${name})`;
1337
- }
1338
1370
  function refFor(name) {
1339
1371
  const m = /^(.*)-(\d{2,4})$/.exec(name);
1340
1372
  if (!m)
@@ -30,6 +30,7 @@
30
30
  * property resolve in their app by position in the string; a recipe resolves by
31
31
  * position in `layers`. They agree only if the reader inserts in the order it read.
32
32
  */
33
+ import { wearGlobal, withGlobal } from "../global-wear.js";
33
34
  import { parseClass, transcribe } from "./transcribe.js";
34
35
  /**
35
36
  * A Tailwind modifier, mapped to the condition it IS.
@@ -136,8 +137,7 @@ export function conditionOf(modifiers) {
136
137
  * condição do DOM dele, escrita por extenso - descartá-la porque a nossa tabela não a lista
137
138
  * seria a tabela decidindo o que o código dele pode dizer. `lost` fica para o que não dá para
138
139
  * ler, não para o que a gente não previu.
139
- */
140
- else if (nome)
140
+ */ else if (nome)
141
141
  out.state = camel(nome);
142
142
  else
143
143
  out.lost = true;
@@ -954,7 +954,19 @@ export function readClassTernaries(source) {
954
954
  * `declared` is their own custom properties, so `bg-ocean-950` comes out as a ref to
955
955
  * the token they declared rather than as a hex we looked up.
956
956
  */
957
- export function transcribeVariants(source, declared) {
957
+ export function transcribeVariants(source, declared,
958
+ /**
959
+ * AS CLASSES QUE A FOLHA GLOBAL DELE DECLARA - obrigatório, e por um motivo medido.
960
+ *
961
+ * O import vestia a regra global na RAIZ do componente e só ali. Uma utility escrita dentro da
962
+ * TABELA DE VARIANTES - que é onde o `Typography` do `codelevel-ui` põe `text-grad-fire` - vinha
963
+ * por aqui, e este caminho não conhecia folha nenhuma: o título dele saía em cor lisa, e nada
964
+ * dizia que o gradiente tinha sido perdido.
965
+ *
966
+ * Sem default de propósito. Um `Map` vazio é a resposta de quem não tem folha global, e ela se
967
+ * escreve em voz alta; um parâmetro que se pode esquecer é o defeito que isto está desfazendo.
968
+ */
969
+ globals) {
958
970
  const reads = readVariants(source);
959
971
  const fromRecords = {};
960
972
  const unslotted = [];
@@ -1048,7 +1060,21 @@ export function transcribeVariants(source, declared) {
1048
1060
  */
1049
1061
  const bare = layer.classes.map((cls) => parseClass(cls).utility);
1050
1062
  const t = transcribe(bare, declared);
1051
- const style = { ...t.base, ...t.dark };
1063
+ /**
1064
+ * A REGRA GLOBAL QUE ESTA CAMADA VESTE - ver `wearGlobal`.
1065
+ *
1066
+ * Antes da transcrição no objeto final, e não depois: uma utility da folha dele e uma utility
1067
+ * da Tailwind pintando a mesma propriedade resolvem pelo cascade, e o cascade diz que a
1068
+ * última classe escrita ganha. `transcribe` já respeita essa ordem entre as dela; aqui a
1069
+ * folha entra como a camada de baixo, que é onde o CSS a coloca.
1070
+ */
1071
+ const wornHere = wearGlobal(bare, globals);
1072
+ const style = {
1073
+ ...wornHere.base,
1074
+ ...wornHere.dark,
1075
+ ...t.base,
1076
+ ...t.dark,
1077
+ };
1052
1078
  return {
1053
1079
  ...(layer.when ? { when: layer.when } : {}),
1054
1080
  ...(layer.at ? { at: layer.at } : {}),
@@ -1063,7 +1089,7 @@ export function transcribeVariants(source, declared) {
1063
1089
  defaults,
1064
1090
  layers,
1065
1091
  raw: allLayers,
1066
- base: transcribe(baseClasses, declared),
1092
+ base: withGlobal(transcribe(baseClasses, declared), baseClasses, globals),
1067
1093
  unslotted,
1068
1094
  notes,
1069
1095
  };
@@ -0,0 +1,44 @@
1
+ import { parseClass } from "./doctor/transcribe.js";
2
+ export function wearGlobal(classes, globals,
3
+ /** Registra quais classes a folha global reivindicou, para o juiz de duplicidade. */
4
+ claimed) {
5
+ const worn = { base: {}, dark: {}, states: {} };
6
+ for (const cls of classes) {
7
+ const rule = globals.get(cls);
8
+ if (!rule)
9
+ continue;
10
+ claimed?.add(cls);
11
+ Object.assign(worn.base, rule.base);
12
+ Object.assign(worn.dark, rule.dark);
13
+ for (const [state, block] of Object.entries(rule.states))
14
+ worn.states[state] = { ...worn.states[state], ...block };
15
+ }
16
+ return worn;
17
+ }
18
+ /** Tem alguma declaração? Uma camada vazia não vira camada. */
19
+ export function wornAnything(worn) {
20
+ return (Object.keys(worn.base).length > 0 ||
21
+ Object.keys(worn.dark).length > 0 ||
22
+ Object.keys(worn.states).length > 0);
23
+ }
24
+ /**
25
+ * A TRANSCRIÇÃO COM A FOLHA GLOBAL DELE POR BAIXO.
26
+ *
27
+ * A folha é a camada de baixo porque é onde o CSS a coloca: uma utility da Tailwind escrita no mesmo
28
+ * elemento ganha da classe da folha quando as duas pintam a mesma propriedade.
29
+ */
30
+ export function withGlobal(t, classes,
31
+ /** Ausente é a resposta de quem não tem folha global: a transcrição volta intacta. */
32
+ globals) {
33
+ if (!globals || globals.size === 0)
34
+ return t;
35
+ const worn = wearGlobal(classes.map((cls) => parseClass(cls).utility), globals);
36
+ if (!wornAnything(worn))
37
+ return t;
38
+ return {
39
+ ...t,
40
+ base: { ...worn.base, ...t.base },
41
+ dark: { ...worn.dark, ...t.dark },
42
+ states: Object.fromEntries([...new Set([...Object.keys(worn.states), ...Object.keys(t.states)])].map((k) => [k, { ...worn.states[k], ...t.states[k] }])),
43
+ };
44
+ }
@@ -253,7 +253,7 @@ export const CHECKER_SINCE = "0.16.250";
253
253
  * publicado antes deste código existir, e uma marca nele calaria o aviso para quem o instalou. Mesma
254
254
  * lição de algumas horas antes, na mesma sessão.
255
255
  */
256
- export const READER_SINCE = "0.16.274";
256
+ export const READER_SINCE = "0.16.288";
257
257
  /**
258
258
  * O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
259
259
  *
@@ -0,0 +1,80 @@
1
+ /**
2
+ * `Modal.Header` E `ModalHeader` SÃO UMA PEÇA - e a vitrine dele mostrava duas.
3
+ *
4
+ * O QUE O CLIENTE VIA: o design system dele contando 75 componentes quando ele escreveu 68. Sete
5
+ * pares apareciam duas vezes - `Modal.Header` + `ModalHeader`, `Modal.Body` + `ModalBody`,
6
+ * `Modal.Footer` + `ModalFooter`, `Toast.Icon` + `ToastIcon`, `Toast.Body` + `ToastBody`,
7
+ * `Tooltip.Content` + `TooltipContent`, `PillNav.Link` + `PillNavLink` - porque as duas grafias são
8
+ * a mesma coisa dita de dois jeitos: `export const ModalHeader` é a DECLARAÇÃO, e
9
+ * `Modal.Header = ModalHeader` é o açúcar de namespace que o React deixa escrever.
10
+ *
11
+ * A DECLARAÇÃO GANHA, e é a mesma regra que o resto do censo já segue: uso é o que alguém escolheu,
12
+ * declaração é o que o autor desenhou. O nome que sobrevive é o que ele exportou.
13
+ *
14
+ * MEDIDO NAS DUAS POPULAÇÕES, e a segunda é a que prova que a régua não é sobre um repositório:
15
+ *
16
+ * codelevel-ui 75 no inventário · 11 nomes com ponto · 7 pares
17
+ * frontend-hub 302 no inventário · 23 nomes com ponto · 0 pares
18
+ *
19
+ * No `frontend-hub` `Card.Root` e `Icon.ChevronUp` não têm um `CardRoot` nem um `IconChevronUp`
20
+ * declarado ao lado - são namespaces de verdade, e continuam como estão. É a EXISTÊNCIA da
21
+ * declaração plana que faz o par, nunca a grafia com ponto por si.
22
+ */
23
+ export function mergeNamespacePairs(components,
24
+ /** O que este escopo DECLARA. Sem isso, um par não pode ser provado - ver `Census.defined`. */
25
+ declared) {
26
+ const byName = new Map(components.map((c) => [c.name, c]));
27
+ const absorbed = new Set();
28
+ const out = [];
29
+ for (const c of components) {
30
+ if (absorbed.has(c.name))
31
+ continue;
32
+ if (!c.name.includes(".")) {
33
+ out.push(c);
34
+ continue;
35
+ }
36
+ const flat = c.name.split(".").join("");
37
+ const twin = byName.get(flat);
38
+ /** Só quando o nome plano EXISTE no inventário e é declarado por este escopo. */
39
+ if (!twin || !declared.has(flat)) {
40
+ out.push(c);
41
+ continue;
42
+ }
43
+ absorbed.add(c.name);
44
+ absorbed.add(flat);
45
+ out.push(fuse(twin, c));
46
+ }
47
+ /** Os planos que foram absorvidos já entraram fundidos; os outros seguem na ordem. */
48
+ return out;
49
+ }
50
+ /**
51
+ * As duas linhas somadas sob o nome declarado.
52
+ *
53
+ * `count` soma - são usos distintos no código dele. `files` fica no MAIOR dos dois em vez de somar:
54
+ * as duas grafias costumam aparecer nos mesmos arquivos, e somar diria que o componente vive em mais
55
+ * lugares do que vive. Um número inflado é pior que um número conservador, porque ele decide
56
+ * prioridade.
57
+ */
58
+ function fuse(flat, dotted) {
59
+ const props = { ...flat.props };
60
+ for (const [p, values] of Object.entries(dotted.props ?? {})) {
61
+ props[p] = [...new Set([...(props[p] ?? []), ...values])];
62
+ }
63
+ const propFiles = { ...(flat.propFiles ?? {}) };
64
+ for (const [p, n] of Object.entries(dotted.propFiles ?? {})) {
65
+ propFiles[p] = Math.max(propFiles[p] ?? 0, n);
66
+ }
67
+ return {
68
+ ...flat,
69
+ count: flat.count + dotted.count,
70
+ files: Math.max(flat.files, dotted.files),
71
+ props,
72
+ ...(Object.keys(propFiles).length > 0 ? { propFiles } : {}),
73
+ /** Os eixos que qualquer uma das duas grafias declarou - é a mesma declaração. */
74
+ ...(flat.declaredAxes || dotted.declaredAxes
75
+ ? {
76
+ declaredAxes: { ...dotted.declaredAxes, ...flat.declaredAxes },
77
+ }
78
+ : {}),
79
+ };
80
+ }