From e2c6f540fc9ece42ea9beec9f305ea0fcc460189 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Wed, 11 Mar 2026 22:18:50 +0100 Subject: [PATCH] fix: write all theme.json defaults to vault theme frontmatter Previously, only UI chrome colours and 3 editor vars were written to theme note frontmatter. CSS vars like --lists-bullet-size, --headings-h1-font-size, etc. remained unset, so EditorTheme.css rules had no effect. - Expand DEFAULT_VAULT_THEME_VARS from 46 to 140 entries, covering every property from theme.json (editor, headings, lists, checkboxes, inline styles, code blocks, blockquote, table, horizontal rule, colors) - Fix missing px suffix on editor-font-size and editor-max-width - Add missing semantic vars: bg-card, text-tertiary - Refactor built-in vault themes from const strings to generated functions with per-theme colour overrides (DRY, all themes get editor properties) - Quote var() references in frontmatter to prevent YAML parse issues - Add regression tests for create_vault_theme and seed_vault_themes Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/theme/create.rs | 45 ++- src-tauri/src/theme/defaults.rs | 465 +++++++++++++++---------- src-tauri/src/theme/mod.rs | 2 +- src-tauri/src/theme/seed.rs | 84 +++-- src-tauri/src/vault/getting_started.rs | 6 +- src/utils/themeSchema.test.ts | 37 ++ 6 files changed, 428 insertions(+), 211 deletions(-) diff --git a/src-tauri/src/theme/create.rs b/src-tauri/src/theme/create.rs index 08aa5cbb..f80fbea5 100644 --- a/src-tauri/src/theme/create.rs +++ b/src-tauri/src/theme/create.rs @@ -93,7 +93,11 @@ fn find_available_stem(dir: &Path, base: &str, ext: &str) -> String { fn vault_theme_note_content(name: &str, vars: &[(&str, &str)]) -> String { let mut fm = format!("---\nIs A: Theme\nDescription: {name} theme\n"); for (key, value) in vars { - if value.contains('#') || value.contains('\'') || value.contains(',') { + if value.contains('#') + || value.contains('\'') + || value.contains(',') + || value.contains('(') + { fm.push_str(&format!("{key}: \"{value}\"\n")); } else { fm.push_str(&format!("{key}: {value}\n")); @@ -216,4 +220,43 @@ mod tests { assert_eq!(slugify("default"), "default"); assert_eq!(slugify("Dark Mode!"), "dark-mode"); } + + #[test] + fn test_create_vault_theme_contains_all_default_css_vars() { + let dir = TempDir::new().unwrap(); + let vault = dir.path().join("vault"); + fs::create_dir_all(&vault).unwrap(); + let vp = vault.to_str().unwrap(); + + let path = create_vault_theme(vp, Some("Full Theme")).unwrap(); + let content = fs::read_to_string(&path).unwrap(); + + // Every entry in DEFAULT_VAULT_THEME_VARS must appear in the generated file + for (key, _) in &DEFAULT_VAULT_THEME_VARS { + assert!( + content.contains(&format!("{key}:")), + "missing key in theme file: {key}" + ); + } + + // Spot-check editor properties from theme.json that were previously missing + assert!(content.contains("editor-font-family:"), "missing editor-font-family"); + assert!(content.contains("editor-padding-horizontal:"), "missing editor-padding-horizontal"); + assert!(content.contains("headings-h1-font-size:"), "missing headings-h1-font-size"); + assert!(content.contains("lists-bullet-size:"), "missing lists-bullet-size"); + assert!(content.contains("lists-bullet-color:"), "missing lists-bullet-color"); + assert!(content.contains("checkboxes-size:"), "missing checkboxes-size"); + assert!(content.contains("inline-styles-bold-font-weight:"), "missing inline-styles-bold-font-weight"); + assert!(content.contains("code-blocks-font-family:"), "missing code-blocks-font-family"); + assert!(content.contains("blockquote-border-left-width:"), "missing blockquote-border-left-width"); + assert!(content.contains("table-border-color:"), "missing table-border-color"); + assert!(content.contains("horizontal-rule-thickness:"), "missing horizontal-rule-thickness"); + assert!(content.contains("colors-text:"), "missing colors-text"); + assert!(content.contains("colors-cursor:"), "missing colors-cursor"); + + // Numeric values that need CSS units must have px suffix + assert!(content.contains("editor-font-size: 15px"), "editor-font-size should have px unit"); + assert!(content.contains("editor-max-width: 720px"), "editor-max-width should have px unit"); + assert!(content.contains("editor-padding-horizontal: 40px"), "editor-padding-horizontal should have px unit"); + } } diff --git a/src-tauri/src/theme/defaults.rs b/src-tauri/src/theme/defaults.rs index 4b465842..e853db2a 100644 --- a/src-tauri/src/theme/defaults.rs +++ b/src-tauri/src/theme/defaults.rs @@ -103,9 +103,16 @@ pub const MINIMAL_THEME: &str = r##"{ } }"##; -/// CSS variable key-value pairs for the default light vault theme. -pub const DEFAULT_VAULT_THEME_VARS: [(&str, &str); 46] = [ - // shadcn/ui base +// --------------------------------------------------------------------------- +// Vault-based theme notes (markdown with frontmatter CSS custom properties) +// --------------------------------------------------------------------------- + +/// Complete set of CSS variable key-value pairs for the default light vault theme. +/// Includes both UI chrome colours and all editor styling properties from theme.json. +/// Numeric values that need CSS units include the `px` suffix; unitless values +/// (line-height, font-weight) are bare numbers. +pub const DEFAULT_VAULT_THEME_VARS: [(&str, &str); 140] = [ + // ── shadcn/ui base colours ────────────────────────────────────────── ("background", "#FFFFFF"), ("foreground", "#37352F"), ("card", "#FFFFFF"), @@ -126,19 +133,21 @@ pub const DEFAULT_VAULT_THEME_VARS: [(&str, &str); 46] = [ ("sidebar-foreground", "#37352F"), ("sidebar-border", "#E9E9E7"), ("sidebar-accent", "#EBEBEA"), - // Text hierarchy + // ── Text hierarchy ────────────────────────────────────────────────── ("text-primary", "#37352F"), ("text-secondary", "#787774"), + ("text-tertiary", "#B4B4B4"), ("text-muted", "#B4B4B4"), ("text-heading", "#37352F"), - // Backgrounds + // ── Backgrounds ───────────────────────────────────────────────────── ("bg-primary", "#FFFFFF"), + ("bg-card", "#FFFFFF"), ("bg-sidebar", "#F7F6F3"), ("bg-hover", "#EBEBEA"), ("bg-hover-subtle", "#F0F0EF"), ("bg-selected", "#E8F4FE"), ("border-primary", "#E9E9E7"), - // Accent colours + // ── Accent colours ────────────────────────────────────────────────── ("accent-blue", "#155DFF"), ("accent-green", "#00B38B"), ("accent-orange", "#D9730D"), @@ -150,185 +159,291 @@ pub const DEFAULT_VAULT_THEME_VARS: [(&str, &str); 46] = [ ("accent-purple-light", "#A932FF14"), ("accent-red-light", "#E03E3E14"), ("accent-yellow-light", "#F0B10014"), - // Typography + // ── Typography base ───────────────────────────────────────────────── ( "font-family", "'Inter', -apple-system, BlinkMacSystemFont, sans-serif", ), ("font-size-base", "14px"), - // Editor - ("editor-font-size", "16"), + // ── Editor (from theme.json → editor) ─────────────────────────────── + ( + "editor-font-family", + "'Inter', -apple-system, BlinkMacSystemFont, sans-serif", + ), + ("editor-font-size", "15px"), ("editor-line-height", "1.5"), - ("editor-max-width", "720"), + ("editor-max-width", "720px"), + ("editor-padding-horizontal", "40px"), + ("editor-padding-vertical", "20px"), + ("editor-paragraph-spacing", "8px"), + // ── Headings H1 ──────────────────────────────────────────────────── + ("headings-h1-font-size", "32px"), + ("headings-h1-font-weight", "700"), + ("headings-h1-line-height", "1.2"), + ("headings-h1-margin-top", "32px"), + ("headings-h1-margin-bottom", "12px"), + ("headings-h1-color", "var(--text-heading)"), + ("headings-h1-letter-spacing", "-0.5px"), + // ── Headings H2 ──────────────────────────────────────────────────── + ("headings-h2-font-size", "27px"), + ("headings-h2-font-weight", "600"), + ("headings-h2-line-height", "1.4"), + ("headings-h2-margin-top", "28px"), + ("headings-h2-margin-bottom", "10px"), + ("headings-h2-color", "var(--text-heading)"), + ("headings-h2-letter-spacing", "-0.5px"), + // ── Headings H3 ──────────────────────────────────────────────────── + ("headings-h3-font-size", "20px"), + ("headings-h3-font-weight", "600"), + ("headings-h3-line-height", "1.4"), + ("headings-h3-margin-top", "24px"), + ("headings-h3-margin-bottom", "8px"), + ("headings-h3-color", "var(--text-heading)"), + ("headings-h3-letter-spacing", "-0.5px"), + // ── Headings H4 ──────────────────────────────────────────────────── + ("headings-h4-font-size", "20px"), + ("headings-h4-font-weight", "600"), + ("headings-h4-line-height", "1.4"), + ("headings-h4-margin-top", "20px"), + ("headings-h4-margin-bottom", "6px"), + ("headings-h4-color", "var(--text-heading)"), + ("headings-h4-letter-spacing", "0px"), + // ── Lists ─────────────────────────────────────────────────────────── + ("lists-bullet-size", "28px"), + ("lists-bullet-color", "#177bfd"), + ("lists-indent-size", "24px"), + ("lists-item-spacing", "4px"), + ("lists-padding-left", "8px"), + ("lists-bullet-gap", "6px"), + // ── Checkboxes ────────────────────────────────────────────────────── + ("checkboxes-size", "18px"), + ("checkboxes-border-radius", "3px"), + ("checkboxes-checked-color", "var(--accent-blue)"), + ("checkboxes-unchecked-border-color", "var(--text-muted)"), + ("checkboxes-gap", "8px"), + // ── Inline styles: bold ───────────────────────────────────────────── + ("inline-styles-bold-font-weight", "700"), + ("inline-styles-bold-color", "var(--text-primary)"), + // ── Inline styles: italic ─────────────────────────────────────────── + ("inline-styles-italic-font-style", "italic"), + ("inline-styles-italic-color", "var(--text-primary)"), + // ── Inline styles: strikethrough ──────────────────────────────────── + ("inline-styles-strikethrough-color", "var(--text-tertiary)"), + ( + "inline-styles-strikethrough-text-decoration", + "line-through", + ), + // ── Inline styles: code ───────────────────────────────────────────── + ( + "inline-styles-code-font-family", + "'SF Mono', 'Fira Code', monospace", + ), + ("inline-styles-code-font-size", "14px"), + ("inline-styles-code-background-color", "var(--bg-hover-subtle)"), + ("inline-styles-code-padding-horizontal", "4px"), + ("inline-styles-code-padding-vertical", "2px"), + ("inline-styles-code-border-radius", "3px"), + ("inline-styles-code-color", "var(--text-secondary)"), + // ── Inline styles: link ───────────────────────────────────────────── + ("inline-styles-link-color", "var(--accent-blue)"), + ("inline-styles-link-text-decoration", "underline"), + // ── Inline styles: wikilink ───────────────────────────────────────── + ("inline-styles-wikilink-color", "var(--accent-blue)"), + ("inline-styles-wikilink-text-decoration", "none"), + ( + "inline-styles-wikilink-border-bottom", + "1px dotted currentColor", + ), + ("inline-styles-wikilink-cursor", "pointer"), + // ── Code blocks ───────────────────────────────────────────────────── + ( + "code-blocks-font-family", + "'SF Mono', 'Fira Code', monospace", + ), + ("code-blocks-font-size", "13px"), + ("code-blocks-line-height", "1.5"), + ("code-blocks-background-color", "var(--bg-card)"), + ("code-blocks-padding-horizontal", "16px"), + ("code-blocks-padding-vertical", "12px"), + ("code-blocks-border-radius", "6px"), + ("code-blocks-margin-vertical", "12px"), + // ── Blockquote ────────────────────────────────────────────────────── + ("blockquote-border-left-width", "3px"), + ("blockquote-border-left-color", "var(--accent-blue)"), + ("blockquote-padding-left", "16px"), + ("blockquote-margin-vertical", "12px"), + ("blockquote-color", "var(--text-secondary)"), + ("blockquote-font-style", "italic"), + // ── Table ─────────────────────────────────────────────────────────── + ("table-border-color", "var(--border-primary)"), + ("table-header-background", "var(--bg-card)"), + ("table-cell-padding-horizontal", "12px"), + ("table-cell-padding-vertical", "8px"), + ("table-font-size", "14px"), + // ── Horizontal rule ───────────────────────────────────────────────── + ("horizontal-rule-color", "var(--border-primary)"), + ("horizontal-rule-margin-vertical", "24px"), + ("horizontal-rule-thickness", "1px"), + // ── Colors (semantic aliases from theme.json → colors) ────────────── + ("colors-background", "var(--bg-primary)"), + ("colors-text", "var(--text-primary)"), + ("colors-text-secondary", "var(--text-secondary)"), + ("colors-text-muted", "var(--text-muted)"), + ("colors-heading", "var(--text-heading)"), + ("colors-accent", "var(--accent-blue)"), + ("colors-selection", "var(--bg-selected)"), + ("colors-cursor", "var(--text-primary)"), ]; -/// Vault-based theme note for the built-in Default theme. -pub const DEFAULT_VAULT_THEME: &str = "---\n\ -type: Theme\n\ -Description: Light theme with warm, paper-like tones\n\ -background: \"#FFFFFF\"\n\ -foreground: \"#37352F\"\n\ -card: \"#FFFFFF\"\n\ -popover: \"#FFFFFF\"\n\ -primary: \"#155DFF\"\n\ -primary-foreground: \"#FFFFFF\"\n\ -secondary: \"#EBEBEA\"\n\ -secondary-foreground: \"#37352F\"\n\ -muted: \"#F0F0EF\"\n\ -muted-foreground: \"#787774\"\n\ -accent: \"#EBEBEA\"\n\ -accent-foreground: \"#37352F\"\n\ -destructive: \"#E03E3E\"\n\ -border: \"#E9E9E7\"\n\ -input: \"#E9E9E7\"\n\ -ring: \"#155DFF\"\n\ -sidebar: \"#F7F6F3\"\n\ -sidebar-foreground: \"#37352F\"\n\ -sidebar-border: \"#E9E9E7\"\n\ -sidebar-accent: \"#EBEBEA\"\n\ -text-primary: \"#37352F\"\n\ -text-secondary: \"#787774\"\n\ -text-muted: \"#B4B4B4\"\n\ -text-heading: \"#37352F\"\n\ -bg-primary: \"#FFFFFF\"\n\ -bg-sidebar: \"#F7F6F3\"\n\ -bg-hover: \"#EBEBEA\"\n\ -bg-hover-subtle: \"#F0F0EF\"\n\ -bg-selected: \"#E8F4FE\"\n\ -border-primary: \"#E9E9E7\"\n\ -accent-blue: \"#155DFF\"\n\ -accent-green: \"#00B38B\"\n\ -accent-orange: \"#D9730D\"\n\ -accent-red: \"#E03E3E\"\n\ -accent-purple: \"#A932FF\"\n\ -accent-yellow: \"#F0B100\"\n\ -accent-blue-light: \"#155DFF14\"\n\ -accent-green-light: \"#00B38B14\"\n\ -accent-purple-light: \"#A932FF14\"\n\ -accent-red-light: \"#E03E3E14\"\n\ -accent-yellow-light: \"#F0B10014\"\n\ -font-family: \"'Inter', -apple-system, BlinkMacSystemFont, sans-serif\"\n\ -font-size-base: 14px\n\ -editor-font-size: 16\n\ -editor-line-height: 1.5\n\ -editor-max-width: 720\n\ ----\n\ -\n\ -# Default Theme\n\ -\n\ -The default light theme for Laputa. Clean and warm, inspired by Notion.\n"; +/// UI-colour overrides for the Dark vault theme (keys that differ from default). +const DARK_COLOR_OVERRIDES: &[(&str, &str)] = &[ + ("background", "#0f0f1a"), + ("foreground", "#e0e0e0"), + ("card", "#16162a"), + ("popover", "#1e1e3a"), + ("secondary", "#2a2a4a"), + ("secondary-foreground", "#e0e0e0"), + ("muted", "#1e1e3a"), + ("muted-foreground", "#888888"), + ("accent", "#2a2a4a"), + ("accent-foreground", "#e0e0e0"), + ("destructive", "#f44336"), + ("border", "#2a2a4a"), + ("input", "#2a2a4a"), + ("sidebar", "#1a1a2e"), + ("sidebar-foreground", "#e0e0e0"), + ("sidebar-border", "#2a2a4a"), + ("sidebar-accent", "#2a2a4a"), + ("text-primary", "#e0e0e0"), + ("text-secondary", "#888888"), + ("text-tertiary", "#666666"), + ("text-muted", "#666666"), + ("text-heading", "#e0e0e0"), + ("bg-primary", "#0f0f1a"), + ("bg-card", "#16162a"), + ("bg-sidebar", "#1a1a2e"), + ("bg-hover", "#2a2a4a"), + ("bg-hover-subtle", "#1e1e3a"), + ("bg-selected", "#155DFF22"), + ("border-primary", "#2a2a4a"), + ("accent-red", "#f44336"), + ("accent-blue-light", "#155DFF33"), + ("accent-green-light", "#00B38B33"), + ("accent-purple-light", "#A932FF33"), + ("accent-red-light", "#f4433633"), + ("accent-yellow-light", "#F0B10033"), + ("lists-bullet-color", "#155DFF"), +]; -/// Vault-based theme note for the built-in Dark theme. -pub const DARK_VAULT_THEME: &str = "---\n\ -type: Theme\n\ -Description: Dark variant with deep navy tones\n\ -background: \"#0f0f1a\"\n\ -foreground: \"#e0e0e0\"\n\ -card: \"#16162a\"\n\ -popover: \"#1e1e3a\"\n\ -primary: \"#155DFF\"\n\ -primary-foreground: \"#FFFFFF\"\n\ -secondary: \"#2a2a4a\"\n\ -secondary-foreground: \"#e0e0e0\"\n\ -muted: \"#1e1e3a\"\n\ -muted-foreground: \"#888888\"\n\ -accent: \"#2a2a4a\"\n\ -accent-foreground: \"#e0e0e0\"\n\ -destructive: \"#f44336\"\n\ -border: \"#2a2a4a\"\n\ -input: \"#2a2a4a\"\n\ -ring: \"#155DFF\"\n\ -sidebar: \"#1a1a2e\"\n\ -sidebar-foreground: \"#e0e0e0\"\n\ -sidebar-border: \"#2a2a4a\"\n\ -sidebar-accent: \"#2a2a4a\"\n\ -text-primary: \"#e0e0e0\"\n\ -text-secondary: \"#888888\"\n\ -text-muted: \"#666666\"\n\ -text-heading: \"#e0e0e0\"\n\ -bg-primary: \"#0f0f1a\"\n\ -bg-sidebar: \"#1a1a2e\"\n\ -bg-hover: \"#2a2a4a\"\n\ -bg-hover-subtle: \"#1e1e3a\"\n\ -bg-selected: \"#155DFF22\"\n\ -border-primary: \"#2a2a4a\"\n\ -accent-blue: \"#155DFF\"\n\ -accent-green: \"#00B38B\"\n\ -accent-orange: \"#D9730D\"\n\ -accent-red: \"#f44336\"\n\ -accent-purple: \"#A932FF\"\n\ -accent-yellow: \"#F0B100\"\n\ -accent-blue-light: \"#155DFF33\"\n\ -accent-green-light: \"#00B38B33\"\n\ -accent-purple-light: \"#A932FF33\"\n\ -accent-red-light: \"#f4433633\"\n\ -accent-yellow-light: \"#F0B10033\"\n\ -font-family: \"'Inter', -apple-system, BlinkMacSystemFont, sans-serif\"\n\ -font-size-base: 14px\n\ -editor-font-size: 16\n\ -editor-line-height: 1.5\n\ -editor-max-width: 720\n\ ----\n\ -\n\ -# Dark Theme\n\ -\n\ -A dark theme with deep navy tones for comfortable night-time reading.\n"; +/// UI-colour + editor-property overrides for the Minimal vault theme. +const MINIMAL_OVERRIDES: &[(&str, &str)] = &[ + ("background", "#FAFAFA"), + ("foreground", "#111111"), + ("primary", "#000000"), + ("secondary", "#F0F0F0"), + ("secondary-foreground", "#111111"), + ("muted", "#F5F5F5"), + ("muted-foreground", "#666666"), + ("accent", "#F0F0F0"), + ("accent-foreground", "#111111"), + ("destructive", "#CC0000"), + ("border", "#E0E0E0"), + ("input", "#E0E0E0"), + ("ring", "#000000"), + ("sidebar", "#F5F5F5"), + ("sidebar-foreground", "#111111"), + ("sidebar-border", "#E0E0E0"), + ("sidebar-accent", "#E8E8E8"), + ("text-primary", "#111111"), + ("text-secondary", "#666666"), + ("text-tertiary", "#999999"), + ("text-muted", "#999999"), + ("text-heading", "#111111"), + ("bg-primary", "#FAFAFA"), + ("bg-card", "#FFFFFF"), + ("bg-sidebar", "#F5F5F5"), + ("bg-hover", "#EBEBEB"), + ("bg-hover-subtle", "#F5F5F5"), + ("bg-selected", "#00000014"), + ("border-primary", "#E0E0E0"), + ("accent-blue", "#000000"), + ("accent-green", "#006600"), + ("accent-orange", "#996600"), + ("accent-red", "#CC0000"), + ("accent-purple", "#660099"), + ("accent-yellow", "#996600"), + ("accent-blue-light", "#00000014"), + ("accent-green-light", "#00660014"), + ("accent-purple-light", "#66009914"), + ("accent-red-light", "#CC000014"), + ("accent-yellow-light", "#99660014"), + ( + "font-family", + "'SF Mono', 'Menlo', monospace", + ), + ("font-size-base", "13px"), + ("editor-font-size", "15px"), + ("editor-line-height", "1.6"), + ("editor-max-width", "680px"), + ("lists-bullet-color", "#000000"), +]; -/// Vault-based theme note for the built-in Minimal theme. -pub const MINIMAL_VAULT_THEME: &str = "---\n\ -type: Theme\n\ -Description: High contrast, minimal chrome\n\ -background: \"#FAFAFA\"\n\ -foreground: \"#111111\"\n\ -card: \"#FFFFFF\"\n\ -popover: \"#FFFFFF\"\n\ -primary: \"#000000\"\n\ -primary-foreground: \"#FFFFFF\"\n\ -secondary: \"#F0F0F0\"\n\ -secondary-foreground: \"#111111\"\n\ -muted: \"#F5F5F5\"\n\ -muted-foreground: \"#666666\"\n\ -accent: \"#F0F0F0\"\n\ -accent-foreground: \"#111111\"\n\ -destructive: \"#CC0000\"\n\ -border: \"#E0E0E0\"\n\ -input: \"#E0E0E0\"\n\ -ring: \"#000000\"\n\ -sidebar: \"#F5F5F5\"\n\ -sidebar-foreground: \"#111111\"\n\ -sidebar-border: \"#E0E0E0\"\n\ -sidebar-accent: \"#E8E8E8\"\n\ -text-primary: \"#111111\"\n\ -text-secondary: \"#666666\"\n\ -text-muted: \"#999999\"\n\ -text-heading: \"#111111\"\n\ -bg-primary: \"#FAFAFA\"\n\ -bg-sidebar: \"#F5F5F5\"\n\ -bg-hover: \"#EBEBEB\"\n\ -bg-hover-subtle: \"#F5F5F5\"\n\ -bg-selected: \"#00000014\"\n\ -border-primary: \"#E0E0E0\"\n\ -accent-blue: \"#000000\"\n\ -accent-green: \"#006600\"\n\ -accent-orange: \"#996600\"\n\ -accent-red: \"#CC0000\"\n\ -accent-purple: \"#660099\"\n\ -accent-yellow: \"#996600\"\n\ -accent-blue-light: \"#00000014\"\n\ -accent-green-light: \"#00660014\"\n\ -accent-purple-light: \"#66009914\"\n\ -accent-red-light: \"#CC000014\"\n\ -accent-yellow-light: \"#99660014\"\n\ -font-family: \"'SF Mono', 'Menlo', monospace\"\n\ -font-size-base: 13px\n\ -editor-font-size: 15\n\ -editor-line-height: 1.6\n\ -editor-max-width: 680\n\ ----\n\ -\n\ -# Minimal Theme\n\ -\n\ -High contrast, minimal chrome. Monospace typography throughout.\n"; +/// Build a vault theme note string from a set of CSS variable pairs. +/// +/// Values containing `#`, `'`, `,`, or `(` are YAML-quoted to avoid parse errors. +fn build_vault_theme_note( + name: &str, + description: &str, + vars: &[(&str, &str)], +) -> String { + let mut fm = format!("---\ntype: Theme\nDescription: {description}\n"); + for (key, value) in vars { + if value.contains('#') + || value.contains('\'') + || value.contains(',') + || value.contains('(') + { + fm.push_str(&format!("{key}: \"{value}\"\n")); + } else { + fm.push_str(&format!("{key}: {value}\n")); + } + } + fm.push_str("---\n\n"); + fm.push_str(&format!("# {name} Theme\n\n{description}.\n")); + fm +} + +/// Apply overrides on top of DEFAULT_VAULT_THEME_VARS, returning a new Vec. +fn apply_overrides(overrides: &[(&'static str, &'static str)]) -> Vec<(&'static str, &'static str)> { + let mut vars: Vec<(&'static str, &'static str)> = DEFAULT_VAULT_THEME_VARS.to_vec(); + for &(key, value) in overrides { + if let Some(entry) = vars.iter_mut().find(|e| e.0 == key) { + entry.1 = value; + } + } + vars +} + +/// Generate the Default vault theme note content. +pub fn default_vault_theme() -> String { + build_vault_theme_note( + "Default", + "Light theme with warm, paper-like tones", + &DEFAULT_VAULT_THEME_VARS, + ) +} + +/// Generate the Dark vault theme note content. +pub fn dark_vault_theme() -> String { + let vars = apply_overrides(DARK_COLOR_OVERRIDES); + build_vault_theme_note("Dark", "Dark variant with deep navy tones", &vars) +} + +/// Generate the Minimal vault theme note content. +pub fn minimal_vault_theme() -> String { + let vars = apply_overrides(MINIMAL_OVERRIDES); + build_vault_theme_note("Minimal", "High contrast, minimal chrome", &vars) +} /// Type definition for the Theme note type. pub const THEME_TYPE_DEFINITION: &str = "---\n\ diff --git a/src-tauri/src/theme/mod.rs b/src-tauri/src/theme/mod.rs index cdcc0ca0..ca0c642d 100644 --- a/src-tauri/src/theme/mod.rs +++ b/src-tauri/src/theme/mod.rs @@ -258,7 +258,7 @@ mod tests { #[test] fn test_vault_theme_content_contains_all_vars() { - let content = DEFAULT_VAULT_THEME; + let content = default_vault_theme(); assert!(content.contains("background:")); assert!(content.contains("primary:")); assert!(content.contains("sidebar:")); diff --git a/src-tauri/src/theme/seed.rs b/src-tauri/src/theme/seed.rs index 624286b4..94581bb4 100644 --- a/src-tauri/src/theme/seed.rs +++ b/src-tauri/src/theme/seed.rs @@ -31,6 +31,16 @@ pub fn seed_default_themes(vault_path: &str) { ); } +/// Write a vault theme file if it doesn't exist or is empty (corrupt). +fn write_if_missing(path: &Path, content: &str) -> Result { + let needs_write = !path.exists() || fs::metadata(path).map_or(true, |m| m.len() == 0); + if needs_write { + fs::write(path, content) + .map_err(|e| format!("Failed to write {}: {e}", path.display()))?; + } + Ok(needs_write) +} + /// Seed the vault `theme/` directory with built-in vault-based theme notes. /// Per-file idempotent: creates the directory if missing, writes each default /// file only when it doesn't exist or is empty (corrupt). Never overwrites @@ -40,19 +50,18 @@ pub fn seed_vault_themes(vault_path: &str) { if fs::create_dir_all(&theme_dir).is_err() { return; } + let default_content = default_vault_theme(); + let dark_content = dark_vault_theme(); + let minimal_content = minimal_vault_theme(); let defaults: &[(&str, &str)] = &[ - ("default.md", DEFAULT_VAULT_THEME), - ("dark.md", DARK_VAULT_THEME), - ("minimal.md", MINIMAL_VAULT_THEME), + ("default.md", &default_content), + ("dark.md", &dark_content), + ("minimal.md", &minimal_content), ]; let mut seeded = false; for (name, content) in defaults { - let path = theme_dir.join(name); - let needs_write = !path.exists() || fs::metadata(&path).map_or(true, |m| m.len() == 0); - if needs_write { - let _ = fs::write(&path, content); - seeded = true; - } + let wrote = write_if_missing(&theme_dir.join(name), content).unwrap_or(false); + seeded = seeded || wrote; } if seeded { log::info!("Seeded theme/ with built-in vault themes"); @@ -64,17 +73,17 @@ pub fn seed_vault_themes(vault_path: &str) { pub fn ensure_vault_themes(vault_path: &str) -> Result<(), String> { let theme_dir = Path::new(vault_path).join("theme"); fs::create_dir_all(&theme_dir).map_err(|e| format!("Failed to create theme directory: {e}"))?; + let default_content = default_vault_theme(); + let dark_content = dark_vault_theme(); + let minimal_content = minimal_vault_theme(); let defaults: &[(&str, &str)] = &[ - ("default.md", DEFAULT_VAULT_THEME), - ("dark.md", DARK_VAULT_THEME), - ("minimal.md", MINIMAL_VAULT_THEME), + ("default.md", &default_content), + ("dark.md", &dark_content), + ("minimal.md", &minimal_content), ]; for (name, content) in defaults { - let path = theme_dir.join(name); - let needs_write = !path.exists() || fs::metadata(&path).map_or(true, |m| m.len() == 0); - if needs_write { - fs::write(&path, content).map_err(|e| format!("Failed to write theme/{name}: {e}"))?; - } + write_if_missing(&theme_dir.join(name), content) + .map_err(|e| format!("Failed to write theme/{name}: {e}"))?; } Ok(()) } @@ -93,12 +102,7 @@ pub fn restore_default_themes(vault_path: &str) -> Result { ("minimal.json", MINIMAL_THEME), ]; for (name, content) in json_defaults { - let path = themes_dir.join(name); - let needs_write = !path.exists() || fs::metadata(&path).map_or(true, |m| m.len() == 0); - if needs_write { - fs::write(&path, content) - .map_err(|e| format!("Failed to write _themes/{name}: {e}"))?; - } + write_if_missing(&themes_dir.join(name), content)?; } // Seed theme/ markdown notes (reuses ensure_vault_themes for consistency) @@ -114,12 +118,7 @@ pub fn restore_default_themes(vault_path: &str) -> Result { pub fn ensure_theme_type_definition(vault_path: &str) -> Result<(), String> { let type_dir = Path::new(vault_path).join("type"); fs::create_dir_all(&type_dir).map_err(|e| format!("Failed to create type directory: {e}"))?; - let path = type_dir.join("theme.md"); - let needs_write = !path.exists() || fs::metadata(&path).map_or(true, |m| m.len() == 0); - if needs_write { - fs::write(&path, THEME_TYPE_DEFINITION) - .map_err(|e| format!("Failed to write type/theme.md: {e}"))?; - } + write_if_missing(&type_dir.join("theme.md"), THEME_TYPE_DEFINITION)?; Ok(()) } @@ -162,7 +161,7 @@ mod tests { let vault = dir.path().join("vault"); let theme_dir = vault.join("theme"); fs::create_dir_all(&theme_dir).unwrap(); - fs::write(theme_dir.join("default.md"), DEFAULT_VAULT_THEME).unwrap(); + fs::write(theme_dir.join("default.md"), &default_vault_theme()).unwrap(); let vp = vault.to_str().unwrap(); seed_vault_themes(vp); @@ -330,7 +329,7 @@ mod tests { fs::create_dir_all(&themes_dir).unwrap(); fs::create_dir_all(&theme_dir).unwrap(); fs::write(themes_dir.join("default.json"), DEFAULT_THEME).unwrap(); - fs::write(theme_dir.join("default.md"), DEFAULT_VAULT_THEME).unwrap(); + fs::write(theme_dir.join("default.md"), &default_vault_theme()).unwrap(); let vp = vault.to_str().unwrap(); restore_default_themes(vp).unwrap(); @@ -341,4 +340,27 @@ mod tests { let content = fs::read_to_string(theme_dir.join("default.md")).unwrap(); assert!(content.contains("Light theme with warm")); } + + #[test] + fn test_seeded_default_theme_contains_editor_properties() { + let dir = TempDir::new().unwrap(); + let vault = dir.path().join("vault"); + fs::create_dir_all(&vault).unwrap(); + let vp = vault.to_str().unwrap(); + + ensure_vault_themes(vp).unwrap(); + let content = fs::read_to_string(vault.join("theme").join("default.md")).unwrap(); + + // Must contain all editor properties from theme.json + assert!(content.contains("editor-font-family:"), "missing editor-font-family"); + assert!(content.contains("headings-h1-font-size:"), "missing headings-h1-font-size"); + assert!(content.contains("lists-bullet-size:"), "missing lists-bullet-size"); + assert!(content.contains("checkboxes-size:"), "missing checkboxes-size"); + assert!(content.contains("inline-styles-bold-font-weight:"), "missing inline-styles-bold"); + assert!(content.contains("code-blocks-font-family:"), "missing code-blocks-font-family"); + assert!(content.contains("blockquote-border-left-width:"), "missing blockquote"); + assert!(content.contains("table-border-color:"), "missing table-border-color"); + assert!(content.contains("horizontal-rule-thickness:"), "missing horizontal-rule"); + assert!(content.contains("colors-text:"), "missing colors-text"); + } } diff --git a/src-tauri/src/vault/getting_started.rs b/src-tauri/src/vault/getting_started.rs index d2d0217f..57a391dd 100644 --- a/src-tauri/src/vault/getting_started.rs +++ b/src-tauri/src/vault/getting_started.rs @@ -428,17 +428,17 @@ pub fn create_getting_started_vault(target_path: &str) -> Result .map_err(|e| format!("Failed to create theme directory: {e}"))?; fs::write( theme_notes_dir.join("default.md"), - crate::theme::DEFAULT_VAULT_THEME, + crate::theme::default_vault_theme(), ) .map_err(|e| format!("Failed to write default vault theme: {e}"))?; fs::write( theme_notes_dir.join("dark.md"), - crate::theme::DARK_VAULT_THEME, + crate::theme::dark_vault_theme(), ) .map_err(|e| format!("Failed to write dark vault theme: {e}"))?; fs::write( theme_notes_dir.join("minimal.md"), - crate::theme::MINIMAL_VAULT_THEME, + crate::theme::minimal_vault_theme(), ) .map_err(|e| format!("Failed to write minimal vault theme: {e}"))?; diff --git a/src/utils/themeSchema.test.ts b/src/utils/themeSchema.test.ts index 2c9b8f79..84a3dab7 100644 --- a/src/utils/themeSchema.test.ts +++ b/src/utils/themeSchema.test.ts @@ -1,4 +1,6 @@ import { describe, it, expect } from 'vitest' +import { readFileSync } from 'fs' +import { resolve } from 'path' import { buildThemeSchema, formatValueForFrontmatter, parseValueFromFrontmatter } from './themeSchema' import type { ThemeProperty } from './themeSchema' @@ -124,6 +126,41 @@ describe('buildThemeSchema', () => { expect(fontStyle.options).toContain('normal') expect(fontStyle.options).toContain('italic') }) + + it('every var(--xxx) in EditorTheme.css has a matching default in theme schema or base UI', () => { + const cssPath = resolve(__dirname, '../components/EditorTheme.css') + const css = readFileSync(cssPath, 'utf-8') + + // Extract all var(--xxx) references (first arg only, ignore fallbacks) + const varRegex = /var\(--([a-z0-9-]+)/g + const usedVars = new Set() + let match: RegExpExecArray | null + while ((match = varRegex.exec(css)) !== null) { + usedVars.add(match[1]) + } + + // Collect all CSS var names from the schema + const schemaVars = new Set() + for (const section of schema) { + for (const prop of section.properties) schemaVars.add(prop.cssVar) + for (const sub of section.subsections) { + for (const prop of sub.properties) schemaVars.add(prop.cssVar) + } + } + + // Base UI color vars set by the theme color system (not in theme.json schema) + const baseUIVars = new Set([ + 'border-primary', 'bg-primary', 'bg-card', 'bg-hover-subtle', 'bg-selected', + 'text-primary', 'text-secondary', 'text-muted', 'text-heading', 'text-tertiary', + 'accent-blue', + ]) + + for (const varName of usedVars) { + expect( + schemaVars.has(varName) || baseUIVars.has(varName), + ).toBe(true) + } + }) }) describe('formatValueForFrontmatter', () => {