synthesisui 0.16.135 → 0.16.137

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.
@@ -29,6 +29,7 @@
29
29
  * validate everywhere and render as a hole, and "valid and unread" is the worst
30
30
  * of the three outcomes this pipeline has paid for twice.
31
31
  */
32
+ import { readSxProps } from "./doctor/style-props.js";
32
33
  import { transcribe, } from "./doctor/transcribe.js";
33
34
  import { frontierKind } from "./frontier-kind.js";
34
35
  /** The ten forms, closed. A renderer that accepts any tag draws anything. */
@@ -141,6 +142,23 @@ function frontierClasses(node, sketch) {
141
142
  const trimmed = raw?.trim();
142
143
  return trimmed ? { classes: trimmed } : {};
143
144
  }
145
+ /**
146
+ * A PROP DE ESTILO DE UM NÓ, verbatim, para o snippet devolvê-la.
147
+ *
148
+ * Numa biblioteca OPACA a gente nomeia e para - ela se desenha sozinha, e as declarações do `sx`
149
+ * não viram estilo nosso. Mas sem a prop de volta, o snippet entrega um `<Box>` que perdeu a
150
+ * margem que eles escreveram: 57 de 57 `sx` do app dele estão exatamente aí (05/08). É a mesma
151
+ * decisão que `motionProps` já carrega.
152
+ */
153
+ function frontierSx(node, sketch) {
154
+ const raw = typeof node.sx === "string"
155
+ ? node.sx
156
+ : typeof node.at === "number"
157
+ ? sketch?.[node.at]?.sx
158
+ : undefined;
159
+ const trimmed = raw?.trim();
160
+ return trimmed ? { sx: trimmed.slice(0, 400) } : {};
161
+ }
144
162
  export function resolveAnatomy(read, declared, deps,
145
163
  /** Their name → the slug it reaches in this system. See `RefResolver`. */
146
164
  resolve,
@@ -155,7 +173,12 @@ resolve,
155
173
  root,
156
174
  /** This component's sketch, so a node can name its classes by INDEX instead
157
175
  * of retyping them - see `AnatomyRead.at`. */
158
- sketch) {
176
+ sketch,
177
+ /**
178
+ * A escala de espaçamento que o projeto DELES declara para o `sx`. Ausente = o default
179
+ * documentado do MUI, que é fato da biblioteca que eles escolheram - ver `spacingOf`.
180
+ */
181
+ sxSpacing) {
159
182
  /**
160
183
  * THE PACKAGE THE RETURNED ELEMENT CAME FROM, measured rather than spelled.
161
184
  * Node 0 of the sketch IS what the component returns, and the reader stamped its
@@ -261,6 +284,7 @@ sketch) {
261
284
  from,
262
285
  ...(version ? { version } : {}),
263
286
  ...frontierClasses(node, sketch),
287
+ ...frontierSx(node, sketch),
264
288
  ...holds,
265
289
  });
266
290
  continue;
@@ -386,7 +410,25 @@ sketch) {
386
410
  : "";
387
411
  const classes = raw.split(/\s+/).filter(Boolean);
388
412
  const t = transcribe(classes, declared);
389
- parts[key] = { base: t.base, dark: t.dark, states: t.states };
413
+ /**
414
+ * A PROP DE ESTILO DESTE NÓ, sob a classe dele.
415
+ *
416
+ * `sx={{ width: "100%" }}` pertence ao elemento que a carrega - a mesma atribuição que a
417
+ * classe tem, e é por isso que ela viaja no sketch. Sob a classe porque uma prop é a
418
+ * camada mais fraca: se a classe declara a mesma propriedade, a classe ganha (é o que o
419
+ * `style={{ }}` já faz na raiz).
420
+ */
421
+ const sxText = typeof node.at === "number"
422
+ ? sketch?.[node.at]?.sx
423
+ : undefined;
424
+ const fromSx = sxText
425
+ ? readSxProps(sxText, sxSpacing).declarations
426
+ : {};
427
+ parts[key] = {
428
+ base: { ...fromSx, ...t.base },
429
+ dark: t.dark,
430
+ states: t.states,
431
+ };
390
432
  /**
391
433
  * A drop on a PART is still a drop. The root path already says these out loud;
392
434
  * this one silently swallowed `p-spacing-sm` on a real menu popup, so the part
@@ -13,7 +13,7 @@ import { describeGate, gateComponent, groupSkips, SCREENS_FOR_SYSTEM, } from "..
13
13
  import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
14
14
  import { countShape, describeCoverage, summarizeCoverage, } from "../doctor/coverage.js";
15
15
  import { crosswalk, floorSize, isLibrary, observedRules, useLiveCatalogue, } from "../doctor/crosswalk.js";
16
- import { keyframeOffsets, moduleImports, readModuleCss, readModuleUsage, transcribeModule, } from "../doctor/css-modules.js";
16
+ import { keyframeOffsets, moduleImports, partialCandidates, readModuleCss, readModuleUsage, sheetImports, transcribeModule, } from "../doctor/css-modules.js";
17
17
  import { dataContract } from "../doctor/data-contract.js";
18
18
  import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
19
19
  import { fragmentsOfSource, fragmentsOfStylesheet, judgeFragments, } from "../doctor/fragments.js";
@@ -27,6 +27,7 @@ import { describeSignals, emptySignals, finishSignals, readSignalsInto, } from "
27
27
  import { sketchOf } from "../doctor/sketch.js";
28
28
  import { describeForeign, readStyleIslands, } from "../doctor/style-island.js";
29
29
  import { buildLedger, describeLedger, } from "../doctor/style-ledger.js";
30
+ import { MUI_DEFAULT_SPACING, readStyledComponents, spacingOf, } from "../doctor/style-props.js";
30
31
  import { buildTable } from "../doctor/tokens.js";
31
32
  import { definitionSpan, readInlineStyle, rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
32
33
  import { transcribeVariants } from "../doctor/variant-read.js";
@@ -128,6 +129,48 @@ async function siblingProjects(root) {
128
129
  * this" - could not be said, and we ended up rediscovering by colour distance
129
130
  * what their stylesheet had spelled out all along.
130
131
  */
132
+ /**
133
+ * UMA FOLHA E TUDO O QUE ELA TRAZ PARA DENTRO DE SI, na ordem em que o sass compila.
134
+ *
135
+ * Uma partial (`_darkMode.scss`) nunca é compilada sozinha: ela entra em quem a importa, e portanto
136
+ * herda o escopo de quem a importa. Ler o `.module.scss` sem seguir os `@import` dele deixava de
137
+ * fora 1 400 declarações (91% de tudo o que a esteira chamava de "CSS global sem leitor") - e elas
138
+ * são, no build dele, as faces light e dark de classes que o componente JÁ veste (medido, 05/08).
139
+ *
140
+ * Devolve o CSS concatenado e os arquivos que entraram, porque o ledger precisa saber que a partial
141
+ * pertence a um componente admitido - senão ela continua contabilizada como forma sem leitor, e o
142
+ * relatório passa a discordar da receita.
143
+ */
144
+ async function sheetWithImports(path, seen = new Set(), depth = 0) {
145
+ if (depth > 6 || seen.has(path))
146
+ return { css: "", files: [] };
147
+ seen.add(path);
148
+ const body = await readFile(path, "utf8").catch(() => "");
149
+ if (!body)
150
+ return { css: "", files: [] };
151
+ let css = "";
152
+ const files = [path];
153
+ /**
154
+ * O IMPORTADO VEM ANTES, que é a ordem do sass: quem importa sobrescreve o que importou. Inverter
155
+ * isto faria a face dark de uma partial vencer a base do próprio module.
156
+ */
157
+ for (const spec of sheetImports(body)) {
158
+ for (const candidate of partialCandidates(spec)) {
159
+ const child = join(dirname(path), candidate);
160
+ const nested = await sheetWithImports(child, seen, depth + 1);
161
+ if (nested.css || nested.files.length > 1 || (await exists(child))) {
162
+ css += nested.css;
163
+ files.push(...nested.files);
164
+ break;
165
+ }
166
+ }
167
+ }
168
+ return { css: `${css}\n${body}`, files };
169
+ }
170
+ /** Existe no disco - o candidato de partial que o sass acharia primeiro. */
171
+ async function exists(path) {
172
+ return await readFile(path, "utf8").then(() => true, () => false);
173
+ }
131
174
  async function harvestOwnCss(root,
132
175
  /**
133
176
  * O LEDGER TAMBÉM COME AQUI, e por um motivo de promessa: este é o único laço que vê TODO
@@ -139,17 +182,40 @@ async function harvestOwnCss(root,
139
182
  * não admitido é um motivo, não um desaparecimento.
140
183
  */
141
184
  into) {
142
- let css = "";
185
+ /**
186
+ * DOIS PASSES, porque o escopo de uma folha está no GRAFO e não no nome dela.
187
+ *
188
+ * O primeiro junta tudo; o segundo julga cada folha sabendo quem importa quem. Um pass só
189
+ * decidia por `/\.module\./` e chamava de "CSS global sem leitor" 1 400 declarações que o build
190
+ * deles compila dentro de um module (medido, 05/08).
191
+ */
192
+ const sheets = [];
143
193
  for await (const file of walkAll([root])) {
144
194
  if (!/\.(css|scss|sass|less)$/i.test(file))
145
195
  continue;
146
196
  const body = await readFile(file, "utf8").catch(() => "");
197
+ sheets.push({ file, body });
198
+ }
199
+ /** Todo caminho que alguma folha importa, resolvido como o sass resolveria. */
200
+ const imported = new Set();
201
+ for (const sheet of sheets)
202
+ for (const spec of sheetImports(sheet.body))
203
+ for (const candidate of partialCandidates(spec))
204
+ imported.add(join(dirname(sheet.file), candidate));
205
+ let css = "";
206
+ for (const { file, body } of sheets) {
147
207
  css += `\n${body}`;
148
- if (into && body)
149
- into.push({
150
- file: relative(root, file),
151
- fragments: fragmentsOfStylesheet(relative(root, file), body, /\.module\./.test(file)),
152
- });
208
+ if (!into || !body)
209
+ continue;
210
+ const partial = basename(file).startsWith("_");
211
+ into.push({
212
+ file: relative(root, file),
213
+ fragments: fragmentsOfStylesheet(relative(root, file), body, /\.module\./.test(file) || (partial && imported.has(file))
214
+ ? "scoped"
215
+ : partial
216
+ ? "dead"
217
+ : "global"),
218
+ });
153
219
  }
154
220
  return css;
155
221
  }
@@ -353,6 +419,8 @@ export async function takeCensus(root, opts) {
353
419
  const islands = new Map();
354
420
  /** How each component expresses style, so the census can state its own coverage. */
355
421
  const shapes = new Map();
422
+ /** A escala de espaçamento que o projeto declara para o `sx` - ver o uso abaixo. */
423
+ let sxSpacing;
356
424
  /** Component FILES the gate accepted - the honest denominator for coverage. A first
357
425
  * version used the whole inventory, which includes everything they compose from a
358
426
  * package, and reported 128 for a library of 36 (probe, 01/08). */
@@ -481,6 +549,20 @@ export async function takeCensus(root, opts) {
481
549
  * permite o relatório dizer o que existe e não foi lido, em vez de só o que foi.
482
550
  */
483
551
  fragments.push(fragmentsOfSource(rel, src));
552
+ /**
553
+ * A ESCALA DE ESPAÇAMENTO DELES, se eles declaram uma.
554
+ *
555
+ * `mt: 2` num `sx` significa `margin-top: 2 * spacing`, e o `spacing` é decisão do projeto.
556
+ * 8px é o default DOCUMENTADO do MUI - fato da biblioteca que eles escolheram, não palpite
557
+ * nosso - mas um projeto que escreve `createTheme({ spacing: 4 })` está declarando outra
558
+ * coisa, e a convenção do usuário ganha da nossa sempre. Medido uma vez, aqui, onde os
559
+ * arquivos já estão abertos: reabri-los depois seria a esteira medindo duas vezes.
560
+ */
561
+ if (sxSpacing == null && /createTheme\s*\(/.test(src)) {
562
+ const declaredSpacing = spacingOf(src);
563
+ if (declaredSpacing !== MUI_DEFAULT_SPACING)
564
+ sxSpacing = declaredSpacing;
565
+ }
484
566
  for (const d of found) {
485
567
  sourceOf.set(d.name, { file: rel, source: src });
486
568
  /** O que a peça É, do portão - ver `GateVerdict.kind` e `CensusLook.kind`. */
@@ -564,7 +646,13 @@ export async function takeCensus(root, opts) {
564
646
  let fromModule = null;
565
647
  for (const mod of modules) {
566
648
  const cssPath = join(dirname(file), mod.specifier);
567
- const moduleCss = await readFile(cssPath, "utf8").catch(() => "");
649
+ /**
650
+ * O MODULE E AS PARTIALS QUE ELE IMPORTA, que no build dele são um arquivo só. Ler apenas
651
+ * o `.module.scss` deixava 1 400 declarações fora - as faces light e dark de classes que
652
+ * o componente já veste, escritas em `_lightMode.scss`/`_darkMode.scss` ao lado.
653
+ */
654
+ const sheet = await sheetWithImports(cssPath);
655
+ const moduleCss = sheet.css.trim();
568
656
  if (!moduleCss)
569
657
  continue;
570
658
  const usage = readModuleUsage(src, mod.local);
@@ -574,8 +662,14 @@ export async function takeCensus(root, opts) {
574
662
  * ESTE MODULE PERTENCE A UM COMPONENTE ADMITIDO - o único fato que este laço tem e o
575
663
  * ledger precisa. Os fragmentos dele vêm do `harvestOwnCss`, que vê todos os arquivos
576
664
  * de estilo e não só os de componente aceito.
665
+ *
666
+ * A PARTIAL ENTRA JUNTO: ela é parte deste module, então o veredito dela é o mesmo. Sem
667
+ * isto o relatório continuaria chamando de "forma sem leitor" exatamente o que a receita
668
+ * acabou de ler - relatório e receita discordando, que é o defeito que o ledger existe
669
+ * para não ter.
577
670
  */
578
- admittedStyles.add(relative(root, cssPath));
671
+ for (const part of sheet.files)
672
+ admittedStyles.add(relative(root, part));
579
673
  const readModule = transcribeModule(readModuleCss(moduleCss), usage, declaredValues);
580
674
  if (readModule.read === 0)
581
675
  continue;
@@ -597,18 +691,34 @@ export async function takeCensus(root, opts) {
597
691
  * not more honest than literal - v1 mirrors, v2 normalises.
598
692
  */
599
693
  const inlineStyle = readInlineStyle(src);
694
+ /**
695
+ * O QUE UM COMPONENTE `styled` DECLARA - e ele não tem JSX, então nada até aqui o leu.
696
+ *
697
+ * `const Title = styled.h1\`font-size: 1.5rem\`` é a aparência de um `<h1>` inteiro, com o
698
+ * nome do componente na variável. O corpo é lido pelo MESMO leitor de regra que lê um
699
+ * module, então `&:hover` e `@media` vêm de graça; o que depende de `${}` é contado e não
700
+ * lido, porque não está na fonte (medido no app dele: 9 componentes, 100 declarações, 5
701
+ * com interpolação).
702
+ */
703
+ const styledHere = readStyledComponents(src).find((c) => c.name === found[0].name);
600
704
  const base = {
601
705
  ...inlineStyle,
706
+ ...styledHere?.base,
602
707
  ...fromModule?.base,
603
708
  ...v.base.base,
604
709
  ...t.base,
605
710
  };
606
711
  const states = {
712
+ ...styledHere?.states,
607
713
  ...fromModule?.states,
608
714
  ...v.base.states,
609
715
  ...t.states,
610
716
  };
611
- const dark = { ...fromModule?.dark, ...t.dark };
717
+ const dark = {
718
+ ...styledHere?.dark,
719
+ ...fromModule?.dark,
720
+ ...t.dark,
721
+ };
612
722
  const layers = [...(fromModule?.layers ?? []), ...v.layers];
613
723
  const parts = { ...fromModule?.parts, ...t.parts };
614
724
  const tree = fromModule?.tree ?? [];
@@ -1468,6 +1578,11 @@ export async function takeCensus(root, opts) {
1468
1578
  * ninguém ter que reportar (dono, 04/08).
1469
1579
  */
1470
1580
  ledger,
1581
+ /**
1582
+ * A ESCALA QUE O `sx` DELES USA, só quando eles declaram uma diferente do default do MUI.
1583
+ * Medida no censo e consumida pela derivação - ver `readSxProps`.
1584
+ */
1585
+ ...(sxSpacing ? { sxSpacing } : {}),
1471
1586
  /**
1472
1587
  * THE SHAPE, AND THE RULE IT BECOMES.
1473
1588
  *
@@ -2228,9 +2343,9 @@ async function resolveReadParts(census, root) {
2228
2343
  ? resolveAnatomy(framed, declared, deps, resolveRef, entry?.root,
2229
2344
  // The component's own sketch, so a node naming itself by index reads
2230
2345
  // the exact class string the census measured (dono, 01/08).
2231
- looks[component]?.sketch)
2346
+ looks[component]?.sketch, census.sxSpacing)
2232
2347
  : derived && derived.read.length > 0
2233
- ? resolveAnatomy(derived.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch)
2348
+ ? resolveAnatomy(derived.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch, census.sxSpacing)
2234
2349
  : Array.isArray(entry?.parts) && entry.parts.length > 0
2235
2350
  ? resolveFlatParts(entry.parts, declared)
2236
2351
  : null;
@@ -220,6 +220,18 @@ export function gateComponent(input) {
220
220
  because: `\`${name}\` renders \`${adapter}\`'s components and no element of its own - its markup and every pixel are that library's, so there is nothing here for this system to re-theme`,
221
221
  };
222
222
  }
223
+ /**
224
+ * UM COMPONENTE `styled` NÃO TEM MARKUP E É UM COMPONENTE.
225
+ *
226
+ * `const Title = styled.h1\`font-size: 1.5rem\`` não escreve JSX nenhum, então caía em "config":
227
+ * uma constante. Mas ele decide a aparência de um `<h1>` inteiro e o nome dele é o nome do
228
+ * componente - recusá-lo é a mesma falha de classificar por FORMA DO ARQUIVO em vez de pelo que
229
+ * a coisa é (medido no app dele: 9 desses, 100 declarações).
230
+ *
231
+ * Antes do teste de JSX, e não depois, porque o teste de JSX é exatamente o que erra aqui.
232
+ */
233
+ if (STYLED_FACTORY.test(source))
234
+ return { ok: true };
223
235
  if (!HAS_JSX.test(source)) {
224
236
  return {
225
237
  ok: false,
@@ -252,6 +264,8 @@ export function gateComponent(input) {
252
264
  * components", which is the one thing a census must never imply. Every name is
253
265
  * carried; the printer shows the first few per group and says how many more.
254
266
  */
267
+ /** `styled.h1\`…\`` / `styled(Botao)\`…\`` - uma fábrica de componente, não uma constante. */
268
+ const STYLED_FACTORY = /\bstyled(?:\.[a-zA-Z][\w]*|\([^)]{1,60}\))\s*(?:<[^>]{0,120}>)?\s*`/;
255
269
  export const SKIP_LABEL = {
256
270
  route: "routes and framework files",
257
271
  screen: "whole screens, and the frames around them",
@@ -335,6 +335,50 @@ export function moduleImports(source) {
335
335
  specifier: m[2],
336
336
  }));
337
337
  }
338
+ /**
339
+ * `@import "./_darkMode"`, `@use "./_lightMode.scss"` - o que uma folha traz para dentro de si.
340
+ *
341
+ * UMA PARTIAL NÃO É UMA FOLHA. Um arquivo `_nome.scss` nunca é compilado sozinho - o sass o
342
+ * compila DENTRO de quem o importa, e por isso o escopo dele é o de quem importa. No app do dono,
343
+ * 122 das 138 folhas "globais" são partials e 120 delas são importadas por um `.module.scss` do
344
+ * mesmo diretório: 1 400 declarações (91%) que a esteira classificava como CSS global sem leitor
345
+ * são, no build dele, regras de module - hasheadas e escopadas ao componente (medido, 05/08).
346
+ *
347
+ * Decidir escopo por NOME DE ARQUIVO (`/\.module\./`) é a lei 14 um nível abaixo: a resposta está
348
+ * no grafo de imports, não na string do caminho.
349
+ */
350
+ const SHEET_IMPORT = /@(?:import|use|forward)\s+["']([^"']+)["']/g;
351
+ export function sheetImports(css) {
352
+ SHEET_IMPORT.lastIndex = 0;
353
+ return ([...stripCssComments(css).matchAll(SHEET_IMPORT)]
354
+ .map((m) => m[1].trim())
355
+ /**
356
+ * Só o que é relativo. `@use "sass:math"` e um pacote do node_modules não carregam decisão de
357
+ * aparência deles, e seguir esses caminhos é como um leitor de CSS vira um resolvedor de
358
+ * módulos pela metade.
359
+ */
360
+ .filter((spec) => spec.startsWith(".") || !spec.includes(":"))
361
+ .filter((spec) => !spec.startsWith("~") && !spec.startsWith("url(")));
362
+ }
363
+ /** `./_darkMode` → os nomes de arquivo que o sass tentaria, na ordem em que ele tenta. */
364
+ export function partialCandidates(specifier) {
365
+ const clean = specifier.replace(/^\.\//, "");
366
+ const slash = clean.lastIndexOf("/");
367
+ const dir = slash === -1 ? "" : `${clean.slice(0, slash)}/`;
368
+ const base = slash === -1 ? clean : clean.slice(slash + 1);
369
+ if (/\.(css|scss|sass|less)$/.test(base))
370
+ return base.startsWith("_")
371
+ ? [`${dir}${base}`]
372
+ : [`${dir}${base}`, `${dir}_${base}`];
373
+ const stem = base.startsWith("_") ? base : `_${base}`;
374
+ return [
375
+ `${dir}${base}.scss`,
376
+ `${dir}${stem}.scss`,
377
+ `${dir}${base}.sass`,
378
+ `${dir}${stem}.sass`,
379
+ `${dir}${base}.css`,
380
+ ];
381
+ }
338
382
  /**
339
383
  * A generic in type position, not an element.
340
384
  *
@@ -13,6 +13,7 @@
13
13
  * se ele foi lido é a interpretação, que roda depois. Juntar as duas coisas numa função seria
14
14
  * repetir a mistura que a tabela de cobertura já paga: forma e veredito são fatos diferentes.
15
15
  */
16
+ import { readStyledComponents, readSxProps } from "./style-props.js";
16
17
  import { readInlineStyle, transcribe } from "./transcribe.js";
17
18
  /** `className="…"` e `className={"…"}` - o literal puro. */
18
19
  const CLASS_LITERAL = /className=(?:"([^"]{0,400})"|\{\s*"([^"]{0,400})"\s*\})/g;
@@ -25,10 +26,16 @@ const CALL_LITERAL = /\b(?:cn|clsx|classNames|cx|twMerge|twJoin|classcat)\(([^)]
25
26
  const STRING_IN_CALL = /"([^"]{0,300})"|'([^']{0,300})'/g;
26
27
  /** `style={{ … }}`. */
27
28
  const INLINE = /style=\{\{([^}]{0,400})\}\}/g;
28
- /** `sx={{ … }}`. */
29
- const SX = /sx=\{\{([^}]{0,400})\}\}/g;
30
- /** `styled.div\`…\`` e `styled(X)\`…\``. */
31
- const STYLED = /styled[.(][^`]{0,60}`([^`]{0,400})`/g;
29
+ /**
30
+ * `sx={{ … }}` e `styled.x\`…\``, achados por CHAVE BALANCEADA e sem teto.
31
+ *
32
+ * As duas versões anteriores eram um regex com `[^}]{0,400}` e `[^`]{0,400}`, e as duas mentiam:
33
+ * `sx={{ "&:hover": { … } }}` para no primeiro `}` e não casa, e um template de 24 declarações
34
+ * passa de 400 chars e desaparece. Medido no app dele: 5 dos 58 `sx` e 3 dos 9 `styled` eram
35
+ * invisíveis - num ledger cuja promessa é que nada é descartado em silêncio (05/08).
36
+ */
37
+ const SX_OPEN = /\bsx=\{\{/g;
38
+ const STYLED_OPEN = /\bstyled(?:\.[a-zA-Z][\w]*|\([^)]{1,60}\))\s*(?:<[^>]{0,120}>)?\s*`/g;
32
39
  /**
33
40
  * Um pedaço de linha de estilo e o que o termina: `{` faz do pedaço um seletor, `;` e `}` fazem
34
41
  * dele uma declaração. Ler a linha inteira de uma vez perde `.card { padding: 8px }` numa linha só,
@@ -97,12 +104,55 @@ export function fragmentsOfSource(file, source) {
97
104
  }
98
105
  for (const m of source.matchAll(INLINE))
99
106
  push("inline", m.index ?? 0, m[1] ?? "");
100
- for (const m of source.matchAll(SX))
101
- push("sx", m.index ?? 0, m[1] ?? "", "shape-not-read");
102
- for (const m of source.matchAll(STYLED))
103
- push("styled", m.index ?? 0, m[1] ?? "", "shape-not-read");
107
+ for (const m of source.matchAll(SX_OPEN)) {
108
+ const from = (m.index ?? 0) + m[0].length;
109
+ push("sx", m.index ?? 0, source.slice(from, balanced(source, from, "}")));
110
+ }
111
+ for (const m of source.matchAll(STYLED_OPEN)) {
112
+ const from = (m.index ?? 0) + m[0].length;
113
+ push("styled", m.index ?? 0, source.slice(from, backtick(source, from)));
114
+ }
104
115
  return out;
105
116
  }
117
+ /** O fim de um objeto aberto com duas chaves - `sx={{ … }}` fecha em duas. */
118
+ function balanced(source, from, close) {
119
+ let depth = 2;
120
+ let i = from;
121
+ while (i < source.length && depth > 0) {
122
+ if (source[i] === "{")
123
+ depth += 1;
124
+ else if (source[i] === close)
125
+ depth -= 1;
126
+ i += 1;
127
+ }
128
+ return i - 2;
129
+ }
130
+ /** O backtick que fecha um template, pulando `${…}` e escape. */
131
+ function backtick(source, from) {
132
+ let i = from;
133
+ while (i < source.length) {
134
+ if (source[i] === "\\") {
135
+ i += 2;
136
+ continue;
137
+ }
138
+ if (source[i] === "`")
139
+ return i;
140
+ if (source[i] === "$" && source[i + 1] === "{") {
141
+ let depth = 1;
142
+ i += 2;
143
+ while (i < source.length && depth > 0) {
144
+ if (source[i] === "{")
145
+ depth += 1;
146
+ else if (source[i] === "}")
147
+ depth -= 1;
148
+ i += 1;
149
+ }
150
+ continue;
151
+ }
152
+ i += 1;
153
+ }
154
+ return source.length;
155
+ }
106
156
  /**
107
157
  * Os fragmentos de um arquivo de estilo: uma declaração é um fragmento.
108
158
  *
@@ -110,7 +160,20 @@ export function fragmentsOfSource(file, source) {
110
160
  * contagem: um global com regra de CLASSE é forma que nenhum leitor cobre hoje - 399 declarações
111
161
  * no repo dele, e a única lacuna que estava calada em vez de declarada (04/08).
112
162
  */
113
- export function fragmentsOfStylesheet(file, css, scoped) {
163
+ export function fragmentsOfStylesheet(file, css,
164
+ /**
165
+ * COMO ESTA FOLHA CHEGA NO BUILD DELES, e isso vem do GRAFO DE IMPORTS e nunca do nome do
166
+ * arquivo. Uma partial (`_darkMode.scss`) é compilada dentro de quem a importa e herda o escopo
167
+ * dele: 122 das 138 folhas "globais" do app dele são partials, e 120 entram num `.module.scss`
168
+ * ao lado (medido, 05/08).
169
+ *
170
+ * scoped module, ou partial de um module: o veredito é do componente dono, e
171
+ * `judgeFragments` o dá olhando se o escopo o admitiu
172
+ * global uma folha que o código importa: forma que nenhum leitor cobre hoje
173
+ * dead partial que ninguém importa: o build deles não a compila, e ela não
174
+ * pinta nada - aqui, nem lá
175
+ */
176
+ kind) {
114
177
  const out = [];
115
178
  let selector = "";
116
179
  for (const [i, raw] of css.split("\n").entries()) {
@@ -143,7 +206,11 @@ export function fragmentsOfStylesheet(file, css, scoped) {
143
206
  line: i + 1,
144
207
  text: `${selector ? `${selector} ` : ""}{ ${m[1]}: ${m[2].trim()} }`,
145
208
  read: false,
146
- ...(scoped ? {} : { reason: "shape-not-read" }),
209
+ ...(kind === "global"
210
+ ? { reason: "shape-not-read" }
211
+ : kind === "dead"
212
+ ? { reason: "sheet-not-imported" }
213
+ : {}),
147
214
  });
148
215
  }
149
216
  }
@@ -212,6 +279,36 @@ export function judgeFragments(seen, declared, admitted) {
212
279
  read: false,
213
280
  reason: "component-not-admitted",
214
281
  };
282
+ case "sx": {
283
+ /**
284
+ * O MESMO LEITOR QUE A RECEITA USA - `readSxProps`. Um segundo julgamento escrito à parte
285
+ * é a maneira mais rápida de o relatório e a receita começarem a discordar.
286
+ */
287
+ const read = readSxProps(f.text);
288
+ return Object.keys(read.declarations).length > 0
289
+ ? { ...f, read: true }
290
+ : {
291
+ ...f,
292
+ read: false,
293
+ /**
294
+ * Nada lido de um `sx` que EXISTE só acontece por valor: um ternário, um objeto
295
+ * responsivo, ou um seletor que aponta para o interior de uma biblioteca. A forma é
296
+ * lida - dizer `shape-not-read` aqui mandaria alguém reescrever o leitor que
297
+ * acabou de rodar.
298
+ */
299
+ reason: "value-not-read",
300
+ };
301
+ }
302
+ case "styled": {
303
+ const read = readStyledComponents(`const X = styled.div\`${f.text}\`;`);
304
+ const first = read[0];
305
+ const got = Object.keys(first?.base ?? {}).length +
306
+ Object.keys(first?.dark ?? {}).length +
307
+ Object.keys(first?.states ?? {}).length;
308
+ return got > 0
309
+ ? { ...f, read: true }
310
+ : { ...f, read: false, reason: "value-not-read" };
311
+ }
215
312
  default:
216
313
  return { ...f, read: false, reason: "shape-not-read" };
217
314
  }
@@ -84,7 +84,14 @@ function whereOf(shape, reason) {
84
84
  module: null,
85
85
  work: reason === "component-not-admitted"
86
86
  ? "the gate refused the component: these styles exist and no recipe claims them. Changing that means moving the GATE - a decision about the vitrine, not a reader to write"
87
- : "the value is assembled at render time and is not in the source at any level - there is nothing to read",
87
+ : reason === "sheet-not-imported"
88
+ ? /**
89
+ * NÃO É LACUNA NOSSA NEM DELES-QUE-A-GENTE-CONSERTA: é um arquivo que o build deles
90
+ * não compila. Cair na frase de runtime dizia que o valor é montado em tempo de
91
+ * execução, o que é falso e mandaria alguém procurar o que não existe.
92
+ */
93
+ "a sass partial no stylesheet imports: their own build never compiles it, so it paints nothing today - here or in their app. Deleting it is their call, not a reader to write"
94
+ : "the value is assembled at render time and is not in the source at any level - there is nothing to read",
88
95
  };
89
96
  }
90
97
  }
@@ -431,6 +431,29 @@ function closeOf(source, open) {
431
431
  * `className={styles.content}` e `className={s.content}` - a forma que um app de CSS Modules
432
432
  * usa em todo elemento. O identificador é o import do module, e a propriedade é a classe.
433
433
  */
434
+ /**
435
+ * O CORPO DE `sx={{ … }}`, por chave balanceada.
436
+ *
437
+ * Um regex `[^}]` para no primeiro `}` e perde `sx={{ "&:hover": { … } }}` inteiro, que é
438
+ * justamente a forma em que a declaração aninhada mora.
439
+ */
440
+ function sxBody(body) {
441
+ const at = body.search(/\bsx=\{\{/);
442
+ if (at === -1)
443
+ return undefined;
444
+ const from = at + (/\bsx=\{\{/.exec(body)?.[0].length ?? 0);
445
+ let depth = 2;
446
+ let i = from;
447
+ while (i < body.length && depth > 0) {
448
+ if (body[i] === "{")
449
+ depth += 1;
450
+ else if (body[i] === "}")
451
+ depth -= 1;
452
+ i += 1;
453
+ }
454
+ const text = body.slice(from, i - 2).trim();
455
+ return text || undefined;
456
+ }
434
457
  const MODULE_CLASS = /className=\{\s*([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*\}/;
435
458
  export function sketchOf(source, name) {
436
459
  /**
@@ -528,6 +551,10 @@ export function sketchOf(source, name) {
528
551
  const mod = MODULE_CLASS.exec(event.body);
529
552
  if (mod)
530
553
  node.moduleClass = mod[2];
554
+ /** A prop de estilo que ele carrega - ver `sx`. */
555
+ const sx = sxBody(event.body);
556
+ if (sx)
557
+ node.sx = sx;
531
558
  // What has to be true for this element to exist at all - see `guardAt`.
532
559
  const guard = guardAt(source, event.at);
533
560
  if (guard) {
@@ -54,6 +54,7 @@ const BECAUSE = {
54
54
  "value-not-read": "the shape is read and this value is not - a utility or property outside the scales this project declares",
55
55
  computed: "the value is assembled at render time, so it is not in the source at any level and there is nothing to read",
56
56
  "component-not-admitted": "read, and it belongs to a component this scope does not admit into the system - the styles exist and no recipe claims them",
57
+ "sheet-not-imported": "this is a sass partial and no stylesheet imports it, so your own build never compiles it - it paints nothing today, here or anywhere",
57
58
  };
58
59
  /**
59
60
  * O ledger de uma varredura, a partir dos fragmentos vistos.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.135",
3
+ "version": "0.16.137",
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": {