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/main-menu/index.tsx"],"sourcesContent":["'use client';\n\nimport { UiLink } from '../lib/link';\nimport {\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type FocusEvent as ReactFocusEvent,\n type HTMLAttributes,\n type KeyboardEvent as ReactKeyboardEvent,\n type MouseEvent as ReactMouseEvent,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { cx } from '../lib/krds';\n\n/**\n * 주메뉴(GNB) — KRDS `krds-main-menu`(PC) · `krds-main-menu-mobile`(모바일).\n *\n * 같은 `items` 를 두 부품에 넘긴다. 킷 CSS 가 1023px 이하에서 PC 메뉴를, 1024px 이상에서\n * 모바일 서랍을 감추므로 미디어쿼리를 따로 쓰지 않는다.\n * 계층은 **최대 3수준**이고 타입이 그것을 강제한다 — {@link MainMenuTop} → {@link MainMenuGroup}\n * → {@link MainMenuLink}. `items` 가 없는 단계는 드롭다운 없는 단순 링크가 된다.\n * **마우스오버로 열리지 않는다**(규격 금지) — 클릭·키보드로만 연다.\n * 화면 안쪽의 하위 메뉴는 `ui/side-navigation` 이다.\n *\n * @example\n * const menu: MainMenuTop[] = [\n * { label: '기관소개', href: '/about' },\n * {\n * label: '정보공개',\n * items: [\n * {\n * label: '사전정보공표',\n * shortcut: { href: '/open' },\n * items: [{ label: '공표목록', href: '/open/list', current: true }],\n * },\n * ],\n * },\n * ];\n * <MainMenu items={menu} />\n * <MainMenuMobile items={menu} open={open} onClose={close} header={<AuthLinks />} />\n *\n * 자세히: docs/krds/09-부품-노트.md#주메뉴\n */\n\n/** 새 창으로 열리는 링크에 붙일 안내 문구. */\nconst NEW_WINDOW_TITLE = '새 창 열림';\n\n/** 링크가 이 문서 밖을 가리키는가. `/` 로 시작하지 않으면 `UiLink` 를 쓰지 않고 맨 <a> 로 그린다. */\nfunction isExternalHref(href: string): boolean {\n return !href.startsWith('/');\n}\n\n/** `useId()` 는 특수문자를 담는다 — id·href 조각으로 쓸 수 있게 깎는다. */\nfunction idBase(raw: string): string {\n return raw.replace(/[^a-zA-Z0-9_-]/g, '') || 'gnb';\n}\n\ntype MenuActionProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {\n /** 없으면 `<button type=\"button\">` 으로 그린다(킷 샘플이 링크와 버튼을 둘 다 허용한다). */\n href?: string;\n newWindow?: boolean;\n 'data-trigger'?: string;\n children: ReactNode;\n};\n\n/** 메뉴 한 칸을 링크(`href` 있음)나 버튼으로 그린다. 새 창이면 안내 `title` 을 붙인다. */\nfunction MenuAction({ href, newWindow, children, ...rest }: MenuActionProps) {\n if (!href) {\n return (\n <button type=\"button\" {...rest}>\n {children}\n </button>\n );\n }\n\n if (newWindow || isExternalHref(href)) {\n return (\n <a\n href={href}\n target={newWindow ? '_blank' : undefined}\n rel={newWindow ? 'noopener noreferrer' : undefined}\n title={newWindow ? NEW_WINDOW_TITLE : undefined}\n {...rest}\n >\n {children}\n </a>\n );\n }\n\n return (\n <UiLink href={href} {...rest}>\n {children}\n </UiLink>\n );\n}\n\n/** 마지막 단(3뎁스) 항목. 여기서 트리가 끝난다 — 규격의 「최대 3수준」을 타입으로 막은 자리다. */\nexport type MainMenuLink = {\n /** React key. 없으면 차례를 쓴다. */\n id?: string;\n /** 메뉴 이름. */\n label: ReactNode;\n /** 이동 주소. 없으면 버튼이 된다. */\n href?: string;\n /** 새 창으로 연다. 외부 링크면 화살표 아이콘이 함께 붙는다. */\n newWindow?: boolean;\n /** 지금 보고 있는 화면. `aria-current=\"page\"` 가 붙는다. */\n current?: boolean;\n /** 메뉴 설명. 한 목록에 하나라도 있으면 그 목록 전체가 킷의 설명형으로 바뀐다. */\n description?: string;\n /** 눌렀을 때. */\n onSelect?: () => void;\n};\n\n/** 드롭다운 오른쪽(또는 아래)의 신규 서비스 배너 — 킷 `gnb-sub-banner`. */\nexport type MainMenuBanner = {\n /** 배지 글자. 배지는 대화형이 아니므로 글자만 넣는다. */\n badge?: ReactNode;\n /** 배너 글자. */\n label: ReactNode;\n /** 이동 주소. */\n href?: string;\n /** 새 창으로 연다. */\n newWindow?: boolean;\n /** 배너를 오른쪽 칸에 세운다. 기본은 목록 아래. */\n side?: boolean;\n /** 눌렀을 때. */\n onSelect?: () => void;\n};\n\n/** 2뎁스. `items` 가 없으면 왼쪽 칸에 링크 한 줄로만 놓인다. */\nexport type MainMenuGroup = {\n /** React key. 없으면 차례를 쓴다. */\n id?: string;\n /** 메뉴 이름. */\n label: ReactNode;\n /** 이동 주소. `items` 가 없을 때 쓰인다. */\n href?: string;\n /** 새 창으로 연다. */\n newWindow?: boolean;\n /** 지금 보고 있는 화면. */\n current?: boolean;\n /** 드롭다운 제목. 생략하면 `label` 을 쓴다 — 제목 없는 드롭다운은 만들 수 없다. */\n title?: ReactNode;\n /** 제목 옆 「바로가기」 링크. */\n shortcut?: { href: string; label?: ReactNode; newWindow?: boolean };\n /** 신규 서비스 배너. */\n banner?: MainMenuBanner;\n /** 3뎁스 항목. */\n items?: MainMenuLink[];\n /** 눌렀을 때. */\n onSelect?: () => void;\n};\n\n/** 1뎁스. `items` 가 없으면 드롭다운 없는 단순 링크다. */\nexport type MainMenuTop = {\n /** React key. 없으면 차례를 쓴다. */\n id?: string;\n /** 메뉴 이름. */\n label: ReactNode;\n /** 이동 주소. `items` 가 없을 때 쓰인다. */\n href?: string;\n /** 새 창으로 연다. */\n newWindow?: boolean;\n /** 지금 보고 있는 화면. */\n current?: boolean;\n /** 2뎁스 항목. */\n items?: MainMenuGroup[];\n /** 눌렀을 때. */\n onSelect?: () => void;\n};\n\n/** 드롭다운 제목의 헤딩 단계. 화면의 제목 위계에 맞춘다. */\nexport type MainMenuTitleLevel = 2 | 3 | 4 | 5 | 6;\n\nconst key = (item: { id?: string }, index: number) => item.id ?? index;\n\n/** 하위가 있는 첫 2뎁스. 없으면 -1(오른쪽 판을 펼치지 않는다). */\nfunction firstPanelIndex(groups: MainMenuGroup[]): number {\n return groups.findIndex((group) => (group.items?.length ?? 0) > 0);\n}\n\nfunction hasItems(group: MainMenuGroup): boolean {\n return (group.items?.length ?? 0) > 0;\n}\n\n/** 드롭다운 한 판의 내용 — 킷 `gnb-sub-content` + `gnb-sub-banner`. 열 배치는 킷이 잡는다. */\nfunction GroupPanel({\n group,\n titleLevel,\n}: {\n group: MainMenuGroup;\n titleLevel: MainMenuTitleLevel;\n}) {\n const Title = `h${titleLevel}` as 'h2';\n const DescTitle = `h${Math.min(titleLevel + 1, 6)}` as 'h3';\n const items = group.items ?? [];\n const described = items.some((item) => item.description);\n const shortcut = group.shortcut;\n\n return (\n <>\n <div className=\"gnb-sub-content\">\n <Title className=\"sub-title\">\n {shortcut ? (\n <>\n {group.title ?? group.label}\n <MenuAction\n href={shortcut.href}\n newWindow={shortcut.newWindow}\n className=\"krds-btn link basic small\"\n >\n <span className=\"underline\">{shortcut.label ?? '바로가기'}</span>\n <i className=\"svg-icon ico-angle right\" aria-hidden=\"true\" />\n </MenuAction>\n </>\n ) : (\n <span>{group.title ?? group.label}</span>\n )}\n </Title>\n\n <ul className={cx(described && 'type-description')}>\n {items.map((item, index) => {\n const external = item.newWindow || (item.href ? isExternalHref(item.href) : false);\n if (described) {\n return (\n <li key={key(item, index)}>\n <DescTitle className=\"tit\">\n <MenuAction\n href={item.href}\n newWindow={item.newWindow}\n aria-current={item.current ? 'page' : undefined}\n onClick={item.onSelect}\n >\n {item.label}\n {external && <i className=\"svg-icon ico-go\" aria-hidden=\"true\" />}\n </MenuAction>\n </DescTitle>\n {item.description && <p className=\"txt\">{item.description}</p>}\n </li>\n );\n }\n return (\n <li key={key(item, index)}>\n <MenuAction\n href={item.href}\n newWindow={item.newWindow}\n className={cx(item.current && 'active')}\n aria-current={item.current ? 'page' : undefined}\n onClick={item.onSelect}\n >\n {item.label}\n </MenuAction>\n </li>\n );\n })}\n </ul>\n </div>\n\n {group.banner && (\n <div className=\"gnb-sub-banner\">\n {group.banner.badge && (\n <span className=\"krds-badge bg-secondary\">{group.banner.badge}</span>\n )}\n <MenuAction\n href={group.banner.href}\n newWindow={group.banner.newWindow}\n className=\"krds-btn medium text\"\n onClick={group.banner.onSelect}\n >\n {group.banner.label}\n <i className=\"svg-icon ico-angle right\" aria-hidden=\"true\" />\n </MenuAction>\n </div>\n )}\n </>\n );\n}\n\nexport type MainMenuProps = Omit<ComponentPropsWithRef<'nav'>, 'children'> & {\n /** 메뉴 트리(최대 3수준). */\n items: MainMenuTop[];\n /** `<nav>` 의 이름. 규격 권장 문구가 「메인 메뉴」다. */\n label?: string;\n /** 드롭다운 제목의 헤딩 단계. 기본 2. */\n titleLevel?: MainMenuTitleLevel;\n /** 딤을 깔고 본문 스크롤을 잠근다. 기본 켬 — 헤더 안 좁은 드롭다운으로 쓸 때만 끈다. */\n backdrop?: boolean;\n};\n\n/**\n * 주메뉴(PC) — 킷 `krds-main-menu`. **1023px 이하에서는 킷이 이 요소를 숨긴다** —\n * 좁은 화면은 {@link MainMenuMobile} 이 맡는다.\n */\nexport function MainMenu({\n items,\n label = '메인 메뉴',\n titleLevel = 2,\n backdrop = true,\n className,\n onKeyDown,\n onBlur,\n ref,\n ...rest\n}: MainMenuProps) {\n const uid = idBase(useId());\n const navRef = useRef<HTMLElement | null>(null);\n const listRefs = useRef<Array<HTMLDivElement | null>>([]);\n const [openIndex, setOpenIndex] = useState<number | null>(null);\n /** 1뎁스 차례 → 펼쳐 둔 2뎁스 차례. 없으면 `firstPanelIndex` 를 쓴다. */\n const [activeGroup, setActiveGroup] = useState<Record<number, number>>({});\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => setMounted(true), []);\n\n const close = useCallback(() => setOpenIndex(null), []);\n\n const triggerAt = useCallback((index: number): HTMLElement | null => {\n const triggers = navRef.current?.querySelectorAll<HTMLElement>('.gnb-main-trigger');\n return triggers?.[index] ?? null;\n }, []);\n\n // 딤이 떠 있는 동안 본문 스크롤 잠금. 스크롤바가 사라지며 생기는 폭 차이는 킷이 메운다.\n useEffect(() => {\n if (openIndex === null || !backdrop) return;\n const needsGutter = document.body.scrollHeight > window.innerHeight;\n document.body.classList.add('is-gnb-web');\n if (needsGutter) document.body.classList.add('hasScrollY');\n return () => {\n document.body.classList.remove('is-gnb-web');\n if (needsGutter) document.body.classList.remove('hasScrollY');\n };\n }, [openIndex, backdrop]);\n\n // 메뉴 밖을 누르면 닫는다(킷 JS 와 같다).\n useEffect(() => {\n if (openIndex === null) return;\n function handleDown(event: MouseEvent) {\n if (!navRef.current?.contains(event.target as Node)) close();\n }\n document.addEventListener('mousedown', handleDown);\n return () => document.removeEventListener('mousedown', handleDown);\n }, [openIndex, close]);\n\n // Esc — 펼쳐진 드롭다운을 전부 접고 **그 1뎁스 메뉴로 초점을 되돌린다**(규격).\n useEffect(() => {\n if (openIndex === null) return;\n function handleKey(event: KeyboardEvent) {\n if (event.key !== 'Escape') return;\n const trigger = triggerAt(openIndex as number);\n close();\n trigger?.focus();\n }\n document.addEventListener('keydown', handleKey);\n return () => document.removeEventListener('keydown', handleKey);\n }, [openIndex, close, triggerAt]);\n\n // 오른쪽 판은 절대 배치라 왼쪽 칸의 높이를 밀어내지 못한다 — 킷 JS 처럼 min-height 로 옮긴다.\n // 이 값이 있어야 70% 상한을 넘겼을 때 드롭다운이 스스로 스크롤한다.\n useEffect(() => {\n if (openIndex === null) return;\n const list = listRefs.current[openIndex];\n if (!list) return;\n const panel = list.querySelector<HTMLElement>('.gnb-sub-list.active');\n list.style.minHeight = panel ? `${panel.scrollHeight}px` : '';\n return () => {\n list.style.minHeight = '';\n };\n }, [openIndex, activeGroup, items]);\n\n function toggleTop(index: number) {\n setOpenIndex((current) => (current === index ? null : index));\n }\n\n /** 키보드 이동 — Home/End 는 1뎁스의 처음·끝으로, ←→↑↓ 는 같은 목록의 형제 트리거로 간다. */\n function handleKeyDown(event: ReactKeyboardEvent<HTMLElement>) {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n\n const target = event.target as HTMLElement | null;\n if (!target || target.dataset?.trigger !== 'gnb') return;\n\n const move = (direction: 'next' | 'prev') => {\n const sibling =\n direction === 'next'\n ? target.closest('li')?.nextElementSibling\n : target.closest('li')?.previousElementSibling;\n sibling?.querySelector<HTMLElement>('[data-trigger]')?.focus();\n };\n\n switch (event.key) {\n case 'Home':\n event.preventDefault();\n triggerAt(0)?.focus();\n break;\n case 'End':\n event.preventDefault();\n triggerAt(items.length - 1)?.focus();\n break;\n case 'ArrowRight':\n case 'ArrowDown':\n event.preventDefault();\n move('next');\n break;\n case 'ArrowLeft':\n case 'ArrowUp':\n event.preventDefault();\n move('prev');\n break;\n default:\n break;\n }\n }\n\n /** Tab 으로 초점이 메뉴 밖으로 나가면 닫는다. 초점이 사라지는 경우(null)는 바깥 클릭이 맡는다. */\n function handleBlur(event: ReactFocusEvent<HTMLElement>) {\n onBlur?.(event);\n const next = event.relatedTarget as Node | null;\n if (next && !event.currentTarget.contains(next)) close();\n }\n\n return (\n <>\n <nav\n // 바깥에서 넘긴 ref 도 살린다 — 안쪽 navRef 는 바깥 클릭·초점 판정에 쓴다.\n ref={(node) => {\n navRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n }}\n className={cx('krds-main-menu', className)}\n aria-label={label}\n onKeyDown={handleKeyDown}\n onBlur={handleBlur}\n {...rest}\n >\n <div className=\"inner\">\n <ul className=\"gnb-menu\">\n {items.map((top, index) => {\n const groups = top.items ?? [];\n\n // 하위가 없으면 단순 링크다 — 킷 `gnb-main-trigger is-link`(펼침 화살표가 지워진다).\n if (groups.length === 0) {\n return (\n <li key={key(top, index)}>\n <MenuAction\n href={top.href}\n newWindow={top.newWindow}\n className=\"gnb-main-trigger is-link\"\n data-trigger=\"gnb\"\n aria-current={top.current ? 'page' : undefined}\n onClick={top.onSelect}\n >\n {top.label}\n </MenuAction>\n </li>\n );\n }\n\n const open = openIndex === index;\n const panelId = `${uid}-gnb-${index}`;\n const activeIndex = activeGroup[index] ?? firstPanelIndex(groups);\n /** 2뎁스가 하나뿐이면 왼쪽 칸 없이 한 판만 편다 — 킷 `single-list`. */\n const single = groups.length === 1 && hasItems(groups[0]);\n\n return (\n <li key={key(top, index)}>\n <button\n type=\"button\"\n className={cx('gnb-main-trigger', open && 'active')}\n data-trigger=\"gnb\"\n aria-expanded={open}\n aria-haspopup\n aria-controls={panelId}\n aria-current={open ? 'true' : top.current ? 'page' : undefined}\n onClick={() => toggleTop(index)}\n >\n {top.label}\n </button>\n\n {/* 규격: 드롭다운 최대 높이는 뷰포트의 70%. 킷 CSS 에 없어 유틸리티로 보충한다. */}\n <div\n id={panelId}\n className={cx(\n 'gnb-toggle-wrap',\n open && 'is-open',\n 'max-h-[70vh] overflow-y-auto',\n )}\n >\n <div\n className=\"gnb-main-list\"\n data-has-submenu={single ? undefined : 'true'}\n ref={(node) => {\n listRefs.current[index] = node;\n }}\n >\n {single ? (\n <div\n className={cx(\n 'gnb-sub-list',\n 'single-list',\n groups[0].banner?.side && 'between',\n )}\n >\n <GroupPanel group={groups[0]} titleLevel={titleLevel} />\n </div>\n ) : (\n <ul>\n {groups.map((group, groupIndex) => {\n if (!hasItems(group)) {\n const external =\n group.newWindow ||\n (group.href ? isExternalHref(group.href) : false);\n return (\n <li key={key(group, groupIndex)}>\n <MenuAction\n href={group.href}\n newWindow={group.newWindow}\n className={cx(\n 'gnb-sub-trigger',\n 'is-link',\n external && 'external-link',\n )}\n data-trigger=\"gnb\"\n aria-current={group.current ? 'page' : undefined}\n onClick={group.onSelect}\n >\n {group.label}\n </MenuAction>\n </li>\n );\n }\n\n const active = activeIndex === groupIndex;\n const subId = `${panelId}-sub-${groupIndex}`;\n\n return (\n <li key={key(group, groupIndex)}>\n <button\n type=\"button\"\n className={cx('gnb-sub-trigger', active && 'active')}\n data-trigger=\"gnb\"\n aria-expanded={active}\n aria-haspopup\n aria-controls={subId}\n onClick={() => {\n setActiveGroup((current) => ({\n ...current,\n [index]: groupIndex,\n }));\n group.onSelect?.();\n }}\n >\n {group.label}\n </button>\n <div\n id={subId}\n className={cx(\n 'gnb-sub-list',\n active && 'active',\n group.banner?.side && 'between',\n )}\n >\n <GroupPanel group={group} titleLevel={titleLevel} />\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </div>\n </li>\n );\n })}\n </ul>\n </div>\n </nav>\n\n {/* 딤. 킷 JS 도 body 끝에 붙인다 — 헤더의 z-index·overflow 에 갇히지 않게 포털로 낸다. */}\n {backdrop && mounted\n ? createPortal(\n <div className={cx('gnb-backdrop', openIndex !== null && 'active')} aria-hidden=\"true\" />,\n document.body,\n )\n : null}\n </>\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────── */\n\n/** 킷 JS 의 지연값. CSS transition 과 짝이라 임의로 줄이면 어긋난다. */\nconst MOBILE_ENTER_DELAY = 100;\nconst MOBILE_LEAVE_DELAY = 400;\n\n/** 킷 CSS 가 PC 주메뉴를 되살리는 폭. 이보다 넓어지면 모바일 서랍을 닫는다. */\nconst PC_MIN_WIDTH = 1024;\n\n/** 초점 가둠 대상. 킷보다 넓게 잡았다. */\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nfunction focusables(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(\n (el) => !el.hasAttribute('inert') && el.getClientRects().length > 0,\n );\n}\n\nexport type MainMenuMobileProps = Omit<ComponentPropsWithRef<'div'>, 'children' | 'id'> & {\n /** 메뉴 트리. PC 주메뉴와 같은 값을 넘긴다. */\n items: MainMenuTop[];\n /** 열림 여부. 상태는 화면 쪽이 쥔다. */\n open: boolean;\n /** 닫아 달라는 요청 — Esc·닫기 단추·PC 폭으로 넓어질 때 온다. */\n onClose: () => void;\n /** 서랍의 id. 여는 단추의 `aria-controls` 와 짝이다. **한 화면에 하나만 둔다.** */\n id?: string;\n /** 서랍의 이름(`aria-label`). 기본 「전체 메뉴」. */\n label?: string;\n /** 닫기 단추의 이름. */\n closeLabel?: string;\n /** 판 제목의 헤딩 단계. 기본 2. */\n titleLevel?: MainMenuTitleLevel;\n /** 서랍 머리 자리 — 유틸 링크·로그인·검색칸. */\n header?: ReactNode;\n /** 서랍 바닥 자리 — 정책 링크 등. */\n bottom?: ReactNode;\n};\n\n/**\n * 전체메뉴(모바일) — 킷 `krds-main-menu-mobile`. 초점을 가두는 대화상자다.\n *\n * 왼쪽 1뎁스는 탭이 아니라 **앵커**다 — 누르면 오른쪽이 그 자리로 스크롤하고 선택 표시가 따라간다.\n * **1024px 이상에서는 킷이 이 요소를 숨긴다** — PC 는 {@link MainMenu} 가 맡는다.\n * 화면이 PC 폭으로 넓어지면 스스로 닫는다(그대로 두면 닫을 길이 사라진다).\n */\nexport function MainMenuMobile({\n items,\n open,\n onClose,\n id = 'mobile-nav',\n label = '전체 메뉴',\n closeLabel = '전체메뉴 닫기',\n titleLevel = 2,\n header,\n bottom,\n className,\n style,\n onKeyDown,\n ...rest\n}: MainMenuMobileProps) {\n const uid = idBase(useId());\n const wrapRef = useRef<HTMLDivElement | null>(null);\n const bodyRef = useRef<HTMLDivElement | null>(null);\n /** `display:block` — 닫히는 동안에도 유지한다. */\n const [shown, setShown] = useState(false);\n /** `.is-open` · `.is-backdrop` — transition 을 태우는 클래스. */\n const [entered, setEntered] = useState(false);\n const [activeTab, setActiveTab] = useState(0);\n const [openDepth3, setOpenDepth3] = useState<Record<string, boolean>>({});\n\n const tabId = (index: number) => `${uid}-m-tab-${index}`;\n const panelId = (index: number) => `${uid}-m-panel-${index}`;\n\n // 열림 — 스크롤 잠금 · 상태 클래스 · 초점 이동. 정리 단계에서 잠금 해제와 초점 복귀.\n useEffect(() => {\n if (!open) return;\n const restore = document.activeElement as HTMLElement | null;\n setShown(true);\n document.body.classList.add('is-gnb-mobile');\n\n const enterTimer = window.setTimeout(() => {\n setEntered(true);\n wrapRef.current?.focus();\n }, MOBILE_ENTER_DELAY);\n\n return () => {\n window.clearTimeout(enterTimer);\n setEntered(false);\n document.body.classList.remove('is-gnb-mobile');\n restore?.focus?.();\n };\n }, [open]);\n\n // 닫힘 — 클래스를 뗀 뒤 transition 만큼 기다렸다가 DOM 에서 감춘다.\n useEffect(() => {\n if (open || !shown) return;\n const timer = window.setTimeout(() => setShown(false), MOBILE_LEAVE_DELAY);\n return () => window.clearTimeout(timer);\n }, [open, shown]);\n\n // 화면이 PC 폭으로 넓어지면 닫는다 — 킷 CSS 가 이 서랍을 숨겨 버려 닫을 길이 사라지기 때문이다.\n useEffect(() => {\n if (!open) return;\n const query = window.matchMedia(`(min-width: ${PC_MIN_WIDTH}px)`);\n const handle = () => {\n if (query.matches) onClose();\n };\n handle();\n query.addEventListener('change', handle);\n return () => query.removeEventListener('change', handle);\n }, [open, onClose]);\n\n /** 오른쪽이 스크롤하면 왼쪽 선택 표시를 따라 옮긴다 — 킷 `setupAnchorScroll`. */\n useEffect(() => {\n const body = bodyRef.current;\n if (!open || !body) return;\n const handle = () => {\n const bottomReached = body.scrollTop + body.clientHeight >= body.scrollHeight - 1;\n if (bottomReached) {\n setActiveTab(items.length - 1);\n return;\n }\n let next = 0;\n items.forEach((_, index) => {\n const panel = document.getElementById(panelId(index));\n if (panel && body.scrollTop >= panel.offsetTop) next = index;\n });\n setActiveTab(next);\n };\n body.addEventListener('scroll', handle, { passive: true });\n return () => body.removeEventListener('scroll', handle);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, items]);\n\n function handleTabClick(event: ReactMouseEvent<HTMLAnchorElement>, index: number) {\n event.preventDefault();\n setActiveTab(index);\n const panel = document.getElementById(panelId(index));\n if (panel) bodyRef.current?.scrollTo({ top: panel.offsetTop, left: 0, behavior: 'smooth' });\n }\n\n /** Esc 로 닫고, Tab 초점을 서랍 안에 가둔다. */\n function handleKeyDown(event: ReactKeyboardEvent<HTMLDivElement>) {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n\n if (event.key === 'Escape') {\n event.stopPropagation();\n onClose();\n return;\n }\n if (event.key !== 'Tab') return;\n\n const wrap = wrapRef.current;\n if (!wrap) return;\n const nodes = focusables(wrap);\n if (nodes.length === 0) {\n event.preventDefault();\n wrap.focus();\n return;\n }\n const first = nodes[0];\n const last = nodes[nodes.length - 1];\n if (event.shiftKey && document.activeElement === first) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault();\n first.focus();\n }\n }\n\n const Title = `h${titleLevel}` as 'h2';\n\n return (\n <div\n id={id}\n className={cx('krds-main-menu-mobile', entered && 'is-open', entered && 'is-backdrop', className)}\n style={{ display: shown ? 'block' : 'none', ...style }}\n onKeyDown={handleKeyDown}\n {...rest}\n >\n <div\n ref={wrapRef}\n className=\"gnb-wrap\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={label}\n tabIndex={-1}\n >\n {header && <div className=\"gnb-header\">{header}</div>}\n\n <div className=\"gnb-body\" ref={bodyRef}>\n {/* 규격 권장 문구 — 컨테이너에 「메인 메뉴」. 바깥 대화상자 이름과 겹치지 않게 나눈다. */}\n <div className=\"gnb-menu\" role=\"navigation\" aria-label=\"메인 메뉴\">\n <div className=\"menu-wrap\">\n <ul role=\"tablist\">\n {items.map((top, index) => (\n <li key={key(top, index)} role=\"none\">\n <a\n href={`#${panelId(index)}`}\n id={tabId(index)}\n className={cx('gnb-main-trigger', activeTab === index && 'active')}\n role=\"tab\"\n aria-selected={activeTab === index}\n aria-controls={panelId(index)}\n onClick={(event) => handleTabClick(event, index)}\n >\n {top.label}\n </a>\n </li>\n ))}\n </ul>\n </div>\n\n <div className=\"submenu-wrap\">\n {items.map((top, index) => (\n <div\n key={key(top, index)}\n id={panelId(index)}\n className=\"gnb-sub-list\"\n role=\"tabpanel\"\n aria-labelledby={tabId(index)}\n >\n <Title className=\"sub-title\">{top.label}</Title>\n <ul>\n {(top.items ?? []).map((group, groupIndex) => {\n const groupKey = `${index}-${groupIndex}`;\n const items3 = group.items ?? [];\n\n if (items3.length === 0) {\n return (\n <li key={key(group, groupIndex)}>\n <MenuAction\n href={group.href}\n newWindow={group.newWindow}\n className={cx('gnb-sub-trigger', group.current && 'selected')}\n aria-current={group.current ? 'page' : undefined}\n onClick={group.onSelect}\n >\n {group.label}\n </MenuAction>\n </li>\n );\n }\n\n const expanded = openDepth3[groupKey] ?? false;\n const depth3Id = `${panelId(index)}-d3-${groupIndex}`;\n\n return (\n <li key={key(group, groupIndex)}>\n <button\n type=\"button\"\n className={cx(\n 'gnb-sub-trigger',\n 'has-depth3',\n expanded && 'active',\n group.current && 'selected',\n )}\n aria-expanded={expanded}\n aria-controls={depth3Id}\n onClick={() => {\n setOpenDepth3((current) => ({\n ...current,\n [groupKey]: !expanded,\n }));\n group.onSelect?.();\n }}\n >\n {group.label}\n </button>\n <div id={depth3Id} className={cx('depth3-wrap', expanded && 'is-open')}>\n <ul>\n {items3.map((item, itemIndex) => (\n <li key={key(item, itemIndex)}>\n <MenuAction\n href={item.href}\n newWindow={item.newWindow}\n className={cx('depth3-trigger', item.current && 'active')}\n aria-current={item.current ? 'page' : undefined}\n onClick={item.onSelect}\n >\n {item.label}\n </MenuAction>\n </li>\n ))}\n </ul>\n </div>\n </li>\n );\n })}\n </ul>\n </div>\n ))}\n </div>\n </div>\n\n {bottom && <div className=\"gnb-bottom\">{bottom}</div>}\n </div>\n\n {/* 킷 CSS 가 `#close-nav` 아이디로 위치를 잡는다 — 바꾸면 단추가 제자리에 놓이지 않는다. */}\n <button type=\"button\" className=\"krds-btn medium icon\" id=\"close-nav\" onClick={onClose}>\n <span className=\"sr-only\">{closeLabel}</span>\n <i className=\"svg-icon ico-popup-close\" aria-hidden=\"true\" />\n </button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,25 @@
1
+ import type { ComponentPropsWithRef, ReactNode } from 'react';
2
+ /**
3
+ * 공식 배너(마스트헤드) — KRDS `#krds-masthead`.
4
+ *
5
+ * 「이 누리집은 대한민국 공식 전자정부 누리집입니다」 띠. 모든 화면 최상단,
6
+ * `SkipLink` 뒤·`Header` 앞에 같은 자리로 둔다(순서는 셸이 지켜야 한다).
7
+ * 문구와 구조는 규격이 변형을 금지한다 — `children` 은 번역을 넣는 자리다.
8
+ * 기관 식별은 푸터의 `Identifier` 가 맡는다.
9
+ *
10
+ * @example
11
+ * <Masthead />
12
+ * <Masthead label="공식 누리집 안내">{t('masthead')}</Masthead>
13
+ *
14
+ * 자세히: docs/krds/09-부품-노트.md#마스트헤드
15
+ */
16
+ /** KRDS 가 규정한 배너 문구. 변형 금지 — 번역 외에 손대지 않는다. */
17
+ export declare const MASTHEAD_TEXT = "\uC774 \uB204\uB9AC\uC9D1\uC740 \uB300\uD55C\uBBFC\uAD6D \uACF5\uC2DD \uC804\uC790\uC815\uBD80 \uB204\uB9AC\uC9D1\uC785\uB2C8\uB2E4.";
18
+ export type MastheadProps = Omit<ComponentPropsWithRef<'div'>, 'id' | 'children'> & {
19
+ /** 배너 문구. 생략하면 KRDS 원문. 번역본 말고 다른 문구를 넣지 마라. */
20
+ children?: ReactNode;
21
+ /** 주면 `role="region" aria-label` 로 랜드마크가 된다. 본문과 같은 문구를 넣지 마라(두 번 읽힌다). */
22
+ label?: string;
23
+ };
24
+ export declare function Masthead({ children, label, className, ...rest }: MastheadProps): import("react").JSX.Element;
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/masthead/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAG9D;;;;;;;;;;;;;GAaG;AAEH,+CAA+C;AAC/C,eAAO,MAAM,aAAa,yIAAgC,CAAC;AAE3D,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,EAAE,IAAI,GAAG,UAAU,CAAC,GAAG;IAClF,gDAAgD;IAChD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,aAAa,+BAqB9E"}
@@ -0,0 +1,21 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cx } from "../lib/krds.js";
3
+ const MASTHEAD_TEXT = "\uC774 \uB204\uB9AC\uC9D1\uC740 \uB300\uD55C\uBBFC\uAD6D \uACF5\uC2DD \uC804\uC790\uC815\uBD80 \uB204\uB9AC\uC9D1\uC785\uB2C8\uB2E4.";
4
+ function Masthead({ children, label, className, ...rest }) {
5
+ return /* @__PURE__ */ jsx(
6
+ "div",
7
+ {
8
+ id: "krds-masthead",
9
+ role: label ? "region" : void 0,
10
+ "aria-label": label,
11
+ className: cx(className) || void 0,
12
+ ...rest,
13
+ children: /* @__PURE__ */ jsx("div", { className: "toggle-wrap", children: /* @__PURE__ */ jsx("div", { className: "toggle-head", children: /* @__PURE__ */ jsx("div", { className: "inner", children: /* @__PURE__ */ jsx("span", { className: "nuri-txt", children: children ?? MASTHEAD_TEXT }) }) }) })
14
+ }
15
+ );
16
+ }
17
+ export {
18
+ MASTHEAD_TEXT,
19
+ Masthead
20
+ };
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/masthead/index.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { cx } from '../lib/krds';\n\n/**\n * 공식 배너(마스트헤드) — KRDS `#krds-masthead`.\n *\n * 「이 누리집은 대한민국 공식 전자정부 누리집입니다」 띠. 모든 화면 최상단,\n * `SkipLink` 뒤·`Header` 앞에 같은 자리로 둔다(순서는 셸이 지켜야 한다).\n * 문구와 구조는 규격이 변형을 금지한다 — `children` 은 번역을 넣는 자리다.\n * 기관 식별은 푸터의 `Identifier` 가 맡는다.\n *\n * @example\n * <Masthead />\n * <Masthead label=\"공식 누리집 안내\">{t('masthead')}</Masthead>\n *\n * 자세히: docs/krds/09-부품-노트.md#마스트헤드\n */\n\n/** KRDS 가 규정한 배너 문구. 변형 금지 — 번역 외에 손대지 않는다. */\nexport const MASTHEAD_TEXT = '이 누리집은 대한민국 공식 전자정부 누리집입니다.';\n\nexport type MastheadProps = Omit<ComponentPropsWithRef<'div'>, 'id' | 'children'> & {\n /** 배너 문구. 생략하면 KRDS 원문. 번역본 말고 다른 문구를 넣지 마라. */\n children?: ReactNode;\n /** 주면 `role=\"region\" aria-label` 로 랜드마크가 된다. 본문과 같은 문구를 넣지 마라(두 번 읽힌다). */\n label?: string;\n};\n\nexport function Masthead({ children, label, className, ...rest }: MastheadProps) {\n return (\n <div\n id=\"krds-masthead\"\n role={label ? 'region' : undefined}\n aria-label={label}\n // 뿌리에는 킷 클래스가 없다(모양이 `#krds-masthead` 아이디에 걸려 있다).\n // 합칠 게 className 뿐이라 비면 속성을 아예 빼서 `class=\"\"` 를 남기지 않는다.\n className={cx(className) || undefined}\n {...rest}\n >\n {/* `.toggle-wrap` 은 킷 CSS 에 규칙이 없지만 규격이 구조 변형을 금지해 샘플 그대로 둔다. */}\n <div className=\"toggle-wrap\">\n <div className=\"toggle-head\">\n <div className=\"inner\">\n <span className=\"nuri-txt\">{children ?? MASTHEAD_TEXT}</span>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"AA2CY;AA1CZ,SAAS,UAAU;AAkBZ,MAAM,gBAAgB;AAStB,SAAS,SAAS,EAAE,UAAU,OAAO,WAAW,GAAG,KAAK,GAAkB;AAC/E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,IAAG;AAAA,MACH,MAAM,QAAQ,WAAW;AAAA,MACzB,cAAY;AAAA,MAGZ,WAAW,GAAG,SAAS,KAAK;AAAA,MAC3B,GAAG;AAAA,MAGJ,8BAAC,SAAI,WAAU,eACb,8BAAC,SAAI,WAAU,eACb,8BAAC,SAAI,WAAU,SACb,8BAAC,UAAK,WAAU,YAAY,sBAAY,eAAc,GACxD,GACF,GACF;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -0,0 +1,105 @@
1
+ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ import { type KrdsSize } from '../lib/krds';
3
+ /**
4
+ * 모달(대화 상자) — KRDS `krds-modal`.
5
+ *
6
+ * **최후의 수단이다.** 폼 오류·성공은 `Field` 의 인라인 문구, 저장 결과는 `Toast` 로 낸다.
7
+ * 폭은 `size` 3단에서 고르고 픽셀을 손으로 넘기지 않는다.
8
+ * 확정·취소는 `Modal.Footer` 에 두고 제출이 가장 오른쪽이다.
9
+ * 승인이 필요한 모달에는 `Modal.Close` 를 두지 않고 `dismissible={false}` 로 Esc 도 막는다.
10
+ * 옆에서 미끄러져 나오는 패널은 `ui/drawer` 다.
11
+ *
12
+ * @example
13
+ * <Modal.Root open={open} onClose={close} size="medium">
14
+ * <Modal.Header>정말 삭제할까요?</Modal.Header>
15
+ * <Modal.Body>지운 글은 되살릴 수 없습니다.</Modal.Body>
16
+ * <Modal.Footer>
17
+ * <Button variant="tertiary" onClick={close}>아니요</Button>
18
+ * <Button variant="primary" onClick={remove}>예</Button>
19
+ * </Modal.Footer>
20
+ * <Modal.Close />
21
+ * </Modal.Root>
22
+ *
23
+ * 자세히: docs/krds/09-부품-노트.md#모달
24
+ */
25
+ /** 폭 3단 — 400 / 560 / 760px. 픽셀을 손으로 넘기지 말고 여기서 고른다. */
26
+ export type ModalSize = Extract<KrdsSize, 'small' | 'medium' | 'large'>;
27
+ /** 유형. `bottom-sheet` 는 킷 안에서 모바일 주메뉴가 쓰던 형태다 — 범용 서랍 대용으로 쓰지 마라. */
28
+ export type ModalType = 'dialog' | 'full' | 'bottom-sheet';
29
+ export type ModalRootProps = ComponentPropsWithRef<'section'> & {
30
+ /** 열림 여부. 상태는 화면 쪽이 쥔다. */
31
+ open: boolean;
32
+ /** 닫아 달라는 요청 — Esc·`Modal.Close`·(켰다면) 딤 클릭에서 온다. */
33
+ onClose: () => void;
34
+ /** 기본 medium(560px). 3단에서 고른다. */
35
+ size?: ModalSize;
36
+ /** 유형. 기본 `dialog`. */
37
+ type?: ModalType;
38
+ /** Esc 로 닫을 수 있는가. 승인이 필요한 모달에서는 false. */
39
+ dismissible?: boolean;
40
+ /** 딤을 눌렀을 때 닫는가. 기본 false 가 킷 동작이다(초점만 되돌린다). */
41
+ closeOnBackdrop?: boolean;
42
+ /** 열릴 때 첫 초점. 기본 `dialog`(보조기술이 이름부터 읽는다) · `first` 는 첫 초점 요소로 보낸다. */
43
+ initialFocus?: 'dialog' | 'first';
44
+ /** 포털 대상. 기본 `document.body`. */
45
+ container?: Element | null;
46
+ /** `.modal-dialog` 에 붙일 클래스. */
47
+ dialogClassName?: string;
48
+ /** `.modal-content` 에 붙일 클래스. */
49
+ contentClassName?: string;
50
+ children?: ReactNode;
51
+ };
52
+ /**
53
+ * 모달 바깥틀. 포털·초점 가둠·Esc·스크롤 잠금·초점 복귀를 모두 여기서 한다.
54
+ *
55
+ * `Modal.Header` 를 두면 `aria-labelledby` 가 자동으로 붙는다. 헤더 없이 쓸 거면
56
+ * `aria-label` 을 직접 넘겨라 — 대화상자에는 반드시 이름이 있어야 한다.
57
+ */
58
+ export declare function ModalRoot({ open, onClose, size, type, dismissible, closeOnBackdrop, initialFocus, container, dialogClassName, contentClassName, className, style, children, onKeyDown, onMouseDown, ...rest }: ModalRootProps): import("react").ReactPortal | null;
59
+ export type ModalHeaderProps = ComponentPropsWithRef<'div'> & {
60
+ /** 제목 태그 단계. 기본 h2 — 화면의 제목 위계에 맞춘다. */
61
+ level?: 1 | 2 | 3 | 4 | 5 | 6;
62
+ /** 제목을 눈에서만 감춘다. 이름 자체는 남는다. */
63
+ hideTitle?: boolean;
64
+ /** 제목 요소에 붙일 클래스. */
65
+ titleClassName?: string;
66
+ /** 제목 오른쪽에 놓을 것. 바텀시트 헤더가 space-between 이라 쓸모 있다. */
67
+ extra?: ReactNode;
68
+ };
69
+ /** 제목 줄 — 킷 `modal-header > .modal-title`. **한 줄로 잘리므로 긴 제목을 넣지 마라.** */
70
+ export declare function ModalHeader({ level, hideTitle, titleClassName, extra, className, children, ...rest }: ModalHeaderProps): import("react").JSX.Element;
71
+ export type ModalBodyProps = ComponentPropsWithRef<'div'> & {
72
+ /** 안쪽 `conts-area` 에 붙일 클래스. 바깥 `modal-conts` 는 `className` 이 받는다. */
73
+ areaClassName?: string;
74
+ };
75
+ /** 본문 — 킷 `modal-conts > .conts-area`. 스크롤이 생기면 `tabindex="0"` 을 붙인다. */
76
+ export declare function ModalBody({ className, areaClassName, children, ref, ...rest }: ModalBodyProps): import("react").JSX.Element;
77
+ export type ModalFooterProps = ComponentPropsWithRef<'div'> & {
78
+ /** 좌우로 갈라 놓는다(킷 `multi-conts`). 왼쪽 보조 액션, 오른쪽 확정 액션. */
79
+ split?: boolean;
80
+ };
81
+ /**
82
+ * 푸터 — 확정·취소 액션 자리. 제출(primary)이 가장 오른쪽이다.
83
+ * 정렬·간격·버튼 최소폭은 킷이 잡는다 — 여백을 손으로 붙이지 마라.
84
+ */
85
+ export declare function ModalFooter({ split, className, children, ...rest }: ModalFooterProps): import("react").JSX.Element;
86
+ export type ModalCloseProps = Omit<ComponentPropsWithRef<'button'>, 'children'> & {
87
+ /** 스크린리더가 읽을 이름. 아이콘만 있는 버튼이라 반드시 있어야 한다. */
88
+ label?: string;
89
+ size?: KrdsSize;
90
+ };
91
+ /**
92
+ * 오른쪽 위 닫기 버튼. **`Modal.Root` 의 마지막 자식으로 두어라** —
93
+ * 그래야 Tab 순서에서 본문·푸터를 지난 뒤 마지막에 닿는다.
94
+ * 승인이 필요한 모달에는 이 버튼을 두지 않는다.
95
+ */
96
+ export declare function ModalClose({ label, size, className, onClick, type, ...rest }: ModalCloseProps): import("react").JSX.Element;
97
+ /** 합성용 묶음. `Root` · `Header` · `Body` · `Footer` · `Close`. */
98
+ export declare const Modal: {
99
+ Root: typeof ModalRoot;
100
+ Header: typeof ModalHeader;
101
+ Body: typeof ModalBody;
102
+ Footer: typeof ModalFooter;
103
+ Close: typeof ModalClose;
104
+ };
105
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/modal/index.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,285 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useEffect,
7
+ useId,
8
+ useMemo,
9
+ useRef,
10
+ useState
11
+ } from "react";
12
+ import { createPortal } from "react-dom";
13
+ import { cx } from "../lib/krds.js";
14
+ const SIZE_CLASS = {
15
+ small: "modal-sm",
16
+ medium: "modal-md",
17
+ large: "modal-lg"
18
+ };
19
+ const ENTER_DELAY = 150;
20
+ const FOCUS_DELAY = 350;
21
+ const LEAVE_DELAY = 350;
22
+ const FOCUSABLE_SELECTOR = [
23
+ "a[href]",
24
+ "area[href]",
25
+ "button:not([disabled])",
26
+ 'input:not([disabled]):not([type="hidden"])',
27
+ "select:not([disabled])",
28
+ "textarea:not([disabled])",
29
+ "details > summary",
30
+ "audio[controls]",
31
+ "video[controls]",
32
+ '[contenteditable]:not([contenteditable="false"])',
33
+ '[tabindex]:not([tabindex="-1"])'
34
+ ].join(",");
35
+ function focusables(root) {
36
+ return Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
37
+ (el) => !el.hasAttribute("inert") && (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0)
38
+ );
39
+ }
40
+ let openModals = 0;
41
+ let scrollLocks = 0;
42
+ let savedOverflow = "";
43
+ function lockScroll() {
44
+ if (scrollLocks++ > 0) return;
45
+ savedOverflow = document.body.style.overflow;
46
+ document.body.style.overflow = "hidden";
47
+ }
48
+ function unlockScroll() {
49
+ scrollLocks = Math.max(0, scrollLocks - 1);
50
+ if (scrollLocks > 0) return;
51
+ document.body.style.overflow = savedOverflow;
52
+ }
53
+ const ModalContext = createContext(null);
54
+ function useModalContext(name) {
55
+ const ctx = useContext(ModalContext);
56
+ if (!ctx) throw new Error(`<${name}> \uC740 <Modal.Root> \uC548\uC5D0\uC11C\uB9CC \uC4F8 \uC218 \uC788\uB2E4.`);
57
+ return ctx;
58
+ }
59
+ function ModalRoot({
60
+ open,
61
+ onClose,
62
+ size = "medium",
63
+ type = "dialog",
64
+ dismissible = true,
65
+ closeOnBackdrop = false,
66
+ initialFocus = "dialog",
67
+ container,
68
+ dialogClassName,
69
+ contentClassName,
70
+ className,
71
+ style,
72
+ children,
73
+ onKeyDown,
74
+ onMouseDown,
75
+ ...rest
76
+ }) {
77
+ const reactId = useId();
78
+ const titleId = `${reactId}-title`;
79
+ const contentRef = useRef(null);
80
+ const [mounted, setMounted] = useState(false);
81
+ const [shown, setShown] = useState(false);
82
+ const [entered, setEntered] = useState(false);
83
+ const [hasTitle, setHasTitle] = useState(false);
84
+ const [stack, setStack] = useState(1);
85
+ const initialFocusRef = useRef(initialFocus);
86
+ initialFocusRef.current = initialFocus;
87
+ useEffect(() => {
88
+ setMounted(true);
89
+ }, []);
90
+ useEffect(() => {
91
+ if (!open) return;
92
+ const restore = document.activeElement;
93
+ openModals += 1;
94
+ setStack(openModals);
95
+ setShown(true);
96
+ lockScroll();
97
+ const enterTimer = window.setTimeout(() => setEntered(true), ENTER_DELAY);
98
+ const focusTimer = window.setTimeout(() => {
99
+ const content = contentRef.current;
100
+ if (!content) return;
101
+ if (initialFocusRef.current === "first") {
102
+ const first = focusables(content)[0];
103
+ if (first) {
104
+ first.focus();
105
+ return;
106
+ }
107
+ }
108
+ content.focus();
109
+ }, FOCUS_DELAY);
110
+ return () => {
111
+ window.clearTimeout(enterTimer);
112
+ window.clearTimeout(focusTimer);
113
+ openModals = Math.max(0, openModals - 1);
114
+ setEntered(false);
115
+ unlockScroll();
116
+ restore?.focus?.();
117
+ };
118
+ }, [open]);
119
+ useEffect(() => {
120
+ if (open || !shown) return;
121
+ const timer = window.setTimeout(() => setShown(false), LEAVE_DELAY);
122
+ return () => window.clearTimeout(timer);
123
+ }, [open, shown]);
124
+ const ctx = useMemo(
125
+ () => ({ titleId, close: onClose, registerTitle: setHasTitle }),
126
+ [titleId, onClose]
127
+ );
128
+ function handleKeyDown(event) {
129
+ onKeyDown?.(event);
130
+ if (event.defaultPrevented) return;
131
+ if (event.key === "Escape" || event.key === "Esc") {
132
+ if (!dismissible) return;
133
+ event.stopPropagation();
134
+ onClose();
135
+ return;
136
+ }
137
+ if (event.key !== "Tab") return;
138
+ const content = contentRef.current;
139
+ if (!content) return;
140
+ const nodes = focusables(content);
141
+ if (nodes.length === 0) {
142
+ event.preventDefault();
143
+ content.focus();
144
+ return;
145
+ }
146
+ const first = nodes[0];
147
+ const last = nodes[nodes.length - 1];
148
+ const active = document.activeElement;
149
+ if (event.shiftKey && (active === first || active === content)) {
150
+ event.preventDefault();
151
+ last.focus();
152
+ } else if (!event.shiftKey && active === last) {
153
+ event.preventDefault();
154
+ first.focus();
155
+ }
156
+ }
157
+ function handleMouseDown(event) {
158
+ onMouseDown?.(event);
159
+ if (event.defaultPrevented) return;
160
+ const target = event.target;
161
+ if (target?.closest(".modal-content")) return;
162
+ if (closeOnBackdrop && dismissible) {
163
+ onClose();
164
+ return;
165
+ }
166
+ contentRef.current?.focus();
167
+ }
168
+ if (!mounted || !shown) return null;
169
+ return createPortal(
170
+ /* @__PURE__ */ jsx(ModalContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxs(
171
+ "section",
172
+ {
173
+ className: cx("krds-modal", "fade", shown && "shown", entered && "in", className),
174
+ role: "dialog",
175
+ "aria-modal": "true",
176
+ "aria-labelledby": hasTitle ? titleId : void 0,
177
+ "data-type": type === "dialog" ? void 0 : type,
178
+ style: stack > 1 ? { zIndex: 1010 + stack, ...style } : style,
179
+ onKeyDown: handleKeyDown,
180
+ onMouseDown: handleMouseDown,
181
+ ...rest,
182
+ children: [
183
+ /* @__PURE__ */ jsx("div", { className: cx("modal-dialog", SIZE_CLASS[size], dialogClassName), children: /* @__PURE__ */ jsx("div", { className: cx("modal-content", contentClassName), ref: contentRef, tabIndex: -1, children }) }),
184
+ /* @__PURE__ */ jsx("div", { className: cx("modal-back", entered && stack === 1 && "in") })
185
+ ]
186
+ }
187
+ ) }),
188
+ container ?? document.body
189
+ );
190
+ }
191
+ function ModalHeader({
192
+ level = 2,
193
+ hideTitle,
194
+ titleClassName,
195
+ extra,
196
+ className,
197
+ children,
198
+ ...rest
199
+ }) {
200
+ const { titleId, registerTitle } = useModalContext("Modal.Header");
201
+ useEffect(() => {
202
+ registerTitle(true);
203
+ return () => registerTitle(false);
204
+ }, [registerTitle]);
205
+ const Heading = `h${level}`;
206
+ return /* @__PURE__ */ jsxs("div", { className: cx("modal-header", className), ...rest, children: [
207
+ /* @__PURE__ */ jsx(Heading, { id: titleId, className: cx(hideTitle ? "sr-only" : "modal-title", titleClassName), children }),
208
+ extra
209
+ ] });
210
+ }
211
+ function ModalBody({ className, areaClassName, children, ref, ...rest }) {
212
+ const innerRef = useRef(null);
213
+ useEffect(() => {
214
+ const node = innerRef.current;
215
+ if (!node) return;
216
+ const sync = () => {
217
+ if (node.scrollHeight > node.clientHeight) node.setAttribute("tabindex", "0");
218
+ else node.removeAttribute("tabindex");
219
+ };
220
+ sync();
221
+ if (typeof ResizeObserver === "undefined") return;
222
+ const observer = new ResizeObserver(sync);
223
+ observer.observe(node);
224
+ for (const child of Array.from(node.children)) observer.observe(child);
225
+ return () => observer.disconnect();
226
+ }, []);
227
+ return /* @__PURE__ */ jsx(
228
+ "div",
229
+ {
230
+ className: cx("modal-conts", className),
231
+ ref: (node) => {
232
+ innerRef.current = node;
233
+ if (typeof ref === "function") ref(node);
234
+ else if (ref) ref.current = node;
235
+ },
236
+ ...rest,
237
+ children: /* @__PURE__ */ jsx("div", { className: cx("conts-area", areaClassName), children })
238
+ }
239
+ );
240
+ }
241
+ function ModalFooter({ split, className, children, ...rest }) {
242
+ return /* @__PURE__ */ jsx("div", { className: cx("modal-btn", "btn-wrap", split && "multi-conts", className), ...rest, children });
243
+ }
244
+ function ModalClose({
245
+ label = "\uB2EB\uAE30",
246
+ size = "medium",
247
+ className,
248
+ onClick,
249
+ type = "button",
250
+ ...rest
251
+ }) {
252
+ const { close } = useModalContext("Modal.Close");
253
+ return /* @__PURE__ */ jsxs(
254
+ "button",
255
+ {
256
+ type,
257
+ className: cx("krds-btn", size, "icon", "btn-close", className),
258
+ onClick: (event) => {
259
+ onClick?.(event);
260
+ if (!event.defaultPrevented) close();
261
+ },
262
+ ...rest,
263
+ children: [
264
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: label }),
265
+ /* @__PURE__ */ jsx("i", { className: "svg-icon ico-popup-close", "aria-hidden": "true" })
266
+ ]
267
+ }
268
+ );
269
+ }
270
+ const Modal = {
271
+ Root: ModalRoot,
272
+ Header: ModalHeader,
273
+ Body: ModalBody,
274
+ Footer: ModalFooter,
275
+ Close: ModalClose
276
+ };
277
+ export {
278
+ Modal,
279
+ ModalBody,
280
+ ModalClose,
281
+ ModalFooter,
282
+ ModalHeader,
283
+ ModalRoot
284
+ };
285
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/modal/index.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type MouseEvent as ReactMouseEvent,\n type KeyboardEvent as ReactKeyboardEvent,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { cx, type KrdsSize } from '../lib/krds';\n\n/**\n * 모달(대화 상자) — KRDS `krds-modal`.\n *\n * **최후의 수단이다.** 폼 오류·성공은 `Field` 의 인라인 문구, 저장 결과는 `Toast` 로 낸다.\n * 폭은 `size` 3단에서 고르고 픽셀을 손으로 넘기지 않는다.\n * 확정·취소는 `Modal.Footer` 에 두고 제출이 가장 오른쪽이다.\n * 승인이 필요한 모달에는 `Modal.Close` 를 두지 않고 `dismissible={false}` 로 Esc 도 막는다.\n * 옆에서 미끄러져 나오는 패널은 `ui/drawer` 다.\n *\n * @example\n * <Modal.Root open={open} onClose={close} size=\"medium\">\n * <Modal.Header>정말 삭제할까요?</Modal.Header>\n * <Modal.Body>지운 글은 되살릴 수 없습니다.</Modal.Body>\n * <Modal.Footer>\n * <Button variant=\"tertiary\" onClick={close}>아니요</Button>\n * <Button variant=\"primary\" onClick={remove}>예</Button>\n * </Modal.Footer>\n * <Modal.Close />\n * </Modal.Root>\n *\n * 자세히: docs/krds/09-부품-노트.md#모달\n */\n\n/** 폭 3단 — 400 / 560 / 760px. 픽셀을 손으로 넘기지 말고 여기서 고른다. */\nexport type ModalSize = Extract<KrdsSize, 'small' | 'medium' | 'large'>;\n\nconst SIZE_CLASS: Record<ModalSize, string> = {\n small: 'modal-sm',\n medium: 'modal-md',\n large: 'modal-lg',\n};\n\n/** 유형. `bottom-sheet` 는 킷 안에서 모바일 주메뉴가 쓰던 형태다 — 범용 서랍 대용으로 쓰지 마라. */\nexport type ModalType = 'dialog' | 'full' | 'bottom-sheet';\n\n/**\n * 킷 JS 가 쓰는 지연값. 킷 CSS transition 과 짝이라 임의로 줄이면 어긋난다.\n * `.in` 전까지 `visibility:hidden` 이라 초점 이동도 기다려야 한다.\n */\nconst ENTER_DELAY = 150;\nconst FOCUS_DELAY = 350;\nconst LEAVE_DELAY = 350;\n\n/** 초점 가둠 대상. 킷 JS 보다 넓게 잡았다. */\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n 'details > summary',\n 'audio[controls]',\n 'video[controls]',\n '[contenteditable]:not([contenteditable=\"false\"])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nfunction focusables(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(\n (el) =>\n !el.hasAttribute('inert') &&\n (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0),\n );\n}\n\n/** 열려 있는 모달 수. 겹쳐 열릴 때 z-index 와 딤 중복을 킷 JS 와 같은 방식으로 처리한다. */\nlet openModals = 0;\n\n/**\n * 뒤 문서 스크롤 잠금. 킷의 `body.scroll-no` 는 CSS 규칙이 없어 `body.style.overflow` 를 직접 다룬다.\n * 겹쳐 열릴 수 있으니 셈해서 마지막 하나가 닫힐 때만 되돌린다.\n */\nlet scrollLocks = 0;\nlet savedOverflow = '';\n\nfunction lockScroll() {\n if (scrollLocks++ > 0) return;\n savedOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n}\n\nfunction unlockScroll() {\n scrollLocks = Math.max(0, scrollLocks - 1);\n if (scrollLocks > 0) return;\n document.body.style.overflow = savedOverflow;\n}\n\ntype ModalContextValue = {\n /** 제목 요소의 id. `Modal.Header` 가 붙이고 `Modal.Root` 가 `aria-labelledby` 로 가리킨다. */\n titleId: string;\n close: () => void;\n /** 제목이 실제로 있는지 알린다 — 없으면 `aria-labelledby` 가 허공을 가리키게 되므로 붙이지 않는다. */\n registerTitle: (has: boolean) => void;\n};\n\nconst ModalContext = createContext<ModalContextValue | null>(null);\n\nfunction useModalContext(name: string): ModalContextValue {\n const ctx = useContext(ModalContext);\n if (!ctx) throw new Error(`<${name}> 은 <Modal.Root> 안에서만 쓸 수 있다.`);\n return ctx;\n}\n\nexport type ModalRootProps = ComponentPropsWithRef<'section'> & {\n /** 열림 여부. 상태는 화면 쪽이 쥔다. */\n open: boolean;\n /** 닫아 달라는 요청 — Esc·`Modal.Close`·(켰다면) 딤 클릭에서 온다. */\n onClose: () => void;\n /** 기본 medium(560px). 3단에서 고른다. */\n size?: ModalSize;\n /** 유형. 기본 `dialog`. */\n type?: ModalType;\n /** Esc 로 닫을 수 있는가. 승인이 필요한 모달에서는 false. */\n dismissible?: boolean;\n /** 딤을 눌렀을 때 닫는가. 기본 false 가 킷 동작이다(초점만 되돌린다). */\n closeOnBackdrop?: boolean;\n /** 열릴 때 첫 초점. 기본 `dialog`(보조기술이 이름부터 읽는다) · `first` 는 첫 초점 요소로 보낸다. */\n initialFocus?: 'dialog' | 'first';\n /** 포털 대상. 기본 `document.body`. */\n container?: Element | null;\n /** `.modal-dialog` 에 붙일 클래스. */\n dialogClassName?: string;\n /** `.modal-content` 에 붙일 클래스. */\n contentClassName?: string;\n children?: ReactNode;\n};\n\n/**\n * 모달 바깥틀. 포털·초점 가둠·Esc·스크롤 잠금·초점 복귀를 모두 여기서 한다.\n *\n * `Modal.Header` 를 두면 `aria-labelledby` 가 자동으로 붙는다. 헤더 없이 쓸 거면\n * `aria-label` 을 직접 넘겨라 — 대화상자에는 반드시 이름이 있어야 한다.\n */\nexport function ModalRoot({\n open,\n onClose,\n size = 'medium',\n type = 'dialog',\n dismissible = true,\n closeOnBackdrop = false,\n initialFocus = 'dialog',\n container,\n dialogClassName,\n contentClassName,\n className,\n style,\n children,\n onKeyDown,\n onMouseDown,\n ...rest\n}: ModalRootProps) {\n const reactId = useId();\n const titleId = `${reactId}-title`;\n const contentRef = useRef<HTMLDivElement | null>(null);\n const [mounted, setMounted] = useState(false);\n /** `.shown` — display:block. 닫힌 뒤에도 사라지는 동안 유지한다. */\n const [shown, setShown] = useState(false);\n /** `.in` — opacity·visibility·z-index. */\n const [entered, setEntered] = useState(false);\n const [hasTitle, setHasTitle] = useState(false);\n /** 몇 번째로 열린 모달인가(1부터). */\n const [stack, setStack] = useState(1);\n\n // 초점 정책이 도중에 바뀌어도 열림 효과가 다시 돌지 않게 ref 로 읽는다.\n const initialFocusRef = useRef(initialFocus);\n initialFocusRef.current = initialFocus;\n\n // 서버에서는 포털을 만들지 않는다(hydration 어긋남 방지).\n useEffect(() => {\n setMounted(true);\n }, []);\n\n // 열림 — 스크롤 잠금 · 상태 클래스 · 초점 이동. 정리 단계에서 잠금 해제와 초점 복귀.\n useEffect(() => {\n if (!open) return;\n const restore = document.activeElement as HTMLElement | null;\n openModals += 1;\n setStack(openModals);\n setShown(true);\n lockScroll();\n\n const enterTimer = window.setTimeout(() => setEntered(true), ENTER_DELAY);\n const focusTimer = window.setTimeout(() => {\n const content = contentRef.current;\n if (!content) return;\n if (initialFocusRef.current === 'first') {\n const first = focusables(content)[0];\n if (first) {\n first.focus();\n return;\n }\n }\n content.focus();\n }, FOCUS_DELAY);\n\n return () => {\n window.clearTimeout(enterTimer);\n window.clearTimeout(focusTimer);\n openModals = Math.max(0, openModals - 1);\n setEntered(false);\n unlockScroll();\n // 모달을 열었던 요소로 초점 복귀 — 킷 `returnFocusToTrigger` 에 해당한다.\n restore?.focus?.();\n };\n }, [open]);\n\n // 닫힘 — `.in` 을 뗀 뒤 transition 만큼 기다렸다가 `.shown`(=DOM)을 뗀다.\n useEffect(() => {\n if (open || !shown) return;\n const timer = window.setTimeout(() => setShown(false), LEAVE_DELAY);\n return () => window.clearTimeout(timer);\n }, [open, shown]);\n\n const ctx = useMemo<ModalContextValue>(\n () => ({ titleId, close: onClose, registerTitle: setHasTitle }),\n [titleId, onClose],\n );\n\n function handleKeyDown(event: ReactKeyboardEvent<HTMLElement>) {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n\n if (event.key === 'Escape' || event.key === 'Esc') {\n if (!dismissible) return;\n // 겹쳐 열린 경우 바깥 모달까지 닫히지 않도록 여기서 끊는다.\n event.stopPropagation();\n onClose();\n return;\n }\n\n if (event.key !== 'Tab') return;\n const content = contentRef.current;\n if (!content) return;\n const nodes = focusables(content);\n if (nodes.length === 0) {\n event.preventDefault();\n content.focus();\n return;\n }\n const first = nodes[0];\n const last = nodes[nodes.length - 1];\n const active = document.activeElement;\n if (event.shiftKey && (active === first || active === content)) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first.focus();\n }\n }\n\n function handleMouseDown(event: ReactMouseEvent<HTMLElement>) {\n onMouseDown?.(event);\n if (event.defaultPrevented) return;\n const target = event.target as HTMLElement | null;\n if (target?.closest('.modal-content')) return;\n if (closeOnBackdrop && dismissible) {\n onClose();\n return;\n }\n // 킷 기본 동작 — 바깥을 눌러도 닫지 않고 초점만 대화상자로 되돌린다.\n contentRef.current?.focus();\n }\n\n if (!mounted || !shown) return null;\n\n return createPortal(\n <ModalContext.Provider value={ctx}>\n <section\n // `fade` 는 샘플이 늘 붙이지만 킷 CSS 에 규칙이 없다(실측 0건). 샘플 충실도로 남긴다.\n className={cx('krds-modal', 'fade', shown && 'shown', entered && 'in', className)}\n role=\"dialog\"\n // 킷 샘플에는 없다. 대화상자 밖을 보조기술이 읽지 않게 하는 표준 속성이라 더했다.\n // (킷 JS 는 대신 `#wrap` 에 inert 를 건다 — 겹쳐 열릴 때 어긋나므로 쓰지 않았다.)\n aria-modal=\"true\"\n aria-labelledby={hasTitle ? titleId : undefined}\n data-type={type === 'dialog' ? undefined : type}\n // 겹쳐 열릴 때만 킷 `updateZIndex` 와 같은 값을 준다(기본은 킷 CSS 의 1010).\n style={stack > 1 ? { zIndex: 1010 + stack, ...style } : style}\n onKeyDown={handleKeyDown}\n onMouseDown={handleMouseDown}\n {...rest}\n >\n <div className={cx('modal-dialog', SIZE_CLASS[size], dialogClassName)}>\n {/* tabIndex -1 — 초점을 받되 Tab 순서에는 들어가지 않는다. 킷 CSS 에 `:focus` 테두리가 있다. */}\n <div className={cx('modal-content', contentClassName)} ref={contentRef} tabIndex={-1}>\n {children}\n </div>\n </div>\n {/* 딤. 겹쳐 열린 두 번째부터는 킷과 같이 켜지 않는다(딤이 겹쳐 어두워지는 것을 막는다). */}\n <div className={cx('modal-back', entered && stack === 1 && 'in')} />\n </section>\n </ModalContext.Provider>,\n container ?? document.body,\n );\n}\n\nexport type ModalHeaderProps = ComponentPropsWithRef<'div'> & {\n /** 제목 태그 단계. 기본 h2 — 화면의 제목 위계에 맞춘다. */\n level?: 1 | 2 | 3 | 4 | 5 | 6;\n /** 제목을 눈에서만 감춘다. 이름 자체는 남는다. */\n hideTitle?: boolean;\n /** 제목 요소에 붙일 클래스. */\n titleClassName?: string;\n /** 제목 오른쪽에 놓을 것. 바텀시트 헤더가 space-between 이라 쓸모 있다. */\n extra?: ReactNode;\n};\n\n/** 제목 줄 — 킷 `modal-header > .modal-title`. **한 줄로 잘리므로 긴 제목을 넣지 마라.** */\nexport function ModalHeader({\n level = 2,\n hideTitle,\n titleClassName,\n extra,\n className,\n children,\n ...rest\n}: ModalHeaderProps) {\n const { titleId, registerTitle } = useModalContext('Modal.Header');\n\n useEffect(() => {\n registerTitle(true);\n return () => registerTitle(false);\n }, [registerTitle]);\n\n const Heading = `h${level}` as 'h2';\n\n return (\n <div className={cx('modal-header', className)} {...rest}>\n <Heading id={titleId} className={cx(hideTitle ? 'sr-only' : 'modal-title', titleClassName)}>\n {children}\n </Heading>\n {extra}\n </div>\n );\n}\n\nexport type ModalBodyProps = ComponentPropsWithRef<'div'> & {\n /** 안쪽 `conts-area` 에 붙일 클래스. 바깥 `modal-conts` 는 `className` 이 받는다. */\n areaClassName?: string;\n};\n\n/** 본문 — 킷 `modal-conts > .conts-area`. 스크롤이 생기면 `tabindex=\"0\"` 을 붙인다. */\nexport function ModalBody({ className, areaClassName, children, ref, ...rest }: ModalBodyProps) {\n const innerRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n const node = innerRef.current;\n if (!node) return;\n const sync = () => {\n if (node.scrollHeight > node.clientHeight) node.setAttribute('tabindex', '0');\n else node.removeAttribute('tabindex');\n };\n sync();\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(sync);\n observer.observe(node);\n for (const child of Array.from(node.children)) observer.observe(child);\n return () => observer.disconnect();\n }, []);\n\n return (\n <div\n className={cx('modal-conts', className)}\n ref={(node) => {\n innerRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) (ref as { current: HTMLDivElement | null }).current = node;\n }}\n {...rest}\n >\n <div className={cx('conts-area', areaClassName)}>{children}</div>\n </div>\n );\n}\n\nexport type ModalFooterProps = ComponentPropsWithRef<'div'> & {\n /** 좌우로 갈라 놓는다(킷 `multi-conts`). 왼쪽 보조 액션, 오른쪽 확정 액션. */\n split?: boolean;\n};\n\n/**\n * 푸터 — 확정·취소 액션 자리. 제출(primary)이 가장 오른쪽이다.\n * 정렬·간격·버튼 최소폭은 킷이 잡는다 — 여백을 손으로 붙이지 마라.\n */\nexport function ModalFooter({ split, className, children, ...rest }: ModalFooterProps) {\n return (\n <div className={cx('modal-btn', 'btn-wrap', split && 'multi-conts', className)} {...rest}>\n {children}\n </div>\n );\n}\n\nexport type ModalCloseProps = Omit<ComponentPropsWithRef<'button'>, 'children'> & {\n /** 스크린리더가 읽을 이름. 아이콘만 있는 버튼이라 반드시 있어야 한다. */\n label?: string;\n size?: KrdsSize;\n};\n\n/**\n * 오른쪽 위 닫기 버튼. **`Modal.Root` 의 마지막 자식으로 두어라** —\n * 그래야 Tab 순서에서 본문·푸터를 지난 뒤 마지막에 닿는다.\n * 승인이 필요한 모달에는 이 버튼을 두지 않는다.\n */\nexport function ModalClose({\n label = '닫기',\n size = 'medium',\n className,\n onClick,\n type = 'button',\n ...rest\n}: ModalCloseProps) {\n const { close } = useModalContext('Modal.Close');\n\n return (\n <button\n type={type}\n className={cx('krds-btn', size, 'icon', 'btn-close', className)}\n onClick={(event) => {\n onClick?.(event);\n if (!event.defaultPrevented) close();\n }}\n {...rest}\n >\n <span className=\"sr-only\">{label}</span>\n <i className=\"svg-icon ico-popup-close\" aria-hidden=\"true\" />\n </button>\n );\n}\n\n/** 합성용 묶음. `Root` · `Header` · `Body` · `Footer` · `Close`. */\nexport const Modal = {\n Root: ModalRoot,\n Header: ModalHeader,\n Body: ModalBody,\n Footer: ModalFooter,\n Close: ModalClose,\n};\n"],"mappings":"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","names":[]}