feat: thèmes rapides gérés par le backend via fichiers CSS
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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; }
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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); }
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.content { font-size: 18px; }
|
||||||
|
.content p { line-height: 1.8; }
|
||||||
@@ -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; }
|
||||||
@@ -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); }
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod file_service;
|
pub mod file_service;
|
||||||
pub mod render_service;
|
pub mod render_service;
|
||||||
|
pub mod theme_service;
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use crate::domain::theme::{Theme, ThemeRepository};
|
||||||
|
|
||||||
|
pub fn list_themes(repo: &dyn ThemeRepository) -> Vec<Theme> {
|
||||||
|
repo.list()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_theme_css(repo: &dyn ThemeRepository, id: &str) -> Option<String> {
|
||||||
|
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<Theme> {
|
||||||
|
self.themes.iter().map(|(id, label, _)| Theme { id: id.to_string(), label: label.to_string() }).collect()
|
||||||
|
}
|
||||||
|
fn get_css(&self, id: &str) -> Option<String> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod render;
|
pub mod render;
|
||||||
|
pub mod theme;
|
||||||
pub mod watch;
|
pub mod watch;
|
||||||
|
|||||||
@@ -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<ThemeDto> {
|
||||||
|
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<String> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mod markdown;
|
mod markdown;
|
||||||
|
pub mod theme;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use markdown::{MarkdownRenderer, RenderOptions};
|
pub use markdown::{MarkdownRenderer, RenderOptions};
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
pub struct Theme {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ThemeRepository: Send + Sync {
|
||||||
|
fn list(&self) -> Vec<Theme>;
|
||||||
|
fn get_css(&self, id: &str) -> Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct StubRepo;
|
||||||
|
impl ThemeRepository for StubRepo {
|
||||||
|
fn list(&self) -> Vec<Theme> {
|
||||||
|
vec![Theme { id: "a".into(), label: "A".into() }]
|
||||||
|
}
|
||||||
|
fn get_css(&self, id: &str) -> Option<String> {
|
||||||
|
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<dyn ThemeRepository> = Box::new(StubRepo);
|
||||||
|
assert_eq!(repo.list().len(), 1);
|
||||||
|
assert!(repo.get_css("a").is_some());
|
||||||
|
assert!(repo.get_css("unknown").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod comrak_renderer;
|
pub mod comrak_renderer;
|
||||||
pub mod file_repository;
|
pub mod file_repository;
|
||||||
pub mod notify_watcher;
|
pub mod notify_watcher;
|
||||||
|
pub mod theme_repository;
|
||||||
|
|||||||
@@ -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<Theme> {
|
||||||
|
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<String> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ pub fn run() {
|
|||||||
commands::render::list_md_files,
|
commands::render::list_md_files,
|
||||||
commands::watch::start_watch,
|
commands::watch::start_watch,
|
||||||
commands::watch::stop_watch,
|
commands::watch::stop_watch,
|
||||||
|
commands::theme::list_themes,
|
||||||
|
commands::theme::get_theme_css,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("Erreur lors du démarrage de l'application Tauri");
|
.expect("Erreur lors du démarrage de l'application Tauri");
|
||||||
|
|||||||
+1
-6
@@ -76,12 +76,7 @@
|
|||||||
<div class="css-modal__body">
|
<div class="css-modal__body">
|
||||||
<div class="css-modal__quick-themes">
|
<div class="css-modal__quick-themes">
|
||||||
<p class="css-modal__quick-themes-label">Thèmes rapides</p>
|
<p class="css-modal__quick-themes-label">Thèmes rapides</p>
|
||||||
<div class="css-modal__quick-themes-list">
|
<div id="quick-themes-list" class="css-modal__quick-themes-list"></div>
|
||||||
<button class="css-modal__theme-badge" data-theme="dark">Mode sombre</button>
|
|
||||||
<button class="css-modal__theme-badge" data-theme="sepia">Sépia</button>
|
|
||||||
<button class="css-modal__theme-badge" data-theme="large-text">Grand texte</button>
|
|
||||||
<button class="css-modal__theme-badge" data-theme="emerald">Accent émeraude</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="css-modal__description">Personnalisez l'apparence de vos pages en modifiant le CSS ci-dessous. Les styles s'appliquent à l'onglet actif.</p>
|
<p class="css-modal__description">Personnalisez l'apparence de vos pages en modifiant le CSS ci-dessous. Les styles s'appliquent à l'onglet actif.</p>
|
||||||
<textarea id="css-editor" class="css-modal__editor" placeholder="/* Entrez votre CSS ici */ /* Exemple : .content { font-size: 18px; } */"></textarea>
|
<textarea id="css-editor" class="css-modal__editor" placeholder="/* Entrez votre CSS ici */ /* Exemple : .content { font-size: 18px; } */"></textarea>
|
||||||
|
|||||||
+1
-1
@@ -32,5 +32,5 @@ document.getElementById('btn-open-dir').addEventListener('click', async () => {
|
|||||||
|
|
||||||
document.getElementById('btn-back').addEventListener('click', showHome);
|
document.getElementById('btn-back').addEventListener('click', showHome);
|
||||||
|
|
||||||
initCssModal();
|
await initCssModal();
|
||||||
showHome();
|
showHome();
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
+42
-39
@@ -1,33 +1,10 @@
|
|||||||
|
import { listThemes, getThemeCss } from '../services/themes.js';
|
||||||
|
|
||||||
const CSS_TABS = ['general', 'police', 'contenu', 'avance'];
|
const CSS_TABS = ['general', 'police', 'contenu', 'avance'];
|
||||||
let activeTab = CSS_TABS[0];
|
let activeTab = CSS_TABS[0];
|
||||||
|
|
||||||
const QUICK_THEME_KEY = 'pena_quick_theme';
|
const QUICK_THEME_KEY = 'pena_quick_theme';
|
||||||
|
const themeCssCache = new Map();
|
||||||
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); }`,
|
|
||||||
};
|
|
||||||
|
|
||||||
function cssKey(tab) { return `pena_css_${tab}`; }
|
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 tabCss = CSS_TABS.map(t => loadTabCss(t)).filter(Boolean).join('\n');
|
||||||
const activeTheme = localStorage.getItem(QUICK_THEME_KEY);
|
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');
|
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();
|
applyCustomCss();
|
||||||
syncThemeBadges();
|
syncThemeBadges();
|
||||||
|
|
||||||
@@ -104,15 +118,4 @@ export function initCssModal() {
|
|||||||
applyCustomCss();
|
applyCustomCss();
|
||||||
syncThemeBadges();
|
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user