synthesisui 0.16.390 → 0.16.395

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.
@@ -10,7 +10,11 @@ const HOME = {
10
10
  };
11
11
  /** Uma palavra de categoria no começo do nome deles não é família - é a categoria repetida. */
12
12
  const CATEGORY = /^(color|colour|radius|rounded|spacing|space|gap|font|type|text|duration|motion|ease|easing)-/;
13
- const clean = (name) => name.replace(/^--/, "").toLowerCase();
13
+ /**
14
+ * O nome sem o sigilo. `$` e `@` entram porque o vocabulário dele pode estar num pré-processador -
15
+ * um `$gray_dark` que mantivesse o `$` viraria o caminho `color.$gray.dark`, que não é um caminho.
16
+ */
17
+ const clean = (name) => name.replace(/^(--|[$@])/, "").toLowerCase();
14
18
  /**
15
19
  * QUAL DOS NOMES DELE, quando mais de um segura o mesmo valor - e aqui a natureza do ACHADO decide.
16
20
  *
@@ -105,7 +109,31 @@ function nearestOwn(theirs, literal, kind) {
105
109
  * existente com outro valor - isso não é absorver, é repintar.
106
110
  */
107
111
  export function absorbPlan(d, theirs, have, cap = 40) {
108
- const unnamed = d.repeats.filter((r) => !r.token);
112
+ /**
113
+ * A REPETIÇÃO É UM FILTRO DE RUÍDO, E RUÍDO É UMA PROPRIEDADE DA ESCALA - não do valor.
114
+ *
115
+ * Ordenar por repetição está certo onde a deriva se concentra: no `~/projects/web-subscribe` são
116
+ * 858 valores distintos escritos à mão, e mostrar os 40 mais repetidos é o que transforma isso numa
117
+ * fila de trabalho. Num projeto que acabou de nascer, o mesmo corte apaga o projeto inteiro.
118
+ *
119
+ * Medido em 07/09 sobre três populações:
120
+ *
121
+ * ~/projects/web-subscribe 858 distintos 484 repetidos 362 apareciam uma vez só
122
+ * ~/projects/web-onboarding 52 distintos 21 repetidos 29 apareciam uma vez só
123
+ * create-next-app real 4 distintos 0 repetidos 4 apareciam uma vez só
124
+ *
125
+ * Naquele último o `absorb` respondia que não havia nada a absorver, três linhas depois de o
126
+ * `doctor` dizer *"0 of 4 have a name waiting"*.
127
+ *
128
+ * A REGRA SAI DO TETO QUE JÁ EXISTE, e por isso não é mais um número escolhido por nós: o corte
129
+ * serve para ESCOLHER quando não cabe tudo. Quando tudo cabe, não há o que escolher, e esconder
130
+ * metade é esconder por hábito. Num repositório grande nada muda - 484 já estouram o teto sozinhos.
131
+ */
132
+ const repeated = d.repeats.filter((r) => !r.token);
133
+ const single = d.once.filter((r) => !r.token);
134
+ const unnamed = repeated.length + single.length <= cap
135
+ ? [...repeated, ...single]
136
+ : repeated;
109
137
  const entries = [];
110
138
  for (const r of unnamed) {
111
139
  if (entries.length >= cap)
@@ -133,7 +161,25 @@ export function absorbPlan(d, theirs, have, cap = 40) {
133
161
  * `var(--dashboard-white-500)` e o `absorb`, no mesmo dia, pedia que ele batizasse `#fff`. Dois
134
162
  * comandos com conselhos opostos sobre o mesmo valor.
135
163
  */
136
- const theirName = nameOf(r.kind, theirs.byValue.get(normalizeValue(r.literal, theirs.rootPx)));
164
+ /**
165
+ * O NOME DELE VEM DAS DUAS FORMAS - e sem a segunda os dois comandos voltam a se contradizer.
166
+ *
167
+ * É o mesmo defeito que esta função já corrigiu uma vez, reaberto por outro caminho: o `doctor`
168
+ * passou a dizer *"#555 · your code calls it $gray_dark"* e o `absorb`, no mesmo repositório,
169
+ * pedia que ele batizasse o `#555`. Dois comandos com conselhos opostos sobre o mesmo valor.
170
+ *
171
+ * A ORDEM É A MESMA DO RESTO DA ESTEIRA: primeiro o nome que o navegador recebe, e só depois o
172
+ * que o pré-processador resolve. Onde ele nomeia nas duas formas, ganha a custom property.
173
+ *
174
+ * E aqui o nome compilado NÃO é um beco: o caminho que sai dele vira um token de verdade na
175
+ * fundação, que o navegador recebe - então as ocorrências em `.tsx`, que nunca poderiam receber
176
+ * um `$`, ficam trocáveis pelo caminho que já existe (`absorb` -> `upgrade` -> `doctor --fix`).
177
+ * Medido em 07/09 no `~/projects/web-subscribe`: 323 ocorrências têm nome dele, e só 52 delas
178
+ * estão num arquivo que poderia escrever o `$` diretamente.
179
+ */
180
+ const value = normalizeValue(r.literal, theirs.rootPx);
181
+ const theirName = nameOf(r.kind, theirs.byValue.get(value)) ??
182
+ nameOf(r.kind, theirs.compiledAway.get(value)?.map((c) => c.name));
137
183
  const path = pathFor(r.kind, theirName);
138
184
  /** "≈ perto de um token seu" só interessa a quem não tem um EXATO - com o nome na mão, a dica
139
185
  * vira ruído, e no arquivo de proposta ela vira uma segunda opção que não é opção. */
@@ -151,7 +197,11 @@ export function absorbPlan(d, theirs, have, cap = 40) {
151
197
  ...(near ? { near } : {}),
152
198
  });
153
199
  }
154
- return { entries, more: Math.max(0, unnamed.length - entries.length) };
200
+ return {
201
+ entries,
202
+ more: Math.max(0, unnamed.length - entries.length),
203
+ unnamed: d.repeats.length + d.once.length,
204
+ };
155
205
  }
156
206
  /** Quantas linhas da proposta ainda precisam de um nome humano. */
157
207
  export const needingName = (plan) => plan.entries.filter((e) => !e.path);
@@ -166,9 +216,21 @@ export function describeAbsorb(plan) {
166
216
  const ready = plan.entries.filter((e) => e.path);
167
217
  const pending = needingName(plan);
168
218
  const lines = [];
219
+ /**
220
+ * VAZIO POR DOIS MOTIVOS OPOSTOS, e a frase afirmava só um.
221
+ *
222
+ * "every repeated value already has a name" é notícia boa. Mas a lista também fica vazia quando não
223
+ * há valor NENHUM escrito à mão - e aí a frase de cima descreve um repositório que não é o dele.
224
+ * Medido em 07/09 numa cópia de `create-next-app`: 4 valores sem nome, e esta era a resposta.
225
+ *
226
+ * A distinção não é cosmética: um manda seguir em frente, o outro diz que o comando não tem
227
+ * trabalho porque o projeto não tem deriva. Duas conversas diferentes.
228
+ */
169
229
  if (plan.entries.length === 0)
170
230
  return [
171
- "Nothing to absorb: every repeated value your code writes by hand already has a name in the system.",
231
+ plan.unnamed > 0
232
+ ? `Nothing to absorb here: the ${plan.unnamed} value${plan.unnamed === 1 ? "" : "s"} written by hand ${plan.unnamed === 1 ? "already has" : "already have"} a name in your system.`
233
+ : "Nothing to absorb: no design value in this project is written by hand.",
172
234
  ];
173
235
  /**
174
236
  * A SEÇÃO DOS QUE JÁ TÊM NOME SÓ EXISTE SE ALGUM TIVER - e no dia 1 nenhum tem.
@@ -187,9 +249,18 @@ export function describeAbsorb(plan) {
187
249
  const close = pending.filter((e) => e.near);
188
250
  if (ready.length > 0)
189
251
  lines.push("");
252
+ /**
253
+ * "REPEATED" SÓ QUANDO ELES SE REPETEM - e desde que o corte por repetição deixou de valer para
254
+ * projeto pequeno, essa palavra passou a descrever um repositório que não é o dele.
255
+ *
256
+ * Medido em 07/09 numa cópia de `create-next-app`: a linha dizia *"4 repeated values"* sobre
257
+ * quatro valores que aparecem uma vez cada. Uma palavra errada na primeira linha do comando gasta
258
+ * a credibilidade das outras dez.
259
+ */
260
+ const everyOneRepeats = pending.every((e) => e.count > 1);
190
261
  lines.push(ready.length > 0
191
262
  ? `${pending.length} nobody names yet. Naming is a design decision, so those wait for a word from you:`
192
- : `${pending.length} repeated values, and none of them has a name yet. Naming is a design decision, so each waits for a word from you:`);
263
+ : `${pending.length} ${everyOneRepeats ? "repeated values" : "values written by hand"}, and none of them has a name yet. Naming is a design decision, so each waits for a word from you:`);
193
264
  for (const e of pending.slice(0, 8))
194
265
  lines.push(` ${e.value.padEnd(24)} ${`<${e.kind}, ${e.files} file${e.files === 1 ? "" : "s"}>`.padEnd(28)} ${e.near ? `≈ your ${e.near.name} (${e.near.away})` : 'path: ""'}`);
195
266
  if (pending.length > 8)
@@ -3,8 +3,10 @@ import { dirname, join, resolve } from "node:path";
3
3
  import { absorbPlan, describeAbsorb, needingName, } from "../absorb-plan.js";
4
4
  import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
5
5
  import { diagnose, scanSource } from "../doctor/scan.js";
6
+ import { withTheirNames } from "../doctor/their-names.js";
6
7
  import { measuredScope, scopePaths } from "../measured-scope.js";
7
8
  import { body, paint, section, snippet } from "../output.js";
9
+ import { resolveDeps, tailwindMajor } from "../stack.js";
8
10
  import { loadSystem, walkAll } from "./doctor.js";
9
11
  /**
10
12
  * `synthesisui absorb` - O SISTEMA APRENDE O DESIGN QUE O CÓDIGO JÁ TEM.
@@ -66,12 +68,28 @@ export async function absorb(opts) {
66
68
  * ideia primeiro e ficou com a implementação de todo mundo.
67
69
  */
68
70
  const theirs = installed.theirs;
71
+ /**
72
+ * SEM SISTEMA NOSSO, A TABELA DA VARREDURA É A DELE - a mesma queda que o `doctor` já faz.
73
+ *
74
+ * O QUE ACONTECIA: este comando varria com `installed.table`, que sem sistema instalado é a tabela
75
+ * VAZIA. Então nada tinha nome, nem o que o próprio código dele nomeia - e a linha
76
+ * `--background: #ffffff`, que é onde o token dele NASCE, entrava na proposta como um valor solto
77
+ * a ser batizado. Medido em 07/09 numa cópia de `create-next-app`: 4 das 8 entradas eram as
78
+ * declarações dos tokens dele, e duas delas já traziam `theirName` preenchido - a proposta pedindo
79
+ * um nome para o que ela mesma acabara de dizer que já tem um.
80
+ *
81
+ * O `doctor` resolve isto há tempos, e a linha é literalmente a dele: quando não há nada nosso, o
82
+ * vocabulário dele é o único que existe, e é contra ele que a medição acontece.
83
+ */
84
+ const table = installed.table.byName.size > 0
85
+ ? installed.table
86
+ : withTheirNames(theirs, theirs);
69
87
  const reports = [];
70
88
  for await (const file of walkAll(roots)) {
71
89
  const source = await readFile(file, "utf8").catch(() => null);
72
90
  if (source === null)
73
91
  continue;
74
- reports.push(scanSource(file.slice(root.length + 1), source, installed.table));
92
+ reports.push(scanSource(file.slice(root.length + 1), source, table));
75
93
  }
76
94
  const plan = absorbPlan(diagnose(reports), theirs, pathsInSystem(installed.documents), opts.cap ?? 40);
77
95
  console.log(section(hasSystem
@@ -173,13 +191,36 @@ async function sendProposal(root, path, registry) {
173
191
  console.log(body(paint.dim(proposal.slug
174
192
  ? "Your config says absorbed values belong in your CSS, so nothing was sent."
175
193
  : "You have no system yet, so these belong in your own CSS - nothing was sent, and nothing needed an account.")));
194
+ /**
195
+ * NUM TAILWIND v4, `:root` DECLARA A VARIÁVEL E NÃO GERA A CLASSE.
196
+ *
197
+ * O QUE ELE VIVIA: nomeava `#383838` de `color.ink.700`, colava o bloco que este comando
198
+ * imprime, e `text-ink-700` continuava não existindo. A variável estava lá, o doctor a
199
+ * reconhecia - e o código dele não tinha como usá-la. A metade visível da promessa não chegava.
200
+ *
201
+ * É a mesma lei que a `INV-VOLTA-13` já enuncia do outro lado da esteira: *"no Tailwind v4 só
202
+ * `@theme` gera utilitário"*. A folha que a plataforma instala sabe disso desde 23/08; o bloco
203
+ * que a plataforma manda ELE colar não sabia.
204
+ *
205
+ * E A SINTAXE SAI DO PROJETO, nunca de um padrão nosso - `tailwindMajor` já existe e o
206
+ * comentário dele diz por quê: *"gerar a sintaxe de uma para um projeto da outra produz um
207
+ * arquivo que o build DELE não entende, e é o pior tipo de saída, porque parece certa até
208
+ * alguém compilar"*. Sem Tailwind, ou na v3, `:root` continua sendo a resposta certa: lá o
209
+ * utilitário nasce do `tailwind.config`, não do CSS.
210
+ */
211
+ const major = tailwindMajor(await resolveDeps(root));
212
+ const block = major === 4 ? "@theme" : ":root";
176
213
  console.log("");
177
- console.log(" :root {");
214
+ console.log(` ${block} {`);
178
215
  for (const e of ready)
179
216
  console.log(` --${e.path.replace(/\./g, "-")}: ${e.value};`);
180
217
  console.log(" }");
181
218
  console.log("");
182
- console.log(body("The next import or `sync` reads these back as tokens you declared, and from then on the doctor holds your code to them by your own names."));
219
+ console.log(body(major === 4
220
+ ? "In `@theme` and not `:root`: on Tailwind v4 only `@theme` generates the utility, so this is what makes `text-ink-700` exist in your code."
221
+ : "The next import or `sync` reads these back as tokens you declared, and from then on the doctor holds your code to them by your own names."));
222
+ if (major === 4)
223
+ console.log(body("The next import or `sync` reads these back as tokens you declared, and from then on the doctor holds your code to them by your own names."));
183
224
  return;
184
225
  }
185
226
  const token = await readToken();
@@ -400,14 +400,20 @@ async function harvestOwnTokens(roots,
400
400
  * e `4px` deixaria de casar com `0.25rem` de um lado só. */
401
401
  measured) {
402
402
  let css = "";
403
+ /** UMA A UMA, e não só concatenadas: a regra de escopo de `their-compiled-names.ts` decide pelo
404
+ * `@import` de um arquivo no outro, e a concatenação apaga de qual arquivo cada linha veio. */
405
+ const sheets = new Map();
403
406
  for await (const file of walkAll(roots)) {
404
407
  if (!/\.(css|scss|sass|less)$/i.test(file))
405
408
  continue;
406
- css += `\n${await readFile(file, "utf8").catch(() => "")}`;
409
+ const source = await readFile(file, "utf8").catch(() => "");
410
+ sheets.set(file, source);
411
+ css += `\n${source}`;
407
412
  }
408
413
  return buildTable({
409
414
  css,
410
415
  source: "yours",
416
+ sheets,
411
417
  rootPx: measured?.px,
412
418
  rootFrom: measured?.from ?? null,
413
419
  });
@@ -464,10 +470,27 @@ function verdict(d, hasSystem, overruled, conflicts) {
464
470
  */
465
471
  if (!hasSystem) {
466
472
  const distinct = new Set(d.findings.map((f) => f.literal.toLowerCase()));
473
+ /**
474
+ * O QUE O CÓDIGO DELE JÁ NOMEIA - e sem esta linha o relatório se contradiz.
475
+ *
476
+ * "none of them has a name yet" é o fecho de um projeto sem sistema, e ele era verdade enquanto
477
+ * a plataforma só lia custom properties. Agora o detalhe imprime `#555 · your code calls it
478
+ * $gray_dark` três linhas acima: o resumo dizendo que nada tem nome, sobre um valor que a linha
479
+ * de cima acabou de nomear, é o defeito que faz alguém desconfiar do relatório inteiro.
480
+ *
481
+ * E o número muda a decisão dele: quem já batizou metade dos valores no Sass não está no dia
482
+ * zero - ele está a um `absorb` de trazer esses nomes para um vocabulário que o navegador
483
+ * recebe, o que é uma conversa diferente de "escolha o sistema de alguém".
484
+ */
485
+ const spoken = new Set(d.findings
486
+ .filter((f) => f.theirCompiledName)
487
+ .map((f) => f.literal.toLowerCase()));
467
488
  return [
468
489
  ...head,
469
490
  body(`${distinct.size} distinct design values are written by hand here.`),
470
- body("No design system is installed, so none of them has a name yet."),
491
+ spoken.size === 0
492
+ ? body("No design system is installed, so none of them has a name yet.")
493
+ : body(`No design system is installed, but your own code already names ${spoken.size} of them - in Sass or Less, which the browser never receives.`),
471
494
  "",
472
495
  body("Name them yourself - this reads them and asks you for the words:"),
473
496
  snippet(["npx synthesisui@latest absorb"]),
@@ -1534,9 +1557,19 @@ export async function doctor(opts) {
1534
1557
  ? `→ no ${x.kind} named for it · the value lives as ${x.token}`
1535
1558
  : nameToWrite(x)
1536
1559
  ? `→ ${nameToWrite(x)}${alsoNamed(x)}`
1537
- : near
1538
- ? `→ nearest is ${near.theirs ?? near.name} (${near.value})`
1539
- : "→ no token holds this value yet";
1560
+ : /**
1561
+ * O CÓDIGO DELE JÁ NOMEIA ISTO, e a linha dizia que nada nomeava.
1562
+ *
1563
+ * Vem antes de `near` porque um nome EXATO que ele escreveu vale mais que o degrau
1564
+ * mais próximo de outra coisa. Sem seta de troca: `$gray_dark` não existe num `.tsx`
1565
+ * e a troca segura depende de duas provas que esta camada não tem - ver
1566
+ * `TheirName.writable`.
1567
+ */
1568
+ x.theirCompiledName
1569
+ ? `· your code calls it ${x.theirCompiledName}`
1570
+ : near
1571
+ ? `→ nearest is ${near.theirs ?? near.name} (${near.value})`
1572
+ : "→ no token holds this value yet";
1540
1573
  say(` ${String(x.line).padStart(4)} ${x.literal} ${named}`);
1541
1574
  }
1542
1575
  if (f.findings.length > shown.length) {
@@ -1891,11 +1924,25 @@ export async function doctor(opts) {
1891
1924
  * que NENHUM dos dois nomeia o valor. É isso que a linha passa a dizer, e é o que separa esta
1892
1925
  * fila da anterior: aquela tem nome esperando, esta precisa de um.
1893
1926
  */
1894
- what: near
1895
- ? `${r.literal} - name it, or snap to ${near.theirs ?? near.name}`
1896
- : r.crossFamily
1897
- ? `${r.literal} - no ${r.kind} named for it, and the value lives as ${r.token} in another family`
1898
- : `${r.literal} - no name for it, here or in your CSS`,
1927
+ /**
1928
+ * O SEU CÓDIGO JÁ NOMEIA ISTO, e a linha dizia o contrário.
1929
+ *
1930
+ * "no name for it, here or in your CSS" é uma afirmação sobre o repositório DELE, e ela era
1931
+ * falsa sempre que o nome estava num pré-processador: medido em 07/09 no `web-subscribe`,
1932
+ * `#555` é `$gray_dark` em `css/colors.scss` e aparecia como "sem nome" em 27 arquivos.
1933
+ *
1934
+ * A linha DIZ o nome e não convida a trocar - `$gray_dark` não existe num `.tsx`, e a troca
1935
+ * segura depende de o arquivo de destino compilar aquele pré-processador e importar aquela
1936
+ * folha, que é o próximo passo desta frente. Dizer o que existe já muda a decisão dele; trocar
1937
+ * sem essas duas provas quebraria o build.
1938
+ */
1939
+ what: r.theirCompiledName
1940
+ ? `${r.literal} - your code calls it ${r.theirCompiledName} (no fix: it never reaches the browser)`
1941
+ : near
1942
+ ? `${r.literal} - name it, or snap to ${near.theirs ?? near.name}`
1943
+ : r.crossFamily
1944
+ ? `${r.literal} - no ${r.kind} named for it, and the value lives as ${r.token} in another family`
1945
+ : `${r.literal} - no name for it, here or in your CSS`,
1899
1946
  size: `${r.files} file${r.files === 1 ? "" : "s"}`,
1900
1947
  cheap: false,
1901
1948
  });
@@ -540,7 +540,18 @@ function scanCore(file, source, table) {
540
540
  ...(/(?<!r)em$/i.test(literal.trim())
541
541
  ? { fontRelative: true }
542
542
  : {}),
543
- ...(theirs ? { theirToken: theirs.name } : {}),
543
+ ...(theirs?.writable ? { theirToken: theirs.name } : {}),
544
+ /**
545
+ * O NOME QUE SÓ SE DIZ - `$gray_dark` do Sass, `@brand` do Less.
546
+ *
547
+ * Separado de `theirToken` porque aquele campo é o que o `--fix` ESCREVE, e um nome de
548
+ * pré-processador escrito num `.tsx` é código quebrado. Aqui ele serve ao relatório, que
549
+ * deixa de afirmar "no name for it, here or in your CSS" sobre um valor que o código dele
550
+ * nomeia - a afirmação era sobre o repositório DELE, e era falsa.
551
+ */
552
+ ...(theirs && !theirs.writable
553
+ ? { theirCompiledName: theirs.name }
554
+ : {}),
544
555
  ...(theirs?.also.length ? { theirAlso: theirs.also } : {}),
545
556
  /**
546
557
  * A COINCIDÊNCIA VIAJA COM O ACHADO - ver `tokenMatch`.
@@ -737,6 +748,9 @@ export function diagnose(files) {
737
748
  kind: f.kind,
738
749
  token: f.token,
739
750
  ...(f.theirToken ? { theirToken: f.theirToken } : {}),
751
+ ...(f.theirCompiledName
752
+ ? { theirCompiledName: f.theirCompiledName }
753
+ : {}),
740
754
  ...(f.theirAlso?.length ? { theirAlso: f.theirAlso } : {}),
741
755
  ...(f.fontRelative ? { fontRelative: true } : {}),
742
756
  count: 1,
@@ -745,25 +759,31 @@ export function diagnose(files) {
745
759
  });
746
760
  }
747
761
  }
748
- const repeats = [...byLiteral.entries()]
762
+ const everyLiteral = [...byLiteral.entries()]
749
763
  .map(([key, v]) => ({
750
764
  literal: key.slice(key.indexOf(":") + 1),
751
765
  kind: v.kind,
752
766
  token: v.token,
753
767
  ...(v.theirToken ? { theirToken: v.theirToken } : {}),
768
+ ...(v.theirCompiledName
769
+ ? { theirCompiledName: v.theirCompiledName }
770
+ : {}),
754
771
  ...(v.theirAlso?.length ? { theirAlso: v.theirAlso } : {}),
755
772
  ...(v.fontRelative ? { fontRelative: true } : {}),
756
773
  count: v.count,
757
774
  files: v.files.size,
758
775
  ...(v.crossFamily ? { crossFamily: true } : {}),
759
776
  }))
760
- .filter((r) => r.count > 1)
761
777
  .sort((a, b) => b.count - a.count);
778
+ const repeats = everyLiteral.filter((r) => r.count > 1);
779
+ /** Ver `Diagnosis.once` - o que o corte por repetição deixa de fora. */
780
+ const once = everyLiteral.filter((r) => r.count === 1);
762
781
  return {
763
782
  // A file with a phantom name and no drift has nothing to say by the old
764
783
  // measure and the worst thing to say by the new one. Both keep it.
765
784
  files: files.filter((f) => f.findings.length > 0 || (f.phantoms?.length ?? 0) > 0),
766
785
  findings: flat,
786
+ once,
767
787
  counts,
768
788
  /**
769
789
  * QUANTOS JÁ TÊM NOME NESTE REPOSITÓRIO - e o dele conta.
@@ -450,6 +450,26 @@ export function describeValueRuler(values) {
450
450
  const decisions = values.seen - values.structure;
451
451
  const closed = values.interpreted + values.answered;
452
452
  const percent = decisions > 0 ? Math.round((closed / decisions) * 100) : 0;
453
+ /**
454
+ * NADA PARA MEDIR NÃO É ZERO POR CENTO - e a frase dizia zero três vezes.
455
+ *
456
+ * O QUE O CLIENTE VIA, caminhado em 07/09 num `create-next-app` recém-criado: *"Of the 0 class
457
+ * declarations on your components, 0 are structure. Of the 0 design decisions, 0 are interpreted -
458
+ * 0%."* Três zeros e um 0% sobre um projeto onde a régua não tinha o que medir, porque ele ainda
459
+ * não escreveu um componente - só páginas, e uma página não é componente de design system, o que o
460
+ * próprio relatório explica seis linhas acima.
461
+ *
462
+ * `0%` é um número de DESEMPENHO, e ali não houve desempenho nenhum. É a lei 14 do `CLAUDE.md`:
463
+ * *"um zero pelado lê como falha nossa; um zero com motivo lê como fato"* - e para quem acabou de
464
+ * apontar a plataforma para o próprio projeto, uma linha de 0% é a primeira impressão.
465
+ *
466
+ * A frase que substitui diz o FATO e o caminho, sem inventar percentual: não há componente para
467
+ * medir ainda.
468
+ */
469
+ if (values.seen === 0)
470
+ return [
471
+ "No component of yours carries a class declaration yet - so there is nothing for this ruler to read. It starts answering once a component exists, and pages do not count as components.",
472
+ ];
453
473
  return [
454
474
  `Of the ${values.seen} class declarations on your components, ${values.structure} are structure (layout plumbing, not design decisions). Of the ${decisions} design decisions, ${closed} are interpreted${values.answered > 0 ? ` (${values.answered} of them answered by you)` : ""} - ${percent}%.`,
455
475
  ];
@@ -0,0 +1,121 @@
1
+ import { dirname, join, resolve } from "node:path";
2
+ /**
3
+ * OS NOMES DELE QUE SOMEM NA COMPILAÇÃO - e que a plataforma afirmava não existir.
4
+ *
5
+ * O QUE O CLIENTE VIA. Um projeto que declara `$gray_dark: #555` em `css/colors.scss` e escreve
6
+ * `#555` em dezenas de arquivos recebia, do nosso relatório: *"#555 - no name for it, here or in
7
+ * your CSS"*. A frase é uma afirmação sobre o repositório DELE, e ela era falsa. Medido em 07/09 no
8
+ * `~/projects/web-subscribe`: `doctor` dizia `13 tokens found` num projeto com 27 nomes de cor
9
+ * declarados e partilhados, e 323 ocorrências ficavam sem o nome que o próprio código já lhes dá.
10
+ *
11
+ * POR QUE A PLATAFORMA NÃO OS VIA, e não era descuido: a colheita lê custom properties (`--x`), que
12
+ * é o que o navegador recebe. Uma variável de pré-processador - `$x` no Sass, `@x` no Less - resolve
13
+ * em tempo de compilação e não chega ao CSS final. Ela é vocabulário dele para LER, nunca endereço
14
+ * para APONTAR: escrever `var($gray_dark)` não existe.
15
+ *
16
+ * ─────────────────────────────────────────────────────────────────────────
17
+ * A REGRA QUE SEPARA A FEATURE DO DEFEITO: ESCOPO.
18
+ *
19
+ * Uma custom property em `:root` é global por construção - é isso que a torna vocabulário. Um `$x` no
20
+ * topo de um arquivo é uma constante DAQUELE arquivo, e tratar as duas como a mesma coisa inventa
21
+ * vocabulário em vez de ler o que existe.
22
+ *
23
+ * Medido no mesmo repositório, e a diferença é a feature inteira:
24
+ *
25
+ * 323 ocorrências ganham o nome certo ($ declarado em folha que outra folha importa)
26
+ * 1085 ganhariam um nome FALSO ($color e $primary-color declarados dentro de um
27
+ * styles.module.scss de um componente - `$color: #FFF`
28
+ * existe em dois módulos diferentes, e `#fff` aparece
29
+ * 596 vezes no projeto inteiro)
30
+ *
31
+ * Então só conta o que é PARTILHADO: um arquivo que outro arquivo importa foi escrito para ser
32
+ * vocabulário comum, e quem decide isso é o `@import`/`@use`/`@forward` dele - não uma convenção de
33
+ * nome de arquivo nossa, que seria a forma de fixar o hábito de um repositório dentro do produto.
34
+ */
35
+ /** As extensões cujo vocabulário some antes do navegador. */
36
+ const PREPROCESSED = /\.(scss|sass|less)$/i;
37
+ /** `$nome: valor;` no Sass, `@nome: valor;` no Less - no topo do arquivo, fora de qualquer bloco. */
38
+ const DECLARATION = /^[ \t]*([$@][a-zA-Z0-9_-]+)[ \t]*:[ \t]*([^;{}]+);/gm;
39
+ /**
40
+ * `!default` É A MARCA DA PRÓPRIA LINGUAGEM PARA "ISTO É O PADRÃO DE UMA BIBLIOTECA".
41
+ *
42
+ * Em Sass ele significa literalmente *este valor vale se quem me usa não definiu outro* - a forma
43
+ * como um pacote publica vocabulário configurável. Um valor que ELE decidiu não carrega `!default`,
44
+ * e é essa diferença que separa a decisão dele do default de um pacote que alguém copiou para
45
+ * dentro do repositório.
46
+ *
47
+ * Medido em 07/09 no `~/projects/web-subscribe`, que tem `bootstrap-sass` e `ionicons` copiados para
48
+ * `css/`: dos 1185 nomes que a varredura encontra, 1125 são desses dois pacotes e TODOS trazem
49
+ * `!default`. Sem esta porta, o relatório responderia que o `#555` dele "se chama
50
+ * `$navbar-default-link-active-color`" - o vocabulário de uma biblioteca apresentado como o dele,
51
+ * que é o oposto exato do que este leitor existe para fazer.
52
+ *
53
+ * O LADO SEGURO DO ERRO, declarado: um projeto que use `!default` no PRÓPRIO vocabulário perde esses
54
+ * nomes e volta a ver "no name for it". Deixar de nomear o que existe custa uma linha de relatório;
55
+ * batizar o valor dele com a palavra de um pacote custa a confiança na próxima linha.
56
+ */
57
+ const LIBRARY_DEFAULT = /!\s*default\b/i;
58
+ /** `@import "colors"`, `@use "./colors" as c`, `@forward "colors"`. */
59
+ const REFERENCE = /@(?:import|use|forward)\s+["']([^"']+)["']/g;
60
+ /**
61
+ * QUAIS FOLHAS OUTRA FOLHA IMPORTA - a prova de que aquele arquivo é vocabulário comum.
62
+ *
63
+ * A resolução segue o que o Sass faz: a extensão é opcional e o parcial pode ter o `_` na frente.
64
+ * Um especificador que não aponta para nenhum arquivo do projeto é um pacote (`@import "bootstrap"`),
65
+ * e pacote não é vocabulário dele.
66
+ */
67
+ export function sharedSheets(sheets) {
68
+ const known = new Set(sheets.keys());
69
+ const shared = new Set();
70
+ for (const [path, source] of sheets) {
71
+ const dir = dirname(path);
72
+ for (const [, spec] of source.matchAll(REFERENCE)) {
73
+ const bare = spec.replace(/\.(scss|sass|less|css)$/i, "");
74
+ const base = bare.split("/").pop() ?? bare;
75
+ const parent = bare.slice(0, bare.length - base.length);
76
+ for (const candidate of [
77
+ `${bare}.scss`,
78
+ `${bare}.sass`,
79
+ `${bare}.less`,
80
+ `${bare}.css`,
81
+ join(parent, `_${base}.scss`),
82
+ join(parent, `_${base}.sass`),
83
+ join(parent, `_${base}.less`),
84
+ ]) {
85
+ const abs = resolve(dir, candidate);
86
+ if (known.has(abs)) {
87
+ shared.add(abs);
88
+ break;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ return shared;
94
+ }
95
+ /**
96
+ * O VOCABULÁRIO PARTILHADO QUE O NAVEGADOR NUNCA VÊ.
97
+ *
98
+ * A primeira declaração vence, a mesma regra que a colheita de custom properties usa: uma folha
99
+ * posterior sobrescrevendo uma anterior é cascata, e este leitor não vê cascata.
100
+ */
101
+ export function compiledNames(sheets) {
102
+ const shared = sharedSheets(sheets);
103
+ const out = [];
104
+ const seen = new Set();
105
+ for (const [path, source] of sheets) {
106
+ if (!PREPROCESSED.test(path) || !shared.has(path))
107
+ continue;
108
+ for (const [, name, value] of source.matchAll(DECLARATION)) {
109
+ if (seen.has(name) || LIBRARY_DEFAULT.test(value))
110
+ continue;
111
+ seen.add(name);
112
+ /** `!global` diz onde a atribuição vale, não o que ela vale - sai do valor e não da lista. */
113
+ out.push({
114
+ name,
115
+ value: value.replace(/!\s*global\b/i, "").trim(),
116
+ file: path,
117
+ });
118
+ }
119
+ }
120
+ return out;
121
+ }
@@ -47,7 +47,12 @@ import { FAMILY_PREFIX, familySays, formDecides, normalizeValue, UNAMBIGUOUS, }
47
47
  * que o próprio css dele nomeia `--dashboard-font-family`. É pouco, e é o único grupo que sobrou do
48
48
  * item que eu tinha nomeado - os `em` saíram na leva anterior.
49
49
  */
50
- const segments = (name) => name.replace(/^--/, "").split("-");
50
+ /**
51
+ * O nome sem o sigilo, em pedaços. `$` e `@` entram porque o vocabulário dele pode estar num
52
+ * pré-processador (ver `their-compiled-names.ts`), e um `$gray-dark` que não perdesse o `$` nunca
53
+ * casaria segmento nenhum no desempate - o critério existiria e nunca alcançaria esses nomes.
54
+ */
55
+ const segments = (name) => name.replace(/^(--|[$@])/, "").split("-");
51
56
  /**
52
57
  * QUAL DOS NOMES DELE, quando mais de um segura o mesmo valor - e são 18 no repo real.
53
58
  *
@@ -132,13 +137,20 @@ roles) {
132
137
  * sistema do dono, `#f59e0b` é `--color-tier-gold` E `--color-feedback-warning` - dois conceitos
133
138
  * dele, gamificação e estado -, e o relatório dizia `→ {color.tier-gold}` como se fosse fato.
134
139
  */
135
- function escolha(candidates, ours, convention, roles) {
140
+ function escolha(candidates, ours, convention, roles,
141
+ /** `false` para nome de pré-processador - ver `TheirName.writable`. */
142
+ writable = true) {
136
143
  const name = pick(candidates, ours, convention, roles);
137
- return { name, also: candidates.filter((c) => c !== name) };
144
+ return { name, also: candidates.filter((c) => c !== name), writable };
138
145
  }
139
146
  export function theirNames(ours, theirs) {
140
147
  const out = new Map();
141
- if (theirs.byName.size === 0)
148
+ /**
149
+ * AS DUAS FONTES, e não só a primeira: um projeto que declara o vocabulário INTEIRO em Sass tem
150
+ * `byName` vazio e mesmo assim tem nomes para dizer. Sair aqui devolvia mapa vazio para ele, e o
151
+ * relatório voltava a afirmar que o repositório não nomeia um valor que o repositório nomeia.
152
+ */
153
+ if (theirs.byName.size === 0 && theirs.compiledAway.size === 0)
142
154
  return out;
143
155
  /**
144
156
  * OS PAPÉIS QUE ESTE SISTEMA DECLARA, derivados dos NOSSOS nomes - ver o critério 2 de `pick`.
@@ -205,6 +217,29 @@ export function theirNames(ours, theirs) {
205
217
  out.set(key, escolha(candidates, null, convention, roles));
206
218
  }
207
219
  }
220
+ /**
221
+ * POR ÚLTIMO, O QUE SÓ SE DIZ - e ser o último é a regra, não a ordem do arquivo.
222
+ *
223
+ * Onde ele nomeia o mesmo valor nas duas formas, quem ganha é a custom property: ela resolve no
224
+ * navegador, o `--fix` pode escrevê-la e ela sobrevive a qualquer mudança de pré-processador. Só
225
+ * quando NENHUM nome que o navegador recebe segura aquele valor é que o nome compilado entra - e
226
+ * entra marcado como não escrevível.
227
+ *
228
+ * A mesma porta da forma vale aqui: `UNAMBIGUOUS` deixa passar o que a FORMA já classifica (cor,
229
+ * duração, pilha de fontes) e deixa o comprimento cru de fora. Sem isso, um `$mobile_gutter: 10px`
230
+ * seria oferecido como o nome de todo `10px` do projeto - medido em 07/09 no `web-subscribe`, onde
231
+ * `10px` aparece em 102 arquivos e quase nenhum é um gutter.
232
+ */
233
+ for (const [value, names] of theirs.compiledAway) {
234
+ for (const [kind, form] of Object.entries(UNAMBIGUOUS)) {
235
+ if (!form.test(value))
236
+ continue;
237
+ const key = `${kind}:${value}`;
238
+ if (out.has(key))
239
+ continue;
240
+ out.set(key, escolha(names.map((n) => n.name), null, convention, roles, false));
241
+ }
242
+ }
208
243
  return out;
209
244
  }
210
245
  /**
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { DEFAULT_ROOT_PX } from "./root-size.js";
15
15
  import { parseSchemeBlocks } from "./scheme-blocks.js";
16
+ import { compiledNames } from "./their-compiled-names.js";
16
17
  export const EMPTY_TABLE = {
17
18
  source: null,
18
19
  name: null,
@@ -24,6 +25,7 @@ export const EMPTY_TABLE = {
24
25
  rootFrom: null,
25
26
  aliases: new Map(),
26
27
  declared: new Set(),
28
+ compiledAway: new Map(),
27
29
  keyframes: new Set(),
28
30
  };
29
31
  const hex2 = (n) => Math.max(0, Math.min(255, Math.round(n)))
@@ -318,6 +320,17 @@ export function buildTable(input) {
318
320
  const byName = source === "installed"
319
321
  ? parseTokens(input.css)
320
322
  : parseRootTokens(input.css);
323
+ /**
324
+ * O VOCABULÁRIO DELE QUE NÃO CHEGA AO NAVEGADOR, somado ao que chega.
325
+ *
326
+ * Entra DEPOIS das custom properties e sem sobrescrever nenhuma: onde as duas formas nomeiam o
327
+ * mesmo valor, o nome que o navegador recebe é o que resolve, e é ele que a folha pode apontar.
328
+ */
329
+ const compiled = source === "installed" || !input.sheets ? [] : compiledNames(input.sheets);
330
+ const compiledAway = new Map();
331
+ for (const one of compiled)
332
+ if (!byName.has(one.name))
333
+ compiledAway.set(one.name, one);
321
334
  /**
322
335
  * Built from the RESOLVED map, not from `byName`. A semantic role that
323
336
  * aliases a primitive has to be findable by the primitive's value, or the
@@ -337,6 +350,24 @@ export function buildTable(input) {
337
350
  */
338
351
  const raw = source === "installed" ? parseTokensWithAliases(input.css) : new Map();
339
352
  const isAlias = (name) => /^var\(/.test((raw.get(name) ?? "").trim());
353
+ /**
354
+ * OS NOMES QUE SÓ SE DIZEM, indexados pelo MESMO valor normalizado das outras tabelas.
355
+ *
356
+ * Fora de `byName` e de `byValue` de propósito: aquelas duas respondem "o meu sistema nomeia
357
+ * isto?", que é a pergunta da cobertura e do `--fix`. Um nome que o navegador nunca recebe não
358
+ * pode entrar em nenhuma das duas - inflaria a contagem de tokens dele (medido em 07/09 no
359
+ * `web-subscribe`: 13 viraria 1198, e 1125 dos acrescentados eram bootstrap e ionicons copiados
360
+ * para dentro do repositório) e ofereceria ao `--fix` um nome que quebra um `.tsx`.
361
+ */
362
+ const byCompiledValue = new Map();
363
+ for (const one of compiledAway.values()) {
364
+ const key = normalizeValue(one.value, rootPx);
365
+ const list = byCompiledValue.get(key);
366
+ if (list)
367
+ list.push(one);
368
+ else
369
+ byCompiledValue.set(key, [one]);
370
+ }
340
371
  const byValue = new Map();
341
372
  for (const [name, value] of resolved) {
342
373
  const key = normalizeValue(value, rootPx);
@@ -360,6 +391,7 @@ export function buildTable(input) {
360
391
  /** Vazio até `withTheirNames` ler o vocabulário dele - ver `their-names.ts`. */
361
392
  aliases: new Map(),
362
393
  declared: parseDeclaredNames(input.css),
394
+ compiledAway: byCompiledValue,
363
395
  keyframes: new Set([...input.css.matchAll(/@keyframes\s+([a-zA-Z0-9_-]+)/g)].map((m) => m[1])),
364
396
  };
365
397
  }
@@ -418,7 +450,11 @@ kind) {
418
450
  const theirs = kind
419
451
  ? table.aliases.get(`${kind}:${normalizeValue(best.value, table.rootPx)}`)
420
452
  : undefined;
421
- return theirs ? { ...best, theirs: theirs.name } : best;
453
+ /**
454
+ * SÓ UM NOME ESCREVÍVEL - esta linha convida a trocar (`snap to X`), e um nome de pré-processador
455
+ * não pode ir para o código dele. Ver `TheirName.writable`.
456
+ */
457
+ return theirs?.writable ? { ...best, theirs: theirs.name } : best;
422
458
  }
423
459
  /**
424
460
  * The family a token belongs to, from the drift it was found in.
package/dist/stack.js CHANGED
@@ -235,11 +235,39 @@ classStyle) {
235
235
  *
236
236
  * `null` quando o pacote não está lá ou quando a faixa não diz um número - um intervalo que não se
237
237
  * resolve é ausência de resposta, e supor a v4 seria escolher o mais novo por conveniência nossa.
238
+ *
239
+ * ─────────────────────────────────────────────────────────────────────────
240
+ * O MAIOR SOZINHO É UMA FAIXA, e exigir o ponto apagava metade dos projetos v4.
241
+ *
242
+ * A primeira versão desta função casava `(\d+)\.` - um número SEGUIDO DE PONTO. `^4.1.2` respondia
243
+ * 4; `^4`, que é exatamente o que o `create-next-app` e o instalador do Tailwind v4 escrevem,
244
+ * respondia `null`. E `null` aqui não é neutro: é a plataforma dizendo *não sei qual versão* sobre um
245
+ * projeto que declara a versão na primeira linha do `package.json` dele.
246
+ *
247
+ * Medido em 07/09 nos sete projetos com Tailwind desta máquina:
248
+ *
249
+ * "^4" 3 projetos -> null test_ds_01, web-subscribe, web-dashboard
250
+ * "^3.4.1" 1 projeto -> 3
251
+ * "^3.3.5" 1 projeto -> 3
252
+ * "3.4.3" 1 projeto -> 3
253
+ * "4.1.11" 1 projeto -> 4
254
+ *
255
+ * Os três invisíveis são os três v4 - e não por acaso: é a v4 que se instala como `^4`. Quem paga é
256
+ * quem começa um projeto HOJE.
257
+ *
258
+ * A AMBIGUIDADE CONTINUA VALENDO `null`, e agora ela é medida em vez de suposta: cada faixa da
259
+ * expressão diz um maior, e a resposta só existe quando todas dizem o MESMO. `>=3 <5` continua sendo
260
+ * ausência de resposta, porque é; `^4` deixa de ser, porque não é.
238
261
  */
239
262
  export function tailwindMajor(deps) {
240
263
  const raw = deps.tailwindcss;
241
264
  if (!raw)
242
265
  return null;
243
- const m = /(\d+)\./.exec(raw.replace(/^[^0-9]*/, ""));
244
- return m ? Number(m[1]) : null;
266
+ const majors = new Set();
267
+ for (const part of raw.split(/\s*\|\|\s*|\s+|,/).filter(Boolean)) {
268
+ const m = /(\d+)/.exec(part.replace(/^[^0-9]*/, ""));
269
+ if (m)
270
+ majors.add(Number(m[1]));
271
+ }
272
+ return majors.size === 1 ? [...majors][0] : null;
245
273
  }
@@ -101,7 +101,14 @@ export function pointAtTheirNames(css, theirs, resolvable) {
101
101
  * buildou não tem como provar o que resolve, e um par afirmado sem prova é pior que par nenhum.
102
102
  */
103
103
  if (!resolvable || theirs.byName.size === 0)
104
- return { css, pointed: 0, pruned: 0, cycles: 0, pairs: [] };
104
+ return {
105
+ css,
106
+ pointed: 0,
107
+ pruned: 0,
108
+ compiledAway: 0,
109
+ cycles: 0,
110
+ pairs: [],
111
+ };
105
112
  const ours = buildTable({ css, source: "installed" });
106
113
  const alias = theirNames(ours, theirs);
107
114
  /**
@@ -116,6 +123,7 @@ export function pointAtTheirNames(css, theirs, resolvable) {
116
123
  bridged.set(theirName, ourName);
117
124
  let pointed = 0;
118
125
  let pruned = 0;
126
+ let compiledAway = 0;
119
127
  let cycles = 0;
120
128
  /** Ver `PointedAt.pairs`: o mapa sai do mesmo casamento que reescreve a linha. */
121
129
  const pairs = [];
@@ -126,9 +134,19 @@ export function pointAtTheirNames(css, theirs, resolvable) {
126
134
  const kind = Object.entries(FAMILY_KIND).find(([prefix]) => name.startsWith(prefix))?.[1];
127
135
  if (!kind)
128
136
  return line;
129
- const theirName = alias.get(`${kind}:${normalizeValue(value.trim(), ours.rootPx)}`)?.name;
137
+ const theirs_ = alias.get(`${kind}:${normalizeValue(value.trim(), ours.rootPx)}`);
138
+ const theirName = theirs_?.name;
130
139
  if (!theirName || theirName === name)
131
140
  return line;
141
+ /**
142
+ * UM NOME DE PRÉ-PROCESSADOR NÃO É UM ENDEREÇO. `var($gray_dark)` não existe em CSS nenhum,
143
+ * então a linha fica com o valor - e a recusa se conta pela SUA causa, não como se o build
144
+ * dele tivesse podado um token que ele pode voltar a usar.
145
+ */
146
+ if (!theirs_.writable) {
147
+ compiledAway += 1;
148
+ return line;
149
+ }
132
150
  /**
133
151
  * A PONTE JÁ APONTA PARA CÁ - então apontar de volta fecha um ciclo, e um ciclo não deixa o
134
152
  * valor errado: deixa a propriedade INVÁLIDA. A utility dele já resolve pelo nosso token, que é
@@ -146,7 +164,7 @@ export function pointAtTheirNames(css, theirs, resolvable) {
146
164
  pairs.push({ ours: name, theirs: theirName, value: value.trim() });
147
165
  return `${indent}${name}${sep}var(${theirName});`;
148
166
  });
149
- return { css: out, pointed, pruned, cycles, pairs };
167
+ return { css: out, pointed, pruned, compiledAway, cycles, pairs };
150
168
  }
151
169
  /**
152
170
  * O PREFIXO DA NOSSA VARIÁVEL -> A FAMÍLIA, e a chave que `theirNames` devolve é `<família>:<valor>`.
@@ -171,7 +189,14 @@ const FAMILY_KIND = {
171
189
  export async function pointTokensAtTheirNames(root, payload) {
172
190
  const css = payload.artifacts["tokens.css"] ?? "";
173
191
  if (!css)
174
- return { css, pointed: 0, pruned: 0, cycles: 0, pairs: [] };
192
+ return {
193
+ css,
194
+ pointed: 0,
195
+ pruned: 0,
196
+ compiledAway: 0,
197
+ cycles: 0,
198
+ pairs: [],
199
+ };
175
200
  const [theirs, resolvable] = await Promise.all([
176
201
  harvestTheirCss(root),
177
202
  resolvableVars(root),
@@ -190,6 +215,9 @@ async function harvestTheirCss(root) {
190
215
  "_synthesisui",
191
216
  ]);
192
217
  let css = "";
218
+ /** As folhas UMA A UMA, porque a regra de escopo de `their-compiled-names.ts` precisa saber qual
219
+ * arquivo importa qual - a concatenação apaga exatamente essa informação. */
220
+ const sheets = new Map();
193
221
  const walk = async (dir) => {
194
222
  for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
195
223
  if (skip.has(entry.name) || entry.name.startsWith("."))
@@ -197,12 +225,15 @@ async function harvestTheirCss(root) {
197
225
  const path = join(dir, entry.name);
198
226
  if (entry.isDirectory())
199
227
  await walk(path);
200
- else if (/\.(css|scss|sass|less)$/i.test(entry.name))
201
- css += `\n${await readFile(path, "utf8").catch(() => "")}`;
228
+ else if (/\.(css|scss|sass|less)$/i.test(entry.name)) {
229
+ const source = await readFile(path, "utf8").catch(() => "");
230
+ sheets.set(path, source);
231
+ css += `\n${source}`;
232
+ }
202
233
  }
203
234
  };
204
235
  await walk(root);
205
- return buildTable({ css, source: "yours" });
236
+ return buildTable({ css, source: "yours", sheets });
206
237
  }
207
238
  /**
208
239
  * AS LINHAS QUE APONTAM PARA UM NOME DELE QUE NÃO EXISTE MAIS.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.390",
3
+ "version": "0.16.395",
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": {