# GUIA_DISENO.md — Template > Este template define la estructura de la guía de diseño de un proyecto que se > construye con agentes de IA. Sale de dos guías reales que siguen vivas: la de > una aplicación de datos con navegación autenticada, que en cinco meses pasó > por más de setenta revisiones y superó las tres mil líneas, y la de un sitio > de contenido en otro stack. Lo que las dos comparten está acá; lo que era de > cada producto quedó afuera. > > **Principio:** este documento es EVOLUTIVO. No se escribe completo al inicio. > Se empieza con las secciones obligatorias y crece con la implementación. Pero > crece con una regla: **una regla sin su cicatriz es una opinión**. Cada norma > que entre después del primer día dice qué pasó para que hiciera falta. > > **Segundo principio:** los valores de esta guía son **derivados, no > autoridad**. Los tokens viven en un archivo del código; si un valor de acá no > coincide con ese archivo, el que está mal es este documento. --- ## Cómo usar este template 1. Copiar como `docs/GUIA_DISENO.md` en tu proyecto. 2. Decidir el perfil. **Aplicación** (navegación autenticada, listas, formularios, detalle) usa todas las secciones. **Sitio** (contenido público, sin CRUD) borra las secciones marcadas *[APLICACIONES]* en vez de dejarlas vacías. 3. Llenar las secciones **[OBLIGATORIO]** antes de la primera sesión de implementación de interfaz. 4. Las secciones **[CRECE CON EL PROYECTO]** se llenan conforme se implementa, cada una con la cicatriz que la originó. 5. Las secciones **[OPCIONAL]** se agregan si el proyecto las necesita. 6. Eliminar esta guía de uso y los comentarios `` cuando el documento esté en uso. ### Relación con otros documentos y skills Este documento es el eslabón del medio de una cadena, y conviene saber qué no le toca: | Pregunta | Quién responde | |---|---| | ¿Qué es esto, cómo se llama y dónde vive? | Skill `information-architecture` | | ¿Cómo se comporta el usuario al usarlo? | Skill `protocolo-ux` (modal vs. página, tablas, formularios, 4 estados) | | ¿Con qué tokens, layouts y componentes se ve en ESTE proyecto? | **Esta guía** | | ¿Con qué código lo construyo? | Tu skill `ux-patterns`, que se escribe a partir de esta guía (ver la última sección) | | ¿Qué componentes existen ya? | El inventario de componentes, documento aparte que el detector de entropía verifica | | ¿Qué dice cada texto? | Skill `ux-writer` y su glosario | | ¿Por qué se eligió esto y no aquello? | `docs/ADR.md`; esta guía enlaza la fila, no repite el argumento | | ¿Qué gotcha tiene el stack fuera de lo visual? | Las notas técnicas del proyecto | Lo que esta guía **no** lleva: el inventario de componentes (crece solo y tiene su documento), los valores de token copiados (se derivan del archivo), el copy (vive en el glosario) y un changelog (vive en git y en los documentos de cambio). --- ## Índice 0. [Dónde vive la verdad](#0-dónde-vive-la-verdad) — OBLIGATORIO 1. [Principio fundamental y sus excepciones](#1-principio-fundamental-y-sus-excepciones) — OBLIGATORIO 2. [Sistema de tokens](#2-sistema-de-tokens) — OBLIGATORIO 3. [Tipografía](#3-tipografía) — OBLIGATORIO 4. [Espaciado, radios y sombras](#4-espaciado-radios-y-sombras) — OBLIGATORIO 5. [Temas](#5-temas) — OBLIGATORIO si hay modo oscuro 6. [Layout en niveles](#6-layout-en-niveles) — OBLIGATORIO · APLICACIONES 7. [Móvil](#7-móvil) — OBLIGATORIO 8. [Listas y tablas](#8-listas-y-tablas) — CRECE · APLICACIONES 9. [Formularios](#9-formularios) — CRECE · APLICACIONES 10. [Movimiento](#10-movimiento) — OPCIONAL 11. [Identidad y pantallas expresivas](#11-identidad-y-pantallas-expresivas) — OPCIONAL 12. [Iconografía](#12-iconografía) — OBLIGATORIO 13. [Accesibilidad](#13-accesibilidad) — OBLIGATORIO 14. [Otras superficies de salida](#14-otras-superficies-de-salida) — OPCIONAL 15. [Componentes con reglas especiales](#15-componentes-con-reglas-especiales) — CRECE 16. [Cicatrices](#16-cicatrices) — CRECE 17. [Checklist para nuevas funcionalidades](#17-checklist-para-nuevas-funcionalidades) — OBLIGATORIO 18. [Tu skill ux-patterns](#18-tu-skill-ux-patterns) — OBLIGATORIO --- ## 0. Dónde vive la verdad [OBLIGATORIO] | Qué | Dónde | Quién lo cambia | |---|---|---| | Tokens de color, radio, sombra, movimiento | `{ruta del archivo de tokens}` | {a mano / generado por `{comando}` desde `{fuente}`} | | Escala tipográfica | `{ruta}` | {…} | | Criterio de contraste | {WCAG 2.x AA / APCA} — medido con `{herramienta o función}` | {quien decide el producto} | | Referencia visual (herramienta de diseño) | {URL o «no hay»} | {dirección del sync: código → diseño, diseño → código, ninguna} | | Inventario de componentes | `{ruta del inventario}` | Se actualiza en el mismo commit que el componente | **Reglas:** - Las tablas de valores de esta guía **son derivadas**. Para el valor exacto de un token, leer el archivo; si difieren, corregir la guía. - Si los tokens se generan, **nadie edita el archivo generado a mano**: el siguiente build lo sobreescribe. Si hay un check que lo detecta, nombrarlo acá. - La referencia visual es fuente de verdad **visual** (tokens, tipografía, layout, estados). No es fuente del inventario funcional: los módulos que aparezcan en sus mockups de navegación son ejemplo, no catálogo. Si divergen, manda la especificación del producto. - Si la herramienta de diseño sólo sincroniza en una dirección, decirlo. Un color editado en el lado que no manda se pierde en el próximo sync. --- ## 1. Principio fundamental y sus excepciones [OBLIGATORIO] **{Tu principio fundamental aquí.}** **Excepciones declaradas.** Toda regla dura tiene excepciones legítimas. Si no se escriben, el check que la verifica cría alarmas que el equipo aprende a ignorar, y un check ignorado no existe. | Excepción | Por qué es legítima | Alcance | |---|---|---| | {p. ej. paneles decorativos de marca usan colores fijos} | {se pintan sobre gradientes de marca, no sobre superficies del tema} | {sólo auth, error, onboarding; nunca UI funcional} | **Cómo se verifica:** `{comando: grep, lint, script}`. Si no hay comando, el principio es una aspiración. --- ## 2. Sistema de tokens [OBLIGATORIO] ### 2.1 Roles **Superficies** | Rol | Light | Dark | Uso | |---|---|---|---| | `background` | `{valor}` | `{valor}` | Cuerpo, inputs, la base más clara | | `muted` / canvas | `{valor}` | `{valor}` | Área de contenido, detrás de las tarjetas | | `card`, `popover` | `{valor}` | `{valor}` | Superficies que flotan sobre el canvas | | `{sidebar}` | `{valor}` | `{valor}` | Navegación principal | **Texto y bordes** | Rol | Light | Dark | Uso | |---|---|---|---| | `foreground` | `{valor}` | `{valor}` | Texto principal | | `muted-foreground` | `{valor}` | `{valor}` | Texto secundario, placeholders, fechas | | `border`, `input` | `{valor}` | `{valor}` | Bordes y contornos de campo | **Acción** | Rol | Light | Dark | Uso | |---|---|---|---| | `primary` / `primary-foreground` | `{valor}` | `{valor}` | Botones, enlaces activos, anillo de foco | | `destructive` / `destructive-foreground` | `{valor}` | `{valor}` | Acciones destructivas | ### 2.2 Jerarquía de superficies ``` Light (más claro = más arriba): canvas < navegación < card = background Dark (más claro = más arriba, pero invertido en valor): background < canvas < navegación < card ``` Las tarjetas «flotan» sobre el canvas por diferencia de luminosidad, no por bordes. En dark la elevación se comunica con **superficie más clara**, no con sombra: las sombras pierden efecto sobre fondos oscuros. ### 2.3 Regla de contención: contenido sobre superficie [APLICACIONES] **Invariante:** ningún contenido vive directamente sobre el canvas. Todo va sobre una superficie que lo separe del fondo: una tarjeta, una tabla con fondo propio, o un panel con fondo diferenciado (raro; documentar la excepción). Únicos elementos permitidos sobre el canvas: el título de página, las migas de pan, las acciones de página y los elementos estructurales del layout. Anti-patrones: formularios «desnudos» sobre el fondo; bloques de indicadores sueltos; y el contrario, el doble contenedor: envolver en tarjeta una tabla que ya trae su fondo. ### 2.4 Los estados son roles, no opacidades Cada estado que necesite color propio es un **rol con nombre** que apunta a un valor de la escala. La opacidad sobre un token queda para lo que compone sobre contenido desconocido (scrims, vidrio) y se declara en el token, no en el uso. | Necesito… | Rol | |---|---| | Hover de fila, ítem de lista, botón fantasma | `{muted-hover}` | | Fila seleccionada o presionada | `{muted-active}` | | Callout o panel dentro de una tarjeta | `{muted-subtle}` | | Divisor más quieto que `border` | `{border-subtle}` | | Borde enfatizado, zona de arrastre | `{border-strong}` | | Glifo deshabilitado o decorativo | `{foreground-disabled}` | | Hover del primario | `{primary-hover}` | | Zona seleccionada con tinte de acento | `{primary-surface}` + `{primary-border}` | | Scrim de diálogo / de drawer | `{overlay}` / `{overlay-soft}` (alfa por diseño) | | Skeleton | `{skeleton}` | **Si el sistema es chico**, dos o tres niveles de opacidad con nombre cumplen lo mismo. Lo que no escala es que cada componente invente el suyo. Un estado nuevo es un rol nuevo, nunca un `/NN` suelto. ### 2.5 Contraste - **Criterio declarado en §0.** WCAG 2.x y APCA no se convierten entre sí: una guía que mezcla los dos no tiene criterio. - **Medir contra el color que la aplicación pinta de verdad.** El texto oscuro casi nunca es negro puro; medir contra `#000` da un resultado que la pantalla no tiene. Cicatriz real: cinco centésimas de luminosidad movieron un texto un escalón entero de tamaño. - **Los pares que no conforman y se aceptan se escriben**, con quién lo decidió y cuándo. Una excepción sin registro se «corrige» seis meses después subiendo un peso que rompe el sistema. | Par que no conforma | Medida | Decisión | Quién / cuándo | |---|---|---|---| | {p. ej. primario a 16px en peso 600} | {valor} | {aceptado; no corregir subiendo peso ni tamaño} | {…} | ### 2.6 Reglas de color 1. **Nunca blanco puro ni negro puro** en fondos. Un tinte mínimo hacia la paleta mantiene la identidad en los dos modos. 2. Los colores de acción **se ajustan por modo**: mismo tono, más luminosidad y menos saturación en dark (detalle en §5). 3. El texto sobre un color de acción **lo decide la medición**, no la costumbre. 4. Los colores de marca fijos (§11) **sólo en superficies decorativas**. La UI funcional usa roles. ### 2.7 Cómo usar tokens ```tsx // CORRECTO — roles
// canvas
// tarjeta

// secundario, pleno; no text-foreground/70 // estado = rol // INCORRECTO — colores de la paleta del framework

``` **Verificación:** `{comando que busca clases de color crudas y opacidades sobre tokens}` debe dar cero fuera de las excepciones de §1. --- ## 3. Tipografía [OBLIGATORIO] ### 3.1 Familias y carga | Rol | Familia | Pesos cargados | Cuándo | |---|---|---|---| | Interfaz | `{fuente}` | {400, 500, 600} | Todo lo que no sea display | | Display | `{fuente o «la misma»}` | {…} | Títulos protagonistas, cifras grandes | | Mono | `{stack del sistema}` | — | Código, identificadores técnicos | ### 3.2 Escala por rol **Un nivel se justifica por ROL, no por tamaño.** Si un diseño pide un tamaño que no está en la escala, la pregunta es qué rol nuevo cumple, no cuántos píxeles mide. Sin rol nuevo, se colapsa al nivel existente más cercano. | Rol | Clase o token | Familia | Dónde | |---|---|---|---| | Título de pantalla | `{…}` | {…} | Uno solo por pantalla; lo pinta el componente de encabezado, no un `

` a mano | | Título de sección | `{…}` | {…} | {…} | | Título de tarjeta | `{…}` | {…} | {…} | | Cuerpo | `{…}` | {…} | Texto general | | Secundario | `{…}` | {…} | Fechas, metadatos, ayuda | | Etiqueta overline | `{…}` | {…} | Encima de un título; el único uppercase permitido | ### 3.3 Pesos - Pesos permitidos: {…}. **Tope:** `{font-semibold}`; ningún componente usa `{font-bold}` salvo las excepciones de abajo. - {Pesos prohibidos: `font-thin`, `font-light`, …} - Excepciones al tope, con su razón: {…} ### 3.4 Reglas - {Fuente display} sólo en {dónde}. Nunca en botones, inputs ni controles. - Tracking negativo sólo en tamaños display. Uppercase sólo en overline. - Nunca cursivas en bloques largos. --- ## 4. Espaciado, radios y sombras [OBLIGATORIO] ### 4.1 Radios | Token | Valor | Uso | |---|---|---| | `{sm}` | {valor} | Badges, chips | | `{md}` | {valor} | Botones, inputs | | `{lg}` | {valor} | Tarjetas, diálogos | | `{xl}` | {valor} | Contenedores grandes, decorativo | **Coherencia:** dentro de un mismo componente, un solo radio. Contenedor grande, controles internos chicos. ### 4.2 Sombras | Token | Valor | Uso | |---|---|---| | `{shadow-sm}` … `{shadow-xl}` | `{valores}` | {…} | - Elevación en dark: superficie más clara, no sombra (§2.2). - Nunca sombras sueltas del framework fuera de estos tokens. ### 4.3 Espaciado común | Contexto | Valor | |---|---| | Padding de página | {valor} móvil / {valor} escritorio | | Gap entre tarjetas | {valor} | | Padding interno de tarjeta | {valor} | | Separación entre secciones | {valor} | | Título ↔ subtítulo de página | {valor} — **uno solo**; la mezcla 50/50 con otro valor es la deriva típica | --- ## 5. Temas [OBLIGATORIO si hay modo oscuro] ### 5.1 Arquitectura ``` {Mecanismo: clase en , atributo data-theme, media query} {Default: system / dark / light} {Persistencia: localStorage, cookie} {Cómo se evita el flash de tema al cargar} ``` ### 5.2 Cómo se adapta un color entre modos Consenso de la industria y de las dos guías de origen: **mismo tono, distinta luminosidad y saturación**. No se invierte, no se deja igual: se ajusta. | Aspecto | Light | Dark | |---|---|---| | Fondo del botón primario | Tono medio-oscuro | Tono más claro | | Saturación | Plena | Ligeramente menor | | Texto del botón | Claro sobre oscuro | Oscuro sobre claro (se invierte) | | Hover | Un poco más oscuro | Un poco más claro | | Elevación | Sombra | Superficie más clara | **Fórmula para un color de acción nuevo:** {p. ej. subir luminosidad +0.07, bajar croma −0.02 a −0.05, invertir el foreground}. ### 5.3 Qué NO hacer - NO dejar el mismo color en ambos modos. - NO invertir el color sin más: rompe la jerarquía y falla contraste. - NO resolver el dark mode a mano en el componente con `dark:{color crudo}`: se resuelve en el token. --- ## 6. Layout en niveles [OBLIGATORIO · APLICACIONES] ### 6.1 Nivel 1 — Shell con navegación principal ``` {Diagrama ASCII: navegación principal + cabecera + área de contenido} ``` **Reglas de separación:** {p. ej. sin bordes entre navegación, cabecera y contenido: la separación es por color y sombra}. **Navegación principal** - Ancho expandido / colapsado: {valores}. Persistencia: {…}. - **Ubicación por frecuencia, no por rol.** Uso diario o semanal → navegación principal. Onboarding u ocasional → configuración. Los procesos internos de un registro no van en la navegación: viven en su detalle. - **Visibilidad por permiso, dos estados.** Sin permiso → no se renderiza. Con permiso y sin implementar → visible con «Próximamente». No existe «visible pero inaccesible». - Ítem activo / inactivo: `{clases}`. **Navegación secundaria (sub-rutas)** - Escritorio: {columna vertical / tabs}. Por debajo de {breakpoint}: {tabs horizontales / el mismo panel en un drawer}. - **El ítem activo lleva superficie propia.** Si el canvas y el ítem activo comparten token, el activo es invisible y sólo lo distingue el color del texto. - Anidamiento: un solo nivel, siempre abierto, con línea guía que lo ate al padre. Un tercer nivel pide otro patrón. - Un panel de navegación es **de módulos, no de registros**: en el detalle de un registro invierte la jerarquía. Un registro navega con tabs. **Cabecera del shell:** {qué va a la izquierda, a la derecha; dónde viven el cambio de tema y de idioma}. ### 6.2 Nivel 2 — Foco (detalle, creación, flujos) Layout **sin navegación principal**. Dos piezas, y cada una hace una sola cosa: **Barra de navegación slim** (`{componente}`, altura fija `{48px}`) | Variante | Cuándo | Contenido | |---|---|---| | `back` | Detalle que vuelve a un listado | `← {Listado}` + título de contexto truncado + slot derecho opcional | | `exit` | Creación o edición | Título de contexto + `✕ Salir` | | `sequential` | Proceso de varios pasos | Título + indicador de paso + `✕ Salir` | Reglas: **sólo navegación** (ni título h1, ni migas, ni acciones); altura fija; «Volver» y «Salir» en variante `outline` para que se lean como clic; si la barra tiene «Salir», el formulario no repite «Cancelar». **Encabezado de página** (`{componente}`, dentro del contenido) ``` {Listado} > {Registro} migas {Título} h1, único nivel de título de pantalla [badge] [identificador] [flag] metadatos [acciones] derecha ``` **No se arma a mano.** Un `

` suelto con la clase correcta arregla hoy y deriva mañana; el componente trae el espaciado, la responsividad de las acciones y su skeleton espejado. ### 6.3 Nivel 3 — Drawer (sub-contenido subordinado) Para consulta o vista previa **subordinada** a un nivel 2. En vez de una ruta nueva que fuerce «volver y volver», un panel lateral que se abre desde el padre. | Drawer | Página (nivel 2) | |---|---| | Consulta o vista previa de sólo lectura | Formulario que el usuario debe llenar | | Necesita el contexto del padre visible | La tarea requiere foco completo | | Contenido subordinado a una entidad | Contenido independiente, con URL compartible | Reglas: ancho fijo en escritorio (`{600px}`), pantalla completa en móvil; scroll independiente; se cierra con X, overlay y Escape; carga perezosa al abrirse; sin barra de navegación propia. **El header de un drawer no lleva contenido de largo libre.** Título, badges y controles sí; un texto escrito por un usuario, no: crece sin tope y se come el alto del contenido. Cicatriz real: motivos de entre 600 y 3.000 caracteres en un header; ninguno cabía, y un `line-clamp` sólo habría tapado el síntoma. ### 6.4 Cuándo usar cada nivel | Nivel 1 | Nivel 2 | Nivel 3 | |---|---|---| | Listados, dashboards | Detalle de una entidad | Vista previa de sub-contenido | | Cualquier vista con navegación principal | Formularios de creación, procesos | Documentos relacionados, historial | **Regla de profundidad:** por debajo del nivel 2 **no se crea una ruta**; se usa un drawer. Si el drawer necesita URL compartible, el contenido no era subordinado y le toca nivel 2. ### 6.5 Detalle en dos columnas [OPCIONAL] Para detalles con tres o más pestañas de contenido: columna izquierda fija con identidad y acciones (`{300px}`, `sticky`), derecha flexible con las pestañas. Reglas: ancho fijo en la columna izquierda, no porcentaje; `min-w-0` en la derecha para que las tablas no la desborden; el banner de estado del registro va en la columna izquierda, visible en cualquier pestaña; en móvil colapsa a una columna. No usar para formularios de creación ni para detalles con una sola pestaña. --- ## 7. Móvil [OBLIGATORIO] ### 7.1 Estrategia Escritorio y móvil comparten datos y lógica; cambia la presentación por breakpoint. La navegación móvil **no es la de escritorio colapsada**. | Breakpoint | Rango | Navegación | Listas | Procesos | |---|---|---|---|---| | Móvil | `< {md}` | {barra inferior + drawer «Más» / menú} | Tarjetas | Stepper vertical colapsable | | Escritorio | `≥ {md}` | {navegación lateral} | Tabla completa | Stepper horizontal | Un solo breakpoint principal (`{md}`) para todas las variaciones de escritorio. ### 7.2 Navegación móvil [APLICACIONES] - {Barra inferior: cuántos slots, cómo se eligen (derivados de la misma fuente que la navegación de escritorio, nunca por rol a mano), qué pasa con el resto}. - **Cero pestañas deshabilitadas**: lo que no está disponible no se muestra. - Ítem activo / inactivo: `{clases}`. Alto mínimo: `{56px}`. ### 7.3 Listas: tabla en escritorio, tarjetas en móvil Las tablas **no scrollean horizontalmente en móvil**: se vuelven tarjetas con la información priorizada. ```tsx
…
{items.map(item => )}
``` Reglas: mostrar sólo nombre, tipo, estado y uno o dos datos clave; toda la tarjeta es el enlace; chevron como affordance; `min-w-0 flex-1` en los textos para que `truncate` funcione; la acción principal es tocar la tarjeta y las secundarias van en un menú, sin botón dividido (no hay ancho). ### 7.4 Objetivos táctiles y área segura - Todo interactivo en móvil: **≥ 44px**. Patrón: `h-11 w-11 md:h-{n} md:w-{n}`. - Elementos fijos al fondo: `pb-[env(safe-area-inset-bottom)]`, y el viewport con `viewport-fit=cover` para que `env()` funcione. - El `
` bajo una barra inferior compensa su alto: `pb-{20} md:pb-{4}`. ### 7.5 Patrones recurrentes | Elemento | Móvil | Escritorio | |---|---|---| | Acción primaria del encabezado | Ancho completo, 44px | Ancho natural | | Acciones secundarias | Colapsadas en un `⋯` | Botones nombrados | | Controles de vista (alcance, período) | Apilados, ancho completo, **nunca dentro del `⋯`** | En fila | | Filtros de formulario | Apilados, ancho completo | En fila, ancho fijo | | Sólo móvil / sólo escritorio | `md:hidden` | `hidden md:block` | Esconder un filtro detrás de un menú oculta el estado de lo que se está viendo. Un filtro se ve siempre. --- ## 8. Listas y tablas [CRECE CON EL PROYECTO · APLICACIONES] ### 8.1 Un cuerpo de filas, un encabezado Una tabla con variantes declara sus encabezados **una sola vez**. Si el cuerpo es común y cada variante declara el suyo, divergen en cuanto alguien agrega una columna, y el fallo es silencioso: las celdas se corren a la columna vecina y nada lanza error. Cicatriz real: ocho celdas contra siete encabezados durante semanas, sin que ningún test lo viera, porque todos afirmaban por texto y el texto estaba, sólo que en la columna equivocada. - Una columna condicional multiplica el riesgo. Un valor idéntico en todas las filas no es una columna: es un dato del encabezado de sección. - El `colSpan` de una fila separadora sale de la misma constante que las columnas. - **El test que sirve:** cada fila tiene tantas `` como `` hay en el ``, en todas las variantes. Una línea, y cubre la clase entera de bug. - Un `overflow-hidden` externo no impide el scroll horizontal: lo esconde a medias. El desborde se resuelve con anchos declarados. Y `table-fixed` no comprime lo que no hace wrap: lo deja salirse encima de la celda vecina. ### 8.2 Tipografía de listas Tres tamaños, y sólo tres. Aplican a la tabla y a las tarjetas móviles que la reemplazan: son la misma lista en dos anchos. | Rol | Tamaño | Dónde | |---|---|---| | Encabezado de columna | `{12px}` | Lo trae el componente; no sobreescribir | | Texto primario (nombre) | `{14px}` | Lo trae la celda; el nombre sólo agrega `font-medium` | | Secundario (id, fecha, hora) | `{12px}` | Lo pone el consumidor, en `muted-foreground` | Nada por debajo del tamaño secundario en una fila. Fecha con hora: la hora lleva **menos peso, no menos tamaño**. La columna de identidad lleva `min-w-*` en el encabezado para que el navegador no le quite ancho a favor de columnas de una palabra. ### 8.3 Acciones de fila Implementadas por `{componente}` en la última columna, **sticky** a la derecha, con fondo opaco y el hover de la fila repintado como overlay dentro de la celda fija (una columna sticky opaca borra los tintes de su fila; un color por capa). | Tipo de lista | Etiqueta de la acción principal | |---|---| | Negocio | «{Revisar}» | | Configuración | «{Editar}» o el verbo real | ### 8.4 Barra de la tabla Toda lista filtrable lleva su barra, en la misma tarjeta que la tabla, como header con borde inferior. ``` [🔍 Buscar…] [filtros •] [orden] [columnas] [⚙] [Acción primaria ▾] ``` 1. **El buscador siempre visible**, a la izquierda, con debounce. No se colapsa en un icono: el usuario llega con un nombre en la mano. 2. **Los controles secundarios son iconos** sin etiqueta. El que tiene estado distinto del default lleva un punto de acento; un indicador que nunca se apaga es decoración. Con dos controles la barra admite etiquetas, por contenedor y no por viewport: el mismo card mide distinto con la navegación abierta o cerrada. 3. **Sólo la acción primaria lleva texto.** Una por barra. La segunda va dentro del desplegable de la primera o en un `⋯`. **Lo que está fuera no se repite dentro.** Un icono en la barra es acceso rápido; el panel de configuración es donde vive el resto. Si un control está en la barra, su sección no se lista otra vez en el panel: son dos puertas al mismo sitio y el usuario deja de predecir qué abre cada cosa. Toda duplicación deliberada se declara con su razón. **Alcance vs. filtros, dos zonas.** El alcance (período, área) va en la cabecera y siempre visible: da denominador a la cifra. Lo aplicado por el usuario va en una fila de chips bajo la barra, que **desaparece cuando no hay ninguno**. El período es encuadre, no filtro: chip permanente sin «×». **Cuando no hay datos hasta ejecutar** (un informe, una consulta parametrizada) no es una tabla filtrada: es un formulario de consulta siempre visible con su botón de ejecutar. Sin barra ni badge de filtros: un badge implica refinar un conjunto que todavía no existe. **Las listas de configuración con dos o tres controles conservan el patrón simple.** Un panel de configuración de vista para dos filtros agrega peso sin agregar nada. Es una excepción declarada, no deuda. ### 8.5 Paginación Cinco reglas duras: 1. **Si la lista pagina, todo filtro se resuelve en el servidor.** Filtrar en cliente lo que el servidor ya recortó da un total que no corresponde a ninguna consulta real, y el usuario ve un número plausible. 2. El contador y el paginador leen el `total` del servidor, nunca `items.length`. 3. Dos listas en la misma URL = dos juegos de parámetros, y la pestaña activa también va en la URL: paginar es navegar. 4. Filtrar reinicia la página de **su** lista, no la de la otra. 5. Vacío por filtro ≠ vacío por falta de datos: el estado vacío lo dice. ### 8.6 Estados vacíos Icono del módulo, mensaje, y «Limpiar filtros» si hay filtros activos. **La acción de crear va dentro del estado vacío.** Si el único botón para crear vive en el componente de lista y ese componente sólo se monta con datos, el recurso queda inalcanzable: no hay cómo crear el primero. Cicatriz real: una cuenta nueva nacía sin catálogo y su pantalla no ofrecía ninguna acción. Aplica a todo recurso que el usuario construye desde cero. ### 8.7 Celdas - **Etiqueta, nunca el valor del enum.** `OPEN` se ve como «Abierta». Si la tabla la arma el servidor (y viaja igual a pantalla y a exportación), la etiqueta se resuelve allá con respaldo al valor crudo, nunca a vacío: un enum nuevo sin etiqueta debe verse raro, no desaparecer. - Si el filtro nombra el valor, la celda usa el **mismo** texto. - Un identificador largo (UUID) se muestra **abreviado** con el completo en un popover y botón de copiar. El recorte es de render, no del dato: la exportación lleva el valor entero. - Columna de fecha: dos líneas (fecha, hora), `whitespace-nowrap`, formateada por la librería de i18n, nunca con métodos nativos sin locale. ### 8.8 Skeletons Todo estado de carga se construye con los **skeletons compartidos** del proyecto (`{ruta}`), no con barras sueltas. - **Reservar la altura de línea del texto real**, no la altura visual de la barra: un título de 22px ocupa una línea de 28px. Una barra suelta desalinea todo lo de abajo. Referencia: `{tamaño}` → `{alto de línea}`. - **Aparición retrasada (200 ms)** para todo skeleton que ocupe un bloque completo: si el servidor responde antes, el contenido entra directo. 200 y no 300: errar por arriba se lee como clic muerto. **No** aplica a un fragmento dentro de contenido ya pintado: ahí el retraso deja un hueco que colapsa. El criterio: ¿el skeleton reemplaza todo lo que va a ocupar ese espacio, o convive con contenido que el usuario ya lee? - **La deriva no se ve en review.** El skeleton se escribe con la estructura de ese día; los cambios posteriores agregan una columna y nadie abre el archivo hermano, porque nunca caen juntos en el diff. Medido: 6 de 47 skeletons declaraban menos columnas de las reales. Lo detecta un script: `{comando que compara columnas del skeleton contra encabezados reales}`. - El skeleton replica la **estructura**, no el detalle: un badge, un avatar y un texto en la misma celda son una barra. - Color: el rol `{skeleton}`, visible sobre el canvas y sobre la tarjeta. --- ## 9. Formularios [CRECE CON EL PROYECTO · APLICACIONES] ### 9.1 Obligatoriedad: depende de la proporción | Formulario | Patrón | |---|---| | Largo, con la mayoría de campos requeridos (≈80 %+) | Marcar los **opcionales** con «(opcional)» junto al label | | Corto, mitad y mitad | Marcar los **requeridos** con `*` | Si casi todo es requerido, el ruido de `*` en cada campo supera su beneficio. ### 9.2 Selección - `Select` hasta `{15}` opciones; por encima, combobox con búsqueda. - Un combobox dentro de un diálogo necesita {sus arreglos: rueda del mouse, `min-w-0` en el grid del diálogo, `w-full` en el ítem}. Ver §16. - Cascadas (país → región → ciudad): cambiar el padre limpia a los hijos, en el estado local y en los cambios pendientes del autoguardado. ### 9.3 Estado de guardado Con guardado manual, el header de la tarjeta muestra «Cambios sin guardar» / «Guardado ✓» después del primer guardado exitoso. El botón se deshabilita cuando no hay cambios. **Crítico:** tras guardar, resetear el formulario a los valores actuales para que el flag de cambios vuelva a falso. ### 9.4 Solo lectura Un registro que ya no se edita muestra un banner informativo al inicio del contenido, con copy específico por estado, en `muted` con borde sutil: no es un error y no usa colores de alerta. ### 9.5 Formularios por secciones Acordeón con sólo la primera sección abierta; badge de completitud por sección (número → check al llenar los obligatorios); texto «X de Y obligatorios»; «Continuar» al final de cada sección; al fallar la validación se abren las secciones con error. ### 9.6 Campos condicionales La visibilidad por dependencia (`{depends_on}`) se evalúa en **un solo lugar** y se usa en render y en el conteo de progreso: sólo los campos visibles cuentan. ### 9.7 Autoguardado Debounce de `{30 s}`; envío incremental de lo que cambió; indicador de estado (inactivo / guardando / guardado / error); flush antes de enviar; recuperación en error (los cambios vuelven a la cola). ### 9.8 Archivos El campo de archivo muestra su estado (subiendo / subido: nombre / error visible) y se deshabilita durante la subida. Un fallo silencioso es un dato que el usuario cree guardado. --- ## 10. Movimiento [OPCIONAL] ### 10.1 Duraciones y curvas son tokens | Token | Duración | Uso | |---|---|---| | `{micro}` | {150 ms} | Hover, opacidad de controles. Default de toda transición | | `{fast}` | {200 ms} | Iconos que rotan, confirmaciones breves | | `{normal}` | {250 ms} | Entrada y salida de contenido | | `{medium}` | {300 ms} | Paneles, drawers, ancho de la navegación | | `{slow}` | {500 ms} | Avance de un proceso que el usuario no controla | Las curvas se llaman como las del framework CSS: un nombre propio dejaría dos sistemas conviviendo. Un `duration-200` suelto o un `0.3s` en un `style` inline {lo rechaza el check / es deuda}. ### 10.2 Elegir por intención | Utilidad | Qué comunica | Duración · curva | |---|---|---| | `{motion-state-change}` | Algo cambió de estado | fast · out | | `{motion-reveal}` / `{motion-dismiss}` | Contenido que aparece / se va | normal · out | | `{motion-progress}` | Avance de proceso | slow · out | | `{motion-lateral}` | Algo se mueve sin entrar ni salir | medium · in-out | ### 10.3 Movimiento reducido Bajo `prefers-reduced-motion: reduce` las duraciones van a `0ms` **en el token**, no componente por componente. Las curvas no se tocan: sobra el tiempo, no la forma. Si hay una librería de animación aparte, tiene su propia política central; dos mecanismos porque son dos motores, no dos políticas. El retraso del skeleton se conserva: es visibilidad, no movimiento. ### 10.4 Principios 1. Cada animación informa, confirma o mantiene la orientación. 2. Sólo `transform` y `opacity`. Nunca `height`, `width`, `top`, `left`. 3. No animar inputs ni conteos de datos: el dato real se lee de inmediato. 4. No superar `{slow}` en acciones del usuario. 5. Una animación no justifica hidratar un componente: si no hay estado, va sin framework. 6. Nunca scroll-jacking. --- ## 11. Identidad y pantallas expresivas [OPCIONAL] ### 11.1 Marca - Activos: `{ruta}` — {logo completo / símbolo, versiones por modo}. - Reglas: {dónde va cada versión; tamaño mínimo; espacio de respeto; nunca rotar, deformar ni recolorear}. - **Anti-referencia:** {qué no queremos parecer}. Si una captura permite adivinar la librería de componentes, falta capa de identidad: tipografía propia, escala propia, superficie propia. ### 11.2 Dónde se permite ser expresivo | Expresivo | Funcional | |---|---| | Login, registro, recuperar contraseña | Dashboard y listados | | Onboarding de primer ingreso | Formularios de creación | | Páginas de error, mantenimiento | Detalle de registros | Regla: formulario de 1 a 3 campos **y** necesidad de contexto emocional → patrón expresivo. Formularios complejos o datos tabulares → layouts funcionales. ### 11.3 Recetas - **Colores de marca fijos:** no cambian entre modos porque se pintan sobre fondos de marca. Sólo en decorativo; nunca en UI funcional. - **Vidrio (glass):** sólo sobre gradientes o imagen (sobre superficie plana no se nota); máximo 4 o 5 formas por panel; siempre `aria-hidden`; fallback opaco sin `backdrop-filter`; nunca puramente decorativo, siempre contiene algo. - **Gradientes:** máximo 3 paradas; el color dominante es el primario; nunca en elementos funcionales. - **Animación decorativa:** duración mínima `{7 s}`, `ease-in-out`, amplitud máxima `{20px}`, duraciones variadas para que no se sincronicen, y nunca en elementos funcionales. El texto de estas pantallas es de la skill `ux-writer`: esta guía no lo define. --- ## 12. Iconografía [OBLIGATORIO] **Librería:** `{nombre}` — única librería de iconos. Nombre **canónico** de cada icono, no el alias deprecado: el alias desaparece cuando la librería lo retire. | Contexto | Tamaño | |---|---| | Navegación | `{clases}` | | Metadatos de tarjeta | `{clases}` | | Acciones | `{clases}` | | Estado vacío | `{clases}` | - Iconos decorativos: `aria-hidden="true"`. Iconos de acción sin texto: `aria-label`. - Grosor de trazo: `{valor}`, uno solo. - NO mezclar librerías. --- ## 13. Accesibilidad [OBLIGATORIO] ### 13.1 Contraste Criterio y herramienta en §0. Excepciones aceptadas en §2.5. ### 13.2 Foco visible Todos los interactivos: `{clases de foco}`. Nunca se remueve el outline globalmente. ### 13.3 ARIA | Patrón | Uso | |---|---| | `aria-label` | Botones de sólo icono | | `aria-current="page"` | Enlace de navegación activo | | `aria-hidden="true"` | Iconos y formas decorativas | | `aria-describedby` + `aria-invalid` | Inputs con error | | `role="alert"` | Errores dinámicos | | `role="status"` | Indicadores de carga | | `aria-live="polite"` | Progreso de un proceso | ### 13.4 Reglas - El color nunca es el único indicador: icono, texto o forma lo acompañan. - Landmarks: un `
`, `
`, `