From a9b39edf36c7dcc311d0f95818473623ff24f408 Mon Sep 17 00:00:00 2001 From: rkdcodus Date: Wed, 1 Oct 2025 20:12:54 +0900 Subject: [PATCH 01/25] =?UTF-8?q?feat:=20Radio(basic)=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jds/src/components/Radio/Radio.style.ts | 55 +++++++++++++++++++ packages/jds/src/components/Radio/Radio.tsx | 34 ++++++++++++ packages/jds/src/components/Radio/index.ts | 1 + .../src/components/Radio/radio.variants.ts | 18 ++++++ 4 files changed, 108 insertions(+) create mode 100644 packages/jds/src/components/Radio/Radio.style.ts create mode 100644 packages/jds/src/components/Radio/Radio.tsx create mode 100644 packages/jds/src/components/Radio/index.ts create mode 100644 packages/jds/src/components/Radio/radio.variants.ts diff --git a/packages/jds/src/components/Radio/Radio.style.ts b/packages/jds/src/components/Radio/Radio.style.ts new file mode 100644 index 00000000..48fa09d6 --- /dev/null +++ b/packages/jds/src/components/Radio/Radio.style.ts @@ -0,0 +1,55 @@ +import styled from '@emotion/styled'; +import { RadioSize } from './Radio'; +import { RADIO_SIZE } from './radio.variants'; +import { interaction, pxToRem } from 'utils'; + +interface RadioStyledProps { + size: RadioSize; +} + +export const RadioLabel = styled.label(({ theme, size }) => { + return { + display: 'inline-flex', + position: 'relative', + + [`input[type="radio"]:checked + .visual`]: { + backgroundColor: theme.color.surface.static.standard, + border: `${RADIO_SIZE[size].border}px solid ${theme.color.accent.neutral}`, + }, + + [`input[type="radio"]:disabled + .visual`]: { + backgroundColor: theme.color.fill.assistive, + borderColor: theme.color.stroke.subtle, + ...interaction(theme, 'normal', 'normal', 'default', 'disabled'), + }, + + [`input[type="radio"]:focus-visible + .visual`]: { + boxShadow: `0 0 0 3px ${theme.color.interaction.focus}`, + }, + }; +}); + +export const RadioInput = styled.input({ + position: 'absolute', + width: 1, + height: 1, + padding: 0, + margin: -1, + border: 0, + overflow: 'hidden', + clipPath: 'inset(50%)', + whiteSpace: 'nowrap', +}); + +export const Visual = styled.span(({ theme, size }) => { + const sizeValue = pxToRem(RADIO_SIZE[size].size); + + return { + borderRadius: theme.scheme.desktop.radius.max, + width: sizeValue, + height: sizeValue, + border: `1px solid ${theme.color.stroke.alpha.assistive}`, + backgroundColor: theme.color.surface.shallow, + ...interaction(theme, 'normal', 'normal', 'default'), + }; +}); diff --git a/packages/jds/src/components/Radio/Radio.tsx b/packages/jds/src/components/Radio/Radio.tsx new file mode 100644 index 00000000..0f9c551c --- /dev/null +++ b/packages/jds/src/components/Radio/Radio.tsx @@ -0,0 +1,34 @@ +import { ChangeEvent, forwardRef } from 'react'; +import { RadioInput, RadioLabel, Visual } from './Radio.style'; + +export type RadioSize = 'lg' | 'md' | 'sm' | 'xs'; + +export interface RadioProps { + isChecked: boolean; + isDisabled?: boolean; + size?: RadioSize; + name?: string; + value?: string; + onChange?: (e: ChangeEvent) => void; +} + +export const Radio = forwardRef( + ({ size = 'md', name, value, isChecked, onChange, isDisabled = false }, ref) => { + return ( + + + + ); + }, +); + +Radio.displayName = 'Radio'; diff --git a/packages/jds/src/components/Radio/index.ts b/packages/jds/src/components/Radio/index.ts new file mode 100644 index 00000000..bfbe6d09 --- /dev/null +++ b/packages/jds/src/components/Radio/index.ts @@ -0,0 +1 @@ +export * from './Radio'; diff --git a/packages/jds/src/components/Radio/radio.variants.ts b/packages/jds/src/components/Radio/radio.variants.ts new file mode 100644 index 00000000..0532d94b --- /dev/null +++ b/packages/jds/src/components/Radio/radio.variants.ts @@ -0,0 +1,18 @@ +export const RADIO_SIZE = { + lg: { + size: 20, + border: 6, + }, + md: { + size: 18, + border: 5, + }, + sm: { + size: 16, + border: 5, + }, + xs: { + size: 14, + border: 4, + }, +}; From 4b32e07140ff06587096fae95edf35d4056b6f3d Mon Sep 17 00:00:00 2001 From: rkdcodus Date: Wed, 1 Oct 2025 20:13:31 +0900 Subject: [PATCH 02/25] =?UTF-8?q?fix:=20interaction=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EC=97=90=20borderRadius=20inherit=20=EC=86=8D=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=A9=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/jds/src/utils/interaction.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/jds/src/utils/interaction.ts b/packages/jds/src/utils/interaction.ts index c88ae0a4..106154ce 100644 --- a/packages/jds/src/utils/interaction.ts +++ b/packages/jds/src/utils/interaction.ts @@ -22,6 +22,7 @@ export function interaction( width: '100%', height: '100%', backgroundColor: backgroundColor, + borderRadius: 'inherit', }; if (state === 'locked') { From 207c47b66031a3b73bd07b804a677757e295b474 Mon Sep 17 00:00:00 2001 From: rkdcodus Date: Wed, 1 Oct 2025 20:13:47 +0900 Subject: [PATCH 03/25] =?UTF-8?q?docs:=20Radio(basic)=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81?= =?UTF-8?q?=EC=9D=84=20=EC=9E=91=EC=84=B1=ED=95=A9=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Radio/Radio.stories.tsx | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 packages/jds/src/components/Radio/Radio.stories.tsx diff --git a/packages/jds/src/components/Radio/Radio.stories.tsx b/packages/jds/src/components/Radio/Radio.stories.tsx new file mode 100644 index 00000000..0bc01577 --- /dev/null +++ b/packages/jds/src/components/Radio/Radio.stories.tsx @@ -0,0 +1,68 @@ +// Radio.stories.tsx +import React, { useState } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { Radio, RadioProps, RadioSize } from './Radio'; + +const meta: Meta = { + title: 'Components/Radio', + component: Radio, + parameters: { + layout: 'centered', + }, + argTypes: { + size: { + control: { type: 'radio' }, + options: ['lg', 'md', 'sm', 'xs'], + }, + isChecked: { control: 'boolean' }, + isDisabled: { control: 'boolean' }, + name: { control: 'text' }, + value: { control: 'text' }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + size: 'lg', + isChecked: false, + isDisabled: false, + name: 'defaultRadio', + value: '1', + }, +}; + +export const Sizes: Story = { + render: () => { + const [checkedSize, setCheckedSize] = useState('md'); + + const sizes: RadioProps['size'][] = ['lg', 'md', 'sm', 'xs']; + + return ( +
+ {sizes.map(size => ( + setCheckedSize(size)} + /> + ))} +
+ ); + }, +}; + +export const Disabled: Story = { + render: () => ( +
+ + +
+ ), +}; From 17ea064bdeea116476e4798ec1f05bd84c5a8a5a Mon Sep 17 00:00:00 2001 From: rkdcodus Date: Thu, 2 Oct 2025 19:19:54 +0900 Subject: [PATCH 04/25] =?UTF-8?q?refactor:=20Radio=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20ComponentPropsWithoutRef?= =?UTF-8?q?=EB=A5=BC=20=ED=99=95=EC=9E=A5=ED=95=98=EC=97=AC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isChecked -> props의 checked 사용 isDisabled -> props의 disabled 사용 --- .../src/components/Radio/Radio.stories.tsx | 30 +++++++------------ .../jds/src/components/Radio/Radio.style.ts | 10 +++---- packages/jds/src/components/Radio/Radio.tsx | 27 +++++------------ .../src/components/Radio/radio.variants.ts | 8 ++--- 4 files changed, 27 insertions(+), 48 deletions(-) diff --git a/packages/jds/src/components/Radio/Radio.stories.tsx b/packages/jds/src/components/Radio/Radio.stories.tsx index 0bc01577..53046b4a 100644 --- a/packages/jds/src/components/Radio/Radio.stories.tsx +++ b/packages/jds/src/components/Radio/Radio.stories.tsx @@ -10,14 +10,10 @@ const meta: Meta = { layout: 'centered', }, argTypes: { - size: { + radioSize: { control: { type: 'radio' }, options: ['lg', 'md', 'sm', 'xs'], }, - isChecked: { control: 'boolean' }, - isDisabled: { control: 'boolean' }, - name: { control: 'text' }, - value: { control: 'text' }, }, }; @@ -27,11 +23,7 @@ type Story = StoryObj; export const Default: Story = { args: { - size: 'lg', - isChecked: false, - isDisabled: false, - name: 'defaultRadio', - value: '1', + radioSize: 'lg', }, }; @@ -39,18 +31,18 @@ export const Sizes: Story = { render: () => { const [checkedSize, setCheckedSize] = useState('md'); - const sizes: RadioProps['size'][] = ['lg', 'md', 'sm', 'xs']; + const sizes: RadioProps['radioSize'][] = ['lg', 'md', 'sm', 'xs']; return (
- {sizes.map(size => ( + {sizes.map(radioSize => ( setCheckedSize(size)} + value={radioSize} + checked={checkedSize === radioSize} + onChange={() => setCheckedSize(radioSize)} /> ))}
@@ -61,8 +53,8 @@ export const Sizes: Story = { export const Disabled: Story = { render: () => (
- - + +
), }; diff --git a/packages/jds/src/components/Radio/Radio.style.ts b/packages/jds/src/components/Radio/Radio.style.ts index 48fa09d6..8d82e686 100644 --- a/packages/jds/src/components/Radio/Radio.style.ts +++ b/packages/jds/src/components/Radio/Radio.style.ts @@ -4,17 +4,17 @@ import { RADIO_SIZE } from './radio.variants'; import { interaction, pxToRem } from 'utils'; interface RadioStyledProps { - size: RadioSize; + radioSize: RadioSize; } -export const RadioLabel = styled.label(({ theme, size }) => { +export const RadioLabel = styled.label(({ theme, radioSize }) => { return { display: 'inline-flex', position: 'relative', [`input[type="radio"]:checked + .visual`]: { backgroundColor: theme.color.surface.static.standard, - border: `${RADIO_SIZE[size].border}px solid ${theme.color.accent.neutral}`, + border: `${RADIO_SIZE[radioSize].border}px solid ${theme.color.accent.neutral}`, }, [`input[type="radio"]:disabled + .visual`]: { @@ -41,8 +41,8 @@ export const RadioInput = styled.input({ whiteSpace: 'nowrap', }); -export const Visual = styled.span(({ theme, size }) => { - const sizeValue = pxToRem(RADIO_SIZE[size].size); +export const Visual = styled.span(({ theme, radioSize }) => { + const sizeValue = pxToRem(RADIO_SIZE[radioSize].radioSize); return { borderRadius: theme.scheme.desktop.radius.max, diff --git a/packages/jds/src/components/Radio/Radio.tsx b/packages/jds/src/components/Radio/Radio.tsx index 0f9c551c..5511dd2b 100644 --- a/packages/jds/src/components/Radio/Radio.tsx +++ b/packages/jds/src/components/Radio/Radio.tsx @@ -1,31 +1,18 @@ -import { ChangeEvent, forwardRef } from 'react'; +import { ComponentPropsWithoutRef, forwardRef } from 'react'; import { RadioInput, RadioLabel, Visual } from './Radio.style'; export type RadioSize = 'lg' | 'md' | 'sm' | 'xs'; -export interface RadioProps { - isChecked: boolean; - isDisabled?: boolean; - size?: RadioSize; - name?: string; - value?: string; - onChange?: (e: ChangeEvent) => void; +export interface RadioProps extends ComponentPropsWithoutRef<'input'> { + radioSize?: RadioSize; } export const Radio = forwardRef( - ({ size = 'md', name, value, isChecked, onChange, isDisabled = false }, ref) => { + ({ radioSize = 'md', ...props }, ref) => { return ( - - -