@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,109 @@
1
+ .root {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ border-radius: var(--radius-sm);
6
+ cursor: pointer;
7
+ border: 1px solid transparent;
8
+ padding: 0;
9
+ flex-shrink: 0;
10
+ transition: var(--transition-colors), var(--transition-ring), var(--transition-press);
11
+ }
12
+ .root:focus-visible {
13
+ outline: none;
14
+ box-shadow: var(--ring-shadow);
15
+ }
16
+ .root:active:not(:disabled) {
17
+ transform: translateY(var(--space-0_5));
18
+ }
19
+ /* Disabled — filled variant(secondary/outline)는 회색 배경 + 실버 색.
20
+ ghost·destructive는 평상시 배경이 없으므로 투명 유지하고 아이콘 색만 fg-disabled로 연해진다.
21
+ Pagination·툴바처럼 아이콘 하나짜리 비활성이 밑 배경을 가리면 어색하기 때문. */
22
+ .root:disabled {
23
+ cursor: not-allowed;
24
+ color: var(--fg-disabled);
25
+ border-color: transparent;
26
+ }
27
+ .secondary:disabled,
28
+ .outline:disabled {
29
+ background: var(--bg-muted);
30
+ }
31
+ .ghost:disabled,
32
+ .destructive:disabled {
33
+ background: transparent;
34
+ }
35
+
36
+ /* sizes — IconButton 전용 --iconbtn-h-*.
37
+ 텍스트 컨트롤(Button/Input)보다 좁게 잡아 아이콘 하나가 헐거워 보이지 않도록.
38
+ sm=인라인·초밀도 / md=툴바 기본(IconButton 기본값) / lg=표준.
39
+
40
+ ⚠️ Button과 사이즈 이름은 같지만 실제 크기는 한 단계씩 작다:
41
+ - IconButton sm(20) → Button에 대응 없음(더 작음)
42
+ - IconButton md(24) → Button sm(28)에 대응
43
+ - IconButton lg(32) → Button md(32)에 대응
44
+ 같은 툴바에 Button과 IconButton을 나란히 놓을 때 이 매핑으로 크기 매칭. */
45
+ .sm { width: var(--iconbtn-h-sm); height: var(--iconbtn-h-sm); border-radius: var(--radius-xs); }
46
+ .md { width: var(--iconbtn-h-md); height: var(--iconbtn-h-md); }
47
+ .lg { width: var(--iconbtn-h-lg); height: var(--iconbtn-h-lg); }
48
+
49
+ /* 아이콘 크기는 라이브러리 무관하게 CSS로 고정.
50
+ family 스왑 시 각 라이브러리의 size prop 처리 편차로 아이콘 크기가 미세하게
51
+ 달라지는 문제를 방지 → svg에 직접 강제. */
52
+ .sm > svg { width: var(--icon-size-xs); height: var(--icon-size-xs); }
53
+ .md > svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
54
+ .lg > svg { width: var(--icon-size-md); height: var(--icon-size-md); }
55
+
56
+ /* Dual: 트레일링 아이콘(예: caret-down) 동반 시 가로형.
57
+ 정사각 width를 풀고, 대칭 좁은 패딩 + gap으로 시각 균형. */
58
+ .dual {
59
+ width: auto;
60
+ gap: var(--space-1);
61
+ }
62
+ .sm.dual { padding-inline: var(--space-1); gap: var(--space-0_5); }
63
+ .md.dual { padding-inline: var(--space-1_5); }
64
+ .lg.dual { padding-inline: var(--space-2); }
65
+
66
+ /* variants ─────────────────────────────────────────────── */
67
+ /* secondary — primary 8% 알파 표면 + primary 텍스트 (Button.secondary와 동일 정책).
68
+ Accent=mono일 때는 시맨틱 토큰이 무채색으로 자동 폴백. */
69
+ .secondary {
70
+ background: var(--secondary-bg);
71
+ color: var(--secondary-fg);
72
+ border-color: transparent;
73
+ }
74
+ .secondary:hover:not(:disabled) {
75
+ background: var(--secondary-bg-hover);
76
+ }
77
+
78
+ /* outline — 경계만 */
79
+ .outline {
80
+ background: transparent;
81
+ color: var(--fg-muted);
82
+ border-color: var(--border);
83
+ }
84
+ .outline:hover:not(:disabled) {
85
+ background: var(--hover);
86
+ }
87
+
88
+ /* ghost — 무게 최소, hover에만 표면. IconButton 기본값. */
89
+ .ghost {
90
+ background: transparent;
91
+ color: var(--fg-subtle);
92
+ border-color: transparent;
93
+ }
94
+ .ghost:hover:not(:disabled) {
95
+ background: var(--hover);
96
+ color: var(--fg);
97
+ }
98
+
99
+ /* destructive — 아이콘 단독은 시각 부피가 작아 솔리드 destructive는 과함.
100
+ 평상시 transparent + destructive 텍스트, hover 시 destructive 12% 표면.
101
+ (Button의 destructive/destructive-ghost 둘 다 여기로 매핑됨.) */
102
+ .destructive {
103
+ background: transparent;
104
+ color: var(--destructive);
105
+ border-color: transparent;
106
+ }
107
+ .destructive:hover:not(:disabled) {
108
+ background: var(--destructive-subtle);
109
+ }
@@ -0,0 +1,110 @@
1
+ "use client";
2
+
3
+ import {
4
+ forwardRef,
5
+ isValidElement,
6
+ cloneElement,
7
+ type ReactNode,
8
+ type ButtonHTMLAttributes,
9
+ } from "react";
10
+ import { Slot } from "@radix-ui/react-slot";
11
+
12
+ import styles from "./IconButton.module.css";
13
+
14
+ /**
15
+ * IconButton 크기.
16
+ * ⚠️ Button과 이름은 같지만 실제 크기는 한 단계씩 작다:
17
+ * - IconButton md(24) ↔ Button sm(28)
18
+ * - IconButton lg(32) ↔ Button md(32)
19
+ * 같은 툴바에 나란히 놓을 때 위 매핑을 기준으로 크기를 맞춘다.
20
+ */
21
+ export type IconButtonSize = "sm" | "md" | "lg";
22
+ export type IconButtonVariant = "secondary" | "outline" | "ghost" | "destructive";
23
+
24
+ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
25
+ /** 아이콘 요소. asChild=true일 땐 자식 요소가 아이콘/링크 전체를 담당하므로 무시된다. */
26
+ icon?: ReactNode;
27
+ /** 트레일링 슬롯. caret-down 같은 보조 아이콘을 넣으면 정사각형→가로형(dual)로 자동 전환. */
28
+ trailingIcon?: ReactNode;
29
+ size?: IconButtonSize;
30
+ variant?: IconButtonVariant;
31
+ asChild?: boolean;
32
+ }
33
+
34
+ const sizeClassMap: Record<IconButtonSize, string> = {
35
+ sm: styles.sm,
36
+ md: styles.md,
37
+ lg: styles.lg,
38
+ };
39
+
40
+ const variantClassMap: Record<IconButtonVariant, string> = {
41
+ secondary: styles.secondary,
42
+ outline: styles.outline,
43
+ ghost: styles.ghost,
44
+ destructive: styles.destructive,
45
+ };
46
+
47
+ function withPhosphorWeight(node: ReactNode): ReactNode {
48
+ if (!isValidElement(node)) return node;
49
+ return cloneElement(node as React.ReactElement, {
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ weight: (node as any).props?.weight ?? "regular",
52
+ });
53
+ }
54
+
55
+ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
56
+ (
57
+ {
58
+ className,
59
+ icon,
60
+ trailingIcon,
61
+ size = "md",
62
+ variant = "ghost",
63
+ asChild = false,
64
+ type,
65
+ children,
66
+ ...props
67
+ },
68
+ ref
69
+ ) => {
70
+ const Comp = asChild ? Slot : "button";
71
+ const dual = trailingIcon !== undefined && trailingIcon !== null && trailingIcon !== false;
72
+
73
+ const rootClass = [
74
+ styles.root,
75
+ sizeClassMap[size],
76
+ variantClassMap[variant],
77
+ dual ? styles.dual : "",
78
+ className ?? "",
79
+ ]
80
+ .filter(Boolean)
81
+ .join(" ");
82
+
83
+ // asChild=true → 자식 요소(예: <a><Icon/></a>)가 아이콘 렌더를 담당.
84
+ // Slot은 단일 자식만 허용하므로 icon/trailingIcon은 무시하고 children을 그대로 넘긴다.
85
+ const content = asChild ? (
86
+ children
87
+ ) : (
88
+ <>
89
+ {withPhosphorWeight(icon)}
90
+ {dual && withPhosphorWeight(trailingIcon)}
91
+ </>
92
+ );
93
+
94
+ return (
95
+ <Comp
96
+ ref={ref}
97
+ className={rootClass}
98
+ type={asChild ? undefined : (type ?? "button")}
99
+ data-dual={dual || undefined}
100
+ {...props}
101
+ >
102
+ {content}
103
+ </Comp>
104
+ );
105
+ }
106
+ );
107
+
108
+ IconButton.displayName = "IconButton";
109
+
110
+ export { IconButton };
@@ -0,0 +1,2 @@
1
+ export { IconButton } from "./IconButton";
2
+ export type { IconButtonProps, IconButtonSize, IconButtonVariant } from "./IconButton";
@@ -0,0 +1,338 @@
1
+ /* Root container ────────────────────────────────────────── */
2
+ .root {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ gap: var(--space-1);
6
+ font-family: var(--font-normal);
7
+ color: var(--fg);
8
+ border: 1px solid var(--border);
9
+ border-radius: var(--radius-sm);
10
+ cursor: text;
11
+ transition:
12
+ background var(--duration-fast) var(--ease-out),
13
+ border-color var(--duration-fast) var(--ease-out),
14
+ box-shadow var(--duration-fast) var(--ease-out);
15
+ min-width: 0;
16
+ }
17
+
18
+ .root:hover:not([data-disabled]):not([data-readonly]),
19
+ .root:focus-visible,
20
+ .root:has(:focus-visible) {
21
+ border-color: var(--border-strong);
22
+ }
23
+
24
+ .root:focus-visible,
25
+ .root:has(:focus-visible) {
26
+ outline: none;
27
+ }
28
+
29
+ /* Variants ──────────────────────────────────────────────── */
30
+ .outline {
31
+ background: transparent;
32
+ }
33
+
34
+ .filled {
35
+ background: var(--bg-subtle);
36
+ border-color: transparent;
37
+ }
38
+
39
+ .filled:hover:not([data-disabled]):not([data-readonly]),
40
+ .filled:focus-visible,
41
+ .filled:has(:focus-visible) {
42
+ border-color: transparent;
43
+ }
44
+
45
+ /* line — 아래 선만 (Material 스타일). sizes 뒤에 정의해 radius/padding override. */
46
+ .root.line {
47
+ background: transparent;
48
+ border-color: transparent;
49
+ border-bottom-color: var(--border);
50
+ border-radius: 0;
51
+ padding-inline: 0;
52
+ }
53
+
54
+ .root.line:hover:not([data-disabled]):not([data-readonly]),
55
+ .root.line:focus-visible,
56
+ .root.line:has(:focus-visible) {
57
+ border-color: transparent;
58
+ border-bottom-color: var(--border-strong);
59
+ }
60
+
61
+ .root.line[data-invalid],
62
+ .root.line[data-invalid]:hover:not([data-disabled]):not([data-readonly]),
63
+ .root.line[data-invalid]:focus-visible,
64
+ .root.line[data-invalid]:has(:focus-visible) {
65
+ border-color: transparent;
66
+ border-bottom-color: var(--destructive);
67
+ }
68
+
69
+ /* States ────────────────────────────────────────────────── */
70
+ .root[data-invalid] {
71
+ border-color: var(--destructive);
72
+ }
73
+
74
+ .root[data-invalid]:hover:not([data-disabled]):not([data-readonly]),
75
+ .root[data-invalid]:focus-visible,
76
+ .root[data-invalid]:has(:focus-visible) {
77
+ border-color: var(--destructive);
78
+ }
79
+
80
+ .root[data-disabled] {
81
+ cursor: not-allowed;
82
+ opacity: 0.5;
83
+ }
84
+
85
+ .root[data-readonly] {
86
+ background: var(--bg-subtle);
87
+ cursor: default;
88
+ }
89
+
90
+ .root[data-readonly].filled {
91
+ background: var(--bg-muted);
92
+ }
93
+
94
+ /* Width fill ────────────────────────────────────────────
95
+ width="fill" 정책 — 부모 폭을 채운다. 폼 필드 실전 기본.
96
+ width="md"/"lg"/number는 컴포넌트에서 inline style로 지정. */
97
+ .fill {
98
+ width: 100%;
99
+ }
100
+
101
+ /* Sizes ─────────────────────────────────────────────────── */
102
+ .sizeSm {
103
+ height: var(--control-h-sm);
104
+ padding-inline: var(--space-2);
105
+ font-size: var(--text-sm);
106
+ border-radius: var(--radius-xs);
107
+ }
108
+
109
+ .sizeMd {
110
+ height: var(--control-h-md);
111
+ padding-inline: var(--space-3);
112
+ font-size: var(--text-sm);
113
+ border-radius: var(--radius-sm);
114
+ }
115
+
116
+ .sizeLg {
117
+ height: var(--control-h-lg);
118
+ padding-inline: var(--space-3);
119
+ font-size: var(--text-base);
120
+ border-radius: var(--radius-md);
121
+ }
122
+
123
+ /* Inner input ───────────────────────────────────────────── */
124
+ .input {
125
+ flex: 1 1 auto;
126
+ min-width: 0;
127
+ height: 100%;
128
+ padding: 0;
129
+ border: 0;
130
+ background: transparent;
131
+ color: inherit;
132
+ font-family: inherit;
133
+ font-size: inherit;
134
+ line-height: var(--lh-sm);
135
+ outline: none;
136
+ /* type="button" 등 non-text input의 기본 center 정렬 무력화. */
137
+ text-align: start;
138
+ }
139
+
140
+ .input::placeholder {
141
+ color: var(--fg-subtle);
142
+ }
143
+
144
+ .input:disabled {
145
+ cursor: not-allowed;
146
+ }
147
+
148
+ /* WebKit autofill — 기본 파란/노란 배경 대신 컨테이너 배경이 보이도록.
149
+ background-color는 직접 못 바꾸니 긴 inset shadow로 덮고, text는 --fg 유지. */
150
+ .input:-webkit-autofill,
151
+ .input:-webkit-autofill:hover,
152
+ .input:-webkit-autofill:focus,
153
+ .input:-webkit-autofill:active {
154
+ -webkit-text-fill-color: var(--fg);
155
+ -webkit-box-shadow: 0 0 0 1000px var(--bg) inset;
156
+ caret-color: var(--fg);
157
+ }
158
+
159
+ /* filled variant는 컨테이너 배경이 --bg-subtle. */
160
+ .filled .input:-webkit-autofill,
161
+ .filled .input:-webkit-autofill:hover,
162
+ .filled .input:-webkit-autofill:focus,
163
+ .filled .input:-webkit-autofill:active {
164
+ -webkit-box-shadow: 0 0 0 1000px var(--bg-subtle) inset;
165
+ }
166
+
167
+ /* Slots ─────────────────────────────────────────────────── */
168
+ .icon,
169
+ .iconBtn,
170
+ .prefix,
171
+ .suffix,
172
+ .actionSlot {
173
+ display: inline-flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ flex: 0 0 auto;
177
+ color: var(--fg);
178
+ }
179
+
180
+ /* leftIcon/rightIcon slot 색은 placeholder와 **동일한** 톤으로.
181
+ 값이 있든 없든, 포커스 상태든 상관없이 아이콘은 그대로 유지 —
182
+ placeholder와 색이 어긋나면 "둘 중 뭐가 primary 정보인지" 혼란. */
183
+ .icon {
184
+ color: var(--fg-subtle);
185
+ }
186
+
187
+ /* Icon svg를 사이즈별로 직접 제어 — Icon 컴포넌트가 svg에 하드코딩한 width/height(em)를 덮어씀. */
188
+ .sizeSm .icon > svg { width: var(--icon-size-xs); height: var(--icon-size-xs); }
189
+ .sizeMd .icon > svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
190
+ .sizeLg .icon > svg { width: var(--icon-size-md); height: var(--icon-size-md); }
191
+
192
+
193
+ .prefix,
194
+ .suffix {
195
+ font-size: var(--text-sm);
196
+ color: var(--fg-muted);
197
+ user-select: none;
198
+ }
199
+
200
+ .iconBtn {
201
+ padding: 0;
202
+ border: 0;
203
+ background: transparent;
204
+ color: var(--fg-muted);
205
+ border-radius: var(--radius-xs);
206
+ cursor: pointer;
207
+ transition: color var(--duration-fast) var(--ease-out);
208
+ }
209
+
210
+ .iconBtn:hover {
211
+ color: var(--fg);
212
+ }
213
+
214
+ .iconBtn:focus-visible {
215
+ outline: none;
216
+ box-shadow: var(--ring-shadow);
217
+ }
218
+
219
+ .actionSlot {
220
+ color: inherit;
221
+ }
222
+
223
+ /* trailing IconButton — 상하/좌우 여백을 모두 --space-1로 통일.
224
+ 버튼 크기 = Input height - space-1 * 2 → 각 사이즈 24/28/32px. */
225
+ .root .trailingBtn {
226
+ flex: 0 0 auto;
227
+ width: calc(var(--control-h-sm) - var(--space-2));
228
+ height: calc(var(--control-h-sm) - var(--space-2));
229
+ border-radius: var(--radius-xs);
230
+ }
231
+ .root.sizeMd .trailingBtn {
232
+ width: calc(var(--control-h-md) - var(--space-2));
233
+ height: calc(var(--control-h-md) - var(--space-2));
234
+ }
235
+ .root.sizeLg .trailingBtn {
236
+ width: calc(var(--control-h-lg) - var(--space-2));
237
+ height: calc(var(--control-h-lg) - var(--space-2));
238
+ border-radius: var(--radius-sm);
239
+ }
240
+ /* trailing IconButton이 있을 때 우측 padding을 좁혀 버튼 상하 여백과 대칭. */
241
+ .sizeSm:has(> .trailingBtn) { padding-inline-end: var(--space-0_5); }
242
+ .sizeMd:has(> .trailingBtn) { padding-inline-end: var(--space-1); }
243
+ .sizeLg:has(> .trailingBtn) { padding-inline-end: var(--space-1); }
244
+
245
+ /* Clear 버튼 enter/exit ─────────────────────────────────────
246
+ wrapper에 opacity·scale, 내부 IconButton엔 press 비활성 — 두 transform이
247
+ 같은 요소에 걸리면 서로 덮기 때문에 레이어 분리. duration/easing은
248
+ fast + ease-out micro-interaction 패턴. */
249
+ .clearBtn {
250
+ display: inline-flex;
251
+ align-items: center;
252
+ justify-content: center;
253
+ opacity: 0;
254
+ transform: scale(0.6);
255
+ pointer-events: none;
256
+ transition:
257
+ opacity var(--duration-fast) var(--ease-out),
258
+ transform var(--duration-fast) var(--ease-out);
259
+ }
260
+ .clearBtn[data-visible] {
261
+ opacity: 1;
262
+ transform: scale(1);
263
+ pointer-events: auto;
264
+ }
265
+ /* wrapper 안쪽 IconButton의 press translate 제거 — 눌릴 때 아래로 내려가는
266
+ 피드백이 미세한 X 버튼엔 과하다. opacity/scale enter·exit만 유지. */
267
+ .clearBtn > button:active:not(:disabled) {
268
+ transform: none;
269
+ }
270
+ @media (prefers-reduced-motion: reduce) {
271
+ .clearBtn,
272
+ .clearBtn[data-visible] {
273
+ transition: none;
274
+ transform: none;
275
+ }
276
+ }
277
+
278
+ /* FormField wrapper ─────────────────────────────────────── */
279
+ .field {
280
+ display: flex;
281
+ flex-direction: column;
282
+ gap: var(--space-1_5);
283
+ min-width: 0;
284
+ }
285
+
286
+ /* line variant는 좌우 padding이 없어 라벨이 더 가까이 붙어야 자연스러움. */
287
+ .field:has(> .root.line) {
288
+ gap: 0;
289
+ }
290
+
291
+ .label {
292
+ display: inline-flex;
293
+ align-items: center;
294
+ gap: var(--space-1);
295
+ font-family: var(--font-normal);
296
+ font-weight: var(--font-weight-medium);
297
+ color: var(--fg-muted);
298
+ transition: color var(--duration-fast) var(--ease-out);
299
+ }
300
+
301
+ .labelSm { font-size: var(--text-xs); }
302
+ .labelMd,
303
+ .labelLg { font-size: var(--text-sm); }
304
+
305
+ .field:focus-within .label {
306
+ color: var(--fg);
307
+ }
308
+
309
+ .requiredMark {
310
+ color: var(--destructive);
311
+ font-weight: var(--font-weight-medium);
312
+ }
313
+
314
+ .description,
315
+ .errorText {
316
+ margin: 0;
317
+ display: inline-flex;
318
+ align-items: center;
319
+ gap: var(--space-1);
320
+ font-family: var(--font-normal);
321
+ font-size: var(--text-xs);
322
+ line-height: var(--lh-xs);
323
+ }
324
+
325
+ .description {
326
+ color: var(--fg-subtle);
327
+ }
328
+
329
+ .errorText {
330
+ color: var(--destructive);
331
+ }
332
+
333
+ .messageIcon {
334
+ flex: 0 0 auto;
335
+ display: inline-flex;
336
+ align-items: center;
337
+ justify-content: center;
338
+ }