@x-plat/design-system 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/components/AutoResizeTextArea/index.cjs +14 -5
  2. package/dist/components/AutoResizeTextArea/index.css +15 -0
  3. package/dist/components/AutoResizeTextArea/index.d.cts +4 -0
  4. package/dist/components/AutoResizeTextArea/index.d.ts +4 -0
  5. package/dist/components/AutoResizeTextArea/index.js +14 -5
  6. package/dist/components/Badge/index.d.cts +1 -1
  7. package/dist/components/Badge/index.d.ts +1 -1
  8. package/dist/components/ChatInput/index.cjs +20 -7
  9. package/dist/components/ChatInput/index.css +15 -0
  10. package/dist/components/ChatInput/index.d.cts +4 -0
  11. package/dist/components/ChatInput/index.d.ts +4 -0
  12. package/dist/components/ChatInput/index.js +20 -7
  13. package/dist/components/CheckBox/index.cjs +8 -3
  14. package/dist/components/CheckBox/index.css +15 -0
  15. package/dist/components/CheckBox/index.d.cts +1 -1
  16. package/dist/components/CheckBox/index.d.ts +1 -1
  17. package/dist/components/CheckBox/index.js +8 -3
  18. package/dist/components/DatePicker/index.cjs +179 -64
  19. package/dist/components/DatePicker/index.css +19 -0
  20. package/dist/components/DatePicker/index.js +179 -64
  21. package/dist/components/Drawer/index.cjs +83 -4
  22. package/dist/components/Drawer/index.js +83 -4
  23. package/dist/components/Dropdown/index.cjs +57 -16
  24. package/dist/components/Dropdown/index.css +3 -0
  25. package/dist/components/Dropdown/index.js +57 -16
  26. package/dist/components/Editor/index.cjs +779 -50
  27. package/dist/components/Editor/index.css +100 -0
  28. package/dist/components/Editor/index.d.cts +1 -1
  29. package/dist/components/Editor/index.d.ts +1 -1
  30. package/dist/components/Editor/index.js +779 -50
  31. package/dist/components/FieldMessage/index.cjs +8 -3
  32. package/dist/components/FieldMessage/index.css +15 -0
  33. package/dist/components/FieldMessage/index.d.cts +11 -4
  34. package/dist/components/FieldMessage/index.d.ts +11 -4
  35. package/dist/components/FieldMessage/index.js +8 -3
  36. package/dist/components/FileUpload/index.cjs +8 -3
  37. package/dist/components/FileUpload/index.css +15 -0
  38. package/dist/components/FileUpload/index.js +8 -3
  39. package/dist/components/ImageSelector/index.cjs +8 -3
  40. package/dist/components/ImageSelector/index.css +15 -0
  41. package/dist/components/ImageSelector/index.js +8 -3
  42. package/dist/components/Input/index.cjs +15 -5
  43. package/dist/components/Input/index.css +15 -0
  44. package/dist/components/Input/index.d.cts +4 -0
  45. package/dist/components/Input/index.d.ts +4 -0
  46. package/dist/components/Input/index.js +15 -5
  47. package/dist/components/Modal/index.cjs +84 -5
  48. package/dist/components/Modal/index.js +84 -5
  49. package/dist/components/NotificationBadge/index.d.cts +1 -1
  50. package/dist/components/NotificationBadge/index.d.ts +1 -1
  51. package/dist/components/Pagination/index.d.cts +1 -1
  52. package/dist/components/Pagination/index.d.ts +1 -1
  53. package/dist/components/PopOver/index.cjs +55 -13
  54. package/dist/components/PopOver/index.css +3 -0
  55. package/dist/components/PopOver/index.js +55 -13
  56. package/dist/components/Progress/index.d.cts +1 -1
  57. package/dist/components/Progress/index.d.ts +1 -1
  58. package/dist/components/Radio/index.cjs +8 -3
  59. package/dist/components/Radio/index.css +15 -0
  60. package/dist/components/Radio/index.d.cts +1 -1
  61. package/dist/components/Radio/index.d.ts +1 -1
  62. package/dist/components/Radio/index.js +8 -3
  63. package/dist/components/Select/index.cjs +120 -39
  64. package/dist/components/Select/index.css +57 -0
  65. package/dist/components/Select/index.d.cts +11 -0
  66. package/dist/components/Select/index.d.ts +11 -0
  67. package/dist/components/Select/index.js +120 -39
  68. package/dist/components/Spinner/index.d.cts +1 -1
  69. package/dist/components/Spinner/index.d.ts +1 -1
  70. package/dist/components/Stat/index.cjs +84 -0
  71. package/dist/components/Stat/index.css +96 -0
  72. package/dist/components/Stat/index.d.cts +37 -0
  73. package/dist/components/Stat/index.d.ts +37 -0
  74. package/dist/components/Stat/index.js +57 -0
  75. package/dist/components/Steps/index.d.cts +1 -1
  76. package/dist/components/Steps/index.d.ts +1 -1
  77. package/dist/components/Switch/index.cjs +14 -5
  78. package/dist/components/Switch/index.css +15 -0
  79. package/dist/components/Switch/index.d.cts +6 -1
  80. package/dist/components/Switch/index.d.ts +6 -1
  81. package/dist/components/Switch/index.js +14 -5
  82. package/dist/components/Tab/index.cjs +49 -25
  83. package/dist/components/Tab/index.css +4 -0
  84. package/dist/components/Tab/index.js +49 -25
  85. package/dist/components/TextArea/index.cjs +14 -4
  86. package/dist/components/TextArea/index.css +15 -0
  87. package/dist/components/TextArea/index.d.cts +4 -0
  88. package/dist/components/TextArea/index.d.ts +4 -0
  89. package/dist/components/TextArea/index.js +14 -4
  90. package/dist/components/TimePicker/index.cjs +461 -0
  91. package/dist/components/TimePicker/index.css +175 -0
  92. package/dist/components/TimePicker/index.d.cts +42 -0
  93. package/dist/components/TimePicker/index.d.ts +42 -0
  94. package/dist/components/TimePicker/index.js +424 -0
  95. package/dist/components/index.cjs +1500 -398
  96. package/dist/components/index.css +385 -0
  97. package/dist/components/index.d.cts +3 -1
  98. package/dist/components/index.d.ts +3 -1
  99. package/dist/components/index.js +1498 -398
  100. package/dist/index.cjs +1524 -422
  101. package/dist/index.css +385 -0
  102. package/dist/index.d.cts +3 -1
  103. package/dist/index.d.ts +3 -1
  104. package/dist/index.js +1522 -422
  105. package/dist/status-njWqQWFV.d.cts +20 -0
  106. package/dist/status-njWqQWFV.d.ts +20 -0
  107. package/guidelines/AGENT_PROMPT.md +3 -3
  108. package/guidelines/Guidelines.md +3 -0
  109. package/guidelines/components/editor.md +141 -0
  110. package/guidelines/components/form.md +19 -0
  111. package/guidelines/components/stat.md +31 -0
  112. package/guidelines/components/time-picker.md +49 -0
  113. package/package.json +1 -1
  114. package/dist/status-g_KP3qT6.d.cts +0 -8
  115. package/dist/status-g_KP3qT6.d.ts +0 -8
@@ -1,5 +1,5 @@
1
1
  // src/components/DatePicker/InputDatePicker/index.tsx
2
- import React6 from "react";
2
+ import React7 from "react";
3
3
 
4
4
  // ../../node_modules/clsx/dist/clsx.mjs
5
5
  function r(e) {
@@ -167,11 +167,16 @@ var FieldMessage = (props) => {
167
167
  FieldMessage.displayName = "FieldMessage";
168
168
  var FieldMessage_default = FieldMessage;
169
169
  var Field = (props) => {
170
- const { validations, children } = props;
171
- if (!validations || validations.length === 0) return /* @__PURE__ */ jsx5(Fragment, { children });
170
+ const { label, required = false, htmlFor, validations, children } = props;
171
+ const hasMessages = !!validations && validations.length > 0;
172
+ if (!label && !hasMessages) return /* @__PURE__ */ jsx5(Fragment, { children });
172
173
  return /* @__PURE__ */ jsxs3("div", { className: "lib-xplat-field", children: [
174
+ label && /* @__PURE__ */ jsxs3("label", { className: "lib-xplat-field-label", htmlFor, children: [
175
+ label,
176
+ required && /* @__PURE__ */ jsx5("span", { className: "required", "aria-hidden": "true", children: "*" })
177
+ ] }),
173
178
  children,
174
- /* @__PURE__ */ jsx5(FieldMessage, { validations })
179
+ hasMessages && /* @__PURE__ */ jsx5(FieldMessage, { validations })
175
180
  ] });
176
181
  };
177
182
  Field.displayName = "Field";
@@ -270,8 +275,12 @@ var Input = React.forwardRef((props, ref) => {
270
275
  suffix,
271
276
  disabled,
272
277
  validations,
278
+ label,
279
+ required,
273
280
  ...inputProps
274
281
  } = props;
282
+ const autoId = React.useId();
283
+ const fieldId = inputProps.id ?? autoId;
275
284
  const localValue = formatValue(type, value);
276
285
  const handleChange = (e) => {
277
286
  let val = e.target.value;
@@ -287,7 +296,7 @@ var Input = React.forwardRef((props, ref) => {
287
296
  onChange(event);
288
297
  }
289
298
  };
290
- return /* @__PURE__ */ jsxs4("div", { className: clsx_default("lib-xplat-input-wrap", className), children: [
299
+ return /* @__PURE__ */ jsx6(Field, { label, required, htmlFor: fieldId, children: /* @__PURE__ */ jsxs4("div", { className: clsx_default("lib-xplat-input-wrap", className), children: [
291
300
  /* @__PURE__ */ jsxs4(
292
301
  "div",
293
302
  {
@@ -298,6 +307,7 @@ var Input = React.forwardRef((props, ref) => {
298
307
  {
299
308
  ...inputProps,
300
309
  ref,
310
+ id: fieldId,
301
311
  disabled,
302
312
  type: type === "number" ? "text" : type,
303
313
  value: localValue,
@@ -310,7 +320,7 @@ var Input = React.forwardRef((props, ref) => {
310
320
  }
311
321
  ),
312
322
  /* @__PURE__ */ jsx6(FieldMessage_default, { validations })
313
- ] });
323
+ ] }) });
314
324
  });
315
325
  Input.displayName = "Input";
316
326
  var Input_default = Input;
@@ -407,16 +417,96 @@ var PasswordInput = React2.forwardRef(
407
417
  PasswordInput.displayName = "PasswordInput";
408
418
 
409
419
  // src/components/Modal/Modal.tsx
410
- import React3 from "react";
420
+ import React4 from "react";
411
421
  import { createPortal } from "react-dom";
422
+
423
+ // src/tokens/hooks/useOverlayKeyboard.ts
424
+ import React3 from "react";
425
+ var FOCUSABLE = [
426
+ "a[href]",
427
+ "button:not([disabled])",
428
+ "input:not([disabled]):not([type='hidden'])",
429
+ "select:not([disabled])",
430
+ "textarea:not([disabled])",
431
+ "[tabindex]:not([tabindex='-1'])"
432
+ ].join(",");
433
+ var focusableIn = (el) => Array.from(el.querySelectorAll(FOCUSABLE)).filter(
434
+ // 화면에 없는 것은 제외 (display:none 등)
435
+ (n) => n.offsetWidth > 0 || n.offsetHeight > 0 || n === document.activeElement
436
+ );
437
+ var useEscapeKey = (enabled, onEscape) => {
438
+ React3.useEffect(() => {
439
+ if (!enabled) return;
440
+ const onKey = (e) => {
441
+ if (e.key !== "Escape") return;
442
+ if (e.defaultPrevented) return;
443
+ e.preventDefault();
444
+ onEscape();
445
+ };
446
+ document.addEventListener("keydown", onKey);
447
+ return () => document.removeEventListener("keydown", onKey);
448
+ }, [enabled, onEscape]);
449
+ };
450
+ var useFocusTrap = (containerRef, enabled, options = {}) => {
451
+ const { trap = false, initialFocus } = options;
452
+ const restoreRef = React3.useRef(null);
453
+ React3.useEffect(() => {
454
+ if (!enabled) return;
455
+ restoreRef.current = document.activeElement;
456
+ const raf = requestAnimationFrame(() => {
457
+ const el = containerRef.current;
458
+ if (!el) return;
459
+ const target = initialFocus?.current ?? focusableIn(el)[0] ?? el;
460
+ if (!target.hasAttribute("tabindex") && !focusableIn(el).includes(target)) {
461
+ target.setAttribute("tabindex", "-1");
462
+ }
463
+ target.focus({ preventScroll: true });
464
+ });
465
+ return () => {
466
+ cancelAnimationFrame(raf);
467
+ const prev = restoreRef.current;
468
+ if (prev && document.contains(prev)) prev.focus({ preventScroll: true });
469
+ };
470
+ }, [enabled, containerRef, initialFocus]);
471
+ React3.useEffect(() => {
472
+ if (!enabled || !trap) return;
473
+ const onKey = (e) => {
474
+ if (e.key !== "Tab") return;
475
+ const el = containerRef.current;
476
+ if (!el) return;
477
+ const items = focusableIn(el);
478
+ if (items.length === 0) {
479
+ e.preventDefault();
480
+ return;
481
+ }
482
+ const first = items[0];
483
+ const last = items[items.length - 1];
484
+ const active = document.activeElement;
485
+ if (e.shiftKey && (active === first || !el.contains(active))) {
486
+ e.preventDefault();
487
+ last.focus();
488
+ } else if (!e.shiftKey && (active === last || !el.contains(active))) {
489
+ e.preventDefault();
490
+ first.focus();
491
+ }
492
+ };
493
+ document.addEventListener("keydown", onKey);
494
+ return () => document.removeEventListener("keydown", onKey);
495
+ }, [enabled, trap, containerRef]);
496
+ };
497
+
498
+ // src/components/Modal/Modal.tsx
412
499
  import { jsx as jsx10 } from "react/jsx-runtime";
413
500
  var ANIMATION_DURATION_MS = 200;
414
501
  var Modal = (props) => {
415
502
  const { isOpen, onClose, children } = props;
416
- const [mounted, setMounted] = React3.useState(false);
417
- const [visible, setVisible] = React3.useState(false);
418
- const [settled, setSettled] = React3.useState(false);
419
- React3.useEffect(() => {
503
+ const [mounted, setMounted] = React4.useState(false);
504
+ const [visible, setVisible] = React4.useState(false);
505
+ const [settled, setSettled] = React4.useState(false);
506
+ const boxRef = React4.useRef(null);
507
+ useEscapeKey(isOpen, onClose);
508
+ useFocusTrap(boxRef, isOpen && mounted, { trap: true });
509
+ React4.useEffect(() => {
420
510
  if (isOpen) {
421
511
  setMounted(true);
422
512
  const show = setTimeout(() => setVisible(true), 1);
@@ -443,6 +533,7 @@ var Modal = (props) => {
443
533
  children: /* @__PURE__ */ jsx10(
444
534
  "div",
445
535
  {
536
+ ref: boxRef,
446
537
  className: clsx_default("lib-xplat-modal", "modal-box", stateClass, settled && "settled"),
447
538
  role: "dialog",
448
539
  "aria-modal": "true",
@@ -1370,15 +1461,15 @@ import { jsx as jsx298, jsxs as jsxs189 } from "react/jsx-runtime";
1370
1461
  import { jsx as jsx299, jsxs as jsxs190 } from "react/jsx-runtime";
1371
1462
 
1372
1463
  // src/components/DatePicker/SingleDatePicker/index.tsx
1373
- import React5 from "react";
1464
+ import React6 from "react";
1374
1465
 
1375
1466
  // src/components/Calendar/useCalendar.ts
1376
- import React4 from "react";
1467
+ import React5 from "react";
1377
1468
  var useCalendar = (initialYear, initialMonth) => {
1378
1469
  const today = /* @__PURE__ */ new Date();
1379
- const [year, setYear] = React4.useState(initialYear ?? today.getFullYear());
1380
- const [month, setMonth] = React4.useState(initialMonth ?? today.getMonth());
1381
- const days = React4.useMemo(() => {
1470
+ const [year, setYear] = React5.useState(initialYear ?? today.getFullYear());
1471
+ const [month, setMonth] = React5.useState(initialMonth ?? today.getMonth());
1472
+ const days = React5.useMemo(() => {
1382
1473
  const firstDay = new Date(year, month, 1);
1383
1474
  const lastDay = new Date(year, month + 1, 0);
1384
1475
  const startDayOfWeek = firstDay.getDay();
@@ -1421,7 +1512,7 @@ var useCalendar = (initialYear, initialMonth) => {
1421
1512
  }
1422
1513
  return result;
1423
1514
  }, [year, month]);
1424
- const goToPrevMonth = React4.useCallback(() => {
1515
+ const goToPrevMonth = React5.useCallback(() => {
1425
1516
  setMonth((prev) => {
1426
1517
  if (prev === 0) {
1427
1518
  setYear((y) => y - 1);
@@ -1430,7 +1521,7 @@ var useCalendar = (initialYear, initialMonth) => {
1430
1521
  return prev - 1;
1431
1522
  });
1432
1523
  }, []);
1433
- const goToNextMonth = React4.useCallback(() => {
1524
+ const goToNextMonth = React5.useCallback(() => {
1434
1525
  setMonth((prev) => {
1435
1526
  if (prev === 11) {
1436
1527
  setYear((y) => y + 1);
@@ -1439,7 +1530,7 @@ var useCalendar = (initialYear, initialMonth) => {
1439
1530
  return prev + 1;
1440
1531
  });
1441
1532
  }, []);
1442
- const goToToday = React4.useCallback(() => {
1533
+ const goToToday = React5.useCallback(() => {
1443
1534
  setYear(today.getFullYear());
1444
1535
  setMonth(today.getMonth());
1445
1536
  }, []);
@@ -1474,7 +1565,7 @@ var MONTH_LABELS = {
1474
1565
 
1475
1566
  // src/components/DatePicker/SingleDatePicker/index.tsx
1476
1567
  import { Fragment as Fragment2, jsx as jsx300, jsxs as jsxs191 } from "react/jsx-runtime";
1477
- var DayCell = React5.memo(
1568
+ var DayCell = React6.memo(
1478
1569
  ({
1479
1570
  day,
1480
1571
  disabled,
@@ -1526,26 +1617,26 @@ var SingleDatePicker = (props) => {
1526
1617
  initialYear,
1527
1618
  initialMonth
1528
1619
  );
1529
- const [pickerMode, setPickerMode] = React5.useState("days");
1530
- const [yearRangeStart, setYearRangeStart] = React5.useState(
1620
+ const [pickerMode, setPickerMode] = React6.useState("days");
1621
+ const [yearRangeStart, setYearRangeStart] = React6.useState(
1531
1622
  Math.floor((initialYear ?? (/* @__PURE__ */ new Date()).getFullYear()) / 12) * 12
1532
1623
  );
1533
- const minTime = React5.useMemo(
1624
+ const minTime = React6.useMemo(
1534
1625
  () => minDate ? new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate()).getTime() : -Infinity,
1535
1626
  [minDate]
1536
1627
  );
1537
- const maxTime = React5.useMemo(
1628
+ const maxTime = React6.useMemo(
1538
1629
  () => maxDate ? new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate()).getTime() : Infinity,
1539
1630
  [maxDate]
1540
1631
  );
1541
- const highlightSet = React5.useMemo(() => {
1632
+ const highlightSet = React6.useMemo(() => {
1542
1633
  const set = /* @__PURE__ */ new Set();
1543
1634
  for (const h of highlightDates) {
1544
1635
  set.add(`${h.getFullYear()}-${h.getMonth()}-${h.getDate()}`);
1545
1636
  }
1546
1637
  return set;
1547
1638
  }, [highlightDates]);
1548
- const handleSelect = React5.useCallback(
1639
+ const handleSelect = React6.useCallback(
1549
1640
  (date) => {
1550
1641
  onChange?.(date);
1551
1642
  },
@@ -1674,8 +1765,8 @@ var formatDate = (date) => {
1674
1765
  };
1675
1766
  var InputDatePicker = (props) => {
1676
1767
  const { value, onChange, minDate, maxDate, disabled, locale = "ko", placeholder } = props;
1677
- const [isOpen, setIsOpen] = React6.useState(false);
1678
- const [tempDate, setTempDate] = React6.useState(value ?? /* @__PURE__ */ new Date());
1768
+ const [isOpen, setIsOpen] = React7.useState(false);
1769
+ const [tempDate, setTempDate] = React7.useState(value ?? /* @__PURE__ */ new Date());
1679
1770
  const handleOpen = () => {
1680
1771
  if (disabled) return;
1681
1772
  setTempDate(value ?? /* @__PURE__ */ new Date());
@@ -1724,23 +1815,27 @@ InputDatePicker.displayName = "InputDatePicker";
1724
1815
  var InputDatePicker_default = InputDatePicker;
1725
1816
 
1726
1817
  // src/components/DatePicker/PopupPicker/index.tsx
1727
- import React10 from "react";
1818
+ import React11 from "react";
1728
1819
 
1729
1820
  // src/components/DatePicker/RangePicker/index.tsx
1730
- import React9 from "react";
1821
+ import React10 from "react";
1731
1822
 
1732
1823
  // src/components/Tab/Tab.tsx
1733
- import React8 from "react";
1824
+ import React9 from "react";
1734
1825
 
1735
1826
  // src/components/Tab/TabItem.tsx
1736
- import React7 from "react";
1827
+ import React8 from "react";
1737
1828
  import { jsx as jsx302 } from "react/jsx-runtime";
1738
- var TabItem = React7.forwardRef((props, ref) => {
1829
+ var TabItem = React8.forwardRef((props, ref) => {
1739
1830
  const { isActive, title, onClick } = props;
1740
1831
  return /* @__PURE__ */ jsx302(
1741
- "div",
1832
+ "button",
1742
1833
  {
1743
1834
  ref,
1835
+ type: "button",
1836
+ role: "tab",
1837
+ "aria-selected": isActive,
1838
+ tabIndex: isActive ? 0 : -1,
1744
1839
  className: clsx_default("tab-item", isActive ? "active" : null),
1745
1840
  onClick,
1746
1841
  children: title
@@ -1755,44 +1850,64 @@ import { jsx as jsx303, jsxs as jsxs193 } from "react/jsx-runtime";
1755
1850
  var Tab = (props) => {
1756
1851
  const { activeIndex, onChange, tabs, variant, type, size = "md" } = props;
1757
1852
  const shape = variant ?? type ?? "default";
1758
- const [underlineStyle, setUnderlineStyle] = React8.useState({
1853
+ const [underlineStyle, setUnderlineStyle] = React9.useState({
1759
1854
  left: 0,
1760
1855
  width: 0
1761
1856
  });
1762
- const itemRefs = React8.useRef([]);
1857
+ const itemRefs = React9.useRef([]);
1858
+ const handleKeyDown = (e) => {
1859
+ const last = tabs.length - 1;
1860
+ let next = null;
1861
+ if (e.key === "ArrowRight") next = activeIndex >= last ? 0 : activeIndex + 1;
1862
+ else if (e.key === "ArrowLeft") next = activeIndex <= 0 ? last : activeIndex - 1;
1863
+ else if (e.key === "Home") next = 0;
1864
+ else if (e.key === "End") next = last;
1865
+ if (next === null) return;
1866
+ e.preventDefault();
1867
+ onChange(tabs[next], next);
1868
+ itemRefs.current[next]?.focus();
1869
+ };
1763
1870
  const handleChangeActiveTab = (tabItem, tabIdx) => {
1764
1871
  onChange(tabItem, tabIdx);
1765
1872
  };
1766
- React8.useEffect(() => {
1873
+ React9.useEffect(() => {
1767
1874
  const el = itemRefs.current[activeIndex];
1768
1875
  if (el) {
1769
1876
  setUnderlineStyle({ left: el.offsetLeft, width: el.offsetWidth });
1770
1877
  }
1771
1878
  }, [activeIndex, tabs.length]);
1772
- return /* @__PURE__ */ jsxs193("div", { className: clsx_default("lib-xplat-tab", `type-${shape}`, size), children: [
1773
- tabs.map((tab, idx) => /* @__PURE__ */ jsx303(
1774
- TabItem_default,
1775
- {
1776
- onClick: () => handleChangeActiveTab(tab, idx),
1777
- isActive: activeIndex === idx,
1778
- ref: (el) => {
1779
- itemRefs.current[idx] = el;
1780
- },
1781
- title: tab.title
1782
- },
1783
- `${tab.value}_${idx}`
1784
- )),
1785
- shape === "toggle" && /* @__PURE__ */ jsx303(
1786
- "div",
1787
- {
1788
- className: "tab-toggle-underline",
1789
- style: {
1790
- left: underlineStyle.left,
1791
- width: underlineStyle.width
1792
- }
1793
- }
1794
- )
1795
- ] });
1879
+ return /* @__PURE__ */ jsxs193(
1880
+ "div",
1881
+ {
1882
+ className: clsx_default("lib-xplat-tab", `type-${shape}`, size),
1883
+ role: "tablist",
1884
+ onKeyDown: handleKeyDown,
1885
+ children: [
1886
+ tabs.map((tab, idx) => /* @__PURE__ */ jsx303(
1887
+ TabItem_default,
1888
+ {
1889
+ onClick: () => handleChangeActiveTab(tab, idx),
1890
+ isActive: activeIndex === idx,
1891
+ ref: (el) => {
1892
+ itemRefs.current[idx] = el;
1893
+ },
1894
+ title: tab.title
1895
+ },
1896
+ `${tab.value}_${idx}`
1897
+ )),
1898
+ shape === "toggle" && /* @__PURE__ */ jsx303(
1899
+ "div",
1900
+ {
1901
+ className: "tab-toggle-underline",
1902
+ style: {
1903
+ left: underlineStyle.left,
1904
+ width: underlineStyle.width
1905
+ }
1906
+ }
1907
+ )
1908
+ ]
1909
+ }
1910
+ );
1796
1911
  };
1797
1912
  Tab.displayName = "Tab";
1798
1913
  var Tab_default = Tab;
@@ -1808,7 +1923,7 @@ var RangePicker = (props) => {
1808
1923
  maxDate,
1809
1924
  locale = "ko"
1810
1925
  } = props;
1811
- const [activeTab, setActiveTab] = React9.useState("start");
1926
+ const [activeTab, setActiveTab] = React10.useState("start");
1812
1927
  const handleStartChange = (date) => {
1813
1928
  if (!date) return;
1814
1929
  const newStart = date > endDate ? endDate : date;
@@ -1893,7 +2008,7 @@ var RangePicker_default = RangePicker;
1893
2008
  import { jsx as jsx305, jsxs as jsxs195 } from "react/jsx-runtime";
1894
2009
  var PopupPicker = (props) => {
1895
2010
  const { component, type, locale } = props;
1896
- const [isOpen, setIsOpen] = React10.useState(false);
2011
+ const [isOpen, setIsOpen] = React11.useState(false);
1897
2012
  const handleClick = () => setIsOpen(true);
1898
2013
  const handleClose = () => setIsOpen(false);
1899
2014
  const handleSingleChange = (date) => {
@@ -1902,7 +2017,7 @@ var PopupPicker = (props) => {
1902
2017
  handleClose();
1903
2018
  };
1904
2019
  return /* @__PURE__ */ jsxs195("div", { className: "lib-xplat-popup-datepicker", children: [
1905
- React10.cloneElement(component, { onClick: handleClick }),
2020
+ React11.cloneElement(component, { onClick: handleClick }),
1906
2021
  /* @__PURE__ */ jsx305(Modal_default, { isOpen, onClose: handleClose, children: /* @__PURE__ */ jsxs195("div", { className: clsx_default("lib-xplat-popup-datepicker-card", type === "range" && "range-mode"), children: [
1907
2022
  /* @__PURE__ */ jsxs195("div", { className: "popup-datepicker-content", children: [
1908
2023
  type === "single" && /* @__PURE__ */ jsx305(
@@ -35,9 +35,84 @@ __export(Drawer_exports, {
35
35
  module.exports = __toCommonJS(Drawer_exports);
36
36
 
37
37
  // src/components/Drawer/Drawer.tsx
38
- var import_react = __toESM(require("react"), 1);
38
+ var import_react2 = __toESM(require("react"), 1);
39
39
  var import_react_dom = require("react-dom");
40
40
 
41
+ // src/tokens/hooks/useOverlayKeyboard.ts
42
+ var import_react = __toESM(require("react"), 1);
43
+ var FOCUSABLE = [
44
+ "a[href]",
45
+ "button:not([disabled])",
46
+ "input:not([disabled]):not([type='hidden'])",
47
+ "select:not([disabled])",
48
+ "textarea:not([disabled])",
49
+ "[tabindex]:not([tabindex='-1'])"
50
+ ].join(",");
51
+ var focusableIn = (el) => Array.from(el.querySelectorAll(FOCUSABLE)).filter(
52
+ // 화면에 없는 것은 제외 (display:none 등)
53
+ (n) => n.offsetWidth > 0 || n.offsetHeight > 0 || n === document.activeElement
54
+ );
55
+ var useEscapeKey = (enabled, onEscape) => {
56
+ import_react.default.useEffect(() => {
57
+ if (!enabled) return;
58
+ const onKey = (e) => {
59
+ if (e.key !== "Escape") return;
60
+ if (e.defaultPrevented) return;
61
+ e.preventDefault();
62
+ onEscape();
63
+ };
64
+ document.addEventListener("keydown", onKey);
65
+ return () => document.removeEventListener("keydown", onKey);
66
+ }, [enabled, onEscape]);
67
+ };
68
+ var useFocusTrap = (containerRef, enabled, options = {}) => {
69
+ const { trap = false, initialFocus } = options;
70
+ const restoreRef = import_react.default.useRef(null);
71
+ import_react.default.useEffect(() => {
72
+ if (!enabled) return;
73
+ restoreRef.current = document.activeElement;
74
+ const raf = requestAnimationFrame(() => {
75
+ const el = containerRef.current;
76
+ if (!el) return;
77
+ const target = initialFocus?.current ?? focusableIn(el)[0] ?? el;
78
+ if (!target.hasAttribute("tabindex") && !focusableIn(el).includes(target)) {
79
+ target.setAttribute("tabindex", "-1");
80
+ }
81
+ target.focus({ preventScroll: true });
82
+ });
83
+ return () => {
84
+ cancelAnimationFrame(raf);
85
+ const prev = restoreRef.current;
86
+ if (prev && document.contains(prev)) prev.focus({ preventScroll: true });
87
+ };
88
+ }, [enabled, containerRef, initialFocus]);
89
+ import_react.default.useEffect(() => {
90
+ if (!enabled || !trap) return;
91
+ const onKey = (e) => {
92
+ if (e.key !== "Tab") return;
93
+ const el = containerRef.current;
94
+ if (!el) return;
95
+ const items = focusableIn(el);
96
+ if (items.length === 0) {
97
+ e.preventDefault();
98
+ return;
99
+ }
100
+ const first = items[0];
101
+ const last = items[items.length - 1];
102
+ const active = document.activeElement;
103
+ if (e.shiftKey && (active === first || !el.contains(active))) {
104
+ e.preventDefault();
105
+ last.focus();
106
+ } else if (!e.shiftKey && (active === last || !el.contains(active))) {
107
+ e.preventDefault();
108
+ first.focus();
109
+ }
110
+ };
111
+ document.addEventListener("keydown", onKey);
112
+ return () => document.removeEventListener("keydown", onKey);
113
+ }, [enabled, trap, containerRef]);
114
+ };
115
+
41
116
  // ../../node_modules/clsx/dist/clsx.mjs
42
117
  function r(e) {
43
118
  var t, f, n = "";
@@ -59,9 +134,12 @@ var import_jsx_runtime = require("react/jsx-runtime");
59
134
  var ANIMATION_DURATION_MS = 250;
60
135
  var Drawer = (props) => {
61
136
  const { isOpen, onClose, placement = "right", size = "md", title, children } = props;
62
- const [mounted, setMounted] = import_react.default.useState(false);
63
- const [visible, setVisible] = import_react.default.useState(false);
64
- import_react.default.useEffect(() => {
137
+ const [mounted, setMounted] = import_react2.default.useState(false);
138
+ const [visible, setVisible] = import_react2.default.useState(false);
139
+ const panelRef = import_react2.default.useRef(null);
140
+ useEscapeKey(isOpen, onClose);
141
+ useFocusTrap(panelRef, isOpen && mounted, { trap: true });
142
+ import_react2.default.useEffect(() => {
65
143
  if (isOpen) {
66
144
  setMounted(true);
67
145
  const t2 = setTimeout(() => setVisible(true), 1);
@@ -83,6 +161,7 @@ var Drawer = (props) => {
83
161
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
84
162
  "div",
85
163
  {
164
+ ref: panelRef,
86
165
  className: clsx_default("lib-xplat-drawer", placement, size, stateClass),
87
166
  role: "dialog",
88
167
  "aria-modal": "true",
@@ -1,7 +1,82 @@
1
1
  // src/components/Drawer/Drawer.tsx
2
- import React from "react";
2
+ import React2 from "react";
3
3
  import { createPortal } from "react-dom";
4
4
 
5
+ // src/tokens/hooks/useOverlayKeyboard.ts
6
+ import React from "react";
7
+ var FOCUSABLE = [
8
+ "a[href]",
9
+ "button:not([disabled])",
10
+ "input:not([disabled]):not([type='hidden'])",
11
+ "select:not([disabled])",
12
+ "textarea:not([disabled])",
13
+ "[tabindex]:not([tabindex='-1'])"
14
+ ].join(",");
15
+ var focusableIn = (el) => Array.from(el.querySelectorAll(FOCUSABLE)).filter(
16
+ // 화면에 없는 것은 제외 (display:none 등)
17
+ (n) => n.offsetWidth > 0 || n.offsetHeight > 0 || n === document.activeElement
18
+ );
19
+ var useEscapeKey = (enabled, onEscape) => {
20
+ React.useEffect(() => {
21
+ if (!enabled) return;
22
+ const onKey = (e) => {
23
+ if (e.key !== "Escape") return;
24
+ if (e.defaultPrevented) return;
25
+ e.preventDefault();
26
+ onEscape();
27
+ };
28
+ document.addEventListener("keydown", onKey);
29
+ return () => document.removeEventListener("keydown", onKey);
30
+ }, [enabled, onEscape]);
31
+ };
32
+ var useFocusTrap = (containerRef, enabled, options = {}) => {
33
+ const { trap = false, initialFocus } = options;
34
+ const restoreRef = React.useRef(null);
35
+ React.useEffect(() => {
36
+ if (!enabled) return;
37
+ restoreRef.current = document.activeElement;
38
+ const raf = requestAnimationFrame(() => {
39
+ const el = containerRef.current;
40
+ if (!el) return;
41
+ const target = initialFocus?.current ?? focusableIn(el)[0] ?? el;
42
+ if (!target.hasAttribute("tabindex") && !focusableIn(el).includes(target)) {
43
+ target.setAttribute("tabindex", "-1");
44
+ }
45
+ target.focus({ preventScroll: true });
46
+ });
47
+ return () => {
48
+ cancelAnimationFrame(raf);
49
+ const prev = restoreRef.current;
50
+ if (prev && document.contains(prev)) prev.focus({ preventScroll: true });
51
+ };
52
+ }, [enabled, containerRef, initialFocus]);
53
+ React.useEffect(() => {
54
+ if (!enabled || !trap) return;
55
+ const onKey = (e) => {
56
+ if (e.key !== "Tab") return;
57
+ const el = containerRef.current;
58
+ if (!el) return;
59
+ const items = focusableIn(el);
60
+ if (items.length === 0) {
61
+ e.preventDefault();
62
+ return;
63
+ }
64
+ const first = items[0];
65
+ const last = items[items.length - 1];
66
+ const active = document.activeElement;
67
+ if (e.shiftKey && (active === first || !el.contains(active))) {
68
+ e.preventDefault();
69
+ last.focus();
70
+ } else if (!e.shiftKey && (active === last || !el.contains(active))) {
71
+ e.preventDefault();
72
+ first.focus();
73
+ }
74
+ };
75
+ document.addEventListener("keydown", onKey);
76
+ return () => document.removeEventListener("keydown", onKey);
77
+ }, [enabled, trap, containerRef]);
78
+ };
79
+
5
80
  // ../../node_modules/clsx/dist/clsx.mjs
6
81
  function r(e) {
7
82
  var t, f, n = "";
@@ -23,9 +98,12 @@ import { jsx, jsxs } from "react/jsx-runtime";
23
98
  var ANIMATION_DURATION_MS = 250;
24
99
  var Drawer = (props) => {
25
100
  const { isOpen, onClose, placement = "right", size = "md", title, children } = props;
26
- const [mounted, setMounted] = React.useState(false);
27
- const [visible, setVisible] = React.useState(false);
28
- React.useEffect(() => {
101
+ const [mounted, setMounted] = React2.useState(false);
102
+ const [visible, setVisible] = React2.useState(false);
103
+ const panelRef = React2.useRef(null);
104
+ useEscapeKey(isOpen, onClose);
105
+ useFocusTrap(panelRef, isOpen && mounted, { trap: true });
106
+ React2.useEffect(() => {
29
107
  if (isOpen) {
30
108
  setMounted(true);
31
109
  const t2 = setTimeout(() => setVisible(true), 1);
@@ -47,6 +125,7 @@ var Drawer = (props) => {
47
125
  children: /* @__PURE__ */ jsxs(
48
126
  "div",
49
127
  {
128
+ ref: panelRef,
50
129
  className: clsx_default("lib-xplat-drawer", placement, size, stateClass),
51
130
  role: "dialog",
52
131
  "aria-modal": "true",