@conscience-technology/ctds 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/components/AIMessage/AIMessage.module.css +129 -0
- package/assets/components/AIMessage/AIMessage.tsx +104 -0
- package/assets/components/AIMessage/index.ts +2 -0
- package/assets/components/AttachmentChip/AttachmentChip.module.css +3 -1
- package/assets/components/AttachmentThumb/AttachmentThumb.module.css +13 -7
- package/assets/components/AttachmentThumb/AttachmentThumb.tsx +4 -1
- package/assets/components/Bubble/Bubble.module.css +65 -0
- package/assets/components/Bubble/Bubble.tsx +63 -0
- package/assets/components/Bubble/index.ts +2 -0
- package/assets/components/Button/Button.module.css +15 -2
- package/assets/components/Button/Button.tsx +13 -0
- package/assets/components/Calendar/Calendar.module.css +163 -0
- package/assets/components/Calendar/Calendar.tsx +323 -0
- package/assets/components/Calendar/index.ts +2 -0
- package/assets/components/ChatInput/ChatInput.module.css +82 -45
- package/assets/components/ChatInput/ChatInput.tsx +46 -34
- package/assets/components/ChatInput/ModelSelector.tsx +23 -27
- package/assets/components/ChatInput/index.ts +0 -1
- package/assets/components/CodeBlock/CodeBlock.module.css +79 -0
- package/assets/components/CodeBlock/CodeBlock.tsx +71 -0
- package/assets/components/CodeBlock/index.ts +2 -0
- package/assets/components/ContextMenu/ContextMenu.tsx +10 -1
- package/assets/components/DropdownMenu/DropdownMenu.module.css +26 -0
- package/assets/components/DropdownMenu/DropdownMenu.tsx +367 -0
- package/assets/components/DropdownMenu/index.ts +9 -0
- package/assets/components/EventChip/EventChip.module.css +57 -0
- package/assets/components/EventChip/EventChip.tsx +41 -0
- package/assets/components/EventChip/index.ts +2 -0
- package/assets/components/Icon/Icon.tsx +3 -0
- package/assets/components/IdentityButton/IdentityButton.module.css +119 -0
- package/assets/components/IdentityButton/IdentityButton.tsx +113 -0
- package/assets/components/IdentityButton/index.ts +6 -0
- package/assets/components/MenuItem/MenuItem.module.css +67 -2
- package/assets/components/MenuItem/MenuItem.tsx +22 -2
- package/assets/components/MessageActions/MessageActions.module.css +43 -0
- package/assets/components/MessageActions/MessageActions.tsx +94 -0
- package/assets/components/MessageActions/index.ts +6 -0
- package/assets/components/MessageQuote/MessageQuote.module.css +29 -0
- package/assets/components/MessageQuote/MessageQuote.tsx +33 -0
- package/assets/components/MessageQuote/index.ts +2 -0
- package/assets/components/MessageSources/MessageSources.module.css +58 -0
- package/assets/components/MessageSources/MessageSources.tsx +64 -0
- package/assets/components/MessageSources/index.ts +2 -0
- package/assets/components/MessageTable/MessageTable.module.css +59 -0
- package/assets/components/MessageTable/MessageTable.tsx +21 -0
- package/assets/components/MessageTable/index.ts +2 -0
- package/assets/components/QuotationBar/QuotationBar.module.css +2 -2
- package/assets/components/Sidebar/Sidebar.module.css +3 -57
- package/assets/components/Sidebar/Sidebar.tsx +18 -21
- package/assets/components/Spinner/LottiePlayer.tsx +82 -0
- package/assets/components/Spinner/Spinner.module.css +170 -0
- package/assets/components/Spinner/Spinner.tsx +117 -0
- package/assets/components/Spinner/index.ts +10 -0
- package/assets/components/Spinner/lottieColor.ts +106 -0
- package/assets/components/Spinner/lottieManifest.ts +100 -0
- package/assets/components/Tabs/Tabs.module.css +3 -3
- package/assets/components/UserMessage/UserMessage.module.css +41 -0
- package/assets/components/UserMessage/UserMessage.tsx +66 -0
- package/assets/components/UserMessage/index.ts +2 -0
- package/assets/components/_data.ts +18 -4
- package/assets/components/tokens.css +20 -0
- package/assets/prompts/components/ai-message.md +45 -0
- package/assets/prompts/components/bubble.md +56 -0
- package/assets/prompts/components/calendar.md +116 -0
- package/assets/prompts/components/code-block.md +35 -0
- package/assets/prompts/components/dropdown-menu.md +145 -0
- package/assets/prompts/components/event-chip.md +74 -0
- package/assets/prompts/components/identity-button.md +168 -0
- package/assets/prompts/components/message-actions.md +49 -0
- package/assets/prompts/components/message-quote.md +26 -0
- package/assets/prompts/components/message-sources.md +33 -0
- package/assets/prompts/components/message-table.md +27 -0
- package/assets/prompts/components/sidebar.md +48 -3
- package/assets/prompts/components/spinner.md +109 -0
- package/assets/prompts/components/user-message.md +45 -0
- package/assets/prompts/primitives/scroll.md +22 -11
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# IdentityButton
|
|
2
|
+
|
|
3
|
+
### 원칙
|
|
4
|
+
식별 가능한 **대상**(워크스페이스·프로필·프로젝트·조직·환경)을 **현재 선택 상태**로 보여주면서, 클릭 가능함을 암시하는 가로 식별 블록. 좌측 identity(Avatar/Icon) + 2줄 라벨(name + sub) + 우측 trailing(caret-down·IconButton·Badge)이 뼈대. 사이드바 Header의 워크스페이스 스위처, Footer의 프로필, Topbar의 조직 스위처, 계정 chooser 리스트 — 전부 이 컴포넌트가 담당.
|
|
5
|
+
|
|
6
|
+
Select/Input/DropdownMenu 자체가 아니다 — 이 컴포넌트는 **트리거** 또는 **표시**다. 전환 메뉴를 열고 싶다면 `asChild`로 DropdownMenu.Trigger에 위임한다.
|
|
7
|
+
|
|
8
|
+
### Do
|
|
9
|
+
- **역할 중립** — workspace·profile·project·org·env 전부 같은 컴포넌트로. "모양"은 같고 trailing만 바뀜 (caret-down=전환 가능, IconButton=메뉴 열기, 생략=표시 전용).
|
|
10
|
+
- **네비게이션 액션에 Input Dropdown 금지** — 워크스페이스 전환은 폼이 아니라 라우트 액션. IdentityButton + DropdownMenu 조합이 표준 ([[antipattern_workspace_switcher]]).
|
|
11
|
+
- **`asChild`로 트리거 위임** — DropdownMenu.Trigger, Popover.Trigger 등에 그대로 `asChild`.
|
|
12
|
+
- **collapsed 상태는 Sidebar가 자동 주입** — 사이드바 밖에서 쓸 땐 신경 안 써도 됨. 사이드바 안에선 context로 전달됨.
|
|
13
|
+
- **leading 자유도** — Avatar가 기본이지만 Icon·Emoji·커스텀 노드 아무거나 `leading` prop에 삽입 가능. 조직 로고·프로젝트 심볼 등.
|
|
14
|
+
- **1줄 모드** — `sub` 생략하면 자동으로 1줄 레이아웃. 라벨만 필요한 가벼운 트리거일 때.
|
|
15
|
+
- **trailing에 bare `<Icon>`을 쓸 땐 `size={14}` 명시** — Icon 기본값(20px)은 라벨 옆에 두면 과하다.
|
|
16
|
+
- **trailing에 `IconButton`을 쓸 땐 size를 IdentityButton size와 맞춘다** — IdentityButton md/lg면 IconButton md, sm이면 IconButton sm. sm 섞어 쓰면 라벨 옆에서 왜소해 보임.
|
|
17
|
+
- **trailing 아이콘은 역할에 따라 바꾼다** — 의미가 어긋나면 사용자가 다른 동작을 기대함:
|
|
18
|
+
- `caret-down` (Icon) — **아래로 열리는 드롭다운**. 가장 기본. 사이드바 Header 워크스페이스 스위처, Topbar 조직 스위처.
|
|
19
|
+
- `caret-up` (Icon) — **위로 열리는 패널/메뉴**. 사이드바 Footer 프로필처럼 위쪽으로 메뉴가 올라올 때.
|
|
20
|
+
- `gear` (IconButton) — **설정 페이지로 이동**. 메뉴가 아니라 바로 라우트 이동. root click과 분리 (`e.stopPropagation()`).
|
|
21
|
+
- `dots-three` (IconButton) — **⋯ 추가 액션 메뉴**. 로그아웃·계정 관리 등 부가 액션.
|
|
22
|
+
- 생략 — **표시 전용**. 전환 불가하거나 다른 자리에서 전환하는 경우.
|
|
23
|
+
|
|
24
|
+
### Don't
|
|
25
|
+
- Select·Input Dropdown으로 이 역할 매핑 금지 ([[antipattern_workspace_switcher]]).
|
|
26
|
+
- 두 가지 "주된 액션"을 넣지 않는다 — root click 하나가 selection이자 trigger. 부가 액션은 `trailing`에 IconButton 하나까지만.
|
|
27
|
+
- 라벨을 두 줄 이상 넣지 않는다 — `name` 1줄 + `sub` 1줄이 상한. 설명이 길면 [[components/tooltip]].
|
|
28
|
+
- height를 CSS로 덮어쓰지 않는다 — `size` prop 경유.
|
|
29
|
+
- 사이드바 밖에서 쓸 때 자체 border/box-shadow 두르지 않는다 — 호스트가 담당 (Topbar·Card 안이면 그 컨테이너의 경계 그대로).
|
|
30
|
+
|
|
31
|
+
### Fallback
|
|
32
|
+
size 애매하면 `md`. variant 애매하면 `ghost`. trailing 애매하면 **caret-down** (전환 가능함을 암시하는 게 안전).
|
|
33
|
+
|
|
34
|
+
### 관련
|
|
35
|
+
- [[components/sidebar]] — Header/Footer 안 가장 흔한 사용처 (Sidebar.Profile은 이 컴포넌트의 preset 래퍼)
|
|
36
|
+
- [[components/dropdown-menu]] — `asChild`로 트리거 위임하는 짝
|
|
37
|
+
- [[components/avatar]] — leading 기본 요소
|
|
38
|
+
- [[axes/density]] — `--control-h-*`가 size에 매핑
|
|
39
|
+
- [[antipattern_workspace_switcher]] — "Input Dropdown으로 매핑 금지" 안티패턴
|
|
40
|
+
|
|
41
|
+
### Props 시그니처
|
|
42
|
+
```tsx
|
|
43
|
+
<IdentityButton
|
|
44
|
+
size="sm | md | lg" // default: md
|
|
45
|
+
variant="ghost | muted" // default: ghost
|
|
46
|
+
avatar={{...AvatarProps}} // 좌측 Avatar. src/initials/icon/status 패스스루
|
|
47
|
+
leading={ReactNode} // avatar 대신 임의 노드 (Icon, Emoji, 로고 SVG)
|
|
48
|
+
name={ReactNode} // 1줄 메인 라벨
|
|
49
|
+
sub={ReactNode} // 2번째 줄 (생략 시 1줄 모드)
|
|
50
|
+
trailing={ReactNode} // caret-down · IconButton · Badge 등
|
|
51
|
+
active={boolean} // 리스트에서 현재 선택
|
|
52
|
+
disabled={boolean}
|
|
53
|
+
collapsed={boolean} // icon-only (Sidebar가 자동 주입)
|
|
54
|
+
asChild={boolean} // Radix Slot — DropdownMenu.Trigger 등에 위임
|
|
55
|
+
onClick={(e) => void}
|
|
56
|
+
/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 상태 매트릭스
|
|
60
|
+
| 상태 | 시각 |
|
|
61
|
+
|---|---|
|
|
62
|
+
| default (ghost) | 투명 bg, `--fg` 텍스트 |
|
|
63
|
+
| hover · focus-visible | `--hover` 배경 |
|
|
64
|
+
| active (현재 선택) | `--bg-muted` 배경 + `--fg-strong` + medium weight. hover가 덮지 않음 ([[feedback_hover_over_active]]) |
|
|
65
|
+
| variant=muted | 항상 `--bg-muted` 배경 — 사이드바 안에서 톤 대비가 필요할 때 |
|
|
66
|
+
| collapsed | leading 하나만, 가운데 정렬, 라벨/sub/trailing 전부 숨김 |
|
|
67
|
+
| disabled | `cursor: not-allowed` + opacity 0.5 |
|
|
68
|
+
|
|
69
|
+
### Size 가이드
|
|
70
|
+
| size | min-height | 언제 |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `sm` | `--control-h-sm` | 조밀한 Topbar 조직 스위처, 리스트 아이템 |
|
|
73
|
+
| `md` | `--control-h-md` | **기본.** 사이드바 Header·Footer, 일반 트리거 |
|
|
74
|
+
| `lg` | `--control-h-lg` | 사이드바 워크스페이스 Header처럼 아이덴티티가 가장 강하게 노출돼야 할 때 |
|
|
75
|
+
|
|
76
|
+
### 조합 예시
|
|
77
|
+
```tsx
|
|
78
|
+
// 1. 사이드바 Header 워크스페이스 스위처 — DropdownMenu로 전환
|
|
79
|
+
<Sidebar.Header>
|
|
80
|
+
<DropdownMenu>
|
|
81
|
+
<DropdownMenu.Trigger asChild>
|
|
82
|
+
<IdentityButton
|
|
83
|
+
size="lg"
|
|
84
|
+
avatar={{ initials: "T", shape: "rounded" }}
|
|
85
|
+
name="Taskify PM"
|
|
86
|
+
sub="Project Workspace"
|
|
87
|
+
trailing={<Icon name="caret-down" size={14} />}
|
|
88
|
+
aria-haspopup="menu"
|
|
89
|
+
/>
|
|
90
|
+
</DropdownMenu.Trigger>
|
|
91
|
+
<DropdownMenu.Content>
|
|
92
|
+
{/* 워크스페이스 리스트 + Separator + "+ 새 워크스페이스" */}
|
|
93
|
+
</DropdownMenu.Content>
|
|
94
|
+
</DropdownMenu>
|
|
95
|
+
</Sidebar.Header>
|
|
96
|
+
|
|
97
|
+
// 2a. 사이드바 Footer 프로필 + 위로 올라가는 메뉴 — caret-up
|
|
98
|
+
<Sidebar.Footer>
|
|
99
|
+
<DropdownMenu>
|
|
100
|
+
<DropdownMenu.Trigger asChild>
|
|
101
|
+
<IdentityButton
|
|
102
|
+
avatar={{ src: user.avatar, status: "online" }}
|
|
103
|
+
name="희영"
|
|
104
|
+
sub="admin@ctds.dev"
|
|
105
|
+
trailing={<Icon name="caret-up" size={14} />}
|
|
106
|
+
aria-haspopup="menu"
|
|
107
|
+
/>
|
|
108
|
+
</DropdownMenu.Trigger>
|
|
109
|
+
<DropdownMenu.Content side="top" align="start">
|
|
110
|
+
{/* 계정 전환 · 설정 · 로그아웃 */}
|
|
111
|
+
</DropdownMenu.Content>
|
|
112
|
+
</DropdownMenu>
|
|
113
|
+
</Sidebar.Footer>
|
|
114
|
+
|
|
115
|
+
// 2b. 사이드바 Footer 프로필 + 설정으로 바로 이동 — gear IconButton
|
|
116
|
+
// trailing의 IconButton onClick은 반드시 e.stopPropagation()으로 root click과 분리.
|
|
117
|
+
<Sidebar.Footer>
|
|
118
|
+
<IdentityButton
|
|
119
|
+
avatar={{ src: user.avatar, status: "online" }}
|
|
120
|
+
name="희영"
|
|
121
|
+
sub="admin@ctds.dev"
|
|
122
|
+
trailing={
|
|
123
|
+
<IconButton
|
|
124
|
+
size="md"
|
|
125
|
+
variant="ghost"
|
|
126
|
+
icon={<Icon name="gear" />}
|
|
127
|
+
aria-label="설정"
|
|
128
|
+
onClick={(e) => { e.stopPropagation(); router.push("/settings"); }}
|
|
129
|
+
/>
|
|
130
|
+
}
|
|
131
|
+
onClick={() => openProfilePanel()}
|
|
132
|
+
/>
|
|
133
|
+
</Sidebar.Footer>
|
|
134
|
+
|
|
135
|
+
// 3. Topbar 조직 스위처 — size=sm
|
|
136
|
+
<DropdownMenu>
|
|
137
|
+
<DropdownMenu.Trigger asChild>
|
|
138
|
+
<IdentityButton
|
|
139
|
+
size="sm"
|
|
140
|
+
leading={<Icon name="buildings" />}
|
|
141
|
+
name="Conscience Technology"
|
|
142
|
+
trailing={<Icon name="caret-down" size={14} />}
|
|
143
|
+
/>
|
|
144
|
+
</DropdownMenu.Trigger>
|
|
145
|
+
</DropdownMenu>
|
|
146
|
+
|
|
147
|
+
// 4. Account chooser 리스트 — active 상태로 현재 선택 표시
|
|
148
|
+
<div role="listbox">
|
|
149
|
+
{accounts.map((a) => (
|
|
150
|
+
<IdentityButton
|
|
151
|
+
key={a.id}
|
|
152
|
+
avatar={{ src: a.avatar }}
|
|
153
|
+
name={a.name}
|
|
154
|
+
sub={a.email}
|
|
155
|
+
active={a.id === currentId}
|
|
156
|
+
onClick={() => select(a.id)}
|
|
157
|
+
/>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
// 5. 1줄 모드 — sub 없이 가볍게
|
|
162
|
+
<IdentityButton
|
|
163
|
+
size="sm"
|
|
164
|
+
leading={<Icon name="folder" />}
|
|
165
|
+
name="Design System"
|
|
166
|
+
trailing={<Icon name="caret-down" size={14} />}
|
|
167
|
+
/>
|
|
168
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Message Actions
|
|
2
|
+
|
|
3
|
+
### 원칙
|
|
4
|
+
메시지 하단에 붙는 **아이콘 액션 바**. [[components/user-message]] · [[components/ai-message]] · MessageState에서 공용. copy / edit / regenerate 등 kind 열거로 아이콘 자동 매핑.
|
|
5
|
+
|
|
6
|
+
### Do
|
|
7
|
+
- UserMessage: `[copy, edit]` 조합이 기본 (오른쪽 정렬 `align=end`).
|
|
8
|
+
- AIMessage: `[copy, regenerate]` 조합이 기본 (왼쪽 정렬 `align=start`).
|
|
9
|
+
- 호버·focus-visible에서만 ring 노출, 평상시엔 투명 bg + fg-muted 아이콘.
|
|
10
|
+
- 커스텀 액션 필요하면 `icon`에 직접 ReactNode 전달.
|
|
11
|
+
|
|
12
|
+
### Don't
|
|
13
|
+
- 액션 라벨을 아이콘 옆에 노출하지 않는다 — `title`(툴팁)과 `aria-label`만.
|
|
14
|
+
- 액션 사이에 구분선 넣지 않는다 — gap으로 공간만 분리.
|
|
15
|
+
- 많은 액션(5개 이상) 나열 금지 — 메뉴 오버플로우로 숨김 고려.
|
|
16
|
+
|
|
17
|
+
### Fallback
|
|
18
|
+
align 애매하면 `start`. kind 없는 커스텀 액션은 `icon` 직접 전달.
|
|
19
|
+
|
|
20
|
+
### 관련
|
|
21
|
+
- [[components/user-message]] / [[components/ai-message]] — 메인 소비처
|
|
22
|
+
- [[components/icon-button]] — 단독 아이콘 버튼은 IconButton 사용
|
|
23
|
+
|
|
24
|
+
### Props 시그니처
|
|
25
|
+
```tsx
|
|
26
|
+
<MessageActions
|
|
27
|
+
actions={[
|
|
28
|
+
{ kind: "copy", onClick },
|
|
29
|
+
{ kind: "regenerate", onClick },
|
|
30
|
+
]}
|
|
31
|
+
align="start | end" // default: start
|
|
32
|
+
/>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Kind 매핑
|
|
36
|
+
| kind | icon | 기본 라벨 |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| copy | Copy | 복사 |
|
|
39
|
+
| edit | PencilSimple | 수정 |
|
|
40
|
+
| regenerate | ArrowClockwise | 다시 생성 |
|
|
41
|
+
| share | ShareNetwork | 공유 |
|
|
42
|
+
| delete | TrashSimple | 삭제 |
|
|
43
|
+
| like | ThumbsUp | 좋아요 |
|
|
44
|
+
| dislike | ThumbsDown | 싫어요 |
|
|
45
|
+
|
|
46
|
+
### 비율
|
|
47
|
+
- 버튼: `space-6` 정사각
|
|
48
|
+
- 아이콘: 버튼 65%
|
|
49
|
+
- gap: `space-1`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Message Quote
|
|
2
|
+
|
|
3
|
+
### 원칙
|
|
4
|
+
[[components/ai-message]] 안의 인용 블록. `plain`(그냥 글)과 `blockquote`(왼쪽 vertical bar 리더) 두 variant. [[components/quotation-bar]]는 ChatInput의 "접힌 인용"이고 — Message Quote는 **메시지 본문 안에 펼친 인용**.
|
|
5
|
+
|
|
6
|
+
### Do
|
|
7
|
+
- `plain`은 다른 글과 톤이 거의 같음 — 가볍게 "참조 중" 암시.
|
|
8
|
+
- `blockquote`는 왼쪽에 bar — markdown `>` 와 동일 톤.
|
|
9
|
+
- 색은 blockquote=fg-muted, plain=fg (본문과 같은 톤).
|
|
10
|
+
|
|
11
|
+
### Don't
|
|
12
|
+
- Quote 안에 아이콘·메타 라벨 넣지 않는다 — 소스/저자 표시가 필요하면 [[components/message-sources]] 또는 상위에서.
|
|
13
|
+
- 중첩 blockquote는 지원하지 않음.
|
|
14
|
+
|
|
15
|
+
### 관련
|
|
16
|
+
- [[components/quotation-bar]] — ChatInput의 접힌 인용
|
|
17
|
+
- [[components/ai-message]] — 상위 소비처
|
|
18
|
+
|
|
19
|
+
### Props 시그니처
|
|
20
|
+
```tsx
|
|
21
|
+
<MessageQuote variant="plain | blockquote">본문</MessageQuote>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 레이아웃
|
|
25
|
+
- plain: 외곽 장식 없음
|
|
26
|
+
- blockquote: `padding-left: space-4`, `border-left: space-0_5 solid border-strong`, color `fg-muted`
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Message Sources
|
|
2
|
+
|
|
3
|
+
### 원칙
|
|
4
|
+
AIMessage의 **하단 액션바 옆에 inline으로 붙는 작은 chip**. 겹친 favicon stack + 첫 소스 라벨 + `+N` 패턴. 블록이 아니라 액션 액세서리 — 실제론 [[components/ai-message]]의 `sources` prop으로 전달해 자동 배치.
|
|
5
|
+
|
|
6
|
+
### Do
|
|
7
|
+
- `<AIMessage sources={...}>` prop으로 전달. standalone으로 쓸 일은 거의 없음.
|
|
8
|
+
- `maxFavicons`로 겹쳐 보여줄 favicon 개수 조정 (기본 3). 나머지는 `+N`.
|
|
9
|
+
- 각 소스에 `icon` 넘겨 favicon을 명시, 없으면 중성 원형 placeholder.
|
|
10
|
+
- 클릭 시 소비처에서 popover(소스 전체 리스트) 열기 — 디자인 시스템 범위 밖.
|
|
11
|
+
|
|
12
|
+
### Don't
|
|
13
|
+
- 큰 chip 리스트로 늘려 메시지 하단에 깔지 않는다 (예전 디자인 — 폐기).
|
|
14
|
+
- Sources를 메시지 본문 중간에 두지 않는다 — 반드시 하단 액션바 라인.
|
|
15
|
+
- chip 안에 긴 설명·날짜 넣지 않는다 — 라벨 하나만.
|
|
16
|
+
|
|
17
|
+
### Props 시그니처
|
|
18
|
+
```tsx
|
|
19
|
+
<MessageSources
|
|
20
|
+
sources={[{ id, label, href?, icon? }]}
|
|
21
|
+
label="Sources" // default
|
|
22
|
+
maxFavicons={3} // default
|
|
23
|
+
onClick={() => void} // 소스 전체 popover 열기 등
|
|
24
|
+
/>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 레이아웃
|
|
28
|
+
- 외곽: ghost pill (transparent → hover bg-subtle)
|
|
29
|
+
- padding: `space-0_5 space-2 space-0_5 space-1`
|
|
30
|
+
- 폰트: `text-xs`, `fg-muted` → hover `fg-strong`
|
|
31
|
+
- favicon stack: 각 원 `space-4`(16px), 서로 `space-1` negative margin으로 겹침
|
|
32
|
+
- 각 favicon에 box-shadow로 bg 색 ring (cutout 효과)
|
|
33
|
+
- `+N`은 `fg-muted`, tabular-nums
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Message Table
|
|
2
|
+
|
|
3
|
+
### 원칙
|
|
4
|
+
[[components/ai-message]] 안의 markdown table 톤 wrapper. 소비처가 `<table><thead><tr><th>…` 생짜 그대로 전달하면 CTDS 톤(hairline border, radius-md, header bg)으로 자동 스타일. 가로 overflow 시 wrapper가 scroll.
|
|
5
|
+
|
|
6
|
+
### Do
|
|
7
|
+
- 생짜 `<table>` 전달 — markdown parser(remark 등)가 뱉은 걸 그대로 wrap.
|
|
8
|
+
- 숫자 셀도 **좌측 정렬 유지** ([[feedback-no-right-align-numbers]]). tabular-nums로 폭만 균일.
|
|
9
|
+
- 가로가 많으면 wrapper 안에서 scroll — 테이블 wrap 깨트리지 않음.
|
|
10
|
+
|
|
11
|
+
### Don't
|
|
12
|
+
- 테이블 안에 생짜 `<button>`·control 넣지 않는다. 그건 TableRow/TableRowEdit 영역.
|
|
13
|
+
- 숫자 셀 right-align 금지.
|
|
14
|
+
|
|
15
|
+
### Props 시그니처
|
|
16
|
+
```tsx
|
|
17
|
+
<MessageTable>
|
|
18
|
+
<table>...</table>
|
|
19
|
+
</MessageTable>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 레이아웃
|
|
23
|
+
- wrapper: hairline border, radius-md, overflow-x auto
|
|
24
|
+
- thead: `bg-subtle`
|
|
25
|
+
- cell padding: `space-2 space-3`
|
|
26
|
+
- cell border-bottom: hairline (마지막 행 제외)
|
|
27
|
+
- 숫자: `font-variant-numeric: tabular-nums`
|
|
@@ -15,6 +15,7 @@ Sidebar는 **Header / Body / Footer 세 영역으로 강제 분리**된다. 역
|
|
|
15
15
|
Header는 **가로 행**이기 때문에 Section을 넣으면 nav 전체가 가로 1줄로 눌려 겹친다. Section·Item·Separator는 반드시 `Sidebar.Body` 안에.
|
|
16
16
|
|
|
17
17
|
### Do
|
|
18
|
+
- **Header/Footer의 워크스페이스·프로필·조직 트리거는 반드시 [[components/identity-button]]** — Select·Input Dropdown·수제 Button+span 조합 전부 금지. Avatar + 2줄 라벨 + trailing 뼈대가 고정이라 전용 컴포넌트가 담당. 사이드바에서 "현재 X 선택 상태 + 전환 가능" 모양이 보이면 100% IdentityButton.
|
|
18
19
|
- Section·Separator는 반드시 **`Sidebar.Body` 안에** 둔다 — Body만 스크롤되는 영역.
|
|
19
20
|
- 라우트 이동은 Item에 `asChild` + Next `<Link>`. 단, `asChild`일 땐 라벨 truncation이 `<Link>` 쪽 책임이 된다 ([[components/menu-item]] Slot 동작) — 긴 라벨이면 Link에 `truncate` 유틸 또는 `title` 속성을 걸 것.
|
|
20
21
|
- 현재 라우트는 `active`로 명시 (MenuItem의 active 시각 그대로).
|
|
@@ -24,6 +25,8 @@ Header는 **가로 행**이기 때문에 Section을 넣으면 nav 전체가 가
|
|
|
24
25
|
- 콘텐츠 영역과의 **경계선은 [[components/app-frame]] `variant="outlined"`** 가 그린다 — Sidebar 자체는 border를 그리지 않음. 사이드바가 녹아 보이면 AppFrame variant부터 확인.
|
|
25
26
|
|
|
26
27
|
### Don't
|
|
28
|
+
- **워크스페이스/프로필 트리거에 Select·Input Dropdown 금지** — 폼 값 선택과 네비게이션 액션은 다른 역할. [[components/identity-button]] + DropdownMenu 조합이 유일한 표준 ([[antipattern_workspace_switcher]]).
|
|
29
|
+
- **수제 Button + span 조립 금지** — 매번 레이아웃·폰트·collapsed 전환이 어긋나서 일관성이 깨짐. IdentityButton 하나로 끝.
|
|
27
30
|
- **Header에 Section·Item·nav를 넣지 않는다** — Header는 가로 flex 행이라 세로 nav가 깔리면 전부 눌려 겹친다. Brand 전용.
|
|
28
31
|
- 페이지 안 뷰 전환에 쓰지 않는다 — 그건 [[components/tabs]].
|
|
29
32
|
- Item 안에 variant로 다른 톤을 쓰지 않는다. active 외의 강조 금지.
|
|
@@ -35,6 +38,7 @@ Header는 **가로 행**이기 때문에 Section을 넣으면 nav 전체가 가
|
|
|
35
38
|
collapsed 결정 애매하면 `false`. Section title 애매하면 생략. 경계선 애매하면 AppFrame `variant="outlined"`.
|
|
36
39
|
|
|
37
40
|
### 관련
|
|
41
|
+
- [[components/identity-button]] — Header/Footer 워크스페이스·프로필·조직 트리거. Sidebar.Profile은 이 컴포넌트의 preset 래퍼.
|
|
38
42
|
- [[components/menu-item]] — Item 시각·상태 전부 재사용
|
|
39
43
|
- [[components/avatar]] — Profile 왼쪽 아바타
|
|
40
44
|
- [[components/app-frame]] — Sidebar를 담는 grid 쉘
|
|
@@ -89,10 +93,24 @@ collapsed 결정 애매하면 `false`. Section title 애매하면 생략. 경계
|
|
|
89
93
|
|
|
90
94
|
### 조합 예시
|
|
91
95
|
```tsx
|
|
92
|
-
//
|
|
96
|
+
// 1. 워크스페이스 트리거 Header + Section 2개 + Footer Profile (가장 흔한 구성)
|
|
93
97
|
<Sidebar>
|
|
94
98
|
<Sidebar.Header>
|
|
95
|
-
<
|
|
99
|
+
<DropdownMenu>
|
|
100
|
+
<DropdownMenu.Trigger asChild>
|
|
101
|
+
<IdentityButton
|
|
102
|
+
size="lg"
|
|
103
|
+
avatar={{ initials: "T", shape: "rounded" }}
|
|
104
|
+
name="Taskify PM"
|
|
105
|
+
sub="Project Workspace"
|
|
106
|
+
trailing={<Icon name="caret-down" size={14} />}
|
|
107
|
+
aria-haspopup="menu"
|
|
108
|
+
/>
|
|
109
|
+
</DropdownMenu.Trigger>
|
|
110
|
+
<DropdownMenu.Content>
|
|
111
|
+
{/* 워크스페이스 리스트 + Separator + "+ 새 워크스페이스" */}
|
|
112
|
+
</DropdownMenu.Content>
|
|
113
|
+
</DropdownMenu>
|
|
96
114
|
</Sidebar.Header>
|
|
97
115
|
|
|
98
116
|
<Sidebar.Body>
|
|
@@ -113,11 +131,38 @@ collapsed 결정 애매하면 `false`. Section title 애매하면 생략. 경계
|
|
|
113
131
|
</Sidebar.Body>
|
|
114
132
|
|
|
115
133
|
<Sidebar.Footer>
|
|
134
|
+
{/* Sidebar.Profile은 IdentityButton의 preset 래퍼 — 커스텀하려면 IdentityButton 직접 사용. */}
|
|
116
135
|
<Sidebar.Profile name="희영" sub="admin@ctds.dev" avatar={{ status: "online" }} />
|
|
117
136
|
</Sidebar.Footer>
|
|
118
137
|
</Sidebar>
|
|
119
138
|
|
|
120
|
-
//
|
|
139
|
+
// 2. Brand 로고 전용 Header (워크스페이스 전환이 필요 없는 단일 제품)
|
|
140
|
+
<Sidebar>
|
|
141
|
+
<Sidebar.Header>
|
|
142
|
+
<Sidebar.Brand symbol={<Icon name="rocket-launch" />} wordmark="CTDS" />
|
|
143
|
+
</Sidebar.Header>
|
|
144
|
+
{/* ... Body / Footer */}
|
|
145
|
+
</Sidebar>
|
|
146
|
+
|
|
147
|
+
// 3. Footer 프로필 + 위로 열리는 메뉴 (caret-up)
|
|
148
|
+
<Sidebar.Footer>
|
|
149
|
+
<DropdownMenu>
|
|
150
|
+
<DropdownMenu.Trigger asChild>
|
|
151
|
+
<IdentityButton
|
|
152
|
+
avatar={{ src: user.avatar, status: "online" }}
|
|
153
|
+
name="희영"
|
|
154
|
+
sub="admin@ctds.dev"
|
|
155
|
+
trailing={<Icon name="caret-up" size={14} />}
|
|
156
|
+
aria-haspopup="menu"
|
|
157
|
+
/>
|
|
158
|
+
</DropdownMenu.Trigger>
|
|
159
|
+
<DropdownMenu.Content side="top" align="start">
|
|
160
|
+
{/* 계정 전환 · 설정 · 로그아웃 */}
|
|
161
|
+
</DropdownMenu.Content>
|
|
162
|
+
</DropdownMenu>
|
|
163
|
+
</Sidebar.Footer>
|
|
164
|
+
|
|
165
|
+
// 4. Collapsed — icon-only (IdentityButton·Profile 모두 자동으로 아바타만 남음)
|
|
121
166
|
<Sidebar collapsed>
|
|
122
167
|
<Sidebar.Header>
|
|
123
168
|
<Sidebar.Brand symbol={<Icon name="rocket-launch" />} />
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Spinner
|
|
2
|
+
|
|
3
|
+
### 원칙
|
|
4
|
+
짧은 비동기 작업의 **로딩 신호**. 색은 currentColor로 묶여 부모 톤을 그대로 승계. **variant 미지정이 기본** — [[axes/motion]] 축의 `--loader-style`을 따라가서 bouncy=dots-bounce / standard=ring / calm=dots-fade로 자동 스왑. "버튼 안 로딩이 테마 바꾸면 같이 바뀌는" 축 추종 구조가 설계의 핵심이다. 긴 작업(파일 업로드 등 진행률이 있는 것)은 [[components/progress-bar]].
|
|
5
|
+
|
|
6
|
+
### Do
|
|
7
|
+
- Button `loading` prop이 자동으로 Spinner를 중앙에 overlay — 소비자가 직접 Spinner 꽂을 필요 없음.
|
|
8
|
+
- 색이 필요하면 부모 요소에 `color: var(--accent)` 식으로 지정 — Spinner는 그대로 승계.
|
|
9
|
+
- 페이지 로딩이면 중앙에 size=`lg` 하나로 충분. 과한 fullscreen overlay는 피한다.
|
|
10
|
+
- variant 미지정이 default — Motion 축 바꾸면 모든 로더가 함께 바뀌는 게 설계 의도.
|
|
11
|
+
|
|
12
|
+
### Don't
|
|
13
|
+
- 진행률(퍼센트) 로딩에 쓰지 않는다 — [[components/progress-bar]].
|
|
14
|
+
- 스켈레톤 자리에 Spinner 쓰지 않는다 — 레이아웃이 이미 그려지는 자리면 shimmer skeleton이 맞음.
|
|
15
|
+
- 색을 하드코딩하지 않는다. currentColor가 부모 색을 승계하도록 두거나, 부모에 토큰 색을 걸 것.
|
|
16
|
+
- `size="xs"`는 아이콘 크기 로더 전용(Button xs 안쪽). 독립적으로 쓸 땐 sm 이상.
|
|
17
|
+
- variant를 소비 측에서 자꾸 override하지 않는다 — 축 추종이 "브랜드 톤 일관성"의 유일한 보장.
|
|
18
|
+
|
|
19
|
+
### Fallback
|
|
20
|
+
`size="sm"`, variant 미지정(= Motion 축 따라감), `label="Loading"`.
|
|
21
|
+
|
|
22
|
+
### 관련
|
|
23
|
+
- [[axes/motion]] — `--loader-style` 토큰을 축이 스왑
|
|
24
|
+
- [[components/button]] — `loading` prop이 Spinner를 자동 overlay
|
|
25
|
+
- [[components/progress-bar]] — 진행률 표시용 (Spinner와 역할 분리)
|
|
26
|
+
|
|
27
|
+
### Props 시그니처
|
|
28
|
+
```tsx
|
|
29
|
+
<Spinner
|
|
30
|
+
size="xs | sm | md | lg" // default: sm
|
|
31
|
+
variant="ring | dots-bounce | dots-fade | pulse | lottie-<name>" // default: Motion 축 따름
|
|
32
|
+
label={string} // SR 라벨. default: "Loading"
|
|
33
|
+
/>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Variant 매트릭스
|
|
37
|
+
| variant | 시각 | 쓰임 |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| ring | 회전 반원 테두리 | 표준·중립. standard motion 자동 매핑 |
|
|
40
|
+
| dots-bounce | 3 dot 순차 bounce | 표현력. bouncy motion 자동 매핑 |
|
|
41
|
+
| dots-fade | 3 dot 순차 fade (위치 고정) | 미니멀. calm motion 자동 매핑 |
|
|
42
|
+
| pulse | 단일 원 scale pulse | 특수(온보딩·대기 신호). 자동 매핑 없음 — 명시 지정만 |
|
|
43
|
+
| `lottie-<name>` | 번들된 Lottie 로티 | 성격 있는 자리(AI 생성·온보딩·브랜드) |
|
|
44
|
+
|
|
45
|
+
### Lottie 로티 (10종 번들)
|
|
46
|
+
`lottie-react` 플레이어가 `/public/lottie/<name>.json`을 로드. 단색 로티는 `currentColor` 추종, 다색 로티는 원본 색 유지.
|
|
47
|
+
|
|
48
|
+
| name | 성격 | colorBind |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `lottie-rabbit` | 뛰어가는 토끼. 긴 작업·AI 생성 대기 | ✓ |
|
|
51
|
+
| `lottie-infiniti` | 무한 루프 선. 네트워크·동기화 | ✓ |
|
|
52
|
+
| `lottie-dots` | 그라데이션 dot 흐름. 포인트 로더 | — |
|
|
53
|
+
| `lottie-spinner` | 표준 라인 스피너 | ✓ |
|
|
54
|
+
| `lottie-clock` | 회전 시계 바늘. 처리 대기·타임아웃 | ✓ |
|
|
55
|
+
| `lottie-cosmos` | 그라데이션 궤도. 몽환적·온보딩 | ✓ |
|
|
56
|
+
| `lottie-diamond-star` | 다이아몬드 바운스. 성공·축하 | — |
|
|
57
|
+
| `lottie-image-gen` | 이미지 생성 대기. AI 이미지 | ✓ |
|
|
58
|
+
| `lottie-loading-square` | 회전하는 사각형. 미니멀 처리 | ✓ |
|
|
59
|
+
|
|
60
|
+
**colorBind=✓** 로티는 Palette/Hue 축 바꾸면 로더 색도 자동 추종. **✗** 로티는 작가 의도된 다색이 그대로 유지됨.
|
|
61
|
+
|
|
62
|
+
**사이즈 주의**: Lottie는 vectorized라 size가 작으면 디테일 뭉개짐. `size="md"` 이상 권장. Button loading 안에 Lottie 쓰는 건 비권장 — 기본 variant(dots/ring)가 적합.
|
|
63
|
+
|
|
64
|
+
### Size 매트릭스
|
|
65
|
+
IconButton 체계와 1:1 매칭. 로더는 캔버스를 다 채우지 않는 특성상 아이콘보다 한 단계 큰 bounds가 체감에 맞음.
|
|
66
|
+
|
|
67
|
+
| size | 실제 크기 | 쓰임 |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| xs | 16px | Button xs 내부, 조밀한 인라인 |
|
|
70
|
+
| sm | 20px | **기본**. 인라인 라벨 옆, 리스트 아이템, AIMessage generating |
|
|
71
|
+
| md | 24px | 섹션 로딩, 카드 중앙 |
|
|
72
|
+
| lg | 32px | 페이지 중앙 로딩, 모달 바디 로딩 |
|
|
73
|
+
|
|
74
|
+
Lottie 로더는 캔버스 충전율이 다르므로 `visualScale`로 bounds를 꽉 채우도록 보정됨 — size만 고르면 체감이 일관되게 맞아야 함. 특정 Lottie가 작아 보이면 `lottieManifest.ts`의 `visualScale`을 올려 글로벌 보정, 사용처에서 size를 올리지 말 것.
|
|
75
|
+
|
|
76
|
+
### 축 추종 매핑 (variant 미지정 시)
|
|
77
|
+
| Motion | Spinner 자동 variant |
|
|
78
|
+
|---|---|
|
|
79
|
+
| bouncy | dots-bounce |
|
|
80
|
+
| standard | ring |
|
|
81
|
+
| calm | dots-fade |
|
|
82
|
+
| none | ring (접근성 예외: 로딩 신호는 유지. `prefers-reduced-motion`은 상위 CSS가 처리) |
|
|
83
|
+
|
|
84
|
+
### 색 바인딩
|
|
85
|
+
Spinner는 `currentColor` 바인딩. 즉:
|
|
86
|
+
- Button primary 안에서는 Button의 fg 색(흰색 등)
|
|
87
|
+
- Button outline 안에서는 fg 기본색
|
|
88
|
+
- 커스텀 자리에 색이 필요하면 부모에 `color: var(--accent)` 식으로 걸 것
|
|
89
|
+
|
|
90
|
+
Palette·Hue 축이 바뀌어도 Spinner는 자동 추종 — 별도 색 지정 금지.
|
|
91
|
+
|
|
92
|
+
### 조합 예시
|
|
93
|
+
```tsx
|
|
94
|
+
// Button loading — 소비자가 신경쓸 거 없음 (Button이 자동 처리)
|
|
95
|
+
<Button loading>저장</Button>
|
|
96
|
+
|
|
97
|
+
// 페이지 중앙 로딩
|
|
98
|
+
<div style={{ display: "grid", placeItems: "center", minHeight: 240 }}>
|
|
99
|
+
<Spinner size="lg" />
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
// 리스트 아이템 inline 로딩
|
|
103
|
+
<li>
|
|
104
|
+
<Spinner size="xs" /> 불러오는 중
|
|
105
|
+
</li>
|
|
106
|
+
|
|
107
|
+
// 축 추종 끄고 명시 variant
|
|
108
|
+
<Spinner variant="pulse" size="md" />
|
|
109
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# User Message
|
|
2
|
+
|
|
3
|
+
### 원칙
|
|
4
|
+
사용자가 보낸 채팅 메시지. [[components/bubble]] + [[components/message-actions]] 조합. **오른쪽 정렬** 고정. 액션은 기본 호버 시만 노출 — 메시지 리스트가 액션 아이콘으로 어수선해지지 않게.
|
|
5
|
+
|
|
6
|
+
### Do
|
|
7
|
+
- 기본 구성: `shape=circular-drop, fill=translucent, tail=end` — 레퍼런스 톤.
|
|
8
|
+
- 액션은 `[copy, edit]` 기본. `actionsVisibility="show" | "hover"`로 상시/호버.
|
|
9
|
+
- 첨부 있으면 `before` slot에 [[components/attachment-thumb]] / [[components/attachment-chip]] 자유 조합. 상위에서 레이아웃 책임.
|
|
10
|
+
- 매우 긴 글은 bubble이 max-width 안에서 wrap, 그보다 넘으면 자연스레 아래로.
|
|
11
|
+
|
|
12
|
+
### Don't
|
|
13
|
+
- 왼쪽 정렬하지 않는다 — user는 **항상 오른쪽**. 왼쪽은 AI/시스템.
|
|
14
|
+
- 액션 라벨을 아이콘 옆에 노출하지 않는다 — title/aria-label만 (MessageActions 규약).
|
|
15
|
+
- bubble 안에 복잡한 레이아웃(table·code block) 넣지 않는다 — 그건 AIMessage 영역.
|
|
16
|
+
- 아바타·이름 넣지 않는다 — user 메시지는 암묵적으로 "나".
|
|
17
|
+
|
|
18
|
+
### Fallback
|
|
19
|
+
shape 애매하면 `circular-drop`, fill 애매하면 `translucent`, visibility 애매하면 `hover`.
|
|
20
|
+
|
|
21
|
+
### 관련
|
|
22
|
+
- [[components/bubble]] — 담는 그릇
|
|
23
|
+
- [[components/message-actions]] — 액션 바
|
|
24
|
+
- [[components/ai-message]] — 짝 컴포넌트
|
|
25
|
+
- [[components/attachment-thumb]] / [[components/attachment-chip]] — before slot 조합
|
|
26
|
+
|
|
27
|
+
### Props 시그니처
|
|
28
|
+
```tsx
|
|
29
|
+
<UserMessage
|
|
30
|
+
shape="rounded | circular-drop | circular | square" // default: circular-drop
|
|
31
|
+
fill="none | solid | translucent | outline" // default: translucent
|
|
32
|
+
size="md | lg" // default: md
|
|
33
|
+
actionsVisibility="show | hover" // default: hover
|
|
34
|
+
actions={[{ kind: "copy" }, { kind: "edit" }]} // 비우려면 []
|
|
35
|
+
before={ReactNode} // 첨부·인용 등 bubble 위
|
|
36
|
+
>
|
|
37
|
+
메시지 본문
|
|
38
|
+
</UserMessage>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 레이아웃
|
|
42
|
+
- root: `flex-direction: column; align-items: flex-end; gap: space-1; width: 100%`
|
|
43
|
+
- 메시지 리스트 안에서 각 UserMessage가 오른쪽에 자동 정렬됨.
|
|
44
|
+
- Bubble `tail="end"` 고정 — 오른쪽 상단 각짐.
|
|
45
|
+
- MessageActions `align="end"`.
|
|
@@ -99,14 +99,20 @@
|
|
|
99
99
|
|
|
100
100
|
### 정답 패턴 (ChatInput이 쓰는 구조)
|
|
101
101
|
|
|
102
|
+
핵심 결정: **wrapper에 padding 0, textarea에 상·하·좌·우 padding 전부**. 왜냐하면
|
|
103
|
+
`overflow: auto` 요소에 padding을 두면 그 padding은 **scroll 시 고정** — 스크롤해도
|
|
104
|
+
빈 띠가 상·하에 남아 "글이 padding에서 미리 잘린다"는 느낌을 줌. 반면 textarea의
|
|
105
|
+
padding은 **자신의 content box 일부**로 취급돼, textarea 전체가 wrapper 안에서 scroll
|
|
106
|
+
되면 padding도 함께 올라감.
|
|
107
|
+
|
|
102
108
|
1. **root는 `padding: 0; overflow: hidden`** — scroll clip이 root border에서 생기도록.
|
|
103
|
-
2.
|
|
104
|
-
3. **textarea는
|
|
109
|
+
2. **wrapper div(`.scroll`)는 `overflow-y: auto; padding: 0`** — padding 두면 "빈 띠" 생김.
|
|
110
|
+
3. **textarea는 상·하·좌·우 padding 전부 + `overflow: hidden`** — 자체 scroll 금지.
|
|
105
111
|
|
|
106
112
|
```css
|
|
107
113
|
.root {
|
|
108
114
|
padding: 0;
|
|
109
|
-
overflow: hidden;
|
|
115
|
+
overflow: hidden;
|
|
110
116
|
border-radius: var(--radius-sm);
|
|
111
117
|
border: var(--hairline) solid var(--border);
|
|
112
118
|
}
|
|
@@ -114,28 +120,33 @@
|
|
|
114
120
|
.scroll {
|
|
115
121
|
overflow-y: auto;
|
|
116
122
|
max-height: calc(var(--text-base) * 1.5 * 8);
|
|
117
|
-
padding:
|
|
123
|
+
padding: 0; /* 반드시 0 */
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
.textarea {
|
|
121
127
|
display: block;
|
|
122
128
|
width: 100%;
|
|
123
|
-
|
|
129
|
+
box-sizing: border-box;
|
|
130
|
+
padding: var(--space-3) var(--space-4); /* 상·하·좌·우 모두 */
|
|
124
131
|
background: transparent;
|
|
125
132
|
border: 0;
|
|
126
133
|
outline: none;
|
|
127
134
|
resize: none;
|
|
128
|
-
overflow: hidden;
|
|
135
|
+
overflow: hidden; /* textarea 자체 scroll 금지 */
|
|
129
136
|
font: inherit;
|
|
130
137
|
line-height: 1.5;
|
|
131
138
|
}
|
|
132
139
|
```
|
|
133
140
|
|
|
134
|
-
이
|
|
135
|
-
|
|
136
|
-
- wrapper max-height
|
|
137
|
-
-
|
|
138
|
-
-
|
|
141
|
+
이 구조에서:
|
|
142
|
+
- **짧은 글**: textarea가 자신의 padding 포함 높이 → wrapper 안에 스크롤 없음.
|
|
143
|
+
- **긴 글**: textarea height > wrapper max-height → wrapper 스크롤 시 textarea 전체가 올라감.
|
|
144
|
+
- 상단에서 textarea padding-top을 지나며 글이 **root 상단 border에서 자연스레 clip**.
|
|
145
|
+
- 하단에서 textarea padding-bottom을 지나며 글이 **root 하단 border에서 자연스레 clip**.
|
|
146
|
+
|
|
147
|
+
### 금지 패턴 추가 (이번 삽질로 확인)
|
|
148
|
+
|
|
149
|
+
❌ **"wrapper에 padding-top, textarea에 padding-bottom"** — 전에 썼던 패턴. wrapper의 padding-top이 scroll 시 "빈 띠"로 남아 글이 그 아래에서 clip돼 어색함.
|
|
139
150
|
|
|
140
151
|
### 함께 적용되는 레이아웃 룰
|
|
141
152
|
|