From f05d46c24b610b816a18f133e27d3dd16b46e837 Mon Sep 17 00:00:00 2001 From: Hunter Copp Date: Wed, 25 Mar 2026 15:27:35 -0400 Subject: [PATCH 1/5] feat: customize radio controlSize & dotSize --- packages/mobile/src/controls/Checkbox.tsx | 2 +- packages/mobile/src/controls/CheckboxCell.tsx | 5 ++- packages/mobile/src/controls/Control.tsx | 17 ++++++++++ packages/mobile/src/controls/Radio.tsx | 31 +++++++++++++++---- packages/mobile/src/controls/RadioCell.tsx | 2 +- packages/mobile/src/controls/Switch.tsx | 2 +- packages/web/src/controls/Radio.tsx | 23 +++++++++++--- 7 files changed, 67 insertions(+), 15 deletions(-) diff --git a/packages/mobile/src/controls/Checkbox.tsx b/packages/mobile/src/controls/Checkbox.tsx index 0d91a87447..ea170edde9 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. diff --git a/packages/mobile/src/controls/CheckboxCell.tsx b/packages/mobile/src/controls/CheckboxCell.tsx index 92863a3f2f..28773883a6 100644 --- a/packages/mobile/src/controls/CheckboxCell.tsx +++ b/packages/mobile/src/controls/CheckboxCell.tsx @@ -27,7 +27,10 @@ export type CheckboxCellBaseProps = { rowGap?: ThemeVars.Space; pressedBorderColor?: ThemeVars.Color; pressedBorderWidth?: ThemeVars.BorderWidth; -} & Omit, 'style' | 'children' | 'title'> & +} & Omit< + ControlBaseProps, + 'style' | 'children' | 'title' | 'controlSize' | 'dotSize' +> & Omit; export type CheckboxCellProps = diff --git a/packages/mobile/src/controls/Control.tsx b/packages/mobile/src/controls/Control.tsx index cf802520ba..a73153764c 100644 --- a/packages/mobile/src/controls/Control.tsx +++ b/packages/mobile/src/controls/Control.tsx @@ -34,6 +34,8 @@ export type ControlIconProps = SharedProps & { borderRadius?: ThemeVars.BorderRadius; borderWidth?: ThemeVars.BorderWidth; elevation?: ElevationLevels; + controlSize?: number; + dotSize?: number; animatedScaleValue: Animated.Value; animatedOpacityValue: Animated.Value; accessible?: boolean; @@ -71,6 +73,15 @@ export type ControlBaseProps = 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 bce33a9e11..d552be3e6c 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 b9eb3ef7ec..b57d06b4fd 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/web/src/controls/Radio.tsx b/packages/web/src/controls/Radio.tsx index 27fc005015..3721c87030 100644 --- a/packages/web/src/controls/Radio.tsx +++ b/packages/web/src/controls/Radio.tsx @@ -18,13 +18,15 @@ import { Control, type ControlBaseProps } from './Control'; const DotSvg = ({ color = 'black', width = 20, + dotSize = (2 * width) / 3, }: { color?: React.CSSProperties['color']; width?: number; + dotSize?: number; }) => { 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 - + )} From 2730fdc77f53414492b7a6d2e6e58573947dbdf8 Mon Sep 17 00:00:00 2001 From: Hunter Copp Date: Wed, 25 Mar 2026 15:47:43 -0400 Subject: [PATCH 2/5] Update tests --- .../controls/__tests__/RadioGroup.test.tsx | 38 ++++++++++++++++ .../controls/__tests__/RadioGroup.test.tsx | 44 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/packages/mobile/src/controls/__tests__/RadioGroup.test.tsx b/packages/mobile/src/controls/__tests__/RadioGroup.test.tsx index f03d66f8cc..3abec81546 100644 --- a/packages/mobile/src/controls/__tests__/RadioGroup.test.tsx +++ b/packages/mobile/src/controls/__tests__/RadioGroup.test.tsx @@ -1,5 +1,6 @@ import { Pressable } from 'react-native'; import { fireEvent, render, screen } from '@testing-library/react-native'; +import { Circle } from 'react-native-svg'; import { Text } from '../../typography/Text'; import { DefaultThemeProvider } from '../../utils/testHelpers'; @@ -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/src/controls/__tests__/RadioGroup.test.tsx b/packages/web/src/controls/__tests__/RadioGroup.test.tsx index 295b2f2a27..3ce54fa356 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( From caa9e957d720631cff1fa11e9ef75726ab459140 Mon Sep 17 00:00:00 2001 From: Hunter Copp Date: Thu, 26 Mar 2026 15:06:41 -0400 Subject: [PATCH 3/5] Fix lint --- packages/mobile/src/controls/__tests__/RadioGroup.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mobile/src/controls/__tests__/RadioGroup.test.tsx b/packages/mobile/src/controls/__tests__/RadioGroup.test.tsx index 3abec81546..08644fd326 100644 --- a/packages/mobile/src/controls/__tests__/RadioGroup.test.tsx +++ b/packages/mobile/src/controls/__tests__/RadioGroup.test.tsx @@ -1,6 +1,6 @@ import { Pressable } from 'react-native'; -import { fireEvent, render, screen } from '@testing-library/react-native'; import { Circle } from 'react-native-svg'; +import { fireEvent, render, screen } from '@testing-library/react-native'; import { Text } from '../../typography/Text'; import { DefaultThemeProvider } from '../../utils/testHelpers'; From a5d1c4681ca4a4eb14a5bf33be192096aaba904d Mon Sep 17 00:00:00 2001 From: Hunter Copp Date: Thu, 26 Mar 2026 16:48:58 -0400 Subject: [PATCH 4/5] Support checkbox customization --- packages/mobile/src/controls/Checkbox.tsx | 8 +++++++- packages/mobile/src/controls/CheckboxCell.tsx | 7 +++---- .../src/controls/__tests__/Checkbox.test.tsx | 15 +++++++++++++++ packages/web/src/controls/Checkbox.tsx | 12 ++++++++---- packages/web/src/controls/CheckboxCell.tsx | 7 +++++++ .../src/controls/__tests__/Checkbox.test.tsx | 17 +++++++++++++++++ 6 files changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/mobile/src/controls/Checkbox.tsx b/packages/mobile/src/controls/Checkbox.tsx index ea170edde9..0ec37a78cc 100644 --- a/packages/mobile/src/controls/Checkbox.tsx +++ b/packages/mobile/src/controls/Checkbox.tsx @@ -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 28773883a6..5c1c704d08 100644 --- a/packages/mobile/src/controls/CheckboxCell.tsx +++ b/packages/mobile/src/controls/CheckboxCell.tsx @@ -27,10 +27,7 @@ export type CheckboxCellBaseProps = { rowGap?: ThemeVars.Space; pressedBorderColor?: ThemeVars.Color; pressedBorderWidth?: ThemeVars.BorderWidth; -} & Omit< - ControlBaseProps, - 'style' | 'children' | 'title' | 'controlSize' | 'dotSize' -> & +} & Omit, 'style' | 'children' | 'title' | 'dotSize'> & Omit; export type CheckboxCellProps = @@ -64,6 +61,7 @@ const CheckboxCellWithRef = forwardRef(function CheckboxCell { 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/web/src/controls/Checkbox.tsx b/packages/web/src/controls/Checkbox.tsx index 391c93bbfd..c16ab1084b 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 df98784b33..aa10cee694 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 { 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( From 649deecc9d8395b4826464ad6d07563b540de0a7 Mon Sep 17 00:00:00 2001 From: Hunter Copp Date: Fri, 27 Mar 2026 08:47:38 -0400 Subject: [PATCH 5/5] Bump version --- packages/common/CHANGELOG.md | 4 ++++ packages/common/package.json | 2 +- packages/mcp-server/CHANGELOG.md | 4 ++++ packages/mcp-server/package.json | 2 +- packages/mobile/CHANGELOG.md | 6 ++++++ packages/mobile/package.json | 2 +- packages/web/CHANGELOG.md | 6 ++++++ packages/web/package.json | 2 +- 8 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index 0579e7b2d1..db4efc6448 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 10a4f8e0aa..3de98d2151 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 770b1b9ff3..78d7c07042 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 d2ac24a62f..697a23b3db 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 eefb2abce6..1573ed87bc 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 46c77dba8f..ce059f0866 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/web/CHANGELOG.md b/packages/web/CHANGELOG.md index 3000261714..11379be821 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 11382cf198..893201da3c 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",