@core-tecnologias-empresariales/core-ui
v0.2.0
Published
Sistema visual transversal (design tokens, 11 temas oficiales, componentes) del ecosistema Core Tecnología Empresarial.
Readme
@core-tecnologias-empresariales/core-ui
Sistema visual transversal del ecosistema Core Tecnología Empresarial. Todos los productos (CorePyme, Core Tributario, Core Contador) consumen esta capa — no crean sistemas visuales paralelos.
Entradas del paquete
import { THEMES, applyTheme } from "@core-tecnologias-empresariales/core-ui"; // tokens/temas, sin React
import { Sheet, VerificationCodeInput } from "@core-tecnologias-empresariales/core-ui/components"; // capa React
import { tailwindPreset } from "@core-tecnologias-empresariales/core-ui/tailwind-preset";Están separadas a propósito: un tailwind.config o un proceso backend importa los temas sin arrastrar React.
Los 11 temas oficiales
LIGHT: light-1 … light-6 DARK: dark-1 … dark-5Transcritos literalmente del CLAUDE.md §8 — 110 valores de color, verificados por tests. Default: light-4 / dark-1.
Histórico: las versiones previas del documento maestro enumeraban un
light-7con paleta idéntica alight-5. Se resolvió como duplicación accidental y se eliminó del catálogo (decisión del 2026-08-29, registrada enCLAUDE.md). Un test impide que dos temas vuelvan a compartir paleta.
Uso
/* Una vez, en el entry CSS de la app */
@import "@core-tecnologias-empresariales/core-ui/styles/tokens.css";import { applyTheme, THEME_IDS, defaultThemeFor } from "@core-tecnologias-empresariales/core-ui";
applyTheme(document.documentElement, "dark-1"); // cambia el tema, sin tocar componentes
defaultThemeFor("dark"); // "dark-1"
THEME_IDS; // para poblar un selector de temasCambiar de tema es escribir data-theme en el elemento raíz. Nunca requiere modificar un componente (CLAUDE.md §8). Sin data-theme, la hoja respeta prefers-color-scheme del sistema.
Tailwind
// tailwind.config.js del producto
import { tailwindPreset } from "@core-tecnologias-empresariales/core-ui/tailwind-preset";
export default { presets: [tailwindPreset], content: [...] };Las clases quedan mapeadas a los tokens: bg-bg-200, text-text-100, text-primary, shadow-md, rounded-lg, z-drawer, font-mono. Un producto que escribe bg-[#123456] está violando la regla contra hardcoding (§8) — el preset existe para que no haga falta.
Tokens disponibles
| Grupo | Tokens |
|---|---|
| Paleta | --primary-100/200/300, --accent-100/200, --text-100/200, --bg-100/200/300 |
| Semánticos | --color-success/warning/danger/info |
| Estado | --color-border/muted/hover/active/focus/selected/disabled (derivados del tema) |
| Tipografía | --font-sans (IBM Plex Sans), --font-mono (IBM Plex Mono) |
| Escalas | --space-*, --radius-*, --shadow-*, --z-*, --breakpoint-*, --motion-* |
Los semánticos se definen una vez por modo (light/dark) en lugar de 4 × 12 temas: menos superficie, mismo resultado accesible. Los de estado se derivan del tema, así que nunca hay que hardcodearlos por producto.
Tipografía
El paquete define el stack (--font-sans / --font-mono) pero no empaqueta los archivos de fuente — cargarlos es decisión del producto (next/font, self-host o CDN), y evita arrastrar ~2MB de woff2 a todos los consumidores. Regla de uso: IBM Plex Sans para toda la interfaz, IBM Plex Mono solo para RUT, folios, IDs, códigos, JSON y logs.
Decisiones tomadas
- Drawer: shadcn/ui
Sheetsobre@radix-ui/react-dialog— CLAUDE.md §15 manda preferir shadcn/Radix. Google Analytics es la referencia de UX (§14), no de librería: se replica su comportamiento (panel lateral, scrim, header con cierre, cuerpo scrolleable, acciones abajo) sin traer Material, que rompería Tailwind y Lucide (§12). - Sin
vaul— el bottom-sheet con gesto de arrastre es patrón móvil/consumer; el Sheet de Radix ya cubre móvil a ancho completo. Se agrega solo si la experiencia móvil lo pide. - Los temas se generan desde datos tipados, no se escriben a mano:
src/themes.tses la única fuente de verdad y alimenta tantostyles/tokens.csscomo el selector de temas.
Componentes (v0.2)
Sheet — panel lateral
El patrón principal de interacción del ecosistema, sobre @radix-ui/react-dialog (que aporta focus trap, cierre con Esc, scroll lock y ARIA sin código propio).
<Sheet>
<SheetTrigger>Nuevo usuario</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Nuevo usuario</SheetTitle>
<SheetDescription>Completa los datos.</SheetDescription>
</SheetHeader>
<SheetBody>{/* formulario */}</SheetBody>
<SheetFooter>
<SheetClose>Cancelar</SheetClose>
<button type="submit">Guardar</button>
</SheetFooter>
</SheetContent>
</Sheet>Estructura Header / Body / Footer obligatoria: el cuerpo scrollea y el footer nunca se va de la vista. Ancho contenido en desktop (420px), full-width bajo sm. side acepta right (default), left, top, bottom.
Cuándo NO usarlo: procesos de múltiples etapas, navegación interna compleja o pantallas de trabajo completas — ahí va una página dedicada.
VerificationCodeInput — código de verificación
Para recuperación de contraseña y MFA. Placeholder de guiones y presentación monoespaciada según el documento maestro.
<VerificationCodeInput
length={6}
value={code}
onChange={setCode}
onComplete={(code) => verificar(code)}
error={codigoInvalido}
/>Implementado como un solo input monoespaciado, no como N casillas: el pegado del código, la navegación con teclado, el borrado y el autocompletado del SMS (autoComplete="one-time-code") los resuelve el input nativo. Las casillas segmentadas exigirían ~100 líneas de manejo de refs y foco para el mismo resultado visible.
No lleva
maxLength: ese atributo cuenta caracteres crudos y truncaba un pegado con separadores ("48-39 20"→"48-39 ") antes de poder extraer los dígitos. El recorte se hace después de limpiar.
Resto de primitivos
Button (variants primary/secondary/ghost/danger/outline, loading, asChild para renderizar como <a>), Input, Textarea, Label (con required), Card/CardHeader/CardTitle/CardDescription/CardContent/CardFooter, Badge, Alert (con ícono propio por variante — el color nunca es el único indicador, CLAUDE.md §26), Skeleton, EmptyState, ErrorState, Tabs, Tooltip, Avatar, Dialog (confirmaciones/acciones destructivas, distinto de Sheet), DropdownMenu, Checkbox, RadioGroup, Switch, Progress (nativo, sin dependencia), Popover, Select.
Badge/Alertno usan fondos translúcidos (bg-danger/15): los tokens de color son hex planos y no soportan el modificador de opacidad de Tailwind sin convertirlos argb()+<alpha-value>. Usan fondo neutro + texto en el color semántico.
Aún no incluido
Toast, Combobox, DatePicker, DataTable, Form (wrapper RHF+Zod), Pagination, y los empresariales RutInput/CurrencyInput/PercentageInput/PhoneInput. Se agregan cuando core-dashboard/core-shell (o un producto) tengan un consumidor real que dicte su forma — no de manera especulativa.
Tests lentos bajo jsdom (conocido, no bloqueante)
Los tests de Tooltip, DropdownMenu, Select, Popover tardan más que el default de vitest: el detector de animaciones de Radix Presence (getAnimations()/animationend) no tiene equivalente real en jsdom y cae a un fallback lento (15-100s por test en este entorno). Se resolvió con dos técnicas, documentadas inline en cada archivo:
- Controlar
opendirectamente en vez de simular el toggle con click/hover, cuando el test no necesita verificar la transición en sí. - Timeout explícito por test cuando sí hace falta una interacción real (
userEvent.click).
En corridas de la suite completa que superan ~10-15 min continuos, el canal RPC interno de vitest (worker↔main thread) puede caerse por agotamiento de recursos del sandbox (Timeout calling "onTaskUpdate"), típicamente durante el test de click de dropdown-menu.test.tsx — no es un fallo de aserción ni un bug del componente (su lógica es idéntica a la de Sheet/Dialog, verificados). Si aparece, correr ese archivo solo.
Desarrollo
pnpm build # compila TS y regenera styles/tokens.css
pnpm teststyles/tokens.css es generado — no editarlo a mano; cambiar src/themes.ts o src/tokens.ts y rebuildear.
