@conscience-technology/ctds 0.1.14 → 0.2.0

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 (64) hide show
  1. package/assets/components/AppFrame/AppFrame.module.css +6 -0
  2. package/assets/components/{AttachmentChip/AttachmentChip.module.css → AttachmentItem/AttachmentItem.module.css} +5 -8
  3. package/assets/components/{AttachmentChip/AttachmentChip.tsx → AttachmentItem/AttachmentItem.tsx} +21 -15
  4. package/assets/components/AttachmentItem/index.ts +6 -0
  5. package/assets/components/Button/Button.module.css +11 -2
  6. package/assets/components/ChatInput/ChatInput.tsx +2 -2
  7. package/assets/components/DatePicker/Calendar.tsx +19 -1
  8. package/assets/components/Drawer/Drawer.tsx +120 -39
  9. package/assets/components/Drawer/index.ts +9 -1
  10. package/assets/components/{MessageAttachment/MessageAttachment.module.css → GeneratedImage/GeneratedImage.module.css} +3 -2
  11. package/assets/components/{MessageAttachment/MessageAttachment.tsx → GeneratedImage/GeneratedImage.tsx} +8 -8
  12. package/assets/components/GeneratedImage/index.ts +5 -0
  13. package/assets/components/Icon/Icon.tsx +4 -1
  14. package/assets/components/Modal/Modal.tsx +114 -33
  15. package/assets/components/Modal/index.ts +12 -2
  16. package/assets/components/Spinner/lottieManifest.ts +8 -8
  17. package/assets/components/Switch/Switch.tsx +6 -0
  18. package/assets/components/TableHeader/TableHeader.module.css +29 -20
  19. package/assets/components/TableRow/TableRow.module.css +7 -5
  20. package/assets/components/TableRowEdit/TableRowEdit.module.css +10 -3
  21. package/assets/components/TableRowEdit/TableRowEdit.tsx +7 -2
  22. package/assets/components/Toast/Toast.tsx +19 -0
  23. package/assets/components/_data.ts +39 -16
  24. package/assets/components/app-shell.css +17 -0
  25. package/assets/components/index.ts +77 -0
  26. package/assets/components/reset.css +63 -0
  27. package/assets/components/themeBootScript.ts +40 -0
  28. package/assets/components/tokens.css +68 -57
  29. package/assets/prompts/GUIDE.md +1 -1
  30. package/assets/prompts/PRINCIPLES.md +9 -0
  31. package/assets/prompts/axes/cascade.md +49 -0
  32. package/assets/prompts/axes/mode.md +3 -1
  33. package/assets/prompts/components/_variants.md +84 -0
  34. package/assets/prompts/components/{attachment-chip.md → attachment-item.md} +12 -10
  35. package/assets/prompts/components/attachment-thumb.md +2 -2
  36. package/assets/prompts/components/avatar-upload.md +53 -0
  37. package/assets/prompts/components/bubble.md +2 -2
  38. package/assets/prompts/components/chat-input.md +7 -8
  39. package/assets/prompts/components/code-block.md +2 -2
  40. package/assets/prompts/components/disclosure.md +47 -0
  41. package/assets/prompts/components/file-icon.md +3 -3
  42. package/assets/prompts/components/generated-image.md +38 -0
  43. package/assets/prompts/components/message-quote.md +2 -2
  44. package/assets/prompts/components/message-sources.md +5 -4
  45. package/assets/prompts/components/message-table.md +9 -6
  46. package/assets/prompts/components/onboarding.md +43 -0
  47. package/assets/prompts/components/quotation-bar.md +1 -1
  48. package/assets/prompts/components/sidebar-split.md +70 -0
  49. package/assets/prompts/components/sidebar.md +2 -0
  50. package/assets/prompts/components/time-picker.md +69 -0
  51. package/assets/prompts/components/toolbar.md +69 -0
  52. package/assets/prompts/components/user-message.md +2 -2
  53. package/package.json +80 -3
  54. package/src/commands/init.js +53 -23
  55. package/src/utils/deps.js +23 -17
  56. package/src/utils/patch.js +155 -0
  57. package/assets/components/AttachmentChip/index.ts +0 -6
  58. package/assets/components/Calendar/Calendar.module.css +0 -163
  59. package/assets/components/Calendar/Calendar.tsx +0 -323
  60. package/assets/components/Calendar/index.ts +0 -2
  61. package/assets/components/MessageAttachment/index.ts +0 -5
  62. package/assets/prompts/components/calendar.md +0 -116
  63. package/assets/prompts/components/form-field.md +0 -74
  64. package/assets/prompts/components/message-attachment.md +0 -40
@@ -133,6 +133,12 @@
133
133
  border-inline-end: var(--hairline) solid var(--border);
134
134
  }
135
135
 
136
+ /* opt-out: 소비처가 data-sidebar-border="none"을 걸면 사이드바 오른쪽 hairline을 숨김.
137
+ docs처럼 사이드바가 bg·padding만으로 자체 영역을 표현할 때 사용. */
138
+ .root[data-sidebar-border="none"] .sidebarSlot {
139
+ border-inline-end: none;
140
+ }
141
+
136
142
  /* floating ────────────────────────────────────────────
137
143
  sidebar/topbar/content가 각각 카드로 떠 있음. */
138
144
  .root[data-variant="floating"] {
@@ -1,4 +1,4 @@
1
- /* AttachmentChip — [leading] filename (meta) [actions]
1
+ /* AttachmentItem — [leading] filename (meta) [actions]
2
2
  size sm/md: 1줄 pill
3
3
  size lg: 2줄 큰 카드 (파일명 위 / meta 아래)
4
4
  폭은 전부 --input-w-lg (400px) 고정 — 소비처가 폭 걱정 없이 넣기만. */
@@ -14,6 +14,8 @@
14
14
  color: var(--fg);
15
15
  transition: var(--transition-colors);
16
16
  box-sizing: border-box;
17
+ /* 아주 연한 drop shadow — 종이가 살짝 떠 있는 정도 (hairline보다 아주 약간만 강조). */
18
+ box-shadow: var(--shadow-xs);
17
19
  }
18
20
 
19
21
  /* ───────── size ─────────
@@ -69,7 +71,7 @@
69
71
  .meta {
70
72
  flex: 1 1 auto;
71
73
  min-width: 0;
72
- color: var(--fg-muted);
74
+ color: var(--fg-subtle);
73
75
  font-weight: var(--font-weight-regular);
74
76
  line-height: 1.2;
75
77
  overflow: hidden;
@@ -89,7 +91,7 @@
89
91
  align-items: center;
90
92
  justify-content: center;
91
93
  flex: 0 0 auto;
92
- color: var(--fg-muted);
94
+ color: var(--primary);
93
95
  width: var(--space-6);
94
96
  height: var(--space-6);
95
97
  }
@@ -101,11 +103,6 @@
101
103
  width: var(--space-8);
102
104
  height: var(--space-8);
103
105
  }
104
- .loadingSpinner {
105
- width: 100%;
106
- height: 100%;
107
- }
108
-
109
106
  /* ───────── actions ───────── */
110
107
  .actions {
111
108
  display: inline-flex;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import type { HTMLAttributes, MouseEvent, ReactNode } from "react";
3
+ import type { CSSProperties, HTMLAttributes, MouseEvent, ReactNode } from "react";
4
4
  import {
5
5
  DotsThree as PhDotsThree,
6
6
  DownloadSimple as PhDownload,
@@ -9,19 +9,19 @@ import {
9
9
  import { FileIcon, type FileIconSize } from "@/components/ctds/FileIcon";
10
10
  import { IconButton, type IconButtonSize } from "@/components/ctds/IconButton";
11
11
  import { Spinner } from "@/components/ctds/Spinner";
12
- import styles from "./AttachmentChip.module.css";
12
+ import styles from "./AttachmentItem.module.css";
13
13
 
14
- export type AttachmentChipSize = "sm" | "md" | "lg";
15
- export type AttachmentChipState = "idle" | "loading";
14
+ export type AttachmentItemSize = "sm" | "md" | "lg";
15
+ export type AttachmentItemState = "idle" | "loading";
16
16
 
17
- export interface AttachmentChipProps
17
+ export interface AttachmentItemProps
18
18
  extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "onError"> {
19
19
  /** 파일명. 확장자가 FileIcon 색/라벨을 결정. */
20
20
  filename: string;
21
21
  /** sm/md=1줄, lg=2줄(파일명 위 / meta 아래). 폭은 전부 400px 고정. */
22
- size?: AttachmentChipSize;
22
+ size?: AttachmentItemSize;
23
23
  /** loading이면 좌측 뱃지가 Spinner(lottie-dots)로 교체. */
24
- state?: AttachmentChipState;
24
+ state?: AttachmentItemState;
25
25
  /** sm/md: 파일명 옆 inline 보조 영역. lg: 파일명 아래 2번째 줄.
26
26
  * 문자열이면 muted 라벨로, ReactNode면 그대로 렌더 (ex. Button ghost xs). */
27
27
  meta?: ReactNode;
@@ -33,26 +33,34 @@ export interface AttachmentChipProps
33
33
  onMore?: (e: MouseEvent<HTMLButtonElement>) => void;
34
34
  }
35
35
 
36
- const sizeClass: Record<AttachmentChipSize, string> = {
36
+ const sizeClass: Record<AttachmentItemSize, string> = {
37
37
  sm: styles.sizeSm,
38
38
  md: styles.sizeMd,
39
39
  lg: styles.sizeLg,
40
40
  };
41
41
 
42
42
  /** 좌측 FileIcon 크기 — sm=sm(16), md=md(24), lg=lg(32). */
43
- const FILE_ICON_SIZE: Record<AttachmentChipSize, FileIconSize> = {
43
+ const FILE_ICON_SIZE: Record<AttachmentItemSize, FileIconSize> = {
44
44
  sm: "sm",
45
45
  md: "md",
46
46
  lg: "lg",
47
47
  };
48
48
 
49
49
  /** trailing IconButton 크기 — sm/md=sm(20), lg=md(24). IconButton 공식 사이즈만 사용. */
50
- const ACTION_BTN_SIZE: Record<AttachmentChipSize, IconButtonSize> = {
50
+ const ACTION_BTN_SIZE: Record<AttachmentItemSize, IconButtonSize> = {
51
51
  sm: "sm",
52
52
  md: "sm",
53
53
  lg: "md",
54
54
  };
55
55
 
56
+ /** loading 자리의 Spinner 크기 — loadingSlot(=FileIcon 치수) 16/24/32와 일치시켜야
57
+ * lottie 내부 scale/offset 계산이 안 깨짐. slot 치수를 그대로 --loader-size로 override. */
58
+ const SPINNER_LOADER_SIZE: Record<AttachmentItemSize, string> = {
59
+ sm: "var(--space-4)",
60
+ md: "var(--space-6)",
61
+ lg: "var(--space-8)",
62
+ };
63
+
56
64
  /** middle ellipsis — 폭 400px 안에서 충분히 들어가는 글자 수.
57
65
  * 확장자는 항상 끝에 남긴다. CSS text-overflow:ellipsis가 뒤에서
58
66
  * 확장자를 다시 자르지 않도록 폭 cap은 CSS와 함께 넉넉히 잡는다. */
@@ -60,7 +68,6 @@ function middleEllipsis(filename: string, max: number): string {
60
68
  if (filename.length <= max) return filename;
61
69
  const dot = filename.lastIndexOf(".");
62
70
  const extLen = filename.length - dot;
63
- // 확장자가 없거나 비정상적으로 긴 경우만 폴백.
64
71
  if (dot <= 0 || extLen > 8 || extLen >= max - 4) {
65
72
  return filename.slice(0, Math.max(1, max - 3)) + "...";
66
73
  }
@@ -69,7 +76,7 @@ function middleEllipsis(filename: string, max: number): string {
69
76
  return head + "..." + ext;
70
77
  }
71
78
 
72
- export function AttachmentChip({
79
+ export function AttachmentItem({
73
80
  filename,
74
81
  size = "md",
75
82
  state = "idle",
@@ -79,7 +86,7 @@ export function AttachmentChip({
79
86
  onMore,
80
87
  className,
81
88
  ...rest
82
- }: AttachmentChipProps) {
89
+ }: AttachmentItemProps) {
83
90
  const cls = [
84
91
  styles.root,
85
92
  sizeClass[size],
@@ -89,7 +96,6 @@ export function AttachmentChip({
89
96
  .filter(Boolean)
90
97
  .join(" ");
91
98
 
92
- // 400px 폭에서 안전하게 들어가는 글자 수 (대략치).
93
99
  const maxChars = size === "lg" ? 44 : size === "md" ? 40 : 36;
94
100
  const label = middleEllipsis(filename, maxChars);
95
101
 
@@ -99,7 +105,7 @@ export function AttachmentChip({
99
105
  <Spinner
100
106
  variant="lottie-dots"
101
107
  label="업로드 중"
102
- className={styles.loadingSpinner}
108
+ style={{ "--loader-size": SPINNER_LOADER_SIZE[size] } as CSSProperties}
103
109
  />
104
110
  </span>
105
111
  ) : (
@@ -0,0 +1,6 @@
1
+ export { AttachmentItem } from "./AttachmentItem";
2
+ export type {
3
+ AttachmentItemProps,
4
+ AttachmentItemSize,
5
+ AttachmentItemState,
6
+ } from "./AttachmentItem";
@@ -38,14 +38,17 @@
38
38
  /* Disabled — variant 무관 통일 스타일.
39
39
  회색 버튼(--bg-muted) + 실버 글자(--fg-disabled) + 투명 보더.
40
40
  loading은 opacity로 유지 (스피너 컨텍스트 보존). */
41
- .root:disabled {
41
+ .root:disabled:not([data-loading="true"]) {
42
42
  cursor: not-allowed;
43
43
  background: var(--bg-muted);
44
44
  color: var(--fg-disabled);
45
45
  border-color: transparent;
46
46
  box-shadow: none;
47
47
  }
48
- .root:disabled .icon {
48
+ .root:disabled[data-loading="true"] {
49
+ cursor: progress;
50
+ }
51
+ .root:disabled:not([data-loading="true"]) .icon {
49
52
  opacity: 1;
50
53
  }
51
54
  /* Loading — 라벨/아이콘은 보이지만 톤 다운, Spinner가 중앙에 overlay.
@@ -227,3 +230,9 @@
227
230
  .spinner {
228
231
  display: inline-flex;
229
232
  }
233
+ /* 투명·배경 위 variant는 로딩 스피너를 muted gray로 — primary 색이 흰 배경 위에서
234
+ 혼자 둥둥 떠 보이는 걸 막는다. */
235
+ .outline .spinner,
236
+ .ghost .spinner {
237
+ color: var(--fg-muted);
238
+ }
@@ -12,7 +12,7 @@ import {
12
12
  type ReactNode,
13
13
  } from "react";
14
14
  import { Paperclip as PhPaperclip } from "@phosphor-icons/react";
15
- import { AttachmentChip } from "@/components/ctds/AttachmentChip";
15
+ import { AttachmentItem } from "@/components/ctds/AttachmentItem";
16
16
  import { AttachmentThumb } from "@/components/ctds/AttachmentThumb";
17
17
  import { DropdownMenu } from "@/components/ctds/DropdownMenu";
18
18
  import { QuotationBar } from "@/components/ctds/QuotationBar";
@@ -262,7 +262,7 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
262
262
  }
263
263
  />
264
264
  ) : (
265
- <AttachmentChip
265
+ <AttachmentItem
266
266
  filename={a.filename}
267
267
  onRemove={
268
268
  onRemoveAttachment
@@ -104,7 +104,7 @@ const MonthView = memo(function MonthView({
104
104
  ))}
105
105
  </div>
106
106
 
107
- <div className={styles.grid} role="grid">
107
+ <div className={styles.grid} role="grid" aria-label={monthLabelText(month)}>
108
108
  {cells.map((day) => {
109
109
  const outside = !isSameMonth(day, month);
110
110
  const disabled =
@@ -144,6 +144,8 @@ const MonthView = memo(function MonthView({
144
144
  onMouseEnter={() => onHoverDay(day)}
145
145
  onMouseLeave={() => onHoverDay(null)}
146
146
  aria-selected={isSelected}
147
+ aria-disabled={disabled || undefined}
148
+ aria-label={dayAriaLabel(day, { isToday, isSelected })}
147
149
  role="gridcell"
148
150
  >
149
151
  <span className={styles.dayLabel}>{day.getDate()}</span>
@@ -155,6 +157,22 @@ const MonthView = memo(function MonthView({
155
157
  );
156
158
  });
157
159
 
160
+ /** "2026년 10월" 류 — grid aria-label용. 월 이름이 바뀌면 utils 쪽 MONTH_LABELS_KO 교체. */
161
+ function monthLabelText(d: Date): string {
162
+ return `${d.getFullYear()}년 ${MONTH_LABELS_KO[d.getMonth()]}`;
163
+ }
164
+
165
+ /** "2026년 10월 9일 (금), 오늘, 선택됨" 류 스크린리더용 라벨. */
166
+ function dayAriaLabel(d: Date, flags: { isToday: boolean; isSelected: boolean }): string {
167
+ const parts = [
168
+ `${d.getFullYear()}년 ${MONTH_LABELS_KO[d.getMonth()]} ${d.getDate()}일`,
169
+ `(${WEEKDAY_LABELS_KO[d.getDay()]})`,
170
+ ];
171
+ if (flags.isToday) parts.push("오늘");
172
+ if (flags.isSelected) parts.push("선택됨");
173
+ return parts.join(", ");
174
+ }
175
+
158
176
  export interface CalendarProps {
159
177
  mode: "single" | "range";
160
178
  months: 1 | 2;
@@ -44,7 +44,16 @@ export interface DrawerProps {
44
44
  backdrop?: DrawerBackdrop;
45
45
  }
46
46
 
47
- export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(function Drawer(
47
+ /**
48
+ * 간편 모드 — 가장 흔한 "title + body + footer" 레이아웃을 flat prop으로 넘긴다.
49
+ * 커스터마이즈가 필요하면 compound 모드:
50
+ * <Drawer.Root open onOpenChange={set} side="right" size="md">
51
+ * <Drawer.Header title="…" />
52
+ * <Drawer.Body>…</Drawer.Body>
53
+ * <Drawer.Footer>…</Drawer.Footer>
54
+ * </Drawer.Root>
55
+ */
56
+ const DrawerComponent = forwardRef<HTMLDivElement, DrawerProps>(function Drawer(
48
57
  {
49
58
  open,
50
59
  onOpenChange,
@@ -62,15 +71,48 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(function Drawer(
62
71
  },
63
72
  ref,
64
73
  ) {
65
- // non-modal이면 Radix에게 modal=false로 넘겨 뒤 영역 포인터 이벤트 유지.
66
- const isModal = backdrop !== "none";
74
+ return (
75
+ <DrawerRoot
76
+ open={open}
77
+ onOpenChange={onOpenChange}
78
+ disableEscape={disableEscape}
79
+ side={side}
80
+ size={size}
81
+ backdrop={backdrop}
82
+ ref={ref}
83
+ >
84
+ <DrawerHeader
85
+ title={title}
86
+ description={description}
87
+ titleSrOnly={titleSrOnly}
88
+ dismissible={dismissible}
89
+ dismissLabel={dismissLabel}
90
+ />
91
+ {children !== undefined && children !== null && <DrawerBody>{children}</DrawerBody>}
92
+ {footer && <DrawerFooter>{footer}</DrawerFooter>}
93
+ </DrawerRoot>
94
+ );
95
+ });
67
96
 
97
+ export interface DrawerRootProps {
98
+ open: boolean;
99
+ onOpenChange: (open: boolean) => void;
100
+ children: ReactNode;
101
+ disableEscape?: boolean;
102
+ side?: DrawerSide;
103
+ size?: DrawerSize;
104
+ backdrop?: DrawerBackdrop;
105
+ }
106
+
107
+ export const DrawerRoot = forwardRef<HTMLDivElement, DrawerRootProps>(function DrawerRoot(
108
+ { open, onOpenChange, children, disableEscape, side = "right", size = "md", backdrop = "scrim" },
109
+ ref,
110
+ ) {
111
+ const isModal = backdrop !== "none";
68
112
  return (
69
113
  <RadixDialog.Root open={open} onOpenChange={onOpenChange} modal={isModal}>
70
114
  <RadixDialog.Portal>
71
- {backdrop !== "none" && (
72
- <RadixDialog.Overlay className={styles.overlay} />
73
- )}
115
+ {backdrop !== "none" && <RadixDialog.Overlay className={styles.overlay} />}
74
116
  <RadixDialog.Content
75
117
  ref={ref}
76
118
  className={styles.content}
@@ -80,47 +122,86 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(function Drawer(
80
122
  if (disableEscape) e.preventDefault();
81
123
  }}
82
124
  onPointerDownOutside={(e) => {
83
- // scrim만 바깥 클릭 닫힘. locked/none은 Radix 기본 닫힘 차단.
84
125
  if (backdrop !== "scrim") e.preventDefault();
85
126
  }}
86
127
  onInteractOutside={(e) => {
87
128
  if (backdrop === "none") e.preventDefault();
88
129
  }}
89
130
  >
90
- <div className={styles.header}>
91
- <div className={styles.headerText}>
92
- <RadixDialog.Title
93
- className={[styles.title, titleSrOnly && "visually-hidden"]
94
- .filter(Boolean)
95
- .join(" ")}
96
- >
97
- {title}
98
- </RadixDialog.Title>
99
- {description && (
100
- <RadixDialog.Description className={styles.description}>
101
- {description}
102
- </RadixDialog.Description>
103
- )}
104
- </div>
105
- {dismissible && (
106
- <RadixDialog.Close asChild>
107
- <IconButton
108
- className={styles.close}
109
- variant="ghost"
110
- icon={<Icon name="x" />}
111
- aria-label={dismissLabel}
112
- />
113
- </RadixDialog.Close>
114
- )}
115
- </div>
116
- {children !== undefined && children !== null && (
117
- <div className={styles.body}>
118
- <div className={styles.bodyInner}>{children}</div>
119
- </div>
120
- )}
121
- {footer && <div className={styles.footer}>{footer}</div>}
131
+ {children}
122
132
  </RadixDialog.Content>
123
133
  </RadixDialog.Portal>
124
134
  </RadixDialog.Root>
125
135
  );
126
136
  });
137
+
138
+ export interface DrawerHeaderProps {
139
+ title: ReactNode;
140
+ description?: ReactNode;
141
+ titleSrOnly?: boolean;
142
+ dismissible?: boolean;
143
+ dismissLabel?: string;
144
+ }
145
+
146
+ export function DrawerHeader({
147
+ title,
148
+ description,
149
+ titleSrOnly,
150
+ dismissible = true,
151
+ dismissLabel,
152
+ }: DrawerHeaderProps) {
153
+ return (
154
+ <div className={styles.header}>
155
+ <div className={styles.headerText}>
156
+ <RadixDialog.Title
157
+ className={[styles.title, titleSrOnly && "visually-hidden"]
158
+ .filter(Boolean)
159
+ .join(" ")}
160
+ >
161
+ {title}
162
+ </RadixDialog.Title>
163
+ {description && (
164
+ <RadixDialog.Description className={styles.description}>
165
+ {description}
166
+ </RadixDialog.Description>
167
+ )}
168
+ </div>
169
+ {dismissible && (
170
+ <RadixDialog.Close asChild>
171
+ <IconButton
172
+ className={styles.close}
173
+ variant="ghost"
174
+ icon={<Icon name="x" />}
175
+ aria-label={dismissLabel}
176
+ />
177
+ </RadixDialog.Close>
178
+ )}
179
+ </div>
180
+ );
181
+ }
182
+
183
+ export function DrawerBody({ children }: { children: ReactNode }) {
184
+ return (
185
+ <div className={styles.body}>
186
+ <div className={styles.bodyInner}>{children}</div>
187
+ </div>
188
+ );
189
+ }
190
+
191
+ export function DrawerFooter({ children }: { children: ReactNode }) {
192
+ return <div className={styles.footer}>{children}</div>;
193
+ }
194
+
195
+ type DrawerCompound = typeof DrawerComponent & {
196
+ Root: typeof DrawerRoot;
197
+ Header: typeof DrawerHeader;
198
+ Body: typeof DrawerBody;
199
+ Footer: typeof DrawerFooter;
200
+ };
201
+
202
+ export const Drawer = Object.assign(DrawerComponent, {
203
+ Root: DrawerRoot,
204
+ Header: DrawerHeader,
205
+ Body: DrawerBody,
206
+ Footer: DrawerFooter,
207
+ }) as DrawerCompound;
@@ -1,6 +1,14 @@
1
- export { Drawer } from "./Drawer";
1
+ export {
2
+ Drawer,
3
+ DrawerRoot,
4
+ DrawerHeader,
5
+ DrawerBody,
6
+ DrawerFooter,
7
+ } from "./Drawer";
2
8
  export type {
3
9
  DrawerProps,
10
+ DrawerRootProps,
11
+ DrawerHeaderProps,
4
12
  DrawerSide,
5
13
  DrawerSize,
6
14
  DrawerBackdrop,
@@ -1,5 +1,4 @@
1
- /* MessageAttachment — AI가 생성한 이미지 큰 표시 (AttachmentThumb은 작은 썸네일).
2
- loading / placeholder (src 없음) / image 세 상태. radius-md 고정, 생성 중 sparkle 숨쉼. */
1
+ /* GeneratedImage — AI가 생성한 이미지 큰 표시. loading / placeholder / image 세 상태. */
3
2
 
4
3
  .root {
5
4
  position: relative;
@@ -45,6 +44,8 @@
45
44
 
46
45
  .spinner {
47
46
  color: var(--bg);
47
+ /* image-gen 로띠는 공간이 넉넉해야 보기 좋음 — loader-size override. */
48
+ --loader-size: var(--space-16);
48
49
  }
49
50
 
50
51
  /* Download 버튼 — hover 시만 노출, 우상단 */
@@ -4,30 +4,30 @@ import type { HTMLAttributes, MouseEvent } from "react";
4
4
  import { DownloadSimple as PhDownload } from "@phosphor-icons/react";
5
5
  import { Spinner } from "@/components/ctds/Spinner";
6
6
  import { Tooltip } from "@/components/ctds/Tooltip";
7
- import styles from "./MessageAttachment.module.css";
7
+ import styles from "./GeneratedImage.module.css";
8
8
 
9
- export type MessageAttachmentAspect = "square" | "portrait" | "landscape";
9
+ export type GeneratedImageAspect = "square" | "portrait" | "landscape";
10
10
 
11
- export interface MessageAttachmentProps
11
+ export interface GeneratedImageProps
12
12
  extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
13
13
  /** 생성된 이미지 URL. 없거나 loading이면 placeholder. */
14
14
  src?: string;
15
15
  alt?: string;
16
16
  /** 가로세로 비율. portrait=3/4(기본, 레퍼런스), square=1/1, landscape=4/3. */
17
- aspect?: MessageAttachmentAspect;
17
+ aspect?: GeneratedImageAspect;
18
18
  /** 생성 중 — placeholder + sparkle breathe 애니. */
19
19
  loading?: boolean;
20
20
  /** 다운로드 버튼 클릭. 미지정 시 버튼 숨김. */
21
21
  onDownload?: (e: MouseEvent<HTMLButtonElement>) => void;
22
22
  }
23
23
 
24
- const aspectClass: Record<MessageAttachmentAspect, string> = {
24
+ const aspectClass: Record<GeneratedImageAspect, string> = {
25
25
  square: styles.aspectSquare,
26
26
  portrait: styles.aspectPortrait,
27
27
  landscape: styles.aspectLandscape,
28
28
  };
29
29
 
30
- export function MessageAttachment({
30
+ export function GeneratedImage({
31
31
  src,
32
32
  alt = "",
33
33
  aspect = "portrait",
@@ -35,7 +35,7 @@ export function MessageAttachment({
35
35
  onDownload,
36
36
  className,
37
37
  ...rest
38
- }: MessageAttachmentProps) {
38
+ }: GeneratedImageProps) {
39
39
  const cls = [
40
40
  styles.root,
41
41
  aspectClass[aspect],
@@ -48,7 +48,7 @@ export function MessageAttachment({
48
48
  return (
49
49
  <div className={cls} {...rest}>
50
50
  {loading || !src ? (
51
- <Spinner variant="lottie-dots" size="md" className={styles.spinner} />
51
+ <Spinner variant="lottie-image-gen" size="lg" className={styles.spinner} />
52
52
  ) : (
53
53
  <img src={src} alt={alt} className={styles.img} />
54
54
  )}
@@ -0,0 +1,5 @@
1
+ export { GeneratedImage } from "./GeneratedImage";
2
+ export type {
3
+ GeneratedImageProps,
4
+ GeneratedImageAspect,
5
+ } from "./GeneratedImage";
@@ -128,6 +128,7 @@ import {
128
128
  Scissors as PhScissors,
129
129
  ClipboardText as PhClipboardText,
130
130
  Camera as PhCamera,
131
+ Globe as PhGlobe,
131
132
  } from "@phosphor-icons/react";
132
133
 
133
134
  // Solar (Linear = regular, Bold = fill) ───────────────────────────
@@ -308,7 +309,8 @@ export type IconName =
308
309
  | "copy-simple"
309
310
  | "scissors"
310
311
  | "clipboard-text"
311
- | "camera";
312
+ | "camera"
313
+ | "globe";
312
314
 
313
315
  type AnyComp = React.ComponentType<any>;
314
316
  // Solar는 weight별 두 컴포넌트가 필요. 다른 family는 단일 컴포넌트.
@@ -422,6 +424,7 @@ const phosphorSet: Partial<Record<IconName, AnyComp>> = {
422
424
  scissors: PhScissors,
423
425
  "clipboard-text": PhClipboardText,
424
426
  camera: PhCamera,
427
+ globe: PhGlobe,
425
428
  };
426
429
 
427
430
  /* solar 세트 — 라이브러리에 자연스러운 1:1 대응이 있는 것만.