orbital-ds 0.2.0 → 0.3.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/AGENTS.md +57 -0
- package/README.md +22 -14
- package/components.manifest.json +1255 -0
- package/contract.json +82 -38
- package/dist/styles.css +486 -427
- package/dist/themes/qratia.css +14 -14
- package/dist/themes/qratia.fonts.css +2 -0
- package/dist/themes/qratia.scoped.css +247 -0
- package/dist/themes/zyro.css +214 -60
- package/dist/themes/zyro.fonts.css +2 -0
- package/dist/themes/zyro.scoped.css +343 -0
- package/dist/ui/attachment.d.ts +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/bubble.d.ts +1 -1
- package/dist/ui/button.d.ts +2 -2
- package/dist/ui/input-group.d.ts +1 -1
- package/dist/ui/item.d.ts +2 -2
- package/dist/ui/sidebar.js +4 -4
- package/dist/ui/tabs.d.ts +1 -1
- package/docs/CONTRACT.md +15 -5
- package/package.json +11 -6
- package/scripts/doctor.mjs +88 -28
- package/scripts/manifest.mjs +79 -0
package/scripts/doctor.mjs
CHANGED
|
@@ -3,14 +3,21 @@
|
|
|
3
3
|
* orbital-ds doctor — comprueba que el sistema se cumple.
|
|
4
4
|
*
|
|
5
5
|
* 1. CONTRATO: cada tema (themes/*.css) define en :root todos los tokens de
|
|
6
|
-
* contract.json
|
|
7
|
-
* 2. REPETIDOS: ninguna variable
|
|
8
|
-
* 3.
|
|
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,
|
|
9
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.
|
|
10
17
|
*
|
|
11
18
|
* Uso:
|
|
12
19
|
* node scripts/doctor.mjs # temas y componentes del paquete
|
|
13
|
-
* node scripts/doctor.mjs --css app.css # además, un CSS de una app (
|
|
20
|
+
* node scripts/doctor.mjs --css app.css # además, un CSS de una app (repetidos)
|
|
14
21
|
*
|
|
15
22
|
* Sale con código 1 si hay errores (los avisos no fallan).
|
|
16
23
|
*/
|
|
@@ -26,7 +33,7 @@ const warnings = [];
|
|
|
26
33
|
const read = (p) => readFileSync(p, 'utf8');
|
|
27
34
|
const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
28
35
|
|
|
29
|
-
/**
|
|
36
|
+
/** Cuerpo de cada bloque `selector { ... }` (respeta llaves anidadas). */
|
|
30
37
|
function blocks(css, selectorRe) {
|
|
31
38
|
const out = [];
|
|
32
39
|
const re = new RegExp(selectorRe.source + '\\s*\\{', 'g');
|
|
@@ -46,25 +53,37 @@ function blocks(css, selectorRe) {
|
|
|
46
53
|
return out;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
|
-
|
|
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
|
+
const themeInfo = {}; // nombre → { rootVars:Set, darkVars:Set }
|
|
50
69
|
|
|
51
|
-
function checkTheme(file,
|
|
70
|
+
function checkTheme(file, name) {
|
|
71
|
+
const label = `tema ${name}`;
|
|
52
72
|
const css = stripComments(read(file));
|
|
53
73
|
const roots = blocks(css, /(?:^|[\s}])\:root/);
|
|
54
74
|
const darks = blocks(css, /(?:^|[\s}])\.dark/);
|
|
55
|
-
const themeBlocks = blocks(css, /@theme\s+inline/);
|
|
56
75
|
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
76
|
+
const rootDecl = roots.flatMap(declarations);
|
|
77
|
+
const darkDecl = darks.flatMap(declarations);
|
|
78
|
+
const rootVars = new Set(rootDecl.map((d) => d.name));
|
|
79
|
+
const darkVars = new Set(darkDecl.map((d) => d.name));
|
|
80
|
+
themeInfo[name] = { rootVars, darkVars };
|
|
60
81
|
|
|
61
82
|
for (const t of contract.root) {
|
|
62
83
|
if (!rootVars.has(`--${t}`)) errors.push(`${label}: falta el token obligatorio --${t} en :root`);
|
|
63
84
|
}
|
|
64
85
|
for (const f of contract.fonts) {
|
|
65
|
-
if (!rootVars.has(`--${f}`)
|
|
66
|
-
errors.push(`${label}: falta la fuente --${f} (en :root o @theme)`);
|
|
67
|
-
}
|
|
86
|
+
if (!rootVars.has(`--${f}`)) errors.push(`${label}: falta la fuente --${f} en :root`);
|
|
68
87
|
}
|
|
69
88
|
if (darks.length) {
|
|
70
89
|
for (const t of contract.dark.avisar_si_falta) {
|
|
@@ -74,38 +93,58 @@ function checkTheme(file, label) {
|
|
|
74
93
|
warnings.push(`${label}: no define piel .dark`);
|
|
75
94
|
}
|
|
76
95
|
|
|
77
|
-
// Repetidos
|
|
78
|
-
for (const [
|
|
96
|
+
// Repetidos (solo declaraciones del nivel superior de cada bloque)
|
|
97
|
+
for (const [kind, list] of [[':root', roots], ['.dark', darks]]) {
|
|
79
98
|
list.forEach((body, idx) => {
|
|
80
99
|
const seen = new Map();
|
|
81
|
-
for (const
|
|
100
|
+
for (const d of declarations(body)) seen.set(d.name, (seen.get(d.name) ?? 0) + 1);
|
|
82
101
|
for (const [v, n] of seen) {
|
|
83
|
-
if (n > 1) errors.push(`${label}: ${v} definido ${n} veces en el mismo bloque ${
|
|
102
|
+
if (n > 1) errors.push(`${label}: ${v} definido ${n} veces en el mismo bloque ${kind} (#${idx + 1})`);
|
|
84
103
|
}
|
|
85
104
|
});
|
|
86
105
|
}
|
|
106
|
+
|
|
107
|
+
// Sombras: `none` invalida el box-shadow
|
|
108
|
+
for (const d of [...rootDecl, ...darkDecl]) {
|
|
109
|
+
if (/^--ds-(?:shadow|sidebar-shadow)/.test(d.name) && /^none$/i.test(d.value)) {
|
|
110
|
+
errors.push(`${label}: ${d.name}: none — usa «0 0 #0000» (none invalida todo el box-shadow)`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
87
113
|
}
|
|
88
114
|
|
|
89
|
-
// 1+2) temas
|
|
115
|
+
// 1+2+3) temas
|
|
90
116
|
const themesDir = join(ROOT, existsSync(join(ROOT, 'src/themes')) ? 'src/themes' : 'dist/themes');
|
|
91
|
-
|
|
92
|
-
|
|
117
|
+
const themeFiles = readdirSync(themesDir).filter(
|
|
118
|
+
(n) => n.endsWith('.css') && !n.endsWith('.fonts.css') && !n.endsWith('.scoped.css'),
|
|
119
|
+
);
|
|
120
|
+
for (const f of themeFiles) checkTheme(join(themesDir, f), f.replace(/\.css$/, ''));
|
|
121
|
+
|
|
122
|
+
// 4) fugas desde el tema base
|
|
123
|
+
const base = contract.baseTheme;
|
|
124
|
+
if (base && themeInfo[base]) {
|
|
125
|
+
const baseDs = [...themeInfo[base].rootVars].filter((v) => v.startsWith('--ds-'));
|
|
126
|
+
for (const [name, info] of Object.entries(themeInfo)) {
|
|
127
|
+
if (name === base) continue;
|
|
128
|
+
for (const v of baseDs) {
|
|
129
|
+
if (!info.rootVars.has(v)) {
|
|
130
|
+
errors.push(`tema ${name}: no define ${v}, que sí define el tema base «${base}» — su valor se colaría al cambiar de tema`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
93
134
|
}
|
|
94
135
|
|
|
95
136
|
// CSS de una app (opcional)
|
|
96
137
|
const cssArgIdx = process.argv.indexOf('--css');
|
|
97
138
|
if (cssArgIdx > -1 && process.argv[cssArgIdx + 1]) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const roots = blocks(css, /(?:^|[\s}])\:root/);
|
|
101
|
-
roots.forEach((body, idx) => {
|
|
139
|
+
const css = stripComments(read(resolve(process.argv[cssArgIdx + 1])));
|
|
140
|
+
blocks(css, /(?:^|[\s}])\:root/).forEach((body, idx) => {
|
|
102
141
|
const seen = new Map();
|
|
103
|
-
for (const
|
|
142
|
+
for (const d of declarations(body)) seen.set(d.name, (seen.get(d.name) ?? 0) + 1);
|
|
104
143
|
for (const [v, n] of seen) if (n > 1) errors.push(`app css: ${v} definido ${n} veces en :root (#${idx + 1})`);
|
|
105
144
|
});
|
|
106
145
|
}
|
|
107
146
|
|
|
108
|
-
//
|
|
147
|
+
// 5) valores fijos en componentes
|
|
109
148
|
const NO_HARDCODE = [
|
|
110
149
|
{ re: /#[0-9a-fA-F]{3,8}\b/g, why: 'color hexadecimal', skip: ['chart.tsx'] },
|
|
111
150
|
{ re: /\b(?:rgb|rgba|hsl|hsla|oklch)\((?!from_)/g, why: 'color literal' },
|
|
@@ -133,10 +172,31 @@ if (existsSync(uiDir)) {
|
|
|
133
172
|
}
|
|
134
173
|
}
|
|
135
174
|
|
|
175
|
+
// 6) API pública frente a la huella commiteada
|
|
176
|
+
const manifestPath = join(ROOT, 'components.manifest.json');
|
|
177
|
+
if (existsSync(manifestPath) && existsSync(uiDir)) {
|
|
178
|
+
const { buildManifest } = await import('./manifest.mjs');
|
|
179
|
+
const before = JSON.parse(read(manifestPath)).components;
|
|
180
|
+
const now = buildManifest().components;
|
|
181
|
+
for (const [name, comp] of Object.entries(before)) {
|
|
182
|
+
if (!now[name]) {
|
|
183
|
+
errors.push(`API pública: desaparece el componente «${name}» (¿versión mayor? regenera con pnpm manifest)`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
for (const e of comp.exports) {
|
|
187
|
+
if (!now[name].exports.includes(e)) {
|
|
188
|
+
errors.push(`API pública: «${name}» ya no exporta ${e} (¿versión mayor? regenera con pnpm manifest)`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const added = Object.keys(now).filter((n) => !before[n]);
|
|
193
|
+
if (added.length) warnings.push(`API pública: componentes nuevos sin registrar en el manifiesto (${added.join(', ')}); ejecuta pnpm manifest`);
|
|
194
|
+
}
|
|
195
|
+
|
|
136
196
|
for (const w of warnings) console.log(`AVISO ${w}`);
|
|
137
197
|
for (const e of errors) console.log(`ERROR ${e}`);
|
|
138
198
|
if (errors.length) {
|
|
139
199
|
console.log(`\ndoctor: ${errors.length} error(es), ${warnings.length} aviso(s)`);
|
|
140
200
|
process.exit(1);
|
|
141
201
|
}
|
|
142
|
-
console.log(`doctor: contrato, repetidos
|
|
202
|
+
console.log(`doctor: contrato, repetidos, sombras, fugas, valores fijos y API pública correctos (${warnings.length} aviso(s)).`);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Manifiesto de componentes de orbital-ds (`components.manifest.json`).
|
|
4
|
+
*
|
|
5
|
+
* Lo leen los agentes de IA (qué hay, cómo se importa, qué `data-slot` expone,
|
|
6
|
+
* qué dependencias opcionales pide) y `pnpm check:ds` lo usa como HUELLA de la API
|
|
7
|
+
* pública: si desaparece un componente o un export respecto al manifiesto
|
|
8
|
+
* commiteado, doctor falla salvo que se haga una versión mayor.
|
|
9
|
+
*
|
|
10
|
+
* node scripts/manifest.mjs # regenera components.manifest.json
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, readdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import { join, dirname, resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const UI = join(ROOT, 'src/ui');
|
|
18
|
+
|
|
19
|
+
// Dependencias que SIEMPRE están (peers obligatorios): no se listan como opcionales.
|
|
20
|
+
const REQUIRED = new Set([
|
|
21
|
+
'react', 'react-dom', 'class-variance-authority', 'clsx', 'tailwind-merge', 'lucide-react',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function parseExports(src) {
|
|
25
|
+
const names = new Set();
|
|
26
|
+
for (const m of src.matchAll(/export\s*\{([^}]*)\}/g)) {
|
|
27
|
+
for (const part of m[1].split(',')) {
|
|
28
|
+
const n = part.trim().split(/\s+as\s+/i).pop()?.trim();
|
|
29
|
+
if (n) names.add(n);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const m of src.matchAll(/export\s+(?:async\s+)?(?:function|const|class)\s+([A-Za-z0-9_]+)/g)) names.add(m[1]);
|
|
33
|
+
return [...names].sort();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseSlots(src) {
|
|
37
|
+
return [...new Set([...src.matchAll(/data-slot="([a-z0-9-]+)"/g)].map((m) => m[1]))].sort();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseOptionalPeers(src) {
|
|
41
|
+
const peers = new Set();
|
|
42
|
+
for (const m of src.matchAll(/from\s+"([^"]+)"/g)) {
|
|
43
|
+
const spec = m[1];
|
|
44
|
+
if (spec.startsWith('.')) continue;
|
|
45
|
+
const pkg = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
|
|
46
|
+
if (pkg === '@base-ui/react' || REQUIRED.has(pkg)) continue;
|
|
47
|
+
peers.add(pkg);
|
|
48
|
+
}
|
|
49
|
+
return [...peers].sort();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildManifest() {
|
|
53
|
+
const components = {};
|
|
54
|
+
for (const f of readdirSync(UI).filter((n) => n.endsWith('.tsx')).sort()) {
|
|
55
|
+
const src = readFileSync(join(UI, f), 'utf8');
|
|
56
|
+
const name = f.replace(/\.tsx$/, '');
|
|
57
|
+
components[name] = {
|
|
58
|
+
import: `orbital-ds/ui/${name}`,
|
|
59
|
+
exports: parseExports(src),
|
|
60
|
+
slots: parseSlots(src),
|
|
61
|
+
optionalPeers: parseOptionalPeers(src),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
|
|
65
|
+
return {
|
|
66
|
+
package: pkg.name,
|
|
67
|
+
version: pkg.version,
|
|
68
|
+
engine: '@base-ui/react',
|
|
69
|
+
docs: ['README.md', 'docs/CONTRACT.md', 'AGENTS.md', 'contract.json'],
|
|
70
|
+
components,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
75
|
+
const out = join(ROOT, 'components.manifest.json');
|
|
76
|
+
const manifest = buildManifest();
|
|
77
|
+
writeFileSync(out, JSON.stringify(manifest, null, 2) + '\n');
|
|
78
|
+
console.log(`manifest: ${Object.keys(manifest.components).length} componentes → components.manifest.json`);
|
|
79
|
+
}
|