synthesisui 0.16.431 → 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.
- package/dist/commands/import.js +9 -0
- package/dist/report-html.js +306 -53
- package/dist/report-insight.js +143 -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 } 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
|
package/dist/report-html.js
CHANGED
|
@@ -47,12 +47,17 @@ function escapar(s) {
|
|
|
47
47
|
export function relatorioHtml(input) {
|
|
48
48
|
const { cabecalho, destaques, secoes } = input;
|
|
49
49
|
const quando = legivel(cabecalho.medidoEm);
|
|
50
|
+
/**
|
|
51
|
+
* O ÍNDICE DIZ O TAMANHO DE CADA CAPÍTULO, e é isso que o torna uma escolha em vez de uma lista.
|
|
52
|
+
* Dezesseis links iguais obrigam a entrar para descobrir o custo; com a contagem ao lado, quem
|
|
53
|
+
* está com pressa sabe onde não entrar.
|
|
54
|
+
*/
|
|
50
55
|
const indice = secoes
|
|
51
|
-
.map((s, i) => `<li><a href="#s${i}">${escapar(s.titulo)}</a></li>`)
|
|
56
|
+
.map((s, i) => `<li><a href="#s${i}">${escapar(s.titulo)}</a><span>${s.linhas.filter((l) => l.trim()).length}</span></li>`)
|
|
52
57
|
.join("\n ");
|
|
53
58
|
const corpo = secoes
|
|
54
59
|
.map((s, i) => ` <section id="s${i}" class="chapter" style="--ordem:${Math.min(i, 12)}">
|
|
55
|
-
<h2>${escapar(s.titulo)}</h2>
|
|
60
|
+
<h2><span>${escapar(s.titulo)}</span></h2>
|
|
56
61
|
${desenharCorpo(s)}
|
|
57
62
|
</section>`)
|
|
58
63
|
.join("\n");
|
|
@@ -81,22 +86,25 @@ ${layout()}
|
|
|
81
86
|
</div>
|
|
82
87
|
</header>
|
|
83
88
|
|
|
84
|
-
<
|
|
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">
|
|
85
98
|
<section class="hero chapter" style="--ordem:0">
|
|
86
99
|
<h1>${escapar(cabecalho.repo)}</h1>
|
|
87
|
-
<p class="lede"
|
|
100
|
+
<p class="lede">${escapar(input.abertura ?? "This is what your own code says about itself.")}</p>
|
|
88
101
|
<div class="stats">
|
|
89
102
|
${numeros}
|
|
90
103
|
</div>
|
|
104
|
+
${desenharAneis(input.aneis ?? [])}
|
|
105
|
+
${desenharCor(input.cor)}
|
|
91
106
|
</section>
|
|
92
107
|
|
|
93
|
-
<nav class="chapter" style="--ordem:1">
|
|
94
|
-
<p class="kicker">What is in here</p>
|
|
95
|
-
<ol>
|
|
96
|
-
${indice}
|
|
97
|
-
</ol>
|
|
98
|
-
</nav>
|
|
99
|
-
|
|
100
108
|
${corpo}
|
|
101
109
|
|
|
102
110
|
<footer>
|
|
@@ -104,10 +112,58 @@ ${corpo}
|
|
|
104
112
|
<p>This file lives in your project and was never sent anywhere. Re-run the command to refresh it.</p>
|
|
105
113
|
</footer>
|
|
106
114
|
</main>
|
|
115
|
+
</div>
|
|
107
116
|
</body>
|
|
108
117
|
</html>
|
|
109
118
|
`;
|
|
110
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
|
+
}
|
|
111
167
|
/**
|
|
112
168
|
* O NÚMERO COM A FATIA DESENHADA - e a barra só aparece quando existe denominador medido.
|
|
113
169
|
*
|
|
@@ -130,26 +186,60 @@ function cartaoDeNumero(d) {
|
|
|
130
186
|
/**
|
|
131
187
|
* COMO CADA CAPÍTULO É DESENHADO - e a decisão é sobre REPETIÇÃO, não sobre o assunto.
|
|
132
188
|
*
|
|
133
|
-
* Medido
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
189
|
+
* Medido na página dele em 18/09: as 94 linhas com a mesma forma saíram em SEIS pedaços, porque o
|
|
190
|
+
* agrupamento só juntava linhas VIZINHAS e as frases de `mx-auto` caem no meio delas. Seis blocos
|
|
191
|
+
* de dez são a mesma parede com mais bordas.
|
|
192
|
+
*
|
|
193
|
+
* Agora a família é da SEÇÃO INTEIRA: irmãs distantes se encontram, e a ordem que sobrevive é a da
|
|
194
|
+
* primeira aparição - o texto continua sendo lido na ordem em que a esteira o escreveu.
|
|
137
195
|
*
|
|
138
|
-
* O
|
|
139
|
-
*
|
|
196
|
+
* E O PRIMEIRO FATO DO CAPÍTULO SOBE: a linha de abertura, que é onde mora o número, deixa de ter
|
|
197
|
+
* o peso do detalhe monoespaçado e vira o CALLOUT que a pessoa lê antes de decidir se desce.
|
|
140
198
|
*/
|
|
141
199
|
function desenharCorpo(s) {
|
|
142
|
-
const
|
|
143
|
-
|
|
200
|
+
const { destaque, resto } = abertura(s.linhas);
|
|
201
|
+
const familias = agrupar(resto);
|
|
202
|
+
const corpo = familias
|
|
144
203
|
.map((f) => f.tipo === "tabela"
|
|
145
|
-
? ` <details class="familia"
|
|
146
|
-
<summary>${escapar(f.rotulo)} <b>${f.itens.length}</b></summary>
|
|
204
|
+
? ` <details class="familia">
|
|
205
|
+
<summary><span>${escapar(f.rotulo)}</span> <b>${f.itens.length}</b></summary>
|
|
147
206
|
<ul class="grade">
|
|
148
207
|
${f.itens.map((i) => ` <li>${escapar(i)}</li>`).join("\n")}
|
|
149
208
|
</ul>
|
|
150
209
|
</details>`
|
|
151
|
-
: ` <
|
|
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>`)
|
|
152
214
|
.join("\n");
|
|
215
|
+
return destaque
|
|
216
|
+
? ` <p class="callout">${escapar(destaque)}</p>\n${corpo}`
|
|
217
|
+
: corpo;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* A ABERTURA DO CAPÍTULO - a primeira linha com conteúdo, e só quando ela CARREGA UM NÚMERO.
|
|
221
|
+
*
|
|
222
|
+
* É o número que faz a frase valer o destaque: "78 of 78 components came out with a blueprint" é
|
|
223
|
+
* um fato que decide se a pessoa lê o resto. Uma abertura sem número seria só a primeira linha
|
|
224
|
+
* promovida por ser a primeira, e aí o destaque deixaria de significar alguma coisa.
|
|
225
|
+
*/
|
|
226
|
+
function abertura(linhas) {
|
|
227
|
+
const i = linhas.findIndex((l) => l.trim());
|
|
228
|
+
if (i < 0)
|
|
229
|
+
return { destaque: null, resto: linhas };
|
|
230
|
+
const primeira = linhas[i].trim();
|
|
231
|
+
if (!/\d/.test(primeira) || primeira.length > 240)
|
|
232
|
+
return { destaque: null, resto: linhas };
|
|
233
|
+
/**
|
|
234
|
+
* E ELA NÃO PODE SER MEMBRO DE UMA FAMÍLIA. Um capítulo que abre com a primeira de noventa
|
|
235
|
+
* linhas iguais promoveria uma irmã qualquer a conclusão, e ainda tiraria uma da contagem -
|
|
236
|
+
* duas mentiras pelo preço de uma. O destaque é para o fato ÚNICO.
|
|
237
|
+
*/
|
|
238
|
+
const f = forma(primeira);
|
|
239
|
+
const irmas = linhas.filter((l) => l.trim() && forma(l) === f).length;
|
|
240
|
+
if (irmas >= MINIMO_PARA_TABELA)
|
|
241
|
+
return { destaque: null, resto: linhas };
|
|
242
|
+
return { destaque: primeira, resto: linhas.slice(i + 1) };
|
|
153
243
|
}
|
|
154
244
|
const MINIMO_PARA_TABELA = 6;
|
|
155
245
|
/**
|
|
@@ -168,30 +258,41 @@ function forma(linha) {
|
|
|
168
258
|
.slice(0, 6)
|
|
169
259
|
.join(" ");
|
|
170
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* AGRUPA A SEÇÃO INTEIRA, e a ordem é a da PRIMEIRA APARIÇÃO de cada forma.
|
|
263
|
+
*
|
|
264
|
+
* Uma varredura só: cada linha entra no balde da forma dela, e os baldes saem na ordem em que
|
|
265
|
+
* nasceram. Quem tem irmãs suficientes vira família; quem não tem volta para a prosa, no lugar
|
|
266
|
+
* onde estava. O que a pessoa lê continua sendo o texto da esteira, na ordem da esteira.
|
|
267
|
+
*/
|
|
171
268
|
function agrupar(linhas) {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const atual = linhas[i];
|
|
176
|
-
if (!atual.trim()) {
|
|
177
|
-
empilharProsa(saida, atual);
|
|
178
|
-
i += 1;
|
|
269
|
+
const contagem = new Map();
|
|
270
|
+
for (const l of linhas) {
|
|
271
|
+
if (!l.trim())
|
|
179
272
|
continue;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
273
|
+
const f = forma(l);
|
|
274
|
+
contagem.set(f, (contagem.get(f) ?? 0) + 1);
|
|
275
|
+
}
|
|
276
|
+
const saida = [];
|
|
277
|
+
const familiaDe = new Map();
|
|
278
|
+
for (const l of linhas) {
|
|
279
|
+
const f = l.trim() ? forma(l) : "";
|
|
280
|
+
if (l.trim() && (contagem.get(f) ?? 0) >= MINIMO_PARA_TABELA) {
|
|
281
|
+
const existente = familiaDe.get(f);
|
|
282
|
+
if (existente) {
|
|
283
|
+
existente.itens.push(l.trim());
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const nova = {
|
|
287
|
+
tipo: "tabela",
|
|
288
|
+
rotulo: rotuloDe(f),
|
|
289
|
+
itens: [l.trim()],
|
|
290
|
+
};
|
|
291
|
+
familiaDe.set(f, nova);
|
|
292
|
+
saida.push(nova);
|
|
191
293
|
continue;
|
|
192
294
|
}
|
|
193
|
-
empilharProsa(saida,
|
|
194
|
-
i += 1;
|
|
295
|
+
empilharProsa(saida, l);
|
|
195
296
|
}
|
|
196
297
|
return saida.filter((f) => f.itens.some((l) => l.trim()));
|
|
197
298
|
}
|
|
@@ -202,9 +303,18 @@ function empilharProsa(saida, linha) {
|
|
|
202
303
|
else
|
|
203
304
|
saida.push({ tipo: "prosa", rotulo: "", itens: [linha] });
|
|
204
305
|
}
|
|
205
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* O RÓTULO DA FAMÍLIA - uma frase que se lê, e não a forma crua.
|
|
308
|
+
*
|
|
309
|
+
* `two parts named · - the second is ·` saía como `two parts named … - the 10`: reticência solta,
|
|
310
|
+
* minúscula, e sem dizer o que aquilo é. O rótulo agora é a parte ESTÁVEL da frase - o que todas as
|
|
311
|
+
* irmãs têm em comum - cortada no primeiro buraco e começando com maiúscula.
|
|
312
|
+
*/
|
|
206
313
|
function rotuloDe(f) {
|
|
207
|
-
|
|
314
|
+
const estavel = f.split("·")[0].replace(/#/g, "n").trim();
|
|
315
|
+
const limpo = estavel.replace(/[\s\-–—:,]+$/, "");
|
|
316
|
+
const frase = limpo || "Repeated lines";
|
|
317
|
+
return frase.charAt(0).toUpperCase() + frase.slice(1);
|
|
208
318
|
}
|
|
209
319
|
/** A data em palavras. Data ilegível some da frase em vez de imprimir `Invalid Date`. */
|
|
210
320
|
function legivel(iso) {
|
|
@@ -239,7 +349,8 @@ function layout() {
|
|
|
239
349
|
const corpo = `var(${FAMILY_SEAM_PREFIX}body, ui-sans-serif, system-ui, sans-serif)`;
|
|
240
350
|
const mono = `var(${FAMILY_SEAM_PREFIX}mono, ui-monospace, monospace)`;
|
|
241
351
|
const display = `var(${FAMILY_SEAM_PREFIX}display, inherit)`;
|
|
242
|
-
return `
|
|
352
|
+
return `html { scroll-behavior: smooth; }
|
|
353
|
+
body {
|
|
243
354
|
margin: 0;
|
|
244
355
|
background: var(--ds-color-semantic-canvas);
|
|
245
356
|
color: var(--ds-color-semantic-foreground);
|
|
@@ -259,7 +370,7 @@ function layout() {
|
|
|
259
370
|
.bar-in {
|
|
260
371
|
max-width: 920px;
|
|
261
372
|
margin-inline: auto;
|
|
262
|
-
padding:
|
|
373
|
+
padding: 11px 24px;
|
|
263
374
|
display: flex;
|
|
264
375
|
flex-wrap: wrap;
|
|
265
376
|
gap: 8px 24px;
|
|
@@ -269,7 +380,27 @@ function layout() {
|
|
|
269
380
|
.bar-repo { margin: 2px 0 0; font-weight: 600; }
|
|
270
381
|
.bar-id { min-width: 0; }
|
|
271
382
|
|
|
272
|
-
.wrap {
|
|
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
|
+
}
|
|
273
404
|
|
|
274
405
|
.kicker {
|
|
275
406
|
margin: 0;
|
|
@@ -321,10 +452,45 @@ h1 {
|
|
|
321
452
|
}
|
|
322
453
|
|
|
323
454
|
nav { margin: 44px 0 8px; }
|
|
324
|
-
nav ol {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
455
|
+
nav ol {
|
|
456
|
+
margin: 12px 0 0;
|
|
457
|
+
padding: 0;
|
|
458
|
+
list-style: none;
|
|
459
|
+
counter-reset: capitulo;
|
|
460
|
+
color: var(--ds-color-semantic-muted);
|
|
461
|
+
display: grid;
|
|
462
|
+
gap: 1px;
|
|
463
|
+
}
|
|
464
|
+
nav li {
|
|
465
|
+
counter-increment: capitulo;
|
|
466
|
+
display: flex;
|
|
467
|
+
align-items: baseline;
|
|
468
|
+
gap: 12px;
|
|
469
|
+
padding: 7px 10px;
|
|
470
|
+
border-radius: var(--ds-radius-sm, 8px);
|
|
471
|
+
}
|
|
472
|
+
nav li:hover { background: var(--ds-color-semantic-surface); }
|
|
473
|
+
nav li::before {
|
|
474
|
+
content: counter(capitulo);
|
|
475
|
+
min-width: 18px;
|
|
476
|
+
font-variant-numeric: tabular-nums;
|
|
477
|
+
font-size: 12px;
|
|
478
|
+
color: var(--ds-color-semantic-faint);
|
|
479
|
+
}
|
|
480
|
+
nav a { color: inherit; text-decoration: none; }
|
|
481
|
+
nav li:hover a { color: var(--ds-color-semantic-accent); }
|
|
482
|
+
/* A regua que sobra entre o nome e a contagem - sem ela os numeros flutuam soltos. */
|
|
483
|
+
nav a::after {
|
|
484
|
+
content: "";
|
|
485
|
+
display: inline-block;
|
|
486
|
+
width: 100%;
|
|
487
|
+
}
|
|
488
|
+
nav li > span {
|
|
489
|
+
margin-left: auto;
|
|
490
|
+
font-variant-numeric: tabular-nums;
|
|
491
|
+
font-size: 12px;
|
|
492
|
+
color: var(--ds-color-semantic-faint);
|
|
493
|
+
}
|
|
328
494
|
|
|
329
495
|
section.chapter {
|
|
330
496
|
background: var(--ds-color-semantic-surface);
|
|
@@ -335,7 +501,35 @@ section.chapter {
|
|
|
335
501
|
scroll-margin-top: 84px;
|
|
336
502
|
}
|
|
337
503
|
.hero, nav { background: none; border: 0; padding-inline: 0; }
|
|
338
|
-
|
|
504
|
+
/* O TITULO DO CAPITULO SEGURA O TOPO ENQUANTO O CAPITULO PASSA - e e assim que a pessoa sempre
|
|
505
|
+
sabe o que esta lendo, sem uma linha de script. Um capitulo de cem linhas rolava ate o fim sem
|
|
506
|
+
nada na tela dizendo de qual dos dezesseis ele era. O deslocamento e a altura da barra: eles se
|
|
507
|
+
encostam em vez de se sobrepor, e o segundo empurra o primeiro so quando chega a vez dele. */
|
|
508
|
+
h2 {
|
|
509
|
+
position: sticky;
|
|
510
|
+
top: 60px;
|
|
511
|
+
z-index: 5;
|
|
512
|
+
margin: -24px -26px 14px;
|
|
513
|
+
padding: 14px 26px 12px;
|
|
514
|
+
font-size: 18px;
|
|
515
|
+
letter-spacing: -0.01em;
|
|
516
|
+
background: var(--ds-color-semantic-surface);
|
|
517
|
+
border-bottom: 1px solid var(--ds-color-semantic-border);
|
|
518
|
+
border-radius: var(--ds-radius-lg, 14px) var(--ds-radius-lg, 14px) 0 0;
|
|
519
|
+
}
|
|
520
|
+
h2 span { display: block; }
|
|
521
|
+
|
|
522
|
+
/* O PRIMEIRO FATO DO CAPITULO, no peso de quem decide se vale descer. Ele e prosa, nao codigo -
|
|
523
|
+
entao sai do monoespacado e ganha a regua da esquerda, que e a unica marca do documento que diz
|
|
524
|
+
isto aqui e a conclusao, o resto e a evidencia. */
|
|
525
|
+
.callout {
|
|
526
|
+
margin: 0 0 16px;
|
|
527
|
+
padding: 2px 0 2px 16px;
|
|
528
|
+
border-left: 2px solid var(--ds-color-semantic-accent);
|
|
529
|
+
font-size: 15.5px;
|
|
530
|
+
line-height: 1.55;
|
|
531
|
+
color: var(--ds-color-semantic-foreground);
|
|
532
|
+
}
|
|
339
533
|
|
|
340
534
|
pre {
|
|
341
535
|
margin: 0 0 12px;
|
|
@@ -382,7 +576,66 @@ footer { margin-top: 40px; color: var(--ds-color-semantic-muted); font-size: 13p
|
|
|
382
576
|
.chapter { animation: entrar 620ms cubic-bezier(0.22, 1, 0.36, 1) both; animation-delay: calc(var(--ordem, 0) * 55ms); }
|
|
383
577
|
|
|
384
578
|
@media (prefers-reduced-motion: reduce) {
|
|
385
|
-
|
|
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; }
|
|
386
639
|
}
|
|
387
640
|
|
|
388
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