synthesisui 0.16.402 → 0.16.403

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.
@@ -8,7 +8,7 @@ import { lockReference } from "../group-role.js";
8
8
  import { buildGuide } from "../guide.js";
9
9
  import { censusScope } from "../measured-scope.js";
10
10
  import { recallAvailable } from "../memory/availability.js";
11
- import { nextFontWeights } from "../next-font-weights.js";
11
+ import { nextFontFacts } from "../next-font-weights.js";
12
12
  import { body as line, section, snippet } from "../output.js";
13
13
  import { fetchDesignSystem } from "../registry.js";
14
14
  import { repoStateOf } from "../repo-state.js";
@@ -242,8 +242,8 @@ export async function add(slug, opts) {
242
242
  * arquivo que o agente dele lê antes de escrever. `null` em projeto que não é Next é a resposta
243
243
  * certa, não uma falha - lá não existe `next/font` para ter peso nenhum.
244
244
  */
245
- const fontWeights = await nextFontWeights(projectRoot);
246
- await writeFile(join(versionDir, "GUIDE.md"), buildGuide(payload, tools.available, fontWeights), "utf8");
245
+ const fontFacts = await nextFontFacts(projectRoot);
246
+ await writeFile(join(versionDir, "GUIDE.md"), buildGuide(payload, tools.available, fontFacts), "utf8");
247
247
  // 4. stable root re-exports for each CSS artifact → always the active version,
248
248
  // so the consumer's @import path never changes across updates
249
249
  const cssArtifacts = Object.keys(payload.artifacts).filter((f) => f.endsWith(".css") && !(f === "shadcn.css" && !wantsShadcn));
@@ -693,7 +693,7 @@ export async function add(slug, opts) {
693
693
  slug: payload.slug,
694
694
  appDir,
695
695
  seam: payload.fontSeam,
696
- weights: fontWeights,
696
+ facts: fontFacts,
697
697
  declaredWeights: payload.document.foundations.typography.weights,
698
698
  })
699
699
  : null;
@@ -771,7 +771,7 @@ export async function add(slug, opts) {
771
771
  slug: payload.slug,
772
772
  appDir: dir,
773
773
  seam: payload.fontSeam,
774
- weights: fontWeights,
774
+ facts: fontFacts,
775
775
  declaredWeights: payload.document.foundations.typography.weights,
776
776
  });
777
777
  if (!plan.ok)
@@ -0,0 +1,24 @@
1
+ /**
2
+ * UM `FontFacts` COMPLETO A PARTIR DO QUE VOCÊ QUER DIZER - a porta que faltava fechar.
3
+ *
4
+ * O SUFIXO `.spec-input` DECLARA A INTENÇÃO NO PONTO DE IMPORTAÇÃO - ver `reachable.spec.ts`: este
5
+ * módulo existe só para alimentar spec, e é uma CATEGORIA, não uma órfã que alguém esqueceu de
6
+ * ligar. Ele nunca entra em código de produto.
7
+ *
8
+ * `buildGuide` e `nextFontSnippet` fecharam "esquecer o ARGUMENTO"; ficou aberto "esquecer um CAMPO
9
+ * dele", e isso custou 35 testes: quando `subsetsOf` nasceu, as fixtures escritas à mão em dez
10
+ * lugares não a tinham, o `guide.spec.ts` parou de CARREGAR, e a suíte seguiu verde com o arquivo
11
+ * inteiro fora da contagem. Só o `tsc` pegava, e o `tsc` dos specs não é o portão de cada rodada.
12
+ *
13
+ * Quem escreve um teste diz só o que importa para ele; os outros campos vêm completos por
14
+ * construção, e um campo novo neste tipo passa a chegar em toda fixture sozinho.
15
+ */
16
+ export function factsOf(over = {}) {
17
+ const weights = over.weights ?? ["400", "500", "600"];
18
+ const subsets = over.subsets ?? ["latin"];
19
+ return {
20
+ weightsOf: () => weights,
21
+ nameOf: (family) => over.names?.[family] ?? family,
22
+ subsetsOf: () => subsets,
23
+ };
24
+ }
package/dist/fonts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { weightsToEmit } from "./next-font-weights.js";
1
+ import { subsetsToEmit, weightsToEmit, } from "./next-font-weights.js";
2
2
  // Fallbacks genéricos do CSS - não são webfonts.
3
3
  const GENERIC_FAMILIES = new Set([
4
4
  "sans-serif",
@@ -89,7 +89,7 @@ export function googleFontsHref(families) {
89
89
  */
90
90
  export const FAMILY_SEAM_PREFIX = "--ds-typography-families-";
91
91
  export function nextFontSnippet(input) {
92
- const { families, slug, appDir = "app", seam, weights, declaredWeights, } = input;
92
+ const { families, slug, appDir = "app", seam, facts, declaredWeights, } = input;
93
93
  /**
94
94
  * A ORDEM É DETERMINÍSTICA e os duplicados saem: isto vira uma linha de arquivo gerado, e um
95
95
  * conjunto que muda de ordem entre rodadas produz diff onde nada mudou.
@@ -114,26 +114,70 @@ export function nextFontSnippet(input) {
114
114
  * suposição que derrubou o build de um projeto real em 08/09. Um arquivo não escrito custa uma
115
115
  * frase ao cliente; um arquivo escrito errado custa o build dele.
116
116
  */
117
- if (!weights)
117
+ if (!facts)
118
118
  return { ok: false, why: "manifest-unreadable", families: named };
119
- const importName = (name) => firstFamily(name).replace(/ /g, "_");
120
- const seen = new Map(); // family name -> const name
119
+ /**
120
+ * O IDENTIFICADOR SAI DA GRAFIA DO MANIFESTO, nunca de uma regra nossa - ver `FontFacts.nameOf`.
121
+ *
122
+ * A versão anterior fazia `replace(/ /g, "_")` sobre o nome como o documento DELE o escreve, e um
123
+ * `font-family: instrument serif` produzia `import { instrument_serif }`, que não existe. Uma
124
+ * capitalização "por palavra" trocaria esse defeito por outro: `IBM Plex Mono` sairia
125
+ * `Ibm_Plex_Mono`.
126
+ *
127
+ * E NÃO HÁ FALLBACK, de propósito. Uma família sem grafia conhecida é uma família que o manifesto
128
+ * não conhece, e ela segue o mesmo caminho de sempre: sai em `unknown`, sem linha escrita. Manter
129
+ * a heurística como último recurso deixaria a porta aberta para reescrever exatamente o import
130
+ * que quebra o build dele - vigiar em vez de fechar.
131
+ */
132
+ const importName = (canonical) => canonical.replace(/ /g, "_");
133
+ /**
134
+ * A DEDUPLICAÇÃO É PELA GRAFIA CANÔNICA, e não pelo texto dele - a porta que a canonização
135
+ * ABRIU, achada pela revisão deste PR.
136
+ *
137
+ * Medido: um documento com `display: "Geist"` e `body: "geist, sans-serif"` - dois papéis
138
+ * chegando à mesma família com caixa diferente, que é o normal quando o CSS dele foi escrito por
139
+ * mãos e épocas distintas - produzia
140
+ *
141
+ * import { Geist, Geist } from "next/font/google";
142
+ * SyntaxError: Identifier 'Geist' has already been declared
143
+ *
144
+ * Enquanto o identificador era composto do texto dele, as duas grafias davam dois nomes
145
+ * diferentes (`geist` e `Geist`) - inválidos, mas distintos. Canonizar o nome sem canonizar a
146
+ * CHAVE trocou "dois imports errados" por "um arquivo que não parseia": o mesmo EXIT=1 da
147
+ * `INV-VOLTA-16`, pela porta que este próprio conserto criou.
148
+ *
149
+ * A chave é a grafia do manifesto - a mesma resposta para as duas escritas dele.
150
+ */
151
+ const seen = new Map(); // canonical family -> const name
121
152
  const importNames = [];
122
153
  const consts = [];
123
154
  /** Família cujos pesos o manifesto não conhece: não se inventa peso para ela. */
124
155
  const unknown = [];
125
156
  for (const role of roles) {
126
157
  const name = firstFamily(families[role] ?? "");
127
- if (!seen.has(name)) {
128
- const available = weights(name);
158
+ const canonical = facts.nameOf(name);
159
+ if (!canonical || !seen.has(canonical)) {
160
+ const available = facts.weightsOf(name);
129
161
  const emit = available ? weightsToEmit(available, wanted) : [];
130
- if (emit.length === 0) {
131
- unknown.push(name);
162
+ if (emit.length === 0 || !canonical) {
163
+ if (!unknown.includes(name))
164
+ unknown.push(name);
132
165
  continue;
133
166
  }
134
- seen.set(name, role);
135
- importNames.push(importName(name));
136
- consts.push(`export const ${seen.get(name)} = ${importName(name)}({`, ` subsets: ["latin"],`,
167
+ /**
168
+ * OS SUBSETS TAMBÉM SÃO LIDOS - ver `subsetsToEmit`. Era `["latin"]` fixo, e o `next` reprova
169
+ * subset inexistente na MESMA função em que reprova peso inexistente: 121 das 1911 famílias
170
+ * do manifesto não têm `latin`, então a terceira porta deste módulo levava ao mesmo EXIT=1.
171
+ *
172
+ * Lista vazia significa que a família não declara subset nenhum, e aí a chave não é escrita:
173
+ * o `next` desliga o preload sozinho, e escrever `["latin"]` seria afirmar o que ela não tem.
174
+ */
175
+ const subsets = subsetsToEmit(facts.subsetsOf(name) ?? []);
176
+ seen.set(canonical, role);
177
+ importNames.push(importName(canonical));
178
+ consts.push(`export const ${role} = ${importName(canonical)}({`, ...(subsets.length > 0
179
+ ? [` subsets: [${subsets.map((x) => `"${x}"`).join(", ")}],`]
180
+ : []),
137
181
  /**
138
182
  * OS PESOS SÃO OS QUE A FAMÍLIA TEM - ver `next-font-weights.ts`.
139
183
  *
@@ -145,7 +189,7 @@ export function nextFontSnippet(input) {
145
189
  * `["400","500","600"]` de antes derrubou um `create-next-app` inteiro em 08/09. Os dois
146
190
  * defeitos são o mesmo erro - afirmar sobre a fonte dele em vez de ler.
147
191
  */
148
- ` weight: [${emit.map((w) => `"${w}"`).join(", ")}],`, ` variable: "--font-ds-${seen.get(name)}",`, `});`);
192
+ ` weight: [${emit.map((w) => `"${w}"`).join(", ")}],`, ` variable: "--font-ds-${role}",`, `});`);
149
193
  }
150
194
  }
151
195
  /** Nenhuma família sobreviveu: o manifesto existe e não conhece nenhuma delas. */
@@ -156,20 +200,29 @@ export function nextFontSnippet(input) {
156
200
  `import { ${importNames.join(", ")} } from "next/font/google";`,
157
201
  ...consts,
158
202
  ];
159
- const roleVar = (role) => {
203
+ /**
204
+ * A CONST QUE ESTE PAPEL USA - uma resposta só, e é o que impede as três leituras de divergirem.
205
+ *
206
+ * O `layout`, o mapa de CSS e o `roleVar` recompunham a chave cada um do seu jeito a partir do
207
+ * texto dele; com a deduplicação passando a ser canônica, três recomposições são três chances de
208
+ * uma delas citar uma const que o arquivo não exporta.
209
+ */
210
+ const constOf = (role) => {
160
211
  const name = firstFamily(families[role] ?? "");
161
- return `--font-ds-${seen.get(name)}`;
212
+ const canonical = facts.nameOf(name);
213
+ return canonical ? seen.get(canonical) : undefined;
162
214
  };
215
+ const roleVar = (role) => `--font-ds-${constOf(role)}`;
163
216
  /**
164
217
  * SÓ AS VAGAS QUE VIRARAM CÓDIGO - uma família descartada por peso desconhecido não pode aparecer
165
218
  * no `layout` nem no mapa de CSS: o import citaria uma const que o `fonts.ts` não exporta, e o
166
219
  * projeto dele deixaria de compilar por causa da linha que a gente mandou colar.
167
220
  */
168
- const emitted = roles.filter((role) => seen.has(firstFamily(families[role] ?? "")));
221
+ const emitted = roles.filter((role) => constOf(role) !== undefined);
169
222
  const layout = [
170
223
  `// ${appDir}/layout.tsx`,
171
- `import { ${[...new Set(emitted.map((r) => seen.get(firstFamily(families[r] ?? ""))))].join(", ")} } from "./fonts";`,
172
- `<body data-ds="${slug}" className={\`${[...new Set(emitted.map((r) => `\${${seen.get(firstFamily(families[r] ?? ""))}.variable}`))].join(" ")}\`}>`,
224
+ `import { ${[...new Set(emitted.map(constOf))].join(", ")} } from "./fonts";`,
225
+ `<body data-ds="${slug}" className={\`${[...new Set(emitted.map((r) => `\${${constOf(r)}.variable}`))].join(" ")}\`}>`,
173
226
  ];
174
227
  const css = [
175
228
  `/* ${appDir}/globals.css - AFTER the tokens.css import */`,
package/dist/guide.js CHANGED
@@ -293,7 +293,7 @@ toolsReachable = false,
293
293
  * essa porta; deixá-la entreaberta um nível acima seria vigiá-la em vez de fechá-la
294
294
  * (CLAUDE.md, Parte I, seção 6).
295
295
  */
296
- fontWeights) {
296
+ fontFacts) {
297
297
  const { document: doc, slug, name, version } = payload;
298
298
  const { meta, foundations, motion, components } = doc;
299
299
  const semanticRoles = Object.keys(foundations.color.semantic);
@@ -341,7 +341,7 @@ fontWeights) {
341
341
  families: foundations.typography.families,
342
342
  slug,
343
343
  seam: payload.fontSeam,
344
- weights: fontWeights,
344
+ facts: fontFacts,
345
345
  /** Os pesos que ELE declara - ver `declaredWeights`. O guia é o arquivo que o agente dele cola. */
346
346
  declaredWeights: foundations.typography.weights,
347
347
  });
@@ -170,7 +170,7 @@
170
170
  * O que o cliente ganha ao rodar `upgrade`: o agente dele no Codex passa a poder PERGUNTAR ao
171
171
  * sistema, em vez de só receber as regras e adivinhar o resto.
172
172
  */
173
- export const MATERIALISER_SINCE = "0.16.402";
173
+ export const MATERIALISER_SINCE = "0.16.403";
174
174
  /**
175
175
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
176
176
  *
@@ -22,8 +22,9 @@
22
22
  * quebra para a fonte que não tem aquele corte. `Instrument Serif` tem exatamente um peso.
23
23
  *
24
24
  * ONDE MORA A RESPOSTA, e por isso ela não é nossa: o próprio `next` que o projeto dele instalou
25
- * carrega o manifesto que o `next/font` usa para VALIDAR. Medido no mesmo projeto: **1942
26
- * famílias**, cada uma com os pesos e estilos que existem. Ler dali significa que o que a gente
25
+ * carrega o manifesto que o `next/font` usa para VALIDAR. *MEDIÇÃO, e a população é a versão do
26
+ * `next` que o projeto instalou: **1942 famílias** no `create-next-app` de 08/09, **1911** no
27
+ * `next` 16.2.0 deste repositório.* Cada uma com os pesos, estilos e subsets que existem. Ler dali significa que o que a gente
27
28
  * emite é exatamente o que o compilador dele aceita - e que uma família nova do Google chega junto
28
29
  * com o `npm update` dele, sem release nosso.
29
30
  *
@@ -83,16 +84,25 @@ async function loadManifest(projectRoot) {
83
84
  * (`"instrument serif"` de um `font-family` em minúsculas é a mesma família), e o manifesto guarda
84
85
  * a grafia do Google.
85
86
  */
86
- export async function nextFontWeights(projectRoot) {
87
+ export async function nextFontFacts(projectRoot) {
87
88
  const manifest = await loadManifest(projectRoot);
88
89
  if (!manifest)
89
90
  return null;
90
- const byLower = new Map();
91
+ const weights = new Map();
92
+ const names = new Map();
93
+ const subsets = new Map();
91
94
  for (const [family, entry] of Object.entries(manifest)) {
92
- const weights = (entry?.weights ?? []).filter((w) => /^\d+$/.test(w));
93
- byLower.set(family.toLowerCase(), weights);
95
+ const key = family.toLowerCase();
96
+ weights.set(key, (entry?.weights ?? []).filter((w) => /^\d+$/.test(w)));
97
+ names.set(key, family);
98
+ subsets.set(key, entry?.subsets ?? []);
94
99
  }
95
- return (family) => byLower.get(family.trim().toLowerCase()) ?? null;
100
+ const key = (family) => family.trim().toLowerCase();
101
+ return {
102
+ weightsOf: (family) => weights.get(key(family)) ?? null,
103
+ nameOf: (family) => names.get(key(family)) ?? null,
104
+ subsetsOf: (family) => subsets.get(key(family)) ?? null,
105
+ };
96
106
  }
97
107
  /**
98
108
  * OS PESOS A EMITIR para uma família, dados os que o gerador quer e os que existem.
@@ -112,3 +122,19 @@ export function weightsToEmit(available, wanted = ["400", "500", "600"]) {
112
122
  const nearest = [...have].sort((a, b) => Math.abs(Number(a) - 400) - Math.abs(Number(b) - 400))[0];
113
123
  return [nearest];
114
124
  }
125
+ /**
126
+ * OS SUBSETS A PEDIR para uma família, dados os que ela tem.
127
+ *
128
+ * `latin` quando ela o tem - é o que o texto dele usa, e pedir mais baixaria arquivo para nada.
129
+ * Sem `latin` mas com outros, os que ela tem: as 11 famílias nessa situação têm exatamente um
130
+ * (`Chenla` e `Content` são `["khmer"]`), então pedir a lista dela é pedir o único que existe.
131
+ *
132
+ * `[]` quando ela não tem subset nenhum, e aí a chave NÃO é escrita: o `next` desliga o preload
133
+ * sozinho nesse caso, e escrever `["latin"]` seria afirmar sobre a fonte dele um subset que ela não
134
+ * declara - a afirmação que este módulo existe para não fazer.
135
+ */
136
+ export function subsetsToEmit(available) {
137
+ if (available.includes("latin"))
138
+ return ["latin"];
139
+ return [...available];
140
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.402",
3
+ "version": "0.16.403",
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": {