@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,279 @@
1
+ "use client";
2
+
3
+ import {
4
+ Children,
5
+ Fragment,
6
+ cloneElement,
7
+ forwardRef,
8
+ isValidElement,
9
+ type AnchorHTMLAttributes,
10
+ type ButtonHTMLAttributes,
11
+ type HTMLAttributes,
12
+ type ReactElement,
13
+ type ReactNode,
14
+ } from "react";
15
+ import * as RPopover from "@radix-ui/react-popover";
16
+ import { Slot, Slottable } from "@radix-ui/react-slot";
17
+
18
+ import { Icon, IconAxisProvider, useIconAxis } from "../Icon";
19
+ import { MenuItem } from "../MenuItem";
20
+ import styles from "./Breadcrumb.module.css";
21
+
22
+ /** 트리거 내부 아이콘 렌더 사이즈 — Button xs와 동일한 12px. */
23
+ const ICON_SIZE = 12;
24
+
25
+ function sizedIcon(node: ReactNode): ReactNode {
26
+ if (!isValidElement(node)) return node;
27
+ const el = node as ReactElement<{ size?: number | string }>;
28
+ if (el.props.size !== undefined) return el;
29
+ return cloneElement(el, { size: ICON_SIZE });
30
+ }
31
+
32
+ // ────────────────────────────────────────────────────────────
33
+ // Root — <nav aria-label="…"> + <ol>. Item 사이에 separator를 자동 삽입.
34
+ // ────────────────────────────────────────────────────────────
35
+
36
+ export interface BreadcrumbProps extends Omit<HTMLAttributes<HTMLElement>, "children"> {
37
+ /** 스크린리더용 라벨. 기본 "Breadcrumb". */
38
+ "aria-label"?: string;
39
+ /** 항목 사이 구분자. 기본은 caret-right 아이콘. */
40
+ separator?: ReactNode;
41
+ children: ReactNode;
42
+ }
43
+
44
+ const Root = forwardRef<HTMLElement, BreadcrumbProps>(
45
+ ({ className, separator, children, "aria-label": ariaLabel = "Breadcrumb", ...rest }, ref) => {
46
+ const sep = separator ?? <Icon name="caret-right" size={12} />;
47
+ const items = Children.toArray(children).filter(isValidElement);
48
+
49
+ return (
50
+ <nav
51
+ ref={ref}
52
+ aria-label={ariaLabel}
53
+ className={[styles.root, className ?? ""].filter(Boolean).join(" ")}
54
+ {...rest}
55
+ >
56
+ <ol className={styles.list}>
57
+ {items.map((child, i) => (
58
+ <Fragment key={(child as ReactElement).key ?? i}>
59
+ <li className={styles.item}>{child}</li>
60
+ {i < items.length - 1 && (
61
+ <li className={styles.separator} aria-hidden>
62
+ {sep}
63
+ </li>
64
+ )}
65
+ </Fragment>
66
+ ))}
67
+ </ol>
68
+ </nav>
69
+ );
70
+ },
71
+ );
72
+ Root.displayName = "Breadcrumb";
73
+
74
+ // ────────────────────────────────────────────────────────────
75
+ // Link — Button xs ghost와 동일한 토큰으로 스타일. asChild로 Next Link 등에 위임 가능.
76
+ // ────────────────────────────────────────────────────────────
77
+
78
+ export interface BreadcrumbLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
79
+ /** 자식 요소로 렌더 (예: Next.js <Link>). */
80
+ asChild?: boolean;
81
+ leftIcon?: ReactNode;
82
+ }
83
+
84
+ const Link = forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(
85
+ ({ asChild = false, leftIcon, className, children, ...rest }, ref) => {
86
+ const Comp = asChild ? Slot : "a";
87
+ const hasChildren = children !== undefined && children !== null && children !== false;
88
+ // 아이콘 단독 모드 — children 없이 leftIcon만: 원형 hit box + 접근성 라벨은 aria-label에 위임.
89
+ const iconOnly = !hasChildren && leftIcon != null;
90
+ const cls = [styles.link, iconOnly ? styles.linkIconOnly : "", className ?? ""]
91
+ .filter(Boolean)
92
+ .join(" ");
93
+ // asChild일 때 label을 Slottable로 감싸면 Slot이 자식 요소의 children으로 병합.
94
+ return (
95
+ <Comp ref={ref} className={cls} {...rest}>
96
+ {leftIcon && (
97
+ <span className={styles.linkIcon} aria-hidden>
98
+ {sizedIcon(leftIcon)}
99
+ </span>
100
+ )}
101
+ {hasChildren && (
102
+ <Slottable>
103
+ <span className={styles.linkLabel}>{children}</span>
104
+ </Slottable>
105
+ )}
106
+ </Comp>
107
+ );
108
+ },
109
+ );
110
+ Link.displayName = "Breadcrumb.Link";
111
+
112
+ // ────────────────────────────────────────────────────────────
113
+ // Current — 현재 페이지. 링크 아님. aria-current="page".
114
+ // ────────────────────────────────────────────────────────────
115
+
116
+ export interface BreadcrumbCurrentProps extends HTMLAttributes<HTMLSpanElement> {
117
+ leftIcon?: ReactNode;
118
+ }
119
+
120
+ const Current = forwardRef<HTMLSpanElement, BreadcrumbCurrentProps>(
121
+ ({ className, leftIcon, children, ...rest }, ref) => {
122
+ const hasChildren = children !== undefined && children !== null && children !== false;
123
+ const iconOnly = !hasChildren && leftIcon != null;
124
+ const cls = [styles.current, iconOnly ? styles.linkIconOnly : "", className ?? ""]
125
+ .filter(Boolean)
126
+ .join(" ");
127
+ return (
128
+ <span ref={ref} aria-current="page" className={cls} {...rest}>
129
+ {leftIcon && (
130
+ <span className={styles.linkIcon} aria-hidden>
131
+ {sizedIcon(leftIcon)}
132
+ </span>
133
+ )}
134
+ {hasChildren && <span className={styles.linkLabel}>{children}</span>}
135
+ </span>
136
+ );
137
+ },
138
+ );
139
+ Current.displayName = "Breadcrumb.Current";
140
+
141
+ // ────────────────────────────────────────────────────────────
142
+ // Ellipsis — 항목이 많을 때 중간 축약. 클릭 시 Select와 동일한 팝오버 표면으로
143
+ // 숨긴 항목 리스트를 연다. items는 필수 — 클릭 불가한 정적 마커는 지원하지 않는다.
144
+ // ────────────────────────────────────────────────────────────
145
+
146
+ export type BreadcrumbEllipsisItem = {
147
+ label: ReactNode;
148
+ href?: string;
149
+ onSelect?: () => void;
150
+ leftIcon?: ReactNode;
151
+ disabled?: boolean;
152
+ };
153
+
154
+ export interface BreadcrumbEllipsisProps
155
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onSelect"> {
156
+ /** 숨긴 항목 리스트. 최소 1개. */
157
+ items: BreadcrumbEllipsisItem[];
158
+ /** 팝오버 side offset (px). 기본 6. */
159
+ sideOffset?: number;
160
+ /** aria-label. 기본 "Show hidden pages". */
161
+ "aria-label"?: string;
162
+ }
163
+
164
+ const Ellipsis = forwardRef<HTMLButtonElement, BreadcrumbEllipsisProps>(
165
+ (
166
+ {
167
+ className,
168
+ items,
169
+ sideOffset = 6,
170
+ "aria-label": ariaLabel = "Show hidden pages",
171
+ ...rest
172
+ },
173
+ ref,
174
+ ) => {
175
+ return (
176
+ <RPopover.Root>
177
+ <RPopover.Trigger asChild>
178
+ <button
179
+ ref={ref}
180
+ type="button"
181
+ aria-label={ariaLabel}
182
+ className={[styles.ellipsisTrigger, className ?? ""].filter(Boolean).join(" ")}
183
+ {...rest}
184
+ >
185
+ &hellip;
186
+ </button>
187
+ </RPopover.Trigger>
188
+ <EllipsisContent items={items} sideOffset={sideOffset} />
189
+ </RPopover.Root>
190
+ );
191
+ },
192
+ );
193
+ Ellipsis.displayName = "Breadcrumb.Ellipsis";
194
+
195
+ /** Portal 하위에서 IconAxis 재주입 — Radix Portal이 트리 컨텍스트를 끊는다. */
196
+ function EllipsisContent({
197
+ items,
198
+ sideOffset,
199
+ }: {
200
+ items: BreadcrumbEllipsisItem[];
201
+ sideOffset: number;
202
+ }) {
203
+ const iconAxis = useIconAxis();
204
+ return (
205
+ <RPopover.Portal>
206
+ <RPopover.Content
207
+ align="start"
208
+ sideOffset={sideOffset}
209
+ collisionPadding={8}
210
+ className={[styles.popoverContent, "m-float"].join(" ")}
211
+ >
212
+ <IconAxisProvider value={iconAxis}>
213
+ <ul className={styles.popoverList} role="menu">
214
+ {items.map((item, i) => {
215
+ const leading = item.leftIcon
216
+ ? [
217
+ isValidElement(item.leftIcon) &&
218
+ (item.leftIcon as ReactElement<{ size?: number | string }>).props.size === undefined
219
+ ? cloneElement(item.leftIcon as ReactElement<{ size?: number | string }>, {
220
+ size: 16,
221
+ })
222
+ : item.leftIcon,
223
+ ]
224
+ : undefined;
225
+ const inner = (
226
+ <MenuItem size="sm" disabled={item.disabled} leadingActions={leading}>
227
+ {item.label}
228
+ </MenuItem>
229
+ );
230
+ return (
231
+ <li key={i} role="none">
232
+ {item.href ? (
233
+ <RPopover.Close asChild>
234
+ <a
235
+ href={item.href}
236
+ role="menuitem"
237
+ className={styles.popoverItemLink}
238
+ aria-disabled={item.disabled || undefined}
239
+ >
240
+ {inner}
241
+ </a>
242
+ </RPopover.Close>
243
+ ) : (
244
+ <RPopover.Close asChild>
245
+ <button
246
+ type="button"
247
+ role="menuitem"
248
+ disabled={item.disabled}
249
+ onClick={() => item.onSelect?.()}
250
+ className={styles.popoverItemButton}
251
+ >
252
+ {inner}
253
+ </button>
254
+ </RPopover.Close>
255
+ )}
256
+ </li>
257
+ );
258
+ })}
259
+ </ul>
260
+ </IconAxisProvider>
261
+ </RPopover.Content>
262
+ </RPopover.Portal>
263
+ );
264
+ }
265
+
266
+ // ────────────────────────────────────────────────────────────
267
+
268
+ type BreadcrumbCompound = typeof Root & {
269
+ Link: typeof Link;
270
+ Current: typeof Current;
271
+ Ellipsis: typeof Ellipsis;
272
+ };
273
+
274
+ const Breadcrumb = Root as BreadcrumbCompound;
275
+ Breadcrumb.Link = Link;
276
+ Breadcrumb.Current = Current;
277
+ Breadcrumb.Ellipsis = Ellipsis;
278
+
279
+ export { Breadcrumb };
@@ -0,0 +1,8 @@
1
+ export { Breadcrumb } from "./Breadcrumb";
2
+ export type {
3
+ BreadcrumbProps,
4
+ BreadcrumbLinkProps,
5
+ BreadcrumbCurrentProps,
6
+ BreadcrumbEllipsisProps,
7
+ BreadcrumbEllipsisItem,
8
+ } from "./Breadcrumb";
@@ -0,0 +1,216 @@
1
+ /* Base ─────────────────────────────────────────────────────── */
2
+ .root {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ gap: var(--space-2);
7
+ font-family: var(--label-font);
8
+ font-weight: var(--label-weight);
9
+ line-height: var(--lh-tight);
10
+ letter-spacing: var(--label-tracking);
11
+ white-space: nowrap;
12
+ cursor: pointer;
13
+ user-select: none;
14
+ border: 1px solid transparent;
15
+ /* 인터랙션 트랜지션은 globals.css의 시맨틱 프리셋 3종 조합.
16
+ 값 튜닝은 --transition-* 토큰에서만. */
17
+ transition: var(--transition-colors), var(--transition-ring), var(--transition-press);
18
+ }
19
+
20
+ .root:focus-visible {
21
+ outline: none;
22
+ box-shadow: var(--ring-shadow);
23
+ }
24
+
25
+ /* Press: 진입·해제 모두 base + ease-out → 대칭 부드러움 */
26
+ .root:active:not(:disabled) {
27
+ transform: translateY(var(--space-0_5));
28
+ }
29
+
30
+ /* Hover 시 아이콘 밝기 상승 (0.75 → 1.0) */
31
+ .root:hover:not(:disabled) .icon {
32
+ opacity: 1;
33
+ }
34
+ .icon {
35
+ transition: opacity var(--duration-slow) var(--ease-out);
36
+ }
37
+
38
+ /* Disabled — variant 무관 통일 스타일.
39
+ 회색 버튼(--bg-muted) + 실버 글자(--fg-disabled) + 투명 보더.
40
+ loading은 opacity로 유지 (스피너 컨텍스트 보존). */
41
+ .root:disabled {
42
+ cursor: not-allowed;
43
+ background: var(--bg-muted);
44
+ color: var(--fg-disabled);
45
+ border-color: transparent;
46
+ box-shadow: none;
47
+ }
48
+ .root:disabled .icon {
49
+ opacity: 1;
50
+ }
51
+ .root[data-loading="true"] {
52
+ cursor: not-allowed;
53
+ opacity: 0.5;
54
+ }
55
+
56
+ /* Full width ─────────────────────────────────────────────────── */
57
+ .fullWidth {
58
+ width: 100%;
59
+ }
60
+
61
+ /* Sizes ──────────────────────────────────────────────────────── */
62
+ /* Size는 높이·패딩·타이포·gap·radius 담당.
63
+ Shape 축(soft/standard/crisp)이 각 --radius-*의 실제 값을 스왑. */
64
+ .xs {
65
+ height: var(--control-h-xs);
66
+ padding-inline: var(--space-1);
67
+ gap: var(--space-1);
68
+ font-size: var(--text-xs);
69
+ border-radius: var(--radius-xs);
70
+ --btn-icon-size: var(--space-3);
71
+ }
72
+ .sm {
73
+ height: var(--control-h-sm);
74
+ padding-inline: var(--space-2);
75
+ gap: var(--space-1_5);
76
+ font-size: var(--label-sm-size);
77
+ border-radius: var(--radius-xs);
78
+ --btn-icon-size: var(--space-4);
79
+ }
80
+ .md {
81
+ height: var(--control-h-md);
82
+ padding-inline: var(--space-2);
83
+ gap: var(--space-1);
84
+ font-size: var(--label-md-size);
85
+ border-radius: var(--radius-sm);
86
+ --btn-icon-size: var(--space-4);
87
+ }
88
+ .lg {
89
+ height: var(--control-h-lg);
90
+ padding-inline: var(--space-4);
91
+ gap: var(--space-1_5);
92
+ font-size: var(--label-md-size);
93
+ border-radius: var(--radius-md);
94
+ --btn-icon-size: var(--space-5);
95
+ }
96
+
97
+
98
+ /* Icon / label slots ─────────────────────────────────────────── */
99
+ /* 아이콘 슬롯을 고정 정사각 박스로 강제. SVG viewBox 내부 여백이
100
+ 라이브러리·아이콘마다 달라도 슬롯 밖 레이아웃(gap·padding)은 항상 균일.
101
+ 슬롯 안에서 SVG는 100% 채움. */
102
+ .icon {
103
+ display: inline-flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ flex-shrink: 0;
107
+ width: var(--btn-icon-size);
108
+ height: var(--btn-icon-size);
109
+ /* 아이콘은 텍스트보다 한 단계 옅게 (Silver 톤).
110
+ currentColor 위에 opacity로 조절해서 filled/ghost 모두 대응. */
111
+ opacity: 0.75;
112
+ }
113
+ .icon > svg {
114
+ width: 100%;
115
+ height: 100%;
116
+ display: block;
117
+ }
118
+ .label {
119
+ display: inline-flex;
120
+ align-items: center;
121
+ }
122
+ /* Badge 슬롯: 라벨 오른쪽 트레일링. Badge 컴포넌트가 자체 색·radius를 가지므로
123
+ 슬롯은 정렬만 담당. gap은 sizeClass 기본값(--space-1~2) 사용. */
124
+ .badge {
125
+ display: inline-flex;
126
+ align-items: center;
127
+ flex-shrink: 0;
128
+ }
129
+ /* 아이콘 크기는 Button.tsx가 Icon size prop으로 직접 주입.
130
+ 여기서 CSS로 강제 축소하면 stroke가 뭉개져 두꺼워 보임. */
131
+
132
+ /* Variants ───────────────────────────────────────────────────── */
133
+ /* primary — 채움, 브랜드 컬러. 위 텍스트/아이콘은 --bg 사용(라이트=흰, 다크=거의 검정) */
134
+ .primary {
135
+ background: var(--primary);
136
+ color: var(--bg);
137
+ border-color: var(--primary);
138
+ }
139
+ .primary:hover:not(:disabled) {
140
+ background: var(--primary-hover);
141
+ border-color: var(--primary-hover);
142
+ }
143
+
144
+ /* inverse — 흑백 반전. mono Accent 프리셋에서 CTA 외 강조 버튼용.
145
+ 라이트: 검정 배경/흰 텍스트, 다크: 흰 배경/검정 텍스트. Hue 축과 무관. */
146
+ .inverse {
147
+ background: var(--fg);
148
+ color: var(--bg);
149
+ border-color: var(--fg);
150
+ }
151
+ .inverse:hover:not(:disabled) {
152
+ background: var(--fg-strong);
153
+ border-color: var(--fg-strong);
154
+ }
155
+
156
+ /* secondary — primary 8% 알파 표면 + primary 텍스트.
157
+ Accent=mono일 때는 무채색(--bg-muted + --fg)으로 자동 폴백. */
158
+ .secondary {
159
+ background: var(--secondary-bg);
160
+ color: var(--secondary-fg);
161
+ border-color: transparent;
162
+ }
163
+ .secondary:hover:not(:disabled) {
164
+ background: var(--secondary-bg-hover);
165
+ }
166
+
167
+ /* outline — 경계만 */
168
+ .outline {
169
+ background: transparent;
170
+ color: var(--fg);
171
+ border-color: var(--border);
172
+ }
173
+ .outline:hover:not(:disabled) {
174
+ background: var(--hover);
175
+ }
176
+
177
+ /* ghost — 무게 최소, hover에만 표면 */
178
+ .ghost {
179
+ background: transparent;
180
+ color: var(--fg);
181
+ border-color: transparent;
182
+ }
183
+ .ghost:hover:not(:disabled) {
184
+ background: var(--hover);
185
+ }
186
+
187
+ /* destructive — 위험 액션. 평상시 destructive 8% 알파 + destructive 텍스트,
188
+ hover 시 solid 빨강 배경 + fg로 전환 (진짜 위험함을 강조). */
189
+ .destructive {
190
+ background: var(--destructive-subtle);
191
+ color: var(--destructive);
192
+ border-color: transparent;
193
+ }
194
+ .destructive:hover:not(:disabled) {
195
+ background: var(--destructive);
196
+ color: var(--destructive-fg);
197
+ border-color: var(--destructive);
198
+ }
199
+
200
+ /* destructive-ghost — 평상시 fg 텍스트(위험 아님을 감춤),
201
+ hover 시 destructive 톤(8% 배경 + destructive 텍스트)으로 전환. */
202
+ .destructiveGhost {
203
+ background: transparent;
204
+ color: var(--fg);
205
+ border-color: transparent;
206
+ }
207
+ .destructiveGhost:hover:not(:disabled) {
208
+ background: var(--destructive-subtle);
209
+ color: var(--destructive);
210
+ }
211
+
212
+ /* Loading spinner slot ───────────────────────────────────────── */
213
+ .spinner {
214
+ display: inline-flex;
215
+ animation: m-spin var(--duration-slower) var(--ease-linear) infinite;
216
+ }
@@ -0,0 +1,149 @@
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
+ import { Slot } from "@radix-ui/react-slot";
12
+
13
+ import styles from "./Button.module.css";
14
+
15
+ export type ButtonVariant =
16
+ | "primary"
17
+ | "inverse"
18
+ | "secondary"
19
+ | "outline"
20
+ | "ghost"
21
+ | "destructive"
22
+ | "destructive-ghost";
23
+
24
+ export type ButtonSize = "xs" | "sm" | "md" | "lg";
25
+
26
+ export type ButtonProps = Omit<
27
+ ButtonHTMLAttributes<HTMLButtonElement>,
28
+ "size"
29
+ > & {
30
+ size?: ButtonSize;
31
+ variant?: ButtonVariant;
32
+ fullWidth?: boolean;
33
+ loading?: boolean;
34
+ leftIcon?: ReactNode;
35
+ rightIcon?: ReactNode;
36
+ /** 라벨 오른쪽 트레일링 슬롯에 붙는 카운트·상태 뱃지.
37
+ * rightIcon과 공존 시 badge → rightIcon 순으로 배치. */
38
+ badge?: ReactNode;
39
+ asChild?: boolean;
40
+ };
41
+
42
+ const variantClass: Record<ButtonVariant, string> = {
43
+ primary: styles.primary,
44
+ inverse: styles.inverse,
45
+ secondary: styles.secondary,
46
+ outline: styles.outline,
47
+ ghost: styles.ghost,
48
+ destructive: styles.destructive,
49
+ "destructive-ghost": styles.destructiveGhost,
50
+ };
51
+
52
+ const sizeClass: Record<ButtonSize, string> = {
53
+ xs: styles.xs,
54
+ sm: styles.sm,
55
+ md: styles.md,
56
+ lg: styles.lg,
57
+ };
58
+
59
+ const ICON_SIZE: Record<ButtonSize, number> = {
60
+ xs: 12,
61
+ sm: 16,
62
+ md: 16,
63
+ lg: 20,
64
+ };
65
+
66
+ function sizedIcon(node: ReactNode, size: number): ReactNode {
67
+ if (!isValidElement(node)) return node;
68
+ const el = node as ReactElement<{ size?: number | string }>;
69
+ if (el.props.size !== undefined) return el;
70
+ return cloneElement(el, { size });
71
+ }
72
+
73
+ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
74
+ (
75
+ {
76
+ className,
77
+ variant = "primary",
78
+ size = "md",
79
+ fullWidth = false,
80
+ loading = false,
81
+ leftIcon,
82
+ rightIcon,
83
+ badge,
84
+ asChild = false,
85
+ disabled,
86
+ type,
87
+ children,
88
+ ...rest
89
+ },
90
+ ref,
91
+ ) => {
92
+ const Comp = asChild ? Slot : "button";
93
+
94
+ const rootClass = [
95
+ styles.root,
96
+ variantClass[variant],
97
+ sizeClass[size],
98
+ fullWidth ? styles.fullWidth : "",
99
+ className ?? "",
100
+ ]
101
+ .filter(Boolean)
102
+ .join(" ");
103
+
104
+ /* asChild 모드: Slot은 자식이 정확히 하나여야 한다. leftIcon/label/badge/rightIcon을
105
+ 모두 넘기면 자식이 여러 개가 되어 터지므로, asChild일 때는 children을 그대로
106
+ Slot에 넘기고(= 소비 측 anchor/div 하나) icon·badge prop은 무시. 아이콘은 소비
107
+ 측에서 anchor 내부에 직접 꽂아 single-child 조건을 지키도록 설계. */
108
+ if (asChild) {
109
+ return (
110
+ <Slot
111
+ ref={ref}
112
+ className={rootClass}
113
+ data-loading={loading || undefined}
114
+ {...rest}
115
+ >
116
+ {children}
117
+ </Slot>
118
+ );
119
+ }
120
+
121
+ return (
122
+ <Comp
123
+ ref={ref}
124
+ className={rootClass}
125
+ type={type ?? "button"}
126
+ disabled={disabled || loading}
127
+ data-loading={loading || undefined}
128
+ {...rest}
129
+ >
130
+ {leftIcon && (
131
+ <span className={styles.icon} aria-hidden>
132
+ {sizedIcon(leftIcon, ICON_SIZE[size])}
133
+ </span>
134
+ )}
135
+ <span className={styles.label}>{children}</span>
136
+ {badge && <span className={styles.badge}>{badge}</span>}
137
+ {rightIcon && (
138
+ <span className={styles.icon} aria-hidden>
139
+ {sizedIcon(rightIcon, ICON_SIZE[size])}
140
+ </span>
141
+ )}
142
+ </Comp>
143
+ );
144
+ },
145
+ );
146
+
147
+ Button.displayName = "Button";
148
+
149
+ export { Button };
@@ -0,0 +1,2 @@
1
+ export { Button } from "./Button";
2
+ export type { ButtonProps, ButtonVariant, ButtonSize } from "./Button";