orbital-ds 0.4.0 → 0.6.0
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/README.md +73 -64
- package/components.manifest.json +12 -1
- package/dist/styles.css +18 -0
- package/dist/themes/friendly.css +310 -0
- package/dist/themes/friendly.fonts.css +2 -0
- package/dist/themes/friendly.guide.md +38 -0
- package/dist/themes/friendly.meta.json +14 -0
- package/dist/themes/friendly.scoped.css +310 -0
- package/dist/themes/qratia.guide.md +34 -0
- package/dist/themes/qratia.meta.json +12 -0
- package/dist/themes/tactile.css +399 -0
- package/dist/themes/tactile.fonts.css +2 -0
- package/dist/themes/tactile.guide.md +49 -0
- package/dist/themes/tactile.meta.json +16 -0
- package/dist/themes/tactile.scoped.css +399 -0
- package/dist/themes/twitter.guide.md +35 -0
- package/dist/themes/twitter.meta.json +14 -0
- package/dist/themes/zyro.guide.md +33 -0
- package/dist/themes/zyro.meta.json +14 -0
- package/dist/ui/icon-tile.d.ts +11 -0
- package/dist/ui/icon-tile.js +47 -0
- package/package.json +3 -1
- package/scripts/doctor.mjs +237 -224
- package/scripts/init.mjs +89 -55
package/scripts/doctor.mjs
CHANGED
|
@@ -1,224 +1,237 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* orbital-ds doctor — comprueba que el sistema se cumple.
|
|
4
|
-
*
|
|
5
|
-
* 1. CONTRATO: cada tema (themes/*.css) define en :root todos los tokens de
|
|
6
|
-
* contract.json y la fuente `--ds-font-sans`. En .dark solo avisa.
|
|
7
|
-
* 2. REPETIDOS: ninguna variable se define dos veces en el mismo bloque.
|
|
8
|
-
* 3. SOMBRAS: «sin sombra» es `0 0 #0000`, nunca `none` (una variable con
|
|
9
|
-
* `none` invalida todo el box-shadow, anillo de foco incluido).
|
|
10
|
-
* 4. FUGAS: el tema base (contract.baseTheme) está aplicado siempre; todo tema
|
|
11
|
-
* con ámbito debe definir cada `--ds-*` que defina el base, o su valor se
|
|
12
|
-
* colaría al cambiar de tema.
|
|
13
|
-
* 5. VALORES FIJOS: ningún componente (ui/*.tsx) escribe colores, z-index,
|
|
14
|
-
* duraciones ni sombras a mano; todo sale de tokens.
|
|
15
|
-
* 6. API PÚBLICA: no desaparece ningún componente ni export respecto a
|
|
16
|
-
* components.manifest.json (huella de la última versión). Lo nuevo solo avisa.
|
|
17
|
-
*
|
|
18
|
-
* Uso:
|
|
19
|
-
* node scripts/doctor.mjs # temas y componentes del paquete
|
|
20
|
-
* node scripts/doctor.mjs --css app.css # además, un CSS de una app (repetidos)
|
|
21
|
-
*
|
|
22
|
-
* Sale con código 1 si hay errores (los avisos no fallan).
|
|
23
|
-
*/
|
|
24
|
-
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
25
|
-
import { join, dirname, resolve } from 'node:path';
|
|
26
|
-
import { fileURLToPath } from 'node:url';
|
|
27
|
-
|
|
28
|
-
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
-
const contract = JSON.parse(readFileSync(join(ROOT, 'contract.json'), 'utf8'));
|
|
30
|
-
|
|
31
|
-
const errors = [];
|
|
32
|
-
const warnings = [];
|
|
33
|
-
const read = (p) => readFileSync(p, 'utf8');
|
|
34
|
-
const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
35
|
-
|
|
36
|
-
/** Cuerpo de cada bloque `selector { ... }` (respeta llaves anidadas). */
|
|
37
|
-
function blocks(css, selectorRe) {
|
|
38
|
-
const out = [];
|
|
39
|
-
const re = new RegExp(selectorRe.source + '\\s*\\{', 'g');
|
|
40
|
-
let m;
|
|
41
|
-
while ((m = re.exec(css))) {
|
|
42
|
-
let depth = 1;
|
|
43
|
-
let i = re.lastIndex;
|
|
44
|
-
const start = i;
|
|
45
|
-
while (i < css.length && depth > 0) {
|
|
46
|
-
if (css[i] === '{') depth++;
|
|
47
|
-
else if (css[i] === '}') depth--;
|
|
48
|
-
i++;
|
|
49
|
-
}
|
|
50
|
-
out.push(css.slice(start, i - 1));
|
|
51
|
-
re.lastIndex = i;
|
|
52
|
-
}
|
|
53
|
-
return out;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Declaraciones `--x: valor;` del nivel superior de un bloque (no las anidadas). */
|
|
57
|
-
function declarations(body) {
|
|
58
|
-
let depth = 0;
|
|
59
|
-
let top = '';
|
|
60
|
-
for (const ch of body) {
|
|
61
|
-
if (ch === '{') depth++;
|
|
62
|
-
else if (ch === '}') depth--;
|
|
63
|
-
else if (depth === 0) top += ch;
|
|
64
|
-
}
|
|
65
|
-
return [...top.matchAll(/(^|[;\s])(--[a-z0-9-]+)\s*:\s*([^;]*);?/gi)].map((m) => ({ name: m[2], value: m[3].trim() }));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Variables --ds-* que la estructura del paquete define (las que una app puede usar). */
|
|
69
|
-
function knownDsVars() {
|
|
70
|
-
const f = existsSync(join(ROOT, 'src/styles.css')) ? join(ROOT, 'src/styles.css') : join(ROOT, 'dist/styles.css');
|
|
71
|
-
return new Set([...read(f).matchAll(/(--ds-[a-z0-9-]+)\s*:/g)].map((m) => m[1]));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// El grupo de marca debe ser coherente con el contrato: cada variable, o es del contrato, o es --ds-*.
|
|
75
|
-
for (const t of contract.brand?.tokens ?? []) {
|
|
76
|
-
const inContract =
|
|
77
|
-
contract.root.includes(t.name) ||
|
|
78
|
-
contract.fonts.includes(t.name) ||
|
|
79
|
-
(t.name.startsWith('ds-') && knownDsVars().has(`--${t.name}`));
|
|
80
|
-
if (!inContract) errors.push(`contrato: la variable de marca --${t.name} ni es del contrato ni la define la estructura`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const themeInfo = {}; // nombre → { rootVars:Set, darkVars:Set }
|
|
84
|
-
|
|
85
|
-
function checkTheme(file, name) {
|
|
86
|
-
const label = `tema ${name}`;
|
|
87
|
-
const css = stripComments(read(file));
|
|
88
|
-
const roots = blocks(css, /(?:^|[\s}])\:root/);
|
|
89
|
-
const darks = blocks(css, /(?:^|[\s}])\.dark/);
|
|
90
|
-
|
|
91
|
-
const rootDecl = roots.flatMap(declarations);
|
|
92
|
-
const darkDecl = darks.flatMap(declarations);
|
|
93
|
-
const rootVars = new Set(rootDecl.map((d) => d.name));
|
|
94
|
-
const darkVars = new Set(darkDecl.map((d) => d.name));
|
|
95
|
-
themeInfo[name] = { rootVars, darkVars };
|
|
96
|
-
|
|
97
|
-
for (const t of contract.root) {
|
|
98
|
-
if (!rootVars.has(`--${t}`)) errors.push(`${label}: falta el token obligatorio --${t} en :root`);
|
|
99
|
-
}
|
|
100
|
-
for (const f of contract.fonts) {
|
|
101
|
-
if (!rootVars.has(`--${f}`)) errors.push(`${label}: falta la fuente --${f} en :root`);
|
|
102
|
-
}
|
|
103
|
-
if (darks.length) {
|
|
104
|
-
for (const t of contract.dark.avisar_si_falta) {
|
|
105
|
-
if (!darkVars.has(`--${t}`)) warnings.push(`${label}: .dark no redefine --${t} (hereda el valor claro)`);
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
warnings.push(`${label}: no define piel .dark`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Repetidos (solo declaraciones del nivel superior de cada bloque)
|
|
112
|
-
for (const [kind, list] of [[':root', roots], ['.dark', darks]]) {
|
|
113
|
-
list.forEach((body, idx) => {
|
|
114
|
-
const seen = new Map();
|
|
115
|
-
for (const d of declarations(body)) seen.set(d.name, (seen.get(d.name) ?? 0) + 1);
|
|
116
|
-
for (const [v, n] of seen) {
|
|
117
|
-
if (n > 1) errors.push(`${label}: ${v} definido ${n} veces en el mismo bloque ${kind} (#${idx + 1})`);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Sombras: `none` invalida el box-shadow
|
|
123
|
-
for (const d of [...rootDecl, ...darkDecl]) {
|
|
124
|
-
if (/^--ds-(?:shadow|sidebar-shadow)/.test(d.name) && /^none$/i.test(d.value)) {
|
|
125
|
-
errors.push(`${label}: ${d.name}: none — usa «0 0 #0000» (none invalida todo el box-shadow)`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 1+2+3) temas
|
|
131
|
-
const themesDir = join(ROOT, existsSync(join(ROOT, 'src/themes')) ? 'src/themes' : 'dist/themes');
|
|
132
|
-
const themeFiles = readdirSync(themesDir).filter(
|
|
133
|
-
(n) => n.endsWith('.css') && !n.endsWith('.fonts.css') && !n.endsWith('.scoped.css'),
|
|
134
|
-
);
|
|
135
|
-
for (const f of themeFiles) checkTheme(join(themesDir, f), f.replace(/\.css$/, ''));
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* orbital-ds doctor — comprueba que el sistema se cumple.
|
|
4
|
+
*
|
|
5
|
+
* 1. CONTRATO: cada tema (themes/*.css) define en :root todos los tokens de
|
|
6
|
+
* contract.json y la fuente `--ds-font-sans`. En .dark solo avisa.
|
|
7
|
+
* 2. REPETIDOS: ninguna variable se define dos veces en el mismo bloque.
|
|
8
|
+
* 3. SOMBRAS: «sin sombra» es `0 0 #0000`, nunca `none` (una variable con
|
|
9
|
+
* `none` invalida todo el box-shadow, anillo de foco incluido).
|
|
10
|
+
* 4. FUGAS: el tema base (contract.baseTheme) está aplicado siempre; todo tema
|
|
11
|
+
* con ámbito debe definir cada `--ds-*` que defina el base, o su valor se
|
|
12
|
+
* colaría al cambiar de tema.
|
|
13
|
+
* 5. VALORES FIJOS: ningún componente (ui/*.tsx) escribe colores, z-index,
|
|
14
|
+
* duraciones ni sombras a mano; todo sale de tokens.
|
|
15
|
+
* 6. API PÚBLICA: no desaparece ningún componente ni export respecto a
|
|
16
|
+
* components.manifest.json (huella de la última versión). Lo nuevo solo avisa.
|
|
17
|
+
*
|
|
18
|
+
* Uso:
|
|
19
|
+
* node scripts/doctor.mjs # temas y componentes del paquete
|
|
20
|
+
* node scripts/doctor.mjs --css app.css # además, un CSS de una app (repetidos)
|
|
21
|
+
*
|
|
22
|
+
* Sale con código 1 si hay errores (los avisos no fallan).
|
|
23
|
+
*/
|
|
24
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
25
|
+
import { join, dirname, resolve } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const contract = JSON.parse(readFileSync(join(ROOT, 'contract.json'), 'utf8'));
|
|
30
|
+
|
|
31
|
+
const errors = [];
|
|
32
|
+
const warnings = [];
|
|
33
|
+
const read = (p) => readFileSync(p, 'utf8');
|
|
34
|
+
const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
35
|
+
|
|
36
|
+
/** Cuerpo de cada bloque `selector { ... }` (respeta llaves anidadas). */
|
|
37
|
+
function blocks(css, selectorRe) {
|
|
38
|
+
const out = [];
|
|
39
|
+
const re = new RegExp(selectorRe.source + '\\s*\\{', 'g');
|
|
40
|
+
let m;
|
|
41
|
+
while ((m = re.exec(css))) {
|
|
42
|
+
let depth = 1;
|
|
43
|
+
let i = re.lastIndex;
|
|
44
|
+
const start = i;
|
|
45
|
+
while (i < css.length && depth > 0) {
|
|
46
|
+
if (css[i] === '{') depth++;
|
|
47
|
+
else if (css[i] === '}') depth--;
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
out.push(css.slice(start, i - 1));
|
|
51
|
+
re.lastIndex = i;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Declaraciones `--x: valor;` del nivel superior de un bloque (no las anidadas). */
|
|
57
|
+
function declarations(body) {
|
|
58
|
+
let depth = 0;
|
|
59
|
+
let top = '';
|
|
60
|
+
for (const ch of body) {
|
|
61
|
+
if (ch === '{') depth++;
|
|
62
|
+
else if (ch === '}') depth--;
|
|
63
|
+
else if (depth === 0) top += ch;
|
|
64
|
+
}
|
|
65
|
+
return [...top.matchAll(/(^|[;\s])(--[a-z0-9-]+)\s*:\s*([^;]*);?/gi)].map((m) => ({ name: m[2], value: m[3].trim() }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Variables --ds-* que la estructura del paquete define (las que una app puede usar). */
|
|
69
|
+
function knownDsVars() {
|
|
70
|
+
const f = existsSync(join(ROOT, 'src/styles.css')) ? join(ROOT, 'src/styles.css') : join(ROOT, 'dist/styles.css');
|
|
71
|
+
return new Set([...read(f).matchAll(/(--ds-[a-z0-9-]+)\s*:/g)].map((m) => m[1]));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// El grupo de marca debe ser coherente con el contrato: cada variable, o es del contrato, o es --ds-*.
|
|
75
|
+
for (const t of contract.brand?.tokens ?? []) {
|
|
76
|
+
const inContract =
|
|
77
|
+
contract.root.includes(t.name) ||
|
|
78
|
+
contract.fonts.includes(t.name) ||
|
|
79
|
+
(t.name.startsWith('ds-') && knownDsVars().has(`--${t.name}`));
|
|
80
|
+
if (!inContract) errors.push(`contrato: la variable de marca --${t.name} ni es del contrato ni la define la estructura`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const themeInfo = {}; // nombre → { rootVars:Set, darkVars:Set }
|
|
84
|
+
|
|
85
|
+
function checkTheme(file, name) {
|
|
86
|
+
const label = `tema ${name}`;
|
|
87
|
+
const css = stripComments(read(file));
|
|
88
|
+
const roots = blocks(css, /(?:^|[\s}])\:root/);
|
|
89
|
+
const darks = blocks(css, /(?:^|[\s}])\.dark/);
|
|
90
|
+
|
|
91
|
+
const rootDecl = roots.flatMap(declarations);
|
|
92
|
+
const darkDecl = darks.flatMap(declarations);
|
|
93
|
+
const rootVars = new Set(rootDecl.map((d) => d.name));
|
|
94
|
+
const darkVars = new Set(darkDecl.map((d) => d.name));
|
|
95
|
+
themeInfo[name] = { rootVars, darkVars };
|
|
96
|
+
|
|
97
|
+
for (const t of contract.root) {
|
|
98
|
+
if (!rootVars.has(`--${t}`)) errors.push(`${label}: falta el token obligatorio --${t} en :root`);
|
|
99
|
+
}
|
|
100
|
+
for (const f of contract.fonts) {
|
|
101
|
+
if (!rootVars.has(`--${f}`)) errors.push(`${label}: falta la fuente --${f} en :root`);
|
|
102
|
+
}
|
|
103
|
+
if (darks.length) {
|
|
104
|
+
for (const t of contract.dark.avisar_si_falta) {
|
|
105
|
+
if (!darkVars.has(`--${t}`)) warnings.push(`${label}: .dark no redefine --${t} (hereda el valor claro)`);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
warnings.push(`${label}: no define piel .dark`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Repetidos (solo declaraciones del nivel superior de cada bloque)
|
|
112
|
+
for (const [kind, list] of [[':root', roots], ['.dark', darks]]) {
|
|
113
|
+
list.forEach((body, idx) => {
|
|
114
|
+
const seen = new Map();
|
|
115
|
+
for (const d of declarations(body)) seen.set(d.name, (seen.get(d.name) ?? 0) + 1);
|
|
116
|
+
for (const [v, n] of seen) {
|
|
117
|
+
if (n > 1) errors.push(`${label}: ${v} definido ${n} veces en el mismo bloque ${kind} (#${idx + 1})`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Sombras: `none` invalida el box-shadow
|
|
123
|
+
for (const d of [...rootDecl, ...darkDecl]) {
|
|
124
|
+
if (/^--ds-(?:shadow|sidebar-shadow)/.test(d.name) && /^none$/i.test(d.value)) {
|
|
125
|
+
errors.push(`${label}: ${d.name}: none — usa «0 0 #0000» (none invalida todo el box-shadow)`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 1+2+3) temas
|
|
131
|
+
const themesDir = join(ROOT, existsSync(join(ROOT, 'src/themes')) ? 'src/themes' : 'dist/themes');
|
|
132
|
+
const themeFiles = readdirSync(themesDir).filter(
|
|
133
|
+
(n) => n.endsWith('.css') && !n.endsWith('.fonts.css') && !n.endsWith('.scoped.css'),
|
|
134
|
+
);
|
|
135
|
+
for (const f of themeFiles) checkTheme(join(themesDir, f), f.replace(/\.css$/, ''));
|
|
136
|
+
|
|
137
|
+
// 1b) documentación de cada tema: guía de diseño (.guide.md) + ficha legible por máquina (.meta.json)
|
|
138
|
+
const META_CLAVES = ['name', 'label', 'essence', 'signature', 'fonts', 'palette', 'references', 'status', 'guide'];
|
|
139
|
+
for (const f of themeFiles) {
|
|
140
|
+
const t = f.replace(/\.css$/, '');
|
|
141
|
+
if (!existsSync(join(themesDir, `${t}.guide.md`))) errors.push(`tema ${t}: falta ${t}.guide.md (guía de diseño)`);
|
|
142
|
+
const metaPath = join(themesDir, `${t}.meta.json`);
|
|
143
|
+
if (!existsSync(metaPath)) { errors.push(`tema ${t}: falta ${t}.meta.json (ficha del tema)`); continue; }
|
|
144
|
+
let meta;
|
|
145
|
+
try { meta = JSON.parse(read(metaPath)); } catch { errors.push(`tema ${t}: ${t}.meta.json no es JSON válido`); continue; }
|
|
146
|
+
for (const k of META_CLAVES) if (!(k in meta)) errors.push(`tema ${t}: ${t}.meta.json no define «${k}»`);
|
|
147
|
+
if (meta.name !== t) errors.push(`tema ${t}: ${t}.meta.json tiene name «${meta.name}»`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 4) fugas desde el tema base
|
|
151
|
+
const base = contract.baseTheme;
|
|
152
|
+
if (base && themeInfo[base]) {
|
|
153
|
+
const baseDs = [...themeInfo[base].rootVars].filter((v) => v.startsWith('--ds-'));
|
|
154
|
+
for (const [name, info] of Object.entries(themeInfo)) {
|
|
155
|
+
if (name === base) continue;
|
|
156
|
+
for (const v of baseDs) {
|
|
157
|
+
if (!info.rootVars.has(v)) {
|
|
158
|
+
errors.push(`tema ${name}: no define ${v}, que sí define el tema base «${base}» — su valor se colaría al cambiar de tema`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// CSS de una app (opcional)
|
|
165
|
+
const cssArgIdx = process.argv.indexOf('--css');
|
|
166
|
+
if (cssArgIdx > -1 && process.argv[cssArgIdx + 1]) {
|
|
167
|
+
const css = stripComments(read(resolve(process.argv[cssArgIdx + 1])));
|
|
168
|
+
blocks(css, /(?:^|[\s}])\:root/).forEach((body, idx) => {
|
|
169
|
+
const seen = new Map();
|
|
170
|
+
for (const d of declarations(body)) seen.set(d.name, (seen.get(d.name) ?? 0) + 1);
|
|
171
|
+
for (const [v, n] of seen) if (n > 1) errors.push(`app css: ${v} definido ${n} veces en :root (#${idx + 1})`);
|
|
172
|
+
// Una variable --ds-* que el paquete ya no tiene: la marca la ignoraría en silencio.
|
|
173
|
+
const known = knownDsVars();
|
|
174
|
+
for (const d of declarations(body)) {
|
|
175
|
+
if (d.name.startsWith('--ds-') && !known.has(d.name)) {
|
|
176
|
+
errors.push(`app css: ${d.name} no existe en orbital-ds (¿renombrada o retirada? mira docs/CONTRACT.md)`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 5) valores fijos en componentes
|
|
183
|
+
const NO_HARDCODE = [
|
|
184
|
+
{ re: /#[0-9a-fA-F]{3,8}\b/g, why: 'color hexadecimal', skip: ['chart.tsx'] },
|
|
185
|
+
{ re: /\b(?:rgb|rgba|hsl|hsla|oklch)\((?!from_)/g, why: 'color literal' },
|
|
186
|
+
{ re: /(?<![\w-])z-(?!0(?![\w-]))\d+(?![\w-])/g, why: 'z-index numérico (usa --ds-z-*)' },
|
|
187
|
+
{ re: /(?<![\w-])duration-(?!0(?![\w-]))\d+(?![\w-])/g, why: 'duración fija (usa --ds-motion-*)' },
|
|
188
|
+
{ re: /(?<![\w-])(?:bg|text|border)-(?:black|white)(?:\/\d+)?(?![\w-])/g, why: 'color fijo (usa un token de color)' },
|
|
189
|
+
{ re: /(?<![\w-])shadow-(?:xs|sm|md|lg|xl|2xl)(?![\w-])/g, why: 'sombra de la escala Tailwind (usa shadow-card/control/raised/overlay/popover)' },
|
|
190
|
+
{ re: /(?<![\w-])text-\[\d+px\]/g, why: 'tamaño de texto arbitrario (usa la escala estándar)' },
|
|
191
|
+
{ re: /(?<![\w-])rounded-(?:3xl|4xl)(?![\w-])/g, why: 'radio fuera de la rampa (usa rounded-control/overlay o la rampa sm…2xl)' },
|
|
192
|
+
{ re: /(?<![\w-])(?:bg|text|border|ring|fill|stroke)-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)-\d{2,3}(?![\w-])/g, why: 'color de la paleta Tailwind (usa tokens del tema)' },
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const uiDir = join(ROOT, 'src/ui');
|
|
196
|
+
if (existsSync(uiDir)) {
|
|
197
|
+
for (const f of readdirSync(uiDir).filter((n) => n.endsWith('.tsx'))) {
|
|
198
|
+
const lines = read(join(uiDir, f)).split(/\r?\n/);
|
|
199
|
+
lines.forEach((line, i) => {
|
|
200
|
+
const code = line.replace(/\/\/.*$/, '');
|
|
201
|
+
for (const rule of NO_HARDCODE) {
|
|
202
|
+
if (rule.skip?.includes(f)) continue;
|
|
203
|
+
const hits = code.match(rule.re);
|
|
204
|
+
if (hits) errors.push(`ui/${f}:${i + 1}: ${rule.why} → ${[...new Set(hits)].join(', ')}`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 6) API pública frente a la huella commiteada
|
|
211
|
+
const manifestPath = join(ROOT, 'components.manifest.json');
|
|
212
|
+
if (existsSync(manifestPath) && existsSync(uiDir)) {
|
|
213
|
+
const { buildManifest } = await import('./manifest.mjs');
|
|
214
|
+
const before = JSON.parse(read(manifestPath)).components;
|
|
215
|
+
const now = buildManifest().components;
|
|
216
|
+
for (const [name, comp] of Object.entries(before)) {
|
|
217
|
+
if (!now[name]) {
|
|
218
|
+
errors.push(`API pública: desaparece el componente «${name}» (¿versión mayor? regenera con pnpm manifest)`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
for (const e of comp.exports) {
|
|
222
|
+
if (!now[name].exports.includes(e)) {
|
|
223
|
+
errors.push(`API pública: «${name}» ya no exporta ${e} (¿versión mayor? regenera con pnpm manifest)`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const added = Object.keys(now).filter((n) => !before[n]);
|
|
228
|
+
if (added.length) warnings.push(`API pública: componentes nuevos sin registrar en el manifiesto (${added.join(', ')}); ejecuta pnpm manifest`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const w of warnings) console.log(`AVISO ${w}`);
|
|
232
|
+
for (const e of errors) console.log(`ERROR ${e}`);
|
|
233
|
+
if (errors.length) {
|
|
234
|
+
console.log(`\ndoctor: ${errors.length} error(es), ${warnings.length} aviso(s)`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
console.log(`doctor: contrato, repetidos, sombras, fugas, valores fijos y API pública correctos (${warnings.length} aviso(s)).`);
|
package/scripts/init.mjs
CHANGED
|
@@ -1,55 +1,89 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* orbital-ds-init — crea el fichero de MARCA de una app.
|
|
4
|
-
*
|
|
5
|
-
* pnpm exec orbital-ds-init # crea src/brand.css si no existe
|
|
6
|
-
* pnpm exec orbital-ds-init --out app/brand.css
|
|
7
|
-
*
|
|
8
|
-
* NUNCA sobrescribe un fichero existente. Ese fichero es de la app: ninguna
|
|
9
|
-
* actualización de orbital-ds lo toca, y se importa DESPUÉS del tema para que gane.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* orbital-ds-init — crea el fichero de MARCA de una app.
|
|
4
|
+
*
|
|
5
|
+
* pnpm exec orbital-ds-init # crea src/brand.css si no existe
|
|
6
|
+
* pnpm exec orbital-ds-init --out app/brand.css
|
|
7
|
+
*
|
|
8
|
+
* NUNCA sobrescribe un fichero existente. Ese fichero es de la app: ninguna
|
|
9
|
+
* actualización de orbital-ds lo toca, y se importa DESPUÉS del tema para que gane.
|
|
10
|
+
*
|
|
11
|
+
* DESIGN.md: orbital-ds NO lo crea. Si la app ya tiene uno (`./DESIGN.md` o
|
|
12
|
+
* `--design ruta`), añade —o actualiza— solo un bloque delimitado por marcas
|
|
13
|
+
* `<!-- orbital-ds:start -->` … `<!-- orbital-ds:end -->`; el resto no se toca.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, resolve } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const contract = JSON.parse(readFileSync(resolve(ROOT, 'contract.json'), 'utf8'));
|
|
21
|
+
|
|
22
|
+
const outIdx = process.argv.indexOf('--out');
|
|
23
|
+
const out = resolve(outIdx > -1 && process.argv[outIdx + 1] ? process.argv[outIdx + 1] : 'src/brand.css');
|
|
24
|
+
|
|
25
|
+
const brandExiste = existsSync(out);
|
|
26
|
+
if (brandExiste) console.log(`Ya existe ${out}: no se toca (es de tu app).`);
|
|
27
|
+
|
|
28
|
+
if (!brandExiste) {
|
|
29
|
+
const lines = contract.brand.tokens.map(
|
|
30
|
+
(t) => ` /* --${t.name}: ${t.example}; */ /* ${t.about} */`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const css = `/*
|
|
34
|
+
* MARCA de la app — fichero TUYO.
|
|
35
|
+
*
|
|
36
|
+
* Ninguna actualización de orbital-ds lo toca. Se importa DESPUÉS del tema, así que
|
|
37
|
+
* lo que pongas aquí gana. Si no defines nada, se aplica el valor estándar del tema.
|
|
38
|
+
*
|
|
39
|
+
* Estas variables son el grupo estable de «lo que una marca suele cambiar»: renombrar
|
|
40
|
+
* o quitar una de ellas es una versión MAYOR de orbital-ds, con aviso previo.
|
|
41
|
+
* Descomenta solo lo que necesites y cambia el valor.
|
|
42
|
+
*
|
|
43
|
+
* Cambia el color principal TODO junto: primary, ring, link, brand, sidebar-primary y
|
|
44
|
+
* chart-1 deben ir a la par (y comprueba el contraste del texto sobre ellos).
|
|
45
|
+
*/
|
|
46
|
+
:root {
|
|
47
|
+
${lines.join('\n')}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Piel oscura: solo si tu marca necesita valores distintos en oscuro. */
|
|
51
|
+
.dark {
|
|
52
|
+
/* --primary: ...; */
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
57
|
+
writeFileSync(out, css);
|
|
58
|
+
console.log(`Creado ${out}.\n\nImpórtalo DESPUÉS del tema, en el CSS de tu app:\n\n @import 'orbital-ds/themes/<tema>.css';\n @import './brand.css';\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── DESIGN.md de la app: bloque adicional, nunca el fichero entero ───────────────
|
|
62
|
+
const designIdx = process.argv.indexOf('--design');
|
|
63
|
+
const design = resolve(designIdx > -1 && process.argv[designIdx + 1] ? process.argv[designIdx + 1] : 'DESIGN.md');
|
|
64
|
+
if (existsSync(design)) {
|
|
65
|
+
const INI = '<!-- orbital-ds:start -->';
|
|
66
|
+
const FIN = '<!-- orbital-ds:end -->';
|
|
67
|
+
const bloque = `${INI}
|
|
68
|
+
## Sistema de diseño (orbital-ds)
|
|
69
|
+
|
|
70
|
+
Bloque gestionado por \`orbital-ds-init\`; lo de fuera de las marcas es de la app y no se toca.
|
|
71
|
+
|
|
72
|
+
- Reglas de uso: \`node_modules/orbital-ds/AGENTS.md\`
|
|
73
|
+
- Contrato de tokens: \`orbital-ds/contract.json\` · componentes: \`orbital-ds/components.manifest.json\`
|
|
74
|
+
- Cada tema trae su guía (\`orbital-ds/themes/<tema>.guide.md\`) y su ficha (\`<tema>.meta.json\`).
|
|
75
|
+
- La marca de la app va en \`brand.css\` (importado después del tema), nunca editando el paquete.
|
|
76
|
+
- Comprobación: \`pnpm exec orbital-ds-doctor\`
|
|
77
|
+
${FIN}`;
|
|
78
|
+
const actual = readFileSync(design, 'utf8');
|
|
79
|
+
let nuevo;
|
|
80
|
+
if (actual.includes(INI) && actual.includes(FIN)) {
|
|
81
|
+
nuevo = actual.slice(0, actual.indexOf(INI)) + bloque + actual.slice(actual.indexOf(FIN) + FIN.length);
|
|
82
|
+
} else {
|
|
83
|
+
nuevo = `${actual.replace(/\s*$/, '')}\n\n${bloque}\n`;
|
|
84
|
+
}
|
|
85
|
+
if (nuevo !== actual) {
|
|
86
|
+
writeFileSync(design, nuevo);
|
|
87
|
+
console.log(`Actualizado el bloque de orbital-ds en ${design} (el resto no se toca).`);
|
|
88
|
+
} else console.log(`${design}: el bloque de orbital-ds ya está al día.`);
|
|
89
|
+
}
|