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

@@ -21,6 +21,10 @@ pub struct LayoutNodeDto {
#[serde(skip_serializing_if = "Option::is_none")]
pub padding: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub justify_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub align_items: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<LayoutChildDto>>,
}
@@ -44,6 +48,8 @@ impl From<&LayoutNode> for LayoutNodeDto {
direction: None,
gap: None,
padding: None,
justify_content: None,
align_items: None,
children: None,
},
LayoutNode::Container(c) => Self {
@@ -58,6 +64,25 @@ impl From<&LayoutNode> for LayoutNodeDto {
),
gap: Some(c.gap),
padding: Some(c.padding),
justify_content: Some(
match c.justify_content {
JustifyContent::Start => "start",
JustifyContent::Center => "center",
JustifyContent::End => "end",
JustifyContent::SpaceBetween => "space_between",
JustifyContent::SpaceEvenly => "space_evenly",
}
.into(),
),
align_items: Some(
match c.align_items {
AlignItems::Start => "start",
AlignItems::Center => "center",
AlignItems::End => "end",
AlignItems::Stretch => "stretch",
}
.into(),
),
children: Some(
c.children
.iter()
@@ -109,12 +134,26 @@ impl LayoutNodeDto {
})
.collect::<Result<Vec<_>, _>>()?;
let justify_content = match self.justify_content.as_deref() {
Some("center") => JustifyContent::Center,
Some("end") => JustifyContent::End,
Some("space_between") => JustifyContent::SpaceBetween,
Some("space_evenly") => JustifyContent::SpaceEvenly,
_ => JustifyContent::Start,
};
let align_items = match self.align_items.as_deref() {
Some("center") => AlignItems::Center,
Some("end") => AlignItems::End,
Some("stretch") => AlignItems::Stretch,
_ => AlignItems::Start,
};
Ok(LayoutNode::Container(ContainerNode {
direction,
gap: self.gap.unwrap_or(0),
padding: self.padding.unwrap_or(0),
justify_content: JustifyContent::Start,
align_items: AlignItems::Stretch,
justify_content,
align_items,
children,
}))
}

View File

@@ -2,10 +2,12 @@ pub mod client;
pub mod data_source;
pub mod layout;
pub mod preset;
pub mod theme;
pub mod widget;
pub use client::ClientDto;
pub use data_source::DataSourceDto;
pub use layout::{LayoutChildDto, LayoutDto, LayoutNodeDto, SizingDto};
pub use preset::{CreatePresetDto, PresetDto};
pub use theme::{ColorDto, ThemeDto};
pub use widget::{CreateWidgetDto, KeyMappingDto, WidgetDto};

View File

@@ -0,0 +1,52 @@
use domain::{ThemeColor, ThemeConfig};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct ColorDto {
pub r: u8,
pub g: u8,
pub b: u8,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ThemeDto {
pub primary: ColorDto,
pub secondary: ColorDto,
pub accent: ColorDto,
pub text: ColorDto,
pub background: ColorDto,
}
impl From<&ThemeConfig> for ThemeDto {
fn from(t: &ThemeConfig) -> Self {
let c = |c: &ThemeColor| ColorDto {
r: c.r,
g: c.g,
b: c.b,
};
Self {
primary: c(&t.primary),
secondary: c(&t.secondary),
accent: c(&t.accent),
text: c(&t.text),
background: c(&t.background),
}
}
}
impl ThemeDto {
pub fn into_domain(self) -> ThemeConfig {
let c = |c: ColorDto| ThemeColor {
r: c.r,
g: c.g,
b: c.b,
};
ThemeConfig {
primary: c(self.primary),
secondary: c(self.secondary),
accent: c(self.accent),
text: c(self.text),
background: c(self.background),
}
}
}