synthesisui 0.16.129 → 0.16.131

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.
@@ -467,9 +467,16 @@ versionOf) {
467
467
  */
468
468
  if (hasKids && as !== "row" && as !== "stack")
469
469
  as = arrangement(classes);
470
- // A plain structural box earns a name only when it carries decisions - and
471
- // it must earn one, because the name is what the class string travels on.
472
- if (!name && classes)
470
+ /**
471
+ * A plain structural box earns a name only when it carries decisions - and it must earn
472
+ * one, because the name is what the class string travels on.
473
+ *
474
+ * UMA CLASSE DE MODULE TAMBÉM É DECISÃO. Num app de CSS Modules o elemento veste
475
+ * `className={styles.content}` e nenhuma string: sem isto o nó não ganhava nome, não virava
476
+ * part, e o bloco de 16 declarações lido do module não tinha onde pousar. O Tooltip dele
477
+ * chegava com 2 das 35 (04/08).
478
+ */
479
+ if (!name && (classes || node.moduleClass))
473
480
  name = "box";
474
481
  /**
475
482
  * A NODE WHOSE CONTENT IS THE CALLER'S is a slot, and it keeps its own name
@@ -261,6 +261,8 @@ export async function takeCensus(root, opts) {
261
261
  const tally = emptyTally();
262
262
  const internal = await internalSpecifiers(root);
263
263
  const defined = [];
264
+ /** Nome → arquivo e fonte, para toda leitura por componente - ver o preenchimento abaixo. */
265
+ const sourceOf = new Map();
264
266
  /** What the gate turned away, with the reason. Never dropped in silence. */
265
267
  const skips = [];
266
268
  // Kept for the reference check below, which needs every file at once: a name
@@ -411,6 +413,21 @@ export async function takeCensus(root, opts) {
411
413
  componentFiles += 1;
412
414
  }
413
415
  defined.push(...found);
416
+ /**
417
+ * O ARQUIVO E A FONTE DE CADA COMPONENTE QUE GANHA UM LOOK.
418
+ *
419
+ * O contrato de dados era estampado a partir de `defined`, a lista que o PORTÃO de
420
+ * componente aceita - 36 numa biblioteca e 29 num app de 690 arquivos. Então 242 receitas
421
+ * do dashboard dele e 19 do web-review chegavam sem `api` nenhum: sem props, sem
422
+ * derivação, sem tipo de eixo e sem o especificador de import, embora 41% e 61% dos
423
+ * arquivos declarem um `<Nome>Props` (04/08).
424
+ *
425
+ * É a mesma classe de defeito da elegibilidade, um nível ao lado: uma leitura pendurada
426
+ * numa lista feita para biblioteca. O look já nasce aqui, com o arquivo e a fonte na mão,
427
+ * e é aqui que a leitura tem que se pendurar.
428
+ */
429
+ for (const d of found)
430
+ sourceOf.set(d.name, { file: rel, source: src });
414
431
  // THE LOOK, from their own class names. One transcription per file, keyed
415
432
  // by the component it defines - the root element's classes are the
416
433
  // component's own look, and everything below it is a part this slice does
@@ -1239,12 +1256,23 @@ export async function takeCensus(root, opts) {
1239
1256
  * as not exported. See `publicApi`.
1240
1257
  */
1241
1258
  const api = await publicApi(root);
1242
- for (const c of components) {
1243
- const file = defined.find((d) => d.name === c.name)?.file;
1244
- const source = file
1245
- ? sources.find((s) => s.file.endsWith(file))?.source
1246
- : undefined;
1247
- const required = source ? requiredProps(source, c.name) : [];
1259
+ /**
1260
+ * TODO NOME QUE TEM LEITURA, e não só os que o inventário de USO lista.
1261
+ *
1262
+ * O laço iterava `components` - o inventário de uso - e pendurava o arquivo em `defined`. Duas
1263
+ * listas feitas para biblioteca, e num app nenhuma das duas alcança uma tela: 242 receitas do
1264
+ * dashboard dele ficaram sem `api` (04/08). Agora a leitura acompanha o LOOK, que é onde o
1265
+ * resto da leitura daquele componente já vive, e o inventário continua sendo inventário.
1266
+ */
1267
+ const named = [
1268
+ ...new Set([...components.map((c) => c.name), ...Object.keys(looks)]),
1269
+ ];
1270
+ for (const name of named) {
1271
+ const c = components.find((x) => x.name === name);
1272
+ const at = sourceOf.get(name);
1273
+ const file = at?.file;
1274
+ const source = at?.source;
1275
+ const required = source ? requiredProps(source, name) : [];
1248
1276
  /**
1249
1277
  * THE COMPONENT'S DATA CONTRACT - its props, its derivations, the copy an option
1250
1278
  * carries, and the type that names an axis's options.
@@ -1256,11 +1284,11 @@ export async function takeCensus(root, opts) {
1256
1284
  * Stamped whether or not the folder is a package: a `src/components` with no
1257
1285
  * manifest of its own has no specifier to offer and its components still have props.
1258
1286
  */
1259
- const span = source ? definitionSpan(source, c.name) : null;
1287
+ const span = source ? definitionSpan(source, name) : null;
1260
1288
  const contract = source
1261
- ? dataContract(source, span ? source.slice(span.from, span.to) : source, c.name, (c.declaredAxes ?? {}))
1289
+ ? dataContract(source, span ? source.slice(span.from, span.to) : source, name, (c?.declaredAxes ?? looks[name]?.declaredAxes ?? {}))
1262
1290
  : null;
1263
- c.api = {
1291
+ const stamped = {
1264
1292
  /**
1265
1293
  * SILENCE WHEN THE ENTRY WAS NOT READ. An unreadable entry gives an empty export
1266
1294
  * set, and calling that "internal" would have marked every component internal at
@@ -1268,7 +1296,7 @@ export async function takeCensus(root, opts) {
1268
1296
  */
1269
1297
  ...(!api?.read
1270
1298
  ? {}
1271
- : api.exports.has(c.name)
1299
+ : api.exports.has(name)
1272
1300
  ? { from: api.from }
1273
1301
  : { exported: false }),
1274
1302
  /**
@@ -1294,8 +1322,18 @@ export async function takeCensus(root, opts) {
1294
1322
  : {}),
1295
1323
  };
1296
1324
  // An api with nothing in it is not a fact - it is an empty object on 36 components.
1297
- if (Object.keys(c.api).length === 0)
1298
- c.api = undefined;
1325
+ /**
1326
+ * ONDE ELE MORA: no LOOK, que é por componente, e também na entrada do inventário quando
1327
+ * existe uma. A entrada é o que a plataforma sempre leu; o look é o que alcança as 242 que
1328
+ * o inventário não tem. Uma leitura, dois endereços, e a plataforma prefere o look.
1329
+ */
1330
+ if (Object.keys(stamped).length === 0)
1331
+ continue;
1332
+ if (c)
1333
+ c.api = stamped;
1334
+ const look = looks[name];
1335
+ if (look)
1336
+ look.api = stamped;
1299
1337
  }
1300
1338
  const brokenRefs = findBrokenRefs(sources, declaredNames);
1301
1339
  const conventions = detectConventions(sources);
@@ -2136,6 +2174,43 @@ async function resolveReadParts(census, root) {
2136
2174
  }
2137
2175
  target.base = { ...style, ...target.base };
2138
2176
  }
2177
+ /**
2178
+ * O QUE UM CSS MODULE DECLAROU, ONTO THE PART THAT WEARS THAT CLASS.
2179
+ *
2180
+ * Num app de CSS Modules a classe do module é a identidade do elemento, e a anatomia dá
2181
+ * outro nome ao mesmo nó - o `content` do Tooltip dele vira `text` pelo papel. As duas
2182
+ * listas nunca se encontravam, porque a junção era por NOME: o módulo entregava
2183
+ * `parts.content` com 16 declarações e a anatomia substituía tudo por `parts.text` vazia.
2184
+ * As 35 declarações do Tooltip chegavam como 2 (04/08).
2185
+ *
2186
+ * A junção é por CLASSE, que é um dado do próprio nó - ver `SketchNode.moduleClass`. Uma
2187
+ * classe cujo nó a anatomia não nomeia é dita em voz alta, nunca empurrada para a raiz.
2188
+ */
2189
+ const sketch = (look?.sketch ?? []);
2190
+ const fromModule = look?.parts ?? {};
2191
+ for (const [index, node] of sketch.entries()) {
2192
+ const cls = node.moduleClass;
2193
+ if (!cls)
2194
+ continue;
2195
+ const block = fromModule[cls];
2196
+ if (!block)
2197
+ continue;
2198
+ const part = partAt[index];
2199
+ const target = part ? resolved.parts[part] : undefined;
2200
+ if (!target) {
2201
+ notes.add(`\`.${cls}\` in a CSS module styles an element the anatomy does not name, so it is not applied - putting it on the component would paint the wrong thing`);
2202
+ continue;
2203
+ }
2204
+ /** O que a class string já disse ganha: um utilitário no mesmo elemento é mais recente. */
2205
+ target.base = { ...block.base, ...target.base };
2206
+ if (Object.keys(block.dark ?? {}).length > 0)
2207
+ target.dark = { ...block.dark, ...target.dark };
2208
+ for (const [state, decls] of Object.entries(block.states ?? {}))
2209
+ target.states = {
2210
+ ...target.states,
2211
+ [state]: { ...decls, ...(target.states?.[state] ?? {}) },
2212
+ };
2213
+ }
2139
2214
  /**
2140
2215
  * REPLACED, NOT APPENDED - because this runs more than once.
2141
2216
  *
@@ -83,7 +83,19 @@ function rules(css, media = null, out = []) {
83
83
  const open = css.indexOf("{", i);
84
84
  if (open === -1)
85
85
  break;
86
- const head = css.slice(i, open).trim();
86
+ /**
87
+ * O SELETOR É O QUE VEM DEPOIS DA ÚLTIMA DECLARAÇÃO, não o texto todo até a chave.
88
+ *
89
+ * Num CSS plano o cabeçalho de uma regra já vem limpo, e por isso isto passou. Num `.scss`
90
+ * aninhado não: o corpo de `.content` é
91
+ *
92
+ * background-color: …; border-radius: 4px; .header {
93
+ *
94
+ * e o cabeçalho da regra do `.header` saía com as duas declarações grudadas na frente.
95
+ * `classesIn` então achava fragmentos como `0` (de `opacity: 0`) e 12 seletores por arquivo
96
+ * iam para `unslotted` (04/08).
97
+ */
98
+ const head = css.slice(i, open).split(/[;}]/).pop()?.trim() ?? "";
87
99
  // Balance to the matching close, because an at-rule contains whole rules.
88
100
  let depth = 0;
89
101
  let close = -1;
@@ -113,6 +125,33 @@ function rules(css, media = null, out = []) {
113
125
  }
114
126
  else if (head) {
115
127
  out.push({ selector: head, body, media });
128
+ /**
129
+ * UM BLOCO ANINHADO É UMA REGRA, e não texto do pai.
130
+ *
131
+ * Esta varredura era plana, e num `.scss` isso significa que
132
+ *
133
+ * [data-theme="light"] { .container { .content { background } } }
134
+ *
135
+ * produzia UMA regra: seletor `[data-theme="light"]`, sem classe nenhuma, então
136
+ * `unslotted` - e o bloco inteiro ia para o lixo. Já a versão sem tema produzia
137
+ * `.container` com TODAS as declarações de todos os níveis, porque `declarations()`
138
+ * varre o corpo e o corpo é o arquivo. Medido no repo dele (04/08): 196 de 200 modules
139
+ * usam aninhamento, o Tooltip saiu com 19 declarações na raiz e 4 classes perdidas, 969
140
+ * de 1637 parts vieram vazias, e 109 blocos dark viraram 5.
141
+ *
142
+ * Descer com o seletor do pai na mão resolve os três de uma vez: o filho passa a ter
143
+ * seletor completo, `classesIn` acha a classe certa, a relação pai-filho é registrada, e
144
+ * `[data-theme="dark"] .x` casa o teste de tema. O `&` do Sass é resolvido contra o pai,
145
+ * que é o que ele significa.
146
+ */
147
+ for (const nested of rules(body, media)) {
148
+ out.push({
149
+ ...nested,
150
+ selector: nested.selector.includes("&")
151
+ ? nested.selector.replace(/&/g, head)
152
+ : `${head} ${nested.selector}`,
153
+ });
154
+ }
116
155
  }
117
156
  i = close + 1;
118
157
  }
@@ -121,8 +160,22 @@ function rules(css, media = null, out = []) {
121
160
  /** `prop: value;` pairs, in the camelCase the contract uses. */
122
161
  function declarations(body) {
123
162
  const out = {};
124
- // Only the top level: a nested rule's declarations belong to that rule.
125
- const flat = body.replace(/\{[^{}]*\}/g, "").replace(/[^;]*\{/g, "");
163
+ /**
164
+ * SÓ O NÍVEL DE CIMA - e "só" exige repetir, porque um module aninha mais de um nível.
165
+ *
166
+ * Uma passada tira o bloco mais interno e deixa o do meio aberto, então as declarações do
167
+ * meio vazavam para o pai: `.card { padding; .title { font-size; > h3 { margin } } }` dava
168
+ * `font-size` ao card e `margin` ao title. Com 196 de 200 modules dele aninhando e a maioria
169
+ * em três níveis, é o vazamento que fazia a raiz vestir o module inteiro (04/08).
170
+ */
171
+ let flat = body;
172
+ for (let guard = 0; guard < 12; guard++) {
173
+ const next = flat.replace(/\{[^{}]*\}/g, "");
174
+ if (next === flat)
175
+ break;
176
+ flat = next;
177
+ }
178
+ flat = flat.replace(/[^;{}]*\{/g, "");
126
179
  for (const piece of flat.split(";")) {
127
180
  const colon = piece.indexOf(":");
128
181
  if (colon === -1)
@@ -427,6 +427,11 @@ function closeOf(source, open) {
427
427
  }
428
428
  return -1;
429
429
  }
430
+ /**
431
+ * `className={styles.content}` e `className={s.content}` - a forma que um app de CSS Modules
432
+ * usa em todo elemento. O identificador é o import do module, e a propriedade é a classe.
433
+ */
434
+ const MODULE_CLASS = /className=\{\s*([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*\}/;
430
435
  export function sketchOf(source, name) {
431
436
  /**
432
437
  * READ THE IMPORTS BEFORE SLICING - they live at the top of the file, and the
@@ -519,6 +524,10 @@ export function sketchOf(source, name) {
519
524
  const list = repeatOf(source, event.at);
520
525
  if (list)
521
526
  node.repeats = list;
527
+ /** A classe de module que ele veste - ver `moduleClass`. */
528
+ const mod = MODULE_CLASS.exec(event.body);
529
+ if (mod)
530
+ node.moduleClass = mod[2];
522
531
  // What has to be true for this element to exist at all - see `guardAt`.
523
532
  const guard = guardAt(source, event.at);
524
533
  if (guard) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.129",
3
+ "version": "0.16.131",
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": {