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.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/accordion/index.d.ts +63 -0
- package/dist/accordion/index.d.ts.map +1 -0
- package/dist/accordion/index.js +145 -0
- package/dist/accordion/index.js.map +1 -0
- package/dist/alert/index.d.ts +44 -0
- package/dist/alert/index.d.ts.map +1 -0
- package/dist/alert/index.js +66 -0
- package/dist/alert/index.js.map +1 -0
- package/dist/avatar/index.d.ts +102 -0
- package/dist/avatar/index.d.ts.map +1 -0
- package/dist/avatar/index.js +146 -0
- package/dist/avatar/index.js.map +1 -0
- package/dist/badge/index.d.ts +49 -0
- package/dist/badge/index.d.ts.map +1 -0
- package/dist/badge/index.js +41 -0
- package/dist/badge/index.js.map +1 -0
- package/dist/breadcrumb/index.d.ts +43 -0
- package/dist/breadcrumb/index.d.ts.map +1 -0
- package/dist/breadcrumb/index.js +43 -0
- package/dist/breadcrumb/index.js.map +1 -0
- package/dist/button/index.d.ts +78 -0
- package/dist/button/index.d.ts.map +1 -0
- package/dist/button/index.js +83 -0
- package/dist/button/index.js.map +1 -0
- package/dist/checkbox/index.d.ts +97 -0
- package/dist/checkbox/index.d.ts.map +1 -0
- package/dist/checkbox/index.js +110 -0
- package/dist/checkbox/index.js.map +1 -0
- package/dist/combobox/index.d.ts +135 -0
- package/dist/combobox/index.d.ts.map +1 -0
- package/dist/combobox/index.js +416 -0
- package/dist/combobox/index.js.map +1 -0
- package/dist/context-menu/index.d.ts +94 -0
- package/dist/context-menu/index.d.ts.map +1 -0
- package/dist/context-menu/index.js +399 -0
- package/dist/context-menu/index.js.map +1 -0
- package/dist/critical-alert/index.d.ts +39 -0
- package/dist/critical-alert/index.d.ts.map +1 -0
- package/dist/critical-alert/index.js +50 -0
- package/dist/critical-alert/index.js.map +1 -0
- package/dist/data-table/index.d.ts +94 -0
- package/dist/data-table/index.d.ts.map +1 -0
- package/dist/data-table/index.js +128 -0
- package/dist/data-table/index.js.map +1 -0
- package/dist/disclosure/index.d.ts +35 -0
- package/dist/disclosure/index.d.ts.map +1 -0
- package/dist/disclosure/index.js +54 -0
- package/dist/disclosure/index.js.map +1 -0
- package/dist/drawer/index.d.ts +106 -0
- package/dist/drawer/index.d.ts.map +1 -0
- package/dist/drawer/index.js +352 -0
- package/dist/drawer/index.js.map +1 -0
- package/dist/empty-state/index.d.ts +86 -0
- package/dist/empty-state/index.d.ts.map +1 -0
- package/dist/empty-state/index.js +138 -0
- package/dist/empty-state/index.js.map +1 -0
- package/dist/env.d.js +1 -0
- package/dist/env.d.js.map +1 -0
- package/dist/field/index.d.ts +149 -0
- package/dist/field/index.d.ts.map +1 -0
- package/dist/field/index.js +121 -0
- package/dist/field/index.js.map +1 -0
- package/dist/file-upload/index.d.ts +140 -0
- package/dist/file-upload/index.d.ts.map +1 -0
- package/dist/file-upload/index.js +249 -0
- package/dist/file-upload/index.js.map +1 -0
- package/dist/footer/index.d.ts +155 -0
- package/dist/footer/index.d.ts.map +1 -0
- package/dist/footer/index.js +186 -0
- package/dist/footer/index.js.map +1 -0
- package/dist/header/index.d.ts +170 -0
- package/dist/header/index.d.ts.map +1 -0
- package/dist/header/index.js +258 -0
- package/dist/header/index.js.map +1 -0
- package/dist/identifier/index.d.ts +38 -0
- package/dist/identifier/index.d.ts.map +1 -0
- package/dist/identifier/index.js +55 -0
- package/dist/identifier/index.js.map +1 -0
- package/dist/in-page-navigation/index.d.ts +71 -0
- package/dist/in-page-navigation/index.d.ts.map +1 -0
- package/dist/in-page-navigation/index.js +137 -0
- package/dist/in-page-navigation/index.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/input-group/index.d.ts +66 -0
- package/dist/input-group/index.d.ts.map +1 -0
- package/dist/input-group/index.js +85 -0
- package/dist/input-group/index.js.map +1 -0
- package/dist/lib/krds.d.ts +45 -0
- package/dist/lib/krds.d.ts.map +1 -0
- package/dist/lib/krds.js +11 -0
- package/dist/lib/krds.js.map +1 -0
- package/dist/lib/link.d.ts +34 -0
- package/dist/lib/link.d.ts.map +1 -0
- package/dist/lib/link.js +8 -0
- package/dist/lib/link.js.map +1 -0
- package/dist/main-menu/index.d.ts +123 -0
- package/dist/main-menu/index.d.ts.map +1 -0
- package/dist/main-menu/index.js +613 -0
- package/dist/main-menu/index.js.map +1 -0
- package/dist/masthead/index.d.ts +25 -0
- package/dist/masthead/index.d.ts.map +1 -0
- package/dist/masthead/index.js +21 -0
- package/dist/masthead/index.js.map +1 -0
- package/dist/modal/index.d.ts +105 -0
- package/dist/modal/index.d.ts.map +1 -0
- package/dist/modal/index.js +285 -0
- package/dist/modal/index.js.map +1 -0
- package/dist/pagination/index.d.ts +58 -0
- package/dist/pagination/index.d.ts.map +1 -0
- package/dist/pagination/index.js +82 -0
- package/dist/pagination/index.js.map +1 -0
- package/dist/progress/index.d.ts +58 -0
- package/dist/progress/index.d.ts.map +1 -0
- package/dist/progress/index.js +125 -0
- package/dist/progress/index.js.map +1 -0
- package/dist/radio/index.d.ts +111 -0
- package/dist/radio/index.d.ts.map +1 -0
- package/dist/radio/index.js +136 -0
- package/dist/radio/index.js.map +1 -0
- package/dist/resize/index.d.ts +118 -0
- package/dist/resize/index.d.ts.map +1 -0
- package/dist/resize/index.js +342 -0
- package/dist/resize/index.js.map +1 -0
- package/dist/scroll-area/index.d.ts +40 -0
- package/dist/scroll-area/index.d.ts.map +1 -0
- package/dist/scroll-area/index.js +52 -0
- package/dist/scroll-area/index.js.map +1 -0
- package/dist/select/index.d.ts +67 -0
- package/dist/select/index.d.ts.map +1 -0
- package/dist/select/index.js +61 -0
- package/dist/select/index.js.map +1 -0
- package/dist/separator/index.d.ts +47 -0
- package/dist/separator/index.d.ts.map +1 -0
- package/dist/separator/index.js +61 -0
- package/dist/separator/index.js.map +1 -0
- package/dist/side-navigation/index.d.ts +78 -0
- package/dist/side-navigation/index.d.ts.map +1 -0
- package/dist/side-navigation/index.js +188 -0
- package/dist/side-navigation/index.js.map +1 -0
- package/dist/skeleton/index.d.ts +76 -0
- package/dist/skeleton/index.d.ts.map +1 -0
- package/dist/skeleton/index.js +78 -0
- package/dist/skeleton/index.js.map +1 -0
- package/dist/skip-link/index.d.ts +46 -0
- package/dist/skip-link/index.d.ts.map +1 -0
- package/dist/skip-link/index.js +37 -0
- package/dist/skip-link/index.js.map +1 -0
- package/dist/spinner/index.d.ts +58 -0
- package/dist/spinner/index.d.ts.map +1 -0
- package/dist/spinner/index.js +36 -0
- package/dist/spinner/index.js.map +1 -0
- package/dist/step-indicator/index.d.ts +41 -0
- package/dist/step-indicator/index.d.ts.map +1 -0
- package/dist/step-indicator/index.js +63 -0
- package/dist/step-indicator/index.js.map +1 -0
- package/dist/structured-list/index.d.ts +150 -0
- package/dist/structured-list/index.d.ts.map +1 -0
- package/dist/structured-list/index.js +194 -0
- package/dist/structured-list/index.js.map +1 -0
- package/dist/tab/index.d.ts +108 -0
- package/dist/tab/index.d.ts.map +1 -0
- package/dist/tab/index.js +198 -0
- package/dist/tab/index.js.map +1 -0
- package/dist/table/index.d.ts +75 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/table/index.js +91 -0
- package/dist/table/index.js.map +1 -0
- package/dist/tag/index.d.ts +71 -0
- package/dist/tag/index.d.ts.map +1 -0
- package/dist/tag/index.js +45 -0
- package/dist/tag/index.js.map +1 -0
- package/dist/text-input/index.d.ts +69 -0
- package/dist/text-input/index.d.ts.map +1 -0
- package/dist/text-input/index.js +70 -0
- package/dist/text-input/index.js.map +1 -0
- package/dist/text-list/index.d.ts +71 -0
- package/dist/text-list/index.d.ts.map +1 -0
- package/dist/text-list/index.js +79 -0
- package/dist/text-list/index.js.map +1 -0
- package/dist/textarea/index.d.ts +29 -0
- package/dist/textarea/index.d.ts.map +1 -0
- package/dist/textarea/index.js +69 -0
- package/dist/textarea/index.js.map +1 -0
- package/dist/toast/index.d.ts +76 -0
- package/dist/toast/index.d.ts.map +1 -0
- package/dist/toast/index.js +183 -0
- package/dist/toast/index.js.map +1 -0
- package/dist/tooltip/index.d.ts +84 -0
- package/dist/tooltip/index.d.ts.map +1 -0
- package/dist/tooltip/index.js +329 -0
- package/dist/tooltip/index.js.map +1 -0
- package/package.json +57 -0
- package/src/accordion/index.tsx +251 -0
- package/src/alert/index.tsx +111 -0
- package/src/avatar/index.tsx +272 -0
- package/src/badge/index.tsx +91 -0
- package/src/breadcrumb/index.tsx +125 -0
- package/src/button/index.tsx +190 -0
- package/src/checkbox/index.tsx +247 -0
- package/src/combobox/index.tsx +700 -0
- package/src/context-menu/index.tsx +623 -0
- package/src/critical-alert/index.tsx +102 -0
- package/src/data-table/index.tsx +289 -0
- package/src/disclosure/index.tsx +87 -0
- package/src/drawer/index.tsx +516 -0
- package/src/empty-state/index.tsx +256 -0
- package/src/env.d.ts +10 -0
- package/src/field/index.tsx +307 -0
- package/src/file-upload/index.tsx +492 -0
- package/src/footer/index.tsx +393 -0
- package/src/header/index.tsx +498 -0
- package/src/identifier/index.tsx +96 -0
- package/src/in-page-navigation/index.tsx +258 -0
- package/src/index.ts +72 -0
- package/src/input-group/index.tsx +197 -0
- package/src/lib/krds.ts +75 -0
- package/src/lib/link.tsx +35 -0
- package/src/main-menu/index.tsx +912 -0
- package/src/masthead/index.tsx +50 -0
- package/src/modal/index.tsx +457 -0
- package/src/pagination/index.tsx +198 -0
- package/src/progress/index.tsx +198 -0
- package/src/radio/index.tsx +283 -0
- package/src/resize/index.tsx +541 -0
- package/src/scroll-area/index.tsx +96 -0
- package/src/select/index.tsx +136 -0
- package/src/separator/index.tsx +107 -0
- package/src/side-navigation/index.tsx +336 -0
- package/src/skeleton/index.tsx +172 -0
- package/src/skip-link/index.tsx +89 -0
- package/src/spinner/index.tsx +98 -0
- package/src/step-indicator/index.tsx +122 -0
- package/src/structured-list/index.tsx +395 -0
- package/src/tab/index.tsx +348 -0
- package/src/table/index.tsx +186 -0
- package/src/tag/index.tsx +127 -0
- package/src/text-input/index.tsx +156 -0
- package/src/text-list/index.tsx +177 -0
- package/src/textarea/index.tsx +113 -0
- package/src/toast/index.tsx +311 -0
- package/src/tooltip/index.tsx +496 -0
- package/styles/krds-tailwind.css +335 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useId,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState
|
|
11
|
+
} from "react";
|
|
12
|
+
import { createPortal } from "react-dom";
|
|
13
|
+
import { cx } from "../lib/krds.js";
|
|
14
|
+
const WIDTH = {
|
|
15
|
+
small: "40rem",
|
|
16
|
+
// 400px — 킷 --krds-modal--size-small
|
|
17
|
+
medium: "56rem",
|
|
18
|
+
// 560px — 킷 --krds-modal--size-medium
|
|
19
|
+
large: "76rem"
|
|
20
|
+
// 760px — 킷 --krds-modal--size-large
|
|
21
|
+
};
|
|
22
|
+
const TRANSITION_MS = 400;
|
|
23
|
+
const Z_BASE = 1010;
|
|
24
|
+
const FOCUSABLE_SELECTOR = [
|
|
25
|
+
"a[href]",
|
|
26
|
+
"area[href]",
|
|
27
|
+
"button:not([disabled])",
|
|
28
|
+
'input:not([disabled]):not([type="hidden"])',
|
|
29
|
+
"select:not([disabled])",
|
|
30
|
+
"textarea:not([disabled])",
|
|
31
|
+
"details > summary",
|
|
32
|
+
"audio[controls]",
|
|
33
|
+
"video[controls]",
|
|
34
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
35
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
36
|
+
].join(",");
|
|
37
|
+
function focusables(root) {
|
|
38
|
+
return Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
|
|
39
|
+
(el) => !el.hasAttribute("inert") && (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
let openDrawers = 0;
|
|
43
|
+
let scrollLocks = 0;
|
|
44
|
+
let savedOverflow = "";
|
|
45
|
+
function lockScroll() {
|
|
46
|
+
if (scrollLocks++ > 0) return;
|
|
47
|
+
savedOverflow = document.body.style.overflow;
|
|
48
|
+
document.body.style.overflow = "hidden";
|
|
49
|
+
}
|
|
50
|
+
function unlockScroll() {
|
|
51
|
+
scrollLocks = Math.max(0, scrollLocks - 1);
|
|
52
|
+
if (scrollLocks > 0) return;
|
|
53
|
+
document.body.style.overflow = savedOverflow;
|
|
54
|
+
}
|
|
55
|
+
const DrawerContext = createContext(null);
|
|
56
|
+
function useDrawerContext(name) {
|
|
57
|
+
const ctx = useContext(DrawerContext);
|
|
58
|
+
if (!ctx) throw new Error(`<${name}> \uC740 <Drawer.Root> \uC548\uC5D0\uC11C\uB9CC \uC4F8 \uC218 \uC788\uB2E4.`);
|
|
59
|
+
return ctx;
|
|
60
|
+
}
|
|
61
|
+
function DrawerRoot({
|
|
62
|
+
open,
|
|
63
|
+
onClose,
|
|
64
|
+
side = "right",
|
|
65
|
+
size = "small",
|
|
66
|
+
modal = true,
|
|
67
|
+
dismissible = true,
|
|
68
|
+
closeOnBackdrop = true,
|
|
69
|
+
initialFocus = "panel",
|
|
70
|
+
container,
|
|
71
|
+
panelClassName,
|
|
72
|
+
panelStyle,
|
|
73
|
+
className,
|
|
74
|
+
style,
|
|
75
|
+
children,
|
|
76
|
+
onKeyDown,
|
|
77
|
+
onMouseDown,
|
|
78
|
+
...rest
|
|
79
|
+
}) {
|
|
80
|
+
const reactId = useId();
|
|
81
|
+
const titleId = `${reactId}-title`;
|
|
82
|
+
const panelRef = useRef(null);
|
|
83
|
+
const [mounted, setMounted] = useState(false);
|
|
84
|
+
const [present, setPresent] = useState(false);
|
|
85
|
+
const [entered, setEntered] = useState(false);
|
|
86
|
+
const [hasTitle, setHasTitle] = useState(false);
|
|
87
|
+
const [stack, setStack] = useState(1);
|
|
88
|
+
const initialFocusRef = useRef(initialFocus);
|
|
89
|
+
initialFocusRef.current = initialFocus;
|
|
90
|
+
const modalRef = useRef(modal);
|
|
91
|
+
modalRef.current = modal;
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
setMounted(true);
|
|
94
|
+
}, []);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!open) return;
|
|
97
|
+
const restore = document.activeElement;
|
|
98
|
+
openDrawers += 1;
|
|
99
|
+
setStack(openDrawers);
|
|
100
|
+
setPresent(true);
|
|
101
|
+
const didLock = modalRef.current;
|
|
102
|
+
if (didLock) lockScroll();
|
|
103
|
+
let inner = 0;
|
|
104
|
+
const outer = requestAnimationFrame(() => {
|
|
105
|
+
inner = requestAnimationFrame(() => setEntered(true));
|
|
106
|
+
});
|
|
107
|
+
const focusTimer = window.setTimeout(() => {
|
|
108
|
+
const panel = panelRef.current;
|
|
109
|
+
if (!panel) return;
|
|
110
|
+
if (initialFocusRef.current === "first") {
|
|
111
|
+
const first = focusables(panel)[0];
|
|
112
|
+
if (first) {
|
|
113
|
+
first.focus();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
panel.focus();
|
|
118
|
+
}, 0);
|
|
119
|
+
return () => {
|
|
120
|
+
cancelAnimationFrame(outer);
|
|
121
|
+
cancelAnimationFrame(inner);
|
|
122
|
+
window.clearTimeout(focusTimer);
|
|
123
|
+
openDrawers = Math.max(0, openDrawers - 1);
|
|
124
|
+
setEntered(false);
|
|
125
|
+
if (didLock) unlockScroll();
|
|
126
|
+
restore?.focus?.();
|
|
127
|
+
};
|
|
128
|
+
}, [open]);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (open || !present) return;
|
|
131
|
+
const timer = window.setTimeout(() => setPresent(false), TRANSITION_MS);
|
|
132
|
+
return () => window.clearTimeout(timer);
|
|
133
|
+
}, [open, present]);
|
|
134
|
+
const ctx = useMemo(
|
|
135
|
+
() => ({ titleId, close: onClose, registerTitle: setHasTitle }),
|
|
136
|
+
[titleId, onClose]
|
|
137
|
+
);
|
|
138
|
+
function handleKeyDown(event) {
|
|
139
|
+
onKeyDown?.(event);
|
|
140
|
+
if (event.defaultPrevented) return;
|
|
141
|
+
if (event.key === "Escape" || event.key === "Esc") {
|
|
142
|
+
if (!dismissible) return;
|
|
143
|
+
event.stopPropagation();
|
|
144
|
+
onClose();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (!modal) return;
|
|
148
|
+
if (event.key !== "Tab") return;
|
|
149
|
+
const panel = panelRef.current;
|
|
150
|
+
if (!panel) return;
|
|
151
|
+
const nodes = focusables(panel);
|
|
152
|
+
if (nodes.length === 0) {
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
panel.focus();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const first = nodes[0];
|
|
158
|
+
const last = nodes[nodes.length - 1];
|
|
159
|
+
const active = document.activeElement;
|
|
160
|
+
if (event.shiftKey && (active === first || active === panel)) {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
last.focus();
|
|
163
|
+
} else if (!event.shiftKey && active === last) {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
first.focus();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function handleMouseDown(event) {
|
|
169
|
+
onMouseDown?.(event);
|
|
170
|
+
if (event.defaultPrevented) return;
|
|
171
|
+
if (!modal) return;
|
|
172
|
+
const target = event.target;
|
|
173
|
+
if (target && panelRef.current?.contains(target)) return;
|
|
174
|
+
if (closeOnBackdrop && dismissible) {
|
|
175
|
+
onClose();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
panelRef.current?.focus();
|
|
179
|
+
}
|
|
180
|
+
if (!mounted || !present) return null;
|
|
181
|
+
const slideOut = side === "right" ? "translate-x-full" : "-translate-x-full";
|
|
182
|
+
return createPortal(
|
|
183
|
+
/* @__PURE__ */ jsx(DrawerContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxs(
|
|
184
|
+
"div",
|
|
185
|
+
{
|
|
186
|
+
role: "dialog",
|
|
187
|
+
"aria-modal": modal ? true : void 0,
|
|
188
|
+
"aria-labelledby": hasTitle ? titleId : void 0,
|
|
189
|
+
className: cx("fixed inset-0", !modal && "pointer-events-none", className),
|
|
190
|
+
style: { zIndex: Z_BASE + stack, ...style },
|
|
191
|
+
onKeyDown: handleKeyDown,
|
|
192
|
+
onMouseDown: handleMouseDown,
|
|
193
|
+
...rest,
|
|
194
|
+
children: [
|
|
195
|
+
modal && /* @__PURE__ */ jsx(
|
|
196
|
+
"div",
|
|
197
|
+
{
|
|
198
|
+
"aria-hidden": "true",
|
|
199
|
+
className: cx(
|
|
200
|
+
"absolute inset-0 bg-bg-dim transition-opacity duration-400 ease-in-out motion-reduce:transition-none",
|
|
201
|
+
entered ? "opacity-100" : "opacity-0"
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
),
|
|
205
|
+
/* @__PURE__ */ jsx(
|
|
206
|
+
"div",
|
|
207
|
+
{
|
|
208
|
+
ref: panelRef,
|
|
209
|
+
tabIndex: -1,
|
|
210
|
+
className: cx(
|
|
211
|
+
"pointer-events-auto absolute inset-y-0 flex h-full max-w-full flex-col bg-surface text-fg",
|
|
212
|
+
"transition-transform duration-400 ease-in-out motion-reduce:transition-none",
|
|
213
|
+
"focus:[box-shadow:var(--krds-box-shadow-outline-inset)] focus:outline-none",
|
|
214
|
+
side === "right" ? "right-0 border-l border-line" : "left-0 border-r border-line",
|
|
215
|
+
!entered && slideOut,
|
|
216
|
+
panelClassName
|
|
217
|
+
),
|
|
218
|
+
style: { width: WIDTH[size], ...panelStyle },
|
|
219
|
+
children
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
) }),
|
|
225
|
+
container ?? document.body
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
function DrawerHeader({
|
|
229
|
+
level = 2,
|
|
230
|
+
hideTitle,
|
|
231
|
+
titleClassName,
|
|
232
|
+
extra,
|
|
233
|
+
className,
|
|
234
|
+
children,
|
|
235
|
+
...rest
|
|
236
|
+
}) {
|
|
237
|
+
const { titleId, registerTitle } = useDrawerContext("Drawer.Header");
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
registerTitle(true);
|
|
240
|
+
return () => registerTitle(false);
|
|
241
|
+
}, [registerTitle]);
|
|
242
|
+
const Heading = `h${level}`;
|
|
243
|
+
return /* @__PURE__ */ jsxs(
|
|
244
|
+
"div",
|
|
245
|
+
{
|
|
246
|
+
className: cx(
|
|
247
|
+
"flex shrink-0 items-center justify-between gap-4 border-b border-line px-4 py-5 md:px-10",
|
|
248
|
+
className
|
|
249
|
+
),
|
|
250
|
+
...rest,
|
|
251
|
+
children: [
|
|
252
|
+
/* @__PURE__ */ jsx(
|
|
253
|
+
Heading,
|
|
254
|
+
{
|
|
255
|
+
id: titleId,
|
|
256
|
+
className: cx(
|
|
257
|
+
hideTitle ? "sr-only" : "truncate text-heading-sm font-bold text-fg-bolder",
|
|
258
|
+
titleClassName
|
|
259
|
+
),
|
|
260
|
+
children
|
|
261
|
+
}
|
|
262
|
+
),
|
|
263
|
+
extra
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
function DrawerBody({ className, children, ref, ...rest }) {
|
|
269
|
+
const innerRef = useRef(null);
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
const node = innerRef.current;
|
|
272
|
+
if (!node) return;
|
|
273
|
+
const sync = () => {
|
|
274
|
+
if (node.scrollHeight > node.clientHeight) node.setAttribute("tabindex", "0");
|
|
275
|
+
else node.removeAttribute("tabindex");
|
|
276
|
+
};
|
|
277
|
+
sync();
|
|
278
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
279
|
+
const observer = new ResizeObserver(sync);
|
|
280
|
+
observer.observe(node);
|
|
281
|
+
for (const child of Array.from(node.children)) observer.observe(child);
|
|
282
|
+
return () => observer.disconnect();
|
|
283
|
+
}, []);
|
|
284
|
+
return /* @__PURE__ */ jsx(
|
|
285
|
+
"div",
|
|
286
|
+
{
|
|
287
|
+
className: cx("min-h-0 flex-1 overflow-y-auto px-4 py-6 md:px-10", className),
|
|
288
|
+
ref: (node) => {
|
|
289
|
+
innerRef.current = node;
|
|
290
|
+
if (typeof ref === "function") ref(node);
|
|
291
|
+
else if (ref) ref.current = node;
|
|
292
|
+
},
|
|
293
|
+
...rest,
|
|
294
|
+
children
|
|
295
|
+
}
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
function DrawerFooter({ split, className, children, ...rest }) {
|
|
299
|
+
return /* @__PURE__ */ jsx(
|
|
300
|
+
"div",
|
|
301
|
+
{
|
|
302
|
+
className: cx(
|
|
303
|
+
"btn-wrap flex shrink-0 flex-wrap items-center gap-2 border-t border-line px-4 py-5 md:px-10",
|
|
304
|
+
split ? "justify-between" : "justify-end",
|
|
305
|
+
className
|
|
306
|
+
),
|
|
307
|
+
...rest,
|
|
308
|
+
children
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
function DrawerClose({
|
|
313
|
+
label = "\uB2EB\uAE30",
|
|
314
|
+
className,
|
|
315
|
+
onClick,
|
|
316
|
+
type = "button",
|
|
317
|
+
...rest
|
|
318
|
+
}) {
|
|
319
|
+
const { close } = useDrawerContext("Drawer.Close");
|
|
320
|
+
return /* @__PURE__ */ jsxs(
|
|
321
|
+
"button",
|
|
322
|
+
{
|
|
323
|
+
type,
|
|
324
|
+
className: cx("krds-btn small icon shrink-0", className),
|
|
325
|
+
onClick: (event) => {
|
|
326
|
+
onClick?.(event);
|
|
327
|
+
if (!event.defaultPrevented) close();
|
|
328
|
+
},
|
|
329
|
+
...rest,
|
|
330
|
+
children: [
|
|
331
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: label }),
|
|
332
|
+
/* @__PURE__ */ jsx("i", { className: "svg-icon ico-popup-close", "aria-hidden": "true" })
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const Drawer = {
|
|
338
|
+
Root: DrawerRoot,
|
|
339
|
+
Header: DrawerHeader,
|
|
340
|
+
Body: DrawerBody,
|
|
341
|
+
Footer: DrawerFooter,
|
|
342
|
+
Close: DrawerClose
|
|
343
|
+
};
|
|
344
|
+
export {
|
|
345
|
+
Drawer,
|
|
346
|
+
DrawerBody,
|
|
347
|
+
DrawerClose,
|
|
348
|
+
DrawerFooter,
|
|
349
|
+
DrawerHeader,
|
|
350
|
+
DrawerRoot
|
|
351
|
+
};
|
|
352
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/drawer/index.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type CSSProperties,\n type KeyboardEvent as ReactKeyboardEvent,\n type MouseEvent as ReactMouseEvent,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { cx } from '../lib/krds';\n\n/**\n * 서랍(측면 패널) — KRDS 에 없다. 모양은 KRDS 토큰, 동작은 `Modal` 과 같은 보충 부품이다.\n *\n * 목록 옆에서 필터·상세를 여는 자리. 폭은 `size` 3단에서 고른다(픽셀을 넘기지 마라).\n * 확정·취소는 `Drawer.Footer` 에 두고 제출이 가장 오른쪽이다.\n * 뒤 화면을 계속 쓸 수 있어야 하면 `modal={false}` — 이때는 초점을 가두지 않는다.\n * 화면 한복판에 띄우는 대화상자는 `ui/modal` 이다.\n *\n * @example\n * <Drawer.Root open={open} onClose={close} side=\"right\" size=\"small\">\n * <Drawer.Header extra={<Drawer.Close />}>검색 조건</Drawer.Header>\n * <Drawer.Body>…필터 폼…</Drawer.Body>\n * <Drawer.Footer split>\n * <Button variant=\"text\" onClick={reset}>초기화</Button>\n * <Button variant=\"primary\" onClick={apply}>적용</Button>\n * </Drawer.Footer>\n * </Drawer.Root>\n *\n * 자세히: docs/krds/09-부품-노트.md#서랍\n */\n\n/** 어느 쪽에서 나오는가. 아래에서 올라오는 것은 킷의 바텀시트(`ui/modal` 의 `type`) 자리다. */\nexport type DrawerSide = 'left' | 'right';\n\n/** 폭 3단 — 400 / 560 / 760px. 킷 모달과 같은 눈금이다. */\nexport type DrawerSize = 'small' | 'medium' | 'large';\n\nconst WIDTH: Record<DrawerSize, string> = {\n small: '40rem', // 400px — 킷 --krds-modal--size-small\n medium: '56rem', // 560px — 킷 --krds-modal--size-medium\n large: '76rem', // 760px — 킷 --krds-modal--size-large\n};\n\n/** 전환 시간(ms). 킷 `--krds-transition-base` 와 같은 값이고 `duration-400` 과 짝이다. */\nconst TRANSITION_MS = 400;\n\n/** 겹침 층. 킷 모달과 같은 값이라 서랍과 모달이 함께 열려도 순서가 뒤집히지 않는다. */\nconst Z_BASE = 1010;\n\n/** 초점 가둠 대상. `Modal` 과 같은 목록이다. */\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n 'details > summary',\n 'audio[controls]',\n 'video[controls]',\n '[contenteditable]:not([contenteditable=\"false\"])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nfunction focusables(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(\n (el) =>\n !el.hasAttribute('inert') &&\n (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0),\n );\n}\n\n/** 열려 있는 서랍 수. 겹쳐 열릴 때 z-index 를 킷 모달과 같은 방식으로 올린다. */\nlet openDrawers = 0;\n\n/**\n * 뒤 문서 스크롤 잠금. 킷의 `body.scroll-no` 는 CSS 규칙이 없어 `body.style.overflow` 를 직접 다룬다.\n * 셈은 이 모듈 안에서만 유효하다 — 모달과 겹쳐 열리면 어긋날 수 있다.\n */\nlet scrollLocks = 0;\nlet savedOverflow = '';\n\nfunction lockScroll() {\n if (scrollLocks++ > 0) return;\n savedOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n}\n\nfunction unlockScroll() {\n scrollLocks = Math.max(0, scrollLocks - 1);\n if (scrollLocks > 0) return;\n document.body.style.overflow = savedOverflow;\n}\n\ntype DrawerContextValue = {\n /** 제목 요소의 id. `Drawer.Header` 가 붙이고 `Drawer.Root` 가 `aria-labelledby` 로 가리킨다. */\n titleId: string;\n close: () => void;\n /** 제목이 실제로 있는지 알린다 — 없으면 `aria-labelledby` 가 허공을 가리킨다. */\n registerTitle: (has: boolean) => void;\n};\n\nconst DrawerContext = createContext<DrawerContextValue | null>(null);\n\nfunction useDrawerContext(name: string): DrawerContextValue {\n const ctx = useContext(DrawerContext);\n if (!ctx) throw new Error(`<${name}> 은 <Drawer.Root> 안에서만 쓸 수 있다.`);\n return ctx;\n}\n\nexport type DrawerRootProps = ComponentPropsWithRef<'div'> & {\n /** 열림 여부. 상태는 화면 쪽이 쥔다. */\n open: boolean;\n /** 닫아 달라는 요청 — Esc·딤 클릭·`Drawer.Close` 에서 온다. */\n onClose: () => void;\n /** 나오는 방향. 기본 오른쪽. */\n side?: DrawerSide;\n /** 기본 small(400px). 3단에서 고른다. */\n size?: DrawerSize;\n /** 딤을 깔고 초점을 가두는가. 기본 true — false 면 뒤 화면을 계속 쓸 수 있다. */\n modal?: boolean;\n /** Esc 로 닫을 수 있는가. */\n dismissible?: boolean;\n /** 딤을 눌렀을 때 닫는가. 기본 true — 값을 잃으면 곤란한 서랍에서는 꺼라. */\n closeOnBackdrop?: boolean;\n /** 열릴 때 첫 초점. 기본 `panel`(보조기술이 이름부터 읽는다) · `first` 는 첫 초점 요소로 보낸다. */\n initialFocus?: 'panel' | 'first';\n /** 포털 대상. 기본 `document.body`. */\n container?: Element | null;\n /** 패널(흰 면)에 붙일 클래스. 바깥틀은 `className` 이 받는다. */\n panelClassName?: string;\n /** 패널에 붙일 인라인 style. 폭을 3단 밖으로 바꿔야 할 때. */\n panelStyle?: CSSProperties;\n children?: ReactNode;\n};\n\n/**\n * 서랍 바깥틀. 포털 · 초점 가둠 · Esc · 스크롤 잠금 · 초점 복귀를 여기서 한다.\n *\n * `Drawer.Header` 를 두면 `aria-labelledby` 가 자동으로 붙는다. 헤더 없이 쓸 거면\n * `aria-label` 을 직접 넘겨라 — 대화상자에는 반드시 이름이 있어야 한다.\n */\nexport function DrawerRoot({\n open,\n onClose,\n side = 'right',\n size = 'small',\n modal = true,\n dismissible = true,\n closeOnBackdrop = true,\n initialFocus = 'panel',\n container,\n panelClassName,\n panelStyle,\n className,\n style,\n children,\n onKeyDown,\n onMouseDown,\n ...rest\n}: DrawerRootProps) {\n const reactId = useId();\n const titleId = `${reactId}-title`;\n const panelRef = useRef<HTMLDivElement | null>(null);\n const [mounted, setMounted] = useState(false);\n /** DOM 에 붙어 있는가. 닫힌 뒤에도 미끄러져 나가는 동안 유지한다. */\n const [present, setPresent] = useState(false);\n /** 다 들어왔는가(= 딤이 보이고 패널이 제자리). 첫 그림 뒤에 켜야 전환이 돈다. */\n const [entered, setEntered] = useState(false);\n const [hasTitle, setHasTitle] = useState(false);\n /** 몇 번째로 열린 서랍인가(1부터). */\n const [stack, setStack] = useState(1);\n\n // 초점 정책이 도중에 바뀌어도 열림 효과가 다시 돌지 않게 ref 로 읽는다.\n const initialFocusRef = useRef(initialFocus);\n initialFocusRef.current = initialFocus;\n const modalRef = useRef(modal);\n modalRef.current = modal;\n\n // 서버에서는 포털을 만들지 않는다(hydration 어긋남 방지).\n useEffect(() => {\n setMounted(true);\n }, []);\n\n // 열림 — 스크롤 잠금 · 상태 · 초점 이동. 정리 단계에서 잠금 해제와 초점 복귀.\n useEffect(() => {\n if (!open) return;\n const restore = document.activeElement as HTMLElement | null;\n openDrawers += 1;\n setStack(openDrawers);\n setPresent(true);\n // 잠갔는지를 지역 변수로 들고 있는다 — 열려 있는 동안 `modal` 이 바뀌어도\n // 잠금과 해제가 짝을 이루게 한다(ref 를 정리 단계에서 다시 읽으면 한쪽만 돌 수 있다).\n const didLock = modalRef.current;\n if (didLock) lockScroll();\n\n // 프레임을 두 번 넘겨야 브라우저가 「밖 → 안」을 전환으로 본다.\n let inner = 0;\n const outer = requestAnimationFrame(() => {\n inner = requestAnimationFrame(() => setEntered(true));\n });\n\n // 서랍은 visibility 를 쓰지 않아 전환을 기다리지 않고 바로 초점을 줄 수 있다.\n const focusTimer = window.setTimeout(() => {\n const panel = panelRef.current;\n if (!panel) return;\n if (initialFocusRef.current === 'first') {\n const first = focusables(panel)[0];\n if (first) {\n first.focus();\n return;\n }\n }\n panel.focus();\n }, 0);\n\n return () => {\n cancelAnimationFrame(outer);\n cancelAnimationFrame(inner);\n window.clearTimeout(focusTimer);\n openDrawers = Math.max(0, openDrawers - 1);\n setEntered(false);\n if (didLock) unlockScroll();\n // 서랍을 열었던 요소로 초점 복귀 — 킷 도움 패널의 `lastFocusedButton.focus()` 에 해당한다.\n restore?.focus?.();\n };\n }, [open]);\n\n // 닫힘 — 전환이 끝난 뒤에 DOM 에서 뗀다.\n useEffect(() => {\n if (open || !present) return;\n const timer = window.setTimeout(() => setPresent(false), TRANSITION_MS);\n return () => window.clearTimeout(timer);\n }, [open, present]);\n\n const ctx = useMemo<DrawerContextValue>(\n () => ({ titleId, close: onClose, registerTitle: setHasTitle }),\n [titleId, onClose],\n );\n\n function handleKeyDown(event: ReactKeyboardEvent<HTMLDivElement>) {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n\n if (event.key === 'Escape' || event.key === 'Esc') {\n if (!dismissible) return;\n // 겹쳐 열린 경우 바깥 서랍·모달까지 닫히지 않도록 여기서 끊는다.\n event.stopPropagation();\n onClose();\n return;\n }\n\n // 딤 없는 서랍은 가두지 않는다 — 뒤 화면을 계속 쓸 수 있어야 하므로 Tab 이 나가야 한다.\n if (!modal) return;\n if (event.key !== 'Tab') return;\n const panel = panelRef.current;\n if (!panel) return;\n const nodes = focusables(panel);\n if (nodes.length === 0) {\n event.preventDefault();\n panel.focus();\n return;\n }\n const first = nodes[0];\n const last = nodes[nodes.length - 1];\n const active = document.activeElement;\n if (event.shiftKey && (active === first || active === panel)) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first.focus();\n }\n }\n\n function handleMouseDown(event: ReactMouseEvent<HTMLDivElement>) {\n onMouseDown?.(event);\n if (event.defaultPrevented) return;\n if (!modal) return;\n // 패널 안을 누른 것은 딤 클릭이 아니다. `mousedown` 으로 보는 이유는 킷 모달과 같다 —\n // 패널 안에서 시작해 딤에서 끝나는 드래그를 「바깥 클릭」으로 오해하지 않기 위해서다.\n const target = event.target as Node | null;\n if (target && panelRef.current?.contains(target)) return;\n if (closeOnBackdrop && dismissible) {\n onClose();\n return;\n }\n // 닫지 않기로 했으면 초점만 되돌린다 — 킷 모달의 바깥 클릭 동작이 이것이다.\n panelRef.current?.focus();\n }\n\n if (!mounted || !present) return null;\n\n const slideOut = side === 'right' ? 'translate-x-full' : '-translate-x-full';\n\n return createPortal(\n <DrawerContext.Provider value={ctx}>\n <div\n role=\"dialog\"\n aria-modal={modal ? true : undefined}\n aria-labelledby={hasTitle ? titleId : undefined}\n // 딤이 없을 때는 바깥틀이 화면을 덮은 채 클릭을 먹지 않게 한다(뒤 화면을 계속 쓴다).\n className={cx('fixed inset-0', !modal && 'pointer-events-none', className)}\n style={{ zIndex: Z_BASE + stack, ...style }}\n onKeyDown={handleKeyDown}\n onMouseDown={handleMouseDown}\n {...rest}\n >\n {modal && (\n <div\n aria-hidden=\"true\"\n className={cx(\n 'absolute inset-0 bg-bg-dim transition-opacity duration-400 ease-in-out motion-reduce:transition-none',\n entered ? 'opacity-100' : 'opacity-0',\n )}\n />\n )}\n {/*\n 패널. tabIndex -1 로 초점만 받고 Tab 순서에는 들어가지 않는다.\n `:focus` 테두리는 킷 모달(`.modal-content:focus`)과 같은 토큰\n `--krds-box-shadow-outline-inset` 을 그대로 쓴다. 표기는 `ui/radio` 와 맞춘 임의 속성 형태다.\n (선명한 화면 모드에서 이 토큰 색이 안 바뀌는 것은 킷 결함이다 —\n `[data-krds-mode=high-contrast] :root` 는 :root 가 자기 후손일 수 없어 맞지 않는다.\n 킷 컴포넌트 전부가 같은 상태라 여기서만 따로 고치지 않는다. `ui/resize` 주석 참조.)\n */}\n <div\n ref={panelRef}\n tabIndex={-1}\n className={cx(\n 'pointer-events-auto absolute inset-y-0 flex h-full max-w-full flex-col bg-surface text-fg',\n 'transition-transform duration-400 ease-in-out motion-reduce:transition-none',\n 'focus:[box-shadow:var(--krds-box-shadow-outline-inset)] focus:outline-none',\n side === 'right' ? 'right-0 border-l border-line' : 'left-0 border-r border-line',\n !entered && slideOut,\n panelClassName,\n )}\n style={{ width: WIDTH[size], ...panelStyle }}\n >\n {children}\n </div>\n </div>\n </DrawerContext.Provider>,\n container ?? document.body,\n );\n}\n\nexport type DrawerHeaderProps = ComponentPropsWithRef<'div'> & {\n /** 제목 태그 단계. 기본 h2 — 화면의 제목 위계에 맞춘다. */\n level?: 1 | 2 | 3 | 4 | 5 | 6;\n /** 제목을 눈에서만 감춘다. 이름 자체는 남는다. */\n hideTitle?: boolean;\n /** 제목 요소에 붙일 클래스. */\n titleClassName?: string;\n /** 제목 오른쪽 자리. 닫기 버튼(`Drawer.Close`)을 여기에 둔다. */\n extra?: ReactNode;\n};\n\n/**\n * 제목 줄. 이 부품을 두면 서랍의 접근 가능한 이름이 자동으로 이어진다.\n *\n * @example\n * <Drawer.Header extra={<Drawer.Close />}>검색 조건</Drawer.Header>\n */\nexport function DrawerHeader({\n level = 2,\n hideTitle,\n titleClassName,\n extra,\n className,\n children,\n ...rest\n}: DrawerHeaderProps) {\n const { titleId, registerTitle } = useDrawerContext('Drawer.Header');\n\n useEffect(() => {\n registerTitle(true);\n return () => registerTitle(false);\n }, [registerTitle]);\n\n const Heading = `h${level}` as 'h2';\n\n return (\n <div\n className={cx(\n 'flex shrink-0 items-center justify-between gap-4 border-b border-line px-4 py-5 md:px-10',\n className,\n )}\n {...rest}\n >\n <Heading\n id={titleId}\n className={cx(\n hideTitle ? 'sr-only' : 'truncate text-heading-sm font-bold text-fg-bolder',\n titleClassName,\n )}\n >\n {children}\n </Heading>\n {extra}\n </div>\n );\n}\n\n/** 본문. 스크롤이 생기면 `tabindex=\"0\"` 을 붙여 키보드로도 굴릴 수 있게 한다. */\nexport function DrawerBody({ className, children, ref, ...rest }: ComponentPropsWithRef<'div'>) {\n const innerRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n const node = innerRef.current;\n if (!node) return;\n const sync = () => {\n if (node.scrollHeight > node.clientHeight) node.setAttribute('tabindex', '0');\n else node.removeAttribute('tabindex');\n };\n sync();\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(sync);\n observer.observe(node);\n for (const child of Array.from(node.children)) observer.observe(child);\n return () => observer.disconnect();\n }, []);\n\n return (\n <div\n className={cx('min-h-0 flex-1 overflow-y-auto px-4 py-6 md:px-10', className)}\n ref={(node) => {\n innerRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) (ref as { current: HTMLDivElement | null }).current = node;\n }}\n {...rest}\n >\n {children}\n </div>\n );\n}\n\nexport type DrawerFooterProps = ComponentPropsWithRef<'div'> & {\n /** 좌우로 갈라 놓는다. 왼쪽에 보조 액션, 오른쪽에 확정 액션을 둘 때. */\n split?: boolean;\n};\n\n/**\n * 푸터 — 확정·취소 액션 자리. 제출(primary)이 가장 오른쪽이다.\n * `split` 을 주면 왼쪽 보조 액션과 오른쪽 확정 액션으로 갈라 놓는다.\n */\nexport function DrawerFooter({ split, className, children, ...rest }: DrawerFooterProps) {\n return (\n <div\n className={cx(\n 'btn-wrap flex shrink-0 flex-wrap items-center gap-2 border-t border-line px-4 py-5 md:px-10',\n split ? 'justify-between' : 'justify-end',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n}\n\nexport type DrawerCloseProps = Omit<ComponentPropsWithRef<'button'>, 'children'> & {\n /** 스크린리더가 읽을 이름. 아이콘만 있는 버튼이라 반드시 있어야 한다. */\n label?: string;\n};\n\n/**\n * 닫기 버튼 — 킷 `krds-btn small icon` + `ico-popup-close`.\n * **모달과 달리 `Drawer.Header` 의 `extra` 에 넣는다.**\n *\n * @example\n * <Drawer.Header extra={<Drawer.Close />}>필터</Drawer.Header>\n */\nexport function DrawerClose({\n label = '닫기',\n className,\n onClick,\n type = 'button',\n ...rest\n}: DrawerCloseProps) {\n const { close } = useDrawerContext('Drawer.Close');\n\n return (\n <button\n type={type}\n // small(40px) — 헤더 한 줄에 들어가야 해서 모달의 medium(48px)보다 한 단 작다.\n className={cx('krds-btn small icon shrink-0', className)}\n onClick={(event) => {\n onClick?.(event);\n if (!event.defaultPrevented) close();\n }}\n {...rest}\n >\n <span className=\"sr-only\">{label}</span>\n <i className=\"svg-icon ico-popup-close\" aria-hidden=\"true\" />\n </button>\n );\n}\n\n/** 합성용 묶음. `Root` · `Header` · `Body` · `Footer` · `Close`. */\nexport const Drawer = {\n Root: DrawerRoot,\n Header: DrawerHeader,\n Body: DrawerBody,\n Footer: DrawerFooter,\n Close: DrawerClose,\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ReactNode } from 'react';
|
|
2
|
+
import { type KrdsIcon, type KrdsTone } from '../lib/krds';
|
|
3
|
+
/**
|
|
4
|
+
* 빈 상태 상자 — KRDS 에 없다. shadcn `Empty` 형태에 KRDS 토큰을 입힌 보충 부품이다.
|
|
5
|
+
*
|
|
6
|
+
* 「보여 줄 것이 없다」를 알린다 — 등록된 것이 없음 · 조건에 맞는 것이 없음 · 권한 없음 · 불러오기 실패.
|
|
7
|
+
* 「알아 둘 것이 있다」는 `Alert`, 표 안 한 행짜리 빈 칸은 `ui/table` 의 `TableEmpty` 다.
|
|
8
|
+
* **막다른 골목을 만들지 마라** — `hints` 나 `actions` 중 하나는 준다.
|
|
9
|
+
* 검색 결과 없음은 규격이 문구와 대안 4개까지 못 박았으므로 {@link SearchEmptyState} 를 쓴다.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <EmptyState reason="no-results" actions={<Button onClick={reset}>조건 초기화</Button>} />
|
|
13
|
+
* <EmptyState reason="forbidden" layout="inline" />
|
|
14
|
+
* <SearchEmptyState query={q} count={0} helpHref="/help/search" />
|
|
15
|
+
*
|
|
16
|
+
* 자세히: docs/krds/09-부품-노트.md#빈상태
|
|
17
|
+
*/
|
|
18
|
+
/** 왜 비었는가. 색·아이콘·기본 문구가 여기서 갈린다. */
|
|
19
|
+
export type EmptyStateReason =
|
|
20
|
+
/** 아직 등록된 것이 없다(조건 문제가 아니다) */
|
|
21
|
+
'empty'
|
|
22
|
+
/** 검색어·필터에 맞는 것이 없다 */
|
|
23
|
+
| 'no-results'
|
|
24
|
+
/** 볼 권한이 없다 */
|
|
25
|
+
| 'forbidden'
|
|
26
|
+
/** 불러오지 못했다 */
|
|
27
|
+
| 'error';
|
|
28
|
+
/** 색. `KrdsTone` 에서 이 상자가 쓰는 넷만 좁힌 것이다. */
|
|
29
|
+
export type EmptyStateTone = Extract<KrdsTone, 'gray' | 'information' | 'warning' | 'danger'>;
|
|
30
|
+
/**
|
|
31
|
+
* 배치. `panel` 은 목록이 있던 넓은 자리(가운데 정렬), `inline` 은 폼 옆·카드 안처럼 좁은 자리다.
|
|
32
|
+
* 테두리는 둘 다 같은 사방 점선이고 정렬과 여백만 다르다.
|
|
33
|
+
*/
|
|
34
|
+
export type EmptyStateLayout = 'panel' | 'inline';
|
|
35
|
+
export type EmptyStateProps = Omit<ComponentPropsWithRef<'div'>, 'title'> & {
|
|
36
|
+
/** 기본 `empty`. 색·아이콘·기본 문구가 여기서 정해진다. */
|
|
37
|
+
reason?: EmptyStateReason;
|
|
38
|
+
/** 사유의 기본 색을 덮는다. 굳이 필요할 때만. */
|
|
39
|
+
tone?: EmptyStateTone;
|
|
40
|
+
/** 기본 `panel`. 좁은 자리에는 `inline`. */
|
|
41
|
+
layout?: EmptyStateLayout;
|
|
42
|
+
/** 킷 아이콘 이름(`ico-` 뺀 것). 사유 기본값을 덮는다. `null` 이면 그리지 않는다. */
|
|
43
|
+
icon?: KrdsIcon | null;
|
|
44
|
+
/** 제목 한 줄. 생략하면 사유의 기본 문구. */
|
|
45
|
+
title?: ReactNode;
|
|
46
|
+
/** 제목 위에 오는 줄. 검색 결과 수처럼 제목보다 먼저 와야 하는 것이 여기 온다. */
|
|
47
|
+
lead?: ReactNode;
|
|
48
|
+
/** 제목 아래 보충 설명. */
|
|
49
|
+
description?: ReactNode;
|
|
50
|
+
/** 대안 행동 목록. 결과 없음에는 반드시 채운다 — 막다른 골목을 만들지 않는다. */
|
|
51
|
+
hints?: ReactNode[];
|
|
52
|
+
/** 아래쪽 단추 자리(`ui/button` 의 Button·LinkButton 을 넣는다). */
|
|
53
|
+
actions?: ReactNode;
|
|
54
|
+
/**
|
|
55
|
+
* `role="status"` 를 붙인다. 클라이언트에서 목록이 바뀌는 화면에서만 켠다 —
|
|
56
|
+
* 이때 상자는 내용이 바뀌기 전부터 DOM 에 있어야 낭독된다.
|
|
57
|
+
*/
|
|
58
|
+
live?: boolean;
|
|
59
|
+
children?: ReactNode;
|
|
60
|
+
};
|
|
61
|
+
export declare function EmptyState({ reason, tone, layout, icon, title, lead, description, hints, actions, live, className, children, ...rest }: EmptyStateProps): import("react").JSX.Element;
|
|
62
|
+
/**
|
|
63
|
+
* 검색 결과 없음 — KRDS 가 문구와 대안까지 못 박은 화면이다.
|
|
64
|
+
*
|
|
65
|
+
* 결과 수 0 · 규격 문구 · 대안 4개(철자 확인 · 다른 검색어 · 더 일반적인 검색어 · 검색 도움말)를
|
|
66
|
+
* 기본값으로 담는다. `<SearchEmptyState query={q} />` 만으로 규격을 채운다.
|
|
67
|
+
*
|
|
68
|
+
* **`helpHref` 를 주지 않으면 대안 ④가 갈 곳 없는 문장으로 남는다** —
|
|
69
|
+
* 도움말 문서가 아직 없다면 `hints` 를 직접 넘겨 그 화면에서 할 수 있는 행동으로 바꿔라.
|
|
70
|
+
*/
|
|
71
|
+
/** 규격이 든 안내 문구 그대로. 바꾸지 마라. */
|
|
72
|
+
export declare const SEARCH_NO_RESULT_MESSAGE = "\uAC80\uC0C9\uC5B4\uC640 \uC77C\uCE58\uD558\uB294 \uACB0\uACFC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
|
|
73
|
+
/** 규격 대안 4개 중 링크가 필요 없는 셋. ④는 `helpHref` 에 따라 아래에서 만든다. */
|
|
74
|
+
export declare const SEARCH_NO_RESULT_HINTS: readonly string[];
|
|
75
|
+
export type SearchEmptyStateProps = Omit<EmptyStateProps, 'reason' | 'lead'> & {
|
|
76
|
+
/** 사용자가 넣은 검색어. 결과 수 줄에 굵게 되짚어 준다. */
|
|
77
|
+
query?: string;
|
|
78
|
+
/** 결과 수. 규격상 0을 반드시 표시한다. 화면 위쪽에서 이미 세고 있으면 `null` 로 숨긴다. */
|
|
79
|
+
count?: number | null;
|
|
80
|
+
/** 검색 도움말 문서 주소. 규격 대안 ④가 링크가 되려면 있어야 한다. */
|
|
81
|
+
helpHref?: string;
|
|
82
|
+
/** 도움말 링크 글자. 기본 「검색 도움말」 — 바꾸면 뒤따르는 조사가 어긋날 수 있다. */
|
|
83
|
+
helpLabel?: string;
|
|
84
|
+
};
|
|
85
|
+
export declare function SearchEmptyState({ query, count, helpHref, helpLabel, hints, title, ...rest }: SearchEmptyStateProps): import("react").JSX.Element;
|
|
86
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/empty-state/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC9D,OAAO,EAAM,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE/D;;;;;;;;;;;;;;GAcG;AAEH,oCAAoC;AACpC,MAAM,MAAM,gBAAgB;AAC1B,+BAA+B;AAC7B,OAAO;AACT,uBAAuB;GACrB,YAAY;AACd,eAAe;GACb,WAAW;AACb,eAAe;GACb,OAAO,CAAC;AAEZ,2CAA2C;AAC3C,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,GAAG,QAAQ,CAAC,CAAC;AAE9F;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,QAAQ,CAAC;AAkBlD,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,GAAG;IAC1E,yCAAyC;IACzC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,gCAAgC;IAChC,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,oCAAoC;IACpC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,4DAA4D;IAC5D,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,8BAA8B;IAC9B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,oDAAoD;IACpD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,mBAAmB;IACnB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,mDAAmD;IACnD,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,wDAAwD;IACxD,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,CAAC;AAEF,wBAAgB,UAAU,CAAC,EACzB,MAAgB,EAChB,IAAI,EACJ,MAAgB,EAChB,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,WAAW,EACX,KAAK,EACL,OAAO,EACP,IAAI,EACJ,SAAS,EACT,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,eAAe,+BAiFjB;AAED;;;;;;;;GAQG;AAEH,+BAA+B;AAC/B,eAAO,MAAM,wBAAwB,uHAA6B,CAAC;AAEnE,2DAA2D;AAC3D,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAInD,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,MAAM,CAAC,GAAG;IAC7E,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,KAAS,EACT,QAAQ,EACR,SAAoB,EACpB,KAAK,EACL,KAAgC,EAChC,GAAG,IAAI,EACR,EAAE,qBAAqB,+BAoCvB"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { UiLink } from "../lib/link.js";
|
|
3
|
+
import { cx } from "../lib/krds.js";
|
|
4
|
+
const REASON = {
|
|
5
|
+
empty: { tone: "gray", icon: "file", title: "\uB4F1\uB85D\uB41C \uC790\uB8CC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." },
|
|
6
|
+
"no-results": { tone: "gray", icon: "sch", title: "\uC870\uAC74\uC5D0 \uB9DE\uB294 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." },
|
|
7
|
+
forbidden: { tone: "warning", icon: null, title: "\uC774 \uB0B4\uC6A9\uC744 \uBCFC \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." },
|
|
8
|
+
error: { tone: "danger", icon: "error-fill", title: "\uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4." }
|
|
9
|
+
};
|
|
10
|
+
const TONE = {
|
|
11
|
+
gray: { line: "border-line", surface: "bg-surface-subtler", tint: "bg-fg-disabled" },
|
|
12
|
+
information: { line: "border-information-line", surface: "bg-information-surface", tint: "bg-information" },
|
|
13
|
+
warning: { line: "border-warning-line", surface: "bg-warning-surface", tint: "bg-warning" },
|
|
14
|
+
danger: { line: "border-danger-line", surface: "bg-danger-surface", tint: "bg-danger" }
|
|
15
|
+
};
|
|
16
|
+
function EmptyState({
|
|
17
|
+
reason = "empty",
|
|
18
|
+
tone,
|
|
19
|
+
layout = "panel",
|
|
20
|
+
icon,
|
|
21
|
+
title,
|
|
22
|
+
lead,
|
|
23
|
+
description,
|
|
24
|
+
hints,
|
|
25
|
+
actions,
|
|
26
|
+
live,
|
|
27
|
+
className,
|
|
28
|
+
children,
|
|
29
|
+
...rest
|
|
30
|
+
}) {
|
|
31
|
+
const preset = REASON[reason];
|
|
32
|
+
const t = TONE[tone ?? preset.tone];
|
|
33
|
+
const panel = layout === "panel";
|
|
34
|
+
const iconName = icon === void 0 ? preset.icon : icon;
|
|
35
|
+
const iconNode = iconName ? /* @__PURE__ */ jsx(
|
|
36
|
+
"i",
|
|
37
|
+
{
|
|
38
|
+
className: cx(
|
|
39
|
+
"svg-icon",
|
|
40
|
+
`ico-${iconName}`,
|
|
41
|
+
panel ? "size-12" : "size-6 shrink-0",
|
|
42
|
+
!iconName.endsWith("-fill") && t.tint
|
|
43
|
+
),
|
|
44
|
+
"aria-hidden": "true"
|
|
45
|
+
}
|
|
46
|
+
) : null;
|
|
47
|
+
const hintList = hints && hints.length > 0 ? (
|
|
48
|
+
// 킷 전역 `ol,ul{list-style:none}` 때문에 목록 의미가 떨어진다 — `ui/text-list` 와 같이 role 을 명시한다.
|
|
49
|
+
/* @__PURE__ */ jsx("ul", { role: "list", className: cx("krds-info-list dash", panel && "w-fit text-left"), children: hints.map((hint, i) => /* @__PURE__ */ jsx("li", { role: "listitem", children: hint }, i)) })
|
|
50
|
+
) : null;
|
|
51
|
+
const header = /* @__PURE__ */ jsxs("div", { className: cx("flex flex-col gap-2", panel && "items-center text-center"), children: [
|
|
52
|
+
panel && iconNode,
|
|
53
|
+
lead && /* @__PURE__ */ jsx("p", { className: "m-0 text-sm text-fg-subtle", children: lead }),
|
|
54
|
+
/* @__PURE__ */ jsx("p", { className: cx("m-0 font-bold text-fg", panel ? "text-heading-xs" : "text-sm"), children: title ?? preset.title }),
|
|
55
|
+
description && /* @__PURE__ */ jsx("p", { className: "m-0 text-sm text-fg-subtle", children: description })
|
|
56
|
+
] });
|
|
57
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
58
|
+
header,
|
|
59
|
+
hintList,
|
|
60
|
+
children,
|
|
61
|
+
actions && /* @__PURE__ */ jsx("div", { className: cx("flex flex-wrap items-center gap-2", panel && "justify-center"), children: actions })
|
|
62
|
+
] });
|
|
63
|
+
return /* @__PURE__ */ jsx(
|
|
64
|
+
"div",
|
|
65
|
+
{
|
|
66
|
+
role: live ? "status" : void 0,
|
|
67
|
+
className: cx(
|
|
68
|
+
// shadcn `Empty` — 점선 사방 테두리 + 둥글기. `border-solid` 가 아니라 `border-dashed` 라야
|
|
69
|
+
// 선이 그려진다(preflight 가 없어 border-style 기본값이 none 이다. `ui/separator` 함정 2).
|
|
70
|
+
"rounded-lg border border-dashed",
|
|
71
|
+
panel ? (
|
|
72
|
+
// 가운데 정렬 + 세로 흐름 `gap-6`(24px). 위아래 40px 은 킷 `.no-results` 실측(`--krds-padding-10`).
|
|
73
|
+
"flex flex-col items-center gap-6 px-6 py-10 text-center"
|
|
74
|
+
) : (
|
|
75
|
+
// 좁은 자리 — 아이콘을 왼쪽에 세우고 글은 왼쪽 정렬. 여백 24/16px.
|
|
76
|
+
"flex items-start gap-2 px-6 py-4 text-left"
|
|
77
|
+
),
|
|
78
|
+
t.line,
|
|
79
|
+
t.surface,
|
|
80
|
+
className
|
|
81
|
+
),
|
|
82
|
+
...rest,
|
|
83
|
+
children: panel ? body : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
84
|
+
iconNode,
|
|
85
|
+
/* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: body })
|
|
86
|
+
] })
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const SEARCH_NO_RESULT_MESSAGE = "\uAC80\uC0C9\uC5B4\uC640 \uC77C\uCE58\uD558\uB294 \uACB0\uACFC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
|
|
91
|
+
const SEARCH_NO_RESULT_HINTS = [
|
|
92
|
+
"\uAC80\uC0C9\uC5B4\uC758 \uCCA0\uC790\uAC00 \uC815\uD655\uD55C\uC9C0 \uD655\uC778\uD574 \uBCF4\uC138\uC694.",
|
|
93
|
+
"\uB2E4\uB978 \uAC80\uC0C9\uC5B4\uB85C \uB2E4\uC2DC \uAC80\uC0C9\uD574 \uBCF4\uC138\uC694.",
|
|
94
|
+
"\uB354 \uC77C\uBC18\uC801\uC778 \uAC80\uC0C9\uC5B4\uB97C \uC0AC\uC6A9\uD574 \uBCF4\uC138\uC694."
|
|
95
|
+
];
|
|
96
|
+
function SearchEmptyState({
|
|
97
|
+
query,
|
|
98
|
+
count = 0,
|
|
99
|
+
helpHref,
|
|
100
|
+
helpLabel = "\uAC80\uC0C9 \uB3C4\uC6C0\uB9D0",
|
|
101
|
+
hints,
|
|
102
|
+
title = SEARCH_NO_RESULT_MESSAGE,
|
|
103
|
+
...rest
|
|
104
|
+
}) {
|
|
105
|
+
const helpLink = helpHref ? helpHref.startsWith("/") ? /* @__PURE__ */ jsx(UiLink, { href: helpHref, className: "text-link underline hover:text-link-hover", children: helpLabel }) : /* @__PURE__ */ jsx("a", { href: helpHref, className: "text-link underline hover:text-link-hover", children: helpLabel }) : null;
|
|
106
|
+
const specHints = [
|
|
107
|
+
...SEARCH_NO_RESULT_HINTS,
|
|
108
|
+
helpLink ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
109
|
+
helpLink,
|
|
110
|
+
"\uC744 \uCC38\uACE0\uD558\uC138\uC694."
|
|
111
|
+
] }) : `${helpLabel}\uC744 \uCC38\uACE0\uD558\uC138\uC694.`
|
|
112
|
+
];
|
|
113
|
+
return /* @__PURE__ */ jsx(
|
|
114
|
+
EmptyState,
|
|
115
|
+
{
|
|
116
|
+
reason: "no-results",
|
|
117
|
+
title,
|
|
118
|
+
lead: count === null ? void 0 : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
119
|
+
query ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
120
|
+
"\u300C",
|
|
121
|
+
/* @__PURE__ */ jsx("b", { className: "text-fg", children: query }),
|
|
122
|
+
"\u300D\uC5D0 \uB300\uD55C \uAC80\uC0C9 \uACB0\uACFC "
|
|
123
|
+
] }) : "\uAC80\uC0C9 \uACB0\uACFC ",
|
|
124
|
+
/* @__PURE__ */ jsx("b", { className: "text-fg", children: count.toLocaleString("ko-KR") }),
|
|
125
|
+
"\uAC74"
|
|
126
|
+
] }),
|
|
127
|
+
hints: hints ?? specHints,
|
|
128
|
+
...rest
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
EmptyState,
|
|
134
|
+
SEARCH_NO_RESULT_HINTS,
|
|
135
|
+
SEARCH_NO_RESULT_MESSAGE,
|
|
136
|
+
SearchEmptyState
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=index.js.map
|