@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,515 @@
1
+ "use client";
2
+
3
+ import { useState, type ReactNode } from "react";
4
+ import { Icon } from "../Icon";
5
+ import { useTheme } from "../ThemeController";
6
+ import { hexToHue, isValidHex } from "../_color";
7
+ import type { FocusRingValue } from "../ThemeController";
8
+ import type {
9
+ DensityValue,
10
+ IconFamily,
11
+ MotionValue,
12
+ PaletteValue,
13
+ ShapeValue,
14
+ VoiceValue,
15
+ } from "../_data";
16
+ import {
17
+ DENSITIES,
18
+ HUE_PRESETS,
19
+ ICON_FAMILIES,
20
+ MOTIONS,
21
+ PALETTES,
22
+ SHAPES,
23
+ VOICES,
24
+ } from "../axisOptions";
25
+ import { THEME_PRESETS, type ThemeId } from "./wizardData";
26
+ import styles from "./Onboarding.module.css";
27
+
28
+ type Variant = "narrow" | "wide";
29
+
30
+ /** Voice 축 미리보기 샘플 문구. 같은 상황(저장 완료 토스트)을 3톤으로. */
31
+ const VOICE_SAMPLE: Record<VoiceValue, string> = {
32
+ formal: "저장되었습니다.",
33
+ neutral: "저장 완료",
34
+ casual: "저장했어요",
35
+ };
36
+
37
+ /** 테마가 "성격"으로 묶는 축: shape · density · motion · iconFamily.
38
+ * 테마 선택 시 이 축들은 preset 값으로 lock. palette/hue/mode는 유지(사용자 브랜드 색 자유). */
39
+ const THEME_LABEL: Record<Exclude<ThemeId, "none">, string> = {
40
+ pure: "Pure",
41
+ spare: "Spare",
42
+ expressive: "Expressive",
43
+ };
44
+
45
+ export function OnboardingAxisPanel({
46
+ variant = "narrow",
47
+ themeId = null,
48
+ }: {
49
+ variant?: Variant;
50
+ /** Summary 고급설정에서 전달. 'none'/null이면 모든 축 자유 조정. */
51
+ themeId?: ThemeId | null;
52
+ } = {}) {
53
+ const [hexDraft, setHexDraft] = useState("");
54
+ const [hexError, setHexError] = useState(false);
55
+ const {
56
+ theme,
57
+ toggleTheme,
58
+ palette,
59
+ setPalette,
60
+ hue,
61
+ setHue,
62
+ accentHue,
63
+ setAccentHue,
64
+ shape,
65
+ setShape,
66
+ density,
67
+ setDensity,
68
+ motionMode,
69
+ setMotionMode,
70
+ iconFamily,
71
+ setIconFamily,
72
+ focusRing,
73
+ setFocusRing,
74
+ voice,
75
+ setVoice,
76
+ reset,
77
+ } = useTheme();
78
+
79
+ const applyBrandHex = (raw: string) => {
80
+ const trimmed = raw.trim();
81
+ setHexDraft(trimmed);
82
+ if (!trimmed) {
83
+ setHexError(false);
84
+ return;
85
+ }
86
+ if (!isValidHex(trimmed)) {
87
+ setHexError(true);
88
+ return;
89
+ }
90
+ const nextHue = hexToHue(trimmed);
91
+ if (nextHue == null) {
92
+ setHexError(true);
93
+ return;
94
+ }
95
+ setHexError(false);
96
+ setHue(nextHue);
97
+ };
98
+
99
+ const wide = variant === "wide";
100
+
101
+ // 테마가 "성격"으로 묶는 축 lock 계산.
102
+ // themeId가 none/null이면 자유 조정. 그 외엔 THEME_PRESETS의 해당 축 값으로 고정.
103
+ const lockedTheme = themeId && themeId !== "none" ? themeId : null;
104
+ const preset = lockedTheme ? THEME_PRESETS[lockedTheme] : null;
105
+ const themeLabel = lockedTheme ? THEME_LABEL[lockedTheme] : null;
106
+ const lockedReason = themeLabel
107
+ ? `${themeLabel} 테마가 이 축을 결정합니다. 바꾸려면 "직접 조합"으로 변경하세요.`
108
+ : undefined;
109
+ const lockedShape = preset?.shape;
110
+ const lockedDensity = preset?.density;
111
+ const lockedMotion = preset?.motion;
112
+ const lockedIconFamily = preset?.iconFamily;
113
+
114
+ return (
115
+ <div
116
+ className={wide ? styles.axisAdv : undefined}
117
+ aria-label="9축 컨트롤"
118
+ >
119
+ {!wide && (
120
+ <div className={styles.axisRailHead}>
121
+ <span className={styles.axisRailTitle}>9축 컨트롤</span>
122
+ <button type="button" className={styles.axisRailReset} onClick={reset}>
123
+ reset
124
+ </button>
125
+ </div>
126
+ )}
127
+ {wide && (
128
+ <div className={styles.axisAdvHead}>
129
+ <button type="button" className={styles.axisRailReset} onClick={reset}>
130
+ 전체 초기화
131
+ </button>
132
+ </div>
133
+ )}
134
+
135
+ <AxisRow label="모드" wide={wide}>
136
+ <SegmentGroup
137
+ wide={wide}
138
+ options={[
139
+ { value: "light", label: "밝게", preview: wide && <ModeSwatch mode="light" /> },
140
+ { value: "dark", label: "어둡게", preview: wide && <ModeSwatch mode="dark" /> },
141
+ ]}
142
+ value={theme}
143
+ onChange={() => toggleTheme()}
144
+ />
145
+ </AxisRow>
146
+
147
+ <AxisRow label="팔레트" wide={wide}>
148
+ <SegmentGroup
149
+ wide={wide}
150
+ options={PALETTES.map((p) => ({
151
+ value: p,
152
+ label: p,
153
+ preview: wide && <PaletteSwatch palette={p} hue={hue} />,
154
+ }))}
155
+ value={palette}
156
+ onChange={(v) => setPalette(v as PaletteValue)}
157
+ />
158
+ </AxisRow>
159
+
160
+ <AxisRow label="브랜드 색" wide={wide}>
161
+ <div className={wide ? styles.axisAdvHue : styles.hueBox}>
162
+ <div className={styles.huePresets}>
163
+ {HUE_PRESETS.map((h) => (
164
+ <button
165
+ type="button"
166
+ key={h.name}
167
+ className={styles.hueChip}
168
+ data-active={hue === h.value || undefined}
169
+ onClick={() => setHue(h.value)}
170
+ title={`${h.name} · ${h.value}°`}
171
+ >
172
+ <span
173
+ className={styles.hueSwatch}
174
+ style={{ background: `oklch(0.55 0.14 ${h.value})` }}
175
+ />
176
+ <span className={styles.hueLabel}>{h.name}</span>
177
+ </button>
178
+ ))}
179
+ </div>
180
+ <div className={styles.hueDial}>
181
+ <input
182
+ type="range"
183
+ min={0}
184
+ max={360}
185
+ value={hue}
186
+ onChange={(e) => setHue(Number(e.target.value))}
187
+ />
188
+ <span className={styles.hueValue}>{hue}°</span>
189
+ </div>
190
+ <div className={styles.hexRow}>
191
+ <span
192
+ className={styles.hexSwatch}
193
+ style={{
194
+ background:
195
+ isValidHex(hexDraft) && !hexError
196
+ ? hexDraft.startsWith("#")
197
+ ? hexDraft
198
+ : `#${hexDraft}`
199
+ : "transparent",
200
+ }}
201
+ aria-hidden
202
+ />
203
+ <input
204
+ type="text"
205
+ placeholder="#3B82F6"
206
+ value={hexDraft}
207
+ onChange={(e) => applyBrandHex(e.target.value)}
208
+ className={styles.hexInput}
209
+ aria-invalid={hexError || undefined}
210
+ aria-label="브랜드 hex"
211
+ />
212
+ </div>
213
+ <label className={styles.axisAdvCheck}>
214
+ <input
215
+ type="checkbox"
216
+ checked={accentHue != null}
217
+ onChange={(e) =>
218
+ setAccentHue(e.target.checked ? (hue + 60) % 360 : null)
219
+ }
220
+ />
221
+ 보조 색상 사용
222
+ </label>
223
+ {accentHue != null && (
224
+ <div className={styles.hueDial}>
225
+ <input
226
+ type="range"
227
+ min={0}
228
+ max={360}
229
+ value={accentHue}
230
+ onChange={(e) => setAccentHue(Number(e.target.value))}
231
+ />
232
+ <span className={styles.hueValue}>{accentHue}°</span>
233
+ </div>
234
+ )}
235
+ </div>
236
+ </AxisRow>
237
+
238
+ <AxisRow label="모양" wide={wide} lockedBy={lockedShape ? themeLabel ?? undefined : undefined}>
239
+ <SegmentGroup
240
+ wide={wide}
241
+ options={SHAPES.map((s) => ({
242
+ value: s,
243
+ label: s,
244
+ preview: wide && <ShapePreview shape={s} />,
245
+ }))}
246
+ value={shape}
247
+ onChange={(v) => setShape(v as ShapeValue)}
248
+ lockedValue={lockedShape}
249
+ lockedReason={lockedReason}
250
+ />
251
+ </AxisRow>
252
+
253
+ <AxisRow label="정보 밀도" wide={wide} lockedBy={lockedDensity ? themeLabel ?? undefined : undefined}>
254
+ <SegmentGroup
255
+ wide={wide}
256
+ options={DENSITIES.map((d) => ({
257
+ value: d,
258
+ label: d,
259
+ preview: wide && <DensityMini density={d} />,
260
+ }))}
261
+ value={density}
262
+ onChange={(v) => setDensity(v as DensityValue)}
263
+ lockedValue={lockedDensity}
264
+ lockedReason={lockedReason}
265
+ />
266
+ </AxisRow>
267
+
268
+ <AxisRow label="모션" wide={wide} lockedBy={lockedMotion ? themeLabel ?? undefined : undefined}>
269
+ <SegmentGroup
270
+ wide={wide}
271
+ options={MOTIONS.map((m) => ({
272
+ value: m,
273
+ label: m,
274
+ preview: wide && <MotionPreview motion={m} />,
275
+ }))}
276
+ value={motionMode}
277
+ onChange={(v) => setMotionMode(v as MotionValue)}
278
+ lockedValue={lockedMotion}
279
+ lockedReason={lockedReason}
280
+ />
281
+ </AxisRow>
282
+
283
+ <AxisRow label="보이스" wide={wide}>
284
+ {wide ? (
285
+ <div className={styles.voiceGroup} role="radiogroup">
286
+ {VOICES.map((v) => (
287
+ <button
288
+ key={v}
289
+ type="button"
290
+ className={styles.voiceItem}
291
+ data-active={voice === v || undefined}
292
+ onClick={() => setVoice(v)}
293
+ role="radio"
294
+ aria-checked={voice === v}
295
+ >
296
+ <span className={styles.voiceSample}>&ldquo;{VOICE_SAMPLE[v]}&rdquo;</span>
297
+ <span className={styles.voiceCode}>{v}</span>
298
+ </button>
299
+ ))}
300
+ </div>
301
+ ) : (
302
+ <SegmentGroup
303
+ wide={wide}
304
+ options={VOICES.map((v) => ({ value: v, label: v }))}
305
+ value={voice}
306
+ onChange={(v) => setVoice(v as VoiceValue)}
307
+ />
308
+ )}
309
+ </AxisRow>
310
+
311
+ <AxisRow label="아이콘 세트" wide={wide} lockedBy={lockedIconFamily ? themeLabel ?? undefined : undefined}>
312
+ <SegmentGroup
313
+ wide={wide}
314
+ options={ICON_FAMILIES.map((f) => ({
315
+ value: f,
316
+ label: f,
317
+ preview: wide && <IconFamilyPreview family={f} />,
318
+ }))}
319
+ value={iconFamily}
320
+ onChange={(v) => setIconFamily(v as IconFamily)}
321
+ lockedValue={lockedIconFamily}
322
+ lockedReason={lockedReason}
323
+ />
324
+ </AxisRow>
325
+
326
+ <AxisRow label="포커스 링" wide={wide}>
327
+ <SegmentGroup
328
+ wide={wide}
329
+ options={[
330
+ { value: "on", label: "표시", preview: wide && <FocusRingPreview on /> },
331
+ { value: "off", label: "숨김", preview: wide && <FocusRingPreview on={false} /> },
332
+ ]}
333
+ value={focusRing}
334
+ onChange={(v) => setFocusRing(v as FocusRingValue)}
335
+ />
336
+ </AxisRow>
337
+ </div>
338
+ );
339
+ }
340
+
341
+ function AxisRow({
342
+ label,
343
+ wide,
344
+ lockedBy,
345
+ children,
346
+ }: {
347
+ label: string;
348
+ wide: boolean;
349
+ /** 테마가 이 축을 잠근 경우 테마 라벨(예: "Spare"). 없으면 자유 조정. */
350
+ lockedBy?: string;
351
+ children: ReactNode;
352
+ }) {
353
+ const title = lockedBy
354
+ ? `${lockedBy} 테마가 이 축을 결정합니다. 바꾸려면 디자인 스타일을 "직접 조합"으로 변경하세요.`
355
+ : undefined;
356
+ return (
357
+ <div className={wide ? styles.axisAdvRow : styles.axisRow}>
358
+ <span className={wide ? styles.axisAdvLabel : styles.axisLabel}>
359
+ {label}
360
+ {wide && lockedBy && (
361
+ <span className={styles.axisLockBadge} title={title}>
362
+ {lockedBy} 결정
363
+ </span>
364
+ )}
365
+ </span>
366
+ <div
367
+ className={wide ? styles.axisAdvControl : undefined}
368
+ title={title}
369
+ aria-disabled={lockedBy ? true : undefined}
370
+ >
371
+ {children}
372
+ </div>
373
+ </div>
374
+ );
375
+ }
376
+
377
+ function SegmentGroup<T extends string>({
378
+ options,
379
+ value,
380
+ onChange,
381
+ wide,
382
+ lockedValue,
383
+ lockedReason,
384
+ }: {
385
+ options: {
386
+ value: T;
387
+ label: string;
388
+ /** wide variant에서 label 아래에 작게 들어가는 보조 텍스트 (예: 코드명). */
389
+ sublabel?: string;
390
+ preview?: ReactNode | false;
391
+ }[];
392
+ value: T;
393
+ onChange: (v: T) => void;
394
+ wide: boolean;
395
+ /** 지정된 경우 이 값만 활성, 나머지는 disabled. */
396
+ lockedValue?: T;
397
+ /** disabled 아이템 hover 시 보여줄 이유 문구. */
398
+ lockedReason?: string;
399
+ }) {
400
+ return (
401
+ <div className={wide ? styles.segmentWide : styles.segment} role="radiogroup">
402
+ {options.map((o) => {
403
+ const disabled = lockedValue != null && o.value !== lockedValue;
404
+ return (
405
+ <button
406
+ key={o.value}
407
+ type="button"
408
+ className={wide ? styles.segmentWideItem : styles.segmentItem}
409
+ data-active={value === o.value || undefined}
410
+ data-disabled={disabled || undefined}
411
+ disabled={disabled}
412
+ title={disabled ? lockedReason : undefined}
413
+ onClick={() => !disabled && onChange(o.value)}
414
+ role="radio"
415
+ aria-checked={value === o.value}
416
+ >
417
+ {wide ? (
418
+ <>
419
+ {o.preview && (
420
+ <span className={styles.segmentWidePreview}>{o.preview}</span>
421
+ )}
422
+ <span className={styles.segmentWideLabel}>{o.label}</span>
423
+ {o.sublabel && (
424
+ <span className={styles.segmentWideSublabel}>{o.sublabel}</span>
425
+ )}
426
+ </>
427
+ ) : (
428
+ o.label
429
+ )}
430
+ </button>
431
+ );
432
+ })}
433
+ </div>
434
+ );
435
+ }
436
+
437
+ // ── Preview atoms ──────────────────────────────────────
438
+
439
+ function ModeSwatch({ mode }: { mode: "light" | "dark" }) {
440
+ return (
441
+ <span
442
+ className={styles.modeSwatch}
443
+ data-mode={mode}
444
+ aria-hidden
445
+ />
446
+ );
447
+ }
448
+
449
+ function PaletteSwatch({ palette, hue }: { palette: PaletteValue; hue: number }) {
450
+ // palette 강도 ≈ chroma/lightness로 근사. 실제 토큰값과 완전 일치 아니라도 비주얼 시그널로 충분.
451
+ const spec: Record<PaletteValue, { l: number; c: number }> = {
452
+ mono: { l: 0.52, c: 0 },
453
+ vivid: { l: 0.58, c: 0.18 },
454
+ standard: { l: 0.55, c: 0.14 },
455
+ calm: { l: 0.52, c: 0.07 },
456
+ pastel: { l: 0.82, c: 0.06 },
457
+ };
458
+ const { l, c } = spec[palette];
459
+ return (
460
+ <span
461
+ className={styles.paletteSwatch}
462
+ style={{ background: `oklch(${l} ${c} ${hue})` }}
463
+ aria-hidden
464
+ />
465
+ );
466
+ }
467
+
468
+ function ShapePreview({ shape }: { shape: ShapeValue }) {
469
+ return (
470
+ <span
471
+ className={styles.shapePreview}
472
+ data-shape={shape}
473
+ aria-hidden
474
+ />
475
+ );
476
+ }
477
+
478
+ function DensityMini({ density }: { density: DensityValue }) {
479
+ return (
480
+ <span className={styles.densityMini} data-density={density} aria-hidden>
481
+ <span />
482
+ <span />
483
+ <span />
484
+ </span>
485
+ );
486
+ }
487
+
488
+ function MotionPreview({ motion }: { motion: MotionValue }) {
489
+ return (
490
+ <span className={styles.motionPreview} aria-hidden>
491
+ <span className={styles.motionDot} data-motion={motion} />
492
+ </span>
493
+ );
494
+ }
495
+
496
+ /** family 특색이 가장 잘 보이는 세 아이콘 트리오. 셋 다 세 family 모두 지원하는 범용 세트. */
497
+ const ICON_TRIO = ["heart", "bell", "gear"] as const;
498
+
499
+ function IconFamilyPreview({ family }: { family: IconFamily }) {
500
+ return (
501
+ <span className={styles.iconTrio} aria-hidden>
502
+ {ICON_TRIO.map((n) => (
503
+ <Icon key={n} name={n} family={family} size={16} />
504
+ ))}
505
+ </span>
506
+ );
507
+ }
508
+
509
+ function FocusRingPreview({ on }: { on: boolean }) {
510
+ return (
511
+ <span className={styles.focusRingPreview} aria-hidden>
512
+ <span className={styles.focusRingButton} data-on={on || undefined} />
513
+ </span>
514
+ );
515
+ }