@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,82 @@
1
+ /* 숫자 아이템은 Button(라벨 있음), prev/next/first/last는 IconButton(정사각 전용).
2
+ Button을 icon-only로 쓰면 가로 padding이 남아 정사각이 안 되므로 IconButton이 맞다.
3
+ 숫자 Button의 min-width만 height에 맞춰 1자리에서도 폭이 흔들리지 않게 한다. 2자리+ 자연 확장.
4
+
5
+ 사이즈 매핑:
6
+ Pagination md = 내부 Button sm(32px) + IconButton md(실제 width/height를 32로 올림)
7
+ Pagination lg = 내부 Button md(36px) + IconButton lg(실제 width/height를 36으로 올림)
8
+ */
9
+ .root {
10
+ display: inline-flex;
11
+ align-items: center;
12
+ }
13
+
14
+ .list {
15
+ display: inline-flex;
16
+ align-items: center;
17
+ gap: var(--space-0_5);
18
+ padding: 0;
19
+ margin: 0;
20
+ list-style: none;
21
+ }
22
+
23
+ /* Compact variant는 "prev · 3/24 · next" 세 요소뿐이므로 gap을 더 조여 하나의 밀집 블록처럼 보이게. */
24
+ .listCompact {
25
+ gap: 0;
26
+ }
27
+
28
+ /* 숫자 Button min-width — 내부 Button height와 맞춘 정사각 hit box.
29
+ padding-inline은 Button 기본값보다 좁혀 1자리 숫자가 정사각으로 떨어지게. */
30
+ .numberBtnMd {
31
+ min-width: var(--control-h-sm);
32
+ padding-inline: var(--space-1);
33
+ }
34
+ .numberBtnLg {
35
+ min-width: var(--control-h-md);
36
+ padding-inline: var(--space-1_5);
37
+ }
38
+
39
+ /* Disabled 시 숫자 Button은 배경 없이 숫자 색만 fg-disabled로 연해지게.
40
+ Button 기본은 모든 variant를 disabled에서 회색 배경(--bg-muted)으로 통일하지만,
41
+ Pagination 내부의 숫자 아이템은 밀집한 자리라 밑 배경이 가려지면 어색하다.
42
+ IconButton의 ghost disabled와 톤 맞춤.
43
+ .list 접두어로 Button 내부 셀렉터보다 특이도 우위 확보. */
44
+ .list .numberBtnMd:disabled,
45
+ .list .numberBtnLg:disabled {
46
+ background: transparent;
47
+ border-color: transparent;
48
+ }
49
+
50
+ /* IconButton 리사이즈 — IconButton의 기본 사이즈 체계는 Button보다 한 단계씩 작게 설계되어
51
+ 있어서(md=24, lg=32) 숫자 Button(sm=32, md=36)과 세로 리듬이 안 맞는다.
52
+ Pagination 안에서만 width/height를 숫자 Button과 동일하게 올려 리듬 일치. */
53
+ .iconBtnMd {
54
+ width: var(--control-h-sm);
55
+ height: var(--control-h-sm);
56
+ }
57
+ .iconBtnLg {
58
+ width: var(--control-h-md);
59
+ height: var(--control-h-md);
60
+ }
61
+
62
+ /* Compact 상태 라벨 — "3 / 24" 류. 숫자/구분자 모두 fg-muted로 가라앉히고
63
+ 인터랙션 영역이 아님을 명확히. gap은 숫자-구분자 사이가 뜨지 않게 짧게. */
64
+ .statusMd,
65
+ .statusLg {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ padding-inline: var(--space-1);
69
+ font-weight: var(--label-weight);
70
+ color: var(--fg-muted);
71
+ font-variant-numeric: tabular-nums;
72
+ white-space: nowrap;
73
+ letter-spacing: var(--label-tracking);
74
+ }
75
+ .statusMd {
76
+ min-height: var(--control-h-sm);
77
+ font-size: var(--label-sm-size);
78
+ }
79
+ .statusLg {
80
+ min-height: var(--control-h-md);
81
+ font-size: var(--label-md-size);
82
+ }
@@ -0,0 +1,248 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Pagination ─ 긴 목록·테이블의 페이지 네비.
5
+ *
6
+ * 설계 원칙 — "variant로 유즈케이스 조립":
7
+ * DatePicker처럼 분화된 컴포넌트(DateRangePicker)를 만들지 않고 하나의 Pagination에
8
+ * `variant` prop으로 numbered / simple / compact / jump를 커버한다. prop 표면이
9
+ * 예측 가능성이 높다.
10
+ *
11
+ * 시각 규칙:
12
+ * - 숫자 아이템은 Button(라벨 있음). prev/next/first/last는 IconButton(정사각 전용).
13
+ * Button을 icon-only로 쓰면 가로 padding이 남아 정사각으로 안 떨어지므로 IconButton이 맞다.
14
+ * - 사이즈 매핑: Pagination md = Button sm + IconButton md, Pagination lg = Button md + IconButton lg.
15
+ * - 현재 페이지는 `variant="secondary"` (primary 8% 알파 배경 + primary 텍스트).
16
+ * primary filled는 페이지 안 다른 primary 액션과 충돌해 쓰지 않음.
17
+ * - 그 외는 `variant="ghost"`로 시각 무게 최소화.
18
+ */
19
+
20
+ import {
21
+ forwardRef,
22
+ useMemo,
23
+ type HTMLAttributes,
24
+ type ReactNode,
25
+ } from "react";
26
+
27
+ import { Button } from "../Button";
28
+ import { IconButton, type IconButtonSize } from "../IconButton";
29
+ import { Icon } from "../Icon";
30
+ import styles from "./Pagination.module.css";
31
+
32
+ /** Pagination size → 내부 Button/IconButton size 매핑.
33
+ * Pagination md = Button sm + IconButton md, Pagination lg = Button md + IconButton lg. */
34
+ const BUTTON_SIZE: Record<"md" | "lg", "sm" | "md"> = {
35
+ md: "sm",
36
+ lg: "md",
37
+ };
38
+ const ICON_BTN_SIZE: Record<"md" | "lg", IconButtonSize> = {
39
+ md: "md",
40
+ lg: "lg",
41
+ };
42
+
43
+ export type PaginationVariant = "numbered" | "compact" | "jump";
44
+ export type PaginationSize = "md" | "lg";
45
+
46
+ export interface PaginationProps
47
+ extends Omit<HTMLAttributes<HTMLElement>, "onChange"> {
48
+ variant?: PaginationVariant;
49
+ /** 아이템 사이즈. md=내부 Button sm, lg=내부 Button md. */
50
+ size?: PaginationSize;
51
+ /** 현재 페이지 (1-indexed) */
52
+ page: number;
53
+ /** 전체 페이지 수 */
54
+ total: number;
55
+ onChange?: (page: number) => void;
56
+ /** numbered·jump에서 한 번에 보여줄 숫자 개수(5~10). 기본 5.
57
+ * 현재 페이지 중심의 슬라이딩 윈도우 — 생략(…)·양 끝 고정 숫자 없음. */
58
+ windowSize?: number;
59
+ disabled?: boolean;
60
+ /** compact variant의 라벨 포맷터. 기본 `${page} / ${total}`. */
61
+ compactFormatter?: (page: number, total: number) => ReactNode;
62
+ "aria-label"?: string;
63
+ }
64
+
65
+ /**
66
+ * 현재 페이지 중심의 슬라이딩 윈도우.
67
+ * 전체가 윈도우보다 작으면 전부 표시. 아니면 현재를 가운데 두되,
68
+ * 왼/오 끝 가까우면 그 쪽으로 밀착해 빈 자리가 생기지 않게 한다.
69
+ * 예: total=100, windowSize=5, page=1 → [1..5]
70
+ * total=100, windowSize=5, page=10 → [8..12]
71
+ * total=100, windowSize=5, page=100 → [96..100]
72
+ */
73
+ function buildWindow(page: number, total: number, windowSize: number): number[] {
74
+ const size = Math.min(windowSize, total);
75
+ const half = Math.floor((size - 1) / 2);
76
+ let start = page - half;
77
+ let end = start + size - 1;
78
+ if (start < 1) {
79
+ start = 1;
80
+ end = size;
81
+ }
82
+ if (end > total) {
83
+ end = total;
84
+ start = total - size + 1;
85
+ }
86
+ return Array.from({ length: size }, (_, i) => start + i);
87
+ }
88
+
89
+ /** windowSize를 5~10 범위로 클램프. */
90
+ function clampWindow(n: number): number {
91
+ if (!Number.isFinite(n)) return 5;
92
+ return Math.min(10, Math.max(5, Math.round(n)));
93
+ }
94
+
95
+ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
96
+ (
97
+ {
98
+ variant = "numbered",
99
+ size = "md",
100
+ page,
101
+ total,
102
+ onChange,
103
+ windowSize = 5,
104
+ disabled = false,
105
+ compactFormatter,
106
+ className,
107
+ "aria-label": ariaLabel = "페이지네이션",
108
+ ...rest
109
+ },
110
+ ref
111
+ ) => {
112
+ const safeTotal = Math.max(1, total);
113
+ const safePage = Math.min(Math.max(1, page), safeTotal);
114
+ const isFirst = safePage <= 1;
115
+ const isLast = safePage >= safeTotal;
116
+
117
+ const go = (p: number) => {
118
+ if (disabled) return;
119
+ const next = Math.min(Math.max(1, p), safeTotal);
120
+ if (next !== safePage) onChange?.(next);
121
+ };
122
+
123
+ const range = useMemo(
124
+ () =>
125
+ variant === "numbered" || variant === "jump"
126
+ ? buildWindow(safePage, safeTotal, clampWindow(windowSize))
127
+ : [],
128
+ [variant, safePage, safeTotal, windowSize]
129
+ );
130
+
131
+ const rootClass = [styles.root, className ?? ""].filter(Boolean).join(" ");
132
+ const numberCls = size === "md" ? styles.numberBtnMd : styles.numberBtnLg;
133
+ const iconCls = size === "md" ? styles.iconBtnMd : styles.iconBtnLg;
134
+ const statusCls = size === "md" ? styles.statusMd : styles.statusLg;
135
+ const buttonSize = BUTTON_SIZE[size];
136
+ const iconSize = ICON_BTN_SIZE[size];
137
+
138
+ /* prev/next는 "보이는 윈도우의 끝 숫자"로 점프 — 현재가 윈도우 중앙이면 윈도우 안에서 끝으로,
139
+ 이미 끝 위에 있으면 다음 호출에서 윈도우가 통째로 밀린다. 숫자 윈도우가 없는 compact에선
140
+ 기존 ±1 유지. */
141
+ const prevTarget = range.length > 0 ? range[0] : safePage - 1;
142
+ const nextTarget =
143
+ range.length > 0 ? range[range.length - 1] : safePage + 1;
144
+ const prevDisabled = disabled || isFirst || prevTarget === safePage;
145
+ const nextDisabled = disabled || isLast || nextTarget === safePage;
146
+
147
+ return (
148
+ <nav
149
+ ref={ref}
150
+ className={rootClass}
151
+ aria-label={ariaLabel}
152
+ role="navigation"
153
+ {...rest}
154
+ >
155
+ <ul
156
+ className={[
157
+ styles.list,
158
+ variant === "compact" ? styles.listCompact : "",
159
+ ]
160
+ .filter(Boolean)
161
+ .join(" ")}
162
+ >
163
+ {variant === "jump" && (
164
+ <li>
165
+ <IconButton
166
+ size={iconSize}
167
+ variant="ghost"
168
+ aria-label="처음 페이지"
169
+ icon={<Icon name="caret-double-left" />}
170
+ disabled={disabled || isFirst}
171
+ onClick={() => go(1)}
172
+ className={iconCls}
173
+ />
174
+ </li>
175
+ )}
176
+
177
+ <li>
178
+ <IconButton
179
+ size={iconSize}
180
+ variant="ghost"
181
+ aria-label="이전 페이지"
182
+ icon={<Icon name="caret-left" />}
183
+ disabled={prevDisabled}
184
+ onClick={() => go(prevTarget)}
185
+ className={iconCls}
186
+ />
187
+ </li>
188
+
189
+ {variant === "compact" && (
190
+ <li className={statusCls} aria-live="polite">
191
+ {compactFormatter
192
+ ? compactFormatter(safePage, safeTotal)
193
+ : `${safePage}/${safeTotal}`}
194
+ </li>
195
+ )}
196
+
197
+ {(variant === "numbered" || variant === "jump") &&
198
+ range.map((item) => {
199
+ const isCurrent = item === safePage;
200
+ return (
201
+ <li key={item}>
202
+ <Button
203
+ size={buttonSize}
204
+ variant={isCurrent ? "secondary" : "ghost"}
205
+ aria-label={`${item} 페이지`}
206
+ aria-current={isCurrent ? "page" : undefined}
207
+ disabled={disabled}
208
+ onClick={() => go(item)}
209
+ className={numberCls}
210
+ >
211
+ {item}
212
+ </Button>
213
+ </li>
214
+ );
215
+ })}
216
+
217
+ <li>
218
+ <IconButton
219
+ size={iconSize}
220
+ variant="ghost"
221
+ aria-label="다음 페이지"
222
+ icon={<Icon name="caret-right" />}
223
+ disabled={nextDisabled}
224
+ onClick={() => go(nextTarget)}
225
+ className={iconCls}
226
+ />
227
+ </li>
228
+
229
+ {variant === "jump" && (
230
+ <li>
231
+ <IconButton
232
+ size={iconSize}
233
+ variant="ghost"
234
+ aria-label="마지막 페이지"
235
+ icon={<Icon name="caret-double-right" />}
236
+ disabled={disabled || isLast}
237
+ onClick={() => go(safeTotal)}
238
+ className={iconCls}
239
+ />
240
+ </li>
241
+ )}
242
+ </ul>
243
+ </nav>
244
+ );
245
+ }
246
+ );
247
+
248
+ Pagination.displayName = "Pagination";
@@ -0,0 +1,6 @@
1
+ export { Pagination } from "./Pagination";
2
+ export type {
3
+ PaginationProps,
4
+ PaginationVariant,
5
+ PaginationSize,
6
+ } from "./Pagination";
@@ -0,0 +1,64 @@
1
+ .root {
2
+ height: var(--space-0_5);
3
+ background: transparent;
4
+ overflow: hidden;
5
+ pointer-events: none;
6
+ transition: opacity var(--duration-fast) var(--ease-out);
7
+ opacity: 0;
8
+ }
9
+ .root[data-active] {
10
+ opacity: 1;
11
+ }
12
+
13
+ .root[data-position="fixed"] {
14
+ position: fixed;
15
+ top: 0;
16
+ left: 0;
17
+ right: 0;
18
+ z-index: var(--z-toast);
19
+ }
20
+
21
+ .bar {
22
+ height: 100%;
23
+ background: var(--fg-strong);
24
+ border-radius: 0 var(--radius-full) var(--radius-full) 0;
25
+ transition: width var(--duration-fast) var(--ease-out);
26
+ }
27
+
28
+ .root[data-mode="determinate"] .bar {
29
+ transform-origin: left center;
30
+ }
31
+
32
+ /* indeterminate — 왼쪽부터 채워지는 NProgress 스타일.
33
+ 빠르게 시작해서 (뜨자마자 30% 이미 채움) 서서히 90%까지. */
34
+ .root[data-mode="indeterminate"] .bar {
35
+ width: 0%;
36
+ transform-origin: left center;
37
+ animation: pb-grow calc(var(--duration-slower) * 3) var(--ease-out) forwards;
38
+ }
39
+
40
+ @keyframes pb-grow {
41
+ 0% { width: 0%; }
42
+ 10% { width: 30%; }
43
+ 30% { width: 55%; }
44
+ 60% { width: 75%; }
45
+ 100% { width: 90%; }
46
+ }
47
+
48
+ /* 사라질 때 100%까지 밀고 fade-out — "완료됐다" 느낌 */
49
+ .root:not([data-active])[data-mode="indeterminate"] .bar {
50
+ animation: none;
51
+ width: 100%;
52
+ transition:
53
+ width var(--duration-fast) var(--ease-out),
54
+ opacity var(--duration-base) var(--ease-out);
55
+ }
56
+
57
+ @media (prefers-reduced-motion: reduce) {
58
+ .root[data-mode="indeterminate"] .bar {
59
+ animation: none;
60
+ width: 100%;
61
+ transform: none;
62
+ opacity: 0.5;
63
+ }
64
+ }
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type HTMLAttributes } from "react";
4
+ import styles from "./ProgressBar.module.css";
5
+
6
+ export type ProgressBarProps = HTMLAttributes<HTMLDivElement> & {
7
+ /**
8
+ * 진행률 (0~100). 미지정이면 무한 왕복 애니메이션(indeterminate).
9
+ * 실제 %가 없는 라우팅/네트워크 로딩용.
10
+ */
11
+ value?: number;
12
+ /** 화면 최상단에 fixed로 붙임. */
13
+ position?: "fixed" | "inline";
14
+ /** 표시 여부. false면 즉시 숨김 + fade-out. */
15
+ active?: boolean;
16
+ };
17
+
18
+ /**
19
+ * 라인 로딩바. 두 가지 모드:
20
+ * - value 있으면 그 %로 채움 (determinate)
21
+ * - value 없으면 무한 왕복 (indeterminate) — 네트워크·라우팅 로딩용
22
+ */
23
+ const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
24
+ ({ value, position = "fixed", active = true, className, ...rest }, ref) => {
25
+ const isDeterminate = typeof value === "number";
26
+ const clamped = isDeterminate
27
+ ? Math.max(0, Math.min(100, value as number))
28
+ : undefined;
29
+
30
+ return (
31
+ <div
32
+ ref={ref}
33
+ role="progressbar"
34
+ aria-valuemin={0}
35
+ aria-valuemax={100}
36
+ aria-valuenow={clamped}
37
+ data-position={position}
38
+ data-active={active || undefined}
39
+ data-mode={isDeterminate ? "determinate" : "indeterminate"}
40
+ className={[styles.root, className ?? ""].filter(Boolean).join(" ")}
41
+ {...rest}
42
+ >
43
+ <div
44
+ className={styles.bar}
45
+ style={isDeterminate ? { width: `${clamped}%` } : undefined}
46
+ />
47
+ </div>
48
+ );
49
+ },
50
+ );
51
+
52
+ ProgressBar.displayName = "ProgressBar";
53
+
54
+ export { ProgressBar };
@@ -0,0 +1,2 @@
1
+ export { ProgressBar } from "./ProgressBar";
2
+ export type { ProgressBarProps } from "./ProgressBar";
@@ -0,0 +1,61 @@
1
+ /* QuotationBar — ChatInput 상단에 붙는 인용 바.
2
+ ↳ icon + 1줄 ellipsis + X 레이아웃. hairline border, radius-md. */
3
+
4
+ .root {
5
+ display: flex;
6
+ align-items: center;
7
+ gap: var(--space-2);
8
+ min-width: 0;
9
+ padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
10
+ background: var(--bg);
11
+ border: var(--hairline) solid var(--border);
12
+ border-radius: var(--radius-md);
13
+ color: var(--fg);
14
+ font-size: var(--text-sm);
15
+ transition: var(--transition-colors);
16
+ }
17
+
18
+ .leadIcon {
19
+ width: var(--space-4);
20
+ height: var(--space-4);
21
+ color: var(--fg-muted);
22
+ flex: 0 0 auto;
23
+ }
24
+
25
+ .text {
26
+ flex: 1 1 auto;
27
+ min-width: 0;
28
+ overflow: hidden;
29
+ text-overflow: ellipsis;
30
+ white-space: nowrap;
31
+ color: var(--fg-muted);
32
+ line-height: 1.3;
33
+ }
34
+
35
+ .removeBtn {
36
+ display: inline-flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ padding: 0;
40
+ border: 0;
41
+ background: transparent;
42
+ color: var(--fg-muted);
43
+ border-radius: var(--radius-sm);
44
+ cursor: pointer;
45
+ transition: var(--transition-colors);
46
+ width: var(--space-5);
47
+ height: var(--space-5);
48
+ flex: 0 0 auto;
49
+ }
50
+ .removeBtn:hover {
51
+ background: var(--bg-subtle);
52
+ color: var(--fg);
53
+ }
54
+ .removeBtn:focus-visible {
55
+ outline: none;
56
+ box-shadow: var(--ring);
57
+ }
58
+ .removeIcon {
59
+ width: 70%;
60
+ height: 70%;
61
+ }
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ import type { HTMLAttributes, MouseEvent } from "react";
4
+ import {
5
+ ArrowBendDownRight as PhArrowBendDownRight,
6
+ X as PhX,
7
+ } from "@phosphor-icons/react";
8
+ import styles from "./QuotationBar.module.css";
9
+
10
+ export interface QuotationBarProps
11
+ extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "onError"> {
12
+ /** 인용 본문. 1줄 ellipsis — 긴 글은 자동 생략. */
13
+ text: string;
14
+ /** 지정 시 우측 X 노출, 클릭 시 호출. */
15
+ onRemove?: (e: MouseEvent<HTMLButtonElement>) => void;
16
+ }
17
+
18
+ export function QuotationBar({
19
+ text,
20
+ onRemove,
21
+ className,
22
+ ...rest
23
+ }: QuotationBarProps) {
24
+ const cls = [styles.root, className].filter(Boolean).join(" ");
25
+
26
+ return (
27
+ <div className={cls} {...rest}>
28
+ <PhArrowBendDownRight
29
+ className={styles.leadIcon}
30
+ weight="regular"
31
+ aria-hidden
32
+ />
33
+ <span className={styles.text} title={text}>
34
+ {text}
35
+ </span>
36
+ {onRemove ? (
37
+ <button
38
+ type="button"
39
+ className={styles.removeBtn}
40
+ onClick={onRemove}
41
+ aria-label="인용 제거"
42
+ >
43
+ <PhX className={styles.removeIcon} weight="regular" />
44
+ </button>
45
+ ) : null}
46
+ </div>
47
+ );
48
+ }
@@ -0,0 +1,2 @@
1
+ export { QuotationBar } from "./QuotationBar";
2
+ export type { QuotationBarProps } from "./QuotationBar";
@@ -0,0 +1,63 @@
1
+ /* Radio ────────────────────────────────────────────────────────
2
+ 폼 · 필터에서 단일 선택을 표시하는 컨트롤.
3
+ 외곽 원 = box-shadow inset, 안쪽 도트 = radial-gradient (indicator의
4
+ 같은 배경 레이어에 그려지므로 서브픽셀 어긋남이 없다). 접근성 = <input type="radio">.
5
+ FilterButton type="radio"도 indicator를 재사용한다. */
6
+ .root {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ gap: var(--space-2);
10
+ cursor: pointer;
11
+ user-select: none;
12
+ color: var(--fg);
13
+ font-family: var(--label-font);
14
+ font-size: var(--label-md-size);
15
+ letter-spacing: var(--label-tracking);
16
+ line-height: var(--lh-tight);
17
+ }
18
+ .root[data-disabled="true"] {
19
+ cursor: not-allowed;
20
+ opacity: 0.5;
21
+ }
22
+
23
+ .indicator {
24
+ display: inline-block;
25
+ flex-shrink: 0;
26
+ width: var(--space-4);
27
+ height: var(--space-4);
28
+ border-radius: var(--radius-full);
29
+ box-shadow: inset 0 0 0 1px var(--border-strong);
30
+ background:
31
+ radial-gradient(circle at center, transparent 0, transparent 25%, transparent 26%),
32
+ var(--bg);
33
+ transition:
34
+ box-shadow var(--duration-slow) var(--ease-out),
35
+ background var(--duration-slow) var(--ease-out);
36
+ }
37
+
38
+ /* Focus */
39
+ .input:focus-visible + .indicator {
40
+ outline: none;
41
+ box-shadow: var(--ring-shadow);
42
+ }
43
+
44
+ /* Hover — 링만 selected-fg로 은은하게 물듦 (checked면 이미 채워져 있어 무시) */
45
+ .root:hover:not([data-disabled="true"]) .input:not(:checked) + .indicator {
46
+ box-shadow: inset 0 0 0 1px var(--selected-fg);
47
+ }
48
+
49
+ /* Checked — 링 + 안쪽 도트 (radial-gradient) 켬. 색은 Accent 축에 따라 mono=fg, 나머지=primary */
50
+ .input:checked + .indicator {
51
+ box-shadow: inset 0 0 0 1px var(--selected-fg);
52
+ background:
53
+ radial-gradient(circle at center, var(--selected-fg) 0, var(--selected-fg) 30%, transparent 31%),
54
+ var(--bg);
55
+ }
56
+
57
+ /* Standalone indicator (FilterButton 내부) */
58
+ .indicator[data-checked="true"] {
59
+ box-shadow: inset 0 0 0 1px var(--selected-fg);
60
+ background:
61
+ radial-gradient(circle at center, var(--selected-fg) 0, var(--selected-fg) 30%, transparent 31%),
62
+ var(--bg);
63
+ }
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type InputHTMLAttributes, type ReactNode } from "react";
4
+
5
+ import styles from "./Radio.module.css";
6
+
7
+ export type RadioProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> & {
8
+ children?: ReactNode;
9
+ };
10
+
11
+ const Radio = forwardRef<HTMLInputElement, RadioProps>(
12
+ ({ className, disabled, children, ...rest }, ref) => {
13
+ const rootClass = [styles.root, className ?? ""].filter(Boolean).join(" ");
14
+
15
+ return (
16
+ <label className={rootClass} data-disabled={disabled ? "true" : "false"}>
17
+ <input
18
+ ref={ref}
19
+ type="radio"
20
+ className={`${styles.input} visually-hidden`}
21
+ disabled={disabled}
22
+ {...rest}
23
+ />
24
+ <span className={styles.indicator} aria-hidden />
25
+ {children != null && <span>{children}</span>}
26
+ </label>
27
+ );
28
+ },
29
+ );
30
+
31
+ Radio.displayName = "Radio";
32
+
33
+ export { Radio };
@@ -0,0 +1,2 @@
1
+ export { Radio } from "./Radio";
2
+ export type { RadioProps } from "./Radio";