Repository navigation
Temas y skins
English · Español
El aspecto de la app es un sistema por capas: un tema de color (una paleta de tweakcn, seleccionada con data-theme en <html>), un modo (clase claro/oscuro), un skin opcional (Zeus o Hera, data-skin), un material de tarjetas/diálogos (sólido, cristal, mate, data-material), un fondo (data-bg), además de deslizadores de efectos y una escala de la UI. El sistema se hereda de un proyecto hermano y se mantiene intacto; además, los usuarios pueden instalar en ejecución cualquier tema de tweakcn. Todo el estado es del lado del cliente (localStorage); nada del aspecto se guarda en un mazo.
| Capa | Gancho en <html>
|
Valores | Fuente de verdad |
|---|---|---|---|
| Tema de color | data-theme="<nombre>" |
43 nombres integrados (THEMES), más tw-<id> para los instalados |
src/themes.css, src/lib/themes.ts
|
| Modo | clase dark
|
light, dark, system (guardado); se resuelve a claro/oscuro |
ThemeProvider |
| Skin | data-skin |
ausente («Plano», liso), zeus, hera
|
src/styles/skin-zeus.css, skin-hera.css
|
| Material | data-material |
ausente (= solid), glass, matte
|
src/styles/materials.css |
| Fondo | data-bg |
ausente (= liso), gradient, aurora
|
src/styles/materials.css |
| Efectos | variables CSS --macup-blur, --macup-noise, --macup-shadow
|
desenfoque en px, grano 0..1, nivel de sombra 0..4 | efectos de ThemeProvider
|
| Flags |
data-fx-sidebar="1", data-hera-filigree="1"
|
activado/desactivado | ThemeProvider |
| Escala de la UI | html { font-size: N% } |
70 a 150 % | ThemeProvider |
| Fichero | Papel |
|---|---|
src/index.css |
Entrada de Tailwind 4 (@import "tailwindcss", tw-animate-css, fonts.css), tokens por defecto de :root/.dark, mapeo de colores con @theme inline, imports de themes.css y styles/*, anulaciones de la casa (radio, color de borde, excepción del tema base), capa base (selección, cursor), barras de scroll, colores de los toasts, regla de opacidad de los modales. |
src/themes.css |
Un bloque [data-theme="<nombre>"] (claro) y un bloque [data-theme="<nombre>"].dark por tema, 43 temas. Variables CSS puras exportadas de tweakcn (los últimos se produjeron con el generador propio del proyecto; base se añadió a mano). |
src/lib/themes.ts |
Mode, TemaDeSerie (unión de los 43 nombres), ThemeName = TemaDeSerie | \tw-${string}`yTHEMES: { name, label, swatch }[](la lista del selector). La primera entrada esbase, a propósito. El fichero se edita a mano; el orden y los nombres deben coincidir con themes.css`. |
src/components/theme-provider.tsx |
ThemeProvider / useTheme(). De solo lectura por regla del proyecto: es código heredado y aquí no se modifica; se amplía alrededor. |
src/lib/tema-inicial.ts |
ponerTemaAntesDeMontar(): aplica el aspecto guardado a <html> antes del primer pintado. |
src/lib/temas-instalados.ts |
Temas de tweakcn instalados: useTemasInstalados, prepararTemasInstalados, esTemaInstalado. |
src/features/settings/appearance.tsx |
La sección Apariencia de Ajustes (todos los controles). |
src/features/settings/temas-tweakcn.tsx |
El diálogo «Más temas…» y SincronizarTemas. |
src/styles/materials.css, skin-zeus.css, skin-hera.css
|
CSS de materiales/fondo y de los skins, ligado a los atributos data-slot de los componentes compartidos. |
src/fonts.css + public/fonts/*.woff2
|
19 familias de letra locales de peso variable que usan los temas integrados (Inter, Plus Jakarta Sans, Open Sans, DM Sans, Montserrat, Poppins, Geist, Geist Mono, Fira Code, JetBrains Mono, IBM Plex Mono, Roboto Mono, Source Code Pro, Source Serif 4, Lora, Merriweather, Libre Baskerville, Antic, Architects Daughter). Cabecera autogenerada; no se edita a mano. |
src-tauri/src/temas.rs, src-tauri/src/commands/temas.rs
|
La mitad de Rust de los temas instalables. |
biome.json excluye src/index.css, src/themes.css, src/fonts.css y src/styles del lint/formato, de modo que esos ficheros conservan el formato de su generador (indentación de dos espacios).
Valores de fábrica: tema amber-minimal, modo dark, sin skin, sin fondo, material matte, desenfoque de 25 px, grano 0.25, nivel de sombra 3 («Fuerte»), escala 100 %, scroll suave activado, fxSidebar y heraFiligree desactivados. Se eligió amber-minimal porque es el tema integrado más cercano al aspecto dorado sobre negro del sitio que reproduce la app.
ThemeProvider aplica el tema desde un useEffect, que se ejecuta después del primer pintado. Con la ventana mostrándose de inmediato (véase Arquitectura del frontend) eso haría destellar la paleta clara por defecto. Por eso main.tsx llama primero a ponerTemaAntesDeMontar(). Esta refleja la lógica del proveedor: lee proxybuilder.theme (solo se acepta si está en THEMES; si no, amber-minimal; si no, el primer tema), proxybuilder.mode (por defecto dark; system usa matchMedia), pone data-theme y conmuta dark; para cualquier tema salvo base también pone data-skin, data-bg y data-material (el material por defecto es matte; el atributo se omite para solid). Las constantes y las claves de almacenamiento están duplicadas respecto al proveedor: si cambias una, cambia la otra (el propio fichero lo dice). Lo protege src/lib/__tests__/tema-inicial.test.ts (oscuro + mate de fábrica, valores guardados, modo sistema, un tema desconocido recurre al de fábrica, base queda limpio).
| Clave | Valor |
|---|---|
proxybuilder.theme |
nombre del tema |
proxybuilder.mode |
light / dark / system
|
proxybuilder.scale |
número (1 = 100 %) |
proxybuilder.skin |
none / zeus / hera
|
proxybuilder.bg |
none / gradient / aurora
|
proxybuilder.material |
solid / glass / matte
|
proxybuilder.blur, .noise, .shadow
|
números (sombra limitada de 0 a 4) |
proxybuilder.fxSidebar, .heraFiligree
|
"1" / "0"
|
proxybuilder.smoothScroll |
"1" / "0" (activado salvo que sea exactamente "0") |
proxybuilder.temasInstalados |
espejo en JSON de los temas instalados (lo escribe temas-instalados.ts) |
Las lecturas numéricas usan numeroGuardado(clave, defecto, valido) en vez de Number(localStorage.getItem(k)), porque Number(null) === 0 es un número válido y anularía en silencio todos los valores de fábrica.
-
Los tokens de color son el conjunto de shadcn (
--background,--card,--primary,--muted,--accent,--destructive,--border,--input,--ring,--chart-1..5,--sidebar-*), mapeados a Tailwind con@theme inline(--color-*) para que funcionenbg-card,text-muted-foreground, etc. Variante para el modo oscuro:@custom-variant dark (&:is(.dark *)). -
Radio de 14 px:
:root, [data-theme], [data-theme].dark { --radius: 0.875rem !important }. El!importanthace falta para vencer al radio.darkde cada tema. Como el selector es[data-theme](cualquier valor), los temastw-*instalados también lo reciben. Los radios de Tailwind derivan de él (--radius-sm/md/lg/xl= ±4/2 px). -
Un solo color de borde:
--input: var(--border) !importanten todas partes. Varias paletas de tweakcn hacen--inputnotablemente más claro que--border, de modo que el borde de un campo no coincidiría con el de su tarjeta; la regla es «los bordes son siempre iguales; un campo se distingue por su fondo». -
El tema
basees la excepción: es el tema shadcn simple que se conserva como referencia de comparación («¿esta fealdad es del tema o del componente?»). Una regla posterior con la misma especificidad restaura--radius: 0.625remy el--inputde serie para[data-theme="base"]y.dark. Además,ThemeProviderrenderizabaselimpio: skin, fondo, material, efectos extra y sombra no se aplican mientras está activo (las elecciones del usuario siguen guardadas y vuelven al elegir otro tema). La tarjeta «Estilo de superficie» de Ajustes muestra una nota ámbar sobre esto cuandotheme === "base". -
La densidad (10 px) y el tamaño de texto base
text-xsson convenciones a nivel de componente (p-2.5,gap-2.5,text-xs,text-[11px]) y no tokens; véase Convenciones. -
Las barras de scroll están tematizadas (finas,
color-mixde--primaryen--card), incluido[data-slot="scroll-area-thumb"], para que no aparezca ninguna barra nativa blanca. -
Toasts: fondo neutro, color solo en el texto (éxito/aviso/info/error); el error además tiñe el fondo y el borde. Las mezclas usan
oklch, que interpola el tono; un gris neutro tiene tono 0, lo que volvería naranja un borde verde, así que el borde nunca se tiñe salvo en los errores (documentado en línea; cámbialo ain oklabsi un skin da un tono a--card). -
El control segmentado activo se detecta por
aria-checked/aria-pressed, nunca pordata-state="on", porqueTooltipTrigger asChildsobrescribe eldata-statedel item. -
prefers-reduced-motion: reducedesactiva todas las animaciones/transiciones salvoanimate-spin/animate-pulse(un spinner congelado parece un cuelgue).
ThemeProvider pone document.documentElement.style.fontSize = "<escala*100>%" (vacío al 100 %) y elimina explícitamente cualquier zoom. Tailwind se basa en rem, así que todo escala junto. El zoom de CSS no se usa nunca: en WKWebView rompe position: fixed y el posicionamiento de los popper de Radix basado en getBoundingClientRect (los modales centrados se desplazan, las superposiciones no cubren la ventana, los desplegables caen lejos de su disparador). El control de Ajustes avanza de 5 en 5 % entre 70 % y 150 % (ESCALA_MIN/ESCALA_MAX en appearance.tsx).
Tanto los skins como los materiales dan estilo a los componentes mediante atributos data-slot que emiten los componentes compartidos (card, button con data-variant, input, select-trigger, select-content, popover-content, dropdown-menu-content, tooltip-content, toggle-group, toggle-group-item, dialog-content, dialog-overlay, badge). Consumen los tokens del tema activo mediante color-mix, de modo que cualquier paleta (incluidas las instaladas) obtiene un acento coherente.
-
Zeus (
skin-zeus.css): relieve neumórfico. Define variables--ef-*(radio, borde, sombras de panel/botón/pulsado/hueco, superficies con degradado, líneas verticales finas de «filigrana») y las aplica a tarjetas, botones (sin animación debox-shadow, que da tirones en WKWebView), campos, menús, interruptores y diálogos. -
Hera (
skin-hera.css): diseño de cristal flotante: variables--hera-*(cristal, borde, línea fina, rayado vertical--hera-lines, brillo). El flag opcional de filigrana (data-hera-filigree="1") superpone el rayado en cada superficie de Hera; en las tarjetas se dibuja en una capa::afterpor encima del contenido para que el escalado de capas de la GPU no lo desenfoque, y el panel de contenido deliberadamente no lo lleva (haría moiré contra las tarjetas de cristal). -
Materiales (
materials.css):glass=color-mixde--cardcon transparencia +backdrop-filter: blur(var(--macup-blur));matte= cristal más un grano de ruido fractal SVG en::before(opacidad--macup-noise,mix-blend-mode: overlay). La sombra de las tarjetas esvar(--macup-shadow), escrita porThemeProvidera partir de la tabla de sombras de cinco pasos (ninguna, suave, normal, fuerte, muy fuerte; forzada anoneparabase). Los diálogos se añadieron a las reglas de material como una superficie más (sin cambiarpositionen el diálogo: esfixedy perdería su centrado). -
fxSidebar(«Incluye los paneles») extiende el cristal/mate a los slotssidebar-innerytopbar.
Deducido: esta app no tiene ningún elemento con los slots sidebar-wrapper, sidebar-inset, sidebar-inner o topbar (no encontré ninguno en src/), porque esas reglas se heredaron del proyecto hermano. Así que el ajuste «Fondo de pantalla» (degradado/aurora apuntan a sidebar-inset), la aurora del lienzo de Hera y el flag fxSidebar no tienen hoy un destino visible aquí; el estilo de tarjetas, botones, inputs, menús, interruptores y diálogos sí se aplica. Comprueba grep -rn 'data-slot="sidebar' src antes de fiarte de ellos.
Decisión: los diálogos son siempre opacos, sea cual sea el material o el skin. Motivo: con el material mate/cristal o el skin Hera, el diálogo dejaba ver lo que había detrás y costaba leerlo. La última regla de src/index.css fija [data-slot="dialog-content"] { background-color: var(--card) !important; backdrop-filter: none !important }; las imágenes de grano y filigrana (dibujadas por encima del color) siguen viéndose, pero nada de lo que hay debajo se transparenta. Va la última del fichero y usa !important para ganar tanto a materials.css como a las reglas de los skins. El subdiálogo de confirmar la salida (ConfirmarSalida en dialog.tsx) también es data-slot="dialog-content", así que sigue la misma regla, y la misma lista de selectores incluye la tarjeta del tutorial ([data-slot="tutorial-tarjeta"], véase Tutoriales), que flota sobre la pantalla oscurecida y tiene que leerse bien por el mismo motivo.
Tarjetas, cada una respaldada por un setter de useTheme(): Apariencia (modo), Tamaño de la interfaz (+ casilla «Scroll suave»), Estilo de superficie (Plano / Zeus / Hera, + casilla Filigrana cuando es Hera), Fondo de pantalla (Liso / Degradado / Aurora), Material de las tarjetas (Sólido / Cristal / Mate), Efectos (Desenfoque de 0 a 40, activado salvo en sólido; Grano de 0 a 0.5, activado solo en mate; Sombra de 0 a 4; casilla «Incluye los paneles») y Tema de color (una cuadrícula de THEMES con muestras, más el botón «Más temas…»). Más en Ajustes y shell.
Los usuarios pueden añadir temas más allá de los 43 integrados: el registro oficial de tweakcn o cualquier tema de la comunidad por enlace/id. La vista web nunca toca la red; Rust los descarga, valida y guarda.
sequenceDiagram
participant UI as temas-tweakcn.tsx
participant ST as useTemasInstalados
participant IPC as call() / ipc.ts
participant RS as temas.rs (Rust)
participant NET as tweakcn.com / Google Fonts
participant PR as protocolo pbimg://
UI->>IPC: theme_fetch(id o enlace)
IPC->>RS: traer()
RS->>NET: GET /r/themes/id
RS-->>UI: TemaCatalogo (css para la vista previa, colores)
UI->>ST: instalar(id)
ST->>IPC: theme_install
RS->>NET: JSON del tema + CSS de letras + ficheros woff2
RS->>RS: validar variables, escribir temas/id.json, temas/fuentes/hash.woff2
RS-->>ST: TemaInstalado (id tw-id, css con marcador BASE)
ST->>ST: aplicar(): sustituir BASE, actualizar style#temas-tweakcn, añadir a THEMES, espejo en localStorage
UI->>UI: setTheme(tw-id)
Note over PR: letras servidas desde /f/hash.woff2
| Función | Qué hace |
|---|---|
id_de(texto) |
Extrae el id de un tema de lo que pega el usuario: un id suelto, /themes/<id>, /r/themes/<id>.json o /editor/theme?theme=<id>. Cualquier cosa que parezca un enlace debe contener tweakcn.com/. Los ids tienen de 2 a 64 caracteres de [A-Za-z0-9_-]. El fallo es InvalidInput con motivo = "tema_enlace". |
catalogo(externo) |
Descarga https://tweakcn.com/r/themes/registry.json (máx. 4 MiB) en un Vec<TemaCatalogo> (id, nombre, 4 colores de muestra para claro y oscuro, letra principal, CSS de vista previa sin letras). |
traer(externo, texto) |
Un tema por id/enlace para previsualizarlo. |
instalar(externo, datos, texto) |
Descarga el JSON del tema, lo convierte a CSS, descarga las letras que necesita, escribe <app_data>/temas/<id>.json (de forma atómica) y devuelve TemaInstalado { id: "tw-<id>", nombre, css, muestra }. |
instalados(datos) |
Lee todos los temas/*.json, ordenados por nombre. |
quitar(datos, id) |
Borra temas/<id>.json; los ficheros de letras se quedan (pueden pertenecer a otro tema). |
letra(datos, nombre) |
Devuelve los bytes de temas/fuentes/<sha256>.woff2 para el protocolo; el nombre debe ser un hash de 64 hex. |
Seguridad (nunca llega CSS libre a la página): solo se usan cssVars (los mapas theme, light, dark). Los nombres de variable deben cumplir [a-z0-9-]{1,40}; los valores, como máximo 400 caracteres y no pueden contener ; { } < > \ @ ni url(. El CSS lo genera la app con la forma exacta de themes.css: [data-theme="tw-<id>"] { --name: value; } y lo mismo con .dark. Cualquier variable infractora hace fallar el tema entero con InvalidInput. Tests: el_id_de_lo_que_se_pegue, el_css_con_el_formato_de_la_app, nada_de_css_libre, solo_se_bajan_las_letras_que_faltan en temas.rs.
Letras: para font-sans, font-serif, font-mono se toma la primera familia de la pila; los nombres se restringen a [A-Za-z0-9 -] y se saltan cuando ya vienen incluidas (YA_ESTAN, que refleja src/fonts.css más las letras del sistema). Para cada familia restante se pide el CSS de Google Fonts (API v1, pesos 300 a 700, latin y latin-ext) presentándose como un navegador (Google solo sirve woff2 a los navegadores que conoce), se descarga cada woff2 (máx. 2 MiB, debe empezar con la firma wOF2), se guarda por su hash SHA-256 y se reescribe el url(...) a {{PBIMG}}f/<hash>.woff2. Una letra que no se pueda descargar solo registra un aviso y el tema recurre a su pila.
Lista de destinos de red permitidos: todas las descargas pasan por fuentes::Externo (véase Fuentes externas), cuya lista PERMITIDOS incluye tweakcn.com /r/themes/, fonts.googleapis.com /css y fonts.gstatic.com /s/; las redirecciones se vuelven a comprobar y los tamaños tienen tope.
themes_catalog, theme_fetch({id}), theme_install({id}), themes_installed, theme_remove({id}), themes_open_community (abre https://tweakcn.com/community en el navegador del sistema con tauri_plugin_opener; la galería de la comunidad no tiene una API pública de listado, así que los usuarios navegan allí y pegan un enlace).
El protocolo pbimg (src-tauri/src/protocol.rs) tiene una ruta /f/<64-hex>.woff2 (Ruta::Letra) que devuelve font/woff2 con Cache-Control: max-age=31536000, immutable (la URL es un hash del contenido). La CSP de src-tauri/tauri.conf.json permite font-src 'self' data: pbimg: http://pbimg.localhost y style-src 'unsafe-inline' (necesario para el <style> inyectado). El path traversal se bloquea exigiendo que el nombre sea un hash válido (es_huella).
-
BASE = "{{PBIMG}}": Rust no puede conocer la raíz del protocolo (cambia según la plataforma), así que el CSS guardado lleva este marcador yaplicar()lo sustituye porraizProtocolo()(src/lib/image-url.ts). -
aplicar(temas)escribe el CSS de todos los temas en un único<style id="temas-tweakcn">en<head>y reconstruye la colatw-*deTHEMES(elimina todas las entradastw-existentes y añade las actuales conswatch = muestra).THEMESes un array de módulo mutable; lo leen tanto el selector comoThemeProvider. -
useTemasInstalados:temas(nullhasta que responde Rust),cargar(),instalar(id)(ordena por nombre, sustituye una entrada existente con el mismo id para que reinstalar no duplique),quitar(id). Cada mutación llama aponer()= aplicar + recordar + fijar el estado. -
recordar()refleja la lista de instalados enlocalStorage["proxybuilder.temasInstalados"], yprepararTemasInstalados()(llamada enmain.tsxantes de montar) la reproduce. Por qué:ThemeProvidersolo acepta un tema guardado que esté enTHEMES; sin el espejo, un tema instalado arrancaría con los colores por defecto hasta que respondiera Rust. Un espejo corrupto se ignora. -
SincronizarTemas(montado enApp.tsx) llama acargar()al arrancar y, si el tema seleccionado es untw-*que ya no existe (quitado, o carpeta de datos borrada), cambia aamber-minimal.TemasTweakcn.quitarTematambién cambia antes de quitarlo si el tema quitado es el activo. - La vista previa del diálogo (
Muestra) inyecta el CSS del candidato en un<style>y renderiza una carta de muestra dentro de un elemento condata-theme="tw-<id>"(másdarkcuando la app está en modo oscuro), de modo que el tema real no se aplica a la app hasta que se instala. - La lista del catálogo excluye los ids que ya coinciden con el nombre de un tema integrado.
Protección: src/lib/__tests__/temas-instalados.test.ts (instalar/quitar no duplican ni dejan restos; el marcador de la raíz se sustituye; la memoria se restaura antes de montar; una memoria rota no rompe el arranque).
Pega la exportación de tweakcn como bloques [data-theme="<nombre>"] más [data-theme="<nombre>"].dark en src/themes.css, añade el nombre a TemaDeSerie y una entrada a THEMES en src/lib/themes.ts (nombre, etiqueta, color de muestra). Si necesita una familia de letra nueva, añade su woff2 en public/fonts/, un @font-face en src/fonts.css y el nombre de la familia en minúsculas a YA_ESTAN en src-tauri/src/temas.rs para que los temas instalados no la vuelvan a descargar. No edites theme-provider.tsx.
-
ThemeProvideres de solo lectura por convención; los cambios de comportamiento van entema-inicial.ts,temas-instalados.tsy CSS. - Las tres constantes de claves de almacenamiento y los valores por defecto existen tanto en
theme-provider.tsxcomo entema-inicial.ts. - Las anulaciones del radio y del color de borde usan
!importanten selectores[data-theme]; un tema nuevo no puede excluirse salvo siendobase. - Todo lo translúcido sobre la página (cristal,
data-bg) necesita algo detrás que desenfocar; con materialglass/mattey sin fondo, la hoja de estilos añade un velo radial tenue por ese motivo. - Prueba los skins en WebKitGTK, no en Chrome (el desenfoque, el backdrop-filter y el comportamiento de las capas de GPU difieren).
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia