@open-mercato/ui 0.5.1-develop.2953.6647bb2c43 → 0.5.1-develop.2964.d5ac4a6ebb

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 (96) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +8 -0
  3. package/dist/backend/CrudForm.js +57 -29
  4. package/dist/backend/CrudForm.js.map +2 -2
  5. package/dist/backend/DataTable.js +32 -14
  6. package/dist/backend/DataTable.js.map +2 -2
  7. package/dist/backend/FilterOverlay.js +23 -17
  8. package/dist/backend/FilterOverlay.js.map +2 -2
  9. package/dist/backend/JsonBuilder.js +32 -18
  10. package/dist/backend/JsonBuilder.js.map +2 -2
  11. package/dist/backend/columns/ColumnChooserPanel.js +12 -13
  12. package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
  13. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +71 -62
  14. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +2 -2
  15. package/dist/backend/date-range/DateRangeSelect.js +11 -10
  16. package/dist/backend/date-range/DateRangeSelect.js.map +2 -2
  17. package/dist/backend/date-range/InlineDateRangeSelect.js +10 -22
  18. package/dist/backend/date-range/InlineDateRangeSelect.js.map +2 -2
  19. package/dist/backend/detail/ActivitiesSection.js +20 -12
  20. package/dist/backend/detail/ActivitiesSection.js.map +2 -2
  21. package/dist/backend/detail/AddressEditor.js +24 -7
  22. package/dist/backend/detail/AddressEditor.js.map +2 -2
  23. package/dist/backend/detail/InlineEditors.js +12 -6
  24. package/dist/backend/detail/InlineEditors.js.map +2 -2
  25. package/dist/backend/detail/NotesSection.js +20 -14
  26. package/dist/backend/detail/NotesSection.js.map +2 -2
  27. package/dist/backend/filters/AdvancedFilterBuilder.js +52 -24
  28. package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
  29. package/dist/backend/injection/InjectedField.js +12 -7
  30. package/dist/backend/injection/InjectedField.js.map +2 -2
  31. package/dist/backend/inputs/ComboboxInput.js.map +2 -2
  32. package/dist/backend/inputs/EventSelect.js +22 -6
  33. package/dist/backend/inputs/EventSelect.js.map +2 -2
  34. package/dist/backend/inputs/PhoneNumberField.js +2 -2
  35. package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
  36. package/dist/backend/inputs/TimeInput.js +9 -10
  37. package/dist/backend/inputs/TimeInput.js.map +2 -2
  38. package/dist/backend/messages/message-compose-form-groups.js +12 -7
  39. package/dist/backend/messages/message-compose-form-groups.js.map +2 -2
  40. package/dist/backend/messages/useMessageCompose.js +7 -1
  41. package/dist/backend/messages/useMessageCompose.js.map +2 -2
  42. package/dist/frontend/LanguageSwitcher.js +19 -14
  43. package/dist/frontend/LanguageSwitcher.js.map +2 -2
  44. package/dist/index.js +5 -0
  45. package/dist/index.js.map +2 -2
  46. package/dist/primitives/checkbox-field.js +17 -5
  47. package/dist/primitives/checkbox-field.js.map +2 -2
  48. package/dist/primitives/input.js +71 -14
  49. package/dist/primitives/input.js.map +2 -2
  50. package/dist/primitives/radio-field.js +74 -0
  51. package/dist/primitives/radio-field.js.map +7 -0
  52. package/dist/primitives/radio.js +37 -0
  53. package/dist/primitives/radio.js.map +7 -0
  54. package/dist/primitives/select.js +155 -0
  55. package/dist/primitives/select.js.map +7 -0
  56. package/dist/primitives/switch-field.js +76 -0
  57. package/dist/primitives/switch-field.js.map +7 -0
  58. package/dist/primitives/switch.js +17 -3
  59. package/dist/primitives/switch.js.map +2 -2
  60. package/dist/primitives/textarea.js +48 -12
  61. package/dist/primitives/textarea.js.map +2 -2
  62. package/dist/primitives/tooltip.js +44 -15
  63. package/dist/primitives/tooltip.js.map +2 -2
  64. package/package.json +5 -3
  65. package/src/backend/CrudForm.tsx +104 -37
  66. package/src/backend/DataTable.tsx +38 -20
  67. package/src/backend/FilterOverlay.tsx +35 -21
  68. package/src/backend/JsonBuilder.tsx +38 -20
  69. package/src/backend/__tests__/FieldDefinitionsEditor.test.tsx +23 -6
  70. package/src/backend/columns/ColumnChooserPanel.tsx +9 -10
  71. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +120 -87
  72. package/src/backend/date-range/DateRangeSelect.tsx +19 -12
  73. package/src/backend/date-range/InlineDateRangeSelect.tsx +16 -20
  74. package/src/backend/detail/ActivitiesSection.tsx +35 -23
  75. package/src/backend/detail/AddressEditor.tsx +30 -16
  76. package/src/backend/detail/InlineEditors.tsx +21 -11
  77. package/src/backend/detail/NotesSection.tsx +35 -25
  78. package/src/backend/filters/AdvancedFilterBuilder.tsx +60 -34
  79. package/src/backend/injection/InjectedField.tsx +21 -12
  80. package/src/backend/inputs/ComboboxInput.tsx +4 -0
  81. package/src/backend/inputs/EventSelect.tsx +30 -17
  82. package/src/backend/inputs/PhoneNumberField.tsx +2 -2
  83. package/src/backend/inputs/TimeInput.tsx +9 -10
  84. package/src/backend/messages/message-compose-form-groups.tsx +21 -12
  85. package/src/backend/messages/useMessageCompose.ts +20 -1
  86. package/src/frontend/LanguageSwitcher.tsx +20 -17
  87. package/src/index.ts +5 -0
  88. package/src/primitives/checkbox-field.tsx +10 -2
  89. package/src/primitives/input.tsx +73 -12
  90. package/src/primitives/radio-field.tsx +92 -0
  91. package/src/primitives/radio.tsx +42 -0
  92. package/src/primitives/select.tsx +200 -0
  93. package/src/primitives/switch-field.tsx +100 -0
  94. package/src/primitives/switch.tsx +17 -4
  95. package/src/primitives/textarea.tsx +67 -11
  96. package/src/primitives/tooltip.tsx +68 -24
@@ -4,6 +4,13 @@ import { type CrudField } from '../CrudForm'
4
4
  import { IconButton } from '../../primitives/icon-button'
5
5
  import { Input } from '../../primitives/input'
6
6
  import { Label } from '../../primitives/label'
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from '../../primitives/select'
7
14
  import { Switch } from '../../primitives/switch'
8
15
  import { AttachmentsSection } from '../detail/AttachmentsSection'
9
16
  import { SwitchableMarkdownInput } from '../inputs/SwitchableMarkdownInput'
@@ -98,19 +105,21 @@ function ContextActionsSection({ compose }: ComposeProps) {
98
105
  <Label htmlFor="messages-compose-context-action-type">
99
106
  {compose.t('messages.composer.objectPicker.actionTypeLabel', 'Action type')}
100
107
  </Label>
101
- <select
102
- id="messages-compose-context-action-type"
103
- value={compose.contextActionType}
104
- onChange={(event) => compose.setContextActionType(event.target.value)}
105
- className="h-9 w-full rounded-md border bg-background px-3 text-sm"
108
+ <Select
109
+ value={compose.contextActionType || undefined}
110
+ onValueChange={(value) => compose.setContextActionType(value || '')}
106
111
  >
107
- <option value="">{compose.t('messages.composer.objectPicker.actionTypePlaceholder', 'Select action')}</option>
108
- {compose.contextActionOptions.map((option) => (
109
- <option key={option.id} value={option.id}>
110
- {option.label}
111
- </option>
112
- ))}
113
- </select>
112
+ <SelectTrigger id="messages-compose-context-action-type">
113
+ <SelectValue placeholder={compose.t('messages.composer.objectPicker.actionTypePlaceholder', 'Select action')} />
114
+ </SelectTrigger>
115
+ <SelectContent>
116
+ {compose.contextActionOptions.map((option) => (
117
+ <SelectItem key={option.id} value={option.id}>
118
+ {option.label}
119
+ </SelectItem>
120
+ ))}
121
+ </SelectContent>
122
+ </Select>
114
123
  </div>
115
124
  ) : null}
116
125
  </div>
@@ -154,6 +154,16 @@ export function useMessageCompose({
154
154
  const [submitting, setSubmitting] = React.useState(false)
155
155
  const [submitMode, setSubmitMode] = React.useState<'send' | 'draft'>('send')
156
156
  const [submitError, setSubmitError] = React.useState<string | null>(null)
157
+ // Tracks whether the composer is currently in the "open" lifecycle so the init
158
+ // effect below only runs on the closed → open transition, not on every parent
159
+ // re-render that produces a new `defaultValues` / `contextObject` reference
160
+ // while the user is typing. Without this guard, an inline literal
161
+ // `defaultValues={{...}}` in a re-rendering parent (e.g. message detail page
162
+ // with live notification badges or queue progress) would clear the body /
163
+ // subject mid-keystroke. CI shard 9 surfaced this as TC-MSG-009 timing out
164
+ // because `keyboard.type` characters appeared to "type nowhere" — they were
165
+ // typed correctly, then immediately wiped by the next effect run.
166
+ const isOpenRef = React.useRef(false)
157
167
 
158
168
  const messageTypesQuery = useQuery({
159
169
  queryKey: ['messages', 'types'],
@@ -219,7 +229,16 @@ export function useMessageCompose({
219
229
  }, [attachmentEntityId, attachmentRecordId, t])
220
230
 
221
231
  React.useEffect(() => {
222
- if (!isOpen) return
232
+ if (!isOpen) {
233
+ isOpenRef.current = false
234
+ return
235
+ }
236
+ // Only initialize on the closed → open transition. Subsequent parent
237
+ // re-renders that change `defaultValues` / `contextObject` references
238
+ // (inline object literals are a new reference on every render) MUST NOT
239
+ // overwrite state the user has typed in.
240
+ if (isOpenRef.current) return
241
+ isOpenRef.current = true
223
242
 
224
243
  const nextRecipients = defaultValues?.recipients?.filter((value) => typeof value === 'string' && value.trim().length > 0) ?? []
225
244
  const dedupedRecipients = Array.from(new Set(nextRecipients))
@@ -3,6 +3,13 @@ import { useId, useTransition } from 'react'
3
3
  import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
4
4
  import { useRouter } from 'next/navigation'
5
5
  import { locales, type Locale } from '@open-mercato/shared/lib/i18n/config'
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from '@open-mercato/ui/primitives/select'
6
13
 
7
14
  export function LanguageSwitcher() {
8
15
  const current = useLocale()
@@ -41,26 +48,22 @@ export function LanguageSwitcher() {
41
48
  return (
42
49
  <div className="flex items-center gap-2 text-xs text-muted-foreground">
43
50
  <label htmlFor={selectId}>{t('common.language')}</label>
44
- <div className="relative">
45
- <select
46
- id={selectId}
47
- className="appearance-none rounded-md border bg-background px-3 py-1 pr-8 text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:opacity-50"
48
- value={current}
49
- onChange={(event) => setLocale(event.target.value as Locale)}
50
- disabled={pending}
51
- >
51
+ <Select
52
+ value={current}
53
+ onValueChange={(value) => setLocale(value as Locale)}
54
+ disabled={pending}
55
+ >
56
+ <SelectTrigger id={selectId} size="sm">
57
+ <SelectValue />
58
+ </SelectTrigger>
59
+ <SelectContent>
52
60
  {locales.map((locale) => (
53
- <option key={locale} value={locale}>
61
+ <SelectItem key={locale} value={locale}>
54
62
  {languageLabels[locale]}
55
- </option>
63
+ </SelectItem>
56
64
  ))}
57
- </select>
58
- <span className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground">
59
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
60
- <path d="M6 9l6 6 6-6" />
61
- </svg>
62
- </span>
63
- </div>
65
+ </SelectContent>
66
+ </Select>
64
67
  </div>
65
68
  )
66
69
  }
package/src/index.ts CHANGED
@@ -32,6 +32,11 @@ export * from './primitives/social-button'
32
32
  export * from './primitives/fancy-button'
33
33
  export * from './primitives/checkbox'
34
34
  export * from './primitives/checkbox-field'
35
+ export * from './primitives/select'
36
+ export * from './primitives/switch'
37
+ export * from './primitives/switch-field'
38
+ export * from './primitives/radio'
39
+ export * from './primitives/radio-field'
35
40
  export * from './primitives/label'
36
41
  export * from './primitives/separator'
37
42
  export * from './primitives/spinner'
@@ -38,13 +38,14 @@ export const CheckboxField = React.forwardRef<
38
38
  const reactId = React.useId()
39
39
  const id = idProp ?? `checkbox-field-${reactId}`
40
40
 
41
+ const hasMultiLine = Boolean(description || sublabel || link)
41
42
  const checkbox = (
42
43
  <Checkbox
43
44
  ref={ref}
44
45
  id={id}
45
46
  size={size}
46
47
  disabled={disabled}
47
- className={cn("mt-0.5", className)}
48
+ className={cn(hasMultiLine && "mt-0.5", className)}
48
49
  {...checkboxProps}
49
50
  />
50
51
  )
@@ -76,7 +77,14 @@ export const CheckboxField = React.forwardRef<
76
77
  )
77
78
 
78
79
  return (
79
- <div className={cn("flex items-start gap-2", flip && "flex-row-reverse", containerClassName)}>
80
+ <div
81
+ className={cn(
82
+ "flex gap-2",
83
+ hasMultiLine ? "items-start" : "items-center",
84
+ flip && "flex-row-reverse",
85
+ containerClassName
86
+ )}
87
+ >
80
88
  {flip ? content : checkbox}
81
89
  {flip ? checkbox : content}
82
90
  </div>
@@ -1,20 +1,81 @@
1
1
  import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
2
3
  import { cn } from '@open-mercato/shared/lib/utils'
3
4
 
4
- type InputProps = React.ComponentPropsWithoutRef<'input'>
5
+ const inputWrapperVariants = cva(
6
+ 'inline-flex w-full items-center gap-2 rounded-md border border-input bg-background shadow-xs transition-colors focus-within:outline-none focus-within:shadow-focus focus-within:border-foreground hover:bg-muted/40 has-[input:disabled]:bg-bg-disabled has-[input:disabled]:border-border-disabled has-[input:disabled]:shadow-none has-[input:disabled]:hover:bg-bg-disabled has-[input[aria-invalid=true]]:border-destructive has-[input[aria-invalid=true]]:focus-within:border-destructive',
7
+ {
8
+ variants: {
9
+ size: {
10
+ sm: 'h-8 px-2.5',
11
+ default: 'h-9 px-3',
12
+ lg: 'h-10 px-3',
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ size: 'default',
17
+ },
18
+ }
19
+ )
20
+
21
+ const inputElementVariants = cva(
22
+ 'flex-1 min-w-0 bg-transparent border-0 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:bg-transparent',
23
+ {
24
+ variants: {
25
+ size: {
26
+ sm: 'text-xs',
27
+ default: 'text-sm',
28
+ lg: 'text-sm',
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ size: 'default',
33
+ },
34
+ }
35
+ )
36
+
37
+ export type InputProps = Omit<React.ComponentPropsWithoutRef<'input'>, 'size'> &
38
+ VariantProps<typeof inputWrapperVariants> & {
39
+ leftIcon?: React.ReactNode
40
+ rightIcon?: React.ReactNode
41
+ /** Optional className on the inner <input> element. */
42
+ inputClassName?: string
43
+ }
5
44
 
6
45
  export const Input = React.forwardRef<HTMLInputElement, InputProps>(
7
- ({ className, type = 'text', ...props }, ref) => (
8
- <input
9
- ref={ref}
10
- type={type}
11
- className={cn(
12
- 'flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
13
- className
14
- )}
15
- {...props}
16
- />
17
- )
46
+ ({ className, inputClassName, type = 'text', size, leftIcon, rightIcon, ...props }, ref) => {
47
+ return (
48
+ <div
49
+ className={cn(inputWrapperVariants({ size }), className)}
50
+ data-slot="input-wrapper"
51
+ >
52
+ {leftIcon ? (
53
+ <span
54
+ className="flex shrink-0 items-center text-muted-foreground [&_svg]:size-4"
55
+ aria-hidden="true"
56
+ >
57
+ {leftIcon}
58
+ </span>
59
+ ) : null}
60
+ <input
61
+ ref={ref}
62
+ type={type}
63
+ className={cn(inputElementVariants({ size }), inputClassName)}
64
+ {...props}
65
+ />
66
+ {rightIcon ? (
67
+ <span
68
+ className="flex shrink-0 items-center text-muted-foreground [&_svg]:size-4"
69
+ aria-hidden="true"
70
+ >
71
+ {rightIcon}
72
+ </span>
73
+ ) : null}
74
+ </div>
75
+ )
76
+ }
18
77
  )
19
78
 
20
79
  Input.displayName = 'Input'
80
+
81
+ export { inputWrapperVariants, inputElementVariants }
@@ -0,0 +1,92 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '@open-mercato/shared/lib/utils'
5
+ import { Radio } from './radio'
6
+
7
+ export type RadioFieldProps = Omit<React.ComponentProps<typeof Radio>, 'id'> & {
8
+ id?: string
9
+ label: React.ReactNode
10
+ sublabel?: React.ReactNode
11
+ description?: React.ReactNode
12
+ badge?: React.ReactNode
13
+ link?: React.ReactNode
14
+ /** When true, renders the radio on the right of the label content. */
15
+ flip?: boolean
16
+ containerClassName?: string
17
+ contentClassName?: string
18
+ }
19
+
20
+ export const RadioField = React.forwardRef<
21
+ React.ElementRef<typeof Radio>,
22
+ RadioFieldProps
23
+ >(({
24
+ id: idProp,
25
+ label,
26
+ sublabel,
27
+ description,
28
+ badge,
29
+ link,
30
+ flip = false,
31
+ containerClassName,
32
+ contentClassName,
33
+ className,
34
+ disabled,
35
+ ...radioProps
36
+ }, ref) => {
37
+ // useId is SSR/HMR-stable; counter-based fallbacks drift on hydration.
38
+ const fallbackId = React.useId()
39
+ const id = idProp ?? fallbackId
40
+
41
+ const hasMultiLine = Boolean(description || sublabel || link)
42
+ const radio = (
43
+ <Radio
44
+ ref={ref}
45
+ id={id}
46
+ disabled={disabled}
47
+ className={cn(hasMultiLine && 'mt-0.5', className)}
48
+ {...radioProps}
49
+ />
50
+ )
51
+
52
+ const content = (
53
+ <div className={cn('flex flex-1 min-w-0 flex-col gap-2.5', contentClassName)}>
54
+ <div className="flex flex-col gap-1">
55
+ <div className="flex flex-wrap items-center gap-1">
56
+ <label
57
+ htmlFor={id}
58
+ className={cn(
59
+ 'text-sm font-medium leading-5 text-foreground select-none',
60
+ disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
61
+ )}
62
+ >
63
+ {label}
64
+ </label>
65
+ {sublabel ? (
66
+ <span className="text-xs leading-4 text-muted-foreground select-none">{sublabel}</span>
67
+ ) : null}
68
+ {badge ? <span className="inline-flex shrink-0">{badge}</span> : null}
69
+ </div>
70
+ {description ? (
71
+ <p className="text-xs leading-4 text-muted-foreground">{description}</p>
72
+ ) : null}
73
+ </div>
74
+ {link ? <div className="flex">{link}</div> : null}
75
+ </div>
76
+ )
77
+
78
+ return (
79
+ <div
80
+ className={cn(
81
+ 'flex gap-2',
82
+ hasMultiLine ? 'items-start' : 'items-center',
83
+ flip && 'flex-row-reverse',
84
+ containerClassName
85
+ )}
86
+ >
87
+ {flip ? content : radio}
88
+ {flip ? radio : content}
89
+ </div>
90
+ )
91
+ })
92
+ RadioField.displayName = 'RadioField'
@@ -0,0 +1,42 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
5
+
6
+ import { cn } from '@open-mercato/shared/lib/utils'
7
+
8
+ export const RadioGroup = React.forwardRef<
9
+ React.ElementRef<typeof RadioGroupPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <RadioGroupPrimitive.Root
13
+ ref={ref}
14
+ className={cn('flex flex-col gap-2', className)}
15
+ {...props}
16
+ />
17
+ ))
18
+ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
19
+
20
+ export const Radio = React.forwardRef<
21
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
22
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
23
+ >(({ className, ...props }, ref) => (
24
+ <RadioGroupPrimitive.Item
25
+ ref={ref}
26
+ className={cn(
27
+ 'aspect-square size-5 shrink-0 rounded-full border border-input bg-background',
28
+ 'flex items-center justify-center transition-colors',
29
+ 'hover:border-muted-foreground/40',
30
+ 'data-[state=checked]:border-accent-indigo data-[state=checked]:bg-accent-indigo',
31
+ 'focus-visible:outline-none focus-visible:shadow-focus',
32
+ 'disabled:cursor-not-allowed disabled:opacity-60',
33
+ className
34
+ )}
35
+ {...props}
36
+ >
37
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
38
+ <span aria-hidden="true" className="block size-2 rounded-full bg-white" />
39
+ </RadioGroupPrimitive.Indicator>
40
+ </RadioGroupPrimitive.Item>
41
+ ))
42
+ Radio.displayName = RadioGroupPrimitive.Item.displayName
@@ -0,0 +1,200 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import * as SelectPrimitive from '@radix-ui/react-select'
5
+ import { Check, ChevronDown, ChevronUp } from 'lucide-react'
6
+ import { cva, type VariantProps } from 'class-variance-authority'
7
+ import { cn } from '@open-mercato/shared/lib/utils'
8
+
9
+ const selectTriggerVariants = cva(
10
+ 'inline-flex w-full items-center justify-between gap-2 rounded-md border border-input bg-background shadow-xs transition-colors outline-none placeholder:text-muted-foreground hover:bg-muted/40 focus:outline-none focus-visible:outline-none focus-visible:shadow-focus focus-visible:border-foreground disabled:cursor-not-allowed disabled:bg-bg-disabled disabled:border-border-disabled disabled:shadow-none disabled:hover:bg-bg-disabled disabled:[&_svg]:opacity-60 aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:border-destructive data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1 [&_svg]:pointer-events-none [&_svg:not([class*=size-])]:size-4 [&_svg]:shrink-0',
11
+ {
12
+ variants: {
13
+ size: {
14
+ sm: 'h-8 px-2.5 text-xs',
15
+ default: 'h-9 px-3 text-sm',
16
+ lg: 'h-10 px-3 text-sm',
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ size: 'default',
21
+ },
22
+ }
23
+ )
24
+
25
+ export type SelectTriggerProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> &
26
+ VariantProps<typeof selectTriggerVariants>
27
+
28
+ const SelectTrigger = React.forwardRef<
29
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
30
+ SelectTriggerProps
31
+ >(({ className, size, children, ...props }, ref) => (
32
+ <SelectPrimitive.Trigger
33
+ ref={ref}
34
+ className={cn(selectTriggerVariants({ size }), className)}
35
+ data-slot="select-trigger"
36
+ {...props}
37
+ >
38
+ {children}
39
+ <SelectPrimitive.Icon asChild>
40
+ <ChevronDown className="text-muted-foreground" aria-hidden="true" />
41
+ </SelectPrimitive.Icon>
42
+ </SelectPrimitive.Trigger>
43
+ ))
44
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
45
+
46
+ const SelectScrollUpButton = React.forwardRef<
47
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
48
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
49
+ >(({ className, ...props }, ref) => (
50
+ <SelectPrimitive.ScrollUpButton
51
+ ref={ref}
52
+ className={cn('flex cursor-default items-center justify-center py-1 text-muted-foreground', className)}
53
+ {...props}
54
+ >
55
+ <ChevronUp className="size-4" aria-hidden="true" />
56
+ </SelectPrimitive.ScrollUpButton>
57
+ ))
58
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
59
+
60
+ const SelectScrollDownButton = React.forwardRef<
61
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
62
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
63
+ >(({ className, ...props }, ref) => (
64
+ <SelectPrimitive.ScrollDownButton
65
+ ref={ref}
66
+ className={cn('flex cursor-default items-center justify-center py-1 text-muted-foreground', className)}
67
+ {...props}
68
+ >
69
+ <ChevronDown className="size-4" aria-hidden="true" />
70
+ </SelectPrimitive.ScrollDownButton>
71
+ ))
72
+ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
73
+
74
+ const SelectContent = React.forwardRef<
75
+ React.ElementRef<typeof SelectPrimitive.Content>,
76
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
77
+ >(({ className, children, position = 'popper', sideOffset = 4, ...props }, ref) => (
78
+ <SelectPrimitive.Portal>
79
+ <SelectPrimitive.Content
80
+ ref={ref}
81
+ position={position}
82
+ sideOffset={sideOffset}
83
+ className={cn(
84
+ 'relative z-dropdown min-w-[8rem] overflow-hidden rounded-md border border-input bg-popover text-popover-foreground shadow-md outline-none',
85
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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',
86
+ position === 'popper' && 'w-full min-w-[var(--radix-select-trigger-width)]',
87
+ className
88
+ )}
89
+ {...props}
90
+ >
91
+ <SelectScrollUpButton />
92
+ <SelectPrimitive.Viewport
93
+ className={cn(
94
+ 'p-1 max-h-[var(--radix-select-content-available-height)] overflow-y-auto',
95
+ position === 'popper' && 'w-full min-w-[var(--radix-select-trigger-width)]'
96
+ )}
97
+ >
98
+ {children}
99
+ </SelectPrimitive.Viewport>
100
+ <SelectScrollDownButton />
101
+ </SelectPrimitive.Content>
102
+ </SelectPrimitive.Portal>
103
+ ))
104
+ SelectContent.displayName = SelectPrimitive.Content.displayName
105
+
106
+ const SelectLabel = React.forwardRef<
107
+ React.ElementRef<typeof SelectPrimitive.Label>,
108
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
109
+ >(({ className, ...props }, ref) => (
110
+ <SelectPrimitive.Label
111
+ ref={ref}
112
+ className={cn('px-2 py-1.5 text-overline uppercase text-muted-foreground', className)}
113
+ {...props}
114
+ />
115
+ ))
116
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
117
+
118
+ const SelectItem = React.forwardRef<
119
+ React.ElementRef<typeof SelectPrimitive.Item>,
120
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
121
+ >(({ className, children, ...props }, ref) => (
122
+ <SelectPrimitive.Item
123
+ ref={ref}
124
+ className={cn(
125
+ 'relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none transition-colors',
126
+ 'focus:bg-muted focus:text-foreground',
127
+ 'data-[state=checked]:bg-muted/70 data-[state=checked]:text-foreground',
128
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
129
+ '[&_svg]:pointer-events-none [&_svg:not([class*=size-])]:size-4 [&_svg]:shrink-0',
130
+ className
131
+ )}
132
+ {...props}
133
+ >
134
+ <span className="flex-1 truncate">
135
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
136
+ </span>
137
+ <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
138
+ <SelectPrimitive.ItemIndicator>
139
+ <Check className="size-4 text-foreground" aria-hidden="true" />
140
+ </SelectPrimitive.ItemIndicator>
141
+ </span>
142
+ </SelectPrimitive.Item>
143
+ ))
144
+ SelectItem.displayName = SelectPrimitive.Item.displayName
145
+
146
+ const SelectSeparator = React.forwardRef<
147
+ React.ElementRef<typeof SelectPrimitive.Separator>,
148
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
149
+ >(({ className, ...props }, ref) => (
150
+ <SelectPrimitive.Separator
151
+ ref={ref}
152
+ className={cn('-mx-1 my-1 h-px bg-border', className)}
153
+ {...props}
154
+ />
155
+ ))
156
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
157
+
158
+ /**
159
+ * Wraps Radix `Select.Root` to absorb the controlled/uncontrolled transition
160
+ * many call sites trigger by passing `value={x || undefined}`. React fires
161
+ * "Select is changing from uncontrolled to controlled" the moment value flips
162
+ * from undefined to a defined string, and Radix's internal state ends up in
163
+ * an inconsistent shape (dropdown flashes, selections no-op). Coercing
164
+ * `undefined` → `''` keeps Radix in stable controlled mode for the lifetime
165
+ * of the component while preserving "no selection" semantics — Radix simply
166
+ * matches no SelectItem and `SelectValue` falls back to the placeholder.
167
+ */
168
+ const Select = React.forwardRef<
169
+ React.ComponentRef<typeof SelectPrimitive.Root>,
170
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root>
171
+ >(({ value, defaultValue, onValueChange, ...props }, _ref) => {
172
+ const isControlled = value !== undefined || onValueChange !== undefined
173
+ if (!isControlled) {
174
+ return <SelectPrimitive.Root defaultValue={defaultValue} {...props} />
175
+ }
176
+ return (
177
+ <SelectPrimitive.Root
178
+ value={value ?? ''}
179
+ onValueChange={onValueChange}
180
+ {...props}
181
+ />
182
+ )
183
+ }) as unknown as typeof SelectPrimitive.Root
184
+ ;(Select as React.ComponentType).displayName = 'Select'
185
+ const SelectGroup = SelectPrimitive.Group
186
+ const SelectValue = SelectPrimitive.Value
187
+
188
+ export {
189
+ Select,
190
+ SelectGroup,
191
+ SelectValue,
192
+ SelectTrigger,
193
+ SelectContent,
194
+ SelectLabel,
195
+ SelectItem,
196
+ SelectSeparator,
197
+ SelectScrollUpButton,
198
+ SelectScrollDownButton,
199
+ selectTriggerVariants,
200
+ }