yet-another-react-lightbox 1.3.5 → 1.4.1

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/README.md CHANGED
@@ -15,7 +15,7 @@ Modern React lightbox component. Performant, easy to use, customizable and exten
15
15
  - **Responsive:** responsive images with automatic resolution switching are supported out of the box
16
16
  - **Video:** video slides are supported via an optional plugin
17
17
  - **Customization:** customize any UI element or add your own custom slides
18
- - **No bloat:** never bundle rarely used features; add optional features via plugins
18
+ - **No bloat:** never bundle rarely used features; add optional features via plugins
19
19
  - **TypeScript:** type definitions come built-in in the package
20
20
 
21
21
  ## Documentation
@@ -40,7 +40,7 @@ yarn add yet-another-react-lightbox
40
40
 
41
41
  ## Minimal Setup Example
42
42
 
43
- ```javascript
43
+ ```jsx
44
44
  import * as React from "react";
45
45
  import Lightbox from "yet-another-react-lightbox";
46
46
  import "yet-another-react-lightbox/styles.css";
@@ -58,8 +58,8 @@ const App = () => {
58
58
  open={open}
59
59
  close={() => setOpen(false)}
60
60
  slides={[
61
- { src: "/image1.jpg" },
62
- { src: "/image2.jpg" },
61
+ { src: "/image1.jpg" },
62
+ { src: "/image2.jpg" },
63
63
  { src: "/image3.jpg" },
64
64
  ]}
65
65
  />
@@ -70,6 +70,68 @@ const App = () => {
70
70
  export default App;
71
71
  ```
72
72
 
73
+ ## Recommended Setup
74
+
75
+ Unlike many other lightbox libraries, Yet Another React Lightbox doesn't have a concept of "thumbnail" or "original"
76
+ (or "full size") images. We use responsive images instead and recommend you provide multiple files of different
77
+ resolutions for each image. Yet Another React Lightbox automatically populates `srcSet` / `sizes` attributes and lets
78
+ the browser decide which image is more appropriate for its viewport size.
79
+
80
+ ```jsx
81
+ import * as React from "react";
82
+ import Lightbox from "yet-another-react-lightbox";
83
+ import "yet-another-react-lightbox/styles.css";
84
+
85
+ const App = () => {
86
+ const [open, setOpen] = React.useState(false);
87
+
88
+ return (
89
+ <>
90
+ <button type="button" onClick={() => setOpen(true)}>
91
+ Open Lightbox
92
+ </button>
93
+
94
+ <Lightbox
95
+ open={open}
96
+ close={() => setOpen(false)}
97
+ slides={[
98
+ {
99
+ src: "/image1x3840.jpg",
100
+ alt: "image 1",
101
+ aspectRatio: 3 / 2,
102
+ srcSet: [
103
+ { src: "/image1x320.jpg", width: 320 },
104
+ { src: "/image1x640.jpg", width: 640 },
105
+ { src: "/image1x1200.jpg", width: 1200 },
106
+ { src: "/image1x2048.jpg", width: 2048 },
107
+ { src: "/image1x3840.jpg", width: 3840 },
108
+ ]
109
+ },
110
+ // ...
111
+ ]}
112
+ />
113
+ </>
114
+ );
115
+ };
116
+
117
+ export default App;
118
+ ```
119
+
120
+ You can also integrate 3rd-party image components (e.g., Next.js Image or Gatsby Image) via a custom render function.
121
+ See [examples](https://yet-another-react-lightbox.vercel.app/examples) on the documentation website.
122
+
123
+ ## Plugins
124
+
125
+ Yet Another React Lightbox allows you to add optional features based on your requirements via plugins.
126
+
127
+ The following plugins come bundled in the package:
128
+
129
+ - [Captions](https://yet-another-react-lightbox.vercel.app/plugins/captions) - adds support for slide title and
130
+ description
131
+ - [Fullscreen](https://yet-another-react-lightbox.vercel.app/plugins/fullscreen) - adds support for fullscreen mode
132
+ - [Inline](https://yet-another-react-lightbox.vercel.app/plugins/inline) - adds support for inline rendering mode
133
+ - [Video](https://yet-another-react-lightbox.vercel.app/plugins/video) - adds support for video slides
134
+
73
135
  ## License
74
136
 
75
- MIT © [Igor Danchenko](https://github.com/igordanchenko)
137
+ MIT © 2022 [Igor Danchenko](https://github.com/igordanchenko)
package/dist/Lightbox.js CHANGED
@@ -24,7 +24,6 @@ const LightboxComponent = (props) => {
24
24
  return React.createElement(React.Fragment, null, renderNode(createNode(CoreModule, config), augmentedProps));
25
25
  };
26
26
  LightboxComponent.propTypes = LightboxPropTypes;
27
- /** Modern React lightbox component */
28
27
  export const Lightbox = (props) => {
29
28
  const { carousel, animation, render, toolbar, controller, on, ...restProps } = props;
30
29
  const { carousel: defaultCarousel, animation: defaultAnimation, render: defaultRender, toolbar: defaultToolbar, controller: defaultController, on: defaultOn, ...restDefaultProps } = LightboxDefaultProps;
@@ -10,7 +10,5 @@ export const createIcon = (name, glyph) => {
10
10
  export const CloseIcon = createIcon("Close", React.createElement("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }));
11
11
  export const PreviousIcon = createIcon("Previous", React.createElement("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }));
12
12
  export const NextIcon = createIcon("Next", React.createElement("path", { d: "M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" }));
13
- export const LoadingIcon = createIcon("Loading", React.createElement(React.Fragment, null, Array.from({ length: 8 }).map((_, index, array) => (React.createElement("line", {
14
- // eslint-disable-next-line react/no-array-index-key
15
- key: index, x1: "12", y1: "6.5", x2: "12", y2: "1.8", strokeLinecap: "round", strokeWidth: "2.6", stroke: "currentColor", strokeOpacity: (1 / array.length) * (index + 1), transform: `rotate(${(360 / array.length) * index}, 12, 12)` })))));
13
+ export const LoadingIcon = createIcon("Loading", React.createElement(React.Fragment, null, Array.from({ length: 8 }).map((_, index, array) => (React.createElement("line", { key: index, x1: "12", y1: "6.5", x2: "12", y2: "1.8", strokeLinecap: "round", strokeWidth: "2.6", stroke: "currentColor", strokeOpacity: (1 / array.length) * (index + 1), transform: `rotate(${(360 / array.length) * index}, 12, 12)` })))));
16
14
  export const ErrorIcon = createIcon("Error", React.createElement("path", { d: "M21.9,21.9l-8.49-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1,0.9,2,2,2h13.17l2.31,2.31L21.9,21.9z M5,18 l3.5-4.5l2.5,3.01L12.17,15l3,3H5z M21,18.17L5.83,3H19c1.1,0,2,0.9,2,2V18.17z" }));
@@ -1,7 +1,9 @@
1
1
  /// <reference types="react" />
2
2
  import { Render, SlideImage } from "../../types.js";
3
+ import { ContainerRect } from "../hooks/index.js";
3
4
  export declare type ImageSlideProps = {
4
5
  slide: SlideImage;
5
- render: Render;
6
+ render?: Render;
7
+ rect?: ContainerRect;
6
8
  };
7
- export declare const ImageSlide: ({ slide: image, render }: ImageSlideProps) => JSX.Element;
9
+ export declare const ImageSlide: ({ slide: image, render, rect }: ImageSlideProps) => JSX.Element;
@@ -1,14 +1,12 @@
1
1
  import * as React from "react";
2
- import { clsx, cssClass } from "../utils.js";
2
+ import { adjustDevicePixelRatio, clsx, cssClass, hasWindow } from "../utils.js";
3
3
  import { useLatest } from "../hooks/index.js";
4
4
  import { ErrorIcon, LoadingIcon } from "./Icons.js";
5
- import { useController } from "../modules/Controller.js";
6
- export const ImageSlide = ({ slide: image, render }) => {
7
- var _a, _b, _c;
5
+ export const ImageSlide = ({ slide: image, render, rect }) => {
6
+ var _a;
8
7
  const [state, setState] = React.useState("loading");
9
8
  const latestState = useLatest(state);
10
9
  const imageRef = React.useRef(null);
11
- const { containerRect } = useController();
12
10
  const handleLoading = React.useCallback((img) => {
13
11
  if (latestState.current === "complete") {
14
12
  return;
@@ -37,29 +35,31 @@ export const ImageSlide = ({ slide: image, render }) => {
37
35
  return (React.createElement(React.Fragment, null,
38
36
  React.createElement("img", { ref: setImageRef, onLoad: onLoad, onError: onError, className: clsx(cssClass("slide_image"), state !== "complete" && cssClass("slide_image_loading")), draggable: false, alt: image.alt, ...(image.srcSet
39
37
  ? {
40
- // this approach does not account for carousel padding,
41
- // but the margin of error should be negligible in most cases
42
- sizes: `${Math.ceil((Math.min(image.aspectRatio ? containerRect.height * image.aspectRatio : Number.MAX_VALUE, containerRect.width) /
43
- window.innerWidth) *
44
- 100)}vw`,
38
+ ...(rect && hasWindow()
39
+ ? {
40
+ sizes: `${Math.ceil((Math.min(image.aspectRatio ? rect.height * image.aspectRatio : Number.MAX_VALUE, rect.width) /
41
+ window.innerWidth) *
42
+ 100)}vw`,
43
+ }
44
+ : null),
45
45
  srcSet: image.srcSet
46
46
  .sort((a, b) => a.width - b.width)
47
47
  .map((item) => `${item.src} ${item.width}w`)
48
48
  .join(", "),
49
49
  style: {
50
- maxWidth: `${Math.max(...image.srcSet.map((x) => x.width))}px`,
50
+ maxWidth: `${adjustDevicePixelRatio(Math.max(...image.srcSet.map((x) => x.width)))}px`,
51
51
  },
52
52
  }
53
53
  : {
54
- style: ((_b = (_a = imageRef.current) === null || _a === void 0 ? void 0 : _a.naturalWidth) !== null && _b !== void 0 ? _b : 0) > 0
54
+ style: imageRef.current && ((_a = imageRef.current) === null || _a === void 0 ? void 0 : _a.naturalWidth) > 0
55
55
  ? {
56
- maxWidth: `${(_c = imageRef.current) === null || _c === void 0 ? void 0 : _c.naturalWidth}px`,
56
+ maxWidth: `${adjustDevicePixelRatio(imageRef.current.naturalWidth)}px`,
57
57
  }
58
58
  : undefined,
59
59
  }), src: image.src }),
60
60
  state !== "complete" && (React.createElement("div", { className: cssClass("slide_placeholder") },
61
61
  state === "loading" &&
62
- (render.iconLoading ? (render.iconLoading()) : (React.createElement(LoadingIcon, { className: clsx(cssClass("icon"), cssClass("slide_loading")) }))),
62
+ ((render === null || render === void 0 ? void 0 : render.iconLoading) ? (render.iconLoading()) : (React.createElement(LoadingIcon, { className: clsx(cssClass("icon"), cssClass("slide_loading")) }))),
63
63
  state === "error" &&
64
- (render.iconError ? (render.iconError()) : (React.createElement(ErrorIcon, { className: clsx(cssClass("icon"), cssClass("slide_error")) })))))));
64
+ ((render === null || render === void 0 ? void 0 : render.iconError) ? (render.iconError()) : (React.createElement(ErrorIcon, { className: clsx(cssClass("icon"), cssClass("slide_error")) })))))));
65
65
  };
@@ -10,14 +10,17 @@ export const useContainerRect = () => {
10
10
  observerRef.current = undefined;
11
11
  }
12
12
  const updateContainerRect = () => {
13
- const width = node === null || node === void 0 ? void 0 : node.clientWidth;
14
- const height = node === null || node === void 0 ? void 0 : node.clientHeight;
15
- setContainerRect(width !== undefined && height !== undefined
16
- ? {
17
- width,
18
- height,
19
- }
20
- : undefined);
13
+ if (node) {
14
+ const styles = window.getComputedStyle(node);
15
+ const parse = (value) => parseFloat(value) || 0;
16
+ setContainerRect({
17
+ width: Math.round(node.clientWidth - parse(styles.paddingLeft) - parse(styles.paddingRight)),
18
+ height: Math.round(node.clientHeight - parse(styles.paddingTop) - parse(styles.paddingBottom)),
19
+ });
20
+ }
21
+ else {
22
+ setContainerRect(undefined);
23
+ }
21
24
  };
22
25
  updateContainerRect();
23
26
  if (node && typeof ResizeObserver !== "undefined") {
@@ -1,2 +1,3 @@
1
1
  import * as React from "react";
2
- export const useEnhancedEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
2
+ import { hasWindow } from "../utils.js";
3
+ export const useEnhancedEffect = hasWindow() ? React.useLayoutEffect : React.useEffect;
@@ -1,35 +1,40 @@
1
1
  import * as React from "react";
2
2
  import { LightboxDefaultProps } from "../../types.js";
3
+ import { useContainerRect } from "../hooks/index.js";
3
4
  import { createModule } from "../config.js";
4
5
  import { clsx, cssClass, cssVar } from "../utils.js";
5
6
  import { ImageSlide } from "../components/index.js";
6
7
  import { useController } from "./Controller.js";
7
- const CarouselSlide = ({ slide, offset, render }) => {
8
- const renderSlide = () => {
9
- if (render.slide) {
10
- const rendered = render.slide(slide);
11
- if (rendered) {
12
- return rendered;
13
- }
8
+ const CarouselSlide = ({ slide, offset }) => {
9
+ const { setContainerRef, containerRect } = useContainerRect();
10
+ const { latestProps } = useController();
11
+ const { render } = latestProps.current;
12
+ const renderSlide = (rect) => {
13
+ var _a, _b, _c, _d;
14
+ let rendered = (_a = render.slide) === null || _a === void 0 ? void 0 : _a.call(render, slide, offset, rect);
15
+ if (!rendered && "src" in slide) {
16
+ rendered = React.createElement(ImageSlide, { slide: slide, render: render, rect: rect });
14
17
  }
15
- return "src" in slide ? React.createElement(ImageSlide, { slide: slide, render: render }) : null;
18
+ return rendered ? (React.createElement(React.Fragment, null, (_b = render.slideHeader) === null || _b === void 0 ? void 0 :
19
+ _b.call(render, slide),
20
+ ((_c = render.slideContainer) !== null && _c !== void 0 ? _c : ((_, x) => x))(slide, rendered), (_d = render.slideFooter) === null || _d === void 0 ? void 0 :
21
+ _d.call(render, slide))) : null;
16
22
  };
17
- return (React.createElement("div", { className: clsx(cssClass("slide"), cssClass("flex_center")), style: { [cssVar("slide_offset")]: offset } }, renderSlide()));
23
+ return (React.createElement("div", { ref: setContainerRef, className: clsx(cssClass("slide"), cssClass("flex_center")), style: { [cssVar("slide_offset")]: offset } }, containerRect && renderSlide(containerRect)));
18
24
  };
19
- export const Carousel = (props) => {
20
- const { slides, carousel: { finite, preload, padding, spacing }, render, } = props;
25
+ export const Carousel = ({ slides, carousel: { finite, preload, padding, spacing } }) => {
21
26
  const { currentIndex, globalIndex } = useController();
22
27
  const items = [];
23
28
  if ((slides === null || slides === void 0 ? void 0 : slides.length) > 0) {
24
29
  for (let i = currentIndex - preload; i < currentIndex; i += 1) {
25
30
  if (!finite || i >= 0) {
26
- items.push(React.createElement(CarouselSlide, { key: globalIndex + i - currentIndex, slide: slides[(i + preload * slides.length) % slides.length], offset: i - currentIndex, render: render }));
31
+ items.push(React.createElement(CarouselSlide, { key: globalIndex + i - currentIndex, slide: slides[(i + preload * slides.length) % slides.length], offset: i - currentIndex }));
27
32
  }
28
33
  }
29
- items.push(React.createElement(CarouselSlide, { key: globalIndex, slide: slides[currentIndex], offset: 0, render: render }));
34
+ items.push(React.createElement(CarouselSlide, { key: globalIndex, slide: slides[currentIndex], offset: 0 }));
30
35
  for (let i = currentIndex + 1; i <= currentIndex + preload; i += 1) {
31
36
  if (!finite || i <= slides.length - 1) {
32
- items.push(React.createElement(CarouselSlide, { key: globalIndex + i - currentIndex, slide: slides[i % slides.length], offset: i - currentIndex, render: render }));
37
+ items.push(React.createElement(CarouselSlide, { key: globalIndex + i - currentIndex, slide: slides[i % slides.length], offset: i - currentIndex }));
33
38
  }
34
39
  }
35
40
  }
@@ -1,9 +1,9 @@
1
1
  import * as React from "react";
2
- import { Component } from "../../types.js";
3
- import { ContainerRect, SubscribeSensors } from "../hooks/index.js";
2
+ import { Component, ComponentProps } from "../../types.js";
3
+ import { SubscribeSensors } from "../hooks/index.js";
4
4
  export declare type ControllerContextType = {
5
+ latestProps: React.MutableRefObject<ComponentProps>;
5
6
  containerRef: React.RefObject<HTMLDivElement>;
6
- containerRect: ContainerRect;
7
7
  currentIndex: number;
8
8
  globalIndex: number;
9
9
  subscribeSensors: SubscribeSensors<HTMLDivElement>;
@@ -2,13 +2,13 @@ import * as React from "react";
2
2
  import { LightboxDefaultProps } from "../../types.js";
3
3
  import { cleanup, clsx, cssClass, cssVar, makeUseContext } from "../utils.js";
4
4
  import { createModule } from "../config.js";
5
- import { useContainerRect, useEnhancedEffect, useSensors } from "../hooks/index.js";
5
+ import { useContainerRect, useEnhancedEffect, useLatest, useSensors } from "../hooks/index.js";
6
6
  import { useEvents, useTimeouts } from "../contexts/index.js";
7
7
  const SWIPE_OFFSET_THRESHOLD = 30;
8
8
  const ControllerContext = React.createContext(null);
9
9
  export const useController = makeUseContext("useController", "ControllerContext", ControllerContext);
10
10
  export const Controller = ({ children, ...props }) => {
11
- const { containerRef, setContainerRef, containerRect } = useContainerRect();
11
+ const { containerRef, setContainerRef } = useContainerRect();
12
12
  const { registerSensors, subscribeSensors } = useSensors();
13
13
  const { subscribe, publish } = useEvents();
14
14
  const { setTimeout, clearTimeout } = useTimeouts();
@@ -16,6 +16,7 @@ export const Controller = ({ children, ...props }) => {
16
16
  currentIndex: props.index,
17
17
  globalIndex: props.index,
18
18
  });
19
+ const latestProps = useLatest(props);
19
20
  const refs = React.useRef({
20
21
  state,
21
22
  props,
@@ -27,9 +28,6 @@ export const Controller = ({ children, ...props }) => {
27
28
  });
28
29
  refs.current.state = state;
29
30
  refs.current.props = props;
30
- refs.current.containerRect = containerRect;
31
- // prevent browser back/forward navigation on touchpad left/right swipe
32
- // this has to be done via non-passive native event handler
33
31
  useEnhancedEffect(() => {
34
32
  const preventDefault = (event) => event.preventDefault();
35
33
  const node = containerRef.current;
@@ -93,7 +91,7 @@ export const Controller = ({ children, ...props }) => {
93
91
  let newSwipeState = "swipe-animation";
94
92
  let newSwipeAnimationDuration = swipeAnimationDuration;
95
93
  if (!direction) {
96
- const containerWidth = (_a = current.containerRect) === null || _a === void 0 ? void 0 : _a.width;
94
+ const containerWidth = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.clientWidth;
97
95
  const elapsedTime = current.swipeStartTime ? Date.now() - current.swipeStartTime : 0;
98
96
  const expectedTime = containerWidth
99
97
  ? (swipeAnimationDuration / containerWidth) * Math.abs(swipeOffset)
@@ -107,7 +105,6 @@ export const Controller = ({ children, ...props }) => {
107
105
  newSwipeAnimationDuration =
108
106
  (newSwipeAnimationDuration / expectedTime) * Math.max(elapsedTime, expectedTime / 5);
109
107
  }
110
- // eslint-disable-next-line no-param-reassign
111
108
  direction = swipeOffset > 0 ? "prev" : "next";
112
109
  }
113
110
  else {
@@ -116,7 +113,7 @@ export const Controller = ({ children, ...props }) => {
116
113
  }
117
114
  const newState = {};
118
115
  if (direction === "prev") {
119
- if (isSwipeValid(swipeOffset)) {
116
+ if (isSwipeValid(1)) {
120
117
  newState.currentIndex = (currentIndex - 1 + slidesCount) % slidesCount;
121
118
  newState.globalIndex = globalIndex - 1;
122
119
  }
@@ -126,7 +123,7 @@ export const Controller = ({ children, ...props }) => {
126
123
  }
127
124
  }
128
125
  else if (direction === "next") {
129
- if (isSwipeValid(swipeOffset)) {
126
+ if (isSwipeValid(-1)) {
130
127
  newState.currentIndex = (currentIndex + 1) % slidesCount;
131
128
  newState.globalIndex = globalIndex + 1;
132
129
  }
@@ -146,7 +143,7 @@ export const Controller = ({ children, ...props }) => {
146
143
  }, newSwipeAnimationDuration);
147
144
  }
148
145
  setState((prev) => ({ ...prev, ...newState }));
149
- }, [setTimeout, resetSwipe, isSwipeValid, rerender]);
146
+ }, [setTimeout, resetSwipe, isSwipeValid, rerender, containerRef]);
150
147
  React.useEffect(() => cleanup(subscribe("prev", () => swipe("prev")), subscribe("next", () => swipe("next"))), [subscribe, swipe]);
151
148
  React.useEffect(() => subscribeSensors("onKeyUp", (event) => {
152
149
  if (event.code === "Escape") {
@@ -205,11 +202,9 @@ export const Controller = ({ children, ...props }) => {
205
202
  const onWheel = React.useCallback((event) => {
206
203
  var _a;
207
204
  if (event.ctrlKey) {
208
- // zoom
209
205
  return;
210
206
  }
211
207
  if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
212
- // pan-y
213
208
  return;
214
209
  }
215
210
  const { current } = refs;
@@ -218,6 +213,9 @@ export const Controller = ({ children, ...props }) => {
218
213
  current.wheelResidualMomentum = event.deltaX;
219
214
  return;
220
215
  }
216
+ if (!isSwipeValid(-event.deltaX)) {
217
+ return;
218
+ }
221
219
  current.swipeIntent += event.deltaX;
222
220
  clearTimeout(current.swipeIntentCleanup);
223
221
  if (Math.abs(current.swipeIntent) > SWIPE_OFFSET_THRESHOLD) {
@@ -235,7 +233,7 @@ export const Controller = ({ children, ...props }) => {
235
233
  }
236
234
  }
237
235
  else if (current.swipeState === "swipe") {
238
- const containerWidth = (_a = current.containerRect) === null || _a === void 0 ? void 0 : _a.width;
236
+ const containerWidth = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.clientWidth;
239
237
  if (containerWidth) {
240
238
  current.swipeOffset -= event.deltaX;
241
239
  current.swipeOffset =
@@ -261,19 +259,20 @@ export const Controller = ({ children, ...props }) => {
261
259
  else {
262
260
  current.wheelResidualMomentum = event.deltaX;
263
261
  }
264
- }, [updateSwipeOffset, setTimeout, clearTimeout, swipe, resetSwipe, rerender]);
262
+ }, [updateSwipeOffset, setTimeout, clearTimeout, swipe, resetSwipe, rerender, isSwipeValid, containerRef]);
265
263
  React.useEffect(() => subscribeSensors("onWheel", onWheel), [subscribeSensors, onWheel]);
266
264
  const context = React.useMemo(() => ({
265
+ latestProps,
267
266
  containerRef,
268
- containerRect,
269
267
  currentIndex: state.currentIndex,
270
268
  globalIndex: state.globalIndex,
271
269
  subscribeSensors,
272
- }), [containerRef, containerRect, state.currentIndex, state.globalIndex, subscribeSensors]);
270
+ }), [latestProps, containerRef, state.currentIndex, state.globalIndex, subscribeSensors]);
273
271
  return (React.createElement("div", { ref: setContainerRef, className: clsx(cssClass("container"), refs.current.swipeState === "swipe" && cssClass("container_swipe")), style: refs.current.swipeAnimationDuration !== LightboxDefaultProps.animation.swipe
274
272
  ? {
275
273
  [cssVar("swipe_animation_duration")]: `${Math.round(refs.current.swipeAnimationDuration)}ms`,
276
274
  }
277
- : undefined, role: "presentation", "aria-live": "polite", tabIndex: -1, ...registerSensors }, containerRect && (React.createElement(ControllerContext.Provider, { value: context }, children))));
275
+ : undefined, role: "presentation", "aria-live": "polite", tabIndex: -1, ...registerSensors },
276
+ React.createElement(ControllerContext.Provider, { value: context }, children)));
278
277
  };
279
278
  export const ControllerModule = createModule("controller", Controller);
@@ -7,7 +7,6 @@ const scrollbarPadding = cssVar("scrollbar_padding");
7
7
  export const NoScroll = ({ children }) => {
8
8
  React.useEffect(() => {
9
9
  const scrollbarWidth = Math.round(window.innerWidth - document.documentElement.clientWidth);
10
- // using an arbitrary threshold to counter the 1px difference in some browsers
11
10
  if (scrollbarWidth > 1) {
12
11
  document.body.style.setProperty(scrollbarPadding, `${scrollbarWidth}px`);
13
12
  document.body.classList.add(padScrollbar);
@@ -3,9 +3,16 @@ import { createModule } from "../config.js";
3
3
  import { cssClass, label } from "../utils.js";
4
4
  import { useEvents } from "../contexts/index.js";
5
5
  import { CloseIcon, IconButton } from "../components/index.js";
6
+ import { useContainerRect } from "../hooks/useContainerRect.js";
6
7
  export const Toolbar = ({ toolbar: { buttons }, labels, render: { buttonClose, iconClose } }) => {
7
8
  const { publish } = useEvents();
9
+ const { setContainerRef, containerRect } = useContainerRect();
10
+ React.useEffect(() => {
11
+ if (containerRect === null || containerRect === void 0 ? void 0 : containerRect.width) {
12
+ publish("toolbar-width", containerRect.width);
13
+ }
14
+ }, [publish, containerRect === null || containerRect === void 0 ? void 0 : containerRect.width]);
8
15
  const renderCloseButton = () => buttonClose ? (buttonClose()) : (React.createElement(IconButton, { key: "close", label: label(labels, "Close"), icon: CloseIcon, renderIcon: iconClose, onClick: () => publish("close") }));
9
- return (React.createElement("div", { className: cssClass("toolbar") }, buttons === null || buttons === void 0 ? void 0 : buttons.map((button) => (button === "close" ? renderCloseButton() : button))));
16
+ return (React.createElement("div", { ref: setContainerRef, className: cssClass("toolbar") }, buttons === null || buttons === void 0 ? void 0 : buttons.map((button) => (button === "close" ? renderCloseButton() : button))));
10
17
  };
11
18
  export const ToolbarModule = createModule("toolbar", Toolbar);
@@ -6,3 +6,5 @@ export declare const cssVar: (name: string) => string;
6
6
  export declare const label: (labels: Labels | undefined, lbl: string) => string;
7
7
  export declare const cleanup: (...cleaners: (() => void)[]) => () => void;
8
8
  export declare const makeUseContext: <T>(name: string, contextName: string, context: React.Context<T | null>) => () => T;
9
+ export declare const hasWindow: () => boolean;
10
+ export declare const adjustDevicePixelRatio: (value: number) => number;
@@ -16,3 +16,5 @@ export const makeUseContext = (name, contextName, context) => () => {
16
16
  }
17
17
  return ctx;
18
18
  };
19
+ export const hasWindow = () => typeof window !== "undefined";
20
+ export const adjustDevicePixelRatio = (value) => hasWindow() ? Math.round(value / (window.devicePixelRatio || 1)) : value;
@@ -0,0 +1,26 @@
1
+ import { Component, Plugin } from "../types.js";
2
+ declare type TextAlignment = "start" | "end" | "center";
3
+ declare module "../types.js" {
4
+ interface SlideImage {
5
+ /** slide title */
6
+ title?: string;
7
+ /** slide description */
8
+ description?: string;
9
+ }
10
+ interface LightboxProps {
11
+ /** Captions plugin settings */
12
+ captions?: {
13
+ /** description text alignment */
14
+ descriptionTextAlign?: TextAlignment;
15
+ /** maximum number of lines to display in the description section */
16
+ descriptionMaxLines?: number;
17
+ };
18
+ }
19
+ }
20
+ /** Captions plugin context holder */
21
+ export declare const CaptionsComponent: Component;
22
+ /** Captions plugin module */
23
+ export declare const CaptionsModule: import("../types.js").Module;
24
+ /** Captions plugin */
25
+ export declare const Captions: Plugin;
26
+ export default Captions;
@@ -0,0 +1,54 @@
1
+ import * as React from "react";
2
+ import { cssClass, cssVar, makeUseContext } from "../core/utils.js";
3
+ import { useEvents } from "../core/contexts/Events.js";
4
+ import { createModule } from "../core/index.js";
5
+ const defaultTextAlign = "start";
6
+ const defaultMaxLines = 3;
7
+ const cls = (className) => cssClass(`slide_${className}`);
8
+ const hasTitle = (slide) => "title" in slide ? typeof slide.title === "string" : false;
9
+ const hasDescription = (slide) => "description" in slide ? typeof slide.description === "string" : false;
10
+ const CaptionsContext = React.createContext(null);
11
+ const useCaptions = makeUseContext("useCaptions", "CaptionsContext", CaptionsContext);
12
+ const Title = ({ title }) => {
13
+ const { toolbarWidth } = useCaptions();
14
+ return (React.createElement("div", { className: cls(`title_container`) },
15
+ React.createElement("div", { className: cls("title"), ...(toolbarWidth ? { style: { [cssVar("toolbar_width")]: `${toolbarWidth}px` } } : null) }, title)));
16
+ };
17
+ const Description = ({ description, align, maxLines }) => (React.createElement("div", { className: cls("description_container") },
18
+ React.createElement("div", { className: cls("description"), ...(align !== defaultTextAlign || maxLines !== defaultMaxLines
19
+ ? {
20
+ style: {
21
+ [cssVar("slide_description_text_align")]: align,
22
+ [cssVar("slide_description_max_lines")]: maxLines,
23
+ },
24
+ }
25
+ : null) }, description.split("\n").flatMap((line, index) => [...(index > 0 ? [React.createElement("br", { key: index })] : []), line]))));
26
+ export const CaptionsComponent = ({ children }) => {
27
+ const { subscribe } = useEvents();
28
+ const [toolbarWidth, setToolbarWidth] = React.useState();
29
+ React.useEffect(() => subscribe("toolbar-width", (topic, event) => {
30
+ if (typeof event === "undefined" || typeof event === "number") {
31
+ setToolbarWidth(event);
32
+ }
33
+ }), [subscribe]);
34
+ const context = React.useMemo(() => ({ toolbarWidth }), [toolbarWidth]);
35
+ return React.createElement(CaptionsContext.Provider, { value: context }, children);
36
+ };
37
+ export const CaptionsModule = createModule("captions", CaptionsComponent);
38
+ export const Captions = ({ augment, addParent }) => {
39
+ addParent("controller", CaptionsModule);
40
+ augment(({ render: { slideFooter: renderFooter, ...restRender }, captions, ...restProps }) => ({
41
+ render: {
42
+ slideFooter: (slide) => {
43
+ var _a, _b;
44
+ return (React.createElement(React.Fragment, null, renderFooter === null || renderFooter === void 0 ? void 0 :
45
+ renderFooter(slide),
46
+ hasTitle(slide) && React.createElement(Title, { title: slide.title }),
47
+ hasDescription(slide) && (React.createElement(Description, { description: slide.description, align: (_a = captions === null || captions === void 0 ? void 0 : captions.descriptionTextAlign) !== null && _a !== void 0 ? _a : defaultTextAlign, maxLines: (_b = captions === null || captions === void 0 ? void 0 : captions.descriptionMaxLines) !== null && _b !== void 0 ? _b : defaultMaxLines }))));
48
+ },
49
+ ...restRender,
50
+ },
51
+ ...restProps,
52
+ }));
53
+ };
54
+ export default Captions;
@@ -2,8 +2,8 @@ import * as React from "react";
2
2
  import { createIcon, IconButton, label, useController, useLatest } from "../core/index.js";
3
3
  const EnterFullscreenIcon = createIcon("EnterFullscreen", React.createElement("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }));
4
4
  const ExitFullscreenIcon = createIcon("ExitFullscreen", React.createElement("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }));
5
- /** Fullscreen button */
6
5
  export const FullscreenButton = ({ auto, labels, render }) => {
6
+ const [mounted, setMounted] = React.useState(false);
7
7
  const [fullscreen, setFullscreen] = React.useState(false);
8
8
  const latestAuto = useLatest(auto);
9
9
  const { containerRef } = useController();
@@ -33,7 +33,6 @@ export const FullscreenButton = ({ auto, labels, render }) => {
33
33
  }
34
34
  }
35
35
  catch (err) {
36
- //
37
36
  }
38
37
  }
39
38
  }, [containerRef]);
@@ -54,7 +53,6 @@ export const FullscreenButton = ({ auto, labels, render }) => {
54
53
  }
55
54
  }
56
55
  catch (err) {
57
- //
58
56
  }
59
57
  }
60
58
  }, [getFullscreenElement]);
@@ -74,6 +72,7 @@ export const FullscreenButton = ({ auto, labels, render }) => {
74
72
  setFullscreen(false);
75
73
  }
76
74
  }, [containerRef, getFullscreenElement]);
75
+ React.useEffect(() => setMounted(true), []);
77
76
  React.useEffect(() => {
78
77
  const events = ["fullscreenchange", "webkitfullscreenchange", "mozfullscreenchange", "MSFullscreenChange"];
79
78
  events.forEach((event) => {
@@ -91,11 +90,10 @@ export const FullscreenButton = ({ auto, labels, render }) => {
91
90
  requestFullscreen();
92
91
  }
93
92
  }, [latestAuto, requestFullscreen]);
94
- if (!isFullscreenEnabled())
93
+ if (!mounted || !isFullscreenEnabled())
95
94
  return null;
96
95
  return render.buttonFullscreen ? (React.createElement(React.Fragment, null, render.buttonFullscreen({ fullscreen, toggleFullscreen }))) : (React.createElement(IconButton, { label: fullscreen ? label(labels, "Exit Fullscreen") : label(labels, "Enter Fullscreen"), icon: fullscreen ? ExitFullscreenIcon : EnterFullscreenIcon, renderIcon: fullscreen ? render.iconExitFullscreen : render.iconEnterFullscreen, onClick: toggleFullscreen }));
97
96
  };
98
- /** Fullscreen plugin */
99
97
  export const Fullscreen = ({ augment }) => {
100
98
  augment(({ toolbar: { buttons, ...restToolbar }, ...restProps }) => ({
101
99
  toolbar: {
@@ -1,10 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { createModule } from "../core/index.js";
3
- /** Inline plugin container */
4
3
  export const InlineContainer = ({ inline, children }) => React.createElement("div", { ...inline }, children);
5
- /** Inline plugin module */
6
4
  export const InlineModule = createModule("inline", InlineContainer);
7
- /** Inline plugin */
8
5
  export const Inline = ({ augment, replace, remove }) => {
9
6
  augment(({ toolbar: { buttons, ...restToolbar }, open, close, controller: { focus, ...restController }, ...restProps }) => ({
10
7
  open: true,
@@ -12,7 +12,6 @@ SlideTypesPropTypes.push(PropTypes.shape({
12
12
  type: PropTypes.string.isRequired,
13
13
  }).isRequired),
14
14
  }));
15
- /** Video slide */
16
15
  export const VideoSlide = ({ slide: { sources, poster, width, height } }) => {
17
16
  const { setContainerRef, containerRect } = useContainerRect();
18
17
  const scaleWidthAndHeight = () => {
@@ -27,21 +26,17 @@ export const VideoSlide = ({ slide: { sources, poster, width, height } }) => {
27
26
  return (React.createElement(React.Fragment, null, sources && (React.createElement("div", { ref: setContainerRef, style: {
28
27
  width: "100%",
29
28
  height: "100%",
30
- }, className: clsx(cssClass("video_container"), cssClass("flex_center")) }, containerRect && (
31
- // eslint-disable-next-line jsx-a11y/media-has-caption
32
- React.createElement("video", { controls: true, playsInline: true, poster: poster, ...scaleWidthAndHeight() }, sources.map(({ src, type }, index) => (
33
- // eslint-disable-next-line react/no-array-index-key
34
- React.createElement("source", { key: index, src: src, type: type })))))))));
29
+ ...(width ? { maxWidth: `${width}px` } : null),
30
+ }, className: clsx(cssClass("video_container"), cssClass("flex_center")) }, containerRect && (React.createElement("video", { controls: true, playsInline: true, poster: poster, ...scaleWidthAndHeight() }, sources.map(({ src, type }, index) => (React.createElement("source", { key: index, src: src, type: type })))))))));
35
31
  };
36
- /** Video plugin */
37
32
  export const Video = ({ augment }) => {
38
33
  augment(({ render: { slide: renderSlide, ...restRender }, ...restProps }) => ({
39
34
  render: {
40
- slide: (slide) => {
35
+ slide: (slide, offset, rect) => {
41
36
  if ("type" in slide && slide.type === "video") {
42
37
  return React.createElement(VideoSlide, { slide: slide });
43
38
  }
44
- return renderSlide === null || renderSlide === void 0 ? void 0 : renderSlide(slide);
39
+ return renderSlide === null || renderSlide === void 0 ? void 0 : renderSlide(slide, offset, rect);
45
40
  },
46
41
  ...restRender,
47
42
  },
@@ -0,0 +1,39 @@
1
+ .yarl__slide_title {
2
+ font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
3
+ font-size: 1.25rem;
4
+ font-weight: 700;
5
+ max-width: calc(100% - var(--yarl__toolbar_width, 0px));
6
+ overflow: hidden;
7
+ text-overflow: ellipsis;
8
+ white-space: nowrap;
9
+ }
10
+ .yarl__slide_title_container {
11
+ position: absolute;
12
+ left: 0;
13
+ right: 0;
14
+ top: 0;
15
+ padding: 16px;
16
+ background: rgba(0, 0, 0, 0.5);
17
+ }
18
+ .yarl__slide_description {
19
+ font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
20
+ font-size: 1rem;
21
+ line-height: 1.2;
22
+ font-weight: 500;
23
+ overflow: hidden;
24
+ -webkit-hyphens: auto;
25
+ -ms-hyphens: auto;
26
+ hyphens: auto;
27
+ display: -webkit-box;
28
+ -webkit-box-orient: vertical;
29
+ -webkit-line-clamp: var(--yarl__slide_description_max_lines, 3);
30
+ text-align: var(--yarl__slide_description_text_align, start);
31
+ }
32
+ .yarl__slide_description_container {
33
+ position: absolute;
34
+ left: 0;
35
+ right: 0;
36
+ bottom: 0;
37
+ padding: 16px;
38
+ background: rgba(0, 0, 0, 0.5);
39
+ }
@@ -1,3 +1,4 @@
1
+ export * from "./Captions.js";
1
2
  export * from "./Fullscreen.js";
2
3
  export * from "./Inline.js";
3
4
  export * from "./Video.js";
@@ -1,3 +1,4 @@
1
+ export * from "./Captions.js";
1
2
  export * from "./Fullscreen.js";
2
3
  export * from "./Inline.js";
3
4
  export * from "./Video.js";
package/dist/styles.css CHANGED
@@ -131,7 +131,7 @@
131
131
  }
132
132
  .yarl__toolbar {
133
133
  position: absolute;
134
- inset: 0 0 auto 0;
134
+ inset: 0 0 auto auto;
135
135
  display: flex;
136
136
  justify-content: flex-end;
137
137
  padding: 8px;
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import PropTypes from "prop-types";
3
+ import { ContainerRect } from "./core/hooks/useContainerRect.js";
3
4
  /** Image slide properties */
4
5
  export interface SlideImage {
5
6
  /** image URL */
@@ -49,7 +50,19 @@ export interface ControllerSettings {
49
50
  /** Custom render functions. */
50
51
  export interface Render {
51
52
  /** render custom slide type, or override the default image slide */
52
- slide?: (slide: Slide) => React.ReactNode;
53
+ slide?: (
54
+ /** slide */
55
+ slide: Slide,
56
+ /** slide offset (`0` - current slide, `1` - next slide, `-1` - previous slide, etc.) */
57
+ offset: number,
58
+ /** container rect */
59
+ rect: ContainerRect) => React.ReactNode;
60
+ /** render custom slide header */
61
+ slideHeader?: (slide: Slide) => React.ReactNode;
62
+ /** render custom slide footer */
63
+ slideFooter?: (slide: Slide) => React.ReactNode;
64
+ /** render custom slide container */
65
+ slideContainer?: (slide: Slide, children: React.ReactNode) => React.ReactNode;
53
66
  /** render custom Prev icon */
54
67
  iconPrev?: () => React.ReactNode;
55
68
  /** render custom Next icon */
@@ -111,7 +124,7 @@ export interface LightboxProps {
111
124
  on: Callbacks;
112
125
  }
113
126
  export declare const ImageSlidePropTypes: PropTypes.Requireable<PropTypes.InferProps<{
114
- src: PropTypes.Validator<string>;
127
+ src: PropTypes.Requireable<string>;
115
128
  alt: PropTypes.Requireable<string>;
116
129
  aspectRatio: PropTypes.Requireable<number>;
117
130
  srcSet: PropTypes.Requireable<PropTypes.InferProps<{
@@ -127,6 +140,9 @@ export declare const LightboxPropTypes: {
127
140
  slides: PropTypes.Validator<any[]>;
128
141
  render: PropTypes.Validator<PropTypes.InferProps<{
129
142
  slide: PropTypes.Requireable<(...args: any[]) => any>;
143
+ slideHeader: PropTypes.Requireable<(...args: any[]) => any>;
144
+ slideFooter: PropTypes.Requireable<(...args: any[]) => any>;
145
+ slideContainer: PropTypes.Requireable<(...args: any[]) => any>;
130
146
  iconPrev: PropTypes.Requireable<(...args: any[]) => any>;
131
147
  iconNext: PropTypes.Requireable<(...args: any[]) => any>;
132
148
  iconClose: PropTypes.Requireable<(...args: any[]) => any>;
package/dist/types.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import PropTypes from "prop-types";
2
2
  export const ImageSlidePropTypes = PropTypes.shape({
3
- src: PropTypes.string.isRequired,
3
+ src: PropTypes.string,
4
4
  alt: PropTypes.string,
5
5
  aspectRatio: PropTypes.number,
6
6
  srcSet: PropTypes.arrayOf(PropTypes.shape({
@@ -16,6 +16,9 @@ export const LightboxPropTypes = {
16
16
  slides: PropTypes.arrayOf(PropTypes.oneOfType(SlideTypesPropTypes).isRequired).isRequired,
17
17
  render: PropTypes.shape({
18
18
  slide: PropTypes.func,
19
+ slideHeader: PropTypes.func,
20
+ slideFooter: PropTypes.func,
21
+ slideContainer: PropTypes.func,
19
22
  iconPrev: PropTypes.func,
20
23
  iconNext: PropTypes.func,
21
24
  iconClose: PropTypes.func,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yet-another-react-lightbox",
3
- "version": "1.3.5",
3
+ "version": "1.4.1",
4
4
  "description": "Modern React lightbox component",
5
5
  "author": "Igor Danchenko",
6
6
  "license": "MIT",
@@ -10,6 +10,8 @@
10
10
  ".": "./dist/index.js",
11
11
  "./core": "./dist/core/index.js",
12
12
  "./plugins": "./dist/plugins/index.js",
13
+ "./plugins/captions": "./dist/plugins/Captions.js",
14
+ "./plugins/captions.css": "./dist/plugins/captions.css",
13
15
  "./plugins/fullscreen": "./dist/plugins/Fullscreen.js",
14
16
  "./plugins/inline": "./dist/plugins/Inline.js",
15
17
  "./plugins/video": "./dist/plugins/Video.js",
@@ -27,6 +29,9 @@
27
29
  "plugins": [
28
30
  "dist/plugins/index.d.ts"
29
31
  ],
32
+ "plugins/captions": [
33
+ "dist/plugins/Captions.d.ts"
34
+ ],
30
35
  "plugins/fullscreen": [
31
36
  "dist/plugins/Fullscreen.d.ts"
32
37
  ],
@@ -59,8 +64,8 @@
59
64
  "build": "npm-run-all clean build:scss build:css build:js build:dts",
60
65
  "build:js": "tsc -p tsconfig.build.js.json",
61
66
  "build:dts": "tsc -p tsconfig.build.dts.json",
62
- "build:css": "postcss src/styles.css -u autoprefixer --no-map -d dist",
63
- "build:scss": "sass src/styles.scss src/styles.css --no-source-map ",
67
+ "build:css": "postcss src/*.css src/**/*.css --base src -d dist -u autoprefixer --no-map",
68
+ "build:scss": "sass src --no-source-map",
64
69
  "start": "npm-run-all clean --parallel \"build:* -- -w\"",
65
70
  "lint": "eslint .",
66
71
  "test": "jest"
@@ -80,13 +85,13 @@
80
85
  "@testing-library/jest-dom": "^5.16.4",
81
86
  "@testing-library/react": "^13.3.0",
82
87
  "@testing-library/user-event": "^14.2.0",
83
- "@types/jest": "^27.5.2",
84
- "@types/react": "^18.0.10",
88
+ "@types/jest": "^28.1.1",
89
+ "@types/react": "^18.0.12",
85
90
  "@types/react-dom": "^18.0.5",
86
- "@typescript-eslint/eslint-plugin": "^5.27.0",
87
- "@typescript-eslint/parser": "^5.27.0",
91
+ "@typescript-eslint/eslint-plugin": "^5.27.1",
92
+ "@typescript-eslint/parser": "^5.27.1",
88
93
  "autoprefixer": "^10.4.7",
89
- "eslint": "^8.16.0",
94
+ "eslint": "^8.17.0",
90
95
  "eslint-config-airbnb": "^19.0.4",
91
96
  "eslint-config-airbnb-typescript": "^17.0.0",
92
97
  "eslint-config-prettier": "^8.5.0",
@@ -96,9 +101,9 @@
96
101
  "eslint-plugin-react": "^7.30.0",
97
102
  "eslint-plugin-react-hooks": "^4.5.0",
98
103
  "husky": "^8.0.1",
99
- "jest": "^28.1.0",
100
- "jest-environment-jsdom": "^28.1.0",
101
- "lint-staged": "^13.0.0",
104
+ "jest": "^28.1.1",
105
+ "jest-environment-jsdom": "^28.1.1",
106
+ "lint-staged": "^13.0.1",
102
107
  "npm-run-all": "^4.1.5",
103
108
  "postcss": "^8.4.14",
104
109
  "postcss-cli": "^9.1.0",
@@ -106,12 +111,13 @@
106
111
  "react": "^18.1.0",
107
112
  "react-dom": "^18.1.0",
108
113
  "rimraf": "^3.0.2",
109
- "sass": "^1.52.1",
110
- "ts-jest": "^28.0.3",
111
- "typescript": "^4.7.2"
114
+ "sass": "^1.52.3",
115
+ "ts-jest": "^28.0.4",
116
+ "typescript": "^4.7.3"
112
117
  },
113
118
  "keywords": [
114
119
  "react",
120
+ "image",
115
121
  "lightbox",
116
122
  "react lightbox"
117
123
  ]