synthesisui 0.16.479 → 0.16.481

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.
@@ -0,0 +1,206 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { blockMarks, readVocabulary } from "./new-project-theme.js";
4
+ /**
5
+ * A VOLTA PARA O CÓDIGO - etapa 05 da jornada "o projeto sem cor escolhe as suas", 28/09.
6
+ *
7
+ * Depois do "Use these colours", ele sai com UM comando e UM prompt. O comando escreve os seis nomes no CSS global
8
+ * dele, NA GRAFIA DELE - o prefixo que o projeto já usa, ou uma neutra quando não usa nenhum -, e pergunta uma vez
9
+ * antes. O prompt manda o agente trocar os valores escritos à mão pelos nomes. As cores não são do agente: foram
10
+ * decididas na tela.
11
+ */
12
+ export const SIX_ROLES = [
13
+ "primary",
14
+ "canvas",
15
+ "surface",
16
+ "foreground",
17
+ "muted",
18
+ "border",
19
+ ];
20
+ /** A palavra de cada papel no código dele - a que ele escreveria, e não o nosso identificador. */
21
+ const WORD = {
22
+ primary: { word: "primary", job: "buttons, links, what you click" },
23
+ canvas: { word: "background", job: "the page itself" },
24
+ surface: { word: "surface", job: "cards and panels" },
25
+ foreground: { word: "text", job: "what people read" },
26
+ muted: { word: "text-muted", job: "hints, captions, labels" },
27
+ border: { word: "border", job: "lines and edges" },
28
+ };
29
+ /**
30
+ * O PREFIXO DELE: quando a maioria dos nomes que ele declara começa pelo mesmo segmento (`--fn-space-1`,
31
+ * `--fn-radius-s`), é ali que os nomes novos entram (`--fn-primary`). Sem nome nenhum, ou sem um segmento que a
32
+ * maioria divida, a grafia é a neutra `--color-*` - a mesma que o `init` escreve num projeto novo.
33
+ */
34
+ function prefixOf(own) {
35
+ if (own.length < 2)
36
+ return "--color-";
37
+ const counts = new Map();
38
+ for (const n of own) {
39
+ const m = /^--([a-z0-9]+)-/i.exec(n);
40
+ if (m)
41
+ counts.set(m[1], (counts.get(m[1]) ?? 0) + 1);
42
+ }
43
+ const [top, n] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? ["", 0];
44
+ const generic = new Set([
45
+ "color",
46
+ "colors",
47
+ "space",
48
+ "spacing",
49
+ "radius",
50
+ "font",
51
+ "text",
52
+ "shadow",
53
+ ]);
54
+ return n > own.length / 2 && !generic.has(top) ? `--${top}-` : "--color-";
55
+ }
56
+ /** O que cada papel pinta - o uso em que a troca vale. A principal vale em qualquer um. */
57
+ const KIND = {
58
+ primary: "any",
59
+ canvas: "background",
60
+ surface: "background",
61
+ foreground: "text",
62
+ muted: "text",
63
+ border: "border",
64
+ };
65
+ const kindOf = (prop) => /^(background|bg)/i.test(prop)
66
+ ? "background"
67
+ : /^color$/i.test(prop)
68
+ ? "text"
69
+ : /(border|outline)/i.test(prop)
70
+ ? "border"
71
+ : "other";
72
+ const SKIP_DIRS = new Set([
73
+ "node_modules",
74
+ ".next",
75
+ "dist",
76
+ "build",
77
+ "out",
78
+ "_synthesisui",
79
+ ".git",
80
+ "coverage",
81
+ ]);
82
+ /** As cores escritas à mão no código dele, com a propriedade que as pinta - CSS e objetos de estilo. */
83
+ async function usesIn(root, slug) {
84
+ const { readdir } = await import("node:fs/promises");
85
+ const { relative } = await import("node:path");
86
+ const out = [];
87
+ const walk = async (dir) => {
88
+ for (const e of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
89
+ if (e.name.startsWith(".") || SKIP_DIRS.has(e.name))
90
+ continue;
91
+ const path = join(dir, e.name);
92
+ if (e.isDirectory())
93
+ await walk(path);
94
+ else if (/\.(css|scss|tsx|jsx)$/i.test(e.name)) {
95
+ const src = (await readFile(path, "utf8").catch(() => "")).replace(blockMarks.END_RE(slug), "\n");
96
+ const file = relative(root, path).split("\\").join("/");
97
+ for (const m of src.matchAll(/([a-zA-Z-]+)\s*:\s*["']?[^;{}\n"']*?(#[0-9a-fA-F]{6})\b/g))
98
+ if (!m[1].startsWith("--"))
99
+ out.push({
100
+ hex: m[2].toLowerCase(),
101
+ kind: kindOf(m[1]),
102
+ file,
103
+ });
104
+ }
105
+ }
106
+ };
107
+ await walk(root);
108
+ return out;
109
+ }
110
+ export async function planColourNames(root, slug, roleHex,
111
+ /** As cores de cada papel ANTES da escolha - a versão instalada antes do `add`. Sem ela, vale a cor nova. */
112
+ before = {}) {
113
+ const v = await readVocabulary(root, slug);
114
+ const prefix = prefixOf([...v.own.keys()]);
115
+ const uses = await usesIn(root, slug);
116
+ const low = (x) => (x ?? "").trim().toLowerCase();
117
+ const taken = new Set();
118
+ const names = SIX_ROLES.filter((r) => roleHex[r]).map((role) => {
119
+ const value = roleHex[role];
120
+ const old = low(before[role] ?? value);
121
+ /**
122
+ * O NOME DELE PRIMEIRO - o b1 declara `--accent: #c2410c`, e `--color-primary` com o mesmo valor ao lado seria o
123
+ * vocabulário dele duplicado pelo nosso. Vale o nome que segura a cor de hoje, ou a nova.
124
+ */
125
+ const his = [...v.own].find(([n, val]) => !taken.has(n) && (low(val) === old || low(val) === low(value)))?.[0];
126
+ const name = his ?? `${prefix}${WORD[role].word}`;
127
+ taken.add(name);
128
+ const have = v.own.get(name);
129
+ const kind = KIND[role];
130
+ const painted = uses.some((u) => u.hex === old && (kind === "any" || u.kind === kind));
131
+ return {
132
+ role,
133
+ name,
134
+ value,
135
+ job: WORD[role].job,
136
+ exists: have != null && low(have) === low(value),
137
+ theirs: his != null,
138
+ was: painted ? old : null,
139
+ };
140
+ });
141
+ return { file: v.global, prefix, names, css: v.css, uses };
142
+ }
143
+ /** Escreve o bloco depois do sim dele - `ask` devolve a resposta; com o não, o arquivo fica como estava. */
144
+ export async function writeColourNames(root, slug, plan, ask) {
145
+ if (!plan.file)
146
+ return { written: false, reason: "no-file" };
147
+ const fresh = plan.names.filter((n) => !n.exists);
148
+ if (fresh.length === 0)
149
+ return { written: false, reason: "nothing-new" };
150
+ if (!(await ask()))
151
+ return { written: false, reason: "declined" };
152
+ const { begin, end, open, END_RE } = blockMarks;
153
+ const path = join(root, plan.file);
154
+ const css = await readFile(path, "utf8");
155
+ const block = [
156
+ begin(slug),
157
+ open(`${slug}: the six colours you chose, under names your code can use - var(${fresh[0]?.name}). They are yours.`),
158
+ ":root {",
159
+ ...fresh.map((n) => ` ${n.name}: ${n.value};`),
160
+ "}",
161
+ end(slug),
162
+ ].join("\n");
163
+ const base = css.replace(END_RE(slug), "\n").replace(/\n*$/, "\n");
164
+ await writeFile(path, `${base}\n${block}\n`, "utf8");
165
+ return { written: true };
166
+ }
167
+ /**
168
+ * O PROMPT PRONTO - o que ele cola no agente.
169
+ *
170
+ * A TROCA É PELA COR DE HOJE, e não pela nova: pela galeria, a cor nova não existe no código dele (medido em 28/09 com
171
+ * a Onyx no e06-galeria), e o que precisa mudar é o teal que ele escreve à mão. E É PELO USO: o branco do b2 é o fundo
172
+ * do cartão e o texto do botão ao mesmo tempo, e só o primeiro é a superfície.
173
+ */
174
+ export function agentPrompt(plan) {
175
+ const where = {
176
+ any: "",
177
+ background: "as a background - ",
178
+ text: "as text - ",
179
+ border: "as a border - ",
180
+ other: "",
181
+ };
182
+ const lines = plan.names
183
+ .filter((n) => n.was)
184
+ .map((n) => ` ${n.was} -> var(${n.name}) (${where[KIND[n.role]]}${n.job})`);
185
+ const leave = [];
186
+ for (const n of plan.names) {
187
+ if (!n.was || KIND[n.role] === "any")
188
+ continue;
189
+ const claimed = new Set(plan.names.filter((m) => m.was === n.was).map((m) => KIND[m.role]));
190
+ const other = plan.uses.filter((u) => u.hex === n.was && !claimed.has(u.kind) && u.kind !== "other");
191
+ for (const kind of new Set(other.map((u) => u.kind))) {
192
+ const files = [
193
+ ...new Set(other.filter((u) => u.kind === kind).map((u) => u.file)),
194
+ ];
195
+ leave.push(` leave ${n.was} where it is ${kind} (${files.join(", ")}) - that is not the ${WORD[n.role].word}`);
196
+ }
197
+ }
198
+ return [
199
+ `Replace the colours written by hand in this project with the names declared in ${plan.file ?? "the global stylesheet"}:`,
200
+ ...(lines.length > 0
201
+ ? lines
202
+ : [" (your code writes none of these by hand - nothing to replace)"]),
203
+ ...leave,
204
+ "Change a value only where it is used as the line says. Leave every other colour as it is, and do not add or invent a name or a colour - these six were chosen by the person.",
205
+ ].join("\n");
206
+ }
@@ -0,0 +1,113 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { agentPrompt, planColourNames, SIX_ROLES, writeColourNames, } from "../colour-names.js";
4
+ import { body, section } from "../output.js";
5
+ import { RegistryError } from "../registry.js";
6
+ import { add } from "./add.js";
7
+ /**
8
+ * `synthesisui colours` - A VOLTA PARA O CÓDIGO, etapa 05 da jornada "o projeto sem cor escolhe as suas", 28/09.
9
+ *
10
+ * O comando que a tela de escolher cores entrega depois do "Use these colours". Ele traz a versão nova do sistema,
11
+ * mostra as seis cores com os nomes que vão entrar no CSS global dele, PERGUNTA UMA VEZ, escreve - e imprime o prompt
12
+ * que manda o agente trocar os valores escritos à mão pelos nomes. Com o não, nada muda no arquivo.
13
+ */
14
+ export async function colours(asked, opts) {
15
+ const root = opts.dir ?? process.cwd();
16
+ const slug = asked ?? (await onlyInstalled(root));
17
+ const slugDir = join(root, "_synthesisui", "ds", slug);
18
+ /**
19
+ * AS CORES DE ANTES, lidas antes do `add` - é o que o código dele pinta hoje. Pela galeria, a cor nova não existe no
20
+ * código dele, e a troca que o prompt pede é da antiga (etapa 01 de "as cores voltam certas", 28/09).
21
+ */
22
+ const before = await installedRoles(slugDir);
23
+ /** A versão publicada pelo "Use these colours" - a instalada aqui ainda é a de antes. */
24
+ await add(slug, { registry: opts.registry, dir: root, setupHints: false });
25
+ const lock = JSON.parse(await readFile(join(slugDir, ".lock"), "utf8").catch(() => "{}"));
26
+ const tokens = typeof lock.version === "number"
27
+ ? await readFile(join(slugDir, `v${lock.version}`, "tokens.css"), "utf8").catch(() => "")
28
+ : "";
29
+ const roleHex = roleColours(tokens);
30
+ const plan = await planColourNames(root, slug, roleHex, before);
31
+ console.log("");
32
+ console.log(section("Your six colours, back in your code"));
33
+ if (!plan.file) {
34
+ console.log(body("No global stylesheet was found here (a globals.css, or a single stylesheet that is not a module) - nothing was written. The prompt below still works: point your agent at the file where your colours should live."));
35
+ }
36
+ else {
37
+ console.log(body(`These go into ${plan.file}, under names your code can use:`));
38
+ for (const n of plan.names)
39
+ console.log(body(` ${n.name}: ${n.value} ${n.exists ? "(already there, your name)" : n.theirs ? `(your name, new value) - ${n.job}` : `- ${n.job}`}`));
40
+ }
41
+ const result = await writeColourNames(root, slug, plan, () => opts.yes
42
+ ? Promise.resolve(true)
43
+ : askOnce(` Write them into ${plan.file}? [y/N] `));
44
+ console.log("");
45
+ if (result.written)
46
+ console.log(body(`✓ ${plan.file}: the six colours are declared, in your names.`));
47
+ else if (result.reason === "declined")
48
+ console.log(body(`Nothing was written - ${plan.file} is as it was. Run this again with --yes when you want them there.`));
49
+ else if (result.reason === "nothing-new")
50
+ console.log(body(`${plan.file} already declares all six with these values - nothing to write.`));
51
+ console.log("");
52
+ console.log(section("Paste this to your agent"));
53
+ for (const line of agentPrompt(plan).split("\n"))
54
+ console.log(body(line));
55
+ }
56
+ /** As seis cores do sistema, pelos papéis - seguindo as referências da folha até o valor. */
57
+ export function roleColours(tokensCss) {
58
+ const base = /\[data-ds="[^"]+"\]\s*\{([^}]*)\}/.exec(tokensCss)?.[1] ?? tokensCss;
59
+ const decl = new Map();
60
+ for (const m of base.matchAll(/(--ds-[a-z0-9-]+)\s*:\s*([^;]+);/gi))
61
+ decl.set(m[1], m[2].trim());
62
+ const resolve = (value, seen = new Set()) => {
63
+ const ref = /^var\(\s*(--ds-[a-z0-9-]+)\s*\)$/i.exec(value);
64
+ if (!ref || seen.has(ref[1]))
65
+ return value;
66
+ seen.add(ref[1]);
67
+ const next = decl.get(ref[1]);
68
+ return next ? resolve(next, seen) : value;
69
+ };
70
+ const out = {};
71
+ for (const role of SIX_ROLES) {
72
+ const v = decl.get(`--ds-color-semantic-${role}`);
73
+ if (v) {
74
+ const hex = resolve(v);
75
+ if (!hex.includes("var("))
76
+ out[role] = hex;
77
+ }
78
+ }
79
+ return out;
80
+ }
81
+ /** As seis cores da versão que está instalada agora - `{}` quando ainda não há nenhuma. */
82
+ async function installedRoles(slugDir) {
83
+ const lock = JSON.parse(await readFile(join(slugDir, ".lock"), "utf8").catch(() => "{}"));
84
+ if (typeof lock.version !== "number")
85
+ return {};
86
+ return roleColours(await readFile(join(slugDir, `v${lock.version}`, "tokens.css"), "utf8").catch(() => ""));
87
+ }
88
+ async function onlyInstalled(root) {
89
+ const entries = await readdir(join(root, "_synthesisui", "ds"), {
90
+ withFileTypes: true,
91
+ }).catch(() => []);
92
+ const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
93
+ if (slugs.length === 1)
94
+ return slugs[0];
95
+ if (slugs.length === 0)
96
+ throw new RegistryError("No design system is installed here - run `synthesisui add <slug>` first.");
97
+ throw new RegistryError(`More than one system is installed here (${slugs.join(", ")}) - name the one: \`synthesisui colours <slug>\`.`);
98
+ }
99
+ async function askOnce(question) {
100
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
101
+ return false;
102
+ const { createInterface } = await import("node:readline/promises");
103
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
104
+ try {
105
+ return /^y(es)?$/i.test((await rl.question(question)).trim());
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ finally {
111
+ rl.close();
112
+ }
113
+ }
@@ -5209,6 +5209,8 @@ export async function runImport(opts) {
5209
5209
  // where the person is standing, and because a number they can check beats an
5210
5210
  // invitation they have to trust.
5211
5211
  const n = payload?.normalize;
5212
+ /** Com uma v2 para aprovar, o link é o lugar da decisão, e ele fica mesmo com a porta das cores. */
5213
+ const approveThere = Boolean(n?.decisions && n.decisions > 0);
5212
5214
  if (n?.decisions && n.decisions > 0) {
5213
5215
  console.log("");
5214
5216
  console.log(section("What a v2 would normalize"));
@@ -5223,11 +5225,15 @@ export async function runImport(opts) {
5223
5225
  console.log("");
5224
5226
  console.log(body("Nothing was changed. You approve it there:"));
5225
5227
  }
5226
- else if (payload?.url) {
5228
+ else if (payload?.url && !payload?.coloursDoor) {
5227
5229
  console.log("");
5228
5230
  console.log(body("Name it, see it, and govern it:"));
5229
5231
  }
5230
- if (payload?.url)
5232
+ /**
5233
+ * UM CONVITE SÓ (etapa 04 de "as cores voltam certas", 28/09): com a porta das cores, o próximo passo é escolher as
5234
+ * cores, e o link do tour competia com ele no fim da saída.
5235
+ */
5236
+ if (payload?.url && (approveThere || !payload?.coloursDoor))
5231
5237
  console.log(body(` ${paint.blue(payload.url)}`));
5232
5238
  console.log("");
5233
5239
  /**
@@ -4,7 +4,7 @@ import { resolveDeps, stylesFor, tailwindMajor } from "../stack.js";
4
4
  import { formMix, measuredStyle } from "../styles-flavour.js";
5
5
  import { readFile } from "node:fs/promises";
6
6
  import { join } from "node:path";
7
- import { readProjectTheme, writeBesideTheirs, writeNewProjectTheme } from "../new-project-theme.js";
7
+ import { readProjectTheme, writeBesideTheirs, writeNewProjectRoot, writeNewProjectTheme } from "../new-project-theme.js";
8
8
  import { add } from "./add.js";
9
9
  const whereOf = (answer) => answer === "file" || answer === "1"
10
10
  ? "file"
@@ -216,6 +216,22 @@ export async function init(opts) {
216
216
  }
217
217
  }
218
218
  }
219
+ /**
220
+ * E O PROJETO SEM TAILWIND TAMBÉM - etapa 05 da jornada "o projeto sem cor escolhe as suas" (28/09). No b10 da
221
+ * bateria o `init --ds` com CSS puro não escrevia nome nenhum, e o agente escreveu 79 valores crus. Mesma trava do
222
+ * caminho acima: só no projeto que ainda não nomeia nada seu.
223
+ */
224
+ if (project !== "tailwind") {
225
+ const slugDir = join(root, "_synthesisui", "ds", opts.ds);
226
+ const lock = JSON.parse(await readFile(join(slugDir, ".lock"), "utf8").catch(() => "{}"));
227
+ const version = typeof lock.version === "number" ? lock.version : null;
228
+ const tokens = version !== null ? await readFile(join(slugDir, `v${version}`, "tokens.css"), "utf8").catch(() => "") : "";
229
+ const theme = tokens && version !== null ? await writeNewProjectRoot(root, tokens, opts.ds, version) : null;
230
+ if (theme?.written) {
231
+ console.log("");
232
+ console.log(`✓ ${theme.written}: ${theme.count} names of ${opts.ds} declared in your own :root, with the values written - your project had none of its own. They are yours: var(--color-primary) and var(--spacing-md) now paint the system.`);
233
+ }
234
+ }
219
235
  /**
220
236
  * ERAM DOIS PASSOS E É UM - `INV-GERAL-13`, 11/09.
221
237
  *
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ import { summary } from "./commands/summary.js";
31
31
  import { sync } from "./commands/sync.js";
32
32
  import { template } from "./commands/template.js";
33
33
  import { upgrade } from "./commands/upgrade.js";
34
+ import { colours } from "./commands/colours.js";
34
35
  import { use } from "./commands/use.js";
35
36
  import { appendEvent } from "./doctor/ledger.js";
36
37
  import { blueprintTarget, installedSlugs } from "./installed.js";
@@ -57,6 +58,7 @@ Usage - deterministic, FREE:
57
58
  synthesisui template <slug> <name> materialize a whole page from a DS template
58
59
  (--as landing-home names the output - multi-page safe)
59
60
  synthesisui upgrade <slug> update an installed DS + regenerate your components + migration brief
61
+ synthesisui colours [slug] write the colours you chose on the site into your CSS, in your names (asks first)
60
62
  synthesisui use <slug> "<intent>" print a ready-to-paste agent prompt to build/modify on-system
61
63
  synthesisui clean [--force] strip create-next-app boilerplate (dry run without --force)
62
64
 
@@ -680,6 +682,17 @@ async function main() {
680
682
  });
681
683
  break;
682
684
  }
685
+ case "colours":
686
+ case "colors": {
687
+ /** Etapa 05 da jornada "o projeto sem cor escolhe as suas" (28/09): o comando que a tela de cores entrega. */
688
+ /** `--yes teste-b2` chega com o nome como valor da flag - ver `boolFlag`. */
689
+ await colours(args[0] ?? (typeof flags.yes === "string" ? flags.yes : undefined), {
690
+ registry,
691
+ dir,
692
+ yes: boolFlag(flags, "yes"),
693
+ });
694
+ break;
695
+ }
683
696
  case "upgrade": {
684
697
  /**
685
698
  * O SLUG É OPCIONAL desde 07/08: `upgrade` deixou de ser ocasional e virou O comando de
@@ -106,6 +106,70 @@ export async function writeNewProjectTheme(root, tokensCss, slug, version) {
106
106
  await writeFile(path, `${base}\n${block}\n`, "utf8");
107
107
  return { written: relative(root, path).split("\\").join("/"), count: lines.length };
108
108
  }
109
+ /**
110
+ * O VOCABULÁRIO DELE E O CSS GLOBAL, para quem escreve nomes fora do `init` - o `colours` (etapa 05, 28/09). A mesma
111
+ * leitura de `readTheirs`, com a mesma regra de qual folha é a global.
112
+ */
113
+ export async function readVocabulary(root, slug) {
114
+ const r = await readTheirs(root, slug);
115
+ const own = new Map([...r.own].map((n) => [n, r.values.get(n) ?? ""]));
116
+ const path = globalSheetOf(r.sheets);
117
+ return {
118
+ own,
119
+ global: path ? relative(root, path).split("\\").join("/") : null,
120
+ css: path ? (r.sheets.get(path) ?? "") : "",
121
+ };
122
+ }
123
+ function globalSheetOf(sheets) {
124
+ const globals = [...sheets.keys()].filter((p) => !/\.module\.css$/i.test(p));
125
+ const named = globals
126
+ .filter((p) => /(^|[\\/])globals\.css$/i.test(p))
127
+ .sort((a, b) => a.split(/[\\/]/).length - b.split(/[\\/]/).length);
128
+ return named[0] ?? (globals.length === 1 ? globals[0] : undefined);
129
+ }
130
+ /** Os marcadores do bloco, para o `colours` escrever e reescrever o seu no mesmo lugar. */
131
+ export const blockMarks = { begin, end, open, END_RE };
132
+ /**
133
+ * O MESMO TEMA NO PROJETO NOVO COM CSS PURO - etapa 05 da jornada "o projeto sem cor escolhe as suas", 28/09.
134
+ *
135
+ * O item 5 da triagem da bateria: no b10 (`init --ds` num Next sem Tailwind) nenhum nome foi escrito, e o agente
136
+ * escreveu 79 valores crus; com Tailwind (b11) o mesmo init escrevia 92 nomes no `@theme`. Aqui vai um `:root` no CSS
137
+ * global dele, com os MESMOS nomes (`--color-primary`, `--spacing-md`) e os valores escritos. Sem o `@theme` eles não
138
+ * viram utilitário - num projeto de CSS puro, o uso é `var(--color-primary)`, que é o que ele escreveria.
139
+ *
140
+ * A MESMA TRAVA: só no projeto que não nomeia nada próprio. O CSS global é o `globals.css` (o mais raso, se houver
141
+ * mais de um), ou a única folha que não é módulo.
142
+ */
143
+ export async function writeNewProjectRoot(root, tokensCss, slug, version) {
144
+ const r = await readTheirs(root, slug);
145
+ if (r.own.size > 0)
146
+ return { written: null, count: 0, reason: "own-vocabulary" };
147
+ const path = globalSheetOf(r.sheets);
148
+ if (!path)
149
+ return { written: null, count: 0, reason: "no-tailwind-entry" };
150
+ const css = r.sheets.get(path) ?? "";
151
+ const system = new Map(systemNames(tokensCss, r.every));
152
+ const lines = [...system].map(([n, v]) => ` ${n}: ${v};`);
153
+ if (lines.length === 0)
154
+ return { written: null, count: 0, reason: "empty-sheet" };
155
+ const canvas = system.get("--color-canvas") ?? system.get("--color-surface");
156
+ const text = system.get("--color-foreground");
157
+ if (canvas && /--background\s*:/.test(css))
158
+ lines.push(` --background: ${canvas};`);
159
+ if (text && /--foreground\s*:/.test(css))
160
+ lines.push(` --foreground: ${text};`);
161
+ const block = [
162
+ begin(slug),
163
+ open(`${slug} v${version}: the system's values under plain names - use them as var(--color-primary). They are yours now: change a value here and every component follows. Written by \`init\`, because this project had no tokens of its own.`),
164
+ ":root {",
165
+ ...lines,
166
+ "}",
167
+ end(slug),
168
+ ].join("\n");
169
+ const base = css.replace(END_RE(slug), "\n").replace(/\n*$/, "\n");
170
+ await writeFile(path, `${base}\n${block}\n`, "utf8");
171
+ return { written: relative(root, path).split("\\").join("/"), count: lines.length };
172
+ }
109
173
  const importLine = (slug) => `@import "./${slug}.css";`;
110
174
  /** O CSS dele sem nada nosso: sem o bloco e sem a linha que importa o arquivo à parte. */
111
175
  const withoutOurs = (css, slug) => css.replace(END_RE(slug), "\n").replace(new RegExp(`^${escape(importLine(slug))}\\n`, "m"), "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.479",
3
+ "version": "0.16.481",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {