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

@@ -0,0 +1,40 @@
use crate::error::SqliteConfigError;
use domain::{ThemeColor, ThemeConfig};
pub fn theme_to_json(theme: &ThemeConfig) -> Result<String, SqliteConfigError> {
let v = serde_json::json!({
"primary": color_to_json(&theme.primary),
"secondary": color_to_json(&theme.secondary),
"accent": color_to_json(&theme.accent),
"text": color_to_json(&theme.text),
"background": color_to_json(&theme.background),
});
serde_json::to_string(&v).map_err(|e| SqliteConfigError::Serialization(e.to_string()))
}
pub fn theme_from_json(json: &str) -> Result<ThemeConfig, SqliteConfigError> {
let v: serde_json::Value =
serde_json::from_str(json).map_err(|e| SqliteConfigError::Serialization(e.to_string()))?;
let err = |msg: &str| SqliteConfigError::Serialization(msg.into());
Ok(ThemeConfig {
primary: color_from_json(&v["primary"]).map_err(|_| err("invalid primary"))?,
secondary: color_from_json(&v["secondary"]).map_err(|_| err("invalid secondary"))?,
accent: color_from_json(&v["accent"]).map_err(|_| err("invalid accent"))?,
text: color_from_json(&v["text"]).map_err(|_| err("invalid text"))?,
background: color_from_json(&v["background"]).map_err(|_| err("invalid background"))?,
})
}
fn color_to_json(c: &ThemeColor) -> serde_json::Value {
serde_json::json!({ "r": c.r, "g": c.g, "b": c.b })
}
fn color_from_json(v: &serde_json::Value) -> Result<ThemeColor, SqliteConfigError> {
let err = |msg: &str| SqliteConfigError::Serialization(msg.into());
Ok(ThemeColor {
r: v["r"].as_u64().ok_or_else(|| err("missing r"))? as u8,
g: v["g"].as_u64().ok_or_else(|| err("missing g"))? as u8,
b: v["b"].as_u64().ok_or_else(|| err("missing b"))? as u8,
})
}