@x-plat/design-system 0.14.13 → 0.14.15

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
@@ -7054,7 +7054,7 @@ Chip.displayName = "Chip";
7054
7054
  var Chip_default = Chip;
7055
7055
 
7056
7056
  // src/components/DatePicker/InputDatePicker/index.tsx
7057
- import React13 from "react";
7057
+ import React14 from "react";
7058
7058
 
7059
7059
  // src/components/Input/Input.tsx
7060
7060
  import React8 from "react";
@@ -7227,11 +7227,149 @@ PasswordInput.displayName = "PasswordInput";
7227
7227
  var PasswordInput_default = PasswordInput;
7228
7228
 
7229
7229
  // src/components/Modal/Modal.tsx
7230
- import React11 from "react";
7230
+ import React12 from "react";
7231
7231
  import { createPortal } from "react-dom";
7232
7232
 
7233
7233
  // src/tokens/hooks/useOverlayKeyboard.ts
7234
+ import React11 from "react";
7235
+
7236
+ // src/tokens/hooks/useAutoPosition.ts
7234
7237
  import React10 from "react";
7238
+ var findScrollParent = (el) => {
7239
+ for (let p = el.parentElement; p; p = p.parentElement) {
7240
+ const { overflowY, overflowX } = getComputedStyle(p);
7241
+ if (/(auto|scroll)/.test(overflowY) || /(auto|scroll)/.test(overflowX)) return p;
7242
+ }
7243
+ return null;
7244
+ };
7245
+ var TRIGGER_GAP = 8;
7246
+ var computePlacement = (triggerRect, popW, popH, clip) => {
7247
+ const roomBelow = clip.bottom - triggerRect.bottom - TRIGGER_GAP;
7248
+ const roomAbove = triggerRect.top - clip.top - TRIGGER_GAP;
7249
+ let direction;
7250
+ let top;
7251
+ let maxHeight = null;
7252
+ if (popH <= roomBelow) {
7253
+ direction = "bottom";
7254
+ top = triggerRect.bottom + TRIGGER_GAP;
7255
+ } else if (popH <= roomAbove) {
7256
+ direction = "top";
7257
+ top = triggerRect.top - TRIGGER_GAP - popH;
7258
+ } else {
7259
+ const room = Math.max(roomBelow, roomAbove, 0);
7260
+ if (roomBelow >= roomAbove) {
7261
+ direction = "bottom";
7262
+ top = triggerRect.bottom + TRIGGER_GAP;
7263
+ } else {
7264
+ direction = "top";
7265
+ top = triggerRect.top - TRIGGER_GAP - room;
7266
+ }
7267
+ maxHeight = room;
7268
+ }
7269
+ let left = triggerRect.left;
7270
+ if (left + popW > clip.right - 8) {
7271
+ left = triggerRect.right - popW;
7272
+ }
7273
+ if (left < clip.left + 8) left = clip.left + 8;
7274
+ const drawnH = maxHeight === null ? popH : Math.min(popH, maxHeight);
7275
+ return {
7276
+ direction,
7277
+ top,
7278
+ left,
7279
+ maxHeight,
7280
+ over: {
7281
+ top: Math.max(0, clip.top - top),
7282
+ right: Math.max(0, left + popW - clip.right),
7283
+ bottom: Math.max(0, top + drawnH - clip.bottom),
7284
+ left: Math.max(0, clip.left - left)
7285
+ }
7286
+ };
7287
+ };
7288
+ var useAutoPosition = (triggerRef, popRef, enabled = true) => {
7289
+ const directionRef = React10.useRef("bottom");
7290
+ const sizeRef = React10.useRef(null);
7291
+ const scrollParentRef = React10.useRef(null);
7292
+ const placedRef = React10.useRef(false);
7293
+ const calculatePosition = React10.useCallback((remeasure = false) => {
7294
+ const pop = popRef.current;
7295
+ if (!triggerRef.current || !pop) return;
7296
+ if (remeasure || !sizeRef.current) {
7297
+ sizeRef.current = {
7298
+ w: pop.offsetWidth,
7299
+ h: Math.max(pop.offsetHeight, pop.scrollHeight)
7300
+ };
7301
+ }
7302
+ const { w: popW, h: popH } = sizeRef.current;
7303
+ const triggerRect = triggerRef.current.getBoundingClientRect();
7304
+ if (popH === 0 || popW === 0) {
7305
+ sizeRef.current = null;
7306
+ return;
7307
+ }
7308
+ const parentRect = scrollParentRef.current?.getBoundingClientRect();
7309
+ const clip = {
7310
+ top: Math.max(parentRect?.top ?? 0, 0),
7311
+ bottom: Math.min(parentRect?.bottom ?? window.innerHeight, window.innerHeight),
7312
+ left: Math.max(parentRect?.left ?? 0, 0),
7313
+ right: Math.min(parentRect?.right ?? window.innerWidth, window.innerWidth)
7314
+ };
7315
+ const first = !placedRef.current;
7316
+ if (first) pop.style.transition = "none";
7317
+ const { direction: next, top, left, over, maxHeight } = computePlacement(
7318
+ triggerRect,
7319
+ popW,
7320
+ popH,
7321
+ clip
7322
+ );
7323
+ pop.style.top = `${top + window.scrollY}px`;
7324
+ pop.style.left = `${left + window.scrollX}px`;
7325
+ pop.style.maxHeight = maxHeight === null ? "" : `${maxHeight}px`;
7326
+ pop.style.clipPath = over.top || over.right || over.bottom || over.left ? `inset(${over.top}px ${over.right}px ${over.bottom}px ${over.left}px)` : "none";
7327
+ directionRef.current = next;
7328
+ pop.classList.toggle("top", next === "top");
7329
+ pop.classList.toggle("bottom", next === "bottom");
7330
+ if (first) {
7331
+ void pop.offsetHeight;
7332
+ pop.style.transition = "";
7333
+ placedRef.current = true;
7334
+ }
7335
+ }, [triggerRef, popRef]);
7336
+ React10.useLayoutEffect(() => {
7337
+ if (!enabled) {
7338
+ sizeRef.current = null;
7339
+ scrollParentRef.current = null;
7340
+ placedRef.current = false;
7341
+ return;
7342
+ }
7343
+ if (triggerRef.current) {
7344
+ scrollParentRef.current = findScrollParent(triggerRef.current);
7345
+ }
7346
+ calculatePosition(true);
7347
+ const raf = requestAnimationFrame(() => calculatePosition(true));
7348
+ return () => cancelAnimationFrame(raf);
7349
+ }, [calculatePosition, enabled, triggerRef]);
7350
+ React10.useEffect(() => {
7351
+ if (!enabled || !popRef.current) return;
7352
+ const observer = new ResizeObserver(() => {
7353
+ calculatePosition(true);
7354
+ });
7355
+ observer.observe(popRef.current);
7356
+ return () => observer.disconnect();
7357
+ }, [calculatePosition, enabled, popRef]);
7358
+ React10.useEffect(() => {
7359
+ if (!enabled) return;
7360
+ const onScroll = () => calculatePosition();
7361
+ const onResize = () => calculatePosition(true);
7362
+ window.addEventListener("resize", onResize, { passive: true });
7363
+ window.addEventListener("scroll", onScroll, { passive: true, capture: true });
7364
+ return () => {
7365
+ window.removeEventListener("resize", onResize);
7366
+ window.removeEventListener("scroll", onScroll, true);
7367
+ };
7368
+ }, [calculatePosition, enabled]);
7369
+ };
7370
+ var useAutoPosition_default = useAutoPosition;
7371
+
7372
+ // src/tokens/hooks/useOverlayKeyboard.ts
7235
7373
  var FOCUSABLE = [
7236
7374
  "a[href]",
7237
7375
  "button:not([disabled])",
@@ -7245,7 +7383,7 @@ var focusableIn = (el) => Array.from(el.querySelectorAll(FOCUSABLE)).filter(
7245
7383
  (n) => n.offsetWidth > 0 || n.offsetHeight > 0 || n === document.activeElement
7246
7384
  );
7247
7385
  var useEscapeKey = (enabled, onEscape) => {
7248
- React10.useEffect(() => {
7386
+ React11.useEffect(() => {
7249
7387
  if (!enabled) return;
7250
7388
  const onKey = (e) => {
7251
7389
  if (e.key !== "Escape") return;
@@ -7259,8 +7397,8 @@ var useEscapeKey = (enabled, onEscape) => {
7259
7397
  };
7260
7398
  var useFocusTrap = (containerRef, enabled, options = {}) => {
7261
7399
  const { trap = false, initialFocus } = options;
7262
- const restoreRef = React10.useRef(null);
7263
- React10.useEffect(() => {
7400
+ const restoreRef = React11.useRef(null);
7401
+ React11.useEffect(() => {
7264
7402
  if (!enabled) return;
7265
7403
  restoreRef.current = document.activeElement;
7266
7404
  const raf = requestAnimationFrame(() => {
@@ -7278,7 +7416,7 @@ var useFocusTrap = (containerRef, enabled, options = {}) => {
7278
7416
  if (prev && document.contains(prev)) prev.focus({ preventScroll: true });
7279
7417
  };
7280
7418
  }, [enabled, containerRef, initialFocus]);
7281
- React10.useEffect(() => {
7419
+ React11.useEffect(() => {
7282
7420
  if (!enabled || !trap) return;
7283
7421
  const onKey = (e) => {
7284
7422
  if (e.key !== "Tab") return;
@@ -7304,22 +7442,22 @@ var useFocusTrap = (containerRef, enabled, options = {}) => {
7304
7442
  return () => document.removeEventListener("keydown", onKey);
7305
7443
  }, [enabled, trap, containerRef]);
7306
7444
  };
7307
- var useCloseOnContainerScroll = (enabled, popRef, onClose) => {
7308
- const onCloseRef = React10.useRef(onClose);
7445
+ var useCloseWhenTriggerHidden = (enabled, triggerRef, onClose) => {
7446
+ const onCloseRef = React11.useRef(onClose);
7309
7447
  onCloseRef.current = onClose;
7310
- React10.useEffect(() => {
7311
- if (!enabled) return;
7312
- const onScroll = (e) => {
7313
- const target = e.target;
7314
- if (target === document || target === document.documentElement || target === document.body) {
7315
- return;
7316
- }
7317
- if (target instanceof Node && popRef.current?.contains(target)) return;
7318
- onCloseRef.current();
7319
- };
7320
- window.addEventListener("scroll", onScroll, { passive: true, capture: true });
7321
- return () => window.removeEventListener("scroll", onScroll, true);
7322
- }, [enabled, popRef]);
7448
+ React11.useEffect(() => {
7449
+ const trigger = triggerRef.current;
7450
+ if (!enabled || !trigger) return;
7451
+ const observer = new IntersectionObserver(
7452
+ ([entry]) => {
7453
+ if (!entry.isIntersecting) onCloseRef.current();
7454
+ },
7455
+ // 그릇이 없으면 root: null → 뷰포트 기준
7456
+ { root: findScrollParent(trigger), threshold: 0 }
7457
+ );
7458
+ observer.observe(trigger);
7459
+ return () => observer.disconnect();
7460
+ }, [enabled, triggerRef]);
7323
7461
  };
7324
7462
 
7325
7463
  // src/components/Modal/Modal.tsx
@@ -7327,13 +7465,13 @@ import { jsx as jsx313 } from "react/jsx-runtime";
7327
7465
  var ANIMATION_DURATION_MS = 200;
7328
7466
  var Modal = (props) => {
7329
7467
  const { isOpen, onClose, children } = props;
7330
- const [mounted, setMounted] = React11.useState(false);
7331
- const [visible, setVisible] = React11.useState(false);
7332
- const [settled, setSettled] = React11.useState(false);
7333
- const boxRef = React11.useRef(null);
7468
+ const [mounted, setMounted] = React12.useState(false);
7469
+ const [visible, setVisible] = React12.useState(false);
7470
+ const [settled, setSettled] = React12.useState(false);
7471
+ const boxRef = React12.useRef(null);
7334
7472
  useEscapeKey(isOpen, onClose);
7335
7473
  useFocusTrap(boxRef, isOpen && mounted, { trap: true });
7336
- React11.useEffect(() => {
7474
+ React12.useEffect(() => {
7337
7475
  if (isOpen) {
7338
7476
  setMounted(true);
7339
7477
  const show = setTimeout(() => setVisible(true), 1);
@@ -7377,9 +7515,9 @@ Modal.displayName = "Modal";
7377
7515
  var Modal_default = Modal;
7378
7516
 
7379
7517
  // src/components/DatePicker/SingleDatePicker/index.tsx
7380
- import React12 from "react";
7518
+ import React13 from "react";
7381
7519
  import { Fragment as Fragment4, jsx as jsx314, jsxs as jsxs200 } from "react/jsx-runtime";
7382
- var DayCell2 = React12.memo(
7520
+ var DayCell2 = React13.memo(
7383
7521
  ({
7384
7522
  day,
7385
7523
  disabled,
@@ -7428,33 +7566,33 @@ var SingleDatePicker = (props) => {
7428
7566
  required,
7429
7567
  validations
7430
7568
  } = props;
7431
- const labelId = React12.useId();
7569
+ const labelId = React13.useId();
7432
7570
  const initialYear = value?.getFullYear();
7433
7571
  const initialMonth = value?.getMonth();
7434
7572
  const { year, month, days, goToPrevMonth, goToNextMonth, setYear, setMonth } = useCalendar(
7435
7573
  initialYear,
7436
7574
  initialMonth
7437
7575
  );
7438
- const [pickerMode, setPickerMode] = React12.useState("days");
7439
- const [yearRangeStart, setYearRangeStart] = React12.useState(
7576
+ const [pickerMode, setPickerMode] = React13.useState("days");
7577
+ const [yearRangeStart, setYearRangeStart] = React13.useState(
7440
7578
  Math.floor((initialYear ?? (/* @__PURE__ */ new Date()).getFullYear()) / 12) * 12
7441
7579
  );
7442
- const minTime = React12.useMemo(
7580
+ const minTime = React13.useMemo(
7443
7581
  () => minDate ? new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate()).getTime() : -Infinity,
7444
7582
  [minDate]
7445
7583
  );
7446
- const maxTime = React12.useMemo(
7584
+ const maxTime = React13.useMemo(
7447
7585
  () => maxDate ? new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate()).getTime() : Infinity,
7448
7586
  [maxDate]
7449
7587
  );
7450
- const highlightSet = React12.useMemo(() => {
7588
+ const highlightSet = React13.useMemo(() => {
7451
7589
  const set = /* @__PURE__ */ new Set();
7452
7590
  for (const h of highlightDates) {
7453
7591
  set.add(`${h.getFullYear()}-${h.getMonth()}-${h.getDate()}`);
7454
7592
  }
7455
7593
  return set;
7456
7594
  }, [highlightDates]);
7457
- const handleSelect = React12.useCallback(
7595
+ const handleSelect = React13.useCallback(
7458
7596
  (date) => {
7459
7597
  onChange?.(date);
7460
7598
  },
@@ -7597,8 +7735,8 @@ var InputDatePicker = (props) => {
7597
7735
  required,
7598
7736
  validations
7599
7737
  } = props;
7600
- const [isOpen, setIsOpen] = React13.useState(false);
7601
- const [tempDate, setTempDate] = React13.useState(value ?? /* @__PURE__ */ new Date());
7738
+ const [isOpen, setIsOpen] = React14.useState(false);
7739
+ const [tempDate, setTempDate] = React14.useState(value ?? /* @__PURE__ */ new Date());
7602
7740
  const handleOpen = () => {
7603
7741
  if (disabled) return;
7604
7742
  setTempDate(value ?? /* @__PURE__ */ new Date());
@@ -7650,18 +7788,18 @@ InputDatePicker.displayName = "InputDatePicker";
7650
7788
  var InputDatePicker_default = InputDatePicker;
7651
7789
 
7652
7790
  // src/components/DatePicker/PopupPicker/index.tsx
7653
- import React17 from "react";
7791
+ import React18 from "react";
7654
7792
 
7655
7793
  // src/components/DatePicker/RangePicker/index.tsx
7656
- import React16 from "react";
7794
+ import React17 from "react";
7657
7795
 
7658
7796
  // src/components/Tab/Tab.tsx
7659
- import React15 from "react";
7797
+ import React16 from "react";
7660
7798
 
7661
7799
  // src/components/Tab/TabItem.tsx
7662
- import React14 from "react";
7800
+ import React15 from "react";
7663
7801
  import { jsx as jsx316 } from "react/jsx-runtime";
7664
- var TabItem = React14.forwardRef((props, ref) => {
7802
+ var TabItem = React15.forwardRef((props, ref) => {
7665
7803
  const { isActive: isActive2, title, onClick } = props;
7666
7804
  return /* @__PURE__ */ jsx316(
7667
7805
  "button",
@@ -7685,11 +7823,11 @@ import { jsx as jsx317, jsxs as jsxs202 } from "react/jsx-runtime";
7685
7823
  var Tab = (props) => {
7686
7824
  const { activeIndex, onChange, tabs, variant, type, size = "md" } = props;
7687
7825
  const shape = variant ?? type ?? "default";
7688
- const [underlineStyle, setUnderlineStyle] = React15.useState({
7826
+ const [underlineStyle, setUnderlineStyle] = React16.useState({
7689
7827
  left: 0,
7690
7828
  width: 0
7691
7829
  });
7692
- const itemRefs = React15.useRef([]);
7830
+ const itemRefs = React16.useRef([]);
7693
7831
  const handleKeyDown = (e) => {
7694
7832
  const last = tabs.length - 1;
7695
7833
  let next = null;
@@ -7705,7 +7843,7 @@ var Tab = (props) => {
7705
7843
  const handleChangeActiveTab = (tabItem, tabIdx) => {
7706
7844
  onChange(tabItem, tabIdx);
7707
7845
  };
7708
- React15.useEffect(() => {
7846
+ React16.useEffect(() => {
7709
7847
  const el = itemRefs.current[activeIndex];
7710
7848
  if (el) {
7711
7849
  setUnderlineStyle({ left: el.offsetLeft, width: el.offsetWidth });
@@ -7761,8 +7899,8 @@ var RangePicker = (props) => {
7761
7899
  required,
7762
7900
  validations
7763
7901
  } = props;
7764
- const labelId = React16.useId();
7765
- const [activeTab, setActiveTab] = React16.useState("start");
7902
+ const labelId = React17.useId();
7903
+ const [activeTab, setActiveTab] = React17.useState("start");
7766
7904
  const handleStartChange = (date) => {
7767
7905
  if (!date) return;
7768
7906
  const newStart = date > endDate ? endDate : date;
@@ -7856,7 +7994,7 @@ var RangePicker_default = RangePicker;
7856
7994
  import { jsx as jsx319, jsxs as jsxs204 } from "react/jsx-runtime";
7857
7995
  var PopupPicker = (props) => {
7858
7996
  const { component, type, locale } = props;
7859
- const [isOpen, setIsOpen] = React17.useState(false);
7997
+ const [isOpen, setIsOpen] = React18.useState(false);
7860
7998
  const handleClick = () => setIsOpen(true);
7861
7999
  const handleClose = () => setIsOpen(false);
7862
8000
  const handleSingleChange = (date) => {
@@ -7865,7 +8003,7 @@ var PopupPicker = (props) => {
7865
8003
  handleClose();
7866
8004
  };
7867
8005
  return /* @__PURE__ */ jsxs204("div", { className: "lib-xplat-popup-datepicker", children: [
7868
- React17.cloneElement(component, { onClick: handleClick }),
8006
+ React18.cloneElement(component, { onClick: handleClick }),
7869
8007
  /* @__PURE__ */ jsx319(Modal_default, { isOpen, onClose: handleClose, children: /* @__PURE__ */ jsxs204("div", { className: clsx_default("lib-xplat-popup-datepicker-card", type === "range" && "range-mode"), children: [
7870
8008
  /* @__PURE__ */ jsxs204("div", { className: "popup-datepicker-content", children: [
7871
8009
  type === "single" && /* @__PURE__ */ jsx319(
@@ -7924,18 +8062,18 @@ Divider.displayName = "Divider";
7924
8062
  var Divider_default = Divider;
7925
8063
 
7926
8064
  // src/components/Drawer/Drawer.tsx
7927
- import React18 from "react";
8065
+ import React19 from "react";
7928
8066
  import { createPortal as createPortal2 } from "react-dom";
7929
8067
  import { jsx as jsx321, jsxs as jsxs205 } from "react/jsx-runtime";
7930
8068
  var ANIMATION_DURATION_MS2 = 250;
7931
8069
  var Drawer = (props) => {
7932
8070
  const { isOpen, onClose, placement = "right", size = "md", title, children } = props;
7933
- const [mounted, setMounted] = React18.useState(false);
7934
- const [visible, setVisible] = React18.useState(false);
7935
- const panelRef = React18.useRef(null);
8071
+ const [mounted, setMounted] = React19.useState(false);
8072
+ const [visible, setVisible] = React19.useState(false);
8073
+ const panelRef = React19.useRef(null);
7936
8074
  useEscapeKey(isOpen, onClose);
7937
8075
  useFocusTrap(panelRef, isOpen && mounted, { trap: true });
7938
- React18.useEffect(() => {
8076
+ React19.useEffect(() => {
7939
8077
  if (isOpen) {
7940
8078
  setMounted(true);
7941
8079
  const t2 = setTimeout(() => setVisible(true), 1);
@@ -7991,141 +8129,6 @@ var Portal = ({ children }) => {
7991
8129
  Portal.displayName = "Portal";
7992
8130
  var Portal_default = Portal;
7993
8131
 
7994
- // src/tokens/hooks/useAutoPosition.ts
7995
- import React19 from "react";
7996
- var findScrollParent = (el) => {
7997
- for (let p = el.parentElement; p; p = p.parentElement) {
7998
- const { overflowY, overflowX } = getComputedStyle(p);
7999
- if (/(auto|scroll)/.test(overflowY) || /(auto|scroll)/.test(overflowX)) return p;
8000
- }
8001
- return null;
8002
- };
8003
- var computePlacement = (triggerRect, popW, popH, clip) => {
8004
- const roomBelow = clip.bottom - triggerRect.bottom;
8005
- const roomAbove = triggerRect.top - clip.top;
8006
- let direction;
8007
- let top;
8008
- let maxHeight = null;
8009
- if (popH <= roomBelow) {
8010
- direction = "bottom";
8011
- top = triggerRect.bottom;
8012
- } else if (popH <= roomAbove) {
8013
- direction = "top";
8014
- top = triggerRect.top - popH;
8015
- } else {
8016
- const room = Math.max(roomBelow, roomAbove, 0);
8017
- if (roomBelow >= roomAbove) {
8018
- direction = "bottom";
8019
- top = triggerRect.bottom;
8020
- } else {
8021
- direction = "top";
8022
- top = triggerRect.top - room;
8023
- }
8024
- maxHeight = room;
8025
- }
8026
- let left = triggerRect.left;
8027
- if (left + popW > clip.right - 8) {
8028
- left = triggerRect.right - popW;
8029
- }
8030
- if (left < clip.left + 8) left = clip.left + 8;
8031
- const drawnH = maxHeight === null ? popH : Math.min(popH, maxHeight);
8032
- return {
8033
- direction,
8034
- top,
8035
- left,
8036
- maxHeight,
8037
- over: {
8038
- top: Math.max(0, clip.top - top),
8039
- right: Math.max(0, left + popW - clip.right),
8040
- bottom: Math.max(0, top + drawnH - clip.bottom),
8041
- left: Math.max(0, clip.left - left)
8042
- }
8043
- };
8044
- };
8045
- var useAutoPosition = (triggerRef, popRef, enabled = true) => {
8046
- const directionRef = React19.useRef("bottom");
8047
- const sizeRef = React19.useRef(null);
8048
- const scrollParentRef = React19.useRef(null);
8049
- const placedRef = React19.useRef(false);
8050
- const calculatePosition = React19.useCallback((remeasure = false) => {
8051
- const pop = popRef.current;
8052
- if (!triggerRef.current || !pop) return;
8053
- if (remeasure || !sizeRef.current) {
8054
- sizeRef.current = {
8055
- w: pop.offsetWidth,
8056
- h: Math.max(pop.offsetHeight, pop.scrollHeight)
8057
- };
8058
- }
8059
- const { w: popW, h: popH } = sizeRef.current;
8060
- const triggerRect = triggerRef.current.getBoundingClientRect();
8061
- if (popH === 0 || popW === 0) {
8062
- sizeRef.current = null;
8063
- return;
8064
- }
8065
- const parentRect = scrollParentRef.current?.getBoundingClientRect();
8066
- const clip = {
8067
- top: Math.max(parentRect?.top ?? 0, 0),
8068
- bottom: Math.min(parentRect?.bottom ?? window.innerHeight, window.innerHeight),
8069
- left: Math.max(parentRect?.left ?? 0, 0),
8070
- right: Math.min(parentRect?.right ?? window.innerWidth, window.innerWidth)
8071
- };
8072
- const first = !placedRef.current;
8073
- if (first) pop.style.transition = "none";
8074
- const { direction: next, top, left, over, maxHeight } = computePlacement(
8075
- triggerRect,
8076
- popW,
8077
- popH,
8078
- clip
8079
- );
8080
- pop.style.top = `${top + window.scrollY}px`;
8081
- pop.style.left = `${left + window.scrollX}px`;
8082
- pop.style.maxHeight = maxHeight === null ? "" : `${maxHeight}px`;
8083
- pop.style.clipPath = over.top || over.right || over.bottom || over.left ? `inset(${over.top}px ${over.right}px ${over.bottom}px ${over.left}px)` : "none";
8084
- directionRef.current = next;
8085
- pop.classList.toggle("top", next === "top");
8086
- pop.classList.toggle("bottom", next === "bottom");
8087
- if (first) {
8088
- void pop.offsetHeight;
8089
- pop.style.transition = "";
8090
- placedRef.current = true;
8091
- }
8092
- }, [triggerRef, popRef]);
8093
- React19.useLayoutEffect(() => {
8094
- if (!enabled) {
8095
- sizeRef.current = null;
8096
- scrollParentRef.current = null;
8097
- placedRef.current = false;
8098
- return;
8099
- }
8100
- if (triggerRef.current) {
8101
- scrollParentRef.current = findScrollParent(triggerRef.current);
8102
- }
8103
- calculatePosition(true);
8104
- const raf = requestAnimationFrame(() => calculatePosition(true));
8105
- return () => cancelAnimationFrame(raf);
8106
- }, [calculatePosition, enabled, triggerRef]);
8107
- React19.useEffect(() => {
8108
- if (!enabled || !popRef.current) return;
8109
- const observer = new ResizeObserver(() => {
8110
- calculatePosition(true);
8111
- });
8112
- observer.observe(popRef.current);
8113
- return () => observer.disconnect();
8114
- }, [calculatePosition, enabled, popRef]);
8115
- React19.useEffect(() => {
8116
- if (!enabled) return;
8117
- const onScroll = () => calculatePosition();
8118
- const onResize = () => calculatePosition(true);
8119
- window.addEventListener("resize", onResize, { passive: true });
8120
- window.addEventListener("scroll", onScroll, { passive: true, capture: true });
8121
- return () => {
8122
- window.removeEventListener("resize", onResize);
8123
- window.removeEventListener("scroll", onScroll, true);
8124
- };
8125
- }, [calculatePosition, enabled]);
8126
- };
8127
- var useAutoPosition_default = useAutoPosition;
8128
-
8129
8132
  // src/tokens/hooks/useClickOutside.ts
8130
8133
  import React20 from "react";
8131
8134
  var useClickOutside = (refs, handler, enabled = true) => {
@@ -8162,7 +8165,7 @@ var Dropdown = (props) => {
8162
8165
  const menuRef = React21.useRef(null);
8163
8166
  useAutoPosition_default(triggerRef, menuRef, mounted);
8164
8167
  useClickOutside_default([triggerRef, menuRef], () => setIsOpen(false), isOpen);
8165
- useCloseOnContainerScroll(isOpen, menuRef, () => setIsOpen(false));
8168
+ useCloseWhenTriggerHidden(isOpen, triggerRef, () => setIsOpen(false));
8166
8169
  useEscapeKey(isOpen, () => {
8167
8170
  setIsOpen(false);
8168
8171
  triggerRef.current?.focus({ preventScroll: true });
@@ -10093,7 +10096,7 @@ var PopOver = (props) => {
10093
10096
  const [localOpen, setLocalOpen] = React27.useState(false);
10094
10097
  const [eventTrigger, setEventTrigger] = React27.useState(false);
10095
10098
  useClickOutside_default([popRef, triggerRef], onClose, isOpen);
10096
- useCloseOnContainerScroll(isOpen, popRef, onClose);
10099
+ useCloseWhenTriggerHidden(isOpen, triggerRef, onClose);
10097
10100
  useEscapeKey(isOpen, () => {
10098
10101
  onClose();
10099
10102
  triggerRef.current?.focus({ preventScroll: true });
@@ -10373,7 +10376,7 @@ var SelectRoot = (props) => {
10373
10376
  }, []);
10374
10377
  const toggle = React31.useCallback(() => setOpen((prev) => !prev), []);
10375
10378
  useClickOutside_default([contentRef, triggerRef], close, isOpen);
10376
- useCloseOnContainerScroll(isOpen, contentRef, close);
10379
+ useCloseWhenTriggerHidden(isOpen, triggerRef, close);
10377
10380
  useEscapeKey(isOpen, () => {
10378
10381
  close();
10379
10382
  triggerRef.current?.focus({ preventScroll: true });
@@ -11314,7 +11317,7 @@ var TimePicker = (props) => {
11314
11317
  }, [step, min, max]);
11315
11318
  const close = React40.useCallback(() => setOpen(false), []);
11316
11319
  useClickOutside_default([triggerRef, contentRef], close, isOpen);
11317
- useCloseOnContainerScroll(isOpen, contentRef, close);
11320
+ useCloseWhenTriggerHidden(isOpen, triggerRef, close);
11318
11321
  useEscapeKey(isOpen, () => {
11319
11322
  close();
11320
11323
  triggerRef.current?.focus({ preventScroll: true });
@@ -284,44 +284,32 @@ Dropdown · PopOver · TimePicker 가 전부 애니메이션 없이 툭 튀어
284
284
  **올리기만 하면 된다. 쓰는 쪽에서 고칠 것은 없다.** `.repositioning` 클래스를
285
285
  직접 잡는 CSS 를 쓰고 있었다면 그 규칙만 지우면 된다.
286
286
 
287
- ## 0.14.6 → 0.14.7 — 안쪽 상자가 스크롤되면 오버레이가 닫힌다
287
+ ## 0.14.7 ~ 0.14.14 — 안쪽 상자가 스크롤되면 닫혔다 (0.14.15 에서 되돌림)
288
288
 
289
- **동작 변경이다.** `Select` · `Dropdown` · `PopOver` · `TimePicker` 를 열어 둔
290
- 채 **페이지가 아닌 안쪽 상자**(`overflow: auto`)를 스크롤하면 닫힌다.
289
+ **이 구간을 쓰고 있었다면** `Select` · `Dropdown` · `PopOver` · `TimePicker`
290
+ 를 열어 둔 채 페이지가 아닌 안쪽 상자를 스크롤하면 닫혔다. **0.14.15 부터는
291
+ 닫지 않는다** — 따라간다.
291
292
 
292
- 닫지 않는 경우:
293
+ 0.14.7 에 그 땜질을 넣은 것은 팝업이 스크롤을 **제때 못 따라가고**(JS 로
294
+ 옮기느라 한 프레임 늦었다) 그릇 밖으로 나가도 안 잘려 헤더를 덮었기
295
+ 때문이다. 어차피 어긋나 보이니 닫는 쪽이 나았다.
293
296
 
294
- - **페이지(문서) 스크롤** — 팝업이 `absolute` + 문서 좌표라 브라우저가 함께
295
- 옮겨 준다. 지연도 겹침도 없으므로 닫을 이유가 없다.
296
- - **팝업 자기 목록 스크롤** — 셀렉트 목록은 `max-height` 안에서 스스로
297
- 스크롤한다. 그걸로 닫히면 고를 수가 없다.
297
+ 그 전제가 둘 다 무너졌다:
298
298
 
299
- ### 왜
299
+ 0.14.3 ~ 0.14.6 자리를 DOM 에 직접 쓰고 문서 좌표로 바꿔 지연이 사라졌다
300
+ 0.14.9 ~ 0.14.14 그릇 기준으로 자리를 잡고 그릇 밖은 clip-path 로 잘라낸다
300
301
 
301
- 팝업은 `body` 밑 포털에 있고 트리거는 상자 안에 있다. 둘을 이어 주는 것이
302
- 브라우저에 없으므로 JS 가 매번 다시 붙여야 하는데, **스크롤은 컴포지터가
303
- 먼저 그리고 `scroll` 이벤트는 그 뒤에 메인 스레드로 온다.** 그래서 JS 로
304
- 옮기는 한 항상 한 프레임 이상 늦는다. 0.12.0 부터 0.14.6 까지 네 번에 걸쳐
305
- 줄여 봤지만(rAF 묶기 → DOM 직접 쓰기 → 문서 좌표 → rAF 제거) 원리가 그대로라
306
- 남았다.
302
+ 이제 따라가도 어긋나 보이지 않는다. 그리고 닫는 쪽이 손해다 — 긴 목록에서
303
+ 아래를 보려고 바퀴를 조금 굴렸을 뿐인데 고르던 것이 사라진다.
307
304
 
308
- 두 번째 증상도 같은 뿌리다. 팝업이 상자 **바깥**에 있어 그 상자에 잘리지
309
- 않으므로, 트리거가 헤더 밑으로 지나가도 팝업만 헤더 위에 남는다. **위치가
310
- 틀린 것이지 순서가 틀린 게 아니라서 `z-index` 로는 못 고친다** — 헤더를
311
- 올리면 이번엔 목록 윗부분이 잘린다.
305
+ ### 다만 트리거가 그릇 밖으로 완전히 나가면 닫는다
312
306
 
313
- 닫으면 둘 다 없어진다.
307
+ 안 닫으면 **패널이 통째로 잘려 안 보이는데 열린 상태**가 된다. 눈에는 닫힌
308
+ 것 같은데 키보드 포커스와 `Escape` 는 거기 살아 있다. 판정은
309
+ `IntersectionObserver` 에 맡긴다 — 스크롤마다 재는 것보다 싸고, 그릇이
310
+ 스크롤하든 페이지가 스크롤하든 같은 방식으로 걸린다.
314
311
 
315
- > **DS `Layout` 을 쓰면 이 조건에 기본으로 들어간다.**
316
- > `.lib-xplat-layout` 이 `overflow: hidden` 이고 `.lib-xplat-layout-content`
317
- > 만 `overflow: auto` 라 페이지가 아예 스크롤되지 않는다.
318
-
319
- ### 쓰는 쪽에서 할 일
320
-
321
- **없다.** 다만 「열어 둔 채 스크롤하며 고르는」 흐름이 있었다면 그것만 막힌다.
322
- 목록이 길어 스크롤이 필요하다면 `Select` 의 `searchable` 로 걸러내는 쪽이 낫다.
323
-
324
- ---
312
+ **쓰는 쪽에서 할 일은 없다.**
325
313
 
326
314
  ## Next.js App Router 에서 반드시 걸리는 것
327
315
 
@@ -395,11 +383,21 @@ export { Button, Input, Select, SideBar /* 쓰는 것만 */ } from "@x-plat/desi
395
383
  | 안쪽 상자 스크롤 시 닫힘 (0.14.7) | **사람이 화면에서 확인함** |
396
384
  | 팝업 목록 안에서 휠 → 안 닫힘 (0.14.7) | **사람이 화면에서 확인함** |
397
385
  | 긴 항목 줄바꿈 (0.14.8) | **사람이 화면에서 확인함** — 두세 줄로 떨어진다 |
386
+ | 자리·간격·여는 동안 (0.14.14) | **프레임 단위로 측정함** — 멈춘 상태 간격 8px, 여는 동안 4→8 로 벌어짐, 포커스 링 여유가 한 프레임도 음수가 아님 |
398
387
  | 페이지(문서) 스크롤 시 안 닫힘 (0.14.7) | **확인 안 됨.** 규칙상 맞지만 그 조건의 화면에 고르개가 없어 볼 자리가 없었다 |
399
388
  | `Layout` 타입 (0.14.2) | 소비자 흉내 프로젝트로 타입 검사 통과 |
400
389
  | `ToggleChip` 커서·초점 테두리 | 마크업·CSS 만 확인. 눌러 본 사람 없음 |
401
390
  | Editor | 아래 |
402
391
 
392
+ > **재는 범위가 화면과 같은지 먼저 볼 것.** `getBoundingClientRect` 는
393
+ > `outline` 도 `box-shadow` 도 안 친다. 상자만 재면 "겹침 0" 이 나오는데
394
+ > 눈에는 포커스 링이 덮여 보인다 — 0.14.14 가 그 건이다. 상자·링·그림자를
395
+ > 같이 재야 한다.
396
+ >
397
+ > 그리고 포커스 링을 볼 때는 **키보드로 열 것**(`Tab` → `Enter`). 트리거가
398
+ > `div[tabindex]` 라 마우스 클릭에는 `:focus-visible` 이 안 걸린다. 마우스로만
399
+ > 열면 링이 아예 안 떠서 그 항목이 **늘 통과한다.**
400
+ >
403
401
  > 확인할 때는 **배포가 앉은 뒤에** 볼 것. 한 번은 "안 닫힌다" 로 나왔다가
404
402
  > 다시 보니 닫혔다 — 반쯤 배포된 화면이었다. 그리고 버전 번호가 아니라
405
403
  > 받은 파일 안을 열어 확인하는 편이 낫다(0.14.1 은 번호만 올라가고 내용이
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-plat/design-system",
3
- "version": "0.14.13",
3
+ "version": "0.14.15",
4
4
  "description": "XPLAT UI Design System",
5
5
  "author": "XPLAT WOONG",
6
6
  "main": "dist/index.cjs",