DESIGN-SKILL.md
El lenguaje de diseño de Open Analytics en un solo archivo, escrito para agentes de IA y los humanos que los dirigen. Cada valor aquí se envía en el producto: códigos de color hex, constantes de muelle, radios, curvas de aceleración. Pega este archivo en el contexto de tu agente (o colócalo en tu repositorio como archivo de reglas) y solicita pantallas "en el estilo de OA".
Esta es la edición compilada de un solo archivo, hecha para pegar. El mismo lenguaje
también se envía en este repositorio como un skill de agente adecuado
(skills/oa-design/: recetas por componente con el código fuente completo tipado
incrustado) y como CLI (npx getopen-design add <component>); prefiere esos
para Claude Code y otros agentes conscientes de skills.
El aspecto en una frase: superficies blancas con esquinas de curvatura continua, descansando sobre un escenario gris silencioso, dibujadas en una sola tinta, movidas por un solo muelle.
1. Las diez reglas
- Una tinta, todo derivado. Todo el sistema neutral es un solo color mezclado con transparencia en porcentajes fijos. Nunca elijas un segundo gris.
- Dos capas, y el espacio entre ellas es la página. Las superficies son un marco blanco que sostiene un inserto gris hundido; las secciones son placas y el fondo de página entre ellas es el único divisor. Sin reglas horizontales.
- Curvaturas continuas (squircles) para superficies, píldoras para acciones. Las tarjetas tienen esquinas de curvatura continua; todo lo que se puede hacer clic y no es una tarjeta es una píldora.
- Una familia de muelles. Siete muelles nombrados cubren todo el producto. No inventes un octavo.
- El chrome nunca espera. El layout y los títulos se renderizan instantáneamente; solo los datos cambian desde un esqueleto de píxeles coincidentes, llegando por desenfoque, no por aparición.
- El peso se detiene en 500. Sin negrita en ninguna parte. La jerarquía proviene del tamaño, el color y el espaciado.
- Un acento, gastado en un solo lugar. Un azul único para acciones primarias y la línea primaria del gráfico; los colores semánticos son tintes de texto, no rellenos.
- Los estados obtienen píldoras, los eventos terminan solos. Las condiciones permanentes renderizan tiras no descartables que viven exactamente mientras dura el estado; los resultados únicos son toasts que se retiran solos.
- El texto es parte del diseño. Mayúscula inicial; los botones dicen lo que sucede; los errores nombran la causa y la salida, sin culpa.
- Suelo de calidad, siempre. Anillos de enfoque,
role="status", decoracionesaria-hidden,prefers-reduced-motion, y sin desplazamiento horizontal de página.
2. Tokens
Claro
:root {
--ink: #292929;
--background: #f6f6f6; /* el escenario gris entre placas */
--card: #ffffff;
--popover: #ffffff;
/* neutrales derivados de ink: los porcentajes SON el sistema */
--border: color-mix(in srgb, var(--ink) 12%, transparent);
--input: color-mix(in srgb, var(--ink) 14%, transparent);
--accent: color-mix(in srgb, var(--ink) 5%, transparent); /* lavado hover */
--muted: color-mix(in srgb, var(--ink) 5%, transparent);
--muted-foreground: #6d6d6d;
--foreground: var(--ink);
--primary: #305dde; /* el único acento */
--primary-foreground: #ffffff;
--ring: #3ba6f1;
--secondary: #e9e9e9; /* controles grises planos, sin borde */
--secondary-foreground: var(--ink);
/* semánticos, como tintes de TEXTO sobre claro (escala Tailwind, fila 700) */
--destructive: var(--color-red-500);
--destructive-foreground: var(--color-red-700);
--success-foreground: var(--color-emerald-700);
--warning-foreground: var(--color-amber-700);
--info-foreground: var(--color-blue-700);
--chart-1: #296FF0; /* línea primaria del gráfico */
--radius: 0.625rem;
}
¿Necesitas un divisor más fuerte? Aumenta el porcentaje de ink (el producto usa solo 4, 5, 10, 12, 14). Los colores semánticos van en palabras y puntos de 8px, nunca en rellenos de panel.
Oscuro
Misma receta, re-entintada, con dos cambios sistemáticos: los porcentajes de línea fina aumentan en 2 puntos, y el texto semántico pasa de la fila 700 a la fila 400.
.dark {
--ink: #ededed;
--background: #191919;
--card: #212121;
--popover: #242424;
--border: color-mix(in srgb, var(--ink) 14%, transparent);
--input: color-mix(in srgb, var(--ink) 16%, transparent);
--muted-foreground: #969696;
--secondary: #2e2e2e;
--primary: #296FF0;
--destructive-foreground: var(--color-red-400);
--success-foreground: var(--color-emerald-400);
}
Radio
Una base, multiplicada; nunca un radio hecho a medida:
--radius: 0.625rem; /* sm ×0.6, md ×0.8, lg ×1, xl ×1.4,
2xl ×1.8, 3xl ×2.2, 4xl ×2.6 */
rounded-full está reservado para píldoras. Las superficies de curvatura continua llevan sus propios radios
más grandes (sección 4).
Tipografía
- Inter Tight, variable, pesos 300 a 500, autoalojado (las CDN de fuentes rompen las compilaciones; mantén el woff2 en el repositorio).
- Geist Mono para
code, kbd, samp, pre, teclas y fragmentos. - El techo de peso es la identidad: nada se renderiza más pesado que 500.
La jerarquía proviene del tamaño,
text-foreground/80vstext-muted-foreground, y el espaciado. - Encabezados:
font-medium tracking-tight; los titulares de marketing añadentext-wrap: balance. - Datos:
tabular-numsen cualquier número en una columna o que se actualice en su lugar. - La voz predeterminada de la interfaz es 14px; las líneas secundarias 12px, con
leading-5/leading-6.
Sombras
La elevación es binaria:
/* tarjeta en reposo */ box-shadow: 0 1px 2px rgba(0,0,0,0.06);
/* chrome flotante */ box-shadow: 0 1px 2px rgba(0,0,0,0.08),
0 8px 24px rgba(0,0,0,0.08);
Disciplina del fondo
body es blanco (el overscroll muestra el body; el gris asomando detrás de una pantalla
blanca se lee como una página diferente). Las pantallas que quieren el escenario gris pintan
su propio fondo. La raíz obtiene overflow-x: clip.
3. Layout
Placas. Las secciones son paneles autocontenidos; el fondo de página que se muestra
entre ellas es el único límite. Los espacios verticales generosos
(gap-12 sm:gap-20 marketing, gap-6 en la app), el padding lateral más ajustado
(px-4 sm:px-6). Los hermanos se espacian por el gap del padre, nunca por
márgenes por hijo. Sin <hr>, sin border-b entre secciones.
Anchos por ámbito: marketing 76rem (6.5xl), páginas del sitio de la app max-w-6xl,
pantallas de listas/selectores max-w-5xl, superficies de lectura ~65ch. Centrado, mx-auto.
Anatomía de la página de la app:
encabezado fijo (h-14) → contenedor pt-14
píldora flotante opcional (fixed top-16, centrada)
<main class="px-4 pb-36 pt-8 sm:px-6 bg-white">
fila de título: h1 text-xl font-medium tracking-tight + UN control a la derecha
tiras de aviso opcionales
placas: flex flex-col gap-6
La fila de título contiene exactamente un control. pb-36 mantiene el contenido fuera del
borde de la ventana gráfica.
Cuadrículas: grid gap-3, columnas responsivas (grid-cols-2 sm:grid-cols-4 para
estadísticas). Las tarjetas comparten una altura de contenido fija y se desplazan internamente; la línea base
de la cuadrícula nunca se rompe.
Responsive: diseñar a 390px y 1280px. Las parejas horizontales se apilan debajo de
sm: con la acción al final. El contenido ancho se desplaza en su propio
contenedor overflow-x-auto; la página nunca se desplaza lateralmente.
Configuración: carril de pestañas estrecho + columna de contenido de paneles de sección; cada sección
se abre con un bloque de encabezado (título + una oración en gris, acción única opcional
a la derecha). Enlace profundo a pestañas mediante ?tab=<id>.
4. Componentes
La superficie de curvatura continua
Esquinas de curvatura continua a través de un clip-path CSS shape() parametrizado por
dos variables, con corner-shape: squircle donde sea compatible:
.squircle {
border-radius: 26px;
--card-clip-radius: 14px;
--card-clip-handle: 2.25px;
clip-path: shape(
from var(--card-clip-radius) 0px,
line to calc(100% - var(--card-clip-radius)) 0px,
curve to 100% var(--card-clip-radius)
with calc(100% - var(--card-clip-handle)) 0px / 100% var(--card-clip-handle),
line to 100% calc(100% - var(--card-clip-radius)),
curve to calc(100% - var(--card-clip-radius)) 100%
with 100% calc(100% - var(--card-clip-handle)) / calc(100% - var(--card-clip-handle)) 100%,
line to var(--card-clip-radius) 100%,
curve to 0px calc(100% - var(--card-clip-radius))
with var(--card-clip-handle) 100% / 0px calc(100% - var(--card-clip-handle)),
line to 0px var(--card-clip-radius),
curve to var(--card-clip-radius) 0px
with 0px var(--card-clip-handle) / var(--card-clip-handle) 0px,
close
);
corner-shape: squircle;
}
@media (min-width: 640px) {
.squircle { border-radius: 50px; --card-clip-radius: 20px; --card-clip-handle: 3px; }
}
La minitarjeta
curvatura continua exterior: bg-card, border, p-1, sombra en reposo
├── tira de encabezado (pb-2 pl-3.5 pr-2 pt-1.5):
│ ícono atenuado de 16px + título text-sm font-medium text-foreground/80
│ chip opcional en la línea central del título
│ "Ver todo ›" (text-xs muted; hover: lavado accent, flecha se desliza 2px)
└── curvatura continua interior: bg #f6f6f6, border, rounded-[22px] sm:rounded-[44px],
ALTURA FIJA (h-44 = cinco filas de 32px), overflow hidden
Las tarjetas nunca crecen con los datos; las listas más largas se desplazan internamente. El desbordamiento muestra un
chip de galón de 24px en la parte inferior derecha, que se mueve y:[0,2.5,0] (bucle easeInOut de 1.4s), desapareciendo
(0.18s, escala 0.7) al final del desplazamiento. Filas: 32px, hover de lavado accent a sangre completa,
valores tabular-nums.
Botones
Píldoras, font-medium, presionados físicamente:
active:translate-y-px active:scale-[0.98].
- primario: azul acento con un bisel (background/border color-mixed hacia
un índigo profundo,
inset 0 1px 0 rgba(255,255,255,0.22)arriba, inset oscuro abajo); hover aclara hacia el primary puro. - secundario:
--secondaryplano, borde transparente, hover mezcla 5% ink. El caballo de batalla. - fantasma: solo texto atenuado. destructivo: relleno rojo, solo paso de confirmación.
- Tamaños h-9 / h-8 / h-7; el chrome de la app usa principalmente los pequeños.
- Cargando mantiene la etiqueta y añade un spinner de anillo de 14px
(
border-2 border-current border-t-transparent); el ancho nunca salta. focus-visible:ring-[3px]en--ring/50. Nunca se elimina.
Paneles desplegables (menús, selects, conmutadores)
Píldora desencadenante → panel flotante (sombra flotante, borde, blanco, rounded-2xl+), entra con muelle en PANEL (550/38) con pequeño desplazamiento y/escala, salida más rápida. Elementos rounded-lg de 32px con hover de lavado accent; el resaltado activo es un elemento compartido que viaja (layoutId o muelles de ancho/alto), nunca repintados por elemento. Cerrar al hacer clic fuera y con Escape.
Modales
El fondo se desvanece ~0.15s; el panel entra con muelle en POP (400/26) desde
opacity 0, scale .96, sale a 380/28. Los diálogos de varios pasos mantienen un panel
cuya altura se anima al tamaño medido de cada paso (LAYOUT 550/40 +
ResizeObserver) mientras los pasos se deslizan (x: ±12px, popLayout, 0.16s
easeOut fades). Pie: tira h-12, Atrás (secundario xs) + primario xs, alineado a la derecha.
Barra de pestañas
La píldora activa es una ventana fija; las etiquetas se deslizan detrás de ella. La etiqueta entrante entra desde la dirección de desplazamiento al 110% de su propio ancho; la saliente sale a 130px fijos en la otra dirección (un porcentaje se queda corto cuando la píldora crece hacia una palabra más larga). El ancho/alto de la píldora sigue la etiqueta medida en muelles LAYOUT. Opacidad completa durante todo el proceso; la máscara hace el trabajo.
Esqueletos
- El chrome nunca espera; solo el área de datos es esqueleto.
- Coincidencia de píxeles: una barra se sienta dentro de la misma ranura de line-height que el texto al que
representa (
flex h-6 items-centerenvolviendo una barrah-3); el intercambio no mueve nada. - Los datos llegan por enfoque: una animación de una sola vez desde
opacity ~0.4 + blur(4px)a nítido. Una animación, no una transición (el contenido se monta ya final; una transición no tiene valor anterior). Manténfilter: blur(0)después.
Barra: rounded bg-muted-foreground/15, barrido lento de fondo-position
(2s linear infinite, desplazamiento inicial -1s para que las barras estén desfasadas).
Avisos, estados vacíos, la píldora flotante
Tira de aviso encima del contenido que explica:
<div class="flex flex-col gap-2 rounded-2xl border border-border bg-card
px-4 py-3 sm:flex-row sm:items-center sm:justify-between"
role="status">
<p class="text-sm leading-6 text-muted-foreground">
<span class="font-medium text-foreground">Aún no hay eventos.</span>
Si el fragmento está activo, navega por tu sitio y captaremos la primera
página vista en segundos.
</p>
<button class="…píldora secundaria…">Verificar la instalación</button>
</div>
Una afirmación en negrita, una oración en gris, una acción secundaria; se apila debajo de
sm:; vive exactamente mientras dura el estado; sin descarte.
Píldora flotante para condiciones permanentes en toda la app: fixed top-16, centrada,
rounded-full bg-card py-1.5 pl-3 pr-4 text-xs font-medium, sombra flotante,
punto de estado de 8px (pulsante cuando es urgente), entra con muelle en BANNER (400/30) desde
y:-8, sale 0.15s. Toda la píldora enlaza a donde se resuelve el estado.
Las píldoras de estado nunca se descartan a sí mismas; las píldoras de evento se retiran después de ~6s.
Toasts
Menos de 0.35s, easing cubic-bezier(0.5, 1, 0.89, 1):
- Éxito: pulso
scale 1 → 1.025 → 0.99 → 1(0.32s). - Error: sacudida
x 0 → -3 → 3 → -3 → 0px (0.28s).
Alterna copias de fotogramas clave pares e impares para que las repeticiones se reinicien.
5. Movimiento
| Nombre | Valor | Usado para |
|---|---|---|
| PANEL | 550 / 38 | desplegables, menús, paneles, alternancias |
| LAYOUT | 550 / 40 | altura/ancho medido, píldoras deslizantes |
| POP | 400 / 26 | entrada de modal |
| POP_EXIT | 380 / 28 | salida de modal |
| BANNER | 400 / 30 | píldoras flotantes, banners de página |
| FLICK | 900 / 50 | micromovimientos de iconos |
| CHART | 300 / 25-30 | tooltips de gráficos, crosshair |
Microdesvanecimientos: 0.1s a 0.18s easeOut. Nada en el chrome de la app se anima más de 0.2s; si se siente lento, baja el damping, no alargues la duración.
Movimientos característicos: coreografía de altura medida (un panel constante, pasos deslizándose), el resaltado viajero (un elemento activo compartido, nunca repintados por elemento), asimetría de entrada/salida (las salidas siempre más rápidas), hover solo como cambio de color (más un deslizamiento de flecha de como máximo 2px; las tarjetas nunca se elevan o escalan), y como máximo un bucle ambiental pequeño por pantalla.
Reglas: AnimatePresence para todo lo que se desmonta; respeta
prefers-reduced-motion (las revelaciones colapsan a visible, los bucles se detienen); nunca
animes un layout no medido; nunca leas el reloj de pared durante el renderizado (suscríbete
a un store tickeante en intervalos de minutos).
6. Páginas de aterrizaje
Mismos tokens, s
…
[truncado para traducción]