Point the range picker at the shared segmented control

RangePicker carried a byte-for-byte copy of ui/segmented.tsx's markup; it
now renders that component instead.

The dashboard's own segmented widget stays where it is: it is not a copy
but a look-aware control, drawn per look in
Dashboard/ui/{fluksio,glass,material}/Controls.tsx over the shared
useSegmented behaviour. Note that in segmented.tsx in place of the stale
size-prop plan.
This commit is contained in:
2026-08-28 18:49:29 +02:00
parent d436597db9
commit 9d11e01a48
2 changed files with 22 additions and 57 deletions
+16 -52
View File
@@ -1,8 +1,4 @@
// The segmented shape's thumb transition lives beside the dashboard's own import { Segmented } from "@/components/ui/segmented"
// widgets, and CSS is chunked per entry — so the rule is pulled in wherever
// this picker is used, or the two copies of one shape would move differently.
import "@/components/Dashboard/dashboard.css"
import { cn } from "@/lib/utils"
/** /**
* A window of history, and everything a query needs to ask for it. * A window of history, and everything a query needs to ask for it.
@@ -39,12 +35,7 @@ export const DEFAULT_RANGE = RANGES[2]
export const rangeStart = (range: Range) => export const rangeStart = (range: Range) =>
new Date(Date.now() - range.hours * 3600_000).toISOString() new Date(Date.now() - range.hours * 3600_000).toISOString()
/** /** The window a screen is showing, as presets. */
* The window a screen is showing, as presets.
*
* The one segmented shape: a single border pill, transparent segments,
* bg-accent on the selected one (root DESIGN-GUIDELINES.md).
*/
export function RangePicker({ export function RangePicker({
value, value,
onChange, onChange,
@@ -52,47 +43,20 @@ export function RangePicker({
value: Range value: Range
onChange: (range: Range) => void onChange: (range: Range) => void
}) { }) {
const chosen = RANGES.findIndex((range) => range.hours === value.hours)
return ( return (
// A `fieldset` carries `min-inline-size: min-content` from the UA sheet, <Segmented
// which no width utility overrides. Equal tracks and no gap put the testId="range-picker"
// sliding thumb at its share of the padded box without measuring — a grid label="Time range"
// rather than a flex row because `flex-1` under `w-fit` sizes the segments value={String(value.hours)}
// to a share of the widest label instead of to the label itself. options={RANGES.map(
<fieldset (range) => [String(range.hours), range.label] as const,
data-testid="range-picker" )}
className="relative grid w-fit min-w-0 items-center rounded-full border border-border p-1" onChange={(hours) =>
style={{ onChange(
gridTemplateColumns: `repeat(${RANGES.length}, minmax(0, 1fr))`, RANGES.find((range) => String(range.hours) === hours) ??
}} DEFAULT_RANGE,
> )
<legend className="sr-only">Time range</legend> }
{chosen >= 0 ? ( />
<span
aria-hidden
className="widget-segment-thumb pointer-events-none absolute inset-y-1 rounded-full bg-accent"
style={{
left: `calc(0.25rem + ${chosen} * (100% - 0.5rem) / ${RANGES.length})`,
width: `calc((100% - 0.5rem) / ${RANGES.length})`,
}}
/>
) : null}
{RANGES.map((range, index) => (
<button
key={range.label}
type="button"
aria-pressed={index === chosen}
onClick={() => onChange(range)}
className={cn(
"relative z-10 min-w-0 rounded-full px-2.5 py-1 text-xs transition-colors",
index === chosen
? "text-accent-foreground"
: "text-muted-foreground hover:bg-accent/50",
)}
>
{range.label}
</button>
))}
</fieldset>
) )
} }
+6 -5
View File
@@ -1,6 +1,5 @@
// The thumb's transition lives beside the dashboard's own widgets, and CSS is // The thumb's transition lives beside the dashboard's own widgets, and CSS is
// chunked per entry — so the rule is pulled in wherever this control is used, // chunked per entry — so the rule is pulled in wherever this control is used.
// or two copies of one shape would move differently.
import "@/components/Dashboard/dashboard.css" import "@/components/Dashboard/dashboard.css"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@@ -9,9 +8,11 @@ import { cn } from "@/lib/utils"
* transparent segments, and `bg-accent` held by a thumb that slides rather than * transparent segments, and `bg-accent` held by a thumb that slides rather than
* a fill that jumps from cell to cell (root DESIGN-GUIDELINES.md). * a fill that jumps from cell to cell (root DESIGN-GUIDELINES.md).
* *
* ponytail: sized for editor chrome — `w-fit`, `text-xs`, a mouse-sized target. * App-side chrome only — `w-fit`, `text-xs`, a mouse-sized target. The
* The widget-side copy in `Dashboard/widgets.tsx` is a full-width pill with a * dashboard's own segmented widget is not this component and never will be:
* 44px touch target, so pointing that one here needs a size prop first. * it is drawn per look by `Dashboard/ui/{fluksio,glass,material}/Controls.tsx`
* over the shared `useSegmented` behaviour, as a full-width pill with a 44px
* touch target. Folding the two together would flatten the three looks.
*/ */
export function Segmented({ export function Segmented({
value, value,