diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index 0579e7b2d..db4efc644 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. +## 8.59.0 ((3/27/2026, 05:43 AM PST)) + +This is an artificial version bump with no new change. + ## 8.58.0 ((3/25/2026, 11:42 AM PST)) This is an artificial version bump with no new change. diff --git a/packages/common/package.json b/packages/common/package.json index 10a4f8e0a..3de98d215 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-common", - "version": "8.58.0", + "version": "8.59.0", "description": "Coinbase Design System - Common", "repository": { "type": "git", diff --git a/packages/mcp-server/CHANGELOG.md b/packages/mcp-server/CHANGELOG.md index 770b1b9ff..78d7c0704 100644 --- a/packages/mcp-server/CHANGELOG.md +++ b/packages/mcp-server/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. +## 8.59.0 ((3/27/2026, 05:43 AM PST)) + +This is an artificial version bump with no new change. + ## 8.58.0 ((3/25/2026, 11:42 AM 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 d2ac24a62..697a23b3d 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-mcp-server", - "version": "8.58.0", + "version": "8.59.0", "description": "Coinbase Design System - MCP Server", "repository": { "type": "git", diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index eefb2abce..1573ed87b 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file. +## 8.59.0 (3/27/2026 PST) + +#### 🚀 Updates + +- Support controlSize on Checkbox and Radio. [[#546](https://github.com/coinbase/cds/pull/546)] + ## 8.58.0 (3/25/2026 PST) #### 🚀 Updates diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 46c77dba8..ce059f086 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-mobile", - "version": "8.58.0", + "version": "8.59.0", "description": "Coinbase Design System - Mobile", "repository": { "type": "git", diff --git a/packages/mobile/src/controls/Checkbox.tsx b/packages/mobile/src/controls/Checkbox.tsx index 0d91a8744..0ec37a78c 100644 --- a/packages/mobile/src/controls/Checkbox.tsx +++ b/packages/mobile/src/controls/Checkbox.tsx @@ -11,7 +11,7 @@ import { Control, type ControlBaseProps, type ControlIconProps } from './Control export type CheckboxBaseProps = Omit< ControlBaseProps, - 'controlColor' + 'controlColor' | 'controlSize' | 'dotSize' > & { /** * Sets the checked/active color of the checkbox. @@ -23,6 +23,11 @@ export type CheckboxBaseProps = Omit< * @default 100 */ borderWidth?: ThemeVars.BorderWidth; + /** + * Sets the outer checkbox control size in pixels. + * @default theme.controlSize.checkboxSize + */ + controlSize?: number; }; export type CheckboxProps = CheckboxBaseProps; @@ -42,10 +47,11 @@ const CheckboxIcon = memo( animatedScaleValue, animatedOpacityValue, testID, + controlSize, }: React.PropsWithChildren) => { const filled = checked || indeterminate; const theme = useTheme(); - const checkboxSize = theme.controlSize.checkboxSize; + const checkboxSize = controlSize ?? theme.controlSize.checkboxSize; const iconPadding = checkboxSize / 5; const iconSize = checkboxSize - iconPadding; diff --git a/packages/mobile/src/controls/CheckboxCell.tsx b/packages/mobile/src/controls/CheckboxCell.tsx index 92863a3f2..5c1c704d0 100644 --- a/packages/mobile/src/controls/CheckboxCell.tsx +++ b/packages/mobile/src/controls/CheckboxCell.tsx @@ -27,7 +27,7 @@ export type CheckboxCellBaseProps = { rowGap?: ThemeVars.Space; pressedBorderColor?: ThemeVars.Color; pressedBorderWidth?: ThemeVars.BorderWidth; -} & Omit, 'style' | 'children' | 'title'> & +} & Omit, 'style' | 'children' | 'title' | 'dotSize'> & Omit; export type CheckboxCellProps = @@ -61,6 +61,7 @@ const CheckboxCellWithRef = forwardRef(function CheckboxCell = Omit< controlColor?: ThemeVars.Color; /** Sets the elevation/drop shadow of the control. */ elevation?: ElevationLevels; + /** + * Sets the control size in pixels. + */ + controlSize?: number; + /** + * Sets the inner dot size in pixels. + * @default 2/3 of controlSize + */ + dotSize?: number; style?: ViewStyle; }; @@ -111,6 +122,8 @@ const ControlWithRef = forwardRef(function ControlWithRef, ref: React.ForwardedRef, @@ -230,7 +243,9 @@ const ControlWithRef = forwardRef(function ControlWithRef = Omit< ControlBaseProps, - 'controlColor' + 'controlColor' | 'controlSize' | 'dotSize' > & { /** * Sets the checked/active color of the radio. @@ -30,14 +30,32 @@ export type RadioBaseProps = Omit< * @default 100 */ borderWidth?: ThemeVars.BorderWidth; + /** + * Sets the outer radio control size in pixels. + * @default theme.controlSize.radioSize + */ + controlSize?: number; + /** + * Sets the inner dot size in pixels. + * @default 2/3 of controlSize + */ + dotSize?: number; }; export type RadioProps = RadioBaseProps; -const DotSvg = ({ color = 'black', width = 20 }: { color?: ColorValue; width?: number }) => { +const DotSvg = ({ + color = 'black', + width = 20, + dotSize = (2 * width) / 3, +}: { + color?: ColorValue; + width?: number; + dotSize?: number; +}) => { return ( - + ); }; @@ -53,11 +71,13 @@ const RadioIcon: React.FC> = ({ animatedScaleValue, animatedOpacityValue, controlColor = 'bgPrimary', + controlSize, + dotSize, borderColor = checked ? controlColor : 'bgLineHeavy', testID, }) => { const theme = useTheme(); - const radioSize = theme.controlSize.radioSize; + const radioSize = controlSize ?? theme.controlSize.radioSize; return ( > = ({ style={{ transform: [{ scale: animatedScaleValue }], opacity: animatedOpacityValue }} > - + @@ -96,7 +116,6 @@ const RadioWithRef = forwardRef(function Radio( typeof children === 'string' && accessibilityLabel === undefined ? children : accessibilityLabel; - return ( {...props} diff --git a/packages/mobile/src/controls/RadioCell.tsx b/packages/mobile/src/controls/RadioCell.tsx index bce33a9e1..d552be3e6 100644 --- a/packages/mobile/src/controls/RadioCell.tsx +++ b/packages/mobile/src/controls/RadioCell.tsx @@ -27,7 +27,7 @@ export type RadioCellBaseProps = { rowGap?: ThemeVars.Space; pressedBorderColor?: ThemeVars.Color; pressedBorderWidth?: ThemeVars.BorderWidth; -} & Omit, 'style' | 'children' | 'title'> & +} & Omit, 'style' | 'children' | 'title' | 'controlSize' | 'dotSize'> & Omit; export type RadioCellProps = RadioCellBaseProps & { diff --git a/packages/mobile/src/controls/Switch.tsx b/packages/mobile/src/controls/Switch.tsx index b9eb3ef7e..b57d06b4f 100644 --- a/packages/mobile/src/controls/Switch.tsx +++ b/packages/mobile/src/controls/Switch.tsx @@ -9,7 +9,7 @@ import { Control, type ControlBaseProps, type ControlIconProps } from './Control export type SwitchBaseProps = Omit< ControlBaseProps, - 'style' + 'style' | 'controlSize' | 'dotSize' >; export type SwitchProps = SwitchBaseProps; diff --git a/packages/mobile/src/controls/__tests__/Checkbox.test.tsx b/packages/mobile/src/controls/__tests__/Checkbox.test.tsx index 90e136945..ecd13f3a2 100644 --- a/packages/mobile/src/controls/__tests__/Checkbox.test.tsx +++ b/packages/mobile/src/controls/__tests__/Checkbox.test.tsx @@ -48,6 +48,21 @@ describe('Checkbox', () => { expect(screen.getByTestId('mock-checkbox')).toBeAccessible(); }); + it('applies controlSize to checkbox container', () => { + render( + + + Checked + + , + ); + + expect(screen.getByTestId('test-checkbox')).toHaveStyle({ + width: 60, + height: 60, + }); + }); + it('renders a minus icon when indeterminate', () => { render( diff --git a/packages/mobile/src/controls/__tests__/RadioGroup.test.tsx b/packages/mobile/src/controls/__tests__/RadioGroup.test.tsx index f03d66f8c..08644fd32 100644 --- a/packages/mobile/src/controls/__tests__/RadioGroup.test.tsx +++ b/packages/mobile/src/controls/__tests__/RadioGroup.test.tsx @@ -1,4 +1,5 @@ import { Pressable } from 'react-native'; +import { Circle } from 'react-native-svg'; import { fireEvent, render, screen } from '@testing-library/react-native'; import { Text } from '../../typography/Text'; @@ -158,4 +159,41 @@ describe('Radio', () => { borderColor: 'rgb(9,133,81)', // This corresponds to bgPositive in defaultTheme }); }); + + it('applies controlSize to radio container', () => { + render( + + + Radio + + , + ); + + expect(screen.getByTestId('test-radio')).toHaveStyle({ + width: 60, + height: 60, + }); + }); + + it('defaults dotSize to two thirds of controlSize and supports explicit dotSize', () => { + const { rerender } = render( + + + Radio + + , + ); + + expect(screen.UNSAFE_getByType(Circle).props.r).toBe(20); + + rerender( + + + Radio + + , + ); + + expect(screen.UNSAFE_getByType(Circle).props.r).toBe(15); + }); }); diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index 300026171..11379be82 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file. +## 8.59.0 (3/27/2026 PST) + +#### 🚀 Updates + +- Suppoer controlSize on Checkbox and Radio. [[#546](https://github.com/coinbase/cds/pull/546)] + ## 8.58.0 (3/25/2026 PST) #### 🚀 Updates diff --git a/packages/web/package.json b/packages/web/package.json index 11382cf19..893201da3 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-web", - "version": "8.58.0", + "version": "8.59.0", "description": "Coinbase Design System - Web", "repository": { "type": "git", diff --git a/packages/web/src/controls/Checkbox.tsx b/packages/web/src/controls/Checkbox.tsx index 391c93bbf..c16ab1084 100644 --- a/packages/web/src/controls/Checkbox.tsx +++ b/packages/web/src/controls/Checkbox.tsx @@ -18,9 +18,6 @@ import { Control, type ControlBaseProps } from './Control'; const checkboxCss = css` position: relative; - width: var(--controlSize-checkboxSize); - height: var(--controlSize-checkboxSize); - border-style: solid; transition: @@ -50,6 +47,11 @@ export type CheckboxBaseProps = ControlBaseProps = CheckboxBaseProps; @@ -65,13 +67,14 @@ const CheckboxWithRef = forwardRef(function CheckboxWithRef, ref: React.ForwardedRef, ) { const filled = checked || indeterminate; const theme = useTheme(); - const checkboxSize = theme.controlSize.checkboxSize; + const checkboxSize = controlSize ?? theme.controlSize.checkboxSize; const iconPadding = checkboxSize / 5; const iconSize = checkboxSize - iconPadding; @@ -116,6 +119,7 @@ const CheckboxWithRef = forwardRef(function CheckboxWithRef diff --git a/packages/web/src/controls/CheckboxCell.tsx b/packages/web/src/controls/CheckboxCell.tsx index df98784b3..aa10cee69 100644 --- a/packages/web/src/controls/CheckboxCell.tsx +++ b/packages/web/src/controls/CheckboxCell.tsx @@ -21,6 +21,11 @@ export type CheckboxCellBaseProps = Omit< 'onChange' | 'title' | 'children' | 'iconStyle' | 'labelStyle' | 'checked' > & { checked?: boolean; + /** + * Sets the outer checkbox control size in pixels. + * @default theme.controlSize.checkboxSize + */ + controlSize?: number; title: React.ReactNode; description?: React.ReactNode; onChange?: (inputChangeEvent: React.ChangeEvent) => void; @@ -87,6 +92,7 @@ const CheckboxCellWithRef = forwardRef(function CheckboxCell { return ( - + ); }; @@ -32,8 +34,6 @@ const DotSvg = ({ const baseCss = css` position: relative; appearance: radio; - width: var(--controlSize-radioSize); - height: var(--controlSize-radioSize); border-style: solid; border-radius: var(--borderRadius-1000); @@ -62,6 +62,16 @@ export type RadioBaseProps = ControlBaseProps = RadioBaseProps; @@ -75,12 +85,14 @@ const RadioWithRef = forwardRef(function RadioWithRef borderColor = checked ? controlColor : 'bgLineHeavy', borderWidth = 100, elevation, + controlSize, + dotSize, ...props }: RadioProps, ref: React.ForwardedRef, ) { const theme = useTheme(); - const iconWidth = theme.controlSize.radioSize; + const iconWidth = controlSize ?? theme.controlSize.radioSize; const innerContainerMotionProps = useMotionProps({ enterConfigs: [checkboxOpacityEnterConfig, checkboxScaleEnterConfig], @@ -107,12 +119,13 @@ const RadioWithRef = forwardRef(function RadioWithRef flexShrink={0} justifyContent="center" role="presentation" + style={{ width: iconWidth, height: iconWidth }} > {checked && ( // setting inner dot to match color of the radio outline - + )} diff --git a/packages/web/src/controls/__tests__/Checkbox.test.tsx b/packages/web/src/controls/__tests__/Checkbox.test.tsx index 7bceb7fa8..851a28630 100644 --- a/packages/web/src/controls/__tests__/Checkbox.test.tsx +++ b/packages/web/src/controls/__tests__/Checkbox.test.tsx @@ -93,6 +93,23 @@ describe('Checkbox', () => { expect(icon.className).toContain('bgNegative'); }); + it('applies controlSize to checkbox container', () => { + render( + + + Checked + + , + ); + + const outline = screen.getByTestId('checkbox-outer'); + + expect(outline).toHaveStyle({ + width: '60px', + height: '60px', + }); + }); + it('uses bg color when unchecked regardless of controlColor prop', () => { render( diff --git a/packages/web/src/controls/__tests__/RadioGroup.test.tsx b/packages/web/src/controls/__tests__/RadioGroup.test.tsx index 295b2f2a2..3ce54fa35 100644 --- a/packages/web/src/controls/__tests__/RadioGroup.test.tsx +++ b/packages/web/src/controls/__tests__/RadioGroup.test.tsx @@ -81,6 +81,50 @@ describe('RadioGroup.test', () => { }); }); + it('applies controlSize to radio container', () => { + render( + + + , + ); + + const radio = screen.getByTestId('test-radio-parent'); + const outlineElement = within(radio).getByRole('presentation'); + + expect(outlineElement).toHaveStyle({ + width: '60px', + height: '60px', + }); + }); + + it('defaults dotSize to two thirds of controlSize', () => { + render( + + + , + ); + + const radio = screen.getByTestId('test-radio-parent'); + const dotElement = within(radio).getByTestId('radio-icon'); + const circle = dotElement.querySelector('circle'); + + expect(circle).toHaveAttribute('r', '20'); + }); + + it('uses explicit dotSize when provided', () => { + render( + + + , + ); + + const radio = screen.getByTestId('test-radio-parent'); + const dotElement = within(radio).getByTestId('radio-icon'); + const circle = dotElement.querySelector('circle'); + + expect(circle).toHaveAttribute('r', '15'); + }); + it('renders options', () => { render(