@djangocfg/ui-core 2.1.412 → 2.1.413
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/components/data/avatar-group/index.tsx +224 -0
- package/src/components/data/badge-overflow/index.tsx +259 -0
- package/src/components/data/circular-progress/index.tsx +358 -0
- package/src/components/data/relative-time-card/index.tsx +191 -0
- package/src/components/data/stat/index.tsx +140 -0
- package/src/components/data/status/index.tsx +80 -0
- package/src/components/effects/GlowBackground.tsx +9 -1
- package/src/components/effects/swap/index.tsx +289 -0
- package/src/components/feedback/banner/index.tsx +693 -0
- package/src/components/forms/checkbox-group/index.tsx +243 -0
- package/src/components/forms/editable/index.tsx +420 -0
- package/src/components/forms/input-otp/index.tsx +12 -3
- package/src/components/forms/mask-input/index.tsx +466 -0
- package/src/components/forms/otp/index.tsx +12 -8
- package/src/components/forms/segmented-input/index.tsx +319 -0
- package/src/components/forms/tags-input/index.tsx +896 -0
- package/src/components/forms/time-picker/index.tsx +285 -0
- package/src/components/index.ts +51 -0
- package/src/components/layout/key-value/index.tsx +884 -0
- package/src/components/layout/stack/index.tsx +349 -0
- package/src/components/navigation/context-menu/index.tsx +9 -6
- package/src/components/navigation/stepper/index.tsx +1307 -0
- package/src/components/select/multi-select-pro-async.tsx +11 -2
- package/src/components/select/multi-select-pro.tsx +11 -2
- package/src/components/specialized/presence/index.tsx +181 -0
- package/src/components/specialized/primitive/index.tsx +83 -0
- package/src/components/specialized/visually-hidden/index.tsx +19 -0
- package/src/components/specialized/visually-hidden-input/index.tsx +99 -0
- package/src/hooks/dom/index.ts +4 -0
- package/src/hooks/dom/useFormReset.ts +49 -0
- package/src/hooks/dom/useLayoutEffect.ts +16 -0
- package/src/hooks/dom/useSize.ts +57 -0
- package/src/hooks/state/index.ts +4 -0
- package/src/hooks/state/useCallbackRef.ts +25 -0
- package/src/hooks/state/usePrevious.ts +20 -0
- package/src/hooks/state/useStateMachine.ts +29 -0
- package/src/lib/compose-event-handlers.ts +22 -0
- package/src/lib/compose-refs.ts +65 -0
- package/src/lib/create-context.tsx +62 -0
- package/src/lib/get-element-ref.ts +33 -0
- package/src/lib/index.ts +5 -0
- package/src/lib/styles.ts +103 -0
- package/src/styles/README.md +43 -0
- package/src/styles/palette/utils.ts +15 -5
- package/src/styles/utilities/animations.css +135 -0
- package/src/styles/utilities/display.css +62 -0
- package/src/styles/utilities/glass.css +57 -0
- package/src/styles/utilities/marquee.css +69 -0
- package/src/styles/utilities/step.css +25 -0
- 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 }
|