diff --git a/apps/eclipse/content/design-system/components/meta.json b/apps/eclipse/content/design-system/components/meta.json index 0e5e59c8b5..5d0d142b60 100644 --- a/apps/eclipse/content/design-system/components/meta.json +++ b/apps/eclipse/content/design-system/components/meta.json @@ -26,6 +26,7 @@ "pagination", "radio-group", "separator", + "select", "slider", "spinner", "statistic", diff --git a/apps/eclipse/content/design-system/components/select.mdx b/apps/eclipse/content/design-system/components/select.mdx new file mode 100644 index 0000000000..94b560fb57 --- /dev/null +++ b/apps/eclipse/content/design-system/components/select.mdx @@ -0,0 +1,714 @@ +--- +title: Select +description: A select component for choosing a value from a list of options. Features keyboard navigation, grouped options, custom positioning, and full accessibility support. +--- + +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectSeparator, + SelectTrigger, + SelectValue, + Field, + FieldLabel, + FieldDescription, + FieldError, +} from "@prisma/eclipse"; + +### Usage + +**Basic Select** + +A simple select with options: + +```tsx +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@prisma/eclipse"; + +export function BasicSelect() { + return ( + + ); +} +``` + +**Live Example:** + +
+

Live Example:

+ +
+ +**Grouped Options** + +Organize options into labeled groups: + +```tsx +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectSeparator, + SelectTrigger, + SelectValue, +} from "@prisma/eclipse"; + +export function GroupedSelect() { + return ( + + ); +} +``` + +**Live Example:** + +
+

Live Example:

+ +
+ +**Small Size** + +Use the `sm` size for compact layouts: + +```tsx +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@prisma/eclipse"; + +export function SmallSelect() { + return ( + + ); +} +``` + +**Live Example:** + +
+

Live Example:

+ +
+ +**Disabled State** + +Disable the select when interaction should be prevented: + +```tsx +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@prisma/eclipse"; + +export function DisabledSelect() { + return ( + + ); +} +``` + +**Live Example:** + +
+

Live Example:

+ +
+ +**With Field Component** + +Use with Field for proper form structure: + +```tsx +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Field, + FieldLabel, + FieldDescription, +} from "@prisma/eclipse"; + +export function SelectWithField() { + return ( + + Country + + Choose your country of residence. + + ); +} +``` + +**Live Example:** + +
+

Live Example:

+ + Country + + Choose your country of residence. + +
+ +**Position Control** + +Control the position and alignment of the dropdown: + +```tsx +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@prisma/eclipse"; + +export function PositionedSelect() { + return ( + + ); +} +``` + +**Live Example:** + +
+

Live Example:

+ +
+ +**Disabled Items** + +Disable individual items: + +```tsx +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@prisma/eclipse"; + +export function SelectWithDisabledItems() { + return ( + + ); +} +``` + +**Live Example:** + +
+

Live Example:

+ +
+ +### API Reference + +#### Select + +The root component that manages select state. + +**Props:** +- `value` - Controlled value (string, optional) +- `onValueChange` - Callback when value changes ((value: string) => void, optional) +- `open` - Controlled open state (boolean, optional) +- `onOpenChange` - Callback when open state changes ((open: boolean) => void, optional) +- `disabled` - Disable the select (boolean, default: false) +- `defaultValue` - Default value for uncontrolled mode (string, optional) +- `defaultOpen` - Default open state (boolean, default: false) +- `name` - Form input name (string, optional) +- `required` - Mark as required (boolean, default: false) + +```tsx + +``` + +#### SelectTrigger + +The button that triggers the select dropdown. + +**Props:** +- `size` - Size variant ("sm" | "default", default: "default") +- `className` - Additional CSS classes (string or function, optional) +- `disabled` - Disable the trigger (boolean, default: false) +- All standard HTML button attributes + +```tsx + + + +``` + +#### SelectValue + +Displays the currently selected value or placeholder. + +**Props:** +- `placeholder` - Text to show when no value is selected (string, optional) +- `className` - Additional CSS classes (string or function, optional) + +```tsx + +``` + +#### SelectContent + +The dropdown container that holds the list of options. + +**Props:** +- `side` - Position relative to trigger ("top" | "bottom" | "left" | "right", default: "bottom") +- `align` - Alignment relative to trigger ("start" | "center" | "end", default: "center") +- `sideOffset` - Distance from trigger in pixels (number, default: 4) +- `alignOffset` - Alignment offset in pixels (number, default: 0) +- `alignItemWithTrigger` - Align selected item with trigger (boolean, default: true) +- `className` - Additional CSS classes (string or function, optional) + +```tsx + + {/* SelectItem components */} + +``` + +#### SelectItem + +An individual option in the select list. + +**Props:** +- `value` - The value of the item (string, required) +- `disabled` - Disable the item (boolean, default: false) +- `className` - Additional CSS classes (string or function, optional) +- `children` - Item content (ReactNode) + +```tsx +Option 1 +Disabled Option +``` + +#### SelectGroup + +A container for grouping related items. + +**Props:** +- `className` - Additional CSS classes (string or function, optional) +- `children` - Group content (ReactNode) + +```tsx + + Category + Item 1 + +``` + +#### SelectLabel + +A label for a group of items. + +**Props:** +- `className` - Additional CSS classes (string or function, optional) +- `children` - Label content (ReactNode) + +```tsx +Frontend Languages +``` + +#### SelectSeparator + +A visual separator between groups or items. + +**Props:** +- `className` - Additional CSS classes (string or function, optional) + +```tsx + +``` + +#### SelectScrollUpButton + +Internal component for scrolling up in long lists. Automatically rendered. + +#### SelectScrollDownButton + +Internal component for scrolling down in long lists. Automatically rendered. + +### Features + +- ✅ Two size variants (default and small) +- ✅ Keyboard navigation (Arrow keys, Enter, Escape, Tab, Home, End) +- ✅ Grouped options with labels +- ✅ Custom positioning and alignment +- ✅ Disabled state for select and individual items +- ✅ Controlled and uncontrolled modes +- ✅ Scroll buttons for long lists +- ✅ Item indicator with checkmark +- ✅ Accessible with ARIA attributes +- ✅ Portal rendering for z-index management +- ✅ Animations for open/close transitions +- ✅ Auto-alignment of selected item with trigger +- ✅ Form integration with name and required attributes +- ✅ Fully typed with TypeScript + +### Best Practices + +- Always provide a placeholder for better UX +- Use SelectGroup and SelectLabel for organized lists +- Consider using the small size in dense layouts +- Disable items that are temporarily unavailable rather than hiding them +- Keep option text concise and scannable +- Use Field component for proper form structure with labels +- Test keyboard navigation thoroughly +- Avoid extremely long lists (consider search/filter for 50+ items) +- Use consistent option formatting +- Provide clear value descriptions + +### Accessibility + +The Select component follows ARIA combobox pattern specifications: + +- Uses semantic ARIA roles (`combobox`, `listbox`, `option`) +- Supports full keyboard navigation: + - `Space/Enter` - Open/close and select + - `Arrow Down/Up` - Navigate through options + - `Home/End` - Jump to first/last option + - `Escape` - Close the dropdown + - `Tab` - Move focus and close dropdown +- Focus management and trapped focus in dropdown +- Screen reader announcements for selections and state changes +- Proper labeling with `aria-label` and `aria-labelledby` +- Visual focus indicators +- Selected state announced to assistive technologies +- Disabled items are properly marked and unfocusable +- Required attribute for form validation + +### Common Use Cases + +The Select component is perfect for: + +- **Form inputs** - Collecting user choices in forms +- **Settings** - Configuration and preference selection +- **Filters** - Filtering data by category +- **Language/locale switchers** - Selecting language or region +- **Status updates** - Changing item status +- **Priority selection** - Setting task or issue priority +- **Category selection** - Choosing from predefined categories +- **Time zones** - Selecting time zone +- **Country/region pickers** - Location selection +- **Sorting options** - Choosing sort order +- **Role assignment** - Selecting user roles + +### Styling + +The Select component uses design tokens and can be customized: + +- **Trigger**: Border, rounded corners, padding, hover/focus states +- **Content**: Popover with shadow, rounded corners, animations +- **Items**: Hover/focus states with accent colors +- **Selected indicator**: Checkmark icon aligned right +- **Groups**: Scroll margin and padding +- **Labels**: Muted text, small font size +- **Separator**: Border line between groups +- **Scroll buttons**: Positioned at top/bottom with icons + +Customize by passing `className` props: + +```tsx + +``` + +### Integration with Forms + +Use with form libraries like React Hook Form: + +```tsx +import { useForm, Controller } from "react-hook-form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@prisma/eclipse"; + +export function SelectForm() { + const { control, handleSubmit } = useForm(); + + return ( +
console.log(data))}> + ( + + )} + /> + + + ); +} +``` + +### Controlled vs Uncontrolled + +**Uncontrolled (with defaultValue):** +```tsx + +``` + +**Controlled (with value and onValueChange):** +```tsx +const [value, setValue] = useState("option1"); + + +``` + +### Performance Tips + +For large option lists: + +1. **Virtualization** - Consider using virtual scrolling for 100+ items +2. **Lazy loading** - Load options on-demand +3. **Memoization** - Use React.memo for SelectItem components +4. **Search/filter** - Add search for easier navigation +5. **Pagination** - Load options in batches + +```tsx +import { useMemo } from "react"; + +export function OptimizedSelect({ items }) { + const memoizedItems = useMemo( + () => items.map((item) => ( + + {item.name} + + )), + [items] + ); + + return ( + + ); +} +``` diff --git a/packages/eclipse/package.json b/packages/eclipse/package.json index fdeef469cd..3ca42b7ae5 100644 --- a/packages/eclipse/package.json +++ b/packages/eclipse/package.json @@ -38,7 +38,7 @@ "scripts": { "build": "rimraf dist && pnpm run build:ts && pnpm run build:styles", "build:ts": "tsdown --config tsdown.config.ts", - "build:styles": "mkdir -p dist/styles dist/static/fonts && cp ./src/styles/globals.css ./dist/styles/globals.css && cp ./src/styles/fonts.css ./dist/styles/fonts.css && cp ./src/styles/steps.css ./dist/styles/steps.css && cp ./src/static/fonts/* ./dist/static/fonts/", + "build:styles": "mkdir -p dist/styles dist/static/fonts && cp ./src/styles/globals.css ./dist/styles/globals.css && cp ./src/styles/fonts.css ./dist/styles/fonts.css && cp ./src/styles/steps.css ./dist/styles/steps.css && cp -r ./src/static/fonts/. ./dist/static/fonts/", "check": "oxfmt . --write && oxlint . --fix", "types:check": "tsc --noEmit", "prepublishOnly": "pnpm run build && pnpm run types:check" diff --git a/packages/eclipse/src/components/index.ts b/packages/eclipse/src/components/index.ts index c882be9f09..fa034f917b 100644 --- a/packages/eclipse/src/components/index.ts +++ b/packages/eclipse/src/components/index.ts @@ -157,6 +157,19 @@ export { export { Alert } from "./alert"; export { Switch } from "./switch"; +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} from "./select"; + export { Empty, EmptyHeader, diff --git a/packages/eclipse/src/components/select.tsx b/packages/eclipse/src/components/select.tsx new file mode 100644 index 0000000000..8c2da48d57 --- /dev/null +++ b/packages/eclipse/src/components/select.tsx @@ -0,0 +1,214 @@ +"use client"; + +import * as React from "react"; +import { Select as SelectPrimitive } from "@base-ui/react/select"; + +import { cn } from "../lib/cn"; + +// Helper to handle className that can be a string or function +function handleClassName( + className: string | ((state: T) => string | undefined) | undefined, + staticClasses: string, +): string | ((state: T) => string | undefined) { + if (typeof className === "function") { + return (state: T) => cn(staticClasses, className(state)); + } + return cn(staticClasses, className); +} + +const Select = SelectPrimitive.Root; + +function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { + return ( + + ); +} + +function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { + return ( + + ); +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: SelectPrimitive.Trigger.Props & { + size?: "sm" | "default"; +}) { + return ( + + {children} + + + ); +} + +function SelectContent({ + className, + children, + side = "bottom", + sideOffset = 4, + align = "start", + alignOffset = 0, + alignItemWithTrigger = false, + ...props +}: SelectPrimitive.Popup.Props & + Pick< + SelectPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" + >) { + return ( + + + + + {children} + + + + + ); +} + +function SelectLabel({ + className, + ...props +}: SelectPrimitive.GroupLabel.Props) { + return ( + + ); +} + +function SelectItem({ + className, + children, + ...props +}: SelectPrimitive.Item.Props) { + return ( + + + {children} + + + + + } + /> + + ); +} + +function SelectSeparator({ + className, + ...props +}: SelectPrimitive.Separator.Props) { + return ( + + ); +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +};