diff --git a/package.json b/package.json index c6eab9009..bb05441cc 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-switch": "^1.1.2", "@t3-oss/env-core": "^0.11.1", "@tanstack/react-query": "^5.62.11", "@tanstack/react-virtual": "^3.11.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6b74ea25..c7fde06b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@hookform/resolvers': specifier: ^3.9.1 version: 3.9.1(react-hook-form@7.54.2(react@18.3.1)) + '@radix-ui/react-switch': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@t3-oss/env-core': specifier: ^0.11.1 version: 0.11.1(typescript@5.6.3)(zod@3.24.1) @@ -962,6 +965,107 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-primitive@2.0.1': + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.1.1': + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.1.2': + resolution: {integrity: sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@rollup/pluginutils@5.1.4': resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} engines: {node: '>=14.0.0'} @@ -5064,6 +5168,83 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@radix-ui/primitive@1.1.1': {} + + '@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-context@1.1.1(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-primitive@2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-slot@1.1.1(@types/react@18.3.18)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-switch@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-use-previous@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + '@rollup/pluginutils@5.1.4(rollup@4.29.1)': dependencies: '@types/estree': 1.0.6 diff --git a/src/ui/switch/index.ts b/src/ui/switch/index.ts new file mode 100644 index 000000000..175be8b98 --- /dev/null +++ b/src/ui/switch/index.ts @@ -0,0 +1 @@ +export { Switch, type SwitchProps } from './switch.tsx'; diff --git a/src/ui/switch/switch.stories.tsx b/src/ui/switch/switch.stories.tsx new file mode 100644 index 000000000..acff96e29 --- /dev/null +++ b/src/ui/switch/switch.stories.tsx @@ -0,0 +1,47 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Switch } from './switch.tsx'; + +const meta: Meta = { + title: 'ui/Switch', + component: Switch, + parameters: { + layout: 'centered', + }, + argTypes: { + left: { control: 'text' }, + right: { control: 'text' }, + }, + tags: ['autodocs'], + decorators: [ + (Story, context) => { + const [value, setValue] = useState(context.args.left); + return ( +
+ +
+ Current state: {value} +
+
+ ); + }, + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const SwitchFirst: Story = { + args: { + left: '증상 AI 검색', + right: '제품 검색', + }, +}; diff --git a/src/ui/switch/switch.styles.css.ts b/src/ui/switch/switch.styles.css.ts new file mode 100644 index 000000000..39007a29c --- /dev/null +++ b/src/ui/switch/switch.styles.css.ts @@ -0,0 +1,67 @@ +import { style } from '@vanilla-extract/css'; +import { recipe } from '@vanilla-extract/recipes'; +import { globalVars } from '../theme.css.ts'; +import { typography } from '../typography.css.ts'; + +export const switchRoot = style([ + typography('body_2_14_sb'), + { + all: 'unset', + position: 'relative', + display: 'inline-flex', + alignItems: 'center', + width: '184px', + height: '40px', + backgroundColor: globalVars.color.grey200, + borderRadius: '20px', + cursor: 'pointer', + gap: '4px', + }, +]); + +export const switchSlider = recipe({ + base: { + position: 'absolute', + width: '50%', + height: '100%', + borderRadius: '20px', + backgroundColor: globalVars.color.mainblue500, + transition: 'transform 200ms ease', + }, + variants: { + checked: { + false: { + transform: 'translateX(0)', + }, + true: { + transform: 'translateX(calc(100%))', + }, + }, + }, +}); + +export const switchThumb = recipe({ + base: { + all: 'unset', + position: 'relative', + zIndex: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '20px', + width: '92px', + borderRadius: '20px', + transition: 'color 200ms ease', + userSelect: 'none', + }, + variants: { + checked: { + true: { + color: globalVars.color.white, + }, + false: { + color: globalVars.color.grey500, + }, + }, + }, +}); diff --git a/src/ui/switch/switch.tsx b/src/ui/switch/switch.tsx new file mode 100644 index 000000000..26800c6b2 --- /dev/null +++ b/src/ui/switch/switch.tsx @@ -0,0 +1,52 @@ +import * as RadixSwitch from '@radix-ui/react-switch'; +import * as styles from './switch.styles.css.ts'; + +export type SwitchProps = Omit< + RadixSwitch.SwitchProps, + 'checked' | 'onCheckedChange' +> & { + left: string; + right: string; + value: string; + onValueChange: (value: string) => void; +}; + +export const Switch = ({ + left, + right, + value, + onValueChange, + ...props +}: SwitchProps) => { + const isChecked = value === right; + + const handleCheckedChange = (checked: boolean) => { + onValueChange(checked ? right : left); + }; + + return ( + +
+ + + {left} + + + {right} + + + ); +};