@rynx-ai/ui 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +37 -0
  2. package/dist/components/alert-dialog.d.ts +16 -0
  3. package/dist/components/alert-dialog.js +34 -0
  4. package/dist/components/button.d.ts +11 -0
  5. package/dist/components/button.js +34 -0
  6. package/dist/components/dialog.d.ts +16 -0
  7. package/dist/components/dialog.js +30 -0
  8. package/dist/components/dropdown-menu.d.ts +8 -0
  9. package/dist/components/dropdown-menu.js +14 -0
  10. package/dist/components/input.d.ts +2 -0
  11. package/dist/components/input.js +5 -0
  12. package/dist/components/interaction-question.d.ts +42 -0
  13. package/dist/components/interaction-question.js +79 -0
  14. package/dist/components/native-select.d.ts +7 -0
  15. package/dist/components/native-select.js +12 -0
  16. package/dist/components/scroll-area.d.ts +9 -0
  17. package/dist/components/scroll-area.js +11 -0
  18. package/dist/components/select.d.ts +14 -0
  19. package/dist/components/select.js +28 -0
  20. package/dist/components/tabs.d.ts +6 -0
  21. package/dist/components/tabs.js +13 -0
  22. package/dist/components/textarea.d.ts +2 -0
  23. package/dist/components/textarea.js +5 -0
  24. package/dist/index.d.ts +12 -0
  25. package/dist/index.js +12 -0
  26. package/dist/lib/utils.d.ts +2 -0
  27. package/dist/lib/utils.js +5 -0
  28. package/package.json +68 -0
  29. package/src/components/alert-dialog.tsx +162 -0
  30. package/src/components/button.tsx +63 -0
  31. package/src/components/dialog.tsx +137 -0
  32. package/src/components/dropdown-menu.tsx +57 -0
  33. package/src/components/input.tsx +19 -0
  34. package/src/components/interaction-question.tsx +364 -0
  35. package/src/components/native-select.tsx +57 -0
  36. package/src/components/scroll-area.tsx +84 -0
  37. package/src/components/select.tsx +191 -0
  38. package/src/components/tabs.tsx +53 -0
  39. package/src/components/textarea.tsx +16 -0
  40. package/src/index.ts +12 -0
  41. package/src/lib/utils.ts +6 -0
  42. package/styles/tailwind.css +31 -0
  43. package/styles/tokens.css +55 -0
  44. package/styles/utilities.css +55 -0
  45. package/styles.css +4 -0
@@ -0,0 +1,364 @@
1
+ import { Check, ChevronLeft, ChevronRight } from "lucide-react";
2
+ import * as React from "react";
3
+
4
+ import { Button } from "./button.js";
5
+ import { Input } from "./input.js";
6
+ import { Textarea } from "./textarea.js";
7
+ import { cn } from "../lib/utils.js";
8
+
9
+ export interface InteractionQuestionOption {
10
+ value: string;
11
+ label: string;
12
+ description?: string;
13
+ }
14
+
15
+ export type InteractionQuestionField =
16
+ | {
17
+ id: string;
18
+ type: "text";
19
+ label: string;
20
+ description?: string;
21
+ required?: boolean;
22
+ secret?: boolean;
23
+ multiline?: boolean;
24
+ placeholder?: string;
25
+ }
26
+ | {
27
+ id: string;
28
+ type: "select";
29
+ label: string;
30
+ description?: string;
31
+ required?: boolean;
32
+ multiple?: boolean;
33
+ allowOther?: boolean;
34
+ options: InteractionQuestionOption[];
35
+ };
36
+
37
+ export type InteractionQuestionAnswer = string | string[];
38
+
39
+ export interface InteractionQuestionProps {
40
+ idPrefix: string;
41
+ fields: readonly InteractionQuestionField[];
42
+ activeFieldId?: string;
43
+ answers: Record<string, InteractionQuestionAnswer>;
44
+ otherValues: Record<string, string>;
45
+ otherSelections: Record<string, boolean>;
46
+ errors?: Record<string, string | undefined>;
47
+ disabled?: boolean;
48
+ onNavigate: (fieldId: string) => void;
49
+ onAnswerChange: (fieldId: string, answer: InteractionQuestionAnswer) => void;
50
+ onOtherValueChange: (fieldId: string, value: string) => void;
51
+ onOtherSelectionChange: (fieldId: string, selected: boolean) => void;
52
+ }
53
+
54
+ /** Shared one-question-at-a-time editor used by every Rynx web surface. */
55
+ export function InteractionQuestion({
56
+ idPrefix,
57
+ fields,
58
+ activeFieldId,
59
+ answers,
60
+ otherValues,
61
+ otherSelections,
62
+ errors = {},
63
+ disabled = false,
64
+ onNavigate,
65
+ onAnswerChange,
66
+ onOtherValueChange,
67
+ onOtherSelectionChange,
68
+ }: InteractionQuestionProps) {
69
+ const questionRef = React.useRef<HTMLDivElement>(null);
70
+ const previousFieldIdRef = React.useRef<string | undefined>(undefined);
71
+ const previousErrorRef = React.useRef<string | undefined>(undefined);
72
+ const requestedIndex = fields.findIndex((candidate) => candidate.id === activeFieldId);
73
+ const index = requestedIndex >= 0 ? requestedIndex : 0;
74
+ const field = fields[index];
75
+ const error = field ? errors[field.id] : undefined;
76
+ const currentAnswer = field ? answers[field.id] : undefined;
77
+
78
+ React.useEffect(() => {
79
+ if (!field) return;
80
+ const fieldChanged = previousFieldIdRef.current !== undefined
81
+ && previousFieldIdRef.current !== field.id;
82
+ const errorAppeared = Boolean(error) && previousErrorRef.current !== error;
83
+ previousFieldIdRef.current = field.id;
84
+ previousErrorRef.current = error;
85
+ if (!fieldChanged && !errorAppeared) return;
86
+ const container = questionRef.current;
87
+ const focusTarget = container?.querySelector<HTMLElement>(
88
+ 'input[aria-invalid="true"]:not([disabled]), textarea[aria-invalid="true"]:not([disabled])',
89
+ ) ?? container?.querySelector<HTMLElement>("input:checked:not([disabled])")
90
+ ?? container?.querySelector<HTMLElement>("input:not([disabled]), textarea:not([disabled])");
91
+ focusTarget?.focus();
92
+ }, [error, field]);
93
+
94
+ if (!field) return null;
95
+
96
+ const domId = `${idPrefix}-field-${index}`;
97
+ const descriptionId = field.description ? `${domId}-description` : undefined;
98
+ const errorId = error ? `${domId}-error` : undefined;
99
+ const describedBy = [descriptionId, errorId].filter(Boolean).join(" ") || undefined;
100
+
101
+ return (
102
+ <div className="grid gap-3">
103
+ {fields.length > 1 && (
104
+ <div className="grid gap-1.5">
105
+ <div className="flex items-center justify-between gap-3">
106
+ <div aria-live="polite" className="font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
107
+ Question {index + 1} of {fields.length}
108
+ </div>
109
+ <div className="text-[11px] text-muted-foreground">
110
+ {field.required ? "Required" : "Optional"}
111
+ </div>
112
+ </div>
113
+ <div
114
+ role="progressbar"
115
+ aria-label="Question progress"
116
+ aria-valuemin={1}
117
+ aria-valuemax={fields.length}
118
+ aria-valuenow={index + 1}
119
+ className="h-1 overflow-hidden rounded-full bg-muted"
120
+ >
121
+ <div
122
+ className="h-full rounded-full bg-primary transition-[width]"
123
+ style={{ width: `${((index + 1) / fields.length) * 100}%` }}
124
+ />
125
+ </div>
126
+ </div>
127
+ )}
128
+
129
+ <div ref={questionRef}>
130
+ {field.type === "text" ? (
131
+ <div className="grid gap-1.5">
132
+ <label htmlFor={domId} className="text-xs font-medium text-foreground">
133
+ {field.label}
134
+ {field.required && <RequiredIndicator />}
135
+ </label>
136
+ {field.description && (
137
+ <p id={descriptionId} className="text-[11px] leading-relaxed text-muted-foreground">
138
+ {field.description}
139
+ </p>
140
+ )}
141
+ {field.multiline && !field.secret ? (
142
+ <Textarea
143
+ id={domId}
144
+ value={typeof currentAnswer === "string" ? currentAnswer : ""}
145
+ disabled={disabled}
146
+ placeholder={field.placeholder}
147
+ aria-invalid={error ? true : undefined}
148
+ aria-describedby={describedBy}
149
+ required={field.required}
150
+ onChange={(event) => onAnswerChange(field.id, event.target.value)}
151
+ />
152
+ ) : (
153
+ <Input
154
+ id={domId}
155
+ type={field.secret ? "password" : "text"}
156
+ autoComplete="off"
157
+ value={typeof currentAnswer === "string" ? currentAnswer : ""}
158
+ disabled={disabled}
159
+ placeholder={field.placeholder}
160
+ aria-invalid={error ? true : undefined}
161
+ aria-describedby={describedBy}
162
+ required={field.required}
163
+ onChange={(event) => onAnswerChange(field.id, event.target.value)}
164
+ />
165
+ )}
166
+ <FieldError id={errorId} error={error} />
167
+ </div>
168
+ ) : (
169
+ <fieldset
170
+ disabled={disabled}
171
+ role={field.multiple ? undefined : "radiogroup"}
172
+ aria-required={!field.multiple && field.required ? true : undefined}
173
+ aria-invalid={error ? true : undefined}
174
+ aria-describedby={describedBy}
175
+ className="grid gap-1.5"
176
+ >
177
+ <legend className="text-xs font-medium text-foreground">
178
+ {field.label}
179
+ {field.required && <RequiredIndicator />}
180
+ </legend>
181
+ {field.description && (
182
+ <p id={descriptionId} className="text-[11px] leading-relaxed text-muted-foreground">
183
+ {field.description}
184
+ </p>
185
+ )}
186
+ <div className="grid gap-1.5">
187
+ {field.options.map((option, optionIndex) => {
188
+ const optionId = `${domId}-option-${optionIndex}`;
189
+ const checked = field.multiple
190
+ ? Array.isArray(currentAnswer) && currentAnswer.includes(option.value)
191
+ : otherSelections[field.id] !== true && currentAnswer === option.value;
192
+ return (
193
+ <ChoiceRow
194
+ key={option.value}
195
+ id={optionId}
196
+ name={field.multiple ? undefined : domId}
197
+ type={field.multiple ? "checkbox" : "radio"}
198
+ checked={checked}
199
+ label={option.label}
200
+ description={option.description}
201
+ onChange={(selected) => {
202
+ if (field.multiple) {
203
+ const values = Array.isArray(currentAnswer) ? currentAnswer : [];
204
+ onAnswerChange(
205
+ field.id,
206
+ selected
207
+ ? [...values.filter((value) => value !== option.value), option.value]
208
+ : values.filter((value) => value !== option.value),
209
+ );
210
+ } else {
211
+ onAnswerChange(field.id, option.value);
212
+ onOtherSelectionChange(field.id, false);
213
+ }
214
+ }}
215
+ />
216
+ );
217
+ })}
218
+ {field.allowOther && (
219
+ <div className="grid gap-1.5 rounded-lg border border-border bg-background/60 px-2.5 py-2 has-[:checked]:border-primary/50 has-[:checked]:bg-primary/5">
220
+ <ChoiceRow
221
+ id={`${domId}-other`}
222
+ name={field.multiple ? undefined : domId}
223
+ type={field.multiple ? "checkbox" : "radio"}
224
+ checked={otherSelections[field.id] === true}
225
+ label="Other"
226
+ embedded
227
+ onChange={(selected) => {
228
+ onOtherSelectionChange(field.id, selected);
229
+ if (selected && !field.multiple) onAnswerChange(field.id, "");
230
+ }}
231
+ />
232
+ <Input
233
+ value={otherValues[field.id] ?? ""}
234
+ disabled={disabled || otherSelections[field.id] !== true}
235
+ aria-label={`Other answer for ${field.label}`}
236
+ aria-invalid={error ? true : undefined}
237
+ aria-describedby={describedBy}
238
+ placeholder="Enter another answer"
239
+ onChange={(event) => onOtherValueChange(field.id, event.target.value)}
240
+ />
241
+ </div>
242
+ )}
243
+ {!field.required
244
+ && !field.multiple
245
+ && (otherSelections[field.id] === true
246
+ || (typeof currentAnswer === "string" && currentAnswer.length > 0)) && (
247
+ <Button
248
+ type="button"
249
+ size="xs"
250
+ variant="ghost"
251
+ className="justify-self-start text-muted-foreground"
252
+ onClick={() => {
253
+ onAnswerChange(field.id, "");
254
+ onOtherSelectionChange(field.id, false);
255
+ }}
256
+ >
257
+ Clear selection
258
+ </Button>
259
+ )}
260
+ </div>
261
+ <FieldError id={errorId} error={error} />
262
+ </fieldset>
263
+ )}
264
+ </div>
265
+
266
+ {fields.length > 1 && (
267
+ <div className="flex items-center justify-between gap-2 border-t border-border/70 pt-2">
268
+ <Button
269
+ type="button"
270
+ size="xs"
271
+ variant="ghost"
272
+ disabled={disabled || index === 0}
273
+ onClick={() => onNavigate(fields[index - 1]!.id)}
274
+ >
275
+ <ChevronLeft />
276
+ Previous
277
+ </Button>
278
+ <Button
279
+ type="button"
280
+ size="xs"
281
+ variant="outline"
282
+ disabled={disabled || index === fields.length - 1}
283
+ onClick={() => onNavigate(fields[index + 1]!.id)}
284
+ >
285
+ Next
286
+ <ChevronRight />
287
+ </Button>
288
+ </div>
289
+ )}
290
+ </div>
291
+ );
292
+ }
293
+
294
+ function ChoiceRow({
295
+ id,
296
+ name,
297
+ type,
298
+ checked,
299
+ label,
300
+ description,
301
+ embedded = false,
302
+ onChange,
303
+ }: {
304
+ id: string;
305
+ name?: string;
306
+ type: "checkbox" | "radio";
307
+ checked: boolean;
308
+ label: string;
309
+ description?: string;
310
+ embedded?: boolean;
311
+ onChange: (checked: boolean) => void;
312
+ }) {
313
+ return (
314
+ <label
315
+ htmlFor={id}
316
+ className={cn(
317
+ "flex cursor-pointer items-start gap-2 text-xs text-foreground",
318
+ !embedded
319
+ && "rounded-lg border border-border bg-background/60 px-2.5 py-2 transition-colors hover:bg-muted/60 has-[:checked]:border-primary/50 has-[:checked]:bg-primary/5",
320
+ )}
321
+ >
322
+ <span className="relative mt-0.5 size-4 shrink-0">
323
+ <input
324
+ id={id}
325
+ type={type}
326
+ name={name}
327
+ checked={checked}
328
+ className="peer size-4 appearance-none rounded-sm border border-input bg-background outline-none transition-colors checked:border-primary checked:bg-primary focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-60"
329
+ onChange={(event) => onChange(event.target.checked)}
330
+ />
331
+ <Check
332
+ aria-hidden="true"
333
+ className="pointer-events-none absolute inset-0 size-4 stroke-[3] text-primary-foreground opacity-0 peer-checked:opacity-100"
334
+ />
335
+ </span>
336
+ <span className="min-w-0">
337
+ <span className="block">{label}</span>
338
+ {description && (
339
+ <span className="mt-0.5 block text-[11px] leading-relaxed text-muted-foreground">
340
+ {description}
341
+ </span>
342
+ )}
343
+ </span>
344
+ </label>
345
+ );
346
+ }
347
+
348
+ function FieldError({ id, error }: { id?: string; error?: string }) {
349
+ if (!error) return null;
350
+ return (
351
+ <p id={id} role="alert" className="text-[11px] text-destructive">
352
+ {error}
353
+ </p>
354
+ );
355
+ }
356
+
357
+ function RequiredIndicator() {
358
+ return (
359
+ <>
360
+ <span aria-hidden="true" className="ml-0.5 text-destructive">*</span>
361
+ <span className="sr-only"> (required)</span>
362
+ </>
363
+ );
364
+ }
@@ -0,0 +1,57 @@
1
+ import { ChevronDown } from "lucide-react";
2
+ import * as React from "react";
3
+
4
+ import { cn } from "../lib/utils.js";
5
+
6
+ export type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & {
7
+ size?: "sm" | "default";
8
+ };
9
+
10
+ export function NativeSelect({
11
+ className,
12
+ size = "default",
13
+ ...props
14
+ }: NativeSelectProps) {
15
+ return (
16
+ <div
17
+ data-slot="native-select-wrapper"
18
+ data-size={size}
19
+ className={cn(
20
+ "group/native-select relative w-fit has-[select:disabled]:opacity-50",
21
+ className,
22
+ )}
23
+ >
24
+ <select
25
+ data-slot="native-select"
26
+ data-size={size}
27
+ className="h-8 w-full min-w-0 appearance-none rounded-lg border border-input bg-transparent py-1 pr-8 pl-2.5 text-sm transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40"
28
+ {...props}
29
+ />
30
+ <ChevronDown
31
+ aria-hidden="true"
32
+ data-slot="native-select-icon"
33
+ className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none"
34
+ />
35
+ </div>
36
+ );
37
+ }
38
+
39
+ export function NativeSelectOption({ className, ...props }: React.ComponentProps<"option">) {
40
+ return (
41
+ <option
42
+ data-slot="native-select-option"
43
+ className={cn("bg-[Canvas] text-[CanvasText]", className)}
44
+ {...props}
45
+ />
46
+ );
47
+ }
48
+
49
+ export function NativeSelectOptGroup({ className, ...props }: React.ComponentProps<"optgroup">) {
50
+ return (
51
+ <optgroup
52
+ data-slot="native-select-optgroup"
53
+ className={cn("bg-[Canvas] text-[CanvasText]", className)}
54
+ {...props}
55
+ />
56
+ );
57
+ }
@@ -0,0 +1,84 @@
1
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
2
+ import * as React from "react";
3
+
4
+ import { cn } from "../lib/utils.js";
5
+
6
+ type ScrollAreaProps = React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
7
+ viewportClassName?: string;
8
+ viewportRef?: React.Ref<React.ElementRef<typeof ScrollAreaPrimitive.Viewport>>;
9
+ viewportProps?: Omit<
10
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport>,
11
+ "children" | "className"
12
+ >;
13
+ scrollbars?: "vertical" | "horizontal" | "both";
14
+ };
15
+
16
+ export const ScrollArea = React.forwardRef<
17
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
18
+ ScrollAreaProps
19
+ >(({
20
+ className,
21
+ children,
22
+ viewportClassName,
23
+ viewportRef,
24
+ viewportProps,
25
+ scrollbars = "vertical",
26
+ type = "auto",
27
+ ...props
28
+ }, ref) => (
29
+ <ScrollAreaPrimitive.Root
30
+ ref={ref}
31
+ data-slot="scroll-area"
32
+ type={type}
33
+ className={cn("relative overflow-hidden", className)}
34
+ {...props}
35
+ >
36
+ <ScrollAreaPrimitive.Viewport
37
+ ref={viewportRef}
38
+ data-slot="scroll-area-viewport"
39
+ className={cn(
40
+ "size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:ring-2 focus-visible:ring-[var(--ring)]/45",
41
+ scrollbars === "vertical" && "[&>div]:!block [&>div]:min-w-0 [&>div]:w-full",
42
+ viewportClassName,
43
+ )}
44
+ {...viewportProps}
45
+ >
46
+ {children}
47
+ </ScrollAreaPrimitive.Viewport>
48
+ {scrollbars !== "horizontal" && <ScrollBar orientation="vertical" />}
49
+ {scrollbars !== "vertical" && <ScrollBar orientation="horizontal" />}
50
+ <ScrollAreaPrimitive.Corner data-slot="scroll-area-corner" />
51
+ </ScrollAreaPrimitive.Root>
52
+ ));
53
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
54
+
55
+ export const ScrollBar = React.forwardRef<
56
+ React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
57
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
58
+ >(({ className, orientation = "vertical", style, ...props }, ref) => (
59
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
60
+ ref={ref}
61
+ data-slot="scroll-area-scrollbar"
62
+ orientation={orientation}
63
+ className={cn(
64
+ "group flex touch-none select-none p-px transition-colors",
65
+ orientation === "vertical" && "h-full w-2",
66
+ orientation === "horizontal" && "h-2 flex-col",
67
+ className,
68
+ )}
69
+ style={{
70
+ ...(orientation === "vertical" ? { right: 2 } : { bottom: 2 }),
71
+ ...style,
72
+ }}
73
+ {...props}
74
+ >
75
+ <ScrollAreaPrimitive.ScrollAreaThumb
76
+ data-slot="scroll-area-thumb"
77
+ className={cn(
78
+ "relative flex-1 rounded-full bg-[var(--scrollbar-thumb)] transition-colors group-hover:bg-[var(--scrollbar-thumb-hover)] active:bg-[var(--scrollbar-thumb-active)]",
79
+ orientation === "vertical" ? "min-h-8 w-full" : "h-full min-w-8",
80
+ )}
81
+ />
82
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
83
+ ));
84
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
@@ -0,0 +1,191 @@
1
+ import * as SelectPrimitive from "@radix-ui/react-select";
2
+ import { Check, ChevronDown, ChevronUp } from "lucide-react";
3
+ import * as React from "react";
4
+
5
+ import { cn } from "../lib/utils.js";
6
+
7
+ export function Select(props: React.ComponentProps<typeof SelectPrimitive.Root>) {
8
+ return <SelectPrimitive.Root data-slot="select" {...props} />;
9
+ }
10
+
11
+ export const SelectGroup = React.forwardRef<
12
+ React.ElementRef<typeof SelectPrimitive.Group>,
13
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Group>
14
+ >(({ className, ...props }, ref) => (
15
+ <SelectPrimitive.Group
16
+ ref={ref}
17
+ data-slot="select-group"
18
+ className={cn("scroll-my-1 p-1", className)}
19
+ {...props}
20
+ />
21
+ ));
22
+ SelectGroup.displayName = SelectPrimitive.Group.displayName;
23
+
24
+ export const SelectValue = React.forwardRef<
25
+ React.ElementRef<typeof SelectPrimitive.Value>,
26
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Value>
27
+ >((props, ref) => <SelectPrimitive.Value ref={ref} data-slot="select-value" {...props} />);
28
+ SelectValue.displayName = SelectPrimitive.Value.displayName;
29
+
30
+ export const SelectTrigger = React.forwardRef<
31
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
32
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
33
+ size?: "sm" | "default";
34
+ }
35
+ >(({ className, size = "default", children, ...props }, ref) => (
36
+ <SelectPrimitive.Trigger
37
+ ref={ref}
38
+ data-slot="select-trigger"
39
+ data-size={size}
40
+ className={cn(
41
+ "flex w-full min-w-0 items-center justify-between gap-1.5 whitespace-nowrap rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&>span]:min-w-0 [&>span]:truncate [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
42
+ className,
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ <SelectPrimitive.Icon asChild>
48
+ <ChevronDown
49
+ aria-hidden="true"
50
+ className="pointer-events-none size-4 shrink-0 text-muted-foreground"
51
+ />
52
+ </SelectPrimitive.Icon>
53
+ </SelectPrimitive.Trigger>
54
+ ));
55
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
56
+
57
+ export const SelectScrollUpButton = React.forwardRef<
58
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
59
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
60
+ >(({ className, ...props }, ref) => (
61
+ <SelectPrimitive.ScrollUpButton
62
+ ref={ref}
63
+ data-slot="select-scroll-up-button"
64
+ className={cn(
65
+ "z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
66
+ className,
67
+ )}
68
+ {...props}
69
+ >
70
+ <ChevronUp aria-hidden="true" />
71
+ </SelectPrimitive.ScrollUpButton>
72
+ ));
73
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
74
+
75
+ export const SelectScrollDownButton = React.forwardRef<
76
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
77
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
78
+ >(({ className, ...props }, ref) => (
79
+ <SelectPrimitive.ScrollDownButton
80
+ ref={ref}
81
+ data-slot="select-scroll-down-button"
82
+ className={cn(
83
+ "z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
84
+ className,
85
+ )}
86
+ {...props}
87
+ >
88
+ <ChevronDown aria-hidden="true" />
89
+ </SelectPrimitive.ScrollDownButton>
90
+ ));
91
+ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
92
+
93
+ export const SelectContent = React.forwardRef<
94
+ React.ElementRef<typeof SelectPrimitive.Content>,
95
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
96
+ >(
97
+ (
98
+ {
99
+ className,
100
+ children,
101
+ position = "popper",
102
+ align = "center",
103
+ sideOffset = 4,
104
+ collisionPadding = 8,
105
+ ...props
106
+ },
107
+ ref,
108
+ ) => (
109
+ <SelectPrimitive.Portal>
110
+ <SelectPrimitive.Content
111
+ ref={ref}
112
+ data-slot="select-content"
113
+ data-align-trigger={position === "item-aligned"}
114
+ position={position}
115
+ align={align}
116
+ sideOffset={sideOffset}
117
+ collisionPadding={collisionPadding}
118
+ className={cn(
119
+ "cn-menu-translucent relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-36 origin-[var(--radix-select-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
120
+ position === "popper" &&
121
+ "w-[var(--radix-select-trigger-width)] data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
122
+ className,
123
+ )}
124
+ {...props}
125
+ >
126
+ <SelectScrollUpButton />
127
+ <SelectPrimitive.Viewport
128
+ data-position={position}
129
+ className={cn(
130
+ "p-1",
131
+ position === "popper" &&
132
+ "min-w-[var(--radix-select-trigger-width)]",
133
+ )}
134
+ >
135
+ {children}
136
+ </SelectPrimitive.Viewport>
137
+ <SelectScrollDownButton />
138
+ </SelectPrimitive.Content>
139
+ </SelectPrimitive.Portal>
140
+ ),
141
+ );
142
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
143
+
144
+ export const SelectLabel = React.forwardRef<
145
+ React.ElementRef<typeof SelectPrimitive.Label>,
146
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
147
+ >(({ className, ...props }, ref) => (
148
+ <SelectPrimitive.Label
149
+ ref={ref}
150
+ data-slot="select-label"
151
+ className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
152
+ {...props}
153
+ />
154
+ ));
155
+ SelectLabel.displayName = SelectPrimitive.Label.displayName;
156
+
157
+ export const SelectItem = React.forwardRef<
158
+ React.ElementRef<typeof SelectPrimitive.Item>,
159
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
160
+ >(({ className, children, ...props }, ref) => (
161
+ <SelectPrimitive.Item
162
+ ref={ref}
163
+ data-slot="select-item"
164
+ className={cn(
165
+ "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
166
+ className,
167
+ )}
168
+ {...props}
169
+ >
170
+ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
171
+ <SelectPrimitive.ItemIndicator>
172
+ <Check aria-hidden="true" className="pointer-events-none" />
173
+ </SelectPrimitive.ItemIndicator>
174
+ </span>
175
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
176
+ </SelectPrimitive.Item>
177
+ ));
178
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
179
+
180
+ export const SelectSeparator = React.forwardRef<
181
+ React.ElementRef<typeof SelectPrimitive.Separator>,
182
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
183
+ >(({ className, ...props }, ref) => (
184
+ <SelectPrimitive.Separator
185
+ ref={ref}
186
+ data-slot="select-separator"
187
+ className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
188
+ {...props}
189
+ />
190
+ ));
191
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;