synthesisui 0.16.495 → 0.16.497

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 CHANGED
@@ -463,6 +463,12 @@ written now** - read the code before you edit it, never a remembered signature.`
463
463
  * as duas portas e a regra que as mantém fechadas no dia a dia: quem cria sem pedir continua livre.
464
464
  * Basta o servidor registrado - a preferência não depende do sistema instalado.
465
465
  */
466
+ /**
467
+ * O PEDIDO LIVRE SAI MASTIGADO (jornada "o pedido livre sai mastigado", E03, 15/10): uma landing pedida em palavras
468
+ * livres passa pelo `landing` antes de qualquer linha - ele lista as seções do catálogo e devolve o prompt da pilha, com
469
+ * as decisões tomadas. Sem servidor também: o `landing` fala com o registro direto.
470
+ */
471
+ const landingLine = '\n\n**A landing asked in free words:** run `npx synthesisui@latest landing <slug> "<their words>"` first, and follow it.';
466
472
  const preferencesLine = recall.available
467
473
  ? `
468
474
 
@@ -523,7 +529,7 @@ If nothing covers it, say which indexed entry you considered and why it did not
523
529
  project's OWN custom properties, exactly as the guide lists them. Do not write raw colours,
524
530
  spacings or radii that a token already covers, and do not invent a new token silently - say so
525
531
  instead, because a new token is a decision for a person to make. There is no component
526
- manifest for an adopted system - the tokens ARE the contract.${theirsLine}${selfCheck}`
532
+ manifest for an adopted system - the tokens ARE the contract.${theirsLine}${landingLine}${selfCheck}`
527
533
  : `**When creating or editing components, read the system's GUIDE.md and follow it:** use the
528
534
  vocabulary it lists - every name in it is a name THIS project already declares, written exactly as
529
535
  you would write it. Do not use raw values outside the system's scale, and never a name from a
@@ -559,7 +565,7 @@ still yours: the \`request_component\` MCP tool, or \`npx synthesisui@latest req
559
565
  what the system's author works from - a refusal said only in chat evaporates. If you want to eyeball a
560
566
  component on its own, an isolated scratch page is optional and never required - and if you make
561
567
  one, put it somewhere that does not become a route in this app, and delete it when you are done.
562
- Do not apply a component to real production pages unless asked.${selfCheck}`;
568
+ Do not apply a component to real production pages unless asked.${landingLine}${selfCheck}`;
563
569
  const locale = await readInterfaceLanguage(projectRoot);
564
570
  const language = locale === null
565
571
  ? ""
@@ -1,3 +1,4 @@
1
+ import { describeDistinct, describeDistinctVerbose, distinctValues } from "../doctor/distinct-values.js";
1
2
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
3
  import { readStyleSheet, scssVariables } from "../doctor/scss-lens.js";
3
4
  import { THEME_SOURCE, tsTokenObjects, tsTokensAsVars } from "../doctor/ts-tokens-lens.js";
@@ -1143,6 +1144,22 @@ export async function doctor(opts) {
1143
1144
  }
1144
1145
  }
1145
1146
  }
1147
+ /**
1148
+ * QUANTOS VALORES DIFERENTES, E DE ONDE - ver `distinct-values.ts` (jornada "o doctor conta o que ninguém escolheu").
1149
+ *
1150
+ * Fora do bloco da cobertura de propósito: um projeto sem token nenhum não tem cobertura para medir, e é justamente
1151
+ * nele que "8 cinzas da caixa do framework" é a frase que falta.
1152
+ */
1153
+ const counted = distinctValues(reports, table);
1154
+ const countedBy = table.source === "installed" ? systemName : null;
1155
+ const distinct = describeDistinct(counted, countedBy);
1156
+ if (distinct.length > 0) {
1157
+ console.log("");
1158
+ const [head, ...rest] = distinct;
1159
+ console.log(body(head));
1160
+ for (const line of rest)
1161
+ console.log(body(paint.dim(` ${line.trimStart()}`)));
1162
+ }
1146
1163
  /**
1147
1164
  * THE RECORD - the layer that remembers, printed where the moment is shown.
1148
1165
  *
@@ -1671,6 +1688,17 @@ export async function doctor(opts) {
1671
1688
  say("");
1672
1689
  }
1673
1690
  }
1691
+ /**
1692
+ * CADA VALOR CONTADO, com a classe que o trouxe - só no `--verbose`, e numa seção própria depois da lista de valores à
1693
+ * mão: é o detalhe para conferir o número contra uma contagem feita por fora, não a primeira coisa que se lê.
1694
+ */
1695
+ const oneByOne = describeDistinctVerbose(counted, countedBy);
1696
+ if (verbose && oneByOne.length > 0) {
1697
+ say(section("Different values, one by one"));
1698
+ for (const line of oneByOne)
1699
+ say(body(line));
1700
+ say("");
1701
+ }
1674
1702
  say(section("What this means"));
1675
1703
  for (const line of verdict(d, hasSystem, overrides.length, conflictsInUse.length))
1676
1704
  say(line);
@@ -0,0 +1,47 @@
1
+ import { resolveRegistry } from "../config.js";
2
+ import { fetchLandingParts, fetchLandingPrompt } from "../registry.js";
3
+ /**
4
+ * `synthesisui landing <slug> "<pedido>"` - O PEDIDO LIVRE SAI MASTIGADO (jornada "o pedido livre sai mastigado", 15/10).
5
+ *
6
+ * Em dois passos, e o agente faz os dois:
7
+ *
8
+ * sem --sections a lista das seções de landing do catálogo, com o lugar, o tipo e o nome, e o que fazer em seguida
9
+ * com --sections o MESMO prompt do "Copy prompt" da pilha para essas seções - um template por seção, fontes, menu do
10
+ * celular -, com o conteúdo vindo do pedido
11
+ *
12
+ * A única decisão que fica com o agente é escolher da lista; o resto é regra nossa, já tomada. Fala com o registro
13
+ * (a mesma fonte do site), por isso não é o `use`, que promete não usar a rede (`architecture/no-network.spec.ts`).
14
+ */
15
+ export async function landing(slug, requestText, opts) {
16
+ const base = resolveRegistry(opts.registry);
17
+ const request = requestText.trim() || "a landing page";
18
+ const chosen = (opts.sections ?? "")
19
+ .split(",")
20
+ .map((s) => s.trim())
21
+ .filter(Boolean);
22
+ if (chosen.length === 0) {
23
+ const parts = await fetchLandingParts(base, slug);
24
+ const byPlace = (place) => parts.filter((p) => p.place === place);
25
+ const line = (p) => ` ${p.id.padEnd(20)} ${p.type} · ${p.name}${p.note ? ` (${p.note})` : ""}`;
26
+ const out = [
27
+ `The landing sections of "${slug}". Pick one navigation, one hero, the body sections in order, and one footer -`,
28
+ "the ones that fit this request:",
29
+ ` "${request}"`,
30
+ "",
31
+ "Top",
32
+ ...byPlace("nav").map(line),
33
+ "Hero",
34
+ ...byPlace("hero").map(line),
35
+ "Body",
36
+ ...byPlace("body").map(line),
37
+ "Bottom",
38
+ ...byPlace("footer").map(line),
39
+ "",
40
+ "Then run, with your picks in page order, and follow the prompt it prints:",
41
+ ` npx synthesisui@latest landing ${slug} --sections <nav>,<hero>,<body...>,<footer> "${request}"`,
42
+ ];
43
+ console.log(out.join("\n"));
44
+ return;
45
+ }
46
+ console.log(await fetchLandingPrompt(base, slug, chosen, request));
47
+ }
@@ -1966,7 +1966,18 @@ cli) {
1966
1966
  const prefs = await fetchPreferences({ base: resolveRegistry(), token }, kind, section, await installedSlug(root));
1967
1967
  if ("failed" in prefs)
1968
1968
  return fromContract(`Their preferences could not be read: ${prefs.failed}. Build with the system as usual and say so in one line.`);
1969
- return preferencesAnswer(kind, prefs, await readSelection(root), await readOff(root));
1969
+ const answer = preferencesAnswer(kind, prefs, await readSelection(root), await readOff(root));
1970
+ /**
1971
+ * O PEDIDO LIVRE SAI MASTIGADO (jornada "o pedido livre sai mastigado", E03, 15/10): a landing inteira passa pelo
1972
+ * `landing`, que escolhe do catálogo e devolve o prompt com as decisões tomadas. Mora AQUI, e não no CLAUDE.md, porque
1973
+ * com o servidor o bloco tem orçamento (`claude-md.f0.spec.ts`) e esta é a ferramenta que o agente já chama antes de
1974
+ * uma página. Uma parte só (`section`) não passa por ele: é uma seção, e o pedido é dela.
1975
+ */
1976
+ if (kind === "landing" && section === undefined) {
1977
+ const slug = (await installedSlug(root)) ?? "<slug>";
1978
+ answer.body = `For the whole landing, first run \`npx synthesisui@latest landing ${slug} "<their words>"\`: pick the sections it lists, run it again with \`--sections\`, and build from the prompt it prints - every decision in it is already made. Apply what follows on top of it.\n\n${answer.body}`;
1979
+ }
1980
+ return answer;
1970
1981
  }
1971
1982
  case "remember_preference": {
1972
1983
  const { kind, layer } = args;
@@ -97,9 +97,20 @@ export async function use(slug, intent, opts) {
97
97
  `(\`npx synthesisui component ${slug} <name>\`) and use this project's own Tailwind utilities ` +
98
98
  `(${utilities}…). Never use raw hex/px outside the system's scale.`;
99
99
  const task = intent.trim() || "build the UI I describe next";
100
+ /**
101
+ * UMA LANDING TEM CAMINHO PRÓPRIO (jornada "o pedido livre sai mastigado", E01, 15/10): o `landing` devolve as seções do
102
+ * catálogo e o prompt com as decisões tomadas. O `use` não busca nada - ele promete não usar a rede -, só aponta.
103
+ */
104
+ const isLanding = /\blanding\b|\bhome ?page\b/i.test(task);
100
105
  const prompt = [
101
106
  `Use the "${name}" design system (slug: ${slug}, v${version}) to: ${task}`,
102
107
  "",
108
+ ...(isLanding
109
+ ? [
110
+ `This is a landing page: before writing anything, run \`npx synthesisui@latest landing ${slug} "${task.replace(/"/g, "'")}"\` - it lists the catalog's sections for you to pick, then prints the full prompt with every decision made. Follow that prompt instead of the rest of this one.`,
111
+ "",
112
+ ]
113
+ : []),
103
114
  "Read these files in the project first (highest authority first):",
104
115
  ...readFirst,
105
116
  "",
package/dist/config.js CHANGED
@@ -251,7 +251,16 @@ export function describeIntent(source) {
251
251
  source.towards
252
252
  ? `derived: this group's reference is \`${source.towards}\`, and your vocabulary is converging on it`
253
253
  : "derived: this is the group's reference system, so the design moves in"
254
- : "nobody chose, so this is the default";
254
+ : /**
255
+ * A ORDEM, dita como ordem - jornada "o doctor conta o que ninguém escolheu", 15/10.
256
+ *
257
+ * Era *"nobody chose, so this is the default"*, e a aula 01 do curso leu a frase como se fosse sobre os
258
+ * VALORES da página - a mesma página em que 53 usos da paleta padrão do Tailwind não entravam em conta
259
+ * nenhuma. O que ninguém escolheu aqui é a ordem da lista (nem `--migrate`, nem `--adopt`), e a frase
260
+ * precisa dizer isso sem que quem lê o terminal abra o código. Sem "nobody", também: a linha de valores
261
+ * distintos diz que ninguém escolheu a paleta, e duas frases iguais sobre coisas diferentes são a confusão.
262
+ */
263
+ "default order - you never set one";
255
264
  const flip = source.intent === "migrate"
256
265
  ? "`doctor --adopt` sorts the other way"
257
266
  : "`doctor --migrate` sorts the other way";
@@ -0,0 +1,251 @@
1
+ import { fragmentsOfSource } from "./fragments.js";
2
+ import { normalizeValue } from "./tokens.js";
3
+ import { parseClass, readUtility } from "./transcribe.js";
4
+ const COLOUR_PROPERTIES = new Set([
5
+ "color",
6
+ "backgroundColor",
7
+ "borderColor",
8
+ "borderTopColor",
9
+ "borderRightColor",
10
+ "borderBottomColor",
11
+ "borderLeftColor",
12
+ "borderInlineColor",
13
+ "borderBlockColor",
14
+ "outlineColor",
15
+ "textDecorationColor",
16
+ "caretColor",
17
+ "accentColor",
18
+ "fill",
19
+ "stroke",
20
+ ]);
21
+ const SPACING_PROPERTY = /^(padding|margin)(Inline|Block|Top|Right|Bottom|Left|InlineStart|InlineEnd|BlockStart|BlockEnd)?$|^(gap|rowGap|columnGap)$/;
22
+ /**
23
+ * AS UTILITIES DE COR QUE O TRADUTOR NÃO LÊ SOZINHAS - `divide-stone-200`, `from-stone-50`, `ring-stone-300`.
24
+ *
25
+ * O tradutor existe para transcrever um componente, e lá um `divide-*` é regra sobre os filhos, não declaração do
26
+ * elemento. Aqui a pergunta é outra - que cor a página usa -, e a cor é a mesma de `text-stone-200`. O prefixo é trocado
27
+ * só quando a leitura devolve uma COR: `ring-2` e `shadow-lg` continuam sem leitura.
28
+ */
29
+ const COLOUR_ONLY_PREFIX = /^(divide|ring-offset|ring|from|via|to|decoration|outline|placeholder|caret|fill|stroke|shadow|inset-shadow)-(.+)$/;
30
+ /** O que não é escolha de valor: herdar, apagar, seguir a cor do texto. */
31
+ const NOT_A_VALUE = /^(transparent|currentcolor|current|inherit|initial|unset|auto|none)$/i;
32
+ /** As duas cores da caixa que o tradutor devolve em hex, sem o carimbo da paleta. */
33
+ const FRAMEWORK_PLAIN = /^(?:[a-z-]+-)?(white|black)$/;
34
+ /** O fim da classe, sem o alfa: `text-accent/80` é o `accent` dele, com transparência - a mesma cor escolhida. */
35
+ const withoutAlpha = (utility) => utility.includes("[") ? utility : utility.replace(/\/\d{1,3}$/, "");
36
+ /**
37
+ * AS PALAVRAS DE CLASSE DE UM ARQUIVO - pelo mesmo extrator que alimenta o ledger, então o que conta aqui é o que o resto
38
+ * do produto chama de classe: `className="…"`, template, e os literais dentro de `cn(…)`.
39
+ *
40
+ * Num template, o pedaço `${…}` é runtime e sai antes do corte: ele não está na fonte.
41
+ */
42
+ export function classWordsOf(file, source) {
43
+ const words = new Set();
44
+ for (const f of fragmentsOfSource(file, source)) {
45
+ if (f.shape !== "class" && f.shape !== "template")
46
+ continue;
47
+ const text = f.shape === "template" ? f.text.replace(/\$\{[^}]*\}/g, " ") : f.text;
48
+ for (const w of text.split(/\s+/))
49
+ if (w)
50
+ words.add(w);
51
+ }
52
+ return [...words];
53
+ }
54
+ function lengthKey(value, rootPx) {
55
+ const m = /^-?(\d*\.?\d+)(px|rem)$/.exec(value.trim());
56
+ if (!m)
57
+ return null;
58
+ const px = Number(m[1]) * (m[2] === "rem" ? rootPx : 1);
59
+ /** `p-0` não é degrau de escala nenhum: é a ausência de espaço. */
60
+ return px === 0 ? null : `${Number(px.toFixed(3))}px`;
61
+ }
62
+ function readWord(word, declared, isSystem, rootPx) {
63
+ const utility = withoutAlpha(parseClass(word).utility.replace(/^!/, "").replace(/!$/, ""));
64
+ let decl = readUtility(utility, declared);
65
+ const prefixed = COLOUR_ONLY_PREFIX.exec(utility);
66
+ if (!decl && prefixed) {
67
+ const asText = readUtility(`text-${prefixed[2]}`, declared);
68
+ if (asText?.property === "color")
69
+ decl = asText;
70
+ }
71
+ if (!decl)
72
+ return null;
73
+ const kind = COLOUR_PROPERTIES.has(decl.property)
74
+ ? "colour"
75
+ : decl.property === "fontSize"
76
+ ? "textSize"
77
+ : SPACING_PROPERTY.test(decl.property)
78
+ ? "spacing"
79
+ : null;
80
+ if (!kind || NOT_A_VALUE.test(decl.value.trim()))
81
+ return null;
82
+ const origin = decl.token
83
+ ? isSystem(decl.token)
84
+ ? "system"
85
+ : "own"
86
+ : utility.includes("[")
87
+ ? "hand"
88
+ : "framework";
89
+ if (kind === "colour") {
90
+ const key = decl.token ?? decl.value.toLowerCase().trim().replace(/\s*,\s*/g, ",").replace(/\s+/g, " ");
91
+ const plain = FRAMEWORK_PLAIN.exec(utility);
92
+ const family = origin !== "framework"
93
+ ? undefined
94
+ : plain
95
+ ? plain[1]
96
+ : /-([a-z]+)-\d{2,3}$/.exec(utility)?.[1];
97
+ return { kind, key, origin, ...(family ? { family } : {}) };
98
+ }
99
+ const key = decl.token ?? lengthKey(decl.value, rootPx);
100
+ return key ? { kind, key, origin } : null;
101
+ }
102
+ const byLength = (a, b) => {
103
+ const x = Number.parseFloat(a);
104
+ const y = Number.parseFloat(b);
105
+ return Number.isNaN(x) || Number.isNaN(y) ? a.localeCompare(b) : x - y;
106
+ };
107
+ /** Quando o mesmo valor chega por dois caminhos, ele conta UMA vez, pelo caminho que tem nome. */
108
+ const PRIORITY = ["system", "own", "framework", "hand"];
109
+ export function distinctValues(reports, table) {
110
+ /**
111
+ * OS TOKENS DO PROJETO INTEIRO, na forma que o tradutor lê (`--color-ink` -> valor). Uma variável é declarada no CSS
112
+ * global e usada no `.tsx`, então só a varredura inteira responde se `text-ink` é dele.
113
+ */
114
+ const declared = new Map();
115
+ for (const r of reports)
116
+ for (const d of r.ownDeclared ?? [])
117
+ if (!declared.has(d.name))
118
+ declared.set(d.name, d.value);
119
+ for (const [name, value] of table.byName)
120
+ if (!declared.has(name))
121
+ declared.set(name, value);
122
+ /**
123
+ * DO SISTEMA INSTALADO É O TOKEN QUE ELE TAMBÉM SEGURA - pelo nome que a tabela gera, ou pelo VALOR.
124
+ *
125
+ * O `init` escreve o tema do sistema no CSS dele, com os nomes dele (`--color-honey-500`, `--text-sm`), e esses nomes
126
+ * não estão no índice de utilities. Medido no projeto da aula com o Moss em 15/10: pelo nome só, 3 cores e 5 tamanhos
127
+ * escritos pelo `init` saíam como "your own tokens". Pelo valor é a mesma relação que o relatório já diz em "hold a
128
+ * value Moss also names". Sem sistema instalado, todo token é do projeto.
129
+ */
130
+ const installed = table.source === "installed";
131
+ const systemNames = new Set(installed ? table.utilities.values() : []);
132
+ const isSystem = (token) => {
133
+ if (!installed)
134
+ return false;
135
+ if (systemNames.has(token) || token.startsWith("--ds-"))
136
+ return true;
137
+ const value = declared.get(token);
138
+ return value !== undefined && table.byValue.has(normalizeValue(value, table.rootPx));
139
+ };
140
+ const seen = {
141
+ colour: new Map(),
142
+ textSize: new Map(),
143
+ spacing: new Map(),
144
+ };
145
+ const families = new Set();
146
+ const keep = (kind, key, origin, example) => {
147
+ const before = seen[kind].get(key);
148
+ if (!before || PRIORITY.indexOf(origin) < PRIORITY.indexOf(before.origin))
149
+ seen[kind].set(key, { origin, example });
150
+ };
151
+ for (const r of reports) {
152
+ for (const word of r.classWords ?? []) {
153
+ const read = readWord(word, declared, isSystem, table.rootPx);
154
+ if (!read)
155
+ continue;
156
+ keep(read.kind, read.key, read.origin, word);
157
+ if (read.family)
158
+ families.add(read.family);
159
+ }
160
+ /**
161
+ * A COR ESCRITA À MÃO FORA DE CLASSE - `#e7e5e4` num `style`, um `rgba()` dentro de uma sombra. São os achados de
162
+ * cor que o doctor já lista em "Values written by hand", então os dois números falam da mesma coisa.
163
+ */
164
+ for (const f of r.findings)
165
+ if (f.kind === "color")
166
+ keep("colour", f.literal.toLowerCase().trim().replace(/\s*,\s*/g, ",").replace(/\s+/g, " "), "hand", `${r.file}:${f.line}`);
167
+ }
168
+ const count = (kind) => {
169
+ const by = { framework: 0, system: 0, own: 0, hand: 0 };
170
+ for (const { origin } of seen[kind].values())
171
+ by[origin] += 1;
172
+ return {
173
+ total: seen[kind].size,
174
+ by,
175
+ families: kind === "colour" ? [...families].sort() : [],
176
+ values: [...seen[kind]]
177
+ .map(([key, { origin, example }]) => ({ key, origin, example }))
178
+ .sort((a, b) => PRIORITY.indexOf(a.origin) - PRIORITY.indexOf(b.origin) || byLength(a.key, b.key)),
179
+ };
180
+ };
181
+ return { colour: count("colour"), textSize: count("textSize"), spacing: count("spacing") };
182
+ }
183
+ const NOUN = {
184
+ colour: ["colour", "colours"],
185
+ textSize: ["text size", "text sizes"],
186
+ spacing: ["spacing", "spacings"],
187
+ };
188
+ /**
189
+ * AS LINHAS DO RELATÓRIO. Curta quando tudo vem de um nome; aberta por origem quando alguma coisa veio da caixa do
190
+ * framework ou da mão - que é exatamente o que ninguém escolheu.
191
+ */
192
+ export function describeDistinct(v, systemName) {
193
+ const kinds = ["colour", "textSize", "spacing"];
194
+ const present = kinds.filter((k) => v[k].total > 0);
195
+ if (present.length === 0)
196
+ return [];
197
+ const noun = (k, n) => `${n} ${NOUN[k][n === 1 ? 0 : 1]}`;
198
+ const head = present.map((k) => noun(k, v[k].total)).join(" · ");
199
+ const unchosen = present.some((k) => v[k].by.framework + v[k].by.hand > 0);
200
+ if (!unchosen) {
201
+ const whose = present.every((k) => v[k].by.own === 0) && systemName
202
+ ? `from ${systemName}`
203
+ : present.every((k) => v[k].by.system === 0)
204
+ ? "from your own tokens"
205
+ : `from ${systemName ?? "the system"} and your own tokens`;
206
+ return [`Different values ${head} - all ${whose}`];
207
+ }
208
+ const lines = [`Different values ${head}`];
209
+ const width = Math.max(...present.map((k) => NOUN[k][1].length));
210
+ for (const k of present) {
211
+ const { by, families } = v[k];
212
+ const parts = [];
213
+ if (by.framework > 0)
214
+ parts.push(k === "colour"
215
+ ? `${by.framework} from the framework palette${families.length > 0 ? ` (${families.join(", ")})` : ""}`
216
+ : `${by.framework} from the framework scale`);
217
+ if (by.system > 0)
218
+ parts.push(`${by.system} from ${systemName ?? "the system"}`);
219
+ if (by.own > 0)
220
+ parts.push(`${by.own} from your own tokens`);
221
+ if (by.hand > 0)
222
+ parts.push(`${by.hand} by hand`);
223
+ lines.push(` ${NOUN[k][1].padEnd(width)} ${parts.join(", ")}`);
224
+ }
225
+ if (v.colour.by.framework + v.textSize.by.framework + v.spacing.by.framework > 0)
226
+ lines.push(" the framework ones came in the box - nobody in this project picked them");
227
+ return lines;
228
+ }
229
+ const ORIGIN_LABEL = {
230
+ framework: "framework",
231
+ system: "system",
232
+ own: "your token",
233
+ hand: "by hand",
234
+ };
235
+ /**
236
+ * O `--verbose`: cada valor contado, com a classe em que apareceu. É o que deixa conferir o número contra uma contagem
237
+ * feita por fora - o `gap-px` que uma regex de dígitos não pega aparece aqui com o nome.
238
+ */
239
+ export function describeDistinctVerbose(v, systemName) {
240
+ const lines = [];
241
+ for (const k of ["colour", "textSize", "spacing"]) {
242
+ if (v[k].total === 0)
243
+ continue;
244
+ lines.push(`${NOUN[k][1]} (${v[k].total})`);
245
+ for (const { key, origin, example } of v[k].values) {
246
+ const label = origin === "system" && systemName ? systemName : ORIGIN_LABEL[origin];
247
+ lines.push(` ${label.padEnd(12)} ${key.padEnd(28)} ${example}`);
248
+ }
249
+ }
250
+ return lines;
251
+ }
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { propertyAt, roleFits } from "./their-names.js";
13
13
  import { utilitiesOn } from "./idiom-names.js";
14
+ import { classWordsOf } from "./distinct-values.js";
14
15
  import { normalizeValue, tokenMatch } from "./tokens.js";
15
16
  import { THEME_SOURCE, tsTokenObjects } from "./ts-tokens-lens.js";
16
17
  /**
@@ -489,7 +490,9 @@ export function scanSource(file, source, table) {
489
490
  : null),
490
491
  };
491
492
  }
492
- return stretchedStack(file, source, themeDefinitionsAside(file, source, scanCore(file, source, table)));
493
+ const report = stretchedStack(file, source, themeDefinitionsAside(file, source, scanCore(file, source, table)));
494
+ const classWords = /\.(tsx|jsx|html|vue|svelte|astro)$/i.test(file) ? classWordsOf(file, source) : [];
495
+ return classWords.length > 0 ? { ...report, classWords } : report;
493
496
  }
494
497
  /**
495
498
  * A GRADE QUE DIVIDE A ALTURA DA TELA - bateria ampliada, item 17 (29/09).
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import { generate } from "./commands/generate.js";
20
20
  import { hook } from "./commands/hook.js";
21
21
  import { runImport } from "./commands/import.js";
22
22
  import { init } from "./commands/init.js";
23
+ import { landing } from "./commands/landing.js";
23
24
  import { list } from "./commands/list.js";
24
25
  import { login } from "./commands/login.js";
25
26
  import { logout } from "./commands/logout.js";
@@ -61,6 +62,7 @@ Usage - deterministic, FREE:
61
62
  synthesisui upgrade <slug> update an installed DS + regenerate your components + migration brief
62
63
  synthesisui colours [slug] write the colours you chose on the site into your CSS, in your names (asks first)
63
64
  synthesisui use <slug> "<intent>" print a ready-to-paste agent prompt to build/modify on-system
65
+ synthesisui landing <slug> "<request>" the catalog's landing sections, then (--sections a,b,c) the prompt with every decision made
64
66
  synthesisui clean [--force] strip create-next-app boilerplate (dry run without --force)
65
67
 
66
68
  Usage - governance (deterministic, FREE):
@@ -719,6 +721,19 @@ async function main() {
719
721
  });
720
722
  break;
721
723
  }
724
+ case "landing": {
725
+ const slug = args[0];
726
+ if (!slug) {
727
+ console.error('error: provide the slug and your request - `synthesisui landing <slug> "<request>"`');
728
+ process.exitCode = 1;
729
+ return;
730
+ }
731
+ await landing(slug, args.slice(1).join(" "), {
732
+ sections: typeof flags.sections === "string" ? flags.sections : undefined,
733
+ registry: typeof flags.registry === "string" ? flags.registry : undefined,
734
+ });
735
+ break;
736
+ }
722
737
  case "use": {
723
738
  const slug = args[0];
724
739
  if (!slug) {
@@ -328,7 +328,12 @@
328
328
  * 0.16.492 -> 0.16.493 em 06/10, e o passo 1 dá **SIM** ("a galeria aprende", E02): o `init` declara as durações do
329
329
  * sistema no @theme (`--duration-fast`), e o blueprint as escreve pelo nome. Um componente escrito antes traz `100ms`.
330
330
  */
331
- export const MATERIALISER_SINCE = "0.16.494";
331
+ /**
332
+ * 0.16.494 -> 0.16.497 em 15/10, e o passo 1 dá **SIM** (o pedido livre sai mastigado, E03): o bloco do `CLAUDE.md` sem o
333
+ * servidor diz que uma landing pedida em palavras livres passa primeiro pelo `landing`. Uma pasta escrita antes deixa o
334
+ * agente decidir a página inteira sozinho.
335
+ */
336
+ export const MATERIALISER_SINCE = "0.16.497";
332
337
  /**
333
338
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
334
339
  *
package/dist/registry.js CHANGED
@@ -230,3 +230,27 @@ export async function postSaveComponent(base, payload) {
230
230
  }
231
231
  return (await res.json());
232
232
  }
233
+ /**
234
+ * O PEDIDO LIVRE SAI MASTIGADO (jornada "o pedido livre sai mastigado", 15/10): a lista de seções de landing do catálogo
235
+ * e o prompt da pilha para as escolhidas. Os dois saem do registro - a mesma fonte do site -, e nunca de uma cópia aqui.
236
+ */
237
+ export async function fetchLandingParts(base, slug) {
238
+ const url = new URL(`${base}/api/registry/ds/${encodeURIComponent(slug)}`);
239
+ url.searchParams.set("landing", "parts");
240
+ const res = await request(url.toString());
241
+ const body = (await res.json().catch(() => ({})));
242
+ if (!res.ok || !body.parts)
243
+ throw new RegistryError(body.message ?? `Registry responded ${res.status} for "${slug}".`);
244
+ return body.parts;
245
+ }
246
+ export async function fetchLandingPrompt(base, slug, sections, requestText) {
247
+ const url = new URL(`${base}/api/registry/ds/${encodeURIComponent(slug)}`);
248
+ url.searchParams.set("landing", "prompt");
249
+ url.searchParams.set("sections", sections.join(","));
250
+ url.searchParams.set("request", requestText);
251
+ const res = await request(url.toString());
252
+ const body = (await res.json().catch(() => ({})));
253
+ if (!res.ok || !body.prompt)
254
+ throw new RegistryError(body.message ?? `Registry responded ${res.status} for "${slug}".`);
255
+ return body.prompt;
256
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.495",
3
+ "version": "0.16.497",
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": {