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.
- package/dist/commands/import.js +15 -0
- package/dist/report-html.js +211 -19
- package/dist/report-insight.js +171 -0
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -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
|
package/dist/report-html.js
CHANGED
|
@@ -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
|
-
<
|
|
85
|
+
<div class="bar-fim">
|
|
86
|
+
<p class="meta">${escapar(quando)} · CLI ${escapar(cabecalho.cli)} · 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
|
-
<
|
|
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"
|
|
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"
|
|
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
|
-
: ` <
|
|
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 `
|
|
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:
|
|
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 {
|
|
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:
|
|
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:
|
|
394
|
-
padding:
|
|
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
|
-
|
|
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