synthesisui 0.16.474 → 0.16.476

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.
@@ -385,8 +385,16 @@ measured) {
385
385
  * unidade. Uma raiz por tabela seria a forma mais silenciosa de discordar.
386
386
  */
387
387
  const theirs = await harvestOwnTokens([root], measured);
388
+ const table = withTheirNames(ours, theirs);
389
+ /**
390
+ * AS LARGURAS DO SISTEMA - ver `breakpoints` em `TokenTable`. `var()` não funciona em `@media`, então o `tokens.css`
391
+ * não as carrega e a escala sai do documento (bateria, 28/09).
392
+ */
393
+ const breakpoints = breakpointsOf(documents, table.rootPx);
394
+ if (breakpoints.size > 0)
395
+ table.breakpoints = breakpoints;
388
396
  return {
389
- table: withTheirNames(ours, theirs),
397
+ table,
390
398
  theirs,
391
399
  recipes,
392
400
  documents,
@@ -1977,3 +1985,17 @@ export async function doctor(opts) {
1977
1985
  process.exitCode = 1;
1978
1986
  }
1979
1987
  }
1988
+ /** `foundations.breakpoints` de cada documento instalado, em px - `{ md: "768px" }` -> `md` -> 768. */
1989
+ export function breakpointsOf(documents, rootPx) {
1990
+ const out = new Map();
1991
+ for (const doc of documents) {
1992
+ const bps = doc?.foundations?.breakpoints;
1993
+ for (const [name, raw] of Object.entries(bps ?? {})) {
1994
+ const m = /^\s*([\d.]+)(px|rem|em)\s*$/.exec(String(raw));
1995
+ if (!m || out.has(name))
1996
+ continue;
1997
+ out.set(name, m[2] === "px" ? Number(m[1]) : Number(m[1]) * rootPx);
1998
+ }
1999
+ }
2000
+ return out;
2001
+ }
@@ -5,7 +5,7 @@ import { changedSince } from "../changed-files.js";
5
5
  import { emptyTally, exportedNames, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
6
6
  import { checkContracts } from "../doctor/contract-check.js";
7
7
  import { appendEvent, ledgerPath } from "../doctor/ledger.js";
8
- import { diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
8
+ import { describeOffScale, diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
9
9
  import { declarados, falaDaPrateleira, naPrateleira } from "../doctor/shelf.js";
10
10
  import { governs, ungovernedIn } from "../governed.js";
11
11
  import { readMode } from "../mode.js";
@@ -307,6 +307,8 @@ async function report(root, filePath, mode) {
307
307
  */
308
308
  const unnamed = d.findings.filter((f) => !nameToWrite(f));
309
309
  const phantoms = d.files.flatMap((f) => f.phantoms ?? []);
310
+ /** Fora da escala - ver `OffScale` (bateria, 28/09). Entra nas mesmas portas que os `phantoms`. */
311
+ const offScale = describeOffScale(d.files);
310
312
  // Every check leaves one line in the ledger - INCLUDING the clean ones,
311
313
  // because a fix is itself a write, so the clean re-check of a file that was
312
314
  // just flagged is the evidence the agent read the report and acted. Without
@@ -326,6 +328,7 @@ async function report(root, filePath, mode) {
326
328
  broke.length === 0 &&
327
329
  named.length === 0 &&
328
330
  phantoms.length === 0 &&
331
+ offScale.length === 0 &&
329
332
  unnamed.length === 0) {
330
333
  const hello = await greet(root, rel);
331
334
  return hello ? say(hello) : null;
@@ -362,7 +365,8 @@ async function report(root, filePath, mode) {
362
365
  if (composed.length === 0 &&
363
366
  broke.length > 0 &&
364
367
  named.length === 0 &&
365
- phantoms.length === 0)
368
+ phantoms.length === 0 &&
369
+ offScale.length === 0)
366
370
  return say(broke.join("\n"));
367
371
  const sample = unnamed
368
372
  .slice(0, 3)
@@ -371,7 +375,8 @@ async function report(root, filePath, mode) {
371
375
  if (composed.length === 0 &&
372
376
  broke.length === 0 &&
373
377
  named.length === 0 &&
374
- phantoms.length === 0)
378
+ phantoms.length === 0 &&
379
+ offScale.length === 0)
375
380
  return say([
376
381
  `${rel} - ${unnamed.length} value${unnamed.length === 1 ? "" : "s"} here ${unnamed.length === 1 ? "has" : "have"} no name in this system, and nothing to replace ${unnamed.length === 1 ? "it" : "them"} with: ${sample}${unnamed.length > 3 ? `, +${unnamed.length - 3} more` : ""}.`,
377
382
  "Do not invent a name - leave them, or ask the person what they would call it.",
@@ -419,6 +424,7 @@ async function report(root, filePath, mode) {
419
424
  if (phantoms.length > 0) {
420
425
  lines.push("", "Names this system does not declare. These look tokenized and apply nothing at all:", ...phantoms.slice(0, 20).map((p) => ` line ${p.line} ${p.name}`), "", "Use a name the system has, or say which value you need and what you would call it. Do NOT invent a token.");
421
426
  }
427
+ lines.push(...offScale);
422
428
  return say(lines.join("\n"));
423
429
  }
424
430
  /**
@@ -10,7 +10,7 @@ import { declareForm } from "../doctor/declared-forms.js";
10
10
  import { describeTriage, triageLedger } from "../doctor/gap-triage.js";
11
11
  import { readEvents } from "../doctor/ledger.js";
12
12
  import { fileRequest } from "../doctor/requests.js";
13
- import { diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
13
+ import { describeOffScale, diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
14
14
  import { nearestToken, normalizeValue, tokenFor } from "../doctor/tokens.js";
15
15
  import { ruleLine } from "../doctrine.js";
16
16
  import { fromCensus } from "../memory/observation.js";
@@ -437,7 +437,10 @@ async function checkFile(root, path) {
437
437
  if (phantoms.length > 0) {
438
438
  out.push("", "Names this system does not declare. These look tokenized and apply nothing at all:", ...phantoms);
439
439
  }
440
- if (d.findings.length === 0 && phantoms.length === 0)
440
+ /** Fora da escala - ver `OffScale` (bateria, 28/09). */
441
+ const offScale = describeOffScale(d.files);
442
+ out.push(...offScale);
443
+ if (d.findings.length === 0 && phantoms.length === 0 && offScale.length === 0)
441
444
  out.push("", "Nothing to fix here.");
442
445
  return fromContract(out.join("\n"));
443
446
  }
@@ -1,4 +1,5 @@
1
1
  import { emittableAttrs } from "./attr-shape.js";
2
+ import { FAMILY_SEAM_PREFIX } from "./fonts.js";
2
3
  import { inTheirTongue, sumSpoken, } from "./their-tongue.js";
3
4
  import { PHRASING_FORMS, } from "./types.js";
4
5
  /**
@@ -550,6 +551,12 @@ const STATIC = {
550
551
  /** Fixed rem → Tailwind numeric scale step (0.25rem base), or null if not a
551
552
  * clean multiple. So `0.5rem` → `2` (h-2/w-2) instead of an arbitrary value. */
552
553
  function remToTwScale(value) {
554
+ /** E em px, no degrau de 4px do Tailwind - bateria, 28/09 (b11): o avatar saía `[width:40px]` em vez de `w-10`. */
555
+ const px = value.trim().match(/^(\d*\.?\d+)px$/);
556
+ if (px) {
557
+ const n = Number.parseFloat(px[1]) / 4;
558
+ return Number.isInteger(n) && n > 0 && n <= 96 ? String(n) : null;
559
+ }
553
560
  const m = value.trim().match(/^(\d*\.?\d+)rem$/);
554
561
  if (!m)
555
562
  return null;
@@ -596,14 +603,21 @@ const minted = (themeVars, cssVar) => themeVars === null || themeVars.has(cssVar
596
603
  export class ThemeVocab extends Set {
597
604
  theirValues;
598
605
  dsValues;
606
+ varies;
599
607
  constructor(
600
608
  /** O valor LITERAL de cada variável dele, com as indireções dele já seguidas. */
601
609
  theirValues,
602
610
  /** O valor LITERAL de cada variável nossa (`--ds-*`), com as indireções seguidas. */
603
- dsValues) {
611
+ dsValues,
612
+ /**
613
+ * AS NOSSAS QUE MUDAM DE VALOR ENTRE OS TEMAS - `dsValues` guarda um tema só. Um literal nunca vira uma destas: o
614
+ * valor cru pinta igual nos dois temas, e o papel não (b11, 28/09). Ver `literalAsRef`.
615
+ */
616
+ varies = new Set()) {
604
617
  super(theirValues.keys());
605
618
  this.theirValues = theirValues;
606
619
  this.dsValues = dsValues;
620
+ this.varies = varies;
607
621
  }
608
622
  /**
609
623
  * O nome DELE para uma referência nossa no namespace `ns` (`color`), ou `null`.
@@ -690,12 +704,37 @@ const namedInside = (prop, value, themeVars) => {
690
704
  return ok ? `var(--ease-${key})` : ref;
691
705
  });
692
706
  };
707
+ /**
708
+ * O VALOR CRU DA RECEITA QUE O SISTEMA JÁ NOMEIA - bateria, 28/09 (b11). A receita da galeria traz `color: #ffffff` e
709
+ * `fontFamily: "Inter, sans-serif"` como literais, e o gerador escrevia `[color:#ffffff]` - que a nossa própria checagem
710
+ * reprova. Quando exatamente UM papel semântico do sistema (ou uma família) segura aquele valor, o literal vira a
711
+ * referência, e o caminho de sempre acha o nome dele. Dois papéis com o mesmo valor: nada é adivinhado.
712
+ */
713
+ function literalAsRef(prop, value, themeVars) {
714
+ if (!(themeVars instanceof ThemeVocab) || /[{(]/.test(value))
715
+ return null;
716
+ const ns = prop === "fontFamily"
717
+ ? FAMILY_SEAM_PREFIX
718
+ : prop === "color" || prop === "backgroundColor" || prop === "borderColor"
719
+ ? "--ds-color-semantic-"
720
+ : null;
721
+ if (!ns)
722
+ return null;
723
+ const hits = [...themeVars.dsValues]
724
+ .filter(([n, v]) => n.startsWith(ns) && !themeVars.varies.has(n) && sameValue(v, value))
725
+ .map(([n]) => n.slice(ns.length));
726
+ if (hits.length !== 1)
727
+ return null;
728
+ return ns === FAMILY_SEAM_PREFIX ? `{typography.families.${hits[0]}}` : `{color.semantic.${hits[0]}}`;
729
+ }
693
730
  /** One declaration → Tailwind classes (pretty when mappable, arbitrary-property
694
731
  * otherwise - never dropped). */
695
732
  function declToTailwind(prop, value, themeVars) {
696
733
  const stat = STATIC[prop]?.[value];
697
734
  if (stat)
698
735
  return [stat];
736
+ /** O valor cru que UM papel do sistema segura vira a referência dele - ver `literalAsRef`. */
737
+ value = literalAsRef(prop, value, themeVars) ?? value;
699
738
  switch (prop) {
700
739
  case "backgroundColor": {
701
740
  if (value === "transparent")
@@ -534,6 +534,7 @@ const DECLARES_ALL = /(?:^|[\s{;])(--[a-z0-9-]+)\s*:\s*([^;{}]+);/gi;
534
534
  function scanCore(file, source, table) {
535
535
  const findings = [];
536
536
  const phantoms = [];
537
+ const offScale = [];
537
538
  const ownDeclared = [];
538
539
  /**
539
540
  * OS NOMES SASS DELE, VISTOS PELA LENTE - rodada 10 do refinamento, 27/09. Num `.scss` lido por `scss-lens.ts`,
@@ -653,6 +654,8 @@ function scanCore(file, source, table) {
653
654
  }
654
655
  for (const name of findPhantoms(line, table))
655
656
  phantoms.push({ name, line: at });
657
+ for (const o of offScaleOf(file, line, table, setAside))
658
+ offScale.push({ ...o, line: at });
656
659
  // Depth at the START of this line, carried before the early return so a
657
660
  // blank line inside an <svg> cannot close the region by accident. The
658
661
  // per-match depth is recomputed below, because an icon is often written on
@@ -904,6 +907,7 @@ function scanCore(file, source, table) {
904
907
  findings,
905
908
  tokenUses,
906
909
  ...(phantoms.length > 0 ? { phantoms } : null),
910
+ ...(offScale.length > 0 ? { offScale } : null),
907
911
  ...(ownDeclared.length > 0 ? { ownDeclared } : null),
908
912
  ...(ownCandidates.size > 0
909
913
  ? {
@@ -1066,3 +1070,50 @@ context = []) {
1066
1070
  repeats,
1067
1071
  };
1068
1072
  }
1073
+ const MEDIA_WIDTH = /\b(?:(?:min|max)-)?width\s*(?::|[<>]=?)\s*([\d.]+)(px|rem|em)\b/g;
1074
+ const TOKEN_ARITHMETIC = /(?:\$\{\s*|:\s*)([A-Za-z_$][\w$]*(?:\.[\w$]+)*\.(?:[\w$]*(?:space|spacing|gap|radius|size|font)[\w$]*)(?:\.[\w$]+)*)\s*([+\-*/])\s*(\d+(?:\.\d+)?)\b/gi;
1075
+ const BORDER_WIDTH = /\bborder(?:-(?:top|right|bottom|left))?(?:-width)?\s*:\s*(\d+(?:\.\d+)?px)\b/g;
1076
+ const BORDER_NAME = /border|line|hairline|stroke|rule|divider|thin/i;
1077
+ /**
1078
+ * FORA DA ESCALA, linha a linha - ver `OffScale`. `setAside` recebe o que foi medido e está certo: o ponto de quebra
1079
+ * na escala é dito pelo nome ("breakpoint md (768px), on your system's scale"), para o silêncio ter motivo.
1080
+ */
1081
+ function offScaleOf(file, line, table, setAside) {
1082
+ const out = [];
1083
+ const scale = table.breakpoints;
1084
+ if (scale && scale.size > 0 && /^@media\b/.test(line)) {
1085
+ for (const m of line.matchAll(MEDIA_WIDTH)) {
1086
+ const n = Number(m[1]);
1087
+ const px = m[2] === "px" ? n : n * table.rootPx;
1088
+ let best = null;
1089
+ for (const [name, bp] of scale)
1090
+ if (!best || Math.abs(bp - px) < Math.abs(best[1] - px))
1091
+ best = [name, bp];
1092
+ if (!best)
1093
+ continue;
1094
+ if (Math.abs(best[1] - px) < 0.5)
1095
+ setAside(`breakpoint ${best[0]} (${best[1]}px), on your system's scale`);
1096
+ else
1097
+ out.push({ literal: `${m[1]}${m[2]}`, why: "breakpoint", nearest: `${best[0]} ${best[1]}px` });
1098
+ }
1099
+ }
1100
+ if (/\.(t|j)sx?$/.test(file))
1101
+ for (const m of line.matchAll(TOKEN_ARITHMETIC))
1102
+ out.push({ literal: `${m[1]} ${m[2]} ${m[3]}`, why: "arithmetic" });
1103
+ for (const m of line.matchAll(BORDER_WIDTH)) {
1104
+ const v = normalizeValue(m[1], table.rootPx);
1105
+ const theirs = [table.aliases.get(`spacing:${v}`), table.aliases.get(`radius:${v}`)].find((a) => a && BORDER_NAME.test(a.name));
1106
+ if (theirs)
1107
+ out.push({ literal: m[1], why: "named", nearest: `var(${theirs.name})` });
1108
+ }
1109
+ return out;
1110
+ }
1111
+ /** As linhas que o `check_file` e o fim do turno dizem sobre `OffScale` - uma frase por caso, a mesma nos dois. */
1112
+ export function describeOffScale(files) {
1113
+ const rows = files.flatMap((f) => (f.offScale ?? []).map((o) => o.why === "breakpoint"
1114
+ ? ` ${f.file}:${o.line} ${o.literal} - a breakpoint off your system's widths; the nearest is ${o.nearest} (a media query cannot use a token, so match the width)`
1115
+ : o.why === "arithmetic"
1116
+ ? ` ${f.file}:${o.line} ${o.literal} - arithmetic on a token lands between the steps; use a step the system has, or ask for one`
1117
+ : ` ${f.file}:${o.line} ${o.literal} - this repo calls it ${o.nearest}`));
1118
+ return rows.length > 0 ? ["", "Off this system's scale:", ...rows] : [];
1119
+ }
@@ -304,7 +304,13 @@
304
304
  * 0.16.472 -> 0.16.473 em 28/09, e o passo 1 dá **SIM** (a bateria): as famílias de sistema (Georgia, Arial, Verdana)
305
305
  * deixam de virar `<link>` do Google Fonts, e a webfont ao lado delas ganha o `fonts.ts` que antes não saía.
306
306
  */
307
- export const MATERIALISER_SINCE = "0.16.473";
307
+ /**
308
+ * 0.16.473 -> 0.16.476 em 28/09, e o passo 1 dá **SIM** (a bateria, b11): o componente que o gerador escreve com Tailwind
309
+ * passa a usar o nome quando a receita traz um valor cru que um papel do sistema segura (`text-foreground` em vez de
310
+ * `[color:#ffffff]`, `font-body` em vez de `[font-family:Inter,_sans-serif]`), e a largura em px no degrau vira `w-10`.
311
+ * Um componente escrito antes reprova na própria checagem.
312
+ */
313
+ export const MATERIALISER_SINCE = "0.16.476";
308
314
  /**
309
315
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
310
316
  *
@@ -490,7 +496,12 @@ export const COUNTED_DIFFERENTLY = "this run counts a value as named only when Y
490
496
  * passa a medir as regras da folha global que ele veste pelo `className`. Um hook anterior diz "no colour or spacing of
491
497
  * its own to measure" sobre o componente de um projeto que estiliza por classe global.
492
498
  */
493
- export const CHECKER_SINCE = "0.16.474";
499
+ /**
500
+ * 0.16.474 -> 0.16.475 em 28/09, e o passo 1 dá **SIM** (a bateria, e a régua dele de 17:39-17:41): o fim do turno diz o
501
+ * que sai da escala - o ponto de quebra fora das larguras do sistema, a conta em cima de um token, e o `1px` de borda
502
+ * num projeto que tem nome para ele. Um hook anterior aprova os três.
503
+ */
504
+ export const CHECKER_SINCE = "0.16.475";
494
505
  /**
495
506
  * A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
496
507
  *
@@ -392,7 +392,15 @@ export async function themeVocabOf(root, slug, names) {
392
392
  dsValues.set(n, value);
393
393
  }
394
394
  /** Um nome sem valor conhecido continua no conjunto: `nameFor` o trata como antes. */
395
- const vocab = new ThemeVocab(theirs, dsValues);
395
+ /** As nossas que o `tokens.css` declara com mais de um valor - uma por tema. Ver `varies` em `ThemeVocab`. */
396
+ const faces = new Map();
397
+ for (const m of installed.matchAll(/(--ds-[a-zA-Z0-9_-]+)\s*:\s*([^;{}]+);/g)) {
398
+ const seen = faces.get(m[1]) ?? new Set();
399
+ seen.add(m[2].trim());
400
+ faces.set(m[1], seen);
401
+ }
402
+ const varies = new Set([...faces].filter(([, v]) => v.size > 1).map(([n]) => n));
403
+ const vocab = new ThemeVocab(theirs, dsValues, varies);
396
404
  for (const n of names)
397
405
  vocab.add(n);
398
406
  return vocab;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.474",
3
+ "version": "0.16.476",
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": {