Temas
Un tema de Claritas son, en lo esencial, dos colores y un modo. Todo lo demás —los estados de hover, los colores de texto contrastados, las superficies del modo oscuro, la escala del acento— se deriva. Cualquier derivada se puede sobreescribir, pero rara vez hace falta.
import { applyTheme } from '@openfactu/ui';
applyTheme({ mode: 'dark', colors: { primary: '#1E102C', accent: '#EC4899' },});Los componentes nunca leen el contexto de React: consumen variables CSS. Por eso
cambiar el tema de una empresa entera es reescribir unas cuantas variables sobre
<html>, sin recompilar ni volver a montar nada.
applyTheme
Sección titulada «applyTheme»Escribe el tema como variables CSS sobre un elemento y devuelve el tema ya resuelto. Es idempotente: llamarla dos veces con lo mismo no acumula estado.
const resuelto = applyTheme({ mode: 'light', colors: { primary: '#0A1628', accent: '#0D9488' } });Sin target escribe en <html>, que es lo habitual. Pasándole un elemento tematiza
solo ese subárbol, que es como se previsualiza un tema dentro de la propia interfaz:
const previewRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => { if (previewRef.current) applyTheme(temaEnPruebas, previewRef.current);}, [temaEnPruebas]);
<div ref={previewRef}> <VistaPreviaDeLaAplicacion /></div>Firma y opciones
Sección titulada «Firma y opciones»applyTheme(theme, target?, options?): ResolvedTheme| Opción | Tipo | Por defecto | Descripción |
|---|---|---|---|
applyDarkClass |
boolean |
true |
Alterna la clase dark en el elemento |
injectFontLink |
boolean |
true |
Inyecta el <link> de Google Fonts de la tipografía elegida |
setBackground |
boolean |
true |
Fija style.background en modo oscuro, para que no se vea el fondo claro entre el primer pintado y el del layout. Solo aplica sobre <html> |
clearTheme(target?) retira las variables en línea que puso applyTheme.
Qué se puede definir
Sección titulada «Qué se puede definir»Un tema no son solo colores: también la redondez y la tipografía, y todos los componentes las siguen.
applyTheme({ mode: 'light', colors: { primary: '#0A1628', accent: '#0D9488' }, radius: 'lg', // 'none' | 'sm' | 'md' | 'lg', o { xs, sm, md, lg, full } typography: { fontFamily: 'geist' }, // id de FONT_OPTIONS});| Campo | Tipo | Descripción |
|---|---|---|
mode |
'light' | 'dark' |
Modo |
colors |
ThemeColors |
Solo primary y accent son obligatorios |
typography |
{ fontFamily?, sans?, display?, mono? } |
fontFamily es un id de FONT_OPTIONS y gana sobre sans/display |
radius |
'none' | 'sm' | 'md' | 'lg' | ThemeRadius |
Redondez |
shadows |
{ sm?, md?, lg?, overlay? } |
Sombras |
zIndex |
{ sticky?, dropdown?, overlay?, modal?, popover?, toast?, tooltip? } |
Capas |
vars |
Record<string, string | number> |
Variables CSS crudas, aplicadas al final. Punto de extensión |
Colores derivables
Sección titulada «Colores derivables»De primary y accent salen solos: primaryHover, primaryFg, accentFg, la escala
accentScale, la escala de tinta ink, las superficies (bgApp, bgCard,
bgSidebar, bgMuted, bgHover), los textos (fgDefault, fgBody, fgMuted,
fgSubtle), los bordes y los semánticos de estado con sus versiones -fg, -bg y
-on. Todos se pueden fijar a mano si el cálculo no te convence.
La capa del sidebar (sidebarFg, sidebarFgMuted, sidebarHover, sidebarActive) se
deriva de bgSidebar midiendo contraste real, de modo que un sidebar claro siga
teniendo la navegación legible.
Tipografías
Sección titulada «Tipografías»FONT_OPTIONS trae seis opciones: sans (DM Sans, la de Keirost), roboto,
roboto-flex, geist, serif y mono. Las que necesitan Google Fonts inyectan su
<link> solas.
import { FONT_OPTIONS, fontOptionFor, googleFontsUrl } from '@openfactu/ui';
FONT_OPTIONS.map((f) => ({ id: f.id, label: f.label }));fontOptionFor('geist'); // la opción, o la primera si el id no existegoogleFontsUrl(fontOptionFor('geist'));ThemeProvider
Sección titulada «ThemeProvider»Provider opcional, para manejar el tema con estado de React. Los componentes no lo necesitan: montarlo no es requisito para usar la librería.
import { ThemeProvider } from '@openfactu/ui';
<ThemeProvider defaultTheme={{ mode: 'light', colors: { primary: '#0A1628', accent: '#0D9488' } }} storageKey="keirost-tema"> <App /></ThemeProvider>| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
children |
ReactNode |
— | La aplicación |
theme |
ThemeInput |
— | Tema controlado; manda sobre el estado interno |
defaultTheme |
ThemeInput |
— | Tema inicial cuando no está controlado |
onThemeChange |
(theme: ResolvedTheme) => void |
— | Cambio de tema |
target |
HTMLElement | null | (() => HTMLElement | null) |
document.documentElement |
Dónde escribir las variables |
applyDarkClass |
boolean |
true |
Alterna la clase dark |
injectFontLink |
boolean |
true |
Inyecta el <link> de la tipografía |
storageKey |
string | false |
false |
Clave de localStorage donde persistir |
Dentro se lee con useTheme():
const { theme, mode, toggleMode, applyPreset, activePresetId } = useTheme();UiThemeProvider y useUiTheme son alias con prefijo, para aplicaciones que ya tienen
su propio ThemeProvider/useTheme y necesitan importar los dos en el mismo fichero
durante una migración.
Evitar el parpadeo inicial
Sección titulada «Evitar el parpadeo inicial»resolveTheme es una función pura, así que el tema se puede precalcular y aplicar
antes de que arranque React:
import { resolveTheme, themeToCssText } from '@openfactu/ui';
const css = themeToCssText(resolveTheme(temaDelTenant)); // string de `:root { … }`Métela en un <style> del HTML servido y la primera pintura ya sale con el tema
correcto.
| Función | Firma | Para qué |
|---|---|---|
resolveTheme |
(theme?: ThemeInput) => ResolvedTheme |
Deriva el tema completo. Pura |
themeToCssText |
(theme: ResolvedTheme) => string |
El bloque :root { … } como texto |
themeToCssVars |
(theme: ResolvedTheme) => ThemeCssVars |
Mapa --variable → valor |
themeToStyle |
(theme: ResolvedTheme) => CSSProperties |
Listo para el atributo style |
DEFAULT_THEME |
ResolvedTheme |
El tema por defecto ya resuelto |
Presets
Sección titulada «Presets»THEME_PRESETS trae nueve temas de fábrica:
| id | Nombre | Modo |
|---|---|---|
keirost-classic |
Keirost Clásico | claro |
keirost-teal |
Teal Fuerte | claro |
keirost-slate |
Slate Monocromo | claro |
keirost-midnight |
Midnight | oscuro |
keirost-carbon |
Carbon | oscuro |
keirost-deep-ocean |
Deep Ocean | oscuro |
keirost-forest |
Forest | oscuro |
keirost-plum |
Plum | oscuro |
keirost-nebula |
Nebula | oscuro |
import { THEME_PRESETS, themePresetById, detectPreset } from '@openfactu/ui';
applyTheme(themePresetById('keirost-midnight')!.theme);
detectPreset(temaActual); // 'keirost-midnight', o null si el tema es a medidadetectPreset compara solo modo, primario y acento, que es lo que define un preset.
El catálogo es abierto: temas de plugin
Sección titulada «El catálogo es abierto: temas de plugin»Un plugin puede aportar sus propios temas en tiempo de ejecución y retirarlos al desactivarse.
import { registerThemePreset, registerTokens, unregisterThemeSource } from '@openfactu/ui';
// Al activar el pluginconst bajaTema = registerThemePreset( { id: 'acme-corporativo', label: 'Acme corporativo', description: 'Azul de marca sobre fondo claro.', theme: { mode: 'light', colors: { primary: '#1e3a8a', accent: '#2563eb' } }, }, { source: 'plugin-acme' },);
registerTokens('plugin-acme', { '--acme-ancho-panel': '280px' });
// Al desactivarlobajaTema();unregisterThemeSource('plugin-acme'); // o de golpe, todo lo suyoLos tokens de plugin se aplican después de los de la librería, así que sirven tanto para añadir variables propias como para pisar una concreta.
API del registro
Sección titulada «API del registro»| Función | Firma | Descripción |
|---|---|---|
registerThemePreset |
(preset, { source?, replace? }?) => () => void |
Añade un tema. Devuelve la función de baja |
unregisterThemePreset |
(id: string) => void |
Da de baja un tema |
unregisterThemeSource |
(source: string) => void |
Da de baja todo lo de una misma fuente |
getThemePresets |
() => RegisteredThemePreset[] |
Catálogo completo: integrados y registrados |
getThemePreset |
(id: string) => RegisteredThemePreset | undefined |
Uno por id |
subscribeThemePresets |
(listener) => () => void |
Avisa cuando entra o sale un tema |
registerTokens |
(namespace, vars) => () => void |
Variables CSS propias de un plugin |
unregisterTokens |
(namespace: string) => void |
Las retira |
getRegisteredTokens |
() => Record<string, string> |
Todos los tokens registrados, mezclados |
RegisteredThemePreset es un ThemePreset más source, que vale 'builtin' en los
integrados.
En React, useThemePresets() devuelve el catálogo y se
repinta solo en cuanto entra o sale un tema:
const presets = useThemePresets();const { applyPreset, activePresetId } = useTheme();
{presets.map((p) => ( <button key={p.id} onClick={() => applyPreset(p.id)} aria-pressed={p.id === activePresetId}> {p.label} </button>))}Validar un tema
Sección titulada «Validar un tema»import { validateTheme } from '@openfactu/ui';
const informe = validateTheme({ colors: { primary: '#1e3a8a', accent: '#2563eb' } });
if (!informe.ok) { informe.issues.forEach((i) => console.log(i.level, i.field, i.message));}ThemeValidation es { ok, issues, resolved? }, y cada issue es
{ level: 'error' | 'warning', field, message }. Es el mismo informe que usa el
registro por dentro, así que puedes enseñarlo en tu propia interfaz de personalización
en lugar de dejar que avise por consola.
Los errores son colores mal formados o la falta de primary/accent; los avisos, el
contraste por debajo del mínimo WCAG AA para texto normal.
Utilidades de color
Sección titulada «Utilidades de color»Las funciones de bajo nivel con las que se construyen las derivadas. Rara vez hacen falta, pero se exportan porque a veces hay que calcular un color en JavaScript.
| Símbolo | Firma | Para qué |
|---|---|---|
normalizeHex |
(hex: string) => string |
Normaliza #rgb a #rrggbb |
isValidHex |
(hex: string) => boolean |
Comprueba el formato |
hexToRgb |
(hex: string) => RGB |
De hexadecimal a triplete |
rgbToHex |
(rgb: RGB) => string |
De triplete a hexadecimal |
rgbToSpaceString |
(rgb: RGB) => string |
'12 34 56', para rgb(var(--x) / .3) |
mixRgb |
(a, b, t) => RGB |
Mezcla dos colores |
lighten / lightenRgb |
(color, amount) => … |
Aclara |
darken / darkenRgb |
(color, amount) => … |
Oscurece |
alpha |
(hex, a) => string |
Color con transparencia |
luminance / relativeLuminance |
(color) => number |
Luminancia |
contrastRatio |
(a, b) => number |
Relación de contraste entre dos colores |
contrastingFg / contrastingFgRgb |
(bg) => … |
El texto que más contrasta sobre un fondo |
mutedOn |
(bg) => string |
Variante atenuada legible sobre un fondo |
clampToDarkSurface |
(color) => string |
Recorta un color para que valga como superficie oscura |
DARK_SURFACE_MAX_LUMINANCE |
number |
El techo de luminancia que usa clampToDarkSurface |