@eggspot/ui 0.0.0
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/eslint.config.js +4 -0
- package/package.json +66 -0
- package/postcss.config.mjs +1 -0
- package/src/components/Button.machine.tsx +50 -0
- package/src/components/Button.tsx +249 -0
- package/src/components/Button.variants.tsx +186 -0
- package/src/components/ButtonGroup.tsx +56 -0
- package/src/components/Calendar.tsx +275 -0
- package/src/components/Calendar.utils.tsx +22 -0
- package/src/components/Checkbox.tsx +199 -0
- package/src/components/ConfirmDialog.tsx +183 -0
- package/src/components/DashboardLayout/DashboardLayout.tsx +348 -0
- package/src/components/DashboardLayout/SidebarNav.tsx +509 -0
- package/src/components/DashboardLayout/index.ts +33 -0
- package/src/components/DataTable/DataTable.tsx +557 -0
- package/src/components/DataTable/DataTableColumnHeader.tsx +122 -0
- package/src/components/DataTable/DataTableDisplaySettings.tsx +265 -0
- package/src/components/DataTable/DataTableFloatingBar.tsx +44 -0
- package/src/components/DataTable/DataTablePagination.tsx +168 -0
- package/src/components/DataTable/DataTableStates.tsx +69 -0
- package/src/components/DataTable/DataTableToolbarContainer.tsx +47 -0
- package/src/components/DataTable/hooks/use-data-table-settings.ts +101 -0
- package/src/components/DataTable/index.ts +7 -0
- package/src/components/DataTable/types/data-table.ts +97 -0
- package/src/components/DatePicker.tsx +213 -0
- package/src/components/DatePicker.utils.tsx +38 -0
- package/src/components/Datefield.tsx +109 -0
- package/src/components/Datefield.utils.ts +10 -0
- package/src/components/Dialog.tsx +167 -0
- package/src/components/Field.tsx +49 -0
- package/src/components/Filter/Filter.store.tsx +122 -0
- package/src/components/Filter/Filter.tsx +11 -0
- package/src/components/Filter/Filter.types.ts +107 -0
- package/src/components/Filter/FilterBar.tsx +38 -0
- package/src/components/Filter/FilterBuilder.tsx +158 -0
- package/src/components/Filter/FilterField/DateModeRowValue.tsx +250 -0
- package/src/components/Filter/FilterField/FilterAsyncSelect.tsx +191 -0
- package/src/components/Filter/FilterField/FilterDateMode.tsx +241 -0
- package/src/components/Filter/FilterField/FilterDateRange.tsx +169 -0
- package/src/components/Filter/FilterField/FilterSelect.tsx +208 -0
- package/src/components/Filter/FilterField/FilterSingleDate.tsx +277 -0
- package/src/components/Filter/FilterField/OptionItem.tsx +112 -0
- package/src/components/Filter/FilterField/index.ts +6 -0
- package/src/components/Filter/FilterRow.tsx +527 -0
- package/src/components/Filter/index.ts +17 -0
- package/src/components/Form.tsx +195 -0
- package/src/components/Heading.tsx +41 -0
- package/src/components/Input.tsx +221 -0
- package/src/components/InputOTP.tsx +78 -0
- package/src/components/Label.tsx +65 -0
- package/src/components/Layout.tsx +129 -0
- package/src/components/ListBox.tsx +97 -0
- package/src/components/Menu.tsx +152 -0
- package/src/components/NativeSelect.tsx +77 -0
- package/src/components/NumberInput.tsx +114 -0
- package/src/components/Popover.tsx +44 -0
- package/src/components/Provider.tsx +22 -0
- package/src/components/RadioGroup.tsx +191 -0
- package/src/components/Resizable.tsx +71 -0
- package/src/components/ScrollArea.tsx +57 -0
- package/src/components/Select.tsx +626 -0
- package/src/components/Select.utils.tsx +64 -0
- package/src/components/Separator.tsx +25 -0
- package/src/components/Sheet.tsx +147 -0
- package/src/components/Sonner.tsx +96 -0
- package/src/components/Spinner.tsx +30 -0
- package/src/components/Switch.tsx +51 -0
- package/src/components/Text.tsx +35 -0
- package/src/components/Tooltip.tsx +58 -0
- package/src/consts/config.ts +2 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/lib/utils.ts +10 -0
- package/tsconfig.json +11 -0
- package/tsconfig.lint.json +8 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
import { Button } from "@eggspot/ui/components/Button"
|
|
5
|
+
import { Checkbox } from "@eggspot/ui/components/Checkbox"
|
|
6
|
+
import { fieldGroupVariants } from "@eggspot/ui/components/Field"
|
|
7
|
+
import { SearchInput } from "@eggspot/ui/components/Input"
|
|
8
|
+
import { Label } from "@eggspot/ui/components/Label"
|
|
9
|
+
import { Popover, PopoverDialog } from "@eggspot/ui/components/Popover"
|
|
10
|
+
import { useAriaSelectProps } from "@eggspot/ui/components/Select.utils"
|
|
11
|
+
import { Spinner } from "@eggspot/ui/components/Spinner"
|
|
12
|
+
import { cn } from "@eggspot/ui/lib/utils"
|
|
13
|
+
import { omit } from "lodash"
|
|
14
|
+
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"
|
|
15
|
+
import type { ListBoxItemProps } from "react-aria-components"
|
|
16
|
+
import {
|
|
17
|
+
Button as AriaButton,
|
|
18
|
+
PopoverProps as AriaPopoverProps,
|
|
19
|
+
Select as AriaSelect,
|
|
20
|
+
SelectProps as AriaSelectProps,
|
|
21
|
+
Autocomplete,
|
|
22
|
+
AutocompleteStateContext,
|
|
23
|
+
Collection,
|
|
24
|
+
DialogTrigger,
|
|
25
|
+
ListBox,
|
|
26
|
+
ListBoxItem,
|
|
27
|
+
ListBoxLoadMoreItem,
|
|
28
|
+
ListLayout,
|
|
29
|
+
Pressable,
|
|
30
|
+
SelectStateContext,
|
|
31
|
+
SelectValue,
|
|
32
|
+
useFilter,
|
|
33
|
+
Virtualizer,
|
|
34
|
+
} from "react-aria-components"
|
|
35
|
+
|
|
36
|
+
const SELECT_ALL_KEYWORD =
|
|
37
|
+
"this_is_keyword_for_select_all_checkbox_asljqdkasjdlkajsd"
|
|
38
|
+
|
|
39
|
+
interface SelectOption<T = string | number> {
|
|
40
|
+
id?: string
|
|
41
|
+
value: T
|
|
42
|
+
label: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface SelectProps<T extends SelectOption, M extends "single" | "multiple">
|
|
46
|
+
extends Omit<
|
|
47
|
+
AriaSelectProps<T>,
|
|
48
|
+
"children" | "onChange" | "value" | "defaultValue" | "selectionMode"
|
|
49
|
+
> {
|
|
50
|
+
/** The selection mode for the select. */
|
|
51
|
+
selectionMode?: M
|
|
52
|
+
|
|
53
|
+
/** Initial value used when the component is uncontrolled. */
|
|
54
|
+
defaultValue?: M extends "multiple" ? Array<T> : T
|
|
55
|
+
|
|
56
|
+
/** If this prop is set, the select operates in a controlled manner and uses this value as its state. */
|
|
57
|
+
value?: M extends "multiple" ? Array<T> : T
|
|
58
|
+
|
|
59
|
+
/** If this prop is set, the select will call this function with the new value whenever the value changes. */
|
|
60
|
+
onChange?: M extends "multiple"
|
|
61
|
+
? (value: Array<T>) => void
|
|
62
|
+
: (value: T) => void
|
|
63
|
+
|
|
64
|
+
/** Information for managing async option loading. */
|
|
65
|
+
optionsLoader?: {
|
|
66
|
+
/** Whether there are more pages of options to load. */
|
|
67
|
+
hasNextPage: boolean
|
|
68
|
+
|
|
69
|
+
/** Callback to fetch the next page of options. */
|
|
70
|
+
onFetchNextPage: () => void
|
|
71
|
+
|
|
72
|
+
/** Whether the next page of options is currently being fetched. */
|
|
73
|
+
isFetching?: boolean
|
|
74
|
+
|
|
75
|
+
/** A function to search for options. */
|
|
76
|
+
onSearch?: (search: string) => void
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The array of options to display in the select dropdown.
|
|
81
|
+
*/
|
|
82
|
+
options?: Array<T>
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* If true, enables a search field for filtering options.
|
|
86
|
+
*/
|
|
87
|
+
isSearchable?: boolean
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Custom render function for the selected value display.
|
|
91
|
+
*/
|
|
92
|
+
renderValue?: (value: T) => React.ReactNode
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Custom render function for each option in the dropdown.
|
|
96
|
+
*/
|
|
97
|
+
renderOption?: (item: T) => React.ReactNode
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The maximum number of badges to display. To show all badges, set to Infinity.
|
|
101
|
+
*/
|
|
102
|
+
maxVisibleBadges?: number
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* If true, the clear button will be shown.
|
|
106
|
+
*/
|
|
107
|
+
isClearable?: boolean
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The message to display when there are no options found.
|
|
111
|
+
*/
|
|
112
|
+
emptyMessage?: string
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The label to display above the select.
|
|
116
|
+
*/
|
|
117
|
+
label?: string
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* If true, an asterisk will be displayed next to the label.
|
|
121
|
+
*/
|
|
122
|
+
withAsterisk?: boolean
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The tooltip for the select.
|
|
126
|
+
*/
|
|
127
|
+
tooltip?: React.ReactNode
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The keyword to search for.
|
|
131
|
+
*/
|
|
132
|
+
keyword?: keyof T
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The action to perform when the create new button is clicked.
|
|
136
|
+
*/
|
|
137
|
+
onCreate?: (value: string) => void
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The class name for the trigger button.
|
|
141
|
+
*/
|
|
142
|
+
triggerClassName?: string
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The class name for the popover.
|
|
146
|
+
*/
|
|
147
|
+
popoverProps?: AriaPopoverProps
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function Select<
|
|
151
|
+
T extends SelectOption,
|
|
152
|
+
M extends "single" | "multiple" = "single",
|
|
153
|
+
>(props: SelectProps<T, M>) {
|
|
154
|
+
const {
|
|
155
|
+
value: controlledValue,
|
|
156
|
+
onChange: controlledOnChange,
|
|
157
|
+
defaultValue,
|
|
158
|
+
optionsLoader,
|
|
159
|
+
options,
|
|
160
|
+
renderOption,
|
|
161
|
+
renderValue,
|
|
162
|
+
isClearable = true,
|
|
163
|
+
maxVisibleBadges = 2,
|
|
164
|
+
placeholder = "Select",
|
|
165
|
+
isSearchable = true,
|
|
166
|
+
emptyMessage = "No results found",
|
|
167
|
+
label,
|
|
168
|
+
withAsterisk,
|
|
169
|
+
tooltip,
|
|
170
|
+
keyword,
|
|
171
|
+
onCreate,
|
|
172
|
+
triggerClassName,
|
|
173
|
+
popoverProps,
|
|
174
|
+
...restProps
|
|
175
|
+
} = props
|
|
176
|
+
const [isOpen, setIsOpen] = React.useState(false)
|
|
177
|
+
const selectionMode = props.selectionMode || "single"
|
|
178
|
+
const isAsync = Boolean(optionsLoader)
|
|
179
|
+
const optionsWithId = options?.map((option) => ({
|
|
180
|
+
...option,
|
|
181
|
+
id: JSON.stringify(option),
|
|
182
|
+
}))
|
|
183
|
+
|
|
184
|
+
const ariaProps = useAriaSelectProps({
|
|
185
|
+
value: controlledValue,
|
|
186
|
+
defaultValue: defaultValue,
|
|
187
|
+
onChange: controlledOnChange,
|
|
188
|
+
selectionMode: selectionMode,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<AriaSelect
|
|
193
|
+
isOpen={isOpen}
|
|
194
|
+
onOpenChange={(isOpen) => {
|
|
195
|
+
if (!isOpen) {
|
|
196
|
+
setIsOpen(false)
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
optionsLoader?.onSearch?.("")
|
|
199
|
+
}, 100)
|
|
200
|
+
}
|
|
201
|
+
}}
|
|
202
|
+
aria-label="Select"
|
|
203
|
+
className={cn("group relative flex w-full flex-col gap-1.5")}
|
|
204
|
+
{...ariaProps}
|
|
205
|
+
{...restProps}
|
|
206
|
+
>
|
|
207
|
+
{label && (
|
|
208
|
+
<Label withAsterisk={withAsterisk} tooltip={tooltip}>
|
|
209
|
+
{label}
|
|
210
|
+
</Label>
|
|
211
|
+
)}
|
|
212
|
+
<AriaButton
|
|
213
|
+
className={cn(
|
|
214
|
+
fieldGroupVariants(),
|
|
215
|
+
"data-focus-visible:ring-accent-9 h-auto min-h-8 cursor-pointer py-1 pr-4 data-focus-visible:ring-2 data-focused:outline-none",
|
|
216
|
+
"relative flex justify-between",
|
|
217
|
+
"group-data-[invalid]:ring-error-7 group-data-[disabled]:cursor-not-allowed group-data-[disabled]:opacity-70",
|
|
218
|
+
"aria-[expanded=true]:ring-accent-9 aria-[expanded=true]:ring-2",
|
|
219
|
+
triggerClassName
|
|
220
|
+
)}
|
|
221
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
222
|
+
>
|
|
223
|
+
<SelectValue<T> className="truncate">
|
|
224
|
+
{() => {
|
|
225
|
+
const selectedItems = [ariaProps.value]
|
|
226
|
+
.flat()
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
.map((v) => JSON.parse(v))
|
|
229
|
+
const isPlaceholder = selectedItems.length === 0
|
|
230
|
+
|
|
231
|
+
if (isPlaceholder) {
|
|
232
|
+
// If placeholder is not set, return an empty div
|
|
233
|
+
if (!placeholder) {
|
|
234
|
+
return (
|
|
235
|
+
<div className="opacity-0" aria-hidden="true">
|
|
236
|
+
|
|
237
|
+
</div>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return <div className="text-gray-11">{placeholder}</div>
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (selectionMode === "single") {
|
|
245
|
+
const selectedItem = selectedItems[0]
|
|
246
|
+
|
|
247
|
+
if (!selectedItem) return null
|
|
248
|
+
|
|
249
|
+
return renderValue
|
|
250
|
+
? renderValue(selectedItem)
|
|
251
|
+
: selectedItem?.label
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (selectionMode === "multiple") {
|
|
255
|
+
const selectedItemsWithId = selectedItems.map((item) => ({
|
|
256
|
+
...item,
|
|
257
|
+
id: JSON.stringify(item),
|
|
258
|
+
}))
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<div className="flex flex-1 flex-wrap gap-1 pr-4">
|
|
262
|
+
{selectedItemsWithId
|
|
263
|
+
?.slice(0, maxVisibleBadges)
|
|
264
|
+
.map((item) => {
|
|
265
|
+
if (!item) return null
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<SelectBadge key={item?.id} title={item.label}>
|
|
269
|
+
<div className="truncate">
|
|
270
|
+
{renderValue ? renderValue(item) : item.label}
|
|
271
|
+
</div>
|
|
272
|
+
<BadgeClearButton data={item} />
|
|
273
|
+
</SelectBadge>
|
|
274
|
+
)
|
|
275
|
+
})}
|
|
276
|
+
|
|
277
|
+
{/* Remaining badges count */}
|
|
278
|
+
{!!selectedItemsWithId?.length &&
|
|
279
|
+
selectedItemsWithId.length > maxVisibleBadges && (
|
|
280
|
+
<RemainingBadges
|
|
281
|
+
items={selectedItemsWithId.slice(maxVisibleBadges)}
|
|
282
|
+
/>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
}}
|
|
288
|
+
</SelectValue>
|
|
289
|
+
<ChevronDownIcon className="text-gray-11 size-4 translate-x-1" />
|
|
290
|
+
{isClearable && <SelectClearButton />}
|
|
291
|
+
</AriaButton>
|
|
292
|
+
<Popover
|
|
293
|
+
className={cn(
|
|
294
|
+
"w-(--trigger-width) overflow-hidden",
|
|
295
|
+
popoverProps?.className
|
|
296
|
+
)}
|
|
297
|
+
{...omit(popoverProps, "className")}
|
|
298
|
+
>
|
|
299
|
+
<div className="flex flex-col">
|
|
300
|
+
<ItemsWrapper
|
|
301
|
+
isAsync={isAsync}
|
|
302
|
+
isSearchable={isSearchable}
|
|
303
|
+
selectionMode={selectionMode}
|
|
304
|
+
manualSearching={!!optionsLoader}
|
|
305
|
+
onSearch={optionsLoader?.onSearch}
|
|
306
|
+
>
|
|
307
|
+
<div>
|
|
308
|
+
<Virtualizer
|
|
309
|
+
layout={ListLayout}
|
|
310
|
+
layoutOptions={{
|
|
311
|
+
estimatedRowHeight: 36,
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
<ListBox
|
|
315
|
+
className="!max-h-[250px] flex-1 scroll-pb-1 overflow-y-auto outline-hidden"
|
|
316
|
+
renderEmptyState={() => {
|
|
317
|
+
if (optionsLoader?.isFetching) return null
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<div className="text-gray-11 flex h-20 items-center justify-center text-center text-sm">
|
|
321
|
+
{emptyMessage}
|
|
322
|
+
</div>
|
|
323
|
+
)
|
|
324
|
+
}}
|
|
325
|
+
>
|
|
326
|
+
{selectionMode === "multiple" && !isAsync && (
|
|
327
|
+
<SelectAllCheckbox
|
|
328
|
+
value={ariaProps.value}
|
|
329
|
+
onChange={ariaProps.onChange}
|
|
330
|
+
options={optionsWithId as Array<Record<string, any>>}
|
|
331
|
+
/>
|
|
332
|
+
)}
|
|
333
|
+
<Collection items={optionsWithId}>
|
|
334
|
+
{(item) => (
|
|
335
|
+
<SelectItem
|
|
336
|
+
selectionMode={selectionMode}
|
|
337
|
+
renderOption={renderOption}
|
|
338
|
+
textValue={(item as any)[keyword || "label"]}
|
|
339
|
+
>
|
|
340
|
+
{item.label}
|
|
341
|
+
</SelectItem>
|
|
342
|
+
)}
|
|
343
|
+
</Collection>
|
|
344
|
+
<ListBoxLoadMoreItem
|
|
345
|
+
onLoadMore={
|
|
346
|
+
optionsLoader?.hasNextPage
|
|
347
|
+
? optionsLoader?.onFetchNextPage
|
|
348
|
+
: undefined
|
|
349
|
+
}
|
|
350
|
+
isLoading={optionsLoader?.isFetching}
|
|
351
|
+
className="flex items-center justify-center pt-1.5"
|
|
352
|
+
>
|
|
353
|
+
<Spinner className="text-gray-11/50 size-5" />
|
|
354
|
+
</ListBoxLoadMoreItem>
|
|
355
|
+
</ListBox>
|
|
356
|
+
</Virtualizer>
|
|
357
|
+
</div>
|
|
358
|
+
<CreateNew onCreate={onCreate} />
|
|
359
|
+
</ItemsWrapper>
|
|
360
|
+
</div>
|
|
361
|
+
</Popover>
|
|
362
|
+
</AriaSelect>
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function SelectBadge({ className, ...props }: React.ComponentProps<"span">) {
|
|
367
|
+
return (
|
|
368
|
+
<span
|
|
369
|
+
className={cn(
|
|
370
|
+
"text-gray-12 bg-gray-4 inline-flex items-center justify-center truncate rounded-sm px-2 py-0.5 text-xs font-medium",
|
|
371
|
+
className
|
|
372
|
+
)}
|
|
373
|
+
{...props}
|
|
374
|
+
/>
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function RemainingBadges({
|
|
379
|
+
items,
|
|
380
|
+
renderValue,
|
|
381
|
+
}: {
|
|
382
|
+
items: Array<SelectOption>
|
|
383
|
+
renderValue?: (item: SelectOption) => React.ReactNode
|
|
384
|
+
}) {
|
|
385
|
+
const [isOpen, setIsOpen] = React.useState(false)
|
|
386
|
+
|
|
387
|
+
return (
|
|
388
|
+
<DialogTrigger>
|
|
389
|
+
<Pressable>
|
|
390
|
+
<SelectBadge
|
|
391
|
+
title="Show more options"
|
|
392
|
+
className="hover:bg-gray-3 focus-visible:outline-none"
|
|
393
|
+
onClick={(e) => {
|
|
394
|
+
e.stopPropagation()
|
|
395
|
+
setIsOpen(!isOpen)
|
|
396
|
+
}}
|
|
397
|
+
>
|
|
398
|
+
<span>{`+${items.length}`}</span>
|
|
399
|
+
</SelectBadge>
|
|
400
|
+
</Pressable>
|
|
401
|
+
<Popover
|
|
402
|
+
isOpen={isOpen}
|
|
403
|
+
onOpenChange={setIsOpen}
|
|
404
|
+
placement="bottom left"
|
|
405
|
+
className="overflow-hidden rounded-md"
|
|
406
|
+
>
|
|
407
|
+
<PopoverDialog className="p-0">
|
|
408
|
+
<div className="flex max-h-[250px] max-w-[260px] min-w-[150px] flex-col gap-1.5 overflow-y-auto p-2">
|
|
409
|
+
{items.map((item) => (
|
|
410
|
+
<SelectBadge key={item?.id} className="w-full justify-between">
|
|
411
|
+
<div className="truncate" title={item.label}>
|
|
412
|
+
{renderValue ? renderValue(item) : item.label}
|
|
413
|
+
</div>
|
|
414
|
+
<BadgeClearButton data={item} />
|
|
415
|
+
</SelectBadge>
|
|
416
|
+
))}
|
|
417
|
+
</div>
|
|
418
|
+
</PopoverDialog>
|
|
419
|
+
</Popover>
|
|
420
|
+
</DialogTrigger>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
interface ItemsWrapperProps {
|
|
425
|
+
children: React.ReactNode
|
|
426
|
+
isSearchable: boolean
|
|
427
|
+
selectionMode: "single" | "multiple"
|
|
428
|
+
manualSearching?: boolean
|
|
429
|
+
onSearch?: (search: string) => void
|
|
430
|
+
isAsync?: boolean
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function ItemsWrapper({
|
|
434
|
+
children,
|
|
435
|
+
isSearchable,
|
|
436
|
+
manualSearching,
|
|
437
|
+
onSearch,
|
|
438
|
+
}: ItemsWrapperProps) {
|
|
439
|
+
const { contains } = useFilter({ sensitivity: "base" })
|
|
440
|
+
|
|
441
|
+
const customFilter = (textValue: string, inputValue: string) => {
|
|
442
|
+
if (textValue === SELECT_ALL_KEYWORD) return true
|
|
443
|
+
|
|
444
|
+
return contains(textValue, inputValue)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return isSearchable ? (
|
|
448
|
+
<Autocomplete
|
|
449
|
+
disableVirtualFocus
|
|
450
|
+
filter={manualSearching ? undefined : customFilter}
|
|
451
|
+
>
|
|
452
|
+
<SearchInput
|
|
453
|
+
autoFocus
|
|
454
|
+
placeholder="Search"
|
|
455
|
+
className="rounded-none border-b ring-0!"
|
|
456
|
+
onChange={onSearch}
|
|
457
|
+
/>
|
|
458
|
+
<div className="p-1.5">{children}</div>
|
|
459
|
+
</Autocomplete>
|
|
460
|
+
) : (
|
|
461
|
+
<div className="p-1.5">{children}</div>
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
interface SelectAllCheckboxProps {
|
|
466
|
+
value: Array<Record<string, any>>
|
|
467
|
+
onChange: (value: Array<Record<string, any>> | null) => void
|
|
468
|
+
options: Array<Record<string, any>>
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function SelectAllCheckbox({
|
|
472
|
+
value,
|
|
473
|
+
onChange,
|
|
474
|
+
options,
|
|
475
|
+
}: SelectAllCheckboxProps) {
|
|
476
|
+
const state = React.useContext(SelectStateContext)
|
|
477
|
+
|
|
478
|
+
const isAllSelected = value?.length === options?.length
|
|
479
|
+
const isIndeterminate = !!value?.length && value?.length < options?.length
|
|
480
|
+
|
|
481
|
+
return (
|
|
482
|
+
<ListBoxItem
|
|
483
|
+
textValue={SELECT_ALL_KEYWORD}
|
|
484
|
+
onClick={(e) => {
|
|
485
|
+
e.stopPropagation()
|
|
486
|
+
if (isAllSelected) {
|
|
487
|
+
onChange([])
|
|
488
|
+
} else {
|
|
489
|
+
const ids = options.map((option) => option.id)
|
|
490
|
+
onChange(ids)
|
|
491
|
+
}
|
|
492
|
+
}}
|
|
493
|
+
className={cn(
|
|
494
|
+
"group text-gray-12 flex cursor-pointer items-center gap-2 rounded-md px-2 py-2 text-sm outline-hidden select-none",
|
|
495
|
+
"data-focused:bg-gray-3"
|
|
496
|
+
)}
|
|
497
|
+
>
|
|
498
|
+
<Checkbox
|
|
499
|
+
readOnly
|
|
500
|
+
reduceMotion
|
|
501
|
+
isSelected={isAllSelected}
|
|
502
|
+
isIndeterminate={isIndeterminate}
|
|
503
|
+
/>
|
|
504
|
+
Select All
|
|
505
|
+
</ListBoxItem>
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
interface CreateNewProps {
|
|
510
|
+
onCreate?: (value: string) => void
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function CreateNew({ onCreate }: CreateNewProps) {
|
|
514
|
+
const selectState = React.useContext(SelectStateContext)
|
|
515
|
+
const autocompleteState = React.useContext(AutocompleteStateContext)
|
|
516
|
+
const inputValue = autocompleteState?.inputValue || ""
|
|
517
|
+
|
|
518
|
+
if (!onCreate) return null
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<div className="py-1.5">
|
|
522
|
+
<Button
|
|
523
|
+
fullWidth
|
|
524
|
+
variant="minimal"
|
|
525
|
+
intent="secondary"
|
|
526
|
+
isDisabled={!inputValue}
|
|
527
|
+
onClick={() => {
|
|
528
|
+
onCreate(inputValue)
|
|
529
|
+
selectState?.close()
|
|
530
|
+
}}
|
|
531
|
+
>
|
|
532
|
+
Create new
|
|
533
|
+
</Button>
|
|
534
|
+
</div>
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function SelectItem<T extends SelectOption>({
|
|
539
|
+
children,
|
|
540
|
+
renderOption,
|
|
541
|
+
selectionMode = "multiple",
|
|
542
|
+
...props
|
|
543
|
+
}: ListBoxItemProps & {
|
|
544
|
+
children: string
|
|
545
|
+
renderOption?: (item: T) => React.ReactNode
|
|
546
|
+
selectionMode: "single" | "multiple"
|
|
547
|
+
}) {
|
|
548
|
+
const content = (
|
|
549
|
+
<div className="flex-1 overflow-hidden text-sm font-normal">
|
|
550
|
+
<div className="truncate">
|
|
551
|
+
{renderOption ? renderOption(props.value as T) : children}
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
return (
|
|
557
|
+
<ListBoxItem
|
|
558
|
+
{...props}
|
|
559
|
+
className={cn(
|
|
560
|
+
"group text-gray-12 flex cursor-pointer items-center gap-2 rounded-md px-2 py-2 outline-hidden select-none",
|
|
561
|
+
"data-focused:bg-gray-3"
|
|
562
|
+
)}
|
|
563
|
+
>
|
|
564
|
+
{({ isSelected }) => (
|
|
565
|
+
<>
|
|
566
|
+
{selectionMode === "single" &&
|
|
567
|
+
(isSelected ? <CheckIcon size={16} /> : <div className="w-4" />)}
|
|
568
|
+
{selectionMode === "multiple" && (
|
|
569
|
+
<Checkbox readOnly reduceMotion isSelected={isSelected} />
|
|
570
|
+
)}
|
|
571
|
+
{content}
|
|
572
|
+
</>
|
|
573
|
+
)}
|
|
574
|
+
</ListBoxItem>
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function SelectClearButton() {
|
|
579
|
+
const state = React.useContext(SelectStateContext)
|
|
580
|
+
const value = state?.value as string | Array<number | string>
|
|
581
|
+
|
|
582
|
+
if (!value || value.length === 0) return null
|
|
583
|
+
|
|
584
|
+
return (
|
|
585
|
+
<div
|
|
586
|
+
role="button"
|
|
587
|
+
tabIndex={0}
|
|
588
|
+
onClick={(e) => {
|
|
589
|
+
e.stopPropagation()
|
|
590
|
+
state?.setValue(null)
|
|
591
|
+
}}
|
|
592
|
+
className={cn(
|
|
593
|
+
"focus-visible:ring-2 focus-visible:outline-none",
|
|
594
|
+
"bg-gray-2 text-gray-11 hover:bg-gray-3 z-10 flex size-6 cursor-pointer items-center justify-center rounded-sm",
|
|
595
|
+
"absolute top-1/2 right-8 -translate-y-1/2"
|
|
596
|
+
)}
|
|
597
|
+
>
|
|
598
|
+
<XIcon className="text-gray-11 size-3.5" />
|
|
599
|
+
</div>
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function BadgeClearButton({ data }: { data: SelectOption }) {
|
|
604
|
+
const state = React.useContext(SelectStateContext)
|
|
605
|
+
const value = state?.value as string | Array<number | string>
|
|
606
|
+
|
|
607
|
+
if (!Array.isArray(value)) return null
|
|
608
|
+
|
|
609
|
+
return (
|
|
610
|
+
<div
|
|
611
|
+
role="button"
|
|
612
|
+
tabIndex={0}
|
|
613
|
+
className="z-10 -mr-1.5 flex size-4! shrink-0 cursor-pointer items-center justify-center rounded-sm bg-transparent hover:bg-neutral-400/15 focus-visible:ring-2 focus-visible:outline-none"
|
|
614
|
+
onClick={(e) => {
|
|
615
|
+
e.stopPropagation()
|
|
616
|
+
const newKeys = value.filter((v) => v !== data.id)
|
|
617
|
+
state?.setValue(newKeys)
|
|
618
|
+
}}
|
|
619
|
+
>
|
|
620
|
+
<XIcon className="size-2.5!" />
|
|
621
|
+
</div>
|
|
622
|
+
)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export { Select }
|
|
626
|
+
export type { SelectOption }
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useControllableState } from "@radix-ui/react-use-controllable-state"
|
|
2
|
+
|
|
3
|
+
interface UseAriaSelectProps {
|
|
4
|
+
/**object or array of objects */
|
|
5
|
+
value: any
|
|
6
|
+
|
|
7
|
+
/**function that takes a value and returns void */
|
|
8
|
+
onChange?: (value: any) => void
|
|
9
|
+
|
|
10
|
+
/**object or array of objects */
|
|
11
|
+
defaultValue: any
|
|
12
|
+
|
|
13
|
+
/**single or multiple */
|
|
14
|
+
selectionMode: "single" | "multiple"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Because React Aria only supports string values, we need to convert the value and onChange to use strings.
|
|
18
|
+
// When the user selects a value, we need to convert the string back into an object.
|
|
19
|
+
|
|
20
|
+
export function useAriaSelectProps({
|
|
21
|
+
value: controlledValue,
|
|
22
|
+
onChange: controlledOnChange,
|
|
23
|
+
defaultValue,
|
|
24
|
+
selectionMode,
|
|
25
|
+
}: UseAriaSelectProps): any {
|
|
26
|
+
const [internalValue, internalOnChange] = useControllableState({
|
|
27
|
+
prop: controlledValue,
|
|
28
|
+
defaultProp: defaultValue,
|
|
29
|
+
onChange: controlledOnChange,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const getValue = () => {
|
|
34
|
+
if (selectionMode === "single") {
|
|
35
|
+
return internalValue ? JSON.stringify(internalValue) : null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return internalValue?.map((v: object) => JSON.stringify(v))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const onChange = (v: any) => {
|
|
42
|
+
if (selectionMode === "single") {
|
|
43
|
+
internalOnChange(v ? JSON.parse(v as string) : null)
|
|
44
|
+
} else {
|
|
45
|
+
internalOnChange(
|
|
46
|
+
(v as string[])
|
|
47
|
+
?.map((v) => {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(v)
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { value: getValue(), onChange }
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(error)
|
|
62
|
+
return {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@eggspot/ui/lib/utils"
|
|
4
|
+
import {
|
|
5
|
+
Separator as AriaSeparator,
|
|
6
|
+
SeparatorProps as AriaSeparatorProps,
|
|
7
|
+
} from "react-aria-components"
|
|
8
|
+
|
|
9
|
+
const Separator = ({
|
|
10
|
+
className,
|
|
11
|
+
orientation = "horizontal",
|
|
12
|
+
...props
|
|
13
|
+
}: AriaSeparatorProps) => (
|
|
14
|
+
<AriaSeparator
|
|
15
|
+
orientation={orientation}
|
|
16
|
+
className={cn(
|
|
17
|
+
"border-gray-6 bg-gray-6",
|
|
18
|
+
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export { Separator }
|