synthesisui 0.16.217 → 0.16.220
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/component.js +2 -2
- package/dist/commands/doctor.js +78 -8
- package/dist/commands/generate.js +2 -2
- package/dist/commands/refit.js +2 -2
- package/dist/commands/upgrade.js +2 -2
- package/dist/component-codegen.js +305 -20
- package/dist/doctor/frozen.js +15 -5
- package/dist/doctor/ledger.js +30 -4
- package/dist/doctor/scan.js +81 -3
- package/dist/doctor/transcribe.js +18 -7
- package/dist/install-marks.js +18 -2
- package/dist/project-facts.js +28 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { generateComponentFiles } from "../component-codegen.js";
|
|
|
6
6
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
7
7
|
import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-templates.js";
|
|
8
8
|
import { body, section, snippet } from "../output.js";
|
|
9
|
-
import { findCollision, reactMajorOf, readInstalledConvention, } from "../project-facts.js";
|
|
9
|
+
import { findCollision, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
10
10
|
import { fetchComponent, RegistryError } from "../registry.js";
|
|
11
11
|
/**
|
|
12
12
|
* Writes the shared `cn.ts` next to the components, built from THIS project's
|
|
@@ -157,7 +157,7 @@ export async function component(slug, name, opts) {
|
|
|
157
157
|
* it, so the TSX and the stylesheet in the same folder agree by
|
|
158
158
|
* construction. Falling back to disk keeps an older registry working.
|
|
159
159
|
*/
|
|
160
|
-
res.classNames ?? (await readInstalledConvention(root, slug)));
|
|
160
|
+
res.classNames ?? (await readInstalledConvention(root, slug)), res.name, await readInstalledScheme(root, slug));
|
|
161
161
|
for (const file of files) {
|
|
162
162
|
await writeFile(join(compDir, file.filename), file.code, "utf8");
|
|
163
163
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -8,7 +8,7 @@ import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, }
|
|
|
8
8
|
import { checkContracts } from "../doctor/contract-check.js";
|
|
9
9
|
import { describeMissing, missingDependencies, summarizeMissing, } from "../doctor/dependencies.js";
|
|
10
10
|
import { findFrozenBindings } from "../doctor/frozen.js";
|
|
11
|
-
import { appendEvent, readEvents, suggestionsFrom, summarize, } from "../doctor/ledger.js";
|
|
11
|
+
import { appendEvent, COVERAGE_RULE, readEvents, suggestionsFrom, summarize, } from "../doctor/ledger.js";
|
|
12
12
|
import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
|
|
13
13
|
import { checkableName, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
|
|
14
14
|
import { diagnose, scanSource, siblingTokens, } from "../doctor/scan.js";
|
|
@@ -352,7 +352,15 @@ function verdict(d, hasSystem, overruled, conflicts) {
|
|
|
352
352
|
}
|
|
353
353
|
const lines = [
|
|
354
354
|
...head,
|
|
355
|
-
|
|
355
|
+
/**
|
|
356
|
+
* "JÁ TÊM NOME" LEU COMO "JÁ ESTÃO NOMEADOS" - e o dono fez essa leitura em voz alta em 13/08:
|
|
357
|
+
* *"aqui diz que eu já tenho 243 nomeados no projeto, certo?"*. Não: os 243 estão DENTRO dos
|
|
358
|
+
* escritos à mão. O nome existe no sistema dele e o código continua escrevendo o literal.
|
|
359
|
+
*
|
|
360
|
+
* A frase estava tecnicamente correta e induziu a leitura errada em quem construiu a ferramenta.
|
|
361
|
+
* O verbo agora é do SISTEMA, não do valor: um nome ESPERA por eles.
|
|
362
|
+
*/
|
|
363
|
+
body(`${d.named} of ${d.findings.length} have a name waiting in your system - still written as literals.`),
|
|
356
364
|
body("Those are the cheap ones: swap the literal for the token."),
|
|
357
365
|
];
|
|
358
366
|
if (d.findings.length > d.named) {
|
|
@@ -702,6 +710,7 @@ export async function doctor(opts) {
|
|
|
702
710
|
kind: "doctor",
|
|
703
711
|
at: new Date().toISOString(),
|
|
704
712
|
coverage: d.coverage,
|
|
713
|
+
rule: COVERAGE_RULE,
|
|
705
714
|
named: d.named,
|
|
706
715
|
...(d.findings.some((f) => f.crossFamily)
|
|
707
716
|
? { crossFamily: d.findings.filter((f) => f.crossFamily).length }
|
|
@@ -718,7 +727,7 @@ export async function doctor(opts) {
|
|
|
718
727
|
console.log(body("and the page would change. Wire the two lines first."));
|
|
719
728
|
}
|
|
720
729
|
else {
|
|
721
|
-
console.log(body(`${d.named} of the ${d.findings.length} hand-written values found
|
|
730
|
+
console.log(body(`${d.named} of the ${d.findings.length} hand-written values found have a name waiting in`));
|
|
722
731
|
console.log(body("your system, and that number becomes actionable the moment both are true."));
|
|
723
732
|
}
|
|
724
733
|
return;
|
|
@@ -757,7 +766,46 @@ export async function doctor(opts) {
|
|
|
757
766
|
if (hasSystem && measurable) {
|
|
758
767
|
console.log("");
|
|
759
768
|
console.log(body(`Token coverage ${meter(d.coverage)} ${paint.strong(`${String(d.coverage).padStart(3)}%`)}`));
|
|
760
|
-
console.log(body(paint.dim(` ${d.tokenUses} from the system, ${d.findings.length} by hand${d.phantomUses > 0 ? `, ${d.phantomUses} naming nothing` : ""}`)));
|
|
769
|
+
console.log(body(paint.dim(` ${d.tokenUses} from the system${d.ownUses > 0 ? `, ${d.ownUses} from your own tokens` : ""}, ${d.findings.length} by hand${d.phantomUses > 0 ? `, ${d.phantomUses} naming nothing` : ""}`)));
|
|
770
|
+
/**
|
|
771
|
+
* A CAMADA DE TOKEN DELE, CONTADA - e a decisão é do dono, em 13/08.
|
|
772
|
+
*
|
|
773
|
+
* `--ds-*` é normalização NOSSA, para poder governar. Um projeto que declara
|
|
774
|
+
* `--color-ocean-500: #059aed` no CSS global tem um design system de verdade, escrito antes de a
|
|
775
|
+
* gente existir. A régua tratava aquilo como se não existisse - não era cobertura e não era
|
|
776
|
+
* deriva -, e a tela dizia "1 do sistema" sobre um repositório com 93 variáveis próprias usadas
|
|
777
|
+
* 122 vezes: *"não está fazendo sentido ter apenas 1 token no sistema"*.
|
|
778
|
+
*
|
|
779
|
+
* E a linha seguinte é a que torna a adoção barata: 89 daquelas 93 seguram um valor que um
|
|
780
|
+
* `--ds-*` também segura. Apontar uma para a outra é uma linha por variável, num arquivo só - e
|
|
781
|
+
* os 122 usos seguem o sistema sem tocar em um componente sequer.
|
|
782
|
+
*/
|
|
783
|
+
if (d.ownTokens > 0) {
|
|
784
|
+
console.log(body(paint.dim(` ${d.ownTokens} token${d.ownTokens === 1 ? "" : "s"} of your own, used ${d.ownUses}x${d.ownMirrored > 0 ? ` - ${d.ownMirrored} hold a value this system also names` : ""}`)));
|
|
785
|
+
if (d.ownMirrored > 0)
|
|
786
|
+
console.log(body(paint.dim(` point those at the \`--ds-*\` that holds it: ${d.ownMirrored} lines, one file`)));
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* O QUE UM COMANDO ALCANÇA, ao lado do que é verdade hoje - e são DUAS contas de propósito.
|
|
790
|
+
*
|
|
791
|
+
* O dono propôs em 13/08 usar os "já têm nome" como a porcentagem do medidor. A metade certa da
|
|
792
|
+
* proposta é que um `0%` sozinho não é acionável: ele descreve e não diz o que fazer. A metade
|
|
793
|
+
* que não pode acontecer é o medidor passar a mostrar a segunda conta - a barra andaria de 0 para
|
|
794
|
+
* 5 sem uma linha do app dele mudar, prometendo uma adoção que não aconteceu.
|
|
795
|
+
*
|
|
796
|
+
* Então o medidor segue medindo o que o código APONTA hoje, e o alcance vira uma linha ao lado,
|
|
797
|
+
* com o comando. O número já era impresso duas seções abaixo, solto: o que faltava era dizer que
|
|
798
|
+
* ele está a um comando de distância.
|
|
799
|
+
*
|
|
800
|
+
* Medido no repo do dono: 1 uso do sistema, 4 433 à mão, 243 com nome - o medidor diz 0% e a
|
|
801
|
+
* linha diz 5%.
|
|
802
|
+
*/
|
|
803
|
+
const nameable = d.findings.filter((f) => f.token).length;
|
|
804
|
+
if (nameable > 0) {
|
|
805
|
+
const reach = Math.round(((d.tokenUses + nameable) / (d.tokenUses + d.findings.length)) * 100);
|
|
806
|
+
console.log(body(paint.dim(` ${reach}% is one command away - ${nameable} of those have a name waiting`)));
|
|
807
|
+
console.log(body(paint.dim(" npx synthesisui doctor --fix")));
|
|
808
|
+
}
|
|
761
809
|
/**
|
|
762
810
|
* ZERO NUM REPO QUE ORIGINOU O SISTEMA É O ESTADO CERTO, e sem esta linha ele lê como falha
|
|
763
811
|
* NOSSA.
|
|
@@ -773,7 +821,18 @@ export async function doctor(opts) {
|
|
|
773
821
|
*
|
|
774
822
|
* A lei 14 em uma linha: um zero pelado lê como falha nossa, um zero com motivo lê como fato.
|
|
775
823
|
*/
|
|
776
|
-
|
|
824
|
+
/**
|
|
825
|
+
* A CONDIÇÃO É O QUE A TELA MOSTRA - e um único uso calava o parágrafo inteiro.
|
|
826
|
+
*
|
|
827
|
+
* Ela perguntava `tokenUses === 0`, e o medidor mostra a PORCENTAGEM: com 1 uso em 4 434
|
|
828
|
+
* valores, a barra diz `0%` e a explicação não sai. Foi exatamente o que o dono viu em 13/08 -
|
|
829
|
+
* *"Token coverage 0% · 1 from the system, 4433 by hand"*, sem uma linha dizendo por quê, que é
|
|
830
|
+
* o zero pelado que o comentário logo acima existe para impedir.
|
|
831
|
+
*
|
|
832
|
+
* O `1` não muda nada do que a frase afirma: os valores escritos à mão continuam sendo a FONTE
|
|
833
|
+
* de onde os tokens saíram, e não um desvio deles.
|
|
834
|
+
*/
|
|
835
|
+
if (d.coverage === 0 && measured.system) {
|
|
777
836
|
console.log(body(paint.dim(` zero is the expected start here - this system was measured FROM`)));
|
|
778
837
|
console.log(body(paint.dim(` \`${measured.system}\`, so these values are its source, not a drift`)));
|
|
779
838
|
console.log(body(paint.dim(` from it. They count once the code points at the names they became.`)));
|
|
@@ -785,7 +844,7 @@ export async function doctor(opts) {
|
|
|
785
844
|
const named = d.findings.filter((f) => f.token).length;
|
|
786
845
|
if (named > 0) {
|
|
787
846
|
console.log("");
|
|
788
|
-
console.log(body(`${paint.strong(String(named))} of the ${d.findings.length} hand-written values
|
|
847
|
+
console.log(body(`${paint.strong(String(named))} of the ${d.findings.length} hand-written values have a name waiting in YOUR system - they are still literals.`));
|
|
789
848
|
console.log(body(paint.dim("Your agent has no way to know: the tokens exist, the contract does not.")));
|
|
790
849
|
}
|
|
791
850
|
}
|
|
@@ -817,6 +876,8 @@ export async function doctor(opts) {
|
|
|
817
876
|
kind: "doctor",
|
|
818
877
|
at: new Date().toISOString(),
|
|
819
878
|
coverage: d.coverage,
|
|
879
|
+
/** Qual régua mediu - ver `COVERAGE_RULE`. Sem isto a tendência compara duas réguas. */
|
|
880
|
+
rule: COVERAGE_RULE,
|
|
820
881
|
named: d.named,
|
|
821
882
|
...(crossFamily > 0 ? { crossFamily } : {}),
|
|
822
883
|
/** E os pares em si, para a plataforma reavaliar sem esperar outra medição - ver `matched`. */
|
|
@@ -842,6 +903,15 @@ export async function doctor(opts) {
|
|
|
842
903
|
if (record.coverage && record.coverage.from !== record.coverage.to) {
|
|
843
904
|
console.log(body(` coverage ${record.coverage.from}% → ${record.coverage.to}% since the first run.`));
|
|
844
905
|
}
|
|
906
|
+
/**
|
|
907
|
+
* A RÉGUA MUDOU, DITO - em vez de um salto que ninguém fez.
|
|
908
|
+
*
|
|
909
|
+
* Quando a cobertura passou a contar a camada de token dele, o mesmo repositório saltou de 0%
|
|
910
|
+
* para 37% sem uma linha de código mudada. Somar as duas fotos anunciaria um progresso
|
|
911
|
+
* inventado; calar a mudança deixaria alguém procurar o que fez o número pular.
|
|
912
|
+
*/
|
|
913
|
+
if (record.ruleChanged)
|
|
914
|
+
console.log(body(" the coverage ruler changed in 0.16.219 - your own tokens now count, so the trend restarts here."));
|
|
845
915
|
}
|
|
846
916
|
}
|
|
847
917
|
/**
|
|
@@ -1077,8 +1147,8 @@ export async function doctor(opts) {
|
|
|
1077
1147
|
if (frozen.length > 0) {
|
|
1078
1148
|
say(section("These will not follow your other scheme"));
|
|
1079
1149
|
say(body(frozen.length === 1
|
|
1080
|
-
? "One recipe names a primitive where a role holds the same value."
|
|
1081
|
-
: `${frozen.length} recipes name a primitive where a role holds the same value.`));
|
|
1150
|
+
? "One recipe names a primitive where a SURFACE role of yours holds the same value."
|
|
1151
|
+
: `${frozen.length} recipes name a primitive where a SURFACE role of yours holds the same value.`));
|
|
1082
1152
|
say("");
|
|
1083
1153
|
for (const f of frozen) {
|
|
1084
1154
|
say(body(`ds-${f.component} · ${f.where}`));
|
|
@@ -2,7 +2,7 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { generateComponentFiles } from "../component-codegen.js";
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
|
-
import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
|
|
5
|
+
import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
6
6
|
import { postGenerate, RegistryError } from "../registry.js";
|
|
7
7
|
/** PascalCase para o hint de import (course-card → CourseCard). */
|
|
8
8
|
function pascalName(name) {
|
|
@@ -74,7 +74,7 @@ export async function generate(description, opts) {
|
|
|
74
74
|
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, version, config.styles, await reactMajorOf(root),
|
|
75
75
|
// Read off the installed document: a generated component lands in the same
|
|
76
76
|
// project as the stylesheet it has to match.
|
|
77
|
-
await readInstalledConvention(root, slug));
|
|
77
|
+
await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug));
|
|
78
78
|
for (const file of files) {
|
|
79
79
|
await writeFile(join(compDir, file.filename), file.code, "utf8");
|
|
80
80
|
}
|
package/dist/commands/refit.js
CHANGED
|
@@ -3,7 +3,7 @@ import { basename, join } from "node:path";
|
|
|
3
3
|
import { generateComponentFiles } from "../component-codegen.js";
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
5
|
import { body, section, snippet } from "../output.js";
|
|
6
|
-
import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
|
|
6
|
+
import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
7
7
|
import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
|
|
8
8
|
/** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
|
|
9
9
|
* a folder holding only refit artifacts doesn't count). */
|
|
@@ -133,7 +133,7 @@ export async function refit(file, opts) {
|
|
|
133
133
|
if (config.target === "next") {
|
|
134
134
|
const compDir = join(root, config.componentsDir, res.name);
|
|
135
135
|
await mkdir(compDir, { recursive: true });
|
|
136
|
-
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, config.styles, await reactMajorOf(root), await readInstalledConvention(root, slug));
|
|
136
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, config.styles, await reactMajorOf(root), await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug));
|
|
137
137
|
for (const f of files) {
|
|
138
138
|
await writeFile(join(compDir, f.filename), f.code, "utf8");
|
|
139
139
|
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -7,7 +7,7 @@ import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
|
7
7
|
import { diffLocalDocuments, localChangelogMarkdown, } from "../document-diff.js";
|
|
8
8
|
import { installedBehind, MATERIALISER_SINCE } from "../install-marks.js";
|
|
9
9
|
import { body, section, snippet } from "../output.js";
|
|
10
|
-
import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
|
|
10
|
+
import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
11
11
|
import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
|
|
12
12
|
import { add } from "./add.js";
|
|
13
13
|
import { reportWhatIsLeft } from "./align.js";
|
|
@@ -300,7 +300,7 @@ export async function upgrade(asked, opts) {
|
|
|
300
300
|
// Both were missing here, and `upgrade` is the command that REWRITES
|
|
301
301
|
// components somebody already has: without the convention it would have
|
|
302
302
|
// taken a working component and stripped its styles.
|
|
303
|
-
await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)));
|
|
303
|
+
await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)), res.name, await readInstalledScheme(root, slug));
|
|
304
304
|
for (const file of files) {
|
|
305
305
|
await writeFile(join(componentsRoot, entry, file.filename), file.code, "utf8");
|
|
306
306
|
}
|
|
@@ -192,12 +192,50 @@ function layerAxes(part, variants) {
|
|
|
192
192
|
}
|
|
193
193
|
return out;
|
|
194
194
|
}
|
|
195
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* The axes of a NODE - root or part: the ones it declares, plus the ones its layers
|
|
197
|
+
* read. Both callers use this; a node that reads only `variants` is the reader that
|
|
198
|
+
* dropped 15 of his 19 components.
|
|
199
|
+
*/
|
|
196
200
|
function partAxesOf(part, variants) {
|
|
197
201
|
const own = axesOf(part.variants ?? {});
|
|
198
202
|
const keys = new Set(own.map((a) => a.key));
|
|
199
203
|
return [...own, ...layerAxes(part, variants).filter((a) => !keys.has(a.key))];
|
|
200
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* THE AXES OF THE COMPONENT ITSELF - and an axis nothing styles is still his API.
|
|
207
|
+
*
|
|
208
|
+
* `axesOf` drops an axis whose every option block is empty, which is right for a
|
|
209
|
+
* recipe we generated: there the option IS the styles, so an empty one is noise we
|
|
210
|
+
* invented. On a recipe we READ it is the opposite. His `Card` declares
|
|
211
|
+
* `variant: default | full-border | left-border` and this document carries no
|
|
212
|
+
* declarations for any of the three - the axis is a fact about HIS component that
|
|
213
|
+
* we failed to read the look for, and dropping the prop would silently narrow the
|
|
214
|
+
* API his own call sites already use.
|
|
215
|
+
*
|
|
216
|
+
* So the prop and the `data-variant` ship. The missing look is a reading gap, and
|
|
217
|
+
* it is already counted as one - `gaps` and the ledger say so with a number, which
|
|
218
|
+
* is the honest place for it. Ten of his components sat in exactly this state.
|
|
219
|
+
*/
|
|
220
|
+
function rootAxes(recipe) {
|
|
221
|
+
const fromLayers = partAxesOf(recipe, recipe.variants);
|
|
222
|
+
const seen = new Set(fromLayers.map((a) => a.key));
|
|
223
|
+
const declared = [];
|
|
224
|
+
for (const [axis, options] of Object.entries(recipe.variants ?? {})) {
|
|
225
|
+
const keys = Object.keys(options);
|
|
226
|
+
if (keys.length === 0 || seen.has(axis))
|
|
227
|
+
continue;
|
|
228
|
+
declared.push({
|
|
229
|
+
key: axis,
|
|
230
|
+
prop: camel(axis),
|
|
231
|
+
attr: kebab(axis),
|
|
232
|
+
boolean: keys.every((k) => k === "true" || k === "false"),
|
|
233
|
+
styledFalse: keys.includes("false"),
|
|
234
|
+
options: keys,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return [...fromLayers, ...declared];
|
|
238
|
+
}
|
|
201
239
|
function axesOf(variants) {
|
|
202
240
|
const axes = [];
|
|
203
241
|
for (const [axis, options] of Object.entries(variants ?? {})) {
|
|
@@ -489,17 +527,196 @@ function declToTailwind(prop, value) {
|
|
|
489
527
|
}
|
|
490
528
|
return [arbitrary(prop, value)];
|
|
491
529
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
530
|
+
/**
|
|
531
|
+
* TAILWIND'S OWN WORD FOR A STATE, WHEN IT HAS ONE - and a mechanical rule when it
|
|
532
|
+
* does not, because a hand-written table would be a table of LIBRARIES.
|
|
533
|
+
*
|
|
534
|
+
* The contract's `STATE_SELECTORS` is a closed vocabulary of about thirty states,
|
|
535
|
+
* and most of them are attribute-shaped on purpose: `pressed` is how @base-ui
|
|
536
|
+
* spells it, `highlighted` is the option the arrow keys are on, `startingStyle` is
|
|
537
|
+
* an enter animation. Naming each one here by hand would rot the moment the
|
|
538
|
+
* contract grows a thirty-first, and the CLI cannot import the contract (it ships
|
|
539
|
+
* standalone), so a mirror would be a second copy to keep in step.
|
|
540
|
+
*
|
|
541
|
+
* The rule instead: a state Tailwind already has a variant for uses that word;
|
|
542
|
+
* everything else becomes `data-[<kebab>]:`, which is exactly the selector the
|
|
543
|
+
* compiler emits for it. New state in the contract, no edit here.
|
|
544
|
+
*/
|
|
545
|
+
const TW_NATIVE = new Set([
|
|
546
|
+
"hover",
|
|
547
|
+
"active",
|
|
548
|
+
"checked",
|
|
549
|
+
"disabled",
|
|
550
|
+
"empty",
|
|
551
|
+
"even",
|
|
552
|
+
"first",
|
|
553
|
+
"indeterminate",
|
|
554
|
+
"invalid",
|
|
555
|
+
"last",
|
|
556
|
+
"odd",
|
|
557
|
+
"open",
|
|
558
|
+
"placeholder",
|
|
559
|
+
"read-only",
|
|
560
|
+
"required",
|
|
561
|
+
"target",
|
|
562
|
+
"visited",
|
|
563
|
+
"after",
|
|
564
|
+
"before",
|
|
565
|
+
]);
|
|
566
|
+
/**
|
|
567
|
+
* WHERE ONE STATE HAS MORE THAN ONE SPELLING, ALL OF THEM SHIP.
|
|
568
|
+
*
|
|
569
|
+
* `disabled` is `:disabled` on a real control, `[aria-disabled]` on an accessible
|
|
570
|
+
* div and `[data-disabled]` in every headless kit - the compiler matches all three
|
|
571
|
+
* with one comma-joined rule, and picking one here would be picking a library. In
|
|
572
|
+
* utilities that costs one extra class per declaration, on the handful of states
|
|
573
|
+
* that actually have two grammars.
|
|
574
|
+
*/
|
|
575
|
+
const TW_ALSO = {
|
|
576
|
+
disabled: ["aria-disabled:", "data-[disabled]:"],
|
|
577
|
+
checked: ["aria-checked:", "data-[checked]:"],
|
|
578
|
+
open: ["aria-expanded:", "data-[open]:"],
|
|
579
|
+
selected: ["aria-selected:", "data-[selected]:"],
|
|
498
580
|
};
|
|
581
|
+
/** A state name → every Tailwind variant that expresses it. */
|
|
582
|
+
function twStateVariants(state) {
|
|
583
|
+
// The compiler maps both to `:focus-visible` - a focus ring that also shows on
|
|
584
|
+
// a mouse click is the one thing nobody wants back.
|
|
585
|
+
if (state === "focus" || state === "focusVisible")
|
|
586
|
+
return ["focus-visible:"];
|
|
587
|
+
const word = kebab(state);
|
|
588
|
+
const native = TW_NATIVE.has(word) ? [`${word}:`] : [`data-[${word}]:`];
|
|
589
|
+
return [...native, ...(TW_ALSO[state] ?? [])];
|
|
590
|
+
}
|
|
499
591
|
function blockToTailwind(block, prefix = "") {
|
|
500
592
|
return Object.entries(block).flatMap(([prop, value]) => declToTailwind(prop, value).map((cls) => `${prefix}${cls}`));
|
|
501
593
|
}
|
|
594
|
+
/** Every prefix combination one layer needs. A condition that cannot be
|
|
595
|
+
* expressed returns no combination, and the layer is dropped rather than
|
|
596
|
+
* emitted wrong - the same choice the compiler makes for an unknown state. */
|
|
597
|
+
function twPrefixes(when, at, scheme,
|
|
598
|
+
/** On the ROOT, `within` IS its own state - the compiler says so explicitly. */
|
|
599
|
+
asRoot) {
|
|
600
|
+
const groups = [];
|
|
601
|
+
/**
|
|
602
|
+
* THE SCHEME THE DOCUMENT OPENS IN IS THE RESTING LOOK, and getting this
|
|
603
|
+
* backwards is the one mistake here that inverts light and dark. His document
|
|
604
|
+
* opens DARK, so its 79 `scheme: dark` layers are what the component looks like
|
|
605
|
+
* at rest and they take no prefix at all; `light` is the one that needs the
|
|
606
|
+
* ancestor. `compile-css.ts` decides it the same way, off `docScheme`.
|
|
607
|
+
*/
|
|
608
|
+
if (when.scheme && when.scheme !== scheme) {
|
|
609
|
+
// Both attributes, because that is what the compiled stylesheet matches and a
|
|
610
|
+
// real project puts one or the other on <html>.
|
|
611
|
+
groups.push([
|
|
612
|
+
`[[data-scheme=${when.scheme}]_&]:`,
|
|
613
|
+
`[[data-theme=${when.scheme}]_&]:`,
|
|
614
|
+
]);
|
|
615
|
+
}
|
|
616
|
+
if (when.within) {
|
|
617
|
+
const own = twStateVariants(when.within);
|
|
618
|
+
if (own.length === 0)
|
|
619
|
+
return [];
|
|
620
|
+
groups.push(asRoot ? own : own.map((v) => `group-${v}`));
|
|
621
|
+
}
|
|
622
|
+
if (when.state) {
|
|
623
|
+
const own = twStateVariants(when.state);
|
|
624
|
+
if (own.length === 0)
|
|
625
|
+
return [];
|
|
626
|
+
groups.push(own);
|
|
627
|
+
}
|
|
628
|
+
for (const [axis, option] of Object.entries(when.variant ?? {})) {
|
|
629
|
+
// `!option` is every OTHER option - `not-data-[…]` is the utility for the
|
|
630
|
+
// `:not()` the compiler writes.
|
|
631
|
+
groups.push(option.startsWith("!")
|
|
632
|
+
? [`not-data-[${kebab(axis)}=${option.slice(1)}]:`]
|
|
633
|
+
: [`data-[${kebab(axis)}=${option}]:`]);
|
|
634
|
+
}
|
|
635
|
+
if (at)
|
|
636
|
+
groups.push([`${at}:`]);
|
|
637
|
+
return groups.reduce((acc, group) => acc.flatMap((prefix) => group.map((g) => `${prefix}${g}`)), [""]);
|
|
638
|
+
}
|
|
639
|
+
function resolveNode(node, scheme, asRoot) {
|
|
640
|
+
const variants = {};
|
|
641
|
+
for (const [axis, options] of Object.entries(node.variants ?? {})) {
|
|
642
|
+
variants[axis] = {};
|
|
643
|
+
for (const [option, block] of Object.entries(options))
|
|
644
|
+
variants[axis][option] = { ...block };
|
|
645
|
+
}
|
|
646
|
+
const states = {};
|
|
647
|
+
for (const [state, block] of Object.entries(node.states ?? {}))
|
|
648
|
+
states[state] = { ...block };
|
|
649
|
+
const conditional = [];
|
|
650
|
+
for (const layer of node.layers ?? []) {
|
|
651
|
+
if (!layer.style || Object.keys(layer.style).length === 0)
|
|
652
|
+
continue;
|
|
653
|
+
const when = { ...(layer.when ?? {}) };
|
|
654
|
+
/**
|
|
655
|
+
* A LAYER FOR THE DOCUMENT'S OWN SCHEME CARRIES NO CONDITION - dropping the key
|
|
656
|
+
* here is what lets `{scheme: dark, variant: ocean}` land in the variant map as
|
|
657
|
+
* plain `ocean`, instead of being pushed into a compound prefix nobody needs.
|
|
658
|
+
*/
|
|
659
|
+
if (when.scheme === scheme)
|
|
660
|
+
delete when.scheme;
|
|
661
|
+
const axes = Object.entries(when.variant ?? {});
|
|
662
|
+
const simpleVariant = !layer.at &&
|
|
663
|
+
!when.state &&
|
|
664
|
+
!when.within &&
|
|
665
|
+
!when.scheme &&
|
|
666
|
+
axes.length === 1 &&
|
|
667
|
+
!axes[0][1].startsWith("!");
|
|
668
|
+
const simpleState = !layer.at &&
|
|
669
|
+
!when.within &&
|
|
670
|
+
!when.scheme &&
|
|
671
|
+
axes.length === 0 &&
|
|
672
|
+
Boolean(when.state);
|
|
673
|
+
if (simpleVariant) {
|
|
674
|
+
const [axis, option] = axes[0];
|
|
675
|
+
variants[axis] = variants[axis] ?? {};
|
|
676
|
+
variants[axis][option] = { ...variants[axis][option], ...layer.style };
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (simpleState && when.state) {
|
|
680
|
+
states[when.state] = { ...states[when.state], ...layer.style };
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
for (const prefix of twPrefixes(when, layer.at, scheme, asRoot))
|
|
684
|
+
conditional.push({ prefix, style: layer.style });
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* AND THE TOGGLE HAS TO BE ABLE TO UNDO THE RESTING LOOK.
|
|
688
|
+
*
|
|
689
|
+
* A part reads as `base: bg-white` + a `dark` layer; on a dark document that
|
|
690
|
+
* layer is correct bare, but bare it also wins when somebody flips to light. So
|
|
691
|
+
* every property a resting-scheme layer overrides gets its BASE value back under
|
|
692
|
+
* the other scheme's ancestor - the same rule `compile-css.ts` applies, and
|
|
693
|
+
* without it the flip is one-way.
|
|
694
|
+
*/
|
|
695
|
+
const alt = scheme === "dark" ? "light" : "dark";
|
|
696
|
+
for (const layer of node.layers ?? []) {
|
|
697
|
+
if (layer.when?.scheme !== scheme)
|
|
698
|
+
continue;
|
|
699
|
+
if (layer.when?.state || layer.when?.within)
|
|
700
|
+
continue;
|
|
701
|
+
const restore = {};
|
|
702
|
+
for (const prop of Object.keys(layer.style ?? {}))
|
|
703
|
+
if (node.base?.[prop] != null)
|
|
704
|
+
restore[prop] = node.base[prop];
|
|
705
|
+
if (Object.keys(restore).length === 0)
|
|
706
|
+
continue;
|
|
707
|
+
for (const prefix of [
|
|
708
|
+
`[[data-scheme=${alt}]_&]:`,
|
|
709
|
+
`[[data-theme=${alt}]_&]:`,
|
|
710
|
+
])
|
|
711
|
+
conditional.push({ prefix, style: restore });
|
|
712
|
+
}
|
|
713
|
+
return { variants, states, conditional };
|
|
714
|
+
}
|
|
715
|
+
/** The classes every compound condition contributes, in layer order. */
|
|
716
|
+
const conditionalClasses = (resolved) => resolved.conditional.flatMap((c) => blockToTailwind(c.style, c.prefix));
|
|
502
717
|
function tailwindClassList(recipe,
|
|
718
|
+
/** The node's states and compound conditions, already read off its layers. */
|
|
719
|
+
resolved,
|
|
503
720
|
/** CSS properties a variant axis owns - see `variantOwnedProps`. */
|
|
504
721
|
exclude) {
|
|
505
722
|
const base = exclude
|
|
@@ -509,7 +726,10 @@ exclude) {
|
|
|
509
726
|
...blockToTailwind(base),
|
|
510
727
|
// States keep everything: `hover:` and `disabled:` cannot collide with an
|
|
511
728
|
// unprefixed variant class, so there is nothing to resolve.
|
|
512
|
-
...Object.entries(
|
|
729
|
+
...Object.entries(resolved.states).flatMap(([state, block]) => twStateVariants(state).flatMap((prefix) => blockToTailwind(block, prefix))),
|
|
730
|
+
// Compound conditions last: they are the most specific thing the recipe says,
|
|
731
|
+
// and in utilities the later class is the one a reader expects to win.
|
|
732
|
+
...conditionalClasses(resolved),
|
|
513
733
|
];
|
|
514
734
|
return classes.join(" ");
|
|
515
735
|
}
|
|
@@ -771,7 +991,16 @@ function emitCssMode(slug, name, recipe, version, props, convention,
|
|
|
771
991
|
localName = name) {
|
|
772
992
|
const { tag, attrs, voidEl } = elementFor(name, recipe);
|
|
773
993
|
const el = asElement(tag, attrs, voidEl);
|
|
774
|
-
|
|
994
|
+
/**
|
|
995
|
+
* THE ROOT READS ITS AXES THE SAME WAY ITS PARTS DO, which is the whole fix.
|
|
996
|
+
*
|
|
997
|
+
* `axesOf` alone keeps an axis only when the option's own block carries
|
|
998
|
+
* declarations - true for a recipe we generated, and false for every recipe we
|
|
999
|
+
* READ, where the block is empty and the look is in `layers`. The parts learned
|
|
1000
|
+
* this on 04/08; the root was left on the old reader and 15 of his 19 components
|
|
1001
|
+
* with an axis shipped with no prop for it.
|
|
1002
|
+
*/
|
|
1003
|
+
const axes = rootAxes(recipe);
|
|
775
1004
|
const comp = pascal(localName);
|
|
776
1005
|
const propNames = axes.map((a) => a.prop);
|
|
777
1006
|
const tree = recipe.preview?.parts ?? [];
|
|
@@ -857,28 +1086,56 @@ ${parts.filter(Boolean).join("\n")}`;
|
|
|
857
1086
|
}
|
|
858
1087
|
function emitTailwindMode(slug, name, recipe, version, props,
|
|
859
1088
|
/** The name it takes in THEIR project. The utilities stay the system's. */
|
|
860
|
-
localName = name
|
|
1089
|
+
localName = name,
|
|
1090
|
+
/** The scheme the document opens in - see `generateComponentFiles`. */
|
|
1091
|
+
scheme = "dark") {
|
|
861
1092
|
const { tag, attrs, voidEl } = elementFor(name, recipe);
|
|
862
1093
|
const el = asElement(tag, attrs, voidEl);
|
|
863
|
-
const axes =
|
|
1094
|
+
const axes = rootAxes(recipe);
|
|
864
1095
|
const comp = pascal(localName);
|
|
1096
|
+
const resolved = resolveNode(recipe, scheme, true);
|
|
865
1097
|
const variantConsts = axes
|
|
866
1098
|
.filter((a) => !a.boolean)
|
|
867
1099
|
.map((a) => {
|
|
868
1100
|
const entries = a.options
|
|
869
|
-
.map((o) => ` ${JSON.stringify(o)}: ${JSON.stringify(blockToTailwind(
|
|
1101
|
+
.map((o) => ` ${JSON.stringify(o)}: ${JSON.stringify(blockToTailwind(resolved.variants[a.key]?.[o] ?? {}).join(" "))},`)
|
|
870
1102
|
.join("\n");
|
|
871
1103
|
return `const ${a.prop.toUpperCase()}: Record<string, string> = {\n${entries}\n};`;
|
|
872
1104
|
});
|
|
873
1105
|
const booleanConsts = axes
|
|
874
1106
|
.filter((a) => a.boolean)
|
|
875
|
-
.map((a) => `const ${a.prop.toUpperCase()} = ${JSON.stringify(blockToTailwind(
|
|
1107
|
+
.map((a) => `const ${a.prop.toUpperCase()} = ${JSON.stringify(blockToTailwind(resolved.variants[a.key]?.true ?? {}).join(" "))};`);
|
|
876
1108
|
// Every property some axis controls leaves BASE, and the base value becomes
|
|
877
1109
|
// that axis's default - so exactly one class ever sets it and the prop
|
|
878
1110
|
// actually wins.
|
|
879
|
-
const owned = new Map(axes.map((a) => [a.prop, variantOwnedProps(
|
|
1111
|
+
const owned = new Map(axes.map((a) => [a.prop, variantOwnedProps(resolved.variants, a)]));
|
|
880
1112
|
const excluded = new Set([...owned.values()].flat());
|
|
881
|
-
|
|
1113
|
+
/**
|
|
1114
|
+
* `group-hover:` NEEDS A GROUP, and the root is the only node that can carry it.
|
|
1115
|
+
*
|
|
1116
|
+
* `within` is a state of the ROOT read from a PART - the idiom a real table row
|
|
1117
|
+
* is built on, and the contract names the utility outright. Emitting
|
|
1118
|
+
* `group-hover:` on the part while nothing marks the group is a class that
|
|
1119
|
+
* matches nothing, which is the same silent nothing this whole file is about.
|
|
1120
|
+
*/
|
|
1121
|
+
const needsGroup = Object.values(recipe.parts ?? {}).some((part) => (part.layers ?? []).some((layer) => layer.when?.within));
|
|
1122
|
+
/**
|
|
1123
|
+
* THE DEFAULT OPTION IS THE RESTING LOOK - which the destructuring already says.
|
|
1124
|
+
*
|
|
1125
|
+
* `{ variant }` undefined means the component IS its default, and on a READ
|
|
1126
|
+
* recipe the base has no value for the property at all (his Button declares no
|
|
1127
|
+
* `backgroundColor` in base - every one of the nine lives in a layer). Falling
|
|
1128
|
+
* back to base alone therefore rendered the default variant as nothing. The
|
|
1129
|
+
* compiler emits the default's layer bare for exactly this reason.
|
|
1130
|
+
*/
|
|
1131
|
+
const fallbackFor = (a) => {
|
|
1132
|
+
const fromBase = Object.fromEntries(Object.entries(recipe.base).filter(([prop]) => (owned.get(a.prop) ?? []).includes(prop)));
|
|
1133
|
+
const preset = recipe.defaults?.[a.key];
|
|
1134
|
+
const fromDefault = preset
|
|
1135
|
+
? (resolved.variants[a.key]?.[preset] ?? {})
|
|
1136
|
+
: {};
|
|
1137
|
+
return JSON.stringify(blockToTailwind({ ...fromBase, ...fromDefault }).join(" "));
|
|
1138
|
+
};
|
|
882
1139
|
const clsParts = [
|
|
883
1140
|
"BASE",
|
|
884
1141
|
...axes.map((a) => a.boolean
|
|
@@ -922,7 +1179,7 @@ ${dataAttrLines(axes)}${axes.length ? "\n" : ""} `;
|
|
|
922
1179
|
import type { ${needsElementType ? `ElementType, ${props}` : props} } from "react";
|
|
923
1180
|
import { cn } from "../cn";
|
|
924
1181
|
|
|
925
|
-
const BASE = ${JSON.stringify(tailwindClassList(recipe, excluded))};
|
|
1182
|
+
const BASE = ${JSON.stringify([needsGroup ? "group" : "", tailwindClassList(recipe, resolved, excluded)].filter(Boolean).join(" "))};
|
|
926
1183
|
${[...variantConsts, ...booleanConsts].join("\n")}
|
|
927
1184
|
|
|
928
1185
|
type ${comp}Props = ${propsType(axes, tag, props, el.offersAs)};
|
|
@@ -965,8 +1222,19 @@ ${orderedParts
|
|
|
965
1222
|
* toggles data-active from its own state, which is how the GUIDE
|
|
966
1223
|
* documents the contract.
|
|
967
1224
|
*/
|
|
968
|
-
|
|
969
|
-
|
|
1225
|
+
/**
|
|
1226
|
+
* AND THE OPTION'S LOOK COMES OFF THE LAYERS TOO - the half of 04/08 that was
|
|
1227
|
+
* left behind. The part got its prop and its `data-status` attribute, and the
|
|
1228
|
+
* classes were read from `part.variants[axis][option]`, which on a read recipe
|
|
1229
|
+
* is the empty block: the attribute shipped, nothing answered it, and the
|
|
1230
|
+
* status colours of his card were still nowhere.
|
|
1231
|
+
*/
|
|
1232
|
+
const partResolved = resolveNode(part, scheme, false);
|
|
1233
|
+
const partVariantClasses = partAxes.flatMap((a) => a.options.flatMap((o) => blockToTailwind(partResolved.variants[a.key]?.[o] ?? {}, `data-[${a.attr}=${o}]:`)));
|
|
1234
|
+
const partCls = [
|
|
1235
|
+
tailwindClassList(part, partResolved),
|
|
1236
|
+
...partVariantClasses,
|
|
1237
|
+
]
|
|
970
1238
|
.filter(Boolean)
|
|
971
1239
|
.join(" ");
|
|
972
1240
|
/**
|
|
@@ -1020,7 +1288,24 @@ convention = DEFAULT_CONVENTION,
|
|
|
1020
1288
|
*
|
|
1021
1289
|
* Absent means the component keeps its own name, which is every caller today.
|
|
1022
1290
|
*/
|
|
1023
|
-
localName = name
|
|
1291
|
+
localName = name,
|
|
1292
|
+
/**
|
|
1293
|
+
* WHICH SCHEME THE DOCUMENT OPENS IN - `meta.scheme`, and the one input here that
|
|
1294
|
+
* cannot be guessed without inverting light and dark.
|
|
1295
|
+
*
|
|
1296
|
+
* A layer for the document's own scheme is the RESTING look and takes no prefix;
|
|
1297
|
+
* the other one hangs off a `[data-scheme]` ancestor. Read it backwards on a
|
|
1298
|
+
* dark-native system and every dark value moves behind an attribute nothing sets,
|
|
1299
|
+
* so the component arrives unpainted. `readInstalledScheme` answers it off the
|
|
1300
|
+
* document on disk - the same one the stylesheet next to it came from, so the two
|
|
1301
|
+
* can never disagree.
|
|
1302
|
+
*
|
|
1303
|
+
* REQUIRED, with no default, deliberately. `compile-css.ts` defaults to `"dark"`
|
|
1304
|
+
* and a default here would agree with it - but it would also let a new caller ship
|
|
1305
|
+
* without ever deciding, and an optional argument that can be forgotten is the
|
|
1306
|
+
* shape this file has already been bitten by. `tsc` refuses the half-call instead.
|
|
1307
|
+
*/
|
|
1308
|
+
scheme) {
|
|
1024
1309
|
const files = [];
|
|
1025
1310
|
const props = propsTypeName(reactMajor);
|
|
1026
1311
|
if (styles === "css") {
|
|
@@ -1033,7 +1318,7 @@ localName = name) {
|
|
|
1033
1318
|
else {
|
|
1034
1319
|
files.push({
|
|
1035
1320
|
filename: `${localName}.tsx`,
|
|
1036
|
-
code: `${emitTailwindMode(slug, name, recipe, version, props, localName)}\n`,
|
|
1321
|
+
code: `${emitTailwindMode(slug, name, recipe, version, props, localName, scheme)}\n`,
|
|
1037
1322
|
});
|
|
1038
1323
|
}
|
|
1039
1324
|
files.push({
|
package/dist/doctor/frozen.js
CHANGED
|
@@ -64,9 +64,23 @@ export function findFrozenBindings(document) {
|
|
|
64
64
|
// Several roles can hold one primitive (Vesper's navy-700 is raised, overlay
|
|
65
65
|
// AND border). Surfaces are preferred, because this only ever reports a
|
|
66
66
|
// background - naming "border" for a background reads like a bug in the tool.
|
|
67
|
+
/**
|
|
68
|
+
* SÓ PAPEL DE SUPERFÍCIE, e agora é uma LISTA FECHADA em vez de uma preferência.
|
|
69
|
+
*
|
|
70
|
+
* Ela nasceu como ranking com fallback - "prefira superfície, e na falta use qualquer um" -, e o
|
|
71
|
+
* fallback é que produziu conselho perigoso no sistema real (13/08): o fundo do `card` apontando
|
|
72
|
+
* para `foreground`, o papel do TEXTO, e o do `text-editor` para `loader`, que vira azul no
|
|
73
|
+
* escuro. O comentário acima já dizia que nomear `border` para um fundo *"lê como um bug na
|
|
74
|
+
* ferramenta"*; nomear `foreground` é a mesma frase, um degrau pior.
|
|
75
|
+
*
|
|
76
|
+
* Um valor que só um papel de texto segura não ganha sugestão nenhuma - e o fato de o fundo não
|
|
77
|
+
* acompanhar o esquema continua sendo dito.
|
|
78
|
+
*/
|
|
67
79
|
const RANK = ["surface", "raised", "canvas", "overlay", "border"];
|
|
68
80
|
const roleOf = new Map();
|
|
69
81
|
for (const [role, ref] of Object.entries(semantic)) {
|
|
82
|
+
if (!RANK.includes(role))
|
|
83
|
+
continue;
|
|
70
84
|
const other = alt[role];
|
|
71
85
|
if (typeof ref !== "string" || typeof other !== "string")
|
|
72
86
|
continue;
|
|
@@ -77,11 +91,7 @@ export function findFrozenBindings(document) {
|
|
|
77
91
|
continue;
|
|
78
92
|
const key = `${m[1]}.${m[2]}`.toLowerCase();
|
|
79
93
|
const held = roleOf.get(key);
|
|
80
|
-
|
|
81
|
-
(RANK.indexOf(role) !== -1 &&
|
|
82
|
-
(RANK.indexOf(held.role) === -1 ||
|
|
83
|
-
RANK.indexOf(role) < RANK.indexOf(held.role)));
|
|
84
|
-
if (better)
|
|
94
|
+
if (!held || RANK.indexOf(role) < RANK.indexOf(held.role))
|
|
85
95
|
roleOf.set(key, { role, becomes: other });
|
|
86
96
|
}
|
|
87
97
|
if (roleOf.size === 0)
|
package/dist/doctor/ledger.js
CHANGED
|
@@ -140,6 +140,18 @@ export async function readEvents(root) {
|
|
|
140
140
|
}
|
|
141
141
|
return out;
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* An INCIDENT is a file going dirty until it is next seen clean. Consecutive
|
|
145
|
+
* dirty checks of the same file are the same incident - an agent that edits a
|
|
146
|
+
* file four times before the fix did not have four problems.
|
|
147
|
+
*/
|
|
148
|
+
/**
|
|
149
|
+
* A VERSÃO DA RÉGUA DA COBERTURA - sobe quando o mesmo repositório passa a dar outro número.
|
|
150
|
+
*
|
|
151
|
+
* A 2 é a camada de token DELE entrando na conta (13/08): `var(--color-ocean-500)`, que ele declara
|
|
152
|
+
* no CSS global, deixou de ser invisível e passou a contar como valor governado.
|
|
153
|
+
*/
|
|
154
|
+
export const COVERAGE_RULE = 2;
|
|
143
155
|
export function summarize(events) {
|
|
144
156
|
const hooks = events.filter((e) => e.kind === "hook" && e.file);
|
|
145
157
|
const byFile = new Map();
|
|
@@ -170,11 +182,25 @@ export function summarize(events) {
|
|
|
170
182
|
}
|
|
171
183
|
}
|
|
172
184
|
const snaps = events.filter((e) => e.kind === "doctor" && typeof e.coverage === "number");
|
|
173
|
-
|
|
185
|
+
/**
|
|
186
|
+
* SÓ O QUE FOI MEDIDO PELA MESMA RÉGUA - ver `LedgerEntry.rule`.
|
|
187
|
+
*
|
|
188
|
+
* A régua da última foto é a que vale; as anteriores viram "a régua mudou". Sem isto, o dia em
|
|
189
|
+
* que a cobertura passou a contar a camada dele viraria um salto de 0% para 37% que ninguém fez.
|
|
190
|
+
*/
|
|
191
|
+
const rule = snaps.length > 0 ? (snaps[snaps.length - 1].rule ?? 0) : 0;
|
|
192
|
+
const same = snaps.filter((e) => (e.rule ?? 0) === rule);
|
|
193
|
+
const coverage = same.length >= 2
|
|
174
194
|
? {
|
|
175
|
-
from:
|
|
176
|
-
to:
|
|
195
|
+
from: same[0].coverage,
|
|
196
|
+
to: same[same.length - 1].coverage,
|
|
177
197
|
}
|
|
178
198
|
: null;
|
|
179
|
-
return {
|
|
199
|
+
return {
|
|
200
|
+
checks: hooks.length,
|
|
201
|
+
resolved,
|
|
202
|
+
open,
|
|
203
|
+
coverage,
|
|
204
|
+
...(same.length !== snaps.length ? { ruleChanged: true } : {}),
|
|
205
|
+
};
|
|
180
206
|
}
|
package/dist/doctor/scan.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Text in, findings out. No filesystem, no AST, no network - a diagnosis that
|
|
10
10
|
* takes eight seconds and needs a build step is a diagnosis nobody runs.
|
|
11
11
|
*/
|
|
12
|
-
import { tokenMatch } from "./tokens.js";
|
|
12
|
+
import { normalizeValue, tokenMatch } from "./tokens.js";
|
|
13
13
|
/**
|
|
14
14
|
* `next/og` renders JSX to a PNG on the server. There is no document, so there
|
|
15
15
|
* is no `var(--ds-*)` to read: every colour in such a file MUST be a literal.
|
|
@@ -269,9 +269,13 @@ export function scanSource(file, source, table) {
|
|
|
269
269
|
}
|
|
270
270
|
return scanCore(file, source, table);
|
|
271
271
|
}
|
|
272
|
+
/** `--color-ocean-500: #059aed;` - uma declaração de custom property no CSS dele. */
|
|
273
|
+
const DECLARES = /^\s*(--[a-z0-9-]+)\s*:\s*([^;]+);/i;
|
|
272
274
|
function scanCore(file, source, table) {
|
|
273
275
|
const findings = [];
|
|
274
276
|
const phantoms = [];
|
|
277
|
+
const ownDeclared = [];
|
|
278
|
+
const ownCandidates = new Map();
|
|
275
279
|
let tokenUses = 0;
|
|
276
280
|
// Reason by reason. Rolling two into "A or B" was the one place the report
|
|
277
281
|
// still lumped things it had told apart everywhere else.
|
|
@@ -295,6 +299,34 @@ function scanCore(file, source, table) {
|
|
|
295
299
|
const line = raw.trim();
|
|
296
300
|
const at = i + 1;
|
|
297
301
|
tokenUses += countTokenUses(line, table);
|
|
302
|
+
/**
|
|
303
|
+
* O QUE ESTE ARQUIVO DECLARA E O QUE ELE USA DE FORA DO SISTEMA - ver `FileReport.ownDeclared`.
|
|
304
|
+
*
|
|
305
|
+
* Os dois são coletados aqui e cruzados no fim: uma variável pode ser usada num arquivo e
|
|
306
|
+
* declarada em outro, então a resposta não existe enquanto a varredura não terminar.
|
|
307
|
+
*/
|
|
308
|
+
const declaredHere = DECLARES.exec(line);
|
|
309
|
+
if (declaredHere && !declaredHere[1].toLowerCase().startsWith("--ds-")) {
|
|
310
|
+
const value = declaredHere[2].trim();
|
|
311
|
+
ownDeclared.push({
|
|
312
|
+
name: declaredHere[1].toLowerCase(),
|
|
313
|
+
value,
|
|
314
|
+
/**
|
|
315
|
+
* O SISTEMA TAMBÉM SEGURA ESTE VALOR? A comparação é aqui porque é aqui que a tabela existe -
|
|
316
|
+
* e é por VALOR, não por nome: `--color-ocean-500` e `--ds-color-ocean-500` só são a mesma
|
|
317
|
+
* decisão porque os dois seguram `#059aed`.
|
|
318
|
+
*/
|
|
319
|
+
...(table.byValue.has(normalizeValue(value))
|
|
320
|
+
? { mirrored: true }
|
|
321
|
+
: null),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
for (const m of line.matchAll(ANY_VAR_USE)) {
|
|
325
|
+
const name = m[1].toLowerCase();
|
|
326
|
+
if (isKnownToken(name, table))
|
|
327
|
+
continue;
|
|
328
|
+
ownCandidates.set(name, (ownCandidates.get(name) ?? 0) + 1);
|
|
329
|
+
}
|
|
298
330
|
for (const name of findPhantoms(line, table))
|
|
299
331
|
phantoms.push({ name, line: at });
|
|
300
332
|
// Depth at the START of this line, carried before the early return so a
|
|
@@ -451,6 +483,15 @@ function scanCore(file, source, table) {
|
|
|
451
483
|
findings,
|
|
452
484
|
tokenUses,
|
|
453
485
|
...(phantoms.length > 0 ? { phantoms } : null),
|
|
486
|
+
...(ownDeclared.length > 0 ? { ownDeclared } : null),
|
|
487
|
+
...(ownCandidates.size > 0
|
|
488
|
+
? {
|
|
489
|
+
ownCandidates: [...ownCandidates].map(([name, count]) => ({
|
|
490
|
+
name,
|
|
491
|
+
count,
|
|
492
|
+
})),
|
|
493
|
+
}
|
|
494
|
+
: null),
|
|
454
495
|
...(aside.size > 0
|
|
455
496
|
? {
|
|
456
497
|
setAside: [...aside].map(([reason, count]) => ({ reason, count })),
|
|
@@ -474,7 +515,41 @@ export function diagnose(files) {
|
|
|
474
515
|
// sit outside the fraction. Reporting 100% directly above "2 names your
|
|
475
516
|
// system never declares" is the report contradicting itself in six lines.
|
|
476
517
|
const phantomUses = files.reduce((n, f) => n + (f.phantoms?.length ?? 0), 0);
|
|
477
|
-
|
|
518
|
+
/**
|
|
519
|
+
* A CAMADA DELE, CRUZADA NO FIM - ver `FileReport.ownDeclared`.
|
|
520
|
+
*
|
|
521
|
+
* Uma variável é declarada num arquivo e usada em vinte, então a resposta só existe depois da
|
|
522
|
+
* varredura inteira. `ownUses` conta só o que aponta para um nome que ESTE repositório declara: um
|
|
523
|
+
* `var(--algo)` que ninguém declara continua fora da conta, porque ele não pinta nada.
|
|
524
|
+
*/
|
|
525
|
+
const ownDeclared = new Map();
|
|
526
|
+
for (const f of files)
|
|
527
|
+
for (const d of f.ownDeclared ?? [])
|
|
528
|
+
if (!ownDeclared.has(d.name))
|
|
529
|
+
ownDeclared.set(d.name, d.value);
|
|
530
|
+
const ownUses = files.reduce((n, f) => n +
|
|
531
|
+
(f.ownCandidates ?? []).reduce((m, c) => m + (ownDeclared.has(c.name) ? c.count : 0), 0), 0);
|
|
532
|
+
/**
|
|
533
|
+
* E QUANTAS DELAS O SISTEMA TAMBÉM NOMEIA - pelo VALOR, que é o que torna a adoção barata.
|
|
534
|
+
*
|
|
535
|
+
* A comparação é sobre o valor declarado e não sobre o nome: `--color-ocean-500` e
|
|
536
|
+
* `--ds-color-ocean-500` só são a mesma decisão porque os dois seguram `#059aed`.
|
|
537
|
+
*/
|
|
538
|
+
const mirrored = new Set();
|
|
539
|
+
for (const f of files)
|
|
540
|
+
for (const d of f.ownDeclared ?? [])
|
|
541
|
+
if (d.mirrored)
|
|
542
|
+
mirrored.add(d.name);
|
|
543
|
+
const ownMirrored = mirrored.size;
|
|
544
|
+
/**
|
|
545
|
+
* A COBERTURA PASSA A CONTAR A CAMADA DELE - e a decisão é do dono, em 13/08.
|
|
546
|
+
*
|
|
547
|
+
* O `--ds-*` é normalização NOSSA, para governar. Um projeto que declara o próprio vocabulário
|
|
548
|
+
* tem um design system de verdade, e medir isso como não-cobertura é dizer que o trabalho dele é
|
|
549
|
+
* deriva. A pergunta que separa governança de bagunça é literal-versus-NOME; de quem é o nome vira
|
|
550
|
+
* a linha de baixo, que diz quantos daqueles nomes o sistema também tem.
|
|
551
|
+
*/
|
|
552
|
+
const total = tokenUses + ownUses + flat.length + phantomUses;
|
|
478
553
|
const byLiteral = new Map();
|
|
479
554
|
for (const f of flat) {
|
|
480
555
|
const key = `${f.kind}:${f.literal.toLowerCase()}`;
|
|
@@ -513,7 +588,10 @@ export function diagnose(files) {
|
|
|
513
588
|
named: flat.filter((f) => f.token).length,
|
|
514
589
|
tokenUses,
|
|
515
590
|
phantomUses,
|
|
516
|
-
coverage: total === 0 ? 100 : Math.round((tokenUses / total) * 100),
|
|
591
|
+
coverage: total === 0 ? 100 : Math.round(((tokenUses + ownUses) / total) * 100),
|
|
592
|
+
ownTokens: ownDeclared.size,
|
|
593
|
+
ownUses,
|
|
594
|
+
ownMirrored,
|
|
517
595
|
scanned: files.length,
|
|
518
596
|
repeats,
|
|
519
597
|
};
|
|
@@ -16,13 +16,24 @@
|
|
|
16
16
|
* chance of a value nobody wrote: every declaration it emits either points at a
|
|
17
17
|
* token they declared or carries a literal they typed.
|
|
18
18
|
*
|
|
19
|
-
* WHY NOT `refit
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* NEAREST allowed token and never emits a raw
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
19
|
+
* WHY NOT `refit`, AND WHERE IT DOES BELONG.
|
|
20
|
+
*
|
|
21
|
+
* That endpoint already turns arbitrary component code into a token-only
|
|
22
|
+
* recipe. It maps every value to the NEAREST allowed token and never emits a raw
|
|
23
|
+
* one - which is NORMALISATION, and v1 is a mirror. That is the whole reason it
|
|
24
|
+
* is not here, and it is a reason about meaning rather than about cost: a v1
|
|
25
|
+
* that arrived already interpreted would leave the v2 with nothing to propose.
|
|
26
|
+
*
|
|
27
|
+
* Its place is exactly there, in the v2 (dono, 01/08). v1 carries their
|
|
28
|
+
* `RadioCard` with the `#ffffff` they actually typed; v2 offers the same
|
|
29
|
+
* component expressed entirely in their tokens, and they approve it. Same
|
|
30
|
+
* division as the colour collapse: mirror first, proposal second, and the person
|
|
31
|
+
* decides.
|
|
32
|
+
*
|
|
33
|
+
* An earlier version of this comment also argued that refit costs credits and
|
|
34
|
+
* carries a daily quota. That argument is retired: quotas are becoming an
|
|
35
|
+
* internal control rather than a product surface, under a 30-day-free-then-paid
|
|
36
|
+
* plan. Cost was never the real reason and should not be recorded as one.
|
|
26
37
|
*
|
|
27
38
|
* THE MODIFIER SAYS WHERE THE VALUE GOES, which is what makes this tractable:
|
|
28
39
|
*
|
package/dist/install-marks.js
CHANGED
|
@@ -64,8 +64,14 @@
|
|
|
64
64
|
* carrega - as regras de elemento da folha global dele. Elas viajavam no `design-system.json` desde
|
|
65
65
|
* sempre e o arquivo que o agente é mandado ler não as citava, então seis regras adotadas no sistema
|
|
66
66
|
* real eram invisíveis para quem ia escrever a próxima tela.
|
|
67
|
+
*
|
|
68
|
+
* 0.16.217 -> 0.16.220 em 13/08, e o SIM mais forte que esta marca já teve: o COMPONENTE que cai na
|
|
69
|
+
* pasta dele mudou. O codegen lia o estilo condicional só onde uma receita NOSSA o guarda, e uma
|
|
70
|
+
* receita LIDA guarda em `layers` - então 15 dos 19 componentes dele com eixo eram materializados sem
|
|
71
|
+
* a prop, e 104 das 207 condições sem expressão nenhuma. Um `upgrade` anterior a esta versão reescreve
|
|
72
|
+
* os componentes dele com a perda intacta.
|
|
67
73
|
*/
|
|
68
|
-
export const MATERIALISER_SINCE = "0.16.
|
|
74
|
+
export const MATERIALISER_SINCE = "0.16.220";
|
|
69
75
|
/**
|
|
70
76
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
71
77
|
*
|
|
@@ -74,7 +80,17 @@ export const MATERIALISER_SINCE = "0.16.217";
|
|
|
74
80
|
*
|
|
75
81
|
* Os arquivos que decidem: `doctor/` inteiro e `commands/hook.ts`.
|
|
76
82
|
*/
|
|
77
|
-
|
|
83
|
+
/**
|
|
84
|
+
* 0.16.202 -> 0.16.218 em 13/08, e o passo 1 foi SIM: a lente `frozen` parou de aconselhar um fundo
|
|
85
|
+
* a apontar para um papel que não é superfície.
|
|
86
|
+
*
|
|
87
|
+
* 0.16.218 -> 0.16.219 no mesmo dia, SIM de novo: a cobertura passou a contar a CAMADA DE TOKEN
|
|
88
|
+
* DELE. Um hook pinado antes disso mede o mesmo repositório e devolve outro número - e o número é o
|
|
89
|
+
* que a pessoa lê para decidir se adotou ou não. Um hook pinado numa versão anterior segue dizendo ao
|
|
90
|
+
* agente dele que o fundo do `card` deveria apontar para `foreground` - o papel do TEXTO -, e é
|
|
91
|
+
* exatamente uma verificação que o pinado faz diferente.
|
|
92
|
+
*/
|
|
93
|
+
export const CHECKER_SINCE = "0.16.219";
|
|
78
94
|
/**
|
|
79
95
|
* A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
|
|
80
96
|
*
|
package/dist/project-facts.js
CHANGED
|
@@ -143,3 +143,31 @@ export async function readInstalledConvention(root, slug) {
|
|
|
143
143
|
return DEFAULT_CONVENTION;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* WHICH SCHEME THE INSTALLED SYSTEM OPENS IN, read off the same document on disk.
|
|
148
|
+
*
|
|
149
|
+
* The twin of `readInstalledConvention`, and for the same reason: the document is
|
|
150
|
+
* the one the compiled stylesheet came from, so this can never disagree with the
|
|
151
|
+
* CSS sitting next to it.
|
|
152
|
+
*
|
|
153
|
+
* It decides whether a `scheme: dark` layer is the RESTING look or the alternative
|
|
154
|
+
* one - the whole difference between a component that arrives painted and one whose
|
|
155
|
+
* every dark value hides behind an attribute nothing sets. `"dark"` when the
|
|
156
|
+
* document cannot be read, which is what the compiler assumes when nobody says.
|
|
157
|
+
*/
|
|
158
|
+
export async function readInstalledScheme(root, slug) {
|
|
159
|
+
const dir = join(root, "_synthesisui", "ds", slug);
|
|
160
|
+
const version = await pinnedVersion(dir);
|
|
161
|
+
const raw = version
|
|
162
|
+
? await readFile(join(dir, `v${version}`, "design-system.json"), "utf8").catch(() => "")
|
|
163
|
+
: "";
|
|
164
|
+
if (!raw)
|
|
165
|
+
return "dark";
|
|
166
|
+
try {
|
|
167
|
+
const doc = JSON.parse(raw);
|
|
168
|
+
return doc.meta?.scheme === "light" ? "light" : "dark";
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return "dark";
|
|
172
|
+
}
|
|
173
|
+
}
|
package/package.json
CHANGED