@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,275 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useContext } from "react"
|
|
4
|
+
import { buttonVariants } from "@eggspot/ui/components/Button"
|
|
5
|
+
import {
|
|
6
|
+
getCalendarMonths,
|
|
7
|
+
getCalendarYears,
|
|
8
|
+
} from "@eggspot/ui/components/Calendar.utils"
|
|
9
|
+
import {
|
|
10
|
+
NativeSelect,
|
|
11
|
+
NativeSelectOption,
|
|
12
|
+
} from "@eggspot/ui/components/NativeSelect"
|
|
13
|
+
import { cn } from "@eggspot/ui/lib/utils"
|
|
14
|
+
import {
|
|
15
|
+
CalendarDate,
|
|
16
|
+
getLocalTimeZone,
|
|
17
|
+
parseDate,
|
|
18
|
+
today,
|
|
19
|
+
} from "@internationalized/date"
|
|
20
|
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
21
|
+
import {
|
|
22
|
+
Button as AriaButton,
|
|
23
|
+
Calendar as AriaCalendar,
|
|
24
|
+
CalendarCell as AriaCalendarCell,
|
|
25
|
+
CalendarCellProps as AriaCalendarCellProps,
|
|
26
|
+
CalendarGrid as AriaCalendarGrid,
|
|
27
|
+
CalendarGridBody as AriaCalendarGridBody,
|
|
28
|
+
CalendarGridBodyProps as AriaCalendarGridBodyProps,
|
|
29
|
+
CalendarGridHeader as AriaCalendarGridHeader,
|
|
30
|
+
CalendarGridHeaderProps as AriaCalendarGridHeaderProps,
|
|
31
|
+
CalendarGridProps as AriaCalendarGridProps,
|
|
32
|
+
CalendarHeaderCell as AriaCalendarHeaderCell,
|
|
33
|
+
CalendarHeaderCellProps as AriaCalendarHeaderCellProps,
|
|
34
|
+
RangeCalendar as AriaRangeCalendar,
|
|
35
|
+
CalendarStateContext,
|
|
36
|
+
composeRenderProps,
|
|
37
|
+
RangeCalendarStateContext,
|
|
38
|
+
useLocale,
|
|
39
|
+
} from "react-aria-components"
|
|
40
|
+
|
|
41
|
+
const Calendar = AriaCalendar
|
|
42
|
+
|
|
43
|
+
const RangeCalendar = AriaRangeCalendar
|
|
44
|
+
|
|
45
|
+
const months = getCalendarMonths()
|
|
46
|
+
const years = getCalendarYears()
|
|
47
|
+
|
|
48
|
+
const CalendarHeading = (props: React.HTMLAttributes<HTMLElement>) => {
|
|
49
|
+
const { direction } = useLocale()
|
|
50
|
+
const calendarState = useContext(CalendarStateContext)
|
|
51
|
+
const rangeCalendarState = useContext(RangeCalendarStateContext)
|
|
52
|
+
const state = calendarState ?? rangeCalendarState
|
|
53
|
+
|
|
54
|
+
// RangeCalendar and Calendar have visibleRange.
|
|
55
|
+
const visibleStart = (state as any)?.visibleRange?.start as
|
|
56
|
+
| CalendarDate
|
|
57
|
+
| undefined
|
|
58
|
+
|
|
59
|
+
const baseDate =
|
|
60
|
+
state?.focusedDate ?? visibleStart ?? today(getLocalTimeZone())
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<header className="flex w-full items-center gap-0.5 pb-1" {...props}>
|
|
64
|
+
<h2 className="flex flex-1 items-center pl-1">
|
|
65
|
+
<NativeSelect
|
|
66
|
+
size="sm"
|
|
67
|
+
variant="ghost"
|
|
68
|
+
value={baseDate.month}
|
|
69
|
+
onChange={(e) => {
|
|
70
|
+
const month = Number(e.target.value)
|
|
71
|
+
const current =
|
|
72
|
+
state?.focusedDate ?? visibleStart ?? today(getLocalTimeZone())
|
|
73
|
+
|
|
74
|
+
const nextFocusedDate = new CalendarDate(
|
|
75
|
+
current.year,
|
|
76
|
+
month,
|
|
77
|
+
current.day
|
|
78
|
+
)
|
|
79
|
+
state?.setFocusedDate(nextFocusedDate)
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{months.map((month) => (
|
|
83
|
+
<NativeSelectOption key={month.value} value={month.value}>
|
|
84
|
+
{month.label}
|
|
85
|
+
</NativeSelectOption>
|
|
86
|
+
))}
|
|
87
|
+
</NativeSelect>
|
|
88
|
+
|
|
89
|
+
<NativeSelect
|
|
90
|
+
size="sm"
|
|
91
|
+
variant="ghost"
|
|
92
|
+
value={baseDate.year}
|
|
93
|
+
onChange={(e) => {
|
|
94
|
+
const year = Number(e.target.value)
|
|
95
|
+
const current =
|
|
96
|
+
state?.focusedDate ?? visibleStart ?? today(getLocalTimeZone())
|
|
97
|
+
|
|
98
|
+
const nextFocusedDate = new CalendarDate(
|
|
99
|
+
year,
|
|
100
|
+
current.month,
|
|
101
|
+
current.day
|
|
102
|
+
)
|
|
103
|
+
state?.setFocusedDate(nextFocusedDate)
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{years.map((year) => (
|
|
107
|
+
<NativeSelectOption key={year} value={year}>
|
|
108
|
+
{year}
|
|
109
|
+
</NativeSelectOption>
|
|
110
|
+
))}
|
|
111
|
+
</NativeSelect>
|
|
112
|
+
</h2>
|
|
113
|
+
|
|
114
|
+
<AriaButton
|
|
115
|
+
slot="previous"
|
|
116
|
+
className={cn(
|
|
117
|
+
buttonVariants({ variant: "ghost", intent: "secondary" }),
|
|
118
|
+
"text-accent-11 size-9 rounded-full bg-transparent p-0",
|
|
119
|
+
"data-disabled:opacity-40",
|
|
120
|
+
"data-hovered:bg-gray-2 data-hovered:text-accent-11 data-hovered:opacity-100"
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
{direction === "rtl" ? (
|
|
124
|
+
<ChevronRight aria-hidden className="size-5" />
|
|
125
|
+
) : (
|
|
126
|
+
<ChevronLeft aria-hidden className="size-5" />
|
|
127
|
+
)}
|
|
128
|
+
</AriaButton>
|
|
129
|
+
|
|
130
|
+
<AriaButton
|
|
131
|
+
slot="next"
|
|
132
|
+
className={cn(
|
|
133
|
+
buttonVariants({ variant: "ghost", intent: "secondary" }),
|
|
134
|
+
"text-accent-11 size-9 rounded-full bg-transparent p-0",
|
|
135
|
+
"data-disabled:opacity-40",
|
|
136
|
+
"data-hovered:bg-gray-2 data-hovered:text-accent-11 data-hovered:opacity-100"
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
{direction === "rtl" ? (
|
|
140
|
+
<ChevronLeft aria-hidden className="size-5" />
|
|
141
|
+
) : (
|
|
142
|
+
<ChevronRight aria-hidden className="size-5" />
|
|
143
|
+
)}
|
|
144
|
+
</AriaButton>
|
|
145
|
+
</header>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const CalendarGrid = ({ className, ...props }: AriaCalendarGridProps) => (
|
|
150
|
+
<AriaCalendarGrid
|
|
151
|
+
className={cn(
|
|
152
|
+
"border-separate border-spacing-x-0.5 border-spacing-y-0.5",
|
|
153
|
+
className
|
|
154
|
+
)}
|
|
155
|
+
{...props}
|
|
156
|
+
/>
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const CalendarGridHeader = ({ ...props }: AriaCalendarGridHeaderProps) => (
|
|
160
|
+
<AriaCalendarGridHeader {...props} />
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const CalendarHeaderCell = ({
|
|
164
|
+
className,
|
|
165
|
+
...props
|
|
166
|
+
}: AriaCalendarHeaderCellProps) => (
|
|
167
|
+
<AriaCalendarHeaderCell
|
|
168
|
+
className={cn("w-9 rounded-md text-[0.8rem] font-normal", className)}
|
|
169
|
+
{...props}
|
|
170
|
+
/>
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const CalendarGridBody = ({
|
|
174
|
+
className,
|
|
175
|
+
...props
|
|
176
|
+
}: AriaCalendarGridBodyProps) => (
|
|
177
|
+
<AriaCalendarGridBody className={cn("[&>tr>td]:p-0", className)} {...props} />
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const CalendarCell = ({ className, ...props }: AriaCalendarCellProps) => {
|
|
181
|
+
return (
|
|
182
|
+
<AriaCalendarCell
|
|
183
|
+
className={composeRenderProps(className, (className, renderProps) =>
|
|
184
|
+
cn(
|
|
185
|
+
"text-gray-12 select-none",
|
|
186
|
+
"relative flex size-9 items-center justify-center rounded-full p-0 text-sm font-normal transition-none",
|
|
187
|
+
/* Disabled */
|
|
188
|
+
renderProps.isDisabled && "text-gray-11 opacity-40",
|
|
189
|
+
/* Selected */
|
|
190
|
+
renderProps.isSelected &&
|
|
191
|
+
"bg-accent-9 text-accent-contrast data-[focused]:bg-accent-9",
|
|
192
|
+
/* Current Date */
|
|
193
|
+
renderProps.date.compare(today(getLocalTimeZone())) === 0 &&
|
|
194
|
+
!renderProps.isSelected &&
|
|
195
|
+
"text-accent-11",
|
|
196
|
+
/* Hovered */
|
|
197
|
+
renderProps.isHovered && "bg-neutral-400/20",
|
|
198
|
+
/* Outside Month */
|
|
199
|
+
renderProps.isOutsideMonth && "opacity-40",
|
|
200
|
+
/* Focused */
|
|
201
|
+
renderProps.isFocused && "ring-2 outline-none ring-inset",
|
|
202
|
+
className
|
|
203
|
+
)
|
|
204
|
+
)}
|
|
205
|
+
{...props}
|
|
206
|
+
/>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Accepts values in the format YYYY-MM-DD */
|
|
211
|
+
interface EgCalendarProps {
|
|
212
|
+
value?: string
|
|
213
|
+
onChange?: (date: string) => void
|
|
214
|
+
defaultValue?: string
|
|
215
|
+
minValue?: string
|
|
216
|
+
maxValue?: string
|
|
217
|
+
variant?: "default" | "unstyled"
|
|
218
|
+
className?: string
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function EgCalendar({
|
|
222
|
+
value: controlledValue,
|
|
223
|
+
onChange: controlledOnChange,
|
|
224
|
+
defaultValue,
|
|
225
|
+
minValue,
|
|
226
|
+
maxValue,
|
|
227
|
+
className,
|
|
228
|
+
variant = "default",
|
|
229
|
+
}: EgCalendarProps) {
|
|
230
|
+
const [uncontrolledValue, setUncontrolledValue] = React.useState<
|
|
231
|
+
string | undefined
|
|
232
|
+
>(defaultValue)
|
|
233
|
+
|
|
234
|
+
const value = controlledValue ?? uncontrolledValue
|
|
235
|
+
const onChange = controlledOnChange ?? setUncontrolledValue
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<Calendar
|
|
239
|
+
value={value ? parseDate(value) : null}
|
|
240
|
+
onChange={(value) => onChange(value?.toString())}
|
|
241
|
+
className={composeRenderProps(className, (className) =>
|
|
242
|
+
cn(
|
|
243
|
+
"w-fit",
|
|
244
|
+
variant === "default" ? "bg-gray-3/40 rounded-lg border p-1" : "",
|
|
245
|
+
className
|
|
246
|
+
)
|
|
247
|
+
)}
|
|
248
|
+
minValue={minValue ? parseDate(minValue) : null}
|
|
249
|
+
maxValue={maxValue ? parseDate(maxValue) : null}
|
|
250
|
+
>
|
|
251
|
+
<CalendarHeading />
|
|
252
|
+
<CalendarGrid>
|
|
253
|
+
<CalendarGridHeader>
|
|
254
|
+
{(day) => <CalendarHeaderCell>{day}</CalendarHeaderCell>}
|
|
255
|
+
</CalendarGridHeader>
|
|
256
|
+
<CalendarGridBody>
|
|
257
|
+
{(date) => <CalendarCell date={date} />}
|
|
258
|
+
</CalendarGridBody>
|
|
259
|
+
</CalendarGrid>
|
|
260
|
+
</Calendar>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export {
|
|
265
|
+
Calendar,
|
|
266
|
+
CalendarCell,
|
|
267
|
+
CalendarGrid,
|
|
268
|
+
CalendarGridBody,
|
|
269
|
+
CalendarGridHeader,
|
|
270
|
+
CalendarHeaderCell,
|
|
271
|
+
CalendarHeading,
|
|
272
|
+
EgCalendar,
|
|
273
|
+
RangeCalendar,
|
|
274
|
+
}
|
|
275
|
+
export type { EgCalendarProps }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MAX_YEAR_FROM_NOW, MIN_YEAR } from "@eggspot/ui/consts/config"
|
|
2
|
+
import { DateFormatter } from "@internationalized/date"
|
|
3
|
+
import dayjs from "dayjs"
|
|
4
|
+
|
|
5
|
+
export const getCalendarYears = () => {
|
|
6
|
+
const startYear = MIN_YEAR
|
|
7
|
+
const endYear = dayjs().year() + MAX_YEAR_FROM_NOW
|
|
8
|
+
const years = Array.from(
|
|
9
|
+
{ length: endYear - startYear + 1 },
|
|
10
|
+
(_, i) => startYear + i
|
|
11
|
+
)
|
|
12
|
+
return years
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const getCalendarMonths = (locale = "en-US") => {
|
|
16
|
+
const formatter = new DateFormatter(locale, { month: "short" })
|
|
17
|
+
|
|
18
|
+
return Array.from({ length: 12 }, (_, i) => ({
|
|
19
|
+
value: i + 1,
|
|
20
|
+
label: formatter.format(new Date(2020, i, 1)),
|
|
21
|
+
}))
|
|
22
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Label, labelVariants } from "@eggspot/ui/components/Label"
|
|
4
|
+
import { cn } from "@eggspot/ui/lib/utils"
|
|
5
|
+
import { useControllableState } from "@radix-ui/react-use-controllable-state"
|
|
6
|
+
import { cva } from "class-variance-authority"
|
|
7
|
+
import { Check, Minus } from "lucide-react"
|
|
8
|
+
import {
|
|
9
|
+
Checkbox as AriaCheckbox,
|
|
10
|
+
CheckboxGroup as AriaCheckboxGroup,
|
|
11
|
+
CheckboxGroupProps as AriaCheckboxGroupProps,
|
|
12
|
+
composeRenderProps,
|
|
13
|
+
type CheckboxProps as AriaCheckboxProps,
|
|
14
|
+
} from "react-aria-components"
|
|
15
|
+
|
|
16
|
+
const checkboxVariants = cva(
|
|
17
|
+
["group/checkbox flex cursor-pointer items-center text-sm"],
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
variant: {
|
|
21
|
+
default: ["group/default gap-x-2"],
|
|
22
|
+
card: [
|
|
23
|
+
"gap-x-3.5",
|
|
24
|
+
"border-gray-7 rounded-md border p-3.5",
|
|
25
|
+
/* Selected */
|
|
26
|
+
"bg-gray-2 data-selected:border-accent-9 data-selected:bg-accent-2 data-selected:border-1",
|
|
27
|
+
/* Hovered */
|
|
28
|
+
"data-hovered:border-gray-8",
|
|
29
|
+
/* Focus Visible */
|
|
30
|
+
"ring-offset-2 data-focus-visible:ring-2 data-pressed:scale-[0.99]",
|
|
31
|
+
/* Invalid */
|
|
32
|
+
"data-[invalid]:text-error-11",
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function Checkbox({
|
|
40
|
+
className,
|
|
41
|
+
children,
|
|
42
|
+
variant = "default",
|
|
43
|
+
reduceMotion = false,
|
|
44
|
+
isDisabled,
|
|
45
|
+
readOnly,
|
|
46
|
+
...props
|
|
47
|
+
}: AriaCheckboxProps & {
|
|
48
|
+
variant?: "default" | "card"
|
|
49
|
+
reduceMotion?: boolean
|
|
50
|
+
readOnly?: boolean
|
|
51
|
+
}) {
|
|
52
|
+
return (
|
|
53
|
+
<AriaCheckbox
|
|
54
|
+
className={composeRenderProps(className, (className) =>
|
|
55
|
+
cn(checkboxVariants({ variant }), labelVariants, className)
|
|
56
|
+
)}
|
|
57
|
+
isDisabled={isDisabled || readOnly}
|
|
58
|
+
{...props}
|
|
59
|
+
>
|
|
60
|
+
{composeRenderProps(children, (children, renderProps) => (
|
|
61
|
+
<>
|
|
62
|
+
<div
|
|
63
|
+
className={cn(
|
|
64
|
+
"transition-all duration-150",
|
|
65
|
+
reduceMotion && "transition-none",
|
|
66
|
+
"bg-gray-2 text-accent-contrast ring-offset-gray-1 flex size-4 shrink-0 items-center justify-center rounded-sm border",
|
|
67
|
+
/* Focus Visible */
|
|
68
|
+
"group-data-[focus-visible]/default:ring-2 group-data-[focus-visible]/default:ring-offset-2 group-data-[focus-visible]/default:outline-none",
|
|
69
|
+
/* Selected */
|
|
70
|
+
"group-data-[indeterminate]/checkbox:bg-accent-9 group-data-[selected]/checkbox:bg-accent-9 group-data-[indeterminate]/checkbox:border-accent-9 group-data-[selected]/checkbox:border-accent-9",
|
|
71
|
+
/* Hovered */
|
|
72
|
+
"group-data-hovered/checkbox:border-gray-8",
|
|
73
|
+
/* Disabled */
|
|
74
|
+
!readOnly &&
|
|
75
|
+
"group-data-[disabled]:cursor-not-allowed group-data-[disabled]/checkbox:cursor-not-allowed group-data-[disabled]/checkbox:opacity-80",
|
|
76
|
+
/* Resets */
|
|
77
|
+
"focus:outline-none focus-visible:outline-none",
|
|
78
|
+
/* Pressed */
|
|
79
|
+
"group-data-pressed/checkbox:scale-[0.95]"
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{renderProps.isIndeterminate ? (
|
|
83
|
+
<Minus
|
|
84
|
+
className={cn(
|
|
85
|
+
"animate-in zoom-in-60 fade-in size-5",
|
|
86
|
+
reduceMotion && "animate-none"
|
|
87
|
+
)}
|
|
88
|
+
/>
|
|
89
|
+
) : renderProps.isSelected ? (
|
|
90
|
+
<Check
|
|
91
|
+
className={cn(
|
|
92
|
+
"animate-in zoom-in-60 fade-in size-5",
|
|
93
|
+
reduceMotion && "animate-none"
|
|
94
|
+
)}
|
|
95
|
+
/>
|
|
96
|
+
) : null}
|
|
97
|
+
</div>
|
|
98
|
+
{children}
|
|
99
|
+
</>
|
|
100
|
+
))}
|
|
101
|
+
</AriaCheckbox>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface CheckboxOption {
|
|
106
|
+
value: string | number
|
|
107
|
+
label: string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface CheckboxGroupProps<T extends CheckboxOption>
|
|
111
|
+
extends Omit<AriaCheckboxGroupProps, "value" | "onChange" | "defaultValue"> {
|
|
112
|
+
/** The label for the checkbox group. */
|
|
113
|
+
label?: string
|
|
114
|
+
|
|
115
|
+
/** The options for the checkbox group. */
|
|
116
|
+
options: Array<T>
|
|
117
|
+
|
|
118
|
+
/** The variant for the checkbox group. */
|
|
119
|
+
variant?: "default" | "card"
|
|
120
|
+
|
|
121
|
+
/** The class name for the checkbox group. */
|
|
122
|
+
className?: string
|
|
123
|
+
|
|
124
|
+
/** Whether to show an asterisk for the label. */
|
|
125
|
+
withAsterisk?: boolean
|
|
126
|
+
|
|
127
|
+
/** The tooltip for the checkbox group. */
|
|
128
|
+
tooltip?: React.ReactNode
|
|
129
|
+
|
|
130
|
+
/** Custom rendering function for each option in the checkbox group. */
|
|
131
|
+
renderOption?: (option: T) => React.ReactNode
|
|
132
|
+
|
|
133
|
+
/** If this prop is set, the checkbox group operates in a controlled manner and uses this value as its state. */
|
|
134
|
+
value?: Array<T>
|
|
135
|
+
|
|
136
|
+
/** If this prop is set, the checkbox group will call this function with the new value whenever the value changes. */
|
|
137
|
+
onChange?: (value: Array<T>) => void
|
|
138
|
+
|
|
139
|
+
/** Initial value used when the component is uncontrolled. */
|
|
140
|
+
defaultValue?: Array<T>
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function CheckboxGroup<T extends CheckboxOption>({
|
|
144
|
+
options,
|
|
145
|
+
renderOption,
|
|
146
|
+
variant = "default",
|
|
147
|
+
className,
|
|
148
|
+
label,
|
|
149
|
+
withAsterisk,
|
|
150
|
+
tooltip,
|
|
151
|
+
value: controlledValue,
|
|
152
|
+
onChange: controlledOnChange,
|
|
153
|
+
defaultValue,
|
|
154
|
+
...props
|
|
155
|
+
}: CheckboxGroupProps<T>) {
|
|
156
|
+
const [value, onChange] = useControllableState({
|
|
157
|
+
prop: controlledValue,
|
|
158
|
+
defaultProp: defaultValue ?? [],
|
|
159
|
+
onChange: controlledOnChange,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<AriaCheckboxGroup
|
|
164
|
+
className="grid gap-1.5"
|
|
165
|
+
value={value.map((v) => JSON.stringify(v))}
|
|
166
|
+
onChange={(v) => onChange(v.map((v) => JSON.parse(v)))}
|
|
167
|
+
{...props}
|
|
168
|
+
>
|
|
169
|
+
{label && (
|
|
170
|
+
<Label withAsterisk={withAsterisk} tooltip={tooltip}>
|
|
171
|
+
{label}
|
|
172
|
+
</Label>
|
|
173
|
+
)}
|
|
174
|
+
<div
|
|
175
|
+
className={cn(
|
|
176
|
+
"grid",
|
|
177
|
+
variant === "card" && "gap-2.5",
|
|
178
|
+
variant === "default" && "gap-1.5",
|
|
179
|
+
className
|
|
180
|
+
)}
|
|
181
|
+
>
|
|
182
|
+
{options.map((option) => {
|
|
183
|
+
return (
|
|
184
|
+
<Checkbox
|
|
185
|
+
key={option.value}
|
|
186
|
+
value={JSON.stringify(option)}
|
|
187
|
+
variant={variant}
|
|
188
|
+
>
|
|
189
|
+
{renderOption ? renderOption(option) : option.label}
|
|
190
|
+
</Checkbox>
|
|
191
|
+
)
|
|
192
|
+
})}
|
|
193
|
+
</div>
|
|
194
|
+
</AriaCheckboxGroup>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { Checkbox, CheckboxGroup }
|
|
199
|
+
export type { CheckboxGroupProps, CheckboxOption }
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react"
|
|
4
|
+
import { Button } from "@eggspot/ui/components/Button"
|
|
5
|
+
import {
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
DialogTrigger,
|
|
12
|
+
} from "@eggspot/ui/components/Dialog"
|
|
13
|
+
import { Form } from "@eggspot/ui/components/Form"
|
|
14
|
+
import { zodResolver } from "@hookform/resolvers/zod"
|
|
15
|
+
import { createStore } from "@xstate/store"
|
|
16
|
+
import { useSelector } from "@xstate/store/react"
|
|
17
|
+
import { useForm, UseFormReturn } from "react-hook-form"
|
|
18
|
+
import { z } from "zod"
|
|
19
|
+
|
|
20
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
|
21
|
+
|
|
22
|
+
interface ConfirmDialogContext<T extends z.ZodSchema<any, any>> {
|
|
23
|
+
/** Indicates whether the confirm dialog is open */
|
|
24
|
+
isOpen: boolean
|
|
25
|
+
|
|
26
|
+
/** Data for the confirm dialog, including title, description, and actions */
|
|
27
|
+
data?: {
|
|
28
|
+
/** Whether to dismiss the dialog when the user clicks outside of it */
|
|
29
|
+
isDismissable?: boolean
|
|
30
|
+
|
|
31
|
+
/** The title displayed in the confirm dialog */
|
|
32
|
+
title: string
|
|
33
|
+
|
|
34
|
+
/** Optional description displayed in the confirm dialog */
|
|
35
|
+
description?: string
|
|
36
|
+
|
|
37
|
+
/** Text for the confirm action button */
|
|
38
|
+
continueText?: string
|
|
39
|
+
|
|
40
|
+
/** Text for the cancel action button */
|
|
41
|
+
cancelText?: string
|
|
42
|
+
|
|
43
|
+
/** Optional variant for the action button */
|
|
44
|
+
intent?: "primary" | "danger"
|
|
45
|
+
|
|
46
|
+
/** Callback function to be called when the confirm action is clicked */
|
|
47
|
+
onContinue?: (values: z.infer<T>) => Promise<void> | void
|
|
48
|
+
|
|
49
|
+
/** Callback function to be called when the cancel action is clicked */
|
|
50
|
+
onCancel?: (values: z.infer<T>) => void
|
|
51
|
+
|
|
52
|
+
form?: {
|
|
53
|
+
schema?: T
|
|
54
|
+
/** Displays the content of the confirm dialog, wrapped in the context of React Hook Form, allowing you to add any fields using react-hook-form. */
|
|
55
|
+
content?: (
|
|
56
|
+
form: UseFormReturn<z.infer<T>, any, z.infer<T>>
|
|
57
|
+
) => React.ReactNode
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const confirmStore = createStore({
|
|
63
|
+
context: {
|
|
64
|
+
isOpen: false,
|
|
65
|
+
} as ConfirmDialogContext<any>,
|
|
66
|
+
on: {
|
|
67
|
+
open: (_, data: ConfirmDialogContext<any>["data"]) => ({
|
|
68
|
+
isOpen: true,
|
|
69
|
+
data,
|
|
70
|
+
}),
|
|
71
|
+
close: (context) => ({ ...context, isOpen: false }),
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const { open, close } = confirmStore.trigger
|
|
76
|
+
|
|
77
|
+
function confirm<T extends z.ZodSchema<any, any>>(
|
|
78
|
+
data: ConfirmDialogContext<T>["data"]
|
|
79
|
+
) {
|
|
80
|
+
if (!data) return
|
|
81
|
+
|
|
82
|
+
open(data)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function ConfirmDialog() {
|
|
86
|
+
const [isPending, setIsPending] = useState(false)
|
|
87
|
+
const isOpen = useSelector(confirmStore, (state) => state.context.isOpen)
|
|
88
|
+
const data = useSelector(confirmStore, (state) => state.context.data)
|
|
89
|
+
const intent = data?.intent || "primary"
|
|
90
|
+
|
|
91
|
+
const form = useForm({
|
|
92
|
+
defaultValues: data?.form?.schema,
|
|
93
|
+
resolver: data?.form?.schema ? zodResolver(data.form.schema) : undefined,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const handleClose = () => {
|
|
97
|
+
close()
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
form.reset()
|
|
100
|
+
setIsPending(false)
|
|
101
|
+
}, 200)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const handleCancel = () => {
|
|
105
|
+
data?.onCancel?.(form.getValues())
|
|
106
|
+
handleClose()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const handleSubmit = form.handleSubmit((values) => {
|
|
110
|
+
if (!data?.onContinue) {
|
|
111
|
+
handleClose()
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (data.onContinue instanceof AsyncFunction) {
|
|
116
|
+
setIsPending(true)
|
|
117
|
+
const onContinueAsync = data.onContinue as (values: any) => Promise<void>
|
|
118
|
+
onContinueAsync(values)
|
|
119
|
+
.then(() => {
|
|
120
|
+
handleClose()
|
|
121
|
+
})
|
|
122
|
+
.catch(() => {
|
|
123
|
+
setIsPending(false)
|
|
124
|
+
})
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
data?.onContinue?.(values)
|
|
129
|
+
handleClose()
|
|
130
|
+
return
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<DialogTrigger
|
|
135
|
+
isOpen={isOpen}
|
|
136
|
+
onOpenChange={(isOpen) => {
|
|
137
|
+
if (!isOpen) {
|
|
138
|
+
close()
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
form.reset()
|
|
141
|
+
}, 200)
|
|
142
|
+
}
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
<DialogContent
|
|
146
|
+
isDismissable={data?.isDismissable ?? false}
|
|
147
|
+
className="md:max-w-[500px]"
|
|
148
|
+
closeButton={false}
|
|
149
|
+
>
|
|
150
|
+
<Form {...form}>
|
|
151
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
152
|
+
<DialogHeader>
|
|
153
|
+
<DialogTitle>{data?.title || "Confirm"}</DialogTitle>
|
|
154
|
+
<DialogDescription>
|
|
155
|
+
{data?.description || "Are you sure you want to confirm?"}
|
|
156
|
+
</DialogDescription>
|
|
157
|
+
</DialogHeader>
|
|
158
|
+
{data?.form?.content && data.form.content(form)}
|
|
159
|
+
<DialogFooter>
|
|
160
|
+
<Button
|
|
161
|
+
variant="minimal"
|
|
162
|
+
onClick={handleCancel}
|
|
163
|
+
isDisabled={isPending}
|
|
164
|
+
>
|
|
165
|
+
{data?.cancelText || "Cancel"}
|
|
166
|
+
</Button>
|
|
167
|
+
<Button
|
|
168
|
+
type="submit"
|
|
169
|
+
variant="solid"
|
|
170
|
+
intent={intent}
|
|
171
|
+
isLoading={isPending}
|
|
172
|
+
>
|
|
173
|
+
{data?.continueText || "Continue"}
|
|
174
|
+
</Button>
|
|
175
|
+
</DialogFooter>
|
|
176
|
+
</form>
|
|
177
|
+
</Form>
|
|
178
|
+
</DialogContent>
|
|
179
|
+
</DialogTrigger>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export { confirm, ConfirmDialog }
|