Draw the colour disc inside its tile rather than past it
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:
+2
-7
@@ -100,13 +100,8 @@ Deferring because out of scope is fine, but don't mention deferring than.
|
|||||||
across all 16 columns. Another tile needs a rearrangement first — worth doing together
|
across all 16 columns. Another tile needs a rearrangement first — worth doing together
|
||||||
with re-authoring `PAGES` as a single section.
|
with re-authoring `PAGES` as a single section.
|
||||||
- CHORE/TEST: no Playwright coverage for the colour tile — `tests/widgets.spec.ts` covers the
|
- CHORE/TEST: no Playwright coverage for the colour tile — `tests/widgets.spec.ts` covers the
|
||||||
other controls. Wants a spec that drags the hue ring and asserts one publish on release.
|
other controls. Wants a spec that drags the disc and asserts one publish on release. The
|
||||||
- CHORE/UI: the colour wheel's arrow keys move a fixed 5°, with no PageUp/PageDown or
|
maths is checked (`color.check.ts`); what is not is the gesture.
|
||||||
Home/End. Fine for picking by eye; a panel that wants an exact hue has no coarse/fine step.
|
|
||||||
- CHORE/UI: the colour wheel reads the pointer's angle only, never its distance from the
|
|
||||||
centre (marked `ponytail:` in `ColorWidget.tsx`), so a colour takes two gestures — the ring,
|
|
||||||
then saturation. A saturation-by-radius disc would need a second axis on a control that can
|
|
||||||
announce one value.
|
|
||||||
- CHORE/DEMO: `seed_demo.py` still writes the Home dashboard as three titled sections
|
- CHORE/DEMO: `seed_demo.py` still writes the Home dashboard as three titled sections
|
||||||
("Right now", "Energy and comfort", "Yield model"). The editor and the panel now read a
|
("Right now", "Energy and comfort", "Yield model"). The editor and the panel now read a
|
||||||
page's sections as one grid, so those headings are no longer drawn and the first editor
|
page's sections as one grid, so those headings are no longer drawn and the first editor
|
||||||
|
|||||||
@@ -55,19 +55,17 @@ export function ColorWidget({ widget, dashboard }: WidgetProps) {
|
|||||||
// The pulse hangs off the frame, so it stays outside every box below.
|
// The pulse hangs off the frame, so it stays outside every box below.
|
||||||
<>
|
<>
|
||||||
{pulse}
|
{pulse}
|
||||||
<div className="min-h-0 flex-1">
|
<ColorDisk
|
||||||
<ColorDisk
|
name={widget.title || target}
|
||||||
name={widget.title || target}
|
hsv={current}
|
||||||
hsv={current}
|
disabled={locked}
|
||||||
disabled={locked}
|
onChange={setDraft}
|
||||||
onChange={setDraft}
|
onCommit={() => {
|
||||||
onCommit={() => {
|
if (draft === null) return
|
||||||
if (draft === null) return
|
send(encodeColor(draft, format))
|
||||||
send(encodeColor(draft, format))
|
setDraft(null)
|
||||||
setDraft(null)
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
* The colour disc.
|
||||||
*
|
*
|
||||||
@@ -132,6 +155,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: min(100cqh, 100cqw - 3.5rem);
|
width: min(100cqh, 100cqw - 3.5rem);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
max-height: 100cqh;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export function ColorDisk({
|
|||||||
})
|
})
|
||||||
const [hue, saturation, brightness] = hsv
|
const [hue, saturation, brightness] = hsv
|
||||||
return (
|
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
|
<div
|
||||||
{...discProps}
|
{...discProps}
|
||||||
data-testid={TESTID.disc}
|
data-testid={TESTID.disc}
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export function ColorDisk({
|
|||||||
})
|
})
|
||||||
const [hue, saturation, brightness] = hsv
|
const [hue, saturation, brightness] = hsv
|
||||||
return (
|
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
|
<div
|
||||||
{...discProps}
|
{...discProps}
|
||||||
data-testid={TESTID.disc}
|
data-testid={TESTID.disc}
|
||||||
|
|||||||
@@ -306,6 +306,22 @@ test("rows are drawn in the order they were configured", async ({ page }) => {
|
|||||||
await expect(rows.nth(2)).toContainText(/grid/i)
|
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 }) => {
|
test("the icon follows what the message says", async ({ page }) => {
|
||||||
await openPanel(page)
|
await openPanel(page)
|
||||||
const glyph = page.getByTestId("icon-glyph")
|
const glyph = page.getByTestId("icon-glyph")
|
||||||
@@ -461,6 +477,37 @@ test("a chart's cursor follows the pointer", async ({ page }) => {
|
|||||||
).toBeLessThan(3)
|
).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.
|
* What a dashboard was told to wear, as a wall panel would be told.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user