@coinbase/cds-common 8.41.0 → 8.43.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 CHANGED
@@ -8,6 +8,16 @@ All notable changes to this project will be documented in this file.
8
8
 
9
9
  <!-- template-start -->
10
10
 
11
+ ## 8.43.0 (2/6/2026 PST)
12
+
13
+ #### 🚀 Updates
14
+
15
+ - Carousel autoplay. [[#361](https://github.com/coinbase/cds/pull/361)]
16
+
17
+ ## 8.42.0 ((2/4/2026, 01:51 PM PST))
18
+
19
+ This is an artificial version bump with no new change.
20
+
11
21
  ## 8.41.0 ((2/4/2026, 09:22 AM PST))
12
22
 
13
23
  This is an artificial version bump with no new change.
@@ -0,0 +1,2 @@
1
+ export * from './useCarouselAutoplay';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/carousel/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,83 @@
1
+ export type CarouselAutoplayOptions = {
2
+ /**
3
+ * Whether autoplay is enabled.
4
+ */
5
+ enabled: boolean;
6
+ /**
7
+ * The interval in milliseconds between auto-advances.
8
+ */
9
+ interval: number;
10
+ /**
11
+ * Callback fired when autoplay starts.
12
+ */
13
+ onStart?: () => void;
14
+ /**
15
+ * Callback fired when autoplay stops.
16
+ */
17
+ onStop?: () => void;
18
+ };
19
+ export type CarouselAutoplayState = {
20
+ /**
21
+ * Whether autoplay is actively running (enabled AND not stopped AND not paused).
22
+ */
23
+ isPlaying: boolean;
24
+ /**
25
+ * Whether autoplay has been stopped by the user.
26
+ */
27
+ isStopped: boolean;
28
+ /**
29
+ * Whether autoplay is temporarily paused due to user interaction (hover/touch).
30
+ */
31
+ isPaused: boolean;
32
+ };
33
+ export type CarouselAutoplayApi = {
34
+ /**
35
+ * Start autoplay. Resumes from paused progress if available.
36
+ */
37
+ start: () => void;
38
+ /**
39
+ * Stop autoplay. Preserves current progress for resuming later.
40
+ */
41
+ stop: () => void;
42
+ /**
43
+ * Toggle autoplay on/off.
44
+ */
45
+ toggle: () => void;
46
+ /**
47
+ * Reset the autoplay timer (e.g., after manual navigation).
48
+ */
49
+ reset: () => void;
50
+ /**
51
+ * Temporarily pause autoplay (e.g., on hover/touch). Does not change isStopped state.
52
+ * Progress is preserved and will resume from where it left off.
53
+ */
54
+ pause: () => void;
55
+ /**
56
+ * Resume autoplay after interaction pause. Only resumes if not user-stopped.
57
+ */
58
+ resume: () => void;
59
+ /**
60
+ * Get the current remaining time. Useful for calculating progress in platform-native animations.
61
+ */
62
+ getRemainingTime: () => number;
63
+ /**
64
+ * Add a listener to be called when the autoplay timer completes.
65
+ * Returns an unsubscribe function.
66
+ */
67
+ addCompletionListener: (callback: () => void) => () => void;
68
+ };
69
+ /**
70
+ * Combined state and API returned by useCarouselAutoplay.
71
+ */
72
+ export type CarouselAutoplay = CarouselAutoplayState & CarouselAutoplayApi;
73
+ /**
74
+ * A hook for managing carousel autoplay state and timing.
75
+ * Provides controls for starting, stopping, and resetting autoplay.
76
+ */
77
+ export declare const useCarouselAutoplay: ({
78
+ enabled,
79
+ interval,
80
+ onStart,
81
+ onStop,
82
+ }: CarouselAutoplayOptions) => CarouselAutoplay;
83
+ //# sourceMappingURL=useCarouselAutoplay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCarouselAutoplay.d.ts","sourceRoot":"","sources":["../../src/carousel/useCarouselAutoplay.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,uBAAuB,GAAG;IACpC;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;OAEG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC;;OAEG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB;;OAEG;IACH,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB;;OAEG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;OAEG;IACH,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB;;OAEG;IACH,gBAAgB,EAAE,MAAM,MAAM,CAAC;IAC/B;;;OAGG;IACH,qBAAqB,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;CAC7D,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,qBAAqB,GAAG,mBAAmB,CAAC;AAE3E;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,yCAKjC,uBAAuB,KAAG,gBAqK5B,CAAC"}
@@ -3,5 +3,7 @@ export declare const useTimer: () => {
3
3
  clear: () => void;
4
4
  pause: () => number;
5
5
  resume: () => void;
6
+ getRemainingTime: () => number;
7
+ reset: () => void;
6
8
  };
7
9
  //# sourceMappingURL=useTimer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useTimer.d.ts","sourceRoot":"","sources":["../../src/hooks/useTimer.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ;sBAgBN,MAAM,IAAI,YAAY,MAAM;;;;CA4C1C,CAAC"}
1
+ {"version":3,"file":"useTimer.d.ts","sourceRoot":"","sources":["../../src/hooks/useTimer.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ;sBAgBN,MAAM,IAAI,YAAY,MAAM;;;;;;CAgE1C,CAAC"}
package/dts/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './carousel';
1
2
  export * from './core/theme';
2
3
  export * from './hooks/useToggler';
3
4
  export * from './lottie/lottieUtils';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mCAAmC,CAAC;AAClD,cAAc,SAAS,CAAC;AACxB,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mCAAmC,CAAC;AAClD,cAAc,SAAS,CAAC;AACxB,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC"}
@@ -0,0 +1 @@
1
+ export * from './useCarouselAutoplay';
@@ -0,0 +1,150 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useTimer } from '../hooks/useTimer';
3
+
4
+ /**
5
+ * Combined state and API returned by useCarouselAutoplay.
6
+ */
7
+
8
+ /**
9
+ * A hook for managing carousel autoplay state and timing.
10
+ * Provides controls for starting, stopping, and resetting autoplay.
11
+ */
12
+ export const useCarouselAutoplay = _ref => {
13
+ let {
14
+ enabled,
15
+ interval,
16
+ onStart,
17
+ onStop
18
+ } = _ref;
19
+ const timer = useTimer();
20
+ const [isStopped, setIsStopped] = useState(false);
21
+ const [isPaused, setIsPaused] = useState(false);
22
+
23
+ // Use refs for synchronous checks to avoid stale closure issues
24
+ const isPlayingRef = useRef(false);
25
+ const isPausedRef = useRef(false);
26
+ const isStoppedRef = useRef(false);
27
+
28
+ // Listeners for timer completion
29
+ const listenersRef = useRef(new Set());
30
+ const notifyListeners = useCallback(() => {
31
+ // Snapshot listeners to avoid issues when Set is modified during iteration
32
+ const listeners = [...listenersRef.current];
33
+ listeners.forEach(listener => listener());
34
+ }, []);
35
+ const addCompletionListener = useCallback(callback => {
36
+ listenersRef.current.add(callback);
37
+ return () => {
38
+ listenersRef.current.delete(callback);
39
+ };
40
+ }, []);
41
+ const isPlaying = enabled && !isStopped && !isPaused;
42
+ const getRemainingTime = useCallback(() => {
43
+ return timer.getRemainingTime();
44
+ }, [timer]);
45
+ const startAutoplay = useCallback(fromPausedProgress => {
46
+ if (!enabled || isStoppedRef.current || isPausedRef.current) return;
47
+ const advance = () => {
48
+ if (!isPlayingRef.current) return;
49
+ notifyListeners();
50
+ };
51
+ if (fromPausedProgress) {
52
+ timer.resume();
53
+ } else {
54
+ timer.start(advance, interval);
55
+ }
56
+ if (!isPlayingRef.current) {
57
+ isPlayingRef.current = true;
58
+ onStart === null || onStart === void 0 || onStart();
59
+ }
60
+ }, [enabled, interval, timer, onStart, notifyListeners]);
61
+ const start = useCallback(() => {
62
+ isStoppedRef.current = false;
63
+ setIsStopped(false);
64
+ // Start timer synchronously if not paused
65
+ if (!isPausedRef.current && enabled) {
66
+ startAutoplay(false);
67
+ }
68
+ }, [enabled, startAutoplay]);
69
+ const stop = useCallback(() => {
70
+ timer.pause();
71
+ isStoppedRef.current = true;
72
+ setIsStopped(true);
73
+ if (isPlayingRef.current) {
74
+ isPlayingRef.current = false;
75
+ onStop === null || onStop === void 0 || onStop();
76
+ }
77
+ }, [timer, onStop]);
78
+ const toggle = useCallback(() => {
79
+ if (isStoppedRef.current) {
80
+ start();
81
+ } else {
82
+ stop();
83
+ }
84
+ }, [start, stop]);
85
+ const reset = useCallback(() => {
86
+ timer.reset();
87
+
88
+ // Start a fresh timer with the full interval
89
+ const advance = () => {
90
+ if (!isPlayingRef.current) return;
91
+ notifyListeners();
92
+ };
93
+ timer.start(advance, interval);
94
+
95
+ // If paused, immediately pause the timer so getRemainingTime() returns the full interval
96
+ if (isPausedRef.current) {
97
+ timer.pause();
98
+ }
99
+ }, [timer, interval, notifyListeners]);
100
+ const pause = useCallback(() => {
101
+ if (!isPlayingRef.current) return;
102
+ timer.pause();
103
+ isPausedRef.current = true;
104
+ setIsPaused(true);
105
+ }, [timer]);
106
+ const resume = useCallback(() => {
107
+ if (isStoppedRef.current) return;
108
+ // Update ref synchronously BEFORE starting timer
109
+ isPausedRef.current = false;
110
+ setIsPaused(false);
111
+ // Start timer synchronously so getRemainingTime() returns correct value
112
+ if (enabled) {
113
+ const hasRemainingTime = timer.getRemainingTime() > 0;
114
+ startAutoplay(hasRemainingTime);
115
+ }
116
+ }, [enabled, timer, startAutoplay]);
117
+
118
+ // Handle initial mount and enabled changes
119
+ // This runs on mount when enabled=true to start autoplay initially
120
+ useEffect(() => {
121
+ if (enabled && !isStoppedRef.current && !isPausedRef.current) {
122
+ // Only start if not already playing (avoid double-start)
123
+ if (!isPlayingRef.current) {
124
+ startAutoplay(false);
125
+ }
126
+ }
127
+ // Keep isPlayingRef in sync with derived state
128
+ isPlayingRef.current = isPlaying;
129
+ }, [enabled, isPlaying, startAutoplay]);
130
+
131
+ // Cleanup timer on unmount
132
+ useEffect(() => {
133
+ return () => {
134
+ timer.clear();
135
+ };
136
+ }, [timer]);
137
+ return useMemo(() => ({
138
+ isPlaying,
139
+ isStopped,
140
+ isPaused,
141
+ start,
142
+ stop,
143
+ toggle,
144
+ reset,
145
+ pause,
146
+ resume,
147
+ getRemainingTime,
148
+ addCompletionListener
149
+ }), [isPlaying, isStopped, isPaused, start, stop, toggle, reset, pause, resume, getRemainingTime, addCompletionListener]);
150
+ };
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef } from 'react';
2
2
 
3
- // timer for single execution
3
+ // Timer for single execution
4
4
  export const useTimer = () => {
5
5
  const timerRef = useRef();
6
6
  const startTimeRef = useRef(0);
@@ -11,11 +11,11 @@ export const useTimer = () => {
11
11
  if (timerRef.current) {
12
12
  clearTimeout(timerRef.current);
13
13
  timerRef.current = undefined;
14
- isPausedRef.current = false;
15
14
  }
15
+ isPausedRef.current = false;
16
16
  }, []);
17
17
  const start = useCallback((callback, duration) => {
18
- // clear existing timer
18
+ // Clear existing timer
19
19
  clear();
20
20
  timerRef.current = setTimeout(callback, duration);
21
21
  callbackRef.current = callback;
@@ -37,6 +37,22 @@ export const useTimer = () => {
37
37
  isPausedRef.current = false;
38
38
  }
39
39
  }, [start]);
40
+ const getRemainingTime = useCallback(() => {
41
+ if (isPausedRef.current) {
42
+ return remainingTimeRef.current;
43
+ }
44
+ if (!timerRef.current) {
45
+ return 0;
46
+ }
47
+ const elapsed = Date.now() - startTimeRef.current;
48
+ return Math.max(0, remainingTimeRef.current - elapsed);
49
+ }, []);
50
+ const reset = useCallback(() => {
51
+ clear();
52
+ remainingTimeRef.current = 0;
53
+ callbackRef.current = undefined;
54
+ isPausedRef.current = false;
55
+ }, [clear]);
40
56
  useEffect(() => {
41
57
  return () => {
42
58
  clear();
@@ -46,6 +62,8 @@ export const useTimer = () => {
46
62
  start,
47
63
  clear,
48
64
  pause,
49
- resume
50
- }), [start, clear, pause, resume]);
65
+ resume,
66
+ getRemainingTime,
67
+ reset
68
+ }), [start, clear, pause, resume, getRemainingTime, reset]);
51
69
  };
package/esm/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './carousel';
1
2
  export * from './core/theme';
2
3
  export * from './hooks/useToggler';
3
4
  export * from './lottie/lottieUtils';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinbase/cds-common",
3
- "version": "8.41.0",
3
+ "version": "8.43.0",
4
4
  "description": "Coinbase Design System - Common",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,9 +38,9 @@
38
38
  "react-dom": "^18.3.1"
39
39
  },
40
40
  "dependencies": {
41
- "@coinbase/cds-icons": "^5.10.0",
41
+ "@coinbase/cds-icons": "^5.11.0",
42
42
  "@coinbase/cds-illustrations": "^4.31.0",
43
- "@coinbase/cds-mcp-server": "^8.41.0",
43
+ "@coinbase/cds-mcp-server": "^8.43.0",
44
44
  "@coinbase/cds-utils": "^2.3.5",
45
45
  "@modelcontextprotocol/sdk": "^1.13.1",
46
46
  "d3-array": "^3.2.4",