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,146 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../lib/krds.js";
3
+ const SIZE = {
4
+ xsmall: { box: "size-8", text: "text-label-xs", icon: "size-4", overlap: "-ml-2" },
5
+ // 32 / 13 / 16
6
+ small: { box: "size-10", text: "text-label-sm", icon: "size-5", overlap: "-ml-2" },
7
+ // 40 / 15 / 20
8
+ medium: { box: "size-12", text: "text-label-md", icon: "size-6", overlap: "-ml-3" },
9
+ // 48 / 17 / 24
10
+ large: { box: "size-14", text: "text-label-lg", icon: "size-7", overlap: "-ml-4" },
11
+ // 56 / 19 / 28
12
+ xlarge: { box: "size-16", text: "text-label-lg", icon: "size-8", overlap: "-ml-4" }
13
+ // 64 / 19 / 32
14
+ };
15
+ const TONE_LIGHT = {
16
+ primary: "bg-primary-surface text-primary",
17
+ secondary: "bg-secondary-surface text-secondary",
18
+ gray: "bg-surface-subtle text-fg-subtle",
19
+ point: "bg-point-surface text-point",
20
+ danger: "bg-danger-surface text-danger",
21
+ warning: "bg-warning-surface text-warning",
22
+ success: "bg-success-surface text-success",
23
+ information: "bg-information-surface text-information"
24
+ };
25
+ const TONE_SOLID = {
26
+ primary: "bg-primary-solid text-fg-inverse",
27
+ secondary: "bg-secondary-solid text-fg-inverse",
28
+ gray: "bg-fg-subtle text-fg-inverse",
29
+ point: "bg-point-solid text-fg-inverse",
30
+ danger: "bg-danger-solid text-fg-inverse",
31
+ warning: "bg-warning-solid text-fg-inverse",
32
+ success: "bg-success-solid text-fg-inverse",
33
+ information: "bg-information-solid text-fg-inverse"
34
+ };
35
+ const HANGUL = /[ㄱ-ㆎ가-힣]/;
36
+ function avatarInitials(name) {
37
+ const words = name.trim().split(/\s+/).filter(Boolean);
38
+ const first = Array.from(words[0] ?? "")[0] ?? "";
39
+ if (!first) return "";
40
+ if (HANGUL.test(first)) return first;
41
+ const second = words.length > 1 ? Array.from(words[1] ?? "")[0] ?? "" : "";
42
+ return (first + second).toUpperCase();
43
+ }
44
+ function Avatar(props) {
45
+ const {
46
+ size = "medium",
47
+ tone = "gray",
48
+ fill = "light",
49
+ src,
50
+ initials,
51
+ bordered,
52
+ decorative = false,
53
+ name,
54
+ label,
55
+ className,
56
+ ...rest
57
+ } = props;
58
+ const metric = SIZE[size];
59
+ const text = initials ?? (name ? avatarInitials(name) : "");
60
+ return /* @__PURE__ */ jsxs(
61
+ "span",
62
+ {
63
+ role: decorative ? void 0 : "img",
64
+ "aria-label": decorative ? void 0 : label ?? name,
65
+ "aria-hidden": decorative ? true : void 0,
66
+ className: cx(
67
+ // relative/overflow-hidden — 사진을 원 안에 가둔다. shrink-0 — 좁은 flex 줄에서 눌려 찌그러지지 않게.
68
+ "relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full align-middle font-bold leading-none",
69
+ metric.box,
70
+ metric.text,
71
+ fill === "solid" ? TONE_SOLID[tone] : TONE_LIGHT[tone],
72
+ // ★ 함정 4 — outline-solid 를 빼면 폭만 있고 선이 없을 수 있다
73
+ bordered && "outline-2 outline-solid outline-bg",
74
+ className
75
+ ),
76
+ ...rest,
77
+ children: [
78
+ text ? (
79
+ // role="img" 는 자손을 읽지 않지만, 그 규칙을 안 지키는 보조기술이 있어 명시해 둔다.
80
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: text })
81
+ ) : /* @__PURE__ */ jsx("i", { className: cx("svg-icon ico-my bg-current", metric.icon), "aria-hidden": "true" }),
82
+ src && /* 이니셜 위에 덮는다. 실패하면 아무것도 안 그려져 밑의 이니셜이 드러난다(함정 2).
83
+ 프레임워크의 이미지 부품이 아니라 맨 `<img>` 다 — 프로필 사진은 기관이 올린 파일이라 호스트가
84
+ 미리 정해져 있지 않다. 저장소 eslint 설정이 `no-img-element` 를 경고로 낮춰 둔 이유가 이것이다. */
85
+ /* @__PURE__ */ jsx(
86
+ "img",
87
+ {
88
+ src,
89
+ alt: "",
90
+ loading: "lazy",
91
+ decoding: "async",
92
+ className: "absolute inset-0 size-full object-cover"
93
+ }
94
+ )
95
+ ]
96
+ }
97
+ );
98
+ }
99
+ function AvatarGroup({
100
+ items,
101
+ max = 4,
102
+ size = "small",
103
+ fill = "light",
104
+ tone = "gray",
105
+ spaced,
106
+ label,
107
+ className,
108
+ ...rest
109
+ }) {
110
+ const limit = Math.max(1, Math.trunc(max) || 1);
111
+ const shown = items.slice(0, limit);
112
+ const rest_ = items.length - shown.length;
113
+ const step = spaced ? "ml-2" : SIZE[size].overlap;
114
+ return /* @__PURE__ */ jsxs("ul", { "aria-label": label, className: cx("flex items-center", className), ...rest, children: [
115
+ shown.map((it, i) => /* @__PURE__ */ jsx("li", { className: cx("shrink-0 first:ml-0", step), children: /* @__PURE__ */ jsx(
116
+ Avatar,
117
+ {
118
+ name: it.name,
119
+ label: it.label,
120
+ src: it.src,
121
+ size,
122
+ fill,
123
+ tone: it.tone ?? tone,
124
+ bordered: true
125
+ }
126
+ ) }, `${it.name}-${i}`)),
127
+ rest_ > 0 && /* @__PURE__ */ jsx("li", { className: cx("shrink-0 first:ml-0", step), children: /* @__PURE__ */ jsx(
128
+ Avatar,
129
+ {
130
+ name: `\uC678 ${rest_}\uBA85`,
131
+ initials: `+${rest_}`,
132
+ size,
133
+ fill,
134
+ tone: "gray",
135
+ bordered: true,
136
+ className: "tabular-nums"
137
+ }
138
+ ) })
139
+ ] });
140
+ }
141
+ export {
142
+ Avatar,
143
+ AvatarGroup,
144
+ avatarInitials
145
+ };
146
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/avatar/index.tsx"],"sourcesContent":["import type { ComponentPropsWithRef } from 'react';\nimport { cx, type KrdsSize, type KrdsTone } from '../lib/krds';\n\n/**\n * 아바타(사용자 표식) — KRDS 킷에 없다. KRDS 토큰을 입힌 보충 부품이다.\n *\n * 사람 이름 옆에 놓아 목록에서 행을 구분한다. 스스로 누를 수 없는 `<span>` 이다 —\n * 누르는 일은 감싸는 `<Link>`·`<button>` 이 한다.\n * 이름이 곁에 이미 적혀 있으면 `decorative` 를 켠다(안 켜면 이름이 두 번 읽힌다).\n * 여러 사람을 겹쳐 늘어놓을 때는 `AvatarGroup` 을 쓴다.\n *\n * @example\n * <Avatar name=\"홍길동\" />\n * <Avatar name=\"홍길동\" src=\"/files/1.jpg\" size=\"small\" />\n * <span className=\"flex items-center gap-2\">\n * <Avatar name={p.author} size=\"xsmall\" decorative /> {p.author}\n * </span>\n * <AvatarGroup label=\"참여자\" items={members} max={4} />\n *\n * 자세히: docs/krds/09-부품-노트.md#아바타\n */\n\n/** 채움 방식. 킷 배지의 `bg-light-*` · `bg-*` 짝을 유틸리티로 옮긴 것이다. */\nexport type AvatarFill = 'light' | 'solid';\n\n/**\n * 크기 → 상자·글자·아이콘·겹침. 상자 높이는 버튼·입력칸과 같은 눈금이고 글자는 레이블 축이다.\n * 아이콘은 상자의 절반, 겹침 폭은 원의 1/4쯤이다.\n */\nconst SIZE: Record<KrdsSize, { box: string; text: string; icon: string; overlap: string }> = {\n xsmall: { box: 'size-8', text: 'text-label-xs', icon: 'size-4', overlap: '-ml-2' }, // 32 / 13 / 16\n small: { box: 'size-10', text: 'text-label-sm', icon: 'size-5', overlap: '-ml-2' }, // 40 / 15 / 20\n medium: { box: 'size-12', text: 'text-label-md', icon: 'size-6', overlap: '-ml-3' }, // 48 / 17 / 24\n large: { box: 'size-14', text: 'text-label-lg', icon: 'size-7', overlap: '-ml-4' }, // 56 / 19 / 28\n xlarge: { box: 'size-16', text: 'text-label-lg', icon: 'size-8', overlap: '-ml-4' }, // 64 / 19 / 32\n};\n\n/**\n * 옅은 채움. 킷 배지 `bg-light-*` 와 같은 색 짝이다(대비를 킷이 이미 검증했다).\n * 완성된 문자열로 적는다 — Tailwind 는 글자 그대로 적힌 클래스만 만든다.\n */\nconst TONE_LIGHT: Record<KrdsTone, string> = {\n primary: 'bg-primary-surface text-primary',\n secondary: 'bg-secondary-surface text-secondary',\n gray: 'bg-surface-subtle text-fg-subtle',\n point: 'bg-point-surface text-point',\n danger: 'bg-danger-surface text-danger',\n warning: 'bg-warning-surface text-warning',\n success: 'bg-success-surface text-success',\n information: 'bg-information-surface text-information',\n};\n\n/**\n * 꽉 찬 채움. 킷 배지 `bg-*`(element 바탕 + 흰 글자)와 같은 짝이다.\n * gray 만 solid 토큰이 없어 `bg-fg-subtle` 로 잇는다.\n */\nconst TONE_SOLID: Record<KrdsTone, string> = {\n primary: 'bg-primary-solid text-fg-inverse',\n secondary: 'bg-secondary-solid text-fg-inverse',\n gray: 'bg-fg-subtle text-fg-inverse',\n point: 'bg-point-solid text-fg-inverse',\n danger: 'bg-danger-solid text-fg-inverse',\n warning: 'bg-warning-solid text-fg-inverse',\n success: 'bg-success-solid text-fg-inverse',\n information: 'bg-information-solid text-fg-inverse',\n};\n\n/** 한글 — 완성형 가나다(AC00~D7A3)와 호환 자모(3131~318E). */\nconst HANGUL = /[ㄱ-ㆎ가-힣]/;\n\n/**\n * 이름에서 이니셜을 뽑는다. 한글은 성 한 글자, 서양식 이름은 첫 두 낱말의 머리글자다.\n *\n * @example\n * avatarInitials('홍길동') // '홍'\n * avatarInitials('남궁 민수') // '남'\n * avatarInitials('Hong Gildong') // 'HG'\n * avatarInitials('gildong') // 'G'\n */\nexport function avatarInitials(name: string): string {\n const words = name.trim().split(/\\s+/).filter(Boolean);\n const first = Array.from(words[0] ?? '')[0] ?? '';\n if (!first) return '';\n if (HANGUL.test(first)) return first;\n const second = words.length > 1 ? (Array.from(words[1] ?? '')[0] ?? '') : '';\n return (first + second).toUpperCase();\n}\n\ntype AvatarBase = {\n /** 기본 medium(48px). 같은 줄의 버튼·입력칸과 **같은 값**을 준다. */\n size?: KrdsSize;\n /** 기본 gray. 뜻이 있을 때만 다른 색을 준다 — 이름 해시로 색을 흩뿌리지 않는다. */\n tone?: KrdsTone;\n fill?: AvatarFill;\n /** 프로필 사진 주소. 불러오기에 실패하면 밑의 이니셜이 드러난다. */\n src?: string;\n /** 이니셜을 직접 준다. 생략하면 `name` 에서 뽑는다. */\n initials?: string;\n /** 겹쳐 놓을 때 서로 떨어져 보이도록 바탕색 테두리를 두른다. `AvatarGroup` 이 켜 준다. */\n bordered?: boolean;\n className?: string;\n};\n\n/** 아바타 하나가 사람을 대신하는 자리. 보조기술이 이름을 읽는다. */\nexport type AvatarProps = Omit<ComponentPropsWithRef<'span'>, 'children'> &\n AvatarBase & {\n decorative?: false;\n /** 사람 이름. 이니셜과 보조기술 이름이 여기서 나온다. */\n name: string;\n /** 읽을 문구를 이름과 다르게 하고 싶을 때(예: `홍길동 (관리자)`). */\n label?: string;\n };\n\n/** 곁에 이름이 이미 적혀 있는 자리. 보조기술에서 감춘다(이름이 두 번 읽히지 않게). */\nexport type DecorativeAvatarProps = Omit<ComponentPropsWithRef<'span'>, 'children'> &\n AvatarBase & {\n decorative: true;\n /** 이니셜을 뽑는 데만 쓴다. 읽히지는 않는다. */\n name?: string;\n };\n\n/** 두 갈래를 한 번에 꺼내기 위한 내부 형. 밖으로 내보내지 않는다. */\ntype AvatarAnyProps = Omit<ComponentPropsWithRef<'span'>, 'children'> &\n AvatarBase & { decorative?: boolean; name?: string; label?: string };\n\n/**\n * 사용자 표식 하나. 이니셜을 깔고 `src` 가 있으면 그 위에 사진을 덮는다.\n * 이름도 이미지도 없으면 킷 아이콘 `ico-my` 를 그린다(비회원 글 등).\n *\n * @example\n * <Avatar name=\"홍길동\" size=\"small\" src={user.photo} />\n */\nexport function Avatar(props: AvatarProps | DecorativeAvatarProps) {\n const {\n size = 'medium',\n tone = 'gray',\n fill = 'light',\n src,\n initials,\n bordered,\n decorative = false,\n name,\n label,\n className,\n ...rest\n } = props as AvatarAnyProps;\n\n const metric = SIZE[size];\n const text = initials ?? (name ? avatarInitials(name) : '');\n\n return (\n <span\n // 장식용이면 통째로 감춘다. 아니면 그림 하나로 읽히고 이름은 aria-label 이 준다.\n role={decorative ? undefined : 'img'}\n aria-label={decorative ? undefined : (label ?? name)}\n aria-hidden={decorative ? true : undefined}\n className={cx(\n // relative/overflow-hidden — 사진을 원 안에 가둔다. shrink-0 — 좁은 flex 줄에서 눌려 찌그러지지 않게.\n 'relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full align-middle font-bold leading-none',\n metric.box,\n metric.text,\n fill === 'solid' ? TONE_SOLID[tone] : TONE_LIGHT[tone],\n // ★ 함정 4 — outline-solid 를 빼면 폭만 있고 선이 없을 수 있다\n bordered && 'outline-2 outline-solid outline-bg',\n className,\n )}\n {...rest}\n >\n {text ? (\n // role=\"img\" 는 자손을 읽지 않지만, 그 규칙을 안 지키는 보조기술이 있어 명시해 둔다.\n <span aria-hidden=\"true\">{text}</span>\n ) : (\n <i className={cx('svg-icon ico-my bg-current', metric.icon)} aria-hidden=\"true\" />\n )}\n {src && (\n /* 이니셜 위에 덮는다. 실패하면 아무것도 안 그려져 밑의 이니셜이 드러난다(함정 2).\n 프레임워크의 이미지 부품이 아니라 맨 `<img>` 다 — 프로필 사진은 기관이 올린 파일이라 호스트가\n 미리 정해져 있지 않다. 저장소 eslint 설정이 `no-img-element` 를 경고로 낮춰 둔 이유가 이것이다. */\n <img\n src={src}\n alt=\"\"\n loading=\"lazy\"\n decoding=\"async\"\n className=\"absolute inset-0 size-full object-cover\"\n />\n )}\n </span>\n );\n}\n\n/** 묶음에 넣을 한 사람. */\nexport type AvatarGroupItem = {\n name: string;\n src?: string;\n tone?: KrdsTone;\n /** 읽을 문구를 이름과 다르게 할 때. */\n label?: string;\n};\n\nexport type AvatarGroupProps = Omit<ComponentPropsWithRef<'ul'>, 'children'> & {\n items: AvatarGroupItem[];\n /** 몇 칸까지 보일지. 나머지는 `+N` 한 칸으로 접는다. 기본 4. */\n max?: number;\n /** 기본 small(40px) — 목록 안에 들어가는 일이 많아 하나짜리보다 작다. */\n size?: KrdsSize;\n fill?: AvatarFill;\n /** 모든 칸의 기본 색. 항목이 `tone` 을 주면 그것이 이긴다. */\n tone?: KrdsTone;\n /** 겹치지 않고 나란히 놓는다. */\n spaced?: boolean;\n /** 묶음 전체의 이름(`<ul>` 의 aria-label). 예: 「참여자」 */\n label?: string;\n};\n\n/**\n * 아바타 묶음 — 담당자·참여자를 겹쳐 늘어놓는다.\n *\n * `children` 이 아니라 `items` 를 받는다. 겹침 간격·테두리·`+N` 칸을 묶음이 정해야 하기 때문이다.\n * `<ul>/<li>` 라 스크린리더가 개수를 세어 준다 — 각 칸의 이름도 그대로 읽힌다.\n *\n * @example\n * <AvatarGroup label=\"참여자\" items={members} max={4} />\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = 'small',\n fill = 'light',\n tone = 'gray',\n spaced,\n label,\n className,\n ...rest\n}: AvatarGroupProps) {\n // 0·음수·소수를 막는다. 부모가 화면 폭에서 계산한 값을 그대로 넘기는 일이 흔하다.\n const limit = Math.max(1, Math.trunc(max) || 1);\n const shown = items.slice(0, limit);\n const rest_ = items.length - shown.length;\n const step = spaced ? 'ml-2' : SIZE[size].overlap;\n\n return (\n <ul aria-label={label} className={cx('flex items-center', className)} {...rest}>\n {shown.map((it, i) => (\n <li key={`${it.name}-${i}`} className={cx('shrink-0 first:ml-0', step)}>\n <Avatar\n name={it.name}\n label={it.label}\n src={it.src}\n size={size}\n fill={fill}\n tone={it.tone ?? tone}\n bordered\n />\n </li>\n ))}\n {rest_ > 0 && (\n <li className={cx('shrink-0 first:ml-0', step)}>\n {/* 화면에는 `+3`, 보조기술에는 「외 3명」. 숫자는 tabular-nums 로 폭을 고정한다. */}\n <Avatar\n name={`외 ${rest_}명`}\n initials={`+${rest_}`}\n size={size}\n fill={fill}\n tone=\"gray\"\n bordered\n className=\"tabular-nums\"\n />\n </li>\n )}\n </ul>\n );\n}\n"],"mappings":"AAuJI,SAmBI,KAnBJ;AAtJJ,SAAS,UAAwC;AA4BjD,MAAM,OAAuF;AAAA,EAC3F,QAAQ,EAAE,KAAK,UAAU,MAAM,iBAAiB,MAAM,UAAU,SAAS,QAAQ;AAAA;AAAA,EACjF,OAAO,EAAE,KAAK,WAAW,MAAM,iBAAiB,MAAM,UAAU,SAAS,QAAQ;AAAA;AAAA,EACjF,QAAQ,EAAE,KAAK,WAAW,MAAM,iBAAiB,MAAM,UAAU,SAAS,QAAQ;AAAA;AAAA,EAClF,OAAO,EAAE,KAAK,WAAW,MAAM,iBAAiB,MAAM,UAAU,SAAS,QAAQ;AAAA;AAAA,EACjF,QAAQ,EAAE,KAAK,WAAW,MAAM,iBAAiB,MAAM,UAAU,SAAS,QAAQ;AAAA;AACpF;AAMA,MAAM,aAAuC;AAAA,EAC3C,SAAS;AAAA,EACT,WAAW;AAAA,EACX,MAAM;AAAA,EACN,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AAMA,MAAM,aAAuC;AAAA,EAC3C,SAAS;AAAA,EACT,WAAW;AAAA,EACX,MAAM;AAAA,EACN,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AAGA,MAAM,SAAS;AAWR,SAAS,eAAe,MAAsB;AACnD,QAAM,QAAQ,KAAK,KAAK,EAAE,MAAM,KAAK,EAAE,OAAO,OAAO;AACrD,QAAM,QAAQ,MAAM,KAAK,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK;AAC/C,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,OAAO,KAAK,KAAK,EAAG,QAAO;AAC/B,QAAM,SAAS,MAAM,SAAS,IAAK,MAAM,KAAK,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAM;AAC1E,UAAQ,QAAQ,QAAQ,YAAY;AACtC;AA8CO,SAAS,OAAO,OAA4C;AACjE,QAAM;AAAA,IACJ,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AAEJ,QAAM,SAAS,KAAK,IAAI;AACxB,QAAM,OAAO,aAAa,OAAO,eAAe,IAAI,IAAI;AAExD,SACE;AAAA,IAAC;AAAA;AAAA,MAEC,MAAM,aAAa,SAAY;AAAA,MAC/B,cAAY,aAAa,SAAa,SAAS;AAAA,MAC/C,eAAa,aAAa,OAAO;AAAA,MACjC,WAAW;AAAA;AAAA,QAET;AAAA,QACA,OAAO;AAAA,QACP,OAAO;AAAA,QACP,SAAS,UAAU,WAAW,IAAI,IAAI,WAAW,IAAI;AAAA;AAAA,QAErD,YAAY;AAAA,QACZ;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA;AAAA,UAEC,oBAAC,UAAK,eAAY,QAAQ,gBAAK;AAAA,YAE/B,oBAAC,OAAE,WAAW,GAAG,8BAA8B,OAAO,IAAI,GAAG,eAAY,QAAO;AAAA,QAEjF;AAAA;AAAA;AAAA,QAIC;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,KAAI;AAAA,YACJ,SAAQ;AAAA,YACR,UAAS;AAAA,YACT,WAAU;AAAA;AAAA,QACZ;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAmCO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AAEnB,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,GAAG,KAAK,CAAC;AAC9C,QAAM,QAAQ,MAAM,MAAM,GAAG,KAAK;AAClC,QAAM,QAAQ,MAAM,SAAS,MAAM;AACnC,QAAM,OAAO,SAAS,SAAS,KAAK,IAAI,EAAE;AAE1C,SACE,qBAAC,QAAG,cAAY,OAAO,WAAW,GAAG,qBAAqB,SAAS,GAAI,GAAG,MACvE;AAAA,UAAM,IAAI,CAAC,IAAI,MACd,oBAAC,QAA2B,WAAW,GAAG,uBAAuB,IAAI,GACnE;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,GAAG;AAAA,QACT,OAAO,GAAG;AAAA,QACV,KAAK,GAAG;AAAA,QACR;AAAA,QACA;AAAA,QACA,MAAM,GAAG,QAAQ;AAAA,QACjB,UAAQ;AAAA;AAAA,IACV,KATO,GAAG,GAAG,IAAI,IAAI,CAAC,EAUxB,CACD;AAAA,IACA,QAAQ,KACP,oBAAC,QAAG,WAAW,GAAG,uBAAuB,IAAI,GAE3C;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,UAAK,KAAK;AAAA,QAChB,UAAU,IAAI,KAAK;AAAA,QACnB;AAAA,QACA;AAAA,QACA,MAAK;AAAA,QACL,UAAQ;AAAA,QACR,WAAU;AAAA;AAAA,IACZ,GACF;AAAA,KAEJ;AAEJ;","names":[]}
@@ -0,0 +1,49 @@
1
+ import type { ComponentPropsWithRef } from 'react';
2
+ import { type KrdsTone } from '../lib/krds';
3
+ /**
4
+ * 배지 — KRDS `krds-badge`.
5
+ *
6
+ * 상태·분류를 한 낱말로 표시한다. 누르는 것이 아니다 —
7
+ * 필터 조건처럼 누르거나 지우는 표시는 `ui/tag` 의 `Tag` · `TagButton` 이다.
8
+ * 여럿을 늘어놓을 때는 `BadgeWrap` 으로 감싼다(간격은 킷이 잡는다).
9
+ *
10
+ * @example
11
+ * <Badge tone="success">승인</Badge>
12
+ * <Badge tone="primary" fill="solid" number>5</Badge>
13
+ * <BadgeWrap>
14
+ * <Badge tone="danger">반려</Badge>
15
+ * <Badge tone="gray">보관</Badge>
16
+ * </BadgeWrap>
17
+ *
18
+ * 자세히: docs/krds/09-부품-노트.md#배지
19
+ */
20
+ /** 채움 방식. 킷 클래스는 각각 `bg-*` · `bg-light-*` · `outline-*`. */
21
+ export type BadgeFill = 'solid' | 'light' | 'outline';
22
+ /** 배지 크기. 킷에 규칙이 있는 것은 `large` 하나뿐이라 그것만 연다. */
23
+ export type BadgeSize = 'large';
24
+ export type BadgeProps = ComponentPropsWithRef<'span'> & {
25
+ /** 색조. 기본 `gray`. */
26
+ tone?: KrdsTone | 'disabled';
27
+ /** 채움 방식. 기본 `light`. */
28
+ fill?: BadgeFill;
29
+ /** 크기. 생략하면 킷 기본 크기. */
30
+ size?: BadgeSize;
31
+ /** 숫자 배지(`number`). 알림 개수처럼 수를 담을 때. */
32
+ number?: boolean;
33
+ /** 점 배지(`dot`). 글자 없이 점만 찍는다 — 곁에 `sr-only` 설명이나 부모 `aria-label` 이 필요하다. */
34
+ dot?: boolean;
35
+ };
36
+ export declare function Badge({ tone, fill, size, number, dot, className, children, ...rest }: BadgeProps): import("react").JSX.Element;
37
+ /**
38
+ * 배지 묶음 — 킷 `krds-badge-wrap`.
39
+ *
40
+ * 배지를 여럿 늘어놓을 때 간격을 킷이 잡아 준다. `ml-*` 를 손으로 붙이지 않는다.
41
+ *
42
+ * @example
43
+ * <BadgeWrap>
44
+ * <Badge tone="primary">공지</Badge>
45
+ * <Badge tone="gray">보관</Badge>
46
+ * </BadgeWrap>
47
+ */
48
+ export declare function BadgeWrap({ className, children, ...rest }: ComponentPropsWithRef<'div'>): import("react").JSX.Element;
49
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/badge/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,OAAO,CAAC;AACnD,OAAO,EAAM,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;;;GAgBG;AAEH,4DAA4D;AAC5D,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;AAEtD,gDAAgD;AAChD,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC;AAEhC,MAAM,MAAM,UAAU,GAAG,qBAAqB,CAAC,MAAM,CAAC,GAAG;IACvD,qBAAqB;IACrB,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC7B,yBAAyB;IACzB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,wBAAwB;IACxB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,4EAA4E;IAC5E,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAQF,wBAAgB,KAAK,CAAC,EACpB,IAAa,EACb,IAAc,EACd,IAAI,EACJ,MAAM,EACN,GAAG,EACH,SAAS,EACT,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,UAAU,+BAgBZ;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,qBAAqB,CAAC,KAAK,CAAC,+BAMvF"}
@@ -0,0 +1,41 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cx } from "../lib/krds.js";
3
+ const FILL_PREFIX = {
4
+ solid: "bg-",
5
+ light: "bg-light-",
6
+ outline: "outline-"
7
+ };
8
+ function Badge({
9
+ tone = "gray",
10
+ fill = "light",
11
+ size,
12
+ number,
13
+ dot,
14
+ className,
15
+ children,
16
+ ...rest
17
+ }) {
18
+ return /* @__PURE__ */ jsx(
19
+ "span",
20
+ {
21
+ className: cx(
22
+ "krds-badge",
23
+ size,
24
+ `${FILL_PREFIX[fill]}${tone}`,
25
+ number && "number",
26
+ dot && "dot",
27
+ className
28
+ ),
29
+ ...rest,
30
+ children
31
+ }
32
+ );
33
+ }
34
+ function BadgeWrap({ className, children, ...rest }) {
35
+ return /* @__PURE__ */ jsx("div", { className: cx("krds-badge-wrap", className), ...rest, children });
36
+ }
37
+ export {
38
+ Badge,
39
+ BadgeWrap
40
+ };
41
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/badge/index.tsx"],"sourcesContent":["import type { ComponentPropsWithRef } from 'react';\nimport { cx, type KrdsTone } from '../lib/krds';\n\n/**\n * 배지 — KRDS `krds-badge`.\n *\n * 상태·분류를 한 낱말로 표시한다. 누르는 것이 아니다 —\n * 필터 조건처럼 누르거나 지우는 표시는 `ui/tag` 의 `Tag` · `TagButton` 이다.\n * 여럿을 늘어놓을 때는 `BadgeWrap` 으로 감싼다(간격은 킷이 잡는다).\n *\n * @example\n * <Badge tone=\"success\">승인</Badge>\n * <Badge tone=\"primary\" fill=\"solid\" number>5</Badge>\n * <BadgeWrap>\n * <Badge tone=\"danger\">반려</Badge>\n * <Badge tone=\"gray\">보관</Badge>\n * </BadgeWrap>\n *\n * 자세히: docs/krds/09-부품-노트.md#배지\n */\n\n/** 채움 방식. 킷 클래스는 각각 `bg-*` · `bg-light-*` · `outline-*`. */\nexport type BadgeFill = 'solid' | 'light' | 'outline';\n\n/** 배지 크기. 킷에 규칙이 있는 것은 `large` 하나뿐이라 그것만 연다. */\nexport type BadgeSize = 'large';\n\nexport type BadgeProps = ComponentPropsWithRef<'span'> & {\n /** 색조. 기본 `gray`. */\n tone?: KrdsTone | 'disabled';\n /** 채움 방식. 기본 `light`. */\n fill?: BadgeFill;\n /** 크기. 생략하면 킷 기본 크기. */\n size?: BadgeSize;\n /** 숫자 배지(`number`). 알림 개수처럼 수를 담을 때. */\n number?: boolean;\n /** 점 배지(`dot`). 글자 없이 점만 찍는다 — 곁에 `sr-only` 설명이나 부모 `aria-label` 이 필요하다. */\n dot?: boolean;\n};\n\nconst FILL_PREFIX: Record<BadgeFill, string> = {\n solid: 'bg-',\n light: 'bg-light-',\n outline: 'outline-',\n};\n\nexport function Badge({\n tone = 'gray',\n fill = 'light',\n size,\n number,\n dot,\n className,\n children,\n ...rest\n}: BadgeProps) {\n return (\n <span\n className={cx(\n 'krds-badge',\n size,\n `${FILL_PREFIX[fill]}${tone}`,\n number && 'number',\n dot && 'dot',\n className,\n )}\n {...rest}\n >\n {children}\n </span>\n );\n}\n\n/**\n * 배지 묶음 — 킷 `krds-badge-wrap`.\n *\n * 배지를 여럿 늘어놓을 때 간격을 킷이 잡아 준다. `ml-*` 를 손으로 붙이지 않는다.\n *\n * @example\n * <BadgeWrap>\n * <Badge tone=\"primary\">공지</Badge>\n * <Badge tone=\"gray\">보관</Badge>\n * </BadgeWrap>\n */\nexport function BadgeWrap({ className, children, ...rest }: ComponentPropsWithRef<'div'>) {\n return (\n <div className={cx('krds-badge-wrap', className)} {...rest}>\n {children}\n </div>\n );\n}\n"],"mappings":"AAyDI;AAxDJ,SAAS,UAAyB;AAuClC,MAAM,cAAyC;AAAA,EAC7C,OAAO;AAAA,EACP,OAAO;AAAA,EACP,SAAS;AACX;AAEO,SAAS,MAAM;AAAA,EACpB,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAe;AACb,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,GAAG,YAAY,IAAI,CAAC,GAAG,IAAI;AAAA,QAC3B,UAAU;AAAA,QACV,OAAO;AAAA,QACP;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAaO,SAAS,UAAU,EAAE,WAAW,UAAU,GAAG,KAAK,GAAiC;AACxF,SACE,oBAAC,SAAI,WAAW,GAAG,mBAAmB,SAAS,GAAI,GAAG,MACnD,UACH;AAEJ;","names":[]}
@@ -0,0 +1,43 @@
1
+ import type { ComponentPropsWithRef, ReactNode } from 'react';
2
+ /**
3
+ * 브레드크럼 — KRDS `krds-breadcrumb-wrap`.
4
+ *
5
+ * 홈은 부품이 늘 앞에 붙이므로 `items` 에는 **홈 다음부터**의 경로만 넣는다.
6
+ * 주소가 없는 칸(`href` 생략)은 링크가 아니라 계층 표시로만 그려진다.
7
+ * 메인·랜딩 화면과 1수준 사이트에는 쓰지 않는다 — `items` 가 비면 아무것도 그리지 않는다.
8
+ * 여백은 킷이 잡으므로 `mb-*` 를 더하지 마라.
9
+ *
10
+ * @example
11
+ * <Breadcrumb
12
+ * items={[
13
+ * { label: '정보공개', href: '/open' },
14
+ * { label: '사전정보공표' },
15
+ * ]}
16
+ * />
17
+ *
18
+ * 자세히: docs/krds/09-부품-노트.md#브레드크럼
19
+ */
20
+ /** 경로 한 칸. */
21
+ export type BreadcrumbItem = {
22
+ /** 화면에 보이는 이름. */
23
+ label: ReactNode;
24
+ /** 이동 주소. 없으면 링크로 그리지 않는다 — 마지막 칸은 현재 화면, 가운데 칸은 흐린 글자가 된다. */
25
+ href?: string;
26
+ /** 바깥 주소 — `UiLink` 대신 맨 `<a>` 로 그린다. */
27
+ external?: boolean;
28
+ };
29
+ export type BreadcrumbProps = Omit<ComponentPropsWithRef<'nav'>, 'children'> & {
30
+ /** 홈 다음부터의 경로. 홈은 부품이 늘 앞에 붙인다. */
31
+ items: BreadcrumbItem[];
32
+ /** 홈 칸 이름. 다국어는 화면에서 넘긴다. */
33
+ homeLabel?: string;
34
+ /** 홈 칸 주소. */
35
+ homeHref?: string;
36
+ /** `<nav>` 의 이름. 기본 「현재 경로」(킷 샘플 문구). */
37
+ label?: string;
38
+ /** PC 에서 보일 최대 칸 수 — 홈 포함. 기본 4. 넘으면 말줄임으로 줄이되 계층은 `sr-only` 로 남는다. */
39
+ maxItems?: number;
40
+ className?: string;
41
+ };
42
+ export declare function Breadcrumb({ items, homeLabel, homeHref, label, maxItems, className, ...rest }: BreadcrumbProps): import("react").JSX.Element | null;
43
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/breadcrumb/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAG9D;;;;;;;;;;;;;;;;;GAiBG;AAEH,cAAc;AACd,MAAM,MAAM,cAAc,GAAG;IAC3B,kBAAkB;IAClB,KAAK,EAAE,SAAS,CAAC;IACjB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,GAAG;IAC7E,mCAAmC;IACnC,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,SAAe,EACf,QAAc,EACd,KAAe,EACf,QAAY,EACZ,SAAS,EACT,GAAG,IAAI,EACR,EAAE,eAAe,sCAqEjB"}
@@ -0,0 +1,43 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { UiLink } from "../lib/link.js";
3
+ import { cx } from "../lib/krds.js";
4
+ function Breadcrumb({
5
+ items,
6
+ homeLabel = "\uD648",
7
+ homeHref = "/",
8
+ label = "\uD604\uC7AC \uACBD\uB85C",
9
+ maxItems = 4,
10
+ className,
11
+ ...rest
12
+ }) {
13
+ if (!items.length) return null;
14
+ const max = Math.max(2, Math.floor(maxItems));
15
+ const tail = max - 1;
16
+ const collapsed = items.length > tail;
17
+ const hidden = collapsed ? items.slice(0, items.length - tail) : [];
18
+ const shown = collapsed ? items.slice(items.length - tail) : items;
19
+ const crumb = (item, key, isLast, offscreen) => {
20
+ const { label: text, href, external } = item;
21
+ const inner = href ? external ? /* @__PURE__ */ jsx("a", { className: "txt", href, children: text }) : /* @__PURE__ */ jsx(UiLink, { className: "txt", href, children: text }) : /* @__PURE__ */ jsx(
22
+ "span",
23
+ {
24
+ className: cx("txt", !isLast && "text-fg-subtle"),
25
+ "aria-current": isLast ? "page" : void 0,
26
+ children: text
27
+ }
28
+ );
29
+ return /* @__PURE__ */ jsx("li", { className: cx(offscreen && "sr-only"), children: inner }, key);
30
+ };
31
+ return /* @__PURE__ */ jsx("nav", { className: cx("krds-breadcrumb-wrap", className), "aria-label": label, ...rest, children: /* @__PURE__ */ jsxs("ol", { className: "breadcrumb", children: [
32
+ /* @__PURE__ */ jsx("li", { className: "home", children: /* @__PURE__ */ jsx(UiLink, { className: "txt", href: homeHref, children: homeLabel }) }),
33
+ hidden.map((item, i) => crumb(item, `hidden-${i}`, false, true)),
34
+ collapsed && // PC 말줄임 칸. 킷에는 PC 용 말줄임 규칙이 없어 `ico-ellipsis` 로 직접 그린다.
35
+ // 계층은 위의 `sr-only` 칸들이 읽어 주므로 이 칸은 장식이다.
36
+ /* @__PURE__ */ jsx("li", { "aria-hidden": "true", children: /* @__PURE__ */ jsx("i", { className: "svg-icon ico-ellipsis size-4" }) }),
37
+ shown.map((item, i) => crumb(item, `shown-${i}`, i === shown.length - 1, false))
38
+ ] }) });
39
+ }
40
+ export {
41
+ Breadcrumb
42
+ };
43
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/breadcrumb/index.tsx"],"sourcesContent":["import { UiLink } from '../lib/link';\nimport type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { cx } from '../lib/krds';\n\n/**\n * 브레드크럼 — KRDS `krds-breadcrumb-wrap`.\n *\n * 홈은 부품이 늘 앞에 붙이므로 `items` 에는 **홈 다음부터**의 경로만 넣는다.\n * 주소가 없는 칸(`href` 생략)은 링크가 아니라 계층 표시로만 그려진다.\n * 메인·랜딩 화면과 1수준 사이트에는 쓰지 않는다 — `items` 가 비면 아무것도 그리지 않는다.\n * 여백은 킷이 잡으므로 `mb-*` 를 더하지 마라.\n *\n * @example\n * <Breadcrumb\n * items={[\n * { label: '정보공개', href: '/open' },\n * { label: '사전정보공표' },\n * ]}\n * />\n *\n * 자세히: docs/krds/09-부품-노트.md#브레드크럼\n */\n\n/** 경로 한 칸. */\nexport type BreadcrumbItem = {\n /** 화면에 보이는 이름. */\n label: ReactNode;\n /** 이동 주소. 없으면 링크로 그리지 않는다 — 마지막 칸은 현재 화면, 가운데 칸은 흐린 글자가 된다. */\n href?: string;\n /** 바깥 주소 — `UiLink` 대신 맨 `<a>` 로 그린다. */\n external?: boolean;\n};\n\nexport type BreadcrumbProps = Omit<ComponentPropsWithRef<'nav'>, 'children'> & {\n /** 홈 다음부터의 경로. 홈은 부품이 늘 앞에 붙인다. */\n items: BreadcrumbItem[];\n /** 홈 칸 이름. 다국어는 화면에서 넘긴다. */\n homeLabel?: string;\n /** 홈 칸 주소. */\n homeHref?: string;\n /** `<nav>` 의 이름. 기본 「현재 경로」(킷 샘플 문구). */\n label?: string;\n /** PC 에서 보일 최대 칸 수 — 홈 포함. 기본 4. 넘으면 말줄임으로 줄이되 계층은 `sr-only` 로 남는다. */\n maxItems?: number;\n className?: string;\n};\n\nexport function Breadcrumb({\n items,\n homeLabel = '홈',\n homeHref = '/',\n label = '현재 경로',\n maxItems = 4,\n className,\n ...rest\n}: BreadcrumbProps) {\n // 규격: 메인 화면에는 브레드크럼을 쓰지 않는다. 홈 하나만 남는 경우가 그것이다.\n if (!items.length) return null;\n\n // 홈 한 칸은 반드시 남으므로 최소 2. 뒤에 남길 칸 수 = 상한 - 홈.\n const max = Math.max(2, Math.floor(maxItems));\n const tail = max - 1;\n // 홈까지 세어 상한을 넘으면 줄인다. 예) 상한 4 · items 4개 → 홈 · … · 뒤 3개.\n const collapsed = items.length > tail;\n const hidden = collapsed ? items.slice(0, items.length - tail) : [];\n const shown = collapsed ? items.slice(items.length - tail) : items;\n\n const crumb = (item: BreadcrumbItem, key: string, isLast: boolean, offscreen: boolean) => {\n const { label: text, href, external } = item;\n\n // 주소가 없으면 `<span class=\"txt\">` 다(킷의 `a.txt.disabled` 는 밑줄이 남아 쓰지 않는다).\n // 흐린 글자색만 보충한다 — 배경·둥글기·여백은 `li .txt` 가 span 에도 걸어 준다.\n const inner = href ? (\n external ? (\n <a className=\"txt\" href={href}>\n {text}\n </a>\n ) : (\n <UiLink className=\"txt\" href={href}>\n {text}\n </UiLink>\n )\n ) : (\n <span\n // 링크가 아닌 중간 단계. `text-fg-disabled`(#8a949e, 흰 바탕 3.08:1)는 **비활성 컨트롤 전용**이라\n // 읽으라고 둔 글자에 쓰면 WCAG 1.4.3(4.5:1) 위반이다 — axe 실측으로 걸렸다. 흐리게는 `subtle`(8.68:1)로.\n className={cx('txt', !isLast && 'text-fg-subtle')}\n aria-current={isLast ? 'page' : undefined}\n >\n {text}\n </span>\n );\n\n return (\n <li key={key} className={cx(offscreen && 'sr-only')}>\n {inner}\n </li>\n );\n };\n\n return (\n <nav className={cx('krds-breadcrumb-wrap', className)} aria-label={label} {...rest}>\n <ol className=\"breadcrumb\">\n {/* 홈 — 집 아이콘은 킷이 `.home .txt::before` 로 그린다. */}\n <li className=\"home\">\n <UiLink className=\"txt\" href={homeHref}>\n {homeLabel}\n </UiLink>\n </li>\n\n {hidden.map((item, i) => crumb(item, `hidden-${i}`, false, true))}\n\n {collapsed && (\n // PC 말줄임 칸. 킷에는 PC 용 말줄임 규칙이 없어 `ico-ellipsis` 로 직접 그린다.\n // 계층은 위의 `sr-only` 칸들이 읽어 주므로 이 칸은 장식이다.\n <li aria-hidden=\"true\">\n <i className=\"svg-icon ico-ellipsis size-4\" />\n </li>\n )}\n\n {shown.map((item, i) => crumb(item, `shown-${i}`, i === shown.length - 1, false))}\n </ol>\n </nav>\n );\n}\n"],"mappings":"AA0EQ,cA4BF,YA5BE;AA1ER,SAAS,cAAc;AAEvB,SAAS,UAAU;AA6CZ,SAAS,WAAW;AAAA,EACzB;AAAA,EACA,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,WAAW;AAAA,EACX;AAAA,EACA,GAAG;AACL,GAAoB;AAElB,MAAI,CAAC,MAAM,OAAQ,QAAO;AAG1B,QAAM,MAAM,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,CAAC;AAC5C,QAAM,OAAO,MAAM;AAEnB,QAAM,YAAY,MAAM,SAAS;AACjC,QAAM,SAAS,YAAY,MAAM,MAAM,GAAG,MAAM,SAAS,IAAI,IAAI,CAAC;AAClE,QAAM,QAAQ,YAAY,MAAM,MAAM,MAAM,SAAS,IAAI,IAAI;AAE7D,QAAM,QAAQ,CAAC,MAAsB,KAAa,QAAiB,cAAuB;AACxF,UAAM,EAAE,OAAO,MAAM,MAAM,SAAS,IAAI;AAIxC,UAAM,QAAQ,OACZ,WACE,oBAAC,OAAE,WAAU,OAAM,MAChB,gBACH,IAEA,oBAAC,UAAO,WAAU,OAAM,MACrB,gBACH,IAGF;AAAA,MAAC;AAAA;AAAA,QAGC,WAAW,GAAG,OAAO,CAAC,UAAU,gBAAgB;AAAA,QAChD,gBAAc,SAAS,SAAS;AAAA,QAE/B;AAAA;AAAA,IACH;AAGF,WACE,oBAAC,QAAa,WAAW,GAAG,aAAa,SAAS,GAC/C,mBADM,GAET;AAAA,EAEJ;AAEA,SACE,oBAAC,SAAI,WAAW,GAAG,wBAAwB,SAAS,GAAG,cAAY,OAAQ,GAAG,MAC5E,+BAAC,QAAG,WAAU,cAEZ;AAAA,wBAAC,QAAG,WAAU,QACZ,8BAAC,UAAO,WAAU,OAAM,MAAM,UAC3B,qBACH,GACF;AAAA,IAEC,OAAO,IAAI,CAAC,MAAM,MAAM,MAAM,MAAM,UAAU,CAAC,IAAI,OAAO,IAAI,CAAC;AAAA,IAE/D;AAAA;AAAA,IAGC,oBAAC,QAAG,eAAY,QACd,8BAAC,OAAE,WAAU,gCAA+B,GAC9C;AAAA,IAGD,MAAM,IAAI,CAAC,MAAM,MAAM,MAAM,MAAM,SAAS,CAAC,IAAI,MAAM,MAAM,SAAS,GAAG,KAAK,CAAC;AAAA,KAClF,GACF;AAEJ;","names":[]}
@@ -0,0 +1,78 @@
1
+ import type { ComponentPropsWithRef, ReactNode } from 'react';
2
+ import { type KrdsIcon, type KrdsSize } from '../lib/krds';
3
+ /**
4
+ * 버튼 — KRDS `krds-btn`.
5
+ *
6
+ * 이동은 `LinkButton`, 동작은 `Button` 을 쓴다(규격상 다른 것이다).
7
+ * 같은 줄의 입력칸과 같은 `size` 를 준다(기본 medium · 48px).
8
+ * 글자 없이 아이콘만 두려면 `iconOnly` 와 `label` 을 함께 준다.
9
+ * 버튼 여럿은 `ButtonGroup` 으로 묶는다 — 제출은 가장 오른쪽이다.
10
+ *
11
+ * @example
12
+ * <Button variant="primary">저장</Button>
13
+ * <Button size="small" variant="secondary" icon="down">내려받기</Button>
14
+ * <Button iconOnly icon="sch" label="검색" />
15
+ * <LinkButton href="/notice" icon="angle" iconDirection="right">공지 전체보기</LinkButton>
16
+ *
17
+ * 자세히: docs/krds/09-부품-노트.md#버튼
18
+ */
19
+ /** 위계. 한 화면에 최상위 강조(`primary`)는 하나만 둔다. */
20
+ export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'text' | 'link';
21
+ type BaseProps = {
22
+ /** 기본 medium(48px). 같은 줄의 입력칸과 같은 값을 준다. */
23
+ size?: KrdsSize;
24
+ /** 위계. 생략하면 킷 기본 버튼. */
25
+ variant?: ButtonVariant;
26
+ /** 킷 아이콘 이름(`ico-` 뺀 것). 예: `sch` · `down` · `go` */
27
+ icon?: KrdsIcon;
28
+ /** 아이콘 회전. 방향은 이름의 일부가 아니라 별도 클래스다 — `icon="angle right"` 로 쓰지 마라. */
29
+ iconDirection?: 'up' | 'down' | 'left' | 'right';
30
+ /** 아이콘 위치. 기본 `end` — 킷 샘플은 xlarge 만 앞에 둔다. */
31
+ iconPosition?: 'start' | 'end';
32
+ /** 테두리 있는 아이콘 버튼(`krds-btn icon border`). iconOnly 일 때만 뜻이 있다. */
33
+ bordered?: boolean;
34
+ className?: string;
35
+ children?: ReactNode;
36
+ };
37
+ export type ButtonProps = Omit<ComponentPropsWithRef<'button'>, 'children'> & BaseProps & {
38
+ /** 글자 없이 아이콘만 두려면 `true` 와 함께 `label`·`icon` 을 준다({@link IconButtonProps}). */
39
+ iconOnly?: false;
40
+ };
41
+ export type IconButtonProps = Omit<ComponentPropsWithRef<'button'>, 'children'> & BaseProps & {
42
+ iconOnly: true;
43
+ /** 스크린리더가 읽을 이름. 아이콘 버튼에는 반드시 있어야 한다. */
44
+ label: string;
45
+ icon: KrdsIcon;
46
+ };
47
+ export declare function Button(props: ButtonProps | IconButtonProps): import("react").JSX.Element;
48
+ /**
49
+ * 링크 — KRDS `krds-btn link`. **이동에만 쓴다**(동작은 `Button`).
50
+ *
51
+ * 밑줄은 킷이 안쪽 `<span class="underline">` 에 건다 — `hiddenUnderline` · `noUnderline` 로 끈다.
52
+ * `target="_blank"` 면 「새 창 열림」 안내가 `title` 로 붙는다.
53
+ */
54
+ export type LinkButtonProps = Omit<ComponentPropsWithRef<'a'>, 'children'> & Omit<BaseProps, 'variant' | 'bordered'> & {
55
+ /** `basic` = 본문 글자색 링크 · `pure` = 가상클래스에서도 색 유지 */
56
+ linkStyle?: 'basic' | 'pure';
57
+ /** 비활성. 킷 클래스 `.disabled` + `aria-disabled` 로 알린다. 이동까지 막으려면 `href` 를 빼라. */
58
+ disabled?: boolean;
59
+ /** 밑줄을 평소엔 감추고 hover/focus 에서만 보인다(`hidden-underline`). */
60
+ hiddenUnderline?: boolean;
61
+ /** 밑줄 자체를 쓰지 않는다. 킷 샘플의 「밑줄 없음」 형태. */
62
+ noUnderline?: boolean;
63
+ };
64
+ export declare function LinkButton({ size, icon, iconDirection, iconPosition, linkStyle, hiddenUnderline, noUnderline, disabled, className, children, target, title, ...rest }: LinkButtonProps): import("react").JSX.Element;
65
+ /**
66
+ * 버튼 묶음 — 킷 `btn-group`. 기본 오른쪽 정렬이고 **제출 버튼이 가장 오른쪽**이다.
67
+ *
68
+ * @example
69
+ * <ButtonGroup>
70
+ * <Button variant="tertiary">취소</Button>
71
+ * <Button variant="primary" type="submit">제출</Button>
72
+ * </ButtonGroup>
73
+ */
74
+ export declare function ButtonGroup({ align, className, children, ...rest }: ComponentPropsWithRef<'div'> & {
75
+ align?: 'start' | 'center' | 'end' | 'between';
76
+ }): import("react").JSX.Element;
77
+ export {};
78
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/button/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC9D,OAAO,EAAM,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE/D;;;;;;;;;;;;;;;GAeG;AAEH,2CAA2C;AAC3C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnF,KAAK,SAAS,GAAG;IACf,4CAA4C;IAC5C,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,wBAAwB;IACxB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,qDAAqD;IACrD,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,sEAAsE;IACtE,aAAa,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACjD,+CAA+C;IAC/C,YAAY,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,GACzE,SAAS,GAAG;IACV,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB,CAAC;AAEJ,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,GAC7E,SAAS,GAAG;IACV,QAAQ,EAAE,IAAI,CAAC;IACf,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAEJ,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,eAAe,+BA8C1D;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC,GAExE,IAAI,CAAC,SAAS,EAAE,SAAS,GAAG,UAAU,CAAC,GAAG;IACxC,mDAAmD;IACnD,SAAS,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uCAAuC;IACvC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEJ,wBAAgB,UAAU,CAAC,EACzB,IAAe,EACf,IAAI,EACJ,aAAa,EACb,YAAoB,EACpB,SAAS,EACT,eAAe,EACf,WAAW,EACX,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,MAAM,EACN,KAAK,EACL,GAAG,IAAI,EACR,EAAE,eAAe,+BAwBjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAa,EACb,SAAS,EACT,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,qBAAqB,CAAC,KAAK,CAAC,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAA;CAAE,+BAcnF"}
@@ -0,0 +1,83 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../lib/krds.js";
3
+ function Button(props) {
4
+ const {
5
+ size = "medium",
6
+ variant,
7
+ icon,
8
+ iconDirection,
9
+ iconPosition = "end",
10
+ bordered,
11
+ className,
12
+ children,
13
+ type = "button",
14
+ ...rest
15
+ } = props;
16
+ const iconOnly = "iconOnly" in props && props.iconOnly === true;
17
+ const label = "label" in props ? props.label : void 0;
18
+ const cls = cx(
19
+ "krds-btn",
20
+ size,
21
+ variant,
22
+ iconOnly && "icon",
23
+ iconOnly && bordered && "border",
24
+ className
25
+ );
26
+ const iconNode = icon ? /* @__PURE__ */ jsx("i", { className: cx(`svg-icon ico-${icon}`, iconDirection), "aria-hidden": "true" }) : null;
27
+ return /* @__PURE__ */ jsx("button", { type, className: cls, ...rest, children: iconOnly ? /* @__PURE__ */ jsxs(Fragment, { children: [
28
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: label }),
29
+ iconNode
30
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
31
+ iconPosition === "start" && iconNode,
32
+ children,
33
+ iconPosition === "end" && iconNode
34
+ ] }) });
35
+ }
36
+ function LinkButton({
37
+ size = "medium",
38
+ icon,
39
+ iconDirection,
40
+ iconPosition = "end",
41
+ linkStyle,
42
+ hiddenUnderline,
43
+ noUnderline,
44
+ disabled,
45
+ className,
46
+ children,
47
+ target,
48
+ title,
49
+ ...rest
50
+ }) {
51
+ const cls = cx("krds-btn", size, "link", linkStyle, disabled && "disabled", className);
52
+ const iconNode = icon ? /* @__PURE__ */ jsx("i", { className: cx(`svg-icon ico-${icon}`, iconDirection), "aria-hidden": "true" }) : null;
53
+ return /* @__PURE__ */ jsxs(
54
+ "a",
55
+ {
56
+ className: cls,
57
+ "aria-disabled": disabled || void 0,
58
+ target,
59
+ title: title ?? (target === "_blank" ? "\uC0C8 \uCC3D \uC5F4\uB9BC" : void 0),
60
+ ...rest,
61
+ children: [
62
+ iconPosition === "start" && iconNode,
63
+ noUnderline ? /* @__PURE__ */ jsx("span", { children }) : /* @__PURE__ */ jsx("span", { className: cx("underline", hiddenUnderline && "hidden-underline"), children }),
64
+ iconPosition === "end" && iconNode
65
+ ]
66
+ }
67
+ );
68
+ }
69
+ function ButtonGroup({
70
+ align = "end",
71
+ className,
72
+ children,
73
+ ...rest
74
+ }) {
75
+ const justify = align === "start" ? "justify-start" : align === "center" ? "justify-center" : align === "between" ? "justify-between" : "justify-end";
76
+ return /* @__PURE__ */ jsx("div", { className: cx("btn-group flex flex-wrap items-center gap-2", justify, className), ...rest, children });
77
+ }
78
+ export {
79
+ Button,
80
+ ButtonGroup,
81
+ LinkButton
82
+ };
83
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/button/index.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { cx, type KrdsIcon, type KrdsSize } from '../lib/krds';\n\n/**\n * 버튼 — KRDS `krds-btn`.\n *\n * 이동은 `LinkButton`, 동작은 `Button` 을 쓴다(규격상 다른 것이다).\n * 같은 줄의 입력칸과 같은 `size` 를 준다(기본 medium · 48px).\n * 글자 없이 아이콘만 두려면 `iconOnly` 와 `label` 을 함께 준다.\n * 버튼 여럿은 `ButtonGroup` 으로 묶는다 — 제출은 가장 오른쪽이다.\n *\n * @example\n * <Button variant=\"primary\">저장</Button>\n * <Button size=\"small\" variant=\"secondary\" icon=\"down\">내려받기</Button>\n * <Button iconOnly icon=\"sch\" label=\"검색\" />\n * <LinkButton href=\"/notice\" icon=\"angle\" iconDirection=\"right\">공지 전체보기</LinkButton>\n *\n * 자세히: docs/krds/09-부품-노트.md#버튼\n */\n\n/** 위계. 한 화면에 최상위 강조(`primary`)는 하나만 둔다. */\nexport type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'text' | 'link';\n\ntype BaseProps = {\n /** 기본 medium(48px). 같은 줄의 입력칸과 같은 값을 준다. */\n size?: KrdsSize;\n /** 위계. 생략하면 킷 기본 버튼. */\n variant?: ButtonVariant;\n /** 킷 아이콘 이름(`ico-` 뺀 것). 예: `sch` · `down` · `go` */\n icon?: KrdsIcon;\n /** 아이콘 회전. 방향은 이름의 일부가 아니라 별도 클래스다 — `icon=\"angle right\"` 로 쓰지 마라. */\n iconDirection?: 'up' | 'down' | 'left' | 'right';\n /** 아이콘 위치. 기본 `end` — 킷 샘플은 xlarge 만 앞에 둔다. */\n iconPosition?: 'start' | 'end';\n /** 테두리 있는 아이콘 버튼(`krds-btn icon border`). iconOnly 일 때만 뜻이 있다. */\n bordered?: boolean;\n className?: string;\n children?: ReactNode;\n};\n\nexport type ButtonProps = Omit<ComponentPropsWithRef<'button'>, 'children'> &\n BaseProps & {\n /** 글자 없이 아이콘만 두려면 `true` 와 함께 `label`·`icon` 을 준다({@link IconButtonProps}). */\n iconOnly?: false;\n };\n\nexport type IconButtonProps = Omit<ComponentPropsWithRef<'button'>, 'children'> &\n BaseProps & {\n iconOnly: true;\n /** 스크린리더가 읽을 이름. 아이콘 버튼에는 반드시 있어야 한다. */\n label: string;\n icon: KrdsIcon;\n };\n\nexport function Button(props: ButtonProps | IconButtonProps) {\n const {\n size = 'medium',\n variant,\n icon,\n iconDirection,\n iconPosition = 'end',\n bordered,\n className,\n children,\n type = 'button',\n ...rest\n } = props as ButtonProps & Partial<IconButtonProps>;\n\n const iconOnly = 'iconOnly' in props && props.iconOnly === true;\n const label = 'label' in props ? props.label : undefined;\n\n const cls = cx(\n 'krds-btn',\n size,\n variant,\n iconOnly && 'icon',\n iconOnly && bordered && 'border',\n className,\n );\n\n const iconNode = icon ? (\n <i className={cx(`svg-icon ico-${icon}`, iconDirection)} aria-hidden=\"true\" />\n ) : null;\n\n return (\n <button type={type} className={cls} {...rest}>\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 );\n}\n\n/**\n * 링크 — KRDS `krds-btn link`. **이동에만 쓴다**(동작은 `Button`).\n *\n * 밑줄은 킷이 안쪽 `<span class=\"underline\">` 에 건다 — `hiddenUnderline` · `noUnderline` 로 끈다.\n * `target=\"_blank\"` 면 「새 창 열림」 안내가 `title` 로 붙는다.\n */\nexport type LinkButtonProps = Omit<ComponentPropsWithRef<'a'>, 'children'> &\n // `variant`·`bordered` 는 버튼 전용이라 뺀다 — 안 빼면 `<a variant=\"primary\">` 로 새어 나간다.\n Omit<BaseProps, 'variant' | 'bordered'> & {\n /** `basic` = 본문 글자색 링크 · `pure` = 가상클래스에서도 색 유지 */\n linkStyle?: 'basic' | 'pure';\n /** 비활성. 킷 클래스 `.disabled` + `aria-disabled` 로 알린다. 이동까지 막으려면 `href` 를 빼라. */\n disabled?: boolean;\n /** 밑줄을 평소엔 감추고 hover/focus 에서만 보인다(`hidden-underline`). */\n hiddenUnderline?: boolean;\n /** 밑줄 자체를 쓰지 않는다. 킷 샘플의 「밑줄 없음」 형태. */\n noUnderline?: boolean;\n };\n\nexport function LinkButton({\n size = 'medium',\n icon,\n iconDirection,\n iconPosition = 'end',\n linkStyle,\n hiddenUnderline,\n noUnderline,\n disabled,\n className,\n children,\n target,\n title,\n ...rest\n}: LinkButtonProps) {\n const cls = cx('krds-btn', size, 'link', linkStyle, disabled && 'disabled', className);\n const iconNode = icon ? (\n <i className={cx(`svg-icon ico-${icon}`, iconDirection)} aria-hidden=\"true\" />\n ) : null;\n\n return (\n <a\n className={cls}\n aria-disabled={disabled || undefined}\n target={target}\n // 새 창으로 열리면 미리 알린다 — KRDS 접근성 요구.\n title={title ?? (target === '_blank' ? '새 창 열림' : undefined)}\n {...rest}\n >\n {iconPosition === 'start' && iconNode}\n {noUnderline ? (\n <span>{children}</span>\n ) : (\n <span className={cx('underline', hiddenUnderline && 'hidden-underline')}>{children}</span>\n )}\n {iconPosition === 'end' && iconNode}\n </a>\n );\n}\n\n/**\n * 버튼 묶음 — 킷 `btn-group`. 기본 오른쪽 정렬이고 **제출 버튼이 가장 오른쪽**이다.\n *\n * @example\n * <ButtonGroup>\n * <Button variant=\"tertiary\">취소</Button>\n * <Button variant=\"primary\" type=\"submit\">제출</Button>\n * </ButtonGroup>\n */\nexport function ButtonGroup({\n align = 'end',\n className,\n children,\n ...rest\n}: ComponentPropsWithRef<'div'> & { align?: 'start' | 'center' | 'end' | 'between' }) {\n const justify =\n align === 'start'\n ? 'justify-start'\n : align === 'center'\n ? 'justify-center'\n : align === 'between'\n ? 'justify-between'\n : 'justify-end';\n return (\n <div className={cx('btn-group flex flex-wrap items-center gap-2', justify, className)} {...rest}>\n {children}\n </div>\n );\n}\n"],"mappings":"AAiFI,SAMI,UANJ,KAMI,YANJ;AAhFJ,SAAS,UAAwC;AAqD1C,SAAS,OAAO,OAAsC;AAC3D,QAAM;AAAA,IACJ,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP,GAAG;AAAA,EACL,IAAI;AAEJ,QAAM,WAAW,cAAc,SAAS,MAAM,aAAa;AAC3D,QAAM,QAAQ,WAAW,QAAQ,MAAM,QAAQ;AAE/C,QAAM,MAAM;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,YAAY,YAAY;AAAA,IACxB;AAAA,EACF;AAEA,QAAM,WAAW,OACf,oBAAC,OAAE,WAAW,GAAG,gBAAgB,IAAI,IAAI,aAAa,GAAG,eAAY,QAAO,IAC1E;AAEJ,SACE,oBAAC,YAAO,MAAY,WAAW,KAAM,GAAG,MACrC,qBACC,iCACE;AAAA,wBAAC,UAAK,WAAU,WAAW,iBAAM;AAAA,IAChC;AAAA,KACH,IAEA,iCACG;AAAA,qBAAiB,WAAW;AAAA,IAC5B;AAAA,IACA,iBAAiB,SAAS;AAAA,KAC7B,GAEJ;AAEJ;AAqBO,SAAS,WAAW;AAAA,EACzB,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,MAAM,GAAG,YAAY,MAAM,QAAQ,WAAW,YAAY,YAAY,SAAS;AACrF,QAAM,WAAW,OACf,oBAAC,OAAE,WAAW,GAAG,gBAAgB,IAAI,IAAI,aAAa,GAAG,eAAY,QAAO,IAC1E;AAEJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,MACX,iBAAe,YAAY;AAAA,MAC3B;AAAA,MAEA,OAAO,UAAU,WAAW,WAAW,+BAAW;AAAA,MACjD,GAAG;AAAA,MAEH;AAAA,yBAAiB,WAAW;AAAA,QAC5B,cACC,oBAAC,UAAM,UAAS,IAEhB,oBAAC,UAAK,WAAW,GAAG,aAAa,mBAAmB,kBAAkB,GAAI,UAAS;AAAA,QAEpF,iBAAiB,SAAS;AAAA;AAAA;AAAA,EAC7B;AAEJ;AAWO,SAAS,YAAY;AAAA,EAC1B,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsF;AACpF,QAAM,UACJ,UAAU,UACN,kBACA,UAAU,WACR,mBACA,UAAU,YACR,oBACA;AACV,SACE,oBAAC,SAAI,WAAW,GAAG,+CAA+C,SAAS,SAAS,GAAI,GAAG,MACxF,UACH;AAEJ;","names":[]}