sellmate-design-system-react 9.0.0-beta.51 → 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.js CHANGED
@@ -6395,8 +6395,8 @@ function SellmateWordmark({ className }) {
6395
6395
  "svg",
6396
6396
  {
6397
6397
  className,
6398
- width: "276",
6399
- height: "40",
6398
+ width: "220.8",
6399
+ height: "32",
6400
6400
  viewBox: "0 0 276 40",
6401
6401
  fill: "#00275F",
6402
6402
  xmlns: "http://www.w3.org/2000/svg",
@@ -6430,7 +6430,7 @@ var SLoginCardImpl = /* @__PURE__ */ forwardRef(function SLoginCard({ product, t
6430
6430
  // 로그인 화면 밖(모달·스토리)에서도 그대로 쓸 수 있는 이유다.
6431
6431
  "flex h-full w-full max-w-[640px] min-w-[368px] shrink-0 flex-col items-center justify-center gap-sd-60",
6432
6432
  // 라운드 24px·폭 상하한(640/368px)은 대응 토큰이 없어 값으로 둔다 (디자인팀 토큰 추가 요청 대상).
6433
- "rounded-[24px] bg-(--sys-color-bg-frame) p-sd-24 text-center",
6433
+ "rounded-[24px] bg-(--sys-color-bg-frame) p-sd-16 text-center",
6434
6434
  // 한글은 기본 규칙으로 두면 어절 한가운데서도 줄이 바뀐다 — `break-keep` 으로 띄어쓰기에서만
6435
6435
  // 끊는다. 끊을 자리가 아예 없는 긴 토큰(URL·주문번호)만 `wrap-break-word` 로 흘려보낸다.
6436
6436
  "break-keep wrap-break-word",
@@ -6443,7 +6443,7 @@ var SLoginCardImpl = /* @__PURE__ */ forwardRef(function SLoginCard({ product, t
6443
6443
  /* @__PURE__ */ jsx(SellmateWordmark, {}),
6444
6444
  product != null && // 24px 는 typo 프리셋(최대 18px)에 없는 등급이다 — 제품명은 로고의 일부처럼 읽혀야
6445
6445
  // 해서 본문 스케일 위에 선다. 여기만 값으로 두고, 프리셋이 생기면 갈아탄다.
6446
- /* @__PURE__ */ jsx("p", { className: "m-0 text-[24px] leading-[36px] font-bold text-(--sys-color-fg-accentLight)", children: product })
6446
+ /* @__PURE__ */ jsx("p", { className: "m-0 text-[24px] leading-[28px] font-bold text-(--sys-color-fg-accentLight)", children: product })
6447
6447
  ] }),
6448
6448
  hasIntro && // 소개와 보조 설명은 한 문단으로 읽힌다.
6449
6449
  // `text-balance` 는 상속되므로 여기 한 번만 걸어 둘을 함께 덮는다 — 가운데 정렬에서는
@@ -7096,6 +7096,149 @@ var SSectionHeaderCardRootImpl = /* @__PURE__ */ forwardRef(
7096
7096
  }
7097
7097
  );
7098
7098
  var SSectionHeaderCard2 = /* @__PURE__ */ withDisplayName(SSectionHeaderCardRootImpl, "SSectionHeaderCard");
7099
+ var OVERLAY_SCROLLBAR_SIZE = 12;
7100
+ var MIN_THUMB_SIZE = 32;
7101
+ var AUTO_HIDE_DELAY = 1e3;
7102
+ var FADE_CLASS = "transition-opacity duration-200 ease-out motion-reduce:transition-none";
7103
+ var HIDDEN = { visible: false, thumbSize: 0, thumbOffset: 0 };
7104
+ function getMetrics(viewportSize, contentSize, scrollOffset) {
7105
+ if (contentSize <= viewportSize) return HIDDEN;
7106
+ const trackSize = viewportSize;
7107
+ const maxScrollOffset = contentSize - viewportSize;
7108
+ const thumbSize = Math.min(
7109
+ trackSize,
7110
+ Math.max(MIN_THUMB_SIZE, viewportSize / contentSize * trackSize)
7111
+ );
7112
+ const maxThumbOffset = trackSize - thumbSize;
7113
+ const thumbOffset = maxScrollOffset > 0 ? scrollOffset / maxScrollOffset * maxThumbOffset : 0;
7114
+ return { visible: true, thumbSize, thumbOffset };
7115
+ }
7116
+ function OverlayScrollbars({
7117
+ scrollRef,
7118
+ vertical: showVertical = true,
7119
+ horizontal: showHorizontal = true,
7120
+ autoHide = false
7121
+ }) {
7122
+ const [vertical, setVertical] = useState(HIDDEN);
7123
+ const [horizontal, setHorizontal] = useState(HIDDEN);
7124
+ const [scrolling, setScrolling] = useState(false);
7125
+ const [dragging, setDragging] = useState(false);
7126
+ const hideTimer = useRef(null);
7127
+ const shown = !autoHide || scrolling || dragging;
7128
+ const update = useCallback(() => {
7129
+ const el = scrollRef.current;
7130
+ if (!el) return;
7131
+ setVertical(showVertical ? getMetrics(el.clientHeight, el.scrollHeight, el.scrollTop) : HIDDEN);
7132
+ setHorizontal(
7133
+ showHorizontal ? getMetrics(el.clientWidth, el.scrollWidth, el.scrollLeft) : HIDDEN
7134
+ );
7135
+ }, [scrollRef, showVertical, showHorizontal]);
7136
+ const wake = useCallback(() => {
7137
+ setScrolling(true);
7138
+ if (hideTimer.current) clearTimeout(hideTimer.current);
7139
+ hideTimer.current = setTimeout(() => setScrolling(false), AUTO_HIDE_DELAY);
7140
+ }, []);
7141
+ useEffect(() => () => void (hideTimer.current && clearTimeout(hideTimer.current)), []);
7142
+ useEffect(() => {
7143
+ const el = scrollRef.current;
7144
+ if (!el) return;
7145
+ const onScroll = () => {
7146
+ update();
7147
+ if (autoHide) wake();
7148
+ };
7149
+ update();
7150
+ el.addEventListener("scroll", onScroll, { passive: true });
7151
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
7152
+ observer?.observe(el);
7153
+ if (el.firstElementChild) observer?.observe(el.firstElementChild);
7154
+ return () => {
7155
+ el.removeEventListener("scroll", onScroll);
7156
+ observer?.disconnect();
7157
+ };
7158
+ }, [scrollRef, update, autoHide, wake]);
7159
+ const drag = (axis) => (startEvent) => {
7160
+ const el = scrollRef.current;
7161
+ const metrics = axis === "vertical" ? vertical : horizontal;
7162
+ if (!el || !metrics.visible) return;
7163
+ startEvent.preventDefault();
7164
+ startEvent.currentTarget.setPointerCapture(startEvent.pointerId);
7165
+ setDragging(true);
7166
+ const isVertical = axis === "vertical";
7167
+ const startPointer = isVertical ? startEvent.clientY : startEvent.clientX;
7168
+ const startScroll = isVertical ? el.scrollTop : el.scrollLeft;
7169
+ const viewportSize = isVertical ? el.clientHeight : el.clientWidth;
7170
+ const contentSize = isVertical ? el.scrollHeight : el.scrollWidth;
7171
+ const maxThumbOffset = viewportSize - metrics.thumbSize;
7172
+ const maxScroll = contentSize - viewportSize;
7173
+ if (maxThumbOffset <= 0 || maxScroll <= 0) {
7174
+ setDragging(false);
7175
+ return;
7176
+ }
7177
+ const onPointerMove = (moveEvent) => {
7178
+ const delta = (isVertical ? moveEvent.clientY : moveEvent.clientX) - startPointer;
7179
+ const next = startScroll + delta / maxThumbOffset * maxScroll;
7180
+ if (isVertical) el.scrollTop = next;
7181
+ else el.scrollLeft = next;
7182
+ };
7183
+ const onPointerUp = () => {
7184
+ setDragging(false);
7185
+ if (autoHide) wake();
7186
+ window.removeEventListener("pointermove", onPointerMove);
7187
+ window.removeEventListener("pointerup", onPointerUp);
7188
+ };
7189
+ window.addEventListener("pointermove", onPointerMove);
7190
+ window.addEventListener("pointerup", onPointerUp, { once: true });
7191
+ };
7192
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
7193
+ vertical.visible && // 트랙은 포인터를 받지 않는다 — 막대가 없는 자리는 아래 콘텐츠가 그대로 눌린다.
7194
+ /* @__PURE__ */ jsx(
7195
+ "div",
7196
+ {
7197
+ "aria-hidden": "true",
7198
+ className: cn(
7199
+ "pointer-events-none absolute top-0 right-0 bottom-0 z-[1] w-[12px]",
7200
+ FADE_CLASS,
7201
+ shown ? "opacity-100" : "opacity-0"
7202
+ ),
7203
+ style: { bottom: horizontal.visible ? OVERLAY_SCROLLBAR_SIZE : 0 },
7204
+ children: /* @__PURE__ */ jsx(
7205
+ "div",
7206
+ {
7207
+ className: "pointer-events-auto absolute right-[2px] w-[8px] cursor-default touch-none rounded-full bg-[var(--color-grey-45)]",
7208
+ style: {
7209
+ height: vertical.thumbSize,
7210
+ transform: `translateY(${vertical.thumbOffset}px)`
7211
+ },
7212
+ onPointerDown: drag("vertical")
7213
+ }
7214
+ )
7215
+ }
7216
+ ),
7217
+ horizontal.visible && /* @__PURE__ */ jsx(
7218
+ "div",
7219
+ {
7220
+ "aria-hidden": "true",
7221
+ className: cn(
7222
+ "pointer-events-none absolute right-0 bottom-0 left-0 z-[1] h-[12px]",
7223
+ FADE_CLASS,
7224
+ shown ? "opacity-100" : "opacity-0"
7225
+ ),
7226
+ style: { right: vertical.visible ? OVERLAY_SCROLLBAR_SIZE : 0 },
7227
+ children: /* @__PURE__ */ jsx(
7228
+ "div",
7229
+ {
7230
+ className: "pointer-events-auto absolute bottom-[2px] h-[8px] cursor-default touch-none rounded-full bg-[var(--color-grey-45)]",
7231
+ style: {
7232
+ width: horizontal.thumbSize,
7233
+ transform: `translateX(${horizontal.thumbOffset}px)`
7234
+ },
7235
+ onPointerDown: drag("horizontal")
7236
+ }
7237
+ )
7238
+ }
7239
+ )
7240
+ ] });
7241
+ }
7099
7242
 
7100
7243
  // src/lib/scrollbar.ts
7101
7244
  var FIREFOX_SCROLLBAR_CLASS = "supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-width:thin] supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_transparent]";
@@ -7115,30 +7258,61 @@ var SScrollArea = /* @__PURE__ */ forwardRef(function SScrollArea2({
7115
7258
  bordered = false,
7116
7259
  maxHeight,
7117
7260
  maxWidth,
7261
+ overlayScrollbar = false,
7118
7262
  className,
7119
7263
  style,
7120
7264
  children,
7121
7265
  ...rest
7122
7266
  }, ref) {
7267
+ const scrollRef = useRef(null);
7268
+ const setScrollRef = useCallback(
7269
+ (node) => {
7270
+ scrollRef.current = node;
7271
+ if (typeof ref === "function") ref(node);
7272
+ else if (ref) ref.current = node;
7273
+ },
7274
+ [ref]
7275
+ );
7123
7276
  const mergedStyle = { ...style };
7124
7277
  if (maxHeight) mergedStyle.maxHeight = maxHeight;
7125
7278
  if (maxWidth) mergedStyle.maxWidth = maxWidth;
7126
- return /* @__PURE__ */ jsx(
7279
+ const scroller = /* @__PURE__ */ jsx(
7127
7280
  "div",
7128
7281
  {
7129
- ref,
7282
+ ref: setScrollRef,
7130
7283
  className: cn(
7131
7284
  AXIS_CLASS[axis],
7132
- SCROLLBAR_CLASS,
7133
- background && SCROLLBAR_TRACK_BG_CLASS,
7134
- bordered && SCROLLBAR_TRACK_BORDER_CLASS,
7135
- className
7285
+ overlayScrollbar ? (
7286
+ // 바깥 상자가 크기를 쥐었으므로 스크롤 요소는 그 안을 채우기만 한다.
7287
+ `${SCROLLBAR_HIDDEN_CLASS} h-full w-full`
7288
+ ) : cn(
7289
+ SCROLLBAR_CLASS,
7290
+ background && SCROLLBAR_TRACK_BG_CLASS,
7291
+ bordered && SCROLLBAR_TRACK_BORDER_CLASS
7292
+ ),
7293
+ !overlayScrollbar && className
7136
7294
  ),
7137
- style: mergedStyle,
7295
+ style: overlayScrollbar ? void 0 : mergedStyle,
7138
7296
  ...rest,
7139
7297
  children
7140
7298
  }
7141
7299
  );
7300
+ if (!overlayScrollbar) return scroller;
7301
+ return (
7302
+ // 막대가 기준으로 삼을 상자. overflow-hidden 은 막대가 모서리 밖으로 새지 않게 한다.
7303
+ /* @__PURE__ */ jsxs("div", { className: cn("relative overflow-hidden", className), style: mergedStyle, children: [
7304
+ scroller,
7305
+ /* @__PURE__ */ jsx(
7306
+ OverlayScrollbars,
7307
+ {
7308
+ scrollRef,
7309
+ vertical: axis !== "horizontal",
7310
+ horizontal: axis !== "vertical",
7311
+ autoHide: true
7312
+ }
7313
+ )
7314
+ ] })
7315
+ );
7142
7316
  });
7143
7317
 
7144
7318
  // src/components/SScrollArea/scrollArea.config.ts
@@ -11771,22 +11945,6 @@ var NATIVE_SCROLLBAR_BACKGROUND_CLASS = {
11771
11945
  neutral: `${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_WHITE_CLASS}`,
11772
11946
  screen: `${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_CLASS}`
11773
11947
  };
11774
- var SCROLLBAR_SIZE = 12;
11775
- var SCROLLBAR_MIN_THUMB_SIZE = 32;
11776
- var getMetrics = (viewportSize, contentSize, scrollOffset) => {
11777
- if (contentSize <= viewportSize) {
11778
- return { visible: false, thumbSize: 0, thumbOffset: 0 };
11779
- }
11780
- const trackSize = viewportSize;
11781
- const maxScrollOffset = contentSize - viewportSize;
11782
- const thumbSize = Math.min(
11783
- trackSize,
11784
- Math.max(SCROLLBAR_MIN_THUMB_SIZE, viewportSize / contentSize * trackSize)
11785
- );
11786
- const maxThumbOffset = trackSize - thumbSize;
11787
- const thumbOffset = maxScrollOffset > 0 ? scrollOffset / maxScrollOffset * maxThumbOffset : 0;
11788
- return { visible: true, thumbSize, thumbOffset };
11789
- };
11790
11948
  var setForwardedRef = (ref, node) => {
11791
11949
  if (typeof ref === "function") {
11792
11950
  ref(node);
@@ -11794,119 +11952,6 @@ var setForwardedRef = (ref, node) => {
11794
11952
  }
11795
11953
  if (ref) ref.current = node;
11796
11954
  };
11797
- function SPageOverlayScrollbars({ scrollRef }) {
11798
- const [vertical, setVertical] = useState({
11799
- visible: false,
11800
- thumbSize: 0,
11801
- thumbOffset: 0
11802
- });
11803
- const [horizontal, setHorizontal] = useState({
11804
- visible: false,
11805
- thumbSize: 0,
11806
- thumbOffset: 0
11807
- });
11808
- const update = useCallback(() => {
11809
- const el = scrollRef.current;
11810
- if (!el) return;
11811
- setVertical(getMetrics(el.clientHeight, el.scrollHeight, el.scrollTop));
11812
- setHorizontal(getMetrics(el.clientWidth, el.scrollWidth, el.scrollLeft));
11813
- }, [scrollRef]);
11814
- useEffect(() => {
11815
- const el = scrollRef.current;
11816
- if (!el) return;
11817
- update();
11818
- el.addEventListener("scroll", update, { passive: true });
11819
- const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
11820
- observer?.observe(el);
11821
- if (el.firstElementChild) observer?.observe(el.firstElementChild);
11822
- return () => {
11823
- el.removeEventListener("scroll", update);
11824
- observer?.disconnect();
11825
- };
11826
- }, [scrollRef, update]);
11827
- const dragVertical = (startEvent) => {
11828
- const el = scrollRef.current;
11829
- if (!el || !vertical.visible) return;
11830
- startEvent.preventDefault();
11831
- startEvent.currentTarget.setPointerCapture(startEvent.pointerId);
11832
- const startPointer = startEvent.clientY;
11833
- const startScrollTop = el.scrollTop;
11834
- const maxThumbOffset = el.clientHeight - vertical.thumbSize;
11835
- const maxScrollTop = el.scrollHeight - el.clientHeight;
11836
- if (maxThumbOffset <= 0 || maxScrollTop <= 0) return;
11837
- const onPointerMove = (moveEvent) => {
11838
- const delta = moveEvent.clientY - startPointer;
11839
- el.scrollTop = startScrollTop + delta / maxThumbOffset * maxScrollTop;
11840
- };
11841
- const onPointerUp = () => {
11842
- window.removeEventListener("pointermove", onPointerMove);
11843
- window.removeEventListener("pointerup", onPointerUp);
11844
- };
11845
- window.addEventListener("pointermove", onPointerMove);
11846
- window.addEventListener("pointerup", onPointerUp, { once: true });
11847
- };
11848
- const dragHorizontal = (startEvent) => {
11849
- const el = scrollRef.current;
11850
- if (!el || !horizontal.visible) return;
11851
- startEvent.preventDefault();
11852
- startEvent.currentTarget.setPointerCapture(startEvent.pointerId);
11853
- const startPointer = startEvent.clientX;
11854
- const startScrollLeft = el.scrollLeft;
11855
- const maxThumbOffset = el.clientWidth - horizontal.thumbSize;
11856
- const maxScrollLeft = el.scrollWidth - el.clientWidth;
11857
- if (maxThumbOffset <= 0 || maxScrollLeft <= 0) return;
11858
- const onPointerMove = (moveEvent) => {
11859
- const delta = moveEvent.clientX - startPointer;
11860
- el.scrollLeft = startScrollLeft + delta / maxThumbOffset * maxScrollLeft;
11861
- };
11862
- const onPointerUp = () => {
11863
- window.removeEventListener("pointermove", onPointerMove);
11864
- window.removeEventListener("pointerup", onPointerUp);
11865
- };
11866
- window.addEventListener("pointermove", onPointerMove);
11867
- window.addEventListener("pointerup", onPointerUp, { once: true });
11868
- };
11869
- return /* @__PURE__ */ jsxs(Fragment, { children: [
11870
- vertical.visible && /* @__PURE__ */ jsx(
11871
- "div",
11872
- {
11873
- "aria-hidden": "true",
11874
- className: "pointer-events-none absolute bottom-0 right-0 top-0 z-[1] w-[12px]",
11875
- style: { bottom: horizontal.visible ? SCROLLBAR_SIZE : 0 },
11876
- children: /* @__PURE__ */ jsx(
11877
- "div",
11878
- {
11879
- className: "pointer-events-auto absolute right-[2px] w-[8px] cursor-default touch-none rounded-full bg-[var(--color-grey-45)]",
11880
- style: {
11881
- height: vertical.thumbSize,
11882
- transform: `translateY(${vertical.thumbOffset}px)`
11883
- },
11884
- onPointerDown: dragVertical
11885
- }
11886
- )
11887
- }
11888
- ),
11889
- horizontal.visible && /* @__PURE__ */ jsx(
11890
- "div",
11891
- {
11892
- "aria-hidden": "true",
11893
- className: "pointer-events-none absolute bottom-0 left-0 right-0 z-[1] h-[12px]",
11894
- style: { right: vertical.visible ? SCROLLBAR_SIZE : 0 },
11895
- children: /* @__PURE__ */ jsx(
11896
- "div",
11897
- {
11898
- className: "pointer-events-auto absolute bottom-[2px] h-[8px] cursor-default touch-none rounded-full bg-[var(--color-grey-45)]",
11899
- style: {
11900
- width: horizontal.thumbSize,
11901
- transform: `translateX(${horizontal.thumbOffset}px)`
11902
- },
11903
- onPointerDown: dragHorizontal
11904
- }
11905
- )
11906
- }
11907
- )
11908
- ] });
11909
- }
11910
11955
  var SPage = /* @__PURE__ */ forwardRef(function SPage2({
11911
11956
  background = "frame",
11912
11957
  scrollEndSpacing = false,
@@ -11979,7 +12024,7 @@ var SPage = /* @__PURE__ */ forwardRef(function SPage2({
11979
12024
  style: gridStyle,
11980
12025
  children: [
11981
12026
  main,
11982
- /* @__PURE__ */ jsx(SPageOverlayScrollbars, { scrollRef })
12027
+ /* @__PURE__ */ jsx(OverlayScrollbars, { scrollRef })
11983
12028
  ]
11984
12029
  }
11985
12030
  );
@@ -15575,15 +15620,20 @@ var STable = /* @__PURE__ */ forwardRef(function STable2({
15575
15620
  var COUNT_NULL = "var(--cmp-table-bar-count-null)";
15576
15621
  var totalColor = (n) => n > 0 ? "var(--cmp-table-bar-count-total)" : COUNT_NULL;
15577
15622
  var selectedColor = (n) => n > 0 ? "var(--color-brilliantblue-75)" : COUNT_NULL;
15578
- var STableBar = /* @__PURE__ */ forwardRef(function STableBar2({ total, selected, actions, rightActions, className, ...rest }, ref) {
15623
+ var STableBar = /* @__PURE__ */ forwardRef(function STableBar2({ title, total, selected, actions, rightActions, className, ...rest }, ref) {
15579
15624
  const hasTotal = total != null;
15580
15625
  const hasSelected = selected != null;
15581
15626
  const hasCount = hasTotal || hasSelected;
15627
+ const hasTitle = title != null;
15628
+ const hasLeading = hasTitle || hasCount;
15629
+ const titleId = useId();
15630
+ const isToolbar = Boolean(actions || rightActions);
15582
15631
  return /* @__PURE__ */ jsxs(
15583
15632
  "div",
15584
15633
  {
15585
15634
  ref,
15586
- role: actions || rightActions ? "toolbar" : void 0,
15635
+ role: isToolbar ? "toolbar" : void 0,
15636
+ "aria-labelledby": isToolbar && hasTitle ? titleId : void 0,
15587
15637
  className: cn(
15588
15638
  "flex w-full items-center gap-[var(--cmp-table-bar-section-gap)] px-[var(--sys-space-table-padding-md)]",
15589
15639
  "h-[var(--cmp-table-bar-height)] shrink-0",
@@ -15592,6 +15642,14 @@ var STableBar = /* @__PURE__ */ forwardRef(function STableBar2({ total, selected
15592
15642
  ),
15593
15643
  ...rest,
15594
15644
  children: [
15645
+ hasTitle && /* @__PURE__ */ jsx(
15646
+ "span",
15647
+ {
15648
+ id: titleId,
15649
+ className: "typo-table-accent whitespace-nowrap text-[var(--sys-color-fg-primary)]",
15650
+ children: title
15651
+ }
15652
+ ),
15595
15653
  hasCount && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-[var(--cmp-table-content-gap)]", children: [
15596
15654
  hasTotal && /* @__PURE__ */ jsxs("span", { className: "typo-table-accent whitespace-nowrap text-[var(--sys-color-fg-primary)]", children: [
15597
15655
  "\uCD1D ",
@@ -15612,7 +15670,7 @@ var STableBar = /* @__PURE__ */ forwardRef(function STableBar2({ total, selected
15612
15670
  ] })
15613
15671
  ] }),
15614
15672
  actions && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-[var(--cmp-table-bar-section-gap)] has-[>div:empty]:hidden", children: [
15615
- hasCount && /* @__PURE__ */ jsx(
15673
+ hasLeading && /* @__PURE__ */ jsx(
15616
15674
  SDivider,
15617
15675
  {
15618
15676
  vertical: true,
@@ -18349,17 +18407,41 @@ function useElementSize() {
18349
18407
  }, []);
18350
18408
  return { ref, size };
18351
18409
  }
18410
+ function useSvgTextMetrics() {
18411
+ const ref = useRef(null);
18412
+ const [metrics, setMetrics] = useState({ top: 0, width: 0 });
18413
+ useEffect(() => {
18414
+ const element = ref.current;
18415
+ if (!element) return;
18416
+ let box3;
18417
+ try {
18418
+ box3 = element.getBBox();
18419
+ } catch {
18420
+ return;
18421
+ }
18422
+ setMetrics(
18423
+ (prev) => Math.abs(prev.top - box3.y) < 0.5 && Math.abs(prev.width - box3.width) < 0.5 ? prev : { top: box3.y, width: box3.width }
18424
+ );
18425
+ });
18426
+ return { ref, metrics };
18427
+ }
18352
18428
  var DOT_SIZE2 = 6;
18353
- var CURSOR_OFFSET = 12;
18354
- function place(cursor, size, extent) {
18355
- if (!extent || !size) return cursor + CURSOR_OFFSET;
18356
- const after = cursor + CURSOR_OFFSET;
18429
+ var ANCHOR_GAP_X = 4;
18430
+ var ANCHOR_GAP_Y = 0;
18431
+ function placeX(anchor, size, extent) {
18432
+ const after = anchor.right + ANCHOR_GAP_X;
18433
+ if (!extent) return after;
18357
18434
  if (after + size <= extent) return after;
18358
- const before = cursor - CURSOR_OFFSET - size;
18435
+ const before = anchor.left - ANCHOR_GAP_X - size;
18359
18436
  if (before >= 0) return before;
18360
18437
  return Math.max(0, Math.min(after, extent - size));
18361
18438
  }
18362
- function ChartTooltip({ title, items, x, y, bounds, className }) {
18439
+ function placeY(anchor, size, extent) {
18440
+ const top = anchor.top + ANCHOR_GAP_Y;
18441
+ if (!extent) return top;
18442
+ return Math.max(0, Math.min(top, extent - size));
18443
+ }
18444
+ function ChartTooltip({ title, items, anchor, bounds, className }) {
18363
18445
  const { ref, size } = useElementSize();
18364
18446
  const measured = size.width > 0 && size.height > 0;
18365
18447
  return /* @__PURE__ */ jsxs(
@@ -18368,7 +18450,7 @@ function ChartTooltip({ title, items, x, y, bounds, className }) {
18368
18450
  ref,
18369
18451
  role: "tooltip",
18370
18452
  className: cn(
18371
- // left/top 대신 transform 으로 옮긴다. 좌표를 바꾸면 마우스가 움직일 때마다
18453
+ // left/top 대신 transform 으로 옮긴다. 좌표를 바꾸면 짚은 항목이 바뀔 때마다
18372
18454
  // 리플로우가 나 차트가 미세하게 흔들려 보인다. transform 은 레이아웃을
18373
18455
  // 건드리지 않으므로, 크기를 재기 전에 잠깐 밖으로 나가도 스크롤이 생기지 않는다.
18374
18456
  "pointer-events-none absolute top-0 left-0 z-10 w-max",
@@ -18378,11 +18460,11 @@ function ChartTooltip({ title, items, x, y, bounds, className }) {
18378
18460
  className
18379
18461
  ),
18380
18462
  style: {
18381
- // 크기를 재기 전에는 원점에 숨겨 둔다. 커서 옆에 그려 두면 그림 밖으로
18463
+ // 크기를 재기 전에는 원점에 숨겨 둔다. 막대 옆에 그려 두면 그림 밖으로
18382
18464
  // 넘쳐 스크롤 영역이 잠깐 늘어나고, 차트 전체가 한 번 흔들린다.
18383
18465
  // transform 도 스크롤 영역 계산에는 포함되므로 위치만 바꿔서는 못 막는다.
18384
- transform: measured ? `translate(${place(x, size.width, bounds?.width ?? 0)}px, ${place(
18385
- y,
18466
+ transform: measured ? `translate(${placeX(anchor, size.width, bounds?.width ?? 0)}px, ${placeY(
18467
+ anchor,
18386
18468
  size.height,
18387
18469
  bounds?.height ?? 0
18388
18470
  )}px)` : void 0,
@@ -18504,8 +18586,7 @@ var SBarChart = /* @__PURE__ */ forwardRef(function SBarChart2({
18504
18586
  const { ref: axisTextRef, size: axisText } = useElementSize();
18505
18587
  const { ref: axisSampleRef, size: axisSample } = useElementSize();
18506
18588
  const clipId = useId();
18507
- const [hovered, setHovered] = useState(null);
18508
- const hoveredCategory = hovered?.category ?? null;
18589
+ const [hoveredCategory, setHoveredCategory] = useState(null);
18509
18590
  const perCategory = series.length === 1;
18510
18591
  const colorAt = useCallback(
18511
18592
  (seriesIndex, categoryIndex) => paletteColor(palette, perCategory ? categoryIndex : seriesIndex, customColors),
@@ -18546,23 +18627,85 @@ var SBarChart = /* @__PURE__ */ forwardRef(function SBarChart2({
18546
18627
  const handleMove = (event) => {
18547
18628
  if (!showTooltip || categories.length === 0 || series.length === 0) return;
18548
18629
  const rect = event.currentTarget.getBoundingClientRect();
18549
- const x = event.clientX - rect.left;
18550
- const y = event.clientY - rect.top;
18551
- const along = (vertical ? x : y) - layout.offset;
18630
+ const along = (vertical ? event.clientX - rect.left : event.clientY - rect.top) - layout.offset;
18552
18631
  const category = Math.floor(along / layout.band);
18553
- if (category < 0 || category >= categories.length) {
18554
- setHovered(null);
18555
- return;
18556
- }
18557
- setHovered({ category, x, y });
18632
+ setHoveredCategory(category >= 0 && category < categories.length ? category : null);
18558
18633
  };
18559
- const tooltipItems = hovered == null ? [] : series.flatMap((s, i) => {
18560
- const value = s.data[hovered.category];
18634
+ const tooltipItems = hoveredCategory == null ? [] : series.flatMap((s, i) => {
18635
+ const value = s.data[hoveredCategory];
18561
18636
  if (value == null || !Number.isFinite(value)) return [];
18562
- return [
18563
- { color: colorAt(i, hovered.category), label: s.name, value: formatValue(value) }
18564
- ];
18637
+ return [{ color: colorAt(i, hoveredCategory), label: s.name, value: formatValue(value) }];
18565
18638
  });
18639
+ const anchorBar = useMemo(() => {
18640
+ if (hoveredCategory == null || size.width <= 0 || size.height <= 0) return null;
18641
+ const pxOf = (value) => vertical ? size.height - ratioOf(value, scale) * size.height : ratioOf(value, scale) * size.width;
18642
+ const anchorOf = (value, from, to, start, thickness) => vertical ? {
18643
+ value,
18644
+ left: start,
18645
+ right: start + thickness,
18646
+ top: Math.min(pxOf(from), pxOf(to))
18647
+ } : {
18648
+ value,
18649
+ left: Math.min(pxOf(from), pxOf(to)),
18650
+ right: Math.max(pxOf(from), pxOf(to)),
18651
+ top: start
18652
+ };
18653
+ if (stacked) {
18654
+ const total = stackTotals[hoveredCategory];
18655
+ if (total == null) return null;
18656
+ const barStart = layout.offset + hoveredCategory * layout.band + (layout.band - layout.bar) / 2;
18657
+ return anchorOf(
18658
+ total.positive + total.negative,
18659
+ total.negative,
18660
+ total.positive,
18661
+ barStart,
18662
+ layout.bar
18663
+ );
18664
+ }
18665
+ let bestIndex = -1;
18666
+ let bestValue = 0;
18667
+ series.forEach((s, seriesIndex) => {
18668
+ const value = s.data[hoveredCategory];
18669
+ if (value == null || !Number.isFinite(value)) return;
18670
+ if (bestIndex === -1 || value > bestValue) {
18671
+ bestIndex = seriesIndex;
18672
+ bestValue = value;
18673
+ }
18674
+ });
18675
+ if (bestIndex === -1) return null;
18676
+ const groupStart = layout.offset + hoveredCategory * layout.band + (layout.band - layout.group) / 2;
18677
+ return anchorOf(
18678
+ bestValue,
18679
+ 0,
18680
+ bestValue,
18681
+ groupStart + bestIndex * (layout.bar + BAR_GAP),
18682
+ layout.bar
18683
+ );
18684
+ }, [
18685
+ hoveredCategory,
18686
+ size.width,
18687
+ size.height,
18688
+ vertical,
18689
+ scale,
18690
+ stacked,
18691
+ stackTotals,
18692
+ layout,
18693
+ series
18694
+ ]);
18695
+ const { ref: valueLabelRef, metrics: valueLabel } = useSvgTextMetrics();
18696
+ const anchorLabel = anchorBar == null ? "" : formatValue(anchorBar.value);
18697
+ const tooltipAnchor = useMemo(() => {
18698
+ if (anchorBar == null) return null;
18699
+ if (!showValueLabel || anchorBar.value < 0) return anchorBar;
18700
+ return vertical ? (
18701
+ // 라벨의 기준선은 막대 윗변에서 VALUE_LABEL_GAP 만큼 위. 거기서 글자
18702
+ // 윗변까지가 metrics.top(음수)이다.
18703
+ { ...anchorBar, top: anchorBar.top - VALUE_LABEL_GAP + valueLabel.top }
18704
+ ) : (
18705
+ // 가로 막대의 라벨은 막대 오른쪽 끝에서 시작해 폭만큼 뻗는다.
18706
+ { ...anchorBar, right: anchorBar.right + VALUE_LABEL_GAP + valueLabel.width }
18707
+ );
18708
+ }, [anchorBar, showValueLabel, vertical, valueLabel.top, valueLabel.width]);
18566
18709
  const categoryLabels = /* @__PURE__ */ jsxs(
18567
18710
  "div",
18568
18711
  {
@@ -18816,7 +18959,7 @@ var SBarChart = /* @__PURE__ */ forwardRef(function SBarChart2({
18816
18959
  ref: plotRef,
18817
18960
  className: "relative h-full w-full",
18818
18961
  onMouseMove: handleMove,
18819
- onMouseLeave: () => setHovered(null),
18962
+ onMouseLeave: () => setHoveredCategory(null),
18820
18963
  children: [
18821
18964
  size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxs(
18822
18965
  "svg",
@@ -18826,6 +18969,19 @@ var SBarChart = /* @__PURE__ */ forwardRef(function SBarChart2({
18826
18969
  role: "img",
18827
18970
  "aria-label": `${categories.length}\uAC1C \uD56D\uBAA9, ${series.length}\uAC1C \uACC4\uC5F4 ${stacked ? "\uB204\uC801 " : ""}\uB9C9\uB300 \uADF8\uB798\uD504`,
18828
18971
  children: [
18972
+ /* @__PURE__ */ jsx(
18973
+ "text",
18974
+ {
18975
+ ref: valueLabelRef,
18976
+ "aria-hidden": true,
18977
+ x: 0,
18978
+ y: 0,
18979
+ className: BAR_DATA_LABEL_TYPO_HOVER,
18980
+ dominantBaseline: "auto",
18981
+ style: { visibility: "hidden" },
18982
+ children: anchorLabel || "0"
18983
+ }
18984
+ ),
18829
18985
  /* @__PURE__ */ jsx(
18830
18986
  ChartGrid,
18831
18987
  {
@@ -18835,12 +18991,12 @@ var SBarChart = /* @__PURE__ */ forwardRef(function SBarChart2({
18835
18991
  height: size.height
18836
18992
  }
18837
18993
  ),
18838
- hovered != null && /* @__PURE__ */ jsx(
18994
+ hoveredCategory != null && /* @__PURE__ */ jsx(
18839
18995
  "rect",
18840
18996
  {
18841
18997
  "aria-hidden": true,
18842
- x: vertical ? layout.offset + hovered.category * layout.band : 0,
18843
- y: vertical ? 0 : layout.offset + hovered.category * layout.band,
18998
+ x: vertical ? layout.offset + hoveredCategory * layout.band : 0,
18999
+ y: vertical ? 0 : layout.offset + hoveredCategory * layout.band,
18844
19000
  width: vertical ? layout.band : size.width,
18845
19001
  height: vertical ? size.height : layout.band,
18846
19002
  style: {
@@ -18853,13 +19009,12 @@ var SBarChart = /* @__PURE__ */ forwardRef(function SBarChart2({
18853
19009
  ]
18854
19010
  }
18855
19011
  ),
18856
- showTooltip && hovered != null && series.length > 0 && /* @__PURE__ */ jsx(
19012
+ showTooltip && hoveredCategory != null && tooltipAnchor != null && tooltipItems.length > 0 && /* @__PURE__ */ jsx(
18857
19013
  ChartTooltip,
18858
19014
  {
18859
- title: categories[hovered.category] ?? "",
19015
+ title: categories[hoveredCategory] ?? "",
18860
19016
  items: tooltipItems,
18861
- x: hovered.x,
18862
- y: hovered.y,
19017
+ anchor: tooltipAnchor,
18863
19018
  bounds: size
18864
19019
  }
18865
19020
  )