synthesisui 0.16.432 → 0.16.434

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.
@@ -56,6 +56,7 @@ import { consumersOf, outsideScope, } from "../outside-scope.js";
56
56
  import { phase, startProgress } from "../progress.js";
57
57
  import { repoStateOf } from "../repo-state.js";
58
58
  import { relatorioHtml, secoesDe } from "../report-html.js";
59
+ import { abertura, aneis, historiaDeCor, oQueMuda } from "../report-insight.js";
59
60
  import { runtimeDeclaredVars, runtimeDeclaredVarsIn } from "../runtime-vars.js";
60
61
  import { detectStack, resolveDeps, stackVersions } from "../stack.js";
61
62
  import { desligarCaptura, ligarCaptura } from "../transcript.js";
@@ -3778,6 +3779,20 @@ async function mostrarInspecao(c, root, transcrito) {
3778
3779
  },
3779
3780
  ],
3780
3781
  secoes,
3782
+ /**
3783
+ * O ESPELHO (dono, 18/09): a pagina abre com a MESMA regua que o produto usa - cobertura de
3784
+ * token, o vocabulario dela, e o quanto a leitura entendeu. Nenhum numero novo: os tres saem
3785
+ * de campos que a medicao ja produz.
3786
+ */
3787
+ abertura: abertura(c, c.measured?.repo ?? basename(root)),
3788
+ aneis: aneis(c),
3789
+ cor: historiaDeCor(c),
3790
+ /**
3791
+ * O COMANDO NO TOPO E NO FIM (dono, 18/09): quem decidiu adotar no meio da leitura nao
3792
+ * deveria ter que voltar ao terminal para descobrir o que digitar.
3793
+ */
3794
+ comando: "npx synthesisui@latest import",
3795
+ muda: oQueMuda(c),
3781
3796
  });
3782
3797
  console.log(section("What came out of your files"));
3783
3798
  console.log(body(`${paint.strong(`${lidos} of ${total}`)} components came out with a blueprint${pct === null
@@ -82,37 +82,113 @@ ${layout()}
82
82
  <p class="kicker">Read by SynthesisUI</p>
83
83
  <p class="bar-repo">${escapar(cabecalho.repo)}</p>
84
84
  </div>
85
- <p class="meta">${escapar(quando)} &middot; CLI ${escapar(cabecalho.cli)} &middot; nothing left this machine</p>
85
+ <div class="bar-fim">
86
+ <p class="meta">${escapar(quando)} &middot; CLI ${escapar(cabecalho.cli)} &middot; nothing left this machine</p>${input.comando
87
+ ? `
88
+ <code class="comando">${escapar(input.comando)}</code>`
89
+ : ""}
90
+ </div>
86
91
  </div>
87
92
  </header>
88
93
 
89
- <main class="wrap">
94
+ <div class="wrap">
95
+ <nav class="lado">
96
+ <p class="kicker">What is in here</p>
97
+ <ol>
98
+ ${indice}
99
+ </ol>
100
+ </nav>
101
+
102
+ <main class="fluxo">
90
103
  <section class="hero chapter" style="--ordem:0">
91
104
  <h1>${escapar(cabecalho.repo)}</h1>
92
- <p class="lede">This is what your own code says about itself.</p>
105
+ <p class="lede">${escapar(input.abertura ?? "This is what your own code says about itself.")}</p>
93
106
  <div class="stats">
94
107
  ${numeros}
95
108
  </div>
109
+ ${desenharAneis(input.aneis ?? [])}
110
+ ${desenharCor(input.cor)}
96
111
  </section>
97
112
 
98
- <nav class="chapter" style="--ordem:1">
99
- <p class="kicker">What is in here</p>
100
- <ol>
101
- ${indice}
102
- </ol>
103
- </nav>
104
-
105
113
  ${corpo}
106
114
 
115
+ ${desenharMuda(input.muda ?? [], input.comando)}
116
+
107
117
  <footer>
108
118
  <p>Dressed by Halogen v${HALOGEN_VERSION}, a design system from the SynthesisUI gallery.</p>
109
119
  <p>This file lives in your project and was never sent anywhere. Re-run the command to refresh it.</p>
110
120
  </footer>
111
121
  </main>
122
+ </div>
112
123
  </body>
113
124
  </html>
114
125
  `;
115
126
  }
127
+ /**
128
+ * A ÚLTIMA COISA QUE ELA LÊ: a conta, e não mais um inventário.
129
+ *
130
+ * Um relatório que termina na décima sexta lista deixa a pessoa com o diagnóstico e sem o passo.
131
+ * Este capítulo diz o que muda COM OS NÚMEROS DELA e entrega o comando ao lado - a mesma linha que
132
+ * já está no topo, porque quem chegou até aqui não deveria ter que subir para achá-la.
133
+ */
134
+ function desenharMuda(linhas, comando) {
135
+ if (linhas.length === 0)
136
+ return "";
137
+ return ` <section class="chapter muda" style="--ordem:12">
138
+ <h2><span>What changes if you bring it in</span></h2>
139
+ ${linhas.map((l) => ` <p class="callout">${escapar(l)}</p>`).join("\n")}${comando
140
+ ? `
141
+ <code class="comando grande">${escapar(comando)}</code>`
142
+ : ""}
143
+ </section>`;
144
+ }
145
+ /**
146
+ * OS TRÊS ANÉIS DA NOSSA LEITURA DE TOKEN (pedido dele, 18/09) - e eles abrem a página porque são
147
+ * a régua do produto: quanto já vem de token, quanto ela declara, e quanto a leitura entendeu.
148
+ *
149
+ * O anel é um `conic-gradient` e nada mais: sem biblioteca, sem `<script>`, e sem uma requisição.
150
+ * A fatia é a proporção MEDIDA - e um anel só existe onde há denominador, porque desenhar a fatia
151
+ * de um total que ninguém contou é inventar a proporção.
152
+ */
153
+ function desenharAneis(aneis) {
154
+ if (aneis.length === 0)
155
+ return "";
156
+ const cartoes = aneis
157
+ .map((a) => {
158
+ const pct = a.de > 0 ? Math.round((a.parte / a.de) * 100) : 0;
159
+ return ` <div class="anel-cartao">
160
+ <div class="anel" style="--fatia:${pct}%" role="img" aria-label="${escapar(`${a.titulo}: ${a.numero}`)}">
161
+ <b>${escapar(a.numero)}</b>
162
+ </div>
163
+ <div class="anel-texto">
164
+ <h3>${escapar(a.titulo)}</h3>
165
+ <p>${escapar(a.frase)}</p>
166
+ </div>
167
+ </div>`;
168
+ })
169
+ .join("\n");
170
+ return ` <section class="aneis chapter" style="--ordem:1">
171
+ ${cartoes}
172
+ </section>`;
173
+ }
174
+ /**
175
+ * A HISTÓRIA DAS CORES, com a paleta desenhada do lado - as cores DELA, nos valores dela.
176
+ *
177
+ * Os quadradinhos não são enfeite: uma frase sobre quarenta e sete cores colidindo é abstrata até
178
+ * a pessoa VER que duas casas ao lado da outra são a mesma cor.
179
+ */
180
+ function desenharCor(cor) {
181
+ if (!cor || cor.linhas.length === 0)
182
+ return "";
183
+ const amostras = cor.paleta
184
+ .map((c) => `<i style="--amostra:${escapar(c)}" title="${escapar(c)}"></i>`)
185
+ .join("");
186
+ return ` <section class="chapter cor" style="--ordem:2">
187
+ <h2><span>Your colours</span></h2>
188
+ ${cor.linhas.map((l) => ` <p class="callout">${escapar(l)}</p>`).join("\n")}
189
+ <div class="paleta" aria-hidden="true">${amostras}</div>
190
+ </section>`;
191
+ }
116
192
  /**
117
193
  * O NÚMERO COM A FATIA DESENHADA - e a barra só aparece quando existe denominador medido.
118
194
  *
@@ -150,13 +226,16 @@ function desenharCorpo(s) {
150
226
  const familias = agrupar(resto);
151
227
  const corpo = familias
152
228
  .map((f) => f.tipo === "tabela"
153
- ? ` <details class="familia" open>
229
+ ? ` <details class="familia">
154
230
  <summary><span>${escapar(f.rotulo)}</span> <b>${f.itens.length}</b></summary>
155
231
  <ul class="grade">
156
232
  ${f.itens.map((i) => ` <li>${escapar(i)}</li>`).join("\n")}
157
233
  </ul>
158
234
  </details>`
159
- : ` <pre>${escapar(f.itens.join("\n"))}</pre>`)
235
+ : ` <details class="tecnico">
236
+ <summary>${f.itens.filter((l) => l.trim()).length} lines of detail</summary>
237
+ <pre>${escapar(f.itens.join("\n"))}</pre>
238
+ </details>`)
160
239
  .join("\n");
161
240
  return destaque
162
241
  ? ` <p class="callout">${escapar(destaque)}</p>\n${corpo}`
@@ -295,7 +374,8 @@ function layout() {
295
374
  const corpo = `var(${FAMILY_SEAM_PREFIX}body, ui-sans-serif, system-ui, sans-serif)`;
296
375
  const mono = `var(${FAMILY_SEAM_PREFIX}mono, ui-monospace, monospace)`;
297
376
  const display = `var(${FAMILY_SEAM_PREFIX}display, inherit)`;
298
- return `body {
377
+ return `html { scroll-behavior: smooth; }
378
+ body {
299
379
  margin: 0;
300
380
  background: var(--ds-color-semantic-canvas);
301
381
  color: var(--ds-color-semantic-foreground);
@@ -312,8 +392,11 @@ function layout() {
312
392
  background: var(--ds-color-semantic-raised, var(--ds-color-semantic-surface));
313
393
  border-bottom: 1px solid var(--ds-color-semantic-border);
314
394
  }
395
+ /* A BARRA ALINHA COM A COLUNA DO CONTEUDO: mesma largura maxima, mesma margem lateral. Antes ela
396
+ media 920px enquanto o corpo media 1200, entao o titulo do topo e o titulo da pagina comecavam em
397
+ colunas diferentes - a primeira coisa que o olho nota, e a mais facil de consertar. */
315
398
  .bar-in {
316
- max-width: 920px;
399
+ max-width: 1200px;
317
400
  margin-inline: auto;
318
401
  padding: 11px 24px;
319
402
  display: flex;
@@ -323,9 +406,48 @@ function layout() {
323
406
  justify-content: space-between;
324
407
  }
325
408
  .bar-repo { margin: 2px 0 0; font-weight: 600; }
409
+ .bar-fim { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
410
+
411
+ /* O COMANDO NO TOPO, sem rolar: quem decidiu adotar no meio da leitura nao volta para procura-lo. */
412
+ .comando {
413
+ font-family: ${mono};
414
+ font-size: 12px;
415
+ padding: 6px 12px;
416
+ border-radius: var(--ds-radius-sm, 8px);
417
+ border: 1px solid var(--ds-color-semantic-border);
418
+ background: var(--ds-color-semantic-canvas);
419
+ color: var(--ds-color-semantic-foreground);
420
+ user-select: all;
421
+ }
422
+ .comando.grande {
423
+ display: inline-block;
424
+ margin-top: 14px;
425
+ font-size: 14px;
426
+ padding: 12px 18px;
427
+ }
326
428
  .bar-id { min-width: 0; }
327
429
 
328
- .wrap { max-width: 920px; margin-inline: auto; padding: 0 24px 96px; }
430
+ .wrap {
431
+ max-width: 1200px;
432
+ margin-inline: auto;
433
+ padding: 0 24px 96px;
434
+ display: grid;
435
+ grid-template-columns: 230px minmax(0, 1fr);
436
+ gap: 40px;
437
+ align-items: start;
438
+ }
439
+ .fluxo { min-width: 0; }
440
+
441
+ /* A LATERAL SEGURA A ROLAGEM INTEIRA: quem esta no capitulo doze pula para o tres sem subir, que
442
+ era o que a lista no topo obrigava. Em tela estreita ela vira topo - uma coluna de 250px num
443
+ celular come metade da leitura. */
444
+ .lado {
445
+ position: sticky;
446
+ top: 76px;
447
+ max-height: calc(100vh - 100px);
448
+ overflow: auto;
449
+ padding: 20px 0 0;
450
+ }
329
451
 
330
452
  .kicker {
331
453
  margin: 0;
@@ -378,7 +500,7 @@ h1 {
378
500
 
379
501
  nav { margin: 44px 0 8px; }
380
502
  nav ol {
381
- margin: 12px 0 0;
503
+ margin: 10px 0 0;
382
504
  padding: 0;
383
505
  list-style: none;
384
506
  counter-reset: capitulo;
@@ -390,10 +512,21 @@ nav li {
390
512
  counter-increment: capitulo;
391
513
  display: flex;
392
514
  align-items: baseline;
393
- gap: 12px;
394
- padding: 7px 10px;
515
+ gap: 10px;
516
+ padding: 5px 9px;
517
+ font-size: 13px;
518
+ line-height: 1.35;
519
+ border: 1px solid transparent;
395
520
  border-radius: var(--ds-radius-sm, 8px);
396
521
  }
522
+ /* O ITEM CLICADO FICA MARCADO: sem isto a lateral e uma lista de dezesseis iguais, e a pessoa perde
523
+ de onde veio no instante em que rola. O foco sobrevive ao clique, entao a marca tambem. */
524
+ nav li:has(a:focus),
525
+ nav li:has(a:focus-visible) {
526
+ border-color: var(--ds-color-semantic-accent);
527
+ background: var(--ds-color-semantic-surface);
528
+ }
529
+ nav li:has(a:focus) a { color: var(--ds-color-semantic-accent); }
397
530
  nav li:hover { background: var(--ds-color-semantic-surface); }
398
531
  nav li::before {
399
532
  content: counter(capitulo);
@@ -501,7 +634,66 @@ footer { margin-top: 40px; color: var(--ds-color-semantic-muted); font-size: 13p
501
634
  .chapter { animation: entrar 620ms cubic-bezier(0.22, 1, 0.36, 1) both; animation-delay: calc(var(--ordem, 0) * 55ms); }
502
635
 
503
636
  @media (prefers-reduced-motion: reduce) {
504
- .chapter, .bar-track i { animation: none; }
637
+ html { scroll-behavior: auto; }
638
+ .chapter, .bar-track i, .anel { animation: none; }
639
+ }
640
+
641
+ /* OS ANEIS: um gradiente conico e mais nada - sem biblioteca e sem script, que e o contrato. */
642
+ .aneis {
643
+ display: grid;
644
+ gap: 18px;
645
+ margin-top: 16px;
646
+ }
647
+ .anel-cartao { display: flex; gap: 18px; align-items: center; }
648
+ .anel {
649
+ flex: 0 0 auto;
650
+ width: 84px;
651
+ height: 84px;
652
+ border-radius: 50%;
653
+ display: grid;
654
+ place-items: center;
655
+ background: conic-gradient(
656
+ var(--ds-color-semantic-accent) var(--fatia),
657
+ var(--ds-color-semantic-overlay, var(--ds-color-semantic-border)) 0
658
+ );
659
+ animation: girar 900ms cubic-bezier(0.22, 1, 0.36, 1) both;
660
+ }
661
+ .anel b {
662
+ width: 62px;
663
+ height: 62px;
664
+ border-radius: 50%;
665
+ background: var(--ds-color-semantic-surface);
666
+ display: grid;
667
+ place-items: center;
668
+ font-size: 15px;
669
+ font-variant-numeric: tabular-nums;
670
+ }
671
+ .anel-texto h3 { margin: 0 0 4px; font-size: 15px; }
672
+ .anel-texto p { margin: 0; color: var(--ds-color-semantic-muted); font-size: 14px; }
673
+
674
+ .paleta { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 14px; }
675
+ .paleta i {
676
+ width: 30px;
677
+ height: 30px;
678
+ border-radius: var(--ds-radius-sm, 8px);
679
+ background: var(--amostra);
680
+ border: 1px solid var(--ds-color-semantic-border);
681
+ }
682
+
683
+ /* O DETALHE TECNICO NASCE FECHADO: ele e a evidencia, nao a conclusao. */
684
+ .tecnico { margin: 0 0 12px; }
685
+ .tecnico summary {
686
+ cursor: pointer;
687
+ font-size: 13px;
688
+ color: var(--ds-color-semantic-faint);
689
+ }
690
+ .tecnico pre { margin-top: 10px; }
691
+
692
+ @keyframes girar { from { --fatia: 0%; } }
693
+
694
+ @media (max-width: 900px) {
695
+ .wrap { grid-template-columns: minmax(0, 1fr); gap: 0; }
696
+ .lado { position: static; max-height: none; padding-bottom: 20px; }
505
697
  }
506
698
 
507
699
  @media print {
@@ -0,0 +1,171 @@
1
+ import { deltaE, isNearDuplicate } from "./doctor/color-distance.js";
2
+ /**
3
+ * OS TRÊS ANÉIS QUE ABREM A PÁGINA (pedido dele, 18/09) - e são os três da nossa própria leitura de
4
+ * token: cobertura, quanto ela declara, e consistência.
5
+ *
6
+ * Um anel só nasce quando existe denominador MEDIDO. Sem ele o número fica sozinho: desenhar uma
7
+ * fatia sobre um total que ninguém contou é inventar a proporção, que é o defeito que a barra da
8
+ * plataforma já cometeu uma vez.
9
+ */
10
+ export function aneis(c) {
11
+ const saida = [];
12
+ const observados = c.observed ?? [];
13
+ if (observados.length > 0) {
14
+ const comNome = observados.filter((v) => v.token).length;
15
+ saida.push({
16
+ titulo: "From your tokens",
17
+ numero: `${pct(comNome, observados.length)}%`,
18
+ parte: comNome,
19
+ de: observados.length,
20
+ frase: `${comNome} of the ${observados.length} design values in your code already have a name in your own system. The other ${observados.length - comNome} are written by hand, so changing one means finding every copy.`,
21
+ });
22
+ }
23
+ const declarados = Object.keys(c.declared ?? {}).length;
24
+ if (declarados > 0 && observados.length > 0) {
25
+ const familias = porFamilia(observados);
26
+ saida.push({
27
+ titulo: "Your vocabulary",
28
+ numero: String(declarados),
29
+ parte: Math.min(declarados, observados.length),
30
+ de: observados.length,
31
+ frase: `You declare ${declarados} tokens today${familias ? `, and the values they cover are ${familias}` : ""}. These are your names, and they travel unchanged.`,
32
+ });
33
+ }
34
+ const cl = c.values?.classes;
35
+ if (cl && cl.seen > cl.structure) {
36
+ const decisoes = cl.seen - cl.structure;
37
+ saida.push({
38
+ titulo: "What we could read",
39
+ numero: `${pct(cl.interpreted, decisoes)}%`,
40
+ parte: cl.interpreted,
41
+ de: decisoes,
42
+ frase: `Of the ${decisoes} style decisions in your components, we understood ${cl.interpreted}. The remaining ${decisoes - cl.interpreted} are shapes this version does not read yet - they are named in the chapters below, with the file and line.`,
43
+ });
44
+ }
45
+ return saida;
46
+ }
47
+ /** As famílias com valor, em palavras: "456 colour, 210 spacing". Vazio quando não há o que dizer. */
48
+ function porFamilia(observados) {
49
+ const nomes = {
50
+ color: "colour",
51
+ spacing: "spacing",
52
+ radius: "radius",
53
+ font: "type",
54
+ motion: "motion",
55
+ };
56
+ const conta = new Map();
57
+ for (const v of observados)
58
+ conta.set(v.kind, (conta.get(v.kind) ?? 0) + 1);
59
+ const partes = [...conta.entries()]
60
+ .sort((a, b) => b[1] - a[1])
61
+ .map(([k, n]) => `${n} ${nomes[k] ?? k}`);
62
+ return partes.join(", ");
63
+ }
64
+ function pct(parte, de) {
65
+ if (de <= 0)
66
+ return 0;
67
+ return Math.max(0, Math.min(100, Math.round((parte / de) * 100)));
68
+ }
69
+ /**
70
+ * O PARÁGRAFO DE ABERTURA - o que foi encontrado, em linguagem de quem não abriu o código.
71
+ *
72
+ * Montado dos NÚMEROS, nunca de um modelo: ele muda quando o repositório muda, e é verdade em
73
+ * qualquer projeto - inclusive num que ninguém aqui jamais viu. Um texto fixo com números colados
74
+ * seria a mesma frase mentindo em metade dos casos.
75
+ */
76
+ export function abertura(c, repo) {
77
+ const observados = c.observed ?? [];
78
+ const comNome = observados.filter((v) => v.token).length;
79
+ const total = c.coverage?.components ?? 0;
80
+ const frases = [];
81
+ if (total > 0)
82
+ frases.push(`We read ${total} component${total === 1 ? "" : "s"} in ${repo} and wrote down how each one is built.`);
83
+ if (observados.length > 0)
84
+ frases.push(comNome === 0
85
+ ? `Every one of the ${observados.length} design values we found is written by hand - there is no token behind any of them yet.`
86
+ : `Of the ${observados.length} design values we found, ${comNome} already come from a token you declare, and ${observados.length - comNome} are still written by hand.`);
87
+ frases.push("Nothing below is a rule we brought. It is what your own files already do, counted.");
88
+ return frases.join(" ");
89
+ }
90
+ /**
91
+ * A HISTÓRIA DAS CORES - a paleta, as que colidem, e o contraste do que dá para checar.
92
+ *
93
+ * COLISÃO NÃO É ERRO, e a frase diz isso: duas cores a menos de um passo perceptível uma da outra
94
+ * são indistinguíveis na tela, então manter as duas custa uma decisão e não entrega nenhuma. Quem
95
+ * decide se elas viram uma é ela.
96
+ *
97
+ * O CONTRASTE É A ÚNICA COISA AQUI COM VEREDITO, porque `4.5:1` não é régua nossa sobre o gosto
98
+ * dela - é o padrão publicado sobre o texto ser legível. E o relatório diz QUANTOS pares conseguiu
99
+ * checar: onde a leitura não enxerga frente e fundo juntos, ele declara a lacuna em vez de dar uma
100
+ * nota que não mediu.
101
+ */
102
+ export function historiaDeCor(c, pares = []) {
103
+ const cores = (c.observed ?? [])
104
+ .filter((v) => v.kind === "color")
105
+ .sort((a, b) => b.count - a.count);
106
+ if (cores.length === 0)
107
+ return { linhas: [], paleta: [] };
108
+ const linhas = [];
109
+ const comNome = cores.filter((v) => v.token).length;
110
+ linhas.push(`You paint with ${cores.length} distinct colours, and ${comNome} of them already have a name in your system.`);
111
+ const colisoes = colidem(cores.map((v) => v.value));
112
+ if (colisoes > 0)
113
+ linhas.push(`${colisoes} of them sit within one perceptible step of another colour you also use - close enough that the eye reads them as the same. Keeping both costs a decision and delivers no difference; whether they become one is yours to say.`);
114
+ else
115
+ linhas.push("No two of them are close enough to be mistaken for each other - every colour you keep is a colour someone can tell apart.");
116
+ if (pares.length === 0) {
117
+ linhas.push("Contrast was not checked: nothing in the files puts a text colour and a background together in a way this reading can see. It needs both sides of a pair, and that only shows up once components are imported.");
118
+ }
119
+ else {
120
+ const baixos = pares.filter((p) => p.razao < 4.5);
121
+ linhas.push(baixos.length === 0
122
+ ? `All ${pares.length} text-on-background pairs we could check reach 4.5:1, the published floor for body text.`
123
+ : `${baixos.length} of the ${pares.length} text-on-background pairs we could check fall below 4.5:1, the published floor for body text - the lowest is ${baixos[0].razao.toFixed(2)}:1. Below that, people with low vision stop being able to read it.`);
124
+ }
125
+ return { linhas, paleta: cores.slice(0, 24).map((v) => v.value) };
126
+ }
127
+ /** Quantas cores têm ao menos uma vizinha indistinguível. Conta CORES, nunca pares. */
128
+ function colidem(valores) {
129
+ const hex = valores.filter((v) => /^#[0-9a-fA-F]{3,8}$/.test(v));
130
+ let n = 0;
131
+ for (let i = 0; i < hex.length; i += 1) {
132
+ for (let j = 0; j < hex.length; j += 1) {
133
+ if (i === j)
134
+ continue;
135
+ if (isNearDuplicate(hex[i], hex[j]) ||
136
+ (deltaE(hex[i], hex[j]) ?? 99) < 2) {
137
+ n += 1;
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ return n;
143
+ }
144
+ /**
145
+ * O QUE MUDA SE ELA ADOTAR - com os números DELA, e sem uma promessa que a medição não sustente.
146
+ *
147
+ * O QUE O CLIENTE GANHA: a última coisa que ela lê não é mais um inventário, é a conta. Quantos dos
148
+ * valores que hoje ela repete à mão passam a ter um nome, e por que o problema para de voltar.
149
+ *
150
+ * E ELE NÃO PROMETE O QUE NÃO MEDIU: o número aqui é o que está escrito à mão HOJE, no repositório
151
+ * dela. Um texto que dissesse "seu design fica consistente" seria a mesma frase para todo mundo, e
152
+ * portanto verdade para ninguém.
153
+ */
154
+ export function oQueMuda(c) {
155
+ const observados = c.observed ?? [];
156
+ if (observados.length === 0)
157
+ return [];
158
+ const aMao = observados.filter((v) => !v.token);
159
+ if (aMao.length === 0)
160
+ return [
161
+ "Every design value in your code already comes from a token you declare. What SynthesisUI adds here is keeping it that way: the check runs after every file your agent writes, and a value that drifts off your own scale comes back to you as a decision instead of landing silently.",
162
+ ];
163
+ const repetidos = aMao.filter((v) => v.count > 1);
164
+ const usos = repetidos.reduce((n, v) => n + v.count, 0);
165
+ const linhas = [];
166
+ linhas.push(`${aMao.length} of your design values are written by hand today. Importing gives each one a name in your own vocabulary, so changing it later means changing it once instead of finding every copy.`);
167
+ if (repetidos.length > 0)
168
+ linhas.push(`${repetidos.length} of those are repeated - ${usos} uses across your files. Every copy is a place the next change can be forgotten, which is how two screens end up almost the same colour.`);
169
+ linhas.push("And it stops coming back: once the system is in the repository, the check runs after every file your agent writes and catches a value that drifts off your own scale, before it ships.");
170
+ return linhas;
171
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.432",
3
+ "version": "0.16.434",
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": {