@bigtablet/design-system 3.6.0 → 3.8.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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import './index.css';
3
- import * as React15 from 'react';
3
+ import * as React16 from 'react';
4
4
  import { createContext, useRef, useState, useId, useEffect, useContext, useCallback, useMemo, Fragment, useImperativeHandle, useLayoutEffect } from 'react';
5
5
  import { useSpring, animated } from '@react-spring/web';
6
6
  import { ChevronDown, ChevronRight, Globe, ChevronLeft, ArrowUp, ArrowDown, ArrowUpDown, XCircle, AlertTriangle, CheckCircle2, Info, Bell, Search, Check, Image, X, TriangleAlert } from 'lucide-react';
@@ -58,15 +58,137 @@ function registerOverlay(onEscape) {
58
58
  };
59
59
  }
60
60
  function useOverlayEscape(active, onEscape) {
61
- const handlerRef = React15.useRef(onEscape);
62
- React15.useEffect(() => {
61
+ const handlerRef = React16.useRef(onEscape);
62
+ React16.useEffect(() => {
63
63
  handlerRef.current = onEscape;
64
64
  });
65
- React15.useEffect(() => {
65
+ React16.useEffect(() => {
66
66
  if (!active) return;
67
67
  return registerOverlay(() => handlerRef.current());
68
68
  }, [active]);
69
69
  }
70
+ var useSafeLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
71
+
72
+ // src/utils/use-anchored-position.ts
73
+ var OPPOSITE = {
74
+ top: "bottom",
75
+ bottom: "top",
76
+ left: "right",
77
+ right: "left"
78
+ };
79
+ var isVertical = (side) => side === "top" || side === "bottom";
80
+ var clamp = (value, min, max) => (
81
+ // max < min(가용 공간이 플로팅보다 작을 때)이면 min(가장자리 여백)에 고정.
82
+ Math.max(min, Math.min(max, value))
83
+ );
84
+ var mainAxisStart = (side, anchor, floating, gap) => {
85
+ switch (side) {
86
+ case "top":
87
+ return anchor.top - gap - floating.height;
88
+ case "bottom":
89
+ return anchor.top + anchor.height + gap;
90
+ case "left":
91
+ return anchor.left - gap - floating.width;
92
+ case "right":
93
+ return anchor.left + anchor.width + gap;
94
+ }
95
+ };
96
+ var fitsMainAxis = (side, anchor, floating, viewport, gap, padding) => {
97
+ const start = mainAxisStart(side, anchor, floating, gap);
98
+ switch (side) {
99
+ case "top":
100
+ return start >= padding;
101
+ case "bottom":
102
+ return start + floating.height <= viewport.height - padding;
103
+ case "left":
104
+ return start >= padding;
105
+ case "right":
106
+ return start + floating.width <= viewport.width - padding;
107
+ }
108
+ };
109
+ function computeAnchoredPosition(anchor, floating, viewport, options) {
110
+ const gap = options.gap ?? 8;
111
+ const padding = options.padding;
112
+ const available = viewport.width - padding * 2;
113
+ const maxWidth = available;
114
+ const sized = {
115
+ width: Math.min(floating.width, available),
116
+ height: floating.height
117
+ };
118
+ let side = options.placement;
119
+ if (!fitsMainAxis(side, anchor, sized, viewport, gap, padding) && fitsMainAxis(OPPOSITE[side], anchor, sized, viewport, gap, padding)) {
120
+ side = OPPOSITE[side];
121
+ }
122
+ let x;
123
+ let y;
124
+ if (isVertical(side)) {
125
+ y = mainAxisStart(side, anchor, sized, gap);
126
+ x = anchor.left + anchor.width / 2 - sized.width / 2;
127
+ x = clamp(x, padding, viewport.width - padding - sized.width);
128
+ } else {
129
+ x = mainAxisStart(side, anchor, sized, gap);
130
+ y = anchor.top + anchor.height / 2 - sized.height / 2;
131
+ y = clamp(y, padding, viewport.height - padding - sized.height);
132
+ }
133
+ return { x, y, placement: side, maxWidth };
134
+ }
135
+ function useAnchoredPosition({
136
+ open,
137
+ anchorRef,
138
+ floatingRef,
139
+ placement,
140
+ gap,
141
+ padding
142
+ }) {
143
+ const [state, setState] = React16.useState({
144
+ x: 0,
145
+ y: 0,
146
+ placement,
147
+ maxWidth: 0,
148
+ ready: false
149
+ });
150
+ useSafeLayoutEffect(() => {
151
+ if (!open) {
152
+ setState((prev) => prev.ready ? { ...prev, ready: false } : prev);
153
+ return;
154
+ }
155
+ const anchor = anchorRef.current;
156
+ const floating = floatingRef.current;
157
+ if (!anchor || !floating) return;
158
+ const update = () => {
159
+ const a = anchor.getBoundingClientRect();
160
+ const f = floating.getBoundingClientRect();
161
+ const result = computeAnchoredPosition(
162
+ { top: a.top, left: a.left, width: a.width, height: a.height },
163
+ { width: f.width, height: f.height },
164
+ { width: window.innerWidth, height: window.innerHeight },
165
+ { placement, gap, padding }
166
+ );
167
+ setState({ ...result, ready: true });
168
+ };
169
+ let frame = 0;
170
+ const schedule = () => {
171
+ if (frame) return;
172
+ frame = requestAnimationFrame(() => {
173
+ frame = 0;
174
+ update();
175
+ });
176
+ };
177
+ update();
178
+ window.addEventListener("scroll", schedule, true);
179
+ window.addEventListener("resize", schedule);
180
+ const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(schedule) : null;
181
+ observer?.observe(floating);
182
+ observer?.observe(anchor);
183
+ return () => {
184
+ if (frame) cancelAnimationFrame(frame);
185
+ window.removeEventListener("scroll", schedule, true);
186
+ window.removeEventListener("resize", schedule);
187
+ observer?.disconnect();
188
+ };
189
+ }, [open, placement, gap, padding, anchorRef, floatingRef]);
190
+ return state;
191
+ }
70
192
  var FOCUSABLE_SELECTORS = [
71
193
  "a[href]",
72
194
  "button:not([disabled])",
@@ -76,8 +198,8 @@ var FOCUSABLE_SELECTORS = [
76
198
  '[tabindex]:not([tabindex="-1"])'
77
199
  ].join(", ");
78
200
  function useFocusTrap(containerRef, isActive) {
79
- const previousActiveElement = React15.useRef(null);
80
- React15.useEffect(() => {
201
+ const previousActiveElement = React16.useRef(null);
202
+ React16.useEffect(() => {
81
203
  if (!isActive) return;
82
204
  const container = containerRef.current;
83
205
  if (!container) return;
@@ -126,8 +248,8 @@ function useFocusTrap(containerRef, isActive) {
126
248
  }, [isActive, containerRef]);
127
249
  }
128
250
  function useIsMounted() {
129
- const [mounted, setMounted] = React15.useState(false);
130
- React15.useEffect(() => {
251
+ const [mounted, setMounted] = React16.useState(false);
252
+ React16.useEffect(() => {
131
253
  setMounted(true);
132
254
  }, []);
133
255
  return mounted;
@@ -161,14 +283,13 @@ function getServerSnapshot() {
161
283
  return false;
162
284
  }
163
285
  function useReducedMotion() {
164
- return React15.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
286
+ return React16.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
165
287
  }
166
- var useSafeLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
167
288
  function useSpringHover({
168
289
  scale = 1.02,
169
290
  lift = -2
170
291
  } = {}) {
171
- const [hovered, setHovered] = React15.useState(false);
292
+ const [hovered, setHovered] = React16.useState(false);
172
293
  const reduced = useReducedMotion();
173
294
  const style = useSpring({
174
295
  transform: hovered ? `translateY(${lift}px) scale(${scale})` : "translateY(0px) scale(1)",
@@ -232,9 +353,9 @@ var Accordion = ({
232
353
  ...props
233
354
  }) => {
234
355
  const isControlled = controlledKeys !== void 0;
235
- const [internalKeys, setInternalKeys] = React15.useState(defaultOpenKeys);
356
+ const [internalKeys, setInternalKeys] = React16.useState(defaultOpenKeys);
236
357
  const open = isControlled ? controlledKeys ?? [] : internalKeys;
237
- const idPrefix = React15.useId();
358
+ const idPrefix = React16.useId();
238
359
  const toggle = (key) => {
239
360
  const isOpen = open.includes(key);
240
361
  const next = isOpen ? open.filter((k) => k !== key) : multiple ? [...open, key] : [key];
@@ -300,7 +421,7 @@ var Avatar = ({
300
421
  style,
301
422
  ...props
302
423
  }) => {
303
- const [imgFailed, setImgFailed] = React15.useState(false);
424
+ const [imgFailed, setImgFailed] = React16.useState(false);
304
425
  const showImage = src && !imgFailed;
305
426
  const initials = getInitials(name);
306
427
  return /* @__PURE__ */ jsx(
@@ -519,12 +640,12 @@ var Breadcrumb = ({
519
640
  }) }) });
520
641
  };
521
642
  var Menu = ({ items, trigger, align = "start" }) => {
522
- const [open, setOpen] = React15.useState(false);
523
- const wrapperRef = React15.useRef(null);
524
- const itemRefs = React15.useRef([]);
525
- const menuId = React15.useId();
643
+ const [open, setOpen] = React16.useState(false);
644
+ const wrapperRef = React16.useRef(null);
645
+ const itemRefs = React16.useRef([]);
646
+ const menuId = React16.useId();
526
647
  const style = useSpringPresence({ visible: open, from: "translateY(-4px)" });
527
- React15.useEffect(() => {
648
+ React16.useEffect(() => {
528
649
  if (!open) return;
529
650
  const handleClick = (e) => {
530
651
  if (!wrapperRef.current?.contains(e.target)) setOpen(false);
@@ -539,7 +660,7 @@ var Menu = ({ items, trigger, align = "start" }) => {
539
660
  document.removeEventListener("keydown", handleEsc);
540
661
  };
541
662
  }, [open]);
542
- React15.useEffect(() => {
663
+ React16.useEffect(() => {
543
664
  const active = document.activeElement;
544
665
  if (!open || active && itemRefs.current.includes(active)) return;
545
666
  const first = items.findIndex((it) => !it.disabled);
@@ -585,7 +706,7 @@ var Menu = ({ items, trigger, align = "start" }) => {
585
706
  break;
586
707
  }
587
708
  };
588
- const triggerWithProps = React15.cloneElement(
709
+ const triggerWithProps = React16.cloneElement(
589
710
  trigger,
590
711
  {
591
712
  onClick: () => setOpen((o) => !o),
@@ -727,6 +848,7 @@ var LocaleSwitcher = ({ locale }) => {
727
848
  const itemRefs = useRef([]);
728
849
  const menuId = useId();
729
850
  const currentOption = locale.options.find((o) => o.value === locale.current);
851
+ const currentLabel = currentOption?.label ?? locale.current.toUpperCase();
730
852
  useEffect(() => {
731
853
  if (!open) return;
732
854
  const handler = (e) => {
@@ -800,10 +922,11 @@ var LocaleSwitcher = ({ locale }) => {
800
922
  "aria-haspopup": "menu",
801
923
  "aria-expanded": open,
802
924
  "aria-controls": menuId,
925
+ "aria-label": locale.ariaLabel ?? currentLabel,
803
926
  onClick: () => setOpen((p) => !p),
804
927
  children: [
805
928
  /* @__PURE__ */ jsx(Globe, { size: iconSize.sm, "aria-hidden": "true" }),
806
- !locale.hideLabel && /* @__PURE__ */ jsx("span", { className: "nav_bar_locale_label", children: currentOption?.label ?? locale.current.toUpperCase() }),
929
+ !locale.hideLabel && /* @__PURE__ */ jsx("span", { className: "nav_bar_locale_label", children: currentLabel }),
807
930
  /* @__PURE__ */ jsx(
808
931
  ChevronDown,
809
932
  {
@@ -876,9 +999,9 @@ var Sidebar = ({
876
999
  ...props
877
1000
  }) => {
878
1001
  const isControlled = collapsedProp !== void 0;
879
- const [internalCollapsed, setInternalCollapsed] = React15.useState(defaultCollapsed);
1002
+ const [internalCollapsed, setInternalCollapsed] = React16.useState(defaultCollapsed);
880
1003
  const collapsed = isControlled ? collapsedProp : internalCollapsed;
881
- const toggle = React15.useCallback(() => {
1004
+ const toggle = React16.useCallback(() => {
882
1005
  const next = !collapsed;
883
1006
  if (!isControlled) setInternalCollapsed(next);
884
1007
  onCollapsedChange?.(next);
@@ -951,9 +1074,9 @@ var SidebarSection = ({ label, className, children, ...props }) => {
951
1074
  children
952
1075
  ] });
953
1076
  };
954
- var TabsContext = React15.createContext(null);
1077
+ var TabsContext = React16.createContext(null);
955
1078
  function useTabsContext() {
956
- const ctx = React15.useContext(TabsContext);
1079
+ const ctx = React16.useContext(TabsContext);
957
1080
  if (!ctx) {
958
1081
  throw new Error(
959
1082
  '[Bigtablet DS] <Tab>, <TabList>, <TabPanel>\uC740 \uBC18\uB4DC\uC2DC <Tabs> \uC548\uC5D0 \uC788\uC5B4\uC57C \uD569\uB2C8\uB2E4.\n\n\uC62C\uBC14\uB978 \uC0AC\uC6A9 \uC608:\n <Tabs defaultValue="a">\n <TabList>\n <Tab value="a">A</Tab>\n </TabList>\n <TabPanel value="a">...</TabPanel>\n </Tabs>'
@@ -972,18 +1095,18 @@ var Tabs = ({
972
1095
  ...props
973
1096
  }) => {
974
1097
  const isControlled = controlledValue !== void 0;
975
- const [internalValue, setInternalValue] = React15.useState(defaultValue);
1098
+ const [internalValue, setInternalValue] = React16.useState(defaultValue);
976
1099
  const value = isControlled ? controlledValue : internalValue;
977
- const setValue = React15.useCallback(
1100
+ const setValue = React16.useCallback(
978
1101
  (v) => {
979
1102
  if (!isControlled) setInternalValue(v);
980
1103
  onValueChange?.(v);
981
1104
  },
982
1105
  [isControlled, onValueChange]
983
1106
  );
984
- const orderRef = React15.useRef([]);
985
- const [firstTabValue, setFirstTabValue] = React15.useState();
986
- const registerTab = React15.useCallback((v) => {
1107
+ const orderRef = React16.useRef([]);
1108
+ const [firstTabValue, setFirstTabValue] = React16.useState();
1109
+ const registerTab = React16.useCallback((v) => {
987
1110
  if (!orderRef.current.includes(v)) {
988
1111
  orderRef.current = [...orderRef.current, v];
989
1112
  setFirstTabValue(orderRef.current[0]);
@@ -993,8 +1116,8 @@ var Tabs = ({
993
1116
  setFirstTabValue(orderRef.current[0]);
994
1117
  };
995
1118
  }, []);
996
- const idPrefix = React15.useId();
997
- const ctx = React15.useMemo(
1119
+ const idPrefix = React16.useId();
1120
+ const ctx = React16.useMemo(
998
1121
  () => ({ value, setValue, variant, size, idPrefix, registerTab, firstTabValue }),
999
1122
  [value, setValue, variant, size, idPrefix, registerTab, firstTabValue]
1000
1123
  );
@@ -1002,9 +1125,9 @@ var Tabs = ({
1002
1125
  };
1003
1126
  var TabList = ({ ariaLabel, className, children, ...props }) => {
1004
1127
  const { variant } = useTabsContext();
1005
- const listRef = React15.useRef(null);
1006
- const [indicator, setIndicator] = React15.useState(null);
1007
- const [hasMounted, setHasMounted] = React15.useState(false);
1128
+ const listRef = React16.useRef(null);
1129
+ const [indicator, setIndicator] = React16.useState(null);
1130
+ const [hasMounted, setHasMounted] = React16.useState(false);
1008
1131
  useSafeLayoutEffect(() => {
1009
1132
  const list = listRef.current;
1010
1133
  if (!list) return;
@@ -1067,7 +1190,7 @@ var Tab = ({ value, className, children, onClick, onKeyDown, ...props }) => {
1067
1190
  const panelId = `${ctx.idPrefix}-panel-${value}`;
1068
1191
  const tabId = `${ctx.idPrefix}-tab-${value}`;
1069
1192
  const { registerTab } = ctx;
1070
- React15.useEffect(() => registerTab(value), [registerTab, value]);
1193
+ React16.useEffect(() => registerTab(value), [registerTab, value]);
1071
1194
  const handleClick = (e) => {
1072
1195
  ctx.setValue(value);
1073
1196
  onClick?.(e);
@@ -1137,14 +1260,7 @@ var TabPanel = ({
1137
1260
  }
1138
1261
  );
1139
1262
  };
1140
- var FOCUSABLE_SELECTORS2 = [
1141
- "a[href]",
1142
- "button:not([disabled])",
1143
- "input:not([disabled])",
1144
- "select:not([disabled])",
1145
- "textarea:not([disabled])",
1146
- '[tabindex]:not([tabindex="-1"])'
1147
- ].join(", ");
1263
+ var POPOVER_GAP = 8;
1148
1264
  var Popover = ({
1149
1265
  trigger,
1150
1266
  content,
@@ -1157,23 +1273,31 @@ var Popover = ({
1157
1273
  className
1158
1274
  }) => {
1159
1275
  const isControlled = openProp !== void 0;
1160
- const [internalOpen, setInternalOpen] = React15.useState(defaultOpen);
1276
+ const [internalOpen, setInternalOpen] = React16.useState(defaultOpen);
1161
1277
  const open = isControlled ? openProp : internalOpen;
1162
- const [shouldRender, setShouldRender] = React15.useState(open ?? false);
1278
+ const [shouldRender, setShouldRender] = React16.useState(open ?? false);
1163
1279
  if (open && !shouldRender) setShouldRender(true);
1164
- const wrapperRef = React15.useRef(null);
1165
- const popoverRef = React15.useRef(null);
1166
- const previousFocusRef = React15.useRef(null);
1167
- const popoverId = React15.useId();
1168
- const setOpen = React15.useCallback(
1280
+ const wrapperRef = React16.useRef(null);
1281
+ const positionRef = React16.useRef(null);
1282
+ const popoverRef = React16.useRef(null);
1283
+ const popoverId = React16.useId();
1284
+ const setOpen = React16.useCallback(
1169
1285
  (next) => {
1170
1286
  if (!isControlled) setInternalOpen(next);
1171
1287
  onOpenChange?.(next);
1172
1288
  },
1173
1289
  [isControlled, onOpenChange]
1174
1290
  );
1291
+ const pos = useAnchoredPosition({
1292
+ open: shouldRender,
1293
+ anchorRef: wrapperRef,
1294
+ floatingRef: positionRef,
1295
+ placement,
1296
+ gap: POPOVER_GAP,
1297
+ padding: 8
1298
+ });
1175
1299
  const fromTransform = (() => {
1176
- switch (placement) {
1300
+ switch (pos.placement) {
1177
1301
  case "top":
1178
1302
  return "translateY(4px)";
1179
1303
  case "bottom":
@@ -1184,28 +1308,27 @@ var Popover = ({
1184
1308
  return "translateX(-4px)";
1185
1309
  }
1186
1310
  })();
1187
- const style = useSpringPresence({ visible: open, from: fromTransform, onExitComplete: () => setShouldRender(false) });
1188
- React15.useEffect(() => {
1189
- if (!open) return;
1190
- previousFocusRef.current = document.activeElement;
1191
- const node = popoverRef.current;
1192
- if (!node) return;
1193
- const focusable = node.querySelector(FOCUSABLE_SELECTORS2);
1194
- (focusable ?? node).focus();
1195
- }, [open]);
1196
- React15.useEffect(() => {
1311
+ const style = useSpringPresence({
1312
+ visible: open,
1313
+ from: fromTransform,
1314
+ onExitComplete: () => setShouldRender(false)
1315
+ });
1316
+ useFocusTrap(popoverRef, open);
1317
+ React16.useEffect(() => {
1197
1318
  if (!open) return;
1198
1319
  const handleClick = (e) => {
1199
- if (!wrapperRef.current?.contains(e.target)) setOpen(false);
1320
+ const target = e.target;
1321
+ if (!wrapperRef.current?.contains(target) && !popoverRef.current?.contains(target)) {
1322
+ setOpen(false);
1323
+ }
1200
1324
  };
1201
1325
  document.addEventListener("mousedown", handleClick);
1202
1326
  return () => document.removeEventListener("mousedown", handleClick);
1203
1327
  }, [open, setOpen]);
1204
1328
  useOverlayEscape(open, () => {
1205
1329
  setOpen(false);
1206
- previousFocusRef.current?.focus();
1207
1330
  });
1208
- const triggerWithProps = React15.cloneElement(
1331
+ const triggerWithProps = React16.cloneElement(
1209
1332
  trigger,
1210
1333
  {
1211
1334
  onClick: (e) => {
@@ -1220,23 +1343,43 @@ var Popover = ({
1220
1343
  );
1221
1344
  return /* @__PURE__ */ jsxs("div", { className: "popover_wrapper", ref: wrapperRef, children: [
1222
1345
  triggerWithProps,
1223
- shouldRender && /* @__PURE__ */ jsx("div", { className: cn("popover_position", `popover_placement_${placement}`), children: /* @__PURE__ */ jsx(
1224
- animated.div,
1225
- {
1226
- id: popoverId,
1227
- ref: popoverRef,
1228
- role: "dialog",
1229
- tabIndex: -1,
1230
- "aria-label": ariaLabel,
1231
- "aria-labelledby": ariaLabelledby,
1232
- style,
1233
- className: cn("popover", className),
1234
- children: content
1235
- }
1236
- ) })
1346
+ shouldRender && typeof document !== "undefined" && createPortal(
1347
+ /* @__PURE__ */ jsx(
1348
+ "div",
1349
+ {
1350
+ ref: positionRef,
1351
+ className: "popover_position",
1352
+ style: {
1353
+ position: "fixed",
1354
+ left: pos.x,
1355
+ top: pos.y,
1356
+ // 최초 측정 전(ready=false)에는 maxWidth(=0)를 걸지 않는다 - 걸면 자연 폭 대신
1357
+ // 0px 로 측정돼 첫 프레임 좌표가 어긋난다. ready 후에만 상한 적용.
1358
+ maxWidth: pos.ready ? pos.maxWidth : void 0,
1359
+ visibility: pos.ready ? void 0 : "hidden"
1360
+ },
1361
+ children: /* @__PURE__ */ jsx(
1362
+ animated.div,
1363
+ {
1364
+ id: popoverId,
1365
+ ref: popoverRef,
1366
+ role: "dialog",
1367
+ tabIndex: -1,
1368
+ "aria-label": ariaLabelledby ? ariaLabel : ariaLabel ?? "Dialog",
1369
+ "aria-labelledby": ariaLabelledby,
1370
+ style,
1371
+ className: cn("popover", className),
1372
+ children: content
1373
+ }
1374
+ )
1375
+ }
1376
+ ),
1377
+ document.body
1378
+ )
1237
1379
  ] });
1238
1380
  };
1239
1381
  Popover.displayName = "Popover";
1382
+ var TOOLTIP_GAP = 6;
1240
1383
  var Tooltip = ({
1241
1384
  content,
1242
1385
  placement = "top",
@@ -1244,33 +1387,45 @@ var Tooltip = ({
1244
1387
  disabled = false,
1245
1388
  children
1246
1389
  }) => {
1247
- const [open, setOpen] = React15.useState(false);
1248
- const timerRef = React15.useRef(null);
1249
- const tooltipId = React15.useId();
1390
+ const [open, setOpen] = React16.useState(false);
1391
+ const [shouldRender, setShouldRender] = React16.useState(false);
1392
+ if (open && !shouldRender) setShouldRender(true);
1393
+ const timerRef = React16.useRef(null);
1394
+ const tooltipId = React16.useId();
1395
+ const wrapperRef = React16.useRef(null);
1396
+ const positionRef = React16.useRef(null);
1250
1397
  const HIDE_DELAY = 120;
1251
- const show = React15.useCallback(() => {
1398
+ const show = React16.useCallback(() => {
1252
1399
  if (timerRef.current) clearTimeout(timerRef.current);
1253
1400
  timerRef.current = setTimeout(() => setOpen(true), delay);
1254
1401
  }, [delay]);
1255
- const hideNow = React15.useCallback(() => {
1402
+ const hideNow = React16.useCallback(() => {
1256
1403
  if (timerRef.current) clearTimeout(timerRef.current);
1257
1404
  setOpen(false);
1258
1405
  }, []);
1259
- const hide = React15.useCallback(() => {
1406
+ const hide = React16.useCallback(() => {
1260
1407
  if (timerRef.current) clearTimeout(timerRef.current);
1261
1408
  timerRef.current = setTimeout(() => setOpen(false), HIDE_DELAY);
1262
1409
  }, []);
1263
- const cancelHide = React15.useCallback(() => {
1410
+ const cancelHide = React16.useCallback(() => {
1264
1411
  if (timerRef.current) clearTimeout(timerRef.current);
1265
1412
  }, []);
1266
- React15.useEffect(() => {
1413
+ React16.useEffect(() => {
1267
1414
  return () => {
1268
1415
  if (timerRef.current) clearTimeout(timerRef.current);
1269
1416
  };
1270
1417
  }, []);
1271
1418
  useOverlayEscape(open, hideNow);
1419
+ const pos = useAnchoredPosition({
1420
+ open: shouldRender,
1421
+ anchorRef: wrapperRef,
1422
+ floatingRef: positionRef,
1423
+ placement,
1424
+ gap: TOOLTIP_GAP,
1425
+ padding: 8
1426
+ });
1272
1427
  const fromTransform = (() => {
1273
- switch (placement) {
1428
+ switch (pos.placement) {
1274
1429
  case "top":
1275
1430
  return "translateY(4px)";
1276
1431
  case "bottom":
@@ -1281,10 +1436,14 @@ var Tooltip = ({
1281
1436
  return "translateX(-4px)";
1282
1437
  }
1283
1438
  })();
1284
- const style = useSpringPresence({ visible: open, from: fromTransform });
1439
+ const style = useSpringPresence({
1440
+ visible: open,
1441
+ from: fromTransform,
1442
+ onExitComplete: () => setShouldRender(false)
1443
+ });
1285
1444
  const child = children;
1286
1445
  const childProps = child.props;
1287
- const trigger = React15.cloneElement(child, {
1446
+ const trigger = React16.cloneElement(child, {
1288
1447
  onMouseEnter: (e) => {
1289
1448
  childProps.onMouseEnter?.(e);
1290
1449
  show();
@@ -1305,25 +1464,29 @@ var Tooltip = ({
1305
1464
  "aria-describedby": open ? [childProps["aria-describedby"], tooltipId].filter(Boolean).join(" ") : childProps["aria-describedby"]
1306
1465
  });
1307
1466
  if (disabled) return children;
1308
- return /* @__PURE__ */ jsxs("span", { className: "tooltip_wrapper", children: [
1467
+ return /* @__PURE__ */ jsxs("span", { className: "tooltip_wrapper", ref: wrapperRef, children: [
1309
1468
  trigger,
1310
- open && /* @__PURE__ */ jsx(
1311
- "span",
1312
- {
1313
- className: cn("tooltip_position", `tooltip_placement_${placement}`),
1314
- onMouseEnter: cancelHide,
1315
- onMouseLeave: hide,
1316
- children: /* @__PURE__ */ jsx(
1317
- animated.span,
1318
- {
1319
- id: tooltipId,
1320
- role: "tooltip",
1321
- style,
1322
- className: cn("tooltip", `tooltip_placement_${placement}`),
1323
- children: content
1324
- }
1325
- )
1326
- }
1469
+ shouldRender && typeof document !== "undefined" && createPortal(
1470
+ /* @__PURE__ */ jsx(
1471
+ "span",
1472
+ {
1473
+ ref: positionRef,
1474
+ className: "tooltip_position",
1475
+ style: {
1476
+ position: "fixed",
1477
+ left: pos.x,
1478
+ top: pos.y,
1479
+ // 최초 측정 전(ready=false)에는 maxWidth(=0)를 걸지 않는다 - 걸면 자연 폭 대신
1480
+ // 0px 로 측정돼 첫 프레임 좌표가 어긋난다. ready 후에만 상한 적용.
1481
+ maxWidth: pos.ready ? pos.maxWidth : void 0,
1482
+ visibility: pos.ready ? void 0 : "hidden"
1483
+ },
1484
+ onMouseEnter: cancelHide,
1485
+ onMouseLeave: hide,
1486
+ children: /* @__PURE__ */ jsx(animated.span, { id: tooltipId, role: "tooltip", style, className: "tooltip", children: content })
1487
+ }
1488
+ ),
1489
+ document.body
1327
1490
  )
1328
1491
  ] });
1329
1492
  };
@@ -2251,12 +2414,12 @@ var Checkbox = ({
2251
2414
  ref,
2252
2415
  ...props
2253
2416
  }) => {
2254
- const inputRef = React15.useRef(null);
2255
- React15.useImperativeHandle(
2417
+ const inputRef = React16.useRef(null);
2418
+ React16.useImperativeHandle(
2256
2419
  ref,
2257
2420
  () => inputRef.current
2258
2421
  );
2259
- React15.useEffect(() => {
2422
+ React16.useEffect(() => {
2260
2423
  if (!inputRef.current) return;
2261
2424
  inputRef.current.indeterminate = Boolean(indeterminate);
2262
2425
  }, [indeterminate]);
@@ -2314,8 +2477,8 @@ var Table = ({
2314
2477
  const isEmpty = !isLoading && data.length === 0;
2315
2478
  const getRowKey = (item, index) => rowKey?.(item) ?? String(index);
2316
2479
  const allRowKeys = selectable ? data.map((item, index) => getRowKey(item, index)) : [];
2317
- const rowClickHintId = React15.useId();
2318
- const selectedSet = React15.useMemo(() => new Set(selectedKeys ?? []), [selectedKeys]);
2480
+ const rowClickHintId = React16.useId();
2481
+ const selectedSet = React16.useMemo(() => new Set(selectedKeys ?? []), [selectedKeys]);
2319
2482
  const selectedCount = allRowKeys.filter((key) => selectedSet.has(key)).length;
2320
2483
  const isAllSelected = allRowKeys.length > 0 && selectedCount === allRowKeys.length;
2321
2484
  const isSomeSelected = selectedCount > 0 && !isAllSelected;
@@ -2538,9 +2701,9 @@ var AlertModal = ({
2538
2701
  onClose
2539
2702
  }) => {
2540
2703
  const dismiss = onCancel ?? onClose;
2541
- const panelRef = React15.useRef(null);
2542
- const titleId = React15.useId();
2543
- const messageId = React15.useId();
2704
+ const panelRef = React16.useRef(null);
2705
+ const titleId = React16.useId();
2706
+ const messageId = React16.useId();
2544
2707
  const [shouldRender, setShouldRender] = useState(isOpen);
2545
2708
  useFocusTrap(panelRef, isOpen);
2546
2709
  useOverlayEscape(isOpen, () => dismiss());
@@ -2560,7 +2723,7 @@ var AlertModal = ({
2560
2723
  immediate: reduced,
2561
2724
  config: { tension: 280, friction: 28, clamp: !isOpen }
2562
2725
  });
2563
- React15.useEffect(() => {
2726
+ React16.useEffect(() => {
2564
2727
  if (!shouldRender) return;
2565
2728
  const body = document.body;
2566
2729
  const openModals = parseInt(body.dataset.openModals || "0", 10);
@@ -2687,7 +2850,7 @@ var Spinner = ({ size = 24, ariaLabel = "Loading" }) => {
2687
2850
  }
2688
2851
  );
2689
2852
  };
2690
- var ToastContext = React15.createContext(null);
2853
+ var ToastContext = React16.createContext(null);
2691
2854
  var VARIANT_ICONS = {
2692
2855
  success: /* @__PURE__ */ jsx(CheckCircle2, { size: iconSize.md }),
2693
2856
  error: /* @__PURE__ */ jsx(XCircle, { size: iconSize.md }),
@@ -2696,9 +2859,9 @@ var VARIANT_ICONS = {
2696
2859
  default: /* @__PURE__ */ jsx(Bell, { size: iconSize.md })
2697
2860
  };
2698
2861
  var ToastItemComponent = ({ item, onRemove, closeAriaLabel }) => {
2699
- const [visible, setVisible] = React15.useState(true);
2700
- const closingRef = React15.useRef(false);
2701
- const rootRef = React15.useRef(null);
2862
+ const [visible, setVisible] = React16.useState(true);
2863
+ const closingRef = React16.useRef(false);
2864
+ const rootRef = React16.useRef(null);
2702
2865
  const style = useSpringPresence({
2703
2866
  visible,
2704
2867
  from: "translateX(20px)",
@@ -2718,7 +2881,7 @@ var ToastItemComponent = ({ item, onRemove, closeAriaLabel }) => {
2718
2881
  onRemove(item.id);
2719
2882
  }
2720
2883
  });
2721
- const close = React15.useCallback(() => {
2884
+ const close = React16.useCallback(() => {
2722
2885
  if (closingRef.current) return;
2723
2886
  closingRef.current = true;
2724
2887
  setVisible(false);
@@ -2752,19 +2915,19 @@ var ToastProvider = ({
2752
2915
  maxCount = 5,
2753
2916
  closeAriaLabel = "Close"
2754
2917
  }) => {
2755
- const [toasts, setToasts] = React15.useState([]);
2918
+ const [toasts, setToasts] = React16.useState([]);
2756
2919
  const isMounted = useIsMounted();
2757
- const addToast = React15.useCallback(
2920
+ const addToast = React16.useCallback(
2758
2921
  (message, variant, duration = 3e3) => {
2759
2922
  const id = crypto.randomUUID();
2760
2923
  setToasts((prev) => [{ id, message, variant, duration }, ...prev].slice(0, maxCount));
2761
2924
  },
2762
2925
  [maxCount]
2763
2926
  );
2764
- const removeToast = React15.useCallback((id) => {
2927
+ const removeToast = React16.useCallback((id) => {
2765
2928
  setToasts((prev) => prev.filter((t) => t.id !== id));
2766
2929
  }, []);
2767
- const contextValue = React15.useMemo(() => ({ addToast }), [addToast]);
2930
+ const contextValue = React16.useMemo(() => ({ addToast }), [addToast]);
2768
2931
  return /* @__PURE__ */ jsxs(ToastContext.Provider, { value: contextValue, children: [
2769
2932
  children,
2770
2933
  isMounted && createPortal(
@@ -3198,9 +3361,9 @@ var DatePicker = ({
3198
3361
  minDateSrFormat = "Minimum date: {date}",
3199
3362
  selectableRangeUntilTodaySrText = "Selectable up to today"
3200
3363
  }) => {
3201
- const groupId = React15.useId();
3202
- const constraintId = React15.useId();
3203
- const { todayYear, todayMonth, todayDay } = React15.useMemo(() => {
3364
+ const groupId = React16.useId();
3365
+ const constraintId = React16.useId();
3366
+ const { todayYear, todayMonth, todayDay } = React16.useMemo(() => {
3204
3367
  const now = /* @__PURE__ */ new Date();
3205
3368
  return {
3206
3369
  todayYear: now.getFullYear(),
@@ -3209,7 +3372,7 @@ var DatePicker = ({
3209
3372
  };
3210
3373
  }, []);
3211
3374
  const endYear = endYearProp ?? todayYear + 10;
3212
- const parsed = React15.useMemo(() => {
3375
+ const parsed = React16.useMemo(() => {
3213
3376
  if (!value) return { year: 0, month: 0, day: 0 };
3214
3377
  const [y, m, d] = value.split("-").map(Number);
3215
3378
  return {
@@ -3218,7 +3381,7 @@ var DatePicker = ({
3218
3381
  day: d || 0
3219
3382
  };
3220
3383
  }, [value]);
3221
- const min = React15.useMemo(() => {
3384
+ const min = React16.useMemo(() => {
3222
3385
  if (!minDate) return { year: 0, month: 0, day: 0 };
3223
3386
  const [y, m, d] = minDate.split("-").map(Number);
3224
3387
  return {
@@ -3238,7 +3401,7 @@ var DatePicker = ({
3238
3401
  min.year > 0 && min.month > 0 && year === min.year && month === min.month ? min.day : 1
3239
3402
  )
3240
3403
  );
3241
- const maxDay = React15.useMemo(() => {
3404
+ const maxDay = React16.useMemo(() => {
3242
3405
  if (!year || !month) return 31;
3243
3406
  const daysInMonth = getDaysInMonth(year, month);
3244
3407
  if (selectableRange === "until-today" && year === todayYear && month === todayMonth) {
@@ -3246,28 +3409,28 @@ var DatePicker = ({
3246
3409
  }
3247
3410
  return daysInMonth;
3248
3411
  }, [year, month, selectableRange, todayYear, todayMonth, todayDay]);
3249
- const yearOptions = React15.useMemo(
3412
+ const yearOptions = React16.useMemo(
3250
3413
  () => range(startYear, maxYear).map((y) => ({
3251
3414
  value: String(y),
3252
3415
  label: String(y)
3253
3416
  })),
3254
3417
  [startYear, maxYear]
3255
3418
  );
3256
- const monthOptions = React15.useMemo(
3419
+ const monthOptions = React16.useMemo(
3257
3420
  () => range(minMonth, Math.max(minMonth, maxMonth)).map((m) => ({
3258
3421
  value: String(m),
3259
3422
  label: pad(m)
3260
3423
  })),
3261
3424
  [minMonth, maxMonth]
3262
3425
  );
3263
- const dayOptions = React15.useMemo(
3426
+ const dayOptions = React16.useMemo(
3264
3427
  () => range(minDay, Math.max(minDay, maxDay)).map((d) => ({
3265
3428
  value: String(d),
3266
3429
  label: pad(d)
3267
3430
  })),
3268
3431
  [minDay, maxDay]
3269
3432
  );
3270
- const emit = React15.useCallback(
3433
+ const emit = React16.useCallback(
3271
3434
  (yy, mm, dd) => {
3272
3435
  const cb = onValueChange ?? onChange;
3273
3436
  if (mode === "year-month") {
@@ -3279,7 +3442,7 @@ var DatePicker = ({
3279
3442
  },
3280
3443
  [mode, onValueChange, onChange]
3281
3444
  );
3282
- const handleYearChange = React15.useCallback(
3445
+ const handleYearChange = React16.useCallback(
3283
3446
  (raw) => {
3284
3447
  if (!raw) return;
3285
3448
  const newYear = Number(raw);
@@ -3296,14 +3459,14 @@ var DatePicker = ({
3296
3459
  },
3297
3460
  [month, day, min.year, min.month, selectableRange, todayYear, todayMonth, emit]
3298
3461
  );
3299
- const handleMonthChange = React15.useCallback(
3462
+ const handleMonthChange = React16.useCallback(
3300
3463
  (raw) => {
3301
3464
  if (!raw || !year) return;
3302
3465
  emit(year, Number(raw), day || void 0);
3303
3466
  },
3304
3467
  [year, day, emit]
3305
3468
  );
3306
- const handleDayChange = React15.useCallback(
3469
+ const handleDayChange = React16.useCallback(
3307
3470
  (raw) => {
3308
3471
  if (!raw || !year || !month) return;
3309
3472
  emit(year, month, Number(raw));
@@ -3387,11 +3550,11 @@ var FileInput = ({
3387
3550
  onChange,
3388
3551
  ...props
3389
3552
  }) => {
3390
- const inputId = React15.useId();
3391
- const helperId = React15.useId();
3392
- const inputRef = React15.useRef(null);
3393
- const [previewUrls, setPreviewUrls] = React15.useState([]);
3394
- const previewUrlsRef = React15.useRef([]);
3553
+ const inputId = React16.useId();
3554
+ const helperId = React16.useId();
3555
+ const inputRef = React16.useRef(null);
3556
+ const [previewUrls, setPreviewUrls] = React16.useState([]);
3557
+ const previewUrlsRef = React16.useRef([]);
3395
3558
  const isPreviewVariant = variant === "preview";
3396
3559
  const showPreview = isPreviewVariant || preview;
3397
3560
  const handleChange = (e) => {
@@ -3432,7 +3595,7 @@ var FileInput = ({
3432
3595
  }
3433
3596
  onFiles?.(null);
3434
3597
  };
3435
- React15.useEffect(() => {
3598
+ React16.useEffect(() => {
3436
3599
  return () => {
3437
3600
  for (const url of previewUrlsRef.current) {
3438
3601
  URL.revokeObjectURL(url);
@@ -3561,12 +3724,20 @@ function ImageCropper({
3561
3724
  onReady,
3562
3725
  onError,
3563
3726
  className,
3564
- label = "\uC774\uBBF8\uC9C0 \uC704\uCE58\uC640 \uBC30\uC728 \uC870\uC815"
3727
+ label = "\uC774\uBBF8\uC9C0 \uC704\uCE58\uC640 \uBC30\uC728 \uC870\uC815",
3728
+ hint = "\uB4DC\uB798\uADF8(\uB610\uB294 \uBC29\uD5A5\uD0A4)\uB85C \uC704\uCE58, \uD720\xB7\uC2AC\uB77C\uC774\uB354\uB85C \uBC30\uC728\uC744 \uB9DE\uCD94\uC138\uC694.",
3729
+ zoomOutLabel = "\uCD95\uC18C",
3730
+ zoomLabel = "\uBC30\uC728",
3731
+ zoomInLabel = "\uD655\uB300",
3732
+ noPanHint = "\uC774\uBBF8\uC9C0\uAC00 \uBDF0\uD3EC\uD2B8\uB97C \uB531 \uCC44\uC6CC \uC774\uB3D9 \uC5EC\uC720\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
3733
+ ...rest
3565
3734
  }) {
3566
3735
  const imageRef = useRef(null);
3736
+ const viewportRef = useRef(null);
3567
3737
  const dragRef = useRef(
3568
3738
  null
3569
3739
  );
3740
+ const hintId = useId();
3570
3741
  const [previewUrl, setPreviewUrl] = useState("");
3571
3742
  const [srcType, setSrcType] = useState("");
3572
3743
  useEffect(() => {
@@ -3582,11 +3753,13 @@ function ImageCropper({
3582
3753
  }, [src]);
3583
3754
  const [imageSize, setImageSize] = useState(null);
3584
3755
  const [zoom, setZoom] = useState(minZoom);
3756
+ const zoomRef = useRef(zoom);
3585
3757
  const [offset, setOffset] = useState({ x: 0, y: 0 });
3586
3758
  const [dragging, setDragging] = useState(false);
3587
3759
  useEffect(() => {
3588
3760
  setImageSize(null);
3589
3761
  setZoom(minZoom);
3762
+ zoomRef.current = minZoom;
3590
3763
  setOffset({ x: 0, y: 0 });
3591
3764
  }, [src, minZoom]);
3592
3765
  const handleImageLoad = (event) => {
@@ -3601,6 +3774,7 @@ function ImageCropper({
3601
3774
  (next) => {
3602
3775
  const clampedZoom = Math.min(maxZoom, Math.max(minZoom, next));
3603
3776
  setZoom(clampedZoom);
3777
+ zoomRef.current = clampedZoom;
3604
3778
  if (imageSize) {
3605
3779
  setOffset((prev) => clampCropOffset(prev, imageSize, viewportSize, clampedZoom));
3606
3780
  }
@@ -3637,11 +3811,16 @@ function ImageCropper({
3637
3811
  setDragging(false);
3638
3812
  }
3639
3813
  };
3640
- const handleWheel = (event) => {
3641
- if (!imageSize) return;
3642
- event.preventDefault();
3643
- applyZoom(zoom - event.deltaY * WHEEL_ZOOM_FACTOR * zoom);
3644
- };
3814
+ useEffect(() => {
3815
+ const viewport = viewportRef.current;
3816
+ if (!viewport || !imageSize) return;
3817
+ const handleWheel = (event) => {
3818
+ event.preventDefault();
3819
+ applyZoom(zoomRef.current - event.deltaY * WHEEL_ZOOM_FACTOR * zoomRef.current);
3820
+ };
3821
+ viewport.addEventListener("wheel", handleWheel, { passive: false });
3822
+ return () => viewport.removeEventListener("wheel", handleWheel);
3823
+ }, [imageSize, applyZoom]);
3645
3824
  const handleKeyDown2 = (event) => {
3646
3825
  if (!imageSize) return;
3647
3826
  switch (event.key) {
@@ -3675,6 +3854,7 @@ function ImageCropper({
3675
3854
  () => ({
3676
3855
  reset: () => {
3677
3856
  setZoom(minZoom);
3857
+ zoomRef.current = minZoom;
3678
3858
  setOffset({ x: 0, y: 0 });
3679
3859
  },
3680
3860
  crop: () => new Promise((resolve, reject) => {
@@ -3722,92 +3902,95 @@ function ImageCropper({
3722
3902
  height: imageSize.height * scale,
3723
3903
  transform: `translate(calc(-50% + ${offset.x}px), calc(-50% + ${offset.y}px))`
3724
3904
  } : { visibility: "hidden" };
3725
- return /* @__PURE__ */ jsxs("div", { className: cn("image_cropper", className), children: [
3726
- /* @__PURE__ */ jsxs(
3727
- "div",
3728
- {
3729
- className: cn("image_cropper_viewport", dragging && "image_cropper_viewport_dragging"),
3730
- style: viewportStyle,
3731
- role: "group",
3732
- "aria-label": label,
3733
- "aria-describedby": "image_cropper_hint",
3734
- tabIndex: imageSize ? 0 : -1,
3735
- onPointerDown: handlePointerDown,
3736
- onPointerMove: handlePointerMove,
3737
- onPointerUp: handlePointerEnd,
3738
- onPointerCancel: handlePointerEnd,
3739
- onWheel: handleWheel,
3740
- onKeyDown: handleKeyDown2,
3741
- children: [
3742
- /* @__PURE__ */ jsx(
3743
- "img",
3744
- {
3745
- ref: imageRef,
3746
- src: previewUrl || void 0,
3747
- alt: "",
3748
- className: "image_cropper_image",
3749
- draggable: false,
3750
- crossOrigin: typeof src === "string" ? "anonymous" : void 0,
3751
- onLoad: handleImageLoad,
3752
- onError,
3753
- style: imageStyle
3754
- }
3755
- ),
3756
- /* @__PURE__ */ jsx(
3757
- "div",
3758
- {
3759
- className: cn(
3760
- "image_cropper_mask",
3761
- circular && "image_cropper_mask_circular",
3762
- dragging && "image_cropper_mask_active"
3763
- ),
3764
- "aria-hidden": "true"
3765
- }
3766
- )
3767
- ]
3768
- }
3769
- ),
3770
- /* @__PURE__ */ jsx("p", { id: "image_cropper_hint", className: "image_cropper_hint", children: "\uB4DC\uB798\uADF8(\uB610\uB294 \uBC29\uD5A5\uD0A4)\uB85C \uC704\uCE58, \uD720\xB7\uC2AC\uB77C\uC774\uB354\uB85C \uBC30\uC728\uC744 \uB9DE\uCD94\uC138\uC694." }),
3771
- /* @__PURE__ */ jsxs("div", { className: "image_cropper_zoom", children: [
3772
- /* @__PURE__ */ jsx(
3773
- "button",
3774
- {
3775
- type: "button",
3776
- className: "image_cropper_zoom_button",
3777
- "aria-label": "\uCD95\uC18C",
3778
- disabled: !imageSize || zoom <= minZoom,
3779
- onClick: () => applyZoom(zoom - ZOOM_KEY_STEP),
3780
- children: "\u2212"
3781
- }
3782
- ),
3783
- /* @__PURE__ */ jsx(
3784
- "input",
3905
+ return (
3906
+ // className 위에서 별도 구조분해되어 rest 에 없으므로 spread 순서와 무관하게 충돌하지 않는다.
3907
+ /* @__PURE__ */ jsxs("div", { ...rest, className: cn("image_cropper", className), children: [
3908
+ /* @__PURE__ */ jsxs(
3909
+ "div",
3785
3910
  {
3786
- type: "range",
3787
- className: "image_cropper_zoom_slider",
3788
- "aria-label": "\uBC30\uC728",
3789
- min: minZoom,
3790
- max: maxZoom,
3791
- step: ZOOM_STEP,
3792
- value: zoom,
3793
- disabled: !imageSize,
3794
- onChange: (event) => applyZoom(Number(event.target.value))
3911
+ ref: viewportRef,
3912
+ className: cn("image_cropper_viewport", dragging && "image_cropper_viewport_dragging"),
3913
+ style: viewportStyle,
3914
+ role: "group",
3915
+ "aria-label": label,
3916
+ "aria-describedby": hintId,
3917
+ tabIndex: imageSize ? 0 : -1,
3918
+ onPointerDown: handlePointerDown,
3919
+ onPointerMove: handlePointerMove,
3920
+ onPointerUp: handlePointerEnd,
3921
+ onPointerCancel: handlePointerEnd,
3922
+ onKeyDown: handleKeyDown2,
3923
+ children: [
3924
+ /* @__PURE__ */ jsx(
3925
+ "img",
3926
+ {
3927
+ ref: imageRef,
3928
+ src: previewUrl || void 0,
3929
+ alt: "",
3930
+ className: "image_cropper_image",
3931
+ draggable: false,
3932
+ crossOrigin: typeof src === "string" ? "anonymous" : void 0,
3933
+ onLoad: handleImageLoad,
3934
+ onError,
3935
+ style: imageStyle
3936
+ }
3937
+ ),
3938
+ /* @__PURE__ */ jsx(
3939
+ "div",
3940
+ {
3941
+ className: cn(
3942
+ "image_cropper_mask",
3943
+ circular && "image_cropper_mask_circular",
3944
+ dragging && "image_cropper_mask_active"
3945
+ ),
3946
+ "aria-hidden": "true"
3947
+ }
3948
+ )
3949
+ ]
3795
3950
  }
3796
3951
  ),
3797
- /* @__PURE__ */ jsx(
3798
- "button",
3799
- {
3800
- type: "button",
3801
- className: "image_cropper_zoom_button",
3802
- "aria-label": "\uD655\uB300",
3803
- disabled: !imageSize || zoom >= maxZoom,
3804
- onClick: () => applyZoom(zoom + ZOOM_KEY_STEP),
3805
- children: "\uFF0B"
3806
- }
3807
- )
3808
- ] }),
3809
- /* @__PURE__ */ jsx("span", { className: "image_cropper_sr_only", "aria-live": "polite", children: imageSize && !canPan ? "\uC774\uBBF8\uC9C0\uAC00 \uBDF0\uD3EC\uD2B8\uB97C \uB531 \uCC44\uC6CC \uC774\uB3D9 \uC5EC\uC720\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." : "" })
3810
- ] });
3952
+ /* @__PURE__ */ jsx("p", { id: hintId, className: "image_cropper_hint", children: hint }),
3953
+ /* @__PURE__ */ jsxs("div", { className: "image_cropper_zoom", children: [
3954
+ /* @__PURE__ */ jsx(
3955
+ "button",
3956
+ {
3957
+ type: "button",
3958
+ className: "image_cropper_zoom_button",
3959
+ "aria-label": zoomOutLabel,
3960
+ disabled: !imageSize || zoom <= minZoom,
3961
+ onClick: () => applyZoom(zoom - ZOOM_KEY_STEP),
3962
+ children: "\u2212"
3963
+ }
3964
+ ),
3965
+ /* @__PURE__ */ jsx(
3966
+ "input",
3967
+ {
3968
+ type: "range",
3969
+ className: "image_cropper_zoom_slider",
3970
+ "aria-label": zoomLabel,
3971
+ min: minZoom,
3972
+ max: maxZoom,
3973
+ step: ZOOM_STEP,
3974
+ value: zoom,
3975
+ disabled: !imageSize,
3976
+ onChange: (event) => applyZoom(Number(event.target.value))
3977
+ }
3978
+ ),
3979
+ /* @__PURE__ */ jsx(
3980
+ "button",
3981
+ {
3982
+ type: "button",
3983
+ className: "image_cropper_zoom_button",
3984
+ "aria-label": zoomInLabel,
3985
+ disabled: !imageSize || zoom >= maxZoom,
3986
+ onClick: () => applyZoom(zoom + ZOOM_KEY_STEP),
3987
+ children: "\uFF0B"
3988
+ }
3989
+ )
3990
+ ] }),
3991
+ /* @__PURE__ */ jsx("span", { className: "image_cropper_sr_only", "aria-live": "polite", children: imageSize && !canPan ? noPanHint : "" })
3992
+ ] })
3993
+ );
3811
3994
  }
3812
3995
  var OtpInput = ({
3813
3996
  length = 6,
@@ -3821,12 +4004,12 @@ var OtpInput = ({
3821
4004
  ariaLabel = "OTP \uC785\uB825",
3822
4005
  className
3823
4006
  }) => {
3824
- const inputsRef = React15.useRef([]);
3825
- const isTypingRef = React15.useRef(false);
3826
- React15.useEffect(() => {
4007
+ const inputsRef = React16.useRef([]);
4008
+ const isTypingRef = React16.useRef(false);
4009
+ React16.useEffect(() => {
3827
4010
  if (autoFocus) inputsRef.current[0]?.focus();
3828
4011
  }, [autoFocus]);
3829
- const digits = React15.useMemo(() => {
4012
+ const digits = React16.useMemo(() => {
3830
4013
  const chars = value.split("").slice(0, length);
3831
4014
  while (chars.length < length) chars.push("");
3832
4015
  return chars;
@@ -3903,7 +4086,7 @@ var OtpInput = ({
3903
4086
  focusInput(nextIndex);
3904
4087
  };
3905
4088
  const rootClassName = cn("otp_input", className);
3906
- const supportingId = React15.useId();
4089
+ const supportingId = React16.useId();
3907
4090
  return (
3908
4091
  // biome-ignore lint/a11y/useSemanticElements: <fieldset> would force border/legend styles; role=group is the WAI-ARIA equivalent for OTP grouping
3909
4092
  /* @__PURE__ */ jsxs("div", { className: rootClassName, role: "group", "aria-label": ariaLabel, children: [
@@ -3947,9 +4130,9 @@ var OtpInput = ({
3947
4130
  );
3948
4131
  };
3949
4132
  OtpInput.displayName = "OtpInput";
3950
- var RadioGroupContext = React15.createContext(null);
4133
+ var RadioGroupContext = React16.createContext(null);
3951
4134
  function useRadioGroupContext() {
3952
- return React15.useContext(RadioGroupContext);
4135
+ return React16.useContext(RadioGroupContext);
3953
4136
  }
3954
4137
  var RadioGroup = ({
3955
4138
  value: controlledValue,
@@ -3967,21 +4150,21 @@ var RadioGroup = ({
3967
4150
  ...props
3968
4151
  }) => {
3969
4152
  const isControlled = controlledValue !== void 0;
3970
- const [internalValue, setInternalValue] = React15.useState(defaultValue);
4153
+ const [internalValue, setInternalValue] = React16.useState(defaultValue);
3971
4154
  const value = isControlled ? controlledValue : internalValue;
3972
- const generatedName = React15.useId();
4155
+ const generatedName = React16.useId();
3973
4156
  const name = nameProp ?? generatedName;
3974
- const idPrefix = React15.useId();
4157
+ const idPrefix = React16.useId();
3975
4158
  const labelId = label ? `${idPrefix}-label` : void 0;
3976
4159
  const helperId = supportingText ? `${idPrefix}-help` : void 0;
3977
- const onChange = React15.useCallback(
4160
+ const onChange = React16.useCallback(
3978
4161
  (next) => {
3979
4162
  if (!isControlled) setInternalValue(next);
3980
4163
  onValueChange?.(next);
3981
4164
  },
3982
4165
  [isControlled, onValueChange]
3983
4166
  );
3984
- const ctx = React15.useMemo(
4167
+ const ctx = React16.useMemo(
3985
4168
  () => ({ name, value, onChange, size, disabled }),
3986
4169
  [name, value, onChange, size, disabled]
3987
4170
  );
@@ -4311,7 +4494,7 @@ var Toggle = ({
4311
4494
  ...props
4312
4495
  }) => {
4313
4496
  const isControlled = checked !== void 0;
4314
- const [innerChecked, setInnerChecked] = React15.useState(!!defaultChecked);
4497
+ const [innerChecked, setInnerChecked] = React16.useState(!!defaultChecked);
4315
4498
  const isOn = isControlled ? !!checked : innerChecked;
4316
4499
  const handleToggle = (e) => {
4317
4500
  props.onClick?.(e);
@@ -4400,7 +4583,7 @@ var Pagination = ({
4400
4583
  const emit = onPageChange ?? onChange;
4401
4584
  const prevDisabled = page <= 1;
4402
4585
  const nextDisabled = page >= totalPages;
4403
- const items = React15.useMemo(() => getPaginationItems(page, totalPages), [page, totalPages]);
4586
+ const items = React16.useMemo(() => getPaginationItems(page, totalPages), [page, totalPages]);
4404
4587
  return /* @__PURE__ */ jsxs("nav", { className: "pagination", "aria-label": "Pagination", children: [
4405
4588
  /* @__PURE__ */ jsx(
4406
4589
  "button",
@@ -4465,9 +4648,9 @@ var Drawer = ({
4465
4648
  className,
4466
4649
  ...props
4467
4650
  }) => {
4468
- const panelRef = React15.useRef(null);
4469
- const titleId = React15.useId();
4470
- const [shouldRender, setShouldRender] = React15.useState(open);
4651
+ const panelRef = React16.useRef(null);
4652
+ const titleId = React16.useId();
4653
+ const [shouldRender, setShouldRender] = React16.useState(open);
4471
4654
  const reduced = useReducedMotion();
4472
4655
  const isMounted = useIsMounted();
4473
4656
  useFocusTrap(panelRef, open && isMounted);
@@ -4486,7 +4669,7 @@ var Drawer = ({
4486
4669
  immediate: reduced,
4487
4670
  config: { tension: 280, friction: 28, clamp: !open }
4488
4671
  });
4489
- React15.useEffect(() => {
4672
+ React16.useEffect(() => {
4490
4673
  if (!open) return;
4491
4674
  const body = document.body;
4492
4675
  const openModals = parseInt(body.dataset.openModals || "0", 10);
@@ -4574,9 +4757,9 @@ var Modal = ({
4574
4757
  ariaLabel,
4575
4758
  ...props
4576
4759
  }) => {
4577
- const panelRef = React15.useRef(null);
4578
- const titleId = React15.useId();
4579
- const [shouldRender, setShouldRender] = React15.useState(open);
4760
+ const panelRef = React16.useRef(null);
4761
+ const titleId = React16.useId();
4762
+ const [shouldRender, setShouldRender] = React16.useState(open);
4580
4763
  const isMounted = useIsMounted();
4581
4764
  useFocusTrap(panelRef, open && isMounted);
4582
4765
  useOverlayEscape(open && isMounted, () => onClose?.());
@@ -4596,7 +4779,7 @@ var Modal = ({
4596
4779
  immediate: reduced,
4597
4780
  config: { tension: 280, friction: 28, clamp: !open }
4598
4781
  });
4599
- React15.useEffect(() => {
4782
+ React16.useEffect(() => {
4600
4783
  if (!open) return;
4601
4784
  const body = document.body;
4602
4785
  const openModals = parseInt(body.dataset.openModals || "0", 10);
@@ -4666,9 +4849,9 @@ var Modal = ({
4666
4849
  document.body
4667
4850
  );
4668
4851
  };
4669
- var ThemeContext = React15.createContext(null);
4852
+ var ThemeContext = React16.createContext(null);
4670
4853
  var useTheme = () => {
4671
- const ctx = React15.useContext(ThemeContext);
4854
+ const ctx = React16.useContext(ThemeContext);
4672
4855
  if (!ctx) {
4673
4856
  throw new Error(
4674
4857
  '[Bigtablet DS] useTheme\uB294 <ThemeProvider> \uC548\uC5D0\uC11C\uB9CC \uC0AC\uC6A9 \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n\n\uC571 \uCD5C\uC0C1\uB2E8\uC5D0 <ThemeProvider>\uB85C \uAC10\uC2F8\uC8FC\uC138\uC694:\n <ThemeProvider mode="system">\n <YourApp />\n </ThemeProvider>'
@@ -4692,15 +4875,15 @@ var ThemeProvider = ({
4692
4875
  targetSelector,
4693
4876
  children
4694
4877
  }) => {
4695
- const [mode, setModeState] = React15.useState(defaultMode);
4696
- const [systemDark, setSystemDark] = React15.useState(false);
4697
- React15.useEffect(() => {
4878
+ const [mode, setModeState] = React16.useState(defaultMode);
4879
+ const [systemDark, setSystemDark] = React16.useState(false);
4880
+ React16.useEffect(() => {
4698
4881
  setModeState(readStored(storageKey) ?? defaultMode);
4699
4882
  if (isClient) {
4700
4883
  setSystemDark(window.matchMedia("(prefers-color-scheme: dark)").matches);
4701
4884
  }
4702
4885
  }, [storageKey, defaultMode]);
4703
- React15.useEffect(() => {
4886
+ React16.useEffect(() => {
4704
4887
  if (!isClient) return;
4705
4888
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
4706
4889
  const handler = (e) => setSystemDark(e.matches);
@@ -4708,7 +4891,7 @@ var ThemeProvider = ({
4708
4891
  return () => mq.removeEventListener("change", handler);
4709
4892
  }, []);
4710
4893
  const resolved = mode === "system" ? systemDark ? "dark" : "light" : mode;
4711
- React15.useEffect(() => {
4894
+ React16.useEffect(() => {
4712
4895
  if (!isClient) return;
4713
4896
  const target = targetSelector ? document.querySelector(targetSelector) : document.documentElement;
4714
4897
  if (!target) return;
@@ -4718,7 +4901,7 @@ var ThemeProvider = ({
4718
4901
  target.setAttribute("data-theme", mode);
4719
4902
  }
4720
4903
  }, [mode, targetSelector]);
4721
- const setMode = React15.useCallback(
4904
+ const setMode = React16.useCallback(
4722
4905
  (next) => {
4723
4906
  setModeState(next);
4724
4907
  if (storageKey && isClient) {
@@ -4730,7 +4913,7 @@ var ThemeProvider = ({
4730
4913
  },
4731
4914
  [storageKey]
4732
4915
  );
4733
- const value = React15.useMemo(
4916
+ const value = React16.useMemo(
4734
4917
  () => ({ mode, resolved, setMode }),
4735
4918
  [mode, resolved, setMode]
4736
4919
  );
@@ -4843,4 +5026,4 @@ var Stack = ({
4843
5026
  );
4844
5027
  };
4845
5028
 
4846
- export { Accordion, AlertProvider, Avatar, Badge, BottomNav, BottomNavItem, BottomNavSpacer, Breadcrumb, Button, Card, Checkbox, Chip, Container, DatePicker, Divider, Drawer, Dropdown, EmptyState, ErrorState, FileInput, Grid, Hero, Icon, IconButton, ImageCropper, LinearProgress, ListItem, MediaCard, Menu, Modal, NavBar, NavLink, OtpInput, Pagination, Popover, Radio, RadioGroup, Section, Sidebar, SidebarItem, SidebarSection, Skeleton, Spinner, Stack, Tab, TabList, TabPanel, Table, Tabs, TextField, Textarea, ThemeProvider, ToastProvider, Toggle, Tooltip, TopLoading, a11y, baseBorderWidth, baseColors, baseTypography, borderWidth, breakpoints, cn, colors, elevation, iconSize, motion, opacity, radius, skeleton, spacing, typography, useAlert, useFocusTrap, useReducedMotion, useSpringHover, useSpringPresence, useTheme, useToast, zIndex };
5029
+ export { Accordion, AlertProvider, Avatar, Badge, BottomNav, BottomNavItem, BottomNavSpacer, Breadcrumb, Button, Card, Checkbox, Chip, Container, DatePicker, Divider, Drawer, Dropdown, EmptyState, ErrorState, FileInput, Grid, Hero, Icon, IconButton, ImageCropper, LinearProgress, ListItem, MediaCard, Menu, Modal, NavBar, NavLink, OtpInput, Pagination, Popover, Radio, RadioGroup, Section, Sidebar, SidebarItem, SidebarSection, Skeleton, Spinner, Stack, Tab, TabList, TabPanel, Table, Tabs, TextField, Textarea, ThemeProvider, ToastProvider, Toggle, Tooltip, TopLoading, a11y, baseBorderWidth, baseColors, baseTypography, borderWidth, breakpoints, cn, colors, elevation, iconSize, motion, opacity, radius, skeleton, spacing, typography, useAlert, useFocusTrap, useRadioGroupContext, useReducedMotion, useSpringHover, useSpringPresence, useTheme, useToast, zIndex };