@open-mercato/ui 0.5.1-develop.2949.009dcdd2d5 → 0.5.1-develop.2954.610bab2d08

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 * as React from 'react'
4
4
  import Link from 'next/link'
5
5
  import { ArrowUpRightSquare, Pencil, Plus, Trash2 } from 'lucide-react'
6
6
  import { Button } from '@open-mercato/ui/primitives/button'
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from '@open-mercato/ui/primitives/select'
7
14
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
8
15
  import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
9
16
  import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
@@ -322,17 +329,21 @@ function ActivityForm({
322
329
  const currentValue =
323
330
  typeof value === 'string' && value.length ? value : normalizedEntityOptions[0]?.id ?? ''
324
331
  return (
325
- <select
326
- className="h-9 w-full rounded border border-muted-foreground/40 bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
327
- value={currentValue}
328
- onChange={(event) => setValue(event.target.value)}
332
+ <Select
333
+ value={currentValue || undefined}
334
+ onValueChange={(next) => setValue(next ?? '')}
329
335
  >
330
- {normalizedEntityOptions.map((option) => (
331
- <option key={option.id} value={option.id}>
332
- {option.label}
333
- </option>
334
- ))}
335
- </select>
336
+ <SelectTrigger>
337
+ <SelectValue />
338
+ </SelectTrigger>
339
+ <SelectContent>
340
+ {normalizedEntityOptions.map((option) => (
341
+ <SelectItem key={option.id} value={option.id}>
342
+ {option.label}
343
+ </SelectItem>
344
+ ))}
345
+ </SelectContent>
346
+ </Select>
336
347
  )
337
348
  },
338
349
  } as CrudField)
@@ -347,20 +358,21 @@ function ActivityForm({
347
358
  component: ({ value, setValue }) => {
348
359
  const currentValue = typeof value === 'string' ? value : ''
349
360
  return (
350
- <select
351
- className="h-9 w-full rounded border border-muted-foreground/40 bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
352
- value={currentValue}
353
- onChange={(event) => setValue(event.target.value)}
361
+ <Select
362
+ value={currentValue || undefined}
363
+ onValueChange={(next) => setValue(next ?? '')}
354
364
  >
355
- <option value="">
356
- {translate('fields.dealPlaceholder', 'No linked deal')}
357
- </option>
358
- {normalizedDealOptions.map((option) => (
359
- <option key={option.id} value={option.id}>
360
- {option.label}
361
- </option>
362
- ))}
363
- </select>
365
+ <SelectTrigger>
366
+ <SelectValue placeholder={translate('fields.dealPlaceholder', 'No linked deal')} />
367
+ </SelectTrigger>
368
+ <SelectContent>
369
+ {normalizedDealOptions.map((option) => (
370
+ <SelectItem key={option.id} value={option.id}>
371
+ {option.label}
372
+ </SelectItem>
373
+ ))}
374
+ </SelectContent>
375
+ </Select>
364
376
  )
365
377
  },
366
378
  } as CrudField)
@@ -6,6 +6,13 @@ import { usePathname, useSearchParams } from 'next/navigation'
6
6
  import { Plus, Settings } from 'lucide-react'
7
7
  import { Button } from '../../primitives/button'
8
8
  import { Input } from '@open-mercato/ui/primitives/input'
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from '@open-mercato/ui/primitives/select'
9
16
  import {
10
17
  Dialog,
11
18
  DialogContent,
@@ -232,24 +239,31 @@ export function AddressEditor<C = unknown>({
232
239
  aria-invalid={errors.name ? 'true' : undefined}
233
240
  />
234
241
  <div className="flex gap-2">
235
- <select
236
- className={inputClass('purpose')}
237
- value={current.purpose}
238
- onChange={(evt) => update('purpose', evt.target.value)}
242
+ <Select
243
+ value={current.purpose || undefined}
244
+ onValueChange={(next) => update('purpose', next ?? '')}
239
245
  disabled={disabled}
240
- aria-invalid={errors.purpose ? 'true' : undefined}
241
246
  >
242
- <option value="">
243
- {addressTypesLoading
244
- ? label('types.loading', 'Loading…')
245
- : label('types.placeholder', 'Address type')}
246
- </option>
247
- {addressTypes.map((entry) => (
248
- <option key={entry.value} value={entry.value}>
249
- {entry.label}
250
- </option>
251
- ))}
252
- </select>
247
+ <SelectTrigger
248
+ className={errors.purpose ? 'border-destructive' : undefined}
249
+ aria-invalid={errors.purpose ? 'true' : undefined}
250
+ >
251
+ <SelectValue
252
+ placeholder={
253
+ addressTypesLoading
254
+ ? label('types.loading', 'Loading…')
255
+ : label('types.placeholder', 'Address type')
256
+ }
257
+ />
258
+ </SelectTrigger>
259
+ <SelectContent>
260
+ {addressTypes.map((entry) => (
261
+ <SelectItem key={entry.value} value={entry.value}>
262
+ {entry.label}
263
+ </SelectItem>
264
+ ))}
265
+ </SelectContent>
266
+ </Select>
253
267
  {addressTypesAdapter?.create ? (
254
268
  <Dialog open={typeDialogOpen} onOpenChange={setTypeDialogOpen}>
255
269
  <DialogTrigger asChild>
@@ -6,6 +6,13 @@ import { FileCode, Loader2, Mail, Pencil, Phone, X } from 'lucide-react'
6
6
  import type { PluggableList } from 'unified'
7
7
  import { PhoneNumberField } from '@open-mercato/ui/backend/inputs/PhoneNumberField'
8
8
  import { Button } from '@open-mercato/ui/primitives/button'
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from '@open-mercato/ui/primitives/select'
9
16
  import { Textarea } from '@open-mercato/ui/primitives/textarea'
10
17
  import { useT } from '@open-mercato/shared/lib/i18n/context'
11
18
  import { cn } from '@open-mercato/shared/lib/utils'
@@ -826,18 +833,21 @@ export function InlineSelectEditor({
826
833
  {renderEditor ? (
827
834
  renderEditor({ value: draft, onChange: setDraft })
828
835
  ) : (
829
- <select
830
- className="w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
831
- value={draft}
832
- onChange={(event) => setDraft(event.target.value)}
836
+ <Select
837
+ value={draft || undefined}
838
+ onValueChange={(next) => setDraft(next ?? '')}
833
839
  >
834
- <option value="">{t('ui.detail.inline.select.placeholder', 'Not set')}</option>
835
- {options.map((option) => (
836
- <option key={option.value} value={option.value}>
837
- {option.label}
838
- </option>
839
- ))}
840
- </select>
840
+ <SelectTrigger>
841
+ <SelectValue placeholder={t('ui.detail.inline.select.placeholder', 'Not set')} />
842
+ </SelectTrigger>
843
+ <SelectContent>
844
+ {options.map((option) => (
845
+ <SelectItem key={option.value} value={option.value}>
846
+ {option.label}
847
+ </SelectItem>
848
+ ))}
849
+ </SelectContent>
850
+ </Select>
841
851
  )}
842
852
  <div className="flex items-center gap-2">
843
853
  <Button type="button" size="sm" onClick={() => void handleSave()} disabled={saving}>
@@ -8,6 +8,13 @@ import type { IconOption } from '@open-mercato/core/modules/dictionaries/compone
8
8
  import { ArrowUpRightSquare, FileCode, Loader2, Palette, Pencil, Plus, Trash2 } from 'lucide-react'
9
9
  import { formatRelativeTime } from '@open-mercato/shared/lib/time'
10
10
  import { Button } from '@open-mercato/ui/primitives/button'
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from '@open-mercato/ui/primitives/select'
11
18
  import { flash } from '../FlashMessages'
12
19
  import { SwitchableMarkdownInput } from '../inputs/SwitchableMarkdownInput'
13
20
  import { ErrorMessage } from './ErrorMessage'
@@ -1013,19 +1020,22 @@ function NotesSectionImpl<C = unknown>({
1013
1020
  >
1014
1021
  {label('fields.entity', 'Assign to customer')}
1015
1022
  </label>
1016
- <select
1017
- id="note-entity-select"
1018
- className="h-9 rounded border border-muted-foreground/40 bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1019
- value={selectedEntityId}
1020
- onChange={(event) => setSelectedEntityId(event.target.value)}
1023
+ <Select
1024
+ value={selectedEntityId || undefined}
1025
+ onValueChange={(next) => setSelectedEntityId(next ?? '')}
1021
1026
  disabled={isSubmitting || isLoading || !normalizedEntityOptions.length}
1022
1027
  >
1023
- {normalizedEntityOptions.map((option) => (
1024
- <option key={option.id} value={option.id}>
1025
- {option.label}
1026
- </option>
1027
- ))}
1028
- </select>
1028
+ <SelectTrigger id="note-entity-select">
1029
+ <SelectValue />
1030
+ </SelectTrigger>
1031
+ <SelectContent>
1032
+ {normalizedEntityOptions.map((option) => (
1033
+ <SelectItem key={option.id} value={option.id}>
1034
+ {option.label}
1035
+ </SelectItem>
1036
+ ))}
1037
+ </SelectContent>
1038
+ </Select>
1029
1039
  </div>
1030
1040
  ) : null}
1031
1041
  {normalizedDealOptions.length ? (
@@ -1036,22 +1046,22 @@ function NotesSectionImpl<C = unknown>({
1036
1046
  >
1037
1047
  {label('fields.deal', 'Link to deal (optional)')}
1038
1048
  </label>
1039
- <select
1040
- id="note-deal-select"
1041
- className="h-9 rounded border border-muted-foreground/40 bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1042
- value={selectedDealId}
1043
- onChange={(event) => setSelectedDealId(event.target.value)}
1049
+ <Select
1050
+ value={selectedDealId || undefined}
1051
+ onValueChange={(next) => setSelectedDealId(next ?? '')}
1044
1052
  disabled={isSubmitting || isLoading}
1045
1053
  >
1046
- <option value="">
1047
- {label('fields.dealPlaceholder', 'No linked deal')}
1048
- </option>
1049
- {normalizedDealOptions.map((option) => (
1050
- <option key={option.id} value={option.id}>
1051
- {option.label}
1052
- </option>
1053
- ))}
1054
- </select>
1054
+ <SelectTrigger id="note-deal-select">
1055
+ <SelectValue placeholder={label('fields.dealPlaceholder', 'No linked deal')} />
1056
+ </SelectTrigger>
1057
+ <SelectContent>
1058
+ {normalizedDealOptions.map((option) => (
1059
+ <SelectItem key={option.id} value={option.id}>
1060
+ {option.label}
1061
+ </SelectItem>
1062
+ ))}
1063
+ </SelectContent>
1064
+ </Select>
1055
1065
  </div>
1056
1066
  ) : null}
1057
1067
  </div>
@@ -3,6 +3,14 @@ import * as React from 'react'
3
3
  import { ChevronDown, Plus, Trash2, X } from 'lucide-react'
4
4
  import { Button } from '../../primitives/button'
5
5
  import { IconButton } from '../../primitives/icon-button'
6
+ import { Input } from '../../primitives/input'
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from '../../primitives/select'
6
14
  import { useT } from '@open-mercato/shared/lib/i18n/context'
7
15
  import type {
8
16
  AdvancedFilterState,
@@ -114,30 +122,41 @@ function ConditionRow({
114
122
  )}
115
123
  </div>
116
124
 
117
- <select
118
- className="rounded border bg-background px-2 py-1.5 text-sm min-w-[140px]"
119
- value={condition.field}
120
- onChange={(e) => handleFieldChange(e.target.value)}
121
- aria-label={t('ui.advancedFilter.selectField', 'Select field')}
125
+ <Select
126
+ value={condition.field || undefined}
127
+ onValueChange={(next) => handleFieldChange(next ?? '')}
122
128
  >
123
- <option value="" disabled>{t('ui.advancedFilter.selectFieldPlaceholder', 'Select field...')}</option>
124
- {fields.map((f) => (
125
- <option key={f.key} value={f.key}>{f.label}</option>
126
- ))}
127
- </select>
129
+ <SelectTrigger
130
+ className="min-w-[140px]"
131
+ aria-label={t('ui.advancedFilter.selectField', 'Select field')}
132
+ >
133
+ <SelectValue placeholder={t('ui.advancedFilter.selectFieldPlaceholder', 'Select field...')} />
134
+ </SelectTrigger>
135
+ <SelectContent>
136
+ {fields.map((f) => (
137
+ <SelectItem key={f.key} value={f.key}>{f.label}</SelectItem>
138
+ ))}
139
+ </SelectContent>
140
+ </Select>
128
141
 
129
- <select
130
- className="rounded border bg-background px-2 py-1.5 text-sm min-w-[120px]"
142
+ <Select
131
143
  value={condition.operator}
132
- onChange={(e) => onUpdate(condition.id, { operator: e.target.value as FilterOperator, value: '' })}
133
- aria-label={t('ui.advancedFilter.selectOperator', 'Select operator')}
144
+ onValueChange={(next) => onUpdate(condition.id, { operator: next as FilterOperator, value: '' })}
134
145
  >
135
- {operators.map((op) => (
136
- <option key={op} value={op}>
137
- {t(`ui.advancedFilter.operator.${op}`, OPERATOR_LABELS[op])}
138
- </option>
139
- ))}
140
- </select>
146
+ <SelectTrigger
147
+ className="min-w-[120px]"
148
+ aria-label={t('ui.advancedFilter.selectOperator', 'Select operator')}
149
+ >
150
+ <SelectValue />
151
+ </SelectTrigger>
152
+ <SelectContent>
153
+ {operators.map((op) => (
154
+ <SelectItem key={op} value={op}>
155
+ {t(`ui.advancedFilter.operator.${op}`, OPERATOR_LABELS[op])}
156
+ </SelectItem>
157
+ ))}
158
+ </SelectContent>
159
+ </Select>
141
160
 
142
161
  {!valueless ? (
143
162
  <ValueInput
@@ -180,17 +199,22 @@ function ValueInput({
180
199
 
181
200
  if (fieldType === 'select' && fieldDef?.options) {
182
201
  return (
183
- <select
184
- className="rounded border bg-background px-2 py-1.5 text-sm min-w-[140px]"
185
- value={typeof value === 'string' ? value : ''}
186
- onChange={(e) => onUpdate(condition.id, { value: e.target.value })}
187
- aria-label={t('ui.advancedFilter.selectValue', 'Select value')}
202
+ <Select
203
+ value={typeof value === 'string' && value ? value : undefined}
204
+ onValueChange={(next) => onUpdate(condition.id, { value: next ?? '' })}
188
205
  >
189
- <option value="" disabled>{t('ui.advancedFilter.selectValuePlaceholder', 'Select...')}</option>
190
- {fieldDef.options.map((opt) => (
191
- <option key={opt.value} value={opt.value}>{opt.label}</option>
192
- ))}
193
- </select>
206
+ <SelectTrigger
207
+ className="min-w-[140px]"
208
+ aria-label={t('ui.advancedFilter.selectValue', 'Select value')}
209
+ >
210
+ <SelectValue placeholder={t('ui.advancedFilter.selectValuePlaceholder', 'Select...')} />
211
+ </SelectTrigger>
212
+ <SelectContent>
213
+ {fieldDef.options.map((opt) => (
214
+ <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
215
+ ))}
216
+ </SelectContent>
217
+ </Select>
194
218
  )
195
219
  }
196
220
 
@@ -208,9 +232,10 @@ function ValueInput({
208
232
 
209
233
  if (fieldType === 'number') {
210
234
  return (
211
- <input
235
+ <Input
212
236
  type="number"
213
- className="rounded border bg-background px-2 py-1.5 text-sm w-[120px]"
237
+ size="sm"
238
+ className="w-[120px]"
214
239
  value={typeof value === 'number' ? value : typeof value === 'string' ? value : ''}
215
240
  onChange={(e) => onUpdate(condition.id, { value: e.target.value })}
216
241
  placeholder={t('ui.advancedFilter.numberPlaceholder', 'Value')}
@@ -220,9 +245,10 @@ function ValueInput({
220
245
  }
221
246
 
222
247
  return (
223
- <input
248
+ <Input
224
249
  type="text"
225
- className="rounded border bg-background px-2 py-1.5 text-sm min-w-[140px]"
250
+ size="sm"
251
+ className="min-w-[140px]"
226
252
  value={typeof value === 'string' ? value : ''}
227
253
  onChange={(e) => onUpdate(condition.id, { value: e.target.value })}
228
254
  placeholder={t('ui.advancedFilter.textPlaceholder', 'Value...')}
@@ -8,6 +8,13 @@ import { Input } from '../../primitives/input'
8
8
  import { Checkbox } from '../../primitives/checkbox'
9
9
  import { Textarea } from '../../primitives/textarea'
10
10
  import { Label } from '../../primitives/label'
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from '../../primitives/select'
11
18
  import { Spinner } from '../../primitives/spinner'
12
19
 
13
20
  type InjectedFieldProps = {
@@ -53,20 +60,22 @@ function SelectField({
53
60
  return (
54
61
  <div className="space-y-2" data-crud-field-id={field.id}>
55
62
  <Label htmlFor={field.id}>{label}</Label>
56
- <select
57
- id={field.id}
58
- className="flex h-9 w-full rounded-md border border-input bg-background pl-3 pr-8 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"
59
- value={typeof value === 'string' ? value : ''}
63
+ <Select
64
+ value={typeof value === 'string' && value ? value : undefined}
65
+ onValueChange={(next) => onChange(field.id, next || undefined)}
60
66
  disabled={disabled || (options.length === 0 && !field.options?.length)}
61
- onChange={(event) => onChange(field.id, event.target.value || undefined)}
62
67
  >
63
- <option value="">{t('ui.filters.select.placeholder', 'Select...')}</option>
64
- {options.map((option) => (
65
- <option key={option.value} value={option.value}>
66
- {option.labelKey ? t(option.labelKey, option.label) : option.label}
67
- </option>
68
- ))}
69
- </select>
68
+ <SelectTrigger id={field.id}>
69
+ <SelectValue placeholder={t('ui.filters.select.placeholder', 'Select...')} />
70
+ </SelectTrigger>
71
+ <SelectContent>
72
+ {options.map((option) => (
73
+ <SelectItem key={option.value} value={option.value}>
74
+ {option.labelKey ? t(option.labelKey, option.label) : option.label}
75
+ </SelectItem>
76
+ ))}
77
+ </SelectContent>
78
+ </Select>
70
79
  {optionsError ? (
71
80
  <div className="text-xs text-muted-foreground">{t('ui.forms.optionsUnavailable', 'Options unavailable')}</div>
72
81
  ) : null}
@@ -208,6 +208,10 @@ export function ComboboxInput({
208
208
 
209
209
  return (
210
210
  <div className="relative w-full">
211
+ {/* Use raw <input> here instead of the DS Input primitive: ComboboxInput's
212
+ focus / suggestions-popup interplay relies on the trigger being a plain
213
+ input element. The DS wrapper introduces a <div> that desyncs autocomplete
214
+ on this specific surface. Keeps the rest of the form on Input primitive. */}
211
215
  <input
212
216
  ref={inputRef}
213
217
  type="text"
@@ -4,6 +4,15 @@ import * as React from 'react'
4
4
  import { useMemo } from 'react'
5
5
  import { useQuery } from '@tanstack/react-query'
6
6
  import { apiCall } from '../utils/apiCall'
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectGroup,
11
+ SelectItem,
12
+ SelectLabel,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from '../../primitives/select'
7
16
 
8
17
  /**
9
18
  * Event definition returned by the API
@@ -101,25 +110,29 @@ export function EventSelect({
101
110
  const isEmpty = !isLoading && filteredEvents.length === 0
102
111
 
103
112
  return (
104
- <select
105
- value={value}
106
- onChange={(e) => onChange(e.target.value)}
107
- className={`h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className || ''}`}
113
+ <Select
114
+ value={value || undefined}
115
+ onValueChange={(next) => onChange(next ?? '')}
108
116
  disabled={disabled || isLoading}
109
117
  >
110
- <option value="" disabled>
111
- {isLoading ? 'Loading...' : isEmpty ? 'No events available' : placeholder}
112
- </option>
113
- {Object.entries(eventsByModule).map(([module, moduleEvents]) => (
114
- <optgroup key={module} label={formatModuleName(module)}>
115
- {moduleEvents.map(event => (
116
- <option key={event.id} value={event.id}>
117
- {event.label}
118
- </option>
119
- ))}
120
- </optgroup>
121
- ))}
122
- </select>
118
+ <SelectTrigger size="lg" className={className}>
119
+ <SelectValue
120
+ placeholder={isLoading ? 'Loading...' : isEmpty ? 'No events available' : placeholder}
121
+ />
122
+ </SelectTrigger>
123
+ <SelectContent>
124
+ {Object.entries(eventsByModule).map(([module, moduleEvents]) => (
125
+ <SelectGroup key={module}>
126
+ <SelectLabel>{formatModuleName(module)}</SelectLabel>
127
+ {moduleEvents.map(event => (
128
+ <SelectItem key={event.id} value={event.id}>
129
+ {event.label}
130
+ </SelectItem>
131
+ ))}
132
+ </SelectGroup>
133
+ ))}
134
+ </SelectContent>
135
+ </Select>
123
136
  )
124
137
  }
125
138
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { extractPhoneDigits, validatePhoneNumber } from '@open-mercato/shared/lib/phone'
5
+ import { Input } from '../../primitives/input'
5
6
 
6
7
  export type PhoneDuplicateMatch = {
7
8
  id: string
@@ -136,9 +137,8 @@ export function PhoneNumberField({
136
137
 
137
138
  return (
138
139
  <div className="space-y-2">
139
- <input
140
+ <Input
140
141
  type="tel"
141
- className="w-full h-9 rounded border px-2 text-sm"
142
142
  value={local}
143
143
  onChange={handleChange}
144
144
  onBlur={handleBlur}
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { cn } from '@open-mercato/shared/lib/utils'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { Input } from '../../primitives/input'
6
7
 
7
8
  export type TimeInputProps = {
8
9
  value?: string | null
@@ -107,16 +108,12 @@ export function TimeInput({
107
108
  [disabled, emitChange, hour, minuteStep]
108
109
  )
109
110
 
110
- const inputClass = cn(
111
- 'w-14 h-9 rounded border text-center text-sm tabular-nums',
112
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',
113
- disabled && 'bg-muted text-muted-foreground cursor-not-allowed',
114
- 'disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed'
115
- )
111
+ const wrapperClass = 'w-14'
112
+ const innerInputClass = 'text-center tabular-nums'
116
113
 
117
114
  return (
118
115
  <div className={cn('flex items-center gap-1', className)}>
119
- <input
116
+ <Input
120
117
  type="number"
121
118
  min={0}
122
119
  max={23}
@@ -126,10 +123,11 @@ export function TimeInput({
126
123
  disabled={disabled}
127
124
  aria-label={hourLabel}
128
125
  data-crud-focus-target=""
129
- className={inputClass}
126
+ className={wrapperClass}
127
+ inputClassName={innerInputClass}
130
128
  />
131
129
  <span className="text-sm font-medium select-none">:</span>
132
- <input
130
+ <Input
133
131
  type="number"
134
132
  min={0}
135
133
  max={59}
@@ -139,7 +137,8 @@ export function TimeInput({
139
137
  onKeyDown={handleMinuteKeyDown}
140
138
  disabled={disabled}
141
139
  aria-label={minuteLabel}
142
- className={inputClass}
140
+ className={wrapperClass}
141
+ inputClassName={innerInputClass}
143
142
  />
144
143
  </div>
145
144
  )