synthesisui 0.16.399 → 0.16.401

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.
@@ -1,5 +1,6 @@
1
1
  import { deltaE, JND } from "./doctor/color-distance.js";
2
2
  import { familySays, nearestToken, normalizeValue, } from "./doctor/tokens.js";
3
+ import { proposeFromTheirScale, tooCloseToPropose } from "./proposed-name.js";
3
4
  /** Onde cada tipo de valor mora na fundação. `color` é o único com dois segmentos. */
4
5
  const HOME = {
5
6
  color: "color",
@@ -180,7 +181,26 @@ export function absorbPlan(d, theirs, have, cap = 40) {
180
181
  const value = normalizeValue(r.literal, theirs.rootPx);
181
182
  const theirName = nameOf(r.kind, theirs.byValue.get(value)) ??
182
183
  nameOf(r.kind, theirs.compiledAway.get(value)?.map((c) => c.name));
183
- const path = pathFor(r.kind, theirName);
184
+ /**
185
+ * A LISTA NÃO CHEGA EM BRANCO - e é o que faz o vocabulário dele CRESCER.
186
+ *
187
+ * Especificado em 07/09 por entrevista (`steps/02-jornada-projeto-novo.md`): a plataforma
188
+ * PROPÕE, ele aprova ou edita, e nada entra sem o aval dele. Sem proposta, cada valor novo custa
189
+ * um batismo manual - e num repositório com 553 valores distintos ninguém batiza 553 vezes. O
190
+ * vocabulário para de crescer e o próximo componente nasce com outro literal, que é o oposto do
191
+ * que a plataforma existe para fazer.
192
+ *
193
+ * A ORDEM É A DE SEMPRE: o nome que ele JÁ deu vence. Só onde ninguém nomeia é que a derivação
194
+ * entra - e ela devolve `null` na dúvida (ver `proposed-name.ts`).
195
+ */
196
+ /**
197
+ * UMA FONTE POR ENQUANTO - a escala dele. A segunda está especificada e NÃO entrou, e o motivo
198
+ * está medido: ver `proposed-name.ts`, no fim.
199
+ */
200
+ const proposal = theirName || tooCloseToPropose(r.literal, theirs)
201
+ ? null
202
+ : proposeFromTheirScale(r.literal, theirs);
203
+ const path = pathFor(r.kind, theirName) || proposal?.path || "";
184
204
  /** "≈ perto de um token seu" só interessa a quem não tem um EXATO - com o nome na mão, a dica
185
205
  * vira ruído, e no arquivo de proposta ela vira uma segunda opção que não é opção. */
186
206
  const near = theirName ? undefined : nearestOwn(theirs, r.literal, r.kind);
@@ -194,6 +214,8 @@ export function absorbPlan(d, theirs, have, cap = 40) {
194
214
  count: r.count,
195
215
  path,
196
216
  ...(theirName ? { theirName: clean(theirName) } : {}),
217
+ /** A PROCEDÊNCIA VIAJA COM A PROPOSTA - ele julga pela origem, nunca pela palavra. */
218
+ ...(proposal ? { proposed: proposal.because } : {}),
197
219
  ...(near ? { near } : {}),
198
220
  });
199
221
  }
@@ -213,7 +235,17 @@ export const needingName = (plan) => plan.entries.filter((e) => !e.path);
213
235
  * nada.
214
236
  */
215
237
  export function describeAbsorb(plan) {
216
- const ready = plan.entries.filter((e) => e.path);
238
+ /**
239
+ * O QUE ELE JÁ NOMEIA E O QUE A PLATAFORMA PROPÔS SÃO DUAS LISTAS - e juntá-las é dizer que ele
240
+ * nomeou o que ele não nomeou.
241
+ *
242
+ * A primeira versão da proposta caiu direto em `ready`, e a linha de cima passou a dizer
243
+ * *"1 of 1 values already have a name in YOUR vocabulary"* sobre um nome que a PLATAFORMA acabou
244
+ * de derivar. É a mesma família de defeito que o dia inteiro de 07/09 consertou: uma frase que
245
+ * afirma sobre o repositório dele algo que veio da nossa dedução.
246
+ */
247
+ const ready = plan.entries.filter((e) => e.path && !e.proposed);
248
+ const proposed = plan.entries.filter((e) => e.proposed);
217
249
  const pending = needingName(plan);
218
250
  const lines = [];
219
251
  /**
@@ -245,6 +277,15 @@ export function describeAbsorb(plan) {
245
277
  lines.push(` ${e.value.padEnd(24)} ${e.path.padEnd(28)} ${e.files} file${e.files === 1 ? "" : "s"}${e.theirName ? ` · your ${e.theirName}` : ""}`);
246
278
  if (ready.length > 8)
247
279
  lines.push(` (${ready.length - 8} more)`);
280
+ if (proposed.length > 0) {
281
+ if (ready.length > 0)
282
+ lines.push("");
283
+ lines.push(`${proposed.length} ${proposed.length === 1 ? "value has" : "values have"} a name PROPOSED from your own scale - approve, edit, or leave ${proposed.length === 1 ? "it" : "them"} hard-coded:`);
284
+ for (const e of proposed.slice(0, 8))
285
+ lines.push(` ${e.value.padEnd(24)} ${e.path.padEnd(28)} ${e.proposed}`);
286
+ if (proposed.length > 8)
287
+ lines.push(` (${proposed.length - 8} more)`);
288
+ }
248
289
  if (pending.length > 0) {
249
290
  const close = pending.filter((e) => e.near);
250
291
  if (ready.length > 0)
@@ -262,7 +303,12 @@ export function describeAbsorb(plan) {
262
303
  ? `${pending.length} nobody names yet. Naming is a design decision, so those wait for a word from you:`
263
304
  : `${pending.length} ${everyOneRepeats ? "repeated values" : "values written by hand"}, and none of them has a name yet. Naming is a design decision, so each waits for a word from you:`);
264
305
  for (const e of pending.slice(0, 8))
265
- lines.push(` ${e.value.padEnd(24)} ${`<${e.kind}, ${e.files} file${e.files === 1 ? "" : "s"}>`.padEnd(28)} ${e.near ? `≈ your ${e.near.name} (${e.near.away})` : 'path: ""'}`);
306
+ lines.push(` ${e.value.padEnd(24)} ${`<${e.kind}, ${e.files} file${e.files === 1 ? "" : "s"}>`.padEnd(28)} ${e.near
307
+ ? `≈ your ${e.near.name} (${e.near.away})`
308
+ : /** A proposta chega COM a procedência: ele julga pela origem, não pela palavra. */
309
+ e.proposed
310
+ ? `${e.path} · ${e.proposed}`
311
+ : 'path: ""'}`);
266
312
  if (pending.length > 8)
267
313
  lines.push(` (${pending.length - 8} more)`);
268
314
  /**
@@ -12,7 +12,7 @@ import { installedThemeCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js
12
12
  import { flavourResolver } from "../styles-flavour.js";
13
13
  import { inTheirTongue, projectTongue, sumSpoken, } from "../their-tongue.js";
14
14
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
15
- import { recordWritten } from "../written.js";
15
+ import { editedSinceWritten, readWritten, recordWritten } from "../written.js";
16
16
  /**
17
17
  * Writes the shared `cn.ts` next to the components, built from THIS project's
18
18
  * installed theme.
@@ -233,6 +233,36 @@ export async function component(slug, name, opts) {
233
233
  * a mesma pergunta - qual nome este comando vai ocupar - e duas respostas para ela é o defeito.
234
234
  */
235
235
  const local = localName(res.name, opts.as);
236
+ /**
237
+ * A EDIÇÃO DELE NO QUE NÓS ESCREVEMOS - a mesma promessa do `upgrade`, no comando que a violava.
238
+ *
239
+ * O QUE ACONTECIA, caminhado em 07/09 com o binário publicado: gerei `btn2`, escrevi uma linha
240
+ * à mão no topo do arquivo, rodei `component codelevel button --as btn2` de novo - e a linha
241
+ * sumiu. Sem aviso, sem `--force`, sem uma frase.
242
+ *
243
+ * O GUARD QUE EXISTE ACIMA RESPONDE OUTRA PERGUNTA: *"você já tem este componente no seu
244
+ * repositório?"*, sobre o arquivo de ORIGEM (`recipe.api.file`), de onde o sistema foi
245
+ * importado. Ele nada sabe sobre o que NÓS escrevemos depois.
246
+ *
247
+ * E a informação existia o tempo todo: `recordWritten` grava o fingerprint de cada arquivo a
248
+ * cada escrita, e o `upgrade` a lê para dizer `kept YOUR file(s)`. Este comando gravava e nunca
249
+ * lia - a promessa valia num caminho e não no outro, e o outro é o mais percorrido: alguém traz
250
+ * o componente, ajusta, e roda de novo para pegar uma correção.
251
+ *
252
+ * A saída oferecida é a mesma que o `upgrade` oferece, e é a que ele já tem na mão: `--force`.
253
+ */
254
+ const slugDir = join(root, "_synthesisui", "ds", slug);
255
+ const editedHere = await editedSinceWritten((await readWritten(slugDir))[local], join(root, config.componentsDir, local));
256
+ if (editedHere && editedHere.length > 0 && !opts.force) {
257
+ console.log(section("You edited this one"));
258
+ console.log(body(`${editedHere.join(", ")} in ${config.componentsDir}/${local} changed after we wrote ${editedHere.length === 1 ? "it" : "them"} - kept YOUR file(s).`));
259
+ console.log("");
260
+ console.log(body("To take the new version anyway:"));
261
+ console.log(snippet([
262
+ `npx synthesisui component ${slug} ${res.name}${opts.as ? ` --as ${opts.as}` : ""} --force`,
263
+ ]));
264
+ return;
265
+ }
236
266
  const localPascal = local
237
267
  .split(/[^a-zA-Z0-9]+/)
238
268
  .filter(Boolean)
@@ -360,6 +360,26 @@ export function describeSignals(s) {
360
360
  else if (t.prefersColorScheme > 0) {
361
361
  out.push(`no dark utilities, but ${t.prefersColorScheme} \`prefers-color-scheme\` rule${t.prefersColorScheme === 1 ? "" : "s"} - the second scheme lives in CSS rather than in class names.`);
362
362
  }
363
+ else if (t.library) {
364
+ /**
365
+ * NENHUM SINAL DE SEGUNDO ESQUEMA, MAS UM ALTERNADOR INSTALADO - e as duas frases se negavam.
366
+ *
367
+ * MEDIDO EM 07/09 no `~/projects/web-subscribe`, caminhando o ATO 3: o relatório imprimia, em
368
+ * linhas CONSECUTIVAS, *"one scheme, and adding a second is a decision rather than a
369
+ * discovery"* e *"switched by next-themes"*. Quem lê recebe as duas ao mesmo tempo: não existe
370
+ * segundo esquema, e existe uma biblioteca que alterna entre esquemas.
371
+ *
372
+ * AS DUAS METADES ESTÃO CERTAS separadamente, e é isso que torna a contradição fácil de
373
+ * escrever: uma conta utilitários `dark:` e regras `prefers-color-scheme` no CSS; a outra lê o
374
+ * `package.json` e o provider. O que faltava era uma olhar para a outra.
375
+ *
376
+ * E O FATO COMBINADO É MAIS ÚTIL QUE QUALQUER UMA DAS DUAS: o alternador está instalado e não
377
+ * há nada para ele alternar. Isso é uma pendência do projeto dele - e é acionável, ao contrário
378
+ * de "adicionar um segundo esquema é uma decisão", que ele já tomou quando instalou a
379
+ * biblioteca.
380
+ */
381
+ out.push(`no dark utilities and no \`prefers-color-scheme\` - so the second scheme has nothing to paint yet, even though ${t.library} is installed to switch between them.`);
382
+ }
363
383
  else {
364
384
  out.push("no dark utilities and no `prefers-color-scheme` - one scheme, and adding a second is a decision rather than a discovery.");
365
385
  }
@@ -0,0 +1,121 @@
1
+ import { chroma, deltaE, lightness } from "./doctor/color-distance.js";
2
+ import { normalizeValue } from "./doctor/tokens.js";
3
+ function stepsOf(theirs) {
4
+ const out = [];
5
+ for (const [name, value] of theirs.byName) {
6
+ const m = /^--(.*[a-z0-9])-(\d{2,4})$/.exec(name.toLowerCase());
7
+ if (!m)
8
+ continue;
9
+ out.push({ family: m[1], step: Number(m[2]), value });
10
+ }
11
+ return out;
12
+ }
13
+ /**
14
+ * O DEGRAU QUE FALTA NA ESCALA DELE.
15
+ *
16
+ * A proposta só existe quando o valor cai ENTRE dois degraus vizinhos da MESMA família dele, e o
17
+ * degrau proposto é o ponto médio entre os dois. A palavra é inteiramente dele; só a posição é
18
+ * calculada.
19
+ *
20
+ * AS TRÊS PORTAS, e cada uma existe para o lado do "propor de menos":
21
+ *
22
+ * mesma família dois degraus de famílias diferentes não formam escala - `ink-700` e
23
+ * `brand-900` não têm um meio
24
+ * entre eles um valor fora do intervalo não tem degrau intermediário; extrapolar seria
25
+ * inventar a continuação da escala dele
26
+ * lugar livre se o degrau do meio já existe, o valor não é um degrau novo - é outra
27
+ * decisão que por acaso caiu perto, e propor sobrescreveria a dele
28
+ */
29
+ export function proposeFromTheirScale(literal, theirs) {
30
+ const l = lightness(normalizeValue(literal, theirs.rootPx));
31
+ if (l === null)
32
+ return null;
33
+ /**
34
+ * O CROMA ENTRA JUNTO COM A LUMINOSIDADE, e sem ele a derivação INVENTA.
35
+ *
36
+ * MEDIDO NA PRIMEIRA VERSÃO DISTO, em 07/09: um verde (`#0d5c2a`) recebeu `color.ink.800`, porque
37
+ * a luminosidade dele cai entre dois cinzas dele. Um degrau que falta numa escala está perto dos
38
+ * vizinhos NA COR, não só no claro-escuro - propor o contrário é dizer que o verde dele é um tom
39
+ * de cinza.
40
+ *
41
+ * O INTERVALO É O DOS PRÓPRIOS VIZINHOS, nunca um limiar escolhido: um cinza tem croma perto de
42
+ * zero e um verde não, então a porta se fecha sozinha. Numa escala de azul os dois vizinhos têm
43
+ * croma alto e o degrau novo passa. Sem folga, porque o lado seguro do erro é hard-coded.
44
+ */
45
+ const c = chroma(normalizeValue(literal, theirs.rootPx));
46
+ if (c === null)
47
+ return null;
48
+ const steps = stepsOf(theirs)
49
+ .map((s) => {
50
+ const v = normalizeValue(s.value, theirs.rootPx);
51
+ return { ...s, l: lightness(v), c: chroma(v) };
52
+ })
53
+ .filter((s) => s.l !== null && s.c !== null);
54
+ if (steps.length < 2)
55
+ return null;
56
+ let best = null;
57
+ for (const a of steps) {
58
+ for (const b of steps) {
59
+ if (a.family !== b.family || a.step >= b.step)
60
+ continue;
61
+ /** O valor tem de cair ENTRE os dois - extrapolar seria inventar a continuação da escala. */
62
+ const lo = Math.min(a.l, b.l);
63
+ const hi = Math.max(a.l, b.l);
64
+ if (l <= lo || l >= hi)
65
+ continue;
66
+ /** E na COR também: fora do croma dos dois vizinhos, não é um degrau desta escala. */
67
+ const cLo = Math.min(a.c, b.c);
68
+ const cHi = Math.max(a.c, b.c);
69
+ if (c < cLo || c > cHi)
70
+ continue;
71
+ const mid = Math.round((a.step + b.step) / 2 / 50) * 50;
72
+ if (mid === a.step || mid === b.step)
73
+ continue;
74
+ /** O degrau já existe: então este valor não é um degrau que falta, é outra decisão. */
75
+ if (steps.some((s) => s.family === a.family && s.step === mid))
76
+ continue;
77
+ const gap = b.step - a.step;
78
+ if (!best || gap < best.gap)
79
+ best = { family: a.family, step: mid, gap };
80
+ }
81
+ }
82
+ if (!best)
83
+ return null;
84
+ return {
85
+ path: `${best.family.replace(/-/g, ".")}.${best.step}`,
86
+ because: `between your ${best.family}-${best.step - Math.round(best.gap / 2)} and ${best.family}-${best.step + Math.round(best.gap / 2)}`,
87
+ };
88
+ }
89
+ /**
90
+ * PERTO DEMAIS DE UM TOKEN DELE NÃO É UM DEGRAU NOVO - é a mesma decisão escrita duas vezes.
91
+ *
92
+ * `nearestOwn` já diz isso na proposta do `absorb` (o `≈` que aparece na lista), e propor um nome
93
+ * ali empurraria para BATIZAR o que a leitura já diz para NORMALIZAR. As duas linhas se
94
+ * contradiriam na mesma tela.
95
+ */
96
+ export function tooCloseToPropose(literal, theirs, jnd = 2) {
97
+ const v = normalizeValue(literal, theirs.rootPx);
98
+ for (const value of theirs.byName.values()) {
99
+ const d = deltaE(v, normalizeValue(value, theirs.rootPx));
100
+ if (d !== null && d < jnd)
101
+ return true;
102
+ }
103
+ return false;
104
+ }
105
+ /**
106
+ * A SEGUNDA FONTE - ESPECIFICADA, TENTADA, E NÃO ENTREGUE. O motivo é medido, e ele é o valor deste
107
+ * comentário.
108
+ *
109
+ * A especificação da PARTE 2 (07/09, por entrevista) admite derivar *"da função observada no código
110
+ * dele - o valor aparece 12× em `background` e 0× em texto, então é proposto como uma superfície"*.
111
+ *
112
+ * O QUE A TENTATIVA ENCONTROU, no mesmo dia: `scanSource` reporta UMA ocorrência por literal POR
113
+ * LINHA. Numa linha com `background: "#7a1f1f"` e `borderColor: "#7a1f1f"`, ele devolve um achado -
114
+ * e a contagem por propriedade opera sobre uma amostra que não é a contagem real. A proposta então
115
+ * afirmaria *"todos os seus 2 usos são texto"* sobre um valor que também é borda.
116
+ *
117
+ * Uma afirmação falsa sobre o repositório DELE é exatamente o que esta parte existe para impedir, e
118
+ * é a razão de a fonte 2 não estar aqui. O pré-requisito está nomeado: o scan precisa reportar cada
119
+ * ocorrência, e não a primeira de cada literal por linha - o que muda a granularidade de tudo que
120
+ * consome `findings`, e por isso é uma frente própria, não uma linha a mais neste arquivo.
121
+ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.399",
3
+ "version": "0.16.401",
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": {