@djangocfg/ui-nextjs 2.1.90 → 2.1.92

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