synthesisui 0.16.283 → 0.16.285

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.
@@ -10,6 +10,7 @@ import { body as line, section, snippet } from "../output.js";
10
10
  import { fetchDesignSystem } from "../registry.js";
11
11
  import { describeFiltered, ruleApplies, rulesForProject, } from "../rule-filter.js";
12
12
  import { detectStack } from "../stack.js";
13
+ import { pointTokensAtTheirNames } from "../their-vars.js";
13
14
  /**
14
15
  * QUAL METADE DESTA PASTA UM TIME COMMITA.
15
16
  *
@@ -193,9 +194,18 @@ export async function add(slug, opts) {
193
194
  // know what was active before, to report install vs update vs switch
194
195
  const prev = await readRootLock(rootLockPath);
195
196
  await mkdir(versionDir, { recursive: true });
197
+ /**
198
+ * O VOCABULÁRIO DELE, LIDO NESTA MÁQUINA - ver `their-vars.ts`, e é por isso que isto acontece
199
+ * aqui e não no compilador: a pergunta *"esta variável existe NESTE repositório?"* só tem resposta
200
+ * onde o repositório está.
201
+ *
202
+ * Falha calada de propósito: um repositório sem folha de estilo, ou sem build, devolve o que veio
203
+ * do servidor. A folha nunca fica pior do que estava.
204
+ */
205
+ const theirVars = await pointTokensAtTheirNames(projectRoot, payload);
196
206
  // 1. server artifacts (tokens.css, theme.css, …) → pinned version folder
197
207
  for (const [filename, content] of Object.entries(payload.artifacts)) {
198
- await writeFile(join(versionDir, filename), content, "utf8");
208
+ await writeFile(join(versionDir, filename), filename === "tokens.css" ? theirVars.css : content, "utf8");
199
209
  }
200
210
  // 2. canonical source of truth
201
211
  await writeFile(join(versionDir, "design-system.json"), `${JSON.stringify(payload.document, null, 2)}\n`, "utf8");
@@ -250,6 +260,14 @@ export async function add(slug, opts) {
250
260
  /** Ver `RegistryPayload.compiler`: é o que faz um conserto de CSS chegar a um install. */
251
261
  ...(payload.compiler != null ? { compiler: payload.compiler } : {}),
252
262
  ...(payload.rulesStamp ? { rules: payload.rulesStamp } : {}),
263
+ /**
264
+ * QUANTAS LINHAS DESTA FOLHA APONTAM PARA UMA VARIÁVEL DELE - ver `their-vars.ts`.
265
+ *
266
+ * Sem isto, o `tokens.css` de dois repositórios do mesmo sistema seria diferente e nada diria por
267
+ * quê. O número é o rastro: o `doctor` sabe que aquelas linhas dependem do vocabulário DESTE
268
+ * repositório, e é ele que avisa quando uma delas deixa de resolver.
269
+ */
270
+ ...(theirVars.pointed > 0 ? { theirVars: theirVars.pointed } : {}),
253
271
  ...(scope ? { scope } : {}),
254
272
  ...(systems.length > 0 ? { scopes: systems } : {}),
255
273
  ...(usage.length > 0 ? { usage } : {}),
@@ -371,6 +389,13 @@ export async function add(slug, opts) {
371
389
  "GUIDE.md",
372
390
  ];
373
391
  console.log(` v${v}/: ${files.join(", ")}`);
392
+ /**
393
+ * A FOLHA NÃO É A QUE O SERVIDOR COMPILOU, e calar isso é o pior dos dois mundos: o cliente vê um
394
+ * `var(--color-ink-500)` num arquivo "gerenciado pelo synthesisui" e não tem como saber de onde
395
+ * veio. Dito assim, ele lê a linha como o que ela é - o sistema seguindo o vocabulário dele.
396
+ */
397
+ if (theirVars.pointed > 0)
398
+ console.log(` ${theirVars.pointed} value${theirVars.pointed === 1 ? "" : "s"} in tokens.css now point at the name YOUR code already gives ${theirVars.pointed === 1 ? "it" : "them"} - change yours and the system follows${theirVars.pruned > 0 ? `; ${theirVars.pruned} matched but your build does not emit ${theirVars.pruned === 1 ? "that name" : "those names"}, so ${theirVars.pruned === 1 ? "it keeps" : "they keep"} the value` : ""}`);
374
399
  /**
375
400
  * O QUE O AGENTE VAI LER, e por qual porta - nomear o arquivo quando a porta é uma ferramenta
376
401
  * mandaria alguém procurar um caminho que não existe mais.
@@ -465,20 +490,39 @@ export async function add(slug, opts) {
465
490
  ? nextFontSnippet(families, payload.slug, appDir)
466
491
  : null;
467
492
  if (nextFonts) {
468
- const fontsPath = join(projectRoot, ...appDir.split("/"), "fonts.ts");
469
- let wroteFonts = false;
470
- if (!(await exists(fontsPath)) &&
471
- (await exists(join(projectRoot, ...appDir.split("/"))))) {
493
+ /**
494
+ * UM `fonts.ts` POR APP, e não só no primeiro - lacuna do próprio conserto de 22/08.
495
+ *
496
+ * Aquele conserto ensinou o `add` a ENCONTRAR os apps de um monorepo e a NOMEAR todos eles, e
497
+ * depois escrevia em um só. Metade da fiação entregue é a metade que engana: o cliente lê que
498
+ * dois apps foram encontrados, vê o arquivo aparecer, e descobre que o segundo ficou sem quando
499
+ * a tipografia dele não carrega.
500
+ *
501
+ * Cada app tem o seu, porque `next/font` é por app: o `layout.tsx` de cada um importa do próprio
502
+ * `fonts.ts`. Um arquivo compartilhado atravessaria a fronteira do workspace.
503
+ */
504
+ const wroteIn = [];
505
+ for (const dir of appDirs.length > 0 ? appDirs : [appDir]) {
506
+ const fontsPath = join(projectRoot, ...dir.split("/"), "fonts.ts");
507
+ /** Arquivo dele que já existe nunca é reescrito - o setup impresso diz o que ele deve exportar. */
508
+ if (await exists(fontsPath))
509
+ continue;
510
+ if (!(await exists(join(projectRoot, ...dir.split("/")))))
511
+ continue;
512
+ const snippet = nextFontSnippet(families, payload.slug, dir);
513
+ if (!snippet)
514
+ continue;
472
515
  const header = [
473
516
  `// Self-hosted type for the "${payload.slug}" design system (via next/font -`,
474
517
  `// preloaded, no font flash). Generated by \`synthesisui add\`; edit freely.`,
475
518
  ];
476
- await writeFile(fontsPath, `${[...header, ...nextFonts.fontsFile.slice(1)].join("\n")}\n`, "utf8");
477
- wroteFonts = true;
519
+ await writeFile(fontsPath, `${[...header, ...snippet.fontsFile.slice(1)].join("\n")}\n`, "utf8");
520
+ wroteIn.push(`${dir}/fonts.ts`);
478
521
  }
522
+ const wroteFonts = wroteIn.length > 0;
479
523
  console.log("");
480
524
  if (wroteFonts) {
481
- console.log(line(`3. ✓ wrote ${appDir}/fonts.ts - self-hosted type via next/font (preloaded, no font flash).`));
525
+ console.log(line(`3. ✓ wrote ${wroteIn.join(", ")} - self-hosted type via next/font (preloaded, no font flash).`));
482
526
  console.log(line(" Finish the wiring with two small edits:"));
483
527
  }
484
528
  else {
@@ -20,6 +20,7 @@ import { groupRole } from "../group-role.js";
20
20
  import { actingSlug, describeScope, measuredScope, scopePaths, } from "../measured-scope.js";
21
21
  import { body, paint, section, snippet } from "../output.js";
22
22
  import { resolveDeps } from "../stack.js";
23
+ import { danglingTheirVars } from "../their-vars.js";
23
24
  /**
24
25
  * `synthesisui doctor` - the check nobody else ships.
25
26
  *
@@ -891,6 +892,25 @@ export async function doctor(opts) {
891
892
  }
892
893
  console.log(body("The exact snippets are in the output of `init`."));
893
894
  }
895
+ /**
896
+ * A FOLHA APONTA PARA O VOCABULÁRIO DELE, E UM NOME SUMIU - ver `their-vars.ts`.
897
+ *
898
+ * O `add` reescreve `--ds-color-ink-500: #6b6b78` como `var(--color-ink-500)` quando o repositório
899
+ * já nomeia aquele valor, para não existirem duas fontes da verdade. Não há fallback, por decisão:
900
+ * uma cópia do valor sobreviveria a um rename DELE pintando o valor velho, que é a plataforma
901
+ * decidindo por cima da decisão dele.
902
+ *
903
+ * O preço é que um rename apaga a cor, e este é o único lugar que conhece as duas pontas. Sem esta
904
+ * linha ele veria um componente sem cor e nada explicaria de onde veio.
905
+ */
906
+ const dangling = danglingTheirVars(await readFile(join(root, "_synthesisui", "ds", table.slug ?? "", "tokens.css"), "utf8").catch(() => ""), installed.theirs);
907
+ if (dangling.length > 0) {
908
+ console.log("");
909
+ console.log(body(`${dangling.length} line${dangling.length === 1 ? "" : "s"} of this system point at ${dangling.length === 1 ? "a variable" : "variables"} your code no longer declares, so ${dangling.length === 1 ? "that value" : "those values"} paint nothing:`));
910
+ for (const { ours, theirs: name } of dangling.slice(0, 6))
911
+ console.log(body(` ${ours} → ${name}`));
912
+ console.log(body("Rename it back, or run `npx synthesisui add <slug>` to re-point the sheet at what you have now."));
913
+ }
894
914
  /**
895
915
  * E AQUI O RELATÓRIO PARA, quando a fiação está vermelha: uma instrução, e nada mais.
896
916
  *
@@ -150,7 +150,24 @@ export async function sync(opts) {
150
150
  const requests = await readRequests(root);
151
151
  /** O que ele declarou sobre as formas dele - ver `declared-forms.ts`. */
152
152
  const forms = await readDeclaredForms(root);
153
- if (events.length === 0 && requests.length === 0) {
153
+ /**
154
+ * O REGISTRO ESTAR VAZIO NÃO É MOTIVO PARA NÃO ENVIAR NADA - e era, e isso custou uma promessa.
155
+ *
156
+ * O QUE O CLIENTE PERDIA. O estado do repositório - em que versão ele está, com qual CLI - viaja no
157
+ * MESMO pacote que o registro de checagens. Este `return` acontecia antes do envio, e ele dispara
158
+ * exatamente no estado de quem acabou de instalar: o hook nunca rodou, então não há evento nenhum.
159
+ *
160
+ * Resultado medido em 22/08: `repo_state` nulo em 25 dos 26 sistemas em produção. A tela nunca
161
+ * conseguiu dizer *"você está na v1 e existe a v2"*, para ninguém, desde que a coluna existe.
162
+ *
163
+ * A ROTA JÁ ESTAVA PRONTA, e é isso que faz disto meia jornada e não uma feature faltando:
164
+ * `api/ledger` grava o estado antes dos eventos e diz por quê - *"gravá-lo não depende de haver
165
+ * evento nenhum"*. Os dois lados concordavam; o cliente é que voltava antes de falar.
166
+ *
167
+ * A frase continua sendo dita - ela é verdade e é útil. O que muda é que ela para de ser uma saída.
168
+ */
169
+ const emptyLedger = events.length === 0 && requests.length === 0;
170
+ if (emptyLedger && !repo && forms.length === 0) {
154
171
  console.log(section("Sync"));
155
172
  console.log(body("Nothing recorded yet. The record fills as the hook checks writes and doctor runs."));
156
173
  return;
@@ -209,7 +226,15 @@ export async function sync(opts) {
209
226
  */
210
227
  const gained = await inherit(root, out.inheritedForms ?? []);
211
228
  console.log(section("Sync"));
212
- console.log(body(`${out.eventsReceived} checks sent, ${out.eventsNew} new. ${out.requestsNow} open request${out.requestsNow === 1 ? "" : "s"}.`));
229
+ /**
230
+ * "0 CHECKS SENT" NÃO É O QUE ACONTECEU quando o registro está vazio e o estado do repo foi.
231
+ *
232
+ * A frase antiga contava eventos; num repositório recém-instalado ela leria "0 checks sent, 0 new"
233
+ * e esconderia a única coisa que realmente subiu. Quem lê "0" conclui que o comando não fez nada.
234
+ */
235
+ console.log(body(emptyLedger
236
+ ? `No checks recorded here yet - the record fills as the hook checks writes and doctor runs.${repo ? " This repo's installed version was sent, so the platform can tell you when a newer one exists." : ""}`
237
+ : `${out.eventsReceived} checks sent, ${out.eventsNew} new. ${out.requestsNow} open request${out.requestsNow === 1 ? "" : "s"}.`));
213
238
  if (gained > 0) {
214
239
  console.log("");
215
240
  console.log(body(`${gained} form${gained === 1 ? "" : "s"} your team already taught the pipeline now apply here too - the next measurement reads them without you declaring anything.`));
@@ -128,7 +128,7 @@
128
128
  * é sempre o bump deste PR - nunca o número que o `package.json` já carrega, porque alguém pode
129
129
  * publicar no meio.
130
130
  */
131
- export const MATERIALISER_SINCE = "0.16.283";
131
+ export const MATERIALISER_SINCE = "0.16.285";
132
132
  /**
133
133
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
134
134
  *
@@ -0,0 +1,200 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { theirNames } from "./doctor/their-names.js";
4
+ import { buildTable, normalizeValue, } from "./doctor/tokens.js";
5
+ /**
6
+ * A FOLHA APONTA PARA O NOME QUE O CÓDIGO DELE JÁ DÁ AO VALOR.
7
+ *
8
+ * O QUE O CLIENTE VÊ. O `tokens.css` que a gente instala declara `--ds-color-ink-500: #6b6b78` num
9
+ * repositório que declara `--color-ink-500: #6b6b78` desde antes da gente existir. Mesmo valor,
10
+ * mesma palavra, prefixo nosso - e duas fontes da verdade. Ele muda a dele e a nossa fica congelada
11
+ * no valor que a gente copiou. Medido em 22/08 no `codelevel-monorepo`: 78 de 161 declarações.
12
+ *
13
+ * Depois disto a nossa linha vira `--ds-color-ink-500: var(--color-ink-500)`, e a cor do sistema
14
+ * segue a decisão dele sem a gente reescrever nada no arquivo dele.
15
+ *
16
+ * ─────────────────────────────────────────────────────────────────────────
17
+ * POR QUE ISTO MORA NO CLI E NÃO NO COMPILADOR, que é onde eu ia colocar.
18
+ *
19
+ * O mesmo `compileDesignSystemCss` serve DOIS consumidores: o `tokens.css` que vai para a pasta dele
20
+ * e o Canvas do Studio, onde a folha de estilo dele não existe. Apontar para `var(--color-ink-500)`
21
+ * lá dentro apagaria a cor do nosso próprio preview - e apagaria também em todo projeto de DESTINO,
22
+ * que é o caso que `orphan-var.ts` já descreve: *"no projeto de origem isso nunca aparece, porque o
23
+ * globals.css dele ainda está lá"*.
24
+ *
25
+ * A informação que decide - *"esta variável existe NESTE repositório?"* - só existe na máquina dele.
26
+ * Rodando aqui, o projeto de destino se resolve sozinho: nada casa, nada é reescrito.
27
+ *
28
+ * ─────────────────────────────────────────────────────────────────────────
29
+ * O QUE PROTEGE, e cada guarda saiu de uma medição.
30
+ *
31
+ * o BUILD é a autoridade o Tailwind PODA `@theme` que nenhuma utility usa. Medido nos 19
32
+ * arquivos CSS que o build dele produziu: 7 das 78 declarações somem.
33
+ * Apontar para uma podada não deixa a cor errada - apaga a cor.
34
+ * (as 29 cores dele sobreviveram; a poda pegou só `--animate-*`)
35
+ * sem build, sem reescrita um repositório que nunca buildou não tem como provar o que resolve,
36
+ * e chutar aqui é o oposto do objetivo. A folha fica como veio e o
37
+ * comando DIZ que ficou.
38
+ * a categoria fecha `theirNames` recusa comprimento cru: `24px` pode ser raio ou
39
+ * espaçamento no vocabulário dele, e escolher errado escreve um token
40
+ * de tipo num `border-radius`.
41
+ * nada de fallback `var(--x, #6b6b78)` sobreviveria a um rename DELE pintando o valor
42
+ * velho - a plataforma decidindo por cima da decisão dele. Se ele
43
+ * renomear, quebra, e o `doctor` diz qual (dono, 22/08: *"a gente não
44
+ * pode ser invasivo... mas tem que ser capaz de identificar"*).
45
+ */
46
+ /** Onde um build costuma deixar o CSS que o navegador realmente recebe. */
47
+ const BUILD_DIRS = [".next", "dist", "build", "out", "storybook-static"];
48
+ async function* cssUnder(dir) {
49
+ for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
50
+ const path = join(dir, entry.name);
51
+ if (entry.isDirectory())
52
+ yield* cssUnder(path);
53
+ else if (entry.name.endsWith(".css"))
54
+ yield path;
55
+ }
56
+ }
57
+ /**
58
+ * AS VARIÁVEIS QUE O BUILD DELE REALMENTE EMITE - a autoridade sobre o que resolve no navegador.
59
+ *
60
+ * `null` quando não há build nenhum, e null significa *não sei*, não *nenhuma*. Quem chama trata os
61
+ * dois de forma oposta.
62
+ */
63
+ export async function resolvableVars(root) {
64
+ const out = new Set();
65
+ let found = false;
66
+ for (const dir of BUILD_DIRS) {
67
+ for await (const file of cssUnder(join(root, dir))) {
68
+ found = true;
69
+ const css = await readFile(file, "utf8").catch(() => "");
70
+ for (const m of css.matchAll(/(--[a-zA-Z0-9_-]+)\s*:/g))
71
+ out.add(m[1]);
72
+ }
73
+ /** Monorepo: o build mora dentro de cada app, não na raiz. */
74
+ for (const group of ["apps", "packages"]) {
75
+ for (const entry of await readdir(join(root, group), {
76
+ withFileTypes: true,
77
+ }).catch(() => [])) {
78
+ if (!entry.isDirectory())
79
+ continue;
80
+ for await (const file of cssUnder(join(root, group, entry.name, dir))) {
81
+ found = true;
82
+ const css = await readFile(file, "utf8").catch(() => "");
83
+ for (const m of css.matchAll(/(--[a-zA-Z0-9_-]+)\s*:/g))
84
+ out.add(m[1]);
85
+ }
86
+ }
87
+ }
88
+ }
89
+ return found ? out : null;
90
+ }
91
+ /**
92
+ * REESCREVE A FOLHA, e só as linhas que passam nos dois portões.
93
+ *
94
+ * `theirs` é a tabela do repositório dele (`harvestOwnTokens`); `resolvable` é o que o build emite,
95
+ * ou `null` quando não há build - e aí nada é reescrito.
96
+ */
97
+ export function pointAtTheirNames(css, theirs, resolvable) {
98
+ if (!resolvable || theirs.byName.size === 0)
99
+ return { css, pointed: 0, pruned: 0 };
100
+ const ours = buildTable({ css, source: "installed" });
101
+ const alias = theirNames(ours, theirs);
102
+ let pointed = 0;
103
+ let pruned = 0;
104
+ const out = css.replace(/^(\s*)(--ds-[a-zA-Z0-9_-]+)(\s*:\s*)([^;]+);/gm, (line, indent, name, sep, value) => {
105
+ /** Uma linha que já aponta para outra variável não é uma cópia de valor. */
106
+ if (value.includes("var("))
107
+ return line;
108
+ const kind = Object.entries(FAMILY_KIND).find(([prefix]) => name.startsWith(prefix))?.[1];
109
+ if (!kind)
110
+ return line;
111
+ const theirName = alias.get(`${kind}:${normalizeValue(value.trim(), ours.rootPx)}`);
112
+ if (!theirName || theirName === name)
113
+ return line;
114
+ if (!resolvable.has(theirName)) {
115
+ pruned += 1;
116
+ return line;
117
+ }
118
+ pointed += 1;
119
+ return `${indent}${name}${sep}var(${theirName});`;
120
+ });
121
+ return { css: out, pointed, pruned };
122
+ }
123
+ /**
124
+ * O PREFIXO DA NOSSA VARIÁVEL -> A FAMÍLIA, e a chave que `theirNames` devolve é `<família>:<valor>`.
125
+ *
126
+ * Comprimento cru fica de fora por decisão de `their-names.ts`, e a razão é a mesma aqui: `24px` pode
127
+ * ser raio ou espaçamento no vocabulário dele.
128
+ */
129
+ const FAMILY_KIND = {
130
+ "--ds-color-": "color",
131
+ "--ds-shadow-": "shadow",
132
+ "--ds-typography-families-": "font",
133
+ "--ds-motion-durations-": "motion",
134
+ "--ds-motion-easings-": "motion",
135
+ };
136
+ /**
137
+ * A JORNADA INTEIRA, num chamador só - varrer, medir o build, reescrever.
138
+ *
139
+ * Recebe o payload em vez do CSS solto porque um argumento opcional a menos é uma chamada pela
140
+ * metade a menos: quem chama não tem como esquecer de passar a folha certa.
141
+ */
142
+ export async function pointTokensAtTheirNames(root, payload) {
143
+ const css = payload.artifacts["tokens.css"] ?? "";
144
+ if (!css)
145
+ return { css, pointed: 0, pruned: 0 };
146
+ const [theirs, resolvable] = await Promise.all([
147
+ harvestTheirCss(root),
148
+ resolvableVars(root),
149
+ ]);
150
+ return pointAtTheirNames(css, theirs, resolvable);
151
+ }
152
+ /** As folhas de estilo DELE, e nenhuma da nossa pasta. Gêmeo de `harvestOwnTokens` no doctor. */
153
+ async function harvestTheirCss(root) {
154
+ const skip = new Set([
155
+ "node_modules",
156
+ ".git",
157
+ "dist",
158
+ ".next",
159
+ "build",
160
+ "out",
161
+ "_synthesisui",
162
+ ]);
163
+ let css = "";
164
+ const walk = async (dir) => {
165
+ for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
166
+ if (skip.has(entry.name) || entry.name.startsWith("."))
167
+ continue;
168
+ const path = join(dir, entry.name);
169
+ if (entry.isDirectory())
170
+ await walk(path);
171
+ else if (/\.(css|scss|sass|less)$/i.test(entry.name))
172
+ css += `\n${await readFile(path, "utf8").catch(() => "")}`;
173
+ }
174
+ };
175
+ await walk(root);
176
+ return buildTable({ css, source: "yours" });
177
+ }
178
+ /**
179
+ * AS LINHAS QUE APONTAM PARA UM NOME DELE QUE NÃO EXISTE MAIS.
180
+ *
181
+ * O dono decidiu a política em 22/08, e ela é o oposto de defensiva: *"só fazer o design dele quebrar
182
+ * e ele mesmo acaba consertando. A gente não pode ser invasivo... mas tem que ser capaz de, se o
183
+ * usuário precisar, corrigir, ou pelo menos identificar o problema."*
184
+ *
185
+ * Então não existe fallback. Se ele renomeia `--color-ink-500`, a cor some - e este é o único lugar
186
+ * que sabe dizer POR QUÊ, porque só ele conhece as duas pontas: a folha que a gente instalou e o
187
+ * vocabulário que o repositório declara hoje.
188
+ */
189
+ export function danglingTheirVars(installedCss, theirs) {
190
+ const out = [];
191
+ for (const m of installedCss.matchAll(/(--ds-[a-zA-Z0-9_-]+)\s*:\s*var\(\s*(--[a-zA-Z0-9_-]+)\s*\)\s*;/g)) {
192
+ const target = m[2];
193
+ /** Uma referência à nossa própria folha resolve nela mesma - não é dependência do repo dele. */
194
+ if (target.startsWith("--ds-"))
195
+ continue;
196
+ if (!theirs.byName.has(target))
197
+ out.push({ ours: m[1], theirs: target });
198
+ }
199
+ return out;
200
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.283",
3
+ "version": "0.16.285",
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": {