import { CaretDownRegularIcon } from "alouette-icons/phosphor-icons/CaretDown"; import type { ReactNode } from "react"; import type { AccentOrNeutral } from "../../core/AlouetteConfig"; import { tv } from "../../core/tv"; import { Icon, type SVGIconElement } from "../primitives/Icon"; import { Text } from "../primitives/Text"; export interface SelectOption { label: string; value: string; disabled?: boolean; } /** * `outlined` is a form field, matching `InputText`. `tonal` is a pill lifted * off the ground it sits on, the material of a `tonal` button — for a select * that is chrome rather than a form field (the team or site an app sidebar * applies to). */ export type SelectVariant = "outlined" | "tonal"; export interface SelectProps { options: SelectOption[]; /** Controlled selected value. */ value?: string; /** Initial value for uncontrolled usage. */ defaultValue?: string; onValueChange?: (value: string) => void; placeholder?: string; /** Leading glyph standing for what is chosen (a place, a team). */ icon?: SVGIconElement; /** Defaults to `outlined`. */ variant?: SelectVariant; disabled?: boolean; /** * The field is in error: the trigger takes the danger accent over `accent` * and is `aria-invalid`. From `FormItem`'s render params, next to * `describedBy`. */ invalid?: boolean; accent?: AccentOrNeutral; "aria-label"?: string; "aria-labelledby"?: string; /** Web only in effect, like on `InputText`. */ "aria-describedby"?: string; "aria-required"?: boolean; testID?: string; } // The trigger, on both platforms. `outlined` is a field and focuses like // InputText, ringed as soon as it holds the focus. `tonal` is the material of a // tonal PressableBox: no focus ground, and the inset ring on keyboard focus // only, inside the raised edge. The ground lives in each // variant × disabled pair rather than in the variant, so the disabled one never // competes with the enabled one at equal specificity. // `flex` is for web, where the trigger is a plain element rather than a View. export const selectTriggerVariants = tv({ base: [ "flex flex-row items-center justify-between gap-xs px-m min-h-control-md", "transition-[border-color,outline-color,background-color,box-shadow] duration-fast ease-in", ].join(" "), variants: { variant: { outlined: "rounded-md border outline-interactive-outlined-pressable", tonal: "rounded-full", }, disabled: { true: "cursor-not-allowed", false: "cursor-pointer", }, }, compoundVariants: [ { variant: "outlined", disabled: true, class: "bg-disabled-interactive-muted border-interactive-outlined-disabled", }, { variant: "outlined", disabled: false, class: [ "bg-highlight", "border-interactive-outlined-pressable", "hover:border-interactive-outlined-hover", "focus:border-interactive-outlined-focus", "focus:outline-1 focus:outline-interactive-outlined-focus focus:outline-offset-0", "active:border-interactive-outlined-active", ].join(" "), }, { variant: "tonal", disabled: true, class: "bg-interactive-tonal-disabled", }, { variant: "tonal", disabled: false, class: [ "shadow-s bg-interactive-tonal-pressable", "hover:bg-interactive-tonal-hover", "active:shadow-lowered active:bg-interactive-tonal-active", "focus-visible:focus-ring-inset", ].join(" "), }, ], defaultVariants: { variant: "outlined", disabled: false }, }); // The label mirrors InputText on an outlined field: sharp value, // form-placeholder, form-disabled-text. On a tonal ground the value takes the // tonal ink, which carries the accent, and so does the leading icon. const triggerContentVariants = tv({ slots: { icon: "", label: "flex-1 text-base", caret: "", }, variants: { state: { value: { icon: "text-muted", label: "text-sharp", caret: "text-muted" }, placeholder: { icon: "text-muted", label: "text-form-placeholder", caret: "text-muted", }, disabled: { icon: "text-form-disabled-text", label: "text-form-disabled-text", caret: "text-form-disabled-text", }, }, variant: { outlined: {}, tonal: {} }, }, compoundVariants: [ { variant: "tonal", state: ["value", "placeholder"], class: { icon: "text-on-tonal" }, }, { variant: "tonal", state: "value", class: { label: "text-on-tonal" } }, ], defaultVariants: { state: "value", variant: "outlined" }, }); export interface SelectTriggerContentProps { label?: string; placeholder?: string; icon?: SVGIconElement; variant?: SelectVariant; disabled?: boolean; } export function SelectTriggerContent({ label, placeholder, icon, variant, disabled, }: SelectTriggerContentProps): ReactNode { const state = ((): "disabled" | "placeholder" | "value" => { // Placeholder keeps its own color even when disabled (mirrors InputText, // whose placeholderTextColor is independent of the disabled text color); // only an actual value switches to the darker disabled text color. if (label === undefined) return "placeholder"; if (disabled) return "disabled"; return "value"; })(); // A disabled placeholder keeps its label color, but its glyphs dim. const styles = triggerContentVariants({ state, variant }); const glyphStyles = triggerContentVariants({ state: disabled ? "disabled" : state, variant, }); return ( <> {icon ? ( ) : null} {label ?? placeholder ?? ""} } size={18} className={glyphStyles.caret()} /> ); }