synthesisui 0.16.421 → 0.16.422

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.
@@ -12,14 +12,40 @@
12
12
  import { utilitiesOn } from "./idiom-names.js";
13
13
  import { normalizeValue, tokenMatch } from "./tokens.js";
14
14
  /**
15
- * O NOME QUE SE ESCREVE NO ARQUIVO DELE - e existe uma função só para isto por um motivo.
15
+ * O NOME QUE SE ESCREVE NO ARQUIVO DELE - e ele é SEMPRE do vocabulário dele (`INV-GERAL-13`).
16
16
  *
17
17
  * Seis lugares decidem o que a pessoa vê ou o que o `--fix` grava: o relatório, o plano, o hook, o
18
18
  * MCP, a aplicação e a contagem. Deixar cada um lembrar de preferir `theirToken` é o padrão do
19
19
  * argumento opcional que se esquece - e o sintoma seria o pior possível: o comando aconselhando um
20
20
  * nome e o hook aconselhando outro, sobre o mesmo arquivo.
21
+ *
22
+ * O NOSSO NOME SAIU DAQUI EM 11/09, e ele era a metade que contrariava a decisão de 07/09.
23
+ *
24
+ * A regra é literal: *"tem nome no sistema DELE? SIM -> a nossa receita usa o nome dele. NÃO ->
25
+ * mantém HARDCODED, e a plataforma só AVISA"*. Esta função devolvia `theirToken ?? token`, e o
26
+ * `token` é o nome COMPILADO do sistema - a grafia `--ds-*`, que é invenção nossa. Então o comando
27
+ * que promete adotar o sistema trocava um literal que funciona por uma variável que só resolve com
28
+ * uma folha nossa carregada no app dele.
29
+ *
30
+ * MEDIDO no `codelevel` em 04/09, sobre as 216 trocas com nome esperando: **60 (28%) escreviam a
31
+ * nossa grafia**. Eram exatamente as que exigiam o `@import` - e para protegê-las o comando recusava
32
+ * as outras 156 por inteiro, cobrando a folha em troca.
33
+ *
34
+ * O valor que só o nosso lado nomeia não desaparece do relatório: ele vira AVISO, com o valor e a
35
+ * linha, e a decisão de batizá-lo continua sendo dele. Ver `ourNameOnly`.
21
36
  */
22
- export const nameToWrite = (f) => f.theirToken ?? f.token;
37
+ export const nameToWrite = (f) => f.theirToken ?? (f.tokenIsTheirs ? f.token : null);
38
+ /**
39
+ * O VALOR QUE **SÓ** O NOSSO LADO NOMEIA - o que a plataforma AVISA em vez de escrever.
40
+ *
41
+ * É a outra metade de `nameToWrite`, e ela existe para o silêncio não acontecer: um achado sem nome
42
+ * dele e com nome nosso some das duas listas se ninguém perguntar por ele, e sumir é
43
+ * indistinguível de "está tudo nomeado" para quem lê (lei 8).
44
+ *
45
+ * Quem chama DIZ o valor e onde ele está - nunca propõe o nome. É a escolha dele de 10/09 entre as
46
+ * duas alternativas, e a mesma que o hook já segue.
47
+ */
48
+ export const ourNameOnly = (f) => !f.theirToken && !f.tokenIsTheirs && Boolean(f.token);
23
49
  /**
24
50
  * `next/og` renders JSX to a PNG on the server. There is no document, so there
25
51
  * is no `var(--ds-*)` to read: every colour in such a file MUST be a literal.
@@ -607,6 +633,8 @@ function scanCore(file, source, table) {
607
633
  * `--spacing` no vocabulário dele, e qual dos dois está certo depende de onde o literal está.
608
634
  */
609
635
  const theirs = table.aliases.get(`${kind}:${normalizeValue(literal, table.rootPx)}`);
636
+ /** A procedência do `token`, lida da tabela que o achou - ver `tokenIsTheirs`. */
637
+ const tableIsTheirs = table.source !== null && table.source !== "installed";
610
638
  findings.push({
611
639
  kind,
612
640
  line: at,
@@ -617,6 +645,9 @@ function scanCore(file, source, table) {
617
645
  ? { fontRelative: true }
618
646
  : {}),
619
647
  ...(theirs?.writable ? { theirToken: theirs.name } : {}),
648
+ ...(tableIsTheirs && match?.token
649
+ ? { tokenIsTheirs: true }
650
+ : {}),
620
651
  /**
621
652
  * O NOME QUE SÓ SE DIZ - `$gray_dark` do Sass, `@brand` do Less.
622
653
  *
@@ -196,8 +196,14 @@ export function theirNames(ours, theirs) {
196
196
  * 66 delas gravadas por um `--fix --write`, e a pior na PRIMEIRA página do relatório -
197
197
  * `0.25em → --radius-xs` em 116 arquivos. Ver `FAMILY_WORDS`.
198
198
  *
199
- * Recusar aqui não perde informação: sem alias, `nameToWrite` cai no NOSSO token, que é da
200
- * família certa porque foi o prefixo dela que o trouxe a este laço.
199
+ * E DESDE 11/09 RECUSAR AQUI SIGNIFICA SILÊNCIO, não um nome nosso no lugar.
200
+ *
201
+ * Esta linha dizia que recusar não perdia informação, *"porque sem alias `nameToWrite` cai no
202
+ * NOSSO token"*. Ele não cai mais: a nossa grafia deixou de ser escrita no código dele
203
+ * (`INV-GERAL-13`), então um valor cuja categoria não fecha sai da lista de trocas em vez de
204
+ * sair com o nosso nome. Isso é o desfecho certo - a troca errada era escrever um token de
205
+ * tipo num `border-radius` -, e o valor não desaparece: ele é DITO, com o valor e a linha,
206
+ * pelo caminho que `ourNameOnly` alimenta.
201
207
  */
202
208
  const usable = formDecides(value)
203
209
  ? candidates
package/dist/fonts.js CHANGED
@@ -89,7 +89,7 @@ export function googleFontsHref(families) {
89
89
  */
90
90
  export const FAMILY_SEAM_PREFIX = "--ds-typography-families-";
91
91
  export function nextFontSnippet(input) {
92
- const { families, slug, appDir = "app", seam, facts, declaredWeights, } = input;
92
+ const { families, appDir = "app", facts, declaredWeights } = input;
93
93
  /**
94
94
  * A ORDEM É DETERMINÍSTICA e os duplicados saem: isto vira uma linha de arquivo gerado, e um
95
95
  * conjunto que muda de ordem entre rodadas produz diff onde nada mudou.
@@ -219,15 +219,39 @@ export function nextFontSnippet(input) {
219
219
  * projeto dele deixaria de compilar por causa da linha que a gente mandou colar.
220
220
  */
221
221
  const emitted = roles.filter((role) => constOf(role) !== undefined);
222
+ /**
223
+ * O `data-ds` SAIU DO LAYOUT DELE EM 11/09 - `INV-GERAL-13`.
224
+ *
225
+ * O atributo existia para a NOSSA folha aplicar dentro dele, e nenhum app dele carrega a nossa
226
+ * folha. O que resta na linha é o que o `next/font` exige e é dele: a classe que carrega a
227
+ * variável de cada fonte.
228
+ */
222
229
  const layout = [
223
230
  `// ${appDir}/layout.tsx`,
224
231
  `import { ${[...new Set(emitted.map(constOf))].join(", ")} } from "./fonts";`,
225
- `<body data-ds="${slug}" className={\`${[...new Set(emitted.map((r) => `\${${constOf(r)}.variable}`))].join(" ")}\`}>`,
232
+ `<body className={\`${[...new Set(emitted.map((r) => `\${${constOf(r)}.variable}`))].join(" ")}\`}>`,
226
233
  ];
234
+ /**
235
+ * O BLOCO DE CSS PASSA A ESCREVER NO VOCABULÁRIO **DELE** - o conserto de 11/09.
236
+ *
237
+ * O QUE ELE ESCREVIA: um `[data-ds="<slug>"]` com `--ds-typography-families-<role>` apontando para
238
+ * a fonte. Nosso escopo, nossa variável, dentro da folha dele - e só resolvia com a nossa folha
239
+ * carregada, que é o que `INV-GERAL-13` proíbe.
240
+ *
241
+ * O QUE ELE ESCREVE AGORA: um `@theme` com `--font-<role>`, que é o namespace de fonte do Tailwind
242
+ * DELE. Duas coisas acontecem de uma vez, e as duas são dele: a utility `font-<role>` passa a
243
+ * existir no projeto dele carregando a fonte que o `next/font` baixou, e `theirThemeVars` -
244
+ * o leitor que decide o utilitário nomeado no codegen - passa a ver aquele nome. Daí em diante o
245
+ * componente que a plataforma escreve veste `font-<role>` em vez de um literal, porque o `@theme`
246
+ * DELE declara o nome.
247
+ *
248
+ * Nenhuma linha aqui menciona o sistema, o slug ou uma variável nossa: é a fonte dele, ligada ao
249
+ * Tailwind dele.
250
+ */
227
251
  const css = [
228
- `/* ${appDir}/globals.css - AFTER the tokens.css import */`,
229
- `[data-ds="${slug}"] {`,
230
- ...emitted.map((role) => ` ${seam?.[role] ?? `${FAMILY_SEAM_PREFIX}${role}`}: var(${roleVar(role)});`),
252
+ `/* ${appDir}/globals.css - in your own @theme */`,
253
+ `@theme {`,
254
+ ...emitted.map((role) => ` --font-${role}: var(${roleVar(role)});`),
231
255
  `}`,
232
256
  ];
233
257
  /**
@@ -1,134 +1,23 @@
1
- import { readdir, readFile } from "node:fs/promises";
2
- import { dirname, join, posix, relative, resolve } from "node:path";
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
3
  import { exists } from "./agent-wiring.js";
4
4
  /**
5
- * A FOLHA GLOBAL QUE OS APPS DELE REALMENTE CARREGAM - e num monorepo ela quase nunca é a do app.
5
+ * AS PASTAS DE APP DESTE REPOSITÓRIO - e era um módulo sobre a folha global dele até 11/09.
6
6
  *
7
- * O QUE O CLIENTE PERCEBE SEM ISTO. O `add` manda importar os tokens em `apps/<x>/app/globals.css`.
8
- * MEDIDO em 01/09 no `codelevel`: os DOIS apps dele fazem `@import "@repo/ui/styles/globals.css"`, e
9
- * o `globals.css` do app diz, em comentário dele, *"Do not redeclare tokens or fonts here - extend in
10
- * styles/"*. A instrução apontava para o arquivo errado, e a convenção certa estava escrita ali do
11
- * lado.
7
+ * O QUE MORAVA AQUI: `globalSheetOf`, `sheetChainOf`, `prefixFrom` e o resolvedor de `exports` do
8
+ * workspace. Os quatro existiam para uma coisa - achar a folha que os apps dele realmente carregam,
9
+ * e contar o `../` até a raiz, para a instrução de `@import` apontar para o arquivo certo.
12
10
  *
13
- * Ele seguiu a instrução no arquivo CERTO - o do pacote - e o caminho relativo que a gente imprimiu
14
- * era o do app. Um monorepo com pacote de UI compartilhado não é exceção: é a forma comum, e a
15
- * instrução tem que derivar de onde o CSS dele mora em vez de assumir.
11
+ * Era a parte mais cuidadosa daquele texto. MEDIDO em 01/09 no `codelevel`: os dois apps dele fazem
12
+ * `@import "@repo/ui/styles/globals.css"`, e o `globals.css` do app diz, em comentário DELE, *"Do
13
+ * not redeclare tokens or fonts here"*. A instrução apontava para o arquivo errado, com o caminho
14
+ * relativo de outro, e o conserto foi seguir a cadeia em vez de assumir.
16
15
  *
17
- * ─────────────────────────────────────────────────────────────────────────
18
- * COMO A FOLHA REAL É ENCONTRADA, e nada aqui é palpite:
19
- *
20
- * 1. abre o `globals.css` do app
21
- * 2. procura `@import "<spec>"` que aponte para um CSS DENTRO deste repositório
22
- * 3. resolve o `<spec>`: caminho relativo, ou pacote do workspace pelo `exports`
23
- * 4. repete na folha encontrada, até ela não re-exportar mais
24
- *
25
- * O QUE NÃO É SEGUIDO: `tailwindcss` e qualquer coisa que não resolva para um arquivo daqui. Um
26
- * `@import "tailwindcss"` é a biblioteca, e mandar o cliente editá-la seria pior que a instrução que
27
- * isto conserta.
16
+ * O TEXTO INTEIRO DEIXOU DE EXISTIR (`INV-GERAL-13`): nada que a plataforma escreve no repositório
17
+ * dele depende de folha nossa, então não há `@import` para apontar. O que sobrevive é a única
18
+ * pergunta que não era sobre a nossa folha - *"quais pastas deste repositório são app?"* -, porque é
19
+ * onde o `fonts.ts` do `next/font` DELE entra, um por app.
28
20
  */
29
- /** Quantos saltos seguir antes de desistir - um ciclo de imports não pode travar um `add`. */
30
- const MAX_HOPS = 5;
31
- const IMPORT = /@import\s+["']([^"']+)["']/g;
32
- /** `@repo/ui/styles/globals.css` → `packages/ui/src/styles/globals.css`, pelo `exports` dele. */
33
- async function fromWorkspace(root, spec) {
34
- for (const group of ["packages", "apps", "libs"]) {
35
- let entries;
36
- try {
37
- const { readdir } = await import("node:fs/promises");
38
- entries = await readdir(join(root, group));
39
- }
40
- catch {
41
- continue;
42
- }
43
- for (const entry of entries) {
44
- const pkgPath = join(root, group, entry, "package.json");
45
- const raw = await readFile(pkgPath, "utf8").catch(() => null);
46
- if (!raw)
47
- continue;
48
- let pkg;
49
- try {
50
- pkg = JSON.parse(raw);
51
- }
52
- catch {
53
- continue;
54
- }
55
- if (!pkg.name || !spec.startsWith(`${pkg.name}/`))
56
- continue;
57
- const sub = `./${spec.slice(pkg.name.length + 1)}`;
58
- const target = pkg.exports?.[sub];
59
- if (typeof target !== "string")
60
- continue;
61
- return posix.join(group, entry, target.replace(/^\.\//, ""));
62
- }
63
- }
64
- return null;
65
- }
66
- /**
67
- * A folha onde os tokens dele devem entrar, relativa à raiz - `appSheet` quando nada a re-exporta.
68
- *
69
- * DELEGA, e não caminha: quem caminha é `sheetChainOf`. Dois caminhadores sobre a mesma cadeia é
70
- * como um deles para de seguir um salto que o outro segue, e ninguém descobre até um cliente
71
- * receber a instrução apontando para a folha errada - que é exatamente o defeito que
72
- * `INV-VOLTA-12` fechou.
73
- */
74
- export async function globalSheetOf(root, appSheet) {
75
- const chain = await sheetChainOf(root, appSheet);
76
- return chain[chain.length - 1] ?? appSheet;
77
- }
78
- /**
79
- * A CADEIA INTEIRA, da folha do app até a última que ninguém re-exporta - e por que ela é pública.
80
- *
81
- * O QUE O CLIENTE GANHA: a resposta "este app carrega o meu design system?" medida no app DELE, e
82
- * não no projeto. `readWiring` varria a raiz e devolvia "existe em algum lugar daqui": num monorepo
83
- * com dois apps servidos, um fiado e outro não, o fiado respondia pelo outro - e a pessoa recebia
84
- * "está tudo certo" sobre o app que não carrega nada.
85
- *
86
- * A pergunta dos IMPORTS não se responde varrendo pasta: ela se responde seguindo a cadeia de
87
- * `@import` a partir da folha daquele app, porque a folha que carrega os tokens quase nunca é a do
88
- * app - num monorepo é a do pacote compartilhado, e os dois apps chegam nela.
89
- *
90
- * A ordem é do app para fora, e o teto de saltos é o mesmo: um ciclo de imports não pode travar um
91
- * comando.
92
- */
93
- export async function sheetChainOf(root, appSheet) {
94
- const chain = [];
95
- let current = appSheet;
96
- for (let hop = 0; hop < MAX_HOPS; hop += 1) {
97
- chain.push(current);
98
- const raw = await readFile(join(root, current), "utf8").catch(() => null);
99
- if (!raw)
100
- return chain;
101
- let next = null;
102
- for (const m of raw.matchAll(IMPORT)) {
103
- const spec = m[1];
104
- if (!spec.endsWith(".css"))
105
- continue;
106
- const candidate = spec.startsWith(".")
107
- ? posix.normalize(posix.join(posix.dirname(current), spec))
108
- : await fromWorkspace(root, spec);
109
- if (!candidate)
110
- continue;
111
- /** Fora da raiz não é folha dele para editar - e um `..` demais sai do repositório. */
112
- if (candidate.startsWith(".."))
113
- continue;
114
- const readable = await readFile(join(root, candidate), "utf8").catch(() => null);
115
- if (readable === null)
116
- continue;
117
- next = candidate;
118
- break;
119
- }
120
- /** Uma folha que aponta para si mesma encerra a cadeia em vez de gastar o teto de saltos. */
121
- if (!next || next === current || chain.includes(next))
122
- return chain;
123
- current = next;
124
- }
125
- return chain;
126
- }
127
- /** O `../` que leva daquela folha até a raiz do repositório - o prefixo do `@import`. */
128
- export function prefixFrom(sheet) {
129
- const up = relative(dirname(resolve("/r", sheet)), "/r");
130
- return up === "" ? "./" : `${up.split(/[\\/]/).join("/")}/`;
131
- }
132
21
  /**
133
22
  * A RAIZ DE CADA APP DESTE REPOSITÓRIO - onde o escopo e a tipografia entram, um por app.
134
23
  *
package/dist/guide.js CHANGED
@@ -293,8 +293,49 @@ toolsReachable = false,
293
293
  * essa porta; deixá-la entreaberta um nível acima seria vigiá-la em vez de fechá-la
294
294
  * (CLAUDE.md, Parte I, seção 6).
295
295
  */
296
- fontFacts) {
296
+ fontFacts,
297
+ /**
298
+ * O VOCABULÁRIO DESTE REPOSITÓRIO - `tongueFromArtifacts`, ou `null` quando não há folha para ler.
299
+ *
300
+ * O QUE ISTO CONSERTA, e é a razão de existir desta etapa: este arquivo é o que o agente DELE lê
301
+ * antes de escrever, e ele ensinava a nossa grafia. *"Color: `var(--ds-color-semantic-<role>)`"*
302
+ * é uma instrução para escrever a NOSSA variável no código dele - e ela só resolve com a nossa
303
+ * folha carregada, que é o que `INV-GERAL-13` proíbe. Um guia que ensina isso produz a violação
304
+ * sem que ninguém decida nada: o agente obedece.
305
+ *
306
+ * Com o vocabulário em mão, cada linha abaixo diz o nome que o código DELE dá àquele valor - ou o
307
+ * valor, quando o código dele não nomeia nenhum.
308
+ *
309
+ * OBRIGATÓRIO, pelo mesmo argumento de `fontFacts` logo acima: um argumento opcional aqui é um
310
+ * argumento que um chamador novo esquece, e o esquecimento devolve a nossa grafia ao arquivo que
311
+ * mais a propaga.
312
+ */
313
+ tongue) {
297
314
  const { document: doc, slug, name, version } = payload;
315
+ /**
316
+ * COMO ISTO SE ESCREVE NO CÓDIGO DELE - a mesma resposta que a materialização usa.
317
+ *
318
+ * `var(--nome-dele)` quando o repositório dele nomeia aquele valor; o valor literal quando não.
319
+ * Sem vocabulário (projeto que nunca buildou, folha ausente) a resposta é `null`, e quem chama
320
+ * DIZ isso em vez de imprimir uma grafia que ninguém pode escrever.
321
+ */
322
+ const saysAs = (ours) => {
323
+ if (!tongue)
324
+ return null;
325
+ const theirs = tongue.names.get(ours);
326
+ if (theirs)
327
+ return `var(${theirs})`;
328
+ return tongue.values.get(ours) ?? null;
329
+ };
330
+ /** `spacing` -> `md → var(--gap-md)`, para cada degrau que o vocabulário alcança. */
331
+ const family = (prefix, keys) => {
332
+ const said = keys
333
+ .map((k) => ({ k, as: saysAs(`--ds-${prefix}-${kebab(k)}`) }))
334
+ .filter((x) => x.as !== null);
335
+ return said.length > 0
336
+ ? said.map((x) => `\`${x.k}\` → \`${x.as}\``).join(", ")
337
+ : list(keys);
338
+ };
298
339
  const { meta, foundations, motion, components } = doc;
299
340
  const semanticRoles = Object.keys(foundations.color.semantic);
300
341
  /**
@@ -339,8 +380,6 @@ fontFacts) {
339
380
  */
340
381
  const fontPlan = nextFontSnippet({
341
382
  families: foundations.typography.families,
342
- slug,
343
- seam: payload.fontSeam,
344
383
  facts: fontFacts,
345
384
  /** Os pesos que ELE declara - ver `declaredWeights`. O guia é o arquivo que o agente dele cola. */
346
385
  declaredWeights: foundations.typography.weights,
@@ -548,7 +587,10 @@ It writes a **deterministic scaffold**: the page uses this DS's \`.ds-*\` recipe
548
587
  path-classes, paired with a co-located scoped CSS (Next target) that carries the **responsive** media
549
588
  queries and the **CSS-only hamburger** - so the page is mobile-ready out of the box. **Refine it in
550
589
  place** - wire real data, split into components, swap the chart/icon/media placeholders - but keep the
551
- \`data-ds="${slug}"\` wrapper and the \`.ds-*\` / layout classes so it stays on-system. Run
590
+ wrapper and the \`.ds-*\` / layout classes: the stylesheet next to the page carries the rules for the
591
+ ones it wears, scoped to that wrapper - the command says how many came along, so the page stands on
592
+ its own with nothing imported. A \`.ds-*\` you ADD while refining has no rule there; bring that piece
593
+ in with \`synthesisui component\` instead. Run
552
594
  \`synthesisui init\` once to set the target (next/general) and the output folder.
553
595
 
554
596
  ## Single components as YOUR code
@@ -560,8 +602,8 @@ synthesisui component ${slug} button
560
602
  It writes \`<componentsDir>/button/\` with \`button.tsx\` (+ colocated \`button.css\` in the default
561
603
  \`styles: "css"\` flavor, or Tailwind utilities inline with \`styles: "tailwind"\` - set once via
562
604
  \`synthesisui init --styles tailwind\`). Import and render: \`import { Button } from "components/button"\`.
563
- The boundary: tokens are global (\`tokens.css\` + the \`data-ds\` root attribute, once per app);
564
- everything a component owns lives in its own folder.
605
+ The boundary: everything a component owns lives in its own folder, and every value in it is a name
606
+ YOUR code declares or the value itself - there is nothing global to set up.
565
607
 
566
608
  **A component's conditional classes are decided at the call site, not in the stylesheet.** A recipe
567
609
  carries the look of a state as a rule keyed on a data attribute (\`[data-size="lg"]\`), and the caller
@@ -623,80 +665,73 @@ ${meta.narrative}
623
665
  ${rulesNote}
624
666
  ## How to apply
625
667
 
626
- 1. Import the system once in your project's global CSS, using a path **relative to that CSS
627
- file** - from \`app/globals.css\` in a Next App Router project that means a leading \`../\`:
628
- \`\`\`css
629
- @import "../_synthesisui/ds/${slug}/tokens.css";
630
- \`\`\`
631
- (drop the \`../\` if your global CSS sits at the project root.)${hasTailwind
632
- ? `\n **Using Tailwind (this project's default)? You also need \`theme.css\` right after \`tokens.css\` -\n without it the DS-backed utilities (\`bg-primary\`, \`p-md\`…) aren't generated and the UI renders\n unstyled. See "Styling with Tailwind v4" below for the full import block.**`
633
- : ""}
668
+ **Nothing here is imported into your app.** \`_synthesisui/ds/${slug}/\` is the reference this guide,
669
+ the check, the doctor and the MCP server read to interpret your design - your code never points at
670
+ it, and nothing we write into this repository needs it at runtime.
634
671
 
635
- 2. Wrap the tree that should use the system with the scope attribute:
636
- \`\`\`html
637
- <div data-ds="${slug}">…your UI here…</div>
638
- \`\`\`
639
- All \`--ds-*\` custom properties and \`.ds-*\` classes only apply inside that scope.
640
- Applying \`data-ds="${slug}"\` at the app root (e.g. \`<body>\` or the root layout)
641
- is the simplest choice - the whole app then wears the system.
642
- ${hasAlt
643
- ? `
644
- 3. Light/dark: an ancestor with \`data-scheme="${altScheme}"\` switches the neutral roles to the opposite mode.
645
- \`\`\`tsx
646
- <div data-scheme="${altScheme}"><div data-ds="${slug}">…</div></div>
672
+ 1. Ask for a component and it arrives as YOUR code:
647
673
  \`\`\`
648
- A theme toggle just adds/removes that attribute on the scope element:
649
- \`\`\`tsx
650
- root.toggleAttribute("data-scheme"); // present = ${altScheme}, absent = ${meta.scheme}
674
+ npx synthesisui component ${slug} <name>
651
675
  \`\`\`
652
- `
676
+ It lands in your components directory speaking the names your own code declares - or carrying the
677
+ value itself, where your code names none. No import, no scope attribute, nothing at runtime.
678
+
679
+ 2. Writing something by hand? Use the vocabulary below. Every name in it is a name **your own code
680
+ declares**, written exactly as you would write it.
681
+ ${hasAlt
682
+ ? `\n3. Light/dark: this system's roles have two readings, and a component that arrives from
683
+ \`component\` carries both. Which one shows is your app's own scheme switch - we do not add one.\n`
653
684
  : ""}${fontsSection}${depsSection}${hasTailwind
654
685
  ? `
655
686
  ## Styling with Tailwind v4 (preferred in this project)
656
687
 
657
- Import \`theme.css\` after \`tailwindcss\` and \`tokens.css\`:
658
- \`\`\`css
659
- @import "tailwindcss";
660
- @import "./_synthesisui/ds/${slug}/tokens.css";
661
- @import "./_synthesisui/ds/${slug}/theme.css";
662
- \`\`\`
663
- This maps the DS tokens onto Tailwind's theme, so inside \`[data-ds="${slug}"]\` you get utilities
664
- backed by the design system: \`bg-*\`/\`text-*\`/\`border-*\` (semantic colors), \`p-*\`/\`m-*\`/\`gap-*\`
665
- (spacing), \`rounded-*\`, \`shadow-*\`, \`font-*\` (families **and** weights), \`text-*\` (type scale), \`ease-*\`${seriesKeys.length > 0
666
- ? `, \`bg-series-*\`/\`text-series-*\`/\`fill-series-*\` (data-viz series)`
667
- : ""}.
688
+ Your own \`@theme\` is what backs the utilities, and it is the one that decides. Where it declares a
689
+ name, the utility already paints your value and it is the most readable code you can write; where it
690
+ does not, write the value.
668
691
 
669
- **Prefer these utilities for layout and new composition** - they are this project's idiom and read
670
- far better than inline \`style\`. Reach for inline \`var(--ds-*)\` only when no utility fits.
692
+ **Prefer utilities for layout and new composition** - they are this project's idiom and read far
693
+ better than inline \`style\`.
671
694
 
672
695
  \`\`\`tsx
673
- // ✅ preferred - Tailwind utilities backed by the DS
674
- <main className="bg-canvas text-foreground p-2xl flex flex-col gap-md">
675
- <button className="ds-button" data-intent="primary">Save</button>
696
+ // preferred - your project's own utilities
697
+ <main className="p-2xl flex flex-col gap-md">
698
+ <button className="rounded-md px-md py-2xs">Save</button>
676
699
  </main>
700
+ \`\`\`
701
+
702
+ To give a utility the system's value, declare the name in **your own** \`@theme\` - it is your
703
+ stylesheet, your namespace, and nothing of ours is involved:
677
704
 
678
- // ❌ avoid - inline styles with raw var() when a utility exists
679
- <main style={{ background: "var(--ds-color-semantic-canvas)", padding: "var(--ds-spacing-2xl)" }}>
705
+ \`\`\`css
706
+ @theme {
707
+ --radius-md: <the value you want rounded-md to paint>;
708
+ }
680
709
  \`\`\`
681
710
 
711
+ Do that and \`synthesisui component\` starts writing \`rounded-md\` instead of the raw value, because
712
+ the name now exists in your project.
713
+
682
714
  ---
683
715
  `
684
716
  : ""}
685
- This is **v${version}**. The stable entrypoints at \`_synthesisui/ds/${slug}/\` (the
686
- \`tokens.css\`/\`theme.css\` re-exports, plus \`.lock\`) always point at the active version - import
687
- those, not the versioned ones. The pinned files for this version - ${artifactList},
717
+ This is **v${version}**. The pinned files for this version - ${artifactList},
688
718
  \`design-system.json\` (canonical source of truth), \`GUIDE.md\` (this file) - live in
689
- \`_synthesisui/ds/${slug}/v${version}/\`.
719
+ \`_synthesisui/ds/${slug}/v${version}/\`, and they are OURS to read: the check, the doctor and the MCP
720
+ server use them to interpret your design. Your app imports none of them.
690
721
 
691
722
  ---
692
723
  ${pagesSection}
693
724
  ## Building with the system
694
725
 
695
726
  **This system is for building real product UI** - pages, layouts, dashboards, whole flows.
696
- Compose the \`.ds-*\` recipes (and their parts) together with the DS-backed utilities to assemble
697
- actual screens. There is **no "samples only" rule**: build the real app. An
698
- \`app/synthesisui-samples/<component>/\` page is a fine *optional* scratch space to eyeball a single
699
- component, but it is never required.
727
+ Bring the recipes in as YOUR code (\`npx synthesisui component ${slug} <name>\`) and compose them with
728
+ your own utilities to assemble actual screens. There is **no "samples only" rule**: build the real
729
+ app. An \`app/synthesisui-samples/<component>/\` page is a fine *optional* scratch space to eyeball a
730
+ single component, but it is never required.
731
+
732
+ The \`.ds-*\` class names below describe the recipe's SHAPE - which parts it has and which
733
+ \`data-*\` each one takes. They are what the materialized component wears, and the stylesheet that
734
+ declares them comes with it.
700
735
 
701
736
  ### Layout & composition
702
737
  The system defines the scale; these are sensible defaults for spending it:
@@ -728,10 +763,9 @@ part classes and their \`data-*\` are listed per component below. Example - a ta
728
763
  `
729
764
  : ""}
730
765
  ### Overlays & portals
731
- Dialogs, menus and toasts are often rendered through a portal at the end of \`<body>\` - **outside**
732
- your \`data-ds\` scope. Since \`.ds-*\`/\`--ds-*\` only resolve inside the scope, wrap any portalled UI
733
- in its own \`<div data-ds="${slug}"${hasAlt ? ` data-scheme="…"` : ""}>\`, or apply \`data-ds\` at the
734
- app root so everything (portals included) inherits it. Behavior (open/close, focus trap, positioning,
766
+ Dialogs, menus and toasts are often rendered through a portal at the end of \`<body>\` - outside
767
+ the tree they were written in. A component brought in by \`synthesisui component\` carries its own
768
+ stylesheet and works anywhere, portal included. Behavior (open/close, focus trap, positioning,
735
769
  keyboard) is yours to wire - the system ships the **looks**, not the JavaScript.
736
770
 
737
771
  ### Interactive recipes - the behavior contract
@@ -756,34 +790,47 @@ styles it, you make it work.
756
790
  ## Rules (follow them when creating components)
757
791
  ${hasTailwind
758
792
  ? `
759
- - **Styling mechanism:** prefer Tailwind utilities backed by the DS (\`bg-primary\`, \`p-md\`,
760
- \`font-display\`, \`font-medium\`, …) for layout and new composition, and reuse the \`.ds-*\` recipes
761
- for components the DS already covers. Use inline \`style\` with \`var(--ds-*)\` only as a last resort.
762
- The token names below are the source vocabulary - every utility derives from them.`
793
+ - **Styling mechanism:** prefer your project's own Tailwind utilities for layout and new
794
+ composition, and bring the recipes in as code (\`synthesisui component ${slug} <name>\`) for the
795
+ components this system already covers. Where no utility fits, write the name from the vocabulary
796
+ below - it is a name your own code declares.`
763
797
  : ""}
764
- - **Always use semantic tokens**, never raw values nor primitives directly.
765
- Color: \`var(--ds-color-semantic-<role>)\`${hasTailwind ? " (utility: `bg-<role>`/`text-<role>`)" : ""}. The roles are: ${list(semanticRoles.map((r) => {
798
+ - **Always use the system's vocabulary**, never raw values picked by eye. Every name below is a
799
+ name **your own code declares** - written exactly as you would write it. Nothing here comes from
800
+ a stylesheet of ours, and nothing needs one.
801
+ Color${hasTailwind ? " (utility: `bg-<role>`/`text-<role>`)" : ""}: ${semanticRoles
802
+ .map((r) => {
766
803
  const theirs = theirRole(r);
767
- return theirs === r ? r : `${r} (${theirs} in your code)`;
768
- }))}.
769
- - Primitives (\`--ds-color-<palette>-<step>\`) exist but should **not** be referenced directly -
770
- they feed the semantic roles.${namedColours.length > 0
771
- ? `\n- **Yours by name** → \`var(--ds-color-<name>)\`: ${namedColours.length} colour${namedColours.length === 1 ? "" : "s"} your code names by PURPOSE rather than by step, so no scale could hold ${namedColours.length === 1 ? "it" : "them"} - ${list(namedColours.slice(0, 8))}${namedColours.length > 8 ? ", …" : ""}. These are yours: reach for them when the purpose matches, and prefer a semantic role when it does not.`
804
+ /** O papel com o nome que ELE dá a ele - e o parêntese só sai quando os dois diferem. */
805
+ const named = theirs === r ? `\`${r}\`` : `\`${r} (${theirs} in your code)\``;
806
+ const as = saysAs(`--ds-color-semantic-${kebab(r)}`);
807
+ return as ? `${named} → \`${as}\`` : named;
808
+ })
809
+ .join(", ")}.${namedColours.length > 0
810
+ ? `\n- **Yours by name**: ${namedColours.length} colour${namedColours.length === 1 ? "" : "s"} your code names by PURPOSE rather than by step, so no scale could hold ${namedColours.length === 1 ? "it" : "them"} - ${family("color", namedColours.slice(0, 8))}${namedColours.length > 8 ? ", …" : ""}. These are yours: reach for them when the purpose matches, and prefer a semantic role when it does not.`
772
811
  : ""}${seriesKeys.length > 0
773
- ? `\n- Data-viz → \`var(--ds-color-series-<n>)\`${hasTailwind ? " (utility: `bg-series-<n>`/`text-series-<n>`/`fill-series-<n>`)" : ""}: categorical chart/series colors, ${seriesKeys.length} of them (${list(seriesKeys)}). Use them in order for multi-series charts; they re-paint with the system.`
812
+ ? `\n- Data-viz${hasTailwind ? " (utility: `bg-series-<n>`/`text-series-<n>`/`fill-series-<n>`)" : ""}: categorical chart/series colors, ${seriesKeys.length} of them - ${family("color-series", seriesKeys)}. Use them in order for multi-series charts; they re-paint with the system.`
774
813
  : ""}
775
- - Spacing → \`var(--ds-spacing-<key>)\`: ${list(Object.keys(foundations.spacing))}.
776
- - Radius → \`var(--ds-radius-<key>)\`: ${list(Object.keys(foundations.radius))}.
777
- - Shadow → \`var(--ds-shadow-<key>)\`: ${list(Object.keys(foundations.shadow))}.
814
+ - Spacing → ${family("spacing", Object.keys(foundations.spacing))}.
815
+ - Radius → ${family("radius", Object.keys(foundations.radius))}.
816
+ - Shadow → ${family("shadow", Object.keys(foundations.shadow))}.
778
817
  - Typography: families ${familySlots(foundations.typography.families)
779
- .map((slot) => `\`${payload.fontSeam?.[slot] ?? `${FAMILY_SEAM_PREFIX}${slot}`}\` (${foundations.typography.families[slot]})`)
818
+ .map((slot) => {
819
+ const as = saysAs(`${FAMILY_SEAM_PREFIX}${kebab(slot)}`);
820
+ const face = foundations.typography.families[slot];
821
+ return as ? `\`${slot}\` → \`${as}\` (${face})` : `\`${slot}\` (${face})`;
822
+ })
780
823
  .join(", ")};
781
824
  weights${hasTailwind ? " (utility: `font-<key>`)" : ""}: ${list(weights)};
782
- scale \`--ds-typography-scale-<key>-font-size\`${hasTailwind ? " (utility: `text-<key>`)" : ""}: ${list(Object.keys(foundations.typography.scale))}.
783
- - Motion: durations \`--ds-motion-durations-<key>\` (${list(Object.keys(motion.durations))}) and
784
- easings \`--ds-motion-easings-<key>\` (${list(Object.keys(motion.easings))}). Use them on
785
- \`transition\` (e.g. \`transition: color var(--ds-motion-durations-fast) var(--ds-motion-easings-standard)\`)
786
- so timing stays on-brand. For ANIMATION, this system ships a named vocabulary - see
825
+ scale${hasTailwind ? " (utility: `text-<key>`)" : ""}: ${Object.keys(foundations.typography.scale)
826
+ .map((k) => {
827
+ const as = saysAs(`--ds-typography-scale-${kebab(k)}-font-size`);
828
+ return as ? `\`${k}\` → \`${as}\`` : `\`${k}\``;
829
+ })
830
+ .join(", ")}.
831
+ - Motion: durations ${family("motion-durations", Object.keys(motion.durations))}
832
+ and easings ${family("motion-easings", Object.keys(motion.easings))}. Use them on
833
+ \`transition\` so timing stays on-brand. For ANIMATION, this system ships a named vocabulary - see
787
834
  **Motion vocabulary** below; never hand-roll \`@keyframes\` or raw durations.
788
835
  - When **creating a new component** the DS does not cover yet: compose it from these semantic
789
836
  tokens to inherit the system's identity; do not invent colors/measures outside the scale.
@@ -817,7 +864,8 @@ tell the person what you chose from the vocabulary instead.`
817
864
 
818
865
  ## Ready-made components
819
866
 
820
- Each recipe becomes a \`.ds-<name>\` class (inside the \`[data-ds="${slug}"]\` scope). Variants are
867
+ Each recipe has a \`.ds-<name>\` class - the SHAPE it wears when \`synthesisui component\` writes it
868
+ into your project, with the stylesheet that declares it alongside. Variants are
821
869
  \`data-<axis>="<option>"\` attributes; states (hover/focus/active/disabled) ship in the CSS;
822
870
  multi-part components expose \`.ds-<name>-<part>\` classes (listed under each).
823
871
 
@@ -832,7 +880,7 @@ A small gamification library the AI advisor (\`synthesisui advise\`) can propose
832
880
  \`.ds-<name>\` recipe shape as the components above, token-only so they wear the system. Use them
833
881
  **only where they fit the product** (progress, retention, recognition); they're a library to compose
834
882
  from, not a default - and lean against over-gamifying a serious B2B product. Each is a \`.ds-<name>\`
835
- class inside the \`[data-ds="${slug}"]\` scope; multi-part ones expose \`.ds-<name>-<part>\`.
883
+ recipe; multi-part ones expose \`.ds-<name>-<part>\`.
836
884
 
837
885
  ${blockLines.join("\n\n")}
838
886
  `
@@ -235,7 +235,7 @@
235
235
  * chama `wireAgent`, então rodar o comando é o caminho de volta - e ele só alarga a string que era
236
236
  * nossa, nunca um filtro que uma pessoa escreveu.
237
237
  */
238
- export const MATERIALISER_SINCE = "0.16.421";
238
+ export const MATERIALISER_SINCE = "0.16.422";
239
239
  /**
240
240
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
241
241
  *
@@ -343,8 +343,8 @@ export const MATERIALISER_SINCE = "0.16.421";
343
343
  * A frase viaja ao lado da versão porque as duas são uma coisa só: quem move a marca troca a
344
344
  * explicação no mesmo lugar, em vez de deixar o CI citando a causa da marca anterior.
345
345
  */
346
- export const COUNTED_DIFFERENTLY_SINCE = "0.16.421";
347
- export const COUNTED_DIFFERENTLY = "this run counts the utilities of your own theme - that version counted only `var(--token)`";
346
+ export const COUNTED_DIFFERENTLY_SINCE = "0.16.422";
347
+ export const COUNTED_DIFFERENTLY = "this run counts a value as named only when YOUR code names it - that version also counted the ones only the system named";
348
348
  /**
349
349
  * 0.16.408 -> 0.16.413 em 10/09: o hook passa a DIZER o valor que nada nomeia. Ele silenciava toda
350
350
  * deriva sem token de destino - a decisão estava escrita como "não vale interromper, porque o único
@@ -366,7 +366,7 @@ export const COUNTED_DIFFERENTLY = "this run counts the utilities of your own th
366
366
  * utility, um arquivo escrito inteiro no vocabulário do sistema recebia zero. A contagem em si é a
367
367
  * outra marca - ver `COUNTED_DIFFERENTLY_SINCE`, que sobe no mesmo diff.
368
368
  */
369
- export const CHECKER_SINCE = "0.16.421";
369
+ export const CHECKER_SINCE = "0.16.422";
370
370
  /**
371
371
  * A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
372
372
  *