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 +8 -2
- package/dist/commands/doctor.js +28 -0
- package/dist/commands/landing.js +47 -0
- package/dist/commands/mcp.js +12 -1
- package/dist/commands/use.js +11 -0
- package/dist/config.js +10 -1
- package/dist/doctor/distinct-values.js +251 -0
- package/dist/doctor/scan.js +4 -1
- package/dist/index.js +15 -0
- package/dist/install-marks.js +6 -1
- package/dist/registry.js +24 -0
- package/package.json +1 -1
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
|
? ""
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/commands/use.js
CHANGED
|
@@ -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
|
-
:
|
|
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
|
+
}
|
package/dist/doctor/scan.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/dist/install-marks.js
CHANGED
|
@@ -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
|
-
|
|
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