Gastro Admin Panel — Design System
Silicon Valley Standard 2026 · ACS v2 Legible por humanos y agentes IA. Cada sección responde "cómo", "por qué" y "cuándo". Aplicación:
apps/gastro/frontend/admin-panel/Actualizado: 2026-05-06
0. Cómo leer este documento (para agentes IA)
Estructura de cada sección: - Qué existe — referencia exacta al código fuente - Cómo usarlo — snippet copiable y ejecutable - Reglas — qué se puede y qué no se puede hacer - Por qué — la razón de la decisión
Para implementar cualquier componente nuevo: 1. Leer §2 (tokens) — elegir los tokens semánticos correctos 2. Leer §4 (componentes) — ver si ya existe algo reutilizable 3. Leer §6 (reglas) — verificar que no se violan restricciones 4. Leer §7 (patrones) — elegir la animación correcta para el contexto
1. Filosofía de Diseño
1.1 Concepto: "The Command Center"
El admin panel es una herramienta de precisión para el dueño del restaurante. La estética está inspirada en:
| Referente | Qué tomamos |
|---|---|
| Linear | Cursor glow en cards, skeleton shimmer, dark surfaces |
| Vercel | Dot grid background, tipografía Inter apretada, tokens semánticos |
| OmniCortex | Glassmorphism, btn-premium animado, accent indigo→violet |
Psicología de color: Deep dark (#06060a) transmite control y precisión. El acento índigo-violeta (#6366f1 → #8b5cf6) evoca tecnología de alta gama sin agresividad.
1.1-A Estándares de Diseño de Referencia Mundial
Este design system está construido sobre los hombros de los sistemas más maduros de la industria. A continuación se documenta qué principio tomamos de cada uno y cómo se manifiesta en el código.
Tier 1 — Fundamentos Estructurales
| Sistema | Empresa | Año | Principio adoptado | Dónde se ve en este codebase |
|---|---|---|---|---|
| Material Design 3 | 2021 | Elevation como comunicación de jerarquía. Surface tiers (surface-1 → surface-3) no son estéticos — indican profundidad de contenido |
tokens-admin.css: --surface (nivel 0), --surface-2 (nivel 1), --surface-3 (hover/active) siguen el mismo modelo de elevation |
|
| Human Interface Guidelines | Apple | 2023 | Touch targets mínimos 44×44px. Feedback táctil inmediato. Sin estados de carga vacíos — siempre skeleton o shimmer | Todo botón tiene min-h-[44px]. Skeletons en ProductCard y CategoryCard antes de que lleguen los datos |
| Fluent Design 2 | Microsoft | 2022 | Acrylic/glassmorphism como indicador de capas flotantes (no decorativo). El blur solo aparece en elementos que flotan sobre contenido | glass-panel en Sidebar solo. Drawers y modals usan glass-panel. Nunca en elementos inline |
| Polaris | Shopify | 2019 | Design para comerciantes, no para diseñadores. Acciones primarias siempre visibles. Destrucción requiere confirmación explícita — nunca en un solo click | Botón delete muestra estado confirmDelete: boolean inline. Nunca window.confirm(). Siempre dos pasos |
| Spectrum | Adobe | 2020 | Tokens semánticos como contrato entre diseño e implementación. Los primitivos no se usan directamente — solo a través de aliases semánticos | Arquitectura de 3 capas: primitivos (Layer 1) → semánticos (Layer 2) → componentes (Layer 3). Idéntica a Spectrum |
Tier 2 — Experiencia e Interacción
| Sistema | Empresa | Principio adoptado | Manifestación en código |
|---|---|---|---|
| Carbon Design System | IBM | Densidad de información alta sin sacrificar legibilidad. Tablas y listas densas son válidas si el contexto lo requiere | Sidebar con items compactos (py-2). Listas de productos con información densa por fila |
| Atlassian Design System | Atlassian | Feedback de operaciones de larga duración. Toast/notification system centralizado para no perder el contexto del usuario | uiStore.notify() centralizado. Toast aparece sin interrumpir el flujo — no bloquea la UI |
| Lightning Design System | Salesforce | Accesibilidad como requisito de producción, no como afterthought. WCAG AA es el mínimo, no el objetivo | Ratios de contraste verificados: text sobre surface ≥ 7:1, text-muted ≥ 4.5:1. Documentados en §6 |
| Primer | GitHub | UI orientada a desarrolladores: estados explícitos, sin ambigüedad. Cada estado del sistema (loading, empty, error, success) tiene representación visual | Cada useQuery renderiza loading skeleton → data → error state. Sin estados undefined |
| Pajamas | GitLab | Open source first: tokens documentados públicamente, sin magic values. Todo valor debe tener nombre y razón | Layer 1 tiene nombre semántico (--p-gray-950) incluso los primitivos. Cero hex directos en componentes |
Tier 3 — Tendencias SV 2024-2026 (Estética Contemporánea)
| Producto | Empresa | Tendencia adoptada | Implementación |
|---|---|---|---|
| Linear | Linear App | Dark UI de alta densidad. Cards con cursor tracking glow. Sin bordes en reposo — se revelan en hover | use-card-glow.ts + .glow-card::before. Bordes en border-border/30 (30% opacity en reposo) |
| Vercel Dashboard | Vercel | Dot-grid como textura de fondo. Comunica "infraestructura" sin ilustraciones | .dot-grid en DashboardLayout. radial-gradient que desvanece en los bordes |
| Raycast | Raycast | Motion con propósito: cada animación comunica relación espacial, no solo decora | slideInFromRight en Drawers (vienen de la derecha = son detalles de lo seleccionado). fadeIn en modals (son contextuales, sin dirección) |
| Figma | Figma | Panel lateral como herramienta de trabajo, no como menú. El sidebar tiene estado persistente y comunica la sección activa con precisión | Sidebar con activeRoute highlight. Estado de colapso guardado en uiStore |
| Stripe Dashboard | Stripe | Tipografía como jerarquía. Sin decoraciones innecesarias — el peso y el tamaño del texto comunican importancia | Escala tipográfica de 7 niveles (§3). font-semibold solo para valores críticos (precio, estado) |
Por Qué Esta Combinación
Estructura → Material Design 3 (elevation, surface tiers)
Accesibilidad → Apple HIG + Salesforce Lightning (touch, WCAG)
Tokens → Adobe Spectrum (3-layer architecture)
UX comercial → Shopify Polaris (merchant-first, safe destructive)
Estética → Linear + Vercel + Raycast (SV dark premium)
Motion → Raycast + Framer Motion (purposeful animation)
El resultado es un sistema que puede operar en contextos B2B exigentes (densidad de Atlassian, accesibilidad de Salesforce) con la estética de las herramientas que usan los propios equipos de producto de Silicon Valley.
Qué NO tomamos (y por qué)
| Patrón | Sistema origen | Razón del rechazo |
|---|---|---|
| Hamburger menu oculto | Muchos sistemas móviles | El dueño opera en crisis — la navegación debe ser siempre visible |
| Skeleton sin límite de tiempo | Cualquiera | Si la API no responde en 10s, mostrar error — no skeleton infinito |
| Tooltips como única fuente de información | Material Design antiguo | En móvil no hay hover. Todo label debe ser visible |
| Modales para confirmaciones simples | Bootstrap/legacy | Un modal para "¿Seguro?" es UX friction innecesaria. Inline confirm es más rápido |
| Colores semánticos fijos (rojo=error siempre) | Bootstrap | En dark mode el rojo puro (#ef4444) baja a 3.8:1 sobre #06060a. Usamos --admin-danger que se ajusta por tema |
1.2 Audiencia
| Persona | Dispositivo | Contexto de uso |
|---|---|---|
| Dueño de restaurante | Teléfono Android (principalmente) | Cocina, mostrador, despacho |
| Personal administrativo | Desktop | Oficina |
Consecuencia directa: Todo elemento interactivo mínimo 44×44px. Cada acción en ≤3 taps.
1.3 Regla de oro de implementación
Los componentes solo usan tokens semánticos (Layer 2). Nunca un color hexadecimal directo, nunca un valor de Layer 1.
// ✅ CORRECTO
className="bg-surface-2 text-text border-border"
// ❌ INCORRECTO — hardcode que rompe theming
className="bg-[#12121e] text-[#f0f0f8]"
// ❌ INCORRECTO — Layer 1 directo
className="bg-[--p-gray-800]"
2. Sistema de Tokens (3 Capas)
Archivo fuente: src/design-system/tokens-admin.css
Tailwind mapping: tailwind.config.js
2.1 Capa 1 — Primitivos (:root)
Valores absolutos. Nunca los usa un componente directamente. Solo Layer 2 los referencia.
/* Grises */
--p-gray-950: #06060a /* bg global dark */
--p-gray-900: #0a0a12
--p-gray-850: #0d0d16 /* surface principal */
--p-gray-800: #12121e /* surface-2, inputs */
--p-gray-700: #1a1a30 /* surface-3, hover */
--p-gray-600: #22223d /* border-2 */
--p-gray-500: #2d2d4a /* text-dim */
--p-gray-400: #6b7280 /* text-muted */
--p-gray-200: #c8cad8
--p-gray-100: #e4e5f0
--p-gray-50: #f0f0f8 /* text dark mode */
--p-white: #ffffff
/* Accentos de marca */
--p-indigo-300: #a5b4fc
--p-indigo-400: #818cf8 /* accent-hover dark */
--p-indigo-500: #6366f1 /* accent principal */
--p-indigo-500-rgb: 99, 102, 241
--p-indigo-600: #4f46e5
--p-indigo-700: #4338ca /* accent-hover light */
--p-violet-500: #8b5cf6 /* accent-2, gradientes premium */
--p-violet-500-rgb: 139, 92, 246
/* Status */
--p-emerald-500: #10b981 --p-emerald-500-rgb: 16, 185, 129
--p-emerald-700: #047857
--p-rose-500: #ef4444 --p-rose-500-rgb: 239, 68, 68
--p-rose-700: #b91c1c
--p-amber-500: #f59e0b --p-amber-500-rgb: 245, 158, 11
--p-amber-700: #b45309
/* Shape — invariantes al tema */
--radius-sm: 6px --radius-md: 10px --radius-lg: 14px --radius-xl: 20px
--font-sans: 'Inter', system-ui, sans-serif
--font-mono: 'JetBrains Mono', monospace
2.2 Capa 2 — Tokens Semánticos
Mapean propósito → valor primitivo. Cambian con el tema. Estos son los tokens que usan los componentes.
Dark (:root, [data-theme="dark"]) — por defecto
| Token | Valor | Uso en componentes |
|---|---|---|
--bg |
#06060a |
Fondo global de la app |
--surface |
#0d0d16 |
Cards, sidebar, drawers |
--surface-2 |
#12121e |
Inputs, filas de tabla, hover bg |
--surface-3 |
#1a1a30 |
Hover states, estados activos |
--border |
#1a1a30 |
Borde por defecto |
--border-2 |
#22223d |
Borde destacado |
--text |
#f0f0f8 |
Texto principal |
--text-secondary |
#a0a8c0 |
Subtítulos, descripciones |
--text-muted |
#6b7280 |
Placeholders, labels secundarios |
--text-dim |
#2d2d4a |
Texto casi invisible (decorativo) |
--accent |
#6366f1 |
Color interactivo principal |
--accent-hover |
#818cf8 |
Hover sobre accent |
--accent-glow |
rgba(99,102,241,0.15) |
Resplandor radial en cards |
--accent-2 |
#8b5cf6 |
Gradientes premium, indicadores secundarios |
--shadow-glow |
0 0 40px rgba(99,102,241,0.15)... |
Sombra con aura índigo |
--shadow-card |
0 4px 6px -1px rgba(0,0,0,0.4)... |
Sombra de elevación media |
--shadow-elevated |
0 10px 25px -5px rgba(0,0,0,0.5)... |
Drawers, modales |
--success |
#10b981 |
Confirmación, activo |
--danger |
#ef4444 |
Error, destrucción, inactivo |
--warning |
#f59e0b |
Advertencia, pendiente |
Light ([data-theme="light"]) — diferencias clave
| Token | Dark | Light | Razón |
|---|---|---|---|
--bg |
#06060a |
#ffffff |
— |
--surface |
#0d0d16 |
#f0f0f8 |
— |
--text |
#f0f0f8 |
#0f0f1a |
Invertido |
--accent-hover |
#818cf8 (más claro) |
#4338ca (más oscuro) |
Contraste sobre fondo claro |
--danger |
#ef4444 |
#b91c1c |
Rose-700 para contraste 4.5:1 sobre blanco |
--success |
#10b981 |
#047857 |
Emerald-700 para contraste |
--warning |
#f59e0b |
#b45309 |
Amber-700 para contraste |
Cómo cambiar de tema (runtime)
// src/shared/stores/uiStore.ts
const { setTheme, toggleTheme } = useUiStore()
setTheme('light') // → document.documentElement.setAttribute('data-theme', 'light')
setTheme('dark') // → document.documentElement.setAttribute('data-theme', 'dark')
toggleTheme() // alterna entre ambos
// El tema persiste en localStorage con clave 'gastro-ui'
// onRehydrateStorage aplica data-theme antes del primer render
2.3 Capa 3 — Clases de Componente (@layer components)
Efectos reutilizables. Se aplican como clases CSS.
| Clase | Efecto | Cuándo usar |
|---|---|---|
.glass |
backdrop-blur(12px) + rgba(10,10,18,0.7) |
Overlays temporales |
.glass-strong |
backdrop-blur(20px) + rgba(13,13,22,0.85) |
Sidebar, drawers permanentes |
.glow-card |
::before radial gradient 400px, opacity 0→1 en hover |
Cards interactivas |
.dot-grid |
Radial gradient dots 1px, 24px grid | Fondo de DashboardLayout |
.grid-bg |
Grid lines 40px | Fondos alternativos |
.btn-premium |
Gradiente animado indigo→violet, box-shadow pulsante | CTA principal único |
.shimmer |
Sliding gradient 200% → -200% | Skeleton loading |
.nav-active-indicator |
Barra 3px left border con glow | NavLink activo en sidebar |
.gradient-text |
Clip white → gray-400 |
Headings h1 |
.accent-gradient-text |
Clip indigo-300 → violet-500 |
Badges premium |
.noise |
::after SVG noise filter opacity 4% |
Textura sobre cards |
2.4 Tailwind — Tokens disponibles como clases
// Superficies
bg-bg bg-surface bg-surface-2 bg-surface-3
// Con opacidad: bg-surface/50 bg-accent/10 etc.
// Bordes
border-border border-border-2
// Texto
text-text text-text-secondary text-text-muted text-text-dim
// Accentos
bg-accent text-accent border-accent
bg-accent/10 text-accent/50 // modificadores de opacidad
bg-accent-2 text-accent-2 // violet-500
// Status
bg-success/10 text-success border-success/20
bg-danger/10 text-danger border-danger/20
bg-warning/10 text-warning border-warning/20
// Sombras
shadow-glow shadow-card shadow-elevated
// Radios
rounded-sm rounded-md rounded-lg rounded-xl
// Legacy
rounded-admin rounded-admin-sm rounded-admin-lg
// Tipografía
font-sans font-mono
// Animaciones (ver §5)
animate-fade-in animate-slide-up animate-scale-in
animate-shimmer animate-breathing animate-pulse-glow
animate-gradient-shift
// Variables CSS del sidebar (custom properties en :root vía JS)
var(--sidebar-width) // 240px desktop
var(--bottom-nav-height) // 64px mobile
2.5 Sistema de Espaciado (grilla de 4px)
Principio: todo margen, padding, gap y tamaño usa múltiplos de 4px. Esto garantiza alineación pixel-perfect entre componentes sin negociación visual.
| Token Tailwind | Valor | px | Uso típico |
|---|---|---|---|
space-1 / p-1 |
0.25rem | 4px | Micro-gaps internos (badge padding) |
space-2 / p-2 |
0.5rem | 8px | Padding de items compactos (nav mobile, chips) |
space-3 / p-3 |
0.75rem | 12px | Padding de rows (ToggleRow, listas) |
space-4 / p-4 |
1rem | 16px | Padding estándar interno de cards |
space-5 / p-5 |
1.25rem | 20px | — (raramente usado) |
space-6 / p-6 |
1.5rem | 24px | Padding de página (<main>) |
space-8 / p-8 |
2rem | 32px | Separación entre secciones |
space-12 |
3rem | 48px | Espaciado mayor entre bloques de contenido |
space-20 |
5rem | 80px | pb-20 — espacio bajo contenido para bottom-nav mobile |
Gaps de grid
| Contexto | Clase | Valor |
|---|---|---|
| Grid de cards (productos/categorías) | gap-4 |
16px |
| Stack de formulario (campos verticales) | space-y-4 |
16px |
| Botones side-by-side (delete confirm) | gap-2 |
8px |
| Secciones de página | space-y-8 |
32px |
| Items de nav sidebar | space-y-1 |
4px |
Reglas
✅ Todo valor de spacing debe ser múltiplo de 4px (clases Tailwind estándar)
✅ Usar size-N en lugar de w-N h-N cuando el ancho y alto sean iguales
✅ Usar gap-* en contenedores flex en lugar de space-x/y para evitar phantom gaps
✅ pb-20 en contenedor de página — reserva espacio para bottom-nav mobile
❌ No usar valores arbitrarios como p-[18px] o gap-[7px]
❌ No mezclar rem hardcodeados en style={{ padding: '14px' }}
❌ No usar margin-bottom en el último hijo de un stack (usar space-y en el padre)
3. Sistema Tipográfico
Fuentes cargadas: Google Fonts en tokens-admin.css línea 8.
3.1 Escala tipográfica aplicada en código
| Uso en código | Tamaño | Peso | Clase Tailwind |
|---|---|---|---|
| Label de input | 0.75rem (12px) |
Bold | text-[0.75rem] font-bold uppercase tracking-wider |
| Label secundario | 0.82rem (13px) |
Medium | text-[0.82rem] font-medium |
| Texto body | 0.875rem (14px) |
Regular | text-sm |
| Precio, código | 0.875rem (14px) |
Bold | text-sm font-mono font-bold |
| Microcopy, badge | 0.65–0.72rem |
Medium/Bold | text-[0.72rem] |
| Heading de sección | 1.125rem (18px) |
Bold | text-lg font-bold |
| Tenant name | 0.75rem (12px) |
Semibold | text-xs font-semibold |
| Nav label mobile | 0.65rem |
Medium | text-[0.65rem] |
3.2 Reglas tipográficas
✅ Usar font-mono para: precios, IDs, códigos, contadores numéricos
✅ Usar tracking-wider + uppercase para: labels de campo
✅ Usar text-text-muted para: subtítulos, descripciones, placeholder
✅ Line-height implícito: 1.5 (base body)
❌ No usar font sizes inferiores a 0.65rem (10.4px) — ilegible en móvil
❌ No usar colores de texto hardcodeados
3.3 Sistema de Iconos
Librería: Lucide React v1.14+ — lucide-react en package.json.
Filosofía: iconos como comunicación, no decoración. Si un icono no reduce la carga cognitiva, no va.
Tamaños estándar
| Contexto | Size prop | px | Clase adicional |
|---|---|---|---|
| Nav items (sidebar desktop) | size={18} |
18px | shrink-0 |
| Nav items (bottom mobile) | size={20} |
20px | shrink-0 |
Botones con icono (sm) |
size={14} |
14px | mr-1.5 |
Botones con icono (md) |
size={16} |
16px | mr-2 |
| TopBar acciones | size={16} |
16px | — |
| Icono de estado (Badge area) | size={12} |
12px | inline |
| Ilustración de empty state | size={48} |
48px | text-text-muted |
| Spinner fallback (no usar SVG) | — | — | Usar <Spinner> |
Stroke width
Lucide usa stroke-width={1.5} por defecto. No cambiar — es parte de la identidad visual del sistema. Excepción: iconos de estado (success/danger/warning) pueden usar stroke-width={2} para mayor énfasis.
Iconos en uso (mapa de navegación)
import { UtensilsCrossed, Store, Palette, LogOut, ChevronRight,
Plus, Pencil, Trash2, Check, X, Upload, Image,
Tag, GripVertical, Eye, EyeOff, AlertCircle,
CheckCircle2, XCircle, Info } from 'lucide-react'
// Navegación
UtensilsCrossed → /menu
Store → /negocio
Palette → /apariencia
LogOut → logout sidebar
// Acciones CRUD
Plus → crear (botón primario de página)
Pencil → editar (inline, en cards)
Trash2 → eliminar (estado confirmDelete)
Check → confirmar, éxito
X → cancelar, cerrar drawer
// Media
Upload → ImageUploader drop zone
Image → placeholder sin imagen
// Estado
CheckCircle2 → success toast / estado activo
XCircle → error toast / estado inactivo
AlertCircle → warning toast
Info → info toast
// UX
GripVertical → drag handle (DnD sorting)
ChevronRight → expansión, drill-down
Eye/EyeOff → toggle visibilidad contraseña
Tag → TagInput chips
Icono + texto — reglas de composición
// ✅ Icono a la izquierda, gap consistente
<Button variant="primary">
<Plus size={16} className="mr-2" />
Nueva categoría
</Button>
// ✅ Solo icono — siempre con aria-label
<button aria-label="Editar producto">
<Pencil size={16} />
</button>
// ❌ Icono a la derecha del texto (no es convención del sistema)
<Button>Nueva categoría <Plus size={16} /></Button>
// ❌ Emoji como icono — no escala, no tematizable
<button>🗑️ Eliminar</button>
Cuándo NO usar icono
❌ Como único indicador de error (siempre acompañar con texto)
❌ En párrafos de texto corrido (interrumpe el ritmo de lectura)
❌ Decorativamente en cards sin función (añade ruido visual)
✅ Como refuerzo semántico junto a texto (ícono + label)
✅ Para reemplazar texto en espacios muy comprimidos (solo con aria-label)
4. Librería de Componentes
4.1 Átomos
Button
Archivo: src/design-system/atoms/Button/Button.tsx
<Button
variant="primary" // 'primary' | 'premium' | 'secondary' | 'ghost' | 'danger' | 'outline'
size="md" // 'sm' | 'md' | 'lg'
loading={false} // muestra Spinner + deshabilita
fullWidth={false} // w-full
disabled={false}
onClick={handler}
>
Texto
</Button>
| Variant | Fondo | Texto | Cuándo |
|---|---|---|---|
primary |
--accent |
white | Acción principal de la pantalla |
premium |
Gradiente indigo→violet animado | white | CTA de onboarding, upgrade |
secondary |
--surface-2 |
--text |
Acciones secundarias |
ghost |
transparent | --text-muted |
Acciones terciarias, navegación |
danger |
danger/10 |
--danger |
Eliminar, acción destructiva |
outline |
transparent | --text |
Bordes visibles sin peso visual |
| Size | Altura mínima | Font size | Uso |
|---|---|---|---|
sm |
32px | 0.7rem | Acciones en filas de tabla, TopBar |
md |
40px | 0.875rem | Por defecto |
lg |
48px | 1rem | CTA en pantallas de onboarding |
Comportamiento accesible:
- disabled + loading → disabled en el DOM
- loading → muestra <Spinner> interno, mantiene el ancho
- focus-visible → ring índigo 2px con offset sobre --bg
- active:scale-[0.98] — feedback táctil
Badge
Archivo: src/design-system/atoms/Badge/Badge.tsx
<Badge variant="success">Activo</Badge>
<Badge variant="danger">Agotado</Badge>
<Badge variant="warning">Pendiente</Badge>
<Badge variant="info">Novedad</Badge>
<Badge variant="neutral">Sin categoría</Badge>
Siempre uppercase + tracking-wider + text-[0.65rem]. No usar para textos largos — máximo 2 palabras.
Input
Archivo: src/design-system/atoms/Input/Input.tsx
<Input
label="Nombre del Restaurante" // genera htmlFor automático
placeholder="Ej: Kasiri Café"
error={errors.name?.message} // texto rojo debajo + border danger
hint="Máximo 255 caracteres" // texto muted debajo (solo si sin error)
required // agrega asterisco rojo al label
{...register('name')} // RHF compatible (extiende InputHTMLAttributes)
/>
Nota: El label genera id automático como label.toLowerCase().replace(/\s+/g, '-'). Pasar id explícito si se necesita un valor distinto.
Textarea
Archivo: src/design-system/atoms/Textarea/Textarea.tsx
Misma API que Input. Props adicional: rows (default: 3). Resize vertical habilitado por el usuario.
Switch
Archivo: src/design-system/atoms/Switch/Switch.tsx
<Switch
checked={isActive}
onChange={(checked) => setValue('is_active', checked)}
label="Restaurante activo" // opcional — texto a la derecha
size="md" // 'sm' | 'md'
disabled={false}
/>
Accesibilidad: role="switch" + aria-checked. Toda el área del label es clickeable. min-h-[44px].
Spinner
Archivo: src/design-system/atoms/Spinner/Spinner.tsx
SVG inline con animate-spin. Color hereda del padre con currentColor. Usado internamente por Button loading.
Avatar
Archivo: src/design-system/atoms/Avatar/Avatar.tsx
Con src → <img>. Sin src → iniciales (máx 2 letras) sobre color determinista por hash del nombre. Colores disponibles: violeta, azul, esmeralda, ámbar, rojo, cyan.
ErrorBoundary
Archivo: src/design-system/atoms/ErrorBoundary/ErrorBoundary.tsx
<ErrorBoundary>
<ComponenteQuePodriaFallar />
</ErrorBoundary>
// Con fallback personalizado:
<ErrorBoundary fallback={<div>Error personalizado</div>}>
...
</ErrorBoundary>
Uso: envuelve cada <Page> en App.tsx. Un crash no derrumba toda la app. Botón "Reintentar" hace setState({ hasError: false }).
4.2 Moléculas
ImageUploader
Archivo: src/design-system/molecules/ImageUploader/ImageUploader.tsx
<ImageUploader
currentUrl={imageUrl ?? null} // URL actual para preview
onUploaded={(url, key) => { // callback tras upload exitoso
setValue('image_url', url)
setValue('image_key', key)
}}
folderType="platos" // 'platos' | 'logos' — requerido por backend
aspectRatio="16/9" // CSS aspect-ratio (default '16/9')
label="Imagen del plato" // texto encima del uploader
/>
Flujo interno:
1. Click o drag → input[type=file] hidden
2. Validación frontend: tipo image/*, max 10MB
3. POST /api/v1/media/upload con multipart/form-data (file + folder_type)
4. Backend: Pillow resize → WEBP → compress → R2 upload
5. Retorna { url, key, width, height, size_bytes }
6. onUploaded(url, key) — caller guarda ambos en el form
Problema de estado con drawers: El preview vive en estado interno. Al reutilizar el componente para distintos productos, el preview anterior persiste. Solución obligatoria: key prop que cambia cuando el form se resetea.
// ✅ CORRECTO — key deriva del valor del formulario
const imageUrl = watch('image_url')
<ImageUploader key={imageUrl ?? 'empty'} currentUrl={imageUrl} ... />
// ❌ INCORRECTO — key por ID causa flash del valor anterior
<ImageUploader key={product?.id ?? 'new'} ... />
PriceInput
Archivo: src/design-system/molecules/PriceInput/PriceInput.tsx
<PriceInput
value={product.price} // string | number
onSave={(newPrice) => mutateFn(newPrice)}
loading={isPending}
/>
Estados: Display (formateado COP) → Click → Edit (input numérico) → Enter/Blur → onSave → Display.
- Escape cancela sin guardar
- Guarda solo si el valor cambió
- Formato: Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP' })
ToggleRow
Archivo: src/design-system/molecules/ToggleRow/ToggleRow.tsx
<ToggleRow
label="Aceptar pedidos online"
description="Los clientes podrán ordenar desde la carta pública" // opcional
checked={isActive}
onChange={(v) => setValue('is_active', v)}
disabled={false}
loading={isPending} // deshabilita el switch durante la mutación
/>
Uso típico: listas de configuración con separadores. El último ítem no tiene border-bottom (:last:border-0).
ColorPicker + SectorPicker
Archivos: molecules/ColorPicker/, molecules/SectorPicker/
Usados exclusivamente en AppearancePage. ColorPicker = input[type=color] con hex label. SectorPicker = selector visual de tipo de local (cafetería, restaurant, bar, etc.).
4.3 Organismos
Sidebar
Archivo: src/design-system/organisms/Sidebar/Sidebar.tsx
Dos implementaciones en un solo componente:
| Versión | Trigger | Características |
|---|---|---|
DesktopSidebar |
≥ 640px (sm) |
Fixed left, 240px (--sidebar-width), glass-strong |
MobileBottomNav |
< 640px |
Fixed bottom, 64px (--bottom-nav-height), glass-strong |
Ítems de navegación (array NAV_ITEMS):
- /menu → UtensilsCrossed (Menú)
- /negocio → Store (Mi Local)
- /apariencia → Palette (Apariencia)
Estado activo: NavLink con isActive prop. Desktop → .nav-active-indicator + bg-accent/10. Mobile → text-accent.
Para agregar una nueva sección: añadir { to: '/nueva-ruta', label: 'Nombre', Icon: LucideIcon } al array NAV_ITEMS. Los Framer Motion mount animations se aplican automáticamente.
TopBar
Archivo: src/design-system/organisms/TopBar/TopBar.tsx
<TopBar
title="Menú del Restaurante"
actions={
<Button size="sm" loading={isPending} disabled={!isDirty}>
Guardar
</Button>
}
/>
Header fijo de cada página. title a la izquierda, actions slot a la derecha.
LoginForm
Archivo: src/design-system/organisms/LoginForm/LoginForm.tsx
Formulario autónomo de autenticación. Consume useAuthStore().login(). Maneja su propio estado de error. Sin props — se renderiza completo.
4.4 Templates
DashboardLayout
Archivo: src/design-system/templates/DashboardLayout/DashboardLayout.tsx
// En cada Page — ya está aplicado automáticamente via AppLayout
function MenuPage() {
return (
<div> {/* sin wrapper extra — DashboardLayout ya provee el padding */}
<TopBar title="Menú" actions={...} />
<div className="p-6">...</div>
</div>
)
}
Layers del layout (z-index):
| Layer | z-index | Elemento | Descripción |
|---|---|---|---|
| 0 | z-0 |
.dot-grid |
Fondo fixed, pointer-events none |
| 0 | z-0 |
Canvas glow | Radial gradient 1200px, mouse tracking |
| 10 | z-10 |
<main> |
Área de contenido scrolleable |
| 50 | z-50 |
Sidebar |
Navegación, siempre encima |
Cursor glow: El layout entero usa useCardGlow — --mouse-x y --mouse-y se actualizan en cada onMouseMove. El canvas glow (1200px radial, var(--accent-glow)) sigue al cursor en tiempo real.
Page transitions: AnimatePresence mode="wait" con key={location.pathname}. Cada cambio de ruta hace fade+slide (0.3s, ease [0.16,1,0.3,1]).
4.5 Componentes de Feature (no reutilizables fuera de su dominio)
CategoryDrawer / ProductDrawer
Archivos: src/features/menu/components/
Drawers laterales para crear/editar categorías y productos. Misma arquitectura:
// Props
interface ProductDrawerProps {
mode: 'create' | 'edit'
product?: AdminProduct // undefined en create
onClose(): void
}
Patrón de form reset:
const form = useForm({
defaultValues: {
name: product?.name ?? '', // ← valores del producto, no vacíos
image_url: product?.image_url ?? null,
}
})
useEffect(() => {
if (product) reset({ name: product.name, ... }) // sync al cambiar el producto
}, [product, reset])
Delete con confirm inline:
// Sin browser confirm(). Estado local confirmDelete: boolean.
{mode === 'edit' && !confirmDelete && (
<Button variant="danger" onClick={() => setConfirmDelete(true)}>
Eliminar plato
</Button>
)}
{confirmDelete && (
<div className="flex gap-2">
<Button variant="danger" onClick={handleDelete}>Sí, eliminar</Button>
<Button variant="secondary" onClick={() => setConfirmDelete(false)}>Cancelar</Button>
</div>
)}
TagInput
Archivo: src/features/menu/components/TagInput.tsx
<TagInput
tags={tags} // string[]
onChange={(tags) => setValue('tags', tags)}
error={errors.tags?.message} // opcional
/>
Reglas: max 10 tags, duplicados rechazados, toLowerCase() al agregar, Enter para confirmar.
5. Sistema de Animación y Movimiento
Motor: Framer Motion v12 para componentes React. CSS @keyframes para efectos de fondo y loading.
5.1 Curva de easing estándar
Todos los page transitions, mount animations del sidebar y drawers usan esta curva.
5.2 Animaciones Tailwind (CSS puro)
| Clase | Duración | Curva | Uso |
|---|---|---|---|
animate-fade-in |
400ms | spring SV | Cards al montar, secciones |
animate-slide-up |
500ms | spring SV | Listas, contenido principal |
animate-scale-in |
350ms | spring SV | Modales, drawers |
animate-slide-in-right |
400ms | spring SV | Paneles laterales |
animate-slide-in-bottom |
500ms | spring SV | Bottom sheets mobile |
animate-shimmer |
1.5s | linear ∞ | Skeleton loading |
animate-breathing |
3s | ease-in-out ∞ | Logo sidebar (0.6→1 opacity, 1→1.05 scale) |
animate-pulse-glow |
2s | ease-in-out ∞ | Alerts, status indicators |
animate-gradient-shift |
4s | ease ∞ | btn-premium background |
5.3 Framer Motion — variantes estándar
// Page transitions (ya en DashboardLayout)
const pageVariants = {
initial: { opacity: 0, y: 12 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -8 },
}
const pageTransition = { duration: 0.3, ease: [0.16, 1, 0.3, 1] }
// Mount animation de ítem en lista (stagger)
<motion.div
initial={{ opacity: 0, x: -12 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.35, delay: index * 0.05, ease: [0.16, 1, 0.3, 1] }}
>
// Hover con movimiento sutil (sidebar logout button)
<motion.button whileHover={{ x: 2 }}>
5.4 Reglas de animación
✅ Siempre usar transform/opacity — no animar width/height/left/top (causa reflow)
✅ Respetar prefers-reduced-motion — ya manejado en tokens-admin.css (0.01ms forzado)
✅ Micro-interacciones: 150–300ms
✅ Page transitions: 300–500ms
✅ Loops decorativos: 2–4s
❌ No animar propiedades que afectan el flujo del documento
❌ No superar 500ms en interacciones de usuario (se siente lento)
❌ No usar spring physics de Framer Motion para loading spinners
5.5 Skeleton Shimmer — patrón estándar
// ✅ CORRECTO — class shimmer de tokens-admin.css
<div className="h-40 rounded-lg shimmer" />
<div className="h-3 w-3/4 rounded shimmer mt-2" />
// Para colecciones
{isLoading
? Array.from({ length: 3 }, (_, i) => (
<div key={i} className="h-20 rounded-lg shimmer" />
))
: items.map(...)
}
6. Accesibilidad (WCAG 2.1 AA)
6.1 Contraste de color (verificado)
| Combinación | Ratio | Resultado |
|---|---|---|
text sobre bg (dark) |
#f0f0f8 / #06060a |
~16:1 ✅ |
text sobre surface (dark) |
#f0f0f8 / #0d0d16 |
~14:1 ✅ |
text-muted sobre surface |
#6b7280 / #0d0d16 |
~6.2:1 ✅ |
text sobre bg (light) |
#0f0f1a / #ffffff |
~19:1 ✅ |
danger texto (light) |
#b91c1c / #ffffff |
~7.1:1 ✅ |
danger texto (dark) |
#ef4444 / #0d0d16 |
~5.4:1 ✅ |
accent sobre bg (dark) |
#6366f1 / #06060a |
~4.9:1 ✅ |
6.2 Checklist de implementación
✅ Touch targets: mínimo 44×44px en todos los elementos interactivos
→ Button: min-h-[32px] sm, [40px] md, [48px] lg
→ Switch: min-h-[44px] en el label wrapper
→ Nav items: min-h-[44px]
→ ToggleRow: min-h-[44px] implícito por py-3
✅ Focus states: focus-visible rings en Button y elementos nativos
→ ring-2 ring-accent/40 ring-offset-2 ring-offset-bg
✅ ARIA semántico:
→ Switch: role="switch" aria-checked={checked}
→ Spinner: aria-label="Cargando"
→ NotificationStack: aria-live="polite" role="status"
→ Modal/Drawer: aria-modal + focus trap (implementar al usar)
✅ Labels asociados: Input/Textarea generan htmlFor automático
✅ Imágenes: Avatar tiene alt={name}, ImageUploader preview tiene alt=""
✅ prefers-reduced-motion: tokens-admin.css fuerza 0.01ms en todas las animaciones
✅ Safe area insets: MobileBottomNav usa pb-[env(safe-area-inset-bottom)]
✅ Interacción Teclado: onClick siempre acompañado de onKeyDown si el elemento es static
### 6.3 React 19 & Ref Pattern
En React 19+, `forwardRef` está deprecado. Los componentes deben recibir `ref` como una prop estándar.
```tsx
// ✅ DO (React 19+)
export const Button = ({ children, ref, ...props }: ButtonProps) => (
<m.button ref={ref} {...props}>{children}</m.button>
)
// ❌ DON'T (Legacy)
export const Button = forwardRef((props, ref) => ...)
6.4 Teclado
Tab: navega entre elementos interactivos (orden DOM = orden visual)
Enter: activa button, confirma input
Escape: cierra drawers, cancela edición inline en PriceInput
Space: activa Switch (nativo de role="switch")
---
## 7. Layout Responsive
### 7.1 Breakpoints
```css
/* Tailwind defaults — mobile-first */
sm: 640px /* Sidebar visible, bottom-nav desaparece */
md: 768px /* Grids 2 columnas */
lg: 1024px /* Grids 3 columnas, padding mayor */
xl: 1280px
2xl: 1536px /* max-w-[1600px] en main */
7.2 Patrón de layout de página
// Todas las páginas siguen este patrón
<div className="flex flex-col h-full bg-admin-bg-2">
{/* TopBar fijo */}
<TopBar title="..." actions={...} />
{/* Contenido scrolleable */}
<div className="p-6 max-w-3xl mx-auto w-full space-y-8 pb-20">
{/* pb-20: espacio para la bottom-nav en mobile */}
...
</div>
</div>
7.3 Variables CSS del shell
/* Aplicadas por JavaScript al montar DashboardLayout */
--sidebar-width: 240px /* solo ≥ sm */
--bottom-nav-height: 64px /* solo < sm */
/* Uso en Tailwind */
sm:pl-[var(--sidebar-width)] /* main content margin */
pb-[var(--bottom-nav-height)] /* espacio para bottom nav */
7.4 Grid de cards (MenuPage pattern)
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{products.map(p => <ProductCard key={p.id} {...p} />)}
</div>
8. Patrones de Interacción
8.1 Optimistic UI (estándar de todos los mutations)
// En menu-hooks.ts — patrón obligatorio para toda mutación que toca la UI
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey }) // cancela fetches en vuelo
const prev = queryClient.getQueryData(queryKey) // snapshot
queryClient.setQueryData(queryKey, producirEstadoOptimista)
return { prev } // contexto para rollback
},
onError: (_err, _vars, ctx) => {
if (ctx?.prev) queryClient.setQueryData(queryKey, ctx.prev)
},
onSettled: () => queryClient.invalidateQueries({ queryKey })
8.2 Inline Editing (PriceInput)
- Display mode: valor formateado, cursor pointer, hover tint acento
- Click → Edit mode:
<input type="number">conselect()automático - Enter / Blur →
onSave(value)si cambió - Escape → vuelve a display sin guardar
8.3 Delete con confirmación inline
Sin browser.confirm(). Sin modal. Estado local confirmDelete: boolean.
Estado normal: [ Eliminar plato ]
↓ click
Estado confirmando: [ Sí, eliminar ] [ Cancelar ]
↓ click "Sí"
Mutation + onClose()
8.4 Toast notifications (uiStore)
const { notify } = useUiStore()
notify('Categoría creada', 'success') // auto-dismiss 4s
notify('Error al guardar', 'error')
notify('Cambios guardados', 'info')
notify('Stock bajo', 'warning')
Las notificaciones se renderizan en NotificationStack (fixed, bottom-right, aria-live="polite").
8.5 Formularios (RHF + Zod pattern)
const schema = z.object({
name: z.string().min(1, 'Requerido').max(255),
price: z.number().positive('Debe ser mayor a 0'),
})
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: { name: entity?.name ?? '', price: entity?.price ?? 0 },
})
// Resetear al cambiar la entidad
useEffect(() => {
if (entity) form.reset({ name: entity.name, price: entity.price })
}, [entity, form.reset])
8.6 Estados Vacíos y de Error
Cada estado del sistema tiene representación visual. Nunca dejar una pantalla en blanco — confunde al usuario sobre si cargó o falló.
Jerarquía de estados de datos
Cargando → Skeleton shimmer (§5.5)
Sin datos → Empty state (este apartado)
Error de API → Error state inline (este apartado)
Crash de JS → ErrorBoundary (§4.1)
Patrón Empty State
// Estructura estándar — usada en MenuPage cuando no hay categorías/productos
<div className="flex flex-col items-center justify-center py-16 text-center">
<UtensilsCrossed size={48} className="text-text-muted mb-4 opacity-40" />
<h3 className="text-text font-semibold text-base mb-1">
Aún no hay productos
</h3>
<p className="text-text-muted text-sm max-w-xs mb-6">
Crea tu primer plato para que aparezca en la carta pública.
</p>
<Button variant="primary" onClick={onCreateClick}>
<Plus size={16} className="mr-2" />
Crear primer plato
</Button>
</div>
Reglas del empty state:
✅ Icono grande (48px) en text-muted — orienta sin alarmar
✅ Título en positivo: "Aún no hay X" no "No se encontraron X"
✅ Descripción de 1 línea que explica qué ocurrirá al actuar
✅ CTA directo — no hacer que el usuario busque cómo empezar
✅ Centrado vertical si ocupa toda la pantalla, alineado izquierda si está inline
❌ No mostrar pantalla completamente vacía (sin icono ni texto)
❌ No usar lenguaje técnico: "No hay registros en la base de datos"
❌ No poner CTA de "Volver" si el usuario acaba de llegar a la pantalla
Patrón Error State (fallo de API)
// Error recuperable — el usuario puede reintentar
<div className="flex flex-col items-center justify-center py-16 text-center">
<AlertCircle size={48} className="text-danger mb-4 opacity-70" />
<h3 className="text-text font-semibold text-base mb-1">
No se pudieron cargar los productos
</h3>
<p className="text-text-muted text-sm max-w-xs mb-6">
Revisa tu conexión e intenta de nuevo.
</p>
<Button variant="secondary" onClick={() => refetch()}>
Reintentar
</Button>
</div>
// Con TanStack Query
const { data, isLoading, isError, refetch } = useProducts()
if (isLoading) return <SkeletonList />
if (isError) return <ErrorState onRetry={refetch} />
return <ProductList data={data} />
Tipos de error y su tratamiento
| Tipo | Dónde mostrar | Acción disponible |
|---|---|---|
| Error de red (sin conexión) | Inline en la página | Reintentar |
| 401 Unauthorized | Redirigir a /login |
— (automático via interceptor) |
| 403 Forbidden | Toast error + mantener pantalla | Notificar, no bloquear |
| 404 Not Found | Inline en la sección | Volver a la lista |
| 500 Server Error | Error state + reintentar | Reintentar tras 3s |
| Timeout (>10s) | Error state | Reintentar |
| Error de validación (422) | Campos del formulario | Corrección inline |
Copy estándar para mensajes de error
✅ Accionable y sin culpar al usuario:
"No se pudo guardar el producto. Intenta de nuevo."
"Revisa tu conexión e intenta más tarde."
"El nombre ya está en uso. Elige otro."
❌ Técnico o que culpa:
"Error 500: Internal Server Error"
"Failed to fetch: NetworkError"
"El campo name es requerido" (en lugar de "El nombre es requerido")
Z-Index de capas de estado
Formaliza los valores mencionados en §4.4 más los de estados:
| Capa | z-index | Elemento |
|---|---|---|
| Fondo | z-0 |
dot-grid, canvas glow |
| Contenido | z-10 |
<main>, páginas |
| Sidebar / TopBar | z-50 |
Navegación permanente |
| Drawers | z-60 |
Paneles laterales |
| Modales | z-70 |
Diálogos bloqueantes |
| Toasts | z-80 |
NotificationStack (siempre encima) |
| Loading overlay | z-90 |
Spinners de página completa (evitar) |
9. Efectos Premium (implementación técnica)
9.1 Cursor Glow — useCardGlow
// src/shared/hooks/use-card-glow.ts
const { ref, onMouseMove, onMouseLeave } = useCardGlow<HTMLDivElement>()
// Aplica al contenedor. Actualiza --mouse-x y --mouse-y como % del elemento.
// onMouseLeave resetea a 50%/50% (centro).
<div ref={ref} onMouseMove={onMouseMove} onMouseLeave={onMouseLeave} className="glow-card">
.glow-card CSS:
.glow-card::before {
content: '';
position: absolute; inset: 0;
background: radial-gradient(400px at var(--mouse-x) var(--mouse-y), var(--accent-glow), transparent 80%);
opacity: 0;
transition: opacity 0.5s ease;
}
.glow-card:hover::before { opacity: 1; }
9.2 Canvas Glow (global)
DashboardLayout tiene un div fixed con radial-gradient(1200px at var(--mouse-x) var(--mouse-y), var(--accent-glow), transparent 80%). Sigue al cursor en toda la página. Opacidad 40%. No bloquea eventos (pointer-events none).
9.3 Glassmorphism
.glass { background: rgba(10,10,18,0.7); backdrop-filter: blur(12px) saturate(180%); border: 1px solid rgba(255,255,255,0.05); }
.glass-strong { background: rgba(13,13,22,0.85); backdrop-filter: blur(20px) saturate(200%); border: 1px solid rgba(255,255,255,0.08); }
Cuándo usar:
- .glass → overlays temporales (tooltips, dropdowns)
- .glass-strong → Sidebar, drawers, panels fijos
Nota de light mode: Estas clases usan rgba hardcoded — no escalan automáticamente a light theme. En light mode, considerar bg-surface/80 con backdrop-blur-lg directamente.
9.4 Dot Grid
.dot-grid {
background-image: radial-gradient(var(--border) 1px, transparent 1px);
background-size: 24px 24px;
}
Solo en DashboardLayout. Opacity 0.35, fixed inset-0, z-0, pointer-events-none. Los puntos cambian de color automáticamente con el tema (usa --border).
9.5 btn-premium
.btn-premium {
background: linear-gradient(135deg, #4f46e5, #6366f1, #8b5cf6);
background-size: 200% 200%;
animation: gradient-shift 4s ease infinite;
box-shadow: 0 0 20px rgba(99,102,241,0.3), 0 4px 15px rgba(99,102,241,0.2);
}
.btn-premium:hover { box-shadow: 0 0 30px rgba(99,102,241,0.5); transform: translateY(-1px); }
.btn-premium:active { transform: scale(0.98); }
Uso: UNA vez por pantalla, máximo. Para el CTA más importante (upgrade, primera acción de onboarding).
10. Guía Do / Don't
Colores
// ✅ DO
<div className="bg-surface-2 border border-border text-text-muted" />
<span className="text-danger" />
<div className="bg-accent/10 text-accent border-accent/20" />
// ❌ DON'T
<div style={{ background: '#12121e', color: '#f0f0f8' }} />
<span className="text-red-400" /> // no usa tokens del sistema
<div className="bg-indigo-500/10" /> // hardcode de primitivo
Componentes
// ✅ DO — usar átomos existentes
<Button variant="danger" loading={isPending}>Eliminar</Button>
<Badge variant="success">Activo</Badge>
// ❌ DON'T — reinventar la rueda
<button className="bg-red-500 text-white px-4 py-2 rounded">Eliminar</button>
<span className="bg-green-100 text-green-700 px-2 py-0.5 rounded">Activo</span>
Animaciones
// ✅ DO — clases Tailwind del sistema
<div className="animate-fade-in">
<div className="shimmer h-20 rounded-lg">
// ❌ DON'T — valores hardcoded
<div style={{ animation: 'fade 0.3s ease' }}>
// (la animación no existe en el sistema, no respeta prefers-reduced-motion)
Formularios
// ✅ DO — RHF + Zod
const { register, formState: { errors } } = useForm({ resolver: zodResolver(schema) })
<Input error={errors.name?.message} {...register('name')} />
// ❌ DON'T — useState por campo
const [name, setName] = useState('')
const [nameError, setNameError] = useState('')
11. Cómo agregar al Design System
Nuevo átomo
- Crear
src/design-system/atoms/NuevoAtomo/NuevoAtomo.tsx - Solo importar:
React,cn()delib/utils, otros átomos - Nunca importar: hooks de datos, stores, servicios, features
- Usar únicamente tokens semánticos (Layer 2)
- Exportar como named export
- Documentar props en esta sección (§4.1)
Nueva molécula
- Crear
src/design-system/molecules/NuevaMolecula/NuevaMolecula.tsx - Puede importar: átomos del design-system,
cn(), tipos - No importar: stores, servicios, hooks de TanStack Query
- Si la lógica crece > 150 líneas → evaluar si pertenece a
features/
Nuevo token
- Agregar primitivo en
:root(Layer 1) si es un nuevo valor base - Agregar semántico en
:root, [data-theme="dark"]Y en[data-theme="light"] - Agregar al
tailwind.config.jssi se usa en Tailwind classes - Documentar en la tabla §2.2
12. Estado de implementación
| Sección | Estado | Notas |
|---|---|---|
| Token Architecture (3 layers) | ✅ Completo | Dark + Light theme funcional |
| Typography System | ✅ Completo | Inter + JetBrains Mono |
| Button (6 variants) | ✅ Completo | React 19 native ref, a11y |
| Badge (5 variants) | ✅ Completo | |
| Input + Textarea | ✅ Completo | label, error, hint, required |
| Switch | ✅ Completo | role="switch", aria-checked |
| Avatar | ✅ Completo | imagen + iniciales + hash color |
| Spinner | ✅ Completo | |
| ErrorBoundary | ✅ Completo | Por página en App.tsx |
| ImageUploader | ✅ Completo | drag/click, 10MB, WEBP pipeline |
| PriceInput | ✅ Completo | COP inline editing, optimized Intl |
| ToggleRow | ✅ Completo | |
| ColorPicker | ✅ Completo | Solo AppearancePage |
| SectorPicker | ✅ Completo | Solo AppearancePage |
| Sidebar (desktop + mobile) | ✅ Completo | glass-strong, nav-active-indicator |
| TopBar | ✅ Completo | slot de actions |
| LoginForm | ✅ Completo | |
| DashboardLayout | ✅ Completo | dot-grid, cursor glow, LazyMotion |
| Cursor glow hook | ✅ Completo | useCardGlow |
| Glassmorphism | ✅ Completo | .glass + .glass-strong |
| Skeleton shimmer | ✅ Completo | .shimmer + stable keys |
| btn-premium | ✅ Completo | gradient-shift animation |
| Dark/Light theming | ✅ Completo | data-theme, persist localStorage |
| TagInput | ✅ Completo | Solo features/menu |
| CategoryDrawer | ✅ Completo | Leak-free (clearTimeout) |
| ProductDrawer | ✅ Completo | Refactored (ProductForm) |
| Radix UI headless | ⬜ M2 | Dialog, Dropdown, Select accesible |
| Storybook | ⬜ M2 | Stories por componente |
| Tailwind v4 | ⬜ M2 | Migración a @theme {} |
Gastro Design System v3.0 — Silicon Valley ACS Standard 2026 — Actualizado 2026-05-06 Ejecutable por humanos y agentes IA — cada sección referencia código fuente verificado ente verificado e verificado ente verificado*