synthesisui 0.16.457 → 0.16.459

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.
@@ -58,6 +58,14 @@ export function readVocabulary(themeCss) {
58
58
  fontWeights: weights,
59
59
  };
60
60
  }
61
+ /**
62
+ * The type scale every Tailwind v4 project has, whether the system declares it or not. Oscilla's
63
+ * theme.css declares only `--text-display`, so `cn("text-xs", "text-2xl")` kept both and the
64
+ * stylesheet order decided (refinement round 1, 27/09).
65
+ */
66
+ const TAILWIND_TEXT_SIZES = [
67
+ "xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl", "9xl",
68
+ ];
61
69
  const set = (names) => `new Set([${names.map((n) => JSON.stringify(n)).join(", ")}])`;
62
70
  /** The `cn.ts` a project gets, written against ITS system's vocabulary. */
63
71
  export function emitCn(slug, vocab) {
@@ -84,7 +92,7 @@ export function emitCn(slug, vocab) {
84
92
 
85
93
  /** This system's own namespaces, so \`text-*\` and \`font-*\` are never a guess. */
86
94
  const COLORS = ${set(vocab.colors)};
87
- const TEXT_SIZES = ${set(vocab.textSizes)};
95
+ const TEXT_SIZES = ${set([...new Set([...TAILWIND_TEXT_SIZES, ...vocab.textSizes])])};
88
96
  const FONT_FAMILIES = ${set(vocab.fontFamilies)};
89
97
  const FONT_WEIGHTS = ${set(vocab.fontWeights)};
90
98
 
@@ -97,8 +105,6 @@ const PREFIXES = [
97
105
  "rounded-tl", "rounded-tr", "rounded-br", "rounded-bl",
98
106
  "rounded-t", "rounded-r", "rounded-b", "rounded-l",
99
107
  "rounded",
100
- "border-t", "border-r", "border-b", "border-l", "border-x", "border-y",
101
- "border",
102
108
  "px", "py", "pt", "pr", "pb", "pl", "ps", "pe", "p",
103
109
  "mx", "my", "mt", "mr", "mb", "ml", "ms", "me", "m",
104
110
  "gap-x", "gap-y", "gap",
@@ -124,6 +130,36 @@ const DISPLAYS = new Set([
124
130
  const FLEX_DIRECTIONS = new Set(["row", "col", "row-reverse", "col-reverse"]);
125
131
  const FLEX_WRAPS = new Set(["wrap", "nowrap", "wrap-reverse"]);
126
132
 
133
+ /**
134
+ * \`border\` heads THREE properties: the width (\`border\`, \`border-2\`), the style
135
+ * (\`border-dashed\`) and the colour (\`border-surface\`). Read as one prefix,
136
+ * \`border border-surface\` kept only the colour and the line vanished - every
137
+ * card, input and badge in the project, in silence (refinement round 1, 27/09).
138
+ */
139
+ const BORDER_SIDES = new Set(["t", "r", "b", "l", "x", "y", "s", "e"]);
140
+ const BORDER_STYLES = new Set(["solid", "dashed", "dotted", "double", "hidden", "none"]);
141
+ const COLOR_KEYWORDS = new Set(["transparent", "current", "inherit", "black", "white"]);
142
+
143
+ function borderGroup(variants: string, rest: string, cls: string): string {
144
+ const first = rest.split("-")[0];
145
+ const side = BORDER_SIDES.has(first) ? \`-\${first}\` : "";
146
+ const tail = side ? rest.slice(first.length + 1) : rest;
147
+ const isWidth =
148
+ tail === "" ||
149
+ [...tail].every((ch) => ch >= "0" && ch <= "9") ||
150
+ (tail.startsWith("[") && !isArbitraryColor(tail));
151
+ if (isWidth) return \`\${variants}border\${side}-width\`;
152
+ if (BORDER_STYLES.has(tail)) return \`\${variants}border-style\`;
153
+ if (COLORS.has(tail) || COLOR_KEYWORDS.has(tail) || isArbitraryColor(tail)) {
154
+ return \`\${variants}border\${side}-color\`;
155
+ }
156
+ return \`\${variants}\${cls}\`;
157
+ }
158
+
159
+ function isArbitraryColor(v: string): boolean {
160
+ return /^\\[(color:|#|rgba?\\(|hsla?\\(|oklch\\(|oklab\\(|var\\(--color)/.test(v);
161
+ }
162
+
127
163
  /**
128
164
  * The property this class sets, or the class itself when we cannot tell.
129
165
  *
@@ -169,6 +205,7 @@ function groupOf(cls: string): string {
169
205
  if (FONT_FAMILIES.has(rest)) return \`\${variants}font-family\`;
170
206
  return \`\${variants}\${cls}\`;
171
207
  }
208
+ if (head === "border") return borderGroup(variants, rest, cls);
172
209
 
173
210
  for (const p of PREFIXES) {
174
211
  if (base === p || base.startsWith(\`\${p}-\`)) return \`\${variants}\${p}\`;
@@ -7,7 +7,7 @@ import { readProjectConfig, resolveRegistry } from "../config.js";
7
7
  import { detectAppDirs } from "../global-sheet.js";
8
8
  import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-templates.js";
9
9
  import { body, section, snippet } from "../output.js";
10
- import { findCollision, reactMajorOf, readInstalledConvention, readInstalledScheme, theirThemeVars, } from "../project-facts.js";
10
+ import { findCollision, reactMajorOf, readInstalledConvention, readInstalledScheme, theirThemeVars, namedUtilitiesIn, themeVocabOf, } from "../project-facts.js";
11
11
  import { fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
12
12
  import { flavourResolver } from "../styles-flavour.js";
13
13
  import { inTheirTongue, projectTongue, sumSpoken, tongueFromArtifacts, } from "../their-tongue.js";
@@ -356,6 +356,7 @@ export async function component(slug, name, opts) {
356
356
  filenames = written.map((f) => f.filename);
357
357
  }
358
358
  else {
359
+ const vocab = await themeVocabOf(root, slug, await theirThemeVars(root, await detectAppDirs(root, config.pagesDir)));
359
360
  const { files, spoken: said, root: generatedRoot, } = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavour, await reactMajorOf(root),
360
361
  /**
361
362
  * THE SPELLING, FROM THE VERSION WE JUST FETCHED.
@@ -385,8 +386,18 @@ export async function component(slug, name, opts) {
385
386
  * propriedade renderiza NADA - pior que o valor arbitrário, porque nada falta na tela. É a
386
387
  * mesma lição de `INV-VOLTA-12`, que saiu com a instrução de import e não com a topologia.
387
388
  */
388
- await theirThemeVars(root, await detectAppDirs(root, config.pagesDir)));
389
- spoken = said;
389
+ vocab);
390
+ /**
391
+ * O UTILITÁRIO NOMEADO TAMBÉM É O NOME DELE - rodada 1 do refinamento, 27/09. A conta somava só
392
+ * as `var()` que a porta traduziu, e o leigo leu "0 references now speak the name YOUR code gives
393
+ * the value" sobre um botão com `bg-primary`, `text-muted` e `p-md`: "se eu acreditasse na
394
+ * mensagem, ia achar que não tinha funcionado".
395
+ */
396
+ const utilities = vocab
397
+ ? files.reduce((n, f) => n + namedUtilitiesIn(f.code, vocab), 0)
398
+ : 0;
399
+ spoken =
400
+ said && utilities > 0 ? { ...said, named: said.named + utilities } : said;
390
401
  root_ = generatedRoot;
391
402
  /**
392
403
  * AS DECLARAÇÕES DO ARQUIVO DELE QUE O INTERPRETADOR NÃO LEU - ver `unread-for-component.ts`.
@@ -409,7 +409,15 @@ async function checkFile(root, path) {
409
409
  for (const f of d.findings.slice(0, 40)) {
410
410
  out.push(nameToWrite(f)
411
411
  ? ` ${f.file}:${f.line} ${f.literal} - this repo calls it ${nameToWrite(f)}`
412
- : ` ${f.file}:${f.line} ${f.literal} - no token holds this value`);
412
+ : /**
413
+ * O SISTEMA SEGURA, O PROJETO AINDA NÃO NOMEIA - rodada 1 do refinamento, 27/09. Dizer "no
414
+ * token holds this value" sobre o `160ms` que o sistema declara fez o leigo concluir que o
415
+ * sistema discordava dele mesmo. O nome continua fora da frase: a grafia `--ds-*` é nossa
416
+ * (`INV-GERAL-13`), e batizar o valor no código dele é decisão dele.
417
+ */
418
+ f.token
419
+ ? ` ${f.file}:${f.line} ${f.literal} - the system holds this value, and your code has no name for it yet`
420
+ : ` ${f.file}:${f.line} ${f.literal} - no token holds this value`);
413
421
  }
414
422
  out.push("", "Replace the ones that have a token. For a value with none, do NOT invent a token: say which value it is and what you would call it, and let a person decide.");
415
423
  }
@@ -10,7 +10,7 @@ import { diffLocalDocuments, localChangelogMarkdown, } from "../document-diff.js
10
10
  import { detectAppDirs } from "../global-sheet.js";
11
11
  import { installedBehind, MATERIALISER_SINCE } from "../install-marks.js";
12
12
  import { body, section, snippet } from "../output.js";
13
- import { reactMajorOf, readInstalledConvention, readInstalledScheme, theirThemeVars, } from "../project-facts.js";
13
+ import { reactMajorOf, readInstalledConvention, readInstalledScheme, theirThemeVars, themeVocabOf, } from "../project-facts.js";
14
14
  import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
15
15
  import { flavourResolver } from "../styles-flavour.js";
16
16
  import { projectTongue } from "../their-tongue.js";
@@ -422,7 +422,8 @@ export async function upgrade(asked, opts) {
422
422
  const tongue = await projectTongue(root, slug);
423
423
  /** O `@theme` DELE é o MESMO para a corrida inteira - ver `theirThemeVars`. */
424
424
  /** As pastas de app - a resposta é a INTERSEÇÃO delas. Ver `theirThemeVars`. */
425
- const themeVars = await theirThemeVars(root, await detectAppDirs(root, config.pagesDir));
425
+ /** Com os valores - o mesmo vocabulário do `component`, para os dois reescreverem igual (27/09). */
426
+ const themeVars = await themeVocabOf(root, slug, await theirThemeVars(root, await detectAppDirs(root, config.pagesDir)));
426
427
  for (const entry of entries) {
427
428
  const tsxPath = join(componentsRoot, entry, `${entry}.tsx`);
428
429
  let head = "";
@@ -563,6 +563,61 @@ function remToTwScale(value) {
563
563
  * legível não custa nada.
564
564
  */
565
565
  const minted = (themeVars, cssVar) => themeVars === null || themeVars.has(cssVar);
566
+ /**
567
+ * O VOCABULÁRIO DELE COM OS VALORES - rodada 1 do refinamento, 27/09.
568
+ *
569
+ * O `Set` de nomes respondia só "ele declara `--color-x`?". Dois defeitos saíam daí, medidos no Oscilla
570
+ * v9 num Next novo: o `hover` do cartão pede um DEGRAU (`{color.phosphor.500}`) que o projeto chama de
571
+ * `--color-faint` pelo mesmo `#5d675f`, e o gerador não achava; e o `foreground` do `create-next-app`
572
+ * (`#171717`) casava pelo NOME com o `foreground` claro do sistema, e o botão saía com texto quase preto
573
+ * sobre o fundo escuro.
574
+ *
575
+ * Continua sendo um `Set`, então todo lugar que só pergunta o nome segue igual. Quem sabe pedir mais
576
+ * pergunta `nameFor`.
577
+ */
578
+ export class ThemeVocab extends Set {
579
+ theirValues;
580
+ dsValues;
581
+ constructor(
582
+ /** O valor LITERAL de cada variável dele, com as indireções dele já seguidas. */
583
+ theirValues,
584
+ /** O valor LITERAL de cada variável nossa (`--ds-*`), com as indireções seguidas. */
585
+ dsValues) {
586
+ super(theirValues.keys());
587
+ this.theirValues = theirValues;
588
+ this.dsValues = dsValues;
589
+ }
590
+ /**
591
+ * O nome DELE para uma referência nossa no namespace `ns` (`color`), ou `null`.
592
+ *
593
+ * O nome que casa vale se o valor também casa - ou se um dos dois lados não tem valor conhecido, que
594
+ * é o comportamento de antes. Sem nome que case, o VALOR acha o nome, mas só quando exatamente uma
595
+ * variável dele o segura: duas seria adivinhar qual das duas ele quis dizer.
596
+ */
597
+ nameFor(ref, ns, byName) {
598
+ const path = /^\{([^}]+)\}$/.exec(ref.trim())?.[1];
599
+ const want = path ? this.dsValues.get(`--ds-${path.replace(/\./g, "-")}`) : undefined;
600
+ if (byName && this.has(`--${ns}-${byName}`)) {
601
+ const theirs = this.theirValues.get(`--${ns}-${byName}`);
602
+ if (!want || !theirs || sameValue(want, theirs))
603
+ return byName;
604
+ }
605
+ if (!want)
606
+ return null;
607
+ const hits = [...this.theirValues]
608
+ .filter(([n, v]) => n.startsWith(`--${ns}-`) && sameValue(v, want))
609
+ .map(([n]) => n.slice(ns.length + 3));
610
+ return hits.length === 1 ? hits[0] : null;
611
+ }
612
+ }
613
+ const sameValue = (a, b) => a.trim().toLowerCase().replace(/\s+/g, " ") === b.trim().toLowerCase().replace(/\s+/g, " ");
614
+ /** A chave de cor que ele escreve: pelo `ThemeVocab` quando há valores, pelo nome quando não. */
615
+ const theirColorKey = (value, themeVars) => {
616
+ const key = colorKey(value);
617
+ if (themeVars instanceof ThemeVocab)
618
+ return themeVars.nameFor(value, "color", key);
619
+ return key && minted(themeVars, `--color-${key}`) ? key : null;
620
+ };
566
621
  /** One declaration → Tailwind classes (pretty when mappable, arbitrary-property
567
622
  * otherwise - never dropped). */
568
623
  function declToTailwind(prop, value, themeVars) {
@@ -573,20 +628,20 @@ function declToTailwind(prop, value, themeVars) {
573
628
  case "backgroundColor": {
574
629
  if (value === "transparent")
575
630
  return ["bg-transparent"];
576
- const key = colorKey(value);
577
- if (key && minted(themeVars, `--color-${key}`))
631
+ const key = theirColorKey(value, themeVars);
632
+ if (key)
578
633
  return [`bg-${key}`];
579
634
  break;
580
635
  }
581
636
  case "color": {
582
- const key = colorKey(value);
583
- if (key && minted(themeVars, `--color-${key}`))
637
+ const key = theirColorKey(value, themeVars);
638
+ if (key)
584
639
  return [`text-${key}`];
585
640
  break;
586
641
  }
587
642
  case "borderColor": {
588
- const key = colorKey(value);
589
- if (key && minted(themeVars, `--color-${key}`))
643
+ const key = theirColorKey(value, themeVars);
644
+ if (key)
590
645
  return [`border-${key}`];
591
646
  break;
592
647
  }
@@ -594,12 +649,26 @@ function declToTailwind(prop, value, themeVars) {
594
649
  // "1px solid {color.semantic.x}" → border + border-<x>
595
650
  const m = value.match(/^1px\s+solid\s+(\{[^}]+\})$/);
596
651
  if (m) {
597
- const key = colorKey(m[1]);
598
- if (key && minted(themeVars, `--color-${key}`))
652
+ const key = theirColorKey(m[1], themeVars);
653
+ if (key)
599
654
  return ["border", `border-${key}`];
600
655
  }
601
656
  break;
602
657
  }
658
+ case "outline": {
659
+ /**
660
+ * "2px solid {color.semantic.accent}" → outline-2 + outline-<x>. Não tinha tradução: o foco de
661
+ * todo botão saía `[outline:2px_solid_#4cd694]`, com o nome dele declarado (rodada 1, 27/09).
662
+ * No Tailwind v4, `outline-<n>` já põe o estilo sólido.
663
+ */
664
+ const m = value.match(/^(\d+)px\s+solid\s+(\{[^}]+\})$/);
665
+ if (m) {
666
+ const key = theirColorKey(m[2], themeVars);
667
+ if (key)
668
+ return [m[1] === "1" ? "outline" : `outline-${m[1]}`, `outline-${key}`];
669
+ }
670
+ break;
671
+ }
603
672
  case "borderRadius": {
604
673
  const key = nsKey(value, "radius");
605
674
  if (key && minted(themeVars, `--radius-${key}`))
@@ -515,6 +515,15 @@ function scanCore(file, source, table) {
515
515
  };
516
516
  }
517
517
  let svgDepth = 0;
518
+ /**
519
+ * DENTRO DE `@theme` OU `:root`, UMA DECLARAÇÃO É O VOCABULÁRIO, NÃO UM USO - rodada 1 do refinamento,
520
+ * 27/09. O leigo copiou os valores do sistema para o `@theme` do projeto novo, como o GUIDE ensina, e o
521
+ * `check_file` respondeu "17% (...) 19 written by hand", linha por linha, com o conserto circular
522
+ * "#0a100d - this repo calls it --color-canvas" na própria linha `--color-canvas: #0a100d;`.
523
+ * Profundidade de chaves, e não uma flag, porque `@theme` pode abrir bloco dentro de bloco.
524
+ */
525
+ let tokenBlockDepth = 0;
526
+ let braceDepth = 0;
518
527
  source.split("\n").forEach((raw, i) => {
519
528
  const line = raw.trim();
520
529
  /**
@@ -528,6 +537,14 @@ function scanCore(file, source, table) {
528
537
  */
529
538
  const indent = line ? raw.indexOf(line) : 0;
530
539
  const at = i + 1;
540
+ const depthBefore = braceDepth;
541
+ const opensTokenBlock = /^(@theme\b|:root\b)/.test(line) && line.includes("{");
542
+ if (opensTokenBlock && tokenBlockDepth === 0)
543
+ tokenBlockDepth = depthBefore + 1;
544
+ const inTokenBlock = tokenBlockDepth > 0 && (opensTokenBlock || depthBefore >= tokenBlockDepth);
545
+ braceDepth = Math.max(0, braceDepth + (line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length);
546
+ if (tokenBlockDepth > 0 && braceDepth < tokenBlockDepth)
547
+ tokenBlockDepth = 0;
531
548
  tokenUses += countTokenUses(line, table);
532
549
  /**
533
550
  * O QUE ESTE ARQUIVO DECLARA E O QUE ELE USA DE FORA DO SISTEMA - ver `FileReport.ownDeclared`.
@@ -577,6 +594,8 @@ function scanCore(file, source, table) {
577
594
  const declares = DECLARES_TOKEN.exec(line);
578
595
  if (declares && table.byName.has(declares[1].toLowerCase()))
579
596
  return;
597
+ if (declares && inTokenBlock)
598
+ return;
580
599
  // `rgba(${r}, ${g}, ${b}, ${a})` is code computing a colour, not a colour
581
600
  // written by hand - flagging it would be telling someone to tokenize a
582
601
  // variable. And the same literal twice in one declaration (a two-stop
@@ -252,7 +252,13 @@
252
252
  * dizia que o hook "reports on every file you write" - a instrução que o agente dele lê sobre o próprio
253
253
  * mecanismo. Passa a dizer o fim do turno (E04), e só o `upgrade` reescreve o bloco.
254
254
  */
255
- export const MATERIALISER_SINCE = "0.16.455";
255
+ /**
256
+ * 0.16.455 -> 0.16.459 em 27/09, e o passo 1 dá **SIM**: o gerador que o `upgrade` usa para reescrever os
257
+ * componentes passa a traduzir `outline` (o foco de todo botão saía `[outline:2px_solid_#…]`) e a achar
258
+ * o nome dele pelo valor quando a receita pede um degrau - rodada 1 do refinamento. Um componente escrito
259
+ * antes carrega o valor cru onde o projeto tem nome.
260
+ */
261
+ export const MATERIALISER_SINCE = "0.16.459";
256
262
  /**
257
263
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
258
264
  *
@@ -402,7 +408,13 @@ export const COUNTED_DIFFERENTLY = "this run counts a value as named only when Y
402
408
  * mesma jornada, e o hook fixado nela manda só o registro em todo turno - nunca o censo, nem com a
403
409
  * main limpa num commit novo. Quem está nela não vê o sistema mudar depois do commit, e precisa saber.
404
410
  */
405
- export const CHECKER_SINCE = "0.16.457";
411
+ /**
412
+ * 0.16.457 -> 0.16.458 em 27/09, e o passo 1 dá **SIM**: a varredura que o hook roda deixa de acusar as
413
+ * declarações dentro de `@theme` e `:root` (rodada 1 do refinamento - o `check_file` dava "19 written by
414
+ * hand" no `@theme` que o leigo copiou do GUIDE). Um hook fixado antes continua acusando o vocabulário do
415
+ * projeto como valor à mão, em todo turno.
416
+ */
417
+ export const CHECKER_SINCE = "0.16.458";
406
418
  /**
407
419
  * A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
408
420
  *
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { readdir, readFile } from "node:fs/promises";
17
17
  import { join, relative, sep } from "node:path";
18
- import { DEFAULT_CONVENTION, } from "./component-codegen.js";
18
+ import { DEFAULT_CONVENTION, ThemeVocab, } from "./component-codegen.js";
19
19
  /**
20
20
  * Which React the consumer is on, for the ref-carrying prop type.
21
21
  *
@@ -317,3 +317,121 @@ function themeVarsIn(css) {
317
317
  }
318
318
  return out;
319
319
  }
320
+ /**
321
+ * OS NOMES DELE COM OS VALORES, e os nossos também - para o gerador achar o nome pelo valor.
322
+ *
323
+ * Rodada 1 do refinamento, 27/09: com só os nomes, o gerador não via que o `--color-faint` dele segura o
324
+ * mesmo `#5d675f` do degrau `{color.phosphor.500}` que a receita pede, e casava o `--color-foreground` do
325
+ * `create-next-app` (`#171717`) com o `foreground` claro do sistema. Ver `ThemeVocab`.
326
+ *
327
+ * `names` é a resposta de `theirThemeVars` - a interseção das pastas de app -, e só esses nomes entram:
328
+ * o valor não pode trazer de volta um nome que aquele app não gera. As indireções são seguidas dos dois
329
+ * lados; um valor que termina numa variável nossa (`var(--ds-…)`) é resolvido pela folha instalada.
330
+ */
331
+ export async function themeVocabOf(root, slug, names) {
332
+ if (!names)
333
+ return null;
334
+ const ds = join(root, "_synthesisui", "ds", slug);
335
+ const pointer = await readFile(join(ds, "tokens.css"), "utf8").catch(() => "");
336
+ const inner = /@import\s+["']\.\/([^"']+)["']/.exec(pointer)?.[1];
337
+ const installed = inner ? await readFile(join(ds, inner), "utf8").catch(() => "") : pointer;
338
+ const dsRaw = declarationsIn(installed, /--ds-[a-zA-Z0-9_-]+/);
339
+ const theirRaw = new Map();
340
+ const walk = async (at) => {
341
+ for (const entry of await readdir(at, { withFileTypes: true }).catch(() => [])) {
342
+ if (SKIP.has(entry.name) || entry.name.startsWith("."))
343
+ continue;
344
+ const path = join(at, entry.name);
345
+ if (entry.isDirectory())
346
+ await walk(path);
347
+ else if (/\.css$/i.test(entry.name)) {
348
+ const css = await readFile(path, "utf8").catch(() => "");
349
+ if (!THEME_BLOCK.test(css) && !/:root\b/.test(css))
350
+ continue;
351
+ for (const [n, v] of declarationsIn(css, /--(?!ds-)[a-zA-Z0-9_-]+/))
352
+ if (!theirRaw.has(n))
353
+ theirRaw.set(n, v);
354
+ }
355
+ }
356
+ };
357
+ await walk(root);
358
+ const resolveIn = (table) => {
359
+ const seen = new Set();
360
+ const go = (value) => {
361
+ const ref = /^var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,([^)]*))?\)$/.exec(value.trim());
362
+ if (!ref)
363
+ return value.trim();
364
+ const target = ref[1];
365
+ if (seen.has(target))
366
+ return null;
367
+ seen.add(target);
368
+ const next = target.startsWith("--ds-") ? dsRaw.get(target) : table.get(target);
369
+ if (next !== undefined)
370
+ return go(next);
371
+ return ref[2]?.trim() || null;
372
+ };
373
+ return (value) => {
374
+ seen.clear();
375
+ return go(value);
376
+ };
377
+ };
378
+ const theirs = new Map();
379
+ const resolveTheirs = resolveIn(theirRaw);
380
+ for (const n of names) {
381
+ const raw = theirRaw.get(n);
382
+ const value = raw === undefined ? null : resolveTheirs(raw);
383
+ if (value)
384
+ theirs.set(n, value);
385
+ }
386
+ const dsValues = new Map();
387
+ const resolveDs = resolveIn(dsRaw);
388
+ for (const [n, raw] of dsRaw) {
389
+ const value = resolveDs(raw);
390
+ if (value)
391
+ dsValues.set(n, value);
392
+ }
393
+ /** Um nome sem valor conhecido continua no conjunto: `nameFor` o trata como antes. */
394
+ const vocab = new ThemeVocab(theirs, dsValues);
395
+ for (const n of names)
396
+ vocab.add(n);
397
+ return vocab;
398
+ }
399
+ /** `--nome: valor` em qualquer formatação - a primeira declaração de cada nome vence. */
400
+ function declarationsIn(css, name) {
401
+ const out = new Map();
402
+ const re = new RegExp(`(?:^|[{;])\\s*(${name.source})\\s*:\\s*([^;}]+)(?=[;}])`, "g");
403
+ for (const m of css.matchAll(re))
404
+ if (!out.has(m[1]))
405
+ out.set(m[1], m[2].trim());
406
+ return out;
407
+ }
408
+ /**
409
+ * QUANTOS UTILITÁRIOS DESTE CÓDIGO SÃO NOMES DELE - `bg-primary` quando ele declara `--color-primary`.
410
+ *
411
+ * Conta pela classe inteira, com a variante tirada (`hover:border-faint` conta), e só nos namespaces que
412
+ * o gerador traduz. Uma classe que não bate com nome nenhum dele não conta - nem para mais, nem para menos.
413
+ */
414
+ export function namedUtilitiesIn(code, names) {
415
+ const NS = [
416
+ [/^(?:bg|text|border(?:-[trblxy])?|outline|fill|stroke|ring)-(.+)$/, ["color"]],
417
+ [/^rounded(?:-[trbl]{1,2})?-(.+)$/, ["radius"]],
418
+ [/^(?:p|px|py|pt|pr|pb|pl|m|mx|my|mt|mr|mb|ml|gap|gap-x|gap-y)-(.+)$/, ["spacing"]],
419
+ [/^text-(.+)$/, ["text"]],
420
+ [/^font-(.+)$/, ["font", "font-weight"]],
421
+ [/^shadow-(.+)$/, ["shadow"]],
422
+ ];
423
+ let n = 0;
424
+ for (const lit of code.matchAll(/"([^"\n]*)"/g)) {
425
+ for (const raw of lit[1].split(/\s+/)) {
426
+ const cls = raw.slice(raw.lastIndexOf(":") + 1);
427
+ if (!cls || cls.startsWith("["))
428
+ continue;
429
+ if (NS.some(([re, nss]) => {
430
+ const key = re.exec(cls)?.[1];
431
+ return key !== undefined && nss.some((ns) => names.has(`--${ns}-${key}`));
432
+ }))
433
+ n += 1;
434
+ }
435
+ }
436
+ return n;
437
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.457",
3
+ "version": "0.16.459",
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": {