diff --git a/.cursor/commands/component-docs.md b/.cursor/commands/component-docs.md
index b8188bbc2a..957f7f79e0 100644
--- a/.cursor/commands/component-docs.md
+++ b/.cursor/commands/component-docs.md
@@ -29,7 +29,7 @@ For updates, focus on the specific areas that need improvement rather than rewri
When creating or updating docs, reference these well-documented components to understand the documentation style and patterns:
-- **LineChart** (`apps/docs/docs/components/graphs/LineChart/`) - Comprehensive example with many composed examples
+- **LineChart** (`apps/docs/docs/components/charts/LineChart/`) - Comprehensive example with many composed examples
- **Button** (`apps/docs/docs/components/buttons/Button/`) - Good basic component documentation
- **IconButton** (`apps/docs/docs/components/buttons/IconButton/`) - Simple component with clear examples
- **Sidebar** (`apps/docs/docs/components/navigation/Sidebar/`) - Complex component with multiple sub-components
diff --git a/.gitignore b/.gitignore
index 941e9c7306..7677621ca0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -176,6 +176,8 @@ cds-biweekly-update.md
missing-files.json
barrel-files.md
base-props.json
+component-peer-dependencies.md
+component-peer-dependencies.json
# temp directory created when running podium scripts. this gets deleted after the script is done, but for the sake of your sanity, let's not track it
**/.podium/
diff --git a/.percy.js b/.percy.js
index 553b54c9af..abbb673f91 100644
--- a/.percy.js
+++ b/.percy.js
@@ -17,7 +17,7 @@ module.exports = {
'Components/SparklineInteractive: Fallback Positive',
'Components/LottieStatusAnimation: Default',
'Components/Loaders/MaterialSpinner: Material Spinner Default',
- 'Components/Chart/LineChart: Transitions',
+ 'Components/Chart/CartesianChart: Transitions',
],
include: [
// 'Core Components/SparklineInteractive:*',
diff --git a/apps/docs/babel.config.cjs b/apps/docs/babel.config.cjs
index 453d0e9668..ff2ed3a4ef 100644
--- a/apps/docs/babel.config.cjs
+++ b/apps/docs/babel.config.cjs
@@ -1,5 +1,9 @@
const docusaurusPreset = require('@docusaurus/core/lib/babel/preset');
+const isTestEnv = process.env.NODE_ENV === 'test';
+
module.exports = {
- presets: [docusaurusPreset],
+ presets: isTestEnv
+ ? [['@babel/preset-env', { modules: 'commonjs' }], '@babel/preset-typescript']
+ : [docusaurusPreset],
};
diff --git a/apps/docs/docs/components/animation/LottieStatusAnimation/mobileMetadata.json b/apps/docs/docs/components/animation/LottieStatusAnimation/mobileMetadata.json
index 7db05149a3..1ba88778e5 100644
--- a/apps/docs/docs/components/animation/LottieStatusAnimation/mobileMetadata.json
+++ b/apps/docs/docs/components/animation/LottieStatusAnimation/mobileMetadata.json
@@ -8,5 +8,10 @@
"url": "/components/animation/Lottie"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "lottie-react-native",
+ "version": "^6.7.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/cards/DataCard/mobileMetadata.json b/apps/docs/docs/components/cards/DataCard/mobileMetadata.json
index 2ab1988e74..28a52d3e47 100644
--- a/apps/docs/docs/components/cards/DataCard/mobileMetadata.json
+++ b/apps/docs/docs/components/cards/DataCard/mobileMetadata.json
@@ -26,7 +26,7 @@
},
{
"label": "LineChart",
- "url": "/components/graphs/LineChart/"
+ "url": "/components/charts/LineChart/"
}
],
"dependencies": []
diff --git a/apps/docs/docs/components/cards/DataCard/webMetadata.json b/apps/docs/docs/components/cards/DataCard/webMetadata.json
index 783542c147..1eaa7b98ae 100644
--- a/apps/docs/docs/components/cards/DataCard/webMetadata.json
+++ b/apps/docs/docs/components/cards/DataCard/webMetadata.json
@@ -27,7 +27,7 @@
},
{
"label": "LineChart",
- "url": "/components/graphs/LineChart/"
+ "url": "/components/charts/LineChart/"
}
],
"dependencies": []
diff --git a/apps/docs/docs/components/cards/NudgeCard/mobileMetadata.json b/apps/docs/docs/components/cards/NudgeCard/mobileMetadata.json
index 5115a3e343..ee33ef4ad1 100644
--- a/apps/docs/docs/components/cards/NudgeCard/mobileMetadata.json
+++ b/apps/docs/docs/components/cards/NudgeCard/mobileMetadata.json
@@ -18,5 +18,10 @@
"url": "/components/cards/ContentCard/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/charts/AreaChart/_mobileExamples.mdx b/apps/docs/docs/components/charts/AreaChart/_mobileExamples.mdx
new file mode 100644
index 0000000000..b3cf17b575
--- /dev/null
+++ b/apps/docs/docs/components/charts/AreaChart/_mobileExamples.mdx
@@ -0,0 +1,605 @@
+AreaChart is a cartesian chart variant that allows for easy visualization of stacked data.
+
+## Basic Example
+
+```jsx
+
+```
+
+## Simple
+
+```jsx
+
+```
+
+## Stacking
+
+You can use the `stacked` prop to stack all areas on top of each other. You can also use the `stackId` prop on a series to create different stack groups. See [CartesianChart](/components/charts/CartesianChart/#series-stacks) for more details.
+
+```jsx
+function StackingExample() {
+ const theme = useTheme();
+ return (
+ }
+ type="dotted"
+ />
+ );
+}
+```
+
+## Negative Values
+
+When an area chart contains negative values, the baseline automatically adjusts to zero instead of the bottom of the chart. The area fills from the data line to the zero baseline, properly showing both positive and negative regions.
+
+```jsx
+ }
+ showYAxis
+ yAxis={{
+ showGrid: true,
+ }}
+/>
+```
+
+## Area Styles
+
+You can have different area styles for each series.
+
+```jsx
+
+```
+
+## Animations
+
+You can configure chart transitions using the `transitions` prop on individual chart elements, or use the `animate` prop to toggle animations entirely.
+
+### Customized Transitions
+
+You can pass in a custom spring or timing based transition to your chart for custom update animations. Use `CartesianChart` with `Area` and `Line` components directly to configure `transitions` per element.
+
+```jsx
+function AnimatedStackedAreas() {
+ const theme = useTheme();
+ const dataCount = 20;
+ const minYValue = 5000;
+ const maxDataOffset = 15000;
+ const minStepOffset = 2500;
+ const maxStepOffset = 10000;
+ const updateInterval = 500;
+ const seriesSpacing = 2000;
+ const myTransition = {
+ update: { type: 'spring', stiffness: 700, damping: 20 },
+ };
+
+ const seriesConfig = [
+ { id: 'red', label: 'Red', color: `rgb(${theme.spectrum.red40})` },
+ { id: 'orange', label: 'Orange', color: `rgb(${theme.spectrum.orange40})` },
+ { id: 'yellow', label: 'Yellow', color: `rgb(${theme.spectrum.yellow40})` },
+ { id: 'green', label: 'Green', color: `rgb(${theme.spectrum.green40})` },
+ { id: 'blue', label: 'Blue', color: `rgb(${theme.spectrum.blue40})` },
+ { id: 'indigo', label: 'Indigo', color: `rgb(${theme.spectrum.indigo40})` },
+ { id: 'purple', label: 'Purple', color: `rgb(${theme.spectrum.purple40})` },
+ ];
+
+ const domainLimit = maxDataOffset + seriesConfig.length * seriesSpacing;
+
+ function generateNextValue(previousValue) {
+ const range = maxStepOffset - minStepOffset;
+ const offset = Math.random() * range + minStepOffset;
+
+ let direction;
+ if (previousValue >= maxDataOffset) {
+ direction = -1;
+ } else if (previousValue <= minYValue) {
+ direction = 1;
+ } else {
+ direction = Math.random() < 0.5 ? -1 : 1;
+ }
+
+ let newValue = previousValue + offset * direction;
+ newValue = Math.max(minYValue, Math.min(maxDataOffset, newValue));
+ return newValue;
+ }
+
+ function generateInitialData() {
+ const data = [];
+
+ let previousValue = minYValue + Math.random() * (maxDataOffset - minYValue);
+ data.push(previousValue);
+
+ for (let i = 1; i < dataCount; i++) {
+ const newValue = generateNextValue(previousValue);
+ data.push(newValue);
+ previousValue = newValue;
+ }
+
+ return data;
+ }
+
+ const MemoizedDottedArea = memo((props) => (
+
+ ));
+
+ function AnimatedChart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((currentData) => {
+ const lastValue = currentData[currentData.length - 1] ?? 0;
+ const newValue = generateNextValue(lastValue);
+
+ return [...currentData.slice(1), newValue];
+ });
+ }, updateInterval);
+
+ return () => clearInterval(intervalId);
+ }, []);
+
+ const series = seriesConfig.map((config, index) => ({
+ id: config.id,
+ label: config.label,
+ color: config.color,
+ data: index === 0 ? data : Array(dataCount).fill(seriesSpacing),
+ }));
+
+ return (
+ '',
+ domain: { min: 0, max: domainLimit },
+ }}
+ />
+ );
+ }
+
+ return ;
+}
+```
+
+### Disable Animations
+
+You can also disable animations by setting the `animate` prop to `false`.
+
+```jsx
+function AnimatedStackedAreas() {
+ const theme = useTheme();
+ const dataCount = 20;
+ const minYValue = 5000;
+ const maxDataOffset = 15000;
+ const minStepOffset = 2500;
+ const maxStepOffset = 10000;
+ const updateInterval = 500;
+ const seriesSpacing = 2000;
+
+ const seriesConfig = [
+ { id: 'red', label: 'Red', color: `rgb(${theme.spectrum.red40})` },
+ { id: 'orange', label: 'Orange', color: `rgb(${theme.spectrum.orange40})` },
+ { id: 'yellow', label: 'Yellow', color: `rgb(${theme.spectrum.yellow40})` },
+ { id: 'green', label: 'Green', color: `rgb(${theme.spectrum.green40})` },
+ { id: 'blue', label: 'Blue', color: `rgb(${theme.spectrum.blue40})` },
+ { id: 'indigo', label: 'Indigo', color: `rgb(${theme.spectrum.indigo40})` },
+ { id: 'purple', label: 'Purple', color: `rgb(${theme.spectrum.purple40})` },
+ ];
+
+ const domainLimit = maxDataOffset + seriesConfig.length * seriesSpacing;
+
+ function generateNextValue(previousValue) {
+ const range = maxStepOffset - minStepOffset;
+ const offset = Math.random() * range + minStepOffset;
+
+ let direction;
+ if (previousValue >= maxDataOffset) {
+ direction = -1;
+ } else if (previousValue <= minYValue) {
+ direction = 1;
+ } else {
+ direction = Math.random() < 0.5 ? -1 : 1;
+ }
+
+ let newValue = previousValue + offset * direction;
+ newValue = Math.max(minYValue, Math.min(maxDataOffset, newValue));
+ return newValue;
+ }
+
+ function generateInitialData() {
+ const data = [];
+
+ let previousValue = minYValue + Math.random() * (maxDataOffset - minYValue);
+ data.push(previousValue);
+
+ for (let i = 1; i < dataCount; i++) {
+ const newValue = generateNextValue(previousValue);
+ data.push(newValue);
+ previousValue = newValue;
+ }
+
+ return data;
+ }
+
+ const MemoizedDottedArea = memo((props) => (
+
+ ));
+
+ function AnimatedChart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((currentData) => {
+ const lastValue = currentData[currentData.length - 1] ?? 0;
+ const newValue = generateNextValue(lastValue);
+
+ return [...currentData.slice(1), newValue];
+ });
+ }, updateInterval);
+
+ return () => clearInterval(intervalId);
+ }, []);
+
+ const series = seriesConfig.map((config, index) => ({
+ id: config.id,
+ label: config.label,
+ color: config.color,
+ data: index === 0 ? data : Array(dataCount).fill(seriesSpacing),
+ }));
+
+ return (
+ '',
+ domain: { min: 0, max: domainLimit },
+ }}
+ />
+ );
+ }
+
+ return ;
+}
+```
+
+## Gradients
+
+You can use the `gradient` prop on `series` to enable gradients.
+
+Each stop requires an `offset`, which is based on the data within the x/y scale and `color`, with an optional `opacity` (defaults to 1).
+
+Values in between stops will be interpolated smoothly.
+
+```jsx
+function ContinuousGradient() {
+ const theme = useTheme();
+ const spectrumColors = [
+ 'blue',
+ 'green',
+ 'orange',
+ 'yellow',
+ 'gray',
+ 'indigo',
+ 'pink',
+ 'purple',
+ 'red',
+ 'teal',
+ 'chartreuse',
+ ];
+ const data = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58];
+
+ const [currentSpectrumColor, setCurrentSpectrumColor] = useState('pink');
+
+ return (
+
+
+ {spectrumColors.map((color) => (
+ setCurrentSpectrumColor(color)}
+ style={{
+ backgroundColor: `rgb(${theme.spectrum[`${color}20`]})`,
+ borderColor: `rgb(${theme.spectrum[`${color}50`]})`,
+ borderWidth: 2,
+ borderRadius: 1000,
+ }}
+ width={16}
+ />
+ ))}
+
+ [
+ { offset: min, color: `rgb(${theme.spectrum[`${currentSpectrumColor}80`]})` },
+ { offset: max, color: `rgb(${theme.spectrum[`${currentSpectrumColor}20`]})` },
+ ],
+ },
+ },
+ ]}
+ showYAxis
+ yAxis={{
+ showGrid: true,
+ }}
+ >
+
+
+
+ );
+}
+```
+
+### Discrete
+
+You can set multiple stops at the same offset to create a discrete gradient.
+
+```jsx
+function DiscreteGradient() {
+ const theme = useTheme();
+ const spectrumColors = [
+ 'blue',
+ 'green',
+ 'orange',
+ 'yellow',
+ 'gray',
+ 'indigo',
+ 'pink',
+ 'purple',
+ 'red',
+ 'teal',
+ 'chartreuse',
+ ];
+ const data = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58];
+
+ const [currentSpectrumColor, setCurrentSpectrumColor] = useState('pink');
+
+ return (
+
+
+ {spectrumColors.map((color) => (
+ setCurrentSpectrumColor(color)}
+ style={{
+ backgroundColor: `rgb(${theme.spectrum[`${color}20`]})`,
+ borderColor: `rgb(${theme.spectrum[`${color}50`]})`,
+ borderWidth: 2,
+ borderRadius: 1000,
+ }}
+ width={16}
+ />
+ ))}
+
+ [
+ { offset: min, color: `rgb(${theme.spectrum[`${currentSpectrumColor}80`]})` },
+ {
+ offset: min + (max - min) / 3,
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}80`]})`,
+ },
+ {
+ offset: min + (max - min) / 3,
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}50`]})`,
+ },
+ {
+ offset: min + ((max - min) / 3) * 2,
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}50`]})`,
+ },
+ {
+ offset: min + ((max - min) / 3) * 2,
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}20`]})`,
+ },
+ { offset: max, color: `rgb(${theme.spectrum[`${currentSpectrumColor}20`]})` },
+ ],
+ },
+ },
+ ]}
+ showLines
+ strokeWidth={4}
+ showYAxis
+ yAxis={{
+ showGrid: true,
+ }}
+ fillOpacity={0.5}
+ >
+
+
+
+ );
+}
+```
+
+### Axes
+
+By default, gradients will be applied to the y-axis. You can apply a gradient to the x-axis by setting `axis` to `x` in the gradient definition.
+
+```jsx
+function XAxisGradient() {
+ const theme = useTheme();
+ const spectrumColors = [
+ 'blue',
+ 'green',
+ 'orange',
+ 'yellow',
+ 'gray',
+ 'indigo',
+ 'pink',
+ 'purple',
+ 'red',
+ 'teal',
+ 'chartreuse',
+ ];
+ const data = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58];
+
+ const [currentSpectrumColor, setCurrentSpectrumColor] = useState('pink');
+
+ return (
+
+
+ {spectrumColors.map((color) => (
+ setCurrentSpectrumColor(color)}
+ style={{
+ backgroundColor: `rgb(${theme.spectrum[`${color}20`]})`,
+ borderColor: `rgb(${theme.spectrum[`${color}50`]})`,
+ borderWidth: 2,
+ borderRadius: 1000,
+ }}
+ width={16}
+ />
+ ))}
+
+ [
+ {
+ offset: min,
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}80`]})`,
+ opacity: 0,
+ },
+ {
+ offset: max,
+ color: `rgb(${theme.spectrum[`${currentSpectrumColor}20`]})`,
+ opacity: 1,
+ },
+ ],
+ },
+ },
+ ]}
+ showYAxis
+ yAxis={{
+ showGrid: true,
+ }}
+ >
+
+
+
+ );
+}
+```
diff --git a/apps/docs/docs/components/graphs/AreaChart/_mobilePropsTable.mdx b/apps/docs/docs/components/charts/AreaChart/_mobilePropsTable.mdx
similarity index 100%
rename from apps/docs/docs/components/graphs/AreaChart/_mobilePropsTable.mdx
rename to apps/docs/docs/components/charts/AreaChart/_mobilePropsTable.mdx
diff --git a/apps/docs/docs/components/graphs/AreaChart/_webExamples.mdx b/apps/docs/docs/components/charts/AreaChart/_webExamples.mdx
similarity index 97%
rename from apps/docs/docs/components/graphs/AreaChart/_webExamples.mdx
rename to apps/docs/docs/components/charts/AreaChart/_webExamples.mdx
index 2eb5751cb3..c57793762d 100644
--- a/apps/docs/docs/components/graphs/AreaChart/_webExamples.mdx
+++ b/apps/docs/docs/components/charts/AreaChart/_webExamples.mdx
@@ -41,7 +41,7 @@ AreaChart is a cartesian chart variant that allows for easy visualization of sta
## Stacking
-You can use the `stacked` prop to stack all areas on top of each other. You can also use the `stackId` prop on a series to create different stack groups. See [CartesianChart](/components/graphs/CartesianChart/#series-stacks) for more details.
+You can use the `stacked` prop to stack all areas on top of each other. You can also use the `stackId` prop on a series to create different stack groups. See [CartesianChart](/components/charts/CartesianChart/#series-stacks) for more details.
```jsx live
+```
+
+## Multiple Series
+
+You can also provide multiple series of data to the chart. Series will have their bars for each data point rendered side by side.
+
+```tsx
+function MonthlyGainsByAsset() {
+ const ThinSolidLine = memo((props: SolidLineProps) => );
+
+ const tickFormatter = useCallback(
+ (amount) =>
+ new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(amount),
+ [],
+ );
+
+ return (
+
+ );
+}
+```
+
+## Series Stacking
+
+You can also configure stacking for your chart using the `stacked` prop.
+
+```tsx
+function MonthlyGainsByAsset() {
+ const ThinSolidLine = memo((props: SolidLineProps) => );
+
+ const tickFormatter = useCallback(
+ (amount) =>
+ new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(amount),
+ [],
+ );
+
+ return (
+
+ );
+}
+```
+
+You can also configure multiple stacks by setting the `stackId` prop on each series.
+
+```tsx
+function MonthlyGainsMultipleStacks() {
+ const ThinSolidLine = memo((props: SolidLineProps) => );
+
+ const tickFormatter = useCallback(
+ (amount) =>
+ new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(amount),
+ [],
+ );
+
+ return (
+
+ );
+}
+```
+
+### Stack Gap
+
+```tsx
+function MonthlyGainsByAsset() {
+ const ThinSolidLine = memo((props: SolidLineProps) => );
+
+ const tickFormatter = useCallback(
+ (amount) =>
+ new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(amount),
+ [],
+ );
+
+ return (
+
+ );
+}
+```
+
+## Border Radius
+
+Bars have a default border radius of `100`. You can change this by setting the `borderRadius` prop on the chart.
+
+Stacks will only round the top corners of touching bars.
+
+```jsx
+ {
+ if (value === 'D') {
+ return {value};
+ }
+ return value;
+ },
+ }}
+ style={{ margin: '0 auto' }}
+/>
+```
+
+### Round Baseline
+
+You can also round the baseline of the bars by setting the `roundBaseline` prop on the chart.
+
+```jsx
+ {
+ if (value === 'D') {
+ return {value};
+ }
+ return value;
+ },
+ }}
+ style={{ margin: '0 auto' }}
+/>
+```
+
+## Data
+
+### Negative
+
+```tsx
+function PositiveAndNegativeCashFlow() {
+ const ThinSolidLine = memo((props: SolidLineProps) => );
+
+ const categories = Array.from({ length: 31 }, (_, i) => `3/${i + 1}`);
+ const gains = [
+ 5, 0, 6, 18, 0, 5, 12, 0, 12, 22, 28, 18, 0, 12, 6, 0, 0, 24, 0, 0, 4, 0, 18, 0, 0, 14, 10, 16,
+ 0, 0, 0,
+ ];
+
+ const losses = [
+ -4, 0, -8, -12, -6, 0, 0, 0, -18, 0, -12, 0, -9, -6, 0, 0, 0, 0, -22, -8, 0, 0, -10, -14, 0, 0,
+ 0, 0, 0, -12, -10,
+ ];
+ const series = [
+ { id: 'gains', data: gains, color: 'var(--color-fgPositive)' },
+ { id: 'losses', data: losses, color: 'var(--color-fgNegative)' },
+ ];
+
+ return (
+ `$${value}M`,
+ }}
+ />
+ );
+}
+```
+
+### Null
+
+You can pass in `null` or `0` values to not render a bar for that data point.
+
+```jsx
+ `$${value}k`,
+ showGrid: true,
+ showTickMarks: true,
+ showLine: true,
+ tickMarkSize: 1.5,
+ domain: { max: 50 },
+ }}
+/>
+```
+
+You can also use the `BarStackComponent` prop to render an empty circle for zero values.
+
+```tsx
+function MonthlyRewards() {
+ const months = ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'];
+ const currentMonth = 7;
+ const purple = [null, 6, 8, 10, 7, 6, 6, 8, null, null, null, null];
+ const blue = [null, 10, 12, 11, 10, 9, 10, 11, null, null, null, null];
+ const cyan = [null, 7, 10, 12, 11, 10, 8, 11, null, null, null, null];
+ const green = [10, null, null, null, 1, null, null, 6, null, null, null, null];
+
+ const series = [
+ { id: 'purple', data: purple, color: '#b399ff' },
+ { id: 'blue', data: blue, color: '#4f7cff' },
+ { id: 'cyan', data: cyan, color: '#00c2df' },
+ { id: 'green', data: green, color: '#33c481' },
+ ];
+
+ const CustomBarStackComponent = ({ children, ...props }: BarStackComponentProps) => {
+ if (props.height === 0) {
+ const diameter = props.width;
+ return (
+
+ );
+ }
+
+ return {children};
+ };
+
+ return (
+ {
+ if (index == currentMonth) {
+ return {months[index]};
+ }
+ return months[index];
+ },
+ categoryPadding: 0.27,
+ }}
+ />
+ );
+}
+```
+
+### Range
+
+You can pass in `[min, max]` tuples as data points to render bars that span a range of values.
+
+```tsx
+function PriceRange() {
+ const candles = btcCandles.slice(0, 180).reverse();
+ const data = candles.map((candle) => [parseFloat(candle.low), parseFloat(candle.high)]);
+
+ const min = Math.min(...data.map(([low]) => low));
+ const max = Math.max(...data.map(([, high]) => high));
+
+ const tickFormatter = useCallback(
+ (value: number) =>
+ new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ notation: 'compact',
+ maximumFractionDigits: 0,
+ }).format(value),
+ [],
+ );
+
+ return (
+
+ );
+}
+```
+
+## Customization
+
+### Bar Spacing
+
+There are two ways to control the spacing between bars. You can set the `barPadding` prop to control the spacing between bars within a series. You can also set the `categoryPadding` prop to control the spacing between stacks of bars.
+
+```jsx
+
+```
+
+### Minimum Size
+
+To better emphasize small values, you can set the `stackMinSize` or `barMinSize` prop to control the minimum size for entire stacks or individual bar.
+It is recommended to only use `stackMinSize` for stacked charts and `barMinSize` for non-stacked charts.
+
+#### Minimum Stack Size
+
+You can set the `stackMinSize` prop to control the minimum size for entire stacks. This will only apply to stacks that have a value that is not `null` or `0`. It will proportionally scale the values of each bar in the stack to reach the minimum size.
+
+```jsx
+
+```
+
+#### Minimum Bar Size
+
+You can also set the `barMinSize` prop to control the minimum size for individual bars. This will only apply to bars that have a value that is not `null` or `0`.
+
+```jsx
+ `$${value}k`,
+ showGrid: true,
+ showTickMarks: true,
+ showLine: true,
+ tickMarkSize: 1.5,
+ domain: { max: 50 },
+ }}
+ barMinSize={4}
+/>
+```
+
+### Multiple Y Axes
+
+You can render bars from separate y axes in one `BarPlot`, however they aren't able to be stacked.
+
+```jsx
+function MultipleYAxes() {
+ const theme = useTheme();
+
+ return (
+
+
+ `$${value}k`}
+ />
+ `${value}%`}
+ />
+
+
+ );
+}
+```
+
+## Animations
+
+You can configure chart transitions using the `transitions` prop.
+
+### Customized Transitions
+
+You can pass in a custom spring based transition to your `BarChart` for a custom update transition.
+
+```jsx
+function AnimatedStackedBars() {
+ const theme = useTheme();
+ const dataCount = 12;
+ const minValue = 20;
+ const maxValue = 100;
+ const minStep = 10;
+ const maxStep = 40;
+ const updateInterval = 600;
+ const seriesSpacing = 30;
+
+ const seriesConfig = [
+ { id: 'red', label: 'Red', color: `rgb(${theme.spectrum.red40})` },
+ { id: 'orange', label: 'Orange', color: `rgb(${theme.spectrum.orange40})` },
+ { id: 'yellow', label: 'Yellow', color: `rgb(${theme.spectrum.yellow40})` },
+ { id: 'green', label: 'Green', color: `rgb(${theme.spectrum.green40})` },
+ { id: 'blue', label: 'Blue', color: `rgb(${theme.spectrum.blue40})` },
+ { id: 'indigo', label: 'Indigo', color: `rgb(${theme.spectrum.indigo40})` },
+ { id: 'purple', label: 'Purple', color: `rgb(${theme.spectrum.purple40})` },
+ ];
+
+ const domainLimit = maxValue + seriesConfig.length * seriesSpacing;
+
+ function generateNextValue(previousValue) {
+ const range = maxStep - minStep;
+ const offset = Math.random() * range + minStep;
+
+ let direction;
+ if (previousValue >= maxValue) {
+ direction = -1;
+ } else if (previousValue <= minValue) {
+ direction = 1;
+ } else {
+ direction = Math.random() < 0.5 ? -1 : 1;
+ }
+
+ let newValue = previousValue + offset * direction;
+ newValue = Math.max(minValue, Math.min(maxValue, newValue));
+ return newValue;
+ }
+
+ function generateInitialData() {
+ const data = [];
+
+ let previousValue = minValue + Math.random() * (maxValue - minValue);
+ data.push(previousValue);
+
+ for (let i = 1; i < dataCount; i++) {
+ const newValue = generateNextValue(previousValue);
+ data.push(newValue);
+ previousValue = newValue;
+ }
+
+ return data;
+ }
+
+ function AnimatedChart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((currentData) => {
+ const lastValue = currentData[currentData.length - 1] ?? minValue;
+ const newValue = generateNextValue(lastValue);
+
+ return [...currentData.slice(1), newValue];
+ });
+ }, updateInterval);
+
+ return () => clearInterval(intervalId);
+ }, []);
+
+ const series = seriesConfig.map((config, index) => ({
+ id: config.id,
+ label: config.label,
+ color: config.color,
+ data: index === 0 ? data : Array(dataCount).fill(seriesSpacing),
+ }));
+
+ return (
+ '',
+ domain: { min: 0, max: domainLimit },
+ }}
+ />
+ );
+ }
+
+ return ;
+}
+```
+
+### Disable Animations
+
+You can also disable animations by setting the `animate` prop to `false`.
+
+```jsx
+function AnimatedStackedBars() {
+ const theme = useTheme();
+ const dataCount = 12;
+ const minValue = 20;
+ const maxValue = 100;
+ const minStep = 10;
+ const maxStep = 40;
+ const updateInterval = 600;
+ const seriesSpacing = 30;
+
+ const seriesConfig = [
+ { id: 'red', label: 'Red', color: `rgb(${theme.spectrum.red40})` },
+ { id: 'orange', label: 'Orange', color: `rgb(${theme.spectrum.orange40})` },
+ { id: 'yellow', label: 'Yellow', color: `rgb(${theme.spectrum.yellow40})` },
+ { id: 'green', label: 'Green', color: `rgb(${theme.spectrum.green40})` },
+ { id: 'blue', label: 'Blue', color: `rgb(${theme.spectrum.blue40})` },
+ { id: 'indigo', label: 'Indigo', color: `rgb(${theme.spectrum.indigo40})` },
+ { id: 'purple', label: 'Purple', color: `rgb(${theme.spectrum.purple40})` },
+ ];
+
+ const domainLimit = maxValue + seriesConfig.length * seriesSpacing;
+
+ function generateNextValue(previousValue) {
+ const range = maxStep - minStep;
+ const offset = Math.random() * range + minStep;
+
+ let direction;
+ if (previousValue >= maxValue) {
+ direction = -1;
+ } else if (previousValue <= minValue) {
+ direction = 1;
+ } else {
+ direction = Math.random() < 0.5 ? -1 : 1;
+ }
+
+ let newValue = previousValue + offset * direction;
+ newValue = Math.max(minValue, Math.min(maxValue, newValue));
+ return newValue;
+ }
+
+ function generateInitialData() {
+ const data = [];
+
+ let previousValue = minValue + Math.random() * (maxValue - minValue);
+ data.push(previousValue);
+
+ for (let i = 1; i < dataCount; i++) {
+ const newValue = generateNextValue(previousValue);
+ data.push(newValue);
+ previousValue = newValue;
+ }
+
+ return data;
+ }
+
+ function AnimatedChart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((currentData) => {
+ const lastValue = currentData[currentData.length - 1] ?? minValue;
+ const newValue = generateNextValue(lastValue);
+
+ return [...currentData.slice(1), newValue];
+ });
+ }, updateInterval);
+
+ return () => clearInterval(intervalId);
+ }, []);
+
+ const series = seriesConfig.map((config, index) => ({
+ id: config.id,
+ label: config.label,
+ color: config.color,
+ data: index === 0 ? data : Array(dataCount).fill(seriesSpacing),
+ }));
+
+ return (
+ '',
+ domain: { min: 0, max: domainLimit },
+ }}
+ />
+ );
+ }
+
+ return ;
+}
+```
+
+### Stagger Delay
+
+You can use the `staggerDelay` property on bar transitions to create a cascading animation effect where bars animate sequentially from left to right. The delay is distributed across bars based on their horizontal position — the leftmost bar starts immediately, and the rightmost bar starts after the full `staggerDelay` duration.
+
+```jsx
+function StaggeredBars() {
+ const [data, setData] = useState([45, 80, 120, 95, 150, 110, 85]);
+ const [key, setKey] = useState(0);
+
+ return (
+
+
+
+
+
+
+ Staggered Enter
+
+
+ Staggered Update
+
+
+ );
+}
+```
+
+### Delay
+
+You can use the `delay` property on transitions to add a pause before the animation starts. This is useful for coordinating animations between different chart elements or creating intentional pauses.
+
+```jsx
+function DelayedBars() {
+ const [key, setKey] = useState(0);
+
+ return (
+
+
+
+ No Delay
+
+
+ 500ms Delay
+
+
+ Stagger + Delay
+
+
+ );
+}
+```
+
+## Composed Examples
+
+### Candlesticks
+
+You can render a candlestick chart by setting the `BarComponent` prop to a custom candlestick component.
+
+```tsx
+function Candlesticks() {
+ const infoTextId = useId();
+ const theme = useTheme();
+ const [currentIndex, setCurrentIndex] = useState();
+ const stockData = btcCandles.slice(0, 90).reverse();
+ const min = Math.min(...stockData.map((data) => parseFloat(data.low)));
+
+ const ThinSolidLine = memo((props: SolidLineProps) => );
+
+ const BandwidthHighlight = memo(({ stroke }: LineComponentProps) => {
+ const { getXSerializableScale, drawingArea } = useCartesianChartContext();
+ const { scrubberPosition } = useScrubberContext();
+ const xScale = useMemo(() => getXSerializableScale(), [getXSerializableScale]);
+
+ const rectWidth = useMemo(() => {
+ if (xScale !== undefined && xScale.type === 'band') {
+ return xScale.bandwidth;
+ }
+ return 0;
+ }, [xScale]);
+
+ const xPos = useDerivedValue(() => {
+ const position = unwrapAnimatedValue(scrubberPosition);
+ const xPos =
+ position !== undefined && xScale
+ ? getPointOnSerializableScale(position, xScale)
+ : undefined;
+ return xPos !== undefined ? xPos - rectWidth / 2 : 0;
+ }, [scrubberPosition, xScale]);
+
+ const opacity = useDerivedValue(() => (xPos.value !== undefined ? 1 : 0), [xPos]);
+
+ return (
+
+ );
+ });
+
+ const candlesData = stockData.map((data) => [parseFloat(data.low), parseFloat(data.high)]);
+
+ const CandlestickBarComponent = memo(
+ ({ x, y, width, height, originY, dataX }) => {
+ const { getYScale } = useCartesianChartContext();
+ const yScale = getYScale();
+
+ const wickX = x + width / 2;
+ const timePeriodValue = stockData[dataX as number];
+
+ const open = parseFloat(timePeriodValue.open);
+ const close = parseFloat(timePeriodValue.close);
+
+ const bullish = open < close;
+ const color = bullish ? theme.color.fgPositive : theme.color.fgNegative;
+ const openY = yScale?.(open) ?? 0;
+ const closeY = yScale?.(close) ?? 0;
+
+ const bodyHeight = Math.abs(openY - closeY);
+ const bodyY = openY < closeY ? openY : closeY;
+
+ return (
+ <>
+
+
+ >
+ );
+ },
+ );
+
+ const formatThousandsPrice = useCallback((price: number) => {
+ const formattedPrice = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(price / 1000);
+
+ return `${formattedPrice}k`;
+ }, []);
+
+ const formatTime = useCallback(
+ (index: number | null) => {
+ if (index === null || index === undefined || index >= stockData.length) return '';
+ const ts = parseInt(stockData[index].start);
+ return new Date(ts * 1000).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ });
+ },
+ [stockData],
+ );
+
+ return (
+
+
+ {currentIndex !== undefined
+ ? `Open: ${formatThousandsPrice(parseFloat(stockData[currentIndex].open))}, Close: ${formatThousandsPrice(parseFloat(stockData[currentIndex].close))}`
+ : formatThousandsPrice(parseFloat(stockData[stockData.length - 1].close))}
+
+
+
+
+
+ <>{children}>}
+ />
+
+
+ );
+}
+```
+
+### Monthly Sunlight
+
+You can combine custom BarPlot components and transitions to create a springy sunlight chart.
+
+```tsx
+function SunlightChartExample() {
+ const theme = useTheme();
+ const dayLength = 1440;
+
+ type SunlightChartData = Array<{
+ label: string;
+ value: number;
+ }>;
+
+ const sunlightData: SunlightChartData = [
+ { label: 'Jan', value: 598 },
+ { label: 'Feb', value: 635 },
+ { label: 'Mar', value: 688 },
+ { label: 'Apr', value: 753 },
+ { label: 'May', value: 812 },
+ { label: 'Jun', value: 855 },
+ { label: 'Jul', value: 861 },
+ { label: 'Aug', value: 828 },
+ { label: 'Sep', value: 772 },
+ { label: 'Oct', value: 710 },
+ { label: 'Nov', value: 648 },
+ { label: 'Dec', value: 605 },
+ ];
+
+ const ThinSolidLine = memo((props: SolidLineProps) => );
+
+ return (
+
+ value),
+ yAxisId: 'sunlight',
+ color: `rgb(${theme.spectrum.yellow40})`,
+ },
+ {
+ id: 'day',
+ data: sunlightData.map(() => dayLength),
+ yAxisId: 'day',
+ color: `rgb(${theme.spectrum.blue100})`,
+ },
+ ]}
+ xAxis={{
+ scaleType: 'band',
+ data: sunlightData.map(({ label }) => label),
+ }}
+ yAxis={[
+ {
+ id: 'day',
+ domain: { min: 0, max: dayLength },
+ domainLimit: 'strict',
+ },
+ {
+ id: 'sunlight',
+ domain: { min: 0, max: dayLength },
+ domainLimit: 'strict',
+ },
+ ]}
+ >
+
+
+
+
+
+
+ 2026 sunlight data for the first day of each month in Atlanta, Georgia, provided by NOAA.
+
+
+ );
+}
+```
diff --git a/apps/docs/docs/components/graphs/BarChart/_mobilePropsTable.mdx b/apps/docs/docs/components/charts/BarChart/_mobilePropsTable.mdx
similarity index 100%
rename from apps/docs/docs/components/graphs/BarChart/_mobilePropsTable.mdx
rename to apps/docs/docs/components/charts/BarChart/_mobilePropsTable.mdx
diff --git a/apps/docs/docs/components/graphs/BarChart/_webExamples.mdx b/apps/docs/docs/components/charts/BarChart/_webExamples.mdx
similarity index 70%
rename from apps/docs/docs/components/graphs/BarChart/_webExamples.mdx
rename to apps/docs/docs/components/charts/BarChart/_webExamples.mdx
index 253361b837..7d82a11d14 100644
--- a/apps/docs/docs/components/graphs/BarChart/_webExamples.mdx
+++ b/apps/docs/docs/components/charts/BarChart/_webExamples.mdx
@@ -352,7 +352,9 @@ You can also round the baseline of the bars by setting the `roundBaseline` prop
/>
```
-## Negative Data
+## Data
+
+### Negative
```jsx live
function PositiveAndNegativeCashFlow() {
@@ -392,7 +394,7 @@ function PositiveAndNegativeCashFlow() {
}
```
-## Missing Bars
+### Null
You can pass in `null` or `0` values to not render a bar for that data point.
@@ -490,6 +492,40 @@ function MonthlyRewards() {
}
```
+### Range
+
+You can pass in `[min, max]` tuples as data points to render bars that span a range of values.
+
+```jsx live
+function PriceRange() {
+ const candles = btcCandles.slice(0, 180).reverse();
+ const data = candles.map((candle) => [parseFloat(candle.low), parseFloat(candle.high)]);
+
+ const min = Math.min(...data.map(([low]) => low));
+ const max = Math.max(...data.map(([, high]) => high));
+
+ const tickFormatter = useCallback(
+ (value) =>
+ new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ notation: 'compact',
+ maximumFractionDigits: 0,
+ }).format(value),
+ [],
+ );
+
+ return (
+
+ );
+}
+```
+
## Customization
### Bar Spacing
@@ -658,9 +694,288 @@ You can render bars from separate y axes in one `BarPlot`, however they aren't a
### Custom Components
-#### Candlesticks
+#### Outlined Stacks
+
+You can set the `BarStackComponent` prop to render a custom component for stacks.
+
+```jsx live
+function MonthlyRewards() {
+ const CustomBarStackComponent = ({ children, ...props }) => {
+ return (
+ <>
+
+ {children}
+ >
+ );
+ };
+
+ return (
+ {
+ if (value === 'D') {
+ return {value};
+ }
+ return value;
+ },
+ }}
+ yAxis={{ range: ({ min, max }) => ({ min, max: max - 4 }) }}
+ style={{ margin: '0 auto' }}
+ />
+ );
+}
+```
+
+## Animations
+
+You can configure chart transitions using the `transitions` prop.
+
+### Customized Transitions
+
+You can pass in a custom spring based transition to your `BarChart` for a custom update transition.
+
+```jsx live
+function AnimatedStackedBars() {
+ const dataCount = 12;
+ const minValue = 20;
+ const maxValue = 100;
+ const minStep = 10;
+ const maxStep = 40;
+ const updateInterval = 600;
+ const seriesSpacing = 30;
+
+ const seriesConfig = [
+ { id: 'red', label: 'Red', color: 'rgb(var(--red40))' },
+ { id: 'orange', label: 'Orange', color: 'rgb(var(--orange40))' },
+ { id: 'yellow', label: 'Yellow', color: 'rgb(var(--yellow40))' },
+ { id: 'green', label: 'Green', color: 'rgb(var(--green40))' },
+ { id: 'blue', label: 'Blue', color: 'rgb(var(--blue40))' },
+ { id: 'indigo', label: 'Indigo', color: 'rgb(var(--indigo40))' },
+ { id: 'purple', label: 'Purple', color: 'rgb(var(--purple40))' },
+ ];
+
+ const domainLimit = maxValue + seriesConfig.length * seriesSpacing;
+
+ function generateNextValue(previousValue) {
+ const range = maxStep - minStep;
+ const offset = Math.random() * range + minStep;
+
+ let direction;
+ if (previousValue >= maxValue) {
+ direction = -1;
+ } else if (previousValue <= minValue) {
+ direction = 1;
+ } else {
+ direction = Math.random() < 0.5 ? -1 : 1;
+ }
+
+ let newValue = previousValue + offset * direction;
+ newValue = Math.max(minValue, Math.min(maxValue, newValue));
+ return newValue;
+ }
+
+ function generateInitialData() {
+ const data = [];
+
+ let previousValue = minValue + Math.random() * (maxValue - minValue);
+ data.push(previousValue);
+
+ for (let i = 1; i < dataCount; i++) {
+ const newValue = generateNextValue(previousValue);
+ data.push(newValue);
+ previousValue = newValue;
+ }
+
+ return data;
+ }
+
+ function AnimatedChart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((currentData) => {
+ const lastValue = currentData[currentData.length - 1] ?? minValue;
+ const newValue = generateNextValue(lastValue);
+
+ return [...currentData.slice(1), newValue];
+ });
+ }, updateInterval);
+
+ return () => clearInterval(intervalId);
+ }, []);
+
+ const series = seriesConfig.map((config, index) => ({
+ id: config.id,
+ label: config.label,
+ color: config.color,
+ data: index === 0 ? data : Array(dataCount).fill(seriesSpacing),
+ }));
+
+ return (
+ '',
+ domain: { min: 0, max: domainLimit },
+ }}
+ />
+ );
+ }
+
+ return ;
+}
+```
+
+### Disable Animations
+
+You can also disable animations by setting the `animate` prop to `false`.
+
+```jsx live
+function AnimatedStackedBars() {
+ const dataCount = 12;
+ const minValue = 20;
+ const maxValue = 100;
+ const minStep = 10;
+ const maxStep = 40;
+ const updateInterval = 600;
+ const seriesSpacing = 30;
+
+ const seriesConfig = [
+ { id: 'red', label: 'Red', color: 'rgb(var(--red40))' },
+ { id: 'orange', label: 'Orange', color: 'rgb(var(--orange40))' },
+ { id: 'yellow', label: 'Yellow', color: 'rgb(var(--yellow40))' },
+ { id: 'green', label: 'Green', color: 'rgb(var(--green40))' },
+ { id: 'blue', label: 'Blue', color: 'rgb(var(--blue40))' },
+ { id: 'indigo', label: 'Indigo', color: 'rgb(var(--indigo40))' },
+ { id: 'purple', label: 'Purple', color: 'rgb(var(--purple40))' },
+ ];
+
+ const domainLimit = maxValue + seriesConfig.length * seriesSpacing;
+
+ function generateNextValue(previousValue) {
+ const range = maxStep - minStep;
+ const offset = Math.random() * range + minStep;
+
+ let direction;
+ if (previousValue >= maxValue) {
+ direction = -1;
+ } else if (previousValue <= minValue) {
+ direction = 1;
+ } else {
+ direction = Math.random() < 0.5 ? -1 : 1;
+ }
+
+ let newValue = previousValue + offset * direction;
+ newValue = Math.max(minValue, Math.min(maxValue, newValue));
+ return newValue;
+ }
+
+ function generateInitialData() {
+ const data = [];
+
+ let previousValue = minValue + Math.random() * (maxValue - minValue);
+ data.push(previousValue);
-You can set the `BarComponent` prop to render a custom component for bars.
+ for (let i = 1; i < dataCount; i++) {
+ const newValue = generateNextValue(previousValue);
+ data.push(newValue);
+ previousValue = newValue;
+ }
+
+ return data;
+ }
+
+ function AnimatedChart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((currentData) => {
+ const lastValue = currentData[currentData.length - 1] ?? minValue;
+ const newValue = generateNextValue(lastValue);
+
+ return [...currentData.slice(1), newValue];
+ });
+ }, updateInterval);
+
+ return () => clearInterval(intervalId);
+ }, []);
+
+ const series = seriesConfig.map((config, index) => ({
+ id: config.id,
+ label: config.label,
+ color: config.color,
+ data: index === 0 ? data : Array(dataCount).fill(seriesSpacing),
+ }));
+
+ return (
+ '',
+ domain: { min: 0, max: domainLimit },
+ }}
+ />
+ );
+ }
+
+ return ;
+}
+```
+
+## Composed Examples
+
+### Candlesticks
+
+You can render a candlestick chart by setting the `BarComponent` prop to a custom candlestick component.
```jsx live
function Candlesticks() {
@@ -698,10 +1013,26 @@ function Candlesticks() {
const candlesData = stockData.map((data) => [parseFloat(data.low), parseFloat(data.high)]);
+ const staggerDelay = 0.25;
+
const CandlestickBarComponent = memo(({ x, y, width, height, originY, dataX, ...props }) => {
- const { getYScale } = useCartesianChartContext();
+ const { getYScale, drawingArea } = useCartesianChartContext();
const yScale = getYScale();
+ const normalizedX = React.useMemo(
+ () => (drawingArea.width > 0 ? (x - drawingArea.x) / drawingArea.width : 0),
+ [x, drawingArea.x, drawingArea.width],
+ );
+
+ const transition = React.useMemo(
+ () => ({
+ type: 'tween',
+ duration: 0.325,
+ delay: normalizedX * staggerDelay,
+ }),
+ [normalizedX],
+ );
+
const wickX = x + width / 2;
const timePeriodValue = stockData[dataX];
@@ -718,10 +1049,10 @@ function Candlesticks() {
const bodyY = openY < closeY ? openY : closeY;
return (
-
+
-
+
);
});
@@ -795,7 +1126,6 @@ function Candlesticks() {
showYAxis
BarComponent={CandlestickBarComponent}
BarStackComponent={({ children }) => {children}}
- animate={false}
borderRadius={0}
height={{ base: 150, tablet: 200, desktop: 250 }}
inset={{ top: 8, bottom: 8, left: 0, right: 0 }}
@@ -830,142 +1160,103 @@ function Candlesticks() {
}
```
-#### Outlined Stacks
-
-You can set the `BarStackComponent` prop to render a custom component for stacks.
+### Monthly Sunlight
+
+You can combine custom and BarPlot components and transitions to create a springy sunlight chart.
+
+```tsx live
+function SunlightChartExample() {
+ const dayLength = 1440;
+ type SunlightChartData = Array<{
+ label: string;
+ value: number;
+ }>;
+ const sunlightData: SunlightChartData = [
+ { label: 'Jan', value: 598 },
+ { label: 'Feb', value: 635 },
+ { label: 'Mar', value: 688 },
+ { label: 'Apr', value: 753 },
+ { label: 'May', value: 812 },
+ { label: 'Jun', value: 855 },
+ { label: 'Jul', value: 861 },
+ { label: 'Aug', value: 828 },
+ { label: 'Sep', value: 772 },
+ { label: 'Oct', value: 710 },
+ { label: 'Nov', value: 648 },
+ { label: 'Dec', value: 605 },
+ ];
-```jsx live
-function MonthlyRewards() {
- const CustomBarStackComponent = ({ children, ...props }) => {
+ function SunlightChart({
+ data,
+ height = 300,
+ ...props
+ }: Omit & { data: SunlightChartData }) {
return (
- <>
-
- {children}
- >
- );
- };
-
- return (
- {
- if (value === 'D') {
- return {value};
- }
- return value;
- },
- }}
- yAxis={{ range: ({ min, max }) => ({ min, max: max - 4 }) }}
- style={{ margin: '0 auto' }}
- />
- );
-}
-```
-
-## Custom Transitions
-
-You can customize the transition animations for your bar chart using the `transition` prop.
-This allows you to control enter, update, and exit animations separately.
-
-```jsx live
-function UpdatingChartValues() {
- const [data, setData] = React.useState([45, 80, 120, 95, 150, 110, 85]);
- const [nullIndex, setNullIndex] = React.useState(null);
-
- const displayData = React.useMemo(() => {
- if (nullIndex === null) return data;
- return data.map((d, i) => (i === nullIndex ? null : d));
- }, [data, nullIndex]);
-
- return (
-
-
-
-
-
-
- Default Animations
- value),
+ yAxisId: 'sunlight',
+ color: 'rgb(var(--yellow40))',
+ },
+ {
+ id: 'day',
+ data: data.map(() => dayLength),
+ yAxisId: 'day',
+ color: 'rgb(var(--blue100))',
},
]}
- showXAxis
- showYAxis
xAxis={{
- data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
- }}
- yAxis={{
- showGrid: true,
- domain: { max: 250 },
+ ...props.xAxis,
+ scaleType: 'band',
+ data: data.map(({ label }) => label),
}}
- />
-
- Custom Update Animations
-
-
- );
+ >
+
+
+
+
+
+ );
+ }
+
+ function Example() {
+ return (
+
+
+
+ 2026 sunlight data for the first day of each month in Atlanta, Georgia, provided by{' '}
+
+ NOAA
+
+ .
+
+
+ );
+ }
+
+ return ;
}
```
diff --git a/apps/docs/docs/components/graphs/BarChart/_webPropsTable.mdx b/apps/docs/docs/components/charts/BarChart/_webPropsTable.mdx
similarity index 100%
rename from apps/docs/docs/components/graphs/BarChart/_webPropsTable.mdx
rename to apps/docs/docs/components/charts/BarChart/_webPropsTable.mdx
diff --git a/apps/docs/docs/components/graphs/BarChart/index.mdx b/apps/docs/docs/components/charts/BarChart/index.mdx
similarity index 100%
rename from apps/docs/docs/components/graphs/BarChart/index.mdx
rename to apps/docs/docs/components/charts/BarChart/index.mdx
diff --git a/apps/docs/docs/components/graphs/BarChart/mobileMetadata.json b/apps/docs/docs/components/charts/BarChart/mobileMetadata.json
similarity index 85%
rename from apps/docs/docs/components/graphs/BarChart/mobileMetadata.json
rename to apps/docs/docs/components/charts/BarChart/mobileMetadata.json
index d2037bf2bb..f46f2c24da 100644
--- a/apps/docs/docs/components/graphs/BarChart/mobileMetadata.json
+++ b/apps/docs/docs/components/charts/BarChart/mobileMetadata.json
@@ -5,15 +5,15 @@
"relatedComponents": [
{
"label": "CartesianChart",
- "url": "/components/graphs/CartesianChart/"
+ "url": "/components/charts/CartesianChart/"
},
{
"label": "XAxis",
- "url": "/components/graphs/XAxis/"
+ "url": "/components/charts/XAxis/"
},
{
"label": "YAxis",
- "url": "/components/graphs/YAxis/"
+ "url": "/components/charts/YAxis/"
}
],
"dependencies": [
diff --git a/apps/docs/docs/components/graphs/BarChart/webMetadata.json b/apps/docs/docs/components/charts/BarChart/webMetadata.json
similarity index 84%
rename from apps/docs/docs/components/graphs/BarChart/webMetadata.json
rename to apps/docs/docs/components/charts/BarChart/webMetadata.json
index 52774ec1d3..633c6ff959 100644
--- a/apps/docs/docs/components/graphs/BarChart/webMetadata.json
+++ b/apps/docs/docs/components/charts/BarChart/webMetadata.json
@@ -6,15 +6,15 @@
"relatedComponents": [
{
"label": "CartesianChart",
- "url": "/components/graphs/CartesianChart/"
+ "url": "/components/charts/CartesianChart/"
},
{
"label": "XAxis",
- "url": "/components/graphs/XAxis/"
+ "url": "/components/charts/XAxis/"
},
{
"label": "YAxis",
- "url": "/components/graphs/YAxis/"
+ "url": "/components/charts/YAxis/"
}
],
"dependencies": [
diff --git a/apps/docs/docs/components/graphs/CartesianChart/_mobileExamples.mdx b/apps/docs/docs/components/charts/CartesianChart/_mobileExamples.mdx
similarity index 74%
rename from apps/docs/docs/components/graphs/CartesianChart/_mobileExamples.mdx
rename to apps/docs/docs/components/charts/CartesianChart/_mobileExamples.mdx
index b56d6d7833..b84132a4b5 100644
--- a/apps/docs/docs/components/graphs/CartesianChart/_mobileExamples.mdx
+++ b/apps/docs/docs/components/charts/CartesianChart/_mobileExamples.mdx
@@ -2,7 +2,7 @@ CartesianChart is a customizable, SVG based component that can be used to displa
## Basics
-[AreaChart](/components/graphs/AreaChart/), [BarChart](/components/graphs/BarChart/), and [LineChart](/components/graphs/LineChart/) are built on top of CartesianChart and have default functionality for your chart.
+[AreaChart](/components/charts/AreaChart/), [BarChart](/components/charts/BarChart/), and [LineChart](/components/charts/LineChart/) are built on top of CartesianChart and have default functionality for your chart.
```jsx
@@ -277,7 +277,7 @@ You can configure your x and y axes with the `xAxis` and `yAxis` props. `xAxis`
```
-For more info, learn about [XAxis](/components/graphs/XAxis/#axis-config) and [YAxis](/components/graphs/YAxis/#axis-config) configuration.
+For more info, learn about [XAxis](/components/charts/XAxis/#axis-config) and [YAxis](/components/charts/YAxis/#axis-config) configuration.
## Inset
@@ -421,6 +421,254 @@ By default, the scrubber will not allow overflow gestures. You can allow overflo
```
+## Animations
+
+CartesianChart delegates transition control to its child components. Each `Line`, `Area`, and `Bar` accepts a `transitions` prop with `enter` (reveal animation) and `update` (data-change animation) keys. Set either to `null` to disable that phase. You can also disable all animations chart-wide by passing `animate={false}` on CartesianChart.
+
+Because transitions live on the children, a single chart can mix behaviors — for example a Line that morphs smoothly while a Bar snaps instantly.
+
+### Enter Only
+
+Disable the update morph animation while keeping a slow enter reveal. Data changes snap instantly but the initial chart appearance animates. Useful when new data arrives frequently and morphing would be distracting.
+
+```tsx
+function EnterAnimationOnly() {
+ const dataCount = 15;
+ const updateInterval = 2500;
+
+ function generateNextValue(prev: number) {
+ const step = Math.random() * 30 - 15;
+ return Math.max(0, Math.min(100, prev + step));
+ }
+
+ function generateInitialData() {
+ const data = [50];
+ for (let i = 1; i < dataCount; i++) {
+ data.push(generateNextValue(data[i - 1]));
+ }
+ return data;
+ }
+
+ function Chart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+
+ );
+ }
+
+ return ;
+}
+```
+
+### Update Only
+
+Disable the enter reveal animation while keeping a slow update morph. The chart appears instantly but data changes animate smoothly. Useful when the chart is embedded in content that should not animate on load.
+
+```tsx
+function UpdateAnimationOnly() {
+ const dataCount = 15;
+ const updateInterval = 2500;
+
+ function generateNextValue(prev: number) {
+ const step = Math.random() * 30 - 15;
+ return Math.max(0, Math.min(100, prev + step));
+ }
+
+ function generateInitialData() {
+ const data = [50];
+ for (let i = 1; i < dataCount; i++) {
+ data.push(generateNextValue(data[i - 1]));
+ }
+ return data;
+ }
+
+ function Chart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+
+ );
+ }
+
+ return ;
+}
+```
+
+### Mixed Transitions Per Child
+
+Each child component can define its own transitions independently. Here, the `Line` uses a spring morph while the bars snap with no update animation. This lets you fine-tune each visual layer within a single chart.
+
+```tsx
+function MixedTransitions() {
+ const theme = useTheme();
+ const dataCount = 10;
+ const updateInterval = 2000;
+
+ function generateNextValue(prev: number) {
+ const step = Math.random() * 20 - 10;
+ return Math.max(10, Math.min(100, prev + step));
+ }
+
+ function generateInitialData() {
+ const data = [50];
+ for (let i = 1; i < dataCount; i++) {
+ data.push(generateNextValue(data[i - 1]));
+ }
+ return data;
+ }
+
+ function Chart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+ d * 0.3),
+ color: theme.color.accentBoldPurple,
+ yAxisId: 'bars',
+ },
+ ]}
+ xAxis={{ scaleType: 'band' }}
+ yAxis={[
+ { id: 'default' },
+ { id: 'bars', range: ({ min, max }) => ({ min: max - 48, max }) },
+ ]}
+ >
+
+
+
+ );
+ }
+
+ return ;
+}
+```
+
+### No Animations
+
+You can disable all animations chart-wide by setting `animate` to `false` on CartesianChart. This is useful for static snapshots or when performance is a concern. Compare this to the animated examples above — data still updates, but changes snap instantly without any transition.
+
+```tsx
+function DisableAnimations() {
+ const dataCount = 15;
+ const updateInterval = 2500;
+
+ function generateNextValue(prev: number) {
+ const step = Math.random() * 30 - 15;
+ return Math.max(0, Math.min(100, prev + step));
+ }
+
+ function generateInitialData() {
+ const data = [50];
+ for (let i = 1; i < dataCount; i++) {
+ data.push(generateNextValue(data[i - 1]));
+ }
+ return data;
+ }
+
+ function Chart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+
+ );
+ }
+
+ return ;
+}
+```
+
## Customization
### Price with Volume
diff --git a/apps/docs/docs/components/graphs/CartesianChart/_mobilePropsTable.mdx b/apps/docs/docs/components/charts/CartesianChart/_mobilePropsTable.mdx
similarity index 100%
rename from apps/docs/docs/components/graphs/CartesianChart/_mobilePropsTable.mdx
rename to apps/docs/docs/components/charts/CartesianChart/_mobilePropsTable.mdx
diff --git a/apps/docs/docs/components/graphs/CartesianChart/_webExamples.mdx b/apps/docs/docs/components/charts/CartesianChart/_webExamples.mdx
similarity index 72%
rename from apps/docs/docs/components/graphs/CartesianChart/_webExamples.mdx
rename to apps/docs/docs/components/charts/CartesianChart/_webExamples.mdx
index a5343805e7..ca6be6d7f2 100644
--- a/apps/docs/docs/components/graphs/CartesianChart/_webExamples.mdx
+++ b/apps/docs/docs/components/charts/CartesianChart/_webExamples.mdx
@@ -2,7 +2,7 @@ CartesianChart is a customizable, SVG based component that can be used to displa
## Basic Example
-[AreaChart](/components/graphs/AreaChart/), [BarChart](/components/graphs/BarChart/), and [LineChart](/components/graphs/LineChart/) are built on top of CartesianChart and have default functionality for your chart.
+[AreaChart](/components/charts/AreaChart/), [BarChart](/components/charts/BarChart/), and [LineChart](/components/charts/LineChart/) are built on top of CartesianChart and have default functionality for your chart.
```jsx live
@@ -244,7 +244,7 @@ You can configure your x and y axes with the `xAxis` and `yAxis` props. `xAxis`
```
-For more info, learn about [XAxis](/components/graphs/XAxis/#axis-config) and [YAxis](/components/graphs/YAxis/#axis-config) configuration.
+For more info, learn about [XAxis](/components/charts/XAxis/#axis-config) and [YAxis](/components/charts/YAxis/#axis-config) configuration.
## Inset
@@ -364,6 +364,255 @@ function Scrubbing() {
}
```
+## Animations
+
+CartesianChart delegates transition control to its child components. You can also disable all animations chart-wide by passing `animate={false}` on CartesianChart.
+
+### Enter Only
+
+Disable the update morph animation while keeping a slow enter reveal. Data changes snap instantly but the initial chart appearance animates. Useful when new data arrives frequently and morphing would be distracting.
+
+```tsx live
+function EnterAnimationOnly() {
+ const dataCount = 15;
+ const updateInterval = 2500;
+
+ function generateNextValue(prev: number) {
+ const step = Math.random() * 30 - 15;
+ return Math.max(0, Math.min(100, prev + step));
+ }
+
+ function generateInitialData() {
+ const data = [50];
+ for (let i = 1; i < dataCount; i++) {
+ data.push(generateNextValue(data[i - 1]));
+ }
+ return data;
+ }
+
+ function Chart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+
+ );
+ }
+
+ return ;
+}
+```
+
+### Update Only
+
+Disable the enter reveal animation while keeping a slow update morph. The chart appears instantly but data changes animate smoothly. Useful when the chart is embedded in content that should not animate on load.
+
+```tsx live
+function UpdateAnimationOnly() {
+ const dataCount = 15;
+ const updateInterval = 2500;
+
+ function generateNextValue(prev: number) {
+ const step = Math.random() * 30 - 15;
+ return Math.max(0, Math.min(100, prev + step));
+ }
+
+ function generateInitialData() {
+ const data = [50];
+ for (let i = 1; i < dataCount; i++) {
+ data.push(generateNextValue(data[i - 1]));
+ }
+ return data;
+ }
+
+ function Chart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+
+ );
+ }
+
+ return ;
+}
+```
+
+### Mixed Transitions Per Child
+
+Each child component can define its own transitions independently. Here, the `Line` uses a spring morph while the bars snap with no update animation. This lets you fine-tune each visual layer within a single chart.
+
+```tsx live
+function MixedTransitions() {
+ const dataCount = 10;
+ const updateInterval = 2000;
+
+ function generateNextValue(prev: number) {
+ const step = Math.random() * 20 - 10;
+ return Math.max(10, Math.min(100, prev + step));
+ }
+
+ function generateInitialData() {
+ const data = [50];
+ for (let i = 1; i < dataCount; i++) {
+ data.push(generateNextValue(data[i - 1]));
+ }
+ return data;
+ }
+
+ function Chart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+ d * 0.3),
+ color: 'var(--color-accentBoldPurple)',
+ yAxisId: 'bars',
+ },
+ ]}
+ xAxis={{ scaleType: 'band' }}
+ yAxis={[
+ { id: 'default' },
+ { id: 'bars', range: ({ min, max }) => ({ min: max - 48, max }) },
+ ]}
+ aria-hidden="true"
+ >
+
+
+
+ );
+ }
+
+ return ;
+}
+```
+
+### No Animations
+
+You can disable all animations chart-wide by setting `animate` to `false` on CartesianChart. This is useful for static snapshots or when performance is a concern. Compare this to the animated examples above — data still updates, but changes snap instantly without any transition.
+
+```tsx live
+function DisableAnimations() {
+ const dataCount = 15;
+ const updateInterval = 2500;
+
+ function generateNextValue(prev: number) {
+ const step = Math.random() * 30 - 15;
+ return Math.max(0, Math.min(100, prev + step));
+ }
+
+ function generateInitialData() {
+ const data = [50];
+ for (let i = 1; i < dataCount; i++) {
+ data.push(generateNextValue(data[i - 1]));
+ }
+ return data;
+ }
+
+ function Chart() {
+ const [data, setData] = useState(generateInitialData);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+
+ );
+ }
+
+ return ;
+}
+```
+
## Customization
### Price with Volume
diff --git a/apps/docs/docs/components/graphs/CartesianChart/_webPropsTable.mdx b/apps/docs/docs/components/charts/CartesianChart/_webPropsTable.mdx
similarity index 100%
rename from apps/docs/docs/components/graphs/CartesianChart/_webPropsTable.mdx
rename to apps/docs/docs/components/charts/CartesianChart/_webPropsTable.mdx
diff --git a/apps/docs/docs/components/graphs/CartesianChart/index.mdx b/apps/docs/docs/components/charts/CartesianChart/index.mdx
similarity index 100%
rename from apps/docs/docs/components/graphs/CartesianChart/index.mdx
rename to apps/docs/docs/components/charts/CartesianChart/index.mdx
diff --git a/apps/docs/docs/components/graphs/CartesianChart/mobileMetadata.json b/apps/docs/docs/components/charts/CartesianChart/mobileMetadata.json
similarity index 80%
rename from apps/docs/docs/components/graphs/CartesianChart/mobileMetadata.json
rename to apps/docs/docs/components/charts/CartesianChart/mobileMetadata.json
index 109b74d3bd..abaf43dac8 100644
--- a/apps/docs/docs/components/graphs/CartesianChart/mobileMetadata.json
+++ b/apps/docs/docs/components/charts/CartesianChart/mobileMetadata.json
@@ -5,23 +5,23 @@
"relatedComponents": [
{
"label": "Point",
- "url": "/components/graphs/Point/"
+ "url": "/components/charts/Point/"
},
{
"label": "ReferenceLine",
- "url": "/components/graphs/ReferenceLine/"
+ "url": "/components/charts/ReferenceLine/"
},
{
"label": "Scrubber",
- "url": "/components/graphs/Scrubber/"
+ "url": "/components/charts/Scrubber/"
},
{
"label": "XAxis",
- "url": "/components/graphs/XAxis/"
+ "url": "/components/charts/XAxis/"
},
{
"label": "YAxis",
- "url": "/components/graphs/YAxis/"
+ "url": "/components/charts/YAxis/"
}
],
"dependencies": [
diff --git a/apps/docs/docs/components/graphs/CartesianChart/webMetadata.json b/apps/docs/docs/components/charts/CartesianChart/webMetadata.json
similarity index 70%
rename from apps/docs/docs/components/graphs/CartesianChart/webMetadata.json
rename to apps/docs/docs/components/charts/CartesianChart/webMetadata.json
index 8f55e33f81..3b029ade16 100644
--- a/apps/docs/docs/components/graphs/CartesianChart/webMetadata.json
+++ b/apps/docs/docs/components/charts/CartesianChart/webMetadata.json
@@ -6,29 +6,24 @@
"relatedComponents": [
{
"label": "Point",
- "url": "/components/graphs/Point/"
+ "url": "/components/charts/Point/"
},
{
"label": "ReferenceLine",
- "url": "/components/graphs/ReferenceLine/"
+ "url": "/components/charts/ReferenceLine/"
},
{
"label": "Scrubber",
- "url": "/components/graphs/Scrubber/"
+ "url": "/components/charts/Scrubber/"
},
{
"label": "XAxis",
- "url": "/components/graphs/XAxis/"
+ "url": "/components/charts/XAxis/"
},
{
"label": "YAxis",
- "url": "/components/graphs/YAxis/"
+ "url": "/components/charts/YAxis/"
}
],
- "dependencies": [
- {
- "name": "framer-motion",
- "version": "^10.18.0"
- }
- ]
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/graphs/Legend/_mobileExamples.mdx b/apps/docs/docs/components/charts/Legend/_mobileExamples.mdx
similarity index 100%
rename from apps/docs/docs/components/graphs/Legend/_mobileExamples.mdx
rename to apps/docs/docs/components/charts/Legend/_mobileExamples.mdx
diff --git a/apps/docs/docs/components/graphs/Legend/_mobilePropsTable.mdx b/apps/docs/docs/components/charts/Legend/_mobilePropsTable.mdx
similarity index 100%
rename from apps/docs/docs/components/graphs/Legend/_mobilePropsTable.mdx
rename to apps/docs/docs/components/charts/Legend/_mobilePropsTable.mdx
diff --git a/apps/docs/docs/components/graphs/Legend/_webExamples.mdx b/apps/docs/docs/components/charts/Legend/_webExamples.mdx
similarity index 100%
rename from apps/docs/docs/components/graphs/Legend/_webExamples.mdx
rename to apps/docs/docs/components/charts/Legend/_webExamples.mdx
diff --git a/apps/docs/docs/components/graphs/Legend/_webPropsTable.mdx b/apps/docs/docs/components/charts/Legend/_webPropsTable.mdx
similarity index 100%
rename from apps/docs/docs/components/graphs/Legend/_webPropsTable.mdx
rename to apps/docs/docs/components/charts/Legend/_webPropsTable.mdx
diff --git a/apps/docs/docs/components/graphs/Legend/index.mdx b/apps/docs/docs/components/charts/Legend/index.mdx
similarity index 100%
rename from apps/docs/docs/components/graphs/Legend/index.mdx
rename to apps/docs/docs/components/charts/Legend/index.mdx
diff --git a/apps/docs/docs/components/graphs/Legend/mobileMetadata.json b/apps/docs/docs/components/charts/Legend/mobileMetadata.json
similarity index 66%
rename from apps/docs/docs/components/graphs/Legend/mobileMetadata.json
rename to apps/docs/docs/components/charts/Legend/mobileMetadata.json
index caf6999301..7ce6bbd017 100644
--- a/apps/docs/docs/components/graphs/Legend/mobileMetadata.json
+++ b/apps/docs/docs/components/charts/Legend/mobileMetadata.json
@@ -5,29 +5,21 @@
"relatedComponents": [
{
"label": "CartesianChart",
- "url": "/components/graphs/CartesianChart/"
+ "url": "/components/charts/CartesianChart/"
},
{
"label": "LineChart",
- "url": "/components/graphs/LineChart/"
+ "url": "/components/charts/LineChart/"
},
{
"label": "BarChart",
- "url": "/components/graphs/BarChart/"
+ "url": "/components/charts/BarChart/"
}
],
"dependencies": [
{
"name": "@shopify/react-native-skia",
"version": "^1.12.4 || ^2.0.0"
- },
- {
- "name": "react-native-gesture-handler",
- "version": "^2.16.2"
- },
- {
- "name": "react-native-reanimated",
- "version": "^3.14.0"
}
]
}
diff --git a/apps/docs/docs/components/graphs/Legend/webMetadata.json b/apps/docs/docs/components/charts/Legend/webMetadata.json
similarity index 67%
rename from apps/docs/docs/components/graphs/Legend/webMetadata.json
rename to apps/docs/docs/components/charts/Legend/webMetadata.json
index 056ff17267..10d2b3ad05 100644
--- a/apps/docs/docs/components/graphs/Legend/webMetadata.json
+++ b/apps/docs/docs/components/charts/Legend/webMetadata.json
@@ -5,21 +5,16 @@
"relatedComponents": [
{
"label": "CartesianChart",
- "url": "/components/graphs/CartesianChart/"
+ "url": "/components/charts/CartesianChart/"
},
{
"label": "LineChart",
- "url": "/components/graphs/LineChart/"
+ "url": "/components/charts/LineChart/"
},
{
"label": "BarChart",
- "url": "/components/graphs/BarChart/"
+ "url": "/components/charts/BarChart/"
}
],
- "dependencies": [
- {
- "name": "framer-motion",
- "version": "^10.18.0"
- }
- ]
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/graphs/LineChart/_mobileExamples.mdx b/apps/docs/docs/components/charts/LineChart/_mobileExamples.mdx
similarity index 97%
rename from apps/docs/docs/components/graphs/LineChart/_mobileExamples.mdx
rename to apps/docs/docs/components/charts/LineChart/_mobileExamples.mdx
index 263dbbbb8c..d030257e30 100644
--- a/apps/docs/docs/components/graphs/LineChart/_mobileExamples.mdx
+++ b/apps/docs/docs/components/charts/LineChart/_mobileExamples.mdx
@@ -1,8 +1,8 @@
-LineChart is a wrapper for [CartesianChart](/components/graphs/CartesianChart) that makes it easy to create standard line charts, supporting a single x/y axis pair. Charts are built using `@shopify/react-native-skia`.
+LineChart is a wrapper for [CartesianChart](/components/charts/CartesianChart) that makes it easy to create standard line charts, supporting a single x/y axis pair. Charts are built using `@shopify/react-native-skia`.
## Setup
-Before using LineChart, you need to wrap your app with `ChartBridgeProvider`. This enables charts to access CDS theming and other React contexts within the Skia renderer. See [CartesianChart](/components/graphs/CartesianChart/#setup) for details.
+Before using LineChart, you need to wrap your app with `ChartBridgeProvider`. This enables charts to access CDS theming and other React contexts within the Skia renderer. See [CartesianChart](/components/charts/CartesianChart/#setup) for details.
## Basics
@@ -249,7 +249,7 @@ function EmptyState() {
### Scales
-LineChart uses `linear` scaling on axes by default, but you can also use other types, such as `log`. See [XAxis](/components/graphs/XAxis) and [YAxis](/components/graphs/YAxis) for more information.
+LineChart uses `linear` scaling on axes by default, but you can also use other types, such as `log`. See [XAxis](/components/charts/XAxis) and [YAxis](/components/charts/YAxis) for more information.
```jsx
);
@@ -739,7 +739,7 @@ function BasicAccessible() {
### Axes
-Using `showXAxis` and `showYAxis` allows you to display the axes. For more information, such as adjusting domain and range, see [XAxis](/components/graphs/XAxis) and [YAxis](/components/graphs/YAxis).
+Using `showXAxis` and `showYAxis` allows you to display the axes. For more information, such as adjusting domain and range, see [XAxis](/components/charts/XAxis) and [YAxis](/components/charts/YAxis).
```tsx
```
-You can also add instances of [ReferenceLine](/components/graphs/ReferenceLine) to your LineChart to highlight a specific x or y value.
+You can also add instances of [ReferenceLine](/components/charts/ReferenceLine) to your LineChart to highlight a specific x or y value.
```jsx
);
@@ -627,7 +627,7 @@ function AccessibleWithHeader() {
### Axes
-Using `showXAxis` and `showYAxis` allows you to display the axes. For more information, such as adjusting domain and range, see [XAxis](/components/graphs/XAxis) and [YAxis](/components/graphs/YAxis).
+Using `showXAxis` and `showYAxis` allows you to display the axes. For more information, such as adjusting domain and range, see [XAxis](/components/charts/XAxis) and [YAxis](/components/charts/YAxis).
```jsx live
```
-You can also add instances of [ReferenceLine](/components/graphs/ReferenceLine) to your LineChart to highlight a specific x or y value.
+You can also add instances of [ReferenceLine](/components/charts/ReferenceLine) to your LineChart to highlight a specific x or y value.
```jsx live
-```
-
-## Simple
-
-```jsx
-
-```
-
-## Stacking
-
-You can use the `stacked` prop to stack all areas on top of each other. You can also use the `stackId` prop on a series to create different stack groups. See [CartesianChart](/components/graphs/CartesianChart/#series-stacks) for more details.
-
-```jsx
-function StackingExample() {
- const theme = useTheme();
- return (
- }
- type="dotted"
- />
- );
-}
-```
-
-## Negative Values
-
-When an area chart contains negative values, the baseline automatically adjusts to zero instead of the bottom of the chart. The area fills from the data line to the zero baseline, properly showing both positive and negative regions.
-
-```jsx
- }
- showYAxis
- yAxis={{
- showGrid: true,
- }}
-/>
-```
-
-## Area Styles
-
-You can have different area styles for each series.
-
-```jsx
-
-```
diff --git a/apps/docs/docs/components/graphs/BarChart/_mobileExamples.mdx b/apps/docs/docs/components/graphs/BarChart/_mobileExamples.mdx
deleted file mode 100644
index 245f147f70..0000000000
--- a/apps/docs/docs/components/graphs/BarChart/_mobileExamples.mdx
+++ /dev/null
@@ -1,663 +0,0 @@
-## Basic Example
-
-Bar charts are a useful component for comparing discrete categories of data.
-They are helpful for highlighting trends to users or allowing them to compare proportions at a glance.
-
-To start, pass in a series of data to the chart.
-
-```jsx
-
-```
-
-## Multiple Series
-
-You can also provide multiple series of data to the chart. Series will have their bars for each data point rendered side by side.
-
-```tsx
-function MonthlyGainsByAsset() {
- const ThinSolidLine = memo((props: SolidLineProps) => );
-
- const tickFormatter = useCallback(
- (amount) =>
- new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- }).format(amount),
- [],
- );
-
- return (
-
- );
-}
-```
-
-## Series Stacking
-
-You can also configure stacking for your chart using the `stacked` prop.
-
-```tsx
-function MonthlyGainsByAsset() {
- const ThinSolidLine = memo((props: SolidLineProps) => );
-
- const tickFormatter = useCallback(
- (amount) =>
- new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- }).format(amount),
- [],
- );
-
- return (
-
- );
-}
-```
-
-You can also configure multiple stacks by setting the `stackId` prop on each series.
-
-```tsx
-function MonthlyGainsMultipleStacks() {
- const ThinSolidLine = memo((props: SolidLineProps) => );
-
- const tickFormatter = useCallback(
- (amount) =>
- new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- }).format(amount),
- [],
- );
-
- return (
-
- );
-}
-```
-
-### Stack Gap
-
-```tsx
-function MonthlyGainsByAsset() {
- const ThinSolidLine = memo((props: SolidLineProps) => );
-
- const tickFormatter = useCallback(
- (amount) =>
- new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- }).format(amount),
- [],
- );
-
- return (
-
- );
-}
-```
-
-## Border Radius
-
-Bars have a default border radius of `100`. You can change this by setting the `borderRadius` prop on the chart.
-
-Stacks will only round the top corners of touching bars.
-
-```jsx
- {
- if (value === 'D') {
- return {value};
- }
- return value;
- },
- }}
- style={{ margin: '0 auto' }}
-/>
-```
-
-### Round Baseline
-
-You can also round the baseline of the bars by setting the `roundBaseline` prop on the chart.
-
-```jsx
- {
- if (value === 'D') {
- return {value};
- }
- return value;
- },
- }}
- style={{ margin: '0 auto' }}
-/>
-```
-
-## Negative Data
-
-```tsx
-function PositiveAndNegativeCashFlow() {
- const ThinSolidLine = memo((props: SolidLineProps) => );
-
- const categories = Array.from({ length: 31 }, (_, i) => `3/${i + 1}`);
- const gains = [
- 5, 0, 6, 18, 0, 5, 12, 0, 12, 22, 28, 18, 0, 12, 6, 0, 0, 24, 0, 0, 4, 0, 18, 0, 0, 14, 10, 16,
- 0, 0, 0,
- ];
-
- const losses = [
- -4, 0, -8, -12, -6, 0, 0, 0, -18, 0, -12, 0, -9, -6, 0, 0, 0, 0, -22, -8, 0, 0, -10, -14, 0, 0,
- 0, 0, 0, -12, -10,
- ];
- const series = [
- { id: 'gains', data: gains, color: 'var(--color-fgPositive)' },
- { id: 'losses', data: losses, color: 'var(--color-fgNegative)' },
- ];
-
- return (
- `$${value}M`,
- }}
- />
- );
-}
-```
-
-## Missing Bars
-
-You can pass in `null` or `0` values to not render a bar for that data point.
-
-```jsx
- `$${value}k`,
- showGrid: true,
- showTickMarks: true,
- showLine: true,
- tickMarkSize: 1.5,
- domain: { max: 50 },
- }}
-/>
-```
-
-You can also use the `BarStackComponent` prop to render an empty circle for zero values.
-
-```tsx
-function MonthlyRewards() {
- const months = ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'];
- const currentMonth = 7;
- const purple = [null, 6, 8, 10, 7, 6, 6, 8, null, null, null, null];
- const blue = [null, 10, 12, 11, 10, 9, 10, 11, null, null, null, null];
- const cyan = [null, 7, 10, 12, 11, 10, 8, 11, null, null, null, null];
- const green = [10, null, null, null, 1, null, null, 6, null, null, null, null];
-
- const series = [
- { id: 'purple', data: purple, color: '#b399ff' },
- { id: 'blue', data: blue, color: '#4f7cff' },
- { id: 'cyan', data: cyan, color: '#00c2df' },
- { id: 'green', data: green, color: '#33c481' },
- ];
-
- const CustomBarStackComponent = ({ children, ...props }: BarStackComponentProps) => {
- if (props.height === 0) {
- const diameter = props.width;
- return (
-
- );
- }
-
- return {children};
- };
-
- return (
- {
- if (index == currentMonth) {
- return {months[index]};
- }
- return months[index];
- },
- categoryPadding: 0.27,
- }}
- />
- );
-}
-```
-
-## Customization
-
-### Bar Spacing
-
-There are two ways to control the spacing between bars. You can set the `barPadding` prop to control the spacing between bars within a series. You can also set the `categoryPadding` prop to control the spacing between stacks of bars.
-
-```jsx
-
-```
-
-### Minimum Size
-
-To better emphasize small values, you can set the `stackMinSize` or `barMinSize` prop to control the minimum size for entire stacks or individual bar.
-It is recommended to only use `stackMinSize` for stacked charts and `barMinSize` for non-stacked charts.
-
-#### Minimum Stack Size
-
-You can set the `stackMinSize` prop to control the minimum size for entire stacks. This will only apply to stacks that have a value that is not `null` or `0`. It will proportionally scale the values of each bar in the stack to reach the minimum size.
-
-```jsx
-
-```
-
-#### Minimum Bar Size
-
-You can also set the `barMinSize` prop to control the minimum size for individual bars. This will only apply to bars that have a value that is not `null` or `0`.
-
-```jsx
- `$${value}k`,
- showGrid: true,
- showTickMarks: true,
- showLine: true,
- tickMarkSize: 1.5,
- domain: { max: 50 },
- }}
- barMinSize={4}
-/>
-```
-
-### Multiple Y Axes
-
-You can render bars from separate y axes in one `BarPlot`, however they aren't able to be stacked.
-
-```jsx
-function MultipleYAxes() {
- const theme = useTheme();
-
- return (
-
-
- `$${value}k`}
- />
- `${value}%`}
- />
-
-
- );
-}
-```
diff --git a/apps/docs/docs/components/inputs/Checkbox/mobileMetadata.json b/apps/docs/docs/components/inputs/Checkbox/mobileMetadata.json
index bef7136e94..07fa93254d 100644
--- a/apps/docs/docs/components/inputs/Checkbox/mobileMetadata.json
+++ b/apps/docs/docs/components/inputs/Checkbox/mobileMetadata.json
@@ -24,5 +24,6 @@
"label": "Switch",
"url": "/components/inputs/Switch/?platform=mobile"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/inputs/CheckboxCell/mobileMetadata.json b/apps/docs/docs/components/inputs/CheckboxCell/mobileMetadata.json
index 3a118b0d5e..8bb9c0ff71 100644
--- a/apps/docs/docs/components/inputs/CheckboxCell/mobileMetadata.json
+++ b/apps/docs/docs/components/inputs/CheckboxCell/mobileMetadata.json
@@ -16,5 +16,6 @@
"label": "RadioCell",
"url": "/components/inputs/RadioCell/?platform=mobile"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/inputs/CheckboxCell/webMetadata.json b/apps/docs/docs/components/inputs/CheckboxCell/webMetadata.json
index 2a66b7bfdf..cb8ba03f5d 100644
--- a/apps/docs/docs/components/inputs/CheckboxCell/webMetadata.json
+++ b/apps/docs/docs/components/inputs/CheckboxCell/webMetadata.json
@@ -17,5 +17,11 @@
"label": "RadioCell",
"url": "/components/inputs/RadioCell/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
]
}
diff --git a/apps/docs/docs/components/inputs/CheckboxGroup/mobileMetadata.json b/apps/docs/docs/components/inputs/CheckboxGroup/mobileMetadata.json
index f8f38cff37..e4e1e13c63 100644
--- a/apps/docs/docs/components/inputs/CheckboxGroup/mobileMetadata.json
+++ b/apps/docs/docs/components/inputs/CheckboxGroup/mobileMetadata.json
@@ -20,5 +20,6 @@
"label": "RadioGroup",
"url": "/components/inputs/RadioGroup"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/inputs/CheckboxGroup/webMetadata.json b/apps/docs/docs/components/inputs/CheckboxGroup/webMetadata.json
index bf0b47a771..4300e5ebb7 100644
--- a/apps/docs/docs/components/inputs/CheckboxGroup/webMetadata.json
+++ b/apps/docs/docs/components/inputs/CheckboxGroup/webMetadata.json
@@ -21,5 +21,11 @@
"label": "RadioGroup",
"url": "/components/inputs/RadioGroup"
}
+ ],
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
]
}
diff --git a/apps/docs/docs/components/inputs/Combobox/mobileMetadata.json b/apps/docs/docs/components/inputs/Combobox/mobileMetadata.json
index a506ad520f..77ea5e689d 100644
--- a/apps/docs/docs/components/inputs/Combobox/mobileMetadata.json
+++ b/apps/docs/docs/components/inputs/Combobox/mobileMetadata.json
@@ -12,8 +12,8 @@
],
"dependencies": [
{
- "name": "fuse.js",
- "version": "^6.6.2"
+ "name": "react-native-safe-area-context",
+ "version": "^4.10.5"
}
]
}
diff --git a/apps/docs/docs/components/inputs/Combobox/webMetadata.json b/apps/docs/docs/components/inputs/Combobox/webMetadata.json
index fe91c1e3b3..4db33d49b9 100644
--- a/apps/docs/docs/components/inputs/Combobox/webMetadata.json
+++ b/apps/docs/docs/components/inputs/Combobox/webMetadata.json
@@ -12,8 +12,12 @@
],
"dependencies": [
{
- "name": "fuse.js",
- "version": "^6.6.2"
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
}
]
}
diff --git a/apps/docs/docs/components/inputs/ControlGroup/mobileMetadata.json b/apps/docs/docs/components/inputs/ControlGroup/mobileMetadata.json
index 9e020f094f..57b7d81acf 100644
--- a/apps/docs/docs/components/inputs/ControlGroup/mobileMetadata.json
+++ b/apps/docs/docs/components/inputs/ControlGroup/mobileMetadata.json
@@ -31,5 +31,6 @@
"label": "Switch",
"url": "/components/inputs/Switch/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/inputs/ControlGroup/webMetadata.json b/apps/docs/docs/components/inputs/ControlGroup/webMetadata.json
index 16eec2d6a2..609d2c9e24 100644
--- a/apps/docs/docs/components/inputs/ControlGroup/webMetadata.json
+++ b/apps/docs/docs/components/inputs/ControlGroup/webMetadata.json
@@ -32,5 +32,6 @@
"label": "Switch",
"url": "/components/inputs/Switch/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/inputs/MediaChip/mobileMetadata.json b/apps/docs/docs/components/inputs/MediaChip/mobileMetadata.json
index 16ae058952..d6c59ebc09 100644
--- a/apps/docs/docs/components/inputs/MediaChip/mobileMetadata.json
+++ b/apps/docs/docs/components/inputs/MediaChip/mobileMetadata.json
@@ -19,5 +19,6 @@
"label": "TabbedChips",
"url": "/components/navigation/TabbedChips/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/inputs/MediaChip/webMetadata.json b/apps/docs/docs/components/inputs/MediaChip/webMetadata.json
index 1deb01bbdd..a26de154bc 100644
--- a/apps/docs/docs/components/inputs/MediaChip/webMetadata.json
+++ b/apps/docs/docs/components/inputs/MediaChip/webMetadata.json
@@ -20,5 +20,6 @@
"label": "TabbedChips",
"url": "/components/navigation/TabbedChips/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/inputs/Radio/mobileMetadata.json b/apps/docs/docs/components/inputs/Radio/mobileMetadata.json
index 155dc8010b..6ef25ea309 100644
--- a/apps/docs/docs/components/inputs/Radio/mobileMetadata.json
+++ b/apps/docs/docs/components/inputs/Radio/mobileMetadata.json
@@ -27,8 +27,8 @@
],
"dependencies": [
{
- "name": "framer-motion",
- "version": "^10.18.0"
+ "name": "react-native-svg",
+ "version": "^14.1.0"
}
]
}
diff --git a/apps/docs/docs/components/inputs/Radio/webMetadata.json b/apps/docs/docs/components/inputs/Radio/webMetadata.json
index 5dd80ea86a..d7c43966c4 100644
--- a/apps/docs/docs/components/inputs/Radio/webMetadata.json
+++ b/apps/docs/docs/components/inputs/Radio/webMetadata.json
@@ -25,5 +25,11 @@
"label": "Switch",
"url": "/components/inputs/Switch"
}
+ ],
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
]
}
diff --git a/apps/docs/docs/components/inputs/RadioCell/mobileMetadata.json b/apps/docs/docs/components/inputs/RadioCell/mobileMetadata.json
index b1caa8484f..685965c943 100644
--- a/apps/docs/docs/components/inputs/RadioCell/mobileMetadata.json
+++ b/apps/docs/docs/components/inputs/RadioCell/mobileMetadata.json
@@ -16,5 +16,11 @@
"label": "Radio",
"url": "/components/inputs/Radio/?platform=mobile"
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
]
}
diff --git a/apps/docs/docs/components/inputs/RadioCell/webMetadata.json b/apps/docs/docs/components/inputs/RadioCell/webMetadata.json
index 4fe91b8b58..1fd5a4a886 100644
--- a/apps/docs/docs/components/inputs/RadioCell/webMetadata.json
+++ b/apps/docs/docs/components/inputs/RadioCell/webMetadata.json
@@ -17,5 +17,11 @@
"label": "Radio",
"url": "/components/inputs/Radio/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
]
}
diff --git a/apps/docs/docs/components/inputs/RadioGroup/mobileMetadata.json b/apps/docs/docs/components/inputs/RadioGroup/mobileMetadata.json
index 66941645bc..c594a0a22a 100644
--- a/apps/docs/docs/components/inputs/RadioGroup/mobileMetadata.json
+++ b/apps/docs/docs/components/inputs/RadioGroup/mobileMetadata.json
@@ -14,5 +14,11 @@
"url": "/components/inputs/ControlGroup/",
"description": "ControlGroup is a component that allows users to group related controls together."
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
]
}
diff --git a/apps/docs/docs/components/inputs/RadioGroup/webMetadata.json b/apps/docs/docs/components/inputs/RadioGroup/webMetadata.json
index 9bf256ba21..ad8a650600 100644
--- a/apps/docs/docs/components/inputs/RadioGroup/webMetadata.json
+++ b/apps/docs/docs/components/inputs/RadioGroup/webMetadata.json
@@ -13,5 +13,11 @@
"label": "ControlGroup",
"url": "/components/inputs/ControlGroup/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
]
}
diff --git a/apps/docs/docs/components/inputs/Select/webMetadata.json b/apps/docs/docs/components/inputs/Select/webMetadata.json
index a6bac0619e..135c57e756 100644
--- a/apps/docs/docs/components/inputs/Select/webMetadata.json
+++ b/apps/docs/docs/components/inputs/Select/webMetadata.json
@@ -23,5 +23,14 @@
"url": "/components/inputs/SelectChip/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/inputs/SelectAlpha/mobileMetadata.json b/apps/docs/docs/components/inputs/SelectAlpha/mobileMetadata.json
index b3746563b4..db9d2c6ec2 100644
--- a/apps/docs/docs/components/inputs/SelectAlpha/mobileMetadata.json
+++ b/apps/docs/docs/components/inputs/SelectAlpha/mobileMetadata.json
@@ -5,5 +5,10 @@
"description": "A flexible select component for both single and multi-selection, built for mobile applications with comprehensive accessibility support.",
"alpha": true,
"relatedComponents": [],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-safe-area-context",
+ "version": "^4.10.5"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/inputs/SelectAlpha/webMetadata.json b/apps/docs/docs/components/inputs/SelectAlpha/webMetadata.json
index bc1cbd1e0c..59cda3ea44 100644
--- a/apps/docs/docs/components/inputs/SelectAlpha/webMetadata.json
+++ b/apps/docs/docs/components/inputs/SelectAlpha/webMetadata.json
@@ -5,5 +5,14 @@
"description": "A flexible select component for both single and multi-selection, built for web applications with comprehensive accessibility support.",
"alpha": true,
"relatedComponents": [],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/inputs/SelectChip/mobileMetadata.json b/apps/docs/docs/components/inputs/SelectChip/mobileMetadata.json
index cf540a6000..036d3ca9a0 100644
--- a/apps/docs/docs/components/inputs/SelectChip/mobileMetadata.json
+++ b/apps/docs/docs/components/inputs/SelectChip/mobileMetadata.json
@@ -22,5 +22,10 @@
"url": "/components/navigation/TabbedChips/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-safe-area-context",
+ "version": "^4.10.5"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/inputs/SelectChip/webMetadata.json b/apps/docs/docs/components/inputs/SelectChip/webMetadata.json
index bf1af8fede..fc3dc6f6c9 100644
--- a/apps/docs/docs/components/inputs/SelectChip/webMetadata.json
+++ b/apps/docs/docs/components/inputs/SelectChip/webMetadata.json
@@ -23,5 +23,14 @@
"url": "/components/navigation/TabbedChips/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/inputs/SelectChipAlpha/mobileMetadata.json b/apps/docs/docs/components/inputs/SelectChipAlpha/mobileMetadata.json
index 8e40b52b48..b8be5679bb 100644
--- a/apps/docs/docs/components/inputs/SelectChipAlpha/mobileMetadata.json
+++ b/apps/docs/docs/components/inputs/SelectChipAlpha/mobileMetadata.json
@@ -17,5 +17,11 @@
"label": "SelectChip",
"url": "/components/inputs/SelectChip/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-native-safe-area-context",
+ "version": "^4.10.5"
+ }
]
}
diff --git a/apps/docs/docs/components/inputs/SelectChipAlpha/webMetadata.json b/apps/docs/docs/components/inputs/SelectChipAlpha/webMetadata.json
index 26cb8d05b8..f7088fafeb 100644
--- a/apps/docs/docs/components/inputs/SelectChipAlpha/webMetadata.json
+++ b/apps/docs/docs/components/inputs/SelectChipAlpha/webMetadata.json
@@ -18,5 +18,15 @@
"label": "SelectChip",
"url": "/components/inputs/SelectChip/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
]
}
diff --git a/apps/docs/docs/components/inputs/TileButton/webMetadata.json b/apps/docs/docs/components/inputs/TileButton/webMetadata.json
index 6cbc699b4e..b634795cd0 100644
--- a/apps/docs/docs/components/inputs/TileButton/webMetadata.json
+++ b/apps/docs/docs/components/inputs/TileButton/webMetadata.json
@@ -13,5 +13,6 @@
"label": "IconButton",
"url": "/components/inputs/IconButton/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/layout/AccordionItem/mobileMetadata.json b/apps/docs/docs/components/layout/AccordionItem/mobileMetadata.json
index 939d020196..8cbed53d2b 100644
--- a/apps/docs/docs/components/layout/AccordionItem/mobileMetadata.json
+++ b/apps/docs/docs/components/layout/AccordionItem/mobileMetadata.json
@@ -9,5 +9,10 @@
"url": "/components/layout/Accordion/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-reanimated",
+ "version": "^3.14.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/layout/AccordionItem/webMetadata.json b/apps/docs/docs/components/layout/AccordionItem/webMetadata.json
index dc5adf5339..b06e1b1a01 100644
--- a/apps/docs/docs/components/layout/AccordionItem/webMetadata.json
+++ b/apps/docs/docs/components/layout/AccordionItem/webMetadata.json
@@ -10,5 +10,10 @@
"url": "/components/layout/Accordion/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/layout/Carousel/mobileMetadata.json b/apps/docs/docs/components/layout/Carousel/mobileMetadata.json
index 62883cd02e..588489fc1d 100644
--- a/apps/docs/docs/components/layout/Carousel/mobileMetadata.json
+++ b/apps/docs/docs/components/layout/Carousel/mobileMetadata.json
@@ -2,5 +2,6 @@
"import": "import { Carousel } from '@coinbase/cds-mobile/carousel/Carousel'",
"source": "https://github.com/coinbase/cds/blob/master/packages/mobile/src/carousel/Carousel.tsx",
"figma": "https://www.figma.com/design/XRzGLH42ezSda4UYVfPMMO/Carousel?node-id=26293-2427",
- "description": "A flexible carousel component for displaying sequences of content with navigation and pagination options."
+ "description": "A flexible carousel component for displaying sequences of content with navigation and pagination options.",
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/layout/Carousel/webMetadata.json b/apps/docs/docs/components/layout/Carousel/webMetadata.json
index 2f8c9a7d60..934c419038 100644
--- a/apps/docs/docs/components/layout/Carousel/webMetadata.json
+++ b/apps/docs/docs/components/layout/Carousel/webMetadata.json
@@ -3,5 +3,11 @@
"source": "https://github.com/coinbase/cds/blob/master/packages/web/src/carousel/Carousel.tsx",
"storybook": "https://cds-storybook.coinbase.com/?path=/story/components-carousel--all",
"figma": "https://www.figma.com/design/XRzGLH42ezSda4UYVfPMMO/Carousel?node-id=26293-2427",
- "description": "A flexible carousel component for displaying sequences of content with navigation and pagination options."
+ "description": "A flexible carousel component for displaying sequences of content with navigation and pagination options.",
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/layout/Dropdown/webMetadata.json b/apps/docs/docs/components/layout/Dropdown/webMetadata.json
index 8f5872c118..d0df1b6cc6 100644
--- a/apps/docs/docs/components/layout/Dropdown/webMetadata.json
+++ b/apps/docs/docs/components/layout/Dropdown/webMetadata.json
@@ -14,5 +14,14 @@
"url": "/components/inputs/SelectChip/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/media/Avatar/mobileMetadata.json b/apps/docs/docs/components/media/Avatar/mobileMetadata.json
index ae31ed58b7..1e9ac28249 100644
--- a/apps/docs/docs/components/media/Avatar/mobileMetadata.json
+++ b/apps/docs/docs/components/media/Avatar/mobileMetadata.json
@@ -12,5 +12,10 @@
"url": "/components/other/DotCount/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/media/Avatar/webMetadata.json b/apps/docs/docs/components/media/Avatar/webMetadata.json
index 2657c73422..8570ed2307 100644
--- a/apps/docs/docs/components/media/Avatar/webMetadata.json
+++ b/apps/docs/docs/components/media/Avatar/webMetadata.json
@@ -13,5 +13,10 @@
"url": "/components/other/DotCount/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/media/CellMedia/mobileMetadata.json b/apps/docs/docs/components/media/CellMedia/mobileMetadata.json
index e2b1126f14..3ad2f79f6f 100644
--- a/apps/docs/docs/components/media/CellMedia/mobileMetadata.json
+++ b/apps/docs/docs/components/media/CellMedia/mobileMetadata.json
@@ -12,5 +12,10 @@
"url": "/components/data-display/ListCell"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/media/HeroSquare/mobileMetadata.json b/apps/docs/docs/components/media/HeroSquare/mobileMetadata.json
index bea11194e7..f9eb2b2ea7 100644
--- a/apps/docs/docs/components/media/HeroSquare/mobileMetadata.json
+++ b/apps/docs/docs/components/media/HeroSquare/mobileMetadata.json
@@ -22,5 +22,10 @@
"url": "/components/media/Pictogram/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/media/LogoMark/mobileMetadata.json b/apps/docs/docs/components/media/LogoMark/mobileMetadata.json
index 7ffdea2742..a2eca6c388 100644
--- a/apps/docs/docs/components/media/LogoMark/mobileMetadata.json
+++ b/apps/docs/docs/components/media/LogoMark/mobileMetadata.json
@@ -15,5 +15,11 @@
"label": "SubBrandLogoWordMark",
"url": "/components/media/SubBrandLogoWordMark/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
]
}
diff --git a/apps/docs/docs/components/media/LogoMark/webMetadata.json b/apps/docs/docs/components/media/LogoMark/webMetadata.json
index 25eb26cfb3..4593b8cd77 100644
--- a/apps/docs/docs/components/media/LogoMark/webMetadata.json
+++ b/apps/docs/docs/components/media/LogoMark/webMetadata.json
@@ -16,5 +16,6 @@
"label": "SubBrandLogoWordMark",
"url": "/components/media/SubBrandLogoWordMark/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/media/LogoWordMark/mobileMetadata.json b/apps/docs/docs/components/media/LogoWordMark/mobileMetadata.json
index 566de1074f..a06f647034 100644
--- a/apps/docs/docs/components/media/LogoWordMark/mobileMetadata.json
+++ b/apps/docs/docs/components/media/LogoWordMark/mobileMetadata.json
@@ -15,5 +15,11 @@
"label": "SubBrandLogoWordMark",
"url": "/components/media/SubBrandLogoWordMark/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
]
}
diff --git a/apps/docs/docs/components/media/LogoWordMark/webMetadata.json b/apps/docs/docs/components/media/LogoWordMark/webMetadata.json
index 928c6d3150..b69c804473 100644
--- a/apps/docs/docs/components/media/LogoWordMark/webMetadata.json
+++ b/apps/docs/docs/components/media/LogoWordMark/webMetadata.json
@@ -16,5 +16,6 @@
"label": "SubBrandLogoWordMark",
"url": "/components/media/SubBrandLogoWordMark/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/media/Pictogram/mobileMetadata.json b/apps/docs/docs/components/media/Pictogram/mobileMetadata.json
index 193d4e199b..30aedc1c64 100644
--- a/apps/docs/docs/components/media/Pictogram/mobileMetadata.json
+++ b/apps/docs/docs/components/media/Pictogram/mobileMetadata.json
@@ -22,5 +22,10 @@
"url": "/components/media/SpotRectangle/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/media/RemoteImage/mobileMetadata.json b/apps/docs/docs/components/media/RemoteImage/mobileMetadata.json
index 95868febe6..ff0b14bfe2 100644
--- a/apps/docs/docs/components/media/RemoteImage/mobileMetadata.json
+++ b/apps/docs/docs/components/media/RemoteImage/mobileMetadata.json
@@ -22,10 +22,6 @@
}
],
"dependencies": [
- {
- "name": "react-native-svg",
- "version": "^14.1.0"
- },
{
"name": "react-native-svg",
"version": "^14.1.0"
diff --git a/apps/docs/docs/components/media/RemoteImageGroup/mobileMetadata.json b/apps/docs/docs/components/media/RemoteImageGroup/mobileMetadata.json
index 0368c7f144..109cd18957 100644
--- a/apps/docs/docs/components/media/RemoteImageGroup/mobileMetadata.json
+++ b/apps/docs/docs/components/media/RemoteImageGroup/mobileMetadata.json
@@ -7,5 +7,11 @@
"label": "RemoteImage",
"url": "/components/media/RemoteImage"
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
]
}
diff --git a/apps/docs/docs/components/media/RemoteImageGroup/webMetadata.json b/apps/docs/docs/components/media/RemoteImageGroup/webMetadata.json
index 2437780eb8..74fd9ce1c3 100644
--- a/apps/docs/docs/components/media/RemoteImageGroup/webMetadata.json
+++ b/apps/docs/docs/components/media/RemoteImageGroup/webMetadata.json
@@ -8,5 +8,6 @@
"label": "RemoteImage",
"url": "/components/media/RemoteImage"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/media/SpotIcon/mobileMetadata.json b/apps/docs/docs/components/media/SpotIcon/mobileMetadata.json
index 4dde7cf0a4..c5697db759 100644
--- a/apps/docs/docs/components/media/SpotIcon/mobileMetadata.json
+++ b/apps/docs/docs/components/media/SpotIcon/mobileMetadata.json
@@ -22,5 +22,10 @@
"url": "/components/media/Pictogram/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/media/SpotRectangle/mobileMetadata.json b/apps/docs/docs/components/media/SpotRectangle/mobileMetadata.json
index 19164301c8..f8817f27ba 100644
--- a/apps/docs/docs/components/media/SpotRectangle/mobileMetadata.json
+++ b/apps/docs/docs/components/media/SpotRectangle/mobileMetadata.json
@@ -22,5 +22,10 @@
"url": "/components/media/Pictogram/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/media/SpotSquare/mobileMetadata.json b/apps/docs/docs/components/media/SpotSquare/mobileMetadata.json
index 4a0a58b9e6..03c6bc3376 100644
--- a/apps/docs/docs/components/media/SpotSquare/mobileMetadata.json
+++ b/apps/docs/docs/components/media/SpotSquare/mobileMetadata.json
@@ -22,5 +22,10 @@
"url": "/components/media/Pictogram/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/media/SubBrandLogoMark/mobileMetadata.json b/apps/docs/docs/components/media/SubBrandLogoMark/mobileMetadata.json
index 3d8e7d1685..16d4c6ef1a 100644
--- a/apps/docs/docs/components/media/SubBrandLogoMark/mobileMetadata.json
+++ b/apps/docs/docs/components/media/SubBrandLogoMark/mobileMetadata.json
@@ -15,5 +15,11 @@
"label": "LogoWordMark",
"url": "/components/media/LogoWordMark/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
]
}
diff --git a/apps/docs/docs/components/media/SubBrandLogoMark/webMetadata.json b/apps/docs/docs/components/media/SubBrandLogoMark/webMetadata.json
index 6acdd99191..daebcf0c2f 100644
--- a/apps/docs/docs/components/media/SubBrandLogoMark/webMetadata.json
+++ b/apps/docs/docs/components/media/SubBrandLogoMark/webMetadata.json
@@ -16,5 +16,6 @@
"label": "LogoWordMark",
"url": "/components/media/LogoWordMark/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/media/SubBrandLogoWordMark/mobileMetadata.json b/apps/docs/docs/components/media/SubBrandLogoWordMark/mobileMetadata.json
index c8dc146392..d0c8956cf6 100644
--- a/apps/docs/docs/components/media/SubBrandLogoWordMark/mobileMetadata.json
+++ b/apps/docs/docs/components/media/SubBrandLogoWordMark/mobileMetadata.json
@@ -15,5 +15,11 @@
"label": "LogoMark",
"url": "/components/media/LogoMark/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
]
}
diff --git a/apps/docs/docs/components/media/SubBrandLogoWordMark/webMetadata.json b/apps/docs/docs/components/media/SubBrandLogoWordMark/webMetadata.json
index 43dbbe392c..64d8efe087 100644
--- a/apps/docs/docs/components/media/SubBrandLogoWordMark/webMetadata.json
+++ b/apps/docs/docs/components/media/SubBrandLogoWordMark/webMetadata.json
@@ -16,5 +16,6 @@
"label": "LogoMark",
"url": "/components/media/LogoMark/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/navigation/BrowserBar/mobileMetadata.json b/apps/docs/docs/components/navigation/BrowserBar/mobileMetadata.json
index a8f518517a..ecc956fceb 100644
--- a/apps/docs/docs/components/navigation/BrowserBar/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/BrowserBar/mobileMetadata.json
@@ -8,5 +8,11 @@
"label": "TopNavBar",
"url": "/components/navigation/TopNavBar/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-native-reanimated",
+ "version": "^3.14.0"
+ }
]
}
diff --git a/apps/docs/docs/components/navigation/Coachmark/mobileMetadata.json b/apps/docs/docs/components/navigation/Coachmark/mobileMetadata.json
index 3f662a998e..3fb20e1696 100644
--- a/apps/docs/docs/components/navigation/Coachmark/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/Coachmark/mobileMetadata.json
@@ -8,5 +8,6 @@
"label": "Tour",
"url": "/components/navigation/Tour/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/navigation/Coachmark/webMetadata.json b/apps/docs/docs/components/navigation/Coachmark/webMetadata.json
index 016d179215..e8e6d59b67 100644
--- a/apps/docs/docs/components/navigation/Coachmark/webMetadata.json
+++ b/apps/docs/docs/components/navigation/Coachmark/webMetadata.json
@@ -9,5 +9,6 @@
"label": "Tour",
"url": "/components/navigation/Tour/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/navigation/NavigationTitle/mobileMetadata.json b/apps/docs/docs/components/navigation/NavigationTitle/mobileMetadata.json
index 1d6122fd11..36bce7323e 100644
--- a/apps/docs/docs/components/navigation/NavigationTitle/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/NavigationTitle/mobileMetadata.json
@@ -11,5 +11,6 @@
"label": "BrowserBar",
"url": "/components/navigation/BrowserBar/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/navigation/NavigationTitle/webMetadata.json b/apps/docs/docs/components/navigation/NavigationTitle/webMetadata.json
index e26924c031..bc6dd4c579 100644
--- a/apps/docs/docs/components/navigation/NavigationTitle/webMetadata.json
+++ b/apps/docs/docs/components/navigation/NavigationTitle/webMetadata.json
@@ -9,5 +9,6 @@
"label": "NavigationBar",
"url": "/components/navigation/NavigationBar/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/navigation/NavigationTitleSelect/mobileMetadata.json b/apps/docs/docs/components/navigation/NavigationTitleSelect/mobileMetadata.json
index 21313e9b78..3388cd780d 100644
--- a/apps/docs/docs/components/navigation/NavigationTitleSelect/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/NavigationTitleSelect/mobileMetadata.json
@@ -19,5 +19,6 @@
"label": "SelectOption",
"url": "/components/inputs/SelectOption"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/navigation/NavigationTitleSelect/webMetadata.json b/apps/docs/docs/components/navigation/NavigationTitleSelect/webMetadata.json
index 2452a9837d..f51f4917f8 100644
--- a/apps/docs/docs/components/navigation/NavigationTitleSelect/webMetadata.json
+++ b/apps/docs/docs/components/navigation/NavigationTitleSelect/webMetadata.json
@@ -16,5 +16,15 @@
"label": "SelectOption",
"url": "/components/inputs/SelectOption"
}
+ ],
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
]
}
diff --git a/apps/docs/docs/components/navigation/SegmentedTabs/mobileMetadata.json b/apps/docs/docs/components/navigation/SegmentedTabs/mobileMetadata.json
index 1296db4821..f0bbda8767 100644
--- a/apps/docs/docs/components/navigation/SegmentedTabs/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/SegmentedTabs/mobileMetadata.json
@@ -13,5 +13,10 @@
"url": "/components/navigation/TabNavigation/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-reanimated",
+ "version": "^3.14.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/navigation/SegmentedTabs/webMetadata.json b/apps/docs/docs/components/navigation/SegmentedTabs/webMetadata.json
index 47364d7966..241626941d 100644
--- a/apps/docs/docs/components/navigation/SegmentedTabs/webMetadata.json
+++ b/apps/docs/docs/components/navigation/SegmentedTabs/webMetadata.json
@@ -14,5 +14,10 @@
"url": "/components/navigation/TabNavigation/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/navigation/SidebarItem/webMetadata.json b/apps/docs/docs/components/navigation/SidebarItem/webMetadata.json
index 45003138f8..648e617b7a 100644
--- a/apps/docs/docs/components/navigation/SidebarItem/webMetadata.json
+++ b/apps/docs/docs/components/navigation/SidebarItem/webMetadata.json
@@ -14,5 +14,10 @@
"url": "/components/navigation/SidebarMoreMenu/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/navigation/SidebarMoreMenu/webMetadata.json b/apps/docs/docs/components/navigation/SidebarMoreMenu/webMetadata.json
index 1d6ab21b87..5fcaf9184d 100644
--- a/apps/docs/docs/components/navigation/SidebarMoreMenu/webMetadata.json
+++ b/apps/docs/docs/components/navigation/SidebarMoreMenu/webMetadata.json
@@ -22,5 +22,10 @@
"url": "/components/inputs/SelectOption/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/navigation/Stepper/mobileMetadata.json b/apps/docs/docs/components/navigation/Stepper/mobileMetadata.json
index 6ee726c9a7..5d5510f986 100644
--- a/apps/docs/docs/components/navigation/Stepper/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/Stepper/mobileMetadata.json
@@ -1,5 +1,6 @@
{
"import": "import { Stepper } from '@coinbase/cds-mobile/stepper/Stepper'",
"source": "https://github.com/coinbase/cds/blob/master/packages/mobile/src/stepper/Stepper.tsx",
- "description": "A component that visualizes states within a multi-step process."
+ "description": "A component that visualizes states within a multi-step process.",
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/navigation/Stepper/webMetadata.json b/apps/docs/docs/components/navigation/Stepper/webMetadata.json
index 0ec304f38a..d7b28d8e3e 100644
--- a/apps/docs/docs/components/navigation/Stepper/webMetadata.json
+++ b/apps/docs/docs/components/navigation/Stepper/webMetadata.json
@@ -2,5 +2,6 @@
"import": "import { Stepper } from '@coinbase/cds-web/stepper/Stepper'",
"source": "https://github.com/coinbase/cds/blob/master/packages/web/src/stepper/Stepper.tsx",
"storybook": "https://cds-storybook.coinbase.com/?path=/story/components-stepper-horizontal--default",
- "description": "A component that visualizes states within a multi-step process."
+ "description": "A component that visualizes states within a multi-step process.",
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/navigation/TabLabel/mobileMetadata.json b/apps/docs/docs/components/navigation/TabLabel/mobileMetadata.json
index aa37cc1e65..322496548a 100644
--- a/apps/docs/docs/components/navigation/TabLabel/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/TabLabel/mobileMetadata.json
@@ -12,5 +12,10 @@
"url": "/components/navigation/TabIndicator/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-reanimated",
+ "version": "^3.14.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/navigation/TabLabel/webMetadata.json b/apps/docs/docs/components/navigation/TabLabel/webMetadata.json
index 59805977c9..0cfca5a265 100644
--- a/apps/docs/docs/components/navigation/TabLabel/webMetadata.json
+++ b/apps/docs/docs/components/navigation/TabLabel/webMetadata.json
@@ -13,5 +13,10 @@
"url": "/components/navigation/TabIndicator/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/navigation/TabNavigation/mobileMetadata.json b/apps/docs/docs/components/navigation/TabNavigation/mobileMetadata.json
index f45ffc076c..4b2b7ef553 100644
--- a/apps/docs/docs/components/navigation/TabNavigation/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/TabNavigation/mobileMetadata.json
@@ -18,5 +18,10 @@
"url": "/components/navigation/SegmentedTabs/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-reanimated",
+ "version": "^3.14.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/navigation/TabNavigation/webMetadata.json b/apps/docs/docs/components/navigation/TabNavigation/webMetadata.json
index 26d3a0c3ae..8a50467c8c 100644
--- a/apps/docs/docs/components/navigation/TabNavigation/webMetadata.json
+++ b/apps/docs/docs/components/navigation/TabNavigation/webMetadata.json
@@ -19,5 +19,10 @@
"url": "/components/navigation/SegmentedTabs/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/navigation/TabbedChips/mobileMetadata.json b/apps/docs/docs/components/navigation/TabbedChips/mobileMetadata.json
index de97e24713..745a786e7b 100644
--- a/apps/docs/docs/components/navigation/TabbedChips/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/TabbedChips/mobileMetadata.json
@@ -10,5 +10,10 @@
"url": "/components/inputs/SelectChip/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-reanimated",
+ "version": "^3.14.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/navigation/TabbedChips/webMetadata.json b/apps/docs/docs/components/navigation/TabbedChips/webMetadata.json
index a0b040318b..d740465b0b 100644
--- a/apps/docs/docs/components/navigation/TabbedChips/webMetadata.json
+++ b/apps/docs/docs/components/navigation/TabbedChips/webMetadata.json
@@ -11,5 +11,10 @@
"url": "/components/inputs/SelectChip/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/navigation/TabbedChipsAlpha/mobileMetadata.json b/apps/docs/docs/components/navigation/TabbedChipsAlpha/mobileMetadata.json
index c9b5956f0a..98412d5d70 100644
--- a/apps/docs/docs/components/navigation/TabbedChipsAlpha/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/TabbedChipsAlpha/mobileMetadata.json
@@ -25,5 +25,11 @@
"label": "SelectChip",
"url": "/components/inputs/SelectChip/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-native-reanimated",
+ "version": "^3.14.0"
+ }
]
}
diff --git a/apps/docs/docs/components/navigation/TabbedChipsAlpha/webMetadata.json b/apps/docs/docs/components/navigation/TabbedChipsAlpha/webMetadata.json
index e6f591c5c8..8ccb13f533 100644
--- a/apps/docs/docs/components/navigation/TabbedChipsAlpha/webMetadata.json
+++ b/apps/docs/docs/components/navigation/TabbedChipsAlpha/webMetadata.json
@@ -26,5 +26,11 @@
"label": "SelectChip",
"url": "/components/inputs/SelectChip/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
]
}
diff --git a/apps/docs/docs/components/navigation/Tabs/mobileMetadata.json b/apps/docs/docs/components/navigation/Tabs/mobileMetadata.json
index 5254b63d15..851ba23c75 100644
--- a/apps/docs/docs/components/navigation/Tabs/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/Tabs/mobileMetadata.json
@@ -12,5 +12,11 @@
"label": "SegmentedTabs",
"url": "/components/navigation/SegmentedTabs/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-native-reanimated",
+ "version": "^3.14.0"
+ }
]
}
diff --git a/apps/docs/docs/components/navigation/Tabs/webMetadata.json b/apps/docs/docs/components/navigation/Tabs/webMetadata.json
index 25e290d848..cefe3817ee 100644
--- a/apps/docs/docs/components/navigation/Tabs/webMetadata.json
+++ b/apps/docs/docs/components/navigation/Tabs/webMetadata.json
@@ -11,5 +11,11 @@
"label": "SegmentedTabs",
"url": "/components/navigation/SegmentedTabs"
}
+ ],
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
]
}
diff --git a/apps/docs/docs/components/navigation/TopNavBar/mobileMetadata.json b/apps/docs/docs/components/navigation/TopNavBar/mobileMetadata.json
index a199005ce2..3fd09a8015 100644
--- a/apps/docs/docs/components/navigation/TopNavBar/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/TopNavBar/mobileMetadata.json
@@ -8,5 +8,11 @@
"label": "BrowserBar",
"url": "/components/navigation/BrowserBar/"
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-native-reanimated",
+ "version": "^3.14.0"
+ }
]
}
diff --git a/apps/docs/docs/components/navigation/Tour/mobileMetadata.json b/apps/docs/docs/components/navigation/Tour/mobileMetadata.json
index fcf58439eb..dcb490e8f9 100644
--- a/apps/docs/docs/components/navigation/Tour/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/Tour/mobileMetadata.json
@@ -8,5 +8,10 @@
"url": "/components/navigation/Coachmark/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/navigation/Tour/webMetadata.json b/apps/docs/docs/components/navigation/Tour/webMetadata.json
index 58ad6838e1..53c33ef98b 100644
--- a/apps/docs/docs/components/navigation/Tour/webMetadata.json
+++ b/apps/docs/docs/components/navigation/Tour/webMetadata.json
@@ -9,5 +9,10 @@
"url": "/components/navigation/Coachmark/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/numbers/RollingNumber/mobileMetadata.json b/apps/docs/docs/components/numbers/RollingNumber/mobileMetadata.json
index 47af1d7479..989c9eeae0 100644
--- a/apps/docs/docs/components/numbers/RollingNumber/mobileMetadata.json
+++ b/apps/docs/docs/components/numbers/RollingNumber/mobileMetadata.json
@@ -4,5 +4,10 @@
"description": "A numeric display that animates value changes with rolling digits.",
"figma": "",
"relatedComponents": [],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-reanimated",
+ "version": "^3.14.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/numbers/RollingNumber/webMetadata.json b/apps/docs/docs/components/numbers/RollingNumber/webMetadata.json
index 2a60e94a21..06f27ff5bd 100644
--- a/apps/docs/docs/components/numbers/RollingNumber/webMetadata.json
+++ b/apps/docs/docs/components/numbers/RollingNumber/webMetadata.json
@@ -5,5 +5,10 @@
"storybook": "https://cds-storybook.coinbase.com/?path=/story/components-rollingnumber--examples",
"figma": "",
"relatedComponents": [],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/other/Calendar/webMetadata.json b/apps/docs/docs/components/other/Calendar/webMetadata.json
index 4d65d5f999..1ac432167b 100644
--- a/apps/docs/docs/components/other/Calendar/webMetadata.json
+++ b/apps/docs/docs/components/other/Calendar/webMetadata.json
@@ -8,5 +8,11 @@
"label": "DatePicker",
"url": "/components/other/DatePicker"
}
+ ],
+ "dependencies": [
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
]
}
diff --git a/apps/docs/docs/components/other/DateInput/mobileMetadata.json b/apps/docs/docs/components/other/DateInput/mobileMetadata.json
index cd5fc1edc3..dc1cbeabe8 100644
--- a/apps/docs/docs/components/other/DateInput/mobileMetadata.json
+++ b/apps/docs/docs/components/other/DateInput/mobileMetadata.json
@@ -12,5 +12,6 @@
"label": "TextInput",
"url": "/components/inputs/TextInput/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/other/DateInput/webMetadata.json b/apps/docs/docs/components/other/DateInput/webMetadata.json
index 90512b1f13..82d4c6ffe7 100644
--- a/apps/docs/docs/components/other/DateInput/webMetadata.json
+++ b/apps/docs/docs/components/other/DateInput/webMetadata.json
@@ -17,5 +17,6 @@
"label": "Calendar",
"url": "/components/other/Calendar/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/other/DatePicker/webMetadata.json b/apps/docs/docs/components/other/DatePicker/webMetadata.json
index ee1d8e73e8..c785ad8c75 100644
--- a/apps/docs/docs/components/other/DatePicker/webMetadata.json
+++ b/apps/docs/docs/components/other/DatePicker/webMetadata.json
@@ -26,6 +26,10 @@
{
"name": "framer-motion",
"version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
}
]
}
diff --git a/apps/docs/docs/components/other/DotSymbol/mobileMetadata.json b/apps/docs/docs/components/other/DotSymbol/mobileMetadata.json
index 357872c4a0..aed0ba53c3 100644
--- a/apps/docs/docs/components/other/DotSymbol/mobileMetadata.json
+++ b/apps/docs/docs/components/other/DotSymbol/mobileMetadata.json
@@ -21,5 +21,10 @@
"url": "/components/media/Icon/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-svg",
+ "version": "^14.1.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/other/MediaQueryProvider/webMetadata.json b/apps/docs/docs/components/other/MediaQueryProvider/webMetadata.json
index 1b0df5620c..e8b0e1171b 100644
--- a/apps/docs/docs/components/other/MediaQueryProvider/webMetadata.json
+++ b/apps/docs/docs/components/other/MediaQueryProvider/webMetadata.json
@@ -12,5 +12,6 @@
"label": "useBreakpoints",
"url": "/hooks/useBreakpoints/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/other/ThemeProvider/mobileMetadata.json b/apps/docs/docs/components/other/ThemeProvider/mobileMetadata.json
index 226cb3b663..e7ad83629f 100644
--- a/apps/docs/docs/components/other/ThemeProvider/mobileMetadata.json
+++ b/apps/docs/docs/components/other/ThemeProvider/mobileMetadata.json
@@ -7,5 +7,6 @@
"label": "useTheme",
"url": "/hooks/useTheme/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/other/ThemeProvider/webMetadata.json b/apps/docs/docs/components/other/ThemeProvider/webMetadata.json
index e5e7133f9c..2da20ef255 100644
--- a/apps/docs/docs/components/other/ThemeProvider/webMetadata.json
+++ b/apps/docs/docs/components/other/ThemeProvider/webMetadata.json
@@ -8,5 +8,6 @@
"label": "useTheme",
"url": "/hooks/useTheme/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/overlay/Alert/webMetadata.json b/apps/docs/docs/components/overlay/Alert/webMetadata.json
index bb40b63213..c89a519b50 100644
--- a/apps/docs/docs/components/overlay/Alert/webMetadata.json
+++ b/apps/docs/docs/components/overlay/Alert/webMetadata.json
@@ -10,5 +10,14 @@
"url": "/components/overlay/Modal/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/overlay/FocusTrap/webMetadata.json b/apps/docs/docs/components/overlay/FocusTrap/webMetadata.json
index b71c051f80..5d326accb5 100644
--- a/apps/docs/docs/components/overlay/FocusTrap/webMetadata.json
+++ b/apps/docs/docs/components/overlay/FocusTrap/webMetadata.json
@@ -12,5 +12,6 @@
"label": "Tray",
"url": "/components/overlay/Tray/"
}
- ]
+ ],
+ "dependencies": []
}
diff --git a/apps/docs/docs/components/overlay/FullscreenAlert/webMetadata.json b/apps/docs/docs/components/overlay/FullscreenAlert/webMetadata.json
index 6a4cf18e98..48a8300764 100644
--- a/apps/docs/docs/components/overlay/FullscreenAlert/webMetadata.json
+++ b/apps/docs/docs/components/overlay/FullscreenAlert/webMetadata.json
@@ -18,6 +18,10 @@
{
"name": "framer-motion",
"version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
}
]
}
diff --git a/apps/docs/docs/components/overlay/FullscreenModal/webMetadata.json b/apps/docs/docs/components/overlay/FullscreenModal/webMetadata.json
index 4509787717..9fb8bfb472 100644
--- a/apps/docs/docs/components/overlay/FullscreenModal/webMetadata.json
+++ b/apps/docs/docs/components/overlay/FullscreenModal/webMetadata.json
@@ -14,6 +14,10 @@
{
"name": "framer-motion",
"version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
}
]
}
diff --git a/apps/docs/docs/components/overlay/FullscreenModalLayout/webMetadata.json b/apps/docs/docs/components/overlay/FullscreenModalLayout/webMetadata.json
index d5da6ac20f..94605e2da2 100644
--- a/apps/docs/docs/components/overlay/FullscreenModalLayout/webMetadata.json
+++ b/apps/docs/docs/components/overlay/FullscreenModalLayout/webMetadata.json
@@ -13,5 +13,15 @@
"label": "FullscreenModal",
"url": "/components/overlay/FullscreenModal"
}
+ ],
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
]
}
diff --git a/apps/docs/docs/components/overlay/Modal/webMetadata.json b/apps/docs/docs/components/overlay/Modal/webMetadata.json
index e54a73d185..6be86f4db3 100644
--- a/apps/docs/docs/components/overlay/Modal/webMetadata.json
+++ b/apps/docs/docs/components/overlay/Modal/webMetadata.json
@@ -34,6 +34,10 @@
{
"name": "framer-motion",
"version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
}
]
}
diff --git a/apps/docs/docs/components/overlay/Overlay/webMetadata.json b/apps/docs/docs/components/overlay/Overlay/webMetadata.json
index aaa0b4fcce..cbbf09367b 100644
--- a/apps/docs/docs/components/overlay/Overlay/webMetadata.json
+++ b/apps/docs/docs/components/overlay/Overlay/webMetadata.json
@@ -20,5 +20,10 @@
"url": "/components/overlay/Alert/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/overlay/Toast/webMetadata.json b/apps/docs/docs/components/overlay/Toast/webMetadata.json
index 8da219d6b5..690f61dd3f 100644
--- a/apps/docs/docs/components/overlay/Toast/webMetadata.json
+++ b/apps/docs/docs/components/overlay/Toast/webMetadata.json
@@ -22,6 +22,10 @@
{
"name": "framer-motion",
"version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
}
]
}
diff --git a/apps/docs/docs/components/overlay/Tooltip/webMetadata.json b/apps/docs/docs/components/overlay/Tooltip/webMetadata.json
index 72a03417d9..1355f725a8 100644
--- a/apps/docs/docs/components/overlay/Tooltip/webMetadata.json
+++ b/apps/docs/docs/components/overlay/Tooltip/webMetadata.json
@@ -18,5 +18,14 @@
"url": "/components/overlay/Toast/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/overlay/Tray/mobileMetadata.json b/apps/docs/docs/components/overlay/Tray/mobileMetadata.json
index ac19077a91..2e1051b2e4 100644
--- a/apps/docs/docs/components/overlay/Tray/mobileMetadata.json
+++ b/apps/docs/docs/components/overlay/Tray/mobileMetadata.json
@@ -21,5 +21,10 @@
"url": "/components/overlay/Overlay/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-safe-area-context",
+ "version": "^4.10.5"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/overlay/Tray/webMetadata.json b/apps/docs/docs/components/overlay/Tray/webMetadata.json
index d6937f705d..0d3166435d 100644
--- a/apps/docs/docs/components/overlay/Tray/webMetadata.json
+++ b/apps/docs/docs/components/overlay/Tray/webMetadata.json
@@ -22,5 +22,14 @@
"url": "/components/overlay/Overlay/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "framer-motion",
+ "version": "^10.18.0"
+ },
+ {
+ "name": "react-dom",
+ "version": "^18.3.1"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/typography/Link/_mobileExamples.mdx b/apps/docs/docs/components/typography/Link/_mobileExamples.mdx
index 60ecbdbda6..bb063fbb78 100644
--- a/apps/docs/docs/components/typography/Link/_mobileExamples.mdx
+++ b/apps/docs/docs/components/typography/Link/_mobileExamples.mdx
@@ -1,16 +1,60 @@
-### Default
+Link renders a pressable [Text](/components/typography/Text) element that opens URLs in an in-app browser by default. It inherits parent text styles and supports the same `font` and `color` props as Text.
-Default Link text will inherit parent text styles.
+## Basics
+
+By default, Link inherits the text styles of its parent. Pass a `to` prop to set the destination URL.
```jsx
-Default is inherited text
+
+ Check out the Coinbase homepage for more info.
+
+```
+
+## Underline
+
+Use the `underline` prop to add a text decoration underline to the link. This is important for inline links within body text to ensure they are visually distinguishable from surrounding text.
+
+```jsx
+
+ Read our{' '}
+
+ terms and conditions
+ {' '}
+ before proceeding.
+
+```
+
+### Underline with different fonts
+
+The underline works across all font styles.
+
+```jsx
+
+
+ body link
+
+
+ label1 link
+
+
+ caption link
+
+
+ legal link
+
+
+ title2 link
+
+
```
-### Styling your link
+## Styling
+
+### Font
-To style a `` component, simply wrap the `Link` node in the desired `Text` component, using the `as` prop for your usecase (for example, `p` for body text, `h1`/`h2`/`h3` for titles, etc). However, there may be times when a rendering a span is exactly what you want. In those cases, providing a font prop makes sense.
+To style a Link, either wrap it in the desired [Text](/components/typography/Text) component or use the `font` prop directly on the Link.
-Wrapping your Link in the appropriate Text element
+#### Wrapping in Text
```jsx
@@ -57,9 +101,9 @@ Wrapping your Link in the appropriate Text element
```
-### Using the font prop
+#### Using the font prop
-If you find yourself in a situation where you simply need to style a link as a Text component without wrapping it in the semantically appropriate text element, reach for the `font` prop.
+If you need to style a link without wrapping it in a parent text element, use the `font` prop directly.
```jsx
@@ -71,39 +115,45 @@ If you find yourself in a situation where you simply need to style a link as a T
```
-### Color override
-
-```jsx
-
- With color override
-
-```
+### Color
-### openInNewWindow and rel props
+Override the default link color using the `color` prop.
```jsx
-
- Open window in existing tab
+
+ fgPrimary (default)
-
- Sets rel to noreferrer
+
+ fgNegative
-
- Sets rel to noopener
+
+ fgNegative with underline
```
-### Handling onPress
+## Navigation
+
+### Browser options
+
+Control how the link opens with `forceOpenOutsideApp`, `preventRedirectionIntoApp`, and `readerMode`.
```jsx
- console.log('pressed link')} rel="noopener">
- Handles onPress
-
+
+
+ Opens outside the app
+
+
+ Prevents redirect back into app
+
+
+ Opens in reader mode (iOS only)
+
+
```
-### Accessibility
+## Accessibility
:::tip Accessibility tip
@@ -115,10 +165,25 @@ _The link text must have a 3:1 contrast ratio from the surrounding non-link text
:::
-### A11y for nested link in text
+Use the `underline` prop on inline links within body text to ensure they are distinguishable without relying on color alone.
+
+```jsx
+
+ By continuing, you agree to the{' '}
+
+ Terms of Service
+ {' '}
+ and{' '}
+
+ Privacy Policy
+
+ .
+
+```
+
+### Nested link in text
-React Native will flatten the nested Text into a string and therefore not able to focus internal link for a11y purpose. You can read more about it in the [official documentation](https://reactnative.dev/docs/text#nested-text).
-For better accessibility, we recommend this pattern in mobile.
+React Native flattens nested Text into a string and cannot focus internal links for accessibility. See the [official documentation](https://reactnative.dev/docs/text#nested-text) for details. For better accessibility, use this pattern:
```jsx
import { AccessibilityInfo, Linking } from 'react-native';
@@ -134,7 +199,6 @@ import { AccessibilityInfo, Linking } from 'react-native';
await openURL('https://www.coinbase.com/');
}
} catch (error) {
- // Handle or log the error appropriately
console.error('Error in onPress handler:', error);
}
}}
@@ -144,11 +208,9 @@ import { AccessibilityInfo, Linking } from 'react-native';
;
```
-### A11y for multiple nested links
+### Multiple nested links
-It is an design anti-pattern to have multiple nested links in a single block of text in
-react native, since it's bad for accessibility. Consider pattern like this if more than
-one link is necessary in one paragraph.
+It is a design anti-pattern to have multiple nested links in a single block of text in React Native since it is bad for accessibility. If more than one link is necessary in one paragraph, separate them:
```jsx
function MultipleLinksA11yExample() {
@@ -166,7 +228,7 @@ function MultipleLinksA11yExample() {
### With padding
-When applying padding to a `Text` component that contains instances of `Link`, wrap in a `Box` to prevent the hitbox from being misaligned.
+When applying padding to a `Text` component that contains a `Link`, wrap in a `Box` to prevent the hitbox from being misaligned.
```jsx
diff --git a/apps/docs/docs/components/typography/Link/_webExamples.mdx b/apps/docs/docs/components/typography/Link/_webExamples.mdx
index 0f07afcad9..5bc2905dee 100644
--- a/apps/docs/docs/components/typography/Link/_webExamples.mdx
+++ b/apps/docs/docs/components/typography/Link/_webExamples.mdx
@@ -1,16 +1,74 @@
-### Default
+Link renders a pressable [Text](/components/typography/Text) element as an anchor (``) by default. It inherits parent text styles and supports the same `font` and `color` props as Text.
-Default Link text will inherit parent text styles.
+## Basics
+
+By default, Link inherits the text styles of its parent. Pass an `href` to set the destination URL.
+
+```jsx live
+
+ Check out the Coinbase homepage for more info.
+
+```
+
+## Underline
+
+Use the `underline` prop to add a text decoration underline to the link. This is particularly important for inline links within body text to meet [WCAG 2.0 accessibility requirements](https://webaim.org/standards/wcag/checklist).
+
+```jsx live
+
+ Read our{' '}
+
+ terms and conditions
+ {' '}
+ before proceeding.
+
+```
+
+### Underline with different fonts
+
+The underline works across all font styles.
+
+```jsx live
+
+
+ body link
+
+
+ label1 link
+
+
+ caption link
+
+
+ legal link
+
+
+ title2 link
+
+
+```
+
+### Underline within a paragraph
+
+When a link appears inline within body text, always use `underline` so users can distinguish the link from surrounding text without relying on color alone.
```jsx live
-Default is inherited text
+
+ This is a paragraph with an{' '}
+
+ inline underlined link
+ {' '}
+ that is clearly distinguishable from the surrounding text.
+
```
-### Styling your link
+## Styling
+
+### Font
-To style a `` component, simply wrap the `Link` node in the desired `Text` component, using the `as` prop for your usecase (for example, `p` for body text, `h1`/`h2`/`h3` for titles, etc). However, there may be times when a rendering a span is exactly what you want. In those cases, providing a font prop makes sense.
+To style a Link, either wrap it in the desired [Text](/components/typography/Text) component with the appropriate `as` prop for semantic HTML, or use the `font` prop directly on the Link.
-Wrapping your Link in the appropriate Text element
+#### Wrapping in Text
```jsx live
@@ -57,9 +115,9 @@ Wrapping your Link in the appropriate Text element
```
-### Using the font prop
+#### Using the font prop
-If you find yourself in a situation where you simply need to style a link as a Text component without wrapping it in the semantically appropriate text element, reach for the `font` prop.
+If you need to style a link without wrapping it in a semantically appropriate text element, use the `font` prop directly.
```jsx live
@@ -71,39 +129,52 @@ If you find yourself in a situation where you simply need to style a link as a T
```
-### Color override
-
-```jsx live
-
- With color override
-
-```
+### Color
-### openInNewWindow and rel props
+Override the default link color using the `color` prop with any CDS foreground token.
```jsx live
-
- Open window in existing tab
+
+ fgPrimary (default)
-
- Sets rel to noreferrer
+
+ fgNegative
-
- Sets rel to noopener
+
+ fgNegative with underline
```
-### Handling onClick
+## Navigation
+
+### openInNewWindow
+
+Set `openInNewWindow` to open the link in a new browser tab.
```jsx live
- console.log('pressed link')} rel="noopener">
- Handles onClick
+
+ Opens in a new tab
```
-### Accessibility
+### rel
+
+Use the `rel` prop to set the relationship between the current document and the linked resource.
+
+```jsx live
+
+
+ rel=noreferrer
+
+
+ rel=noopener
+
+
+```
+
+## Accessibility
:::tip Accessibility tip
@@ -114,3 +185,19 @@ If you find yourself in a situation where you simply need to style a link as a T
_The link text must have a 3:1 contrast ratio from the surrounding non-link text. The link must present a "non-color designator" (typically the introduction of the underline) on both mouse hover and keyboard focus. These two requirements help ensure that all users can differentiate links from non-link text, even if they have low vision, color deficiency, or have overridden page colors._
:::
+
+Use the `underline` prop on inline links within body text to ensure they are distinguishable without relying on color alone.
+
+```jsx live
+
+ By continuing, you agree to the{' '}
+
+ Terms of Service
+ {' '}
+ and{' '}
+
+ Privacy Policy
+
+ .
+
+```
diff --git a/apps/docs/docs/components/typography/Link/index.mdx b/apps/docs/docs/components/typography/Link/index.mdx
index 0255324b9d..8d5a635c05 100644
--- a/apps/docs/docs/components/typography/Link/index.mdx
+++ b/apps/docs/docs/components/typography/Link/index.mdx
@@ -5,22 +5,17 @@ platform_switcher_options: { web: true, mobile: true }
hide_title: true
---
+import { VStack } from '@coinbase/cds-web/layout';
import { ComponentHeader } from '@site/src/components/page/ComponentHeader';
import { LinkBanner } from '@site/src/components/page/ComponentBanner/LinkBanner';
import { ComponentTabsContainer } from '@site/src/components/page/ComponentTabsContainer';
-import MobilePropsTable from './_mobilePropsTable.mdx';
-import mobilePropsToc from ':docgen/mobile/typography/Text/toc-props';
+import webPropsToc from ':docgen/web/typography/Link/toc-props';
+import mobilePropsToc from ':docgen/mobile/typography/Link/toc-props';
import WebPropsTable from './_webPropsTable.mdx';
-import webPropsToc from ':docgen/web/typography/Text/toc-props';
-
-import MobileExamples, { toc as mobileExamplesToc } from './_mobileExamples.mdx';
+import MobilePropsTable from './_mobilePropsTable.mdx';
import WebExamples, { toc as webExamplesToc } from './_webExamples.mdx';
-
-import { sharedParentTypes } from ':docgen/_types/sharedParentTypes';
-import { sharedTypeAliases } from ':docgen/_types/sharedTypeAliases';
-import { VStack } from '@coinbase/cds-web/layout';
-
+import MobileExamples, { toc as mobileExamplesToc } from './_mobileExamples.mdx';
import webMetadata from './webMetadata.json';
import mobileMetadata from './mobileMetadata.json';
@@ -30,16 +25,15 @@ import mobileMetadata from './mobileMetadata.json';
webMetadata={webMetadata}
mobileMetadata={mobileMetadata}
banner={}
-/>
-
+ />
}
webExamples={}
- mobilePropsTable={}
- mobileExamples={}
webExamplesToc={webExamplesToc}
- mobileExamplesToc={mobileExamplesToc}
+ webPropsTable={}
webPropsToc={webPropsToc}
+ mobileExamples={}
+ mobileExamplesToc={mobileExamplesToc}
+ mobilePropsTable={}
mobilePropsToc={mobilePropsToc}
/>
diff --git a/apps/docs/docs/components/typography/Link/mobileMetadata.json b/apps/docs/docs/components/typography/Link/mobileMetadata.json
index 7c163fc42c..ae155635a9 100644
--- a/apps/docs/docs/components/typography/Link/mobileMetadata.json
+++ b/apps/docs/docs/components/typography/Link/mobileMetadata.json
@@ -9,5 +9,10 @@
"url": "/components/typography/Text/"
}
],
- "dependencies": []
+ "dependencies": [
+ {
+ "name": "react-native-inappbrowser-reborn",
+ "version": "^3.7.0"
+ }
+ ]
}
diff --git a/apps/docs/docs/components/typography/Tag/_mobileExamples.mdx b/apps/docs/docs/components/typography/Tag/_mobileExamples.mdx
index dd4328636f..ac890e4e85 100644
--- a/apps/docs/docs/components/typography/Tag/_mobileExamples.mdx
+++ b/apps/docs/docs/components/typography/Tag/_mobileExamples.mdx
@@ -32,6 +32,49 @@ You can control the visual prominence of the Tag using the `emphasis` prop. By d
```
+### Icons
+
+Use the `startIcon` and `endIcon` props to render an icon at the start or end of the tag.
+
+```jsx
+
+
+ Start icon
+
+
+ End icon
+
+
+ Both icons
+
+
+ Promotional with icons
+
+
+```
+
+### Custom Nodes
+
+Use the `start` and `end` props to render custom nodes at the start or end of the tag for full control over styling.
+
+```jsx
+
+ }>
+ Custom start node
+
+ }>
+ Custom end node
+
+ }
+ end={}
+ >
+ Both custom nodes
+
+
+```
+
### Composed Examples
#### Account Status
diff --git a/apps/docs/docs/components/typography/Tag/_webExamples.mdx b/apps/docs/docs/components/typography/Tag/_webExamples.mdx
index 97efb0b4fa..4adf685816 100644
--- a/apps/docs/docs/components/typography/Tag/_webExamples.mdx
+++ b/apps/docs/docs/components/typography/Tag/_webExamples.mdx
@@ -90,6 +90,49 @@ You can control the visual prominence of the Tag using the `emphasis` prop. By d
```
+### Icons
+
+Use the `startIcon` and `endIcon` props to render an icon at the start or end of the tag.
+
+```jsx live
+
+
+ Start icon
+
+
+ End icon
+
+
+ Both icons
+
+
+ Promotional with icons
+
+
+```
+
+### Custom Nodes
+
+Use the `start` and `end` props to render custom nodes at the start or end of the tag for full control over styling.
+
+```jsx live
+
+ }>
+ Custom start node
+
+ }>
+ Custom end node
+
+ }
+ end={}
+ >
+ Both custom nodes
+
+
+```
+
### Composed Examples
#### Account Status
diff --git a/apps/docs/docusaurus.config.ts b/apps/docs/docusaurus.config.ts
index 8ee661a4c7..63ce097a96 100644
--- a/apps/docs/docusaurus.config.ts
+++ b/apps/docs/docusaurus.config.ts
@@ -251,6 +251,17 @@ const config: Config = {
plugins: [
['@docusaurus/plugin-sitemap', { id: 'sitemap' }],
+ [
+ '@docusaurus/plugin-client-redirects',
+ {
+ redirects: [],
+ createRedirects(existingPath: string) {
+ if (existingPath.includes('/components/charts/')) {
+ return [existingPath.replace('/components/charts/', '/components/graphs/')];
+ }
+ },
+ },
+ ],
[
'@docusaurus/theme-live-codeblock',
{
diff --git a/apps/docs/jest.config.js b/apps/docs/jest.config.js
new file mode 100644
index 0000000000..7cb8e35cb9
--- /dev/null
+++ b/apps/docs/jest.config.js
@@ -0,0 +1,8 @@
+/** @type {import('jest').Config} */
+const config = {
+ preset: '../../jest.preset.js',
+ displayName: 'docs',
+ testEnvironment: 'node',
+};
+
+export default config;
diff --git a/apps/docs/package.json b/apps/docs/package.json
index b783ea8100..76d8e4f2a3 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -27,21 +27,22 @@
"@coinbase/docusaurus-plugin-docgen": "workspace:^",
"@coinbase/docusaurus-plugin-kbar": "workspace:^",
"@coinbase/docusaurus-plugin-llm-dev-server": "workspace:^",
- "@docusaurus/core": "^3.7.0",
- "@docusaurus/faster": "^3.7.0",
- "@docusaurus/mdx-loader": "^3.7.0",
- "@docusaurus/plugin-content-blog": "^3.7.0",
- "@docusaurus/plugin-content-docs": "^3.7.0",
- "@docusaurus/plugin-content-pages": "^3.7.0",
- "@docusaurus/plugin-debug": "^3.7.0",
- "@docusaurus/plugin-google-gtag": "^3.7.0",
- "@docusaurus/plugin-google-tag-manager": "^3.7.0",
- "@docusaurus/plugin-sitemap": "^3.7.0",
- "@docusaurus/preset-classic": "^3.7.0",
- "@docusaurus/theme-classic": "^3.7.0",
- "@docusaurus/theme-common": "^3.7.0",
- "@docusaurus/theme-live-codeblock": "^3.7.0",
- "@docusaurus/theme-search-algolia": "^3.7.0",
+ "@docusaurus/core": "~3.7.0",
+ "@docusaurus/faster": "~3.7.0",
+ "@docusaurus/mdx-loader": "~3.7.0",
+ "@docusaurus/plugin-client-redirects": "~3.7.0",
+ "@docusaurus/plugin-content-blog": "~3.7.0",
+ "@docusaurus/plugin-content-docs": "~3.7.0",
+ "@docusaurus/plugin-content-pages": "~3.7.0",
+ "@docusaurus/plugin-debug": "~3.7.0",
+ "@docusaurus/plugin-google-gtag": "~3.7.0",
+ "@docusaurus/plugin-google-tag-manager": "~3.7.0",
+ "@docusaurus/plugin-sitemap": "~3.7.0",
+ "@docusaurus/preset-classic": "~3.7.0",
+ "@docusaurus/theme-classic": "~3.7.0",
+ "@docusaurus/theme-common": "~3.7.0",
+ "@docusaurus/theme-live-codeblock": "~3.7.0",
+ "@docusaurus/theme-search-algolia": "~3.7.0",
"@mdx-js/react": "^3.1.0",
"@react-spring/three": "^9.7.4",
"@react-spring/web": "^9.7.4",
@@ -65,9 +66,9 @@
"@babel/preset-env": "^7.28.0",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
- "@docusaurus/module-type-aliases": "^3.7.0",
- "@docusaurus/tsconfig": "^3.7.0",
- "@docusaurus/types": "^3.7.0",
+ "@docusaurus/module-type-aliases": "~3.7.0",
+ "@docusaurus/tsconfig": "~3.7.0",
+ "@docusaurus/types": "~3.7.0",
"@linaria/babel-preset": "^3.0.0-beta.22",
"@linaria/core": "^3.0.0-beta.22",
"@linaria/webpack-loader": "^3.0.0-beta.22",
diff --git a/apps/docs/project.json b/apps/docs/project.json
index 3189b2d8d7..3cbc945cd4 100644
--- a/apps/docs/project.json
+++ b/apps/docs/project.json
@@ -33,11 +33,21 @@
"cwd": "apps/docs"
}
},
+ "serve": {
+ "command": "docusaurus serve --dir dist --port 3000",
+ "dependsOn": [
+ "build"
+ ],
+ "options": {
+ "cwd": "apps/docs"
+ }
+ },
"build": {
"executor": "nx:run-commands",
"dependsOn": [
"^build",
- "^typecheck"
+ "^typecheck",
+ "peer-dependencies-check"
],
"options": {
"commands": [
@@ -49,6 +59,12 @@
"parallel": false
}
},
+ "test": {
+ "executor": "@nx/jest:jest",
+ "options": {
+ "jestConfig": "{projectRoot}/jest.config.js"
+ }
+ },
"lint": {
"executor": "@nx/eslint:lint"
},
@@ -82,6 +98,18 @@
"options": {
"cwd": "{workspaceRoot}"
}
+ },
+ "peer-dependencies": {
+ "command": "tsx {projectRoot}/utils/generateComponentPeerDeps.ts",
+ "options": {
+ "cwd": "{workspaceRoot}"
+ }
+ },
+ "peer-dependencies-check": {
+ "command": "tsx {projectRoot}/utils/generateComponentPeerDeps.ts --fail-on-changes",
+ "options": {
+ "cwd": "{workspaceRoot}"
+ }
}
}
}
diff --git a/apps/docs/sidebars.ts b/apps/docs/sidebars.ts
index 7f6e115b77..f94696e2e8 100644
--- a/apps/docs/sidebars.ts
+++ b/apps/docs/sidebars.ts
@@ -607,81 +607,81 @@ const sidebars: SidebarsConfig = {
},
{
type: 'category',
- label: 'Graphs',
+ label: 'Charts',
items: [
{
type: 'doc',
- id: 'components/graphs/AreaChart/areaChart',
+ id: 'components/charts/AreaChart/areaChart',
label: 'AreaChart',
},
{
type: 'doc',
- id: 'components/graphs/BarChart/barChart',
+ id: 'components/charts/BarChart/barChart',
label: 'BarChart',
},
{
type: 'doc',
- id: 'components/graphs/CartesianChart/cartesianChart',
+ id: 'components/charts/CartesianChart/cartesianChart',
label: 'CartesianChart',
},
{
type: 'doc',
- id: 'components/graphs/Legend/legend',
+ id: 'components/charts/Legend/legend',
label: 'Legend',
},
{
type: 'doc',
- id: 'components/graphs/LineChart/lineChart',
+ id: 'components/charts/LineChart/lineChart',
label: 'LineChart',
},
{
type: 'doc',
- id: 'components/graphs/ReferenceLine/referenceLine',
+ id: 'components/charts/ReferenceLine/referenceLine',
label: 'ReferenceLine',
},
{
type: 'doc',
- id: 'components/graphs/PeriodSelector/periodSelector',
+ id: 'components/charts/PeriodSelector/periodSelector',
label: 'PeriodSelector',
},
{
type: 'doc',
- id: 'components/graphs/Point/point',
+ id: 'components/charts/Point/point',
label: 'Point',
},
{
type: 'doc',
- id: 'components/graphs/Scrubber/scrubber',
+ id: 'components/charts/Scrubber/scrubber',
label: 'Scrubber',
},
{
type: 'doc',
- id: 'components/graphs/Sparkline/sparkline',
+ id: 'components/charts/Sparkline/sparkline',
label: 'Sparkline (Deprecated)',
},
{
type: 'doc',
- id: 'components/graphs/SparklineGradient/sparklineGradient',
+ id: 'components/charts/SparklineGradient/sparklineGradient',
label: 'SparklineGradient (Deprecated)',
},
{
type: 'doc',
- id: 'components/graphs/SparklineInteractive/sparklineInteractive',
+ id: 'components/charts/SparklineInteractive/sparklineInteractive',
label: 'SparklineInteractive (Deprecated)',
},
{
type: 'doc',
- id: 'components/graphs/SparklineInteractiveHeader/sparklineInteractiveHeader',
+ id: 'components/charts/SparklineInteractiveHeader/sparklineInteractiveHeader',
label: 'SparklineInteractiveHeader (Deprecated)',
},
{
type: 'doc',
- id: 'components/graphs/XAxis/xAxis',
+ id: 'components/charts/XAxis/xAxis',
label: 'XAxis',
},
{
type: 'doc',
- id: 'components/graphs/YAxis/yAxis',
+ id: 'components/charts/YAxis/yAxis',
label: 'YAxis',
},
],
diff --git a/apps/docs/src/utils/__tests__/isTypeAlias.test.ts b/apps/docs/src/utils/__tests__/isTypeAlias.test.ts
new file mode 100644
index 0000000000..1c8e01c5ef
--- /dev/null
+++ b/apps/docs/src/utils/__tests__/isTypeAlias.test.ts
@@ -0,0 +1,49 @@
+const isTypeAlias = require('../isTypeAlias');
+
+describe('isTypeAlias', () => {
+ function prop(raw: string, value: unknown[]) {
+ return { type: { raw, value } };
+ }
+
+ it('returns true for uppercase raw name with 2+ values', () => {
+ expect(isTypeAlias(prop('SpacingScale', [{ value: '0' }, { value: '1' }]))).toBe(true);
+ });
+
+ it('returns true for uppercase raw name with many values', () => {
+ expect(
+ isTypeAlias(prop('IconName', [{ value: 'add' }, { value: 'remove' }, { value: 'search' }])),
+ ).toBe(true);
+ });
+
+ it('returns false when raw starts with lowercase', () => {
+ expect(isTypeAlias(prop('spacingScale', [{ value: '0' }, { value: '1' }]))).toBe(false);
+ });
+
+ it('returns false when raw contains a pipe (union type literal)', () => {
+ expect(isTypeAlias(prop('SpacingScale | number', [{ value: '0' }, { value: '1' }]))).toBe(
+ false,
+ );
+ });
+
+ it('returns false when value has fewer than 2 items', () => {
+ expect(isTypeAlias(prop('SpacingScale', [{ value: '0' }]))).toBe(false);
+ });
+
+ it('returns false when value is empty', () => {
+ expect(isTypeAlias(prop('SpacingScale', []))).toBe(false);
+ });
+
+ it('returns false when value is not an array', () => {
+ expect(isTypeAlias({ type: { raw: 'SpacingScale', value: 'string' } })).toBe(false);
+ });
+
+ it('returns false for empty raw string', () => {
+ expect(isTypeAlias(prop('', [{ value: '0' }, { value: '1' }]))).toBe(false);
+ });
+
+ it('returns false when raw is undefined', () => {
+ expect(isTypeAlias({ type: { raw: undefined, value: [{ value: 'a' }, { value: 'b' }] } })).toBe(
+ false,
+ );
+ });
+});
diff --git a/apps/docs/src/utils/__tests__/shouldAddToParentTypes.test.ts b/apps/docs/src/utils/__tests__/shouldAddToParentTypes.test.ts
new file mode 100644
index 0000000000..a74823ebb2
--- /dev/null
+++ b/apps/docs/src/utils/__tests__/shouldAddToParentTypes.test.ts
@@ -0,0 +1,88 @@
+const shouldAddToParentTypes = require('../shouldAddToParentTypes');
+
+function doc(displayName: string) {
+ return { displayName };
+}
+
+function prop(name: string, parent: string, required = false) {
+ return { name, parent, required };
+}
+
+describe('shouldAddToParentTypes', () => {
+ describe('required props are always kept in the props table', () => {
+ it('returns false for required props even with parent type match', () => {
+ expect(shouldAddToParentTypes(doc('Button'), prop('label', 'HTMLAttributes', true))).toBe(
+ false,
+ );
+ });
+ });
+
+ describe('always-included prop names', () => {
+ const alwaysIncluded = ['onChange', 'onPress', 'testID', 'type', 'value'];
+
+ it.each(alwaysIncluded)('keeps %s in props table regardless of parent', (name) => {
+ expect(shouldAddToParentTypes(doc('Input'), prop(name, 'HTMLAttributes'))).toBe(false);
+ });
+ });
+
+ describe('layout parent types', () => {
+ const layoutParents = [
+ 'BorderedStyles',
+ 'BoxBaseProps',
+ 'DimensionStyles',
+ 'FlexProps',
+ 'PositionStyles',
+ 'SpacingProps',
+ ];
+
+ it.each(layoutParents)('moves %s props to parent types for non-Box components', (parent) => {
+ expect(shouldAddToParentTypes(doc('VStack'), prop('gap', parent))).toBe(true);
+ });
+
+ it.each(layoutParents)('keeps %s props in table for Box component', (parent) => {
+ expect(shouldAddToParentTypes(doc('Box'), prop('gap', parent))).toBe(false);
+ });
+ });
+
+ describe('pressable parent types', () => {
+ const pressableParents = ['LinkableProps', 'PressableProps', 'Touchable'];
+
+ it.each(pressableParents)(
+ 'moves %s props to parent types for non-Pressable components',
+ (parent) => {
+ expect(shouldAddToParentTypes(doc('Button'), prop('onPressIn', parent))).toBe(true);
+ },
+ );
+
+ it.each(pressableParents)('keeps %s props in table for Pressable component', (parent) => {
+ expect(shouldAddToParentTypes(doc('Pressable'), prop('onPressIn', parent))).toBe(false);
+ });
+ });
+
+ describe('always-moved parent types', () => {
+ const alwaysMoved = [
+ 'AccessibilityProps',
+ 'AriaAttributes',
+ 'ComponentEventHandlerProps',
+ 'DOMAttributes',
+ 'GestureResponderHandlers',
+ 'HTMLAttributes',
+ 'TVViewProps',
+ 'ViewProps',
+ ];
+
+ it.each(alwaysMoved)('moves %s props to parent types', (parent) => {
+ expect(shouldAddToParentTypes(doc('Button'), prop('aria-label', parent))).toBe(true);
+ });
+ });
+
+ describe('custom component props stay in table', () => {
+ it('returns false for non-matching parent types', () => {
+ expect(shouldAddToParentTypes(doc('Button'), prop('variant', 'ButtonProps'))).toBe(false);
+ });
+
+ it('returns false for component-specific parents', () => {
+ expect(shouldAddToParentTypes(doc('Avatar'), prop('size', 'AvatarBaseProps'))).toBe(false);
+ });
+ });
+});
diff --git a/apps/docs/static/img/componentCardBanners/graphs_dark.svg b/apps/docs/static/img/componentCardBanners/charts_dark.svg
similarity index 100%
rename from apps/docs/static/img/componentCardBanners/graphs_dark.svg
rename to apps/docs/static/img/componentCardBanners/charts_dark.svg
diff --git a/apps/docs/static/img/componentCardBanners/graphs_dark_hover.svg b/apps/docs/static/img/componentCardBanners/charts_dark_hover.svg
similarity index 100%
rename from apps/docs/static/img/componentCardBanners/graphs_dark_hover.svg
rename to apps/docs/static/img/componentCardBanners/charts_dark_hover.svg
diff --git a/apps/docs/static/img/componentCardBanners/graphs_light.svg b/apps/docs/static/img/componentCardBanners/charts_light.svg
similarity index 100%
rename from apps/docs/static/img/componentCardBanners/graphs_light.svg
rename to apps/docs/static/img/componentCardBanners/charts_light.svg
diff --git a/apps/docs/static/img/componentCardBanners/graphs_light_hover.svg b/apps/docs/static/img/componentCardBanners/charts_light_hover.svg
similarity index 100%
rename from apps/docs/static/img/componentCardBanners/graphs_light_hover.svg
rename to apps/docs/static/img/componentCardBanners/charts_light_hover.svg
diff --git a/apps/docs/utils/__tests__/generateComponentPeerDeps.test.ts b/apps/docs/utils/__tests__/generateComponentPeerDeps.test.ts
new file mode 100644
index 0000000000..c660e7ff64
--- /dev/null
+++ b/apps/docs/utils/__tests__/generateComponentPeerDeps.test.ts
@@ -0,0 +1,93 @@
+import { loadPeerDependencyVersions, syncDependencyVersions } from '../generateComponentPeerDeps';
+
+describe('syncDependencyVersions', () => {
+ const peerDepVersions = new Map([
+ ['react-native-reanimated', '^3.14.0'],
+ ['react-native-gesture-handler', '^2.16.2'],
+ ['framer-motion', '^10.18.0'],
+ ['react-dom', '^18.3.1'],
+ ]);
+
+ it('updates stale versions to match current peerDependencies', () => {
+ const deps = [{ name: 'framer-motion', version: '^9.0.0' }];
+ const { synced, warnings } = syncDependencyVersions(deps, peerDepVersions);
+ expect(synced).toEqual([{ name: 'framer-motion', version: '^10.18.0' }]);
+ expect(warnings).toEqual([]);
+ });
+
+ it('leaves up-to-date versions unchanged', () => {
+ const deps = [{ name: 'framer-motion', version: '^10.18.0' }];
+ const { synced } = syncDependencyVersions(deps, peerDepVersions);
+ expect(synced).toEqual(deps);
+ });
+
+ it('syncs multiple dependencies at once', () => {
+ const deps = [
+ { name: 'framer-motion', version: '^9.0.0' },
+ { name: 'react-dom', version: '^17.0.0' },
+ ];
+ const { synced } = syncDependencyVersions(deps, peerDepVersions);
+ expect(synced).toEqual([
+ { name: 'framer-motion', version: '^10.18.0' },
+ { name: 'react-dom', version: '^18.3.1' },
+ ]);
+ });
+
+ it('warns and preserves deps not found in peerDependencies', () => {
+ const deps = [{ name: 'unknown-package', version: '^1.0.0' }];
+ const { synced, warnings } = syncDependencyVersions(deps, peerDepVersions);
+ expect(synced).toEqual([{ name: 'unknown-package', version: '^1.0.0' }]);
+ expect(warnings).toEqual([
+ 'unknown-package is not listed in any package.json peerDependencies',
+ ]);
+ });
+
+ it('returns empty array for empty dependencies', () => {
+ const { synced, warnings } = syncDependencyVersions([], peerDepVersions);
+ expect(synced).toEqual([]);
+ expect(warnings).toEqual([]);
+ });
+
+ it('handles mix of known and unknown deps', () => {
+ const deps = [
+ { name: 'framer-motion', version: '^9.0.0' },
+ { name: 'not-a-real-dep', version: '^1.0.0' },
+ { name: 'react-dom', version: '^18.3.1' },
+ ];
+ const { synced, warnings } = syncDependencyVersions(deps, peerDepVersions);
+ expect(synced).toEqual([
+ { name: 'framer-motion', version: '^10.18.0' },
+ { name: 'not-a-real-dep', version: '^1.0.0' },
+ { name: 'react-dom', version: '^18.3.1' },
+ ]);
+ expect(warnings).toHaveLength(1);
+ });
+
+ it('preserves extra fields on dependency objects', () => {
+ const deps = [{ name: 'framer-motion', version: '^9.0.0', url: 'https://example.com' }];
+ const { synced } = syncDependencyVersions(deps, peerDepVersions);
+ expect(synced[0]).toEqual({
+ name: 'framer-motion',
+ version: '^10.18.0',
+ url: 'https://example.com',
+ });
+ });
+});
+
+describe('loadPeerDependencyVersions', () => {
+ it('loads versions from real package.json files', () => {
+ const versions = loadPeerDependencyVersions();
+ expect(versions.size).toBeGreaterThan(0);
+ expect(versions.get('react')).toBeDefined();
+ });
+
+ it('includes mobile-specific peer deps', () => {
+ const versions = loadPeerDependencyVersions();
+ expect(versions.get('react-native')).toBeDefined();
+ });
+
+ it('includes web-specific peer deps', () => {
+ const versions = loadPeerDependencyVersions();
+ expect(versions.get('framer-motion')).toBeDefined();
+ });
+});
diff --git a/apps/docs/utils/generateComponentPeerDeps.ts b/apps/docs/utils/generateComponentPeerDeps.ts
index 9ceb55e7c8..4a009a0815 100644
--- a/apps/docs/utils/generateComponentPeerDeps.ts
+++ b/apps/docs/utils/generateComponentPeerDeps.ts
@@ -5,267 +5,185 @@ import path from 'path';
import type { Dependency } from '../src/components/page/Metadata';
-type ComponentPeerDeps = {
- [componentName: string]: {
- filePath: string;
- peerDependencies: Dependency[];
- exportPath: string;
- };
+type PackageConfig = {
+ packageName: string;
+ packageDir: string;
};
-type PackageAnalysis = {
- web: ComponentPeerDeps;
- mobile: ComponentPeerDeps;
-};
-
-const PEER_DEPS_TO_IGNORE = ['react', 'react-native'];
-
-function extractImports(fileContent: string): string[] {
- const importRegex = /import[\s\S]*?from\s+['"]([^'"]+)['"]/g;
- const imports: string[] = [];
- let match;
-
- while ((match = importRegex.exec(fileContent)) !== null) {
- imports.push(match[1]);
- }
-
- return imports;
-}
-
-function getPackageName(importPath: string): string {
- if (importPath.startsWith('@')) {
- const parts = importPath.split('/');
- return parts.length > 1 ? `${parts[0]}/${parts[1]}` : parts[0];
+const PACKAGES: PackageConfig[] = [
+ { packageName: '@coinbase/cds-web', packageDir: 'packages/web' },
+ { packageName: '@coinbase/cds-mobile', packageDir: 'packages/mobile' },
+ { packageName: '@coinbase/cds-web-visualization', packageDir: 'packages/web-visualization' },
+ {
+ packageName: '@coinbase/cds-mobile-visualization',
+ packageDir: 'packages/mobile-visualization',
+ },
+];
+
+/**
+ * Build a combined map of peer dependency versions from all known packages.
+ * Keys are dependency names, values are the version range from package.json.
+ */
+function loadPeerDependencyVersions(): Map {
+ const versions = new Map();
+ for (const pkg of PACKAGES) {
+ try {
+ const packageJson = JSON.parse(fs.readFileSync(`${pkg.packageDir}/package.json`, 'utf-8'));
+ const peerDeps: Record = packageJson.peerDependencies ?? {};
+ for (const [name, version] of Object.entries(peerDeps)) {
+ versions.set(name, version);
+ }
+ } catch {
+ // skip if package.json can't be read
+ }
}
- return importPath.split('/')[0];
+ return versions;
}
-function isExternalDependency(importPath: string): boolean {
- return !importPath.startsWith('.') && !importPath.startsWith('/');
+/**
+ * Given a metadata object with a `dependencies` array and the current peer
+ * dependency version map, return a copy with versions synced. Only updates
+ * versions for deps already listed -- never adds or removes entries.
+ */
+function syncDependencyVersions(
+ dependencies: Dependency[],
+ peerDepVersions: Map,
+): { synced: Dependency[]; warnings: string[] } {
+ const warnings: string[] = [];
+ const synced = dependencies.map((dep) => {
+ const currentVersion = peerDepVersions.get(dep.name);
+ if (!currentVersion) {
+ warnings.push(`${dep.name} is not listed in any package.json peerDependencies`);
+ return dep;
+ }
+ return { ...dep, version: currentVersion };
+ });
+ return { synced, warnings };
}
-function getExportPath(filePath: string, platform: 'web' | 'mobile'): string {
- const srcPath = `packages/${platform}/src/`;
- const relativePath = filePath.replace(srcPath, '');
- const dir = path.dirname(relativePath);
- return dir === '.' ? '' : `/${dir}`;
-}
+type MetadataFileResult = {
+ filePath: string;
+ updated: boolean;
+ warnings: string[];
+};
-async function analyzePackageForDocs(
- packagePath: string,
- platform: 'web' | 'mobile',
- packageJson: any,
-): Promise {
- const packagePeerDependencies = packageJson.peerDependencies;
- const componentFiles = await glob(`${packagePath}/src/**/*.tsx`, {
- ignore: ['**/__tests__/**', '**/__stories__/**', '**/index.ts'],
- });
+async function updateMetadataFiles(peerDepVersions: Map): Promise {
+ console.log('Syncing peer dependency versions in metadata files...');
- const results: ComponentPeerDeps = {};
+ const metadataFiles = await glob('apps/docs/docs/components/**/*Metadata.json');
+ const results: MetadataFileResult[] = [];
- for (const filePath of componentFiles) {
+ for (const metadataFile of metadataFiles) {
try {
- const fileContent = fs.readFileSync(filePath, 'utf-8');
- const imports = extractImports(fileContent);
-
- const componentName = path.basename(filePath, '.tsx');
- const peerDependencies: Dependency[] = [];
-
- for (const importPath of imports) {
- if (isExternalDependency(importPath)) {
- const packageName = getPackageName(importPath);
- if (
- Object.keys(packagePeerDependencies).includes(packageName) &&
- !PEER_DEPS_TO_IGNORE.includes(packageName)
- ) {
- peerDependencies.push({
- name: packageName,
- version: packagePeerDependencies[packageName],
- });
- }
- }
- }
+ const fileName = path.basename(metadataFile);
+ if (fileName !== 'webMetadata.json' && fileName !== 'mobileMetadata.json') continue;
- // Only include components that are actually exported
- const exportPath = getExportPath(filePath, platform);
- const hasExport =
- packageJson.exports &&
- (packageJson.exports[`.${exportPath}`] ||
- packageJson.exports[`.${exportPath}/${componentName}`]);
-
- if (hasExport || peerDependencies.length > 0) {
- results[componentName] = {
- filePath,
- peerDependencies: peerDependencies.sort((a, b) => a.name.localeCompare(b.name)),
- exportPath: exportPath || 'root',
- };
+ const raw = fs.readFileSync(metadataFile, 'utf-8');
+ const metadata = JSON.parse(raw);
+ const deps: Dependency[] = metadata.dependencies ?? [];
+ if (deps.length === 0) continue;
+
+ const { synced, warnings } = syncDependencyVersions(deps, peerDepVersions);
+ const changed = JSON.stringify(deps) !== JSON.stringify(synced);
+
+ if (changed) {
+ metadata.dependencies = synced;
+ fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 2) + '\n');
}
+
+ results.push({ filePath: metadataFile, updated: changed, warnings });
} catch (error) {
- console.error(`Error analyzing ${filePath}:`, error);
+ console.error(`Error processing ${metadataFile}:`, error);
}
}
- return results;
-}
+ const updatedCount = results.filter((r) => r.updated).length;
+ const warningResults = results.filter((r) => r.warnings.length > 0);
-function generateDocumentationTable(analysis: PackageAnalysis): string {
- let documentation = '# Component Peer Dependencies\n\n';
- documentation +=
- 'This document lists the peer dependencies required for each component when importing individually.\n\n';
-
- // Web Components
- documentation += '## Web Components (@coinbase/cds-web)\n\n';
- documentation += '| Component | Import Path | Peer Dependencies |\n';
- documentation += '|-----------|-------------|-------------------|\n';
-
- const webComponents = Object.entries(analysis.web).sort(([a], [b]) => a.localeCompare(b));
- for (const [componentName, info] of webComponents) {
- const importPath = `@coinbase/cds-web${info.exportPath === 'root' ? '' : info.exportPath}`;
- const peerDeps =
- info.peerDependencies.length > 0
- ? info.peerDependencies.map((d) => `${d.name}@${d.version}`).join(', ')
- : 'react';
- documentation += `| ${componentName} | \`${importPath}\` | ${peerDeps} |\n`;
- }
+ console.log(`\nVersion sync complete:`);
+ console.log(`- Files updated: ${updatedCount}`);
- // Mobile Components
- documentation += '\n## Mobile Components (@coinbase/cds-mobile)\n\n';
- documentation += '| Component | Import Path | Peer Dependencies |\n';
- documentation += '|-----------|-------------|-------------------|\n';
-
- const mobileComponents = Object.entries(analysis.mobile).sort(([a], [b]) => a.localeCompare(b));
- for (const [componentName, info] of mobileComponents) {
- const importPath = `@coinbase/cds-mobile${info.exportPath === 'root' ? '' : info.exportPath}`;
- const peerDeps =
- info.peerDependencies.length > 0
- ? info.peerDependencies.map((d) => `${d.name}@${d.version}`).join(', ')
- : 'react';
- documentation += `| ${componentName} | \`${importPath}\` | ${peerDeps} |\n`;
+ if (warningResults.length > 0) {
+ console.warn(`\nWarnings:`);
+ for (const r of warningResults) {
+ for (const w of r.warnings) {
+ console.warn(` ${r.filePath}: ${w}`);
+ }
+ }
}
-
- return documentation;
}
-function generateJSONOutput(analysis: PackageAnalysis): string {
- return JSON.stringify(analysis, null, 2);
-}
+async function checkMetadataFiles(peerDepVersions: Map): Promise {
+ console.log('Checking metadata files for outdated peer dependency versions...');
-async function updateMetadataFiles(analysis: PackageAnalysis): Promise {
- console.log('Updating metadata files with peer dependency information...');
-
- // Find all metadata files in the docs directory
const metadataFiles = await glob('apps/docs/docs/components/**/*Metadata.json');
-
- let updatedFiles = 0;
- let notFoundComponents = 0;
+ const outdatedFiles: string[] = [];
for (const metadataFile of metadataFiles) {
try {
- const metadata = JSON.parse(fs.readFileSync(metadataFile, 'utf-8'));
const fileName = path.basename(metadataFile);
- const isWeb = fileName === 'webMetadata.json';
- const isMobile = fileName === 'mobileMetadata.json';
+ if (fileName !== 'webMetadata.json' && fileName !== 'mobileMetadata.json') continue;
- if (!isWeb && !isMobile) continue;
+ const metadata = JSON.parse(fs.readFileSync(metadataFile, 'utf-8'));
+ const deps: Dependency[] = metadata.dependencies ?? [];
+ if (deps.length === 0) continue;
- // Extract component name from the import statement
- const importMatch = metadata.import?.match(/import\s*{\s*([^}]+)\s*}/);
- if (!importMatch) {
- console.warn(`Could not extract component name from: ${metadataFile}`);
- continue;
+ const { synced } = syncDependencyVersions(deps, peerDepVersions);
+ if (JSON.stringify(deps) !== JSON.stringify(synced)) {
+ outdatedFiles.push(metadataFile);
}
+ } catch {
+ // skip unparseable files
+ }
+ }
- const componentName = importMatch[1].trim();
- const platform = isWeb ? 'web' : 'mobile';
- const componentData = analysis[platform][componentName];
-
- if (componentData) {
- // Add peer dependencies to metadata
- metadata.dependencies = componentData.peerDependencies;
-
- // Write back to file with pretty formatting
- fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 2) + '\n');
- updatedFiles++;
- } else {
- console.warn(`Component ${componentName} not found in ${platform} analysis`);
- notFoundComponents++;
- }
- } catch (error) {
- console.error(`Error updating ${metadataFile}:`, error);
+ if (outdatedFiles.length > 0) {
+ console.error(
+ `\n${outdatedFiles.length} metadata file(s) have outdated peer dependency versions:`,
+ );
+ for (const file of outdatedFiles) {
+ console.error(` - ${file}`);
}
+ console.error('\nRun "yarn nx run docs:peer-dependencies" to update them.');
+ return false;
}
- console.log(`\nMetadata update complete:`);
- console.log(`- Files updated: ${updatedFiles}`);
- console.log(`- Components not found: ${notFoundComponents}`);
+ console.log('\nAll metadata files have up-to-date versions.');
+ return true;
}
async function main(): Promise {
- const shouldUpdateMetadata = await input({
- message: 'Should update metadata files? (y/n)',
- default: 'y',
- validate: (value: string) => ['y', 'n'].includes(value) || 'Please enter y or n',
- });
- const shouldGenerateReportFiles = await input({
- message: 'Should generate report files? (y/n)',
- default: 'y',
- validate: (value: string) => ['y', 'n'].includes(value) || 'Please enter y or n',
- });
+ const ciMode = process.argv.includes('--ci');
+ const checkMode = process.argv.includes('--fail-on-changes');
+
+ let shouldUpdate = 'y';
- console.log('Analyzing component peer dependencies for documentation...');
+ if (!ciMode && !checkMode) {
+ shouldUpdate = await input({
+ message: 'Sync peer dependency versions in metadata files? (y/n)',
+ default: 'y',
+ validate: (value: string) => ['y', 'n'].includes(value) || 'Please enter y or n',
+ });
+ }
- const webPackageJson = JSON.parse(fs.readFileSync('packages/web/package.json', 'utf-8'));
- const mobilePackageJson = JSON.parse(fs.readFileSync('packages/mobile/package.json', 'utf-8'));
+ const peerDepVersions = loadPeerDependencyVersions();
- // Analyze peer dependencies for each component in both packages
- const webAnalysis = await analyzePackageForDocs('packages/web', 'web', webPackageJson);
- const mobileAnalysis = await analyzePackageForDocs(
- 'packages/mobile',
- 'mobile',
- mobilePackageJson,
+ console.log(
+ `Loaded ${peerDepVersions.size} peer dependency versions from ${PACKAGES.length} packages.`,
);
- const analysis: PackageAnalysis = {
- web: webAnalysis,
- mobile: mobileAnalysis,
- };
-
- if (shouldGenerateReportFiles === 'y') {
- // Generate documentation
- const docsContent = generateDocumentationTable(analysis);
- fs.writeFileSync('component-peer-dependencies.md', docsContent);
- const jsonContent = generateJSONOutput(analysis);
- fs.writeFileSync('component-peer-dependencies.json', jsonContent);
+ if (checkMode) {
+ const passed = await checkMetadataFiles(peerDepVersions);
+ process.exit(passed ? 0 : 1);
}
- if (shouldUpdateMetadata === 'y') {
- // Update metadata files
- await updateMetadataFiles(analysis);
+ if (shouldUpdate === 'y') {
+ await updateMetadataFiles(peerDepVersions);
}
-
- // Print summary
- console.log('\nDocumentation generated:');
- console.log('- component-peer-dependencies.md');
- console.log('- component-peer-dependencies.json');
-
- console.log(`\nSummary:`);
- console.log(`- Web components analyzed: ${Object.keys(webAnalysis).length}`);
- console.log(`- Mobile components analyzed: ${Object.keys(mobileAnalysis).length}`);
-
- const webPeerDeps = new Set(
- Object.values(webAnalysis).flatMap((c) => c.peerDependencies.map((d) => d.name)),
- );
- const mobilePeerDeps = new Set(
- Object.values(mobileAnalysis).flatMap((c) => c.peerDependencies.map((d) => d.name)),
- );
-
- console.log(`\nUnique peer dependencies:`);
- console.log(`- Web: ${Array.from(webPeerDeps).join(', ')}`);
- console.log(`- Mobile: ${Array.from(mobilePeerDeps).join(', ')}`);
}
if (require.main === module) {
main().catch(console.error);
}
-export { analyzePackageForDocs, generateDocumentationTable };
+export { loadPeerDependencyVersions, syncDependencyVersions };
diff --git a/apps/mobile-app/scripts/utils/routes.mjs b/apps/mobile-app/scripts/utils/routes.mjs
index 20e857e39e..0d2ce796d5 100644
--- a/apps/mobile-app/scripts/utils/routes.mjs
+++ b/apps/mobile-app/scripts/utils/routes.mjs
@@ -142,9 +142,10 @@ export const routes = [
.default,
},
{
- key: 'Chart',
+ key: 'ChartTransitions',
getComponent: () =>
- require('@coinbase/cds-mobile-visualization/chart/__stories__/Chart.stories').default,
+ require('@coinbase/cds-mobile-visualization/chart/__stories__/ChartTransitions.stories')
+ .default,
},
{
key: 'Checkbox',
@@ -246,6 +247,11 @@ export const routes = [
getComponent: () =>
require('@coinbase/cds-mobile/overlays/__stories__/DrawerMisc.stories').default,
},
+ {
+ key: 'DrawerReduceMotion',
+ getComponent: () =>
+ require('@coinbase/cds-mobile/overlays/__stories__/DrawerReduceMotion.stories').default,
+ },
{
key: 'DrawerRight',
getComponent: () =>
@@ -840,6 +846,11 @@ export const routes = [
getComponent: () =>
require('@coinbase/cds-mobile/overlays/__stories__/TrayRedesign.stories').default,
},
+ {
+ key: 'TrayReduceMotion',
+ getComponent: () =>
+ require('@coinbase/cds-mobile/overlays/__stories__/TrayReduceMotion.stories').default,
+ },
{
key: 'TrayScrollable',
getComponent: () =>
diff --git a/apps/mobile-app/src/routes.ts b/apps/mobile-app/src/routes.ts
index 45dc154916..0d2ce796d5 100644
--- a/apps/mobile-app/src/routes.ts
+++ b/apps/mobile-app/src/routes.ts
@@ -142,9 +142,10 @@ export const routes = [
.default,
},
{
- key: 'Chart',
+ key: 'ChartTransitions',
getComponent: () =>
- require('@coinbase/cds-mobile-visualization/chart/__stories__/Chart.stories').default,
+ require('@coinbase/cds-mobile-visualization/chart/__stories__/ChartTransitions.stories')
+ .default,
},
{
key: 'Checkbox',
diff --git a/libs/docusaurus-plugin-docgen/package.json b/libs/docusaurus-plugin-docgen/package.json
index 40a6978902..5e988e810a 100644
--- a/libs/docusaurus-plugin-docgen/package.json
+++ b/libs/docusaurus-plugin-docgen/package.json
@@ -38,8 +38,8 @@
"type-fest": "^2.19.0"
},
"dependencies": {
- "@docusaurus/logger": "^3.7.0",
- "@docusaurus/utils": "^3.7.0",
+ "@docusaurus/logger": "~3.7.0",
+ "@docusaurus/utils": "~3.7.0",
"ejs": "^3.1.7",
"react-docgen-typescript": "^2.4.0"
},
@@ -48,7 +48,7 @@
"@babel/preset-env": "^7.28.0",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
- "@docusaurus/types": "^3.7.0",
+ "@docusaurus/types": "~3.7.0",
"@types/ejs": "^3.1.0",
"@types/lodash": "^4.14.178"
}
diff --git a/libs/docusaurus-plugin-docgen/src/scripts/docgenParser.test.ts b/libs/docusaurus-plugin-docgen/src/scripts/docgenParser.test.ts
new file mode 100644
index 0000000000..ca60844dcc
--- /dev/null
+++ b/libs/docusaurus-plugin-docgen/src/scripts/docgenParser.test.ts
@@ -0,0 +1,86 @@
+import type { Doc } from '../types';
+
+import { formatPropItemType, formatString, getDocExample } from './docgenParser';
+
+describe('formatString', () => {
+ it('removes single and double quotes', () => {
+ expect(formatString(`"hello" 'world'`)).toBe('hello world');
+ });
+
+ it('replaces newlines with spaces', () => {
+ expect(formatString('line1\nline2\nline3')).toBe('line1 line2 line3');
+ });
+
+ it('removes backticks', () => {
+ expect(formatString('`code`')).toBe('code');
+ });
+
+ it('handles all transformations together', () => {
+ expect(formatString(`"hello"\n'world' \`code\``)).toBe('hello world code');
+ });
+
+ it('returns empty string for empty input', () => {
+ expect(formatString('')).toBe('');
+ });
+
+ it('leaves normal text unchanged', () => {
+ expect(formatString('just plain text')).toBe('just plain text');
+ });
+});
+
+describe('formatPropItemType', () => {
+ it('simplifies ReactElement type', () => {
+ expect(formatPropItemType('ReactElement>')).toBe(
+ 'ReactElement',
+ );
+ });
+
+ it('simplifies ReactNode union type', () => {
+ expect(
+ formatPropItemType(
+ 'Iterable | ReactElement> | ReactPortal | false | null | number | string | true | {}',
+ ),
+ ).toBe('ReactNode');
+ });
+
+ it('simplifies Animated ViewStyle type', () => {
+ expect(
+ formatPropItemType(
+ 'false | RegisteredStyle | Value | AnimatedInterpolation | WithAnimatedObject | WithAnimatedArray<...> | null',
+ ),
+ ).toBe('Animated | ViewStyle');
+ });
+
+ it('passes unrecognized types through formatString', () => {
+ expect(formatPropItemType('string')).toBe('string');
+ expect(formatPropItemType('number | undefined')).toBe('number | undefined');
+ });
+
+ it('cleans quotes and backticks from unrecognized types', () => {
+ expect(formatPropItemType(`"primary" | "secondary"`)).toBe('primary | secondary');
+ });
+});
+
+describe('getDocExample', () => {
+ function docWithExample(example?: string): Doc {
+ return { tags: example !== undefined ? { example } : undefined } as unknown as Doc;
+ }
+
+ it('returns undefined when no tags exist', () => {
+ expect(getDocExample({ tags: undefined } as unknown as Doc)).toBeUndefined();
+ });
+
+ it('returns undefined when no example tag exists', () => {
+ expect(getDocExample(docWithExample(undefined))).toBeUndefined();
+ });
+
+ it('wraps plain code examples in tsx live fences', () => {
+ const example = '';
+ expect(getDocExample(docWithExample(example))).toBe('```tsx live\n\n```');
+ });
+
+ it('replaces tsx with tsx live in pre-fenced examples', () => {
+ const example = '```tsx\n\n```';
+ expect(getDocExample(docWithExample(example))).toBe('```tsx live\n\n```');
+ });
+});
diff --git a/libs/docusaurus-plugin-docgen/src/scripts/docgenParser.ts b/libs/docusaurus-plugin-docgen/src/scripts/docgenParser.ts
index fd5015050b..55c10d0f38 100644
--- a/libs/docusaurus-plugin-docgen/src/scripts/docgenParser.ts
+++ b/libs/docusaurus-plugin-docgen/src/scripts/docgenParser.ts
@@ -493,14 +493,14 @@ function getDocParent({ declarations = [], parent }: PropItem) {
return declaration ?? parent?.name ?? '';
}
-function getDocExample(doc: Doc) {
+export function getDocExample(doc: Doc) {
if (!doc.tags?.example) return undefined;
return doc.tags.example.includes('tsx')
? doc.tags.example.replaceAll('tsx', 'tsx live')
: '```tsx live\n' + doc.tags.example + '\n```';
}
-function formatPropItemType(value: string) {
+export function formatPropItemType(value: string) {
switch (value) {
case 'ReactElement>':
return 'ReactElement';
diff --git a/libs/docusaurus-plugin-kbar/package.json b/libs/docusaurus-plugin-kbar/package.json
index 74914ed67e..6d32eeef4a 100644
--- a/libs/docusaurus-plugin-kbar/package.json
+++ b/libs/docusaurus-plugin-kbar/package.json
@@ -29,9 +29,9 @@
],
"dependencies": {
"@coinbase/cds-common": "workspace:^",
- "@docusaurus/logger": "^3.7.0",
- "@docusaurus/plugin-content-docs": "^3.7.0",
- "@docusaurus/types": "^3.7.0",
+ "@docusaurus/logger": "~3.7.0",
+ "@docusaurus/plugin-content-docs": "~3.7.0",
+ "@docusaurus/types": "~3.7.0",
"kbar": "^0.1.0-beta.45",
"lodash": "^4.17.21",
"type-fest": "^2.19.0"
diff --git a/libs/docusaurus-plugin-llm-dev-server/package.json b/libs/docusaurus-plugin-llm-dev-server/package.json
index 9e93d62470..a5140a1d2e 100644
--- a/libs/docusaurus-plugin-llm-dev-server/package.json
+++ b/libs/docusaurus-plugin-llm-dev-server/package.json
@@ -27,7 +27,7 @@
"@babel/preset-env": "^7.28.0",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
- "@docusaurus/types": "^3.7.0",
+ "@docusaurus/types": "~3.7.0",
"@types/express": "^4.17.21"
}
}
diff --git a/package.json b/package.json
index c998c73387..c794838cd9 100644
--- a/package.json
+++ b/package.json
@@ -76,7 +76,10 @@
"expo-splash-screen": "patch:expo-splash-screen@npm:0.27.5#.yarn/patches/expo-splash-screen-npm-0.27.5-f91e0b41df.patch",
"react-native-navigation-bar-color": "patch:react-native-navigation-bar-color@npm:2.0.2#.yarn/patches/react-native-navigation-bar-color-npm-2.0.2-9a2ea3aaf6.patch",
"expo-dev-launcher": "patch:expo-dev-launcher@npm:4.0.27#.yarn/patches/expo-dev-launcher-npm-4.0.27-c2ab5dd4a5.patch",
- "react-helmet-async": "^2.0.5"
+ "react-helmet-async": "^2.0.5",
+ "ajv@^6.0.0": "^6.14.0",
+ "elliptic": "^6.6.0",
+ "ip": "^2.0.1"
},
"resolutionComments": {
"@testing-library/user-event@^14.0.4": "Create subpath export for types.",
@@ -87,7 +90,10 @@
"expo-splash-screen": "[Managed by CMR] Adds support for transitions and bottom images in the splash screen. We aim to upstream some of those improvements to Expo.",
"react-native-navigation-bar-color": "[Managed by CMR] Promisify react-native-navigation-bar-color.",
"expo-dev-launcher": "[Managed by CMR] Necessary to install the Expo network inspector for the Android `development` build type. Issue tracked internally by Expo.",
- "react-helmet-async": "Working around Nx graph resolution issue"
+ "react-helmet-async": "Working around Nx graph resolution issue",
+ "ajv": "Request from Coinbase Security team to fix security vulnerability",
+ "elliptic": "Request from Coinbase Security team to fix security vulnerability",
+ "ip": "Request from Coinbase Security team to fix security vulnerability"
},
"workspaces": [
"actions/*",
diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md
index ccade5c2fd..7c4dc49d2b 100644
--- a/packages/common/CHANGELOG.md
+++ b/packages/common/CHANGELOG.md
@@ -8,6 +8,30 @@ All notable changes to this project will be documented in this file.
+## 8.48.3 ((2/25/2026, 08:36 PM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.48.2 ((2/25/2026, 04:21 PM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.48.1 ((2/25/2026, 01:30 PM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.48.0 ((2/24/2026, 10:33 AM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.47.4 ((2/23/2026, 03:04 PM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.47.3 ((2/20/2026, 09:16 AM PST))
+
+This is an artificial version bump with no new change.
+
## 8.47.2 ((2/19/2026, 03:18 PM PST))
This is an artificial version bump with no new change.
diff --git a/packages/common/package.json b/packages/common/package.json
index 9a016cc3c6..3f6b9cbd29 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-common",
- "version": "8.47.2",
+ "version": "8.48.3",
"description": "Coinbase Design System - Common",
"repository": {
"type": "git",
diff --git a/packages/mcp-server/CHANGELOG.md b/packages/mcp-server/CHANGELOG.md
index 7295b17606..75c8b42529 100644
--- a/packages/mcp-server/CHANGELOG.md
+++ b/packages/mcp-server/CHANGELOG.md
@@ -8,6 +8,30 @@ All notable changes to this project will be documented in this file.
+## 8.48.3 ((2/25/2026, 08:36 PM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.48.2 ((2/25/2026, 04:21 PM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.48.1 ((2/25/2026, 01:30 PM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.48.0 ((2/24/2026, 10:33 AM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.47.4 ((2/23/2026, 03:04 PM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.47.3 ((2/20/2026, 09:16 AM PST))
+
+This is an artificial version bump with no new change.
+
## 8.47.2 ((2/19/2026, 03:18 PM PST))
This is an artificial version bump with no new change.
diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json
index 5ea99e2fce..e5b4bb2d64 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-mcp-server",
- "version": "8.47.2",
+ "version": "8.48.3",
"description": "Coinbase Design System - MCP Server",
"repository": {
"type": "git",
diff --git a/packages/mobile-visualization/CHANGELOG.md b/packages/mobile-visualization/CHANGELOG.md
index 72f646363a..48842d56f9 100644
--- a/packages/mobile-visualization/CHANGELOG.md
+++ b/packages/mobile-visualization/CHANGELOG.md
@@ -8,6 +8,18 @@ All notable changes to this project will be documented in this file.
+## Unreleased
+
+#### 📘 Misc
+
+- Update outdated doc links. [[#440](https://github.com/coinbase/cds/pull/440)]
+
+## 3.4.0-beta.19 (2/20/2026 PST)
+
+#### 🚀 Updates
+
+- Support custom enter transitions [[#400](https://github.com/coinbase/cds/pull/400/)]
+
## 3.4.0-beta.18 (2/6/2026 PST)
#### 🚀 Updates
diff --git a/packages/mobile-visualization/package.json b/packages/mobile-visualization/package.json
index 020326eb82..d15fe92a58 100644
--- a/packages/mobile-visualization/package.json
+++ b/packages/mobile-visualization/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-mobile-visualization",
- "version": "3.4.0-beta.18",
+ "version": "3.4.0-beta.19",
"description": "Coinbase Design System - Mobile Visualization Native",
"repository": {
"type": "git",
diff --git a/packages/mobile-visualization/src/chart/ChartProvider.tsx b/packages/mobile-visualization/src/chart/ChartProvider.tsx
index 3704494738..7491d0989a 100644
--- a/packages/mobile-visualization/src/chart/ChartProvider.tsx
+++ b/packages/mobile-visualization/src/chart/ChartProvider.tsx
@@ -10,7 +10,7 @@ export const useCartesianChartContext = (): CartesianChartContextValue => {
const context = useContext(CartesianChartContext);
if (!context) {
throw new Error(
- 'useCartesianChartContext must be used within a CartesianChart component. See http://cds.coinbase.com/components/graphs/CartesianChart.',
+ 'useCartesianChartContext must be used within a CartesianChart component. See https://cds.coinbase.com/components/charts/CartesianChart.',
);
}
return context;
diff --git a/packages/mobile-visualization/src/chart/Path.tsx b/packages/mobile-visualization/src/chart/Path.tsx
index d0d9bfddd9..30cf54a5be 100644
--- a/packages/mobile-visualization/src/chart/Path.tsx
+++ b/packages/mobile-visualization/src/chart/Path.tsx
@@ -10,8 +10,14 @@ import {
usePathInterpolation,
} from '@shopify/react-native-skia';
-import type { Transition } from './utils/transition';
-import { usePathTransition } from './utils/transition';
+import { defaultPathEnterTransition } from './utils/path';
+import {
+ buildTransition,
+ defaultTransition,
+ getTransition,
+ type Transition,
+ usePathTransition,
+} from './utils/transition';
import { useCartesianChartContext } from './ChartProvider';
import { unwrapAnimatedValue } from './utils';
@@ -70,6 +76,40 @@ export type PathProps = PathBaseProps &
| 'style'
| 'transform'
> & {
+ /**
+ * Transition configuration for enter and update animations.
+ * @note Disable an animation by passing in null.
+ *
+ * @default transitions = {{
+ * enter: { type: 'timing', duration: 500 },
+ * update: { type: 'spring', stiffness: 900, damping: 120 }
+ * }}
+ *
+ * @example
+ * // Custom enter and update transitions
+ * transitions={{ enter: { type: 'timing', duration: 300 }, update: { type: 'spring', damping: 20 } }}
+ *
+ * @example
+ * // Disable enter animation
+ * transitions={{ enter: null }}
+ */
+ transitions?: {
+ /**
+ * Transition for the initial enter/reveal animation.
+ * Set to `null` to disable.
+ */
+ enter?: Transition | null;
+ /**
+ * Transition for subsequent data update animations.
+ * Set to `null` to disable.
+ */
+ update?: Transition | null;
+ };
+ /**
+ * Transition for updates.
+ * @deprecated Use `transitions.update` instead.
+ */
+ transition?: Transition;
/**
* The SVG path data string.
*/
@@ -90,21 +130,16 @@ export type PathProps = PathBaseProps &
* Will be overridden by clipPath if set.
*/
clipRect?: Rect;
- /**
- * Animation transition
- *
- * @example
- * // Duration based
- * transition={{ type: 'timing', duration: 300 }}
- *
- * @example
- * // Spring based
- * transition={{ type: 'spring', damping: 20, stiffness: 300 }}
- */
- transition?: Transition;
};
-const AnimatedPath = memo>(
+const AnimatedPath = memo<
+ Omit<
+ PathProps,
+ 'animate' | 'clipRect' | 'clipOffset' | 'clipPath' | 'transitions' | 'transition'
+ > & {
+ transitions?: { enter?: Transition; update?: Transition };
+ }
+>(
({
d = '',
initialPath,
@@ -116,17 +151,15 @@ const AnimatedPath = memo {
const isDAnimated = typeof d !== 'string';
- // When d is animated, usePathTransition handles static path transitions.
- // For animated d values, we skip usePathTransition and use useDerivedValue directly.
const animatedPath = usePathTransition({
currentPath: isDAnimated ? '' : d,
initialPath,
- transition,
+ transitions,
});
const isFilled = fill !== undefined && fill !== 'none';
@@ -187,6 +220,7 @@ export const Path = memo((props) => {
strokeCap,
strokeJoin,
children,
+ transitions,
transition,
...pathProps
} = props;
@@ -197,6 +231,21 @@ export const Path = memo((props) => {
const isReady = !!context.getXScale();
+ const enterTransition = useMemo(
+ () => getTransition(transitions?.enter, animate, defaultPathEnterTransition),
+ [animate, transitions?.enter],
+ );
+
+ const updateTransition = useMemo(
+ () =>
+ getTransition(
+ transitions?.update !== undefined ? transitions.update : transition,
+ animate,
+ defaultTransition,
+ ),
+ [animate, transitions?.update, transition],
+ );
+
// The clip offset provides extra padding to prevent path from being cut off
// Area charts typically use offset=0 for exact clipping, while lines use offset=2 for breathing room
const totalOffset = clipOffset * 2; // Applied on both sides
@@ -206,9 +255,9 @@ export const Path = memo((props) => {
useEffect(() => {
if (animate && isReady) {
- clipProgress.value = withTiming(1, { duration: pathEnterTransitionDuration });
+ clipProgress.value = buildTransition(1, enterTransition);
}
- }, [animate, isReady, clipProgress]);
+ }, [animate, isReady, clipProgress, enterTransition]);
// Create initial and target clip paths for animation
const { initialClipPath, targetClipPath } = useMemo(() => {
@@ -308,7 +357,7 @@ export const Path = memo((props) => {
strokeJoin={strokeJoin}
strokeOpacity={strokeOpacity}
strokeWidth={strokeWidth}
- transition={transition}
+ transitions={{ enter: enterTransition, update: updateTransition }}
>
{children}
diff --git a/packages/mobile-visualization/src/chart/__stories__/CartesianChart.stories.tsx b/packages/mobile-visualization/src/chart/__stories__/CartesianChart.stories.tsx
index e05850f1da..6ffb3b5b93 100644
--- a/packages/mobile-visualization/src/chart/__stories__/CartesianChart.stories.tsx
+++ b/packages/mobile-visualization/src/chart/__stories__/CartesianChart.stories.tsx
@@ -425,83 +425,6 @@ function TradingTrends() {
);
}
-const UVGradient: GradientDefinition = {
- axis: 'y',
- stops: [
- { offset: 0, color: 'green' },
- { offset: 3, color: 'yellow' },
- { offset: 5, color: 'orange' },
- { offset: 8, color: 'red' },
- { offset: 10, color: 'purple' },
- ],
-};
-
-const PreviousData = memo(
- ({
- children,
- currentHour,
- clipOffset = 0,
- }: {
- children: React.ReactNode;
- currentHour: number;
- clipOffset?: number;
- }) => {
- // we will clip the data to the current hour
- const { drawingArea, getXScale } = useCartesianChartContext();
- const xScale = getXScale();
-
- const currentHourX = xScale?.(currentHour);
-
- const clipPath = useMemo(() => {
- if (!xScale || currentHourX === undefined) return null;
-
- // Create a rectangle from top-left of drawing area to currentHourX on the right
- // Apply clipOffset to left, top, and bottom edges only (NOT to currentHourX)
- const pathString = `M ${drawingArea.x - clipOffset} ${drawingArea.y - clipOffset} L ${currentHourX} ${drawingArea.y - clipOffset} L ${currentHourX} ${drawingArea.y + drawingArea.height + clipOffset} L ${drawingArea.x - clipOffset} ${drawingArea.y + drawingArea.height + clipOffset} Z`;
- return Skia.Path.MakeFromSVGString(pathString);
- }, [xScale, currentHourX, drawingArea, clipOffset]);
-
- if (!clipPath) return null;
-
- return (
-
- {children}
-
- );
- },
-);
-
-const FutureData = memo(
- ({
- children,
- currentHour,
- clipOffset = 0,
- }: {
- children: React.ReactNode;
- currentHour: number;
- clipOffset?: number;
- }) => {
- // we will clip the data from the current hour to the right edge
- const { drawingArea, getXScale } = useCartesianChartContext();
- const xScale = getXScale();
-
- const currentHourX = xScale?.(currentHour);
-
- const clipPath = useMemo(() => {
- if (!xScale || currentHourX === undefined) return null;
-
- // Create a rectangle from currentHourX to right edge of drawing area
- // Apply clipOffset to top, bottom, and right, but NOT left (currentHourX)
- const pathString = `M ${currentHourX} ${drawingArea.y - clipOffset} L ${drawingArea.x + drawingArea.width + clipOffset} ${drawingArea.y - clipOffset} L ${drawingArea.x + drawingArea.width + clipOffset} ${drawingArea.y + drawingArea.height + clipOffset} L ${currentHourX} ${drawingArea.y + drawingArea.height + clipOffset} Z`;
- return Skia.Path.MakeFromSVGString(pathString);
- }, [xScale, currentHourX, drawingArea, clipOffset]);
-
- if (!clipPath) return null;
-
- return {children};
- },
-);
-
const ScatterplotWithCustomLabels = memo(() => {
const theme = useTheme();
const dataPoints = useMemo(
diff --git a/packages/mobile-visualization/src/chart/__stories__/Chart.stories.tsx b/packages/mobile-visualization/src/chart/__stories__/Chart.stories.tsx
deleted file mode 100644
index 4dde0e99bd..0000000000
--- a/packages/mobile-visualization/src/chart/__stories__/Chart.stories.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Example, ExampleScreen } from '@coinbase/cds-mobile/examples/ExampleScreen';
-
-import { CartesianChart, DottedArea, Line, LineChart, SolidLine } from '../';
-
-const defaultChartHeight = 250;
-
-const BasicLineChart = () => {
- const chartData = [65, 78, 45, 88, 92, 73, 69];
-
- return (
- `$${value}`,
- showGrid: true,
- }}
- />
- );
-};
-
-const LineStyles = () => {
- const topChartData = [15, 28, 32, 44, 46, 36, 40, 45, 48, 38];
- const upperMiddleChartData = [12, 23, 21, 29, 34, 28, 31, 38, 42, 35];
- const lowerMiddleChartData = [8, 15, 14, 25, 20, 18, 22, 28, 24, 30];
- const bottomChartData = [4, 8, 11, 15, 16, 14, 16, 10, 12, 14];
-
- return (
-
-
-
- }
- curve="natural"
- seriesId="lowerMiddle"
- />
-
-
- );
-};
-
-const ChartStories = () => {
- return (
-
-
-
-
-
-
-
-
- );
-};
-
-export default ChartStories;
diff --git a/packages/mobile-visualization/src/chart/__stories__/ChartTransitions.stories.tsx b/packages/mobile-visualization/src/chart/__stories__/ChartTransitions.stories.tsx
new file mode 100644
index 0000000000..128e944046
--- /dev/null
+++ b/packages/mobile-visualization/src/chart/__stories__/ChartTransitions.stories.tsx
@@ -0,0 +1,547 @@
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { Button } from '@coinbase/cds-mobile/buttons/Button';
+import { IconButton } from '@coinbase/cds-mobile/buttons/IconButton';
+import { ExampleScreen } from '@coinbase/cds-mobile/examples/ExampleScreen';
+import { Box, HStack, VStack } from '@coinbase/cds-mobile/layout';
+import { Text } from '@coinbase/cds-mobile/typography';
+
+import { Area } from '../area/Area';
+import type { BarProps } from '../bar/Bar';
+import { BarChart } from '../bar/BarChart';
+import { CartesianChart } from '../CartesianChart';
+import { Line, type LineProps } from '../line/Line';
+import type { PathProps } from '../Path';
+import type { PointBaseProps, PointProps } from '../point';
+import { Scrubber, type ScrubberRef } from '../scrubber';
+
+const dataCount = 15;
+const updateInterval = 2500;
+const rapidUpdateInterval = 800;
+
+function generateNextValue(previousValue: number) {
+ const step = Math.random() * 30 - 15;
+ return Math.max(0, Math.min(100, previousValue + step));
+}
+
+function generateInitialData() {
+ const data = [50];
+ for (let i = 1; i < dataCount; i++) {
+ data.push(generateNextValue(data[i - 1]));
+ }
+ return data;
+}
+
+// Transition presets
+const enterOnly: PathProps['transitions'] = {
+ update: null,
+};
+const updateOnly: PathProps['transitions'] = {
+ enter: null,
+};
+const bothDisabled: PathProps['transitions'] = { enter: null, update: null };
+const customEnterUpdate: PathProps['transitions'] = {
+ enter: { type: 'timing', duration: 1500 },
+ update: { type: 'spring', stiffness: 400, damping: 30 },
+};
+const customEnterUpdateBeacon: PathProps['transitions'] = {
+ enter: { type: 'timing', duration: 500, delay: 1000 },
+ update: { type: 'spring', stiffness: 400, damping: 30 },
+};
+const slowSpringBoth: PathProps['transitions'] = {
+ enter: { type: 'spring', stiffness: 100, damping: 10 },
+ update: { type: 'spring', stiffness: 100, damping: 10 },
+};
+const staggeredBoth: BarProps['transitions'] = {
+ enter: { type: 'timing', duration: 750, staggerDelay: 250 },
+ update: { type: 'spring', stiffness: 300, damping: 20, staggerDelay: 150 },
+};
+const slowTimingBoth: PathProps['transitions'] = {
+ enter: { type: 'timing', duration: 2000 },
+ update: { type: 'timing', duration: 2000 },
+};
+
+// --- Reusable Chart Components ---
+
+const TransitionLineChart = memo<{
+ data: number[];
+ transitions: PathProps['transitions'];
+ scrubberTransitions?: PathProps['transitions'];
+ animate?: boolean;
+ idlePulse?: boolean;
+ scrubberRef?: React.RefObject;
+ enableScrubbing?: boolean;
+ points?: LineProps['points'];
+}>(
+ ({
+ data,
+ transitions,
+ scrubberTransitions,
+ animate: animateProp,
+ idlePulse,
+ scrubberRef,
+ enableScrubbing = true,
+ points,
+ }) => (
+
+
+ {enableScrubbing && (
+ }
+ hideOverlay
+ idlePulse={idlePulse}
+ transitions={scrubberTransitions ?? transitions}
+ />
+ )}
+
+ ),
+);
+
+const TransitionAreaChart = memo<{
+ data: number[];
+ transitions: PathProps['transitions'];
+ idlePulse?: boolean;
+ scrubberRef?: React.RefObject;
+}>(({ data, transitions, idlePulse, scrubberRef }) => (
+
+
+
+ }
+ hideOverlay
+ idlePulse={idlePulse}
+ transitions={transitions}
+ />
+
+));
+
+const MultiLineChart = memo<{
+ data1: number[];
+ data2: number[];
+ transitions: PathProps['transitions'];
+}>(({ data1, data2, transitions }) => (
+
+
+
+
+
+));
+
+// --- Self-contained Example Wrappers ---
+
+function LineExample({
+ transitions,
+ scrubberTransitions,
+ pointTransitions,
+ animate,
+ idlePulse,
+ resettable = true,
+ imperative = false,
+ points,
+}: {
+ transitions: PathProps['transitions'];
+ scrubberTransitions?: PathProps['transitions'];
+ pointTransitions?: PointProps['transitions'];
+ animate?: boolean;
+ idlePulse?: boolean;
+ resettable?: boolean;
+ imperative?: boolean;
+ points?: boolean;
+}) {
+ const scrubberRef = useRef(null);
+ const [data, setData] = useState(generateInitialData);
+ const [resetKey, setResetKey] = useState(0);
+ const handleReset = useCallback(() => setResetKey((k) => k + 1), []);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ if (imperative) scrubberRef.current?.pulse();
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, [imperative]);
+
+ const pointFunction: LineProps['points'] = (props: PointBaseProps) => ({
+ ...props,
+ transitions: pointTransitions,
+ });
+
+ const pointProps: LineProps['points'] = points ? pointFunction : false;
+
+ return (
+
+
+ {resettable && (
+
+
+
+ )}
+
+ );
+}
+
+function AreaExample({
+ transitions,
+ idlePulse,
+ resettable = true,
+ imperative = false,
+}: {
+ transitions: PathProps['transitions'];
+ idlePulse?: boolean;
+ resettable?: boolean;
+ imperative?: boolean;
+}) {
+ const scrubberRef = useRef(null);
+ const [data, setData] = useState(generateInitialData);
+ const [resetKey, setResetKey] = useState(0);
+ const handleReset = useCallback(() => setResetKey((k) => k + 1), []);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ if (imperative) scrubberRef.current?.pulse();
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, [imperative]);
+
+ return (
+
+
+ {resettable && (
+
+
+
+ )}
+
+ );
+}
+
+// --- Bar Chart Components ---
+
+const barCategories = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
+
+function generateBarData() {
+ return barCategories.map(() => Math.round(Math.random() * 80 + 10));
+}
+
+const barChartProps = {
+ showXAxis: true,
+ enableScrubbing: true,
+ height: 200,
+ xAxis: { data: barCategories },
+ yAxis: { domain: { min: 0, max: 100 } },
+} as const;
+
+const TransitionBarChart = memo<{
+ data: number[];
+ transitions: PathProps['transitions'];
+}>(({ data, transitions }) => (
+
+
+
+));
+
+function BarExample({
+ transitions,
+ resettable = true,
+}: {
+ transitions: PathProps['transitions'];
+ resettable?: boolean;
+}) {
+ const [data, setData] = useState(generateBarData);
+ const [resetKey, setResetKey] = useState(0);
+ const handleReset = useCallback(() => setResetKey((k) => k + 1), []);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData(generateBarData());
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+ {resettable && (
+
+
+
+ )}
+
+ );
+}
+
+function RapidLineExample({ transitions }: { transitions: PathProps['transitions'] }) {
+ const [data, setData] = useState(generateInitialData);
+ const [resetKey, setResetKey] = useState(0);
+ const handleReset = useCallback(() => setResetKey((k) => k + 1), []);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ }, rapidUpdateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+function RapidBarExample({ transitions }: { transitions: PathProps['transitions'] }) {
+ const [data, setData] = useState(generateBarData);
+ const [resetKey, setResetKey] = useState(0);
+ const handleReset = useCallback(() => setResetKey((k) => k + 1), []);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData(generateBarData());
+ }, rapidUpdateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+function MultiLineExample({ transitions }: { transitions: PathProps['transitions'] }) {
+ const [data1, setData1] = useState(generateInitialData);
+ const [data2, setData2] = useState(generateInitialData);
+ const [resetKey, setResetKey] = useState(0);
+ const handleReset = useCallback(() => setResetKey((k) => k + 1), []);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData1((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ setData2((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+// --- Main Navigator ---
+
+type ExampleItem = {
+ category: string;
+ title: string;
+ component: React.ReactNode;
+};
+
+function ExampleNavigator() {
+ const [currentIndex, setCurrentIndex] = useState(0);
+
+ const examples = useMemo(
+ () => [
+ {
+ category: 'Line',
+ title: 'Enter Only',
+ component: ,
+ },
+ {
+ category: 'Line',
+ title: 'Update Only',
+ component: (
+
+ ),
+ },
+ {
+ category: 'Line',
+ title: 'Both Disabled',
+ component: ,
+ },
+ {
+ category: 'Line',
+ title: 'Custom 2',
+ component: (
+
+ ),
+ },
+ {
+ category: 'Line',
+ title: 'Imperative Pulse',
+ component: ,
+ },
+ {
+ category: 'Multi-Line',
+ title: 'Update Only',
+ component: ,
+ },
+ {
+ category: 'Area',
+ title: 'Both Disabled',
+ component: ,
+ },
+ {
+ category: 'Area',
+ title: 'Imperative Pulse',
+ component: ,
+ },
+ {
+ category: 'Bar',
+ title: 'Enter Only',
+ component: ,
+ },
+ {
+ category: 'Bar',
+ title: 'Update Only',
+ component: ,
+ },
+ {
+ category: 'Bar',
+ title: 'Both Disabled',
+ component: ,
+ },
+ {
+ category: 'Bar',
+ title: 'Slow Spring Both',
+ component: ,
+ },
+ {
+ category: 'Bar',
+ title: 'Staggered Both',
+ component: ,
+ },
+ {
+ category: 'Line',
+ title: 'Rapid Interrupts',
+ component: ,
+ },
+ {
+ category: 'Bar',
+ title: 'Rapid Interrupts',
+ component: ,
+ },
+ ],
+ [],
+ );
+
+ const currentExample = examples[currentIndex];
+
+ const handlePrevious = useCallback(() => {
+ setCurrentIndex((prev) => (prev - 1 + examples.length) % examples.length);
+ }, [examples.length]);
+
+ const handleNext = useCallback(() => {
+ setCurrentIndex((prev) => (prev + 1) % examples.length);
+ }, [examples.length]);
+
+ return (
+
+
+
+
+
+
+ {currentExample.category}
+
+ {currentExample.title}
+
+ {currentIndex + 1} / {examples.length}
+
+
+
+
+ {currentExample.component}
+
+
+ );
+}
+
+export default ExampleNavigator;
diff --git a/packages/mobile-visualization/src/chart/area/Area.tsx b/packages/mobile-visualization/src/chart/area/Area.tsx
index a7d59ae53e..b10433e00b 100644
--- a/packages/mobile-visualization/src/chart/area/Area.tsx
+++ b/packages/mobile-visualization/src/chart/area/Area.tsx
@@ -1,7 +1,8 @@
import React, { memo, useMemo } from 'react';
import { useCartesianChartContext } from '../ChartProvider';
-import { type ChartPathCurveType, getAreaPath, type Transition } from '../utils';
+import type { PathBaseProps, PathProps } from '../Path';
+import { type ChartPathCurveType, getAreaPath } from '../utils';
import type { GradientDefinition } from '../utils/gradient';
import { DottedArea } from './DottedArea';
@@ -36,13 +37,13 @@ export type AreaBaseProps = {
* The color of the area.
* @default color of the series or 'var(--color-fgPrimary)'
*/
- fill?: string;
+ fill?: PathBaseProps['fill'];
/**
* Opacity of the area
* @note when combined with gradient, both will be applied
* @default 1
*/
- fillOpacity?: number;
+ fillOpacity?: PathBaseProps['fillOpacity'];
/**
* Baseline value for the gradient.
* When set, overrides the default baseline.
@@ -57,19 +58,14 @@ export type AreaBaseProps = {
* Whether to animate the area.
* Overrides the animate value from the chart context.
*/
- animate?: boolean;
+ animate?: PathBaseProps['animate'];
};
-export type AreaProps = AreaBaseProps & {
- /**
- * Transition configuration for path animations.
- */
- transition?: Transition;
-};
+export type AreaProps = AreaBaseProps & Pick;
export type AreaComponentProps = Pick<
AreaProps,
- 'fill' | 'fillOpacity' | 'baseline' | 'gradient' | 'animate' | 'transition'
+ 'fill' | 'fillOpacity' | 'baseline' | 'gradient' | 'animate' | 'transitions' | 'transition'
> & {
/**
* Path of the area
@@ -95,6 +91,7 @@ export const Area = memo(
baseline,
connectNulls,
gradient: gradientProp,
+ transitions,
transition,
animate,
}) => {
@@ -159,6 +156,7 @@ export const Area = memo(
fillOpacity={fillOpacity}
gradient={gradient}
transition={transition}
+ transitions={transitions}
yAxisId={matchedSeries?.yAxisId}
/>
);
diff --git a/packages/mobile-visualization/src/chart/area/DottedArea.tsx b/packages/mobile-visualization/src/chart/area/DottedArea.tsx
index 43c77d79c6..2d52eacbca 100644
--- a/packages/mobile-visualization/src/chart/area/DottedArea.tsx
+++ b/packages/mobile-visualization/src/chart/area/DottedArea.tsx
@@ -66,6 +66,7 @@ export const DottedArea = memo(
yAxisId,
gradient: gradientProp,
animate: animateProp,
+ transitions,
transition,
...pathProps
}) => {
@@ -96,7 +97,7 @@ export const DottedArea = memo(
const animatedClipPath = usePathTransition({
currentPath: d,
- transition,
+ transitions: { update: transition },
});
const staticClipPath = useMemo(() => {
@@ -119,6 +120,7 @@ export const DottedArea = memo(
d={dottedPath}
fill={fill}
transition={transition}
+ transitions={transitions}
{...pathProps}
>
{gradient && }
diff --git a/packages/mobile-visualization/src/chart/area/GradientArea.tsx b/packages/mobile-visualization/src/chart/area/GradientArea.tsx
index 1f59e0cd34..87e75d7aea 100644
--- a/packages/mobile-visualization/src/chart/area/GradientArea.tsx
+++ b/packages/mobile-visualization/src/chart/area/GradientArea.tsx
@@ -52,6 +52,7 @@ export const GradientArea = memo(
baseline,
yAxisId,
animate,
+ transitions,
transition,
...pathProps
}) => {
@@ -80,6 +81,7 @@ export const GradientArea = memo(
fill={fill}
fillOpacity={fillOpacity}
transition={transition}
+ transitions={transitions}
{...pathProps}
>
{gradient && }
diff --git a/packages/mobile-visualization/src/chart/area/SolidArea.tsx b/packages/mobile-visualization/src/chart/area/SolidArea.tsx
index 4f0d5c8a58..fdbc477559 100644
--- a/packages/mobile-visualization/src/chart/area/SolidArea.tsx
+++ b/packages/mobile-visualization/src/chart/area/SolidArea.tsx
@@ -27,7 +27,17 @@ export type SolidAreaProps = Pick<
* Otherwise, renders with solid fill.
*/
export const SolidArea = memo(
- ({ d, fill, fillOpacity = 1, yAxisId, animate, transition, gradient, ...pathProps }) => {
+ ({
+ d,
+ fill,
+ fillOpacity = 1,
+ yAxisId,
+ animate,
+ transitions,
+ transition,
+ gradient,
+ ...pathProps
+ }) => {
const theme = useTheme();
return (
@@ -37,6 +47,7 @@ export const SolidArea = memo(
fill={fill ?? theme.color.fgPrimary}
fillOpacity={fillOpacity}
transition={transition}
+ transitions={transitions}
{...pathProps}
>
{gradient && }
diff --git a/packages/mobile-visualization/src/chart/bar/Bar.tsx b/packages/mobile-visualization/src/chart/bar/Bar.tsx
index 7a156cb9dd..6b5709690a 100644
--- a/packages/mobile-visualization/src/chart/bar/Bar.tsx
+++ b/packages/mobile-visualization/src/chart/bar/Bar.tsx
@@ -1,7 +1,7 @@
import React, { memo, useMemo } from 'react';
import { useTheme } from '@coinbase/cds-mobile/hooks/useTheme';
-import { getBarPath, type Transition } from '../utils';
+import { type BarTransition, getBarPath, type Transition } from '../utils';
import { DefaultBar } from './DefaultBar';
@@ -75,7 +75,37 @@ export type BarBaseProps = {
export type BarProps = BarBaseProps & {
/**
- * Transition configuration for bar animations.
+ * Transition configuration for enter and update animations.
+ * @note Disable an animation by passing in null.
+ *
+ * @default transitions = {{
+ * enter: { type: 'spring', stiffness: 900, damping: 120, staggerDelay: 250 },
+ * update: { type: 'spring', stiffness: 900, damping: 120 }
+ * }}
+ *
+ * @example
+ * // Custom staggered enter and spring update
+ * transitions={{ enter: { type: 'timing', duration: 500, staggerDelay: 300 }, update: { type: 'spring', damping: 20 } }}
+ *
+ * @example
+ * // Disable enter animation
+ * transitions={{ enter: null }}
+ */
+ transitions?: {
+ /**
+ * Transition for the initial enter/reveal animation.
+ * Set to `null` to disable.
+ */
+ enter?: BarTransition | null;
+ /**
+ * Transition for subsequent data update animations.
+ * Set to `null` to disable.
+ */
+ update?: BarTransition | null;
+ };
+ /**
+ * Transition for updates.
+ * @deprecated Use `transitions.update` instead.
*/
transition?: Transition;
};
@@ -119,6 +149,7 @@ export const Bar = memo(
borderRadius = 4,
roundTop = true,
roundBottom = true,
+ transitions,
transition,
}) => {
const theme = useTheme();
@@ -155,6 +186,7 @@ export const Bar = memo(
stroke={stroke}
strokeWidth={strokeWidth}
transition={transition}
+ transitions={transitions}
width={width}
x={x}
y={y}
diff --git a/packages/mobile-visualization/src/chart/bar/BarChart.tsx b/packages/mobile-visualization/src/chart/bar/BarChart.tsx
index e19577fdaa..8149307de2 100644
--- a/packages/mobile-visualization/src/chart/bar/BarChart.tsx
+++ b/packages/mobile-visualization/src/chart/bar/BarChart.tsx
@@ -26,6 +26,7 @@ export type BarChartBaseProps = Omit & {
/**
@@ -90,6 +91,7 @@ export const BarChart = memo(
stackGap,
barMinSize,
stackMinSize,
+ transitions,
transition,
...chartProps
},
@@ -183,6 +185,7 @@ export const BarChart = memo(
stroke={stroke}
strokeWidth={strokeWidth}
transition={transition}
+ transitions={transitions}
/>
{children}
diff --git a/packages/mobile-visualization/src/chart/bar/BarPlot.tsx b/packages/mobile-visualization/src/chart/bar/BarPlot.tsx
index 7d24fefd9f..b265b931d0 100644
--- a/packages/mobile-visualization/src/chart/bar/BarPlot.tsx
+++ b/packages/mobile-visualization/src/chart/bar/BarPlot.tsx
@@ -29,7 +29,8 @@ export type BarPlotBaseProps = Pick<
seriesIds?: string[];
};
-export type BarPlotProps = BarPlotBaseProps & Pick;
+export type BarPlotProps = BarPlotBaseProps &
+ Pick;
/**
* BarPlot component that handles multiple series with proper stacking coordination.
@@ -51,6 +52,7 @@ export const BarPlot = memo(
stackGap,
barMinSize,
stackMinSize,
+ transitions,
transition,
}) => {
const { series: allSeries, drawingArea } = useCartesianChartContext();
@@ -135,6 +137,7 @@ export const BarPlot = memo(
strokeWidth={defaultStrokeWidth}
totalStacks={stackGroups.length}
transition={transition}
+ transitions={transitions}
yAxisId={group.yAxisId}
/>
))}
diff --git a/packages/mobile-visualization/src/chart/bar/BarStack.tsx b/packages/mobile-visualization/src/chart/bar/BarStack.tsx
index 6bc56b4a96..81c3212db5 100644
--- a/packages/mobile-visualization/src/chart/bar/BarStack.tsx
+++ b/packages/mobile-visualization/src/chart/bar/BarStack.tsx
@@ -3,11 +3,11 @@ import type { Rect } from '@coinbase/cds-common';
import { useTheme } from '@coinbase/cds-mobile/hooks/useTheme';
import { useCartesianChartContext } from '../ChartProvider';
-import type { ChartScaleFunction, Series, Transition } from '../utils';
+import type { ChartScaleFunction, Series } from '../utils';
import { evaluateGradientAtValue, getGradientStops } from '../utils/gradient';
import { convertToSerializableScale } from '../utils/scale';
-import { Bar, type BarComponent, type BarProps } from './Bar';
+import { Bar, type BarBaseProps, type BarComponent, type BarProps } from './Bar';
import { DefaultBarStack } from './DefaultBarStack';
const EPSILON = 1e-4;
@@ -23,7 +23,7 @@ export type BarSeries = Series & {
};
export type BarStackBaseProps = Pick<
- BarProps,
+ BarBaseProps,
'BarComponent' | 'fillOpacity' | 'stroke' | 'strokeWidth' | 'borderRadius'
> & {
/**
@@ -79,16 +79,11 @@ export type BarStackBaseProps = Pick<
stackMinSize?: number;
};
-export type BarStackProps = BarStackBaseProps & {
- /**
- * Transition configurations for different animation phases.
- */
- transition?: Transition;
-};
+export type BarStackProps = BarStackBaseProps & Pick;
export type BarStackComponentProps = Pick<
BarStackProps,
- 'x' | 'width' | 'categoryIndex' | 'borderRadius' | 'transition'
+ 'x' | 'width' | 'categoryIndex' | 'borderRadius' | 'transitions' | 'transition'
> & {
/**
* The y position of the stack.
@@ -140,6 +135,7 @@ export const BarStack = memo(
barMinSize,
stackMinSize,
roundBaseline,
+ transitions,
transition,
}) => {
const theme = useTheme();
@@ -692,6 +688,7 @@ export const BarStack = memo(
stroke={defaultStroke}
strokeWidth={defaultStrokeWidth}
transition={transition}
+ transitions={transitions}
width={bar.width}
x={bar.x}
y={bar.y}
@@ -711,6 +708,7 @@ export const BarStack = memo(
roundBottom={stackRoundBottom}
roundTop={stackRoundTop}
transition={transition}
+ transitions={transitions}
width={stackRect.width}
x={stackRect.x}
y={stackRect.y}
diff --git a/packages/mobile-visualization/src/chart/bar/BarStackGroup.tsx b/packages/mobile-visualization/src/chart/bar/BarStackGroup.tsx
index 2ce56d065a..74a1787182 100644
--- a/packages/mobile-visualization/src/chart/bar/BarStackGroup.tsx
+++ b/packages/mobile-visualization/src/chart/bar/BarStackGroup.tsx
@@ -17,6 +17,7 @@ export type BarStackGroupProps = Pick<
| 'barMinSize'
| 'stackMinSize'
| 'BarStackComponent'
+ | 'transitions'
| 'transition'
> &
Pick & {
@@ -88,7 +89,7 @@ export const BarStackGroup = memo(
if (xScale && !isCategoricalScale(xScale)) {
throw new Error(
- 'BarStackGroup requires a band scale for x-axis. See https://cds.coinbase.com/components/graphs/XAxis/#scale-type',
+ 'BarStackGroup requires a band scale for x-axis. See https://cds.coinbase.com/components/charts/XAxis/#scale-type',
);
}
diff --git a/packages/mobile-visualization/src/chart/bar/DefaultBar.tsx b/packages/mobile-visualization/src/chart/bar/DefaultBar.tsx
index ff9bd80149..d575a75627 100644
--- a/packages/mobile-visualization/src/chart/bar/DefaultBar.tsx
+++ b/packages/mobile-visualization/src/chart/bar/DefaultBar.tsx
@@ -3,7 +3,8 @@ import { useTheme } from '@coinbase/cds-mobile/hooks/useTheme';
import { useCartesianChartContext } from '../ChartProvider';
import { Path } from '../Path';
-import { getBarPath } from '../utils';
+import { defaultBarEnterTransition, getBarPath, withStaggerDelayTransition } from '../utils';
+import { defaultTransition, getTransition } from '../utils/transition';
import type { BarComponentProps } from './Bar';
@@ -18,7 +19,7 @@ export const DefaultBar = memo(
y,
width,
height,
- borderRadius,
+ borderRadius = 4,
roundTop,
roundBottom,
d,
@@ -27,60 +28,59 @@ export const DefaultBar = memo(
stroke,
strokeWidth,
originY,
+ transitions,
transition,
}) => {
- const { animate } = useCartesianChartContext();
+ const { animate, drawingArea } = useCartesianChartContext();
const theme = useTheme();
const defaultFill = fill || theme.color.fgPrimary;
- const targetPath = useMemo(() => {
- const effectiveBorderRadius = borderRadius ?? 0;
- const effectiveRoundTop = roundTop ?? true;
- const effectiveRoundBottom = roundBottom ?? true;
+ const normalizedX = useMemo(
+ () => (drawingArea.width > 0 ? (x - drawingArea.x) / drawingArea.width : 0),
+ [x, drawingArea.x, drawingArea.width],
+ );
- return (
- d ||
- getBarPath(
- x,
- y,
- width,
- height,
- effectiveBorderRadius,
- effectiveRoundTop,
- effectiveRoundBottom,
- )
- );
- }, [x, y, width, height, borderRadius, roundTop, roundBottom, d]);
+ const enterTransition = useMemo(
+ () =>
+ withStaggerDelayTransition(
+ getTransition(transitions?.enter, animate, defaultBarEnterTransition),
+ normalizedX,
+ ),
+ [transitions?.enter, animate, normalizedX],
+ );
+ const updateTransition = useMemo(
+ () =>
+ withStaggerDelayTransition(
+ getTransition(
+ transitions?.update !== undefined ? transitions.update : transition,
+ animate,
+ defaultTransition,
+ ),
+ normalizedX,
+ ),
+ [transitions?.update, transition, animate, normalizedX],
+ );
const initialPath = useMemo(() => {
- const effectiveBorderRadius = borderRadius ?? 0;
- const effectiveRoundTop = roundTop ?? true;
- const effectiveRoundBottom = roundBottom ?? true;
const baselineY = originY ?? y + height;
-
- return getBarPath(
- x,
- baselineY,
- width,
- 1,
- effectiveBorderRadius,
- effectiveRoundTop,
- effectiveRoundBottom,
- );
+ return getBarPath(x, baselineY, width, 1, borderRadius, !!roundTop, !!roundBottom);
}, [x, originY, y, height, width, borderRadius, roundTop, roundBottom]);
return (
);
},
diff --git a/packages/mobile-visualization/src/chart/bar/DefaultBarStack.tsx b/packages/mobile-visualization/src/chart/bar/DefaultBarStack.tsx
index 18bf3064c5..783c8bd417 100644
--- a/packages/mobile-visualization/src/chart/bar/DefaultBarStack.tsx
+++ b/packages/mobile-visualization/src/chart/bar/DefaultBarStack.tsx
@@ -3,7 +3,8 @@ import { Group } from '@shopify/react-native-skia';
import { useCartesianChartContext } from '../ChartProvider';
import { getBarPath } from '../utils';
-import { usePathTransition } from '../utils/transition';
+import { defaultBarEnterTransition, withStaggerDelayTransition } from '../utils/bar';
+import { defaultTransition, getTransition, usePathTransition } from '../utils/transition';
import type { BarStackComponentProps } from './BarStack';
@@ -23,9 +24,37 @@ export const DefaultBarStack = memo(
roundTop = true,
roundBottom = true,
yOrigin,
+ transitions,
transition,
}) => {
- const { animate } = useCartesianChartContext();
+ const { animate, drawingArea } = useCartesianChartContext();
+
+ // Compute normalized x position for stagger delay calculation
+ const normalizedX = useMemo(
+ () => (drawingArea.width > 0 ? (x - drawingArea.x) / drawingArea.width : 0),
+ [x, drawingArea.x, drawingArea.width],
+ );
+
+ const enterTransition = useMemo(
+ () =>
+ withStaggerDelayTransition(
+ getTransition(transitions?.enter, animate, defaultBarEnterTransition),
+ normalizedX,
+ ),
+ [animate, transitions?.enter, normalizedX],
+ );
+ const updateTransition = useMemo(
+ () =>
+ withStaggerDelayTransition(
+ getTransition(
+ transitions?.update !== undefined ? transitions.update : transition,
+ animate,
+ defaultTransition,
+ ),
+ normalizedX,
+ ),
+ [animate, transitions?.update, transition, normalizedX],
+ );
// Generate target clip path (full bar)
const targetPath = useMemo(() => {
@@ -41,7 +70,7 @@ export const DefaultBarStack = memo(
const animatedClipPath = usePathTransition({
currentPath: targetPath,
initialPath,
- transition,
+ transitions: { enter: enterTransition, update: updateTransition },
});
const clipPath = animate ? animatedClipPath : targetPath;
diff --git a/packages/mobile-visualization/src/chart/bar/__stories__/BarChart.stories.tsx b/packages/mobile-visualization/src/chart/bar/__stories__/BarChart.stories.tsx
index ba10f6d9d0..43be6aff35 100644
--- a/packages/mobile-visualization/src/chart/bar/__stories__/BarChart.stories.tsx
+++ b/packages/mobile-visualization/src/chart/bar/__stories__/BarChart.stories.tsx
@@ -1,12 +1,21 @@
-import { memo, useEffect, useState } from 'react';
-import { Button } from '@coinbase/cds-mobile/buttons';
-import { Example, ExampleScreen } from '@coinbase/cds-mobile/examples/ExampleScreen';
+import { memo, useCallback, useEffect, useId, useMemo, useState } from 'react';
+import { useDerivedValue } from 'react-native-reanimated';
+import { assets } from '@coinbase/cds-common/internal/data/assets';
+import { candles as btcCandles } from '@coinbase/cds-common/internal/data/candles';
+import { Button, IconButton } from '@coinbase/cds-mobile/buttons';
+import { ExampleScreen } from '@coinbase/cds-mobile/examples/ExampleScreen';
import { useTheme } from '@coinbase/cds-mobile/hooks/useTheme';
-import { VStack } from '@coinbase/cds-mobile/layout';
+import { Box, HStack, VStack } from '@coinbase/cds-mobile/layout';
+import { Text } from '@coinbase/cds-mobile/typography';
+import { Line as SkiaLine, Rect } from '@shopify/react-native-skia';
import { XAxis, YAxis } from '../../axis';
-import { CartesianChart } from '../../CartesianChart';
-import { ReferenceLine, SolidLine, type SolidLineProps } from '../../line';
+import { CartesianChart, type CartesianChartProps } from '../../CartesianChart';
+import { useCartesianChartContext } from '../../ChartProvider';
+import { type LineComponentProps, ReferenceLine, SolidLine, type SolidLineProps } from '../../line';
+import { Scrubber } from '../../scrubber';
+import { getPointOnSerializableScale, unwrapAnimatedValue, useScrubberContext } from '../../utils';
+import type { BarComponentProps } from '../Bar';
import { Bar } from '../Bar';
import { BarChart } from '../BarChart';
import { BarPlot } from '../BarPlot';
@@ -612,56 +621,477 @@ const BandGridPositionExample = ({
);
-const BarChartStories = () => {
+// --- Composed Examples ---
+
+const candlestickStockData = btcCandles.slice(0, 90).reverse();
+
+const CandlesticksHeader = memo(({ currentIndex }: { currentIndex: number | undefined }) => {
+ const formatPrice = useCallback((price: string) => {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(parseFloat(price));
+ }, []);
+
+ const formatThousandsPriceNumber = useCallback((price: number) => {
+ const formattedPrice = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(price / 1000);
+
+ return `${formattedPrice}k`;
+ }, []);
+
+ const currentText = useMemo(() => {
+ if (currentIndex !== undefined) {
+ return `Open: ${formatThousandsPriceNumber(parseFloat(candlestickStockData[currentIndex].open))}, Close: ${formatThousandsPriceNumber(
+ parseFloat(candlestickStockData[currentIndex].close),
+ )}, Volume: ${(parseFloat(candlestickStockData[currentIndex].volume) / 1000).toFixed(2)}k`;
+ }
+ return formatPrice(candlestickStockData[candlestickStockData.length - 1].close);
+ }, [currentIndex, formatThousandsPriceNumber, formatPrice]);
+
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {currentText}
+
+ );
+});
+
+const CandlesticksChart = memo(
+ ({
+ infoTextId,
+ onScrubberPositionChange,
+ }: {
+ infoTextId: string;
+ onScrubberPositionChange: (index: number | undefined) => void;
+ }) => {
+ const theme = useTheme();
+ const min = useMemo(
+ () => Math.min(...candlestickStockData.map((data) => parseFloat(data.low))),
+ [],
+ );
+
+ const CandleThinSolidLine = memo((props: SolidLineProps) => (
+
+ ));
+
+ const BandwidthHighlight = memo(({ stroke }: LineComponentProps) => {
+ const { getXSerializableScale, drawingArea } = useCartesianChartContext();
+ const { scrubberPosition } = useScrubberContext();
+ const xScale = useMemo(() => getXSerializableScale(), [getXSerializableScale]);
+
+ const rectWidth = useMemo(() => {
+ if (xScale !== undefined && xScale.type === 'band') {
+ return xScale.bandwidth;
+ }
+ return 0;
+ }, [xScale]);
+
+ const xPos = useDerivedValue(() => {
+ const position = unwrapAnimatedValue(scrubberPosition);
+ const xPos =
+ position !== undefined && xScale
+ ? getPointOnSerializableScale(position, xScale)
+ : undefined;
+ return xPos !== undefined ? xPos - rectWidth / 2 : 0;
+ }, [scrubberPosition, xScale]);
+
+ const opacity = useDerivedValue(() => (xPos.value !== undefined ? 1 : 0), [xPos]);
+
+ return (
+
+ );
+ });
+
+ const candlesData = useMemo(
+ () =>
+ candlestickStockData.map((data) => [parseFloat(data.low), parseFloat(data.high)]) as [
+ number,
+ number,
+ ][],
+ [],
+ );
+
+ const CandlestickBarComponent = memo(
+ ({ x, y, width, height, originY, dataX, ...props }) => {
+ const { getYScale } = useCartesianChartContext();
+ const yScale = getYScale();
+
+ const wickX = x + width / 2;
+
+ const timePeriodValue = candlestickStockData[dataX as number];
+
+ const open = parseFloat(timePeriodValue.open);
+ const close = parseFloat(timePeriodValue.close);
+
+ const bullish = open < close;
+ const theme = useTheme();
+ const color = bullish ? theme.color.fgPositive : theme.color.fgNegative;
+ const openY = yScale?.(open) ?? 0;
+ const closeY = yScale?.(close) ?? 0;
+
+ const bodyHeight = Math.abs(openY - closeY);
+ const bodyY = openY < closeY ? openY : closeY;
+
+ return (
+ <>
+
+
+ >
+ );
+ },
+ );
+
+ const formatThousandsPriceNumber = useCallback((price: number) => {
+ const formattedPrice = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(price / 1000);
+
+ return `${formattedPrice}k`;
+ }, []);
+
+ const formatTime = useCallback((index: number | null) => {
+ if (index === null || index === undefined || index >= candlestickStockData.length) return '';
+ const ts = parseInt(candlestickStockData[index].start);
+ return new Date(ts * 1000).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ });
+ }, []);
+
+ return (
+
+
+
+
+ <>{children}>}
+ />
+
+ );
+ },
+);
+
+const Candlesticks = () => {
+ const infoTextId = useId();
+ const [currentIndex, setCurrentIndex] = useState();
+
+ return (
+
+
+
+
);
};
-export default BarChartStories;
+const DAY_LENGTH_MINUTES = 1440;
+
+type SunlightChartData = Array<{
+ label: string;
+ value: number;
+}>;
+
+const sunlightData: SunlightChartData = [
+ { label: 'Jan', value: 598 },
+ { label: 'Feb', value: 635 },
+ { label: 'Mar', value: 688 },
+ { label: 'Apr', value: 753 },
+ { label: 'May', value: 812 },
+ { label: 'Jun', value: 855 },
+ { label: 'Jul', value: 861 },
+ { label: 'Aug', value: 828 },
+ { label: 'Sep', value: 772 },
+ { label: 'Oct', value: 710 },
+ { label: 'Nov', value: 648 },
+ { label: 'Dec', value: 605 },
+];
+
+const SunlightChartInner = memo(
+ ({
+ data,
+ height = 300,
+ ...props
+ }: Omit & { data: SunlightChartData }) => {
+ const theme = useTheme();
+
+ const SunlightThinSolidLine = memo((props: SolidLineProps) => (
+
+ ));
+
+ return (
+ value),
+ yAxisId: 'sunlight',
+ color: `rgb(${theme.spectrum.yellow40})`,
+ },
+ {
+ id: 'day',
+ data: data.map(() => DAY_LENGTH_MINUTES),
+ yAxisId: 'day',
+ color: `rgb(${theme.spectrum.blue100})`,
+ },
+ ]}
+ xAxis={{
+ ...props.xAxis,
+ scaleType: 'band',
+ data: data.map(({ label }) => label),
+ }}
+ yAxis={[
+ {
+ id: 'day',
+ domain: { min: 0, max: DAY_LENGTH_MINUTES },
+ domainLimit: 'strict',
+ },
+ {
+ id: 'sunlight',
+ domain: { min: 0, max: DAY_LENGTH_MINUTES },
+ domainLimit: 'strict',
+ },
+ ]}
+ >
+
+
+
+
+
+ );
+ },
+);
+
+const SunlightChart = () => {
+ return (
+
+
+
+ 2026 Sunlight data for the first day of each month in Atlanta, Georgia, provided by NOAA.
+
+
+ );
+};
+
+const PriceRange = () => {
+ const candles = btcCandles.slice(0, 180).reverse();
+ const data: [number, number][] = useMemo(
+ () => candles.map((candle) => [parseFloat(candle.low), parseFloat(candle.high)]),
+ [candles],
+ );
+
+ const min = useMemo(() => Math.min(...data.map(([low]) => low)), [data]);
+ const max = useMemo(() => Math.max(...data.map(([, high]) => high)), [data]);
+
+ const tickFormatter = useCallback(
+ (value: number) =>
+ new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ notation: 'compact',
+ maximumFractionDigits: 0,
+ }).format(value),
+ [],
+ );
+
+ return (
+
+ );
+};
+
+type ExampleItem = {
+ title: string;
+ component: React.ReactNode;
+};
+
+function ExampleNavigator() {
+ const [currentIndex, setCurrentIndex] = useState(0);
+
+ const examples = useMemo(
+ () => [
+ {
+ title: 'Basic',
+ component: ,
+ },
+ {
+ title: 'Animated Auto-Updating',
+ component: ,
+ },
+ {
+ title: 'Negative Values with Top Axis',
+ component: ,
+ },
+ {
+ title: 'Positive and Negative Cash Flow',
+ component: ,
+ },
+ {
+ title: 'Fiat & Stablecoin Balance',
+ component: ,
+ },
+ {
+ title: 'Monthly Rewards',
+ component: ,
+ },
+ {
+ title: 'Multiple Y Axes',
+ component: ,
+ },
+ {
+ title: 'Y-Axis Continuous ColorMap',
+ component: ,
+ },
+ {
+ title: 'Y-Axis Discrete ColorMap',
+ component: ,
+ },
+ {
+ title: 'X-Axis Continuous ColorMap',
+ component: ,
+ },
+ {
+ title: 'X-Axis Discrete ColorMap',
+ component: ,
+ },
+ {
+ title: 'X-Axis Multi-Segment ColorMap',
+ component: ,
+ },
+ {
+ title: 'ColorMap with Opacity',
+ component: ,
+ },
+ {
+ title: 'Band Grid Position',
+ component: (
+
+
+
+
+
+
+ ),
+ },
+ {
+ title: 'Candlesticks',
+ component: ,
+ },
+ {
+ title: 'Monthly Sunlight',
+ component: ,
+ },
+ {
+ title: 'Price Range',
+ component: ,
+ },
+ ],
+ [],
+ );
+
+ const currentExample = examples[currentIndex];
+
+ const handlePrevious = useCallback(() => {
+ setCurrentIndex((prev) => (prev - 1 + examples.length) % examples.length);
+ }, [examples.length]);
+
+ const handleNext = useCallback(() => {
+ setCurrentIndex((prev) => (prev + 1 + examples.length) % examples.length);
+ }, [examples.length]);
+
+ return (
+
+
+
+
+
+ {currentExample.title}
+
+ {currentIndex + 1} / {examples.length}
+
+
+
+
+ {currentExample.component}
+
+
+ );
+}
+
+export default ExampleNavigator;
diff --git a/packages/mobile-visualization/src/chart/line/DottedLine.tsx b/packages/mobile-visualization/src/chart/line/DottedLine.tsx
index 3045e9472c..fe61472fff 100644
--- a/packages/mobile-visualization/src/chart/line/DottedLine.tsx
+++ b/packages/mobile-visualization/src/chart/line/DottedLine.tsx
@@ -37,6 +37,7 @@ export const DottedLine = memo(
yAxisId,
d,
animate,
+ transitions,
transition,
...props
}) => {
@@ -54,6 +55,7 @@ export const DottedLine = memo(
strokeOpacity={strokeOpacity}
strokeWidth={strokeWidth}
transition={transition}
+ transitions={transitions}
{...props}
>
diff --git a/packages/mobile-visualization/src/chart/line/Line.tsx b/packages/mobile-visualization/src/chart/line/Line.tsx
index 547461ef77..9eb9aef30e 100644
--- a/packages/mobile-visualization/src/chart/line/Line.tsx
+++ b/packages/mobile-visualization/src/chart/line/Line.tsx
@@ -1,20 +1,16 @@
-import React, { memo, useEffect, useMemo } from 'react';
-import { useSharedValue, withDelay, withTiming } from 'react-native-reanimated';
+import React, { memo, useMemo } from 'react';
import { useTheme } from '@coinbase/cds-mobile';
import { type AnimatedProp, Group } from '@shopify/react-native-skia';
import { Area, type AreaComponent } from '../area/Area';
import { useCartesianChartContext } from '../ChartProvider';
-import { type PathProps } from '../Path';
+import type { PathProps } from '../Path';
import { Point, type PointBaseProps, type PointProps } from '../point';
import {
- accessoryFadeTransitionDelay,
- accessoryFadeTransitionDuration,
type ChartPathCurveType,
getLineData,
getLinePath,
type GradientDefinition,
- type Transition,
} from '../utils';
import { evaluateGradientAtValue, getGradientStops } from '../utils/gradient';
import { convertToSerializableScale } from '../utils/scale';
@@ -110,16 +106,11 @@ export type LineBaseProps = {
animate?: boolean;
};
-export type LineProps = LineBaseProps & {
- /**
- * Transition configuration for line animations.
- */
- transition?: Transition;
-};
+export type LineProps = LineBaseProps & Pick;
export type LineComponentProps = Pick<
LineProps,
- 'stroke' | 'strokeOpacity' | 'strokeWidth' | 'gradient' | 'animate' | 'transition'
+ 'stroke' | 'strokeOpacity' | 'strokeWidth' | 'gradient' | 'animate' | 'transitions' | 'transition'
> &
Pick & {
/**
@@ -150,6 +141,7 @@ export const Line = memo(
opacity = 1,
points,
connectNulls,
+ transitions,
transition,
gradient: gradientProp,
...props
@@ -158,21 +150,6 @@ export const Line = memo(
const { animate, getSeries, getSeriesData, getXScale, getYScale, getXAxis } =
useCartesianChartContext();
- const isReady = !!getXScale();
-
- // Animation state for delayed point rendering (matches web timing)
- const pointsOpacity = useSharedValue(animate ? 0 : 1);
-
- // Delay point appearance until after path enter animation completes
- useEffect(() => {
- if (animate && isReady) {
- pointsOpacity.value = withDelay(
- accessoryFadeTransitionDelay,
- withTiming(1, { duration: accessoryFadeTransitionDuration }),
- );
- }
- }, [animate, isReady, pointsOpacity]);
-
const matchedSeries = useMemo(() => getSeries(seriesId), [getSeries, seriesId]);
const gradient = useMemo(
() => gradientProp ?? matchedSeries?.gradient,
@@ -263,6 +240,7 @@ export const Line = memo(
gradient={gradient}
seriesId={seriesId}
transition={transition}
+ transitions={transitions}
type={areaType}
/>
)}
@@ -273,11 +251,12 @@ export const Line = memo(
stroke={stroke}
strokeOpacity={strokeOpacity ?? opacity}
transition={transition}
+ transitions={transitions}
yAxisId={matchedSeries?.yAxisId}
{...props}
/>
{points && (
-
+
{chartData.map((value: number | null, index: number) => {
if (value === null) return;
diff --git a/packages/mobile-visualization/src/chart/line/SolidLine.tsx b/packages/mobile-visualization/src/chart/line/SolidLine.tsx
index ddd8938333..75ae5214f5 100644
--- a/packages/mobile-visualization/src/chart/line/SolidLine.tsx
+++ b/packages/mobile-visualization/src/chart/line/SolidLine.tsx
@@ -30,6 +30,7 @@ export const SolidLine = memo(
yAxisId,
d,
animate,
+ transitions,
transition,
...props
}) => {
@@ -47,6 +48,7 @@ export const SolidLine = memo(
strokeOpacity={strokeOpacity}
strokeWidth={strokeWidth}
transition={transition}
+ transitions={transitions}
{...props}
>
{gradient && }
diff --git a/packages/mobile-visualization/src/chart/line/__stories__/LineChart.stories.tsx b/packages/mobile-visualization/src/chart/line/__stories__/LineChart.stories.tsx
index 5e2a24bb30..3926086eb3 100644
--- a/packages/mobile-visualization/src/chart/line/__stories__/LineChart.stories.tsx
+++ b/packages/mobile-visualization/src/chart/line/__stories__/LineChart.stories.tsx
@@ -1,4 +1,4 @@
-import { forwardRef, memo, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
+import { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { View } from 'react-native';
import {
useAnimatedReaction,
@@ -9,7 +9,6 @@ import {
} from 'react-native-reanimated';
import type { ThemeVars } from '@coinbase/cds-common/core/theme';
import { assets, ethBackground } from '@coinbase/cds-common/internal/data/assets';
-import { candles as btcCandles } from '@coinbase/cds-common/internal/data/candles';
import { prices } from '@coinbase/cds-common/internal/data/prices';
import { sparklineInteractiveData } from '@coinbase/cds-common/internal/visualizations/SparklineInteractiveData';
import { useTabsContext } from '@coinbase/cds-common/tabs/TabsContext';
@@ -32,8 +31,6 @@ import {
Circle,
FontWeight,
Group,
- Line as SkiaLine,
- Rect,
Skia,
type SkTextStyle,
TextAlign,
@@ -41,7 +38,6 @@ import {
import { Area, DottedArea, type DottedAreaProps } from '../../area';
import { DefaultAxisTickLabel, XAxis, YAxis } from '../../axis';
-import { type BarComponentProps, BarPlot } from '../../bar';
import { CartesianChart } from '../../CartesianChart';
import { useCartesianChartContext } from '../../ChartProvider';
import { PeriodSelector, PeriodSelectorActiveIndicator } from '../../PeriodSelector';
@@ -57,7 +53,6 @@ import {
buildTransition,
defaultTransition,
getLineData,
- getPointOnSerializableScale,
projectPointWithSerializableScale,
type Transition,
unwrapAnimatedValue,
@@ -68,7 +63,6 @@ import {
type DottedLineProps,
Line,
LineChart,
- type LineComponentProps,
ReferenceLine,
SolidLine,
type SolidLineProps,
@@ -1327,215 +1321,6 @@ function Performance() {
);
}
-const candlestickStockData = btcCandles.slice(0, 90).reverse();
-
-const CandlesticksHeader = memo(({ currentIndex }: { currentIndex: number | undefined }) => {
- const formatPrice = useCallback((price: string) => {
- return new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- }).format(parseFloat(price));
- }, []);
-
- const formatThousandsPriceNumber = useCallback((price: number) => {
- const formattedPrice = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- minimumFractionDigits: 0,
- maximumFractionDigits: 0,
- }).format(price / 1000);
-
- return `${formattedPrice}k`;
- }, []);
-
- const currentText = useMemo(() => {
- if (currentIndex !== undefined) {
- return `Open: ${formatThousandsPriceNumber(parseFloat(candlestickStockData[currentIndex].open))}, Close: ${formatThousandsPriceNumber(
- parseFloat(candlestickStockData[currentIndex].close),
- )}, Volume: ${(parseFloat(candlestickStockData[currentIndex].volume) / 1000).toFixed(2)}k`;
- }
- return formatPrice(candlestickStockData[candlestickStockData.length - 1].close);
- }, [currentIndex, formatThousandsPriceNumber, formatPrice]);
-
- return (
-
- {currentText}
-
- );
-});
-
-const CandlesticksChart = memo(
- ({
- infoTextId,
- onScrubberPositionChange,
- }: {
- infoTextId: string;
- onScrubberPositionChange: (index: number | undefined) => void;
- }) => {
- const theme = useTheme();
- const min = useMemo(
- () => Math.min(...candlestickStockData.map((data) => parseFloat(data.low))),
- [],
- );
-
- const ThinSolidLine = memo((props: SolidLineProps) => );
-
- // Custom line component that renders a rect to highlight the entire bandwidth
- const BandwidthHighlight = memo(({ stroke }: LineComponentProps) => {
- const { getXSerializableScale, drawingArea } = useCartesianChartContext();
- const { scrubberPosition } = useScrubberContext();
- const xScale = useMemo(() => getXSerializableScale(), [getXSerializableScale]);
-
- const rectWidth = useMemo(() => {
- if (xScale !== undefined && xScale.type === 'band') {
- return xScale.bandwidth;
- }
- return 0;
- }, [xScale]);
-
- const xPos = useDerivedValue(() => {
- const position = unwrapAnimatedValue(scrubberPosition);
- const xPos =
- position !== undefined && xScale
- ? getPointOnSerializableScale(position, xScale)
- : undefined;
- return xPos !== undefined ? xPos - rectWidth / 2 : 0;
- }, [scrubberPosition, xScale]);
-
- const opacity = useDerivedValue(() => (xPos.value !== undefined ? 1 : 0), [xPos]);
-
- return (
-
- );
- });
-
- const candlesData = useMemo(
- () =>
- candlestickStockData.map((data) => [parseFloat(data.low), parseFloat(data.high)]) as [
- number,
- number,
- ][],
- [],
- );
-
- const CandlestickBarComponent = memo(
- ({ x, y, width, height, originY, dataX, ...props }) => {
- const { getYScale } = useCartesianChartContext();
- const yScale = getYScale();
-
- const wickX = x + width / 2;
-
- const timePeriodValue = candlestickStockData[dataX as number];
-
- const open = parseFloat(timePeriodValue.open);
- const close = parseFloat(timePeriodValue.close);
-
- const bullish = open < close;
- const theme = useTheme();
- const color = bullish ? theme.color.fgPositive : theme.color.fgNegative;
- const openY = yScale?.(open) ?? 0;
- const closeY = yScale?.(close) ?? 0;
-
- const bodyHeight = Math.abs(openY - closeY);
- const bodyY = openY < closeY ? openY : closeY;
-
- return (
- <>
-
-
- >
- );
- },
- );
-
- const formatThousandsPriceNumber = useCallback((price: number) => {
- const formattedPrice = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- minimumFractionDigits: 0,
- maximumFractionDigits: 0,
- }).format(price / 1000);
-
- return `${formattedPrice}k`;
- }, []);
-
- const formatTime = useCallback((index: number | null) => {
- if (index === null || index === undefined || index >= candlestickStockData.length) return '';
- const ts = parseInt(candlestickStockData[index].start);
- return new Date(ts * 1000).toLocaleDateString('en-US', {
- month: 'short',
- day: 'numeric',
- });
- }, []);
-
- return (
-
-
-
-
- {children}}
- />
-
- );
- },
-);
-
-function Candlesticks() {
- const infoTextId = useId();
- const [currentIndex, setCurrentIndex] = useState();
-
- return (
-
-
-
-
- );
-}
-
function MonotoneAssetPrice() {
const theme = useTheme();
const prices = sparklineInteractiveData.hour;
@@ -2288,10 +2073,6 @@ function ExampleNavigator() {
title: 'Performance',
component: ,
},
- {
- title: 'Candlesticks',
- component: ,
- },
{
title: 'Monotone Asset Price',
component: ,
diff --git a/packages/mobile-visualization/src/chart/point/Point.tsx b/packages/mobile-visualization/src/chart/point/Point.tsx
index 39a302cc63..0d10a96d76 100644
--- a/packages/mobile-visualization/src/chart/point/Point.tsx
+++ b/packages/mobile-visualization/src/chart/point/Point.tsx
@@ -7,7 +7,13 @@ import { Circle, type Color, Group, interpolateColors } from '@shopify/react-nat
import { useCartesianChartContext } from '../ChartProvider';
import type { ChartTextChildren, ChartTextProps } from '../text/ChartText';
import { type PointLabelPosition, projectPoint } from '../utils';
-import { buildTransition, defaultTransition, type Transition } from '../utils/transition';
+import {
+ buildTransition,
+ defaultAccessoryEnterTransition,
+ defaultTransition,
+ getTransition,
+ type Transition,
+} from '../utils/transition';
import { DefaultPointLabel } from './DefaultPointLabel';
@@ -123,8 +129,24 @@ export type PointProps = PointBaseProps & {
*/
label?: ChartTextChildren;
/**
- * Transition configuration for point animations.
- * Defines how the point transitions when position or color changes.
+ * Transition configuration for enter and update animations.
+ * @note Disable an animation by passing in null.
+ */
+ transitions?: {
+ /**
+ * Transition for the initial enter/reveal animation.
+ * Set to `null` to disable.
+ */
+ enter?: Transition | null;
+ /**
+ * Transition for subsequent data update animations.
+ * Set to `null` to disable.
+ */
+ update?: Transition | null;
+ };
+ /**
+ * Transition for updates.
+ * @deprecated Use `transitions.update` instead.
*/
transition?: Transition;
};
@@ -144,7 +166,8 @@ export const Point = memo(
labelPosition = 'center',
labelOffset,
labelFont,
- transition = defaultTransition,
+ transitions,
+ transition,
animate: animateProp,
}) => {
const theme = useTheme();
@@ -164,6 +187,21 @@ export const Point = memo(
const shouldAnimate = animate ?? false;
+ const updateTransition = useMemo(
+ () =>
+ getTransition(
+ transitions?.update !== undefined ? transitions.update : transition,
+ animate,
+ defaultTransition,
+ ),
+ [animate, transitions?.update, transition],
+ );
+
+ const enterTransition = useMemo(
+ () => getTransition(transitions?.enter, animate, defaultAccessoryEnterTransition),
+ [animate, transitions?.enter],
+ );
+
// Calculate pixel coordinates from data coordinates
const pixelCoordinate = useMemo(() => {
if (!xScale || !yScale) {
@@ -185,9 +223,17 @@ export const Point = memo(
const animatedX = useSharedValue(0);
const animatedY = useSharedValue(0);
- // Animated value for color interpolation (0 = old color, 1 = new color)
+ const enterOpacity = useSharedValue(shouldAnimate ? 0 : 1);
+
const colorProgress = useSharedValue(1);
+ const isReady = !!xScale && !!yScale;
+
+ useEffect(() => {
+ if (!shouldAnimate || !isReady) return;
+ enterOpacity.value = buildTransition(1, enterTransition);
+ }, [shouldAnimate, isReady, enterTransition, enterOpacity]);
+
// Update position when coordinates change
useEffect(() => {
if (!pixelCoordinate) {
@@ -195,26 +241,33 @@ export const Point = memo(
}
if (shouldAnimate && previousPixelCoordinate) {
- animatedX.value = buildTransition(pixelCoordinate.x, transition);
- animatedY.value = buildTransition(pixelCoordinate.y, transition);
+ animatedX.value = buildTransition(pixelCoordinate.x, updateTransition);
+ animatedY.value = buildTransition(pixelCoordinate.y, updateTransition);
} else {
cancelAnimation(animatedX);
cancelAnimation(animatedY);
animatedX.value = pixelCoordinate.x;
animatedY.value = pixelCoordinate.y;
}
- }, [pixelCoordinate, shouldAnimate, previousPixelCoordinate, animatedX, animatedY, transition]);
+ }, [
+ pixelCoordinate,
+ shouldAnimate,
+ previousPixelCoordinate,
+ animatedX,
+ animatedY,
+ updateTransition,
+ ]);
// Update color when fill changes
useEffect(() => {
if (shouldAnimate && previousFill && previousFill !== fill) {
colorProgress.value = 0;
- colorProgress.value = buildTransition(1, transition);
+ colorProgress.value = buildTransition(1, updateTransition);
} else {
cancelAnimation(colorProgress);
colorProgress.value = 1;
}
- }, [fill, shouldAnimate, previousFill, colorProgress, transition]);
+ }, [fill, shouldAnimate, previousFill, colorProgress, updateTransition]);
// Create animated point for circles
const animatedPoint = useDerivedValue(() => {
@@ -229,21 +282,20 @@ export const Point = memo(
return interpolateColors(colorProgress.value, [0, 1], [previousFill, fill]);
}, [colorProgress, previousFill, fill]);
- // Check if point is within drawing area
- const isWithinDrawingArea = useDerivedValue(() => {
+ const isWithinDrawingArea = useMemo(() => {
+ if (!pixelCoordinate) return false;
return (
- animatedX.value >= drawingArea.x &&
- animatedX.value <= drawingArea.x + drawingArea.width &&
- animatedY.value >= drawingArea.y &&
- animatedY.value <= drawingArea.y + drawingArea.height
+ pixelCoordinate.x >= drawingArea.x &&
+ pixelCoordinate.x <= drawingArea.x + drawingArea.width &&
+ pixelCoordinate.y >= drawingArea.y &&
+ pixelCoordinate.y <= drawingArea.y + drawingArea.height
);
- }, [animatedX, animatedY, drawingArea]);
+ }, [pixelCoordinate, drawingArea]);
- // Compute effective opacity based on drawing area bounds
const effectiveOpacity = useDerivedValue(() => {
const baseOpacity = opacity ?? 1;
- return isWithinDrawingArea.value ? baseOpacity : 0;
- }, [isWithinDrawingArea, opacity]);
+ return isWithinDrawingArea ? baseOpacity * enterOpacity.value : 0;
+ }, [isWithinDrawingArea, opacity, enterOpacity]);
const offset = useMemo(() => labelOffset ?? radius * 2, [labelOffset, radius]);
@@ -251,8 +303,7 @@ export const Point = memo(
return null;
}
- // If animation is disabled or on first render, use static rendering
- if (!shouldAnimate || !previousPixelCoordinate) {
+ if (!shouldAnimate) {
const isWithinBounds =
pixelCoordinate.x >= drawingArea.x &&
pixelCoordinate.x <= drawingArea.x + drawingArea.width &&
@@ -296,17 +347,12 @@ export const Point = memo(
);
}
- // Animated rendering
return (
- <>
-
- {/* Outer stroke circle */}
- {strokeWidth > 0 && (
-
- )}
- {/* Inner fill circle with animated color */}
-
-
+
+ {strokeWidth > 0 && (
+
+ )}
+
{label && (
(
{label}
)}
- >
+
);
},
);
diff --git a/packages/mobile-visualization/src/chart/scrubber/DefaultScrubberBeacon.tsx b/packages/mobile-visualization/src/chart/scrubber/DefaultScrubberBeacon.tsx
index d4fa5bd422..96cb70314a 100644
--- a/packages/mobile-visualization/src/chart/scrubber/DefaultScrubberBeacon.tsx
+++ b/packages/mobile-visualization/src/chart/scrubber/DefaultScrubberBeacon.tsx
@@ -16,7 +16,13 @@ import { Circle, Group } from '@shopify/react-native-skia';
import { useCartesianChartContext } from '../ChartProvider';
import { unwrapAnimatedValue } from '../utils';
import { projectPointWithSerializableScale } from '../utils/point';
-import { buildTransition, defaultTransition, type Transition } from '../utils/transition';
+import {
+ buildTransition,
+ defaultTransition,
+ getTransition,
+ instantTransition,
+ type Transition,
+} from '../utils/transition';
import type { ScrubberBeaconProps, ScrubberBeaconRef } from './Scrubber';
@@ -85,8 +91,8 @@ export const DefaultScrubberBeacon = memo(
);
const updateTransition = useMemo(
- () => transitions?.update ?? defaultTransition,
- [transitions?.update],
+ () => getTransition(transitions?.update, animate, defaultTransition),
+ [transitions?.update, animate],
);
const pulseTransition = useMemo(
() => transitions?.pulse ?? defaultPulseTransition,
diff --git a/packages/mobile-visualization/src/chart/scrubber/Scrubber.tsx b/packages/mobile-visualization/src/chart/scrubber/Scrubber.tsx
index c256f0460f..d4c377671c 100644
--- a/packages/mobile-visualization/src/chart/scrubber/Scrubber.tsx
+++ b/packages/mobile-visualization/src/chart/scrubber/Scrubber.tsx
@@ -11,8 +11,6 @@ import {
useAnimatedReaction,
useDerivedValue,
useSharedValue,
- withDelay,
- withTiming,
} from 'react-native-reanimated';
import { useTheme } from '@coinbase/cds-mobile';
import { type AnimatedProp, Group, Rect, type SkParagraph } from '@shopify/react-native-skia';
@@ -25,14 +23,15 @@ import {
} from '../line';
import type { ChartTextChildren, ChartTextProps } from '../text';
import {
- accessoryFadeTransitionDelay,
- accessoryFadeTransitionDuration,
type ChartInset,
+ defaultAccessoryEnterTransition,
getPointOnSerializableScale,
+ getTransition,
type Series,
useScrubberContext,
} from '../utils';
import type { Transition } from '../utils/transition';
+import { buildTransition } from '../utils/transition';
import { DefaultScrubberBeacon } from './DefaultScrubberBeacon';
import { DefaultScrubberLabel } from './DefaultScrubberLabel';
@@ -56,7 +55,7 @@ export type ScrubberBeaconRef = {
pulse: () => void;
};
-export type ScrubberBeaconProps = {
+export type ScrubberBeaconBaseProps = {
/**
* Id of the series.
*/
@@ -89,18 +88,36 @@ export type ScrubberBeaconProps = {
* @default to ChartContext's animate value
*/
animate?: boolean;
+ /**
+ * Opacity of the beacon.
+ * @default 1
+ */
+ opacity?: AnimatedProp;
+ /**
+ * Stroke color of the beacon circle.
+ * @default theme.color.bg
+ */
+ stroke?: string;
+};
+
+export type ScrubberBeaconProps = ScrubberBeaconBaseProps & {
/**
* Transition configuration for beacon animations.
*/
transitions?: {
/**
- * Transition used for beacon position updates.
- * @default defaultTransition
+ * Transition for the initial enter/reveal animation.
+ * Set to `null` to disable.
+ */
+ enter?: Transition | null;
+ /**
+ * Transition for subsequent data update animations.
+ * Set to `null` to disable.
*/
- update?: Transition;
+ update?: Transition | null;
/**
* Transition used for the pulse animation.
- * @default { type: 'timing', duration: 1600, easing: Easing.bezier(0.0, 0.0, 0.0, 1.0) }
+ * @default transition { type: 'timing', duration: 1600, easing: Easing.bezier(0.0, 0.0, 0.0, 1.0) }
*/
pulse?: Transition;
/**
@@ -110,16 +127,6 @@ export type ScrubberBeaconProps = {
*/
pulseRepeatDelay?: number;
};
- /**
- * Opacity of the beacon.
- * @default 1
- */
- opacity?: AnimatedProp;
- /**
- * Stroke color of the beacon circle.
- * @default theme.color.bg
- */
- stroke?: string;
};
export type ScrubberBeaconComponent = React.FC<
@@ -211,10 +218,6 @@ export type ScrubberBaseProps = Pick
* Stroke color for the scrubber line.
*/
lineStroke?: ReferenceLineBaseProps['stroke'];
- /**
- * Transition configuration for the scrubber beacon.
- */
- beaconTransitions?: ScrubberBeaconProps['transitions'];
/**
* Stroke color of the scrubber beacon circle.
* @default theme.color.bg
@@ -222,7 +225,18 @@ export type ScrubberBaseProps = Pick
beaconStroke?: string;
};
-export type ScrubberProps = ScrubberBaseProps;
+export type ScrubberProps = ScrubberBaseProps & {
+ /**
+ * Transition configuration for the scrubber.
+ * Controls enter, update, and pulse animations for beacons and beacon labels.
+ */
+ transitions?: ScrubberBeaconProps['transitions'];
+ /**
+ * Transition configuration for the scrubber beacon.
+ * @deprecated Use `transitions` instead.
+ */
+ beaconTransitions?: ScrubberBeaconProps['transitions'];
+};
export type ScrubberRef = ScrubberBeaconGroupRef;
@@ -253,6 +267,7 @@ export const Scrubber = memo(
beaconLabelFont,
idlePulse,
beaconTransitions,
+ transitions = beaconTransitions,
beaconStroke,
},
ref,
@@ -364,14 +379,16 @@ export const Scrubber = memo(
const isReady = !!xScale;
+ const groupEnterTransition = useMemo(
+ () => getTransition(transitions?.enter, animate, defaultAccessoryEnterTransition),
+ [transitions?.enter, animate],
+ );
+
useEffect(() => {
if (animate && isReady) {
- scrubberOpacity.value = withDelay(
- accessoryFadeTransitionDelay,
- withTiming(1, { duration: accessoryFadeTransitionDuration }),
- );
+ scrubberOpacity.value = buildTransition(1, groupEnterTransition);
}
- }, [animate, isReady, scrubberOpacity]);
+ }, [animate, isReady, scrubberOpacity, groupEnterTransition]);
if (!isReady) return;
@@ -406,7 +423,7 @@ export const Scrubber = memo(
idlePulse={idlePulse}
seriesIds={filteredSeriesIds}
stroke={beaconStroke}
- transitions={beaconTransitions}
+ transitions={transitions}
/>
{!hideBeaconLabels && beaconLabels.length > 0 && (
)}
diff --git a/packages/mobile-visualization/src/chart/scrubber/ScrubberBeaconLabelGroup.tsx b/packages/mobile-visualization/src/chart/scrubber/ScrubberBeaconLabelGroup.tsx
index 449c409891..31161a4ed9 100644
--- a/packages/mobile-visualization/src/chart/scrubber/ScrubberBeaconLabelGroup.tsx
+++ b/packages/mobile-visualization/src/chart/scrubber/ScrubberBeaconLabelGroup.tsx
@@ -1,11 +1,11 @@
import { memo, useCallback, useMemo, useState } from 'react';
import type { SharedValue } from 'react-native-reanimated';
-import { useDerivedValue } from 'react-native-reanimated';
+import { useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated';
import type { AnimatedProp } from '@shopify/react-native-skia';
import { useCartesianChartContext } from '../ChartProvider';
import type { ChartTextChildren, ChartTextProps } from '../text';
-import { applySerializableScale, useScrubberContext } from '../utils';
+import { applySerializableScale, unwrapAnimatedValue, useScrubberContext } from '../utils';
import {
calculateLabelYPositions,
getLabelPosition,
@@ -13,14 +13,26 @@ import {
type LabelPosition,
type ScrubberLabelPosition,
} from '../utils/scrubber';
+import {
+ buildTransition,
+ defaultTransition,
+ getTransition,
+ type Transition,
+} from '../utils/transition';
import { DefaultScrubberBeaconLabel } from './DefaultScrubberBeaconLabel';
-import type { ScrubberBeaconLabelComponent, ScrubberBeaconLabelProps } from './Scrubber';
+import type {
+ ScrubberBeaconLabelComponent,
+ ScrubberBeaconLabelProps,
+ ScrubberBeaconProps,
+} from './Scrubber';
const PositionedLabel = memo<{
index: number;
positions: SharedValue<(LabelPosition | null)[]>;
position: SharedValue;
+ isIdle: AnimatedProp;
+ updateTransition: Transition;
label: ChartTextChildren;
color?: string;
seriesId: string;
@@ -33,6 +45,8 @@ const PositionedLabel = memo<{
index,
positions,
position,
+ isIdle,
+ updateTransition,
label,
color,
seriesId,
@@ -46,7 +60,20 @@ const PositionedLabel = memo<{
[positions, index],
);
const x = useDerivedValue(() => positions.value[index]?.x ?? 0, [positions, index]);
- const y = useDerivedValue(() => positions.value[index]?.y ?? 0, [positions, index]);
+ const targetY = useDerivedValue(() => positions.value[index]?.y ?? 0, [positions, index]);
+
+ const animatedY = useSharedValue(0);
+ useAnimatedReaction(
+ () => ({ y: targetY.value, idle: unwrapAnimatedValue(isIdle) }),
+ (current, previous) => {
+ if (previous === null || !previous.idle || !current.idle) {
+ animatedY.value = current.y;
+ } else {
+ animatedY.value = buildTransition(current.y, updateTransition);
+ }
+ },
+ [updateTransition],
+ );
const dx = useDerivedValue(() => {
return position.value === 'right' ? labelHorizontalOffset : -labelHorizontalOffset;
@@ -68,7 +95,7 @@ const PositionedLabel = memo<{
opacity={opacity}
seriesId={seriesId}
x={x}
- y={y}
+ y={animatedY}
/>
);
},
@@ -107,6 +134,10 @@ export type ScrubberBeaconLabelGroupProps = ScrubberBeaconLabelGroupBaseProps &
* @default DefaultScrubberBeaconLabel
*/
BeaconLabelComponent?: ScrubberBeaconLabelComponent;
+ /**
+ * Transition configuration for beacon label animations.
+ */
+ transitions?: ScrubberBeaconProps['transitions'];
};
export const ScrubberBeaconLabelGroup = memo(
@@ -117,6 +148,7 @@ export const ScrubberBeaconLabelGroup = memo(
labelFont,
labelPreferredSide = 'right',
BeaconLabelComponent = DefaultScrubberBeaconLabel,
+ transitions,
}) => {
const {
getSeries,
@@ -126,9 +158,19 @@ export const ScrubberBeaconLabelGroup = memo(
getXAxis,
drawingArea,
dataLength,
+ animate,
} = useCartesianChartContext();
const { scrubberPosition } = useScrubberContext();
+ const isIdle = useDerivedValue(() => {
+ return scrubberPosition.value === undefined;
+ }, [scrubberPosition]);
+
+ const updateTransition = useMemo(
+ () => getTransition(transitions?.update, animate, defaultTransition),
+ [transitions?.update, animate],
+ );
+
const [labelDimensions, setLabelDimensions] = useState>({});
const handleDimensionsChange = useCallback((id: string, dimensions: LabelDimensions) => {
@@ -281,6 +323,7 @@ export const ScrubberBeaconLabelGroup = memo(
BeaconLabelComponent={BeaconLabelComponent}
color={labelInfo.color}
index={index}
+ isIdle={isIdle}
label={labelInfo.label}
labelFont={labelFont}
labelHorizontalOffset={labelHorizontalOffset}
@@ -288,6 +331,7 @@ export const ScrubberBeaconLabelGroup = memo(
position={currentPosition}
positions={allLabelPositions}
seriesId={info.seriesId}
+ updateTransition={updateTransition}
/>
);
});
diff --git a/packages/mobile-visualization/src/chart/utils/__tests__/bar.test.ts b/packages/mobile-visualization/src/chart/utils/__tests__/bar.test.ts
index a4952fb69b..f6b5730db7 100644
--- a/packages/mobile-visualization/src/chart/utils/__tests__/bar.test.ts
+++ b/packages/mobile-visualization/src/chart/utils/__tests__/bar.test.ts
@@ -1,5 +1,10 @@
import { getBarSizeAdjustment } from '../bar';
+jest.mock('@shopify/react-native-skia', () => ({
+ Skia: { Path: { Make: jest.fn(), MakeFromSVGString: jest.fn() } },
+ notifyChange: jest.fn(),
+}));
+
describe('getBarSizeAdjustment', () => {
it('should return 0 when barCount is 0', () => {
const result = getBarSizeAdjustment(0, 10);
diff --git a/packages/mobile-visualization/src/chart/utils/__tests__/transition.test.ts b/packages/mobile-visualization/src/chart/utils/__tests__/transition.test.ts
index 6d21762226..7973e4830c 100644
--- a/packages/mobile-visualization/src/chart/utils/__tests__/transition.test.ts
+++ b/packages/mobile-visualization/src/chart/utils/__tests__/transition.test.ts
@@ -5,7 +5,6 @@ import {
buildTransition,
defaultTransition,
type Transition,
- useD3PathInterpolation,
usePathTransition,
} from '../transition';
@@ -165,68 +164,6 @@ describe('buildTransition', () => {
});
});
-describe('useD3PathInterpolation', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('should create interpolated path', () => {
- const progress = { value: 0 };
- const fromPath = 'M0,0L10,10';
- const toPath = 'M0,0L20,20';
-
- const { result } = renderHook(() => useD3PathInterpolation(progress as any, fromPath, toPath));
-
- expect(result.current).toBeDefined();
- expect(result.current).toHaveProperty('value');
- });
-
- it('should handle path changes', () => {
- const progress = { value: 0.5 };
- const fromPath1 = 'M0,0L10,10';
- const toPath1 = 'M0,0L20,20';
-
- const { result, rerender } = renderHook(
- ({ from, to }) => useD3PathInterpolation(progress as any, from, to),
- {
- initialProps: { from: fromPath1, to: toPath1 },
- },
- );
-
- const firstResult = result.current;
- expect(firstResult).toBeDefined();
-
- // Update paths
- const fromPath2 = 'M0,0L15,15';
- const toPath2 = 'M0,0L25,25';
- rerender({ from: fromPath2, to: toPath2 });
-
- // Result should be updated
- expect(result.current).toBeDefined();
- });
-
- it('should call d3 interpolatePath', () => {
- const { interpolatePath } = require('d3-interpolate-path');
- const progress = { value: 0 };
- const fromPath = 'M0,0L10,10';
- const toPath = 'M0,0L20,20';
-
- renderHook(() => useD3PathInterpolation(progress as any, fromPath, toPath));
-
- expect(interpolatePath).toHaveBeenCalledWith(fromPath, toPath);
- });
-
- it('should create Skia paths from SVG strings', () => {
- const progress = { value: 0 };
- const fromPath = 'M0,0L10,10';
- const toPath = 'M0,0L20,20';
-
- renderHook(() => useD3PathInterpolation(progress as any, fromPath, toPath));
-
- expect(Skia.Path.MakeFromSVGString).toHaveBeenCalled();
- });
-});
-
describe('useInterpolator', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -362,7 +299,7 @@ describe('usePathTransition', () => {
const { result } = renderHook(() =>
usePathTransition({
currentPath,
- transition,
+ transitions: { update: transition },
}),
);
diff --git a/packages/mobile-visualization/src/chart/utils/axis.ts b/packages/mobile-visualization/src/chart/utils/axis.ts
index 1abdf96a59..bda9239f0e 100644
--- a/packages/mobile-visualization/src/chart/utils/axis.ts
+++ b/packages/mobile-visualization/src/chart/utils/axis.ts
@@ -162,7 +162,7 @@ export const getAxisScale = ({
if (!isValidBounds(adjustedDomain))
throw new Error(
- 'Invalid domain bounds. See https://cds.coinbase.com/http://localhost:3000/components/graphs/XAxis/#domain',
+ 'Invalid domain bounds. See https://cds.coinbase.com/components/charts/XAxis/#domain',
);
if (scaleType === 'band') {
@@ -211,7 +211,7 @@ export const getAxisConfig = (
// forces id to be defined on every input config when there are multiple axes
if (axesLength > 1 && axes.some(({ id }) => id === undefined)) {
throw new Error(
- 'When defining multiple axes, each must have a unique id. See https://cds.coinbase.com/components/graphs/YAxis/#multiple-y-axes.',
+ 'When defining multiple axes, each must have a unique id. See https://cds.coinbase.com/components/charts/YAxis/#multiple-y-axes.',
);
}
diff --git a/packages/mobile-visualization/src/chart/utils/bar.ts b/packages/mobile-visualization/src/chart/utils/bar.ts
index c30de9154e..e69c1224b4 100644
--- a/packages/mobile-visualization/src/chart/utils/bar.ts
+++ b/packages/mobile-visualization/src/chart/utils/bar.ts
@@ -1,3 +1,51 @@
+import { defaultTransition, type Transition } from './transition';
+
+/**
+ * A bar-specific transition that extends Transition with stagger support.
+ * When `staggerDelay` is provided, bars will animate with increasing delays
+ * based on their horizontal position (leftmost starts first, rightmost last).
+ *
+ * @example
+ * // Bars stagger in from left to right over 250ms, each animating for 750ms
+ * { type: 'timing', duration: 750, staggerDelay: 250 }
+ */
+export type BarTransition = Transition & {
+ /**
+ * Maximum stagger delay (ms) distributed across bars by x position.
+ * Leftmost bar starts immediately, rightmost starts after this delay.
+ */
+ staggerDelay?: number;
+};
+
+/**
+ * Strips `staggerDelay` from a transition and computes a positional delay.
+ *
+ * @param transition - The transition config (may include staggerDelay)
+ * @param normalizedX - The bar's normalized x position (0 = left edge, 1 = right edge)
+ * @returns A standard Transition with computed delay
+ */
+export const withStaggerDelayTransition = (
+ transition: BarTransition,
+ normalizedX: number,
+): Transition => {
+ const { staggerDelay, ...baseTransition } = transition;
+ if (!staggerDelay) return transition;
+ return {
+ ...baseTransition,
+ delay: (baseTransition?.delay ?? 0) + normalizedX * staggerDelay,
+ };
+};
+
+/**
+ * Default bar enter transition. Uses the default spring with a stagger delay
+ * so bars spring into place from left to right.
+ * `{ type: 'spring', stiffness: 900, damping: 120, staggerDelay: 250 }`
+ */
+export const defaultBarEnterTransition: BarTransition = {
+ ...defaultTransition,
+ staggerDelay: 250,
+};
+
/**
* Calculates the size adjustment needed for bars when accounting for gaps between them.
* This function helps determine how much to reduce each bar's width to accommodate
diff --git a/packages/mobile-visualization/src/chart/utils/path.ts b/packages/mobile-visualization/src/chart/utils/path.ts
index dd3330e925..089976dbe0 100644
--- a/packages/mobile-visualization/src/chart/utils/path.ts
+++ b/packages/mobile-visualization/src/chart/utils/path.ts
@@ -14,6 +14,16 @@ import {
import { projectPoint, projectPoints } from './point';
import { type ChartScaleFunction, isCategoricalScale } from './scale';
+import type { Transition } from './transition';
+
+/**
+ * Default enter transition for path-based components (Line, Area).
+ * `{ type: 'timing', duration: 500 }`
+ */
+export const defaultPathEnterTransition: Transition = {
+ type: 'timing',
+ duration: 500,
+};
export type ChartPathCurveType =
| 'bump'
diff --git a/packages/mobile-visualization/src/chart/utils/transition.ts b/packages/mobile-visualization/src/chart/utils/transition.ts
index 5824668c10..2337b672b7 100644
--- a/packages/mobile-visualization/src/chart/utils/transition.ts
+++ b/packages/mobile-visualization/src/chart/utils/transition.ts
@@ -4,6 +4,7 @@ import {
type SharedValue,
useAnimatedReaction,
useSharedValue,
+ withDelay,
withSpring,
type WithSpringConfig,
withTiming,
@@ -25,12 +26,23 @@ import { interpolatePath } from 'd3-interpolate-path';
* // Timing animation
* { type: 'timing', duration: 500, easing: Easing.inOut(Easing.ease) }
*/
-export type Transition =
+export type Transition = (
| ({ type: 'timing' } & WithTimingConfig)
- | ({ type: 'spring' } & WithSpringConfig);
+ | ({ type: 'spring' } & WithSpringConfig)
+) & {
+ /**
+ * Delay in milliseconds (ms) before the animation starts.
+ *
+ * @example
+ * // Wait 2 seconds before animating
+ * { type: 'timing', duration: 500, delay: 2000 }
+ */
+ delay?: number;
+};
/**
- * Default transition configuration used across all chart components.
+ * Default update transition used across all chart components.
+ * `{ type: 'spring', stiffness: 900, damping: 120 }`
*/
export const defaultTransition: Transition = {
type: 'spring',
@@ -38,6 +50,15 @@ export const defaultTransition: Transition = {
damping: 120,
};
+/**
+ * Instant transition that completes immediately with no animation.
+ * Used when a transition is set to `null`.
+ */
+export const instantTransition: Transition = {
+ type: 'timing',
+ duration: 0,
+};
+
/**
* Duration in milliseconds for accessory elements to fade in.
*/
@@ -49,45 +70,27 @@ export const accessoryFadeTransitionDuration = 150;
export const accessoryFadeTransitionDelay = 350;
/**
- * Custom hook that uses d3-interpolate-path for more robust path interpolation.
- * then use Skia's native interpolation in the worklet.
- *
- * @param progress - Shared value between 0 and 1
- * @param fromPath - Starting path as SVG string
- * @param toPath - Ending path as SVG string
- * @returns Interpolated SkPath as a shared value
+ * Default enter transition for accessory elements (Point, Scrubber beacons).
+ * `{ type: 'timing', duration: 150, delay: 350 }`
*/
-export const useD3PathInterpolation = (
- progress: SharedValue,
- fromPath: string,
- toPath: string,
-): SharedValue => {
- // Pre-compute intermediate paths on JS thread using d3-interpolate-path
- const { fromSkiaPath, i0, i1, toSkiaPath } = useMemo(() => {
- const pathInterpolator = interpolatePath(fromPath, toPath);
- const d = 1e-3;
-
- return {
- fromSkiaPath: Skia.Path.MakeFromSVGString(fromPath) ?? Skia.Path.Make(),
- i0: Skia.Path.MakeFromSVGString(pathInterpolator(d)) ?? Skia.Path.Make(),
- i1: Skia.Path.MakeFromSVGString(pathInterpolator(1 - d)) ?? Skia.Path.Make(),
- toSkiaPath: Skia.Path.MakeFromSVGString(toPath) ?? Skia.Path.Make(),
- };
- }, [fromPath, toPath]);
-
- const result = useSharedValue(fromSkiaPath);
-
- useAnimatedReaction(
- () => progress.value,
- (t) => {
- 'worklet';
- result.value = i1.interpolate(i0, t) ?? toSkiaPath;
- notifyChange(result);
- },
- [fromSkiaPath, i0, i1, toSkiaPath],
- );
+export const defaultAccessoryEnterTransition: Transition = {
+ type: 'timing',
+ duration: accessoryFadeTransitionDuration,
+ delay: accessoryFadeTransitionDelay,
+};
- return result;
+/**
+ * Resolves a transition value based on the animation state and a default.
+ * @note Passing in null will disable an animation.
+ * @note Passing in undefined will use the provided default.
+ */
+export const getTransition = (
+ value: Transition | null | undefined,
+ animate: boolean,
+ defaultValue: Transition,
+): Transition => {
+ if (!animate || value === null) return instantTransition;
+ return value ?? defaultValue;
};
// Interpolator and useInterpolator are brought over from non exported code in @shopify/react-native-skia
@@ -147,18 +150,29 @@ export const useInterpolator = (
*/
export const buildTransition = (targetValue: number, transition: Transition): number => {
'worklet';
- switch (transition.type) {
+ const { delay: delayMs, ...config } = transition;
+
+ let animation: number;
+ switch (config.type) {
case 'timing': {
- return withTiming(targetValue, transition);
+ animation = withTiming(targetValue, config);
+ break;
}
case 'spring': {
- return withSpring(targetValue, transition);
+ animation = withSpring(targetValue, config);
+ break;
}
default: {
- // Fallback to default transition config
- return withSpring(targetValue, defaultTransition);
+ animation = withSpring(targetValue, defaultTransition);
+ break;
}
}
+
+ if (delayMs && delayMs > 0) {
+ return withDelay(delayMs, animation);
+ }
+
+ return animation;
};
/**
@@ -166,15 +180,16 @@ export const buildTransition = (targetValue: number, transition: Transition): nu
*
* @param currentPath - Current target path to animate to
* @param initialPath - Initial path for enter animation. When provided, the first animation will go from initialPath to currentPath.
- * @param transition - Transition configuration
+ * @param transitions - Transition configuration for enter and update animations
* @returns Animated SkPath as a shared value
*
* @example
* // Simple path transition
* const path = usePathTransition({
* currentPath: d ?? '',
- * animate: shouldAnimate,
- * transition: { type: 'timing', duration: 3000 }
+ * transitions: {
+ * update: { type: 'timing', duration: 3000 },
+ * },
* });
*
* @example
@@ -182,13 +197,16 @@ export const buildTransition = (targetValue: number, transition: Transition): nu
* const path = usePathTransition({
* currentPath: targetPath,
* initialPath: baselinePath,
- * animate: true,
- * transition: { type: 'timing', duration: 300 }
+ * transitions: {
+ * enter: { type: 'tween', duration: 500 },
+ * update: { type: 'spring', stiffness: 900, damping: 120 },
+ * },
* });
*/
export const usePathTransition = ({
currentPath,
initialPath,
+ transitions,
transition = defaultTransition,
}: {
/**
@@ -202,31 +220,92 @@ export const usePathTransition = ({
*/
initialPath?: string;
/**
- * Transition configuration
+ * Transition configuration for enter and update animations.
+ */
+ transitions?: {
+ /**
+ * Transition for the initial enter animation (initialPath → currentPath).
+ * Only used when `initialPath` is provided.
+ * If not provided, falls back to `update`.
+ */
+ enter?: Transition;
+ /**
+ * Transition for subsequent data update animations.
+ * @default defaultTransition
+ */
+ update?: Transition;
+ };
+ /**
+ * Transition for updates.
+ * @deprecated Use `transitions.update` instead.
*/
transition?: Transition;
}): SharedValue => {
- // Track the previous path - updated in useEffect AFTER render,
- // so during render it naturally holds the "from" path value
- const previousPathRef = useRef(initialPath ?? currentPath);
+ const updateTransition = transitions?.update ?? transition;
+ const enterTransition = transitions?.enter;
+
+ const targetPathRef = useRef(initialPath ?? currentPath);
+ const isFirstAnimation = useRef(!!initialPath);
+ const interpolatorRef = useRef<((t: number) => string) | null>(null);
const progress = useSharedValue(0);
- // During render: previousPathRef still has old value, currentPath is new
- const fromPath = previousPathRef.current;
- const toPath = currentPath;
+ const initialSkiaPath =
+ Skia.Path.MakeFromSVGString(initialPath ?? currentPath) ?? Skia.Path.Make();
+ const normalizedStartShared = useSharedValue(initialSkiaPath);
+ const normalizedEndShared = useSharedValue(initialSkiaPath);
+ const fallbackPathShared = useSharedValue(initialSkiaPath);
+ const result = useSharedValue(initialSkiaPath);
useEffect(() => {
- const shouldAnimate = previousPathRef.current !== currentPath;
+ if (targetPathRef.current !== currentPath) {
+ let fromPath = targetPathRef.current;
+ if (interpolatorRef.current) {
+ const p = Math.min(Math.max(progress.value, 0), 1);
+ fromPath = interpolatorRef.current(p);
+ }
+
+ targetPathRef.current = currentPath;
+
+ const pathInterpolator = interpolatePath(fromPath, currentPath);
+ interpolatorRef.current = pathInterpolator;
- if (shouldAnimate) {
- // Update ref for next path change (happens after this render)
- previousPathRef.current = currentPath;
+ normalizedStartShared.value =
+ Skia.Path.MakeFromSVGString(pathInterpolator(0)) ?? Skia.Path.Make();
+ normalizedEndShared.value =
+ Skia.Path.MakeFromSVGString(pathInterpolator(1)) ?? Skia.Path.Make();
+ fallbackPathShared.value = Skia.Path.MakeFromSVGString(currentPath) ?? Skia.Path.Make();
+
+ const activeTransition =
+ isFirstAnimation.current && enterTransition !== undefined
+ ? enterTransition
+ : updateTransition;
+
+ isFirstAnimation.current = false;
- // Animate from old path to new path
progress.value = 0;
- progress.value = buildTransition(1, transition);
+ progress.value = buildTransition(1, activeTransition);
}
- }, [currentPath, transition, progress]);
+ }, [
+ currentPath,
+ updateTransition,
+ enterTransition,
+ progress,
+ normalizedStartShared,
+ normalizedEndShared,
+ fallbackPathShared,
+ ]);
- return useD3PathInterpolation(progress, fromPath, toPath);
+ useAnimatedReaction(
+ () => ({ p: progress.value, to: fallbackPathShared.value }),
+ ({ p }) => {
+ 'worklet';
+ result.value =
+ normalizedEndShared.value.interpolate(normalizedStartShared.value, p) ??
+ fallbackPathShared.value;
+ notifyChange(result);
+ },
+ [],
+ );
+
+ return result;
};
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index d6b320056b..28d5b3cac4 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -8,6 +8,36 @@ All notable changes to this project will be documented in this file.
+## 8.48.3 ((2/25/2026, 08:36 PM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.48.2 (2/25/2026 PST)
+
+#### 🐞 Fixes
+
+- Deprecate useStatusBarHeight hook.
+
+## 8.48.1 ((2/25/2026, 01:30 PM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.48.0 (2/24/2026 PST)
+
+#### 🚀 Updates
+
+- Add start/end icon/node support to Tag. [[#421](https://github.com/coinbase/cds/pull/421)]
+
+## 8.47.4 (2/23/2026 PST)
+
+#### 🐞 Fixes
+
+- Fix: set paddingStart on Input for compact label. [[#423](https://github.com/coinbase/cds/pull/423)]
+
+## 8.47.3 ((2/20/2026, 09:16 AM PST))
+
+This is an artificial version bump with no new change.
+
## 8.47.2 (2/19/2026 PST)
#### 🐞 Fixes
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
index 7f4cec0b36..b5aee4161c 100644
--- a/packages/mobile/package.json
+++ b/packages/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-mobile",
- "version": "8.47.2",
+ "version": "8.48.3",
"description": "Coinbase Design System - Mobile",
"repository": {
"type": "git",
diff --git a/packages/mobile/src/alpha/select/__stories__/AlphaSelect.stories.tsx b/packages/mobile/src/alpha/select/__stories__/AlphaSelect.stories.tsx
index 408c152b44..9220000b13 100644
--- a/packages/mobile/src/alpha/select/__stories__/AlphaSelect.stories.tsx
+++ b/packages/mobile/src/alpha/select/__stories__/AlphaSelect.stories.tsx
@@ -57,6 +57,14 @@ const exampleOptionsWithNoLabelOrDescription = [
{ value: '4' },
];
+const exampleOptionsWithLongLabels = [
+ { value: null, label: 'Remove selection' },
+ { value: '1', label: 'Fraction fraction fraction fraction fraction' },
+ { value: '2', label: 'Truncation truncation truncation truncation truncation' },
+ { value: '3', label: 'A A A A A A A A A A A A A A A A' },
+ { value: '4', label: 'Bee Bee Bee Bee Bee Bee Bee Bee Bee Bee' },
+];
+
const exampleOptionsWithSomeDisabled = [
{ value: null, label: 'Remove selection' },
{ value: '1', label: 'Option 1', disabled: true },
@@ -886,6 +894,23 @@ const MultiSelectCustomSelectAllOptionExample = () => {
);
};
+const MultiSelectLongLabelOptionsExample = () => {
+ const { value, onChange } = useMultiSelect({
+ initialValue: ['1'],
+ });
+
+ return (
+
+ );
+};
+
const MultiSelectDisabledExample = () => {
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -1405,6 +1430,9 @@ const SelectV3Screen = () => {
+
+
+
diff --git a/packages/mobile/src/controls/TextInput.tsx b/packages/mobile/src/controls/TextInput.tsx
index 97aaa20072..9e8e6e487d 100644
--- a/packages/mobile/src/controls/TextInput.tsx
+++ b/packages/mobile/src/controls/TextInput.tsx
@@ -346,7 +346,7 @@ export const TextInput = memo(
importantForAccessibility={startIconA11yLabel ? 'auto' : 'no'}
onPress={handleNodePress}
>
-
+
{compact &&
(labelNode ? labelNode : !!label && {label})}
{!!start && (
diff --git a/packages/mobile/src/hooks/useStatusBarHeight.ts b/packages/mobile/src/hooks/useStatusBarHeight.ts
index 0b47f88dfa..ac690b95be 100644
--- a/packages/mobile/src/hooks/useStatusBarHeight.ts
+++ b/packages/mobile/src/hooks/useStatusBarHeight.ts
@@ -9,8 +9,18 @@ type StatusBarNativeModule = {
} & NativeModule;
/**
- * StatusBar api returns weird incorrect values for iOS.
- * This implementation is based off of the implementation identified in this article. https://blog.expo.dev/the-status-bar-manager-in-react-native-6226058ecba
+ * @deprecated Use `useSafeAreaInsets().top` from `react-native-safe-area-context` instead.
+ * This approach is recommended by Expo and provides more reliable values across platforms.
+ * @see https://docs.expo.dev/versions/latest/sdk/safe-area-context/
+ *
+ * @example
+ * // Before (deprecated)
+ * const statusBarHeight = useStatusBarHeight();
+ *
+ * // After (recommended)
+ * import { useSafeAreaInsets } from 'react-native-safe-area-context';
+ * const insets = useSafeAreaInsets();
+ * const statusBarHeight = insets.top;
*/
export const useStatusBarHeight = () => {
const [statusBarHeight, setStatusBarHeight] = useState();
diff --git a/packages/mobile/src/tag/Tag.tsx b/packages/mobile/src/tag/Tag.tsx
index b6c4d3ba75..ce625bd35f 100644
--- a/packages/mobile/src/tag/Tag.tsx
+++ b/packages/mobile/src/tag/Tag.tsx
@@ -8,6 +8,7 @@ import {
tagHorizontalSpacing,
} from '@coinbase/cds-common/tokens/tags';
import type {
+ IconName,
SharedAccessibilityProps,
SharedProps,
TagColorScheme,
@@ -16,6 +17,7 @@ import type {
} from '@coinbase/cds-common/types';
import { useTheme } from '../hooks/useTheme';
+import { Icon } from '../icons/Icon';
import { Box, type BoxProps } from '../layout';
import { Text } from '../typography/Text';
@@ -44,6 +46,18 @@ export type TagBaseProps = SharedProps &
color?: ThemeVars.SpectrumColor;
/** Setting a custom max width for this tag will enable text truncation */
maxWidth?: BoxProps['maxWidth'];
+ /** Set the start node */
+ start?: React.ReactNode;
+ /** Icon to render at the start of the tag. */
+ startIcon?: IconName;
+ /** Whether the start icon is active */
+ startIconActive?: boolean;
+ /** Set the end node */
+ end?: React.ReactNode;
+ /** Icon to render at the end of the tag. */
+ endIcon?: IconName;
+ /** Whether the end icon is active */
+ endIconActive?: boolean;
};
export type TagProps = TagBaseProps &
@@ -59,8 +73,17 @@ export const Tag = memo(
colorScheme = 'blue',
background: customBackground,
color: customColor,
+ start,
+ startIcon,
+ startIconActive,
+ end,
+ endIcon,
+ endIconActive,
alignItems = 'center',
+ flexDirection = 'row',
+ gap = 0.5,
justifyContent = 'center',
+ paddingY = 0.25,
testID = 'cds-tag',
...props
}: TagProps,
@@ -78,12 +101,20 @@ export const Tag = memo(
background="bg"
borderRadius={tagBorderRadiusMap[intent]}
dangerouslySetBackground={backgroundColor}
+ flexDirection={flexDirection}
+ gap={gap}
justifyContent={justifyContent}
paddingX={tagHorizontalSpacing[intent]}
- paddingY={0.25}
+ paddingY={paddingY}
testID={testID}
{...props}
>
+ {start ? (
+ start
+ ) : startIcon ? (
+
+ ) : null}
+
{children}
+
+ {end ? (
+ end
+ ) : endIcon ? (
+
+ ) : null}
);
},
diff --git a/packages/mobile/src/tag/__stories__/Tag.stories.tsx b/packages/mobile/src/tag/__stories__/Tag.stories.tsx
index d422536d13..cff48c4bec 100644
--- a/packages/mobile/src/tag/__stories__/Tag.stories.tsx
+++ b/packages/mobile/src/tag/__stories__/Tag.stories.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import startCase from 'lodash/startCase';
import { Example, ExampleScreen } from '../../examples/ExampleScreen';
+import { Icon } from '../../icons/Icon';
import { Tag, type TagBaseProps } from '../Tag';
type TagPropConfig = {
@@ -87,6 +88,35 @@ const TagScreen = () => {
})}
))}
+
+
+ Start icon
+
+
+ End icon
+
+
+ Both icons
+
+
+ Promotional with icons
+
+
+
+ }>
+ Custom start node
+
+ }>
+ Custom end node
+
+ }
+ start={}
+ >
+ Both custom nodes
+
+
);
};
diff --git a/packages/mobile/src/tag/__tests__/Tag.test.tsx b/packages/mobile/src/tag/__tests__/Tag.test.tsx
index 7e65aef05d..ccea43ac4c 100644
--- a/packages/mobile/src/tag/__tests__/Tag.test.tsx
+++ b/packages/mobile/src/tag/__tests__/Tag.test.tsx
@@ -1,4 +1,4 @@
-import { Text } from 'react-native';
+import { Text, View } from 'react-native';
import { tagColorMap, tagEmphasisColorMap } from '@coinbase/cds-common/tokens/tags';
import { render, screen } from '@testing-library/react-native';
@@ -106,6 +106,52 @@ describe('Tag', () => {
});
});
+ it('renders with a startIcon', () => {
+ render(
+
+
+ Tag
+
+ ,
+ );
+ expect(screen.getByTestId(TEST_ID)).toBeDefined();
+ expect(screen.getByText('Tag')).toBeDefined();
+ });
+
+ it('renders with an endIcon', () => {
+ render(
+
+
+ Tag
+
+ ,
+ );
+ expect(screen.getByTestId(TEST_ID)).toBeDefined();
+ expect(screen.getByText('Tag')).toBeDefined();
+ });
+
+ it('renders with a custom start node', () => {
+ render(
+
+ } testID={TEST_ID}>
+ Tag
+
+ ,
+ );
+ expect(screen.getByTestId('custom-start')).toBeDefined();
+ });
+
+ it('renders with a custom end node', () => {
+ render(
+
+ } testID={TEST_ID}>
+ Tag
+
+ ,
+ );
+ expect(screen.getByTestId('custom-end')).toBeDefined();
+ });
+
it('verifies tagColorMap maps correctly to tagEmphasisColorMap for backward compatibility', () => {
expect(tagColorMap.informational).toEqual(tagEmphasisColorMap.low);
expect(tagColorMap.promotional).toEqual(tagEmphasisColorMap.high);
diff --git a/packages/ui-mobile-playground/CHANGELOG.md b/packages/ui-mobile-playground/CHANGELOG.md
index 76e93afb4c..826c941c7c 100644
--- a/packages/ui-mobile-playground/CHANGELOG.md
+++ b/packages/ui-mobile-playground/CHANGELOG.md
@@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.
+## 4.9.0 (2/20/2026 PST)
+
+#### 🚀 Updates
+
+- Add new mobile routes. [[#400](https://github.com/coinbase/cds/pull/400)]
+
## 4.8.0 (2/6/2026 PST)
#### 🚀 Updates
diff --git a/packages/ui-mobile-playground/package.json b/packages/ui-mobile-playground/package.json
index e0846121aa..fbf4104fe3 100644
--- a/packages/ui-mobile-playground/package.json
+++ b/packages/ui-mobile-playground/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/ui-mobile-playground",
- "version": "4.8.0",
+ "version": "4.9.0",
"description": "Mobile UI Components in a Playground",
"repository": {
"type": "git",
diff --git a/packages/ui-mobile-playground/src/routes.ts b/packages/ui-mobile-playground/src/routes.ts
index 20e857e39e..0d2ce796d5 100644
--- a/packages/ui-mobile-playground/src/routes.ts
+++ b/packages/ui-mobile-playground/src/routes.ts
@@ -142,9 +142,10 @@ export const routes = [
.default,
},
{
- key: 'Chart',
+ key: 'ChartTransitions',
getComponent: () =>
- require('@coinbase/cds-mobile-visualization/chart/__stories__/Chart.stories').default,
+ require('@coinbase/cds-mobile-visualization/chart/__stories__/ChartTransitions.stories')
+ .default,
},
{
key: 'Checkbox',
@@ -246,6 +247,11 @@ export const routes = [
getComponent: () =>
require('@coinbase/cds-mobile/overlays/__stories__/DrawerMisc.stories').default,
},
+ {
+ key: 'DrawerReduceMotion',
+ getComponent: () =>
+ require('@coinbase/cds-mobile/overlays/__stories__/DrawerReduceMotion.stories').default,
+ },
{
key: 'DrawerRight',
getComponent: () =>
@@ -840,6 +846,11 @@ export const routes = [
getComponent: () =>
require('@coinbase/cds-mobile/overlays/__stories__/TrayRedesign.stories').default,
},
+ {
+ key: 'TrayReduceMotion',
+ getComponent: () =>
+ require('@coinbase/cds-mobile/overlays/__stories__/TrayReduceMotion.stories').default,
+ },
{
key: 'TrayScrollable',
getComponent: () =>
diff --git a/packages/ui-mobile-visreg/src/routes.ts b/packages/ui-mobile-visreg/src/routes.ts
index 20e857e39e..0d2ce796d5 100644
--- a/packages/ui-mobile-visreg/src/routes.ts
+++ b/packages/ui-mobile-visreg/src/routes.ts
@@ -142,9 +142,10 @@ export const routes = [
.default,
},
{
- key: 'Chart',
+ key: 'ChartTransitions',
getComponent: () =>
- require('@coinbase/cds-mobile-visualization/chart/__stories__/Chart.stories').default,
+ require('@coinbase/cds-mobile-visualization/chart/__stories__/ChartTransitions.stories')
+ .default,
},
{
key: 'Checkbox',
@@ -246,6 +247,11 @@ export const routes = [
getComponent: () =>
require('@coinbase/cds-mobile/overlays/__stories__/DrawerMisc.stories').default,
},
+ {
+ key: 'DrawerReduceMotion',
+ getComponent: () =>
+ require('@coinbase/cds-mobile/overlays/__stories__/DrawerReduceMotion.stories').default,
+ },
{
key: 'DrawerRight',
getComponent: () =>
@@ -840,6 +846,11 @@ export const routes = [
getComponent: () =>
require('@coinbase/cds-mobile/overlays/__stories__/TrayRedesign.stories').default,
},
+ {
+ key: 'TrayReduceMotion',
+ getComponent: () =>
+ require('@coinbase/cds-mobile/overlays/__stories__/TrayReduceMotion.stories').default,
+ },
{
key: 'TrayScrollable',
getComponent: () =>
diff --git a/packages/web-visualization/CHANGELOG.md b/packages/web-visualization/CHANGELOG.md
index 3c7023d45b..28d016b84f 100644
--- a/packages/web-visualization/CHANGELOG.md
+++ b/packages/web-visualization/CHANGELOG.md
@@ -12,6 +12,17 @@ All notable changes to this project will be documented in this file.
#### 📘 Misc
+- Clarify framer-motion is a peerDependency. [[#437](https://github.com/coinbase/cds/pull/437)]
+- Update oudated doc links. [[#440](https://github.com/coinbase/cds/pull/440)]
+
+## 3.4.0-beta.19 (2/20/2026 PST)
+
+#### 🚀 Updates
+
+- Support custom enter transitions [[#400](https://github.com/coinbase/cds/pull/400/)]
+
+#### 📘 Misc
+
- Update jsdocs for styles props. [[#384](https://github.com/coinbase/cds/pull/384)]
## 3.4.0-beta.18 (2/8/2026 PST)
diff --git a/packages/web-visualization/package.json b/packages/web-visualization/package.json
index 8c31aa5ed8..65267cbfa4 100644
--- a/packages/web-visualization/package.json
+++ b/packages/web-visualization/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-web-visualization",
- "version": "3.4.0-beta.18",
+ "version": "3.4.0-beta.19",
"description": "Coinbase Design System - Web Sparkline",
"repository": {
"type": "git",
@@ -42,6 +42,7 @@
"@coinbase/cds-lottie-files": "workspace:^",
"@coinbase/cds-utils": "workspace:^",
"@coinbase/cds-web": "workspace:^",
+ "framer-motion": "^10.18.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
@@ -64,6 +65,7 @@
"@coinbase/cds-web": "workspace:^",
"@linaria/core": "^3.0.0-beta.22",
"@types/react": "^18.3.12",
- "@types/react-dom": "^18.3.1"
+ "@types/react-dom": "^18.3.1",
+ "framer-motion": "^10.18.0"
}
}
diff --git a/packages/web-visualization/src/chart/ChartProvider.tsx b/packages/web-visualization/src/chart/ChartProvider.tsx
index 6cb100f2b1..192421a5d0 100644
--- a/packages/web-visualization/src/chart/ChartProvider.tsx
+++ b/packages/web-visualization/src/chart/ChartProvider.tsx
@@ -10,7 +10,7 @@ export const useCartesianChartContext = (): CartesianChartContextValue => {
const context = useContext(CartesianChartContext);
if (!context) {
throw new Error(
- 'useCartesianChartContext must be used within a CartesianChart component. See http://cds.coinbase.com/components/graphs/CartesianChart.',
+ 'useCartesianChartContext must be used within a CartesianChart component. See https://cds.coinbase.com/components/charts/CartesianChart.',
);
}
return context;
diff --git a/packages/web-visualization/src/chart/Path.tsx b/packages/web-visualization/src/chart/Path.tsx
index cfc8892f5b..a9881a2c81 100644
--- a/packages/web-visualization/src/chart/Path.tsx
+++ b/packages/web-visualization/src/chart/Path.tsx
@@ -3,11 +3,13 @@ import type { SVGProps } from 'react';
import type { Rect, SharedProps } from '@coinbase/cds-common/types';
import { m as motion, type Transition } from 'framer-motion';
-import { usePathTransition } from './utils/transition';
+import { defaultPathEnterTransition } from './utils/path';
+import { defaultTransition, getTransition, usePathTransition } from './utils/transition';
import { useCartesianChartContext } from './ChartProvider';
/**
* Duration in seconds for path enter transition.
+ * @deprecated Use `transitions.enter` on the Path component instead.
*/
export const pathEnterTransitionDuration = 0.5;
@@ -16,6 +18,20 @@ export type PathBaseProps = SharedProps & {
* Whether to animate this path. Overrides the animate prop on the Chart component.
*/
animate?: boolean;
+ /**
+ * Initial path for enter animation.
+ * When provided, the first animation will go from initialPath to d.
+ * If not provided, defaults to d (no path enter animation).
+ */
+ initialPath?: string;
+ /**
+ * Fill color for the path.
+ */
+ fill?: string;
+ /**
+ * Opacity for the path fill.
+ */
+ fillOpacity?: number;
};
export type PathProps = PathBaseProps &
@@ -34,6 +50,40 @@ export type PathProps = PathBaseProps &
| 'onDragEndCapture'
| 'onDragStartCapture'
> & {
+ /**
+ * Transition configuration for enter and update animations.
+ * @note Disable an animation by passing in null.
+ *
+ * @default transitions = {{
+ * enter: { type: 'tween', duration: 0.5 },
+ * update: { type: 'spring', stiffness: 900, damping: 120, mass: 4 }
+ * }}
+ *
+ * @example
+ * // Custom enter and update transitions
+ * transitions={{ enter: { type: 'tween', duration: 0.3 }, update: { type: 'spring', damping: 20 } }}
+ *
+ * @example
+ * // Disable enter animation
+ * transitions={{ enter: null }}
+ */
+ transitions?: {
+ /**
+ * Transition for the initial enter/reveal animation.
+ * Set to `null` to disable.
+ */
+ enter?: Transition | null;
+ /**
+ * Transition for subsequent data update animations.
+ * Set to `null` to disable.
+ */
+ update?: Transition | null;
+ };
+ /**
+ * Transition for updates.
+ * @deprecated Use `transitions.update` instead.
+ */
+ transition?: Transition;
/**
* Offset added to the clip rect boundaries.
*/
@@ -44,36 +94,52 @@ export type PathProps = PathBaseProps &
* @default drawingArea of chart + clipOffset
*/
clipRect?: Rect | null;
- /**
- * Transition configuration for path.
- *
- * @example
- * // Timing based animation
- * transition={{ type: 'tween', duration: 0.2, ease: 'easeOut' }}
- *
- * @example
- * // Spring animation
- * transition={{ type: 'spring', damping: 20, stiffness: 300 }}
- */
- transition?: Transition;
};
-const AnimatedPath = memo>(({ d = '', transition, ...pathProps }) => {
+const AnimatedPath = memo<
+ Omit & {
+ transitions?: { enter?: Transition; update?: Transition };
+ }
+>(({ d = '', initialPath, transitions, ...pathProps }) => {
const interpolatedPath = usePathTransition({
currentPath: d,
- transition,
+ initialPath,
+ transitions,
});
return ;
});
export const Path = memo(
- ({ animate: animateProp, clipRect, clipOffset = 0, d = '', transition, ...pathProps }) => {
+ ({
+ animate: animateProp,
+ clipRect,
+ clipOffset = 0,
+ d = '',
+ transitions,
+ transition,
+ ...pathProps
+ }) => {
const clipPathId = useId();
const context = useCartesianChartContext();
const rect = clipRect !== undefined ? clipRect : context.drawingArea;
const animate = animateProp ?? context.animate;
+ const enterTransition = useMemo(
+ () => getTransition(transitions?.enter, animate, defaultPathEnterTransition),
+ [animate, transitions?.enter],
+ );
+
+ const updateTransition = useMemo(
+ () =>
+ getTransition(
+ transitions?.update !== undefined ? transitions.update : transition,
+ animate,
+ defaultTransition,
+ ),
+ [animate, transitions?.update, transition],
+ );
+
// The clip offset provides extra padding to prevent path from being cut off
// Area charts typically use offset=0 for exact clipping, while lines use offset=2 for breathing room
const totalOffset = clipOffset * 2; // Applied on both sides
@@ -84,13 +150,10 @@ export const Path = memo(
hidden: { width: 0 },
visible: {
width: rect.width + totalOffset,
- transition: {
- type: 'timing',
- duration: pathEnterTransitionDuration,
- },
+ transition: enterTransition,
},
};
- }, [rect, totalOffset]);
+ }, [rect, totalOffset, enterTransition]);
const clipPath = useMemo(
() => (rect !== null ? `url(#${clipPathId})` : undefined),
@@ -102,31 +165,23 @@ export const Path = memo(
{rect !== null && (
- {!animate ? (
-
- ) : (
-
- )}
+
)}
- {!animate ? (
-
- ) : (
-
- )}
+
>
);
},
diff --git a/packages/web-visualization/src/chart/__stories__/ChartTransitions.stories.tsx b/packages/web-visualization/src/chart/__stories__/ChartTransitions.stories.tsx
new file mode 100644
index 0000000000..f2d786742c
--- /dev/null
+++ b/packages/web-visualization/src/chart/__stories__/ChartTransitions.stories.tsx
@@ -0,0 +1,410 @@
+import {
+ memo,
+ type PropsWithChildren,
+ type RefObject,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
+import { Button } from '@coinbase/cds-web/buttons';
+import { Box, VStack } from '@coinbase/cds-web/layout';
+import { Text } from '@coinbase/cds-web/typography';
+
+import { Area } from '../area/Area';
+import type { BarProps } from '../bar/Bar';
+import { BarChart } from '../bar/BarChart';
+import { CartesianChart } from '../CartesianChart';
+import { Line, type LineProps } from '../line/Line';
+import type { PathProps } from '../Path';
+import type { PointBaseProps, PointProps } from '../point';
+import { Scrubber, type ScrubberProps, type ScrubberRef } from '../scrubber';
+
+export default {
+ title: 'Components/Chart/CartesianChart',
+ component: CartesianChart,
+ parameters: {
+ percy: { skip: true },
+ },
+};
+
+const dataCount = 15;
+const updateInterval = 2500;
+
+function generateNextValue(previousValue: number) {
+ const step = Math.random() * 30 - 15;
+ return Math.max(0, Math.min(100, previousValue + step));
+}
+
+function generateInitialData() {
+ const data = [50];
+ for (let i = 1; i < dataCount; i++) {
+ data.push(generateNextValue(data[i - 1]));
+ }
+ return data;
+}
+
+const enterOnly: PathProps['transitions'] = {
+ update: null,
+};
+const updateOnly: PathProps['transitions'] = {
+ enter: null,
+};
+const bothDisabled: PathProps['transitions'] = { enter: null, update: null };
+const customEnterUpdate: PathProps['transitions'] = {
+ enter: { type: 'tween', duration: 1.5 },
+ update: { type: 'spring', stiffness: 400, damping: 30 },
+};
+const customEnterUpdateBeacon: PathProps['transitions'] = {
+ enter: { type: 'tween', duration: 0.5, delay: 1.0 },
+ update: { type: 'spring', stiffness: 400, damping: 30 },
+};
+const slowSpringBoth: PathProps['transitions'] = {
+ enter: { type: 'spring', stiffness: 100, damping: 10 },
+ update: { type: 'spring', stiffness: 100, damping: 10 },
+};
+const staggeredBoth: BarProps['transitions'] = {
+ enter: { type: 'tween', duration: 0.75, staggerDelay: 0.25 },
+ update: { type: 'spring', stiffness: 300, damping: 20, staggerDelay: 0.15 },
+};
+
+const TransitionLineChart = memo<{
+ data: number[];
+ transitions: PathProps['transitions'];
+ scrubberTransitions?: PathProps['transitions'];
+ animate?: boolean;
+ idlePulse?: boolean;
+ scrubberRef?: RefObject;
+ enableScrubbing?: boolean;
+ points?: LineProps['points'];
+}>(
+ ({
+ data,
+ transitions,
+ scrubberTransitions,
+ animate: animateProp,
+ idlePulse,
+ scrubberRef,
+ enableScrubbing = true,
+ points,
+ }) => (
+
+
+ {enableScrubbing && (
+ }
+ hideOverlay
+ idlePulse={idlePulse}
+ transitions={scrubberTransitions ?? transitions}
+ />
+ )}
+
+ ),
+);
+
+const TransitionAreaChart = memo<{
+ data: number[];
+ transitions: PathProps['transitions'];
+ idlePulse?: boolean;
+ scrubberRef?: RefObject;
+}>(({ data, transitions, idlePulse, scrubberRef }) => (
+
+
+
+ }
+ hideOverlay
+ idlePulse={idlePulse}
+ transitions={transitions}
+ />
+
+));
+
+const MultiLineChart = memo<{
+ data1: number[];
+ data2: number[];
+ transitions: PathProps['transitions'];
+}>(({ data1, data2, transitions }) => (
+
+
+
+
+
+));
+
+function LineExample({
+ transitions,
+ scrubberTransitions,
+ pointTransitions,
+ animate,
+ idlePulse,
+ resettable = true,
+ imperative = false,
+ points,
+}: {
+ transitions: PathProps['transitions'];
+ scrubberTransitions?: ScrubberProps['transitions'];
+ pointTransitions?: PointProps['transitions'];
+ animate?: boolean;
+ idlePulse?: boolean;
+ resettable?: boolean;
+ imperative?: boolean;
+ points?: boolean;
+}) {
+ const scrubberRef = useRef(null);
+ const [data, setData] = useState(generateInitialData);
+ const [resetKey, setResetKey] = useState(0);
+ const handleReset = useCallback(() => setResetKey((k) => k + 1), []);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ if (imperative) scrubberRef.current?.pulse();
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, [imperative]);
+
+ const pointFunction: LineProps['points'] = (props: PointBaseProps) => ({
+ ...props,
+ transitions: pointTransitions,
+ });
+
+ const pointProps: LineProps['points'] = points ? pointFunction : false;
+
+ return (
+
+
+ {resettable && (
+
+
+
+ )}
+
+ );
+}
+
+function AreaExample({
+ transitions,
+ idlePulse,
+ resettable = true,
+ imperative = false,
+}: {
+ transitions: PathProps['transitions'];
+ idlePulse?: boolean;
+ resettable?: boolean;
+ imperative?: boolean;
+}) {
+ const scrubberRef = useRef(null);
+ const [data, setData] = useState(generateInitialData);
+ const [resetKey, setResetKey] = useState(0);
+ const handleReset = useCallback(() => setResetKey((k) => k + 1), []);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ if (imperative) scrubberRef.current?.pulse();
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, [imperative]);
+
+ return (
+
+
+ {resettable && (
+
+
+
+ )}
+
+ );
+}
+
+const barCategories = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
+
+function generateBarData() {
+ return barCategories.map(() => Math.round(Math.random() * 80 + 10));
+}
+
+const barChartProps = {
+ showXAxis: true,
+ enableScrubbing: true,
+ height: 250,
+ xAxis: { data: barCategories },
+ yAxis: { domain: { min: 0, max: 100 } },
+} as const;
+
+const TransitionBarChart = memo<{
+ data: number[];
+ transitions: PathProps['transitions'];
+}>(({ data, transitions }) => (
+
+
+
+));
+
+function BarExample({
+ transitions,
+ resettable = true,
+}: {
+ transitions: PathProps['transitions'];
+ resettable?: boolean;
+}) {
+ const [data, setData] = useState(generateBarData);
+ const [resetKey, setResetKey] = useState(0);
+ const handleReset = useCallback(() => setResetKey((k) => k + 1), []);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData(generateBarData());
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+ {resettable && (
+
+
+
+ )}
+
+ );
+}
+
+function MultiLineExample({ transitions }: { transitions: PathProps['transitions'] }) {
+ const [data1, setData1] = useState(generateInitialData);
+ const [data2, setData2] = useState(generateInitialData);
+ const [resetKey, setResetKey] = useState(0);
+ const handleReset = useCallback(() => setResetKey((k) => k + 1), []);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setData1((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ setData2((current) => {
+ const last = current[current.length - 1];
+ return [...current.slice(1), generateNextValue(last)];
+ });
+ }, updateInterval);
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+const Example = ({
+ category,
+ title,
+ children,
+}: PropsWithChildren<{ category: string; title: string }>) => (
+
+
+
+ {category}
+
+ {title}
+
+ {children}
+
+);
+
+export const Transitions = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/web-visualization/src/chart/area/Area.tsx b/packages/web-visualization/src/chart/area/Area.tsx
index 74e9c52221..ad1320a616 100644
--- a/packages/web-visualization/src/chart/area/Area.tsx
+++ b/packages/web-visualization/src/chart/area/Area.tsx
@@ -1,8 +1,8 @@
import React, { memo, useMemo } from 'react';
import type { SVGProps } from 'react';
-import type { Transition } from 'framer-motion';
import { useCartesianChartContext } from '../ChartProvider';
+import type { PathBaseProps, PathProps } from '../Path';
import { type ChartPathCurveType, getAreaPath, type GradientDefinition } from '../utils';
import { DottedArea } from './DottedArea';
@@ -37,13 +37,13 @@ export type AreaBaseProps = {
* The color of the area.
* @default color of the series or 'var(--color-fgPrimary)'
*/
- fill?: string;
+ fill?: PathBaseProps['fill'];
/**
* Opacity of the area
* @note when combined with gradient, both will be applied
* @default 1
*/
- fillOpacity?: number;
+ fillOpacity?: PathBaseProps['fillOpacity'];
/**
* Baseline value for the gradient.
* When set, overrides the default baseline.
@@ -58,19 +58,14 @@ export type AreaBaseProps = {
* Whether to animate the area.
* Overrides the animate value from the chart context.
*/
- animate?: boolean;
+ animate?: PathBaseProps['animate'];
};
-export type AreaProps = AreaBaseProps & {
- /**
- * Transition configuration for path animations.
- */
- transition?: Transition;
-};
+export type AreaProps = AreaBaseProps & Pick;
export type AreaComponentProps = Pick<
AreaProps,
- 'fill' | 'fillOpacity' | 'baseline' | 'gradient' | 'animate' | 'transition'
+ 'fill' | 'fillOpacity' | 'baseline' | 'gradient' | 'animate' | 'transitions' | 'transition'
> & {
/**
* Path of the area
@@ -96,6 +91,7 @@ export const Area = memo(
baseline,
connectNulls,
gradient: gradientProp,
+ transitions,
transition,
animate,
}) => {
@@ -160,6 +156,7 @@ export const Area = memo(
fillOpacity={fillOpacity}
gradient={gradient}
transition={transition}
+ transitions={transitions}
yAxisId={matchedSeries?.yAxisId}
/>
);
diff --git a/packages/web-visualization/src/chart/area/AreaChart.tsx b/packages/web-visualization/src/chart/area/AreaChart.tsx
index 7d3f9c3337..c99f374e49 100644
--- a/packages/web-visualization/src/chart/area/AreaChart.tsx
+++ b/packages/web-visualization/src/chart/area/AreaChart.tsx
@@ -21,7 +21,14 @@ export type AreaSeries = Series &
Partial<
Pick<
AreaProps,
- 'AreaComponent' | 'curve' | 'fillOpacity' | 'type' | 'fill' | 'connectNulls' | 'transition'
+ | 'AreaComponent'
+ | 'curve'
+ | 'fillOpacity'
+ | 'type'
+ | 'fill'
+ | 'connectNulls'
+ | 'transitions'
+ | 'transition'
>
> &
Partial> & {
@@ -36,7 +43,13 @@ export type AreaSeries = Series &
export type AreaChartBaseProps = Omit &
Pick<
AreaProps,
- 'AreaComponent' | 'curve' | 'fillOpacity' | 'type' | 'connectNulls' | 'transition'
+ | 'AreaComponent'
+ | 'curve'
+ | 'fillOpacity'
+ | 'type'
+ | 'connectNulls'
+ | 'transitions'
+ | 'transition'
> &
Pick & {
/**
@@ -99,6 +112,7 @@ export const AreaChart = memo(
fillOpacity,
type,
connectNulls,
+ transitions,
transition,
LineComponent,
strokeWidth,
@@ -222,6 +236,7 @@ export const AreaChart = memo(
fillOpacity={fillOpacity}
seriesId={id}
transition={seriesTransition ?? transition}
+ transitions={transitions}
type={type}
{...areaPropsFromSeries}
/>
@@ -253,6 +268,7 @@ export const AreaChart = memo(
seriesId={id}
strokeWidth={strokeWidth}
transition={seriesTransition ?? transition}
+ transitions={transitions}
type={seriesLineType ?? lineType}
{...otherPropsFromSeries}
/>
diff --git a/packages/web-visualization/src/chart/area/DottedArea.tsx b/packages/web-visualization/src/chart/area/DottedArea.tsx
index df83fa5775..95a64c116d 100644
--- a/packages/web-visualization/src/chart/area/DottedArea.tsx
+++ b/packages/web-visualization/src/chart/area/DottedArea.tsx
@@ -61,6 +61,7 @@ export const DottedArea = memo(
yAxisId,
gradient: gradientProp,
animate,
+ transitions,
transition,
...pathProps
}) => {
@@ -94,14 +95,20 @@ export const DottedArea = memo(
-
+
{gradient && (
)}
@@ -112,6 +119,7 @@ export const DottedArea = memo(
fill={gradient ? `url(#${gradientId})` : fill}
mask={`url(#${maskId})`}
transition={transition}
+ transitions={transitions}
{...pathProps}
/>
diff --git a/packages/web-visualization/src/chart/area/GradientArea.tsx b/packages/web-visualization/src/chart/area/GradientArea.tsx
index f55f0c5ad7..a5afa3aacc 100644
--- a/packages/web-visualization/src/chart/area/GradientArea.tsx
+++ b/packages/web-visualization/src/chart/area/GradientArea.tsx
@@ -54,6 +54,7 @@ export const GradientArea = memo(
yAxisId,
gradient: gradientProp,
animate,
+ transitions,
transition,
...pathProps
}) => {
@@ -78,7 +79,7 @@ export const GradientArea = memo(
animate={animate}
gradient={gradient}
id={patternId}
- transition={transition}
+ transition={transitions?.update ?? transition}
yAxisId={yAxisId}
/>
@@ -89,6 +90,7 @@ export const GradientArea = memo(
fill={gradient ? `url(#${patternId})` : fill}
fillOpacity={fillOpacity}
transition={transition}
+ transitions={transitions}
{...pathProps}
/>
>
diff --git a/packages/web-visualization/src/chart/area/SolidArea.tsx b/packages/web-visualization/src/chart/area/SolidArea.tsx
index 8df896e2b7..3487bb9773 100644
--- a/packages/web-visualization/src/chart/area/SolidArea.tsx
+++ b/packages/web-visualization/src/chart/area/SolidArea.tsx
@@ -32,6 +32,7 @@ export const SolidArea = memo(
fillOpacity = 1,
yAxisId,
animate,
+ transitions,
transition,
gradient,
...pathProps
@@ -46,7 +47,7 @@ export const SolidArea = memo(
animate={animate}
gradient={gradient}
id={patternId}
- transition={transition}
+ transition={transitions?.update ?? transition}
yAxisId={yAxisId}
/>
@@ -57,6 +58,7 @@ export const SolidArea = memo(
fill={gradient ? `url(#${patternId})` : fill}
fillOpacity={fillOpacity}
transition={transition}
+ transitions={transitions}
{...pathProps}
/>
>
diff --git a/packages/web-visualization/src/chart/area/__stories__/AreaChart.stories.tsx b/packages/web-visualization/src/chart/area/__stories__/AreaChart.stories.tsx
index 7c100cb40e..8835eab92e 100644
--- a/packages/web-visualization/src/chart/area/__stories__/AreaChart.stories.tsx
+++ b/packages/web-visualization/src/chart/area/__stories__/AreaChart.stories.tsx
@@ -1,9 +1,10 @@
import { VStack } from '@coinbase/cds-web/layout';
import { Text } from '@coinbase/cds-web/typography';
-import { DottedLine } from '../../line';
+import { CartesianChart } from '../../CartesianChart';
+import { DottedLine, Line } from '../../line';
import { Scrubber } from '../../scrubber/Scrubber';
-import { AreaChart } from '..';
+import { Area, AreaChart } from '..';
export default {
title: 'Components/Chart/AreaChart',
diff --git a/packages/web-visualization/src/chart/bar/Bar.tsx b/packages/web-visualization/src/chart/bar/Bar.tsx
index dc6e5d8738..953a78e196 100644
--- a/packages/web-visualization/src/chart/bar/Bar.tsx
+++ b/packages/web-visualization/src/chart/bar/Bar.tsx
@@ -2,7 +2,7 @@ import React, { memo, useMemo } from 'react';
import type { SVGProps } from 'react';
import type { Transition } from 'framer-motion';
-import { getBarPath } from '../utils';
+import { type BarTransition, getBarPath } from '../utils';
import { DefaultBar } from './';
@@ -77,7 +77,37 @@ export type BarBaseProps = {
export type BarProps = BarBaseProps & {
/**
- * Transition configuration for animation.
+ * Transition configuration for enter and update animations.
+ * @note Disable an animation by passing in null.
+ *
+ * @default transitions = {{
+ * enter: { type: 'spring', stiffness: 900, damping: 120, mass: 4, staggerDelay: 0.25 },
+ * update: { type: 'spring', stiffness: 900, damping: 120, mass: 4 }
+ * }}
+ *
+ * @example
+ * // Custom staggered enter and spring update
+ * transitions={{ enter: { type: 'tween', duration: 0.5, staggerDelay: 0.3 }, update: { type: 'spring', damping: 20 } }}
+ *
+ * @example
+ * // Disable enter animation
+ * transitions={{ enter: null }}
+ */
+ transitions?: {
+ /**
+ * Transition for the initial enter/reveal animation.
+ * Set to `null` to disable.
+ */
+ enter?: BarTransition | null;
+ /**
+ * Transition for subsequent data update animations.
+ * Set to `null` to disable.
+ */
+ update?: BarTransition | null;
+ };
+ /**
+ * Transition for updates.
+ * @deprecated Use `transitions.update` instead.
*/
transition?: Transition;
};
@@ -121,6 +151,7 @@ export const Bar = memo(
borderRadius = 4,
roundTop = true,
roundBottom = true,
+ transitions,
transition,
}) => {
const barPath = useMemo(() => {
@@ -149,6 +180,7 @@ export const Bar = memo(
stroke={stroke}
strokeWidth={strokeWidth}
transition={transition}
+ transitions={transitions}
width={width}
x={x}
y={y}
diff --git a/packages/web-visualization/src/chart/bar/BarChart.tsx b/packages/web-visualization/src/chart/bar/BarChart.tsx
index 6aa15d8561..62cb31f05b 100644
--- a/packages/web-visualization/src/chart/bar/BarChart.tsx
+++ b/packages/web-visualization/src/chart/bar/BarChart.tsx
@@ -25,6 +25,7 @@ export type BarChartBaseProps = Omit & {
/**
@@ -89,6 +90,7 @@ export const BarChart = memo(
stackGap,
barMinSize,
stackMinSize,
+ transitions,
transition,
...chartProps
},
@@ -182,6 +184,7 @@ export const BarChart = memo(
stroke={stroke}
strokeWidth={strokeWidth}
transition={transition}
+ transitions={transitions}
/>
{children}
diff --git a/packages/web-visualization/src/chart/bar/BarPlot.tsx b/packages/web-visualization/src/chart/bar/BarPlot.tsx
index 08e1c21a6f..f8152c560f 100644
--- a/packages/web-visualization/src/chart/bar/BarPlot.tsx
+++ b/packages/web-visualization/src/chart/bar/BarPlot.tsx
@@ -28,7 +28,8 @@ export type BarPlotBaseProps = Pick<
seriesIds?: string[];
};
-export type BarPlotProps = BarPlotBaseProps & Pick;
+export type BarPlotProps = BarPlotBaseProps &
+ Pick;
/**
* BarPlot component that handles multiple series with proper stacking coordination.
@@ -50,6 +51,7 @@ export const BarPlot = memo(
stackGap,
barMinSize,
stackMinSize,
+ transitions,
transition,
}) => {
const { series: allSeries, drawingArea } = useCartesianChartContext();
@@ -130,6 +132,7 @@ export const BarPlot = memo(
strokeWidth={defaultStrokeWidth}
totalStacks={stackGroups.length}
transition={transition}
+ transitions={transitions}
yAxisId={group.yAxisId}
/>
))}
diff --git a/packages/web-visualization/src/chart/bar/BarStack.tsx b/packages/web-visualization/src/chart/bar/BarStack.tsx
index 64453e9803..128219b6ff 100644
--- a/packages/web-visualization/src/chart/bar/BarStack.tsx
+++ b/packages/web-visualization/src/chart/bar/BarStack.tsx
@@ -1,12 +1,11 @@
import React, { memo, useMemo } from 'react';
import type { Rect } from '@coinbase/cds-common';
-import type { Transition } from 'framer-motion';
import { useCartesianChartContext } from '../ChartProvider';
import type { ChartScaleFunction, Series } from '../utils';
import { evaluateGradientAtValue, getGradientConfig } from '../utils/gradient';
-import { Bar, type BarComponent, type BarProps } from './Bar';
+import { Bar, type BarBaseProps, type BarComponent, type BarProps } from './Bar';
import { DefaultBarStack } from './DefaultBarStack';
const EPSILON = 1e-4;
@@ -22,7 +21,7 @@ export type BarSeries = Series & {
};
export type BarStackBaseProps = Pick<
- BarProps,
+ BarBaseProps,
'BarComponent' | 'fillOpacity' | 'stroke' | 'strokeWidth' | 'borderRadius'
> & {
/**
@@ -78,16 +77,11 @@ export type BarStackBaseProps = Pick<
stackMinSize?: number;
};
-export type BarStackProps = BarStackBaseProps & {
- /**
- * Transition configuration for animation.
- */
- transition?: Transition;
-};
+export type BarStackProps = BarStackBaseProps & Pick;
export type BarStackComponentProps = Pick<
BarStackProps,
- 'x' | 'width' | 'categoryIndex' | 'borderRadius' | 'transition'
+ 'x' | 'width' | 'categoryIndex' | 'borderRadius' | 'transitions' | 'transition'
> & {
/**
* The y position of the stack.
@@ -139,6 +133,7 @@ export const BarStack = memo(
barMinSize,
stackMinSize,
roundBaseline,
+ transitions,
transition,
}) => {
const { getSeriesData, getXAxis, getXScale, getSeries } = useCartesianChartContext();
@@ -701,6 +696,7 @@ export const BarStack = memo(
stroke={bar.stroke ?? defaultStroke}
strokeWidth={bar.strokeWidth ?? defaultStrokeWidth}
transition={transition}
+ transitions={transitions}
width={bar.width}
x={bar.x}
y={bar.y}
@@ -720,6 +716,7 @@ export const BarStack = memo(
roundBottom={stackRoundBottom}
roundTop={stackRoundTop}
transition={transition}
+ transitions={transitions}
width={stackRect.width}
x={stackRect.x}
y={stackRect.y}
diff --git a/packages/web-visualization/src/chart/bar/BarStackGroup.tsx b/packages/web-visualization/src/chart/bar/BarStackGroup.tsx
index 31ea2e64ab..40d6dd27c1 100644
--- a/packages/web-visualization/src/chart/bar/BarStackGroup.tsx
+++ b/packages/web-visualization/src/chart/bar/BarStackGroup.tsx
@@ -19,6 +19,7 @@ export type BarStackGroupProps = Pick<
| 'barMinSize'
| 'stackMinSize'
| 'BarStackComponent'
+ | 'transitions'
| 'transition'
> &
Pick & {
@@ -90,7 +91,7 @@ export const BarStackGroup = memo(
if (xScale && !isCategoricalScale(xScale)) {
throw new Error(
- 'BarStackGroup requires a band scale for x-axis. See https://cds.coinbase.com/components/graphs/XAxis/#scale-type',
+ 'BarStackGroup requires a band scale for x-axis. See https://cds.coinbase.com/components/charts/XAxis/#scale-type',
);
}
diff --git a/packages/web-visualization/src/chart/bar/DefaultBar.tsx b/packages/web-visualization/src/chart/bar/DefaultBar.tsx
index 4382b81b6b..d72d3156ce 100644
--- a/packages/web-visualization/src/chart/bar/DefaultBar.tsx
+++ b/packages/web-visualization/src/chart/bar/DefaultBar.tsx
@@ -1,8 +1,14 @@
import React, { memo, useMemo } from 'react';
-import { m as motion } from 'framer-motion';
import { useCartesianChartContext } from '../ChartProvider';
-import { getBarPath } from '../utils';
+import { Path } from '../Path';
+import {
+ defaultBarEnterTransition,
+ defaultTransition,
+ getBarPath,
+ getTransition,
+ withStaggerDelayTransition,
+} from '../utils';
import type { BarComponentProps } from './Bar';
@@ -34,32 +40,66 @@ export const DefaultBar = memo(
dataX,
dataY,
seriesId,
+ transitions,
transition,
...props
}) => {
- const { animate } = useCartesianChartContext();
+ const { animate, drawingArea } = useCartesianChartContext();
+
+ const normalizedX = useMemo(
+ () => (drawingArea.width > 0 ? (x - drawingArea.x) / drawingArea.width : 0),
+ [x, drawingArea.x, drawingArea.width],
+ );
+
+ const enterTransition = useMemo(
+ () =>
+ withStaggerDelayTransition(
+ getTransition(transitions?.enter, animate, defaultBarEnterTransition),
+ normalizedX,
+ ),
+ [transitions?.enter, animate, normalizedX],
+ );
+ const updateTransition = useMemo(
+ () =>
+ withStaggerDelayTransition(
+ getTransition(
+ transitions?.update !== undefined ? transitions.update : transition,
+ animate,
+ defaultTransition,
+ ),
+ normalizedX,
+ ),
+ [transitions?.update, transition, animate, normalizedX],
+ );
const initialPath = useMemo(() => {
- if (!animate) return undefined;
- // Need a minimum height to allow for animation
const minHeight = 1;
const initialY = (originY ?? 0) - minHeight;
- return getBarPath(x, initialY, width, minHeight, borderRadius, !!roundTop, !!roundBottom);
- }, [animate, x, originY, width, borderRadius, roundTop, roundBottom]);
-
- if (animate && initialPath) {
- return (
-
+ return getBarPath(
+ x,
+ initialY,
+ width,
+ minHeight,
+ borderRadius ?? 0,
+ !!roundTop,
+ !!roundBottom,
);
- }
+ }, [x, originY, width, borderRadius, roundTop, roundBottom]);
- return ;
+ return (
+
+ );
},
);
diff --git a/packages/web-visualization/src/chart/bar/DefaultBarStack.tsx b/packages/web-visualization/src/chart/bar/DefaultBarStack.tsx
index 932a3c5b13..b5547d74ca 100644
--- a/packages/web-visualization/src/chart/bar/DefaultBarStack.tsx
+++ b/packages/web-visualization/src/chart/bar/DefaultBarStack.tsx
@@ -2,7 +2,14 @@ import { memo, useId, useMemo } from 'react';
import { m as motion } from 'framer-motion';
import { useCartesianChartContext } from '../ChartProvider';
-import { getBarPath } from '../utils';
+import {
+ defaultBarEnterTransition,
+ defaultTransition,
+ getBarPath,
+ getTransition,
+ withStaggerDelayTransition,
+} from '../utils';
+import { usePathTransition } from '../utils/transition';
import type { BarStackComponentProps } from './BarStack';
@@ -33,33 +40,58 @@ export const DefaultBarStack = memo(
roundTop = true,
roundBottom = true,
yOrigin,
+ transitions,
transition,
}) => {
- const { animate } = useCartesianChartContext();
+ const { animate, drawingArea } = useCartesianChartContext();
const clipPathId = useId();
- const clipPathData = useMemo(() => {
+ const normalizedX = useMemo(
+ () => (drawingArea.width > 0 ? (x - drawingArea.x) / drawingArea.width : 0),
+ [x, drawingArea.x, drawingArea.width],
+ );
+
+ const enterTransition = useMemo(
+ () =>
+ withStaggerDelayTransition(
+ getTransition(transitions?.enter, animate, defaultBarEnterTransition),
+ normalizedX,
+ ),
+ [animate, transitions?.enter, normalizedX],
+ );
+ const updateTransition = useMemo(
+ () =>
+ withStaggerDelayTransition(
+ getTransition(
+ transitions?.update !== undefined ? transitions.update : transition,
+ animate,
+ defaultTransition,
+ ),
+ normalizedX,
+ ),
+ [animate, transitions?.update, transition, normalizedX],
+ );
+
+ const targetPath = useMemo(() => {
return getBarPath(x, y, width, height, borderRadius, roundTop, roundBottom);
}, [x, y, width, height, borderRadius, roundTop, roundBottom]);
- const initialClipPathData = useMemo(() => {
- if (!animate) return undefined;
- return getBarPath(x, yOrigin ?? y + height, width, 1, borderRadius, roundTop, roundBottom);
- }, [animate, x, yOrigin, y, height, width, borderRadius, roundTop, roundBottom]);
+ const initialPath = useMemo(() => {
+ const baselineY = yOrigin ?? y + height;
+ return getBarPath(x, baselineY, width, 1, borderRadius, roundTop, roundBottom);
+ }, [x, yOrigin, y, height, width, borderRadius, roundTop, roundBottom]);
+
+ const animatedClipPath = usePathTransition({
+ currentPath: targetPath,
+ initialPath,
+ transitions: { enter: enterTransition, update: updateTransition },
+ });
return (
<>
- {animate ? (
-
- ) : (
-
- )}
+
diff --git a/packages/web-visualization/src/chart/bar/__stories__/BarChart.stories.tsx b/packages/web-visualization/src/chart/bar/__stories__/BarChart.stories.tsx
index 893e453cbc..6e94b84ebb 100644
--- a/packages/web-visualization/src/chart/bar/__stories__/BarChart.stories.tsx
+++ b/packages/web-visualization/src/chart/bar/__stories__/BarChart.stories.tsx
@@ -1,7 +1,9 @@
-import React, { memo, useId } from 'react';
+import React, { memo, useEffect, useId, useMemo, useState } from 'react';
+import { assets } from '@coinbase/cds-common/internal/data/assets';
import { candles as btcCandles } from '@coinbase/cds-common/internal/data/candles';
import { HStack, VStack } from '@coinbase/cds-web/layout';
import { Text } from '@coinbase/cds-web/typography';
+import { m as motion, type Transition } from 'framer-motion';
import { CartesianChart } from '../..';
import { XAxis, YAxis } from '../../axis';
@@ -223,6 +225,8 @@ const BandGridPositionExample = ({
);
const Candlesticks = () => {
+ const staggerDelay = 0.25;
+
const infoTextRef = React.useRef(null);
const selectedIndexRef = React.useRef(undefined);
const [timePeriod, setTimePeriod] = React.useState(tabs[0]);
@@ -264,9 +268,23 @@ const Candlesticks = () => {
const CandlestickBarComponent = memo(
({ x, y, width, height, originY, dataX, ...props }) => {
- const { getYScale } = useCartesianChartContext();
+ const { getYScale, drawingArea } = useCartesianChartContext();
const yScale = getYScale();
+ const normalizedX = useMemo(
+ () => (drawingArea.width > 0 ? (x - drawingArea.x) / drawingArea.width : 0),
+ [x, drawingArea.x, drawingArea.width],
+ );
+
+ const transition: Transition = useMemo(
+ () => ({
+ type: 'tween',
+ duration: 0.325,
+ delay: normalizedX * staggerDelay,
+ }),
+ [normalizedX],
+ );
+
const wickX = x + width / 2;
const timePeriodValue = stockData[dataX as number];
@@ -283,10 +301,14 @@ const Candlesticks = () => {
const bodyY = openY < closeY ? openY : closeY;
return (
-
+
-
+
);
},
);
@@ -361,7 +383,6 @@ const Candlesticks = () => {
showYAxis
BarComponent={CandlestickBarComponent}
BarStackComponent={({ children, ...props }) => {children}}
- animate={false}
aria-labelledby={infoTextId}
borderRadius={0}
height={400}
@@ -404,6 +425,103 @@ const Candlesticks = () => {
);
};
+type SunlightChartData = Array<{ label: string; value: number }>;
+
+const sunlightData: SunlightChartData = [
+ { label: 'Jan', value: 598 },
+ { label: 'Feb', value: 635 },
+ { label: 'Mar', value: 688 },
+ { label: 'Apr', value: 753 },
+ { label: 'May', value: 812 },
+ { label: 'Jun', value: 855 },
+ { label: 'Jul', value: 861 },
+ { label: 'Aug', value: 828 },
+ { label: 'Sep', value: 772 },
+ { label: 'Oct', value: 710 },
+ { label: 'Nov', value: 648 },
+ { label: 'Dec', value: 605 },
+];
+
+const dayLength = 1440;
+
+const MonthlySunlight = () => {
+ return (
+ value),
+ yAxisId: 'sunlight',
+ color: 'rgb(var(--yellow40))',
+ },
+ {
+ id: 'day',
+ data: sunlightData.map(() => dayLength),
+ yAxisId: 'day',
+ color: 'rgb(var(--blue100))',
+ },
+ ]}
+ xAxis={{
+ scaleType: 'band',
+ data: sunlightData.map(({ label }) => label),
+ }}
+ yAxis={[
+ {
+ id: 'day',
+ domain: { min: 0, max: dayLength },
+ domainLimit: 'strict',
+ },
+ {
+ id: 'sunlight',
+ domain: { min: 0, max: dayLength },
+ domainLimit: 'strict',
+ },
+ ]}
+ >
+
+
+
+
+
+ );
+};
+
+const PriceRange = () => {
+ const candles = btcCandles.slice(0, 180).reverse();
+ const data: [number, number][] = candles.map((candle) => [
+ parseFloat(candle.low),
+ parseFloat(candle.high),
+ ]);
+
+ const min = Math.min(...data.map(([low]) => low));
+ const max = Math.max(...data.map(([, high]) => high));
+
+ const tickFormatter = React.useCallback(
+ (value: number) =>
+ new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ notation: 'compact',
+ maximumFractionDigits: 0,
+ }).format(value),
+ [],
+ );
+
+ return (
+
+ );
+};
+
export const All = () => {
return (
@@ -777,6 +895,12 @@ export const All = () => {
+
+
+
+
+
+
);
};
diff --git a/packages/web-visualization/src/chart/line/DottedLine.tsx b/packages/web-visualization/src/chart/line/DottedLine.tsx
index 404720344c..93c1a8bfba 100644
--- a/packages/web-visualization/src/chart/line/DottedLine.tsx
+++ b/packages/web-visualization/src/chart/line/DottedLine.tsx
@@ -40,6 +40,7 @@ export const DottedLine = memo(
gradient,
yAxisId,
animate,
+ transitions,
transition,
d,
...props
@@ -54,7 +55,7 @@ export const DottedLine = memo(
animate={animate}
gradient={gradient}
id={gradientId}
- transition={transition}
+ transition={transitions?.update ?? transition}
yAxisId={yAxisId}
/>
@@ -71,6 +72,7 @@ export const DottedLine = memo(
strokeOpacity={strokeOpacity}
strokeWidth={strokeWidth}
transition={transition}
+ transitions={transitions}
vectorEffect={vectorEffect}
{...props}
/>
diff --git a/packages/web-visualization/src/chart/line/Line.tsx b/packages/web-visualization/src/chart/line/Line.tsx
index a58c5b835c..ebd43e1ae0 100644
--- a/packages/web-visualization/src/chart/line/Line.tsx
+++ b/packages/web-visualization/src/chart/line/Line.tsx
@@ -1,15 +1,12 @@
import React, { memo, useMemo } from 'react';
import type { SVGProps } from 'react';
import type { SharedProps } from '@coinbase/cds-common/types';
-import { m as motion, type Transition } from 'framer-motion';
import { Area, type AreaComponent } from '../area/Area';
import { useCartesianChartContext } from '../ChartProvider';
import type { PathProps } from '../Path';
import { Point, type PointBaseProps, type PointProps } from '../point';
import {
- accessoryFadeTransitionDelay,
- accessoryFadeTransitionDuration,
type ChartPathCurveType,
evaluateGradientAtValue,
getGradientConfig,
@@ -109,25 +106,22 @@ export type LineBaseProps = SharedProps & {
animate?: boolean;
};
-export type LineProps = LineBaseProps & {
- /**
- * Transition configuration for line animations.
- */
- transition?: Transition;
- /**
- * Handler for when a point is clicked.
- * Passed through to Point components rendered via points.
- */
- onPointClick?: PointProps['onClick'];
- /**
- * Custom style for the line.
- */
- style?: React.CSSProperties;
- /**
- * Custom className for the line.
- */
- className?: string;
-};
+export type LineProps = LineBaseProps &
+ Pick & {
+ /**
+ * Handler for when a point is clicked.
+ * Passed through to Point components rendered via points.
+ */
+ onPointClick?: PointProps['onClick'];
+ /**
+ * Custom style for the line.
+ */
+ style?: React.CSSProperties;
+ /**
+ * Custom className for the line.
+ */
+ className?: string;
+ };
export type LineComponentProps = Pick<
LineProps,
@@ -136,6 +130,7 @@ export type LineComponentProps = Pick<
| 'strokeWidth'
| 'gradient'
| 'animate'
+ | 'transitions'
| 'transition'
| 'style'
| 'className'
@@ -170,6 +165,7 @@ export const Line = memo(
opacity = 1,
points,
connectNulls,
+ transitions,
transition,
gradient: gradientProp,
...props
@@ -264,6 +260,7 @@ export const Line = memo(
gradient={gradient}
seriesId={seriesId}
transition={transition}
+ transitions={transitions}
type={areaType}
/>
)}
@@ -273,26 +270,12 @@ export const Line = memo(
stroke={stroke}
strokeOpacity={strokeOpacity ?? opacity}
transition={transition}
+ transitions={transitions}
yAxisId={matchedSeries?.yAxisId}
{...props}
/>
{points && (
-
+
{chartData.map((value: number | null, index: number) => {
if (value === null) return;
@@ -333,6 +316,7 @@ export const Line = memo(
key={`${seriesId}-${index}`}
onClick={onPointClick}
transition={transition}
+ transitions={transitions}
{...defaults}
/>
);
@@ -350,12 +334,13 @@ export const Line = memo(
key={`${seriesId}-${index}`}
onClick={pointConfig.onClick ?? onPointClick}
transition={transition}
+ transitions={transitions}
{...defaults}
{...pointConfig}
/>
);
})}
-
+
)}
>
);
diff --git a/packages/web-visualization/src/chart/line/LineChart.tsx b/packages/web-visualization/src/chart/line/LineChart.tsx
index c14c98df45..162e00e003 100644
--- a/packages/web-visualization/src/chart/line/LineChart.tsx
+++ b/packages/web-visualization/src/chart/line/LineChart.tsx
@@ -28,6 +28,7 @@ export type LineSeries = Series &
| 'opacity'
| 'points'
| 'connectNulls'
+ | 'transitions'
| 'transition'
| 'onPointClick'
>
@@ -46,6 +47,7 @@ export type LineChartBaseProps = Omit
diff --git a/packages/web-visualization/src/chart/line/SolidLine.tsx b/packages/web-visualization/src/chart/line/SolidLine.tsx
index 743312f12b..01d5b1108d 100644
--- a/packages/web-visualization/src/chart/line/SolidLine.tsx
+++ b/packages/web-visualization/src/chart/line/SolidLine.tsx
@@ -37,6 +37,7 @@ export const SolidLine = memo(
gradient,
yAxisId,
animate,
+ transitions,
transition,
d,
...props
@@ -51,7 +52,7 @@ export const SolidLine = memo(
animate={animate}
gradient={gradient}
id={gradientId}
- transition={transition}
+ transition={transitions?.update ?? transition}
yAxisId={yAxisId}
/>
@@ -67,6 +68,7 @@ export const SolidLine = memo(
strokeOpacity={strokeOpacity}
strokeWidth={strokeWidth}
transition={transition}
+ transitions={transitions}
{...props}
/>
>
diff --git a/packages/web-visualization/src/chart/line/__stories__/LineChart.stories.tsx b/packages/web-visualization/src/chart/line/__stories__/LineChart.stories.tsx
index 62dbd66905..d5a648b89e 100644
--- a/packages/web-visualization/src/chart/line/__stories__/LineChart.stories.tsx
+++ b/packages/web-visualization/src/chart/line/__stories__/LineChart.stories.tsx
@@ -22,7 +22,6 @@ import { Text } from '@coinbase/cds-web/typography';
import { m } from 'framer-motion';
import {
- type AxisBounds,
DefaultScrubberBeacon,
defaultTransition,
PeriodSelector,
@@ -1751,139 +1750,6 @@ export const All = () => {
);
};
-export const Transitions = () => {
- const dataCount = 20;
- const maxDataOffset = 15000;
- const minStepOffset = 2500;
- const maxStepOffset = 10000;
- const domainLimit = 20000;
- const updateInterval = 500;
-
- const myTransitionConfig = { type: 'spring', stiffness: 700, damping: 20 };
- const negativeColor = 'rgb(var(--gray15))';
- const positiveColor = 'var(--color-fgPositive)';
-
- function generateNextValue(previousValue: number) {
- const range = maxStepOffset - minStepOffset;
- const offset = Math.random() * range + minStepOffset;
-
- let direction;
- if (previousValue >= maxDataOffset) {
- direction = -1;
- } else if (previousValue <= -maxDataOffset) {
- direction = 1;
- } else {
- direction = Math.random() < 0.5 ? -1 : 1;
- }
-
- let newValue = previousValue + offset * direction;
- newValue = Math.max(-maxDataOffset, Math.min(maxDataOffset, newValue));
- return newValue;
- }
-
- function generateInitialData() {
- const data = [];
-
- let previousValue = Math.random() * 2 * maxDataOffset - maxDataOffset;
- data.push(previousValue);
-
- for (let i = 1; i < dataCount; i++) {
- const newValue = generateNextValue(previousValue);
- data.push(newValue);
- previousValue = newValue;
- }
-
- return data;
- }
-
- const MyGradient = memo((props: DottedAreaProps) => {
- const areaGradient = {
- stops: ({ min, max }: AxisBounds) => [
- { offset: min, color: negativeColor, opacity: 1 },
- { offset: 0, color: negativeColor, opacity: 0 },
- { offset: 0, color: positiveColor, opacity: 0 },
- { offset: max, color: positiveColor, opacity: 1 },
- ],
- };
-
- return ;
- });
-
- function CustomTransitionsChart() {
- const [data, setData] = useState(generateInitialData);
-
- useEffect(() => {
- const intervalId = setInterval(() => {
- setData((currentData) => {
- const lastValue = currentData[currentData.length - 1] ?? 0;
- const newValue = generateNextValue(lastValue);
-
- return [...currentData.slice(1), newValue];
- });
- }, updateInterval);
-
- return () => clearInterval(intervalId);
- }, []);
-
- const tickLabelFormatter = useCallback(
- (value: number) =>
- new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- maximumFractionDigits: 0,
- }).format(value),
- [],
- );
-
- const valueAtIndexFormatter = useCallback(
- (dataIndex: number) =>
- new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- }).format(data[dataIndex]),
- [data],
- );
-
- const lineGradient = {
- stops: [
- { offset: 0, color: negativeColor },
- { offset: 0, color: positiveColor },
- ],
- };
-
- return (
-
-
-
-
-
- );
- }
-
- return ;
-};
function DataCardWithLineChart() {
const exampleThumbnail = (
(
labelFont,
testID,
animate: animateProp,
+ transitions,
transition,
...svgProps
}) => {
@@ -255,6 +278,21 @@ export const Point = memo(
} = useCartesianChartContext();
const animate = animateProp ?? animationEnabled;
+ const enterTransition = useMemo(
+ () => getTransition(transitions?.enter, animate, defaultAccessoryEnterTransition),
+ [animate, transitions?.enter],
+ );
+
+ const updateTransition = useMemo(
+ () =>
+ getTransition(
+ transitions?.update !== undefined ? transitions.update : transition,
+ animate,
+ defaultTransition,
+ ),
+ [animate, transitions?.update, transition],
+ );
+
const xScale = getXScale();
const yScale = getYScale(yAxisId);
@@ -347,7 +385,6 @@ export const Point = memo(
cx={pixelCoordinate.x}
cy={pixelCoordinate.y}
fill={fill}
- initial={false}
onClick={
onClick
? (event: any) =>
@@ -361,7 +398,10 @@ export const Point = memo(
strokeWidth={strokeWidth}
style={mergedStyles}
tabIndex={onClick ? 0 : -1}
- transition={transition}
+ transition={{
+ cx: updateTransition,
+ cy: updateTransition,
+ }}
variants={variants}
whileHover={onClick ? 'hovered' : 'default'}
whileTap={onClick ? 'pressed' : 'default'}
@@ -385,7 +425,7 @@ export const Point = memo(
pixelCoordinate.x,
pixelCoordinate.y,
accessibilityLabel,
- transition,
+ updateTransition,
]);
if (!xScale || !yScale) {
@@ -394,28 +434,34 @@ export const Point = memo(
return (
-
- {innerPoint}
-
- {label && (
-
- {label}
-
- )}
+ {innerPoint}
+
+ {label && (
+
+ {label}
+
+ )}
+
);
},
diff --git a/packages/web-visualization/src/chart/scrubber/DefaultScrubberBeacon.tsx b/packages/web-visualization/src/chart/scrubber/DefaultScrubberBeacon.tsx
index aeeb9f89e4..649c3b5a74 100644
--- a/packages/web-visualization/src/chart/scrubber/DefaultScrubberBeacon.tsx
+++ b/packages/web-visualization/src/chart/scrubber/DefaultScrubberBeacon.tsx
@@ -1,4 +1,5 @@
import { forwardRef, memo, useImperativeHandle, useMemo } from 'react';
+import { usePreviousValue } from '@coinbase/cds-common/hooks/usePreviousValue';
import {
m as motion,
type Transition,
@@ -7,7 +8,7 @@ import {
} from 'framer-motion';
import { useCartesianChartContext } from '../ChartProvider';
-import { defaultTransition, projectPoint } from '../utils';
+import { defaultTransition, getTransition, instantTransition, projectPoint } from '../utils';
import type { ScrubberBeaconProps, ScrubberBeaconRef } from './Scrubber';
@@ -81,10 +82,14 @@ export const DefaultScrubberBeacon = memo(
[colorProp, targetSeries],
);
- const updateTransition = useMemo(
- () => transitions?.update ?? defaultTransition,
- [transitions?.update],
- );
+ const prevIsIdle = usePreviousValue(isIdle);
+ const isIdleTransition = prevIsIdle !== undefined && isIdle !== prevIsIdle;
+
+ const updateTransition = useMemo(() => {
+ if (isIdleTransition) return instantTransition;
+ if (!isIdle) return instantTransition;
+ return getTransition(transitions?.update, animate, defaultTransition);
+ }, [transitions?.update, isIdle, animate, isIdleTransition]);
const pulseTransition = useMemo(
() => transitions?.pulse ?? defaultPulseTransition,
[transitions?.pulse],
@@ -169,8 +174,17 @@ export const DefaultScrubberBeacon = memo(
/>
);
- const beaconCircle =
- isIdle && animate ? (
+ return (
+
+ {isIdle && (
+
+ {pulseCircle}
+
+ )}
- ) : (
-
- );
-
- return (
-
- {isIdle &&
- (animate ? (
-
- {pulseCircle}
-
- ) : (
-
- {pulseCircle}
-
- ))}
- {beaconCircle}
);
},
diff --git a/packages/web-visualization/src/chart/scrubber/DefaultScrubberBeaconLabel.tsx b/packages/web-visualization/src/chart/scrubber/DefaultScrubberBeaconLabel.tsx
index 3d0386eb91..4a5c5a2f95 100644
--- a/packages/web-visualization/src/chart/scrubber/DefaultScrubberBeaconLabel.tsx
+++ b/packages/web-visualization/src/chart/scrubber/DefaultScrubberBeaconLabel.tsx
@@ -1,4 +1,5 @@
import { memo } from 'react';
+import { m as motion } from 'framer-motion';
import { ChartText, type ChartTextProps } from '../text';
@@ -31,22 +32,27 @@ export const DefaultScrubberBeaconLabel = memo(
bottom: labelVerticalInset,
},
label,
+ transition,
+ y,
...chartTextProps
}) => {
return (
-
- {label}
-
+
+
+ {label}
+
+
);
},
);
diff --git a/packages/web-visualization/src/chart/scrubber/Scrubber.tsx b/packages/web-visualization/src/chart/scrubber/Scrubber.tsx
index c25607a9df..d9a366979d 100644
--- a/packages/web-visualization/src/chart/scrubber/Scrubber.tsx
+++ b/packages/web-visualization/src/chart/scrubber/Scrubber.tsx
@@ -10,11 +10,11 @@ import {
} from '../line';
import type { ChartTextChildren, ChartTextProps } from '../text';
import {
- accessoryFadeTransitionDelay,
- accessoryFadeTransitionDuration,
type ChartInset,
type ChartScaleFunction,
+ defaultAccessoryEnterTransition,
getPointOnScale,
+ getTransition,
type Series,
useScrubberContext,
} from '../utils';
@@ -41,7 +41,7 @@ export type ScrubberBeaconRef = {
pulse: () => void;
};
-export type ScrubberBeaconProps = SharedProps & {
+export type ScrubberBeaconBaseProps = {
/**
* Id of the series.
*/
@@ -74,27 +74,6 @@ export type ScrubberBeaconProps = SharedProps & {
* @default to ChartContext's animate value
*/
animate?: boolean;
- /**
- * Transition configuration for beacon animations.
- */
- transitions?: {
- /**
- * Transition used for beacon position updates.
- * @default defaultTransition
- */
- update?: Transition;
- /**
- * Transition used for the pulse animation.
- * @default { duration: 1.6, ease: 'easeInOut' }
- */
- pulse?: Transition;
- /**
- * Delay, in seconds between pulse transitions
- * when `idlePulse` is enabled.
- * @default 0.4
- */
- pulseRepeatDelay?: number;
- };
/**
* Opacity of the beacon.
* @default 1
@@ -105,16 +84,46 @@ export type ScrubberBeaconProps = SharedProps & {
* @default 'var(--color-bg)'
*/
stroke?: string;
- /**
- * Custom className for styling.
- */
- className?: string;
- /**
- * Custom inline styles.
- */
- style?: React.CSSProperties;
};
+export type ScrubberBeaconProps = SharedProps &
+ ScrubberBeaconBaseProps & {
+ /**
+ * Transition configuration for beacon animations.
+ */
+ transitions?: {
+ /**
+ * Transition for the initial enter/reveal animation.
+ * Set to `null` to disable.
+ */
+ enter?: Transition | null;
+ /**
+ * Transition for subsequent data update animations.
+ * Set to `null` to disable.
+ */
+ update?: Transition | null;
+ /**
+ * Transition used for the pulse animation.
+ * @default transition { duration: 1.6, ease: 'easeInOut' }
+ */
+ pulse?: Transition;
+ /**
+ * Delay, in seconds between pulse transitions
+ * when `idlePulse` is enabled.
+ * @default 0.4
+ */
+ pulseRepeatDelay?: number;
+ };
+ /**
+ * Custom className for styling.
+ */
+ className?: string;
+ /**
+ * Custom inline styles.
+ */
+ style?: React.CSSProperties;
+ };
+
export type ScrubberBeaconComponent = React.FC<
ScrubberBeaconProps & { ref?: React.Ref }
>;
@@ -132,6 +141,11 @@ export type ScrubberBeaconLabelProps = Pick &
* Id of the series.
*/
seriesId: Series['id'];
+ /**
+ * Transition configuration for position animations.
+ * When provided, the label component should animate its y position using this transition.
+ */
+ transition?: Transition;
};
export type ScrubberBeaconLabelComponent = React.FC;
@@ -196,7 +210,7 @@ export type ScrubberBaseProps = SharedProps &
labelFont?: ChartTextProps['font'];
/**
* Bounds inset for the scrubber line label to prevent cutoff at chart edges.
- * @default { top: 4, bottom: 20, left: 12, right: 12 } when labelElevated is true, otherwise none
+ * @default inset { top: 4, bottom: 20, left: 12, right: 12 } when labelElevated is true, otherwise none
*/
labelBoundsInset?: number | ChartInset;
/**
@@ -207,10 +221,6 @@ export type ScrubberBaseProps = SharedProps &
* Stroke color for the scrubber line.
*/
lineStroke?: ReferenceLineBaseProps['stroke'];
- /**
- * Transition configuration for the scrubber beacon.
- */
- beaconTransitions?: ScrubberBeaconProps['transitions'];
/**
* Stroke color of the scrubber beacon circle.
* @default 'var(--color-bg)'
@@ -219,6 +229,16 @@ export type ScrubberBaseProps = SharedProps &
};
export type ScrubberProps = ScrubberBaseProps & {
+ /**
+ * Transition configuration for the scrubber.
+ * Controls enter, update, and pulse animations for beacons and beacon labels.
+ */
+ transitions?: ScrubberBeaconProps['transitions'];
+ /**
+ * Transition configuration for the scrubber beacon.
+ * @deprecated Use `transitions` instead.
+ */
+ beaconTransitions?: ScrubberBeaconProps['transitions'];
/**
* Accessibility label for the scrubber. Can be a static string or a function that receives the current dataIndex.
* If not provided, label will be used if it resolves to a string.
@@ -279,6 +299,7 @@ export const Scrubber = memo(
testID,
idlePulse,
beaconTransitions,
+ transitions = beaconTransitions,
beaconStroke,
styles,
classNames,
@@ -353,6 +374,11 @@ export const Scrubber = memo(
[series, filteredSeriesIds],
);
+ const groupEnterTransition = useMemo(
+ () => getTransition(transitions?.enter, animate, defaultAccessoryEnterTransition),
+ [transitions?.enter, animate],
+ );
+
// Check if we have at least the default X scale
const defaultXScale = getXScale();
if (!defaultXScale) return null;
@@ -372,12 +398,8 @@ export const Scrubber = memo(
? {
animate: {
opacity: 1,
- transition: {
- duration: accessoryFadeTransitionDuration,
- delay: accessoryFadeTransitionDelay,
- },
+ transition: groupEnterTransition,
},
- exit: { opacity: 0, transition: { duration: accessoryFadeTransitionDuration } },
initial: { opacity: 0 },
}
: {})}
@@ -417,7 +439,7 @@ export const Scrubber = memo(
stroke={beaconStroke}
style={styles?.beacon}
testID={testID}
- transitions={beaconTransitions}
+ transitions={transitions}
/>
{!hideBeaconLabels && beaconLabels.length > 0 && (
)}
diff --git a/packages/web-visualization/src/chart/scrubber/ScrubberBeaconLabelGroup.tsx b/packages/web-visualization/src/chart/scrubber/ScrubberBeaconLabelGroup.tsx
index b751f17ccc..94561ef38d 100644
--- a/packages/web-visualization/src/chart/scrubber/ScrubberBeaconLabelGroup.tsx
+++ b/packages/web-visualization/src/chart/scrubber/ScrubberBeaconLabelGroup.tsx
@@ -1,9 +1,17 @@
import { memo, useCallback, useMemo, useState } from 'react';
+import { usePreviousValue } from '@coinbase/cds-common/hooks/usePreviousValue';
import type { SharedProps } from '@coinbase/cds-common/types';
+import type { Transition } from 'framer-motion';
import { useCartesianChartContext } from '../ChartProvider';
import type { ChartTextChildren, ChartTextProps } from '../text';
-import { getPointOnScale, useScrubberContext } from '../utils';
+import {
+ defaultTransition,
+ getPointOnScale,
+ getTransition,
+ instantTransition,
+ useScrubberContext,
+} from '../utils';
import {
calculateLabelYPositions,
getLabelPosition,
@@ -13,7 +21,11 @@ import {
} from '../utils/scrubber';
import { DefaultScrubberBeaconLabel } from './DefaultScrubberBeaconLabel';
-import type { ScrubberBeaconLabelComponent, ScrubberBeaconLabelProps } from './Scrubber';
+import type {
+ ScrubberBeaconLabelComponent,
+ ScrubberBeaconLabelProps,
+ ScrubberBeaconProps,
+} from './Scrubber';
const PositionedLabel = memo<{
index: number;
@@ -26,6 +38,7 @@ const PositionedLabel = memo<{
BeaconLabelComponent: ScrubberBeaconLabelComponent;
labelHorizontalOffset: number;
labelFont?: ChartTextProps['font'];
+ updateTransition: Transition;
}>(
({
index,
@@ -38,6 +51,7 @@ const PositionedLabel = memo<{
BeaconLabelComponent,
labelHorizontalOffset,
labelFont,
+ updateTransition,
}) => {
const pos = positions[index];
@@ -60,6 +74,7 @@ const PositionedLabel = memo<{
label={label}
onDimensionsChange={(d) => onDimensionsChange(seriesId, d)}
seriesId={seriesId}
+ transition={updateTransition}
x={x}
y={y}
/>
@@ -100,6 +115,10 @@ export type ScrubberBeaconLabelGroupProps = ScrubberBeaconLabelGroupBaseProps &
* @default DefaultScrubberBeaconLabel
*/
BeaconLabelComponent?: ScrubberBeaconLabelComponent;
+ /**
+ * Transition configuration for beacon label animations.
+ */
+ transitions?: ScrubberBeaconProps['transitions'];
};
export const ScrubberBeaconLabelGroup = memo(
@@ -110,11 +129,31 @@ export const ScrubberBeaconLabelGroup = memo(
labelFont,
labelPreferredSide = 'right',
BeaconLabelComponent = DefaultScrubberBeaconLabel,
+ transitions,
}) => {
- const { getSeries, getSeriesData, getXScale, getYScale, getXAxis, drawingArea, dataLength } =
- useCartesianChartContext();
+ const {
+ getSeries,
+ getSeriesData,
+ getXScale,
+ getYScale,
+ getXAxis,
+ drawingArea,
+ dataLength,
+ animate,
+ } = useCartesianChartContext();
const { scrubberPosition } = useScrubberContext();
+ const isIdle = scrubberPosition === undefined;
+
+ const prevIsIdle = usePreviousValue(isIdle);
+ const isIdleTransition = prevIsIdle !== undefined && isIdle !== prevIsIdle;
+
+ const updateTransition = useMemo(() => {
+ if (isIdleTransition) return instantTransition;
+ if (!isIdle) return instantTransition;
+ return getTransition(transitions?.update, animate, defaultTransition);
+ }, [transitions?.update, isIdle, animate, isIdleTransition]);
+
const [labelDimensions, setLabelDimensions] = useState>({});
const handleDimensionsChange = useCallback((seriesId: string, dimensions: LabelDimensions) => {
@@ -275,6 +314,7 @@ export const ScrubberBeaconLabelGroup = memo(
position={currentPosition}
positions={allLabelPositions}
seriesId={info.seriesId}
+ updateTransition={updateTransition}
/>
);
});
diff --git a/packages/web-visualization/src/chart/utils/__tests__/transition.test.ts b/packages/web-visualization/src/chart/utils/__tests__/transition.test.ts
index 8ea8ac990a..d0c4890988 100644
--- a/packages/web-visualization/src/chart/utils/__tests__/transition.test.ts
+++ b/packages/web-visualization/src/chart/utils/__tests__/transition.test.ts
@@ -25,16 +25,11 @@ jest.mock('framer-motion', () => {
return {
useMotionValue: jest.fn((initial) => mockMotionValue(initial)),
- useTransform: jest.fn((source, transformer) => {
- const result = mockMotionValue(transformer(source.get()));
- source.onChange((v: any) => {
- result.set(transformer(v));
- });
- return result;
- }),
- animate: jest.fn((value, target, config) => {
- // Immediately set to target for testing
- value.set(target);
+ animate: jest.fn((_from, _to, config) => {
+ // Simulate instant completion: call onUpdate with final value, then onComplete
+ if (config?.onUpdate) {
+ config.onUpdate(_to);
+ }
if (config?.onComplete) {
config.onComplete();
}
@@ -167,7 +162,7 @@ describe('usePathTransition', () => {
const { result } = renderHook(() =>
usePathTransition({
currentPath,
- transition,
+ transitions: { update: transition },
}),
);
@@ -178,7 +173,7 @@ describe('usePathTransition', () => {
({ path }) =>
usePathTransition({
currentPath: path,
- transition,
+ transitions: { update: transition },
}),
{
initialProps: { path: currentPath },
@@ -236,12 +231,12 @@ describe('usePathTransition', () => {
expect(interpolatePath).toHaveBeenCalled();
});
- it('should cancel ongoing animation when path changes', () => {
+ it('should stop ongoing animation when path changes', () => {
const { animate } = require('framer-motion');
- const cancelMock = jest.fn();
+ const stopMock = jest.fn();
animate.mockReturnValue({
- cancel: cancelMock,
- stop: jest.fn(),
+ cancel: jest.fn(),
+ stop: stopMock,
});
const { rerender } = renderHook(
@@ -257,10 +252,10 @@ describe('usePathTransition', () => {
// Trigger first animation
rerender({ path: 'M0,0L20,20' });
- // Trigger second animation (should cancel first)
+ // Trigger second animation (should stop first)
rerender({ path: 'M0,0L30,30' });
- expect(cancelMock).toHaveBeenCalled();
+ expect(stopMock).toHaveBeenCalled();
});
it('should handle smooth interruption of ongoing animation', () => {
@@ -293,10 +288,10 @@ describe('usePathTransition', () => {
it('should cleanup animation on unmount', () => {
const { animate } = require('framer-motion');
- const cancelMock = jest.fn();
+ const stopMock = jest.fn();
animate.mockReturnValue({
- cancel: cancelMock,
- stop: jest.fn(),
+ cancel: jest.fn(),
+ stop: stopMock,
});
const { unmount, rerender } = renderHook(
@@ -312,10 +307,10 @@ describe('usePathTransition', () => {
// Trigger animation
rerender({ path: 'M0,0L20,20' });
- // Unmount should cancel animation
+ // Unmount should stop animation
unmount();
- expect(cancelMock).toHaveBeenCalled();
+ expect(stopMock).toHaveBeenCalled();
});
it('should maintain previous path reference across renders', () => {
@@ -348,8 +343,10 @@ describe('usePathTransition', () => {
const { animate } = require('framer-motion');
let onCompleteCallback: (() => void) | undefined;
- animate.mockImplementation((value: any, target: any, config: any) => {
- value.set(target);
+ animate.mockImplementation((_from: any, _to: any, config: any) => {
+ if (config?.onUpdate) {
+ config.onUpdate(_to);
+ }
onCompleteCallback = config?.onComplete;
return {
cancel: jest.fn(),
diff --git a/packages/web-visualization/src/chart/utils/axis.ts b/packages/web-visualization/src/chart/utils/axis.ts
index 73fac3a3d2..1b0a7c954f 100644
--- a/packages/web-visualization/src/chart/utils/axis.ts
+++ b/packages/web-visualization/src/chart/utils/axis.ts
@@ -162,7 +162,7 @@ export const getAxisScale = ({
if (!isValidBounds(adjustedDomain))
throw new Error(
- 'Invalid domain bounds. See https://cds.coinbase.com/http://localhost:3000/components/graphs/XAxis/#domain',
+ 'Invalid domain bounds. See https://cds.coinbase.com/components/charts/XAxis/#domain',
);
if (scaleType === 'band') {
@@ -211,7 +211,7 @@ export const getAxisConfig = (
// forces id to be defined on every input config when there are multiple axes
if (axesLength > 1 && axes.some(({ id }) => id === undefined)) {
throw new Error(
- 'When defining multiple axes, each must have a unique id. See https://cds.coinbase.com/components/graphs/YAxis/#multiple-y-axes.',
+ 'When defining multiple axes, each must have a unique id. See https://cds.coinbase.com/components/charts/YAxis/#multiple-y-axes.',
);
}
diff --git a/packages/web-visualization/src/chart/utils/bar.ts b/packages/web-visualization/src/chart/utils/bar.ts
index c30de9154e..28a437d264 100644
--- a/packages/web-visualization/src/chart/utils/bar.ts
+++ b/packages/web-visualization/src/chart/utils/bar.ts
@@ -1,3 +1,53 @@
+import type { Transition } from 'framer-motion';
+
+import { defaultTransition } from './transition';
+
+/**
+ * A bar-specific transition that extends Transition with stagger support.
+ * When `staggerDelay` is provided, bars will animate with increasing delays
+ * based on their horizontal position (leftmost starts first, rightmost last).
+ *
+ * @example
+ * // Bars stagger in from left to right over 0.25s, each animating for 0.75s
+ * { type: 'tween', duration: 0.75, staggerDelay: 0.25 }
+ */
+export type BarTransition = Transition & {
+ /**
+ * Maximum stagger delay (seconds) distributed across bars by x position.
+ * Leftmost bar starts immediately, rightmost starts after this delay.
+ */
+ staggerDelay?: number;
+};
+
+/**
+ * Strips `staggerDelay` from a transition and computes a positional delay.
+ *
+ * @param transition - The transition config (may include staggerDelay)
+ * @param normalizedX - The bar's normalized x position (0 = left edge, 1 = right edge)
+ * @returns A standard Transition with computed delay
+ */
+export const withStaggerDelayTransition = (
+ transition: BarTransition,
+ normalizedX: number,
+): Transition => {
+ const { staggerDelay, ...baseTransition } = transition;
+ if (!staggerDelay) return transition;
+ return {
+ ...baseTransition,
+ delay: (baseTransition?.delay ?? 0) + normalizedX * staggerDelay,
+ };
+};
+
+/**
+ * Default bar enter transition. Uses the default spring with a stagger delay
+ * so bars spring into place from left to right.
+ * `{ type: 'spring', stiffness: 900, damping: 120, mass: 4, staggerDelay: 0.25 }`
+ */
+export const defaultBarEnterTransition: BarTransition = {
+ ...defaultTransition,
+ staggerDelay: 0.25,
+};
+
/**
* Calculates the size adjustment needed for bars when accounting for gaps between them.
* This function helps determine how much to reduce each bar's width to accommodate
diff --git a/packages/web-visualization/src/chart/utils/path.ts b/packages/web-visualization/src/chart/utils/path.ts
index 9932df4d9e..927e8819f2 100644
--- a/packages/web-visualization/src/chart/utils/path.ts
+++ b/packages/web-visualization/src/chart/utils/path.ts
@@ -11,10 +11,20 @@ import {
curveStepBefore,
line as d3Line,
} from 'd3-shape';
+import type { Transition } from 'framer-motion';
import { projectPoint, projectPoints } from './point';
import { type ChartScaleFunction, isCategoricalScale } from './scale';
+/**
+ * Default enter transition for path-based components (Line, Area).
+ * `{ type: 'tween', duration: 0.5 }`
+ */
+export const defaultPathEnterTransition: Transition = {
+ type: 'tween',
+ duration: 0.5,
+};
+
export type ChartPathCurveType =
| 'bump'
| 'catmullRom'
diff --git a/packages/web-visualization/src/chart/utils/transition.ts b/packages/web-visualization/src/chart/utils/transition.ts
index f4fa560082..2e61eecc2e 100644
--- a/packages/web-visualization/src/chart/utils/transition.ts
+++ b/packages/web-visualization/src/chart/utils/transition.ts
@@ -6,12 +6,12 @@ import {
type MotionValue,
type Transition,
useMotionValue,
- useTransform,
type ValueAnimationTransition,
} from 'framer-motion';
/**
- * Default transition configuration used across all chart components.
+ * Default update transition used across all chart components.
+ * `{ type: 'spring', stiffness: 900, damping: 120, mass: 4 }`
*/
export const defaultTransition: Transition = {
type: 'spring',
@@ -20,6 +20,15 @@ export const defaultTransition: Transition = {
mass: 4,
};
+/**
+ * Instant transition that completes immediately with no animation.
+ * Used when a transition is set to `null`.
+ */
+export const instantTransition: Transition = {
+ type: 'tween',
+ duration: 0,
+};
+
/**
* Duration in seconds for accessory elements to fade in.
*/
@@ -30,40 +39,62 @@ export const accessoryFadeTransitionDuration = 0.15;
*/
export const accessoryFadeTransitionDelay = 0.35;
+/**
+ * Default enter transition for accessory elements (Point, Scrubber beacons).
+ * `{ type: 'tween', duration: 0.15, delay: 0.35 }`
+ */
+export const defaultAccessoryEnterTransition: Transition = {
+ type: 'tween',
+ duration: accessoryFadeTransitionDuration,
+ delay: accessoryFadeTransitionDelay,
+};
+
+/**
+ * Resolves a transition value based on the animation state and a default.
+ * @note Passing in null will disable an animation.
+ * @note Passing in undefined will use the provided default.
+ */
+export const getTransition = (
+ value: Transition | null | undefined,
+ animate: boolean,
+ defaultValue: Transition,
+): Transition => {
+ if (!animate || value === null) return instantTransition;
+ return value ?? defaultValue;
+};
+
/**
* Hook for path animation state and transitions.
*
* @param currentPath - Current target path to animate to
* @param initialPath - Initial path for enter animation. When provided, the first animation will go from initialPath to currentPath.
- * @param transition - Transition configuration
+ * @param transitions - Transition configuration for enter and update animations
* @returns MotionValue containing the current interpolated path string
*
* @example
* // Simple path transition
* const animatedPath = usePathTransition({
* currentPath: d ?? '',
- * transition: {
- * type: 'spring',
- * stiffness: 300,
- * damping: 20
- * }
+ * transitions: {
+ * update: { type: 'spring', stiffness: 300, damping: 20 },
+ * },
* });
*
* @example
- * // Time based animation
+ * // Enter animation with different initial config (like DefaultBar)
* const animatedPath = usePathTransition({
* currentPath: targetPath,
* initialPath: baselinePath,
- * transition: {
- * type: 'tween',
- * duration: 0.3,
- * ease: 'easeInOut'
- * }
+ * transitions: {
+ * enter: { type: 'tween', duration: 0.5 },
+ * update: { type: 'spring', stiffness: 900, damping: 120, mass: 4 },
+ * },
* });
*/
export const usePathTransition = ({
currentPath,
initialPath,
+ transitions,
transition = defaultTransition,
}: {
/**
@@ -77,63 +108,77 @@ export const usePathTransition = ({
*/
initialPath?: string;
/**
- * Transition configuration
+ * Transition configuration for enter and update animations.
+ */
+ transitions?: {
+ /**
+ * Transition for the initial enter animation (initialPath → currentPath).
+ * Only used when `initialPath` is provided.
+ * If not provided, falls back to `update`.
+ */
+ enter?: Transition;
+ /**
+ * Transition for subsequent data update animations.
+ * @default defaultTransition
+ */
+ update?: Transition;
+ };
+ /**
+ * Transition for updates.
+ * @deprecated Use `transitions.update` instead.
*/
transition?: Transition;
}): MotionValue => {
- const isInitialRender = useRef(true);
+ const updateTransition = transitions?.update ?? transition;
+ const enterTransition = transitions?.enter;
+
const previousPathRef = useRef(initialPath ?? currentPath);
- const targetPathRef = useRef(currentPath);
+ const targetPathRef = useRef(initialPath ?? currentPath);
const animationRef = useRef(null);
- const progress = useMotionValue(0);
+ const isFirstAnimation = useRef(!!initialPath);
- // Derive the interpolated path from progress using useTransform
- const interpolatedPath = useTransform(progress, (latest) => {
- const pathInterpolator = interpolatePath(previousPathRef.current, targetPathRef.current);
- return pathInterpolator(latest);
- });
+ const animatedPath = useMotionValue(initialPath ?? currentPath);
useEffect(() => {
- // Only proceed if the target path has actually changed
if (targetPathRef.current !== currentPath) {
- // Cancel any ongoing animation before starting a new one
- const wasAnimating = !!animationRef.current;
+ const currentVisualPath = animatedPath.get();
+
if (animationRef.current) {
- animationRef.current.cancel();
+ animationRef.current.stop();
animationRef.current = null;
+ previousPathRef.current = currentVisualPath;
}
- const currentInterpolatedPath = interpolatedPath.get();
+ targetPathRef.current = currentPath;
- // If we were animating and the interpolated path is different from both start and end,
- // use it as the starting point for the next animation (smooth interruption)
- const isInterpolatedPosition =
- currentInterpolatedPath !== previousPathRef.current &&
- currentInterpolatedPath !== currentPath;
+ const activeTransition =
+ isFirstAnimation.current && enterTransition !== undefined
+ ? enterTransition
+ : updateTransition;
- if (wasAnimating && isInterpolatedPosition) {
- previousPathRef.current = currentInterpolatedPath;
- }
+ isFirstAnimation.current = false;
- targetPathRef.current = currentPath;
+ const pathInterpolator = interpolatePath(previousPathRef.current, currentPath);
- progress.set(0);
- animationRef.current = animate(progress, 1, {
- ...(transition as ValueAnimationTransition),
+ animationRef.current = animate(0, 1, {
+ ...(activeTransition as ValueAnimationTransition),
+ onUpdate: (latest) => {
+ animatedPath.set(pathInterpolator(latest));
+ },
onComplete: () => {
+ animatedPath.set(currentPath);
previousPathRef.current = currentPath;
+ animationRef.current = null;
},
});
-
- isInitialRender.current = false;
}
return () => {
if (animationRef.current) {
- animationRef.current.cancel();
+ animationRef.current.stop();
}
};
- }, [currentPath, transition, progress, interpolatedPath]);
+ }, [currentPath, updateTransition, enterTransition, animatedPath]);
- return interpolatedPath;
+ return animatedPath;
};
diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md
index ed7b6ad17d..c61c26ad28 100644
--- a/packages/web/CHANGELOG.md
+++ b/packages/web/CHANGELOG.md
@@ -8,6 +8,38 @@ All notable changes to this project will be documented in this file.
+## 8.48.3 (2/25/2026 PST)
+
+#### 🐞 Fixes
+
+- Fix: allow arrow up/down keys within focus trapped text area. [[#417](https://github.com/coinbase/cds/pull/417)]
+
+## 8.48.2 ((2/25/2026, 04:21 PM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.48.1 (2/25/2026 PST)
+
+#### 🐞 Fixes
+
+- Truncate text mid-word in multi-select chips. [[#412](https://github.com/coinbase/cds/pull/412)]
+
+## 8.48.0 (2/24/2026 PST)
+
+#### 🚀 Updates
+
+- Add start/end icon/node support to Tag. [[#421](https://github.com/coinbase/cds/pull/421)]
+
+## 8.47.4 ((2/23/2026, 03:04 PM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.47.3 (2/20/2026 PST)
+
+#### 🐞 Fixes
+
+- Remove behavior of scrolling inside TextInput updating numeric values. [[#413](https://github.com/coinbase/cds/pull/413)]
+
## 8.47.2 ((2/19/2026, 03:18 PM PST))
This is an artificial version bump with no new change.
diff --git a/packages/web/package.json b/packages/web/package.json
index 4961adf451..3c30154334 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-web",
- "version": "8.47.2",
+ "version": "8.48.3",
"description": "Coinbase Design System - Web",
"repository": {
"type": "git",
diff --git a/packages/web/src/alpha/select/DefaultSelectControl.tsx b/packages/web/src/alpha/select/DefaultSelectControl.tsx
index f19e111163..592469f15b 100644
--- a/packages/web/src/alpha/select/DefaultSelectControl.tsx
+++ b/packages/web/src/alpha/select/DefaultSelectControl.tsx
@@ -35,6 +35,15 @@ const noFocusOutlineCss = css`
}
`;
+const selectedOptionChipContentCss = css`
+ min-width: 0;
+
+ & > :not(:last-child) {
+ min-width: 0;
+ max-width: 100%;
+ }
+`;
+
const variantColor: Record = {
foreground: 'fg',
positive: 'fgPositive',
@@ -280,12 +289,15 @@ const DefaultSelectControlComponent = memo(
data-selected-value
accessibilityLabel={`${removeSelectedOptionAccessibilityLabel} ${accessibilityLabel}`}
borderWidth={0}
+ classNames={{ content: selectedOptionChipContentCss }}
disabled={option.disabled}
invertColorScheme={false}
maxWidth={200}
onClick={(event) => handleUnselectValue(event, index)}
>
- {option.label ?? option.description ?? option.value ?? ''}
+
+ {option.label ?? option.description ?? option.value ?? ''}
+
);
})}
diff --git a/packages/web/src/alpha/select/__stories__/MultiSelect.stories.tsx b/packages/web/src/alpha/select/__stories__/MultiSelect.stories.tsx
index 1e799b54fd..a5a1c000c7 100644
--- a/packages/web/src/alpha/select/__stories__/MultiSelect.stories.tsx
+++ b/packages/web/src/alpha/select/__stories__/MultiSelect.stories.tsx
@@ -362,6 +362,30 @@ export const CustomSelectAllOption = () => {
);
};
+export const LongOptionLabels = () => {
+ const exampleOptions = [
+ { value: null, label: 'Remove selection' },
+ { value: '1', label: 'Fraction fraction fraction fraction fraction' },
+ { value: '2', label: 'Truncation truncation truncation truncation truncation' },
+ { value: '3', label: 'A A A A A A A A A A A A A A A A' },
+ { value: '4', label: 'Bee Bee Bee Bee Bee Bee Bee Bee Bee Bee' },
+ ];
+ const { value, onChange } = useMultiSelect({
+ initialValue: ['1'],
+ });
+
+ return (
+
+ );
+};
+
export const Disabled = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
diff --git a/packages/web/src/controls/TextInput.tsx b/packages/web/src/controls/TextInput.tsx
index 9fdef90e33..aabb00dc9a 100644
--- a/packages/web/src/controls/TextInput.tsx
+++ b/packages/web/src/controls/TextInput.tsx
@@ -203,20 +203,28 @@ export const TextInput = memo(
'cds-textinput-label',
]);
+ // Native browser behavior adjusts the value of numeric inputs when the user is focused on the input
+ // and scrolls the page. This prevents that behavior so accidental values changes don't occur.
+ const preventWheelScroll = useCallback((event: WheelEvent) => {
+ event.preventDefault();
+ }, []);
+
const handleOnFocus = useCallback(
(e: React.FocusEvent) => {
setFocused(true);
onFocus?.(e);
+ internalRef.current?.addEventListener('wheel', preventWheelScroll);
},
- [onFocus],
+ [onFocus, internalRef, preventWheelScroll],
);
const handleOnBlur = useCallback(
(e: React.FocusEvent) => {
onBlur?.(e);
setFocused(false);
+ internalRef.current?.removeEventListener('wheel', preventWheelScroll);
},
- [onBlur],
+ [onBlur, preventWheelScroll],
);
const handleNodePress = useCallback(() => {
diff --git a/packages/web/src/controls/__stories__/TextInput.stories.tsx b/packages/web/src/controls/__stories__/TextInput.stories.tsx
index 40fda42a10..03b460a67f 100644
--- a/packages/web/src/controls/__stories__/TextInput.stories.tsx
+++ b/packages/web/src/controls/__stories__/TextInput.stories.tsx
@@ -153,6 +153,10 @@ export const ColorSurge = () => {
);
};
+export const NumberInput = function NumberInput() {
+ return ;
+};
+
export const Width = function Width() {
const widths = ['100%', '30%', '75%', '10%'] as const;
diff --git a/packages/web/src/overlays/FocusTrap.tsx b/packages/web/src/overlays/FocusTrap.tsx
index 1f15334eef..079a5edf64 100644
--- a/packages/web/src/overlays/FocusTrap.tsx
+++ b/packages/web/src/overlays/FocusTrap.tsx
@@ -140,6 +140,13 @@ export const FocusTrap = memo(function FocusTrap({
if (!element || !document) return;
+ const textAreas = element.querySelectorAll('textarea');
+ const activeElementIsTextArea =
+ activeElement && Array.from(textAreas).includes(activeElement as HTMLTextAreaElement);
+ if (activeElementIsTextArea && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
+ return;
+ }
+
let focusableElements = Array.from(
element.querySelectorAll(
focusTabIndexElements ? FOCUSABLE_ELEMENTS_INCLUDING_TABINDEX : FOCUSABLE_ELEMENTS,
diff --git a/packages/web/src/overlays/__stories__/FocusTrap.stories.tsx b/packages/web/src/overlays/__stories__/FocusTrap.stories.tsx
index 9bcf937130..f47d61a66d 100644
--- a/packages/web/src/overlays/__stories__/FocusTrap.stories.tsx
+++ b/packages/web/src/overlays/__stories__/FocusTrap.stories.tsx
@@ -1,4 +1,5 @@
import { Button } from '../../buttons';
+import { NativeTextArea } from '../../controls/NativeTextArea';
import { TextInput } from '../../controls/TextInput';
import { VStack } from '../../layout/VStack';
import { Text } from '../../typography/Text';
@@ -46,3 +47,19 @@ export const SingleFocusableChild = () => {
);
};
+
+export const TextArea = () => {
+ return (
+
+
+
+ Up/Down arrow keys should work
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/web/src/overlays/__tests__/FocusTrap.test.tsx b/packages/web/src/overlays/__tests__/FocusTrap.test.tsx
index b709ffe426..3dbac8ed5c 100644
--- a/packages/web/src/overlays/__tests__/FocusTrap.test.tsx
+++ b/packages/web/src/overlays/__tests__/FocusTrap.test.tsx
@@ -183,4 +183,32 @@ describe('FocusTrap', () => {
fireEvent.keyDown(screen.getByTestId('first'), { key: 'Tab', code: 'Tab', shiftKey: true });
expect(trigger).toHaveFocus();
});
+
+ it('allows up/down arrow key navigation in textareas', async () => {
+ const user = userEvent.setup();
+ render(
+
+
+
+
+
+
+
+ ,
+ );
+
+ const textarea = screen.getByTestId('textarea');
+ await user.click(textarea);
+
+ const events: KeyboardEvent[] = [];
+ textarea.addEventListener('keydown', (e) => events.push(e));
+
+ await user.keyboard('{ArrowDown}');
+ expect(events).toHaveLength(1);
+ expect(events[0].defaultPrevented).toBe(false);
+
+ await user.keyboard('{ArrowUp}');
+ expect(events).toHaveLength(2);
+ expect(events[1].defaultPrevented).toBe(false);
+ });
});
diff --git a/packages/web/src/tag/Tag.tsx b/packages/web/src/tag/Tag.tsx
index 88dfdc1425..b1abfd0273 100644
--- a/packages/web/src/tag/Tag.tsx
+++ b/packages/web/src/tag/Tag.tsx
@@ -7,17 +7,25 @@ import {
tagHorizontalSpacing,
} from '@coinbase/cds-common/tokens/tags';
import type {
+ IconName,
SharedAccessibilityProps,
SharedProps,
TagColorScheme,
TagEmphasis,
TagIntent,
} from '@coinbase/cds-common/types';
+import { css } from '@linaria/core';
import { useTheme } from '../hooks/useTheme';
+import { Icon } from '../icons/Icon';
import { Box, type BoxDefaultElement, type BoxProps } from '../layout/Box';
import { Text } from '../typography/Text';
+const nodeCss = css`
+ display: inline-flex;
+ align-items: center;
+`;
+
export const tagStaticClassName = 'cds-tag';
export type TagBaseProps = SharedProps &
@@ -45,6 +53,18 @@ export type TagBaseProps = SharedProps &
color?: ThemeVars.SpectrumColor;
/** Setting a custom max width for this tag will enable text truncation */
maxWidth?: BoxProps['maxWidth'];
+ /** Set the start node */
+ start?: React.ReactNode;
+ /** Icon to render at the start of the tag. */
+ startIcon?: IconName;
+ /** Whether the start icon is active */
+ startIconActive?: boolean;
+ /** Set the end node */
+ end?: React.ReactNode;
+ /** Icon to render at the end of the tag. */
+ endIcon?: IconName;
+ /** Whether the end icon is active */
+ endIconActive?: boolean;
};
export type TagProps = TagBaseProps &
@@ -59,9 +79,17 @@ export const Tag = memo(
colorScheme = 'blue',
background: customBackground,
color: customColor,
+ start,
+ startIcon,
+ startIconActive,
+ end,
+ endIcon,
+ endIconActive,
display = 'inline-flex',
alignItems = 'center',
+ gap = 0.5,
justifyContent = 'center',
+ paddingY = 0.25,
testID = tagStaticClassName,
...props
}: TagProps,
@@ -72,14 +100,9 @@ export const Tag = memo(
const boxStyles = useMemo(
() => ({
backgroundColor: `rgb(${theme.spectrum[customBackground ?? background]})`,
- }),
- [background, customBackground, theme.spectrum],
- );
- const textStyles = useMemo(
- () => ({
color: `rgb(${theme.spectrum[customColor ?? foreground]})`,
}),
- [foreground, customColor, theme.spectrum],
+ [background, customBackground, foreground, customColor, theme.spectrum],
);
return (
@@ -91,22 +114,39 @@ export const Tag = memo(
className={tagStaticClassName}
data-testid={testID}
display={display}
+ gap={gap}
justifyContent={justifyContent}
paddingX={tagHorizontalSpacing[intent]}
- paddingY={0.25}
+ paddingY={paddingY}
style={boxStyles}
testID={testID}
{...props}
>
+ {start ? (
+ {start}
+ ) : startIcon ? (
+
+
+
+ ) : null}
+
{children}
+
+ {end ? (
+ {end}
+ ) : endIcon ? (
+
+
+
+ ) : null}
);
}),
diff --git a/packages/web/src/tag/__stories__/Tag.stories.tsx b/packages/web/src/tag/__stories__/Tag.stories.tsx
index 34e8bc060f..4e9c9a8682 100644
--- a/packages/web/src/tag/__stories__/Tag.stories.tsx
+++ b/packages/web/src/tag/__stories__/Tag.stories.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import startCase from 'lodash/startCase';
+import { Icon } from '../../icons/Icon';
import { VStack } from '../../layout';
import { Tag, type TagBaseProps } from '../Tag';
@@ -104,6 +105,41 @@ export const Truncated = () => (
);
+export const WithIcons = () => (
+
+
+ Start icon
+
+
+ End icon
+
+
+ Both icons
+
+
+ Promotional with icons
+
+
+);
+
+export const WithCustomNodes = () => (
+
+ }>
+ Custom start node
+
+ }>
+ Custom end node
+
+ }
+ start={}
+ >
+ Both custom nodes
+
+
+);
+
const textStyles = {
padding: 0,
margin: 0,
diff --git a/packages/web/src/tag/__tests__/Tag.test.tsx b/packages/web/src/tag/__tests__/Tag.test.tsx
index 7cdfd83aae..3bbdb2de87 100644
--- a/packages/web/src/tag/__tests__/Tag.test.tsx
+++ b/packages/web/src/tag/__tests__/Tag.test.tsx
@@ -113,6 +113,52 @@ describe('Tag', () => {
});
});
+ it('renders with a startIcon', () => {
+ render(
+
+
+ Tag
+
+ ,
+ );
+ expect(screen.getByTestId(TEST_ID)).toBeDefined();
+ expect(screen.getByText('Tag')).toBeDefined();
+ });
+
+ it('renders with an endIcon', () => {
+ render(
+
+
+ Tag
+
+ ,
+ );
+ expect(screen.getByTestId(TEST_ID)).toBeDefined();
+ expect(screen.getByText('Tag')).toBeDefined();
+ });
+
+ it('renders with a custom start node', () => {
+ render(
+
+ *} testID={TEST_ID}>
+ Tag
+
+ ,
+ );
+ expect(screen.getByTestId('custom-start')).toBeInTheDocument();
+ });
+
+ it('renders with a custom end node', () => {
+ render(
+
+ *} testID={TEST_ID}>
+ Tag
+
+ ,
+ );
+ expect(screen.getByTestId('custom-end')).toBeInTheDocument();
+ });
+
it('verifies tagColorMap maps correctly to tagEmphasisColorMap for backward compatibility', () => {
expect(tagColorMap.informational).toEqual(tagEmphasisColorMap.low);
expect(tagColorMap.promotional).toEqual(tagEmphasisColorMap.high);
diff --git a/yarn.lock b/yarn.lock
index ff908da91a..38f29572e2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -24,6 +24,18 @@ __metadata:
languageName: node
linkType: hard
+"@algolia/abtesting@npm:1.15.1":
+ version: 1.15.1
+ resolution: "@algolia/abtesting@npm:1.15.1"
+ dependencies:
+ "@algolia/client-common": "npm:5.49.1"
+ "@algolia/requester-browser-xhr": "npm:5.49.1"
+ "@algolia/requester-fetch": "npm:5.49.1"
+ "@algolia/requester-node-http": "npm:5.49.1"
+ checksum: 10c0/97004c81724f02981815ef3c7d97e457f42180b517387502b3258cfe21be5f481524e066d1c8ea8e40891bc17fb2b8c901f0518152f5cf00096547f2dce038ea
+ languageName: node
+ linkType: hard
+
"@algolia/autocomplete-core@npm:1.17.9":
version: 1.17.9
resolution: "@algolia/autocomplete-core@npm:1.17.9"
@@ -67,82 +79,82 @@ __metadata:
languageName: node
linkType: hard
-"@algolia/client-abtesting@npm:5.20.0":
- version: 5.20.0
- resolution: "@algolia/client-abtesting@npm:5.20.0"
+"@algolia/client-abtesting@npm:5.49.1":
+ version: 5.49.1
+ resolution: "@algolia/client-abtesting@npm:5.49.1"
dependencies:
- "@algolia/client-common": "npm:5.20.0"
- "@algolia/requester-browser-xhr": "npm:5.20.0"
- "@algolia/requester-fetch": "npm:5.20.0"
- "@algolia/requester-node-http": "npm:5.20.0"
- checksum: 10c0/9c374efbb79d9ec322f92618d70183aad90f1e386e8df2f82c776af7011f2ddc0feafdb1639edfd40a4a12394e44f442016bca2e125a20d52e6227d7fbb23646
+ "@algolia/client-common": "npm:5.49.1"
+ "@algolia/requester-browser-xhr": "npm:5.49.1"
+ "@algolia/requester-fetch": "npm:5.49.1"
+ "@algolia/requester-node-http": "npm:5.49.1"
+ checksum: 10c0/ffb14177e0cebaa66ac0a334c61b97b005446ac3f2dd44eba1fcff6d2852555a57ccb274f898fed7213e49ffe23aa41ad370888c4a2c7d6fe5feb9cd625a7cac
languageName: node
linkType: hard
-"@algolia/client-analytics@npm:5.20.0":
- version: 5.20.0
- resolution: "@algolia/client-analytics@npm:5.20.0"
+"@algolia/client-analytics@npm:5.49.1":
+ version: 5.49.1
+ resolution: "@algolia/client-analytics@npm:5.49.1"
dependencies:
- "@algolia/client-common": "npm:5.20.0"
- "@algolia/requester-browser-xhr": "npm:5.20.0"
- "@algolia/requester-fetch": "npm:5.20.0"
- "@algolia/requester-node-http": "npm:5.20.0"
- checksum: 10c0/c3cc9b0eea8af6f22a4598decd1be9d3df3f4aabc7301abed38e7f3dec078827b69de38893e93c0cc2c1d0d07af03d536577c967270cb5328aeb9af2ee8eb807
+ "@algolia/client-common": "npm:5.49.1"
+ "@algolia/requester-browser-xhr": "npm:5.49.1"
+ "@algolia/requester-fetch": "npm:5.49.1"
+ "@algolia/requester-node-http": "npm:5.49.1"
+ checksum: 10c0/f004d4ac201ee83bbe4009445b180d773043f1265fc7ebf349c24b35751be90e8cb41b0f3bdf2e7a84af15c80e89ea2fc51c5d31f12d20d0ea44bcc65f3b3dc0
languageName: node
linkType: hard
-"@algolia/client-common@npm:5.20.0":
- version: 5.20.0
- resolution: "@algolia/client-common@npm:5.20.0"
- checksum: 10c0/c1288c7a3f3366c48b31a4810223d9ca17878a9da656f89dda5e8348e3ec5dc82d538bfd6ad8c203e1aa28d191ef93b10cdad90ad3a96dddd7772ffc4f26ad4e
+"@algolia/client-common@npm:5.49.1":
+ version: 5.49.1
+ resolution: "@algolia/client-common@npm:5.49.1"
+ checksum: 10c0/dadb91cc29152629675227d028443f6f5a3542e26bc86d69e014c87930900a9afcf1ab3ce97943bd3ec7bf6272b854176501de6c878485f17f98591023004e07
languageName: node
linkType: hard
-"@algolia/client-insights@npm:5.20.0":
- version: 5.20.0
- resolution: "@algolia/client-insights@npm:5.20.0"
+"@algolia/client-insights@npm:5.49.1":
+ version: 5.49.1
+ resolution: "@algolia/client-insights@npm:5.49.1"
dependencies:
- "@algolia/client-common": "npm:5.20.0"
- "@algolia/requester-browser-xhr": "npm:5.20.0"
- "@algolia/requester-fetch": "npm:5.20.0"
- "@algolia/requester-node-http": "npm:5.20.0"
- checksum: 10c0/79a4353464ce1480b446a704c2bf95db33911fce1c6975dea26bfd2cf68ca50dfaf6e5643fc11dfda8b2d3f4a7e921a615372ce61b4b781fff8c961b96a0f992
+ "@algolia/client-common": "npm:5.49.1"
+ "@algolia/requester-browser-xhr": "npm:5.49.1"
+ "@algolia/requester-fetch": "npm:5.49.1"
+ "@algolia/requester-node-http": "npm:5.49.1"
+ checksum: 10c0/d7c38abe71fe973654f4843450a25684d27bb32786a0b14b9203217b61ba51bfa689f7826111ae35b283bc52dea15bf193d4d386e4d1cd7c9e4074fe16c34286
languageName: node
linkType: hard
-"@algolia/client-personalization@npm:5.20.0":
- version: 5.20.0
- resolution: "@algolia/client-personalization@npm:5.20.0"
+"@algolia/client-personalization@npm:5.49.1":
+ version: 5.49.1
+ resolution: "@algolia/client-personalization@npm:5.49.1"
dependencies:
- "@algolia/client-common": "npm:5.20.0"
- "@algolia/requester-browser-xhr": "npm:5.20.0"
- "@algolia/requester-fetch": "npm:5.20.0"
- "@algolia/requester-node-http": "npm:5.20.0"
- checksum: 10c0/c7fbea1e3f7023c8687f21da25421187478440a16816ffaf3c0191b922ebfba23122d145cc270860f5e5a2f90157db8f0579330c2652a41280e907cd1c50c016
+ "@algolia/client-common": "npm:5.49.1"
+ "@algolia/requester-browser-xhr": "npm:5.49.1"
+ "@algolia/requester-fetch": "npm:5.49.1"
+ "@algolia/requester-node-http": "npm:5.49.1"
+ checksum: 10c0/e3ba4eb278624c8e0c6911221a6fb8e0d6ae8d103b5fd0382f992623d13d5e7ab9464c6658f3ff414ebe5b209e9b4196d65bf1206eaeca5fbc94af3a5533a403
languageName: node
linkType: hard
-"@algolia/client-query-suggestions@npm:5.20.0":
- version: 5.20.0
- resolution: "@algolia/client-query-suggestions@npm:5.20.0"
+"@algolia/client-query-suggestions@npm:5.49.1":
+ version: 5.49.1
+ resolution: "@algolia/client-query-suggestions@npm:5.49.1"
dependencies:
- "@algolia/client-common": "npm:5.20.0"
- "@algolia/requester-browser-xhr": "npm:5.20.0"
- "@algolia/requester-fetch": "npm:5.20.0"
- "@algolia/requester-node-http": "npm:5.20.0"
- checksum: 10c0/ffaadf1b1df25fe2006daafd4d5cef97897b17a944d4263df8ff892195f5ba9fb4cf51c33f6672c41d1fe593e2ed032fa28f586dc6a14abcec64c77ce3f38b63
+ "@algolia/client-common": "npm:5.49.1"
+ "@algolia/requester-browser-xhr": "npm:5.49.1"
+ "@algolia/requester-fetch": "npm:5.49.1"
+ "@algolia/requester-node-http": "npm:5.49.1"
+ checksum: 10c0/72220c68caf8d41e4af4cbf058b908c061347634b6654e36b091e86b1e3f97201e8652d27c20d9397054d8784c7172921fdcf32e3f32cd76b9f412672266d7e1
languageName: node
linkType: hard
-"@algolia/client-search@npm:5.20.0":
- version: 5.20.0
- resolution: "@algolia/client-search@npm:5.20.0"
+"@algolia/client-search@npm:5.49.1":
+ version: 5.49.1
+ resolution: "@algolia/client-search@npm:5.49.1"
dependencies:
- "@algolia/client-common": "npm:5.20.0"
- "@algolia/requester-browser-xhr": "npm:5.20.0"
- "@algolia/requester-fetch": "npm:5.20.0"
- "@algolia/requester-node-http": "npm:5.20.0"
- checksum: 10c0/2d62718f3b054a3dbee6f4b07a51eef5102c41b336e7d7768afe26889dc1852b92c0f9c747d1b44a9b921eb8daef7dfe2b2087f44a3177d21fe7d7080c83f9fe
+ "@algolia/client-common": "npm:5.49.1"
+ "@algolia/requester-browser-xhr": "npm:5.49.1"
+ "@algolia/requester-fetch": "npm:5.49.1"
+ "@algolia/requester-node-http": "npm:5.49.1"
+ checksum: 10c0/2efc425aff032b3d0f037cd9c07b43a6c53e8e2bb2efc23478a15df85c89eda017dd8c32b695092c2d6be16565951046891b808b0e0d25b82cc61425d0a4a322
languageName: node
linkType: hard
@@ -153,66 +165,66 @@ __metadata:
languageName: node
linkType: hard
-"@algolia/ingestion@npm:1.20.0":
- version: 1.20.0
- resolution: "@algolia/ingestion@npm:1.20.0"
+"@algolia/ingestion@npm:1.49.1":
+ version: 1.49.1
+ resolution: "@algolia/ingestion@npm:1.49.1"
dependencies:
- "@algolia/client-common": "npm:5.20.0"
- "@algolia/requester-browser-xhr": "npm:5.20.0"
- "@algolia/requester-fetch": "npm:5.20.0"
- "@algolia/requester-node-http": "npm:5.20.0"
- checksum: 10c0/be77d56c378e9196c817b66afd922a4a812d4cb0fa0f8b7c09c8eca219f1262212e02f948d54e5ae460aea2a08dcc67f1968a1fcfdf18a1f0fd5267e8b1881d9
+ "@algolia/client-common": "npm:5.49.1"
+ "@algolia/requester-browser-xhr": "npm:5.49.1"
+ "@algolia/requester-fetch": "npm:5.49.1"
+ "@algolia/requester-node-http": "npm:5.49.1"
+ checksum: 10c0/7bc3d4ef918db32975e855a744f68fe4316aa98209af7c84f4a2a808a17e802cc4ee38d6b6ad708d8001fd20d9eaf3e5408669a5035ab455bdc605ea4146764d
languageName: node
linkType: hard
-"@algolia/monitoring@npm:1.20.0":
- version: 1.20.0
- resolution: "@algolia/monitoring@npm:1.20.0"
+"@algolia/monitoring@npm:1.49.1":
+ version: 1.49.1
+ resolution: "@algolia/monitoring@npm:1.49.1"
dependencies:
- "@algolia/client-common": "npm:5.20.0"
- "@algolia/requester-browser-xhr": "npm:5.20.0"
- "@algolia/requester-fetch": "npm:5.20.0"
- "@algolia/requester-node-http": "npm:5.20.0"
- checksum: 10c0/0b2f9d899e2662fe0e6eb0c45fb3cc46c546951603f1ea52f9adc8d2dd4296f7010e93b2b2e0b94c1f51a2e1edc887eeb054db76c6b6f417fa123d4f1c674bdd
+ "@algolia/client-common": "npm:5.49.1"
+ "@algolia/requester-browser-xhr": "npm:5.49.1"
+ "@algolia/requester-fetch": "npm:5.49.1"
+ "@algolia/requester-node-http": "npm:5.49.1"
+ checksum: 10c0/5d48226ef571a286daf45de0038bbdabceba339ec261f508e4e2e53f7b5822c0f598b5f2f39122787aab282850397099043de10023e2379b02185e24e8b230a6
languageName: node
linkType: hard
-"@algolia/recommend@npm:5.20.0":
- version: 5.20.0
- resolution: "@algolia/recommend@npm:5.20.0"
+"@algolia/recommend@npm:5.49.1":
+ version: 5.49.1
+ resolution: "@algolia/recommend@npm:5.49.1"
dependencies:
- "@algolia/client-common": "npm:5.20.0"
- "@algolia/requester-browser-xhr": "npm:5.20.0"
- "@algolia/requester-fetch": "npm:5.20.0"
- "@algolia/requester-node-http": "npm:5.20.0"
- checksum: 10c0/ce62228b630864ed0faf78c0f3b5fbca5ef38e9c07ec6e492d7d36b948418ec87b82869d78740c980f5d0bbfbff37f15f394bfffd0571fdfb8a0973915b200cb
+ "@algolia/client-common": "npm:5.49.1"
+ "@algolia/requester-browser-xhr": "npm:5.49.1"
+ "@algolia/requester-fetch": "npm:5.49.1"
+ "@algolia/requester-node-http": "npm:5.49.1"
+ checksum: 10c0/70db57b9bd7f132b4076e3b159f2eadd5d9b9265e349cfe1822c91994b96abba11dde8a12d2bd5442ee623e00bf371ce8f53f1af90b1b875de63d27d873e4e38
languageName: node
linkType: hard
-"@algolia/requester-browser-xhr@npm:5.20.0":
- version: 5.20.0
- resolution: "@algolia/requester-browser-xhr@npm:5.20.0"
+"@algolia/requester-browser-xhr@npm:5.49.1":
+ version: 5.49.1
+ resolution: "@algolia/requester-browser-xhr@npm:5.49.1"
dependencies:
- "@algolia/client-common": "npm:5.20.0"
- checksum: 10c0/80ae38016d682404468c8c8f3765fef468dc9f83095366f8531f48982400c1e2d7c55f95b331c23d44563cbf38afcf71c29a59c65dee5ca503a6b2a8386b2eea
+ "@algolia/client-common": "npm:5.49.1"
+ checksum: 10c0/81eaa2f8798b9ccdf993214bb86dbd42cca2a30f5e8a408e53d91ebc2990d987ceb7a54d93ff496af527bacf508485710b7133e05b7b1c054bcff714ed54db94
languageName: node
linkType: hard
-"@algolia/requester-fetch@npm:5.20.0":
- version: 5.20.0
- resolution: "@algolia/requester-fetch@npm:5.20.0"
+"@algolia/requester-fetch@npm:5.49.1":
+ version: 5.49.1
+ resolution: "@algolia/requester-fetch@npm:5.49.1"
dependencies:
- "@algolia/client-common": "npm:5.20.0"
- checksum: 10c0/8d9118088a39be10ba362fd37963c41a62dfe480ef42dfa17a32438c1278041074be12d2c459de0c0a1575452f64edb64856e8f47a4bba9b732cf1fe60ad0f92
+ "@algolia/client-common": "npm:5.49.1"
+ checksum: 10c0/24987ac0f6d58993c661d8507a83e4b050fd4ba0261f0e074670cf492225b0b30a427e705145c184d703861651aede7451cedc88580063a64a7959763157d1bb
languageName: node
linkType: hard
-"@algolia/requester-node-http@npm:5.20.0":
- version: 5.20.0
- resolution: "@algolia/requester-node-http@npm:5.20.0"
+"@algolia/requester-node-http@npm:5.49.1":
+ version: 5.49.1
+ resolution: "@algolia/requester-node-http@npm:5.49.1"
dependencies:
- "@algolia/client-common": "npm:5.20.0"
- checksum: 10c0/f1e2277c675d866e143ddb4c5b2eae69cd8af62194489e802cae25152854afdad03d2ce59d354b6a57952857b460962a65909ed5dfd4164db833690dbedcf7c7
+ "@algolia/client-common": "npm:5.49.1"
+ checksum: 10c0/a75f152697ac282a31dbcbeabcc6b23e649955f6f70e64f38cc96482f3b0506afce7c46f0a5c7fbf87468c9a766015f534a8bf63777fe1386722802f9f231479
languageName: node
linkType: hard
@@ -2597,12 +2609,14 @@ __metadata:
d3-interpolate-path: "npm:^2.3.0"
d3-selection: "npm:^3.0.0"
d3-transition: "npm:^3.0.1"
+ framer-motion: "npm:^10.18.0"
lodash: "npm:^4.17.21"
peerDependencies:
"@coinbase/cds-common": "workspace:^"
"@coinbase/cds-lottie-files": "workspace:^"
"@coinbase/cds-utils": "workspace:^"
"@coinbase/cds-web": "workspace:^"
+ framer-motion: ^10.18.0
react: ^18.3.1
react-dom: ^18.3.1
languageName: unknown
@@ -2662,9 +2676,9 @@ __metadata:
"@babel/preset-env": "npm:^7.28.0"
"@babel/preset-react": "npm:^7.27.1"
"@babel/preset-typescript": "npm:^7.27.1"
- "@docusaurus/logger": "npm:^3.7.0"
- "@docusaurus/types": "npm:^3.7.0"
- "@docusaurus/utils": "npm:^3.7.0"
+ "@docusaurus/logger": "npm:~3.7.0"
+ "@docusaurus/types": "npm:~3.7.0"
+ "@docusaurus/utils": "npm:~3.7.0"
"@types/ejs": "npm:^3.1.0"
"@types/lodash": "npm:^4.14.178"
ejs: "npm:^3.1.7"
@@ -2686,9 +2700,9 @@ __metadata:
"@babel/preset-react": "npm:^7.27.1"
"@babel/preset-typescript": "npm:^7.27.1"
"@coinbase/cds-common": "workspace:^"
- "@docusaurus/logger": "npm:^3.7.0"
- "@docusaurus/plugin-content-docs": "npm:^3.7.0"
- "@docusaurus/types": "npm:^3.7.0"
+ "@docusaurus/logger": "npm:~3.7.0"
+ "@docusaurus/plugin-content-docs": "npm:~3.7.0"
+ "@docusaurus/types": "npm:~3.7.0"
kbar: "npm:^0.1.0-beta.45"
lodash: "npm:^4.17.21"
type-fest: "npm:^2.19.0"
@@ -2703,7 +2717,7 @@ __metadata:
"@babel/preset-env": "npm:^7.28.0"
"@babel/preset-react": "npm:^7.27.1"
"@babel/preset-typescript": "npm:^7.27.1"
- "@docusaurus/types": "npm:^3.7.0"
+ "@docusaurus/types": "npm:~3.7.0"
"@types/express": "npm:^4.17.21"
languageName: unknown
linkType: soft
@@ -3376,25 +3390,25 @@ __metadata:
languageName: node
linkType: hard
-"@docsearch/css@npm:3.8.3":
- version: 3.8.3
- resolution: "@docsearch/css@npm:3.8.3"
- checksum: 10c0/76f09878ccc1db0f83bb3608b1717733486f9043e0f642f79e7d0c0cb492f1e84a827eeffa2a6e4285c23e3c7b668dae46a307a90dc97958c1b0e5f9275bcc10
+"@docsearch/css@npm:3.9.0":
+ version: 3.9.0
+ resolution: "@docsearch/css@npm:3.9.0"
+ checksum: 10c0/6300551e1cab7a5487063ec3581ae78ddaee3d93ec799556b451054448559b3ba849751b825fbd8b678367ef944bd82b3f11bc1d9e74e08e3cc48db40487b396
languageName: node
linkType: hard
"@docsearch/react@npm:^3.8.1":
- version: 3.8.3
- resolution: "@docsearch/react@npm:3.8.3"
+ version: 3.9.0
+ resolution: "@docsearch/react@npm:3.9.0"
dependencies:
"@algolia/autocomplete-core": "npm:1.17.9"
"@algolia/autocomplete-preset-algolia": "npm:1.17.9"
- "@docsearch/css": "npm:3.8.3"
+ "@docsearch/css": "npm:3.9.0"
algoliasearch: "npm:^5.14.2"
peerDependencies:
- "@types/react": ">= 16.8.0 < 19.0.0"
- react: ">= 16.8.0 < 19.0.0"
- react-dom: ">= 16.8.0 < 19.0.0"
+ "@types/react": ">= 16.8.0 < 20.0.0"
+ react: ">= 16.8.0 < 20.0.0"
+ react-dom: ">= 16.8.0 < 20.0.0"
search-insights: ">= 1 < 3"
peerDependenciesMeta:
"@types/react":
@@ -3405,7 +3419,7 @@ __metadata:
optional: true
search-insights:
optional: true
- checksum: 10c0/e64c38ebd2beaf84cfc68ede509caff1a4a779863322e14ec68a13136501388753986e7caa0c65080ec562cf3b5529923557974fa62844a17697671724ea8f69
+ checksum: 10c0/5e737a5d9ef1daae1cd93e89870214c1ab0c36a3a2193e898db044bcc5d9de59f85228b2360ec0e8f10cdac7fd2fe3c6ec8a05d943ee7e17d6c1cef2e6e9ff2d
languageName: node
linkType: hard
@@ -3470,7 +3484,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/core@npm:3.7.0, @docusaurus/core@npm:^3.7.0":
+"@docusaurus/core@npm:3.7.0, @docusaurus/core@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/core@npm:3.7.0"
dependencies:
@@ -3538,7 +3552,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/faster@npm:^3.7.0":
+"@docusaurus/faster@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/faster@npm:3.7.0"
dependencies:
@@ -3567,7 +3581,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/logger@npm:3.7.0, @docusaurus/logger@npm:^3.7.0":
+"@docusaurus/logger@npm:3.7.0, @docusaurus/logger@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/logger@npm:3.7.0"
dependencies:
@@ -3577,7 +3591,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/mdx-loader@npm:3.7.0, @docusaurus/mdx-loader@npm:^3.7.0":
+"@docusaurus/mdx-loader@npm:3.7.0, @docusaurus/mdx-loader@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/mdx-loader@npm:3.7.0"
dependencies:
@@ -3612,7 +3626,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/module-type-aliases@npm:3.7.0, @docusaurus/module-type-aliases@npm:^3.7.0":
+"@docusaurus/module-type-aliases@npm:3.7.0, @docusaurus/module-type-aliases@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/module-type-aliases@npm:3.7.0"
dependencies:
@@ -3630,7 +3644,27 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/plugin-content-blog@npm:3.7.0, @docusaurus/plugin-content-blog@npm:^3.7.0":
+"@docusaurus/plugin-client-redirects@npm:~3.7.0":
+ version: 3.7.0
+ resolution: "@docusaurus/plugin-client-redirects@npm:3.7.0"
+ dependencies:
+ "@docusaurus/core": "npm:3.7.0"
+ "@docusaurus/logger": "npm:3.7.0"
+ "@docusaurus/utils": "npm:3.7.0"
+ "@docusaurus/utils-common": "npm:3.7.0"
+ "@docusaurus/utils-validation": "npm:3.7.0"
+ eta: "npm:^2.2.0"
+ fs-extra: "npm:^11.1.1"
+ lodash: "npm:^4.17.21"
+ tslib: "npm:^2.6.0"
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ checksum: 10c0/ecdd5061a683541125f14b0f1e5e1afcecefc358bf16e1b71c8e4c66ae8f70f03fd18f00fcbb3525229c8692f8976158eaee1791a68baa7451047d521d619b95
+ languageName: node
+ linkType: hard
+
+"@docusaurus/plugin-content-blog@npm:3.7.0, @docusaurus/plugin-content-blog@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/plugin-content-blog@npm:3.7.0"
dependencies:
@@ -3660,7 +3694,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/plugin-content-docs@npm:3.7.0, @docusaurus/plugin-content-docs@npm:^3.7.0":
+"@docusaurus/plugin-content-docs@npm:3.7.0, @docusaurus/plugin-content-docs@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/plugin-content-docs@npm:3.7.0"
dependencies:
@@ -3688,7 +3722,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/plugin-content-pages@npm:3.7.0, @docusaurus/plugin-content-pages@npm:^3.7.0":
+"@docusaurus/plugin-content-pages@npm:3.7.0, @docusaurus/plugin-content-pages@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/plugin-content-pages@npm:3.7.0"
dependencies:
@@ -3707,7 +3741,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/plugin-debug@npm:3.7.0, @docusaurus/plugin-debug@npm:^3.7.0":
+"@docusaurus/plugin-debug@npm:3.7.0, @docusaurus/plugin-debug@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/plugin-debug@npm:3.7.0"
dependencies:
@@ -3739,7 +3773,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/plugin-google-gtag@npm:3.7.0, @docusaurus/plugin-google-gtag@npm:^3.7.0":
+"@docusaurus/plugin-google-gtag@npm:3.7.0, @docusaurus/plugin-google-gtag@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/plugin-google-gtag@npm:3.7.0"
dependencies:
@@ -3755,7 +3789,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/plugin-google-tag-manager@npm:3.7.0, @docusaurus/plugin-google-tag-manager@npm:^3.7.0":
+"@docusaurus/plugin-google-tag-manager@npm:3.7.0, @docusaurus/plugin-google-tag-manager@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/plugin-google-tag-manager@npm:3.7.0"
dependencies:
@@ -3770,7 +3804,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/plugin-sitemap@npm:3.7.0, @docusaurus/plugin-sitemap@npm:^3.7.0":
+"@docusaurus/plugin-sitemap@npm:3.7.0, @docusaurus/plugin-sitemap@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/plugin-sitemap@npm:3.7.0"
dependencies:
@@ -3809,7 +3843,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/preset-classic@npm:^3.7.0":
+"@docusaurus/preset-classic@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/preset-classic@npm:3.7.0"
dependencies:
@@ -3834,7 +3868,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/theme-classic@npm:3.7.0, @docusaurus/theme-classic@npm:^3.7.0":
+"@docusaurus/theme-classic@npm:3.7.0, @docusaurus/theme-classic@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/theme-classic@npm:3.7.0"
dependencies:
@@ -3871,7 +3905,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/theme-common@npm:3.7.0, @docusaurus/theme-common@npm:^3.7.0":
+"@docusaurus/theme-common@npm:3.7.0, @docusaurus/theme-common@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/theme-common@npm:3.7.0"
dependencies:
@@ -3895,7 +3929,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/theme-live-codeblock@npm:^3.7.0":
+"@docusaurus/theme-live-codeblock@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/theme-live-codeblock@npm:3.7.0"
dependencies:
@@ -3915,7 +3949,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/theme-search-algolia@npm:3.7.0, @docusaurus/theme-search-algolia@npm:^3.7.0":
+"@docusaurus/theme-search-algolia@npm:3.7.0, @docusaurus/theme-search-algolia@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/theme-search-algolia@npm:3.7.0"
dependencies:
@@ -3952,14 +3986,14 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/tsconfig@npm:^3.7.0":
+"@docusaurus/tsconfig@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/tsconfig@npm:3.7.0"
checksum: 10c0/22a076fa3cf6da25a76f87fbe5b37c09997f5a8729fdc1a69c0c7955dff9f9850f16dc1de8c6d5096d258a95c428fb8839b252b9dbaa648acb7de8a0e5889dea
languageName: node
linkType: hard
-"@docusaurus/types@npm:3.7.0, @docusaurus/types@npm:^3.7.0":
+"@docusaurus/types@npm:3.7.0, @docusaurus/types@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/types@npm:3.7.0"
dependencies:
@@ -4005,7 +4039,7 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/utils@npm:3.7.0, @docusaurus/utils@npm:^3.7.0":
+"@docusaurus/utils@npm:3.7.0, @docusaurus/utils@npm:~3.7.0":
version: 3.7.0
resolution: "@docusaurus/utils@npm:3.7.0"
dependencies:
@@ -4473,89 +4507,91 @@ __metadata:
languageName: node
linkType: hard
-"@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0, @eslint-community/eslint-utils@npm:^4.7.0":
- version: 4.7.0
- resolution: "@eslint-community/eslint-utils@npm:4.7.0"
+"@eslint-community/eslint-utils@npm:^4.4.0, @eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0":
+ version: 4.9.1
+ resolution: "@eslint-community/eslint-utils@npm:4.9.1"
dependencies:
eslint-visitor-keys: "npm:^3.4.3"
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
- checksum: 10c0/c0f4f2bd73b7b7a9de74b716a664873d08ab71ab439e51befe77d61915af41a81ecec93b408778b3a7856185244c34c2c8ee28912072ec14def84ba2dec70adf
+ checksum: 10c0/dc4ab5e3e364ef27e33666b11f4b86e1a6c1d7cbf16f0c6ff87b1619b3562335e9201a3d6ce806221887ff780ec9d828962a290bb910759fd40a674686503f02
languageName: node
linkType: hard
"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1":
- version: 4.12.1
- resolution: "@eslint-community/regexpp@npm:4.12.1"
- checksum: 10c0/a03d98c246bcb9109aec2c08e4d10c8d010256538dcb3f56610191607214523d4fb1b00aa81df830b6dffb74c5fa0be03642513a289c567949d3e550ca11cdf6
+ version: 4.12.2
+ resolution: "@eslint-community/regexpp@npm:4.12.2"
+ checksum: 10c0/fddcbc66851b308478d04e302a4d771d6917a0b3740dc351513c0da9ca2eab8a1adf99f5e0aa7ab8b13fa0df005c81adeee7e63a92f3effd7d367a163b721c2d
languageName: node
linkType: hard
-"@eslint/config-array@npm:^0.19.2":
- version: 0.19.2
- resolution: "@eslint/config-array@npm:0.19.2"
+"@eslint/config-array@npm:^0.21.1":
+ version: 0.21.1
+ resolution: "@eslint/config-array@npm:0.21.1"
dependencies:
- "@eslint/object-schema": "npm:^2.1.6"
+ "@eslint/object-schema": "npm:^2.1.7"
debug: "npm:^4.3.1"
minimatch: "npm:^3.1.2"
- checksum: 10c0/dd68da9abb32d336233ac4fe0db1e15a0a8d794b6e69abb9e57545d746a97f6f542496ff9db0d7e27fab1438546250d810d90b1904ac67677215b8d8e7573f3d
+ checksum: 10c0/2f657d4edd6ddcb920579b72e7a5b127865d4c3fb4dda24f11d5c4f445a93ca481aebdbd6bf3291c536f5d034458dbcbb298ee3b698bc6c9dd02900fe87eec3c
languageName: node
linkType: hard
-"@eslint/config-helpers@npm:^0.1.0":
- version: 0.1.0
- resolution: "@eslint/config-helpers@npm:0.1.0"
- checksum: 10c0/3562b5325f42740fc83b0b92b7d13a61b383f8db064915143eec36184f09a09fad73eca6c2955ab6c248b0d04fa03c140f9af2f2c4c06770781a6b79f300a01e
+"@eslint/config-helpers@npm:^0.4.2":
+ version: 0.4.2
+ resolution: "@eslint/config-helpers@npm:0.4.2"
+ dependencies:
+ "@eslint/core": "npm:^0.17.0"
+ checksum: 10c0/92efd7a527b2d17eb1a148409d71d80f9ac160b565ac73ee092252e8bf08ecd08670699f46b306b94f13d22e88ac88a612120e7847570dd7cdc72f234d50dcb4
languageName: node
linkType: hard
-"@eslint/core@npm:^0.12.0":
- version: 0.12.0
- resolution: "@eslint/core@npm:0.12.0"
+"@eslint/core@npm:^0.17.0":
+ version: 0.17.0
+ resolution: "@eslint/core@npm:0.17.0"
dependencies:
"@types/json-schema": "npm:^7.0.15"
- checksum: 10c0/d032af81195bb28dd800c2b9617548c6c2a09b9490da3c5537fd2a1201501666d06492278bb92cfccac1f7ac249e58601dd87f813ec0d6a423ef0880434fa0c3
+ checksum: 10c0/9a580f2246633bc752298e7440dd942ec421860d1946d0801f0423830e67887e4aeba10ab9a23d281727a978eb93d053d1922a587d502942a713607f40ed704e
languageName: node
linkType: hard
-"@eslint/eslintrc@npm:^3.3.0":
- version: 3.3.0
- resolution: "@eslint/eslintrc@npm:3.3.0"
+"@eslint/eslintrc@npm:^3.3.1":
+ version: 3.3.4
+ resolution: "@eslint/eslintrc@npm:3.3.4"
dependencies:
- ajv: "npm:^6.12.4"
+ ajv: "npm:^6.14.0"
debug: "npm:^4.3.2"
espree: "npm:^10.0.1"
globals: "npm:^14.0.0"
ignore: "npm:^5.2.0"
import-fresh: "npm:^3.2.1"
- js-yaml: "npm:^4.1.0"
- minimatch: "npm:^3.1.2"
+ js-yaml: "npm:^4.1.1"
+ minimatch: "npm:^3.1.3"
strip-json-comments: "npm:^3.1.1"
- checksum: 10c0/215de990231b31e2fe6458f225d8cea0f5c781d3ecb0b7920703501f8cd21b3101fc5ef2f0d4f9a38865d36647b983e0e8ce8bf12fd2bcdd227fc48a5b1a43be
+ checksum: 10c0/1fe481a6af03c09be8d92d67e2bbf693b7522b0591934bfb44bd13e297649b13e4ec5e3fc70b02e4497a17c1afbfa22f5bf5efa4fc06a24abace8e5d097fec8c
languageName: node
linkType: hard
-"@eslint/js@npm:9.22.0":
- version: 9.22.0
- resolution: "@eslint/js@npm:9.22.0"
- checksum: 10c0/5bcd009bb579dc6c6ed760703bdd741e08a48cd9decd677aa2cf67fe66236658cb09a00185a0369f3904e5cffba9e6e0f2ff4d9ba4fdf598fcd81d34c49213a5
+"@eslint/js@npm:9.39.3":
+ version: 9.39.3
+ resolution: "@eslint/js@npm:9.39.3"
+ checksum: 10c0/df1c70d6681c8daf4a3c86dfac159fcd98a73c4620c4fbe2be6caab1f30a34c7de0ad88ab0e81162376d2cde1a2eed1c32eff5f917ca369870930a51f8e818f1
languageName: node
linkType: hard
-"@eslint/object-schema@npm:^2.1.6":
- version: 2.1.6
- resolution: "@eslint/object-schema@npm:2.1.6"
- checksum: 10c0/b8cdb7edea5bc5f6a96173f8d768d3554a628327af536da2fc6967a93b040f2557114d98dbcdbf389d5a7b290985ad6a9ce5babc547f36fc1fde42e674d11a56
+"@eslint/object-schema@npm:^2.1.7":
+ version: 2.1.7
+ resolution: "@eslint/object-schema@npm:2.1.7"
+ checksum: 10c0/936b6e499853d1335803f556d526c86f5fe2259ed241bc665000e1d6353828edd913feed43120d150adb75570cae162cf000b5b0dfc9596726761c36b82f4e87
languageName: node
linkType: hard
-"@eslint/plugin-kit@npm:^0.2.7":
- version: 0.2.7
- resolution: "@eslint/plugin-kit@npm:0.2.7"
+"@eslint/plugin-kit@npm:^0.4.1":
+ version: 0.4.1
+ resolution: "@eslint/plugin-kit@npm:0.4.1"
dependencies:
- "@eslint/core": "npm:^0.12.0"
+ "@eslint/core": "npm:^0.17.0"
levn: "npm:^0.4.1"
- checksum: 10c0/0a1aff1ad63e72aca923217e556c6dfd67d7cd121870eb7686355d7d1475d569773528a8b2111b9176f3d91d2ea81f7413c34600e8e5b73d59e005d70780b633
+ checksum: 10c0/51600f78b798f172a9915dffb295e2ffb44840d583427bc732baf12ecb963eb841b253300e657da91d890f4b323d10a1bd12934bf293e3018d8bb66fdce5217b
languageName: node
linkType: hard
@@ -10005,91 +10041,91 @@ __metadata:
languageName: node
linkType: hard
-"@swc/html-darwin-arm64@npm:1.10.12":
- version: 1.10.12
- resolution: "@swc/html-darwin-arm64@npm:1.10.12"
+"@swc/html-darwin-arm64@npm:1.15.13":
+ version: 1.15.13
+ resolution: "@swc/html-darwin-arm64@npm:1.15.13"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
-"@swc/html-darwin-x64@npm:1.10.12":
- version: 1.10.12
- resolution: "@swc/html-darwin-x64@npm:1.10.12"
+"@swc/html-darwin-x64@npm:1.15.13":
+ version: 1.15.13
+ resolution: "@swc/html-darwin-x64@npm:1.15.13"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
-"@swc/html-linux-arm-gnueabihf@npm:1.10.12":
- version: 1.10.12
- resolution: "@swc/html-linux-arm-gnueabihf@npm:1.10.12"
+"@swc/html-linux-arm-gnueabihf@npm:1.15.13":
+ version: 1.15.13
+ resolution: "@swc/html-linux-arm-gnueabihf@npm:1.15.13"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
-"@swc/html-linux-arm64-gnu@npm:1.10.12":
- version: 1.10.12
- resolution: "@swc/html-linux-arm64-gnu@npm:1.10.12"
+"@swc/html-linux-arm64-gnu@npm:1.15.13":
+ version: 1.15.13
+ resolution: "@swc/html-linux-arm64-gnu@npm:1.15.13"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
-"@swc/html-linux-arm64-musl@npm:1.10.12":
- version: 1.10.12
- resolution: "@swc/html-linux-arm64-musl@npm:1.10.12"
+"@swc/html-linux-arm64-musl@npm:1.15.13":
+ version: 1.15.13
+ resolution: "@swc/html-linux-arm64-musl@npm:1.15.13"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
-"@swc/html-linux-x64-gnu@npm:1.10.12":
- version: 1.10.12
- resolution: "@swc/html-linux-x64-gnu@npm:1.10.12"
+"@swc/html-linux-x64-gnu@npm:1.15.13":
+ version: 1.15.13
+ resolution: "@swc/html-linux-x64-gnu@npm:1.15.13"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
-"@swc/html-linux-x64-musl@npm:1.10.12":
- version: 1.10.12
- resolution: "@swc/html-linux-x64-musl@npm:1.10.12"
+"@swc/html-linux-x64-musl@npm:1.15.13":
+ version: 1.15.13
+ resolution: "@swc/html-linux-x64-musl@npm:1.15.13"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
-"@swc/html-win32-arm64-msvc@npm:1.10.12":
- version: 1.10.12
- resolution: "@swc/html-win32-arm64-msvc@npm:1.10.12"
+"@swc/html-win32-arm64-msvc@npm:1.15.13":
+ version: 1.15.13
+ resolution: "@swc/html-win32-arm64-msvc@npm:1.15.13"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
-"@swc/html-win32-ia32-msvc@npm:1.10.12":
- version: 1.10.12
- resolution: "@swc/html-win32-ia32-msvc@npm:1.10.12"
+"@swc/html-win32-ia32-msvc@npm:1.15.13":
+ version: 1.15.13
+ resolution: "@swc/html-win32-ia32-msvc@npm:1.15.13"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
-"@swc/html-win32-x64-msvc@npm:1.10.12":
- version: 1.10.12
- resolution: "@swc/html-win32-x64-msvc@npm:1.10.12"
+"@swc/html-win32-x64-msvc@npm:1.15.13":
+ version: 1.15.13
+ resolution: "@swc/html-win32-x64-msvc@npm:1.15.13"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@swc/html@npm:^1.7.39":
- version: 1.10.12
- resolution: "@swc/html@npm:1.10.12"
+ version: 1.15.13
+ resolution: "@swc/html@npm:1.15.13"
dependencies:
"@swc/counter": "npm:^0.1.3"
- "@swc/html-darwin-arm64": "npm:1.10.12"
- "@swc/html-darwin-x64": "npm:1.10.12"
- "@swc/html-linux-arm-gnueabihf": "npm:1.10.12"
- "@swc/html-linux-arm64-gnu": "npm:1.10.12"
- "@swc/html-linux-arm64-musl": "npm:1.10.12"
- "@swc/html-linux-x64-gnu": "npm:1.10.12"
- "@swc/html-linux-x64-musl": "npm:1.10.12"
- "@swc/html-win32-arm64-msvc": "npm:1.10.12"
- "@swc/html-win32-ia32-msvc": "npm:1.10.12"
- "@swc/html-win32-x64-msvc": "npm:1.10.12"
+ "@swc/html-darwin-arm64": "npm:1.15.13"
+ "@swc/html-darwin-x64": "npm:1.15.13"
+ "@swc/html-linux-arm-gnueabihf": "npm:1.15.13"
+ "@swc/html-linux-arm64-gnu": "npm:1.15.13"
+ "@swc/html-linux-arm64-musl": "npm:1.15.13"
+ "@swc/html-linux-x64-gnu": "npm:1.15.13"
+ "@swc/html-linux-x64-musl": "npm:1.15.13"
+ "@swc/html-win32-arm64-msvc": "npm:1.15.13"
+ "@swc/html-win32-ia32-msvc": "npm:1.15.13"
+ "@swc/html-win32-x64-msvc": "npm:1.15.13"
dependenciesMeta:
"@swc/html-darwin-arm64":
optional: true
@@ -10111,7 +10147,7 @@ __metadata:
optional: true
"@swc/html-win32-x64-msvc":
optional: true
- checksum: 10c0/b7a1f9b932adf4c3f3c9208c9a9d9044d1cccfea4c4b3c6d1a0431210f9fd028a404ed490e447849c3c2cf36a09657d24b981457ba4ba4c436d9ddf3abd63c33
+ checksum: 10c0/d312a2ce41c89c42cd54544d89edce0355257b3febe9b164c684da46545f4c1beddb093dd20cc74aab99ee122d36d384ca97f06a973d82f2a501752949a67153
languageName: node
linkType: hard
@@ -12932,59 +12968,60 @@ __metadata:
languageName: node
linkType: hard
-"ajv@npm:^6.1.0, ajv@npm:^6.10.2, ajv@npm:^6.12.2, ajv@npm:^6.12.4, ajv@npm:^6.12.5, ajv@npm:^6.12.6":
- version: 6.12.6
- resolution: "ajv@npm:6.12.6"
+"ajv@npm:^6.1.0, ajv@npm:^6.10.2, ajv@npm:^6.12.2, ajv@npm:^6.12.4, ajv@npm:^6.12.5, ajv@npm:^6.12.6, ajv@npm:^6.14.0":
+ version: 6.14.0
+ resolution: "ajv@npm:6.14.0"
dependencies:
fast-deep-equal: "npm:^3.1.1"
fast-json-stable-stringify: "npm:^2.0.0"
json-schema-traverse: "npm:^0.4.1"
uri-js: "npm:^4.2.2"
- checksum: 10c0/41e23642cbe545889245b9d2a45854ebba51cda6c778ebced9649420d9205f2efb39cb43dbc41e358409223b1ea43303ae4839db682c848b891e4811da1a5a71
+ checksum: 10c0/a2bc39b0555dc9802c899f86990eb8eed6e366cddbf65be43d5aa7e4f3c4e1a199d5460fd7ca4fb3d864000dbbc049253b72faa83b3b30e641ca52cb29a68c22
languageName: node
linkType: hard
"ajv@npm:^8.0.0, ajv@npm:^8.0.1, ajv@npm:^8.11.0, ajv@npm:^8.6.2, ajv@npm:^8.6.3, ajv@npm:^8.9.0":
- version: 8.17.1
- resolution: "ajv@npm:8.17.1"
+ version: 8.18.0
+ resolution: "ajv@npm:8.18.0"
dependencies:
fast-deep-equal: "npm:^3.1.3"
fast-uri: "npm:^3.0.1"
json-schema-traverse: "npm:^1.0.0"
require-from-string: "npm:^2.0.2"
- checksum: 10c0/ec3ba10a573c6b60f94639ffc53526275917a2df6810e4ab5a6b959d87459f9ef3f00d5e7865b82677cb7d21590355b34da14d1d0b9c32d75f95a187e76fff35
+ checksum: 10c0/e7517c426173513a07391be951879932bdf3348feaebd2199f5b901c20f99d60db8cd1591502d4d551dc82f594e82a05c4fe1c70139b15b8937f7afeaed9532f
languageName: node
linkType: hard
"algoliasearch-helper@npm:^3.22.6":
- version: 3.24.1
- resolution: "algoliasearch-helper@npm:3.24.1"
+ version: 3.28.0
+ resolution: "algoliasearch-helper@npm:3.28.0"
dependencies:
"@algolia/events": "npm:^4.0.1"
peerDependencies:
algoliasearch: ">= 3.1 < 6"
- checksum: 10c0/b6065ef5404e25f3cb65430f92b7926a7e597be34855eff86a616ae75bfb6d5f524fe8e34dcccde5df617a1eec1c01c20706f53a778d0006337ca40451e773d0
+ checksum: 10c0/a354f6a5074dd5c548ef112f5aec51720cdb0f28ad6144f4d6d9c8829f934722b55f1aa34dfb261a7247330fd7a0de1b6028846a08419b6ca4090456ab0d57fb
languageName: node
linkType: hard
"algoliasearch@npm:^5.14.2, algoliasearch@npm:^5.17.1":
- version: 5.20.0
- resolution: "algoliasearch@npm:5.20.0"
- dependencies:
- "@algolia/client-abtesting": "npm:5.20.0"
- "@algolia/client-analytics": "npm:5.20.0"
- "@algolia/client-common": "npm:5.20.0"
- "@algolia/client-insights": "npm:5.20.0"
- "@algolia/client-personalization": "npm:5.20.0"
- "@algolia/client-query-suggestions": "npm:5.20.0"
- "@algolia/client-search": "npm:5.20.0"
- "@algolia/ingestion": "npm:1.20.0"
- "@algolia/monitoring": "npm:1.20.0"
- "@algolia/recommend": "npm:5.20.0"
- "@algolia/requester-browser-xhr": "npm:5.20.0"
- "@algolia/requester-fetch": "npm:5.20.0"
- "@algolia/requester-node-http": "npm:5.20.0"
- checksum: 10c0/34bbe5ea83b62ea7604fd50ef61d9225cfa1bf5b1bf064500c46dddbebad922d38dfb7fd7c531591ada113879ed81c3896912a561012b9e1c1b1ae3ec68b6edf
+ version: 5.49.1
+ resolution: "algoliasearch@npm:5.49.1"
+ dependencies:
+ "@algolia/abtesting": "npm:1.15.1"
+ "@algolia/client-abtesting": "npm:5.49.1"
+ "@algolia/client-analytics": "npm:5.49.1"
+ "@algolia/client-common": "npm:5.49.1"
+ "@algolia/client-insights": "npm:5.49.1"
+ "@algolia/client-personalization": "npm:5.49.1"
+ "@algolia/client-query-suggestions": "npm:5.49.1"
+ "@algolia/client-search": "npm:5.49.1"
+ "@algolia/ingestion": "npm:1.49.1"
+ "@algolia/monitoring": "npm:1.49.1"
+ "@algolia/recommend": "npm:5.49.1"
+ "@algolia/requester-browser-xhr": "npm:5.49.1"
+ "@algolia/requester-fetch": "npm:5.49.1"
+ "@algolia/requester-node-http": "npm:5.49.1"
+ checksum: 10c0/8bc2ee9f321b4c758427342bddd9706d9944128644ebe806a8a79cfe924ba2ce3be90ad4d9d234eedf3ef3e33710eda8f9c7b38290e95182a0aa7323d0003e59
languageName: node
linkType: hard
@@ -15154,9 +15191,9 @@ __metadata:
linkType: hard
"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001616, caniuse-lite@npm:^1.0.30001646, caniuse-lite@npm:^1.0.30001669, caniuse-lite@npm:^1.0.30001726":
- version: 1.0.30001762
- resolution: "caniuse-lite@npm:1.0.30001762"
- checksum: 10c0/93707eac5b0240af3f2ce6e2d7ab504a6fefcf9c2f9cd8fb9d488e496a333c61e557dab0472c1b00c17bc386a5dbb792aa4c778cda2d768e17f986617d7aec53
+ version: 1.0.30001774
+ resolution: "caniuse-lite@npm:1.0.30001774"
+ checksum: 10c0/cc6a340a5421b9a67d8fa80889065ee27b2839ad62993571dded5296e18f02bbf685ce7094e93fe908cddc9fefdfad35d6c010b724cc3d22a6479b0d0b679f8c
languageName: node
linkType: hard
@@ -16217,9 +16254,9 @@ __metadata:
linkType: hard
"copy-text-to-clipboard@npm:^3.2.0":
- version: 3.2.0
- resolution: "copy-text-to-clipboard@npm:3.2.0"
- checksum: 10c0/d60fdadc59d526e19d56ad23cec2b292d33c771a5091621bd322d138804edd3c10eb2367d46ec71b39f5f7f7116a2910b332281aeb36a5b679199d746a8a5381
+ version: 3.2.2
+ resolution: "copy-text-to-clipboard@npm:3.2.2"
+ checksum: 10c0/451796734a380f7da7b0af27c4d94e449719d3a2f2170e99c7e46eeee54cf3c4b4fdeabc185f6d6e8cbdf932e350831d05e8387c4bae8dcedb7fb961c600ddd4
languageName: node
linkType: hard
@@ -18005,24 +18042,25 @@ __metadata:
"@coinbase/docusaurus-plugin-docgen": "workspace:^"
"@coinbase/docusaurus-plugin-kbar": "workspace:^"
"@coinbase/docusaurus-plugin-llm-dev-server": "workspace:^"
- "@docusaurus/core": "npm:^3.7.0"
- "@docusaurus/faster": "npm:^3.7.0"
- "@docusaurus/mdx-loader": "npm:^3.7.0"
- "@docusaurus/module-type-aliases": "npm:^3.7.0"
- "@docusaurus/plugin-content-blog": "npm:^3.7.0"
- "@docusaurus/plugin-content-docs": "npm:^3.7.0"
- "@docusaurus/plugin-content-pages": "npm:^3.7.0"
- "@docusaurus/plugin-debug": "npm:^3.7.0"
- "@docusaurus/plugin-google-gtag": "npm:^3.7.0"
- "@docusaurus/plugin-google-tag-manager": "npm:^3.7.0"
- "@docusaurus/plugin-sitemap": "npm:^3.7.0"
- "@docusaurus/preset-classic": "npm:^3.7.0"
- "@docusaurus/theme-classic": "npm:^3.7.0"
- "@docusaurus/theme-common": "npm:^3.7.0"
- "@docusaurus/theme-live-codeblock": "npm:^3.7.0"
- "@docusaurus/theme-search-algolia": "npm:^3.7.0"
- "@docusaurus/tsconfig": "npm:^3.7.0"
- "@docusaurus/types": "npm:^3.7.0"
+ "@docusaurus/core": "npm:~3.7.0"
+ "@docusaurus/faster": "npm:~3.7.0"
+ "@docusaurus/mdx-loader": "npm:~3.7.0"
+ "@docusaurus/module-type-aliases": "npm:~3.7.0"
+ "@docusaurus/plugin-client-redirects": "npm:~3.7.0"
+ "@docusaurus/plugin-content-blog": "npm:~3.7.0"
+ "@docusaurus/plugin-content-docs": "npm:~3.7.0"
+ "@docusaurus/plugin-content-pages": "npm:~3.7.0"
+ "@docusaurus/plugin-debug": "npm:~3.7.0"
+ "@docusaurus/plugin-google-gtag": "npm:~3.7.0"
+ "@docusaurus/plugin-google-tag-manager": "npm:~3.7.0"
+ "@docusaurus/plugin-sitemap": "npm:~3.7.0"
+ "@docusaurus/preset-classic": "npm:~3.7.0"
+ "@docusaurus/theme-classic": "npm:~3.7.0"
+ "@docusaurus/theme-common": "npm:~3.7.0"
+ "@docusaurus/theme-live-codeblock": "npm:~3.7.0"
+ "@docusaurus/theme-search-algolia": "npm:~3.7.0"
+ "@docusaurus/tsconfig": "npm:~3.7.0"
+ "@docusaurus/types": "npm:~3.7.0"
"@linaria/babel-preset": "npm:^3.0.0-beta.22"
"@linaria/core": "npm:^3.0.0-beta.22"
"@linaria/webpack-loader": "npm:^3.0.0-beta.22"
@@ -18402,9 +18440,9 @@ __metadata:
languageName: node
linkType: hard
-"elliptic@npm:^6.5.3":
- version: 6.5.4
- resolution: "elliptic@npm:6.5.4"
+"elliptic@npm:^6.6.0":
+ version: 6.6.1
+ resolution: "elliptic@npm:6.6.1"
dependencies:
bn.js: "npm:^4.11.9"
brorand: "npm:^1.1.0"
@@ -18413,7 +18451,7 @@ __metadata:
inherits: "npm:^2.0.4"
minimalistic-assert: "npm:^1.0.1"
minimalistic-crypto-utils: "npm:^1.0.1"
- checksum: 10c0/5f361270292c3b27cf0843e84526d11dec31652f03c2763c6c2b8178548175ff5eba95341dd62baff92b2265d1af076526915d8af6cc9cb7559c44a62f8ca6e2
+ checksum: 10c0/8b24ef782eec8b472053793ea1e91ae6bee41afffdfcb78a81c0a53b191e715cbe1292aa07165958a9bbe675bd0955142560b1a007ffce7d6c765bcaf951a867
languageName: node
linkType: hard
@@ -19454,13 +19492,13 @@ __metadata:
languageName: node
linkType: hard
-"eslint-scope@npm:^8.3.0":
- version: 8.3.0
- resolution: "eslint-scope@npm:8.3.0"
+"eslint-scope@npm:^8.4.0":
+ version: 8.4.0
+ resolution: "eslint-scope@npm:8.4.0"
dependencies:
esrecurse: "npm:^4.3.0"
estraverse: "npm:^5.2.0"
- checksum: 10c0/23bf54345573201fdf06d29efa345ab508b355492f6c6cc9e2b9f6d02b896f369b6dd5315205be94b8853809776c4d13353b85c6b531997b164ff6c3328ecf5b
+ checksum: 10c0/407f6c600204d0f3705bd557f81bd0189e69cd7996f408f8971ab5779c0af733d1af2f1412066b40ee1588b085874fc37a2333986c6521669cdbdd36ca5058e0
languageName: node
linkType: hard
@@ -19497,30 +19535,29 @@ __metadata:
linkType: hard
"eslint@npm:^9.22.0":
- version: 9.22.0
- resolution: "eslint@npm:9.22.0"
+ version: 9.39.3
+ resolution: "eslint@npm:9.39.3"
dependencies:
- "@eslint-community/eslint-utils": "npm:^4.2.0"
+ "@eslint-community/eslint-utils": "npm:^4.8.0"
"@eslint-community/regexpp": "npm:^4.12.1"
- "@eslint/config-array": "npm:^0.19.2"
- "@eslint/config-helpers": "npm:^0.1.0"
- "@eslint/core": "npm:^0.12.0"
- "@eslint/eslintrc": "npm:^3.3.0"
- "@eslint/js": "npm:9.22.0"
- "@eslint/plugin-kit": "npm:^0.2.7"
+ "@eslint/config-array": "npm:^0.21.1"
+ "@eslint/config-helpers": "npm:^0.4.2"
+ "@eslint/core": "npm:^0.17.0"
+ "@eslint/eslintrc": "npm:^3.3.1"
+ "@eslint/js": "npm:9.39.3"
+ "@eslint/plugin-kit": "npm:^0.4.1"
"@humanfs/node": "npm:^0.16.6"
"@humanwhocodes/module-importer": "npm:^1.0.1"
"@humanwhocodes/retry": "npm:^0.4.2"
"@types/estree": "npm:^1.0.6"
- "@types/json-schema": "npm:^7.0.15"
ajv: "npm:^6.12.4"
chalk: "npm:^4.0.0"
cross-spawn: "npm:^7.0.6"
debug: "npm:^4.3.2"
escape-string-regexp: "npm:^4.0.0"
- eslint-scope: "npm:^8.3.0"
- eslint-visitor-keys: "npm:^4.2.0"
- espree: "npm:^10.3.0"
+ eslint-scope: "npm:^8.4.0"
+ eslint-visitor-keys: "npm:^4.2.1"
+ espree: "npm:^10.4.0"
esquery: "npm:^1.5.0"
esutils: "npm:^2.0.2"
fast-deep-equal: "npm:^3.1.3"
@@ -19542,18 +19579,18 @@ __metadata:
optional: true
bin:
eslint: bin/eslint.js
- checksum: 10c0/7b5ab6f2365971c16efe97349565f75d8343347562fb23f12734c6ab2cd5e35301373a0d51e194789ddcfdfca21db7b62ff481b03d524b8169896c305b65ff48
+ checksum: 10c0/5e5dbf84d4f604f5d2d7a58c5c3fcdde30a01b8973ff3caeca8b2bacc16066717cedb4385ce52db1a2746d0b621770d4d4227cc7f44982b0b03818be2c31538d
languageName: node
linkType: hard
-"espree@npm:^10.0.1, espree@npm:^10.3.0":
- version: 10.3.0
- resolution: "espree@npm:10.3.0"
+"espree@npm:^10.0.1, espree@npm:^10.4.0":
+ version: 10.4.0
+ resolution: "espree@npm:10.4.0"
dependencies:
- acorn: "npm:^8.14.0"
+ acorn: "npm:^8.15.0"
acorn-jsx: "npm:^5.3.2"
- eslint-visitor-keys: "npm:^4.2.0"
- checksum: 10c0/272beeaca70d0a1a047d61baff64db04664a33d7cfb5d144f84bc8a5c6194c6c8ebe9cc594093ca53add88baa23e59b01e69e8a0160ab32eac570482e165c462
+ eslint-visitor-keys: "npm:^4.2.1"
+ checksum: 10c0/c63fe06131c26c8157b4083313cb02a9a54720a08e21543300e55288c40e06c3fc284bdecf108d3a1372c5934a0a88644c98714f38b6ae8ed272b40d9ea08d6b
languageName: node
linkType: hard
@@ -19568,11 +19605,11 @@ __metadata:
linkType: hard
"esquery@npm:^1.4.0, esquery@npm:^1.5.0":
- version: 1.6.0
- resolution: "esquery@npm:1.6.0"
+ version: 1.7.0
+ resolution: "esquery@npm:1.7.0"
dependencies:
estraverse: "npm:^5.1.0"
- checksum: 10c0/cb9065ec605f9da7a76ca6dadb0619dfb611e37a81e318732977d90fab50a256b95fee2d925fba7c2f3f0523aa16f91587246693bc09bc34d5a59575fe6e93d2
+ checksum: 10c0/77d5173db450b66f3bc685d11af4c90cffeedb340f34a39af96d43509a335ce39c894fd79233df32d38f5e4e219fa0f7076f6ec90bae8320170ba082c0db4793
languageName: node
linkType: hard
@@ -20416,9 +20453,9 @@ __metadata:
linkType: hard
"fast-uri@npm:^3.0.1":
- version: 3.0.1
- resolution: "fast-uri@npm:3.0.1"
- checksum: 10c0/3cd46d6006083b14ca61ffe9a05b8eef75ef87e9574b6f68f2e17ecf4daa7aaadeff44e3f0f7a0ef4e0f7e7c20fc07beec49ff14dc72d0b500f00386592f2d10
+ version: 3.1.0
+ resolution: "fast-uri@npm:3.1.0"
+ checksum: 10c0/44364adca566f70f40d1e9b772c923138d47efeac2ae9732a872baafd77061f26b097ba2f68f0892885ad177becd065520412b8ffeec34b16c99433c5b9e2de7
languageName: node
linkType: hard
@@ -23362,10 +23399,10 @@ __metadata:
languageName: node
linkType: hard
-"ip@npm:^2.0.0":
- version: 2.0.0
- resolution: "ip@npm:2.0.0"
- checksum: 10c0/8d186cc5585f57372847ae29b6eba258c68862055e18a75cc4933327232cb5c107f89800ce29715d542eef2c254fbb68b382e780a7414f9ee7caf60b7a473958
+"ip@npm:^2.0.1":
+ version: 2.0.1
+ resolution: "ip@npm:2.0.1"
+ checksum: 10c0/cab8eb3e88d0abe23e4724829621ec4c4c5cb41a7f936a2e626c947128c1be16ed543448d42af7cca95379f9892bfcacc1ccd8d09bc7e8bea0e86d492ce33616
languageName: node
linkType: hard
@@ -25198,14 +25235,14 @@ __metadata:
languageName: node
linkType: hard
-"js-yaml@npm:^4.1.0":
- version: 4.1.0
- resolution: "js-yaml@npm:4.1.0"
+"js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1":
+ version: 4.1.1
+ resolution: "js-yaml@npm:4.1.1"
dependencies:
argparse: "npm:^2.0.1"
bin:
js-yaml: bin/js-yaml.js
- checksum: 10c0/184a24b4eaacfce40ad9074c64fd42ac83cf74d8c8cd137718d456ced75051229e5061b8633c3366b8aada17945a7a356b337828c19da92b51ae62126575018f
+ checksum: 10c0/561c7d7088c40a9bb53cc75becbfb1df6ae49b34b5e6e5a81744b14ae8667ec564ad2527709d1a6e7d5e5fa6d483aa0f373a50ad98d42fde368ec4a190d4fae7
languageName: node
linkType: hard
@@ -28329,12 +28366,12 @@ __metadata:
languageName: node
linkType: hard
-"minimatch@npm:2 || 3, minimatch@npm:3.1.2, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2":
- version: 3.1.2
- resolution: "minimatch@npm:3.1.2"
+"minimatch@npm:2 || 3, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2, minimatch@npm:^3.1.3":
+ version: 3.1.5
+ resolution: "minimatch@npm:3.1.5"
dependencies:
brace-expansion: "npm:^1.1.7"
- checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311
+ checksum: 10c0/2ecbdc0d33f07bddb0315a8b5afbcb761307a8778b48f0b312418ccbced99f104a2d17d8aca7573433c70e8ccd1c56823a441897a45e384ea76ef401a26ace70
languageName: node
linkType: hard
@@ -28347,6 +28384,15 @@ __metadata:
languageName: node
linkType: hard
+"minimatch@npm:3.1.2":
+ version: 3.1.2
+ resolution: "minimatch@npm:3.1.2"
+ dependencies:
+ brace-expansion: "npm:^1.1.7"
+ checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311
+ languageName: node
+ linkType: hard
+
"minimatch@npm:9.0.3":
version: 9.0.3
resolution: "minimatch@npm:9.0.3"