Styling & Motion
Chroma-Style, Anime.js y ECharts en Targ-Apps-Finance — política de UI sin Tailwind ni GSAP.
Política de stack visual
| Capa | Tecnología | Ubicación |
|---|---|---|
| Tokens y utilidades | Chroma-Style (@chroma/style) | plugins/chroma-style/ |
| Estilos de app | CSS puro | src/styles/ (cargados en index.html) |
| Animación | Anime.js (solo CDN UMD) | index.html + src/animations.js |
| Gráficos | ECharts (CDN UMD) | index.html → globalThis.echarts |
| Iconos | Lucide (CDN UMD) | index.html → globalThis.lucide |
No se usan Tailwind, GSAP ni otros frameworks de UI. Los componentes Chroma siguen usando cva/cx del motor WASM con presets de Chroma-Style.
Chroma-Style
Declarado en chroma.json:
"plugins": ["chroma-fs", "chroma-t2t", "chroma-style"]plugins/loader.js inyecta tokens.css y utilities.css vía injectStyles(). Los estilos de app se cargan con <link rel="stylesheet"> en index.html (el servidor Chroma sirve .css como text/css, no como módulos ES):
<link rel="stylesheet" href="./plugins/chroma-style/tokens.css" />
<link rel="stylesheet" href="./plugins/chroma-style/utilities.css" />
<link rel="stylesheet" href="./src/styles/theme.css" />
<link rel="stylesheet" href="./src/styles/layout.css" />
<link rel="stylesheet" href="./src/styles/finance.css" />En src/main.js:
import { cva, cx } from "../pkg/chroma.js";
import { loadPlugins } from "../plugins/loader.js";
const plugins = await loadPlugins();
globalThis.__chromaStyle = plugins.style.bootstrapStyle({ cva, cx });Paleta de marca (TargApps)
src/styles/theme.css define los mismos tokens que plugins/chroma-style/tokens.css para evitar flash sin estilo. Variables canónicas --cs-* (hex); alias semánticos (--background, --primary, …) alimentan utilidades y componentes.
| Token | Claro | Oscuro |
|---|---|---|
--cs-background | #FAF9F5 | #262624 |
--cs-foreground | #3D3929 | #C3C0B6 |
--cs-primary | #C96442 | #D97757 |
--cs-secondary | #E9E6DC | #FAF9F5 |
--cs-muted | #EDE9DE | #1B1B19 |
--cs-border | #DAD9D4 | #3E3E38 |
--cs-destructive | #141413 | #EF4444 |
Tipografía (Grift)
--font-sans es Grift Variable, autoalojada en fonts/Grift Variable-VF.ttf. El @font-face vive en src/styles/theme.css (pesos 100–900). Display y cuerpo usan la misma familia con pesos distintos: titular de auth a 750, labels a 550, texto a 400.
Tema: data-theme="light|dark" en <html> o preferencia del sistema (logic/theme.js). Versión del plugin: 0.1.2.
Presets disponibles: presets.button, presets.card, presets.badge, presets.input.
Clases para Anime.js
Chroma-Style expone selectores explícitos (válidos para querySelector):
| Selector | Uso |
|---|---|
.cs-card | Shell de tarjeta |
.cs-btn + variantes (.cs-btn-primary, …) | Botones |
[data-animate="fade-up"] | Entrada con desplazamiento |
[data-animate="fade-in"] | Entrada por opacidad |
[data-animate="scale-in"] | Entrada con escala |
.is-disabled | Estado deshabilitado (alternativa a pseudo-clases) |
Utilidades atómicas (bg-card, text-sm, rounded-lg, …) siguen disponibles. Ver Chroma-Style.
Anime.js
CDN en index.html:
<script src="https://unpkg.com/animejs@4.5.0/dist/bundles/anime.umd.min.js"></script>Helpers en src/animations.js — importar desde páginas o el shell cuando haga falta re-animar tras cambio de ruta:
import { initAnimations, refreshAnimations, animateCards, pressButton, CS_SELECTORS } from "./animations.js";| Export | Descripción |
|---|---|
CS_SELECTORS | Mapa de selectores Chroma-Style |
getAnime() | Acceso a globalThis.anime (o null) |
initAnimations(root?) | Entrada [data-animate] + feedback de click en .cs-btn |
refreshAnimations(root?) | Re-ejecuta entradas tras swap de contenido |
animateEntry(root?) | Solo hooks [data-animate] |
animateCards(root?) | Stagger en .cs-card |
pressButton(el) | Micro-interacción de pulsación |
animateFormFocus(fieldEl) | Micro-animación en foco de campo |
animateSelectOpen(menuEl) | Apertura del dropdown custom Select |
animateSelectClose(menuEl, onComplete?) | Cierre del dropdown custom Select |
animateSelector(sel, params, root?) | Helper genérico |
Respeta prefers-reduced-motion: reduce.
ECharts
CDN en index.html:
<script src="https://cdn.jsdelivr.net/npm/echarts@6.1.0/dist/echarts.min.js" integrity="sha256-tmslrrTfhOMxmdwhaUAU0zbSIsvZ3rDlp8FL1qoND9A=" crossorigin="anonymous"></script>En componentes de gráficos (TAF-013+), usar globalThis.echarts — p. ej. echarts.init(dom) con option de bar, line o pie. Colores desde tokens CSS vía getChartTheme() en chartStyles.js. No añadir npm deps de gráficos ni React.
CSS de app
index.html enlaza, en orden: theme.css (tokens de marca), layout.css (shell y layout), finance.css (componentes y charts). Tokens canónicos también en plugins/chroma-style/tokens.css (data-theme="dark"). No usar import "*.css" en JS — Chroma dev no los sirve como módulos ES.
Tokens compartidos (redesign foundation)
Definidos en finance.css (:root) para shell y páginas:
| Token | Uso |
|---|---|
--taf-shell-ledger | Acento cobre del shell (var(--cs-primary)) — nav activo, reglas ledger |
--taf-frame / --frame | Token del marco viewport deprecado (theme.css, cobre + --card); el header usa --background |
--taf-frame-thickness | Grosor del borde viewport (legado; el marco ya no se monta) |
--taf-frame-corner-size | Tamaño SVG de esquina (legado; 50px) |
--taf-header-label-height | Altura del header (2.75rem) |
--taf-header-label-offset | Inset superior desktop del header (0; el header es flush) |
--taf-page-gap | Separación vertical entre bloques de página (1.5rem) |
--taf-section-gap | Separación entre secciones internas (1.25rem) |
--taf-icon-size-sm/md/lg | 16 / 20 / 24px — iconos Lucide inline |
--taf-z-frame … --taf-z-toast | Stack del shell — ver Layout |
Clases base reutilizables:
| Clase / selector | Uso |
|---|---|
[data-component="page-header"] | Cabecera de página (PageHeader) |
.taf-stat-card / [data-component="stat-card"] | KPI con barra lateral data-variant |
.taf-section-header / .taf-section-title | Encabezados de sección dentro de páginas |
.taf-empty-state / [data-component="empty-state"] | Estados vacíos |
.taf-icon | Wrapper con tamaño fijo para iconos Lucide UMD |
.taf-icon__glyph / svg.lucide | SVG generado por lucide.createIcons() |
.taf-header-label | Header fijo flush (desktop + mobile) |
.site-frame-shell / .site-frame--* | Marco viewport (SiteFrame, no montado) |
.site-corner--* | Esquinas SVG de SiteFrame (no montadas) |
Iconos Lucide (UMD + data-lucide)
Lucide se carga como script UMD en index.html (sin lucide-react ni dependencia npm):
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.js"></script>src/icons.js renderiza placeholders <i data-lucide="icon-name"> dentro de un wrapper .taf-icon con tamaño en px. Tras cada actualización del DOM, refreshLucideIcons() llama a globalThis.lucide.createIcons().
import { Icon, IconSizes, refreshLucideIcons } from "./src/icons.js";
e(Icon, { name: "settings", size: IconSizes.md }) // 20px
e(Icon, { name: "sun", size: 16 })
e(Icon, { name: "arrowLeftRight", size: 14 }) // alias → arrow-left-right| Export | Descripción |
|---|---|
Icon | Wrapper + <i data-lucide> (name, size, class) |
LucideIcon | Alias de Icon (src/components/LucideIcon.js) |
IconSizes | { sm: 16, md: 20, lg: 24 } |
toLucideName(name) | camelCase / alias → kebab-case de Lucide |
refreshLucideIcons(root?) | Ejecuta lucide.createIcons() |
hasIcon(name) | Comprueba que el nombre se resuelva |
Hook de re-render: AppShell en src/App.js ejecuta refreshLucideIcons() en useLayoutEffect (cada ciclo de render). src/main.js también lo llama tras el render inicial.
Nombres en kebab-case (layout-dashboard, chart-column) o camelCase con alias (pieChart → chart-pie, barChart3 → chart-column, building2 → building-2). Catálogo completo en lucide.dev/icons.
ThemeToggle y páginas usan Icon / LucideIcon — no emojis en el chrome compartido. NavBar es solo texto.
Formularios (FormField)
Controles nativos usan clases en finance.css (no utilidades sueltas):
| Clase | Uso |
|---|---|
.taf-form-field | Wrapper label + control + error |
.taf-form-label | Etiqueta |
.taf-form-error / .taf-form-hint | Mensajes inline |
.taf-input | Base para input, textarea |
.taf-select | Wrapper del <Select> custom (listbox) |
.taf-select-trigger | Botón disparador — hereda .taf-input |
.taf-select-menu | Panel desplegable (position: absolute respecto a .taf-select, width: calc(100% + 4px)) |
.taf-select--open | Wrapper abierto + ancestros (:has) con --taf-z-select-open (1300) para quedar sobre campos hermanos y shell |
.taf-select-option | Ítem de lista (role="option") |
.taf-date-picker | Wrapper del <DatePicker> custom (calendario) |
.taf-date-picker-trigger | Botón disparador — hereda .taf-input |
.taf-date-picker-panel | Panel calendario (hairline border, acento cobre en día seleccionado) |
.taf-input--invalid | Borde/outline destructivo |
inputClass(invalid, variant) en src/components/FormField.js compone clases para texto/textarea (variant: text | textarea). Los desplegables usan <Select> (src/components/Select.js) o FormField con variant="select". Las fechas usan <DatePicker> (src/components/DatePicker.js) o FormField con variant="date" — ver DatePicker.
Select custom (Select.js)
Reemplaza <select> nativo en toda la app. API:
| Prop | Tipo | Descripción |
|---|---|---|
value | unknown | () => unknown | Valor controlado |
onChange | (ev) => void | ev.target.value compatible con handlers existentes |
options | { value, label, disabled? }[] | getter | Opciones del listbox |
placeholder | string | Etiqueta cuando value está vacío |
disabled | boolean | getter | Deshabilita el trigger |
invalid | boolean | getter | Estilo .taf-input--invalid |
id / name | string | ARIA + <input type="hidden"> para forms |
aria-labelledby | string | Enlaza con el <label> del FormField |
Comportamiento: teclado (↑/↓, Enter, Escape, Home/End, typeahead), click fuera para cerrar, animación open/close vía animateSelectOpen / animateSelectClose en src/animations.js. El menú se ancla con CSS (top: calc(100% + 2px), left: -2px) al wrapper .taf-select para alinear bordes con el trigger; Select.js solo ajusta max-height y voltea arriba (.taf-select-menu--above) si no hay espacio en viewport. Al abrir, .taf-form-field / .taf-filter-field / .settings-form-row que contienen el select suben a z-index: 1300 para que el listbox no quede tapado por campos o acciones posteriores en el formulario. ARIA: role="listbox" / role="option", aria-expanded, aria-labelledby.
Estados hover/focus usan --cs-border y anillo --cs-primary.
Iconos Lucide (LucideIcon.js)
Re-export de Icon desde src/icons.js. Misma API UMD + data-lucide:
import { LucideIcon } from "../src/components/LucideIcon.js";
e(LucideIcon, { name: "plus", size: 16 })Catálogo usado en transacciones/recurrentes: filter, search, pencil, arrowLeftRight, repeat, pause, play, trash2, x, etc.
Páginas Transacciones y Recurrentes
| Namespace CSS | Página |
|---|---|
.taf-transactions-* | Filtros compactos, filas con monto prominente, grupos por fecha |
.taf-recurring-* | Formulario de regla, tarjetas con badge de automatización y estado |
Convención para agentes
- Componer UI con utilidades Chroma-Style + presets
cva. - Añadir
.cs-card/.cs-btn-*cuando el elemento deba animarse. - Marcar entradas con
data-animateen lugar de estilos inline iniciales. - Gráficos solo vía ECharts UMD; animaciones solo vía Anime.js.