@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
@@ -29,7 +29,16 @@ export interface ModalProps {
29
29
  disableEscape?: boolean;
30
30
  }
31
31
 
32
- export const Modal = forwardRef<HTMLDivElement, ModalProps>(function Modal(
32
+ /**
33
+ * 간편 모드 — 가장 흔한 "title + body + footer" 레이아웃을 flat prop으로 넘긴다.
34
+ * 커스터마이즈가 필요하면 compound 모드:
35
+ * <Modal.Root open onOpenChange={set}>
36
+ * <Modal.Header title="…" />
37
+ * <Modal.Body>…</Modal.Body>
38
+ * <Modal.Footer>…</Modal.Footer>
39
+ * </Modal.Root>
40
+ */
41
+ const ModalComponent = forwardRef<HTMLDivElement, ModalProps>(function Modal(
33
42
  {
34
43
  open,
35
44
  onOpenChange,
@@ -43,6 +52,38 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(function Modal(
43
52
  disableEscape,
44
53
  },
45
54
  ref,
55
+ ) {
56
+ return (
57
+ <ModalRoot
58
+ open={open}
59
+ onOpenChange={onOpenChange}
60
+ disableEscape={disableEscape}
61
+ ref={ref}
62
+ >
63
+ <ModalHeader
64
+ title={title}
65
+ description={description}
66
+ titleSrOnly={titleSrOnly}
67
+ dismissible={dismissible}
68
+ dismissLabel={dismissLabel}
69
+ />
70
+ {children !== undefined && children !== null && <ModalBody>{children}</ModalBody>}
71
+ {footer && <ModalFooter>{footer}</ModalFooter>}
72
+ </ModalRoot>
73
+ );
74
+ });
75
+
76
+ export interface ModalRootProps {
77
+ open: boolean;
78
+ onOpenChange: (open: boolean) => void;
79
+ children: ReactNode;
80
+ /** ESC 키 닫힘 차단. 중요한 확인/저장 중엔 true. */
81
+ disableEscape?: boolean;
82
+ }
83
+
84
+ export const ModalRoot = forwardRef<HTMLDivElement, ModalRootProps>(function ModalRoot(
85
+ { open, onOpenChange, children, disableEscape },
86
+ ref,
46
87
  ) {
47
88
  return (
48
89
  <RadixDialog.Root open={open} onOpenChange={onOpenChange}>
@@ -59,40 +100,80 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(function Modal(
59
100
  e.preventDefault();
60
101
  }}
61
102
  >
62
- <div className={styles.header}>
63
- <div className={styles.headerText}>
64
- <RadixDialog.Title
65
- className={[styles.title, titleSrOnly && "visually-hidden"]
66
- .filter(Boolean)
67
- .join(" ")}
68
- >
69
- {title}
70
- </RadixDialog.Title>
71
- {description && (
72
- <RadixDialog.Description className={styles.description}>
73
- {description}
74
- </RadixDialog.Description>
75
- )}
76
- </div>
77
- {dismissible && (
78
- <RadixDialog.Close asChild>
79
- <IconButton
80
- className={styles.close}
81
- variant="ghost"
82
- icon={<Icon name="x" />}
83
- aria-label={dismissLabel}
84
- />
85
- </RadixDialog.Close>
86
- )}
87
- </div>
88
- {children !== undefined && children !== null && (
89
- <div className={styles.body}>
90
- <div className={styles.bodyInner}>{children}</div>
91
- </div>
92
- )}
93
- {footer && <div className={styles.footer}>{footer}</div>}
103
+ {children}
94
104
  </RadixDialog.Content>
95
105
  </RadixDialog.Portal>
96
106
  </RadixDialog.Root>
97
107
  );
98
108
  });
109
+
110
+ export interface ModalHeaderProps {
111
+ title: ReactNode;
112
+ description?: ReactNode;
113
+ titleSrOnly?: boolean;
114
+ dismissible?: boolean;
115
+ dismissLabel?: string;
116
+ }
117
+
118
+ export function ModalHeader({
119
+ title,
120
+ description,
121
+ titleSrOnly,
122
+ dismissible = true,
123
+ dismissLabel,
124
+ }: ModalHeaderProps) {
125
+ return (
126
+ <div className={styles.header}>
127
+ <div className={styles.headerText}>
128
+ <RadixDialog.Title
129
+ className={[styles.title, titleSrOnly && "visually-hidden"]
130
+ .filter(Boolean)
131
+ .join(" ")}
132
+ >
133
+ {title}
134
+ </RadixDialog.Title>
135
+ {description && (
136
+ <RadixDialog.Description className={styles.description}>
137
+ {description}
138
+ </RadixDialog.Description>
139
+ )}
140
+ </div>
141
+ {dismissible && (
142
+ <RadixDialog.Close asChild>
143
+ <IconButton
144
+ className={styles.close}
145
+ variant="ghost"
146
+ icon={<Icon name="x" />}
147
+ aria-label={dismissLabel}
148
+ />
149
+ </RadixDialog.Close>
150
+ )}
151
+ </div>
152
+ );
153
+ }
154
+
155
+ export function ModalBody({ children }: { children: ReactNode }) {
156
+ return (
157
+ <div className={styles.body}>
158
+ <div className={styles.bodyInner}>{children}</div>
159
+ </div>
160
+ );
161
+ }
162
+
163
+ export function ModalFooter({ children }: { children: ReactNode }) {
164
+ return <div className={styles.footer}>{children}</div>;
165
+ }
166
+
167
+ type ModalCompound = typeof ModalComponent & {
168
+ Root: typeof ModalRoot;
169
+ Header: typeof ModalHeader;
170
+ Body: typeof ModalBody;
171
+ Footer: typeof ModalFooter;
172
+ };
173
+
174
+ export const Modal = Object.assign(ModalComponent, {
175
+ Root: ModalRoot,
176
+ Header: ModalHeader,
177
+ Body: ModalBody,
178
+ Footer: ModalFooter,
179
+ }) as ModalCompound;
@@ -1,2 +1,12 @@
1
- export { Modal } from "./Modal";
2
- export type { ModalProps } from "./Modal";
1
+ export {
2
+ Modal,
3
+ ModalRoot,
4
+ ModalHeader,
5
+ ModalBody,
6
+ ModalFooter,
7
+ } from "./Modal";
8
+ export type {
9
+ ModalProps,
10
+ ModalRootProps,
11
+ ModalHeaderProps,
12
+ } from "./Modal";
@@ -42,21 +42,21 @@ export const LOTTIE_MANIFEST: Record<LottieName, LottieManifestEntry> = {
42
42
  name: "rabbit",
43
43
  path: "/lottie/rabbit.json",
44
44
  colorBind: true,
45
- visualScale: 1.6,
45
+ visualScale: 1.4,
46
46
  description: "뛰어가는 토끼. 긴 작업·AI 생성 대기",
47
47
  },
48
48
  infiniti: {
49
49
  name: "infiniti",
50
50
  path: "/lottie/infiniti.json",
51
51
  colorBind: true,
52
- visualScale: 3.6,
52
+ visualScale: 3.2,
53
53
  description: "무한 루프 선. 네트워크·동기화",
54
54
  },
55
55
  dots: {
56
56
  name: "dots",
57
57
  path: "/lottie/dots.json",
58
58
  colorBind: true,
59
- visualScale: 1.08,
59
+ visualScale: 0.95,
60
60
  offsetY: 0.025,
61
61
  description: "그라데이션 dot 흐름. 포인트 로더",
62
62
  },
@@ -64,35 +64,35 @@ export const LOTTIE_MANIFEST: Record<LottieName, LottieManifestEntry> = {
64
64
  name: "spinner",
65
65
  path: "/lottie/spinner.json",
66
66
  colorBind: true,
67
- visualScale: 2.25,
67
+ visualScale: 2,
68
68
  description: "표준 라인 스피너",
69
69
  },
70
70
  clock: {
71
71
  name: "clock",
72
72
  path: "/lottie/clock.json",
73
73
  colorBind: true,
74
- visualScale: 4.25,
74
+ visualScale: 3.8,
75
75
  description: "회전 시계 바늘. 처리 대기·타임아웃",
76
76
  },
77
77
  cosmos: {
78
78
  name: "cosmos",
79
79
  path: "/lottie/cosmos.json",
80
80
  colorBind: true,
81
- visualScale: 1.35,
81
+ visualScale: 1.2,
82
82
  description: "그라데이션 궤도. 몽환적·온보딩",
83
83
  },
84
84
  "image-gen": {
85
85
  name: "image-gen",
86
86
  path: "/lottie/image-gen.json",
87
87
  colorBind: true,
88
- visualScale: 1.25,
88
+ visualScale: 1.1,
89
89
  description: "이미지 생성 대기. AI 이미지",
90
90
  },
91
91
  "loading-square": {
92
92
  name: "loading-square",
93
93
  path: "/lottie/loading-square.json",
94
94
  colorBind: true,
95
- visualScale: 3.9,
95
+ visualScale: 3.5,
96
96
  description: "회전하는 사각형. 미니멀 처리 대기",
97
97
  },
98
98
  };
@@ -16,6 +16,12 @@ export type SwitchProps = Omit<
16
16
  "type" | "size"
17
17
  > & {
18
18
  size?: SwitchSize;
19
+ /**
20
+ * 토글 변경 콜백. 공식 API — 대부분의 경우 이걸 쓴다.
21
+ * 네이티브 `onChange`도 받아낼 수 있는데, 둘 다 쓰면 호출 순서는:
22
+ * 1) onChange(e) — 호출자가 e.preventDefault() 할 수 있는 escape hatch.
23
+ * 2) onChange가 preventDefault 하지 않으면 onCheckedChange(checked).
24
+ */
19
25
  onCheckedChange?: (checked: boolean) => void;
20
26
  };
21
27
 
@@ -92,16 +92,18 @@
92
92
  background: var(--bg);
93
93
  }
94
94
 
95
- /* pinned 경계 — hairline으로 "고정 영역" 분리. TableRow와 짝. */
95
+ /* pinned 경계 — hairline으로 "고정 영역" 분리. TableRow와 짝.
96
+ border 대신 inset box-shadow로 그어 sticky 레이어 subpixel 겹침(두 줄처럼
97
+ 보이는 Chrome 렌더 현상) 회피 + 셀 폭에 영향 안 줌. */
96
98
  .cellSlot[data-pinned="left"] {
97
- border-right: var(--hairline) solid var(--border);
99
+ box-shadow: inset calc(var(--hairline) * -1) 0 0 var(--border);
98
100
  }
99
101
  .cellSlot[data-pinned="right"] {
100
- border-left: var(--hairline) solid var(--border);
102
+ box-shadow: inset var(--hairline) 0 0 var(--border);
101
103
  }
102
104
 
103
105
  .selectCell {
104
- border-right: var(--hairline) solid var(--border);
106
+ box-shadow: inset calc(var(--hairline) * -1) 0 0 var(--border);
105
107
  }
106
108
 
107
109
  /* Cell (columnheader) ─────────────────────────────────────── */
@@ -210,6 +212,15 @@
210
212
  display: none;
211
213
  }
212
214
 
215
+ /* pinned 경계 hairline과 tick(cellSlot 좌측 세로선) 겹침 방지.
216
+ - pinned-left 셀의 바로 다음 cellSlot: pinned의 border-right와 다음 셀의
217
+ tick이 바로 붙어 두 줄처럼 보이는 현상 제거.
218
+ - pinned-right 셀 자신: border-left와 자기 tick이 같은 지점에 겹침. */
219
+ .cellSlot[data-pinned="left"] + .cellSlot .tick,
220
+ .cellSlot[data-pinned="right"] .tick {
221
+ display: none;
222
+ }
223
+
213
224
  /* Resize handle ──────────────────────────────────────────────
214
225
  히트 영역은 셀 우측 경계에 걸쳐 있는 투명 버튼. 시각 세로선은 안쪽 .resizeLine이
215
226
  담당하고, 라인은 handle 폭 안에서 "우측 끝"이 아니라 "중앙"에 위치 →
@@ -217,11 +228,10 @@
217
228
  .resizeHandle {
218
229
  position: absolute;
219
230
  top: 0;
220
- /* handle 중심을 다음 세로 구분선 위치에 정확히 맞춤.
221
- - 데이터 row의 세로선: 다음 cellSlot의 좌측 경계(= 현재 셀 우측 + cellSlot gap).
222
- - handle 중심이 그 지점에 오도록: right = -(gap + handleWidth/2).
223
- cellSlot gap은 TableRow의 column-gap(--space-1). */
224
- right: calc((var(--space-1) * -1) - (var(--space-3) * 0.5));
231
+ /* handle 중심을 셀 우측 경계(= 데이터 row의 세로 구분선)에 정확히 맞춤.
232
+ TableRow는 cellSlot 사이 column-gap을 쓰지 않으므로 추가 보정 없음.
233
+ right = -(handleWidth/2). */
234
+ right: calc(var(--space-3) * -0.5);
225
235
  bottom: 0;
226
236
  width: var(--space-3);
227
237
  padding: 0;
@@ -241,36 +251,35 @@
241
251
  border-radius: var(--radius-xs);
242
252
  }
243
253
 
244
- /* 시각 세로선 — plate에서만 hover 시 표시. line은 자체 tick으로 대체하므로 숨김.
245
- 색은 border-strong(살짝 밝은 hairline 톤) — fg-subtle은 너무 진해서 데이터 색과 경쟁함. */
246
254
  /* 둥근 pill 핸들 — 선이 아니라 떠있는 그립 느낌.
247
- 폭은 좁고 세로로 길쭉한 캡슐, 연한 중성 톤에 그림자로 "집어들 수 있는" 어포던스. */
255
+ 흰(bg) 캡슐에 연한 hairline + 연한 그림자로 "집어들 수 있는" 어포던스.
256
+ hover 전엔 숨겨두고, 셀 hover 때 opacity로 올려 보여줌. */
248
257
  .resizeLine {
249
258
  display: block;
250
- width: var(--space-1);
251
- height: 60%;
252
- background: var(--border-strong);
259
+ width: calc(var(--space-1) * 1.25);
260
+ height: 48%;
261
+ background: var(--bg);
262
+ border: var(--hairline) solid var(--border);
253
263
  border-radius: var(--radius-full);
254
264
  opacity: 0;
255
265
  box-shadow: var(--shadow-xs);
256
266
  transition:
257
267
  opacity var(--duration-fast) var(--ease-out),
258
- background var(--duration-fast) var(--ease-out),
259
268
  height var(--duration-fast) var(--ease-out),
260
269
  box-shadow var(--duration-fast) var(--ease-out);
261
270
  }
262
271
 
263
272
  /* 셀 전체 hover: pill 연하게 노출. */
264
273
  .cell:hover .resizeLine {
265
- opacity: 0.6;
274
+ opacity: 0.7;
266
275
  }
267
276
 
268
- /* handle hover/active: 더 또렷하게 + 길어지고 shadow 깊어짐 — "집은 상태". */
277
+ /* handle hover/active: 또렷하게 + 길어지고 그림자 깊어짐 — "집은 상태".
278
+ 색/스트로크는 유지, 변하는 건 opacity·height·shadow 뿐. */
269
279
  .resizeHandle:hover .resizeLine,
270
280
  .resizeHandle:active .resizeLine {
271
281
  opacity: 1;
272
- background: var(--fg-muted);
273
- height: 80%;
282
+ height: 64%;
274
283
  box-shadow: var(--shadow-sm);
275
284
  }
276
285
 
@@ -146,18 +146,20 @@ a.root:not(.disabled):focus-visible,
146
146
  }
147
147
 
148
148
  /* pinned 경계 — scroll shadow 대신 상시 hairline으로 "여기까지 고정 영역"을 분명히.
149
- 좌측 pinned → 우측 경계에 세로 hairline. 우측 pinned → 좌측 경계. */
149
+ 좌측 pinned → 우측 경계, 우측 pinned → 좌측 경계.
150
+ border 대신 inset box-shadow로 그어 sticky 레이어 subpixel 겹침(두 줄처럼
151
+ 보이는 Chrome 렌더 현상) 회피 + 셀 폭에 영향 안 줌. */
150
152
  .cellSlot[data-pinned="left"] {
151
- border-right: var(--hairline) solid var(--border);
153
+ box-shadow: inset calc(var(--hairline) * -1) 0 0 var(--border);
152
154
  }
153
155
  .cellSlot[data-pinned="right"] {
154
- border-left: var(--hairline) solid var(--border);
156
+ box-shadow: inset var(--hairline) 0 0 var(--border);
155
157
  }
156
158
 
157
159
  /* selectable + pinned 조합일 때 selectCell이 "실질적 좌측 pinned의 선두" 역할이라
158
- selectCell에도 우측 hairline을 그어 경계 톤 일치. */
160
+ selectCell에도 우측 경계를 그어 톤 일치. */
159
161
  .selectCell {
160
- border-right: var(--hairline) solid var(--border);
162
+ box-shadow: inset calc(var(--hairline) * -1) 0 0 var(--border);
161
163
  }
162
164
 
163
165
  /* ── Cell ───────────────────────────────────────────────
@@ -63,6 +63,7 @@
63
63
  pointer-events: none;
64
64
  }
65
65
 
66
+
66
67
  /* ── Edit cell ────────────────────────────────────────────
67
68
  클릭/포커스 타겟. 활성 상태엔 input/select가 셀을 꽉 채움. */
68
69
  .editCell {
@@ -102,11 +103,17 @@
102
103
  left: 0;
103
104
  right: 0;
104
105
  /* 1줄 상태엔 셀 높이에 딱 맞도록 min-height: 100%. textarea autosize가
105
- 여러 줄로 늘어나면 host는 content 높이를 따라감(아래로 확장). */
106
+ 여러 줄로 늘어나면 host 자체가 scroll 컨테이너가 되고(max-height 도달 시),
107
+ textarea 전체(자기 padding 포함)가 host 안에서 위로 올라감 → 상·하 padding이
108
+ scroll content의 일부라 host border에서 자연스레 clip, 안쪽 여백 항상 보존.
109
+ scroll.md "textarea 특수 패턴" 규약. padding은 textarea가 책임. */
106
110
  min-height: 100%;
111
+ /* 3.5줄 × line-height + 상·하 padding(space-2 각각) — 그 이상은 host가 scroll. */
112
+ max-height: calc(var(--lh-sm) * 3.5 + var(--space-2) * 2);
107
113
  display: flex;
108
- align-items: center;
109
- padding-inline: var(--space-2);
114
+ align-items: stretch;
115
+ padding: 0;
116
+ overflow-y: auto;
110
117
  background: var(--bg);
111
118
  border: var(--hairline) solid var(--border-strong);
112
119
  border-radius: var(--radius-sm);
@@ -179,8 +179,13 @@ function TableRowEditInner<T>(
179
179
  /* 세로 구분선은 editable 여부와 무관하게 모든 셀 사이에 균일하게.
180
180
  엑셀 톤 "셀 그리드" 신호는 일관성이 핵심 — read-only 셀만 선이 빠지면
181
181
  리듬이 깨져 보임. 첫 데이터 셀은 왼쪽에 선 없음(테이블 외곽).
182
- selectable이면 체크박스 ↔ 첫 데이터 셀 사이에도 선. */
183
- const showLeftDivider = idx > 0 || selectable;
182
+ selectable이면 체크박스 ↔ 첫 데이터 셀 사이에도 선.
183
+ pinned 경계는 cellSlot box-shadow가 담당 — pinned-right 셀 자신과
184
+ pinned-left 바로 다음 셀의 왼쪽 divider는 그으면 두 줄로 겹쳐 보임. */
185
+ const prevCol = idx > 0 ? columnMap.get(orderedKeys[idx - 1]) : undefined;
186
+ const skipDivider =
187
+ col.pinned === "right" || prevCol?.pinned === "left";
188
+ const showLeftDivider = (idx > 0 || selectable) && !skipDivider;
184
189
  return (
185
190
  <div
186
191
  key={col.key}
@@ -27,7 +27,12 @@ export interface ToastOptions {
27
27
  tone?: ToastTone;
28
28
  surface?: ToastSurface;
29
29
  icon?: IconName;
30
+ /** 액션 요소(Button 등). `actionAltText`도 함께 넘겨야 렌더링된다 — 스크린리더용 라벨이 없으면 Radix가 action을 숨긴다. */
30
31
  action?: ReactNode;
32
+ /**
33
+ * action을 쓸 때 **필수** — 스크린리더가 action 버튼을 어떻게 안내할지. Toast 사라져도
34
+ * 사용자가 수행할 수 있는 대안 설명(예: "실행 취소", "설정 열기"). 생략 시 action 미렌더.
35
+ */
31
36
  actionAltText?: string;
32
37
  dismissible?: boolean;
33
38
  dismissLabel?: string;
@@ -227,6 +232,9 @@ export const Toast = forwardRef<HTMLLIElement, ToastProps>(function Toast(
227
232
  {action}
228
233
  </RadixToast.Action>
229
234
  )}
235
+ {action && !actionAltText && typeof window !== "undefined" && process.env.NODE_ENV !== "production" && (
236
+ <ToastActionWarning />
237
+ )}
230
238
  {dismissible && (
231
239
  <RadixToast.Close asChild>
232
240
  <IconButton
@@ -241,3 +249,14 @@ export const Toast = forwardRef<HTMLLIElement, ToastProps>(function Toast(
241
249
  </RadixToast.Root>
242
250
  );
243
251
  });
252
+
253
+ /** dev-only. action은 주고 actionAltText를 빠뜨리면 Radix가 조용히 숨기므로 console로 경고. */
254
+ function ToastActionWarning() {
255
+ if (typeof window !== "undefined") {
256
+ // eslint-disable-next-line no-console
257
+ console.warn(
258
+ "[CTDS Toast] `action` prop을 넘겼는데 `actionAltText`가 비어있어요. 스크린리더용 라벨이 없으면 action이 렌더되지 않습니다.",
259
+ );
260
+ }
261
+ return null;
262
+ }
@@ -585,7 +585,6 @@ export const componentSlugs: Record<string, string> = {
585
585
  "Table Row": "data/table-row",
586
586
  "Table Row Select": "data/table-row-select",
587
587
  "Table Row Edit": "data/table-row-edit",
588
- Calendar: "data/calendar",
589
588
  "Event Chip": "data/event-chip",
590
589
  "List Item": "data/list-item",
591
590
  "Metric Card": "data/metric-card",
@@ -595,7 +594,7 @@ export const componentSlugs: Record<string, string> = {
595
594
  Spinner: "feedback/spinner",
596
595
  "File Icon": "chat/file-icon",
597
596
  "Attachment Thumb": "chat/attachment-thumb",
598
- "Attachment Chip": "chat/attachment-chip",
597
+ "Attachment Item": "data/attachment-item",
599
598
  "Quotation Bar": "chat/quotation-bar",
600
599
  "Chat Input": "chat/chat-input",
601
600
  "Bubble": "chat/bubble",
@@ -607,14 +606,13 @@ export const componentSlugs: Record<string, string> = {
607
606
  "Message Sources": "chat/message-sources",
608
607
  "Message Table": "chat/message-table",
609
608
  "Message State": "chat/message-state",
610
- "Message Attachment": "chat/message-attachment",
609
+ "Generated Image": "chat/generated-image",
611
610
  };
612
611
 
613
- /** 작업중 — 사이드바에서 접근은 가능하지만 "작업중" 뱃지로 표시해 사용 자제 유도. */
614
- export const componentsInProgress: Set<string> = new Set([
615
- "Drawer",
616
- "Calendar",
617
- ]);
612
+ /** 작업중 — 사이드바에서 "작업중" 뱃지로 표시되고,
613
+ * 외부에서 import하면 pre-commit 훅(scripts/lint-wip.mjs)이 차단한다.
614
+ * 문서 라우트(web/app/components/**)와 컴포넌트 자신의 폴더는 허용. */
615
+ export const componentsInProgress: Set<string> = new Set([]);
618
616
 
619
617
  export const components: { id: string; title: string; items: string[] }[] = [
620
618
  {
@@ -623,6 +621,7 @@ export const components: { id: string; title: string; items: string[] }[] = [
623
621
  items: [
624
622
  "Button", "Split Button", "Icon Button", "Menu Item", "Toolbar", "Badge", "Avatar",
625
623
  "Tooltip", "Shortcut", "Filter Button", "Chip", "Separator", "Skeleton", "Aspect Ratio",
624
+ "Code Block",
626
625
  ],
627
626
  },
628
627
  {
@@ -632,7 +631,7 @@ export const components: { id: string; title: string; items: string[] }[] = [
632
631
  "Input", "Input OTP", "Textarea", "Select", "Combobox", "Multi-Select",
633
632
  "Checkbox", "Radio Group", "Selectable Card", "Choice Card", "Switch", "Slider",
634
633
  "Date Picker", "Time Picker",
635
- "File Upload", "Form",
634
+ "File Upload",
636
635
  ],
637
636
  },
638
637
  {
@@ -640,7 +639,7 @@ export const components: { id: string; title: string; items: string[] }[] = [
640
639
  title: "Data Display",
641
640
  items: [
642
641
  "Table Header", "Table Row", "Table Row Select", "Table Row Edit",
643
- "List Item", "Metric Card", "Gallery Item", "Calendar", "Event Chip",
642
+ "List Item", "Metric Card", "Gallery Item", "Attachment Item", "Event Chip",
644
643
  "Empty State", "Description List", "Timeline",
645
644
  ],
646
645
  },
@@ -669,13 +668,37 @@ export const components: { id: string; title: string; items: string[] }[] = [
669
668
  ],
670
669
  },
671
670
  {
672
- id: "chat",
673
- title: "AI Chat",
671
+ id: "chat-input",
672
+ title: "AI Chat / 입력",
673
+ items: [
674
+ "Chat Input", "Quotation Bar",
675
+ ],
676
+ },
677
+ {
678
+ id: "chat-attachment",
679
+ title: "AI Chat / 첨부",
680
+ items: [
681
+ "File Icon", "Attachment Thumb", "Generated Image",
682
+ ],
683
+ },
684
+ {
685
+ id: "chat-message",
686
+ title: "AI Chat / 메시지",
687
+ items: [
688
+ "Bubble", "User Message", "AI Message", "Message State", "Message Actions",
689
+ ],
690
+ },
691
+ {
692
+ id: "chat-content",
693
+ title: "AI Chat / 메시지 콘텐츠",
694
+ items: [
695
+ "Message Quote", "Message Sources", "Message Table",
696
+ ],
697
+ },
698
+ {
699
+ id: "chat-shell",
700
+ title: "AI Chat / 쉘",
674
701
  items: [
675
- "File Icon", "Attachment Thumb", "Attachment Chip", "Quotation Bar", "Chat Input",
676
- "Bubble", "Message Actions", "User Message", "AI Message",
677
- "Code Block", "Message Quote", "Message Sources", "Message Table", "Message State",
678
- "Message Attachment",
679
702
  "Chat Sidebar",
680
703
  ],
681
704
  },
@@ -0,0 +1,17 @@
1
+ /* ============================================================
2
+ CTDS app-shell — opt-in. AppFrame(100dvh overflow:hidden)을 쓰는
3
+ "앱 셸" 레이아웃 전제 스타일. 소비자 프로젝트가 일반 body-scroll
4
+ 페이지라면 이 파일을 import하지 않는다. 데모 앱(web/app/globals.css)
5
+ 과 ctds-cli init이 복사하는 앱 템플릿만 사용.
6
+
7
+ app-scroll 패러다임: AppFrame이 뷰포트 100dvh를 꽉 채우고 자기 overflow:hidden,
8
+ 스크롤은 AppFrame 안의 .contentSlot만 담당 — html/body는 스크롤할 필요가 없다.
9
+ overflow:hidden으로 바깥 러버밴드/스크롤 체이닝 자체를 차단.
10
+ ============================================================ */
11
+
12
+ html,
13
+ body {
14
+ height: 100%;
15
+ overflow: hidden;
16
+ overscroll-behavior: none;
17
+ }