Slider reading in a tile: a line under the value in every look, row and fader; Beside it on every dashboard; tile geometry in the docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Melvin Strobl
2026-10-14 15:51:04 +02:00
co-authored by Claude Opus 5.5
parent 84071ee106
commit a6a1f2a2c3
4 changed files with 106 additions and 15 deletions
+3 -3
View File
@@ -511,7 +511,7 @@ export function WidgetPanel({
onChange({ config: { ...cfg, ...changes } })
const series = seriesOf(widget)
// What a slider on a round face reads beside its own value.
// What a slider reads beside its own value.
const reading = (cfg.reading ?? {}) as Series
const runsPick = (cfg.runs ?? {}) as RunsPick
const setSeries = (next: Series[]) => set({ series: next })
@@ -1133,9 +1133,9 @@ export function WidgetPanel({
</div>
) : null}
{widget.type === "slider" && round ? (
{widget.type === "slider" ? (
<div className="grid gap-1.5">
<FieldLabel help="A reading drawn large in the middle of the face — the room's temperature beside the setpoint this sets. The slider's own value then moves to the line at the bottom. Written in the slider's unit and to its step's decimals.">
<FieldLabel help="A reading shown with the value this sets, such as the room's temperature beside its setpoint, in the slider's unit and to its step's decimals. A tile writes it under the value; a round face draws it large in the middle.">
Beside it
</FieldLabel>
<div className="flex items-end gap-1.5">
+54 -6
View File
@@ -19,7 +19,14 @@ import { PlayerWidget } from "./PlayerWidget"
import { usePublish } from "./publish"
import { useUi } from "./ui"
import { COLOR_DTYPES, colorFormatOf } from "./ui/core/color"
import { buttonsOf, config, num, rowsOf, text } from "./ui/core/config"
import {
buttonsOf,
config,
decimalsOf,
num,
rowsOf,
text,
} from "./ui/core/config"
/** Widget types that put a value into the graph rather than read one. */
export const INPUT_WIDGETS = new Set([
@@ -659,6 +666,9 @@ function SliderWidget({ widget, dashboard }: WidgetProps) {
const cfg = config(widget)
const { target, value, send, pulse, locked } = usePublish(widget, dashboard)
const min = num(cfg.min, 0)
// What it sets against: a room's temperature beside its setpoint.
const beside = (cfg.reading ?? {}) as Series
const read = useBoundValue(beside.message || undefined)
if (!target) return <Unbound />
const unit = cfg.unit ? text(cfg.unit) : undefined
@@ -686,6 +696,30 @@ function SliderWidget({ widget, dashboard }: WidgetProps) {
const readout = (
<Readout value={reading} precision={null} unit={unit} size="inline" />
)
// A line under the value, smaller, so the value being set is still the one
// that reads. In the slider's unit and to its step's decimals, as a round
// face writes it: the two are read against each other.
const against = beside.message ? (
<span
data-testid="slider-reading"
className="flex h-4 max-w-full items-center gap-1.5 text-xs"
>
{beside.label ? (
<span className="min-w-0 max-w-24 truncate text-muted-foreground">
{beside.label}
</span>
) : null}
{/* The label gives way first: the number is what the line is for. */}
<span className="shrink-0">
<Readout
value={read?.value}
precision={decimalsOf(num(cfg.step, 1))}
unit={unit}
size="inline"
/>
</span>
</span>
) : null
// A column takes the tile's height and puts the reading under it, which is
// what a fader looks like. A volume control is the case it exists for: it is
@@ -695,7 +729,10 @@ function SliderWidget({ widget, dashboard }: WidgetProps) {
<div className="flex h-full min-h-0 flex-col items-center gap-2">
{pulse}
{control}
<div className="shrink-0">{readout}</div>
<div className="flex max-w-full shrink-0 flex-col items-center">
{readout}
{against}
</div>
</div>
)
}
@@ -706,10 +743,21 @@ function SliderWidget({ widget, dashboard }: WidgetProps) {
<div className="flex items-start gap-3">
{pulse}
<div className="min-w-0 flex-1">{control}</div>
{/* Held to the control's own height, so the value sits on the track's
midline whether or not there is a row of ticks under it. */}
<div className="flex h-[var(--dui-control)] shrink-0 items-center">
{readout}
{/* The reading, if any, under the value: in the ticks' row when there
is one, so it costs the tile no height. Held to half the row, so a
long label in a small tile cannot squeeze the track to nothing. */}
<div
className={cn(
"flex shrink-0 flex-col items-end",
against && "max-w-1/2",
)}
>
{/* Held to the control's own height, so the value sits on the track's
midline whether or not there is a row of ticks under it. */}
<div className="flex h-[var(--dui-control)] items-center">
{readout}
</div>
{against}
</div>
</div>
)