synthesisui 0.16.274 → 0.16.276

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.
@@ -211,8 +211,17 @@ function measuredAt(node, sketch) {
211
211
  if (measured?.behaviourProps &&
212
212
  Object.keys(measured.behaviourProps).length > 0)
213
213
  out.behaviourProps = measured.behaviourProps;
214
- if (measured?.passes && Object.keys(measured.passes).length > 0)
214
+ if (measured?.passes && Object.keys(measured.passes).length > 0) {
215
215
  out.passes = measured.passes;
216
+ /**
217
+ * A FORMA VIAJA COM O VALOR, SEMPRE - inclusive vazia.
218
+ *
219
+ * Um `passes` sem `passesExpr` obriga quem emite a adivinhar entre aspas e chaves, e as duas
220
+ * adivinhações produzem código errado no arquivo dele. A lista vazia responde "nenhuma é
221
+ * expressão"; a AUSÊNCIA responde "este censo é velho", e são coisas diferentes.
222
+ */
223
+ out.passesExpr = measured.passesExpr ?? [];
224
+ }
216
225
  if (measured?.repeats)
217
226
  out.repeats = measured.repeats;
218
227
  /**
@@ -0,0 +1,150 @@
1
+ /**
2
+ * O QUE UM ATRIBUTO É, pela FORMA da chave e pela ORIGEM da tag - nunca por uma lista de nomes.
3
+ *
4
+ * O QUÊ. `passes` carrega tudo que um elemento recebe, e "tudo" são quatro coisas com destinos
5
+ * opostos: a acessibilidade que ele escreveu (`role`, `aria-valuenow`), o atributo específico do
6
+ * elemento (`type`, `viewBox`), o handler (`onMouseMove`) e a prop de OUTRO componente
7
+ * (`variant="premium"`). Emitir as quatro igual entrega código que não compila; calar as quatro
8
+ * entrega uma `<div>` muda.
9
+ *
10
+ * POR QUÊ IMPORTA. Um agente que reconstrói o componente a partir da receita escreve o que a receita
11
+ * disser. Medido nos dois clientes: `role` em 9 raízes de um e em 4 do outro, `aria-*` em 46 e 23
12
+ * chaves. Sem esta classificação a barra de progresso dele chega sem `role="progressbar"` e sem
13
+ * `aria-valuenow` - a lei 10 proíbe acessibilidade SUPOSTA, e calar a que ele ESCREVEU é o inverso.
14
+ *
15
+ * COMO. Duas perguntas, e nenhuma delas é "como se chama?":
16
+ *
17
+ * tem prefixo (`aria-`, `data-`, `xlink:`) -> global, válido em QUALQUER elemento
18
+ * a tag é minúscula -> é elemento HTML/SVG: tudo nele é atributo
19
+ * a tag é Capitalizada -> é componente: tudo nele é prop dele
20
+ *
21
+ * A lista `GLOBAL_ATTRS` é o único literal, e ela é um fato da especificação do HTML - esses
22
+ * atributos valem em todo elemento -, não um palpite sobre o código de ninguém. Medida em duas
23
+ * populações: cobre 100% dos casos de acessibilidade de raiz das duas.
24
+ *
25
+ * GÊMEO BYTE-IDÊNTICO de `packages/cli/src/attr-shape.ts`, provado por spec. O CLI é publicado
26
+ * standalone e não pode importar os contratos.
27
+ */
28
+ /**
29
+ * OS ATRIBUTOS QUE VALEM EM QUALQUER ELEMENTO, pela especificação do HTML.
30
+ *
31
+ * Não é vocabulário do cliente e não é heurística: `role` e `tabindex` são globais, e um
32
+ * `aria-*` qualquer também. Por isso eles podem ser escritos no elemento que a gente emite mesmo
33
+ * quando a tag emitida difere da que ele escreveu - o que `element` não pode.
34
+ */
35
+ const GLOBAL_ATTRS = new Set([
36
+ "role",
37
+ "title",
38
+ "id",
39
+ "lang",
40
+ "dir",
41
+ "tabIndex",
42
+ "tabindex",
43
+ "hidden",
44
+ "slot",
45
+ "translate",
46
+ "inert",
47
+ "popover",
48
+ "autoFocus",
49
+ "autofocus",
50
+ "spellCheck",
51
+ "spellcheck",
52
+ "contentEditable",
53
+ "contenteditable",
54
+ "draggable",
55
+ "accessKey",
56
+ "accesskey",
57
+ ]);
58
+ /**
59
+ * A CLASSE DE UMA CHAVE, dada a tag que a carrega.
60
+ *
61
+ * `tag` ausente significa que a origem não é conhecida, e aí a resposta conservadora é
62
+ * `composition`: guardar não quebra nada, emitir pode.
63
+ */
64
+ export function attrKind(key, tag) {
65
+ if (/^on[A-Z]/.test(key))
66
+ return "handler";
67
+ /**
68
+ * O PREFIXO É A PROVA. `aria-`, `data-` e `xlink:` são espaços de nome do próprio HTML, então uma
69
+ * chave com um deles é atributo em qualquer elemento - e é exatamente a chave que o coletor
70
+ * destruía até 22/08, quando `\w` engolia o hífen e `aria-hidden` virava `hidden`.
71
+ */
72
+ if (/^(aria|data)-/.test(key) || key.includes(":"))
73
+ return "global";
74
+ if (GLOBAL_ATTRS.has(key))
75
+ return "global";
76
+ const spelled = String(tag ?? "").trim();
77
+ if (!spelled)
78
+ return "composition";
79
+ /**
80
+ * A ORIGEM DECIDE, NÃO O NOME - a lei 13, um nível abaixo. Uma tag minúscula é um elemento do
81
+ * HTML ou do SVG: tudo que ela recebe é atributo, por construção. Uma tag Capitalizada é um
82
+ * componente, e `variant` nela é a API dele, não marcação.
83
+ */
84
+ return /^[a-z]/.test(spelled) ? "element" : "composition";
85
+ }
86
+ /** O texto JSX de um atributo, com a forma que a leitura mediu - ver `passesExpr`. */
87
+ export function attrText(key, value, isExpression) {
88
+ return isExpression ? ` ${key}={${value}}` : ` ${key}="${value}"`;
89
+ }
90
+ /**
91
+ * OS ATRIBUTOS QUE PODEM SER ESCRITOS, e os que ficam guardados - numa decisão só.
92
+ *
93
+ * A função recebe o NÓ inteiro de propósito. A versão que recebia `passes` e `passesExpr` soltos
94
+ * podia ser chamada pela metade, e um `passes` sem a forma faz quem emite adivinhar entre aspas e
95
+ * chaves: as duas adivinhações produzem JSX que não compila. Fechar a porta é mais barato que
96
+ * vigiá-la (CLAUDE.md §6).
97
+ *
98
+ * `sameTag` diz se o elemento que a gente EMITE é o mesmo que ele escreveu. Só aí um atributo
99
+ * específico de elemento pode viajar: `viewBox` num `<span>` nosso é marcação inválida no arquivo
100
+ * dele.
101
+ *
102
+ * `requires` é o conjunto de props que a receita ALVO marca como obrigatória. Uma prop de
103
+ * composição obrigatória entra agora, porque sem ela o código não roda; o resto fica guardado na
104
+ * receita e só aparece quando alguém perguntar quais opções aquele componente tem (dono, 22/08).
105
+ *
106
+ * `expressions: "hold"` para quem GERA CÓDIGO, e não é preciosismo. `aria-valuenow={filled}` é
107
+ * verdadeiro no arquivo dele, onde `filled` existe; escrito dentro de um componente que a gente
108
+ * gera, `filled` não existe em lugar nenhum e o build DELE quebra. Um snippet é para uma pessoa
109
+ * adaptar e pode citar o nome dela; um arquivo gerado é para compilar.
110
+ */
111
+ export function emittableAttrs(node, opts) {
112
+ /**
113
+ * FORMA DESCONHECIDA SEGURA TUDO - e este é o caso do censo tirado antes de 22/08.
114
+ *
115
+ * `passesExpr` ausente não quer dizer "nenhuma é expressão": quer dizer que o leitor daquele dia
116
+ * não sabia responder. Tratar como literal escreveria `title="metric.label"` no arquivo dele onde
117
+ * ele escreveu `title={metric.label}` - uma string no lugar de uma variável, que é leitura errada
118
+ * virando código errado, exatamente o defeito que esta família de consertos existe para não
119
+ * cometer.
120
+ *
121
+ * A omissão, ao contrário, é reparável: um `sync` traz a forma e o atributo aparece.
122
+ */
123
+ if (node.passes && Object.keys(node.passes).length > 0 && !node.passesExpr)
124
+ return { write: "", held: Object.keys(node.passes) };
125
+ const expr = new Set(node.passesExpr ?? []);
126
+ const write = [];
127
+ const held = [];
128
+ for (const [key, value] of Object.entries(node.passes ?? {})) {
129
+ const kind = attrKind(key, null);
130
+ /**
131
+ * A CLASSE SEM TAG SÓ SEPARA GLOBAL E HANDLER. Para as outras duas a pergunta é o que a gente
132
+ * emite, e quem sabe isso é quem chama - por isso `sameTag` e `requires` chegam de fora.
133
+ */
134
+ if (kind === "handler")
135
+ continue;
136
+ const isExpression = expr.has(key);
137
+ if (isExpression && opts?.expressions === "hold") {
138
+ held.push(key);
139
+ continue;
140
+ }
141
+ const allowed = kind === "global" ||
142
+ Boolean(opts?.sameTag) ||
143
+ Boolean(opts?.requires?.has(key));
144
+ if (allowed)
145
+ write.push(attrText(key, value, isExpression));
146
+ else
147
+ held.push(key);
148
+ }
149
+ return { write: write.join(""), held };
150
+ }
@@ -3084,13 +3084,45 @@ function rootPackage(tag, sketch) {
3084
3084
  * Off the FIRST sketch node, which is the returned element - the same lookup `rootPackage`
3085
3085
  * makes one line up, guarded the same way.
3086
3086
  */
3087
- function rootBehaviour(tag, sketch) {
3088
- const first = sketch[0];
3089
- if (!tag || !first || first.tag !== tag)
3087
+ export function rootBehaviour(tag, sketch) {
3088
+ /**
3089
+ * PELO QUE O COMPONENTE RETORNA, e não pelo primeiro do arquivo.
3090
+ *
3091
+ * `sketch[0]` é uma suposição que a segunda população desmente: um arquivo declara com frequência
3092
+ * um closure ANTES do render que o usa, e é exatamente por isso que `SketchNode.root` existe.
3093
+ * Medido: em 62 dos 323 componentes do dashboard os dois divergem, e em 4 deles o `rootProps`
3094
+ * já se perdia hoje por causa disso. No primeiro cliente o número é 0 - uma população só nunca
3095
+ * teria mostrado.
3096
+ */
3097
+ const root = sketch.find((n) => n.root) ?? sketch[0];
3098
+ if (!tag || !root || root.tag !== tag)
3090
3099
  return {};
3091
- return first.behaviourProps && Object.keys(first.behaviourProps).length > 0
3092
- ? { rootProps: first.behaviourProps }
3100
+ const props = root.behaviourProps && Object.keys(root.behaviourProps).length > 0
3101
+ ? { rootProps: root.behaviourProps }
3102
+ : {};
3103
+ /**
3104
+ * E O QUE ELE ESCREVE NA RAIZ - `role`, `aria-*`, `type`. Este é o campo que faltava.
3105
+ *
3106
+ * `passes` só sobrevive num nó da árvore, e a raiz de um componente é CONSUMIDA: ela É o
3107
+ * componente, então não existe nó onde o fato dela caiba. Medido: 26 dos 59 componentes do
3108
+ * primeiro cliente e 113 dos 323 do segundo têm `passes` numa raiz sem slot, e um agente
3109
+ * reconstruindo o `Progress` a partir da receita escrevia uma `<div>` muda - sem
3110
+ * `role="progressbar"`, sem `aria-valuenow`. A lei 10 proíbe acessibilidade SUPOSTA; isto era o
3111
+ * inverso, que é pior: ele escreveu, o censo colheu, e a receita calou.
3112
+ *
3113
+ * SEPARADO DE `rootProps` de propósito. `rootProps` significa *prop cujo valor é uma condição*, e
3114
+ * `component-behaviour.ts` extrai nomes de estado dos valores dele. Jogar `role="progressbar"`
3115
+ * ali faria "progressbar" virar um nome de estado que o componente segura.
3116
+ */
3117
+ const given = root.passes && Object.keys(root.passes).length > 0
3118
+ ? {
3119
+ rootPasses: root.passes,
3120
+ ...(root.passesExpr && root.passesExpr.length > 0
3121
+ ? { rootPassesExpr: root.passesExpr }
3122
+ : {}),
3123
+ }
3093
3124
  : {};
3125
+ return { ...props, ...given };
3094
3126
  }
3095
3127
  export async function resolveReadParts(census, root,
3096
3128
  /** `true` numa re-medição: o resumo desta etapa fica para o `sync --full` - ver `say`. */
@@ -1,3 +1,4 @@
1
+ import { emittableAttrs } from "./attr-shape.js";
1
2
  import { PHRASING_FORMS, } from "./types.js";
2
3
  export const DEFAULT_CONVENTION = {
3
4
  prefix: "ds-",
@@ -71,7 +72,27 @@ function elementFor(name, recipe) {
71
72
  ? { tag: "button", attrs: ' type="button"' }
72
73
  : undefined);
73
74
  const tag = hit?.tag ?? "div";
74
- const attrs = hit?.attrs ?? "";
75
+ /**
76
+ * O QUE ELE ESCREVEU NA RAIZ VENCE O QUE A GENTE SUPÕE - a lei 10, aplicada onde ela nasceu.
77
+ *
78
+ * A tabela acima inventa `role="switch"` a partir do NOME da receita. Isso é uma suposição
79
+ * razoável e continua sendo uma suposição; `role="progressbar"` medido no `<div>` que ELE escreveu
80
+ * é um fato. Quando os dois falam da mesma chave, o fato ganha - e até 22/08 o fato nem chegava
81
+ * aqui: 26 dos 59 componentes de um cliente e 113 dos 323 do outro tinham a raiz com atributo e a
82
+ * receita muda.
83
+ *
84
+ * SÓ LITERAL. `aria-valuenow={filled}` é verdade no arquivo dele, onde `filled` existe; escrito
85
+ * dentro de um componente que a gente GERA, `filled` não existe e o build dele quebra. Ver
86
+ * `expressions: "hold"` em `attr-shape.ts`.
87
+ */
88
+ const theirs = emittableAttrs({
89
+ passes: recipe.preview?.rootPasses,
90
+ passesExpr: recipe.preview?.rootPassesExpr,
91
+ }, {
92
+ sameTag: tag === recipe.preview?.rootTag,
93
+ expressions: "hold",
94
+ }).write;
95
+ const attrs = mergeAttrs(hit?.attrs ?? "", theirs);
75
96
  const isVoid = tag === "input" || tag === "hr";
76
97
  /**
77
98
  * A VOID ELEMENT CANNOT HOST THE PARTS WE TELL PEOPLE TO PUT INSIDE IT.
@@ -338,6 +359,24 @@ function asElement(tag, attrs, voidEl) {
338
359
  offersAs: true,
339
360
  };
340
361
  }
362
+ /**
363
+ * DUAS LISTAS DE ATRIBUTOS NUMA, e a DELE ganha a chave repetida.
364
+ *
365
+ * Sem isto o elemento sairia com `role="switch" role="progressbar"` - JSX que não compila e, se
366
+ * compilasse, resolveria pelo último. A chave é extraída do texto porque é assim que as duas metades
367
+ * já viajam; nada aqui inventa atributo.
368
+ */
369
+ function mergeAttrs(ours, theirs) {
370
+ const taken = new Set([...theirs.matchAll(/\s([A-Za-z][\w:-]*)=/g)].map((m) => m[1]));
371
+ const kept = [...ours.matchAll(/\s[A-Za-z][\w:-]*=(?:"[^"]*"|\{[^}]*\})/g)]
372
+ .map((m) => m[0])
373
+ .filter((piece) => {
374
+ const key = /\s([A-Za-z][\w:-]*)=/.exec(piece)?.[1] ?? "";
375
+ return !taken.has(key);
376
+ })
377
+ .join("");
378
+ return `${kept}${theirs}`;
379
+ }
341
380
  function dataAttrLines(axes) {
342
381
  return axes
343
382
  .map((a) => a.boolean
@@ -891,8 +930,20 @@ function treeParts(nodes, out = []) {
891
930
  function emitTree(nodes, comp, indent) {
892
931
  const lines = [];
893
932
  for (const node of nodes) {
933
+ /**
934
+ * O QUE ELE ESCREVEU NESTE NÓ - `role`, `aria-*`, `data-*`, e a prop que o alvo EXIGE.
935
+ *
936
+ * Só literal: uma expressão nomeia uma variável do arquivo DELE que não existe dentro do
937
+ * componente que a gente gera, e o build dele quebra. `sameTag` é falso porque a tag emitida sai
938
+ * do formulário do nó (`FORM_TAG`), que é escolha nossa - então atravessa o que o HTML define
939
+ * como global, que é justamente onde a acessibilidade mora.
940
+ */
941
+ const attrs = emittableAttrs(node, {
942
+ requires: new Set(node.passesRequired ?? []),
943
+ expressions: "hold",
944
+ }).write;
894
945
  if (node.as === "component") {
895
- lines.push(`${indent}{/* your <${pascal(node.ref ?? "")} /> goes here - it has a recipe of its own, so it is not inlined */}`);
946
+ lines.push(`${indent}{/* your <${pascal(node.ref ?? "")}${attrs} /> goes here - it has a recipe of its own, so it is not inlined */}`);
896
947
  continue;
897
948
  }
898
949
  if (node.as === "external") {
@@ -918,17 +969,17 @@ function emitTree(nodes, comp, indent) {
918
969
  }
919
970
  const partComp = `${comp}${pascal(node.part)}`;
920
971
  if (ARRANGES.has(node.as) && node.children && node.children.length > 0) {
921
- lines.push(`${indent}<${partComp}>`);
972
+ lines.push(`${indent}<${partComp}${attrs}>`);
922
973
  lines.push(emitTree(node.children, comp, `${indent} `));
923
974
  lines.push(`${indent}</${partComp}>`);
924
975
  continue;
925
976
  }
926
977
  const { voidEl } = FORM_TAG[node.as] ?? {};
927
978
  if (voidEl) {
928
- lines.push(`${indent}<${partComp} />`);
979
+ lines.push(`${indent}<${partComp}${attrs} />`);
929
980
  continue;
930
981
  }
931
- lines.push(`${indent}<${partComp}>${node.text ?? `{/* ${node.part} */}`}</${partComp}>`);
982
+ lines.push(`${indent}<${partComp}${attrs}>${node.text ?? `{/* ${node.part} */}`}</${partComp}>`);
932
983
  }
933
984
  return lines.filter(Boolean).join("\n");
934
985
  }
@@ -362,8 +362,9 @@ export function passedProps(body,
362
362
  /** O que `behaviourProps` já levou, para cada fato viver num lugar só. */
363
363
  taken) {
364
364
  const out = {};
365
+ const expression = [];
365
366
  const SKIP = /^(className|class|key|ref|style|children)$/;
366
- for (const m of body.matchAll(/(\w+)=(?:"([^"]{0,80})"|'([^']{0,80})'|\{([^{}]{1,80})\})/g)) {
367
+ for (const m of body.matchAll(PROP_ATTR)) {
367
368
  const prop = m[1];
368
369
  if (SKIP.test(prop) || taken?.[prop] || out[prop])
369
370
  continue;
@@ -377,9 +378,27 @@ taken) {
377
378
  if (!value || /=>|\bfunction\b/.test(value) || value.startsWith("{"))
378
379
  continue;
379
380
  out[prop] = value;
381
+ expression.push(prop);
380
382
  }
381
- return Object.keys(out).length > 0 ? out : null;
383
+ return Object.keys(out).length > 0 ? { props: out, expression } : null;
382
384
  }
385
+ /**
386
+ * UM NOME DE PROP INCLUI O HÍFEN, e ele era o pedaço que a acessibilidade dele inteira carregava.
387
+ *
388
+ * O padrão anterior era `(\w+)=`, e `\w` não casa `-`. Num `aria-hidden="true"` o motor tentava
389
+ * `aria`, batia no hífen, voltava, desistia, e recomeçava em `label`/`hidden` - então o censo
390
+ * gravava a chave SEM o prefixo. Medido no fonte dos dois clientes: 83 `aria-*` + 4 `data-*` no
391
+ * `packages/ui` do primeiro, 174 + 623 no dashboard do segundo.
392
+ *
393
+ * O custo não era só de perda. `aria-hidden` gravado como `hidden` e emitido de volta escreve o
394
+ * atributo HTML `hidden`, que APAGA o elemento da tela: a leitura errada viraria código errado no
395
+ * arquivo dele. Por isso o conserto mora na COLETA e não na emissão - depois daqui a informação já
396
+ * não existe, e nenhum `reinterpret` a recupera.
397
+ *
398
+ * `[\w:-]` cobre as três grafias que o JSX aceita num nome de atributo: `data-x`, `aria-x` e
399
+ * `xlink:href`. A âncora `[A-Za-z]` na primeira posição impede que um hífen solto vire nome.
400
+ */
401
+ const PROP_ATTR = /\b([A-Za-z][\w:-]*)=(?:"([^"]{0,80})"|'([^']{0,80})'|\{([^{}]{1,80})\})/g;
383
402
  /**
384
403
  * A expressão que um `map` percorre, quando este elemento está dentro de um - ver `repeats`.
385
404
  *
@@ -542,8 +561,21 @@ export function sketchOf(source, name) {
542
561
  * para a receita de uma TELA carregar a fiação, e não só a casca.
543
562
  */
544
563
  const given = passedProps(event.body, conditional);
545
- if (given)
546
- node.passes = given;
564
+ if (given) {
565
+ node.passes = given.props;
566
+ /**
567
+ * A FORMA VIAJA SEMPRE QUE O VALOR VIAJA, inclusive VAZIA - e a lista vazia é o ponto.
568
+ *
569
+ * Se `passesExpr` só aparecesse quando há expressão, "ausente" significaria duas coisas ao
570
+ * mesmo tempo: *nenhuma delas é expressão* e *este censo é velho e não sabe responder*. Quem
571
+ * emite não tem como distinguir, e as duas exigem comportamentos OPOSTOS - escrever tudo como
572
+ * literal, ou não escrever nada.
573
+ *
574
+ * Com o array sempre presente, ausência quer dizer uma coisa só: censo tirado antes de
575
+ * 0.16.275. Ver `emittableAttrs`, que segura o atributo nesse caso em vez de adivinhar.
576
+ */
577
+ node.passesExpr = given.expression;
578
+ }
547
579
  const list = repeatOf(source, event.at);
548
580
  if (list)
549
581
  node.repeats = list;
@@ -2014,11 +2014,60 @@ function bodyBrace(source, declAt) {
2014
2014
  * function to alias to either: the body IS the argument.
2015
2015
  */
2016
2016
  const head = source.slice(declAt, declAt + 400);
2017
- const wrapper = /=\s*(?:React\.)?(?:forwardRef|memo|styled\([^)]*\))\s*(?:<[^>(]*>)?\s*\(/.exec(head);
2017
+ /**
2018
+ * O GENÉRICO PODE SER ANINHADO, e `<[^>(]*>` proibia isso.
2019
+ *
2020
+ * `forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(` tem um `>` dentro do argumento de
2021
+ * tipo, então o padrão parava no primeiro e o wrapper inteiro deixava de casar - o componente caía
2022
+ * na varredura simples, que conta parêntese do `forwardRef` como se fosse do corpo. `ModalBody` e
2023
+ * `ModalFooter` do cliente ficavam sem raiz por isso.
2024
+ *
2025
+ * `[^(]*` deixa o `>` passar e para no parêntese, que é o delimitador de verdade aqui.
2026
+ */
2027
+ const wrapper = /=\s*(?:React\.)?(?:forwardRef|memo|styled\([^)]*\))\s*(?:<[^(]*>)?\s*\(/.exec(head);
2018
2028
  if (wrapper) {
2019
- const arrow = /(?:=>|function\s*[A-Za-z0-9_]*\s*\([^)]*\))\s*\{/.exec(source.slice(declAt + wrapper.index));
2029
+ const from = declAt + wrapper.index;
2030
+ const rest = source.slice(from);
2031
+ /**
2032
+ * A LISTA DE PARÂMETROS PODE TER UM PARÊNTESE DENTRO, e o padrão anterior não podia saber disso.
2033
+ *
2034
+ * Ele exigia `\([^)]*\)`, que para no PRIMEIRO `)`. O `NoiseOverlay` do cliente escreve
2035
+ * `color = "rgba(0,0,0,0.025)"` como default de prop, então a varredura terminava dentro da
2036
+ * lista, não achava a chave do corpo, e o componente inteiro ficava sem raiz: sem `rootTag`, sem
2037
+ * nó marcado `root`, sem `rootClasses` e sem `rootArrange`.
2038
+ *
2039
+ * Medido nas duas populações: 7 de 59 componentes de um cliente (12%) e 13 de 320 do outro
2040
+ * ficavam assim. É a forma de perda que o mapa cataloga - uma enumeração mais estreita que a
2041
+ * própria entrada -, e o conserto é contar parêntese em vez de proibi-lo.
2042
+ */
2043
+ const named = /function\s*[A-Za-z0-9_$]*\s*\(/.exec(rest);
2044
+ const arrow = /=>\s*\{/.exec(rest);
2045
+ /**
2046
+ * O QUE VIER PRIMEIRO, como a alternância anterior já decidia. Procurar `function` no arquivo
2047
+ * inteiro casaria a PRÓXIMA função declarada quando o wrapper carrega um arrow - `forwardRef((
2048
+ * props, ref) => { … })` -, e o corpo lido seria o de outro componente.
2049
+ */
2050
+ const namedFirst = named != null && (arrow == null || named.index < arrow.index);
2051
+ if (namedFirst && named) {
2052
+ const open = named.index + named[0].length - 1;
2053
+ let depth = 0;
2054
+ for (let j = open; j < rest.length; j++) {
2055
+ const ch = rest[j];
2056
+ if (ch === "(")
2057
+ depth += 1;
2058
+ else if (ch === ")") {
2059
+ depth -= 1;
2060
+ if (depth === 0) {
2061
+ const body = /^\s*\{/.exec(rest.slice(j + 1));
2062
+ if (body)
2063
+ return from + j + body[0].length;
2064
+ break;
2065
+ }
2066
+ }
2067
+ }
2068
+ }
2020
2069
  if (arrow) {
2021
- const at = declAt + wrapper.index + arrow.index + arrow[0].length - 1;
2070
+ const at = from + arrow.index + arrow[0].length - 1;
2022
2071
  if (source[at] === "{")
2023
2072
  return at;
2024
2073
  }
@@ -2088,7 +2137,26 @@ function ownReturn(source, declAt) {
2088
2137
  continue;
2089
2138
  }
2090
2139
  if (ch === '"' || ch === "'" || ch === "`") {
2091
- quote = ch;
2140
+ /**
2141
+ * UM APÓSTROFO DENTRO DE UMA PALAVRA NÃO ABRE STRING - `Don't`, `we'll`, `it's`.
2142
+ *
2143
+ * Em texto JSX o apóstrofo é uma letra como outra qualquer, e tratá-lo como delimitador abria
2144
+ * uma "string" que só fechava no próximo apóstrofo do arquivo. Tudo depois disso ficava fora
2145
+ * de sincronia: a contagem de chaves perdia o passo e o `return` da raiz nunca era visto no
2146
+ * nível certo.
2147
+ *
2148
+ * Medido no dashboard do segundo cliente: 6 componentes ficavam sem raiz nenhuma por causa
2149
+ * disto - `CurateTemplate`, `CustomCardList`, `TunePreferences` e mais três -, e com a regra
2150
+ * nova nenhuma das 376 respostas que já estavam certas nas duas populações muda.
2151
+ *
2152
+ * A regra é estreita de propósito: só o apóstrofo com letra dos dois lados. `'a'` continua
2153
+ * string, e `it's` continua texto.
2154
+ */
2155
+ const dentroDePalavra = ch === "'" &&
2156
+ /[A-Za-z]/.test(source[j - 1] ?? "") &&
2157
+ /[A-Za-z]/.test(source[j + 1] ?? "");
2158
+ if (!dentroDePalavra)
2159
+ quote = ch;
2092
2160
  continue;
2093
2161
  }
2094
2162
  if (ch === "{")
@@ -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.270";
131
+ export const MATERIALISER_SINCE = "0.16.276";
132
132
  /**
133
133
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
134
134
  *
@@ -0,0 +1,53 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ /**
4
+ * ONDE ESTÃO AS POPULAÇÕES MEDIDAS - e por que elas não moram mais na máquina de ninguém.
5
+ *
6
+ * O QUÊ. 23 specs medem contra o censo de dois repositórios reais. Até 22/08 cada um apontava para
7
+ * `~/projects/frontend-hub` ou `~/personal/codelevel-monorepo` na home do dono.
8
+ *
9
+ * POR QUÊ ISSO ERA UM DEFEITO. Não porque quebrava - 35 dos 36 pulavam educadamente quando o
10
+ * arquivo sumia. Porque ficava VERDE. Medido em 22/08: três artefatos do `signalui` já não estavam
11
+ * no disco e 12 testes pulavam em silêncio havia sabe-se lá quanto tempo. Uma suíte que fica verde
12
+ * quando parou de medir é pior que uma vermelha, e essa é a única classe de defeito que esta
13
+ * esteira não pode aceitar num instrumento.
14
+ *
15
+ * E O SEGUNDO CUSTO, que é o do produto: sem a segunda população, toda régua nova passa a ser
16
+ * medida num repositório só. Três réguas escritas em 22/08 estariam erradas por isso.
17
+ *
18
+ * COMO. `fixtures/populations/`, versionado, compacto. `npm run populations:refresh` atualiza a
19
+ * partir dos repositórios vivos quando eles existem - e o que mudou aparece no `git diff`, que é
20
+ * onde uma mudança de insumo pode ser revisada em vez de sentida.
21
+ *
22
+ * GÊMEO de `apps/web/src/lib/ds/populations.ts`: o CLI é publicado standalone e não importa de lá. As
23
+ * duas cópias resolvem o mesmo diretório por caminho relativo, então elas não podem divergir de
24
+ * conteúdo - só de posição.
25
+ */
26
+ /** A raiz do monorepo, a partir deste arquivo: `packages/cli/src` → 3 níveis acima. */
27
+ const ROOT = join(import.meta.dirname, "..", "..", "..");
28
+ const POPULATIONS = join(ROOT, "fixtures", "populations");
29
+ /**
30
+ * 323 componentes, escopo `apps/web-dashboard`. A população que derruba régua feita em cima da
31
+ * outra: ela não declara um `--animate-*` sequer, escreve o relógio literal na receita, e tem 62
32
+ * componentes cujo primeiro nó do sketch não é a raiz.
33
+ */
34
+ export const FRONTEND_HUB_CENSUS = join(POPULATIONS, "frontend-hub.census.json");
35
+ /**
36
+ * 59 componentes, escopo `packages/ui`. O caso de teste vivo do dia a dia - é dele que saem os
37
+ * números do `triage` e é nele que a jornada é conferida na tela.
38
+ */
39
+ export const CODELEVEL_CENSUS = join(POPULATIONS, "codelevel-ui.census.json");
40
+ /**
41
+ * OS REPOSITÓRIOS VIVOS - e estes NÃO estão congelados, de propósito.
42
+ *
43
+ * Um punhado de specs não mede o censo: eles varrem os `.tsx` de ORIGEM para exercitar o COLETOR -
44
+ * `sketchOf`, `rootTag`, o leitor de `cn()`. Congelar 69 arquivos de código de cliente dentro deste
45
+ * repositório é uma decisão diferente de congelar uma medição, e ela não foi tomada.
46
+ *
47
+ * Então estes caminhos continuam dependendo da máquina, e é aqui que isso está DITO. Todo spec que
48
+ * os usa tem que guardar com `existsSync` - sem o guard, a ausência derruba a suíte em vez de pular.
49
+ *
50
+ * Medido em 22/08: 5 specs no `apps/web` e no `packages/cli` estão nesta situação.
51
+ */
52
+ export const CODELEVEL_REPO = join(homedir(), "personal", "codelevel-monorepo");
53
+ export const FRONTEND_HUB_REPO = join(homedir(), "projects", "frontend-hub");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.274",
3
+ "version": "0.16.276",
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": {