orbital-ds 0.2.0 → 0.4.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 +60 -0
- package/README.md +28 -14
- package/components.manifest.json +1255 -0
- package/contract.json +162 -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/twitter.css +290 -0
- package/dist/themes/twitter.fonts.css +6 -0
- package/dist/themes/twitter.scoped.css +290 -0
- package/dist/themes/zyro.css +252 -60
- package/dist/themes/zyro.fonts.css +2 -0
- package/dist/themes/zyro.scoped.css +381 -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 +44 -5
- package/package.json +15 -8
- package/scripts/doctor.mjs +110 -28
- package/scripts/init.mjs +55 -0
- 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,52 @@ 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
|
+
/** 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
|
+
}
|
|
50
82
|
|
|
51
|
-
|
|
83
|
+
const themeInfo = {}; // nombre → { rootVars:Set, darkVars:Set }
|
|
84
|
+
|
|
85
|
+
function checkTheme(file, name) {
|
|
86
|
+
const label = `tema ${name}`;
|
|
52
87
|
const css = stripComments(read(file));
|
|
53
88
|
const roots = blocks(css, /(?:^|[\s}])\:root/);
|
|
54
89
|
const darks = blocks(css, /(?:^|[\s}])\.dark/);
|
|
55
|
-
const themeBlocks = blocks(css, /@theme\s+inline/);
|
|
56
90
|
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
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 };
|
|
60
96
|
|
|
61
97
|
for (const t of contract.root) {
|
|
62
98
|
if (!rootVars.has(`--${t}`)) errors.push(`${label}: falta el token obligatorio --${t} en :root`);
|
|
63
99
|
}
|
|
64
100
|
for (const f of contract.fonts) {
|
|
65
|
-
if (!rootVars.has(`--${f}`)
|
|
66
|
-
errors.push(`${label}: falta la fuente --${f} (en :root o @theme)`);
|
|
67
|
-
}
|
|
101
|
+
if (!rootVars.has(`--${f}`)) errors.push(`${label}: falta la fuente --${f} en :root`);
|
|
68
102
|
}
|
|
69
103
|
if (darks.length) {
|
|
70
104
|
for (const t of contract.dark.avisar_si_falta) {
|
|
@@ -74,38 +108,65 @@ function checkTheme(file, label) {
|
|
|
74
108
|
warnings.push(`${label}: no define piel .dark`);
|
|
75
109
|
}
|
|
76
110
|
|
|
77
|
-
// Repetidos
|
|
78
|
-
for (const [
|
|
111
|
+
// Repetidos (solo declaraciones del nivel superior de cada bloque)
|
|
112
|
+
for (const [kind, list] of [[':root', roots], ['.dark', darks]]) {
|
|
79
113
|
list.forEach((body, idx) => {
|
|
80
114
|
const seen = new Map();
|
|
81
|
-
for (const
|
|
115
|
+
for (const d of declarations(body)) seen.set(d.name, (seen.get(d.name) ?? 0) + 1);
|
|
82
116
|
for (const [v, n] of seen) {
|
|
83
|
-
if (n > 1) errors.push(`${label}: ${v} definido ${n} veces en el mismo bloque ${
|
|
117
|
+
if (n > 1) errors.push(`${label}: ${v} definido ${n} veces en el mismo bloque ${kind} (#${idx + 1})`);
|
|
84
118
|
}
|
|
85
119
|
});
|
|
86
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
|
+
}
|
|
87
128
|
}
|
|
88
129
|
|
|
89
|
-
// 1+2) temas
|
|
130
|
+
// 1+2+3) temas
|
|
90
131
|
const themesDir = join(ROOT, existsSync(join(ROOT, 'src/themes')) ? 'src/themes' : 'dist/themes');
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
// 4) fugas desde el tema base
|
|
138
|
+
const base = contract.baseTheme;
|
|
139
|
+
if (base && themeInfo[base]) {
|
|
140
|
+
const baseDs = [...themeInfo[base].rootVars].filter((v) => v.startsWith('--ds-'));
|
|
141
|
+
for (const [name, info] of Object.entries(themeInfo)) {
|
|
142
|
+
if (name === base) continue;
|
|
143
|
+
for (const v of baseDs) {
|
|
144
|
+
if (!info.rootVars.has(v)) {
|
|
145
|
+
errors.push(`tema ${name}: no define ${v}, que sí define el tema base «${base}» — su valor se colaría al cambiar de tema`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
93
149
|
}
|
|
94
150
|
|
|
95
151
|
// CSS de una app (opcional)
|
|
96
152
|
const cssArgIdx = process.argv.indexOf('--css');
|
|
97
153
|
if (cssArgIdx > -1 && process.argv[cssArgIdx + 1]) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const roots = blocks(css, /(?:^|[\s}])\:root/);
|
|
101
|
-
roots.forEach((body, idx) => {
|
|
154
|
+
const css = stripComments(read(resolve(process.argv[cssArgIdx + 1])));
|
|
155
|
+
blocks(css, /(?:^|[\s}])\:root/).forEach((body, idx) => {
|
|
102
156
|
const seen = new Map();
|
|
103
|
-
for (const
|
|
157
|
+
for (const d of declarations(body)) seen.set(d.name, (seen.get(d.name) ?? 0) + 1);
|
|
104
158
|
for (const [v, n] of seen) if (n > 1) errors.push(`app css: ${v} definido ${n} veces en :root (#${idx + 1})`);
|
|
159
|
+
// Una variable --ds-* que el paquete ya no tiene: la marca la ignoraría en silencio.
|
|
160
|
+
const known = knownDsVars();
|
|
161
|
+
for (const d of declarations(body)) {
|
|
162
|
+
if (d.name.startsWith('--ds-') && !known.has(d.name)) {
|
|
163
|
+
errors.push(`app css: ${d.name} no existe en orbital-ds (¿renombrada o retirada? mira docs/CONTRACT.md)`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
105
166
|
});
|
|
106
167
|
}
|
|
107
168
|
|
|
108
|
-
//
|
|
169
|
+
// 5) valores fijos en componentes
|
|
109
170
|
const NO_HARDCODE = [
|
|
110
171
|
{ re: /#[0-9a-fA-F]{3,8}\b/g, why: 'color hexadecimal', skip: ['chart.tsx'] },
|
|
111
172
|
{ re: /\b(?:rgb|rgba|hsl|hsla|oklch)\((?!from_)/g, why: 'color literal' },
|
|
@@ -133,10 +194,31 @@ if (existsSync(uiDir)) {
|
|
|
133
194
|
}
|
|
134
195
|
}
|
|
135
196
|
|
|
197
|
+
// 6) API pública frente a la huella commiteada
|
|
198
|
+
const manifestPath = join(ROOT, 'components.manifest.json');
|
|
199
|
+
if (existsSync(manifestPath) && existsSync(uiDir)) {
|
|
200
|
+
const { buildManifest } = await import('./manifest.mjs');
|
|
201
|
+
const before = JSON.parse(read(manifestPath)).components;
|
|
202
|
+
const now = buildManifest().components;
|
|
203
|
+
for (const [name, comp] of Object.entries(before)) {
|
|
204
|
+
if (!now[name]) {
|
|
205
|
+
errors.push(`API pública: desaparece el componente «${name}» (¿versión mayor? regenera con pnpm manifest)`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
for (const e of comp.exports) {
|
|
209
|
+
if (!now[name].exports.includes(e)) {
|
|
210
|
+
errors.push(`API pública: «${name}» ya no exporta ${e} (¿versión mayor? regenera con pnpm manifest)`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const added = Object.keys(now).filter((n) => !before[n]);
|
|
215
|
+
if (added.length) warnings.push(`API pública: componentes nuevos sin registrar en el manifiesto (${added.join(', ')}); ejecuta pnpm manifest`);
|
|
216
|
+
}
|
|
217
|
+
|
|
136
218
|
for (const w of warnings) console.log(`AVISO ${w}`);
|
|
137
219
|
for (const e of errors) console.log(`ERROR ${e}`);
|
|
138
220
|
if (errors.length) {
|
|
139
221
|
console.log(`\ndoctor: ${errors.length} error(es), ${warnings.length} aviso(s)`);
|
|
140
222
|
process.exit(1);
|
|
141
223
|
}
|
|
142
|
-
console.log(`doctor: contrato, repetidos
|
|
224
|
+
console.log(`doctor: contrato, repetidos, sombras, fugas, valores fijos y API pública correctos (${warnings.length} aviso(s)).`);
|
package/scripts/init.mjs
ADDED
|
@@ -0,0 +1,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
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const contract = JSON.parse(readFileSync(resolve(ROOT, 'contract.json'), 'utf8'));
|
|
17
|
+
|
|
18
|
+
const outIdx = process.argv.indexOf('--out');
|
|
19
|
+
const out = resolve(outIdx > -1 && process.argv[outIdx + 1] ? process.argv[outIdx + 1] : 'src/brand.css');
|
|
20
|
+
|
|
21
|
+
if (existsSync(out)) {
|
|
22
|
+
console.log(`Ya existe ${out}: no se toca (es de tu app).`);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const lines = contract.brand.tokens.map(
|
|
27
|
+
(t) => ` /* --${t.name}: ${t.example}; */ /* ${t.about} */`,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const css = `/*
|
|
31
|
+
* MARCA de la app — fichero TUYO.
|
|
32
|
+
*
|
|
33
|
+
* Ninguna actualización de orbital-ds lo toca. Se importa DESPUÉS del tema, así que
|
|
34
|
+
* lo que pongas aquí gana. Si no defines nada, se aplica el valor estándar del tema.
|
|
35
|
+
*
|
|
36
|
+
* Estas variables son el grupo estable de «lo que una marca suele cambiar»: renombrar
|
|
37
|
+
* o quitar una de ellas es una versión MAYOR de orbital-ds, con aviso previo.
|
|
38
|
+
* Descomenta solo lo que necesites y cambia el valor.
|
|
39
|
+
*
|
|
40
|
+
* Cambia el color principal TODO junto: primary, ring, link, brand, sidebar-primary y
|
|
41
|
+
* chart-1 deben ir a la par (y comprueba el contraste del texto sobre ellos).
|
|
42
|
+
*/
|
|
43
|
+
:root {
|
|
44
|
+
${lines.join('\n')}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Piel oscura: solo si tu marca necesita valores distintos en oscuro. */
|
|
48
|
+
.dark {
|
|
49
|
+
/* --primary: ...; */
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
54
|
+
writeFileSync(out, css);
|
|
55
|
+
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`);
|
|
@@ -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
|
+
}
|