orbital-ds 0.6.2 → 0.8.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 +20 -0
- package/README.md +87 -73
- package/components.manifest.json +2 -1
- package/contract.json +168 -162
- package/dist/styles.css +32 -10
- package/dist/themes/friendly.css +318 -310
- package/dist/themes/friendly.scoped.css +318 -310
- package/dist/themes/qratia.css +8 -0
- package/dist/themes/qratia.scoped.css +10 -2
- package/dist/themes/tactile.css +430 -415
- package/dist/themes/tactile.meta.json +75 -9
- package/dist/themes/tactile.scoped.css +430 -415
- package/dist/themes/twitter.css +298 -290
- package/dist/themes/twitter.meta.json +54 -7
- package/dist/themes/twitter.scoped.css +298 -290
- package/dist/themes/zyro.css +389 -381
- package/dist/themes/zyro.scoped.css +389 -381
- package/dist/ui/sidebar.js +1 -1
- package/dist/ui/tooltip.js +1 -1
- package/docs/CONTRACT.md +1 -1
- package/package.json +1 -1
- package/scripts/doctor.mjs +29 -0
- package/scripts/init.mjs +351 -89
package/dist/ui/sidebar.js
CHANGED
|
@@ -421,7 +421,7 @@ function SidebarMenuItem({ className, ...props }) {
|
|
|
421
421
|
);
|
|
422
422
|
}
|
|
423
423
|
const sidebarMenuButtonVariants = cva(
|
|
424
|
-
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-(--ds-sidebar-item-radius) px-3 py-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
|
424
|
+
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-(--ds-sidebar-item-radius) px-3 py-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:mx-auto group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
|
425
425
|
{
|
|
426
426
|
variants: {
|
|
427
427
|
variant: {
|
package/dist/ui/tooltip.js
CHANGED
|
@@ -48,7 +48,7 @@ function TooltipContent({
|
|
|
48
48
|
...props,
|
|
49
49
|
children: [
|
|
50
50
|
children,
|
|
51
|
-
/* @__PURE__ */ jsx(TooltipPrimitive.Arrow, { className: "z-(--ds-z-overlay) size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=left]:translate-x-[-1.5px] data-[side=right]:translate-x-[1.5px]" })
|
|
51
|
+
/* @__PURE__ */ jsx(TooltipPrimitive.Arrow, { "data-slot": "tooltip-arrow", className: "z-(--ds-z-overlay) size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=left]:translate-x-[-1.5px] data-[side=right]:translate-x-[1.5px]" })
|
|
52
52
|
]
|
|
53
53
|
}
|
|
54
54
|
)
|
package/docs/CONTRACT.md
CHANGED
|
@@ -77,7 +77,7 @@ Los define la estructura con un valor neutro; un tema los sobrescribe si quiere.
|
|
|
77
77
|
| Capas | `--ds-z-raised`, `--ds-z-sticky`, `--ds-z-sidebar` (sobre la cabecera sticky), `--ds-z-overlay` |
|
|
78
78
|
| Movimiento | `--ds-motion-fast`, `--ds-motion-base`, `--ds-motion-slow`, `--ds-motion-blink` |
|
|
79
79
|
| Foco | `--ds-ring-width` |
|
|
80
|
-
|
|
|
80
|
+
| Lateral | `--ds-sidebar-icon`, `--ds-sidebar-icon-stroke` (iconos), `--ds-sidebar-rail-w` (ancho contraído), `--ds-sidebar-rail-item` (tecla del raíl), `--ds-sidebar-logo` (logo y avatar), `--ds-sidebar-sep-inset` (separadores; negativo = de borde a borde). **Todo tema los define** (contrato) |
|
|
81
81
|
| Velo | `--color-scrim` |
|
|
82
82
|
| **Materiales** | `--ds-card-backdrop`, `--ds-overlay-backdrop`, `--ds-field-backdrop`, `--ds-sidebar-backdrop` (un `backdrop-filter` completo; por defecto `none`) y `--ds-page-backdrop` (un `background-image` para el fondo de la página; por defecto `none`) |
|
|
83
83
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orbital-ds",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Sistema de diseño genérico: estructura común + temas con personalidad (qratia, zyro) + ~60 componentes sobre Base UI y Tailwind v4, con contrato verificable y legible por agentes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"design-system",
|
package/scripts/doctor.mjs
CHANGED
|
@@ -207,6 +207,35 @@ for (const f of themeFiles) {
|
|
|
207
207
|
const r = ratio(over(fg, fondo), fondo);
|
|
208
208
|
if (r < 4.5) errors.push(`tema ${tema} (${piel}): ${estado} sobre ${estado}-soft = ${r.toFixed(2)}:1 (mínimo 4.5:1)`);
|
|
209
209
|
}
|
|
210
|
+
|
|
211
|
+
// Pares principales de texto sobre su superficie. AA (4.5:1) es el mínimo exigible; las
|
|
212
|
+
// excepciones deben estar declaradas en `<tema>.meta.json` → `a11y.exceptions` ("fg/bg").
|
|
213
|
+
// AAA (7:1) es el objetivo del texto del cuerpo: solo avisa, y solo con `--aaa`.
|
|
214
|
+
const PARES = [
|
|
215
|
+
['foreground', 'background'], ['card-foreground', 'card'], ['popover-foreground', 'popover'],
|
|
216
|
+
['primary-foreground', 'primary'], ['secondary-foreground', 'secondary'],
|
|
217
|
+
['muted-foreground', 'background'], ['muted-foreground', 'card'],
|
|
218
|
+
['sidebar-foreground', 'sidebar'], ['sidebar-primary-foreground', 'sidebar-primary'],
|
|
219
|
+
['destructive-foreground', 'destructive'],
|
|
220
|
+
];
|
|
221
|
+
let excepciones = [];
|
|
222
|
+
try { excepciones = JSON.parse(read(join(themesDir, `${tema}.meta.json`))).a11y?.exceptions ?? []; } catch { /* sin ficha: lo avisa 1b */ }
|
|
223
|
+
const blanco = { r: 255, g: 255, b: 255, a: 1 };
|
|
224
|
+
for (const [fgName, bgName] of PARES) {
|
|
225
|
+
const fgRaw = vars[fgName] && parseColor(vars[fgName]);
|
|
226
|
+
const bgRaw = vars[bgName] && parseColor(vars[bgName]);
|
|
227
|
+
if (!fgRaw || !bgRaw) continue;
|
|
228
|
+
const bg = over(bgRaw, over(lienzo, blanco));
|
|
229
|
+
const r = ratio(over(fgRaw, bg), bg);
|
|
230
|
+
const par = `${fgName}/${bgName}`;
|
|
231
|
+
if (r < 4.5) {
|
|
232
|
+
const msg = `tema ${tema} (${piel}): ${par} = ${r.toFixed(2)}:1 (mínimo AA 4.5:1)`;
|
|
233
|
+
if (excepciones.includes(par)) warnings.push(`${msg} — excepción declarada en la ficha`);
|
|
234
|
+
else errors.push(`${msg}; corrígelo o decláralo en ${tema}.meta.json → a11y.exceptions`);
|
|
235
|
+
} else if (process.argv.includes('--aaa') && r < 7 && (par === 'foreground/background' || par === 'card-foreground/card')) {
|
|
236
|
+
warnings.push(`tema ${tema} (${piel}): ${par} = ${r.toFixed(2)}:1 (objetivo AAA 7:1 para el texto del cuerpo)`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
210
239
|
}
|
|
211
240
|
}
|
|
212
241
|
}
|
package/scripts/init.mjs
CHANGED
|
@@ -1,89 +1,351 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* orbital-ds-init —
|
|
4
|
-
*
|
|
5
|
-
* pnpm exec orbital-ds-init
|
|
6
|
-
* pnpm exec orbital-ds-init --
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
let
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* orbital-ds-init — asistente de MARCA de una app.
|
|
4
|
+
*
|
|
5
|
+
* pnpm exec orbital-ds-init # asistente interactivo (terminal)
|
|
6
|
+
* pnpm exec orbital-ds-init --primary "#1D63D8" # sin preguntas (scripts / CI)
|
|
7
|
+
*
|
|
8
|
+
* Opciones:
|
|
9
|
+
* --theme <tema> tema para el que se genera (por defecto: el que importa tu CSS, o qratia)
|
|
10
|
+
* --primary <color> color primario: hex, rgb(), hsl() u oklch()
|
|
11
|
+
* --accent <color> color de acento/gesto (si no, el complementario del primario)
|
|
12
|
+
* --no-marker apaga el rotulador del tema (si el tema lo tiene)
|
|
13
|
+
* --out <fichero> fichero de marca (por defecto src/brand.css)
|
|
14
|
+
* --update reescribe el bloque generado si ya existe
|
|
15
|
+
* --dry-run muestra el resultado sin escribir nada
|
|
16
|
+
* --wizard fuerza el asistente aunque no haya terminal interactiva
|
|
17
|
+
* --yes no pregunta (usa los valores dados)
|
|
18
|
+
* --design <ruta> DESIGN.md donde añadir el bloque de orbital-ds
|
|
19
|
+
*
|
|
20
|
+
* Qué hace: a partir del primario deriva —en OKLCH— los neutros teñidos (papel, tinta,
|
|
21
|
+
* tarjeta, bordes, lateral), el texto sobre el primario, el enlace, el foco, las dos
|
|
22
|
+
* pieles (clara y oscura) y comprueba el contraste (WCAG AA 4,5:1). Escribe SOLO un
|
|
23
|
+
* bloque delimitado en el fichero de marca; lo que la app tenga fuera no se toca.
|
|
24
|
+
*
|
|
25
|
+
* NUNCA sobrescribe lo que es de la app. Sin opciones y sin terminal interactiva (CI),
|
|
26
|
+
* se comporta como siempre: crea la plantilla de marca si falta y añade el bloque de
|
|
27
|
+
* DESIGN.md.
|
|
28
|
+
*
|
|
29
|
+
* Sin dependencias: el color se calcula aquí (matrices OKLab estándar).
|
|
30
|
+
*/
|
|
31
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
32
|
+
import { dirname, join, resolve } from 'node:path';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
import { createInterface } from 'node:readline/promises';
|
|
35
|
+
|
|
36
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
37
|
+
const contract = JSON.parse(readFileSync(resolve(ROOT, 'contract.json'), 'utf8'));
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
const flag = (n) => args.includes(`--${n}`);
|
|
40
|
+
const opt = (n) => { const i = args.indexOf(`--${n}`); return i > -1 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : undefined; };
|
|
41
|
+
|
|
42
|
+
/* ═══════════════════════════════ COLOR ═══════════════════════════════════════ */
|
|
43
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
44
|
+
const toGamma = (x) => (x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055);
|
|
45
|
+
const toLinear = (x) => (x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4);
|
|
46
|
+
|
|
47
|
+
/** OKLCH → sRGB lineal (sin recortar). */
|
|
48
|
+
function oklchLinear({ l, c, h }) {
|
|
49
|
+
const a = c * Math.cos((h * Math.PI) / 180), b = c * Math.sin((h * Math.PI) / 180);
|
|
50
|
+
const l_ = (l + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
51
|
+
const m_ = (l - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
52
|
+
const s_ = (l - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
53
|
+
return [4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_, -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_, -0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_];
|
|
54
|
+
}
|
|
55
|
+
const inGamut = (lin) => lin.every((v) => v >= -1e-4 && v <= 1 + 1e-4);
|
|
56
|
+
|
|
57
|
+
/** OKLCH → {r,g,b} 0..255, reduciendo el croma hasta caber en sRGB. */
|
|
58
|
+
function oklchToRgb({ l, c, h }) {
|
|
59
|
+
let cc = c;
|
|
60
|
+
if (!inGamut(oklchLinear({ l, c: cc, h }))) {
|
|
61
|
+
let lo = 0, hi = c;
|
|
62
|
+
for (let i = 0; i < 24; i++) { const mid = (lo + hi) / 2; if (inGamut(oklchLinear({ l, c: mid, h }))) lo = mid; else hi = mid; }
|
|
63
|
+
cc = lo;
|
|
64
|
+
}
|
|
65
|
+
const lin = oklchLinear({ l, c: cc, h }).map(clamp01);
|
|
66
|
+
const [r, g, b] = lin.map((v) => Math.round(255 * toGamma(v)));
|
|
67
|
+
return { r, g, b };
|
|
68
|
+
}
|
|
69
|
+
/** {r,g,b} 0..255 → OKLCH. */
|
|
70
|
+
function rgbToOklch({ r, g, b }) {
|
|
71
|
+
const [lr, lg, lb] = [r, g, b].map((v) => toLinear(v / 255));
|
|
72
|
+
const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
|
|
73
|
+
const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
|
|
74
|
+
const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
|
|
75
|
+
const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
|
|
76
|
+
const A = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
|
|
77
|
+
const B = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
|
|
78
|
+
const c = Math.hypot(A, B);
|
|
79
|
+
let h = (Math.atan2(B, A) * 180) / Math.PI; if (h < 0) h += 360;
|
|
80
|
+
return { l: L, c, h: c < 1e-4 ? 0 : h };
|
|
81
|
+
}
|
|
82
|
+
const hex = ({ r, g, b }) => '#' + [r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('');
|
|
83
|
+
const rgba = ({ r, g, b }, a) => `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
84
|
+
const ok = (l, c, h) => oklchToRgb({ l, c, h });
|
|
85
|
+
|
|
86
|
+
function lumin({ r, g, b }) { const f = (v) => toLinear(v / 255); return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); }
|
|
87
|
+
function ratio(a, b) { const [x, y] = [lumin(a), lumin(b)].sort((p, q) => q - p); return (x + 0.05) / (y + 0.05); }
|
|
88
|
+
|
|
89
|
+
/** Color CSS → {r,g,b}. Acepta #hex, rgb(), hsl() y oklch(). */
|
|
90
|
+
function parseColor(raw) {
|
|
91
|
+
const v = raw.trim().toLowerCase();
|
|
92
|
+
let m = v.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
|
|
93
|
+
if (m) { let h = m[1]; if (h.length === 3) h = [...h].map((c) => c + c).join(''); return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16) }; }
|
|
94
|
+
m = v.match(/^rgb\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)\s*\)$/);
|
|
95
|
+
if (m) return { r: +m[1], g: +m[2], b: +m[3] };
|
|
96
|
+
m = v.match(/^hsl\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%\s*\)$/);
|
|
97
|
+
if (m) {
|
|
98
|
+
const h = +m[1] / 360, s = +m[2] / 100, l = +m[3] / 100;
|
|
99
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q;
|
|
100
|
+
const f = (t) => { t = (t + 1) % 1; return t < 1 / 6 ? p + (q - p) * 6 * t : t < 1 / 2 ? q : t < 2 / 3 ? p + (q - p) * (2 / 3 - t) * 6 : p; };
|
|
101
|
+
return { r: Math.round(255 * f(h + 1 / 3)), g: Math.round(255 * f(h)), b: Math.round(255 * f(h - 1 / 3)) };
|
|
102
|
+
}
|
|
103
|
+
m = v.match(/^oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)\s*\)$/);
|
|
104
|
+
if (m) return oklchToRgb({ l: m[2] ? +m[1] / 100 : +m[1], c: +m[3], h: +m[4] });
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ═══════════════════════════════ DERIVACIÓN ══════════════════════════════════ */
|
|
109
|
+
/** Ajusta la luminosidad de `color` (OKLCH) en la dirección dada hasta que `bien(rgb)` sea cierto. */
|
|
110
|
+
function ajustar(color, dir, bien) {
|
|
111
|
+
const o = rgbToOklch(color);
|
|
112
|
+
for (let i = 0; i <= 80; i++) {
|
|
113
|
+
const c = oklchToRgb({ ...o, l: clamp01(o.l + dir * 0.01 * i) });
|
|
114
|
+
if (bien(c)) return c;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Deriva la marca. Devuelve { claro, oscuro, avisos } con los tokens (sin el prefijo --).
|
|
121
|
+
* `acento` y `marcador` solo si el tema tiene gestos.
|
|
122
|
+
*/
|
|
123
|
+
function derivar(primario, { acento, marcador = true, gestos = false }) {
|
|
124
|
+
const avisos = [];
|
|
125
|
+
const P = rgbToOklch(primario);
|
|
126
|
+
const H = P.c < 0.02 ? 255 : P.h; // primario casi gris: neutros azulados por defecto
|
|
127
|
+
const tinte = Math.min(0.02, 0.008 + P.c * 0.05);
|
|
128
|
+
|
|
129
|
+
/* ── piel clara ── */
|
|
130
|
+
const papel = ok(0.965, tinte, H), tinta = ok(0.235, 0.05, H), blanco = { r: 255, g: 255, b: 255 };
|
|
131
|
+
const suave = ok(0.925, tinte + 0.008, H);
|
|
132
|
+
let prim = primario;
|
|
133
|
+
const textoSobre = (c) => (ratio(blanco, c) >= ratio(tinta, c) ? blanco : tinta);
|
|
134
|
+
if (ratio(textoSobre(prim), prim) < 4.5) {
|
|
135
|
+
const m = ajustar(prim, -1, (c) => ratio(blanco, c) >= 4.5);
|
|
136
|
+
if (m) { avisos.push(`primario claro ajustado a ${hex(m)} para que el texto sobre él llegue a 4.5:1 (era ${hex(prim)})`); prim = m; }
|
|
137
|
+
}
|
|
138
|
+
const primFg = textoSobre(prim);
|
|
139
|
+
const enlace = ajustar(prim, -1, (c) => ratio(c, papel) >= 4.5 && ratio(c, blanco) >= 4.5) ?? prim;
|
|
140
|
+
const enlaceHover = ajustar(enlace, -1, (c) => rgbToOklch(c).l <= rgbToOklch(enlace).l - 0.07) ?? enlace;
|
|
141
|
+
const brillo = oklchToRgb({ ...rgbToOklch(prim), l: Math.min(0.95, rgbToOklch(prim).l + 0.08) });
|
|
142
|
+
|
|
143
|
+
const acentoC = acento ?? ok(0.82, 0.16, (H + 180) % 360);
|
|
144
|
+
const claro = {
|
|
145
|
+
primary: hex(prim), 'primary-foreground': hex(primFg), ring: hex(prim), link: hex(enlace), 'link-hover': hex(enlaceHover),
|
|
146
|
+
brand: hex(prim), 'brand-bright': hex(brillo), 'sidebar-primary': hex(prim), 'sidebar-primary-foreground': hex(primFg), 'sidebar-ring': hex(prim),
|
|
147
|
+
'chart-1': hex(prim), 'accent-foreground': hex(tinta),
|
|
148
|
+
background: hex(papel), foreground: hex(tinta), card: '#ffffff', 'card-foreground': hex(tinta), popover: '#ffffff', 'popover-foreground': hex(tinta),
|
|
149
|
+
secondary: hex(suave), 'secondary-foreground': hex(tinta), muted: hex(suave), 'muted-foreground': rgba(tinta, 0.72),
|
|
150
|
+
border: rgba(tinta, 0.14), input: hex(tinta),
|
|
151
|
+
sidebar: hex(suave), 'sidebar-foreground': hex(tinta), 'sidebar-accent': '#ffffff', 'sidebar-accent-foreground': hex(tinta), 'sidebar-border': rgba(tinta, 0.26),
|
|
152
|
+
inset: hex(suave), 'inset-foreground': hex(tinta), neutral: rgba(tinta, 0.72), 'neutral-soft': rgba(tinta, 0.08),
|
|
153
|
+
};
|
|
154
|
+
if (gestos) {
|
|
155
|
+
claro['ds-key-outline'] = hex(tinta); claro['ds-key-edge'] = rgba(tinta, 0.26);
|
|
156
|
+
claro['ds-marker'] = marcador ? rgba(acentoC, 0.62) : 'transparent';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ── piel oscura ── */
|
|
160
|
+
const fondoO = ok(0.19, 0.03, H), tarjetaO = ok(0.25, 0.04, H), suaveO = ok(0.22, 0.035, H), textoO = ok(0.95, 0.012, H);
|
|
161
|
+
let primO = oklchToRgb({ ...P, l: Math.max(P.l, 0.74), c: Math.min(P.c, 0.2) });
|
|
162
|
+
const primOFg = fondoO;
|
|
163
|
+
if (ratio(primOFg, primO) < 4.5) primO = ajustar(primO, +1, (c) => ratio(primOFg, c) >= 4.5) ?? primO;
|
|
164
|
+
const enlaceO = ajustar(primO, +1, (c) => ratio(c, fondoO) >= 4.5) ?? primO;
|
|
165
|
+
const enlaceHoverO = oklchToRgb({ ...rgbToOklch(enlaceO), l: Math.min(0.95, rgbToOklch(enlaceO).l + 0.07) });
|
|
166
|
+
const oscuro = {
|
|
167
|
+
primary: hex(primO), 'primary-foreground': hex(primOFg), ring: hex(primO), link: hex(enlaceO), 'link-hover': hex(enlaceHoverO),
|
|
168
|
+
brand: hex(primO), 'brand-bright': hex(enlaceO), 'sidebar-primary': hex(primO), 'sidebar-primary-foreground': hex(primOFg), 'sidebar-ring': hex(primO),
|
|
169
|
+
'chart-1': hex(primO), 'accent-foreground': hex(textoO),
|
|
170
|
+
background: hex(fondoO), foreground: hex(textoO), card: hex(tarjetaO), 'card-foreground': hex(textoO), popover: hex(tarjetaO), 'popover-foreground': hex(textoO),
|
|
171
|
+
secondary: hex(suaveO), 'secondary-foreground': hex(textoO), muted: hex(suaveO), 'muted-foreground': rgba(textoO, 0.72),
|
|
172
|
+
border: rgba(textoO, 0.14), input: rgba(textoO, 0.92),
|
|
173
|
+
sidebar: hex(suaveO), 'sidebar-foreground': hex(textoO), 'sidebar-accent': hex(tarjetaO), 'sidebar-accent-foreground': hex(textoO), 'sidebar-border': rgba(textoO, 0.26),
|
|
174
|
+
inset: hex(suaveO), 'inset-foreground': hex(textoO), neutral: rgba(textoO, 0.72), 'neutral-soft': rgba(textoO, 0.1),
|
|
175
|
+
};
|
|
176
|
+
if (gestos) {
|
|
177
|
+
oscuro['ds-key-outline'] = rgba(textoO, 0.92); oscuro['ds-key-edge'] = rgba(textoO, 0.26);
|
|
178
|
+
oscuro['ds-marker'] = marcador ? rgba(acentoC, 0.45) : 'transparent';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* ── comprobación ── */
|
|
182
|
+
const pares = [
|
|
183
|
+
['claro', 'texto/papel', tinta, papel], ['claro', 'texto/tarjeta', tinta, blanco], ['claro', 'texto sobre primario', primFg, prim],
|
|
184
|
+
['claro', 'enlace/papel', enlace, papel], ['oscuro', 'texto/fondo', textoO, fondoO], ['oscuro', 'texto sobre primario', primOFg, primO], ['oscuro', 'enlace/fondo', enlaceO, fondoO],
|
|
185
|
+
].map(([piel, nombre, f, b]) => ({ piel, nombre, r: ratio(f, b) }));
|
|
186
|
+
return { claro, oscuro, avisos, pares, muestras: { papel, tinta, prim, suave, acentoC, fondoO, textoO, primO } };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* ═══════════════════════════════ SALIDA ══════════════════════════════════════ */
|
|
190
|
+
const TTY_COLOR = process.stdout.isTTY && (process.env.COLORTERM === 'truecolor' || process.env.COLORTERM === '24bit' || process.env.WT_SESSION);
|
|
191
|
+
const muestra = (c) => (TTY_COLOR ? `\x1b[48;2;${c.r};${c.g};${c.b}m \x1b[0m ` : '');
|
|
192
|
+
const INI = '/* orbital-ds:brand:start */';
|
|
193
|
+
const FIN = '/* orbital-ds:brand:end */';
|
|
194
|
+
|
|
195
|
+
function bloqueCss(res, { tema, primario }) {
|
|
196
|
+
const fmt = (tokens) => Object.entries(tokens).map(([k, v]) => ` --${k}: ${v};`).join('\n');
|
|
197
|
+
return `${INI}
|
|
198
|
+
/* Generado por \`orbital-ds-init\` (tema ${tema}, primario ${primario}). Si vuelves a ejecutarlo con
|
|
199
|
+
* --update, solo se reescribe este bloque; lo que tengas fuera no se toca. */
|
|
200
|
+
:root {
|
|
201
|
+
${fmt(res.claro)}
|
|
202
|
+
}
|
|
203
|
+
.dark {
|
|
204
|
+
${fmt(res.oscuro)}
|
|
205
|
+
}
|
|
206
|
+
${FIN}
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function escribirBloque(file, bloque) {
|
|
211
|
+
const actual = existsSync(file) ? readFileSync(file, 'utf8') : null;
|
|
212
|
+
if (actual === null) {
|
|
213
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
214
|
+
writeFileSync(file, `/*\n * MARCA de la app — fichero TUYO. Se importa DESPUÉS del tema, así que lo que pongas aquí gana.\n */\n\n${bloque}`);
|
|
215
|
+
return 'creado';
|
|
216
|
+
}
|
|
217
|
+
if (actual.includes(INI) && actual.includes(FIN)) {
|
|
218
|
+
const nuevo = actual.slice(0, actual.indexOf(INI)) + bloque.trimEnd() + actual.slice(actual.indexOf(FIN) + FIN.length);
|
|
219
|
+
if (nuevo === actual) return 'igual';
|
|
220
|
+
writeFileSync(file, nuevo);
|
|
221
|
+
return 'actualizado';
|
|
222
|
+
}
|
|
223
|
+
writeFileSync(file, `${actual.replace(/\s*$/, '')}\n\n${bloque}`);
|
|
224
|
+
return 'añadido';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* ═══════════════════════════════ TEMAS ═══════════════════════════════════════ */
|
|
228
|
+
const temasDir = existsSync(join(ROOT, 'src/themes')) ? join(ROOT, 'src/themes') : join(ROOT, 'dist/themes');
|
|
229
|
+
const temas = readdirSync(temasDir).filter((f) => f.endsWith('.meta.json')).map((f) => JSON.parse(readFileSync(join(temasDir, f), 'utf8')));
|
|
230
|
+
const TEMA_CSS = (() => {
|
|
231
|
+
for (const f of ['src/styles.css', 'app/styles.css', 'src/index.css']) {
|
|
232
|
+
if (existsSync(f)) { const m = readFileSync(f, 'utf8').match(/orbital-ds\/themes\/([\w-]+)\.css/); if (m) return m[1]; }
|
|
233
|
+
}
|
|
234
|
+
return 'qratia';
|
|
235
|
+
})();
|
|
236
|
+
/** Un tema tiene gestos si su ficha (`<tema>.meta.json`) los declara con variables públicas. */
|
|
237
|
+
const tieneGestos = (tema) => (temas.find((t) => t.name === tema)?.gestures ?? []).length > 0;
|
|
238
|
+
|
|
239
|
+
/* ═══════════════════════════════ FLUJO ═══════════════════════════════════════ */
|
|
240
|
+
function plantillaClasica() {
|
|
241
|
+
const out = resolve(opt('out') ?? 'src/brand.css');
|
|
242
|
+
if (existsSync(out)) console.log(`Ya existe ${out}: no se toca (es de tu app).`);
|
|
243
|
+
else {
|
|
244
|
+
const lines = contract.brand.tokens.map((t) => ` /* --${t.name}: ${t.example}; */ /* ${t.about} */`);
|
|
245
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
246
|
+
writeFileSync(out, `/*\n * MARCA de la app — fichero TUYO.\n *\n * Ninguna actualización de orbital-ds lo toca. Se importa DESPUÉS del tema, así que\n * lo que pongas aquí gana. Si no defines nada, se aplica el valor estándar del tema.\n *\n * Para generar la marca completa desde un color: pnpm exec orbital-ds-init --primary "#1D63D8"\n */\n:root {\n${lines.join('\n')}\n}\n\n/* Piel oscura: solo si tu marca necesita valores distintos en oscuro. */\n.dark {\n /* --primary: ...; */\n}\n`);
|
|
247
|
+
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`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function actualizarDesign() {
|
|
252
|
+
const design = resolve(opt('design') ?? 'DESIGN.md');
|
|
253
|
+
if (!existsSync(design)) return;
|
|
254
|
+
const I = '<!-- orbital-ds:start -->', F = '<!-- orbital-ds:end -->';
|
|
255
|
+
const bloque = `${I}
|
|
256
|
+
## Sistema de diseño (orbital-ds)
|
|
257
|
+
|
|
258
|
+
Bloque gestionado por \`orbital-ds-init\`; lo de fuera de las marcas es de la app y no se toca.
|
|
259
|
+
|
|
260
|
+
- Reglas de uso y estándar de integración: \`node_modules/orbital-ds/AGENTS.md\`
|
|
261
|
+
- Contrato de tokens: \`orbital-ds/contract.json\` · componentes: \`orbital-ds/components.manifest.json\`
|
|
262
|
+
- Cada tema trae su guía (\`orbital-ds/themes/<tema>.guide.md\`) y su ficha (\`<tema>.meta.json\`).
|
|
263
|
+
- La marca de la app va en \`brand.css\` (importado después del tema), nunca editando el paquete.
|
|
264
|
+
- Comprobación: \`pnpm exec orbital-ds-doctor --css src/brand.css\` (contraste AA incluido)
|
|
265
|
+
${F}`;
|
|
266
|
+
const actual = readFileSync(design, 'utf8');
|
|
267
|
+
const nuevo = actual.includes(I) && actual.includes(F)
|
|
268
|
+
? actual.slice(0, actual.indexOf(I)) + bloque + actual.slice(actual.indexOf(F) + F.length)
|
|
269
|
+
: `${actual.replace(/\s*$/, '')}\n\n${bloque}\n`;
|
|
270
|
+
if (nuevo !== actual) { writeFileSync(design, nuevo); console.log(`Actualizado el bloque de orbital-ds en ${design} (el resto no se toca).`); }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function mostrar(res, tema, primario) {
|
|
274
|
+
const m = res.muestras;
|
|
275
|
+
console.log(`\nMarca derivada de ${primario} para el tema «${tema}»\n`);
|
|
276
|
+
console.log(` Claro ${muestra(m.papel)}papel ${hex(m.papel)} ${muestra(m.tinta)}tinta ${hex(m.tinta)} ${muestra(m.prim)}primario ${hex(m.prim)} ${muestra(m.acentoC)}acento ${hex(m.acentoC)}`);
|
|
277
|
+
console.log(` Oscuro ${muestra(m.fondoO)}fondo ${hex(m.fondoO)} ${muestra(m.textoO)}texto ${hex(m.textoO)} ${muestra(m.primO)}primario ${hex(m.primO)}\n`);
|
|
278
|
+
console.log(' Contraste (mínimo AA 4.5:1):');
|
|
279
|
+
for (const p of res.pares) console.log(` ${p.r >= 7 ? '✓ AAA' : p.r >= 4.5 ? '✓ AA ' : '✗ '} ${p.r.toFixed(2).padStart(5)}:1 ${p.piel.padEnd(6)} ${p.nombre}`);
|
|
280
|
+
for (const a of res.avisos) console.log(` ! ${a}`);
|
|
281
|
+
console.log('');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function asistente() {
|
|
285
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
286
|
+
const lineas = rl[Symbol.asyncIterator]();
|
|
287
|
+
const preguntar = async (q, def) => {
|
|
288
|
+
process.stdout.write(def ? `${q} [${def}] ` : `${q} `);
|
|
289
|
+
const { value, done } = await lineas.next();
|
|
290
|
+
const r = (done ? '' : value).trim();
|
|
291
|
+
if (!process.stdin.isTTY) process.stdout.write(`${r}
|
|
292
|
+
`); // eco cuando las respuestas llegan por tubería
|
|
293
|
+
return r || def || '';
|
|
294
|
+
};
|
|
295
|
+
console.log('\norbital-ds · asistente de marca\n');
|
|
296
|
+
console.log('Temas disponibles:');
|
|
297
|
+
temas.forEach((t, i) => console.log(` ${i + 1}. ${t.name.padEnd(9)} ${t.essence}`));
|
|
298
|
+
const nt = await preguntar('\n¿Para qué tema? (número o nombre)', TEMA_CSS);
|
|
299
|
+
const tema = (temas[+nt - 1] ?? temas.find((t) => t.name === nt) ?? temas.find((t) => t.name === TEMA_CSS)).name;
|
|
300
|
+
let primario;
|
|
301
|
+
for (;;) {
|
|
302
|
+
const raw = await preguntar('Color primario (hex, rgb(), hsl() u oklch()):', '');
|
|
303
|
+
primario = raw && parseColor(raw);
|
|
304
|
+
if (primario) { console.log(` ${muestra(primario)}${hex(primario)}`); break; }
|
|
305
|
+
console.log(' No entiendo ese color. Ejemplos: #1D63D8 · rgb(29 99 216) · hsl(219 76% 48%) · oklch(0.55 0.2 260)');
|
|
306
|
+
}
|
|
307
|
+
const gestos = tieneGestos(tema);
|
|
308
|
+
let marcador = true, acento;
|
|
309
|
+
if (gestos) {
|
|
310
|
+
const P = rgbToOklch(primario);
|
|
311
|
+
const prop = [['complementario', 180], ['análogo', 30], ['triádico', 120]].map(([n, d]) => [n, ok(0.82, 0.16, (P.h + d) % 360)]);
|
|
312
|
+
console.log('\nAcento / color del rotulador (opcional). Propuestas:');
|
|
313
|
+
prop.forEach(([n, c], i) => console.log(` ${i + 1}. ${muestra(c)}${n.padEnd(15)} ${hex(c)}`));
|
|
314
|
+
const a = await preguntar('Elige 1-3, escribe otro color, o Enter para el complementario:', '1');
|
|
315
|
+
acento = prop[+a - 1]?.[1] ?? parseColor(a) ?? prop[0][1];
|
|
316
|
+
const r = await preguntar('¿Rotulador bajo los títulos? (s/n)', 's');
|
|
317
|
+
marcador = !/^n/i.test(r);
|
|
318
|
+
}
|
|
319
|
+
const res = derivar(primario, { acento, marcador, gestos });
|
|
320
|
+
mostrar(res, tema, hex(primario));
|
|
321
|
+
const out = resolve(opt('out') ?? 'src/brand.css');
|
|
322
|
+
const ok2 = await preguntar(`¿Escribo el bloque en ${out}? (s/n)`, 's');
|
|
323
|
+
rl.close();
|
|
324
|
+
if (/^n/i.test(ok2)) { console.log('No se ha escrito nada.'); return; }
|
|
325
|
+
const estado = escribirBloque(out, bloqueCss(res, { tema, primario: hex(primario) }));
|
|
326
|
+
console.log(`\nBloque ${estado} en ${out}. Lo que tengas fuera de las marcas no se ha tocado.`);
|
|
327
|
+
actualizarDesign();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function directo() {
|
|
331
|
+
const primario = parseColor(opt('primary'));
|
|
332
|
+
if (!primario) { console.error('Color primario no válido. Usa hex, rgb(), hsl() u oklch().'); process.exit(1); }
|
|
333
|
+
const tema = opt('theme') ?? TEMA_CSS;
|
|
334
|
+
if (!temas.some((t) => t.name === tema)) { console.error(`El tema «${tema}» no existe. Disponibles: ${temas.map((t) => t.name).join(', ')}`); process.exit(1); }
|
|
335
|
+
const acento = opt('accent') ? parseColor(opt('accent')) : undefined;
|
|
336
|
+
const res = derivar(primario, { acento, marcador: !flag('no-marker'), gestos: tieneGestos(tema) });
|
|
337
|
+
mostrar(res, tema, hex(primario));
|
|
338
|
+
const bloque = bloqueCss(res, { tema, primario: hex(primario) });
|
|
339
|
+
const out = resolve(opt('out') ?? 'src/brand.css');
|
|
340
|
+
if (flag('dry-run')) { console.log(bloque); return; }
|
|
341
|
+
if (existsSync(out) && readFileSync(out, 'utf8').includes(INI) && !flag('update')) {
|
|
342
|
+
console.log(`Ya hay un bloque generado en ${out}. Usa --update para reescribirlo.`); return;
|
|
343
|
+
}
|
|
344
|
+
const estado = escribirBloque(out, bloque);
|
|
345
|
+
console.log(`Bloque ${estado} en ${out}.`);
|
|
346
|
+
actualizarDesign();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (opt('primary')) await directo();
|
|
350
|
+
else if (flag('wizard') || (process.stdin.isTTY && process.stdout.isTTY && !flag('classic'))) await asistente();
|
|
351
|
+
else { plantillaClasica(); actualizarDesign(); }
|