Files
siftlode/frontend/src/components/ui/SelectTrigger.tsx
T
peter 95b94a1d72 fix(ui): a longer label fits its control — pill value truncates, headers get their room (E30 S5)
Found in the Hungarian UAT: a toolbar SelectTrigger's text value wrapped out of the pill in a narrow
place (the playlists rail) — it now truncates in its own span; the Channels "Prioritás" and "Utolsó
feltöltés" headers were clipped by their fixed columns — sized for the label, funnel and sort arrow;
a sortable header is at least 24px wide (Hungarian "Ki" was a 12px target). PRE-EXISTING, any
language: the messages unlock button and "Forgot passphrase" sat flush — one gapped row.
2026-09-30 01:08:29 +02:00

72 lines
3.1 KiB
TypeScript

import { forwardRef } from "react";
import type { ButtonHTMLAttributes, ReactNode } from "react";
import clsx from "clsx";
import { ChevronDown } from "lucide-react";
import { FocusSafeButton } from "./FocusSafeButton";
// The dropdown trigger (docs/DESIGN.md A5, E28 S3c): the one button that opens a list of choices —
// `SortControl`, `FilterSelect`, the view switcher, the date filter. By context: a pill at the toolbar's
// 28 (in a toolbar the pill means "a filter or a choice"), or r8 at the form's 32, full width, lining up
// with the inputs around it. The form trigger is one of the field family, so it wears the field's frame
// (see-through, the ≥ 3:1 `edge-strong`) and does not stand out among the inputs; the toolbar pill keeps
// the pills' faint edge, its label naming it (owner decision F14). Both hover by the accent edge. Open is
// the accent edge and the chevron turned over. The popover wiring
// (ref, `getReferenceProps`, the accessible name carrying the value) is the caller's.
export const SelectTrigger = forwardRef<
HTMLButtonElement,
ButtonHTMLAttributes<HTMLButtonElement> & {
context: "toolbar" | "form";
open: boolean;
/** The muted "Sort:" prefix before the value. */
label?: ReactNode;
/** A leading glyph (the view's icon, a calendar). */
icon?: ReactNode;
/** Between the value and the chevron (the sort direction arrow). */
trailing?: ReactNode;
}
>(function SelectTrigger(
{ context, open, label, icon, trailing, className, children, ...rest },
ref,
) {
return (
<FocusSafeButton
ref={ref}
type="button"
className={clsx(
"glass-hover inline-flex max-w-full shrink-0 items-center gap-1.5 border text-left text-sm whitespace-nowrap transition touch-target",
context === "toolbar"
? "glass-card h-7 rounded-full px-3"
: "h-8 w-full rounded-lg bg-transparent px-2.5",
open ? "border-accent" : context === "toolbar" ? "border-border" : "edge-strong",
className,
)}
{...rest}
>
{icon}
{label != null && <span className="shrink-0 text-muted">{label}:</span>}
{/* The toolbar value is a row, whatever the caller puts in it: an <svg> is display:block under the
preflight, and in a plain span it broke the value onto its own line (the saved-view chip,
E28). The form value is text that truncates, which a flex box would not do — nor would a bare
text node in the toolbar row, so a text value gets its own truncating span: in a narrow place
(the playlists rail) a long value ("Rendezés: Egyéni sorrend") used to wrap out of the pill. */}
<span
className={clsx(
"text-fg",
context === "form" ? "flex-1 truncate" : "flex min-w-0 items-center gap-1.5",
)}
>
{context === "toolbar" && typeof children === "string" ? (
<span className="truncate">{children}</span>
) : (
children
)}
</span>
{trailing}
<ChevronDown
aria-hidden="true"
className={clsx("size-3.5 shrink-0 text-muted transition-transform", open && "rotate-180")}
/>
</FocusSafeButton>
);
});