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/toast/index.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { cx } from '../lib/krds';\n\n/**\n * 토스트 — 킷에 없다. KRDS 토큰으로 그리는 보충 부품이다.\n *\n * 저장·삭제 결과를 한 줄로 알리고 스스로 사라진다. `ToastProvider` 를 **레이아웃 한 곳에** 두고\n * 화면에서는 `useToast()` 로 띄운다.\n * **한 줄 짧은 문장만** 담는다 — 두 줄 이상 설명은 `ui/modal`, 되돌리기 같은 행동이 붙으면 스낵바다.\n * 폼 필드의 오류·성공은 토스트가 아니라 `ui/field` 의 인라인 문구다.\n * 재난·장애는 `ui/critical-alert`, 늘 떠 있는 안내는 `ui/alert` 다.\n * `duration: 0` 이면 자동으로 사라지지 않는다(닫기 단추로만 닫힌다).\n *\n * @example\n * // 레이아웃\n * <ToastProvider><App /></ToastProvider>\n * // 화면\n * const toast = useToast();\n * toast.success('저장했습니다.');\n * toast.error('저장하지 못했습니다.');\n *\n * 자세히: docs/krds/09-부품-노트.md#토스트\n */\n\n/** 색조. 킷 상태 아이콘이 셋뿐이라 세 가지다 — 「주의·경고형」은 `danger` 를 쓴다. */\nexport type ToastTone = 'information' | 'success' | 'danger';\n\n/** 색조 → 킷 아이콘. 킷의 `form-hint-*` 가 쓰는 짝 그대로다. */\nconst TONE_ICON: Record<ToastTone, string> = {\n information: 'ico-information-fill',\n success: 'ico-success-fill',\n danger: 'ico-error-fill',\n};\n\n/** 색조 → 테두리색. 의미 토큰이라 선명한 화면 모드도 따라간다. */\nconst TONE_BORDER: Record<ToastTone, string> = {\n information: 'border-information-line',\n success: 'border-success-line',\n danger: 'border-danger-line',\n};\n\n/** 가이드의 권장 노출 시간(ms). 정보형 2~3초 · 주의·경고형 3~4초 중 상한이다. */\nconst DEFAULT_DURATION: Record<ToastTone, number> = {\n information: 3000,\n success: 3000,\n danger: 4000,\n};\n\n/** 사라지는 동안의 시간(ms). 아래 `duration-200` 과 짝이라 한쪽만 바꾸면 어긋난다. */\nconst LEAVE_MS = 200;\n\n/* ────────────────────────────────────────────────────────────────────────\n * 토스트 한 줄\n * ──────────────────────────────────────────────────────────────────── */\n\nexport type ToastProps = Omit<ComponentPropsWithRef<'div'>, 'children'> & {\n /** 색조. 기본 `information`. */\n tone?: ToastTone;\n /** 한 줄 메시지. **일부러 `string` 이다** — 더 길거나 링크가 붙으면 토스트가 아니다. */\n children: string;\n /** 닫기 단추를 누름. 주지 않으면 닫기 단추를 그리지 않는다. */\n onClose?: () => void;\n /** 닫기 단추의 스크린리더 이름. 아이콘뿐이라 이 문구가 없으면 읽을 것이 없다. */\n closeLabel?: string;\n};\n\n/**\n * 토스트 막대 하나. 보통은 `useToast()` 로 띄우고 이것을 직접 쓰지 않는다.\n * **직접 쓰면 라이브 영역이 아니다** — 소리로는 알려지지 않는다.\n */\nexport function Toast({\n tone = 'information',\n children,\n onClose,\n closeLabel = '알림 닫기',\n className,\n ...rest\n}: ToastProps) {\n return (\n <div\n data-tone={tone}\n className={cx(\n 'flex max-w-full items-center gap-2 rounded-lg border bg-surface px-4 py-3 text-sm text-fg',\n // 킷 모달의 그림자 토큰 그대로(`--krds-modal--wrap-shadow`). 우리 눈금에 그림자 단계가 없어 값으로 적는다.\n 'shadow-[0_0_0.2rem_0_var(--krds-light-color-alpha-shadow2),0_1.6rem_2.4rem_0_var(--krds-light-color-alpha-shadow3)]',\n TONE_BORDER[tone],\n className,\n )}\n {...rest}\n >\n {/* 색조는 아이콘과 테두리가 나른다. 글자는 본문색 그대로 둔다 — 한 줄 문장이라 색까지 입히면 읽기 나빠진다. */}\n <i className={cx('svg-icon shrink-0', TONE_ICON[tone])} aria-hidden=\"true\" />\n <p className=\"m-0 min-w-0 flex-1\">{children}</p>\n {onClose && (\n // 킷 아이콘 버튼 규격 그대로: `krds-btn icon` + sr-only 이름 + `ico-popup-close`(24px 정사각).\n <button type=\"button\" className=\"krds-btn medium icon shrink-0\" onClick={onClose}>\n <span className=\"sr-only\">{closeLabel}</span>\n <i className=\"svg-icon ico-popup-close\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n * 띄우고 지우는 쪽\n * ──────────────────────────────────────────────────────────────────── */\n\nexport type ToastOptions = {\n /** 색조. 기본 `information`. */\n tone?: ToastTone;\n /** 노출 시간(ms). 생략하면 색조별 권장값. **`0` 이면 자동으로 사라지지 않는다.** */\n duration?: number;\n /** 닫기 단추의 스크린리더 이름. */\n closeLabel?: string;\n};\n\ntype ToastEntry = Required<Pick<ToastOptions, 'tone' | 'duration'>> & {\n id: string;\n message: string;\n closeLabel?: string;\n /** 사라지는 중. 화면에서 지우기 전 `LEAVE_MS` 동안만 참이다. */\n leaving?: boolean;\n};\n\nexport type ToastApi = {\n /** 토스트를 띄우고 식별자를 돌려준다. */\n show(message: string, options?: ToastOptions): string;\n /** `show(message, { tone: 'information' })` */\n info(message: string, options?: Omit<ToastOptions, 'tone'>): string;\n /** `show(message, { tone: 'success' })` — 저장·삭제 성공 */\n success(message: string, options?: Omit<ToastOptions, 'tone'>): string;\n /** `show(message, { tone: 'danger' })` — 실패. 되풀이되면 토스트 말고 모달로 낸다. */\n error(message: string, options?: Omit<ToastOptions, 'tone'>): string;\n /** 하나를 지운다. */\n dismiss(id: string): void;\n /** 전부 즉시 지운다(사라지는 동작 없음). 화면을 옮길 때 쓴다. */\n dismissAll(): void;\n};\n\nconst ToastContext = createContext<ToastApi | null>(null);\n\n/** 토스트를 띄운다. `<ToastProvider>` 안에서만 쓸 수 있다. */\nexport function useToast(): ToastApi {\n const api = useContext(ToastContext);\n if (!api) {\n throw new Error('useToast() 는 <ToastProvider> 안에서만 쓸 수 있다. 레이아웃에 넣었는지 확인하라.');\n }\n return api;\n}\n\nexport type ToastProviderProps = {\n /** 앱 전체. */\n children?: ReactNode;\n /** 한 번에 띄울 최대 개수. 기본 3 — 넘으면 오래된 것부터 사라진다. */\n max?: number;\n /** 뷰포트에 붙는다. 하단 고정 요소와 겹칠 때 `bottom-*` 을 올리는 자리. */\n className?: string;\n};\n\n/**\n * 토스트 뷰포트 + 띄우는 함수를 공급한다. **레이아웃 한 곳에만 둔다.**\n * 뷰포트는 비어 있어도 그려진다 — 라이브 영역이 메시지보다 먼저 있어야 낭독된다.\n */\nexport function ToastProvider({ children, max = 3, className }: ToastProviderProps) {\n const [items, setItems] = useState<ToastEntry[]>([]);\n /** 포인터가 올라가 있거나 안에 초점이 있으면 참. 이때 자동 사라짐을 멈춘다(WCAG 2.2.1). */\n const [paused, setPaused] = useState(false);\n const [mounted, setMounted] = useState(false);\n const seq = useRef(0);\n const leaveTimers = useRef(new Map<string, number>());\n\n useEffect(() => setMounted(true), []);\n\n // 화면을 떠날 때 남은 타이머를 정리한다.\n useEffect(() => {\n const timers = leaveTimers.current;\n return () => {\n timers.forEach((t) => window.clearTimeout(t));\n timers.clear();\n };\n }, []);\n\n const dismiss = useCallback((id: string) => {\n if (leaveTimers.current.has(id)) return; // 이미 사라지는 중\n setItems((list) => list.map((it) => (it.id === id ? { ...it, leaving: true } : it)));\n const timer = window.setTimeout(() => {\n leaveTimers.current.delete(id);\n setItems((list) => list.filter((it) => it.id !== id));\n }, LEAVE_MS);\n leaveTimers.current.set(id, timer);\n }, []);\n\n const api = useMemo<ToastApi>(() => {\n const show = (message: string, options: ToastOptions = {}) => {\n const tone = options.tone ?? 'information';\n seq.current += 1;\n const id = `toast-${seq.current}`;\n const entry: ToastEntry = {\n id,\n message,\n tone,\n duration: options.duration ?? DEFAULT_DURATION[tone],\n closeLabel: options.closeLabel,\n };\n // 넘치면 오래된 것부터 버린다.\n setItems((list) => [...list, entry].slice(-max));\n return id;\n };\n return {\n show,\n info: (message, options) => show(message, { ...options, tone: 'information' }),\n success: (message, options) => show(message, { ...options, tone: 'success' }),\n error: (message, options) => show(message, { ...options, tone: 'danger' }),\n dismiss,\n dismissAll: () => {\n leaveTimers.current.forEach((t) => window.clearTimeout(t));\n leaveTimers.current.clear();\n setItems([]);\n },\n };\n }, [dismiss, max]);\n\n return (\n <ToastContext.Provider value={api}>\n {children}\n {mounted &&\n createPortal(\n <div\n role=\"status\"\n aria-live=\"polite\"\n // 기본값이 true 라 토스트가 하나 늘 때마다 영역 전체가 다시 낭독된다.\n aria-atomic=\"false\"\n className={cx(\n // 가이드: 「토스트는 화면 중앙 하단에 표시한다」.\n 'pointer-events-none fixed bottom-10 left-1/2 z-[1030] -translate-x-1/2',\n 'flex w-full max-w-[48rem] flex-col items-center gap-2 px-4',\n className,\n )}\n // 포인터가 올라가거나 초점이 들어오면 자동 사라짐을 멈춘다(WCAG 2.2.1).\n onMouseEnter={() => setPaused(true)}\n onMouseLeave={() => setPaused(false)}\n onFocus={() => setPaused(true)}\n onBlur={() => setPaused(false)}\n >\n {items.map((item) => (\n <ToastRow key={item.id} item={item} paused={paused} onDismiss={dismiss} />\n ))}\n </div>,\n document.body,\n )}\n </ToastContext.Provider>\n );\n}\n\n/** 뷰포트 안의 토스트 한 줄. 자기 타이머와 나타남/사라짐만 맡는다. */\nfunction ToastRow({\n item,\n paused,\n onDismiss,\n}: {\n item: ToastEntry;\n paused: boolean;\n onDismiss: (id: string) => void;\n}) {\n const [entered, setEntered] = useState(false);\n\n // 붙자마자 한 프레임 뒤에 켠다 — 같은 프레임에 바꾸면 전이가 일어나지 않는다.\n useEffect(() => {\n const frame = window.requestAnimationFrame(() => setEntered(true));\n return () => window.cancelAnimationFrame(frame);\n }, []);\n\n useEffect(() => {\n if (paused || item.leaving || !item.duration) return;\n const timer = window.setTimeout(() => onDismiss(item.id), item.duration);\n return () => window.clearTimeout(timer);\n // 멈췄다 풀리면 시간을 처음부터 다시 센다 — 남은 시간을 이어 세지 않는다.\n // 읽던 사람에게 더 넉넉한 쪽이라 그대로 뒀다.\n }, [paused, item.leaving, item.duration, item.id, onDismiss]);\n\n const shown = entered && !item.leaving;\n\n return (\n <Toast\n tone={item.tone}\n closeLabel={item.closeLabel}\n onClose={() => onDismiss(item.id)}\n className={cx(\n // 뷰포트가 pointer-events-none 이라 막대에서 되살린다 — 아래 화면을 가리지 않기 위해서다.\n 'pointer-events-auto transition duration-200 ease-out motion-reduce:transition-none',\n shown ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0',\n )}\n >\n {item.message}\n </Toast>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,84 @@
1
+ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ import { type KrdsIcon, type KrdsSize } from '../lib/krds';
3
+ /** 버튼 위계. `ui/button` 의 `ButtonVariant` 와 같은 값이다(부품 간 의존을 만들지 않으려고 따로 적었다). */
4
+ export type TooltipVariant = 'primary' | 'secondary' | 'tertiary' | 'text' | 'link';
5
+ type TooltipBaseProps = {
6
+ /** 팝오버 본문. **150자 내외**로 쓴다 — 더 길면 맥락적 도움말·도움 패널·별도 페이지다. */
7
+ content: ReactNode;
8
+ /** 기본 medium(48px). 같은 줄의 입력칸·버튼과 같은 값을 준다. */
9
+ size?: KrdsSize;
10
+ /** 트리거 버튼의 위계. */
11
+ variant?: TooltipVariant;
12
+ /** 킷 아이콘 이름(`ico-` 뺀 것). 예: `help` · `information` */
13
+ icon?: KrdsIcon;
14
+ /** 아이콘 회전. 방향은 이름의 일부가 아니라 별도 클래스다 — `icon="angle right"` 로 쓰지 마라. */
15
+ iconDirection?: 'up' | 'down' | 'left' | 'right';
16
+ /** 아이콘 위치. 기본 `end`. */
17
+ iconPosition?: 'start' | 'end';
18
+ /** 테두리 있는 아이콘 버튼. `iconOnly` 일 때만 뜻이 있다. */
19
+ bordered?: boolean;
20
+ /** 박스형 — 흰 바탕에 테두리, 줄바꿈 허용. 세로 방향으로 뜬다. */
21
+ box?: boolean;
22
+ /** 인라인형을 세로(위/아래) 방향으로 띄운다. */
23
+ vertical?: boolean;
24
+ /** 팝오버 맨 앞에 숨겨 넣을 트리거 이름. 생략하면 트리거의 글자에서 읽어 온다. */
25
+ name?: string;
26
+ /** 트리거 버튼에 붙일 클래스. */
27
+ className?: string;
28
+ /** 트리거 글자. `iconOnly` 가 아닐 때. */
29
+ children?: ReactNode;
30
+ };
31
+ export type TooltipProps = Omit<ComponentPropsWithRef<'button'>, 'children' | 'content'> & TooltipBaseProps & {
32
+ iconOnly?: false;
33
+ };
34
+ export type IconTooltipProps = Omit<ComponentPropsWithRef<'button'>, 'children' | 'content'> & TooltipBaseProps & {
35
+ iconOnly: true;
36
+ /** 스크린리더가 읽을 버튼 이름. **툴팁 글월은 이름의 대체물이 아니다**(가이드 원문). */
37
+ label: string;
38
+ icon: KrdsIcon;
39
+ };
40
+ /**
41
+ * 툴팁 — KRDS `krds-tooltip`. hover/focus 로 뜨는 부가 설명이다.
42
+ *
43
+ * **팝오버 안에 링크·버튼을 넣지 마라** — hover 로만 뜨는 영역은 마우스를 옮기는 동안 사라진다
44
+ * (대화형 요소가 필요하면 {@link ContextualHelp}).
45
+ * **필수 정보를 툴팁에만 담지 마라** — 터치 기기에는 hover 가 없다.
46
+ * `disabled` 트리거에는 붙지 않는다(마우스·초점 이벤트가 오지 않는다).
47
+ */
48
+ export declare function Tooltip(props: TooltipProps | IconTooltipProps): import("react").JSX.Element;
49
+ /** 팝오버가 붙는 세로 방향. */
50
+ export type ContextualHelpSide = 'top' | 'bottom';
51
+ /** 팝오버가 붙는 가로 정렬. */
52
+ export type ContextualHelpAlign = 'left' | 'center' | 'right';
53
+ export type ContextualHelpProps = Omit<ComponentPropsWithRef<'div'>, 'title'> & {
54
+ /** 아이콘 버튼의 이름. **고유하고 구체적으로** — 「도움말」 여러 개보다 「신청 자격 도움말」이 맞다. */
55
+ label: string;
56
+ /** 팝오버 제목. 한 줄로 잘린다. */
57
+ heading?: ReactNode;
58
+ /** 아이콘 버튼 옆에 놓이는 글자. 대개 그 도움말이 설명하는 항목의 이름이다. */
59
+ text?: ReactNode;
60
+ /** 킷 아이콘 이름. 기본 `tooltip` — 정보 제공이면 `information`, 과업 도움이면 `help`. */
61
+ icon?: KrdsIcon;
62
+ /** 팝오버가 붙는 세로 방향. 기본 `top`. */
63
+ side?: ContextualHelpSide;
64
+ /** 팝오버가 붙는 가로 정렬. 기본 `left`. */
65
+ align?: ContextualHelpAlign;
66
+ /** 아이콘 버튼 크기. 기본 medium(48px). */
67
+ buttonSize?: KrdsSize;
68
+ /** 닫기 버튼 이름. */
69
+ closeLabel?: string;
70
+ /** 제목 태그 단계. 기본 4 — 문서 구조에 맞춰 고른다. */
71
+ headingLevel?: 2 | 3 | 4 | 5 | 6;
72
+ /** 팝오버 본문. 링크는 `<div className="btn-wrap">` 안에 넣는다. */
73
+ children?: ReactNode;
74
+ };
75
+ /**
76
+ * 맥락적 도움말 — KRDS `krds-contextual-help`. click 으로 열리고 **팝오버 안에 링크를 둘 수 있다.**
77
+ *
78
+ * **스스로 열리면 안 된다**(규격 금지) — 그래서 `open`·`defaultOpen` 이 일부러 없다.
79
+ * 닫는 방법은 셋(닫기 버튼 · Esc · 바깥 누르기)이고, 열면 초점이 팝오버 안으로 들어간다.
80
+ * 제목은 한 줄로 잘린다. 한 화면에서 {@link Tooltip} 과 섞어 쓰지 마라.
81
+ */
82
+ export declare function ContextualHelp({ label, heading, text, icon, side, align, buttonSize, closeLabel, headingLevel, className, children, ...rest }: ContextualHelpProps): import("react").JSX.Element;
83
+ export {};
84
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tooltip/index.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,329 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useId,
7
+ useLayoutEffect,
8
+ useRef,
9
+ useState
10
+ } from "react";
11
+ import { cx } from "../lib/krds.js";
12
+ const TOOLTIP_GAP = 12;
13
+ const MOBILE_MAX = 767;
14
+ const NARROW_MAX = 420;
15
+ const CONTEXTUAL_FOCUSABLE = 'a, button, [tabindex="0"], input, textarea, select';
16
+ const useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
17
+ function computeTooltipPosition(trigger, popover, vertical) {
18
+ const { clientHeight: popH, clientWidth: popW } = popover;
19
+ const rect = trigger.getBoundingClientRect();
20
+ const halfW = window.innerWidth / 2;
21
+ const halfH = window.innerHeight / 2;
22
+ const isVertical = vertical || window.innerWidth <= MOBILE_MAX;
23
+ const cls = [];
24
+ let top;
25
+ let left;
26
+ if (isVertical) {
27
+ if (rect.top + rect.height > halfH) {
28
+ top = rect.top - popH - TOOLTIP_GAP;
29
+ cls.push("top");
30
+ } else {
31
+ top = rect.top + rect.height + TOOLTIP_GAP;
32
+ cls.push("bottom");
33
+ }
34
+ if (rect.left + rect.width > halfW) {
35
+ left = rect.right - popW;
36
+ if (window.innerWidth - (rect.left + rect.width) > popW / 2) {
37
+ left = rect.left + (rect.width - popW) / 2;
38
+ } else {
39
+ cls.push("right");
40
+ }
41
+ } else {
42
+ left = rect.left + (rect.width - popW) / 2;
43
+ if (left < 0) {
44
+ left = rect.left;
45
+ cls.push("left");
46
+ }
47
+ }
48
+ } else {
49
+ top = rect.top + (rect.height - popH) / 2;
50
+ if (rect.left + rect.width > halfW) {
51
+ left = rect.left - popW - TOOLTIP_GAP;
52
+ cls.push("right");
53
+ } else {
54
+ left = rect.right + TOOLTIP_GAP;
55
+ }
56
+ }
57
+ return { top, left, centered: window.innerWidth <= NARROW_MAX, cls: cls.join(" ") };
58
+ }
59
+ function Tooltip(props) {
60
+ const {
61
+ content,
62
+ size = "medium",
63
+ variant,
64
+ icon,
65
+ iconDirection,
66
+ iconPosition = "end",
67
+ bordered,
68
+ box,
69
+ vertical,
70
+ name,
71
+ className,
72
+ children,
73
+ type = "button",
74
+ disabled,
75
+ ref,
76
+ onMouseEnter,
77
+ onMouseLeave,
78
+ onFocus,
79
+ onBlur,
80
+ ...rest
81
+ } = props;
82
+ const iconOnly = "iconOnly" in props && props.iconOnly === true;
83
+ const label = "label" in props ? props.label : void 0;
84
+ const popoverId = useId();
85
+ const triggerRef = useRef(null);
86
+ const popoverRef = useRef(null);
87
+ const [open, setOpen] = useState(false);
88
+ const [pos, setPos] = useState(null);
89
+ const [autoName, setAutoName] = useState(label ?? "");
90
+ const setTriggerRef = useCallback(
91
+ (el) => {
92
+ triggerRef.current = el;
93
+ if (typeof ref === "function") ref(el);
94
+ else if (ref) ref.current = el;
95
+ },
96
+ [ref]
97
+ );
98
+ useEffect(() => {
99
+ if (name !== void 0) return;
100
+ const text = (triggerRef.current?.innerText ?? "").trim();
101
+ if (text && text !== autoName) setAutoName(text);
102
+ });
103
+ useIsoLayoutEffect(() => {
104
+ if (!open) {
105
+ setPos(null);
106
+ return;
107
+ }
108
+ const trigger = triggerRef.current;
109
+ const popover = popoverRef.current;
110
+ if (!trigger || !popover) return;
111
+ setPos(computeTooltipPosition(trigger, popover, Boolean(box || vertical)));
112
+ }, [open, box, vertical]);
113
+ useEffect(() => {
114
+ if (!open) return;
115
+ const close = () => setOpen(false);
116
+ const onKeyDown = (event) => {
117
+ if (event.key === "Escape" || event.key === "Esc") close();
118
+ };
119
+ document.addEventListener("keydown", onKeyDown);
120
+ window.addEventListener("scroll", close, true);
121
+ window.addEventListener("resize", close);
122
+ return () => {
123
+ document.removeEventListener("keydown", onKeyDown);
124
+ window.removeEventListener("scroll", close, true);
125
+ window.removeEventListener("resize", close);
126
+ };
127
+ }, [open]);
128
+ const show = () => !disabled && setOpen(true);
129
+ const hide = () => setOpen(false);
130
+ const iconNode = icon ? /* @__PURE__ */ jsx("i", { className: cx(`svg-icon ico-${icon}`, iconDirection), "aria-hidden": "true" }) : null;
131
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
132
+ /* @__PURE__ */ jsx(
133
+ "button",
134
+ {
135
+ type,
136
+ ref: setTriggerRef,
137
+ disabled,
138
+ className: cx(
139
+ "krds-btn",
140
+ size,
141
+ variant,
142
+ iconOnly && "icon",
143
+ iconOnly && bordered && "border",
144
+ "krds-tooltip",
145
+ box && "tooltip-box",
146
+ vertical && "tooltip-vertical",
147
+ className
148
+ ),
149
+ "aria-labelledby": disabled ? void 0 : popoverId,
150
+ onMouseEnter: (event) => {
151
+ show();
152
+ onMouseEnter?.(event);
153
+ },
154
+ onMouseLeave: (event) => {
155
+ hide();
156
+ onMouseLeave?.(event);
157
+ },
158
+ onFocus: (event) => {
159
+ show();
160
+ onFocus?.(event);
161
+ },
162
+ onBlur: (event) => {
163
+ hide();
164
+ onBlur?.(event);
165
+ },
166
+ ...rest,
167
+ children: iconOnly ? /* @__PURE__ */ jsxs(Fragment, { children: [
168
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: label }),
169
+ iconNode
170
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
171
+ iconPosition === "start" && iconNode,
172
+ children,
173
+ iconPosition === "end" && iconNode
174
+ ] })
175
+ }
176
+ ),
177
+ !disabled && /* @__PURE__ */ jsxs(
178
+ "div",
179
+ {
180
+ id: popoverId,
181
+ ref: popoverRef,
182
+ className: cx(
183
+ "krds-tooltip-popover",
184
+ open && "active",
185
+ box && "tooltip-box",
186
+ vertical && "tooltip-vertical",
187
+ open && pos?.cls
188
+ ),
189
+ "aria-hidden": "true",
190
+ style: open && pos ? { top: `${pos.top}px`, left: pos.centered ? "50%" : `${pos.left}px` } : void 0,
191
+ children: [
192
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: name ?? autoName }),
193
+ " ",
194
+ content
195
+ ]
196
+ }
197
+ )
198
+ ] });
199
+ }
200
+ function adjustContextualPopover(popover, action) {
201
+ if (window.innerWidth > MOBILE_MAX) {
202
+ popover.style.left = "";
203
+ popover.style.width = "";
204
+ return;
205
+ }
206
+ const raw = getComputedStyle(document.documentElement).getPropertyValue(
207
+ "--krds-contents-padding-x"
208
+ );
209
+ const pad = Number.parseFloat(raw) || 0;
210
+ const offsetLeft = action.getBoundingClientRect().left - pad;
211
+ popover.style.left = `${-offsetLeft}px`;
212
+ popover.style.width = `${document.body.clientWidth - pad * 2}px`;
213
+ }
214
+ function ContextualHelp({
215
+ label,
216
+ heading,
217
+ text,
218
+ icon = "tooltip",
219
+ side = "top",
220
+ align = "left",
221
+ buttonSize = "medium",
222
+ closeLabel = "\uB2EB\uAE30",
223
+ headingLevel = 4,
224
+ className,
225
+ children,
226
+ ...rest
227
+ }) {
228
+ const actionRef = useRef(null);
229
+ const buttonRef = useRef(null);
230
+ const popoverRef = useRef(null);
231
+ const [open, setOpen] = useState(false);
232
+ const close = useCallback((refocus) => {
233
+ setOpen(false);
234
+ if (refocus) buttonRef.current?.focus();
235
+ }, []);
236
+ useIsoLayoutEffect(() => {
237
+ const popover = popoverRef.current;
238
+ const action = actionRef.current;
239
+ if (!popover || !action) return;
240
+ if (!open) {
241
+ popover.style.left = "";
242
+ popover.style.width = "";
243
+ return;
244
+ }
245
+ adjustContextualPopover(popover, action);
246
+ }, [open]);
247
+ useEffect(() => {
248
+ if (!open) return;
249
+ popoverRef.current?.querySelector(CONTEXTUAL_FOCUSABLE)?.focus();
250
+ }, [open]);
251
+ useEffect(() => {
252
+ if (!open) return;
253
+ const action = actionRef.current;
254
+ const onKeyDown = (event) => {
255
+ if (event.key === "Escape" || event.key === "Esc") close(true);
256
+ };
257
+ const onDocumentClick = (event) => {
258
+ if (!action?.contains(event.target)) setOpen(false);
259
+ };
260
+ const onFocusOut = (event) => {
261
+ const next = event.relatedTarget;
262
+ if (next && action?.contains(next)) return;
263
+ setOpen(false);
264
+ };
265
+ const onResize = () => {
266
+ if (popoverRef.current && action) adjustContextualPopover(popoverRef.current, action);
267
+ };
268
+ document.addEventListener("keydown", onKeyDown);
269
+ document.addEventListener("click", onDocumentClick);
270
+ action?.addEventListener("focusout", onFocusOut);
271
+ window.addEventListener("resize", onResize);
272
+ return () => {
273
+ document.removeEventListener("keydown", onKeyDown);
274
+ document.removeEventListener("click", onDocumentClick);
275
+ action?.removeEventListener("focusout", onFocusOut);
276
+ window.removeEventListener("resize", onResize);
277
+ };
278
+ }, [open, close]);
279
+ const Heading = `h${headingLevel}`;
280
+ return /* @__PURE__ */ jsxs("div", { className: cx("krds-contextual-help", side, align, className), ...rest, children: [
281
+ text != null && /* @__PURE__ */ jsx("p", { className: "tooltip-txt", children: text }),
282
+ /* @__PURE__ */ jsxs("div", { className: "tooltip-action", ref: actionRef, children: [
283
+ /* @__PURE__ */ jsxs(
284
+ "button",
285
+ {
286
+ type: "button",
287
+ ref: buttonRef,
288
+ className: cx("krds-btn", buttonSize, "icon", "tooltip-btn"),
289
+ "aria-expanded": open,
290
+ onClick: () => open ? close(true) : setOpen(true),
291
+ children: [
292
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: label }),
293
+ /* @__PURE__ */ jsx("i", { className: `svg-icon ico-${icon}`, "aria-hidden": "true" })
294
+ ]
295
+ }
296
+ ),
297
+ /* @__PURE__ */ jsxs(
298
+ "div",
299
+ {
300
+ className: "tooltip-popover",
301
+ role: "tooltip",
302
+ ref: popoverRef,
303
+ style: { display: open ? "block" : void 0 },
304
+ children: [
305
+ heading != null && /* @__PURE__ */ jsx(Heading, { className: "tooltip-title", children: heading }),
306
+ /* @__PURE__ */ jsx("div", { className: "tooltip-contents", children }),
307
+ /* @__PURE__ */ jsxs(
308
+ "button",
309
+ {
310
+ type: "button",
311
+ className: "krds-btn xsmall icon tooltip-close",
312
+ onClick: () => close(true),
313
+ children: [
314
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: closeLabel }),
315
+ /* @__PURE__ */ jsx("i", { className: "svg-icon ico-modal-close", "aria-hidden": "true" })
316
+ ]
317
+ }
318
+ )
319
+ ]
320
+ }
321
+ )
322
+ ] })
323
+ ] });
324
+ }
325
+ export {
326
+ ContextualHelp,
327
+ Tooltip
328
+ };
329
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tooltip/index.tsx"],"sourcesContent":["'use client';\n\nimport {\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type FocusEvent as ReactFocusEvent,\n type MouseEvent as ReactMouseEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { cx, type KrdsIcon, type KrdsSize } from '../lib/krds';\n\n/**\n * 툴팁 — KRDS `krds-tooltip` · 맥락적 도움말 — KRDS `krds-contextual-help`.\n *\n * 갈림길은 하나다 — **팝오버 안에 링크·버튼이 필요하면 툴팁이 아니라 맥락적 도움말이다.**\n * 툴팁은 hover/focus 로 뜨고 맥락적 도움말은 click 으로 연다.\n * **한 화면에 둘을 섞어 쓰지 마라**(가이드 원문) — 버튼이 똑같이 생겼는데 상호작용이 달라진다.\n * 본문은 150자 내외로 쓰고, 그보다 길면 도움 패널이나 별도 도움말 페이지로 보낸다.\n * `title` 속성과 겹쳐 쓰지 않는다 — 같은 말이 두 번 읽힌다.\n *\n * @example\n * <Tooltip iconOnly icon=\"help\" label=\"처리 기간 도움말\" content=\"접수일로부터 10일 이내\" />\n * <Tooltip variant=\"text\" icon=\"angle\" iconDirection=\"right\" content=\"예상 처리 기간입니다.\">\n * 처리 기간\n * </Tooltip>\n * <ContextualHelp label=\"신청 자격 도움말\" heading=\"신청 자격\" text=\"신청 자격\">\n * <p>만 18세 이상이면 신청할 수 있습니다.</p>\n * <div className=\"btn-wrap\">\n * <LinkButton size=\"xsmall\" href=\"/help/eligibility\">자세히 보기</LinkButton>\n * </div>\n * </ContextualHelp>\n *\n * 자세히: docs/krds/09-부품-노트.md#툴팁\n */\n\n/** 킷 JS 의 `tooltipGap`. CSS 토큰이 아니라 코드 상수다. */\nconst TOOLTIP_GAP = 12;\n\n/** 킷 CSS 의 좁은 화면 경계. */\nconst MOBILE_MAX = 767;\n\n/** 킷 CSS 가 팝오버를 화면 폭에 맞춰 버리는 경계. 이때는 좌표를 계산하지 않는다. */\nconst NARROW_MAX = 420;\n\n/** 팝오버를 열 때 초점을 줄 첫 대화형 요소. 킷 JS 의 선택자 그대로다. */\nconst CONTEXTUAL_FOCUSABLE = 'a, button, [tabindex=\"0\"], input, textarea, select';\n\n/** SSR 경고를 피하려고 갈아 끼운다. 위치 계산은 그리기 전에 끝나야 좌상단에 번쩍이지 않는다. */\nconst useIsoLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\n/** 버튼 위계. `ui/button` 의 `ButtonVariant` 와 같은 값이다(부품 간 의존을 만들지 않으려고 따로 적었다). */\nexport type TooltipVariant = 'primary' | 'secondary' | 'tertiary' | 'text' | 'link';\n\n/** 계산된 팝오버 위치. `cls` 는 킷이 말풍선 꼬리를 붙일 때 쓰는 방향 클래스다. */\ntype TooltipPosition = { top: number; left: number; centered: boolean; cls: string };\n\n/** 팝오버 좌표. 킷 JS 의 `calculateTooltipPosition()` 이식. 좌표가 뷰포트 기준이라 스크롤에 닫는다. */\nfunction computeTooltipPosition(\n trigger: HTMLElement,\n popover: HTMLElement,\n vertical: boolean,\n): TooltipPosition {\n const { clientHeight: popH, clientWidth: popW } = popover;\n const rect = trigger.getBoundingClientRect();\n const halfW = window.innerWidth / 2;\n const halfH = window.innerHeight / 2;\n // 좁은 화면에서는 방향 지정과 무관하게 세로형으로 떨어뜨린다(킷 JS 의 isMobile 분기).\n const isVertical = vertical || window.innerWidth <= MOBILE_MAX;\n const cls: string[] = [];\n let top: number;\n let left: number;\n\n if (isVertical) {\n if (rect.top + rect.height > halfH) {\n top = rect.top - popH - TOOLTIP_GAP; // 화면 아래쪽 요소 → 위로 띄운다\n cls.push('top');\n } else {\n top = rect.top + rect.height + TOOLTIP_GAP;\n cls.push('bottom');\n }\n if (rect.left + rect.width > halfW) {\n left = rect.right - popW; // 오른쪽 정렬\n // 오른쪽에 여유가 있으면 가운데로 되돌린다\n if (window.innerWidth - (rect.left + rect.width) > popW / 2) {\n left = rect.left + (rect.width - popW) / 2;\n } else {\n cls.push('right');\n }\n } else {\n left = rect.left + (rect.width - popW) / 2;\n if (left < 0) {\n left = rect.left; // 왼쪽 공간 부족 보정\n cls.push('left');\n }\n }\n } else {\n // 가로형: 트리거 옆에 세로 가운데를 맞춰 붙인다\n top = rect.top + (rect.height - popH) / 2;\n if (rect.left + rect.width > halfW) {\n left = rect.left - popW - TOOLTIP_GAP; // 왼쪽에 붙인다\n cls.push('right'); // 꼬리는 팝오버의 오른쪽에 난다\n } else {\n left = rect.right + TOOLTIP_GAP;\n }\n }\n\n return { top, left, centered: window.innerWidth <= NARROW_MAX, cls: cls.join(' ') };\n}\n\ntype TooltipBaseProps = {\n /** 팝오버 본문. **150자 내외**로 쓴다 — 더 길면 맥락적 도움말·도움 패널·별도 페이지다. */\n content: ReactNode;\n /** 기본 medium(48px). 같은 줄의 입력칸·버튼과 같은 값을 준다. */\n size?: KrdsSize;\n /** 트리거 버튼의 위계. */\n variant?: TooltipVariant;\n /** 킷 아이콘 이름(`ico-` 뺀 것). 예: `help` · `information` */\n icon?: KrdsIcon;\n /** 아이콘 회전. 방향은 이름의 일부가 아니라 별도 클래스다 — `icon=\"angle right\"` 로 쓰지 마라. */\n iconDirection?: 'up' | 'down' | 'left' | 'right';\n /** 아이콘 위치. 기본 `end`. */\n iconPosition?: 'start' | 'end';\n /** 테두리 있는 아이콘 버튼. `iconOnly` 일 때만 뜻이 있다. */\n bordered?: boolean;\n /** 박스형 — 흰 바탕에 테두리, 줄바꿈 허용. 세로 방향으로 뜬다. */\n box?: boolean;\n /** 인라인형을 세로(위/아래) 방향으로 띄운다. */\n vertical?: boolean;\n /** 팝오버 맨 앞에 숨겨 넣을 트리거 이름. 생략하면 트리거의 글자에서 읽어 온다. */\n name?: string;\n /** 트리거 버튼에 붙일 클래스. */\n className?: string;\n /** 트리거 글자. `iconOnly` 가 아닐 때. */\n children?: ReactNode;\n};\n\nexport type TooltipProps = Omit<ComponentPropsWithRef<'button'>, 'children' | 'content'> &\n TooltipBaseProps & {\n iconOnly?: false;\n };\n\nexport type IconTooltipProps = Omit<ComponentPropsWithRef<'button'>, 'children' | 'content'> &\n TooltipBaseProps & {\n iconOnly: true;\n /** 스크린리더가 읽을 버튼 이름. **툴팁 글월은 이름의 대체물이 아니다**(가이드 원문). */\n label: string;\n icon: KrdsIcon;\n };\n\n/**\n * 툴팁 — KRDS `krds-tooltip`. hover/focus 로 뜨는 부가 설명이다.\n *\n * **팝오버 안에 링크·버튼을 넣지 마라** — hover 로만 뜨는 영역은 마우스를 옮기는 동안 사라진다\n * (대화형 요소가 필요하면 {@link ContextualHelp}).\n * **필수 정보를 툴팁에만 담지 마라** — 터치 기기에는 hover 가 없다.\n * `disabled` 트리거에는 붙지 않는다(마우스·초점 이벤트가 오지 않는다).\n */\nexport function Tooltip(props: TooltipProps | IconTooltipProps) {\n const {\n content,\n size = 'medium',\n variant,\n icon,\n iconDirection,\n iconPosition = 'end',\n bordered,\n box,\n vertical,\n name,\n className,\n children,\n type = 'button',\n disabled,\n ref,\n onMouseEnter,\n onMouseLeave,\n onFocus,\n onBlur,\n ...rest\n } = props as TooltipProps & Partial<IconTooltipProps>;\n\n const iconOnly = 'iconOnly' in props && props.iconOnly === true;\n const label = 'label' in props ? props.label : undefined;\n\n const popoverId = useId();\n const triggerRef = useRef<HTMLButtonElement | null>(null);\n const popoverRef = useRef<HTMLDivElement | null>(null);\n const [open, setOpen] = useState(false);\n const [pos, setPos] = useState<TooltipPosition | null>(null);\n // 트리거 글자 사본. `name` 을 주면 안 읽는다.\n const [autoName, setAutoName] = useState(label ?? '');\n\n // 밖에서 온 ref 와 부품 ref 를 한 요소에 함께 건다.\n const setTriggerRef = useCallback(\n (el: HTMLButtonElement | null) => {\n triggerRef.current = el;\n if (typeof ref === 'function') ref(el);\n else if (ref) (ref as { current: HTMLButtonElement | null }).current = el;\n },\n [ref],\n );\n\n // 트리거 글자 읽기(팝오버가 이름을 갈아 끼우므로 사본이 필요하다). 값이 같으면 아무것도 하지 않는다.\n useEffect(() => {\n if (name !== undefined) return;\n const text = (triggerRef.current?.innerText ?? '').trim();\n if (text && text !== autoName) setAutoName(text);\n });\n\n // 열린 뒤 그리기 전에 좌표를 잡는다. 팝오버는 `.active` 라야 크기를 잴 수 있다.\n useIsoLayoutEffect(() => {\n if (!open) {\n setPos(null);\n return;\n }\n const trigger = triggerRef.current;\n const popover = popoverRef.current;\n if (!trigger || !popover) return;\n setPos(computeTooltipPosition(trigger, popover, Boolean(box || vertical)));\n }, [open, box, vertical]);\n\n // 닫는 사건들 — Esc · 스크롤 · 크기 변경.\n useEffect(() => {\n if (!open) return;\n const close = () => setOpen(false);\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' || event.key === 'Esc') close();\n };\n document.addEventListener('keydown', onKeyDown);\n // 안쪽 스크롤 상자에서도 좌표가 낡으므로 캡처로 넓혀 잡았다.\n window.addEventListener('scroll', close, true);\n window.addEventListener('resize', close);\n return () => {\n document.removeEventListener('keydown', onKeyDown);\n window.removeEventListener('scroll', close, true);\n window.removeEventListener('resize', close);\n };\n }, [open]);\n\n const show = () => !disabled && setOpen(true);\n const hide = () => setOpen(false);\n\n const iconNode = icon ? (\n <i className={cx(`svg-icon ico-${icon}`, iconDirection)} aria-hidden=\"true\" />\n ) : null;\n\n return (\n <>\n <button\n type={type}\n ref={setTriggerRef}\n disabled={disabled}\n className={cx(\n 'krds-btn',\n size,\n variant,\n iconOnly && 'icon',\n iconOnly && bordered && 'border',\n 'krds-tooltip',\n box && 'tooltip-box',\n vertical && 'tooltip-vertical',\n className,\n )}\n // 이름이 팝오버로 갈린다. 팝오버가 없는 disabled 일 때는 걸지 않는다.\n aria-labelledby={disabled ? undefined : popoverId}\n onMouseEnter={(event: ReactMouseEvent<HTMLButtonElement>) => {\n show();\n onMouseEnter?.(event);\n }}\n onMouseLeave={(event: ReactMouseEvent<HTMLButtonElement>) => {\n hide();\n onMouseLeave?.(event);\n }}\n onFocus={(event: ReactFocusEvent<HTMLButtonElement>) => {\n show();\n onFocus?.(event);\n }}\n onBlur={(event: ReactFocusEvent<HTMLButtonElement>) => {\n hide();\n onBlur?.(event);\n }}\n {...rest}\n >\n {iconOnly ? (\n <>\n <span className=\"sr-only\">{label}</span>\n {iconNode}\n </>\n ) : (\n <>\n {iconPosition === 'start' && iconNode}\n {children}\n {iconPosition === 'end' && iconNode}\n </>\n )}\n </button>\n {!disabled && (\n <div\n id={popoverId}\n ref={popoverRef}\n className={cx(\n 'krds-tooltip-popover',\n open && 'active',\n box && 'tooltip-box',\n vertical && 'tooltip-vertical',\n open && pos?.cls,\n )}\n // 킷이 만드는 팝오버는 늘 aria-hidden 이다 — 소리로는 위 aria-labelledby 로 한 번만 전해진다.\n aria-hidden=\"true\"\n style={\n open && pos\n ? { top: `${pos.top}px`, left: pos.centered ? '50%' : `${pos.left}px` }\n : undefined\n }\n >\n <span className=\"sr-only\">{name ?? autoName}</span> {content}\n </div>\n )}\n </>\n );\n}\n\n/** 팝오버가 붙는 세로 방향. */\nexport type ContextualHelpSide = 'top' | 'bottom';\n\n/** 팝오버가 붙는 가로 정렬. */\nexport type ContextualHelpAlign = 'left' | 'center' | 'right';\n\nexport type ContextualHelpProps = Omit<ComponentPropsWithRef<'div'>, 'title'> & {\n /** 아이콘 버튼의 이름. **고유하고 구체적으로** — 「도움말」 여러 개보다 「신청 자격 도움말」이 맞다. */\n label: string;\n /** 팝오버 제목. 한 줄로 잘린다. */\n heading?: ReactNode;\n /** 아이콘 버튼 옆에 놓이는 글자. 대개 그 도움말이 설명하는 항목의 이름이다. */\n text?: ReactNode;\n /** 킷 아이콘 이름. 기본 `tooltip` — 정보 제공이면 `information`, 과업 도움이면 `help`. */\n icon?: KrdsIcon;\n /** 팝오버가 붙는 세로 방향. 기본 `top`. */\n side?: ContextualHelpSide;\n /** 팝오버가 붙는 가로 정렬. 기본 `left`. */\n align?: ContextualHelpAlign;\n /** 아이콘 버튼 크기. 기본 medium(48px). */\n buttonSize?: KrdsSize;\n /** 닫기 버튼 이름. */\n closeLabel?: string;\n /** 제목 태그 단계. 기본 4 — 문서 구조에 맞춰 고른다. */\n headingLevel?: 2 | 3 | 4 | 5 | 6;\n /** 팝오버 본문. 링크는 `<div className=\"btn-wrap\">` 안에 넣는다. */\n children?: ReactNode;\n};\n\n/**\n * 좁은 화면에서 팝오버를 본문 폭에 맞춘다. 킷 JS 이식이되 넓은 화면에서는 인라인 폭을 지우기만 한다\n * (킷 CSS 의 rem 폭이 글자 크기 조정을 따라오기 때문이다).\n */\nfunction adjustContextualPopover(popover: HTMLElement, action: HTMLElement) {\n // 킷 JS 는 768 로 재서 CSS 의 767 과 1px 어긋난다 — 여기서는 CSS 에 맞춰 767 로 잰다.\n if (window.innerWidth > MOBILE_MAX) {\n popover.style.left = '';\n popover.style.width = '';\n return;\n }\n const raw = getComputedStyle(document.documentElement).getPropertyValue(\n '--krds-contents-padding-x',\n );\n const pad = Number.parseFloat(raw) || 0;\n const offsetLeft = action.getBoundingClientRect().left - pad;\n popover.style.left = `${-offsetLeft}px`;\n popover.style.width = `${document.body.clientWidth - pad * 2}px`;\n}\n\n/**\n * 맥락적 도움말 — KRDS `krds-contextual-help`. click 으로 열리고 **팝오버 안에 링크를 둘 수 있다.**\n *\n * **스스로 열리면 안 된다**(규격 금지) — 그래서 `open`·`defaultOpen` 이 일부러 없다.\n * 닫는 방법은 셋(닫기 버튼 · Esc · 바깥 누르기)이고, 열면 초점이 팝오버 안으로 들어간다.\n * 제목은 한 줄로 잘린다. 한 화면에서 {@link Tooltip} 과 섞어 쓰지 마라.\n */\nexport function ContextualHelp({\n label,\n heading,\n text,\n icon = 'tooltip',\n side = 'top',\n align = 'left',\n buttonSize = 'medium',\n closeLabel = '닫기',\n headingLevel = 4,\n className,\n children,\n ...rest\n}: ContextualHelpProps) {\n const actionRef = useRef<HTMLDivElement | null>(null);\n const buttonRef = useRef<HTMLButtonElement | null>(null);\n const popoverRef = useRef<HTMLDivElement | null>(null);\n const [open, setOpen] = useState(false);\n\n /** 규격이 초점을 아이콘 버튼으로 되돌리라고 못 박은 경우에만 `refocus` 를 켠다. */\n const close = useCallback((refocus: boolean) => {\n setOpen(false);\n if (refocus) buttonRef.current?.focus();\n }, []);\n\n // 좁은 화면 좌표·폭. 그리기 전에 넣어야 어긋난 자리에 번쩍이지 않는다.\n useIsoLayoutEffect(() => {\n const popover = popoverRef.current;\n const action = actionRef.current;\n if (!popover || !action) return;\n if (!open) {\n popover.style.left = '';\n popover.style.width = '';\n return;\n }\n adjustContextualPopover(popover, action);\n }, [open]);\n\n // 열리면 초점을 팝오버 안 첫 대화형 요소로(닫기 버튼도 그 대상에 든다).\n useEffect(() => {\n if (!open) return;\n popoverRef.current?.querySelector<HTMLElement>(CONTEXTUAL_FOCUSABLE)?.focus();\n }, [open]);\n\n useEffect(() => {\n if (!open) return;\n const action = actionRef.current;\n const onKeyDown = (event: KeyboardEvent) => {\n // 초점이 어디 있든 Esc 로 닫고 아이콘 버튼으로 돌아간다(규격).\n if (event.key === 'Escape' || event.key === 'Esc') close(true);\n };\n const onDocumentClick = (event: MouseEvent) => {\n if (!action?.contains(event.target as Node)) setOpen(false);\n };\n const onFocusOut = (event: FocusEvent) => {\n const next = event.relatedTarget as Node | null;\n if (next && action?.contains(next)) return;\n setOpen(false); // 초점은 사용자가 간 곳에 둔다 — 킷은 버튼으로 끌어와 키보드 함정이 된다\n };\n const onResize = () => {\n if (popoverRef.current && action) adjustContextualPopover(popoverRef.current, action);\n };\n document.addEventListener('keydown', onKeyDown);\n document.addEventListener('click', onDocumentClick);\n action?.addEventListener('focusout', onFocusOut);\n window.addEventListener('resize', onResize);\n return () => {\n document.removeEventListener('keydown', onKeyDown);\n document.removeEventListener('click', onDocumentClick);\n action?.removeEventListener('focusout', onFocusOut);\n window.removeEventListener('resize', onResize);\n };\n }, [open, close]);\n\n const Heading = `h${headingLevel}` as 'h4';\n\n return (\n <div className={cx('krds-contextual-help', side, align, className)} {...rest}>\n {text != null && <p className=\"tooltip-txt\">{text}</p>}\n <div className=\"tooltip-action\" ref={actionRef}>\n <button\n type=\"button\"\n ref={buttonRef}\n className={cx('krds-btn', buttonSize, 'icon', 'tooltip-btn')}\n aria-expanded={open}\n onClick={() => (open ? close(true) : setOpen(true))}\n >\n <span className=\"sr-only\">{label}</span>\n <i className={`svg-icon ico-${icon}`} aria-hidden=\"true\" />\n </button>\n {/* 킷은 팝오버를 인라인 display 로 여닫는다. */}\n <div\n className=\"tooltip-popover\"\n role=\"tooltip\"\n ref={popoverRef}\n style={{ display: open ? 'block' : undefined }}\n >\n {heading != null && <Heading className=\"tooltip-title\">{heading}</Heading>}\n <div className=\"tooltip-contents\">{children}</div>\n <button\n type=\"button\"\n className=\"krds-btn xsmall icon tooltip-close\"\n onClick={() => close(true)}\n >\n <span className=\"sr-only\">{closeLabel}</span>\n <i className=\"svg-icon ico-modal-close\" aria-hidden=\"true\" />\n </button>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "moru-krds-react",
3
+ "version": "0.1.0",
4
+ "description": "React components built on the CSS of KRDS, the Korean government design system",
5
+ "license": "MIT",
6
+ "author": "mhb8436",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./styles/krds-tailwind.css": "./styles/krds-tailwind.css",
15
+ "./package.json": "./package.json",
16
+ "./*": "./dist/*"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "styles",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "keywords": [
26
+ "krds",
27
+ "react",
28
+ "design-system",
29
+ "korea",
30
+ "government",
31
+ "accessibility",
32
+ "wcag",
33
+ "admin",
34
+ "backoffice"
35
+ ],
36
+ "peerDependencies": {
37
+ "react": ">=18",
38
+ "react-dom": ">=18"
39
+ },
40
+ "devDependencies": {
41
+ "@types/react": "^19.2.18",
42
+ "@types/react-dom": "^19.2.5",
43
+ "tsup": "^8.5.1",
44
+ "typescript": "^6.0.3"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup && npm run build:types && node scripts/fix-esm-imports.mjs",
48
+ "build:types": "tsc -p tsconfig.build.json",
49
+ "typecheck": "tsc --noEmit",
50
+ "prepublishOnly": "npm run build"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/mhb8436/moru-krds-react.git"
55
+ },
56
+ "types": "./dist/index.d.ts"
57
+ }