@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,371 @@
1
+ "use client";
2
+
3
+ import {
4
+ forwardRef,
5
+ useCallback,
6
+ useId,
7
+ useRef,
8
+ useState,
9
+ type ChangeEvent,
10
+ type InputHTMLAttributes,
11
+ type KeyboardEvent,
12
+ type MouseEvent,
13
+ type ReactNode,
14
+ } from "react";
15
+
16
+ import { Icon } from "../Icon";
17
+ import { IconButton, type IconButtonSize } from "../IconButton";
18
+ import styles from "./Input.module.css";
19
+
20
+ export type InputSize = "sm" | "md" | "lg";
21
+ export type InputVariant = "outline" | "filled" | "line";
22
+ /**
23
+ * 가로 폭 정책.
24
+ * - "fill"(기본): 부모 폭을 채운다. 폼 필드 실전 기본.
25
+ * - "md" | "lg": 권장 폭 토큰(--input-w-md=280px · --input-w-lg=400px)에 고정.
26
+ * 카드/툴바 안에서 여러 필드의 폭을 규격화할 때.
27
+ * - number: 임의 픽셀 폭. 예외적으로만 사용.
28
+ * sm 사이즈용 폭 토큰은 두지 않는다 — 좁은 필드는 부모 컨테이너 폭으로 통제하는 게 자연스러움.
29
+ */
30
+ export type InputWidth = "fill" | "md" | "lg" | number;
31
+
32
+ export interface InputProps
33
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "prefix" | "width"> {
34
+ size?: InputSize;
35
+ variant?: InputVariant;
36
+ /** 폼 검증 실패. aria-invalid + 시각 피드백. warning이 있으면 자동으로 true. */
37
+ invalid?: boolean;
38
+ /** 왼쪽/오른쪽 아이콘 (input 안쪽, 클릭 불가) */
39
+ leftIcon?: ReactNode;
40
+ rightIcon?: ReactNode;
41
+ /** 왼쪽/오른쪽 짧은 텍스트 (예: "$", "USD", "kg") */
42
+ prefix?: ReactNode;
43
+ suffix?: ReactNode;
44
+ /** X 버튼으로 clear. value가 있어야 나타남. controlled 전용. */
45
+ clearable?: boolean;
46
+ onClear?: () => void;
47
+ /** password 토글 아이콘 자동 노출 (type="password"에 부착) */
48
+ passwordToggle?: boolean;
49
+ /** 커스텀 우측 액션 (예: 버튼). rightIcon보다 우선. */
50
+ rightAction?: ReactNode;
51
+ /** 가로 폭. 기본 "fill" (부모 폭 채움). "md"=280px · "lg"=400px · number=px. */
52
+ width?: InputWidth;
53
+ /** 라벨 텍스트. 있으면 <label>이 위에 렌더링되고 aria로 자동 연결. */
54
+ label?: ReactNode;
55
+ /** 중립 톤의 설명 메시지. 인풋 아래 표시. */
56
+ description?: ReactNode;
57
+ /** 경고 메시지. 있으면 invalid로 취급되어 보더가 destructive로 바뀐다. */
58
+ warning?: ReactNode;
59
+ }
60
+
61
+ const WRAPPER_EVENT_KEYS = [
62
+ "onClick",
63
+ "onMouseDown",
64
+ "onMouseUp",
65
+ "onPointerDown",
66
+ "onPointerUp",
67
+ "onKeyDown",
68
+ "onKeyUp",
69
+ "onFocus",
70
+ "onBlur",
71
+ ] as const;
72
+
73
+ const WRAPPER_A11Y_KEYS = [
74
+ "role",
75
+ "tabIndex",
76
+ "style",
77
+ "aria-haspopup",
78
+ "aria-expanded",
79
+ "aria-controls",
80
+ "aria-label",
81
+ "data-state",
82
+ ] as const;
83
+
84
+ const sizeClass: Record<InputSize, string> = {
85
+ sm: styles.sizeSm,
86
+ md: styles.sizeMd,
87
+ lg: styles.sizeLg,
88
+ };
89
+
90
+ const variantClass: Record<InputVariant, string> = {
91
+ outline: styles.outline,
92
+ filled: styles.filled,
93
+ line: styles.line,
94
+ };
95
+
96
+ const labelSizeClass: Record<InputSize, string> = {
97
+ sm: styles.labelSm,
98
+ md: styles.labelMd,
99
+ lg: styles.labelLg,
100
+ };
101
+
102
+ const iconButtonSizeFor: Record<InputSize, IconButtonSize> = {
103
+ sm: "sm",
104
+ md: "sm",
105
+ lg: "md",
106
+ };
107
+
108
+ export const Input = forwardRef<HTMLElement, InputProps>(
109
+ (
110
+ {
111
+ className,
112
+ size = "md",
113
+ variant = "outline",
114
+ invalid: invalidProp,
115
+ leftIcon,
116
+ rightIcon,
117
+ prefix,
118
+ suffix,
119
+ clearable = false,
120
+ onClear,
121
+ passwordToggle = false,
122
+ rightAction,
123
+ width = "fill",
124
+ type = "text",
125
+ disabled,
126
+ readOnly,
127
+ required,
128
+ value,
129
+ defaultValue,
130
+ onChange,
131
+ id,
132
+ label,
133
+ description,
134
+ warning,
135
+ ...props
136
+ },
137
+ ref
138
+ ) => {
139
+ const innerRef = useRef<HTMLInputElement>(null);
140
+
141
+ const hasWarning = warning != null && warning !== false;
142
+ const invalid = invalidProp ?? hasWarning;
143
+
144
+ const autoId = useId();
145
+ const inputId = id ?? autoId;
146
+ const descriptionId = description != null ? `${inputId}-desc` : undefined;
147
+ const warningId = hasWarning ? `${inputId}-warn` : undefined;
148
+ const describedBy =
149
+ [warningId, descriptionId].filter(Boolean).join(" ") || undefined;
150
+
151
+ const wrapperProps: Record<string, unknown> = {};
152
+ for (const key of WRAPPER_EVENT_KEYS) {
153
+ if (key in props) {
154
+ wrapperProps[key] = (props as Record<string, unknown>)[key];
155
+ delete (props as Record<string, unknown>)[key];
156
+ }
157
+ }
158
+ for (const key of WRAPPER_A11Y_KEYS) {
159
+ if (key in props) {
160
+ wrapperProps[key] = (props as Record<string, unknown>)[key];
161
+ delete (props as Record<string, unknown>)[key];
162
+ }
163
+ }
164
+ const wrapperClick = wrapperProps.onClick as
165
+ | ((e: MouseEvent<HTMLDivElement>) => void)
166
+ | undefined;
167
+ const wrapperKeyDown = wrapperProps.onKeyDown as
168
+ | ((e: KeyboardEvent<HTMLDivElement>) => void)
169
+ | undefined;
170
+ const isInteractive =
171
+ Boolean(wrapperProps.role) || WRAPPER_EVENT_KEYS.some((k) => k in wrapperProps);
172
+
173
+ const [uncontrolledValue, setUncontrolledValue] = useState<string>(
174
+ String(defaultValue ?? "")
175
+ );
176
+ const [passwordVisible, setPasswordVisible] = useState(false);
177
+
178
+ const isControlled = value !== undefined;
179
+ const currentValue = isControlled ? String(value ?? "") : uncontrolledValue;
180
+ const hasValue = currentValue.length > 0;
181
+
182
+ const handleChange = useCallback(
183
+ (e: ChangeEvent<HTMLInputElement>) => {
184
+ if (!isControlled) setUncontrolledValue(e.target.value);
185
+ onChange?.(e);
186
+ },
187
+ [isControlled, onChange]
188
+ );
189
+
190
+ const handleClear = useCallback(() => {
191
+ if (!isControlled) setUncontrolledValue("");
192
+ onClear?.();
193
+ innerRef.current?.focus();
194
+ }, [isControlled, onClear]);
195
+
196
+ const effectiveType =
197
+ passwordToggle && type === "password" && passwordVisible ? "text" : type;
198
+
199
+ const showClear = clearable && hasValue && !disabled && !readOnly;
200
+ const showPasswordToggle = passwordToggle && type === "password" && !disabled;
201
+
202
+ /* width 정책 — "fill"은 부모 폭 채움(기본), "md"/"lg"는 토큰 폭, number는 px.
203
+ "fill" 이외에는 inline style로 지정해 CSS의 확장 여지를 열어둔다.
204
+ widthStyle이 있으면 폼 필드 래퍼(.field)에도 같은 값이 필요 — 아래에서 fieldStyle로 합침. */
205
+ const widthStyle: string | undefined =
206
+ width === "fill"
207
+ ? undefined
208
+ : width === "md"
209
+ ? "var(--input-w-md)"
210
+ : width === "lg"
211
+ ? "var(--input-w-lg)"
212
+ : `${width}px`;
213
+
214
+ const rootClass = [
215
+ styles.root,
216
+ variantClass[variant],
217
+ sizeClass[size],
218
+ width === "fill" ? styles.fill : "",
219
+ className ?? "",
220
+ ]
221
+ .filter(Boolean)
222
+ .join(" ");
223
+
224
+ const mergedStyle: React.CSSProperties | undefined = widthStyle
225
+ ? { ...(wrapperProps.style as React.CSSProperties | undefined), width: widthStyle }
226
+ : (wrapperProps.style as React.CSSProperties | undefined);
227
+
228
+ const control = (
229
+ <div
230
+ {...wrapperProps}
231
+ ref={ref as React.Ref<HTMLDivElement>}
232
+ className={rootClass}
233
+ style={mergedStyle}
234
+ data-invalid={invalid || undefined}
235
+ data-disabled={disabled || undefined}
236
+ data-readonly={readOnly || undefined}
237
+ tabIndex={
238
+ isInteractive
239
+ ? (wrapperProps.tabIndex as number | undefined) ?? 0
240
+ : (wrapperProps.tabIndex as number | undefined)
241
+ }
242
+ onClick={(e) => {
243
+ wrapperClick?.(e);
244
+ if (!isInteractive) innerRef.current?.focus();
245
+ }}
246
+ onKeyDown={wrapperKeyDown}
247
+ >
248
+ {prefix && <span className={styles.prefix}>{prefix}</span>}
249
+ {leftIcon && (
250
+ <span className={styles.icon} aria-hidden>
251
+ {leftIcon}
252
+ </span>
253
+ )}
254
+
255
+ <input
256
+ ref={innerRef}
257
+ id={inputId}
258
+ className={styles.input}
259
+ type={effectiveType}
260
+ value={isControlled ? currentValue : undefined}
261
+ defaultValue={isControlled ? undefined : defaultValue}
262
+ onChange={handleChange}
263
+ disabled={disabled}
264
+ readOnly={readOnly}
265
+ required={required}
266
+ aria-invalid={invalid || undefined}
267
+ aria-describedby={describedBy}
268
+ {...(isInteractive
269
+ ? { tabIndex: -1, style: { pointerEvents: "none" } }
270
+ : {})}
271
+ {...props}
272
+ />
273
+
274
+ {clearable && !disabled && !readOnly && (
275
+ /* 항상 렌더 — data-visible 토글로 opacity/scale 트랜지션 (Input.module.css).
276
+ wrapper에 transform(scale)·opacity를, 내부 IconButton엔 자체 press transform을
277
+ 분리해 담아 두 애니메이션이 서로 덮지 않도록 한다. */
278
+ <span
279
+ className={`${styles.trailingBtn} ${styles.clearBtn}`}
280
+ data-visible={showClear || undefined}
281
+ aria-hidden={!showClear || undefined}
282
+ >
283
+ <IconButton
284
+ size={iconButtonSizeFor[size]}
285
+ icon={<Icon name="x" />}
286
+ aria-label="지우기"
287
+ tabIndex={-1}
288
+ onClick={(e) => {
289
+ e.stopPropagation();
290
+ handleClear();
291
+ }}
292
+ />
293
+ </span>
294
+ )}
295
+
296
+ {showPasswordToggle && (
297
+ <IconButton
298
+ size={iconButtonSizeFor[size]}
299
+ icon={<Icon name={passwordVisible ? "eye-slash" : "eye"} />}
300
+ aria-label={passwordVisible ? "비밀번호 숨기기" : "비밀번호 보기"}
301
+ tabIndex={-1}
302
+ className={styles.trailingBtn}
303
+ onClick={(e) => {
304
+ e.stopPropagation();
305
+ setPasswordVisible((v) => !v);
306
+ }}
307
+ />
308
+ )}
309
+
310
+ {rightAction
311
+ ? <span className={styles.actionSlot}>{rightAction}</span>
312
+ : rightIcon && (
313
+ <span className={styles.icon} aria-hidden>
314
+ {rightIcon}
315
+ </span>
316
+ )}
317
+
318
+ {suffix && <span className={styles.suffix}>{suffix}</span>}
319
+ </div>
320
+ );
321
+
322
+ if (label == null && description == null && !hasWarning) {
323
+ return control;
324
+ }
325
+
326
+ /* field 래퍼도 같은 폭을 이어받아야 label · description · warning 라인이 control과 정렬된다.
327
+ width="fill"이면 field 자체는 100% (styles.field는 min-width:0만 갖고 별도 폭이 없어
328
+ 부모 폭을 따르는 게 자연스러움). 지정 폭이면 그 값을 얹는다. */
329
+ const fieldStyle: React.CSSProperties | undefined = widthStyle
330
+ ? { width: widthStyle }
331
+ : undefined;
332
+
333
+ return (
334
+ <div className={styles.field} style={fieldStyle} data-disabled={disabled || undefined}>
335
+ {label != null && (
336
+ <label
337
+ htmlFor={inputId}
338
+ className={[styles.label, labelSizeClass[size]].join(" ")}
339
+ >
340
+ {label}
341
+ {required && (
342
+ <span aria-hidden className={styles.requiredMark}>*</span>
343
+ )}
344
+ </label>
345
+ )}
346
+
347
+ {control}
348
+
349
+ {hasWarning && (
350
+ <p id={warningId} role="alert" className={styles.errorText}>
351
+ <span className={styles.messageIcon} aria-hidden>
352
+ <Icon name="warning-circle" size={14} />
353
+ </span>
354
+ {warning}
355
+ </p>
356
+ )}
357
+
358
+ {description != null && !hasWarning && (
359
+ <p id={descriptionId} className={styles.description}>
360
+ <span className={styles.messageIcon} aria-hidden>
361
+ <Icon name="info" size={14} />
362
+ </span>
363
+ {description}
364
+ </p>
365
+ )}
366
+ </div>
367
+ );
368
+ }
369
+ );
370
+
371
+ Input.displayName = "Input";
@@ -0,0 +1,2 @@
1
+ export { Input } from "./Input";
2
+ export type { InputProps, InputSize, InputVariant, InputWidth } from "./Input";
@@ -0,0 +1,318 @@
1
+ /*
2
+ * ListItem — row 레벨 수평 anatomy. 수직 리듬은 parent gap/hairline에 위임.
3
+ *
4
+ * padding/border/bg는 자체적으로 걸지 않지만, hover bg는 "chrome"이 아니라
5
+ * "상호작용 피드백"이므로 row 레벨에서 담당.
6
+ *
7
+ * 색상 훅 (부모 컨테이너에서 오버라이드):
8
+ * --list-item-fg body 서술 색 (default: --fg)
9
+ * --list-item-from-fg from(누구) 색 (default: --fg-strong)
10
+ * --list-item-meta-fg meta/상대시간 색 (default: --fg-muted)
11
+ * --list-item-hover-bg hover 시 row bg (default: --hover). transparent로 끄기
12
+ */
13
+
14
+ .root {
15
+ display: flex;
16
+ align-items: flex-start;
17
+ gap: var(--space-3);
18
+ color: var(--list-item-fg, var(--fg));
19
+ text-decoration: none;
20
+ min-width: 0;
21
+ /* 좌우 padding 없음 — 가로 anatomy는 flex gap만 담당해서 좌우 비대칭 방지.
22
+ 세로 padding만 걸어 hover hit area 확보. 부모 li가 세로 hairline/padding을
23
+ 담당하면 음의 margin으로 리듬 유지. 좌우로 hover bg를 넓히고 싶으면
24
+ 부모 wrapper에 padding/negative margin을 걸어 조정한다. */
25
+ padding-block: var(--space-3);
26
+ margin-block: calc(var(--space-3) * -1);
27
+ transition: var(--transition-colors);
28
+ }
29
+
30
+ .inline {
31
+ align-items: center;
32
+ }
33
+
34
+ .stacked {
35
+ align-items: flex-start;
36
+ /* 두 줄짜리는 세로 호흡을 한 단계 더. margin-block은 root 기본값(-space-3) 유지해
37
+ 실제 레이아웃 박스가 벌어지도록. */
38
+ padding-block: var(--space-4);
39
+ }
40
+
41
+ /* row hover — 모든 리스트 항목에서 "지금 이 줄" 신호.
42
+ pure 좌표에서 끄려면 --list-item-hover-bg: transparent. */
43
+ .root:hover {
44
+ background: var(--list-item-hover-bg, var(--hover));
45
+ }
46
+
47
+ .interactive {
48
+ cursor: pointer;
49
+ }
50
+
51
+ .interactive:focus-visible {
52
+ outline: none;
53
+ box-shadow: var(--ring-shadow);
54
+ }
55
+
56
+ /* Leading — Avatar / Icon / 커스텀 */
57
+ .leading {
58
+ flex-shrink: 0;
59
+ display: inline-flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ color: var(--fg-muted);
63
+ }
64
+
65
+ /* leading 안쪽에 size=auto인 leadingIcon이 있으면 wrapper를 투명화해서
66
+ leadingIcon이 root flex의 직접 자식이 되게 한다 — 그래야 align-self: stretch가
67
+ root content 전체 높이를 받고, root gap이 leadingIcon에 직접 적용됨. */
68
+ .leading:has(.leadingIcon.auto) {
69
+ display: contents;
70
+ }
71
+
72
+ /* ListItemLeadingIcon — "배경색 + 아이콘" leading 타입.
73
+ Avatar와 같은 사이즈 트랙에 올려 리스트에서 섞여도 정렬이 깨지지 않는다.
74
+ bg/fg는 tone 프리셋 또는 CSS 변수로 자유 조절. */
75
+ .leadingIcon {
76
+ flex-shrink: 0;
77
+ display: inline-flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ background: var(--leading-icon-bg, var(--bg-muted));
81
+ color: var(--leading-icon-fg, var(--fg));
82
+ border-radius: var(--radius-sm);
83
+ }
84
+
85
+ .leadingIcon.xs { width: var(--space-4); height: var(--space-4); border-radius: var(--radius-xs); }
86
+ .leadingIcon.sm { width: var(--space-6); height: var(--space-6); border-radius: var(--radius-sm); }
87
+ .leadingIcon.md { width: var(--space-8); height: var(--space-8); border-radius: var(--radius-md); }
88
+ .leadingIcon.lg { width: var(--space-12); height: var(--space-12); border-radius: var(--radius-lg); }
89
+
90
+ /* auto — stacked 두 줄 content 높이에 딱 맞는 정사각 마크.
91
+ flex 아이템에서 aspect-ratio + align-self: stretch 조합은 width가 min-content로
92
+ 풀려 세로로 길어진다. 그래서 height를 토큰 기반으로 명시해 폭을 그에 묶는다.
93
+ 계산: stacked content 두 줄 = from(text-sm × lh-tight) + body(text-sm × lh-sm) + gap(space-1).
94
+ 오버라이드가 필요하면 --leading-icon-size로 통째로 교체. */
95
+ .leadingIcon.auto {
96
+ align-self: center;
97
+ height: var(--leading-icon-size,
98
+ calc(var(--text-sm) * var(--lh-tight) + var(--lh-sm) + var(--space-1))
99
+ );
100
+ aspect-ratio: 1 / 1;
101
+ width: auto;
102
+ border-radius: var(--radius-md);
103
+ }
104
+
105
+ /* tone 프리셋 — subtle bg + solid fg 쌍. */
106
+ .leadingIcon[data-tone="neutral"] {
107
+ --leading-icon-bg: var(--bg-muted);
108
+ --leading-icon-fg: var(--fg);
109
+ }
110
+ .leadingIcon[data-tone="primary"] {
111
+ --leading-icon-bg: var(--primary-subtle);
112
+ --leading-icon-fg: var(--primary);
113
+ }
114
+ .leadingIcon[data-tone="success"] {
115
+ --leading-icon-bg: var(--success-subtle);
116
+ --leading-icon-fg: var(--success);
117
+ }
118
+ .leadingIcon[data-tone="warning"] {
119
+ --leading-icon-bg: var(--warning-subtle);
120
+ --leading-icon-fg: var(--warning);
121
+ }
122
+ .leadingIcon[data-tone="danger"] {
123
+ --leading-icon-bg: var(--destructive-subtle);
124
+ --leading-icon-fg: var(--destructive);
125
+ }
126
+ .leadingIcon[data-tone="info"] {
127
+ --leading-icon-bg: var(--info-subtle);
128
+ --leading-icon-fg: var(--info);
129
+ }
130
+
131
+ /* Read-state 톤 신호 ──────────────────────────────────
132
+ dot/badge 없이 텍스트 톤만으로 읽음/안읽음을 표현.
133
+ - unread: 전체 톤이 가장 진하게 (body=fg-strong), meta는 primary로 "지금"을 강조
134
+ - read: 전체 톤이 한두 단계 뒤로 물러남
135
+ - 둘 다 미지정: 중립 — 리드 개념 없는 자리 */
136
+
137
+ /* unread — meta primary, body/from 가장 진한 톤 */
138
+ .unread .body,
139
+ .unread .target,
140
+ .unread .narrativeBody {
141
+ color: var(--fg-strong, var(--fg));
142
+ }
143
+ .unread .from {
144
+ color: var(--fg-strong, var(--fg));
145
+ font-weight: var(--font-weight-medium);
146
+ }
147
+ .unread .meta {
148
+ color: var(--primary);
149
+ font-weight: var(--font-weight-regular);
150
+ }
151
+
152
+ /* read — 전체 한 단계 뒤로 */
153
+ .read .body,
154
+ .read .target,
155
+ .read .narrativeBody {
156
+ color: var(--fg-muted);
157
+ }
158
+ .read .from {
159
+ color: var(--fg-muted);
160
+ font-weight: var(--font-weight-regular);
161
+ }
162
+ .read .meta,
163
+ .read .verb,
164
+ .read .surface {
165
+ color: var(--fg-subtle);
166
+ }
167
+ .read .leading {
168
+ opacity: 0.7;
169
+ }
170
+
171
+ /* Content wrapper. stacked에서 title↔subtitle gap은 xs(space-1). */
172
+ .content {
173
+ flex: 1;
174
+ min-width: 0;
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: var(--space-1);
178
+ }
179
+
180
+ /* 서술 모드 (A): from body (inline). dash 없이 공백으로만 분리. */
181
+ .narrativeBody {
182
+ display: inline;
183
+ font-size: var(--text-sm);
184
+ line-height: var(--lh-sm);
185
+ color: var(--list-item-fg, var(--fg));
186
+ font-weight: var(--font-weight-regular);
187
+ min-width: 0;
188
+ }
189
+
190
+ .narrativeBody .from {
191
+ margin-inline-end: var(--space-2);
192
+ }
193
+
194
+ /* 서술 모드 (B): from · verb · target · surface */
195
+ .actionBody {
196
+ display: inline-flex;
197
+ flex-wrap: wrap;
198
+ align-items: baseline;
199
+ gap: var(--space-1_5);
200
+ font-size: var(--text-sm);
201
+ line-height: var(--lh-sm);
202
+ min-width: 0;
203
+ }
204
+
205
+ /* stacked: content가 flex-column이라 from·body가 자동 2줄로. */
206
+ .stacked .from {
207
+ font-size: var(--text-sm);
208
+ line-height: var(--lh-tight);
209
+ }
210
+
211
+ .stacked .body {
212
+ font-size: var(--text-sm);
213
+ line-height: var(--lh-sm);
214
+ color: var(--list-item-fg, var(--fg));
215
+ }
216
+
217
+ .from {
218
+ font-weight: var(--font-weight-medium);
219
+ color: var(--list-item-from-fg, var(--fg-strong));
220
+ }
221
+
222
+ .body {
223
+ color: var(--list-item-fg, var(--fg));
224
+ font-weight: var(--font-weight-regular);
225
+ }
226
+
227
+ .verb {
228
+ color: var(--list-item-meta-fg, var(--fg-muted));
229
+ font-weight: var(--font-weight-regular);
230
+ }
231
+
232
+ .target {
233
+ color: var(--list-item-fg, var(--fg));
234
+ font-weight: var(--font-weight-regular);
235
+ }
236
+
237
+ /* 작은 글자(surface, meta)는 본문보다 한 단계 더 연하게 — subtle. */
238
+ .surface {
239
+ font-size: var(--text-xs);
240
+ color: var(--list-item-meta-fg, var(--fg-subtle));
241
+ margin-inline-start: var(--space-1);
242
+ }
243
+
244
+ /* Meta — 오른쪽 끝 상대시간. 작은 글자라 subtle 톤. */
245
+ .meta {
246
+ flex-shrink: 0;
247
+ font-size: var(--text-xs);
248
+ color: var(--list-item-meta-fg, var(--fg-subtle));
249
+ font-variant-numeric: tabular-nums;
250
+ align-self: center;
251
+ }
252
+
253
+ .stacked .meta {
254
+ align-self: flex-start;
255
+ margin-top: var(--space-0_5);
256
+ }
257
+
258
+ /* Trailing — meta 자리 커스텀 */
259
+ .trailing {
260
+ flex-shrink: 0;
261
+ display: inline-flex;
262
+ align-items: center;
263
+ gap: var(--space-2);
264
+ color: var(--list-item-meta-fg, var(--fg-muted));
265
+ font-size: var(--text-xs);
266
+ }
267
+
268
+ /* hoverActions — 평상시 숨김, row hover/focus에서 등장 */
269
+ .hoverActions {
270
+ display: inline-flex;
271
+ align-items: center;
272
+ gap: var(--space-1);
273
+ flex-shrink: 0;
274
+ width: 0;
275
+ margin-inline-start: 0;
276
+ opacity: 0;
277
+ pointer-events: none;
278
+ overflow: hidden;
279
+ transition:
280
+ width var(--duration-fast) var(--ease-out),
281
+ margin-inline-start var(--duration-fast) var(--ease-out),
282
+ opacity var(--duration-fast) var(--ease-out);
283
+ }
284
+
285
+ .root:hover .hoverActions,
286
+ .root:focus-visible .hoverActions,
287
+ .root:focus-within .hoverActions {
288
+ width: auto;
289
+ margin-inline-start: var(--space-2);
290
+ opacity: 1;
291
+ pointer-events: auto;
292
+ }
293
+
294
+ @media (hover: none) {
295
+ .hoverActions {
296
+ width: auto;
297
+ margin-inline-start: var(--space-2);
298
+ opacity: 1;
299
+ pointer-events: auto;
300
+ }
301
+ }
302
+
303
+ /* Variant별 default — anatomy는 공통. */
304
+ .activity {
305
+ /* no-op */
306
+ }
307
+
308
+ .notification {
309
+ /* TSX에서 unread default true 처리 */
310
+ }
311
+
312
+ .mention .body {
313
+ font-style: italic;
314
+ }
315
+
316
+ .system .leading {
317
+ color: var(--fg-muted);
318
+ }