@terraware/web-components 5.0.11 → 5.0.13
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/components/PhotosCarousel/PhotoSlide.d.ts +10 -0
- package/components/PhotosCarousel/PhotoSlide.d.ts.map +1 -0
- package/components/PhotosCarousel/PhotoSlide.js +36 -0
- package/components/PhotosCarousel/index.d.ts +2 -2
- package/components/PhotosCarousel/index.d.ts.map +1 -1
- package/components/PhotosCarousel/index.js +72 -83
- package/components/PhotosCarousel/styles.scss +100 -13
- package/components/PhotosCarousel/usePhotosCarousel.d.ts +15 -0
- package/components/PhotosCarousel/usePhotosCarousel.d.ts.map +1 -0
- package/components/PhotosCarousel/usePhotosCarousel.js +79 -0
- package/components/TimelineSliderV2/layout.d.ts +16 -0
- package/components/TimelineSliderV2/layout.d.ts.map +1 -0
- package/components/TimelineSliderV2/layout.js +168 -0
- package/components/TimelineSliderV2/types.d.ts +43 -0
- package/components/TimelineSliderV2/types.d.ts.map +1 -0
- package/components/TimelineSliderV2/types.js +1 -0
- package/components/ViewPhotosDialog/index.d.ts +0 -2
- package/components/ViewPhotosDialog/index.d.ts.map +1 -1
- package/components/ViewPhotosDialog/index.js +2 -18
- package/components/VirtualWalkthrough/AnnotationPanel.js +2 -2
- package/license-report.html +1 -1
- package/package.json +2 -2
- package/_vendor/react-multi-carousel/index.js +0 -13
- package/_vendor/react-multi-carousel/lib/Arrows.js +0 -13
- package/_vendor/react-multi-carousel/lib/Carousel.js +0 -19
- package/_vendor/react-multi-carousel/lib/CarouselItems.js +0 -14
- package/_vendor/react-multi-carousel/lib/Dots.js +0 -16
- package/_vendor/react-multi-carousel/lib/index.js +0 -13
- package/_vendor/react-multi-carousel/lib/types.js +0 -13
- package/_vendor/react-multi-carousel/lib/utils/clones.js +0 -14
- package/_vendor/react-multi-carousel/lib/utils/common.js +0 -13
- package/_vendor/react-multi-carousel/lib/utils/dots.js +0 -14
- package/_vendor/react-multi-carousel/lib/utils/elementWidth.js +0 -12
- package/_vendor/react-multi-carousel/lib/utils/index.js +0 -20
- package/_vendor/react-multi-carousel/lib/utils/mouseOrTouchMove.js +0 -14
- package/_vendor/react-multi-carousel/lib/utils/next.js +0 -13
- package/_vendor/react-multi-carousel/lib/utils/previous.js +0 -14
- package/_vendor/react-multi-carousel/lib/utils/throttle.js +0 -12
- package/_vendor/react-multi-carousel/lib/utils/throwError.js +0 -14
- package/_virtual/Arrows.js +0 -3
- package/_virtual/Carousel.js +0 -3
- package/_virtual/CarouselItems.js +0 -3
- package/_virtual/Dots.js +0 -3
- package/_virtual/_commonjsHelpers.js +0 -5
- package/_virtual/clones.js +0 -3
- package/_virtual/common.js +0 -3
- package/_virtual/dots2.js +0 -3
- package/_virtual/elementWidth.js +0 -3
- package/_virtual/index.js +0 -7
- package/_virtual/index2.js +0 -3
- package/_virtual/index3.js +0 -3
- package/_virtual/mouseOrTouchMove.js +0 -3
- package/_virtual/next.js +0 -3
- package/_virtual/previous.js +0 -3
- package/_virtual/throttle.js +0 -3
- package/_virtual/throwError.js +0 -3
- package/_virtual/types.js +0 -3
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PhotoItem } from './index';
|
|
2
|
+
interface PhotoSlideProps {
|
|
3
|
+
photo: PhotoItem;
|
|
4
|
+
index: number;
|
|
5
|
+
total: number;
|
|
6
|
+
selected: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare const PhotoSlide: ({ photo, index, total, selected }: PhotoSlideProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export default PhotoSlide;
|
|
10
|
+
//# sourceMappingURL=PhotoSlide.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PhotoSlide.d.ts","sourceRoot":"","sources":["../../../src/components/PhotosCarousel/PhotoSlide.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,UAAU,eAAe;IACvB,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,QAAA,MAAM,UAAU,GAAI,mCAAmC,eAAe,4CAkBrE,CAAC;AAEF,eAAe,UAAU,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import BusySpinner from '../BusySpinner/index.js';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
const PhotoSlide = ({
|
|
6
|
+
photo,
|
|
7
|
+
index,
|
|
8
|
+
total,
|
|
9
|
+
selected
|
|
10
|
+
}) => {
|
|
11
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
12
|
+
const finishLoading = () => setIsLoading(false);
|
|
13
|
+
return /*#__PURE__*/jsxs("div", {
|
|
14
|
+
className: "embla__slide",
|
|
15
|
+
role: "group",
|
|
16
|
+
"aria-roledescription": "slide",
|
|
17
|
+
"aria-label": `${index + 1} of ${total}`,
|
|
18
|
+
children: [isLoading && /*#__PURE__*/jsx(BusySpinner, {
|
|
19
|
+
noBackground: true
|
|
20
|
+
}), /*#__PURE__*/jsx("a", {
|
|
21
|
+
href: photo.url,
|
|
22
|
+
target: "_blank",
|
|
23
|
+
rel: "noopener noreferrer",
|
|
24
|
+
tabIndex: selected ? 0 : -1,
|
|
25
|
+
children: /*#__PURE__*/jsx("img", {
|
|
26
|
+
className: "embla__slide__img",
|
|
27
|
+
src: photo.url,
|
|
28
|
+
alt: photo.alt,
|
|
29
|
+
onLoad: finishLoading,
|
|
30
|
+
onError: finishLoading
|
|
31
|
+
})
|
|
32
|
+
})]
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export { PhotoSlide as default };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { type JSX } from 'react';
|
|
2
|
-
import 'react-multi-carousel/lib/styles.css';
|
|
3
2
|
import './styles.scss';
|
|
4
3
|
export type PhotoItem = {
|
|
5
4
|
url: string;
|
|
@@ -14,5 +13,6 @@ export interface PhotosCarouselProps {
|
|
|
14
13
|
numbered?: boolean;
|
|
15
14
|
dots?: boolean;
|
|
16
15
|
}
|
|
17
|
-
|
|
16
|
+
declare const PhotosCarousel: (props: PhotosCarouselProps) => JSX.Element;
|
|
17
|
+
export default PhotosCarousel;
|
|
18
18
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/PhotosCarousel/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,KAAK,GAAG,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/PhotosCarousel/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AAMxC,OAAO,eAAe,CAAC;AAGvB,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC9B,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,QAAA,MAAM,cAAc,GAAI,OAAO,mBAAmB,KAAG,GAAG,CAAC,OAyExD,CAAC;AAEF,eAAe,cAAc,CAAC"}
|
|
@@ -1,97 +1,86 @@
|
|
|
1
|
-
import
|
|
2
|
-
import CarouselImport from '../../_virtual/index.js';
|
|
3
|
-
import 'react-multi-carousel/lib/styles.css';
|
|
1
|
+
import 'react';
|
|
4
2
|
import { Box, Typography } from '@mui/material';
|
|
5
|
-
import
|
|
3
|
+
import Icon from '../Icon/Icon.js';
|
|
4
|
+
import PhotoSlide from './PhotoSlide.js';
|
|
6
5
|
import './styles.scss';
|
|
7
|
-
import
|
|
6
|
+
import usePhotosCarousel from './usePhotosCarousel.js';
|
|
7
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const responsive = {
|
|
11
|
-
mobile: {
|
|
12
|
-
breakpoint: {
|
|
13
|
-
max: 4000,
|
|
14
|
-
min: 0
|
|
15
|
-
},
|
|
16
|
-
items: 1
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
function PhotosCarousel(props) {
|
|
9
|
+
const PhotosCarousel = props => {
|
|
20
10
|
const {
|
|
21
11
|
photos,
|
|
22
|
-
|
|
23
|
-
onSlideChange,
|
|
24
|
-
showArrows,
|
|
12
|
+
showArrows = false,
|
|
25
13
|
numbered,
|
|
26
|
-
dots
|
|
14
|
+
dots = true
|
|
27
15
|
} = props;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}, [photoKey]);
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
if (myCarousel.current && myCarousel.current.state.currentSlide !== currentSlide) {
|
|
42
|
-
myCarousel.current.goToSlide(currentSlide);
|
|
43
|
-
}
|
|
44
|
-
}, [currentSlide]);
|
|
45
|
-
const handleAfterChange = useCallback(() => {
|
|
46
|
-
const slide = myCarousel.current?.state.currentSlide ?? 0;
|
|
47
|
-
if (!isControlled) {
|
|
48
|
-
setInternalSlide(slide);
|
|
49
|
-
}
|
|
50
|
-
if (slide !== currentSlide) {
|
|
51
|
-
onSlideChange?.(slide);
|
|
52
|
-
}
|
|
53
|
-
}, [isControlled, currentSlide, onSlideChange]);
|
|
54
|
-
const finishLoading = index => {
|
|
55
|
-
setIsLoading(prev => {
|
|
56
|
-
const next = [...prev];
|
|
57
|
-
next[index] = false;
|
|
58
|
-
return next;
|
|
59
|
-
});
|
|
60
|
-
};
|
|
16
|
+
const {
|
|
17
|
+
emblaRef,
|
|
18
|
+
currentSlide,
|
|
19
|
+
scrollSnaps,
|
|
20
|
+
prevButtonDisabled,
|
|
21
|
+
nextButtonDisabled,
|
|
22
|
+
scrollPrev,
|
|
23
|
+
scrollNext,
|
|
24
|
+
scrollTo,
|
|
25
|
+
onKeyDown
|
|
26
|
+
} = usePhotosCarousel(props);
|
|
61
27
|
return /*#__PURE__*/jsxs(Box, {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
28
|
+
className: "photos-carousel",
|
|
29
|
+
role: "region",
|
|
30
|
+
"aria-roledescription": "carousel",
|
|
31
|
+
"aria-label": "Photos",
|
|
32
|
+
children: [/*#__PURE__*/jsxs("div", {
|
|
33
|
+
className: "embla",
|
|
34
|
+
children: [/*#__PURE__*/jsx("div", {
|
|
35
|
+
className: "embla__viewport",
|
|
36
|
+
ref: emblaRef,
|
|
37
|
+
onKeyDown: onKeyDown,
|
|
38
|
+
children: /*#__PURE__*/jsx("div", {
|
|
39
|
+
className: "embla__container",
|
|
40
|
+
children: photos.map((photo, index) => /*#__PURE__*/jsx(PhotoSlide, {
|
|
41
|
+
photo: photo,
|
|
42
|
+
index: index,
|
|
43
|
+
total: photos.length,
|
|
44
|
+
selected: index === currentSlide
|
|
45
|
+
}, `${photo.url}-${index}`))
|
|
46
|
+
})
|
|
47
|
+
}), showArrows && photos.length > 1 && /*#__PURE__*/jsxs(Fragment, {
|
|
48
|
+
children: [/*#__PURE__*/jsx("button", {
|
|
49
|
+
type: "button",
|
|
50
|
+
className: "embla__prev",
|
|
51
|
+
"aria-label": "Previous photo",
|
|
52
|
+
disabled: prevButtonDisabled,
|
|
53
|
+
onClick: scrollPrev,
|
|
54
|
+
children: /*#__PURE__*/jsx(Icon, {
|
|
55
|
+
name: "caretLeft",
|
|
56
|
+
fillColor: "currentColor"
|
|
57
|
+
})
|
|
58
|
+
}), /*#__PURE__*/jsx("button", {
|
|
59
|
+
type: "button",
|
|
60
|
+
className: "embla__next",
|
|
61
|
+
"aria-label": "Next photo",
|
|
62
|
+
disabled: nextButtonDisabled,
|
|
63
|
+
onClick: scrollNext,
|
|
64
|
+
children: /*#__PURE__*/jsx(Icon, {
|
|
65
|
+
name: "caretRight",
|
|
66
|
+
fillColor: "currentColor"
|
|
87
67
|
})
|
|
88
68
|
})]
|
|
89
|
-
},
|
|
90
|
-
|
|
69
|
+
}), dots && /*#__PURE__*/jsx("div", {
|
|
70
|
+
className: "embla__dots",
|
|
71
|
+
children: scrollSnaps.map((_, index) => /*#__PURE__*/jsx("button", {
|
|
72
|
+
type: "button",
|
|
73
|
+
className: `embla__dot${index === currentSlide ? ' embla__dot--selected' : ''}`,
|
|
74
|
+
"aria-label": `Go to photo ${index + 1}`,
|
|
75
|
+
"aria-current": index === currentSlide ? 'true' : undefined,
|
|
76
|
+
onClick: () => scrollTo(index)
|
|
77
|
+
}, index))
|
|
78
|
+
})]
|
|
79
|
+
}), numbered && /*#__PURE__*/jsx(Typography, {
|
|
91
80
|
className: "photo-numbering",
|
|
92
|
-
children: `${currentSlide + 1}/${photos.length}`
|
|
93
|
-
})
|
|
81
|
+
children: `${photos.length ? currentSlide + 1 : 0}/${photos.length}`
|
|
82
|
+
}), photos[currentSlide]?.decoration]
|
|
94
83
|
});
|
|
95
|
-
}
|
|
84
|
+
};
|
|
96
85
|
|
|
97
86
|
export { PhotosCarousel as default };
|
|
@@ -1,18 +1,105 @@
|
|
|
1
1
|
@use '../../style-dictionary-dist/terraware.scss';
|
|
2
2
|
|
|
3
|
-
.photos-carousel
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
.photos-carousel {
|
|
4
|
+
--arrow-size: 43px;
|
|
5
|
+
--control-spacing: 20px;
|
|
6
|
+
--arrow-inset: calc(4% + 1px);
|
|
7
|
+
text-align: center;
|
|
8
|
+
.embla {
|
|
9
|
+
position: relative;
|
|
10
|
+
padding-bottom: var(--control-spacing);
|
|
11
|
+
}
|
|
7
12
|
|
|
8
|
-
.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
.embla__viewport {
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.embla__container {
|
|
18
|
+
display: flex;
|
|
19
|
+
touch-action: pan-y pinch-zoom;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.embla__slide {
|
|
23
|
+
flex: 0 0 100%;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
position: relative;
|
|
26
|
+
min-height: 200px;
|
|
27
|
+
background: terraware.$tw-clr-bg;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.embla__slide__img {
|
|
31
|
+
object-fit: contain;
|
|
32
|
+
margin: auto;
|
|
33
|
+
height: 400px;
|
|
34
|
+
width: 100%;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.embla__prev,
|
|
38
|
+
.embla__next {
|
|
39
|
+
position: absolute;
|
|
40
|
+
top: calc((100% - var(--control-spacing)) / 2);
|
|
41
|
+
transform: translateY(-50%);
|
|
42
|
+
width: var(--arrow-size);
|
|
43
|
+
height: var(--arrow-size);
|
|
44
|
+
border: 0;
|
|
45
|
+
border-radius: 50%;
|
|
46
|
+
background: rgba(0, 0, 0, 0.5);
|
|
47
|
+
color: white;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
|
|
53
|
+
.tw-icon {
|
|
54
|
+
stroke: currentColor;
|
|
55
|
+
stroke-width: 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
&:hover {
|
|
59
|
+
background: rgba(0, 0, 0, 0.8);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
&:disabled {
|
|
63
|
+
visibility: hidden;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.embla__prev {
|
|
68
|
+
left: var(--arrow-inset);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.embla__next {
|
|
72
|
+
right: var(--arrow-inset);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.embla__dots {
|
|
76
|
+
position: absolute;
|
|
77
|
+
bottom: 0;
|
|
78
|
+
left: 0;
|
|
79
|
+
width: 100%;
|
|
80
|
+
display: flex;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
gap: 6px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.embla__dot {
|
|
86
|
+
width: 12px;
|
|
87
|
+
height: 12px;
|
|
88
|
+
padding: 0;
|
|
89
|
+
margin: 0;
|
|
90
|
+
border: 2px solid grey;
|
|
91
|
+
border-radius: 50%;
|
|
92
|
+
background: white;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
|
|
95
|
+
&--selected,
|
|
96
|
+
&:hover {
|
|
97
|
+
background: #080808;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
14
100
|
|
|
15
|
-
.photo-numbering {
|
|
16
|
-
|
|
17
|
-
|
|
101
|
+
.photo-numbering {
|
|
102
|
+
font-size: 12px;
|
|
103
|
+
color: terraware.$tw-clr-txt-info;
|
|
104
|
+
}
|
|
18
105
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type KeyboardEvent } from 'react';
|
|
2
|
+
import type { PhotosCarouselProps } from './index';
|
|
3
|
+
declare const usePhotosCarousel: ({ photos, selectedSlide, onSlideChange, }: Pick<PhotosCarouselProps, "photos" | "selectedSlide" | "onSlideChange">) => {
|
|
4
|
+
emblaRef: import("embla-carousel-react").EmblaViewportRefType;
|
|
5
|
+
currentSlide: number;
|
|
6
|
+
scrollSnaps: number[];
|
|
7
|
+
prevButtonDisabled: boolean;
|
|
8
|
+
nextButtonDisabled: boolean;
|
|
9
|
+
scrollPrev: () => void | undefined;
|
|
10
|
+
scrollNext: () => void | undefined;
|
|
11
|
+
scrollTo: (index: number) => void | undefined;
|
|
12
|
+
onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
13
|
+
};
|
|
14
|
+
export default usePhotosCarousel;
|
|
15
|
+
//# sourceMappingURL=usePhotosCarousel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePhotosCarousel.d.ts","sourceRoot":"","sources":["../../../src/components/PhotosCarousel/usePhotosCarousel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAuB,MAAM,OAAO,CAAC;AAIhE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD,QAAA,MAAM,iBAAiB,GAAI,2CAIxB,IAAI,CAAC,mBAAmB,EAAE,QAAQ,GAAG,eAAe,GAAG,eAAe,CAAC;;;;;;;;sBAY/C,MAAM;uBAsCL,aAAa,CAAC,cAAc,CAAC;CAsBxD,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import useEmblaCarousel from 'embla-carousel-react';
|
|
3
|
+
|
|
4
|
+
const usePhotosCarousel = ({
|
|
5
|
+
photos,
|
|
6
|
+
selectedSlide,
|
|
7
|
+
onSlideChange
|
|
8
|
+
}) => {
|
|
9
|
+
const [internalSlide, setInternalSlide] = useState(0);
|
|
10
|
+
const currentSlide = Math.max(0, Math.min(selectedSlide ?? internalSlide, photos.length - 1));
|
|
11
|
+
const [startIndex] = useState(currentSlide);
|
|
12
|
+
const [emblaRef, emblaApi] = useEmblaCarousel({
|
|
13
|
+
loop: false,
|
|
14
|
+
startIndex
|
|
15
|
+
});
|
|
16
|
+
const [scrollSnaps, setScrollSnaps] = useState([]);
|
|
17
|
+
const [prevButtonDisabled, setPrevButtonDisabled] = useState(true);
|
|
18
|
+
const [nextButtonDisabled, setNextButtonDisabled] = useState(true);
|
|
19
|
+
const photoKey = JSON.stringify(photos.map(photo => photo.url));
|
|
20
|
+
const scrollPrev = () => emblaApi?.scrollPrev();
|
|
21
|
+
const scrollNext = () => emblaApi?.scrollNext();
|
|
22
|
+
const scrollTo = index => emblaApi?.scrollTo(index);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
emblaApi?.reInit();
|
|
25
|
+
}, [emblaApi, photoKey]);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!emblaApi) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Synchronize external selection before subscribing so it does not echo a change callback.
|
|
31
|
+
if (emblaApi.selectedScrollSnap() !== currentSlide) {
|
|
32
|
+
emblaApi.scrollTo(currentSlide);
|
|
33
|
+
}
|
|
34
|
+
const updateControls = () => {
|
|
35
|
+
setPrevButtonDisabled(!emblaApi.canScrollPrev());
|
|
36
|
+
setNextButtonDisabled(!emblaApi.canScrollNext());
|
|
37
|
+
};
|
|
38
|
+
const handleInit = () => {
|
|
39
|
+
setScrollSnaps(emblaApi.scrollSnapList());
|
|
40
|
+
updateControls();
|
|
41
|
+
};
|
|
42
|
+
const handleSelect = () => {
|
|
43
|
+
updateControls();
|
|
44
|
+
const slide = emblaApi.selectedScrollSnap();
|
|
45
|
+
setInternalSlide(slide);
|
|
46
|
+
if (slide !== currentSlide) {
|
|
47
|
+
onSlideChange?.(slide);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
handleInit();
|
|
51
|
+
emblaApi.on('select', handleSelect).on('reInit', handleInit);
|
|
52
|
+
return () => {
|
|
53
|
+
emblaApi.off('select', handleSelect).off('reInit', handleInit);
|
|
54
|
+
};
|
|
55
|
+
}, [emblaApi, currentSlide, onSlideChange, photoKey]);
|
|
56
|
+
const onKeyDown = event => {
|
|
57
|
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
if (event.key === 'ArrowLeft') {
|
|
60
|
+
scrollPrev();
|
|
61
|
+
} else {
|
|
62
|
+
scrollNext();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
emblaRef,
|
|
68
|
+
currentSlide,
|
|
69
|
+
scrollSnaps,
|
|
70
|
+
prevButtonDisabled,
|
|
71
|
+
nextButtonDisabled,
|
|
72
|
+
scrollPrev,
|
|
73
|
+
scrollNext,
|
|
74
|
+
scrollTo,
|
|
75
|
+
onKeyDown
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export { usePhotosCarousel as default };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BuildLayoutParams, ColorWeight, LayoutMark, PositionedMark, TimelineCluster, TimelineLayout } from './types';
|
|
2
|
+
export declare const DOT_SIZE_PX = 12;
|
|
3
|
+
export declare const CLUSTER_DOT_SIZE_PX = 18;
|
|
4
|
+
export declare const EXPANDED_SPACING_PX = 20;
|
|
5
|
+
export declare const MIN_SQUEEZE_PITCH_PX = 12;
|
|
6
|
+
export declare const BAND_PADDING_PX = 8;
|
|
7
|
+
export declare const SQUEEZE_GAP_PX = 12;
|
|
8
|
+
export declare const MAX_BAND_RATIO = 0.8;
|
|
9
|
+
export declare const DEFAULT_CLUSTER_THRESHOLD_PX = 16;
|
|
10
|
+
export declare const DIMMED_OPACITY = 0.35;
|
|
11
|
+
export declare const normalizePositions: (marks: LayoutMark[], containerWidth: number) => PositionedMark[];
|
|
12
|
+
export declare const toColorWeights: (colors: string[]) => ColorWeight[];
|
|
13
|
+
export declare const buildClusters: (marks: LayoutMark[], containerWidth: number, thresholdPx: number) => TimelineCluster[];
|
|
14
|
+
export declare const toConicGradient: (colorWeights: ColorWeight[]) => string;
|
|
15
|
+
export declare const buildLayout: ({ containerWidth, expandedClusterId, marks, thresholdPx, }: BuildLayoutParams) => TimelineLayout;
|
|
16
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../../src/components/TimelineSliderV2/layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,cAAc,EACd,eAAe,EACf,cAAc,EAEf,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,WAAW,KAAK,CAAC;AAC9B,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC,eAAO,MAAM,oBAAoB,KAAc,CAAC;AAChD,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,eAAO,MAAM,cAAc,MAAM,CAAC;AAClC,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAC/C,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC,eAAO,MAAM,kBAAkB,GAAI,OAAO,UAAU,EAAE,EAAE,gBAAgB,MAAM,KAAG,cAAc,EAkB9F,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,QAAQ,MAAM,EAAE,KAAG,WAAW,EAQ5D,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,OAAO,UAAU,EAAE,EAAE,gBAAgB,MAAM,EAAE,aAAa,MAAM,KAAG,eAAe,EAkC/G,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,cAAc,WAAW,EAAE,KAAG,MAiB7D,CAAC;AAkCF,eAAO,MAAM,WAAW,GAAI,4DAKzB,iBAAiB,KAAG,cAyDtB,CAAC"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const DOT_SIZE_PX = 12;
|
|
2
|
+
const CLUSTER_DOT_SIZE_PX = 18;
|
|
3
|
+
const EXPANDED_SPACING_PX = 20;
|
|
4
|
+
const MIN_SQUEEZE_PITCH_PX = DOT_SIZE_PX;
|
|
5
|
+
const BAND_PADDING_PX = 8;
|
|
6
|
+
const SQUEEZE_GAP_PX = 12;
|
|
7
|
+
const MAX_BAND_RATIO = 0.8;
|
|
8
|
+
const DEFAULT_CLUSTER_THRESHOLD_PX = 16;
|
|
9
|
+
const DIMMED_OPACITY = 0.35;
|
|
10
|
+
const normalizePositions = (marks, containerWidth) => {
|
|
11
|
+
if (marks.length === 0) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const values = marks.map(mark => mark.value);
|
|
15
|
+
const minValue = Math.min(...values);
|
|
16
|
+
const maxValue = Math.max(...values);
|
|
17
|
+
const range = maxValue - minValue;
|
|
18
|
+
|
|
19
|
+
// Mirrors TimelineSlider's behavior: when every mark shares one value, they all land at the right edge.
|
|
20
|
+
return marks.map(mark => ({
|
|
21
|
+
color: mark.color,
|
|
22
|
+
id: mark.id,
|
|
23
|
+
positionPx: (range === 0 ? 1 : (mark.value - minValue) / range) * containerWidth
|
|
24
|
+
})).sort((a, b) => a.positionPx - b.positionPx);
|
|
25
|
+
};
|
|
26
|
+
const toColorWeights = colors => {
|
|
27
|
+
const weights = new Map();
|
|
28
|
+
colors.forEach(color => {
|
|
29
|
+
weights.set(color, (weights.get(color) ?? 0) + 1);
|
|
30
|
+
});
|
|
31
|
+
return [...weights.entries()].map(([color, weight]) => ({
|
|
32
|
+
color,
|
|
33
|
+
weight
|
|
34
|
+
}));
|
|
35
|
+
};
|
|
36
|
+
const buildClusters = (marks, containerWidth, thresholdPx) => {
|
|
37
|
+
const positioned = normalizePositions(marks, containerWidth);
|
|
38
|
+
const clusters = [];
|
|
39
|
+
let current = [];
|
|
40
|
+
const flush = () => {
|
|
41
|
+
if (current.length === 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const total = current.reduce((sum, member) => sum + member.positionPx, 0);
|
|
45
|
+
clusters.push({
|
|
46
|
+
anchorPx: total / current.length,
|
|
47
|
+
id: `cluster-${current[0].id}`,
|
|
48
|
+
members: current
|
|
49
|
+
});
|
|
50
|
+
current = [];
|
|
51
|
+
};
|
|
52
|
+
positioned.forEach(mark => {
|
|
53
|
+
const previous = current[current.length - 1];
|
|
54
|
+
if (previous !== undefined && mark.positionPx - previous.positionPx >= thresholdPx) {
|
|
55
|
+
flush();
|
|
56
|
+
}
|
|
57
|
+
current.push(mark);
|
|
58
|
+
});
|
|
59
|
+
flush();
|
|
60
|
+
return clusters;
|
|
61
|
+
};
|
|
62
|
+
const toConicGradient = colorWeights => {
|
|
63
|
+
if (colorWeights.length <= 1) {
|
|
64
|
+
return colorWeights[0]?.color ?? 'transparent';
|
|
65
|
+
}
|
|
66
|
+
const total = colorWeights.reduce((sum, {
|
|
67
|
+
weight
|
|
68
|
+
}) => sum + weight, 0);
|
|
69
|
+
let consumed = 0;
|
|
70
|
+
const stops = colorWeights.map(({
|
|
71
|
+
color,
|
|
72
|
+
weight
|
|
73
|
+
}) => {
|
|
74
|
+
const startDeg = consumed / total * 360;
|
|
75
|
+
consumed += weight;
|
|
76
|
+
const endDeg = consumed / total * 360;
|
|
77
|
+
return `${color} ${startDeg}deg ${endDeg}deg`;
|
|
78
|
+
});
|
|
79
|
+
return `conic-gradient(${stops.join(', ')})`;
|
|
80
|
+
};
|
|
81
|
+
const toCollapsedNode = (cluster, dimmed) => ({
|
|
82
|
+
colorWeights: toColorWeights(cluster.members.map(member => member.color)),
|
|
83
|
+
dimmed,
|
|
84
|
+
id: cluster.id,
|
|
85
|
+
leftPx: cluster.anchorPx,
|
|
86
|
+
markIds: cluster.members.map(member => member.id),
|
|
87
|
+
sizePx: cluster.members.length > 1 ? CLUSTER_DOT_SIZE_PX : DOT_SIZE_PX
|
|
88
|
+
});
|
|
89
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
90
|
+
const expandedSpacing = (memberCount, containerWidth, maxBandWidth) => {
|
|
91
|
+
if (memberCount <= 1) {
|
|
92
|
+
return EXPANDED_SPACING_PX;
|
|
93
|
+
}
|
|
94
|
+
const cap = Math.min(containerWidth * MAX_BAND_RATIO, maxBandWidth);
|
|
95
|
+
const fitted = (cap - DOT_SIZE_PX - 2 * BAND_PADDING_PX) / (memberCount - 1);
|
|
96
|
+
return Math.max(0, Math.min(EXPANDED_SPACING_PX, fitted));
|
|
97
|
+
};
|
|
98
|
+
const rescale = (value, fromStart, fromEnd, toStart, toEnd) => {
|
|
99
|
+
const fromSpan = fromEnd - fromStart;
|
|
100
|
+
if (fromSpan <= 0) {
|
|
101
|
+
return toStart;
|
|
102
|
+
}
|
|
103
|
+
return toStart + (value - fromStart) / fromSpan * (toEnd - toStart);
|
|
104
|
+
};
|
|
105
|
+
const buildLayout = ({
|
|
106
|
+
containerWidth,
|
|
107
|
+
expandedClusterId,
|
|
108
|
+
marks,
|
|
109
|
+
thresholdPx = DEFAULT_CLUSTER_THRESHOLD_PX
|
|
110
|
+
}) => {
|
|
111
|
+
if (containerWidth <= 0 || marks.length === 0) {
|
|
112
|
+
return {
|
|
113
|
+
nodes: []
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const clusters = buildClusters(marks, containerWidth, thresholdPx);
|
|
117
|
+
const expanded = clusters.find(cluster => cluster.id === expandedClusterId && cluster.members.length > 1);
|
|
118
|
+
if (expanded === undefined) {
|
|
119
|
+
return {
|
|
120
|
+
nodes: clusters.map(cluster => toCollapsedNode(cluster, false))
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const sideClusters = clusters.filter(cluster => cluster.id !== expanded.id);
|
|
124
|
+
const leftCount = sideClusters.filter(cluster => cluster.anchorPx < expanded.anchorPx).length;
|
|
125
|
+
const rightCount = sideClusters.length - leftCount;
|
|
126
|
+
const reserve = count => count === 0 ? 0 : SQUEEZE_GAP_PX + (count - 1) * MIN_SQUEEZE_PITCH_PX;
|
|
127
|
+
const leftReserve = reserve(leftCount);
|
|
128
|
+
const rightReserve = reserve(rightCount);
|
|
129
|
+
const minBandWidth = DOT_SIZE_PX + 2 * BAND_PADDING_PX;
|
|
130
|
+
const maxBandWidth = Math.max(minBandWidth, containerWidth - leftReserve - rightReserve);
|
|
131
|
+
const spacing = expandedSpacing(expanded.members.length, containerWidth, maxBandWidth);
|
|
132
|
+
const widthPx = Math.min(maxBandWidth, (expanded.members.length - 1) * spacing + minBandWidth);
|
|
133
|
+
const leftPx = clamp(expanded.anchorPx - widthPx / 2, leftReserve, Math.max(leftReserve, containerWidth - widthPx - rightReserve));
|
|
134
|
+
const band = {
|
|
135
|
+
leftPx,
|
|
136
|
+
memberCount: expanded.members.length,
|
|
137
|
+
widthPx
|
|
138
|
+
};
|
|
139
|
+
const leftLimit = Math.max(0, leftPx - SQUEEZE_GAP_PX);
|
|
140
|
+
const rightLimit = Math.min(containerWidth, leftPx + widthPx + SQUEEZE_GAP_PX);
|
|
141
|
+
const nodes = clusters.flatMap(cluster => {
|
|
142
|
+
if (cluster.id === expanded.id) {
|
|
143
|
+
return cluster.members.map((member, index) => ({
|
|
144
|
+
colorWeights: [{
|
|
145
|
+
color: member.color,
|
|
146
|
+
weight: 1
|
|
147
|
+
}],
|
|
148
|
+
dimmed: false,
|
|
149
|
+
id: member.id,
|
|
150
|
+
leftPx: leftPx + BAND_PADDING_PX + DOT_SIZE_PX / 2 + index * spacing,
|
|
151
|
+
markIds: [member.id],
|
|
152
|
+
sizePx: DOT_SIZE_PX
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
const collapsed = toCollapsedNode(cluster, true);
|
|
156
|
+
const squeezed = cluster.anchorPx < expanded.anchorPx ? rescale(cluster.anchorPx, 0, expanded.anchorPx, 0, leftLimit) : rescale(cluster.anchorPx, expanded.anchorPx, containerWidth, rightLimit, containerWidth);
|
|
157
|
+
return [{
|
|
158
|
+
...collapsed,
|
|
159
|
+
leftPx: squeezed
|
|
160
|
+
}];
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
band,
|
|
164
|
+
nodes
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export { BAND_PADDING_PX, CLUSTER_DOT_SIZE_PX, DEFAULT_CLUSTER_THRESHOLD_PX, DIMMED_OPACITY, DOT_SIZE_PX, EXPANDED_SPACING_PX, MAX_BAND_RATIO, MIN_SQUEEZE_PITCH_PX, SQUEEZE_GAP_PX, buildClusters, buildLayout, normalizePositions, toColorWeights, toConicGradient };
|