Files
stack/packages/ui/src/components/Select.tsx
T
2026-02-22 21:12:13 +00:00

102 lines
2.6 KiB
TypeScript

import { useState } from "react";
import type { SelectHTMLAttributes, ReactElement } from "react";
export interface SelectOption {
value: string;
label: string;
disabled?: boolean;
}
export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "size"> {
label?: string;
error?: string;
helperText?: string;
fullWidth?: boolean;
options: SelectOption[];
placeholder?: string;
}
export function Select({
label,
error,
helperText,
fullWidth = false,
options,
placeholder = "Select an option...",
className = "",
id,
style,
onFocus,
onBlur,
...props
}: SelectProps): ReactElement {
const [isFocused, setIsFocused] = useState(false);
const selectId = id ?? `select-${Math.random().toString(36).substring(2, 11)}`;
const errorId = error ? `${selectId}-error` : undefined;
const helperId = helperText ? `${selectId}-helper` : undefined;
const selectStyle: React.CSSProperties = {
background: "var(--bg-mid)",
border: error
? `1px solid var(--danger)`
: isFocused
? `1px solid var(--primary)`
: `1px solid var(--border)`,
color: "var(--text)",
outline: "none",
boxShadow: isFocused ? `0 0 0 2px rgba(47,128,255,0.2)` : "none",
...style,
};
const widthClass = fullWidth ? "w-full" : "";
return (
<div className={fullWidth ? "w-full" : ""}>
{label && (
<label
htmlFor={selectId}
className="block text-sm font-medium mb-1"
style={{ color: "var(--text-2)" }}
>
{label}
</label>
)}
<select
id={selectId}
className={`px-3 py-2 rounded-md transition-colors ${widthClass} ${className}`}
style={selectStyle}
aria-invalid={error ? "true" : "false"}
aria-describedby={[errorId, helperId].filter(Boolean).join(" ") || undefined}
onFocus={(e) => {
setIsFocused(true);
onFocus?.(e);
}}
onBlur={(e) => {
setIsFocused(false);
onBlur?.(e);
}}
{...props}
>
<option value="" disabled>
{placeholder}
</option>
{options.map((option) => (
<option key={option.value} value={option.value} disabled={option.disabled}>
{option.label}
</option>
))}
</select>
{error && (
<p id={errorId} className="mt-1 text-sm" style={{ color: "var(--danger)" }} role="alert">
{error}
</p>
)}
{helperText && !error && (
<p id={helperId} className="mt-1 text-sm" style={{ color: "var(--muted)" }}>
{helperText}
</p>
)}
</div>
);
}