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:
@@ -1,2 +1,3 @@
|
||||
pub mod file_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 theme;
|
||||
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;
|
||||
pub mod theme;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
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 file_repository;
|
||||
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::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");
|
||||
|
||||
Reference in New Issue
Block a user