From 31f06cd59673f885c12a0834acf37244739ae56e Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Mon, 13 Oct 2025 12:15:44 +0200 Subject: [PATCH 1/3] feat(Switch): new sizes --- .changeset/clever-gifts-crash.md | 5 +++ .../fields/Switch/Switch.stories.tsx | 38 +++++++++++++++---- src/components/fields/Switch/Switch.tsx | 31 +++++++++------ 3 files changed, 55 insertions(+), 19 deletions(-) create mode 100644 .changeset/clever-gifts-crash.md diff --git a/.changeset/clever-gifts-crash.md b/.changeset/clever-gifts-crash.md new file mode 100644 index 000000000..5fc8722ac --- /dev/null +++ b/.changeset/clever-gifts-crash.md @@ -0,0 +1,5 @@ +--- +"@cube-dev/ui-kit": minor +--- + +New Switch sizes: `small` -> `medium` (and now default). new `small` size. diff --git a/src/components/fields/Switch/Switch.stories.tsx b/src/components/fields/Switch/Switch.stories.tsx index bee4e5d92..a3d207e10 100644 --- a/src/components/fields/Switch/Switch.stories.tsx +++ b/src/components/fields/Switch/Switch.stories.tsx @@ -82,7 +82,7 @@ export default { /* Presentation */ size: { - options: ['small', 'large'], + options: ['small', 'medium', 'large'], control: { type: 'radio' }, description: 'Switch size', table: { @@ -127,12 +127,6 @@ WithDefaultSelected.args = { defaultSelected: true, }; -export const Small = Template.bind({}); -Small.args = { - children: 'Small switch', - size: 'small', -}; - export const Invalid = Template.bind({}); Invalid.args = { children: 'Required switch', @@ -152,6 +146,36 @@ Loading.args = { isLoading: true, }; +// Stories showing all sizes for visual comparison +const SizesTemplate: StoryFn = (props) => ( +
+ console.log('small change', isSelected)} + > + Small switch + + console.log('medium change', isSelected)} + > + Medium switch + + console.log('large change', isSelected)} + > + Large switch + +
+); + +export const Sizes = SizesTemplate.bind({}); +Sizes.args = {}; + // Stories showing both selected and unselected states for visual testing const MultiStateTemplate: StoryFn = (props) => (
diff --git a/src/components/fields/Switch/Switch.tsx b/src/components/fields/Switch/Switch.tsx index a5001c597..91331d701 100644 --- a/src/components/fields/Switch/Switch.tsx +++ b/src/components/fields/Switch/Switch.tsx @@ -60,16 +60,18 @@ const SwitchElement = tasty({ }, border: { '': '#dark-05', - checked: '#purple', + checked: '#purple-text', disabled: '#dark-05', }, width: { - '': '5.25x 5.25x', - '[data-size="small"]': '4x 4x', + '': '5x 5x', + '[data-size="medium"]': '4x 4x', + '[data-size="small"]': '3x 3x', }, height: { '': '3x 3x', - '[data-size="small"]': '2.5x 2.5x', + '[data-size="medium"]': '2.5x 2.5x', + '[data-size="small"]': '2x 2x', }, outline: { '': '#purple-text.0', @@ -87,11 +89,13 @@ const SwitchElement = tasty({ position: 'absolute', width: { '': '2x 2x', - '[data-size="small"]': '1.5x 1.5x', + '[data-size="medium"]': '1.5x 1.5x', + '[data-size="small"]': '1.25x 1.25x', }, height: { '': '2x 2x', - '[data-size="small"]': '1.5x 1.5x', + '[data-size="medium"]': '1.5x 1.5x', + '[data-size="small"]': '1.25x 1.25x', }, radius: 'round', fill: { @@ -100,13 +104,16 @@ const SwitchElement = tasty({ }, top: { '': '.375x', - '[data-size="small"]': '.375x', + '[data-size="medium"]': '.375x', + '[data-size="small"]': '.25x', }, left: { '': '.375x', - '[data-size="small"]': '.375x', - checked: '2.5x', - 'checked & [data-size="small"]': '1.75x', + '[data-size="medium"]': '.375x', + '[data-size="small"]': '.25x', + checked: '2.25x', + 'checked & [data-size="medium"]': '1.75x', + 'checked & [data-size="small"]': '1.25x', }, transition: 'left, theme', cursor: 'pointer', @@ -122,7 +129,7 @@ export interface CubeSwitchProps AriaSwitchProps { inputStyles?: Styles; isLoading?: boolean; - size?: 'large' | 'small'; + size?: 'large' | 'medium' | 'small'; } function Switch(props: WithNullableSelected, ref) { @@ -149,7 +156,7 @@ function Switch(props: WithNullableSelected, ref) { labelPosition, inputStyles, validationState, - size = 'large', + size = 'medium', } = props; let styles = extractStyles(props, OUTER_STYLES); From 7f720d906622a2bed1a027e608b4f8f77f26b08d Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Mon, 13 Oct 2025 12:59:11 +0200 Subject: [PATCH 2/3] fix(Switch): connect label by id --- .../fields/Switch/Switch.stories.tsx | 45 +++++-------------- src/components/fields/Switch/Switch.tsx | 27 ++++++----- 2 files changed, 26 insertions(+), 46 deletions(-) diff --git a/src/components/fields/Switch/Switch.stories.tsx b/src/components/fields/Switch/Switch.stories.tsx index a3d207e10..c282698d5 100644 --- a/src/components/fields/Switch/Switch.stories.tsx +++ b/src/components/fields/Switch/Switch.stories.tsx @@ -92,29 +92,21 @@ export default { /* Events */ onChange: { - action: 'change', description: 'Callback fired when the switch value changes', control: { type: null }, }, onFocus: { - action: 'focus', description: 'Callback fired when the switch receives focus', control: { type: null }, }, onBlur: { - action: 'blur', description: 'Callback fired when the switch loses focus', control: { type: null }, }, }, }; -const Template: StoryFn = (props) => ( - console.log('change', isSelected)} - /> -); +const Template: StoryFn = (props) => ; export const Default = Template.bind({}); Default.args = { @@ -146,28 +138,21 @@ Loading.args = { isLoading: true, }; +export const WithLabel = Template.bind({}); +WithLabel.args = { + label: 'Toggle feature', +}; + // Stories showing all sizes for visual comparison const SizesTemplate: StoryFn = (props) => (
- console.log('small change', isSelected)} - > + Small switch - console.log('medium change', isSelected)} - > + Medium switch - console.log('large change', isSelected)} - > + Large switch
@@ -179,18 +164,10 @@ Sizes.args = {}; // Stories showing both selected and unselected states for visual testing const MultiStateTemplate: StoryFn = (props) => (
- console.log('unselected change', isSelected)} - > + {props.children} (unselected) - console.log('selected change', isSelected)} - > + {props.children} (selected)
diff --git a/src/components/fields/Switch/Switch.tsx b/src/components/fields/Switch/Switch.tsx index 91331d701..e1cfb1baf 100644 --- a/src/components/fields/Switch/Switch.tsx +++ b/src/components/fields/Switch/Switch.tsx @@ -22,6 +22,7 @@ import { castNullableIsSelected, WithNullableSelected, } from '../../../utils/react/nullableValue'; +import { useId } from '../../../utils/react/useId'; import { Text } from '../../content/Text'; import { useFieldProps, useFormProps, wrapWithField } from '../../form'; import { HiddenInput } from '../../HiddenInput'; @@ -82,7 +83,7 @@ const SwitchElement = tasty({ cursor: 'pointer', shadow: { '': '0 0 0 0 #clear', - invalid: '0 0 0 1bw #white, 0 0 0 1ow #danger', + invalid: '0 0 0 1bw #white, 0 0 0 2bw #danger', }, Thumb: { @@ -128,6 +129,7 @@ export interface CubeSwitchProps FieldBaseProps, AriaSwitchProps { inputStyles?: Styles; + fieldStyles?: Styles; isLoading?: boolean; size?: 'large' | 'medium' | 'small'; } @@ -155,10 +157,13 @@ function Switch(props: WithNullableSelected, ref) { isLoading, labelPosition, inputStyles, + fieldStyles, validationState, size = 'medium', } = props; + const id = useId(props.id); + let styles = extractStyles(props, OUTER_STYLES); inputStyles = extractStyles(props, BLOCK_STYLES, inputStyles); @@ -169,16 +174,7 @@ function Switch(props: WithNullableSelected, ref) { let inputRef = useRef(null); let domRef = useFocusableRef(ref, inputRef); - let { inputProps } = useSwitch( - { - ...props, - ...(typeof label === 'string' && label.trim() - ? { 'aria-label': label } - : {}), - }, - useToggleState(props), - inputRef, - ); + let { inputProps } = useSwitch(props, useToggleState(props), inputRef); const mods = { checked: inputProps.checked, @@ -196,12 +192,14 @@ function Switch(props: WithNullableSelected, ref) { qa={qa || 'SwitchWrapper'} mods={mods} data-size={size} + styles={styles} {...hoverProps} > , ref) { return wrapWithField(switchField, domRef, { ...props, + id, + labelProps: { + ...props.labelProps, + for: id, + }, children: null, labelStyles, inputStyles, - styles, + styles: fieldStyles, }); } From e7f723467d9827c1cf9b90441c48e3576ac53e6c Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Mon, 13 Oct 2025 13:09:09 +0200 Subject: [PATCH 3/3] fix(Switch): disabled state styles --- src/components/fields/Switch/Switch.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/fields/Switch/Switch.tsx b/src/components/fields/Switch/Switch.tsx index e1cfb1baf..17803a2a3 100644 --- a/src/components/fields/Switch/Switch.tsx +++ b/src/components/fields/Switch/Switch.tsx @@ -53,7 +53,8 @@ const SwitchElement = tasty({ fill: { '': '#white', checked: '#purple', - disabled: '#border', + disabled: '#border.5', + 'disabled & checked': '#border', }, color: { '': '#dark-03', @@ -62,7 +63,8 @@ const SwitchElement = tasty({ border: { '': '#dark-05', checked: '#purple-text', - disabled: '#dark-05', + disabled: '#dark-05.5', + invalid: '#danger', }, width: { '': '5x 5x', @@ -81,10 +83,6 @@ const SwitchElement = tasty({ outlineOffset: 1, transition: 'theme', cursor: 'pointer', - shadow: { - '': '0 0 0 0 #clear', - invalid: '0 0 0 1bw #white, 0 0 0 2bw #danger', - }, Thumb: { position: 'absolute', @@ -101,7 +99,8 @@ const SwitchElement = tasty({ radius: 'round', fill: { '': 'currentColor', - disabled: '#white.7', + disabled: '#current.5', + 'disabled & checked': '#white.8', }, top: { '': '.375x',