synthesisui 0.16.268 → 0.16.269

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.
@@ -3,6 +3,7 @@ import { readdir, readFile } from "node:fs/promises";
3
3
  import { join, relative, resolve } from "node:path";
4
4
  import { provenanceAction, toolSlug, withSource, } from "../agent-provenance.js";
5
5
  import { pinnedHookVersion } from "../agent-wiring.js";
6
+ import { elementClass, kebabAxis } from "../component-codegen.js";
6
7
  import { composePlan, FAMILIES, isFamily, noFamilyAnswer, renderPlan, } from "../compose-context.js";
7
8
  import { readToken, resolveRegistry } from "../config.js";
8
9
  import { readEvents } from "../doctor/ledger.js";
@@ -11,6 +12,7 @@ import { diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
11
12
  import { nearestToken, normalizeValue, tokenFor } from "../doctor/tokens.js";
12
13
  import { fromCensus } from "../memory/observation.js";
13
14
  import { handleRecall, handleRemember, MEMORY_TOOLS } from "../memory/tools.js";
15
+ import { readInstalledConvention } from "../project-facts.js";
14
16
  import { repoStateOf } from "../repo-state.js";
15
17
  import { detectStack } from "../stack.js";
16
18
  import { component } from "./component.js";
@@ -974,7 +976,7 @@ query) {
974
976
  }
975
977
  }
976
978
  async function describeComponent(root, name) {
977
- const { documents, requires } = await loadSystem(root);
979
+ const { documents, requires, table } = await loadSystem(root);
978
980
  let recipe;
979
981
  for (const doc of documents) {
980
982
  const d = doc;
@@ -1118,6 +1120,69 @@ async function describeComponent(root, name) {
1118
1120
  if (composes.length > 0) {
1119
1121
  out.push("", `Built out of: ${composes.join(", ")}. Change one of those in one place rather than reproducing it here, and call describe_component on it before you do.`);
1120
1122
  }
1123
+ /**
1124
+ * HOW TO DRESS IT - and it was the half this answer never carried.
1125
+ *
1126
+ * Measured on the owner's own census (21/08): his Button declares three axes -
1127
+ * `variant` with eight options, `size` with four, `glow` with two - plus
1128
+ * defaults, 32 layers and 78 properties. The whole reply for a component like
1129
+ * that was its name, its description and "no rules govern this yet". An agent
1130
+ * reading it composes without knowing `gold` exists, and nothing anywhere
1131
+ * says an option was missed.
1132
+ *
1133
+ * The material was already here: `recipe.variants` is the SAME field the
1134
+ * `GUIDE.md` reads to print `data-variant="primary|gold|…"`. Two surfaces
1135
+ * answering one question, and only the file on disk answered it.
1136
+ *
1137
+ * The class comes from `readInstalledConvention`, never from a literal `ds-`:
1138
+ * an imported system may carry its own prefix in `meta.classNames`, and
1139
+ * telling the agent to write a class that does not exist is worse than
1140
+ * telling it nothing.
1141
+ */
1142
+ const axes = Object.entries(recipe.variants ?? {});
1143
+ if (axes.length > 0) {
1144
+ const convention = await readInstalledConvention(root, String(table.slug ?? ""));
1145
+ const cls = elementClass(name, convention);
1146
+ out.push("", `How to dress it: \`${cls}\`, plus one data attribute per axis.`);
1147
+ for (const [axis, options] of axes) {
1148
+ const names = Object.keys(options ?? {});
1149
+ if (names.length === 0)
1150
+ continue;
1151
+ const resting = recipe.defaults?.[axis];
1152
+ /**
1153
+ * O DEFAULT É O QUE ELE NÃO PRECISA ESCREVER, e dizer isso encurta o
1154
+ * código que o agente produz em vez de só informá-lo.
1155
+ */
1156
+ const rest = resting
1157
+ ? ` - at rest it is \`${resting}\`, so you can leave the attribute out`
1158
+ : "";
1159
+ out.push(` data-${kebabAxis(axis)}="${names.join("|")}"${rest}`);
1160
+ }
1161
+ const states = Object.keys(recipe.states ?? {});
1162
+ if (states.length > 0) {
1163
+ out.push(` states the recipe already carries: ${states.join(", ")} - they are CSS, so do not write them again in JS.`);
1164
+ }
1165
+ out.push(` Or call add_component { "name": "${name}" } and get it as typed code, one prop per axis.`);
1166
+ }
1167
+ /**
1168
+ * AS INSTRUÇÕES DE RENDER QUE A LEITURA MEDIU - e até aqui elas morriam no
1169
+ * terminal.
1170
+ *
1171
+ * O CLI mede `renderNotes` por componente e imprime uma vez, no import. São
1172
+ * 25 delas em 14 dos 59 componentes do dono, e uma diz: *"`ring` gates a look
1173
+ * and is a boolean rather than an axis - `data-ring` has to be set for it to
1174
+ * apply"*. Sem essa linha o agente escreve markup correto e o look nunca
1175
+ * aparece, que é o pior modo de falhar - nada reclama.
1176
+ *
1177
+ * O princípio, e ele vale além destas notas: **interpretada ou não por nós,
1178
+ * a instrução vai para quem constrói**. Uma nota que a esteira não soube
1179
+ * transformar em regra continua sendo a coisa mais útil que existe sobre
1180
+ * aquele componente.
1181
+ */
1182
+ const renderNotes = recipe.renderNotes ?? [];
1183
+ if (renderNotes.length > 0) {
1184
+ out.push("", "Read off your own code, and it decides whether the look appears:", ...renderNotes.map((n) => ` ${n}`));
1185
+ }
1121
1186
  /**
1122
1187
  * WHAT IT NEEDS INSTALLED - the whole reason this tool earns its place.
1123
1188
  *
@@ -4,8 +4,16 @@ export const DEFAULT_CONVENTION = {
4
4
  partSeparator: "-",
5
5
  };
6
6
  const kebab = (v) => v.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
7
- /** `metric-card` → `ds-metric-card`, or `metric-card`, or `sui-metric-card`. */
8
- const elementClass = (name, c) => `${c.prefix}${kebab(name)}`;
7
+ /**
8
+ * `metric-card` `ds-metric-card`, or `metric-card`, or `sui-metric-card`.
9
+ *
10
+ * EXPORTADA porque o `describe_component` passou a dizer ao agente qual classe
11
+ * escrever, e uma segunda implementação do mesmo nome é como as duas começam a
12
+ * divergir: o MCP mandaria escrever uma classe que este codegen não emite.
13
+ */
14
+ export const elementClass = (name, c) => `${c.prefix}${kebab(name)}`;
15
+ /** O mesmo kebab que decide a classe e o `data-*`, para quem precisa só do eixo. */
16
+ export const kebabAxis = (v) => kebab(v);
9
17
  /** `metric-card` + `title` → `ds-metric-card-title`, or `metric-card__title`. */
10
18
  const partClassName = (name, part, c) => `${elementClass(name, c)}${c.partSeparator}${kebab(part)}`;
11
19
  const pascal = (name) => name
package/dist/fonts.js CHANGED
@@ -12,12 +12,31 @@ const GENERIC_FAMILIES = new Set([
12
12
  "inherit",
13
13
  "initial",
14
14
  ]);
15
- /** Famílias CUSTOM (display/body/mono), dedup e sem os genéricos. */
15
+ /**
16
+ * TODA VAGA DE FONTE DO DOCUMENTO, dedup e sem os genéricos.
17
+ *
18
+ * Enumerava `[display, body, mono]` literalmente, e o documento passou a aceitar as famílias que o
19
+ * código DELE declara além das nossas três - `--font-serif` do codelevel é "Instrument Serif", 25 usos,
20
+ * reservada por ele para "editorial moments". O token existia, o compilador emitia
21
+ * `--ds-typography-families-serif`, e ninguém baixava a fonte: o texto saía no fallback do navegador e
22
+ * nada avisava. Um token que aponta para uma fonte que ninguém carregou é pior que um token ausente -
23
+ * o ausente pelo menos aparece na conta.
24
+ *
25
+ * A ORDEM É DETERMINÍSTICA: as três vagas primeiro, as dele em ordem alfabética. Isto vira href num
26
+ * arquivo gerado, e um conjunto que muda de ordem entre rodadas produz diff onde nada mudou.
27
+ */
28
+ export const familySlots = (families) => {
29
+ const ours = ["display", "body", "mono"].filter((k) => k in families);
30
+ const theirs = Object.keys(families)
31
+ .filter((k) => !ours.includes(k))
32
+ .sort();
33
+ return [...ours, ...theirs];
34
+ };
16
35
  export function customFontFamilies(families) {
17
36
  const seen = new Set();
18
37
  const names = [];
19
- for (const family of [families.display, families.body, families.mono]) {
20
- const name = family?.trim();
38
+ for (const slot of familySlots(families)) {
39
+ const name = families[slot]?.trim();
21
40
  if (!name)
22
41
  continue;
23
42
  const key = name.toLowerCase();
@@ -47,7 +66,8 @@ export function googleFontsHref(families) {
47
66
  * fallback - it works everywhere but swaps visibly on cold loads.
48
67
  */
49
68
  export function nextFontSnippet(families, slug, appDir = "app") {
50
- const roles = ["display", "body", "mono"].filter((role) => {
69
+ /** Toda vaga que o documento tem, não as nossas três - ver `customFontFamilies`. */
70
+ const roles = familySlots(families).filter((role) => {
51
71
  const name = families[role]?.trim();
52
72
  return name && !GENERIC_FAMILIES.has(name.toLowerCase());
53
73
  });
@@ -58,7 +78,7 @@ export function nextFontSnippet(families, slug, appDir = "app") {
58
78
  const importNames = [];
59
79
  const consts = [];
60
80
  for (const role of roles) {
61
- const name = families[role].trim();
81
+ const name = (families[role] ?? "").trim();
62
82
  if (!seen.has(name)) {
63
83
  seen.set(name, role);
64
84
  importNames.push(importName(name));
@@ -81,13 +101,13 @@ export function nextFontSnippet(families, slug, appDir = "app") {
81
101
  ...consts,
82
102
  ];
83
103
  const roleVar = (role) => {
84
- const name = families[role].trim();
104
+ const name = (families[role] ?? "").trim();
85
105
  return `--font-ds-${seen.get(name)}`;
86
106
  };
87
107
  const layout = [
88
108
  `// ${appDir}/layout.tsx`,
89
- `import { ${[...new Set(roles.map((r) => seen.get(families[r].trim())))].join(", ")} } from "./fonts";`,
90
- `<body data-ds="${slug}" className={\`${[...new Set(roles.map((r) => `\${${seen.get(families[r].trim())}.variable}`))].join(" ")}\`}>`,
109
+ `import { ${[...new Set(roles.map((r) => seen.get((families[r] ?? "").trim())))].join(", ")} } from "./fonts";`,
110
+ `<body data-ds="${slug}" className={\`${[...new Set(roles.map((r) => `\${${seen.get((families[r] ?? "").trim())}.variable}`))].join(" ")}\`}>`,
91
111
  ];
92
112
  const css = [
93
113
  `/* ${appDir}/globals.css - AFTER the tokens.css import */`,
package/dist/guide.js CHANGED
@@ -1,4 +1,12 @@
1
- import { nextFontSnippet } from "./fonts.js";
1
+ /**
2
+ * UMA IMPLEMENTAÇÃO SÓ, e este arquivo tinha a terceira.
3
+ *
4
+ * `customFontFamilies` existia aqui, em `fonts.ts` e em `ds-font-link.tsx` - três cópias de um
5
+ * julgamento, e as três enumeravam `[display, body, mono]`. Quando o documento passou a aceitar as
6
+ * famílias que o código dele declara, as três deixaram de carregar a fonte ao mesmo tempo. Este
7
+ * arquivo já importava `nextFontSnippet` do mesmo módulo; a cópia não tinha razão de existir.
8
+ */
9
+ import { customFontFamilies, familySlots, nextFontSnippet } from "./fonts.js";
2
10
  import { animationShorthand, classifyKeyframe, describeKeyframe, isFullRotation, } from "./motion.js";
3
11
  const kebab = (v) => v.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
4
12
  /**
@@ -25,21 +33,6 @@ const GENERIC_FAMILIES = new Set([
25
33
  "initial",
26
34
  ]);
27
35
  /** Famílias custom do documento (display/body/mono), deduplicadas, sem genéricos. */
28
- function customFontFamilies(families) {
29
- const seen = new Set();
30
- const out = [];
31
- for (const family of [families.display, families.body, families.mono]) {
32
- const name = family?.trim();
33
- if (!name)
34
- continue;
35
- const key = name.toLowerCase();
36
- if (GENERIC_FAMILIES.has(key) || seen.has(key))
37
- continue;
38
- seen.add(key);
39
- out.push(name);
40
- }
41
- return out;
42
- }
43
36
  /**
44
37
  * THE STRUCTURE, WITH WHAT EACH PART IS FOR AND WHEN IT EXISTS.
45
38
  *
@@ -269,6 +262,14 @@ export function buildGuide(payload) {
269
262
  const { document: doc, slug, name, version } = payload;
270
263
  const { meta, foundations, motion, components } = doc;
271
264
  const semanticRoles = Object.keys(foundations.color.semantic);
265
+ /**
266
+ * AS CORES QUE ELE NOMEIA POR PROPÓSITO, no arquivo que o agente lê primeiro.
267
+ *
268
+ * O compilador passou a emitir `--ds-color-tier-gold` e o `find_token` já as acha (ele lê o
269
+ * `tokens.css`), mas o GUIDE é onde o agente descobre o vocabulário ANTES de perguntar - e ele não
270
+ * as mencionava. São 19 no censo do dono, e são a parte que faz o sistema dele ser dele.
271
+ */
272
+ const namedColours = Object.keys(foundations.color.named ?? {});
272
273
  const seriesKeys = Object.keys(foundations.color.series ?? {});
273
274
  const fontFamilies = customFontFamilies(foundations.typography.families);
274
275
  const fontsHref = fontFamilies.length > 0
@@ -680,13 +681,17 @@ ${hasTailwind
680
681
  - **Always use semantic tokens**, never raw values nor primitives directly.
681
682
  Color: \`var(--ds-color-semantic-<role>)\`${hasTailwind ? " (utility: `bg-<role>`/`text-<role>`)" : ""}. The roles are: ${list(semanticRoles)}.
682
683
  - Primitives (\`--ds-color-<palette>-<step>\`) exist but should **not** be referenced directly -
683
- they feed the semantic roles.${seriesKeys.length > 0
684
+ they feed the semantic roles.${namedColours.length > 0
685
+ ? `\n- **Yours by name** → \`var(--ds-color-<name>)\`: ${namedColours.length} colour${namedColours.length === 1 ? "" : "s"} your code names by PURPOSE rather than by step, so no scale could hold ${namedColours.length === 1 ? "it" : "them"} - ${list(namedColours.slice(0, 8))}${namedColours.length > 8 ? ", …" : ""}. These are yours: reach for them when the purpose matches, and prefer a semantic role when it does not.`
686
+ : ""}${seriesKeys.length > 0
684
687
  ? `\n- Data-viz → \`var(--ds-color-series-<n>)\`${hasTailwind ? " (utility: `bg-series-<n>`/`text-series-<n>`/`fill-series-<n>`)" : ""}: categorical chart/series colors, ${seriesKeys.length} of them (${list(seriesKeys)}). Use them in order for multi-series charts; they re-paint with the system.`
685
688
  : ""}
686
689
  - Spacing → \`var(--ds-spacing-<key>)\`: ${list(Object.keys(foundations.spacing))}.
687
690
  - Radius → \`var(--ds-radius-<key>)\`: ${list(Object.keys(foundations.radius))}.
688
691
  - Shadow → \`var(--ds-shadow-<key>)\`: ${list(Object.keys(foundations.shadow))}.
689
- - Typography: families \`--ds-typography-families-{display,body,mono}\` (${foundations.typography.families.display}, ${foundations.typography.families.body}, ${foundations.typography.families.mono});
692
+ - Typography: families ${familySlots(foundations.typography.families)
693
+ .map((slot) => `\`--ds-typography-families-${slot}\` (${foundations.typography.families[slot]})`)
694
+ .join(", ")};
690
695
  weights${hasTailwind ? " (utility: `font-<key>`)" : ""}: ${list(weights)};
691
696
  scale \`--ds-typography-scale-<key>-font-size\`${hasTailwind ? " (utility: `text-<key>`)" : ""}: ${list(Object.keys(foundations.typography.scale))}.
692
697
  - Motion: durations \`--ds-motion-durations-<key>\` (${list(Object.keys(motion.durations))}) and
@@ -115,7 +115,7 @@
115
115
  * O índice de componentes é BYTE-IDÊNTICO nos dois caminhos - mesma fonte canônica, mesmos nomes,
116
116
  * mesma quantidade (`claude-md.f0.spec.ts`). O corte troca uma frase, nunca o inventário.
117
117
  */
118
- export const MATERIALISER_SINCE = "0.16.265";
118
+ export const MATERIALISER_SINCE = "0.16.269";
119
119
  /**
120
120
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
121
121
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.268",
3
+ "version": "0.16.269",
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": {