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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user