diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 2e040901..131ada73 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -7,6 +7,7 @@ "@material-tailwind/react": "^2.1.7", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", diff --git a/packages/frontend/src/components/shared/Input/Input.theme.ts b/packages/frontend/src/components/shared/Input/Input.theme.ts index 8def6a0d..bf5ce915 100644 --- a/packages/frontend/src/components/shared/Input/Input.theme.ts +++ b/packages/frontend/src/components/shared/Input/Input.theme.ts @@ -50,6 +50,7 @@ export const inputTheme = tv( 'outline-offset-0', 'outline-border-danger', 'shadow-none', + 'focus:outline-border-danger', ], helperText: 'text-elements-danger', }, diff --git a/packages/frontend/src/components/shared/Switch/Switch.theme.ts b/packages/frontend/src/components/shared/Switch/Switch.theme.ts new file mode 100644 index 00000000..87fbd092 --- /dev/null +++ b/packages/frontend/src/components/shared/Switch/Switch.theme.ts @@ -0,0 +1,84 @@ +import { tv, type VariantProps } from 'tailwind-variants'; + +export const switchTheme = tv({ + slots: { + wrapper: ['flex', 'items-start', 'gap-4', 'w-[375px]'], + switch: [ + 'h-6', + 'w-12', + 'rounded-full', + 'transition-all', + 'duration-500', + 'relative', + 'cursor-default', + 'shadow-inset', + 'focus-ring', + 'outline-none', + ], + thumb: [ + 'block', + 'h-4', + 'w-4', + 'translate-x-1', + 'transition-transform', + 'duration-100', + 'will-change-transform', + 'rounded-full', + 'shadow-button', + 'data-[state=checked]:translate-x-7', + 'bg-controls-elevated', + ], + label: [ + 'flex', + 'flex-1', + 'flex-col', + 'px-1', + 'gap-1', + 'text-sm', + 'text-elements-high-em', + 'tracking-[-0.006em]', + ], + description: ['text-xs', 'text-elements-low-em'], + }, + variants: { + checked: { + true: { + switch: [ + 'bg-controls-primary', + 'hover:bg-controls-primary-hovered', + 'focus-visible:bg-controls-primary-hovered', + ], + }, + false: { + switch: [ + 'bg-controls-inset', + 'hover:bg-controls-inset-hovered', + 'focus-visible:bg-controls-inset-hovered', + ], + }, + }, + disabled: { + true: { + switch: ['bg-controls-disabled', 'cursor-not-allowed'], + thumb: ['bg-elements-on-disabled'], + }, + }, + fullWidth: { + true: { + wrapper: ['w-full', 'justify-between'], + }, + }, + }, + compoundVariants: [ + { + checked: true, + disabled: true, + class: { + switch: ['bg-controls-disabled-active'], + thumb: ['bg-snowball-900'], + }, + }, + ], +}); + +export type SwitchVariants = VariantProps; diff --git a/packages/frontend/src/components/shared/Switch/Switch.tsx b/packages/frontend/src/components/shared/Switch/Switch.tsx new file mode 100644 index 00000000..32d35957 --- /dev/null +++ b/packages/frontend/src/components/shared/Switch/Switch.tsx @@ -0,0 +1,85 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; +import { type SwitchProps as SwitchRadixProps } from '@radix-ui/react-switch'; +import * as SwitchRadix from '@radix-ui/react-switch'; + +import { switchTheme, type SwitchVariants } from './Switch.theme'; + +interface SwitchProps + extends Omit, + SwitchVariants { + /** + * The label of the switch. + */ + label?: string; + /** + * The description of the switch. + */ + description?: string; + /** + * Custom wrapper props for the switch. + */ + wrapperProps?: ComponentPropsWithoutRef<'div'>; + /** + * Function that is called when the checked state of the switch changes. + * @param checked The new checked state of the switch. + */ + onCheckedChange?(checked: boolean): void; +} + +/** + * A switch is a component used for toggling between two states. + */ +export const Switch = ({ + className, + checked, + label, + description, + disabled, + name, + wrapperProps, + fullWidth, + ...props +}: SwitchProps) => { + const { + wrapper, + switch: switchClass, + thumb, + label: labelClass, + description: descriptionClass, + } = switchTheme({ + checked, + disabled, + fullWidth, + }); + + const switchComponent = ( + + + + ); + + // If a label is provided, wrap the switch in a label element. + if (label) { + return ( +
+ + {switchComponent} +
+ ); + } + + return switchComponent; +}; diff --git a/packages/frontend/src/components/shared/Switch/index.ts b/packages/frontend/src/components/shared/Switch/index.ts new file mode 100644 index 00000000..1b19c1d3 --- /dev/null +++ b/packages/frontend/src/components/shared/Switch/index.ts @@ -0,0 +1 @@ +export * from './Switch'; diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index ed51dc5d..88ddb371 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -20,6 +20,7 @@ import { } from './renders/tabs'; import { SegmentedControls } from 'components/shared/SegmentedControls'; import { SEGMENTED_CONTROLS_OPTIONS } from './renders/segmentedControls'; +import { Switch } from 'components/shared/Switch'; import { RADIO_OPTIONS } from './renders/radio'; import { Radio } from 'components/shared/Radio'; import { @@ -34,6 +35,7 @@ const Page = () => { const [dateRange, setDateRange] = useState(); const [selectedSegmentedControl, setSelectedSegmentedControl] = useState('Test 1'); + const [switchValue, setSwitchValue] = useState(false); const [selectedRadio, setSelectedRadio] = useState(''); return ( @@ -187,6 +189,28 @@ const Page = () => {
+ {/* Switch */} +
+

Switch

+
+ + + + +
+
+ +
+ {/* Radio */}

Radio

diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js index 9a814026..cce3380a 100644 --- a/packages/frontend/tailwind.config.js +++ b/packages/frontend/tailwind.config.js @@ -154,6 +154,7 @@ export default withMT({ calendar: '0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)', field: '0px 1px 2px rgba(0, 0, 0, 0.04)', + inset: 'inset 0px 1px 0px rgba(8, 47, 86, 0.06)', }, spacing: { 2.5: '0.625rem', diff --git a/yarn.lock b/yarn.lock index b789155b..f6800333 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3480,6 +3480,20 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" +"@radix-ui/react-switch@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e" + integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-previous" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/react-tabs@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2"