- 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
75 lines
2.7 KiB
TypeScript
75 lines
2.7 KiB
TypeScript
// 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>
|
|
)
|
|
}
|