synthesisui 0.16.431 → 0.16.432

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.
Files changed (2) hide show
  1. package/dist/report-html.js +158 -39
  2. package/package.json +1 -1
@@ -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");
@@ -130,26 +135,57 @@ function cartaoDeNumero(d) {
130
135
  /**
131
136
  * COMO CADA CAPÍTULO É DESENHADO - e a decisão é sobre REPETIÇÃO, não sobre o assunto.
132
137
  *
133
- * Medido no `codelevel-monorepo` em 18/09: uma única seção trazia 94 linhas com a mesma forma
134
- * (`two parts named X - the second is Y`), num bloco monoespaçado onde nada se acha. Uma lista de
135
- * itens repetidos é uma TABELA, e uma tabela é varrida com o olho; o bloco de texto é lido linha a
136
- * linha ou não é lido.
138
+ * Medido na página dele em 18/09: as 94 linhas com a mesma forma saíram em SEIS pedaços, porque o
139
+ * agrupamento só juntava linhas VIZINHAS e as frases de `mx-auto` caem no meio delas. Seis blocos
140
+ * de dez são a mesma parede com mais bordas.
141
+ *
142
+ * Agora a família é da SEÇÃO INTEIRA: irmãs distantes se encontram, e a ordem que sobrevive é a da
143
+ * primeira aparição - o texto continua sendo lido na ordem em que a esteira o escreveu.
137
144
  *
138
- * O corte é mecânico e não conhece assunto nenhum: uma família só vira tabela a partir de seis
139
- * linhas irmãs, porque abaixo disso a tabela custa mais atenção do que economiza.
145
+ * E O PRIMEIRO FATO DO CAPÍTULO SOBE: a linha de abertura, que é onde mora o número, deixa de ter
146
+ * o peso do detalhe monoespaçado e vira o CALLOUT que a pessoa lê antes de decidir se desce.
140
147
  */
141
148
  function desenharCorpo(s) {
142
- const familias = agrupar(s.linhas);
143
- return familias
149
+ const { destaque, resto } = abertura(s.linhas);
150
+ const familias = agrupar(resto);
151
+ const corpo = familias
144
152
  .map((f) => f.tipo === "tabela"
145
153
  ? ` <details class="familia" open>
146
- <summary>${escapar(f.rotulo)} <b>${f.itens.length}</b></summary>
154
+ <summary><span>${escapar(f.rotulo)}</span> <b>${f.itens.length}</b></summary>
147
155
  <ul class="grade">
148
156
  ${f.itens.map((i) => ` <li>${escapar(i)}</li>`).join("\n")}
149
157
  </ul>
150
158
  </details>`
151
159
  : ` <pre>${escapar(f.itens.join("\n"))}</pre>`)
152
160
  .join("\n");
161
+ return destaque
162
+ ? ` <p class="callout">${escapar(destaque)}</p>\n${corpo}`
163
+ : corpo;
164
+ }
165
+ /**
166
+ * A ABERTURA DO CAPÍTULO - a primeira linha com conteúdo, e só quando ela CARREGA UM NÚMERO.
167
+ *
168
+ * É o número que faz a frase valer o destaque: "78 of 78 components came out with a blueprint" é
169
+ * um fato que decide se a pessoa lê o resto. Uma abertura sem número seria só a primeira linha
170
+ * promovida por ser a primeira, e aí o destaque deixaria de significar alguma coisa.
171
+ */
172
+ function abertura(linhas) {
173
+ const i = linhas.findIndex((l) => l.trim());
174
+ if (i < 0)
175
+ return { destaque: null, resto: linhas };
176
+ const primeira = linhas[i].trim();
177
+ if (!/\d/.test(primeira) || primeira.length > 240)
178
+ return { destaque: null, resto: linhas };
179
+ /**
180
+ * E ELA NÃO PODE SER MEMBRO DE UMA FAMÍLIA. Um capítulo que abre com a primeira de noventa
181
+ * linhas iguais promoveria uma irmã qualquer a conclusão, e ainda tiraria uma da contagem -
182
+ * duas mentiras pelo preço de uma. O destaque é para o fato ÚNICO.
183
+ */
184
+ const f = forma(primeira);
185
+ const irmas = linhas.filter((l) => l.trim() && forma(l) === f).length;
186
+ if (irmas >= MINIMO_PARA_TABELA)
187
+ return { destaque: null, resto: linhas };
188
+ return { destaque: primeira, resto: linhas.slice(i + 1) };
153
189
  }
154
190
  const MINIMO_PARA_TABELA = 6;
155
191
  /**
@@ -168,30 +204,41 @@ function forma(linha) {
168
204
  .slice(0, 6)
169
205
  .join(" ");
170
206
  }
207
+ /**
208
+ * AGRUPA A SEÇÃO INTEIRA, e a ordem é a da PRIMEIRA APARIÇÃO de cada forma.
209
+ *
210
+ * Uma varredura só: cada linha entra no balde da forma dela, e os baldes saem na ordem em que
211
+ * nasceram. Quem tem irmãs suficientes vira família; quem não tem volta para a prosa, no lugar
212
+ * onde estava. O que a pessoa lê continua sendo o texto da esteira, na ordem da esteira.
213
+ */
171
214
  function agrupar(linhas) {
172
- const saida = [];
173
- let i = 0;
174
- while (i < linhas.length) {
175
- const atual = linhas[i];
176
- if (!atual.trim()) {
177
- empilharProsa(saida, atual);
178
- i += 1;
215
+ const contagem = new Map();
216
+ for (const l of linhas) {
217
+ if (!l.trim())
179
218
  continue;
180
- }
181
- const f = forma(atual);
182
- let j = i;
183
- const irmas = [];
184
- while (j < linhas.length && linhas[j].trim() && forma(linhas[j]) === f) {
185
- irmas.push(linhas[j].trim());
186
- j += 1;
187
- }
188
- if (irmas.length >= MINIMO_PARA_TABELA) {
189
- saida.push({ tipo: "tabela", rotulo: rotuloDe(f), itens: irmas });
190
- i = j;
219
+ const f = forma(l);
220
+ contagem.set(f, (contagem.get(f) ?? 0) + 1);
221
+ }
222
+ const saida = [];
223
+ const familiaDe = new Map();
224
+ for (const l of linhas) {
225
+ const f = l.trim() ? forma(l) : "";
226
+ if (l.trim() && (contagem.get(f) ?? 0) >= MINIMO_PARA_TABELA) {
227
+ const existente = familiaDe.get(f);
228
+ if (existente) {
229
+ existente.itens.push(l.trim());
230
+ continue;
231
+ }
232
+ const nova = {
233
+ tipo: "tabela",
234
+ rotulo: rotuloDe(f),
235
+ itens: [l.trim()],
236
+ };
237
+ familiaDe.set(f, nova);
238
+ saida.push(nova);
191
239
  continue;
192
240
  }
193
- empilharProsa(saida, atual);
194
- i += 1;
241
+ empilharProsa(saida, l);
195
242
  }
196
243
  return saida.filter((f) => f.itens.some((l) => l.trim()));
197
244
  }
@@ -202,9 +249,18 @@ function empilharProsa(saida, linha) {
202
249
  else
203
250
  saida.push({ tipo: "prosa", rotulo: "", itens: [linha] });
204
251
  }
205
- /** O rótulo da família é a própria forma, com os buracos ditos em vez de mostrados. */
252
+ /**
253
+ * O RÓTULO DA FAMÍLIA - uma frase que se lê, e não a forma crua.
254
+ *
255
+ * `two parts named · - the second is ·` saía como `two parts named … - the 10`: reticência solta,
256
+ * minúscula, e sem dizer o que aquilo é. O rótulo agora é a parte ESTÁVEL da frase - o que todas as
257
+ * irmãs têm em comum - cortada no primeiro buraco e começando com maiúscula.
258
+ */
206
259
  function rotuloDe(f) {
207
- return f.replace(/·/g, "…").replace(/#/g, "n").trim() || "repeated lines";
260
+ const estavel = f.split("·")[0].replace(/#/g, "n").trim();
261
+ const limpo = estavel.replace(/[\s\-–—:,]+$/, "");
262
+ const frase = limpo || "Repeated lines";
263
+ return frase.charAt(0).toUpperCase() + frase.slice(1);
208
264
  }
209
265
  /** A data em palavras. Data ilegível some da frase em vez de imprimir `Invalid Date`. */
210
266
  function legivel(iso) {
@@ -259,7 +315,7 @@ function layout() {
259
315
  .bar-in {
260
316
  max-width: 920px;
261
317
  margin-inline: auto;
262
- padding: 12px 24px;
318
+ padding: 11px 24px;
263
319
  display: flex;
264
320
  flex-wrap: wrap;
265
321
  gap: 8px 24px;
@@ -321,10 +377,45 @@ h1 {
321
377
  }
322
378
 
323
379
  nav { margin: 44px 0 8px; }
324
- nav ol { margin: 10px 0 0; padding-left: 20px; color: var(--ds-color-semantic-muted); }
325
- nav li { margin: 3px 0; }
326
- nav a { color: inherit; text-underline-offset: 3px; }
327
- nav a:hover { color: var(--ds-color-semantic-accent); }
380
+ nav ol {
381
+ margin: 12px 0 0;
382
+ padding: 0;
383
+ list-style: none;
384
+ counter-reset: capitulo;
385
+ color: var(--ds-color-semantic-muted);
386
+ display: grid;
387
+ gap: 1px;
388
+ }
389
+ nav li {
390
+ counter-increment: capitulo;
391
+ display: flex;
392
+ align-items: baseline;
393
+ gap: 12px;
394
+ padding: 7px 10px;
395
+ border-radius: var(--ds-radius-sm, 8px);
396
+ }
397
+ nav li:hover { background: var(--ds-color-semantic-surface); }
398
+ nav li::before {
399
+ content: counter(capitulo);
400
+ min-width: 18px;
401
+ font-variant-numeric: tabular-nums;
402
+ font-size: 12px;
403
+ color: var(--ds-color-semantic-faint);
404
+ }
405
+ nav a { color: inherit; text-decoration: none; }
406
+ nav li:hover a { color: var(--ds-color-semantic-accent); }
407
+ /* A regua que sobra entre o nome e a contagem - sem ela os numeros flutuam soltos. */
408
+ nav a::after {
409
+ content: "";
410
+ display: inline-block;
411
+ width: 100%;
412
+ }
413
+ nav li > span {
414
+ margin-left: auto;
415
+ font-variant-numeric: tabular-nums;
416
+ font-size: 12px;
417
+ color: var(--ds-color-semantic-faint);
418
+ }
328
419
 
329
420
  section.chapter {
330
421
  background: var(--ds-color-semantic-surface);
@@ -335,7 +426,35 @@ section.chapter {
335
426
  scroll-margin-top: 84px;
336
427
  }
337
428
  .hero, nav { background: none; border: 0; padding-inline: 0; }
338
- h2 { margin: 0 0 14px; font-size: 18px; letter-spacing: -0.01em; }
429
+ /* O TITULO DO CAPITULO SEGURA O TOPO ENQUANTO O CAPITULO PASSA - e e assim que a pessoa sempre
430
+ sabe o que esta lendo, sem uma linha de script. Um capitulo de cem linhas rolava ate o fim sem
431
+ nada na tela dizendo de qual dos dezesseis ele era. O deslocamento e a altura da barra: eles se
432
+ encostam em vez de se sobrepor, e o segundo empurra o primeiro so quando chega a vez dele. */
433
+ h2 {
434
+ position: sticky;
435
+ top: 60px;
436
+ z-index: 5;
437
+ margin: -24px -26px 14px;
438
+ padding: 14px 26px 12px;
439
+ font-size: 18px;
440
+ letter-spacing: -0.01em;
441
+ background: var(--ds-color-semantic-surface);
442
+ border-bottom: 1px solid var(--ds-color-semantic-border);
443
+ border-radius: var(--ds-radius-lg, 14px) var(--ds-radius-lg, 14px) 0 0;
444
+ }
445
+ h2 span { display: block; }
446
+
447
+ /* O PRIMEIRO FATO DO CAPITULO, no peso de quem decide se vale descer. Ele e prosa, nao codigo -
448
+ entao sai do monoespacado e ganha a regua da esquerda, que e a unica marca do documento que diz
449
+ isto aqui e a conclusao, o resto e a evidencia. */
450
+ .callout {
451
+ margin: 0 0 16px;
452
+ padding: 2px 0 2px 16px;
453
+ border-left: 2px solid var(--ds-color-semantic-accent);
454
+ font-size: 15.5px;
455
+ line-height: 1.55;
456
+ color: var(--ds-color-semantic-foreground);
457
+ }
339
458
 
340
459
  pre {
341
460
  margin: 0 0 12px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.431",
3
+ "version": "0.16.432",
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": {