@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +8 -0
- package/dist/backend/CrudForm.js +57 -29
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +32 -14
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/FilterOverlay.js +23 -17
- package/dist/backend/FilterOverlay.js.map +2 -2
- package/dist/backend/JsonBuilder.js +32 -18
- package/dist/backend/JsonBuilder.js.map +2 -2
- package/dist/backend/columns/ColumnChooserPanel.js +12 -13
- package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js +71 -62
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +2 -2
- package/dist/backend/date-range/DateRangeSelect.js +11 -10
- package/dist/backend/date-range/DateRangeSelect.js.map +2 -2
- package/dist/backend/date-range/InlineDateRangeSelect.js +10 -22
- package/dist/backend/date-range/InlineDateRangeSelect.js.map +2 -2
- package/dist/backend/detail/ActivitiesSection.js +20 -12
- package/dist/backend/detail/ActivitiesSection.js.map +2 -2
- package/dist/backend/detail/AddressEditor.js +24 -7
- package/dist/backend/detail/AddressEditor.js.map +2 -2
- package/dist/backend/detail/InlineEditors.js +12 -6
- package/dist/backend/detail/InlineEditors.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +20 -14
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterBuilder.js +52 -24
- package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
- package/dist/backend/injection/InjectedField.js +12 -7
- package/dist/backend/injection/InjectedField.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/inputs/EventSelect.js +22 -6
- package/dist/backend/inputs/EventSelect.js.map +2 -2
- package/dist/backend/inputs/PhoneNumberField.js +2 -2
- package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
- package/dist/backend/inputs/TimeInput.js +9 -10
- package/dist/backend/inputs/TimeInput.js.map +2 -2
- package/dist/backend/messages/message-compose-form-groups.js +12 -7
- package/dist/backend/messages/message-compose-form-groups.js.map +2 -2
- package/dist/backend/messages/useMessageCompose.js +7 -1
- package/dist/backend/messages/useMessageCompose.js.map +2 -2
- package/dist/frontend/LanguageSwitcher.js +19 -14
- package/dist/frontend/LanguageSwitcher.js.map +2 -2
- package/dist/index.js +5 -0
- package/dist/index.js.map +2 -2
- package/dist/primitives/checkbox-field.js +17 -5
- package/dist/primitives/checkbox-field.js.map +2 -2
- package/dist/primitives/input.js +71 -14
- package/dist/primitives/input.js.map +2 -2
- package/dist/primitives/radio-field.js +74 -0
- package/dist/primitives/radio-field.js.map +7 -0
- package/dist/primitives/radio.js +37 -0
- package/dist/primitives/radio.js.map +7 -0
- package/dist/primitives/select.js +155 -0
- package/dist/primitives/select.js.map +7 -0
- package/dist/primitives/switch-field.js +76 -0
- package/dist/primitives/switch-field.js.map +7 -0
- package/dist/primitives/switch.js +17 -3
- package/dist/primitives/switch.js.map +2 -2
- package/dist/primitives/textarea.js +48 -12
- package/dist/primitives/textarea.js.map +2 -2
- package/dist/primitives/tooltip.js +44 -15
- package/dist/primitives/tooltip.js.map +2 -2
- package/package.json +5 -3
- package/src/backend/CrudForm.tsx +104 -37
- package/src/backend/DataTable.tsx +38 -20
- package/src/backend/FilterOverlay.tsx +35 -21
- package/src/backend/JsonBuilder.tsx +38 -20
- package/src/backend/__tests__/FieldDefinitionsEditor.test.tsx +23 -6
- package/src/backend/columns/ColumnChooserPanel.tsx +9 -10
- package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +120 -87
- package/src/backend/date-range/DateRangeSelect.tsx +19 -12
- package/src/backend/date-range/InlineDateRangeSelect.tsx +16 -20
- package/src/backend/detail/ActivitiesSection.tsx +35 -23
- package/src/backend/detail/AddressEditor.tsx +30 -16
- package/src/backend/detail/InlineEditors.tsx +21 -11
- package/src/backend/detail/NotesSection.tsx +35 -25
- package/src/backend/filters/AdvancedFilterBuilder.tsx +60 -34
- package/src/backend/injection/InjectedField.tsx +21 -12
- package/src/backend/inputs/ComboboxInput.tsx +4 -0
- package/src/backend/inputs/EventSelect.tsx +30 -17
- package/src/backend/inputs/PhoneNumberField.tsx +2 -2
- package/src/backend/inputs/TimeInput.tsx +9 -10
- package/src/backend/messages/message-compose-form-groups.tsx +21 -12
- package/src/backend/messages/useMessageCompose.ts +20 -1
- package/src/frontend/LanguageSwitcher.tsx +20 -17
- package/src/index.ts +5 -0
- package/src/primitives/checkbox-field.tsx +10 -2
- package/src/primitives/input.tsx +73 -12
- package/src/primitives/radio-field.tsx +92 -0
- package/src/primitives/radio.tsx +42 -0
- package/src/primitives/select.tsx +200 -0
- package/src/primitives/switch-field.tsx +100 -0
- package/src/primitives/switch.tsx +17 -4
- package/src/primitives/textarea.tsx +67 -11
- 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
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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)
|
|
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
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
<
|
|
61
|
+
<SelectItem key={locale} value={locale}>
|
|
54
62
|
{languageLabels[locale]}
|
|
55
|
-
</
|
|
63
|
+
</SelectItem>
|
|
56
64
|
))}
|
|
57
|
-
</
|
|
58
|
-
|
|
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
|
|
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>
|
package/src/primitives/input.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
}
|