ADR-0022 stage 1/8: theme token-class colour fields
Add seven `tok_*` Color fields to Theme — keyword, identifier, number, string, punct, flag, error — populated in both dark and light themes. WCAG-AA contrast against each theme's bg. Identifier and punct sit close to fg/muted so dominant content reads quietly; literals + flags get warm accent tones; keyword takes a cool accent (purple) distinct from the mode-banner blue. tok_error reuses the existing error palette so lex-error tokens read consistently with [error] lines elsewhere. New helper Theme::token_color(&TokenKind) -> Color maps each token kind to its display colour. Tests: 672 passing, 0 failing, 1 ignored (668 baseline → +4 theme tests). Clippy clean. Pure addition; no existing render path uses these yet. Stage 2 wires them into the input panel.
This commit is contained in:
+150
@@ -5,9 +5,21 @@
|
|||||||
//! small for the walking skeleton; it grows as more views are
|
//! small for the walking skeleton; it grows as more views are
|
||||||
//! added. Contrast is chosen against the target background so
|
//! added. Contrast is chosen against the target background so
|
||||||
//! that foreground text meets WCAG-AA (NFR-5) on both variants.
|
//! that foreground text meets WCAG-AA (NFR-5) on both variants.
|
||||||
|
//!
|
||||||
|
//! Per-token-class colours (the `tok_*` fields) drive ambient
|
||||||
|
//! typing assistance (ADR-0022 §3). Each token class has a
|
||||||
|
//! distinct colour so the user can tell keywords from
|
||||||
|
//! identifiers from string literals at a glance, with punct and
|
||||||
|
//! identifier intentionally close to `fg` to keep the surface
|
||||||
|
//! quiet for the dominant content. The `tok_error` colour
|
||||||
|
//! reuses the existing error palette so lex-error tokens and
|
||||||
|
//! parse-error overlays read consistently with `[error]`
|
||||||
|
//! lines elsewhere.
|
||||||
|
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
|
|
||||||
|
use crate::dsl::lexer::TokenKind;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Background {
|
pub enum Background {
|
||||||
Light,
|
Light,
|
||||||
@@ -26,6 +38,14 @@ pub struct Theme {
|
|||||||
pub mode_advanced: Color,
|
pub mode_advanced: Color,
|
||||||
pub system: Color,
|
pub system: Color,
|
||||||
pub error: Color,
|
pub error: Color,
|
||||||
|
// ---- Per-token-class colours (ADR-0022 §3) -------------------
|
||||||
|
pub tok_keyword: Color,
|
||||||
|
pub tok_identifier: Color,
|
||||||
|
pub tok_number: Color,
|
||||||
|
pub tok_string: Color,
|
||||||
|
pub tok_punct: Color,
|
||||||
|
pub tok_flag: Color,
|
||||||
|
pub tok_error: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Theme {
|
impl Theme {
|
||||||
@@ -42,6 +62,20 @@ impl Theme {
|
|||||||
mode_advanced: Color::Rgb(0xFF, 0x9E, 0x6B),
|
mode_advanced: Color::Rgb(0xFF, 0x9E, 0x6B),
|
||||||
system: Color::Rgb(0x9F, 0xD8, 0x91),
|
system: Color::Rgb(0x9F, 0xD8, 0x91),
|
||||||
error: Color::Rgb(0xFF, 0x6B, 0x6B),
|
error: Color::Rgb(0xFF, 0x6B, 0x6B),
|
||||||
|
// Token classes — distinct enough to tell apart at a
|
||||||
|
// glance, quiet enough that 80-char lines don't read
|
||||||
|
// like a Christmas tree. Identifier and punct sit
|
||||||
|
// close to `fg`/`muted` so the dominant content
|
||||||
|
// remains restful; literals and flags get warm
|
||||||
|
// accent tones; keyword takes a cool accent tone
|
||||||
|
// distinct from the mode-banner blue.
|
||||||
|
tok_keyword: Color::Rgb(0xC7, 0x92, 0xEA), // muted purple
|
||||||
|
tok_identifier: Color::Rgb(0xE6, 0xE6, 0xE6), // == fg
|
||||||
|
tok_number: Color::Rgb(0xF7, 0x8C, 0x6C), // warm orange
|
||||||
|
tok_string: Color::Rgb(0xC3, 0xE8, 0x8D), // soft green
|
||||||
|
tok_punct: Color::Rgb(0x8B, 0x90, 0x9A), // == muted
|
||||||
|
tok_flag: Color::Rgb(0xFF, 0xCB, 0x6B), // amber
|
||||||
|
tok_error: Color::Rgb(0xFF, 0x6B, 0x6B), // == error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +92,33 @@ impl Theme {
|
|||||||
mode_advanced: Color::Rgb(0xB0, 0x4A, 0x12),
|
mode_advanced: Color::Rgb(0xB0, 0x4A, 0x12),
|
||||||
system: Color::Rgb(0x2E, 0x7C, 0x3C),
|
system: Color::Rgb(0x2E, 0x7C, 0x3C),
|
||||||
error: Color::Rgb(0xC0, 0x39, 0x2B),
|
error: Color::Rgb(0xC0, 0x39, 0x2B),
|
||||||
|
// Light-theme token palette: same intent as dark —
|
||||||
|
// identifier/punct close to fg/muted; warm tones for
|
||||||
|
// literals + flags; cool accent for keyword.
|
||||||
|
tok_keyword: Color::Rgb(0x6F, 0x42, 0xC1), // royal purple
|
||||||
|
tok_identifier: Color::Rgb(0x1A, 0x1F, 0x2C), // == fg
|
||||||
|
tok_number: Color::Rgb(0xBC, 0x4F, 0x1F), // burnt orange
|
||||||
|
tok_string: Color::Rgb(0x22, 0x86, 0x3A), // forest green
|
||||||
|
tok_punct: Color::Rgb(0x60, 0x66, 0x73), // == muted
|
||||||
|
tok_flag: Color::Rgb(0xB0, 0x88, 0x00), // mustard
|
||||||
|
tok_error: Color::Rgb(0xC0, 0x39, 0x2B), // == error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a `TokenKind` to its display colour for ambient
|
||||||
|
/// highlighting (ADR-0022 §3). Lex-error tokens always render
|
||||||
|
/// in `tok_error`, regardless of the parse-time error overlay
|
||||||
|
/// applied separately by the renderer.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn token_color(&self, kind: &TokenKind) -> Color {
|
||||||
|
match kind {
|
||||||
|
TokenKind::Keyword(_) => self.tok_keyword,
|
||||||
|
TokenKind::Identifier(_) => self.tok_identifier,
|
||||||
|
TokenKind::Number(_) => self.tok_number,
|
||||||
|
TokenKind::StringLiteral(_) => self.tok_string,
|
||||||
|
TokenKind::Punct(_) => self.tok_punct,
|
||||||
|
TokenKind::Flag(_) => self.tok_flag,
|
||||||
|
TokenKind::Error(_) => self.tok_error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,3 +128,92 @@ impl Default for Theme {
|
|||||||
Self::dark()
|
Self::dark()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::dsl::keyword::{Keyword, Punct};
|
||||||
|
use crate::dsl::lexer::LexError;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dark_theme_token_colours_differ_from_background() {
|
||||||
|
let t = Theme::dark();
|
||||||
|
for (name, c) in [
|
||||||
|
("tok_keyword", t.tok_keyword),
|
||||||
|
("tok_number", t.tok_number),
|
||||||
|
("tok_string", t.tok_string),
|
||||||
|
("tok_flag", t.tok_flag),
|
||||||
|
("tok_error", t.tok_error),
|
||||||
|
] {
|
||||||
|
assert_ne!(
|
||||||
|
c, t.bg,
|
||||||
|
"{name} must contrast against bg in dark theme",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn light_theme_token_colours_differ_from_background() {
|
||||||
|
let t = Theme::light();
|
||||||
|
for (name, c) in [
|
||||||
|
("tok_keyword", t.tok_keyword),
|
||||||
|
("tok_number", t.tok_number),
|
||||||
|
("tok_string", t.tok_string),
|
||||||
|
("tok_flag", t.tok_flag),
|
||||||
|
("tok_error", t.tok_error),
|
||||||
|
] {
|
||||||
|
assert_ne!(
|
||||||
|
c, t.bg,
|
||||||
|
"{name} must contrast against bg in light theme",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_color_maps_each_kind_to_the_expected_field() {
|
||||||
|
let t = Theme::dark();
|
||||||
|
assert_eq!(
|
||||||
|
t.token_color(&TokenKind::Keyword(Keyword::Create)),
|
||||||
|
t.tok_keyword,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
t.token_color(&TokenKind::Identifier("Customers".to_string())),
|
||||||
|
t.tok_identifier,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
t.token_color(&TokenKind::Number("42".to_string())),
|
||||||
|
t.tok_number,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
t.token_color(&TokenKind::StringLiteral("hi".to_string())),
|
||||||
|
t.tok_string,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
t.token_color(&TokenKind::Punct(Punct::Colon)),
|
||||||
|
t.tok_punct,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
t.token_color(&TokenKind::Flag("all-rows".to_string())),
|
||||||
|
t.tok_flag,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
t.token_color(&TokenKind::Error(LexError::UnknownChar('$'))),
|
||||||
|
t.tok_error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lex_error_tokens_render_in_tok_error_regardless_of_kind() {
|
||||||
|
let t = Theme::dark();
|
||||||
|
for err in [
|
||||||
|
LexError::UnknownChar('$'),
|
||||||
|
LexError::UnterminatedString,
|
||||||
|
LexError::BadFlag,
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
t.token_color(&TokenKind::Error(err)),
|
||||||
|
t.tok_error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user