synthesisui 0.16.285 → 0.16.289
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.
- package/dist/anatomy-read.js +20 -5
- package/dist/commands/import.js +129 -13
- package/dist/doctor/broken-refs.js +15 -1
- package/dist/doctor/component-gate.js +59 -2
- package/dist/doctor/crosswalk.js +33 -4
- package/dist/doctor/css-modules.js +77 -47
- package/dist/doctor/definitions-scan.js +59 -3
- package/dist/doctor/transcribe.js +82 -50
- package/dist/doctor/variant-read.js +31 -5
- package/dist/global-wear.js +44 -0
- package/dist/install-marks.js +1 -1
- package/dist/namespace-pairs.js +80 -0
- package/dist/not-expressed.js +76 -0
- package/dist/token-ref.js +93 -0
- package/package.json +1 -1
|
@@ -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
|
-
/**
|
|
62
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
/**
|
|
717
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
*
|
|
@@ -357,10 +358,29 @@ const FONT_WEIGHT = {
|
|
|
357
358
|
black: "900",
|
|
358
359
|
};
|
|
359
360
|
/** The three family slots the document actually has. Nothing else may be a ref. */
|
|
361
|
+
/**
|
|
362
|
+
* O NOME DELE JÁ É O SLOT, e por isso esta tabela tem seis linhas e não três.
|
|
363
|
+
*
|
|
364
|
+
* O QUE O CLIENTE VIA: `--font-display: "Bricolage Grotesque"` declarado no `@theme`, a utility
|
|
365
|
+
* `font-display` escrita no `ModalHeader` e no `Typography` inteiro, e a leitura devolvendo nada -
|
|
366
|
+
* então o título dele chega no Studio na fonte do corpo (`codelevel-ui`, 23/08).
|
|
367
|
+
*
|
|
368
|
+
* As três primeiras traduzem o vocabulário da TAILWIND para o do documento (`font-sans` é o corpo).
|
|
369
|
+
* As duas últimas são o vocabulário do documento dito por ELE: quem declara `--font-display` não
|
|
370
|
+
* está pedindo tradução nenhuma, está nomeando o slot. Recusar isso era exigir que ele chamasse a
|
|
371
|
+
* fonte de display de `serif` para ser ouvido.
|
|
372
|
+
*
|
|
373
|
+
* E SÓ OS NOMES LITERAIS DO SLOT - `heading` fica de fora, e a tentação de incluí-lo é o erro que
|
|
374
|
+
* esta tabela evita. Um projeto pode declarar `--font-heading` E `--font-display` com fontes
|
|
375
|
+
* diferentes; mapear sinônimo colapsaria as duas em uma e apagaria uma decisão que ele escreveu.
|
|
376
|
+
* `font-heading` continua não lida, com o motivo no relatório, que é a resposta honesta.
|
|
377
|
+
*/
|
|
360
378
|
const FAMILY_SLOT = {
|
|
361
379
|
sans: "body",
|
|
362
380
|
serif: "display",
|
|
363
381
|
mono: "mono",
|
|
382
|
+
display: "display",
|
|
383
|
+
body: "body",
|
|
364
384
|
};
|
|
365
385
|
/** Type utilities with no scale behind them - a fact, not a decision deferred. */
|
|
366
386
|
const TYPE_KEYWORD = {
|
|
@@ -1170,8 +1190,67 @@ function readUtilityCore(utility, declared) {
|
|
|
1170
1190
|
if (RADIUS[rest])
|
|
1171
1191
|
return { property: "borderRadius", value: RADIUS[rest] };
|
|
1172
1192
|
}
|
|
1193
|
+
/**
|
|
1194
|
+
* A UTILITY QUE O `@theme` DELE CRIOU - o último recurso, e é mecanismo, não caso.
|
|
1195
|
+
*
|
|
1196
|
+
* O QUE O CLIENTE VÊ SEM ISTO: o título do `ModalHeader` dele usa `font-display` e
|
|
1197
|
+
* `tracking-tightest`, os dois declarados no `@theme` como `--font-display: "Bricolage
|
|
1198
|
+
* Grotesque"` e `--tracking-tightest: -0.06em`. A leitura devolvia "não resolve nada" para
|
|
1199
|
+
* ambos, então o título chega no Studio na fonte do corpo e sem o aperto de letra que ele
|
|
1200
|
+
* escolheu - e o relatório afirma que a decisão não existe, com ela escrita no código
|
|
1201
|
+
* (`codelevel-ui`, 23/08).
|
|
1202
|
+
*
|
|
1203
|
+
* NO TAILWIND V4 TODO TOKEN DE UM NAMESPACE DO `@theme` CRIA A UTILITY DE MESMO NOME. As tabelas
|
|
1204
|
+
* acima conhecem as escalas que a Tailwind PUBLICA - `tracking-tight`, `text-2xl` -, então toda
|
|
1205
|
+
* escala própria de todo cliente caía neste buraco. A régua aqui não cita um nome de token: ela
|
|
1206
|
+
* pergunta se ELE declarou aquele nome.
|
|
1207
|
+
*
|
|
1208
|
+
* DEPOIS DE TUDO, e não antes: `tracking-tight` continua vindo da tabela fixa, e nenhuma leitura
|
|
1209
|
+
* que já funcionava muda de resposta. Medido nas duas populações: 2 classes resgatadas de 163
|
|
1210
|
+
* não lidas no `codelevel-ui`, 0 de 837 no `frontend-hub`, que não declara namespace próprio.
|
|
1211
|
+
*
|
|
1212
|
+
* SÓ OS NAMESPACES DE ATRIBUIÇÃO DIRETA. `--blur-*` e `--perspective-*` viram
|
|
1213
|
+
* `filter: blur(…)`/`transform`, e escrever a propriedade crua com o valor dentro produziria CSS
|
|
1214
|
+
* inválido - pior que não ler. Ver `tokenRefFor` para como o valor viaja: com ref de documento
|
|
1215
|
+
* onde existe grammar (`{typography.families.display}`) e como `var()` onde não existe, que é
|
|
1216
|
+
* CSS correto contra a folha dele.
|
|
1217
|
+
*/
|
|
1218
|
+
const themeProperty = THEME_NAMESPACE[prefix];
|
|
1219
|
+
if (themeProperty && rest) {
|
|
1220
|
+
const own = `--${prefix}-${rest}`;
|
|
1221
|
+
if (declared.has(own)) {
|
|
1222
|
+
return {
|
|
1223
|
+
property: themeProperty,
|
|
1224
|
+
value: tokenRefFor(own, declared),
|
|
1225
|
+
token: own,
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1173
1229
|
return null;
|
|
1174
1230
|
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Namespace do `@theme` → a propriedade CSS que a utility de mesmo nome escreve.
|
|
1233
|
+
*
|
|
1234
|
+
* Só atribuição direta: cada uma destas escreve `<propriedade>: var(--<namespace>-<nome>)` e nada
|
|
1235
|
+
* mais. `--blur-*` e `--perspective-*` ficam fora porque viram `filter: blur(…)`/`transform`, e a
|
|
1236
|
+
* propriedade crua com o valor dentro seria CSS inválido - pior que não ler.
|
|
1237
|
+
*
|
|
1238
|
+
* QUATRO LINHAS, E NÃO SETE. `text`, `shadow` e `animate` estavam aqui e foram REMOVIDOS depois de
|
|
1239
|
+
* medidos: os ramos acima já os resolvem, então a linha nunca era alcançada. Uma entrada que nada
|
|
1240
|
+
* alcança é a mesma dívida que os specs de alcance existem para impedir - conferido com
|
|
1241
|
+
* `--text-hero`, `--shadow-lifted` e `--animate-drift`, que continuam lidos sem elas.
|
|
1242
|
+
*/
|
|
1243
|
+
const THEME_NAMESPACE = {
|
|
1244
|
+
/**
|
|
1245
|
+
* `font` NÃO ENTRA AQUI de propósito: o ramo de `font` acima decide sozinho e retorna, então uma
|
|
1246
|
+
* linha para ele seria código que nada alcança. A família dele é resolvida por `FAMILY_SLOT`,
|
|
1247
|
+
* que é onde o slot do documento mora.
|
|
1248
|
+
*/
|
|
1249
|
+
tracking: "letterSpacing",
|
|
1250
|
+
leading: "lineHeight",
|
|
1251
|
+
ease: "transitionTimingFunction",
|
|
1252
|
+
aspect: "aspectRatio",
|
|
1253
|
+
};
|
|
1175
1254
|
/**
|
|
1176
1255
|
* A colour token ref in the document's own spelling.
|
|
1177
1256
|
*
|
|
@@ -1236,7 +1315,7 @@ function resolveArbitrary(inner, declared) {
|
|
|
1236
1315
|
if (raw.startsWith("--")) {
|
|
1237
1316
|
const name = raw.split(",")[0].trim();
|
|
1238
1317
|
return declared.has(name)
|
|
1239
|
-
? { value: tokenRefFor(name), token: name }
|
|
1318
|
+
? { value: tokenRefFor(name, declared), token: name }
|
|
1240
1319
|
: null;
|
|
1241
1320
|
}
|
|
1242
1321
|
// A literal in brackets is still a real value somebody typed.
|
|
@@ -1244,7 +1323,7 @@ function resolveArbitrary(inner, declared) {
|
|
|
1244
1323
|
}
|
|
1245
1324
|
const name = v[1];
|
|
1246
1325
|
if (declared.has(name))
|
|
1247
|
-
return { value: tokenRefFor(name), token: name };
|
|
1326
|
+
return { value: tokenRefFor(name, declared), token: name };
|
|
1248
1327
|
/**
|
|
1249
1328
|
* A `var()` THEY NEVER DECLARED, WHEN THEY DECLARED IT UNDER ITS NAMESPACE.
|
|
1250
1329
|
*
|
|
@@ -1264,7 +1343,7 @@ function resolveArbitrary(inner, declared) {
|
|
|
1264
1343
|
*/
|
|
1265
1344
|
const meant = MEANT_NAMESPACES.map((ns) => `--${ns}-${name.slice(2)}`).find((candidate) => declared.has(candidate));
|
|
1266
1345
|
if (meant)
|
|
1267
|
-
return { value: tokenRefFor(meant), token: meant };
|
|
1346
|
+
return { value: tokenRefFor(meant, declared), token: meant };
|
|
1268
1347
|
return { value: raw };
|
|
1269
1348
|
}
|
|
1270
1349
|
/**
|
|
@@ -1288,53 +1367,6 @@ const MEANT_NAMESPACES = [
|
|
|
1288
1367
|
* `--text-body-s` → `{typography.scale.body-s.fontSize}`. Their name is the path,
|
|
1289
1368
|
* which is the whole "your names travel unchanged" promise applied one level deeper.
|
|
1290
1369
|
*/
|
|
1291
|
-
function tokenRefFor(name) {
|
|
1292
|
-
const bare = name.replace(/^--/, "");
|
|
1293
|
-
if (bare.startsWith("color-"))
|
|
1294
|
-
return refFor(bare.slice("color-".length));
|
|
1295
|
-
if (bare.startsWith("radius-"))
|
|
1296
|
-
return `{radius.${bare.slice(7)}}`;
|
|
1297
|
-
if (bare.startsWith("spacing-"))
|
|
1298
|
-
return `{spacing.${bare.slice(8)}}`;
|
|
1299
|
-
if (bare.startsWith("shadow-"))
|
|
1300
|
-
return `{shadow.${bare.slice(7)}}`;
|
|
1301
|
-
// `--gradient-ui` and `--background-image-gradient-ui` are one token in two
|
|
1302
|
-
// spellings - Tailwind v4's utility namespace wraps the first. Both reach
|
|
1303
|
-
// `{gradients.ui}`, which is where the census files them.
|
|
1304
|
-
if (bare.startsWith("background-image-gradient-")) {
|
|
1305
|
-
return `{gradients.${bare.slice("background-image-gradient-".length)}}`;
|
|
1306
|
-
}
|
|
1307
|
-
if (bare.startsWith("gradient-"))
|
|
1308
|
-
return `{gradients.${bare.slice(9)}}`;
|
|
1309
|
-
if (bare.startsWith("text-")) {
|
|
1310
|
-
/**
|
|
1311
|
-
* THE COMPANION TOKEN. Tailwind v4 spells "the line-height OF text-body-s" as
|
|
1312
|
-
* `--text-body-s--line-height` - a double dash inside one name. Read as a step name
|
|
1313
|
-
* it produced `{typography.scale.body-s--line-height.fontSize}`, whose double dash
|
|
1314
|
-
* no ref grammar accepts, and the whole recipe was REFUSED at validation (test13,
|
|
1315
|
-
* 01/08). The suffix names the property; the middle names the step.
|
|
1316
|
-
*/
|
|
1317
|
-
const companion = /^text-(.+?)--(line-height|letter-spacing|font-weight)$/.exec(bare);
|
|
1318
|
-
if (companion) {
|
|
1319
|
-
const prop = {
|
|
1320
|
-
"line-height": "lineHeight",
|
|
1321
|
-
"letter-spacing": "letterSpacing",
|
|
1322
|
-
"font-weight": "weight",
|
|
1323
|
-
}[companion[2]];
|
|
1324
|
-
return `{typography.scale.${companion[1]}.${prop}}`;
|
|
1325
|
-
}
|
|
1326
|
-
// Any other double dash is a name this grammar cannot hold - the literal var()
|
|
1327
|
-
// still resolves against their own stylesheet, and an invalid ref helps nobody.
|
|
1328
|
-
if (bare.slice(5).includes("--"))
|
|
1329
|
-
return `var(${name})`;
|
|
1330
|
-
return `{typography.scale.${bare.slice(5)}.fontSize}`;
|
|
1331
|
-
}
|
|
1332
|
-
if (bare.startsWith("font-"))
|
|
1333
|
-
return `{typography.families.${bare.slice(5)}}`;
|
|
1334
|
-
// A namespace we do not model. The literal `var()` is still correct CSS against
|
|
1335
|
-
// their own stylesheet, and inventing a ref would point at nothing.
|
|
1336
|
-
return `var(${name})`;
|
|
1337
|
-
}
|
|
1338
1370
|
function refFor(name) {
|
|
1339
1371
|
const m = /^(.*)-(\d{2,4})$/.exec(name);
|
|
1340
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
|
-
|
|
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
|
+
}
|
package/dist/install-marks.js
CHANGED
|
@@ -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.
|
|
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
|
+
}
|