synthesisui 0.16.498 → 0.16.499

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.
@@ -2,7 +2,8 @@ import { describeDistinct, describeDistinctVerbose, distinctValues } from "../do
2
2
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { readStyleSheet, scssVariables } from "../doctor/scss-lens.js";
4
4
  import { THEME_SOURCE, tsTokenObjects, tsTokensAsVars } from "../doctor/ts-tokens-lens.js";
5
- import { dirname, join, relative, resolve } from "node:path";
5
+ import { dirname, join, relative, resolve, sep } from "node:path";
6
+ import { SET_PROPERTY } from "../doctor/broken-refs.js";
6
7
  import { describeIntent, intentOf, readProjectConfig } from "../config.js";
7
8
  import { describeFix, planFix, readerFor, writeFix, } from "../doctor/apply-fix.js";
8
9
  import { baselineOf, compareToBaseline, describeVerdict, githubAnnotations, sarifOf, } from "../doctor/ci-format.js";
@@ -777,6 +778,28 @@ export async function doctor(opts) {
777
778
  for await (const file of walkAll([root]))
778
779
  if (THEME_SOURCE.test(file) && !/\.d\.ts$/.test(file))
779
780
  tsObjects.push(...tsTokenObjects(await readFile(file, "utf8").catch(() => "")));
781
+ /**
782
+ * O QUE O JAVASCRIPT DELE MEDE, por pasta - ver `TokenTable.measuredInJs` (aula 01, 15/10). A reserva de
783
+ * `var(--sui-hero-name, clamp(...))` sai da conta só quando a pasta do CSS é a que grava a variável.
784
+ */
785
+ const measuredInJs = new Map();
786
+ for await (const file of walkAll([root])) {
787
+ if (!/\.(tsx|jsx|ts|js|mjs)$/.test(file) || /\.d\.ts$/.test(file))
788
+ continue;
789
+ const text = await readFile(file, "utf8").catch(() => "");
790
+ if (!text.includes("setProperty"))
791
+ continue;
792
+ const rel = relative(root, file);
793
+ const dir = rel.includes(sep) ? rel.slice(0, rel.lastIndexOf(sep)).split(sep).join("/") : "";
794
+ for (const m of text.matchAll(SET_PROPERTY)) {
795
+ const name = m[1].toLowerCase();
796
+ const dirs = measuredInJs.get(name) ?? [];
797
+ if (!dirs.includes(dir))
798
+ measuredInJs.set(name, [...dirs, dir]);
799
+ }
800
+ }
801
+ if (measuredInJs.size > 0)
802
+ table = { ...table, measuredInJs };
780
803
  const tally = emptyTally();
781
804
  const internalSpecs = await internalSpecifiers(root);
782
805
  for await (const file of scopes.length > 0
@@ -40,7 +40,7 @@ const VAR_REF = /var\(\s*(--[a-zA-Z0-9_-]+)\s*(,)?/g;
40
40
  * Medido em quatro populações: 2 nomes no `codelevel-ui`, 0 no `packages/ui` do `frontend-hub`, 2 no
41
41
  * dashboard dele e 3 no nosso próprio app. Três de quatro escrevem variável por JS.
42
42
  */
43
- const SET_PROPERTY = /setProperty\(\s*['"`](--[a-zA-Z0-9_-]+)/g;
43
+ export const SET_PROPERTY = /setProperty\(\s*['"`](--[a-zA-Z0-9_-]+)/g;
44
44
  /**
45
45
  * VARIABLES A HEADLESS LIBRARY SETS AT RUNTIME, which no stylesheet can declare.
46
46
  *
@@ -174,6 +174,15 @@ const SPACING_DECL = new RegExp(`(?:padding|margin|gap)(?:[A-Z][a-z]+|(?:-[a-z]+
174
174
  const SPACING_UTIL = /(?:\b[pmg](?:[trblxy])?-\[|gap-\[)([^\]]*)/g;
175
175
  /** A font stack written by hand rather than taken from the type scale. */
176
176
  const FONT = /font-family\s*:\s*([^;}\n]+)/g;
177
+ /**
178
+ * O TAMANHO DE TEXTO ESCRITO À MÃO (defeito 3 da aula 01, 15/10) - `font-size: 14px`, `fontSize: "1rem"`,
179
+ * `[font-size:0.875rem]`. Não havia leitor nenhum: `distinct-values` contava esses valores como "by hand" e a cobertura,
180
+ * na mesma tela, não, então o `--fix` não tinha o que trocar com `--text-sm` declarado no projeto. A família é `font`,
181
+ * porque é a palavra que os nomes de escala de tipo carregam (`--text-sm`, `--ds-typography-...`).
182
+ */
183
+ const FONT_SIZE_DECL = new RegExp(`font-?[Ss]ize${DECL_VALUE}`, "g");
184
+ /** `text-[14px]` - o valor entre colchetes; `text-[#111]` não tem comprimento e não rende nada. */
185
+ const FONT_SIZE_UTIL = /\btext-\[([^\]]*)/g;
177
186
  /**
178
187
  * MOTION, the family the scan never read (item 12, F3).
179
188
  *
@@ -351,6 +360,38 @@ function fallbackSpans(line, table) {
351
360
  }
352
361
  return out;
353
362
  }
363
+ /** `var(--x, clamp(4rem, 17vw, 16rem))` - o nome e a reserva INTEIRA, com os parênteses de dentro contados. */
364
+ function fallbacksOf(line) {
365
+ const out = [];
366
+ for (const m of line.matchAll(VAR_WITH_FALLBACK)) {
367
+ const start = (m.index ?? 0) + m[0].length;
368
+ let depth = 1;
369
+ let end = start;
370
+ for (; end < line.length && depth > 0; end++) {
371
+ if (line[end] === "(")
372
+ depth++;
373
+ else if (line[end] === ")")
374
+ depth--;
375
+ }
376
+ if (depth === 0)
377
+ out.push({ name: m[1].toLowerCase(), span: [start, end - 1] });
378
+ }
379
+ return out;
380
+ }
381
+ /**
382
+ * A RESERVA DE UM TAMANHO QUE O COMPONENTE MEDE SOZINHO - ver `TokenTable.measuredInJs` (aula 01, 15/10).
383
+ *
384
+ * Só quando um arquivo da MESMA pasta grava a variável: um `var(--qualquer, 18px)` que nada grava continua acusado, senão
385
+ * a reserva vira esconderijo de valor à mão.
386
+ */
387
+ function measuredSpans(file, line, table) {
388
+ if (!table.measuredInJs?.size)
389
+ return [];
390
+ const dir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
391
+ return fallbacksOf(line)
392
+ .filter((f) => table.measuredInJs?.get(f.name)?.includes(dir))
393
+ .map((f) => f.span);
394
+ }
354
395
  /**
355
396
  * ONDE A CASCATA DO APP NÃO ALCANÇA - e trocar por `var()` ali APAGA a cor.
356
397
  *
@@ -754,6 +795,8 @@ function scanCore(file, source, table) {
754
795
  const declared = declarationSpans(line, inTokenBlock && !opensTokenBlock);
755
796
  const inDeclaration = (at) => declared.some(([a, b]) => at >= a && at < b);
756
797
  const spans = fallbackSpans(line, table);
798
+ const measured = measuredSpans(file, line, table);
799
+ const inMeasured = (at) => measured.some(([a, b]) => at >= a && at <= b);
757
800
  const inFallback = (at) => spans.some(([a, b]) => at >= a && at <= b);
758
801
  /** Ver `markupTextSpans`: HTML montado como texto sai do alcance da folha do app. */
759
802
  const markup = markupTextSpans(file, line);
@@ -770,6 +813,10 @@ function scanCore(file, source, table) {
770
813
  /** A definição do token não é drift - ver `declarationSpans`. */
771
814
  if (col >= 0 && inDeclaration(col))
772
815
  return;
816
+ if (col >= 0 && inMeasured(col)) {
817
+ setAside("the fallback of a size the component measures itself");
818
+ return;
819
+ }
773
820
  if (col >= 0 && inFallback(col)) {
774
821
  setAside("a token's own fallback");
775
822
  return;
@@ -800,14 +847,20 @@ function scanCore(file, source, table) {
800
847
  seen.add(key);
801
848
  // The kind is already known here, and without passing it the lookup
802
849
  // answers a `gap` with a radius token.
803
- const match = tokenMatch(table, literal, kind);
850
+ /**
851
+ * O `_` DA CLASSE ARBITRÁRIA É O ESPAÇO DO TAILWIND (aula 01, 15/10). O achado guarda o valor como ESTÁ ESCRITO -
852
+ * `Anton,_sans-serif` -, porque é nessa grafia que o `--fix` acha a coluna para trocar; só a busca do nome lê o `_`
853
+ * como espaço, que é como o token declara a pilha.
854
+ */
855
+ const lookup = kind === "font" ? literal.replace(/_/g, " ") : literal;
856
+ const match = tokenMatch(table, lookup, kind);
804
857
  /**
805
858
  * O NOME DELE VEM DA MESMA CHAVE QUE A NOSSA FAMÍLIA VALIDOU - ver `theirNames`.
806
859
  *
807
860
  * A chave carrega o `kind` porque o valor sozinho não decide: `4px` é `--radius-xs` e
808
861
  * `--spacing` no vocabulário dele, e qual dos dois está certo depende de onde o literal está.
809
862
  */
810
- const aliased = table.aliases.get(`${kind}:${normalizeValue(literal, table.rootPx)}`);
863
+ const aliased = table.aliases.get(`${kind}:${normalizeValue(lookup, table.rootPx)}`);
811
864
  /** O valor igual não basta - o papel também, ver `roleFits` (r13 e r14, 28/09). */
812
865
  const theirs = aliased && roleFits(propertyAt(line, col), aliased.name) ? aliased : undefined;
813
866
  /** A procedência do `token`, lida da tabela que o achou - ver `tokenIsTheirs`. */
@@ -896,8 +949,19 @@ function scanCore(file, source, table) {
896
949
  pushLengths("spacing", m);
897
950
  for (const m of line.matchAll(SPACING_DECL))
898
951
  pushLengths("spacing", m);
952
+ for (const m of line.matchAll(FONT_SIZE_UTIL))
953
+ pushLengths("font", m);
954
+ for (const m of line.matchAll(FONT_SIZE_DECL))
955
+ pushLengths("font", m);
899
956
  for (const m of line.matchAll(FONT)) {
900
- const stack = m[1].trim();
957
+ /**
958
+ * A CLASSE ARBITRÁRIA DO TAILWIND TERMINA NO `]` (pedido da aula 01, 15/10). Em `[font-family:Inter,_sans-serif]
959
+ * font-medium ...` a regra de CSS - que só para em `;`, `}` ou fim de linha - engolia o resto da string de classes, e
960
+ * o `--verbose` mostrava `Inter,_sans-serif] font-medium [white-space:nowrap] ...` como uma família. Dentro do
961
+ * colchete o valor vai até o `]`; o `_` fica como está escrito, e quem o lê como espaço é a busca do nome, em `push`.
962
+ */
963
+ const inBrackets = line[(m.index ?? 0) - 1] === "[";
964
+ const stack = (inBrackets ? (m[1].split("]")[0] ?? "") : m[1]).trim();
901
965
  // A stack already reading from the system is the point, not a problem.
902
966
  if (stack.startsWith("var("))
903
967
  continue;
@@ -338,7 +338,13 @@
338
338
  * novo grava no `.lock` os nomes que acabou de declarar, e o `template` e o `component` passam a escrevê-los - 51 nomes e 3
339
339
  * valores crus no hero, contra 0 e 54 antes. Uma pasta escrita antes traz a landing com os valores à mão.
340
340
  */
341
- export const MATERIALISER_SINCE = "0.16.498";
341
+ /**
342
+ * 0.16.498 -> 0.16.499 em 15/10, e o passo 1 dá **SIM** (o defeito 1 da aula 01): o tamanho de texto pedido pelo PAPEL
343
+ * (`--ds-typography-role-sm-font-size`) passa a sair com o nome do degrau que vale o mesmo, quando o projeto o declara -
344
+ * `[font-size:var(--text-sm)]` em vez de `[font-size:0.875rem]`. Um componente escrito antes traz os tamanhos à mão. No
345
+ * mesmo PR, o `init` declara a largura da página (`--container-page`) e a seção a escreve pelo nome, não como `1440px`.
346
+ */
347
+ export const MATERIALISER_SINCE = "0.16.499";
342
348
  /**
343
349
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
344
350
  *
@@ -551,7 +557,12 @@ export const COUNTED_DIFFERENTLY = "this run counts a value as named only when Y
551
557
  * à que tem o nome dele. Medido no `codelevel`: 33 checagens do componente mais checado chegavam sem componente. Um
552
558
  * hook anterior continua mandando essas checagens sem dono, e elas não contam para o degrau.
553
559
  */
554
- export const CHECKER_SINCE = "0.16.487";
560
+ /**
561
+ * 0.16.487 -> 0.16.499 em 15/10, e o passo 1 dá **SIM** (os defeitos 2 e 3 da aula 01): a checagem passa a acusar o tamanho
562
+ * de texto escrito à mão (`font-size`, `[font-size:X]`, `text-[X]`), e a família de fonte no colchete do Tailwind deixa de
563
+ * engolir o resto da linha. Um hook anterior diz "limpo" sobre um `[font-size:0.875rem]` com `--text-sm` declarado.
564
+ */
565
+ export const CHECKER_SINCE = "0.16.499";
555
566
  /**
556
567
  * A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
557
568
  *
@@ -1027,7 +1038,11 @@ export const CHECKER_SINCE = "0.16.487";
1027
1038
  * mostra quando duas folhas brigam, deixa de fora a folha que ninguém carrega, conta o pages router como rota e diz os
1028
1039
  * componentes em Vue, Svelte, Astro e Angular. Um censo lido antes guardou "names no token yet" onde havia tokens.
1029
1040
  */
1030
- export const READER_SINCE = "0.16.485";
1041
+ /**
1042
+ * 0.16.485 -> 0.16.499 em 15/10, e o passo 1 dá **SIM** (o defeito 3 da aula 01): a varredura lê o tamanho de texto escrito
1043
+ * à mão, e o censo passa a gravá-lo. Um censo lido antes não tem os tamanhos na fila de trabalho dele.
1044
+ */
1045
+ export const READER_SINCE = "0.16.499";
1031
1046
  /**
1032
1047
  * O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
1033
1048
  *
@@ -278,6 +278,15 @@ async function readTheirs(root, slug) {
278
278
  * rodada: medido na prova de 27/09, 82 nomes viraram 38.
279
279
  */
280
280
  /** Os mesmos nomes de `systemNames`, com a variável nossa ao lado - o par que o `.lock` guarda. */
281
+ /**
282
+ * A LARGURA DA PÁGINA - a régua em que as seções do catálogo param o conteúdo (defeito 4 da aula 01, 15/10).
283
+ *
284
+ * Ela não é token do sistema: é a régua do catálogo (`apps/web/src/lib/catalog/page-width.ts`, pedido dele em 14/10), e as
285
+ * seções a escrevem como `var(--ds-layout-page-width, 1440px)`. Aqui ela ganha o nome que o Tailwind v4 lê
286
+ * (`--container-*`), e o par vai para o `.lock`: a seção materializada escreve `var(--container-page)`, e a pessoa muda a
287
+ * largura num lugar só. O valor é o MESMO do catálogo - `page-width-twin.spec.ts` compara os dois.
288
+ */
289
+ export const PAGE_WIDTH = { ours: "--ds-layout-page-width", theirs: "--container-page", value: "1440px" };
281
290
  function systemPairs(tokensCss, lookup = new Map()) {
282
291
  const { names, values } = tongueOf([], tokensCss);
283
292
  const out = [];
@@ -290,6 +299,8 @@ function systemPairs(tokensCss, lookup = new Map()) {
290
299
  if (value && !value.includes("var(--ds-"))
291
300
  out.push({ ours, theirs, value });
292
301
  }
302
+ if (out.length > 0)
303
+ out.push({ ...PAGE_WIDTH });
293
304
  return out;
294
305
  }
295
306
  function systemNames(tokensCss, lookup = new Map()) {
@@ -304,6 +315,8 @@ function systemNames(tokensCss, lookup = new Map()) {
304
315
  if (value && !value.includes("var(--ds-"))
305
316
  out.push([theirs, value]);
306
317
  }
318
+ if (out.length > 0)
319
+ out.push([PAGE_WIDTH.theirs, PAGE_WIDTH.value]);
307
320
  return out;
308
321
  }
309
322
  const same = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
@@ -71,7 +71,8 @@ export function inTheirTongue(css, tongue, destination = "stylesheet") {
71
71
  inlined += 1;
72
72
  if (tongue.frozen.has(name))
73
73
  froze += 1;
74
- const font = destination === "stylesheet" ? viaNextFont(name, value, tongue) : null;
74
+ /** Nas duas saídas - a classe do Tailwind também (defeito 1 da aula 01, 15/10); `write` escapa o espaço. */
75
+ const font = viaNextFont(name, value, tongue);
75
76
  return write(font ?? value);
76
77
  }
77
78
  /**
@@ -155,6 +156,8 @@ artifacts) {
155
156
  */
156
157
  export function tongueOf(map, installedCss) {
157
158
  const names = new Map(map.map((p) => [p.ours, p.theirs]));
159
+ /** O valor que o `.lock` mediu para cada par - a folha reescrita já não o declara (`var(--text-sm)`). */
160
+ const pairValue = new Map(map.flatMap((p) => (p.value ? [[p.ours, p.value]] : [])));
158
161
  const values = new Map();
159
162
  /**
160
163
  * O que cada variável nossa declara na folha, antes de qualquer indireção ser seguida.
@@ -226,6 +229,32 @@ export function tongueOf(map, installedCss) {
226
229
  else
227
230
  values.set(ours, destination.value);
228
231
  }
232
+ /**
233
+ * O PAPEL DE TIPO FALA O NOME DO DEGRAU QUE VALE O MESMO (defeito 1 da aula 01, 15/10).
234
+ *
235
+ * O componente pede o tamanho pelo PAPEL (`--ds-typography-role-sm-font-size`), e o nome dele só existe para o DEGRAU da
236
+ * escala (`--ds-typography-scale-sm-font-size` -> `--text-sm`). O papel saía como `[font-size:0.875rem]` num projeto
237
+ * que declara `--text-sm: 0.875rem`, e o `doctor` acusava na hora um valor com nome esperando.
238
+ *
239
+ * Só quando o MESMO degrau (`role-sm` -> `scale-sm`) JÁ tem nome dele e vale exatamente o mesmo, na mesma propriedade -
240
+ * é a troca que o `--fix` faria depois. O papel nunca vira um degrau que o projeto não declara: sem nome, continua o valor.
241
+ */
242
+ for (const [ours, value] of values) {
243
+ const role = /^--ds-typography-role-(.+)-(font-size|line-height|letter-spacing)$/.exec(ours);
244
+ if (!role)
245
+ continue;
246
+ /** O MESMO degrau, nunca outro que por acaso vale o mesmo: `role-sm` -> `scale-sm` (prova de 15/10). */
247
+ const step = `--ds-typography-scale-${role[1]}-${role[2]}`;
248
+ const theirName = names.get(step);
249
+ if (!theirName)
250
+ continue;
251
+ const written = declared.get(step);
252
+ const stepValue = written?.startsWith("var(") ? pairValue.get(step) : written;
253
+ if (stepValue === undefined || stepValue.trim().toLowerCase() !== value.trim().toLowerCase())
254
+ continue;
255
+ names.set(ours, theirName);
256
+ values.delete(ours);
257
+ }
229
258
  /**
230
259
  * AS QUE MUDAM ENTRE ESQUEMAS E VIRAM LITERAL - o subconjunto que congela.
231
260
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.498",
3
+ "version": "0.16.499",
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": {