synthesisui 0.16.405 → 0.16.407
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/absorb-plan.js +129 -12
- package/dist/commands/absorb.js +46 -7
- package/dist/commands/doctor.js +1 -1
- package/dist/doctor/color-distance.js +22 -17
- package/dist/doctor/scan.js +2 -2
- package/dist/proposed-name.js +269 -12
- package/dist/use-by-property.js +239 -0
- package/package.json +1 -1
package/dist/absorb-plan.js
CHANGED
|
@@ -1,6 +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
|
+
import { proposeFromTheirScale, proposeFromTheirUse, proposeFromTheirValue, tooCloseToPropose, whyNotProposedFromUse, whyNotProposedFromValue, } from "./proposed-name.js";
|
|
4
4
|
/** Onde cada tipo de valor mora na fundação. `color` é o único com dois segmentos. */
|
|
5
5
|
const HOME = {
|
|
6
6
|
color: "color",
|
|
@@ -109,7 +109,20 @@ function nearestOwn(theirs, literal, kind) {
|
|
|
109
109
|
* `have` são os caminhos que a fundação já tem, para a proposta nunca sugerir sobrescrever um token
|
|
110
110
|
* existente com outro valor - isso não é absorver, é repintar.
|
|
111
111
|
*/
|
|
112
|
-
export function absorbPlan(d, theirs, have, cap
|
|
112
|
+
export function absorbPlan(d, theirs, have, cap,
|
|
113
|
+
/**
|
|
114
|
+
* A FUNÇÃO DE CADA COR NO CÓDIGO DELE, contada por `countUseByProperty` sobre os mesmos arquivos
|
|
115
|
+
* da varredura. OBRIGATÓRIO de propósito: um parâmetro opcional que dá para esquecer é a próxima
|
|
116
|
+
* ocorrência do defeito em que a tela promete o que a chamada não passou. Quem não tem a contagem
|
|
117
|
+
* passa um mapa vazio e a fonte 2 cala - explicitamente.
|
|
118
|
+
*/
|
|
119
|
+
uses,
|
|
120
|
+
/**
|
|
121
|
+
* A RAIZ MEDIDA no projeto dele, com a ambiguidade - e OBRIGATÓRIA pelo mesmo motivo que `uses`:
|
|
122
|
+
* `theirs.rootPx` carrega o número e perde o "não sei", e é o "não sei" que impede a proposta de
|
|
123
|
+
* afirmar `spacing.32` sobre um `2rem` de um projeto com duas raízes. Ver `unconvertible`.
|
|
124
|
+
*/
|
|
125
|
+
root) {
|
|
113
126
|
/**
|
|
114
127
|
* A REPETIÇÃO É UM FILTRO DE RUÍDO, E RUÍDO É UMA PROPRIEDADE DA ESCALA - não do valor.
|
|
115
128
|
*
|
|
@@ -136,6 +149,14 @@ export function absorbPlan(d, theirs, have, cap = 40) {
|
|
|
136
149
|
? [...repeated, ...single]
|
|
137
150
|
: repeated;
|
|
138
151
|
const entries = [];
|
|
152
|
+
/**
|
|
153
|
+
* OS CAMINHOS JÁ PROPOSTOS NESTA RODADA, por qualquer fonte. Dois literais diferentes podem cair
|
|
154
|
+
* na mesma posição (`#111111` e `#121212` são ambos luminosidade 7): o primeiro da lista - o mais
|
|
155
|
+
* repetido, porque é a ordem em que ele vê a lista - leva o caminho, e o segundo fica com o
|
|
156
|
+
* motivo. Sem isto os dois saíam com `color.background.7` e o `--send` postava dois tokens para o
|
|
157
|
+
* mesmo nome.
|
|
158
|
+
*/
|
|
159
|
+
const taken = new Map();
|
|
139
160
|
for (const r of unnamed) {
|
|
140
161
|
if (entries.length >= cap)
|
|
141
162
|
break;
|
|
@@ -194,19 +215,73 @@ export function absorbPlan(d, theirs, have, cap = 40) {
|
|
|
194
215
|
* entra - e ela devolve `null` na dúvida (ver `proposed-name.ts`).
|
|
195
216
|
*/
|
|
196
217
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
218
|
+
* DUAS FONTES, NESTA ORDEM (D4, dono, 08/09): a escala dele, e só onde ela cala, a FUNÇÃO que o
|
|
219
|
+
* valor exerce no código dele. Perto demais de um token dele veta as duas - ali a resposta é
|
|
220
|
+
* normalizar, não batizar. A segunda fonte só existe para cor: é a única natureza em que a
|
|
221
|
+
* propriedade escrita é uma função (`background`, `border`), e a única que a contagem lê.
|
|
199
222
|
*/
|
|
200
|
-
|
|
223
|
+
/**
|
|
224
|
+
* E SÓ PARA COR - por dois motivos distintos, e vale dizer os dois.
|
|
225
|
+
*
|
|
226
|
+
* A fonte 1 é escala de cor: medido no `frontend-hub` real em 08/09, ela propôs
|
|
227
|
+
* `dashboard.blue.650` para `30px`, `40px` e `50px`, porque `lightness("30px")` lia um prefixo
|
|
228
|
+
* hexadecimal de "3300pp" - consertado na raiz em `color-distance.ts`, e a porta fechada aqui
|
|
229
|
+
* também. A fonte 2 (a FUNÇÃO) é só para cor porque a medição de 08/09 mostrou que a função não
|
|
230
|
+
* separa espaçamento (`8px` é 409× padding e 303× margin no `web-subscribe`): comprimento é
|
|
231
|
+
* proposto pelo próprio VALOR, logo abaixo. O `≈` de vizinhança continua sendo o de `nearestOwn`.
|
|
232
|
+
*/
|
|
233
|
+
const colour = r.kind !== "color" || theirName || tooCloseToPropose(r.literal, theirs)
|
|
201
234
|
? null
|
|
202
|
-
: proposeFromTheirScale(r.literal, theirs)
|
|
203
|
-
|
|
235
|
+
: (proposeFromTheirScale(r.literal, theirs) ??
|
|
236
|
+
proposeFromTheirUse(r.literal, uses, theirs.rootPx, r.count));
|
|
204
237
|
/** "≈ perto de um token seu" só interessa a quem não tem um EXATO - com o nome na mão, a dica
|
|
205
238
|
* vira ruído, e no arquivo de proposta ela vira uma segunda opção que não é opção. */
|
|
206
239
|
const near = theirName ? undefined : nearestOwn(theirs, r.literal, r.kind);
|
|
240
|
+
/**
|
|
241
|
+
* ESPAÇO E RAIO: A TERCEIRA FONTE, e a ordem é a D5 (lead, 08/09) - nome dele > `≈` um token
|
|
242
|
+
* dele > o próprio valor. O `≈` vem ANTES da proposta porque um `15px` a 1px do `--spacing-md`
|
|
243
|
+
* dele é a mesma decisão escrita duas vezes, e propor `spacing.15` ali empurraria para BATIZAR
|
|
244
|
+
* o que a leitura já diz para NORMALIZAR. Ver `proposeFromTheirValue`.
|
|
245
|
+
*/
|
|
246
|
+
const length = (r.kind === "spacing" || r.kind === "radius") && !theirName && !near
|
|
247
|
+
? proposeFromTheirValue(r.literal, r.kind, uses, root, r.count)
|
|
248
|
+
: null;
|
|
249
|
+
const proposal = colour ?? length;
|
|
250
|
+
const own = pathFor(r.kind, theirName);
|
|
207
251
|
/** Já existe na fundação com OUTRO valor: absorver aqui seria repintar o token deles. */
|
|
208
|
-
if (
|
|
252
|
+
if (own && have.has(own))
|
|
209
253
|
continue;
|
|
254
|
+
/**
|
|
255
|
+
* COLISÃO DECLARADA, NUNCA DESCARTADA. Quando o caminho PROPOSTO já existe na fundação para
|
|
256
|
+
* outro valor, a primeira versão fazia `continue` - e a entrada sumia da lista enquanto a
|
|
257
|
+
* última linha do comando dizia "re-run and the next batch appears". Reproduzido pela revisão de
|
|
258
|
+
* DX em 08/09 em duas rodadas seguidas do `absorb`. A entrada FICA, sem caminho, e o motivo
|
|
259
|
+
* diz o que aconteceria: ele decide se é o mesmo token ou outro nome.
|
|
260
|
+
*/
|
|
261
|
+
const collided = proposal !== null && have.has(proposal.path);
|
|
262
|
+
/** EMPATE NA MESMA RODADA, declarado: só um leva o caminho, e o outro diz quem levou. */
|
|
263
|
+
const rival = proposal !== null && !collided ? taken.get(proposal.path) : undefined;
|
|
264
|
+
const path = own || (collided || rival ? "" : (proposal?.path ?? ""));
|
|
265
|
+
if (proposal !== null && !own && !collided && !rival)
|
|
266
|
+
taken.set(proposal.path, { literal: r.literal, count: r.count });
|
|
267
|
+
/**
|
|
268
|
+
* E QUANDO AS FONTES CALAM E NÃO HÁ VIZINHO, O CAMPO EM BRANCO DIZ POR QUÊ - em toda natureza.
|
|
269
|
+
* Cor: a função medida. Espaço e raio: a unidade, o degrau, o zero, o negativo. Motion: a
|
|
270
|
+
* contagem, e que duração não é proposta ainda. E o que nem chegou ao passe: o motivo genérico.
|
|
271
|
+
* Nenhuma linha em branco calada é a promessa da `INV-VOC-08`, estendida às outras naturezas
|
|
272
|
+
* em 08/09.
|
|
273
|
+
*/
|
|
274
|
+
const silent = !theirName && !proposal && !near;
|
|
275
|
+
const unproposed = collided
|
|
276
|
+
? `would be ${proposal?.path}, but you already named a different ${r.kind === "color" ? "colour" : "value"} that way`
|
|
277
|
+
: rival
|
|
278
|
+
? `would be ${proposal?.path}, but ${rival.literal} (${rival.count}×) takes that position this round - name one of them and the other follows`
|
|
279
|
+
: silent && r.kind === "color"
|
|
280
|
+
? whyNotProposedFromUse(r.literal, uses, theirs.rootPx, r.count)
|
|
281
|
+
: silent && r.kind !== "font"
|
|
282
|
+
? (whyNotProposedFromValue(r.literal, r.kind, uses, root, r.count) ??
|
|
283
|
+
"never inside a style property - declared or computed, not painted")
|
|
284
|
+
: null;
|
|
210
285
|
entries.push({
|
|
211
286
|
value: r.literal,
|
|
212
287
|
kind: r.kind,
|
|
@@ -215,8 +290,11 @@ export function absorbPlan(d, theirs, have, cap = 40) {
|
|
|
215
290
|
path,
|
|
216
291
|
...(theirName ? { theirName: clean(theirName) } : {}),
|
|
217
292
|
/** A PROCEDÊNCIA VIAJA COM A PROPOSTA - ele julga pela origem, nunca pela palavra. */
|
|
218
|
-
...(proposal
|
|
293
|
+
...(proposal && !collided && !rival
|
|
294
|
+
? { proposed: proposal.because }
|
|
295
|
+
: {}),
|
|
219
296
|
...(near ? { near } : {}),
|
|
297
|
+
...(unproposed ? { unproposed } : {}),
|
|
220
298
|
});
|
|
221
299
|
}
|
|
222
300
|
return {
|
|
@@ -225,6 +303,28 @@ export function absorbPlan(d, theirs, have, cap = 40) {
|
|
|
225
303
|
unnamed: d.repeats.length + d.once.length,
|
|
226
304
|
};
|
|
227
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* NO TAILWIND v4 O NOME É A UTILIDADE - e aprovar `spacing.16` muda o que `p-16` já significa.
|
|
308
|
+
*
|
|
309
|
+
* `absorb --send` no caminho "code" faz ele colar `@theme { --spacing-16: 16px }`, e na v4
|
|
310
|
+
* `--spacing-<n>` é o namespace que resolve `p-16`, `m-16`, `gap-16`, `w-16`, `size-16` e
|
|
311
|
+
* `space-y-16`. Com o padrão (`--spacing: 0.25rem`), `p-16` vale 64px; depois do paste vale 16px, e
|
|
312
|
+
* o projeto dele muda inteiro, calado. Todo degrau de 1 a 96 colide, sempre.
|
|
313
|
+
*
|
|
314
|
+
* A PLATAFORMA NÃO MUDA O NOME (decisão do lead, 08/09, reabrível): o degrau é o VALOR dele, e isso
|
|
315
|
+
* é a promessa da `INV-VOC-09`. Ela DECLARA a consequência quando a stack é v4 - `tailwindMajor` já
|
|
316
|
+
* sabe qual é -, na linha da proposta e no bloco que ele cola. Renomear é escolha dele, no arquivo.
|
|
317
|
+
*
|
|
318
|
+
* Uma frase, um lugar: a lista e o bloco a leem daqui, senão as duas telas divergem no dia em que
|
|
319
|
+
* alguém editar uma.
|
|
320
|
+
*/
|
|
321
|
+
export function spacingUtilitiesRedefined(paths) {
|
|
322
|
+
const hit = paths.find((p) => /^spacing\.\d+$/.test(p));
|
|
323
|
+
if (!hit)
|
|
324
|
+
return null;
|
|
325
|
+
const step = hit.slice("spacing.".length);
|
|
326
|
+
return `On Tailwind v4 the name IS the utility: --spacing-${step} is what p-${step}, m-${step}, gap-${step} and w-${step} read, so approving ${hit} changes what those already mean in your project. The value is yours - rename the step in the file if you want the utilities untouched.`;
|
|
327
|
+
}
|
|
228
328
|
/** Quantas linhas da proposta ainda precisam de um nome humano. */
|
|
229
329
|
export const needingName = (plan) => plan.entries.filter((e) => !e.path);
|
|
230
330
|
/**
|
|
@@ -234,7 +334,14 @@ export const needingName = (plan) => plan.entries.filter((e) => !e.path);
|
|
|
234
334
|
* um repo em migração já tem nome no vocabulário do próprio cliente, e essas viajam sem ninguém digitar
|
|
235
335
|
* nada.
|
|
236
336
|
*/
|
|
237
|
-
export function describeAbsorb(plan
|
|
337
|
+
export function describeAbsorb(plan,
|
|
338
|
+
/**
|
|
339
|
+
* A STACK DELE, e OBRIGATÓRIA de propósito: o aviso do namespace da v4 (ver
|
|
340
|
+
* `spacingUtilitiesRedefined`) é a metade que faz a proposta ser honesta naquele projeto, e um
|
|
341
|
+
* argumento opcional que dá para esquecer é a próxima ocorrência do defeito em que a tela promete
|
|
342
|
+
* o que a chamada não passou. `false` é resposta explícita.
|
|
343
|
+
*/
|
|
344
|
+
tailwind4) {
|
|
238
345
|
/**
|
|
239
346
|
* O QUE ELE JÁ NOMEIA E O QUE A PLATAFORMA PROPÔS SÃO DUAS LISTAS - e juntá-las é dizer que ele
|
|
240
347
|
* nomeou o que ele não nomeou.
|
|
@@ -280,11 +387,18 @@ export function describeAbsorb(plan) {
|
|
|
280
387
|
if (proposed.length > 0) {
|
|
281
388
|
if (ready.length > 0)
|
|
282
389
|
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:`);
|
|
390
|
+
lines.push(`${proposed.length} ${proposed.length === 1 ? "value has" : "values have"} a name PROPOSED from your own vocabulary - your scale, how you use the value, or the value itself - approve, edit, or leave ${proposed.length === 1 ? "it" : "them"} hard-coded:`);
|
|
284
391
|
for (const e of proposed.slice(0, 8))
|
|
285
392
|
lines.push(` ${e.value.padEnd(24)} ${e.path.padEnd(28)} ${e.proposed}`);
|
|
286
393
|
if (proposed.length > 8)
|
|
287
394
|
lines.push(` (${proposed.length - 8} more)`);
|
|
395
|
+
const redefined = tailwind4
|
|
396
|
+
? spacingUtilitiesRedefined(proposed.map((e) => e.path))
|
|
397
|
+
: null;
|
|
398
|
+
if (redefined) {
|
|
399
|
+
lines.push("");
|
|
400
|
+
lines.push(redefined);
|
|
401
|
+
}
|
|
288
402
|
}
|
|
289
403
|
if (pending.length > 0) {
|
|
290
404
|
const close = pending.filter((e) => e.near);
|
|
@@ -308,7 +422,10 @@ export function describeAbsorb(plan) {
|
|
|
308
422
|
: /** A proposta chega COM a procedência: ele julga pela origem, não pela palavra. */
|
|
309
423
|
e.proposed
|
|
310
424
|
? `${e.path} · ${e.proposed}`
|
|
311
|
-
:
|
|
425
|
+
: /** E o campo em branco chega com o motivo, quando a função medida o tem. */
|
|
426
|
+
e.unproposed
|
|
427
|
+
? `path: "" · ${e.unproposed}`
|
|
428
|
+
: 'path: ""'}`);
|
|
312
429
|
if (pending.length > 8)
|
|
313
430
|
lines.push(` (${pending.length - 8} more)`);
|
|
314
431
|
/**
|
package/dist/commands/absorb.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { absorbPlan, describeAbsorb, needingName, } from "../absorb-plan.js";
|
|
3
|
+
import { absorbPlan, describeAbsorb, needingName, spacingUtilitiesRedefined, } from "../absorb-plan.js";
|
|
4
4
|
import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
|
|
5
|
-
import {
|
|
5
|
+
import { DEFAULT_ROOT_PX, rootSizeOf } from "../doctor/root-size.js";
|
|
6
|
+
import { diagnose, IS_FIXTURE, scanSource, } from "../doctor/scan.js";
|
|
6
7
|
import { withTheirNames } from "../doctor/their-names.js";
|
|
7
8
|
import { measuredScope, scopePaths } from "../measured-scope.js";
|
|
8
9
|
import { body, paint, section, snippet } from "../output.js";
|
|
9
10
|
import { resolveDeps, tailwindMajor } from "../stack.js";
|
|
10
|
-
import {
|
|
11
|
+
import { countUseByProperty } from "../use-by-property.js";
|
|
12
|
+
import { loadSystem, sheetsIn, walkAll } from "./doctor.js";
|
|
11
13
|
/**
|
|
12
14
|
* `synthesisui absorb` - O SISTEMA APRENDE O DESIGN QUE O CÓDIGO JÁ TEM.
|
|
13
15
|
*
|
|
@@ -33,7 +35,26 @@ export async function absorb(opts) {
|
|
|
33
35
|
const path = join(root, FILE);
|
|
34
36
|
if (opts.send)
|
|
35
37
|
return sendProposal(root, path, opts.registry);
|
|
36
|
-
|
|
38
|
+
/**
|
|
39
|
+
* A RAIZ, MEDIDA ANTES DE PROPOR NOME NENHUM - e sem isto este comando AFIRMA um número falso.
|
|
40
|
+
*
|
|
41
|
+
* Este comando chamava `loadSystem(root)` sem a raiz medida, então `1rem` valia 16px sempre. Até a
|
|
42
|
+
* Parte 2 isso só o fazia CASAR menos - um `0.25rem` dele não achava o token em `4px`, e o erro
|
|
43
|
+
* era por omissão. A proposta pelo valor (`INV-VOC-09`) transformou a omissão em afirmação: num
|
|
44
|
+
* projeto que escreve `html { font-size: 62.5% }`, um `padding: 2rem` saía como `spacing.32` e
|
|
45
|
+
* *"32px, used 2× as margin and padding"* - e `2rem` ali vale 20px. Um nome errado e um número
|
|
46
|
+
* falso no terminal dele, sobre o repositório dele. Medido no comando real em 08/09.
|
|
47
|
+
*
|
|
48
|
+
* A medição é a MESMA do `doctor` (`rootSizeOf` sobre as folhas de estilo, só o bloco
|
|
49
|
+
* `html`/`:root`), sobre a RAIZ do projeto - que é a população de onde `loadSystem` colhe o
|
|
50
|
+
* vocabulário dele, mesmo quando a leitura é escopada. Ambíguo (duas raízes, um `calc()`) cai no
|
|
51
|
+
* padrão do navegador, como no `doctor`: quem decide isso é `root-size.ts`, não este comando.
|
|
52
|
+
*/
|
|
53
|
+
const rootSize = rootSizeOf(await sheetsIn([root]));
|
|
54
|
+
const installed = await loadSystem(root, {
|
|
55
|
+
px: rootSize.ambiguous ? DEFAULT_ROOT_PX : rootSize.px,
|
|
56
|
+
from: rootSize.from,
|
|
57
|
+
});
|
|
37
58
|
/**
|
|
38
59
|
* SEM SISTEMA INSTALADO ISTO CONTINUA VALENDO - e recusar aqui fechava o dia 1.
|
|
39
60
|
*
|
|
@@ -85,19 +106,30 @@ export async function absorb(opts) {
|
|
|
85
106
|
? installed.table
|
|
86
107
|
: withTheirNames(theirs, theirs);
|
|
87
108
|
const reports = [];
|
|
109
|
+
/**
|
|
110
|
+
* A FUNÇÃO DE CADA COR, contada num passe próprio sobre os MESMOS arquivos - é a segunda fonte do
|
|
111
|
+
* nome proposto (`proposed-name.ts`). Fixture e teste ficam de fora pela mesma regra do scan: as
|
|
112
|
+
* cores de um `.stories` são do exemplo, não do produto dele.
|
|
113
|
+
*/
|
|
114
|
+
const uses = new Map();
|
|
88
115
|
for await (const file of walkAll(roots)) {
|
|
89
116
|
const source = await readFile(file, "utf8").catch(() => null);
|
|
90
117
|
if (source === null)
|
|
91
118
|
continue;
|
|
92
|
-
|
|
119
|
+
const rel = file.slice(root.length + 1);
|
|
120
|
+
reports.push(scanSource(rel, source, table));
|
|
121
|
+
if (!IS_FIXTURE.test(rel))
|
|
122
|
+
countUseByProperty(source, uses, rootSize);
|
|
93
123
|
}
|
|
94
|
-
const plan = absorbPlan(diagnose(reports), theirs, pathsInSystem(installed.documents), opts.cap ?? 40);
|
|
124
|
+
const plan = absorbPlan(diagnose(reports), theirs, pathsInSystem(installed.documents), opts.cap ?? 40, uses, rootSize);
|
|
95
125
|
console.log(section(hasSystem
|
|
96
126
|
? "What the system could absorb"
|
|
97
127
|
: "Name what you wrote by hand"));
|
|
98
128
|
console.log(body(paint.dim(`read from ${rel.length > 0 ? rel.join(", ") : "this project"} · your own vocabulary read from the root`)));
|
|
99
129
|
console.log("");
|
|
100
|
-
|
|
130
|
+
/** A stack dele decide uma frase da lista - ver `spacingUtilitiesRedefined`. */
|
|
131
|
+
const major = tailwindMajor(await resolveDeps(root));
|
|
132
|
+
for (const line of describeAbsorb(plan, major === 4))
|
|
101
133
|
console.log(body(line));
|
|
102
134
|
if (plan.entries.length === 0)
|
|
103
135
|
return;
|
|
@@ -215,6 +247,13 @@ async function sendProposal(root, path, registry) {
|
|
|
215
247
|
for (const e of ready)
|
|
216
248
|
console.log(` --${e.path.replace(/\./g, "-")}: ${e.value};`);
|
|
217
249
|
console.log(" }");
|
|
250
|
+
/** E O QUE ESSE PASTE MUDA NO PROJETO DELE, dito antes de ele colar - ver
|
|
251
|
+
* `spacingUtilitiesRedefined`. Na v4 o nome É a utilidade. */
|
|
252
|
+
const redefined = major === 4 ? spacingUtilitiesRedefined(ready.map((e) => e.path)) : null;
|
|
253
|
+
if (redefined) {
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log(body(redefined));
|
|
256
|
+
}
|
|
218
257
|
console.log("");
|
|
219
258
|
console.log(body(major === 4
|
|
220
259
|
? "In `@theme` and not `:root`: on Tailwind v4 only `@theme` generates the utility, so this is what makes `text-ink-700` exist in your code."
|
package/dist/commands/doctor.js
CHANGED
|
@@ -385,7 +385,7 @@ measured) {
|
|
|
385
385
|
* common path pays nothing for it.
|
|
386
386
|
*/
|
|
387
387
|
/** As folhas de estilo dele, para a medição da raiz. Só css - nada de `.tsx`. */
|
|
388
|
-
async function sheetsIn(roots) {
|
|
388
|
+
export async function sheetsIn(roots) {
|
|
389
389
|
const out = [];
|
|
390
390
|
for await (const file of walkAll(roots)) {
|
|
391
391
|
if (!/\.(css|scss|sass|less)$/i.test(file))
|
|
@@ -97,20 +97,34 @@ export function isNearDuplicate(a, b, threshold = JND) {
|
|
|
97
97
|
* the two halves deciding differently what counts as a dark canvas is worse
|
|
98
98
|
* than either of them being wrong.
|
|
99
99
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
/**
|
|
101
|
+
* OS SEIS DÍGITOS DE UMA COR, OU NADA - e o "nada" é o que faltava.
|
|
102
|
+
*
|
|
103
|
+
* `lightness("30px")` devolvia 0.143: "30px" tem 4 caracteres, era expandido para "3300pp", e
|
|
104
|
+
* `parseInt("3300pp", 16)` lê o prefixo hexadecimal em vez de devolver NaN. Efeito medido no
|
|
105
|
+
* `frontend-hub` real em 08/09, pelo `absorb`: a fonte 1 propôs `dashboard.blue.650` para `30px`
|
|
106
|
+
* (espaçamento), `40px` e `50px` (raio) - "between your dashboard-blue-500 and dashboard-blue-800" -,
|
|
107
|
+
* um nome de cor sobre um comprimento. `rgb()`/`hsl()`/`oklch()` não chegam aqui: `normalizeValue`
|
|
108
|
+
* os converte para `#rrggbbaa` antes; o que chega e NÃO é cor é comprimento, tempo ou texto, e a
|
|
109
|
+
* resposta certa para eles é `null`.
|
|
110
|
+
*/
|
|
111
|
+
function sixDigits(hex) {
|
|
112
|
+
const h = hex.trim().replace(/^#/, "");
|
|
113
|
+
if (!/^[0-9a-f]{3,4}$|^[0-9a-f]{6}$|^[0-9a-f]{8}$/i.test(h))
|
|
114
|
+
return null;
|
|
115
|
+
return h.length === 3 || h.length === 4
|
|
103
116
|
? h
|
|
104
117
|
.slice(0, 3)
|
|
105
118
|
.split("")
|
|
106
119
|
.map((c) => c + c)
|
|
107
120
|
.join("")
|
|
108
121
|
: h.slice(0, 6);
|
|
109
|
-
|
|
122
|
+
}
|
|
123
|
+
export function lightness(hex) {
|
|
124
|
+
const full = sixDigits(hex);
|
|
125
|
+
if (full === null)
|
|
110
126
|
return null;
|
|
111
127
|
const n = Number.parseInt(full, 16);
|
|
112
|
-
if (Number.isNaN(n))
|
|
113
|
-
return null;
|
|
114
128
|
return ((0.2126 * ((n >> 16) & 255) +
|
|
115
129
|
0.7152 * ((n >> 8) & 255) +
|
|
116
130
|
0.0722 * (n & 255)) /
|
|
@@ -120,19 +134,10 @@ export function lightness(hex) {
|
|
|
120
134
|
* chromatic, and treating it as coloured throws every off-white surface out of
|
|
121
135
|
* the neutral ladder. */
|
|
122
136
|
export function chroma(hex) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
? h
|
|
126
|
-
.slice(0, 3)
|
|
127
|
-
.split("")
|
|
128
|
-
.map((c) => c + c)
|
|
129
|
-
.join("")
|
|
130
|
-
: h.slice(0, 6);
|
|
131
|
-
if (full.length !== 6)
|
|
137
|
+
const full = sixDigits(hex);
|
|
138
|
+
if (full === null)
|
|
132
139
|
return null;
|
|
133
140
|
const n = Number.parseInt(full, 16);
|
|
134
|
-
if (Number.isNaN(n))
|
|
135
|
-
return null;
|
|
136
141
|
const r = (n >> 16) & 255;
|
|
137
142
|
const g = (n >> 8) & 255;
|
|
138
143
|
const b = n & 255;
|
package/dist/doctor/scan.js
CHANGED
|
@@ -54,7 +54,7 @@ const IGNORE_LINE = /^\s*(import|@import|\/\/|\*|\/\*)/;
|
|
|
54
54
|
* VISTO pelo scanner e INTERPRETADO por `colorForm` para chegar a uma comparação. As duas metades
|
|
55
55
|
* entraram na mesma mudança porque meia jornada não é meio valor.
|
|
56
56
|
*/
|
|
57
|
-
const COLOR = /#[0-9a-fA-F]{8}\b|#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b|rgba?\([^)]*\)|hsla?\([^)]*\)|oklch\([^)]*\)/g;
|
|
57
|
+
export const COLOR = /#[0-9a-fA-F]{8}\b|#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b|rgba?\([^)]*\)|hsla?\([^)]*\)|oklch\([^)]*\)/g;
|
|
58
58
|
/**
|
|
59
59
|
* THE QUOTE THAT MADE HALF THE DRIFT INVISIBLE.
|
|
60
60
|
*
|
|
@@ -386,7 +386,7 @@ const IDIOM = new Set(["0", "0px", "1px", "9999px", "100%", "50%"]);
|
|
|
386
386
|
* Counting them accuses the author of drift they did not write, and crediting
|
|
387
387
|
* them inflates a coverage number nobody can act on.
|
|
388
388
|
*/
|
|
389
|
-
const IS_FIXTURE = /(\.(spec|test|stories)\.[a-z]+$|__tests__\/|\/fixtures?\/|(^|\/)\.storybook\/)/;
|
|
389
|
+
export const IS_FIXTURE = /(\.(spec|test|stories)\.[a-z]+$|__tests__\/|\/fixtures?\/|(^|\/)\.storybook\/)/;
|
|
390
390
|
export function scanSource(file, source, table) {
|
|
391
391
|
if (IS_FIXTURE.test(file)) {
|
|
392
392
|
const real = scanCore(file, source, table);
|
package/dist/proposed-name.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { chroma, deltaE, lightness } from "./doctor/color-distance.js";
|
|
2
2
|
import { normalizeValue } from "./doctor/tokens.js";
|
|
3
|
+
import { lengthKey, ROOTS_OF_KIND, SVG, } from "./use-by-property.js";
|
|
3
4
|
function stepsOf(theirs) {
|
|
4
5
|
const out = [];
|
|
5
6
|
for (const [name, value] of theirs.byName) {
|
|
@@ -102,20 +103,276 @@ export function tooCloseToPropose(literal, theirs, jnd = 2) {
|
|
|
102
103
|
}
|
|
103
104
|
return false;
|
|
104
105
|
}
|
|
106
|
+
/** O que o mapa diz deste literal: as funções, mais usada primeiro, e quantas vezes ao todo. */
|
|
107
|
+
function usesOf(literal, uses, rootPx) {
|
|
108
|
+
const key = normalizeValue(literal, rootPx);
|
|
109
|
+
const per = uses.get(key);
|
|
110
|
+
if (!per)
|
|
111
|
+
return null;
|
|
112
|
+
const entries = [...per.entries()].filter(([, n]) => n > 0);
|
|
113
|
+
if (entries.length === 0)
|
|
114
|
+
return null;
|
|
115
|
+
return {
|
|
116
|
+
key,
|
|
117
|
+
/** Mais usada primeiro; no empate, a ordem alfabética - a frase não pode depender de qual
|
|
118
|
+
* arquivo foi lido antes. */
|
|
119
|
+
per: entries.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)),
|
|
120
|
+
total: entries.reduce((n, [, c]) => n + c, 0),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A POSIÇÃO É A LUMINOSIDADE DO PRÓPRIO VALOR, em 0..100 - estável entre rodadas, e é isso que a
|
|
125
|
+
* torna um NOME e não um rank.
|
|
126
|
+
*
|
|
127
|
+
* A primeira versão ordenava os literais da mesma função e dava `.1` ao mais escuro. Reproduzido
|
|
128
|
+
* pela revisão de DX em 08/09, rodando o `absorb` duas vezes (o fluxo que o próprio teto de 40
|
|
129
|
+
* recomenda): rodada 1, `#111111` é o mais escuro de dois, sai `color.background.1` e é enviado;
|
|
130
|
+
* rodada 2, aparece `#000000`, mais escuro - ele vira `.1`, colide com o nome já dado, e era
|
|
131
|
+
* descartado em silêncio enquanto a mensagem final dizia "re-run and the next batch appears". Um
|
|
132
|
+
* rank colide sistematicamente com qualquer valor novo mais escuro que os já nomeados.
|
|
133
|
+
*
|
|
134
|
+
* `Math.round(lightness × 100)` do próprio valor não depende de vizinho nenhum: `#111111` é `.7`
|
|
135
|
+
* hoje e em qualquer rodada, `#000000` é `.0`, `#fafafa` é `.98`. Refinamento da D3 (o espírito é
|
|
136
|
+
* "posição por luminosidade; só a posição é calculada"), decisão de implementação de 08/09,
|
|
137
|
+
* reabrível pelo dono.
|
|
138
|
+
*/
|
|
139
|
+
function positionOf(key) {
|
|
140
|
+
const l = lightness(key);
|
|
141
|
+
return l === null ? null : Math.round(l * 100);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* O NOME PELA FUNÇÃO MEDIDA: `color.<propriedade-dele>.<luminosidade 0..100>`.
|
|
145
|
+
*
|
|
146
|
+
* `counted` é quantas ocorrências o SCAN viu para este literal (`r.count` no plano). Se o passe
|
|
147
|
+
* alcançou menos do que isso, parte dos usos está fora de qualquer propriedade de estilo e a
|
|
148
|
+
* contagem é parcial: `unknown` é estado, e uma contagem parcial não vira afirmação - nem "uma
|
|
149
|
+
* função só", nem "dois trabalhos".
|
|
150
|
+
*
|
|
151
|
+
* Só fala onde `theirName` e `proposeFromTheirScale` calam - a ordem é de `absorb-plan.ts` (D4).
|
|
152
|
+
* O `because` carrega a contagem E a posição, para ele julgar pela origem e não pela palavra:
|
|
153
|
+
* *"12× in background, never anywhere else · lightness 7 of 100"*.
|
|
154
|
+
*/
|
|
155
|
+
export function proposeFromTheirUse(literal, uses, rootPx, counted) {
|
|
156
|
+
const u = usesOf(literal, uses, rootPx);
|
|
157
|
+
if (!u || u.total < counted)
|
|
158
|
+
return null;
|
|
159
|
+
if (u.per.length !== 1 || u.total < 2)
|
|
160
|
+
return null;
|
|
161
|
+
const root = u.per[0]?.[0] ?? "";
|
|
162
|
+
if (root === SVG)
|
|
163
|
+
return null;
|
|
164
|
+
const position = positionOf(u.key);
|
|
165
|
+
if (position === null)
|
|
166
|
+
return null;
|
|
167
|
+
return {
|
|
168
|
+
path: `color.${root}.${position}`,
|
|
169
|
+
because: `${u.total}× in ${root}, never anywhere else · lightness ${position} of 100`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const WORDS = [
|
|
173
|
+
"",
|
|
174
|
+
"one",
|
|
175
|
+
"two",
|
|
176
|
+
"three",
|
|
177
|
+
"four",
|
|
178
|
+
"five",
|
|
179
|
+
"six",
|
|
180
|
+
"seven",
|
|
181
|
+
"eight",
|
|
182
|
+
"nine",
|
|
183
|
+
];
|
|
184
|
+
/**
|
|
185
|
+
* POR QUE O CAMPO FICOU EM BRANCO, em uma linha - a lacuna declarada em vez de calada.
|
|
186
|
+
*
|
|
187
|
+
* *"4× background · 1× border - two jobs, no single name"* é a leitura que o cliente consegue agir
|
|
188
|
+
* sobre: ou o valor é dois tokens, ou é um e ele diz qual. A linha de motivo é verdadeira nas MESMAS
|
|
189
|
+
* condições da proposta - e quando a contagem não alcança, ela diz que não alcança.
|
|
190
|
+
*
|
|
191
|
+
* NUNCA MENTE SOZINHA: devolve `null` quando `proposeFromTheirUse` propõe para o mesmo literal, para
|
|
192
|
+
* que o guard não more só em quem chama.
|
|
193
|
+
*
|
|
194
|
+
* E NENHUMA COR FICA EM BRANCO CALADA. Medido no `web-subscribe` real em 08/09: de 644 cores, 55
|
|
195
|
+
* saíam sem contagem nenhuma - `let strokes = ['#26bcfd', …]`, `export const boulder = '#797979'`,
|
|
196
|
+
* `@mixin nav-divider($color: #e5e5e5)` - e o campo em branco não dizia nada. Um valor que nunca
|
|
197
|
+
* está dentro de uma propriedade de estilo não exerce função: é declarado ou calculado, não pintado,
|
|
198
|
+
* e isso é um motivo, não um silêncio. Ou a cor tem proposta, ou tem `≈`, ou tem motivo.
|
|
199
|
+
*/
|
|
200
|
+
export function whyNotProposedFromUse(literal, uses, rootPx, counted) {
|
|
201
|
+
if (proposeFromTheirUse(literal, uses, rootPx, counted))
|
|
202
|
+
return null;
|
|
203
|
+
const u = usesOf(literal, uses, rootPx);
|
|
204
|
+
if (!u)
|
|
205
|
+
return "never inside a style property - declared or computed, not painted";
|
|
206
|
+
if (u.total < counted)
|
|
207
|
+
return `read ${u.total} of ${counted} uses - the rest sit outside a style property`;
|
|
208
|
+
if (u.per.length > 1) {
|
|
209
|
+
const jobs = WORDS[u.per.length] ?? String(u.per.length);
|
|
210
|
+
return `${u.per.map(([root, n]) => `${n}× ${root}`).join(" · ")} - ${jobs} jobs, no single name`;
|
|
211
|
+
}
|
|
212
|
+
if (u.total < 2)
|
|
213
|
+
return "seen once";
|
|
214
|
+
const root = u.per[0]?.[0] ?? "";
|
|
215
|
+
if (root === SVG)
|
|
216
|
+
return "only in svg";
|
|
217
|
+
return `${u.total}× in ${root}, but the value does not read as a colour`;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* A TERCEIRA FONTE - O PRÓPRIO VALOR, para espaço e raio. Especificada em 08/09
|
|
221
|
+
* (`steps/03-parte-3-espacamento-na-lista.md`), depois de a medição descartar a função como
|
|
222
|
+
* separadora de espaçamento: no `web-subscribe` real, `8px` é 409× padding, 303× margin e 18× gap -
|
|
223
|
+
* um degrau de espaço é usado em padding E margin por definição, e "fonte 2 para spacing" proporia
|
|
224
|
+
* na cauda e calaria na cabeça, exatamente onde ele olha.
|
|
225
|
+
*
|
|
226
|
+
* O QUE O CLIENTE GANHA: depois da cor, a primeira tela do `absorb` no `web-subscribe` tinha 24 de 40
|
|
227
|
+
* linhas de espaçamento em branco e SEM motivo. Agora `16px` chega como `spacing.16` com *"16px, used
|
|
228
|
+
* 642× as padding, margin and gap - one step of your spacing"*; ele renomeia na revisão (`spacing.md`)
|
|
229
|
+
* ou deixa hard-coded. E `1em` chega com o motivo: depende da fonte de cada elemento, não há degrau
|
|
230
|
+
* fixo.
|
|
231
|
+
*
|
|
232
|
+
* O CAMINHO É O PRÓPRIO VALOR (D2): é o único nome derivável do repositório dele que é VERDADEIRO. A
|
|
233
|
+
* palavra `spacing`/`radius` é a casa que o produto já usa nos caminhos dos nomes DELE (`HOME[kind]`
|
|
234
|
+
* em `absorb-plan.ts`) - não é uma palavra nossa sobre o valor, é onde o valor mora. `sm`, `md`,
|
|
235
|
+
* `tight` seriam invenção, e não entram nem como sugestão.
|
|
236
|
+
*
|
|
237
|
+
* AS PORTAS (D2-D4), todas para o lado do "propor de menos":
|
|
238
|
+
*
|
|
239
|
+
* natureza só `spacing` e `radius`; motion tem motivo e não tem proposta (fora, declarado)
|
|
240
|
+
* unidade FIXA px ou rem; `em` vale 16px só quando a fonte do elemento é a da raiz - é a regra
|
|
241
|
+
* do `fontRelative` que o `--fix` já segue (D3)
|
|
242
|
+
* degrau INTEIRO `3.2px` (de `0.2em` ou `0.2rem`) não é um degrau (D4)
|
|
243
|
+
* positivo zero não é degrau; negativo não é degrau (D4)
|
|
244
|
+
* contado o valor chegou ao passe em alguma função da natureza certa - sem contagem não
|
|
245
|
+
* há distribuição para dizer, e o motivo é o genérico do plano
|
|
246
|
+
*/
|
|
247
|
+
/** `16px` → 16 · `1rem` → 16 · `0.2rem` → 3.2 · não-comprimento → null. */
|
|
248
|
+
function pxOf(literal, rootPx) {
|
|
249
|
+
const m = /^(-?\d*\.?\d+)px$/.exec(normalizeValue(literal, rootPx));
|
|
250
|
+
return m ? Number.parseFloat(m[1] ?? "") : null;
|
|
251
|
+
}
|
|
252
|
+
const isEm = (literal) => /(?<!r)em$/i.test(literal.trim());
|
|
253
|
+
const isRem = (literal) => /rem$/i.test(literal.trim());
|
|
105
254
|
/**
|
|
106
|
-
*
|
|
107
|
-
* comentário.
|
|
255
|
+
* RAIZ SEM UMA RESPOSTA NÃO CONVERTE `rem` - e é o mesmo lado do erro que o `doctor` já escolheu.
|
|
108
256
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
257
|
+
* Duas declarações de `font-size` na raiz (ou uma que não dá para ler) e `1rem` deixa de ter um
|
|
258
|
+
* valor em px: propor `spacing.32` ali seria afirmar uma conversão que a plataforma não sabe fazer.
|
|
259
|
+
* `px` continua propondo, porque px não depende da raiz de nada. Decisão de implementação do lead
|
|
260
|
+
* (08/09), reabrível pelo dono; a régua que decide o que é ambíguo é `root-size.ts`, não esta.
|
|
261
|
+
*/
|
|
262
|
+
const unconvertible = (literal, root) => Boolean(root.ambiguous) && isRem(literal);
|
|
263
|
+
/** As funções da natureza certa em que o valor aparece, mais usada primeiro, com o total. */
|
|
264
|
+
function lengthUses(literal, kind, uses, root) {
|
|
265
|
+
const roots = ROOTS_OF_KIND[kind];
|
|
266
|
+
if (!roots)
|
|
267
|
+
return null;
|
|
268
|
+
const per = uses.get(lengthKey(literal, root));
|
|
269
|
+
if (!per)
|
|
270
|
+
return null;
|
|
271
|
+
const entries = [...per.entries()]
|
|
272
|
+
.filter(([root, n]) => n > 0 && roots.includes(root))
|
|
273
|
+
.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
|
|
274
|
+
if (entries.length === 0)
|
|
275
|
+
return null;
|
|
276
|
+
return { per: entries, total: entries.reduce((n, [, c]) => n + c, 0) };
|
|
277
|
+
}
|
|
278
|
+
/** `padding, margin and gap` · `padding and margin` · `padding`. */
|
|
279
|
+
const listed = (roots) => roots.length <= 1
|
|
280
|
+
? (roots[0] ?? "")
|
|
281
|
+
: `${roots.slice(0, -1).join(", ")} and ${roots[roots.length - 1]}`;
|
|
282
|
+
/**
|
|
283
|
+
* O NOME PELO PRÓPRIO VALOR: `spacing.16`, `radius.6`.
|
|
111
284
|
*
|
|
112
|
-
* O
|
|
113
|
-
*
|
|
114
|
-
|
|
115
|
-
|
|
285
|
+
* O `because` carrega a distribuição por função, contada por ocorrência no mesmo passe da cor, para
|
|
286
|
+
* ele julgar pela origem: *"16px, used 642× as padding, margin and gap - one step of your spacing"*.
|
|
287
|
+
*/
|
|
288
|
+
export function proposeFromTheirValue(literal, kind, uses,
|
|
289
|
+
/**
|
|
290
|
+
* A RAIZ MEDIDA, com a ambiguidade - o objeto inteiro, e não os pixels soltos: a ambiguidade é
|
|
291
|
+
* parte da medição, e perdê-la no caminho é o que fazia um `2rem` sair `spacing.32` num projeto
|
|
292
|
+
* cuja raiz não tem uma resposta. Ver `unconvertible`.
|
|
293
|
+
*/
|
|
294
|
+
root,
|
|
295
|
+
/**
|
|
296
|
+
* QUANTAS OCORRÊNCIAS O SCAN VIU - e a proposta cala quando o passe alcançou MENOS: a
|
|
297
|
+
* distribuição seria uma afirmação sobre um pedaço. É a mesma metade `unknown` que a cor já tem
|
|
298
|
+
* (`INV-VOC-08`), e é obrigatória por isso.
|
|
299
|
+
*/
|
|
300
|
+
counted) {
|
|
301
|
+
if (kind !== "spacing" && kind !== "radius")
|
|
302
|
+
return null;
|
|
303
|
+
if (isEm(literal) || unconvertible(literal, root))
|
|
304
|
+
return null;
|
|
305
|
+
const px = pxOf(literal, root.px);
|
|
306
|
+
if (px === null || !Number.isInteger(px) || px <= 0)
|
|
307
|
+
return null;
|
|
308
|
+
const u = lengthUses(literal, kind, uses, root);
|
|
309
|
+
/**
|
|
310
|
+
* DUAS OCORRÊNCIAS, A MESMA RÉGUA DA COR (D2 do dono, 07/09: 100% e >= 2, *"na dúvida,
|
|
311
|
+
* hard-coded"*). Uma cor vista uma vez sai "seen once" sem proposta; um comprimento visto uma vez
|
|
312
|
+
* saía `spacing.3 · "one step of your spacing"` - duas réguas de repetição na mesma tela, e a
|
|
313
|
+
* mais frouxa era a que propunha. A D2 é do dono; estendê-la a comprimento é decisão do lead
|
|
314
|
+
* (08/09), reabrível.
|
|
315
|
+
*/
|
|
316
|
+
if (!u || u.total < 2 || u.total < counted)
|
|
317
|
+
return null;
|
|
318
|
+
return {
|
|
319
|
+
path: `${kind}.${px}`,
|
|
320
|
+
because: `${px}px, used ${u.total}× as ${listed(u.per.map(([root]) => root))} - one step of your ${kind}`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* POR QUE NÃO HÁ PROPOSTA PELO VALOR, em uma linha - e nunca `null` quando há contagem.
|
|
116
325
|
*
|
|
117
|
-
*
|
|
118
|
-
* é a
|
|
119
|
-
*
|
|
120
|
-
*
|
|
326
|
+
* *"font-relative (em): 597× across padding and margin - depends on each element's font, so no fixed
|
|
327
|
+
* step"* é a leitura que ele consegue agir sobre. `counted` é o que o scan viu, e serve a motion, que
|
|
328
|
+
* o passe não conta. `null` só quando o literal nem chegou ao passe - aí o plano usa o motivo
|
|
329
|
+
* genérico *"never inside a style property…"*. Nunca mente sozinha: devolve `null` onde
|
|
330
|
+
* `proposeFromTheirValue` propõe.
|
|
121
331
|
*/
|
|
332
|
+
export function whyNotProposedFromValue(literal, kind, uses, root, counted) {
|
|
333
|
+
if (kind === "motion")
|
|
334
|
+
return `${counted}× - durations are not proposed yet`;
|
|
335
|
+
if (proposeFromTheirValue(literal, kind, uses, root, counted))
|
|
336
|
+
return null;
|
|
337
|
+
if (kind !== "spacing" && kind !== "radius")
|
|
338
|
+
return null;
|
|
339
|
+
/**
|
|
340
|
+
* A RAIZ AMBÍGUA É O MOTIVO ANTES DE QUALQUER CONTAGEM - ela não depende de quantas vezes ele
|
|
341
|
+
* escreveu o valor, e dizer "read 0 of 4 uses" ali culparia a propriedade por uma lacuna que é da
|
|
342
|
+
* raiz. Ver `unconvertible`.
|
|
343
|
+
*/
|
|
344
|
+
if (unconvertible(literal, root)) {
|
|
345
|
+
const saw = root.ambiguous?.saw.length ?? 0;
|
|
346
|
+
return `your root font-size is not one answer (${saw} declaration${saw === 1 ? "" : "s"}) - px and rem are not compared here, so no fixed step from ${literal.trim().toLowerCase()}`;
|
|
347
|
+
}
|
|
348
|
+
const u = lengthUses(literal, kind, uses, root);
|
|
349
|
+
/**
|
|
350
|
+
* O PASSE NÃO ALCANÇOU O QUE O SCAN CONTOU: `unknown` é estado, e a frase diz qual é a lacuna.
|
|
351
|
+
*
|
|
352
|
+
* `scroll-padding: 18px`, `grid-gap: 22px` e `scroll-margin: 26px` chegam ao scan e não a este
|
|
353
|
+
* passe - a propriedade não está nas que ele lê. O motivo genérico dizia *"never inside a style
|
|
354
|
+
* property - declared or computed, not painted"*, falso em dois pontos: o valor ESTÁ numa
|
|
355
|
+
* propriedade de estilo, e "painted" é vocabulário de cor numa linha de espaçamento.
|
|
356
|
+
*/
|
|
357
|
+
if ((u?.total ?? 0) < counted)
|
|
358
|
+
return `read ${u?.total ?? 0} of ${counted} uses - the property it sits in is not one this reads yet`;
|
|
359
|
+
if (!u)
|
|
360
|
+
return null;
|
|
361
|
+
const across = listed(u.per.map(([root]) => root));
|
|
362
|
+
if (isEm(literal))
|
|
363
|
+
return `font-relative (em): ${u.total}× across ${across} - depends on each element's font, so no fixed step`;
|
|
364
|
+
const px = pxOf(literal, root.px);
|
|
365
|
+
if (px === null)
|
|
366
|
+
return `${u.total}× across ${across} - not a length we can read`;
|
|
367
|
+
if (px === 0)
|
|
368
|
+
return "zero is not a step";
|
|
369
|
+
if (px < 0)
|
|
370
|
+
return "negative values are not steps";
|
|
371
|
+
if (!Number.isInteger(px)) {
|
|
372
|
+
const written = literal.trim().toLowerCase();
|
|
373
|
+
const from = written === `${px}px` ? "" : ` (from ${written})`;
|
|
374
|
+
return `${px}px${from} is not a whole step`;
|
|
375
|
+
}
|
|
376
|
+
/** Uma ocorrência não é uso medido - a mesma frase que a cor já usa (D2). */
|
|
377
|
+
return "seen once";
|
|
378
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { COLOR } from "./doctor/scan.js";
|
|
2
|
+
import { normalizeValue } from "./doctor/tokens.js";
|
|
3
|
+
/** A raiz que conta como função e NUNCA vira palavra: cor de ícone, não do sistema dele. */
|
|
4
|
+
export const SVG = "svg";
|
|
5
|
+
/** As raízes que recebem COMPRIMENTO - espaço e raio. Tudo o mais recebe cor. */
|
|
6
|
+
const LENGTH_ROOTS = new Set([
|
|
7
|
+
"padding",
|
|
8
|
+
"margin",
|
|
9
|
+
"gap",
|
|
10
|
+
"border-radius",
|
|
11
|
+
]);
|
|
12
|
+
/** As raízes de espaço, por natureza do achado - o `because` só lista as da natureza certa. */
|
|
13
|
+
export const ROOTS_OF_KIND = {
|
|
14
|
+
spacing: ["padding", "margin", "gap"],
|
|
15
|
+
radius: ["border-radius"],
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* UTILIDADE TAILWIND → PROPRIEDADE CSS QUE ELA ESCREVE. A palavra é a da direita, sempre: `ring` não
|
|
19
|
+
* é propriedade CSS nenhuma - o Tailwind a compila em `box-shadow`, e é isso que o código dele pinta.
|
|
20
|
+
* `px` é `padding` (dos dois lados), `mt` é `margin`, `rounded` é `border-radius`.
|
|
21
|
+
*/
|
|
22
|
+
export const TAILWIND_WRITES = {
|
|
23
|
+
bg: "background",
|
|
24
|
+
from: "background",
|
|
25
|
+
via: "background",
|
|
26
|
+
to: "background",
|
|
27
|
+
text: "color",
|
|
28
|
+
placeholder: "color",
|
|
29
|
+
border: "border",
|
|
30
|
+
divide: "border",
|
|
31
|
+
shadow: "box-shadow",
|
|
32
|
+
ring: "box-shadow",
|
|
33
|
+
"ring-offset": "box-shadow",
|
|
34
|
+
outline: "outline",
|
|
35
|
+
decoration: "text-decoration-color",
|
|
36
|
+
accent: "accent-color",
|
|
37
|
+
caret: "caret-color",
|
|
38
|
+
fill: SVG,
|
|
39
|
+
stroke: SVG,
|
|
40
|
+
p: "padding",
|
|
41
|
+
px: "padding",
|
|
42
|
+
py: "padding",
|
|
43
|
+
pt: "padding",
|
|
44
|
+
pr: "padding",
|
|
45
|
+
pb: "padding",
|
|
46
|
+
pl: "padding",
|
|
47
|
+
ps: "padding",
|
|
48
|
+
pe: "padding",
|
|
49
|
+
m: "margin",
|
|
50
|
+
mx: "margin",
|
|
51
|
+
my: "margin",
|
|
52
|
+
mt: "margin",
|
|
53
|
+
mr: "margin",
|
|
54
|
+
mb: "margin",
|
|
55
|
+
ml: "margin",
|
|
56
|
+
ms: "margin",
|
|
57
|
+
me: "margin",
|
|
58
|
+
gap: "gap",
|
|
59
|
+
rounded: "border-radius",
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* `16px` · `1.5rem` · `.5em` · `-1px` · `1PX` - o número e a unidade, como o scan os lê. O
|
|
63
|
+
* lookbehind impede `2px` dentro de `12px`; o lookahead impede `em` dentro de `emphasis`.
|
|
64
|
+
*
|
|
65
|
+
* A CAIXA NÃO DECIDE: CSS não distingue `1px` de `1PX`, e código gerado escreve das duas formas.
|
|
66
|
+
* Sem a flag `i` um `margin: 1REM` não era contado, e a linha dele saía sem motivo.
|
|
67
|
+
*/
|
|
68
|
+
const LENGTH = /(?<![\w.#-])-?\d*\.?\d+(?:px|rem|em)(?![\w-])/gi;
|
|
69
|
+
/**
|
|
70
|
+
* `background:` · `backgroundColor:` · `"background-color":` · `fill="`
|
|
71
|
+
*
|
|
72
|
+
* O identificador não pode vir colado a `.`, `$`, `@`, `-`, letra ou dígito (`colors.dark :` num
|
|
73
|
+
* ternário não é uma propriedade), e a aspa de abertura, quando existe, tem de fechar antes do `:` -
|
|
74
|
+
* senão `"#fff" :` de um ternário viraria a âncora `fff`. O `=` só vale na forma de ATRIBUTO - colado
|
|
75
|
+
* ao nome e seguido de aspa ou `{` -, porque `const BRAND = "#0f5132"` é uma constante, não uma
|
|
76
|
+
* função. O que casa aqui ainda passa por `propertyRoot`: só o que pinta ou espaça vira âncora.
|
|
77
|
+
*/
|
|
78
|
+
const PROPERTY = /(?<![\w.$@#-])(["']?)[a-zA-Z][\w-]*\1(?:\s*:|=(?=["'{]))/g;
|
|
79
|
+
/**
|
|
80
|
+
* `bg-[#…]` · `hover:text-[#…]` · `border-t-[rgba(…)]` · `gap-x-[…]` · `rounded-tl-[…]` - as
|
|
81
|
+
* utilidades da tabela, com o sufixo de lado que algumas aceitam.
|
|
82
|
+
*/
|
|
83
|
+
const UTILITY = new RegExp(`(?<![\\w-])(${Object.keys(TAILWIND_WRITES)
|
|
84
|
+
.sort((a, b) => b.length - a.length)
|
|
85
|
+
.join("|")})(?:-[trblxyse]{1,2})?-\\[`, "g");
|
|
86
|
+
/**
|
|
87
|
+
* A RAIZ DE UMA PROPRIEDADE CSS QUE CARREGA COR OU COMPRIMENTO (D1) - ou `null` para o que não é
|
|
88
|
+
* uma. A palavra que ele escreveu, sem o sufixo que só diz o lado ou o dialeto: `background-color`,
|
|
89
|
+
* `backgroundColor` e `background-image` são `background`; `border-top-color` e `borderColor` são
|
|
90
|
+
* `border`; `padding-top`, `paddingTop` são `padding`; `border-top-left-radius` e `borderRadius`
|
|
91
|
+
* são `border-radius`. Uma chave de objeto, uma variável, uma propriedade que não pinta nem espaça:
|
|
92
|
+
* `null`.
|
|
93
|
+
*/
|
|
94
|
+
export function propertyRoot(raw) {
|
|
95
|
+
const kebab = raw
|
|
96
|
+
.trim()
|
|
97
|
+
.replace(/^["']|["']$/g, "")
|
|
98
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
99
|
+
.toLowerCase();
|
|
100
|
+
if (/^border(-[a-z]+)*-radius$/.test(kebab))
|
|
101
|
+
return "border-radius";
|
|
102
|
+
if (/^padding(-|$)/.test(kebab))
|
|
103
|
+
return "padding";
|
|
104
|
+
if (/^margin(-|$)/.test(kebab))
|
|
105
|
+
return "margin";
|
|
106
|
+
if (/^(gap|row-gap|column-gap)$/.test(kebab))
|
|
107
|
+
return "gap";
|
|
108
|
+
if (/^background(-|$)/.test(kebab))
|
|
109
|
+
return "background";
|
|
110
|
+
if (/^border(-|$)/.test(kebab))
|
|
111
|
+
return "border";
|
|
112
|
+
if (kebab === "color")
|
|
113
|
+
return "color";
|
|
114
|
+
if (/^outline(-|$)/.test(kebab))
|
|
115
|
+
return "outline";
|
|
116
|
+
if (kebab === "box-shadow")
|
|
117
|
+
return "box-shadow";
|
|
118
|
+
if (kebab === "text-shadow")
|
|
119
|
+
return "text-shadow";
|
|
120
|
+
if (/^(fill|stroke|stop-color|flood-color|lighting-color)$/.test(kebab))
|
|
121
|
+
return SVG;
|
|
122
|
+
if (/^(caret-color|accent-color|text-decoration-color|text-emphasis-color|column-rule-color)$/.test(kebab))
|
|
123
|
+
return kebab;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A CHAVE DE UM COMPRIMENTO: o valor em px sobre a raiz medida, e `@em` quando a unidade escrita
|
|
128
|
+
* é `em` - porque aí o valor não é fixo, e uma contagem que o somasse ao `16px` afirmaria um degrau
|
|
129
|
+
* sobre ocorrências que dependem da fonte de cada elemento (D3). `1rem` e `16px` são a mesma chave.
|
|
130
|
+
*/
|
|
131
|
+
export function lengthKey(literal, root) {
|
|
132
|
+
const px = normalizeValue(literal, root.px);
|
|
133
|
+
const written = literal.trim();
|
|
134
|
+
if (/(?<!r)em$/i.test(written))
|
|
135
|
+
return `${px}@em`;
|
|
136
|
+
if (root.ambiguous && /rem$/i.test(written))
|
|
137
|
+
return `${px}@rem?`;
|
|
138
|
+
return px;
|
|
139
|
+
}
|
|
140
|
+
/** Toda declaração escrita na linha, em ordem, com o limite de cada uma. */
|
|
141
|
+
function marksOf(line) {
|
|
142
|
+
const out = [];
|
|
143
|
+
for (const m of line.matchAll(PROPERTY))
|
|
144
|
+
out.push({
|
|
145
|
+
start: m.index,
|
|
146
|
+
end: m.index + m[0].length,
|
|
147
|
+
root: propertyRoot(m[0].replace(/\s*[:=]$/, "")),
|
|
148
|
+
until: Number.POSITIVE_INFINITY,
|
|
149
|
+
});
|
|
150
|
+
for (const m of line.matchAll(UTILITY)) {
|
|
151
|
+
const root = TAILWIND_WRITES[m[1] ?? ""];
|
|
152
|
+
if (!root)
|
|
153
|
+
continue;
|
|
154
|
+
const end = m.index + m[0].length;
|
|
155
|
+
const close = line.indexOf("]", end);
|
|
156
|
+
out.push({
|
|
157
|
+
start: m.index,
|
|
158
|
+
end,
|
|
159
|
+
root,
|
|
160
|
+
until: close < 0 ? Number.POSITIVE_INFINITY : close,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
out.sort((a, b) => a.start - b.start);
|
|
164
|
+
/** O limite de cada uma é o começo da próxima - quem escreveu outra propriedade fechou esta. */
|
|
165
|
+
for (const [i, mark] of out.entries()) {
|
|
166
|
+
const next = out[i + 1];
|
|
167
|
+
if (next)
|
|
168
|
+
mark.until = Math.min(mark.until, next.start);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* A DECLARAÇÃO QUE COBRE ESTE LITERAL - ou `null`, e `null` é resposta: o valor não está numa
|
|
174
|
+
* propriedade que este passe lê, então ele não exerce função nenhuma que a gente possa afirmar.
|
|
175
|
+
* Sem declaração na linha, vale a que ficou aberta na anterior.
|
|
176
|
+
*/
|
|
177
|
+
function anchorFor(marks, at, carried) {
|
|
178
|
+
if (marks.length === 0)
|
|
179
|
+
return carried;
|
|
180
|
+
let hit = null;
|
|
181
|
+
for (const mark of marks) {
|
|
182
|
+
if (mark.end > at)
|
|
183
|
+
break;
|
|
184
|
+
hit = mark;
|
|
185
|
+
}
|
|
186
|
+
if (hit === null || hit.root === null)
|
|
187
|
+
return null;
|
|
188
|
+
return at < hit.until ? hit.root : null;
|
|
189
|
+
}
|
|
190
|
+
/** A linha FECHOU a declaração: o que vier depois não é continuação dela. */
|
|
191
|
+
const ENDS_DECLARATION = /[;{}]\s*$/;
|
|
192
|
+
/**
|
|
193
|
+
* Conta cada ocorrência de cor e de comprimento de UM arquivo, linha a linha, na função da âncora
|
|
194
|
+
* mais próxima antes dela. `box-shadow: 0 1px #000, 0 2px #fff` conta as duas cores em
|
|
195
|
+
* `box-shadow` (e nenhum dos comprimentos); `padding: 1px 2px` conta `1px` e `2px` uma vez cada em
|
|
196
|
+
* `padding`; uma linha com `background` e `borderColor` do mesmo hex conta as duas funções. Acumula
|
|
197
|
+
* em `into` para que o repositório inteiro caiba num único mapa.
|
|
198
|
+
*
|
|
199
|
+
* A DECLARAÇÃO PODE CONTINUAR NA LINHA SEGUINTE - regra do CSS, não de um repositório:
|
|
200
|
+
*
|
|
201
|
+
* box-shadow:
|
|
202
|
+
* 1px 1px 0 0 #2b2927,
|
|
203
|
+
* 2px 2px 0 0 #2b2927;
|
|
204
|
+
*
|
|
205
|
+
* Medido no `web-subscribe` real em 08/09 (`framework/style/_term.scss`): 9 ocorrências assim, sem
|
|
206
|
+
* âncora na própria linha, saíam sem função nenhuma - e o campo em branco saía calado. Quando a linha
|
|
207
|
+
* não tem âncora e a anterior não FECHOU a declaração (não termina em `;`, `}` ou `{`), a âncora da
|
|
208
|
+
* anterior vale para esta. `background: #111;` seguido de `#222` NÃO herda: o `;` fechou.
|
|
209
|
+
*/
|
|
210
|
+
export function countUseByProperty(source, into,
|
|
211
|
+
/** A raiz MEDIDA, com a ambiguidade dela - ver `lengthKey`. */
|
|
212
|
+
root) {
|
|
213
|
+
const bump = (key, root) => {
|
|
214
|
+
const per = into.get(key) ?? new Map();
|
|
215
|
+
per.set(root, (per.get(root) ?? 0) + 1);
|
|
216
|
+
into.set(key, per);
|
|
217
|
+
};
|
|
218
|
+
/** A âncora da declaração ainda aberta, vinda das linhas anteriores. */
|
|
219
|
+
let carried = null;
|
|
220
|
+
for (const line of source.split("\n")) {
|
|
221
|
+
const marks = marksOf(line);
|
|
222
|
+
for (const m of line.matchAll(COLOR)) {
|
|
223
|
+
const at = anchorFor(marks, m.index, carried);
|
|
224
|
+
if (at !== null && !LENGTH_ROOTS.has(at))
|
|
225
|
+
bump(normalizeValue(m[0], root.px), at);
|
|
226
|
+
}
|
|
227
|
+
for (const m of line.matchAll(LENGTH)) {
|
|
228
|
+
const at = anchorFor(marks, m.index, carried);
|
|
229
|
+
if (at !== null && LENGTH_ROOTS.has(at))
|
|
230
|
+
bump(lengthKey(m[0], root), at);
|
|
231
|
+
}
|
|
232
|
+
/** A ÚLTIMA declaração da linha é a que pode continuar - e se ela é de uma propriedade que este
|
|
233
|
+
* passe não lê, o que continua é o silêncio dela, nunca a âncora de antes. */
|
|
234
|
+
if (ENDS_DECLARATION.test(line))
|
|
235
|
+
carried = null;
|
|
236
|
+
else if (marks.length > 0)
|
|
237
|
+
carried = marks[marks.length - 1]?.root ?? null;
|
|
238
|
+
}
|
|
239
|
+
}
|
package/package.json
CHANGED