synthesisui 0.16.130 → 0.16.132
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-from-sketch.js +10 -3
- package/dist/commands/import.js +102 -3
- package/dist/doctor/component-gate.js +25 -1
- package/dist/doctor/css-modules.js +56 -3
- package/dist/doctor/page-laws.js +106 -0
- package/dist/doctor/sketch.js +9 -0
- package/package.json +1 -1
|
@@ -467,9 +467,16 @@ versionOf) {
|
|
|
467
467
|
*/
|
|
468
468
|
if (hasKids && as !== "row" && as !== "stack")
|
|
469
469
|
as = arrangement(classes);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
470
|
+
/**
|
|
471
|
+
* A plain structural box earns a name only when it carries decisions - and it must earn
|
|
472
|
+
* one, because the name is what the class string travels on.
|
|
473
|
+
*
|
|
474
|
+
* UMA CLASSE DE MODULE TAMBÉM É DECISÃO. Num app de CSS Modules o elemento veste
|
|
475
|
+
* `className={styles.content}` e nenhuma string: sem isto o nó não ganhava nome, não virava
|
|
476
|
+
* part, e o bloco de 16 declarações lido do module não tinha onde pousar. O Tooltip dele
|
|
477
|
+
* chegava com 2 das 35 (04/08).
|
|
478
|
+
*/
|
|
479
|
+
if (!name && (classes || node.moduleClass))
|
|
473
480
|
name = "box";
|
|
474
481
|
/**
|
|
475
482
|
* A NODE WHOSE CONTENT IS THE CALLER'S is a slot, and it keeps its own name
|
package/dist/commands/import.js
CHANGED
|
@@ -17,6 +17,7 @@ import { keyframeOffsets, moduleImports, readModuleCss, readModuleUsage, transcr
|
|
|
17
17
|
import { dataContract } from "../doctor/data-contract.js";
|
|
18
18
|
import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
|
|
19
19
|
import { describeConvention, describeRemainder, detectConventions, } from "../doctor/idiom.js";
|
|
20
|
+
import { pageLaws } from "../doctor/page-laws.js";
|
|
20
21
|
import { callSiteNode, placeLayers, } from "../doctor/place-layers.js";
|
|
21
22
|
import { publicApi, requiredProps } from "../doctor/public-api.js";
|
|
22
23
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
@@ -263,6 +264,10 @@ export async function takeCensus(root, opts) {
|
|
|
263
264
|
const defined = [];
|
|
264
265
|
/** Nome → arquivo e fonte, para toda leitura por componente - ver o preenchimento abaixo. */
|
|
265
266
|
const sourceOf = new Map();
|
|
267
|
+
/** Nome → o que a peça é, quando o portão soube dizer. Ver `CensusLook.kind`. */
|
|
268
|
+
const kindOf = new Map();
|
|
269
|
+
/** A raiz e a composição de cada página, para as leis - ver `pageLaws`. */
|
|
270
|
+
const pages = [];
|
|
266
271
|
/** What the gate turned away, with the reason. Never dropped in silence. */
|
|
267
272
|
const skips = [];
|
|
268
273
|
// Kept for the reference check below, which needs every file at once: a name
|
|
@@ -391,9 +396,33 @@ export async function takeCensus(root, opts) {
|
|
|
391
396
|
internal,
|
|
392
397
|
});
|
|
393
398
|
if (verdict.ok) {
|
|
394
|
-
|
|
399
|
+
/**
|
|
400
|
+
* O QUE A PEÇA É viaja com ela - ver `GateVerdict.kind`. Um template decide arranjo e
|
|
401
|
+
* não aparência, e três superfícies precisam saber disso para não cobrar cor de quem
|
|
402
|
+
* nunca prometeu cor.
|
|
403
|
+
*/
|
|
404
|
+
found.push(verdict.kind ? { ...d, kind: verdict.kind } : d);
|
|
395
405
|
continue;
|
|
396
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* A RAIZ DE UMA PÁGINA E O QUE ELA COMPÕE, para as leis - ver `pageLaws`.
|
|
409
|
+
*
|
|
410
|
+
* Uma rota individualmente não vale receita: 5 elementos de média e a raiz repetindo o
|
|
411
|
+
* que outras oito já disseram. Juntas elas dizem como é a casca deste projeto, que é o
|
|
412
|
+
* que falta para alguém escrever a próxima (04/08).
|
|
413
|
+
*/
|
|
414
|
+
if (verdict.why === "route" || verdict.why === "screen") {
|
|
415
|
+
const sketch = sketchOf(src, d.name);
|
|
416
|
+
const root = sketch[0];
|
|
417
|
+
pages.push({
|
|
418
|
+
root: root?.classes
|
|
419
|
+
? transcribe(root.classes.split(/\s+/).filter(Boolean), declaredValues).base
|
|
420
|
+
: {},
|
|
421
|
+
composes: sketch
|
|
422
|
+
.filter((n) => /^[A-Z]/.test(n.tag))
|
|
423
|
+
.map((n) => n.tag.split(".")[0]),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
397
426
|
skips.push({
|
|
398
427
|
name: d.name,
|
|
399
428
|
file: rel,
|
|
@@ -426,8 +455,12 @@ export async function takeCensus(root, opts) {
|
|
|
426
455
|
* numa lista feita para biblioteca. O look já nasce aqui, com o arquivo e a fonte na mão,
|
|
427
456
|
* e é aqui que a leitura tem que se pendurar.
|
|
428
457
|
*/
|
|
429
|
-
for (const d of found)
|
|
458
|
+
for (const d of found) {
|
|
430
459
|
sourceOf.set(d.name, { file: rel, source: src });
|
|
460
|
+
/** O que a peça É, do portão - ver `GateVerdict.kind` e `CensusLook.kind`. */
|
|
461
|
+
if (d.kind)
|
|
462
|
+
kindOf.set(d.name, d.kind);
|
|
463
|
+
}
|
|
431
464
|
// THE LOOK, from their own class names. One transcription per file, keyed
|
|
432
465
|
// by the component it defines - the root element's classes are the
|
|
433
466
|
// component's own look, and everything below it is a part this slice does
|
|
@@ -733,6 +766,18 @@ export async function takeCensus(root, opts) {
|
|
|
733
766
|
if (hasScreens) {
|
|
734
767
|
const keep = [];
|
|
735
768
|
for (const d of defined) {
|
|
769
|
+
/**
|
|
770
|
+
* UM TEMPLATE VIVE NUMA TELA SÓ POR DEFINIÇÃO - ele É a tela.
|
|
771
|
+
*
|
|
772
|
+
* A regra de duas telas existe para separar "decisão que o produto compartilha" de
|
|
773
|
+
* "markup daquela tela", e está certa para uma PEÇA. Aplicada a um template ela recusa
|
|
774
|
+
* exatamente o que o portão acabou de aceitar: as 16 telas do dashboard dele passavam o
|
|
775
|
+
* portão como `template` e caíam aqui, aparecendo no relatório como `feature` (04/08).
|
|
776
|
+
*/
|
|
777
|
+
if (d.kind === "template") {
|
|
778
|
+
keep.push(d);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
736
781
|
const screens = screensOf.get(d.name)?.size ?? 0;
|
|
737
782
|
if (screens >= SCREENS_FOR_SYSTEM) {
|
|
738
783
|
keep.push(d);
|
|
@@ -1126,6 +1171,19 @@ export async function takeCensus(root, opts) {
|
|
|
1126
1171
|
* The skill asks which has priority; the chosen one becomes a system-wide rule that
|
|
1127
1172
|
* reaches CLAUDE.md, so every prompt after this respects where a new component goes.
|
|
1128
1173
|
*/
|
|
1174
|
+
/**
|
|
1175
|
+
* AS LEIS DAS PÁGINAS, ditas onde a arquitetura já é dita - governam o sistema, não uma peça,
|
|
1176
|
+
* então `applies` é vazio e elas chegam a todo prompt. Ver `pageLaws`.
|
|
1177
|
+
*/
|
|
1178
|
+
const pageRules = pageLaws(pages);
|
|
1179
|
+
if (pageRules.length > 0) {
|
|
1180
|
+
console.log("");
|
|
1181
|
+
console.log(section("What your pages agree on"));
|
|
1182
|
+
for (const law of pageRules) {
|
|
1183
|
+
console.log(body(law.text));
|
|
1184
|
+
console.log(body(paint.faint(` ${law.evidence}`)));
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1129
1187
|
if (architectures.length > 0) {
|
|
1130
1188
|
console.log("");
|
|
1131
1189
|
console.log(section("How this project is organised"));
|
|
@@ -1332,8 +1390,12 @@ export async function takeCensus(root, opts) {
|
|
|
1332
1390
|
if (c)
|
|
1333
1391
|
c.api = stamped;
|
|
1334
1392
|
const look = looks[name];
|
|
1335
|
-
if (look)
|
|
1393
|
+
if (look) {
|
|
1336
1394
|
look.api = stamped;
|
|
1395
|
+
const kind = kindOf.get(name);
|
|
1396
|
+
if (kind)
|
|
1397
|
+
look.kind = kind;
|
|
1398
|
+
}
|
|
1337
1399
|
}
|
|
1338
1400
|
const brokenRefs = findBrokenRefs(sources, declaredNames);
|
|
1339
1401
|
const conventions = detectConventions(sources);
|
|
@@ -2174,6 +2236,43 @@ async function resolveReadParts(census, root) {
|
|
|
2174
2236
|
}
|
|
2175
2237
|
target.base = { ...style, ...target.base };
|
|
2176
2238
|
}
|
|
2239
|
+
/**
|
|
2240
|
+
* O QUE UM CSS MODULE DECLAROU, ONTO THE PART THAT WEARS THAT CLASS.
|
|
2241
|
+
*
|
|
2242
|
+
* Num app de CSS Modules a classe do module é a identidade do elemento, e a anatomia dá
|
|
2243
|
+
* outro nome ao mesmo nó - o `content` do Tooltip dele vira `text` pelo papel. As duas
|
|
2244
|
+
* listas nunca se encontravam, porque a junção era por NOME: o módulo entregava
|
|
2245
|
+
* `parts.content` com 16 declarações e a anatomia substituía tudo por `parts.text` vazia.
|
|
2246
|
+
* As 35 declarações do Tooltip chegavam como 2 (04/08).
|
|
2247
|
+
*
|
|
2248
|
+
* A junção é por CLASSE, que é um dado do próprio nó - ver `SketchNode.moduleClass`. Uma
|
|
2249
|
+
* classe cujo nó a anatomia não nomeia é dita em voz alta, nunca empurrada para a raiz.
|
|
2250
|
+
*/
|
|
2251
|
+
const sketch = (look?.sketch ?? []);
|
|
2252
|
+
const fromModule = look?.parts ?? {};
|
|
2253
|
+
for (const [index, node] of sketch.entries()) {
|
|
2254
|
+
const cls = node.moduleClass;
|
|
2255
|
+
if (!cls)
|
|
2256
|
+
continue;
|
|
2257
|
+
const block = fromModule[cls];
|
|
2258
|
+
if (!block)
|
|
2259
|
+
continue;
|
|
2260
|
+
const part = partAt[index];
|
|
2261
|
+
const target = part ? resolved.parts[part] : undefined;
|
|
2262
|
+
if (!target) {
|
|
2263
|
+
notes.add(`\`.${cls}\` in a CSS module styles an element the anatomy does not name, so it is not applied - putting it on the component would paint the wrong thing`);
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
/** O que a class string já disse ganha: um utilitário no mesmo elemento é mais recente. */
|
|
2267
|
+
target.base = { ...block.base, ...target.base };
|
|
2268
|
+
if (Object.keys(block.dark ?? {}).length > 0)
|
|
2269
|
+
target.dark = { ...block.dark, ...target.dark };
|
|
2270
|
+
for (const [state, decls] of Object.entries(block.states ?? {}))
|
|
2271
|
+
target.states = {
|
|
2272
|
+
...target.states,
|
|
2273
|
+
[state]: { ...decls, ...(target.states?.[state] ?? {}) },
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2177
2276
|
/**
|
|
2178
2277
|
* REPLACED, NOT APPENDED - because this runs more than once.
|
|
2179
2278
|
*
|
|
@@ -43,6 +43,19 @@ const RESERVED_FILE_OTHER = /(^|[/\\])(\+page|\+layout|\+server|_app|_document|_
|
|
|
43
43
|
* hundred libraries. It only counts when the file itself is a route.
|
|
44
44
|
*/
|
|
45
45
|
const SCREEN_SUFFIX = /(Page|Screen|Route|Template|Layout)$/;
|
|
46
|
+
/**
|
|
47
|
+
* Quantos elementos uma tela tem e quantos deles são peça de alguém - o que separa um template
|
|
48
|
+
* de uma casca. Contado no arquivo, sem sketch: este portão roda antes de qualquer leitura.
|
|
49
|
+
*/
|
|
50
|
+
function screenShape(source, name) {
|
|
51
|
+
const at = source.indexOf(name);
|
|
52
|
+
const body = at === -1 ? source : source.slice(at);
|
|
53
|
+
const tags = [...body.matchAll(/<([A-Za-z][\w.]*)/g)].map((m) => m[1]);
|
|
54
|
+
return {
|
|
55
|
+
nodes: tags.length,
|
|
56
|
+
composes: new Set(tags.filter((t) => /^[A-Z]/.test(t)).map((t) => t.split(".")[0])).size,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
46
59
|
/** Something that carries context down the tree rather than drawing. */
|
|
47
60
|
const PROVIDER_SUFFIX = /(Provider|Providers|Context|Boundary|Guard|Wrapper)$/;
|
|
48
61
|
/** Any JSX at all in the file. A capitalised export with none is a constant, a
|
|
@@ -174,11 +187,22 @@ export function gateComponent(input) {
|
|
|
174
187
|
because: `\`${file}\` is a file name the framework reserves - it is a route, not a component somebody can import`,
|
|
175
188
|
};
|
|
176
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* UMA TELA ENTRA COMO TEMPLATE, e o que decide é o que ela FAZ.
|
|
192
|
+
*
|
|
193
|
+
* O nome é a pista (`…Template`, `…Screen`, `…Page`) e não a prova: uma tela que compõe três
|
|
194
|
+
* ou mais peças numa árvore de dez ou mais nós é uma decisão de arranjo que vale receita -
|
|
195
|
+
* `CurateTemplate` tem 89 nós e 237 declarações. Uma que não alcança isso é casca, e casca é
|
|
196
|
+
* lei, não peça.
|
|
197
|
+
*/
|
|
177
198
|
if (SCREEN_SUFFIX.test(name)) {
|
|
199
|
+
const shape = screenShape(source, name);
|
|
200
|
+
if (shape.composes >= 3 && shape.nodes >= 10)
|
|
201
|
+
return { ok: true, kind: "template" };
|
|
178
202
|
return {
|
|
179
203
|
ok: false,
|
|
180
204
|
why: "screen",
|
|
181
|
-
because: `\`${name}\` names a
|
|
205
|
+
because: `\`${name}\` names a screen, and composes ${shape.composes} piece${shape.composes === 1 ? "" : "s"} across ${shape.nodes} element${shape.nodes === 1 ? "" : "s"} - too thin to be a template of yours. A design system is made of the pieces a screen is built FROM`,
|
|
182
206
|
};
|
|
183
207
|
}
|
|
184
208
|
if (PROVIDER_SUFFIX.test(name) || IS_CONTEXT.test(source)) {
|
|
@@ -83,7 +83,19 @@ function rules(css, media = null, out = []) {
|
|
|
83
83
|
const open = css.indexOf("{", i);
|
|
84
84
|
if (open === -1)
|
|
85
85
|
break;
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* O SELETOR É O QUE VEM DEPOIS DA ÚLTIMA DECLARAÇÃO, não o texto todo até a chave.
|
|
88
|
+
*
|
|
89
|
+
* Num CSS plano o cabeçalho de uma regra já vem limpo, e por isso isto passou. Num `.scss`
|
|
90
|
+
* aninhado não: o corpo de `.content` é
|
|
91
|
+
*
|
|
92
|
+
* background-color: …; border-radius: 4px; .header {
|
|
93
|
+
*
|
|
94
|
+
* e o cabeçalho da regra do `.header` saía com as duas declarações grudadas na frente.
|
|
95
|
+
* `classesIn` então achava fragmentos como `0` (de `opacity: 0`) e 12 seletores por arquivo
|
|
96
|
+
* iam para `unslotted` (04/08).
|
|
97
|
+
*/
|
|
98
|
+
const head = css.slice(i, open).split(/[;}]/).pop()?.trim() ?? "";
|
|
87
99
|
// Balance to the matching close, because an at-rule contains whole rules.
|
|
88
100
|
let depth = 0;
|
|
89
101
|
let close = -1;
|
|
@@ -113,6 +125,33 @@ function rules(css, media = null, out = []) {
|
|
|
113
125
|
}
|
|
114
126
|
else if (head) {
|
|
115
127
|
out.push({ selector: head, body, media });
|
|
128
|
+
/**
|
|
129
|
+
* UM BLOCO ANINHADO É UMA REGRA, e não texto do pai.
|
|
130
|
+
*
|
|
131
|
+
* Esta varredura era plana, e num `.scss` isso significa que
|
|
132
|
+
*
|
|
133
|
+
* [data-theme="light"] { .container { .content { background } } }
|
|
134
|
+
*
|
|
135
|
+
* produzia UMA regra: seletor `[data-theme="light"]`, sem classe nenhuma, então
|
|
136
|
+
* `unslotted` - e o bloco inteiro ia para o lixo. Já a versão sem tema produzia
|
|
137
|
+
* `.container` com TODAS as declarações de todos os níveis, porque `declarations()`
|
|
138
|
+
* varre o corpo e o corpo é o arquivo. Medido no repo dele (04/08): 196 de 200 modules
|
|
139
|
+
* usam aninhamento, o Tooltip saiu com 19 declarações na raiz e 4 classes perdidas, 969
|
|
140
|
+
* de 1637 parts vieram vazias, e 109 blocos dark viraram 5.
|
|
141
|
+
*
|
|
142
|
+
* Descer com o seletor do pai na mão resolve os três de uma vez: o filho passa a ter
|
|
143
|
+
* seletor completo, `classesIn` acha a classe certa, a relação pai-filho é registrada, e
|
|
144
|
+
* `[data-theme="dark"] .x` casa o teste de tema. O `&` do Sass é resolvido contra o pai,
|
|
145
|
+
* que é o que ele significa.
|
|
146
|
+
*/
|
|
147
|
+
for (const nested of rules(body, media)) {
|
|
148
|
+
out.push({
|
|
149
|
+
...nested,
|
|
150
|
+
selector: nested.selector.includes("&")
|
|
151
|
+
? nested.selector.replace(/&/g, head)
|
|
152
|
+
: `${head} ${nested.selector}`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
116
155
|
}
|
|
117
156
|
i = close + 1;
|
|
118
157
|
}
|
|
@@ -121,8 +160,22 @@ function rules(css, media = null, out = []) {
|
|
|
121
160
|
/** `prop: value;` pairs, in the camelCase the contract uses. */
|
|
122
161
|
function declarations(body) {
|
|
123
162
|
const out = {};
|
|
124
|
-
|
|
125
|
-
|
|
163
|
+
/**
|
|
164
|
+
* SÓ O NÍVEL DE CIMA - e "só" exige repetir, porque um module aninha mais de um nível.
|
|
165
|
+
*
|
|
166
|
+
* Uma passada tira o bloco mais interno e deixa o do meio aberto, então as declarações do
|
|
167
|
+
* meio vazavam para o pai: `.card { padding; .title { font-size; > h3 { margin } } }` dava
|
|
168
|
+
* `font-size` ao card e `margin` ao title. Com 196 de 200 modules dele aninhando e a maioria
|
|
169
|
+
* em três níveis, é o vazamento que fazia a raiz vestir o module inteiro (04/08).
|
|
170
|
+
*/
|
|
171
|
+
let flat = body;
|
|
172
|
+
for (let guard = 0; guard < 12; guard++) {
|
|
173
|
+
const next = flat.replace(/\{[^{}]*\}/g, "");
|
|
174
|
+
if (next === flat)
|
|
175
|
+
break;
|
|
176
|
+
flat = next;
|
|
177
|
+
}
|
|
178
|
+
flat = flat.replace(/[^;{}]*\{/g, "");
|
|
126
179
|
for (const piece of flat.split(";")) {
|
|
127
180
|
const colon = piece.indexOf(":");
|
|
128
181
|
if (colon === -1)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O QUE AS PÁGINAS DE UM APP CONCORDAM, quando nenhuma delas vale uma receita.
|
|
3
|
+
*
|
|
4
|
+
* As 129 rotas do dashboard dele têm média de 5 elementos e 2 peças compostas, e a raiz de uma
|
|
5
|
+
* repete o que outras oito já disseram. Cada uma individualmente não tem o que uma receita
|
|
6
|
+
* carrega - virar 129 entradas na vitrine seria o problema de escala sem o benefício.
|
|
7
|
+
*
|
|
8
|
+
* Mas juntas elas dizem uma coisa que hoje não existe em lugar nenhum do sistema. Medido nas 129
|
|
9
|
+
* (04/08):
|
|
10
|
+
*
|
|
11
|
+
* width: 100% 12 páginas
|
|
12
|
+
* position: sticky 8 ┐
|
|
13
|
+
* top: 0 8 ├ juntos: o cabeçalho fixo de página
|
|
14
|
+
* z-index: 50 8 ┘
|
|
15
|
+
* padding: 1rem 8
|
|
16
|
+
* min-height: 100vh 6 a casca da página
|
|
17
|
+
* ContentWrapper 22 páginas compõem
|
|
18
|
+
* PageHeader 14
|
|
19
|
+
*
|
|
20
|
+
* Três ou mais arquivos concordando é hábito - a mesma escada que o resto da esteira usa. E uma
|
|
21
|
+
* lei com a contagem atrás vale mais que a receita de qualquer uma das 129, porque é isso que
|
|
22
|
+
* alguém precisa saber para escrever a 130ª.
|
|
23
|
+
*
|
|
24
|
+
* O QUE ELA NUNCA FAZ: propor. Uma lei aqui é a descrição do que eles já fazem, com o número, e
|
|
25
|
+
* o que NÃO se repete não vira lei - duas páginas concordando é coincidência, e chamar isso de
|
|
26
|
+
* convenção do projeto seria inventar uma decisão que ninguém tomou.
|
|
27
|
+
*/
|
|
28
|
+
/** O mínimo para um hábito, e é o mesmo número que o resto da esteira usa. */
|
|
29
|
+
const AGREE = 3;
|
|
30
|
+
/** Propriedades que descrevem a MOLDURA de uma página, e não a aparência de uma peça. */
|
|
31
|
+
const SHELL = /^(minHeight|height|width|maxWidth|padding|paddingTop|paddingBottom|margin|marginTop|display|gridTemplateColumns|gap|position|top|zIndex|overflow|justifyContent|alignItems)$/;
|
|
32
|
+
/**
|
|
33
|
+
* As leis que as páginas de um escopo concordam, ou nada quando não concordam.
|
|
34
|
+
*
|
|
35
|
+
* `pages` é uma entrada por página: o que a RAIZ dela declara e quais peças ela compõe. Quem
|
|
36
|
+
* chama já leu as duas coisas para o relatório do portão, então isto não abre nenhum arquivo.
|
|
37
|
+
*/
|
|
38
|
+
export function pageLaws(pages) {
|
|
39
|
+
if (pages.length < AGREE)
|
|
40
|
+
return [];
|
|
41
|
+
const values = new Map();
|
|
42
|
+
const composed = new Map();
|
|
43
|
+
for (const page of pages) {
|
|
44
|
+
for (const [prop, value] of Object.entries(page.root)) {
|
|
45
|
+
if (!SHELL.test(prop))
|
|
46
|
+
continue;
|
|
47
|
+
const key = `${prop}: ${value}`;
|
|
48
|
+
values.set(key, (values.get(key) ?? 0) + 1);
|
|
49
|
+
}
|
|
50
|
+
for (const name of new Set(page.composes))
|
|
51
|
+
composed.set(name, (composed.get(name) ?? 0) + 1);
|
|
52
|
+
}
|
|
53
|
+
const agreed = [...values.entries()]
|
|
54
|
+
.filter(([, n]) => n >= AGREE)
|
|
55
|
+
.sort((a, b) => b[1] - a[1]);
|
|
56
|
+
const laws = [];
|
|
57
|
+
/**
|
|
58
|
+
* A CASCA: o que a raiz de uma página deste projeto declara. Uma altura mínima de viewport e um
|
|
59
|
+
* padding de container não são a aparência de uma peça - são a moldura, e é o que falta a quem
|
|
60
|
+
* vai escrever a próxima página.
|
|
61
|
+
*/
|
|
62
|
+
const shell = agreed.filter(([key]) => !/^(position|top|zIndex):/.test(key));
|
|
63
|
+
if (shell.length > 0) {
|
|
64
|
+
laws.push({
|
|
65
|
+
text: `A page of this project is ${shell
|
|
66
|
+
.slice(0, 4)
|
|
67
|
+
.map(([key]) => `\`${key}\``)
|
|
68
|
+
.join(", ")}` +
|
|
69
|
+
`${composed.size > 0 && (composed.get(topOf(composed)) ?? 0) >= AGREE ? ` and puts its content inside \`${topOf(composed)}\`` : ""}` +
|
|
70
|
+
`. Write the next one the same way, or change all of them.`,
|
|
71
|
+
applies: [],
|
|
72
|
+
kind: "implementation",
|
|
73
|
+
evidence: shell
|
|
74
|
+
.slice(0, 4)
|
|
75
|
+
.map(([key, n]) => `${key} in ${n} of ${pages.length} pages`)
|
|
76
|
+
.join("; "),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* O CABEÇALHO FIXO: os três juntos são um padrão, e separados não dizem nada. Só vale como lei
|
|
81
|
+
* quando os três concordam, porque `position: sticky` sozinho pode ser qualquer coisa.
|
|
82
|
+
*/
|
|
83
|
+
const sticky = agreed.filter(([key]) => /^(position: sticky|top: 0|zIndex:)/.test(key));
|
|
84
|
+
if (sticky.length >= 3) {
|
|
85
|
+
const header = [...composed.entries()]
|
|
86
|
+
.filter(([name, n]) => n >= AGREE && /header/i.test(name))
|
|
87
|
+
.sort((a, b) => b[1] - a[1])[0];
|
|
88
|
+
laws.push({
|
|
89
|
+
text: `The header of a page is ${sticky
|
|
90
|
+
.map(([key]) => `\`${key}\``)
|
|
91
|
+
.join(", ")}` +
|
|
92
|
+
`${header ? ` and it is \`${header[0]}\`` : ""}` +
|
|
93
|
+
`. A second sticky element at another z-index is how two headers end up fighting.`,
|
|
94
|
+
applies: [],
|
|
95
|
+
kind: "implementation",
|
|
96
|
+
evidence: sticky
|
|
97
|
+
.map(([key, n]) => `${key} in ${n} of ${pages.length} pages`)
|
|
98
|
+
.join("; "),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return laws;
|
|
102
|
+
}
|
|
103
|
+
/** O nome mais composto, que é o candidato a "onde o conteúdo vai". */
|
|
104
|
+
function topOf(composed) {
|
|
105
|
+
return [...composed.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "";
|
|
106
|
+
}
|
package/dist/doctor/sketch.js
CHANGED
|
@@ -427,6 +427,11 @@ function closeOf(source, open) {
|
|
|
427
427
|
}
|
|
428
428
|
return -1;
|
|
429
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* `className={styles.content}` e `className={s.content}` - a forma que um app de CSS Modules
|
|
432
|
+
* usa em todo elemento. O identificador é o import do module, e a propriedade é a classe.
|
|
433
|
+
*/
|
|
434
|
+
const MODULE_CLASS = /className=\{\s*([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*\}/;
|
|
430
435
|
export function sketchOf(source, name) {
|
|
431
436
|
/**
|
|
432
437
|
* READ THE IMPORTS BEFORE SLICING - they live at the top of the file, and the
|
|
@@ -519,6 +524,10 @@ export function sketchOf(source, name) {
|
|
|
519
524
|
const list = repeatOf(source, event.at);
|
|
520
525
|
if (list)
|
|
521
526
|
node.repeats = list;
|
|
527
|
+
/** A classe de module que ele veste - ver `moduleClass`. */
|
|
528
|
+
const mod = MODULE_CLASS.exec(event.body);
|
|
529
|
+
if (mod)
|
|
530
|
+
node.moduleClass = mod[2];
|
|
522
531
|
// What has to be true for this element to exist at all - see `guardAt`.
|
|
523
532
|
const guard = guardAt(source, event.at);
|
|
524
533
|
if (guard) {
|
package/package.json
CHANGED