Bump dashboards: bar, icon, forecast and clock widget types

Plumbing only: the widget-type literal and its dtype table on both sides,
the regenerated client, a curated lucide map and four stubs the renderers
are wired to. Also a latching switch and a segmented dropdown, both a
`style` on the control that already publishes and reads back, plus the
option editor a dropdown never had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
This commit is contained in:
2026-08-20 08:31:36 +02:00
co-authored by Claude Opus 5
parent c005ea48e1
commit 21b01e2d25
11 changed files with 389 additions and 22 deletions
+11 -2
View File
@@ -42,6 +42,10 @@ WidgetType = Literal[
"markdown",
"agenda",
"notification",
"bar",
"icon",
"forecast",
"clock",
# Input
"button",
"switch",
@@ -66,6 +70,10 @@ WIDGET_DTYPES: dict[str, set[str]] = {
"switch": {"bool"},
"agenda": {"list"},
"notification": {"record"},
"bar": {"float", "int"},
"forecast": {"list"},
# An icon maps weather strings, bool hints and numbers alike, and a clock
# binds nothing at all, so neither has a row to be held to.
}
@@ -124,7 +132,8 @@ class WidgetDef(BaseModel):
if series.get("message")
]
name = self.config.get("message")
return [str(name)] if name else []
inner = self.config.get("inner") # only a bar nests a second reading
return [str(value) for value in (name, inner) if value]
@property
def target(self) -> str:
@@ -161,7 +170,7 @@ class WidgetDef(BaseModel):
str(series.get("dtype") or "")
for series in self.config.get("series") or []
]
return [str(self.config.get("dtype") or "")]
return [str(self.config.get(key) or "") for key in ("dtype", "inner_dtype")]
@model_validator(mode="after")
def _check_binding(self) -> WidgetDef:
+36
View File
@@ -213,6 +213,7 @@ def test_a_widget_refuses_a_dtype_it_cannot_carry():
def test_the_structured_widgets_bind_their_shapes():
WidgetDef(id="a", type="agenda", config={"message": "a.b", "dtype": "list"})
WidgetDef(id="n", type="notification", config={"message": "a.b", "dtype": "record"})
WidgetDef(id="f", type="forecast", config={"message": "a.b", "dtype": "list"})
with pytest.raises(ValueError):
WidgetDef(id="a", type="agenda", config={"message": "a.b", "dtype": "json"})
@@ -220,6 +221,41 @@ def test_the_structured_widgets_bind_their_shapes():
WidgetDef(
id="n", type="notification", config={"message": "a.b", "dtype": "list"}
)
with pytest.raises(ValueError):
WidgetDef(id="f", type="forecast", config={"message": "a.b", "dtype": "json"})
def test_a_bar_nests_a_second_number():
WidgetDef(
id="b",
type="bar",
config={
"message": "a.in",
"dtype": "float",
"inner": "a.pv",
"inner_dtype": "int",
},
)
with pytest.raises(ValueError):
WidgetDef(
id="b",
type="bar",
config={"message": "a.in", "dtype": "float", "inner_dtype": "bool"},
)
def test_a_bar_is_drawn_on_both_readings_it_nests():
widget = WidgetDef(id="b", type="bar", config={"message": "a.in", "inner": "a.pv"})
assert widget.messages == ["a.in", "a.pv"]
def test_a_clock_reads_nothing_and_publishes_nothing():
widget = WidgetDef(id="c", type="clock", config={"format": "24h"})
assert widget.messages == []
assert widget.target == ""
def test_a_querying_chart_asks_with_a_record_and_draws_a_series():