Draw the colour disc inside its tile rather than past it
Docs / docs (push) Successful in 42s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m10s
Playwright Tests / test-playwright (2, 2) (push) Failing after 22s
pre-commit / pre-commit (push) Failing after 2m7s
Test Backend / test-backend (push) Failing after 48s
Compose Smoke Test / test-compose (push) Failing after 22s
Playwright Tests / merge-reports (push) Failing after 1m6s

The disc was sized against its parents in percentages, and every box
between it and the tile is sized by what is in it — so the chain never
resolved, the brightness slider grew, and the tile scrolled with half a
disc showing. Container units are the tile's own height whatever sits in
between, so the widget now fits exactly.

Two guards behind it, since both are things a picture can fail silently:
nothing on a panel of pictures may overflow its tile, and Touch has to
actually make a control bigger while leaving what it publishes alone.
This commit is contained in:
2026-08-23 22:07:21 +02:00
parent 5b9628c02e
commit 57c5e3233b
6 changed files with 86 additions and 22 deletions
@@ -55,19 +55,17 @@ export function ColorWidget({ widget, dashboard }: WidgetProps) {
// The pulse hangs off the frame, so it stays outside every box below.
<>
{pulse}
<div className="min-h-0 flex-1">
<ColorDisk
name={widget.title || target}
hsv={current}
disabled={locked}
onChange={setDraft}
onCommit={() => {
if (draft === null) return
send(encodeColor(draft, format))
setDraft(null)
}}
/>
</div>
<ColorDisk
name={widget.title || target}
hsv={current}
disabled={locked}
onChange={setDraft}
onCommit={() => {
if (draft === null) return
send(encodeColor(draft, format))
setDraft(null)
}}
/>
</>
)
}
@@ -118,6 +118,29 @@
}
}
/*
* The colour widget: a disc, and brightness beside it.
*
* Sized in container units rather than percentages. A percentage height only
* resolves against a parent whose own height is already known, and every box
* between here and the tile is sized by its contents — so the chain never
* resolved and the slider grew until the tile scrolled. `cqh` is the tile's
* height whatever is in between.
*/
.dui-color {
display: grid;
height: 100cqh;
min-height: 0;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
justify-items: center;
gap: 0.75rem;
}
.dui-color > .dui-slider {
height: 100cqh;
}
/*
* The colour disc.
*
@@ -132,6 +155,7 @@
position: relative;
width: min(100cqh, 100cqw - 3.5rem);
max-width: 100%;
max-height: 100cqh;
aspect-ratio: 1;
border-radius: 50%;
touch-action: none;
@@ -177,7 +177,7 @@ export function ColorDisk({
})
const [hue, saturation, brightness] = hsv
return (
<div className="grid h-full min-h-0 grid-cols-[minmax(0,1fr)_auto] items-center justify-items-center gap-3">
<div className="dui-color">
<div
{...discProps}
data-testid={TESTID.disc}
@@ -171,7 +171,7 @@ export function ColorDisk({
})
const [hue, saturation, brightness] = hsv
return (
<div className="grid h-full min-h-0 grid-cols-[minmax(0,1fr)_auto] items-center justify-items-center gap-3">
<div className="dui-color">
<div
{...discProps}
data-testid={TESTID.disc}
+47
View File
@@ -306,6 +306,22 @@ test("rows are drawn in the order they were configured", async ({ page }) => {
await expect(rows.nth(2)).toContainText(/grid/i)
})
test("a widget is drawn inside its tile rather than scrolled", async ({
page,
}) => {
await openPanel(page)
// A picture that overflows its tile is a picture nobody can see the rest of.
// Only the widgets that are text or a list may scroll, and this panel holds
// none of them — so nothing here should.
const overflowing = await page.evaluate(() =>
[...document.querySelectorAll(".dui-frame-body")]
.filter((body) => body.scrollHeight > body.clientHeight + 1)
.map((body) => body.closest("[data-testid=widget-frame]")?.textContent),
)
expect(overflowing, "these widgets overflow their tile").toEqual([])
})
test("the icon follows what the message says", async ({ page }) => {
await openPanel(page)
const glyph = page.getByTestId("icon-glyph")
@@ -461,6 +477,37 @@ test("a chart's cursor follows the pointer", async ({ page }) => {
).toBeLessThan(3)
})
test("touch makes the controls bigger without changing what they do", async ({
page,
}) => {
await openPanel(page)
const control = page.getByTestId("widget-frame").filter({ hasText: "Mode" })
const pointer = (await control.getByRole("button").first().boundingBox())!
.height
await setLook(page, dashboardName, { touch: { value: true } })
await openPanel(page)
await expect(page.getByTestId("canvas-surface")).toHaveAttribute(
"data-touch",
"",
)
const touched = (await control.getByRole("button").first().boundingBox())!
.height
expect(
touched,
`a segment is ${touched.toFixed(1)} touched and ${pointer.toFixed(1)} pointed at`,
).toBeGreaterThan(pointer)
// The control is the same control: it still publishes what it always did.
await control.getByRole("button", { name: "Boost" }).click()
await expect(control.getByRole("button", { name: "Boost" })).toHaveAttribute(
"aria-pressed",
"true",
)
await setLook(page, dashboardName, { touch: { value: false } })
})
/**
* What a dashboard was told to wear, as a wall panel would be told.
*