diff --git a/AGENTS.md b/AGENTS.md index 85648a1..86f2918 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,7 +85,7 @@ eternalibre-notes/ │ ├── notes.rs # Note model + file-system persistence │ ├── markdown.rs # Markdown → HTML with code block interception │ ├── syntax.rs # syntect engine + per-theme tmTheme generation -│ ├── themes.rs # 34 predefined themes + CSS variable engine +│ ├── themes.rs # 40 predefined themes + CSS variable engine │ └── storage.rs # Storage re-export module ├── static/ │ ├── alpine.min.js # Vendored Alpine.js @@ -121,7 +121,9 @@ pub struct ANSIPalette { } ``` -`PREDEFINED_THEMES` contains 34 alphabetically ordered themes: Amber on Black, Autumn Forest, Blueberry on Black, Breeze, Breeze Dark, Chartreuse on Black, Classic Windows 95, Cyan on Black, Cyberpunk 2077, Dark, Dracula, Foggy Morning, Gruvbox Dark, Gruvbox Light, Light, Lilac Paper, Lime Green on Black, Monokai, Nord, One Dark, Orchid Noir, Platinum Desktop, Red on Black, Rose Quartz, Solarized Dark, Solarized Light, Spring Blossom, Stormy Night, Summer Sunset, Tokyo Night, Velvet Dusk, Vista Aero, Windows XP, and Winter Frost. +`PREDEFINED_THEMES` contains 40 alphabetically ordered themes: Amber on Black, Autumn Forest, Blueberry on Black, Blueberry Professional, Breeze, Breeze Dark, Chartreuse on Black, Classic Windows 95, Claude, Cyan on Black, Cyberpunk 2077, Dark, Debian Professional, Dracula, Foggy Morning, Gentoo, Gentoo Professional, Golden Hour, Gruvbox Dark, Gruvbox Light, Light, Lilac Paper, Lime Green on Black, Monokai, Nord, One Dark, Orchid Noir, Platinum Desktop, Red on Black, Rose Quartz, Solarized Dark, Solarized Light, Stormy Night, Summer Sunset, Tokyo Night, Ubuntu Professional, Velvet Dusk, Vista Aero, Windows XP, and Winter Frost. + +The Gentoo palette is ported from Konsole: `Background` and `Foreground` supply `bg` and `fg`, `Color0`–`Color7` map to `ansi_normal`, and `Color0Intense`–`Color7Intense` map to `ansi_bright`. The Konsole `Faint` variants and terminal wallpaper/opacity options have no corresponding fields in `Theme` and are not represented. "Sunny Day" was removed: a mid-green background with a pale yellow foreground left almost nothing legible on it, and no amount of syntax tuning rescues a palette that bad. @@ -176,6 +178,8 @@ fn button_colors(&self, accent: usize) -> (&'static str, &'static str) { Themes that previously used `#0000ff` on a pure black background were also corrected at the source: that slot is now `#a9a9f9` in the affected palettes (Cyan on Black, Red on Black, Lime Green on Black, Chartreuse on Black, and Amber on Black). +Gentoo intentionally preserves Konsole's `#6f5fa2` bright blue-violet exactly. Its contrast against the current dark `#0c0c0c` button text is only 3.57:1, so `button_colors(4)` uses white text on that button (5.48:1) without changing the ANSI palette. + --- ## 6. Syntax Highlighting @@ -222,7 +226,7 @@ Where no candidate clears the floor, the palette is at fault, not the code. **So Comments also carry `fontStyle: italic` in the tmTheme, which syntect renders as `font-style:italic` on the span. Color alone is not a reliable signal, since a comment that falls back to the foreground is by definition the same color as the prose. -The three failure modes are each pinned by a test, and each test was verified to fail when the corresponding bug is reintroduced: `comments_are_readable_in_every_theme`, `no_theme_uses_its_background_for_comments`, and `comments_stay_distinct_from_the_foreground_when_they_can`. `syntax_color_always_picks_the_more_readable_variant` additionally pins the selection rule itself across all 34 themes and all 8 slots. +The three failure modes are each pinned by a test, and each test was verified to fail when the corresponding bug is reintroduced: `comments_are_readable_in_every_theme`, `no_theme_uses_its_background_for_comments`, and `comments_stay_distinct_from_the_foreground_when_they_can`. `syntax_color_always_picks_the_more_readable_variant` additionally pins the selection rule itself across all 40 themes and all 8 slots. ### The Light theme has a data ceiling, not a logic bug @@ -248,7 +252,7 @@ Implementation notes: > **Do not reintroduce `overflow`, `transform`, `filter`, or `opacity` on `.menu-bar`, `.menu-list`, or `.menu-item`.** `overflow` on the dropdown *itself* is safe (it only clips its own contents), but on an ancestor it traps the dropdown. -The 34-entry Theme menu can exceed the viewport height, so its `.menu-dropdown` carries `max-height` plus `overflow-y: auto`. The short File dropdown is the exception: `.file-menu-dropdown` uses `overflow: visible` so the Export Note As flyout can escape to the right; scrolling is confined to the flyout itself. Keep overflow off the menu-bar ancestors. +The 40-entry Theme menu can exceed the viewport height, so its `.menu-dropdown` carries `max-height` plus `overflow-y: auto`. The short File dropdown is the exception: `.file-menu-dropdown` uses `overflow: visible` so the Export Note As flyout can escape to the right; scrolling is confined to the flyout itself. Keep overflow off the menu-bar ancestors. ### Every screen needs a viewport meta, or the mobile CSS is dead @@ -269,7 +273,7 @@ The trap is that the CSS is *correct and present*. You can add a complete mobile **Its version and theme counts are derived, never typed.** The version comes from `env!("CARGO_PKG_VERSION")`, and the same macro drives the `.app-title` in the menu bar — which previously carried a hand-written `"v0.1"` that had already drifted from the crate's `0.1.0`. The theme line is `PREDEFINED_THEMES.len()` split by `Theme::is_dark()`, so adding or removing a palette needs no edit here. Both are pinned by `about_page_reports_the_manifest_version` and `about_page_theme_counts_come_from_the_palette`. -`Theme::is_dark` classifies on the local gamma-encoded `relative_luminance` rather than the linearised one in `syntax.rs`. That is safe *only* because the shipped palettes are strongly bimodal: the lightest dark background scores 0.095 and the darkest light one 0.63, so any threshold in that gap agrees. If a future palette lands near 0.5, switch this to the WCAG function rather than nudging the threshold — the two agree on all 34 themes today, so the current answer is not a close call. +`Theme::is_dark` classifies on the local gamma-encoded `relative_luminance` rather than the linearised one in `syntax.rs`. That is safe *only* because the shipped palettes are strongly bimodal: the lightest dark background scores 0.095 and the darkest light one 0.63, so any threshold in that gap agrees. If a future palette lands near 0.5, switch this to the WCAG function rather than nudging the threshold — the two agree on all 40 themes today, so the current answer is not a close call. **Every outbound link carries `rel="noopener noreferrer"`.** These are the only links that leave the machine, and the app advertises that it never talks to a third party. `about_page_external_links_are_hardened` walks every `) -> Markup { li { (format!( "{} built-in themes ({} dark, {} light) with a full \ - ANSI palette — Solarized, Dracula, KDE Breeze, Lilac Paper, and more", + ANSI palette — Solarized, Dracula, KDE Breeze, Gentoo, and more", themes.len(), dark_themes, themes.len() - dark_themes, diff --git a/src/themes.rs b/src/themes.rs index 31c4854..5039941 100644 --- a/src/themes.rs +++ b/src/themes.rs @@ -70,7 +70,7 @@ impl Theme { /// any threshold in that gap returns the same answer. That margin is why /// this can use the local gamma-encoded `relative_luminance` rather than /// the linearised one in `syntax.rs` — for a binary split the two agree - /// on all 34 themes, and adding a third implementation would cost more + /// on all 40 themes, and adding a third implementation would cost more /// than the precision is worth. pub fn is_dark(&self) -> bool { relative_luminance(self.bg) < 0.5 @@ -80,7 +80,11 @@ impl Theme { // Use the bright variant of the accent color if it has decent luminance; // otherwise fall back to the theme foreground color so text remains readable. let bright = self.ansi_bright[accent]; - if relative_luminance(bright) < 0.15 { + if self.name == "Gentoo" && accent == 4 { + // Gentoo keeps Konsole's exact ANSI blue-violet, but that accent + // needs a light label to maintain readable primary-button text. + (bright, "#ffffff") + } else if relative_luminance(bright) < 0.15 { (self.fg, self.bg) } else { (bright, self.bg) @@ -133,6 +137,17 @@ pub static PREDEFINED_THEMES: &[Theme] = &[ "#37474f", "#1e88e5", "#42a5f5", "#64b5f6", "#90caf9", "#bbdefb", "#e3f2fd", "#ffffff", ], }, + Theme { + name: "Blueberry Professional", + fg: "#263247", + bg: "#f4efe6", + ansi_normal: [ + "#f4efe6", "#9d4d47", "#58734f", "#8c6a2f", "#356a9a", "#735c89", "#347b83", "#263247", + ], + ansi_bright: [ + "#857d75", "#873d3a", "#4d6545", "#75531f", "#2d5e8b", "#624977", "#28666f", "#1d2634", + ], + }, Theme { name: "Breeze", fg: "#232629", @@ -177,6 +192,17 @@ pub static PREDEFINED_THEMES: &[Theme] = &[ "#808080", "#c00000", "#008000", "#9a6700", "#004e98", "#9a4e9a", "#008b8b", "#ffffff", ], }, + Theme { + name: "Claude", + fg: "#3c3530", + bg: "#f4efe6", + ansi_normal: [ + "#f4efe6", "#a85245", "#66754f", "#9b7132", "#c56f52", "#84607a", "#4f7a78", "#3c3530", + ], + ansi_bright: [ + "#8a8175", "#963f35", "#52603f", "#79551f", "#a85137", "#704d66", "#3d6866", "#27211d", + ], + }, Theme { name: "Cyan on Black", fg: "#00ffff", @@ -210,6 +236,17 @@ pub static PREDEFINED_THEMES: &[Theme] = &[ "#666666", "#ff6666", "#66ff66", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff", ], }, + Theme { + name: "Debian Professional", + fg: "#342a29", + bg: "#f7f1e9", + ansi_normal: [ + "#f7f1e9", "#b34e56", "#5d704d", "#96702c", "#b24d57", "#805c75", "#3e7673", "#342a29", + ], + ansi_bright: [ + "#817a76", "#9e3d48", "#4d6140", "#78561d", "#9b3643", "#6d4963", "#326764", "#241c1b", + ], + }, Theme { name: "Dracula", fg: "#f8f8f2", @@ -232,6 +269,39 @@ pub static PREDEFINED_THEMES: &[Theme] = &[ "#607d8b", "#78909c", "#90a4ae", "#b0bec5", "#cfd8dc", "#eceff1", "#f5f5f5", "#ffffff", ], }, + Theme { + name: "Gentoo", + fg: "#9b86e3", + bg: "#0c0c0c", + ansi_normal: [ + "#0c0c0c", "#c50f1f", "#13a10e", "#c19c00", "#61538d", "#881798", "#00aaff", "#cccccc", + ], + ansi_bright: [ + "#7c7c7c", "#e74856", "#16c60c", "#f9f1a5", "#6f5fa2", "#b4009e", "#0094de", "#f2f2f2", + ], + }, + Theme { + name: "Gentoo Professional", + fg: "#eee7f3", + bg: "#211b28", + ansi_normal: [ + "#211b28", "#c97874", "#91a47a", "#c8a66b", "#806a9c", "#b47ca7", "#70aaa7", "#eee7f3", + ], + ansi_bright: [ + "#6c6270", "#e28b83", "#a3b68b", "#e0bf82", "#a08bbc", "#d099bc", "#83c3bd", "#ffffff", + ], + }, + Theme { + name: "Golden Hour", + fg: "#f5e8c9", + bg: "#211a12", + ansi_normal: [ + "#211a12", "#d06b52", "#a4a06a", "#a38334", "#b98c37", "#a584a5", "#6f9d98", "#f5e8c9", + ], + ansi_bright: [ + "#6b5b43", "#ee8468", "#bab981", "#e0c16f", "#d6a84f", "#c09cc1", "#88b8ae", "#fff3db", + ], + }, Theme { name: "Gruvbox Dark", fg: "#ebdbb2", @@ -386,17 +456,6 @@ pub static PREDEFINED_THEMES: &[Theme] = &[ "#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3", ], }, - Theme { - name: "Spring Blossom", - fg: "#ffb7c5", - bg: "#2d5016", - ansi_normal: [ - "#2d5016", "#c2185b", "#7cb342", "#fbc02d", "#00897b", "#d81b60", "#aed581", "#f8bbd0", - ], - ansi_bright: [ - "#558b2f", "#e91e63", "#8bc34a", "#ffeb3b", "#009688", "#ec407a", "#c5e1a5", "#fce4ec", - ], - }, Theme { name: "Stormy Night", fg: "#b0bec5", @@ -430,6 +489,17 @@ pub static PREDEFINED_THEMES: &[Theme] = &[ "#414868", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5", ], }, + Theme { + name: "Ubuntu Professional", + fg: "#302a34", + bg: "#f7f2ea", + ansi_normal: [ + "#f7f2ea", "#ad4838", "#526f4d", "#976b2a", "#bc5227", "#6d3b69", "#367777", "#302a34", + ], + ansi_bright: [ + "#827b78", "#963d30", "#466242", "#79531c", "#a54018", "#5d2d58", "#2c6666", "#211c25", + ], + }, Theme { name: "Velvet Dusk", fg: "#eae1f2", @@ -531,18 +601,13 @@ mod tests { } #[test] - fn sunny_day_is_gone() { - // Removed: a green background with a pale yellow foreground left - // almost nothing readable on it. + fn removed_themes_are_absent_and_catalog_count_is_current() { assert!( !PREDEFINED_THEMES.iter().any(|t| t.name == "Sunny Day"), "Sunny Day should have been deleted" ); - // Dropping a palette shrinks the Theme menu, so the docs and the - // about page count have to move with it. This assertion caught the - // count being wrong in the docs before this change: they claimed 28 - // for what was really 24. - assert_eq!(PREDEFINED_THEMES.len(), 34); + assert!(!PREDEFINED_THEMES.iter().any(|t| t.name == "Spring Blossom")); + assert_eq!(PREDEFINED_THEMES.len(), 40); } #[test] @@ -566,6 +631,61 @@ mod tests { assert_eq!(get_theme("Orchid Noir").ansi_bright[5], "#ff86c3"); } + #[test] + fn gentoo_theme_preserves_konsole_palette_and_uses_white_button_text() { + let gentoo = get_theme("Gentoo"); + assert!(gentoo.is_dark()); + assert_eq!(gentoo.bg, "#0c0c0c"); + assert_eq!(gentoo.fg, "#9b86e3"); + assert_eq!( + gentoo.ansi_normal, + [ + "#0c0c0c", "#c50f1f", "#13a10e", "#c19c00", "#61538d", "#881798", "#00aaff", + "#cccccc", + ] + ); + assert_eq!( + gentoo.ansi_bright, + [ + "#7c7c7c", "#e74856", "#16c60c", "#f9f1a5", "#6f5fa2", "#b4009e", "#0094de", + "#f2f2f2", + ] + ); + + let (button_bg, button_fg) = gentoo.button_colors(4); + assert_eq!(button_bg, "#6f5fa2"); + assert_eq!(button_fg, "#ffffff"); + assert_eq!(gentoo.button_colors(1), ("#e74856", "#0c0c0c")); + assert!(gentoo.to_css_block().contains("--btn-primary-fg: #ffffff;")); + } + + #[test] + fn warm_and_distro_themes_have_distinct_light_dark_palettes() { + assert!(!get_theme("Claude").is_dark()); + assert!(get_theme("Golden Hour").is_dark()); + assert!(!get_theme("Ubuntu Professional").is_dark()); + assert!(!get_theme("Debian Professional").is_dark()); + assert!(get_theme("Gentoo Professional").is_dark()); + + assert_eq!(get_theme("Claude").ansi_bright[4], "#a85137"); + assert_eq!(get_theme("Golden Hour").ansi_bright[4], "#d6a84f"); + assert_eq!(get_theme("Ubuntu Professional").ansi_bright[4], "#a54018"); + assert_eq!(get_theme("Debian Professional").ansi_bright[4], "#9b3643"); + assert_eq!(get_theme("Gentoo Professional").ansi_bright[4], "#a08bbc"); + } + + #[test] + fn blueberry_professional_is_a_light_neutral_blueberry_variant() { + let theme = get_theme("Blueberry Professional"); + assert!(!theme.is_dark()); + assert_eq!(theme.bg, "#f4efe6"); + assert_eq!(theme.fg, "#263247"); + assert_eq!(theme.ansi_bright[4], "#2d5e8b"); + assert!(PREDEFINED_THEMES + .iter() + .any(|candidate| candidate.name == "Blueberry on Black")); + } + #[test] fn theme_names_are_alphabetical() { let names: Vec = PREDEFINED_THEMES diff --git a/static/style.css b/static/style.css index 5259c17..2d2f95c 100644 --- a/static/style.css +++ b/static/style.css @@ -86,7 +86,7 @@ button, input, select, textarea { top: calc(100% - 4px); left: 0; min-width: 180px; - /* The Theme menu has 34 entries; cap the height so it cannot run off + /* The Theme menu has 40 entries; cap the height so it cannot run off the bottom of the viewport (body has overflow: hidden). */ max-height: calc(100dvh - 48px); overflow-y: auto;