@xyhp915/slack-base-ui 0.0.5 → 0.0.7
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/libs/Combobox.d.ts +77 -0
- package/libs/Combobox.d.ts.map +1 -0
- package/libs/Dialog.d.ts +1 -1
- package/libs/Dialog.d.ts.map +1 -1
- package/libs/Dropdown.d.ts +177 -0
- package/libs/Dropdown.d.ts.map +1 -0
- package/libs/index.d.ts +4 -0
- package/libs/index.d.ts.map +1 -1
- package/libs/index.js +9368 -7097
- package/package.json +1 -1
- package/src/components/Combobox.tsx +441 -0
- package/src/components/Dialog.tsx +63 -60
- package/src/components/Dropdown.tsx +520 -0
- package/src/components/index.ts +38 -0
- package/src/main.tsx +4 -1
- package/src/pages/ComponentShowcase.tsx +324 -0
package/package.json
CHANGED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Combobox as BaseCombobox } from '@base-ui/react'
|
|
3
|
+
import clsx from 'clsx'
|
|
4
|
+
import { Check, ChevronDown, X } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface ComboboxOption {
|
|
9
|
+
value: string
|
|
10
|
+
label: string
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ComboboxGroup {
|
|
15
|
+
label: string
|
|
16
|
+
options: ComboboxOption[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ComboboxProps {
|
|
20
|
+
/** Flat option list */
|
|
21
|
+
options?: ComboboxOption[]
|
|
22
|
+
/** Grouped option list (mutually exclusive with `options`) */
|
|
23
|
+
groups?: ComboboxGroup[]
|
|
24
|
+
/** Controlled selected value */
|
|
25
|
+
value?: ComboboxOption | null
|
|
26
|
+
/** Initial selected value (uncontrolled) */
|
|
27
|
+
defaultValue?: ComboboxOption | null
|
|
28
|
+
/** Callback when the selected value changes */
|
|
29
|
+
onValueChange?: (value: ComboboxOption | null) => void
|
|
30
|
+
/** Placeholder text shown in the input */
|
|
31
|
+
placeholder?: string
|
|
32
|
+
/** Whether the combobox is disabled */
|
|
33
|
+
disabled?: boolean
|
|
34
|
+
/** Whether a value is required */
|
|
35
|
+
required?: boolean
|
|
36
|
+
/** Label shown above the input */
|
|
37
|
+
label?: string
|
|
38
|
+
/** Error message shown below the input */
|
|
39
|
+
error?: string
|
|
40
|
+
/** Whether the combobox takes full width */
|
|
41
|
+
fullWidth?: boolean
|
|
42
|
+
/** Whether the user can clear the selected value */
|
|
43
|
+
clearable?: boolean
|
|
44
|
+
/** No-results message */
|
|
45
|
+
emptyMessage?: string
|
|
46
|
+
/** Additional CSS class on the outermost container */
|
|
47
|
+
className?: string
|
|
48
|
+
/** HTML id forwarded to the input */
|
|
49
|
+
id?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ComboboxMultipleProps {
|
|
53
|
+
/** Flat option list */
|
|
54
|
+
options?: ComboboxOption[]
|
|
55
|
+
/** Grouped option list */
|
|
56
|
+
groups?: ComboboxGroup[]
|
|
57
|
+
/** Controlled selected values */
|
|
58
|
+
value?: ComboboxOption[]
|
|
59
|
+
/** Initial selected values (uncontrolled) */
|
|
60
|
+
defaultValue?: ComboboxOption[]
|
|
61
|
+
/** Callback when selected values change */
|
|
62
|
+
onValueChange?: (value: ComboboxOption[]) => void
|
|
63
|
+
/** Placeholder text shown in the input */
|
|
64
|
+
placeholder?: string
|
|
65
|
+
/** Whether the combobox is disabled */
|
|
66
|
+
disabled?: boolean
|
|
67
|
+
/** Whether a value is required */
|
|
68
|
+
required?: boolean
|
|
69
|
+
/** Label shown above the input */
|
|
70
|
+
label?: string
|
|
71
|
+
/** Error message shown below the input */
|
|
72
|
+
error?: string
|
|
73
|
+
/** Whether the combobox takes full width */
|
|
74
|
+
fullWidth?: boolean
|
|
75
|
+
/** Whether the user can clear all selected values */
|
|
76
|
+
clearable?: boolean
|
|
77
|
+
/** No-results message */
|
|
78
|
+
emptyMessage?: string
|
|
79
|
+
/** Additional CSS class on the outermost container */
|
|
80
|
+
className?: string
|
|
81
|
+
/** HTML id forwarded to the input */
|
|
82
|
+
id?: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Shared styles ──────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const inputGroupStyles = clsx(
|
|
88
|
+
'inline-flex w-full items-center rounded-md border',
|
|
89
|
+
'bg-(--bg-primary) text-(--text-primary)',
|
|
90
|
+
'transition-[border-color,box-shadow] outline-none',
|
|
91
|
+
'has-[:focus]:border-(--accent)/70',
|
|
92
|
+
'has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50',
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const inputStyles = clsx(
|
|
96
|
+
'flex-1 min-w-0 appearance-none bg-transparent text-[14px] leading-[1.4]',
|
|
97
|
+
'px-3 py-2 text-(--text-primary) placeholder-(--text-muted)',
|
|
98
|
+
'outline-none disabled:cursor-not-allowed',
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const popupStyles = clsx(
|
|
102
|
+
'min-w-[var(--anchor-width)] overflow-auto rounded-lg border border-(--border-light) bg-(--bg-primary)',
|
|
103
|
+
'py-1 shadow-lg',
|
|
104
|
+
'origin-[var(--transform-origin)]',
|
|
105
|
+
'transition-[transform,scale,opacity]',
|
|
106
|
+
'data-[starting-style]:scale-95 data-[starting-style]:opacity-0',
|
|
107
|
+
'data-[ending-style]:scale-95 data-[ending-style]:opacity-0',
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const itemStyles = clsx(
|
|
111
|
+
'relative flex cursor-default select-none items-center rounded px-3 py-1.5 text-[14px] text-(--text-primary) mx-1',
|
|
112
|
+
'data-[highlighted]:bg-(--bg-hover) data-[highlighted]:outline-none',
|
|
113
|
+
'data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed',
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// ── Single-select Combobox ─────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export const Combobox = React.forwardRef<HTMLInputElement, ComboboxProps>(
|
|
119
|
+
(
|
|
120
|
+
{
|
|
121
|
+
options,
|
|
122
|
+
groups,
|
|
123
|
+
value,
|
|
124
|
+
defaultValue,
|
|
125
|
+
onValueChange,
|
|
126
|
+
placeholder = 'Search…',
|
|
127
|
+
disabled,
|
|
128
|
+
required,
|
|
129
|
+
label,
|
|
130
|
+
error,
|
|
131
|
+
fullWidth,
|
|
132
|
+
clearable = true,
|
|
133
|
+
emptyMessage = 'No results found',
|
|
134
|
+
className,
|
|
135
|
+
id,
|
|
136
|
+
},
|
|
137
|
+
ref,
|
|
138
|
+
) => {
|
|
139
|
+
const generatedId = React.useId()
|
|
140
|
+
const inputId = id ?? generatedId
|
|
141
|
+
|
|
142
|
+
// Build the flat items array that Base UI needs for filtering
|
|
143
|
+
const items = React.useMemo<ComboboxOption[]>(() => {
|
|
144
|
+
if (groups) return groups.flatMap((g) => g.options)
|
|
145
|
+
return options ?? []
|
|
146
|
+
}, [options, groups])
|
|
147
|
+
|
|
148
|
+
// Build grouped items when `groups` is provided
|
|
149
|
+
const groupedItems = React.useMemo(() => {
|
|
150
|
+
if (!groups) return undefined
|
|
151
|
+
return groups.map((g) => ({
|
|
152
|
+
value: g.label,
|
|
153
|
+
items: g.options,
|
|
154
|
+
}))
|
|
155
|
+
}, [groups])
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className={clsx('flex flex-col gap-1.5', fullWidth && 'w-full', className)}>
|
|
159
|
+
{label && (
|
|
160
|
+
<label htmlFor={inputId} className="text-[14px] font-semibold text-(--text-primary)">
|
|
161
|
+
{label}
|
|
162
|
+
{required && <span className="ml-0.5 text-(--danger)">*</span>}
|
|
163
|
+
</label>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
<BaseCombobox.Root
|
|
167
|
+
value={value}
|
|
168
|
+
defaultValue={defaultValue}
|
|
169
|
+
onValueChange={
|
|
170
|
+
onValueChange
|
|
171
|
+
? (v: ComboboxOption | null) => onValueChange(v)
|
|
172
|
+
: undefined
|
|
173
|
+
}
|
|
174
|
+
items={groupedItems ?? items}
|
|
175
|
+
disabled={disabled}
|
|
176
|
+
required={required}
|
|
177
|
+
>
|
|
178
|
+
{/* Input row — wraps input + action buttons */}
|
|
179
|
+
<div
|
|
180
|
+
className={clsx(
|
|
181
|
+
inputGroupStyles,
|
|
182
|
+
error
|
|
183
|
+
? 'border-(--danger) has-[:focus]:border-(--danger) has-[:focus]:shadow-[0_0_0_2px_var(--danger)]'
|
|
184
|
+
: 'border-(--border-light) hover:border-(--text-primary)/30',
|
|
185
|
+
)}
|
|
186
|
+
>
|
|
187
|
+
<BaseCombobox.Input
|
|
188
|
+
ref={ref}
|
|
189
|
+
id={inputId}
|
|
190
|
+
placeholder={placeholder}
|
|
191
|
+
className={inputStyles}
|
|
192
|
+
/>
|
|
193
|
+
<div className="flex items-center pr-2 gap-0.5 shrink-0">
|
|
194
|
+
{clearable && (
|
|
195
|
+
<BaseCombobox.Clear
|
|
196
|
+
aria-label="Clear"
|
|
197
|
+
className={clsx(
|
|
198
|
+
'p-0.5 rounded text-(--text-muted) hover:text-(--text-primary)',
|
|
199
|
+
'transition-colors cursor-pointer',
|
|
200
|
+
)}
|
|
201
|
+
>
|
|
202
|
+
<X size={14} />
|
|
203
|
+
</BaseCombobox.Clear>
|
|
204
|
+
)}
|
|
205
|
+
<BaseCombobox.Trigger
|
|
206
|
+
aria-label="Open"
|
|
207
|
+
className="p-0.5 rounded text-(--text-muted) hover:text-(--text-primary) transition-colors cursor-pointer"
|
|
208
|
+
>
|
|
209
|
+
<ChevronDown size={16} />
|
|
210
|
+
</BaseCombobox.Trigger>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<BaseCombobox.Portal>
|
|
215
|
+
<BaseCombobox.Positioner sideOffset={4} className="z-50">
|
|
216
|
+
<BaseCombobox.Popup className={popupStyles}>
|
|
217
|
+
<BaseCombobox.Empty className="px-3 py-2 text-[13px] text-(--text-muted)">
|
|
218
|
+
{emptyMessage}
|
|
219
|
+
</BaseCombobox.Empty>
|
|
220
|
+
|
|
221
|
+
<BaseCombobox.List className="outline-none max-h-60 overflow-y-auto">
|
|
222
|
+
{groups
|
|
223
|
+
? (group: { value: string; items: ComboboxOption[] }) => (
|
|
224
|
+
<BaseCombobox.Group key={group.value}>
|
|
225
|
+
<BaseCombobox.GroupLabel className="px-3 py-1 text-[12px] font-semibold text-(--text-muted) uppercase tracking-wider">
|
|
226
|
+
{group.value}
|
|
227
|
+
</BaseCombobox.GroupLabel>
|
|
228
|
+
{group.items.map((opt) => (
|
|
229
|
+
<ComboboxItem key={opt.value} option={opt} />
|
|
230
|
+
))}
|
|
231
|
+
</BaseCombobox.Group>
|
|
232
|
+
)
|
|
233
|
+
: (item: ComboboxOption) => (
|
|
234
|
+
<ComboboxItem key={item.value} option={item} />
|
|
235
|
+
)}
|
|
236
|
+
</BaseCombobox.List>
|
|
237
|
+
</BaseCombobox.Popup>
|
|
238
|
+
</BaseCombobox.Positioner>
|
|
239
|
+
</BaseCombobox.Portal>
|
|
240
|
+
</BaseCombobox.Root>
|
|
241
|
+
|
|
242
|
+
{error && (
|
|
243
|
+
<span className="flex items-center gap-1 text-[12px] font-medium leading-tight text-(--danger)">
|
|
244
|
+
⚠️ {error}
|
|
245
|
+
</span>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
)
|
|
249
|
+
},
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
Combobox.displayName = 'Combobox'
|
|
253
|
+
|
|
254
|
+
// ── Multiple-select Combobox ──────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
export const ComboboxMultiple = React.forwardRef<HTMLInputElement, ComboboxMultipleProps>(
|
|
257
|
+
(
|
|
258
|
+
{
|
|
259
|
+
options,
|
|
260
|
+
groups,
|
|
261
|
+
value: valueProp,
|
|
262
|
+
defaultValue,
|
|
263
|
+
onValueChange,
|
|
264
|
+
placeholder = 'Search…',
|
|
265
|
+
disabled,
|
|
266
|
+
required,
|
|
267
|
+
label,
|
|
268
|
+
error,
|
|
269
|
+
fullWidth,
|
|
270
|
+
clearable = true,
|
|
271
|
+
emptyMessage = 'No results found',
|
|
272
|
+
className,
|
|
273
|
+
id,
|
|
274
|
+
},
|
|
275
|
+
ref,
|
|
276
|
+
) => {
|
|
277
|
+
const generatedId = React.useId()
|
|
278
|
+
const inputId = id ?? generatedId
|
|
279
|
+
|
|
280
|
+
// Track selected values internally so we can render chips correctly
|
|
281
|
+
const [internalValue, setInternalValue] = React.useState<ComboboxOption[]>(
|
|
282
|
+
defaultValue ?? [],
|
|
283
|
+
)
|
|
284
|
+
const selectedValues = valueProp ?? internalValue
|
|
285
|
+
|
|
286
|
+
const handleValueChange = React.useCallback(
|
|
287
|
+
(v: ComboboxOption[]) => {
|
|
288
|
+
if (valueProp === undefined) {
|
|
289
|
+
setInternalValue(v)
|
|
290
|
+
}
|
|
291
|
+
onValueChange?.(v)
|
|
292
|
+
},
|
|
293
|
+
[valueProp, onValueChange],
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
const items = React.useMemo<ComboboxOption[]>(() => {
|
|
297
|
+
if (groups) return groups.flatMap((g) => g.options)
|
|
298
|
+
return options ?? []
|
|
299
|
+
}, [options, groups])
|
|
300
|
+
|
|
301
|
+
const groupedItems = React.useMemo(() => {
|
|
302
|
+
if (!groups) return undefined
|
|
303
|
+
return groups.map((g) => ({
|
|
304
|
+
value: g.label,
|
|
305
|
+
items: g.options,
|
|
306
|
+
}))
|
|
307
|
+
}, [groups])
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<div className={clsx('flex flex-col gap-1.5', fullWidth && 'w-full', className)}>
|
|
311
|
+
{label && (
|
|
312
|
+
<label htmlFor={inputId} className="text-[14px] font-semibold text-(--text-primary)">
|
|
313
|
+
{label}
|
|
314
|
+
{required && <span className="ml-0.5 text-(--danger)">*</span>}
|
|
315
|
+
</label>
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
<BaseCombobox.Root
|
|
319
|
+
multiple
|
|
320
|
+
value={selectedValues}
|
|
321
|
+
onValueChange={handleValueChange}
|
|
322
|
+
items={groupedItems ?? items}
|
|
323
|
+
disabled={disabled}
|
|
324
|
+
required={required}
|
|
325
|
+
>
|
|
326
|
+
{/* Input row with chips */}
|
|
327
|
+
<div
|
|
328
|
+
className={clsx(
|
|
329
|
+
inputGroupStyles,
|
|
330
|
+
'flex-wrap gap-1 py-1',
|
|
331
|
+
error
|
|
332
|
+
? 'border-(--danger) has-[:focus]:border-(--danger) has-[:focus]:shadow-[0_0_0_2px_var(--danger)]'
|
|
333
|
+
: 'border-(--border-light) hover:border-(--text-primary)/30',
|
|
334
|
+
)}
|
|
335
|
+
>
|
|
336
|
+
{selectedValues.length > 0 && (
|
|
337
|
+
<BaseCombobox.Chips className="flex flex-wrap gap-1 pl-2">
|
|
338
|
+
{selectedValues.map((opt) => (
|
|
339
|
+
<BaseCombobox.Chip
|
|
340
|
+
key={opt.value}
|
|
341
|
+
className={clsx(
|
|
342
|
+
'inline-flex items-center gap-1 rounded-md px-2 py-0.5',
|
|
343
|
+
'bg-(--bg-hover) text-(--text-primary) text-[12px] font-medium',
|
|
344
|
+
'border border-(--border-light)',
|
|
345
|
+
)}
|
|
346
|
+
>
|
|
347
|
+
{opt.label}
|
|
348
|
+
<BaseCombobox.ChipRemove
|
|
349
|
+
className="rounded-sm p-0.5 hover:bg-(--bg-secondary) text-(--text-muted) hover:text-(--text-primary) transition-colors cursor-pointer"
|
|
350
|
+
aria-label={`Remove ${opt.label}`}
|
|
351
|
+
>
|
|
352
|
+
<X size={12} />
|
|
353
|
+
</BaseCombobox.ChipRemove>
|
|
354
|
+
</BaseCombobox.Chip>
|
|
355
|
+
))}
|
|
356
|
+
</BaseCombobox.Chips>
|
|
357
|
+
)}
|
|
358
|
+
|
|
359
|
+
<BaseCombobox.Input
|
|
360
|
+
ref={ref}
|
|
361
|
+
id={inputId}
|
|
362
|
+
placeholder={placeholder}
|
|
363
|
+
className={clsx(inputStyles, 'py-1.5')}
|
|
364
|
+
/>
|
|
365
|
+
|
|
366
|
+
<div className="flex items-center pr-2 gap-0.5 shrink-0">
|
|
367
|
+
{clearable && (
|
|
368
|
+
<BaseCombobox.Clear
|
|
369
|
+
aria-label="Clear all"
|
|
370
|
+
className={clsx(
|
|
371
|
+
'p-0.5 rounded text-(--text-muted) hover:text-(--text-primary)',
|
|
372
|
+
'transition-colors cursor-pointer',
|
|
373
|
+
)}
|
|
374
|
+
>
|
|
375
|
+
<X size={14} />
|
|
376
|
+
</BaseCombobox.Clear>
|
|
377
|
+
)}
|
|
378
|
+
<BaseCombobox.Trigger
|
|
379
|
+
aria-label="Open"
|
|
380
|
+
className="p-0.5 rounded text-(--text-muted) hover:text-(--text-primary) transition-colors cursor-pointer"
|
|
381
|
+
>
|
|
382
|
+
<ChevronDown size={16} />
|
|
383
|
+
</BaseCombobox.Trigger>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<BaseCombobox.Portal>
|
|
388
|
+
<BaseCombobox.Positioner sideOffset={4} className="z-50">
|
|
389
|
+
<BaseCombobox.Popup className={popupStyles}>
|
|
390
|
+
<BaseCombobox.Empty className="px-3 py-2 text-[13px] text-(--text-muted)">
|
|
391
|
+
{emptyMessage}
|
|
392
|
+
</BaseCombobox.Empty>
|
|
393
|
+
|
|
394
|
+
<BaseCombobox.List className="outline-none max-h-60 overflow-y-auto">
|
|
395
|
+
{groups
|
|
396
|
+
? (group: { value: string; items: ComboboxOption[] }) => (
|
|
397
|
+
<BaseCombobox.Group key={group.value}>
|
|
398
|
+
<BaseCombobox.GroupLabel className="px-3 py-1 text-[12px] font-semibold text-(--text-muted) uppercase tracking-wider">
|
|
399
|
+
{group.value}
|
|
400
|
+
</BaseCombobox.GroupLabel>
|
|
401
|
+
{group.items.map((opt) => (
|
|
402
|
+
<ComboboxItem key={opt.value} option={opt} />
|
|
403
|
+
))}
|
|
404
|
+
</BaseCombobox.Group>
|
|
405
|
+
)
|
|
406
|
+
: (item: ComboboxOption) => (
|
|
407
|
+
<ComboboxItem key={item.value} option={item} />
|
|
408
|
+
)}
|
|
409
|
+
</BaseCombobox.List>
|
|
410
|
+
</BaseCombobox.Popup>
|
|
411
|
+
</BaseCombobox.Positioner>
|
|
412
|
+
</BaseCombobox.Portal>
|
|
413
|
+
</BaseCombobox.Root>
|
|
414
|
+
|
|
415
|
+
{error && (
|
|
416
|
+
<span className="flex items-center gap-1 text-[12px] font-medium leading-tight text-(--danger)">
|
|
417
|
+
⚠️ {error}
|
|
418
|
+
</span>
|
|
419
|
+
)}
|
|
420
|
+
</div>
|
|
421
|
+
)
|
|
422
|
+
},
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
ComboboxMultiple.displayName = 'ComboboxMultiple'
|
|
426
|
+
|
|
427
|
+
// ── Internal helpers ──────────────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
/** Renders a single selectable option inside the popup list. */
|
|
430
|
+
function ComboboxItem({ option }: { option: ComboboxOption }) {
|
|
431
|
+
return (
|
|
432
|
+
<BaseCombobox.Item value={option} disabled={option.disabled} className={itemStyles}>
|
|
433
|
+
<BaseCombobox.ItemIndicator className="absolute right-3 flex items-center text-(--accent)">
|
|
434
|
+
<Check size={14} />
|
|
435
|
+
</BaseCombobox.ItemIndicator>
|
|
436
|
+
<span className="flex-1">{option.label}</span>
|
|
437
|
+
</BaseCombobox.Item>
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
|
|
@@ -10,7 +10,7 @@ import React, {
|
|
|
10
10
|
import { X } from 'lucide-react'
|
|
11
11
|
import { Button } from './Button'
|
|
12
12
|
|
|
13
|
-
export type DialogSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
13
|
+
export type DialogSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
|
|
14
14
|
|
|
15
15
|
export interface DialogProps {
|
|
16
16
|
open?: boolean;
|
|
@@ -29,7 +29,7 @@ export const Dialog = ({
|
|
|
29
29
|
title,
|
|
30
30
|
description,
|
|
31
31
|
children,
|
|
32
|
-
size = '
|
|
32
|
+
size = 'xl',
|
|
33
33
|
showCloseButton = true,
|
|
34
34
|
className,
|
|
35
35
|
}: DialogProps) => {
|
|
@@ -38,6 +38,9 @@ export const Dialog = ({
|
|
|
38
38
|
md: 'max-w-md',
|
|
39
39
|
lg: 'max-w-lg',
|
|
40
40
|
xl: 'max-w-xl',
|
|
41
|
+
'2xl': 'max-w-2xl',
|
|
42
|
+
'3xl': 'max-w-3xl',
|
|
43
|
+
'4xl': 'max-w-4xl',
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
return (
|
|
@@ -242,27 +245,27 @@ export const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|
|
242
245
|
}, [])
|
|
243
246
|
|
|
244
247
|
const value = useMemo<UseDialogReturn>(
|
|
245
|
-
|
|
246
|
-
|
|
248
|
+
() => ({ show, confirm, alert }),
|
|
249
|
+
[show, confirm, alert],
|
|
247
250
|
)
|
|
248
251
|
|
|
249
252
|
return (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
253
|
+
<DialogImperativeContext.Provider value={value}>
|
|
254
|
+
{children}
|
|
255
|
+
{dialogs.map(dialog => (
|
|
256
|
+
<ImperativeDialogItem
|
|
257
|
+
key={dialog.id}
|
|
258
|
+
dialog={dialog}
|
|
259
|
+
onRemove={() => removeDialog(dialog.id)}
|
|
260
|
+
/>
|
|
261
|
+
))}
|
|
262
|
+
</DialogImperativeContext.Provider>
|
|
260
263
|
)
|
|
261
264
|
}
|
|
262
265
|
|
|
263
266
|
DialogProvider.displayName = 'DialogProvider'
|
|
264
267
|
|
|
265
|
-
function ImperativeDialogItem({
|
|
268
|
+
function ImperativeDialogItem ({
|
|
266
269
|
dialog,
|
|
267
270
|
onRemove,
|
|
268
271
|
}: {
|
|
@@ -272,13 +275,13 @@ function ImperativeDialogItem({
|
|
|
272
275
|
const [open, setOpen] = useState(true)
|
|
273
276
|
|
|
274
277
|
const handleClose = useCallback(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
278
|
+
(result: boolean) => {
|
|
279
|
+
setOpen(false)
|
|
280
|
+
dialog.resolve(result)
|
|
281
|
+
// Allow close animation to finish before unmounting
|
|
282
|
+
setTimeout(onRemove, 300)
|
|
283
|
+
},
|
|
284
|
+
[dialog, onRemove],
|
|
282
285
|
)
|
|
283
286
|
|
|
284
287
|
const commonProps = {
|
|
@@ -291,55 +294,55 @@ function ImperativeDialogItem({
|
|
|
291
294
|
|
|
292
295
|
if (dialog.type === 'show') {
|
|
293
296
|
return (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
297
|
+
<Dialog
|
|
298
|
+
{...commonProps}
|
|
299
|
+
showCloseButton={dialog.showCloseButton ?? true}
|
|
300
|
+
onOpenChange={o => !o && handleClose(false)}
|
|
301
|
+
>
|
|
302
|
+
{dialog.content}
|
|
303
|
+
</Dialog>
|
|
301
304
|
)
|
|
302
305
|
}
|
|
303
306
|
|
|
304
307
|
if (dialog.type === 'confirm') {
|
|
305
308
|
return (
|
|
309
|
+
<Dialog
|
|
310
|
+
{...commonProps}
|
|
311
|
+
size={dialog.size ?? 'sm'}
|
|
312
|
+
showCloseButton={false}
|
|
313
|
+
onOpenChange={o => !o && handleClose(false)}
|
|
314
|
+
>
|
|
315
|
+
{dialog.content}
|
|
316
|
+
<DialogFooter>
|
|
317
|
+
<Button variant="secondary" onClick={() => handleClose(false)}>
|
|
318
|
+
{dialog.cancelLabel ?? 'Cancel'}
|
|
319
|
+
</Button>
|
|
320
|
+
<Button
|
|
321
|
+
variant={dialog.confirmVariant ?? 'primary'}
|
|
322
|
+
onClick={() => handleClose(true)}
|
|
323
|
+
>
|
|
324
|
+
{dialog.confirmLabel ?? 'Confirm'}
|
|
325
|
+
</Button>
|
|
326
|
+
</DialogFooter>
|
|
327
|
+
</Dialog>
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// alert
|
|
332
|
+
return (
|
|
306
333
|
<Dialog
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
334
|
+
{...commonProps}
|
|
335
|
+
size={dialog.size ?? 'sm'}
|
|
336
|
+
showCloseButton={false}
|
|
337
|
+
onOpenChange={o => !o && handleClose(true)}
|
|
311
338
|
>
|
|
312
339
|
{dialog.content}
|
|
313
340
|
<DialogFooter>
|
|
314
|
-
<Button variant="
|
|
315
|
-
{dialog.
|
|
316
|
-
</Button>
|
|
317
|
-
<Button
|
|
318
|
-
variant={dialog.confirmVariant ?? 'primary'}
|
|
319
|
-
onClick={() => handleClose(true)}
|
|
320
|
-
>
|
|
321
|
-
{dialog.confirmLabel ?? 'Confirm'}
|
|
341
|
+
<Button variant="primary" onClick={() => handleClose(true)}>
|
|
342
|
+
{dialog.confirmLabel ?? 'OK'}
|
|
322
343
|
</Button>
|
|
323
344
|
</DialogFooter>
|
|
324
345
|
</Dialog>
|
|
325
|
-
)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// alert
|
|
329
|
-
return (
|
|
330
|
-
<Dialog
|
|
331
|
-
{...commonProps}
|
|
332
|
-
size={dialog.size ?? 'sm'}
|
|
333
|
-
showCloseButton={false}
|
|
334
|
-
onOpenChange={o => !o && handleClose(true)}
|
|
335
|
-
>
|
|
336
|
-
{dialog.content}
|
|
337
|
-
<DialogFooter>
|
|
338
|
-
<Button variant="primary" onClick={() => handleClose(true)}>
|
|
339
|
-
{dialog.confirmLabel ?? 'OK'}
|
|
340
|
-
</Button>
|
|
341
|
-
</DialogFooter>
|
|
342
|
-
</Dialog>
|
|
343
346
|
)
|
|
344
347
|
}
|
|
345
348
|
|
|
@@ -363,7 +366,7 @@ function ImperativeDialogItem({
|
|
|
363
366
|
* await alert({ title: 'Error', description: 'Something went wrong.' })
|
|
364
367
|
* ```
|
|
365
368
|
*/
|
|
366
|
-
export function useDialog(): UseDialogReturn {
|
|
369
|
+
export function useDialog (): UseDialogReturn {
|
|
367
370
|
const ctx = useContext(DialogImperativeContext)
|
|
368
371
|
if (!ctx) throw new Error('useDialog must be used within <DialogProvider>')
|
|
369
372
|
return ctx
|