@bigtablet/design-system 3.19.1 → 3.19.3

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.css CHANGED
@@ -1068,6 +1068,10 @@ body:has(.bottom_nav) {
1068
1068
  border-radius: 12px;
1069
1069
  box-shadow: var(--bt-elevation-level2);
1070
1070
  padding: 4px 0;
1071
+ box-sizing: border-box;
1072
+ overflow-y: auto;
1073
+ scrollbar-width: thin;
1074
+ scrollbar-color: var(--bt-color-border-hover) transparent;
1071
1075
  will-change: transform, opacity;
1072
1076
  }
1073
1077
  [data-theme=dark] .menu {
@@ -1882,6 +1886,9 @@ body:has(.bottom_nav) {
1882
1886
  box-sizing: border-box;
1883
1887
  min-width: min(200px, 100%);
1884
1888
  max-width: min(320px, 100%);
1889
+ overflow-y: auto;
1890
+ scrollbar-width: thin;
1891
+ scrollbar-color: var(--bt-color-border-hover) transparent;
1885
1892
  color: var(--bt-color-text-body);
1886
1893
  font-size: 14px;
1887
1894
  font-weight: 400;
@@ -1929,6 +1936,10 @@ body:has(.bottom_nav) {
1929
1936
  }
1930
1937
  .tooltip {
1931
1938
  display: block;
1939
+ box-sizing: border-box;
1940
+ overflow-y: auto;
1941
+ scrollbar-width: thin;
1942
+ scrollbar-color: var(--bt-color-border-hover) transparent;
1932
1943
  background: var(--bt-color-accent-strong);
1933
1944
  color: var(--bt-color-accent-on-surface);
1934
1945
  padding: 6px 10px;
@@ -4783,6 +4794,8 @@ body:has(.bottom_nav) {
4783
4794
  position: fixed;
4784
4795
  z-index: 1000;
4785
4796
  box-sizing: border-box;
4797
+ display: flex;
4798
+ flex-direction: column;
4786
4799
  width: max-content;
4787
4800
  background: var(--bt-color-bg-solid);
4788
4801
  border: 1px solid var(--bt-color-border-default);
@@ -4800,6 +4813,7 @@ body:has(.bottom_nav) {
4800
4813
  }
4801
4814
  .combobox_list {
4802
4815
  max-height: 18rem;
4816
+ min-height: 0;
4803
4817
  overflow-y: auto;
4804
4818
  scrollbar-width: thin;
4805
4819
  scrollbar-color: var(--bt-color-border-hover) transparent;
@@ -5029,6 +5043,8 @@ body:has(.bottom_nav) {
5029
5043
  position: fixed;
5030
5044
  z-index: 1000;
5031
5045
  box-sizing: border-box;
5046
+ display: flex;
5047
+ flex-direction: column;
5032
5048
  width: max-content;
5033
5049
  background: var(--bt-color-bg-solid);
5034
5050
  border: 1px solid var(--bt-color-border-default);
@@ -5084,6 +5100,7 @@ body:has(.bottom_nav) {
5084
5100
  }
5085
5101
  .dropdown_options {
5086
5102
  max-height: 18rem;
5103
+ min-height: 0;
5087
5104
  overflow-y: auto;
5088
5105
  scrollbar-width: thin;
5089
5106
  scrollbar-color: var(--bt-color-border-hover) transparent;
package/dist/index.d.ts CHANGED
@@ -181,6 +181,11 @@ interface UseListboxPopupResult {
181
181
  * 이 값으로 좌표를 잡는데 패널 폭을 트리거 폭으로 두면 오른쪽이 잘린다.
182
182
  */
183
183
  maxWidth: number;
184
+ /**
185
+ * 배치된 방향의 가용 높이 상한(px). 목록이 길고 뷰포트가 낮으면 이걸 안 걸었을 때
186
+ * 패널이 화면 밖으로 나간다(#621).
187
+ */
188
+ maxHeight: number;
184
189
  /** 최초 측정 전에는 false - 이때 숨겨 (0,0) 깜빡임을 막는다 */
185
190
  ready: boolean;
186
191
  };
package/dist/index.js CHANGED
@@ -254,6 +254,22 @@ var mainAxisStart = (side, anchor, floating, gap) => {
254
254
  return anchor.left + anchor.width + gap;
255
255
  }
256
256
  };
257
+ var mainAxisSpace = (side, anchor, viewport, gap, padding) => {
258
+ const top = clamp(anchor.top, 0, viewport.height);
259
+ const bottom = clamp(anchor.top + anchor.height, 0, viewport.height);
260
+ const left = clamp(anchor.left, 0, viewport.width);
261
+ const right = clamp(anchor.left + anchor.width, 0, viewport.width);
262
+ switch (side) {
263
+ case "top":
264
+ return top - gap - padding;
265
+ case "bottom":
266
+ return viewport.height - bottom - gap - padding;
267
+ case "left":
268
+ return left - gap - padding;
269
+ case "right":
270
+ return viewport.width - right - gap - padding;
271
+ }
272
+ };
257
273
  var fitsMainAxis = (side, anchor, floating, viewport, gap, padding) => {
258
274
  const start = mainAxisStart(side, anchor, floating, gap);
259
275
  switch (side) {
@@ -277,9 +293,14 @@ function computeAnchoredPosition(anchor, floating, viewport, options) {
277
293
  height: floating.height
278
294
  };
279
295
  let side = options.placement;
280
- if (!fitsMainAxis(side, anchor, sized, viewport, gap, padding) && fitsMainAxis(OPPOSITE[side], anchor, sized, viewport, gap, padding)) {
281
- side = OPPOSITE[side];
296
+ if (!fitsMainAxis(side, anchor, sized, viewport, gap, padding)) {
297
+ const opposite = OPPOSITE[side];
298
+ if (fitsMainAxis(opposite, anchor, sized, viewport, gap, padding) || mainAxisSpace(opposite, anchor, viewport, gap, padding) > mainAxisSpace(side, anchor, viewport, gap, padding)) {
299
+ side = opposite;
300
+ }
282
301
  }
302
+ const maxHeight = isVertical(side) ? Math.max(mainAxisSpace(side, anchor, viewport, gap, padding), 0) : viewport.height - padding * 2;
303
+ sized.height = Math.min(sized.height, maxHeight);
283
304
  const align = options.align ?? "center";
284
305
  let x;
285
306
  let y;
@@ -287,12 +308,14 @@ function computeAnchoredPosition(anchor, floating, viewport, options) {
287
308
  y = mainAxisStart(side, anchor, sized, gap);
288
309
  x = align === "start" ? anchor.left : align === "end" ? anchor.left + anchor.width - sized.width : anchor.left + anchor.width / 2 - sized.width / 2;
289
310
  x = clamp(x, padding, viewport.width - padding - sized.width);
311
+ y = clamp(y, padding, viewport.height - padding - floating.height);
290
312
  } else {
291
313
  x = mainAxisStart(side, anchor, sized, gap);
292
314
  y = align === "start" ? anchor.top : align === "end" ? anchor.top + anchor.height - sized.height : anchor.top + anchor.height / 2 - sized.height / 2;
293
315
  y = clamp(y, padding, viewport.height - padding - sized.height);
316
+ x = clamp(x, padding, viewport.width - padding - floating.width);
294
317
  }
295
- return { x, y, placement: side, maxWidth };
318
+ return { x, y, placement: side, maxWidth, maxHeight };
296
319
  }
297
320
  function useAnchoredPosition({
298
321
  open,
@@ -308,12 +331,16 @@ function useAnchoredPosition({
308
331
  y: 0,
309
332
  placement,
310
333
  maxWidth: 0,
334
+ maxHeight: 0,
311
335
  ready: false,
312
- anchorWidth: 0
336
+ anchorWidth: 0,
337
+ anchorHidden: false
313
338
  });
314
339
  useSafeLayoutEffect(() => {
315
340
  if (!open) {
316
- setState((prev) => prev.ready ? { ...prev, ready: false } : prev);
341
+ setState(
342
+ (prev) => prev.ready || prev.anchorHidden ? { ...prev, ready: false, anchorHidden: false } : prev
343
+ );
317
344
  return;
318
345
  }
319
346
  const anchor = anchorRef.current;
@@ -328,10 +355,12 @@ function useAnchoredPosition({
328
355
  { width: window.innerWidth, height: window.innerHeight },
329
356
  { placement, align, gap, padding }
330
357
  );
358
+ const anchorHidden = a.width > 0 && a.height > 0 && (a.bottom <= 0 || a.top >= window.innerHeight || a.right <= 0 || a.left >= window.innerWidth);
331
359
  setState({
332
360
  ...result,
333
361
  ready: true,
334
- anchorWidth: Math.min(a.width, result.maxWidth)
362
+ anchorWidth: Math.min(a.width, result.maxWidth),
363
+ anchorHidden
335
364
  });
336
365
  };
337
366
  let frame = 0;
@@ -661,6 +690,12 @@ function useListboxPopup({
661
690
  gap: LIST_GAP,
662
691
  padding: LIST_PADDING
663
692
  });
693
+ useEffect(() => {
694
+ if (!isOpen || !anchored.ready || !anchored.anchorHidden) return;
695
+ const focusInPanel = !!panelRef.current?.contains(document.activeElement);
696
+ setIsOpen(false);
697
+ if (focusInPanel && returnFocusOnClose) triggerRef.current?.focus({ preventScroll: true });
698
+ }, [isOpen, anchored.ready, anchored.anchorHidden, returnFocusOnClose]);
664
699
  return {
665
700
  isOpen,
666
701
  setIsOpen,
@@ -670,6 +705,7 @@ function useListboxPopup({
670
705
  y: anchored.y,
671
706
  width: anchored.anchorWidth,
672
707
  maxWidth: anchored.maxWidth,
708
+ maxHeight: anchored.maxHeight,
673
709
  ready: anchored.ready
674
710
  },
675
711
  activeIndex,
@@ -1255,6 +1291,14 @@ var Menu = ({ items, trigger, align = "start" }) => {
1255
1291
  gap: MENU_GAP,
1256
1292
  padding: 8
1257
1293
  });
1294
+ React11.useEffect(() => {
1295
+ if (!open || !pos.ready || !pos.anchorHidden) return;
1296
+ const focusInMenu = !!menuRef.current?.contains(document.activeElement);
1297
+ setOpen(false);
1298
+ if (focusInMenu) {
1299
+ wrapperRef.current?.querySelector("[aria-haspopup]")?.focus({ preventScroll: true });
1300
+ }
1301
+ }, [open, pos.ready, pos.anchorHidden]);
1258
1302
  const style = useSpringPresence({
1259
1303
  visible: open,
1260
1304
  from: pos.placement === "top" ? `translateY(${MENU_GAP}px)` : `translateY(-${MENU_GAP}px)`
@@ -1346,6 +1390,9 @@ var Menu = ({ items, trigger, align = "start" }) => {
1346
1390
  position: "fixed",
1347
1391
  left: pos.x,
1348
1392
  top: pos.y,
1393
+ // 배치된 방향에 남은 높이. 항목이 많은 메뉴가 낮은 뷰포트에서 화면 밖으로
1394
+ // 나가지 않게 한다(#621). 넘치는 만큼은 메뉴 안에서 스크롤한다.
1395
+ maxHeight: pos.ready ? pos.maxHeight : void 0,
1349
1396
  visibility: pos.ready ? void 0 : "hidden"
1350
1397
  },
1351
1398
  className: cn("menu", `menu_align_${align}`),
@@ -2016,7 +2063,7 @@ var Popover = ({
2016
2063
  tabIndex: -1,
2017
2064
  "aria-label": ariaLabel,
2018
2065
  "aria-labelledby": ariaLabelledby,
2019
- style,
2066
+ style: { ...style, maxHeight: pos.ready ? pos.maxHeight : void 0 },
2020
2067
  className: cn("popover", className),
2021
2068
  children: content
2022
2069
  }
@@ -2073,6 +2120,9 @@ var Tooltip = ({
2073
2120
  gap: TOOLTIP_GAP,
2074
2121
  padding: 8
2075
2122
  });
2123
+ React11.useEffect(() => {
2124
+ if (open && pos.ready && pos.anchorHidden) hideNow();
2125
+ }, [open, pos.ready, pos.anchorHidden, hideNow]);
2076
2126
  const fromTransform = (() => {
2077
2127
  switch (pos.placement) {
2078
2128
  case "top":
@@ -2132,7 +2182,16 @@ var Tooltip = ({
2132
2182
  },
2133
2183
  onMouseEnter: cancelHide,
2134
2184
  onMouseLeave: hide,
2135
- children: /* @__PURE__ */ jsx(animated.span, { id: tooltipId, role: "tooltip", style, className: "tooltip", children: content })
2185
+ children: /* @__PURE__ */ jsx(
2186
+ animated.span,
2187
+ {
2188
+ id: tooltipId,
2189
+ role: "tooltip",
2190
+ style: { ...style, maxHeight: pos.ready ? pos.maxHeight : void 0 },
2191
+ className: "tooltip",
2192
+ children: content
2193
+ }
2194
+ )
2136
2195
  }
2137
2196
  ),
2138
2197
  document.body
@@ -4303,6 +4362,8 @@ var Combobox = ({
4303
4362
  minWidth: popup.position.width || void 0,
4304
4363
  // 트리거가 뷰포트보다 넓으면 좌표만 줄어들고 패널은 그대로 넘친다.
4305
4364
  maxWidth: popup.position.ready ? popup.position.maxWidth : void 0,
4365
+ // 배치된 방향에 남은 높이 - Dropdown 과 같은 이유(#621).
4366
+ maxHeight: popup.position.ready ? popup.position.maxHeight : void 0,
4306
4367
  visibility: popup.position.ready ? void 0 : "hidden"
4307
4368
  },
4308
4369
  children: !hasList ? /* @__PURE__ */ jsx("p", { className: "combobox_message", role: "status", children: showIdle ? idleMessage : emptyMessage }) : /* @__PURE__ */ jsx(
@@ -4526,6 +4587,9 @@ var Dropdown = (props) => {
4526
4587
  minWidth: position.width || void 0,
4527
4588
  // 트리거가 뷰포트보다 넓으면 좌표만 줄어들고 패널은 그대로 넘친다.
4528
4589
  maxWidth: position.ready ? position.maxWidth : void 0,
4590
+ // 배치된 방향에 남은 높이. 안 걸면 긴 목록이 낮은 뷰포트에서 화면 밖으로
4591
+ // 나간다(#621). 패널이 세로 flex 라 목록이 이 안에서 줄고 스크롤한다.
4592
+ maxHeight: position.ready ? position.maxHeight : void 0,
4529
4593
  // 최초 측정 전에는 숨긴다 - (0,0) 에서 한 프레임 깜빡이는 것을 막는다.
4530
4594
  visibility: position.ready ? void 0 : "hidden"
4531
4595
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigtablet/design-system",
3
- "version": "3.19.1",
3
+ "version": "3.19.3",
4
4
  "description": "Bigtablet Design System UI Components",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",
@@ -125,17 +125,18 @@
125
125
  "size-limit": [
126
126
  {
127
127
  "path": "dist/index.js",
128
- "limit": "60 kB",
129
- "message": "Raised 50 kB -> 60 kB on 2026-08-05: baseline was 49.34 kB, leaving 0.66 kB (1.3%) under the old 50 kB limit. The current limit leaves 10.66 kB. Re-measure before raising again."
128
+ "limit": "70 kB",
129
+ "message": "Raised 60 kB -> 70 kB on 2026-09-10: baseline was 59.82 kB on develop (59.91 kB with PR #625), leaving 0.09-0.18 kB (0.3%) under the old 60 kB limit. Of that baseline, 18.48 kB is @react-spring/web (a runtime dependency, so size-limit bundles it) and 40.55 kB is this package - measure `dist/index.js` alone before blaming the dependency. The current limit leaves 10.09 kB. Re-measure before raising again."
130
130
  },
131
131
  {
132
132
  "path": "dist/vanilla/bigtablet.min.js",
133
- "limit": "7 kB",
134
- "message": "Raised 5 kB -> 7 kB on 2026-08-06: Dropdown multiple/searchable pushed the baseline from 4.13 kB to 5.6 kB, 0.6 kB over the old limit. The current limit leaves 1.4 kB. Re-measure before raising again."
133
+ "limit": "8 kB",
134
+ "message": "Raised 7 kB -> 8 kB on 2026-09-10: baseline was 6.83 kB, leaving 0.17 kB (2.4%) under the old 7 kB limit. The current limit leaves 1.17 kB. Re-measure before raising again."
135
135
  },
136
136
  {
137
137
  "path": "dist/index.css",
138
- "limit": "130 kB"
138
+ "limit": "18 kB",
139
+ "message": "Lowered 130 kB -> 18 kB on 2026-09-10: baseline is 14.56 kB brotlied, so the old limit sat 9x above reality and could never fire. The current limit leaves 3.44 kB. Raise it with a measurement, not to make a build pass."
139
140
  }
140
141
  ],
141
142
  "dependencies": {