diff --git a/docs/interface/dashboards.md b/docs/interface/dashboards.md index b75f731..922052e 100644 --- a/docs/interface/dashboards.md +++ b/docs/interface/dashboards.md @@ -48,7 +48,7 @@ still names the widget for accessibility and published values. | **Button** | fixed value | one-shot action | | **Buttons** | a value per button | grid of actions | | **Switch** | `bool` | on/off | -| **Slider** | `float`, `int` | min, max, step; horizontal or vertical | +| **Slider** | `float`, `int` | min, max, step; horizontal or vertical; a reading beside it | | **Input** | text or number | free entry | | **Selector** | selected value | menu or row of choices | | **Colour** | list or string | HSV, RGB or hex | @@ -63,6 +63,27 @@ endpoints feeding the nodes that consume those messages. Bindings are checked in the editor and on the server. See [Payload types](../reference/payload-types.md). +### A slider's reading + +A Slider can also show what it sets against, such as the room's temperature +beside a setpoint: **Beside it** binds `reading: {message, dtype, label}` to a +`float` or `int`. The reading is only shown, never published. It is written in +the slider's unit and to its step's decimals (a step of 0.5 writes 21 as +`21.0`); `—` stands in for the value until one arrives. On a round face it +takes the middle (see [Round dashboards](#round-dashboards)); in a tile it is a +line under the slider's value. + +Sizes are CSS pixels of the canvas, as in the round geometry below. The +slider's own value is at the tile's text size (14; 16 on a touch-friendly +panel), centred on a line as tall as the control (32; 48 touch-friendly). + +| Part | Value | +|---|---| +| Line | 16 high, text 12: the label (muted, at most 96 wide, cut with an ellipsis), 6, the value (text colour), 6, the unit (muted). Without a label, the value and unit alone | +| Row | directly under the slider's value, right-aligned with it, in the column right of the track (12 apart). With ticks the line shares the ticks' row, so the tile grows no taller; without them it adds 16 | +| Row width | the value column is as wide as its wider line, but at most half the row; the track takes the rest. What does not fit is cut from the label; the value and unit never are | +| Fader | directly under the slider's value, both centred under the track, 8 below it; the line no wider than the tile inside its padding, cut from the label first | + ## Charts Choose the chart's data source: @@ -188,11 +209,8 @@ at once and is published once, 300 ms after the last notch or when the pointer is released. A Gauge only shows. On any other face, turning a knob only wakes the screen. -A Slider can also show what it sets against, such as the room's temperature -beside a setpoint: **Beside it** binds `reading: {message, dtype, label}` to a -`float` or `int`. That reading is written large in the middle, in the slider's -unit and to its step's decimals, under its label; the slider's own title and -value move to the line in the ring's gap. +A Slider's reading (**Beside it**) is written large in the middle, under its +label; the slider's own title and value move to the line in the ring's gap. Value, Switch, Button, Clock, Icon, Notification and Text have faces of their own. Every other type is drawn as its rectangular body in the middle of the diff --git a/frontend/src/components/Dashboard/panels.tsx b/frontend/src/components/Dashboard/panels.tsx index a7cebcc..871dcfd 100644 --- a/frontend/src/components/Dashboard/panels.tsx +++ b/frontend/src/components/Dashboard/panels.tsx @@ -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({ ) : null} - {widget.type === "slider" && round ? ( + {widget.type === "slider" ? (
- + Beside it
diff --git a/frontend/src/components/Dashboard/widgets.tsx b/frontend/src/components/Dashboard/widgets.tsx index 03dc8ec..0767d6c 100644 --- a/frontend/src/components/Dashboard/widgets.tsx +++ b/frontend/src/components/Dashboard/widgets.tsx @@ -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 const unit = cfg.unit ? text(cfg.unit) : undefined @@ -686,6 +696,30 @@ function SliderWidget({ widget, dashboard }: WidgetProps) { const readout = ( ) + // 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 ? ( + + {beside.label ? ( + + {beside.label} + + ) : null} + {/* The label gives way first: the number is what the line is for. */} + + + + + ) : 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) {
{pulse} {control} -
{readout}
+
+ {readout} + {against} +
) } @@ -706,10 +743,21 @@ function SliderWidget({ widget, dashboard }: WidgetProps) {
{pulse}
{control}
- {/* 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. */} -
- {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. */} +
+ {/* 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. */} +
+ {readout} +
+ {against}
) diff --git a/frontend/tests/widgets.spec.ts b/frontend/tests/widgets.spec.ts index 1898858..7de9fc7 100644 --- a/frontend/tests/widgets.spec.ts +++ b/frontend/tests/widgets.spec.ts @@ -249,6 +249,21 @@ test.beforeAll(async ({ browser }) => { unit: " kW", }, }, + { + id: "against", + type: "slider", + title: "Against", + layout: { lg: { x: 6, y: 0, w: 4, h: 2 } }, + config: { + target: w("setpoint"), + dtype: "float", + min: 0, + max: 100, + step: 0.5, + unit: " kW", + reading: { message: w("level"), dtype: "float", label: "Level" }, + }, + }, ] const stacked = await ( await api(page, `/dashboards/${stackName}`, { method: "PUT", data: stack }) @@ -409,6 +424,16 @@ test("rows are drawn in the order they were configured", async ({ page }) => { await expect(rows.nth(2)).toContainText(/grid/i) }) +test("a slider writes what it sets against under its value", async ({ + page, +}) => { + await openPanel(page, stackName) + // In the slider's unit and to its step's decimals, not the reading's own. + await expect(page.getByTestId("slider-reading")).toHaveText( + /^Level\s*80\.0\s*kW$/, + ) +}) + test("a reading is written to its own precision while it is moving", async ({ page, }) => {