@djangocfg/ui-nextjs 1.4.45
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/LICENSE +21 -0
- package/README.md +152 -0
- package/package.json +110 -0
- package/src/animations/AnimatedBackground.tsx +645 -0
- package/src/animations/index.ts +2 -0
- package/src/blocks/ArticleCard.tsx +94 -0
- package/src/blocks/ArticleList.tsx +95 -0
- package/src/blocks/CTASection.tsx +136 -0
- package/src/blocks/FeatureSection.tsx +104 -0
- package/src/blocks/Hero.tsx +102 -0
- package/src/blocks/NewsletterSection.tsx +119 -0
- package/src/blocks/StatsSection.tsx +103 -0
- package/src/blocks/SuperHero.tsx +328 -0
- package/src/blocks/TestimonialSection.tsx +122 -0
- package/src/blocks/index.ts +9 -0
- package/src/components/README.md +2018 -0
- package/src/components/breadcrumb-navigation.tsx +127 -0
- package/src/components/breadcrumb.tsx +132 -0
- package/src/components/button-download.tsx +275 -0
- package/src/components/dropdown-menu.tsx +219 -0
- package/src/components/index.ts +86 -0
- package/src/components/markdown/MarkdownMessage.tsx +338 -0
- package/src/components/markdown/index.ts +5 -0
- package/src/components/menubar.tsx +274 -0
- package/src/components/multi-select-pro/async.tsx +608 -0
- package/src/components/multi-select-pro/helpers.tsx +84 -0
- package/src/components/multi-select-pro/index.tsx +622 -0
- package/src/components/navigation-menu.tsx +153 -0
- package/src/components/pagination-static.tsx +348 -0
- package/src/components/pagination.tsx +138 -0
- package/src/components/phone-input.tsx +276 -0
- package/src/components/sidebar.tsx +866 -0
- package/src/components/sonner.tsx +31 -0
- package/src/components/ssr-pagination.tsx +237 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/useCfgRouter.ts +153 -0
- package/src/hooks/useLocalStorage.ts +221 -0
- package/src/hooks/useQueryParams.ts +73 -0
- package/src/hooks/useSessionStorage.ts +188 -0
- package/src/hooks/useTheme.ts +57 -0
- package/src/index.ts +24 -0
- package/src/lib/index.ts +2 -0
- package/src/styles/index.css +2 -0
- package/src/theme/ForceTheme.tsx +115 -0
- package/src/theme/ThemeProvider.tsx +82 -0
- package/src/theme/ThemeToggle.tsx +52 -0
- package/src/theme/index.ts +3 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +199 -0
- package/src/tools/JsonForm/examples/BotConfigExample.tsx +245 -0
- package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +157 -0
- package/src/tools/JsonForm/index.ts +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +73 -0
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +106 -0
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +34 -0
- package/src/tools/JsonForm/templates/FieldTemplate.tsx +61 -0
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +43 -0
- package/src/tools/JsonForm/templates/index.ts +12 -0
- package/src/tools/JsonForm/types.ts +83 -0
- package/src/tools/JsonForm/utils.ts +212 -0
- package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +36 -0
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +88 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +100 -0
- package/src/tools/JsonForm/widgets/SwitchWidget.tsx +34 -0
- package/src/tools/JsonForm/widgets/TextWidget.tsx +95 -0
- package/src/tools/JsonForm/widgets/index.ts +12 -0
- package/src/tools/JsonTree/index.tsx +252 -0
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +212 -0
- package/src/tools/LottiePlayer/index.tsx +54 -0
- package/src/tools/LottiePlayer/types.ts +108 -0
- package/src/tools/LottiePlayer/useLottie.ts +163 -0
- package/src/tools/Mermaid/Mermaid.client.tsx +341 -0
- package/src/tools/Mermaid/index.tsx +40 -0
- package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +144 -0
- package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +255 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +123 -0
- package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +98 -0
- package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +164 -0
- package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
- package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +169 -0
- package/src/tools/OpenapiViewer/components/VersionSelector.tsx +64 -0
- package/src/tools/OpenapiViewer/components/index.ts +14 -0
- package/src/tools/OpenapiViewer/constants.ts +39 -0
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +338 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
- package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +203 -0
- package/src/tools/OpenapiViewer/index.tsx +36 -0
- package/src/tools/OpenapiViewer/types.ts +152 -0
- package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
- package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
- package/src/tools/OpenapiViewer/utils/index.ts +9 -0
- package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +217 -0
- package/src/tools/PrettyCode/index.tsx +43 -0
- package/src/tools/VideoPlayer/README.md +239 -0
- package/src/tools/VideoPlayer/VideoControls.tsx +138 -0
- package/src/tools/VideoPlayer/VideoPlayer.tsx +230 -0
- package/src/tools/VideoPlayer/index.ts +9 -0
- package/src/tools/VideoPlayer/types.ts +62 -0
- package/src/tools/index.ts +43 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
import { Check, ChevronsUpDown, X, XCircle } from "lucide-react"
|
|
6
|
+
import { cn } from "@djangocfg/ui-core/lib"
|
|
7
|
+
import {
|
|
8
|
+
Button,
|
|
9
|
+
Command,
|
|
10
|
+
CommandEmpty,
|
|
11
|
+
CommandGroup,
|
|
12
|
+
CommandInput,
|
|
13
|
+
CommandItem,
|
|
14
|
+
CommandList,
|
|
15
|
+
CommandSeparator,
|
|
16
|
+
Popover,
|
|
17
|
+
PopoverContent,
|
|
18
|
+
PopoverTrigger,
|
|
19
|
+
Badge,
|
|
20
|
+
Separator,
|
|
21
|
+
} from "@djangocfg/ui-core/components"
|
|
22
|
+
|
|
23
|
+
// ==================== TYPES ====================
|
|
24
|
+
|
|
25
|
+
export interface MultiSelectProOption {
|
|
26
|
+
label: string
|
|
27
|
+
value: string
|
|
28
|
+
description?: string // Optional subtitle/description text shown below label
|
|
29
|
+
icon?: React.ComponentType<{ className?: string }>
|
|
30
|
+
disabled?: boolean
|
|
31
|
+
style?: {
|
|
32
|
+
badgeColor?: string
|
|
33
|
+
iconColor?: string
|
|
34
|
+
gradient?: string
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MultiSelectProGroup {
|
|
39
|
+
heading: string
|
|
40
|
+
options: MultiSelectProOption[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AnimationConfig {
|
|
44
|
+
badgeAnimation?: "bounce" | "pulse" | "wiggle" | "fade" | "slide" | "none"
|
|
45
|
+
popoverAnimation?: "scale" | "slide" | "fade" | "flip" | "none"
|
|
46
|
+
optionHoverAnimation?: "highlight" | "scale" | "glow" | "none"
|
|
47
|
+
duration?: number
|
|
48
|
+
delay?: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ResponsiveConfig {
|
|
52
|
+
mobile?: { maxDisplay?: number; compact?: boolean }
|
|
53
|
+
tablet?: { maxDisplay?: number; compact?: boolean }
|
|
54
|
+
desktop?: { maxDisplay?: number; compact?: boolean }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface MultiSelectProRef {
|
|
58
|
+
reset: () => void
|
|
59
|
+
getSelectedValues: () => string[]
|
|
60
|
+
setSelectedValues: (values: string[]) => void
|
|
61
|
+
clear: () => void
|
|
62
|
+
focus: () => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ==================== VARIANTS ====================
|
|
66
|
+
|
|
67
|
+
const multiSelectVariants = cva(
|
|
68
|
+
"w-full justify-between min-h-10 h-auto py-2",
|
|
69
|
+
{
|
|
70
|
+
variants: {
|
|
71
|
+
variant: {
|
|
72
|
+
default: "border-input bg-background hover:bg-accent/50",
|
|
73
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
74
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
75
|
+
inverted: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
defaultVariants: {
|
|
79
|
+
variant: "default",
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const badgeAnimations = {
|
|
85
|
+
bounce: "animate-bounce",
|
|
86
|
+
pulse: "animate-pulse",
|
|
87
|
+
wiggle: "animate-wiggle",
|
|
88
|
+
fade: "animate-fadeIn",
|
|
89
|
+
slide: "animate-slideIn",
|
|
90
|
+
none: "",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const popoverAnimations = {
|
|
94
|
+
scale: "data-[state=open]:animate-scaleIn data-[state=closed]:animate-scaleOut",
|
|
95
|
+
slide: "data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp",
|
|
96
|
+
fade: "data-[state=open]:animate-fadeIn data-[state=closed]:animate-fadeOut",
|
|
97
|
+
flip: "data-[state=open]:animate-flipIn data-[state=closed]:animate-flipOut",
|
|
98
|
+
none: "",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ==================== PROPS ====================
|
|
102
|
+
|
|
103
|
+
export interface MultiSelectProProps extends VariantProps<typeof multiSelectVariants> {
|
|
104
|
+
options: MultiSelectProOption[] | MultiSelectProGroup[]
|
|
105
|
+
onValueChange?: (value: string[]) => void
|
|
106
|
+
defaultValue?: string[]
|
|
107
|
+
placeholder?: string
|
|
108
|
+
variant?: "default" | "secondary" | "destructive" | "inverted"
|
|
109
|
+
animation?: number
|
|
110
|
+
animationConfig?: AnimationConfig
|
|
111
|
+
maxCount?: number
|
|
112
|
+
modalPopover?: boolean
|
|
113
|
+
asChild?: boolean
|
|
114
|
+
className?: string
|
|
115
|
+
hideSelectAll?: boolean
|
|
116
|
+
searchable?: boolean
|
|
117
|
+
emptyIndicator?: React.ReactNode
|
|
118
|
+
autoSize?: boolean
|
|
119
|
+
singleLine?: boolean
|
|
120
|
+
popoverClassName?: string
|
|
121
|
+
disabled?: boolean
|
|
122
|
+
responsive?: boolean | ResponsiveConfig
|
|
123
|
+
minWidth?: string
|
|
124
|
+
maxWidth?: string
|
|
125
|
+
deduplicateOptions?: boolean
|
|
126
|
+
resetOnDefaultValueChange?: boolean
|
|
127
|
+
closeOnSelect?: boolean
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ==================== HELPERS ====================
|
|
131
|
+
|
|
132
|
+
function isGroupedOptions(options: MultiSelectProOption[] | MultiSelectProGroup[]): options is MultiSelectProGroup[] {
|
|
133
|
+
return options.length > 0 && 'heading' in options[0]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function flattenOptions(options: MultiSelectProOption[] | MultiSelectProGroup[]): MultiSelectProOption[] {
|
|
137
|
+
if (isGroupedOptions(options)) {
|
|
138
|
+
return options.flatMap((group) => group.options)
|
|
139
|
+
}
|
|
140
|
+
return options
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function deduplicateOptions(options: MultiSelectProOption[]): MultiSelectProOption[] {
|
|
144
|
+
const seen = new Set<string>()
|
|
145
|
+
return options.filter((option) => {
|
|
146
|
+
if (seen.has(option.value)) {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
seen.add(option.value)
|
|
150
|
+
return true
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ==================== COMPONENT ====================
|
|
155
|
+
|
|
156
|
+
export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectProProps>(
|
|
157
|
+
(
|
|
158
|
+
{
|
|
159
|
+
options,
|
|
160
|
+
onValueChange,
|
|
161
|
+
defaultValue = [],
|
|
162
|
+
placeholder = "Select options",
|
|
163
|
+
variant = "default",
|
|
164
|
+
animation = 0,
|
|
165
|
+
animationConfig,
|
|
166
|
+
maxCount = 3,
|
|
167
|
+
modalPopover = false,
|
|
168
|
+
className,
|
|
169
|
+
hideSelectAll = false,
|
|
170
|
+
searchable = true,
|
|
171
|
+
emptyIndicator,
|
|
172
|
+
autoSize = false,
|
|
173
|
+
singleLine = false,
|
|
174
|
+
popoverClassName,
|
|
175
|
+
disabled = false,
|
|
176
|
+
responsive = false,
|
|
177
|
+
minWidth,
|
|
178
|
+
maxWidth,
|
|
179
|
+
deduplicateOptions: shouldDeduplicate = false,
|
|
180
|
+
resetOnDefaultValueChange = true,
|
|
181
|
+
closeOnSelect = false,
|
|
182
|
+
},
|
|
183
|
+
ref
|
|
184
|
+
) => {
|
|
185
|
+
const [open, setOpen] = React.useState(false)
|
|
186
|
+
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue)
|
|
187
|
+
const [search, setSearch] = React.useState("")
|
|
188
|
+
const buttonRef = React.useRef<HTMLButtonElement>(null)
|
|
189
|
+
const [announcements, setAnnouncements] = React.useState<string>("")
|
|
190
|
+
// Cache selected options to persist them when they disappear from current options
|
|
191
|
+
const [selectedOptionsCache, setSelectedOptionsCache] = React.useState<Map<string, MultiSelectProOption>>(new Map())
|
|
192
|
+
|
|
193
|
+
// Process options
|
|
194
|
+
const flatOptions = React.useMemo(() => {
|
|
195
|
+
const flat = flattenOptions(options)
|
|
196
|
+
return shouldDeduplicate ? deduplicateOptions(flat) : flat
|
|
197
|
+
}, [options, shouldDeduplicate])
|
|
198
|
+
|
|
199
|
+
// Update cache whenever new options appear
|
|
200
|
+
React.useEffect(() => {
|
|
201
|
+
setSelectedOptionsCache(prev => {
|
|
202
|
+
const updated = new Map(prev)
|
|
203
|
+
flatOptions.forEach(option => {
|
|
204
|
+
if (selectedValues.includes(option.value)) {
|
|
205
|
+
updated.set(option.value, option)
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
return updated
|
|
209
|
+
})
|
|
210
|
+
}, [flatOptions, selectedValues])
|
|
211
|
+
|
|
212
|
+
// Responsive configuration
|
|
213
|
+
const responsiveConfig = React.useMemo((): ResponsiveConfig => {
|
|
214
|
+
if (typeof responsive === 'boolean') {
|
|
215
|
+
return responsive
|
|
216
|
+
? {
|
|
217
|
+
mobile: { maxDisplay: 2, compact: true },
|
|
218
|
+
tablet: { maxDisplay: 4, compact: false },
|
|
219
|
+
desktop: { maxDisplay: 6, compact: false },
|
|
220
|
+
}
|
|
221
|
+
: {}
|
|
222
|
+
}
|
|
223
|
+
return responsive || {}
|
|
224
|
+
}, [responsive])
|
|
225
|
+
|
|
226
|
+
// Animation configuration
|
|
227
|
+
const animConfig = React.useMemo(
|
|
228
|
+
(): AnimationConfig => ({
|
|
229
|
+
badgeAnimation: animationConfig?.badgeAnimation || (animation > 0 ? "bounce" : "none"),
|
|
230
|
+
popoverAnimation: animationConfig?.popoverAnimation || "scale",
|
|
231
|
+
optionHoverAnimation: animationConfig?.optionHoverAnimation || "highlight",
|
|
232
|
+
duration: animationConfig?.duration || animation || 0.3,
|
|
233
|
+
delay: animationConfig?.delay || 0,
|
|
234
|
+
}),
|
|
235
|
+
[animation, animationConfig]
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
// Reset on defaultValue change
|
|
239
|
+
React.useEffect(() => {
|
|
240
|
+
if (resetOnDefaultValueChange) {
|
|
241
|
+
setSelectedValues(defaultValue)
|
|
242
|
+
}
|
|
243
|
+
}, [defaultValue, resetOnDefaultValueChange])
|
|
244
|
+
|
|
245
|
+
// Announce changes for screen readers
|
|
246
|
+
const announce = React.useCallback((message: string) => {
|
|
247
|
+
setAnnouncements(message)
|
|
248
|
+
setTimeout(() => setAnnouncements(""), 1000)
|
|
249
|
+
}, [])
|
|
250
|
+
|
|
251
|
+
// Toggle selection
|
|
252
|
+
const toggleOption = React.useCallback(
|
|
253
|
+
(value: string) => {
|
|
254
|
+
const isRemoving = selectedValues.includes(value)
|
|
255
|
+
const newValues = isRemoving
|
|
256
|
+
? selectedValues.filter((v) => v !== value)
|
|
257
|
+
: [...selectedValues, value]
|
|
258
|
+
|
|
259
|
+
setSelectedValues(newValues)
|
|
260
|
+
onValueChange?.(newValues)
|
|
261
|
+
|
|
262
|
+
const option = flatOptions.find((o) => o.value === value)
|
|
263
|
+
if (option) {
|
|
264
|
+
// Add to cache when selecting
|
|
265
|
+
if (!isRemoving) {
|
|
266
|
+
setSelectedOptionsCache(prev => new Map(prev).set(value, option))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
announce(
|
|
270
|
+
isRemoving
|
|
271
|
+
? `Removed ${option.label}`
|
|
272
|
+
: `Added ${option.label}`
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (closeOnSelect) {
|
|
277
|
+
setOpen(false)
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
[selectedValues, onValueChange, flatOptions, announce, closeOnSelect]
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
// Select all
|
|
284
|
+
const handleSelectAll = React.useCallback(() => {
|
|
285
|
+
const allValues = flatOptions.filter((o) => !o.disabled).map((o) => o.value)
|
|
286
|
+
setSelectedValues(allValues)
|
|
287
|
+
onValueChange?.(allValues)
|
|
288
|
+
|
|
289
|
+
// Cache all selected options
|
|
290
|
+
setSelectedOptionsCache(prev => {
|
|
291
|
+
const updated = new Map(prev)
|
|
292
|
+
flatOptions.forEach(option => {
|
|
293
|
+
if (!option.disabled) {
|
|
294
|
+
updated.set(option.value, option)
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
return updated
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
announce(`Selected all ${allValues.length} options`)
|
|
301
|
+
}, [flatOptions, onValueChange, announce])
|
|
302
|
+
|
|
303
|
+
// Clear all
|
|
304
|
+
const handleClearAll = React.useCallback(() => {
|
|
305
|
+
setSelectedValues([])
|
|
306
|
+
onValueChange?.([])
|
|
307
|
+
announce("Cleared all selections")
|
|
308
|
+
}, [onValueChange, announce])
|
|
309
|
+
|
|
310
|
+
// Imperative methods
|
|
311
|
+
React.useImperativeHandle(ref, () => ({
|
|
312
|
+
reset: () => {
|
|
313
|
+
setSelectedValues(defaultValue)
|
|
314
|
+
announce("Reset to default values")
|
|
315
|
+
},
|
|
316
|
+
getSelectedValues: () => selectedValues,
|
|
317
|
+
setSelectedValues: (values: string[]) => {
|
|
318
|
+
setSelectedValues(values)
|
|
319
|
+
onValueChange?.(values)
|
|
320
|
+
},
|
|
321
|
+
clear: handleClearAll,
|
|
322
|
+
focus: () => buttonRef.current?.focus(),
|
|
323
|
+
}))
|
|
324
|
+
|
|
325
|
+
// Filter options
|
|
326
|
+
const filteredOptions = React.useMemo(() => {
|
|
327
|
+
if (!search) return options
|
|
328
|
+
const searchLower = search.toLowerCase()
|
|
329
|
+
|
|
330
|
+
if (isGroupedOptions(options)) {
|
|
331
|
+
return options
|
|
332
|
+
.map((group) => ({
|
|
333
|
+
...group,
|
|
334
|
+
options: group.options.filter(
|
|
335
|
+
(option) =>
|
|
336
|
+
option.label.toLowerCase().includes(searchLower) ||
|
|
337
|
+
option.value.toLowerCase().includes(searchLower)
|
|
338
|
+
),
|
|
339
|
+
}))
|
|
340
|
+
.filter((group) => group.options.length > 0)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return options.filter(
|
|
344
|
+
(option) =>
|
|
345
|
+
option.label.toLowerCase().includes(searchLower) ||
|
|
346
|
+
option.value.toLowerCase().includes(searchLower)
|
|
347
|
+
)
|
|
348
|
+
}, [options, search])
|
|
349
|
+
|
|
350
|
+
// Selected options for display - use cache as fallback
|
|
351
|
+
const selectedOptions = React.useMemo(
|
|
352
|
+
() => selectedValues.map(value => {
|
|
353
|
+
// First try to find in current options
|
|
354
|
+
const option = flatOptions.find(o => o.value === value)
|
|
355
|
+
// If not found, fallback to cache
|
|
356
|
+
return option || selectedOptionsCache.get(value)
|
|
357
|
+
}).filter((option): option is MultiSelectProOption => option !== undefined),
|
|
358
|
+
[flatOptions, selectedValues, selectedOptionsCache]
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
// Render badge with custom styles
|
|
362
|
+
const renderBadge = (option: MultiSelectProOption, index: number) => {
|
|
363
|
+
const { style, icon: Icon } = option
|
|
364
|
+
const badgeStyle: React.CSSProperties = {}
|
|
365
|
+
|
|
366
|
+
if (style?.gradient) {
|
|
367
|
+
badgeStyle.background = style.gradient
|
|
368
|
+
badgeStyle.color = style.iconColor || "white"
|
|
369
|
+
} else if (style?.badgeColor) {
|
|
370
|
+
badgeStyle.backgroundColor = style.badgeColor
|
|
371
|
+
badgeStyle.color = style.iconColor || "white"
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const animationClass = animConfig.badgeAnimation
|
|
375
|
+
? badgeAnimations[animConfig.badgeAnimation]
|
|
376
|
+
: ""
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<Badge
|
|
380
|
+
key={option.value}
|
|
381
|
+
variant={variant === "default" ? "secondary" : "outline"}
|
|
382
|
+
className={cn(
|
|
383
|
+
"mr-1 mb-1 text-xs gap-1 flex items-center",
|
|
384
|
+
animationClass
|
|
385
|
+
)}
|
|
386
|
+
style={{
|
|
387
|
+
...badgeStyle,
|
|
388
|
+
animationDelay: `${(animConfig.delay || 0) * index}s`,
|
|
389
|
+
animationDuration: `${animConfig.duration}s`,
|
|
390
|
+
}}
|
|
391
|
+
>
|
|
392
|
+
{Icon && <Icon className="h-3 w-3" />}
|
|
393
|
+
<span>{option.label}</span>
|
|
394
|
+
{!disabled && (
|
|
395
|
+
<button
|
|
396
|
+
className="ml-1 rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
397
|
+
onClick={(e) => {
|
|
398
|
+
e.stopPropagation()
|
|
399
|
+
toggleOption(option.value)
|
|
400
|
+
}}
|
|
401
|
+
aria-label={`Remove ${option.label}`}
|
|
402
|
+
>
|
|
403
|
+
<X className="h-3 w-3" />
|
|
404
|
+
</button>
|
|
405
|
+
)}
|
|
406
|
+
</Badge>
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Display value
|
|
411
|
+
const displayValue = React.useMemo(() => {
|
|
412
|
+
if (selectedOptions.length === 0) {
|
|
413
|
+
return <span className="text-muted-foreground">{placeholder}</span>
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const displayed = selectedOptions.slice(0, maxCount)
|
|
417
|
+
const remaining = selectedOptions.length - maxCount
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
<div className={cn("flex gap-1", singleLine ? "flex-nowrap overflow-x-auto" : "flex-wrap")}>
|
|
421
|
+
{displayed.map((option, index) => renderBadge(option, index))}
|
|
422
|
+
{remaining > 0 && (
|
|
423
|
+
<Badge variant="outline" className="text-xs">
|
|
424
|
+
+{remaining} more
|
|
425
|
+
</Badge>
|
|
426
|
+
)}
|
|
427
|
+
</div>
|
|
428
|
+
)
|
|
429
|
+
}, [selectedOptions, maxCount, placeholder, singleLine, variant, disabled, animConfig])
|
|
430
|
+
|
|
431
|
+
// Render options
|
|
432
|
+
const renderOptions = () => {
|
|
433
|
+
if (isGroupedOptions(filteredOptions)) {
|
|
434
|
+
return filteredOptions.map((group, groupIndex) => (
|
|
435
|
+
<React.Fragment key={group.heading}>
|
|
436
|
+
{groupIndex > 0 && <CommandSeparator />}
|
|
437
|
+
<CommandGroup heading={group.heading}>
|
|
438
|
+
{group.options.map((option) => {
|
|
439
|
+
const isSelected = selectedValues.includes(option.value)
|
|
440
|
+
const Icon = option.icon
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<CommandItem
|
|
444
|
+
key={option.value}
|
|
445
|
+
value={option.value}
|
|
446
|
+
onSelect={() => !option.disabled && toggleOption(option.value)}
|
|
447
|
+
disabled={option.disabled}
|
|
448
|
+
className={cn(
|
|
449
|
+
"cursor-pointer",
|
|
450
|
+
option.disabled && "opacity-50 cursor-not-allowed"
|
|
451
|
+
)}
|
|
452
|
+
>
|
|
453
|
+
<Check
|
|
454
|
+
className={cn(
|
|
455
|
+
"mr-2 h-4 w-4 shrink-0",
|
|
456
|
+
isSelected ? "opacity-100" : "opacity-0"
|
|
457
|
+
)}
|
|
458
|
+
/>
|
|
459
|
+
{Icon && <Icon className="mr-2 h-4 w-4" />}
|
|
460
|
+
<div className="flex-1 flex flex-col gap-0.5">
|
|
461
|
+
<span>{option.label}</span>
|
|
462
|
+
{option.description && (
|
|
463
|
+
<span className="text-xs text-muted-foreground">{option.description}</span>
|
|
464
|
+
)}
|
|
465
|
+
</div>
|
|
466
|
+
</CommandItem>
|
|
467
|
+
)
|
|
468
|
+
})}
|
|
469
|
+
</CommandGroup>
|
|
470
|
+
</React.Fragment>
|
|
471
|
+
))
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return (
|
|
475
|
+
<CommandGroup>
|
|
476
|
+
{(filteredOptions as MultiSelectProOption[]).map((option) => {
|
|
477
|
+
const isSelected = selectedValues.includes(option.value)
|
|
478
|
+
const Icon = option.icon
|
|
479
|
+
|
|
480
|
+
return (
|
|
481
|
+
<CommandItem
|
|
482
|
+
key={option.value}
|
|
483
|
+
value={option.value}
|
|
484
|
+
onSelect={() => !option.disabled && toggleOption(option.value)}
|
|
485
|
+
disabled={option.disabled}
|
|
486
|
+
className={cn(
|
|
487
|
+
"cursor-pointer",
|
|
488
|
+
option.disabled && "opacity-50 cursor-not-allowed"
|
|
489
|
+
)}
|
|
490
|
+
>
|
|
491
|
+
<Check
|
|
492
|
+
className={cn(
|
|
493
|
+
"mr-2 h-4 w-4 shrink-0",
|
|
494
|
+
isSelected ? "opacity-100" : "opacity-0"
|
|
495
|
+
)}
|
|
496
|
+
/>
|
|
497
|
+
{Icon && <Icon className="mr-2 h-4 w-4" />}
|
|
498
|
+
<div className="flex-1 flex flex-col gap-0.5">
|
|
499
|
+
<span>{option.label}</span>
|
|
500
|
+
{option.description && (
|
|
501
|
+
<span className="text-xs text-muted-foreground">{option.description}</span>
|
|
502
|
+
)}
|
|
503
|
+
</div>
|
|
504
|
+
</CommandItem>
|
|
505
|
+
)
|
|
506
|
+
})}
|
|
507
|
+
</CommandGroup>
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const containerStyle: React.CSSProperties = {}
|
|
512
|
+
if (minWidth) containerStyle.minWidth = minWidth
|
|
513
|
+
if (maxWidth) containerStyle.maxWidth = maxWidth
|
|
514
|
+
|
|
515
|
+
return (
|
|
516
|
+
<div style={containerStyle} className="relative">
|
|
517
|
+
{/* ARIA Live Region for announcements */}
|
|
518
|
+
<div
|
|
519
|
+
role="status"
|
|
520
|
+
aria-live="polite"
|
|
521
|
+
aria-atomic="true"
|
|
522
|
+
className="sr-only"
|
|
523
|
+
>
|
|
524
|
+
{announcements}
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<Popover
|
|
528
|
+
open={open}
|
|
529
|
+
onOpenChange={(isOpen) => {
|
|
530
|
+
if (!disabled) {
|
|
531
|
+
setOpen(isOpen)
|
|
532
|
+
if (isOpen) {
|
|
533
|
+
announce(`Dropdown opened. ${flatOptions.length} options available`)
|
|
534
|
+
} else {
|
|
535
|
+
setSearch("")
|
|
536
|
+
announce("Dropdown closed")
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}}
|
|
540
|
+
modal={modalPopover}
|
|
541
|
+
>
|
|
542
|
+
<PopoverTrigger asChild>
|
|
543
|
+
<Button
|
|
544
|
+
ref={buttonRef}
|
|
545
|
+
variant="outline"
|
|
546
|
+
role="combobox"
|
|
547
|
+
aria-expanded={open}
|
|
548
|
+
aria-label={placeholder}
|
|
549
|
+
className={cn(multiSelectVariants({ variant }), className)}
|
|
550
|
+
disabled={disabled}
|
|
551
|
+
>
|
|
552
|
+
<div className={cn("flex-1 text-left", autoSize ? "" : "overflow-hidden")}>
|
|
553
|
+
{displayValue}
|
|
554
|
+
</div>
|
|
555
|
+
<div className="flex items-center gap-1 ml-2">
|
|
556
|
+
{selectedValues.length > 0 && !disabled && (
|
|
557
|
+
<button
|
|
558
|
+
onClick={(e) => {
|
|
559
|
+
e.stopPropagation()
|
|
560
|
+
handleClearAll()
|
|
561
|
+
}}
|
|
562
|
+
className="rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
563
|
+
aria-label="Clear all selections"
|
|
564
|
+
>
|
|
565
|
+
<XCircle className="h-4 w-4 shrink-0 opacity-50" />
|
|
566
|
+
</button>
|
|
567
|
+
)}
|
|
568
|
+
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
|
569
|
+
</div>
|
|
570
|
+
</Button>
|
|
571
|
+
</PopoverTrigger>
|
|
572
|
+
<PopoverContent
|
|
573
|
+
className={cn(
|
|
574
|
+
"w-[var(--radix-popover-trigger-width)] p-0",
|
|
575
|
+
animConfig.popoverAnimation ? popoverAnimations[animConfig.popoverAnimation] : "",
|
|
576
|
+
popoverClassName
|
|
577
|
+
)}
|
|
578
|
+
align="start"
|
|
579
|
+
style={{
|
|
580
|
+
animationDuration: `${animConfig.duration}s`,
|
|
581
|
+
}}
|
|
582
|
+
>
|
|
583
|
+
<Command shouldFilter={false}>
|
|
584
|
+
{searchable && (
|
|
585
|
+
<CommandInput
|
|
586
|
+
placeholder="Search..."
|
|
587
|
+
value={search}
|
|
588
|
+
onValueChange={setSearch}
|
|
589
|
+
/>
|
|
590
|
+
)}
|
|
591
|
+
<CommandList>
|
|
592
|
+
{!hideSelectAll && !isGroupedOptions(options) && (
|
|
593
|
+
<>
|
|
594
|
+
<CommandGroup>
|
|
595
|
+
<CommandItem
|
|
596
|
+
onSelect={handleSelectAll}
|
|
597
|
+
className="cursor-pointer justify-center font-medium"
|
|
598
|
+
>
|
|
599
|
+
Select All
|
|
600
|
+
</CommandItem>
|
|
601
|
+
</CommandGroup>
|
|
602
|
+
<Separator />
|
|
603
|
+
</>
|
|
604
|
+
)}
|
|
605
|
+
|
|
606
|
+
{filteredOptions.length === 0 ? (
|
|
607
|
+
<CommandEmpty>
|
|
608
|
+
{emptyIndicator || "No results found."}
|
|
609
|
+
</CommandEmpty>
|
|
610
|
+
) : (
|
|
611
|
+
renderOptions()
|
|
612
|
+
)}
|
|
613
|
+
</CommandList>
|
|
614
|
+
</Command>
|
|
615
|
+
</PopoverContent>
|
|
616
|
+
</Popover>
|
|
617
|
+
</div>
|
|
618
|
+
)
|
|
619
|
+
}
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
MultiSelectPro.displayName = "MultiSelectPro"
|