@catalystsoftware/ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +7 -0
  2. package/components/catalyst-ui/buttons/burger.tsx +207 -0
  3. package/components/catalyst-ui/core/data-display/timeline.tsx +210 -0
  4. package/components/catalyst-ui/core/feedback/alert.tsx +491 -0
  5. package/components/catalyst-ui/core/feedback/spinner-1.tsx +65 -0
  6. package/components/catalyst-ui/core/feedback/toast.tsx +1857 -0
  7. package/components/catalyst-ui/core/navigation/menu.tsx +164 -0
  8. package/components/catalyst-ui/forms/toggle-class.tsx +176 -0
  9. package/components/catalyst-ui/hooks/use-copy-to-clipboard.tsx +419 -0
  10. package/components/catalyst-ui/hooks/use-counter.tsx +13 -0
  11. package/components/catalyst-ui/hooks/use-event-listener.tsx +23 -0
  12. package/components/catalyst-ui/hooks/use-export-markdown.tsx +47 -0
  13. package/components/catalyst-ui/hooks/use-focus.tsx +17 -0
  14. package/components/catalyst-ui/hooks/use-interval.tsx +23 -0
  15. package/components/catalyst-ui/hooks/use-is-client.tsx +16 -0
  16. package/components/catalyst-ui/hooks/use-media-query.tsx +19 -0
  17. package/components/catalyst-ui/hooks/use-mobile.tsx +19 -0
  18. package/components/catalyst-ui/hooks/use-resize-observer.tsx +81 -0
  19. package/components/catalyst-ui/hooks/use-timeout.tsx +21 -0
  20. package/components/catalyst-ui/hooks/use-timer.tsx +209 -0
  21. package/components/catalyst-ui/hooks/use-toggle.tsx +12 -0
  22. package/components/catalyst-ui/media/image.tsx +13 -0
  23. package/components/catalyst-ui/overlays/dual-sidebar.tsx +4142 -0
  24. package/components/catalyst-ui/overlays/sidebar-original.tsx +726 -0
  25. package/components/catalyst-ui/primitives/accordion.tsx +250 -0
  26. package/components/catalyst-ui/primitives/alert-dialog.tsx +126 -0
  27. package/components/catalyst-ui/primitives/aspect-ratio.tsx +9 -0
  28. package/components/catalyst-ui/primitives/avatar.tsx +296 -0
  29. package/components/catalyst-ui/primitives/badge.tsx +57 -0
  30. package/components/catalyst-ui/primitives/breadcrumb.tsx +101 -0
  31. package/components/catalyst-ui/primitives/button.tsx +265 -0
  32. package/components/catalyst-ui/primitives/calendar-v4.tsx +208 -0
  33. package/components/catalyst-ui/primitives/calendar.tsx +295 -0
  34. package/components/catalyst-ui/primitives/card.tsx +618 -0
  35. package/components/catalyst-ui/primitives/carousel.tsx +238 -0
  36. package/components/catalyst-ui/primitives/chart.tsx +347 -0
  37. package/components/catalyst-ui/primitives/checkbox.tsx +225 -0
  38. package/components/catalyst-ui/primitives/collapsible.tsx +212 -0
  39. package/components/catalyst-ui/primitives/command.tsx +393 -0
  40. package/components/catalyst-ui/primitives/context-menu.tsx +236 -0
  41. package/components/catalyst-ui/primitives/dialog.tsx +471 -0
  42. package/components/catalyst-ui/primitives/drawer.tsx +761 -0
  43. package/components/catalyst-ui/primitives/dropdown-menu.tsx +290 -0
  44. package/components/catalyst-ui/primitives/empty.tsx +104 -0
  45. package/components/catalyst-ui/primitives/field.tsx +244 -0
  46. package/components/catalyst-ui/primitives/hover-card.tsx +124 -0
  47. package/components/catalyst-ui/primitives/input-otp.tsx +76 -0
  48. package/components/catalyst-ui/primitives/input.tsx +64 -0
  49. package/components/catalyst-ui/primitives/item.tsx +196 -0
  50. package/components/catalyst-ui/primitives/kbd.tsx +75 -0
  51. package/components/catalyst-ui/primitives/label.tsx +24 -0
  52. package/components/catalyst-ui/primitives/navigation-menu.tsx +150 -0
  53. package/components/catalyst-ui/primitives/pagination.tsx +198 -0
  54. package/components/catalyst-ui/primitives/popover.tsx +232 -0
  55. package/components/catalyst-ui/primitives/progress.tsx +34 -0
  56. package/components/catalyst-ui/primitives/radio-group.tsx +43 -0
  57. package/components/catalyst-ui/primitives/resizable.tsx +56 -0
  58. package/components/catalyst-ui/primitives/select.tsx +155 -0
  59. package/components/catalyst-ui/primitives/separator.tsx +74 -0
  60. package/components/catalyst-ui/primitives/sheet.tsx +126 -0
  61. package/components/catalyst-ui/primitives/skeleton.tsx +15 -0
  62. package/components/catalyst-ui/primitives/slider.tsx +27 -0
  63. package/components/catalyst-ui/primitives/switch.tsx +187 -0
  64. package/components/catalyst-ui/primitives/tabs.tsx +335 -0
  65. package/components/catalyst-ui/primitives/textarea.tsx +24 -0
  66. package/components/catalyst-ui/primitives/toggle-group.tsx +55 -0
  67. package/components/catalyst-ui/primitives/toggle.tsx +42 -0
  68. package/components/catalyst-ui/primitives/tooltip.tsx +116 -0
  69. package/components/catalyst-ui/utils/basic-auth.tsx +40 -0
  70. package/components/catalyst-ui/utils/context-storage.tsx +19 -0
  71. package/components/catalyst-ui/utils/cors-middleware.tsx +71 -0
  72. package/components/catalyst-ui/utils/deferred-content.tsx +595 -0
  73. package/components/catalyst-ui/utils/honeypot-middleware.tsx +38 -0
  74. package/components/catalyst-ui/utils/incId.tsx +75 -0
  75. package/components/catalyst-ui/utils/jwk-auth.tsx +36 -0
  76. package/components/catalyst-ui/utils/request-id.tsx +14 -0
  77. package/components/catalyst-ui/utils/secure-headers.tsx +37 -0
  78. package/components/catalyst-ui/utils/server-timing.tsx +23 -0
  79. package/components/catalyst-ui/utils/utils.ts +43 -0
  80. package/components/catalyst-ui/utils/with-cookie.tsx +43 -0
  81. package/components/catalyst-ui/x/accordian-x.tsx +428 -0
  82. package/components/catalyst-ui/x/alert-x.tsx +413 -0
  83. package/components/catalyst-ui/x/animated-text-x.tsx +2242 -0
  84. package/components/catalyst-ui/x/avatar-x.tsx +515 -0
  85. package/components/catalyst-ui/x/badge-x.tsx +670 -0
  86. package/components/catalyst-ui/x/button-X.tsx +2857 -0
  87. package/components/catalyst-ui/x/button-group-x.tsx +847 -0
  88. package/components/catalyst-ui/x/calendar-x.tsx +1910 -0
  89. package/components/catalyst-ui/x/card-x.tsx +2597 -0
  90. package/components/catalyst-ui/x/checkbox-x.tsx +656 -0
  91. package/components/catalyst-ui/x/collapsible-x.tsx +1360 -0
  92. package/components/catalyst-ui/x/combobox-x.tsx +911 -0
  93. package/components/catalyst-ui/x/data-table-x.tsx +1753 -0
  94. package/components/catalyst-ui/x/date-picker-x.tsx +648 -0
  95. package/components/catalyst-ui/x/dialog-x.tsx +659 -0
  96. package/components/catalyst-ui/x/dropdown-menu-x.tsx +612 -0
  97. package/components/catalyst-ui/x/hover-card-x.tsx +375 -0
  98. package/components/catalyst-ui/x/icon-x.tsx +840 -0
  99. package/components/catalyst-ui/x/input-mask-x.tsx +981 -0
  100. package/components/catalyst-ui/x/input-otp-x.tsx +659 -0
  101. package/components/catalyst-ui/x/loader-x.tsx +1757 -0
  102. package/components/catalyst-ui/x/pagination-x.tsx +622 -0
  103. package/components/catalyst-ui/x/popover-x.tsx +744 -0
  104. package/components/catalyst-ui/x/radio-group-x.tsx +499 -0
  105. package/components/catalyst-ui/x/select-x.tsx +1127 -0
  106. package/components/catalyst-ui/x/sheet-x.tsx +668 -0
  107. package/components/catalyst-ui/x/switch-x.tsx +681 -0
  108. package/components/catalyst-ui/x/table-x.tsx +574 -0
  109. package/components/catalyst-ui/x/tabs-x.tsx +839 -0
  110. package/components/catalyst-ui/x/textarea-x.tsx +1263 -0
  111. package/components/catalyst-ui/x/tooltip-x.tsx +396 -0
  112. package/components/catalyst-ui/x/tracker-x.tsx +560 -0
  113. package/data/bg-data.tsx +901 -0
  114. package/data/buttons-data.tsx +2327 -0
  115. package/data/charts-data.tsx +102 -0
  116. package/data/chat-data.tsx +83 -0
  117. package/data/code-data.tsx +1040 -0
  118. package/data/comboboxes-data.tsx +1843 -0
  119. package/data/command-data.tsx +1381 -0
  120. package/data/core-data.tsx +15953 -0
  121. package/data/crm-data.tsx +47 -0
  122. package/data/data.tsx +159 -0
  123. package/data/date-and-time-data.tsx +554 -0
  124. package/data/dependencies.tsx +7 -0
  125. package/data/ecommerce-data.tsx +1387 -0
  126. package/data/forms-data.tsx +7890 -0
  127. package/data/hooks-data.tsx +5487 -0
  128. package/data/index.ts +34 -0
  129. package/data/inputs-data.tsx +557 -0
  130. package/data/interactive-data.tsx +5394 -0
  131. package/data/lofi-data.tsx +18295 -0
  132. package/data/marketing-data.tsx +2546 -0
  133. package/data/media-data.tsx +1510 -0
  134. package/data/motion-data.tsx +5801 -0
  135. package/data/overlay-data.tsx +4136 -0
  136. package/data/pdf-data.tsx +124 -0
  137. package/data/pos-data.tsx +213 -0
  138. package/data/postcss.config.js +6 -0
  139. package/data/primitive-data.tsx +5170 -0
  140. package/data/prompt-data.tsx +1226 -0
  141. package/data/requiredLibs.ts +4 -0
  142. package/data/sandbox-data.tsx +1 -0
  143. package/data/sidebars-data.tsx +5421 -0
  144. package/data/stacks-data.tsx +32 -0
  145. package/data/table-data.tsx +706 -0
  146. package/data/tailwind.config.js +3830 -0
  147. package/data/tailwind.config.ngin.js +3830 -0
  148. package/data/tailwind.css +431 -0
  149. package/data/tools-data.tsx +6910 -0
  150. package/data/typography-data.tsx +2050 -0
  151. package/data/utils-data.tsx +6500 -0
  152. package/data/x-data.tsx +1171 -0
  153. package/dist/index.d.ts +3 -0
  154. package/dist/index.d.ts.map +1 -0
  155. package/dist/index.js +30245 -0
  156. package/dist/index.js.map +362 -0
  157. package/package.json +50 -0
@@ -0,0 +1,744 @@
1
+ /**
2
+
3
+
4
+
5
+
6
+
7
+
8
+ export function PopoverX({ popover = 'default', children, ...props }: any) {
9
+ switch (popover) {
10
+ case 'Closable':
11
+ break
12
+ default:
13
+ break;
14
+ }
15
+ }
16
+
17
+ const PopoverRatingsDemo = () => {
18
+ const totalReviews = Object.values(ratings).reduce((acc, count) => acc + count, 0)
19
+ const totalRating = Object.entries(ratings).reduce((acc, [star, count]) => acc + Number(star) * count, 0)
20
+ const averageRating = Number((totalRating / totalReviews || 0).toFixed(2))
21
+
22
+ return (
23
+ <Popover>
24
+ <PopoverTrigger asChild>
25
+ <Button variant='outline' size='icon'>
26
+ <StarIcon />
27
+ <span className='sr-only'>Ratings & reviews</span>
28
+ </Button>
29
+ </PopoverTrigger>
30
+ <PopoverContent className='w-80'>
31
+ <div className='grid gap-4'>
32
+ <div className='space-y-1'>
33
+ <div className='flex items-center gap-1'>
34
+ <span className='text-xl font-semibold'>{averageRating}</span>
35
+ <StarIcon className='size-5 fill-amber-500 stroke-amber-500 dark:fill-amber-400 dark:stroke-amber-400' />
36
+ </div>
37
+ <div className='text-sm font-medium'>Total {totalReviews} reviews</div>
38
+ <p className='text-muted-foreground text-sm'>All reviews are from genuine customers.</p>
39
+ </div>
40
+ <div className='grid'>
41
+ <div className='flex items-center justify-between'>
42
+ <Badge variant='secondary' className='rounded-sm'>
43
+ +6 this week
44
+ </Badge>
45
+ <a href='#' className='text-sm hover:underline'>
46
+ See all
47
+ </a>
48
+ </div>
49
+ <Separator className='my-2' />
50
+ <ul className='space-y-2'>
51
+ {Object.entries(ratings)
52
+ .reverse()
53
+ .map(([star, count]) => (
54
+ <li key={star} className='flex items-center gap-2'>
55
+ <span className='shrink-0 text-sm'>{star} star</span>
56
+ <Progress value={(count / totalReviews) * 100} className='w-full' />
57
+ <span className='shrink-0 text-sm'>{count.toString()}</span>
58
+ </li>
59
+ ))}
60
+ </ul>
61
+ </div>
62
+ </div>
63
+ </PopoverContent>
64
+ </Popover>
65
+ )
66
+ }
67
+
68
+ const PopoverDimensionsDemo = () => {
69
+ return (
70
+ <Popover>
71
+ <PopoverTrigger asChild>
72
+ <Button variant='outline' size='icon'>
73
+ <PencilRulerIcon />
74
+ <span className='sr-only'>Dimensions</span>
75
+ </Button>
76
+ </PopoverTrigger>
77
+ <PopoverContent className='w-80'>
78
+ <div className='grid gap-4'>
79
+ <div className='space-y-2'>
80
+ <h4 className='leading-none font-medium'>Dimensions</h4>
81
+ <p className='text-muted-foreground text-sm'>Set the dimensions for the layer.</p>
82
+ </div>
83
+ <div className='grid gap-2'>
84
+ <div className='grid grid-cols-3 items-center gap-4'>
85
+ <Label htmlFor='width'>Width</Label>
86
+ <Input id='width' defaultValue='100%' className='col-span-2 h-8' />
87
+ </div>
88
+ <div className='grid grid-cols-3 items-center gap-4'>
89
+ <Label htmlFor='maxWidth'>Max. width</Label>
90
+ <Input id='maxWidth' defaultValue='300px' className='col-span-2 h-8' />
91
+ </div>
92
+ <div className='grid grid-cols-3 items-center gap-4'>
93
+ <Label htmlFor='height'>Height</Label>
94
+ <Input id='height' defaultValue='25px' className='col-span-2 h-8' />
95
+ </div>
96
+ <div className='grid grid-cols-3 items-center gap-4'>
97
+ <Label htmlFor='maxHeight'>Max. height</Label>
98
+ <Input id='maxHeight' defaultValue='none' className='col-span-2 h-8' />
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </PopoverContent>
103
+ </Popover>
104
+ )
105
+ }
106
+
107
+ const PopoverPricingDemo = () => {
108
+ return (
109
+ <Popover>
110
+ <PopoverTrigger asChild>
111
+ <Button variant='outline' size='icon'>
112
+ <DollarSignIcon />
113
+ <span className='sr-only'>Pricing details</span>
114
+ </Button>
115
+ </PopoverTrigger>
116
+ <PopoverContent className='w-80'>
117
+ <div className='grid gap-2.5'>
118
+ <div className='flex items-center justify-between'>
119
+ <span className='text-lg font-semibold'>Enterprise Plan</span>
120
+ <span className='text-sm font-medium'>$49.99/month</span>
121
+ </div>
122
+ <p className='text-sm'>
123
+ Get unlimited access to all features including AI-powered analytics, custom branding, priority support, and
124
+ advanced team collaboration tools.
125
+ </p>
126
+ <div className='flex items-center gap-2'>
127
+ <Badge variant='destructive' className='rounded-sm px-1.5 py-px text-xs'>
128
+ Limited Offer
129
+ </Badge>
130
+ <span className='text-muted-foreground text-xs'>20% discount on annual plan</span>
131
+ </div>
132
+ </div>
133
+ </PopoverContent>
134
+ </Popover>
135
+ )
136
+ }
137
+
138
+ const PopoverVolumeDemo = () => {
139
+ const [value, setValue] = useState([45])
140
+
141
+ return (
142
+ <Popover>
143
+ <PopoverTrigger asChild>
144
+ <Button variant='outline' size='icon'>
145
+ <Volume2Icon />
146
+ <span className='sr-only'>Volume control</span>
147
+ </Button>
148
+ </PopoverTrigger>
149
+ <PopoverContent className='w-80'>
150
+ <div className='space-y-3'>
151
+ <div className='flex items-center justify-between gap-2'>
152
+ <Label className='leading-5'>Volume</Label>
153
+ <output className='text-sm font-medium tabular-nums'>{value[0]}</output>
154
+ </div>
155
+ <div className='flex items-center gap-2'>
156
+ <VolumeXIcon className='size-4 shrink-0 opacity-60' />
157
+ <Slider value={value} onValueChange={setValue} aria-label='Volume slider' />
158
+ <Volume2Icon className='size-4 shrink-0 opacity-60' />
159
+ </div>
160
+ </div>
161
+ </PopoverContent>
162
+ </Popover>
163
+ )
164
+ }
165
+
166
+ const PopoverAboutDemo = () => {
167
+ return (
168
+ <Popover>
169
+ <PopoverTrigger asChild>
170
+ <Button variant='outline' size='icon'>
171
+ <InfoIcon />
172
+ <span className='sr-only'>About Shadcn Studio</span>
173
+ </Button>
174
+ </PopoverTrigger>
175
+ <PopoverContent className='w-80'>
176
+ <div className='grid gap-4'>
177
+ <div className='space-y-1.5 text-center'>
178
+ <div className='text-lg font-semibold'>About Shadcn Studio</div>
179
+ <p className='text-muted-foreground text-sm'>
180
+ Welcome to Shadcn Studio — your toolkit for building sleek, customizable UI components with ease!
181
+ </p>
182
+ </div>
183
+ <Button size='sm' asChild>
184
+ <a
185
+ href='https://shadcnstudio.com/docs/getting-started/introduction'
186
+ target='_blank'
187
+ rel='noopener noreferrer'
188
+ >
189
+ Learn More
190
+ </a>
191
+ </Button>
192
+ </div>
193
+ </PopoverContent>
194
+ </Popover>
195
+ )
196
+ }
197
+
198
+ const PopoverDownloadDemo = () => {
199
+ const [isPaused, setIsPaused] = useState(false)
200
+ const [isCanceled, setIsCanceled] = useState(false)
201
+ const [value, setValue] = useState(0)
202
+ const [open, setOpen] = useState(false)
203
+ const [hasStarted, setHasStarted] = useState(false)
204
+
205
+ useEffect(() => {
206
+ if (open && !hasStarted && !isCanceled) {
207
+ setHasStarted(true)
208
+ }
209
+ }, [open, hasStarted, isCanceled])
210
+
211
+ useEffect(() => {
212
+ if (!hasStarted || isPaused || isCanceled) return
213
+
214
+ const timer = setInterval(() => {
215
+ setValue(prev => {
216
+ if (prev < 100) {
217
+ return Math.min(100, prev + Math.floor(Math.random() * 10) + 1)
218
+ } else {
219
+ clearInterval(timer)
220
+
221
+ return prev
222
+ }
223
+ })
224
+ }, 500)
225
+
226
+ return () => {
227
+ clearInterval(timer)
228
+ }
229
+ }, [open, isPaused, isCanceled, hasStarted])
230
+
231
+ const getText = () => {
232
+ if (isCanceled) return 'Download Canceled'
233
+ if (isPaused) return 'Download Paused'
234
+ if (value === 100) return 'Download Complete'
235
+
236
+ return 'Downloading File'
237
+ }
238
+
239
+ return (
240
+ <Popover onOpenChange={setOpen} open={open}>
241
+ <PopoverTrigger asChild>
242
+ <Button variant='outline' size='icon'>
243
+ <DownloadIcon />
244
+ <span className='sr-only'>Download File</span>
245
+ </Button>
246
+ </PopoverTrigger>
247
+ <PopoverContent className='w-80'>
248
+ <div className='grid gap-4'>
249
+ <div className='flex items-center gap-2'>
250
+ <div className='relative flex size-6 items-center justify-center'>
251
+ <span
252
+ className={cn('border-primary absolute inset-0 rounded-full border border-dashed', {
253
+ 'animate-spin [animation-duration:3s]': value < 100 && !isPaused && !isCanceled
254
+ })}
255
+ />
256
+ <DownloadIcon className='z-1 size-3' />
257
+ </div>
258
+ <span className='flex-1 text-sm font-medium'>{getText()}</span>
259
+ {!isCanceled && <span className='text-sm font-semibold'>{`${value}%`}</span>}
260
+ </div>
261
+ <Progress value={value} className='w-full' />
262
+ <div className='grid grid-cols-2 gap-2'>
263
+ <Button size='sm' onClick={() => setIsPaused(!isPaused)} disabled={value === 100 || isCanceled}>
264
+ {isPaused ? 'Resume' : 'Pause'}
265
+ </Button>
266
+ <Button
267
+ variant='secondary'
268
+ size='sm'
269
+ onClick={() => {
270
+ if (value < 100) {
271
+ setValue(0)
272
+ setIsCanceled(true)
273
+ setHasStarted(false)
274
+ }
275
+
276
+ setOpen(false)
277
+ }}
278
+ >
279
+ Cancel
280
+ </Button>
281
+ </div>
282
+ </div>
283
+ </PopoverContent>
284
+ </Popover>
285
+ )
286
+ }
287
+
288
+ const PopoverDeleteFileDemo = () => {
289
+ return (
290
+ <Popover>
291
+ <PopoverTrigger asChild>
292
+ <Button variant='outline' size='icon'>
293
+ <FileWarningIcon />
294
+ <span className='sr-only'>Delete File</span>
295
+ </Button>
296
+ </PopoverTrigger>
297
+ <PopoverContent className='w-80'>
298
+ <div className='flex flex-col items-center gap-4'>
299
+ <div className='flex aspect-square size-12 items-center justify-center rounded-full bg-red-500/10'>
300
+ <FileWarningIcon className='text-destructive size-6' />
301
+ </div>
302
+ <div className='space-y-2 text-center'>
303
+ <div className='font-semibold text-balance'>Are you sure you want to delete this file?</div>
304
+ <p className='text-muted-foreground text-sm'>
305
+ Deleting this file can affect your project and other files connection so keep in mind before making
306
+ decision
307
+ </p>
308
+ </div>
309
+ <div className='grid w-full grid-cols-2 gap-2'>
310
+ <Button variant='secondary' size='sm'>
311
+ Cancel
312
+ </Button>
313
+ <Button variant='destructive' size='sm'>
314
+ Delete File
315
+ </Button>
316
+ </div>
317
+ </div>
318
+ </PopoverContent>
319
+ </Popover>
320
+ )
321
+ }
322
+
323
+ const PopoverFeedbackDemo = () => {
324
+ return (
325
+ <Popover>
326
+ <PopoverTrigger asChild>
327
+ <Button variant='outline' size='icon'>
328
+ <MessageCircleIcon />
329
+ <span className='sr-only'>Feedback</span>
330
+ </Button>
331
+ </PopoverTrigger>
332
+ <PopoverContent className='w-80'>
333
+ <div className='grid gap-2'>
334
+ <div className='font-medium'>Feedback</div>
335
+ <Textarea placeholder='Type your message here.' className='max-h-56' />
336
+ <div className='grid w-full grid-cols-2 gap-2'>
337
+ <Button size='sm'>Send</Button>
338
+ <Button variant='secondary' size='sm'>
339
+ Cancel
340
+ </Button>
341
+ </div>
342
+ </div>
343
+ </PopoverContent>
344
+ </Popover>
345
+ )
346
+ }
347
+
348
+ const PopoverFilterDemo = () => {
349
+ const [selected, setSelected] = useState(['Most liked', 'Newest'])
350
+ const [price, setPrice] = useState([450])
351
+
352
+ return (
353
+ <Popover>
354
+ <PopoverTrigger asChild>
355
+ <Button variant='outline' size='icon'>
356
+ <FunnelPlusIcon />
357
+ <span className='sr-only'>Filter</span>
358
+ </Button>
359
+ </PopoverTrigger>
360
+ <PopoverContent>
361
+ <div className='grid gap-4'>
362
+ <div className='flex items-center justify-between gap-2'>
363
+ <span className='font-medium'>Filter</span>
364
+ <Button
365
+ variant='secondary'
366
+ className='h-7 rounded-full px-2 py-1 text-xs'
367
+ onClick={() => {
368
+ setSelected(['Most liked', 'Newest'])
369
+ setPrice([450])
370
+ }}
371
+ >
372
+ Reset all
373
+ </Button>
374
+ </div>
375
+ <div className='flex flex-col gap-2'>
376
+ {filters.map((label, index) => (
377
+ <div key={index} className='flex items-center gap-2'>
378
+ <Checkbox
379
+ id={`filter-${index + 1}`}
380
+ checked={selected.includes(label)}
381
+ onCheckedChange={checked =>
382
+ setSelected(checked ? [...selected, label] : selected.filter(item => item !== label))
383
+ }
384
+ />
385
+ <Label htmlFor={`filter-${index + 1}`}>{label}</Label>
386
+ </div>
387
+ ))}
388
+ </div>
389
+ <div className='grid gap-3'>
390
+ <Label>Price range</Label>
391
+ <div className='space-y-2'>
392
+ <Slider value={price} onValueChange={setPrice} step={50} max={1000} aria-label='Price range' />
393
+ <span className='text-muted-foreground flex w-full items-center justify-between gap-1 text-xs font-medium'>
394
+ <span>0</span>
395
+ <span>500</span>
396
+ <span>1000</span>
397
+ </span>
398
+ </div>
399
+ </div>
400
+ </div>
401
+ </PopoverContent>
402
+ </Popover>
403
+ )
404
+ }
405
+
406
+ const useDebounce = (value: string, delay: number = 300) => {
407
+ const [debouncedValue, setDebouncedValue] = useState(value)
408
+
409
+ useEffect(() => {
410
+ const timer = setTimeout(() => {
411
+ setDebouncedValue(value)
412
+ }, delay)
413
+
414
+ return () => {
415
+ clearTimeout(timer)
416
+ }
417
+ }, [value, delay])
418
+
419
+ return debouncedValue
420
+ }
421
+
422
+ const PopoverSearchDemo = () => {
423
+ const [inputValue, setInputValue] = useState('')
424
+ const [isLoading, setIsLoading] = useState(false)
425
+ const debouncedSearch = useDebounce(inputValue)
426
+ const [filteredUsers, setFilteredUsers] = useState(users)
427
+
428
+ // Handle loading state when input changes
429
+ useEffect(() => {
430
+ if (inputValue) {
431
+ setIsLoading(true)
432
+ } else {
433
+ setIsLoading(false)
434
+ }
435
+ }, [inputValue])
436
+
437
+ // Apply filtering after debounce and update loading state
438
+ useEffect(() => {
439
+ if (debouncedSearch.trim() === '') {
440
+ setFilteredUsers(users)
441
+ setIsLoading(false)
442
+ } else {
443
+ const searchTerm = debouncedSearch.toLowerCase()
444
+
445
+ const filtered = users.filter(user => user.name.toLowerCase().includes(searchTerm))
446
+
447
+ setFilteredUsers(filtered)
448
+ setIsLoading(false)
449
+ }
450
+ }, [debouncedSearch])
451
+
452
+ return (
453
+ <Popover>
454
+ <PopoverTrigger asChild>
455
+ <Button variant='outline' size='icon'>
456
+ <SearchIcon />
457
+ <span className='sr-only'>Search users</span>
458
+ </Button>
459
+ </PopoverTrigger>
460
+ <PopoverContent className='w-80'>
461
+ <div className='grid gap-4'>
462
+ <div className='relative'>
463
+ <div className='text-muted-foreground pointer-events-none absolute inset-y-0 left-0 flex items-center justify-center pl-3 peer-disabled:opacity-50'>
464
+ <SearchIcon className='size-4' />
465
+ <span className='sr-only'>Search</span>
466
+ </div>
467
+ <Input
468
+ type='search'
469
+ placeholder='Search users'
470
+ value={inputValue}
471
+ onChange={e => setInputValue(e.target.value)}
472
+ className='peer px-9 [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none'
473
+ />
474
+ {isLoading && (
475
+ <div className='text-muted-foreground pointer-events-none absolute inset-y-0 right-0 flex items-center justify-center pr-3 peer-disabled:opacity-50'>
476
+ <LoaderCircleIcon className='size-4 animate-spin' />
477
+ <span className='sr-only'>Loading...</span>
478
+ </div>
479
+ )}
480
+ </div>
481
+ <ul className='space-y-2'>
482
+ {filteredUsers.length > 0 ? (
483
+ filteredUsers.map((user, index) => (
484
+ <li key={index} className='flex items-center gap-2'>
485
+ <Avatar className='size-6'>
486
+ <AvatarImage src={user.image} alt={user.name} />
487
+ <AvatarFallback className='text-xs'>{user.fallback}</AvatarFallback>
488
+ </Avatar>
489
+ <div className='flex-1 text-sm font-medium'>{user.name}</div>
490
+ {user.notifications && (
491
+ <span className='text-muted-foreground text-xs'>{`${user.notifications} Notification${user.notifications > 1 ? 's' : ''}`}</span>
492
+ )}
493
+ </li>
494
+ ))
495
+ ) : (
496
+ <li className='py-2 text-center text-sm'>No users found</li>
497
+ )}
498
+ </ul>
499
+ </div>
500
+ </PopoverContent>
501
+ </Popover>
502
+ )
503
+ }
504
+
505
+ const PopoverNotificationsDemo = () => {
506
+ const [readMessages, setReadMessages] = useState([3])
507
+
508
+ return (
509
+ <Popover>
510
+ <PopoverTrigger asChild>
511
+ <Button variant='outline' size='icon'>
512
+ <BellIcon />
513
+ <span className='sr-only'>Notifications</span>
514
+ </Button>
515
+ </PopoverTrigger>
516
+ <PopoverContent className='w-80 p-0'>
517
+ <div className='grid'>
518
+ <div className='flex items-center justify-between gap-2 px-4 py-2.5'>
519
+ <span className='font-medium'>Notifications</span>
520
+ <Button
521
+ variant='secondary'
522
+ className='h-7 rounded-full px-2 py-1 text-xs'
523
+ onClick={() => setReadMessages(notifications.map(item => item.id))}
524
+ >
525
+ Mark as all read
526
+ </Button>
527
+ </div>
528
+ <Separator className='' />
529
+ <ul className='grid gap-4 p-2'>
530
+ {notifications.map(item => (
531
+ <li
532
+ key={item.id}
533
+ className='hover:bg-accent flex items-start gap-2 rounded-lg px-2 py-1.5'
534
+ onClick={() => setReadMessages([...readMessages, item.id])}
535
+ >
536
+ <Avatar className='rounded-lg'>
537
+ <AvatarImage src={item.image} alt={item.fallback} />
538
+ <AvatarFallback className='rounded-lg text-xs'>{item.fallback}</AvatarFallback>
539
+ </Avatar>
540
+ <div className='flex-1 space-y-1'>
541
+ <div className='text-sm font-medium'>{item.message}</div>
542
+ <p className='text-muted-foreground text-xs'>{`${item.time} ago`}</p>
543
+ </div>
544
+ {!readMessages.includes(item.id) && (
545
+ <CircleIcon className='fill-primary text-primary size-2 self-center' />
546
+ )}
547
+ </li>
548
+ ))}
549
+ </ul>
550
+ </div>
551
+ </PopoverContent>
552
+ </Popover>
553
+ )
554
+ }
555
+
556
+ const PopoverAboutHimalayasDemo = () => {
557
+ return (
558
+ <Popover>
559
+ <PopoverTrigger asChild>
560
+ <Button variant='outline' size='icon'>
561
+ <MapPinIcon />
562
+ <span className='sr-only'>About Himalayas</span>
563
+ </Button>
564
+ </PopoverTrigger>
565
+ <PopoverContent className='w-85 p-0'>
566
+ <div className='flex'>
567
+ <div className='space-y-2 p-4'>
568
+ <p className='font-medium'>About Himalayas</p>
569
+ <p className='text-muted-foreground text-xs'>
570
+ The Great Himalayan mountain ranges in the Indian sub-continent region.{' '}
571
+ </p>
572
+ <a
573
+ href='https://en.wikipedia.org/wiki/Himalayas'
574
+ target='_blank'
575
+ rel='noopener noreferrer'
576
+ className='flex w-fit text-xs hover:underline'
577
+ >
578
+ Read more
579
+ <ChevronRightIcon className='size-4' />
580
+ </a>
581
+ </div>
582
+ <img
583
+ src='https://lp-cms-production.imgix.net/2021-01/GettyRF_450207051.jpg?height=136'
584
+ alt='the himalayas'
585
+ className='h-34 w-2/5 rounded-r-md object-cover'
586
+ />
587
+ </div>
588
+ </PopoverContent>
589
+ </Popover>
590
+ )
591
+ }
592
+
593
+ const PopoverSlideInLeftDemo = () => {
594
+ const [copied, setCopied] = useState<boolean>(false)
595
+
596
+ const handleCopy = async () => {
597
+ try {
598
+ await navigator.clipboard.writeText('SUMMER25OFF')
599
+ setCopied(true)
600
+ setTimeout(() => setCopied(false), 1500)
601
+ } catch (err) {
602
+ console.error('Failed to copy text: ', err)
603
+ }
604
+ }
605
+
606
+ return (
607
+ <Popover>
608
+ <PopoverTrigger asChild>
609
+ <Button variant='outline'>Slide-in from left</Button>
610
+ </PopoverTrigger>
611
+ <PopoverContent className='data-[state=open]:slide-in-from-left-20 data-[state=closed]:slide-out-to-left-20 data-[state=open]:slide-in-from-top-0 data-[state=closed]:slide-out-to-top-0 data-[state=closed]:zoom-out-100 data-[state=open]:zoom-in-100 w-80 duration-400'>
612
+ <div className='flex flex-col items-center gap-4'>
613
+ <div className='space-y-1 text-center'>
614
+ <div className='text-lg font-semibold'>Summer Sale Discount</div>
615
+ <p className='text-sm'>Scan this code at checkout for 25% off</p>
616
+ </div>
617
+ <div className='aspect-square rounded-xl border p-2'>
618
+ <img
619
+ src='https://cdn.shadcnstudio.com/ss-assets/components/popover/qr-code.png?height=152'
620
+ alt='Discount QR Code'
621
+ className='size-38 rounded-md'
622
+ />
623
+ </div>
624
+ <div className='flex w-full items-center gap-1.5'>
625
+ <Separator className='flex-1' />
626
+ <span className='text-muted-foreground text-xs'>or use coupon code</span>
627
+ <Separator className='flex-1' />
628
+ </div>
629
+ <div className='flex w-full gap-2'>
630
+ <Input
631
+ type='text'
632
+ placeholder='Discount code'
633
+ defaultValue='SUMMER25OFF'
634
+ className='disabled:bg-muted'
635
+ disabled
636
+ />
637
+ <Button variant='outline' size='icon' className='relative' onClick={handleCopy}>
638
+ <span className={cn('transition-all', copied ? 'scale-100 opacity-100' : 'scale-0 opacity-0')}>
639
+ <CheckIcon className='stroke-green-600 dark:stroke-green-400' />
640
+ </span>
641
+ <span
642
+ className={cn(
643
+ 'absolute left-2.25 transition-all',
644
+ copied ? 'scale-0 opacity-0' : 'scale-100 opacity-100'
645
+ )}
646
+ >
647
+ <CopyIcon />
648
+ </span>
649
+ </Button>
650
+ </div>
651
+ </div>
652
+ </PopoverContent>
653
+ </Popover>
654
+ )
655
+ }
656
+
657
+ const PopoverSlideInBottomDemo = () => {
658
+ const id = useId()
659
+
660
+ return (
661
+ <Popover>
662
+ <PopoverTrigger asChild>
663
+ <Button variant='outline'>Slide-in from bottom</Button>
664
+ </PopoverTrigger>
665
+ <PopoverContent className='data-[state=open]:slide-in-from-bottom-20 data-[state=closed]:slide-out-to-bottom-20 data-[state=closed]:zoom-out-100 data-[state=open]:zoom-in-100 w-80 duration-400'>
666
+ <div className='grid gap-4'>
667
+ <div className='space-y-1'>
668
+ <div className='font-medium'>Share to team members</div>
669
+ <p className='text-muted-foreground text-sm'>
670
+ Give your team members access to this project and start collaborating in real time
671
+ </p>
672
+ </div>
673
+ <div className='w-full space-y-1.5'>
674
+ <Label htmlFor={id} className='text-muted-foreground text-xs font-normal'>
675
+ Email address
676
+ </Label>
677
+ <div className='flex gap-2'>
678
+ <Input id={id} type='email' placeholder='example@xyz.com' className='h-8' />
679
+ <Button type='submit' size='sm'>
680
+ Share invite
681
+ </Button>
682
+ </div>
683
+ </div>
684
+ <div className='space-y-1.5'>
685
+ <Label className='text-muted-foreground text-xs font-normal'>Team members</Label>
686
+ <ul className='grid gap-2'>
687
+ {members.map((member, index) => (
688
+ <li key={index} className='flex items-center gap-3'>
689
+ <Checkbox id={`member-${index + 1}`} />
690
+ <Label htmlFor={`member-${index + 1}`} className='flex flex-1 items-center gap-2'>
691
+ <div className='flex flex-1 items-center gap-2'>
692
+ <Avatar className='size-6'>
693
+ <AvatarImage src={member.image} alt={member.name} />
694
+ <AvatarFallback className='text-xs'>{member.fallback}</AvatarFallback>
695
+ </Avatar>
696
+ <span className='text-sm font-medium'>{member.name}</span>
697
+ </div>
698
+ <span className='text-muted-foreground text-xs'>{member.designation}</span>
699
+ </Label>
700
+ </li>
701
+ ))}
702
+ </ul>
703
+ </div>
704
+ </div>
705
+ </PopoverContent>
706
+ </Popover>
707
+ )
708
+ }
709
+
710
+ const PopoverZoomInDemo = () => {
711
+ return (
712
+ <Popover>
713
+ <PopoverTrigger asChild>
714
+ <Button variant='outline'>Zoom in</Button>
715
+ </PopoverTrigger>
716
+ <PopoverContent className='data-[state=open]:!zoom-in-0 data-[state=closed]:!zoom-out-0 origin-center duration-400'>
717
+ <div className='grid gap-4'>
718
+ <div className='flex flex-col items-center gap-2'>
719
+ <Avatar className='size-20'>
720
+ <AvatarImage src='https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-5.png' alt='Howard Lloyd' />
721
+ <AvatarFallback className='text-xs'>HL</AvatarFallback>
722
+ </Avatar>
723
+ <div className='flex flex-col items-center text-center'>
724
+ <p className='text-lg font-semibold'>Howard Lloyd</p>
725
+ <span className='text-sm'>@iamhoward</span>
726
+ </div>
727
+ </div>
728
+ <div className='from-border/20 via-border to-border/20 mx-auto h-px w-45 bg-gradient-to-r' />
729
+ <p className='text-center text-sm italic'>
730
+ Product Manager @oliviasparks, passionate about building user-centric solutions that solve real problems.
731
+ </p>
732
+ <div className='flex justify-center gap-2 text-sm'>
733
+ <div className='font-medium'>
734
+ 512 <span className='text-muted-foreground font-normal'>followers</span>
735
+ </div>
736
+ <div className='font-medium'>
737
+ 128 <span className='text-muted-foreground font-normal'>following</span>
738
+ </div>
739
+ </div>
740
+ </div>
741
+ </PopoverContent>
742
+ </Popover>
743
+ )
744
+ } */