@conscience-technology/ctds 0.0.0-stage → 0.1.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 (289) hide show
  1. package/README.md +128 -2
  2. package/assets/components/AppFrame/AppFrame.module.css +177 -0
  3. package/assets/components/AppFrame/AppFrame.tsx +93 -0
  4. package/assets/components/AppFrame/index.ts +6 -0
  5. package/assets/components/AttachmentChip/AttachmentChip.module.css +71 -0
  6. package/assets/components/AttachmentChip/AttachmentChip.tsx +74 -0
  7. package/assets/components/AttachmentChip/index.ts +2 -0
  8. package/assets/components/AttachmentThumb/AttachmentThumb.module.css +107 -0
  9. package/assets/components/AttachmentThumb/AttachmentThumb.tsx +91 -0
  10. package/assets/components/AttachmentThumb/index.ts +2 -0
  11. package/assets/components/Avatar/Avatar.module.css +167 -0
  12. package/assets/components/Avatar/Avatar.tsx +228 -0
  13. package/assets/components/Avatar/index.ts +2 -0
  14. package/assets/components/AvatarUpload/AvatarUpload.module.css +130 -0
  15. package/assets/components/AvatarUpload/AvatarUpload.tsx +139 -0
  16. package/assets/components/AvatarUpload/index.ts +2 -0
  17. package/assets/components/Badge/Badge.module.css +76 -0
  18. package/assets/components/Badge/Badge.tsx +68 -0
  19. package/assets/components/Badge/index.ts +2 -0
  20. package/assets/components/Breadcrumb/Breadcrumb.module.css +213 -0
  21. package/assets/components/Breadcrumb/Breadcrumb.tsx +279 -0
  22. package/assets/components/Breadcrumb/index.ts +8 -0
  23. package/assets/components/Button/Button.module.css +216 -0
  24. package/assets/components/Button/Button.tsx +149 -0
  25. package/assets/components/Button/index.ts +2 -0
  26. package/assets/components/Callout/Callout.module.css +146 -0
  27. package/assets/components/Callout/Callout.tsx +138 -0
  28. package/assets/components/Callout/index.ts +2 -0
  29. package/assets/components/ChatInput/ChatInput.module.css +241 -0
  30. package/assets/components/ChatInput/ChatInput.tsx +322 -0
  31. package/assets/components/ChatInput/ModelSelector.tsx +40 -0
  32. package/assets/components/ChatInput/SendButton.tsx +41 -0
  33. package/assets/components/ChatInput/index.ts +10 -0
  34. package/assets/components/Checkbox/Checkbox.module.css +66 -0
  35. package/assets/components/Checkbox/Checkbox.tsx +36 -0
  36. package/assets/components/Checkbox/index.ts +2 -0
  37. package/assets/components/Chip/Chip.module.css +128 -0
  38. package/assets/components/Chip/Chip.tsx +103 -0
  39. package/assets/components/Chip/index.ts +2 -0
  40. package/assets/components/ChoiceCard/ChoiceCard.module.css +142 -0
  41. package/assets/components/ChoiceCard/ChoiceCard.tsx +132 -0
  42. package/assets/components/ChoiceCard/index.ts +2 -0
  43. package/assets/components/ContextMenu/ContextMenu.module.css +102 -0
  44. package/assets/components/ContextMenu/ContextMenu.tsx +392 -0
  45. package/assets/components/ContextMenu/index.ts +9 -0
  46. package/assets/components/DatePicker/Calendar.tsx +234 -0
  47. package/assets/components/DatePicker/DESIGN_NOTES.md +106 -0
  48. package/assets/components/DatePicker/DESIGN_NOTES_TIME.md +125 -0
  49. package/assets/components/DatePicker/DatePicker.module.css +295 -0
  50. package/assets/components/DatePicker/DatePicker.tsx +387 -0
  51. package/assets/components/DatePicker/TimeSelector.tsx +69 -0
  52. package/assets/components/DatePicker/index.ts +8 -0
  53. package/assets/components/DatePicker/presets.ts +80 -0
  54. package/assets/components/DatePicker/utils.ts +148 -0
  55. package/assets/components/Disclosure/Disclosure.module.css +73 -0
  56. package/assets/components/Disclosure/Disclosure.tsx +129 -0
  57. package/assets/components/Disclosure/index.ts +6 -0
  58. package/assets/components/Drawer/Drawer.module.css +257 -0
  59. package/assets/components/Drawer/Drawer.tsx +126 -0
  60. package/assets/components/Drawer/index.ts +7 -0
  61. package/assets/components/EmptyState/EmptyState.module.css +102 -0
  62. package/assets/components/EmptyState/EmptyState.tsx +80 -0
  63. package/assets/components/EmptyState/index.ts +2 -0
  64. package/assets/components/FileIcon/FileIcon.module.css +66 -0
  65. package/assets/components/FileIcon/FileIcon.tsx +84 -0
  66. package/assets/components/FileIcon/fileExtMap.ts +112 -0
  67. package/assets/components/FileIcon/index.ts +9 -0
  68. package/assets/components/FilterButton/FilterButton.module.css +130 -0
  69. package/assets/components/FilterButton/FilterButton.tsx +94 -0
  70. package/assets/components/FilterButton/index.ts +2 -0
  71. package/assets/components/GalleryItem/GalleryItem.module.css +220 -0
  72. package/assets/components/GalleryItem/GalleryItem.tsx +163 -0
  73. package/assets/components/GalleryItem/index.ts +7 -0
  74. package/assets/components/Icon/Icon.tsx +616 -0
  75. package/assets/components/Icon/context.tsx +39 -0
  76. package/assets/components/Icon/index.ts +4 -0
  77. package/assets/components/IconButton/IconButton.module.css +109 -0
  78. package/assets/components/IconButton/IconButton.tsx +110 -0
  79. package/assets/components/IconButton/index.ts +2 -0
  80. package/assets/components/Input/Input.module.css +338 -0
  81. package/assets/components/Input/Input.tsx +371 -0
  82. package/assets/components/Input/index.ts +2 -0
  83. package/assets/components/ListItem/ListItem.module.css +318 -0
  84. package/assets/components/ListItem/ListItem.tsx +262 -0
  85. package/assets/components/ListItem/index.ts +9 -0
  86. package/assets/components/MenuItem/MenuItem.module.css +437 -0
  87. package/assets/components/MenuItem/MenuItem.tsx +529 -0
  88. package/assets/components/MenuItem/index.ts +8 -0
  89. package/assets/components/MetricCard/MetricCard.module.css +153 -0
  90. package/assets/components/MetricCard/MetricCard.tsx +140 -0
  91. package/assets/components/MetricCard/index.ts +2 -0
  92. package/assets/components/Modal/Modal.module.css +175 -0
  93. package/assets/components/Modal/Modal.tsx +98 -0
  94. package/assets/components/Modal/index.ts +2 -0
  95. package/assets/components/Onboarding/DocsIndex.tsx +82 -0
  96. package/assets/components/Onboarding/LayoutBuilder.tsx +160 -0
  97. package/assets/components/Onboarding/Onboarding.module.css +1394 -0
  98. package/assets/components/Onboarding/OnboardingAxisPanel.tsx +515 -0
  99. package/assets/components/Onboarding/OnboardingPage.tsx +1193 -0
  100. package/assets/components/Onboarding/PresetsGallery.tsx +65 -0
  101. package/assets/components/Onboarding/actions.ts +250 -0
  102. package/assets/components/Onboarding/index.ts +8 -0
  103. package/assets/components/Onboarding/palette.css +40 -0
  104. package/assets/components/Onboarding/wizardData.ts +265 -0
  105. package/assets/components/Pagination/Pagination.module.css +82 -0
  106. package/assets/components/Pagination/Pagination.tsx +248 -0
  107. package/assets/components/Pagination/index.ts +6 -0
  108. package/assets/components/ProgressBar/ProgressBar.module.css +64 -0
  109. package/assets/components/ProgressBar/ProgressBar.tsx +54 -0
  110. package/assets/components/ProgressBar/index.ts +2 -0
  111. package/assets/components/QuotationBar/QuotationBar.module.css +61 -0
  112. package/assets/components/QuotationBar/QuotationBar.tsx +48 -0
  113. package/assets/components/QuotationBar/index.ts +2 -0
  114. package/assets/components/Radio/Radio.module.css +63 -0
  115. package/assets/components/Radio/Radio.tsx +33 -0
  116. package/assets/components/Radio/index.ts +2 -0
  117. package/assets/components/Select/Select.module.css +151 -0
  118. package/assets/components/Select/Select.tsx +425 -0
  119. package/assets/components/Select/index.ts +10 -0
  120. package/assets/components/SelectableCard/SelectableCard.module.css +177 -0
  121. package/assets/components/SelectableCard/SelectableCard.tsx +83 -0
  122. package/assets/components/SelectableCard/index.ts +2 -0
  123. package/assets/components/Shortcut/Shortcut.module.css +56 -0
  124. package/assets/components/Shortcut/Shortcut.tsx +156 -0
  125. package/assets/components/Shortcut/index.ts +2 -0
  126. package/assets/components/Sidebar/Sidebar.module.css +242 -0
  127. package/assets/components/Sidebar/Sidebar.tsx +264 -0
  128. package/assets/components/Sidebar/index.ts +7 -0
  129. package/assets/components/SidebarSplit/SidebarSplit.module.css +126 -0
  130. package/assets/components/SidebarSplit/SidebarSplit.tsx +222 -0
  131. package/assets/components/SidebarSplit/index.ts +6 -0
  132. package/assets/components/Slider/Slider.module.css +120 -0
  133. package/assets/components/Slider/Slider.tsx +88 -0
  134. package/assets/components/Slider/index.ts +2 -0
  135. package/assets/components/SplitButton/SplitButton.module.css +190 -0
  136. package/assets/components/SplitButton/SplitButton.tsx +145 -0
  137. package/assets/components/SplitButton/index.ts +6 -0
  138. package/assets/components/Stepper/Stepper.module.css +182 -0
  139. package/assets/components/Stepper/Stepper.tsx +185 -0
  140. package/assets/components/Stepper/index.ts +2 -0
  141. package/assets/components/Switch/Switch.module.css +116 -0
  142. package/assets/components/Switch/Switch.tsx +58 -0
  143. package/assets/components/Switch/index.ts +2 -0
  144. package/assets/components/TableCell/TableCell.module.css +137 -0
  145. package/assets/components/TableCell/TableCellAvatar.tsx +42 -0
  146. package/assets/components/TableCell/TableCellBadge.tsx +23 -0
  147. package/assets/components/TableCell/TableCellIcon.tsx +39 -0
  148. package/assets/components/TableCell/TableCellImage.tsx +49 -0
  149. package/assets/components/TableCell/TableCellLink.tsx +33 -0
  150. package/assets/components/TableCell/TableCellMeter.tsx +55 -0
  151. package/assets/components/TableCell/TableCellNumber.tsx +43 -0
  152. package/assets/components/TableCell/TableCellText.tsx +30 -0
  153. package/assets/components/TableCell/index.ts +23 -0
  154. package/assets/components/TableHeader/TableHeader.module.css +287 -0
  155. package/assets/components/TableHeader/TableHeader.tsx +590 -0
  156. package/assets/components/TableHeader/index.ts +9 -0
  157. package/assets/components/TableRow/TableRow.module.css +290 -0
  158. package/assets/components/TableRow/TableRow.tsx +253 -0
  159. package/assets/components/TableRow/index.ts +8 -0
  160. package/assets/components/TableRowEdit/TableRowEdit.module.css +126 -0
  161. package/assets/components/TableRowEdit/TableRowEdit.tsx +247 -0
  162. package/assets/components/TableRowEdit/index.ts +2 -0
  163. package/assets/components/TableRowSelect/TableRowSelect.module.css +23 -0
  164. package/assets/components/TableRowSelect/TableRowSelect.tsx +175 -0
  165. package/assets/components/TableRowSelect/index.ts +2 -0
  166. package/assets/components/Tabs/Tabs.module.css +234 -0
  167. package/assets/components/Tabs/Tabs.tsx +426 -0
  168. package/assets/components/Tabs/index.ts +9 -0
  169. package/assets/components/Textarea/Textarea.module.css +318 -0
  170. package/assets/components/Textarea/Textarea.tsx +426 -0
  171. package/assets/components/Textarea/index.ts +7 -0
  172. package/assets/components/ThemeController.tsx +271 -0
  173. package/assets/components/TimePicker/TimePicker.module.css +317 -0
  174. package/assets/components/TimePicker/TimePicker.tsx +821 -0
  175. package/assets/components/TimePicker/index.ts +8 -0
  176. package/assets/components/TimePicker/utils.ts +115 -0
  177. package/assets/components/Toast/Toast.module.css +264 -0
  178. package/assets/components/Toast/Toast.tsx +243 -0
  179. package/assets/components/Toast/index.ts +15 -0
  180. package/assets/components/Toolbar/Toolbar.module.css +72 -0
  181. package/assets/components/Toolbar/Toolbar.tsx +75 -0
  182. package/assets/components/Toolbar/index.ts +2 -0
  183. package/assets/components/Tooltip/Tooltip.module.css +42 -0
  184. package/assets/components/Tooltip/Tooltip.tsx +332 -0
  185. package/assets/components/Tooltip/index.ts +2 -0
  186. package/assets/components/Topbar/Topbar.module.css +26 -0
  187. package/assets/components/Topbar/Topbar.tsx +47 -0
  188. package/assets/components/Topbar/index.ts +2 -0
  189. package/assets/components/_cn.ts +3 -0
  190. package/assets/components/_color.ts +48 -0
  191. package/assets/components/_data.ts +659 -0
  192. package/assets/components/_utils.css +19 -0
  193. package/assets/components/axisOptions.ts +24 -0
  194. package/assets/components/tokens.css +742 -0
  195. package/assets/prompts/GUIDE.md +251 -0
  196. package/assets/prompts/OVERRIDE.md +170 -0
  197. package/assets/prompts/PRINCIPLES.md +130 -0
  198. package/assets/prompts/axes/density.md +38 -0
  199. package/assets/prompts/axes/font.md +36 -0
  200. package/assets/prompts/axes/hue.md +36 -0
  201. package/assets/prompts/axes/icon.md +48 -0
  202. package/assets/prompts/axes/mode.md +50 -0
  203. package/assets/prompts/axes/motion.md +77 -0
  204. package/assets/prompts/axes/palette.md +35 -0
  205. package/assets/prompts/axes/shape.md +33 -0
  206. package/assets/prompts/axes/voice.md +54 -0
  207. package/assets/prompts/components/README.md +84 -0
  208. package/assets/prompts/components/app-frame.md +94 -0
  209. package/assets/prompts/components/attachment-chip.md +46 -0
  210. package/assets/prompts/components/attachment-thumb.md +49 -0
  211. package/assets/prompts/components/avatar.md +86 -0
  212. package/assets/prompts/components/badge.md +104 -0
  213. package/assets/prompts/components/breadcrumb.md +77 -0
  214. package/assets/prompts/components/button.md +99 -0
  215. package/assets/prompts/components/callout.md +81 -0
  216. package/assets/prompts/components/chat-input.md +84 -0
  217. package/assets/prompts/components/checkbox.md +46 -0
  218. package/assets/prompts/components/chip.md +84 -0
  219. package/assets/prompts/components/choice-card.md +127 -0
  220. package/assets/prompts/components/context-menu.md +145 -0
  221. package/assets/prompts/components/date-picker.md +104 -0
  222. package/assets/prompts/components/drawer.md +152 -0
  223. package/assets/prompts/components/empty-state.md +69 -0
  224. package/assets/prompts/components/file-icon.md +73 -0
  225. package/assets/prompts/components/filter-button.md +42 -0
  226. package/assets/prompts/components/form-field.md +74 -0
  227. package/assets/prompts/components/gallery-item.md +158 -0
  228. package/assets/prompts/components/icon-button.md +82 -0
  229. package/assets/prompts/components/icon.md +68 -0
  230. package/assets/prompts/components/input.md +95 -0
  231. package/assets/prompts/components/list-item.md +235 -0
  232. package/assets/prompts/components/menu-item.md +234 -0
  233. package/assets/prompts/components/metric-card.md +132 -0
  234. package/assets/prompts/components/modal.md +131 -0
  235. package/assets/prompts/components/pagination.md +92 -0
  236. package/assets/prompts/components/progress-bar.md +61 -0
  237. package/assets/prompts/components/quotation-bar.md +39 -0
  238. package/assets/prompts/components/radio.md +41 -0
  239. package/assets/prompts/components/select.md +104 -0
  240. package/assets/prompts/components/selectable-card.md +67 -0
  241. package/assets/prompts/components/shortcut.md +76 -0
  242. package/assets/prompts/components/sidebar.md +107 -0
  243. package/assets/prompts/components/slider.md +89 -0
  244. package/assets/prompts/components/split-button.md +71 -0
  245. package/assets/prompts/components/stepper.md +93 -0
  246. package/assets/prompts/components/switch.md +86 -0
  247. package/assets/prompts/components/table-cell.md +87 -0
  248. package/assets/prompts/components/table-header.md +157 -0
  249. package/assets/prompts/components/table-row-edit.md +129 -0
  250. package/assets/prompts/components/table-row-select.md +82 -0
  251. package/assets/prompts/components/table-row.md +156 -0
  252. package/assets/prompts/components/tabs.md +122 -0
  253. package/assets/prompts/components/textarea.md +105 -0
  254. package/assets/prompts/components/toast.md +149 -0
  255. package/assets/prompts/components/tooltip.md +71 -0
  256. package/assets/prompts/components/topbar.md +148 -0
  257. package/assets/prompts/primitives/emerge.md +43 -0
  258. package/assets/prompts/primitives/fade.md +31 -0
  259. package/assets/prompts/primitives/scale.md +32 -0
  260. package/assets/prompts/primitives/scroll.md +75 -0
  261. package/assets/prompts/primitives/slide.md +31 -0
  262. package/assets/prompts/themes/_SCHEMA.md +135 -0
  263. package/assets/prompts/themes/_TEMPLATE.md +73 -0
  264. package/assets/prompts/themes/expressive.md +118 -0
  265. package/assets/prompts/themes/pure.md +87 -0
  266. package/assets/prompts/themes/spare.md +73 -0
  267. package/assets/prompts/tokens/breakpoint.md +29 -0
  268. package/assets/prompts/tokens/color.md +78 -0
  269. package/assets/prompts/tokens/radius.md +33 -0
  270. package/assets/prompts/tokens/shadow.md +35 -0
  271. package/assets/prompts/tokens/spacing.md +86 -0
  272. package/assets/prompts/tokens/typography.md +76 -0
  273. package/assets/prompts/tokens/z-index.md +35 -0
  274. package/assets/public/expressive.png +0 -0
  275. package/assets/public/pure.png +0 -0
  276. package/assets/public/spare.png +0 -0
  277. package/assets/skill/SKILL.md +36 -0
  278. package/bin/ctds.js +76 -0
  279. package/package.json +35 -4
  280. package/src/commands/init.js +215 -0
  281. package/src/commands/update.js +275 -0
  282. package/src/utils/assets.js +78 -0
  283. package/src/utils/deps.js +51 -0
  284. package/src/utils/detect.js +47 -0
  285. package/src/utils/diff.js +52 -0
  286. package/src/utils/hash.js +16 -0
  287. package/src/utils/log.js +41 -0
  288. package/src/utils/meta.js +73 -0
  289. package/src/utils/prompt.js +28 -0
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
4
+
5
+ import { Icon, type IconName } from "../Icon";
6
+
7
+ import styles from "./EmptyState.module.css";
8
+
9
+ /**
10
+ * EmptyState — 리스트·테이블·대시보드 영역이 비었을 때 그 영역 전체를 채우는 자리채움.
11
+ * 세로 중앙정렬: 시각 요소(illustration ∨ icon) → 타이틀 → 설명 → 액션.
12
+ *
13
+ * 배치 규칙:
14
+ * - size는 **컨테이너 폭/영역 크기**에 맞춰 선택 (sm/md/lg)
15
+ * - illustration이 있으면 icon은 무시 (병치 금지)
16
+ * - 액션은 Button slot. primary 1 + ghost 1 패턴, destructive 금지
17
+ */
18
+ export type EmptyStateSize = "sm" | "md";
19
+
20
+ export interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
21
+ size?: EmptyStateSize;
22
+ /** 큰 3D 일러스트 등 ReactNode. 지정되면 icon보다 우선. */
23
+ illustration?: ReactNode;
24
+ /** illustration이 없을 때만 렌더되는 Icon slot. */
25
+ icon?: IconName;
26
+ title: ReactNode;
27
+ description?: ReactNode;
28
+ /** Button 1~2개. primary 1 + ghost 1 패턴 권장. */
29
+ actions?: ReactNode;
30
+ }
31
+
32
+ const sizeClass: Record<EmptyStateSize, string> = {
33
+ sm: styles.sizeSm,
34
+ md: styles.sizeMd,
35
+ };
36
+
37
+ const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
38
+ (
39
+ {
40
+ className,
41
+ size = "sm",
42
+ illustration,
43
+ icon,
44
+ title,
45
+ description,
46
+ actions,
47
+ ...rest
48
+ },
49
+ ref,
50
+ ) => {
51
+ const rootClass = [styles.root, sizeClass[size], className ?? ""]
52
+ .filter(Boolean)
53
+ .join(" ");
54
+
55
+ const visual = illustration ? (
56
+ <div className={styles.illustration} aria-hidden>
57
+ {illustration}
58
+ </div>
59
+ ) : icon ? (
60
+ <span className={styles.icon} aria-hidden>
61
+ <Icon name={icon} />
62
+ </span>
63
+ ) : null;
64
+
65
+ return (
66
+ <div ref={ref} className={rootClass} {...rest}>
67
+ {visual}
68
+ <div className={styles.text}>
69
+ <div className={styles.title}>{title}</div>
70
+ {description && <div className={styles.description}>{description}</div>}
71
+ </div>
72
+ {actions && <div className={styles.actions}>{actions}</div>}
73
+ </div>
74
+ );
75
+ },
76
+ );
77
+
78
+ EmptyState.displayName = "EmptyState";
79
+
80
+ export { EmptyState };
@@ -0,0 +1,2 @@
1
+ export { EmptyState } from "./EmptyState";
2
+ export type { EmptyStateProps, EmptyStateSize } from "./EmptyState";
@@ -0,0 +1,66 @@
1
+ /* FileIcon — 확장자별 색/라벨/아이콘을 담는 둥근 사각 뱃지.
2
+ 비율:
3
+ radius: Shape 축 반응. sm(16px)만 --radius-checkbox로 캡해 원 방지.
4
+ md(24) → --radius-xs, lg(32) → --radius-sm
5
+ label font / size = 0.53 — semibold
6
+ glyph / size = 0.60 — 브랜드 아이콘 자산 가독성
7
+ 사이즈 리터럴은 FileIcon 고유 치수라 전역 토큰 없음 — 라인별 ignore. */
8
+
9
+ .root {
10
+ --fi-size: 24px; /* lint-hardcoded: ignore */
11
+ --fi-radius: var(--radius-xs);
12
+
13
+ display: inline-flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ width: var(--fi-size);
17
+ height: var(--fi-size);
18
+ border-radius: var(--fi-radius);
19
+ flex: 0 0 auto;
20
+ overflow: hidden;
21
+ user-select: none;
22
+ transition: var(--transition-colors);
23
+ }
24
+
25
+ /* 사이즈 — Shape 축 반응하되 soft 과도 방지로 한 단계 내려 사용.
26
+ sm/md: --radius-checkbox (soft에서 6px로 캡)
27
+ lg: --radius-xs */
28
+ .sizeSm {
29
+ --fi-size: 16px; /* lint-hardcoded: ignore */
30
+ --fi-radius: var(--radius-checkbox);
31
+ }
32
+ .sizeMd {
33
+ --fi-size: 24px; /* lint-hardcoded: ignore */
34
+ --fi-radius: var(--radius-checkbox);
35
+ }
36
+ .sizeLg {
37
+ --fi-size: 32px; /* lint-hardcoded: ignore */
38
+ --fi-radius: var(--radius-xs);
39
+ }
40
+
41
+ /* 브랜드 라벨 — "Ai"/"Ps"/"X"/… 2글자 */
42
+ .label {
43
+ font-size: calc(var(--fi-size) * 0.53);
44
+ font-weight: var(--font-weight-semibold);
45
+ line-height: 1;
46
+ font-family: var(--font-sans);
47
+ /* 2글자 뭉침 방지용 negative tracking. uppercase 아님 — 혼합 대소문자 원본 유지. */
48
+ letter-spacing: -0.02em; /* lint-hardcoded: ignore */
49
+ }
50
+
51
+ /* 제너릭 glyph (phosphor) — size × 0.60 */
52
+ .glyph {
53
+ width: calc(var(--fi-size) * 0.60);
54
+ height: calc(var(--fi-size) * 0.60);
55
+ }
56
+
57
+ /* 제너릭 L/D 자동 전환 — CSS 변수를 Mode 축에서 선택 */
58
+ .generic {
59
+ background: var(--fi-bg-light);
60
+ color: var(--fi-fg-light);
61
+ }
62
+
63
+ :global([data-mode="dark"]) .generic {
64
+ background: var(--fi-bg-dark);
65
+ color: var(--fi-fg-dark);
66
+ }
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import type { HTMLAttributes } from "react";
4
+ import {
5
+ File as PhFile,
6
+ FileZip as PhFileZip,
7
+ MusicNote as PhMusicNote,
8
+ } from "@phosphor-icons/react";
9
+ import styles from "./FileIcon.module.css";
10
+ import { resolveFileIcon } from "./fileExtMap";
11
+
12
+ export type FileIconSize = "sm" | "md" | "lg";
13
+
14
+ export interface FileIconProps extends HTMLAttributes<HTMLSpanElement> {
15
+ /** 확장자 또는 파일명. 'psd' / '.psd' / 'report.psd' 다 허용. */
16
+ ext: string;
17
+ /** 16 / 24 / 32. 기본 md=24. */
18
+ size?: FileIconSize;
19
+ }
20
+
21
+ const sizeClass: Record<FileIconSize, string> = {
22
+ sm: styles.sizeSm,
23
+ md: styles.sizeMd,
24
+ lg: styles.sizeLg,
25
+ };
26
+
27
+ const PHOSPHOR = {
28
+ file: PhFile,
29
+ "file-zip": PhFileZip,
30
+ "music-note": PhMusicNote,
31
+ } as const;
32
+
33
+ export function FileIcon({
34
+ ext,
35
+ size = "md",
36
+ className,
37
+ style,
38
+ ...rest
39
+ }: FileIconProps) {
40
+ const spec = resolveFileIcon(ext);
41
+
42
+ const root = [styles.root, sizeClass[size], className]
43
+ .filter(Boolean)
44
+ .join(" ");
45
+
46
+ if (spec.kind === "brand") {
47
+ return (
48
+ <span
49
+ className={root}
50
+ style={{
51
+ background: spec.bg,
52
+ color: spec.fg,
53
+ ...style,
54
+ }}
55
+ data-kind="brand"
56
+ aria-hidden
57
+ {...rest}
58
+ >
59
+ <span className={styles.label}>{spec.label}</span>
60
+ </span>
61
+ );
62
+ }
63
+
64
+ // 제너릭 — L/D pair. CSS 변수로 내보내서 data-mode에 CSS가 반응.
65
+ const Ph = PHOSPHOR[spec.icon];
66
+ const vars = {
67
+ "--fi-bg-light": spec.light.bg,
68
+ "--fi-fg-light": spec.light.fg,
69
+ "--fi-bg-dark": spec.dark.bg,
70
+ "--fi-fg-dark": spec.dark.fg,
71
+ } as React.CSSProperties;
72
+
73
+ return (
74
+ <span
75
+ className={`${root} ${styles.generic}`}
76
+ style={{ ...vars, ...style }}
77
+ data-kind="generic"
78
+ aria-hidden
79
+ {...rest}
80
+ >
81
+ <Ph className={styles.glyph} weight="regular" />
82
+ </span>
83
+ );
84
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * 파일 확장자 → FileIcon 렌더 정보 매핑.
3
+ *
4
+ * 두 종류 — "브랜드"와 "제너릭".
5
+ * - 브랜드(ai/psd/xls/ppt/doc/pdf): 단일 색 + 2글자 라벨. Mode 축 무관하게 고정.
6
+ * - 제너릭(others/zip/mp3): L/D 두 색 pair + 아이콘. Mode 축에 자동 반응.
7
+ *
8
+ * 색은 레퍼런스 디자인(.refs/chat/file-icon-ref.png)에서 직접 추출한 hex.
9
+ * CTDS 하드코딩 금지 원칙의 명시적 예외 — 파일 포맷 식별은 브랜드 자산 성격이라
10
+ * 토큰으로 추상화하면 의미를 잃는다. [[feedback-no-hardcoded-values]]의 예외
11
+ * 범주이며, lint baseline에 등록해 통과시킨다.
12
+ */
13
+
14
+ export type FileIconKind = "brand" | "generic";
15
+
16
+ /** 브랜드형 — bg·fg 고정, 라벨은 2글자. */
17
+ export interface BrandSpec {
18
+ kind: "brand";
19
+ bg: string;
20
+ fg: string;
21
+ label: string;
22
+ }
23
+
24
+ /** 제너릭형 — L/D pair, phosphor 아이콘. */
25
+ export interface GenericSpec {
26
+ kind: "generic";
27
+ light: { bg: string; fg: string };
28
+ dark: { bg: string; fg: string };
29
+ icon: "file" | "file-zip" | "music-note";
30
+ }
31
+
32
+ export type FileIconSpec = BrandSpec | GenericSpec;
33
+
34
+ /** 제너릭 키들 (ext → group) */
35
+ const GENERIC_GROUP: Record<string, "zip" | "mp3" | "others"> = {
36
+ zip: "zip",
37
+ rar: "zip",
38
+ "7z": "zip",
39
+ tar: "zip",
40
+ gz: "zip",
41
+
42
+ mp3: "mp3",
43
+ wav: "mp3",
44
+ flac: "mp3",
45
+ m4a: "mp3",
46
+ aac: "mp3",
47
+ ogg: "mp3",
48
+ };
49
+
50
+ /** 그룹별 제너릭 스펙 */
51
+ const GENERIC_SPEC: Record<"zip" | "mp3" | "others", GenericSpec> = {
52
+ others: {
53
+ kind: "generic",
54
+ light: { bg: "#EDEAB0", fg: "#B2AA54" },
55
+ dark: { bg: "#363229", fg: "#A19B5A" },
56
+ icon: "file",
57
+ },
58
+ zip: {
59
+ kind: "generic",
60
+ light: { bg: "#EDEAB0", fg: "#B2AA54" },
61
+ dark: { bg: "#363229", fg: "#A19B59" },
62
+ icon: "file-zip",
63
+ },
64
+ mp3: {
65
+ kind: "generic",
66
+ light: { bg: "#BEFDA8", fg: "#4B9C2D" },
67
+ dark: { bg: "#254924", fg: "#BDFCA8" },
68
+ icon: "music-note",
69
+ },
70
+ };
71
+
72
+ /** 브랜드 파일 — 확장자별 단일 스펙 */
73
+ const BRAND_SPEC: Record<string, BrandSpec> = {
74
+ ai: { kind: "brand", bg: "#2E0402", fg: "#F09E38", label: "Ai" },
75
+ psd: { kind: "brand", bg: "#081D34", fg: "#57A5F8", label: "Ps" },
76
+ xls: { kind: "brand", bg: "#3B804C", fg: "#FFFFFF", label: "X" },
77
+ xlsx: { kind: "brand", bg: "#3B804C", fg: "#FFFFFF", label: "X" },
78
+ ppt: { kind: "brand", bg: "#B84C2C", fg: "#FFFFFF", label: "P" },
79
+ pptx: { kind: "brand", bg: "#B84C2C", fg: "#FFFFFF", label: "P" },
80
+ doc: { kind: "brand", bg: "#3044C7", fg: "#FFFFFF", label: "W" },
81
+ docx: { kind: "brand", bg: "#3044C7", fg: "#FFFFFF", label: "W" },
82
+ pdf: { kind: "brand", bg: "#EA336C", fg: "#FFFFFF", label: "Pd" },
83
+ };
84
+
85
+ /**
86
+ * 확장자 정규화 — 선행 점·대문자 모두 허용.
87
+ * 공백·null 안전.
88
+ */
89
+ export function normalizeExt(ext: string | null | undefined): string {
90
+ if (!ext) return "";
91
+ return ext.trim().replace(/^\./, "").toLowerCase();
92
+ }
93
+
94
+ /** 파일명(또는 확장자)에서 FileIcon 스펙 조회. */
95
+ export function resolveFileIcon(input: string): FileIconSpec {
96
+ const ext = normalizeExt(
97
+ input.includes(".") ? input.split(".").pop() ?? "" : input
98
+ );
99
+
100
+ if (ext in BRAND_SPEC) return BRAND_SPEC[ext];
101
+
102
+ const group = GENERIC_GROUP[ext] ?? "others";
103
+ return GENERIC_SPEC[group];
104
+ }
105
+
106
+ /** 데모·테스트에서 전체 리스트 돌 때 쓰려고 export. */
107
+ export const KNOWN_BRAND_EXTS = Object.keys(BRAND_SPEC);
108
+ export const KNOWN_GENERIC_GROUPS = Object.keys(GENERIC_SPEC) as (
109
+ | "others"
110
+ | "zip"
111
+ | "mp3"
112
+ )[];
@@ -0,0 +1,9 @@
1
+ export { FileIcon } from "./FileIcon";
2
+ export type { FileIconProps, FileIconSize } from "./FileIcon";
3
+ export {
4
+ resolveFileIcon,
5
+ normalizeExt,
6
+ KNOWN_BRAND_EXTS,
7
+ KNOWN_GENERIC_GROUPS,
8
+ } from "./fileExtMap";
9
+ export type { FileIconSpec, BrandSpec, GenericSpec } from "./fileExtMap";
@@ -0,0 +1,130 @@
1
+ /* FilterButton ────────────────────────────────────────────────────
2
+ 토글 상태를 가진 "필터 선택" 컨트롤.
3
+ 기본은 outline(회색 선 + fg-muted 텍스트), selected=true면 반전(fg 배경 + bg 텍스트).
4
+ Button과 시각적으로 형제이지만 시맨틱이 다름(액션 실행 vs 선택 상태 유지).
5
+ aria-pressed로 스크린리더에 토글 상태 전달. */
6
+ .root {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ gap: var(--space-1_5);
11
+ font-family: var(--label-font);
12
+ font-weight: var(--label-weight);
13
+ line-height: var(--lh-tight);
14
+ letter-spacing: var(--label-tracking);
15
+ white-space: nowrap;
16
+ cursor: pointer;
17
+ user-select: none;
18
+ background: transparent;
19
+ color: var(--fg);
20
+ border: 1px solid var(--border-strong);
21
+ /* selected 토글은 fg↔bg 반전이라 background/color를 보간하면 중간에 반드시
22
+ 저대비 구간(반투명 검정 위 회색 글자)을 지나 글자가 사라져 보임.
23
+ → 색은 즉시 스위칭, motion은 border/shadow/transform에만. */
24
+ transition:
25
+ border-color var(--duration-fast) var(--ease-out),
26
+ box-shadow var(--duration-fast) var(--ease-out),
27
+ transform var(--duration-base) var(--ease-out);
28
+ }
29
+
30
+ .root:focus-visible {
31
+ outline: none;
32
+ box-shadow: var(--ring-shadow);
33
+ }
34
+
35
+ .root:hover:not(:disabled) {
36
+ background: var(--hover);
37
+ color: var(--fg);
38
+ border-color: var(--fg-muted);
39
+ }
40
+
41
+ .root:active:not(:disabled) {
42
+ transform: translateY(var(--space-0_5));
43
+ }
44
+
45
+ .root:disabled {
46
+ cursor: not-allowed;
47
+ opacity: 0.5;
48
+ }
49
+
50
+ /* Selected — inverse 스타일 반전. Hue 축과 무관하게 흑백 대비로 강조.
51
+ 필터가 "적용 중"임을 팔레트와 상관없이 명확히 알림. */
52
+ .root[aria-pressed="true"] {
53
+ background: var(--fg);
54
+ color: var(--bg);
55
+ border-color: var(--fg);
56
+ }
57
+
58
+ /* selected hover는 unselected hover의 color/border까지 명시적으로 덮어써야 함 —
59
+ 빼먹으면 검정 배경 위 검정 글자로 대비가 무너짐. */
60
+ .root[aria-pressed="true"]:hover:not(:disabled) {
61
+ background: var(--fg-strong);
62
+ color: var(--bg);
63
+ border-color: var(--fg-strong);
64
+ }
65
+
66
+ /* Sizes — Button과 동일 스케일(control-h-*)로 나란히 놓아도 정렬 맞음.
67
+ Shape 축이 radius를 스왑. */
68
+ .sm {
69
+ height: var(--control-h-sm);
70
+ padding-inline: var(--space-2);
71
+ gap: var(--space-1);
72
+ font-size: var(--label-sm-size);
73
+ letter-spacing: var(--label-tracking);
74
+ border-radius: var(--radius-xs);
75
+ --chip-icon-size: var(--space-4);
76
+ }
77
+ .md {
78
+ height: var(--control-h-md);
79
+ padding-inline: var(--space-2_5);
80
+ gap: var(--space-1_5);
81
+ font-size: var(--label-md-size);
82
+ letter-spacing: var(--label-tracking);
83
+ border-radius: var(--radius-sm);
84
+ --chip-icon-size: var(--space-4);
85
+ }
86
+
87
+ /* Icon slot — Button과 동일 방식(정사각 슬롯, opacity로 톤 조절). */
88
+ .icon {
89
+ display: inline-flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ flex-shrink: 0;
93
+ width: var(--chip-icon-size);
94
+ height: var(--chip-icon-size);
95
+ opacity: 0.75;
96
+ }
97
+ .icon > svg {
98
+ width: 100%;
99
+ height: 100%;
100
+ display: block;
101
+ }
102
+ .root:hover:not(:disabled) .icon,
103
+ .root[aria-pressed="true"] .icon {
104
+ opacity: 1;
105
+ }
106
+
107
+ .label {
108
+ display: inline-flex;
109
+ align-items: center;
110
+ }
111
+
112
+ /* Count — 중립 그레이 pill. selected 시 반전 바탕 위라 bg 배경으로 다시 떠올림. */
113
+ .count {
114
+ display: inline-flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ min-width: var(--space-4);
118
+ padding: 0 var(--space-1);
119
+ height: var(--space-4);
120
+ border-radius: var(--radius-xs);
121
+ background: var(--bg-subtle);
122
+ color: var(--fg-muted);
123
+ font-size: var(--text-xs);
124
+ font-variant-numeric: tabular-nums;
125
+ line-height: 1;
126
+ }
127
+ .root[aria-pressed="true"] .count {
128
+ background: var(--bg);
129
+ color: var(--fg-muted);
130
+ }
@@ -0,0 +1,94 @@
1
+ "use client";
2
+
3
+ import {
4
+ cloneElement,
5
+ forwardRef,
6
+ isValidElement,
7
+ type ButtonHTMLAttributes,
8
+ type ReactElement,
9
+ type ReactNode,
10
+ } from "react";
11
+
12
+ import styles from "./FilterButton.module.css";
13
+
14
+ export type FilterButtonSize = "sm" | "md";
15
+
16
+ export type FilterButtonProps = Omit<
17
+ ButtonHTMLAttributes<HTMLButtonElement>,
18
+ "size"
19
+ > & {
20
+ size?: FilterButtonSize;
21
+ selected?: boolean;
22
+ onToggle?: (next: boolean) => void;
23
+ leftIcon?: ReactNode;
24
+ count?: number;
25
+ };
26
+
27
+ const sizeClass: Record<FilterButtonSize, string> = {
28
+ sm: styles.sm,
29
+ md: styles.md,
30
+ };
31
+
32
+ const ICON_SIZE: Record<FilterButtonSize, number> = {
33
+ sm: 14,
34
+ md: 16,
35
+ };
36
+
37
+ function sizedIcon(node: ReactNode, size: number): ReactNode {
38
+ if (!isValidElement(node)) return node;
39
+ const el = node as ReactElement<{ size?: number | string }>;
40
+ if (el.props.size !== undefined) return el;
41
+ return cloneElement(el, { size });
42
+ }
43
+
44
+ const FilterButton = forwardRef<HTMLButtonElement, FilterButtonProps>(
45
+ (
46
+ {
47
+ className,
48
+ size = "md",
49
+ selected = false,
50
+ onToggle,
51
+ onClick,
52
+ leftIcon,
53
+ count,
54
+ type,
55
+ children,
56
+ ...rest
57
+ },
58
+ ref,
59
+ ) => {
60
+ const rootClass = [styles.root, sizeClass[size], className ?? ""]
61
+ .filter(Boolean)
62
+ .join(" ");
63
+
64
+ return (
65
+ <button
66
+ ref={ref}
67
+ type={type ?? "button"}
68
+ className={rootClass}
69
+ onClick={(e) => {
70
+ onClick?.(e);
71
+ if (!e.defaultPrevented) onToggle?.(!selected);
72
+ }}
73
+ aria-pressed={selected}
74
+ {...rest}
75
+ >
76
+ {leftIcon && (
77
+ <span className={styles.icon} aria-hidden>
78
+ {sizedIcon(leftIcon, ICON_SIZE[size])}
79
+ </span>
80
+ )}
81
+ {children && <span className={styles.label}>{children}</span>}
82
+ {count !== undefined && (
83
+ <span className={styles.count} aria-hidden>
84
+ {count}
85
+ </span>
86
+ )}
87
+ </button>
88
+ );
89
+ },
90
+ );
91
+
92
+ FilterButton.displayName = "FilterButton";
93
+
94
+ export { FilterButton };
@@ -0,0 +1,2 @@
1
+ export { FilterButton } from "./FilterButton";
2
+ export type { FilterButtonProps, FilterButtonSize } from "./FilterButton";