@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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/package.json +110 -0
  4. package/src/animations/AnimatedBackground.tsx +645 -0
  5. package/src/animations/index.ts +2 -0
  6. package/src/blocks/ArticleCard.tsx +94 -0
  7. package/src/blocks/ArticleList.tsx +95 -0
  8. package/src/blocks/CTASection.tsx +136 -0
  9. package/src/blocks/FeatureSection.tsx +104 -0
  10. package/src/blocks/Hero.tsx +102 -0
  11. package/src/blocks/NewsletterSection.tsx +119 -0
  12. package/src/blocks/StatsSection.tsx +103 -0
  13. package/src/blocks/SuperHero.tsx +328 -0
  14. package/src/blocks/TestimonialSection.tsx +122 -0
  15. package/src/blocks/index.ts +9 -0
  16. package/src/components/README.md +2018 -0
  17. package/src/components/breadcrumb-navigation.tsx +127 -0
  18. package/src/components/breadcrumb.tsx +132 -0
  19. package/src/components/button-download.tsx +275 -0
  20. package/src/components/dropdown-menu.tsx +219 -0
  21. package/src/components/index.ts +86 -0
  22. package/src/components/markdown/MarkdownMessage.tsx +338 -0
  23. package/src/components/markdown/index.ts +5 -0
  24. package/src/components/menubar.tsx +274 -0
  25. package/src/components/multi-select-pro/async.tsx +608 -0
  26. package/src/components/multi-select-pro/helpers.tsx +84 -0
  27. package/src/components/multi-select-pro/index.tsx +622 -0
  28. package/src/components/navigation-menu.tsx +153 -0
  29. package/src/components/pagination-static.tsx +348 -0
  30. package/src/components/pagination.tsx +138 -0
  31. package/src/components/phone-input.tsx +276 -0
  32. package/src/components/sidebar.tsx +866 -0
  33. package/src/components/sonner.tsx +31 -0
  34. package/src/components/ssr-pagination.tsx +237 -0
  35. package/src/hooks/index.ts +19 -0
  36. package/src/hooks/useCfgRouter.ts +153 -0
  37. package/src/hooks/useLocalStorage.ts +221 -0
  38. package/src/hooks/useQueryParams.ts +73 -0
  39. package/src/hooks/useSessionStorage.ts +188 -0
  40. package/src/hooks/useTheme.ts +57 -0
  41. package/src/index.ts +24 -0
  42. package/src/lib/index.ts +2 -0
  43. package/src/styles/index.css +2 -0
  44. package/src/theme/ForceTheme.tsx +115 -0
  45. package/src/theme/ThemeProvider.tsx +82 -0
  46. package/src/theme/ThemeToggle.tsx +52 -0
  47. package/src/theme/index.ts +3 -0
  48. package/src/tools/JsonForm/JsonSchemaForm.tsx +199 -0
  49. package/src/tools/JsonForm/examples/BotConfigExample.tsx +245 -0
  50. package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +157 -0
  51. package/src/tools/JsonForm/index.ts +46 -0
  52. package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +46 -0
  53. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +73 -0
  54. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +106 -0
  55. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +34 -0
  56. package/src/tools/JsonForm/templates/FieldTemplate.tsx +61 -0
  57. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +43 -0
  58. package/src/tools/JsonForm/templates/index.ts +12 -0
  59. package/src/tools/JsonForm/types.ts +83 -0
  60. package/src/tools/JsonForm/utils.ts +212 -0
  61. package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +36 -0
  62. package/src/tools/JsonForm/widgets/NumberWidget.tsx +88 -0
  63. package/src/tools/JsonForm/widgets/SelectWidget.tsx +100 -0
  64. package/src/tools/JsonForm/widgets/SwitchWidget.tsx +34 -0
  65. package/src/tools/JsonForm/widgets/TextWidget.tsx +95 -0
  66. package/src/tools/JsonForm/widgets/index.ts +12 -0
  67. package/src/tools/JsonTree/index.tsx +252 -0
  68. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +212 -0
  69. package/src/tools/LottiePlayer/index.tsx +54 -0
  70. package/src/tools/LottiePlayer/types.ts +108 -0
  71. package/src/tools/LottiePlayer/useLottie.ts +163 -0
  72. package/src/tools/Mermaid/Mermaid.client.tsx +341 -0
  73. package/src/tools/Mermaid/index.tsx +40 -0
  74. package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +144 -0
  75. package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +255 -0
  76. package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +123 -0
  77. package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +98 -0
  78. package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +164 -0
  79. package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
  80. package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +169 -0
  81. package/src/tools/OpenapiViewer/components/VersionSelector.tsx +64 -0
  82. package/src/tools/OpenapiViewer/components/index.ts +14 -0
  83. package/src/tools/OpenapiViewer/constants.ts +39 -0
  84. package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +338 -0
  85. package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
  86. package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
  87. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +203 -0
  88. package/src/tools/OpenapiViewer/index.tsx +36 -0
  89. package/src/tools/OpenapiViewer/types.ts +152 -0
  90. package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
  91. package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
  92. package/src/tools/OpenapiViewer/utils/index.ts +9 -0
  93. package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
  94. package/src/tools/PrettyCode/PrettyCode.client.tsx +217 -0
  95. package/src/tools/PrettyCode/index.tsx +43 -0
  96. package/src/tools/VideoPlayer/README.md +239 -0
  97. package/src/tools/VideoPlayer/VideoControls.tsx +138 -0
  98. package/src/tools/VideoPlayer/VideoPlayer.tsx +230 -0
  99. package/src/tools/VideoPlayer/index.ts +9 -0
  100. package/src/tools/VideoPlayer/types.ts +62 -0
  101. 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"