synthesisui 0.16.385 → 0.16.387

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.
@@ -9,7 +9,7 @@ import { body, section, snippet } from "../output.js";
9
9
  import { findCollision, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
10
10
  import { fetchComponent, RegistryError } from "../registry.js";
11
11
  import { flavourResolver } from "../styles-flavour.js";
12
- import { inTheirTongue, projectTongue } from "../their-tongue.js";
12
+ import { inTheirTongue, projectTongue, sumSpoken, } from "../their-tongue.js";
13
13
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
14
14
  import { recordWritten } from "../written.js";
15
15
  /**
@@ -100,27 +100,26 @@ export async function component(slug, name, opts) {
100
100
  * continua sendo o caminho - o comando DIZ qual dos dois aconteceu.
101
101
  */
102
102
  const tongue = await projectTongue(root, slug);
103
- const spoken = tongue ? inTheirTongue(res.css, tongue) : null;
104
- const css = spoken ? spoken.css : res.css;
105
103
  /**
106
- * E ELE FICA SABENDO - traduzir em silêncio é a outra metade do mesmo erro.
104
+ * O RELATÓRIO SÓ EXISTE DEPOIS DE OS BYTES EXISTIREM - e é essa ordem que corrige o defeito.
107
105
  *
108
- * A última linha é a que decide se a nossa folha ainda é necessária aqui, e ela é a diferença
109
- * entre "seu repositório dispensa a folha" e "quase" - dizer a primeira quando a verdade é a
110
- * segunda é a promessa que quebra na tela dele.
106
+ * Ele era calculado aqui, sobre `res.css`, e impresso ANTES da materialização. Num projeto
107
+ * tailwind o CSS não vai a disco: a frase falava de um arquivo inexistente enquanto o `.tsx`
108
+ * escrito apontava 45 vezes para a nossa folha (medido no `button` do codelevel, 07/09). Agora
109
+ * cada caminho de escrita preenche esta variável com a tradução DAQUELES arquivos, e a mensagem
110
+ * sai no fim - depois da linha que nomeia o que foi escrito.
111
111
  */
112
- if (spoken && (spoken.named > 0 || spoken.inlined > 0)) {
113
- console.log(` ${spoken.named} reference${spoken.named === 1 ? "" : "s"} now speak${spoken.named === 1 ? "s" : ""} the name YOUR code gives the value${spoken.inlined > 0 ? `, and ${spoken.inlined} carr${spoken.inlined === 1 ? "ies" : "y"} the value because your code names no token for it` : ""}.`);
114
- console.log(spoken.left.length === 0
115
- ? ` Nothing in this file points at our stylesheet - it renders on your own tokens alone.`
116
- : ` ${spoken.left.length} still point${spoken.left.length === 1 ? "s" : ""} at our stylesheet (${spoken.left.slice(0, 3).join(", ")}${spoken.left.length > 3 ? ", …" : ""}), so tokens.css is still needed here.`);
117
- }
112
+ let spoken = null;
118
113
  const artifactsAreTheProduct = opts.artifactsOnly === true || config.target !== "next";
119
114
  if (artifactsAreTheProduct) {
120
115
  const dir = join(root, "_synthesisui", "ds", slug, "components");
121
116
  await mkdir(dir, { recursive: true });
122
117
  await writeFile(join(dir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
123
- await writeFile(join(dir, `${res.name}.css`), `${css}\n`, "utf8");
118
+ /** A FOLHA TAMBÉM PASSA PELA PORTA - este caminho escreve sem o codegen. */
119
+ const sheet = tongue ? inTheirTongue(res.css, tongue) : null;
120
+ if (sheet)
121
+ spoken = sheet;
122
+ await writeFile(join(dir, `${res.name}.css`), `${sheet ? sheet.css : res.css}\n`, "utf8");
124
123
  console.log(`✓ ${res.name} → _synthesisui/ds/${slug}/components/${res.name}.{json,css} (${slug} v${res.version})`);
125
124
  }
126
125
  // 2. YOUR component - a real, importable `export function <Pascal>()` in the
@@ -169,7 +168,29 @@ export async function component(slug, name, opts) {
169
168
  */
170
169
  const theirs = res.recipe?.api;
171
170
  const already = theirs?.file && existsSync(join(root, theirs.file)) ? theirs : null;
172
- if (already && !opts.force) {
171
+ /**
172
+ * UM NOME PRÓPRIO JÁ É A RESPOSTA PARA A COLISÃO - achado do dono em 06/09, e é a SEGUNDA volta
173
+ * deste mesmo defeito.
174
+ *
175
+ * O QUE ELE FEZ: `component codelevel button --as=ButtonSample`, no repositório de onde o
176
+ * sistema foi importado. A recusa disparou, disse *"writing another one would leave you with
177
+ * two"* e ofereceu `--force` - com um `--as` na linha de comando, ignorado.
178
+ *
179
+ * OS DOIS ERROS DE UMA VEZ. O primeiro é a decisão: este guard responde "você já tem" sem
180
+ * nunca ler `opts.as`, embora `localName` - a função que resolve o nome - fique 70 linhas
181
+ * abaixo. O segundo é a frase: com um nome próprio NÃO ficariam dois `Button`; ficaria o dele
182
+ * e um `ButtonSample`. E a única saída oferecida, `--force`, escreve com o nome ORIGINAL - a
183
+ * única opção que de fato causaria a duplicação que a mensagem existe para evitar.
184
+ *
185
+ * A PRIMEIRA VOLTA está registrada em `component-as.spec.ts`: a recusa IMPRIMIA `--as` e a
186
+ * flag não existia em `ComponentOptions`. A flag nasceu; o guard não passou a consultá-la.
187
+ *
188
+ * E A POPULAÇÃO É O CLIENTE-ALVO INTEIRO. Medido na base viva em 06/09: no `codelevel`, **64
189
+ * dos 66** componentes (97%) apontam para um arquivo que existe no repositório dele - porque o
190
+ * sistema NASCEU dali. Nos 25 sistemas gerados aqui, **0 de ~45**. O comando funcionava para
191
+ * quem não é o cliente e recusava para quem é.
192
+ */
193
+ if (already && !opts.force && !opts.as?.trim()) {
173
194
  console.log(section("You already have this component"));
174
195
  console.log(body(`\`${already.name}\` lives in ${already.file} - writing another one would leave you with two.`));
175
196
  if (already.from) {
@@ -180,14 +201,44 @@ export async function component(slug, name, opts) {
180
201
  console.log(body(`It is not re-exported from that folder's entry, so import it by path.`));
181
202
  }
182
203
  console.log("");
183
- console.log(body("The recipe and the compiled CSS are on disk either way - only the .tsx was not written. Pass --force to write it anyway."));
204
+ /**
205
+ * A SAÍDA QUE NÃO DUPLICA VEM PRIMEIRO, e ela é a que a pessoa quer em quase todo caso.
206
+ *
207
+ * `--force` continua existindo e continua last: ele escreve com o nome DELE, que é a única
208
+ * opção que realmente deixa dois. Oferecê-lo como única saída era mandar a pessoa fazer
209
+ * exatamente o que a frase acima diz para evitar.
210
+ */
211
+ console.log(body(`To see what this recipe produces without touching your file, give it a name of its own:`));
212
+ console.log("");
213
+ console.log(snippet([
214
+ `npx synthesisui@latest component ${slug} ${res.name} --as ${res.name}-sample`,
215
+ ]));
216
+ console.log("");
217
+ console.log(body("The recipe and the compiled CSS are on disk either way - only the .tsx was not written. Pass --force to overwrite yours instead."));
184
218
  return;
185
219
  }
186
- const clash = await findCollision(root, config.componentsDir, res.name, pascalName);
220
+ /**
221
+ * A COLISÃO SE MEDE PELO NOME QUE VAI SER ESCRITO, e não pelo do blueprint.
222
+ *
223
+ * A metade que o guard acima não resolvia sozinha: passado o primeiro guard com `--as`, este
224
+ * procurava por `button`/`Button` - o nome do BLUEPRINT - e achava o componente dele de novo.
225
+ * Consertar um e deixar o outro deixaria o `--as` aceito e barrado uma linha depois, o que é
226
+ * pior que recusar de cara: a pessoa faz o que a tela mandou e recebe outra recusa.
227
+ *
228
+ * `local` já era calculado, 70 linhas abaixo, para decidir a PASTA. Ele sobe para cá porque é
229
+ * a mesma pergunta - qual nome este comando vai ocupar - e duas respostas para ela é o defeito.
230
+ */
231
+ const local = localName(res.name, opts.as);
232
+ const localPascal = local
233
+ .split(/[^a-zA-Z0-9]+/)
234
+ .filter(Boolean)
235
+ .map((p) => p[0].toUpperCase() + p.slice(1))
236
+ .join("");
237
+ const clash = await findCollision(root, config.componentsDir, local, localPascal);
187
238
  if (!opts.force && (clash.exported || (clash.file && !clash.ours))) {
188
239
  console.log(section("This name is already taken in your project"));
189
240
  if (clash.exported) {
190
- console.log(body(`\`${pascalName}\` is already exported from ${clash.exported}.`));
241
+ console.log(body(`\`${localPascal}\` is already exported from ${clash.exported}.`));
191
242
  }
192
243
  if (clash.file && !clash.ours) {
193
244
  console.log(body(`${clash.file} exists and we did not write it.`));
@@ -211,7 +262,7 @@ export async function component(slug, name, opts) {
211
262
  * componente. O CSS não muda de nome: a receita é a do blueprint, e as classes dela são
212
263
  * `.ds-<blueprint>` - o componente novo veste o mesmo desenho, com o nome dele no arquivo.
213
264
  */
214
- const local = localName(res.name, opts.as);
265
+ /** Ver o cálculo acima: uma resposta só para "qual nome este comando ocupa". */
215
266
  const compDir = join(root, config.componentsDir, local);
216
267
  await mkdir(compDir, { recursive: true });
217
268
  let filenames;
@@ -220,9 +271,23 @@ export async function component(slug, name, opts) {
220
271
  // it wears (.css) + the barrel. Ignores the css|tailwind flavor - the
221
272
  // template drives itself off the .ds-* classes.
222
273
  const tsx = interactiveTemplate(res.name);
274
+ /**
275
+ * O TEMPLATE CURADO TAMBÉM FALA A LÍNGUA DELE - e como `stylesheet`.
276
+ *
277
+ * Ele escreve `transition: opacity var(--ds-motion-durations-base)` num `style` inline de
278
+ * JSX, onde o espaço é válido: pedir o escape do Tailwind aqui trocaria um valor correto por
279
+ * um inválido. O caminho interativo escreve sem o codegen, então a porta é chamada aqui.
280
+ */
281
+ const say = (text) => tongue ? inTheirTongue(text, tongue, "stylesheet") : null;
282
+ const saidTsx = say(tsx);
283
+ const saidCss = say(res.css);
284
+ spoken = sumSpoken([saidTsx, saidCss]);
223
285
  const written = [
224
- { filename: `${local}.tsx`, content: tsx },
225
- { filename: `${local}.css`, content: `${css}\n` },
286
+ { filename: `${local}.tsx`, content: saidTsx ? saidTsx.css : tsx },
287
+ {
288
+ filename: `${local}.css`,
289
+ content: `${saidCss ? saidCss.css : res.css}\n`,
290
+ },
226
291
  { filename: "index.ts", content: `export * from "./${local}";\n` },
227
292
  ];
228
293
  for (const f of written)
@@ -232,7 +297,7 @@ export async function component(slug, name, opts) {
232
297
  filenames = written.map((f) => f.filename);
233
298
  }
234
299
  else {
235
- const files = generateComponentFiles(slug, res.name, res.recipe, css, res.version, flavour, await reactMajorOf(root),
300
+ const { files, spoken: said } = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavour, await reactMajorOf(root),
236
301
  /**
237
302
  * THE SPELLING, FROM THE VERSION WE JUST FETCHED.
238
303
  *
@@ -251,7 +316,8 @@ export async function component(slug, name, opts) {
251
316
  * O ARQUIVO e o EXPORT levam o nome dele; a CLASSE continua sendo a do blueprint. Um
252
317
  * `<CardPreview>` vestindo `.ds-card` está estilizado certo e não faz sombra em nada dele.
253
318
  */
254
- local, await readInstalledScheme(root, slug));
319
+ local, await readInstalledScheme(root, slug), tongue);
320
+ spoken = said;
255
321
  /**
256
322
  * AS DECLARAÇÕES DO ARQUIVO DELE QUE O INTERPRETADOR NÃO LEU - ver `unread-for-component.ts`.
257
323
  *
@@ -285,6 +351,19 @@ export async function component(slug, name, opts) {
285
351
  else if (opts.interactive && !hasInteractiveTemplate(res.name)) {
286
352
  console.log(` note: no interactive template for "${res.name}" - materialized the standard shell.`);
287
353
  }
354
+ /**
355
+ * O RELATÓRIO DA TRADUÇÃO - agora sobre os arquivos que ACABARAM de ser nomeados acima.
356
+ *
357
+ * Um ponto só, depois de todo caminho de escrita (artefato, interativo, codegen), porque a
358
+ * pergunta é uma: o que foi escrito ainda precisa da nossa folha? Enquanto isto era calculado
359
+ * antes da materialização, a resposta descrevia outro arquivo.
360
+ */
361
+ if (spoken && (spoken.named > 0 || spoken.inlined > 0)) {
362
+ console.log(` ${spoken.named} reference${spoken.named === 1 ? "" : "s"} now speak${spoken.named === 1 ? "s" : ""} the name YOUR code gives the value${spoken.inlined > 0 ? `, and ${spoken.inlined} carr${spoken.inlined === 1 ? "ies" : "y"} the value because your code names no token for it` : ""}.`);
363
+ console.log(spoken.left.length === 0
364
+ ? ` No variable in what was just written points at our stylesheet - they are all names YOUR code declares.`
365
+ : ` ${spoken.left.length} still point${spoken.left.length === 1 ? "s" : ""} at our stylesheet (${spoken.left.slice(0, 3).join(", ")}${spoken.left.length > 3 ? ", …" : ""}), so tokens.css carries ${spoken.left.length === 1 ? "it" : "those"}.`);
366
+ }
288
367
  // ── DX: concrete paths + copy-pasteable snippets, with breathing room ──
289
368
  const tailwind = flavour === "tailwind";
290
369
  const imports = tailwind
@@ -295,6 +374,23 @@ export async function component(slug, name, opts) {
295
374
  ]
296
375
  : [`@import "../_synthesisui/ds/${slug}/tokens.css";`];
297
376
  console.log(section(`One-time setup (once per app, for "${slug}")`));
377
+ /**
378
+ * POR QUE O SETUP CONTINUA AQUI QUANDO NENHUMA VARIÁVEL É NOSSA - e dizer isto é o conserto.
379
+ *
380
+ * A tentação era suprimir o bloco quando `left` é vazio, e ele leria como a promessa "seu
381
+ * repositório dispensa a folha". Medido no codelevel (07/09): as duas coisas são independentes.
382
+ * As VARIÁVEIS podem ser todas dele e o arquivo continua vestindo utilitários que só o adaptador
383
+ * do sistema gera (`rounded-3`, `gap-2xs`) e movimento que o `theme.css` declara referenciando
384
+ * `--ds-*`. Suprimir entregaria um componente sem raio e sem gap.
385
+ *
386
+ * Então o bloco não muda de tamanho: ele muda de MOTIVO. A linha abaixo diz qual das duas coisas
387
+ * a importação ainda serve, e ela é derivada, nunca fixa.
388
+ */
389
+ if (spoken && spoken.left.length === 0) {
390
+ console.log(body(`(the variables are already yours - what these imports still bring is the`));
391
+ console.log(body(` system's own utilities and its motion, which the app has to generate)`));
392
+ console.log("");
393
+ }
298
394
  console.log(body(`1. Import the design system in your GLOBAL stylesheet, e.g. app/globals.css`));
299
395
  console.log(body(` (the path is relative to that file - hence the leading ../):`));
300
396
  console.log("");
@@ -307,7 +403,19 @@ export async function component(slug, name, opts) {
307
403
  console.log(body(`(If you haven't installed the system yet, run: synthesisui add ${slug})`));
308
404
  console.log(section("Use it"));
309
405
  if (!opts.artifactsOnly && config.target === "next") {
310
- const pascalName = res.name
406
+ /**
407
+ * O EXEMPLO APONTA PARA O QUE FOI ESCRITO - a terceira metade do defeito do `--as` (06/09).
408
+ *
409
+ * Este bloco derivava o nome e o caminho de `res.name`, o nome do BLUEPRINT. Com
410
+ * `--as button-sample`, o arquivo vai para `components/button-sample/` e o exemplo mandava
411
+ * importar de `components/button` - um caminho que não existe. Um comando que escreve certo e
412
+ * instrui errado é pior que um que recusa: a pessoa copia a linha e o build quebra.
413
+ *
414
+ * `localName` é a mesma função dos dois guards acima. Uma resposta só para o nome, do primeiro
415
+ * guard até a última linha impressa.
416
+ */
417
+ const usedName = localName(res.name, opts.as);
418
+ const pascalName = usedName
311
419
  .split(/[^a-zA-Z0-9]+/)
312
420
  .filter(Boolean)
313
421
  .map((p) => p[0].toUpperCase() + p.slice(1))
@@ -318,7 +426,7 @@ export async function component(slug, name, opts) {
318
426
  ? ` ${firstAxis[0]}="${Object.keys(firstAxis[1])[0]}"`
319
427
  : "";
320
428
  console.log(snippet([
321
- `import { ${pascalName} } from "@/${config.componentsDir}/${res.name}";`,
429
+ `import { ${pascalName} } from "@/${config.componentsDir}/${usedName}";`,
322
430
  "",
323
431
  `<${pascalName}${exampleProp} />`,
324
432
  ]));
@@ -327,7 +435,7 @@ export async function component(slug, name, opts) {
327
435
  console.log("");
328
436
  console.log(body("Or ask your agent:"));
329
437
  console.log(snippet([
330
- `"Use the ${pascalName} component from ${config.componentsDir}/${res.name} (SynthesisUI ${slug})."`,
438
+ `"Use the ${pascalName} component from ${config.componentsDir}/${usedName} (SynthesisUI ${slug})."`,
331
439
  ]));
332
440
  }
333
441
  else {
@@ -6,6 +6,7 @@ import { installedSlugs } from "../installed.js";
6
6
  import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
7
7
  import { postGenerate, RegistryError } from "../registry.js";
8
8
  import { flavourResolver } from "../styles-flavour.js";
9
+ import { projectTongue } from "../their-tongue.js";
9
10
  import { recordWritten } from "../written.js";
10
11
  /** PascalCase para o hint de import (course-card → CourseCard). */
11
12
  function pascalName(name) {
@@ -69,10 +70,12 @@ export async function generate(description, opts) {
69
70
  await mkdir(compDir, { recursive: true });
70
71
  // A LÍNGUA DE CADA COMPONENTE, resolvida uma vez - ver `flavourResolver`.
71
72
  const flavourOf = await flavourResolver(root, config.styles);
72
- const files = generateComponentFiles(slug, res.name, res.recipe, res.css, version, flavourOf(res.name), await reactMajorOf(root),
73
+ const { files } = generateComponentFiles(slug, res.name, res.recipe, res.css, version, flavourOf(res.name), await reactMajorOf(root),
73
74
  // Read off the installed document: a generated component lands in the same
74
75
  // project as the stylesheet it has to match.
75
- await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug));
76
+ await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug),
77
+ /** O VOCABULÁRIO DELE - um componente gerado cai no mesmo projeto e fala a mesma língua. */
78
+ await projectTongue(root, slug));
76
79
  for (const file of files) {
77
80
  await writeFile(join(compDir, file.filename), file.code, "utf8");
78
81
  }
@@ -7,6 +7,7 @@ import { body, section, snippet } from "../output.js";
7
7
  import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
8
8
  import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
9
9
  import { flavourResolver } from "../styles-flavour.js";
10
+ import { projectTongue } from "../their-tongue.js";
10
11
  import { recordWritten } from "../written.js";
11
12
  /** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
12
13
  * a folder holding only refit artifacts doesn't count). */
@@ -118,7 +119,9 @@ export async function refit(file, opts) {
118
119
  await mkdir(compDir, { recursive: true });
119
120
  // A LÍNGUA DE CADA COMPONENTE, resolvida uma vez - ver `flavourResolver`.
120
121
  const flavourOf = await flavourResolver(root, config.styles);
121
- const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, flavourOf(res.name), await reactMajorOf(root), await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug));
122
+ const { files } = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, flavourOf(res.name), await reactMajorOf(root), await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug),
123
+ /** O VOCABULÁRIO DELE - o `refit` reescreve o componente e não passava pela porta. */
124
+ await projectTongue(root, slug));
122
125
  for (const f of files) {
123
126
  await writeFile(join(compDir, f.filename), f.code, "utf8");
124
127
  }
@@ -11,6 +11,7 @@ import { body, section, snippet } from "../output.js";
11
11
  import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
12
12
  import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
13
13
  import { flavourResolver } from "../styles-flavour.js";
14
+ import { projectTongue } from "../their-tongue.js";
14
15
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
15
16
  import { editedSinceWritten, keptLine, readWritten, recordWritten, } from "../written.js";
16
17
  import { add } from "./add.js";
@@ -323,6 +324,13 @@ export async function upgrade(asked, opts) {
323
324
  // componente: `upgrade` reescreve tudo que já está instalado, e reler o censo a cada
324
325
  // arquivo seria a mesma resposta lida N vezes. Ver `flavourResolver`.
325
326
  const flavourOf = await flavourResolver(root, config.styles);
327
+ /**
328
+ * O VOCABULÁRIO DELE, e este comando é o que mais precisava dele: `upgrade` REESCREVE
329
+ * componentes que a pessoa já tem, e ele não passava pela porta - então cada corrida DESFAZIA a
330
+ * tradução que o `component` tinha feito, devolvendo `var(--ds-*)` a arquivos que já falavam a
331
+ * língua do repositório. Resolvido uma vez para a corrida inteira, como o sabor.
332
+ */
333
+ const tongue = await projectTongue(root, slug);
326
334
  for (const entry of entries) {
327
335
  const tsxPath = join(componentsRoot, entry, `${entry}.tsx`);
328
336
  let head = "";
@@ -347,11 +355,11 @@ export async function upgrade(asked, opts) {
347
355
  }
348
356
  try {
349
357
  const res = await fetchComponent(base, slug, entry);
350
- const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavourOf(res.name),
358
+ const { files } = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavourOf(res.name),
351
359
  // Both were missing here, and `upgrade` is the command that REWRITES
352
360
  // components somebody already has: without the convention it would have
353
361
  // taken a working component and stripped its styles.
354
- await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)), res.name, await readInstalledScheme(root, slug));
362
+ await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)), res.name, await readInstalledScheme(root, slug), tongue);
355
363
  /**
356
364
  * A NOTA DAS DECLARAÇÕES NÃO INTERPRETADAS SOBREVIVE AO UPGRADE.
357
365
  *
@@ -1,4 +1,5 @@
1
1
  import { emittableAttrs } from "./attr-shape.js";
2
+ import { inTheirTongue, sumSpoken, } from "./their-tongue.js";
2
3
  import { PHRASING_FORMS, } from "./types.js";
3
4
  export const DEFAULT_CONVENTION = {
4
5
  prefix: "ds-",
@@ -1384,7 +1385,20 @@ localName = name,
1384
1385
  * without ever deciding, and an optional argument that can be forgotten is the
1385
1386
  * shape this file has already been bitten by. `tsc` refuses the half-call instead.
1386
1387
  */
1387
- scheme) {
1388
+ scheme,
1389
+ /**
1390
+ * O VOCABULÁRIO DESTE REPOSITÓRIO - `projectTongue(root, slug)`, ou `null` quando não há mapa.
1391
+ *
1392
+ * OBRIGATÓRIO, e é a única forma de fechar a porta (INV-VOLTA-02). A tradução morava em cada
1393
+ * comando: o `component` traduzia o CSS por fora e o `upgrade`, o `refit` e o `generate` não
1394
+ * traduziam nada - três dos cinco materializadores reescreviam o repositório dele com
1395
+ * `var(--ds-*)` cru, e o `upgrade` DESFAZIA a tradução que o `component` tinha feito. O gate lia
1396
+ * o texto do módulo e via a porta importada, então passava verde.
1397
+ *
1398
+ * Aqui a tradução acontece uma vez, sobre os bytes que vão a disco, e `tsc` recusa a meia-chamada
1399
+ * de um materializador novo - o mesmo argumento do `scheme` logo acima.
1400
+ */
1401
+ tongue) {
1388
1402
  const files = [];
1389
1403
  const props = propsTypeName(reactMajor);
1390
1404
  if (styles === "css") {
@@ -1404,5 +1418,28 @@ scheme) {
1404
1418
  filename: "index.ts",
1405
1419
  code: `export * from "./${localName}";\n`,
1406
1420
  });
1407
- return files;
1421
+ return speak(files, tongue, styles === "tailwind" ? "tailwind-class" : "stylesheet");
1422
+ }
1423
+ /**
1424
+ * A TRADUÇÃO, SOBRE OS BYTES, UMA VEZ - e o destino é de quem GEROU o texto, não da extensão.
1425
+ *
1426
+ * No modo tailwind toda referência nossa está DENTRO de um valor arbitrário (`arbitrary()` é o
1427
+ * único ponto do módulo que escreve `var(--ds-…)`, e ele sempre a envolve em `[prop:…]`), então um
1428
+ * gradiente inlined precisa de `_` no lugar do espaço ou a classe não é gerada. Numa folha, e num
1429
+ * `style` inline de JSX, o espaço é válido e `_` seria o defeito - por isso `interactive-templates`
1430
+ * (que escreve `transition: opacity var(--ds-…)`) chama a porta como `stylesheet`.
1431
+ *
1432
+ * `index.ts` é um re-export e não carrega estilo, mas passa pela mesma porta: um arquivo isento por
1433
+ * lista é a lista que alguém esquece de atualizar.
1434
+ */
1435
+ function speak(files, tongue, tsxDestination) {
1436
+ if (!tongue)
1437
+ return { files, spoken: null };
1438
+ const reports = [];
1439
+ const said = files.map((file) => {
1440
+ const spoken = inTheirTongue(file.code, tongue, file.filename.endsWith(".css") ? "stylesheet" : tsxDestination);
1441
+ reports.push(spoken);
1442
+ return { filename: file.filename, code: spoken.css };
1443
+ });
1444
+ return { files: said, spoken: sumSpoken(reports) };
1408
1445
  }
@@ -170,7 +170,7 @@
170
170
  * O que o cliente ganha ao rodar `upgrade`: o agente dele no Codex passa a poder PERGUNTAR ao
171
171
  * sistema, em vez de só receber as regras e adivinhar o resto.
172
172
  */
173
- export const MATERIALISER_SINCE = "0.16.370";
173
+ export const MATERIALISER_SINCE = "0.16.387";
174
174
  /**
175
175
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
176
176
  *
@@ -1,6 +1,29 @@
1
1
  /** `var(--ds-x)` e `var(--ds-x, fallback)` - o que a folha compilada escreve. */
2
2
  const OUR_VAR = /var\(\s*(--ds-[a-zA-Z0-9_-]+)\s*(?:,([^()]*))?\)/g;
3
- export function inTheirTongue(css, tongue) {
3
+ /**
4
+ * VÁRIOS ARQUIVOS, UM NÚMERO - porque a promessa é sobre a MATERIALIZAÇÃO, não sobre um arquivo.
5
+ *
6
+ * Um componente sai como `.tsx` + `.css` + barrel, e dizer "nada aponta para a nossa folha" só é
7
+ * verdade quando nenhum dos três aponta. Somar aqui, e não em cada comando, é o que impede duas
8
+ * contas diferentes para a mesma pergunta.
9
+ *
10
+ * `null` quando nada foi traduzido (projeto sem mapa) - e aí quem chama DIZ isso.
11
+ */
12
+ export function sumSpoken(reports) {
13
+ const real = reports.filter((r) => r !== null);
14
+ if (real.length === 0)
15
+ return null;
16
+ const left = new Set();
17
+ for (const r of real)
18
+ for (const name of r.left)
19
+ left.add(name);
20
+ return {
21
+ named: real.reduce((n, r) => n + r.named, 0),
22
+ inlined: real.reduce((n, r) => n + r.inlined, 0),
23
+ left: [...left].sort(),
24
+ };
25
+ }
26
+ export function inTheirTongue(css, tongue, destination = "stylesheet") {
4
27
  let named = 0;
5
28
  let inlined = 0;
6
29
  const left = new Set();
@@ -18,7 +41,9 @@ export function inTheirTongue(css, tongue) {
18
41
  const value = tongue.values.get(name);
19
42
  if (value) {
20
43
  inlined += 1;
21
- return value;
44
+ return destination === "tailwind-class"
45
+ ? value.replace(/\s+/g, "_")
46
+ : value;
22
47
  }
23
48
  left.add(name);
24
49
  return fallback ? whole : whole;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.385",
3
+ "version": "0.16.387",
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": {