@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.
Files changed (57) hide show
  1. package/components/PhotosCarousel/PhotoSlide.d.ts +10 -0
  2. package/components/PhotosCarousel/PhotoSlide.d.ts.map +1 -0
  3. package/components/PhotosCarousel/PhotoSlide.js +36 -0
  4. package/components/PhotosCarousel/index.d.ts +2 -2
  5. package/components/PhotosCarousel/index.d.ts.map +1 -1
  6. package/components/PhotosCarousel/index.js +72 -83
  7. package/components/PhotosCarousel/styles.scss +100 -13
  8. package/components/PhotosCarousel/usePhotosCarousel.d.ts +15 -0
  9. package/components/PhotosCarousel/usePhotosCarousel.d.ts.map +1 -0
  10. package/components/PhotosCarousel/usePhotosCarousel.js +79 -0
  11. package/components/TimelineSliderV2/layout.d.ts +16 -0
  12. package/components/TimelineSliderV2/layout.d.ts.map +1 -0
  13. package/components/TimelineSliderV2/layout.js +168 -0
  14. package/components/TimelineSliderV2/types.d.ts +43 -0
  15. package/components/TimelineSliderV2/types.d.ts.map +1 -0
  16. package/components/TimelineSliderV2/types.js +1 -0
  17. package/components/ViewPhotosDialog/index.d.ts +0 -2
  18. package/components/ViewPhotosDialog/index.d.ts.map +1 -1
  19. package/components/ViewPhotosDialog/index.js +2 -18
  20. package/components/VirtualWalkthrough/AnnotationPanel.js +2 -2
  21. package/license-report.html +1 -1
  22. package/package.json +2 -2
  23. package/_vendor/react-multi-carousel/index.js +0 -13
  24. package/_vendor/react-multi-carousel/lib/Arrows.js +0 -13
  25. package/_vendor/react-multi-carousel/lib/Carousel.js +0 -19
  26. package/_vendor/react-multi-carousel/lib/CarouselItems.js +0 -14
  27. package/_vendor/react-multi-carousel/lib/Dots.js +0 -16
  28. package/_vendor/react-multi-carousel/lib/index.js +0 -13
  29. package/_vendor/react-multi-carousel/lib/types.js +0 -13
  30. package/_vendor/react-multi-carousel/lib/utils/clones.js +0 -14
  31. package/_vendor/react-multi-carousel/lib/utils/common.js +0 -13
  32. package/_vendor/react-multi-carousel/lib/utils/dots.js +0 -14
  33. package/_vendor/react-multi-carousel/lib/utils/elementWidth.js +0 -12
  34. package/_vendor/react-multi-carousel/lib/utils/index.js +0 -20
  35. package/_vendor/react-multi-carousel/lib/utils/mouseOrTouchMove.js +0 -14
  36. package/_vendor/react-multi-carousel/lib/utils/next.js +0 -13
  37. package/_vendor/react-multi-carousel/lib/utils/previous.js +0 -14
  38. package/_vendor/react-multi-carousel/lib/utils/throttle.js +0 -12
  39. package/_vendor/react-multi-carousel/lib/utils/throwError.js +0 -14
  40. package/_virtual/Arrows.js +0 -3
  41. package/_virtual/Carousel.js +0 -3
  42. package/_virtual/CarouselItems.js +0 -3
  43. package/_virtual/Dots.js +0 -3
  44. package/_virtual/_commonjsHelpers.js +0 -5
  45. package/_virtual/clones.js +0 -3
  46. package/_virtual/common.js +0 -3
  47. package/_virtual/dots2.js +0 -3
  48. package/_virtual/elementWidth.js +0 -3
  49. package/_virtual/index.js +0 -7
  50. package/_virtual/index2.js +0 -3
  51. package/_virtual/index3.js +0 -3
  52. package/_virtual/mouseOrTouchMove.js +0 -3
  53. package/_virtual/next.js +0 -3
  54. package/_virtual/previous.js +0 -3
  55. package/_virtual/throttle.js +0 -3
  56. package/_virtual/throwError.js +0 -3
  57. 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
- export default function PhotosCarousel(props: PhotosCarouselProps): JSX.Element;
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,EAA4C,MAAM,OAAO,CAAC;AAElF,OAAO,qCAAqC,CAAC;AAK7C,OAAO,eAAe,CAAC;AAUvB,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;AASD,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,GAAG,CAAC,OAAO,CAsE9E"}
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 { useState, useRef, useEffect, useCallback } from 'react';
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 BusySpinner from '../BusySpinner/index.js';
3
+ import Icon from '../Icon/Icon.js';
4
+ import PhotoSlide from './PhotoSlide.js';
6
5
  import './styles.scss';
7
- import { jsxs, jsx } from 'react/jsx-runtime';
6
+ import usePhotosCarousel from './usePhotosCarousel.js';
7
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
8
8
 
9
- const Carousel = CarouselImport.default ?? CarouselImport;
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
- selectedSlide,
23
- onSlideChange,
24
- showArrows,
12
+ showArrows = false,
25
13
  numbered,
26
- dots
14
+ dots = true
27
15
  } = props;
28
- const isControlled = selectedSlide !== undefined;
29
- const [internalSlide, setInternalSlide] = useState(0);
30
- const [isLoading, setIsLoading] = useState([]);
31
- const myCarousel = useRef(null);
32
- const currentSlide = isControlled ? selectedSlide : internalSlide;
33
-
34
- // Keyed on the URLs (not just photos.length) so swapping in a same-length array of
35
- // different photos still resets the loading state instead of leaving stale flags.
36
- const photoKey = photos.map(p => p.url).join('|');
37
- useEffect(() => {
38
- setIsLoading(new Array(photos.length).fill(true));
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
- sx: {
63
- '& .react-multi-carousel-list': {
64
- paddingBottom: '20px'
65
- }
66
- },
67
- children: [/*#__PURE__*/jsx(Carousel, {
68
- responsive: responsive,
69
- ref: myCarousel,
70
- showDots: dots ?? true,
71
- arrows: showArrows ?? false,
72
- ssr: true,
73
- afterChange: handleAfterChange,
74
- children: photos.map((p, i) => /*#__PURE__*/jsxs("div", {
75
- className: "photos-carousel-container",
76
- children: [isLoading[i] ? /*#__PURE__*/jsx(BusySpinner, {
77
- noBackground: true
78
- }) : undefined, /*#__PURE__*/jsx("a", {
79
- href: p.url,
80
- target: "_blank",
81
- rel: "noopener noreferrer",
82
- children: /*#__PURE__*/jsx("img", {
83
- className: "photos-carousel-image",
84
- src: p.url,
85
- alt: p.alt,
86
- onLoad: () => finishLoading(i)
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
- }, `photo-${i}-container`))
90
- }), numbered ? /*#__PURE__*/jsx(Typography, {
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
- }) : undefined, photos[currentSlide] && photos[currentSlide].decoration]
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-container {
4
- min-height: 200px;
5
- background: terraware.$tw-clr-bg;
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
- .photos-carousel-image {
9
- object-fit: contain;
10
- margin: auto;
11
- height: 400px;
12
- width: 100%;
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
- font-size: 12px;
17
- color: terraware.$tw-clr-txt-info;
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 };