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.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
  );
@@ -15603,15 +15648,20 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
15603
15648
  var COUNT_NULL = "var(--cmp-table-bar-count-null)";
15604
15649
  var totalColor = (n) => n > 0 ? "var(--cmp-table-bar-count-total)" : COUNT_NULL;
15605
15650
  var selectedColor = (n) => n > 0 ? "var(--color-brilliantblue-75)" : COUNT_NULL;
15606
- 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) {
15607
15652
  const hasTotal = total != null;
15608
15653
  const hasSelected = selected != null;
15609
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);
15610
15659
  return /* @__PURE__ */ jsxRuntime.jsxs(
15611
15660
  "div",
15612
15661
  {
15613
15662
  ref,
15614
- role: actions || rightActions ? "toolbar" : void 0,
15663
+ role: isToolbar ? "toolbar" : void 0,
15664
+ "aria-labelledby": isToolbar && hasTitle ? titleId : void 0,
15615
15665
  className: cn(
15616
15666
  "flex w-full items-center gap-[var(--cmp-table-bar-section-gap)] px-[var(--sys-space-table-padding-md)]",
15617
15667
  "h-[var(--cmp-table-bar-height)] shrink-0",
@@ -15620,6 +15670,14 @@ var STableBar = /* @__PURE__ */ react.forwardRef(function STableBar2({ total, se
15620
15670
  ),
15621
15671
  ...rest,
15622
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
+ ),
15623
15681
  hasCount && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-[var(--cmp-table-content-gap)]", children: [
15624
15682
  hasTotal && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "typo-table-accent whitespace-nowrap text-[var(--sys-color-fg-primary)]", children: [
15625
15683
  "\uCD1D ",
@@ -15640,7 +15698,7 @@ var STableBar = /* @__PURE__ */ react.forwardRef(function STableBar2({ total, se
15640
15698
  ] })
15641
15699
  ] }),
15642
15700
  actions && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-[var(--cmp-table-bar-section-gap)] has-[>div:empty]:hidden", children: [
15643
- hasCount && /* @__PURE__ */ jsxRuntime.jsx(
15701
+ hasLeading && /* @__PURE__ */ jsxRuntime.jsx(
15644
15702
  SDivider,
15645
15703
  {
15646
15704
  vertical: true,
@@ -18377,17 +18435,41 @@ function useElementSize() {
18377
18435
  }, []);
18378
18436
  return { ref, size };
18379
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
+ }
18380
18456
  var DOT_SIZE2 = 6;
18381
- var CURSOR_OFFSET = 12;
18382
- function place(cursor, size, extent) {
18383
- if (!extent || !size) return cursor + CURSOR_OFFSET;
18384
- 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;
18385
18462
  if (after + size <= extent) return after;
18386
- const before = cursor - CURSOR_OFFSET - size;
18463
+ const before = anchor.left - ANCHOR_GAP_X - size;
18387
18464
  if (before >= 0) return before;
18388
18465
  return Math.max(0, Math.min(after, extent - size));
18389
18466
  }
18390
- 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 }) {
18391
18473
  const { ref, size } = useElementSize();
18392
18474
  const measured = size.width > 0 && size.height > 0;
18393
18475
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -18396,7 +18478,7 @@ function ChartTooltip({ title, items, x, y, bounds, className }) {
18396
18478
  ref,
18397
18479
  role: "tooltip",
18398
18480
  className: cn(
18399
- // left/top 대신 transform 으로 옮긴다. 좌표를 바꾸면 마우스가 움직일 때마다
18481
+ // left/top 대신 transform 으로 옮긴다. 좌표를 바꾸면 짚은 항목이 바뀔 때마다
18400
18482
  // 리플로우가 나 차트가 미세하게 흔들려 보인다. transform 은 레이아웃을
18401
18483
  // 건드리지 않으므로, 크기를 재기 전에 잠깐 밖으로 나가도 스크롤이 생기지 않는다.
18402
18484
  "pointer-events-none absolute top-0 left-0 z-10 w-max",
@@ -18406,11 +18488,11 @@ function ChartTooltip({ title, items, x, y, bounds, className }) {
18406
18488
  className
18407
18489
  ),
18408
18490
  style: {
18409
- // 크기를 재기 전에는 원점에 숨겨 둔다. 커서 옆에 그려 두면 그림 밖으로
18491
+ // 크기를 재기 전에는 원점에 숨겨 둔다. 막대 옆에 그려 두면 그림 밖으로
18410
18492
  // 넘쳐 스크롤 영역이 잠깐 늘어나고, 차트 전체가 한 번 흔들린다.
18411
18493
  // transform 도 스크롤 영역 계산에는 포함되므로 위치만 바꿔서는 못 막는다.
18412
- transform: measured ? `translate(${place(x, size.width, bounds?.width ?? 0)}px, ${place(
18413
- y,
18494
+ transform: measured ? `translate(${placeX(anchor, size.width, bounds?.width ?? 0)}px, ${placeY(
18495
+ anchor,
18414
18496
  size.height,
18415
18497
  bounds?.height ?? 0
18416
18498
  )}px)` : void 0,
@@ -18532,8 +18614,7 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18532
18614
  const { ref: axisTextRef, size: axisText } = useElementSize();
18533
18615
  const { ref: axisSampleRef, size: axisSample } = useElementSize();
18534
18616
  const clipId = react.useId();
18535
- const [hovered, setHovered] = react.useState(null);
18536
- const hoveredCategory = hovered?.category ?? null;
18617
+ const [hoveredCategory, setHoveredCategory] = react.useState(null);
18537
18618
  const perCategory = series.length === 1;
18538
18619
  const colorAt = react.useCallback(
18539
18620
  (seriesIndex, categoryIndex) => paletteColor(palette, perCategory ? categoryIndex : seriesIndex, customColors),
@@ -18574,23 +18655,85 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18574
18655
  const handleMove = (event) => {
18575
18656
  if (!showTooltip || categories.length === 0 || series.length === 0) return;
18576
18657
  const rect = event.currentTarget.getBoundingClientRect();
18577
- const x = event.clientX - rect.left;
18578
- const y = event.clientY - rect.top;
18579
- const along = (vertical ? x : y) - layout.offset;
18658
+ const along = (vertical ? event.clientX - rect.left : event.clientY - rect.top) - layout.offset;
18580
18659
  const category = Math.floor(along / layout.band);
18581
- if (category < 0 || category >= categories.length) {
18582
- setHovered(null);
18583
- return;
18584
- }
18585
- setHovered({ category, x, y });
18660
+ setHoveredCategory(category >= 0 && category < categories.length ? category : null);
18586
18661
  };
18587
- const tooltipItems = hovered == null ? [] : series.flatMap((s, i) => {
18588
- const value = s.data[hovered.category];
18662
+ const tooltipItems = hoveredCategory == null ? [] : series.flatMap((s, i) => {
18663
+ const value = s.data[hoveredCategory];
18589
18664
  if (value == null || !Number.isFinite(value)) return [];
18590
- return [
18591
- { color: colorAt(i, hovered.category), label: s.name, value: formatValue(value) }
18592
- ];
18665
+ return [{ color: colorAt(i, hoveredCategory), label: s.name, value: formatValue(value) }];
18593
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]);
18594
18737
  const categoryLabels = /* @__PURE__ */ jsxRuntime.jsxs(
18595
18738
  "div",
18596
18739
  {
@@ -18844,7 +18987,7 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18844
18987
  ref: plotRef,
18845
18988
  className: "relative h-full w-full",
18846
18989
  onMouseMove: handleMove,
18847
- onMouseLeave: () => setHovered(null),
18990
+ onMouseLeave: () => setHoveredCategory(null),
18848
18991
  children: [
18849
18992
  size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
18850
18993
  "svg",
@@ -18854,6 +18997,19 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18854
18997
  role: "img",
18855
18998
  "aria-label": `${categories.length}\uAC1C \uD56D\uBAA9, ${series.length}\uAC1C \uACC4\uC5F4 ${stacked ? "\uB204\uC801 " : ""}\uB9C9\uB300 \uADF8\uB798\uD504`,
18856
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
+ ),
18857
19013
  /* @__PURE__ */ jsxRuntime.jsx(
18858
19014
  ChartGrid,
18859
19015
  {
@@ -18863,12 +19019,12 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18863
19019
  height: size.height
18864
19020
  }
18865
19021
  ),
18866
- hovered != null && /* @__PURE__ */ jsxRuntime.jsx(
19022
+ hoveredCategory != null && /* @__PURE__ */ jsxRuntime.jsx(
18867
19023
  "rect",
18868
19024
  {
18869
19025
  "aria-hidden": true,
18870
- x: vertical ? layout.offset + hovered.category * layout.band : 0,
18871
- 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,
18872
19028
  width: vertical ? layout.band : size.width,
18873
19029
  height: vertical ? size.height : layout.band,
18874
19030
  style: {
@@ -18881,13 +19037,12 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
18881
19037
  ]
18882
19038
  }
18883
19039
  ),
18884
- showTooltip && hovered != null && series.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
19040
+ showTooltip && hoveredCategory != null && tooltipAnchor != null && tooltipItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
18885
19041
  ChartTooltip,
18886
19042
  {
18887
- title: categories[hovered.category] ?? "",
19043
+ title: categories[hoveredCategory] ?? "",
18888
19044
  items: tooltipItems,
18889
- x: hovered.x,
18890
- y: hovered.y,
19045
+ anchor: tooltipAnchor,
18891
19046
  bounds: size
18892
19047
  }
18893
19048
  )