orbital-ds 0.7.0 → 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 +1 -1
- package/README.md +87 -73
- package/components.manifest.json +1 -1
- package/dist/themes/tactile.css +24 -23
- package/dist/themes/tactile.meta.json +75 -9
- package/dist/themes/tactile.scoped.css +24 -23
- package/package.json +10 -10
- package/scripts/doctor.mjs +0 -0
- package/scripts/init.mjs +351 -89
package/AGENTS.md
CHANGED
|
@@ -64,7 +64,7 @@ Copia `themes/qratia.css` (o `zyro.css`), cambia valores, no quites ninguno, añ
|
|
|
64
64
|
Para que una app (o un agente que la edite) use orbital-ds sin romper nada:
|
|
65
65
|
|
|
66
66
|
1. **La marca vive solo en `brand.css`.** Ninguna regla de color propia fuera de ese fichero; jamás se edita el paquete.
|
|
67
|
-
2. **Solo variables públicas.** Nada de variables privadas de un tema
|
|
67
|
+
2. **Solo variables públicas.** Nada de variables privadas de un tema: pueden renombrarse. Usa `--ds-*` y los tokens del contrato (`--zw-*` de `tactile` están obsoletas: usa `--ds-key-outline`, `--ds-key-edge` y `--ds-marker`).
|
|
68
68
|
3. **Sin `!important` ni selectores propios** para vencer a la librería (lateral, raíl, botones). Si algo no se puede ajustar con un token, es un hueco de la librería: abre un issue.
|
|
69
69
|
4. **El tema se elige con un solo `@import`** (o `pnpm theme`). Los temas con ámbito (`*.scoped.css`) son solo para un banco de pruebas; la marca (`brand.css`) gana siempre, también sobre ellos.
|
|
70
70
|
5. **Patrón de página.** La cabecera lleva solo botón de menú, título y, siempre visibles, idioma y tema (y avisos si la app los tiene). El subtítulo y las acciones de la vista van al inicio del contenido, centrado en un ancho máximo; solo un lienzo (mapa, kanban, editor) ocupa todo el ancho.
|
package/README.md
CHANGED
|
@@ -1,73 +1,87 @@
|
|
|
1
|
-
# orbital-ds
|
|
2
|
-
|
|
3
|
-
Sistema de diseño genérico: **estructura común + temas intercambiables + ~60 componentes**
|
|
4
|
-
sobre [Base UI](https://base-ui.com) y Tailwind v4 (los de shadcn en su versión Base UI).
|
|
5
|
-
|
|
6
|
-
- `styles.css` — la ESTRUCTURA: mapea variables de tema a utilidades de Tailwind y define los
|
|
7
|
-
tokens semánticos (radios, alturas, sombras, espaciado, capas, movimiento, foco, materiales)
|
|
8
|
-
y la capa base. No lleva colores ni fuentes.
|
|
9
|
-
- `themes/<tema>.css` — un TEMA: paleta (claro/oscuro), radio base y tipografía.
|
|
10
|
-
Hoy: `qratia` y `zyro` (cristal). Añadir otro = añadir otro fichero.
|
|
11
|
-
- `themes/<tema>.fonts.css` — las fuentes del tema (un `@import url()`; va el primero).
|
|
12
|
-
- `ui/*` — componentes, uno por fichero (`orbital-ds/ui/button`, …): quien no usa gráficos
|
|
13
|
-
no necesita `recharts`, quien no usa el calendario no necesita `react-day-picker`.
|
|
14
|
-
- `contract.json` + `pnpm check:ds` — el contrato de tema y su comprobación.
|
|
15
|
-
|
|
16
|
-
## Uso en una app (Tailwind v4)
|
|
17
|
-
|
|
18
|
-
```css
|
|
19
|
-
@import 'orbital-ds/themes/qratia.fonts.css'; /* fuentes: lo primero */
|
|
20
|
-
@import 'tailwindcss';
|
|
21
|
-
@plugin '@tailwindcss/typography';
|
|
22
|
-
@import 'tw-animate-css';
|
|
23
|
-
@import 'orbital-ds/styles.css'; /* estructura */
|
|
24
|
-
@import 'orbital-ds/themes/qratia.css'; /* tema (después de la estructura) */
|
|
25
|
-
@source '../node_modules/orbital-ds/dist'; /* que Tailwind vea las clases del paquete */
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
```tsx
|
|
29
|
-
import { Button } from 'orbital-ds/ui/button';
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Cambiar de identidad = cambiar `qratia` por `zyro` en esas dos líneas de tema/fuentes.
|
|
33
|
-
Detalle del contrato, tokens y reglas de nombres en [`docs/CONTRACT.md`](docs/CONTRACT.md).
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
##
|
|
72
|
-
|
|
73
|
-
|
|
1
|
+
# orbital-ds
|
|
2
|
+
|
|
3
|
+
Sistema de diseño genérico: **estructura común + temas intercambiables + ~60 componentes**
|
|
4
|
+
sobre [Base UI](https://base-ui.com) y Tailwind v4 (los de shadcn en su versión Base UI).
|
|
5
|
+
|
|
6
|
+
- `styles.css` — la ESTRUCTURA: mapea variables de tema a utilidades de Tailwind y define los
|
|
7
|
+
tokens semánticos (radios, alturas, sombras, espaciado, capas, movimiento, foco, materiales)
|
|
8
|
+
y la capa base. No lleva colores ni fuentes.
|
|
9
|
+
- `themes/<tema>.css` — un TEMA: paleta (claro/oscuro), radio base y tipografía.
|
|
10
|
+
Hoy: `qratia` y `zyro` (cristal). Añadir otro = añadir otro fichero.
|
|
11
|
+
- `themes/<tema>.fonts.css` — las fuentes del tema (un `@import url()`; va el primero).
|
|
12
|
+
- `ui/*` — componentes, uno por fichero (`orbital-ds/ui/button`, …): quien no usa gráficos
|
|
13
|
+
no necesita `recharts`, quien no usa el calendario no necesita `react-day-picker`.
|
|
14
|
+
- `contract.json` + `pnpm check:ds` — el contrato de tema y su comprobación.
|
|
15
|
+
|
|
16
|
+
## Uso en una app (Tailwind v4)
|
|
17
|
+
|
|
18
|
+
```css
|
|
19
|
+
@import 'orbital-ds/themes/qratia.fonts.css'; /* fuentes: lo primero */
|
|
20
|
+
@import 'tailwindcss';
|
|
21
|
+
@plugin '@tailwindcss/typography';
|
|
22
|
+
@import 'tw-animate-css';
|
|
23
|
+
@import 'orbital-ds/styles.css'; /* estructura */
|
|
24
|
+
@import 'orbital-ds/themes/qratia.css'; /* tema (después de la estructura) */
|
|
25
|
+
@source '../node_modules/orbital-ds/dist'; /* que Tailwind vea las clases del paquete */
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { Button } from 'orbital-ds/ui/button';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Cambiar de identidad = cambiar `qratia` por `zyro` en esas dos líneas de tema/fuentes.
|
|
33
|
+
Detalle del contrato, tokens y reglas de nombres en [`docs/CONTRACT.md`](docs/CONTRACT.md).
|
|
34
|
+
|
|
35
|
+
## Generar la marca desde un color (asistente)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
pnpm exec orbital-ds-init # asistente interactivo
|
|
39
|
+
pnpm exec orbital-ds-init --primary "#1D63D8" # sin preguntas (CI, scripts)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A partir del **color primario** (hex, `rgb()`, `hsl()` u `oklch()`) deriva en OKLCH los neutros teñidos
|
|
43
|
+
(papel, tinta, tarjeta, bordes, lateral), el texto sobre el primario, el enlace y el foco, en piel clara y
|
|
44
|
+
oscura, y **comprueba el contraste** (AA 4,5:1). Si un color no llega, lo ajusta lo mínimo y te lo dice.
|
|
45
|
+
Escribe solo un bloque delimitado en `brand.css`; con `--update` lo reescribe y lo tuyo de fuera no se
|
|
46
|
+
toca. Opciones: `--theme`, `--accent`, `--no-marker` (apaga el rotulador de `tactile`), `--dry-run`,
|
|
47
|
+
`--yes`, `--out`. Sin dependencias.
|
|
48
|
+
|
|
49
|
+
## Guía de cada tema
|
|
50
|
+
|
|
51
|
+
Cada tema lleva su documentación, que viaja con el paquete:
|
|
52
|
+
|
|
53
|
+
- `orbital-ds/themes/<tema>.guide.md` — guía de diseño: esencia, gesto firma, principios, referencias (qué se tomó de cada una), paleta, tipografía, formas, accesibilidad y estado de los valores (confirmados, aproximados o de diseño propio).
|
|
54
|
+
- `orbital-ds/themes/<tema>.meta.json` — la misma ficha legible por máquina.
|
|
55
|
+
|
|
56
|
+
`pnpm check:ds` falla si a un tema le falta alguna de las dos. Un tema nuevo debe nacer con ambas.
|
|
57
|
+
|
|
58
|
+
## Marca de una app
|
|
59
|
+
|
|
60
|
+
Lo propio de cada app (color principal, acento, radio, fuentes) va en **su** `src/brand.css`,
|
|
61
|
+
importado después del tema. Ninguna actualización de orbital-ds lo toca. `pnpm exec orbital-ds-init`
|
|
62
|
+
lo crea la primera vez. Detalle en [`docs/CONTRACT.md`](docs/CONTRACT.md).
|
|
63
|
+
|
|
64
|
+
## Regla
|
|
65
|
+
|
|
66
|
+
Ningún componente lleva valores propios (colores, medidas, sombras, z-index, duraciones):
|
|
67
|
+
pide un hueco a una variable. Para cambiar el aspecto se cambia el tema, no el componente.
|
|
68
|
+
Si un componente necesita un hueco nuevo, se AÑADE el token con un valor que deje el aspecto
|
|
69
|
+
como estaba. `pnpm check:ds` lo comprueba.
|
|
70
|
+
|
|
71
|
+
## Desarrollo
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pnpm install
|
|
75
|
+
pnpm verify # doctor + typecheck + build
|
|
76
|
+
pnpm check:ds # solo el contrato de tema y los valores fijos
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Peers obligatorios: `react`, `react-dom`, `@base-ui/react`, `tailwindcss`, `tailwind-merge`,
|
|
80
|
+
`class-variance-authority`, `clsx`, `lucide-react`, `tw-animate-css`.
|
|
81
|
+
Opcionales (solo si usas ese componente): `recharts`, `sonner`, `next-themes`, `cmdk`,
|
|
82
|
+
`date-fns` + `react-day-picker`, `embla-carousel-react`, `input-otp`,
|
|
83
|
+
`react-resizable-panels`, `@shadcn/react`.
|
|
84
|
+
|
|
85
|
+
## Licencia
|
|
86
|
+
|
|
87
|
+
MIT © Fractalia Systems
|
package/components.manifest.json
CHANGED
package/dist/themes/tactile.css
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* - Formas: 8 · 12 · 14 · 16. Sin sombras difusas: la profundidad la dan las aristas.
|
|
11
11
|
* - Titulares en Fraunces (serifa suave) y cuerpo en Figtree.
|
|
12
12
|
*
|
|
13
|
-
* Importar DESPUÉS de la estructura.
|
|
13
|
+
* Importar DESPUÉS de la estructura. Gesto: `--ds-key-outline`, `--ds-key-edge`, `--ds-marker`.
|
|
14
|
+
* Las antiguas `--zw-outline|edge|mark` siguen funcionando como alternativa, pero están obsoletas.
|
|
14
15
|
* Fuentes: `tactile.fonts.css`, lo primero del CSS de la app.
|
|
15
16
|
*/
|
|
16
17
|
|
|
@@ -138,10 +139,10 @@
|
|
|
138
139
|
--ds-icon-tile-radius: var(--ds-radius-xl);
|
|
139
140
|
--ds-icon-tile-fill: 0.2;
|
|
140
141
|
|
|
141
|
-
/*
|
|
142
|
-
--
|
|
143
|
-
--
|
|
144
|
-
--zw-mark
|
|
142
|
+
/* Gesto de tactile (variables públicas; apaga el rotulador con --ds-marker: transparent) */
|
|
143
|
+
--ds-key-outline: var(--zw-outline, #2a2233); /* contorno de las teclas (--zw-outline está obsoleta) */
|
|
144
|
+
--ds-key-edge: var(--zw-edge, rgba(42, 34, 51, 0.26)); /* arista de las teclas */
|
|
145
|
+
--ds-marker: var(--zw-mark, color-mix(in srgb, var(--primary) 62%, transparent)); /* rotulador bajo los títulos; transparent = apagado */
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
/* ── VARIABLES · PIEL OSCURA («noche») ───────────────────────────────────── */
|
|
@@ -205,9 +206,9 @@
|
|
|
205
206
|
--ds-shadow-overlay: 0 8px 0 rgba(0, 0, 0, 0.4);
|
|
206
207
|
--ds-shadow-popover: 0 6px 0 rgba(0, 0, 0, 0.4);
|
|
207
208
|
|
|
208
|
-
--
|
|
209
|
-
--
|
|
210
|
-
--zw-mark
|
|
209
|
+
--ds-key-outline: var(--zw-outline, rgba(244, 235, 218, 0.92));
|
|
210
|
+
--ds-key-edge: var(--zw-edge, rgba(244, 235, 218, 0.26));
|
|
211
|
+
--ds-marker: var(--zw-mark, color-mix(in srgb, var(--primary) 45%, transparent));
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
/* ── PERSONALIDAD ────────────────────────────────────────────────────────────
|
|
@@ -217,7 +218,7 @@
|
|
|
217
218
|
:root {
|
|
218
219
|
/* Teclas: contorno de tinta y arista inferior; al pulsar, se hunden */
|
|
219
220
|
[data-slot='button'] {
|
|
220
|
-
border: 2px solid var(--
|
|
221
|
+
border: 2px solid var(--ds-key-outline);
|
|
221
222
|
border-bottom-width: 5px;
|
|
222
223
|
border-radius: 14px;
|
|
223
224
|
font-weight: 700;
|
|
@@ -270,7 +271,7 @@
|
|
|
270
271
|
/* Superficies: filete suave y una arista inferior más marcada */
|
|
271
272
|
[data-slot='card'] {
|
|
272
273
|
border: 2px solid var(--border);
|
|
273
|
-
border-bottom: 5px solid var(--
|
|
274
|
+
border-bottom: 5px solid var(--ds-key-edge);
|
|
274
275
|
box-shadow: none;
|
|
275
276
|
}
|
|
276
277
|
|
|
@@ -303,12 +304,12 @@
|
|
|
303
304
|
[data-slot='tabs-trigger'][data-active] {
|
|
304
305
|
background: var(--primary);
|
|
305
306
|
color: var(--primary-foreground);
|
|
306
|
-
border-color: var(--
|
|
307
|
+
border-color: var(--ds-key-outline);
|
|
307
308
|
}
|
|
308
309
|
|
|
309
310
|
/* Interruptor, casilla y progreso, con tinta */
|
|
310
311
|
[data-slot='switch'] {
|
|
311
|
-
border: 2px solid var(--
|
|
312
|
+
border: 2px solid var(--ds-key-outline);
|
|
312
313
|
border-bottom-width: 4px;
|
|
313
314
|
border-radius: 12px;
|
|
314
315
|
}
|
|
@@ -317,10 +318,10 @@
|
|
|
317
318
|
}
|
|
318
319
|
[data-slot='switch-thumb'] {
|
|
319
320
|
border-radius: 7px;
|
|
320
|
-
background: var(--
|
|
321
|
+
background: var(--ds-key-outline);
|
|
321
322
|
}
|
|
322
323
|
[data-slot='checkbox'] {
|
|
323
|
-
border: 2px solid var(--
|
|
324
|
+
border: 2px solid var(--ds-key-outline);
|
|
324
325
|
border-radius: 8px;
|
|
325
326
|
}
|
|
326
327
|
[data-slot='checkbox'][data-checked] {
|
|
@@ -328,7 +329,7 @@
|
|
|
328
329
|
color: var(--primary-foreground);
|
|
329
330
|
}
|
|
330
331
|
[data-slot='progress'] {
|
|
331
|
-
border: 2px solid var(--
|
|
332
|
+
border: 2px solid var(--ds-key-outline);
|
|
332
333
|
border-radius: 8px;
|
|
333
334
|
background: var(--background);
|
|
334
335
|
}
|
|
@@ -339,19 +340,19 @@
|
|
|
339
340
|
/* Capas flotantes: contorno de tinta y arista inferior */
|
|
340
341
|
[data-slot='dialog-content'],
|
|
341
342
|
[data-slot='sheet-content'] {
|
|
342
|
-
border: 2px solid var(--
|
|
343
|
+
border: 2px solid var(--ds-key-outline);
|
|
343
344
|
border-bottom-width: 6px;
|
|
344
345
|
}
|
|
345
346
|
[data-slot='popover-content'],
|
|
346
347
|
[data-slot='dropdown-menu-content'],
|
|
347
348
|
[data-slot='select-content'] {
|
|
348
|
-
border: 2px solid var(--
|
|
349
|
+
border: 2px solid var(--ds-key-outline);
|
|
349
350
|
border-bottom-width: 4px;
|
|
350
351
|
}
|
|
351
352
|
|
|
352
353
|
/* Avisos con tinta */
|
|
353
354
|
[data-slot='alert'] {
|
|
354
|
-
border: 2px solid var(--
|
|
355
|
+
border: 2px solid var(--ds-key-outline);
|
|
355
356
|
border-radius: 14px;
|
|
356
357
|
}
|
|
357
358
|
|
|
@@ -369,7 +370,7 @@
|
|
|
369
370
|
|
|
370
371
|
/* Teclas del teclado */
|
|
371
372
|
[data-slot='kbd'] {
|
|
372
|
-
border: 2px solid var(--
|
|
373
|
+
border: 2px solid var(--ds-key-outline);
|
|
373
374
|
border-bottom-width: 4px;
|
|
374
375
|
border-radius: 8px;
|
|
375
376
|
background: var(--card);
|
|
@@ -383,7 +384,7 @@
|
|
|
383
384
|
header h1 {
|
|
384
385
|
width: fit-content;
|
|
385
386
|
max-width: 100%;
|
|
386
|
-
background: linear-gradient(transparent 56%, var(--
|
|
387
|
+
background: linear-gradient(transparent 56%, var(--ds-marker) 56%);
|
|
387
388
|
padding-inline: 0.14em;
|
|
388
389
|
margin-inline: -0.14em;
|
|
389
390
|
-webkit-box-decoration-break: clone;
|
|
@@ -400,7 +401,7 @@
|
|
|
400
401
|
/* Señalada: la tecla se asoma (papel de tarjeta, contorno de tinta y arista); pulsada, se hunde */
|
|
401
402
|
[data-sidebar='menu-button']:hover:not([data-active='true']) {
|
|
402
403
|
background: var(--card);
|
|
403
|
-
border-color: var(--
|
|
404
|
+
border-color: var(--ds-key-outline);
|
|
404
405
|
border-bottom-width: 4px;
|
|
405
406
|
}
|
|
406
407
|
[data-sidebar='menu-button']:active:not([data-active='true']) {
|
|
@@ -411,7 +412,7 @@
|
|
|
411
412
|
[data-sidebar='menu-button'][data-active='true'] {
|
|
412
413
|
background: var(--primary);
|
|
413
414
|
color: var(--primary-foreground);
|
|
414
|
-
border-color: var(--
|
|
415
|
+
border-color: var(--ds-key-outline);
|
|
415
416
|
border-bottom-width: 5px;
|
|
416
417
|
}
|
|
417
418
|
/* Tooltips: etiqueta de tinta con arista inferior, como una tecla invertida */
|
|
@@ -419,7 +420,7 @@
|
|
|
419
420
|
background: var(--foreground);
|
|
420
421
|
color: var(--background);
|
|
421
422
|
border-radius: 12px;
|
|
422
|
-
border-bottom: 4px solid var(--
|
|
423
|
+
border-bottom: 4px solid var(--ds-key-edge);
|
|
423
424
|
font-weight: 700;
|
|
424
425
|
}
|
|
425
426
|
/* Sin flecha: un rombo de tinta tapaba la primera letra de la etiqueta */
|
|
@@ -2,15 +2,81 @@
|
|
|
2
2
|
"name": "tactile",
|
|
3
3
|
"label": "Táctil",
|
|
4
4
|
"essence": "Interfaz de papel y tinta que se toca: teclas con arista y rotulador bajo los títulos.",
|
|
5
|
-
"signature": [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
"signature": [
|
|
6
|
+
"tecla con arista inferior que baja al pulsar",
|
|
7
|
+
"rotulador bajo los títulos"
|
|
8
|
+
],
|
|
9
|
+
"fonts": {
|
|
10
|
+
"display": "Fraunces",
|
|
11
|
+
"body": "Figtree",
|
|
12
|
+
"file": "tactile.fonts.css",
|
|
13
|
+
"distributable": true
|
|
14
|
+
},
|
|
15
|
+
"palette": {
|
|
16
|
+
"light": {
|
|
17
|
+
"background": "#F6F0E3",
|
|
18
|
+
"foreground": "#2A2233",
|
|
19
|
+
"primary": "#F2B33D"
|
|
20
|
+
},
|
|
21
|
+
"dark": {
|
|
22
|
+
"background": "#1D1824"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"radii": [
|
|
26
|
+
8,
|
|
27
|
+
12,
|
|
28
|
+
14,
|
|
29
|
+
16
|
|
30
|
+
],
|
|
9
31
|
"references": [
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
|
|
32
|
+
{
|
|
33
|
+
"name": "Duolingo",
|
|
34
|
+
"tookFromIt": "arista inferior que baja al pulsar",
|
|
35
|
+
"source": "fuentes de terceros"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "Endorsal",
|
|
39
|
+
"tookFromIt": "rotulador bajo títulos, iconos duotono",
|
|
40
|
+
"source": "análisis en navegador"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "SerpWatch",
|
|
44
|
+
"tookFromIt": "lienzo no blanco",
|
|
45
|
+
"source": "análisis en navegador"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"status": {
|
|
49
|
+
"verified": [],
|
|
50
|
+
"approximate": [],
|
|
51
|
+
"designed": [
|
|
52
|
+
"todos los valores"
|
|
53
|
+
],
|
|
54
|
+
"pending": [
|
|
55
|
+
"piel noche con captura real",
|
|
56
|
+
"variante índigo"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
"guide": "tactile.guide.md",
|
|
60
|
+
"gestures": [
|
|
61
|
+
{
|
|
62
|
+
"id": "marker",
|
|
63
|
+
"token": "--ds-marker",
|
|
64
|
+
"off": "transparent",
|
|
65
|
+
"about": "rotulador bajo los títulos"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "key-outline",
|
|
69
|
+
"token": "--ds-key-outline",
|
|
70
|
+
"about": "contorno de las teclas"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "key-edge",
|
|
74
|
+
"token": "--ds-key-edge",
|
|
75
|
+
"about": "arista inferior de las teclas"
|
|
76
|
+
}
|
|
13
77
|
],
|
|
14
|
-
"
|
|
15
|
-
|
|
78
|
+
"a11y": {
|
|
79
|
+
"level": "AA",
|
|
80
|
+
"exceptions": []
|
|
81
|
+
}
|
|
16
82
|
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* - Formas: 8 · 12 · 14 · 16. Sin sombras difusas: la profundidad la dan las aristas.
|
|
11
11
|
* - Titulares en Fraunces (serifa suave) y cuerpo en Figtree.
|
|
12
12
|
*
|
|
13
|
-
* Importar DESPUÉS de la estructura.
|
|
13
|
+
* Importar DESPUÉS de la estructura. Gesto: `--ds-key-outline`, `--ds-key-edge`, `--ds-marker`.
|
|
14
|
+
* Las antiguas `--zw-outline|edge|mark` siguen funcionando como alternativa, pero están obsoletas.
|
|
14
15
|
* Fuentes: `tactile.fonts.css`, lo primero del CSS de la app.
|
|
15
16
|
*/
|
|
16
17
|
|
|
@@ -138,10 +139,10 @@
|
|
|
138
139
|
--ds-icon-tile-radius: var(--ds-radius-xl);
|
|
139
140
|
--ds-icon-tile-fill: 0.2;
|
|
140
141
|
|
|
141
|
-
/*
|
|
142
|
-
--
|
|
143
|
-
--
|
|
144
|
-
--zw-mark
|
|
142
|
+
/* Gesto de tactile (variables públicas; apaga el rotulador con --ds-marker: transparent) */
|
|
143
|
+
--ds-key-outline: var(--zw-outline, #2a2233); /* contorno de las teclas (--zw-outline está obsoleta) */
|
|
144
|
+
--ds-key-edge: var(--zw-edge, rgba(42, 34, 51, 0.26)); /* arista de las teclas */
|
|
145
|
+
--ds-marker: var(--zw-mark, color-mix(in srgb, var(--primary) 62%, transparent)); /* rotulador bajo los títulos; transparent = apagado */
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
/* ── VARIABLES · PIEL OSCURA («noche») ───────────────────────────────────── */
|
|
@@ -205,9 +206,9 @@
|
|
|
205
206
|
--ds-shadow-overlay: 0 8px 0 rgba(0, 0, 0, 0.4);
|
|
206
207
|
--ds-shadow-popover: 0 6px 0 rgba(0, 0, 0, 0.4);
|
|
207
208
|
|
|
208
|
-
--
|
|
209
|
-
--
|
|
210
|
-
--zw-mark
|
|
209
|
+
--ds-key-outline: var(--zw-outline, rgba(244, 235, 218, 0.92));
|
|
210
|
+
--ds-key-edge: var(--zw-edge, rgba(244, 235, 218, 0.26));
|
|
211
|
+
--ds-marker: var(--zw-mark, color-mix(in srgb, var(--primary) 45%, transparent));
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
/* ── PERSONALIDAD ────────────────────────────────────────────────────────────
|
|
@@ -217,7 +218,7 @@
|
|
|
217
218
|
:root:where([data-theme='tactile']) {
|
|
218
219
|
/* Teclas: contorno de tinta y arista inferior; al pulsar, se hunden */
|
|
219
220
|
[data-slot='button'] {
|
|
220
|
-
border: 2px solid var(--
|
|
221
|
+
border: 2px solid var(--ds-key-outline);
|
|
221
222
|
border-bottom-width: 5px;
|
|
222
223
|
border-radius: 14px;
|
|
223
224
|
font-weight: 700;
|
|
@@ -270,7 +271,7 @@
|
|
|
270
271
|
/* Superficies: filete suave y una arista inferior más marcada */
|
|
271
272
|
[data-slot='card'] {
|
|
272
273
|
border: 2px solid var(--border);
|
|
273
|
-
border-bottom: 5px solid var(--
|
|
274
|
+
border-bottom: 5px solid var(--ds-key-edge);
|
|
274
275
|
box-shadow: none;
|
|
275
276
|
}
|
|
276
277
|
|
|
@@ -303,12 +304,12 @@
|
|
|
303
304
|
[data-slot='tabs-trigger'][data-active] {
|
|
304
305
|
background: var(--primary);
|
|
305
306
|
color: var(--primary-foreground);
|
|
306
|
-
border-color: var(--
|
|
307
|
+
border-color: var(--ds-key-outline);
|
|
307
308
|
}
|
|
308
309
|
|
|
309
310
|
/* Interruptor, casilla y progreso, con tinta */
|
|
310
311
|
[data-slot='switch'] {
|
|
311
|
-
border: 2px solid var(--
|
|
312
|
+
border: 2px solid var(--ds-key-outline);
|
|
312
313
|
border-bottom-width: 4px;
|
|
313
314
|
border-radius: 12px;
|
|
314
315
|
}
|
|
@@ -317,10 +318,10 @@
|
|
|
317
318
|
}
|
|
318
319
|
[data-slot='switch-thumb'] {
|
|
319
320
|
border-radius: 7px;
|
|
320
|
-
background: var(--
|
|
321
|
+
background: var(--ds-key-outline);
|
|
321
322
|
}
|
|
322
323
|
[data-slot='checkbox'] {
|
|
323
|
-
border: 2px solid var(--
|
|
324
|
+
border: 2px solid var(--ds-key-outline);
|
|
324
325
|
border-radius: 8px;
|
|
325
326
|
}
|
|
326
327
|
[data-slot='checkbox'][data-checked] {
|
|
@@ -328,7 +329,7 @@
|
|
|
328
329
|
color: var(--primary-foreground);
|
|
329
330
|
}
|
|
330
331
|
[data-slot='progress'] {
|
|
331
|
-
border: 2px solid var(--
|
|
332
|
+
border: 2px solid var(--ds-key-outline);
|
|
332
333
|
border-radius: 8px;
|
|
333
334
|
background: var(--background);
|
|
334
335
|
}
|
|
@@ -339,19 +340,19 @@
|
|
|
339
340
|
/* Capas flotantes: contorno de tinta y arista inferior */
|
|
340
341
|
[data-slot='dialog-content'],
|
|
341
342
|
[data-slot='sheet-content'] {
|
|
342
|
-
border: 2px solid var(--
|
|
343
|
+
border: 2px solid var(--ds-key-outline);
|
|
343
344
|
border-bottom-width: 6px;
|
|
344
345
|
}
|
|
345
346
|
[data-slot='popover-content'],
|
|
346
347
|
[data-slot='dropdown-menu-content'],
|
|
347
348
|
[data-slot='select-content'] {
|
|
348
|
-
border: 2px solid var(--
|
|
349
|
+
border: 2px solid var(--ds-key-outline);
|
|
349
350
|
border-bottom-width: 4px;
|
|
350
351
|
}
|
|
351
352
|
|
|
352
353
|
/* Avisos con tinta */
|
|
353
354
|
[data-slot='alert'] {
|
|
354
|
-
border: 2px solid var(--
|
|
355
|
+
border: 2px solid var(--ds-key-outline);
|
|
355
356
|
border-radius: 14px;
|
|
356
357
|
}
|
|
357
358
|
|
|
@@ -369,7 +370,7 @@
|
|
|
369
370
|
|
|
370
371
|
/* Teclas del teclado */
|
|
371
372
|
[data-slot='kbd'] {
|
|
372
|
-
border: 2px solid var(--
|
|
373
|
+
border: 2px solid var(--ds-key-outline);
|
|
373
374
|
border-bottom-width: 4px;
|
|
374
375
|
border-radius: 8px;
|
|
375
376
|
background: var(--card);
|
|
@@ -383,7 +384,7 @@
|
|
|
383
384
|
header h1 {
|
|
384
385
|
width: fit-content;
|
|
385
386
|
max-width: 100%;
|
|
386
|
-
background: linear-gradient(transparent 56%, var(--
|
|
387
|
+
background: linear-gradient(transparent 56%, var(--ds-marker) 56%);
|
|
387
388
|
padding-inline: 0.14em;
|
|
388
389
|
margin-inline: -0.14em;
|
|
389
390
|
-webkit-box-decoration-break: clone;
|
|
@@ -400,7 +401,7 @@
|
|
|
400
401
|
/* Señalada: la tecla se asoma (papel de tarjeta, contorno de tinta y arista); pulsada, se hunde */
|
|
401
402
|
[data-sidebar='menu-button']:hover:not([data-active='true']) {
|
|
402
403
|
background: var(--card);
|
|
403
|
-
border-color: var(--
|
|
404
|
+
border-color: var(--ds-key-outline);
|
|
404
405
|
border-bottom-width: 4px;
|
|
405
406
|
}
|
|
406
407
|
[data-sidebar='menu-button']:active:not([data-active='true']) {
|
|
@@ -411,7 +412,7 @@
|
|
|
411
412
|
[data-sidebar='menu-button'][data-active='true'] {
|
|
412
413
|
background: var(--primary);
|
|
413
414
|
color: var(--primary-foreground);
|
|
414
|
-
border-color: var(--
|
|
415
|
+
border-color: var(--ds-key-outline);
|
|
415
416
|
border-bottom-width: 5px;
|
|
416
417
|
}
|
|
417
418
|
/* Tooltips: etiqueta de tinta con arista inferior, como una tecla invertida */
|
|
@@ -419,7 +420,7 @@
|
|
|
419
420
|
background: var(--foreground);
|
|
420
421
|
color: var(--background);
|
|
421
422
|
border-radius: 12px;
|
|
422
|
-
border-bottom: 4px solid var(--
|
|
423
|
+
border-bottom: 4px solid var(--ds-key-edge);
|
|
423
424
|
font-weight: 700;
|
|
424
425
|
}
|
|
425
426
|
/* Sin flecha: un rombo de tinta tapaba la primera letra de la etiqueta */
|
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",
|
|
@@ -49,6 +49,14 @@
|
|
|
49
49
|
"./contract.json": "./contract.json",
|
|
50
50
|
"./components.manifest.json": "./components.manifest.json"
|
|
51
51
|
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup && node scripts/build-css.mjs",
|
|
54
|
+
"dev": "tsup --watch --onSuccess \"node scripts/copy-css.mjs\"",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"verify": "pnpm check:ds && pnpm typecheck && pnpm build",
|
|
57
|
+
"manifest": "node scripts/manifest.mjs",
|
|
58
|
+
"check:ds": "node scripts/doctor.mjs"
|
|
59
|
+
},
|
|
52
60
|
"peerDependencies": {
|
|
53
61
|
"@base-ui/react": ">=1.9",
|
|
54
62
|
"class-variance-authority": ">=0.7",
|
|
@@ -130,13 +138,5 @@
|
|
|
130
138
|
"bin": {
|
|
131
139
|
"orbital-ds-doctor": "./scripts/doctor.mjs",
|
|
132
140
|
"orbital-ds-init": "./scripts/init.mjs"
|
|
133
|
-
},
|
|
134
|
-
"scripts": {
|
|
135
|
-
"build": "tsup && node scripts/build-css.mjs",
|
|
136
|
-
"dev": "tsup --watch --onSuccess \"node scripts/copy-css.mjs\"",
|
|
137
|
-
"typecheck": "tsc --noEmit",
|
|
138
|
-
"verify": "pnpm check:ds && pnpm typecheck && pnpm build",
|
|
139
|
-
"manifest": "node scripts/manifest.mjs",
|
|
140
|
-
"check:ds": "node scripts/doctor.mjs"
|
|
141
141
|
}
|
|
142
|
-
}
|
|
142
|
+
}
|
package/scripts/doctor.mjs
CHANGED
|
File without changes
|
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(); }
|