@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.
Files changed (77) hide show
  1. package/assets/components/AIMessage/AIMessage.module.css +129 -0
  2. package/assets/components/AIMessage/AIMessage.tsx +104 -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 +82 -45
  16. package/assets/components/ChatInput/ChatInput.tsx +46 -34
  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 +29 -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 +58 -0
  42. package/assets/components/MessageSources/MessageSources.tsx +64 -0
  43. package/assets/components/MessageSources/index.ts +2 -0
  44. package/assets/components/MessageTable/MessageTable.module.css +59 -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 +82 -0
  51. package/assets/components/Spinner/Spinner.module.css +170 -0
  52. package/assets/components/Spinner/Spinner.tsx +117 -0
  53. package/assets/components/Spinner/index.ts +10 -0
  54. package/assets/components/Spinner/lottieColor.ts +106 -0
  55. package/assets/components/Spinner/lottieManifest.ts +100 -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 +45 -0
  63. package/assets/prompts/components/bubble.md +56 -0
  64. package/assets/prompts/components/calendar.md +116 -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/event-chip.md +74 -0
  68. package/assets/prompts/components/identity-button.md +168 -0
  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 +33 -0
  72. package/assets/prompts/components/message-table.md +27 -0
  73. package/assets/prompts/components/sidebar.md +48 -3
  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/package.json +1 -1
@@ -572,6 +572,7 @@ export const componentSlugs: Record<string, string> = {
572
572
  Sidebar: "nav/sidebar",
573
573
  "Sidebar Split": "nav/sidebar-split",
574
574
  Topbar: "nav/topbar",
575
+ "Identity Button": "nav/identity-button",
575
576
  Pagination: "nav/pagination",
576
577
  "App Frame": "layout/app-frame",
577
578
  Stepper: "nav/stepper",
@@ -584,22 +585,33 @@ export const componentSlugs: Record<string, string> = {
584
585
  "Table Row": "data/table-row",
585
586
  "Table Row Select": "data/table-row-select",
586
587
  "Table Row Edit": "data/table-row-edit",
588
+ Calendar: "data/calendar",
589
+ "Event Chip": "data/event-chip",
587
590
  "List Item": "data/list-item",
588
591
  "Metric Card": "data/metric-card",
589
592
  "Gallery Item": "data/gallery-item",
590
593
  "Empty State": "data/empty-state",
591
594
  "Progress Bar": "feedback/progress-bar",
595
+ Spinner: "feedback/spinner",
592
596
  "File Icon": "chat/file-icon",
593
597
  "Attachment Thumb": "chat/attachment-thumb",
594
598
  "Attachment Chip": "chat/attachment-chip",
595
599
  "Quotation Bar": "chat/quotation-bar",
596
600
  "Chat Input": "chat/chat-input",
601
+ "Bubble": "chat/bubble",
602
+ "Message Actions": "chat/message-actions",
603
+ "User Message": "chat/user-message",
604
+ "AI Message": "chat/ai-message",
605
+ "Code Block": "chat/code-block",
606
+ "Message Quote": "chat/message-quote",
607
+ "Message Sources": "chat/message-sources",
608
+ "Message Table": "chat/message-table",
597
609
  };
598
610
 
599
611
  /** 작업중 — 사이드바에서 접근은 가능하지만 "작업중" 뱃지로 표시해 사용 자제 유도. */
600
612
  export const componentsInProgress: Set<string> = new Set([
601
613
  "Drawer",
602
- "Chat Input",
614
+ "Calendar",
603
615
  ]);
604
616
 
605
617
  export const components: { id: string; title: string; items: string[] }[] = [
@@ -626,7 +638,7 @@ export const components: { id: string; title: string; items: string[] }[] = [
626
638
  title: "Data Display",
627
639
  items: [
628
640
  "Table Header", "Table Row", "Table Row Select", "Table Row Edit",
629
- "List Item", "Metric Card", "Gallery Item",
641
+ "List Item", "Metric Card", "Gallery Item", "Calendar", "Event Chip",
630
642
  "Empty State", "Description List", "Timeline",
631
643
  ],
632
644
  },
@@ -634,7 +646,7 @@ export const components: { id: string; title: string; items: string[] }[] = [
634
646
  id: "nav",
635
647
  title: "Navigation",
636
648
  items: [
637
- "App Frame", "Tabs", "Breadcrumb", "Sidebar", "Sidebar Split", "Topbar", "Pagination",
649
+ "App Frame", "Tabs", "Breadcrumb", "Sidebar", "Sidebar Split", "Topbar", "Identity Button", "Pagination",
638
650
  "Stepper",
639
651
  ],
640
652
  },
@@ -659,7 +671,9 @@ export const components: { id: string; title: string; items: string[] }[] = [
659
671
  title: "AI Chat",
660
672
  items: [
661
673
  "File Icon", "Attachment Thumb", "Attachment Chip", "Quotation Bar", "Chat Input",
662
- "Message Bubble", "Chat Sidebar",
674
+ "Bubble", "Message Actions", "User Message", "AI Message",
675
+ "Code Block", "Message Quote", "Message Sources", "Message Table",
676
+ "Chat Sidebar",
663
677
  ],
664
678
  },
665
679
  ];
@@ -80,6 +80,17 @@
80
80
  --icon-size-md: calc(20px * var(--space-scale));
81
81
  --icon-size-lg: calc(24px * var(--space-scale));
82
82
 
83
+ /* Spinner · 로더 표준 크기 — IconButton 체계와 1:1 매칭 (xs=16, sm=20, md=24, lg=32).
84
+ 로더는 캔버스를 다 채우지 않는 특성상 아이콘보다 한 단계 큰 bounds가 체감에 맞음. */
85
+ --loader-size-xs: calc(16px * var(--space-scale));
86
+ --loader-size-sm: calc(20px * var(--space-scale));
87
+ --loader-size-md: calc(24px * var(--space-scale));
88
+ --loader-size-lg: calc(32px * var(--space-scale));
89
+
90
+ /* Loader 성격 — Motion 축이 스왑 ([data-motion] 블록 참고).
91
+ 기본값은 standard와 같은 ring. */
92
+ --loader-style: ring;
93
+
83
94
  /* 폼 컨트롤 권장 폭 — Input · Select 등 텍스트 필드 계열이 공유.
84
95
  기본 동작은 부모 폭 채움(fill)이지만, 카드/툴바 안에서 규격을 잡을 때 참조.
85
96
  md = 이메일 · 이름 · 짧은 문장 표준. lg = URL · 설명 · 긴 문장.
@@ -221,6 +232,11 @@
221
232
  --tone-hue: 260;
222
233
  --tone-chroma: 0.005;
223
234
 
235
+ /* 테마 중립 fixed 색 — 어두운 오버레이 위 로더·배지처럼
236
+ 라이트/다크 공통으로 흰색이어야 하는 자리 전용 */
237
+ --fixed-white: #fff;
238
+ --fixed-black: #000;
239
+
224
240
  /* semantic — light mode */
225
241
  --bg: oklch(1 var(--tone-chroma) var(--tone-hue));
226
242
  --bg-subtle: oklch(0.978 var(--tone-chroma) var(--tone-hue));
@@ -520,20 +536,24 @@ html[data-focus-ring="off"] {
520
536
  --motion-scale: 1.2; /* ~192ms · 오버슛 여운만큼 */
521
537
  --motion-distance-scale: 1.1;
522
538
  --ease-emphasized: cubic-bezier(0.34, 1.4, 0.64, 1); /* 절제된 오버슛 spring */
539
+ --loader-style: dots-bounce; /* 통통한 로더 — bouncy 성격 */
523
540
  }
524
541
  [data-motion="standard"] {
525
542
  --motion-scale: 1; /* 160ms */
526
543
  --motion-distance-scale: 1;
527
544
  --ease-emphasized: cubic-bezier(0.32, 0.72, 0, 1); /* Linear/Vercel ease-out */
545
+ --loader-style: ring; /* 표준 회전 ring */
528
546
  }
529
547
  [data-motion="calm"] {
530
548
  --motion-scale: 0.9; /* ~144ms · flat 곡선이라 짧아도 안전 */
531
549
  --motion-distance-scale: 0.9;
532
550
  --ease-emphasized: cubic-bezier(0.4, 0, 0.2, 1); /* 표준 ease-out · 심심할수록 차분 */
551
+ --loader-style: dots-fade; /* 위치 이동 없는 미니멀 dot fade */
533
552
  }
534
553
  [data-motion="none"] {
535
554
  --motion-scale: 0;
536
555
  --motion-distance-scale: 0;
556
+ --loader-style: ring; /* 로더는 접근성 예외: 로딩 신호는 유지 */
537
557
  }
538
558
  [data-motion="none"] *,
539
559
  [data-motion="none"] *::before,
@@ -0,0 +1,45 @@
1
+ # AI Message
2
+
3
+ ### 원칙
4
+ AI가 보낸 응답 메시지. **왼쪽 정렬**, bubble 없이 본문 그대로 노출 — [[components/user-message]]의 bubble과 대비. 상단에 선택적 라벨, 본문(markdown 또는 콘텐츠 블록), 하단에 [[components/message-actions]].
5
+
6
+ ### Do
7
+ - `label`로 메타 제목(예: &ldquo;LongText&rdquo;, &ldquo;Code Block&rdquo;)을 짧게 노출. 생략 가능.
8
+ - `children`에 markdown HTML을 직접 넣거나, 콘텐츠 블록 컴포넌트(CodeBlock·Quote·Sources·Table)를 조합.
9
+ - 기본 액션 `[copy, regenerate]`, `actionsVisibility`로 상시/호버 전환.
10
+ - 생성 중엔 `isGenerating`로 액션 숨김. 로딩 UI는 [[components/message-state]] 조합.
11
+
12
+ ### Don't
13
+ - bubble로 감싸지 않는다 — AI는 "본문"이 메시지 자체. bubble은 UserMessage 전용.
14
+ - 아바타·모델명 넣지 않는다 — 상위 shell(ChatThread)에서 책임.
15
+ - 오른쪽 정렬하지 않는다 — 왼쪽 고정.
16
+ - edit 액션 넣지 않는다 — AI 응답은 수정 대상 아님 (regenerate가 대신).
17
+
18
+ ### Fallback
19
+ visibility 애매하면 `hover`, actions 애매하면 기본 `[copy, regenerate]`.
20
+
21
+ ### 관련
22
+ - [[components/user-message]] — 짝 컴포넌트
23
+ - [[components/message-actions]] — 하단 액션 바
24
+ - [[components/message-state]] — 로딩/상태 라인 (F5)
25
+ - 콘텐츠 블록: `CodeBlock` / `Quote` / `Sources` / `Table` (F4)
26
+
27
+ ### Props 시그니처
28
+ ```tsx
29
+ <AIMessage
30
+ label={ReactNode} // 상단 메타 라벨
31
+ actionsVisibility="show | hover" // default: hover
32
+ actions={[{ kind: "copy" }, { kind: "regenerate" }]} // 비우려면 []
33
+ sources={[{ id, label, href?, icon? }]} // 액션바 옆 inline chip
34
+ onSourcesClick={() => void} // chip 클릭 (popover 열기 등)
35
+ isGenerating={boolean} // true면 액션/sources 숨김
36
+ >
37
+ 본문 — markdown 또는 블록 조합
38
+ </AIMessage>
39
+ ```
40
+
41
+ ### 레이아웃
42
+ - root: `flex-direction: column; align-items: flex-start; gap: space-2`
43
+ - label: `text-xs`, `fg-muted`
44
+ - body: `text-base`, `line-height: 2` (평소의 ~2배, 가독성), 색 `--fg` (다크 그레이, strong보다 한 톤 연함), markdown 기본 톤
45
+ - actions: `align=start`
@@ -0,0 +1,56 @@
1
+ # Bubble
2
+
3
+ ### 원칙
4
+ 메시지 말풍선 **primitive**. Shape 4종 × Fill 4종 조합. [[components/user-message]]·[[components/ai-message]]의 Quote 블록·AttachmentChip 등 "담는 그릇" 역할 전부에 재사용.
5
+
6
+ ### Do
7
+ - UserMessage는 `shape=circular-drop fill=translucent tail=end` 조합이 기본.
8
+ - AI Quote 블록은 `shape=rounded fill=outline` 또는 `fill=none`.
9
+ - Shape 축 따라 radius 변화 — rounded는 `radius-xl` 토큰 사용.
10
+ - circular-drop의 `tail` prop으로 꼬리 방향 지정 — user는 end, assistant/quote는 start.
11
+
12
+ ### Don't
13
+ - Bubble을 "일반 Card" 용도로 쓰지 않는다 — 그건 [[components/metric-card]] / 커스텀.
14
+ - fill=`solid`는 user "강조" 메시지(선택된/하이라이트)에만 — 기본은 translucent.
15
+ - 꼬리를 양쪽 다 각지게 하지 않는다 — tail은 한 방향만.
16
+
17
+ ### Fallback
18
+ shape 애매하면 `rounded`, fill 애매하면 `translucent`, size 애매하면 `md`.
19
+
20
+ ### 관련
21
+ - [[components/user-message]] / [[components/ai-message]] — 상위 소비처
22
+ - [[components/badge]] — 작은 라벨은 Badge
23
+ - [[components/callout]] — 안내 상자는 Callout
24
+
25
+ ### Props 시그니처
26
+ ```tsx
27
+ <Bubble
28
+ shape="rounded | circular-drop | circular | square" // default: rounded
29
+ fill="none | solid | translucent | outline" // default: translucent
30
+ size="md | lg" // default: md
31
+ tail="start | end" // circular-drop에서만
32
+ >
33
+ Text
34
+ </Bubble>
35
+ ```
36
+
37
+ ### 매트릭스
38
+ | shape | radius |
39
+ |---|---|
40
+ | rounded | `--radius-md` (Shape 축 반응 명확: soft 14 / standard 10 / crisp 6) |
41
+ | circular-drop | `--radius-md` 세 코너 + `0` tail 코너 하나 (각짐) |
42
+ | circular | `--radius-full` (알약) |
43
+ | square | `--radius-sm` |
44
+
45
+ | fill | bg | fg | border |
46
+ |---|---|---|---|
47
+ | none | transparent | fg-strong | — |
48
+ | solid | primary | bg | — |
49
+ | translucent | primary-subtle (알파 8%) | fg-strong | — |
50
+ | outline | bg | fg-strong | hairline border |
51
+
52
+ ### Size
53
+ - `md` (기본): padding `space-2 space-3`, `text-base` — 일반 메시지 bubble.
54
+ - `lg`: padding `space-3 space-4`, `text-base` — 히어로/강조 bubble. 패딩만 넉넉, 폰트는 md와 동일.
55
+
56
+ 소형 라벨이 필요하면 [[components/badge]] 사용.
@@ -0,0 +1,116 @@
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/event-chip]] — 셀 안에 쌓이는 일정 블록. Calendar가 자동으로 그려주지만 톤·props 체계는 EventChip 문서를 1차 참조.
30
+ - [[components/date-picker]] — "날짜 입력"이 목적이면 그쪽. Popover + Input 트리거. Calendar와 utils(buildMonthGrid 등) 공유.
31
+ - [[components/badge]] — "+N"·"오늘" 메타 뱃지는 Badge soft. EventChip 톤 체계도 Badge와 동일 토큰.
32
+ - [[components/icon-button]] — 월 이동 네비게이션(이전해·이전달·다음달·다음해 4개). ghost + sm.
33
+ - [[components/button]] — "오늘" 복귀 버튼은 outline·sm.
34
+ - [[axes/shape]] — 셀 radius는 없음(격자형이라 radius 신호를 셀이 받지 않음). EventChip·Badge만 Shape 축에 반응.
35
+ - [[axes/density]] — compact일수록 셀 padding이 줄고 EventChip이 더 많이 보임.
36
+ - [[tokens/color]] — `--primary`(선택), `--hover`(셀 hover), `--border`(hairline), `--info`/`--destructive`(요일 신호).
37
+
38
+ ### Props 시그니처
39
+
40
+ ```tsx
41
+ <Calendar
42
+ month={Date} // required — 현재 보여줄 달 (controlled)
43
+ today={Date} // default: new Date()
44
+ selectedDate={Date | null} // 셀 하이라이트
45
+ events={CalendarEvent[]} // id/date/title/code?/tone?/resolved?
46
+ weekStartsOn={0 | 1} // default: 1 (월)
47
+ showWeekend={boolean} // default: true
48
+ leadingSlot={ReactNode} // 상단바 왼쪽 — Google 연결 뱃지 등
49
+ showTodayButton={boolean} // default: true
50
+ onMonthChange={(next: Date) => void}
51
+ onSelectedDateChange={(next: Date) => void}
52
+ />
53
+
54
+ type CalendarEvent = {
55
+ id: string;
56
+ date: string; // yyyy-MM-dd
57
+ title: string;
58
+ tone?: "default" | "accent" | "success" | "warning" | "danger" | "info" | "neutral";
59
+ };
60
+ // 셀 안 렌더링은 EventChip이 담당. 상세 라벨(코드·시각·주최자)은 Calendar 밖
61
+ // 사이드 패널·상세 뷰에서 다룬다. [[components/event-chip]] 참조.
62
+ ```
63
+
64
+ ### Tone
65
+
66
+ 이벤트 톤 7개(default/neutral/accent/success/warning/danger/info) — 상세 매트릭스는 [[components/event-chip]]에서 1차 정의한다. Calendar는 `event.tone`을 그대로 EventChip에 전달.
67
+
68
+ **금지 조합**
69
+ - `tone="danger"`를 "눈에 띄게" 쓰려고 전부 danger로 깔기 — 색이 의미 신호를 잃는다. 셀당 1개가 danger라면 다른 셀은 default/accent여야 한다.
70
+ - Calendar를 가로 너비 300px 미만에 끼워넣기 — 셀이 사각형을 못 지키고 pill 1개도 안 들어간다. 이 자리는 DatePicker.
71
+ - `showWeekend=false` + 월말 날짜가 토/일에 몰린 달 — 선택 가능한 셀이 사라져 "클릭이 안 되는 날짜"가 생긴다. 주말 숨김은 평일만 다루는 업무 캘린더 전용.
72
+
73
+ ### 조합 예시
74
+
75
+ ```tsx
76
+ // 기본 — 페이지 메인에 깔리는 월간 뷰
77
+ const [month, setMonth] = useState(new Date());
78
+ const [selected, setSelected] = useState<Date | null>(new Date());
79
+
80
+ <Calendar
81
+ month={month}
82
+ selectedDate={selected}
83
+ events={events}
84
+ onMonthChange={setMonth}
85
+ onSelectedDateChange={setSelected}
86
+ />
87
+
88
+ // 2단 레이아웃 — 선택 날짜 상세 패널과 짝. 리스트는 ListItem으로 조립.
89
+ <div className="flex min-h-0 flex-1">
90
+ <Calendar
91
+ month={month}
92
+ selectedDate={selected}
93
+ events={events}
94
+ onMonthChange={setMonth}
95
+ onSelectedDateChange={setSelected}
96
+ />
97
+ <aside className="w-80 shrink-0 border-l border-[var(--border)]">
98
+ <SelectedDayPanel date={selected} items={eventsOf(selected)} />
99
+ </aside>
100
+ </div>
101
+
102
+ // 외부 상태 신호가 있는 상단바 — leadingSlot으로 Badge 꽂음
103
+ <Calendar
104
+ leadingSlot={<Badge variant="soft" status="info">Google 연결됨</Badge>}
105
+ month={month}
106
+ onMonthChange={setMonth}
107
+ />
108
+
109
+ // 업무 전용 — 주말 숨김, 월요일 시작
110
+ <Calendar
111
+ month={month}
112
+ showWeekend={false}
113
+ weekStartsOn={1}
114
+ onMonthChange={setMonth}
115
+ />
116
+ ```
@@ -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,74 @@
1
+ # Event Chip
2
+
3
+ ### 원칙
4
+
5
+ 캘린더 셀 안에서 "하루에 쌓이는 일정 하나"를 표현하는 작은 정보 블록. [[components/chip]]은 필터 선택 토큰 전용, [[components/badge]]는 카운트·상태 라벨 전용 — 캘린더 셀 안 일정은 이 자리. 가로 폭은 부모(셀)에 꽉 차고, 세로로 쌓이며, 넘치는 title은 ellipsis로 접힌다.
6
+
7
+ ### Do
8
+ - props는 `title` + `tone` + `faded` 세 개만. title 하나로 끝난다 — 부가 라벨(코드·시각·주최자)은 셀 안이 아니라 상세 패널에서 다룬다.
9
+ - 톤은 7종(default/neutral/accent/success/warning/danger/info)만 사용. Badge soft와 동일 토큰 체계.
10
+ - 폭은 부모 flex column에 맞춰 자동. `min-width: 0`을 부모에 걸어주면 ellipsis가 자연히 동작한다.
11
+ - outside 월(전/다음 달)이나 과거 등 "눈에 띄지 않아야 할" 자리는 `faded`로 흐리게. 캘린더가 자동으로 넘겨준다.
12
+ - 라운드는 Shape 축에 반응하는 `--radius-xs`. 자체 radius 리터럴 금지.
13
+
14
+ ### Don't
15
+ - title 외 보조 라벨(코드·사건번호·주최자) 넣기 금지 — 셀 안은 밀도가 승부처라 한 줄만. 그게 필요하면 셀 클릭 → 상세 패널.
16
+ - 클릭 가능한 "안쪽 버튼" 금지 — EventChip 자체는 presentational. 상호작용은 부모 셀이 책임진다. 안에 또 버튼이 들어가면 캘린더 클릭 타겟이 미궁에 빠진다.
17
+ - pill radius(완전 둥근) 쓰지 않는다 — Shape 축이 radius를 쥐고 있는데 pill로 고정하면 Soft/Standard/Crisp 신호가 셀 안에서 깨진다.
18
+ - "눈에 띄게" 하려고 전부 `danger` 톤으로 깔기 금지 — 색이 의미 신호를 잃는다.
19
+ - 아이콘·아바타 삽입 금지 — 이 자리는 "하루 셀 안 한 줄"이지 ListItem이 아니다.
20
+
21
+ ### Fallback
22
+ `tone` 애매하면 `default`. `faded`는 캘린더 외 자리에서는 생략해도 됨.
23
+
24
+ ### 관련
25
+ - [[components/calendar]] — 1차 소비자. 셀 안에서 EventChip을 세로로 쌓고, 넘치면 "+N" Badge로 접는다.
26
+ - [[components/badge]] — 톤 토큰 체계 공유 (soft variant). "+N"·"오늘" 같은 메타 라벨은 Badge.
27
+ - [[components/chip]] — 혼동 금지. Chip은 필터 선택 토큰("X 제거 가능"), EventChip은 캘린더 일정 토큰.
28
+ - [[axes/shape]] — radius가 Shape 축에 반응.
29
+ - [[tokens/color]] — `--bg-muted`(default) · `--secondary-bg`(neutral) · `--primary-subtle`(accent) · `*-subtle`(상태) · `--fg`/`--fg-muted`(글자).
30
+
31
+ ### Props 시그니처
32
+
33
+ ```tsx
34
+ <EventChip
35
+ title={string} // required
36
+ tone="default | neutral | accent | success | warning | danger | info"
37
+ faded={boolean} // outside 월·과거 등 흐리게
38
+ />
39
+ ```
40
+
41
+ ### Tone 매트릭스
42
+
43
+ | tone | 배경 | 글자 | 쓰임새 |
44
+ |---|---|---|---|
45
+ | `default` | `--bg-muted` | `--fg` | 미분류 · 중립 일정 (기본값) |
46
+ | `neutral` | `--secondary-bg` | `--secondary-fg` | 사내 공지 · 루틴 |
47
+ | `accent` | `--primary-subtle` | `--secondary-fg` | 주요 미팅 · 내 일정 |
48
+ | `success` | `--success-subtle` | `--success` | 확정 · 완료 |
49
+ | `warning` | `--warning-subtle` | `--warning` | 임박 · 주의 |
50
+ | `danger` | `--destructive-subtle` | `--destructive` | 마감 · 긴급 |
51
+ | `info` | `--info-subtle` | `--info` | 참고 · 외부 캘린더 |
52
+
53
+ ### 조합 예시
54
+
55
+ ```tsx
56
+ // 캘린더 셀 안쪽 — Calendar가 내부에서 쓰는 방식.
57
+ // 부모가 min-width: 0 걸어주면 title ellipsis가 자동.
58
+ <div className="cellEvents">
59
+ {events.map((e) => (
60
+ <EventChip key={e.id} title={e.title} tone={e.tone} faded={outside} />
61
+ ))}
62
+ </div>
63
+
64
+ // 캘린더 외부 — 선택된 날짜의 상세 리스트에서 재사용.
65
+ // Calendar와 짝 이루는 사이드 패널에서도 톤 체계가 통일되게 유지된다.
66
+ <aside>
67
+ {dayEvents.map((e) => (
68
+ <div key={e.id} className="dayRow">
69
+ <EventChip title={e.title} tone={e.tone} />
70
+ <time>{e.time}</time>
71
+ </div>
72
+ ))}
73
+ </aside>
74
+ ```