@conscience-technology/ctds 0.1.8 → 0.1.10

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.
Files changed (78) hide show
  1. package/assets/components/AIMessage/AIMessage.module.css +117 -0
  2. package/assets/components/AIMessage/AIMessage.tsx +90 -0
  3. package/assets/components/AIMessage/index.ts +2 -0
  4. package/assets/components/AttachmentChip/AttachmentChip.module.css +3 -1
  5. package/assets/components/AttachmentThumb/AttachmentThumb.module.css +13 -7
  6. package/assets/components/AttachmentThumb/AttachmentThumb.tsx +4 -1
  7. package/assets/components/Bubble/Bubble.module.css +65 -0
  8. package/assets/components/Bubble/Bubble.tsx +63 -0
  9. package/assets/components/Bubble/index.ts +2 -0
  10. package/assets/components/Button/Button.module.css +15 -2
  11. package/assets/components/Button/Button.tsx +13 -0
  12. package/assets/components/Calendar/Calendar.module.css +163 -0
  13. package/assets/components/Calendar/Calendar.tsx +323 -0
  14. package/assets/components/Calendar/index.ts +2 -0
  15. package/assets/components/ChatInput/ChatInput.module.css +81 -41
  16. package/assets/components/ChatInput/ChatInput.tsx +45 -32
  17. package/assets/components/ChatInput/ModelSelector.tsx +23 -27
  18. package/assets/components/ChatInput/index.ts +0 -1
  19. package/assets/components/CodeBlock/CodeBlock.module.css +79 -0
  20. package/assets/components/CodeBlock/CodeBlock.tsx +71 -0
  21. package/assets/components/CodeBlock/index.ts +2 -0
  22. package/assets/components/ContextMenu/ContextMenu.tsx +10 -1
  23. package/assets/components/DropdownMenu/DropdownMenu.module.css +26 -0
  24. package/assets/components/DropdownMenu/DropdownMenu.tsx +367 -0
  25. package/assets/components/DropdownMenu/index.ts +9 -0
  26. package/assets/components/EventChip/EventChip.module.css +57 -0
  27. package/assets/components/EventChip/EventChip.tsx +41 -0
  28. package/assets/components/EventChip/index.ts +2 -0
  29. package/assets/components/Icon/Icon.tsx +3 -0
  30. package/assets/components/IdentityButton/IdentityButton.module.css +119 -0
  31. package/assets/components/IdentityButton/IdentityButton.tsx +113 -0
  32. package/assets/components/IdentityButton/index.ts +6 -0
  33. package/assets/components/MenuItem/MenuItem.module.css +67 -2
  34. package/assets/components/MenuItem/MenuItem.tsx +22 -2
  35. package/assets/components/MessageActions/MessageActions.module.css +43 -0
  36. package/assets/components/MessageActions/MessageActions.tsx +94 -0
  37. package/assets/components/MessageActions/index.ts +6 -0
  38. package/assets/components/MessageQuote/MessageQuote.module.css +18 -0
  39. package/assets/components/MessageQuote/MessageQuote.tsx +33 -0
  40. package/assets/components/MessageQuote/index.ts +2 -0
  41. package/assets/components/MessageSources/MessageSources.module.css +63 -0
  42. package/assets/components/MessageSources/MessageSources.tsx +50 -0
  43. package/assets/components/MessageSources/index.ts +2 -0
  44. package/assets/components/MessageTable/MessageTable.module.css +46 -0
  45. package/assets/components/MessageTable/MessageTable.tsx +21 -0
  46. package/assets/components/MessageTable/index.ts +2 -0
  47. package/assets/components/QuotationBar/QuotationBar.module.css +2 -2
  48. package/assets/components/Sidebar/Sidebar.module.css +3 -57
  49. package/assets/components/Sidebar/Sidebar.tsx +18 -21
  50. package/assets/components/Spinner/LottiePlayer.tsx +75 -0
  51. package/assets/components/Spinner/Spinner.module.css +167 -0
  52. package/assets/components/Spinner/Spinner.tsx +119 -0
  53. package/assets/components/Spinner/index.ts +10 -0
  54. package/assets/components/Spinner/lottieColor.ts +76 -0
  55. package/assets/components/Spinner/lottieManifest.ts +99 -0
  56. package/assets/components/Tabs/Tabs.module.css +3 -3
  57. package/assets/components/UserMessage/UserMessage.module.css +41 -0
  58. package/assets/components/UserMessage/UserMessage.tsx +66 -0
  59. package/assets/components/UserMessage/index.ts +2 -0
  60. package/assets/components/_data.ts +18 -4
  61. package/assets/components/tokens.css +20 -0
  62. package/assets/prompts/components/ai-message.md +43 -0
  63. package/assets/prompts/components/bubble.md +56 -0
  64. package/assets/prompts/components/calendar.md +123 -0
  65. package/assets/prompts/components/code-block.md +35 -0
  66. package/assets/prompts/components/dropdown-menu.md +145 -0
  67. package/assets/prompts/components/identity-button.md +168 -0
  68. package/assets/prompts/components/menu-item.md +2 -1
  69. package/assets/prompts/components/message-actions.md +49 -0
  70. package/assets/prompts/components/message-quote.md +26 -0
  71. package/assets/prompts/components/message-sources.md +26 -0
  72. package/assets/prompts/components/message-table.md +27 -0
  73. package/assets/prompts/components/sidebar.md +106 -37
  74. package/assets/prompts/components/spinner.md +109 -0
  75. package/assets/prompts/components/user-message.md +45 -0
  76. package/assets/prompts/primitives/scroll.md +22 -11
  77. package/assets/skill/SKILL.md +1 -0
  78. package/package.json +1 -1
@@ -0,0 +1,123 @@
1
+ # Calendar
2
+
3
+ ### 원칙
4
+
5
+ 페이지에 **인라인**으로 들어가는 월간 캘린더. 날짜 하나를 "고르기만 하는" [[components/date-picker]]와는 역할이 완전히 다르다 — Calendar는 **콘텐츠로서의 달력**이다. 셀마다 이벤트 pill이 쌓이고, 선택된 날짜가 페이지 다른 영역(사이드 리스트·상세 패널)의 입력이 된다. 그래서 Popover 안에 넣지 않는다. 날짜 입력이 필요하면 DatePicker, 월간 뷰가 필요하면 Calendar.
6
+
7
+ ### Do
8
+ - `month`는 controlled — 외부 상태로 쥔다. `onMonthChange` 짝. `selectedDate`도 controlled.
9
+ - 이벤트 pill의 `tone`은 **의미**에 매핑. 색은 7개 토큰(default/accent/success/warning/danger/info/neutral)만 — Presto류 14색 팔레트 재현 금지.
10
+ - 셀을 넘치는 이벤트는 자동으로 `+N` 뱃지로 접힌다. 내부 스크롤을 만들지 않는다 — 셀 안 스크롤은 밀도 신호를 깬다.
11
+ - `weekStartsOn`은 기본 `1` (월요일) — 업무 캘린더 관행. 생활 캘린더는 `0`으로 명시.
12
+ - 토요일은 `--info`, 일요일은 `--destructive` 톤. 이 두 색만 요일 신호로 쓰고 임의 색 추가 금지.
13
+ - 선택된 셀은 inset ring(--primary)로만 신호한다. 배경을 primary로 채우지 않는다 — 그 자리는 이벤트 pill 공간이다.
14
+ - 보더는 그리드가 top/left를, 각 셀이 right/bottom을 그린다 — 열 수가 바뀌어도(5↔7) 왼쪽 세로선이 안정적으로 유지된다. 셀 nth-child 보더 제거 트릭 쓰지 말 것.
15
+
16
+ ### Don't
17
+ - Popover로 감싸지 않는다 — 이건 DatePicker 자리. Calendar는 페이지 레이아웃의 블록 영역을 차지한다.
18
+ - 셀 안에 "클릭 가능한 이벤트 pill"을 만들지 않는다 — 셀 전체가 하나의 클릭 타겟이고, 상세는 사이드 패널·Drawer에서 연다. pill 안에 또 버튼이 들어가면 클릭 영역이 미궁에 빠진다.
19
+ - 요일 헤더에 "월요일/화요일" 두 글자 표기 금지 — "월·화" 한 글자. 두 글자는 가로 공간을 먹고 셀 수가 줄어든 느낌을 준다.
20
+ - 하루 셀에 이벤트 pill 5개 이상 노출 금지. 넘치면 `+N`으로 접고 선택 시 사이드 리스트에 전부 뿌린다.
21
+ - `showWeekend=false`일 때 토·일 셀 자리를 유지하지 않는다 — 숨길 거면 열 자체를 5열로 접는다.
22
+ - 이벤트 pill에 `pill` radius(완전 둥근) 쓰지 않는다 — Shape 축이 radius를 쥐고 있는데 pill로 고정하면 Soft/Standard/Crisp 신호가 셀 안에서 깨진다.
23
+ - Calendar 자체 상단바에 "주말 숨김" 류 **뷰 옵션 토글**을 넣지 않는다 — Calendar는 콘텐츠 블록이고, 뷰 옵션은 호스트 화면의 툴바 영역이 담당한다. 외부 state로 `showWeekend`를 넘겨 조립할 것. (월 이동·오늘 복귀는 Calendar 내부 — 뷰 옵션이 아니라 "지금 보는 좌표" 조작이므로 예외.)
24
+
25
+ ### Fallback
26
+ `today` 생략하면 `new Date()`. `selectedDate` 없이도 성립 — 그냥 뷰어로 쓸 수 있다. `events` 빈 배열이어도 그리드만 그려진다. `tone` 애매하면 `default`.
27
+
28
+ ### 관련
29
+ - [[components/date-picker]] — "날짜 입력"이 목적이면 그쪽. Popover + Input 트리거. Calendar와 utils(buildMonthGrid 등) 공유.
30
+ - [[components/badge]] — 이벤트 pill의 톤 체계는 Badge soft와 동일 토큰. "+N"·"오늘" 뱃지도 Badge 재사용.
31
+ - [[components/icon-button]] — 월 이동 네비게이션(이전해·이전달·다음달·다음해 4개). ghost + sm.
32
+ - [[components/button]] — "오늘" 복귀 버튼은 outline·sm, "주말" 토글은 ghost/secondary·sm.
33
+ - [[axes/shape]] — 셀 radius는 없음(격자형이라 radius 신호를 셀이 받지 않음). pill·badge만 Shape 축에 반응.
34
+ - [[axes/density]] — compact일수록 셀 padding이 줄고 pill이 더 많이 보임.
35
+ - [[tokens/color]] — `--primary`(선택), `--hover`(셀 hover), `--border`(hairline), `--bg-muted` + `*-subtle`(pill 배경), `--info`/`--destructive`(요일 신호).
36
+
37
+ ### Props 시그니처
38
+
39
+ ```tsx
40
+ <Calendar
41
+ month={Date} // required — 현재 보여줄 달 (controlled)
42
+ today={Date} // default: new Date()
43
+ selectedDate={Date | null} // 셀 하이라이트
44
+ events={CalendarEvent[]} // id/date/title/code?/tone?/resolved?
45
+ weekStartsOn={0 | 1} // default: 1 (월)
46
+ showWeekend={boolean} // default: true
47
+ leadingSlot={ReactNode} // 상단바 왼쪽 — Google 연결 뱃지 등
48
+ showTodayButton={boolean} // default: true
49
+ onMonthChange={(next: Date) => void}
50
+ onSelectedDateChange={(next: Date) => void}
51
+ />
52
+
53
+ type CalendarEvent = {
54
+ id: string;
55
+ date: string; // yyyy-MM-dd
56
+ title: string;
57
+ code?: string; // 보조 라벨 (사건번호·코드 등)
58
+ tone?: "default" | "accent" | "success" | "warning" | "danger" | "info" | "neutral";
59
+ resolved?: boolean; // 체크 아이콘
60
+ };
61
+ ```
62
+
63
+ ### Tone 매트릭스
64
+
65
+ | tone | 배경 | 글자 | 쓰임새 |
66
+ |---|---|---|---|
67
+ | `default` | `--bg-muted` | `--fg` | 중립 — 기본값. 미분류 일정 |
68
+ | `neutral` | `--secondary-bg` | `--secondary-fg` | 약한 강조 — 사내 공지·루틴 |
69
+ | `accent` | `--primary-subtle` | `--secondary-fg` | 브랜드 액션 — "내 일정"·주요 미팅 |
70
+ | `success` | `--success-subtle` | `--success` | 확정·완료 |
71
+ | `warning` | `--warning-subtle` | `--warning` | 임박·주의 |
72
+ | `danger` | `--destructive-subtle` | `--destructive` | 마감·실패 |
73
+ | `info` | `--info-subtle` | `--info` | 참고·외부 캘린더 |
74
+
75
+ **금지 조합**
76
+ - `tone="danger"`를 "눈에 띄게" 쓰려고 전부 danger로 깔기 — 색이 의미 신호를 잃는다. 셀당 1개가 danger라면 다른 셀은 default/accent여야 한다.
77
+ - Calendar를 가로 너비 300px 미만에 끼워넣기 — 셀이 사각형을 못 지키고 pill 1개도 안 들어간다. 이 자리는 DatePicker.
78
+ - `showWeekend=false` + 월말 날짜가 토/일에 몰린 달 — 선택 가능한 셀이 사라져 "클릭이 안 되는 날짜"가 생긴다. 주말 숨김은 평일만 다루는 업무 캘린더 전용.
79
+
80
+ ### 조합 예시
81
+
82
+ ```tsx
83
+ // 기본 — 페이지 메인에 깔리는 월간 뷰
84
+ const [month, setMonth] = useState(new Date());
85
+ const [selected, setSelected] = useState<Date | null>(new Date());
86
+
87
+ <Calendar
88
+ month={month}
89
+ selectedDate={selected}
90
+ events={events}
91
+ onMonthChange={setMonth}
92
+ onSelectedDateChange={setSelected}
93
+ />
94
+
95
+ // 2단 레이아웃 — 선택 날짜 상세 패널과 짝. 리스트는 ListItem으로 조립.
96
+ <div className="flex min-h-0 flex-1">
97
+ <Calendar
98
+ month={month}
99
+ selectedDate={selected}
100
+ events={events}
101
+ onMonthChange={setMonth}
102
+ onSelectedDateChange={setSelected}
103
+ />
104
+ <aside className="w-80 shrink-0 border-l border-[var(--border)]">
105
+ <SelectedDayPanel date={selected} items={eventsOf(selected)} />
106
+ </aside>
107
+ </div>
108
+
109
+ // 외부 상태 신호가 있는 상단바 — leadingSlot으로 Badge 꽂음
110
+ <Calendar
111
+ leadingSlot={<Badge variant="soft" status="info">Google 연결됨</Badge>}
112
+ month={month}
113
+ onMonthChange={setMonth}
114
+ />
115
+
116
+ // 업무 전용 — 주말 숨김, 월요일 시작
117
+ <Calendar
118
+ month={month}
119
+ showWeekend={false}
120
+ weekStartsOn={1}
121
+ onMonthChange={setMonth}
122
+ />
123
+ ```
@@ -0,0 +1,35 @@
1
+ # Code Block
2
+
3
+ ### 원칙
4
+ [[components/ai-message]] 안에 들어가는 코드 블록. 상단 언어 라벨 + copy 버튼, 본문은 mono font pre+code. 가로 overflow 시 scroll. **Syntax highlighting은 CodeBlock의 책임이 아님** — 소비처가 highlighted ReactNode를 `code`로 넘기거나(span 조합) plain string.
5
+
6
+ ### Do
7
+ - 언어 아는 경우 `language="html"` 처럼 라벨 노출 — 작게 fg-muted로.
8
+ - `onCopy`를 안 주면 clipboard API로 자동 복사. 성공 시 Check 아이콘으로 1.5s 피드백.
9
+ - 가로가 넘치면 pre 안에서 가로 스크롤 — body wrapping 대신.
10
+
11
+ ### Don't
12
+ - CodeBlock 안에 "실행" 버튼이나 "preview 전환" 넣지 않는다. 그건 소비처 책임.
13
+ - syntax highlighting을 내장하지 않는다 — shiki/highlight.js는 소비처에서.
14
+
15
+ ### Fallback
16
+ language 애매하면 생략. onCopy 애매하면 clipboard API 기본.
17
+
18
+ ### 관련
19
+ - [[components/ai-message]] — 상위 소비처
20
+ - [[primitives/scroll]] — pre 가로 스크롤 룰
21
+
22
+ ### Props 시그니처
23
+ ```tsx
24
+ <CodeBlock
25
+ code={string | ReactNode} // 코드 본문 (string이면 clipboard 복사 대상)
26
+ language={string} // "html" / "tsx" 등. 없으면 숨김
27
+ onCopyCode={(code) => void} // 미지정 시 navigator.clipboard (React의 onCopy와 이름 충돌 회피)
28
+ />
29
+ ```
30
+
31
+ ### 레이아웃
32
+ - root: `primary-subtle` bg, `radius-md`, overflow hidden
33
+ - header: `space-2 space-3` padding, 언어 라벨(mono text-xs fg-muted) + copy 버튼
34
+ - copy 버튼: `space-6` 정사각, `bg-muted` 기본
35
+ - 본문 `<pre>`: `space-2 space-4 space-4` padding, mono text-sm, line-height 1.6, overflow-x auto
@@ -0,0 +1,145 @@
1
+ # DropdownMenu
2
+
3
+ ### 원칙
4
+ 버튼 클릭(또는 Enter/Space)으로 열리는 **트리거 중심 액션·옵션 메뉴**. 모델 전환, 뷰 전환, 유저 메뉴, 리스트 행의 ⋯ 메뉴처럼 "이 버튼을 눌러서 선택지 중 하나를 고른다"에 쓴다. [[components/context-menu]]와 트리거 방식만 다르고 Content 이하(Item·CheckboxItem·RadioItem·Sub) 시각·슬롯·상태는 전부 동일 — 두 컴포넌트는 같은 `.surface`와 [[components/menu-item]] 어댑터를 공유한다. Radix `@radix-ui/react-dropdown-menu` 위에 얹은 compound 어댑터.
5
+
6
+ ### Do
7
+ - **트리거는 명확한 버튼**: 모델 셀렉터·"더 보기 ⋯"·유저 아바타처럼 눌러서 메뉴가 열림을 암시하는 요소. caret 아이콘·⋯ 아이콘을 붙이면 발견성↑.
8
+ - **`asChild`로 트리거 위임**: `<DropdownMenu.Trigger asChild>`로 Button·IconButton·IdentityButton 어느 것이든 트리거로. 자체 button 중첩되지 않도록 반드시 `asChild`.
9
+ - **값 선택 vs 액션 발사**: 값 선택이 폼의 일부고 label·description·invalid가 붙어야 하면 [[components/select]]를 쓸 것. 그 외(명령 발화·토글·배타 옵션 선택)는 DropdownMenu.
10
+ - **논리 그룹화**: 관련 액션끼리 묶고 `Separator`로 분리. 파괴적 액션은 맨 아래, `variant="destructive"`.
11
+ - **키보드는 Radix에 맡김**: 화살표·Home/End·타이핑 서치·Esc·Tab 이미 처리됨. 자체 키 핸들러 금지.
12
+ - **선택 후 자동 닫힘 활용**: `onSelect`가 발화하면 Radix가 자동 닫음. 다중 토글처럼 열림 유지가 필요하면 `e.preventDefault()`.
13
+ - **상태 아이템**: 토글은 `CheckboxItem`, 배타 선택은 `RadioGroup` + `RadioItem`. 인디케이터는 자동.
14
+ - **중첩은 `Sub` 트리오**: `Sub` / `SubTrigger` / `SubContent`.
15
+ - **단축키는 실제 심볼**: `⌘K` · `⇧⌘P` · `⌫`. uppercase 금지 — [[feedback_no_uppercase]].
16
+ - **아이콘은 액션 의미와 정확히 일치할 때만 붙인다.** 어긋나면 라벨만 둬도 좋다. 유효한 아이콘 이름은 [[web/components/Icon/Icon.tsx]]에 있는 세트에서만 — 없는 이름 지어내지 말 것(렌더 실패).
17
+ - **라벨은 짧게 — 한글 8–12자 · 영문 12–20자 이내.** 폭은 표준 224px이고 최대 한 스텝(+48px)만 넓어진다.
18
+
19
+ ### Don't
20
+ - 폼 필드 자리에 DropdownMenu를 쓰지 말 것. label·description·invalid·필드 폭 통일이 필요하면 [[components/select]].
21
+ - 핵심 CTA를 메뉴 안에 숨기지 말 것. 자주 쓰는 액션은 메뉴 밖 버튼으로.
22
+ - 한 메뉴에 12개 이상 아이템 금지 — 넘치면 `Sub`로 접기.
23
+ - **긴 라벨 두 줄 금지.** `white-space: nowrap` + 표준 폭이 시각 계약. 짧게 못 쓰면 라벨 자체를 재설계.
24
+ - Trigger에 button을 중첩하지 말 것 — `<button><Button /></button>` 되지 않도록 `asChild` 사용.
25
+ - 시각 위해 Content에 hard-coded 색·radius·shadow 얹지 말 것. 토큰 경유 ([[PRINCIPLES]] §6).
26
+ - Radix 포지셔닝을 뒤흔들지 말 것 (Content `position: fixed`/`transform` 강제 금지). `collisionPadding`·`side`·`align`·`sideOffset`만 조절.
27
+
28
+ ### Fallback
29
+ size 애매하면 `md`. Item variant 애매하면 `default`. 선택 상태가 폼 값이면 [[components/select]]로 넘길 것.
30
+
31
+ ### 관련
32
+ - [[components/menu-item]] — 시각·슬롯·상태 규칙의 원본. DropdownMenu는 이 위에 얇게 얹은 어댑터.
33
+ - [[components/context-menu]] — 트리거만 다름(우클릭 vs 클릭). Content 이하는 동일 패턴 공유.
34
+ - [[components/select]] — 폼 필드로서의 "선택" UI. form-field trigger가 필요하면 이쪽.
35
+ - [[components/icon-button]] — 케밥(⋮)·미트볼(⋯) 트리거.
36
+ - [[components/alert-dialog]] — 파괴적 액션 확인은 여기.
37
+ - [[axes/shape]] · [[axes/density]] — Content radius·아이템 min-height가 반응.
38
+ - [[tokens/color]] — `--bg` · `--border` · `--shadow-lg` · `--hover`가 시각.
39
+
40
+ ### Props 시그니처
41
+ ```tsx
42
+ <DropdownMenu> // Radix Root — open/onOpenChange/modal 전부 통과
43
+ <DropdownMenu.Trigger asChild> // 클릭·Enter·Space 트리거
44
+ <Button>...</Button>
45
+ </DropdownMenu.Trigger>
46
+ <DropdownMenu.Content
47
+ size="sm | md" // default: md
48
+ side="top | right | bottom | left" // Radix default: bottom
49
+ align="start | center | end" // default: center
50
+ sideOffset={6} // default: 6
51
+ collisionPadding={8} // default: 8
52
+ >
53
+ <DropdownMenu.Label>...</DropdownMenu.Label>
54
+ <DropdownMenu.Item
55
+ size="sm | md"
56
+ variant="default | destructive"
57
+ leadingActions={MenuItemAction[]}
58
+ trailingActions={MenuItemAction[]}
59
+ shortcut="⌘C"
60
+ disabled={boolean}
61
+ onSelect={(e) => void}
62
+ >
63
+ ...
64
+ </DropdownMenu.Item>
65
+ <DropdownMenu.CheckboxItem
66
+ checked={boolean | "indeterminate"}
67
+ onCheckedChange={(v) => void}
68
+ />
69
+ <DropdownMenu.RadioGroup value onValueChange>
70
+ <DropdownMenu.RadioItem value="..." />
71
+ </DropdownMenu.RadioGroup>
72
+ <DropdownMenu.Sub>
73
+ <DropdownMenu.SubTrigger>...</DropdownMenu.SubTrigger>
74
+ <DropdownMenu.SubContent>...</DropdownMenu.SubContent>
75
+ </DropdownMenu.Sub>
76
+ <DropdownMenu.Separator />
77
+ </DropdownMenu.Content>
78
+ </DropdownMenu>
79
+ ```
80
+
81
+ ### 상태 매트릭스
82
+ | 상태 | 시각 |
83
+ |---|---|
84
+ | 열림 · 닫힘 | Content in/out은 전역 `.m-float` 유틸. 자체 애니메이션 재정의 금지. |
85
+ | 아이템 highlight | `--hover` 배경, `fg-strong` — MenuItem 규칙 그대로 |
86
+ | checked (Checkbox) | leading 맨 앞 checkmark 자동 |
87
+ | checked (Radio) | leading 맨 앞 dot 인디케이터 |
88
+ | disabled | `fg-disabled`, 커서 not-allowed, 키보드 순환 제외 |
89
+ | destructive · hover | 배경 destructive 8%, 텍스트·아이콘 destructive |
90
+
91
+ ### Size 가이드
92
+ | size | 언제 |
93
+ |---|---|
94
+ | `sm` | 밀도 높은 관리 UI · 툴바 메뉴 |
95
+ | `md` | **기본.** 일반 드롭다운 |
96
+
97
+ ### 조합 예시
98
+ ```tsx
99
+ // ChatInput 모델 셀렉터
100
+ <DropdownMenu>
101
+ <DropdownMenu.Trigger asChild>
102
+ <ModelSelector label="Llama" />
103
+ </DropdownMenu.Trigger>
104
+ <DropdownMenu.Content align="end" size="sm">
105
+ <DropdownMenu.RadioGroup value={model} onValueChange={setModel}>
106
+ <DropdownMenu.RadioItem value="llama">Llama</DropdownMenu.RadioItem>
107
+ <DropdownMenu.RadioItem value="claude">Claude</DropdownMenu.RadioItem>
108
+ <DropdownMenu.RadioItem value="gpt">GPT</DropdownMenu.RadioItem>
109
+ </DropdownMenu.RadioGroup>
110
+ </DropdownMenu.Content>
111
+ </DropdownMenu>
112
+
113
+ // 리스트 행의 ⋯ 메뉴
114
+ <DropdownMenu>
115
+ <DropdownMenu.Trigger asChild>
116
+ <IconButton icon="dots-three" aria-label="더 보기" />
117
+ </DropdownMenu.Trigger>
118
+ <DropdownMenu.Content align="end">
119
+ <DropdownMenu.Item shortcut="⌘E" onSelect={rename}>이름 변경</DropdownMenu.Item>
120
+ <DropdownMenu.Item shortcut="⌘D" onSelect={duplicate}>복제</DropdownMenu.Item>
121
+ <DropdownMenu.Separator />
122
+ <DropdownMenu.Item variant="destructive" shortcut="⌫" onSelect={remove}>삭제</DropdownMenu.Item>
123
+ </DropdownMenu.Content>
124
+ </DropdownMenu>
125
+
126
+ // 유저 아바타 메뉴 — Label + 중첩
127
+ <DropdownMenu>
128
+ <DropdownMenu.Trigger asChild>
129
+ <IdentityButton avatar={...} />
130
+ </DropdownMenu.Trigger>
131
+ <DropdownMenu.Content align="end">
132
+ <DropdownMenu.Label>Signed in as hehe@...</DropdownMenu.Label>
133
+ <DropdownMenu.Item onSelect={profile}>프로필</DropdownMenu.Item>
134
+ <DropdownMenu.Sub>
135
+ <DropdownMenu.SubTrigger>테마</DropdownMenu.SubTrigger>
136
+ <DropdownMenu.SubContent>
137
+ <DropdownMenu.Item onSelect={() => setTheme("light")}>라이트</DropdownMenu.Item>
138
+ <DropdownMenu.Item onSelect={() => setTheme("dark")}>다크</DropdownMenu.Item>
139
+ </DropdownMenu.SubContent>
140
+ </DropdownMenu.Sub>
141
+ <DropdownMenu.Separator />
142
+ <DropdownMenu.Item variant="destructive" onSelect={signOut}>로그아웃</DropdownMenu.Item>
143
+ </DropdownMenu.Content>
144
+ </DropdownMenu>
145
+ ```
@@ -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
+ ```
@@ -19,7 +19,7 @@ root는 `<div role="menuitem">`이다 — `leadingActions`/`trailingActions`에
19
19
  ### Don't
20
20
  - MenuItem 안에 두 개 이상의 &ldquo;주된 액션&rdquo;을 두지 않는다 — 아이템 전체가 하나의 selection. 부가 액션(즐겨찾기 토글·⋯메뉴)만 slot에 얹는다.
21
21
  - 라벨을 두 줄 이상 넣지 않는다. 설명이 필요하면 [[components/command-palette]] description 슬롯이나 [[components/tooltip]].
22
- - 페이지 이동에 root click 시맨틱 금지. 라우팅 아이템이면 `asChild`로 `<Link>`/`<a>`에 위임.
22
+ - 페이지 이동에 root click 시맨틱 금지. 라우팅 아이템이면 `asChild`로 `<Link>`/`<a>`에 위임. **단, `asChild`일 땐 라벨 `.label` 래퍼가 적용되지 않는다** — 자식(`<Link>`) 자체가 라벨 역할이라 truncation·color·weight가 자동 상속되지 않음. 긴 라벨이면 `<Link>` 쪽에 truncate 유틸을 걸거나, 짧게 유지하는 설계로.
23
23
  - height를 CSS로 덮어쓰지 않는다. `size` prop 경유 (Density 축이 스왑).
24
24
  - hover-only 액션을 &ldquo;시각적으로 숨김&rdquo;으로만 처리하지 않는다 — `opacity: 0` + `pointer-events: none`이 짝. 안 그러면 안 보이는 버튼이 클릭됨.
25
25
 
@@ -81,6 +81,7 @@ type MenuItemAction =
81
81
  checked={boolean} // leadingActions 맨 앞에 checkmark 자동 삽입
82
82
  active={boolean} // 현재 페이지/선택 아이템 강조
83
83
  disabled={boolean}
84
+ collapsed={boolean} // icon-only. leading 하나만 남기고 label/trailing/shortcut 전부 숨김. (Sidebar가 자동 주입)
84
85
  asChild={boolean} // Radix Slot — <Link>/<a>/DropdownMenu.Item 등에 위임
85
86
  onClick={(e) => void}
86
87
  />
@@ -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,26 @@
1
+ # Message Sources
2
+
3
+ ### 원칙
4
+ [[components/ai-message]] 하단에 붙는 소스 참조 chip 리스트. "Sources" 라벨 + 가로 wrap chip. 각 chip은 외부 링크 (favicon·링크 아이콘 좌측).
5
+
6
+ ### Do
7
+ - Search 응답 뒤에 자동으로 노출 — AI가 참고한 출처 투명하게.
8
+ - chip label은 호스트명 중심으로 짧게 (`"nytimes.com"`, `"wikipedia.org — 제목"`).
9
+ - 긴 라벨은 중간 ellipsis가 아니라 끝 ellipsis (호스트명 보존).
10
+
11
+ ### Don't
12
+ - Sources 안에 추가 메타(날짜·발행자) 넣지 않는다 — 깔끔한 chip만.
13
+ - Sources를 메시지 본문 중간에 두지 않는다 — 반드시 하단.
14
+
15
+ ### Props 시그니처
16
+ ```tsx
17
+ <MessageSources
18
+ sources={[{ id, label, href?, icon? }]}
19
+ label="Sources" // default
20
+ />
21
+ ```
22
+
23
+ ### 레이아웃
24
+ - label: text-xs, fg-muted
25
+ - chip: `space-1 space-2` padding, `bg-subtle`, `radius-full`
26
+ - gap: `space-1` (chip↔chip)
@@ -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`