synthesisui 0.16.329 → 0.16.330
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/commands/import.js +72 -9
- package/dist/doctor/default-scheme.js +143 -0
- package/dist/doctor/scheme-blocks.js +42 -65
- package/dist/doctor/scheme-scope.js +272 -0
- package/dist/doctor/tokens.js +12 -96
- package/dist/install-marks.js +1 -1
- package/dist/merge-census.js +41 -0
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -18,6 +18,7 @@ import { classifyAside, crosswalk, floorSize, isLibrary, observedRules, useLiveC
|
|
|
18
18
|
import { keyframeOffsets, moduleImports, partialCandidates, readGlobalClasses, readModuleCss, readModuleUsage, sheetImports, transcribeModule, } from "../doctor/css-modules.js";
|
|
19
19
|
import { dataContract } from "../doctor/data-contract.js";
|
|
20
20
|
import { inherit, readDeclaredForms, } from "../doctor/declared-forms.js";
|
|
21
|
+
import { defaultSchemeOf, } from "../doctor/default-scheme.js";
|
|
21
22
|
import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
|
|
22
23
|
import { accountExportsInto, emptyExportAccount, exportInvariantHolds, } from "../doctor/export-census.js";
|
|
23
24
|
import { fragmentsOfSource, fragmentsOfStylesheet, judgeFragments, } from "../doctor/fragments.js";
|
|
@@ -2101,6 +2102,24 @@ export async function takeCensus(root, opts) {
|
|
|
2101
2102
|
* `workspaces` declaram, que é onde recharts/chakra/mui realmente moram.
|
|
2102
2103
|
*/
|
|
2103
2104
|
finishSignals(signals, importTally, versions);
|
|
2105
|
+
/**
|
|
2106
|
+
* QUAL FACE O PROJETO ABRE - observação estrutural, aqui porque os sinais de tema já
|
|
2107
|
+
* fecharam. Ver `default-scheme.ts` para a escada de evidência e para por que ela nunca
|
|
2108
|
+
* usa luminância. `census.scheme`, decidido bem depois em `runImport`, é outra coisa: a
|
|
2109
|
+
* decisão RESOLVIDA daquela execução, e ela nunca reescreve o que foi observado.
|
|
2110
|
+
*/
|
|
2111
|
+
const polarity = defaultSchemeOf({
|
|
2112
|
+
sheets: globalSheets.map((g) => ({ file: g.file, css: g.body })),
|
|
2113
|
+
signals: signals.theme,
|
|
2114
|
+
});
|
|
2115
|
+
const declaredSchemes = {
|
|
2116
|
+
base: Object.fromEntries(schemes.base),
|
|
2117
|
+
light: Object.fromEntries(schemes.light),
|
|
2118
|
+
dark: Object.fromEntries(schemes.dark),
|
|
2119
|
+
defaultScheme: polarity.scheme,
|
|
2120
|
+
defaultSchemeSource: polarity.source,
|
|
2121
|
+
...(polarity.evidence ? { defaultSchemeEvidence: polarity.evidence } : {}),
|
|
2122
|
+
};
|
|
2104
2123
|
// Every name their CSS defines, from the same harvest the token table came
|
|
2105
2124
|
// from - so a reference is judged against what actually exists, not against
|
|
2106
2125
|
// the subset we managed to read as a ramp.
|
|
@@ -2343,9 +2362,10 @@ export async function takeCensus(root, opts) {
|
|
|
2343
2362
|
classStyle,
|
|
2344
2363
|
...(Object.keys(looks).length > 0 ? { looks } : {}),
|
|
2345
2364
|
...(naming.components > 0 ? { naming } : {}),
|
|
2346
|
-
...(schemes.
|
|
2347
|
-
? { declaredAlt: Object.fromEntries(schemes.
|
|
2365
|
+
...(schemes.dark.size > 0
|
|
2366
|
+
? { declaredAlt: Object.fromEntries(schemes.dark) }
|
|
2348
2367
|
: {}),
|
|
2368
|
+
declaredSchemes,
|
|
2349
2369
|
observed: distinctValues(d),
|
|
2350
2370
|
...(components.length > 0 ? { components } : {}),
|
|
2351
2371
|
...(defined.length > 0 ? { defined } : {}),
|
|
@@ -3351,6 +3371,41 @@ async function askScheme(suggested) {
|
|
|
3351
3371
|
* light greys, and its dashboard is near-black - so the count agrees with the
|
|
3352
3372
|
* screen. It is only a default; the person confirms it either way.
|
|
3353
3373
|
*/
|
|
3374
|
+
/**
|
|
3375
|
+
* A FACE QUE O PROJETO ABRE, quando a estrutura dele respondeu - senão `null`.
|
|
3376
|
+
*
|
|
3377
|
+
* Só vale para medição NOVA: um censo sem `declaredSchemes` foi medido por uma versão que
|
|
3378
|
+
* não fazia esta pergunta, e responder por ele seria inventar. Ver a nota de compatibilidade
|
|
3379
|
+
* em `Census.declaredSchemes`.
|
|
3380
|
+
*/
|
|
3381
|
+
export function observedPolarity(census) {
|
|
3382
|
+
const fact = census.declaredSchemes;
|
|
3383
|
+
if (!fact || fact.defaultScheme === "unknown")
|
|
3384
|
+
return null;
|
|
3385
|
+
return fact.defaultScheme;
|
|
3386
|
+
}
|
|
3387
|
+
/**
|
|
3388
|
+
* QUANDO A ESTEIRA TEM DE FICAR CALADA sobre qual face abre.
|
|
3389
|
+
*
|
|
3390
|
+
* Medição nova, polaridade `unknown`, e nenhum lado declarado explicitamente: aqui a base
|
|
3391
|
+
* é de verdade indeterminada, e a escada de neutros só sabe responder por LUMINÂNCIA - a
|
|
3392
|
+
* evidência que este contrato recusa. Sem TTY, `askScheme` devolve esse palpite calado, e
|
|
3393
|
+
* era assim que "a base é clara" voltava pela porta dos fundos.
|
|
3394
|
+
*
|
|
3395
|
+
* Com pelo menos um override explícito, as duas faces são conhecíveis mesmo sem saber qual
|
|
3396
|
+
* abre, então este portão não fecha - o que fica pendente é só o padrão.
|
|
3397
|
+
*
|
|
3398
|
+
* Censo LEGADO nunca cai aqui: ele não tem o fato, e o comportamento dele é preservado
|
|
3399
|
+
* inteiro. Decisão do dono (28/08, saída A).
|
|
3400
|
+
*/
|
|
3401
|
+
export function mustNotGuessPolarity(census) {
|
|
3402
|
+
const fact = census.declaredSchemes;
|
|
3403
|
+
if (!fact)
|
|
3404
|
+
return false;
|
|
3405
|
+
if (fact.defaultScheme !== "unknown")
|
|
3406
|
+
return false;
|
|
3407
|
+
return (Object.keys(fact.light).length === 0 && Object.keys(fact.dark).length === 0);
|
|
3408
|
+
}
|
|
3354
3409
|
export function defaultScheme(declared) {
|
|
3355
3410
|
let dark = 0;
|
|
3356
3411
|
let light = 0;
|
|
@@ -4088,15 +4143,23 @@ export async function runImport(opts) {
|
|
|
4088
4143
|
// contradict it.
|
|
4089
4144
|
(read?.has?.length === 1
|
|
4090
4145
|
? read.has[0]
|
|
4091
|
-
:
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4146
|
+
: (observedPolarity(census) ??
|
|
4147
|
+
(mustNotGuessPolarity(census)
|
|
4148
|
+
? undefined
|
|
4149
|
+
: reach.light && reach.dark
|
|
4150
|
+
? await askScheme(defaultScheme(census.declared))
|
|
4151
|
+
: reach.dark
|
|
4152
|
+
? "dark"
|
|
4153
|
+
: reach.light
|
|
4154
|
+
? "light"
|
|
4155
|
+
: undefined)));
|
|
4098
4156
|
if (scheme)
|
|
4099
4157
|
census.scheme = scheme;
|
|
4158
|
+
if (!scheme && mustNotGuessPolarity(census)) {
|
|
4159
|
+
console.log("");
|
|
4160
|
+
console.log(body("Your stylesheet does not say which face this project opens in, and nothing here declares a light or a dark scheme on its own. Nothing was guessed: the measurement is complete and the default stays yours to pick."));
|
|
4161
|
+
console.log(body(`Name it with ${paint.strong("--scheme dark")} or ${paint.strong("--scheme light")}, or answer the question this command asks at a terminal.`));
|
|
4162
|
+
}
|
|
4100
4163
|
// The file on disk has to say what we sent, or the next person to read it is
|
|
4101
4164
|
// reading a different import than the one that happened.
|
|
4102
4165
|
await writeFile(out, `${JSON.stringify(census, null, 2)}\n`, "utf8");
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUAL FACE O PROJETO ABRE - lida da estrutura dele, ou não afirmada.
|
|
3
|
+
*
|
|
4
|
+
* O QUE ISTO RESOLVE PARA O CLIENTE: um sistema que abre no escuro parava de ser escuro
|
|
5
|
+
* ao chegar aqui, porque a esteira tratava "a base da cascata" como se fosse "a face
|
|
6
|
+
* clara". Ninguém nunca declarou isso; era uma suposição embutida em dois leitores.
|
|
7
|
+
*
|
|
8
|
+
* ESTE FATO É SÓ OBSERVAÇÃO. `census.scheme` é a decisão RESOLVIDA daquela execução -
|
|
9
|
+
* `--scheme`, a resposta de quem estava no terminal, a leitura do agente. Uma decisão
|
|
10
|
+
* nunca reescreve o que foi observado: se ele digitou `--scheme=dark` num projeto sem
|
|
11
|
+
* sinal nenhum, o fato continua `unknown` e a execução continua `dark`. Misturar os dois
|
|
12
|
+
* apagaria a única informação capaz de dizer "este projeto não nos conta qual face abre".
|
|
13
|
+
*
|
|
14
|
+
* A EVIDÊNCIA É ESTRUTURAL, e por decisão do dono (28/08) nunca:
|
|
15
|
+
* luminância das cores, nome de token, nome de repositório, presença isolada de `.dark`
|
|
16
|
+
* ou `.light`, convenção de um framework específico.
|
|
17
|
+
*
|
|
18
|
+
* Sem sinal, `unknown`. Um palpite aqui vale menos que a pergunta que a esteira já sabe
|
|
19
|
+
* fazer.
|
|
20
|
+
*/
|
|
21
|
+
import { scopedBlocks } from "./scheme-scope.js";
|
|
22
|
+
/** `color-scheme: dark` - e as formas que não respondem nada. */
|
|
23
|
+
const COLOR_SCHEME = /^color-scheme$/i;
|
|
24
|
+
function schemeWord(value) {
|
|
25
|
+
const words = value.toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
26
|
+
// `light dark`, `normal`, `only light` declaram capacidade ou neutralidade, não
|
|
27
|
+
// polaridade. Só uma palavra sozinha é uma afirmação.
|
|
28
|
+
if (words.length !== 1)
|
|
29
|
+
return null;
|
|
30
|
+
return words[0] === "dark" || words[0] === "light" ? words[0] : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* O sinal mais forte, e o único que vive dentro da própria folha: um `color-scheme`
|
|
34
|
+
* numa raiz SEM escopo de esquema.
|
|
35
|
+
*
|
|
36
|
+
* A restrição do escopo é o que faz a regra valer em qualquer projeto. Medido em 28/08:
|
|
37
|
+
* o `frontend-hub` declara `[data-theme='dark'] { color-scheme: light }` dentro de um
|
|
38
|
+
* `.module.scss` de componente - contraditório e local. Ele é excluído duas vezes, por
|
|
39
|
+
* ter escopo de esquema e por não ser de documento, então um componente nunca decide a
|
|
40
|
+
* polaridade do projeto.
|
|
41
|
+
*
|
|
42
|
+
* E `html[data-theme="light"] { color-scheme: light }` é CORROBORAÇÃO do override, não
|
|
43
|
+
* voto para o padrão: o bloco já diz de qual face ele é.
|
|
44
|
+
*/
|
|
45
|
+
function fromColorScheme(sheets) {
|
|
46
|
+
const votes = [];
|
|
47
|
+
for (const sheet of sheets) {
|
|
48
|
+
for (const block of scopedBlocks(sheet.css)) {
|
|
49
|
+
if (block.scope !== "base")
|
|
50
|
+
continue;
|
|
51
|
+
for (const d of block.declarations) {
|
|
52
|
+
if (!COLOR_SCHEME.test(d.name))
|
|
53
|
+
continue;
|
|
54
|
+
const word = schemeWord(d.value);
|
|
55
|
+
if (!word)
|
|
56
|
+
continue;
|
|
57
|
+
votes.push({
|
|
58
|
+
scheme: word,
|
|
59
|
+
file: sheet.file,
|
|
60
|
+
selector: block.stack[block.stack.length - 1] ?? "",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (votes.length === 0)
|
|
66
|
+
return null;
|
|
67
|
+
const distinct = new Set(votes.map((v) => v.scheme));
|
|
68
|
+
if (distinct.size > 1)
|
|
69
|
+
return { scheme: "unknown", source: "conflicting" };
|
|
70
|
+
return {
|
|
71
|
+
scheme: votes[0].scheme,
|
|
72
|
+
source: "root-color-scheme",
|
|
73
|
+
evidence: { file: votes[0].file, selector: votes[0].selector },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A polaridade padrão do projeto, ou `unknown`.
|
|
78
|
+
*
|
|
79
|
+
* A ESCADA, e a primeira que responde vence:
|
|
80
|
+
*
|
|
81
|
+
* 1 `color-scheme` numa raiz sem escopo de esquema - está na folha dele
|
|
82
|
+
* 2 `defaultTheme="dark"` no provider - o próprio provider diz
|
|
83
|
+
* 3 `<html data-theme="dark">` num layout raiz - o portador, verbatim
|
|
84
|
+
* 4 nada disso unknown
|
|
85
|
+
*
|
|
86
|
+
* `color-scheme` vem primeiro porque é a declaração do PADRÃO WEB para exatamente esta
|
|
87
|
+
* pergunta, e porque ela mora na folha que o escopo medido já carrega - os sinais 2 e 3
|
|
88
|
+
* vêm de um app que um `--scope packages/ui` pode não estar lendo. Medido no
|
|
89
|
+
* FlowSanctuary: os três campos de tema vêm `null`, e só o sinal 1 responde.
|
|
90
|
+
*
|
|
91
|
+
* `nota`: `scopedBlocks` é chamado de novo aqui em vez de receber os blocos prontos. É
|
|
92
|
+
* uma varredura de string sobre folhas que já estão em memória, e o custo de passá-la
|
|
93
|
+
* adiante seria acoplar esta pergunta ao laço do import.
|
|
94
|
+
*/
|
|
95
|
+
export function defaultSchemeOf(input) {
|
|
96
|
+
/**
|
|
97
|
+
* A PORTA FECHADA EM VEZ DE VIGIADA: nenhuma saída daqui pode dizer ao mesmo tempo "não
|
|
98
|
+
* sei" e "achei na folha dele". Uma edição futura que produzisse esse par cairia no
|
|
99
|
+
* estado honesto em vez de viajar até a plataforma como um fato que não se sustenta.
|
|
100
|
+
*/
|
|
101
|
+
const fact = pickDefaultScheme(input);
|
|
102
|
+
return schemeFactIsCoherent({
|
|
103
|
+
defaultScheme: fact.scheme,
|
|
104
|
+
defaultSchemeSource: fact.source,
|
|
105
|
+
})
|
|
106
|
+
? fact
|
|
107
|
+
: { scheme: "unknown", source: "absent" };
|
|
108
|
+
}
|
|
109
|
+
function pickDefaultScheme(input) {
|
|
110
|
+
const declared = fromColorScheme(input.sheets);
|
|
111
|
+
if (declared)
|
|
112
|
+
return declared;
|
|
113
|
+
const provider = input.signals?.defaultTheme?.trim().toLowerCase();
|
|
114
|
+
if (provider === "dark" || provider === "light") {
|
|
115
|
+
return { scheme: provider, source: "theme-default" };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* O `<html>` carrega o tema escrito à mão. Casado contra o atributo que o PRÓPRIO
|
|
119
|
+
* projeto usa: um `class="dark"` e um `data-theme="dark"` são o mesmo fato em
|
|
120
|
+
* projetos diferentes, e cravar um dos dois seria escolher por convenção.
|
|
121
|
+
*/
|
|
122
|
+
const attribute = input.signals?.attribute?.trim();
|
|
123
|
+
for (const carried of input.signals?.htmlCarries ?? []) {
|
|
124
|
+
const key = attribute && attribute !== "class" ? attribute : "class";
|
|
125
|
+
const m = new RegExp(`${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=\\s*["'\`{]*([^"'\`}]*)`, "i").exec(carried);
|
|
126
|
+
const word = m?.[1] && schemeWord(m[1].split(/\s+/).pop() ?? "");
|
|
127
|
+
if (word)
|
|
128
|
+
return { scheme: word, source: "html-carrier" };
|
|
129
|
+
}
|
|
130
|
+
return { scheme: "unknown", source: "absent" };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A INVARIANTE DO CONTRATO, escrita uma vez e usada pelos dois lados.
|
|
134
|
+
*
|
|
135
|
+
* `unknown` se e somente se a fonte é `conflicting` ou `absent`. Sem isto, um `unknown`
|
|
136
|
+
* com fonte `root-color-scheme` seria representável - e ele diria ao mesmo tempo "não sei"
|
|
137
|
+
* e "achei na folha dele".
|
|
138
|
+
*/
|
|
139
|
+
export function schemeFactIsCoherent(fact) {
|
|
140
|
+
const undetermined = fact.defaultSchemeSource === "conflicting" ||
|
|
141
|
+
fact.defaultSchemeSource === "absent";
|
|
142
|
+
return (fact.defaultScheme === "unknown") === undetermined;
|
|
143
|
+
}
|
|
@@ -1,77 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TOKENS
|
|
3
|
-
* the dark one.
|
|
2
|
+
* TOKENS POR ESQUEMA - de qual face é cada declaração que o projeto escreveu.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* could read it.
|
|
4
|
+
* A colheita achatava toda folha num único mapa `nome → valor` com a primeira declaração
|
|
5
|
+
* vencendo, o que descarta em silêncio a segunda metade de todo projeto com tema: um
|
|
6
|
+
* codebase shadcn declara o vocabulário inteiro duas vezes - `:root` e `.dark` - e metade
|
|
7
|
+
* era jogada fora antes de qualquer um ler.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* O QUE MUDOU EM 28/08, e é a razão de existirem três baldes em vez de dois: `base` e
|
|
10
|
+
* `alt` misturavam duas perguntas. `:root` responde ONDE na cascata; `.dark` responde DE
|
|
11
|
+
* QUAL ESQUEMA. Tratar a primeira como se fosse a segunda embute "a base é a face clara",
|
|
12
|
+
* que nenhum projeto declara - e um projeto que abre no escuro recebia a face clara
|
|
13
|
+
* pintada com os valores do escuro. Medido no FlowSanctuary: 17 declarações claras, 0
|
|
14
|
+
* chegando, `canvas` e `foreground` no mesmo hex.
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* scope is this declaration in" - and it survives the nesting Tailwind v4
|
|
20
|
-
* produces (`@layer base { :root { … } }`) without a dependency.
|
|
21
|
-
*/
|
|
22
|
-
/** A scope that means "this is the dark theme". Covers the spellings that
|
|
23
|
-
* actually appear: shadcn's `.dark`, the data-attribute forms, and the
|
|
24
|
-
* `:where(...)` wrapper Tailwind v4 emits for a custom variant.
|
|
16
|
+
* Agora `base` é posição, `light` e `dark` são esquema, e qual face abre por padrão é
|
|
17
|
+
* outro fato inteiro - ver `default-scheme.ts`.
|
|
25
18
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
* Nota de escopo honesta (medida em 30/07 no projeto para o qual isto foi escrito): o
|
|
20
|
+
* bloco escuro dele tem exatamente dois tokens, porque o modo escuro dele vive numa
|
|
21
|
+
* variante `dark:` do Tailwind aplicada nos componentes, não em valor de token. Este
|
|
22
|
+
* leitor acha o que um projeto DECLARA; não acha o que ele expressa em nome de classe.
|
|
23
|
+
*/
|
|
24
|
+
import { scopedBlocks } from "./scheme-scope.js";
|
|
25
|
+
/**
|
|
26
|
+
* A máquina do Tailwind v4, que não é vocabulário de ninguém - ela é emitida em `@theme`
|
|
27
|
+
* junto com o que a pessoa escreveu.
|
|
28
|
+
*/
|
|
29
|
+
const MACHINERY = /^--tw-/;
|
|
32
30
|
export function parseSchemeBlocks(css) {
|
|
33
31
|
const base = new Map();
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
for (let i = 0; i < clean.length; i++) {
|
|
40
|
-
const ch = clean[i];
|
|
41
|
-
if (ch === "{") {
|
|
42
|
-
stack.push(prelude.trim());
|
|
43
|
-
prelude = "";
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (ch === "}") {
|
|
47
|
-
stack.pop();
|
|
48
|
-
prelude = "";
|
|
32
|
+
const light = new Map();
|
|
33
|
+
const dark = new Map();
|
|
34
|
+
const into = { base, light, dark };
|
|
35
|
+
for (const block of scopedBlocks(css)) {
|
|
36
|
+
if (block.scope === "other")
|
|
49
37
|
continue;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
continue;
|
|
57
|
-
const dark = stack.some((s) => DARK_SCOPE.test(s));
|
|
58
|
-
const rooted = stack.some((s) => ROOT_SCOPE.test(s) || /^@theme\b/i.test(s));
|
|
59
|
-
if (!dark && !rooted)
|
|
38
|
+
const bucket = into[block.scope];
|
|
39
|
+
for (const d of block.declarations) {
|
|
40
|
+
// Só vocabulário: `scopedBlocks` também colhe propriedade padrão, porque a
|
|
41
|
+
// detecção de polaridade precisa do `color-scheme` com a MESMA classificação
|
|
42
|
+
// de escopo. Aqui só interessa o que o projeto NOMEIA.
|
|
43
|
+
if (!d.name.startsWith("--") || MACHINERY.test(d.name))
|
|
60
44
|
continue;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const into = dark ? alt : base;
|
|
68
|
-
// First declaration wins, the same rule the flat harvest used - a later
|
|
69
|
-
// sheet overriding an earlier one is a cascade question this cannot see.
|
|
70
|
-
if (!into.has(name))
|
|
71
|
-
into.set(name, value);
|
|
72
|
-
continue;
|
|
45
|
+
// A primeira declaração vence, a mesma regra que a colheita achatada usava - uma
|
|
46
|
+
// folha posterior sobrescrevendo uma anterior é uma questão de cascata que este
|
|
47
|
+
// scanner não vê. Vale DENTRO de um balde; entre baldes quem resolve é a
|
|
48
|
+
// plataforma, e ali o override vence a base.
|
|
49
|
+
if (!bucket.has(d.name))
|
|
50
|
+
bucket.set(d.name, d.value);
|
|
73
51
|
}
|
|
74
|
-
prelude += ch;
|
|
75
52
|
}
|
|
76
|
-
return { base,
|
|
53
|
+
return { base, light, dark };
|
|
77
54
|
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONDE UMA DECLARAÇÃO VIVE - e o cliente para de receber a face errada do próprio sistema.
|
|
3
|
+
*
|
|
4
|
+
* O QUE ELE VIVIA: um projeto que abre no escuro e declara o claro à mão em
|
|
5
|
+
* `[data-theme="light"]` recebia a face clara pintada com os valores do escuro - fundo e
|
|
6
|
+
* texto no mesmo hex, contraste 1,00:1. Medido no FlowSanctuary em 28/08: 17 declarações
|
|
7
|
+
* claras, 0 chegando.
|
|
8
|
+
*
|
|
9
|
+
* A CAUSA ERAM DUAS COISAS MISTURADAS num só juízo. `:root` responde "onde na cascata",
|
|
10
|
+
* `.dark` responde "de qual esquema" - e os dois leitores de token tratavam a primeira
|
|
11
|
+
* pergunta como se respondesse a segunda. Daí "base significa claro", que nenhum projeto
|
|
12
|
+
* jamais declarou.
|
|
13
|
+
*
|
|
14
|
+
* Aqui elas se separam. Este módulo responde SÓ a pergunta do escopo, e responde a mesma
|
|
15
|
+
* coisa para os dois leitores - `parseRootTokens` e `parseSchemeBlocks` tinham regex
|
|
16
|
+
* próprias e DISCORDAVAM: `html.dark` era escuro para um e raiz para o outro.
|
|
17
|
+
*
|
|
18
|
+
* A polaridade padrão do projeto - qual face abre - é outro fato, e mora em
|
|
19
|
+
* `defaultSchemeOf`. Um seletor não sabe qual face é a padrão; só sabe de qual face ele é.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* O MARCADOR DE ESQUEMA, nas grafias que aparecem de verdade.
|
|
23
|
+
*
|
|
24
|
+
* O `]` de fechamento carrega peso e um spec já pegou ele faltando: sem ele,
|
|
25
|
+
* `[data-theme="darkroom"]` era lido como o tema escuro.
|
|
26
|
+
*
|
|
27
|
+
* Sem fronteira à esquerda de propósito - `html.dark` é escuro, e exigir espaço antes do
|
|
28
|
+
* ponto fazia essa forma (a que o Tailwind com `darkMode: "class"` produz) cair no
|
|
29
|
+
* repouso. A fronteira que importa é a da DIREITA, e ela está no `\b` / no `]`.
|
|
30
|
+
*/
|
|
31
|
+
const SCHEME_MARK = /(\.dark\b|\.light\b|\.theme-dark\b|\.theme-light\b|\[data-(?:theme|scheme|mode)\s*[~^|*$]?=\s*["']?(?:dark|light)["']?\s*\])/gi;
|
|
32
|
+
const DARK_MARK = /(\.dark\b|\.theme-dark\b|\[data-(?:theme|scheme|mode)\s*[~^|*$]?=\s*["']?dark["']?\s*\])/i;
|
|
33
|
+
const LIGHT_MARK = /(\.light\b|\.theme-light\b|\[data-(?:theme|scheme|mode)\s*[~^|*$]?=\s*["']?light["']?\s*\])/i;
|
|
34
|
+
/** As raízes do documento - os lugares onde alguém escreve o vocabulário do projeto. */
|
|
35
|
+
const ROOT_MARK = /(^|[\s>+~,])(:root|html|:host|body)\b/gi;
|
|
36
|
+
/** `@theme` do Tailwind v4 é raiz por definição: é onde a paleta inteira é declarada. */
|
|
37
|
+
const AT_THEME = /^@theme\b/i;
|
|
38
|
+
/**
|
|
39
|
+
* A OUTRA GRAFIA DE "ESTA DECLARAÇÃO É DAQUELE ESQUEMA" - a que o CSS oferece nativamente.
|
|
40
|
+
*
|
|
41
|
+
* `@media (prefers-color-scheme: dark) { :root { … } }` diz exatamente o mesmo que
|
|
42
|
+
* `.dark { … }`, e sem esta linha ela cairia em `base` - o mesmo defeito de sempre com
|
|
43
|
+
* outra sintaxe. Zero ocorrências nas quatro populações medidas em 28/08; entra porque
|
|
44
|
+
* ignorá-la APAGARIA declarações que hoje chegam, não para cobrir um caso hipotético.
|
|
45
|
+
*/
|
|
46
|
+
const PREFERS = /prefers-color-scheme\s*:\s*(dark|light)/i;
|
|
47
|
+
/**
|
|
48
|
+
* AT-RULES QUE SÓ EMBRULHAM - elas não são seletor de ninguém.
|
|
49
|
+
*
|
|
50
|
+
* `@layer base { :root { … } }` é a forma que o Tailwind v4 emite, e tratá-la como um
|
|
51
|
+
* nível da pilha fazia o `:root` de dentro virar `other`. `@media (min-width: …)` idem.
|
|
52
|
+
*/
|
|
53
|
+
const TRANSPARENT_AT = /^@(layer|supports|container|scope|media)\b/i;
|
|
54
|
+
/**
|
|
55
|
+
* O seletor fala do DOCUMENTO INTEIRO, ou de um pedaço dele?
|
|
56
|
+
*
|
|
57
|
+
* A régua: tire os marcadores de esquema e as raízes. Se sobrou seletor, sobrou
|
|
58
|
+
* componente - e uma declaração de componente não é foundation.
|
|
59
|
+
*
|
|
60
|
+
* `html.dark .holo-card` vira `html .holo-card` vira ` .holo-card` - sobrou, então é
|
|
61
|
+
* local. `:root[data-theme="light"]` vira `:root` vira vazio - documento.
|
|
62
|
+
*
|
|
63
|
+
* Medido nas quatro populações em 28/08: a régua tira 2 declarações de 1128, e as 2 são
|
|
64
|
+
* o defeito (`--holo-inner-bg`, uma variável da `@utility holo-card` do codelevel).
|
|
65
|
+
*/
|
|
66
|
+
function isDocumentScoped(selector) {
|
|
67
|
+
if (AT_THEME.test(selector.trim()))
|
|
68
|
+
return true;
|
|
69
|
+
// `:where(…)` e `:is(…)` são pura AGRUPAÇÃO - o que importa é o que está dentro.
|
|
70
|
+
// Sem desembrulhar, `:where([data-theme=dark], [data-theme=dark] *)` do repositório
|
|
71
|
+
// real virava local, e com ele os dois tokens que o comentário de `tokens.ts` cita
|
|
72
|
+
// por nome (`--loader-color`, `--color-track`).
|
|
73
|
+
const flat = selector.replace(/:(?:where|is)\s*\(/gi, "").replace(/\)/g, "");
|
|
74
|
+
return flat.split(",").every((one) => {
|
|
75
|
+
SCHEME_MARK.lastIndex = 0;
|
|
76
|
+
const scoped = SCHEME_MARK.test(one);
|
|
77
|
+
ROOT_MARK.lastIndex = 0;
|
|
78
|
+
const rooted = ROOT_MARK.test(one);
|
|
79
|
+
let bare = one.replace(SCHEME_MARK, "").replace(ROOT_MARK, "$1");
|
|
80
|
+
// `[data-theme=dark] *` é a mesma declaração alcançando tudo que herda dela - a
|
|
81
|
+
// forma que o Tailwind v4 emite. O `*` só some quando o pedaço JÁ é de esquema ou
|
|
82
|
+
// de raiz; `*, ::before, ::after` sozinho continua sendo o `--tw-*` do v3, que
|
|
83
|
+
// nunca foi vocabulário de ninguém.
|
|
84
|
+
if (scoped || rooted)
|
|
85
|
+
bare = bare.replace(/\*/g, "");
|
|
86
|
+
return bare.replace(/[\s>+~]/g, "").trim() === "";
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* De qual face é esta pilha de seletores - do mais externo ao mais interno.
|
|
91
|
+
*
|
|
92
|
+
* PILHA e não seletor solto porque o aninhamento decide: `@layer base { html { … } }` e
|
|
93
|
+
* `[data-theme="light"] { .x { … } }` só se respondem olhando os dois níveis.
|
|
94
|
+
*
|
|
95
|
+
* A ORDEM DA DECISÃO É LEI: esquema explícito vence posição. Um bloco que diz `light`
|
|
96
|
+
* é claro mesmo escrito em cima de `:root`, e é justamente essa inversão que fazia 14
|
|
97
|
+
* declarações claras da nossa própria folha entrarem como base.
|
|
98
|
+
*
|
|
99
|
+
* Claro E escuro na mesma pilha é contradição aninhada, e contradição não se adivinha:
|
|
100
|
+
* sai `other`.
|
|
101
|
+
*/
|
|
102
|
+
export function schemeScopeOf(stack) {
|
|
103
|
+
const all = stack.map((s) => s.trim()).filter((s) => s !== "");
|
|
104
|
+
if (all.length === 0)
|
|
105
|
+
return "other";
|
|
106
|
+
/** O que o `@media (prefers-color-scheme: …)` da pilha diz, quando ela tem um. */
|
|
107
|
+
const prefers = all
|
|
108
|
+
.filter((s) => TRANSPARENT_AT.test(s))
|
|
109
|
+
.map((s) => PREFERS.exec(s)?.[1]?.toLowerCase())
|
|
110
|
+
.find((s) => s != null);
|
|
111
|
+
/** Os embrulhos saem: eles não selecionam nada, então não classificam nada. */
|
|
112
|
+
const real = all.filter((s) => !TRANSPARENT_AT.test(s));
|
|
113
|
+
if (real.length === 0)
|
|
114
|
+
return "other";
|
|
115
|
+
const dark = prefers === "dark" || real.some((s) => DARK_MARK.test(s));
|
|
116
|
+
const light = prefers === "light" || real.some((s) => LIGHT_MARK.test(s));
|
|
117
|
+
if (dark && light)
|
|
118
|
+
return "other";
|
|
119
|
+
// Toda parte da pilha precisa ser de documento. Um `html { }` que contém `.card { }`
|
|
120
|
+
// fala do card, e o nível interno é quem denuncia isso.
|
|
121
|
+
if (!real.every((s) => isDocumentScoped(s)))
|
|
122
|
+
return "other";
|
|
123
|
+
if (dark)
|
|
124
|
+
return "dark";
|
|
125
|
+
if (light)
|
|
126
|
+
return "light";
|
|
127
|
+
// Sem marcador de esquema, só é base quem é raiz de verdade. `isDocumentScoped` já
|
|
128
|
+
// aprovou a pilha inteira, então basta ter uma raiz nomeada em algum nível - o
|
|
129
|
+
// `@layer base { :root { … } }` que o Tailwind v4 emite passa por aqui.
|
|
130
|
+
const rooted = real.some((s) => {
|
|
131
|
+
if (AT_THEME.test(s.trim()))
|
|
132
|
+
return true;
|
|
133
|
+
ROOT_MARK.lastIndex = 0;
|
|
134
|
+
return ROOT_MARK.test(s);
|
|
135
|
+
});
|
|
136
|
+
return rooted ? "base" : "other";
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A VARREDURA ÚNICA - os dois leitores de token passam por aqui.
|
|
140
|
+
*
|
|
141
|
+
* Deliberadamente um scanner, não um parser de CSS. O que ele precisa responder é uma
|
|
142
|
+
* pergunta só - "de qual escopo é esta declaração" -, e para isso bastam a profundidade
|
|
143
|
+
* de chave e o seletor que abriu cada nível. É o que o faz sobreviver ao aninhamento que
|
|
144
|
+
* o Tailwind v4 produz sem uma dependência (o CLI é publicado standalone e tem zero deps).
|
|
145
|
+
*
|
|
146
|
+
* Existe porque `parseRootTokens` e `parseSchemeBlocks` tinham cada um a sua varredura e a
|
|
147
|
+
* sua regex de escopo, e DISCORDAVAM: `html.dark` era raiz para um e nada para o outro.
|
|
148
|
+
* Duas respostas para a mesma pergunta é como a face errada chega na tela do cliente.
|
|
149
|
+
*
|
|
150
|
+
* MAS UM SCANNER DE CHAVE INGÊNUO LÊ CONTEÚDO COMO SE FOSSE ESTRUTURA, e isso não é
|
|
151
|
+
* hipótese: medido em 29/08 contra a versão de 28/08, 7 de 12 formas de CSS VÁLIDO
|
|
152
|
+
* quebravam. `content: "}"` fechava o bloco e a declaração seguinte sumia; `--x: "a;b"`
|
|
153
|
+
* chegava truncado em `"a`; um `url("data:image/svg+xml;base64,…")` perdia o payload
|
|
154
|
+
* inteiro; e a remoção de comentário feita por regex sobre o arquivo inteiro comia o
|
|
155
|
+
* miolo de qualquer string que contivesse `/*`.
|
|
156
|
+
*
|
|
157
|
+
* A PROPRIEDADE, e ela vale para qualquer folha: delimitador que pertence ao CONTEÚDO
|
|
158
|
+
* léxico de um valor não pode mexer na pilha estrutural. Por isso o laço abaixo tem
|
|
159
|
+
* estado - código, aspas simples, aspas duplas, escape, comentário e profundidade de
|
|
160
|
+
* parênteses -, e `{`, `}`, `;` e `:` só são estruturais no primeiro deles.
|
|
161
|
+
*/
|
|
162
|
+
export function scopedBlocks(css) {
|
|
163
|
+
const out = [];
|
|
164
|
+
const stack = [];
|
|
165
|
+
/** Índice em `out` do bloco de cada nível aberto, para as declarações irem no lugar. */
|
|
166
|
+
const at = [];
|
|
167
|
+
/** O texto desde o último delimitador - vira seletor num `{` e declaração num `;`/`}`. */
|
|
168
|
+
let prelude = "";
|
|
169
|
+
/** Onde, dentro de `prelude`, está o `:` que separa nome de valor. -1 = nenhum ainda. */
|
|
170
|
+
let colon = -1;
|
|
171
|
+
/** `null` fora de string; a aspa que abriu, dentro dela. */
|
|
172
|
+
let quote = null;
|
|
173
|
+
let inComment = false;
|
|
174
|
+
/**
|
|
175
|
+
* `url(data:image/svg+xml;base64,…)` sem aspas é CSS válido, e ali o `;` é payload.
|
|
176
|
+
* Contar parênteses cobre isso e todo `calc()`/`clamp()` de uma vez, sem citar função.
|
|
177
|
+
*/
|
|
178
|
+
let parens = 0;
|
|
179
|
+
const flush = (closing) => {
|
|
180
|
+
const text = prelude;
|
|
181
|
+
const nameEnd = colon;
|
|
182
|
+
prelude = "";
|
|
183
|
+
colon = -1;
|
|
184
|
+
const here = at[at.length - 1];
|
|
185
|
+
if (here != null && nameEnd >= 0) {
|
|
186
|
+
const name = text.slice(0, nameEnd).trim().toLowerCase();
|
|
187
|
+
const value = text.slice(nameEnd + 1).trim();
|
|
188
|
+
if (name && value && NAME.test(name)) {
|
|
189
|
+
out[here].declarations.push({ name, value });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (closing) {
|
|
193
|
+
stack.pop();
|
|
194
|
+
at.pop();
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
for (let i = 0; i < css.length; i++) {
|
|
198
|
+
const ch = css[i];
|
|
199
|
+
if (inComment) {
|
|
200
|
+
if (ch === "*" && css[i + 1] === "/") {
|
|
201
|
+
inComment = false;
|
|
202
|
+
i += 1;
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (quote) {
|
|
207
|
+
// Uma barra invertida consome o próximo caractere, seja ele qual for - inclusive
|
|
208
|
+
// outra barra e a própria aspa de fechamento.
|
|
209
|
+
if (ch === "\\") {
|
|
210
|
+
prelude += ch + (css[i + 1] ?? "");
|
|
211
|
+
i += 1;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (ch === quote)
|
|
215
|
+
quote = null;
|
|
216
|
+
prelude += ch;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
// `/*` só ABRE comentário fora de string - senão `"/* isto não é comentário */"`
|
|
220
|
+
// perderia o próprio conteúdo.
|
|
221
|
+
if (ch === "/" && css[i + 1] === "*") {
|
|
222
|
+
inComment = true;
|
|
223
|
+
i += 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (ch === '"' || ch === "'") {
|
|
227
|
+
quote = ch;
|
|
228
|
+
prelude += ch;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (ch === "(")
|
|
232
|
+
parens += 1;
|
|
233
|
+
else if (ch === ")")
|
|
234
|
+
parens = Math.max(0, parens - 1);
|
|
235
|
+
if (parens === 0) {
|
|
236
|
+
if (ch === "{") {
|
|
237
|
+
stack.push(prelude.trim());
|
|
238
|
+
prelude = "";
|
|
239
|
+
colon = -1;
|
|
240
|
+
at.push(out.length);
|
|
241
|
+
out.push({
|
|
242
|
+
scope: schemeScopeOf(stack),
|
|
243
|
+
stack: [...stack],
|
|
244
|
+
declarations: [],
|
|
245
|
+
});
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (ch === "}" || ch === ";") {
|
|
249
|
+
// A ÚLTIMA DECLARAÇÃO NÃO PRECISA DE `;`, e `:root { --x: red }` é CSS válido -
|
|
250
|
+
// por isso o `}` também fecha uma declaração antes de fechar o bloco.
|
|
251
|
+
flush(ch === "}");
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
// O PRIMEIRO `:` do prelúdio separa nome de valor. Só o primeiro: um
|
|
255
|
+
// `background: url(a:b)` já está protegido pelos parênteses, e `a:hover { }`
|
|
256
|
+
// nunca chega ao `flush` porque termina em `{`.
|
|
257
|
+
if (ch === ":" && colon < 0)
|
|
258
|
+
colon = prelude.length;
|
|
259
|
+
}
|
|
260
|
+
prelude += ch;
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Um nome de propriedade, custom ou padrão. `--my_var` é legal, então `_` entra.
|
|
266
|
+
*
|
|
267
|
+
* Propriedade padrão também passa: `color-scheme: dark` numa raiz é a evidência
|
|
268
|
+
* estrutural de qual face o projeto abre, e ela precisa da MESMA classificação de escopo
|
|
269
|
+
* que os tokens - ver `default-scheme.ts`. Quem só quer vocabulário filtra por `--` no
|
|
270
|
+
* consumo.
|
|
271
|
+
*/
|
|
272
|
+
const NAME = /^(--[a-z0-9_-]+|-{0,2}[a-z][a-z0-9-]*)$/i;
|
package/dist/doctor/tokens.js
CHANGED
|
@@ -11,17 +11,8 @@
|
|
|
11
11
|
* Pure and dependency-free on purpose: every function here takes text and
|
|
12
12
|
* returns data, so the whole diagnosis is testable without a filesystem.
|
|
13
13
|
*/
|
|
14
|
-
/**
|
|
15
|
-
* Where the vocabulary being measured against came from.
|
|
16
|
-
*
|
|
17
|
-
* `"installed"` is a system we wrote. `"yours"` is the project's OWN custom
|
|
18
|
-
* properties, harvested from its stylesheets - the case that matters most,
|
|
19
|
-
* because the people who feel this problem hardest already have a design
|
|
20
|
-
* system and had no reason to adopt ours before seeing a number.
|
|
21
|
-
*/
|
|
22
|
-
/** `"adopted"` is theirs too, but described by `adopt` and therefore NAMED -
|
|
23
|
-
* it must not be offered a system to install, having just adopted one. */
|
|
24
14
|
import { DEFAULT_ROOT_PX } from "./root-size.js";
|
|
15
|
+
import { parseSchemeBlocks } from "./scheme-blocks.js";
|
|
25
16
|
export const EMPTY_TABLE = {
|
|
26
17
|
source: null,
|
|
27
18
|
name: null,
|
|
@@ -307,94 +298,19 @@ export function parseDeclaredNames(css) {
|
|
|
307
298
|
* (declared on `*, ::before, ::after`) and Radix's `--radix-*` (set on the
|
|
308
299
|
* component) without naming a single vendor.
|
|
309
300
|
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
301
|
+
* A CLASSIFICAÇÃO É COMPARTILHADA desde 28/08 - ver `scheme-scope.ts`. Este leitor tinha
|
|
302
|
+
* regex própria de raiz e discordava do leitor de esquemas: `html.dark .holo-card` era
|
|
303
|
+
* raiz aqui e nada lá, então uma variável LOCAL de um componente do codelevel
|
|
304
|
+
* (`--holo-inner-bg`, declarada dentro de `@utility holo-card`) subia para cá como token
|
|
305
|
+
* global do sistema - e subia com o valor do tema ESCURO na face que a plataforma lê como
|
|
306
|
+
* padrão. Duas respostas para a mesma pergunta é como a face errada chega na tela.
|
|
307
|
+
*
|
|
308
|
+
* Aqui ficam só as declarações de escopo `base`: raiz de documento, sem escopo de esquema.
|
|
309
|
+
* O que um projeto declara explicitamente como claro ou escuro viaja em
|
|
310
|
+
* `Census.declaredSchemes`, com a face nomeada em vez de achatada.
|
|
314
311
|
*/
|
|
315
312
|
export function parseRootTokens(css) {
|
|
316
|
-
|
|
317
|
-
const isRoot = (sel) => /^@theme\b/i.test(sel) || /(^|[\s,>+~])(:root|html|:host)\b/i.test(sel);
|
|
318
|
-
// Every `<selector> {` in the file. Nested ones show up too and are filtered
|
|
319
|
-
// by isRoot, so an @keyframes inside @theme is skipped rather than mined.
|
|
320
|
-
const opens = /([^{}]*)\{/g;
|
|
321
|
-
let m = opens.exec(css);
|
|
322
|
-
while (m !== null) {
|
|
323
|
-
// The capture runs back to the previous brace, so it carries imports and
|
|
324
|
-
// comments with it. The SELECTOR is only what follows the last `;` or
|
|
325
|
-
// comment - and `@theme` is anchored to the start, so without this it
|
|
326
|
-
// matched only when the block happened to be the first thing in the file.
|
|
327
|
-
// Every fixture had it first. This repo's own globals.css does not, and
|
|
328
|
-
// reported 0 of its 48 tokens (27/07).
|
|
329
|
-
const sel = (m[1]
|
|
330
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
331
|
-
.split(";")
|
|
332
|
-
.pop() ?? "").trim();
|
|
333
|
-
if (!isRoot(sel)) {
|
|
334
|
-
m = opens.exec(css);
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
// Walk to the MATCHING close, counting depth. The flat regex this replaces
|
|
338
|
-
// required a body with no braces at all, so it silently skipped any
|
|
339
|
-
// `@theme` containing `@keyframes` - which is the documented Tailwind v4
|
|
340
|
-
// layout. Measured against this repo's own globals.css: 48 tokens present,
|
|
341
|
-
// 2 found, and the 2 came from unrelated test fixtures (27/07).
|
|
342
|
-
const start = m.index + m[0].length;
|
|
343
|
-
let depth = 1;
|
|
344
|
-
let i = start;
|
|
345
|
-
while (i < css.length && depth > 0) {
|
|
346
|
-
const c = css[i];
|
|
347
|
-
if (c === "{")
|
|
348
|
-
depth++;
|
|
349
|
-
else if (c === "}")
|
|
350
|
-
depth--;
|
|
351
|
-
i++;
|
|
352
|
-
}
|
|
353
|
-
// Only declarations that are DIRECT children count. A custom property
|
|
354
|
-
// inside a keyframe step is animation state, not a design token.
|
|
355
|
-
let flat = "";
|
|
356
|
-
let d = 0;
|
|
357
|
-
for (let j = start; j < i - 1; j++) {
|
|
358
|
-
const c = css[j];
|
|
359
|
-
if (c === "{")
|
|
360
|
-
d++;
|
|
361
|
-
else if (c === "}")
|
|
362
|
-
d--;
|
|
363
|
-
else if (d === 0)
|
|
364
|
-
flat += c;
|
|
365
|
-
}
|
|
366
|
-
for (const t of flat.matchAll(/(--[a-z0-9_-]+)\s*:\s*([^;}]+)/gi)) {
|
|
367
|
-
const name = t[1].toLowerCase();
|
|
368
|
-
// Belt and braces: v4 emits some `--tw-*` bookkeeping into @theme, and
|
|
369
|
-
// it is machinery, not somebody's design vocabulary.
|
|
370
|
-
if (name.startsWith("--tw-"))
|
|
371
|
-
continue;
|
|
372
|
-
const value = t[2].trim();
|
|
373
|
-
if (!value)
|
|
374
|
-
continue;
|
|
375
|
-
/**
|
|
376
|
-
* O ALIAS FICA, E É RESOLVIDO DEPOIS - antes ele era descartado aqui, e era justamente o
|
|
377
|
-
* token mais cheio de significado que sumia.
|
|
378
|
-
*
|
|
379
|
-
* `--loader-color: var(--color-ocean-500)` não é ruído: é um PAPEL, a frase que diz "esta
|
|
380
|
-
* função é esta cor". Descartá-lo deixava só as primitivas - a paleta sem o vocabulário.
|
|
381
|
-
*
|
|
382
|
-
* A assimetria que isso produzia, medida no repo do dono em 11/08: `declaredAlt` sai de
|
|
383
|
-
* `parseSchemeBlocks`, que não tem este filtro, então o valor ESCURO de `--loader-color` e
|
|
384
|
-
* `--color-track` viajava e o CLARO não. Dois tokens presentes só no dark, e o
|
|
385
|
-
* `CircularProgress.tsx:100` lendo `var(--color-track, …)` sem que ninguém soubesse o que
|
|
386
|
-
* `--color-track` é.
|
|
387
|
-
*
|
|
388
|
-
* Resolver é o mesmo que o caminho `installed` já faz com `resolveAliases`: a máquina existia
|
|
389
|
-
* e só não era usada aqui.
|
|
390
|
-
*/
|
|
391
|
-
if (!out.has(name))
|
|
392
|
-
out.set(name, value);
|
|
393
|
-
}
|
|
394
|
-
opens.lastIndex = i;
|
|
395
|
-
m = opens.exec(css);
|
|
396
|
-
}
|
|
397
|
-
return out;
|
|
313
|
+
return parseSchemeBlocks(css).base;
|
|
398
314
|
}
|
|
399
315
|
export function buildTable(input) {
|
|
400
316
|
const source = input.source ?? "installed";
|
package/dist/install-marks.js
CHANGED
|
@@ -367,7 +367,7 @@ export const CHECKER_SINCE = "0.16.308";
|
|
|
367
367
|
* nunca era alcançado. Medido: 1134 componentes, 77 mudaram, e a receita que o cliente recebe muda
|
|
368
368
|
* com eles. Uma medição anterior não produz o conserto, então a marca sobe.
|
|
369
369
|
*/
|
|
370
|
-
export const READER_SINCE = "0.16.
|
|
370
|
+
export const READER_SINCE = "0.16.330";
|
|
371
371
|
/**
|
|
372
372
|
* O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
|
|
373
373
|
*
|
package/dist/merge-census.js
CHANGED
|
@@ -89,6 +89,23 @@ export function mergeCensus(list) {
|
|
|
89
89
|
];
|
|
90
90
|
const declared = { ...asRecord(first.declared) };
|
|
91
91
|
const declaredAlt = { ...asRecord(first.declaredAlt) };
|
|
92
|
+
/**
|
|
93
|
+
* OS TRÊS BALDES DE ESQUEMA, escopo a escopo - ver `Census.declaredSchemes`.
|
|
94
|
+
*
|
|
95
|
+
* Mesmo desempate de `declared`: o primeiro escopo vence e a perda entra em `conflicts`,
|
|
96
|
+
* porque dois pacotes que declaram o mesmo nome é um achado, não um detalhe.
|
|
97
|
+
*
|
|
98
|
+
* A POLARIDADE NÃO SE SOMA. Dois escopos que observam faces padrão diferentes não têm
|
|
99
|
+
* média: viram `conflicting`, que é a verdade. Um escopo que não observou nada não vota -
|
|
100
|
+
* senão o silêncio de um pacote apagaria a evidência do outro.
|
|
101
|
+
*/
|
|
102
|
+
const firstSchemes = first.declaredSchemes;
|
|
103
|
+
const schemeBuckets = {
|
|
104
|
+
base: { ...asRecord(firstSchemes?.base) },
|
|
105
|
+
light: { ...asRecord(firstSchemes?.light) },
|
|
106
|
+
dark: { ...asRecord(firstSchemes?.dark) },
|
|
107
|
+
};
|
|
108
|
+
const polarities = firstSchemes ? [firstSchemes] : [];
|
|
92
109
|
const keyframes = { ...(first.keyframes ?? {}) };
|
|
93
110
|
const looks = { ...(first.looks ?? {}) };
|
|
94
111
|
let observed = first.observed.map((v) => ({ ...v }));
|
|
@@ -111,6 +128,13 @@ export function mergeCensus(list) {
|
|
|
111
128
|
const firstName = nameOf(first, 0);
|
|
112
129
|
mergeDeclared(declared, asRecord(c.declared), firstName, scopeName, conflicts);
|
|
113
130
|
mergeDeclared(declaredAlt, asRecord(c.declaredAlt), firstName, scopeName, conflicts);
|
|
131
|
+
const theirSchemes = c.declaredSchemes;
|
|
132
|
+
if (theirSchemes) {
|
|
133
|
+
polarities.push(theirSchemes);
|
|
134
|
+
for (const face of ["base", "light", "dark"]) {
|
|
135
|
+
mergeDeclared(schemeBuckets[face], asRecord(theirSchemes[face]), firstName, scopeName, conflicts);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
114
138
|
/**
|
|
115
139
|
* O COMPONENTE QUE OS DOIS DEFINEM fica com a receita do primeiro escopo, e
|
|
116
140
|
* o descarte entra em `collisions` - a mesma lista que `name-claim.ts`
|
|
@@ -197,6 +221,23 @@ export function mergeCensus(list) {
|
|
|
197
221
|
};
|
|
198
222
|
if (Object.keys(declaredAlt).length > 0)
|
|
199
223
|
out.declaredAlt = declaredAlt;
|
|
224
|
+
if (polarities.length > 0) {
|
|
225
|
+
const observed = polarities.filter((p) => p.defaultScheme !== "unknown");
|
|
226
|
+
const distinct = new Set(observed.map((p) => p.defaultScheme));
|
|
227
|
+
const winner = distinct.size === 1 ? observed[0] : null;
|
|
228
|
+
out.declaredSchemes = {
|
|
229
|
+
...schemeBuckets,
|
|
230
|
+
defaultScheme: winner ? winner.defaultScheme : "unknown",
|
|
231
|
+
defaultSchemeSource: winner
|
|
232
|
+
? winner.defaultSchemeSource
|
|
233
|
+
: distinct.size > 1
|
|
234
|
+
? "conflicting"
|
|
235
|
+
: "absent",
|
|
236
|
+
...(winner?.defaultSchemeEvidence
|
|
237
|
+
? { defaultSchemeEvidence: winner.defaultSchemeEvidence }
|
|
238
|
+
: {}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
200
241
|
if (Object.keys(keyframes).length > 0)
|
|
201
242
|
out.keyframes = keyframes;
|
|
202
243
|
if (Object.keys(looks).length > 0)
|
package/package.json
CHANGED