Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/design-system/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as Button } from './button/Button';
export { Switch } from './switch/switch';
export { default as Input } from './input/Input';
export { Textarea } from './textarea/Textarea';
110 changes: 110 additions & 0 deletions packages/design-system/src/components/textarea/Textarea.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { within, userEvent, expect } from '@storybook/test';
import { Textarea } from './Textarea';

const meta: Meta<typeof Textarea> = {
title: 'Components/Textarea',
component: Textarea,
tags: ['autodocs'],
parameters: {
layout: 'centered',
docs: {
description: {
component:
'고정 크기 **h-[12rem] / w-[24.8rem]** · 기본 **500자** 제한 · 내용 초과 시 내부 스크롤이 나타나는 텍스트영역입니다. ' +
'`maxLength`로 글자수 제한을 변경할 수 있습니다.',
},
},
},
argTypes: {
className: { table: { disable: true } },
maxLength: { control: { type: 'number', min: 1 } },
placeholder: { control: 'text' },
defaultValue: { control: 'text' },
},
args: {
placeholder: '나중에 내가 꺼내줄 수 있게 살짝 적어줘!',
maxLength: 500,
},
};

export default meta;
type Story = StoryObj<typeof Textarea>;

export const Basic: Story = {
render: (args) => <Textarea {...args} />,
};

export const WithMaxLength500: Story = {
name: 'MaxLength=500',
args: { maxLength: 500, placeholder: '최대 500자' },
render: (args) => <Textarea {...args} />,
};

export const ScrollableOverflow: Story = {
name: '넘치면 스크롤',
args: {
defaultValue: `계절이 지나가는 하늘에는
가을로 가득 차 있습니다.

나는 아무 걱정도 없이
가을 속의 별들을 다 헤일 듯합니다.

가슴속에 하나둘 새겨지는 별을
이제 다 못 헤는 것은
쉬이 아침이 오는 까닭이요,
내일 밤이 남은 까닭이요,
아직 나의 청춘이 다하지 않은 까닭입니다.

별 하나에 추억과
별 하나에 사랑과
별 하나에 쓸쓸함과
별 하나에 동경과
별 하나에 시와
Comment on lines +53 to +63
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엌,, 삭막한 테스트용 글귀에 윤동주씨 시라니,,, 감성 좋아여어 짱

별 하나에 어머니, 어머니,

어머님, 나는 별 하나에 아름다운 말 한마디씩 불러 봅니다. 소학교 때 책상을 같이 했던 아이들의 이름과, 패, 경, 옥, 이런 이국 소녀들의 이름과, 벌써 아기 어머니 된 계집애들의 이름과, 가난한 이웃 사람들의 이름과, 비둘기, 강아지, 토끼, 노새, 노루, '프랑시스 잠', '라이너 마리아 릴케' 이런 시인의 이름을 불러 봅니다.

이네들은 너무나 멀리 있습니다.
별이 아스라이 멀듯이.

어머님,
그리고 당신은 멀리 북간도에 계십니다.

나는 무엇인지 그리워
이 많은 별빛이 내린 언덕 위에
내 이름자를 써 보고
흙으로 덮어 버리었습니다.

딴은 밤을 새워 우는 벌레는
부끄러운 이름을 슬퍼하는 까닭입니다.

그러나 겨울이 지나고 나의 별에도 봄이 오면
무덤 위에 파란 잔디가 피어나듯이
내 이름자 묻힌 언덕 위에도
자랑처럼 풀이 무성할 거외다.
`,
},
render: (args) => <Textarea {...args} />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const ta = (await canvas.findByRole('textbox')) as HTMLTextAreaElement;
await expect(ta.scrollHeight).toBeGreaterThan(ta.clientHeight);
},
};

export const PreventOverflowByMaxLength: Story = {
name: '입력 길이 제한 동작',
args: { maxLength: 50, placeholder: '최대 50자' },
render: (args) => <Textarea {...args} />,
play: async ({ canvasElement, args }) => {
const limit = Number(args.maxLength ?? 500);
const canvas = within(canvasElement);
const ta = (await canvas.findByRole('textbox')) as HTMLTextAreaElement;

await userEvent.click(ta);
await userEvent.type(ta, 'a'.repeat(limit + 10));

await expect(ta.value.length).toBe(limit);
},
};
32 changes: 32 additions & 0 deletions packages/design-system/src/components/textarea/Textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Textarea.tsx
import * as React from 'react';
import { cn } from '@/lib/utils';

export interface TextareaProps
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength'> {
maxLength: number;
}

export function Textarea({
className,
maxLength,
style,
...props
}: TextareaProps) {
return (
<textarea
data-slot="textarea"
maxLength={maxLength}
className={cn(
Comment on lines +18 to +20
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxLength 상수화에 대한 의견은 좋은 것 같습니다!

근데 just 질문이 있는데, 혹시 이 maxLength를 별도로 받는 이유가 있을까요?? 이 length로 글자수 제한이나 입력창 높이가 달라지는 걸까요? figma상에서는 옛날 앱잼 UI때처럼 0/500 이런 텍스트 처리 멘트가 없어져서요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원래 textarea에서 고정으로 지정할까싶었지만 추후 확장성을 고려해서 사용처에서 내려주는게 좋다고 판단했습니다-!
이 length로 글자수 제한은 달라지지만 ui상에서 달라지는건 없습니다

'h-[12rem] w-full',
'resize-none overflow-y-auto',
Comment on lines +21 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

rows/height 충돌: 고정 height가 소비자 props(예: rows, style.height)를 무력화합니다

h-[12rem]가 명시되어 있어 rows나 인라인 style={{ height }}를 전달해도 효과가 없습니다. 디자인 시스템 컴포넌트는 합리적인 기본값을 주되, 소비자 제어를 막지 않는 쪽이 좋습니다. 최소 높이만 보장하고 나머지는 소비자/브라우저 기본 동작에 맡기는 형태로 변경을 권장합니다.

아래처럼 최소 높이로 완화하면 rows/style.height와도 잘 공존합니다.

-        'h-[12rem] w-full',
+        'min-h-[12rem] h-auto w-full',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'h-[12rem] w-full',
'resize-none overflow-y-auto',
'min-h-[12rem] h-auto w-full',
'resize-none overflow-y-auto',
🤖 Prompt for AI Agents
In packages/design-system/src/components/textarea/Textarea.tsx around lines
21-22, the textarea uses a fixed height class 'h-[12rem]' which prevents
consumer props like rows or inline style={{ height }} from taking effect;
replace the fixed height with a minimum height (e.g., 'min-h-[12rem]') so the
component provides a sensible default while allowing consumers or the browser to
override height via rows/style; ensure the class list retains 'w-full' and any
overflow/resize classes and that the min-h class appears where the fixed height
was.

'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem] pr-[1.4rem]',
'focus:border-input outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0',
'ds-scrollbar',
className
)}
Comment on lines +21 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

접근성: 포커스 인디케이터가 충분히 눈에 띄지 않을 수 있습니다

현재 클래스는 outline/ring을 제거하고 border 색만 바꾸는데, 이는 키보드 사용자에게 포커스가 잘 보이지 않을 수 있습니다. 최소 focus-visible 상태에서 두께 있는 ring과 대비를 주길 권장합니다.

권장 수정(토큰 네이밍은 디자인 토큰에 맞춰 조정하세요):

-        'focus:border-input outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0',
+        'focus:border-input outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',

추가로 배경색 토큰이 있다면 focus-visible:ring-offset-2 focus-visible:ring-offset-[bg-token]도 고려해 주세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'h-[12rem] w-full',
'resize-none overflow-y-auto',
'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem] pr-[1.4rem]',
'focus:border-input outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0',
'ds-scrollbar',
className
)}
// packages/design-system/src/components/textarea/Textarea.tsx
className={clsx(
'h-[12rem] w-full',
'resize-none overflow-y-auto',
'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem] pr-[1.4rem]',
'focus:border-input outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',
'ds-scrollbar',
className
)}
🤖 Prompt for AI Agents
In packages/design-system/src/components/textarea/Textarea.tsx around lines 21
to 27, the current classes remove outline/ring making keyboard focus hard to
see; update the focus-visible styles to add a thicker, high-contrast ring and
optional offset instead of fully suppressing focus indicators (e.g., add classes
like focus-visible:ring-2 focus-visible:ring-[token-foreground]
focus-visible:ring-offset-2 focus-visible:ring-offset-[token-background] and
keep focus:outline-none only for non-visible focus while ensuring focus-visible
shows the ring); use your design token names for the ring color and offset
background.

style={{ scrollbarGutter: 'stable', ...style }}
{...props}
Comment on lines +28 to +29
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오왕 scrollbarGutter는 스크롤바 커스텀해주는 속성인건가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아뇽-!! 스크롤바가 생길 때를 대비해 콘텐츠 옆에 공간을 미리 확보해서 스크롤바 등장/사라짐으로 인한 레이아웃 흔들림을 막아줍니다-!

/>
);
}
31 changes: 31 additions & 0 deletions packages/tailwind-config/shared-styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,34 @@
@layer components {
/* 공통 컴포넌트 스타일 */
}

@layer utilities {
:where(.ds-scrollbar) {
--ds-scroll-size: 0.4rem;
--ds-scroll-thumb: var(--color-gray100);
--ds-scroll-track: transparent;

scrollbar-color: var(--ds-scroll-thumb) var(--ds-scroll-track);

scrollbar-gutter: stable;
}
Comment on lines +250 to +251
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Textarea의 stableScrollbarGutter 속성이 비활성화되지 않는 버그

.ds-scrollbarscrollbar-gutter: stable;가 기본 적용되어 있어, 컴포넌트에서 stableScrollbarGutter={false}로 내려도 실제로는 비활성화되지 않습니다. 현재 구현(Textarea.tsx)은 true일 때만 인라인 스타일을 추가하므로 false일 때 CSS 규칙이 그대로 남습니다.

  • 옵션을 신뢰할 수 있게 하려면 인라인 스타일에서 항상 값을 명시해 우선순위로 덮어쓰는 방식이 안전합니다.

컴포넌트 쪽 수정 제안(Textarea.tsx 내 style 속성만 교체):

-      style={{
-        ...(stableScrollbarGutter ? { scrollbarGutter: 'stable' } : {}),
-        ...style,
-      }}
+      style={{
+        scrollbarGutter: stableScrollbarGutter ? 'stable' : 'auto',
+        ...style,
+      }}

또는 CSS에서 기본값을 제거하고(아래 라인 삭제) 컴포넌트에서만 제어하는 것도 방법입니다.

-    scrollbar-gutter: stable;
🤖 Prompt for AI Agents
In packages/tailwind-config/shared-styles.css around lines 250-251 the rule
"scrollbar-gutter: stable;" applied to .ds-scrollbar forces the gutter on all
textareas and prevents stableScrollbarGutter={false} from taking effect; either
remove this default CSS line so the component controls the gutter, or
(preferred) update Textarea.tsx to always set an inline scrollbar-gutter style
that overrides the stylesheet — e.g. in the style prop always include
scrollbarGutter: stableScrollbarGutter ? 'stable' : 'unset' (or 'auto') so the
prop reliably enables or disables the gutter via inline priority.


:where(.ds-scrollbar)::-webkit-scrollbar,
:where(.ds-scrollbar)::-webkit-scrollbar:hover,
:where(.ds-scrollbar)::-webkit-scrollbar:active {
width: var(--ds-scroll-size);
height: var(--ds-scroll-size);
}
Comment on lines +253 to +258
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

불필요/무효한 선택자 정리: ::-webkit-scrollbar:hover, :active

::-webkit-scrollbar 자체에는 :hover/:active 상태가 적용되지 않습니다. 현재 셀렉터 병합은 의미가 없고 규칙 파싱만 늘립니다. 기본 ::-webkit-scrollbar만 남기세요.

아래처럼 간소화:

-  :where(.ds-scrollbar)::-webkit-scrollbar,
-  :where(.ds-scrollbar)::-webkit-scrollbar:hover,
-  :where(.ds-scrollbar)::-webkit-scrollbar:active {
+  :where(.ds-scrollbar)::-webkit-scrollbar {
     width: var(--ds-scroll-size);
     height: var(--ds-scroll-size);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
:where(.ds-scrollbar)::-webkit-scrollbar,
:where(.ds-scrollbar)::-webkit-scrollbar:hover,
:where(.ds-scrollbar)::-webkit-scrollbar:active {
width: var(--ds-scroll-size);
height: var(--ds-scroll-size);
}
:where(.ds-scrollbar)::-webkit-scrollbar {
width: var(--ds-scroll-size);
height: var(--ds-scroll-size);
}
🤖 Prompt for AI Agents
In packages/tailwind-config/shared-styles.css around lines 253 to 258, the
selector group includes invalid/unnecessary pseudo-classes
(::-webkit-scrollbar:hover and ::-webkit-scrollbar:active) which have no effect
on the pseudo-element; remove the :hover and :active variants and keep only the
base ::-webkit-scrollbar selector (and its width/height declarations) to
simplify and avoid meaningless selector duplication.


:where(.ds-scrollbar)::-webkit-scrollbar-track {
background: var(--ds-scroll-track);
}

:where(.ds-scrollbar)::-webkit-scrollbar-thumb,
:where(.ds-scrollbar)::-webkit-scrollbar-thumb:hover,
:where(.ds-scrollbar)::-webkit-scrollbar-thumb:active {
background: var(--ds-scroll-thumb);
border-radius: 9999px;
transition: none;
}
}
Loading