synthesisui 0.16.286 → 0.16.290

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.
@@ -38,6 +38,7 @@
38
38
  * and both branches naming a class in this file. Exactly one is a ternary between two
39
39
  * string literals. So this reader covers a family I had written off as impossible.
40
40
  */
41
+ import { tokenRefFor } from "../token-ref.js";
41
42
  /** A CSS pseudo-class or attribute, mapped to the state name the contract uses. */
42
43
  const STATE_OF = [
43
44
  [/:hover\b/, "hover"],
@@ -58,8 +59,19 @@ const STATE_OF = [
58
59
  [/:last-child\b/, "last"],
59
60
  [/:empty\b/, "empty"],
60
61
  ];
61
- /** `[data-theme="dark"]`, `.dark`, `[data-scheme=dark]` - how a project says dark. */
62
- const DARK = /\[data-theme=["']?dark["']?\]|\[data-scheme=["']?dark["']?\]|(^|\s)\.dark(\s|$)/;
62
+ /**
63
+ * `[data-theme="dark"]`, `.dark`, `html.dark`, `[data-scheme=dark]` - como um projeto diz escuro.
64
+ *
65
+ * O QUE O CLIENTE PERDIA: o esquema escuro escrito na folha dele. O padrão exigia que `.dark`
66
+ * viesse no início do seletor ou depois de um espaço, e ele escreve `html.dark .stroke-text` - a
67
+ * classe colada na tag, que é a forma que o Tailwind com `darkMode: "class"` produz quando o
68
+ * atributo vive no `<html>`. Cinco classes da folha dele caíam nisso, entre elas o `stroke-text` e o
69
+ * `holo-card`, e a regra escura delas era lida como se fosse a regra de repouso: o contorno claro do
70
+ * texto vinha pintado com a cor do tema escuro.
71
+ *
72
+ * `(?![\w-])` para não casar `.darkroom`, que é uma classe de alguém e não um esquema.
73
+ */
74
+ const DARK = /\[data-theme=["']?dark["']?\]|\[data-scheme=["']?dark["']?\]|(?:^|[\s>+~]|[a-zA-Z0-9\]])\.dark(?![\w-])/;
63
75
  /** `kebab-case` → `camelCase`, which is the only spelling the contract accepts. */
64
76
  export function camel(prop) {
65
77
  return prop.trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -274,7 +286,21 @@ export function readModuleCss(css) {
274
286
  const selector = one.trim();
275
287
  if (!selector)
276
288
  continue;
277
- const names = classesIn(selector);
289
+ /**
290
+ * O SELETOR DE ESQUEMA NÃO É UM ANCESTRAL DE ANATOMIA - e era registrado como um.
291
+ *
292
+ * `html.dark .stroke-text` tem duas classes, e a leitura tomava `dark` como PAI de
293
+ * `stroke-text`. Duas consequências, as duas na tela dele: a classe entrava na lista de
294
+ * "filho que só existe num contexto" e era descartada inteira - `stroke-text`,
295
+ * `stroke-text-soft`, `stroke-text-grad`, `stage-3d` e `holo-card`, cinco das 24 da folha
296
+ * dele -, e a árvore ganhava um nó chamado `dark` que não existe em marcação nenhuma.
297
+ *
298
+ * `.dark` é COMO o projeto diz escuro, não onde um elemento mora. Tirado dos nomes antes de
299
+ * decidir alvo e pai; o `isDark` abaixo continua lendo o seletor inteiro e mandando o bloco
300
+ * para `dark`, que é onde ele pertence.
301
+ */
302
+ const scheme = DARK.test(selector);
303
+ const names = classesIn(selector).filter((n) => !(scheme && (n === "dark" || n === "light")));
278
304
  if (names.length === 0) {
279
305
  // An element or `:root` selector styles something this reader cannot attach to
280
306
  // a component. Said out loud rather than dropped.
@@ -283,6 +309,9 @@ export function readModuleCss(css) {
283
309
  }
284
310
  const target = names[names.length - 1];
285
311
  const entry = out.classes[target] ?? emptyClass();
312
+ /** Alvo sozinho no seletor: é regra dela, e não regra dentro de outra. Ver `ModuleClass.own`. */
313
+ if (names.length === 1)
314
+ entry.own = true;
286
315
  out.classes[target] = entry;
287
316
  // A descendant selector records the RELATION as well as the styles: `.panel
288
317
  // .title` says title lives inside panel, which is the anatomy for free.
@@ -296,7 +325,7 @@ export function readModuleCss(css) {
296
325
  const block = declarations(rule.body);
297
326
  if (Object.keys(block).length === 0)
298
327
  continue;
299
- const isDark = DARK.test(selector);
328
+ const isDark = scheme;
300
329
  const state = STATE_OF.find(([re]) => re.test(selector))?.[1] ?? null;
301
330
  const width = minWidthOf(rule.media);
302
331
  if (width) {
@@ -526,7 +555,7 @@ export function transcribeModule(read, usage, declared) {
526
555
  const resolve = (block) => {
527
556
  const out = {};
528
557
  for (const [prop, value] of Object.entries(block)) {
529
- out[prop] = value.replace(/var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)/g, (whole, name) => declared.has(name) ? tokenRefFor(name) : whole);
558
+ out[prop] = value.replace(/var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)/g, (whole, name) => declared.has(name) ? tokenRefFor(name, declared) : whole);
530
559
  }
531
560
  return out;
532
561
  };
@@ -645,44 +674,6 @@ function formFor(tag) {
645
674
  * A declared custom property as a token ref in the document's namespace. Their name is
646
675
  * the path, which is "your names travel unchanged" one level deeper.
647
676
  */
648
- function tokenRefFor(name) {
649
- const bare = name.replace(/^--/, "");
650
- if (bare.startsWith("color-")) {
651
- const rest = bare.slice("color-".length);
652
- const m = /^(.*)-(\d{2,4})$/.exec(rest);
653
- return m ? `{color.${m[1]}.${m[2]}}` : `{color.${rest}}`;
654
- }
655
- if (bare.startsWith("radius-"))
656
- return `{radius.${bare.slice(7)}}`;
657
- if (bare.startsWith("spacing-"))
658
- return `{spacing.${bare.slice(8)}}`;
659
- if (bare.startsWith("shadow-"))
660
- return `{shadow.${bare.slice(7)}}`;
661
- if (bare.startsWith("background-image-gradient-")) {
662
- return `{gradients.${bare.slice("background-image-gradient-".length)}}`;
663
- }
664
- if (bare.startsWith("gradient-"))
665
- return `{gradients.${bare.slice(9)}}`;
666
- if (bare.startsWith("text-")) {
667
- // The companion token: `--text-body-s--line-height` names the line-height OF a
668
- // step, and a double dash inside a ref is a grammar nothing accepts (test13).
669
- const companion = /^text-(.+?)--(line-height|letter-spacing|font-weight)$/.exec(bare);
670
- if (companion) {
671
- const prop = {
672
- "line-height": "lineHeight",
673
- "letter-spacing": "letterSpacing",
674
- "font-weight": "weight",
675
- }[companion[2]];
676
- return `{typography.scale.${companion[1]}.${prop}}`;
677
- }
678
- if (bare.slice(5).includes("--"))
679
- return `var(${name})`;
680
- return `{typography.scale.${bare.slice(5)}.fontSize}`;
681
- }
682
- if (bare.startsWith("font-"))
683
- return `{typography.families.${bare.slice(5)}}`;
684
- return `var(${name})`;
685
- }
686
677
  /**
687
678
  * AS REGRAS DE CLASSE DE UMA FOLHA GLOBAL, prontas para o componente que as veste.
688
679
  *
@@ -700,21 +691,60 @@ function tokenRefFor(name) {
700
691
  * descendente (`.a .b` - o contexto é do pai) e seletor composto, que `readModuleCss`
701
692
  * já reporta em `unslotted`.
702
693
  */
694
+ /**
695
+ * `@utility x { … }` LIDA COMO `.x { … }` - a porta que o Tailwind v4 abriu, e nada mais.
696
+ *
697
+ * O QUE O CLIENTE PERDIA: as utilities ASSINATURA dele. No `codelevel-ui` são 23, e entre elas
698
+ * `text-grad`, `text-grad-fire`, `text-grad-xp` e `text-grad-gold` - os gradientes de texto que dão
699
+ * a cara do produto. O componente que escreve `text-grad` viajava sem o gradiente e o título dele
700
+ * chegava em cor lisa; das 198 declarações do `globals.css` que não alcançavam receita nenhuma, é
701
+ * aqui que a maior parte morava.
702
+ *
703
+ * É NORMALIZAÇÃO DE SINTAXE, E NÃO MOTOR NOVO. Uma `@utility` é uma classe com outro nome de porta:
704
+ * mesmo corpo, mesmas declarações, mesmo cascade. A máquina que resolve o `var()` dele para o token
705
+ * que ELE nomeou, que lê estado e `dark`, e que veste a classe na raiz do componente já existe
706
+ * inteira - só não conhecia a porta.
707
+ *
708
+ * MEDIDO EM TRÊS POPULAÇÕES: `codelevel-ui` 23 em 6 folhas, `synthesisui/web` 7 em 1 folha
709
+ * (`bg-ember`, `text-ember`, `mask-fade-x`…), `frontend-hub` 0 em 722 - ele é CSS Modules e não usa
710
+ * a sintaxe. Duas populações independentes com a forma, e uma sem, que é o que prova que a régua não
711
+ * assume o formato de um repositório só.
712
+ *
713
+ * A FUNCIONAL FICA DE FORA, e é o único caso: `@utility tab-* { tab-size: --value(integer) }` tem
714
+ * molde no lugar do nome, então não há classe fixa que uma marcação possa vestir. Nenhuma das 30
715
+ * medidas é funcional, e quando uma for, ela cai nas sobras com arquivo e linha, que é a resposta
716
+ * honesta.
717
+ */
718
+ function asClasses(body) {
719
+ return body.replace(/@utility\s+([a-zA-Z][\w-]*)(\s*\{)/g, (_whole, name, brace) => `.${name}${brace}`);
720
+ }
703
721
  export function readGlobalClasses(globals, declared) {
704
722
  const resolve = (block) => {
705
723
  const out = {};
706
724
  for (const [prop, value] of Object.entries(block)) {
707
- out[prop] = value.replace(/var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)/g, (whole, name) => declared.has(name) ? tokenRefFor(name) : whole);
725
+ out[prop] = value.replace(/var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)/g, (whole, name) => declared.has(name) ? tokenRefFor(name, declared) : whole);
708
726
  }
709
727
  return out;
710
728
  };
711
729
  const out = new Map();
712
730
  for (const sheet of globals) {
713
- const read = readModuleCss(sheet.body);
731
+ const read = readModuleCss(asClasses(sheet.body));
714
732
  const dependent = new Set(Object.values(read.classes).flatMap((c) => c.children));
715
733
  for (const [name, cls] of Object.entries(read.classes)) {
716
- /** O filho de `.a .b` só existe naquele contexto - vesti-lo solto pintaria errado. */
717
- if (dependent.has(name))
734
+ /**
735
+ * O filho de `.a .b` só existe naquele contexto - vesti-lo solto pintaria errado. MAS uma
736
+ * classe que também tem REGRA PRÓPRIA não é um filho: é uma classe com variação contextual, e
737
+ * descartá-la joga fora a regra base junto.
738
+ *
739
+ * O QUE ISSO CUSTAVA: cinco das 24 classes da folha dele, entre elas `stroke-text` e
740
+ * `holo-card` - o contorno de texto e a borda holográfica. A causa era uma linha de tema,
741
+ * `html.dark .stroke-text { … }`, que fazia a classe aparecer como filha em algum lugar.
742
+ *
743
+ * O PADRÃO É UNIVERSAL, e é isso que torna o conserto necessário e não específico: `html.dark
744
+ * .x` é como se escreve tema em CSS e `.parent:hover .child` é como se escreve estado. Medido
745
+ * em duas folhas independentes - 5 de 24 na dele, 2 de 22 na nossa.
746
+ */
747
+ if (dependent.has(name) && !cls.own)
718
748
  continue;
719
749
  const states = {};
720
750
  for (const [state, block] of Object.entries(cls.states))
@@ -51,7 +51,50 @@ export function tierOf(file) {
51
51
  return undefined;
52
52
  }
53
53
  /** `export function X`, `export default function X`, `export const X =`. */
54
- const EXPORTED = /export\s+(?:default\s+)?(?:async\s+)?(?:function\s+([A-Z][A-Za-z0-9_]*)|const\s+([A-Z][A-Za-z0-9_]*)\s*[:=])/g;
54
+ const EXPORTED = /export\s+(?:default\s+)?(?:async\s+)?(?:function\s+([A-Z][A-Za-z0-9_]*)|const\s+([A-Z][A-Za-z0-9_]*)\s*[:=]|class\s+([A-Z][A-Za-z0-9_]*))/g;
55
+ /**
56
+ * O `export default` QUE NOMEIA A PEÇA EM OUTRA LINHA - e é assim que metade da indústria escreve.
57
+ *
58
+ * O QUE O CLIENTE PERDIA: 108 componentes do dashboard dele, o kit de mídia inteiro entre eles
59
+ * (`Media`, `MediaHeader`, `MediaTitle`, `MediaFooter`, `MediaDescription`), mais o `SelectItem` e o
60
+ * `CurateContentSection`. Nenhum no censo, nenhum aviso: o leitor exigia a palavra `export` na MESMA
61
+ * linha da declaração. `export default function Media()` era lido, e
62
+ * `function Media() {…}` + `export default Media;` no fim do arquivo, não - a mesma peça, com a
63
+ * exportação numa linha própria.
64
+ *
65
+ * MEDIDO EM QUATRO POPULAÇÕES, e é o que decide o que cobrir:
66
+ *
67
+ * forma codelevel fh/ui fh/dashboard synthesisui
68
+ * export default function Name 0 0 519 73
69
+ * export default Name; 0 0 110 0
70
+ * export default memo/forwardRef(…) 0 0 1 0
71
+ * export { Name as default } 0 0 0 0
72
+ * export default class Name 0 0 0 0
73
+ * export default () => / function() 0 0 0 0
74
+ *
75
+ * As formas com zero entram de propósito: são o que a LINGUAGEM oferece, e uma régua que só cobre a
76
+ * forma do repositório que eu tenho na mão é uma régua que quebra no próximo cliente.
77
+ *
78
+ * O ANÔNIMO FICA DE FORA, declarado: em `export default () => …` não existe nome para ler. O único
79
+ * candidato seria o nome do arquivo, e batizar peça alheia é supor - o que este censo não faz.
80
+ */
81
+ const DEFAULT_NAMED = [
82
+ /** `export default Media;` e `export default memo(forwardRef(Media))`, com ou sem `React.` */
83
+ /export\s+default\s+(?:(?:React\.)?(?:memo|forwardRef)\s*\(\s*)*([A-Z][A-Za-z0-9_]*)/g,
84
+ /** `export { Media as default }` - a forma canônica do ES, e ela aceita vizinhos na mesma chave */
85
+ /export\s*\{[^}]*?\b([A-Z][A-Za-z0-9_]*)\s+as\s+default\b/g,
86
+ ];
87
+ /**
88
+ * O NOME PRECISA SER DECLARADO AQUI, e é isto que separa uma definição de um repasse.
89
+ *
90
+ * `import Button from "@acme/ui"; export default Button;` é um re-export: a peça é de outra pessoa e
91
+ * reivindicá-la faria o censo listar como dele um componente que ele só encaminha. A pergunta é se
92
+ * ESTE arquivo constrói o nome - `function X`, `const X =`, `class X`.
93
+ */
94
+ function declaresName(source, name) {
95
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
96
+ return new RegExp(`(?:^|\\n)\\s*(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?(?:function|class|const|let|var)\\s+${escaped}\\b`).test(source);
97
+ }
55
98
  /** `type ButtonProps = { … }` or `interface ButtonProps { … }`, to its closing
56
99
  * brace, wherever it sits.
57
100
  *
@@ -102,8 +145,21 @@ export function scanDefinitions(file, source) {
102
145
  const out = [];
103
146
  const seen = new Set();
104
147
  EXPORTED.lastIndex = 0;
105
- for (const m of source.matchAll(EXPORTED)) {
106
- const name = m[1] ?? m[2];
148
+ /**
149
+ * A EXPORTAÇÃO EM LINHA PRÓPRIA ENTRA NA MESMA FILA - ver `DEFAULT_NAMED`.
150
+ *
151
+ * Reunidas antes do laço para que a deduplicação por `seen` valha para as duas origens: um
152
+ * arquivo que escreve `export default function Card()` casa nos dois padrões e a peça é uma só.
153
+ */
154
+ const names = [...source.matchAll(EXPORTED)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
155
+ for (const rx of DEFAULT_NAMED) {
156
+ rx.lastIndex = 0;
157
+ for (const m of source.matchAll(rx)) {
158
+ if (m[1] && declaresName(source, m[1]))
159
+ names.push(m[1]);
160
+ }
161
+ }
162
+ for (const name of names) {
107
163
  if (!name || seen.has(name))
108
164
  continue;
109
165
  seen.add(name);
@@ -42,6 +42,7 @@
42
42
  * hover:shadow-md → states.hover
43
43
  * data-[checked]:bg-ocean-50 → states.checked
44
44
  */
45
+ import { tokenRefFor } from "../token-ref.js";
45
46
  /**
46
47
  * Utility prefix → the property name THE DOCUMENT USES, which is camelCase.
47
48
  *
@@ -1218,7 +1219,11 @@ function readUtilityCore(utility, declared) {
1218
1219
  if (themeProperty && rest) {
1219
1220
  const own = `--${prefix}-${rest}`;
1220
1221
  if (declared.has(own)) {
1221
- return { property: themeProperty, value: tokenRefFor(own), token: own };
1222
+ return {
1223
+ property: themeProperty,
1224
+ value: tokenRefFor(own, declared),
1225
+ token: own,
1226
+ };
1222
1227
  }
1223
1228
  }
1224
1229
  return null;
@@ -1310,7 +1315,7 @@ function resolveArbitrary(inner, declared) {
1310
1315
  if (raw.startsWith("--")) {
1311
1316
  const name = raw.split(",")[0].trim();
1312
1317
  return declared.has(name)
1313
- ? { value: tokenRefFor(name), token: name }
1318
+ ? { value: tokenRefFor(name, declared), token: name }
1314
1319
  : null;
1315
1320
  }
1316
1321
  // A literal in brackets is still a real value somebody typed.
@@ -1318,7 +1323,7 @@ function resolveArbitrary(inner, declared) {
1318
1323
  }
1319
1324
  const name = v[1];
1320
1325
  if (declared.has(name))
1321
- return { value: tokenRefFor(name), token: name };
1326
+ return { value: tokenRefFor(name, declared), token: name };
1322
1327
  /**
1323
1328
  * A `var()` THEY NEVER DECLARED, WHEN THEY DECLARED IT UNDER ITS NAMESPACE.
1324
1329
  *
@@ -1338,7 +1343,7 @@ function resolveArbitrary(inner, declared) {
1338
1343
  */
1339
1344
  const meant = MEANT_NAMESPACES.map((ns) => `--${ns}-${name.slice(2)}`).find((candidate) => declared.has(candidate));
1340
1345
  if (meant)
1341
- return { value: tokenRefFor(meant), token: meant };
1346
+ return { value: tokenRefFor(meant, declared), token: meant };
1342
1347
  return { value: raw };
1343
1348
  }
1344
1349
  /**
@@ -1362,53 +1367,6 @@ const MEANT_NAMESPACES = [
1362
1367
  * `--text-body-s` → `{typography.scale.body-s.fontSize}`. Their name is the path,
1363
1368
  * which is the whole "your names travel unchanged" promise applied one level deeper.
1364
1369
  */
1365
- function tokenRefFor(name) {
1366
- const bare = name.replace(/^--/, "");
1367
- if (bare.startsWith("color-"))
1368
- return refFor(bare.slice("color-".length));
1369
- if (bare.startsWith("radius-"))
1370
- return `{radius.${bare.slice(7)}}`;
1371
- if (bare.startsWith("spacing-"))
1372
- return `{spacing.${bare.slice(8)}}`;
1373
- if (bare.startsWith("shadow-"))
1374
- return `{shadow.${bare.slice(7)}}`;
1375
- // `--gradient-ui` and `--background-image-gradient-ui` are one token in two
1376
- // spellings - Tailwind v4's utility namespace wraps the first. Both reach
1377
- // `{gradients.ui}`, which is where the census files them.
1378
- if (bare.startsWith("background-image-gradient-")) {
1379
- return `{gradients.${bare.slice("background-image-gradient-".length)}}`;
1380
- }
1381
- if (bare.startsWith("gradient-"))
1382
- return `{gradients.${bare.slice(9)}}`;
1383
- if (bare.startsWith("text-")) {
1384
- /**
1385
- * THE COMPANION TOKEN. Tailwind v4 spells "the line-height OF text-body-s" as
1386
- * `--text-body-s--line-height` - a double dash inside one name. Read as a step name
1387
- * it produced `{typography.scale.body-s--line-height.fontSize}`, whose double dash
1388
- * no ref grammar accepts, and the whole recipe was REFUSED at validation (test13,
1389
- * 01/08). The suffix names the property; the middle names the step.
1390
- */
1391
- const companion = /^text-(.+?)--(line-height|letter-spacing|font-weight)$/.exec(bare);
1392
- if (companion) {
1393
- const prop = {
1394
- "line-height": "lineHeight",
1395
- "letter-spacing": "letterSpacing",
1396
- "font-weight": "weight",
1397
- }[companion[2]];
1398
- return `{typography.scale.${companion[1]}.${prop}}`;
1399
- }
1400
- // Any other double dash is a name this grammar cannot hold - the literal var()
1401
- // still resolves against their own stylesheet, and an invalid ref helps nobody.
1402
- if (bare.slice(5).includes("--"))
1403
- return `var(${name})`;
1404
- return `{typography.scale.${bare.slice(5)}.fontSize}`;
1405
- }
1406
- if (bare.startsWith("font-"))
1407
- return `{typography.families.${bare.slice(5)}}`;
1408
- // A namespace we do not model. The literal `var()` is still correct CSS against
1409
- // their own stylesheet, and inventing a ref would point at nothing.
1410
- return `var(${name})`;
1411
- }
1412
1370
  function refFor(name) {
1413
1371
  const m = /^(.*)-(\d{2,4})$/.exec(name);
1414
1372
  if (!m)
@@ -30,6 +30,7 @@
30
30
  * property resolve in their app by position in the string; a recipe resolves by
31
31
  * position in `layers`. They agree only if the reader inserts in the order it read.
32
32
  */
33
+ import { wearGlobal, withGlobal } from "../global-wear.js";
33
34
  import { parseClass, transcribe } from "./transcribe.js";
34
35
  /**
35
36
  * A Tailwind modifier, mapped to the condition it IS.
@@ -136,8 +137,7 @@ export function conditionOf(modifiers) {
136
137
  * condição do DOM dele, escrita por extenso - descartá-la porque a nossa tabela não a lista
137
138
  * seria a tabela decidindo o que o código dele pode dizer. `lost` fica para o que não dá para
138
139
  * ler, não para o que a gente não previu.
139
- */
140
- else if (nome)
140
+ */ else if (nome)
141
141
  out.state = camel(nome);
142
142
  else
143
143
  out.lost = true;
@@ -954,7 +954,19 @@ export function readClassTernaries(source) {
954
954
  * `declared` is their own custom properties, so `bg-ocean-950` comes out as a ref to
955
955
  * the token they declared rather than as a hex we looked up.
956
956
  */
957
- export function transcribeVariants(source, declared) {
957
+ export function transcribeVariants(source, declared,
958
+ /**
959
+ * AS CLASSES QUE A FOLHA GLOBAL DELE DECLARA - obrigatório, e por um motivo medido.
960
+ *
961
+ * O import vestia a regra global na RAIZ do componente e só ali. Uma utility escrita dentro da
962
+ * TABELA DE VARIANTES - que é onde o `Typography` do `codelevel-ui` põe `text-grad-fire` - vinha
963
+ * por aqui, e este caminho não conhecia folha nenhuma: o título dele saía em cor lisa, e nada
964
+ * dizia que o gradiente tinha sido perdido.
965
+ *
966
+ * Sem default de propósito. Um `Map` vazio é a resposta de quem não tem folha global, e ela se
967
+ * escreve em voz alta; um parâmetro que se pode esquecer é o defeito que isto está desfazendo.
968
+ */
969
+ globals) {
958
970
  const reads = readVariants(source);
959
971
  const fromRecords = {};
960
972
  const unslotted = [];
@@ -1048,7 +1060,21 @@ export function transcribeVariants(source, declared) {
1048
1060
  */
1049
1061
  const bare = layer.classes.map((cls) => parseClass(cls).utility);
1050
1062
  const t = transcribe(bare, declared);
1051
- const style = { ...t.base, ...t.dark };
1063
+ /**
1064
+ * A REGRA GLOBAL QUE ESTA CAMADA VESTE - ver `wearGlobal`.
1065
+ *
1066
+ * Antes da transcrição no objeto final, e não depois: uma utility da folha dele e uma utility
1067
+ * da Tailwind pintando a mesma propriedade resolvem pelo cascade, e o cascade diz que a
1068
+ * última classe escrita ganha. `transcribe` já respeita essa ordem entre as dela; aqui a
1069
+ * folha entra como a camada de baixo, que é onde o CSS a coloca.
1070
+ */
1071
+ const wornHere = wearGlobal(bare, globals);
1072
+ const style = {
1073
+ ...wornHere.base,
1074
+ ...wornHere.dark,
1075
+ ...t.base,
1076
+ ...t.dark,
1077
+ };
1052
1078
  return {
1053
1079
  ...(layer.when ? { when: layer.when } : {}),
1054
1080
  ...(layer.at ? { at: layer.at } : {}),
@@ -1063,7 +1089,7 @@ export function transcribeVariants(source, declared) {
1063
1089
  defaults,
1064
1090
  layers,
1065
1091
  raw: allLayers,
1066
- base: transcribe(baseClasses, declared),
1092
+ base: withGlobal(transcribe(baseClasses, declared), baseClasses, globals),
1067
1093
  unslotted,
1068
1094
  notes,
1069
1095
  };
@@ -0,0 +1,44 @@
1
+ import { parseClass } from "./doctor/transcribe.js";
2
+ export function wearGlobal(classes, globals,
3
+ /** Registra quais classes a folha global reivindicou, para o juiz de duplicidade. */
4
+ claimed) {
5
+ const worn = { base: {}, dark: {}, states: {} };
6
+ for (const cls of classes) {
7
+ const rule = globals.get(cls);
8
+ if (!rule)
9
+ continue;
10
+ claimed?.add(cls);
11
+ Object.assign(worn.base, rule.base);
12
+ Object.assign(worn.dark, rule.dark);
13
+ for (const [state, block] of Object.entries(rule.states))
14
+ worn.states[state] = { ...worn.states[state], ...block };
15
+ }
16
+ return worn;
17
+ }
18
+ /** Tem alguma declaração? Uma camada vazia não vira camada. */
19
+ export function wornAnything(worn) {
20
+ return (Object.keys(worn.base).length > 0 ||
21
+ Object.keys(worn.dark).length > 0 ||
22
+ Object.keys(worn.states).length > 0);
23
+ }
24
+ /**
25
+ * A TRANSCRIÇÃO COM A FOLHA GLOBAL DELE POR BAIXO.
26
+ *
27
+ * A folha é a camada de baixo porque é onde o CSS a coloca: uma utility da Tailwind escrita no mesmo
28
+ * elemento ganha da classe da folha quando as duas pintam a mesma propriedade.
29
+ */
30
+ export function withGlobal(t, classes,
31
+ /** Ausente é a resposta de quem não tem folha global: a transcrição volta intacta. */
32
+ globals) {
33
+ if (!globals || globals.size === 0)
34
+ return t;
35
+ const worn = wearGlobal(classes.map((cls) => parseClass(cls).utility), globals);
36
+ if (!wornAnything(worn))
37
+ return t;
38
+ return {
39
+ ...t,
40
+ base: { ...worn.base, ...t.base },
41
+ dark: { ...worn.dark, ...t.dark },
42
+ states: Object.fromEntries([...new Set([...Object.keys(worn.states), ...Object.keys(t.states)])].map((k) => [k, { ...worn.states[k], ...t.states[k] }])),
43
+ };
44
+ }
@@ -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.285";
131
+ export const MATERIALISER_SINCE = "0.16.290";
132
132
  /**
133
133
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
134
134
  *
@@ -253,7 +253,7 @@ export const CHECKER_SINCE = "0.16.250";
253
253
  * publicado antes deste código existir, e uma marca nele calaria o aviso para quem o instalou. Mesma
254
254
  * lição de algumas horas antes, na mesma sessão.
255
255
  */
256
- export const READER_SINCE = "0.16.274";
256
+ export const READER_SINCE = "0.16.288";
257
257
  /**
258
258
  * O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
259
259
  *
@@ -0,0 +1,80 @@
1
+ /**
2
+ * `Modal.Header` E `ModalHeader` SÃO UMA PEÇA - e a vitrine dele mostrava duas.
3
+ *
4
+ * O QUE O CLIENTE VIA: o design system dele contando 75 componentes quando ele escreveu 68. Sete
5
+ * pares apareciam duas vezes - `Modal.Header` + `ModalHeader`, `Modal.Body` + `ModalBody`,
6
+ * `Modal.Footer` + `ModalFooter`, `Toast.Icon` + `ToastIcon`, `Toast.Body` + `ToastBody`,
7
+ * `Tooltip.Content` + `TooltipContent`, `PillNav.Link` + `PillNavLink` - porque as duas grafias são
8
+ * a mesma coisa dita de dois jeitos: `export const ModalHeader` é a DECLARAÇÃO, e
9
+ * `Modal.Header = ModalHeader` é o açúcar de namespace que o React deixa escrever.
10
+ *
11
+ * A DECLARAÇÃO GANHA, e é a mesma regra que o resto do censo já segue: uso é o que alguém escolheu,
12
+ * declaração é o que o autor desenhou. O nome que sobrevive é o que ele exportou.
13
+ *
14
+ * MEDIDO NAS DUAS POPULAÇÕES, e a segunda é a que prova que a régua não é sobre um repositório:
15
+ *
16
+ * codelevel-ui 75 no inventário · 11 nomes com ponto · 7 pares
17
+ * frontend-hub 302 no inventário · 23 nomes com ponto · 0 pares
18
+ *
19
+ * No `frontend-hub` `Card.Root` e `Icon.ChevronUp` não têm um `CardRoot` nem um `IconChevronUp`
20
+ * declarado ao lado - são namespaces de verdade, e continuam como estão. É a EXISTÊNCIA da
21
+ * declaração plana que faz o par, nunca a grafia com ponto por si.
22
+ */
23
+ export function mergeNamespacePairs(components,
24
+ /** O que este escopo DECLARA. Sem isso, um par não pode ser provado - ver `Census.defined`. */
25
+ declared) {
26
+ const byName = new Map(components.map((c) => [c.name, c]));
27
+ const absorbed = new Set();
28
+ const out = [];
29
+ for (const c of components) {
30
+ if (absorbed.has(c.name))
31
+ continue;
32
+ if (!c.name.includes(".")) {
33
+ out.push(c);
34
+ continue;
35
+ }
36
+ const flat = c.name.split(".").join("");
37
+ const twin = byName.get(flat);
38
+ /** Só quando o nome plano EXISTE no inventário e é declarado por este escopo. */
39
+ if (!twin || !declared.has(flat)) {
40
+ out.push(c);
41
+ continue;
42
+ }
43
+ absorbed.add(c.name);
44
+ absorbed.add(flat);
45
+ out.push(fuse(twin, c));
46
+ }
47
+ /** Os planos que foram absorvidos já entraram fundidos; os outros seguem na ordem. */
48
+ return out;
49
+ }
50
+ /**
51
+ * As duas linhas somadas sob o nome declarado.
52
+ *
53
+ * `count` soma - são usos distintos no código dele. `files` fica no MAIOR dos dois em vez de somar:
54
+ * as duas grafias costumam aparecer nos mesmos arquivos, e somar diria que o componente vive em mais
55
+ * lugares do que vive. Um número inflado é pior que um número conservador, porque ele decide
56
+ * prioridade.
57
+ */
58
+ function fuse(flat, dotted) {
59
+ const props = { ...flat.props };
60
+ for (const [p, values] of Object.entries(dotted.props ?? {})) {
61
+ props[p] = [...new Set([...(props[p] ?? []), ...values])];
62
+ }
63
+ const propFiles = { ...(flat.propFiles ?? {}) };
64
+ for (const [p, n] of Object.entries(dotted.propFiles ?? {})) {
65
+ propFiles[p] = Math.max(propFiles[p] ?? 0, n);
66
+ }
67
+ return {
68
+ ...flat,
69
+ count: flat.count + dotted.count,
70
+ files: Math.max(flat.files, dotted.files),
71
+ props,
72
+ ...(Object.keys(propFiles).length > 0 ? { propFiles } : {}),
73
+ /** Os eixos que qualquer uma das duas grafias declarou - é a mesma declaração. */
74
+ ...(flat.declaredAxes || dotted.declaredAxes
75
+ ? {
76
+ declaredAxes: { ...dotted.declaredAxes, ...flat.declaredAxes },
77
+ }
78
+ : {}),
79
+ };
80
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * O QUE ESTA LEITURA NÃO CONSEGUIU SEGURAR, escrito pela esteira e não pedido a ninguém.
3
+ *
4
+ * O QUE O CLIENTE GANHA: ele sabe, no minuto do import, o que não chegou - com o número do
5
+ * repositório dele e o arquivo e a linha de cada caso. É a lei 8 do método: o cliente perdoa o que
6
+ * a gente diz que não faz, e não perdoa descobrir sozinho.
7
+ *
8
+ * POR QUE ISTO EXISTE. O arquivo era escrito pelo AGENTE, instruído por três frases dentro das
9
+ * descrições das tools do MCP. Em 23/08 o mesmo repositório foi importado duas vezes: a primeira
10
+ * rodada escreveu 37 linhas de lacunas declaradas, a segunda escreveu nenhuma. As lacunas
11
+ * continuaram todas lá - 198 declarações de CSS sem leitor, 5 `var()` que não pintam nada -, e só o
12
+ * aviso desapareceu. Um pedido em texto a um modelo é uma sugestão, e nada percebia quando ela não
13
+ * era seguida.
14
+ *
15
+ * O PISO É DETERMINÍSTICO, e todo dado que ele usa já estava medido no censo. O agente continua
16
+ * livre para ACRESCENTAR o que só ele viu; o que ele não pode mais é ser a única testemunha.
17
+ *
18
+ * `null` quando não há lacuna nenhuma: um arquivo vazio dizendo "nada a declarar" é ruído, e a
19
+ * ausência do arquivo já é a resposta.
20
+ */
21
+ export function notExpressed(census) {
22
+ const ledger = census.ledger;
23
+ const unread = ledger?.unread ?? [];
24
+ const broken = census.brokenRefs ?? [];
25
+ const skipped = census.skipped ?? [];
26
+ if (unread.length === 0 && broken.length === 0 && skipped.length === 0)
27
+ return null;
28
+ const out = [];
29
+ const scope = census.scope ? ` of \`${census.scope}\`` : "";
30
+ out.push(`# What this reading could not hold`);
31
+ out.push("");
32
+ out.push(`Measured by synthesisui \`${ledger?.cli ?? "unknown"}\`${scope}. Every number here is from` +
33
+ ` your files. This file is written by the pipeline on every import - what it lists is what` +
34
+ ` did NOT reach a recipe.`);
35
+ if (unread.length > 0) {
36
+ out.push("");
37
+ out.push("## Style your files hold and no recipe received");
38
+ for (const group of unread) {
39
+ out.push("");
40
+ out.push(`### ${group.uses} ${group.shape} declaration${group.uses === 1 ? "" : "s"}` +
41
+ ` across ${group.files} file${group.files === 1 ? "" : "s"} - ${group.reason}`);
42
+ out.push("");
43
+ out.push(group.because);
44
+ out.push("");
45
+ for (const ex of group.examples ?? [])
46
+ out.push(` ${ex.file}:${ex.line} ${ex.text}`);
47
+ }
48
+ }
49
+ if (broken.length > 0) {
50
+ out.push("");
51
+ out.push("## Variables referenced and never declared");
52
+ out.push("");
53
+ out.push("These do not paint a different colour - they paint nothing. They are yours to fix, in your" +
54
+ " own words. Up to three places are listed for each; the count says how many there are in" +
55
+ " total.");
56
+ out.push("");
57
+ for (const ref of broken) {
58
+ out.push(` ${ref.name} ${ref.count} reference${ref.count === 1 ? "" : "s"}` +
59
+ ` in ${ref.files} file${ref.files === 1 ? "" : "s"}`);
60
+ for (const place of ref.at ?? [])
61
+ out.push(` ${place.file}:${place.line}`);
62
+ }
63
+ }
64
+ if (skipped.length > 0) {
65
+ out.push("");
66
+ out.push("## Exports the gate left out, and why");
67
+ out.push("");
68
+ out.push("Nothing here was dropped in silence. If one of these IS a component of your system, that is" +
69
+ " a reading to correct - and the reason below is the one to argue with.");
70
+ out.push("");
71
+ for (const s of skipped)
72
+ out.push(` ${s.name}${s.file ? ` (${s.file})` : ""}\n ${s.because}`);
73
+ }
74
+ out.push("");
75
+ return `${out.join("\n")}\n`;
76
+ }