synthesisui 0.16.388 → 0.16.390
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/claude-md.js +1 -1
- package/dist/commands/add.js +2 -53
- package/dist/commands/component.js +2 -7
- package/dist/commands/doctor.js +43 -9
- package/dist/commands/generate.js +2 -2
- package/dist/commands/init.js +3 -3
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/refit.js +2 -2
- package/dist/commands/upgrade.js +4 -2
- package/dist/commands/use.js +1 -1
- package/dist/component-codegen.js +70 -27
- package/dist/copy/connect.pt-BR.js +1 -1
- package/dist/global-sheet.js +50 -1
- package/dist/install-marks.js +1 -1
- package/dist/project-facts.js +40 -0
- package/dist/setup-prompt.js +95 -0
- package/dist/skill-configure.js +1 -1
- package/dist/skills.js +1 -1
- package/package.json +1 -1
- package/dist/wiring-prompt.js +0 -34
package/dist/claude-md.js
CHANGED
|
@@ -329,7 +329,7 @@ async function renderRegion(projectRoot, installed) {
|
|
|
329
329
|
START,
|
|
330
330
|
"## Design system",
|
|
331
331
|
"",
|
|
332
|
-
"This project
|
|
332
|
+
"This project has the synthesisui agent set up and **no design system yet** - so there is",
|
|
333
333
|
"no token vocabulary to follow, and nothing here to obey.",
|
|
334
334
|
"",
|
|
335
335
|
"To turn this repository into one, run `/sui-import-ds` and I will read what is",
|
package/dist/commands/add.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { access, mkdir,
|
|
1
|
+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { syncClaudeMd } from "../claude-md.js";
|
|
4
4
|
import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
|
|
5
5
|
import { customFontFamilies, googleFontsHref, nextFontSnippet, } from "../fonts.js";
|
|
6
|
-
import { globalSheetOf, prefixFrom } from "../global-sheet.js";
|
|
6
|
+
import { detectAppDirs, globalSheetOf, prefixFrom } from "../global-sheet.js";
|
|
7
7
|
import { lockReference } from "../group-role.js";
|
|
8
8
|
import { buildGuide } from "../guide.js";
|
|
9
9
|
import { censusScope } from "../measured-scope.js";
|
|
@@ -131,57 +131,6 @@ async function readRootLock(path) {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
const exists = (path) => access(path).then(() => true, () => false);
|
|
134
|
-
/**
|
|
135
|
-
* ONDE AS ROTAS DELE MORAM DE VERDADE - e num monorepo elas não moram na raiz.
|
|
136
|
-
*
|
|
137
|
-
* Olhava só `app/` e `src/app/` na raiz. O `codelevel-monorepo` tem `apps/web/app` e
|
|
138
|
-
* `apps/landing/app`, então a busca voltava null, o `appDir` caía no default `"app"`, e TODO caminho
|
|
139
|
-
* impresso no setup apontava para uma pasta que não existe: `app/globals.css`, `app/layout.tsx`,
|
|
140
|
-
* `app/fonts.ts`. O cliente lê três instruções e nenhuma serve para o repositório dele.
|
|
141
|
-
*
|
|
142
|
-
* O que faz uma pasta ser raiz de app é o `layout` do App Router morar dentro dela - por isso o
|
|
143
|
-
* `apps/api` do Nest, que também é um workspace, não entra na lista.
|
|
144
|
-
*
|
|
145
|
-
* DEVOLVE TODAS, em ordem determinística: o setup é "once per app" e num monorepo isso é literal.
|
|
146
|
-
* Escolher uma calada seria acertar uma e errar a outra sem dizer qual.
|
|
147
|
-
*/
|
|
148
|
-
export async function detectAppDirs(root, pagesDir) {
|
|
149
|
-
const isAppRoot = async (dir) => {
|
|
150
|
-
if (!(await exists(join(root, dir))))
|
|
151
|
-
return false;
|
|
152
|
-
for (const ext of ["tsx", "jsx", "ts", "js"])
|
|
153
|
-
if (await exists(join(root, dir, `layout.${ext}`)))
|
|
154
|
-
return true;
|
|
155
|
-
return false;
|
|
156
|
-
};
|
|
157
|
-
const here = [pagesDir, `src/${pagesDir}`];
|
|
158
|
-
const nested = [];
|
|
159
|
-
for (const group of ["apps", "packages"]) {
|
|
160
|
-
let entries;
|
|
161
|
-
try {
|
|
162
|
-
entries = await readdir(join(root, group));
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
for (const entry of entries.sort())
|
|
168
|
-
nested.push(`${group}/${entry}/${pagesDir}`, `${group}/${entry}/src/${pagesDir}`);
|
|
169
|
-
}
|
|
170
|
-
const found = [];
|
|
171
|
-
for (const dir of [...here, ...nested])
|
|
172
|
-
if (await isAppRoot(dir))
|
|
173
|
-
found.push(dir);
|
|
174
|
-
/**
|
|
175
|
-
* A RAIZ QUE EXISTE MAS NÃO TEM LAYOUT ainda é o melhor palpite de um app avulso - é o caso de um
|
|
176
|
-
* `create-next-app` no meio de uma migração. Sem isto, um projeto de app único perderia a detecção
|
|
177
|
-
* que já funcionava.
|
|
178
|
-
*/
|
|
179
|
-
if (found.length === 0)
|
|
180
|
-
for (const dir of here)
|
|
181
|
-
if (await exists(join(root, dir)))
|
|
182
|
-
return [dir];
|
|
183
|
-
return found;
|
|
184
|
-
}
|
|
185
134
|
/**
|
|
186
135
|
* Materializes a published DS into `_synthesisui/ds/<slug>/v<version>/`, points
|
|
187
136
|
* stable root re-exports (tokens.css/theme.css) and a `.lock` at it, and updates
|
|
@@ -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, readInstalledScheme, } from "../project-facts.js";
|
|
9
|
+
import { findCollision, installedThemeVars, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
10
10
|
import { fetchComponent, RegistryError } from "../registry.js";
|
|
11
11
|
import { flavourResolver } from "../styles-flavour.js";
|
|
12
12
|
import { inTheirTongue, projectTongue, sumSpoken, } from "../their-tongue.js";
|
|
@@ -149,11 +149,6 @@ export async function component(slug, name, opts) {
|
|
|
149
149
|
* Re-materializing something WE generated is almost always what they meant, so
|
|
150
150
|
* that keeps going. Shadowing a component of theirs stops and asks.
|
|
151
151
|
*/
|
|
152
|
-
const pascalName = res.name
|
|
153
|
-
.split(/[^a-zA-Z0-9]+/)
|
|
154
|
-
.filter(Boolean)
|
|
155
|
-
.map((p) => p[0].toUpperCase() + p.slice(1))
|
|
156
|
-
.join("");
|
|
157
152
|
/**
|
|
158
153
|
* THEY ALREADY HAVE THIS COMPONENT, and the recipe says where.
|
|
159
154
|
*
|
|
@@ -316,7 +311,7 @@ export async function component(slug, name, opts) {
|
|
|
316
311
|
* O ARQUIVO e o EXPORT levam o nome dele; a CLASSE continua sendo a do blueprint. Um
|
|
317
312
|
* `<CardPreview>` vestindo `.ds-card` está estilizado certo e não faz sombra em nada dele.
|
|
318
313
|
*/
|
|
319
|
-
local, await readInstalledScheme(root, slug), tongue);
|
|
314
|
+
local, await readInstalledScheme(root, slug), tongue, await installedThemeVars(root, slug));
|
|
320
315
|
spoken = said;
|
|
321
316
|
/**
|
|
322
317
|
* AS DECLARAÇÕES DO ARQUIVO DELE QUE O INTERPRETADOR NÃO LEU - ver `unread-for-component.ts`.
|
package/dist/commands/doctor.js
CHANGED
|
@@ -17,12 +17,13 @@ import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from ".
|
|
|
17
17
|
import { withTheirNames } from "../doctor/their-names.js";
|
|
18
18
|
import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
|
|
19
19
|
import { FAMILY_SEAM_PREFIX } from "../fonts.js";
|
|
20
|
+
import { detectAppDirs, globalSheetOf, prefixFrom } from "../global-sheet.js";
|
|
20
21
|
import { groupRole } from "../group-role.js";
|
|
21
22
|
import { actingSlug, describeScope, measuredScope, scopePaths, } from "../measured-scope.js";
|
|
22
23
|
import { body, paint, section, snippet } from "../output.js";
|
|
24
|
+
import { setupPrompt } from "../setup-prompt.js";
|
|
23
25
|
import { resolveDeps } from "../stack.js";
|
|
24
26
|
import { danglingTheirVars } from "../their-vars.js";
|
|
25
|
-
import { wiringPrompt } from "../wiring-prompt.js";
|
|
26
27
|
/**
|
|
27
28
|
* `synthesisui doctor` - the check nobody else ships.
|
|
28
29
|
*
|
|
@@ -698,7 +699,9 @@ export async function doctor(opts) {
|
|
|
698
699
|
* grupo está migrando para ela - e antes disto a referência existia na
|
|
699
700
|
* plataforma e o comando que ordena a lista nunca ficava sabendo.
|
|
700
701
|
*/
|
|
701
|
-
|
|
702
|
+
/** Lida uma vez: o `intent` ordena a lista e o setup abaixo deriva a folha e o nº de imports. */
|
|
703
|
+
const config = await readProjectConfig(root);
|
|
704
|
+
const intent = intentOf(config, opts.intent, groupRole(installed.groupLocks, installed.table.slug));
|
|
702
705
|
const { recipes, documents } = installed;
|
|
703
706
|
/**
|
|
704
707
|
* A TABELA JÁ VEM COM O VOCABULÁRIO DELE DENTRO - ver `loadSystem` e `their-names.ts`.
|
|
@@ -961,7 +964,7 @@ export async function doctor(opts) {
|
|
|
961
964
|
(!wiring.imported || !wiring.scoped || fontsPending);
|
|
962
965
|
if (unwired) {
|
|
963
966
|
console.log("");
|
|
964
|
-
console.log(body(`${table.name ?? table.slug} is installed - but not
|
|
967
|
+
console.log(body(`${table.name ?? table.slug} is installed - but this project does not load it yet.`));
|
|
965
968
|
console.log("");
|
|
966
969
|
console.log(body(wiring.imported
|
|
967
970
|
? ` ✓ some stylesheet imports _synthesisui/ds/${table.slug}/tokens.css`
|
|
@@ -1005,19 +1008,50 @@ export async function doctor(opts) {
|
|
|
1005
1008
|
* aqui já importou, materializou e rodou upgrade: mandá-lo ao `init` é devolver ao começo
|
|
1006
1009
|
* alguém que está a um passo do fim.
|
|
1007
1010
|
*
|
|
1008
|
-
* E O PASSO JÁ EXISTIA PRONTO. `
|
|
1011
|
+
* E O PASSO JÁ EXISTIA PRONTO. `setupPrompt` mora em `setup-prompt.ts`, exportado, e era
|
|
1009
1012
|
* usado só pelo `init`. Imprimi-lo aqui não é texto novo: é a MESMA fonte chegando na tela
|
|
1010
1013
|
* onde a pergunta nasce. Duas redações do mesmo passo divergiriam no primeiro conserto.
|
|
1011
1014
|
*
|
|
1012
|
-
* O PROMPT
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1015
|
+
* O PROMPT LEVA A MEDIÇÃO, e até 07/09 ele era FIXO - o defeito que o dono leu na própria tela.
|
|
1016
|
+
*
|
|
1017
|
+
* As três linhas acima acabaram de imprimir `✗ ✓ ✓`: só o import faltava. O texto seguinte
|
|
1018
|
+
* mandava *"Do the ONE-TIME SETUP it names, all of it"* e listava as três, então um agente
|
|
1019
|
+
* obediente reescreve o escopo e o mapa de tipografia que já estavam corretos - contra a última
|
|
1020
|
+
* linha do próprio texto, que pede para não mexer nos estilos dele. Um comando que mede e depois
|
|
1021
|
+
* diz outra coisa gasta a medição.
|
|
1022
|
+
*
|
|
1023
|
+
* E A FOLHA TEM NOME, ao contrário do que este comentário dizia antes. `globalSheetOf` +
|
|
1024
|
+
* `prefixFrom` são as mesmas funções que o `add` usa para achar a folha que os apps REALMENTE
|
|
1025
|
+
* carregam - num monorepo não é o `globals.css` do app - e para contar o caminho relativo até a
|
|
1026
|
+
* raiz. Elas existiam e este comando não as chamava; então ele descrevia o arquivo em vez de
|
|
1027
|
+
* nomeá-lo, e o agente tinha que redescobrir o que a gente já sabia.
|
|
1028
|
+
*
|
|
1029
|
+
* Quando a derivação falha, `sheet` fica ausente e o texto volta a descrever - nenhum caminho é
|
|
1030
|
+
* inventado.
|
|
1015
1031
|
*/
|
|
1016
1032
|
if (table.slug) {
|
|
1033
|
+
const appDirs = await detectAppDirs(root, config.pagesDir);
|
|
1034
|
+
const sheet = appDirs[0]
|
|
1035
|
+
? await globalSheetOf(root, `${appDirs[0]}/globals.css`)
|
|
1036
|
+
: null;
|
|
1017
1037
|
console.log("");
|
|
1018
|
-
console.log(body("Paste this into your agent - it does the
|
|
1038
|
+
console.log(body("Paste this into your agent - it does the setup for you:"));
|
|
1019
1039
|
console.log("");
|
|
1020
|
-
console.log(snippet(
|
|
1040
|
+
console.log(snippet(setupPrompt(table.slug, {
|
|
1041
|
+
tokens: !wiring.imported,
|
|
1042
|
+
scope: !wiring.scoped,
|
|
1043
|
+
/** Só é passo quando o projeto TEM o arquivo de fontes - ver `readWiring`. */
|
|
1044
|
+
type: wiring.fontsWritten && !wiring.fontsMapped,
|
|
1045
|
+
...(sheet
|
|
1046
|
+
? { sheet: { path: sheet, prefix: prefixFrom(sheet) } }
|
|
1047
|
+
: {}),
|
|
1048
|
+
/**
|
|
1049
|
+
* DUAS LINHAS COM TAILWIND, UMA SEM - a mesma decisão que o `add` já toma pelo projeto.
|
|
1050
|
+
* Dizer "as duas linhas" a um projeto que precisa de uma manda o agente procurar o que
|
|
1051
|
+
* não existe.
|
|
1052
|
+
*/
|
|
1053
|
+
imports: config.styles === "css" ? 1 : 2,
|
|
1054
|
+
}).split("\n")));
|
|
1021
1055
|
}
|
|
1022
1056
|
}
|
|
1023
1057
|
/**
|
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { generateComponentFiles } from "../component-codegen.js";
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
5
|
import { installedSlugs } from "../installed.js";
|
|
6
|
-
import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
6
|
+
import { installedThemeVars, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
7
7
|
import { postGenerate, RegistryError } from "../registry.js";
|
|
8
8
|
import { flavourResolver } from "../styles-flavour.js";
|
|
9
9
|
import { projectTongue } from "../their-tongue.js";
|
|
@@ -75,7 +75,7 @@ export async function generate(description, opts) {
|
|
|
75
75
|
// project as the stylesheet it has to match.
|
|
76
76
|
await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug),
|
|
77
77
|
/** O VOCABULÁRIO DELE - um componente gerado cai no mesmo projeto e fala a mesma língua. */
|
|
78
|
-
await projectTongue(root, slug));
|
|
78
|
+
await projectTongue(root, slug), await installedThemeVars(root, slug));
|
|
79
79
|
for (const file of files) {
|
|
80
80
|
await writeFile(join(compDir, file.filename), file.code, "utf8");
|
|
81
81
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_CONFIG, writeProjectConfig } from "../config.js";
|
|
2
2
|
import { body, section, snippet } from "../output.js";
|
|
3
|
+
import { setupPrompt } from "../setup-prompt.js";
|
|
3
4
|
import { resolveDeps, stylesFor, tailwindMajor } from "../stack.js";
|
|
4
5
|
import { formMix, measuredStyle } from "../styles-flavour.js";
|
|
5
|
-
import { wiringPrompt } from "../wiring-prompt.js";
|
|
6
6
|
import { add } from "./add.js";
|
|
7
7
|
/**
|
|
8
8
|
* Bootstraps a project for SynthesisUI: writes `_synthesisui/config.json`
|
|
@@ -141,9 +141,9 @@ export async function init(opts) {
|
|
|
141
141
|
console.log("");
|
|
142
142
|
console.log(snippet(["npx synthesisui@latest connect"]));
|
|
143
143
|
console.log("");
|
|
144
|
-
console.log(body("2. Open your agent and paste this once - it does the
|
|
144
|
+
console.log(body("2. Open your agent and paste this once - it does the setup for you:"));
|
|
145
145
|
console.log("");
|
|
146
|
-
console.log(snippet(
|
|
146
|
+
console.log(snippet(setupPrompt(opts.ds).split("\n")));
|
|
147
147
|
console.log("");
|
|
148
148
|
console.log(body("Then ask it for something real. The check introduces itself on the first"));
|
|
149
149
|
console.log(body("clean file and goes quiet after that."));
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1286,7 +1286,7 @@ async function describeComponent(root, name) {
|
|
|
1286
1286
|
const r = recipe.runtime;
|
|
1287
1287
|
const lines = [];
|
|
1288
1288
|
if (r.stateOwner === "caller") {
|
|
1289
|
-
lines.push("State: the CALLER owns it - controlled only.
|
|
1289
|
+
lines.push("State: the CALLER owns it - controlled only. Pass the pair below; do not add internal state.");
|
|
1290
1290
|
}
|
|
1291
1291
|
else if (r.stateOwner === "self") {
|
|
1292
1292
|
lines.push("State: the component manages itself. Do not wire external state unless you need to read it.");
|
package/dist/commands/refit.js
CHANGED
|
@@ -4,7 +4,7 @@ import { generateComponentFiles } from "../component-codegen.js";
|
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
5
|
import { installedSlugs } from "../installed.js";
|
|
6
6
|
import { body, section, snippet } from "../output.js";
|
|
7
|
-
import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
7
|
+
import { installedThemeVars, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
8
8
|
import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
|
|
9
9
|
import { flavourResolver } from "../styles-flavour.js";
|
|
10
10
|
import { projectTongue } from "../their-tongue.js";
|
|
@@ -121,7 +121,7 @@ export async function refit(file, opts) {
|
|
|
121
121
|
const flavourOf = await flavourResolver(root, config.styles);
|
|
122
122
|
const { files } = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, flavourOf(res.name), await reactMajorOf(root), await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug),
|
|
123
123
|
/** O VOCABULÁRIO DELE - o `refit` reescreve o componente e não passava pela porta. */
|
|
124
|
-
await projectTongue(root, slug));
|
|
124
|
+
await projectTongue(root, slug), await installedThemeVars(root, slug));
|
|
125
125
|
for (const f of files) {
|
|
126
126
|
await writeFile(join(compDir, f.filename), f.code, "utf8");
|
|
127
127
|
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -8,7 +8,7 @@ import { unsentEvents } from "../doctor/ledger.js";
|
|
|
8
8
|
import { diffLocalDocuments, localChangelogMarkdown, } from "../document-diff.js";
|
|
9
9
|
import { installedBehind, MATERIALISER_SINCE } from "../install-marks.js";
|
|
10
10
|
import { body, section, snippet } from "../output.js";
|
|
11
|
-
import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
11
|
+
import { installedThemeVars, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
12
12
|
import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
|
|
13
13
|
import { flavourResolver } from "../styles-flavour.js";
|
|
14
14
|
import { projectTongue } from "../their-tongue.js";
|
|
@@ -331,6 +331,8 @@ export async function upgrade(asked, opts) {
|
|
|
331
331
|
* língua do repositório. Resolvido uma vez para a corrida inteira, como o sabor.
|
|
332
332
|
*/
|
|
333
333
|
const tongue = await projectTongue(root, slug);
|
|
334
|
+
/** A folha instalada é a MESMA para a corrida inteira - ver `installedThemeVars`. */
|
|
335
|
+
const themeVars = await installedThemeVars(root, slug);
|
|
334
336
|
for (const entry of entries) {
|
|
335
337
|
const tsxPath = join(componentsRoot, entry, `${entry}.tsx`);
|
|
336
338
|
let head = "";
|
|
@@ -359,7 +361,7 @@ export async function upgrade(asked, opts) {
|
|
|
359
361
|
// Both were missing here, and `upgrade` is the command that REWRITES
|
|
360
362
|
// components somebody already has: without the convention it would have
|
|
361
363
|
// taken a working component and stripped its styles.
|
|
362
|
-
await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)), res.name, await readInstalledScheme(root, slug), tongue);
|
|
364
|
+
await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)), res.name, await readInstalledScheme(root, slug), tongue, themeVars);
|
|
363
365
|
/**
|
|
364
366
|
* A NOTA DAS DECLARAÇÕES NÃO INTERPRETADAS SOBREVIVE AO UPGRADE.
|
|
365
367
|
*
|
package/dist/commands/use.js
CHANGED
|
@@ -111,7 +111,7 @@ export async function use(slug, intent, opts) {
|
|
|
111
111
|
config.target === "next"
|
|
112
112
|
? '- Make sure `tokens.css` + `theme.css` are imported in the global CSS (see the GUIDE\'s "How to apply").'
|
|
113
113
|
: '- Make sure `tokens.css` is imported in the global CSS (see the GUIDE\'s "How to apply").',
|
|
114
|
-
"-
|
|
114
|
+
"- Implement the behavior yourself (open/close, focus, routing) - the system ships the looks, not the JS.",
|
|
115
115
|
"",
|
|
116
116
|
"Composition (make it look composed, not just correct):",
|
|
117
117
|
config.target === "next"
|
|
@@ -485,9 +485,31 @@ function remToTwScale(value) {
|
|
|
485
485
|
const n = Number.parseFloat(m[1]) / 0.25;
|
|
486
486
|
return Number.isInteger(n) && n >= 0 && n <= 96 ? String(n) : null;
|
|
487
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* O UTILITÁRIO NOMEADO SÓ VALE QUANDO A FOLHA INSTALADA O DECLARA - e é isso que decide o VALOR.
|
|
490
|
+
*
|
|
491
|
+
* `rounded-lg` existe em qualquer projeto com Tailwind, então a classe nunca "falta". O que muda é
|
|
492
|
+
* de quem é o valor: quando o `theme.css` que caiu na pasta dele declara `--radius-lg`, a classe
|
|
493
|
+
* pinta o raio DO SISTEMA; quando não declara, ela pinta o default do Tailwind - e o componente
|
|
494
|
+
* mente em silêncio, que é pior que uma classe morta, porque nada na tela parece faltar.
|
|
495
|
+
*
|
|
496
|
+
* A folha não declara por dois motivos, os dois legítimos: a redefinição roubaria uma classe que o
|
|
497
|
+
* app dele já usa (`INV-VOLTA-13`, o alinhamento), ou o compilador não faz ponte para um nome que
|
|
498
|
+
* não está em `meta.ownedNames` (o `--font-body` do codelevel). Nos dois casos a resposta certa é a
|
|
499
|
+
* mesma: emitir o valor arbitrário com `var(--ds-*)`, que resolve sempre dentro de `[data-ds]`.
|
|
500
|
+
*
|
|
501
|
+
* MEDIDO em 07/09 sobre TODOS os componentes das duas populações:
|
|
502
|
+
*
|
|
503
|
+
* codelevel, repo real 58 de 110 utilitários de token (53%) não carregavam o valor dele
|
|
504
|
+
* ember, app novo 34 de 241 (14%)
|
|
505
|
+
*
|
|
506
|
+
* `null` é projeto sem folha instalada - `refit` e `generate` já dizem que nada pinta sem o `add`,
|
|
507
|
+
* e ali o nome legível não custa nada.
|
|
508
|
+
*/
|
|
509
|
+
const minted = (themeVars, cssVar) => themeVars === null || themeVars.has(cssVar);
|
|
488
510
|
/** One declaration → Tailwind classes (pretty when mappable, arbitrary-property
|
|
489
511
|
* otherwise - never dropped). */
|
|
490
|
-
function declToTailwind(prop, value) {
|
|
512
|
+
function declToTailwind(prop, value, themeVars) {
|
|
491
513
|
const stat = STATIC[prop]?.[value];
|
|
492
514
|
if (stat)
|
|
493
515
|
return [stat];
|
|
@@ -496,19 +518,19 @@ function declToTailwind(prop, value) {
|
|
|
496
518
|
if (value === "transparent")
|
|
497
519
|
return ["bg-transparent"];
|
|
498
520
|
const key = colorKey(value);
|
|
499
|
-
if (key)
|
|
521
|
+
if (key && minted(themeVars, `--color-${key}`))
|
|
500
522
|
return [`bg-${key}`];
|
|
501
523
|
break;
|
|
502
524
|
}
|
|
503
525
|
case "color": {
|
|
504
526
|
const key = colorKey(value);
|
|
505
|
-
if (key)
|
|
527
|
+
if (key && minted(themeVars, `--color-${key}`))
|
|
506
528
|
return [`text-${key}`];
|
|
507
529
|
break;
|
|
508
530
|
}
|
|
509
531
|
case "borderColor": {
|
|
510
532
|
const key = colorKey(value);
|
|
511
|
-
if (key)
|
|
533
|
+
if (key && minted(themeVars, `--color-${key}`))
|
|
512
534
|
return [`border-${key}`];
|
|
513
535
|
break;
|
|
514
536
|
}
|
|
@@ -517,25 +539,28 @@ function declToTailwind(prop, value) {
|
|
|
517
539
|
const m = value.match(/^1px\s+solid\s+(\{[^}]+\})$/);
|
|
518
540
|
if (m) {
|
|
519
541
|
const key = colorKey(m[1]);
|
|
520
|
-
if (key)
|
|
542
|
+
if (key && minted(themeVars, `--color-${key}`))
|
|
521
543
|
return ["border", `border-${key}`];
|
|
522
544
|
}
|
|
523
545
|
break;
|
|
524
546
|
}
|
|
525
547
|
case "borderRadius": {
|
|
526
548
|
const key = nsKey(value, "radius");
|
|
527
|
-
if (key)
|
|
549
|
+
if (key && minted(themeVars, `--radius-${key}`))
|
|
528
550
|
return [`rounded-${key}`];
|
|
529
551
|
break;
|
|
530
552
|
}
|
|
531
553
|
case "gap": {
|
|
532
554
|
const key = nsKey(value, "spacing");
|
|
533
|
-
if (key)
|
|
555
|
+
if (key && minted(themeVars, `--spacing-${key}`))
|
|
534
556
|
return [`gap-${key}`];
|
|
535
557
|
break;
|
|
536
558
|
}
|
|
537
559
|
case "padding": {
|
|
538
|
-
const keys = value
|
|
560
|
+
const keys = value
|
|
561
|
+
.split(/\s+/)
|
|
562
|
+
.map((v) => nsKey(v, "spacing"))
|
|
563
|
+
.map((k) => (k && minted(themeVars, `--spacing-${k}`) ? k : null));
|
|
539
564
|
if (keys.length === 1 && keys[0])
|
|
540
565
|
return [`p-${keys[0]}`];
|
|
541
566
|
if (keys.length === 2 && keys[0] && keys[1])
|
|
@@ -544,25 +569,25 @@ function declToTailwind(prop, value) {
|
|
|
544
569
|
}
|
|
545
570
|
case "fontSize": {
|
|
546
571
|
const key = scaleKey(value);
|
|
547
|
-
if (key)
|
|
572
|
+
if (key && minted(themeVars, `--text-${key}`))
|
|
548
573
|
return [`text-${key}`];
|
|
549
574
|
break;
|
|
550
575
|
}
|
|
551
576
|
case "fontFamily": {
|
|
552
577
|
const key = nsKey(value, "typography.families");
|
|
553
|
-
if (key)
|
|
578
|
+
if (key && minted(themeVars, `--font-${key}`))
|
|
554
579
|
return [`font-${key}`];
|
|
555
580
|
break;
|
|
556
581
|
}
|
|
557
582
|
case "fontWeight": {
|
|
558
583
|
const key = nsKey(value, "typography.weights");
|
|
559
|
-
if (key)
|
|
584
|
+
if (key && minted(themeVars, `--font-weight-${key}`))
|
|
560
585
|
return [`font-${key}`];
|
|
561
586
|
break;
|
|
562
587
|
}
|
|
563
588
|
case "boxShadow": {
|
|
564
589
|
const key = nsKey(value, "shadow");
|
|
565
|
-
if (key)
|
|
590
|
+
if (key && minted(themeVars, `--shadow-${key}`))
|
|
566
591
|
return [`shadow-${key}`];
|
|
567
592
|
break;
|
|
568
593
|
}
|
|
@@ -648,8 +673,8 @@ function twStateVariants(state) {
|
|
|
648
673
|
const native = TW_NATIVE.has(word) ? [`${word}:`] : [`data-[${word}]:`];
|
|
649
674
|
return [...native, ...(TW_ALSO[state] ?? [])];
|
|
650
675
|
}
|
|
651
|
-
function blockToTailwind(block, prefix = "") {
|
|
652
|
-
return Object.entries(block).flatMap(([prop, value]) => declToTailwind(prop, value).map((cls) => `${prefix}${cls}`));
|
|
676
|
+
function blockToTailwind(block, themeVars, prefix = "") {
|
|
677
|
+
return Object.entries(block).flatMap(([prop, value]) => declToTailwind(prop, value, themeVars).map((cls) => `${prefix}${cls}`));
|
|
653
678
|
}
|
|
654
679
|
/** Every prefix combination one layer needs. A condition that cannot be
|
|
655
680
|
* expressed returns no combination, and the layer is dropped rather than
|
|
@@ -773,23 +798,25 @@ function resolveNode(node, scheme, asRoot) {
|
|
|
773
798
|
return { variants, states, conditional };
|
|
774
799
|
}
|
|
775
800
|
/** The classes every compound condition contributes, in layer order. */
|
|
776
|
-
const conditionalClasses = (resolved) => resolved.conditional.flatMap((c) => blockToTailwind(c.style, c.prefix));
|
|
801
|
+
const conditionalClasses = (resolved, themeVars) => resolved.conditional.flatMap((c) => blockToTailwind(c.style, themeVars, c.prefix));
|
|
777
802
|
function tailwindClassList(recipe,
|
|
778
803
|
/** The node's states and compound conditions, already read off its layers. */
|
|
779
804
|
resolved,
|
|
805
|
+
/** O que a folha instalada declara - ver `minted`. */
|
|
806
|
+
themeVars,
|
|
780
807
|
/** CSS properties a variant axis owns - see `variantOwnedProps`. */
|
|
781
808
|
exclude) {
|
|
782
809
|
const base = exclude
|
|
783
810
|
? Object.fromEntries(Object.entries(recipe.base).filter(([prop]) => !exclude.has(prop)))
|
|
784
811
|
: recipe.base;
|
|
785
812
|
const classes = [
|
|
786
|
-
...blockToTailwind(base),
|
|
813
|
+
...blockToTailwind(base, themeVars),
|
|
787
814
|
// States keep everything: `hover:` and `disabled:` cannot collide with an
|
|
788
815
|
// unprefixed variant class, so there is nothing to resolve.
|
|
789
|
-
...Object.entries(resolved.states).flatMap(([state, block]) => twStateVariants(state).flatMap((prefix) => blockToTailwind(block, prefix))),
|
|
816
|
+
...Object.entries(resolved.states).flatMap(([state, block]) => twStateVariants(state).flatMap((prefix) => blockToTailwind(block, themeVars, prefix))),
|
|
790
817
|
// Compound conditions last: they are the most specific thing the recipe says,
|
|
791
818
|
// and in utilities the later class is the one a reader expects to win.
|
|
792
|
-
...conditionalClasses(resolved),
|
|
819
|
+
...conditionalClasses(resolved, themeVars),
|
|
793
820
|
];
|
|
794
821
|
return classes.join(" ");
|
|
795
822
|
}
|
|
@@ -1160,7 +1187,14 @@ function emitTailwindMode(slug, name, recipe, version, props,
|
|
|
1160
1187
|
/** The name it takes in THEIR project. The utilities stay the system's. */
|
|
1161
1188
|
localName = name,
|
|
1162
1189
|
/** The scheme the document opens in - see `generateComponentFiles`. */
|
|
1163
|
-
scheme = "dark"
|
|
1190
|
+
scheme = "dark",
|
|
1191
|
+
/**
|
|
1192
|
+
* O QUE A FOLHA INSTALADA DECLARA - ver `minted`, e é o que decide de quem é o VALOR.
|
|
1193
|
+
*
|
|
1194
|
+
* `null` mantém o comportamento de sempre: um projeto sem `add` não tem folha nenhuma, e ali o
|
|
1195
|
+
* nome legível não custa nada porque nada pinta de qualquer forma.
|
|
1196
|
+
*/
|
|
1197
|
+
themeVars = null) {
|
|
1164
1198
|
const { tag, attrs, voidEl } = elementFor(name, recipe);
|
|
1165
1199
|
const el = asElement(tag, attrs, voidEl);
|
|
1166
1200
|
const axes = rootAxes(recipe);
|
|
@@ -1170,13 +1204,13 @@ scheme = "dark") {
|
|
|
1170
1204
|
.filter((a) => !a.boolean)
|
|
1171
1205
|
.map((a) => {
|
|
1172
1206
|
const entries = a.options
|
|
1173
|
-
.map((o) => ` ${JSON.stringify(o)}: ${JSON.stringify(blockToTailwind(resolved.variants[a.key]?.[o] ?? {}).join(" "))},`)
|
|
1207
|
+
.map((o) => ` ${JSON.stringify(o)}: ${JSON.stringify(blockToTailwind(resolved.variants[a.key]?.[o] ?? {}, themeVars).join(" "))},`)
|
|
1174
1208
|
.join("\n");
|
|
1175
1209
|
return `const ${a.prop.toUpperCase()}: Record<string, string> = {\n${entries}\n};`;
|
|
1176
1210
|
});
|
|
1177
1211
|
const booleanConsts = axes
|
|
1178
1212
|
.filter((a) => a.boolean)
|
|
1179
|
-
.map((a) => `const ${a.prop.toUpperCase()} = ${JSON.stringify(blockToTailwind(resolved.variants[a.key]?.true ?? {}).join(" "))};`);
|
|
1213
|
+
.map((a) => `const ${a.prop.toUpperCase()} = ${JSON.stringify(blockToTailwind(resolved.variants[a.key]?.true ?? {}, themeVars).join(" "))};`);
|
|
1180
1214
|
// Every property some axis controls leaves BASE, and the base value becomes
|
|
1181
1215
|
// that axis's default - so exactly one class ever sets it and the prop
|
|
1182
1216
|
// actually wins.
|
|
@@ -1206,7 +1240,7 @@ scheme = "dark") {
|
|
|
1206
1240
|
const fromDefault = preset
|
|
1207
1241
|
? (resolved.variants[a.key]?.[preset] ?? {})
|
|
1208
1242
|
: {};
|
|
1209
|
-
return JSON.stringify(blockToTailwind({ ...fromBase, ...fromDefault }).join(" "));
|
|
1243
|
+
return JSON.stringify(blockToTailwind({ ...fromBase, ...fromDefault }, themeVars).join(" "));
|
|
1210
1244
|
};
|
|
1211
1245
|
const clsParts = [
|
|
1212
1246
|
"BASE",
|
|
@@ -1251,7 +1285,7 @@ ${dataAttrLines(axes)}${axes.length ? "\n" : ""} `;
|
|
|
1251
1285
|
import type { ${needsElementType ? `ElementType, ${props}` : props} } from "react";
|
|
1252
1286
|
import { cn } from "../cn";
|
|
1253
1287
|
|
|
1254
|
-
const BASE = ${JSON.stringify([needsGroup ? "group" : "", tailwindClassList(recipe, resolved, excluded)].filter(Boolean).join(" "))};
|
|
1288
|
+
const BASE = ${JSON.stringify([needsGroup ? "group" : "", tailwindClassList(recipe, resolved, themeVars, excluded)].filter(Boolean).join(" "))};
|
|
1255
1289
|
${[...variantConsts, ...booleanConsts].join("\n")}
|
|
1256
1290
|
|
|
1257
1291
|
type ${comp}Props = ${propsType(axes, tag, props, el.offersAs)};
|
|
@@ -1302,9 +1336,9 @@ ${orderedParts
|
|
|
1302
1336
|
* status colours of his card were still nowhere.
|
|
1303
1337
|
*/
|
|
1304
1338
|
const partResolved = resolveNode(part, scheme, false);
|
|
1305
|
-
const partVariantClasses = partAxes.flatMap((a) => a.options.flatMap((o) => blockToTailwind(partResolved.variants[a.key]?.[o] ?? {}, `data-[${a.attr}=${o}]:`)));
|
|
1339
|
+
const partVariantClasses = partAxes.flatMap((a) => a.options.flatMap((o) => blockToTailwind(partResolved.variants[a.key]?.[o] ?? {}, themeVars, `data-[${a.attr}=${o}]:`)));
|
|
1306
1340
|
const partCls = [
|
|
1307
|
-
tailwindClassList(part, partResolved),
|
|
1341
|
+
tailwindClassList(part, partResolved, themeVars),
|
|
1308
1342
|
...partVariantClasses,
|
|
1309
1343
|
]
|
|
1310
1344
|
.filter(Boolean)
|
|
@@ -1398,7 +1432,16 @@ scheme,
|
|
|
1398
1432
|
* Aqui a tradução acontece uma vez, sobre os bytes que vão a disco, e `tsc` recusa a meia-chamada
|
|
1399
1433
|
* de um materializador novo - o mesmo argumento do `scheme` logo acima.
|
|
1400
1434
|
*/
|
|
1401
|
-
tongue
|
|
1435
|
+
tongue,
|
|
1436
|
+
/**
|
|
1437
|
+
* O QUE A FOLHA INSTALADA DECLARA NO `@theme` - `installedThemeVars(root, slug)`.
|
|
1438
|
+
*
|
|
1439
|
+
* OBRIGATÓRIO, pelo mesmo argumento do `scheme` e do `tongue`: o utilitário nomeado que este
|
|
1440
|
+
* módulo escreve só carrega o valor DO SISTEMA quando a folha que caiu na pasta dele declara a
|
|
1441
|
+
* variável. Sem essa resposta o codegen escreve `rounded-lg` e a classe pinta o default do
|
|
1442
|
+
* Tailwind - o componente mente sem nada faltar na tela. `null` é projeto sem folha instalada.
|
|
1443
|
+
*/
|
|
1444
|
+
themeVars) {
|
|
1402
1445
|
const files = [];
|
|
1403
1446
|
const props = propsTypeName(reactMajor);
|
|
1404
1447
|
if (styles === "css") {
|
|
@@ -1411,7 +1454,7 @@ tongue) {
|
|
|
1411
1454
|
else {
|
|
1412
1455
|
files.push({
|
|
1413
1456
|
filename: `${localName}.tsx`,
|
|
1414
|
-
code: `${emitTailwindMode(slug, name, recipe, version, props, localName, scheme)}\n`,
|
|
1457
|
+
code: `${emitTailwindMode(slug, name, recipe, version, props, localName, scheme, themeVars)}\n`,
|
|
1415
1458
|
});
|
|
1416
1459
|
}
|
|
1417
1460
|
files.push({
|
|
@@ -104,5 +104,5 @@ register("pt-BR", {
|
|
|
104
104
|
"the import, orchestrated": "o import, orquestrado",
|
|
105
105
|
"one component against the system": "um componente contra o sistema",
|
|
106
106
|
"build what the system does not have": "construir o que o sistema não tem",
|
|
107
|
-
"the tokens,
|
|
107
|
+
"the tokens, loaded by your app": "os tokens, carregados pelo seu app",
|
|
108
108
|
});
|
package/dist/global-sheet.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, posix, relative, resolve } from "node:path";
|
|
3
|
+
import { exists } from "./agent-wiring.js";
|
|
3
4
|
/**
|
|
4
5
|
* A FOLHA GLOBAL QUE OS APPS DELE REALMENTE CARREGAM - e num monorepo ela quase nunca é a do app.
|
|
5
6
|
*
|
|
@@ -101,3 +102,51 @@ export function prefixFrom(sheet) {
|
|
|
101
102
|
const up = relative(dirname(resolve("/r", sheet)), "/r");
|
|
102
103
|
return up === "" ? "./" : `${up.split(/[\\/]/).join("/")}/`;
|
|
103
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* A RAIZ DE CADA APP DESTE REPOSITÓRIO - onde o escopo e a tipografia entram, um por app.
|
|
107
|
+
*
|
|
108
|
+
* MORA AQUI E NÃO NO `add` (07/09): o `doctor` precisa desta resposta para nomear a folha no texto
|
|
109
|
+
* que ele entrega, e importá-la de `commands/add.ts` puxou a rede para dentro de um comando local -
|
|
110
|
+
* `INV-COLETA-05` reprovou, e com razão. A pergunta *"quais pastas deste repositório são app?"* é
|
|
111
|
+
* leitura de disco e não tem nada a ver com publicar nem buscar: ela pertence ao módulo da folha.
|
|
112
|
+
*
|
|
113
|
+
* O que faz uma pasta ser app é o `layout` do App Router morar dentro dela. Um workspace sem
|
|
114
|
+
* `layout` (uma API, um pacote de config) não é app e não recebe escopo.
|
|
115
|
+
*/
|
|
116
|
+
export async function detectAppDirs(root, pagesDir) {
|
|
117
|
+
const isAppRoot = async (dir) => {
|
|
118
|
+
if (!(await exists(join(root, dir))))
|
|
119
|
+
return false;
|
|
120
|
+
for (const ext of ["tsx", "jsx", "ts", "js"])
|
|
121
|
+
if (await exists(join(root, dir, `layout.${ext}`)))
|
|
122
|
+
return true;
|
|
123
|
+
return false;
|
|
124
|
+
};
|
|
125
|
+
const here = [pagesDir, `src/${pagesDir}`];
|
|
126
|
+
const nested = [];
|
|
127
|
+
for (const group of ["apps", "packages"]) {
|
|
128
|
+
let entries;
|
|
129
|
+
try {
|
|
130
|
+
entries = await readdir(join(root, group));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
for (const entry of entries.sort())
|
|
136
|
+
nested.push(`${group}/${entry}/${pagesDir}`, `${group}/${entry}/src/${pagesDir}`);
|
|
137
|
+
}
|
|
138
|
+
const found = [];
|
|
139
|
+
for (const dir of [...here, ...nested])
|
|
140
|
+
if (await isAppRoot(dir))
|
|
141
|
+
found.push(dir);
|
|
142
|
+
/**
|
|
143
|
+
* A RAIZ QUE EXISTE MAS NÃO TEM LAYOUT ainda é o melhor palpite de um app avulso - é o caso de um
|
|
144
|
+
* `create-next-app` no meio de uma migração. Sem isto, um projeto de app único perderia a detecção
|
|
145
|
+
* que já funcionava.
|
|
146
|
+
*/
|
|
147
|
+
if (found.length === 0)
|
|
148
|
+
for (const dir of here)
|
|
149
|
+
if (await exists(join(root, dir)))
|
|
150
|
+
return [dir];
|
|
151
|
+
return found;
|
|
152
|
+
}
|
package/dist/install-marks.js
CHANGED
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
* O que o cliente ganha ao rodar `upgrade`: o agente dele no Codex passa a poder PERGUNTAR ao
|
|
171
171
|
* sistema, em vez de só receber as regras e adivinhar o resto.
|
|
172
172
|
*/
|
|
173
|
-
export const MATERIALISER_SINCE = "0.16.
|
|
173
|
+
export const MATERIALISER_SINCE = "0.16.390";
|
|
174
174
|
/**
|
|
175
175
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
176
176
|
*
|
package/dist/project-facts.js
CHANGED
|
@@ -171,3 +171,43 @@ export async function readInstalledScheme(root, slug) {
|
|
|
171
171
|
return "dark";
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* O QUE A FOLHA INSTALADA DECLARA NO `@theme` - a resposta que decide de quem é o VALOR.
|
|
176
|
+
*
|
|
177
|
+
* O QUE O CLIENTE PERCEBE SEM ISTO: o componente sai vestindo `rounded-lg` e `font-mono`, a classe
|
|
178
|
+
* existe em qualquer projeto com Tailwind, e ela pinta o **default do Tailwind** em vez do raio e da
|
|
179
|
+
* fonte do sistema dele. Nada falta na tela - é a mesma peça com os números de outra biblioteca, o
|
|
180
|
+
* que é mais difícil de achar que uma classe morta.
|
|
181
|
+
*
|
|
182
|
+
* A folha deixa de declarar por dois motivos legítimos: o alinhamento tirou a linha para não roubar
|
|
183
|
+
* uma classe que o app dele já usa (`INV-VOLTA-13`), ou o compilador não faz ponte para um nome que
|
|
184
|
+
* não está em `meta.ownedNames`. Os dois se leem no mesmo lugar - o arquivo que caiu na pasta dele -
|
|
185
|
+
* e é por isso que a pergunta é respondida AQUI e não no servidor: só nesta máquina existe a folha
|
|
186
|
+
* que ele realmente tem.
|
|
187
|
+
*
|
|
188
|
+
* MEDIDO em 07/09 sobre todos os componentes: 58 de 110 utilitários de token no `codelevel` (53%) e
|
|
189
|
+
* 34 de 241 no `ember` (14%) não carregavam o valor do sistema.
|
|
190
|
+
*
|
|
191
|
+
* `null` quando não há folha - projeto sem `add`, ou install antigo. Nesse caso nada pinta de
|
|
192
|
+
* qualquer forma, e o `refit`/`generate` já dizem isso.
|
|
193
|
+
*/
|
|
194
|
+
export async function installedThemeVars(root, slug) {
|
|
195
|
+
const dir = join(root, "_synthesisui", "ds", slug);
|
|
196
|
+
const version = await pinnedVersion(dir);
|
|
197
|
+
if (!version)
|
|
198
|
+
return null;
|
|
199
|
+
const css = await readFile(join(dir, `v${version}`, "theme.css"), "utf8").catch(() => "");
|
|
200
|
+
if (!css)
|
|
201
|
+
return null;
|
|
202
|
+
/**
|
|
203
|
+
* Só o que está DENTRO de um `@theme` conta - é a única coisa que o Tailwind v4 lê para gerar
|
|
204
|
+
* utilitário. Uma variável no `[data-ds]` do `tokens.css` não gera nada, e foi tratar as duas como
|
|
205
|
+
* iguais que fez o `theme.css` parecer suficiente quando não era.
|
|
206
|
+
*/
|
|
207
|
+
const blocks = css.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g);
|
|
208
|
+
const declared = new Set();
|
|
209
|
+
for (const block of blocks)
|
|
210
|
+
for (const m of block[1].matchAll(/^\s*(--[a-zA-Z0-9-]+)\s*:/gm))
|
|
211
|
+
declared.add(m[1]);
|
|
212
|
+
return declared;
|
|
213
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O TEXTO QUE TIRA AS EDIÇÕES DE SETUP DA MÃO DA PESSOA - e que agora USA a medição.
|
|
3
|
+
*
|
|
4
|
+
* O QUE O CLIENTE GANHA: um design system instalado não muda um pixel até que alguma folha que o app
|
|
5
|
+
* carrega importe os tokens e algum elemento carregue o escopo. Em 27/07 isso foi medido em 0% - a
|
|
6
|
+
* lista numerada de três passos existia e ninguém a executava. A pessoa que roda o comando tem um
|
|
7
|
+
* agente aberto ao lado, então o fecho deixou de ser tarefa e passou a ser um texto para colar.
|
|
8
|
+
*
|
|
9
|
+
* O DEFEITO QUE ISTO CORRIGE (dono, 07/09): o texto era FIXO. O `doctor` acabava de imprimir
|
|
10
|
+
* `✗ ✓ ✓` - só o import faltava, o escopo e a tipografia já estavam lá - e o parágrafo seguinte
|
|
11
|
+
* mandava *"Do the ONE-TIME SETUP it names, all of it"*, listando as três. Um agente obediente
|
|
12
|
+
* reescreve o que já estava correto, e o próprio texto termina com "não mexa nos meus estilos": ele
|
|
13
|
+
* se contradizia dentro de si mesmo.
|
|
14
|
+
*
|
|
15
|
+
* Então o que falta é ARGUMENTO. Sem ele o texto pede tudo, que é a verdade nos dois lugares onde
|
|
16
|
+
* ninguém mediu nada: o fecho do `init` (o projeto acabou de receber o sistema) e a tela do
|
|
17
|
+
* onboarding (a plataforma não vê o disco de ninguém). Com ele, o texto pede o que falta e NOMEIA o
|
|
18
|
+
* que já está pronto para o agente deixar em paz.
|
|
19
|
+
*
|
|
20
|
+
* GÊMEO POR SPEC com `apps/web/src/lib/ds/install-mission.ts`, que serve o mesmo texto na tela -
|
|
21
|
+
* `setup-prompt.spec.ts` reprova o drift. O CLI é publicado standalone e não pode importar de
|
|
22
|
+
* `apps/web`, então a cópia é inevitável; o que não é inevitável é ela divergir em silêncio, e dois
|
|
23
|
+
* textos diferentes para o mesmo setup ensinam dois setups.
|
|
24
|
+
*/
|
|
25
|
+
export function initCommand(slug) {
|
|
26
|
+
return `npx synthesisui@latest init --styles tailwind --ds ${slug}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A LINHA QUE DÁ SAÍDA QUANDO UMA VERSÃO ESTÁ PROPAGANDO (dono, 07/09).
|
|
30
|
+
*
|
|
31
|
+
* Ele rodou `npx synthesisui@latest doctor` e recebeu `ETARGET / No matching version found`: o
|
|
32
|
+
* `dist-tag` do npm já apontava para a versão nova e o tarball ainda não estava na réplica que a
|
|
33
|
+
* máquina dele consultou. O erro é do npm e acontece ANTES do nosso código rodar, então não há como
|
|
34
|
+
* interceptá-lo - o que está na nossa mão é a instrução não deixar quem lê sem saída, porque
|
|
35
|
+
* `ETARGET` não diz que é transitório nem o que fazer.
|
|
36
|
+
*/
|
|
37
|
+
const PROPAGATING = "If npm answers `ETARGET` or `No matching version found`, a release is still propagating - wait a minute and run it again, or pin the version the registry does have (`npm view synthesisui version`).";
|
|
38
|
+
const CONNECT_STEP = `Run: npx synthesisui@latest connect
|
|
39
|
+
${PROPAGATING}
|
|
40
|
+
If it answers "already had it" on both lines, everything is live and you are done.
|
|
41
|
+
If it WROTE either of them, then they are not live in this session, because hooks and tools are only read at startup. Stop there and tell me, in bold, on its own line: **RESTART THIS SESSION - the check is installed but not running yet.** Do not keep writing files after that; a session that cannot be checked is the state this whole setup exists to avoid.`;
|
|
42
|
+
/** As linhas exatas quando a folha é conhecida; a descrição do arquivo quando não é. */
|
|
43
|
+
function importStep(slug, gap) {
|
|
44
|
+
const lines = [
|
|
45
|
+
`@import "${gap.sheet ? gap.sheet.prefix : "<path to the repo root>/"}_synthesisui/ds/${slug}/tokens.css";`,
|
|
46
|
+
...(gap.imports === 2
|
|
47
|
+
? [
|
|
48
|
+
`@import "${gap.sheet ? gap.sheet.prefix : "<path to the repo root>/"}_synthesisui/ds/${slug}/theme.css";`,
|
|
49
|
+
]
|
|
50
|
+
: []),
|
|
51
|
+
];
|
|
52
|
+
const where = gap.sheet
|
|
53
|
+
? `\`${gap.sheet.path}\``
|
|
54
|
+
: "the global stylesheet this project's apps actually load (in a monorepo that is usually the shared package's, not the app's)";
|
|
55
|
+
return `Add ${gap.imports === 2 ? "these two lines" : "this line"} to ${where}, RIGHT AFTER its \`@import "tailwindcss"\` line:
|
|
56
|
+
|
|
57
|
+
${lines.map((l) => ` ${l}`).join("\n")}
|
|
58
|
+
|
|
59
|
+
The position is not cosmetic. Read there, this sheet comes BEFORE any \`@theme\` this project declares, so wherever both name the same variable the project's own value wins. Moved to the end of the file, ours would win instead - silently.`;
|
|
60
|
+
}
|
|
61
|
+
export function setupPrompt(slug, gap) {
|
|
62
|
+
const g = gap ?? {
|
|
63
|
+
tokens: true,
|
|
64
|
+
scope: true,
|
|
65
|
+
type: true,
|
|
66
|
+
imports: 2,
|
|
67
|
+
};
|
|
68
|
+
const done = [
|
|
69
|
+
g.tokens ? null : "a stylesheet already imports the system's tokens",
|
|
70
|
+
g.scope ? null : `\`data-ds="${slug}"\` is already on a root element`,
|
|
71
|
+
g.type
|
|
72
|
+
? null
|
|
73
|
+
: "this project's type is already mapped onto the system's family tokens",
|
|
74
|
+
].filter(Boolean);
|
|
75
|
+
const steps = [];
|
|
76
|
+
if (g.tokens)
|
|
77
|
+
steps.push(importStep(slug, g));
|
|
78
|
+
if (g.scope)
|
|
79
|
+
steps.push(`Put \`data-ds="${slug}"\` on a root element of each app - the element every page renders inside. Keep any classes it already has; a background they chose is a decision they made.`);
|
|
80
|
+
if (g.type)
|
|
81
|
+
steps.push(`Map this project's type onto the system's family tokens: import from the fonts file the install wrote and point the family variables at it, in the same stylesheet. This is the step people skip, and without it the pages render in the framework's default face.`);
|
|
82
|
+
steps.push(`Run: npx synthesisui@latest doctor
|
|
83
|
+
${PROPAGATING}
|
|
84
|
+
If it says "no system installed", the install never happened - run \`${initCommand(slug)}\` and then start over from step 1.
|
|
85
|
+
It must NOT say "this project does not load it yet". If it does, it names exactly which piece is still missing - fix that one and run it again.`);
|
|
86
|
+
steps.push(CONNECT_STEP);
|
|
87
|
+
const alreadyDone = done.length > 0
|
|
88
|
+
? `\nAlready in place - LEAVE THESE ALONE:\n${done.map((d) => `- ${d}`).join("\n")}\n`
|
|
89
|
+
: "";
|
|
90
|
+
return `Set up the "${slug}" design system in this project, so its tokens reach the browser. I already ran the install in my terminal.
|
|
91
|
+
${alreadyDone}
|
|
92
|
+
${steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}
|
|
93
|
+
|
|
94
|
+
Do not change any of my existing styles.`;
|
|
95
|
+
}
|
package/dist/skill-configure.js
CHANGED
|
@@ -10,4 +10,4 @@
|
|
|
10
10
|
* e essa era a única parte da instalação que ninguém escrevia, só imprimia.
|
|
11
11
|
*/
|
|
12
12
|
export const CONFIGURE_SKILL_PATH = ".claude/skills/sui-configure-ds/SKILL.md";
|
|
13
|
-
export const CONFIGURE_SKILL = '---\nname: sui-configure-ds\ndescription:
|
|
13
|
+
export const CONFIGURE_SKILL = '---\nname: sui-configure-ds\ndescription: Set up an installed design system in the app so its tokens actually reach the browser - the stylesheet import, the data-ds scope, and the type. Use when a system is installed and the app does not look themed, when `doctor` reports "installed - but this project does not load it yet", or when somebody asks to set up / connect / configure the design system in their app ("configura o design system aqui", "why aren\'t the tokens working", "/sui-configure-ds"). Asks the doctor first and does nothing when the three checks already pass.\n---\n\n# Setting the system up in the app - served live\n\nThis playbook is served from the platform, not shipped in this file - it is\nalways current, and your context only carries the step you are on.\n\n1. Call the `playbook` tool on the `synthesisui` MCP server with\n { "skill": "configure" } - you get the framing and a table of contents.\n2. Fetch ONLY the chapter for the step you are on:\n { "skill": "configure", "section": "<id from the toc>" }. Never fetch more\n than the current step needs.\n3. Follow it exactly. When the step is done, fetch the next chapter.\n\nStart by running `npx synthesisui doctor`: the three lines it prints are what\ndecide the work, and three ticks means there is nothing to do.\n\nIf the tool answers that you are not signed in, run `npx synthesisui login`\nin the terminal and call it again. If the `synthesisui` MCP server is not\navailable at all, run `npx synthesisui connect`, restart the session, and\ninvoke this skill again.\n';
|
package/dist/skills.js
CHANGED
package/package.json
CHANGED
package/dist/wiring-prompt.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* O PASTE QUE TIRA AS QUATRO EDIÇÕES DA MÃO DA PESSOA.
|
|
3
|
-
*
|
|
4
|
-
* `init --ds` termina imprimindo uma seção "One-time setup" com três passos
|
|
5
|
-
* numerados para o humano fazer à mão: os dois `@import`, o `data-ds` na raiz
|
|
6
|
-
* e a fiação das fontes. Foi exatamente aí que 27/07 mediu 0% de fiação -
|
|
7
|
-
* ninguém fazia, e o sistema que acabara de chegar não aparecia na tela.
|
|
8
|
-
*
|
|
9
|
-
* A pessoa que roda esse comando tem um agente aberto ao lado. Então o fecho
|
|
10
|
-
* deixa de ser uma lista de tarefas e passa a ser uma coisa para colar.
|
|
11
|
-
*
|
|
12
|
-
* GÊMEO POR SPEC com `apps/web/src/lib/ds/install-mission.ts`, que serve o
|
|
13
|
-
* mesmo texto na tela do onboarding - `wiring-prompt.spec.ts` reprova o drift.
|
|
14
|
-
* O CLI é publicado standalone e não pode importar de `apps/web`, então a
|
|
15
|
-
* cópia é inevitável; o que não é inevitável é ela divergir em silêncio, e um
|
|
16
|
-
* prompt que diverge entre a tela e o terminal ensina duas fiações diferentes
|
|
17
|
-
* para o mesmo sistema.
|
|
18
|
-
*/
|
|
19
|
-
export function initCommand(slug) {
|
|
20
|
-
return `npx synthesisui@latest init --styles tailwind --ds ${slug}`;
|
|
21
|
-
}
|
|
22
|
-
export function wiringPrompt(slug) {
|
|
23
|
-
return `Wire the "${slug}" design system into this project. I already ran the install in my terminal.
|
|
24
|
-
|
|
25
|
-
1. Run: npx synthesisui@latest doctor
|
|
26
|
-
If it says "no system installed", I skipped the install - run ${initCommand(slug)} first, then carry on.
|
|
27
|
-
2. Do the ONE-TIME SETUP it names, all of it: the two @import lines in the project's global stylesheet (the path is relative to that file), data-ds="${slug}" on the root element, and the font wiring - importing from the fonts file it writes and mapping those variables in the stylesheet. The type is the step people skip.
|
|
28
|
-
3. Run doctor again. It must NOT say "not wired up yet". If it does, it names exactly which piece is missing - fix that and run it again.
|
|
29
|
-
4. Run: npx synthesisui@latest connect
|
|
30
|
-
If it answers "already had it" on both lines, everything is live and you are done.
|
|
31
|
-
If it WROTE either of them, then they are not live in this session, because hooks and tools are only read at startup. Stop there and tell me, in bold, on its own line: **RESTART THIS SESSION - the check is installed but not running yet.** Do not keep writing files after that; a session that cannot be checked is the state this whole setup exists to avoid.
|
|
32
|
-
|
|
33
|
-
Do not change any of my existing styles.`;
|
|
34
|
-
}
|