synthesisui 0.16.284 → 0.16.286

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.
@@ -490,20 +490,39 @@ export async function add(slug, opts) {
490
490
  ? nextFontSnippet(families, payload.slug, appDir)
491
491
  : null;
492
492
  if (nextFonts) {
493
- const fontsPath = join(projectRoot, ...appDir.split("/"), "fonts.ts");
494
- let wroteFonts = false;
495
- if (!(await exists(fontsPath)) &&
496
- (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;
497
515
  const header = [
498
516
  `// Self-hosted type for the "${payload.slug}" design system (via next/font -`,
499
517
  `// preloaded, no font flash). Generated by \`synthesisui add\`; edit freely.`,
500
518
  ];
501
- await writeFile(fontsPath, `${[...header, ...nextFonts.fontsFile.slice(1)].join("\n")}\n`, "utf8");
502
- wroteFonts = true;
519
+ await writeFile(fontsPath, `${[...header, ...snippet.fontsFile.slice(1)].join("\n")}\n`, "utf8");
520
+ wroteIn.push(`${dir}/fonts.ts`);
503
521
  }
522
+ const wroteFonts = wroteIn.length > 0;
504
523
  console.log("");
505
524
  if (wroteFonts) {
506
- 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).`));
507
526
  console.log(line(" Finish the wiring with two small edits:"));
508
527
  }
509
528
  else {
@@ -732,9 +732,21 @@ export async function takeCensus(root, opts) {
732
732
  if (esize > 0 || etag || esketch.length > 0) {
733
733
  looks[extra.name] = {
734
734
  ...(esketch.length > 0 ? { sketch: esketch } : {}),
735
- /** Ver `CensusLook.aside` - o segundo componente do arquivo também. */
736
- ...(classifyAside(extra.name)
737
- ? { aside: classifyAside(extra.name) }
735
+ /**
736
+ * Ver `CensusLook.aside` - o segundo componente do arquivo também.
737
+ *
738
+ * AQUI OS EIXOS NÃO SÃO CONHECIDOS: `transcribeVariants` roda para o primeiro
739
+ * componente do arquivo, e este é o segundo. Passar `undefined` é dizer a verdade
740
+ * ("não sei se declara"), e não uma omissão - e é por isso que o veto final, em
741
+ * `exclusive-contract.ts`, pergunta pelos eixos em vez de confiar só neste campo.
742
+ */
743
+ ...(classifyAside({ name: extra.name, declaredAxes: undefined })
744
+ ? {
745
+ aside: classifyAside({
746
+ name: extra.name,
747
+ declaredAxes: undefined,
748
+ }),
749
+ }
738
750
  : {}),
739
751
  ...et,
740
752
  ...(etag ? { rootTag: etag } : {}),
@@ -1025,9 +1037,19 @@ export async function takeCensus(root, opts) {
1025
1037
  if (size > 0 || tag) {
1026
1038
  looks[found[0].name] = {
1027
1039
  ...(sketch.length > 0 ? { sketch } : {}),
1028
- /** Ver `CensusLook.aside`: o fato viaja, a regra fica de um lado só. */
1029
- ...(classifyAside(found[0].name)
1030
- ? { aside: classifyAside(found[0].name) }
1040
+ /**
1041
+ * Ver `CensusLook.aside`: o fato viaja, a regra fica de um lado só.
1042
+ *
1043
+ * `v.axes` é a DECLARAÇÃO de variação deste componente, e é ela que separa um glifo de
1044
+ * um componente cujo nome só parece com um. Ver `classifyAside`.
1045
+ */
1046
+ ...(classifyAside({ name: found[0].name, declaredAxes: v.axes })
1047
+ ? {
1048
+ aside: classifyAside({
1049
+ name: found[0].name,
1050
+ declaredAxes: v.axes,
1051
+ }),
1052
+ }
1031
1053
  : {}),
1032
1054
  ...withoutDark,
1033
1055
  base,
@@ -2848,6 +2870,23 @@ export function ladderReach(declared) {
2848
2870
  }
2849
2871
  return { light, dark };
2850
2872
  }
2873
+ /**
2874
+ * QUEM DÁ NOME AO SISTEMA, e o cliente para de ganhar um sistema morto.
2875
+ *
2876
+ * O QUE ELE VIVEU EM 23/08: respondeu "CodeLevel" à pergunta do nome, o agente escreveu isso em
2877
+ * `reading.name` - o campo certo -, e o sistema nasceu como `repo-ui`, tirado do `@repo/ui` do
2878
+ * `package.json`. O slug não muda depois de criado, então corrigir custou um segundo import e ele
2879
+ * ficou com dois sistemas para uma biblioteca, um deles morto, e nenhum caminho de volta que não
2880
+ * seja apagar à mão.
2881
+ *
2882
+ * A ORDEM É A MESMA DO `scheme`, dez linhas abaixo de onde isto é chamado: a flag é uma decisão
2883
+ * digitada agora, a leitura é uma resposta que alguém já deu, e a pergunta é o último recurso.
2884
+ * `null` quer dizer "ninguém disse" - o ÚNICO caso em que se pergunta - e é o que fecha a porta:
2885
+ * não há como esta função devolver um palpite quando existe uma resposta.
2886
+ */
2887
+ export function chosenName(input) {
2888
+ return input.flag?.trim() || input.read?.trim() || null;
2889
+ }
2851
2890
  export function deriveName(pkgName, dirName) {
2852
2891
  const raw = (pkgName ?? "").trim();
2853
2892
  const scoped = /^@([^/]+)\/(.+)$/.exec(raw);
@@ -3671,7 +3710,8 @@ export async function runImport(opts) {
3671
3710
  return;
3672
3711
  }
3673
3712
  const suggested = deriveName(census.project.name, basename(root) || "system");
3674
- const chosen = opts.name?.trim() || (await askName(suggested));
3713
+ const chosen = chosenName({ flag: opts.name, read: census.reading?.name }) ??
3714
+ (await askName(suggested));
3675
3715
  const reach = ladderReach(census.declared);
3676
3716
  // An explicit flag is a decision already made. Otherwise the person answers,
3677
3717
  // starting from the agent's reading if there is one and from the ladder's own
@@ -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.`));
@@ -293,10 +293,36 @@ export function axisOverlap(a, b) {
293
293
  */
294
294
  const IS_ICON = /(^Icon|Icon$)/;
295
295
  const IS_PROVIDER = /(Provider|Context)$/;
296
- export function classifyAside(name) {
297
- if (IS_ICON.test(name))
296
+ /**
297
+ * O QUE O CLIENTE GANHA: o componente dele para de desaparecer por causa do nome.
298
+ *
299
+ * Um glifo é modelado como biblioteca de ícones e não como receita - decisão de produto, e ela
300
+ * continua valendo. O que mudou é QUEM decide. O nome decidia sozinho, e em 23/08 isso custou o
301
+ * `IconButton` do `codelevel-ui`: três eixos declarados (`glow`, `size`, `variant`), a forma
302
+ * aninhada já derivada, e nenhuma linha no design system dele. Quem abrisse o Studio pediria um
303
+ * botão de ícone e ouviria que o sistema não tem - com o componente pronto no código.
304
+ *
305
+ * A lei 13 diz que a origem decide e não o nome, e um regex sobre o nome é exatamente o caso
306
+ * especial que ela proíbe. O discriminante estava no dado, e foi medido nas DUAS populações:
307
+ *
308
+ * frontend-hub 43 nomes casam o regex · 0 declaram eixos
309
+ * codelevel-ui 3 nomes casam o regex · 2 declaram eixos
310
+ *
311
+ * UM GLIFO NÃO DECLARA VARIAÇÃO - é um desenho, e desenho não tem `variant`. Quem declara eixos
312
+ * escreveu um contrato, e contrato é receita. Nenhum dos 43 do repositório real muda de lado, então
313
+ * a regra não afrouxou: ela passou a perguntar a coisa certa.
314
+ *
315
+ * `props` NÃO serve para isto, e a diferença é o caso do `Icon` dele: recebe `size` e `color` no uso
316
+ * e não declara nem um nem outro. Uso é o que alguém escolheu; declaração é o que o autor desenhou.
317
+ *
318
+ * A FORMA VEM INTEIRA, e não como segundo parâmetro opcional (§6): um argumento que pode ser
319
+ * esquecido é o defeito que este conserto está desfazendo, e não há como chamar isto pela metade.
320
+ */
321
+ export function classifyAside(shape) {
322
+ const declares = Object.keys(shape.declaredAxes ?? {}).length > 0;
323
+ if (IS_ICON.test(shape.name) && !declares)
298
324
  return "icon";
299
- if (IS_PROVIDER.test(name))
325
+ if (IS_PROVIDER.test(shape.name))
300
326
  return "provider";
301
327
  return null;
302
328
  }
@@ -429,7 +455,10 @@ opts) {
429
455
  }
430
456
  return mine
431
457
  .map((component) => {
432
- const aside = classifyAside(component.name);
458
+ const aside = classifyAside({
459
+ name: component.name,
460
+ declaredAxes: component.declaredAxes,
461
+ });
433
462
  if (aside) {
434
463
  return {
435
464
  component,
@@ -357,10 +357,29 @@ const FONT_WEIGHT = {
357
357
  black: "900",
358
358
  };
359
359
  /** The three family slots the document actually has. Nothing else may be a ref. */
360
+ /**
361
+ * O NOME DELE JÁ É O SLOT, e por isso esta tabela tem seis linhas e não três.
362
+ *
363
+ * O QUE O CLIENTE VIA: `--font-display: "Bricolage Grotesque"` declarado no `@theme`, a utility
364
+ * `font-display` escrita no `ModalHeader` e no `Typography` inteiro, e a leitura devolvendo nada -
365
+ * então o título dele chega no Studio na fonte do corpo (`codelevel-ui`, 23/08).
366
+ *
367
+ * As três primeiras traduzem o vocabulário da TAILWIND para o do documento (`font-sans` é o corpo).
368
+ * As duas últimas são o vocabulário do documento dito por ELE: quem declara `--font-display` não
369
+ * está pedindo tradução nenhuma, está nomeando o slot. Recusar isso era exigir que ele chamasse a
370
+ * fonte de display de `serif` para ser ouvido.
371
+ *
372
+ * E SÓ OS NOMES LITERAIS DO SLOT - `heading` fica de fora, e a tentação de incluí-lo é o erro que
373
+ * esta tabela evita. Um projeto pode declarar `--font-heading` E `--font-display` com fontes
374
+ * diferentes; mapear sinônimo colapsaria as duas em uma e apagaria uma decisão que ele escreveu.
375
+ * `font-heading` continua não lida, com o motivo no relatório, que é a resposta honesta.
376
+ */
360
377
  const FAMILY_SLOT = {
361
378
  sans: "body",
362
379
  serif: "display",
363
380
  mono: "mono",
381
+ display: "display",
382
+ body: "body",
364
383
  };
365
384
  /** Type utilities with no scale behind them - a fact, not a decision deferred. */
366
385
  const TYPE_KEYWORD = {
@@ -1170,8 +1189,63 @@ function readUtilityCore(utility, declared) {
1170
1189
  if (RADIUS[rest])
1171
1190
  return { property: "borderRadius", value: RADIUS[rest] };
1172
1191
  }
1192
+ /**
1193
+ * A UTILITY QUE O `@theme` DELE CRIOU - o último recurso, e é mecanismo, não caso.
1194
+ *
1195
+ * O QUE O CLIENTE VÊ SEM ISTO: o título do `ModalHeader` dele usa `font-display` e
1196
+ * `tracking-tightest`, os dois declarados no `@theme` como `--font-display: "Bricolage
1197
+ * Grotesque"` e `--tracking-tightest: -0.06em`. A leitura devolvia "não resolve nada" para
1198
+ * ambos, então o título chega no Studio na fonte do corpo e sem o aperto de letra que ele
1199
+ * escolheu - e o relatório afirma que a decisão não existe, com ela escrita no código
1200
+ * (`codelevel-ui`, 23/08).
1201
+ *
1202
+ * NO TAILWIND V4 TODO TOKEN DE UM NAMESPACE DO `@theme` CRIA A UTILITY DE MESMO NOME. As tabelas
1203
+ * acima conhecem as escalas que a Tailwind PUBLICA - `tracking-tight`, `text-2xl` -, então toda
1204
+ * escala própria de todo cliente caía neste buraco. A régua aqui não cita um nome de token: ela
1205
+ * pergunta se ELE declarou aquele nome.
1206
+ *
1207
+ * DEPOIS DE TUDO, e não antes: `tracking-tight` continua vindo da tabela fixa, e nenhuma leitura
1208
+ * que já funcionava muda de resposta. Medido nas duas populações: 2 classes resgatadas de 163
1209
+ * não lidas no `codelevel-ui`, 0 de 837 no `frontend-hub`, que não declara namespace próprio.
1210
+ *
1211
+ * SÓ OS NAMESPACES DE ATRIBUIÇÃO DIRETA. `--blur-*` e `--perspective-*` viram
1212
+ * `filter: blur(…)`/`transform`, e escrever a propriedade crua com o valor dentro produziria CSS
1213
+ * inválido - pior que não ler. Ver `tokenRefFor` para como o valor viaja: com ref de documento
1214
+ * onde existe grammar (`{typography.families.display}`) e como `var()` onde não existe, que é
1215
+ * CSS correto contra a folha dele.
1216
+ */
1217
+ const themeProperty = THEME_NAMESPACE[prefix];
1218
+ if (themeProperty && rest) {
1219
+ const own = `--${prefix}-${rest}`;
1220
+ if (declared.has(own)) {
1221
+ return { property: themeProperty, value: tokenRefFor(own), token: own };
1222
+ }
1223
+ }
1173
1224
  return null;
1174
1225
  }
1226
+ /**
1227
+ * Namespace do `@theme` → a propriedade CSS que a utility de mesmo nome escreve.
1228
+ *
1229
+ * Só atribuição direta: cada uma destas escreve `<propriedade>: var(--<namespace>-<nome>)` e nada
1230
+ * mais. `--blur-*` e `--perspective-*` ficam fora porque viram `filter: blur(…)`/`transform`, e a
1231
+ * propriedade crua com o valor dentro seria CSS inválido - pior que não ler.
1232
+ *
1233
+ * QUATRO LINHAS, E NÃO SETE. `text`, `shadow` e `animate` estavam aqui e foram REMOVIDOS depois de
1234
+ * medidos: os ramos acima já os resolvem, então a linha nunca era alcançada. Uma entrada que nada
1235
+ * alcança é a mesma dívida que os specs de alcance existem para impedir - conferido com
1236
+ * `--text-hero`, `--shadow-lifted` e `--animate-drift`, que continuam lidos sem elas.
1237
+ */
1238
+ const THEME_NAMESPACE = {
1239
+ /**
1240
+ * `font` NÃO ENTRA AQUI de propósito: o ramo de `font` acima decide sozinho e retorna, então uma
1241
+ * linha para ele seria código que nada alcança. A família dele é resolvida por `FAMILY_SLOT`,
1242
+ * que é onde o slot do documento mora.
1243
+ */
1244
+ tracking: "letterSpacing",
1245
+ leading: "lineHeight",
1246
+ ease: "transitionTimingFunction",
1247
+ aspect: "aspectRatio",
1248
+ };
1175
1249
  /**
1176
1250
  * A colour token ref in the document's own spelling.
1177
1251
  *
@@ -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.284";
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
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.284",
3
+ "version": "0.16.286",
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": {