@coinbase/cds-mobile 8.38.7 → 8.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dts/carousel/Carousel.d.ts +16 -11
- package/dts/carousel/Carousel.d.ts.map +1 -1
- package/dts/carousel/CarouselContext.d.ts +13 -0
- package/dts/carousel/CarouselContext.d.ts.map +1 -0
- package/dts/carousel/CarouselItem.d.ts +1 -1
- package/dts/carousel/CarouselItem.d.ts.map +1 -1
- package/dts/carousel/index.d.ts +1 -0
- package/dts/carousel/index.d.ts.map +1 -1
- package/esm/carousel/Carousel.js +298 -50
- package/esm/carousel/CarouselContext.js +9 -0
- package/esm/carousel/CarouselItem.js +1 -1
- package/esm/carousel/__stories__/Carousel.stories.js +76 -1
- package/esm/carousel/index.js +1 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.
|
|
|
8
8
|
|
|
9
9
|
<!-- template-start -->
|
|
10
10
|
|
|
11
|
+
## 8.39.0 (1/27/2026 PST)
|
|
12
|
+
|
|
13
|
+
#### 🚀 Updates
|
|
14
|
+
|
|
15
|
+
- Support Carousel looping. [[#327](https://github.com/coinbase/cds/pull/327)]
|
|
16
|
+
|
|
11
17
|
## 8.38.7 (1/26/2026 PST)
|
|
12
18
|
|
|
13
19
|
#### 🐞 Fixes
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type StyleProp, type TextStyle, type ViewStyle } from 'react-native';
|
|
3
|
-
import type {
|
|
3
|
+
import type { SharedAccessibilityProps, SharedProps } from '@coinbase/cds-common/types';
|
|
4
4
|
import { type BoxBaseProps, type BoxProps } from '../layout/Box';
|
|
5
|
+
import { CarouselContext, type CarouselContextValue, useCarouselContext } from './CarouselContext';
|
|
5
6
|
export type CarouselItemRenderChildren = React.FC<{
|
|
6
7
|
isVisible: boolean;
|
|
7
8
|
}>;
|
|
@@ -20,16 +21,8 @@ export type CarouselItemBaseProps = Omit<BoxBaseProps, 'children'> &
|
|
|
20
21
|
export type CarouselItemProps = Omit<BoxProps, 'children'> & CarouselItemBaseProps;
|
|
21
22
|
export type CarouselItemComponent = React.FC<CarouselItemProps>;
|
|
22
23
|
export type CarouselItemElement = React.ReactElement<CarouselItemProps, CarouselItemComponent>;
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
unregisterItem: (id: string) => void;
|
|
26
|
-
/**
|
|
27
|
-
* Set of item IDs that are currently visible in the carousel viewport.
|
|
28
|
-
*/
|
|
29
|
-
visibleCarouselItems: Set<string>;
|
|
30
|
-
};
|
|
31
|
-
export declare const CarouselContext: React.Context<CarouselContextValue | undefined>;
|
|
32
|
-
export declare const useCarouselContext: () => CarouselContextValue;
|
|
24
|
+
export { CarouselContext, useCarouselContext };
|
|
25
|
+
export type { CarouselContextValue };
|
|
33
26
|
export type CarouselNavigationComponentBaseProps = {
|
|
34
27
|
/**
|
|
35
28
|
* Callback for when the previous button is pressed.
|
|
@@ -180,6 +173,12 @@ export type CarouselBaseProps = SharedProps &
|
|
|
180
173
|
* Callback fired when the user ends dragging the carousel.
|
|
181
174
|
*/
|
|
182
175
|
onDragEnd?: () => void;
|
|
176
|
+
/**
|
|
177
|
+
* Enables infinite looping. When true, the carousel will seamlessly
|
|
178
|
+
* loop from the last item back to the first.
|
|
179
|
+
* @note Requires at least 2 pages worth of content to function.
|
|
180
|
+
*/
|
|
181
|
+
loop?: boolean;
|
|
183
182
|
};
|
|
184
183
|
export type CarouselProps = CarouselBaseProps & {
|
|
185
184
|
/**
|
|
@@ -312,6 +311,12 @@ export declare const Carousel: React.MemoExoticComponent<
|
|
|
312
311
|
* Callback fired when the user ends dragging the carousel.
|
|
313
312
|
*/
|
|
314
313
|
onDragEnd?: () => void;
|
|
314
|
+
/**
|
|
315
|
+
* Enables infinite looping. When true, the carousel will seamlessly
|
|
316
|
+
* loop from the last item back to the first.
|
|
317
|
+
* @note Requires at least 2 pages worth of content to function.
|
|
318
|
+
*/
|
|
319
|
+
loop?: boolean;
|
|
315
320
|
} & {
|
|
316
321
|
/**
|
|
317
322
|
* Custom styles for the root element.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../src/carousel/Carousel.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../src/carousel/Carousel.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Rect } from '@coinbase/cds-common/types';
|
|
3
|
+
export type CarouselContextValue = {
|
|
4
|
+
registerItem: (id: string, state: Rect) => void;
|
|
5
|
+
unregisterItem: (id: string) => void;
|
|
6
|
+
/**
|
|
7
|
+
* Set of item IDs that are currently visible in the carousel viewport.
|
|
8
|
+
*/
|
|
9
|
+
visibleCarouselItems: Set<string>;
|
|
10
|
+
};
|
|
11
|
+
export declare const CarouselContext: React.Context<CarouselContextValue | undefined>;
|
|
12
|
+
export declare const useCarouselContext: () => CarouselContextValue;
|
|
13
|
+
//# sourceMappingURL=CarouselContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CarouselContext.d.ts","sourceRoot":"","sources":["../../src/carousel/CarouselContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAqB,MAAM,OAAO,CAAC;AAC1C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,KAAK,IAAI,CAAC;IAChD,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;OAEG;IACH,oBAAoB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnC,CAAC;AAEF,eAAO,MAAM,eAAe,iDAAmE,CAAC;AAEhG,eAAO,MAAM,kBAAkB,QAAO,oBAMrC,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import type { CarouselItemProps } from './Carousel';
|
|
3
3
|
export declare const CarouselItem: React.MemoExoticComponent<
|
|
4
4
|
({ children, id, style, ...props }: CarouselItemProps) => import('react/jsx-runtime').JSX.Element
|
|
5
5
|
>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CarouselItem.d.ts","sourceRoot":"","sources":["../../src/carousel/CarouselItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAK5D,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"CarouselItem.d.ts","sourceRoot":"","sources":["../../src/carousel/CarouselItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAK5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGpD,eAAO,MAAM,YAAY,gEAA4C,iBAAiB,6CAwCpF,CAAC"}
|
package/dts/carousel/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/carousel/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/carousel/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC"}
|
package/esm/carousel/Carousel.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const _excluded = ["children", "title", "hideNavigation", "hidePagination", "drag", "snapMode", "NavigationComponent", "PaginationComponent", "style", "styles", "nextPageAccessibilityLabel", "previousPageAccessibilityLabel", "goToPageAccessibilityLabel", "onChangePage", "onDragStart", "onDragEnd"];
|
|
1
|
+
const _excluded = ["children", "title", "hideNavigation", "hidePagination", "drag", "snapMode", "NavigationComponent", "PaginationComponent", "style", "styles", "nextPageAccessibilityLabel", "previousPageAccessibilityLabel", "goToPageAccessibilityLabel", "onChangePage", "onDragStart", "onDragEnd", "loop"];
|
|
2
2
|
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
|
|
3
3
|
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
4
|
-
import React, { forwardRef, memo, useCallback,
|
|
4
|
+
import React, { forwardRef, memo, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { View } from 'react-native';
|
|
6
6
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
7
7
|
import { animated, useSpring } from '@react-spring/native';
|
|
@@ -9,24 +9,40 @@ import { useLayout } from '../hooks/useLayout';
|
|
|
9
9
|
import { HStack } from '../layout/HStack';
|
|
10
10
|
import { VStack } from '../layout/VStack';
|
|
11
11
|
import { Text } from '../typography/Text';
|
|
12
|
+
import { CarouselContext, useCarouselContext } from './CarouselContext';
|
|
13
|
+
import { CarouselItem } from './CarouselItem';
|
|
12
14
|
import { DefaultCarouselNavigation } from './DefaultCarouselNavigation';
|
|
13
15
|
import { DefaultCarouselPagination } from './DefaultCarouselPagination';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a value within a range (min, max) for circular indexing.
|
|
19
|
+
* @param min - The minimum value of the range.
|
|
20
|
+
* @param max - The maximum value of the range (exclusive).
|
|
21
|
+
* @param value - The value to wrap.
|
|
22
|
+
* @returns The wrapped value within the range.
|
|
23
|
+
*/
|
|
14
24
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (!context) {
|
|
19
|
-
throw new Error('useCarouselContext must be used within a Carousel component');
|
|
20
|
-
}
|
|
21
|
-
return context;
|
|
25
|
+
const wrap = (min, max, value) => {
|
|
26
|
+
const range = max - min;
|
|
27
|
+
return min + ((value - min) % range + range) % range;
|
|
22
28
|
};
|
|
29
|
+
export { CarouselContext, useCarouselContext };
|
|
23
30
|
/**
|
|
24
31
|
* Calculates the locations of each item in the carousel, offset from the first item.
|
|
25
32
|
* @param itemRects - The items to get the offsets for.
|
|
26
33
|
* @returns The item offsets.
|
|
27
34
|
*/
|
|
28
35
|
const getItemOffsets = itemRects => {
|
|
29
|
-
|
|
36
|
+
// Filter out clone items (they have IDs starting with "clone-")
|
|
37
|
+
const originalItems = Object.entries(itemRects).filter(_ref => {
|
|
38
|
+
let [id] = _ref;
|
|
39
|
+
return !id.startsWith('clone-');
|
|
40
|
+
}).map(_ref2 => {
|
|
41
|
+
let [, rect] = _ref2;
|
|
42
|
+
return rect;
|
|
43
|
+
});
|
|
44
|
+
if (originalItems.length === 0) return [];
|
|
45
|
+
const sortedItems = originalItems.sort((a, b) => a.x - b.x);
|
|
30
46
|
const initialItemOffset = sortedItems[0].x;
|
|
31
47
|
return sortedItems.map(item => _extends({}, item, {
|
|
32
48
|
x: item.x - initialItemOffset
|
|
@@ -52,14 +68,61 @@ const getNearestPageIndexFromOffset = (scrollOffset, pageOffsets) => {
|
|
|
52
68
|
return closestPageIndex;
|
|
53
69
|
};
|
|
54
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Finds the nearest offset from a set of candidate offsets, considering loop cycles.
|
|
73
|
+
* Checks current, previous, and next cycles to find the shortest path.
|
|
74
|
+
* @param currentOffset - The current scroll offset.
|
|
75
|
+
* @param candidateOffsets - Array of candidate offsets within a single loop cycle.
|
|
76
|
+
* @param loopLength - The total length of one loop cycle.
|
|
77
|
+
* @returns The nearest offset and its index in the candidates array.
|
|
78
|
+
*/
|
|
79
|
+
const findNearestLoopOffset = (currentOffset, candidateOffsets, loopLength) => {
|
|
80
|
+
const currentCycle = Math.floor(currentOffset / loopLength);
|
|
81
|
+
let nearest = {
|
|
82
|
+
offset: 0,
|
|
83
|
+
index: 0,
|
|
84
|
+
distance: Infinity
|
|
85
|
+
};
|
|
86
|
+
for (const [index, candidateOffset] of candidateOffsets.entries()) {
|
|
87
|
+
for (const cycle of [currentCycle - 1, currentCycle, currentCycle + 1]) {
|
|
88
|
+
const cycleOffset = cycle * loopLength + candidateOffset;
|
|
89
|
+
const distance = Math.abs(currentOffset - cycleOffset);
|
|
90
|
+
if (distance < nearest.distance) {
|
|
91
|
+
nearest = {
|
|
92
|
+
offset: cycleOffset,
|
|
93
|
+
index,
|
|
94
|
+
distance
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
offset: nearest.offset,
|
|
101
|
+
index: nearest.index
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
55
105
|
/**
|
|
56
106
|
* Calculates the offsets for a given set of items grouped by item.
|
|
107
|
+
* @note when looping, all items have a page offset, otherwise we find
|
|
108
|
+
* the last item that can start a page and still show all remaining items.
|
|
57
109
|
* @param items - The items to get the page offsets for.
|
|
58
110
|
* @param containerWidth - The width of the container.
|
|
59
111
|
* @param maxScrollOffset - The maximum scroll offset.
|
|
112
|
+
* @param loop - Whether looping is enabled.
|
|
60
113
|
* @returns The page offsets and the total number of pages.
|
|
61
114
|
*/
|
|
62
|
-
const getSnapItemPageOffsets = (items, containerWidth, maxScrollOffset) => {
|
|
115
|
+
const getSnapItemPageOffsets = (items, containerWidth, maxScrollOffset, loop) => {
|
|
116
|
+
if (loop) {
|
|
117
|
+
const offsets = [];
|
|
118
|
+
for (let i = 0; i < items.length; i++) {
|
|
119
|
+
offsets.push(items[i].x);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
totalPages: offsets.length,
|
|
123
|
+
pageOffsets: offsets
|
|
124
|
+
};
|
|
125
|
+
}
|
|
63
126
|
let lastPageStartIndex = items.length - 1;
|
|
64
127
|
const lastItem = items[lastPageStartIndex];
|
|
65
128
|
const lastItemsEndPosition = lastItem.x + lastItem.width;
|
|
@@ -146,6 +209,24 @@ const clampWithElasticResistance = function (offset, maxScrollOffset, elasticAmo
|
|
|
146
209
|
return offset;
|
|
147
210
|
};
|
|
148
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Calculates how many items need to be cloned for looping to fill the viewport.
|
|
214
|
+
* For backward clones, pass the items array reversed.
|
|
215
|
+
* @param items - The item rects sorted by position (or reversed for backward clones).
|
|
216
|
+
* @param containerWidth - The width of the container viewport.
|
|
217
|
+
* @returns The number of items to clone.
|
|
218
|
+
*/
|
|
219
|
+
const getCloneCount = (items, containerWidth) => {
|
|
220
|
+
let widthSum = 0;
|
|
221
|
+
let count = 0;
|
|
222
|
+
for (const item of items) {
|
|
223
|
+
widthSum += item.width;
|
|
224
|
+
count++;
|
|
225
|
+
if (widthSum >= containerWidth) break;
|
|
226
|
+
}
|
|
227
|
+
return Math.max(1, count);
|
|
228
|
+
};
|
|
229
|
+
|
|
149
230
|
/**
|
|
150
231
|
* Calculates which items are visible in the carousel based on scroll offset and viewport.
|
|
151
232
|
* @param itemRects - The items to get the visibility for.
|
|
@@ -157,8 +238,8 @@ const getVisibleItems = (itemRects, containerWidth, scrollOffset) => {
|
|
|
157
238
|
const visibleItems = new Set();
|
|
158
239
|
const viewportLeft = scrollOffset;
|
|
159
240
|
const viewportRight = scrollOffset + containerWidth;
|
|
160
|
-
Object.entries(itemRects).forEach(
|
|
161
|
-
let [itemId, rect] =
|
|
241
|
+
Object.entries(itemRects).forEach(_ref3 => {
|
|
242
|
+
let [itemId, rect] = _ref3;
|
|
162
243
|
const itemLeft = rect.x;
|
|
163
244
|
const itemRight = rect.x + rect.width;
|
|
164
245
|
const isVisible = itemLeft < viewportRight && itemRight > viewportLeft;
|
|
@@ -172,7 +253,7 @@ const animationConfig = {
|
|
|
172
253
|
tension: 200,
|
|
173
254
|
friction: 25
|
|
174
255
|
};
|
|
175
|
-
export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((
|
|
256
|
+
export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref4, ref) => {
|
|
176
257
|
let {
|
|
177
258
|
children,
|
|
178
259
|
title,
|
|
@@ -189,9 +270,10 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
|
|
|
189
270
|
goToPageAccessibilityLabel,
|
|
190
271
|
onChangePage,
|
|
191
272
|
onDragStart,
|
|
192
|
-
onDragEnd
|
|
193
|
-
|
|
194
|
-
|
|
273
|
+
onDragEnd,
|
|
274
|
+
loop
|
|
275
|
+
} = _ref4,
|
|
276
|
+
props = _objectWithoutPropertiesLoose(_ref4, _excluded);
|
|
195
277
|
const carouselScrollX = useRef(0);
|
|
196
278
|
const animationApi = useSpring({
|
|
197
279
|
x: carouselScrollX.current,
|
|
@@ -218,15 +300,83 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
|
|
|
218
300
|
const lastItem = items[items.length - 1];
|
|
219
301
|
return lastItem.x + lastItem.width;
|
|
220
302
|
}, [carouselItemRects]);
|
|
303
|
+
const maxScrollOffset = Math.max(0, contentWidth - containerSize.width);
|
|
304
|
+
const hasCalculatedDimensions = contentWidth > 0 && containerSize.width > 0;
|
|
305
|
+
|
|
306
|
+
// Calculate gap between items (needed for loopLength to maintain consistent spacing at wrap seam)
|
|
307
|
+
const gap = useMemo(() => {
|
|
308
|
+
if (Object.keys(carouselItemRects).length < 2) return 0;
|
|
309
|
+
const items = getItemOffsets(carouselItemRects);
|
|
310
|
+
const firstItemEnd = items[0].x + items[0].width;
|
|
311
|
+
const secondItemStart = items[1].x;
|
|
312
|
+
return Math.max(0, secondItemStart - firstItemEnd);
|
|
313
|
+
}, [carouselItemRects]);
|
|
314
|
+
const shouldLoop = useMemo(() => loop && hasCalculatedDimensions && maxScrollOffset > 0, [loop, hasCalculatedDimensions, maxScrollOffset]);
|
|
315
|
+
const loopLength = useMemo(() => {
|
|
316
|
+
if (!shouldLoop) return 0;
|
|
317
|
+
return contentWidth + gap;
|
|
318
|
+
}, [shouldLoop, contentWidth, gap]);
|
|
319
|
+
const isLoopingActive = shouldLoop && loopLength > 0;
|
|
320
|
+
|
|
321
|
+
// Calculate how many items to clone for each direction (enough to fill viewport)
|
|
322
|
+
const cloneCounts = useMemo(() => {
|
|
323
|
+
if (!shouldLoop || Object.keys(carouselItemRects).length === 0 || containerSize.width === 0) {
|
|
324
|
+
return {
|
|
325
|
+
forward: 0,
|
|
326
|
+
backward: 0
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const items = getItemOffsets(carouselItemRects);
|
|
330
|
+
return {
|
|
331
|
+
forward: getCloneCount(items, containerSize.width),
|
|
332
|
+
backward: getCloneCount([...items].reverse(), containerSize.width)
|
|
333
|
+
};
|
|
334
|
+
}, [shouldLoop, carouselItemRects, containerSize.width]);
|
|
221
335
|
const updateVisibleCarouselItems = useCallback(scrollOffset => {
|
|
222
336
|
if (containerSize.width === 0) {
|
|
223
337
|
setVisibleCarouselItems(new Set());
|
|
224
338
|
return;
|
|
225
339
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
340
|
+
|
|
341
|
+
// For original items: wrap the offset to check visibility within one cycle
|
|
342
|
+
const adjustedOffset = isLoopingActive ? (scrollOffset % loopLength + loopLength) % loopLength : scrollOffset;
|
|
343
|
+
const visibleItems = getVisibleItems(carouselItemRects, containerSize.width, adjustedOffset);
|
|
344
|
+
|
|
345
|
+
// For clones: check visibility against actual (unwrapped) scroll position
|
|
346
|
+
if (isLoopingActive && children) {
|
|
347
|
+
const childrenArray = React.Children.toArray(children);
|
|
348
|
+
const items = getItemOffsets(carouselItemRects);
|
|
349
|
+
const viewportLeft = scrollOffset;
|
|
350
|
+
const viewportRight = scrollOffset + containerSize.width;
|
|
351
|
+
|
|
352
|
+
// Check backward clones visibility
|
|
353
|
+
const backwardStartIndex = childrenArray.length - cloneCounts.backward;
|
|
354
|
+
for (let i = 0; i < cloneCounts.backward; i++) {
|
|
355
|
+
const originalIndex = backwardStartIndex + i;
|
|
356
|
+
const itemData = items[originalIndex];
|
|
357
|
+
if (itemData) {
|
|
358
|
+
const cloneX = itemData.x - loopLength;
|
|
359
|
+
const cloneRight = cloneX + itemData.width;
|
|
360
|
+
if (cloneX < viewportRight && cloneRight > viewportLeft) {
|
|
361
|
+
visibleItems.add("clone-backward-" + childrenArray[originalIndex].props.id);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Check forward clones visibility
|
|
367
|
+
for (let i = 0; i < cloneCounts.forward; i++) {
|
|
368
|
+
const itemData = items[i];
|
|
369
|
+
if (itemData) {
|
|
370
|
+
const cloneX = itemData.x + loopLength;
|
|
371
|
+
const cloneRight = cloneX + itemData.width;
|
|
372
|
+
if (cloneX < viewportRight && cloneRight > viewportLeft) {
|
|
373
|
+
visibleItems.add("clone-forward-" + childrenArray[i].props.id);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
setVisibleCarouselItems(visibleItems);
|
|
379
|
+
}, [carouselItemRects, containerSize.width, isLoopingActive, loopLength, children, cloneCounts]);
|
|
230
380
|
|
|
231
381
|
// Calculate pages and their offsets based on snapMode
|
|
232
382
|
const {
|
|
@@ -241,29 +391,37 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
|
|
|
241
391
|
}
|
|
242
392
|
let pageOffsets;
|
|
243
393
|
if (snapMode === 'item') {
|
|
244
|
-
pageOffsets = getSnapItemPageOffsets(getItemOffsets(carouselItemRects), containerSize.width, maxScrollOffset);
|
|
394
|
+
pageOffsets = getSnapItemPageOffsets(getItemOffsets(carouselItemRects), containerSize.width, maxScrollOffset, shouldLoop);
|
|
245
395
|
} else {
|
|
246
396
|
pageOffsets = getSnapPageOffsets(getItemOffsets(carouselItemRects), containerSize.width, maxScrollOffset);
|
|
247
397
|
}
|
|
248
398
|
updateActivePageIndex(pageIndex => Math.min(pageIndex, pageOffsets.totalPages - 1));
|
|
249
399
|
return pageOffsets;
|
|
250
|
-
}, [hasCalculatedDimensions, carouselItemRects, snapMode, containerSize.width, maxScrollOffset, updateActivePageIndex]);
|
|
400
|
+
}, [hasCalculatedDimensions, carouselItemRects, snapMode, containerSize.width, maxScrollOffset, shouldLoop, updateActivePageIndex]);
|
|
251
401
|
const goToPage = useCallback(page => {
|
|
252
402
|
const newPage = Math.max(0, Math.min(totalPages - 1, page));
|
|
253
403
|
updateActivePageIndex(newPage);
|
|
254
|
-
|
|
404
|
+
updateVisibleCarouselItems(pageOffsets[newPage]);
|
|
405
|
+
const targetOffset = isLoopingActive ? findNearestLoopOffset(carouselScrollX.current, [pageOffsets[newPage]], loopLength).offset : pageOffsets[newPage];
|
|
255
406
|
carouselScrollX.current = targetOffset;
|
|
256
|
-
updateVisibleCarouselItems(targetOffset);
|
|
257
407
|
animationApi.x.start({
|
|
258
408
|
to: targetOffset,
|
|
259
409
|
config: animationConfig
|
|
260
410
|
});
|
|
261
|
-
}, [totalPages, pageOffsets,
|
|
411
|
+
}, [isLoopingActive, loopLength, totalPages, pageOffsets, animationApi.x, updateVisibleCarouselItems, updateActivePageIndex]);
|
|
262
412
|
useImperativeHandle(ref, () => ({
|
|
263
413
|
activePageIndex,
|
|
264
414
|
totalPages,
|
|
265
415
|
goToPage
|
|
266
416
|
}), [activePageIndex, totalPages, goToPage]);
|
|
417
|
+
const handleGoNext = useCallback(() => {
|
|
418
|
+
const nextPage = shouldLoop ? wrap(0, totalPages, activePageIndex + 1) : activePageIndex + 1;
|
|
419
|
+
goToPage(nextPage);
|
|
420
|
+
}, [shouldLoop, totalPages, activePageIndex, goToPage]);
|
|
421
|
+
const handleGoPrevious = useCallback(() => {
|
|
422
|
+
const prevPage = shouldLoop ? wrap(0, totalPages, activePageIndex - 1) : activePageIndex - 1;
|
|
423
|
+
goToPage(prevPage);
|
|
424
|
+
}, [shouldLoop, totalPages, activePageIndex, goToPage]);
|
|
267
425
|
const handleDragStart = useCallback(() => {
|
|
268
426
|
onDragStart == null || onDragStart();
|
|
269
427
|
}, [onDragStart]);
|
|
@@ -272,36 +430,68 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
|
|
|
272
430
|
}, [onDragEnd]);
|
|
273
431
|
const handleDragTransition = useCallback(targetOffsetScroll => {
|
|
274
432
|
if (drag === 'none') return targetOffsetScroll;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
433
|
+
if (isLoopingActive) {
|
|
434
|
+
const {
|
|
435
|
+
offset: nearestOffset,
|
|
436
|
+
index: pageIndex
|
|
437
|
+
} = findNearestLoopOffset(targetOffsetScroll, pageOffsets, loopLength);
|
|
438
|
+
updateActivePageIndex(pageIndex);
|
|
439
|
+
if (drag === 'snap') {
|
|
440
|
+
updateVisibleCarouselItems(pageOffsets[pageIndex]);
|
|
441
|
+
return nearestOffset;
|
|
442
|
+
}
|
|
443
|
+
const currentCycle = Math.floor(targetOffsetScroll / loopLength);
|
|
444
|
+
const localOffset = targetOffsetScroll - currentCycle * loopLength;
|
|
445
|
+
updateVisibleCarouselItems(localOffset);
|
|
446
|
+
return targetOffsetScroll;
|
|
447
|
+
} else {
|
|
448
|
+
// Non-looping logic with clamping
|
|
449
|
+
const clampedScrollOffset = clampWithElasticResistance(targetOffsetScroll, maxScrollOffset, 0);
|
|
450
|
+
const closestPageIndex = getNearestPageIndexFromOffset(clampedScrollOffset, pageOffsets);
|
|
451
|
+
updateActivePageIndex(closestPageIndex);
|
|
452
|
+
if (drag === 'snap') {
|
|
453
|
+
const snapOffset = pageOffsets[closestPageIndex];
|
|
454
|
+
updateVisibleCarouselItems(snapOffset);
|
|
455
|
+
return snapOffset;
|
|
456
|
+
}
|
|
457
|
+
updateVisibleCarouselItems(clampedScrollOffset);
|
|
458
|
+
return targetOffsetScroll;
|
|
281
459
|
}
|
|
282
|
-
|
|
283
|
-
return targetOffsetScroll;
|
|
284
|
-
}, [drag, pageOffsets, updateVisibleCarouselItems, updateActivePageIndex]);
|
|
460
|
+
}, [drag, isLoopingActive, loopLength, maxScrollOffset, pageOffsets, updateVisibleCarouselItems, updateActivePageIndex]);
|
|
285
461
|
const panGesture = useMemo(() => Gesture.Pan().onStart(() => {
|
|
286
462
|
if (!isDragEnabled) return;
|
|
287
463
|
handleDragStart();
|
|
288
|
-
}).onUpdate(
|
|
464
|
+
}).onUpdate(_ref5 => {
|
|
289
465
|
let {
|
|
290
466
|
translationX
|
|
291
|
-
} =
|
|
467
|
+
} = _ref5;
|
|
292
468
|
if (!isDragEnabled) return;
|
|
293
|
-
|
|
469
|
+
let newOffset;
|
|
470
|
+
if (shouldLoop) {
|
|
471
|
+
newOffset = carouselScrollX.current - translationX;
|
|
472
|
+
} else {
|
|
473
|
+
newOffset = clampWithElasticResistance(carouselScrollX.current - translationX, maxScrollOffset);
|
|
474
|
+
}
|
|
294
475
|
animationApi.x.set(newOffset);
|
|
295
|
-
}).onEnd(
|
|
476
|
+
}).onEnd(_ref6 => {
|
|
296
477
|
let {
|
|
297
478
|
translationX,
|
|
298
479
|
velocityX
|
|
299
|
-
} =
|
|
480
|
+
} = _ref6;
|
|
300
481
|
if (!isDragEnabled) return;
|
|
301
|
-
let projectedOffset
|
|
482
|
+
let projectedOffset;
|
|
483
|
+
if (shouldLoop) {
|
|
484
|
+
projectedOffset = carouselScrollX.current - translationX;
|
|
485
|
+
} else {
|
|
486
|
+
projectedOffset = clampWithElasticResistance(carouselScrollX.current - translationX, maxScrollOffset);
|
|
487
|
+
}
|
|
302
488
|
const power = drag === 'free' ? 0.25 : 0.125;
|
|
303
489
|
const momentumDistance = velocityX * power;
|
|
304
|
-
|
|
490
|
+
if (shouldLoop) {
|
|
491
|
+
projectedOffset = projectedOffset - momentumDistance;
|
|
492
|
+
} else {
|
|
493
|
+
projectedOffset = clampWithElasticResistance(projectedOffset - momentumDistance, maxScrollOffset, 0);
|
|
494
|
+
}
|
|
305
495
|
const finalOffset = handleDragTransition(projectedOffset);
|
|
306
496
|
carouselScrollX.current = finalOffset;
|
|
307
497
|
animationApi.x.start({
|
|
@@ -309,7 +499,59 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
|
|
|
309
499
|
config: _extends({}, animationConfig)
|
|
310
500
|
});
|
|
311
501
|
handleDragEnd();
|
|
312
|
-
}).runOnJS(true), [isDragEnabled, maxScrollOffset, animationApi, drag, handleDragTransition, handleDragStart, handleDragEnd]);
|
|
502
|
+
}).runOnJS(true), [isDragEnabled, shouldLoop, maxScrollOffset, animationApi, drag, handleDragTransition, handleDragStart, handleDragEnd]);
|
|
503
|
+
const childrenWithClones = useMemo(() => {
|
|
504
|
+
if (!loop) return children;
|
|
505
|
+
const childrenArray = React.Children.toArray(children);
|
|
506
|
+
if (childrenArray.length === 0) return children;
|
|
507
|
+
const result = [];
|
|
508
|
+
|
|
509
|
+
// Add backward clones (only when we have enough data to position them)
|
|
510
|
+
if (isLoopingActive && cloneCounts.backward > 0) {
|
|
511
|
+
const items = getItemOffsets(carouselItemRects);
|
|
512
|
+
const itemsToCloneBackward = childrenArray.slice(-cloneCounts.backward);
|
|
513
|
+
itemsToCloneBackward.forEach((child, cloneIndex) => {
|
|
514
|
+
var _itemData$x;
|
|
515
|
+
const originalIndex = childrenArray.length - cloneCounts.backward + cloneIndex;
|
|
516
|
+
const itemData = items[originalIndex];
|
|
517
|
+
const cloneId = "clone-backward-" + child.props.id;
|
|
518
|
+
result.push(/*#__PURE__*/_jsx(CarouselItem, {
|
|
519
|
+
"aria-hidden": true,
|
|
520
|
+
id: cloneId,
|
|
521
|
+
style: {
|
|
522
|
+
position: 'absolute',
|
|
523
|
+
left: ((_itemData$x = itemData == null ? void 0 : itemData.x) != null ? _itemData$x : 0) - loopLength,
|
|
524
|
+
width: itemData == null ? void 0 : itemData.width,
|
|
525
|
+
height: itemData == null ? void 0 : itemData.height
|
|
526
|
+
},
|
|
527
|
+
children: child.props.children
|
|
528
|
+
}, cloneId));
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Add original children (always present, never changes structure)
|
|
533
|
+
result.push(...childrenArray);
|
|
534
|
+
|
|
535
|
+
// Add forward clones (only when we have enough data)
|
|
536
|
+
if (isLoopingActive && cloneCounts.forward > 0) {
|
|
537
|
+
const items = getItemOffsets(carouselItemRects);
|
|
538
|
+
const itemsToCloneForward = childrenArray.slice(0, cloneCounts.forward);
|
|
539
|
+
itemsToCloneForward.forEach((child, cloneIndex) => {
|
|
540
|
+
const itemData = items[cloneIndex];
|
|
541
|
+
const cloneId = "clone-forward-" + child.props.id;
|
|
542
|
+
result.push(/*#__PURE__*/_jsx(CarouselItem, {
|
|
543
|
+
"aria-hidden": true,
|
|
544
|
+
id: cloneId,
|
|
545
|
+
style: {
|
|
546
|
+
width: itemData == null ? void 0 : itemData.width,
|
|
547
|
+
height: itemData == null ? void 0 : itemData.height
|
|
548
|
+
},
|
|
549
|
+
children: child.props.children
|
|
550
|
+
}, cloneId));
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
return result;
|
|
554
|
+
}, [loop, children, isLoopingActive, loopLength, cloneCounts, carouselItemRects]);
|
|
313
555
|
const containerStyle = useMemo(() => [{
|
|
314
556
|
flex: 1,
|
|
315
557
|
overflow: 'hidden'
|
|
@@ -322,9 +564,15 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
|
|
|
322
564
|
}, styles == null ? void 0 : styles.carousel), [styles == null ? void 0 : styles.carousel]);
|
|
323
565
|
const animatedTransform = useMemo(() => ({
|
|
324
566
|
transform: [{
|
|
325
|
-
translateX: animationApi.x.to(value =>
|
|
567
|
+
translateX: animationApi.x.to(value => {
|
|
568
|
+
if (!shouldLoop || !loopLength) return -value;
|
|
569
|
+
// Wrap the value to stay within one cycle for visual continuity
|
|
570
|
+
// Ensure wrapped is always in range [0, loopLength)
|
|
571
|
+
const wrapped = (value % loopLength + loopLength) % loopLength;
|
|
572
|
+
return -wrapped;
|
|
573
|
+
})
|
|
326
574
|
}]
|
|
327
|
-
}), [animationApi]);
|
|
575
|
+
}), [animationApi, shouldLoop, loopLength]);
|
|
328
576
|
const registerItem = useCallback((id, rect) => {
|
|
329
577
|
setCarouselItemRects(prev => _extends({}, prev, {
|
|
330
578
|
[id]: rect
|
|
@@ -361,11 +609,11 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
|
|
|
361
609
|
style: styles == null ? void 0 : styles.title,
|
|
362
610
|
children: title
|
|
363
611
|
}) : title, !hideNavigation && /*#__PURE__*/_jsx(NavigationComponent, {
|
|
364
|
-
disableGoNext: activePageIndex >= totalPages - 1,
|
|
365
|
-
disableGoPrevious: activePageIndex <= 0,
|
|
612
|
+
disableGoNext: totalPages <= 1 || !shouldLoop && activePageIndex >= totalPages - 1,
|
|
613
|
+
disableGoPrevious: totalPages <= 1 || !shouldLoop && activePageIndex <= 0,
|
|
366
614
|
nextPageAccessibilityLabel: nextPageAccessibilityLabel,
|
|
367
|
-
onGoNext:
|
|
368
|
-
onGoPrevious:
|
|
615
|
+
onGoNext: handleGoNext,
|
|
616
|
+
onGoPrevious: handleGoPrevious,
|
|
369
617
|
previousPageAccessibilityLabel: previousPageAccessibilityLabel,
|
|
370
618
|
style: styles == null ? void 0 : styles.navigation
|
|
371
619
|
})]
|
|
@@ -376,7 +624,7 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
|
|
|
376
624
|
style: scrollViewStyle,
|
|
377
625
|
children: /*#__PURE__*/_jsx(animated.View, {
|
|
378
626
|
style: [animatedStyle, animatedTransform],
|
|
379
|
-
children:
|
|
627
|
+
children: childrenWithClones
|
|
380
628
|
})
|
|
381
629
|
})
|
|
382
630
|
}), !hidePagination && /*#__PURE__*/_jsx(PaginationComponent, {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
export const CarouselContext = /*#__PURE__*/React.createContext(undefined);
|
|
3
|
+
export const useCarouselContext = () => {
|
|
4
|
+
const context = useContext(CarouselContext);
|
|
5
|
+
if (!context) {
|
|
6
|
+
throw new Error('useCarouselContext must be used within a Carousel component');
|
|
7
|
+
}
|
|
8
|
+
return context;
|
|
9
|
+
};
|
|
@@ -4,7 +4,7 @@ function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t =
|
|
|
4
4
|
import React, { memo, useCallback, useEffect } from 'react';
|
|
5
5
|
import { useLayout } from '../hooks/useLayout';
|
|
6
6
|
import { Box } from '../layout/Box';
|
|
7
|
-
import { useCarouselContext } from './
|
|
7
|
+
import { useCarouselContext } from './CarouselContext';
|
|
8
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
9
|
export const CarouselItem = /*#__PURE__*/memo(_ref => {
|
|
10
10
|
let {
|
|
@@ -616,6 +616,80 @@ const ImperativeApiExample = () => {
|
|
|
616
616
|
})
|
|
617
617
|
});
|
|
618
618
|
};
|
|
619
|
+
const LoopingExamples = () => {
|
|
620
|
+
const theme = useTheme();
|
|
621
|
+
const horizontalPadding = theme.space[2];
|
|
622
|
+
const windowWidth = Dimensions.get('window').width;
|
|
623
|
+
const carouselSizing = windowWidth - horizontalPadding * 2;
|
|
624
|
+
const horizontalGap = theme.space[1];
|
|
625
|
+
const threeItemsWidth = (carouselSizing - horizontalGap * 2) / 3;
|
|
626
|
+
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
627
|
+
children: [/*#__PURE__*/_jsx(Example, {
|
|
628
|
+
paddingX: 0,
|
|
629
|
+
children: /*#__PURE__*/_jsx(Carousel, {
|
|
630
|
+
loop: true,
|
|
631
|
+
drag: "snap",
|
|
632
|
+
snapMode: "page",
|
|
633
|
+
styles: {
|
|
634
|
+
root: {
|
|
635
|
+
paddingHorizontal: horizontalPadding
|
|
636
|
+
},
|
|
637
|
+
carousel: {
|
|
638
|
+
gap: horizontalGap
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
title: "Looping - Snap Page",
|
|
642
|
+
children: sampleItems.map((item, index) => /*#__PURE__*/_jsx(CarouselItem, {
|
|
643
|
+
id: "loop-page-" + index,
|
|
644
|
+
width: threeItemsWidth,
|
|
645
|
+
children: item
|
|
646
|
+
}, "loop-page-" + index))
|
|
647
|
+
})
|
|
648
|
+
}), /*#__PURE__*/_jsx(Example, {
|
|
649
|
+
paddingX: 0,
|
|
650
|
+
children: /*#__PURE__*/_jsx(Carousel, {
|
|
651
|
+
loop: true,
|
|
652
|
+
drag: "snap",
|
|
653
|
+
snapMode: "item",
|
|
654
|
+
styles: {
|
|
655
|
+
root: {
|
|
656
|
+
paddingHorizontal: horizontalPadding
|
|
657
|
+
},
|
|
658
|
+
carousel: {
|
|
659
|
+
gap: horizontalGap
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
title: "Looping - Snap Item",
|
|
663
|
+
children: sampleItems.map((item, index) => /*#__PURE__*/_jsx(CarouselItem, {
|
|
664
|
+
id: "loop-item-" + index,
|
|
665
|
+
width: threeItemsWidth,
|
|
666
|
+
children: item
|
|
667
|
+
}, "loop-item-" + index))
|
|
668
|
+
})
|
|
669
|
+
}), /*#__PURE__*/_jsx(Example, {
|
|
670
|
+
paddingX: 0,
|
|
671
|
+
children: /*#__PURE__*/_jsx(Carousel, {
|
|
672
|
+
loop: true,
|
|
673
|
+
drag: "free",
|
|
674
|
+
snapMode: "item",
|
|
675
|
+
styles: {
|
|
676
|
+
root: {
|
|
677
|
+
paddingHorizontal: horizontalPadding
|
|
678
|
+
},
|
|
679
|
+
carousel: {
|
|
680
|
+
gap: horizontalGap
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
title: "Looping - Free Drag",
|
|
684
|
+
children: sampleItems.map((item, index) => /*#__PURE__*/_jsx(CarouselItem, {
|
|
685
|
+
id: "loop-free-" + index,
|
|
686
|
+
width: threeItemsWidth,
|
|
687
|
+
children: item
|
|
688
|
+
}, "loop-free-" + index))
|
|
689
|
+
})
|
|
690
|
+
})]
|
|
691
|
+
});
|
|
692
|
+
};
|
|
619
693
|
const AnimatedPaginationExample = () => {
|
|
620
694
|
const theme = useTheme();
|
|
621
695
|
const AnimatedDot = /*#__PURE__*/memo(_ref5 => {
|
|
@@ -736,6 +810,7 @@ const AnimatedPaginationExample = () => {
|
|
|
736
810
|
};
|
|
737
811
|
export default function CarouselScreen() {
|
|
738
812
|
return /*#__PURE__*/_jsxs(ExampleScreen, {
|
|
739
|
-
|
|
813
|
+
paddingX: 0,
|
|
814
|
+
children: [/*#__PURE__*/_jsx(BasicExamples, {}), /*#__PURE__*/_jsx(CustomComponentsExample, {}), /*#__PURE__*/_jsx(DynamicContentExample, {}), /*#__PURE__*/_jsx(AnimatedExample, {}), /*#__PURE__*/_jsx(ImperativeApiExample, {}), /*#__PURE__*/_jsx(AnimatedPaginationExample, {}), /*#__PURE__*/_jsx(LoopingExamples, {})]
|
|
740
815
|
});
|
|
741
816
|
}
|
package/esm/carousel/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coinbase/cds-mobile",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.39.0",
|
|
4
4
|
"description": "Coinbase Design System - Mobile",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"react-native-svg": "^14.1.0"
|
|
199
199
|
},
|
|
200
200
|
"dependencies": {
|
|
201
|
-
"@coinbase/cds-common": "^8.
|
|
201
|
+
"@coinbase/cds-common": "^8.39.0",
|
|
202
202
|
"@coinbase/cds-icons": "^5.9.0",
|
|
203
203
|
"@coinbase/cds-illustrations": "^4.29.0",
|
|
204
204
|
"@coinbase/cds-lottie-files": "^3.3.4",
|