theme config, layout preview, container alignment

Server: ThemeConfig entity + CRUD (GET/PUT /theme), SQLite persistence,
ThemeUpdate broadcast to ESP32 on save and initial connect.
Client: render engine uses theme colors, full-screen redraw on theme change.
SPA: theme page with color pickers + presets, layout preview with TS port
of layout engine, justify/align controls on containers.
DisplayHint refactored to struct (kind + h_align + v_align).
This commit is contained in:
2026-06-19 03:26:18 +02:00
parent 81a4167382
commit fe59b68c37
46 changed files with 1276 additions and 118 deletions

View File

@@ -7,39 +7,73 @@ fn theme() -> ThemeConfig {
#[test]
fn plain_text_produces_single_span() {
let spans = parse_markup("hello world", &theme());
assert_eq!(spans, vec![
TextSpan { text: "hello world".into(), color: theme().text },
]);
assert_eq!(
spans,
vec![TextSpan {
text: "hello world".into(),
color: theme().text
},]
);
}
#[test]
fn hex_color_span() {
let spans = parse_markup("temp: {#FF0000}72°F{/}", &theme());
assert_eq!(spans, vec![
TextSpan { text: "temp: ".into(), color: theme().text },
TextSpan { text: "72°F".into(), color: Color(0xFF, 0, 0) },
]);
assert_eq!(
spans,
vec![
TextSpan {
text: "temp: ".into(),
color: theme().text
},
TextSpan {
text: "72°F".into(),
color: Color(0xFF, 0, 0)
},
]
);
}
#[test]
fn theme_color_spans() {
let t = theme();
let spans = parse_markup("{primary}hello{/} {accent}world{/}", &t);
assert_eq!(spans, vec![
TextSpan { text: "hello".into(), color: t.primary },
TextSpan { text: " ".into(), color: t.text },
TextSpan { text: "world".into(), color: t.accent },
]);
assert_eq!(
spans,
vec![
TextSpan {
text: "hello".into(),
color: t.primary
},
TextSpan {
text: " ".into(),
color: t.text
},
TextSpan {
text: "world".into(),
color: t.accent
},
]
);
}
#[test]
fn reset_returns_to_text_color() {
let t = theme();
let spans = parse_markup("{accent}hi{/}bye", &t);
assert_eq!(spans, vec![
TextSpan { text: "hi".into(), color: t.accent },
TextSpan { text: "bye".into(), color: t.text },
]);
assert_eq!(
spans,
vec![
TextSpan {
text: "hi".into(),
color: t.accent
},
TextSpan {
text: "bye".into(),
color: t.text
},
]
);
}
#[test]
@@ -52,16 +86,29 @@ fn empty_input_produces_no_spans() {
fn adjacent_color_spans_no_text_between() {
let t = theme();
let spans = parse_markup("{primary}a{secondary}b{/}", &t);
assert_eq!(spans, vec![
TextSpan { text: "a".into(), color: t.primary },
TextSpan { text: "b".into(), color: t.secondary },
]);
assert_eq!(
spans,
vec![
TextSpan {
text: "a".into(),
color: t.primary
},
TextSpan {
text: "b".into(),
color: t.secondary
},
]
);
}
#[test]
fn unknown_tag_treated_as_literal() {
let spans = parse_markup("{unknown}text", &theme());
assert_eq!(spans, vec![
TextSpan { text: "text".into(), color: theme().text },
]);
assert_eq!(
spans,
vec![TextSpan {
text: "text".into(),
color: theme().text
},]
);
}