synthesisui 0.16.286 → 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.
@@ -32,6 +32,7 @@
32
32
  import { readSxProps } from "./doctor/style-props.js";
33
33
  import { transcribe, } from "./doctor/transcribe.js";
34
34
  import { frontierKind, packageRoot } from "./frontier-kind.js";
35
+ import { withGlobal } from "./global-wear.js";
35
36
  /** The ten forms, closed. A renderer that accepts any tag draws anything. */
36
37
  const FORMS = new Set([
37
38
  "image",
@@ -252,7 +253,18 @@ sketch,
252
253
  * A escala de espaçamento que o projeto DELES declara para o `sx`. Ausente = o default
253
254
  * documentado do MUI, que é fato da biblioteca que eles escolheram - ver `spacingOf`.
254
255
  */
255
- sxSpacing) {
256
+ sxSpacing,
257
+ /**
258
+ * AS CLASSES DA FOLHA GLOBAL DELE - ver `Census.globalClasses` e `wearGlobal`.
259
+ *
260
+ * O QUE ISSO ALCANÇA QUE NADA MAIS ALCANÇAVA: uma utility que ele escreve num NÓ FILHO. O
261
+ * `LeaderboardRow` do `codelevel-ui` põe `text-grad-xp` num `<span>` dentro de um ternário, e o
262
+ * `QuestItem` põe `text-grad` numa tabela que pertence ao filho - as duas passam por aqui, e não
263
+ * pela raiz nem pela tabela de variantes do componente.
264
+ *
265
+ * Ausente é a resposta de quem não tem folha global, e nesse caso nada muda.
266
+ */
267
+ globals) {
256
268
  /**
257
269
  * THE PACKAGE THE RETURNED ELEMENT CAME FROM, measured rather than spelled.
258
270
  * Node 0 of the sketch IS what the component returns, and the reader stamped its
@@ -474,7 +486,7 @@ sxSpacing) {
474
486
  ? (sketch?.[node.at]?.classes ?? "")
475
487
  : "";
476
488
  const classes = raw.split(/\s+/).filter(Boolean);
477
- const t = transcribe(classes, declared);
489
+ const t = withGlobal(transcribe(classes, declared), classes, globals);
478
490
  /**
479
491
  * A PROP DE ESTILO DESTE NÓ, sob a classe dele.
480
492
  *
@@ -618,7 +630,7 @@ sxSpacing) {
618
630
  * otherwise. So a drawn root sends nothing here at all.
619
631
  */
620
632
  const rootStyle = rootPainted
621
- ? transcribe(rootPainted.split(/\s+/).filter(Boolean), declared)
633
+ ? withGlobal(transcribe(rootPainted.split(/\s+/).filter(Boolean), declared), rootPainted.split(/\s+/).filter(Boolean), globals)
622
634
  : null;
623
635
  const paints = rootStyle && Object.keys(rootStyle.base).length > 0
624
636
  ? { style: rootStyle.base }
@@ -710,13 +722,16 @@ sxSpacing) {
710
722
  * correctly, and the absence of a tree means the preview lays them out in a row
711
723
  * exactly as it did before.
712
724
  */
713
- export function resolveFlatParts(read, declared) {
725
+ export function resolveFlatParts(read, declared,
726
+ /** As classes da folha global dele - ver `wearGlobal`. */
727
+ globals) {
714
728
  const parts = {};
715
729
  for (const part of read) {
716
730
  const name = safePartName(String(part?.name ?? ""));
717
731
  if (!name || typeof part.classes !== "string")
718
732
  continue;
719
- const t = transcribe(part.classes.split(/\s+/).filter(Boolean), declared);
733
+ const partClasses = part.classes.split(/\s+/).filter(Boolean);
734
+ const t = withGlobal(transcribe(partClasses, declared), partClasses, globals);
720
735
  parts[name] = { base: t.base, dark: t.dark, states: t.states };
721
736
  }
722
737
  return { parts, tree: [], composes: [], external: [], notes: [], partAt: {} };
@@ -40,7 +40,9 @@ import { keyframesInSheets } from "../global-keyframes.js";
40
40
  import { withLibraryStructure } from "../library-structure.js";
41
41
  import { mergeCensus } from "../merge-census.js";
42
42
  import { claimName } from "../name-claim.js";
43
+ import { mergeNamespacePairs } from "../namespace-pairs.js";
43
44
  import { namingQueue } from "../naming-queue.js";
45
+ import { notExpressed } from "../not-expressed.js";
44
46
  import { body, paint, section } from "../output.js";
45
47
  import { outsideScope } from "../outside-scope.js";
46
48
  import { phase, startProgress } from "../progress.js";
@@ -367,6 +369,8 @@ function screenOf(rel) {
367
369
  const after = parts.slice(i + 1).filter((p) => !p.startsWith("("));
368
370
  return `${parts[i]}/${after.slice(0, 2).join("/")}`;
369
371
  }
372
+ /** Quantas classes da folha global viajam no censo. Ver o aviso em `takeCensus`. */
373
+ const MAX_GLOBAL_CLASSES = 300;
370
374
  export async function takeCensus(root, opts) {
371
375
  /**
372
376
  * O RELATÓRIO SÓ SAI QUANDO ALGUÉM O PEDIU - `say` cala quando o chamador é uma RE-medição.
@@ -450,6 +454,17 @@ export async function takeCensus(root, opts) {
450
454
  * porque é isso que o juiz do ledger precisa para chamar a classe e a regra de LIDAS.
451
455
  */
452
456
  const globalClasses = readGlobalClasses(globalSheets, declaredValues);
457
+ /**
458
+ * TETO DAS CLASSES GLOBAIS QUE VIAJAM NO CENSO - e quando ele corta, ele fala.
459
+ *
460
+ * Medido nas três populações: 24 classes na folha do `codelevel-ui`, 22 na nossa, 0 no
461
+ * `apps/web-dashboard` do repo real, que é CSS Modules. O teto é para a folha de um app que
462
+ * escreva utilitário por atacado, e existe porque um censo que dobra de tamanho em silêncio é o
463
+ * tipo de surpresa que ninguém pede.
464
+ */
465
+ if (globalClasses.size > MAX_GLOBAL_CLASSES) {
466
+ console.log(body(paint.faint(`${globalClasses.size} classes in your global sheet, and ${globalClasses.size - MAX_GLOBAL_CLASSES} of them are not travelling with this census - the first ${MAX_GLOBAL_CLASSES} are.`)));
467
+ }
453
468
  /**
454
469
  * OS KEYFRAMES DA FOLHA DELE, colhidos onde as folhas globais já estão na mão.
455
470
  *
@@ -735,16 +750,18 @@ export async function takeCensus(root, opts) {
735
750
  /**
736
751
  * Ver `CensusLook.aside` - o segundo componente do arquivo também.
737
752
  *
738
- * AQUI OS EIXOS NÃO SÃO CONHECIDOS: `transcribeVariants` roda para o primeiro
739
- * componente do arquivo, e este é o segundo. Passar `undefined` é dizer a verdade
740
- * ("não sei se declara"), e não uma omissão - e é por isso que o veto final, em
741
- * `exclusive-contract.ts`, pergunta pelos eixos em vez de confiar neste campo.
753
+ * E COM OS EIXOS QUE O TIPO DELE DECLARA. `transcribeVariants` roda para o primeiro
754
+ * componente do arquivo, então aqui não tabela de variantes - mas `extra.axes` é a
755
+ * declaração da interface, lida por `scanDefinitions` para TODA definição do arquivo.
756
+ * Era `undefined`, e isso pôs de lado o `ToastIcon` dele: terceiro componente do
757
+ * `Toast.tsx`, cinco tons declarados no tipo, um gradiente para cada. Ver
758
+ * `classifyAside`.
742
759
  */
743
- ...(classifyAside({ name: extra.name, declaredAxes: undefined })
760
+ ...(classifyAside({ name: extra.name, declaredAxes: extra.axes })
744
761
  ? {
745
762
  aside: classifyAside({
746
763
  name: extra.name,
747
- declaredAxes: undefined,
764
+ declaredAxes: extra.axes,
748
765
  }),
749
766
  }
750
767
  : {}),
@@ -782,7 +799,7 @@ export async function takeCensus(root, opts) {
782
799
  * for the result, which is the "valid and unread" failure this pipeline has
783
800
  * paid for twice already.
784
801
  */
785
- const v = transcribeVariants(src, declaredValues);
802
+ const v = transcribeVariants(src, declaredValues, globalClasses);
786
803
  /**
787
804
  * THE RESTING OPTION, from the definition itself. `cva` says it in
788
805
  * `defaultVariants`; a lookup-record component says it in the destructuring -
@@ -1040,14 +1057,20 @@ export async function takeCensus(root, opts) {
1040
1057
  /**
1041
1058
  * Ver `CensusLook.aside`: o fato viaja, a regra fica de um lado só.
1042
1059
  *
1043
- * `v.axes` é a DECLARAÇÃO de variação deste componente, e é ela que separa um glifo de
1044
- * um componente cujo nome parece com um. Ver `classifyAside`.
1060
+ * AS DUAS FONTES DE EIXO, e é a mesma decisão do autor nas duas. `found[0].axes` é o que
1061
+ * o TIPO declara (`interface ToastIconProps { tone?: "gold" | "cool" }`); `v.axes` é o
1062
+ * que a TABELA declara (`cva({ variants: … })`). Consultar só a segunda pôs de lado um
1063
+ * componente que pinta por `Record<tone, gradiente>` - forma que a tabela não vê.
1064
+ * Ver `classifyAside`.
1045
1065
  */
1046
- ...(classifyAside({ name: found[0].name, declaredAxes: v.axes })
1066
+ ...(classifyAside({
1067
+ name: found[0].name,
1068
+ declaredAxes: { ...found[0].axes, ...v.axes },
1069
+ })
1047
1070
  ? {
1048
1071
  aside: classifyAside({
1049
1072
  name: found[0].name,
1050
- declaredAxes: v.axes,
1073
+ declaredAxes: { ...found[0].axes, ...v.axes },
1051
1074
  }),
1052
1075
  }
1053
1076
  : {}),
@@ -1864,7 +1887,14 @@ export async function takeCensus(root, opts) {
1864
1887
  list.push(kebab(pair.child));
1865
1888
  companionsOf.set(pair.component, list);
1866
1889
  }
1867
- const components = merged.map((c) => ({
1890
+ /**
1891
+ * `Modal.Header` E `ModalHeader` CONTADOS UMA VEZ - ver `mergeNamespacePairs`.
1892
+ *
1893
+ * Antes do enriquecimento porque veredito, leis e companheiros são indexados por NOME: fundir
1894
+ * depois deixaria metade do que se sabe sobre a peça pendurado no nome que deixou de existir.
1895
+ */
1896
+ const paired = mergeNamespacePairs(merged, new Set(defined.map((d) => d.name)));
1897
+ const components = paired.map((c) => ({
1868
1898
  ...c,
1869
1899
  ...(verdicts.get(idOf(c)) ?? {}),
1870
1900
  ...(laws.has(c.name) ? { laws: laws.get(c.name) } : {}),
@@ -2071,6 +2101,25 @@ export async function takeCensus(root, opts) {
2071
2101
  }
2072
2102
  : {}),
2073
2103
  ...(conventions.length > 0 ? { conventions } : {}),
2104
+ /**
2105
+ * AS CLASSES DA FOLHA GLOBAL DELE - ver `Census.globalClasses`.
2106
+ *
2107
+ * TODAS AS QUE A FOLHA DECLARA, e não só as que a raiz vestiu. A primeira tentativa levou apenas
2108
+ * as reivindicadas por `globalWear`, que só roda na raiz do componente - e as utilities usadas
2109
+ * numa tabela de variantes ou num nó filho, que são a maioria, ficavam de fora justamente do
2110
+ * campo que existe para alcançá-las. Duas de 24 chegavam.
2111
+ *
2112
+ * O CUSTO ESTÁ MEDIDO: a folha do `codelevel-ui` declara 24 classes, a nossa 22, e o
2113
+ * `apps/web-dashboard` do repo real declara 0 - ele é CSS Modules. Nas três populações isso é
2114
+ * ruído no tamanho do censo. O teto de `MAX_GLOBAL_CLASSES` existe para a folha de um app que
2115
+ * escreva utilitário por atacado, e quando ele corta, o número cortado é DITO em vez de
2116
+ * desaparecer.
2117
+ */
2118
+ ...(globalClasses.size > 0
2119
+ ? {
2120
+ globalClasses: Object.fromEntries([...globalClasses.entries()].slice(0, MAX_GLOBAL_CLASSES)),
2121
+ }
2122
+ : {}),
2074
2123
  classStyle,
2075
2124
  ...(Object.keys(looks).length > 0 ? { looks } : {}),
2076
2125
  ...(naming.components > 0 ? { naming } : {}),
@@ -3166,6 +3215,16 @@ export function rootBehaviour(tag, sketch) {
3166
3215
  export async function resolveReadParts(census, root,
3167
3216
  /** `true` numa re-medição: o resumo desta etapa fica para o `sync --full` - ver `say`. */
3168
3217
  quiet = false) {
3218
+ /**
3219
+ * AS CLASSES DA FOLHA GLOBAL DELE, DO CENSO - ver `Census.globalClasses`.
3220
+ *
3221
+ * Esta etapa roda depois da medição, e também no `sync` e do lado da plataforma, sobre censo já
3222
+ * guardado - então ela não tem como voltar à folha no disco. Vindo do censo, uma utility que ele
3223
+ * escreve num NÓ FILHO chega ao look: o `LeaderboardRow` põe `text-grad-xp` num `<span>` dentro de
3224
+ * um ternário, e é aqui que aquele nó é lido.
3225
+ */
3226
+ const globalClasses = new Map(Object.entries(census
3227
+ .globalClasses ?? {}));
3169
3228
  const say = quiet
3170
3229
  ? () => { }
3171
3230
  : (line) => {
@@ -3278,13 +3337,13 @@ quiet = false) {
3278
3337
  ? resolveAnatomy(framed, declared, deps, resolveRef, entry?.root,
3279
3338
  // The component's own sketch, so a node naming itself by index reads
3280
3339
  // the exact class string the census measured (dono, 01/08).
3281
- looks[component]?.sketch, census.sxSpacing)
3340
+ looks[component]?.sketch, census.sxSpacing, globalClasses)
3282
3341
  : patched
3283
- ? resolveAnatomy(patched.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch, census.sxSpacing)
3342
+ ? resolveAnatomy(patched.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch, census.sxSpacing, globalClasses)
3284
3343
  : derived && derived.read.length > 0
3285
- ? resolveAnatomy(derived.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch, census.sxSpacing)
3344
+ ? resolveAnatomy(derived.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch, census.sxSpacing, globalClasses)
3286
3345
  : Array.isArray(entry?.parts) && entry.parts.length > 0
3287
- ? resolveFlatParts(entry.parts, declared)
3346
+ ? resolveFlatParts(entry.parts, declared, globalClasses)
3288
3347
  : null;
3289
3348
  if (!resolved)
3290
3349
  continue;
@@ -3699,6 +3758,23 @@ export async function runImport(opts) {
3699
3758
  const out = opts.census ?? join(root, "_synthesisui", "census.json");
3700
3759
  await mkdir(dirname(out), { recursive: true });
3701
3760
  await writeFile(out, `${JSON.stringify(census, null, 2)}\n`, "utf8");
3761
+ /**
3762
+ * O QUE NÃO CHEGOU, DECLARADO PELA ESTEIRA - ver `notExpressed`.
3763
+ *
3764
+ * Escrito ao lado do censo e na mesma rodada, porque a lacuna e a medição são a mesma verdade
3765
+ * vista dos dois lados. Era o agente que escrevia este arquivo, instruído por uma frase dentro da
3766
+ * descrição de uma tool - e em 23/08 o mesmo repositório foi importado duas vezes, a primeira
3767
+ * declarando 37 linhas de lacuna e a segunda nenhuma, com as lacunas todas no lugar. Um pedido a
3768
+ * um modelo não é um artefato.
3769
+ *
3770
+ * O agente ainda ACRESCENTA o que só ele vê. O piso deixou de depender dele.
3771
+ */
3772
+ const gaps = notExpressed(census);
3773
+ if (gaps) {
3774
+ const gapFile = join(dirname(out), "not-expressed.md");
3775
+ await writeFile(gapFile, gaps, "utf8");
3776
+ console.log(body(`Gaps written to ${paint.strong(relative(root, gapFile))}`));
3777
+ }
3702
3778
  console.log("");
3703
3779
  console.log(body(`Written to ${paint.strong(relative(root, out))}`));
3704
3780
  if (opts.dry) {
@@ -19,6 +19,8 @@
19
19
  */
20
20
  import { frontierKind } from "../frontier-kind.js";
21
21
  import { importMap } from "./imports.js";
22
+ /** Quantos lugares viajam por referência quebrada. Ver `BrokenRef.at`. */
23
+ const MAX_PLACES = 3;
22
24
  /** `var(--x)`, including inside a Tailwind arbitrary value: `bg-[var(--x)]`. */
23
25
  const VAR_REF = /var\(\s*(--[a-zA-Z0-9_-]+)/g;
24
26
  /**
@@ -96,9 +98,20 @@ export function findBrokenRefs(sources, declared) {
96
98
  continue;
97
99
  if (runtime && (RUNTIME_ANCHOR.has(name) || RUNTIME_PREFIX.test(name)))
98
100
  continue;
99
- const hit = seen.get(name) ?? { count: 0, files: new Set() };
101
+ const hit = seen.get(name) ?? {
102
+ count: 0,
103
+ files: new Set(),
104
+ at: [],
105
+ };
100
106
  hit.count += 1;
101
107
  hit.files.add(file);
108
+ /** A linha contada do índice do match - ver `BrokenRef.at` para o teto. */
109
+ if (hit.at.length < MAX_PLACES) {
110
+ hit.at.push({
111
+ file,
112
+ line: source.slice(0, m.index ?? 0).split("\n").length,
113
+ });
114
+ }
102
115
  seen.set(name, hit);
103
116
  }
104
117
  }
@@ -111,6 +124,7 @@ export function findBrokenRefs(sources, declared) {
111
124
  count: hit.count,
112
125
  files: hit.files.size,
113
126
  ...(meant ? { meant } : {}),
127
+ ...(hit.at.length > 0 ? { at: hit.at } : {}),
114
128
  });
115
129
  }
116
130
  // A broken reference somebody typed nine times is worse than one typed once,
@@ -61,6 +61,8 @@ const PROVIDER_SUFFIX = /(Provider|Providers|Context|Boundary|Guard|Wrapper)$/;
61
61
  /** Any JSX at all in the file. A capitalised export with none is a constant, a
62
62
  * config object or a helper - `export const ROUTES = {…}` is not a component. */
63
63
  const HAS_JSX = /<[A-Za-z][^>]*>|<>/;
64
+ /** `ICON_SIZE`, `AURORA_PALETTES`, `ARTICLE_TAB` - a convenção universal para valor, não para peça. */
65
+ const SCREAMING_SNAKE = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/;
64
66
  /**
65
67
  * Any sign that this file decides how something LOOKS.
66
68
  *
@@ -71,7 +73,30 @@ const HAS_JSX = /<[A-Za-z][^>]*>|<>/;
71
73
  const HAS_STYLING = /className|class=|style=|styled[.(]|css`|cva\(|\btv\(|makeStyles|sx=|tw`/;
72
74
  /** `createContext` + a `.Provider` in the return is a provider whatever it is
73
75
  * called - `RootWrapper` and `CommonProviders` both read this way. */
74
- const IS_CONTEXT = /createContext\s*[<(]/;
76
+ /**
77
+ * QUEM RECEBE O `createContext` - o nome, e não o arquivo em volta dele.
78
+ *
79
+ * O QUE O CLIENTE PERDIA: o `Tabs` inteiro. `Tabs.tsx` declara `const TabsCtx = createContext(…)`, e
80
+ * a pergunta era se o ARQUIVO tem contexto - então `Tabs`, `TabsList`, `TabsTrigger` e `TabsPanel`
81
+ * saíram juntos, com `cn("flex flex-col gap-4")` escrito neles (`codelevel-ui`, 23/08). Composto com
82
+ * contexto é o padrão dominante de React - Tabs, Accordion, Select, Dropdown -, então recusar o
83
+ * arquivo recusa a família toda.
84
+ *
85
+ * É a mesma falha de forma que `HAS_JSX` já cometeu do outro lado: uma pergunta sobre o arquivo
86
+ * respondendo sobre o export. Um provider é UM nome; os vizinhos respondem por si.
87
+ *
88
+ * Medido em três populações - `codelevel-ui` recupera 4, o dashboard do `frontend-hub` recupera 7
89
+ * (o `Pagination` inteiro), e os 6 que terminam em `Provider` continuam fora pelo sufixo.
90
+ */
91
+ const CONTEXT_OWNER = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]*)?=\s*(?:React\.)?createContext\s*[<(]/g;
92
+ /** Os nomes que ESTE arquivo atribui a um `createContext`. */
93
+ function contextNames(source) {
94
+ CONTEXT_OWNER.lastIndex = 0;
95
+ const out = new Set();
96
+ for (const m of source.matchAll(CONTEXT_OWNER))
97
+ out.add(m[1]);
98
+ return out;
99
+ }
75
100
  /** A lowercase JSX tag: an html element this file renders ITSELF. */
76
101
  const OWN_ELEMENT = /<(?:[a-z][a-z0-9]*)(\s|>|\/)/;
77
102
  /** Every capitalised JSX tag the file opens, dots included. */
@@ -205,7 +230,7 @@ export function gateComponent(input) {
205
230
  because: `\`${name}\` names a screen, and composes ${shape.composes} piece${shape.composes === 1 ? "" : "s"} across ${shape.nodes} element${shape.nodes === 1 ? "" : "s"} - too thin to be a template of yours. A design system is made of the pieces a screen is built FROM`,
206
231
  };
207
232
  }
208
- if (PROVIDER_SUFFIX.test(name) || IS_CONTEXT.test(source)) {
233
+ if (PROVIDER_SUFFIX.test(name) || contextNames(source).has(name)) {
209
234
  return {
210
235
  ok: false,
211
236
  why: "provider",
@@ -232,6 +257,38 @@ export function gateComponent(input) {
232
257
  */
233
258
  if (STYLED_FACTORY.test(source))
234
259
  return { ok: true };
260
+ /**
261
+ * UMA CONSTANTE NÃO É COMPONENTE, e o arquivo em volta dela não muda isso.
262
+ *
263
+ * O QUE O CLIENTE VIA: `ICON_SIZE` e `AURORA_PALETTES` ocupando vaga na vitrine do
264
+ * `codelevel-ui`, e uma delas com forma desenhada - um mapa de tamanhos renderizado como se fosse
265
+ * peça. Ele contava 59 componentes no design system dele e dois não existiam.
266
+ *
267
+ * O teste de markup logo abaixo já recusa constante, e ele pergunta pelo ARQUIVO INTEIRO:
268
+ * `ICON_SIZE` mora dentro do `Icon.tsx`, que tem JSX de sobra, então ela entrava pela porta do
269
+ * vizinho. Vem ANTES dele por isso - é a mesma falha de forma que o teste de contexto acima
270
+ * corrigiu do outro lado.
271
+ *
272
+ * MEDIDO EM QUATRO POPULAÇÕES, e as três encontradas são objeto literal no código real:
273
+ *
274
+ * codelevel-ui packages/ui 75 nomes · 2 (ICON_SIZE, AURORA_PALETTES)
275
+ * frontend-hub packages/ui 69 nomes · 0
276
+ * frontend-hub apps/web-dashboard 526 nomes · 1 (ARTICLE_TAB, um enum de aba)
277
+ * synthesisui apps/web · 0
278
+ *
279
+ * Nenhum componente das quatro usa este formato de nome - JSX escreve `<Button>` e nunca
280
+ * `<ICON_SIZE>` -, então a regra não recusa peça de ninguém. E ela só pôde entrar junto com o
281
+ * leitor de `export default` em linha própria: sozinha, ela deixava órfãs as 294 declarações de
282
+ * CSS Modules do único arquivo do dashboard onde a constante era a ÚNICA coisa que o leitor via.
283
+ * O dono daquele arquivo é o `CurateContentSection`, que agora é lido.
284
+ */
285
+ if (SCREAMING_SNAKE.test(name)) {
286
+ return {
287
+ ok: false,
288
+ why: "config",
289
+ because: `\`${name}\` is spelled as a constant - a value the code reads, not a component anything renders`,
290
+ };
291
+ }
235
292
  if (!HAS_JSX.test(source)) {
236
293
  return {
237
294
  ok: false,
@@ -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
  *
@@ -1218,7 +1219,11 @@ function readUtilityCore(utility, declared) {
1218
1219
  if (themeProperty && rest) {
1219
1220
  const own = `--${prefix}-${rest}`;
1220
1221
  if (declared.has(own)) {
1221
- return { property: themeProperty, value: tokenRefFor(own), token: own };
1222
+ return {
1223
+ property: themeProperty,
1224
+ value: tokenRefFor(own, declared),
1225
+ token: own,
1226
+ };
1222
1227
  }
1223
1228
  }
1224
1229
  return null;
@@ -1310,7 +1315,7 @@ function resolveArbitrary(inner, declared) {
1310
1315
  if (raw.startsWith("--")) {
1311
1316
  const name = raw.split(",")[0].trim();
1312
1317
  return declared.has(name)
1313
- ? { value: tokenRefFor(name), token: name }
1318
+ ? { value: tokenRefFor(name, declared), token: name }
1314
1319
  : null;
1315
1320
  }
1316
1321
  // A literal in brackets is still a real value somebody typed.
@@ -1318,7 +1323,7 @@ function resolveArbitrary(inner, declared) {
1318
1323
  }
1319
1324
  const name = v[1];
1320
1325
  if (declared.has(name))
1321
- return { value: tokenRefFor(name), token: name };
1326
+ return { value: tokenRefFor(name, declared), token: name };
1322
1327
  /**
1323
1328
  * A `var()` THEY NEVER DECLARED, WHEN THEY DECLARED IT UNDER ITS NAMESPACE.
1324
1329
  *
@@ -1338,7 +1343,7 @@ function resolveArbitrary(inner, declared) {
1338
1343
  */
1339
1344
  const meant = MEANT_NAMESPACES.map((ns) => `--${ns}-${name.slice(2)}`).find((candidate) => declared.has(candidate));
1340
1345
  if (meant)
1341
- return { value: tokenRefFor(meant), token: meant };
1346
+ return { value: tokenRefFor(meant, declared), token: meant };
1342
1347
  return { value: raw };
1343
1348
  }
1344
1349
  /**
@@ -1362,53 +1367,6 @@ const MEANT_NAMESPACES = [
1362
1367
  * `--text-body-s` → `{typography.scale.body-s.fontSize}`. Their name is the path,
1363
1368
  * which is the whole "your names travel unchanged" promise applied one level deeper.
1364
1369
  */
1365
- function tokenRefFor(name) {
1366
- const bare = name.replace(/^--/, "");
1367
- if (bare.startsWith("color-"))
1368
- return refFor(bare.slice("color-".length));
1369
- if (bare.startsWith("radius-"))
1370
- return `{radius.${bare.slice(7)}}`;
1371
- if (bare.startsWith("spacing-"))
1372
- return `{spacing.${bare.slice(8)}}`;
1373
- if (bare.startsWith("shadow-"))
1374
- return `{shadow.${bare.slice(7)}}`;
1375
- // `--gradient-ui` and `--background-image-gradient-ui` are one token in two
1376
- // spellings - Tailwind v4's utility namespace wraps the first. Both reach
1377
- // `{gradients.ui}`, which is where the census files them.
1378
- if (bare.startsWith("background-image-gradient-")) {
1379
- return `{gradients.${bare.slice("background-image-gradient-".length)}}`;
1380
- }
1381
- if (bare.startsWith("gradient-"))
1382
- return `{gradients.${bare.slice(9)}}`;
1383
- if (bare.startsWith("text-")) {
1384
- /**
1385
- * THE COMPANION TOKEN. Tailwind v4 spells "the line-height OF text-body-s" as
1386
- * `--text-body-s--line-height` - a double dash inside one name. Read as a step name
1387
- * it produced `{typography.scale.body-s--line-height.fontSize}`, whose double dash
1388
- * no ref grammar accepts, and the whole recipe was REFUSED at validation (test13,
1389
- * 01/08). The suffix names the property; the middle names the step.
1390
- */
1391
- const companion = /^text-(.+?)--(line-height|letter-spacing|font-weight)$/.exec(bare);
1392
- if (companion) {
1393
- const prop = {
1394
- "line-height": "lineHeight",
1395
- "letter-spacing": "letterSpacing",
1396
- "font-weight": "weight",
1397
- }[companion[2]];
1398
- return `{typography.scale.${companion[1]}.${prop}}`;
1399
- }
1400
- // Any other double dash is a name this grammar cannot hold - the literal var()
1401
- // still resolves against their own stylesheet, and an invalid ref helps nobody.
1402
- if (bare.slice(5).includes("--"))
1403
- return `var(${name})`;
1404
- return `{typography.scale.${bare.slice(5)}.fontSize}`;
1405
- }
1406
- if (bare.startsWith("font-"))
1407
- return `{typography.families.${bare.slice(5)}}`;
1408
- // A namespace we do not model. The literal `var()` is still correct CSS against
1409
- // their own stylesheet, and inventing a ref would point at nothing.
1410
- return `var(${name})`;
1411
- }
1412
1370
  function refFor(name) {
1413
1371
  const m = /^(.*)-(\d{2,4})$/.exec(name);
1414
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
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * O QUE ESTA LEITURA NÃO CONSEGUIU SEGURAR, escrito pela esteira e não pedido a ninguém.
3
+ *
4
+ * O QUE O CLIENTE GANHA: ele sabe, no minuto do import, o que não chegou - com o número do
5
+ * repositório dele e o arquivo e a linha de cada caso. É a lei 8 do método: o cliente perdoa o que
6
+ * a gente diz que não faz, e não perdoa descobrir sozinho.
7
+ *
8
+ * POR QUE ISTO EXISTE. O arquivo era escrito pelo AGENTE, instruído por três frases dentro das
9
+ * descrições das tools do MCP. Em 23/08 o mesmo repositório foi importado duas vezes: a primeira
10
+ * rodada escreveu 37 linhas de lacunas declaradas, a segunda escreveu nenhuma. As lacunas
11
+ * continuaram todas lá - 198 declarações de CSS sem leitor, 5 `var()` que não pintam nada -, e só o
12
+ * aviso desapareceu. Um pedido em texto a um modelo é uma sugestão, e nada percebia quando ela não
13
+ * era seguida.
14
+ *
15
+ * O PISO É DETERMINÍSTICO, e todo dado que ele usa já estava medido no censo. O agente continua
16
+ * livre para ACRESCENTAR o que só ele viu; o que ele não pode mais é ser a única testemunha.
17
+ *
18
+ * `null` quando não há lacuna nenhuma: um arquivo vazio dizendo "nada a declarar" é ruído, e a
19
+ * ausência do arquivo já é a resposta.
20
+ */
21
+ export function notExpressed(census) {
22
+ const ledger = census.ledger;
23
+ const unread = ledger?.unread ?? [];
24
+ const broken = census.brokenRefs ?? [];
25
+ const skipped = census.skipped ?? [];
26
+ if (unread.length === 0 && broken.length === 0 && skipped.length === 0)
27
+ return null;
28
+ const out = [];
29
+ const scope = census.scope ? ` of \`${census.scope}\`` : "";
30
+ out.push(`# What this reading could not hold`);
31
+ out.push("");
32
+ out.push(`Measured by synthesisui \`${ledger?.cli ?? "unknown"}\`${scope}. Every number here is from` +
33
+ ` your files. This file is written by the pipeline on every import - what it lists is what` +
34
+ ` did NOT reach a recipe.`);
35
+ if (unread.length > 0) {
36
+ out.push("");
37
+ out.push("## Style your files hold and no recipe received");
38
+ for (const group of unread) {
39
+ out.push("");
40
+ out.push(`### ${group.uses} ${group.shape} declaration${group.uses === 1 ? "" : "s"}` +
41
+ ` across ${group.files} file${group.files === 1 ? "" : "s"} - ${group.reason}`);
42
+ out.push("");
43
+ out.push(group.because);
44
+ out.push("");
45
+ for (const ex of group.examples ?? [])
46
+ out.push(` ${ex.file}:${ex.line} ${ex.text}`);
47
+ }
48
+ }
49
+ if (broken.length > 0) {
50
+ out.push("");
51
+ out.push("## Variables referenced and never declared");
52
+ out.push("");
53
+ out.push("These do not paint a different colour - they paint nothing. They are yours to fix, in your" +
54
+ " own words. Up to three places are listed for each; the count says how many there are in" +
55
+ " total.");
56
+ out.push("");
57
+ for (const ref of broken) {
58
+ out.push(` ${ref.name} ${ref.count} reference${ref.count === 1 ? "" : "s"}` +
59
+ ` in ${ref.files} file${ref.files === 1 ? "" : "s"}`);
60
+ for (const place of ref.at ?? [])
61
+ out.push(` ${place.file}:${place.line}`);
62
+ }
63
+ }
64
+ if (skipped.length > 0) {
65
+ out.push("");
66
+ out.push("## Exports the gate left out, and why");
67
+ out.push("");
68
+ out.push("Nothing here was dropped in silence. If one of these IS a component of your system, that is" +
69
+ " a reading to correct - and the reason below is the one to argue with.");
70
+ out.push("");
71
+ for (const s of skipped)
72
+ out.push(` ${s.name}${s.file ? ` (${s.file})` : ""}\n ${s.because}`);
73
+ }
74
+ out.push("");
75
+ return `${out.join("\n")}\n`;
76
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * O NOME QUE UM TOKEN DELE TEM DENTRO DO DOCUMENTO - uma regra, num lugar só.
3
+ *
4
+ * O QUE O CLIENTE GANHA: mudar um valor no Studio repinta o componente que o usa. Um ref liga os
5
+ * dois; um `var()` literal não liga nada - ele renderiza certo hoje e fica órfão da edição.
6
+ *
7
+ * ESTA FUNÇÃO EXISTIA DUAS VEZES, em `transcribe.ts` e em `css-modules.ts`, e as duas divergiam: a
8
+ * segunda não conhecia `--font-*`, então a família de fonte dele virava referência quando lida de uma
9
+ * classe e `var()` quando lida de uma folha - a mesma decisão dele com dois destinos, dependendo de
10
+ * onde a esteira olhou. Gêmeo não declarado sempre acaba assim.
11
+ *
12
+ * E O GRADIENTE ERA DECIDIDO PELO NOME. A regra reconhecia `--gradient-*`; o cliente escreveu
13
+ * `--grad-cool`. O escritor da plataforma reconhece pelo VALOR - `linear-gradient(…)` - e guarda em
14
+ * `foundations.gradients.grad-cool`, então o token TINHA casa no documento e a classe que o usa
15
+ * apontava para o vazio. É a lei 13 aplicada a token: a origem decide, e aqui a origem é o valor.
16
+ *
17
+ * Medido: `--grad-*` são 5 no `codelevel-ui`, `--gradient-*` são 3 no `frontend-hub`, e as duas
18
+ * grafias passam a chegar onde o documento as guarda.
19
+ */
20
+ export function tokenRefFor(name,
21
+ /**
22
+ * O que cada token DELE vale. O mapa é o que permite reconhecer um gradiente pela forma em vez do
23
+ * nome; vazio, a decisão volta a ser só pelo nome, que é o comportamento de quem não tem o valor
24
+ * em mãos.
25
+ */
26
+ declared) {
27
+ const bare = name.replace(/^--/, "");
28
+ if (bare.startsWith("color-"))
29
+ return refFor(bare.slice("color-".length));
30
+ if (bare.startsWith("radius-"))
31
+ return `{radius.${bare.slice(7)}}`;
32
+ if (bare.startsWith("spacing-"))
33
+ return `{spacing.${bare.slice(8)}}`;
34
+ if (bare.startsWith("shadow-"))
35
+ return `{shadow.${bare.slice(7)}}`;
36
+ // `--gradient-ui` e `--background-image-gradient-ui` são um token em duas grafias - o namespace de
37
+ // utility do Tailwind v4 embrulha o primeiro. Os dois chegam a `{gradients.ui}`.
38
+ if (bare.startsWith("background-image-gradient-")) {
39
+ return `{gradients.${bare.slice("background-image-gradient-".length)}}`;
40
+ }
41
+ if (bare.startsWith("gradient-"))
42
+ return `{gradients.${bare.slice(9)}}`;
43
+ if (bare.startsWith("text-")) {
44
+ /**
45
+ * O TOKEN COMPANHEIRO. O Tailwind v4 escreve "a altura de linha DE text-body-s" como
46
+ * `--text-body-s--line-height` - um duplo hífen dentro de um nome. Lido como nome de passo ele
47
+ * produzia `{typography.scale.body-s--line-height.fontSize}`, cujo duplo hífen nenhuma gramática
48
+ * de ref aceita, e a receita inteira era RECUSADA na validação (test13, 01/08). O sufixo nomeia a
49
+ * propriedade; o meio nomeia o passo.
50
+ */
51
+ const companion = /^text-(.+?)--(line-height|letter-spacing|font-weight)$/.exec(bare);
52
+ if (companion) {
53
+ const prop = {
54
+ "line-height": "lineHeight",
55
+ "letter-spacing": "letterSpacing",
56
+ "font-weight": "weight",
57
+ }[companion[2]];
58
+ return `{typography.scale.${companion[1]}.${prop}}`;
59
+ }
60
+ // Qualquer outro duplo hífen é um nome que esta gramática não segura - o `var()` literal ainda
61
+ // resolve contra a folha dele, e um ref inválido não ajuda ninguém.
62
+ if (bare.slice(5).includes("--"))
63
+ return `var(${name})`;
64
+ return `{typography.scale.${bare.slice(5)}.fontSize}`;
65
+ }
66
+ if (bare.startsWith("font-"))
67
+ return `{typography.families.${bare.slice(5)}}`;
68
+ /**
69
+ * O GRADIENTE PELA FORMA DO VALOR, e é a última pergunta e não a primeira.
70
+ *
71
+ * Os namespaces acima são vocabulário conhecido e ganham deste teste - `--color-brand` que valha um
72
+ * `linear-gradient` continua sendo cor pelo nome que ele deu. Aqui embaixo estão os nomes que a
73
+ * gente NÃO modela, e é onde `--grad-cool` cai. A chave é o nome dele inteiro, que é exatamente o
74
+ * que `gradientsFromCensus` guarda no documento.
75
+ */
76
+ const value = declared.get(name);
77
+ if (value && /\b(?:linear|radial|conic)-gradient\(/.test(value)) {
78
+ return `{gradients.${bare}}`;
79
+ }
80
+ // Um namespace que não modelamos. O `var()` literal continua sendo CSS correto contra a folha
81
+ // dele, e inventar um ref apontaria para nada.
82
+ return `var(${name})`;
83
+ }
84
+ /**
85
+ * `ocean-500` → `{color.ocean.500}`. O passo é o número final; o que vem antes é a família, hífens
86
+ * intactos, porque `royal-blue-500` é uma família chamada `royal-blue`.
87
+ */
88
+ function refFor(name) {
89
+ const m = /^(.*)-(\d{2,4})$/.exec(name);
90
+ if (!m)
91
+ return `{color.${name}}`;
92
+ return `{color.${m[1]}.${m[2]}}`;
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.286",
3
+ "version": "0.16.289",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {