@boyernick/standard-ui-react 0.1.1-canary.2 → 0.1.1-canary.20
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 +5 -3
- package/src/accordion.tsx +1 -1
- package/src/alert-dialog.tsx +1 -1
- package/src/autocomplete.tsx +12 -11
- package/src/badge.tsx +73 -9
- package/src/brand.tsx +2 -2
- package/src/breadcrumb.tsx +38 -20
- package/src/button.tsx +2 -2
- package/src/calendar.tsx +22 -11
- package/src/card.tsx +57 -14
- package/src/carousel.tsx +163 -6
- package/src/checkbox.tsx +35 -13
- package/src/collapsible.tsx +4 -2
- package/src/combobox.tsx +13 -12
- package/src/command.tsx +38 -14
- package/src/context-menu.tsx +6 -3
- package/src/date-picker.tsx +174 -0
- package/src/dialog.tsx +33 -10
- package/src/drawer.tsx +1 -1
- package/src/empty.tsx +4 -4
- package/src/field.tsx +1 -1
- package/src/filter-group.tsx +139 -0
- package/src/icons.tsx +39 -0
- package/src/illustrations.tsx +219 -141
- package/src/image-modal.tsx +440 -49
- package/src/index.ts +163 -0
- package/src/input.tsx +1 -1
- package/src/kbd.tsx +49 -0
- package/src/lib/motion.ts +128 -18
- package/src/lib/popup.ts +45 -0
- package/src/menu.tsx +7 -4
- package/src/menubar.tsx +5 -0
- package/src/meter.tsx +1 -1
- package/src/minimap.tsx +267 -0
- package/src/navigation-menu.tsx +336 -67
- package/src/number-field.tsx +327 -61
- package/src/pagination.tsx +258 -42
- package/src/popover.tsx +3 -1
- package/src/preview-card.tsx +3 -1
- package/src/progress.tsx +6 -1
- package/src/questionnaire.tsx +279 -0
- package/src/scroll-area.tsx +1 -1
- package/src/select.tsx +8 -6
- package/src/sidebar.tsx +109 -28
- package/src/slider.tsx +64 -6
- package/src/sounds.tsx +6 -10
- package/src/spinner.tsx +105 -32
- package/src/switch.tsx +2 -2
- package/src/table.tsx +82 -15
- package/src/tabs.tsx +184 -38
- package/src/text-animate.tsx +71 -28
- package/src/textarea.tsx +1 -1
- package/src/toast.tsx +156 -23
- package/src/tooltip.tsx +43 -3
- package/src/video-player.tsx +296 -120
package/src/image-modal.tsx
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useState,
|
|
10
|
+
type ComponentProps,
|
|
11
|
+
type ImgHTMLAttributes,
|
|
12
|
+
type KeyboardEvent,
|
|
13
|
+
type ReactElement,
|
|
14
|
+
type ReactNode,
|
|
15
|
+
} from "react"
|
|
16
|
+
import { Button, buttonVariants } from "./button"
|
|
17
|
+
import {
|
|
18
|
+
Carousel,
|
|
19
|
+
CarouselContent,
|
|
20
|
+
CarouselItem,
|
|
21
|
+
type CarouselApi,
|
|
22
|
+
} from "./carousel"
|
|
5
23
|
import {
|
|
6
24
|
Dialog,
|
|
7
25
|
DialogBackdrop,
|
|
@@ -11,18 +29,103 @@ import {
|
|
|
11
29
|
DialogTitle,
|
|
12
30
|
DialogTrigger,
|
|
13
31
|
} from "./dialog"
|
|
32
|
+
import {
|
|
33
|
+
IconArrowDown,
|
|
34
|
+
IconChevronRightSmall,
|
|
35
|
+
IconCrossSmall,
|
|
36
|
+
} from "./icons"
|
|
14
37
|
import { cn } from "./lib/cn"
|
|
15
38
|
import { motion } from "./lib/motion"
|
|
39
|
+
import {
|
|
40
|
+
Tooltip,
|
|
41
|
+
TooltipPopup,
|
|
42
|
+
TooltipPortal,
|
|
43
|
+
TooltipPositioner,
|
|
44
|
+
TooltipProvider,
|
|
45
|
+
TooltipTrigger,
|
|
46
|
+
type TooltipPositionerProps,
|
|
47
|
+
} from "./tooltip"
|
|
48
|
+
|
|
49
|
+
const TRACKPAD_LABEL =
|
|
50
|
+
"Swipe, use the arrow keys, or use the controls to navigate"
|
|
51
|
+
|
|
52
|
+
const imageModalControlClassName =
|
|
53
|
+
"border-border-inverted bg-surface-inverted/60 text-fg-inverted shadow-md backdrop-blur-md hover:bg-surface-inverted/80 hover:text-fg-inverted focus-visible:border-ring focus-visible:ring-border-inverted focus-visible:ring-offset-surface-inverted"
|
|
54
|
+
|
|
55
|
+
export const imageModalContentVariants = cva(
|
|
56
|
+
"h-dvh max-h-dvh w-screen max-w-none gap-0 overflow-hidden rounded-none border-0 bg-transparent p-0 shadow-none max-sm:max-w-none",
|
|
57
|
+
{
|
|
58
|
+
variants: {
|
|
59
|
+
variant: {
|
|
60
|
+
default: "[&_[data-slot=image-modal-stage]]:pb-0",
|
|
61
|
+
caption: "[&_[data-slot=image-modal-stage]]:pb-20",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
defaultVariants: {
|
|
65
|
+
variant: "default",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
)
|
|
16
69
|
|
|
17
70
|
export type ImageModalProps = ComponentProps<typeof Dialog>
|
|
18
71
|
export type ImageModalTriggerProps = ComponentProps<typeof DialogTrigger>
|
|
19
|
-
export type ImageModalContentProps =
|
|
72
|
+
export type ImageModalContentProps = Omit<
|
|
73
|
+
ComponentProps<typeof DialogPopup>,
|
|
74
|
+
"children"
|
|
75
|
+
> &
|
|
76
|
+
VariantProps<typeof imageModalContentVariants> & {
|
|
77
|
+
src: string
|
|
78
|
+
alt: string
|
|
79
|
+
caption?: ReactNode
|
|
80
|
+
downloadSrc?: string
|
|
81
|
+
downloadName?: string
|
|
82
|
+
imgProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">
|
|
83
|
+
children?: ReactNode
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ImageModalItem = {
|
|
20
87
|
src: string
|
|
21
88
|
alt: string
|
|
22
|
-
caption?:
|
|
89
|
+
caption?: ReactNode
|
|
90
|
+
downloadSrc?: string
|
|
91
|
+
downloadName?: string
|
|
23
92
|
imgProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">
|
|
24
93
|
}
|
|
25
94
|
|
|
95
|
+
export type ImageModalGalleryProps = Omit<ImageModalProps, "children"> &
|
|
96
|
+
VariantProps<typeof imageModalContentVariants> & {
|
|
97
|
+
images: readonly ImageModalItem[]
|
|
98
|
+
children: ReactNode
|
|
99
|
+
loop?: boolean
|
|
100
|
+
contentClassName?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type ImageModalGalleryTriggerProps = ImageModalTriggerProps & {
|
|
104
|
+
index: number
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type ImageModalTriggerClickEvent = Parameters<
|
|
108
|
+
NonNullable<ImageModalTriggerProps["onClick"]>
|
|
109
|
+
>[0]
|
|
110
|
+
|
|
111
|
+
type ImageModalGalleryContextValue = {
|
|
112
|
+
images: readonly ImageModalItem[]
|
|
113
|
+
setActiveIndex: (index: number) => void
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ImageModalGalleryContext =
|
|
117
|
+
createContext<ImageModalGalleryContextValue | null>(null)
|
|
118
|
+
|
|
119
|
+
const useImageModalGallery = () => {
|
|
120
|
+
const context = useContext(ImageModalGalleryContext)
|
|
121
|
+
if (!context) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"ImageModalGalleryTrigger must be used within <ImageModalGallery>",
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
return context
|
|
127
|
+
}
|
|
128
|
+
|
|
26
129
|
export const ImageModal = (props: ImageModalProps) => <Dialog {...props} />
|
|
27
130
|
|
|
28
131
|
export const ImageModalTrigger = ({
|
|
@@ -38,73 +141,361 @@ export const ImageModalTrigger = ({
|
|
|
38
141
|
/>
|
|
39
142
|
)
|
|
40
143
|
|
|
144
|
+
const ImageModalBackdrop = () => (
|
|
145
|
+
<DialogBackdrop className="bg-surface-inverted/95" />
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const ImageModalBackground = ({ src }: { src: string }) => (
|
|
149
|
+
<div
|
|
150
|
+
aria-hidden="true"
|
|
151
|
+
className="pointer-events-none absolute inset-0 overflow-hidden"
|
|
152
|
+
>
|
|
153
|
+
{/* Consumers may use any image host, so this package deliberately renders
|
|
154
|
+
the platform-neutral image element rather than Next.js Image. */}
|
|
155
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
156
|
+
<img
|
|
157
|
+
src={src}
|
|
158
|
+
alt=""
|
|
159
|
+
className="size-full scale-110 object-cover opacity-25 blur-3xl"
|
|
160
|
+
/>
|
|
161
|
+
<div className="absolute inset-0 bg-surface-inverted/65" />
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const ImageModalImage = ({
|
|
166
|
+
src,
|
|
167
|
+
alt,
|
|
168
|
+
imgProps,
|
|
169
|
+
}: Pick<ImageModalItem, "src" | "alt" | "imgProps">) => {
|
|
170
|
+
const { className, ...props } = imgProps ?? {}
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
174
|
+
<img
|
|
175
|
+
src={src}
|
|
176
|
+
alt={alt}
|
|
177
|
+
draggable={false}
|
|
178
|
+
className={cn(
|
|
179
|
+
"max-h-[82dvh] max-w-[88vw] rounded-lg object-contain shadow-lg select-none max-sm:max-h-[calc(100dvh-2rem)] max-sm:max-w-[calc(100vw-2rem)]",
|
|
180
|
+
className,
|
|
181
|
+
)}
|
|
182
|
+
{...props}
|
|
183
|
+
/>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ImageModalControlTooltip = ({
|
|
188
|
+
label,
|
|
189
|
+
side = "bottom",
|
|
190
|
+
children,
|
|
191
|
+
}: {
|
|
192
|
+
label: string
|
|
193
|
+
side?: TooltipPositionerProps["side"]
|
|
194
|
+
children: ReactElement
|
|
195
|
+
}) => (
|
|
196
|
+
<Tooltip>
|
|
197
|
+
<TooltipTrigger render={children} />
|
|
198
|
+
<TooltipPortal>
|
|
199
|
+
<TooltipPositioner side={side}>
|
|
200
|
+
<TooltipPopup variant="inverted">{label}</TooltipPopup>
|
|
201
|
+
</TooltipPositioner>
|
|
202
|
+
</TooltipPortal>
|
|
203
|
+
</Tooltip>
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const ImageModalDownload = ({
|
|
207
|
+
src,
|
|
208
|
+
name,
|
|
209
|
+
}: {
|
|
210
|
+
src: string
|
|
211
|
+
name?: string
|
|
212
|
+
}) => (
|
|
213
|
+
<ImageModalControlTooltip label="Download image">
|
|
214
|
+
<a
|
|
215
|
+
href={src}
|
|
216
|
+
download={name ?? ""}
|
|
217
|
+
aria-label="Download image"
|
|
218
|
+
className={cn(
|
|
219
|
+
buttonVariants({
|
|
220
|
+
variant: "ghost",
|
|
221
|
+
size: "md",
|
|
222
|
+
rounded: true,
|
|
223
|
+
iconOnly: true,
|
|
224
|
+
}),
|
|
225
|
+
imageModalControlClassName,
|
|
226
|
+
"absolute top-4 right-4 z-20",
|
|
227
|
+
)}
|
|
228
|
+
>
|
|
229
|
+
<IconArrowDown aria-hidden />
|
|
230
|
+
</a>
|
|
231
|
+
</ImageModalControlTooltip>
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const ImageModalDismiss = () => (
|
|
235
|
+
<ImageModalControlTooltip label="Close image">
|
|
236
|
+
<DialogClose
|
|
237
|
+
render={
|
|
238
|
+
<Button
|
|
239
|
+
type="button"
|
|
240
|
+
variant="ghost"
|
|
241
|
+
size="md"
|
|
242
|
+
iconOnly
|
|
243
|
+
rounded
|
|
244
|
+
aria-label="Close image"
|
|
245
|
+
className={cn(
|
|
246
|
+
imageModalControlClassName,
|
|
247
|
+
"absolute top-4 left-4 z-20",
|
|
248
|
+
)}
|
|
249
|
+
/>
|
|
250
|
+
}
|
|
251
|
+
>
|
|
252
|
+
<IconCrossSmall aria-hidden />
|
|
253
|
+
</DialogClose>
|
|
254
|
+
</ImageModalControlTooltip>
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
const ImageModalCaption = ({ children }: { children: ReactNode }) => (
|
|
258
|
+
<div className="absolute inset-x-0 bottom-0 z-10 flex min-h-20 items-center justify-center bg-surface-inverted/65 px-6 py-4 text-center text-sm text-fg-inverted-secondary backdrop-blur-md">
|
|
259
|
+
{children}
|
|
260
|
+
</div>
|
|
261
|
+
)
|
|
262
|
+
|
|
41
263
|
export const ImageModalContent = ({
|
|
42
264
|
src,
|
|
43
265
|
alt,
|
|
44
266
|
caption,
|
|
267
|
+
downloadSrc,
|
|
268
|
+
downloadName,
|
|
269
|
+
variant = "default",
|
|
45
270
|
className,
|
|
46
271
|
imgProps,
|
|
47
272
|
children,
|
|
48
273
|
...props
|
|
49
274
|
}: ImageModalContentProps) => (
|
|
50
275
|
<DialogPortal>
|
|
51
|
-
<
|
|
276
|
+
<ImageModalBackdrop />
|
|
52
277
|
<DialogPopup
|
|
53
278
|
data-slot="image-modal-content"
|
|
54
279
|
className={cn(
|
|
55
|
-
|
|
280
|
+
imageModalContentVariants({ variant }),
|
|
56
281
|
motion.popupCenter,
|
|
57
282
|
className,
|
|
58
283
|
)}
|
|
59
284
|
{...props}
|
|
60
285
|
>
|
|
61
|
-
<
|
|
286
|
+
<TooltipProvider delay={250}>
|
|
287
|
+
<ImageModalBackground src={src} />
|
|
62
288
|
<DialogTitle className="sr-only">{alt}</DialogTitle>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
alt={alt}
|
|
67
|
-
className={cn(
|
|
68
|
-
"max-h-[min(80vh,56rem)] w-full object-contain bg-background-secondary",
|
|
69
|
-
imgProps?.className,
|
|
70
|
-
)}
|
|
71
|
-
{...imgProps}
|
|
72
|
-
/>
|
|
73
|
-
<DialogClose
|
|
74
|
-
render={
|
|
75
|
-
<Button
|
|
76
|
-
type="button"
|
|
77
|
-
variant="secondary"
|
|
78
|
-
size="sm"
|
|
79
|
-
iconOnly
|
|
80
|
-
rounded
|
|
81
|
-
aria-label="Close"
|
|
82
|
-
className="absolute top-3 right-3"
|
|
83
|
-
/>
|
|
84
|
-
}
|
|
289
|
+
<div
|
|
290
|
+
data-slot="image-modal-stage"
|
|
291
|
+
className="relative z-10 flex size-full items-center justify-center p-4"
|
|
85
292
|
>
|
|
86
|
-
<
|
|
87
|
-
</DialogClose>
|
|
88
|
-
</div>
|
|
89
|
-
{caption || children ? (
|
|
90
|
-
<div className="border-t border-border-primary px-4 py-3">
|
|
91
|
-
{caption ? (
|
|
92
|
-
<p className="text-sm text-fg-secondary">{caption}</p>
|
|
93
|
-
) : null}
|
|
94
|
-
{children}
|
|
293
|
+
<ImageModalImage src={src} alt={alt} imgProps={imgProps} />
|
|
95
294
|
</div>
|
|
96
|
-
|
|
295
|
+
<ImageModalDismiss />
|
|
296
|
+
<ImageModalDownload
|
|
297
|
+
src={downloadSrc ?? src}
|
|
298
|
+
name={downloadName}
|
|
299
|
+
/>
|
|
300
|
+
{variant === "caption" && (caption || children) ? (
|
|
301
|
+
<ImageModalCaption>{caption ?? children}</ImageModalCaption>
|
|
302
|
+
) : null}
|
|
303
|
+
</TooltipProvider>
|
|
97
304
|
</DialogPopup>
|
|
98
305
|
</DialogPortal>
|
|
99
306
|
)
|
|
100
307
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
308
|
+
export const ImageModalGalleryTrigger = ({
|
|
309
|
+
index,
|
|
310
|
+
onClick,
|
|
311
|
+
...props
|
|
312
|
+
}: ImageModalGalleryTriggerProps) => {
|
|
313
|
+
const { images, setActiveIndex } = useImageModalGallery()
|
|
314
|
+
|
|
315
|
+
const handleClick = (event: ImageModalTriggerClickEvent) => {
|
|
316
|
+
onClick?.(event)
|
|
317
|
+
if (!event.defaultPrevented && images[index]) setActiveIndex(index)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return <ImageModalTrigger onClick={handleClick} {...props} />
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const ImageModalGalleryContent = ({
|
|
324
|
+
images,
|
|
325
|
+
activeIndex,
|
|
326
|
+
setActiveIndex,
|
|
327
|
+
loop,
|
|
328
|
+
variant,
|
|
329
|
+
className,
|
|
330
|
+
}: {
|
|
331
|
+
images: readonly ImageModalItem[]
|
|
332
|
+
activeIndex: number
|
|
333
|
+
setActiveIndex: (index: number) => void
|
|
334
|
+
loop: boolean
|
|
335
|
+
variant: "default" | "caption"
|
|
336
|
+
className?: string
|
|
337
|
+
}) => {
|
|
338
|
+
const [api, setApi] = useState<CarouselApi>()
|
|
339
|
+
const activeImage = images[activeIndex]
|
|
340
|
+
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
api?.scrollTo(activeIndex, true)
|
|
343
|
+
}, [activeIndex, api])
|
|
344
|
+
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
if (!api) return
|
|
347
|
+
const syncIndex = () => setActiveIndex(api.selectedScrollSnap())
|
|
348
|
+
api.on("select", syncIndex)
|
|
349
|
+
api.on("reInit", syncIndex)
|
|
350
|
+
return () => {
|
|
351
|
+
api.off("select", syncIndex)
|
|
352
|
+
api.off("reInit", syncIndex)
|
|
353
|
+
}
|
|
354
|
+
}, [api, setActiveIndex])
|
|
355
|
+
|
|
356
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
357
|
+
if (event.key === "ArrowLeft") {
|
|
358
|
+
event.preventDefault()
|
|
359
|
+
api?.scrollPrev()
|
|
360
|
+
} else if (event.key === "ArrowRight") {
|
|
361
|
+
event.preventDefault()
|
|
362
|
+
api?.scrollNext()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!activeImage) return null
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<DialogPortal>
|
|
370
|
+
<ImageModalBackdrop />
|
|
371
|
+
<DialogPopup
|
|
372
|
+
data-slot="image-modal-gallery-content"
|
|
373
|
+
aria-describedby={undefined}
|
|
374
|
+
onKeyDownCapture={handleKeyDown}
|
|
375
|
+
className={cn(
|
|
376
|
+
imageModalContentVariants({ variant }),
|
|
377
|
+
motion.popupCenter,
|
|
378
|
+
className,
|
|
379
|
+
)}
|
|
380
|
+
>
|
|
381
|
+
<TooltipProvider delay={250}>
|
|
382
|
+
<ImageModalBackground src={activeImage.src} />
|
|
383
|
+
<DialogTitle className="sr-only">{activeImage.alt}</DialogTitle>
|
|
384
|
+
<p className="sr-only" aria-live="polite">
|
|
385
|
+
Image {activeIndex + 1} of {images.length}. {TRACKPAD_LABEL}.
|
|
386
|
+
</p>
|
|
387
|
+
<Carousel
|
|
388
|
+
setApi={setApi}
|
|
389
|
+
opts={{ loop, startIndex: activeIndex, duration: 24 }}
|
|
390
|
+
data-slot="image-modal-stage"
|
|
391
|
+
className="relative z-10 size-full"
|
|
392
|
+
aria-label="Image gallery"
|
|
393
|
+
>
|
|
394
|
+
<CarouselContent className="h-full -ml-0">
|
|
395
|
+
{images.map((image, index) => (
|
|
396
|
+
<CarouselItem
|
|
397
|
+
key={`${image.src}-${index}`}
|
|
398
|
+
className="flex h-full items-center justify-center pl-0 p-4"
|
|
399
|
+
aria-label={`${index + 1} of ${images.length}`}
|
|
400
|
+
>
|
|
401
|
+
<ImageModalImage {...image} />
|
|
402
|
+
</CarouselItem>
|
|
403
|
+
))}
|
|
404
|
+
</CarouselContent>
|
|
405
|
+
</Carousel>
|
|
406
|
+
<ImageModalDismiss />
|
|
407
|
+
<ImageModalDownload
|
|
408
|
+
src={activeImage.downloadSrc ?? activeImage.src}
|
|
409
|
+
name={activeImage.downloadName}
|
|
410
|
+
/>
|
|
411
|
+
{images.length > 1 ? (
|
|
412
|
+
<>
|
|
413
|
+
<ImageModalControlTooltip label="Previous image" side="right">
|
|
414
|
+
<Button
|
|
415
|
+
type="button"
|
|
416
|
+
variant="ghost"
|
|
417
|
+
size="lg"
|
|
418
|
+
iconOnly
|
|
419
|
+
rounded
|
|
420
|
+
aria-label="Previous image"
|
|
421
|
+
disabled={!loop && !api?.canScrollPrev()}
|
|
422
|
+
onClick={() => api?.scrollPrev()}
|
|
423
|
+
className={cn(
|
|
424
|
+
imageModalControlClassName,
|
|
425
|
+
"absolute top-1/2 left-4 z-20 -translate-y-1/2 active:!-translate-y-1/2",
|
|
426
|
+
)}
|
|
427
|
+
>
|
|
428
|
+
<IconChevronRightSmall className="rotate-180" aria-hidden />
|
|
429
|
+
</Button>
|
|
430
|
+
</ImageModalControlTooltip>
|
|
431
|
+
<ImageModalControlTooltip label="Next image" side="left">
|
|
432
|
+
<Button
|
|
433
|
+
type="button"
|
|
434
|
+
variant="ghost"
|
|
435
|
+
size="lg"
|
|
436
|
+
iconOnly
|
|
437
|
+
rounded
|
|
438
|
+
aria-label="Next image"
|
|
439
|
+
disabled={!loop && !api?.canScrollNext()}
|
|
440
|
+
onClick={() => api?.scrollNext()}
|
|
441
|
+
className={cn(
|
|
442
|
+
imageModalControlClassName,
|
|
443
|
+
"absolute top-1/2 right-4 z-20 -translate-y-1/2 active:!-translate-y-1/2",
|
|
444
|
+
)}
|
|
445
|
+
>
|
|
446
|
+
<IconChevronRightSmall aria-hidden />
|
|
447
|
+
</Button>
|
|
448
|
+
</ImageModalControlTooltip>
|
|
449
|
+
</>
|
|
450
|
+
) : null}
|
|
451
|
+
{variant === "caption" && activeImage.caption ? (
|
|
452
|
+
<ImageModalCaption>{activeImage.caption}</ImageModalCaption>
|
|
453
|
+
) : null}
|
|
454
|
+
</TooltipProvider>
|
|
455
|
+
</DialogPopup>
|
|
456
|
+
</DialogPortal>
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export const ImageModalGallery = ({
|
|
461
|
+
images,
|
|
462
|
+
children,
|
|
463
|
+
loop = true,
|
|
464
|
+
variant = "default",
|
|
465
|
+
contentClassName,
|
|
466
|
+
...props
|
|
467
|
+
}: ImageModalGalleryProps) => {
|
|
468
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
469
|
+
const safeIndex = images.length
|
|
470
|
+
? Math.min(activeIndex, images.length - 1)
|
|
471
|
+
: 0
|
|
472
|
+
const setSafeActiveIndex = useCallback(
|
|
473
|
+
(index: number) => {
|
|
474
|
+
if (images[index]) setActiveIndex(index)
|
|
475
|
+
},
|
|
476
|
+
[images],
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
<Dialog {...props}>
|
|
481
|
+
<ImageModalGalleryContext.Provider
|
|
482
|
+
value={{
|
|
483
|
+
images,
|
|
484
|
+
setActiveIndex: setSafeActiveIndex,
|
|
485
|
+
}}
|
|
486
|
+
>
|
|
487
|
+
{children}
|
|
488
|
+
{images.length ? (
|
|
489
|
+
<ImageModalGalleryContent
|
|
490
|
+
images={images}
|
|
491
|
+
activeIndex={safeIndex}
|
|
492
|
+
setActiveIndex={setSafeActiveIndex}
|
|
493
|
+
loop={loop}
|
|
494
|
+
variant={variant ?? "default"}
|
|
495
|
+
className={contentClassName}
|
|
496
|
+
/>
|
|
497
|
+
) : null}
|
|
498
|
+
</ImageModalGalleryContext.Provider>
|
|
499
|
+
</Dialog>
|
|
500
|
+
)
|
|
501
|
+
}
|