moru-krds-react 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 (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/dist/accordion/index.d.ts +63 -0
  4. package/dist/accordion/index.d.ts.map +1 -0
  5. package/dist/accordion/index.js +145 -0
  6. package/dist/accordion/index.js.map +1 -0
  7. package/dist/alert/index.d.ts +44 -0
  8. package/dist/alert/index.d.ts.map +1 -0
  9. package/dist/alert/index.js +66 -0
  10. package/dist/alert/index.js.map +1 -0
  11. package/dist/avatar/index.d.ts +102 -0
  12. package/dist/avatar/index.d.ts.map +1 -0
  13. package/dist/avatar/index.js +146 -0
  14. package/dist/avatar/index.js.map +1 -0
  15. package/dist/badge/index.d.ts +49 -0
  16. package/dist/badge/index.d.ts.map +1 -0
  17. package/dist/badge/index.js +41 -0
  18. package/dist/badge/index.js.map +1 -0
  19. package/dist/breadcrumb/index.d.ts +43 -0
  20. package/dist/breadcrumb/index.d.ts.map +1 -0
  21. package/dist/breadcrumb/index.js +43 -0
  22. package/dist/breadcrumb/index.js.map +1 -0
  23. package/dist/button/index.d.ts +78 -0
  24. package/dist/button/index.d.ts.map +1 -0
  25. package/dist/button/index.js +83 -0
  26. package/dist/button/index.js.map +1 -0
  27. package/dist/checkbox/index.d.ts +97 -0
  28. package/dist/checkbox/index.d.ts.map +1 -0
  29. package/dist/checkbox/index.js +110 -0
  30. package/dist/checkbox/index.js.map +1 -0
  31. package/dist/combobox/index.d.ts +135 -0
  32. package/dist/combobox/index.d.ts.map +1 -0
  33. package/dist/combobox/index.js +416 -0
  34. package/dist/combobox/index.js.map +1 -0
  35. package/dist/context-menu/index.d.ts +94 -0
  36. package/dist/context-menu/index.d.ts.map +1 -0
  37. package/dist/context-menu/index.js +399 -0
  38. package/dist/context-menu/index.js.map +1 -0
  39. package/dist/critical-alert/index.d.ts +39 -0
  40. package/dist/critical-alert/index.d.ts.map +1 -0
  41. package/dist/critical-alert/index.js +50 -0
  42. package/dist/critical-alert/index.js.map +1 -0
  43. package/dist/data-table/index.d.ts +94 -0
  44. package/dist/data-table/index.d.ts.map +1 -0
  45. package/dist/data-table/index.js +128 -0
  46. package/dist/data-table/index.js.map +1 -0
  47. package/dist/disclosure/index.d.ts +35 -0
  48. package/dist/disclosure/index.d.ts.map +1 -0
  49. package/dist/disclosure/index.js +54 -0
  50. package/dist/disclosure/index.js.map +1 -0
  51. package/dist/drawer/index.d.ts +106 -0
  52. package/dist/drawer/index.d.ts.map +1 -0
  53. package/dist/drawer/index.js +352 -0
  54. package/dist/drawer/index.js.map +1 -0
  55. package/dist/empty-state/index.d.ts +86 -0
  56. package/dist/empty-state/index.d.ts.map +1 -0
  57. package/dist/empty-state/index.js +138 -0
  58. package/dist/empty-state/index.js.map +1 -0
  59. package/dist/env.d.js +1 -0
  60. package/dist/env.d.js.map +1 -0
  61. package/dist/field/index.d.ts +149 -0
  62. package/dist/field/index.d.ts.map +1 -0
  63. package/dist/field/index.js +121 -0
  64. package/dist/field/index.js.map +1 -0
  65. package/dist/file-upload/index.d.ts +140 -0
  66. package/dist/file-upload/index.d.ts.map +1 -0
  67. package/dist/file-upload/index.js +249 -0
  68. package/dist/file-upload/index.js.map +1 -0
  69. package/dist/footer/index.d.ts +155 -0
  70. package/dist/footer/index.d.ts.map +1 -0
  71. package/dist/footer/index.js +186 -0
  72. package/dist/footer/index.js.map +1 -0
  73. package/dist/header/index.d.ts +170 -0
  74. package/dist/header/index.d.ts.map +1 -0
  75. package/dist/header/index.js +258 -0
  76. package/dist/header/index.js.map +1 -0
  77. package/dist/identifier/index.d.ts +38 -0
  78. package/dist/identifier/index.d.ts.map +1 -0
  79. package/dist/identifier/index.js +55 -0
  80. package/dist/identifier/index.js.map +1 -0
  81. package/dist/in-page-navigation/index.d.ts +71 -0
  82. package/dist/in-page-navigation/index.d.ts.map +1 -0
  83. package/dist/in-page-navigation/index.js +137 -0
  84. package/dist/in-page-navigation/index.js.map +1 -0
  85. package/dist/index.d.ts +57 -0
  86. package/dist/index.d.ts.map +1 -0
  87. package/dist/index.js +47 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/input-group/index.d.ts +66 -0
  90. package/dist/input-group/index.d.ts.map +1 -0
  91. package/dist/input-group/index.js +85 -0
  92. package/dist/input-group/index.js.map +1 -0
  93. package/dist/lib/krds.d.ts +45 -0
  94. package/dist/lib/krds.d.ts.map +1 -0
  95. package/dist/lib/krds.js +11 -0
  96. package/dist/lib/krds.js.map +1 -0
  97. package/dist/lib/link.d.ts +34 -0
  98. package/dist/lib/link.d.ts.map +1 -0
  99. package/dist/lib/link.js +8 -0
  100. package/dist/lib/link.js.map +1 -0
  101. package/dist/main-menu/index.d.ts +123 -0
  102. package/dist/main-menu/index.d.ts.map +1 -0
  103. package/dist/main-menu/index.js +613 -0
  104. package/dist/main-menu/index.js.map +1 -0
  105. package/dist/masthead/index.d.ts +25 -0
  106. package/dist/masthead/index.d.ts.map +1 -0
  107. package/dist/masthead/index.js +21 -0
  108. package/dist/masthead/index.js.map +1 -0
  109. package/dist/modal/index.d.ts +105 -0
  110. package/dist/modal/index.d.ts.map +1 -0
  111. package/dist/modal/index.js +285 -0
  112. package/dist/modal/index.js.map +1 -0
  113. package/dist/pagination/index.d.ts +58 -0
  114. package/dist/pagination/index.d.ts.map +1 -0
  115. package/dist/pagination/index.js +82 -0
  116. package/dist/pagination/index.js.map +1 -0
  117. package/dist/progress/index.d.ts +58 -0
  118. package/dist/progress/index.d.ts.map +1 -0
  119. package/dist/progress/index.js +125 -0
  120. package/dist/progress/index.js.map +1 -0
  121. package/dist/radio/index.d.ts +111 -0
  122. package/dist/radio/index.d.ts.map +1 -0
  123. package/dist/radio/index.js +136 -0
  124. package/dist/radio/index.js.map +1 -0
  125. package/dist/resize/index.d.ts +118 -0
  126. package/dist/resize/index.d.ts.map +1 -0
  127. package/dist/resize/index.js +342 -0
  128. package/dist/resize/index.js.map +1 -0
  129. package/dist/scroll-area/index.d.ts +40 -0
  130. package/dist/scroll-area/index.d.ts.map +1 -0
  131. package/dist/scroll-area/index.js +52 -0
  132. package/dist/scroll-area/index.js.map +1 -0
  133. package/dist/select/index.d.ts +67 -0
  134. package/dist/select/index.d.ts.map +1 -0
  135. package/dist/select/index.js +61 -0
  136. package/dist/select/index.js.map +1 -0
  137. package/dist/separator/index.d.ts +47 -0
  138. package/dist/separator/index.d.ts.map +1 -0
  139. package/dist/separator/index.js +61 -0
  140. package/dist/separator/index.js.map +1 -0
  141. package/dist/side-navigation/index.d.ts +78 -0
  142. package/dist/side-navigation/index.d.ts.map +1 -0
  143. package/dist/side-navigation/index.js +188 -0
  144. package/dist/side-navigation/index.js.map +1 -0
  145. package/dist/skeleton/index.d.ts +76 -0
  146. package/dist/skeleton/index.d.ts.map +1 -0
  147. package/dist/skeleton/index.js +78 -0
  148. package/dist/skeleton/index.js.map +1 -0
  149. package/dist/skip-link/index.d.ts +46 -0
  150. package/dist/skip-link/index.d.ts.map +1 -0
  151. package/dist/skip-link/index.js +37 -0
  152. package/dist/skip-link/index.js.map +1 -0
  153. package/dist/spinner/index.d.ts +58 -0
  154. package/dist/spinner/index.d.ts.map +1 -0
  155. package/dist/spinner/index.js +36 -0
  156. package/dist/spinner/index.js.map +1 -0
  157. package/dist/step-indicator/index.d.ts +41 -0
  158. package/dist/step-indicator/index.d.ts.map +1 -0
  159. package/dist/step-indicator/index.js +63 -0
  160. package/dist/step-indicator/index.js.map +1 -0
  161. package/dist/structured-list/index.d.ts +150 -0
  162. package/dist/structured-list/index.d.ts.map +1 -0
  163. package/dist/structured-list/index.js +194 -0
  164. package/dist/structured-list/index.js.map +1 -0
  165. package/dist/tab/index.d.ts +108 -0
  166. package/dist/tab/index.d.ts.map +1 -0
  167. package/dist/tab/index.js +198 -0
  168. package/dist/tab/index.js.map +1 -0
  169. package/dist/table/index.d.ts +75 -0
  170. package/dist/table/index.d.ts.map +1 -0
  171. package/dist/table/index.js +91 -0
  172. package/dist/table/index.js.map +1 -0
  173. package/dist/tag/index.d.ts +71 -0
  174. package/dist/tag/index.d.ts.map +1 -0
  175. package/dist/tag/index.js +45 -0
  176. package/dist/tag/index.js.map +1 -0
  177. package/dist/text-input/index.d.ts +69 -0
  178. package/dist/text-input/index.d.ts.map +1 -0
  179. package/dist/text-input/index.js +70 -0
  180. package/dist/text-input/index.js.map +1 -0
  181. package/dist/text-list/index.d.ts +71 -0
  182. package/dist/text-list/index.d.ts.map +1 -0
  183. package/dist/text-list/index.js +79 -0
  184. package/dist/text-list/index.js.map +1 -0
  185. package/dist/textarea/index.d.ts +29 -0
  186. package/dist/textarea/index.d.ts.map +1 -0
  187. package/dist/textarea/index.js +69 -0
  188. package/dist/textarea/index.js.map +1 -0
  189. package/dist/toast/index.d.ts +76 -0
  190. package/dist/toast/index.d.ts.map +1 -0
  191. package/dist/toast/index.js +183 -0
  192. package/dist/toast/index.js.map +1 -0
  193. package/dist/tooltip/index.d.ts +84 -0
  194. package/dist/tooltip/index.d.ts.map +1 -0
  195. package/dist/tooltip/index.js +329 -0
  196. package/dist/tooltip/index.js.map +1 -0
  197. package/package.json +57 -0
  198. package/src/accordion/index.tsx +251 -0
  199. package/src/alert/index.tsx +111 -0
  200. package/src/avatar/index.tsx +272 -0
  201. package/src/badge/index.tsx +91 -0
  202. package/src/breadcrumb/index.tsx +125 -0
  203. package/src/button/index.tsx +190 -0
  204. package/src/checkbox/index.tsx +247 -0
  205. package/src/combobox/index.tsx +700 -0
  206. package/src/context-menu/index.tsx +623 -0
  207. package/src/critical-alert/index.tsx +102 -0
  208. package/src/data-table/index.tsx +289 -0
  209. package/src/disclosure/index.tsx +87 -0
  210. package/src/drawer/index.tsx +516 -0
  211. package/src/empty-state/index.tsx +256 -0
  212. package/src/env.d.ts +10 -0
  213. package/src/field/index.tsx +307 -0
  214. package/src/file-upload/index.tsx +492 -0
  215. package/src/footer/index.tsx +393 -0
  216. package/src/header/index.tsx +498 -0
  217. package/src/identifier/index.tsx +96 -0
  218. package/src/in-page-navigation/index.tsx +258 -0
  219. package/src/index.ts +72 -0
  220. package/src/input-group/index.tsx +197 -0
  221. package/src/lib/krds.ts +75 -0
  222. package/src/lib/link.tsx +35 -0
  223. package/src/main-menu/index.tsx +912 -0
  224. package/src/masthead/index.tsx +50 -0
  225. package/src/modal/index.tsx +457 -0
  226. package/src/pagination/index.tsx +198 -0
  227. package/src/progress/index.tsx +198 -0
  228. package/src/radio/index.tsx +283 -0
  229. package/src/resize/index.tsx +541 -0
  230. package/src/scroll-area/index.tsx +96 -0
  231. package/src/select/index.tsx +136 -0
  232. package/src/separator/index.tsx +107 -0
  233. package/src/side-navigation/index.tsx +336 -0
  234. package/src/skeleton/index.tsx +172 -0
  235. package/src/skip-link/index.tsx +89 -0
  236. package/src/spinner/index.tsx +98 -0
  237. package/src/step-indicator/index.tsx +122 -0
  238. package/src/structured-list/index.tsx +395 -0
  239. package/src/tab/index.tsx +348 -0
  240. package/src/table/index.tsx +186 -0
  241. package/src/tag/index.tsx +127 -0
  242. package/src/text-input/index.tsx +156 -0
  243. package/src/text-list/index.tsx +177 -0
  244. package/src/textarea/index.tsx +113 -0
  245. package/src/toast/index.tsx +311 -0
  246. package/src/tooltip/index.tsx +496 -0
  247. package/styles/krds-tailwind.css +335 -0
@@ -0,0 +1,416 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useId,
7
+ useMemo,
8
+ useRef,
9
+ useState
10
+ } from "react";
11
+ import { cx } from "../lib/krds.js";
12
+ function Combobox(props) {
13
+ const {
14
+ label,
15
+ labelledBy,
16
+ id: idProp,
17
+ size = "medium",
18
+ options,
19
+ groups,
20
+ value,
21
+ defaultValue,
22
+ onValueChange,
23
+ onSelect,
24
+ openOnFocus = true,
25
+ defaultOpen = false,
26
+ autoHighlight = false,
27
+ emptyMessage,
28
+ header,
29
+ footer,
30
+ listLabel = "\uC81C\uC548 \uBAA9\uB85D",
31
+ action,
32
+ searchLayout,
33
+ className,
34
+ inputClassName,
35
+ menuClassName,
36
+ onChange,
37
+ onFocus,
38
+ onKeyDown,
39
+ onBlur,
40
+ autoComplete = "off",
41
+ ref: forwardedRef,
42
+ ...rest
43
+ } = props;
44
+ const reactId = useId();
45
+ const id = idProp ?? `${reactId}-input`;
46
+ const listId = `${reactId}-list`;
47
+ const wrapRef = useRef(null);
48
+ const inputRef = useRef(null);
49
+ const [open, setOpen] = useState(defaultOpen);
50
+ const [activeIndex, setActiveIndex] = useState(defaultOpen && autoHighlight ? 0 : -1);
51
+ const [innerValue, setInnerValue] = useState(defaultValue ?? "");
52
+ const isControlled = value !== void 0;
53
+ const text = isControlled ? value : innerValue;
54
+ const setText = useCallback(
55
+ (next) => {
56
+ if (!isControlled) setInnerValue(next);
57
+ onValueChange?.(next);
58
+ },
59
+ [isControlled, onValueChange]
60
+ );
61
+ const sections = useMemo(
62
+ () => groups?.length ? groups : [{ options: options ?? [] }],
63
+ [groups, options]
64
+ );
65
+ const flat = useMemo(
66
+ () => sections.flatMap(
67
+ (section, si) => section.options.map((option, oi) => ({
68
+ ...option,
69
+ id: `${reactId}-o-${si}-${oi}`
70
+ }))
71
+ ),
72
+ [sections, reactId]
73
+ );
74
+ const offsets = useMemo(
75
+ () => sections.map(
76
+ (_, si) => sections.slice(0, si).reduce((n, s) => n + s.options.length, 0)
77
+ ),
78
+ [sections]
79
+ );
80
+ const expanded = open && (flat.length > 0 || emptyMessage != null && text.length > 0);
81
+ const activeOption = expanded && activeIndex >= 0 ? flat[activeIndex] : void 0;
82
+ const closeMenu = useCallback(() => {
83
+ setOpen(false);
84
+ setActiveIndex(-1);
85
+ }, []);
86
+ const move = useCallback(
87
+ (delta) => {
88
+ if (!flat.length) return;
89
+ setActiveIndex((prev) => {
90
+ let next = prev < 0 ? delta > 0 ? -1 : 0 : prev;
91
+ for (let step = 0; step < flat.length; step += 1) {
92
+ next = (next + delta + flat.length) % flat.length;
93
+ if (!flat[next].disabled) return next;
94
+ }
95
+ return prev;
96
+ });
97
+ },
98
+ [flat]
99
+ );
100
+ const select = useCallback(
101
+ (option) => {
102
+ if (option.disabled) return;
103
+ setText(option.value);
104
+ closeMenu();
105
+ onSelect?.(option);
106
+ inputRef.current?.focus();
107
+ },
108
+ [setText, closeMenu, onSelect]
109
+ );
110
+ useEffect(() => {
111
+ if (!activeOption) return;
112
+ document.getElementById(activeOption.id)?.scrollIntoView({ block: "nearest" });
113
+ }, [activeOption]);
114
+ useEffect(() => {
115
+ if (!expanded) return;
116
+ const onPointerDown = (e) => {
117
+ if (!wrapRef.current?.contains(e.target)) closeMenu();
118
+ };
119
+ document.addEventListener("pointerdown", onPointerDown);
120
+ return () => document.removeEventListener("pointerdown", onPointerDown);
121
+ }, [expanded, closeMenu]);
122
+ const handleKeyDown = (e) => {
123
+ onKeyDown?.(e);
124
+ if (e.defaultPrevented) return;
125
+ if (e.nativeEvent.isComposing) return;
126
+ switch (e.key) {
127
+ case "ArrowDown":
128
+ e.preventDefault();
129
+ if (!expanded) {
130
+ setOpen(true);
131
+ setActiveIndex(flat.findIndex((o) => !o.disabled));
132
+ } else {
133
+ move(1);
134
+ }
135
+ break;
136
+ case "ArrowUp":
137
+ e.preventDefault();
138
+ if (!expanded) {
139
+ setOpen(true);
140
+ setActiveIndex(flat.length - 1);
141
+ } else {
142
+ move(-1);
143
+ }
144
+ break;
145
+ case "Enter":
146
+ if (activeOption) {
147
+ e.preventDefault();
148
+ select(activeOption);
149
+ } else {
150
+ closeMenu();
151
+ }
152
+ break;
153
+ case "Escape":
154
+ if (expanded) {
155
+ e.preventDefault();
156
+ e.stopPropagation();
157
+ closeMenu();
158
+ }
159
+ break;
160
+ case "Tab":
161
+ closeMenu();
162
+ break;
163
+ default:
164
+ break;
165
+ }
166
+ };
167
+ const handleBlur = (e) => {
168
+ onBlur?.(e);
169
+ const next = e.relatedTarget;
170
+ if (next && !wrapRef.current?.contains(next)) closeMenu();
171
+ };
172
+ const status = !expanded ? "" : flat.length ? `\uC81C\uC548 ${flat.length}\uAC74` : "\uC81C\uC548 \uC5C6\uC74C";
173
+ const setInputRef = (node) => {
174
+ inputRef.current = node;
175
+ if (typeof forwardedRef === "function") forwardedRef(node);
176
+ else if (forwardedRef)
177
+ forwardedRef.current = node;
178
+ };
179
+ const renderItem = (option, index) => /* @__PURE__ */ jsx(
180
+ Item,
181
+ {
182
+ option,
183
+ index,
184
+ active: index === activeIndex,
185
+ onPick: select,
186
+ onHover: setActiveIndex
187
+ },
188
+ option.id
189
+ );
190
+ const row = /* @__PURE__ */ jsxs(Fragment, { children: [
191
+ /* @__PURE__ */ jsx(
192
+ "input",
193
+ {
194
+ ref: setInputRef,
195
+ id,
196
+ type: "text",
197
+ className: cx("krds-input", size, inputClassName),
198
+ role: "combobox",
199
+ "aria-label": label,
200
+ "aria-labelledby": labelledBy,
201
+ "aria-expanded": expanded,
202
+ "aria-controls": listId,
203
+ "aria-autocomplete": "list",
204
+ "aria-activedescendant": activeOption?.id,
205
+ autoComplete,
206
+ value: text,
207
+ onChange: (e) => {
208
+ onChange?.(e);
209
+ setText(e.target.value);
210
+ setOpen(true);
211
+ setActiveIndex(autoHighlight ? 0 : -1);
212
+ },
213
+ onFocus: (e) => {
214
+ onFocus?.(e);
215
+ if (openOnFocus) setOpen(true);
216
+ },
217
+ onKeyDown: handleKeyDown,
218
+ onBlur: handleBlur,
219
+ ...rest
220
+ }
221
+ ),
222
+ /* @__PURE__ */ jsx(
223
+ "div",
224
+ {
225
+ className: cx("drop-menu w-full mt-1 before:hidden", menuClassName),
226
+ style: expanded ? { display: "block" } : void 0,
227
+ onMouseDown: (e) => e.preventDefault(),
228
+ children: /* @__PURE__ */ jsxs("div", { className: "drop-in", children: [
229
+ header && /* @__PURE__ */ jsx("div", { className: "drop-top", children: header }),
230
+ groups?.length ? /* @__PURE__ */ jsx(
231
+ "div",
232
+ {
233
+ id: listId,
234
+ role: "listbox",
235
+ "aria-label": listLabel,
236
+ className: "w-full max-h-80 overflow-y-auto flex flex-col gap-2",
237
+ children: sections.map((section, si) => /* @__PURE__ */ jsxs(
238
+ "div",
239
+ {
240
+ role: "group",
241
+ "aria-labelledby": `${reactId}-g-${si}`,
242
+ className: "w-full",
243
+ children: [
244
+ /* @__PURE__ */ jsx(
245
+ "h3",
246
+ {
247
+ id: `${reactId}-g-${si}`,
248
+ className: "px-4 py-1 text-xs font-bold text-fg-subtle",
249
+ children: section.label
250
+ }
251
+ ),
252
+ /* @__PURE__ */ jsx("ul", { className: "drop-list", role: "none", children: section.options.map(
253
+ (_, oi) => renderItem(flat[offsets[si] + oi], offsets[si] + oi)
254
+ ) })
255
+ ]
256
+ },
257
+ section.label ?? si
258
+ ))
259
+ }
260
+ ) : /* @__PURE__ */ jsx(
261
+ "ul",
262
+ {
263
+ id: listId,
264
+ role: "listbox",
265
+ "aria-label": listLabel,
266
+ className: "drop-list max-h-80 overflow-y-auto",
267
+ children: flat.map(renderItem)
268
+ }
269
+ ),
270
+ flat.length === 0 && emptyMessage != null && /* @__PURE__ */ jsx("p", { className: "w-full px-4 py-3 text-sm text-fg-subtle", children: emptyMessage }),
271
+ footer && /* @__PURE__ */ jsx("div", { className: "drop-bottom", children: footer })
272
+ ] })
273
+ }
274
+ ),
275
+ action
276
+ ] });
277
+ return /* @__PURE__ */ jsxs(
278
+ "div",
279
+ {
280
+ ref: wrapRef,
281
+ className: cx("krds-drop-wrap drop-left w-full", className),
282
+ children: [
283
+ searchLayout ? /* @__PURE__ */ jsx("div", { className: "sch-input w-full", children: row }) : row,
284
+ /* @__PURE__ */ jsx("span", { className: "sr-only", role: "status", "aria-live": "polite", children: status })
285
+ ]
286
+ }
287
+ );
288
+ }
289
+ function Item({
290
+ option,
291
+ active,
292
+ index,
293
+ onPick,
294
+ onHover
295
+ }) {
296
+ return /* @__PURE__ */ jsxs(
297
+ "li",
298
+ {
299
+ id: option.id,
300
+ role: "option",
301
+ "aria-selected": active,
302
+ "aria-disabled": option.disabled || void 0,
303
+ className: cx(
304
+ "item-link",
305
+ active && "active",
306
+ option.disabled ? "text-fg-disabled cursor-not-allowed" : "cursor-pointer"
307
+ ),
308
+ onClick: () => onPick(option),
309
+ onMouseMove: () => {
310
+ if (!option.disabled) onHover(index);
311
+ },
312
+ children: [
313
+ option.icon && /* @__PURE__ */ jsx("i", { className: `svg-icon ico-${option.icon}`, "aria-hidden": "true" }),
314
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: option.label ?? option.value }),
315
+ option.description && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm text-fg-subtle", children: option.description })
316
+ ]
317
+ }
318
+ );
319
+ }
320
+ function SearchCombobox({
321
+ label = "\uAC80\uC0C9\uC5B4",
322
+ buttonLabel = "\uAC80\uC0C9",
323
+ clearLabel = "\uAC80\uC0C9\uC5B4 \uC804\uCCB4 \uC0AD\uC81C",
324
+ clearable = true,
325
+ onClear,
326
+ onSearch,
327
+ buttonType = "submit",
328
+ buttonProps,
329
+ value,
330
+ defaultValue,
331
+ onValueChange,
332
+ onSelect,
333
+ onKeyDown,
334
+ inputClassName,
335
+ ref,
336
+ ...rest
337
+ }) {
338
+ const inputRef = useRef(null);
339
+ const [innerValue, setInnerValue] = useState(defaultValue ?? "");
340
+ const text = value !== void 0 ? value : innerValue;
341
+ const handleValueChange = (next) => {
342
+ if (value === void 0) setInnerValue(next);
343
+ onValueChange?.(next);
344
+ };
345
+ const setRef = (node) => {
346
+ inputRef.current = node;
347
+ if (typeof ref === "function") ref(node);
348
+ else if (ref) ref.current = node;
349
+ };
350
+ return /* @__PURE__ */ jsx(
351
+ Combobox,
352
+ {
353
+ ...rest,
354
+ ref: setRef,
355
+ label,
356
+ searchLayout: true,
357
+ value: text,
358
+ onValueChange: handleValueChange,
359
+ onSelect: (option) => {
360
+ onSelect?.(option);
361
+ onSearch?.(option.value);
362
+ },
363
+ onKeyDown: (e) => {
364
+ onKeyDown?.(e);
365
+ if (e.defaultPrevented || e.key !== "Enter" || e.nativeEvent.isComposing)
366
+ return;
367
+ if (e.currentTarget.getAttribute("aria-activedescendant")) return;
368
+ onSearch?.(e.currentTarget.value);
369
+ },
370
+ inputClassName: cx(clearable && "pr-20", inputClassName),
371
+ action: (
372
+ // `.sch-input .ico-search{position:absolute;top:50%;right:1.6rem;transform:translateY(-50%)}`
373
+ // 킷의 그 위치 규칙을 감싸개가 받는다. gap-2(8px)는 킷 `--krds-gap-3` 과 같은 값.
374
+ /* @__PURE__ */ jsxs("div", { className: "ico-search flex items-center gap-2", children: [
375
+ clearable && text.length > 0 && /* @__PURE__ */ jsxs(
376
+ "button",
377
+ {
378
+ type: "button",
379
+ className: "krds-btn medium icon",
380
+ onClick: () => {
381
+ handleValueChange("");
382
+ onClear?.();
383
+ inputRef.current?.focus();
384
+ },
385
+ children: [
386
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: clearLabel }),
387
+ /* @__PURE__ */ jsx("i", { className: "svg-icon ico-del", "aria-hidden": "true" })
388
+ ]
389
+ }
390
+ ),
391
+ /* @__PURE__ */ jsxs(
392
+ "button",
393
+ {
394
+ type: buttonType,
395
+ className: "krds-btn medium icon",
396
+ ...buttonProps,
397
+ onClick: (e) => {
398
+ buttonProps?.onClick?.(e);
399
+ onSearch?.(text);
400
+ },
401
+ children: [
402
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: buttonLabel }),
403
+ /* @__PURE__ */ jsx("i", { className: "svg-icon ico-sch", "aria-hidden": "true" })
404
+ ]
405
+ }
406
+ )
407
+ ] })
408
+ )
409
+ }
410
+ );
411
+ }
412
+ export {
413
+ Combobox,
414
+ SearchCombobox
415
+ };
416
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/combobox/index.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type FocusEvent as ReactFocusEvent,\n type KeyboardEvent as ReactKeyboardEvent,\n type ReactNode,\n} from \"react\";\nimport { cx, type KrdsFieldSize, type KrdsIcon } from \"../lib/krds\";\n\n/**\n * 콤보박스 — 입력하면 목록이 좁혀지는 자동완성. KRDS 킷에 없는 보충 부품이다.\n *\n * 껍데기는 킷 드롭다운(`krds-drop-wrap`), 동작은 WAI-ARIA 1.2 콤보박스 패턴이다.\n * ↓↑ 는 양 끝에서 순환하고 Esc 는 닫기만 하며 값은 건드리지 않는다.\n * 검색칸(돋보기 · 검색어 삭제 · 실행 버튼)이 필요하면 {@link SearchCombobox} 를 쓴다.\n * 최근·인기·제안처럼 종류가 갈리면 `options` 대신 `groups` 로 계층화한다(규격 요구).\n *\n * @example\n * <Combobox label=\"기관명\" options={hits} onSelect={(o) => setOrg(o.value)} />\n * <SearchCombobox\n * groups={[\n * { label: '최근 검색어', options: recent },\n * { label: '인기 검색어', options: popular },\n * ]}\n * emptyMessage=\"일치하는 검색어가 없습니다.\"\n * onSearch={run}\n * />\n *\n * 자세히: docs/krds/09-부품-노트.md#콤보박스\n */\n\n/* ────────────────────────────── 자료 모양 ────────────────────────────── */\n\n/** 제안 항목 하나. */\nexport type ComboboxOption = {\n /** 고르면 입력칸에 들어갈 글자. */\n value: string;\n /** 화면에 보일 것. 없으면 `value` 를 그대로 쓴다(일치 글자 강조를 넣을 자리). */\n label?: ReactNode;\n /** 오른쪽 끝 보조 설명(분류·결과 수 등). */\n description?: ReactNode;\n /** 앞 아이콘 이름(`ico-` 뺀 것). 최근 검색어에 `log`, 인기 검색어에 `like` 같은 식. */\n icon?: KrdsIcon;\n /** 보이되 고를 수 없다. ↑↓ 순회에서도 건너뛴다. */\n disabled?: boolean;\n};\n\n/** 제안 묶음. 규격이 요구하는 계층화의 단위다 — 최근 / 인기 / 제안이 각각 한 묶음이다. */\nexport type ComboboxGroup = {\n /** 묶음 제목. `<h3>` 으로 그리고 `role=\"group\"` 의 이름으로도 쓴다. */\n label: string;\n options: readonly ComboboxOption[];\n};\n\n/** 내부용 — 평평하게 편 항목. `id` 가 `aria-activedescendant` 로 가리킬 자리다. */\ntype FlatOption = ComboboxOption & { id: string };\n\n/* ────────────────────────────── Props ────────────────────────────── */\n\ntype ComboboxOwnProps = {\n /** 입력칸의 접근 가능한 이름. 보이는 `<label>` 이 있으면 `labelledBy` 를 대신 준다. */\n label?: string;\n /** 보이는 `<label>` 의 id. `label` 과 함께 줄 수 없다. */\n labelledBy?: string;\n /** `<input>` 의 id. 생략하면 `useId()` 로 만든다. */\n id?: string;\n /** 기본 medium(48px). 같은 줄의 버튼과 같은 값을 준다. */\n size?: KrdsFieldSize;\n\n /** 한 덩어리 목록. `groups` 를 함께 주면 `groups` 만 그린다. */\n options?: readonly ComboboxOption[];\n /** 제목이 붙은 묶음 목록(계층화). */\n groups?: readonly ComboboxGroup[];\n\n /** 제어 입력값. 주면 제어 부품이 된다. */\n value?: string;\n /** 비제어 초기값. */\n defaultValue?: string;\n /** 입력값이 바뀔 때(사람이 치거나 항목을 골랐을 때 모두). */\n onValueChange?: (value: string) => void;\n /** 항목을 골랐을 때. 입력값을 `option.value` 로 바꾼 **뒤** 부른다. */\n onSelect?: (option: ComboboxOption) => void;\n\n /** 초점이 들어오면 값이 비어 있어도 목록을 연다. 기본 true(규격의 Focusin 동작). */\n openOnFocus?: boolean;\n /** 처음부터 펼친 채로 그린다. 비제어 초깃값이라 사람이 닫으면 다시 열리지 않는다(문서·시각 시험용). */\n defaultOpen?: boolean;\n /** 열 때 첫 항목을 미리 활성으로 둔다. 기본 false — 치던 글자가 Enter 로 바뀌는 사고를 막는다. */\n autoHighlight?: boolean;\n\n /** 결과가 없을 때 보일 글. **글자를 한 자 이상 친 뒤에만** 뜬다. 주지 않으면 목록이 열리지 않는다. */\n emptyMessage?: ReactNode;\n /** 목록 위 고정 영역(킷 `.drop-top`). 「검색 도움말」 같은 것. */\n header?: ReactNode;\n /** 목록 아래 고정 영역(킷 `.drop-bottom`). 「검색 기록 전체 삭제」 같은 것. */\n footer?: ReactNode;\n\n /** 목록의 접근 가능한 이름. 기본 「제안 목록」. */\n listLabel?: string;\n /** 입력칸 오른쪽에 놓일 것(검색 버튼 등). 목록보다 뒤에 그려진다(규격의 읽기 순서). */\n action?: ReactNode;\n /** 입력칸 줄을 킷 `.sch-input`(돋보기 겹침 규칙)으로 감싼다. {@link SearchCombobox} 가 켠다. */\n searchLayout?: boolean;\n\n /** `.krds-drop-wrap` 감싸개에 붙는다. 바깥 여백·너비는 여기서 조절한다. */\n className?: string;\n /** `<input>` 에 붙는다. */\n inputClassName?: string;\n /** `.drop-menu` 에 붙는다. */\n menuClassName?: string;\n};\n\n/** `<input>` 에서 부품이 직접 다루는 것은 뺀다. `onSelect` 는 DOM 이벤트와 이름이 겹쳐 막았다. */\ntype ComboboxInputProps = Omit<\n ComponentPropsWithRef<\"input\">,\n | \"size\"\n | \"value\"\n | \"defaultValue\"\n | \"onSelect\"\n | \"role\"\n | \"children\"\n | \"list\"\n | \"type\"\n>;\n\ntype ComboboxBaseProps = ComboboxInputProps & ComboboxOwnProps;\n\n/** 이름 없는 콤보박스를 만들 수 없게 타입으로 막는다 — `label` 이나 `labelledBy` 중 하나는 필수다. */\nexport type ComboboxProps = ComboboxBaseProps &\n (\n | { label: string; labelledBy?: never }\n | { label?: never; labelledBy: string }\n );\n\n/* ────────────────────────────── 부품 ────────────────────────────── */\n\nexport function Combobox(props: ComboboxProps) {\n // 이름 유니온은 판별자가 없어 그대로 구조 분해가 안 된다. 넓은 쪽으로 한 번 받아 쓴다.\n const {\n label,\n labelledBy,\n id: idProp,\n size = \"medium\",\n options,\n groups,\n value,\n defaultValue,\n onValueChange,\n onSelect,\n openOnFocus = true,\n defaultOpen = false,\n autoHighlight = false,\n emptyMessage,\n header,\n footer,\n listLabel = \"제안 목록\",\n action,\n searchLayout,\n className,\n inputClassName,\n menuClassName,\n onChange,\n onFocus,\n onKeyDown,\n onBlur,\n autoComplete = \"off\",\n ref: forwardedRef,\n ...rest\n }: ComboboxBaseProps = props;\n\n const reactId = useId();\n const id = idProp ?? `${reactId}-input`;\n const listId = `${reactId}-list`;\n\n const wrapRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement | null>(null);\n\n // `defaultOpen` 은 초깃값일 뿐이다 — 닫은 뒤에는 보통 부품과 똑같이 움직인다.\n const [open, setOpen] = useState(defaultOpen);\n const [activeIndex, setActiveIndex] = useState(defaultOpen && autoHighlight ? 0 : -1);\n const [innerValue, setInnerValue] = useState(defaultValue ?? \"\");\n\n const isControlled = value !== undefined;\n const text = isControlled ? value : innerValue;\n\n const setText = useCallback(\n (next: string) => {\n if (!isControlled) setInnerValue(next);\n onValueChange?.(next);\n },\n [isControlled, onValueChange],\n );\n\n /** 묶음이 있으면 묶음, 없으면 제목 없는 묶음 하나. 아래 계산을 한 갈래로 만들려는 정규화다. */\n const sections = useMemo<\n ReadonlyArray<{ label?: string; options: readonly ComboboxOption[] }>\n >(\n () => (groups?.length ? groups : [{ options: options ?? [] }]),\n [groups, options],\n );\n\n /** 평평하게 편 항목. 이 순서가 곧 ↑↓ 순서다. `id` 는 `aria-activedescendant` 가 가리킨다. */\n const flat = useMemo<FlatOption[]>(\n () =>\n sections.flatMap((section, si) =>\n section.options.map((option, oi) => ({\n ...option,\n id: `${reactId}-o-${si}-${oi}`,\n })),\n ),\n [sections, reactId],\n );\n\n /**\n * 묶음 si 의 첫 항목이 `flat` 에서 몇 번째인지. 묶음별로 그릴 때 전역 번호를 되찾는 데 쓴다\n * (전역 번호가 곧 ↑↓ 순서라 묶음 안 번호로는 안 된다). 묶음은 서넛뿐이라 앞부분을 다시 세도 된다.\n */\n const offsets = useMemo(\n () =>\n sections.map((_, si) =>\n sections.slice(0, si).reduce((n, s) => n + s.options.length, 0),\n ),\n [sections],\n );\n\n /**\n * 빈 안내는 **친 글자가 있을 때만** 연다. 초점만으로 여는 자리(규격의 Focusin)는\n * 「인기 검색어·이전 검색 기록」을 보여 주는 자리라, 항목이 하나도 없으면 보여 줄 것이 없다 —\n * 예전에는 `emptyMessage` 만 주면 `options={[]}` 인 화면에서 초점을 줄 때마다 빈 상자가 떴다.\n */\n const expanded =\n open && (flat.length > 0 || (emptyMessage != null && text.length > 0));\n const activeOption =\n expanded && activeIndex >= 0 ? flat[activeIndex] : undefined;\n\n const closeMenu = useCallback(() => {\n setOpen(false);\n setActiveIndex(-1);\n }, []);\n\n /**\n * ↑↓ 이동. **양 끝에서 순환한다** — 규격 표가 못 박은 동작이다.\n * `disabled` 는 건너뛰고, 전부 disabled 면 한 바퀴만 돌고 제자리에 둔다.\n */\n const move = useCallback(\n (delta: number) => {\n if (!flat.length) return;\n setActiveIndex((prev) => {\n // 아직 아무 데도 없으면 ↓ 는 첫 항목, ↑ 는 마지막 항목에서 시작하게 자리를 잡아 둔다.\n let next = prev < 0 ? (delta > 0 ? -1 : 0) : prev;\n for (let step = 0; step < flat.length; step += 1) {\n next = (next + delta + flat.length) % flat.length;\n if (!flat[next].disabled) return next;\n }\n return prev;\n });\n },\n [flat],\n );\n\n /** 항목을 고른다. 입력값을 바꾸고 닫는다(규격: Click/Enter 는 그 항목으로 검색 실행). */\n const select = useCallback(\n (option: ComboboxOption) => {\n if (option.disabled) return;\n setText(option.value);\n closeMenu();\n onSelect?.(option);\n inputRef.current?.focus();\n },\n [setText, closeMenu, onSelect],\n );\n\n // 활성 항목이 스크롤 밖으로 나가지 않게 따라간다. `block:'nearest'` 라 필요할 때만 움직인다.\n useEffect(() => {\n if (!activeOption) return;\n document\n .getElementById(activeOption.id)\n ?.scrollIntoView({ block: \"nearest\" });\n }, [activeOption]);\n\n // 바깥을 누르면 닫는다. click 이 아니라 pointerdown 이라 드래그로 나가도 닫힌다.\n useEffect(() => {\n if (!expanded) return;\n const onPointerDown = (e: PointerEvent) => {\n if (!wrapRef.current?.contains(e.target as Node)) closeMenu();\n };\n document.addEventListener(\"pointerdown\", onPointerDown);\n return () => document.removeEventListener(\"pointerdown\", onPointerDown);\n }, [expanded, closeMenu]);\n\n const handleKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n\n // ★ 한글 조합 중에는 어떤 키도 가로채지 않는다(함정 4). 조합을 끝내는 Enter 가 검색을 실행해 버린다.\n if (e.nativeEvent.isComposing) return;\n\n switch (e.key) {\n case \"ArrowDown\":\n e.preventDefault();\n // 규격: 「방향키 ↓ → 제안 목록의 가장 첫 번째 요소로」. 닫혀 있으면 열면서 첫 항목으로 간다.\n if (!expanded) {\n setOpen(true);\n setActiveIndex(flat.findIndex((o) => !o.disabled));\n } else {\n move(1);\n }\n break;\n case \"ArrowUp\":\n e.preventDefault();\n if (!expanded) {\n setOpen(true);\n setActiveIndex(flat.length - 1);\n } else {\n move(-1);\n }\n break;\n case \"Enter\":\n // 활성 항목이 있을 때만 가로챈다 — 없으면 폼 제출(=검색 실행)이 그대로 지나가야 한다.\n if (activeOption) {\n e.preventDefault();\n select(activeOption);\n } else {\n closeMenu();\n }\n break;\n case \"Escape\":\n // 규격: 「창이 닫히면서 초점이 검색어 입력 필드로 이동」. 값은 건드리지 않는다(함정 3).\n // 초점은 이미 입력칸에 있으므로 닫기만 하면 규격을 만족한다.\n if (expanded) {\n e.preventDefault();\n e.stopPropagation();\n closeMenu();\n }\n break;\n case \"Tab\":\n // 열어 둔 채 다음 요소로 넘어가면 유령 상자가 남는다.\n closeMenu();\n break;\n default:\n break;\n }\n };\n\n // 부품 밖으로 초점이 나가면 닫는다. relatedTarget 이 없으면(브라우저 UI 로 이동) 그대로 둔다.\n const handleBlur = (e: ReactFocusEvent<HTMLInputElement>) => {\n onBlur?.(e);\n const next = e.relatedTarget as Node | null;\n if (next && !wrapRef.current?.contains(next)) closeMenu();\n };\n\n /**\n * 소리로 알리는 제안 개수(함정 5). 닫혀 있으면 빈 글자라 아무것도 읽지 않는다 —\n * 닫힘 자체는 알리지 않는다(모든 Esc 마다 떠드는 화면이 된다).\n */\n const status = !expanded\n ? \"\"\n : flat.length\n ? `제안 ${flat.length}건`\n : \"제안 없음\";\n\n /** 바깥에서 온 ref 와 부품이 쓰는 ref 를 함께 채운다. React 19 라 forwardRef 가 필요 없다. */\n const setInputRef = (node: HTMLInputElement | null) => {\n inputRef.current = node;\n if (typeof forwardedRef === \"function\") forwardedRef(node);\n else if (forwardedRef)\n (forwardedRef as { current: HTMLInputElement | null }).current = node;\n };\n\n const renderItem = (option: FlatOption, index: number) => (\n <Item\n key={option.id}\n option={option}\n index={index}\n active={index === activeIndex}\n onPick={select}\n onHover={setActiveIndex}\n />\n );\n\n const row = (\n <>\n <input\n ref={setInputRef}\n id={id}\n // `search` 로 두면 웹킷이 제 지우기 버튼을 그려 돋보기와 겹친다 — 킷 샘플도 text 다.\n type=\"text\"\n className={cx(\"krds-input\", size, inputClassName)}\n /* ── WAI-ARIA 1.2 콤보박스 배선 ── */\n role=\"combobox\"\n aria-label={label}\n aria-labelledby={labelledBy}\n aria-expanded={expanded}\n aria-controls={listId}\n aria-autocomplete=\"list\"\n aria-activedescendant={activeOption?.id}\n // 브라우저 자동완성 상자가 우리 목록 위에 겹치는 것을 막는다.\n autoComplete={autoComplete}\n value={text}\n onChange={(e) => {\n onChange?.(e);\n setText(e.target.value);\n // 규격: 「Keyup — 한 글자 이상 입력되면 … 검색어 제안으로 변경된다」. 칠 때마다 연다.\n setOpen(true);\n setActiveIndex(autoHighlight ? 0 : -1);\n }}\n onFocus={(e) => {\n onFocus?.(e);\n // 규격: 「Focusin — 값이 비어 있으면 인기 검색어와 이전 검색 기록이 제공된다」.\n if (openOnFocus) setOpen(true);\n }}\n onKeyDown={handleKeyDown}\n onBlur={handleBlur}\n {...rest}\n />\n\n {/*\n ★ 목록은 입력칸의 「다음 요소」다 — 규격의 읽기 순서 요구(함정 1). action 보다 앞에 둔다.\n 여는 방법은 인라인 display:block — 킷에 여는 클래스가 없다(보충 1).\n onMouseDown 을 막아 항목을 눌러도 입력칸이 초점을 잃지 않게 한다(닫혔다 열리는 깜빡임 방지).\n */}\n <div\n className={cx(\"drop-menu w-full mt-1 before:hidden\", menuClassName)}\n style={expanded ? { display: \"block\" } : undefined}\n onMouseDown={(e) => e.preventDefault()}\n >\n <div className=\"drop-in\">\n {header && <div className=\"drop-top\">{header}</div>}\n\n {/*\n 역할 구조: listbox → (group → option) 또는 listbox → option.\n 묶음이 있을 땐 감싸개를 div(listbox)로 두고 `<ul class=\"drop-list\">` 는 role=\"none\" 으로 비운다 —\n listbox 의 자식은 option 이나 group 만 허용되므로 ul/li 의 목록 역할을 지워야 한다.\n 제목 `<h3>` 은 group 안에 두고 aria-labelledby 로 그 group 의 이름이 되게 한다.\n 규격의 「제목 구조로 계층화」와 ARIA 구조를 동시에 만족시키는 배치다.\n */}\n {groups?.length ? (\n <div\n id={listId}\n role=\"listbox\"\n aria-label={listLabel}\n className=\"w-full max-h-80 overflow-y-auto flex flex-col gap-2\"\n >\n {sections.map((section, si) => (\n <div\n key={section.label ?? si}\n role=\"group\"\n aria-labelledby={`${reactId}-g-${si}`}\n className=\"w-full\"\n >\n {/* 킷에 목록 소제목 규칙이 없다(보충 5) — 유틸리티로 그린다. 좌우 16px 은 .item-link 와 맞춘 값. */}\n <h3\n id={`${reactId}-g-${si}`}\n className=\"px-4 py-1 text-xs font-bold text-fg-subtle\"\n >\n {section.label}\n </h3>\n <ul className=\"drop-list\" role=\"none\">\n {section.options.map((_, oi) =>\n renderItem(flat[offsets[si] + oi], offsets[si] + oi),\n )}\n </ul>\n </div>\n ))}\n </div>\n ) : (\n <ul\n id={listId}\n role=\"listbox\"\n aria-label={listLabel}\n className=\"drop-list max-h-80 overflow-y-auto\"\n >\n {flat.map(renderItem)}\n </ul>\n )}\n\n {/* 빈 결과 안내. listbox 밖이라 option 개수에 섞이지 않는다. */}\n {flat.length === 0 && emptyMessage != null && (\n <p className=\"w-full px-4 py-3 text-sm text-fg-subtle\">\n {emptyMessage}\n </p>\n )}\n\n {footer && <div className=\"drop-bottom\">{footer}</div>}\n </div>\n </div>\n\n {action}\n </>\n );\n\n return (\n <div\n ref={wrapRef}\n // `krds-drop-wrap` 이 있어야 `.drop-menu`·`.item-link` 규칙이 걸린다(전부 후손 선택자).\n // `drop-left` 도 킷 클래스다 — 가운데 정렬을 왼쪽 맞춤으로 되돌린다.\n className={cx(\"krds-drop-wrap drop-left w-full\", className)}\n >\n {searchLayout ? <div className=\"sch-input w-full\">{row}</div> : row}\n {/* 함정 5 — 눈에는 안 보이고 소리로만 개수 변화를 알린다. */}\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {status}\n </span>\n </div>\n );\n}\n\n/** 제안 항목 하나 — 킷 `.item-link`. 초점을 받으면 안 되므로 `<li role=\"option\">` 이다. */\nfunction Item({\n option,\n active,\n index,\n onPick,\n onHover,\n}: {\n option: FlatOption;\n active: boolean;\n index: number;\n onPick: (option: ComboboxOption) => void;\n onHover: (index: number) => void;\n}) {\n return (\n <li\n id={option.id}\n role=\"option\"\n aria-selected={active}\n aria-disabled={option.disabled || undefined}\n // `.active` 는 킷 클래스다(굵게 + secondary 배경). 활성 표시를 새로 그리지 않는다.\n className={cx(\n \"item-link\",\n active && \"active\",\n option.disabled\n ? \"text-fg-disabled cursor-not-allowed\"\n : \"cursor-pointer\",\n )}\n onClick={() => onPick(option)}\n // 마우스를 올리면 활성도 따라온다 — 키보드로 고른 자리와 마우스가 가리키는 자리가 갈라지지 않게.\n onMouseMove={() => {\n if (!option.disabled) onHover(index);\n }}\n >\n {option.icon && (\n <i className={`svg-icon ico-${option.icon}`} aria-hidden=\"true\" />\n )}\n {/* 항목 높이가 48px 고정이라 줄바꿈이 나면 넘친다. 규격이 금지한 말줄임은 「입력 필드」쪽 이야기다. */}\n <span className=\"min-w-0 flex-1 truncate\">\n {option.label ?? option.value}\n </span>\n {option.description && (\n <span className=\"shrink-0 text-sm text-fg-subtle\">\n {option.description}\n </span>\n )}\n </li>\n );\n}\n\n/* ────────────────────────────── 검색어 자동완성 ────────────────────────────── */\n\n/**\n * 검색어 자동완성 — {@link Combobox} + 킷 `sch-input`(돋보기 겹침) + 검색어 삭제 버튼.\n *\n * KRDS 검색 규격의 구성 요소 5(검색어 삭제)·7(실시간 제안)을 한 부품으로 채운다.\n * 규격이 정한 이름 셋이 기본값이다 — 입력 「검색어」·삭제 「검색어 전체 삭제」·실행 「검색」.\n * `buttonType` 기본이 `submit` 이라 `<form action>` 안에 두면 JS 없이도 검색이 된다.\n *\n * @example\n * <form action=\"/search\">\n * <SearchCombobox name=\"q\" groups={suggestions} onSearch={run} />\n * </form>\n */\nexport type SearchComboboxProps = Omit<\n ComboboxBaseProps,\n \"action\" | \"searchLayout\" | \"label\" | \"labelledBy\"\n> & {\n /** 입력칸의 접근 가능한 이름. 규격이 정한 기본값 「검색어」. */\n label?: string;\n /** 실행 버튼의 이름(`sr-only`). 규격이 정한 기본값 「검색」. */\n buttonLabel?: string;\n /** 삭제 버튼의 이름(`sr-only`). 규격이 정한 기본값 「검색어 전체 삭제」. */\n clearLabel?: string;\n /** 검색어 삭제 버튼을 둔다. 기본 true — 규격 구성 요소 5는 선택이 아니다. */\n clearable?: boolean;\n /** 삭제 버튼을 누른 뒤. 입력값은 부품이 이미 비운 뒤 부른다. */\n onClear?: () => void;\n /**\n * 검색 실행. Enter(활성 항목이 없을 때)·돋보기 클릭·제안 선택 세 곳에서 부른다.\n * `preventDefault` 를 하지 않으므로 `<form>` 안이면 폼 제출도 함께 일어난다.\n */\n onSearch?: (query: string) => void;\n /** 기본 submit — `<form action>` 안에 두면 JS 없이도 검색이 된다. */\n buttonType?: \"submit\" | \"button\";\n buttonProps?: Omit<\n ComponentPropsWithRef<\"button\">,\n \"type\" | \"className\" | \"children\"\n >;\n};\n\nexport function SearchCombobox({\n label = \"검색어\",\n buttonLabel = \"검색\",\n clearLabel = \"검색어 전체 삭제\",\n clearable = true,\n onClear,\n onSearch,\n buttonType = \"submit\",\n buttonProps,\n value,\n defaultValue,\n onValueChange,\n onSelect,\n onKeyDown,\n inputClassName,\n ref,\n ...rest\n}: SearchComboboxProps) {\n const inputRef = useRef<HTMLInputElement | null>(null);\n const [innerValue, setInnerValue] = useState(defaultValue ?? \"\");\n const text = value !== undefined ? value : innerValue;\n\n const handleValueChange = (next: string) => {\n if (value === undefined) setInnerValue(next);\n onValueChange?.(next);\n };\n\n const setRef = (node: HTMLInputElement | null) => {\n inputRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as { current: HTMLInputElement | null }).current = node;\n };\n\n return (\n <Combobox\n {...rest}\n ref={setRef}\n label={label}\n searchLayout\n value={text}\n onValueChange={handleValueChange}\n onSelect={(option) => {\n onSelect?.(option);\n // 규격: 「Click — 선택된 항목으로 검색 실행」 / 「Enter — 초점을 가진 항목으로 검색 실행」.\n onSearch?.(option.value);\n }}\n onKeyDown={(e) => {\n onKeyDown?.(e);\n if (\n e.defaultPrevented ||\n e.key !== \"Enter\" ||\n e.nativeEvent.isComposing\n )\n return;\n // 활성 항목이 있으면 Combobox 가 「항목 선택」으로 처리한다 — 여기서 두 번 실행하지 않는다.\n if (e.currentTarget.getAttribute(\"aria-activedescendant\")) return;\n onSearch?.(e.currentTarget.value);\n }}\n // 킷 여백(48px)은 돋보기 하나 몫이다. 삭제 버튼까지 둘이라 80px 로 넓힌다(값이 없어도 유지).\n inputClassName={cx(clearable && \"pr-20\", inputClassName)}\n action={\n // `.sch-input .ico-search{position:absolute;top:50%;right:1.6rem;transform:translateY(-50%)}`\n // 킷의 그 위치 규칙을 감싸개가 받는다. gap-2(8px)는 킷 `--krds-gap-3` 과 같은 값.\n <div className=\"ico-search flex items-center gap-2\">\n {clearable && text.length > 0 && (\n <button\n type=\"button\"\n className=\"krds-btn medium icon\"\n onClick={() => {\n handleValueChange(\"\");\n onClear?.();\n inputRef.current?.focus();\n }}\n >\n <span className=\"sr-only\">{clearLabel}</span>\n <i className=\"svg-icon ico-del\" aria-hidden=\"true\" />\n </button>\n )}\n <button\n type={buttonType}\n className=\"krds-btn medium icon\"\n {...buttonProps}\n onClick={(e) => {\n buttonProps?.onClick?.(e);\n onSearch?.(text);\n }}\n >\n <span className=\"sr-only\">{buttonLabel}</span>\n <i className=\"svg-icon ico-sch\" aria-hidden=\"true\" />\n </button>\n </div>\n }\n />\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,94 @@
1
+ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ import { type KrdsIcon, type KrdsSize } from '../lib/krds';
3
+ /**
4
+ * 상황 메뉴(더보기 · 우클릭) — KRDS 에 없는 보충 부품이다.
5
+ *
6
+ * 모양은 킷 드롭다운(`krds-drop-wrap`), 동작은 WAI-ARIA APG 의 Menu Button 패턴이다.
7
+ * 표 한 칸에 늘어선 행 동작을 하나로 접을 때 쓴다 — 「삭제」는 `danger` 로 표시하고
8
+ * 구분선(`{ separator: true }`)으로 나머지와 떼어 놓는다.
9
+ * **동작 목록에만 쓴다.** 메뉴는 Tab 을 가로막으므로 언어 전환·화면 크기처럼 Tab 으로 닿아야
10
+ * 하는 목록에는 `ui/resize` 의 드롭다운을 쓴다. 하위 메뉴(2단)는 지원하지 않는다.
11
+ *
12
+ * @example
13
+ * <ContextMenu
14
+ * label={`${row.name} 행 더보기`}
15
+ * items={[
16
+ * { id: 'edit', label: '수정', icon: 'setting', onSelect: () => edit(row) },
17
+ * { separator: true },
18
+ * { id: 'delete', label: '삭제', icon: 'delete', danger: true, onSelect: () => remove(row) },
19
+ * ]}
20
+ * />
21
+ *
22
+ * 자세히: docs/krds/09-부품-노트.md#컨텍스트메뉴
23
+ */
24
+ /** 실행 항목. `href` 가 있으면 링크(`<a>`), 없으면 단추(`<button>`)로 그린다. */
25
+ export type ContextMenuAction = {
26
+ id: string;
27
+ label: ReactNode;
28
+ /** 글자 탐색에 쓸 문자열. `label` 이 문자열이면 필요 없다. */
29
+ text?: string;
30
+ /** 킷 아이콘 이름(`ico-` 뺀 것). 예: `delete` · `setting` · `download` */
31
+ icon?: KrdsIcon;
32
+ /** 이동이면 링크로 그린다. 동작에는 주지 않는다. */
33
+ href?: string;
34
+ target?: string;
35
+ /** 초점은 받되 실행되지 않는다(`aria-disabled`). */
36
+ disabled?: boolean;
37
+ /** 되돌릴 수 없는 동작(삭제 등). 글자를 위험색으로 물들인다. */
38
+ danger?: boolean;
39
+ /** 현재 상태 표시(킷 `.active`). 정렬 기준처럼 「지금 이것」을 보일 때. */
40
+ active?: boolean;
41
+ onSelect?: (id: string) => void;
42
+ separator?: never;
43
+ };
44
+ /** 구분선. 「삭제」처럼 되돌릴 수 없는 항목을 나머지와 떼어 놓는다. */
45
+ export type ContextMenuSeparatorItem = {
46
+ separator: true;
47
+ /** 목록 key 용. 없으면 순번으로 만든다. */
48
+ id?: string;
49
+ };
50
+ export type ContextMenuItem = ContextMenuAction | ContextMenuSeparatorItem;
51
+ /** 킷의 드롭다운 정렬 클래스(`.drop-left` · `.drop-right`). 생략하면 가운데(킷 기본). */
52
+ export type ContextMenuAlign = 'left' | 'center' | 'right';
53
+ export type ContextMenuProps = Omit<ComponentPropsWithRef<'div'>, 'onSelect'> & {
54
+ items: ReadonlyArray<ContextMenuItem>;
55
+ /** 단추의 이름(`sr-only`). 표 안에서는 어느 행인지까지 담는다 — 「홍길동 행 더보기」. */
56
+ label: string;
57
+ /** 글자가 있는 단추로 그린다. 주면 아이콘 대신 「글자 + ico-toggle」이 된다. */
58
+ triggerLabel?: ReactNode;
59
+ /** 아이콘만인 단추의 아이콘. 기본 `more`(킷 `ico_more.svg`). */
60
+ icon?: KrdsIcon;
61
+ /** 단추 크기. 기본 small(40px) — 표 안 행 동작 자리에 맞춘 값이다. */
62
+ size?: KrdsSize;
63
+ /** 메뉴가 펼쳐지는 쪽. 기본 `right`(오른쪽 끝 열에 놓이는 자리라 왼쪽으로 편다). */
64
+ align?: ContextMenuAlign;
65
+ /** 항목별 `onSelect` 와 별개로, 무엇을 골랐든 한 번 부른다. */
66
+ onSelect?: (id: string) => void;
67
+ onOpenChange?: (open: boolean) => void;
68
+ /** 여는 단추에 붙일 클래스. */
69
+ triggerClassName?: string;
70
+ };
71
+ /** 더보기 메뉴 — 표 행 동작·카드 우상단처럼 「이 대상에 대한 동작 묶음」을 접는다. */
72
+ export declare function ContextMenu({ items, label, triggerLabel, icon, size, align, onSelect, onOpenChange, triggerClassName, className, ref, ...rest }: ContextMenuProps): import("react").JSX.Element;
73
+ export type ContextMenuAreaProps = Omit<ComponentPropsWithRef<'div'>, 'onSelect'> & {
74
+ items: ReadonlyArray<ContextMenuItem>;
75
+ /** 메뉴 이름. 여는 단추가 없으니 여기서 준다 — 「목록 항목 동작」처럼. */
76
+ label: string;
77
+ onSelect?: (id: string) => void;
78
+ onOpenChange?: (open: boolean) => void;
79
+ /** 우클릭을 가로채지 않는다(브라우저 기본 메뉴가 뜬다). */
80
+ disabled?: boolean;
81
+ };
82
+ /**
83
+ * 우클릭 메뉴 — 영역 안에서 오른쪽 단추(또는 Shift+F10 · ContextMenu 키)로 연다.
84
+ *
85
+ * **브라우저 기본 메뉴를 가린다.** 「새 탭에서 열기」를 빼앗으므로 꼭 필요한 곳에만 쓰고,
86
+ * 같은 동작을 여는 눈에 보이는 단추(`ContextMenu`)를 함께 둔다.
87
+ *
88
+ * @example
89
+ * <ContextMenuArea label="목록 항목 동작" items={rowActions}>
90
+ * <ul>…</ul>
91
+ * </ContextMenuArea>
92
+ */
93
+ export declare function ContextMenuArea({ items, label, onSelect, onOpenChange, disabled, className, children, ref, ...rest }: ContextMenuAreaProps): import("react").JSX.Element;
94
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/context-menu/index.tsx"],"names":[],"mappings":"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"}