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 @@
1
+ {"version":3,"sources":["../../src/scroll-area/index.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { cx } from '../lib/krds';\n\n/**\n * 스크롤 영역 — KRDS 에 없다. 킷 스크롤 막대 모양을 유틸리티로 옮긴 보충 부품이다.\n *\n * 넘치는 내용을 가둬 굴린다. **초점을 받아 키보드로도 굴러간다** — 그것이 이 부품의 존재 이유다.\n * `role=\"region\"` 이라 이름(`label` 또는 `labelledBy`)이 필수이고, 랜드마크가 목록에 쌓이므로\n * 이름을 붙일 만큼 뜻이 있는 칸에만 쓴다. 단순히 넘치는 표는 `ui/table` 의 감싸개다.\n * **높이(폭)를 제한하지 않으면 아무것도 스크롤되지 않는다** — `className=\"max-h-80\"` 처럼 준다.\n *\n * @example\n * <ScrollArea label=\"약관 전문\" className=\"max-h-80 rounded-lg border border-solid border-line p-4\">\n * {terms}\n * </ScrollArea>\n *\n * 자세히: docs/krds/09-부품-노트.md#스크롤영역\n */\n\n/** 굴릴 방향. 기본은 세로. */\nexport type ScrollAreaAxis = 'vertical' | 'horizontal' | 'both';\n\ntype BaseProps = {\n /** 굴릴 방향. 기본 세로. */\n axis?: ScrollAreaAxis;\n /** 끝에 닿았을 때 바깥까지 따라 굴러가는 것을 막는다. 모달·서랍처럼 겹쳐 뜨는 칸에 켠다. */\n containScroll?: boolean;\n /** 칸에 붙일 클래스. **여기서 높이(폭)를 제한해야 스크롤이 생긴다.** */\n className?: string;\n /** 굴릴 내용. */\n children?: ReactNode;\n};\n\n/** 이름은 반드시 하나 있어야 한다 — 둘 다 주지도 둘 다 빼지도 못하게 타입으로 막는다. */\ntype NameProps =\n | { label: string; labelledBy?: never }\n | { label?: never; labelledBy: string };\n\nexport type ScrollAreaProps = Omit<\n ComponentPropsWithRef<'div'>,\n 'aria-label' | 'aria-labelledby' | 'children'\n> &\n BaseProps &\n NameProps;\n\nconst OVERFLOW: Record<ScrollAreaAxis, string> = {\n vertical: 'overflow-y-auto overflow-x-hidden',\n horizontal: 'overflow-x-auto overflow-y-hidden',\n both: 'overflow-auto',\n};\n\n/** 스크롤 막대 모양. 킷 모달의 막대 mixin 을 유틸리티로 옮긴 것이다(8px). */\nconst SCROLLBAR = cx(\n // 표준 속성 — 파이어폭스와 크롬 121+ 가 본다.\n '[scrollbar-width:thin]',\n '[scrollbar-color:var(--color-line)_var(--color-surface-subtler)]',\n // 웹킷 의사요소 — 킷과 같은 8px 막대.\n '[&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2',\n '[&::-webkit-scrollbar-track]:bg-surface-subtler',\n '[&::-webkit-scrollbar-thumb]:rounded-lg [&::-webkit-scrollbar-thumb]:bg-line',\n);\n\nexport function ScrollArea(props: ScrollAreaProps) {\n const {\n axis = 'vertical',\n containScroll,\n label,\n labelledBy,\n className,\n children,\n // 기본 0. 스크롤 칸은 초점을 받아야 ↑↓·PageUp/PageDown·Space 로 굴러간다(브라우저가 처리한다).\n tabIndex = 0,\n ...rest\n } = props as BaseProps &\n Omit<ComponentPropsWithRef<'div'>, 'children'> & { label?: string; labelledBy?: string };\n\n return (\n <div\n role=\"region\"\n aria-label={label}\n aria-labelledby={labelledBy}\n tabIndex={tabIndex}\n className={cx(\n OVERFLOW[axis],\n // flex/grid 자식일 때 내용만큼 늘어나 스크롤이 안 생기는 것을 막는다.\n 'min-h-0 min-w-0',\n containScroll && 'overscroll-contain',\n SCROLLBAR,\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n}\n"],"mappings":"AA6EI;AA5EJ,SAAS,UAAU;AA4CnB,MAAM,WAA2C;AAAA,EAC/C,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AACR;AAGA,MAAM,YAAY;AAAA;AAAA,EAEhB;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,WAAW,OAAwB;AACjD,QAAM;AAAA,IACJ,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA,WAAW;AAAA,IACX,GAAG;AAAA,EACL,IAAI;AAGJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,mBAAiB;AAAA,MACjB;AAAA,MACA,WAAW;AAAA,QACT,SAAS,IAAI;AAAA;AAAA,QAEb;AAAA,QACA,iBAAiB;AAAA,QACjB;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -0,0 +1,67 @@
1
+ import type { ComponentPropsWithRef } from 'react';
2
+ import { type KrdsFieldSize, type KrdsFieldState } from '../lib/krds';
3
+ /**
4
+ * 셀렉트 — KRDS `krds-form-select`.
5
+ *
6
+ * 이 부품이 그리는 것은 `<select>` 하나다. 레이블·도움말·상태 골격은 `ui/field` 의 `Field` 가 맡는다.
7
+ * 크기 기본이 medium(48px)이라 같은 줄의 버튼과 어긋나지 않는다(킷 기본은 large 56px).
8
+ * **항목이 7개 미만이면 라디오**, 20개를 넘으면 콤보박스나 셀렉트 분할을 쓴다.
9
+ * 여러 개를 고르는 자리에 `multiple` 을 쓰지 않는다(→ 체크박스).
10
+ * 목록 위 정렬·개수 도구는 테두리 없는 {@link SortSelect} 다.
11
+ *
12
+ * @example
13
+ * <Field id="board" label="게시판">
14
+ * {(a) => <Select {...a} placeholder="선택하세요" options={boards} />}
15
+ * </Field>
16
+ * <SortSelect aria-label="정렬기준" options={sorts} value={sort} onChange={onSort} />
17
+ *
18
+ * 자세히: docs/krds/09-부품-노트.md#셀렉트
19
+ */
20
+ /** 셀렉트 크기 — 40 / 48 / 56px. 입력칸에는 있는 xlarge 가 셀렉트에는 없다. */
21
+ export type SelectSize = Exclude<KrdsFieldSize, 'xlarge'>;
22
+ /** `options` 로 넘길 항목 하나. */
23
+ export type SelectOption = {
24
+ /** 값. */
25
+ value: string | number;
26
+ /** 화면에 보이는 글자. 요소가 아니라 글자여야 한다(브라우저가 마크업을 그리지 않는다). */
27
+ label: string;
28
+ /** 비활성. */
29
+ disabled?: boolean;
30
+ };
31
+ export type SelectProps = Omit<ComponentPropsWithRef<'select'>, 'size'> & {
32
+ /** 기본 medium(48px). 같은 줄의 입력칸·버튼과 같은 값을 준다. HTML `size` 속성은 지원하지 않는다. */
33
+ size?: SelectSize;
34
+ /** 폼 상태. 킷이 셀렉트에 실제로 그려 주는 것은 `is-error` 하나뿐이다. */
35
+ state?: KrdsFieldState;
36
+ /**
37
+ * 선택 완료. **켜지 않으면 값을 고른 뒤 초점을 잃었을 때 글자가 다시 흐려진다.**
38
+ * 생략하면 `value`(또는 `defaultValue`)가 빈 값이 아닌지로 정한다 —
39
+ * 값 없이 시작하는 비제어 셀렉트는 화면 쪽에서 직접 넘긴다.
40
+ */
41
+ completed?: boolean;
42
+ /** 항목 목록. `children` 으로 `<option>` 을 직접 써도 된다. */
43
+ options?: readonly SelectOption[];
44
+ /** 맨 앞에 붙는 `value=""` 항목. 「선택하세요」 자리다. */
45
+ placeholder?: string;
46
+ };
47
+ /**
48
+ * `<select class="krds-form-select">` 하나. 골격은 `ui/field` 가 맡는다.
49
+ *
50
+ * **이름을 반드시 준다** — `<label for>` 가 원칙이고, 안 되면 `aria-label` 이다(부품이 지어내지 않는다).
51
+ * 값을 바꿨다고 폼이 제출되게 하지 마라. 폭은 100% 라 줄이려면 `w-*` 를 더한다.
52
+ */
53
+ export declare function Select({ size, state, completed, options, placeholder, className, children, ...rest }: SelectProps): import("react").JSX.Element;
54
+ /**
55
+ * 정렬용 셀렉트 — KRDS `krds-form-select-sort`. **`Select` 와 다른 부품이다.**
56
+ *
57
+ * 테두리 없이 버튼처럼 생긴 목록 도구다 — 목록 위 「정렬기준」·「목록 표시 개수」 자리에 쓴다.
58
+ * 상태도 선택 완료도 없다(글자색이 늘 진하다). 폼 안의 입력에는 `Select` 를 쓴다.
59
+ */
60
+ export type SortSelectProps = Omit<ComponentPropsWithRef<'select'>, 'size'> & {
61
+ /** 기본 medium. `large` 는 글자가 커진다 — 목록 위 제목처럼 쓰는 자리 전용이다. */
62
+ size?: SelectSize;
63
+ /** 항목 목록. `children` 으로 `<option>` 을 직접 써도 된다. */
64
+ options?: readonly SelectOption[];
65
+ };
66
+ export declare function SortSelect({ size, options, className, children, ...rest }: SortSelectProps): import("react").JSX.Element;
67
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/select/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,OAAO,CAAC;AACnD,OAAO,EAAkB,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAEtF;;;;;;;;;;;;;;;;GAgBG;AAEH,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;AAE1D,4BAA4B;AAC5B,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS;IACT,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,WAAW;IACX,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,GAAG;IACxE,yEAAyE;IACzE,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,mDAAmD;IACnD,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kDAAkD;IAClD,OAAO,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAClC,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAgBF;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,EACrB,IAAe,EACf,KAAK,EACL,SAAS,EACT,OAAO,EACP,WAAW,EACX,SAAS,EACT,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,WAAW,+BA0Bb;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,GAAG;IAC5E,2DAA2D;IAC3D,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,kDAAkD;IAClD,OAAO,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;CACnC,CAAC;AAEF,wBAAgB,UAAU,CAAC,EAAE,IAAe,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,eAAe,+BAcrG"}
@@ -0,0 +1,61 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cx, stateClass } from "../lib/krds.js";
3
+ function hasValue(value) {
4
+ if (value === void 0 || value === null) return false;
5
+ if (Array.isArray(value)) return value.length > 0;
6
+ return String(value) !== "";
7
+ }
8
+ function withoutInputClass(className) {
9
+ if (!className) return void 0;
10
+ const kept = className.split(/\s+/).filter((one) => one && one !== "krds-input");
11
+ return kept.length ? kept.join(" ") : void 0;
12
+ }
13
+ function Select({
14
+ size = "medium",
15
+ state,
16
+ completed,
17
+ options,
18
+ placeholder,
19
+ className,
20
+ children,
21
+ ...rest
22
+ }) {
23
+ const isCompleted = completed ?? hasValue(rest.value ?? rest.defaultValue);
24
+ return /* @__PURE__ */ jsxs(
25
+ "select",
26
+ {
27
+ "aria-invalid": state === "error" ? true : void 0,
28
+ className: cx(
29
+ "krds-form-select",
30
+ size,
31
+ isCompleted && "completed",
32
+ stateClass(state),
33
+ withoutInputClass(className)
34
+ ),
35
+ ...rest,
36
+ children: [
37
+ placeholder !== void 0 && /* @__PURE__ */ jsx("option", { value: "", children: placeholder }),
38
+ options?.map((one) => /* @__PURE__ */ jsx("option", { value: one.value, disabled: one.disabled, children: one.label }, String(one.value))),
39
+ children
40
+ ]
41
+ }
42
+ );
43
+ }
44
+ function SortSelect({ size = "medium", options, className, children, ...rest }) {
45
+ return /* @__PURE__ */ jsxs(
46
+ "select",
47
+ {
48
+ className: cx("krds-form-select-sort", size, withoutInputClass(className)),
49
+ ...rest,
50
+ children: [
51
+ options?.map((one) => /* @__PURE__ */ jsx("option", { value: one.value, disabled: one.disabled, children: one.label }, String(one.value))),
52
+ children
53
+ ]
54
+ }
55
+ );
56
+ }
57
+ export {
58
+ Select,
59
+ SortSelect
60
+ };
61
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/select/index.tsx"],"sourcesContent":["import type { ComponentPropsWithRef } from 'react';\nimport { cx, stateClass, type KrdsFieldSize, type KrdsFieldState } from '../lib/krds';\n\n/**\n * 셀렉트 — KRDS `krds-form-select`.\n *\n * 이 부품이 그리는 것은 `<select>` 하나다. 레이블·도움말·상태 골격은 `ui/field` 의 `Field` 가 맡는다.\n * 크기 기본이 medium(48px)이라 같은 줄의 버튼과 어긋나지 않는다(킷 기본은 large 56px).\n * **항목이 7개 미만이면 라디오**, 20개를 넘으면 콤보박스나 셀렉트 분할을 쓴다.\n * 여러 개를 고르는 자리에 `multiple` 을 쓰지 않는다(→ 체크박스).\n * 목록 위 정렬·개수 도구는 테두리 없는 {@link SortSelect} 다.\n *\n * @example\n * <Field id=\"board\" label=\"게시판\">\n * {(a) => <Select {...a} placeholder=\"선택하세요\" options={boards} />}\n * </Field>\n * <SortSelect aria-label=\"정렬기준\" options={sorts} value={sort} onChange={onSort} />\n *\n * 자세히: docs/krds/09-부품-노트.md#셀렉트\n */\n\n/** 셀렉트 크기 — 40 / 48 / 56px. 입력칸에는 있는 xlarge 가 셀렉트에는 없다. */\nexport type SelectSize = Exclude<KrdsFieldSize, 'xlarge'>;\n\n/** `options` 로 넘길 항목 하나. */\nexport type SelectOption = {\n /** 값. */\n value: string | number;\n /** 화면에 보이는 글자. 요소가 아니라 글자여야 한다(브라우저가 마크업을 그리지 않는다). */\n label: string;\n /** 비활성. */\n disabled?: boolean;\n};\n\nexport type SelectProps = Omit<ComponentPropsWithRef<'select'>, 'size'> & {\n /** 기본 medium(48px). 같은 줄의 입력칸·버튼과 같은 값을 준다. HTML `size` 속성은 지원하지 않는다. */\n size?: SelectSize;\n /** 폼 상태. 킷이 셀렉트에 실제로 그려 주는 것은 `is-error` 하나뿐이다. */\n state?: KrdsFieldState;\n /**\n * 선택 완료. **켜지 않으면 값을 고른 뒤 초점을 잃었을 때 글자가 다시 흐려진다.**\n * 생략하면 `value`(또는 `defaultValue`)가 빈 값이 아닌지로 정한다 —\n * 값 없이 시작하는 비제어 셀렉트는 화면 쪽에서 직접 넘긴다.\n */\n completed?: boolean;\n /** 항목 목록. `children` 으로 `<option>` 을 직접 써도 된다. */\n options?: readonly SelectOption[];\n /** 맨 앞에 붙는 `value=\"\"` 항목. 「선택하세요」 자리다. */\n placeholder?: string;\n};\n\n/** 값이 골라진 것인지. 빈 문자열·undefined·빈 배열은 미선택으로 본다. */\nfunction hasValue(value: SelectProps['value'] | SelectProps['defaultValue']): boolean {\n if (value === undefined || value === null) return false;\n if (Array.isArray(value)) return value.length > 0;\n return String(value) !== '';\n}\n\n/** 들어온 className 에서 `krds-input` 만 걷어낸다 — 입력칸 클래스가 함께 붙으면 높이가 두 규칙으로 갈린다. */\nfunction withoutInputClass(className?: string): string | undefined {\n if (!className) return undefined;\n const kept = className.split(/\\s+/).filter((one) => one && one !== 'krds-input');\n return kept.length ? kept.join(' ') : undefined;\n}\n\n/**\n * `<select class=\"krds-form-select\">` 하나. 골격은 `ui/field` 가 맡는다.\n *\n * **이름을 반드시 준다** — `<label for>` 가 원칙이고, 안 되면 `aria-label` 이다(부품이 지어내지 않는다).\n * 값을 바꿨다고 폼이 제출되게 하지 마라. 폭은 100% 라 줄이려면 `w-*` 를 더한다.\n */\nexport function Select({\n size = 'medium',\n state,\n completed,\n options,\n placeholder,\n className,\n children,\n ...rest\n}: SelectProps) {\n const isCompleted = completed ?? hasValue(rest.value ?? rest.defaultValue);\n\n return (\n <select\n // 오류를 색으로만 알리면 스크린리더가 읽지 못한다. 킷 샘플에는 없지만 붙인다.\n // (뒤의 {...rest} 가 이기므로 화면 쪽에서 덮어쓸 수 있다 — FormField 가 이미 넣어 주는 경우 등)\n aria-invalid={state === 'error' ? true : undefined}\n className={cx(\n 'krds-form-select',\n size,\n isCompleted && 'completed',\n stateClass(state),\n withoutInputClass(className),\n )}\n {...rest}\n >\n {placeholder !== undefined && <option value=\"\">{placeholder}</option>}\n {options?.map((one) => (\n <option key={String(one.value)} value={one.value} disabled={one.disabled}>\n {one.label}\n </option>\n ))}\n {children}\n </select>\n );\n}\n\n/**\n * 정렬용 셀렉트 — KRDS `krds-form-select-sort`. **`Select` 와 다른 부품이다.**\n *\n * 테두리 없이 버튼처럼 생긴 목록 도구다 — 목록 위 「정렬기준」·「목록 표시 개수」 자리에 쓴다.\n * 상태도 선택 완료도 없다(글자색이 늘 진하다). 폼 안의 입력에는 `Select` 를 쓴다.\n */\nexport type SortSelectProps = Omit<ComponentPropsWithRef<'select'>, 'size'> & {\n /** 기본 medium. `large` 는 글자가 커진다 — 목록 위 제목처럼 쓰는 자리 전용이다. */\n size?: SelectSize;\n /** 항목 목록. `children` 으로 `<option>` 을 직접 써도 된다. */\n options?: readonly SelectOption[];\n};\n\nexport function SortSelect({ size = 'medium', options, className, children, ...rest }: SortSelectProps) {\n return (\n <select\n className={cx('krds-form-select-sort', size, withoutInputClass(className))}\n {...rest}\n >\n {options?.map((one) => (\n <option key={String(one.value)} value={one.value} disabled={one.disabled}>\n {one.label}\n </option>\n ))}\n {children}\n </select>\n );\n}\n"],"mappings":"AAoFI,SAagC,KAbhC;AAnFJ,SAAS,IAAI,kBAA2D;AAmDxE,SAAS,SAAS,OAAoE;AACpF,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,SAAS;AAChD,SAAO,OAAO,KAAK,MAAM;AAC3B;AAGA,SAAS,kBAAkB,WAAwC;AACjE,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,OAAO,UAAU,MAAM,KAAK,EAAE,OAAO,CAAC,QAAQ,OAAO,QAAQ,YAAY;AAC/E,SAAO,KAAK,SAAS,KAAK,KAAK,GAAG,IAAI;AACxC;AAQO,SAAS,OAAO;AAAA,EACrB,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAgB;AACd,QAAM,cAAc,aAAa,SAAS,KAAK,SAAS,KAAK,YAAY;AAEzE,SACE;AAAA,IAAC;AAAA;AAAA,MAGC,gBAAc,UAAU,UAAU,OAAO;AAAA,MACzC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,eAAe;AAAA,QACf,WAAW,KAAK;AAAA,QAChB,kBAAkB,SAAS;AAAA,MAC7B;AAAA,MACC,GAAG;AAAA,MAEH;AAAA,wBAAgB,UAAa,oBAAC,YAAO,OAAM,IAAI,uBAAY;AAAA,QAC3D,SAAS,IAAI,CAAC,QACb,oBAAC,YAA+B,OAAO,IAAI,OAAO,UAAU,IAAI,UAC7D,cAAI,SADM,OAAO,IAAI,KAAK,CAE7B,CACD;AAAA,QACA;AAAA;AAAA;AAAA,EACH;AAEJ;AAeO,SAAS,WAAW,EAAE,OAAO,UAAU,SAAS,WAAW,UAAU,GAAG,KAAK,GAAoB;AACtG,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,yBAAyB,MAAM,kBAAkB,SAAS,CAAC;AAAA,MACxE,GAAG;AAAA,MAEH;AAAA,iBAAS,IAAI,CAAC,QACb,oBAAC,YAA+B,OAAO,IAAI,OAAO,UAAU,IAAI,UAC7D,cAAI,SADM,OAAO,IAAI,KAAK,CAE7B,CACD;AAAA,QACA;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -0,0 +1,47 @@
1
+ import type { ComponentPropsWithRef } from 'react';
2
+ import { type KrdsTone } from '../lib/krds';
3
+ /**
4
+ * 구분선 — KRDS 에 없다. KRDS 토큰으로 그리는 보충 부품이다.
5
+ *
6
+ * **`<hr>` 을 쓰지 마라** — 킷이 sr-only 규칙에 끼워 `!important` 로 숨겨 화면에서 사라진다.
7
+ * 이 부품은 `<div role="separator">` 로 그린다.
8
+ * 목록 사이를 촘촘히 나누는 뜻 없는 선은 `decorative` 로 두어 소리에서 뺀다.
9
+ * 세로선은 자기 높이가 없다 — 부모를 flex 로 두거나 `className` 으로 높이를 준다.
10
+ *
11
+ * @example
12
+ * <Separator />
13
+ * <Separator tone="line" spacing="large" />
14
+ * <div className="flex items-center gap-2">
15
+ * <span>등록일</span>
16
+ * <Separator orientation="vertical" spacing="none" decorative className="h-4" />
17
+ * <span>조회 12</span>
18
+ * </div>
19
+ *
20
+ * 자세히: docs/krds/09-부품-노트.md#구분선
21
+ */
22
+ /**
23
+ * 선 색. 앞 넷은 중립 선이고(`divider` 가 KRDS 의 「나누는 선」 전용 토큰), 나머지는 `KrdsTone` 이다.
24
+ * `gray` 만 전용 선 토큰이 없어 `strong` 과 같은 값을 쓴다.
25
+ */
26
+ export type SeparatorTone = KrdsTone | 'divider' | 'line' | 'strong' | 'darker';
27
+ /** 앞뒤 여백 — 8 / 16 / 24px. KRDS gap 눈금이다. */
28
+ export type SeparatorSpacing = 'none' | 'small' | 'medium' | 'large';
29
+ export type SeparatorProps = ComponentPropsWithRef<'div'> & {
30
+ /** 방향. 기본 가로선. **세로선은 자기 높이가 없다** — 부모를 flex 로 두거나 높이를 준다. */
31
+ orientation?: 'horizontal' | 'vertical';
32
+ /** 선 색. 기본 `divider`. */
33
+ tone?: SeparatorTone;
34
+ /** 1px(`thin`, 기본) · 2px(`thick`). */
35
+ thickness?: 'thin' | 'thick';
36
+ /** 점선. */
37
+ dashed?: boolean;
38
+ /** 앞뒤 여백. 기본 `medium`(16px). */
39
+ spacing?: SeparatorSpacing;
40
+ /** 뜻 없이 모양만인 선. `role="none"` + `aria-hidden` 이 되어 소리에서 빠진다. */
41
+ decorative?: boolean;
42
+ };
43
+ /**
44
+ * 구분선 하나. 가로선은 그냥 놓으면 되고, **세로선은 부모가 flex 이거나 높이를 줘야 보인다.**
45
+ */
46
+ export declare function Separator({ orientation, tone, thickness, dashed, spacing, decorative, className, ...rest }: SeparatorProps): import("react").JSX.Element;
47
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/separator/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,OAAO,CAAC;AACnD,OAAO,EAAM,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEhF,4CAA4C;AAC5C,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AA6BrE,MAAM,MAAM,cAAc,GAAG,qBAAqB,CAAC,KAAK,CAAC,GAAG;IAC1D,8DAA8D;IAC9D,WAAW,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;IACxC,yBAAyB;IACzB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7B,UAAU;IACV,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gCAAgC;IAChC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,wBAAgB,SAAS,CAAC,EACxB,WAA0B,EAC1B,IAAgB,EAChB,SAAkB,EAClB,MAAM,EACN,OAAkB,EAClB,UAAU,EACV,SAAS,EACT,GAAG,IAAI,EACR,EAAE,cAAc,+BAoBhB"}
@@ -0,0 +1,61 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cx } from "../lib/krds.js";
3
+ const TONE_CLASS = {
4
+ divider: "border-divider",
5
+ line: "border-line",
6
+ strong: "border-line-strong",
7
+ darker: "border-line-darker",
8
+ primary: "border-primary-line",
9
+ secondary: "border-secondary-line",
10
+ gray: "border-line-strong",
11
+ // gray 전용 선 토큰이 없다 — 위 주석 참고
12
+ point: "border-point-line",
13
+ danger: "border-danger-line",
14
+ warning: "border-warning-line",
15
+ success: "border-success-line",
16
+ information: "border-information-line"
17
+ };
18
+ const EDGE_CLASS = {
19
+ horizontal: { thin: "border-t", thick: "border-t-2" },
20
+ // krds-check: 세로 구분선은 한쪽 선이 본질이다
21
+ vertical: { thin: "border-l", thick: "border-l-2" }
22
+ };
23
+ const SPACING_CLASS = {
24
+ horizontal: { none: "", small: "my-2", medium: "my-4", large: "my-6" },
25
+ vertical: { none: "", small: "mx-2", medium: "mx-4", large: "mx-6" }
26
+ };
27
+ function Separator({
28
+ orientation = "horizontal",
29
+ tone = "divider",
30
+ thickness = "thin",
31
+ dashed,
32
+ spacing = "medium",
33
+ decorative,
34
+ className,
35
+ ...rest
36
+ }) {
37
+ return /* @__PURE__ */ jsx(
38
+ "div",
39
+ {
40
+ role: decorative ? "none" : "separator",
41
+ "aria-hidden": decorative ? true : void 0,
42
+ "aria-orientation": !decorative && orientation === "vertical" ? "vertical" : void 0,
43
+ className: cx(
44
+ "self-stretch",
45
+ orientation === "vertical" && "shrink-0",
46
+ // 좁은 flex 줄에서 눌려 사라지지 않게
47
+ EDGE_CLASS[orientation][thickness],
48
+ dashed ? "border-dashed" : "border-solid",
49
+ // 빼면 폭만 있고 선이 안 그려진다
50
+ TONE_CLASS[tone],
51
+ SPACING_CLASS[orientation][spacing],
52
+ className
53
+ ),
54
+ ...rest
55
+ }
56
+ );
57
+ }
58
+ export {
59
+ Separator
60
+ };
61
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/separator/index.tsx"],"sourcesContent":["import type { ComponentPropsWithRef } from 'react';\nimport { cx, type KrdsTone } from '../lib/krds';\n\n/**\n * 구분선 — KRDS 에 없다. KRDS 토큰으로 그리는 보충 부품이다.\n *\n * **`<hr>` 을 쓰지 마라** — 킷이 sr-only 규칙에 끼워 `!important` 로 숨겨 화면에서 사라진다.\n * 이 부품은 `<div role=\"separator\">` 로 그린다.\n * 목록 사이를 촘촘히 나누는 뜻 없는 선은 `decorative` 로 두어 소리에서 뺀다.\n * 세로선은 자기 높이가 없다 — 부모를 flex 로 두거나 `className` 으로 높이를 준다.\n *\n * @example\n * <Separator />\n * <Separator tone=\"line\" spacing=\"large\" />\n * <div className=\"flex items-center gap-2\">\n * <span>등록일</span>\n * <Separator orientation=\"vertical\" spacing=\"none\" decorative className=\"h-4\" />\n * <span>조회 12</span>\n * </div>\n *\n * 자세히: docs/krds/09-부품-노트.md#구분선\n */\n\n/**\n * 선 색. 앞 넷은 중립 선이고(`divider` 가 KRDS 의 「나누는 선」 전용 토큰), 나머지는 `KrdsTone` 이다.\n * `gray` 만 전용 선 토큰이 없어 `strong` 과 같은 값을 쓴다.\n */\nexport type SeparatorTone = KrdsTone | 'divider' | 'line' | 'strong' | 'darker';\n\n/** 앞뒤 여백 — 8 / 16 / 24px. KRDS gap 눈금이다. */\nexport type SeparatorSpacing = 'none' | 'small' | 'medium' | 'large';\n\nconst TONE_CLASS: Record<SeparatorTone, string> = {\n divider: 'border-divider',\n line: 'border-line',\n strong: 'border-line-strong',\n darker: 'border-line-darker',\n primary: 'border-primary-line',\n secondary: 'border-secondary-line',\n gray: 'border-line-strong', // gray 전용 선 토큰이 없다 — 위 주석 참고\n point: 'border-point-line',\n danger: 'border-danger-line',\n warning: 'border-warning-line',\n success: 'border-success-line',\n information: 'border-information-line',\n};\n\n/** 굵기 → 어느 변에 그릴지까지 한 표에 담는다. 1px·2px 는 KRDS 테두리 눈금과 같다. */\nconst EDGE_CLASS = {\n horizontal: { thin: 'border-t', thick: 'border-t-2' },\n // krds-check: 세로 구분선은 한쪽 선이 본질이다\n vertical: { thin: 'border-l', thick: 'border-l-2' },\n} as const;\n\nconst SPACING_CLASS = {\n horizontal: { none: '', small: 'my-2', medium: 'my-4', large: 'my-6' },\n vertical: { none: '', small: 'mx-2', medium: 'mx-4', large: 'mx-6' },\n} as const;\n\nexport type SeparatorProps = ComponentPropsWithRef<'div'> & {\n /** 방향. 기본 가로선. **세로선은 자기 높이가 없다** — 부모를 flex 로 두거나 높이를 준다. */\n orientation?: 'horizontal' | 'vertical';\n /** 선 색. 기본 `divider`. */\n tone?: SeparatorTone;\n /** 1px(`thin`, 기본) · 2px(`thick`). */\n thickness?: 'thin' | 'thick';\n /** 점선. */\n dashed?: boolean;\n /** 앞뒤 여백. 기본 `medium`(16px). */\n spacing?: SeparatorSpacing;\n /** 뜻 없이 모양만인 선. `role=\"none\"` + `aria-hidden` 이 되어 소리에서 빠진다. */\n decorative?: boolean;\n};\n\n/**\n * 구분선 하나. 가로선은 그냥 놓으면 되고, **세로선은 부모가 flex 이거나 높이를 줘야 보인다.**\n */\nexport function Separator({\n orientation = 'horizontal',\n tone = 'divider',\n thickness = 'thin',\n dashed,\n spacing = 'medium',\n decorative,\n className,\n ...rest\n}: SeparatorProps) {\n return (\n <div\n // 장식용이면 role=\"none\" — 보조기술이 통째로 건너뛴다.\n role={decorative ? 'none' : 'separator'}\n aria-hidden={decorative ? true : undefined}\n // role=\"separator\" 의 기본값이 horizontal 이라 세로일 때만 적는다.\n aria-orientation={!decorative && orientation === 'vertical' ? 'vertical' : undefined}\n className={cx(\n 'self-stretch',\n orientation === 'vertical' && 'shrink-0', // 좁은 flex 줄에서 눌려 사라지지 않게\n EDGE_CLASS[orientation][thickness],\n dashed ? 'border-dashed' : 'border-solid', // 빼면 폭만 있고 선이 안 그려진다\n TONE_CLASS[tone],\n SPACING_CLASS[orientation][spacing],\n className,\n )}\n {...rest}\n />\n );\n}\n"],"mappings":"AAwFI;AAvFJ,SAAS,UAAyB;AA+BlC,MAAM,aAA4C;AAAA,EAChD,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,WAAW;AAAA,EACX,MAAM;AAAA;AAAA,EACN,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AAGA,MAAM,aAAa;AAAA,EACjB,YAAY,EAAE,MAAM,YAAY,OAAO,aAAa;AAAA;AAAA,EAEpD,UAAU,EAAE,MAAM,YAAY,OAAO,aAAa;AACpD;AAEA,MAAM,gBAAgB;AAAA,EACpB,YAAY,EAAE,MAAM,IAAI,OAAO,QAAQ,QAAQ,QAAQ,OAAO,OAAO;AAAA,EACrE,UAAU,EAAE,MAAM,IAAI,OAAO,QAAQ,QAAQ,QAAQ,OAAO,OAAO;AACrE;AAoBO,SAAS,UAAU;AAAA,EACxB,cAAc;AAAA,EACd,OAAO;AAAA,EACP,YAAY;AAAA,EACZ;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAmB;AACjB,SACE;AAAA,IAAC;AAAA;AAAA,MAEC,MAAM,aAAa,SAAS;AAAA,MAC5B,eAAa,aAAa,OAAO;AAAA,MAEjC,oBAAkB,CAAC,cAAc,gBAAgB,aAAa,aAAa;AAAA,MAC3E,WAAW;AAAA,QACT;AAAA,QACA,gBAAgB,cAAc;AAAA;AAAA,QAC9B,WAAW,WAAW,EAAE,SAAS;AAAA,QACjC,SAAS,kBAAkB;AAAA;AAAA,QAC3B,WAAW,IAAI;AAAA,QACf,cAAc,WAAW,EAAE,OAAO;AAAA,QAClC;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -0,0 +1,78 @@
1
+ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ export type SideNavProps = Omit<ComponentPropsWithRef<'nav'>, 'title'> & {
3
+ /** LNB **전체**의 제목. 대메뉴 이름 하나다 — 묶음마다 쓰면 위계가 뒤집힌다. */
4
+ title?: ReactNode;
5
+ /** `<nav>` 의 이름. 기본 「사이드 메뉴」(가이드가 명시한 문구). */
6
+ label?: string;
7
+ };
8
+ export declare function SideNav({ title, label, className, children, ...rest }: SideNavProps): import("react").JSX.Element;
9
+ /**
10
+ * 메뉴 목록 — 킷 `lnb-list`. 여기 담긴 항목이 1뎁스다.
11
+ *
12
+ * `role="menubar"` 는 킷 샘플 그대로지만 메뉴 위젯의 키보드 규약은 구현하지 않는다 —
13
+ * 목록 탐색으로 두려면 `role="list"` 를 넘겨 덮는다.
14
+ */
15
+ export declare function SideNavList({ className, children, ...rest }: ComponentPropsWithRef<'ul'>): import("react").JSX.Element;
16
+ /**
17
+ * 묶음 — 목록 여러 개를 제목으로 나눌 때(관리자 화면처럼).
18
+ * 킷에 이 자리의 클래스가 없어 작은 글머리(`<h3>`)로 보충했다 — `lnb-tit` 을 재사용하면 위계가 뒤집힌다.
19
+ */
20
+ export type SideNavSectionProps = Omit<ComponentPropsWithRef<'div'>, 'title'> & {
21
+ title: ReactNode;
22
+ };
23
+ export declare function SideNavSection({ title, className, children, ...rest }: SideNavSectionProps): import("react").JSX.Element;
24
+ type ItemBaseProps = {
25
+ /** 지금 보고 있는 화면인가. 킷 `.selected` 와 `aria-current` 가 함께 붙는다. */
26
+ current?: boolean;
27
+ /** `<li>` 에 얹을 클래스. `className` 은 링크/버튼 쪽으로 간다. */
28
+ itemClassName?: string;
29
+ };
30
+ export type SideNavLinkProps = ComponentPropsWithRef<'a'> & ItemBaseProps & {
31
+ href: string;
32
+ /** 바깥 사이트. 새 창으로 열고 `UiLink` 대신 맨 `<a>` 로 그린다. */
33
+ external?: boolean;
34
+ };
35
+ /**
36
+ * 메뉴 링크 — 킷 `lnb-btn lnb-link`. `<li role="none">` 과 `<a role="menuitem">` 을 함께 그린다.
37
+ * 깊이에 맞는 `<li>` 클래스도 문맥에서 붙는다.
38
+ */
39
+ export declare function SideNavLink({ href, current, external, itemClassName, className, children, ...rest }: SideNavLinkProps): import("react").JSX.Element;
40
+ export type SideNavButtonProps = ComponentPropsWithRef<'button'> & ItemBaseProps;
41
+ /**
42
+ * 이동이 아니라 **화면 안에서 갈아 끼우는** 메뉴(관리자 탭). 모양은 링크와 같다.
43
+ * 주소가 바뀌는 이동이라면 `SideNavLink` 를 쓴다.
44
+ */
45
+ export declare function SideNavButton({ current, itemClassName, className, children, type, ...rest }: SideNavButtonProps): import("react").JSX.Element;
46
+ export type SideNavGroupProps = Omit<ComponentPropsWithRef<'li'>, 'title'> & {
47
+ /** 접기 버튼에 쓸 글자. */
48
+ label: ReactNode;
49
+ /** 열려 있는가(제어형). 주면 이 값이 곧 상태다. */
50
+ open?: boolean;
51
+ /** 처음 열려 있는가(비제어형). **현재 화면을 품은 묶음은 열어 둔다.** */
52
+ defaultOpen?: boolean;
53
+ /** 여닫을 때. */
54
+ onOpenChange?: (open: boolean) => void;
55
+ /** 이 묶음 자체가 현재 위치인가. */
56
+ current?: boolean;
57
+ };
58
+ /**
59
+ * 접히는 묶음 — 킷 `lnb-btn lnb-toggle` + `lnb-submenu`.
60
+ * **1뎁스에서만 펴진다** — 2뎁스에 또 접기를 넣으면 킷에 규칙이 없어 열리지 않는다.
61
+ * 3뎁스 아래는 {@link SideNavPopup} 으로 푼다.
62
+ */
63
+ export declare function SideNavGroup({ label, open: openProp, defaultOpen, onOpenChange, current, className, children, ...rest }: SideNavGroupProps): import("react").JSX.Element;
64
+ export type SideNavPopupProps = Omit<ComponentPropsWithRef<'li'>, 'title'> & {
65
+ /** 여는 버튼의 글자(3뎁스 메뉴 이름). */
66
+ label: ReactNode;
67
+ /** 패널 머리의 되돌아가기 버튼 글자. 없으면 `label` 을 쓴다. */
68
+ title?: ReactNode;
69
+ };
70
+ /**
71
+ * 4뎁스 팝업 — 킷 `lnb-btn lnb-toggle-popup` + `lnb-submenu-lv2`.
72
+ *
73
+ * LNB 폭을 통째로 덮으며 왼쪽에서 밀려 들어오는 패널이다 — 4뎁스는 접기가 아니라 화면 전환이다.
74
+ * 열면 패널 머리 버튼으로 초점이 가고, 되돌아가기 버튼과 Esc 로 닫으면 원래 버튼으로 돌아온다.
75
+ */
76
+ export declare function SideNavPopup({ label, title, className, children, ...rest }: SideNavPopupProps): import("react").JSX.Element;
77
+ export {};
78
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/side-navigation/index.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,188 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { UiLink } from "../lib/link.js";
4
+ import {
5
+ createContext,
6
+ useContext,
7
+ useEffect,
8
+ useId,
9
+ useRef,
10
+ useState
11
+ } from "react";
12
+ import { cx } from "../lib/krds.js";
13
+ const DepthContext = createContext(1);
14
+ function itemClass(depth) {
15
+ return depth === 1 ? "lnb-item" : depth === 2 ? "lnb-subitem" : void 0;
16
+ }
17
+ function SideNav({ title, label = "\uC0AC\uC774\uB4DC \uBA54\uB274", className, children, ...rest }) {
18
+ return /* @__PURE__ */ jsxs("nav", { className: cx("krds-side-navigation", className), "aria-label": label, ...rest, children: [
19
+ title != null && /* @__PURE__ */ jsx("h2", { className: "lnb-tit", children: title }),
20
+ children
21
+ ] });
22
+ }
23
+ function SideNavList({ className, children, ...rest }) {
24
+ return /* @__PURE__ */ jsx(DepthContext, { value: 1, children: /* @__PURE__ */ jsx("ul", { className: cx("lnb-list", className), role: "menubar", ...rest, children }) });
25
+ }
26
+ function SideNavSection({ title, className, children, ...rest }) {
27
+ return /* @__PURE__ */ jsxs("div", { className: cx(className), ...rest, children: [
28
+ /* @__PURE__ */ jsx("h3", { className: "px-2 pt-6 pb-1 text-xs font-bold tracking-wider text-fg-subtle", children: title }),
29
+ children
30
+ ] });
31
+ }
32
+ function SideNavLink({
33
+ href,
34
+ current,
35
+ external,
36
+ itemClassName,
37
+ className,
38
+ children,
39
+ ...rest
40
+ }) {
41
+ const depth = useContext(DepthContext);
42
+ const cls = cx("lnb-btn", depth < 3 && "lnb-link", current && "selected", className);
43
+ const aria = {
44
+ role: "menuitem",
45
+ "aria-current": current ? "page" : void 0
46
+ };
47
+ return /* @__PURE__ */ jsx("li", { className: cx(itemClass(depth), current && "active", itemClassName), role: "none", children: external ? /* @__PURE__ */ jsx(
48
+ "a",
49
+ {
50
+ href,
51
+ className: cls,
52
+ target: "_blank",
53
+ rel: "noopener noreferrer",
54
+ title: "\uC0C8 \uCC3D \uC5F4\uB9BC",
55
+ ...aria,
56
+ ...rest,
57
+ children
58
+ }
59
+ ) : /* @__PURE__ */ jsx(UiLink, { href, className: cls, ...aria, ...rest, children }) });
60
+ }
61
+ function SideNavButton({
62
+ current,
63
+ itemClassName,
64
+ className,
65
+ children,
66
+ type = "button",
67
+ ...rest
68
+ }) {
69
+ const depth = useContext(DepthContext);
70
+ return /* @__PURE__ */ jsx("li", { className: cx(itemClass(depth), current && "active", itemClassName), role: "none", children: /* @__PURE__ */ jsx(
71
+ "button",
72
+ {
73
+ type,
74
+ className: cx("lnb-btn", depth < 3 && "lnb-link", current && "selected", className),
75
+ role: "menuitem",
76
+ "aria-current": current ? "page" : void 0,
77
+ ...rest,
78
+ children
79
+ }
80
+ ) });
81
+ }
82
+ function SideNavGroup({
83
+ label,
84
+ open: openProp,
85
+ defaultOpen = false,
86
+ onOpenChange,
87
+ current,
88
+ className,
89
+ children,
90
+ ...rest
91
+ }) {
92
+ const depth = useContext(DepthContext);
93
+ const menuId = useId();
94
+ const [selfOpen, setSelfOpen] = useState(defaultOpen);
95
+ const open = openProp ?? selfOpen;
96
+ const toggle = () => {
97
+ const next = !open;
98
+ if (openProp === void 0) setSelfOpen(next);
99
+ onOpenChange?.(next);
100
+ };
101
+ return /* @__PURE__ */ jsxs("li", { className: cx(itemClass(depth), open && "active", className), role: "none", ...rest, children: [
102
+ /* @__PURE__ */ jsx(
103
+ "button",
104
+ {
105
+ type: "button",
106
+ className: cx("lnb-btn", "lnb-toggle", open && "active", current && "selected"),
107
+ role: "menuitem",
108
+ "aria-controls": menuId,
109
+ "aria-expanded": open,
110
+ onClick: toggle,
111
+ children: label
112
+ }
113
+ ),
114
+ /* @__PURE__ */ jsx("div", { className: "lnb-submenu", children: /* @__PURE__ */ jsx("ul", { id: menuId, role: "menu", children: /* @__PURE__ */ jsx(DepthContext, { value: 2, children }) }) })
115
+ ] });
116
+ }
117
+ function SideNavPopup({
118
+ label,
119
+ title,
120
+ className,
121
+ children,
122
+ ...rest
123
+ }) {
124
+ const depth = useContext(DepthContext);
125
+ const menuId = useId();
126
+ const [open, setOpen] = useState(false);
127
+ const triggerRef = useRef(null);
128
+ const titleRef = useRef(null);
129
+ useEffect(() => {
130
+ if (open) titleRef.current?.focus();
131
+ }, [open]);
132
+ const closeAndReturn = () => {
133
+ setOpen(false);
134
+ triggerRef.current?.focus();
135
+ };
136
+ const handleBlur = (e) => {
137
+ if (!open) return;
138
+ const next = e.relatedTarget;
139
+ if (next && e.currentTarget.contains(next)) return;
140
+ setOpen(false);
141
+ };
142
+ const handleKeyDown = (e) => {
143
+ if (e.key === "Escape") {
144
+ e.stopPropagation();
145
+ closeAndReturn();
146
+ }
147
+ };
148
+ return /* @__PURE__ */ jsxs("li", { className: cx(itemClass(depth), className), role: "none", ...rest, children: [
149
+ /* @__PURE__ */ jsx(
150
+ "button",
151
+ {
152
+ ref: triggerRef,
153
+ type: "button",
154
+ className: "lnb-btn lnb-toggle-popup",
155
+ role: "menuitem",
156
+ "aria-controls": menuId,
157
+ "aria-expanded": open,
158
+ "aria-haspopup": "true",
159
+ onClick: () => setOpen(true),
160
+ children: label
161
+ }
162
+ ),
163
+ /* @__PURE__ */ jsxs(
164
+ "div",
165
+ {
166
+ id: menuId,
167
+ className: cx("lnb-submenu-lv2", open && "active"),
168
+ role: "menu",
169
+ onBlur: handleBlur,
170
+ onKeyDown: handleKeyDown,
171
+ children: [
172
+ /* @__PURE__ */ jsx("button", { ref: titleRef, type: "button", className: "lnb-btn-tit", onClick: closeAndReturn, children: title ?? label }),
173
+ /* @__PURE__ */ jsx("ul", { children: /* @__PURE__ */ jsx(DepthContext, { value: 3, children }) })
174
+ ]
175
+ }
176
+ )
177
+ ] });
178
+ }
179
+ export {
180
+ SideNav,
181
+ SideNavButton,
182
+ SideNavGroup,
183
+ SideNavLink,
184
+ SideNavList,
185
+ SideNavPopup,
186
+ SideNavSection
187
+ };
188
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/side-navigation/index.tsx"],"sourcesContent":["'use client';\n\nimport { UiLink } from '../lib/link';\nimport {\n createContext,\n useContext,\n useEffect,\n useId,\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 } from '../lib/krds';\n\n/**\n * 사이드 메뉴(LNB) — KRDS `krds-side-navigation`.\n *\n * 서비스 정보구조를 왼쪽에 펼친다. 조각을 조립해 쓴다 —\n * `SideNav`(제목) → `SideNavList` → `SideNavLink`·`SideNavButton`·`SideNavGroup`·`SideNavPopup`.\n * 깊이는 문맥이 세므로 `depth` 를 손으로 넘기지 않는다.\n * **현재 화면 표시는 `current`(→ 킷 `.selected`)다** — 킷 샘플의 `.active` 는 열림 표시일 뿐이다.\n * `lnb-tit`(= `SideNav` 의 `title`)은 LNB **전체**의 제목이라 한 번만 쓴다. 묶음 제목은 `SideNavSection`.\n * 같은 화면 안의 섹션 목차는 `ui/in-page-navigation` 이다.\n *\n * @example\n * <SideNav title=\"정보공개\">\n * <SideNavList>\n * <SideNavLink href=\"/open/list\" current>공표목록</SideNavLink>\n * <SideNavGroup label=\"청구·처리\" defaultOpen>\n * <SideNavLink href=\"/open/request\">청구 안내</SideNavLink>\n * </SideNavGroup>\n * </SideNavList>\n * </SideNav>\n *\n * 자세히: docs/krds/09-부품-노트.md#사이드내비게이션\n */\n\n/** 깊이. 킷은 깊이마다 `<li>` 클래스가 다르다: 1 `lnb-item` · 2 `lnb-subitem` · 3 없음. */\ntype Depth = 1 | 2 | 3;\n\n/** 깊이 문맥. 화면 쪽에서 깊이를 손으로 세지 않게 한다 — 잘못 세면 없어야 할 구분선이 생긴다. */\nconst DepthContext = createContext<Depth>(1);\n\nfunction itemClass(depth: Depth): string | undefined {\n return depth === 1 ? 'lnb-item' : depth === 2 ? 'lnb-subitem' : undefined;\n}\n\n/* ────────────────────────────────────────────────────────────────────────── */\n\nexport type SideNavProps = Omit<ComponentPropsWithRef<'nav'>, 'title'> & {\n /** LNB **전체**의 제목. 대메뉴 이름 하나다 — 묶음마다 쓰면 위계가 뒤집힌다. */\n title?: ReactNode;\n /** `<nav>` 의 이름. 기본 「사이드 메뉴」(가이드가 명시한 문구). */\n label?: string;\n};\n\nexport function SideNav({ title, label = '사이드 메뉴', className, children, ...rest }: SideNavProps) {\n return (\n <nav className={cx('krds-side-navigation', className)} aria-label={label} {...rest}>\n {title != null && <h2 className=\"lnb-tit\">{title}</h2>}\n {children}\n </nav>\n );\n}\n\n/**\n * 메뉴 목록 — 킷 `lnb-list`. 여기 담긴 항목이 1뎁스다.\n *\n * `role=\"menubar\"` 는 킷 샘플 그대로지만 메뉴 위젯의 키보드 규약은 구현하지 않는다 —\n * 목록 탐색으로 두려면 `role=\"list\"` 를 넘겨 덮는다.\n */\nexport function SideNavList({ className, children, ...rest }: ComponentPropsWithRef<'ul'>) {\n return (\n <DepthContext value={1}>\n <ul className={cx('lnb-list', className)} role=\"menubar\" {...rest}>\n {children}\n </ul>\n </DepthContext>\n );\n}\n\n/**\n * 묶음 — 목록 여러 개를 제목으로 나눌 때(관리자 화면처럼).\n * 킷에 이 자리의 클래스가 없어 작은 글머리(`<h3>`)로 보충했다 — `lnb-tit` 을 재사용하면 위계가 뒤집힌다.\n */\nexport type SideNavSectionProps = Omit<ComponentPropsWithRef<'div'>, 'title'> & {\n title: ReactNode;\n};\n\nexport function SideNavSection({ title, className, children, ...rest }: SideNavSectionProps) {\n return (\n <div className={cx(className)} {...rest}>\n <h3 className=\"px-2 pt-6 pb-1 text-xs font-bold tracking-wider text-fg-subtle\">{title}</h3>\n {children}\n </div>\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────── */\n\ntype ItemBaseProps = {\n /** 지금 보고 있는 화면인가. 킷 `.selected` 와 `aria-current` 가 함께 붙는다. */\n current?: boolean;\n /** `<li>` 에 얹을 클래스. `className` 은 링크/버튼 쪽으로 간다. */\n itemClassName?: string;\n};\n\nexport type SideNavLinkProps = ComponentPropsWithRef<'a'> &\n ItemBaseProps & {\n href: string;\n /** 바깥 사이트. 새 창으로 열고 `UiLink` 대신 맨 `<a>` 로 그린다. */\n external?: boolean;\n };\n\n/**\n * 메뉴 링크 — 킷 `lnb-btn lnb-link`. `<li role=\"none\">` 과 `<a role=\"menuitem\">` 을 함께 그린다.\n * 깊이에 맞는 `<li>` 클래스도 문맥에서 붙는다.\n */\nexport function SideNavLink({\n href,\n current,\n external,\n itemClassName,\n className,\n children,\n ...rest\n}: SideNavLinkProps) {\n const depth = useContext(DepthContext);\n const cls = cx('lnb-btn', depth < 3 && 'lnb-link', current && 'selected', className);\n const aria = {\n role: 'menuitem',\n 'aria-current': current ? ('page' as const) : undefined,\n };\n\n return (\n <li className={cx(itemClass(depth), current && 'active', itemClassName)} role=\"none\">\n {external ? (\n <a\n href={href}\n className={cls}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n // 새 창으로 열리면 미리 알린다 — KRDS 접근성 요구.\n title=\"새 창 열림\"\n {...aria}\n {...rest}\n >\n {children}\n </a>\n ) : (\n <UiLink href={href} className={cls} {...aria} {...rest}>\n {children}\n </UiLink>\n )}\n </li>\n );\n}\n\nexport type SideNavButtonProps = ComponentPropsWithRef<'button'> & ItemBaseProps;\n\n/**\n * 이동이 아니라 **화면 안에서 갈아 끼우는** 메뉴(관리자 탭). 모양은 링크와 같다.\n * 주소가 바뀌는 이동이라면 `SideNavLink` 를 쓴다.\n */\nexport function SideNavButton({\n current,\n itemClassName,\n className,\n children,\n type = 'button',\n ...rest\n}: SideNavButtonProps) {\n const depth = useContext(DepthContext);\n return (\n <li className={cx(itemClass(depth), current && 'active', itemClassName)} role=\"none\">\n <button\n type={type}\n className={cx('lnb-btn', depth < 3 && 'lnb-link', current && 'selected', className)}\n role=\"menuitem\"\n aria-current={current ? 'page' : undefined}\n {...rest}\n >\n {children}\n </button>\n </li>\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────── */\n\nexport type SideNavGroupProps = Omit<ComponentPropsWithRef<'li'>, 'title'> & {\n /** 접기 버튼에 쓸 글자. */\n label: ReactNode;\n /** 열려 있는가(제어형). 주면 이 값이 곧 상태다. */\n open?: boolean;\n /** 처음 열려 있는가(비제어형). **현재 화면을 품은 묶음은 열어 둔다.** */\n defaultOpen?: boolean;\n /** 여닫을 때. */\n onOpenChange?: (open: boolean) => void;\n /** 이 묶음 자체가 현재 위치인가. */\n current?: boolean;\n};\n\n/**\n * 접히는 묶음 — 킷 `lnb-btn lnb-toggle` + `lnb-submenu`.\n * **1뎁스에서만 펴진다** — 2뎁스에 또 접기를 넣으면 킷에 규칙이 없어 열리지 않는다.\n * 3뎁스 아래는 {@link SideNavPopup} 으로 푼다.\n */\nexport function SideNavGroup({\n label,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n current,\n className,\n children,\n ...rest\n}: SideNavGroupProps) {\n const depth = useContext(DepthContext);\n const menuId = useId();\n const [selfOpen, setSelfOpen] = useState(defaultOpen);\n const open = openProp ?? selfOpen;\n\n const toggle = () => {\n const next = !open;\n if (openProp === undefined) setSelfOpen(next);\n onOpenChange?.(next);\n };\n\n return (\n <li className={cx(itemClass(depth), open && 'active', className)} role=\"none\" {...rest}>\n <button\n type=\"button\"\n className={cx('lnb-btn', 'lnb-toggle', open && 'active', current && 'selected')}\n role=\"menuitem\"\n aria-controls={menuId}\n aria-expanded={open}\n onClick={toggle}\n >\n {label}\n </button>\n <div className=\"lnb-submenu\">\n <ul id={menuId} role=\"menu\">\n <DepthContext value={2}>{children}</DepthContext>\n </ul>\n </div>\n </li>\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────── */\n\nexport type SideNavPopupProps = Omit<ComponentPropsWithRef<'li'>, 'title'> & {\n /** 여는 버튼의 글자(3뎁스 메뉴 이름). */\n label: ReactNode;\n /** 패널 머리의 되돌아가기 버튼 글자. 없으면 `label` 을 쓴다. */\n title?: ReactNode;\n};\n\n/**\n * 4뎁스 팝업 — 킷 `lnb-btn lnb-toggle-popup` + `lnb-submenu-lv2`.\n *\n * LNB 폭을 통째로 덮으며 왼쪽에서 밀려 들어오는 패널이다 — 4뎁스는 접기가 아니라 화면 전환이다.\n * 열면 패널 머리 버튼으로 초점이 가고, 되돌아가기 버튼과 Esc 로 닫으면 원래 버튼으로 돌아온다.\n */\nexport function SideNavPopup({\n label,\n title,\n className,\n children,\n ...rest\n}: SideNavPopupProps) {\n const depth = useContext(DepthContext);\n const menuId = useId();\n const [open, setOpen] = useState(false);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const titleRef = useRef<HTMLButtonElement>(null);\n\n // 열리면 패널 머리로 초점을 옮긴다.\n useEffect(() => {\n if (open) titleRef.current?.focus();\n }, [open]);\n\n const closeAndReturn = () => {\n setOpen(false);\n triggerRef.current?.focus();\n };\n\n const handleBlur = (e: ReactFocusEvent<HTMLDivElement>) => {\n if (!open) return;\n const next = e.relatedTarget as Node | null;\n if (next && e.currentTarget.contains(next)) return;\n setOpen(false);\n };\n\n const handleKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n closeAndReturn();\n }\n };\n\n return (\n <li className={cx(itemClass(depth), className)} role=\"none\" {...rest}>\n <button\n ref={triggerRef}\n type=\"button\"\n className=\"lnb-btn lnb-toggle-popup\"\n role=\"menuitem\"\n aria-controls={menuId}\n aria-expanded={open}\n aria-haspopup=\"true\"\n onClick={() => setOpen(true)}\n >\n {label}\n </button>\n <div\n id={menuId}\n className={cx('lnb-submenu-lv2', open && 'active')}\n role=\"menu\"\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n >\n <button ref={titleRef} type=\"button\" className=\"lnb-btn-tit\" onClick={closeAndReturn}>\n {title ?? label}\n </button>\n <ul>\n <DepthContext value={3}>{children}</DepthContext>\n </ul>\n </div>\n </li>\n );\n}\n"],"mappings":"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","names":[]}