Dashboard chrome: an icon picker, one segmented shape, a rail without bars
- IconPicker replaces the three icon selects (rail icon, icon-widget rule,
"Otherwise"): the glyphs in a grid, and a button that clears back to none —
which a Radix SelectItem could never offer.
- ModePicker/StylePicker drop out in favour of a shared ui/Segmented, the same
sliding-thumb shape RangePicker and the widget-side control already wear.
- PanelRail draws no scrollbars at all: hiding them also takes back the gutter
a vertical bar claimed from a column exactly as wide as its buttons, which is
what pushed a horizontal bar under them.
- The panels dialog can re-pair one screen (POST /panels/{id}/unpair) without
deleting the panel it hangs on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tULRZJUkZsw7rMJ3h4xvu
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
// 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,
|
||||
// or two copies of one shape would move differently.
|
||||
import "@/components/Dashboard/dashboard.css"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
/**
|
||||
* One of N, as the one segmented shape: a single border pill, no dividers,
|
||||
* 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).
|
||||
*
|
||||
* ponytail: sized for editor chrome — `w-fit`, `text-xs`, a mouse-sized target.
|
||||
* The widget-side copy in `Dashboard/widgets.tsx` is a full-width pill with a
|
||||
* 44px touch target, so pointing that one here needs a size prop first.
|
||||
*/
|
||||
export function Segmented({
|
||||
value,
|
||||
options,
|
||||
label,
|
||||
testId,
|
||||
onChange,
|
||||
}: {
|
||||
value: string
|
||||
/** `[value, label]` pairs, in the order they are drawn. */
|
||||
options: readonly (readonly [string, string])[]
|
||||
/** Names the group for screen readers. */
|
||||
label: string
|
||||
testId?: string
|
||||
onChange: (value: string) => void
|
||||
}) {
|
||||
const chosen = options.findIndex(([option]) => option === value)
|
||||
return (
|
||||
// A `fieldset` carries `min-inline-size: min-content` from the UA sheet,
|
||||
// which no width utility overrides. Equal tracks and no gap put the thumb
|
||||
// at its share of the padded box without measuring — a grid rather than a
|
||||
// flex row because `flex-1` under `w-fit` sizes the segments to a share of
|
||||
// the widest label instead of to the label itself.
|
||||
<fieldset
|
||||
data-testid={testId}
|
||||
className="relative grid w-fit min-w-0 items-center rounded-full border border-border p-1"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${options.length}, minmax(0, 1fr))`,
|
||||
}}
|
||||
>
|
||||
<legend className="sr-only">{label}</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) / ${options.length})`,
|
||||
width: `calc((100% - 0.5rem) / ${options.length})`,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{options.map(([option, optionLabel], index) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
aria-pressed={index === chosen}
|
||||
onClick={() => onChange(option)}
|
||||
className={cn(
|
||||
"relative z-10 min-w-0 truncate rounded-full px-2.5 py-1 text-xs transition-colors",
|
||||
index === chosen
|
||||
? "text-accent-foreground"
|
||||
: "text-muted-foreground hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
{optionLabel}
|
||||
</button>
|
||||
))}
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user