@djangocfg/ui-core 2.1.412 → 2.1.415

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 (51) hide show
  1. package/package.json +4 -4
  2. package/src/components/data/avatar-group/index.tsx +224 -0
  3. package/src/components/data/badge-overflow/index.tsx +259 -0
  4. package/src/components/data/circular-progress/index.tsx +358 -0
  5. package/src/components/data/relative-time-card/index.tsx +191 -0
  6. package/src/components/data/stat/index.tsx +140 -0
  7. package/src/components/data/status/index.tsx +80 -0
  8. package/src/components/effects/GlowBackground.tsx +9 -1
  9. package/src/components/effects/swap/index.tsx +289 -0
  10. package/src/components/feedback/banner/index.tsx +693 -0
  11. package/src/components/forms/checkbox-group/index.tsx +243 -0
  12. package/src/components/forms/editable/index.tsx +420 -0
  13. package/src/components/forms/input-otp/index.tsx +12 -3
  14. package/src/components/forms/mask-input/index.tsx +466 -0
  15. package/src/components/forms/otp/index.tsx +12 -8
  16. package/src/components/forms/segmented-input/index.tsx +319 -0
  17. package/src/components/forms/tags-input/index.tsx +896 -0
  18. package/src/components/forms/time-picker/index.tsx +285 -0
  19. package/src/components/index.ts +51 -0
  20. package/src/components/layout/key-value/index.tsx +884 -0
  21. package/src/components/layout/stack/index.tsx +349 -0
  22. package/src/components/navigation/context-menu/index.tsx +9 -6
  23. package/src/components/navigation/stepper/index.tsx +1307 -0
  24. package/src/components/select/multi-select-pro-async.tsx +11 -2
  25. package/src/components/select/multi-select-pro.tsx +11 -2
  26. package/src/components/specialized/presence/index.tsx +181 -0
  27. package/src/components/specialized/primitive/index.tsx +83 -0
  28. package/src/components/specialized/visually-hidden/index.tsx +19 -0
  29. package/src/components/specialized/visually-hidden-input/index.tsx +99 -0
  30. package/src/hooks/dom/index.ts +4 -0
  31. package/src/hooks/dom/useFormReset.ts +49 -0
  32. package/src/hooks/dom/useLayoutEffect.ts +16 -0
  33. package/src/hooks/dom/useSize.ts +57 -0
  34. package/src/hooks/state/index.ts +4 -0
  35. package/src/hooks/state/useCallbackRef.ts +25 -0
  36. package/src/hooks/state/usePrevious.ts +20 -0
  37. package/src/hooks/state/useStateMachine.ts +29 -0
  38. package/src/lib/compose-event-handlers.ts +22 -0
  39. package/src/lib/compose-refs.ts +65 -0
  40. package/src/lib/create-context.tsx +62 -0
  41. package/src/lib/get-element-ref.ts +33 -0
  42. package/src/lib/index.ts +5 -0
  43. package/src/lib/styles.ts +103 -0
  44. package/src/styles/README.md +43 -0
  45. package/src/styles/palette/utils.ts +15 -5
  46. package/src/styles/utilities/animations.css +135 -0
  47. package/src/styles/utilities/display.css +62 -0
  48. package/src/styles/utilities/glass.css +57 -0
  49. package/src/styles/utilities/marquee.css +69 -0
  50. package/src/styles/utilities/step.css +25 -0
  51. package/src/styles/utilities.css +6 -259
@@ -0,0 +1,693 @@
1
+ "use client"
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import { X } from "lucide-react"
5
+ import * as React from "react"
6
+ import { Slot } from "@radix-ui/react-slot"
7
+ import * as ReactDOM from "react-dom"
8
+
9
+ import { cn } from "../../../lib/utils"
10
+ import { Button } from "../../forms/button"
11
+
12
+ const BANNER_ANIMATION_DURATION = 400
13
+ const DEFAULT_BANNER_PRIORITY = 0
14
+ const DEFAULT_BANNER_DISMISSIBLE = true
15
+
16
+ type BannerVariant = "default" | "info" | "success" | "warning" | "destructive"
17
+ type BannerSide = "top" | "bottom"
18
+
19
+ interface DivProps extends React.ComponentProps<"div"> {
20
+ asChild?: boolean
21
+ }
22
+
23
+ type CloseElement = HTMLButtonElement
24
+
25
+ interface BannerRenderProps {
26
+ id: string
27
+ variant?: BannerVariant
28
+ dismissible: boolean
29
+ onClose: () => void
30
+ onRemove: () => void
31
+ }
32
+
33
+ type BannerContent =
34
+ | React.ReactNode
35
+ | ((props: BannerRenderProps) => React.ReactNode)
36
+
37
+ interface BannerData {
38
+ id: string
39
+ content: BannerContent
40
+ variant?: BannerVariant
41
+ priority?: number
42
+ dismissible?: boolean
43
+ duration?: number
44
+ onDismiss?: () => void
45
+ }
46
+
47
+ interface StoreState {
48
+ banners: BannerData[]
49
+ removing: Set<string>
50
+ heights: Map<string, number>
51
+ }
52
+
53
+ interface Store {
54
+ subscribe: (callback: () => void) => () => void
55
+ getState: () => StoreState
56
+ notify: () => void
57
+ onBannerAdd: (banner: Omit<BannerData, "id">) => string
58
+ onBannerRemove: (id: string) => void
59
+ onBannersClear: () => void
60
+ onRemovingChange: (id: string, value: boolean) => void
61
+ onHeightChange: (id: string, height: number) => void
62
+ onHeightRemove: (id: string) => void
63
+ }
64
+
65
+ const StoreContext = React.createContext<Store | null>(null)
66
+
67
+ function useStoreContext(consumerName: string) {
68
+ const context = React.useContext(StoreContext)
69
+ if (!context) {
70
+ throw new Error(`\`${consumerName}\` must be used within \`Banners\``)
71
+ }
72
+ return context
73
+ }
74
+
75
+ function useStore<T>(store: Store, selector: (state: StoreState) => T): T {
76
+ return React.useSyncExternalStore(
77
+ store.subscribe,
78
+ () => selector(store.getState()),
79
+ () => selector(store.getState()),
80
+ )
81
+ }
82
+
83
+ interface BannerContextValue {
84
+ id?: string
85
+ variant?: BannerVariant | null
86
+ dismissible?: boolean
87
+ onClose?: () => void
88
+ }
89
+
90
+ const BannerContext = React.createContext<BannerContextValue | null>(null)
91
+
92
+ function useBannerContext(consumerName: string) {
93
+ const context = React.useContext(BannerContext)
94
+ if (!context) {
95
+ throw new Error(`\`${consumerName}\` must be used within \`Banner\``)
96
+ }
97
+ return context
98
+ }
99
+
100
+ function useBanner() {
101
+ const { id, variant, dismissible, onClose } = useBannerContext("useBanner")
102
+ const storeContext = React.useContext(StoreContext)
103
+
104
+ return React.useMemo(() => {
105
+ const onRemove =
106
+ id && storeContext ? () => storeContext.onBannerRemove(id) : undefined
107
+
108
+ return {
109
+ id,
110
+ variant,
111
+ dismissible,
112
+ onClose,
113
+ onRemove,
114
+ }
115
+ }, [id, variant, dismissible, onClose, storeContext])
116
+ }
117
+
118
+ interface BannersProps {
119
+ children?: React.ReactNode
120
+ maxVisible?: number
121
+ side?: BannerSide
122
+ container?: Element | DocumentFragment | null
123
+ }
124
+
125
+ function Banners(props: BannersProps) {
126
+ const {
127
+ children,
128
+ maxVisible = 1,
129
+ side = "top",
130
+ container: containerProp,
131
+ } = props
132
+
133
+ const stateRef = React.useRef<StoreState>({
134
+ banners: [],
135
+ removing: new Set(),
136
+ heights: new Map(),
137
+ })
138
+ const listenersRef = React.useRef<Set<() => void>>(new Set())
139
+ const timeoutsRef = React.useRef<Map<string, ReturnType<typeof setTimeout>>>(
140
+ new Map(),
141
+ )
142
+
143
+ const store: Store = React.useMemo(
144
+ () => ({
145
+ subscribe: (cb) => {
146
+ listenersRef.current.add(cb)
147
+ return () => listenersRef.current.delete(cb)
148
+ },
149
+ getState: () => stateRef.current,
150
+ notify: () => {
151
+ for (const listener of listenersRef.current) {
152
+ listener()
153
+ }
154
+ },
155
+ onBannerAdd: (banner) => {
156
+ const id = crypto.randomUUID()
157
+ const newBanner: BannerData = { ...banner, id }
158
+ const priority = banner.priority ?? DEFAULT_BANNER_PRIORITY
159
+
160
+ const banners = [...stateRef.current.banners]
161
+ const insertIndex = banners.findIndex(
162
+ (b) => (b.priority ?? DEFAULT_BANNER_PRIORITY) < priority,
163
+ )
164
+
165
+ if (insertIndex === -1) {
166
+ banners.push(newBanner)
167
+ } else {
168
+ banners.splice(insertIndex, 0, newBanner)
169
+ }
170
+
171
+ stateRef.current.banners = banners
172
+ store.notify()
173
+
174
+ if (banner.duration && banner.duration > 0) {
175
+ const timeoutId = setTimeout(() => {
176
+ store.onRemovingChange(id, true)
177
+ timeoutsRef.current.delete(id)
178
+ }, banner.duration)
179
+ timeoutsRef.current.set(id, timeoutId)
180
+ }
181
+
182
+ return id
183
+ },
184
+ onBannerRemove: (id) => {
185
+ const banner = stateRef.current.banners.find((b) => b.id === id)
186
+ if (!banner) return
187
+
188
+ const timeoutId = timeoutsRef.current.get(id)
189
+ if (timeoutId) {
190
+ clearTimeout(timeoutId)
191
+ timeoutsRef.current.delete(id)
192
+ }
193
+
194
+ const newRemoving = new Set(stateRef.current.removing)
195
+ newRemoving.delete(id)
196
+ stateRef.current.removing = newRemoving
197
+
198
+ banner.onDismiss?.()
199
+ stateRef.current.banners = stateRef.current.banners.filter(
200
+ (b) => b.id !== id,
201
+ )
202
+ store.notify()
203
+ },
204
+ onBannersClear: () => {
205
+ for (const timeoutId of timeoutsRef.current.values()) {
206
+ clearTimeout(timeoutId)
207
+ }
208
+ timeoutsRef.current.clear()
209
+ stateRef.current.removing = new Set()
210
+ stateRef.current.heights = new Map()
211
+ stateRef.current.banners = []
212
+ store.notify()
213
+ },
214
+ onRemovingChange: (id, value) => {
215
+ const newSet = new Set(stateRef.current.removing)
216
+ if (value) {
217
+ newSet.add(id)
218
+ } else {
219
+ newSet.delete(id)
220
+ }
221
+ stateRef.current.removing = newSet
222
+ store.notify()
223
+ },
224
+ onHeightChange: (id, height) => {
225
+ if (stateRef.current.heights.get(id) === height) return
226
+ const newHeights = new Map(stateRef.current.heights)
227
+ newHeights.set(id, height)
228
+ stateRef.current.heights = newHeights
229
+ store.notify()
230
+ },
231
+ onHeightRemove: (id) => {
232
+ if (!stateRef.current.heights.has(id)) return
233
+ const newHeights = new Map(stateRef.current.heights)
234
+ newHeights.delete(id)
235
+ stateRef.current.heights = newHeights
236
+ store.notify()
237
+ },
238
+ }),
239
+ [],
240
+ )
241
+
242
+ const banners = useStore(store, (state) => state.banners)
243
+ const heights = useStore(store, (state) => state.heights)
244
+ const visibleBanners = banners.slice(0, maxVisible)
245
+ const container = containerProp ?? globalThis.document?.body ?? null
246
+
247
+ const totalHeight = React.useMemo(() => {
248
+ let total = 0
249
+ for (const banner of visibleBanners) {
250
+ total += heights.get(banner.id) ?? 0
251
+ }
252
+ return total
253
+ }, [visibleBanners, heights])
254
+
255
+ return (
256
+ <StoreContext.Provider value={store}>
257
+ {children}
258
+ {container &&
259
+ visibleBanners.length > 0 &&
260
+ ReactDOM.createPortal(
261
+ <div
262
+ data-slot="banner-container"
263
+ data-side={side}
264
+ className={cn(
265
+ "pointer-events-none fixed right-0 left-0 isolate z-50",
266
+ side === "top" ? "top-0" : "bottom-0",
267
+ )}
268
+ style={{
269
+ height: totalHeight > 0 ? totalHeight : "auto",
270
+ transition: `height ${BANNER_ANIMATION_DURATION}ms cubic-bezier(0.32, 0.72, 0, 1)`,
271
+ }}
272
+ >
273
+ {visibleBanners.map((banner, index) => (
274
+ <BannerImpl
275
+ key={banner.id}
276
+ banner={banner}
277
+ side={side}
278
+ index={index}
279
+ />
280
+ ))}
281
+ </div>,
282
+ container,
283
+ )}
284
+ </StoreContext.Provider>
285
+ )
286
+ }
287
+
288
+ function useBanners() {
289
+ const store = useStoreContext("useBanners")
290
+ const banners = useStore(store, (state) => state.banners)
291
+
292
+ return React.useMemo(
293
+ () => ({
294
+ onBannerAdd: store.onBannerAdd,
295
+ onBannerRemove: store.onBannerRemove,
296
+ onBannersClear: store.onBannersClear,
297
+ banners,
298
+ }),
299
+ [store, banners],
300
+ )
301
+ }
302
+
303
+ const bannerVariants = cva(
304
+ "pointer-events-auto relative flex w-full items-center gap-3 border-b px-4 py-3 text-sm motion-reduce:transition-none",
305
+ {
306
+ variants: {
307
+ variant: {
308
+ default: "bg-card text-card-foreground",
309
+ info: "bg-blue-50 text-blue-900 dark:bg-blue-950 dark:text-blue-50",
310
+ success:
311
+ "bg-green-50 text-green-900 dark:bg-green-950 dark:text-green-50",
312
+ warning:
313
+ "bg-yellow-50 text-yellow-900 dark:bg-yellow-950 dark:text-yellow-50",
314
+ destructive: "bg-red-50 text-red-900 dark:bg-red-950 dark:text-red-50",
315
+ },
316
+ },
317
+ defaultVariants: {
318
+ variant: "default",
319
+ },
320
+ },
321
+ )
322
+
323
+ interface BannerImplProps {
324
+ banner: BannerData
325
+ side: BannerSide
326
+ index: number
327
+ }
328
+
329
+ function BannerImpl(props: BannerImplProps) {
330
+ const { banner, side, index } = props
331
+
332
+ const store = useStoreContext("BannerImpl")
333
+ const removing = useStore(store, (state) => state.removing.has(banner.id))
334
+ const banners = useStore(store, (state) => state.banners)
335
+ const heights = useStore(store, (state) => state.heights)
336
+
337
+ const [mounted, setMounted] = React.useState(false)
338
+ const bannerRef = React.useRef<HTMLDivElement>(null)
339
+ const offsetBeforeRemoveRef = React.useRef(0)
340
+
341
+ const offset = React.useMemo(() => {
342
+ let total = 0
343
+ for (const b of banners) {
344
+ if (b.id === banner.id) break
345
+ total += heights.get(b.id) ?? 0
346
+ }
347
+ return total
348
+ }, [banners, heights, banner.id])
349
+
350
+ if (!removing) {
351
+ offsetBeforeRemoveRef.current = offset
352
+ }
353
+
354
+ React.useEffect(() => {
355
+ const frame = requestAnimationFrame(() => setMounted(true))
356
+ return () => cancelAnimationFrame(frame)
357
+ }, [])
358
+
359
+ React.useLayoutEffect(() => {
360
+ if (!bannerRef.current || removing) return
361
+ const height = bannerRef.current.getBoundingClientRect().height
362
+ store.onHeightChange(banner.id, height)
363
+ }, [store, banner.id, removing])
364
+
365
+ React.useEffect(() => {
366
+ if (!removing) return
367
+ store.onHeightRemove(banner.id)
368
+ const timeoutId = setTimeout(
369
+ () => store.onBannerRemove(banner.id),
370
+ BANNER_ANIMATION_DURATION,
371
+ )
372
+ return () => clearTimeout(timeoutId)
373
+ }, [removing, store, banner.id])
374
+
375
+ const onClose = React.useCallback(
376
+ () => store.onRemovingChange(banner.id, true),
377
+ [store, banner.id],
378
+ )
379
+
380
+ const onRemove = React.useCallback(
381
+ () => store.onBannerRemove(banner.id),
382
+ [store, banner.id],
383
+ )
384
+
385
+ const dismissible = banner.dismissible ?? DEFAULT_BANNER_DISMISSIBLE
386
+
387
+ const contextValue = React.useMemo<BannerContextValue>(
388
+ () => ({ id: banner.id, variant: banner.variant, dismissible, onClose }),
389
+ [banner.id, banner.variant, dismissible, onClose],
390
+ )
391
+
392
+ const renderProps = React.useMemo<BannerRenderProps>(
393
+ () => ({
394
+ id: banner.id,
395
+ variant: banner.variant,
396
+ dismissible,
397
+ onClose,
398
+ onRemove,
399
+ }),
400
+ [banner.id, banner.variant, dismissible, onClose, onRemove],
401
+ )
402
+
403
+ const currentOffset = removing ? offsetBeforeRemoveRef.current : offset
404
+ const isTop = side === "top"
405
+
406
+ function getTransform() {
407
+ if (!mounted) return isTop ? "translateY(-100%)" : "translateY(100%)"
408
+ if (removing) {
409
+ return isTop
410
+ ? `translateY(calc(${currentOffset}px - 100%))`
411
+ : `translateY(calc(-${currentOffset}px + 100%))`
412
+ }
413
+ return isTop
414
+ ? `translateY(${currentOffset}px)`
415
+ : `translateY(-${currentOffset}px)`
416
+ }
417
+
418
+ return (
419
+ <BannerContext.Provider value={contextValue}>
420
+ <div
421
+ role="status"
422
+ aria-live="polite"
423
+ data-slot="queued-banner"
424
+ data-state={removing ? "closed" : "open"}
425
+ data-mounted={mounted}
426
+ data-removed={removing}
427
+ data-side={side}
428
+ data-front={index === 0}
429
+ data-index={index}
430
+ ref={bannerRef}
431
+ className={bannerVariants({ variant: banner.variant })}
432
+ style={{
433
+ position: "absolute",
434
+ [isTop ? "top" : "bottom"]: 0,
435
+ left: 0,
436
+ right: 0,
437
+ zIndex: removing ? 0 : 50 - index,
438
+ transform: getTransform(),
439
+ opacity: mounted && !removing ? 1 : 0,
440
+ transition: `transform ${BANNER_ANIMATION_DURATION}ms cubic-bezier(0.32, 0.72, 0, 1), opacity ${removing ? BANNER_ANIMATION_DURATION / 2 : BANNER_ANIMATION_DURATION}ms ease`,
441
+ }}
442
+ >
443
+ {typeof banner.content === "function"
444
+ ? banner.content(renderProps)
445
+ : banner.content}
446
+ </div>
447
+ </BannerContext.Provider>
448
+ )
449
+ }
450
+
451
+ interface BannerProps extends DivProps, VariantProps<typeof bannerVariants> {
452
+ open?: boolean
453
+ defaultOpen?: boolean
454
+ onOpenChange?: (open: boolean) => void
455
+ onDismiss?: () => void
456
+ priority?: number
457
+ dismissible?: boolean
458
+ duration?: number
459
+ }
460
+
461
+ function Banner(props: BannerProps) {
462
+ const {
463
+ className,
464
+ variant = "default",
465
+ open: openProp,
466
+ defaultOpen,
467
+ onOpenChange,
468
+ onDismiss,
469
+ priority,
470
+ dismissible = DEFAULT_BANNER_DISMISSIBLE,
471
+ duration,
472
+ children,
473
+ asChild,
474
+ ...rootProps
475
+ } = props
476
+
477
+ const store = React.useContext(StoreContext)
478
+
479
+ const isInsideStore = store !== null
480
+ const isControlled = openProp !== undefined
481
+
482
+ const openRef = React.useRef(openProp ?? defaultOpen ?? true)
483
+ const listenersRef = React.useRef<Set<() => void>>(new Set())
484
+ const bannerIdRef = React.useRef<string | null>(null)
485
+ const onDismissRef = React.useRef(onDismiss)
486
+ const onOpenChangeRef = React.useRef(onOpenChange)
487
+
488
+ if (isControlled) {
489
+ openRef.current = openProp
490
+ }
491
+
492
+ React.useEffect(() => {
493
+ onDismissRef.current = onDismiss
494
+ }, [onDismiss])
495
+
496
+ React.useEffect(() => {
497
+ onOpenChangeRef.current = onOpenChange
498
+ }, [onOpenChange])
499
+
500
+ const subscribe = React.useCallback(
501
+ (cb: () => void) => {
502
+ listenersRef.current.add(cb)
503
+ return () => listenersRef.current.delete(cb)
504
+ },
505
+ [],
506
+ )
507
+
508
+ const getSnapshot = React.useCallback(() => openRef.current, [])
509
+
510
+ const open = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
511
+
512
+ React.useEffect(() => {
513
+ if (!isInsideStore || !store || !open) return
514
+
515
+ const id = store.onBannerAdd({
516
+ content: children,
517
+ variant: variant ?? undefined,
518
+ priority,
519
+ dismissible,
520
+ duration,
521
+ onDismiss: () => {
522
+ onDismissRef.current?.()
523
+ onOpenChangeRef.current?.(false)
524
+ },
525
+ })
526
+ bannerIdRef.current = id
527
+
528
+ return () => {
529
+ if (bannerIdRef.current) {
530
+ store.onBannerRemove(bannerIdRef.current)
531
+ bannerIdRef.current = null
532
+ }
533
+ }
534
+ }, [
535
+ isInsideStore,
536
+ store,
537
+ open,
538
+ children,
539
+ variant,
540
+ priority,
541
+ dismissible,
542
+ duration,
543
+ ])
544
+
545
+ const onClose = React.useCallback(() => {
546
+ if (!isControlled) {
547
+ openRef.current = false
548
+ for (const listener of listenersRef.current) {
549
+ listener()
550
+ }
551
+ }
552
+ onOpenChangeRef.current?.(false)
553
+ }, [isControlled])
554
+
555
+ const contextValue = React.useMemo<BannerContextValue>(
556
+ () => ({
557
+ variant,
558
+ dismissible,
559
+ onClose,
560
+ }),
561
+ [variant, dismissible, onClose],
562
+ )
563
+
564
+ if (!open || isInsideStore) return null
565
+
566
+ const RootPrimitive = asChild ? Slot : "div"
567
+
568
+ return (
569
+ <BannerContext.Provider value={contextValue}>
570
+ <RootPrimitive
571
+ role="status"
572
+ aria-live="polite"
573
+ data-slot="banner"
574
+ data-state="open"
575
+ className={cn(bannerVariants({ variant, className }))}
576
+ {...rootProps}
577
+ >
578
+ {children}
579
+ </RootPrimitive>
580
+ </BannerContext.Provider>
581
+ )
582
+ }
583
+
584
+ function BannerIcon(props: DivProps) {
585
+ const { className, asChild, ...iconProps } = props
586
+
587
+ const IconPrimitive = asChild ? Slot : "div"
588
+
589
+ return (
590
+ <IconPrimitive
591
+ data-slot="banner-icon"
592
+ className={cn("flex shrink-0 items-center [&>svg]:size-4", className)}
593
+ {...iconProps}
594
+ />
595
+ )
596
+ }
597
+
598
+ function BannerContent(props: DivProps) {
599
+ const { className, asChild, ...contentProps } = props
600
+
601
+ const ContentPrimitive = asChild ? Slot : "div"
602
+
603
+ return (
604
+ <ContentPrimitive
605
+ data-slot="banner-content"
606
+ className={cn("flex min-w-0 flex-1 flex-col gap-1", className)}
607
+ {...contentProps}
608
+ />
609
+ )
610
+ }
611
+
612
+ function BannerTitle(props: React.ComponentProps<"div">) {
613
+ const { className, ...titleProps } = props
614
+
615
+ return (
616
+ <div
617
+ data-slot="banner-title"
618
+ className={cn("font-medium text-sm leading-none", className)}
619
+ {...titleProps}
620
+ />
621
+ )
622
+ }
623
+
624
+ function BannerDescription(props: React.ComponentProps<"div">) {
625
+ const { className, ...descriptionProps } = props
626
+
627
+ return (
628
+ <div
629
+ data-slot="banner-description"
630
+ className={cn("text-xs opacity-90", className)}
631
+ {...descriptionProps}
632
+ />
633
+ )
634
+ }
635
+
636
+ function BannerActions(props: DivProps) {
637
+ const { className, asChild, ...actionsProps } = props
638
+
639
+ const ActionsPrimitive = asChild ? Slot : "div"
640
+
641
+ return (
642
+ <ActionsPrimitive
643
+ data-slot="banner-actions"
644
+ className={cn("flex items-center gap-2", className)}
645
+ {...actionsProps}
646
+ />
647
+ )
648
+ }
649
+
650
+ function BannerClose(props: React.ComponentProps<typeof Button>) {
651
+ const { onClick: onClickProp, disabled, children, ...closeProps } = props
652
+
653
+ const { dismissible = DEFAULT_BANNER_DISMISSIBLE, onClose } =
654
+ useBannerContext("BannerClose")
655
+
656
+ const isDisabled = disabled ?? !dismissible
657
+
658
+ const onClick = React.useCallback(
659
+ (event: React.MouseEvent<CloseElement>) => {
660
+ onClickProp?.(event)
661
+ if (event.defaultPrevented || isDisabled) return
662
+ onClose?.()
663
+ },
664
+ [onClickProp, isDisabled, onClose],
665
+ )
666
+
667
+ return (
668
+ <Button
669
+ data-slot="banner-close"
670
+ variant="ghost"
671
+ size="sm"
672
+ onClick={onClick}
673
+ disabled={isDisabled}
674
+ {...closeProps}
675
+ >
676
+ {children ?? <X className="size-3.5" />}
677
+ </Button>
678
+ )
679
+ }
680
+
681
+ export {
682
+ Banner,
683
+ BannerActions,
684
+ BannerClose,
685
+ BannerContent,
686
+ BannerDescription,
687
+ BannerIcon,
688
+ Banners,
689
+ BannerTitle,
690
+ useBanner,
691
+ useBanners,
692
+ }
693
+ export type { BannerProps, BannersProps, BannerVariant }