synthesisui 0.16.464 → 0.16.467
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/add.js +7 -2
- package/dist/commands/hook.js +15 -0
- package/dist/commands/init.js +82 -0
- package/dist/component-codegen.js +14 -1
- package/dist/guide.js +16 -17
- package/dist/index.js +1 -0
- package/dist/install-marks.js +13 -2
- package/dist/never-measured.js +21 -0
- package/dist/new-project-theme.js +306 -0
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { neverMeasured } from "../never-measured.js";
|
|
1
2
|
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { agentsToMaintain } from "../agents-chosen.js";
|
|
@@ -347,7 +348,11 @@ export async function add(slug, opts) {
|
|
|
347
348
|
registry: base,
|
|
348
349
|
...(opts.cli ? { cli: opts.cli } : {}),
|
|
349
350
|
/** O que o `sync` aprendeu sobre a leitura não se perde no próximo `connect` (27/09). */
|
|
350
|
-
|
|
351
|
+
/**
|
|
352
|
+
* E O SISTEMA QUE NUNCA FOI MEDIDO NUM CÓDIGO já nasce assim - ver `neverMeasured` (rodada 5, 27/09):
|
|
353
|
+
* o fork do leigo-5 terminava o `connect` mandando rodar `sync` para uma pasta que não existe.
|
|
354
|
+
*/
|
|
355
|
+
...(prev?.reading === "none" || neverMeasured(payload.document) ? { reading: "none" } : {}),
|
|
351
356
|
/** Ver `RegistryPayload.compiler`: é o que faz um conserto de CSS chegar a um install. */
|
|
352
357
|
...(payload.compiler != null ? { compiler: payload.compiler } : {}),
|
|
353
358
|
...(payload.rulesStamp ? { rules: payload.rulesStamp } : {}),
|
|
@@ -586,7 +591,7 @@ export async function add(slug, opts) {
|
|
|
586
591
|
* A linha diz quantos utilitários do Tailwind ficaram apontando para a decisão dele e quantos
|
|
587
592
|
* saíram por serem nossos, com os primeiros nomes. Quem lê pode discordar de qualquer um.
|
|
588
593
|
*/
|
|
589
|
-
if (aligned.dropped.length > 0 || aligned.own.length > 0) {
|
|
594
|
+
if (!opts.themeFollows && (aligned.dropped.length > 0 || aligned.own.length > 0)) {
|
|
590
595
|
console.log(line(
|
|
591
596
|
/**
|
|
592
597
|
* A LINHA DIZ QUE FICARAM DE FORA - e nao AFIRMA por que, porque ela nao sabe.
|
package/dist/commands/hook.js
CHANGED
|
@@ -528,6 +528,21 @@ round) {
|
|
|
528
528
|
* pelo nome é o que transforma uma perda silenciosa numa lacuna declarada (lei 8).
|
|
529
529
|
*/
|
|
530
530
|
const rest = changed.slice(AT_MOST);
|
|
531
|
+
/**
|
|
532
|
+
* NO FIM DO TURNO, O REGISTRO É DO TURNO INTEIRO - rodada 5 do refinamento, 27/09.
|
|
533
|
+
*
|
|
534
|
+
* O relatório para em três, e a linha do ledger só nascia dentro dele: o agente do leigo-5 escreveu
|
|
535
|
+
* sete arquivos do dashboard num turno, o ledger ficou com três linhas `hook` e o envio com
|
|
536
|
+
* `count: 3`. O `connect` promete "every file (...) is checked at the end of each agent turn", e o
|
|
537
|
+
* painel contava três. Os demais agora são checados e registrados em silêncio; o texto continua em
|
|
538
|
+
* três, com o resto pelo nome logo abaixo.
|
|
539
|
+
*
|
|
540
|
+
* Só no turno: um comando de shell continua como era, porque ali o corte existe para um `git
|
|
541
|
+
* checkout` que muda a árvore inteira não virar trinta checagens a cada comando.
|
|
542
|
+
*/
|
|
543
|
+
if (round === "turn")
|
|
544
|
+
for (const { rel } of rest.slice(0, REFUSAL_CEILING))
|
|
545
|
+
await report(root, join(root, rel), mode).catch(() => null);
|
|
531
546
|
/**
|
|
532
547
|
* E EM CONSUMIR, O TETO DO RELATÓRIO NÃO É O TETO DA RECUSA - achado da revisão de QA no fecho,
|
|
533
548
|
* e ele falsificava a invariante inteira.
|
package/dist/commands/init.js
CHANGED
|
@@ -2,7 +2,43 @@ import { DEFAULT_CONFIG, writeProjectConfig } from "../config.js";
|
|
|
2
2
|
import { body, section, snippet } from "../output.js";
|
|
3
3
|
import { resolveDeps, stylesFor, tailwindMajor } from "../stack.js";
|
|
4
4
|
import { formMix, measuredStyle } from "../styles-flavour.js";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { readProjectTheme, writeBesideTheirs, writeNewProjectTheme } from "../new-project-theme.js";
|
|
5
8
|
import { add } from "./add.js";
|
|
9
|
+
const whereOf = (answer) => answer === "file" || answer === "1"
|
|
10
|
+
? "file"
|
|
11
|
+
: answer === "separate" || answer === "2"
|
|
12
|
+
? "separate"
|
|
13
|
+
: answer === "none" || answer === "3" || answer === ""
|
|
14
|
+
? "none"
|
|
15
|
+
: null;
|
|
16
|
+
/**
|
|
17
|
+
* A PERGUNTA REAL DO TERCEIRO CASO - dono, 27/09: *"quando o projeto já tem os tokens, a gente tem que fazer
|
|
18
|
+
* uma pergunta real"*. Só num terminal de verdade; sem ele, `null`, e aí é "em lugar nenhum". O Enter também é
|
|
19
|
+
* "em lugar nenhum": escrever os nomes do sistema no projeto dele só com o sim dele.
|
|
20
|
+
*/
|
|
21
|
+
async function askWhere(tokensFile, separate, entry, intro) {
|
|
22
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
23
|
+
return null;
|
|
24
|
+
const { createInterface } = await import("node:readline/promises");
|
|
25
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
26
|
+
try {
|
|
27
|
+
console.log("");
|
|
28
|
+
console.log(` ${intro}`);
|
|
29
|
+
console.log(` 1 in your tokens file (${tokensFile}), in a block of its own`);
|
|
30
|
+
console.log(` 2 in a file of its own, ${separate}, imported at the top of ${entry}`);
|
|
31
|
+
console.log(" 3 nowhere - keep only yours; components come with the values written");
|
|
32
|
+
const answer = (await rl.question(" [1/2/3] (3): ")).trim();
|
|
33
|
+
return whereOf(answer) ?? "none";
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
rl.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
6
42
|
/**
|
|
7
43
|
* Bootstraps a project for SynthesisUI: writes `_synthesisui/config.json`
|
|
8
44
|
* (where `template` materializes pages, where components live, which framework to
|
|
@@ -132,8 +168,54 @@ export async function init(opts) {
|
|
|
132
168
|
registry: opts.registry,
|
|
133
169
|
dir: root,
|
|
134
170
|
setupHints: false,
|
|
171
|
+
...(major !== null && major >= 4 ? { themeFollows: true } : {}),
|
|
135
172
|
...(opts.cli ? { cli: opts.cli } : {}),
|
|
136
173
|
});
|
|
174
|
+
/**
|
|
175
|
+
* O TEMA DO SISTEMA, PELO ESTADO MEDIDO DO PROJETO - ver `readProjectTheme` (rodada 4, 27/09).
|
|
176
|
+
*
|
|
177
|
+
* Só com Tailwind v4, que é quem lê `@theme`. Projeto novo recebe os nomes do sistema; projeto cujos
|
|
178
|
+
* valores já são os do sistema não recebe nada, e os componentes falam os nomes dele; projeto com outro
|
|
179
|
+
* sistema recebe a pergunta de onde o bloco mora - e sem resposta, nada é escrito.
|
|
180
|
+
*/
|
|
181
|
+
if (major !== null && major >= 4) {
|
|
182
|
+
const slugDir = join(root, "_synthesisui", "ds", opts.ds);
|
|
183
|
+
const lock = JSON.parse(await readFile(join(slugDir, ".lock"), "utf8").catch(() => "{}"));
|
|
184
|
+
const version = typeof lock.version === "number" ? lock.version : null;
|
|
185
|
+
const tokens = version !== null
|
|
186
|
+
? await readFile(join(slugDir, `v${version}`, "tokens.css"), "utf8").catch(() => "")
|
|
187
|
+
: "";
|
|
188
|
+
const plan = tokens ? await readProjectTheme(root, tokens, opts.ds) : null;
|
|
189
|
+
if (plan?.kind === "new" && version !== null) {
|
|
190
|
+
const theme = await writeNewProjectTheme(root, tokens, opts.ds, version);
|
|
191
|
+
if (theme.written) {
|
|
192
|
+
console.log("");
|
|
193
|
+
console.log(`✓ ${theme.written}: ${theme.count} names of ${opts.ds} declared in your own @theme, with the values written - your project had none of its own. They are yours: bg-accent, p-md and rounded-lg now paint the system. Fonts come from the GUIDE's next/font recipe; the alternate scheme is not wired.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else if (plan?.kind === "matches") {
|
|
197
|
+
console.log("");
|
|
198
|
+
console.log(` ${plan.matched} of ${opts.ds}'s ${plan.colours} colours are already values in your CSS, so its components speak your names - nothing was written to it.`);
|
|
199
|
+
}
|
|
200
|
+
else if (plan?.kind === "differs" && version !== null && plan.entry) {
|
|
201
|
+
const separate = `${plan.entry.replace(/[^/]*$/, "")}${opts.ds}.css`;
|
|
202
|
+
const intro = `your CSS names its own tokens, and ${plan.matched} of ${opts.ds}'s ${plan.colours} colours are among them - it is another system. Where should ${opts.ds}'s names go?`;
|
|
203
|
+
const where = whereOf(opts.themeIn) ??
|
|
204
|
+
(await askWhere(plan.tokensFile ?? plan.entry, separate, plan.entry, intro)) ??
|
|
205
|
+
"none";
|
|
206
|
+
const theme = await writeBesideTheirs(root, tokens, opts.ds, version, where);
|
|
207
|
+
console.log("");
|
|
208
|
+
if (!theme.written)
|
|
209
|
+
console.log(` ${opts.ds}'s names were not written - your CSS keeps only yours, and components come with the values written. Run init again with --theme-in file or --theme-in separate to change that.`);
|
|
210
|
+
else {
|
|
211
|
+
console.log(`✓ ${theme.written}: ${opts.ds} beside your tokens - ${theme.added} names your project did not have, and every value inside [data-theme="${opts.ds}"]. Put that attribute on the part of the page that should wear it.`);
|
|
212
|
+
if (theme.collisions.length > 0)
|
|
213
|
+
console.log(` ${theme.collisions.length} name${theme.collisions.length === 1 ? "" : "s"} you already use keep your value everywhere else: ${theme.collisions.slice(0, 5).join(", ")}${theme.collisions.length > 5 ? ", …" : ""}.`);
|
|
214
|
+
if (theme.throughInline.length > 0)
|
|
215
|
+
console.log(` declared in your @theme inline, so the selector changes the variable underneath: ${theme.throughInline.slice(0, 3).join(", ")}${theme.throughInline.length > 3 ? ", …" : ""}.`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
137
219
|
/**
|
|
138
220
|
* ERAM DOIS PASSOS E É UM - `INV-GERAL-13`, 11/09.
|
|
139
221
|
*
|
|
@@ -672,9 +672,22 @@ const preferFor = (prop) => (prop.startsWith("border") ? "border" : null);
|
|
|
672
672
|
const namedInside = (prop, value, themeVars) => {
|
|
673
673
|
if (themeVars === null)
|
|
674
674
|
return value;
|
|
675
|
-
return value
|
|
675
|
+
return value
|
|
676
|
+
.replace(/\{color\.[a-zA-Z0-9.-]+\}/g, (ref) => {
|
|
676
677
|
const key = theirColorKey(ref, themeVars, preferFor(prop));
|
|
677
678
|
return key ? `var(--color-${key})` : ref;
|
|
679
|
+
})
|
|
680
|
+
/**
|
|
681
|
+
* E A CURVA - rodada 5, 27/09: o `card` do Zephyr saía com `cubic-bezier(0.2, 0.7, 0.2, 1)` escrito
|
|
682
|
+
* dentro da `transition`, com `--ease-lumina` declarado pelo `init`. Mesma regra: o nome dele vale
|
|
683
|
+
* quando o valor bate.
|
|
684
|
+
*/
|
|
685
|
+
.replace(/\{motion\.easings\.([a-zA-Z0-9-]+)\}/g, (ref, name) => {
|
|
686
|
+
const key = kebab(name);
|
|
687
|
+
const ok = themeVars instanceof ThemeVocab
|
|
688
|
+
? themeVars.nameFor(ref, "ease", key) === key
|
|
689
|
+
: minted(themeVars, `--ease-${key}`);
|
|
690
|
+
return ok ? `var(--ease-${key})` : ref;
|
|
678
691
|
});
|
|
679
692
|
};
|
|
680
693
|
/** One declaration → Tailwind classes (pretty when mappable, arbitrary-property
|
package/dist/guide.js
CHANGED
|
@@ -461,7 +461,17 @@ ${depLines.join("\n")}
|
|
|
461
461
|
Object.keys(foundations.color.semanticAlt).length > 0;
|
|
462
462
|
const altScheme = meta.scheme === "light" ? "dark" : "light";
|
|
463
463
|
const hasTailwind = "theme.css" in payload.artifacts;
|
|
464
|
-
|
|
464
|
+
/**
|
|
465
|
+
* O EXEMPLO DE PEÇA COM PARTES SAI DE UMA QUE O SISTEMA TEM - rodada 5, 27/09. Era sempre uma tabela
|
|
466
|
+
* (`.ds-table`, `.ds-table-head`…), bastando QUALQUER peça ter partes: o Zephyr tem `streak`, `xp-bar` e
|
|
467
|
+
* `join-field`, nenhuma tabela, e o agente dele foi procurar classes que não existem. A tabela só é o
|
|
468
|
+
* exemplo quando o sistema a tem.
|
|
469
|
+
*/
|
|
470
|
+
const withParts = Object.entries(components).filter(([, r]) => r.parts && Object.keys(r.parts).length > 0);
|
|
471
|
+
const picked = withParts.find(([n]) => n === "table") ?? withParts[0];
|
|
472
|
+
const multiPart = picked
|
|
473
|
+
? { name: kebab(picked[0]), parts: Object.keys(picked[1].parts ?? {}).map(kebab) }
|
|
474
|
+
: null;
|
|
465
475
|
const componentLines = Object.entries(components).map(([cname, recipe]) => componentEntry(cname, recipe));
|
|
466
476
|
/**
|
|
467
477
|
* O CORTE SÓ ACONTECE QUANDO ELE DE FATO CORTA - e este é o único jeito honesto de prometê-lo.
|
|
@@ -739,26 +749,15 @@ The system defines the scale; these are sensible defaults for spending it:
|
|
|
739
749
|
- **Section gaps:** \`lg\` (or the nearest large step). **Card/panel padding:** \`md\`.
|
|
740
750
|
- **Field / tight gaps:** \`2xs\`/\`3xs\`.
|
|
741
751
|
- The system imposes no content max-width - cap long-form/text columns yourself for readability.
|
|
742
|
-
${
|
|
752
|
+
${multiPart
|
|
743
753
|
? `
|
|
744
754
|
### Multi-part components
|
|
745
755
|
Components that have **parts** compile to \`.ds-<name>-<part>\` classes you nest yourself; the exact
|
|
746
|
-
part classes and their \`data-*\` are listed per component below. Example -
|
|
756
|
+
part classes and their \`data-*\` are listed per component below. Example - ${multiPart.name}:
|
|
747
757
|
\`\`\`tsx
|
|
748
|
-
<
|
|
749
|
-
<
|
|
750
|
-
|
|
751
|
-
<th className="ds-table-cell-head">Name</th>
|
|
752
|
-
<th className="ds-table-cell-head" data-align="end">Updated</th>
|
|
753
|
-
</tr>
|
|
754
|
-
</thead>
|
|
755
|
-
<tbody>
|
|
756
|
-
<tr className="ds-table-row">
|
|
757
|
-
<td className="ds-table-cell">Halogen</td>
|
|
758
|
-
<td className="ds-table-cell" data-align="end">2h ago</td>
|
|
759
|
-
</tr>
|
|
760
|
-
</tbody>
|
|
761
|
-
</table>
|
|
758
|
+
<div className="ds-${multiPart.name}">
|
|
759
|
+
${multiPart.parts.map((p) => ` <div className="ds-${multiPart.name}-${p}">…</div>`).join("\n")}
|
|
760
|
+
</div>
|
|
762
761
|
\`\`\`
|
|
763
762
|
`
|
|
764
763
|
: ""}
|
package/dist/index.js
CHANGED
package/dist/install-marks.js
CHANGED
|
@@ -275,7 +275,13 @@
|
|
|
275
275
|
* `--color-accent` e `--color-primary` declarados - e a desempatar pela palavra da propriedade numa borda
|
|
276
276
|
* (rodada 4 do refinamento).
|
|
277
277
|
*/
|
|
278
|
-
|
|
278
|
+
/**
|
|
279
|
+
* 0.16.464 -> 0.16.467 em 27/09, e o passo 1 dá **SIM**: o gerador que o `upgrade` usa passa a escrever a
|
|
280
|
+
* curva de uma `transition` pelo nome dele (`var(--ease-lumina)`), o `GUIDE.md` deixa de ensinar uma tabela
|
|
281
|
+
* que o sistema não tem, e o `.lock` de um sistema nunca medido num código nasce com `reading: "none"` -
|
|
282
|
+
* rodada 5 do refinamento. Uma pasta escrita antes ensina `.ds-table` ao agente.
|
|
283
|
+
*/
|
|
284
|
+
export const MATERIALISER_SINCE = "0.16.467";
|
|
279
285
|
/**
|
|
280
286
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
281
287
|
*
|
|
@@ -437,7 +443,12 @@ export const COUNTED_DIFFERENTLY = "this run counts a value as named only when Y
|
|
|
437
443
|
* rodada 3 do refinamento: o dashboard do leigo-3 inteiro nunca foi checado. Um hook anterior segue calado
|
|
438
444
|
* nos dois casos.
|
|
439
445
|
*/
|
|
440
|
-
|
|
446
|
+
/**
|
|
447
|
+
* 0.16.463 -> 0.16.467 em 27/09, e o passo 1 dá **SIM**: o fim do turno passa a registrar no ledger TODOS os
|
|
448
|
+
* arquivos do turno, e não só os três que relata - rodada 5: o leigo-5 escreveu sete e o painel recebeu
|
|
449
|
+
* três. Um hook anterior segue mandando ao painel um turno pela metade.
|
|
450
|
+
*/
|
|
451
|
+
export const CHECKER_SINCE = "0.16.467";
|
|
441
452
|
/**
|
|
442
453
|
* A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
|
|
443
454
|
*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O SISTEMA NUNCA FOI MEDIDO NUM CÓDIGO - rodada 5 do refinamento, 27/09.
|
|
3
|
+
*
|
|
4
|
+
* O `connect` do leigo-5 terminou com "Still out of alignment: \"zephyr\" does not record where it was
|
|
5
|
+
* measured (...) npx synthesisui sync". O aviso só se calava com `reading: "none"` no `.lock`, e isso só
|
|
6
|
+
* era gravado depois de um `sync` que perguntava. Só que o documento já responde: cada fundação carrega
|
|
7
|
+
* quem a escreveu (`foundations.source`, os `FoundationAuthor` da plataforma), e num fork nascido na
|
|
8
|
+
* plataforma todas são `seed`. Não existe pasta de onde ele veio, e não há o que remedir.
|
|
9
|
+
*
|
|
10
|
+
* OS AUTORES QUE LERAM UM CÓDIGO são `import`, `sync`, `reground` e `reinterpret`. Um deles basta para o
|
|
11
|
+
* sistema ter origem num repositório, e aí a pergunta do `sync` continua valendo. Sem `source` nenhum, o
|
|
12
|
+
* documento não diz - e o silêncio não vira afirmação.
|
|
13
|
+
*/
|
|
14
|
+
const FROM_CODE = new Set(["import", "sync", "reground", "reinterpret"]);
|
|
15
|
+
export function neverMeasured(doc) {
|
|
16
|
+
const source = doc?.foundations?.source;
|
|
17
|
+
if (!source || typeof source !== "object")
|
|
18
|
+
return false;
|
|
19
|
+
const authors = Object.values(source);
|
|
20
|
+
return authors.length > 0 && !authors.some((a) => FROM_CODE.has(String(a)));
|
|
21
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
|
+
import { tongueOf } from "./their-tongue.js";
|
|
4
|
+
/** O que o `create-next-app` escreve - não conta como vocabulário DELE. */
|
|
5
|
+
const BOILERPLATE = new Set([
|
|
6
|
+
"--background",
|
|
7
|
+
"--foreground",
|
|
8
|
+
"--color-background",
|
|
9
|
+
"--color-foreground",
|
|
10
|
+
"--font-sans",
|
|
11
|
+
"--font-mono",
|
|
12
|
+
"--font-geist-sans",
|
|
13
|
+
"--font-geist-mono",
|
|
14
|
+
]);
|
|
15
|
+
const SKIP = new Set(["node_modules", ".next", "dist", "build", "out", "_synthesisui", "coverage"]);
|
|
16
|
+
const TAILWIND_IMPORT = /@import\s+[\x22\x27]tailwindcss[\x22\x27]/;
|
|
17
|
+
/**
|
|
18
|
+
* O COMENTÁRIO DO CSS SE ESCREVE COM `\u002a` NO FONTE, e não com o asterisco - a guarda de
|
|
19
|
+
* `nothing-of-ours-is-asked-for.spec.ts` apaga comentários do fonte antes de ler os literais, e um `/*`
|
|
20
|
+
* dentro de uma string a fazia comer o arquivo até o próximo fecho, deixando aspas soltas (a varredura
|
|
21
|
+
* saía de 1,9s para mais de 25s). O texto que chega ao CSS dele é o mesmo.
|
|
22
|
+
*/
|
|
23
|
+
const open = (text) => `/\u002a ${text} \u002a/`;
|
|
24
|
+
const begin = (slug) => open(`synthesisui: the ${slug} theme, in your names - begin`);
|
|
25
|
+
const end = (slug) => open(`synthesisui: the ${slug} theme - end`);
|
|
26
|
+
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
27
|
+
const END_RE = (slug) => new RegExp(`\\n*${escape(begin(slug))}[\\s\\S]*?${escape(end(slug))}\\n?`);
|
|
28
|
+
async function sheetsOf(root) {
|
|
29
|
+
const out = new Map();
|
|
30
|
+
const walk = async (dir) => {
|
|
31
|
+
for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
32
|
+
if (entry.name.startsWith(".") || SKIP.has(entry.name))
|
|
33
|
+
continue;
|
|
34
|
+
const path = join(dir, entry.name);
|
|
35
|
+
if (entry.isDirectory())
|
|
36
|
+
await walk(path);
|
|
37
|
+
else if (/\.css$/i.test(entry.name))
|
|
38
|
+
out.set(path, await readFile(path, "utf8").catch(() => ""));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
await walk(root);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** O nome DELE para uma variável nossa, no namespace que o Tailwind v4 lê - ou `null`. */
|
|
45
|
+
function theirNameFor(ours) {
|
|
46
|
+
const bare = ours.slice("--ds-".length);
|
|
47
|
+
let m;
|
|
48
|
+
if ((m = /^color-semantic-(.+)$/.exec(bare)))
|
|
49
|
+
return `--color-${m[1]}`;
|
|
50
|
+
if ((m = /^color-series-(.+)$/.exec(bare)))
|
|
51
|
+
return `--color-series-${m[1]}`;
|
|
52
|
+
if ((m = /^color-(.+)$/.exec(bare)))
|
|
53
|
+
return `--color-${m[1]}`;
|
|
54
|
+
if ((m = /^(spacing|radius|shadow)-(.+)$/.exec(bare)))
|
|
55
|
+
return `--${m[1]}-${m[2]}`;
|
|
56
|
+
if ((m = /^typography-weights-(.+)$/.exec(bare)))
|
|
57
|
+
return `--font-weight-${m[1]}`;
|
|
58
|
+
if ((m = /^typography-scale-(.+)-font-size$/.exec(bare)))
|
|
59
|
+
return `--text-${m[1]}`;
|
|
60
|
+
if ((m = /^typography-scale-(.+)-(line-height|letter-spacing)$/.exec(bare)))
|
|
61
|
+
return `--text-${m[1]}--${m[2]}`;
|
|
62
|
+
if ((m = /^motion-easings-(.+)$/.exec(bare)))
|
|
63
|
+
return `--ease-${m[1]}`;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
export async function writeNewProjectTheme(root, tokensCss, slug, version) {
|
|
67
|
+
const sheets = await sheetsOf(root);
|
|
68
|
+
const own = new Set();
|
|
69
|
+
for (const css of sheets.values()) {
|
|
70
|
+
const withoutOurs = css.replace(END_RE(slug), "\n");
|
|
71
|
+
for (const m of withoutOurs.matchAll(/(?:^|[{;\s])(--[a-zA-Z0-9_-]+)\s*:/g)) {
|
|
72
|
+
const name = m[1];
|
|
73
|
+
if (!name.startsWith("--ds-") && !name.startsWith("--tw-") && !BOILERPLATE.has(name))
|
|
74
|
+
own.add(name);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (own.size > 0)
|
|
78
|
+
return { written: null, count: 0, reason: "own-vocabulary" };
|
|
79
|
+
const entries = [...sheets].filter(([, css]) => TAILWIND_IMPORT.test(css));
|
|
80
|
+
if (entries.length === 0)
|
|
81
|
+
return { written: null, count: 0, reason: "no-tailwind-entry" };
|
|
82
|
+
if (entries.length > 1)
|
|
83
|
+
return { written: null, count: 0, reason: "many-tailwind-entries" };
|
|
84
|
+
const [path, css] = entries[0];
|
|
85
|
+
const system = new Map(systemNames(tokensCss, (await readTheirs(root, slug)).every));
|
|
86
|
+
const lines = [...system].map(([n, v]) => ` ${n}: ${v};`);
|
|
87
|
+
if (lines.length === 0)
|
|
88
|
+
return { written: null, count: 0, reason: "empty-sheet" };
|
|
89
|
+
const next = [];
|
|
90
|
+
const canvas = system.get("--color-canvas") ?? system.get("--color-surface");
|
|
91
|
+
const text = system.get("--color-foreground");
|
|
92
|
+
if (canvas && /--background\s*:/.test(css))
|
|
93
|
+
next.push(` --background: ${canvas};`);
|
|
94
|
+
if (text && /--foreground\s*:/.test(css))
|
|
95
|
+
next.push(` --foreground: ${text};`);
|
|
96
|
+
const block = [
|
|
97
|
+
begin(slug),
|
|
98
|
+
open(`${slug} v${version}: the system's values under the names Tailwind turns into utilities. They are yours now - change a value here and every component follows. Written by \`init\`, because this project had no tokens of its own.`),
|
|
99
|
+
"@theme {",
|
|
100
|
+
...lines,
|
|
101
|
+
"}",
|
|
102
|
+
...(next.length > 0 ? ["", ":root {", ...next, "}"] : []),
|
|
103
|
+
end(slug),
|
|
104
|
+
].join("\n");
|
|
105
|
+
const base = css.replace(END_RE(slug), "\n").replace(/\n*$/, "\n");
|
|
106
|
+
await writeFile(path, `${base}\n${block}\n`, "utf8");
|
|
107
|
+
return { written: relative(root, path).split("\\").join("/"), count: lines.length };
|
|
108
|
+
}
|
|
109
|
+
const importLine = (slug) => `@import "./${slug}.css";`;
|
|
110
|
+
/** O CSS dele sem nada nosso: sem o bloco e sem a linha que importa o arquivo à parte. */
|
|
111
|
+
const withoutOurs = (css, slug) => css.replace(END_RE(slug), "\n").replace(new RegExp(`^${escape(importLine(slug))}\\n`, "m"), "");
|
|
112
|
+
async function readTheirs(root, slug) {
|
|
113
|
+
const sheets = await sheetsOf(root);
|
|
114
|
+
const raw = new Map();
|
|
115
|
+
const own = new Set();
|
|
116
|
+
const inline = new Map();
|
|
117
|
+
const counts = new Map();
|
|
118
|
+
for (const [path, full] of sheets) {
|
|
119
|
+
if (full.startsWith(begin(slug)))
|
|
120
|
+
continue;
|
|
121
|
+
const css = withoutOurs(full, slug);
|
|
122
|
+
let n = 0;
|
|
123
|
+
for (const m of css.matchAll(/(?:^|[{;\s])(--[a-zA-Z0-9_-]+)\s*:\s*([^;}]+)/g)) {
|
|
124
|
+
const name = m[1];
|
|
125
|
+
if (name.startsWith("--ds-") || name.startsWith("--tw-"))
|
|
126
|
+
continue;
|
|
127
|
+
if (!raw.has(name))
|
|
128
|
+
raw.set(name, m[2].trim());
|
|
129
|
+
if (!BOILERPLATE.has(name)) {
|
|
130
|
+
own.add(name);
|
|
131
|
+
n += 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
counts.set(path, n);
|
|
135
|
+
for (const block of css.matchAll(/@theme\s+inline\s*\{([^}]*)\}/g))
|
|
136
|
+
for (const m of block[1].matchAll(/(--[a-zA-Z0-9_-]+)\s*:\s*var\(\s*(--[a-zA-Z0-9_-]+)\s*\)/g))
|
|
137
|
+
inline.set(m[1], m[2]);
|
|
138
|
+
}
|
|
139
|
+
const resolved = (table) => {
|
|
140
|
+
const out = new Map();
|
|
141
|
+
for (const name of table.keys()) {
|
|
142
|
+
const seen = new Set();
|
|
143
|
+
let value = table.get(name);
|
|
144
|
+
for (let ref = /^var\(\s*(--[a-zA-Z0-9_-]+)\s*\)$/.exec(value); ref; ref = /^var\(\s*(--[a-zA-Z0-9_-]+)\s*\)$/.exec(value)) {
|
|
145
|
+
const next = table.get(ref[1]);
|
|
146
|
+
if (next === undefined || seen.has(ref[1]))
|
|
147
|
+
break;
|
|
148
|
+
seen.add(ref[1]);
|
|
149
|
+
value = next;
|
|
150
|
+
}
|
|
151
|
+
out.set(name, value);
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
};
|
|
155
|
+
const values = resolved(raw);
|
|
156
|
+
const everyRaw = new Map(raw);
|
|
157
|
+
for (const full of sheets.values())
|
|
158
|
+
for (const m of full.matchAll(/(?:^|[{;\s])(--[a-zA-Z0-9_-]+)\s*:\s*([^;}]+)/g))
|
|
159
|
+
if (!everyRaw.has(m[1]) && !m[1].startsWith("--ds-"))
|
|
160
|
+
everyRaw.set(m[1], m[2].trim());
|
|
161
|
+
const every = resolved(everyRaw);
|
|
162
|
+
const entries = [...sheets].filter(([, css]) => TAILWIND_IMPORT.test(css)).map(([p]) => p);
|
|
163
|
+
const rel = (p) => relative(root, p).split("\\").join("/");
|
|
164
|
+
const top = [...counts].sort((a, b) => b[1] - a[1])[0];
|
|
165
|
+
const entry = entries.length === 1 ? entries[0] : null;
|
|
166
|
+
return {
|
|
167
|
+
sheets,
|
|
168
|
+
values,
|
|
169
|
+
own,
|
|
170
|
+
inline,
|
|
171
|
+
every,
|
|
172
|
+
entry: entry ? rel(entry) : null,
|
|
173
|
+
entries: entries.length,
|
|
174
|
+
tokensFile: top && top[1] > 0 ? rel(top[0]) : entry ? rel(entry) : null,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Os nomes do sistema no namespace do Tailwind, com o valor - a mesma tabela do projeto novo.
|
|
179
|
+
*
|
|
180
|
+
* `lookup` responde o valor de um nome DO PROJETO, com o nosso bloco anterior incluído. Depois da primeira
|
|
181
|
+
* rodada existe build, e o `add` seguinte reescreve a folha para apontar para os nomes que o NOSSO bloco
|
|
182
|
+
* criou (`--ds-color-amber-400: var(--color-amber-400)`). Pular essas linhas encolhia o bloco na segunda
|
|
183
|
+
* rodada: medido na prova de 27/09, 82 nomes viraram 38.
|
|
184
|
+
*/
|
|
185
|
+
function systemNames(tokensCss, lookup = new Map()) {
|
|
186
|
+
const { names, values } = tongueOf([], tokensCss);
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const ours of new Set([...values.keys(), ...names.keys()])) {
|
|
189
|
+
const theirs = theirNameFor(ours);
|
|
190
|
+
if (!theirs)
|
|
191
|
+
continue;
|
|
192
|
+
const pointed = names.get(ours);
|
|
193
|
+
const value = pointed ? lookup.get(pointed) : values.get(ours);
|
|
194
|
+
if (value && !value.includes("var(--ds-"))
|
|
195
|
+
out.push([theirs, value]);
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
const same = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
200
|
+
export async function readProjectTheme(root, tokensCss, slug) {
|
|
201
|
+
const r = await readTheirs(root, slug);
|
|
202
|
+
const base = { entry: r.entry, tokensFile: r.tokensFile };
|
|
203
|
+
const colourValues = [
|
|
204
|
+
...new Set(systemNames(tokensCss, r.every).filter(([n]) => n.startsWith("--color-")).map(([, v]) => v.toLowerCase())),
|
|
205
|
+
];
|
|
206
|
+
const held = new Set([...r.values.values()].map((v) => v.trim().toLowerCase()));
|
|
207
|
+
const matched = colourValues.filter((v) => held.has(v)).length;
|
|
208
|
+
const counts = { matched, colours: colourValues.length };
|
|
209
|
+
if (r.entries === 0)
|
|
210
|
+
return { kind: "no-tailwind-entry", ...counts, ...base };
|
|
211
|
+
if (r.entries > 1)
|
|
212
|
+
return { kind: "many-tailwind-entries", ...counts, ...base };
|
|
213
|
+
if (r.own.size === 0)
|
|
214
|
+
return { kind: "new", ...counts, ...base };
|
|
215
|
+
return { kind: matched * 2 >= colourValues.length && matched > 0 ? "matches" : "differs", ...counts, ...base };
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* O BLOCO DO SISTEMA AO LADO DO DELE - o terceiro caso.
|
|
219
|
+
*
|
|
220
|
+
* O MESMO BLOCO, onde quer que ele more: um `@theme` com os nomes do sistema que ele NÃO declara (as
|
|
221
|
+
* classes passam a existir; as dele continuam significando o mesmo) e `[data-theme="<slug>"]` com todos
|
|
222
|
+
* os valores, inclusive os dos nomes que colidem - o `--color-primary` dele vale no site inteiro, e só
|
|
223
|
+
* dentro do seletor vira o do sistema. No Tailwind v4 a classe de um `@theme` comum lê a variável em
|
|
224
|
+
* tempo de execução (`.bg-canvas{background-color:var(--color-canvas)}`, medido no build de 27/09), e é
|
|
225
|
+
* isso que deixa o seletor trocar o valor.
|
|
226
|
+
*
|
|
227
|
+
* O LIMITE, que o comando diz: um nome que ele declara em `@theme inline` tem o valor colado na classe, e
|
|
228
|
+
* o seletor não o alcança. Ali o bloco troca a variável de baixo (`--color-foreground` -> `--foreground`).
|
|
229
|
+
*
|
|
230
|
+
* `where`: `file` no arquivo de tokens dele, `separate` num `<slug>.css` ao lado do CSS de entrada com o
|
|
231
|
+
* `@import` no topo, `none` em lugar nenhum. Rodar de novo MOVE o bloco: o que estava no outro lugar sai.
|
|
232
|
+
*/
|
|
233
|
+
export async function writeBesideTheirs(root, tokensCss, slug, version, where) {
|
|
234
|
+
const r = await readTheirs(root, slug);
|
|
235
|
+
const entryPath = r.entry ? join(root, r.entry) : null;
|
|
236
|
+
const separatePath = entryPath ? join(dirname(entryPath), `${slug}.css`) : null;
|
|
237
|
+
/** Tira de todo lugar o que é nosso, antes de escrever no lugar escolhido. */
|
|
238
|
+
const cleaned = new Map();
|
|
239
|
+
for (const [path, css] of r.sheets) {
|
|
240
|
+
if (path === separatePath && css.startsWith(begin(slug)))
|
|
241
|
+
continue;
|
|
242
|
+
cleaned.set(path, withoutOurs(css, slug));
|
|
243
|
+
}
|
|
244
|
+
const flush = async () => {
|
|
245
|
+
for (const [path, css] of cleaned)
|
|
246
|
+
if (css !== r.sheets.get(path))
|
|
247
|
+
await writeFile(path, css, "utf8");
|
|
248
|
+
if (separatePath && r.sheets.get(separatePath)?.startsWith(begin(slug)) && where !== "separate")
|
|
249
|
+
await rm(separatePath, { force: true });
|
|
250
|
+
};
|
|
251
|
+
const empty = { written: null, collisions: [], throughInline: [], added: 0 };
|
|
252
|
+
if (where === "none" || !entryPath || !separatePath) {
|
|
253
|
+
await flush();
|
|
254
|
+
return empty;
|
|
255
|
+
}
|
|
256
|
+
const system = systemNames(tokensCss, r.every);
|
|
257
|
+
if (system.length === 0) {
|
|
258
|
+
await flush();
|
|
259
|
+
return empty;
|
|
260
|
+
}
|
|
261
|
+
const missing = system.filter(([n]) => !r.own.has(n) && !r.values.has(n));
|
|
262
|
+
const collisions = system
|
|
263
|
+
.filter(([n, v]) => r.values.has(n) && !r.inline.has(n) && !same(r.values.get(n), v))
|
|
264
|
+
.map(([n]) => n);
|
|
265
|
+
const throughInline = [];
|
|
266
|
+
const scoped = system.map(([n, v]) => {
|
|
267
|
+
const under = r.inline.get(n);
|
|
268
|
+
if (under) {
|
|
269
|
+
throughInline.push(`${n} -> ${under}`);
|
|
270
|
+
return ` ${under}: ${v};`;
|
|
271
|
+
}
|
|
272
|
+
return ` ${n}: ${v};`;
|
|
273
|
+
});
|
|
274
|
+
const block = [
|
|
275
|
+
begin(slug),
|
|
276
|
+
open(`${slug} v${version}, beside your own tokens. The @theme adds only the names your project did not have; inside the data-theme selector below every value is the system's, including the names you already use, which keep their value everywhere else. Written by \`init\`, where you chose.`),
|
|
277
|
+
...(missing.length > 0 ? ["@theme {", ...missing.map(([n, v]) => ` ${n}: ${v};`), "}", ""] : []),
|
|
278
|
+
`[data-theme="${slug}"] {`,
|
|
279
|
+
...scoped,
|
|
280
|
+
"}",
|
|
281
|
+
end(slug),
|
|
282
|
+
].join("\n");
|
|
283
|
+
let written;
|
|
284
|
+
if (where === "separate") {
|
|
285
|
+
const entry = cleaned.get(entryPath) ?? "";
|
|
286
|
+
const m = TAILWIND_IMPORT.exec(entry);
|
|
287
|
+
const at = m ? entry.indexOf("\n", m.index) + 1 : 0;
|
|
288
|
+
cleaned.set(entryPath, `${entry.slice(0, at)}${importLine(slug)}\n${entry.slice(at)}`);
|
|
289
|
+
await flush();
|
|
290
|
+
await writeFile(separatePath, `${block}\n`, "utf8");
|
|
291
|
+
written = relative(root, separatePath);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
const target = join(root, r.tokensFile ?? r.entry);
|
|
295
|
+
const css = (cleaned.get(target) ?? "").replace(/\n*$/, "\n");
|
|
296
|
+
cleaned.set(target, `${css}\n${block}\n`);
|
|
297
|
+
await flush();
|
|
298
|
+
written = relative(root, target);
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
written: written.split("\\").join("/"),
|
|
302
|
+
collisions,
|
|
303
|
+
throughInline,
|
|
304
|
+
added: missing.length,
|
|
305
|
+
};
|
|
306
|
+
}
|
package/package.json
CHANGED