@higinio/design-mcp 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/assets/celeste/design-doc.md +130 -15
- package/assets/celeste/framework.css +348 -0
- package/assets/celeste/styleguide.html +1292 -55
- package/assets/celeste/tailwind.css +4 -0
- package/assets/nocturna/design-doc.md +176 -37
- package/assets/nocturna/framework.css +329 -10
- package/assets/nocturna/styleguide.html +1213 -52
- package/assets/nocturna/tailwind.css +8 -2
- package/assets/nocturna/tokens.css +14 -8
- package/assets/nocturna/tokens.json +26 -1
- package/dist/slop.js +3 -3
- package/package.json +30 -8
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ VS Code usa la clave `servers`, no `mcpServers`:
|
|
|
47
47
|
|
|
48
48
|
En los tres casos, reemplaza `ABS/RUTA/mcp/dist/index.js` por la ruta absoluta real al archivo compilado (por ejemplo `C:/Users/higinio/Documents/Proyectos/Diseño/mcp/dist/index.js`), usando `/` en vez de `\` para evitar problemas de escape en JSON.
|
|
49
49
|
|
|
50
|
-
**Nota:**
|
|
50
|
+
**Nota:** el paquete ya está publicado en npm como [`@higinio/design-mcp`](https://www.npmjs.com/package/@higinio/design-mcp), así que la config puede usar `npx` directamente y deja de depender de una ruta local:
|
|
51
51
|
|
|
52
52
|
```json
|
|
53
53
|
{ "command": "npx", "args": ["-y", "@higinio/design-mcp"] }
|
|
@@ -107,12 +107,15 @@ Para desplegarlo en **Dokploy** y conectarlo desde Claude Code (local o remoto),
|
|
|
107
107
|
|
|
108
108
|
## CLI / pre-commit
|
|
109
109
|
|
|
110
|
-
Los cuatro validadores puros (`validate_code`, `validate_motion`, `check_slop`, `checklist_preflight`) también se pueden correr sobre archivos locales sin pasar por MCP, con el binario `design-lint
|
|
110
|
+
Los cuatro validadores puros (`validate_code`, `validate_motion`, `check_slop`, `checklist_preflight`) también se pueden correr sobre archivos locales sin pasar por MCP, con el binario `design-lint`. Como el paquete está publicado en npm, en otro proyecto lo ejecutas con `npx` sin instalar nada; dentro de este repo apuntas a `dist/cli.js`:
|
|
111
111
|
|
|
112
112
|
```
|
|
113
113
|
design-lint --system celeste|nocturna [--lang css|html|js] [--json] <archivos o globs...>
|
|
114
114
|
|
|
115
|
-
#
|
|
115
|
+
# desde cualquier proyecto (paquete publicado)
|
|
116
|
+
npx @higinio/design-mcp design-lint --system celeste "src/**/*.css"
|
|
117
|
+
|
|
118
|
+
# dentro de este repo (binario compilado)
|
|
116
119
|
node dist/cli.js --system celeste src/styles/*.css
|
|
117
120
|
node dist/cli.js --system nocturna --json "src/**/*.html"
|
|
118
121
|
node dist/cli.js --system celeste --lang css tokens.txt # fuerza el lenguaje
|
|
@@ -306,9 +306,10 @@ Export propio (extraído del estado loading de Button). SVG 16px default: círcu
|
|
|
306
306
|
- **Disabled:** fondo `neutral-100`, texto `--text-disabled`, cursor `not-allowed`.
|
|
307
307
|
- **Texto de ayuda:** `text-sm` `--text-muted` debajo del input.
|
|
308
308
|
- Con icono leading: padding-left 40px, icono 18px `--text-disabled`.
|
|
309
|
-
- **`inputmode` según el dato** (`numeric`, `email`, `tel`, `decimal`) para el teclado correcto en móvil; `spellcheck="false"` en emails, códigos y usernames.
|
|
309
|
+
- **`inputmode` según el dato** (`numeric`, `email`, `tel`, `decimal`) para el teclado correcto en móvil; `spellcheck="false"` en emails, códigos y usernames; **`autocomplete` apropiado** (`email`, `current-password`, `one-time-code`, `name`…) para el autorrelleno del navegador.
|
|
310
310
|
- **Nunca bloquear el pegado** (`onpaste`): rompe gestores de contraseñas y códigos de verificación.
|
|
311
311
|
- **Al enviar con errores,** mover el foco al primer campo inválido (no solo pintarlo de rojo).
|
|
312
|
+
- **Campo obligatorio (`.field--required`):** el `.field__label` muestra un asterisco decorativo vía `::after` en color `--danger-500`, y el `<input>` lleva `required` + `aria-required="true"` como señal accesible real (el asterisco es cromo, no el mecanismo). No marques campos "opcionales" con asterisco: el asterisco significa obligatorio.
|
|
312
313
|
|
|
313
314
|
### 10.5 Textarea
|
|
314
315
|
|
|
@@ -326,7 +327,7 @@ Mismos estados que input. Min-height 96px, `resize: vertical`, padding 12px 14px
|
|
|
326
327
|
Pill compacta de **2–4 opciones mutuamente excluyentes** (switches de vista, filtros rápidos). Para >4 opciones o labels largos, usar `Select`.
|
|
327
328
|
|
|
328
329
|
- Contenedor: fondo `--surface-alt`, radius `md`, padding 3px. Altura 40px (md) / 32px (sm); segmentos = altura − 6px, padding horizontal 16px.
|
|
329
|
-
-
|
|
330
|
+
- Opción activa: fondo blanco (`--surface`), radius `calc(radius-md - 3px)`, `shadow-xs`; **transición de fondo y color 200ms `--ease-out`** (swap de color, nunca animando `left`/`width`).
|
|
330
331
|
- Segmento activo: `--text-primary` semibold; inactivo: `--text-secondary` medium; transición de color 150ms. Texto `text-sm`.
|
|
331
332
|
|
|
332
333
|
### 10.8 Slider
|
|
@@ -344,6 +345,7 @@ Rango de valor único.
|
|
|
344
345
|
- Checked: fondo `--accent`, check blanco animado (stroke-dashoffset, 200ms).
|
|
345
346
|
- Hover: borde `primary-400`. Focus: ring como inputs.
|
|
346
347
|
- Label clicable, gap 10px, `text-base`. `RadioGroup` agrupa con `role="radiogroup"`.
|
|
348
|
+
- **Indeterminate (`input:indeterminate`):** fondo `--accent` con un guion horizontal (10×2px, `--text-on-primary`) en vez del check. Es el estado de un checkbox "seleccionar todo" cuando la selección es parcial. Es una **propiedad del DOM** (`checkbox.indeterminate = true`), no un atributo HTML: se fija por JS y se sincroniza con las filas seleccionadas (ver §10.21).
|
|
347
349
|
|
|
348
350
|
### 10.10 Switch
|
|
349
351
|
|
|
@@ -409,9 +411,17 @@ Progreso **determinado** (para carga indeterminada usar `Skeleton`, no Progress)
|
|
|
409
411
|
- Panel: fondo blanco, radius `lg`, `shadow-xl`, max-width 480px (confirm) / 640px (formulario), padding 24px.
|
|
410
412
|
- Entrada: fade + scale(0.96→1), 250ms. Salida: 180ms.
|
|
411
413
|
- Header con título `text-xl` + IconButton cerrar; footer con acciones alineadas a la derecha (primaria al extremo).
|
|
412
|
-
- Cerrar con Esc y click en overlay (excepto flujos destructivos).
|
|
414
|
+
- Cerrar con Esc y click en overlay (excepto flujos destructivos: el overlay no cierra para evitar descartes accidentales; Esc y los botones sí).
|
|
413
415
|
- `overscroll-behavior: contain` en el panel scrolleable — al llegar al fondo, el scroll no se encadena a la página de atrás.
|
|
414
416
|
|
|
417
|
+
**Accesibilidad y foco (obligatorio):**
|
|
418
|
+
|
|
419
|
+
- **Roles y etiquetado:** el contenedor lleva `role="dialog"`, `aria-modal="true"` y `aria-labelledby` apuntando al id del título (`.modal__title`). Si hay texto descriptivo, `aria-describedby` con el id de ese párrafo.
|
|
420
|
+
- **Foco al abrir:** el foco entra al panel (`.modal__panel` con `tabindex="-1"`) o al primer control enfocable del modal.
|
|
421
|
+
- **Focus-trap:** mientras está abierto, Tab y Shift+Tab ciclan solo entre los elementos enfocables del modal; el foco nunca escapa al contenido de atrás (que además está inerte por el overlay).
|
|
422
|
+
- **Retorno de foco:** al cerrar (Esc, botón ×, Cancelar o click en overlay), el foco vuelve al elemento que abrió el modal.
|
|
423
|
+
- El panel se marca con `tabindex="-1"` para poder recibir foco programático sin quedar en el orden de tabulación.
|
|
424
|
+
|
|
415
425
|
### 10.20 Menu
|
|
416
426
|
|
|
417
427
|
Lista de acciones desplegable anclada a un trigger (acciones de fila, botones "más", menú de cuenta).
|
|
@@ -424,11 +434,20 @@ Lista de acciones desplegable anclada a un trigger (acciones de fila, botones "m
|
|
|
424
434
|
|
|
425
435
|
### 10.21 Tabla
|
|
426
436
|
|
|
427
|
-
- Header: fondo `neutral-50`, `text-xs` uppercase `--tracking-label`, `--text-muted`, borde inferior `--border`.
|
|
437
|
+
- Header: fondo `neutral-50`, `text-xs` uppercase `--tracking-label`, `--text-muted`, borde inferior `--border`. Cada `<th>` lleva `scope="col"`.
|
|
428
438
|
- Filas: altura 52px, borde inferior `neutral-100`, hover fondo `neutral-50`.
|
|
429
439
|
- Celdas numéricas alineadas a la derecha con `tabular-nums`.
|
|
430
440
|
- Fila seleccionada: fondo `--accent-soft`.
|
|
431
441
|
|
|
442
|
+
**Variantes (clases del framework):**
|
|
443
|
+
|
|
444
|
+
- **`.table--zebra`** — filas alternas (`tr:nth-child(even)`) con fondo `--surface-alt`; el hover sigue ganando.
|
|
445
|
+
- **`.table--sticky`** — `thead th` con `position: sticky; top: 0` (fondo opaco del header + `z-index`). El contenedor scrolleable acota la altura para que el header quede fijo.
|
|
446
|
+
- **Header ordenable** — dentro del `<th>` un `<button class="th-sort">` (o `.th-sort--num` para columnas numéricas) con el texto + `.sort-icon`. El `<th>` declara `aria-sort="none|ascending|descending"`; el JS alterna el valor y reordena el `tbody`. El icono se atenúa en `none` y toma `--accent` (rotado 180° en `descending`) al ordenar.
|
|
447
|
+
- **Columna de selección** — `.col-select` (44px, centrada) con un `.checkbox` por fila (`aria-label` describiendo la fila) y un checkbox "seleccionar todo" en el `<th>`. La fila marcada lleva `.row--selected` (fondo `--accent-soft`); el checkbox del header pasa a `indeterminate` cuando la selección es parcial (ver §10.9).
|
|
448
|
+
|
|
449
|
+
**Accesibilidad:** `scope="col"` en headers; `aria-sort` en la columna ordenable; el `.sort-icon` es `aria-hidden` (el estado real lo da `aria-sort` + el texto del header); cada checkbox de fila con `aria-label` propio.
|
|
450
|
+
|
|
432
451
|
### 10.22 Stat
|
|
433
452
|
|
|
434
453
|
Tile de KPI único (componer dentro de `Card` para métricas de dashboard).
|
|
@@ -462,9 +481,100 @@ Navegador de páginas 1-indexado, bajo una `Table`.
|
|
|
462
481
|
|
|
463
482
|
- **Card:** fondo `--surface`, borde `1px --border`, radius `lg`, padding 24px (16px en móvil), `shadow-sm`.
|
|
464
483
|
- **Card interactiva:** cursor pointer; hover eleva — `translateY(-2px)` + `shadow-md`; transición de transform/sombra 200ms (`--duration-base`) ease-out. Sin glow: en light-first la elevación es sombra + desplazamiento, nunca brillo.
|
|
465
|
-
- **Link:** color `--link` (primary-700, AA
|
|
484
|
+
- **Link:** color `--link` (primary-700, AA 5.94:1); subrayado animado 1px `currentColor` que crece de 0%→100% del ancho desde la izquierda (`background-size`, 200ms ease-out). Externo: `target="_blank" rel="noopener noreferrer"` + icono flecha 12px, gap 4px.
|
|
466
485
|
- **Code (inline):** IBM Plex Mono `0.9em`, texto `--text-primary`, fondo `--surface-alt`, borde `1px --border`, radius `sm`, padding `1px 6px`.
|
|
467
486
|
|
|
487
|
+
### 10.27 AvatarGroup
|
|
488
|
+
|
|
489
|
+
Stack de avatares solapados para representar participantes de un proyecto, miembros de un equipo o asignados de una tarea. Complementa al `Avatar` (§10.12).
|
|
490
|
+
|
|
491
|
+
- **`.avatar-group`** — contenedor `inline-flex`; cada `.avatar` (y el `.avatar-group__more`) se solapa con `margin-left: -8px`.
|
|
492
|
+
- **Anillo de separación** — cada avatar del grupo lleva un borde `2px solid var(--surface)` (radius full) que lo recorta del de atrás; en dark hereda el `--surface` oscuro. El orden de pintado deja el último encima.
|
|
493
|
+
- **`.avatar-group__more`** — chip circular del mismo tamaño (36px) para el `+N` de overflow: fondo `--accent-soft-strong`, texto `--text-on-primary-solid` semibold `text-xs`, mismo anillo y solapamiento. Va al final del grupo.
|
|
494
|
+
- Para más de ~5 avatares, corta la lista y muestra el resto como `+N` (evita filas larguísimas).
|
|
495
|
+
|
|
496
|
+
### 10.28 Combobox / Autocomplete y Búsqueda
|
|
497
|
+
|
|
498
|
+
Campo de texto con lista de opciones filtrable (Combobox) y su primo especializado para búsqueda (Search). Para una lista fija sin filtrado, usar `Select` (§10.6); para texto libre sin sugerencias, `Input` (§10.4).
|
|
499
|
+
|
|
500
|
+
**Combobox (`.combobox`):**
|
|
501
|
+
|
|
502
|
+
- **`.combobox__input`** — input `role="combobox"` con `aria-expanded`, `aria-controls` (id de la lista), `aria-autocomplete="list"` y `aria-activedescendant` apuntando a la opción activa. Mismo look que `Input` (borde `--border-strong`, focus `--accent` + `--ring`).
|
|
503
|
+
- **`.combobox__list`** — dropdown `role="listbox"` con id; fondo `--surface`, `shadow-lg`, radius `md`, max-height 264px con scroll. Entra con fade + translateY(-4px→0), `--duration-base` `--ease-out`.
|
|
504
|
+
- **`.combobox__option`** — `role="option"` + `aria-selected`. `.is-active` = resaltado de teclado (fondo `--accent-soft`); `[aria-selected="true"]` = elegida (texto `--link`, medium, check `--accent` a la derecha — el estado no se comunica solo por color).
|
|
505
|
+
- **Teclado:** ↑/↓ mueven la opción activa **sin mover el foco** (que se queda en el input); Enter selecciona, Esc cierra, Home/End saltan a los extremos. La opción activa se señala con `aria-activedescendant`, no con foco real.
|
|
506
|
+
|
|
507
|
+
**Búsqueda (`.search`):**
|
|
508
|
+
|
|
509
|
+
- **`.search__icon`** — lupa 18px a la izquierda, `aria-hidden` (decorativa).
|
|
510
|
+
- **`.search__input`** — input con padding lateral 40px para dejar sitio al icono y a la ×.
|
|
511
|
+
- **`.search__clear`** — botón × que aparece solo cuando hay texto (`:not(:placeholder-shown)`), `aria-label="Limpiar"`; al pulsarlo vacía el campo y le devuelve el foco.
|
|
512
|
+
|
|
513
|
+
### 10.29 Drawer / Sheet
|
|
514
|
+
|
|
515
|
+
Panel que entra deslizando desde un borde para tareas contextuales (filtros, detalle, formulario lateral) sin sacar al usuario de la vista. Modal en comportamiento, lateral en forma.
|
|
516
|
+
|
|
517
|
+
- **`.drawer`** — contenedor/estado a pantalla completa (`position: fixed`), oculto hasta `.is-open`.
|
|
518
|
+
- **`.drawer__overlay`** — capa `--overlay` que atenúa el fondo; cierra al hacer click.
|
|
519
|
+
- **`.drawer__panel`** — panel `--surface` con `shadow-xl`; `overscroll-behavior: contain` para que el scroll no se encadene a la página. El contenedor lleva `role="dialog"` + `aria-modal="true"` + `aria-labelledby`.
|
|
520
|
+
- **Variantes:** `.drawer--right` (default), `.drawer--left`, `.drawer--bottom` (esta última full-width, con radius superior `lg`).
|
|
521
|
+
- **Foco (igual que Modal, §10.19):** al abrir, el foco entra al panel (`tabindex="-1"`), queda atrapado (Tab/Shift+Tab ciclan) y vuelve al disparador al cerrar. Esc y click en overlay cierran.
|
|
522
|
+
- **Motion:** entra con `translateX(100%)→0` (o `translateY` en la variante bottom) + fade del overlay, `--duration-slow` `--ease-out`; la salida es más corta (`--duration-base`, `--ease-in`). Solo `transform`/`opacity`, bajo el techo `--duration-page`.
|
|
523
|
+
|
|
524
|
+
### 10.30 Popover
|
|
525
|
+
|
|
526
|
+
Contenedor flotante con **contenido interactivo** anclado a un disparador. A diferencia del Tooltip (§10.15, solo texto y aparición en hover), el Popover se abre por **click** y puede llevar enlaces, botones o campos.
|
|
527
|
+
|
|
528
|
+
- **`.popover`** — contenedor relativo (`.is-open` lo abre).
|
|
529
|
+
- **`.popover__trigger`** — disparador con `aria-haspopup="dialog"` + `aria-expanded`.
|
|
530
|
+
- **`.popover__panel`** — `role="dialog"` + `aria-label`/`aria-labelledby`; fondo `--surface`, borde `--border`, `shadow-lg`, radius `md`. El foco entra al panel al abrir; cierra con Esc y click fuera.
|
|
531
|
+
- **Motion:** aparece desde el `transform-origin` del trigger (fade + `scale(0.96→1)`), `--duration-base` `--ease-out` — sin rebote (es un utilitario). Variante `.popover--right` ancla el panel a la derecha.
|
|
532
|
+
|
|
533
|
+
### 10.31 ButtonGroup
|
|
534
|
+
|
|
535
|
+
Botones adyacentes con bordes compartidos para acciones relacionadas (barra de fechas, alineación, cambio de vista). Para un único-seleccionado mutuamente excluyente, usar `SegmentedControl` (§10.7).
|
|
536
|
+
|
|
537
|
+
- **`.btn-group`** — contenedor `inline-flex` con `role="group"` + `aria-label`. Usa los `.btn`/`.btn--secondary` existentes dentro.
|
|
538
|
+
- **Bordes:** radius solo en los extremos (el primer hijo redondea la izquierda, el último la derecha); los bordes internos se colapsan con `margin-left: -1px` para no duplicar la línea. En hover/focus el botón sube en `z-index` para que su borde y su ring queden por encima del vecino.
|
|
539
|
+
|
|
540
|
+
### 10.32 DatePicker
|
|
541
|
+
|
|
542
|
+
Campo de fecha con un calendario en popover. Reusa el patrón visual del `Popover` (§10.30, panel `--surface` + `shadow-lg`, aparición desde el `transform-origin` del disparador) y la navegación por teclado del `Combobox` (§10.28).
|
|
543
|
+
|
|
544
|
+
- **`.datepicker`** — contenedor relativo (`.is-open` lo abre). **`.datepicker__input`** con el look de `Input` (borde `--border-strong`, focus `--accent` + `--ring`), `readonly` + `aria-haspopup="dialog"` + `aria-expanded` + `aria-controls`; un `.datepicker__icon` de calendario 18px a la derecha (decorativo, `aria-hidden`).
|
|
545
|
+
- **`.datepicker__calendar`** — panel flotante `role="dialog"` con `aria-label`, mismo patrón que `.popover__panel`. Variante `.datepicker--right` ancla a la derecha.
|
|
546
|
+
- **`.calendar`** — dentro del panel: `.calendar__header` (mes/año en `.calendar__title` + botones prev/next con `#i-chevron-left/right`), `.calendar__weekdays` (7 encabezados `role="columnheader"` con `aria-label` completo: "lunes"…), `.calendar__grid` (`role="grid"`), y `.calendar__day` (botón `role="gridcell"`, `aria-label` de la fecha completa vía `Intl.DateTimeFormat` y `aria-selected`). Semana empieza en lunes.
|
|
547
|
+
- **Estados de día:** `.calendar__day--today` (semibold + anillo interior `--border-strong`), `.calendar__day--selected` (fondo `--accent`, texto `--text-on-primary`), `.calendar__day--outside` (días de otro mes, `--text-disabled`), `[disabled]` (no seleccionable). El hover usa `--accent-soft`.
|
|
548
|
+
- **Teclado y foco (como Popover):** el input abre por click (o Enter/Espacio/↓); al abrir, el foco entra al calendario (día enfocado). Dentro de la rejilla las **flechas** mueven el foco entre días (roving `tabindex`, cambiando de mes al cruzar el borde), Home/End saltan a los extremos de la semana, PageUp/PageDown cambian de mes, Enter/Espacio selecciona; **Esc** cierra y devuelve el foco al input. La fecha se formatea con `Intl` (`es-MX`), nunca a mano.
|
|
549
|
+
- **Motion:** el panel aparece con fade + `scale(0.96→1)`, `--duration-base` `--ease-out`, bajo el techo `--duration-page`. Solo `transform`/`opacity`.
|
|
550
|
+
|
|
551
|
+
### 10.33 FileUpload
|
|
552
|
+
|
|
553
|
+
Zona de arrastre (dropzone) + lista de archivos.
|
|
554
|
+
|
|
555
|
+
- **`.file-upload`** — dropzone: borde `2px dashed --border-strong`, radius `lg`, fondo `--surface`. Estado **`.file-upload.is-dragover`** (borde `--accent` + fondo `--accent-soft`) mientras se arrastra un archivo encima.
|
|
556
|
+
- **`.file-upload__input`** — el `<input type="file">` real, oculto de forma accesible (no `display:none`), con **label asociado** (`.file-upload__label for=…`) y `aria-describedby` que apunta al hint de formatos/tamaño. La zona describe la acción ("Arrastra archivos o elige").
|
|
557
|
+
- **`.file-upload__label`** — área central con icono de subida (`#i-upload`), `.file-upload__title` (instrucción), `.file-upload__button` (pseudo-botón "Elegir archivos" — no un `<button>` real, para no anidar interactivos dentro del label) y `.file-upload__hint` (formatos y tamaño máximo).
|
|
558
|
+
- **`.file-upload__list`** / **`.file-upload__item`** — la lista de subidas. Cada item: `.file-upload__icon` (`#i-file`, o `#i-circle-alert` en error), `.file-upload__name` (trunca con ellipsis), `.file-upload__size` (metadatos/estado) y `.file-upload__remove` (× 32px con `aria-label` que nombra el archivo). Un item que sube incluye la barra `.progress` (§10.18) dentro de su meta.
|
|
559
|
+
- **Error (`.file-upload__item--error`):** borde `--danger-500`, icono y texto de estado en `--danger-text`; **el estado no se comunica solo por color** — el item lleva icono de alerta + texto que explica qué pasó y qué hacer ("Formato no admitido · elige PDF, PNG o JPG").
|
|
560
|
+
- **JS mínimo:** `is-dragover` en dragenter/dragover, se quita en dragleave/drop; el × elimina su item. La subida real y su validación son responsabilidad de la app.
|
|
561
|
+
|
|
562
|
+
### 10.34 Stepper / Wizard
|
|
563
|
+
|
|
564
|
+
Indicador de progreso multipaso.
|
|
565
|
+
|
|
566
|
+
- **`.stepper`** — un **`<ol>`** horizontal; `.stepper--vertical` lo apila. Cada **`.step`** contiene `.step__marker` (círculo 32px), `.step__label` y, salvo el último, un `.step__connector` (línea de 2px hacia el siguiente paso).
|
|
567
|
+
- **Estados:** `.step--complete` (marcador `--accent` con **`#i-check`** blanco, conector `--accent`), `.step--current` (marcador con borde e ícono/número `--accent`, label semibold `--text-primary`, `aria-current="step"`), `.step--upcoming` (marcador neutro con su **número**). El completo muestra check y el próximo muestra número: **el estado no depende solo del color**.
|
|
568
|
+
- **Conector:** hereda el color de su paso — se pinta `--accent` solo tras un paso completo, marcando hasta dónde llegó el progreso; el marcador va en `z-index` superior para que el círculo tape la línea.
|
|
569
|
+
- a11y: lista ordenada real (`<ol>`), `aria-current="step"` en el actual, iconos decorativos (`aria-hidden`); el número/label textual da la señal no cromática.
|
|
570
|
+
|
|
571
|
+
### 10.35 Kbd
|
|
572
|
+
|
|
573
|
+
Tecla del teclado para atajos y ayuda contextual.
|
|
574
|
+
|
|
575
|
+
- **`.kbd`** (para `<kbd>`) — tecla con borde `--border-strong`, radius `sm`, **sombra sutil de tinta** (`--shadow-xs`), tipografía `--font-mono` `text-xs`, color `--text-secondary`, **mínimo 20px** de ancho/alto. Funciona inline en el texto o suelta.
|
|
576
|
+
- **Combos:** varias `.kbd` con el signo **"+" literal** entre teclas (envueltas en `.kbd-combo` para alinear el "+" en `--text-muted`). Ejemplos: `⌘ + K`, `Ctrl + S`, `Esc`.
|
|
577
|
+
|
|
468
578
|
---
|
|
469
579
|
|
|
470
580
|
## 11. Animaciones y motion
|
|
@@ -509,7 +619,7 @@ Antes de animar algo, pregúntate: ¿de dónde vino, qué cambió, a dónde se f
|
|
|
509
619
|
6. **Origen consciente** — Popovers, menús y dropdowns aparecen desde el `transform-origin` de su disparador; los modales, desde el centro. Nunca un origen arbitrario.
|
|
510
620
|
7. **Interrumpible** — Toda animación puede invertirse desde su valor visible actual, sin saltar al destino y reanimar. Clave en hover y gestos. En la práctica: para UI que se dispara con frecuencia (toasts, dropdowns, hover) usa `transition` — con `@starting-style` para la entrada al montar —, no `@keyframes`: un keyframe no puede invertirse a mitad de camino. Los `@keyframes` quedan para loops de carga (spinner, shimmer).
|
|
511
621
|
8. **La salida es más discreta que la entrada** — Además de ~20% más corta, atenúa: menor desplazamiento y menor opacidad. Entrar se nota; salir se difumina.
|
|
512
|
-
9. **El easing según el trabajo** — Entrar → `--ease-out`; salir → `--ease-in`; un elemento que se mueve de un punto a otro de la pantalla (indicador de tabs
|
|
622
|
+
9. **El easing según el trabajo** — Entrar → `--ease-out`; salir → `--ease-in`; un elemento que se mueve de un punto a otro de la pantalla (indicador de tabs que se desliza, reorder) → `--ease-in-out`, que acelera y frena en espejo; hover y cambios de color → `--ease-out` corto; loops de carga → `linear`.
|
|
513
623
|
10. **`will-change` solo mientras anima** — Se pone justo antes de animar y se quita al terminar; nunca permanente en el CSS. Presupuesto de capas GPU: menos de 10 elementos promovidos a la vez.
|
|
514
624
|
|
|
515
625
|
### 11.3 Micro-interacciones por componente
|
|
@@ -523,7 +633,7 @@ Antes de animar algo, pregúntate: ¿de dónde vino, qué cambió, a dónde se f
|
|
|
523
633
|
| Input | focus | ring que aparece con fade 150ms |
|
|
524
634
|
| Checkbox | check | trazo del check dibujándose 200ms `ease-spring` |
|
|
525
635
|
| Toggle | cambio | thumb desliza 200ms `ease-spring` |
|
|
526
|
-
| SegmentedControl | cambio |
|
|
636
|
+
| SegmentedControl | cambio | la opción activa toma fondo blanco 200ms `--ease-out` |
|
|
527
637
|
| Slider | press | thumb escala 1.15, 100ms |
|
|
528
638
|
| Acordeón | abrir | `grid-template-rows` (0fr→1fr) 250ms + fade del contenido |
|
|
529
639
|
| Dropdown / Menu | abrir | fade + translateY(-4px→0) 160–180ms |
|
|
@@ -601,11 +711,13 @@ Regla: un gap en UI primaria (el flujo principal) es crítico; en UI secundaria,
|
|
|
601
711
|
|
|
602
712
|
## 12. Accesibilidad y contraste
|
|
603
713
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
-
|
|
714
|
+
Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/contraste-tokens.test.ts` los recalcula desde `tokens.css` en cada corrida: si alguien retoca la paleta y rompe un mínimo, la suite falla.
|
|
715
|
+
|
|
716
|
+
- `neutral-800` sobre blanco: **12.42:1** ✅ AAA.
|
|
717
|
+
- Blanco sobre `primary-500`: **3.22:1** ⚠️ solo texto ≥18px/bold o componentes UI; para texto normal usar fondo `primary-700`.
|
|
718
|
+
- `--link` (`primary-700`) sobre blanco: **5.94:1** ✅ AA — por eso links y ghost usan `--link`, no `primary-500`.
|
|
607
719
|
- `primary-300` (#a0c0d4) **nunca como texto sobre blanco** — solo decorativo.
|
|
608
|
-
- Tonos `--*-text` garantizan AA sobre los fondos suaves `*-100
|
|
720
|
+
- Tonos `--*-text` garantizan AA sobre los fondos suaves `*-100`: success **5.30:1** · warning **4.72:1** · danger **6.60:1** · info **4.83:1**.
|
|
609
721
|
- Mínimos WCAG AA: 4.5:1 texto normal, 3:1 texto grande y UI.
|
|
610
722
|
- Focus visible siempre (ring global `:focus-visible`, §6); estado nunca comunicado solo con color (icono + texto).
|
|
611
723
|
- Touch targets mínimos: 44×44px.
|
|
@@ -772,7 +884,7 @@ Fondos suaves semánticos en dark: `success #12291f` · `warning #2b2210` · `da
|
|
|
772
884
|
|
|
773
885
|
### 15.4 Ajustes de componentes en dark
|
|
774
886
|
|
|
775
|
-
- **Botón primario:** fondo `#87a8c7`, texto `#16181e` (
|
|
887
|
+
- **Botón primario:** fondo `#87a8c7`, texto `#16181e` (7.14:1 ✅). Hover `#a0c0d4`.
|
|
776
888
|
- **Inputs:** fondo `#252935`, borde `#3a3f4d`, placeholder `#8b91a0`; focus borde `#a0c0d4` + ring.
|
|
777
889
|
- **Tabla:** header `#1d2028`, hover de fila `#252935`, seleccionada `rgba(160,192,212,.1)`.
|
|
778
890
|
- **Sidebar activo:** fondo `rgba(113,146,186,.15)`, texto `#e6e9ee`, icono `#a0c0d4`.
|
|
@@ -783,9 +895,12 @@ Fondos suaves semánticos en dark: `success #12291f` · `warning #2b2210` · `da
|
|
|
783
895
|
|
|
784
896
|
### 15.5 Contraste en dark
|
|
785
897
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
- `#
|
|
898
|
+
Ratios calculados, no estimados (mismo test que §12).
|
|
899
|
+
|
|
900
|
+
- `#e6e9ee` sobre `#16181e`: **14.58:1** ✅ AAA.
|
|
901
|
+
- `#a0c0d4` sobre `#16181e`: **9.28:1** ✅ — en dark el celeste **sí** puede ser texto/link (al revés que en light).
|
|
902
|
+
- `#7192ba` sobre `#16181e`: **5.51:1** ✅ AA — usable en texto normal.
|
|
903
|
+
- Tonos `--*-text` sobre sus `*-100` en dark: success **8.76:1** · warning **9.60:1** · danger **8.03:1** · info **8.20:1**.
|
|
789
904
|
|
|
790
905
|
### 15.6 Implementación
|
|
791
906
|
|
|
@@ -304,6 +304,8 @@
|
|
|
304
304
|
.field__help { font-size: var(--text-sm); color: var(--text-muted); }
|
|
305
305
|
.field__error { font-size: var(--text-sm); color: var(--danger-500); display: inline-flex; align-items: center; gap: 6px; }
|
|
306
306
|
.field__error svg { width: 14px; height: 14px; flex: none; }
|
|
307
|
+
/* Campo obligatorio (§10.4): asterisco decorativo; el input lleva required/aria-required */
|
|
308
|
+
.field--required .field__label::after { content: " *"; color: var(--danger-500); }
|
|
307
309
|
|
|
308
310
|
.input, .textarea, .select {
|
|
309
311
|
width: 100%; font-family: var(--font-body); font-size: var(--text-base);
|
|
@@ -421,6 +423,15 @@
|
|
|
421
423
|
.checkbox input:checked ~ .checkbox__box svg { stroke-dashoffset: 0; }
|
|
422
424
|
.checkbox input:focus-visible ~ .checkbox__box { box-shadow: var(--ring); }
|
|
423
425
|
.checkbox input:disabled ~ * { opacity: .5; cursor: not-allowed; }
|
|
426
|
+
/* Estado indeterminate (§10.9): guion horizontal para "seleccionar todo" parcial */
|
|
427
|
+
.checkbox__box { position: relative; }
|
|
428
|
+
.checkbox input:indeterminate ~ .checkbox__box { background: var(--accent); border-color: var(--accent); }
|
|
429
|
+
.checkbox input:indeterminate ~ .checkbox__box svg { visibility: hidden; }
|
|
430
|
+
.checkbox input:indeterminate ~ .checkbox__box::after {
|
|
431
|
+
content: ""; position: absolute; top: 50%; left: 50%;
|
|
432
|
+
width: 10px; height: 2px;
|
|
433
|
+
background: var(--text-on-primary); transform: translate(-50%, -50%);
|
|
434
|
+
}
|
|
424
435
|
|
|
425
436
|
.radio__dot {
|
|
426
437
|
width: 20px; height: 20px; flex: none; display: grid; place-items: center;
|
|
@@ -526,6 +537,19 @@
|
|
|
526
537
|
.avatar__dot--busy { background: var(--danger-500); }
|
|
527
538
|
.avatar__dot--away { background: var(--warning-500); }
|
|
528
539
|
|
|
540
|
+
/* AvatarGroup (§10.27): stack solapado con anillo de superficie */
|
|
541
|
+
.avatar-group { display: inline-flex; align-items: center; }
|
|
542
|
+
.avatar-group .avatar { border: 2px solid var(--surface); border-radius: var(--radius-full); }
|
|
543
|
+
.avatar-group .avatar + .avatar,
|
|
544
|
+
.avatar-group .avatar-group__more { margin-left: -8px; }
|
|
545
|
+
.avatar-group__more {
|
|
546
|
+
display: grid; place-items: center; flex: none;
|
|
547
|
+
width: 36px; height: 36px; border-radius: var(--radius-full);
|
|
548
|
+
border: 2px solid var(--surface);
|
|
549
|
+
background: var(--accent-soft-strong); color: var(--text-on-primary-solid);
|
|
550
|
+
font-family: var(--font-body); font-size: var(--text-xs); font-weight: var(--weight-semibold);
|
|
551
|
+
}
|
|
552
|
+
|
|
529
553
|
.divider { border: 0; border-top: var(--border-width) solid var(--border); margin: var(--space-4) 0; }
|
|
530
554
|
.divider--label {
|
|
531
555
|
display: flex; align-items: center; gap: 12px; border: 0; margin: var(--space-4) 0;
|
|
@@ -787,6 +811,30 @@
|
|
|
787
811
|
.table tbody tr.is-selected { background: var(--accent-soft); }
|
|
788
812
|
.table .num, .table td.num, .table th.num { text-align: right; font-family: var(--font-mono); }
|
|
789
813
|
|
|
814
|
+
/* Variantes de tabla (§10.21): zebra, sticky header, orden y selección */
|
|
815
|
+
.table--zebra tbody tr:nth-child(even) { background: var(--surface-alt); }
|
|
816
|
+
.table--zebra tbody tr:nth-child(even):hover { background: var(--bg); }
|
|
817
|
+
.table--sticky thead th { position: sticky; top: 0; z-index: 1; }
|
|
818
|
+
.table tbody tr.row--selected,
|
|
819
|
+
.table tbody tr.row--selected:hover { background: var(--accent-soft); }
|
|
820
|
+
.table .col-select { width: 44px; text-align: center; }
|
|
821
|
+
.table .col-select .checkbox { display: inline-flex; justify-content: center; gap: 0; }
|
|
822
|
+
|
|
823
|
+
.th-sort {
|
|
824
|
+
display: inline-flex; align-items: center; gap: 6px; width: 100%;
|
|
825
|
+
border: 0; background: transparent; cursor: pointer;
|
|
826
|
+
font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit; text-align: left;
|
|
827
|
+
}
|
|
828
|
+
.th-sort--num { justify-content: flex-end; }
|
|
829
|
+
.th-sort .sort-icon {
|
|
830
|
+
width: 14px; height: 14px; flex: none; opacity: .35;
|
|
831
|
+
transition: opacity var(--duration-fast) var(--ease-out),
|
|
832
|
+
transform var(--duration-base) var(--ease-out);
|
|
833
|
+
}
|
|
834
|
+
.table th[aria-sort="ascending"] .sort-icon,
|
|
835
|
+
.table th[aria-sort="descending"] .sort-icon { opacity: 1; color: var(--accent); }
|
|
836
|
+
.table th[aria-sort="descending"] .sort-icon { transform: rotate(180deg); }
|
|
837
|
+
|
|
790
838
|
.stat__label {
|
|
791
839
|
display: inline-flex; align-items: center; gap: 8px; margin-bottom: 8px;
|
|
792
840
|
font-size: var(--text-sm); color: var(--text-muted);
|
|
@@ -902,6 +950,306 @@
|
|
|
902
950
|
border-radius: var(--radius-sm); padding: 1px 6px;
|
|
903
951
|
}
|
|
904
952
|
|
|
953
|
+
/* --------------------------------------------------------------------------
|
|
954
|
+
13b. COMBOBOX · BÚSQUEDA · DRAWER · POPOVER · BUTTONGROUP (§10.28–10.31)
|
|
955
|
+
-------------------------------------------------------------------------- */
|
|
956
|
+
|
|
957
|
+
/* Combobox / Autocomplete (§10.28) */
|
|
958
|
+
.combobox { position: relative; }
|
|
959
|
+
.combobox__input {
|
|
960
|
+
width: 100%; height: var(--control-height-md); padding: 0 14px;
|
|
961
|
+
font-family: var(--font-body); font-size: var(--text-base);
|
|
962
|
+
color: var(--text-primary); background: var(--surface);
|
|
963
|
+
border: var(--border-width) solid var(--border-strong); border-radius: var(--radius-sm);
|
|
964
|
+
transition: border-color var(--duration-fast) var(--ease-out),
|
|
965
|
+
box-shadow var(--duration-fast) var(--ease-out);
|
|
966
|
+
}
|
|
967
|
+
.combobox__input:hover:not(:disabled):not(:focus) { border-color: var(--text-disabled); }
|
|
968
|
+
.combobox__input:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
|
|
969
|
+
.combobox__input:disabled { background: var(--surface-alt); color: var(--text-disabled); cursor: not-allowed; }
|
|
970
|
+
.combobox__list {
|
|
971
|
+
position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 50;
|
|
972
|
+
max-height: 264px; overflow-y: auto; margin: 0; padding: 6px; list-style: none;
|
|
973
|
+
background: var(--surface); border: var(--border-width) solid var(--border);
|
|
974
|
+
border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
|
|
975
|
+
opacity: 0; transform: translateY(-4px); pointer-events: none;
|
|
976
|
+
transition: opacity var(--duration-base) var(--ease-out),
|
|
977
|
+
transform var(--duration-base) var(--ease-out);
|
|
978
|
+
}
|
|
979
|
+
.combobox.is-open .combobox__list { opacity: 1; transform: none; pointer-events: auto; }
|
|
980
|
+
.combobox__option {
|
|
981
|
+
display: flex; align-items: center; gap: 8px;
|
|
982
|
+
padding: 10px 12px; border-radius: var(--radius-sm);
|
|
983
|
+
font-size: var(--text-sm); color: var(--text-primary); cursor: pointer;
|
|
984
|
+
transition: background-color var(--duration-fast) var(--ease-out);
|
|
985
|
+
}
|
|
986
|
+
.combobox__option .combobox__check {
|
|
987
|
+
width: 16px; height: 16px; margin-left: auto; flex: none;
|
|
988
|
+
color: var(--accent); opacity: 0;
|
|
989
|
+
}
|
|
990
|
+
.combobox__option.is-active { background: var(--accent-soft); }
|
|
991
|
+
.combobox__option[aria-selected="true"] { color: var(--link); font-weight: var(--weight-medium); }
|
|
992
|
+
.combobox__option[aria-selected="true"] .combobox__check { opacity: 1; }
|
|
993
|
+
.combobox__empty { padding: 10px 12px; font-size: var(--text-sm); color: var(--text-muted); }
|
|
994
|
+
|
|
995
|
+
/* Búsqueda (§10.28) */
|
|
996
|
+
.search { position: relative; display: flex; align-items: center; }
|
|
997
|
+
.search__icon {
|
|
998
|
+
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
|
|
999
|
+
width: 18px; height: 18px; color: var(--text-disabled); pointer-events: none;
|
|
1000
|
+
}
|
|
1001
|
+
.search__input {
|
|
1002
|
+
width: 100%; height: var(--control-height-md); padding: 0 40px;
|
|
1003
|
+
font-family: var(--font-body); font-size: var(--text-base);
|
|
1004
|
+
color: var(--text-primary); background: var(--surface);
|
|
1005
|
+
border: var(--border-width) solid var(--border-strong); border-radius: var(--radius-sm);
|
|
1006
|
+
transition: border-color var(--duration-fast) var(--ease-out),
|
|
1007
|
+
box-shadow var(--duration-fast) var(--ease-out);
|
|
1008
|
+
}
|
|
1009
|
+
.search__input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
|
|
1010
|
+
.search__input:hover:not(:focus) { border-color: var(--text-disabled); }
|
|
1011
|
+
.search__input:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
|
|
1012
|
+
.search__clear {
|
|
1013
|
+
position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
|
|
1014
|
+
display: none; width: 32px; height: 32px; align-items: center; justify-content: center;
|
|
1015
|
+
border: 0; border-radius: var(--radius-full); background: transparent;
|
|
1016
|
+
color: var(--text-muted); cursor: pointer;
|
|
1017
|
+
transition: background-color var(--duration-fast) var(--ease-out),
|
|
1018
|
+
color var(--duration-fast) var(--ease-out);
|
|
1019
|
+
}
|
|
1020
|
+
.search__clear:hover { background: var(--surface-alt); color: var(--text-primary); }
|
|
1021
|
+
.search__clear svg { width: 16px; height: 16px; }
|
|
1022
|
+
.search__input:not(:placeholder-shown) ~ .search__clear { display: inline-flex; }
|
|
1023
|
+
|
|
1024
|
+
/* Drawer / Sheet (§10.29) */
|
|
1025
|
+
.drawer {
|
|
1026
|
+
position: fixed; inset: 0; z-index: 100;
|
|
1027
|
+
visibility: hidden; pointer-events: none;
|
|
1028
|
+
transition: visibility 0s linear var(--duration-base);
|
|
1029
|
+
}
|
|
1030
|
+
.drawer.is-open { visibility: visible; pointer-events: auto; transition-delay: 0s; }
|
|
1031
|
+
.drawer__overlay {
|
|
1032
|
+
position: absolute; inset: 0; background: var(--overlay);
|
|
1033
|
+
opacity: 0; transition: opacity var(--duration-base) var(--ease-out);
|
|
1034
|
+
}
|
|
1035
|
+
.drawer.is-open .drawer__overlay { opacity: 1; }
|
|
1036
|
+
.drawer__panel {
|
|
1037
|
+
position: absolute; inset: 0 0 0 auto; width: min(400px, 100%);
|
|
1038
|
+
display: flex; flex-direction: column; gap: var(--space-5);
|
|
1039
|
+
padding: var(--card-padding); background: var(--surface);
|
|
1040
|
+
box-shadow: var(--shadow-xl); overflow-y: auto; overscroll-behavior: contain;
|
|
1041
|
+
transform: translateX(100%);
|
|
1042
|
+
transition: transform var(--duration-base) var(--ease-in);
|
|
1043
|
+
}
|
|
1044
|
+
.drawer.is-open .drawer__panel {
|
|
1045
|
+
transform: none;
|
|
1046
|
+
transition: transform var(--duration-slow) var(--ease-out);
|
|
1047
|
+
}
|
|
1048
|
+
.drawer--right .drawer__panel { inset: 0 0 0 auto; transform: translateX(100%); }
|
|
1049
|
+
.drawer--left .drawer__panel { inset: 0 auto 0 0; transform: translateX(-100%); }
|
|
1050
|
+
.drawer--bottom .drawer__panel {
|
|
1051
|
+
inset: auto 0 0 0; width: 100%; max-height: 85vh;
|
|
1052
|
+
border-radius: var(--radius-lg) var(--radius-lg) 0 0; transform: translateY(100%);
|
|
1053
|
+
}
|
|
1054
|
+
.drawer__header {
|
|
1055
|
+
display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
|
|
1056
|
+
}
|
|
1057
|
+
.drawer__title { font-size: var(--text-lg); font-weight: var(--weight-semibold); margin: 0; }
|
|
1058
|
+
.drawer__body { flex: 1; }
|
|
1059
|
+
.drawer__footer {
|
|
1060
|
+
display: flex; justify-content: flex-end; gap: var(--space-3); margin-top: auto;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/* Popover (§10.30) */
|
|
1064
|
+
.popover { position: relative; display: inline-block; }
|
|
1065
|
+
.popover__panel {
|
|
1066
|
+
position: absolute; top: calc(100% + 8px); left: 0; z-index: 60;
|
|
1067
|
+
width: max-content; max-width: 300px; padding: var(--space-4);
|
|
1068
|
+
background: var(--surface); border: var(--border-width) solid var(--border);
|
|
1069
|
+
border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
|
|
1070
|
+
transform-origin: top left; opacity: 0; transform: scale(.96); pointer-events: none;
|
|
1071
|
+
transition: opacity var(--duration-base) var(--ease-out),
|
|
1072
|
+
transform var(--duration-base) var(--ease-out);
|
|
1073
|
+
}
|
|
1074
|
+
.popover.is-open .popover__panel { opacity: 1; transform: none; pointer-events: auto; }
|
|
1075
|
+
.popover--right .popover__panel { left: auto; right: 0; transform-origin: top right; }
|
|
1076
|
+
|
|
1077
|
+
/* ButtonGroup (§10.31) */
|
|
1078
|
+
.btn-group { display: inline-flex; }
|
|
1079
|
+
.btn-group .btn { border-radius: 0; }
|
|
1080
|
+
.btn-group .btn:first-child {
|
|
1081
|
+
border-top-left-radius: var(--radius-md); border-bottom-left-radius: var(--radius-md);
|
|
1082
|
+
}
|
|
1083
|
+
.btn-group .btn:last-child {
|
|
1084
|
+
border-top-right-radius: var(--radius-md); border-bottom-right-radius: var(--radius-md);
|
|
1085
|
+
}
|
|
1086
|
+
.btn-group .btn:not(:first-child) { margin-left: calc(var(--border-width) * -1); }
|
|
1087
|
+
.btn-group .btn:hover, .btn-group .btn:focus-visible { z-index: 1; }
|
|
1088
|
+
|
|
1089
|
+
/* --------------------------------------------------------------------------
|
|
1090
|
+
13c. DATEPICKER · FILEUPLOAD · STEPPER · KBD (§10.32–10.35)
|
|
1091
|
+
-------------------------------------------------------------------------- */
|
|
1092
|
+
|
|
1093
|
+
/* DatePicker (§10.32) */
|
|
1094
|
+
.datepicker { position: relative; display: inline-block; }
|
|
1095
|
+
.datepicker__input {
|
|
1096
|
+
width: 100%; height: var(--control-height-md); padding: 0 40px 0 14px;
|
|
1097
|
+
font-family: var(--font-body); font-size: var(--text-base);
|
|
1098
|
+
color: var(--text-primary); background: var(--surface);
|
|
1099
|
+
border: var(--border-width) solid var(--border-strong); border-radius: var(--radius-sm);
|
|
1100
|
+
transition: border-color var(--duration-fast) var(--ease-out),
|
|
1101
|
+
box-shadow var(--duration-fast) var(--ease-out);
|
|
1102
|
+
}
|
|
1103
|
+
.datepicker__input:hover:not(:disabled):not(:focus) { border-color: var(--text-disabled); }
|
|
1104
|
+
.datepicker__input:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
|
|
1105
|
+
.datepicker__input:disabled { background: var(--surface-alt); color: var(--text-disabled); cursor: not-allowed; }
|
|
1106
|
+
.datepicker__icon {
|
|
1107
|
+
position: absolute; right: 12px; top: calc(var(--control-height-md) / 2); transform: translateY(-50%);
|
|
1108
|
+
width: 18px; height: 18px; color: var(--text-muted); pointer-events: none;
|
|
1109
|
+
}
|
|
1110
|
+
.datepicker__calendar {
|
|
1111
|
+
position: absolute; top: calc(100% + 8px); left: 0; z-index: 60;
|
|
1112
|
+
width: max-content; padding: var(--space-4);
|
|
1113
|
+
background: var(--surface); border: var(--border-width) solid var(--border);
|
|
1114
|
+
border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
|
|
1115
|
+
transform-origin: top left; opacity: 0; transform: scale(.96); pointer-events: none;
|
|
1116
|
+
transition: opacity var(--duration-base) var(--ease-out),
|
|
1117
|
+
transform var(--duration-base) var(--ease-out);
|
|
1118
|
+
}
|
|
1119
|
+
.datepicker.is-open .datepicker__calendar { opacity: 1; transform: none; pointer-events: auto; }
|
|
1120
|
+
.datepicker--right .datepicker__calendar { left: auto; right: 0; transform-origin: top right; }
|
|
1121
|
+
|
|
1122
|
+
.calendar { width: 15.75rem; }
|
|
1123
|
+
.calendar__header {
|
|
1124
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
1125
|
+
gap: var(--space-2); margin-bottom: var(--space-3);
|
|
1126
|
+
}
|
|
1127
|
+
.calendar__title { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text-primary); }
|
|
1128
|
+
.calendar__weekdays,
|
|
1129
|
+
.calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
|
|
1130
|
+
.calendar__weekdays { margin-bottom: 4px; }
|
|
1131
|
+
.calendar__weekday {
|
|
1132
|
+
display: flex; align-items: center; justify-content: center; height: 28px;
|
|
1133
|
+
font-size: var(--text-xs); font-weight: var(--weight-medium); color: var(--text-muted);
|
|
1134
|
+
}
|
|
1135
|
+
.calendar__day {
|
|
1136
|
+
display: flex; align-items: center; justify-content: center;
|
|
1137
|
+
width: 34px; height: 34px; margin: 0 auto; padding: 0;
|
|
1138
|
+
border: 0; border-radius: var(--radius-sm); background: transparent;
|
|
1139
|
+
font-family: var(--font-body); font-size: var(--text-sm); color: var(--text-primary);
|
|
1140
|
+
cursor: pointer;
|
|
1141
|
+
transition: background-color var(--duration-fast) var(--ease-out),
|
|
1142
|
+
color var(--duration-fast) var(--ease-out);
|
|
1143
|
+
}
|
|
1144
|
+
.calendar__day:hover:not(:disabled):not(.calendar__day--selected) { background: var(--accent-soft); }
|
|
1145
|
+
.calendar__day--outside { color: var(--text-disabled); }
|
|
1146
|
+
.calendar__day--today { font-weight: var(--weight-semibold); box-shadow: inset 0 0 0 1px var(--border-strong); }
|
|
1147
|
+
.calendar__day--selected { background: var(--accent); color: var(--text-on-primary); font-weight: var(--weight-semibold); }
|
|
1148
|
+
.calendar__day:disabled { color: var(--text-disabled); cursor: not-allowed; }
|
|
1149
|
+
|
|
1150
|
+
/* FileUpload (§10.33) */
|
|
1151
|
+
.file-upload {
|
|
1152
|
+
border: 2px dashed var(--border-strong); border-radius: var(--radius-lg);
|
|
1153
|
+
background: var(--surface);
|
|
1154
|
+
transition: border-color var(--duration-fast) var(--ease-out),
|
|
1155
|
+
background-color var(--duration-fast) var(--ease-out);
|
|
1156
|
+
}
|
|
1157
|
+
.file-upload.is-dragover { border-color: var(--accent); background: var(--accent-soft); }
|
|
1158
|
+
.file-upload__input {
|
|
1159
|
+
position: absolute; width: 1px; height: 1px; margin: -1px;
|
|
1160
|
+
clip-path: inset(50%); overflow: hidden; white-space: nowrap;
|
|
1161
|
+
}
|
|
1162
|
+
.file-upload__label {
|
|
1163
|
+
display: flex; flex-direction: column; align-items: center; text-align: center;
|
|
1164
|
+
gap: var(--space-2); padding: var(--space-8) var(--space-6); cursor: pointer;
|
|
1165
|
+
}
|
|
1166
|
+
.file-upload__label > svg { width: 32px; height: 32px; color: var(--text-muted); }
|
|
1167
|
+
.file-upload__title { font-size: var(--text-sm); color: var(--text-secondary); }
|
|
1168
|
+
.file-upload__hint { font-size: var(--text-xs); color: var(--text-muted); }
|
|
1169
|
+
.file-upload__button {
|
|
1170
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
1171
|
+
height: var(--control-height-sm); padding: 0 16px; margin-top: var(--space-1);
|
|
1172
|
+
border: var(--border-width) solid var(--border-strong); border-radius: var(--radius-md);
|
|
1173
|
+
font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text-primary);
|
|
1174
|
+
background: var(--surface);
|
|
1175
|
+
transition: background-color var(--duration-fast) var(--ease-out);
|
|
1176
|
+
}
|
|
1177
|
+
.file-upload__label:hover .file-upload__button { background: var(--bg); }
|
|
1178
|
+
.file-upload__list { list-style: none; margin: var(--space-4) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
|
|
1179
|
+
.file-upload__item {
|
|
1180
|
+
display: flex; align-items: center; gap: var(--space-3);
|
|
1181
|
+
padding: var(--space-3); border: var(--border-width) solid var(--border);
|
|
1182
|
+
border-radius: var(--radius-md); background: var(--surface);
|
|
1183
|
+
}
|
|
1184
|
+
.file-upload__icon { width: 20px; height: 20px; color: var(--text-muted); flex: none; }
|
|
1185
|
+
.file-upload__meta { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; }
|
|
1186
|
+
.file-upload__name {
|
|
1187
|
+
font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-primary);
|
|
1188
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
1189
|
+
}
|
|
1190
|
+
.file-upload__size { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-xs); color: var(--text-muted); }
|
|
1191
|
+
.file-upload__size svg { width: 12px; height: 12px; flex: none; }
|
|
1192
|
+
.file-upload__meta .progress { margin-top: 2px; }
|
|
1193
|
+
.file-upload__remove {
|
|
1194
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
1195
|
+
width: 32px; height: 32px; flex: none; border: 0; border-radius: var(--radius-full);
|
|
1196
|
+
background: transparent; color: var(--text-muted); cursor: pointer;
|
|
1197
|
+
transition: background-color var(--duration-fast) var(--ease-out),
|
|
1198
|
+
color var(--duration-fast) var(--ease-out);
|
|
1199
|
+
}
|
|
1200
|
+
.file-upload__remove:hover { background: var(--surface-alt); color: var(--text-primary); }
|
|
1201
|
+
.file-upload__remove svg { width: 16px; height: 16px; }
|
|
1202
|
+
.file-upload__item--error { border-color: var(--danger-500); }
|
|
1203
|
+
.file-upload__item--error .file-upload__icon,
|
|
1204
|
+
.file-upload__item--error .file-upload__size { color: var(--danger-text); }
|
|
1205
|
+
|
|
1206
|
+
/* Stepper / Wizard (§10.34) */
|
|
1207
|
+
.stepper { display: flex; list-style: none; margin: 0; padding: 0; }
|
|
1208
|
+
.step {
|
|
1209
|
+
position: relative; flex: 1;
|
|
1210
|
+
display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--space-2);
|
|
1211
|
+
}
|
|
1212
|
+
.step__marker {
|
|
1213
|
+
position: relative; z-index: 1;
|
|
1214
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
1215
|
+
width: 32px; height: 32px; flex: none; border-radius: var(--radius-full);
|
|
1216
|
+
border: 2px solid var(--border-strong); background: var(--surface);
|
|
1217
|
+
font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text-muted);
|
|
1218
|
+
}
|
|
1219
|
+
.step__marker svg { width: 16px; height: 16px; }
|
|
1220
|
+
.step__label { font-size: var(--text-sm); color: var(--text-secondary); }
|
|
1221
|
+
.step__connector {
|
|
1222
|
+
position: absolute; top: 16px; z-index: 0; left: 50%; right: -50%;
|
|
1223
|
+
height: 2px; margin: 0 20px; background: var(--border);
|
|
1224
|
+
}
|
|
1225
|
+
.step--complete .step__marker { background: var(--accent); border-color: var(--accent); color: var(--text-on-primary); }
|
|
1226
|
+
.step--complete .step__label { color: var(--text-primary); }
|
|
1227
|
+
.step--complete .step__connector { background: var(--accent); }
|
|
1228
|
+
.step--current .step__marker { border-color: var(--accent); color: var(--accent); }
|
|
1229
|
+
.step--current .step__label { color: var(--text-primary); font-weight: var(--weight-semibold); }
|
|
1230
|
+
.stepper--vertical { flex-direction: column; align-items: stretch; }
|
|
1231
|
+
.stepper--vertical .step {
|
|
1232
|
+
flex-direction: row; align-items: flex-start; text-align: left;
|
|
1233
|
+
gap: var(--space-3); padding-bottom: var(--space-6);
|
|
1234
|
+
}
|
|
1235
|
+
.stepper--vertical .step:last-child { padding-bottom: 0; }
|
|
1236
|
+
.stepper--vertical .step__connector {
|
|
1237
|
+
top: 36px; bottom: -4px; left: 15px; right: auto;
|
|
1238
|
+
width: 2px; height: auto; margin: 0;
|
|
1239
|
+
}
|
|
1240
|
+
.stepper--vertical .step__label { padding-top: 5px; }
|
|
1241
|
+
|
|
1242
|
+
/* Kbd (§10.35) */
|
|
1243
|
+
.kbd {
|
|
1244
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
1245
|
+
min-width: 20px; height: 20px; padding: 0 6px;
|
|
1246
|
+
font-family: var(--font-mono); font-size: var(--text-xs); line-height: 1;
|
|
1247
|
+
color: var(--text-secondary); background: var(--surface);
|
|
1248
|
+
border: var(--border-width) solid var(--border-strong);
|
|
1249
|
+
border-radius: var(--radius-sm); box-shadow: var(--shadow-xs);
|
|
1250
|
+
}
|
|
1251
|
+
.kbd-combo { display: inline-flex; align-items: center; gap: 4px; color: var(--text-muted); font-size: var(--text-xs); }
|
|
1252
|
+
|
|
905
1253
|
/* --------------------------------------------------------------------------
|
|
906
1254
|
14. UTILIDADES MÍNIMAS
|
|
907
1255
|
-------------------------------------------------------------------------- */
|