synthesisui 0.16.432 → 0.16.433

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 } 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,14 @@ 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),
3781
3790
  });
3782
3791
  console.log(section("What came out of your files"));
3783
3792
  console.log(body(`${paint.strong(`${lidos} of ${total}`)} components came out with a blueprint${pct === null
@@ -86,22 +86,25 @@ ${layout()}
86
86
  </div>
87
87
  </header>
88
88
 
89
- <main class="wrap">
89
+ <div class="wrap">
90
+ <nav class="lado">
91
+ <p class="kicker">What is in here</p>
92
+ <ol>
93
+ ${indice}
94
+ </ol>
95
+ </nav>
96
+
97
+ <main class="fluxo">
90
98
  <section class="hero chapter" style="--ordem:0">
91
99
  <h1>${escapar(cabecalho.repo)}</h1>
92
- <p class="lede">This is what your own code says about itself.</p>
100
+ <p class="lede">${escapar(input.abertura ?? "This is what your own code says about itself.")}</p>
93
101
  <div class="stats">
94
102
  ${numeros}
95
103
  </div>
104
+ ${desenharAneis(input.aneis ?? [])}
105
+ ${desenharCor(input.cor)}
96
106
  </section>
97
107
 
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
108
  ${corpo}
106
109
 
107
110
  <footer>
@@ -109,10 +112,58 @@ ${corpo}
109
112
  <p>This file lives in your project and was never sent anywhere. Re-run the command to refresh it.</p>
110
113
  </footer>
111
114
  </main>
115
+ </div>
112
116
  </body>
113
117
  </html>
114
118
  `;
115
119
  }
120
+ /**
121
+ * OS TRÊS ANÉIS DA NOSSA LEITURA DE TOKEN (pedido dele, 18/09) - e eles abrem a página porque são
122
+ * a régua do produto: quanto já vem de token, quanto ela declara, e quanto a leitura entendeu.
123
+ *
124
+ * O anel é um `conic-gradient` e nada mais: sem biblioteca, sem `<script>`, e sem uma requisição.
125
+ * A fatia é a proporção MEDIDA - e um anel só existe onde há denominador, porque desenhar a fatia
126
+ * de um total que ninguém contou é inventar a proporção.
127
+ */
128
+ function desenharAneis(aneis) {
129
+ if (aneis.length === 0)
130
+ return "";
131
+ const cartoes = aneis
132
+ .map((a) => {
133
+ const pct = a.de > 0 ? Math.round((a.parte / a.de) * 100) : 0;
134
+ return ` <div class="anel-cartao">
135
+ <div class="anel" style="--fatia:${pct}%" role="img" aria-label="${escapar(`${a.titulo}: ${a.numero}`)}">
136
+ <b>${escapar(a.numero)}</b>
137
+ </div>
138
+ <div class="anel-texto">
139
+ <h3>${escapar(a.titulo)}</h3>
140
+ <p>${escapar(a.frase)}</p>
141
+ </div>
142
+ </div>`;
143
+ })
144
+ .join("\n");
145
+ return ` <section class="aneis chapter" style="--ordem:1">
146
+ ${cartoes}
147
+ </section>`;
148
+ }
149
+ /**
150
+ * A HISTÓRIA DAS CORES, com a paleta desenhada do lado - as cores DELA, nos valores dela.
151
+ *
152
+ * Os quadradinhos não são enfeite: uma frase sobre quarenta e sete cores colidindo é abstrata até
153
+ * a pessoa VER que duas casas ao lado da outra são a mesma cor.
154
+ */
155
+ function desenharCor(cor) {
156
+ if (!cor || cor.linhas.length === 0)
157
+ return "";
158
+ const amostras = cor.paleta
159
+ .map((c) => `<i style="--amostra:${escapar(c)}" title="${escapar(c)}"></i>`)
160
+ .join("");
161
+ return ` <section class="chapter cor" style="--ordem:2">
162
+ <h2><span>Your colours</span></h2>
163
+ ${cor.linhas.map((l) => ` <p class="callout">${escapar(l)}</p>`).join("\n")}
164
+ <div class="paleta" aria-hidden="true">${amostras}</div>
165
+ </section>`;
166
+ }
116
167
  /**
117
168
  * O NÚMERO COM A FATIA DESENHADA - e a barra só aparece quando existe denominador medido.
118
169
  *
@@ -150,13 +201,16 @@ function desenharCorpo(s) {
150
201
  const familias = agrupar(resto);
151
202
  const corpo = familias
152
203
  .map((f) => f.tipo === "tabela"
153
- ? ` <details class="familia" open>
204
+ ? ` <details class="familia">
154
205
  <summary><span>${escapar(f.rotulo)}</span> <b>${f.itens.length}</b></summary>
155
206
  <ul class="grade">
156
207
  ${f.itens.map((i) => ` <li>${escapar(i)}</li>`).join("\n")}
157
208
  </ul>
158
209
  </details>`
159
- : ` <pre>${escapar(f.itens.join("\n"))}</pre>`)
210
+ : ` <details class="tecnico">
211
+ <summary>${f.itens.filter((l) => l.trim()).length} lines of detail</summary>
212
+ <pre>${escapar(f.itens.join("\n"))}</pre>
213
+ </details>`)
160
214
  .join("\n");
161
215
  return destaque
162
216
  ? ` <p class="callout">${escapar(destaque)}</p>\n${corpo}`
@@ -295,7 +349,8 @@ function layout() {
295
349
  const corpo = `var(${FAMILY_SEAM_PREFIX}body, ui-sans-serif, system-ui, sans-serif)`;
296
350
  const mono = `var(${FAMILY_SEAM_PREFIX}mono, ui-monospace, monospace)`;
297
351
  const display = `var(${FAMILY_SEAM_PREFIX}display, inherit)`;
298
- return `body {
352
+ return `html { scroll-behavior: smooth; }
353
+ body {
299
354
  margin: 0;
300
355
  background: var(--ds-color-semantic-canvas);
301
356
  color: var(--ds-color-semantic-foreground);
@@ -325,7 +380,27 @@ function layout() {
325
380
  .bar-repo { margin: 2px 0 0; font-weight: 600; }
326
381
  .bar-id { min-width: 0; }
327
382
 
328
- .wrap { max-width: 920px; margin-inline: auto; padding: 0 24px 96px; }
383
+ .wrap {
384
+ max-width: 1200px;
385
+ margin-inline: auto;
386
+ padding: 0 24px 96px;
387
+ display: grid;
388
+ grid-template-columns: 250px minmax(0, 1fr);
389
+ gap: 40px;
390
+ align-items: start;
391
+ }
392
+ .fluxo { min-width: 0; }
393
+
394
+ /* A LATERAL SEGURA A ROLAGEM INTEIRA: quem esta no capitulo doze pula para o tres sem subir, que
395
+ era o que a lista no topo obrigava. Em tela estreita ela vira topo - uma coluna de 250px num
396
+ celular come metade da leitura. */
397
+ .lado {
398
+ position: sticky;
399
+ top: 76px;
400
+ max-height: calc(100vh - 100px);
401
+ overflow: auto;
402
+ padding: 20px 0 0;
403
+ }
329
404
 
330
405
  .kicker {
331
406
  margin: 0;
@@ -501,7 +576,66 @@ footer { margin-top: 40px; color: var(--ds-color-semantic-muted); font-size: 13p
501
576
  .chapter { animation: entrar 620ms cubic-bezier(0.22, 1, 0.36, 1) both; animation-delay: calc(var(--ordem, 0) * 55ms); }
502
577
 
503
578
  @media (prefers-reduced-motion: reduce) {
504
- .chapter, .bar-track i { animation: none; }
579
+ html { scroll-behavior: auto; }
580
+ .chapter, .bar-track i, .anel { animation: none; }
581
+ }
582
+
583
+ /* OS ANEIS: um gradiente conico e mais nada - sem biblioteca e sem script, que e o contrato. */
584
+ .aneis {
585
+ display: grid;
586
+ gap: 18px;
587
+ margin-top: 16px;
588
+ }
589
+ .anel-cartao { display: flex; gap: 18px; align-items: center; }
590
+ .anel {
591
+ flex: 0 0 auto;
592
+ width: 84px;
593
+ height: 84px;
594
+ border-radius: 50%;
595
+ display: grid;
596
+ place-items: center;
597
+ background: conic-gradient(
598
+ var(--ds-color-semantic-accent) var(--fatia),
599
+ var(--ds-color-semantic-overlay, var(--ds-color-semantic-border)) 0
600
+ );
601
+ animation: girar 900ms cubic-bezier(0.22, 1, 0.36, 1) both;
602
+ }
603
+ .anel b {
604
+ width: 62px;
605
+ height: 62px;
606
+ border-radius: 50%;
607
+ background: var(--ds-color-semantic-surface);
608
+ display: grid;
609
+ place-items: center;
610
+ font-size: 15px;
611
+ font-variant-numeric: tabular-nums;
612
+ }
613
+ .anel-texto h3 { margin: 0 0 4px; font-size: 15px; }
614
+ .anel-texto p { margin: 0; color: var(--ds-color-semantic-muted); font-size: 14px; }
615
+
616
+ .paleta { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 14px; }
617
+ .paleta i {
618
+ width: 30px;
619
+ height: 30px;
620
+ border-radius: var(--ds-radius-sm, 8px);
621
+ background: var(--amostra);
622
+ border: 1px solid var(--ds-color-semantic-border);
623
+ }
624
+
625
+ /* O DETALHE TECNICO NASCE FECHADO: ele e a evidencia, nao a conclusao. */
626
+ .tecnico { margin: 0 0 12px; }
627
+ .tecnico summary {
628
+ cursor: pointer;
629
+ font-size: 13px;
630
+ color: var(--ds-color-semantic-faint);
631
+ }
632
+ .tecnico pre { margin-top: 10px; }
633
+
634
+ @keyframes girar { from { --fatia: 0%; } }
635
+
636
+ @media (max-width: 900px) {
637
+ .wrap { grid-template-columns: minmax(0, 1fr); gap: 0; }
638
+ .lado { position: static; max-height: none; padding-bottom: 20px; }
505
639
  }
506
640
 
507
641
  @media print {
@@ -0,0 +1,143 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.432",
3
+ "version": "0.16.433",
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": {