@flamingo-stack/openframe-frontend-core 0.0.486 → 0.0.487
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/dist/{chunk-PHYEC3EG.cjs → chunk-4TFUB2CR.cjs} +26 -26
- package/dist/{chunk-PHYEC3EG.cjs.map → chunk-4TFUB2CR.cjs.map} +1 -1
- package/dist/{chunk-ZVFH4LTD.cjs → chunk-6SBAF5VG.cjs} +5 -5
- package/dist/{chunk-ZVFH4LTD.cjs.map → chunk-6SBAF5VG.cjs.map} +1 -1
- package/dist/{chunk-5WZGZ6B2.js → chunk-AUGM74CO.js} +4 -4
- package/dist/{chunk-REURWDBC.js → chunk-BC74IKAX.js} +4705 -4482
- package/dist/chunk-BC74IKAX.js.map +1 -0
- package/dist/{chunk-Z3V42W7P.js → chunk-BTMYUK4P.js} +2 -2
- package/dist/{chunk-HQITFULS.cjs → chunk-EO7MUGY4.cjs} +9 -9
- package/dist/{chunk-HQITFULS.cjs.map → chunk-EO7MUGY4.cjs.map} +1 -1
- package/dist/{chunk-ZTRRB2Y2.js → chunk-G4XU7BYU.js} +2 -2
- package/dist/{chunk-J5KX5K5T.js → chunk-GXOUXAXU.js} +2 -2
- package/dist/{chunk-F3BH5B7M.cjs → chunk-HWK5CH5N.cjs} +11 -11
- package/dist/{chunk-F3BH5B7M.cjs.map → chunk-HWK5CH5N.cjs.map} +1 -1
- package/dist/{chunk-KRKNTK5Y.js → chunk-ITEPRN6L.js} +2 -2
- package/dist/{chunk-RILRBUDL.cjs → chunk-K6MITU6M.cjs} +62 -62
- package/dist/{chunk-RILRBUDL.cjs.map → chunk-K6MITU6M.cjs.map} +1 -1
- package/dist/{chunk-C4QKMOQK.cjs → chunk-KTD2MGYF.cjs} +7 -7
- package/dist/{chunk-C4QKMOQK.cjs.map → chunk-KTD2MGYF.cjs.map} +1 -1
- package/dist/{chunk-OW33LYO2.js → chunk-NETEA5E4.js} +4 -4
- package/dist/{chunk-6QJ2QRQE.js → chunk-QBDHNUQX.js} +4 -4
- package/dist/{chunk-RB6HX4KC.cjs → chunk-QL7S3CH4.cjs} +26 -26
- package/dist/{chunk-RB6HX4KC.cjs.map → chunk-QL7S3CH4.cjs.map} +1 -1
- package/dist/{chunk-XJ6VL3AP.cjs → chunk-QW6SPNPT.cjs} +37 -37
- package/dist/{chunk-XJ6VL3AP.cjs.map → chunk-QW6SPNPT.cjs.map} +1 -1
- package/dist/{chunk-NTHTORJ3.cjs → chunk-RAYDQGJV.cjs} +59 -59
- package/dist/{chunk-NTHTORJ3.cjs.map → chunk-RAYDQGJV.cjs.map} +1 -1
- package/dist/{chunk-KA4ILVCX.js → chunk-SDNMOAT4.js} +2 -2
- package/dist/{chunk-XOIKQW2H.js → chunk-SMD67GJP.js} +2 -2
- package/dist/{chunk-NZYUSFMI.cjs → chunk-TJSJPOPL.cjs} +573 -350
- package/dist/chunk-TJSJPOPL.cjs.map +1 -0
- package/dist/{chunk-MD5E5BKO.js → chunk-UBK5HIMH.js} +4 -4
- package/dist/{chunk-BVHMDHQY.cjs → chunk-UU4DW7JX.cjs} +31 -31
- package/dist/{chunk-BVHMDHQY.cjs.map → chunk-UU4DW7JX.cjs.map} +1 -1
- package/dist/{chunk-RR6XPEEG.cjs → chunk-W6GDTB4R.cjs} +15 -15
- package/dist/{chunk-RR6XPEEG.cjs.map → chunk-W6GDTB4R.cjs.map} +1 -1
- package/dist/{chunk-2BMYCH3D.js → chunk-WLRBZXFB.js} +2 -2
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +144 -130
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +23 -9
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/assignee-dropdown.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +16 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +15 -1
- package/dist/components/ui/searchable-select.d.ts +43 -0
- package/dist/components/ui/searchable-select.d.ts.map +1 -0
- package/dist/components/ui/test-run.d.ts +74 -0
- package/dist/components/ui/test-run.d.ts.map +1 -0
- package/dist/index.cjs +16 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +15 -1
- package/package.json +1 -1
- package/src/components/ui/assignee-dropdown.tsx +36 -91
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/searchable-select.tsx +186 -0
- package/src/components/ui/test-run.tsx +282 -0
- package/src/stories/SearchableSelect.stories.tsx +139 -0
- package/src/stories/TestRun.stories.tsx +118 -0
- package/dist/chunk-NZYUSFMI.cjs.map +0 -1
- package/dist/chunk-REURWDBC.js.map +0 -1
- /package/dist/{chunk-5WZGZ6B2.js.map → chunk-AUGM74CO.js.map} +0 -0
- /package/dist/{chunk-Z3V42W7P.js.map → chunk-BTMYUK4P.js.map} +0 -0
- /package/dist/{chunk-ZTRRB2Y2.js.map → chunk-G4XU7BYU.js.map} +0 -0
- /package/dist/{chunk-J5KX5K5T.js.map → chunk-GXOUXAXU.js.map} +0 -0
- /package/dist/{chunk-KRKNTK5Y.js.map → chunk-ITEPRN6L.js.map} +0 -0
- /package/dist/{chunk-OW33LYO2.js.map → chunk-NETEA5E4.js.map} +0 -0
- /package/dist/{chunk-6QJ2QRQE.js.map → chunk-QBDHNUQX.js.map} +0 -0
- /package/dist/{chunk-KA4ILVCX.js.map → chunk-SDNMOAT4.js.map} +0 -0
- /package/dist/{chunk-XOIKQW2H.js.map → chunk-SMD67GJP.js.map} +0 -0
- /package/dist/{chunk-MD5E5BKO.js.map → chunk-UBK5HIMH.js.map} +0 -0
- /package/dist/{chunk-2BMYCH3D.js.map → chunk-WLRBZXFB.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -504,6 +504,7 @@ import {
|
|
|
504
504
|
ScrollFadeOverlay,
|
|
505
505
|
ScrollShadow,
|
|
506
506
|
SearchInput,
|
|
507
|
+
SearchableSelect,
|
|
507
508
|
SectionSelector,
|
|
508
509
|
Select,
|
|
509
510
|
SelectButton,
|
|
@@ -586,6 +587,9 @@ import {
|
|
|
586
587
|
TagsSelector,
|
|
587
588
|
TaskTypeIcon,
|
|
588
589
|
TermsAgreementLabel,
|
|
590
|
+
TestResultsSkeleton,
|
|
591
|
+
TestResultsTable,
|
|
592
|
+
TestRunResults,
|
|
589
593
|
Textarea,
|
|
590
594
|
ThemeProvider,
|
|
591
595
|
ThinkingDisplay,
|
|
@@ -604,6 +608,7 @@ import {
|
|
|
604
608
|
TimeTrackerHeaderButton,
|
|
605
609
|
TimeTrackerPanel,
|
|
606
610
|
TimeTrackerProvider,
|
|
611
|
+
TimingStat,
|
|
607
612
|
TitleBlock,
|
|
608
613
|
TitleContentBlock,
|
|
609
614
|
Toggle,
|
|
@@ -749,6 +754,7 @@ import {
|
|
|
749
754
|
formatRelativeTime,
|
|
750
755
|
formatReleaseDate,
|
|
751
756
|
formatSingularLookupInvocation,
|
|
757
|
+
formatTestRunDuration,
|
|
752
758
|
formatTicketFullTimestamp,
|
|
753
759
|
formatTicketRelativeTime,
|
|
754
760
|
formatTimeWithTimezone,
|
|
@@ -965,6 +971,7 @@ import {
|
|
|
965
971
|
useSlashCommandRegistry,
|
|
966
972
|
useSlashCommands,
|
|
967
973
|
useSseChatAdapter,
|
|
974
|
+
useTestRunState,
|
|
968
975
|
useTheme,
|
|
969
976
|
useThemeToggle,
|
|
970
977
|
useTimeTracker,
|
|
@@ -980,7 +987,7 @@ import {
|
|
|
980
987
|
walkthroughDismissCookieName,
|
|
981
988
|
withFirstTouchAttribution,
|
|
982
989
|
writeDismissCookie
|
|
983
|
-
} from "./chunk-
|
|
990
|
+
} from "./chunk-BC74IKAX.js";
|
|
984
991
|
import {
|
|
985
992
|
CommandApprovalToast,
|
|
986
993
|
DEFAULT_MIN_FILL_MS,
|
|
@@ -1956,6 +1963,7 @@ export {
|
|
|
1956
1963
|
ScrollShadow,
|
|
1957
1964
|
SearchIcon,
|
|
1958
1965
|
SearchInput,
|
|
1966
|
+
SearchableSelect,
|
|
1959
1967
|
SectionSelector,
|
|
1960
1968
|
Select,
|
|
1961
1969
|
SelectButton,
|
|
@@ -2064,6 +2072,9 @@ export {
|
|
|
2064
2072
|
TaskTypeIcon,
|
|
2065
2073
|
TelegramIcon,
|
|
2066
2074
|
TermsAgreementLabel,
|
|
2075
|
+
TestResultsSkeleton,
|
|
2076
|
+
TestResultsTable,
|
|
2077
|
+
TestRunResults,
|
|
2067
2078
|
Textarea,
|
|
2068
2079
|
ThemeProvider,
|
|
2069
2080
|
ThinkingDisplay,
|
|
@@ -2084,6 +2095,7 @@ export {
|
|
|
2084
2095
|
TimeTrackerHeaderButton,
|
|
2085
2096
|
TimeTrackerPanel,
|
|
2086
2097
|
TimeTrackerProvider,
|
|
2098
|
+
TimingStat,
|
|
2087
2099
|
TitleBlock,
|
|
2088
2100
|
TitleContentBlock,
|
|
2089
2101
|
ToastCard,
|
|
@@ -2267,6 +2279,7 @@ export {
|
|
|
2267
2279
|
formatRelativeTime,
|
|
2268
2280
|
formatReleaseDate,
|
|
2269
2281
|
formatSingularLookupInvocation,
|
|
2282
|
+
formatTestRunDuration,
|
|
2270
2283
|
formatTicketFullTimestamp,
|
|
2271
2284
|
formatTicketRelativeTime,
|
|
2272
2285
|
formatTimeWithTimezone,
|
|
@@ -2570,6 +2583,7 @@ export {
|
|
|
2570
2583
|
useSseChatAdapter,
|
|
2571
2584
|
useSuppressCloneFocus,
|
|
2572
2585
|
useTablePagination,
|
|
2586
|
+
useTestRunState,
|
|
2573
2587
|
useTheme,
|
|
2574
2588
|
useThemeToggle,
|
|
2575
2589
|
useThrottle,
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
|
4
3
|
import * as React from 'react'
|
|
5
|
-
import {
|
|
4
|
+
import { PenEditIcon, UserIcon, UserPlusIcon } from '../icons-v2-generated'
|
|
6
5
|
import { cn } from '../../utils/cn'
|
|
7
6
|
import { Autocomplete, type AutocompleteOption } from './autocomplete'
|
|
8
|
-
import {
|
|
7
|
+
import { SearchableSelect, type SearchableSelectOption } from './searchable-select'
|
|
9
8
|
import { SquareAvatar } from './square-avatar'
|
|
10
9
|
|
|
11
10
|
export interface TicketAssigneeOption {
|
|
@@ -42,31 +41,33 @@ function CompactAssigneeDropdown({
|
|
|
42
41
|
onAssign,
|
|
43
42
|
className,
|
|
44
43
|
}: AssigneeDropdownProps) {
|
|
45
|
-
const [isOpen, setIsOpen] = React.useState(false)
|
|
46
|
-
const [search, setSearch] = React.useState('')
|
|
47
44
|
const hasAssignee = !!currentAssignee
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
46
|
+
// Current assignee first; SearchableSelect filters but never reorders.
|
|
47
|
+
const orderedOptions = React.useMemo<SearchableSelectOption[]>(() => {
|
|
48
|
+
const withAvatars = options.map(o => ({
|
|
49
|
+
value: o.value,
|
|
50
|
+
label: o.label,
|
|
51
|
+
icon: (
|
|
52
|
+
<SquareAvatar
|
|
53
|
+
src={o.imageUrl}
|
|
54
|
+
alt={o.label}
|
|
55
|
+
fallback={o.label}
|
|
56
|
+
size="sm"
|
|
57
|
+
variant="round"
|
|
58
|
+
className="h-6 w-6 shrink-0"
|
|
59
|
+
/>
|
|
60
|
+
),
|
|
61
|
+
}))
|
|
62
|
+
if (!currentAssignee) return withAvatars
|
|
63
|
+
const current = withAvatars.find(o => o.value === currentAssignee.id)
|
|
64
|
+
if (!current) return withAvatars
|
|
65
|
+
return [current, ...withAvatars.filter(o => o.value !== currentAssignee.id)]
|
|
66
|
+
}, [options, currentAssignee])
|
|
65
67
|
|
|
66
68
|
const handleSelect = (userId: string) => {
|
|
67
|
-
|
|
68
|
-
onAssign(
|
|
69
|
-
setIsOpen(false)
|
|
69
|
+
// Selecting the current assignee again unassigns.
|
|
70
|
+
onAssign(currentAssignee?.id === userId ? null : userId)
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
const trigger = hasAssignee ? (
|
|
@@ -102,73 +103,17 @@ function CompactAssigneeDropdown({
|
|
|
102
103
|
)
|
|
103
104
|
|
|
104
105
|
return (
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
'data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',
|
|
117
|
-
)}
|
|
118
|
-
>
|
|
119
|
-
<div className="border-b border-ods-border">
|
|
120
|
-
<Input
|
|
121
|
-
autoFocus
|
|
122
|
-
value={search}
|
|
123
|
-
onChange={e => setSearch(e.target.value)}
|
|
124
|
-
placeholder="Search users..."
|
|
125
|
-
startAdornment={<SearchIcon className="size-4 text-ods-text-secondary" />}
|
|
126
|
-
className="rounded-none border-0"
|
|
127
|
-
/>
|
|
128
|
-
</div>
|
|
129
|
-
<div className="max-h-80 overflow-y-auto py-[var(--spacing-system-xs)]" role="listbox">
|
|
130
|
-
{isLoading ? (
|
|
131
|
-
<div className="px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-h5 text-ods-text-secondary">
|
|
132
|
-
Loading…
|
|
133
|
-
</div>
|
|
134
|
-
) : orderedOptions.length === 0 ? (
|
|
135
|
-
<div className="px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-h5 text-ods-text-secondary">
|
|
136
|
-
No users found
|
|
137
|
-
</div>
|
|
138
|
-
) : (
|
|
139
|
-
orderedOptions.map(opt => {
|
|
140
|
-
const isCurrent = currentAssignee?.id === opt.value
|
|
141
|
-
return (
|
|
142
|
-
<button
|
|
143
|
-
key={opt.value}
|
|
144
|
-
type="button"
|
|
145
|
-
role="option"
|
|
146
|
-
aria-selected={isCurrent}
|
|
147
|
-
onClick={() => handleSelect(opt.value)}
|
|
148
|
-
className={cn(
|
|
149
|
-
'flex items-center gap-[var(--spacing-system-xs)] w-full px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-left',
|
|
150
|
-
'hover:bg-ods-bg-hover transition-colors',
|
|
151
|
-
isCurrent && 'bg-ods-bg-hover',
|
|
152
|
-
)}
|
|
153
|
-
>
|
|
154
|
-
<SquareAvatar
|
|
155
|
-
src={opt.imageUrl}
|
|
156
|
-
alt={opt.label}
|
|
157
|
-
fallback={opt.label}
|
|
158
|
-
size="sm"
|
|
159
|
-
variant="round"
|
|
160
|
-
className="h-6 w-6 shrink-0"
|
|
161
|
-
/>
|
|
162
|
-
<span className="flex-1 truncate text-h4 text-ods-text-primary" title={opt.label}>{opt.label}</span>
|
|
163
|
-
{isCurrent && <CheckIcon className="size-4 shrink-0 text-ods-accent" />}
|
|
164
|
-
</button>
|
|
165
|
-
)
|
|
166
|
-
})
|
|
167
|
-
)}
|
|
168
|
-
</div>
|
|
169
|
-
</PopoverPrimitive.Content>
|
|
170
|
-
</PopoverPrimitive.Portal>
|
|
171
|
-
</PopoverPrimitive.Root>
|
|
106
|
+
<SearchableSelect
|
|
107
|
+
options={orderedOptions}
|
|
108
|
+
value={currentAssignee?.id ?? null}
|
|
109
|
+
onValueChange={handleSelect}
|
|
110
|
+
searchPlaceholder="Search users..."
|
|
111
|
+
emptyText="No users found"
|
|
112
|
+
isLoading={isLoading}
|
|
113
|
+
trigger={trigger}
|
|
114
|
+
align="end"
|
|
115
|
+
contentClassName="w-72"
|
|
116
|
+
/>
|
|
172
117
|
)
|
|
173
118
|
}
|
|
174
119
|
|
|
@@ -167,6 +167,8 @@ export * from './ticket-status-tag'
|
|
|
167
167
|
export * from './ticket-status-config-row'
|
|
168
168
|
export * from './color-swatch'
|
|
169
169
|
export * from './color-preset-select'
|
|
170
|
+
export * from './searchable-select'
|
|
171
|
+
export * from './test-run'
|
|
170
172
|
export * from './assignee-dropdown'
|
|
171
173
|
export * from './ticket-info-section'
|
|
172
174
|
export * from './ticket-detail-section'
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
import { CheckIcon, SearchIcon } from '../icons-v2-generated'
|
|
6
|
+
import { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon'
|
|
7
|
+
import { cn } from '../../utils/cn'
|
|
8
|
+
import { Input } from './input'
|
|
9
|
+
|
|
10
|
+
export interface SearchableSelectOption {
|
|
11
|
+
value: string
|
|
12
|
+
label: string
|
|
13
|
+
/** Optional leading visual for the option row (avatar, icon, badge). */
|
|
14
|
+
icon?: React.ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SearchableSelectProps {
|
|
18
|
+
options: SearchableSelectOption[]
|
|
19
|
+
/** Currently selected option value; null/undefined when nothing is selected. */
|
|
20
|
+
value?: string | null
|
|
21
|
+
onValueChange: (value: string) => void
|
|
22
|
+
/** Default trigger text while nothing is selected. */
|
|
23
|
+
placeholder?: string
|
|
24
|
+
/** Placeholder of the search input rendered as the first dropdown item. */
|
|
25
|
+
searchPlaceholder?: string
|
|
26
|
+
emptyText?: string
|
|
27
|
+
loadingText?: string
|
|
28
|
+
isLoading?: boolean
|
|
29
|
+
disabled?: boolean
|
|
30
|
+
/**
|
|
31
|
+
* Custom trigger node (rendered via Popover.Trigger asChild). When omitted,
|
|
32
|
+
* a select-like button (Input/SelectTrigger styling, chevron, placeholder)
|
|
33
|
+
* is rendered.
|
|
34
|
+
*/
|
|
35
|
+
trigger?: React.ReactNode
|
|
36
|
+
/** Custom option-row renderer; default is icon + label + selected check. */
|
|
37
|
+
renderOption?: (option: SearchableSelectOption, isSelected: boolean) => React.ReactNode
|
|
38
|
+
align?: 'start' | 'center' | 'end'
|
|
39
|
+
/** Classes for the default trigger button. */
|
|
40
|
+
className?: string
|
|
41
|
+
/** Classes for the popover content (e.g. a fixed width for icon triggers). */
|
|
42
|
+
contentClassName?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Select-style dropdown whose first item is a search field filtering the
|
|
47
|
+
* option list — the pattern used by the ticket assignee picker ("Search
|
|
48
|
+
* users...") generalized for any entity (devices, users, ...). Provide a
|
|
49
|
+
* custom `trigger` for icon-button use cases; the default trigger looks and
|
|
50
|
+
* behaves like `SelectTrigger`.
|
|
51
|
+
*/
|
|
52
|
+
export function SearchableSelect({
|
|
53
|
+
options,
|
|
54
|
+
value = null,
|
|
55
|
+
onValueChange,
|
|
56
|
+
placeholder = 'Select...',
|
|
57
|
+
searchPlaceholder = 'Search...',
|
|
58
|
+
emptyText = 'No results found',
|
|
59
|
+
loadingText = 'Loading…',
|
|
60
|
+
isLoading = false,
|
|
61
|
+
disabled = false,
|
|
62
|
+
trigger,
|
|
63
|
+
renderOption,
|
|
64
|
+
align = 'start',
|
|
65
|
+
className,
|
|
66
|
+
contentClassName,
|
|
67
|
+
}: SearchableSelectProps) {
|
|
68
|
+
const [isOpen, setIsOpen] = React.useState(false)
|
|
69
|
+
const [search, setSearch] = React.useState('')
|
|
70
|
+
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
if (!isOpen) setSearch('')
|
|
73
|
+
}, [isOpen])
|
|
74
|
+
|
|
75
|
+
const filtered = React.useMemo(() => {
|
|
76
|
+
const q = search.trim().toLowerCase()
|
|
77
|
+
if (!q) return options
|
|
78
|
+
return options.filter(o => o.label.toLowerCase().includes(q))
|
|
79
|
+
}, [options, search])
|
|
80
|
+
|
|
81
|
+
const selectedOption = React.useMemo(
|
|
82
|
+
() => (value != null ? options.find(o => o.value === value) : undefined),
|
|
83
|
+
[options, value],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const handleSelect = (optionValue: string) => {
|
|
87
|
+
onValueChange(optionValue)
|
|
88
|
+
setIsOpen(false)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const defaultTrigger = (
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
disabled={disabled}
|
|
95
|
+
className={cn(
|
|
96
|
+
// Mirrors SelectTrigger so the closed state is indistinguishable from
|
|
97
|
+
// the regular Select.
|
|
98
|
+
'flex w-full items-center justify-between gap-2 rounded-md border px-3 h-11 md:h-12 outline-none text-left',
|
|
99
|
+
'text-h4 bg-ods-card border-ods-border',
|
|
100
|
+
selectedOption ? 'text-ods-text-primary' : 'text-ods-text-secondary',
|
|
101
|
+
'enabled:hover:bg-ods-bg-hover enabled:hover:border-ods-border-hover enabled:active:bg-ods-bg-active enabled:active:border-ods-border-active',
|
|
102
|
+
'data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent',
|
|
103
|
+
'group disabled:!cursor-not-allowed disabled:bg-ods-bg transition-colors duration-200 cursor-pointer',
|
|
104
|
+
className,
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
<span className="line-clamp-1">{selectedOption ? selectedOption.label : placeholder}</span>
|
|
108
|
+
<Chevron02DownIcon
|
|
109
|
+
className="shrink-0 text-ods-text-secondary transition-all duration-200 group-data-[state=open]:rotate-180 group-data-[state=open]:text-ods-accent"
|
|
110
|
+
size={24}
|
|
111
|
+
/>
|
|
112
|
+
</button>
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<PopoverPrimitive.Root open={isOpen} onOpenChange={disabled ? undefined : setIsOpen} modal={false}>
|
|
117
|
+
<PopoverPrimitive.Trigger asChild>{trigger ?? defaultTrigger}</PopoverPrimitive.Trigger>
|
|
118
|
+
<PopoverPrimitive.Portal>
|
|
119
|
+
<PopoverPrimitive.Content
|
|
120
|
+
align={align}
|
|
121
|
+
sideOffset={6}
|
|
122
|
+
className={cn(
|
|
123
|
+
'z-50 min-w-[var(--radix-popover-trigger-width)] bg-ods-card border border-ods-border rounded-md shadow-lg overflow-hidden',
|
|
124
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
125
|
+
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
126
|
+
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
127
|
+
'data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',
|
|
128
|
+
contentClassName,
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
<div className="border-b border-ods-border">
|
|
132
|
+
<Input
|
|
133
|
+
autoFocus
|
|
134
|
+
value={search}
|
|
135
|
+
onChange={e => setSearch(e.target.value)}
|
|
136
|
+
placeholder={searchPlaceholder}
|
|
137
|
+
startAdornment={<SearchIcon className="size-4 text-ods-text-secondary" />}
|
|
138
|
+
className="rounded-none border-0"
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="max-h-80 overflow-y-auto py-[var(--spacing-system-xs)]" role="listbox">
|
|
142
|
+
{isLoading ? (
|
|
143
|
+
<div className="px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-h5 text-ods-text-secondary">
|
|
144
|
+
{loadingText}
|
|
145
|
+
</div>
|
|
146
|
+
) : filtered.length === 0 ? (
|
|
147
|
+
<div className="px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-h5 text-ods-text-secondary">
|
|
148
|
+
{emptyText}
|
|
149
|
+
</div>
|
|
150
|
+
) : (
|
|
151
|
+
filtered.map(opt => {
|
|
152
|
+
const isSelected = value === opt.value
|
|
153
|
+
return (
|
|
154
|
+
<button
|
|
155
|
+
key={opt.value}
|
|
156
|
+
type="button"
|
|
157
|
+
role="option"
|
|
158
|
+
aria-selected={isSelected}
|
|
159
|
+
onClick={() => handleSelect(opt.value)}
|
|
160
|
+
className={cn(
|
|
161
|
+
'flex items-center gap-[var(--spacing-system-xs)] w-full px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-left',
|
|
162
|
+
'hover:bg-ods-bg-hover transition-colors',
|
|
163
|
+
isSelected && 'bg-ods-bg-hover',
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
{renderOption ? (
|
|
167
|
+
renderOption(opt, isSelected)
|
|
168
|
+
) : (
|
|
169
|
+
<>
|
|
170
|
+
{opt.icon}
|
|
171
|
+
<span className="flex-1 truncate text-h4 text-ods-text-primary" title={opt.label}>
|
|
172
|
+
{opt.label}
|
|
173
|
+
</span>
|
|
174
|
+
{isSelected && <CheckIcon className="size-4 shrink-0 text-ods-accent" />}
|
|
175
|
+
</>
|
|
176
|
+
)}
|
|
177
|
+
</button>
|
|
178
|
+
)
|
|
179
|
+
})
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
</PopoverPrimitive.Content>
|
|
183
|
+
</PopoverPrimitive.Portal>
|
|
184
|
+
</PopoverPrimitive.Root>
|
|
185
|
+
)
|
|
186
|
+
}
|