Answer five things the panel got wrong

- A tile no longer lifts under the pointer. A finger does not move away
  afterwards the way a cursor does, so whatever hover raised stayed
  raised until something else was touched: a tile stuck, not answering.
- The ground's blobs wander a closed path on their own clock instead of
  sliding back and forth along one line, which read as things moving
  rather than as light in a room.
- A bar is one grid now, its rows borrowing its columns, so names of
  different lengths no longer start and end their tracks in different
  places — two bars that share no baseline cannot be compared, which is
  the one thing a stack of them is for. The names read rightward into
  their tracks, with room either side.
- A reading on its way somewhere is written to as many decimals as the
  value it is heading for. Without that a slider stepping in halves
  passed through 22.37460937 on its way to 24: a number nobody asked
  for, a different width every frame.
- The brightness column is the same control as the slider widget's,
  stood on its end and thicker, and exactly as tall as the disc beside
  it. Getting there meant drawing a slider's rail, fill and handle
  rather than styling `::-webkit-slider-*`: those need one set of rules
  per orientation, each with its own centring quirk, and the handle
  landed off its track when the writing mode turned. The native input
  stays, laid transparent over the top, so the keyboard, the pointer and
  every `aria-` are still its.
This commit is contained in:
2026-08-23 23:28:47 +02:00
parent 6ccc6e9e26
commit 68fa5527b1
14 changed files with 403 additions and 236 deletions
+69 -1
View File
@@ -21,7 +21,7 @@ const stackName = `${flowName}_stack`
const w = (name: string) => `${flowName}.${name}`
/** Every tile the dashboard carries, including the deliberately unbound one. */
const TILES = 8
const TILES = 9
test.use({ storageState: "playwright/.auth/user.json" })
@@ -54,6 +54,7 @@ test.beforeAll(async ({ browser }) => {
{ name: "days", dtype: "list", item: "record" },
{ name: "mode", dtype: "str" },
{ name: "lamp", dtype: "bool" },
{ name: "setpoint", dtype: "float" },
],
},
],
@@ -71,6 +72,7 @@ test.beforeAll(async ({ browser }) => {
await publish(page, w("pv"), 30)
await publish(page, w("grid"), 20)
await publish(page, w("condition"), "sun")
await publish(page, w("setpoint"), 21.5)
await publish(page, w("days"), [
{ label: "Mon", icon: "sun", value: "21°" },
{ label: "Tue", icon: "cloudy", value: "18°" },
@@ -84,6 +86,9 @@ test.beforeAll(async ({ browser }) => {
const dashboard = await (
await api(page, `/dashboards/${dashboardName}`, { method: "POST" })
).json()
// Two rows taller than the default panel: the tiles below already fill it,
// and a widget past the last row is clipped rather than drawn.
dashboard.canvas_height = 1400
dashboard.pages[0].sections[0].widgets = [
{
id: "load",
@@ -159,6 +164,22 @@ test.beforeAll(async ({ browser }) => {
layout: { lg: { x: 3, y: 4, w: 3, h: 2 } },
config: {},
},
{
// No precision configured, so what it is worth is what decides how it is
// written — including on the way there.
id: "aim",
type: "slider",
title: "Setpoint",
layout: { lg: { x: 0, y: 6, w: 4, h: 2 } },
config: {
target: w("setpoint"),
dtype: "float",
min: 16,
max: 24,
step: 0.5,
unit: "°C",
},
},
{
id: "trend",
type: "chart",
@@ -306,6 +327,53 @@ test("rows are drawn in the order they were configured", async ({ page }) => {
await expect(rows.nth(2)).toContainText(/grid/i)
})
test("a reading is written to its own precision while it is moving", async ({
page,
}) => {
await openPanel(page)
const readout = page
.getByTestId("widget-frame")
.filter({ hasText: "Setpoint" })
.getByTestId("readout")
await expect(readout).toContainText("21.5")
// A value tweening toward 24 must pass through 22.0 and 23.5, not
// 22.37460937: a reading nobody asked for, a different width every frame.
// Started before the publish, not awaited: the frames worth looking at are
// the ones between the old reading and the new one.
const seen = page.evaluate(async () => {
const cell = [
...document.querySelectorAll("[data-testid=widget-frame]"),
].find((frame) => frame.textContent?.includes("Setpoint"))
const el = cell?.querySelector("[data-testid=readout]")
const samples: string[] = []
const until = performance.now() + 600
return new Promise<string[]>((resolve) => {
const step = () => {
samples.push(el?.textContent ?? "")
if (performance.now() < until) requestAnimationFrame(step)
else resolve(samples)
}
requestAnimationFrame(step)
})
})
await publish(page, w("setpoint"), 24)
const frames = await seen
for (const frame of frames) {
expect(frame, "a reading grew decimals on its way").toMatch(
/^-?\d+(\.\d)?\s*°C$/,
)
}
// Otherwise the frames above are all the reading standing still, and every
// one of them would pass whatever the tween was doing.
expect(
new Set(frames).size,
"the reading never moved, so nothing was watched",
).toBeGreaterThan(1)
await publish(page, w("setpoint"), 21.5)
})
test("a widget is drawn inside its tile rather than scrolled", async ({
page,
}) => {