@conscience-technology/ctds 0.1.9 → 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 (76) 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 +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 +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/message-actions.md +49 -0
  69. package/assets/prompts/components/message-quote.md +26 -0
  70. package/assets/prompts/components/message-sources.md +26 -0
  71. package/assets/prompts/components/message-table.md +27 -0
  72. package/assets/prompts/components/sidebar.md +48 -3
  73. package/assets/prompts/components/spinner.md +109 -0
  74. package/assets/prompts/components/user-message.md +45 -0
  75. package/assets/prompts/primitives/scroll.md +22 -11
  76. package/package.json +1 -1
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Spinner — Motion 축이 로더 성격을 결정.
5
+ *
6
+ * variant 생략 시 Motion 축의 `--loader-style`을 추종한다.
7
+ * - bouncy → dots-bounce
8
+ * - standard → ring
9
+ * - calm → dots-fade
10
+ * - none → ring (접근성 예외: 로딩 신호는 유지)
11
+ *
12
+ * `lottie-<name>` variant는 번들된 Lottie 로티를 렌더 (colorBind=true면 currentColor 추종).
13
+ *
14
+ * 색은 currentColor로 묶여 부모 color를 그대로 승계 (Palette/Hue 축 호환).
15
+ */
16
+
17
+ import { forwardRef, type HTMLAttributes } from "react";
18
+ import styles from "./Spinner.module.css";
19
+ import { LOTTIE_MANIFEST, type LottieName } from "./lottieManifest";
20
+ import { LottiePlayer } from "./LottiePlayer";
21
+
22
+ export type SpinnerSize = "xs" | "sm" | "md" | "lg";
23
+ export type SpinnerBaseVariant = "ring" | "dots-bounce" | "dots-fade" | "pulse";
24
+ export type SpinnerLottieVariant = `lottie-${LottieName}`;
25
+ export type SpinnerVariant = SpinnerBaseVariant | SpinnerLottieVariant;
26
+
27
+ export type SpinnerProps = HTMLAttributes<HTMLSpanElement> & {
28
+ size?: SpinnerSize;
29
+ /** 생략 시 현재 Motion 축의 `--loader-style`을 따름 */
30
+ variant?: SpinnerVariant;
31
+ /** 스크린리더 라벨 */
32
+ label?: string;
33
+ };
34
+
35
+ const sizeClass: Record<SpinnerSize, string> = {
36
+ xs: styles.xs,
37
+ sm: styles.sm,
38
+ md: styles.md,
39
+ lg: styles.lg,
40
+ };
41
+
42
+ const baseVariantClass: Record<SpinnerBaseVariant, string> = {
43
+ ring: styles["v-ring"],
44
+ "dots-bounce": `${styles["v-dots"]} ${styles["v-dots-bounce"]}`,
45
+ "dots-fade": `${styles["v-dots"]} ${styles["v-dots-fade"]}`,
46
+ pulse: styles["v-pulse"],
47
+ };
48
+
49
+ function isLottieVariant(v: SpinnerVariant): v is SpinnerLottieVariant {
50
+ return v.startsWith("lottie-");
51
+ }
52
+
53
+ const Spinner = forwardRef<HTMLSpanElement, SpinnerProps>(
54
+ ({ className, size = "sm", variant, label = "Loading", ...rest }, ref) => {
55
+ let variantPart = "";
56
+ let lottieName: LottieName | null = null;
57
+
58
+ if (variant) {
59
+ if (isLottieVariant(variant)) {
60
+ lottieName = variant.slice("lottie-".length) as LottieName;
61
+ const aspectClass =
62
+ LOTTIE_MANIFEST[lottieName].aspect === "wide" ? styles.wide : "";
63
+ variantPart = `${styles["v-lottie"]} ${aspectClass}`.trim();
64
+ } else {
65
+ variantPart = baseVariantClass[variant];
66
+ }
67
+ } else {
68
+ variantPart = styles["v-auto"];
69
+ }
70
+
71
+ const rootClass = [styles.root, sizeClass[size], variantPart, className ?? ""]
72
+ .filter(Boolean)
73
+ .join(" ");
74
+
75
+ return (
76
+ <span
77
+ ref={ref}
78
+ className={rootClass}
79
+ role="status"
80
+ aria-label={label}
81
+ {...rest}
82
+ >
83
+ {lottieName ? <LottiePlayer name={lottieName} /> : <SpinnerBody />}
84
+ </span>
85
+ );
86
+ },
87
+ );
88
+
89
+ Spinner.displayName = "Spinner";
90
+
91
+ /* 비-로티 variant용 공통 바디. 활성 variant 클래스가 해당하는 자식만 display. */
92
+ function SpinnerBody() {
93
+ return (
94
+ <>
95
+ <svg className={styles.ring} viewBox="0 0 24 24" fill="none" aria-hidden>
96
+ <circle
97
+ cx="12"
98
+ cy="12"
99
+ r="9"
100
+ stroke="currentColor"
101
+ strokeOpacity="0.2"
102
+ strokeWidth="3"
103
+ />
104
+ <path
105
+ d="M12 3a9 9 0 0 1 9 9"
106
+ stroke="currentColor"
107
+ strokeWidth="3"
108
+ strokeLinecap="round"
109
+ />
110
+ </svg>
111
+ <span className={styles.dot} aria-hidden />
112
+ <span className={styles.dot} aria-hidden />
113
+ <span className={styles.dot} aria-hidden />
114
+ <span className={styles.pulse} aria-hidden />
115
+ </>
116
+ );
117
+ }
118
+
119
+ export { Spinner };
@@ -0,0 +1,10 @@
1
+ export { Spinner } from "./Spinner";
2
+ export type {
3
+ SpinnerProps,
4
+ SpinnerSize,
5
+ SpinnerVariant,
6
+ SpinnerBaseVariant,
7
+ SpinnerLottieVariant,
8
+ } from "./Spinner";
9
+ export { LOTTIE_MANIFEST, LOTTIE_NAMES } from "./lottieManifest";
10
+ export type { LottieName, LottieManifestEntry } from "./lottieManifest";
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Lottie JSON 안의 모든 색 배열을 target 색으로 치환.
3
+ *
4
+ * Lottie 색 포맷: `[r, g, b]` 또는 `[r, g, b, a]` (0~1 정규화).
5
+ * 매니페스트에서 `colorBind: true`인 단색 로티에만 사용 — 다색 로티는 색 구조가
6
+ * 흐트러져서 적용 불가.
7
+ */
8
+
9
+ export type Rgb = [number, number, number];
10
+
11
+ /** CSS 색 문자열 → [r,g,b] 0~1 정규화. 서버에서는 null 반환.
12
+ * oklch()/color()처럼 getComputedStyle이 색공간 문자열 그대로 뱉는 경우를 피하려고
13
+ * canvas 2d에 그려 실제 sRGB 픽셀값을 읽어낸다. */
14
+ export function parseCssColor(css: string): Rgb | null {
15
+ if (typeof document === "undefined") return null;
16
+ const canvas = document.createElement("canvas");
17
+ canvas.width = 1;
18
+ canvas.height = 1;
19
+ const ctx = canvas.getContext("2d");
20
+ if (!ctx) return null;
21
+ ctx.fillStyle = "#000";
22
+ ctx.fillStyle = css;
23
+ // 브라우저가 유효하지 않다고 판단한 색은 이전 fillStyle(#000)이 유지됨 → 바로 그리고 읽기
24
+ ctx.fillRect(0, 0, 1, 1);
25
+ const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
26
+ return [r / 255, g / 255, b / 255];
27
+ }
28
+
29
+ /** 원본 색의 밝기(luminance)만 추출해 target 색조에 적용.
30
+ * 단색 로티: 모든 색이 비슷한 밝기 → target 색 그대로.
31
+ * 그라데이션/다색 로티: 밝기 계조는 유지, 색조만 target으로 → 그라데이션 보존.
32
+ *
33
+ * Rec. 601 luma 가중치. 흰색(1,1,1)→target 밝게, 검정(0,0,0)→target 어둡게. */
34
+ export function recolorLottie(data: unknown, target: Rgb): unknown {
35
+ const [tr, tg, tb] = target;
36
+ // target 자신의 luma — 원본이 target과 같은 밝기(=중간)일 때 target 그대로 나오도록 anchor.
37
+ const targetLuma = 0.299 * tr + 0.587 * tg + 0.114 * tb;
38
+
39
+ function recolorOne(src: Rgb): Rgb {
40
+ const [sr, sg, sb] = src;
41
+ const srcLuma = 0.299 * sr + 0.587 * sg + 0.114 * sb;
42
+ if (targetLuma === 0) {
43
+ // target이 완전 검정이면 비율 scale 불가 — 원본 luma만큼 어둡게.
44
+ return [srcLuma * tr, srcLuma * tg, srcLuma * tb];
45
+ }
46
+ // 비율 조정 — srcLuma=targetLuma면 target 그대로, 더 밝으면 target을 더 밝게.
47
+ const k = srcLuma / targetLuma;
48
+ const clamp = (v: number) => Math.max(0, Math.min(1, v));
49
+ return [clamp(tr * k), clamp(tg * k), clamp(tb * k)];
50
+ }
51
+
52
+ function walk(node: unknown): unknown {
53
+ if (Array.isArray(node)) {
54
+ if (
55
+ (node.length === 3 || node.length === 4) &&
56
+ node.every((v) => typeof v === "number" && v >= 0 && v <= 1)
57
+ ) {
58
+ const [r, g, b] = node as number[];
59
+ const [nr, ng, nb] = recolorOne([r, g, b]);
60
+ const alpha = node.length === 4 ? node[3] : undefined;
61
+ return alpha !== undefined ? [nr, ng, nb, alpha] : [nr, ng, nb];
62
+ }
63
+ return node.map(walk);
64
+ }
65
+ if (node && typeof node === "object") {
66
+ const out: Record<string, unknown> = {};
67
+ for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
68
+ out[k] = walk(v);
69
+ }
70
+ return out;
71
+ }
72
+ return node;
73
+ }
74
+
75
+ return walk(data);
76
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Lottie 로더 매니페스트.
3
+ *
4
+ * `name`: Spinner variant로 쓰는 식별자 (`lottie-<name>` 또는 매니페스트의 name 그대로)
5
+ * `path`: /public 밑 상대 경로
6
+ * `colorBind`:
7
+ * true — 단색 로티. 런타임에 모든 색을 currentColor로 치환해 Palette/Hue 축을 추종.
8
+ * false — 다색 로티. 원본 색 유지 (브랜드 톤 또는 작가 의도).
9
+ * `aspect`: 'square' | 'wide' — 바깥 박스 크기 결정에 참조.
10
+ */
11
+
12
+ export type LottieName =
13
+ | "rabbit"
14
+ | "infiniti"
15
+ | "dots"
16
+ | "spinner"
17
+ | "clock"
18
+ | "cosmos"
19
+ | "image-gen"
20
+ | "loading-square";
21
+
22
+ export type LottieManifestEntry = {
23
+ name: LottieName;
24
+ path: string;
25
+ colorBind: boolean;
26
+ aspect: "square" | "wide";
27
+ /** 로티 원본이 캔버스를 덜 채워 체감 크기가 작을 때, 쇼케이스·사용처에서 쓸 보정 배율.
28
+ * 1 = 원본 그대로. 1보다 크면 bounds 안에서 scale-up (overflow 숨김 전제). */
29
+ visualScale?: number;
30
+ /** 한줄 설명 — showcase · 자동완성용 */
31
+ description: string;
32
+ };
33
+
34
+ export const LOTTIE_MANIFEST: Record<LottieName, LottieManifestEntry> = {
35
+ rabbit: {
36
+ name: "rabbit",
37
+ path: "/lottie/rabbit.json",
38
+ colorBind: true,
39
+ aspect: "square",
40
+ visualScale: 1.4,
41
+ description: "뛰어가는 토끼. 긴 작업·AI 생성 대기",
42
+ },
43
+ infiniti: {
44
+ name: "infiniti",
45
+ path: "/lottie/infiniti.json",
46
+ colorBind: true,
47
+ aspect: "wide",
48
+ visualScale: 1.3,
49
+ description: "무한 루프 선. 네트워크·동기화",
50
+ },
51
+ dots: {
52
+ name: "dots",
53
+ path: "/lottie/dots.json",
54
+ colorBind: false,
55
+ aspect: "square",
56
+ visualScale: 1.3,
57
+ description: "그라데이션 dot 흐름. 포인트 로더",
58
+ },
59
+ spinner: {
60
+ name: "spinner",
61
+ path: "/lottie/spinner.json",
62
+ colorBind: true,
63
+ aspect: "wide",
64
+ description: "표준 라인 스피너",
65
+ },
66
+ clock: {
67
+ name: "clock",
68
+ path: "/lottie/clock.json",
69
+ colorBind: true,
70
+ aspect: "wide",
71
+ visualScale: 1.8,
72
+ description: "회전 시계 바늘. 처리 대기·타임아웃",
73
+ },
74
+ cosmos: {
75
+ name: "cosmos",
76
+ path: "/lottie/cosmos.json",
77
+ colorBind: true,
78
+ aspect: "square",
79
+ description: "그라데이션 궤도. 몽환적·온보딩",
80
+ },
81
+ "image-gen": {
82
+ name: "image-gen",
83
+ path: "/lottie/image-gen.json",
84
+ colorBind: true,
85
+ aspect: "square",
86
+ visualScale: 1.1,
87
+ description: "이미지 생성 대기. AI 이미지",
88
+ },
89
+ "loading-square": {
90
+ name: "loading-square",
91
+ path: "/lottie/loading-square.json",
92
+ colorBind: true,
93
+ aspect: "square",
94
+ visualScale: 2.8,
95
+ description: "회전하는 사각형. 미니멀 처리 대기",
96
+ },
97
+ };
98
+
99
+ export const LOTTIE_NAMES = Object.keys(LOTTIE_MANIFEST) as LottieName[];
@@ -107,7 +107,7 @@
107
107
  position: relative;
108
108
  }
109
109
  .size_sm .triggerUnderline { padding-inline: var(--space-1_5); }
110
- .size_lg .triggerUnderline { padding-inline: var(--space-3); }
110
+ .size_lg .triggerUnderline { padding-inline: var(--space-1_5); }
111
111
 
112
112
  .listUnderline { position: relative; }
113
113
 
@@ -126,8 +126,8 @@
126
126
  transform: translateX(calc(var(--tab-indicator-x) + var(--space-1_5)));
127
127
  }
128
128
  .size_lg .underlineBar {
129
- width: calc(var(--tab-indicator-w) - var(--space-3) * 2);
130
- transform: translateX(calc(var(--tab-indicator-x) + var(--space-3)));
129
+ width: calc(var(--tab-indicator-w) - var(--space-1_5) * 2);
130
+ transform: translateX(calc(var(--tab-indicator-x) + var(--space-1_5)));
131
131
  }
132
132
  /* ready=true 이후에만 transition — 초기 배치는 순간이동. */
133
133
  .listUnderline[data-ready="true"] .underlineBar {
@@ -0,0 +1,41 @@
1
+ /* UserMessage — Bubble + MessageActions. 오른쪽 정렬. */
2
+
3
+ .root {
4
+ display: flex;
5
+ flex-direction: column;
6
+ align-items: flex-end;
7
+ gap: var(--space-1);
8
+ width: 100%;
9
+ }
10
+
11
+ /* 첨부·인용 등 bubble 위에 올리는 영역 */
12
+ .before {
13
+ display: flex;
14
+ flex-direction: column;
15
+ align-items: flex-end;
16
+ gap: var(--space-1);
17
+ max-width: 100%;
18
+ }
19
+
20
+ .actions {
21
+ display: flex;
22
+ }
23
+
24
+ /* actions-visibility=show: 상시 노출 */
25
+ .root[data-actions-visibility="show"] .actions {
26
+ opacity: 1;
27
+ }
28
+
29
+ /* actions-visibility=hover: hover/focus-within에서만. 부드러운 fade + slight translate. */
30
+ .root[data-actions-visibility="hover"] .actions {
31
+ opacity: 0;
32
+ transform: translateY(calc(var(--space-0_5) * -1));
33
+ transition:
34
+ opacity var(--duration-slow) var(--ease-out),
35
+ transform var(--duration-slow) var(--ease-out);
36
+ }
37
+ .root[data-actions-visibility="hover"]:hover .actions,
38
+ .root[data-actions-visibility="hover"]:focus-within .actions {
39
+ opacity: 1;
40
+ transform: translateY(0);
41
+ }
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import type { HTMLAttributes, ReactNode } from "react";
4
+ import { Bubble, type BubbleFill, type BubbleShape, type BubbleSize } from "@/components/ctds/Bubble";
5
+ import {
6
+ MessageActions,
7
+ type MessageAction,
8
+ } from "@/components/ctds/MessageActions";
9
+ import styles from "./UserMessage.module.css";
10
+
11
+ export type UserMessageActionsVisibility = "show" | "hover";
12
+
13
+ export interface UserMessageProps extends HTMLAttributes<HTMLDivElement> {
14
+ /** 메시지 본문. string 또는 ReactNode. */
15
+ children: ReactNode;
16
+ /** 버블 모양. 기본 circular-drop. */
17
+ shape?: BubbleShape;
18
+ /** 버블 fill. 기본 translucent. */
19
+ fill?: BubbleFill;
20
+ /** 버블 size. 기본 md. */
21
+ size?: BubbleSize;
22
+ /** 액션 노출 — "show" 상시 / "hover" 호버 시만. 기본 hover. */
23
+ actionsVisibility?: UserMessageActionsVisibility;
24
+ /** 액션 커스터마이즈. 기본 [copy, edit]. 비우려면 []. */
25
+ actions?: MessageAction[];
26
+ /** 메시지 위에 올라올 첨부(이미지/파일) 등 — 상위에서 자유 조합. */
27
+ before?: ReactNode;
28
+ }
29
+
30
+ const DEFAULT_ACTIONS: MessageAction[] = [
31
+ { kind: "copy" },
32
+ { kind: "edit" },
33
+ ];
34
+
35
+ export function UserMessage({
36
+ children,
37
+ shape = "circular-drop",
38
+ fill = "translucent",
39
+ size = "md",
40
+ actionsVisibility = "hover",
41
+ actions = DEFAULT_ACTIONS,
42
+ before,
43
+ className,
44
+ ...rest
45
+ }: UserMessageProps) {
46
+ const cls = [styles.root, className].filter(Boolean).join(" ");
47
+ const hasActions = actions.length > 0;
48
+
49
+ return (
50
+ <div
51
+ className={cls}
52
+ data-actions-visibility={actionsVisibility}
53
+ {...rest}
54
+ >
55
+ {before ? <div className={styles.before}>{before}</div> : null}
56
+ <Bubble shape={shape} fill={fill} size={size} tail="end">
57
+ {children}
58
+ </Bubble>
59
+ {hasActions ? (
60
+ <div className={styles.actions}>
61
+ <MessageActions align="end" actions={actions} />
62
+ </div>
63
+ ) : null}
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,2 @@
1
+ export { UserMessage } from "./UserMessage";
2
+ export type { UserMessageProps, UserMessageActionsVisibility } from "./UserMessage";
@@ -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,43 @@
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
+ isGenerating={boolean} // true면 액션 숨김
34
+ >
35
+ 본문 — markdown 또는 블록 조합
36
+ </AIMessage>
37
+ ```
38
+
39
+ ### 레이아웃
40
+ - root: `flex-direction: column; align-items: flex-start; gap: space-2`
41
+ - label: `text-xs`, `fg-muted`
42
+ - body: `text-base`, `line-height: 2` (평소의 ~2배, 가독성), 색 `--fg` (다크 그레이, strong보다 한 톤 연함), markdown 기본 톤
43
+ - 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]] 사용.