@jobber/components 6.113.1 → 6.114.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.
@@ -8,6 +8,8 @@ require('../LightBox-cjs.js');
8
8
  require('framer-motion');
9
9
  require('react-dom');
10
10
  require('@jobber/hooks');
11
+ require('../noop-cjs.js');
12
+ require('../_commonjsHelpers-cjs.js');
11
13
  require('../ButtonDismiss-cjs.js');
12
14
  require('../Button-cjs.js');
13
15
  require('react-router-dom');
@@ -17,7 +19,6 @@ require('../Typography-cjs.js');
17
19
  require('../Text-cjs.js');
18
20
  require('../Heading-cjs.js');
19
21
  require('../AtlantisThemeContext-cjs.js');
20
- require('../_commonjsHelpers-cjs.js');
21
22
  require('../identity-cjs.js');
22
23
  require('../isTypedArray-cjs.js');
23
24
  require('../isObjectLike-cjs.js');
@@ -34,7 +35,6 @@ require('../Content-cjs.js');
34
35
  require('../ProgressBar-cjs.js');
35
36
  require('../ConfirmationModal-cjs.js');
36
37
  require('../Modal/index.cjs');
37
- require('../noop-cjs.js');
38
38
  require('../floating-ui.react-cjs.js');
39
39
  require('react/jsx-runtime');
40
40
  require('../AtlantisPortalContent-cjs.js');
@@ -6,6 +6,8 @@ import '../LightBox-es.js';
6
6
  import 'framer-motion';
7
7
  import 'react-dom';
8
8
  import '@jobber/hooks';
9
+ import '../noop-es.js';
10
+ import '../_commonjsHelpers-es.js';
9
11
  import '../ButtonDismiss-es.js';
10
12
  import '../Button-es.js';
11
13
  import 'react-router-dom';
@@ -15,7 +17,6 @@ import '../Typography-es.js';
15
17
  import '../Text-es.js';
16
18
  import '../Heading-es.js';
17
19
  import '../AtlantisThemeContext-es.js';
18
- import '../_commonjsHelpers-es.js';
19
20
  import '../identity-es.js';
20
21
  import '../isTypedArray-es.js';
21
22
  import '../isObjectLike-es.js';
@@ -32,7 +33,6 @@ import '../Content-es.js';
32
33
  import '../ProgressBar-es.js';
33
34
  import '../ConfirmationModal-es.js';
34
35
  import '../Modal/index.mjs';
35
- import '../noop-es.js';
36
36
  import '../floating-ui.react-es.js';
37
37
  import 'react/jsx-runtime';
38
38
  import '../AtlantisPortalContent-es.js';
@@ -0,0 +1,22 @@
1
+ import type { RefObject } from "react";
2
+ export declare const BUTTON_DEBOUNCE_DELAY = 250;
3
+ export declare const MOVEMENT_DEBOUNCE_DELAY = 1000;
4
+ export declare const swipeConfidenceThreshold = 10000;
5
+ export declare const swipePower: (offset: number, velocity: number) => number;
6
+ export declare const slideVariants: {
7
+ enter: (directionRef: RefObject<number>) => {
8
+ x: string;
9
+ };
10
+ center: {
11
+ x: number;
12
+ };
13
+ exit: (directionRef: RefObject<number>) => {
14
+ x: string;
15
+ };
16
+ };
17
+ export declare const imageTransition: {
18
+ x: {
19
+ duration: number;
20
+ ease: number[];
21
+ };
22
+ };
@@ -1,56 +1,120 @@
1
- import type { CSSProperties } from "react";
2
1
  import React from "react";
3
- interface PresentedImage {
4
- title?: string;
5
- caption?: string;
6
- alt?: string;
7
- url: string;
2
+ import type { LightBoxNavigationProps, LightBoxProps } from "./LightBox.types";
3
+ import { LightBoxProvider } from "./LightBoxContext";
4
+ export declare function LightBoxContent(): React.JSX.Element;
5
+ /**
6
+ * Blurred, desaturated copy of the current image rendered as a full-bleed
7
+ * background behind the lightbox. Pass `className` to apply additional styles.
8
+ */
9
+ declare function LightBoxBackground({ className }: {
10
+ readonly className?: string;
11
+ }): React.JSX.Element;
12
+ /**
13
+ * Semi-transparent blur backdrop. Clicking it calls `onRequestClose`.
14
+ * Pass `className` to apply additional styles.
15
+ */
16
+ declare function LightBoxOverlay({ className }: {
17
+ readonly className?: string;
18
+ }): React.JSX.Element;
19
+ /**
20
+ * Top bar showing the current image counter (`1/3`) and a close button.
21
+ * Styled for dark backgrounds.
22
+ */
23
+ declare function LightBoxToolbar(): React.JSX.Element;
24
+ /**
25
+ * The animated hero image with swipe-to-navigate and slide animation.
26
+ *
27
+ * Pass `className` to add styles to the image wrapper. Supports
28
+ * swipe-to-navigate (drag). Keyboard arrow navigation is handled by
29
+ * `LightBox.Provider`.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * <LightBox.Slides className={styles.imageArea} />
34
+ * <LightBox.Navigation
35
+ * prevButtonClassName={styles.prev}
36
+ * nextButtonClassName={styles.next}
37
+ * />
38
+ * ```
39
+ */
40
+ declare function LightBoxSlides({ className }: {
41
+ readonly className?: string;
42
+ }): React.JSX.Element;
43
+ /**
44
+ * Previous and next navigation buttons. Returns `null` when the image set
45
+ * has only one image.
46
+ *
47
+ * Use `prevButtonClassName` and `nextButtonClassName` to override the styles
48
+ * on each button's wrapper for custom layouts.
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * <LightBox.Navigation
53
+ * prevButtonClassName={styles.prev}
54
+ * nextButtonClassName={styles.next}
55
+ * />
56
+ * ```
57
+ */
58
+ declare function LightBoxNavigation({ prevButtonClassName, nextButtonClassName, }: LightBoxNavigationProps): React.JSX.Element | null;
59
+ /**
60
+ * Title and caption text for the current image. Only renders when the current
61
+ * image has a `title` or `caption`. Styled for dark backgrounds.
62
+ */
63
+ declare function LightBoxCaption(): React.JSX.Element | null;
64
+ /**
65
+ * Scrollable thumbnail strip. Only renders when there are two or more images.
66
+ */
67
+ declare function LightBoxThumbnails(): React.JSX.Element | null;
68
+ /**
69
+ * LightBox displays images in a fullscreen overlay.
70
+ *
71
+ * **Self-contained (legacy) usage:**
72
+ * ```tsx
73
+ * <LightBox
74
+ * open={isOpen}
75
+ * images={images}
76
+ * imageIndex={imageIndex}
77
+ * onRequestClose={({ lastPosition }) => { setIsOpen(false); }}
78
+ * />
79
+ * ```
80
+ *
81
+ * **Full composable (fullscreen) usage:**
82
+ * ```tsx
83
+ * <LightBox.Provider open={isOpen} images={images} onRequestClose={onClose}>
84
+ * <LightBox.Content />
85
+ * </LightBox.Provider>
86
+ * ```
87
+ *
88
+ * **Inline gallery usage (no overlay, no close):**
89
+ * ```tsx
90
+ * <LightBox.Provider
91
+ * open={true}
92
+ * images={images}
93
+ * imageIndex={activeIndex}
94
+ * onImageChange={onImageChange}
95
+ * >
96
+ * <div className={styles.lightboxWrapper} onMouseMove={handleMouseMove}>
97
+ * <LightBox.Background className={styles.backgroundImage} />
98
+ * <LightBox.Overlay className={styles.blurOverlay} />
99
+ * <LightBox.Slides className={styles.imageArea} />
100
+ * <LightBox.Navigation
101
+ * prevButtonClassName={styles.prev}
102
+ * nextButtonClassName={styles.next}
103
+ * />
104
+ * </div>
105
+ * </LightBox.Provider>
106
+ * ```
107
+ */
108
+ declare function LightBox(props: LightBoxProps): React.JSX.Element;
109
+ declare namespace LightBox {
110
+ var Provider: typeof LightBoxProvider;
111
+ var Content: typeof LightBoxContent;
112
+ var Background: typeof LightBoxBackground;
113
+ var Overlay: typeof LightBoxOverlay;
114
+ var Toolbar: typeof LightBoxToolbar;
115
+ var Slides: typeof LightBoxSlides;
116
+ var Navigation: typeof LightBoxNavigation;
117
+ var Caption: typeof LightBoxCaption;
118
+ var Thumbnails: typeof LightBoxThumbnails;
8
119
  }
9
- interface RequestCloseOptions {
10
- lastPosition: number;
11
- }
12
- interface LightBoxProps {
13
- /**
14
- * Specify if the Lightbox is open or closed.
15
- */
16
- readonly open: boolean;
17
- /**
18
- * Images is an array of objects defining a LightBox image. This object consists of
19
- * `title`, `caption`, `alt` and `url`. `title`, `alt` and `caption` are optional, `url` is
20
- * required, for each image.
21
- */
22
- readonly images: PresentedImage[];
23
- /**
24
- * Use this to specify which image in `images` to initialize the lightbox with.
25
- * This is useful when you have a collection of thumbnails as you only need one
26
- * collection of image urls, order doesn't matter.
27
- */
28
- readonly imageIndex?: number;
29
- /**
30
- * This function must set open to false in order to close the lightbox. Note there
31
- * is a 300ms easing animation on lightbox close that occurs before this function
32
- * is called.
33
- * This function receives an object as an argument with the key `lastPosition`
34
- * that has the index of the image the user was on when LightBox was closed.
35
- */
36
- onRequestClose(options: RequestCloseOptions): void;
37
- /**
38
- * Sets the box-sizing for the thumbnails in the lightbox. This is a solution for a problem where
39
- * tailwind was setting the box-sizing to `border-box` and causing issues with the lightbox.
40
- * @default "content-box"
41
- */
42
- readonly boxSizing?: CSSProperties["boxSizing"];
43
- }
44
- export declare const slideVariants: {
45
- enter: (directionRef: React.RefObject<number>) => {
46
- x: string;
47
- };
48
- center: {
49
- x: number;
50
- };
51
- exit: (directionRef: React.RefObject<number>) => {
52
- x: string;
53
- };
54
- };
55
- export declare function LightBox({ boxSizing, open, images, imageIndex, onRequestClose, }: LightBoxProps): React.JSX.Element;
56
- export {};
120
+ export { LightBox };
@@ -0,0 +1,94 @@
1
+ import type { CSSProperties, ReactNode, RefObject } from "react";
2
+ import type { PanInfo } from "framer-motion";
3
+ export interface PresentedImage {
4
+ title?: string;
5
+ caption?: string;
6
+ alt?: string;
7
+ url: string;
8
+ }
9
+ export interface RequestCloseOptions {
10
+ lastPosition: number;
11
+ }
12
+ export interface LightBoxContextType {
13
+ readonly open: boolean;
14
+ readonly images: PresentedImage[];
15
+ readonly currentImageIndex: number;
16
+ readonly mouseIsStationary: boolean;
17
+ readonly boxSizing: CSSProperties["boxSizing"];
18
+ readonly directionRef: RefObject<number>;
19
+ readonly selectedThumbnailRef: RefObject<HTMLDivElement | null>;
20
+ readonly lightboxRef: RefObject<HTMLDivElement | null>;
21
+ readonly mounted: RefObject<boolean>;
22
+ handleMouseMove(): void;
23
+ handleRequestClose(): void;
24
+ handleOnDragEnd(event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo): void;
25
+ handleThumbnailClick(index: number): void;
26
+ debouncedHandleNext(): void;
27
+ debouncedHandlePrevious(): void;
28
+ }
29
+ export interface NavButtonProps {
30
+ readonly onClick: () => void;
31
+ readonly hideButton: boolean;
32
+ readonly className: string;
33
+ }
34
+ export interface LightBoxProps {
35
+ /**
36
+ * Specify if the Lightbox is open or closed.
37
+ */
38
+ readonly open: boolean;
39
+ /**
40
+ * Images is an array of objects defining a LightBox image. This object consists of
41
+ * `title`, `caption`, `alt` and `url`. `title`, `alt` and `caption` are optional, `url` is
42
+ * required, for each image.
43
+ */
44
+ readonly images: PresentedImage[];
45
+ /**
46
+ * Use this to specify which image in `images` to initialize the lightbox with.
47
+ * This is useful when you have a collection of thumbnails as you only need one
48
+ * collection of image urls, order doesn't matter.
49
+ */
50
+ readonly imageIndex?: number;
51
+ /**
52
+ * This function must set open to false in order to close the lightbox. Note there
53
+ * is a 300ms easing animation on lightbox close that occurs before this function
54
+ * is called.
55
+ * This function receives an object as an argument with the key `lastPosition`
56
+ * that has the index of the image the user was on when LightBox was closed.
57
+ */
58
+ onRequestClose(options: RequestCloseOptions): void;
59
+ /**
60
+ * Sets the box-sizing for the thumbnails in the lightbox. This is a solution for a problem where
61
+ * tailwind was setting the box-sizing to `border-box` and causing issues with the lightbox.
62
+ * @default "content-box"
63
+ */
64
+ readonly boxSizing?: CSSProperties["boxSizing"];
65
+ }
66
+ export type LightBoxProviderProps = Omit<LightBoxProps, "onRequestClose"> & {
67
+ /**
68
+ * This function must set open to false in order to close the lightbox. Note there
69
+ * is a 300ms easing animation on lightbox close that occurs before this function
70
+ * is called.
71
+ * This function receives an object as an argument with the key `lastPosition`
72
+ * that has the index of the image the user was on when LightBox was closed.
73
+ */
74
+ onRequestClose?(options: RequestCloseOptions): void;
75
+ /**
76
+ * Callback function that is invoked whenever the current image index changes.
77
+ * This includes when the user navigates to a different image (via arrow keys,
78
+ * navigation buttons, or swipe gestures) or when clicking a thumbnail.
79
+ *
80
+ * @param index - The new current image index (0-based)
81
+ */
82
+ onImageChange?(index: number): void;
83
+ readonly children: ReactNode;
84
+ };
85
+ export interface LightBoxNavigationProps {
86
+ /**
87
+ * The class name to apply to the previous button wrapper.
88
+ */
89
+ readonly prevButtonClassName?: string;
90
+ /**
91
+ * The class name to apply to the next button wrapper.
92
+ */
93
+ readonly nextButtonClassName?: string;
94
+ }
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ import type { LightBoxContextType, LightBoxProviderProps } from "./LightBox.types";
3
+ export declare const LightBoxContext: React.Context<LightBoxContextType>;
4
+ export declare function LightBoxProvider({ open, images, imageIndex, onRequestClose, onImageChange, boxSizing, children, }: LightBoxProviderProps): React.JSX.Element;
5
+ export declare function useLightBoxContext(): LightBoxContextType;
@@ -6,6 +6,8 @@ require('framer-motion');
6
6
  require('react-dom');
7
7
  require('@jobber/hooks');
8
8
  require('classnames');
9
+ require('../noop-cjs.js');
10
+ require('../_commonjsHelpers-cjs.js');
9
11
  require('../ButtonDismiss-cjs.js');
10
12
  require('../Button-cjs.js');
11
13
  require('react-router-dom');
@@ -16,7 +18,6 @@ require('../Typography-cjs.js');
16
18
  require('../Text-cjs.js');
17
19
  require('../Heading-cjs.js');
18
20
  require('../AtlantisThemeContext-cjs.js');
19
- require('../_commonjsHelpers-cjs.js');
20
21
  require('../identity-cjs.js');
21
22
  require('../isTypedArray-cjs.js');
22
23
  require('../isObjectLike-cjs.js');
@@ -29,3 +30,4 @@ require('../_setToString-cjs.js');
29
30
 
30
31
 
31
32
  exports.LightBox = LightBox.LightBox;
33
+ exports.useLightBoxContext = LightBox.useLightBoxContext;
@@ -1 +1,3 @@
1
1
  export { LightBox } from "./LightBox";
2
+ export type { LightBoxProps, LightBoxProviderProps, PresentedImage, } from "./LightBox.types";
3
+ export { useLightBoxContext } from "./LightBoxContext";
@@ -1,9 +1,11 @@
1
- export { L as LightBox } from '../LightBox-es.js';
1
+ export { L as LightBox, u as useLightBoxContext } from '../LightBox-es.js';
2
2
  import 'react';
3
3
  import 'framer-motion';
4
4
  import 'react-dom';
5
5
  import '@jobber/hooks';
6
6
  import 'classnames';
7
+ import '../noop-es.js';
8
+ import '../_commonjsHelpers-es.js';
7
9
  import '../ButtonDismiss-es.js';
8
10
  import '../Button-es.js';
9
11
  import 'react-router-dom';
@@ -14,7 +16,6 @@ import '../Typography-es.js';
14
16
  import '../Text-es.js';
15
17
  import '../Heading-es.js';
16
18
  import '../AtlantisThemeContext-es.js';
17
- import '../_commonjsHelpers-es.js';
18
19
  import '../identity-es.js';
19
20
  import '../isTypedArray-es.js';
20
21
  import '../isObjectLike-es.js';
@@ -5,6 +5,7 @@ var framerMotion = require('framer-motion');
5
5
  var ReactDOM = require('react-dom');
6
6
  var jobberHooks = require('@jobber/hooks');
7
7
  var classnames = require('classnames');
8
+ var noop = require('./noop-cjs.js');
8
9
  var ButtonDismiss = require('./ButtonDismiss-cjs.js');
9
10
  var Text = require('./Text-cjs.js');
10
11
  var Button = require('./Button-cjs.js');
@@ -13,6 +14,11 @@ var AtlantisThemeContext = require('./AtlantisThemeContext-cjs.js');
13
14
 
14
15
  var styles = {"backgroundImage":"i9Tw1T65W-k-","next":"Q8amcRaTGf0-","prev":"W9FVb24yJrk-","buttonHidden":"nsN0TPWsBXI-","buttonVisible":"dkLYp7AD2jE-","lightboxWrapper":"_5p2iAj4JfoE-","toolbar":"rMK4cKdOxFw-","closeButton":"_0m6vb11DgiA-","slideNumber":"kCc68gGuTgg-","image":"yYFVVScosfQ-","imageArea":"UskuwLHR6fg-","captionWrapper":"OGjhge-r-U4-","title":"tZU2g-NYdIs-","blurOverlay":"GKIdLTmvcvQ-","thumbnailBar":"_3TfQLQEE3GQ-","thumbnailImage":"eBMzUOlcfQ4-","thumbnail":"eapm2zruLn8-","selected":"PeLn2u-QB0k-","spinning":"_8tDoqjgfLcw-"};
15
16
 
17
+ // A little bit more than the transition's duration
18
+ // We're doing this to prevent a bug from framer-motion
19
+ // https://github.com/framer/motion/issues/1769
20
+ const BUTTON_DEBOUNCE_DELAY = 250;
21
+ const MOVEMENT_DEBOUNCE_DELAY = 1000;
16
22
  const swipeConfidenceThreshold = 10000;
17
23
  const swipePower = (offset, velocity) => {
18
24
  return Math.abs(offset) * velocity;
@@ -31,12 +37,26 @@ const slideVariants = {
31
37
  const imageTransition = {
32
38
  x: { duration: 0.65, ease: [0.42, 0, 0, 1.03] },
33
39
  };
34
- // A little bit more than the transition's duration
35
- // We're doing this to prevent a bug from framer-motion
36
- // https://github.com/framer/motion/issues/1769
37
- const BUTTON_DEBOUNCE_DELAY = 250;
38
- const MOVEMENT_DEBOUNCE_DELAY = 1000;
39
- function LightBox({ boxSizing = "content-box", open, images, imageIndex = 0, onRequestClose, }) {
40
+
41
+ /* eslint-disable max-statements */
42
+ const LightBoxContext = React.createContext({
43
+ open: false,
44
+ images: [],
45
+ currentImageIndex: 0,
46
+ mouseIsStationary: true,
47
+ boxSizing: "content-box",
48
+ directionRef: { current: 0 },
49
+ selectedThumbnailRef: { current: null },
50
+ lightboxRef: { current: null },
51
+ mounted: { current: false },
52
+ handleMouseMove: noop.noop,
53
+ handleRequestClose: noop.noop,
54
+ handleOnDragEnd: noop.noop,
55
+ handleThumbnailClick: noop.noop,
56
+ debouncedHandleNext: noop.noop,
57
+ debouncedHandlePrevious: noop.noop,
58
+ });
59
+ function LightBoxProvider({ open = true, images, imageIndex = 0, onRequestClose = noop.noop, onImageChange = noop.noop, boxSizing = "content-box", children, }) {
40
60
  const [currentImageIndex, setCurrentImageIndex] = React.useState(imageIndex);
41
61
  const directionRef = React.useRef(0);
42
62
  const [mouseIsStationary, setMouseIsStationary] = React.useState(true);
@@ -59,6 +79,7 @@ function LightBox({ boxSizing = "content-box", open, images, imageIndex = 0, onR
59
79
  });
60
80
  React.useEffect(() => {
61
81
  setCurrentImageIndex(imageIndex);
82
+ onImageChange(imageIndex);
62
83
  }, [imageIndex, open]);
63
84
  if (prevOpen.current !== open) {
64
85
  prevOpen.current = open;
@@ -72,57 +93,28 @@ function LightBox({ boxSizing = "content-box", open, images, imageIndex = 0, onR
72
93
  inline: "center",
73
94
  });
74
95
  }, [currentImageIndex]);
75
- const template = (React.createElement(React.Fragment, null, open && (React.createElement("div", { className: styles.lightboxWrapper, tabIndex: 0, "aria-label": "Lightbox", key: "Lightbox", ref: lightboxRef, onMouseMove: () => {
76
- if (mouseIsStationary) {
77
- setMouseIsStationary(false);
78
- }
79
- handleMouseMovement();
80
- } },
81
- React.createElement("div", { className: styles.backgroundImage, style: {
82
- backgroundImage: `url("${images[currentImageIndex].url}")`,
83
- } }),
84
- React.createElement("div", { className: styles.blurOverlay, onClick: handleRequestClose }),
85
- React.createElement(AtlantisThemeContext.AtlantisThemeContextProvider, { dangerouslyOverrideTheme: "dark" },
86
- React.createElement("div", { className: styles.toolbar },
87
- React.createElement("div", { className: styles.slideNumber },
88
- React.createElement(Text.Text, null, `${currentImageIndex + 1}/${images.length}`)),
89
- React.createElement("div", { className: styles.closeButton },
90
- React.createElement(ButtonDismiss.ButtonDismiss, { ariaLabel: "Close", onClick: handleRequestClose })))),
91
- React.createElement("div", { className: styles.imageArea },
92
- React.createElement(framerMotion.AnimatePresence, { initial: false },
93
- React.createElement(framerMotion.motion.img, { key: currentImageIndex, variants: slideVariants, src: images[currentImageIndex].url, custom: directionRef, className: styles.image, initial: "enter", alt: images[currentImageIndex].alt ||
94
- images[currentImageIndex].title ||
95
- "", animate: "center", exit: "exit", transition: imageTransition, drag: "x", dragConstraints: { left: 0, right: 0 }, dragElastic: 1, onDragEnd: handleOnDragEnd }))),
96
- images.length > 1 && (React.createElement(React.Fragment, null,
97
- React.createElement(PreviousButton, { onClick: debouncedHandlePrevious, hideButton: mouseIsStationary }),
98
- React.createElement(NextButton, { onClick: debouncedHandleNext, hideButton: mouseIsStationary }))),
99
- (images[currentImageIndex].title ||
100
- images[currentImageIndex].caption) && (React.createElement("div", { className: styles.captionWrapper },
101
- React.createElement(AtlantisThemeContext.AtlantisThemeContextProvider, { dangerouslyOverrideTheme: "dark" },
102
- images[currentImageIndex].title && (React.createElement("div", { className: styles.title },
103
- React.createElement(Heading.Heading, { level: 4 }, images[currentImageIndex].title))),
104
- images[currentImageIndex].caption && (React.createElement(Text.Text, { size: "large" }, images[currentImageIndex].caption))))),
105
- images.length > 1 && (React.createElement("div", { className: styles.thumbnailBar, style: {
106
- "--lightbox--box-sizing": boxSizing,
107
- }, "data-testid": "ATL-Thumbnail-Bar" }, images.map((image, index) => (React.createElement("div", { key: index, className: classnames(styles.thumbnail, {
108
- [styles.selected]: index === currentImageIndex,
109
- }), onClick: () => handleThumbnailClick(index), ref: index === currentImageIndex ? selectedThumbnailRef : null },
110
- React.createElement("img", { key: index, src: image.url, alt: image.alt || image.title || "", className: styles.thumbnailImage }))))))))));
111
- return mounted.current
112
- ? ReactDOM.createPortal(template, document.body)
113
- : template;
96
+ function handleMouseMove() {
97
+ if (mouseIsStationary) {
98
+ setMouseIsStationary(false);
99
+ }
100
+ handleMouseMovement();
101
+ }
114
102
  function handleMovePrevious() {
115
103
  directionRef.current = -1;
116
- setCurrentImageIndex((currentImageIndex + images.length - 1) % images.length);
104
+ const newIndex = (currentImageIndex + images.length - 1) % images.length;
105
+ setCurrentImageIndex(newIndex);
106
+ onImageChange(newIndex);
117
107
  }
118
108
  function handleMoveNext() {
119
109
  directionRef.current = 1;
120
- setCurrentImageIndex((currentImageIndex + 1) % images.length);
110
+ const newIndex = (currentImageIndex + 1) % images.length;
111
+ setCurrentImageIndex(newIndex);
112
+ onImageChange(newIndex);
121
113
  }
122
114
  function handleRequestClose() {
123
115
  onRequestClose({ lastPosition: currentImageIndex });
124
116
  }
125
- function handleOnDragEnd(event, { offset, velocity }) {
117
+ function handleOnDragEnd(_event, { offset, velocity }) {
126
118
  const swipe = swipePower(offset.x, velocity.x);
127
119
  if (swipe < -swipeConfidenceThreshold) {
128
120
  handleMoveNext();
@@ -139,17 +131,28 @@ function LightBox({ boxSizing = "content-box", open, images, imageIndex = 0, onR
139
131
  directionRef.current = 1;
140
132
  }
141
133
  setCurrentImageIndex(index);
134
+ onImageChange(index);
142
135
  }
136
+ return (React.createElement(LightBoxContext.Provider, { value: {
137
+ open,
138
+ images,
139
+ currentImageIndex,
140
+ mouseIsStationary,
141
+ boxSizing,
142
+ directionRef,
143
+ selectedThumbnailRef,
144
+ lightboxRef,
145
+ mounted,
146
+ handleMouseMove,
147
+ handleRequestClose,
148
+ handleOnDragEnd,
149
+ handleThumbnailClick,
150
+ debouncedHandleNext,
151
+ debouncedHandlePrevious,
152
+ } }, children));
143
153
  }
144
- function PreviousButton({ onClick, hideButton }) {
145
- const { mediumAndUp } = jobberHooks.useBreakpoints();
146
- return (React.createElement("div", { className: `${styles.prev} ${hideButton ? styles.buttonHidden : styles.buttonVisible}` },
147
- React.createElement(Button.Button, { size: mediumAndUp ? "large" : "small", variation: "subtle", type: "secondary", icon: "arrowLeft", ariaLabel: "Previous image", onClick: onClick })));
148
- }
149
- function NextButton({ onClick, hideButton }) {
150
- const { mediumAndUp } = jobberHooks.useBreakpoints();
151
- return (React.createElement("div", { className: `${styles.next} ${hideButton ? styles.buttonHidden : styles.buttonVisible}` },
152
- React.createElement(Button.Button, { size: mediumAndUp ? "large" : "small", variation: "subtle", type: "secondary", icon: "arrowRight", ariaLabel: "Next image", onClick: onClick })));
154
+ function useLightBoxContext() {
155
+ return React.useContext(LightBoxContext);
153
156
  }
154
157
  function togglePrintStyles(open) {
155
158
  try {
@@ -165,4 +168,189 @@ function togglePrintStyles(open) {
165
168
  }
166
169
  }
167
170
 
171
+ function LightBoxContent() {
172
+ const { open, lightboxRef, handleMouseMove } = useLightBoxContext();
173
+ const mounted = jobberHooks.useIsMounted();
174
+ const template = (React.createElement(React.Fragment, null, open && (React.createElement("div", { className: styles.lightboxWrapper, tabIndex: 0, "aria-label": "Lightbox", key: "Lightbox", ref: lightboxRef, onMouseMove: handleMouseMove },
175
+ React.createElement(LightBoxBackground, null),
176
+ React.createElement(LightBoxOverlay, null),
177
+ React.createElement(LightBoxToolbar, null),
178
+ React.createElement(LightBoxSlides, null),
179
+ React.createElement(LightBoxNavigation, null),
180
+ React.createElement(LightBoxCaption, null),
181
+ React.createElement(LightBoxThumbnails, null)))));
182
+ return mounted.current
183
+ ? ReactDOM.createPortal(template, document.body)
184
+ : template;
185
+ }
186
+ function PreviousButton({ onClick, hideButton, className }) {
187
+ const { mediumAndUp } = jobberHooks.useBreakpoints();
188
+ return (React.createElement("div", { className: `${className} ${hideButton ? styles.buttonHidden : styles.buttonVisible}` },
189
+ React.createElement(Button.Button, { size: mediumAndUp ? "large" : "small", variation: "subtle", type: "secondary", icon: "arrowLeft", ariaLabel: "Previous image", onClick: onClick })));
190
+ }
191
+ function NextButton({ onClick, hideButton, className }) {
192
+ const { mediumAndUp } = jobberHooks.useBreakpoints();
193
+ return (React.createElement("div", { className: `${className} ${hideButton ? styles.buttonHidden : styles.buttonVisible}` },
194
+ React.createElement(Button.Button, { size: mediumAndUp ? "large" : "small", variation: "subtle", type: "secondary", icon: "arrowRight", ariaLabel: "Next image", onClick: onClick })));
195
+ }
196
+ /**
197
+ * Blurred, desaturated copy of the current image rendered as a full-bleed
198
+ * background behind the lightbox. Pass `className` to apply additional styles.
199
+ */
200
+ function LightBoxBackground({ className }) {
201
+ const { images, currentImageIndex } = useLightBoxContext();
202
+ return (React.createElement("div", { className: classnames(styles.backgroundImage, className), style: {
203
+ backgroundImage: `url("${images[currentImageIndex].url}")`,
204
+ } }));
205
+ }
206
+ /**
207
+ * Semi-transparent blur backdrop. Clicking it calls `onRequestClose`.
208
+ * Pass `className` to apply additional styles.
209
+ */
210
+ function LightBoxOverlay({ className }) {
211
+ const { handleRequestClose } = useLightBoxContext();
212
+ return (React.createElement("div", { className: classnames(styles.blurOverlay, className), onClick: handleRequestClose }));
213
+ }
214
+ /**
215
+ * Top bar showing the current image counter (`1/3`) and a close button.
216
+ * Styled for dark backgrounds.
217
+ */
218
+ function LightBoxToolbar() {
219
+ const { images, currentImageIndex, handleRequestClose } = useLightBoxContext();
220
+ return (React.createElement(AtlantisThemeContext.AtlantisThemeContextProvider, { dangerouslyOverrideTheme: "dark" },
221
+ React.createElement("div", { className: styles.toolbar },
222
+ React.createElement("div", { className: styles.slideNumber },
223
+ React.createElement(Text.Text, null, `${currentImageIndex + 1}/${images.length}`)),
224
+ React.createElement("div", { className: styles.closeButton },
225
+ React.createElement(ButtonDismiss.ButtonDismiss, { ariaLabel: "Close", onClick: handleRequestClose })))));
226
+ }
227
+ /**
228
+ * The animated hero image with swipe-to-navigate and slide animation.
229
+ *
230
+ * Pass `className` to add styles to the image wrapper. Supports
231
+ * swipe-to-navigate (drag). Keyboard arrow navigation is handled by
232
+ * `LightBox.Provider`.
233
+ *
234
+ * @example
235
+ * ```tsx
236
+ * <LightBox.Slides className={styles.imageArea} />
237
+ * <LightBox.Navigation
238
+ * prevButtonClassName={styles.prev}
239
+ * nextButtonClassName={styles.next}
240
+ * />
241
+ * ```
242
+ */
243
+ function LightBoxSlides({ className }) {
244
+ const { images, currentImageIndex, directionRef, handleOnDragEnd } = useLightBoxContext();
245
+ return (React.createElement("div", { className: classnames(styles.imageArea, className) },
246
+ React.createElement(framerMotion.AnimatePresence, { initial: false },
247
+ React.createElement(framerMotion.motion.img, { key: currentImageIndex, variants: slideVariants, src: images[currentImageIndex].url, custom: directionRef, className: styles.image, initial: "enter", alt: images[currentImageIndex].alt ||
248
+ images[currentImageIndex].title ||
249
+ "", animate: "center", exit: "exit", transition: imageTransition, drag: "x", dragConstraints: { left: 0, right: 0 }, dragElastic: 1, onDragEnd: handleOnDragEnd }))));
250
+ }
251
+ /**
252
+ * Previous and next navigation buttons. Returns `null` when the image set
253
+ * has only one image.
254
+ *
255
+ * Use `prevButtonClassName` and `nextButtonClassName` to override the styles
256
+ * on each button's wrapper for custom layouts.
257
+ *
258
+ * @example
259
+ * ```tsx
260
+ * <LightBox.Navigation
261
+ * prevButtonClassName={styles.prev}
262
+ * nextButtonClassName={styles.next}
263
+ * />
264
+ * ```
265
+ */
266
+ function LightBoxNavigation({ prevButtonClassName, nextButtonClassName, }) {
267
+ const { images, mouseIsStationary, debouncedHandleNext, debouncedHandlePrevious, } = useLightBoxContext();
268
+ if (images.length <= 1)
269
+ return null;
270
+ return (React.createElement(React.Fragment, null,
271
+ React.createElement(PreviousButton, { onClick: debouncedHandlePrevious, hideButton: mouseIsStationary, className: classnames(styles.prev, prevButtonClassName) }),
272
+ React.createElement(NextButton, { onClick: debouncedHandleNext, hideButton: mouseIsStationary, className: classnames(styles.next, nextButtonClassName) })));
273
+ }
274
+ /**
275
+ * Title and caption text for the current image. Only renders when the current
276
+ * image has a `title` or `caption`. Styled for dark backgrounds.
277
+ */
278
+ function LightBoxCaption() {
279
+ const { images, currentImageIndex } = useLightBoxContext();
280
+ const { title, caption } = images[currentImageIndex];
281
+ if (!title && !caption)
282
+ return null;
283
+ return (React.createElement("div", { className: styles.captionWrapper },
284
+ React.createElement(AtlantisThemeContext.AtlantisThemeContextProvider, { dangerouslyOverrideTheme: "dark" },
285
+ title && (React.createElement("div", { className: styles.title },
286
+ React.createElement(Heading.Heading, { level: 4 }, title))),
287
+ caption && React.createElement(Text.Text, { size: "large" }, caption))));
288
+ }
289
+ /**
290
+ * Scrollable thumbnail strip. Only renders when there are two or more images.
291
+ */
292
+ function LightBoxThumbnails() {
293
+ const { images, currentImageIndex, boxSizing, selectedThumbnailRef, handleThumbnailClick, } = useLightBoxContext();
294
+ if (images.length <= 1)
295
+ return null;
296
+ return (React.createElement("div", { className: styles.thumbnailBar, style: { "--lightbox--box-sizing": boxSizing }, "data-testid": "ATL-Thumbnail-Bar" }, images.map((image, index) => (React.createElement("div", { key: index, className: classnames(styles.thumbnail, {
297
+ [styles.selected]: index === currentImageIndex,
298
+ }), onClick: () => handleThumbnailClick(index), ref: index === currentImageIndex ? selectedThumbnailRef : null },
299
+ React.createElement("img", { key: index, src: image.url, alt: image.alt || image.title || "", className: styles.thumbnailImage }))))));
300
+ }
301
+ /**
302
+ * LightBox displays images in a fullscreen overlay.
303
+ *
304
+ * **Self-contained (legacy) usage:**
305
+ * ```tsx
306
+ * <LightBox
307
+ * open={isOpen}
308
+ * images={images}
309
+ * imageIndex={imageIndex}
310
+ * onRequestClose={({ lastPosition }) => { setIsOpen(false); }}
311
+ * />
312
+ * ```
313
+ *
314
+ * **Full composable (fullscreen) usage:**
315
+ * ```tsx
316
+ * <LightBox.Provider open={isOpen} images={images} onRequestClose={onClose}>
317
+ * <LightBox.Content />
318
+ * </LightBox.Provider>
319
+ * ```
320
+ *
321
+ * **Inline gallery usage (no overlay, no close):**
322
+ * ```tsx
323
+ * <LightBox.Provider
324
+ * open={true}
325
+ * images={images}
326
+ * imageIndex={activeIndex}
327
+ * onImageChange={onImageChange}
328
+ * >
329
+ * <div className={styles.lightboxWrapper} onMouseMove={handleMouseMove}>
330
+ * <LightBox.Background className={styles.backgroundImage} />
331
+ * <LightBox.Overlay className={styles.blurOverlay} />
332
+ * <LightBox.Slides className={styles.imageArea} />
333
+ * <LightBox.Navigation
334
+ * prevButtonClassName={styles.prev}
335
+ * nextButtonClassName={styles.next}
336
+ * />
337
+ * </div>
338
+ * </LightBox.Provider>
339
+ * ```
340
+ */
341
+ function LightBox(props) {
342
+ return (React.createElement(LightBoxProvider, Object.assign({}, props),
343
+ React.createElement(LightBoxContent, null)));
344
+ }
345
+ LightBox.Provider = LightBoxProvider;
346
+ LightBox.Content = LightBoxContent;
347
+ LightBox.Background = LightBoxBackground;
348
+ LightBox.Overlay = LightBoxOverlay;
349
+ LightBox.Toolbar = LightBoxToolbar;
350
+ LightBox.Slides = LightBoxSlides;
351
+ LightBox.Navigation = LightBoxNavigation;
352
+ LightBox.Caption = LightBoxCaption;
353
+ LightBox.Thumbnails = LightBoxThumbnails;
354
+
168
355
  exports.LightBox = LightBox;
356
+ exports.useLightBoxContext = useLightBoxContext;
@@ -1,8 +1,9 @@
1
- import React__default, { useState, useRef, useEffect } from 'react';
1
+ import React__default, { createContext, useContext, useState, useRef, useEffect } from 'react';
2
2
  import { AnimatePresence, motion } from 'framer-motion';
3
3
  import ReactDOM__default from 'react-dom';
4
4
  import { useFocusTrap, useDebounce, useIsMounted, useRefocusOnActivator, useOnKeyDown, useBreakpoints } from '@jobber/hooks';
5
5
  import classnames from 'classnames';
6
+ import { n as noop } from './noop-es.js';
6
7
  import { B as ButtonDismiss } from './ButtonDismiss-es.js';
7
8
  import { T as Text } from './Text-es.js';
8
9
  import { B as Button } from './Button-es.js';
@@ -11,6 +12,11 @@ import { A as AtlantisThemeContextProvider } from './AtlantisThemeContext-es.js'
11
12
 
12
13
  var styles = {"backgroundImage":"i9Tw1T65W-k-","next":"Q8amcRaTGf0-","prev":"W9FVb24yJrk-","buttonHidden":"nsN0TPWsBXI-","buttonVisible":"dkLYp7AD2jE-","lightboxWrapper":"_5p2iAj4JfoE-","toolbar":"rMK4cKdOxFw-","closeButton":"_0m6vb11DgiA-","slideNumber":"kCc68gGuTgg-","image":"yYFVVScosfQ-","imageArea":"UskuwLHR6fg-","captionWrapper":"OGjhge-r-U4-","title":"tZU2g-NYdIs-","blurOverlay":"GKIdLTmvcvQ-","thumbnailBar":"_3TfQLQEE3GQ-","thumbnailImage":"eBMzUOlcfQ4-","thumbnail":"eapm2zruLn8-","selected":"PeLn2u-QB0k-","spinning":"_8tDoqjgfLcw-"};
13
14
 
15
+ // A little bit more than the transition's duration
16
+ // We're doing this to prevent a bug from framer-motion
17
+ // https://github.com/framer/motion/issues/1769
18
+ const BUTTON_DEBOUNCE_DELAY = 250;
19
+ const MOVEMENT_DEBOUNCE_DELAY = 1000;
14
20
  const swipeConfidenceThreshold = 10000;
15
21
  const swipePower = (offset, velocity) => {
16
22
  return Math.abs(offset) * velocity;
@@ -29,12 +35,26 @@ const slideVariants = {
29
35
  const imageTransition = {
30
36
  x: { duration: 0.65, ease: [0.42, 0, 0, 1.03] },
31
37
  };
32
- // A little bit more than the transition's duration
33
- // We're doing this to prevent a bug from framer-motion
34
- // https://github.com/framer/motion/issues/1769
35
- const BUTTON_DEBOUNCE_DELAY = 250;
36
- const MOVEMENT_DEBOUNCE_DELAY = 1000;
37
- function LightBox({ boxSizing = "content-box", open, images, imageIndex = 0, onRequestClose, }) {
38
+
39
+ /* eslint-disable max-statements */
40
+ const LightBoxContext = createContext({
41
+ open: false,
42
+ images: [],
43
+ currentImageIndex: 0,
44
+ mouseIsStationary: true,
45
+ boxSizing: "content-box",
46
+ directionRef: { current: 0 },
47
+ selectedThumbnailRef: { current: null },
48
+ lightboxRef: { current: null },
49
+ mounted: { current: false },
50
+ handleMouseMove: noop,
51
+ handleRequestClose: noop,
52
+ handleOnDragEnd: noop,
53
+ handleThumbnailClick: noop,
54
+ debouncedHandleNext: noop,
55
+ debouncedHandlePrevious: noop,
56
+ });
57
+ function LightBoxProvider({ open = true, images, imageIndex = 0, onRequestClose = noop, onImageChange = noop, boxSizing = "content-box", children, }) {
38
58
  const [currentImageIndex, setCurrentImageIndex] = useState(imageIndex);
39
59
  const directionRef = useRef(0);
40
60
  const [mouseIsStationary, setMouseIsStationary] = useState(true);
@@ -57,6 +77,7 @@ function LightBox({ boxSizing = "content-box", open, images, imageIndex = 0, onR
57
77
  });
58
78
  useEffect(() => {
59
79
  setCurrentImageIndex(imageIndex);
80
+ onImageChange(imageIndex);
60
81
  }, [imageIndex, open]);
61
82
  if (prevOpen.current !== open) {
62
83
  prevOpen.current = open;
@@ -70,57 +91,28 @@ function LightBox({ boxSizing = "content-box", open, images, imageIndex = 0, onR
70
91
  inline: "center",
71
92
  });
72
93
  }, [currentImageIndex]);
73
- const template = (React__default.createElement(React__default.Fragment, null, open && (React__default.createElement("div", { className: styles.lightboxWrapper, tabIndex: 0, "aria-label": "Lightbox", key: "Lightbox", ref: lightboxRef, onMouseMove: () => {
74
- if (mouseIsStationary) {
75
- setMouseIsStationary(false);
76
- }
77
- handleMouseMovement();
78
- } },
79
- React__default.createElement("div", { className: styles.backgroundImage, style: {
80
- backgroundImage: `url("${images[currentImageIndex].url}")`,
81
- } }),
82
- React__default.createElement("div", { className: styles.blurOverlay, onClick: handleRequestClose }),
83
- React__default.createElement(AtlantisThemeContextProvider, { dangerouslyOverrideTheme: "dark" },
84
- React__default.createElement("div", { className: styles.toolbar },
85
- React__default.createElement("div", { className: styles.slideNumber },
86
- React__default.createElement(Text, null, `${currentImageIndex + 1}/${images.length}`)),
87
- React__default.createElement("div", { className: styles.closeButton },
88
- React__default.createElement(ButtonDismiss, { ariaLabel: "Close", onClick: handleRequestClose })))),
89
- React__default.createElement("div", { className: styles.imageArea },
90
- React__default.createElement(AnimatePresence, { initial: false },
91
- React__default.createElement(motion.img, { key: currentImageIndex, variants: slideVariants, src: images[currentImageIndex].url, custom: directionRef, className: styles.image, initial: "enter", alt: images[currentImageIndex].alt ||
92
- images[currentImageIndex].title ||
93
- "", animate: "center", exit: "exit", transition: imageTransition, drag: "x", dragConstraints: { left: 0, right: 0 }, dragElastic: 1, onDragEnd: handleOnDragEnd }))),
94
- images.length > 1 && (React__default.createElement(React__default.Fragment, null,
95
- React__default.createElement(PreviousButton, { onClick: debouncedHandlePrevious, hideButton: mouseIsStationary }),
96
- React__default.createElement(NextButton, { onClick: debouncedHandleNext, hideButton: mouseIsStationary }))),
97
- (images[currentImageIndex].title ||
98
- images[currentImageIndex].caption) && (React__default.createElement("div", { className: styles.captionWrapper },
99
- React__default.createElement(AtlantisThemeContextProvider, { dangerouslyOverrideTheme: "dark" },
100
- images[currentImageIndex].title && (React__default.createElement("div", { className: styles.title },
101
- React__default.createElement(Heading, { level: 4 }, images[currentImageIndex].title))),
102
- images[currentImageIndex].caption && (React__default.createElement(Text, { size: "large" }, images[currentImageIndex].caption))))),
103
- images.length > 1 && (React__default.createElement("div", { className: styles.thumbnailBar, style: {
104
- "--lightbox--box-sizing": boxSizing,
105
- }, "data-testid": "ATL-Thumbnail-Bar" }, images.map((image, index) => (React__default.createElement("div", { key: index, className: classnames(styles.thumbnail, {
106
- [styles.selected]: index === currentImageIndex,
107
- }), onClick: () => handleThumbnailClick(index), ref: index === currentImageIndex ? selectedThumbnailRef : null },
108
- React__default.createElement("img", { key: index, src: image.url, alt: image.alt || image.title || "", className: styles.thumbnailImage }))))))))));
109
- return mounted.current
110
- ? ReactDOM__default.createPortal(template, document.body)
111
- : template;
94
+ function handleMouseMove() {
95
+ if (mouseIsStationary) {
96
+ setMouseIsStationary(false);
97
+ }
98
+ handleMouseMovement();
99
+ }
112
100
  function handleMovePrevious() {
113
101
  directionRef.current = -1;
114
- setCurrentImageIndex((currentImageIndex + images.length - 1) % images.length);
102
+ const newIndex = (currentImageIndex + images.length - 1) % images.length;
103
+ setCurrentImageIndex(newIndex);
104
+ onImageChange(newIndex);
115
105
  }
116
106
  function handleMoveNext() {
117
107
  directionRef.current = 1;
118
- setCurrentImageIndex((currentImageIndex + 1) % images.length);
108
+ const newIndex = (currentImageIndex + 1) % images.length;
109
+ setCurrentImageIndex(newIndex);
110
+ onImageChange(newIndex);
119
111
  }
120
112
  function handleRequestClose() {
121
113
  onRequestClose({ lastPosition: currentImageIndex });
122
114
  }
123
- function handleOnDragEnd(event, { offset, velocity }) {
115
+ function handleOnDragEnd(_event, { offset, velocity }) {
124
116
  const swipe = swipePower(offset.x, velocity.x);
125
117
  if (swipe < -swipeConfidenceThreshold) {
126
118
  handleMoveNext();
@@ -137,17 +129,28 @@ function LightBox({ boxSizing = "content-box", open, images, imageIndex = 0, onR
137
129
  directionRef.current = 1;
138
130
  }
139
131
  setCurrentImageIndex(index);
132
+ onImageChange(index);
140
133
  }
134
+ return (React__default.createElement(LightBoxContext.Provider, { value: {
135
+ open,
136
+ images,
137
+ currentImageIndex,
138
+ mouseIsStationary,
139
+ boxSizing,
140
+ directionRef,
141
+ selectedThumbnailRef,
142
+ lightboxRef,
143
+ mounted,
144
+ handleMouseMove,
145
+ handleRequestClose,
146
+ handleOnDragEnd,
147
+ handleThumbnailClick,
148
+ debouncedHandleNext,
149
+ debouncedHandlePrevious,
150
+ } }, children));
141
151
  }
142
- function PreviousButton({ onClick, hideButton }) {
143
- const { mediumAndUp } = useBreakpoints();
144
- return (React__default.createElement("div", { className: `${styles.prev} ${hideButton ? styles.buttonHidden : styles.buttonVisible}` },
145
- React__default.createElement(Button, { size: mediumAndUp ? "large" : "small", variation: "subtle", type: "secondary", icon: "arrowLeft", ariaLabel: "Previous image", onClick: onClick })));
146
- }
147
- function NextButton({ onClick, hideButton }) {
148
- const { mediumAndUp } = useBreakpoints();
149
- return (React__default.createElement("div", { className: `${styles.next} ${hideButton ? styles.buttonHidden : styles.buttonVisible}` },
150
- React__default.createElement(Button, { size: mediumAndUp ? "large" : "small", variation: "subtle", type: "secondary", icon: "arrowRight", ariaLabel: "Next image", onClick: onClick })));
152
+ function useLightBoxContext() {
153
+ return useContext(LightBoxContext);
151
154
  }
152
155
  function togglePrintStyles(open) {
153
156
  try {
@@ -163,4 +166,188 @@ function togglePrintStyles(open) {
163
166
  }
164
167
  }
165
168
 
166
- export { LightBox as L };
169
+ function LightBoxContent() {
170
+ const { open, lightboxRef, handleMouseMove } = useLightBoxContext();
171
+ const mounted = useIsMounted();
172
+ const template = (React__default.createElement(React__default.Fragment, null, open && (React__default.createElement("div", { className: styles.lightboxWrapper, tabIndex: 0, "aria-label": "Lightbox", key: "Lightbox", ref: lightboxRef, onMouseMove: handleMouseMove },
173
+ React__default.createElement(LightBoxBackground, null),
174
+ React__default.createElement(LightBoxOverlay, null),
175
+ React__default.createElement(LightBoxToolbar, null),
176
+ React__default.createElement(LightBoxSlides, null),
177
+ React__default.createElement(LightBoxNavigation, null),
178
+ React__default.createElement(LightBoxCaption, null),
179
+ React__default.createElement(LightBoxThumbnails, null)))));
180
+ return mounted.current
181
+ ? ReactDOM__default.createPortal(template, document.body)
182
+ : template;
183
+ }
184
+ function PreviousButton({ onClick, hideButton, className }) {
185
+ const { mediumAndUp } = useBreakpoints();
186
+ return (React__default.createElement("div", { className: `${className} ${hideButton ? styles.buttonHidden : styles.buttonVisible}` },
187
+ React__default.createElement(Button, { size: mediumAndUp ? "large" : "small", variation: "subtle", type: "secondary", icon: "arrowLeft", ariaLabel: "Previous image", onClick: onClick })));
188
+ }
189
+ function NextButton({ onClick, hideButton, className }) {
190
+ const { mediumAndUp } = useBreakpoints();
191
+ return (React__default.createElement("div", { className: `${className} ${hideButton ? styles.buttonHidden : styles.buttonVisible}` },
192
+ React__default.createElement(Button, { size: mediumAndUp ? "large" : "small", variation: "subtle", type: "secondary", icon: "arrowRight", ariaLabel: "Next image", onClick: onClick })));
193
+ }
194
+ /**
195
+ * Blurred, desaturated copy of the current image rendered as a full-bleed
196
+ * background behind the lightbox. Pass `className` to apply additional styles.
197
+ */
198
+ function LightBoxBackground({ className }) {
199
+ const { images, currentImageIndex } = useLightBoxContext();
200
+ return (React__default.createElement("div", { className: classnames(styles.backgroundImage, className), style: {
201
+ backgroundImage: `url("${images[currentImageIndex].url}")`,
202
+ } }));
203
+ }
204
+ /**
205
+ * Semi-transparent blur backdrop. Clicking it calls `onRequestClose`.
206
+ * Pass `className` to apply additional styles.
207
+ */
208
+ function LightBoxOverlay({ className }) {
209
+ const { handleRequestClose } = useLightBoxContext();
210
+ return (React__default.createElement("div", { className: classnames(styles.blurOverlay, className), onClick: handleRequestClose }));
211
+ }
212
+ /**
213
+ * Top bar showing the current image counter (`1/3`) and a close button.
214
+ * Styled for dark backgrounds.
215
+ */
216
+ function LightBoxToolbar() {
217
+ const { images, currentImageIndex, handleRequestClose } = useLightBoxContext();
218
+ return (React__default.createElement(AtlantisThemeContextProvider, { dangerouslyOverrideTheme: "dark" },
219
+ React__default.createElement("div", { className: styles.toolbar },
220
+ React__default.createElement("div", { className: styles.slideNumber },
221
+ React__default.createElement(Text, null, `${currentImageIndex + 1}/${images.length}`)),
222
+ React__default.createElement("div", { className: styles.closeButton },
223
+ React__default.createElement(ButtonDismiss, { ariaLabel: "Close", onClick: handleRequestClose })))));
224
+ }
225
+ /**
226
+ * The animated hero image with swipe-to-navigate and slide animation.
227
+ *
228
+ * Pass `className` to add styles to the image wrapper. Supports
229
+ * swipe-to-navigate (drag). Keyboard arrow navigation is handled by
230
+ * `LightBox.Provider`.
231
+ *
232
+ * @example
233
+ * ```tsx
234
+ * <LightBox.Slides className={styles.imageArea} />
235
+ * <LightBox.Navigation
236
+ * prevButtonClassName={styles.prev}
237
+ * nextButtonClassName={styles.next}
238
+ * />
239
+ * ```
240
+ */
241
+ function LightBoxSlides({ className }) {
242
+ const { images, currentImageIndex, directionRef, handleOnDragEnd } = useLightBoxContext();
243
+ return (React__default.createElement("div", { className: classnames(styles.imageArea, className) },
244
+ React__default.createElement(AnimatePresence, { initial: false },
245
+ React__default.createElement(motion.img, { key: currentImageIndex, variants: slideVariants, src: images[currentImageIndex].url, custom: directionRef, className: styles.image, initial: "enter", alt: images[currentImageIndex].alt ||
246
+ images[currentImageIndex].title ||
247
+ "", animate: "center", exit: "exit", transition: imageTransition, drag: "x", dragConstraints: { left: 0, right: 0 }, dragElastic: 1, onDragEnd: handleOnDragEnd }))));
248
+ }
249
+ /**
250
+ * Previous and next navigation buttons. Returns `null` when the image set
251
+ * has only one image.
252
+ *
253
+ * Use `prevButtonClassName` and `nextButtonClassName` to override the styles
254
+ * on each button's wrapper for custom layouts.
255
+ *
256
+ * @example
257
+ * ```tsx
258
+ * <LightBox.Navigation
259
+ * prevButtonClassName={styles.prev}
260
+ * nextButtonClassName={styles.next}
261
+ * />
262
+ * ```
263
+ */
264
+ function LightBoxNavigation({ prevButtonClassName, nextButtonClassName, }) {
265
+ const { images, mouseIsStationary, debouncedHandleNext, debouncedHandlePrevious, } = useLightBoxContext();
266
+ if (images.length <= 1)
267
+ return null;
268
+ return (React__default.createElement(React__default.Fragment, null,
269
+ React__default.createElement(PreviousButton, { onClick: debouncedHandlePrevious, hideButton: mouseIsStationary, className: classnames(styles.prev, prevButtonClassName) }),
270
+ React__default.createElement(NextButton, { onClick: debouncedHandleNext, hideButton: mouseIsStationary, className: classnames(styles.next, nextButtonClassName) })));
271
+ }
272
+ /**
273
+ * Title and caption text for the current image. Only renders when the current
274
+ * image has a `title` or `caption`. Styled for dark backgrounds.
275
+ */
276
+ function LightBoxCaption() {
277
+ const { images, currentImageIndex } = useLightBoxContext();
278
+ const { title, caption } = images[currentImageIndex];
279
+ if (!title && !caption)
280
+ return null;
281
+ return (React__default.createElement("div", { className: styles.captionWrapper },
282
+ React__default.createElement(AtlantisThemeContextProvider, { dangerouslyOverrideTheme: "dark" },
283
+ title && (React__default.createElement("div", { className: styles.title },
284
+ React__default.createElement(Heading, { level: 4 }, title))),
285
+ caption && React__default.createElement(Text, { size: "large" }, caption))));
286
+ }
287
+ /**
288
+ * Scrollable thumbnail strip. Only renders when there are two or more images.
289
+ */
290
+ function LightBoxThumbnails() {
291
+ const { images, currentImageIndex, boxSizing, selectedThumbnailRef, handleThumbnailClick, } = useLightBoxContext();
292
+ if (images.length <= 1)
293
+ return null;
294
+ return (React__default.createElement("div", { className: styles.thumbnailBar, style: { "--lightbox--box-sizing": boxSizing }, "data-testid": "ATL-Thumbnail-Bar" }, images.map((image, index) => (React__default.createElement("div", { key: index, className: classnames(styles.thumbnail, {
295
+ [styles.selected]: index === currentImageIndex,
296
+ }), onClick: () => handleThumbnailClick(index), ref: index === currentImageIndex ? selectedThumbnailRef : null },
297
+ React__default.createElement("img", { key: index, src: image.url, alt: image.alt || image.title || "", className: styles.thumbnailImage }))))));
298
+ }
299
+ /**
300
+ * LightBox displays images in a fullscreen overlay.
301
+ *
302
+ * **Self-contained (legacy) usage:**
303
+ * ```tsx
304
+ * <LightBox
305
+ * open={isOpen}
306
+ * images={images}
307
+ * imageIndex={imageIndex}
308
+ * onRequestClose={({ lastPosition }) => { setIsOpen(false); }}
309
+ * />
310
+ * ```
311
+ *
312
+ * **Full composable (fullscreen) usage:**
313
+ * ```tsx
314
+ * <LightBox.Provider open={isOpen} images={images} onRequestClose={onClose}>
315
+ * <LightBox.Content />
316
+ * </LightBox.Provider>
317
+ * ```
318
+ *
319
+ * **Inline gallery usage (no overlay, no close):**
320
+ * ```tsx
321
+ * <LightBox.Provider
322
+ * open={true}
323
+ * images={images}
324
+ * imageIndex={activeIndex}
325
+ * onImageChange={onImageChange}
326
+ * >
327
+ * <div className={styles.lightboxWrapper} onMouseMove={handleMouseMove}>
328
+ * <LightBox.Background className={styles.backgroundImage} />
329
+ * <LightBox.Overlay className={styles.blurOverlay} />
330
+ * <LightBox.Slides className={styles.imageArea} />
331
+ * <LightBox.Navigation
332
+ * prevButtonClassName={styles.prev}
333
+ * nextButtonClassName={styles.next}
334
+ * />
335
+ * </div>
336
+ * </LightBox.Provider>
337
+ * ```
338
+ */
339
+ function LightBox(props) {
340
+ return (React__default.createElement(LightBoxProvider, Object.assign({}, props),
341
+ React__default.createElement(LightBoxContent, null)));
342
+ }
343
+ LightBox.Provider = LightBoxProvider;
344
+ LightBox.Content = LightBoxContent;
345
+ LightBox.Background = LightBoxBackground;
346
+ LightBox.Overlay = LightBoxOverlay;
347
+ LightBox.Toolbar = LightBoxToolbar;
348
+ LightBox.Slides = LightBoxSlides;
349
+ LightBox.Navigation = LightBoxNavigation;
350
+ LightBox.Caption = LightBoxCaption;
351
+ LightBox.Thumbnails = LightBoxThumbnails;
352
+
353
+ export { LightBox as L, useLightBoxContext as u };
package/dist/index.cjs CHANGED
@@ -293,6 +293,7 @@ exports.InputText = InputText_index.InputText;
293
293
  exports.InputTime = InputTime_index.InputTime;
294
294
  exports.InputValidation = InputValidation.InputValidation;
295
295
  exports.LightBox = LightBox.LightBox;
296
+ exports.useLightBoxContext = LightBox.useLightBoxContext;
296
297
  exports.Link = Link.Link;
297
298
  exports.List = List.List;
298
299
  exports.ListItem = List.ListItem;
package/dist/index.mjs CHANGED
@@ -63,7 +63,7 @@ export { InputPhoneNumber } from './InputPhoneNumber/index.mjs';
63
63
  export { InputText } from './InputText/index.mjs';
64
64
  export { InputTime } from './InputTime/index.mjs';
65
65
  export { I as InputValidation } from './InputValidation-es.js';
66
- export { L as LightBox } from './LightBox-es.js';
66
+ export { L as LightBox, u as useLightBoxContext } from './LightBox-es.js';
67
67
  export { L as Link } from './Link-es.js';
68
68
  export { L as List, a as ListItem } from './List-es.js';
69
69
  export { M as Markdown } from './Markdown-es.js';
@@ -135,6 +135,15 @@
135
135
  "InputTime",
136
136
  "InputValidation",
137
137
  "LightBox",
138
+ "LightBox.Background",
139
+ "LightBox.Caption",
140
+ "LightBox.Content",
141
+ "LightBox.Navigation",
142
+ "LightBox.Overlay",
143
+ "LightBox.Provider",
144
+ "LightBox.Slides",
145
+ "LightBox.Thumbnails",
146
+ "LightBox.Toolbar",
138
147
  "Link",
139
148
  "List",
140
149
  "ListItem",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components",
3
- "version": "6.113.1",
3
+ "version": "6.114.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -538,5 +538,5 @@
538
538
  "> 1%",
539
539
  "IE 10"
540
540
  ],
541
- "gitHead": "45e47debd0c3f7e4f9c85be0c3a811803096cd26"
541
+ "gitHead": "fa776129b22e49b376813fafbbfdfe3d48ea88e4"
542
542
  }