diff --git a/crates/adapters/config-sqlite/src/lib.rs b/crates/adapters/config-sqlite/src/lib.rs index 2f4557d..bce9b96 100644 --- a/crates/adapters/config-sqlite/src/lib.rs +++ b/crates/adapters/config-sqlite/src/lib.rs @@ -96,6 +96,14 @@ impl SqliteConfigStore { .execute(&self.pool) .await?; + // Add alignment columns to widgets (idempotent) + let _ = sqlx::query("ALTER TABLE widgets ADD COLUMN h_align TEXT NOT NULL DEFAULT 'left'") + .execute(&self.pool) + .await; + let _ = sqlx::query("ALTER TABLE widgets ADD COLUMN v_align TEXT NOT NULL DEFAULT 'top'") + .execute(&self.pool) + .await; + Ok(()) } } diff --git a/crates/adapters/config-sqlite/src/repository/widgets.rs b/crates/adapters/config-sqlite/src/repository/widgets.rs index 48fcc74..b7ae2a9 100644 --- a/crates/adapters/config-sqlite/src/repository/widgets.rs +++ b/crates/adapters/config-sqlite/src/repository/widgets.rs @@ -34,15 +34,19 @@ impl SqliteConfigStore { config: &WidgetConfig, ) -> Result<(), SqliteConfigError> { let mappings_json = ser::mappings_to_json(&config.mappings)?; - let hint_str = ser::display_hint_to_str(&config.display_hint); + let hint_str = ser::display_hint_kind_to_str(&config.display_hint); + let h_align_str = ser::h_align_to_str(config.display_hint.h_align); + let v_align_str = ser::v_align_to_str(config.display_hint.v_align); sqlx::query( - "INSERT OR REPLACE INTO widgets (id, name, display_hint, data_source_id, mappings, max_data_size) - VALUES (?, ?, ?, ?, ?, ?)" + "INSERT OR REPLACE INTO widgets (id, name, display_hint, h_align, v_align, data_source_id, mappings, max_data_size) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)" ) .bind(config.id as i64) .bind(&config.name) .bind(hint_str) + .bind(h_align_str) + .bind(v_align_str) .bind(config.data_source_id as i64) .bind(&mappings_json) .bind(config.max_data_size as i64) diff --git a/crates/adapters/config-sqlite/src/serialization/widget.rs b/crates/adapters/config-sqlite/src/serialization/widget.rs index bb089e3..2719e27 100644 --- a/crates/adapters/config-sqlite/src/serialization/widget.rs +++ b/crates/adapters/config-sqlite/src/serialization/widget.rs @@ -1,9 +1,9 @@ use crate::error::SqliteConfigError; -use domain::{DisplayHint, DisplayHintKind, KeyMapping, WidgetConfig}; +use domain::{DisplayHint, DisplayHintKind, HAlign, KeyMapping, VAlign, WidgetConfig}; use sqlx::Row; use sqlx::sqlite::SqliteRow; -pub fn display_hint_to_str(hint: &DisplayHint) -> &'static str { +pub fn display_hint_kind_to_str(hint: &DisplayHint) -> &'static str { match hint.kind { DisplayHintKind::IconValue => "icon_value", DisplayHintKind::TextBlock => "text_block", @@ -11,17 +11,55 @@ pub fn display_hint_to_str(hint: &DisplayHint) -> &'static str { } } -fn display_hint_from_str(s: &str) -> Result { +pub fn h_align_to_str(a: HAlign) -> &'static str { + match a { + HAlign::Left => "left", + HAlign::Center => "center", + HAlign::Right => "right", + } +} + +pub fn v_align_to_str(a: VAlign) -> &'static str { + match a { + VAlign::Top => "top", + VAlign::Middle => "middle", + VAlign::Bottom => "bottom", + } +} + +fn hint_kind_from_str(s: &str) -> Result { match s { - "icon_value" => Ok(DisplayHint::new(DisplayHintKind::IconValue)), - "text_block" => Ok(DisplayHint::new(DisplayHintKind::TextBlock)), - "key_value" => Ok(DisplayHint::new(DisplayHintKind::KeyValue)), + "icon_value" => Ok(DisplayHintKind::IconValue), + "text_block" => Ok(DisplayHintKind::TextBlock), + "key_value" => Ok(DisplayHintKind::KeyValue), _ => Err(SqliteConfigError::Serialization(format!( "unknown display hint: {s}" ))), } } +fn h_align_from_str(s: &str) -> Result { + match s { + "left" => Ok(HAlign::Left), + "center" => Ok(HAlign::Center), + "right" => Ok(HAlign::Right), + _ => Err(SqliteConfigError::Serialization(format!( + "unknown h_align: {s}" + ))), + } +} + +fn v_align_from_str(s: &str) -> Result { + match s { + "top" => Ok(VAlign::Top), + "middle" => Ok(VAlign::Middle), + "bottom" => Ok(VAlign::Bottom), + _ => Err(SqliteConfigError::Serialization(format!( + "unknown v_align: {s}" + ))), + } +} + pub fn mappings_to_json(mappings: &[KeyMapping]) -> Result { let entries: Vec = mappings .iter() @@ -60,6 +98,8 @@ pub fn widget_from_row(row: &SqliteRow) -> Result Result String { + "left".into() +} + +fn default_v_align() -> String { + "top".into() +} + #[derive(Serialize, Deserialize)] pub struct WidgetDto { pub id: u16, pub name: String, - pub display_hint: String, + pub display_hint: DisplayHintDto, pub data_source_id: u16, pub mappings: Vec, pub max_data_size: u16, @@ -21,7 +38,7 @@ pub struct WidgetDto { pub struct CreateWidgetDto { pub id: u16, pub name: String, - pub display_hint: String, + pub display_hint: DisplayHintDto, pub data_source_id: u16, pub mappings: Vec, #[serde(default = "default_max_data_size")] @@ -32,17 +49,40 @@ fn default_max_data_size() -> u16 { 2048 } +fn kind_to_str(kind: &DisplayHintKind) -> &'static str { + match kind { + DisplayHintKind::IconValue => "icon_value", + DisplayHintKind::TextBlock => "text_block", + DisplayHintKind::KeyValue => "key_value", + } +} + +fn h_align_to_str(a: HAlign) -> &'static str { + match a { + HAlign::Left => "left", + HAlign::Center => "center", + HAlign::Right => "right", + } +} + +fn v_align_to_str(a: VAlign) -> &'static str { + match a { + VAlign::Top => "top", + VAlign::Middle => "middle", + VAlign::Bottom => "bottom", + } +} + impl From<&WidgetConfig> for WidgetDto { fn from(w: &WidgetConfig) -> Self { Self { id: w.id, name: w.name.clone(), - display_hint: match w.display_hint.kind { - DisplayHintKind::IconValue => "icon_value", - DisplayHintKind::TextBlock => "text_block", - DisplayHintKind::KeyValue => "key_value", - } - .into(), + display_hint: DisplayHintDto { + kind: kind_to_str(&w.display_hint.kind).into(), + h_align: h_align_to_str(w.display_hint.h_align).into(), + v_align: v_align_to_str(w.display_hint.v_align).into(), + }, data_source_id: w.data_source_id, mappings: w .mappings @@ -59,16 +99,32 @@ impl From<&WidgetConfig> for WidgetDto { impl CreateWidgetDto { pub fn into_domain(self) -> Result { - let hint = match self.display_hint.as_str() { - "icon_value" => DisplayHint::new(DisplayHintKind::IconValue), - "text_block" => DisplayHint::new(DisplayHintKind::TextBlock), - "key_value" => DisplayHint::new(DisplayHintKind::KeyValue), - h => return Err(format!("unknown display_hint: {h}")), + let kind = match self.display_hint.kind.as_str() { + "icon_value" => DisplayHintKind::IconValue, + "text_block" => DisplayHintKind::TextBlock, + "key_value" => DisplayHintKind::KeyValue, + h => return Err(format!("unknown display_hint kind: {h}")), + }; + let h_align = match self.display_hint.h_align.as_str() { + "left" => HAlign::Left, + "center" => HAlign::Center, + "right" => HAlign::Right, + h => return Err(format!("unknown h_align: {h}")), + }; + let v_align = match self.display_hint.v_align.as_str() { + "top" => VAlign::Top, + "middle" => VAlign::Middle, + "bottom" => VAlign::Bottom, + v => return Err(format!("unknown v_align: {v}")), }; Ok(WidgetConfig { id: self.id, name: self.name, - display_hint: hint, + display_hint: DisplayHint { + kind, + h_align, + v_align, + }, data_source_id: self.data_source_id, mappings: self .mappings diff --git a/spa/src/api/types.ts b/spa/src/api/types.ts index fe44303..3200705 100644 --- a/spa/src/api/types.ts +++ b/spa/src/api/types.ts @@ -1,4 +1,12 @@ -export type DisplayHint = "icon_value" | "text_block" | "key_value" +export type DisplayHintKind = "icon_value" | "text_block" | "key_value" +export type HAlign = "left" | "center" | "right" +export type VAlign = "top" | "middle" | "bottom" + +export interface DisplayHint { + kind: DisplayHintKind + h_align: HAlign + v_align: VAlign +} export type SourceType = "weather" | "media" | "rss" | "http_json" | "webhook" export type SizingType = "fixed" | "flex" export type Direction = "row" | "column" diff --git a/spa/src/components/layout-preview.tsx b/spa/src/components/layout-preview.tsx index d496127..c594b6c 100644 --- a/spa/src/components/layout-preview.tsx +++ b/spa/src/components/layout-preview.tsx @@ -54,6 +54,11 @@ export function LayoutPreview({ const box = bounds.get(wid) if (!box) return null const w = widgets.find((w) => w.id === wid) + const hAlign = w?.display_hint?.h_align ?? "left" + const vAlign = w?.display_hint?.v_align ?? "top" + const flexAlign = hAlign === "center" ? "center" : hAlign === "right" ? "flex-end" : "flex-start" + const flexJustify = vAlign === "middle" ? "center" : vAlign === "bottom" ? "flex-end" : "flex-start" + const textAlign = hAlign === "center" ? "center" as const : hAlign === "right" ? "right" as const : "left" as const return (
@@ -88,10 +93,10 @@ export function LayoutPreview({ style={{ fontSize: 8 * scale, color: colorToCSS(theme.accent), - textAlign: "center", + textAlign, }} > - {w.display_hint} + {w.display_hint.kind} )}
diff --git a/spa/src/pages/widgets.tsx b/spa/src/pages/widgets.tsx index b468162..ca0a154 100644 --- a/spa/src/pages/widgets.tsx +++ b/spa/src/pages/widgets.tsx @@ -7,7 +7,7 @@ import { useWidgetPreview, } from "@/api/widgets" import { useDataSources } from "@/api/data-sources" -import type { Widget, DisplayHint, KeyMapping } from "@/api/types" +import type { Widget, DisplayHintKind, HAlign, VAlign, KeyMapping } from "@/api/types" import { Button } from "@/components/ui/button" import { Card, @@ -46,13 +46,12 @@ import { Badge } from "@/components/ui/badge" import { Plus, Pencil, Trash2, X, Eye } from "lucide-react" import { toast } from "sonner" -const DISPLAY_HINTS: DisplayHint[] = ["icon_value", "text_block", "key_value"] - +const DISPLAY_HINT_KINDS: DisplayHintKind[] = ["icon_value", "text_block", "key_value"] const EMPTY: Widget = { id: 0, name: "", - display_hint: "icon_value", + display_hint: { kind: "icon_value", h_align: "left", v_align: "top" }, data_source_id: 0, mappings: [], max_data_size: 2048, @@ -141,7 +140,7 @@ export function WidgetsPage() {
{w.name} - {w.display_hint} + {w.display_hint.kind} source: {sourceName(w.data_source_id)} {w.mappings.length} mapping(s) @@ -326,14 +325,14 @@ function WidgetForm({
+
+
+ + +
+
+ + +
+