synthesisui 0.16.396 → 0.16.397

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.
@@ -8,6 +8,7 @@ import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-tem
8
8
  import { body, section, snippet } from "../output.js";
9
9
  import { findCollision, installedThemeVars, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
10
10
  import { fetchComponent, RegistryError } from "../registry.js";
11
+ import { installedThemeCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js";
11
12
  import { flavourResolver } from "../styles-flavour.js";
12
13
  import { inTheirTongue, projectTongue, sumSpoken, } from "../their-tongue.js";
13
14
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
@@ -110,6 +111,13 @@ export async function component(slug, name, opts) {
110
111
  * sai no fim - depois da linha que nomeia o que foi escrito.
111
112
  */
112
113
  let spoken = null;
114
+ /**
115
+ * OS BYTES QUE FORAM A DISCO - é sobre ELES que a pergunta "precisa da folha?" se responde.
116
+ *
117
+ * Pela mesma razão que `spoken` é preenchido em cada caminho de escrita: perguntar sobre o
118
+ * recipe, ou sobre o css antes da tradução, seria responder sobre um arquivo que ninguém abriu.
119
+ */
120
+ let writtenSource = "";
113
121
  const artifactsAreTheProduct = opts.artifactsOnly === true || config.target !== "next";
114
122
  if (artifactsAreTheProduct) {
115
123
  const dir = join(root, "_synthesisui", "ds", slug, "components");
@@ -119,6 +127,7 @@ export async function component(slug, name, opts) {
119
127
  const sheet = tongue ? inTheirTongue(res.css, tongue) : null;
120
128
  if (sheet)
121
129
  spoken = sheet;
130
+ writtenSource = sheet ? sheet.css : res.css;
122
131
  await writeFile(join(dir, `${res.name}.css`), `${sheet ? sheet.css : res.css}\n`, "utf8");
123
132
  console.log(`✓ ${res.name} → _synthesisui/ds/${slug}/components/${res.name}.{json,css} (${slug} v${res.version})`);
124
133
  }
@@ -287,6 +296,7 @@ export async function component(slug, name, opts) {
287
296
  ];
288
297
  for (const f of written)
289
298
  await writeFile(join(compDir, f.filename), f.content, "utf8");
299
+ writtenSource = written.map((f) => f.content).join("\n");
290
300
  /** O fingerprint do que escrevemos - é o que protege a edição dele no upgrade (T7). */
291
301
  await recordWritten(join(root, "_synthesisui", "ds", slug), local, written);
292
302
  filenames = written.map((f) => f.filename);
@@ -335,6 +345,7 @@ export async function component(slug, name, opts) {
335
345
  }));
336
346
  for (const f of written)
337
347
  await writeFile(join(compDir, f.filename), f.content, "utf8");
348
+ writtenSource = written.map((f) => f.content).join("\n");
338
349
  await recordWritten(join(root, "_synthesisui", "ds", slug), local, written);
339
350
  if (flavour === "tailwind")
340
351
  await writeCn(root, compDir, slug);
@@ -368,34 +379,63 @@ export async function component(slug, name, opts) {
368
379
  `@import "../_synthesisui/ds/${slug}/theme.css";`,
369
380
  ]
370
381
  : [`@import "../_synthesisui/ds/${slug}/tokens.css";`];
371
- console.log(section(`One-time setup (once per app, for "${slug}")`));
372
382
  /**
373
- * POR QUE O SETUP CONTINUA AQUI QUANDO NENHUMA VARIÁVEL É NOSSA - e dizer isto é o conserto.
383
+ * O SETUP SÓ É COBRADO QUANDO ESTE ARQUIVO PRECISA DELE - ver `sheet-needed.ts`.
384
+ *
385
+ * O DEFEITO, apontado pelo dono olhando a própria tela (07/09): o comando terminava dizendo
386
+ * *"No variable in what was just written points at our stylesheet"* e, três linhas abaixo,
387
+ * imprimia **One-time setup** mandando colar dois `@import`. As duas frases não podem ser
388
+ * verdadeiras ao mesmo tempo, e a segunda pedia uma dependência de runtime para um arquivo que
389
+ * não a usa.
374
390
  *
375
- * A tentação era suprimir o bloco quando `left` é vazio, e ele leria como a promessa "seu
376
- * repositório dispensa a folha". Medido no codelevel (07/09): as duas coisas são independentes.
377
- * As VARIÁVEIS podem ser todas dele e o arquivo continua vestindo utilitários que só o adaptador
378
- * do sistema gera (`rounded-3`, `gap-2xs`) e movimento que o `theme.css` declara referenciando
379
- * `--ds-*`. Suprimir entregaria um componente sem raio e sem gap.
391
+ * A VERSÃO ANTERIOR DESTE BLOCO já tinha visto metade do problema e parou na metade: manteve o
392
+ * setup e acrescentou *"o que estes imports ainda trazem são os utilitários do sistema e o
393
+ * movimento"*. A razão é real - `INV-VOLTA-13` deixa no `@theme` os nomes que só o sistema tem -
394
+ * mas ela era AFIRMADA, nunca medida. No `codelevel` aquele `@theme` declara 50 variáveis, todas
395
+ * `--animate-*` e `--max-width-*`, e o componente gerado não veste nenhuma delas: as duas
396
+ * animações que ele usa são declaradas pelo `globals.css` DELE.
380
397
  *
381
- * Então o bloco não muda de tamanho: ele muda de MOTIVO. A linha abaixo diz qual das duas coisas
382
- * a importação ainda serve, e ela é derivada, nunca fixa.
398
+ * Então a pergunta passa a ser feita: sobrou variável nossa, ou classe que só o nosso `@theme`
399
+ * gera? Se nenhuma das duas, o arquivo está isolado e o bloco diz isso - com o que o import ainda
400
+ * daria, para a escolha continuar sendo dele.
383
401
  */
384
- if (spoken && spoken.left.length === 0) {
385
- console.log(body(`(the variables are already yours - what these imports still bring is the`));
386
- console.log(body(` system's own utilities and its motion, which the app has to generate)`));
402
+ const need = whatOnlyTheSheetResolves({
403
+ source: writtenSource,
404
+ themeCss: await installedThemeCss(root, slug),
405
+ /** O vocabulário DELE sai da conta - ver `theirNames`. O mapa do `.lock` é a via mais barata. */
406
+ theirNames: tongue?.names ? [...tongue.names.values()] : [],
407
+ });
408
+ if (!need.needed) {
409
+ console.log(section("This file needs nothing else"));
410
+ console.log(body("Every variable and class in it is declared by your own code - no import, no scope"));
411
+ console.log(body("attribute, nothing at runtime. It is yours."));
387
412
  console.log("");
413
+ console.log(body(`The system stays what it is here: the reference your agent reads to build ON it.`));
414
+ console.log(body(`If you later want the utilities only its own @theme generates, that is when`));
415
+ console.log(body(`importing earns its place - and \`doctor\` says so.`));
416
+ }
417
+ /**
418
+ * E O "USE IT" SAI NOS DOIS CAMINHOS - a primeira versão disto era um `return`, e ela levava
419
+ * junto a única linha que diz COMO importar o componente. Dizer "está isolado" e sonegar o
420
+ * `import { ButtonSample }` troca um defeito por outro.
421
+ */
422
+ if (need.needed) {
423
+ console.log(section(`One-time setup (once per app, for "${slug}")`));
424
+ console.log(body(need.variables.length > 0
425
+ ? `(what still needs the sheet here: ${need.variables.slice(0, 3).join(", ")}${need.variables.length > 3 ? `, +${need.variables.length - 3}` : ""} - your code names no value for ${need.variables.length === 1 ? "it" : "them"})`
426
+ : `(what still needs the sheet here: the ${need.classes.slice(0, 3).join(", ")}${need.classes.length > 3 ? `, +${need.classes.length - 3}` : ""} ${need.classes.length === 1 ? "utility" : "utilities"}, which only this system's @theme generates)`));
427
+ console.log("");
428
+ console.log(body(`1. Import the design system in your GLOBAL stylesheet, e.g. app/globals.css`));
429
+ console.log(body(` (the path is relative to that file - hence the leading ../):`));
430
+ console.log("");
431
+ console.log(snippet(imports));
432
+ console.log("");
433
+ console.log(body(`2. Scope your app: add data-ds="${slug}" to a ROOT element, e.g. app/layout.tsx:`));
434
+ console.log("");
435
+ console.log(snippet([`<body data-ds="${slug}">{children}</body>`]));
436
+ console.log("");
437
+ console.log(body(`(If you haven't installed the system yet, run: synthesisui add ${slug})`));
388
438
  }
389
- console.log(body(`1. Import the design system in your GLOBAL stylesheet, e.g. app/globals.css`));
390
- console.log(body(` (the path is relative to that file - hence the leading ../):`));
391
- console.log("");
392
- console.log(snippet(imports));
393
- console.log("");
394
- console.log(body(`2. Scope your app: add data-ds="${slug}" to a ROOT element, e.g. app/layout.tsx:`));
395
- console.log("");
396
- console.log(snippet([`<body data-ds="${slug}">{children}</body>`]));
397
- console.log("");
398
- console.log(body(`(If you haven't installed the system yet, run: synthesisui add ${slug})`));
399
439
  console.log(section("Use it"));
400
440
  if (!opts.artifactsOnly && config.target === "next") {
401
441
  /**
@@ -22,6 +22,7 @@ import { groupRole } from "../group-role.js";
22
22
  import { actingSlug, describeScope, measuredScope, scopePaths, } from "../measured-scope.js";
23
23
  import { body, paint, section, snippet } from "../output.js";
24
24
  import { setupPrompt } from "../setup-prompt.js";
25
+ import { installedThemeCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js";
25
26
  import { resolveDeps } from "../stack.js";
26
27
  import { danglingTheirVars } from "../their-vars.js";
27
28
  /**
@@ -782,6 +783,14 @@ export async function doctor(opts) {
782
783
  * Nobody debugs from 0%. They conclude the product does not work.
783
784
  */
784
785
  const wiring = await readWiring(root, table.slug);
786
+ /**
787
+ * O QUE, NESTE REPOSITÓRIO, SÓ A NOSSA FOLHA RESOLVE - acumulado na varredura QUE JÁ ACONTECE.
788
+ *
789
+ * Uma segunda passada pelo disco para responder isto custaria o dobro num repositório de 178
790
+ * arquivos, e a resposta está no mesmo texto que o scan já tem na mão. Ver `sheet-needed.ts`.
791
+ */
792
+ const themeCss = table.slug ? await installedThemeCss(root, table.slug) : "";
793
+ let scannedSource = "";
785
794
  const skippedProjects = [];
786
795
  const tally = emptyTally();
787
796
  const internalSpecs = await internalSpecifiers(root);
@@ -793,6 +802,9 @@ export async function doctor(opts) {
793
802
  continue;
794
803
  const rel = relative(root, file);
795
804
  reports.push(scanSource(rel, src, table));
805
+ /** A pasta que a plataforma escreve não conta: ela É a folha, não um consumidor dela. */
806
+ if (!rel.startsWith("_synthesisui"))
807
+ scannedSource += `\n${src}`;
796
808
  // Composition, alongside values: the contract check needs to know which
797
809
  // elements this file writes and with what options. Stories and tests are
798
810
  // excluded for the reason they are excluded everywhere else - they compose
@@ -967,7 +979,22 @@ export async function doctor(opts) {
967
979
  * `table.source` é a distinção certa e ela já estava aqui, uma linha abaixo, em `unwired`:
968
980
  * `"yours"` e `"adopted"` são o vocabulário dela, e não há duas linhas para ligar.
969
981
  */
970
- const blocked = table.source === "installed" && (!wiring.imported || !wiring.scoped);
982
+ const sheetNeed = whatOnlyTheSheetResolves({
983
+ source: scannedSource,
984
+ themeCss,
985
+ theirNames: installed.theirs.byName.keys(),
986
+ });
987
+ /**
988
+ * BLOQUEADO SÓ QUANDO A FOLHA É NECESSÁRIA - a mesma pergunta que decide `unwired`.
989
+ *
990
+ * Sem esta condição, o comando parava de cobrar a fiação lá em cima e continuava recusando o
991
+ * `--fix` aqui embaixo, fechando com *"esse número fica acionável no momento em que as duas forem
992
+ * verdade"* - duas condições que ele mesmo tinha acabado de deixar de pedir. Um relatório que
993
+ * retira a exigência numa seção e a mantém na outra ensina que nenhuma das duas é para valer.
994
+ */
995
+ const blocked = table.source === "installed" &&
996
+ sheetNeed.needed &&
997
+ (!wiring.imported || !wiring.scoped);
971
998
  /**
972
999
  * QUANTAS DAS DUAS FALTAM - e sem este número três frases mandavam consertar DUAS coisas quando
973
1000
  * faltava UMA.
@@ -983,12 +1010,55 @@ export async function doctor(opts) {
983
1010
  */
984
1011
  const missingWiring = (wiring.imported ? 0 : 1) + (wiring.scoped ? 0 : 1);
985
1012
  const bothMissing = missingWiring === 2;
1013
+ /**
1014
+ * A FIAÇÃO SÓ É COBRADA QUANDO ALGUMA COISA NESTE REPOSITÓRIO PRECISA DELA - ver `sheet-needed.ts`.
1015
+ *
1016
+ * O DEFEITO, apontado pelo dono em 07/09 sobre o próprio repositório: *"por que a gente precisa
1017
+ * desses imports, visto que o design do projeto dele já funciona? criar esse import gera
1018
+ * dependência ao design system, que é coisa que a gente não quer - a gente quer isolar os dois
1019
+ * pontos, sendo que o nosso deve ser apenas uma REFERÊNCIA para que qualquer agente que gera
1020
+ * código saiba como utilizar o design system"*.
1021
+ *
1022
+ * MEDIDO NO `codelevel` no mesmo dia: no repositório inteiro, apenas dois arquivos mencionam
1023
+ * `--ds-*` - um componente que a PLATAFORMA gerou com uma versão anterior à tradução, e a costura
1024
+ * de tipografia que o nosso próprio setup pediu. Zero linhas do código que ELE escreveu. Regerado
1025
+ * com o CLI de hoje, o componente sai com 0 referências à nossa folha, e as duas animações que ele
1026
+ * veste são declaradas pelo `globals.css` dele.
1027
+ *
1028
+ * Ou seja: o comando cobrava uma dependência de runtime que nada naquele repositório usava - e o
1029
+ * produto já promete o contrário em `their-tongue.ts` (*"nenhuma folha nossa precisa existir no
1030
+ * repositório dele"*). As duas metades discordavam porque nenhuma perguntava.
1031
+ *
1032
+ * O ESCOPO (`data-ds`) SEGUE A MESMA REGRA e pelo mesmo motivo: ele existe para a folha aplicar
1033
+ * dentro dele. Sem folha necessária, um atributo na árvore dele é a mesma dependência com outro
1034
+ * nome.
1035
+ *
1036
+ * E QUANDO ALGO PRECISA, tudo continua exatamente como era - inclusive o roteiro para o agente. A
1037
+ * mudança não é deixar de cobrar: é parar de cobrar sem ter perguntado.
1038
+ */
986
1039
  const unwired = table.source === "installed" &&
1040
+ sheetNeed.needed &&
987
1041
  (!wiring.imported || !wiring.scoped || fontsPending);
988
1042
  if (unwired) {
989
1043
  console.log("");
990
1044
  console.log(body(`${table.name ?? table.slug} is installed - but this project does not load it yet.`));
991
1045
  console.log("");
1046
+ /**
1047
+ * O QUE ESTÁ COBRANDO A FOLHA, POR NOME - senão a exigência lê como uma regra nossa.
1048
+ *
1049
+ * Ela não é: é um efeito do que ESTE repositório escreveu. Medido no `codelevel` (07/09), as
1050
+ * duas coisas que a exigiam eram um componente gerado por uma versão anterior à tradução do CLI
1051
+ * e a costura de tipografia que o nosso próprio setup pediu - nenhuma linha do código dele.
1052
+ * Regerado com o CLI de hoje, o mesmo componente sai com zero referências e a exigência some.
1053
+ *
1054
+ * Sem esta linha, quem lê não tem como saber que a dependência é REMOVÍVEL, e a única saída
1055
+ * visível é aceitar o import.
1056
+ */
1057
+ const asks = sheetNeed.variables.length > 0
1058
+ ? `${sheetNeed.variables.slice(0, 3).join(", ")}${sheetNeed.variables.length > 3 ? `, +${sheetNeed.variables.length - 3} more` : ""} - re-run \`component\` on whatever writes ${sheetNeed.variables.length === 1 ? "it" : "them"} and it speaks your own names instead`
1059
+ : `the ${sheetNeed.classes.slice(0, 3).join(", ")}${sheetNeed.classes.length > 3 ? `, +${sheetNeed.classes.length - 3} more` : ""} ${sheetNeed.classes.length === 1 ? "utility" : "utilities"}, which only this system's @theme generates`;
1060
+ console.log(body(` what asks for it: ${asks}`));
1061
+ console.log("");
992
1062
  console.log(body(wiring.imported
993
1063
  ? ` ✓ some stylesheet imports _synthesisui/ds/${table.slug}/tokens.css`
994
1064
  : ` ✗ no stylesheet imports _synthesisui/ds/${table.slug}/tokens.css`));
@@ -0,0 +1,76 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { TAILWIND_THEME_VARS } from "./tailwind-theme-vars.js";
4
+ /** `--animate-shimmer` no `@theme` -> o nome `shimmer`, que é o que uma classe carrega. */
5
+ function themeNames(themeCss) {
6
+ const out = new Set();
7
+ for (const block of themeCss.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g))
8
+ for (const m of block[1].matchAll(/^\s*--([a-z]+)-([a-zA-Z0-9_-]+)\s*:/gm))
9
+ out.add(m[2]);
10
+ return [...out];
11
+ }
12
+ /** As classes escritas no código - `hover:animate-shimmer` conta como `animate-shimmer`. */
13
+ function classesIn(source) {
14
+ const out = new Set();
15
+ for (const m of source.matchAll(/class(?:Name)?=(?:"([^"]*)"|'([^']*)'|\{`([^`]*)`\}|\{"([^"]*)"\})/g)) {
16
+ const body = m[1] ?? m[2] ?? m[3] ?? m[4] ?? "";
17
+ for (const raw of body.split(/\s+/)) {
18
+ if (!raw)
19
+ continue;
20
+ const bare = raw.split(":").pop() ?? raw;
21
+ out.add(bare.replace(/^[!-]/, ""));
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+ /**
27
+ * O QUE, NESTE TEXTO, SÓ A FOLHA RESOLVE.
28
+ *
29
+ * Puro de propósito: quem varre o disco é quem chama, e um spec precisa poder afirmar a regra sem
30
+ * um diretório temporário.
31
+ */
32
+ export function whatOnlyTheSheetResolves(input) {
33
+ const variables = [
34
+ ...new Set([...input.source.matchAll(/var\(\s*(--ds-[a-zA-Z0-9_-]+)/g)].map((m) => m[1])),
35
+ ];
36
+ /**
37
+ * `--animate-shimmer` declarado por ele -> o nome `shimmer` sai da conta.
38
+ *
39
+ * E O QUE O PRÓPRIO TAILWIND DECLARA sai junto, pela mesma razão e pela lista que já existe:
40
+ * `animate-pulse` existe em qualquer projeto que importe o Tailwind, e acusá-la de exigir a nossa
41
+ * folha é o mesmo aviso falso que `TAILWIND_THEME_VARS` foi escrito para acabar. Medido no
42
+ * `codelevel` (07/09): sem esta linha, `animate-pulse` era a ÚNICA coisa sustentando a cobrança
43
+ * do import naquele repositório.
44
+ */
45
+ const theirs = new Set();
46
+ for (const name of [...TAILWIND_THEME_VARS, ...(input.theirNames ?? [])]) {
47
+ const m = /^--[a-z]+-(.+)$/.exec(name.toLowerCase());
48
+ if (m)
49
+ theirs.add(m[1]);
50
+ }
51
+ const names = themeNames(input.themeCss).filter((n) => !theirs.has(n));
52
+ const written = classesIn(input.source);
53
+ const classes = [
54
+ ...new Set([...written].filter((c) => names.some((n) => c === n || c.endsWith(`-${n}`)))),
55
+ ];
56
+ return {
57
+ variables,
58
+ classes,
59
+ needed: variables.length > 0 || classes.length > 0,
60
+ };
61
+ }
62
+ /** O `theme.css` da versão instalada - a folha da raiz é um re-export de uma linha. */
63
+ export async function installedThemeCss(root, slug) {
64
+ const dir = join(root, "_synthesisui", "ds", slug);
65
+ const lock = await readFile(join(dir, ".lock"), "utf8").catch(() => null);
66
+ let version = 0;
67
+ if (lock) {
68
+ try {
69
+ version = JSON.parse(lock).version ?? 0;
70
+ }
71
+ catch {
72
+ version = 0;
73
+ }
74
+ }
75
+ return readFile(join(dir, `v${version}`, "theme.css"), "utf8").catch(() => "");
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.396",
3
+ "version": "0.16.397",
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": {