From e34528c886ca11f1e4650507e433e3d53909518b Mon Sep 17 00:00:00 2001 From: Gato Date: Fri, 19 Jun 2026 15:04:11 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20th=C3=A8mes=20rapides=20g=C3=A9r=C3=A9s?= =?UTF-8?q?=20par=20le=20backend=20via=20fichiers=20CSS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les thèmes ne sont plus codés en dur dans le JS. Chaque thème est un fichier CSS dans src-tauri/resources/themes/, embarqué à la compilation via include_str!. Le backend expose list_themes et get_theme_css comme commandes Tauri ; le frontend charge et met en cache le CSS à la demande. Ajout du thème "Défaut" (basé sur Shell Indigo) avec commentaires sur chaque règle et rendu complet des tableaux (en-têtes en majuscules, bordure extérieure, séparateurs verticaux, lignes alternées) aligné sur MarkdownRender-nodejs. Co-Authored-By: Claude Sonnet 4.6 --- src-tauri/resources/themes/dark.css | 9 + src-tauri/resources/themes/default.css | 234 ++++++++++++++++++ src-tauri/resources/themes/emerald.css | 3 + src-tauri/resources/themes/large-text.css | 2 + src-tauri/resources/themes/sepia.css | 6 + src-tauri/resources/themes/shell-indigo.css | 27 ++ src-tauri/src/application/mod.rs | 1 + src-tauri/src/application/theme_service.rs | 59 +++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/theme.rs | 54 ++++ src-tauri/src/domain/mod.rs | 1 + src-tauri/src/domain/theme.rs | 39 +++ src-tauri/src/infrastructure/mod.rs | 1 + .../src/infrastructure/theme_repository.rs | 68 +++++ src-tauri/src/lib.rs | 2 + src/index.html | 7 +- src/main.js | 2 +- src/services/themes.js | 9 + src/ui/css-modal.js | 81 +++--- 19 files changed, 560 insertions(+), 46 deletions(-) create mode 100644 src-tauri/resources/themes/dark.css create mode 100644 src-tauri/resources/themes/default.css create mode 100644 src-tauri/resources/themes/emerald.css create mode 100644 src-tauri/resources/themes/large-text.css create mode 100644 src-tauri/resources/themes/sepia.css create mode 100644 src-tauri/resources/themes/shell-indigo.css create mode 100644 src-tauri/src/application/theme_service.rs create mode 100644 src-tauri/src/commands/theme.rs create mode 100644 src-tauri/src/domain/theme.rs create mode 100644 src-tauri/src/infrastructure/theme_repository.rs create mode 100644 src/services/themes.js diff --git a/src-tauri/resources/themes/dark.css b/src-tauri/resources/themes/dark.css new file mode 100644 index 0000000..39e2c9d --- /dev/null +++ b/src-tauri/resources/themes/dark.css @@ -0,0 +1,9 @@ +.content { background: #1e1e2e; color: #cdd6f4; } +.content h1, .content h2, .content h3, .content h4, .content h5, .content h6 { color: #cdd6f4; } +.content p, .content ul li, .content ol li { color: #cdd6f4; } +.content a { color: #89b4fa; } +.content a:visited { color: #cba6f7; } +.content code { background: #313244; color: #cba6f7; } +.content pre { background: #313244; } +.content blockquote { border-color: #6c7086; color: #a6adc8; } +.content table td, .content table th { color: #cdd6f4; } diff --git a/src-tauri/resources/themes/default.css b/src-tauri/resources/themes/default.css new file mode 100644 index 0000000..1bd593d --- /dev/null +++ b/src-tauri/resources/themes/default.css @@ -0,0 +1,234 @@ +/* ───────────────────────────────────────────── + Thème par défaut — basé sur Shell Indigo + Modifiez les valeurs pour personnaliser + l'apparence de Pena. + ───────────────────────────────────────────── */ + + +/* ── Page ──────────────────────────────────── */ + +/* Couleur de fond de la fenêtre (visible autour du contenu) */ +body { + background: #f1f5f9; +} + +/* Couleur de surbrillance lors d'une sélection de texte */ +::selection { + background: #c7d2fe; +} + + +/* ── Zone de contenu ────────────────────────── */ + +/* Fond et couleur de texte principale de la zone de lecture */ +.content { + background: #ffffff; + color: #1e293b; +} + + +/* ── Liens ──────────────────────────────────── */ + +/* Lien normal (non visité) */ +.content a { + color: #4f46e5; + border-bottom: 1px solid #c7d2fe; +} + +/* Lien au survol de la souris */ +.content a:hover { + color: #3730a3; + border-bottom-color: #4f46e5; +} + +/* Lien déjà visité */ +.content a:visited { + color: #6d28d9; +} + +/* Lien déjà visité au survol */ +.content a:visited:hover { + color: #3730a3; +} + + +/* ── Titres ─────────────────────────────────── */ + +/* Couleur commune à tous les niveaux de titre (h1 à h6) */ +.content h1, +.content h2, +.content h3, +.content h4, +.content h5, +.content h6 { + color: #0f172a; +} + + +/* ── Texte courant ──────────────────────────── */ + +/* Paragraphes */ +.content p { + color: #1e293b; +} + +/* Éléments de liste (à puces et numérotées) */ +.content ul li, +.content ol li { + color: #1e293b; +} + +/* Texte en gras */ +.content strong { + color: #0f172a; +} + +/* Ligne de séparation horizontale (---) */ +.content hr { + border-color: #e2e8f0; +} + + +/* ── Code ───────────────────────────────────── */ + +/* Code inline (entre backticks simples) */ +.content code { + background: #f1f5f9; + color: #4f46e5; + border: 1px solid #e2e8f0; +} + +/* Bloc de code (entre triple backticks) */ +.content pre { + background: #0f172a; + border: 1px solid #1e293b; + border-radius: 10px; +} + +/* Texte à l'intérieur d'un bloc de code */ +.content pre code { + background: none; + color: #e2e8f0; +} + + +/* ── Citation ───────────────────────────────── */ + +/* Bloc de citation (> texte) */ +.content blockquote { + border-color: #818cf8; + background: #f5f3ff; + color: #4338ca; +} + + +/* ── Tableaux ───────────────────────────────── */ + +/* Conteneur : bordure extérieure et coins arrondis */ +.content table { + font-size: 0.875rem; + border: 1px solid #cbd5e1; + border-radius: 8px; + overflow: hidden; + color: #374151; +} + +/* Ligne d'en-tête : fond gris clair */ +.content thead { + background: #e2e8f0; +} + +/* Cellule d'en-tête : texte en majuscules espacées, séparateur vertical */ +.content table th { + padding: 0.6rem 1rem; + font-size: 0.77rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: #334155; + border-bottom: 2px solid #4f46e5; + border-right: 1px solid #cbd5e1; + text-align: left; +} + +/* Pas de bordure droite sur la dernière colonne d'en-tête */ +.content table th:last-child { + border-right: none; +} + +/* Cellule de données : padding, séparateurs horizontaux et verticaux */ +.content table td { + padding: 0.6rem 1rem; + border-bottom: 1px solid #e2e8f0; + border-right: 1px solid #e2e8f0; + vertical-align: top; +} + +/* Pas de bordure droite sur la dernière colonne */ +.content table td:last-child { + border-right: none; +} + +/* Pas de bordure basse sur la dernière ligne (évite le double bord avec le conteneur) */ +.content tbody tr:last-child td { + border-bottom: none; +} + +/* Lignes paires : fond légèrement différent pour faciliter la lecture */ +.content tbody tr:nth-child(even) { + background: #f8fafc; +} + +/* Ligne au survol : mise en évidence */ +.content tbody tr:hover { + background: #eff6ff; +} + + +/* ── Barre latérale ─────────────────────────── */ + +/* Lien de premier niveau au survol */ +.sidebar__menu__first-level-title__link:hover { + color: #818cf8; +} + +/* Lien de premier niveau actif (page ouverte) */ +.sidebar__menu__first-level-title__link.active { + color: #818cf8; + background: rgba(129, 140, 248, 0.08); +} + +/* Lien de second niveau au survol */ +.sidebar__menu__second-level-title__link:hover { + color: #818cf8; +} + +/* Lien de second niveau actif (page ouverte) */ +.sidebar__menu__second-level-title__link.active { + color: #818cf8; +} + + +/* ── Barre de titre ─────────────────────────── */ + +/* Fond et bordure inférieure de la barre de titre */ +#titlebar { + background: #0f172a; + border-bottom: 1px solid #1e293b; +} + +/* Texte du titre de la fenêtre */ +#titlebar-title { + color: rgba(255, 255, 255, 0.5); +} + +/* Boutons de contrôle (réduire, agrandir, fermer) au repos */ +.titlebar-controls button { + color: rgba(255, 255, 255, 0.45); +} + +/* Boutons de contrôle au survol */ +.titlebar-controls button:hover { + background: rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.9); +} diff --git a/src-tauri/resources/themes/emerald.css b/src-tauri/resources/themes/emerald.css new file mode 100644 index 0000000..44be8bd --- /dev/null +++ b/src-tauri/resources/themes/emerald.css @@ -0,0 +1,3 @@ +.content a:hover { color: #10b981; } +.content table th { border-bottom-color: #10b981; } +.content table td { border-bottom-color: rgba(16, 185, 129, 0.25); } diff --git a/src-tauri/resources/themes/large-text.css b/src-tauri/resources/themes/large-text.css new file mode 100644 index 0000000..92f8d78 --- /dev/null +++ b/src-tauri/resources/themes/large-text.css @@ -0,0 +1,2 @@ +.content { font-size: 18px; } +.content p { line-height: 1.8; } diff --git a/src-tauri/resources/themes/sepia.css b/src-tauri/resources/themes/sepia.css new file mode 100644 index 0000000..398ce85 --- /dev/null +++ b/src-tauri/resources/themes/sepia.css @@ -0,0 +1,6 @@ +.content { background: #f4ecd8; } +.content h1, .content h2, .content h3, .content h4, .content h5, .content h6 { color: #5c4a1e; } +.content p, .content ul li, .content ol li { color: #5c4a1e; } +.content a { color: #7a5c2e; } +.content code { background: #e8d5b0; color: #5c4a1e; } +.content pre { background: #e8d5b0; } diff --git a/src-tauri/resources/themes/shell-indigo.css b/src-tauri/resources/themes/shell-indigo.css new file mode 100644 index 0000000..d60b943 --- /dev/null +++ b/src-tauri/resources/themes/shell-indigo.css @@ -0,0 +1,27 @@ +body { background: #f1f5f9; } +::selection { background: #c7d2fe; } +.content { background: #ffffff; color: #1e293b; } +.content a { color: #4f46e5; border-bottom: 1px solid #c7d2fe; } +.content a:hover { color: #3730a3; border-bottom-color: #4f46e5; } +.content a:visited { color: #6d28d9; } +.content a:visited:hover { color: #3730a3; } +.content h1, .content h2, .content h3, .content h4, .content h5, .content h6 { color: #0f172a; } +.content p { color: #1e293b; } +.content ul li, .content ol li { color: #1e293b; } +.content code { background: #f1f5f9; color: #4f46e5; border: 1px solid #e2e8f0; } +.content pre { background: #0f172a; border: 1px solid #1e293b; border-radius: 10px; } +.content pre code { background: none; color: #e2e8f0; } +.content blockquote { border-color: #818cf8; background: #f5f3ff; color: #4338ca; } +.content table { color: #374151; } +.content table th { border-bottom-color: #4f46e5; color: #334155; } +.content table td { border-bottom-color: rgba(79, 70, 229, 0.25); } +.content strong { color: #0f172a; } +.content hr { border-color: #e2e8f0; } +.sidebar__menu__first-level-title__link:hover { color: #818cf8; } +.sidebar__menu__first-level-title__link.active { color: #818cf8; background: rgba(129, 140, 248, 0.08); } +.sidebar__menu__second-level-title__link:hover { color: #818cf8; } +.sidebar__menu__second-level-title__link.active { color: #818cf8; } +#titlebar { background: #0f172a; border-bottom: 1px solid #1e293b; } +#titlebar-title { color: rgba(255,255,255,0.5); } +.titlebar-controls button { color: rgba(255,255,255,0.45); } +.titlebar-controls button:hover { background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.9); } diff --git a/src-tauri/src/application/mod.rs b/src-tauri/src/application/mod.rs index 7745604..d9978c6 100644 --- a/src-tauri/src/application/mod.rs +++ b/src-tauri/src/application/mod.rs @@ -1,2 +1,3 @@ pub mod file_service; pub mod render_service; +pub mod theme_service; diff --git a/src-tauri/src/application/theme_service.rs b/src-tauri/src/application/theme_service.rs new file mode 100644 index 0000000..f9d14b0 --- /dev/null +++ b/src-tauri/src/application/theme_service.rs @@ -0,0 +1,59 @@ +use crate::domain::theme::{Theme, ThemeRepository}; + +pub fn list_themes(repo: &dyn ThemeRepository) -> Vec { + repo.list() +} + +pub fn get_theme_css(repo: &dyn ThemeRepository, id: &str) -> Option { + repo.get_css(id) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::domain::theme::{Theme, ThemeRepository}; + + struct MockRepo { + themes: Vec<(&'static str, &'static str, &'static str)>, + } + + impl ThemeRepository for MockRepo { + fn list(&self) -> Vec { + self.themes.iter().map(|(id, label, _)| Theme { id: id.to_string(), label: label.to_string() }).collect() + } + fn get_css(&self, id: &str) -> Option { + self.themes.iter().find(|(i, _, _)| *i == id).map(|(_, _, css)| css.to_string()) + } + } + + fn mock_repo() -> MockRepo { + MockRepo { + themes: vec![ + ("dark", "Mode sombre", ".content{background:#000}"), + ("light", "Clair", ".content{background:#fff}"), + ], + } + } + + #[test] + fn list_themes_delegates_to_repo() { + let repo = mock_repo(); + let themes = list_themes(&repo); + assert_eq!(themes.len(), 2); + assert_eq!(themes[0].id, "dark"); + assert_eq!(themes[1].label, "Clair"); + } + + #[test] + fn get_theme_css_returns_css_for_known_id() { + let repo = mock_repo(); + let css = get_theme_css(&repo, "dark"); + assert_eq!(css, Some(".content{background:#000}".to_string())); + } + + #[test] + fn get_theme_css_returns_none_for_unknown_id() { + let repo = mock_repo(); + assert!(get_theme_css(&repo, "unknown").is_none()); + } +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index bf80f3e..4427544 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,2 +1,3 @@ pub mod render; +pub mod theme; pub mod watch; diff --git a/src-tauri/src/commands/theme.rs b/src-tauri/src/commands/theme.rs new file mode 100644 index 0000000..cbe8ef2 --- /dev/null +++ b/src-tauri/src/commands/theme.rs @@ -0,0 +1,54 @@ +use crate::application::theme_service; +use crate::infrastructure::theme_repository::StaticThemeRepository; +use serde::Serialize; + +#[derive(Serialize)] +pub struct ThemeDto { + pub id: String, + pub label: String, +} + +#[tauri::command] +pub fn list_themes() -> Vec { + let repo = StaticThemeRepository; + theme_service::list_themes(&repo) + .into_iter() + .map(|t| ThemeDto { id: t.id, label: t.label }) + .collect() +} + +#[tauri::command] +pub fn get_theme_css(id: String) -> Option { + let repo = StaticThemeRepository; + theme_service::get_theme_css(&repo, &id) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn list_themes_returns_non_empty() { + let themes = list_themes(); + assert!(!themes.is_empty()); + } + + #[test] + fn list_themes_ids_are_non_empty() { + for theme in list_themes() { + assert!(!theme.id.is_empty()); + assert!(!theme.label.is_empty()); + } + } + + #[test] + fn get_theme_css_known() { + assert!(get_theme_css("dark".into()).is_some()); + assert!(get_theme_css("shell-indigo".into()).is_some()); + } + + #[test] + fn get_theme_css_unknown_returns_none() { + assert!(get_theme_css("does-not-exist".into()).is_none()); + } +} diff --git a/src-tauri/src/domain/mod.rs b/src-tauri/src/domain/mod.rs index 307c3b5..60f2ed8 100644 --- a/src-tauri/src/domain/mod.rs +++ b/src-tauri/src/domain/mod.rs @@ -1,4 +1,5 @@ mod markdown; +pub mod theme; #[allow(unused_imports)] pub use markdown::{MarkdownRenderer, RenderOptions}; diff --git a/src-tauri/src/domain/theme.rs b/src-tauri/src/domain/theme.rs new file mode 100644 index 0000000..026f24b --- /dev/null +++ b/src-tauri/src/domain/theme.rs @@ -0,0 +1,39 @@ +pub struct Theme { + pub id: String, + pub label: String, +} + +pub trait ThemeRepository: Send + Sync { + fn list(&self) -> Vec; + fn get_css(&self, id: &str) -> Option; +} + +#[cfg(test)] +mod tests { + use super::*; + + struct StubRepo; + impl ThemeRepository for StubRepo { + fn list(&self) -> Vec { + vec![Theme { id: "a".into(), label: "A".into() }] + } + fn get_css(&self, id: &str) -> Option { + if id == "a" { Some(".x{}".into()) } else { None } + } + } + + #[test] + fn theme_fields_accessible() { + let t = Theme { id: "dark".into(), label: "Mode sombre".into() }; + assert_eq!(t.id, "dark"); + assert_eq!(t.label, "Mode sombre"); + } + + #[test] + fn repository_trait_object_works() { + let repo: Box = Box::new(StubRepo); + assert_eq!(repo.list().len(), 1); + assert!(repo.get_css("a").is_some()); + assert!(repo.get_css("unknown").is_none()); + } +} diff --git a/src-tauri/src/infrastructure/mod.rs b/src-tauri/src/infrastructure/mod.rs index e04ccaf..dedaea3 100644 --- a/src-tauri/src/infrastructure/mod.rs +++ b/src-tauri/src/infrastructure/mod.rs @@ -1,3 +1,4 @@ pub mod comrak_renderer; pub mod file_repository; pub mod notify_watcher; +pub mod theme_repository; diff --git a/src-tauri/src/infrastructure/theme_repository.rs b/src-tauri/src/infrastructure/theme_repository.rs new file mode 100644 index 0000000..c5a60d8 --- /dev/null +++ b/src-tauri/src/infrastructure/theme_repository.rs @@ -0,0 +1,68 @@ +use crate::domain::theme::{Theme, ThemeRepository}; + +const DEFAULT_CSS: &str = include_str!("../../resources/themes/default.css"); +const DARK_CSS: &str = include_str!("../../resources/themes/dark.css"); +const SEPIA_CSS: &str = include_str!("../../resources/themes/sepia.css"); +const LARGE_TEXT_CSS: &str = include_str!("../../resources/themes/large-text.css"); +const EMERALD_CSS: &str = include_str!("../../resources/themes/emerald.css"); +const SHELL_INDIGO_CSS: &str = include_str!("../../resources/themes/shell-indigo.css"); + +pub struct StaticThemeRepository; + +impl ThemeRepository for StaticThemeRepository { + fn list(&self) -> Vec { + vec![ + Theme { id: "default".into(), label: "Défaut".into() }, + Theme { id: "dark".into(), label: "Mode sombre".into() }, + Theme { id: "sepia".into(), label: "Sépia".into() }, + Theme { id: "large-text".into(), label: "Grand texte".into() }, + Theme { id: "emerald".into(), label: "Accent émeraude".into() }, + Theme { id: "shell-indigo".into(), label: "Shell Indigo".into() }, + ] + } + + fn get_css(&self, id: &str) -> Option { + match id { + "default" => Some(DEFAULT_CSS.to_string()), + "dark" => Some(DARK_CSS.to_string()), + "sepia" => Some(SEPIA_CSS.to_string()), + "large-text" => Some(LARGE_TEXT_CSS.to_string()), + "emerald" => Some(EMERALD_CSS.to_string()), + "shell-indigo" => Some(SHELL_INDIGO_CSS.to_string()), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::domain::theme::ThemeRepository; + + #[test] + fn list_returns_all_themes() { + let repo = StaticThemeRepository; + let themes = repo.list(); + assert_eq!(themes.len(), 6); + let ids: Vec<&str> = themes.iter().map(|t| t.id.as_str()).collect(); + assert!(ids.contains(&"default")); + assert!(ids.contains(&"dark")); + assert!(ids.contains(&"shell-indigo")); + } + + #[test] + fn get_css_known_themes() { + let repo = StaticThemeRepository; + for id in ["default", "dark", "sepia", "large-text", "emerald", "shell-indigo"] { + let css = repo.get_css(id); + assert!(css.is_some(), "CSS manquant pour le thème {id}"); + assert!(!css.unwrap().is_empty()); + } + } + + #[test] + fn get_css_unknown_returns_none() { + let repo = StaticThemeRepository; + assert!(repo.get_css("nonexistent").is_none()); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6a0118e..54359be 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,6 +17,8 @@ pub fn run() { commands::render::list_md_files, commands::watch::start_watch, commands::watch::stop_watch, + commands::theme::list_themes, + commands::theme::get_theme_css, ]) .run(tauri::generate_context!()) .expect("Erreur lors du démarrage de l'application Tauri"); diff --git a/src/index.html b/src/index.html index 945789b..ceefa27 100644 --- a/src/index.html +++ b/src/index.html @@ -76,12 +76,7 @@

Thèmes rapides

-
- - - - -
+

Personnalisez l'apparence de vos pages en modifiant le CSS ci-dessous. Les styles s'appliquent à l'onglet actif.

diff --git a/src/main.js b/src/main.js index 22f248a..f6182ac 100644 --- a/src/main.js +++ b/src/main.js @@ -32,5 +32,5 @@ document.getElementById('btn-open-dir').addEventListener('click', async () => { document.getElementById('btn-back').addEventListener('click', showHome); -initCssModal(); +await initCssModal(); showHome(); diff --git a/src/services/themes.js b/src/services/themes.js new file mode 100644 index 0000000..c989062 --- /dev/null +++ b/src/services/themes.js @@ -0,0 +1,9 @@ +const { invoke } = window.__TAURI__.core; + +export function listThemes() { + return invoke('list_themes'); +} + +export function getThemeCss(id) { + return invoke('get_theme_css', { id }); +} diff --git a/src/ui/css-modal.js b/src/ui/css-modal.js index 63349b5..0f2c205 100644 --- a/src/ui/css-modal.js +++ b/src/ui/css-modal.js @@ -1,33 +1,10 @@ +import { listThemes, getThemeCss } from '../services/themes.js'; + const CSS_TABS = ['general', 'police', 'contenu', 'avance']; let activeTab = CSS_TABS[0]; const QUICK_THEME_KEY = 'pena_quick_theme'; - -const QUICK_THEMES = { - dark: `.content { background: #1e1e2e; color: #cdd6f4; } -.content h1, .content h2, .content h3, .content h4, .content h5, .content h6 { color: #cdd6f4; } -.content p, .content ul li, .content ol li { color: #cdd6f4; } -.content a { color: #89b4fa; } -.content a:visited { color: #cba6f7; } -.content code { background: #313244; color: #cba6f7; } -.content pre { background: #313244; } -.content blockquote { border-color: #6c7086; color: #a6adc8; } -.content table td, .content table th { color: #cdd6f4; }`, - - sepia: `.content { background: #f4ecd8; } -.content h1, .content h2, .content h3, .content h4, .content h5, .content h6 { color: #5c4a1e; } -.content p, .content ul li, .content ol li { color: #5c4a1e; } -.content a { color: #7a5c2e; } -.content code { background: #e8d5b0; color: #5c4a1e; } -.content pre { background: #e8d5b0; }`, - - 'large-text': `.content { font-size: 18px; } -.content p { line-height: 1.8; }`, - - emerald: `.content a:hover { color: #10b981; } -.content table th { border-bottom-color: #10b981; } -.content table td { border-bottom-color: rgba(16, 185, 129, 0.25); }`, -}; +const themeCssCache = new Map(); function cssKey(tab) { return `pena_css_${tab}`; } @@ -47,7 +24,7 @@ function applyCustomCss() { } const tabCss = CSS_TABS.map(t => loadTabCss(t)).filter(Boolean).join('\n'); const activeTheme = localStorage.getItem(QUICK_THEME_KEY); - const themeCss = activeTheme ? (QUICK_THEMES[activeTheme] ?? '') : ''; + const themeCss = activeTheme ? (themeCssCache.get(activeTheme) ?? '') : ''; styleEl.textContent = [tabCss, themeCss].filter(Boolean).join('\n'); } @@ -58,7 +35,44 @@ function syncThemeBadges() { }); } -export function initCssModal() { +function buildThemeBadges(themes) { + const list = document.getElementById('quick-themes-list'); + list.innerHTML = ''; + for (const { id, label } of themes) { + const btn = document.createElement('button'); + btn.className = 'css-modal__theme-badge'; + btn.dataset.theme = id; + btn.textContent = label; + btn.addEventListener('click', async () => { + const current = localStorage.getItem(QUICK_THEME_KEY); + if (current === id) { + localStorage.removeItem(QUICK_THEME_KEY); + } else { + if (!themeCssCache.has(id)) { + const css = await getThemeCss(id); + if (css) themeCssCache.set(id, css); + } + localStorage.setItem(QUICK_THEME_KEY, id); + } + applyCustomCss(); + syncThemeBadges(); + }); + list.appendChild(btn); + } +} + +async function preloadActiveThemeCss() { + const activeTheme = localStorage.getItem(QUICK_THEME_KEY); + if (activeTheme && !themeCssCache.has(activeTheme)) { + const css = await getThemeCss(activeTheme); + if (css) themeCssCache.set(activeTheme, css); + } +} + +export async function initCssModal() { + const themes = await listThemes(); + buildThemeBadges(themes); + await preloadActiveThemeCss(); applyCustomCss(); syncThemeBadges(); @@ -104,15 +118,4 @@ export function initCssModal() { applyCustomCss(); syncThemeBadges(); }); - - document.querySelectorAll('.css-modal__theme-badge').forEach(btn => { - btn.addEventListener('click', () => { - const theme = btn.dataset.theme; - const current = localStorage.getItem(QUICK_THEME_KEY); - if (current === theme) localStorage.removeItem(QUICK_THEME_KEY); - else localStorage.setItem(QUICK_THEME_KEY, theme); - applyCustomCss(); - syncThemeBadges(); - }); - }); }