synthesisui 0.16.266 → 0.16.269
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/anatomy-patch.js +62 -0
- package/dist/color-forms.js +180 -0
- package/dist/commands/import.js +27 -5
- package/dist/commands/mcp.js +66 -1
- package/dist/component-codegen.js +10 -2
- package/dist/doctor/color-distance.js +32 -11
- package/dist/fonts.js +28 -8
- package/dist/guide.js +23 -18
- package/dist/install-marks.js +1 -1
- package/dist/naming-queue.js +43 -1
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Um patch que não diz nada não é um patch - e distinguir isso de `undefined` importa. */
|
|
2
|
+
export function hasEdits(patch) {
|
|
3
|
+
return Boolean((patch?.names?.length ?? 0) > 0 || (patch?.fold?.length ?? 0) > 0);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* O PATCH APLICADO SOBRE A ÁRVORE DERIVADA - e ela é a fonte, não o rascunho.
|
|
7
|
+
*
|
|
8
|
+
* Idempotente por construção: aplicar duas vezes o mesmo patch dá o mesmo resultado, porque a
|
|
9
|
+
* operação é indexada por `at` e não acumula.
|
|
10
|
+
*
|
|
11
|
+
* O que um índice desconhecido faz: NADA, e em silêncio é errado. Quem chama recebe a lista dos
|
|
12
|
+
* índices que não existiam, para dizer em voz alta - um nome escrito para um nó que a derivação não
|
|
13
|
+
* produziu é uma decisão perdida, e perder calado é o defeito que a lei 8 proíbe.
|
|
14
|
+
*/
|
|
15
|
+
export function applyAnatomyPatch(read, patch) {
|
|
16
|
+
const byAt = new Map();
|
|
17
|
+
for (const n of patch.names ?? [])
|
|
18
|
+
byAt.set(n.at, n.name);
|
|
19
|
+
const fold = new Set(patch.fold ?? []);
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
let renamed = 0;
|
|
22
|
+
let folded = 0;
|
|
23
|
+
const walk = (nodes) => {
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const node of nodes) {
|
|
26
|
+
const at = typeof node.at === "number" ? node.at : null;
|
|
27
|
+
if (at !== null)
|
|
28
|
+
seen.add(at);
|
|
29
|
+
const children = Array.isArray(node.children) ? walk(node.children) : [];
|
|
30
|
+
/**
|
|
31
|
+
* DOBRAR SOBE OS FILHOS. Um nó dobrado que era uma FRONTEIRA não some: `component` e
|
|
32
|
+
* `external` são o que o cliente declarou, e a Lei "o código deles vence" não abre exceção
|
|
33
|
+
* para economia nossa.
|
|
34
|
+
*/
|
|
35
|
+
if (at !== null &&
|
|
36
|
+
fold.has(at) &&
|
|
37
|
+
node.as !== "component" &&
|
|
38
|
+
node.as !== "external") {
|
|
39
|
+
folded += 1;
|
|
40
|
+
out.push(...children);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const name = at !== null ? byAt.get(at) : undefined;
|
|
44
|
+
if (name !== undefined && name !== node.name)
|
|
45
|
+
renamed += 1;
|
|
46
|
+
out.push({
|
|
47
|
+
...node,
|
|
48
|
+
...(name !== undefined ? { name } : {}),
|
|
49
|
+
...(children.length > 0 ? { children } : {}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
};
|
|
54
|
+
const patched = walk(read);
|
|
55
|
+
const asked = [...byAt.keys(), ...fold];
|
|
56
|
+
return {
|
|
57
|
+
read: patched,
|
|
58
|
+
renamed,
|
|
59
|
+
folded,
|
|
60
|
+
unknown: [...new Set(asked.filter((at) => !seen.has(at)))].sort((a, b) => a - b),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TODA FORMA DE COR, REDUZIDA A UMA - e sem isto o detector via duas cores onde há uma.
|
|
3
|
+
*
|
|
4
|
+
* O QUÊ: `#fff`, `#ffffff`, `rgb(255 255 255)` e `rgba(255,255,255,1)` passam a ser o mesmo valor, e
|
|
5
|
+
* uma cor com alfa passa a ser dita como *"o seu token com N% de opacidade"* em vez de uma cor nova.
|
|
6
|
+
*
|
|
7
|
+
* POR QUÊ: medido no `codelevel-ds` em 21/08 e é literal - o leitor gravou `#fff` do código-fonte e a
|
|
8
|
+
* receita compilada carrega `#ffffff`. **Para um detector que compara string são duas cores**, então o
|
|
9
|
+
* branco pintado **43 vezes** nas receitas dele nunca apareceu na lista de "valores que o seu sistema
|
|
10
|
+
* nunca nomeou". E as maiores órfãs dele são `rgba(15,23,42,0.30)` e `rgba(192,132,252,0.18)`, que o
|
|
11
|
+
* filtro de `#hex` descartava inteiras.
|
|
12
|
+
*
|
|
13
|
+
* A meta é 100% de cobertura das receitas (dono, 21/08), e ela não sobrevive a um comparador de
|
|
14
|
+
* string.
|
|
15
|
+
*
|
|
16
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
17
|
+
* O ALFA É SEPARADO DO RGB, e essa é a decisão que impede poluir o vocabulário dele
|
|
18
|
+
*
|
|
19
|
+
* `rgba(37,99,235,0.5)` tem duas leituras possíveis e só ele sabe qual é: *"uma cor nova"* ou *"o meu
|
|
20
|
+
* `brand-blue` com metade da opacidade"*. Guardar o alfa à parte é o que permite a segunda: o RGB
|
|
21
|
+
* casa com um token que ele já declara, e a tela diz **opacidade** em vez de sugerir um nome novo.
|
|
22
|
+
* Sem isso, cada nível de transparência de uma mesma cor pediria um token próprio.
|
|
23
|
+
*
|
|
24
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
25
|
+
* O QUE ESTE MÓDULO NÃO FAZ, e é declarado em vez de fingido
|
|
26
|
+
*
|
|
27
|
+
* `color-mix(in srgb, #fff 40%, transparent)` é uma FUNÇÃO de composição, não um valor: resolvê-la
|
|
28
|
+
* exige o espaço de cor e o segundo operando, e chutar produziria um valor que o navegador nunca
|
|
29
|
+
* pinta. O que este módulo faz é EXTRAIR as cores literais de dentro dela, que é o que permite dizer
|
|
30
|
+
* *"esta mistura usa um branco que nada nomeia"* sem afirmar a cor resultante.
|
|
31
|
+
*
|
|
32
|
+
* `currentColor`, `transparent`, `inherit` e nome de cor CSS não entram: os três primeiros não são
|
|
33
|
+
* decisão de paleta, e um nome CSS (`rebeccapurple`) é raro o bastante para não valer uma tabela de
|
|
34
|
+
* 148 entradas que envelhece.
|
|
35
|
+
*/
|
|
36
|
+
const clamp255 = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
37
|
+
const hex2 = (n) => clamp255(n).toString(16).padStart(2, "0");
|
|
38
|
+
/** `#f0a` -> `#ff00aa`, e `#ff00aa80` devolve o alfa junto. */
|
|
39
|
+
function fromHex(raw) {
|
|
40
|
+
const h = raw.trim().replace(/^#/, "");
|
|
41
|
+
if (!/^[0-9a-f]{3,8}$/i.test(h))
|
|
42
|
+
return null;
|
|
43
|
+
const dup = (s) => s
|
|
44
|
+
.split("")
|
|
45
|
+
.map((c) => c + c)
|
|
46
|
+
.join("");
|
|
47
|
+
if (h.length === 3 || h.length === 4) {
|
|
48
|
+
const [r, g, b, a] = dup(h).match(/../g);
|
|
49
|
+
return {
|
|
50
|
+
hex: `#${r}${g}${b}`.toLowerCase(),
|
|
51
|
+
alpha: a === undefined ? 1 : Number.parseInt(a, 16) / 255,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (h.length === 6)
|
|
55
|
+
return { hex: `#${h}`.toLowerCase(), alpha: 1 };
|
|
56
|
+
if (h.length === 8)
|
|
57
|
+
return {
|
|
58
|
+
hex: `#${h.slice(0, 6)}`.toLowerCase(),
|
|
59
|
+
alpha: Number.parseInt(h.slice(6, 8), 16) / 255,
|
|
60
|
+
};
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/** `50%` -> 0.5 · `.5` -> 0.5 · `1` -> 1. Serve alfa e componente percentual. */
|
|
64
|
+
const unit = (raw, of = 1) => {
|
|
65
|
+
const t = raw.trim();
|
|
66
|
+
if (t.endsWith("%")) {
|
|
67
|
+
const n = Number.parseFloat(t);
|
|
68
|
+
return Number.isFinite(n) ? (n / 100) * of : Number.NaN;
|
|
69
|
+
}
|
|
70
|
+
const n = Number.parseFloat(t);
|
|
71
|
+
return Number.isFinite(n) ? n : Number.NaN;
|
|
72
|
+
};
|
|
73
|
+
/** `rgb(255 0 0 / 50%)` e `rgba(255,0,0,.5)` - a sintaxe moderna e a legada. */
|
|
74
|
+
function fromRgb(raw) {
|
|
75
|
+
const m = /^rgba?\(([^)]*)\)$/i.exec(raw.trim());
|
|
76
|
+
if (!m)
|
|
77
|
+
return null;
|
|
78
|
+
const [body, alphaPart] = m[1].split("/");
|
|
79
|
+
const parts = body
|
|
80
|
+
.trim()
|
|
81
|
+
.split(/[\s,]+/)
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
if (parts.length < 3)
|
|
84
|
+
return null;
|
|
85
|
+
const [r, g, b] = parts.slice(0, 3).map((p) => unit(p, 255));
|
|
86
|
+
if (![r, g, b].every(Number.isFinite))
|
|
87
|
+
return null;
|
|
88
|
+
/** O alfa vem depois da barra na sintaxe moderna, ou como quarto item na legada. */
|
|
89
|
+
const rawAlpha = alphaPart ?? parts[3];
|
|
90
|
+
const alpha = rawAlpha === undefined ? 1 : unit(rawAlpha);
|
|
91
|
+
return {
|
|
92
|
+
hex: `#${hex2(r)}${hex2(g)}${hex2(b)}`,
|
|
93
|
+
alpha: Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** `hsl(210 40% 50%)` e `hsla(210,40%,50%,.5)`. */
|
|
97
|
+
function fromHsl(raw) {
|
|
98
|
+
const m = /^hsla?\(([^)]*)\)$/i.exec(raw.trim());
|
|
99
|
+
if (!m)
|
|
100
|
+
return null;
|
|
101
|
+
const [body, alphaPart] = m[1].split("/");
|
|
102
|
+
const parts = body
|
|
103
|
+
.trim()
|
|
104
|
+
.split(/[\s,]+/)
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
if (parts.length < 3)
|
|
107
|
+
return null;
|
|
108
|
+
const h = ((Number.parseFloat(parts[0]) % 360) + 360) % 360;
|
|
109
|
+
const s = unit(parts[1]);
|
|
110
|
+
const l = unit(parts[2]);
|
|
111
|
+
if (![h, s, l].every(Number.isFinite))
|
|
112
|
+
return null;
|
|
113
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
114
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
115
|
+
const mm = l - c / 2;
|
|
116
|
+
const seg = Math.floor(h / 60) % 6;
|
|
117
|
+
const table = [
|
|
118
|
+
[c, x, 0],
|
|
119
|
+
[x, c, 0],
|
|
120
|
+
[0, c, x],
|
|
121
|
+
[0, x, c],
|
|
122
|
+
[x, 0, c],
|
|
123
|
+
[c, 0, x],
|
|
124
|
+
];
|
|
125
|
+
const [r, g, b] = table[seg].map((v) => (v + mm) * 255);
|
|
126
|
+
const rawAlpha = alphaPart ?? parts[3];
|
|
127
|
+
const alpha = rawAlpha === undefined ? 1 : unit(rawAlpha);
|
|
128
|
+
return {
|
|
129
|
+
hex: `#${hex2(r)}${hex2(g)}${hex2(b)}`,
|
|
130
|
+
alpha: Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* QUALQUER FORMA -> a canônica. `null` para o que não é uma cor literal.
|
|
135
|
+
*
|
|
136
|
+
* Palavra-chave fica fora de propósito: `transparent` e `currentColor` não são decisão de paleta, e um
|
|
137
|
+
* token para elas seria vocabulário que ninguém procura.
|
|
138
|
+
*/
|
|
139
|
+
export function colorForm(value) {
|
|
140
|
+
const v = value.trim();
|
|
141
|
+
if (!v || /^(transparent|currentcolor|inherit|initial|unset|none)$/i.test(v))
|
|
142
|
+
return null;
|
|
143
|
+
return fromHex(v) ?? fromRgb(v) ?? fromHsl(v);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* AS CORES LITERAIS DENTRO DE UM VALOR - inclusive as de dentro de uma função de composição.
|
|
147
|
+
*
|
|
148
|
+
* Um `boxShadow` carrega a cor no meio de offsets (`0 2px 4px rgba(0,0,0,.02)`), e um `color-mix`
|
|
149
|
+
* carrega duas. Comparar o valor inteiro contra uma paleta nunca casaria; extrair as cores casa.
|
|
150
|
+
*
|
|
151
|
+
* A ordem de varredura é a das formas mais longas primeiro (`rgba(` antes de `#`), senão um hex dentro
|
|
152
|
+
* de um `color-mix` seria contado duas vezes - uma pela função e outra por ele mesmo.
|
|
153
|
+
*/
|
|
154
|
+
export function colorsIn(value) {
|
|
155
|
+
const out = [];
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
const push = (c) => {
|
|
158
|
+
if (!c)
|
|
159
|
+
return;
|
|
160
|
+
const key = `${c.hex}|${c.alpha}`;
|
|
161
|
+
if (seen.has(key))
|
|
162
|
+
return;
|
|
163
|
+
seen.add(key);
|
|
164
|
+
out.push(c);
|
|
165
|
+
};
|
|
166
|
+
const rest = value.replace(/\b(?:rgba?|hsla?)\([^)]*\)/gi, (m) => (push(colorForm(m)), " "));
|
|
167
|
+
for (const m of rest.match(/#[0-9a-f]{3,8}\b/gi) ?? [])
|
|
168
|
+
push(colorForm(m));
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A COMPARAÇÃO "É O MEU TOKEN COM ALFA?" NÃO MORA AQUI, e a razão é o `reachable`.
|
|
173
|
+
*
|
|
174
|
+
* Escrevi um `sameHue(a, b)` e ele nasceu órfão: quem faz essa pergunta - `v2-proposal` - precisa
|
|
175
|
+
* responder para uma PALETA inteira, e um mapa de hex canônico -> token responde em O(1) onde uma
|
|
176
|
+
* comparação par a par pediria uma varredura. O helper era mais bonito e menos útil.
|
|
177
|
+
*
|
|
178
|
+
* O que este módulo entrega para aquela pergunta é o `hex` canônico separado do `alpha`, que é a parte
|
|
179
|
+
* difícil. Comparar hex é `===`.
|
|
180
|
+
*/
|
package/dist/commands/import.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, join, relative } from "node:path";
|
|
3
3
|
import { anatomyFromSketch } from "../anatomy-from-sketch.js";
|
|
4
|
+
import { applyAnatomyPatch, hasEdits, } from "../anatomy-patch.js";
|
|
4
5
|
import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
|
|
5
6
|
import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
|
|
6
7
|
import { declaredElsewhere } from "../declared-elsewhere.js";
|
|
@@ -3101,16 +3102,37 @@ quiet = false) {
|
|
|
3101
3102
|
? (entry?.root).at
|
|
3102
3103
|
: undefined)
|
|
3103
3104
|
: anatomy;
|
|
3105
|
+
/**
|
|
3106
|
+
* A DERIVAÇÃO COM O PATCH DO AGENTE POR CIMA - a fonte é o CLI, a decisão é dele.
|
|
3107
|
+
*
|
|
3108
|
+
* A ordem das três é deliberada: anatomia autoral vence (ela sempre venceu), depois a derivada
|
|
3109
|
+
* COM patch, depois a derivada crua. Um patch sobre uma árvore que não existe não tem onde
|
|
3110
|
+
* aplicar, então ele só é consultado quando há derivação.
|
|
3111
|
+
*
|
|
3112
|
+
* O que sai daqui é `AnatomyRead[]` nos três casos - por isso `resolveAnatomy`, `censusToPatch`,
|
|
3113
|
+
* o codegen e o Canvas não sabem que o patch existe.
|
|
3114
|
+
*/
|
|
3115
|
+
const patched = derived && derived.read.length > 0 && hasEdits(entry?.patch)
|
|
3116
|
+
? applyAnatomyPatch(derived.read, entry?.patch)
|
|
3117
|
+
: null;
|
|
3118
|
+
if (patched && patched.unknown.length > 0)
|
|
3119
|
+
/**
|
|
3120
|
+
* DITO, NUNCA ENGOLIDO: um nome escrito para um nó que a derivação não produziu é uma decisão
|
|
3121
|
+
* perdida, e o agente precisa saber que a perdeu para escrevê-la onde ela cabe.
|
|
3122
|
+
*/
|
|
3123
|
+
notes.add(`${component}: the anatomy patch named ${patched.unknown.length} node${patched.unknown.length === 1 ? "" : "s"} the derivation does not have (sketch ${patched.unknown.join(", ")}) - those names were dropped`);
|
|
3104
3124
|
const resolved = Array.isArray(framed) && framed.length > 0
|
|
3105
3125
|
? resolveAnatomy(framed, declared, deps, resolveRef, entry?.root,
|
|
3106
3126
|
// The component's own sketch, so a node naming itself by index reads
|
|
3107
3127
|
// the exact class string the census measured (dono, 01/08).
|
|
3108
3128
|
looks[component]?.sketch, census.sxSpacing)
|
|
3109
|
-
:
|
|
3110
|
-
? resolveAnatomy(
|
|
3111
|
-
:
|
|
3112
|
-
?
|
|
3113
|
-
:
|
|
3129
|
+
: patched
|
|
3130
|
+
? resolveAnatomy(patched.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch, census.sxSpacing)
|
|
3131
|
+
: derived && derived.read.length > 0
|
|
3132
|
+
? resolveAnatomy(derived.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch, census.sxSpacing)
|
|
3133
|
+
: Array.isArray(entry?.parts) && entry.parts.length > 0
|
|
3134
|
+
? resolveFlatParts(entry.parts, declared)
|
|
3135
|
+
: null;
|
|
3114
3136
|
if (!resolved)
|
|
3115
3137
|
continue;
|
|
3116
3138
|
const hasParts = Object.keys(resolved.parts).length > 0;
|
package/dist/commands/mcp.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readdir, readFile } from "node:fs/promises";
|
|
|
3
3
|
import { join, relative, resolve } from "node:path";
|
|
4
4
|
import { provenanceAction, toolSlug, withSource, } from "../agent-provenance.js";
|
|
5
5
|
import { pinnedHookVersion } from "../agent-wiring.js";
|
|
6
|
+
import { elementClass, kebabAxis } from "../component-codegen.js";
|
|
6
7
|
import { composePlan, FAMILIES, isFamily, noFamilyAnswer, renderPlan, } from "../compose-context.js";
|
|
7
8
|
import { readToken, resolveRegistry } from "../config.js";
|
|
8
9
|
import { readEvents } from "../doctor/ledger.js";
|
|
@@ -11,6 +12,7 @@ import { diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
|
|
|
11
12
|
import { nearestToken, normalizeValue, tokenFor } from "../doctor/tokens.js";
|
|
12
13
|
import { fromCensus } from "../memory/observation.js";
|
|
13
14
|
import { handleRecall, handleRemember, MEMORY_TOOLS } from "../memory/tools.js";
|
|
15
|
+
import { readInstalledConvention } from "../project-facts.js";
|
|
14
16
|
import { repoStateOf } from "../repo-state.js";
|
|
15
17
|
import { detectStack } from "../stack.js";
|
|
16
18
|
import { component } from "./component.js";
|
|
@@ -974,7 +976,7 @@ query) {
|
|
|
974
976
|
}
|
|
975
977
|
}
|
|
976
978
|
async function describeComponent(root, name) {
|
|
977
|
-
const { documents, requires } = await loadSystem(root);
|
|
979
|
+
const { documents, requires, table } = await loadSystem(root);
|
|
978
980
|
let recipe;
|
|
979
981
|
for (const doc of documents) {
|
|
980
982
|
const d = doc;
|
|
@@ -1118,6 +1120,69 @@ async function describeComponent(root, name) {
|
|
|
1118
1120
|
if (composes.length > 0) {
|
|
1119
1121
|
out.push("", `Built out of: ${composes.join(", ")}. Change one of those in one place rather than reproducing it here, and call describe_component on it before you do.`);
|
|
1120
1122
|
}
|
|
1123
|
+
/**
|
|
1124
|
+
* HOW TO DRESS IT - and it was the half this answer never carried.
|
|
1125
|
+
*
|
|
1126
|
+
* Measured on the owner's own census (21/08): his Button declares three axes -
|
|
1127
|
+
* `variant` with eight options, `size` with four, `glow` with two - plus
|
|
1128
|
+
* defaults, 32 layers and 78 properties. The whole reply for a component like
|
|
1129
|
+
* that was its name, its description and "no rules govern this yet". An agent
|
|
1130
|
+
* reading it composes without knowing `gold` exists, and nothing anywhere
|
|
1131
|
+
* says an option was missed.
|
|
1132
|
+
*
|
|
1133
|
+
* The material was already here: `recipe.variants` is the SAME field the
|
|
1134
|
+
* `GUIDE.md` reads to print `data-variant="primary|gold|…"`. Two surfaces
|
|
1135
|
+
* answering one question, and only the file on disk answered it.
|
|
1136
|
+
*
|
|
1137
|
+
* The class comes from `readInstalledConvention`, never from a literal `ds-`:
|
|
1138
|
+
* an imported system may carry its own prefix in `meta.classNames`, and
|
|
1139
|
+
* telling the agent to write a class that does not exist is worse than
|
|
1140
|
+
* telling it nothing.
|
|
1141
|
+
*/
|
|
1142
|
+
const axes = Object.entries(recipe.variants ?? {});
|
|
1143
|
+
if (axes.length > 0) {
|
|
1144
|
+
const convention = await readInstalledConvention(root, String(table.slug ?? ""));
|
|
1145
|
+
const cls = elementClass(name, convention);
|
|
1146
|
+
out.push("", `How to dress it: \`${cls}\`, plus one data attribute per axis.`);
|
|
1147
|
+
for (const [axis, options] of axes) {
|
|
1148
|
+
const names = Object.keys(options ?? {});
|
|
1149
|
+
if (names.length === 0)
|
|
1150
|
+
continue;
|
|
1151
|
+
const resting = recipe.defaults?.[axis];
|
|
1152
|
+
/**
|
|
1153
|
+
* O DEFAULT É O QUE ELE NÃO PRECISA ESCREVER, e dizer isso encurta o
|
|
1154
|
+
* código que o agente produz em vez de só informá-lo.
|
|
1155
|
+
*/
|
|
1156
|
+
const rest = resting
|
|
1157
|
+
? ` - at rest it is \`${resting}\`, so you can leave the attribute out`
|
|
1158
|
+
: "";
|
|
1159
|
+
out.push(` data-${kebabAxis(axis)}="${names.join("|")}"${rest}`);
|
|
1160
|
+
}
|
|
1161
|
+
const states = Object.keys(recipe.states ?? {});
|
|
1162
|
+
if (states.length > 0) {
|
|
1163
|
+
out.push(` states the recipe already carries: ${states.join(", ")} - they are CSS, so do not write them again in JS.`);
|
|
1164
|
+
}
|
|
1165
|
+
out.push(` Or call add_component { "name": "${name}" } and get it as typed code, one prop per axis.`);
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* AS INSTRUÇÕES DE RENDER QUE A LEITURA MEDIU - e até aqui elas morriam no
|
|
1169
|
+
* terminal.
|
|
1170
|
+
*
|
|
1171
|
+
* O CLI mede `renderNotes` por componente e imprime uma vez, no import. São
|
|
1172
|
+
* 25 delas em 14 dos 59 componentes do dono, e uma diz: *"`ring` gates a look
|
|
1173
|
+
* and is a boolean rather than an axis - `data-ring` has to be set for it to
|
|
1174
|
+
* apply"*. Sem essa linha o agente escreve markup correto e o look nunca
|
|
1175
|
+
* aparece, que é o pior modo de falhar - nada reclama.
|
|
1176
|
+
*
|
|
1177
|
+
* O princípio, e ele vale além destas notas: **interpretada ou não por nós,
|
|
1178
|
+
* a instrução vai para quem constrói**. Uma nota que a esteira não soube
|
|
1179
|
+
* transformar em regra continua sendo a coisa mais útil que existe sobre
|
|
1180
|
+
* aquele componente.
|
|
1181
|
+
*/
|
|
1182
|
+
const renderNotes = recipe.renderNotes ?? [];
|
|
1183
|
+
if (renderNotes.length > 0) {
|
|
1184
|
+
out.push("", "Read off your own code, and it decides whether the look appears:", ...renderNotes.map((n) => ` ${n}`));
|
|
1185
|
+
}
|
|
1121
1186
|
/**
|
|
1122
1187
|
* WHAT IT NEEDS INSTALLED - the whole reason this tool earns its place.
|
|
1123
1188
|
*
|
|
@@ -4,8 +4,16 @@ export const DEFAULT_CONVENTION = {
|
|
|
4
4
|
partSeparator: "-",
|
|
5
5
|
};
|
|
6
6
|
const kebab = (v) => v.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* `metric-card` → `ds-metric-card`, or `metric-card`, or `sui-metric-card`.
|
|
9
|
+
*
|
|
10
|
+
* EXPORTADA porque o `describe_component` passou a dizer ao agente qual classe
|
|
11
|
+
* escrever, e uma segunda implementação do mesmo nome é como as duas começam a
|
|
12
|
+
* divergir: o MCP mandaria escrever uma classe que este codegen não emite.
|
|
13
|
+
*/
|
|
14
|
+
export const elementClass = (name, c) => `${c.prefix}${kebab(name)}`;
|
|
15
|
+
/** O mesmo kebab que decide a classe e o `data-*`, para quem precisa só do eixo. */
|
|
16
|
+
export const kebabAxis = (v) => kebab(v);
|
|
9
17
|
/** `metric-card` + `title` → `ds-metric-card-title`, or `metric-card__title`. */
|
|
10
18
|
const partClassName = (name, part, c) => `${elementClass(name, c)}${c.partSeparator}${kebab(part)}`;
|
|
11
19
|
const pascal = (name) => name
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { colorForm } from "../color-forms.js";
|
|
1
2
|
/**
|
|
2
3
|
* PERCEPTUAL COLOUR DISTANCE, in the CLI.
|
|
3
4
|
*
|
|
@@ -18,20 +19,40 @@
|
|
|
18
19
|
* exactly the evidence the normalisation pass is looking for. With this, a rare
|
|
19
20
|
* value rides along when it sits close to one we already kept.
|
|
20
21
|
*/
|
|
21
|
-
const HEX6 = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
22
22
|
/** The just-noticeable difference for ΔE76. Same constant as the platform's. */
|
|
23
23
|
export const JND = 2.3;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
/**
|
|
25
|
+
* QUALQUER FORMA DE COR -> RGB, e é aqui que o motor deixou de ser hex-only (21/08).
|
|
26
|
+
*
|
|
27
|
+
* O QUÊ: `rgb(79 70 229)` e `hsl(0 0% 100%)` são cor, e este motor devolvia `null` para as duas.
|
|
28
|
+
*
|
|
29
|
+
* POR QUÊ, medido: num projeto que declara a paleta em `rgb`/`hsl`, `deltaE` devolvia `null` para todo
|
|
30
|
+
* par, o que reconhecia **0 de 2** tokens dele - e a consequência era o oposto do propósito do
|
|
31
|
+
* produto: propor NOME NOVO para as duas cores que ele já nomeia. A ferramenta cunhando exatamente a
|
|
32
|
+
* duplicata que existe para retirar. Não é caso exótico: é o que Tailwind 4 emite em `@theme` e o que
|
|
33
|
+
* qualquer projeto de CSS Modules escreve à mão.
|
|
34
|
+
*
|
|
35
|
+
* COMO: `colorForm` já sabe ler hex 3/6/8, `rgb`/`rgba` (legado e moderno) e `hsl`/`hsla`, então a
|
|
36
|
+
* conversão passa por ela em vez de por um regex próprio. `src/color-forms.ts` é byte-idêntico ao
|
|
37
|
+
* `apps/web/src/lib/engine/color-forms.ts`, garantido por spec - o CLI publica standalone e não pode
|
|
38
|
+
* importar do app.
|
|
39
|
+
*
|
|
40
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
41
|
+
* TRANSLÚCIDO CONTINUA FORA, e isso é deliberado
|
|
42
|
+
*
|
|
43
|
+
* `rgba(0,0,0,.02)` e `#000000` têm o mesmo RGB e aparências opostas. Aceitar o alfa aqui faria o
|
|
44
|
+
* agrupamento dobrar um véu de 2% dentro de um preto sólido com ΔE 0 - "a mesma decisão", que é
|
|
45
|
+
* falso. Um valor translúcido é *"o token dele com alfa"* (decisão 4 do dono, 21/08), e quem trata
|
|
46
|
+
* disso é a lista de opacidades da proposta, não a distância perceptual.
|
|
47
|
+
*
|
|
48
|
+
* Então: alfa < 1 devolve `null`, exatamente como antes - o hex de 8 dígitos e o `rgba` já caíam fora
|
|
49
|
+
* por não casarem o regex. O que muda é só a cor OPACA escrita em outra forma.
|
|
50
|
+
*/
|
|
51
|
+
function toRgb(value) {
|
|
52
|
+
const form = colorForm(value);
|
|
53
|
+
if (!form || form.alpha < 1)
|
|
27
54
|
return null;
|
|
28
|
-
const
|
|
29
|
-
? h
|
|
30
|
-
.split("")
|
|
31
|
-
.map((c) => c + c)
|
|
32
|
-
.join("")
|
|
33
|
-
: h;
|
|
34
|
-
const n = Number.parseInt(full, 16);
|
|
55
|
+
const n = Number.parseInt(form.hex.slice(1), 16);
|
|
35
56
|
if (Number.isNaN(n))
|
|
36
57
|
return null;
|
|
37
58
|
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
package/dist/fonts.js
CHANGED
|
@@ -12,12 +12,31 @@ const GENERIC_FAMILIES = new Set([
|
|
|
12
12
|
"inherit",
|
|
13
13
|
"initial",
|
|
14
14
|
]);
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* TODA VAGA DE FONTE DO DOCUMENTO, dedup e sem os genéricos.
|
|
17
|
+
*
|
|
18
|
+
* Enumerava `[display, body, mono]` literalmente, e o documento passou a aceitar as famílias que o
|
|
19
|
+
* código DELE declara além das nossas três - `--font-serif` do codelevel é "Instrument Serif", 25 usos,
|
|
20
|
+
* reservada por ele para "editorial moments". O token existia, o compilador emitia
|
|
21
|
+
* `--ds-typography-families-serif`, e ninguém baixava a fonte: o texto saía no fallback do navegador e
|
|
22
|
+
* nada avisava. Um token que aponta para uma fonte que ninguém carregou é pior que um token ausente -
|
|
23
|
+
* o ausente pelo menos aparece na conta.
|
|
24
|
+
*
|
|
25
|
+
* A ORDEM É DETERMINÍSTICA: as três vagas primeiro, as dele em ordem alfabética. Isto vira href num
|
|
26
|
+
* arquivo gerado, e um conjunto que muda de ordem entre rodadas produz diff onde nada mudou.
|
|
27
|
+
*/
|
|
28
|
+
export const familySlots = (families) => {
|
|
29
|
+
const ours = ["display", "body", "mono"].filter((k) => k in families);
|
|
30
|
+
const theirs = Object.keys(families)
|
|
31
|
+
.filter((k) => !ours.includes(k))
|
|
32
|
+
.sort();
|
|
33
|
+
return [...ours, ...theirs];
|
|
34
|
+
};
|
|
16
35
|
export function customFontFamilies(families) {
|
|
17
36
|
const seen = new Set();
|
|
18
37
|
const names = [];
|
|
19
|
-
for (const
|
|
20
|
-
const name =
|
|
38
|
+
for (const slot of familySlots(families)) {
|
|
39
|
+
const name = families[slot]?.trim();
|
|
21
40
|
if (!name)
|
|
22
41
|
continue;
|
|
23
42
|
const key = name.toLowerCase();
|
|
@@ -47,7 +66,8 @@ export function googleFontsHref(families) {
|
|
|
47
66
|
* fallback - it works everywhere but swaps visibly on cold loads.
|
|
48
67
|
*/
|
|
49
68
|
export function nextFontSnippet(families, slug, appDir = "app") {
|
|
50
|
-
|
|
69
|
+
/** Toda vaga que o documento tem, não só as nossas três - ver `customFontFamilies`. */
|
|
70
|
+
const roles = familySlots(families).filter((role) => {
|
|
51
71
|
const name = families[role]?.trim();
|
|
52
72
|
return name && !GENERIC_FAMILIES.has(name.toLowerCase());
|
|
53
73
|
});
|
|
@@ -58,7 +78,7 @@ export function nextFontSnippet(families, slug, appDir = "app") {
|
|
|
58
78
|
const importNames = [];
|
|
59
79
|
const consts = [];
|
|
60
80
|
for (const role of roles) {
|
|
61
|
-
const name = families[role].trim();
|
|
81
|
+
const name = (families[role] ?? "").trim();
|
|
62
82
|
if (!seen.has(name)) {
|
|
63
83
|
seen.set(name, role);
|
|
64
84
|
importNames.push(importName(name));
|
|
@@ -81,13 +101,13 @@ export function nextFontSnippet(families, slug, appDir = "app") {
|
|
|
81
101
|
...consts,
|
|
82
102
|
];
|
|
83
103
|
const roleVar = (role) => {
|
|
84
|
-
const name = families[role].trim();
|
|
104
|
+
const name = (families[role] ?? "").trim();
|
|
85
105
|
return `--font-ds-${seen.get(name)}`;
|
|
86
106
|
};
|
|
87
107
|
const layout = [
|
|
88
108
|
`// ${appDir}/layout.tsx`,
|
|
89
|
-
`import { ${[...new Set(roles.map((r) => seen.get(families[r].trim())))].join(", ")} } from "./fonts";`,
|
|
90
|
-
`<body data-ds="${slug}" className={\`${[...new Set(roles.map((r) => `\${${seen.get(families[r].trim())}.variable}`))].join(" ")}\`}>`,
|
|
109
|
+
`import { ${[...new Set(roles.map((r) => seen.get((families[r] ?? "").trim())))].join(", ")} } from "./fonts";`,
|
|
110
|
+
`<body data-ds="${slug}" className={\`${[...new Set(roles.map((r) => `\${${seen.get((families[r] ?? "").trim())}.variable}`))].join(" ")}\`}>`,
|
|
91
111
|
];
|
|
92
112
|
const css = [
|
|
93
113
|
`/* ${appDir}/globals.css - AFTER the tokens.css import */`,
|
package/dist/guide.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* UMA IMPLEMENTAÇÃO SÓ, e este arquivo tinha a terceira.
|
|
3
|
+
*
|
|
4
|
+
* `customFontFamilies` existia aqui, em `fonts.ts` e em `ds-font-link.tsx` - três cópias de um
|
|
5
|
+
* julgamento, e as três enumeravam `[display, body, mono]`. Quando o documento passou a aceitar as
|
|
6
|
+
* famílias que o código dele declara, as três deixaram de carregar a fonte ao mesmo tempo. Este
|
|
7
|
+
* arquivo já importava `nextFontSnippet` do mesmo módulo; a cópia não tinha razão de existir.
|
|
8
|
+
*/
|
|
9
|
+
import { customFontFamilies, familySlots, nextFontSnippet } from "./fonts.js";
|
|
2
10
|
import { animationShorthand, classifyKeyframe, describeKeyframe, isFullRotation, } from "./motion.js";
|
|
3
11
|
const kebab = (v) => v.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
4
12
|
/**
|
|
@@ -25,21 +33,6 @@ const GENERIC_FAMILIES = new Set([
|
|
|
25
33
|
"initial",
|
|
26
34
|
]);
|
|
27
35
|
/** Famílias custom do documento (display/body/mono), deduplicadas, sem genéricos. */
|
|
28
|
-
function customFontFamilies(families) {
|
|
29
|
-
const seen = new Set();
|
|
30
|
-
const out = [];
|
|
31
|
-
for (const family of [families.display, families.body, families.mono]) {
|
|
32
|
-
const name = family?.trim();
|
|
33
|
-
if (!name)
|
|
34
|
-
continue;
|
|
35
|
-
const key = name.toLowerCase();
|
|
36
|
-
if (GENERIC_FAMILIES.has(key) || seen.has(key))
|
|
37
|
-
continue;
|
|
38
|
-
seen.add(key);
|
|
39
|
-
out.push(name);
|
|
40
|
-
}
|
|
41
|
-
return out;
|
|
42
|
-
}
|
|
43
36
|
/**
|
|
44
37
|
* THE STRUCTURE, WITH WHAT EACH PART IS FOR AND WHEN IT EXISTS.
|
|
45
38
|
*
|
|
@@ -269,6 +262,14 @@ export function buildGuide(payload) {
|
|
|
269
262
|
const { document: doc, slug, name, version } = payload;
|
|
270
263
|
const { meta, foundations, motion, components } = doc;
|
|
271
264
|
const semanticRoles = Object.keys(foundations.color.semantic);
|
|
265
|
+
/**
|
|
266
|
+
* AS CORES QUE ELE NOMEIA POR PROPÓSITO, no arquivo que o agente lê primeiro.
|
|
267
|
+
*
|
|
268
|
+
* O compilador passou a emitir `--ds-color-tier-gold` e o `find_token` já as acha (ele lê o
|
|
269
|
+
* `tokens.css`), mas o GUIDE é onde o agente descobre o vocabulário ANTES de perguntar - e ele não
|
|
270
|
+
* as mencionava. São 19 no censo do dono, e são a parte que faz o sistema dele ser dele.
|
|
271
|
+
*/
|
|
272
|
+
const namedColours = Object.keys(foundations.color.named ?? {});
|
|
272
273
|
const seriesKeys = Object.keys(foundations.color.series ?? {});
|
|
273
274
|
const fontFamilies = customFontFamilies(foundations.typography.families);
|
|
274
275
|
const fontsHref = fontFamilies.length > 0
|
|
@@ -680,13 +681,17 @@ ${hasTailwind
|
|
|
680
681
|
- **Always use semantic tokens**, never raw values nor primitives directly.
|
|
681
682
|
Color: \`var(--ds-color-semantic-<role>)\`${hasTailwind ? " (utility: `bg-<role>`/`text-<role>`)" : ""}. The roles are: ${list(semanticRoles)}.
|
|
682
683
|
- Primitives (\`--ds-color-<palette>-<step>\`) exist but should **not** be referenced directly -
|
|
683
|
-
they feed the semantic roles.${
|
|
684
|
+
they feed the semantic roles.${namedColours.length > 0
|
|
685
|
+
? `\n- **Yours by name** → \`var(--ds-color-<name>)\`: ${namedColours.length} colour${namedColours.length === 1 ? "" : "s"} your code names by PURPOSE rather than by step, so no scale could hold ${namedColours.length === 1 ? "it" : "them"} - ${list(namedColours.slice(0, 8))}${namedColours.length > 8 ? ", …" : ""}. These are yours: reach for them when the purpose matches, and prefer a semantic role when it does not.`
|
|
686
|
+
: ""}${seriesKeys.length > 0
|
|
684
687
|
? `\n- Data-viz → \`var(--ds-color-series-<n>)\`${hasTailwind ? " (utility: `bg-series-<n>`/`text-series-<n>`/`fill-series-<n>`)" : ""}: categorical chart/series colors, ${seriesKeys.length} of them (${list(seriesKeys)}). Use them in order for multi-series charts; they re-paint with the system.`
|
|
685
688
|
: ""}
|
|
686
689
|
- Spacing → \`var(--ds-spacing-<key>)\`: ${list(Object.keys(foundations.spacing))}.
|
|
687
690
|
- Radius → \`var(--ds-radius-<key>)\`: ${list(Object.keys(foundations.radius))}.
|
|
688
691
|
- Shadow → \`var(--ds-shadow-<key>)\`: ${list(Object.keys(foundations.shadow))}.
|
|
689
|
-
- Typography: families
|
|
692
|
+
- Typography: families ${familySlots(foundations.typography.families)
|
|
693
|
+
.map((slot) => `\`--ds-typography-families-${slot}\` (${foundations.typography.families[slot]})`)
|
|
694
|
+
.join(", ")};
|
|
690
695
|
weights${hasTailwind ? " (utility: `font-<key>`)" : ""}: ${list(weights)};
|
|
691
696
|
scale \`--ds-typography-scale-<key>-font-size\`${hasTailwind ? " (utility: `text-<key>`)" : ""}: ${list(Object.keys(foundations.typography.scale))}.
|
|
692
697
|
- Motion: durations \`--ds-motion-durations-<key>\` (${list(Object.keys(motion.durations))}) and
|
package/dist/install-marks.js
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
* O índice de componentes é BYTE-IDÊNTICO nos dois caminhos - mesma fonte canônica, mesmos nomes,
|
|
116
116
|
* mesma quantidade (`claude-md.f0.spec.ts`). O corte troca uma frase, nunca o inventário.
|
|
117
117
|
*/
|
|
118
|
-
export const MATERIALISER_SINCE = "0.16.
|
|
118
|
+
export const MATERIALISER_SINCE = "0.16.269";
|
|
119
119
|
/**
|
|
120
120
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
121
121
|
*
|
package/dist/naming-queue.js
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import { anatomyFromSketch } from "./anatomy-from-sketch.js";
|
|
2
2
|
/** O nó que a derivação não soube nomear. `box` é o último recurso dela. */
|
|
3
3
|
const UNNAMED = "box";
|
|
4
|
+
/**
|
|
5
|
+
* O ENCANAMENTO, RECONHECIDO POR REGRA - e ele sai da fila sem sair da árvore.
|
|
6
|
+
*
|
|
7
|
+
* O playbook já enuncia a regra: *"What you still collapse is a wrapper whose only job is `flex` -
|
|
8
|
+
* fold it into the `row` it already is, because that is not a region, it is plumbing."* E a Lei 12 já
|
|
9
|
+
* lê `flex-col`/`grid`/`flex` do class string em `arrangement()`, então **a informação de layout já
|
|
10
|
+
* viaja**. Pedir um nome para um `div` cujo class list inteiro é `mt-5` é pedir uma decisão de modelo
|
|
11
|
+
* que não acrescenta nada ao que o cliente recebe.
|
|
12
|
+
*
|
|
13
|
+
* PADDING NÃO É ENCANAMENTO, e a medição é o que decidiu. `p-4` num modal É o corpo dele; `space-y-3
|
|
14
|
+
* p-4` num cartão É o miolo. Margem, `gap` e `flex` movem o nó no pai e não desenham caixa nenhuma;
|
|
15
|
+
* padding cria uma área interna, que é exatamente o que uma região "body" parece.
|
|
16
|
+
*
|
|
17
|
+
* O custo de ser conservador é pequeno e foi medido, não suposto: dos 757 nós da fila em
|
|
18
|
+
* `apps/web-dashboard`, **311 são só arranjo** e 3 não têm classe, contra **12 de padding puro** e 35
|
|
19
|
+
* de arranjo+padding. Excluir padding custa 47 nós e preserva 47 regiões de verdade.
|
|
20
|
+
*
|
|
21
|
+
* Foi o fixture do próprio spec que levantou a dúvida - `p-[1rem] pr-[2rem] pl-[2rem]`, do
|
|
22
|
+
* `SubscribeSetting` real - e ele continua sendo cobrado, porque continua sendo uma região.
|
|
23
|
+
*
|
|
24
|
+
* NADA É RENOMEADO. O nó continua na árvore com o `box` que a derivação lhe deu, e `part` continua
|
|
25
|
+
* saindo igual - inclusive o sufixo `-2`. Renomear seria mudar chave de `part` no documento do
|
|
26
|
+
* cliente, que é referenciada por codegen e por override dele: um ganho de tempo nosso não paga isso.
|
|
27
|
+
*
|
|
28
|
+
* A RÉGUA É CONSERVADORA NO LADO CERTO: qualquer classe de APARÊNCIA mantém o nó na fila.
|
|
29
|
+
* `w-full bg-darkgray-800/95 absolute h-full` sobrevive por causa do `bg-`; o que cai é `mt-5`.
|
|
30
|
+
*/
|
|
31
|
+
const PLUMBING = /^(?:m[trblxy]?-|space-[xy]-|gap(?:-[xy])?-|flex$|flex-|grid$|grid-|items-|justify-|self-|content-|place-|w-full$|h-full$|min-[wh]-|max-[wh]-|relative$|absolute$|static$|block$|inline(?:-|$)|shrink|grow|order-|col-|row-|basis-)/;
|
|
32
|
+
/** Só encanamento, ou nenhuma classe: os dois casos são estrutura sem aparência. */
|
|
33
|
+
function isPlumbing(classes) {
|
|
34
|
+
const words = (classes ?? "").trim().split(/\s+/).filter(Boolean);
|
|
35
|
+
return words.every((w) => PLUMBING.test(w));
|
|
36
|
+
}
|
|
4
37
|
function walk(read, onNode) {
|
|
5
38
|
for (const node of read) {
|
|
6
39
|
onNode(node);
|
|
@@ -21,6 +54,7 @@ export function namingQueue(looks, defines, versionOf) {
|
|
|
21
54
|
let nodes = 0;
|
|
22
55
|
let named = 0;
|
|
23
56
|
let settled = 0;
|
|
57
|
+
let plumbing = 0;
|
|
24
58
|
for (const component of Object.keys(looks)) {
|
|
25
59
|
const sketch = looks[component]?.sketch;
|
|
26
60
|
if (!Array.isArray(sketch) || sketch.length === 0)
|
|
@@ -40,6 +74,14 @@ export function namingQueue(looks, defines, versionOf) {
|
|
|
40
74
|
if (typeof at !== "number")
|
|
41
75
|
return;
|
|
42
76
|
const raw = sketch[at];
|
|
77
|
+
/**
|
|
78
|
+
* Encanamento não entra na fila, e é CONTADO - senão a diferença entre "não havia trabalho" e
|
|
79
|
+
* "a régua o dispensou" fica invisível, e um número que ninguém explica é pior que nenhum.
|
|
80
|
+
*/
|
|
81
|
+
if (isPlumbing(raw?.classes)) {
|
|
82
|
+
plumbing += 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
43
85
|
unnamed.push({
|
|
44
86
|
tag: raw?.tag ?? "div",
|
|
45
87
|
at,
|
|
@@ -52,5 +94,5 @@ export function namingQueue(looks, defines, versionOf) {
|
|
|
52
94
|
}
|
|
53
95
|
pending.push({ component, nodes: unnamed });
|
|
54
96
|
}
|
|
55
|
-
return { components, nodes, named, settled, pending };
|
|
97
|
+
return { components, nodes, named, settled, plumbing, pending };
|
|
56
98
|
}
|
package/package.json
CHANGED