Targ Apps Docs

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

CapaTecnologíaUbicación
Tokens y utilidadesChroma-Style (@chroma/style)plugins/chroma-style/
Estilos de appCSS purosrc/styles/ (cargados en index.html)
AnimaciónAnime.js (solo CDN UMD)index.html + src/animations.js
GráficosECharts (CDN UMD)index.html → globalThis.echarts
IconosLucide (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.

TokenClaroOscuro
--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):

SelectorUso
.cs-cardShell 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-disabledEstado 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";
ExportDescripción
CS_SELECTORSMapa 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:

TokenUso
--taf-shell-ledgerAcento cobre del shell (var(--cs-primary)) — nav activo, reglas ledger
--taf-frame / --frameToken del marco viewport deprecado (theme.css, cobre + --card); el header usa --background
--taf-frame-thicknessGrosor del borde viewport (legado; el marco ya no se monta)
--taf-frame-corner-sizeTamaño SVG de esquina (legado; 50px)
--taf-header-label-heightAltura del header (2.75rem)
--taf-header-label-offsetInset superior desktop del header (0; el header es flush)
--taf-page-gapSeparación vertical entre bloques de página (1.5rem)
--taf-section-gapSeparación entre secciones internas (1.25rem)
--taf-icon-size-sm/md/lg16 / 20 / 24px — iconos Lucide inline
--taf-z-frame … --taf-z-toastStack del shell — ver Layout

Clases base reutilizables:

Clase / selectorUso
[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-titleEncabezados de sección dentro de páginas
.taf-empty-state / [data-component="empty-state"]Estados vacíos
.taf-iconWrapper con tamaño fijo para iconos Lucide UMD
.taf-icon__glyph / svg.lucideSVG generado por lucide.createIcons()
.taf-header-labelHeader 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
ExportDescripción
IconWrapper + <i data-lucide> (name, size, class)
LucideIconAlias 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):

ClaseUso
.taf-form-fieldWrapper label + control + error
.taf-form-labelEtiqueta
.taf-form-error / .taf-form-hintMensajes inline
.taf-inputBase para input, textarea
.taf-selectWrapper del <Select> custom (listbox)
.taf-select-triggerBotón disparador — hereda .taf-input
.taf-select-menuPanel desplegable (position: absolute respecto a .taf-select, width: calc(100% + 4px))
.taf-select--openWrapper 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-pickerWrapper del <DatePicker> custom (calendario)
.taf-date-picker-triggerBotón disparador — hereda .taf-input
.taf-date-picker-panelPanel calendario (hairline border, acento cobre en día seleccionado)
.taf-input--invalidBorde/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:

PropTipoDescripción
valueunknown | () => unknownValor controlado
onChange(ev) => voidev.target.value compatible con handlers existentes
options{ value, label, disabled? }[] | getterOpciones del listbox
placeholderstringEtiqueta cuando value está vacío
disabledboolean | getterDeshabilita el trigger
invalidboolean | getterEstilo .taf-input--invalid
id / namestringARIA + <input type="hidden"> para forms
aria-labelledbystringEnlaza 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 CSSPá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

  1. Componer UI con utilidades Chroma-Style + presets cva.
  2. Añadir .cs-card / .cs-btn-* cuando el elemento deba animarse.
  3. Marcar entradas con data-animate en lugar de estilos inline iniciales.
  4. Gráficos solo vía ECharts UMD; animaciones solo vía Anime.js.

On this page