sellmate-design-system-react 9.0.0-beta.50 → 9.0.0-beta.52

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.cjs CHANGED
@@ -6423,8 +6423,8 @@ function SellmateWordmark({ className }) {
6423
6423
  "svg",
6424
6424
  {
6425
6425
  className,
6426
- width: "276",
6427
- height: "40",
6426
+ width: "220.8",
6427
+ height: "32",
6428
6428
  viewBox: "0 0 276 40",
6429
6429
  fill: "#00275F",
6430
6430
  xmlns: "http://www.w3.org/2000/svg",
@@ -6458,7 +6458,7 @@ var SLoginCardImpl = /* @__PURE__ */ react.forwardRef(function SLoginCard({ prod
6458
6458
  // 로그인 화면 밖(모달·스토리)에서도 그대로 쓸 수 있는 이유다.
6459
6459
  "flex h-full w-full max-w-[640px] min-w-[368px] shrink-0 flex-col items-center justify-center gap-sd-60",
6460
6460
  // 라운드 24px·폭 상하한(640/368px)은 대응 토큰이 없어 값으로 둔다 (디자인팀 토큰 추가 요청 대상).
6461
- "rounded-[24px] bg-(--sys-color-bg-frame) p-sd-24 text-center",
6461
+ "rounded-[24px] bg-(--sys-color-bg-frame) p-sd-16 text-center",
6462
6462
  // 한글은 기본 규칙으로 두면 어절 한가운데서도 줄이 바뀐다 — `break-keep` 으로 띄어쓰기에서만
6463
6463
  // 끊는다. 끊을 자리가 아예 없는 긴 토큰(URL·주문번호)만 `wrap-break-word` 로 흘려보낸다.
6464
6464
  "break-keep wrap-break-word",
@@ -6471,7 +6471,7 @@ var SLoginCardImpl = /* @__PURE__ */ react.forwardRef(function SLoginCard({ prod
6471
6471
  /* @__PURE__ */ jsxRuntime.jsx(SellmateWordmark, {}),
6472
6472
  product != null && // 24px 는 typo 프리셋(최대 18px)에 없는 등급이다 — 제품명은 로고의 일부처럼 읽혀야
6473
6473
  // 해서 본문 스케일 위에 선다. 여기만 값으로 두고, 프리셋이 생기면 갈아탄다.
6474
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-[24px] leading-[36px] font-bold text-(--sys-color-fg-accentLight)", children: product })
6474
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-[24px] leading-[28px] font-bold text-(--sys-color-fg-accentLight)", children: product })
6475
6475
  ] }),
6476
6476
  hasIntro && // 소개와 보조 설명은 한 문단으로 읽힌다.
6477
6477
  // `text-balance` 는 상속되므로 여기 한 번만 걸어 둘을 함께 덮는다 — 가운데 정렬에서는
@@ -7124,6 +7124,149 @@ var SSectionHeaderCardRootImpl = /* @__PURE__ */ react.forwardRef(
7124
7124
  }
7125
7125
  );
7126
7126
  var SSectionHeaderCard2 = /* @__PURE__ */ withDisplayName(SSectionHeaderCardRootImpl, "SSectionHeaderCard");
7127
+ var OVERLAY_SCROLLBAR_SIZE = 12;
7128
+ var MIN_THUMB_SIZE = 32;
7129
+ var AUTO_HIDE_DELAY = 1e3;
7130
+ var FADE_CLASS = "transition-opacity duration-200 ease-out motion-reduce:transition-none";
7131
+ var HIDDEN = { visible: false, thumbSize: 0, thumbOffset: 0 };
7132
+ function getMetrics(viewportSize, contentSize, scrollOffset) {
7133
+ if (contentSize <= viewportSize) return HIDDEN;
7134
+ const trackSize = viewportSize;
7135
+ const maxScrollOffset = contentSize - viewportSize;
7136
+ const thumbSize = Math.min(
7137
+ trackSize,
7138
+ Math.max(MIN_THUMB_SIZE, viewportSize / contentSize * trackSize)
7139
+ );
7140
+ const maxThumbOffset = trackSize - thumbSize;
7141
+ const thumbOffset = maxScrollOffset > 0 ? scrollOffset / maxScrollOffset * maxThumbOffset : 0;
7142
+ return { visible: true, thumbSize, thumbOffset };
7143
+ }
7144
+ function OverlayScrollbars({
7145
+ scrollRef,
7146
+ vertical: showVertical = true,
7147
+ horizontal: showHorizontal = true,
7148
+ autoHide = false
7149
+ }) {
7150
+ const [vertical, setVertical] = react.useState(HIDDEN);
7151
+ const [horizontal, setHorizontal] = react.useState(HIDDEN);
7152
+ const [scrolling, setScrolling] = react.useState(false);
7153
+ const [dragging, setDragging] = react.useState(false);
7154
+ const hideTimer = react.useRef(null);
7155
+ const shown = !autoHide || scrolling || dragging;
7156
+ const update = react.useCallback(() => {
7157
+ const el = scrollRef.current;
7158
+ if (!el) return;
7159
+ setVertical(showVertical ? getMetrics(el.clientHeight, el.scrollHeight, el.scrollTop) : HIDDEN);
7160
+ setHorizontal(
7161
+ showHorizontal ? getMetrics(el.clientWidth, el.scrollWidth, el.scrollLeft) : HIDDEN
7162
+ );
7163
+ }, [scrollRef, showVertical, showHorizontal]);
7164
+ const wake = react.useCallback(() => {
7165
+ setScrolling(true);
7166
+ if (hideTimer.current) clearTimeout(hideTimer.current);
7167
+ hideTimer.current = setTimeout(() => setScrolling(false), AUTO_HIDE_DELAY);
7168
+ }, []);
7169
+ react.useEffect(() => () => void (hideTimer.current && clearTimeout(hideTimer.current)), []);
7170
+ react.useEffect(() => {
7171
+ const el = scrollRef.current;
7172
+ if (!el) return;
7173
+ const onScroll = () => {
7174
+ update();
7175
+ if (autoHide) wake();
7176
+ };
7177
+ update();
7178
+ el.addEventListener("scroll", onScroll, { passive: true });
7179
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
7180
+ observer?.observe(el);
7181
+ if (el.firstElementChild) observer?.observe(el.firstElementChild);
7182
+ return () => {
7183
+ el.removeEventListener("scroll", onScroll);
7184
+ observer?.disconnect();
7185
+ };
7186
+ }, [scrollRef, update, autoHide, wake]);
7187
+ const drag = (axis) => (startEvent) => {
7188
+ const el = scrollRef.current;
7189
+ const metrics = axis === "vertical" ? vertical : horizontal;
7190
+ if (!el || !metrics.visible) return;
7191
+ startEvent.preventDefault();
7192
+ startEvent.currentTarget.setPointerCapture(startEvent.pointerId);
7193
+ setDragging(true);
7194
+ const isVertical = axis === "vertical";
7195
+ const startPointer = isVertical ? startEvent.clientY : startEvent.clientX;
7196
+ const startScroll = isVertical ? el.scrollTop : el.scrollLeft;
7197
+ const viewportSize = isVertical ? el.clientHeight : el.clientWidth;
7198
+ const contentSize = isVertical ? el.scrollHeight : el.scrollWidth;
7199
+ const maxThumbOffset = viewportSize - metrics.thumbSize;
7200
+ const maxScroll = contentSize - viewportSize;
7201
+ if (maxThumbOffset <= 0 || maxScroll <= 0) {
7202
+ setDragging(false);
7203
+ return;
7204
+ }
7205
+ const onPointerMove = (moveEvent) => {
7206
+ const delta = (isVertical ? moveEvent.clientY : moveEvent.clientX) - startPointer;
7207
+ const next = startScroll + delta / maxThumbOffset * maxScroll;
7208
+ if (isVertical) el.scrollTop = next;
7209
+ else el.scrollLeft = next;
7210
+ };
7211
+ const onPointerUp = () => {
7212
+ setDragging(false);
7213
+ if (autoHide) wake();
7214
+ window.removeEventListener("pointermove", onPointerMove);
7215
+ window.removeEventListener("pointerup", onPointerUp);
7216
+ };
7217
+ window.addEventListener("pointermove", onPointerMove);
7218
+ window.addEventListener("pointerup", onPointerUp, { once: true });
7219
+ };
7220
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
7221
+ vertical.visible && // 트랙은 포인터를 받지 않는다 — 막대가 없는 자리는 아래 콘텐츠가 그대로 눌린다.
7222
+ /* @__PURE__ */ jsxRuntime.jsx(
7223
+ "div",
7224
+ {
7225
+ "aria-hidden": "true",
7226
+ className: cn(
7227
+ "pointer-events-none absolute top-0 right-0 bottom-0 z-[1] w-[12px]",
7228
+ FADE_CLASS,
7229
+ shown ? "opacity-100" : "opacity-0"
7230
+ ),
7231
+ style: { bottom: horizontal.visible ? OVERLAY_SCROLLBAR_SIZE : 0 },
7232
+ children: /* @__PURE__ */ jsxRuntime.jsx(
7233
+ "div",
7234
+ {
7235
+ className: "pointer-events-auto absolute right-[2px] w-[8px] cursor-default touch-none rounded-full bg-[var(--color-grey-45)]",
7236
+ style: {
7237
+ height: vertical.thumbSize,
7238
+ transform: `translateY(${vertical.thumbOffset}px)`
7239
+ },
7240
+ onPointerDown: drag("vertical")
7241
+ }
7242
+ )
7243
+ }
7244
+ ),
7245
+ horizontal.visible && /* @__PURE__ */ jsxRuntime.jsx(
7246
+ "div",
7247
+ {
7248
+ "aria-hidden": "true",
7249
+ className: cn(
7250
+ "pointer-events-none absolute right-0 bottom-0 left-0 z-[1] h-[12px]",
7251
+ FADE_CLASS,
7252
+ shown ? "opacity-100" : "opacity-0"
7253
+ ),
7254
+ style: { right: vertical.visible ? OVERLAY_SCROLLBAR_SIZE : 0 },
7255
+ children: /* @__PURE__ */ jsxRuntime.jsx(
7256
+ "div",
7257
+ {
7258
+ className: "pointer-events-auto absolute bottom-[2px] h-[8px] cursor-default touch-none rounded-full bg-[var(--color-grey-45)]",
7259
+ style: {
7260
+ width: horizontal.thumbSize,
7261
+ transform: `translateX(${horizontal.thumbOffset}px)`
7262
+ },
7263
+ onPointerDown: drag("horizontal")
7264
+ }
7265
+ )
7266
+ }
7267
+ )
7268
+ ] });
7269
+ }
7127
7270
 
7128
7271
  // src/lib/scrollbar.ts
7129
7272
  var FIREFOX_SCROLLBAR_CLASS = "supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-width:thin] supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_transparent]";
@@ -7143,30 +7286,61 @@ var SScrollArea = /* @__PURE__ */ react.forwardRef(function SScrollArea2({
7143
7286
  bordered = false,
7144
7287
  maxHeight,
7145
7288
  maxWidth,
7289
+ overlayScrollbar = false,
7146
7290
  className,
7147
7291
  style,
7148
7292
  children,
7149
7293
  ...rest
7150
7294
  }, ref) {
7295
+ const scrollRef = react.useRef(null);
7296
+ const setScrollRef = react.useCallback(
7297
+ (node) => {
7298
+ scrollRef.current = node;
7299
+ if (typeof ref === "function") ref(node);
7300
+ else if (ref) ref.current = node;
7301
+ },
7302
+ [ref]
7303
+ );
7151
7304
  const mergedStyle = { ...style };
7152
7305
  if (maxHeight) mergedStyle.maxHeight = maxHeight;
7153
7306
  if (maxWidth) mergedStyle.maxWidth = maxWidth;
7154
- return /* @__PURE__ */ jsxRuntime.jsx(
7307
+ const scroller = /* @__PURE__ */ jsxRuntime.jsx(
7155
7308
  "div",
7156
7309
  {
7157
- ref,
7310
+ ref: setScrollRef,
7158
7311
  className: cn(
7159
7312
  AXIS_CLASS[axis],
7160
- SCROLLBAR_CLASS,
7161
- background && SCROLLBAR_TRACK_BG_CLASS,
7162
- bordered && SCROLLBAR_TRACK_BORDER_CLASS,
7163
- className
7313
+ overlayScrollbar ? (
7314
+ // 바깥 상자가 크기를 쥐었으므로 스크롤 요소는 그 안을 채우기만 한다.
7315
+ `${SCROLLBAR_HIDDEN_CLASS} h-full w-full`
7316
+ ) : cn(
7317
+ SCROLLBAR_CLASS,
7318
+ background && SCROLLBAR_TRACK_BG_CLASS,
7319
+ bordered && SCROLLBAR_TRACK_BORDER_CLASS
7320
+ ),
7321
+ !overlayScrollbar && className
7164
7322
  ),
7165
- style: mergedStyle,
7323
+ style: overlayScrollbar ? void 0 : mergedStyle,
7166
7324
  ...rest,
7167
7325
  children
7168
7326
  }
7169
7327
  );
7328
+ if (!overlayScrollbar) return scroller;
7329
+ return (
7330
+ // 막대가 기준으로 삼을 상자. overflow-hidden 은 막대가 모서리 밖으로 새지 않게 한다.
7331
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("relative overflow-hidden", className), style: mergedStyle, children: [
7332
+ scroller,
7333
+ /* @__PURE__ */ jsxRuntime.jsx(
7334
+ OverlayScrollbars,
7335
+ {
7336
+ scrollRef,
7337
+ vertical: axis !== "horizontal",
7338
+ horizontal: axis !== "vertical",
7339
+ autoHide: true
7340
+ }
7341
+ )
7342
+ ] })
7343
+ );
7170
7344
  });
7171
7345
 
7172
7346
  // src/components/SScrollArea/scrollArea.config.ts
@@ -11799,22 +11973,6 @@ var NATIVE_SCROLLBAR_BACKGROUND_CLASS = {
11799
11973
  neutral: `${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_WHITE_CLASS}`,
11800
11974
  screen: `${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_CLASS}`
11801
11975
  };
11802
- var SCROLLBAR_SIZE = 12;
11803
- var SCROLLBAR_MIN_THUMB_SIZE = 32;
11804
- var getMetrics = (viewportSize, contentSize, scrollOffset) => {
11805
- if (contentSize <= viewportSize) {
11806
- return { visible: false, thumbSize: 0, thumbOffset: 0 };
11807
- }
11808
- const trackSize = viewportSize;
11809
- const maxScrollOffset = contentSize - viewportSize;
11810
- const thumbSize = Math.min(
11811
- trackSize,
11812
- Math.max(SCROLLBAR_MIN_THUMB_SIZE, viewportSize / contentSize * trackSize)
11813
- );
11814
- const maxThumbOffset = trackSize - thumbSize;
11815
- const thumbOffset = maxScrollOffset > 0 ? scrollOffset / maxScrollOffset * maxThumbOffset : 0;
11816
- return { visible: true, thumbSize, thumbOffset };
11817
- };
11818
11976
  var setForwardedRef = (ref, node) => {
11819
11977
  if (typeof ref === "function") {
11820
11978
  ref(node);
@@ -11822,119 +11980,6 @@ var setForwardedRef = (ref, node) => {
11822
11980
  }
11823
11981
  if (ref) ref.current = node;
11824
11982
  };
11825
- function SPageOverlayScrollbars({ scrollRef }) {
11826
- const [vertical, setVertical] = react.useState({
11827
- visible: false,
11828
- thumbSize: 0,
11829
- thumbOffset: 0
11830
- });
11831
- const [horizontal, setHorizontal] = react.useState({
11832
- visible: false,
11833
- thumbSize: 0,
11834
- thumbOffset: 0
11835
- });
11836
- const update = react.useCallback(() => {
11837
- const el = scrollRef.current;
11838
- if (!el) return;
11839
- setVertical(getMetrics(el.clientHeight, el.scrollHeight, el.scrollTop));
11840
- setHorizontal(getMetrics(el.clientWidth, el.scrollWidth, el.scrollLeft));
11841
- }, [scrollRef]);
11842
- react.useEffect(() => {
11843
- const el = scrollRef.current;
11844
- if (!el) return;
11845
- update();
11846
- el.addEventListener("scroll", update, { passive: true });
11847
- const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
11848
- observer?.observe(el);
11849
- if (el.firstElementChild) observer?.observe(el.firstElementChild);
11850
- return () => {
11851
- el.removeEventListener("scroll", update);
11852
- observer?.disconnect();
11853
- };
11854
- }, [scrollRef, update]);
11855
- const dragVertical = (startEvent) => {
11856
- const el = scrollRef.current;
11857
- if (!el || !vertical.visible) return;
11858
- startEvent.preventDefault();
11859
- startEvent.currentTarget.setPointerCapture(startEvent.pointerId);
11860
- const startPointer = startEvent.clientY;
11861
- const startScrollTop = el.scrollTop;
11862
- const maxThumbOffset = el.clientHeight - vertical.thumbSize;
11863
- const maxScrollTop = el.scrollHeight - el.clientHeight;
11864
- if (maxThumbOffset <= 0 || maxScrollTop <= 0) return;
11865
- const onPointerMove = (moveEvent) => {
11866
- const delta = moveEvent.clientY - startPointer;
11867
- el.scrollTop = startScrollTop + delta / maxThumbOffset * maxScrollTop;
11868
- };
11869
- const onPointerUp = () => {
11870
- window.removeEventListener("pointermove", onPointerMove);
11871
- window.removeEventListener("pointerup", onPointerUp);
11872
- };
11873
- window.addEventListener("pointermove", onPointerMove);
11874
- window.addEventListener("pointerup", onPointerUp, { once: true });
11875
- };
11876
- const dragHorizontal = (startEvent) => {
11877
- const el = scrollRef.current;
11878
- if (!el || !horizontal.visible) return;
11879
- startEvent.preventDefault();
11880
- startEvent.currentTarget.setPointerCapture(startEvent.pointerId);
11881
- const startPointer = startEvent.clientX;
11882
- const startScrollLeft = el.scrollLeft;
11883
- const maxThumbOffset = el.clientWidth - horizontal.thumbSize;
11884
- const maxScrollLeft = el.scrollWidth - el.clientWidth;
11885
- if (maxThumbOffset <= 0 || maxScrollLeft <= 0) return;
11886
- const onPointerMove = (moveEvent) => {
11887
- const delta = moveEvent.clientX - startPointer;
11888
- el.scrollLeft = startScrollLeft + delta / maxThumbOffset * maxScrollLeft;
11889
- };
11890
- const onPointerUp = () => {
11891
- window.removeEventListener("pointermove", onPointerMove);
11892
- window.removeEventListener("pointerup", onPointerUp);
11893
- };
11894
- window.addEventListener("pointermove", onPointerMove);
11895
- window.addEventListener("pointerup", onPointerUp, { once: true });
11896
- };
11897
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11898
- vertical.visible && /* @__PURE__ */ jsxRuntime.jsx(
11899
- "div",
11900
- {
11901
- "aria-hidden": "true",
11902
- className: "pointer-events-none absolute bottom-0 right-0 top-0 z-[1] w-[12px]",
11903
- style: { bottom: horizontal.visible ? SCROLLBAR_SIZE : 0 },
11904
- children: /* @__PURE__ */ jsxRuntime.jsx(
11905
- "div",
11906
- {
11907
- className: "pointer-events-auto absolute right-[2px] w-[8px] cursor-default touch-none rounded-full bg-[var(--color-grey-45)]",
11908
- style: {
11909
- height: vertical.thumbSize,
11910
- transform: `translateY(${vertical.thumbOffset}px)`
11911
- },
11912
- onPointerDown: dragVertical
11913
- }
11914
- )
11915
- }
11916
- ),
11917
- horizontal.visible && /* @__PURE__ */ jsxRuntime.jsx(
11918
- "div",
11919
- {
11920
- "aria-hidden": "true",
11921
- className: "pointer-events-none absolute bottom-0 left-0 right-0 z-[1] h-[12px]",
11922
- style: { right: vertical.visible ? SCROLLBAR_SIZE : 0 },
11923
- children: /* @__PURE__ */ jsxRuntime.jsx(
11924
- "div",
11925
- {
11926
- className: "pointer-events-auto absolute bottom-[2px] h-[8px] cursor-default touch-none rounded-full bg-[var(--color-grey-45)]",
11927
- style: {
11928
- width: horizontal.thumbSize,
11929
- transform: `translateX(${horizontal.thumbOffset}px)`
11930
- },
11931
- onPointerDown: dragHorizontal
11932
- }
11933
- )
11934
- }
11935
- )
11936
- ] });
11937
- }
11938
11983
  var SPage = /* @__PURE__ */ react.forwardRef(function SPage2({
11939
11984
  background = "frame",
11940
11985
  scrollEndSpacing = false,
@@ -12007,7 +12052,7 @@ var SPage = /* @__PURE__ */ react.forwardRef(function SPage2({
12007
12052
  style: gridStyle,
12008
12053
  children: [
12009
12054
  main,
12010
- /* @__PURE__ */ jsxRuntime.jsx(SPageOverlayScrollbars, { scrollRef })
12055
+ /* @__PURE__ */ jsxRuntime.jsx(OverlayScrollbars, { scrollRef })
12011
12056
  ]
12012
12057
  }
12013
12058
  );
@@ -13468,47 +13513,167 @@ function SCalendarBoard({
13468
13513
  );
13469
13514
  }
13470
13515
 
13471
- // src/lib/floating-height.ts
13472
- var POPOVER_AVAILABLE_HEIGHT = "var(--radix-popover-content-available-height)";
13473
- function clampToAvailableHeight(value, availableHeight) {
13474
- if (value == null || value === "") return availableHeight;
13475
- const css = typeof value === "number" ? `${value}px` : value;
13476
- return `min(${css}, ${availableHeight})`;
13516
+ // src/lib/row-metrics.ts
13517
+ function createRowMetrics(count, estimate) {
13518
+ const safeCount = Math.max(0, Math.floor(count));
13519
+ const safeEstimate = Math.max(1, estimate);
13520
+ const metrics = {
13521
+ sizes: new Array(safeCount).fill(safeEstimate),
13522
+ measured: new Array(safeCount).fill(false),
13523
+ offsets: [],
13524
+ estimate: safeEstimate,
13525
+ measuredCount: 0,
13526
+ measuredTotal: 0,
13527
+ dirtyFrom: 0
13528
+ };
13529
+ rebuildOffsets(metrics);
13530
+ return metrics;
13477
13531
  }
13478
- var FONT = {
13479
- sm: {
13480
- fontSize: 12,
13481
- lineHeight: 20,
13482
- paddingX: "var(--cmp-textinput-sm-paddingX)",
13483
- gap: "var(--cmp-textinput-sm-gap)"
13484
- },
13485
- md: {
13486
- fontSize: 14,
13487
- lineHeight: 24,
13488
- paddingX: "var(--cmp-textinput-md-paddingX)",
13489
- gap: "var(--cmp-textinput-md-gap)"
13532
+ function resizeRowMetrics(metrics, count) {
13533
+ const next = Math.max(0, Math.floor(count));
13534
+ const prev = metrics.sizes.length;
13535
+ if (next === prev) return;
13536
+ if (next < prev) {
13537
+ for (let i = next; i < prev; i++) {
13538
+ if (metrics.measured[i]) {
13539
+ metrics.measuredCount--;
13540
+ metrics.measuredTotal -= metrics.sizes[i];
13541
+ }
13542
+ }
13543
+ metrics.sizes.length = next;
13544
+ metrics.measured.length = next;
13545
+ } else {
13546
+ for (let i = prev; i < next; i++) {
13547
+ metrics.sizes.push(metrics.estimate);
13548
+ metrics.measured.push(false);
13549
+ }
13490
13550
  }
13491
- };
13492
- var SSearchInput = /* @__PURE__ */ react.forwardRef(function SSearchInput2({
13493
- value,
13494
- onValueChange,
13495
- onChange,
13496
- onSearch,
13497
- size = "sm",
13498
- placeholder = "\uACB0\uACFC \uB0B4 \uAC80\uC0C9",
13499
- clearable = false,
13500
- disabled = false,
13501
- width,
13502
- focused,
13503
- hovered,
13504
- inputClassName,
13505
- inputStyle,
13506
- className,
13507
- style,
13508
- onKeyDown,
13509
- ...rest
13510
- }, ref) {
13511
- const f = FONT[size];
13551
+ markDirty(metrics, Math.min(prev, next));
13552
+ }
13553
+ function resetRowMetrics(metrics, count) {
13554
+ const fresh = createRowMetrics(count, metrics.estimate);
13555
+ metrics.sizes = fresh.sizes;
13556
+ metrics.measured = fresh.measured;
13557
+ metrics.offsets = fresh.offsets;
13558
+ metrics.measuredCount = 0;
13559
+ metrics.measuredTotal = 0;
13560
+ metrics.dirtyFrom = Infinity;
13561
+ }
13562
+ function setRowSize(metrics, index, size) {
13563
+ if (index < 0 || index >= metrics.sizes.length) return false;
13564
+ const next = Math.max(1, Math.round(size));
13565
+ const wasMeasured = metrics.measured[index];
13566
+ const prev = metrics.sizes[index];
13567
+ if (wasMeasured && prev === next) return false;
13568
+ if (wasMeasured) {
13569
+ metrics.measuredTotal += next - prev;
13570
+ } else {
13571
+ metrics.measured[index] = true;
13572
+ metrics.measuredCount++;
13573
+ metrics.measuredTotal += next;
13574
+ }
13575
+ metrics.sizes[index] = next;
13576
+ let changedFrom = prev === next ? Infinity : index;
13577
+ const average = Math.max(1, Math.round(metrics.measuredTotal / metrics.measuredCount));
13578
+ if (average !== metrics.estimate) {
13579
+ metrics.estimate = average;
13580
+ for (let i = 0; i < metrics.sizes.length; i++) {
13581
+ if (metrics.measured[i] || metrics.sizes[i] === average) continue;
13582
+ metrics.sizes[i] = average;
13583
+ if (i < changedFrom) changedFrom = i;
13584
+ }
13585
+ }
13586
+ if (changedFrom === Infinity) return false;
13587
+ markDirty(metrics, changedFrom);
13588
+ return true;
13589
+ }
13590
+ function markDirty(metrics, from) {
13591
+ metrics.dirtyFrom = Math.min(metrics.dirtyFrom, Math.max(0, from));
13592
+ }
13593
+ function rebuildOffsets(metrics) {
13594
+ const count = metrics.sizes.length;
13595
+ const stale = metrics.offsets.length !== count + 1;
13596
+ if (!stale && metrics.dirtyFrom === Infinity) return;
13597
+ const start = stale ? 0 : Math.min(metrics.dirtyFrom, count);
13598
+ const acc0 = start === 0 ? 0 : metrics.offsets[start] ?? 0;
13599
+ metrics.offsets.length = count + 1;
13600
+ let acc = acc0;
13601
+ for (let i = start; i < count; i++) {
13602
+ metrics.offsets[i] = acc;
13603
+ acc += metrics.sizes[i];
13604
+ }
13605
+ metrics.offsets[count] = acc;
13606
+ metrics.dirtyFrom = Infinity;
13607
+ }
13608
+ function totalSize(metrics) {
13609
+ rebuildOffsets(metrics);
13610
+ return metrics.offsets[metrics.sizes.length] ?? 0;
13611
+ }
13612
+ function offsetOf(metrics, index) {
13613
+ rebuildOffsets(metrics);
13614
+ const clamped = Math.max(0, Math.min(Math.floor(index), metrics.sizes.length));
13615
+ return metrics.offsets[clamped] ?? 0;
13616
+ }
13617
+ function sizeOf(metrics, index) {
13618
+ if (index < 0 || index >= metrics.sizes.length) return metrics.estimate;
13619
+ return metrics.sizes[index];
13620
+ }
13621
+ function indexAtOffset(metrics, y) {
13622
+ rebuildOffsets(metrics);
13623
+ const count = metrics.sizes.length;
13624
+ if (count === 0) return 0;
13625
+ const target = Math.max(0, y);
13626
+ let lo = 0;
13627
+ let hi = count - 1;
13628
+ while (lo < hi) {
13629
+ const mid = lo + hi + 1 >> 1;
13630
+ if (metrics.offsets[mid] <= target) lo = mid;
13631
+ else hi = mid - 1;
13632
+ }
13633
+ return lo;
13634
+ }
13635
+
13636
+ // src/lib/floating-height.ts
13637
+ var POPOVER_AVAILABLE_HEIGHT = "var(--radix-popover-content-available-height)";
13638
+ function clampToAvailableHeight(value, availableHeight) {
13639
+ if (value == null || value === "") return availableHeight;
13640
+ const css = typeof value === "number" ? `${value}px` : value;
13641
+ return `min(${css}, ${availableHeight})`;
13642
+ }
13643
+ var FONT = {
13644
+ sm: {
13645
+ fontSize: 12,
13646
+ lineHeight: 20,
13647
+ paddingX: "var(--cmp-textinput-sm-paddingX)",
13648
+ gap: "var(--cmp-textinput-sm-gap)"
13649
+ },
13650
+ md: {
13651
+ fontSize: 14,
13652
+ lineHeight: 24,
13653
+ paddingX: "var(--cmp-textinput-md-paddingX)",
13654
+ gap: "var(--cmp-textinput-md-gap)"
13655
+ }
13656
+ };
13657
+ var SSearchInput = /* @__PURE__ */ react.forwardRef(function SSearchInput2({
13658
+ value,
13659
+ onValueChange,
13660
+ onChange,
13661
+ onSearch,
13662
+ size = "sm",
13663
+ placeholder = "\uACB0\uACFC \uB0B4 \uAC80\uC0C9",
13664
+ clearable = false,
13665
+ disabled = false,
13666
+ width,
13667
+ focused,
13668
+ hovered,
13669
+ inputClassName,
13670
+ inputStyle,
13671
+ className,
13672
+ style,
13673
+ onKeyDown,
13674
+ ...rest
13675
+ }, ref) {
13676
+ const f = FONT[size];
13512
13677
  const hasValue = value !== void 0 && value !== null && String(value) !== "";
13513
13678
  return /* @__PURE__ */ jsxRuntime.jsx(
13514
13679
  SField,
@@ -13636,126 +13801,6 @@ var OPTION_ROW_HEIGHT = 28;
13636
13801
  var DEFAULT_VIRTUAL_BUFFER = 10;
13637
13802
  var DEFAULT_REACH_END_THRESHOLD = 10;
13638
13803
  var DEFAULT_SEARCH_DEBOUNCE = 300;
13639
-
13640
- // src/components/SSelect/row-metrics.ts
13641
- function createRowMetrics(count, estimate) {
13642
- const safeCount = Math.max(0, Math.floor(count));
13643
- const safeEstimate = Math.max(1, estimate);
13644
- const metrics = {
13645
- sizes: new Array(safeCount).fill(safeEstimate),
13646
- measured: new Array(safeCount).fill(false),
13647
- offsets: [],
13648
- estimate: safeEstimate,
13649
- measuredCount: 0,
13650
- measuredTotal: 0,
13651
- dirtyFrom: 0
13652
- };
13653
- rebuildOffsets(metrics);
13654
- return metrics;
13655
- }
13656
- function resizeRowMetrics(metrics, count) {
13657
- const next = Math.max(0, Math.floor(count));
13658
- const prev = metrics.sizes.length;
13659
- if (next === prev) return;
13660
- if (next < prev) {
13661
- for (let i = next; i < prev; i++) {
13662
- if (metrics.measured[i]) {
13663
- metrics.measuredCount--;
13664
- metrics.measuredTotal -= metrics.sizes[i];
13665
- }
13666
- }
13667
- metrics.sizes.length = next;
13668
- metrics.measured.length = next;
13669
- } else {
13670
- for (let i = prev; i < next; i++) {
13671
- metrics.sizes.push(metrics.estimate);
13672
- metrics.measured.push(false);
13673
- }
13674
- }
13675
- markDirty(metrics, Math.min(prev, next));
13676
- }
13677
- function resetRowMetrics(metrics, count) {
13678
- const fresh = createRowMetrics(count, metrics.estimate);
13679
- metrics.sizes = fresh.sizes;
13680
- metrics.measured = fresh.measured;
13681
- metrics.offsets = fresh.offsets;
13682
- metrics.measuredCount = 0;
13683
- metrics.measuredTotal = 0;
13684
- metrics.dirtyFrom = Infinity;
13685
- }
13686
- function setRowSize(metrics, index, size) {
13687
- if (index < 0 || index >= metrics.sizes.length) return false;
13688
- const next = Math.max(1, Math.round(size));
13689
- const wasMeasured = metrics.measured[index];
13690
- const prev = metrics.sizes[index];
13691
- if (wasMeasured && prev === next) return false;
13692
- if (wasMeasured) {
13693
- metrics.measuredTotal += next - prev;
13694
- } else {
13695
- metrics.measured[index] = true;
13696
- metrics.measuredCount++;
13697
- metrics.measuredTotal += next;
13698
- }
13699
- metrics.sizes[index] = next;
13700
- let changedFrom = prev === next ? Infinity : index;
13701
- const average = Math.max(1, Math.round(metrics.measuredTotal / metrics.measuredCount));
13702
- if (average !== metrics.estimate) {
13703
- metrics.estimate = average;
13704
- for (let i = 0; i < metrics.sizes.length; i++) {
13705
- if (metrics.measured[i] || metrics.sizes[i] === average) continue;
13706
- metrics.sizes[i] = average;
13707
- if (i < changedFrom) changedFrom = i;
13708
- }
13709
- }
13710
- if (changedFrom === Infinity) return false;
13711
- markDirty(metrics, changedFrom);
13712
- return true;
13713
- }
13714
- function markDirty(metrics, from) {
13715
- metrics.dirtyFrom = Math.min(metrics.dirtyFrom, Math.max(0, from));
13716
- }
13717
- function rebuildOffsets(metrics) {
13718
- const count = metrics.sizes.length;
13719
- const stale = metrics.offsets.length !== count + 1;
13720
- if (!stale && metrics.dirtyFrom === Infinity) return;
13721
- const start = stale ? 0 : Math.min(metrics.dirtyFrom, count);
13722
- const acc0 = start === 0 ? 0 : metrics.offsets[start] ?? 0;
13723
- metrics.offsets.length = count + 1;
13724
- let acc = acc0;
13725
- for (let i = start; i < count; i++) {
13726
- metrics.offsets[i] = acc;
13727
- acc += metrics.sizes[i];
13728
- }
13729
- metrics.offsets[count] = acc;
13730
- metrics.dirtyFrom = Infinity;
13731
- }
13732
- function totalSize(metrics) {
13733
- rebuildOffsets(metrics);
13734
- return metrics.offsets[metrics.sizes.length] ?? 0;
13735
- }
13736
- function offsetOf(metrics, index) {
13737
- rebuildOffsets(metrics);
13738
- const clamped = Math.max(0, Math.min(Math.floor(index), metrics.sizes.length));
13739
- return metrics.offsets[clamped] ?? 0;
13740
- }
13741
- function sizeOf(metrics, index) {
13742
- if (index < 0 || index >= metrics.sizes.length) return metrics.estimate;
13743
- return metrics.sizes[index];
13744
- }
13745
- function indexAtOffset(metrics, y) {
13746
- rebuildOffsets(metrics);
13747
- const count = metrics.sizes.length;
13748
- if (count === 0) return 0;
13749
- const target = Math.max(0, y);
13750
- let lo = 0;
13751
- let hi = count - 1;
13752
- while (lo < hi) {
13753
- const mid = lo + hi + 1 >> 1;
13754
- if (metrics.offsets[mid] <= target) lo = mid;
13755
- else hi = mid - 1;
13756
- }
13757
- return lo;
13758
- }
13759
13804
  var SEARCH_THRESHOLD = 11;
13760
13805
  var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
13761
13806
  value,
@@ -14654,6 +14699,7 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
14654
14699
  rows = [],
14655
14700
  rowKey = "id",
14656
14701
  selectable = false,
14702
+ isRowSelectable,
14657
14703
  selected = [],
14658
14704
  onSelectedChange,
14659
14705
  sort = null,
@@ -14680,6 +14726,7 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
14680
14726
  rowHeight,
14681
14727
  virtualBuffer = 5,
14682
14728
  virtualEndThreshold = 10,
14729
+ onScroll,
14683
14730
  onVirtualUpdate,
14684
14731
  onVirtualReachEnd,
14685
14732
  tableId,
@@ -14763,16 +14810,21 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
14763
14810
  (row) => selectedKeys.has(row[rowKey]),
14764
14811
  [selectedKeys, rowKey]
14765
14812
  );
14813
+ const canSelectRow = react.useCallback(
14814
+ (row) => isRowSelectable ? isRowSelectable(row) : true,
14815
+ [isRowSelectable]
14816
+ );
14766
14817
  const getIsAllChecked = react.useCallback(
14767
14818
  (rws) => {
14768
- const total = rws.length;
14819
+ const targets = rws.filter(canSelectRow);
14820
+ const total = targets.length;
14769
14821
  if (total === 0) return false;
14770
- const cnt = rws.filter((r) => selectedKeys.has(r[rowKey])).length;
14822
+ const cnt = targets.filter((r) => selectedKeys.has(r[rowKey])).length;
14771
14823
  if (cnt === 0) return false;
14772
14824
  if (cnt === total) return true;
14773
14825
  return null;
14774
14826
  },
14775
- [selectedKeys, rowKey]
14827
+ [selectedKeys, rowKey, canSelectRow]
14776
14828
  );
14777
14829
  const [currentPage, setCurrentPage] = react.useState(pagination?.page ?? 1);
14778
14830
  const [innerRowsPerPage, setInnerRowsPerPage] = react.useState(pagination?.rowsPerPage ?? 10);
@@ -14787,8 +14839,23 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
14787
14839
  const [headerGutter, setHeaderGutter] = react.useState(0);
14788
14840
  const [vsRange, setVsRange] = react.useState({ start: 0, end: 0 });
14789
14841
  const lastReachEndRef = react.useRef(-1);
14790
- const cbRef = react.useRef({ onVirtualUpdate, onVirtualReachEnd });
14791
- cbRef.current = { onVirtualUpdate, onVirtualReachEnd };
14842
+ const cbRef = react.useRef({ onVirtualUpdate, onVirtualReachEnd, onScroll });
14843
+ cbRef.current = { onVirtualUpdate, onVirtualReachEnd, onScroll };
14844
+ const metricsRef = react.useRef(null);
14845
+ metricsRef.current ?? (metricsRef.current = createRowMetrics(0, effectiveRowHeight));
14846
+ const metrics = metricsRef.current;
14847
+ const measuredKeyRef = react.useRef("");
14848
+ const pendingRowRef = react.useRef(null);
14849
+ const pinnedTopRef = react.useRef(-1);
14850
+ const lastEstimateRef = react.useRef(effectiveRowHeight);
14851
+ if (lastEstimateRef.current !== effectiveRowHeight) {
14852
+ lastEstimateRef.current = effectiveRowHeight;
14853
+ metrics.estimate = Math.max(1, effectiveRowHeight);
14854
+ resetRowMetrics(metrics, rowCount);
14855
+ measuredKeyRef.current = "";
14856
+ } else {
14857
+ resizeRowMetrics(metrics, rowCount);
14858
+ }
14792
14859
  const [scrolledLeft, setScrolledLeft] = react.useState(false);
14793
14860
  const [scrolledRight, setScrolledRight] = react.useState(false);
14794
14861
  const setRowCount = react.useCallback((count) => {
@@ -14798,13 +14865,16 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
14798
14865
  }, []);
14799
14866
  const calcRange = react.useCallback(
14800
14867
  (scrollTop, containerHeight) => {
14801
- const h = Math.max(1, effectiveRowHeight);
14868
+ const m = metricsRef.current;
14869
+ if (m == null) return { start: 0, end: 0 };
14802
14870
  const buf = Math.max(0, Math.floor(virtualBuffer));
14803
- const first = Math.floor(Math.max(0, scrollTop) / h);
14804
- const vis = Math.ceil(Math.max(0, containerHeight) / h);
14805
- return { start: Math.max(0, first - buf), end: Math.min(rowCount, first + vis + buf) };
14871
+ const viewTop = Math.max(0, scrollTop);
14872
+ const viewBottom = viewTop + Math.max(0, containerHeight);
14873
+ const start = Math.max(0, indexAtOffset(m, viewTop) - buf);
14874
+ const end = Math.min(rowCount, indexAtOffset(m, viewBottom) + 1 + buf);
14875
+ return { start, end };
14806
14876
  },
14807
- [effectiveRowHeight, virtualBuffer, rowCount]
14877
+ [virtualBuffer, rowCount]
14808
14878
  );
14809
14879
  const handleScroll = react.useCallback(() => {
14810
14880
  const el = scrollRef.current;
@@ -14822,9 +14892,59 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
14822
14892
  setVsRange((prev) => prev.start === start && prev.end === end ? prev : { start, end });
14823
14893
  }
14824
14894
  }, [virtualScroll, calcRange]);
14895
+ const handleScrollEvent = react.useCallback(
14896
+ (event) => {
14897
+ pendingRowRef.current = null;
14898
+ pinnedTopRef.current = -1;
14899
+ handleScroll();
14900
+ cbRef.current.onScroll?.(event);
14901
+ },
14902
+ [handleScroll]
14903
+ );
14825
14904
  react.useEffect(() => {
14826
14905
  handleScroll();
14827
14906
  }, [handleScroll, rowCount, columnWidths, loading2, height]);
14907
+ const handleScrollRef = react.useRef(handleScroll);
14908
+ handleScrollRef.current = handleScroll;
14909
+ useIsomorphicLayoutEffect(() => {
14910
+ if (!virtualScroll) return;
14911
+ const el = scrollRef.current;
14912
+ const m = metricsRef.current;
14913
+ if (!el || m == null) return;
14914
+ const pending2 = pendingRowRef.current;
14915
+ const key = `${vsRange.start}:${vsRange.end}:${rowCount}`;
14916
+ const alreadyMeasured = measuredKeyRef.current === key;
14917
+ if (alreadyMeasured && pending2 == null) return;
14918
+ measuredKeyRef.current = key;
14919
+ const anchor = vsRange.start;
14920
+ const anchorBefore = offsetOf(m, anchor);
14921
+ let changed = false;
14922
+ if (!alreadyMeasured) {
14923
+ for (const tr of el.querySelectorAll("tr[data-row-index]")) {
14924
+ const index = Number(tr.dataset.rowIndex);
14925
+ if (!Number.isInteger(index)) continue;
14926
+ const h = tr.offsetHeight;
14927
+ if (h <= 0) continue;
14928
+ if (setRowSize(m, index, h)) changed = true;
14929
+ }
14930
+ }
14931
+ if (pending2 != null) {
14932
+ const top = offsetOf(m, pending2);
14933
+ if (pinnedTopRef.current === top) {
14934
+ pendingRowRef.current = null;
14935
+ pinnedTopRef.current = -1;
14936
+ return;
14937
+ }
14938
+ pinnedTopRef.current = top;
14939
+ el.scrollTop = top;
14940
+ handleScrollRef.current();
14941
+ return;
14942
+ }
14943
+ if (!changed) return;
14944
+ const delta = offsetOf(m, anchor) - anchorBefore;
14945
+ if (delta !== 0 && el.scrollTop > 0) el.scrollTop += delta;
14946
+ handleScrollRef.current();
14947
+ });
14828
14948
  react.useEffect(() => {
14829
14949
  if (!virtualScroll) return;
14830
14950
  cbRef.current.onVirtualUpdate?.({ from: vsRange.start, to: vsRange.end });
@@ -14873,15 +14993,42 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
14873
14993
  };
14874
14994
  },
14875
14995
  getStickyStyle,
14996
+ getScrollElement: () => scrollRef.current,
14997
+ getFirstVisibleRow: () => {
14998
+ const el = scrollRef.current;
14999
+ const m = metricsRef.current;
15000
+ if (!el) return 0;
15001
+ if (virtualScroll && m != null) return indexAtOffset(m, el.scrollTop);
15002
+ const top = el.scrollTop;
15003
+ for (const tr of el.querySelectorAll("tr[data-row-index]")) {
15004
+ if (tr.offsetTop + tr.offsetHeight > top) return Number(tr.dataset.rowIndex) || 0;
15005
+ }
15006
+ return 0;
15007
+ },
15008
+ scrollToRow: (index) => {
15009
+ const el = scrollRef.current;
15010
+ const m = metricsRef.current;
15011
+ if (!el) return;
15012
+ const target = Math.min(Math.max(0, Math.floor(index)), Math.max(0, rowCount - 1));
15013
+ if (virtualScroll && m != null) {
15014
+ pendingRowRef.current = target;
15015
+ pinnedTopRef.current = -1;
15016
+ el.scrollTop = offsetOf(m, target);
15017
+ return;
15018
+ }
15019
+ const tr = el.querySelector(`tr[data-row-index="${target}"]`);
15020
+ if (tr) el.scrollTop = tr.offsetTop;
15021
+ },
14876
15022
  setRowCount,
14877
15023
  updateRowSelect: (row) => {
15024
+ if (!canSelectRow(row)) return;
14878
15025
  if (selectedKeys.has(row[rowKey]))
14879
15026
  onSelectedChange?.(selected.filter((r) => r[rowKey] !== row[rowKey]));
14880
15027
  else onSelectedChange?.([...selected, row]);
14881
15028
  },
14882
15029
  toggleSelectAll: (checked, rws) => {
14883
15030
  if (checked) {
14884
- const add = rws.filter((r) => !selectedKeys.has(r[rowKey]));
15031
+ const add = rws.filter((r) => canSelectRow(r) && !selectedKeys.has(r[rowKey]));
14885
15032
  onSelectedChange?.([...selected, ...add]);
14886
15033
  } else {
14887
15034
  const keys = new Set(rws.map((r) => r[rowKey]));
@@ -14892,8 +15039,11 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
14892
15039
  [
14893
15040
  isRowSelected,
14894
15041
  getIsAllChecked,
15042
+ canSelectRow,
14895
15043
  getStickyStyle,
14896
15044
  setRowCount,
15045
+ virtualScroll,
15046
+ rowCount,
14897
15047
  internalPagination,
14898
15048
  pagination,
14899
15049
  currentPage,
@@ -14944,12 +15094,13 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
14944
15094
  if (virtualScroll) {
14945
15095
  bodyRows = rows.slice(vsRange.start, vsRange.end);
14946
15096
  rowOffset = vsRange.start;
14947
- topSpacer = vsRange.start * effectiveRowHeight;
14948
- bottomSpacer = Math.max(0, (rowCount - vsRange.end) * effectiveRowHeight);
15097
+ topSpacer = offsetOf(metrics, vsRange.start);
15098
+ bottomSpacer = Math.max(0, totalSize(metrics) - offsetOf(metrics, vsRange.end));
14949
15099
  } else if (pageInfo) {
14950
15100
  bodyRows = rows.slice(pageInfo.startIndex, pageInfo.endIndex);
14951
15101
  rowOffset = pageInfo.startIndex;
14952
15102
  }
15103
+ const hasSelectableRow = bodyRows.some(canSelectRow);
14953
15104
  const isNoData = rowCount === 0 && !loading2;
14954
15105
  const showNoDataSlot = isNoData && emptySlot != null;
14955
15106
  const showPagination = !virtualScroll && (pagination != null || internalPagination);
@@ -14977,17 +15128,18 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
14977
15128
  const pageKeys = new Set(bodyRows.map((r) => r[rowKey]));
14978
15129
  onSelectedChange?.(selected.filter((r) => !pageKeys.has(r[rowKey])));
14979
15130
  } else {
14980
- const add = bodyRows.filter((r) => !selectedKeys.has(r[rowKey]));
15131
+ const add = bodyRows.filter((r) => canSelectRow(r) && !selectedKeys.has(r[rowKey]));
14981
15132
  onSelectedChange?.([...selected, ...add]);
14982
15133
  }
14983
15134
  };
14984
15135
  const toggleRow = (row, index) => {
15136
+ if (!canSelectRow(row)) return;
14985
15137
  const anchor = rangeAnchorRef.current;
14986
15138
  const anchorAlive = anchor != null && anchor.index !== index && rows[anchor.index]?.[rowKey] === anchor.key;
14987
15139
  if (shiftPressedRef.current && anchor != null && anchorAlive) {
14988
15140
  const [from, to] = anchor.index < index ? [anchor.index, index] : [index, anchor.index];
14989
15141
  const shouldSelect = selectedKeys.has(anchor.key);
14990
- const range = rows.slice(from, to + 1);
15142
+ const range = rows.slice(from, to + 1).filter(canSelectRow);
14991
15143
  if (shouldSelect) {
14992
15144
  const add = range.filter((r) => !selectedKeys.has(r[rowKey]));
14993
15145
  onSelectedChange?.([...selected, ...add]);
@@ -15141,7 +15293,14 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
15141
15293
  minWidth: SELECTABLE_COLUMN_WIDTH,
15142
15294
  maxWidth: SELECTABLE_COLUMN_WIDTH
15143
15295
  },
15144
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: headerCheck, onValueChange: toggleAll }) })
15296
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
15297
+ SCheckbox,
15298
+ {
15299
+ value: headerCheck,
15300
+ disabled: !hasSelectableRow,
15301
+ onValueChange: toggleAll
15302
+ }
15303
+ ) })
15145
15304
  }
15146
15305
  ),
15147
15306
  headerGroupInfo.cells.map((group) => /* @__PURE__ */ jsxRuntime.jsxs(
@@ -15183,7 +15342,14 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
15183
15342
  maxWidth: SELECTABLE_COLUMN_WIDTH
15184
15343
  },
15185
15344
  children: [
15186
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: headerCheck, onValueChange: toggleAll }) }),
15345
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
15346
+ SCheckbox,
15347
+ {
15348
+ value: headerCheck,
15349
+ disabled: !hasSelectableRow,
15350
+ onValueChange: toggleAll
15351
+ }
15352
+ ) }),
15187
15353
  /* @__PURE__ */ jsxRuntime.jsx(
15188
15354
  "span",
15189
15355
  {
@@ -15310,7 +15476,7 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
15310
15476
  "div",
15311
15477
  {
15312
15478
  ref: scrollRef,
15313
- onScroll: handleScroll,
15479
+ onScroll: handleScrollEvent,
15314
15480
  className: cn(
15315
15481
  bodyScrollClass,
15316
15482
  // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 스크롤 차단
@@ -15331,6 +15497,7 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
15331
15497
  return /* @__PURE__ */ jsxRuntime.jsxs(
15332
15498
  "tr",
15333
15499
  {
15500
+ "data-row-index": rowOffset + ri,
15334
15501
  onClick: () => onRowClick?.(row),
15335
15502
  className: cn(
15336
15503
  "group/table-row",
@@ -15363,6 +15530,7 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
15363
15530
  SCheckbox,
15364
15531
  {
15365
15532
  value: isSel,
15533
+ disabled: !canSelectRow(row),
15366
15534
  onValueChange: () => toggleRow(row, rowOffset + ri)
15367
15535
  }
15368
15536
  ) })
@@ -15480,15 +15648,20 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
15480
15648
  var COUNT_NULL = "var(--cmp-table-bar-count-null)";
15481
15649
  var totalColor = (n) => n > 0 ? "var(--cmp-table-bar-count-total)" : COUNT_NULL;
15482
15650
  var selectedColor = (n) => n > 0 ? "var(--color-brilliantblue-75)" : COUNT_NULL;
15483
- var STableBar = /* @__PURE__ */ react.forwardRef(function STableBar2({ total, selected, actions, rightActions, className, ...rest }, ref) {
15651
+ var STableBar = /* @__PURE__ */ react.forwardRef(function STableBar2({ title, total, selected, actions, rightActions, className, ...rest }, ref) {
15484
15652
  const hasTotal = total != null;
15485
15653
  const hasSelected = selected != null;
15486
15654
  const hasCount = hasTotal || hasSelected;
15655
+ const hasTitle = title != null;
15656
+ const hasLeading = hasTitle || hasCount;
15657
+ const titleId = react.useId();
15658
+ const isToolbar = Boolean(actions || rightActions);
15487
15659
  return /* @__PURE__ */ jsxRuntime.jsxs(
15488
15660
  "div",
15489
15661
  {
15490
15662
  ref,
15491
- role: actions || rightActions ? "toolbar" : void 0,
15663
+ role: isToolbar ? "toolbar" : void 0,
15664
+ "aria-labelledby": isToolbar && hasTitle ? titleId : void 0,
15492
15665
  className: cn(
15493
15666
  "flex w-full items-center gap-[var(--cmp-table-bar-section-gap)] px-[var(--sys-space-table-padding-md)]",
15494
15667
  "h-[var(--cmp-table-bar-height)] shrink-0",
@@ -15497,6 +15670,14 @@ var STableBar = /* @__PURE__ */ react.forwardRef(function STableBar2({ total, se
15497
15670
  ),
15498
15671
  ...rest,
15499
15672
  children: [
15673
+ hasTitle && /* @__PURE__ */ jsxRuntime.jsx(
15674
+ "span",
15675
+ {
15676
+ id: titleId,
15677
+ className: "typo-table-accent whitespace-nowrap text-[var(--sys-color-fg-primary)]",
15678
+ children: title
15679
+ }
15680
+ ),
15500
15681
  hasCount && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-[var(--cmp-table-content-gap)]", children: [
15501
15682
  hasTotal && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "typo-table-accent whitespace-nowrap text-[var(--sys-color-fg-primary)]", children: [
15502
15683
  "\uCD1D ",
@@ -15517,7 +15698,7 @@ var STableBar = /* @__PURE__ */ react.forwardRef(function STableBar2({ total, se
15517
15698
  ] })
15518
15699
  ] }),
15519
15700
  actions && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-[var(--cmp-table-bar-section-gap)] has-[>div:empty]:hidden", children: [
15520
- hasCount && /* @__PURE__ */ jsxRuntime.jsx(
15701
+ hasLeading && /* @__PURE__ */ jsxRuntime.jsx(
15521
15702
  SDivider,
15522
15703
  {
15523
15704
  vertical: true,
@@ -18168,10 +18349,10 @@ function ChartLegend({ items, align = "center", activeIndex, className }) {
18168
18349
  "li",
18169
18350
  {
18170
18351
  className: cn(
18171
- "flex items-center gap-(--cmp-chart-legend-legendItem-gap)",
18172
- "py-(--cmp-chart-legend-legendItem-paddingY)",
18173
- "typo-body-sm-medium text-(--cmp-chart-legend-legendItem-color)",
18174
- "transition-opacity",
18352
+ "flex items-center gap-(--cmp-chart-legendItem-gap)",
18353
+ "py-(--cmp-chart-legendItem-paddingY)",
18354
+ "typo-body-sm-medium text-(--cmp-chart-legendItem-color)",
18355
+ "transition-opacity duration-200 ease-out motion-reduce:transition-none",
18175
18356
  activeIndex != null && activeIndex !== index && "opacity-(--opacity-060)"
18176
18357
  ),
18177
18358
  children: [
@@ -18254,17 +18435,41 @@ function useElementSize() {
18254
18435
  }, []);
18255
18436
  return { ref, size };
18256
18437
  }
18438
+ function useSvgTextMetrics() {
18439
+ const ref = react.useRef(null);
18440
+ const [metrics, setMetrics] = react.useState({ top: 0, width: 0 });
18441
+ react.useEffect(() => {
18442
+ const element = ref.current;
18443
+ if (!element) return;
18444
+ let box3;
18445
+ try {
18446
+ box3 = element.getBBox();
18447
+ } catch {
18448
+ return;
18449
+ }
18450
+ setMetrics(
18451
+ (prev) => Math.abs(prev.top - box3.y) < 0.5 && Math.abs(prev.width - box3.width) < 0.5 ? prev : { top: box3.y, width: box3.width }
18452
+ );
18453
+ });
18454
+ return { ref, metrics };
18455
+ }
18257
18456
  var DOT_SIZE2 = 6;
18258
- var CURSOR_OFFSET = 12;
18259
- function place(cursor, size, extent) {
18260
- if (!extent || !size) return cursor + CURSOR_OFFSET;
18261
- const after = cursor + CURSOR_OFFSET;
18457
+ var ANCHOR_GAP_X = 4;
18458
+ var ANCHOR_GAP_Y = 0;
18459
+ function placeX(anchor, size, extent) {
18460
+ const after = anchor.right + ANCHOR_GAP_X;
18461
+ if (!extent) return after;
18262
18462
  if (after + size <= extent) return after;
18263
- const before = cursor - CURSOR_OFFSET - size;
18463
+ const before = anchor.left - ANCHOR_GAP_X - size;
18264
18464
  if (before >= 0) return before;
18265
18465
  return Math.max(0, Math.min(after, extent - size));
18266
18466
  }
18267
- function ChartTooltip({ title, items, x, y, bounds, className }) {
18467
+ function placeY(anchor, size, extent) {
18468
+ const top = anchor.top + ANCHOR_GAP_Y;
18469
+ if (!extent) return top;
18470
+ return Math.max(0, Math.min(top, extent - size));
18471
+ }
18472
+ function ChartTooltip({ title, items, anchor, bounds, className }) {
18268
18473
  const { ref, size } = useElementSize();
18269
18474
  const measured = size.width > 0 && size.height > 0;
18270
18475
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -18273,7 +18478,7 @@ function ChartTooltip({ title, items, x, y, bounds, className }) {
18273
18478
  ref,
18274
18479
  role: "tooltip",
18275
18480
  className: cn(
18276
- // left/top 대신 transform 으로 옮긴다. 좌표를 바꾸면 마우스가 움직일 때마다
18481
+ // left/top 대신 transform 으로 옮긴다. 좌표를 바꾸면 짚은 항목이 바뀔 때마다
18277
18482
  // 리플로우가 나 차트가 미세하게 흔들려 보인다. transform 은 레이아웃을
18278
18483
  // 건드리지 않으므로, 크기를 재기 전에 잠깐 밖으로 나가도 스크롤이 생기지 않는다.
18279
18484
  "pointer-events-none absolute top-0 left-0 z-10 w-max",
@@ -18283,11 +18488,11 @@ function ChartTooltip({ title, items, x, y, bounds, className }) {
18283
18488
  className
18284
18489
  ),
18285
18490
  style: {
18286
- // 크기를 재기 전에는 원점에 숨겨 둔다. 커서 옆에 그려 두면 그림 밖으로
18491
+ // 크기를 재기 전에는 원점에 숨겨 둔다. 막대 옆에 그려 두면 그림 밖으로
18287
18492
  // 넘쳐 스크롤 영역이 잠깐 늘어나고, 차트 전체가 한 번 흔들린다.
18288
18493
  // transform 도 스크롤 영역 계산에는 포함되므로 위치만 바꿔서는 못 막는다.
18289
- transform: measured ? `translate(${place(x, size.width, bounds?.width ?? 0)}px, ${place(
18290
- y,
18494
+ transform: measured ? `translate(${placeX(anchor, size.width, bounds?.width ?? 0)}px, ${placeY(
18495
+ anchor,
18291
18496
  size.height,
18292
18497
  bounds?.height ?? 0
18293
18498
  )}px)` : void 0,
@@ -18295,18 +18500,21 @@ function ChartTooltip({ title, items, x, y, bounds, className }) {
18295
18500
  },
18296
18501
  children: [
18297
18502
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-body-md-bold text-(--cmp-chart-chartTooltip-title-color)", children: title }),
18298
- items.map((item4, index) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-sd-8", children: [
18299
- /* @__PURE__ */ jsxRuntime.jsx(
18300
- "span",
18301
- {
18302
- "aria-hidden": true,
18303
- className: "shrink-0 rounded-full",
18304
- style: { width: DOT_SIZE2, height: DOT_SIZE2, background: item4.color }
18305
- }
18306
- ),
18307
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-body-sm-default text-(--sys-color-fg-primary)", children: item4.label }),
18308
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-body-sm-bold ml-auto text-(--sys-color-fg-primary)", children: item4.value })
18309
- ] }, `${item4.label}-${index}`))
18503
+ items.map((item4, index) => (
18504
+ // TODO: 토큰 생성되면 gap 을 `--cmp-chart-chartTooltipItem-gap` 으로 교체
18505
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-sd-8", children: [
18506
+ /* @__PURE__ */ jsxRuntime.jsx(
18507
+ "span",
18508
+ {
18509
+ "aria-hidden": true,
18510
+ className: "shrink-0 rounded-full",
18511
+ style: { width: DOT_SIZE2, height: DOT_SIZE2, background: item4.color }
18512
+ }
18513
+ ),
18514
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-body-sm-default text-(--sys-color-fg-primary)", children: item4.label }),
18515
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-body-sm-bold ml-auto text-(--sys-color-fg-primary)", children: item4.value })
18516
+ ] }, `${item4.label}-${index}`)
18517
+ ))
18310
18518
  ]
18311
18519
  }
18312
18520
  );
@@ -18318,11 +18526,16 @@ var BAR_SIZE_MAX = 80;
18318
18526
  var BAR_GAP = 6;
18319
18527
  var GROUP_PADDING_MIN = 8;
18320
18528
  var GROUP_PADDING_MAX = 24;
18529
+ var AXIS_EDGE_PADDING = 24;
18321
18530
  var VALUE_LABEL_GAP = 6;
18322
18531
  var BAR_RADIUS = 8;
18323
- var BAR_OPACITY_DEFAULT = "var(--opacity-080)";
18324
- var BAR_OPACITY_DIMMED = "var(--opacity-060)";
18325
- var BAR_VALUE_COLOR = "var(--sys-color-field-text-readOnly)";
18532
+ var BAR_OPACITY_DEFAULT = "var(--cmp-chart-bar-opacity-default)";
18533
+ var BAR_OPACITY_DIMMED = "var(--cmp-chart-bar-opacity-dimmed)";
18534
+ var BAR_OPACITY_HOVER = 1;
18535
+ var BAR_DATA_LABEL_COLOR = "var(--cmp-chart-bar-dataLabel-color-default)";
18536
+ var BAR_DATA_LABEL_TYPO = "typo-body-sm-medium";
18537
+ var BAR_DATA_LABEL_TYPO_HOVER = "typo-body-md-bold";
18538
+ var BAR_HOVER_TRANSITION = "transition-opacity duration-200 ease-out motion-reduce:transition-none";
18326
18539
  function bandLayout(size, categoryCount, seriesCount, barSizeMax = BAR_SIZE_MAX) {
18327
18540
  const categories = Math.max(1, categoryCount);
18328
18541
  const series = Math.max(1, seriesCount);
@@ -18333,7 +18546,8 @@ function bandLayout(size, categoryCount, seriesCount, barSizeMax = BAR_SIZE_MAX)
18333
18546
  const tMax = (maxBar - BAR_SIZE_MIN) / barRange;
18334
18547
  const bandMin = BAR_SIZE_MIN * series + gaps + GROUP_PADDING_MIN * 2;
18335
18548
  const span = barRange * series + padRange * 2;
18336
- const band = Math.min(size / categories, bandMin + span * tMax);
18549
+ const inner = Math.max(0, size - AXIS_EDGE_PADDING * 2);
18550
+ const band = Math.min(inner / categories, bandMin + span * tMax);
18337
18551
  const t = Math.min(Math.max((band - bandMin) / span, 0), tMax);
18338
18552
  const bar = BAR_SIZE_MIN + barRange * t;
18339
18553
  const padding = GROUP_PADDING_MIN + padRange * t;
@@ -18343,7 +18557,7 @@ function bandLayout(size, categoryCount, seriesCount, barSizeMax = BAR_SIZE_MAX)
18343
18557
  bar,
18344
18558
  group,
18345
18559
  padding,
18346
- offset: Math.max(0, (size - band * categories) / 2)
18560
+ offset: AXIS_EDGE_PADDING + Math.max(0, (inner - band * categories) / 2)
18347
18561
  };
18348
18562
  }
18349
18563
  var defaultFormat = (value) => value.toLocaleString();
@@ -18351,6 +18565,10 @@ function minBand(seriesCount) {
18351
18565
  const series = Math.max(1, seriesCount);
18352
18566
  return series * BAR_SIZE_MIN + (series - 1) * BAR_GAP + GROUP_PADDING_MIN * 2;
18353
18567
  }
18568
+ function barOpacity(hoveredCategory, categoryIndex) {
18569
+ if (hoveredCategory == null) return BAR_OPACITY_DEFAULT;
18570
+ return hoveredCategory === categoryIndex ? BAR_OPACITY_HOVER : BAR_OPACITY_DIMMED;
18571
+ }
18354
18572
  function barPath(x, y, width, height, direction, negative) {
18355
18573
  const r = Math.max(0, Math.min(BAR_RADIUS, width / 2, height / 2));
18356
18574
  if (r === 0) return `M${x} ${y}h${width}v${height}h${-width}Z`;
@@ -18396,8 +18614,7 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18396
18614
  const { ref: axisTextRef, size: axisText } = useElementSize();
18397
18615
  const { ref: axisSampleRef, size: axisSample } = useElementSize();
18398
18616
  const clipId = react.useId();
18399
- const [hovered, setHovered] = react.useState(null);
18400
- const hoveredCategory = hovered?.category ?? null;
18617
+ const [hoveredCategory, setHoveredCategory] = react.useState(null);
18401
18618
  const perCategory = series.length === 1;
18402
18619
  const colorAt = react.useCallback(
18403
18620
  (seriesIndex, categoryIndex) => paletteColor(palette, perCategory ? categoryIndex : seriesIndex, customColors),
@@ -18431,30 +18648,92 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18431
18648
  [axisLength, categories.length, barSlots, barSize]
18432
18649
  );
18433
18650
  const zeroRatio = ratioOf(0, scale);
18434
- const minPlotWidth = vertical ? categories.length * minBand(barSlots) : 0;
18651
+ const minPlotWidth = vertical ? categories.length * minBand(barSlots) + AXIS_EDGE_PADDING * 2 : 0;
18435
18652
  const totalHeight = height ?? (vertical ? 320 : void 0);
18436
- const plotHeight = totalHeight == null ? Math.max(1, categories.length) * minBand(barSlots) : typeof totalHeight === "number" ? Math.max(0, totalHeight - axisText.height) : `calc(${totalHeight} - ${axisText.height}px)`;
18653
+ const plotHeight = totalHeight == null ? Math.max(1, categories.length) * minBand(barSlots) + AXIS_EDGE_PADDING * 2 : typeof totalHeight === "number" ? Math.max(0, totalHeight - axisText.height) : `calc(${totalHeight} - ${axisText.height}px)`;
18437
18654
  const legend = showLegend ? /* @__PURE__ */ jsxRuntime.jsx(ChartLegend, { items: series.map((s, i) => ({ name: s.name, color: colorAt(i, 0) })) }) : null;
18438
18655
  const handleMove = (event) => {
18439
18656
  if (!showTooltip || categories.length === 0 || series.length === 0) return;
18440
18657
  const rect = event.currentTarget.getBoundingClientRect();
18441
- const x = event.clientX - rect.left;
18442
- const y = event.clientY - rect.top;
18443
- const along = (vertical ? x : y) - layout.offset;
18658
+ const along = (vertical ? event.clientX - rect.left : event.clientY - rect.top) - layout.offset;
18444
18659
  const category = Math.floor(along / layout.band);
18445
- if (category < 0 || category >= categories.length) {
18446
- setHovered(null);
18447
- return;
18448
- }
18449
- setHovered({ category, x, y });
18660
+ setHoveredCategory(category >= 0 && category < categories.length ? category : null);
18450
18661
  };
18451
- const tooltipItems = hovered == null ? [] : series.flatMap((s, i) => {
18452
- const value = s.data[hovered.category];
18662
+ const tooltipItems = hoveredCategory == null ? [] : series.flatMap((s, i) => {
18663
+ const value = s.data[hoveredCategory];
18453
18664
  if (value == null || !Number.isFinite(value)) return [];
18454
- return [
18455
- { color: colorAt(i, hovered.category), label: s.name, value: formatValue(value) }
18456
- ];
18665
+ return [{ color: colorAt(i, hoveredCategory), label: s.name, value: formatValue(value) }];
18457
18666
  });
18667
+ const anchorBar = react.useMemo(() => {
18668
+ if (hoveredCategory == null || size.width <= 0 || size.height <= 0) return null;
18669
+ const pxOf = (value) => vertical ? size.height - ratioOf(value, scale) * size.height : ratioOf(value, scale) * size.width;
18670
+ const anchorOf = (value, from, to, start, thickness) => vertical ? {
18671
+ value,
18672
+ left: start,
18673
+ right: start + thickness,
18674
+ top: Math.min(pxOf(from), pxOf(to))
18675
+ } : {
18676
+ value,
18677
+ left: Math.min(pxOf(from), pxOf(to)),
18678
+ right: Math.max(pxOf(from), pxOf(to)),
18679
+ top: start
18680
+ };
18681
+ if (stacked) {
18682
+ const total = stackTotals[hoveredCategory];
18683
+ if (total == null) return null;
18684
+ const barStart = layout.offset + hoveredCategory * layout.band + (layout.band - layout.bar) / 2;
18685
+ return anchorOf(
18686
+ total.positive + total.negative,
18687
+ total.negative,
18688
+ total.positive,
18689
+ barStart,
18690
+ layout.bar
18691
+ );
18692
+ }
18693
+ let bestIndex = -1;
18694
+ let bestValue = 0;
18695
+ series.forEach((s, seriesIndex) => {
18696
+ const value = s.data[hoveredCategory];
18697
+ if (value == null || !Number.isFinite(value)) return;
18698
+ if (bestIndex === -1 || value > bestValue) {
18699
+ bestIndex = seriesIndex;
18700
+ bestValue = value;
18701
+ }
18702
+ });
18703
+ if (bestIndex === -1) return null;
18704
+ const groupStart = layout.offset + hoveredCategory * layout.band + (layout.band - layout.group) / 2;
18705
+ return anchorOf(
18706
+ bestValue,
18707
+ 0,
18708
+ bestValue,
18709
+ groupStart + bestIndex * (layout.bar + BAR_GAP),
18710
+ layout.bar
18711
+ );
18712
+ }, [
18713
+ hoveredCategory,
18714
+ size.width,
18715
+ size.height,
18716
+ vertical,
18717
+ scale,
18718
+ stacked,
18719
+ stackTotals,
18720
+ layout,
18721
+ series
18722
+ ]);
18723
+ const { ref: valueLabelRef, metrics: valueLabel } = useSvgTextMetrics();
18724
+ const anchorLabel = anchorBar == null ? "" : formatValue(anchorBar.value);
18725
+ const tooltipAnchor = react.useMemo(() => {
18726
+ if (anchorBar == null) return null;
18727
+ if (!showValueLabel || anchorBar.value < 0) return anchorBar;
18728
+ return vertical ? (
18729
+ // 라벨의 기준선은 막대 윗변에서 VALUE_LABEL_GAP 만큼 위. 거기서 글자
18730
+ // 윗변까지가 metrics.top(음수)이다.
18731
+ { ...anchorBar, top: anchorBar.top - VALUE_LABEL_GAP + valueLabel.top }
18732
+ ) : (
18733
+ // 가로 막대의 라벨은 막대 오른쪽 끝에서 시작해 폭만큼 뻗는다.
18734
+ { ...anchorBar, right: anchorBar.right + VALUE_LABEL_GAP + valueLabel.width }
18735
+ );
18736
+ }, [anchorBar, showValueLabel, vertical, valueLabel.top, valueLabel.width]);
18458
18737
  const categoryLabels = /* @__PURE__ */ jsxRuntime.jsxs(
18459
18738
  "div",
18460
18739
  {
@@ -18495,6 +18774,7 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18495
18774
  const value = s.data[categoryIndex];
18496
18775
  if (value == null || !Number.isFinite(value)) return null;
18497
18776
  const dimmed = hoveredCategory != null && hoveredCategory !== categoryIndex;
18777
+ const opacity = barOpacity(hoveredCategory, categoryIndex);
18498
18778
  const groupStart = layout.offset + categoryIndex * layout.band + (layout.band - layout.group) / 2;
18499
18779
  const offset = groupStart + seriesIndex * (layout.bar + BAR_GAP);
18500
18780
  const valueRatio = ratioOf(value, scale);
@@ -18529,9 +18809,10 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18529
18809
  "path",
18530
18810
  {
18531
18811
  d: barPath(x, y, width, barHeight, direction, negative),
18812
+ className: BAR_HOVER_TRANSITION,
18532
18813
  style: {
18533
18814
  fill: colorAt(seriesIndex, categoryIndex),
18534
- opacity: dimmed ? BAR_OPACITY_DIMMED : BAR_OPACITY_DEFAULT,
18815
+ opacity,
18535
18816
  cursor: onBarClick ? "pointer" : void 0
18536
18817
  },
18537
18818
  onClick: onBarClick ? () => onBarClick({ seriesIndex, categoryIndex, value }) : void 0,
@@ -18543,8 +18824,14 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18543
18824
  {
18544
18825
  x: labelX,
18545
18826
  y: labelY,
18546
- className: "typo-body-sm-medium",
18547
- style: { fill: BAR_VALUE_COLOR },
18827
+ className: cn(
18828
+ hoveredCategory === categoryIndex ? BAR_DATA_LABEL_TYPO_HOVER : BAR_DATA_LABEL_TYPO,
18829
+ BAR_HOVER_TRANSITION
18830
+ ),
18831
+ style: {
18832
+ fill: BAR_DATA_LABEL_COLOR,
18833
+ opacity: dimmed ? BAR_OPACITY_DIMMED : void 0
18834
+ },
18548
18835
  textAnchor: vertical ? "middle" : negative ? "end" : "start",
18549
18836
  dominantBaseline: vertical ? "auto" : "central",
18550
18837
  children: formatValue(value)
@@ -18576,6 +18863,7 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18576
18863
  const pxOf = (value) => vertical ? size.height - ratioOf(value, scale) * size.height : ratioOf(value, scale) * size.width;
18577
18864
  return categories.map((_, categoryIndex) => {
18578
18865
  const dimmed = hoveredCategory != null && hoveredCategory !== categoryIndex;
18866
+ const opacity = barOpacity(hoveredCategory, categoryIndex);
18579
18867
  const barStart = layout.offset + categoryIndex * layout.band + (layout.band - layout.bar) / 2;
18580
18868
  let positive = 0;
18581
18869
  let negative = 0;
@@ -18619,10 +18907,12 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18619
18907
  width: box3.width,
18620
18908
  height: box3.height,
18621
18909
  clipPath: `url(#${segment.value < 0 ? negativeClip : positiveClip})`,
18910
+ className: BAR_HOVER_TRANSITION,
18622
18911
  style: {
18912
+ // 조각과 라벨에 따로 건다 — 묶음(<g>)에 걸면 호버가 없을 때의
18913
+ // 기본 흐림까지 합계 라벨에 묻어 숫자가 늘 옅어진다.
18623
18914
  fill: colorAt(segment.seriesIndex, categoryIndex),
18624
- // 흐림은 조각에만 건다 — 묶음에 걸면 합계 라벨까지 같이 흐려진다.
18625
- opacity: dimmed ? BAR_OPACITY_DIMMED : BAR_OPACITY_DEFAULT,
18915
+ opacity,
18626
18916
  cursor: onBarClick ? "pointer" : void 0
18627
18917
  },
18628
18918
  onClick: onBarClick ? () => onBarClick({
@@ -18640,8 +18930,14 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18640
18930
  {
18641
18931
  x: labelX,
18642
18932
  y: labelY,
18643
- className: "typo-body-sm-medium",
18644
- style: { fill: BAR_VALUE_COLOR },
18933
+ className: cn(
18934
+ hoveredCategory === categoryIndex ? BAR_DATA_LABEL_TYPO_HOVER : BAR_DATA_LABEL_TYPO,
18935
+ BAR_HOVER_TRANSITION
18936
+ ),
18937
+ style: {
18938
+ fill: BAR_DATA_LABEL_COLOR,
18939
+ opacity: dimmed ? BAR_OPACITY_DIMMED : void 0
18940
+ },
18645
18941
  textAnchor: vertical ? "middle" : total >= 0 ? "start" : "end",
18646
18942
  dominantBaseline: vertical ? "auto" : "central",
18647
18943
  children: formatValue(total)
@@ -18691,7 +18987,7 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18691
18987
  ref: plotRef,
18692
18988
  className: "relative h-full w-full",
18693
18989
  onMouseMove: handleMove,
18694
- onMouseLeave: () => setHovered(null),
18990
+ onMouseLeave: () => setHoveredCategory(null),
18695
18991
  children: [
18696
18992
  size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
18697
18993
  "svg",
@@ -18701,6 +18997,19 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18701
18997
  role: "img",
18702
18998
  "aria-label": `${categories.length}\uAC1C \uD56D\uBAA9, ${series.length}\uAC1C \uACC4\uC5F4 ${stacked ? "\uB204\uC801 " : ""}\uB9C9\uB300 \uADF8\uB798\uD504`,
18703
18999
  children: [
19000
+ /* @__PURE__ */ jsxRuntime.jsx(
19001
+ "text",
19002
+ {
19003
+ ref: valueLabelRef,
19004
+ "aria-hidden": true,
19005
+ x: 0,
19006
+ y: 0,
19007
+ className: BAR_DATA_LABEL_TYPO_HOVER,
19008
+ dominantBaseline: "auto",
19009
+ style: { visibility: "hidden" },
19010
+ children: anchorLabel || "0"
19011
+ }
19012
+ ),
18704
19013
  /* @__PURE__ */ jsxRuntime.jsx(
18705
19014
  ChartGrid,
18706
19015
  {
@@ -18710,12 +19019,12 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18710
19019
  height: size.height
18711
19020
  }
18712
19021
  ),
18713
- hovered != null && /* @__PURE__ */ jsxRuntime.jsx(
19022
+ hoveredCategory != null && /* @__PURE__ */ jsxRuntime.jsx(
18714
19023
  "rect",
18715
19024
  {
18716
19025
  "aria-hidden": true,
18717
- x: vertical ? layout.offset + hovered.category * layout.band : 0,
18718
- y: vertical ? 0 : layout.offset + hovered.category * layout.band,
19026
+ x: vertical ? layout.offset + hoveredCategory * layout.band : 0,
19027
+ y: vertical ? 0 : layout.offset + hoveredCategory * layout.band,
18719
19028
  width: vertical ? layout.band : size.width,
18720
19029
  height: vertical ? size.height : layout.band,
18721
19030
  style: {
@@ -18728,13 +19037,12 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18728
19037
  ]
18729
19038
  }
18730
19039
  ),
18731
- showTooltip && hovered != null && series.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
19040
+ showTooltip && hoveredCategory != null && tooltipAnchor != null && tooltipItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
18732
19041
  ChartTooltip,
18733
19042
  {
18734
- title: categories[hovered.category] ?? "",
19043
+ title: categories[hoveredCategory] ?? "",
18735
19044
  items: tooltipItems,
18736
- x: hovered.x,
18737
- y: hovered.y,
19045
+ anchor: tooltipAnchor,
18738
19046
  bounds: size
18739
19047
  }
18740
19048
  )