synthesisui 0.16.219 → 0.16.222
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/align.js +31 -0
- package/dist/commands/component.js +2 -2
- package/dist/commands/connect.js +12 -18
- package/dist/commands/doctor.js +15 -1
- package/dist/commands/generate.js +2 -2
- package/dist/commands/mcp.js +41 -0
- package/dist/commands/refit.js +2 -2
- package/dist/commands/request.js +20 -5
- package/dist/commands/upgrade.js +2 -2
- package/dist/component-codegen.js +305 -20
- package/dist/doctor/requests.js +10 -1
- package/dist/install-marks.js +7 -1
- package/dist/project-facts.js +28 -0
- package/dist/skill-adapt.js +195 -0
- package/dist/skills.js +39 -0
- package/package.json +1 -1
package/dist/commands/align.js
CHANGED
|
@@ -7,6 +7,7 @@ import { readToken, resolveRegistry } from "../config.js";
|
|
|
7
7
|
import { unsentEvents } from "../doctor/ledger.js";
|
|
8
8
|
import { CHECKER_SINCE, installedBehind, MATERIALISER_SINCE, READER_SINCE, } from "../install-marks.js";
|
|
9
9
|
import { measuredScope } from "../measured-scope.js";
|
|
10
|
+
import { SKILLS } from "../skills.js";
|
|
10
11
|
/**
|
|
11
12
|
* QUAL CLI MEDIU O CENSO EM DISCO - e era o `reader`, um inteiro, até 11/08.
|
|
12
13
|
*
|
|
@@ -150,6 +151,36 @@ opts = {}) {
|
|
|
150
151
|
*/
|
|
151
152
|
run: "npx synthesisui sync",
|
|
152
153
|
});
|
|
154
|
+
/**
|
|
155
|
+
* A SKILL QUE ESTE CLI TEM E ESTE REPO NÃO - e sem isto ela nunca chegaria.
|
|
156
|
+
*
|
|
157
|
+
* `connect` é idempotente e atualiza sozinho, mas ninguém roda um comando de novo sem motivo: uma
|
|
158
|
+
* skill nova ficava esperando o acaso. Aqui ela vira uma linha do alinho, que é a única coisa que
|
|
159
|
+
* fala com a pessoa sem ela pedir.
|
|
160
|
+
*
|
|
161
|
+
* Compara o CONTEÚDO e não só a existência: uma skill velha descreve um fluxo que este CLI já não
|
|
162
|
+
* tem, e isso é pior que não ter skill nenhuma - quem lê não tem como perceber.
|
|
163
|
+
*/
|
|
164
|
+
const installedSkills = await Promise.all(SKILLS.map((skill) => readFile(join(root, skill.path), "utf8").catch(() => null)));
|
|
165
|
+
/**
|
|
166
|
+
* E SÓ FALA COM QUEM JÁ TEM ALGUMA - senão isto vira o alarme que ensina uma palavra nova a quem
|
|
167
|
+
* não pediu.
|
|
168
|
+
*
|
|
169
|
+
* Um repositório sem skill nenhuma nunca ligou o agente, e pode nem usar Claude Code. Dizer a essa
|
|
170
|
+
* pessoa que "uma skill está faltando" é nomear uma ausência que ela escolheu, e `align` é a única
|
|
171
|
+
* superfície que fala sem ser chamada - a regra dela é ficar calada no estado saudável.
|
|
172
|
+
*
|
|
173
|
+
* Com uma instalada, o silêncio passa a ser o erro: ela ligou o agente, e uma skill nova ficaria
|
|
174
|
+
* esperando o acaso de alguém rodar `connect` de novo.
|
|
175
|
+
*/
|
|
176
|
+
const staleSkills = SKILLS.filter((skill, i) => installedSkills[i] !== skill.source).map((skill) => skill.label);
|
|
177
|
+
if (installedSkills.some((have) => have != null) && staleSkills.length > 0)
|
|
178
|
+
out.push({
|
|
179
|
+
says: staleSkills.length === 1
|
|
180
|
+
? `${staleSkills[0]} is missing or older than this CLI - it is a skill your agent can invoke, and it is not here.`
|
|
181
|
+
: `${staleSkills.length} skills are missing or older than this CLI (${staleSkills.join(", ")}) - your agent can invoke them, and they are not here.`,
|
|
182
|
+
run: "npx synthesisui connect",
|
|
183
|
+
});
|
|
153
184
|
/**
|
|
154
185
|
* SÓ O QUE NÃO SUBIU - ver `unsentEvents`. Isto contava o arquivo inteiro, e como o ledger é
|
|
155
186
|
* append-only e o `sync` manda tudo (a plataforma deduplica), a linha nunca mais saía da tela e o
|
|
@@ -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/connect.js
CHANGED
|
@@ -6,8 +6,7 @@ import { resolveRegistry } from "../config.js";
|
|
|
6
6
|
import { body, paint, section, snippet } from "../output.js";
|
|
7
7
|
import { readShellAnswer, rememberShellNo } from "../shell-answer.js";
|
|
8
8
|
import { existingRc, hasHook, pinnedInHook, rcPathFor, shellFrom, shellSnippet, withHook, } from "../shell-hook.js";
|
|
9
|
-
import {
|
|
10
|
-
import { INIT_SKILL, INIT_SKILL_PATH } from "../skill-init.js";
|
|
9
|
+
import { SKILLS } from "../skills.js";
|
|
11
10
|
import { add } from "./add.js";
|
|
12
11
|
import { reportWhatIsLeft } from "./align.js";
|
|
13
12
|
import { ci } from "./ci.js";
|
|
@@ -293,24 +292,19 @@ export async function connect(opts) {
|
|
|
293
292
|
* something change" - so nobody has to remember a second command.
|
|
294
293
|
*/
|
|
295
294
|
/**
|
|
296
|
-
* AS
|
|
295
|
+
* AS TRÊS SKILLS, e o `/sui-init` é a que importa mais aqui: é a PRIMEIRA CORRIDA, e uma skill que
|
|
297
296
|
* só aparece depois de reiniciar o editor não serve para a primeira corrida de ninguém. Ela vem no
|
|
298
|
-
* mesmo `connect` que ainda vai pedir o reinício - então quando a pessoa reabre, as
|
|
297
|
+
* mesmo `connect` que ainda vai pedir o reinício - então quando a pessoa reabre, as três existem.
|
|
298
|
+
*
|
|
299
|
+
* A TERCEIRA É DE MANUTENÇÃO, e ela é de outra natureza: as duas primeiras são de ENTRADA e rodam
|
|
300
|
+
* uma vez. `/sui-adapt` responde a pergunta do dia seguinte - *"isso aqui está de acordo com o meu
|
|
301
|
+
* design system?"* -, apontando para um componente, toda semana ou depois de mexer em alguma coisa.
|
|
302
|
+
*
|
|
303
|
+
* Ela quase ficou de fora daqui, e teria sido o erro de sempre: uma skill que só existe no NOSSO
|
|
304
|
+
* repositório é uma skill que só nós rodamos, e o caso de uso inteiro dela é o cliente rodando
|
|
305
|
+
* sozinho. Meia jornada não é meio valor.
|
|
299
306
|
*/
|
|
300
|
-
const skills =
|
|
301
|
-
{
|
|
302
|
-
path: INIT_SKILL_PATH,
|
|
303
|
-
source: INIT_SKILL,
|
|
304
|
-
label: "/sui-init",
|
|
305
|
-
what: "the first run, start to finish",
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
path: IMPORT_SKILL_PATH,
|
|
309
|
-
source: IMPORT_SKILL,
|
|
310
|
-
label: "/sui-import-ds",
|
|
311
|
-
what: "the import, orchestrated",
|
|
312
|
-
},
|
|
313
|
-
];
|
|
307
|
+
const skills = SKILLS;
|
|
314
308
|
/**
|
|
315
309
|
* A PASTA VELHA SAI, e isto é obrigatório numa renomeação de skill distribuída.
|
|
316
310
|
*
|
package/dist/commands/doctor.js
CHANGED
|
@@ -130,6 +130,20 @@ export async function* walkAll(roots) {
|
|
|
130
130
|
* and the recipes `add` put next to them in `design-system.json`. Those
|
|
131
131
|
* recipes are why the component pass can exist at all - a linter has no idea
|
|
132
132
|
* what `ds-button` promised. */
|
|
133
|
+
/**
|
|
134
|
+
* COMO CADA PEDIDO SE LÊ NUMA LINHA - e o `else` desta expressão mentia.
|
|
135
|
+
*
|
|
136
|
+
* Ela dizia `component "<nome>"` para tudo que não fosse token, então um pedido de REGRA aparecia no
|
|
137
|
+
* doctor como se fosse um componente pedido. Uma tela que rotula errado é pior que uma que não
|
|
138
|
+
* rotula: quem lê decide em cima do rótulo.
|
|
139
|
+
*/
|
|
140
|
+
function requestLabel(r) {
|
|
141
|
+
if (r.kind === "token")
|
|
142
|
+
return `token ${r.value} as ${r.name}`;
|
|
143
|
+
if (r.kind === "rule")
|
|
144
|
+
return `rule "${r.name}"`;
|
|
145
|
+
return `component "${r.name}"`;
|
|
146
|
+
}
|
|
133
147
|
export async function loadSystem(root) {
|
|
134
148
|
const dsDir = join(root, "_synthesisui", "ds");
|
|
135
149
|
let slugs;
|
|
@@ -941,7 +955,7 @@ export async function doctor(opts) {
|
|
|
941
955
|
console.log("");
|
|
942
956
|
console.log(section("What your agent asked for"));
|
|
943
957
|
for (const r of requests.slice(0, 8)) {
|
|
944
|
-
console.log(body(` ${r.id} ${r
|
|
958
|
+
console.log(body(` ${r.id} ${requestLabel(r)}${r.area === "platform" ? " [platform]" : ""}`));
|
|
945
959
|
console.log(body(` for: ${r.purpose}`));
|
|
946
960
|
if (r.considered)
|
|
947
961
|
console.log(body(` considered: ${r.considered}`));
|
|
@@ -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/mcp.js
CHANGED
|
@@ -200,6 +200,29 @@ const TOOLS = [
|
|
|
200
200
|
required: ["value", "name", "purpose"],
|
|
201
201
|
},
|
|
202
202
|
},
|
|
203
|
+
{
|
|
204
|
+
name: "request_rule",
|
|
205
|
+
description: "File a rule request when this component does something the system's doctrine does not cover, and you had to decide alone. Read `system_doctrine` first: if a rule already answers it, follow it instead of filing. This is for the case with no rule - what the rule would say, and the case that asked for it. Do NOT invent a convention and move on; a decision that lives only in one file is not a decision of the system.",
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
name: {
|
|
210
|
+
type: "string",
|
|
211
|
+
description: "What the rule would say, in one line: 'a card that opens a dialog carries the trigger, never the panel'",
|
|
212
|
+
},
|
|
213
|
+
purpose: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "The case that asked for it - what you were building when no rule answered.",
|
|
216
|
+
},
|
|
217
|
+
considered: {
|
|
218
|
+
type: "string",
|
|
219
|
+
description: "Which existing rules you read and why they did not answer.",
|
|
220
|
+
},
|
|
221
|
+
file: { type: "string", description: "Where the case lives." },
|
|
222
|
+
},
|
|
223
|
+
required: ["name", "purpose"],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
203
226
|
];
|
|
204
227
|
/**
|
|
205
228
|
* QUANTAS FERRAMENTAS ESTE SERVIDOR SERVE, lido da lista.
|
|
@@ -1037,6 +1060,24 @@ cli) {
|
|
|
1037
1060
|
? `Filed as ${r.id} and routed to the synthesisui platform team - the system's contract already promises this and the shipped css does not deliver it. No one needs to act: it closes itself when an update lands. Keep the quiet base meanwhile.`
|
|
1038
1061
|
: `Filed as ${r.id}. Do not add the token yourself - the request shows up in \`synthesisui doctor\` for a person to decide.`);
|
|
1039
1062
|
}
|
|
1063
|
+
case "request_rule": {
|
|
1064
|
+
/**
|
|
1065
|
+
* SEM TRIAGEM AUTOMÁTICA, e de propósito.
|
|
1066
|
+
*
|
|
1067
|
+
* `request_token` sabe rotear para a plataforma quando o contrato do sistema já promete o
|
|
1068
|
+
* nome - é uma pergunta que a máquina responde. "Isto deveria ser uma regra?" não é: ela é a
|
|
1069
|
+
* decisão de quem é dono do sistema, e roteá-la sozinho seria inventar a resposta em vez de
|
|
1070
|
+
* abrir a pergunta.
|
|
1071
|
+
*/
|
|
1072
|
+
const r = await fileRequest(root, {
|
|
1073
|
+
kind: "rule",
|
|
1074
|
+
name: String(args.name ?? ""),
|
|
1075
|
+
purpose: String(args.purpose ?? ""),
|
|
1076
|
+
considered: args.considered ? String(args.considered) : undefined,
|
|
1077
|
+
file: args.file ? String(args.file) : undefined,
|
|
1078
|
+
});
|
|
1079
|
+
return text(`Filed as ${r.id}. Do not adopt the convention as if it were a rule - it shows up in \`synthesisui doctor\` and travels to the system's queue, for the owner to make it a rule or decline it.`);
|
|
1080
|
+
}
|
|
1040
1081
|
default:
|
|
1041
1082
|
return text(`No tool named ${name}.`, true);
|
|
1042
1083
|
}
|
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/request.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { closeRequest, fileRequest, readRequests } from "../doctor/requests.js";
|
|
2
|
+
import { closeRequest, fileRequest, KINDS, readRequests, } from "../doctor/requests.js";
|
|
3
3
|
import { body, section } from "../output.js";
|
|
4
4
|
/**
|
|
5
5
|
* `synthesisui request` - the queue of what the agent needed and was refused.
|
|
@@ -12,6 +12,12 @@ import { body, section } from "../output.js";
|
|
|
12
12
|
* Closing is deliberately explicit. A request nobody got to is still a
|
|
13
13
|
* request; nothing here expires.
|
|
14
14
|
*/
|
|
15
|
+
/** Como cada tipo se lê numa linha - um lugar, três telas. */
|
|
16
|
+
const labelOf = (r) => r.kind === "token"
|
|
17
|
+
? `token ${r.value} as ${r.name}`
|
|
18
|
+
: r.kind === "rule"
|
|
19
|
+
? `rule "${r.name}"`
|
|
20
|
+
: `component "${r.name}"`;
|
|
15
21
|
export async function request(opts) {
|
|
16
22
|
const root = resolve(opts.dir ?? process.cwd());
|
|
17
23
|
if (opts.done) {
|
|
@@ -29,7 +35,7 @@ export async function request(opts) {
|
|
|
29
35
|
return;
|
|
30
36
|
}
|
|
31
37
|
for (const r of all) {
|
|
32
|
-
console.log(body(` ${r.id} ${r
|
|
38
|
+
console.log(body(` ${r.id} ${labelOf(r)}`));
|
|
33
39
|
console.log(body(` for: ${r.purpose}`));
|
|
34
40
|
if (r.considered)
|
|
35
41
|
console.log(body(` considered: ${r.considered}`));
|
|
@@ -40,14 +46,23 @@ export async function request(opts) {
|
|
|
40
46
|
console.log(body("Close one: synthesisui request --done <id>"));
|
|
41
47
|
return;
|
|
42
48
|
}
|
|
43
|
-
if (
|
|
44
|
-
console.log(`Unknown kind "${opts.kind}" - component or
|
|
49
|
+
if (!KINDS.has(opts.kind)) {
|
|
50
|
+
console.log(`Unknown kind "${opts.kind}" - component, token or rule.`);
|
|
45
51
|
return;
|
|
46
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* UMA REGRA PRECISA DO CASO, e é por isso que ela exige `--for` como as outras.
|
|
55
|
+
*
|
|
56
|
+
* "Isto deveria ser uma regra" sem o caso que a motivou é uma opinião. Com o caso, quem decidir do
|
|
57
|
+
* outro lado tem o que a doutrina não cobria e onde isso apareceu - que é a diferença entre uma
|
|
58
|
+
* fila que vira decisão e uma que vira backlog.
|
|
59
|
+
*/
|
|
47
60
|
if (!opts.name || !opts.purpose || (opts.kind === "token" && !opts.value)) {
|
|
48
61
|
console.log(opts.kind === "token"
|
|
49
62
|
? "A token request needs --value, --name and --for."
|
|
50
|
-
:
|
|
63
|
+
: opts.kind === "rule"
|
|
64
|
+
? "A rule request needs --name and --for - what the rule would say, and the case that asked for it."
|
|
65
|
+
: "A component request needs --name and --for.");
|
|
51
66
|
return;
|
|
52
67
|
}
|
|
53
68
|
const r = await fileRequest(root, {
|
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/requests.js
CHANGED
|
@@ -22,6 +22,8 @@ import { join } from "node:path";
|
|
|
22
22
|
* how a team shares a queue.
|
|
23
23
|
*/
|
|
24
24
|
export const REQUESTS_FILE = "requests.jsonl";
|
|
25
|
+
/** Os tipos que a fila aceita, num lugar só - ver `GapRequest.kind`. */
|
|
26
|
+
export const KINDS = new Set(["component", "token", "rule"]);
|
|
25
27
|
const path = (root) => join(root, "_synthesisui", REQUESTS_FILE);
|
|
26
28
|
/** Stable-enough id from content: 6 chars, collision-safe at queue scale. */
|
|
27
29
|
function idOf(kind, name, at) {
|
|
@@ -65,7 +67,14 @@ export async function readRequests(root) {
|
|
|
65
67
|
continue;
|
|
66
68
|
try {
|
|
67
69
|
const r = JSON.parse(line);
|
|
68
|
-
|
|
70
|
+
/**
|
|
71
|
+
* O FILTRO DA LEITURA - e ele é um dos três lugares onde um tipo novo some CALADO.
|
|
72
|
+
*
|
|
73
|
+
* Uma linha com `kind` desconhecido é descartada aqui sem erro, então acrescentar um tipo sem
|
|
74
|
+
* passar por este ponto produz um pedido que o agente arquiva, o arquivo guarda, e ninguém
|
|
75
|
+
* nunca lê.
|
|
76
|
+
*/
|
|
77
|
+
if (r?.id && r.name && KINDS.has(r.kind))
|
|
69
78
|
out.push(r);
|
|
70
79
|
}
|
|
71
80
|
catch {
|
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
|
*
|
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
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A SKILL DE MANUTENÇÃO, como o CLI a distribui.
|
|
3
|
+
*
|
|
4
|
+
* Mesmo motivo do `skill-init.ts` e do `skill-import.ts`: o build é `tsc` e nada mais, então um `.md`
|
|
5
|
+
* precisaria de um passo de cópia que pode silenciosamente não rodar. Um módulo TypeScript não pode
|
|
6
|
+
* falhar em ser empacotado.
|
|
7
|
+
*
|
|
8
|
+
* ESTA É A FONTE. A cópia em `.claude/skills/` é o que o nosso editor lê, e o spec assere que as duas
|
|
9
|
+
* são idênticas.
|
|
10
|
+
*
|
|
11
|
+
* E ela é a TERCEIRA que o `connect` instala - as outras duas são de ENTRADA (primeira corrida,
|
|
12
|
+
* import). Esta responde a pergunta do dia seguinte, apontando para uma tela: *"isso aqui está de
|
|
13
|
+
* acordo com o meu design system?"*. Deixá-la fora do `connect` faria dela uma skill nossa, e o caso
|
|
14
|
+
* de uso que a motivou é o cliente rodando sozinho toda semana.
|
|
15
|
+
*/
|
|
16
|
+
export const ADAPT_SKILL = `---
|
|
17
|
+
name: sui-adapt
|
|
18
|
+
description: Confronta UM componente (ou uma página, ou uma pasta) contra o design system instalado e adapta o que der - cobertura de token medida, conserto mecânico proposto antes de escrever, e o que sobra classificado em "regra nova" ou "conserto local". Use quando alguém aponta para uma peça e pergunta se ela está de acordo com o sistema (ex. "/sui-adapt components/ui/card", "analisa esse componente aqui", "isso aqui segue o meu design system?"), como rotina semanal, ou logo depois de criar/alterar um componente. Mede antes de propor, propõe antes de escrever, e nunca arquiva pedido em nome de ninguém.
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Adaptar uma peça ao sistema
|
|
22
|
+
|
|
23
|
+
O propósito do produto, que decide todo empate abaixo: **ler o repositório do cliente e
|
|
24
|
+
devolver receitas com paridade visual, semântica e funcional, sem supor e sem inventar
|
|
25
|
+
nada.**
|
|
26
|
+
|
|
27
|
+
Esta skill é a ponta de manutenção. As outras duas que o cliente tem são de ENTRADA -
|
|
28
|
+
\`sui-init\` e \`sui-import-ds\` transformam o repositório dele em sistema. Esta responde a
|
|
29
|
+
pergunta do dia seguinte, que ele faz apontando para uma tela: *"isso aqui está de acordo
|
|
30
|
+
com o meu design system?"*.
|
|
31
|
+
|
|
32
|
+
\`CLAUDE.md\` manda. Quando os dois divergirem, este arquivo é que está velho.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 0. O QUE ESTA SKILL NÃO FAZ
|
|
37
|
+
|
|
38
|
+
Três limites, e cada um existe por um motivo que já custou alguma coisa:
|
|
39
|
+
|
|
40
|
+
\`\`\`
|
|
41
|
+
não escreve sem propor é o repositório DELE. \`--fix --write\` sem confirmação é
|
|
42
|
+
outra categoria de confiança, e uma skill que perde essa
|
|
43
|
+
confiança não é rodada uma segunda vez
|
|
44
|
+
não arquiva pedido \`request\` fila uma decisão na plataforma. A skill MOSTRA o
|
|
45
|
+
comando; quem roda é ele
|
|
46
|
+
não inventa token, nome um agente que cala um relatório fazendo o sistema crescer é
|
|
47
|
+
nem regra pior que a deriva que ele veio medir
|
|
48
|
+
\`\`\`
|
|
49
|
+
|
|
50
|
+
## 1. O ALVO, E POR QUE O ESCOPO É DECISÃO DA SKILL
|
|
51
|
+
|
|
52
|
+
Componente quase nunca é um arquivo. \`Card.tsx\` costuma vir com \`Card.css\`, \`Card.stories.tsx\`
|
|
53
|
+
e às vezes um \`index.ts\` - e medir só o \`.tsx\` produz um número que mente por omissão: o css
|
|
54
|
+
ao lado é justamente onde os valores à mão se escondem.
|
|
55
|
+
|
|
56
|
+
\`\`\`
|
|
57
|
+
1. resolva o alvo o que ele apontou, ou o arquivo aberto, ou o que ele acabou de mexer
|
|
58
|
+
2. suba para a PASTA quando o irmão existir (mesmo nome, extensão diferente)
|
|
59
|
+
3. DIGA qual escopo você usou, com o número de arquivos
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
Nunca meça os dois e escolha o maior. Diga o que mediu.
|
|
63
|
+
|
|
64
|
+
## 2. MEÇA - e a medida é determinística, não sua
|
|
65
|
+
|
|
66
|
+
Duas portas, mesma resposta. Use a que a sessão tiver:
|
|
67
|
+
|
|
68
|
+
\`\`\`
|
|
69
|
+
MCP check_file { path }
|
|
70
|
+
terminal npx synthesisui doctor <alvo>
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
O que volta, medido num componente real (\`ArticleCard\`, 13/08):
|
|
74
|
+
|
|
75
|
+
\`\`\`
|
|
76
|
+
SignalUI v7 - 181 tokens, 1 file read
|
|
77
|
+
scope: packages/ui/src/lib/SignalUI/organisms/ArticleCard/ArticleCard.tsx
|
|
78
|
+
|
|
79
|
+
Token coverage ░░░░░░░░░░░░░░░░░░░░░░░░ 0%
|
|
80
|
+
0 from the system, 2 by hand
|
|
81
|
+
50% is one command away - 1 of those have a name waiting
|
|
82
|
+
\`\`\`
|
|
83
|
+
|
|
84
|
+
Três números, e eles já vêm separados por natureza:
|
|
85
|
+
|
|
86
|
+
\`\`\`
|
|
87
|
+
from the system já usa o vocabulário. Nada a fazer
|
|
88
|
+
have a name waiting o sistema JÁ nomeia esse valor -> mecânico, é o passo 3
|
|
89
|
+
no name for it o sistema não nomeia -> DECISÃO dele, é o passo 5
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
Não recalcule nada disso de cabeça. O número que você reporta é o que o comando disse.
|
|
93
|
+
|
|
94
|
+
## 3. PROPONHA O MECÂNICO, E SÓ DEPOIS ESCREVA
|
|
95
|
+
|
|
96
|
+
\`--fix\` mostra; \`--write\` escreve. Rode o primeiro, cole a saída, espere.
|
|
97
|
+
|
|
98
|
+
\`\`\`
|
|
99
|
+
npx synthesisui doctor <alvo> --fix
|
|
100
|
+
\`\`\`
|
|
101
|
+
|
|
102
|
+
\`\`\`
|
|
103
|
+
Would replace 1 hand-written value with the token your system already has, across 1 file.
|
|
104
|
+
var(--ds-color-ocean-50) · 1 time
|
|
105
|
+
1 finding left: values your system has no name for. Those are decisions.
|
|
106
|
+
\`\`\`
|
|
107
|
+
|
|
108
|
+
Ofereça exatamente três saídas, nesta ordem:
|
|
109
|
+
|
|
110
|
+
\`\`\`
|
|
111
|
+
confirmar npx synthesisui doctor <alvo> --fix --write
|
|
112
|
+
à mão você lista as trocas e ele edita - use quando ele quiser revisar linha a linha
|
|
113
|
+
cancelar e a medição fica, que já é resultado: ele sabe onde está
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
\`--fix\` só troca o que o sistema DELE já nomeia. Ele nunca inventa um nome, e é por isso
|
|
117
|
+
que o conserto pode ser mecânico.
|
|
118
|
+
|
|
119
|
+
## 4. AS REGRAS, QUE É A METADE QUE NENHUM COMANDO FAZ
|
|
120
|
+
|
|
121
|
+
O passo 2 é determinístico e sai de graça. Este não: o sistema carrega uma doutrina em
|
|
122
|
+
prosa, e **nada a verifica mecanicamente**. É aqui que você trabalha.
|
|
123
|
+
|
|
124
|
+
\`\`\`
|
|
125
|
+
MCP system_doctrine
|
|
126
|
+
terminal as regras viajam no documento instalado (_synthesisui/ds/<slug>/doctrine.json)
|
|
127
|
+
\`\`\`
|
|
128
|
+
|
|
129
|
+
Leia as regras e confronte o componente com cada uma. Três respostas possíveis por regra,
|
|
130
|
+
e a terceira é a que interessa:
|
|
131
|
+
|
|
132
|
+
\`\`\`
|
|
133
|
+
cumpre diga em uma linha, sem cerimônia
|
|
134
|
+
NÃO cumpre cite a regra, o lugar no arquivo, e proponha o conserto
|
|
135
|
+
a regra não fala sobre isto <- o passo 5
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
Uma regra que você teve que interpretar para aplicar não é "cumpre". É o terceiro caso.
|
|
139
|
+
|
|
140
|
+
## 5. O QUE SOBROU, CLASSIFICADO - e cada classe tem um destino
|
|
141
|
+
|
|
142
|
+
Aqui a pergunta deixa de ser "está certo?" e passa a ser **"isto vira sistema, ou fica
|
|
143
|
+
aqui?"**. Três destinos, e todos existem como comando:
|
|
144
|
+
|
|
145
|
+
\`\`\`
|
|
146
|
+
valor sem nome no sistema
|
|
147
|
+
-> npx synthesisui request token --value "<valor>" --name "<como se chamaria>" --for "<o caso>"
|
|
148
|
+
ou, pelo MCP: request_token
|
|
149
|
+
|
|
150
|
+
peça que falta
|
|
151
|
+
-> npx synthesisui request component --name "<nome>" --for "<o caso>"
|
|
152
|
+
ou: request_component
|
|
153
|
+
|
|
154
|
+
a doutrina não cobre este caso
|
|
155
|
+
-> npx synthesisui request rule --name "<o que a regra diria>" --for "<o caso que pediu>"
|
|
156
|
+
ou: request_rule
|
|
157
|
+
\`\`\`
|
|
158
|
+
|
|
159
|
+
**Você mostra o comando. Ele roda.** Um pedido é uma decisão entrando na fila do sistema
|
|
160
|
+
dele - e a fila viaja para a plataforma no próximo \`sync\`, que é o que a torna
|
|
161
|
+
compartilhável quando houver time. Arquivar em nome dele seria decidir por ele e ainda
|
|
162
|
+
tirar dele a chance de dizer "não, isso fica local mesmo".
|
|
163
|
+
|
|
164
|
+
Antes de propor \`request rule\`, leia a doutrina inteira. Uma regra que já existe e você não
|
|
165
|
+
achou vira uma regra duplicada na fila, e a fila perde valor na terceira duplicata.
|
|
166
|
+
|
|
167
|
+
## 6. FECHE EM QUATRO LINHAS
|
|
168
|
+
|
|
169
|
+
Sem prosa. O cliente precisa decidir, não auditar:
|
|
170
|
+
|
|
171
|
+
\`\`\`
|
|
172
|
+
<Componente> <n> arquivos
|
|
173
|
+
cobertura X% -> Y% (<n> trocas escritas · <n> continuam à mão)
|
|
174
|
+
regras <n> de <n> cumpridas <as que não, nomeadas>
|
|
175
|
+
na sua mão <n> decisões <token · componente · regra>, com o comando ao lado
|
|
176
|
+
\`\`\`
|
|
177
|
+
|
|
178
|
+
Se nada mudou, diga isso e pare. Uma skill que sempre encontra trabalho é uma skill que
|
|
179
|
+
inventa trabalho.
|
|
180
|
+
|
|
181
|
+
## 7. ROTINA
|
|
182
|
+
|
|
183
|
+
Ela foi desenhada para duas horas do dia, e a segunda é a que mais rende:
|
|
184
|
+
|
|
185
|
+
\`\`\`
|
|
186
|
+
semanal "roda o sui-adapt no dashboard" - pega deriva antes de virar hábito
|
|
187
|
+
depois de mexer componente novo, ou alteração grande: rode ANTES do commit, enquanto a
|
|
188
|
+
decisão ainda está quente e o conserto ainda é barato
|
|
189
|
+
\`\`\`
|
|
190
|
+
|
|
191
|
+
O hook (\`PostToolUse\`) já roda a metade determinística a cada escrita, calado quando não há
|
|
192
|
+
o que dizer. Esta skill é o passo deliberado: ela junta o hook, a doutrina e a fila numa
|
|
193
|
+
conversa só, e termina com o cliente decidindo - não com um relatório.
|
|
194
|
+
`;
|
|
195
|
+
export const ADAPT_SKILL_PATH = ".claude/skills/sui-adapt/SKILL.md";
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ADAPT_SKILL, ADAPT_SKILL_PATH } from "./skill-adapt.js";
|
|
2
|
+
import { IMPORT_SKILL, IMPORT_SKILL_PATH } from "./skill-import.js";
|
|
3
|
+
import { INIT_SKILL, INIT_SKILL_PATH } from "./skill-init.js";
|
|
4
|
+
/**
|
|
5
|
+
* AS SKILLS QUE O CLI DISTRIBUI, numa lista só - e ela existe porque DOIS comandos precisam dela.
|
|
6
|
+
*
|
|
7
|
+
* `connect` escreve; `align` cobra o que falta. Enquanto a lista morava dentro do `connect`, o
|
|
8
|
+
* `align` não tinha como saber que existia uma terceira - e uma skill nova só chegava a quem, por
|
|
9
|
+
* conta própria, rodasse `connect` de novo. Ninguém roda um comando de novo sem motivo.
|
|
10
|
+
*
|
|
11
|
+
* É a lei 8 no caso mais barato dela: a lacuna existe, a gente sabe qual é, e dizer custa uma linha.
|
|
12
|
+
*/
|
|
13
|
+
export const SKILLS = [
|
|
14
|
+
{
|
|
15
|
+
path: INIT_SKILL_PATH,
|
|
16
|
+
source: INIT_SKILL,
|
|
17
|
+
label: "/sui-init",
|
|
18
|
+
what: "the first run, start to finish",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: IMPORT_SKILL_PATH,
|
|
22
|
+
source: IMPORT_SKILL,
|
|
23
|
+
label: "/sui-import-ds",
|
|
24
|
+
what: "the import, orchestrated",
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* A DE MANUTENÇÃO, e ela é de outra natureza que as duas acima.
|
|
28
|
+
*
|
|
29
|
+
* As duas primeiras são de ENTRADA: rodam uma vez, e depois nunca mais. Esta responde a pergunta do
|
|
30
|
+
* dia seguinte, apontando para uma tela - *"isso aqui está de acordo com o meu design system?"* -,
|
|
31
|
+
* toda semana ou depois de mexer em alguma coisa. É a primeira que uma pessoa roda mais de uma vez.
|
|
32
|
+
*/
|
|
33
|
+
{
|
|
34
|
+
path: ADAPT_SKILL_PATH,
|
|
35
|
+
source: ADAPT_SKILL,
|
|
36
|
+
label: "/sui-adapt",
|
|
37
|
+
what: "one component against the system, and what to do about it",
|
|
38
|
+
},
|
|
39
|
+
];
|
package/package.json
CHANGED