sellmate-design-system-react 9.0.0-beta.92 → 9.0.0-beta.93

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
@@ -7711,32 +7711,47 @@ var TREE_LABEL_TYPOGRAPHY = {
7711
7711
  rowHeight: "h-[32px]"
7712
7712
  }
7713
7713
  };
7714
- function collectIds(nodes) {
7715
- return nodes.flatMap((node) => [node.id, ...collectIds(node.children ?? [])]);
7716
- }
7717
- function collectSelectableIds(node) {
7718
- if (node.disabled) return [];
7719
- return [node.id, ...(node.children ?? []).flatMap(collectSelectableIds)];
7714
+ var TREE_ROW_CLASS = "group/tree-item box-border flex w-full items-center gap-(--cmp-tree-item-rowGap) overflow-hidden bg-(--cmp-tree-item-bg-default) px-(--cmp-tree-item-paddingX) text-left hover:bg-(--cmp-tree-item-bg-hover)";
7715
+ var TREE_ROW_BORDER_CLASS = "border-b border-(--cmp-tree-item-border-default)";
7716
+ var TREE_ROW_ACTIVE_CLASS = "bg-(--cmp-tree-item-bg-hover)";
7717
+ var TREE_CONTENT_CLASS = "box-border flex min-w-0 flex-1 items-center gap-(--cmp-tree-item-contentGap)";
7718
+ function treeLabelClass(size, {
7719
+ emphasized,
7720
+ disabled,
7721
+ selectable
7722
+ }) {
7723
+ const typography = TREE_LABEL_TYPOGRAPHY[size];
7724
+ return cn(
7725
+ "block min-w-0 flex-1 truncate py-(--cmp-tree-item-paddingY) text-(--cmp-tree-item-text-default)",
7726
+ emphasized ? typography.selected : typography.default,
7727
+ disabled && "opacity-60",
7728
+ disabled ? "cursor-not-allowed" : selectable ? "cursor-pointer" : void 0
7729
+ );
7720
7730
  }
7721
- function toSelectedSet(value) {
7722
- return new Set(value ?? []);
7731
+ function isRowControlClick(event) {
7732
+ const control = event.target.closest("[data-tree-control]");
7733
+ return control != null && event.currentTarget.contains(control);
7723
7734
  }
7724
- var TREE_ALL_NODE = { id: "__all__", label: "\uC804\uCCB4" };
7725
- function getSelectionState(node, selectedSet) {
7726
- const selectableChildren = (node.children ?? []).filter(
7727
- (child) => collectSelectableIds(child).length > 0
7728
- );
7729
- if (selectableChildren.length === 0) {
7730
- return { selected: selectedSet.has(node.id), indeterminate: false };
7731
- }
7732
- const childStates = selectableChildren.map(
7733
- (child) => getSelectionState(child, selectedSet)
7735
+ function TreeCaret({
7736
+ expanded,
7737
+ onToggle
7738
+ }) {
7739
+ return /* @__PURE__ */ jsxRuntime.jsx(
7740
+ SGhostButton,
7741
+ {
7742
+ icon: expanded ? "caretDown" : "caretRight",
7743
+ size: "xxs",
7744
+ ariaLabel: expanded ? "collapse tree item" : "expand tree item",
7745
+ ariaExpanded: expanded,
7746
+ onClick: (event) => {
7747
+ event.stopPropagation();
7748
+ onToggle(event);
7749
+ }
7750
+ }
7734
7751
  );
7735
- const selectedCount = childStates.filter((state2) => state2.selected).length;
7736
- return {
7737
- selected: selectedCount === childStates.length,
7738
- indeterminate: childStates.some((state2) => state2.indeterminate) || selectedCount > 0 && selectedCount < childStates.length
7739
- };
7752
+ }
7753
+ function TreeCaretSpacer() {
7754
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-(--cmp-ghostButton-xxs-size) shrink-0", "aria-hidden": "true" });
7740
7755
  }
7741
7756
  function TreeConnector({
7742
7757
  guideline,
@@ -7787,6 +7802,83 @@ function TreeConnector({
7787
7802
  ] })
7788
7803
  ] });
7789
7804
  }
7805
+ function TreeAllRow({
7806
+ label = "\uC804\uCCB4",
7807
+ selected,
7808
+ indeterminate = false,
7809
+ size = "sm",
7810
+ border = true,
7811
+ active = false,
7812
+ tabIndex,
7813
+ onKeyDown,
7814
+ onToggle,
7815
+ className,
7816
+ ...rest
7817
+ }) {
7818
+ return /* @__PURE__ */ jsxRuntime.jsx(
7819
+ "div",
7820
+ {
7821
+ role: "treeitem",
7822
+ "aria-level": 1,
7823
+ "aria-selected": selected,
7824
+ tabIndex,
7825
+ onKeyDown,
7826
+ onClick: (event) => {
7827
+ if (!isRowControlClick(event)) onToggle(event);
7828
+ },
7829
+ className: cn(
7830
+ TREE_ROW_CLASS,
7831
+ "cursor-pointer",
7832
+ TREE_LABEL_TYPOGRAPHY[size].rowHeight,
7833
+ border && TREE_ROW_BORDER_CLASS,
7834
+ active && TREE_ROW_ACTIVE_CLASS,
7835
+ className
7836
+ ),
7837
+ ...rest,
7838
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: TREE_CONTENT_CLASS, children: [
7839
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-tree-control": true, className: "flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: indeterminate ? null : selected, onValueChange: () => onToggle() }) }),
7840
+ /* @__PURE__ */ jsxRuntime.jsx(
7841
+ "span",
7842
+ {
7843
+ className: treeLabelClass(size, {
7844
+ emphasized: selected || indeterminate,
7845
+ disabled: false,
7846
+ selectable: false
7847
+ }),
7848
+ children: label
7849
+ }
7850
+ )
7851
+ ] })
7852
+ }
7853
+ );
7854
+ }
7855
+ function collectIds(nodes) {
7856
+ return nodes.flatMap((node) => [node.id, ...collectIds(node.children ?? [])]);
7857
+ }
7858
+ function collectSelectableIds(node) {
7859
+ if (node.disabled) return [];
7860
+ return [node.id, ...(node.children ?? []).flatMap(collectSelectableIds)];
7861
+ }
7862
+ function toSelectedSet(value) {
7863
+ return new Set(value ?? []);
7864
+ }
7865
+ var TREE_ALL_NODE = { id: "__all__", label: "\uC804\uCCB4" };
7866
+ function getSelectionState(node, selectedSet) {
7867
+ const selectableChildren = (node.children ?? []).filter(
7868
+ (child) => collectSelectableIds(child).length > 0
7869
+ );
7870
+ if (selectableChildren.length === 0) {
7871
+ return { selected: selectedSet.has(node.id), indeterminate: false };
7872
+ }
7873
+ const childStates = selectableChildren.map(
7874
+ (child) => getSelectionState(child, selectedSet)
7875
+ );
7876
+ const selectedCount = childStates.filter((state2) => state2.selected).length;
7877
+ return {
7878
+ selected: selectedCount === childStates.length,
7879
+ indeterminate: childStates.some((state2) => state2.indeterminate) || selectedCount > 0 && selectedCount < childStates.length
7880
+ };
7881
+ }
7790
7882
  function TreeBranch({ expanded, children }) {
7791
7883
  const branchRef = useInertRef(!expanded);
7792
7884
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -7802,10 +7894,6 @@ function TreeBranch({ expanded, children }) {
7802
7894
  }
7803
7895
  );
7804
7896
  }
7805
- function isRowControlClick(event) {
7806
- const control = event.target.closest("[data-tree-control]");
7807
- return control != null && event.currentTarget.contains(control);
7808
- }
7809
7897
  function STreeAllItem({
7810
7898
  selected,
7811
7899
  indeterminate,
@@ -7816,39 +7904,17 @@ function STreeAllItem({
7816
7904
  onKeyDown,
7817
7905
  onSelect
7818
7906
  }) {
7819
- const checkboxValue = indeterminate ? null : selected;
7820
- const typography = TREE_LABEL_TYPOGRAPHY[size];
7821
- const labelTypo = selected || indeterminate ? typography.selected : typography.default;
7822
7907
  return /* @__PURE__ */ jsxRuntime.jsx(
7823
- "div",
7908
+ TreeAllRow,
7824
7909
  {
7825
- role: "treeitem",
7826
- "aria-level": 1,
7827
- "aria-selected": selected,
7828
7910
  "data-tree-row": rowId,
7829
7911
  tabIndex,
7830
7912
  onKeyDown,
7831
- onClick: (event) => {
7832
- if (!isRowControlClick(event)) onSelect(event);
7833
- },
7834
- className: cn(
7835
- "group/tree-item box-border flex w-full cursor-pointer items-center overflow-hidden bg-(--cmp-tree-item-bg-default) px-(--cmp-tree-item-paddingX) text-left hover:bg-(--cmp-tree-item-bg-hover)",
7836
- typography.rowHeight,
7837
- border && "border-b border-(--cmp-tree-item-border-default)"
7838
- ),
7839
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "box-border flex min-w-0 flex-1 items-center gap-(--cmp-tree-item-contentGap)", children: [
7840
- /* @__PURE__ */ jsxRuntime.jsx("span", { "data-tree-control": true, className: "flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: checkboxValue, onValueChange: () => onSelect() }) }),
7841
- /* @__PURE__ */ jsxRuntime.jsx(
7842
- "span",
7843
- {
7844
- className: cn(
7845
- "block min-w-0 flex-1 truncate py-(--cmp-tree-item-paddingY) text-(--cmp-tree-item-text-default)",
7846
- labelTypo
7847
- ),
7848
- children: "\uC804\uCCB4"
7849
- }
7850
- )
7851
- ] })
7913
+ selected,
7914
+ indeterminate,
7915
+ border,
7916
+ size,
7917
+ onToggle: onSelect
7852
7918
  }
7853
7919
  );
7854
7920
  }
@@ -7889,8 +7955,6 @@ var STreeItemImpl = /* @__PURE__ */ react.forwardRef(function STreeItem({
7889
7955
  const leadingNode = typeof leading === "function" ? leading(renderState) : leading;
7890
7956
  const trailingNode = typeof trailing === "function" ? trailing(renderState) : trailing;
7891
7957
  const checkboxValue = indeterminate ? null : selected;
7892
- const typography = TREE_LABEL_TYPOGRAPHY[size];
7893
- const labelTypo = selected || indeterminate ? typography.selected : typography.default;
7894
7958
  const handleSelect = (event) => {
7895
7959
  if (node.disabled) return;
7896
7960
  onTreeSelect?.(node, event);
@@ -7916,10 +7980,9 @@ var STreeItemImpl = /* @__PURE__ */ react.forwardRef(function STreeItem({
7916
7980
  onKeyDown,
7917
7981
  onClick: handleRowClick,
7918
7982
  className: cn(
7919
- "group/tree-item box-border flex w-full items-center gap-(--cmp-tree-item-rowGap) overflow-hidden bg-(--cmp-tree-item-bg-default) px-(--cmp-tree-item-paddingX) text-left",
7920
- typography.rowHeight,
7921
- border && "border-b border-(--cmp-tree-item-border-default)",
7922
- "hover:bg-(--cmp-tree-item-bg-hover)",
7983
+ TREE_ROW_CLASS,
7984
+ TREE_LABEL_TYPOGRAPHY[size].rowHeight,
7985
+ border && TREE_ROW_BORDER_CLASS,
7923
7986
  hasChildren && "cursor-pointer",
7924
7987
  className
7925
7988
  ),
@@ -7936,25 +7999,8 @@ var STreeItemImpl = /* @__PURE__ */ react.forwardRef(function STreeItem({
7936
7999
  ancestorLast
7937
8000
  }
7938
8001
  ),
7939
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "box-border flex min-w-0 flex-1 items-center gap-(--cmp-tree-item-contentGap)", children: [
7940
- hasChildren ? /* @__PURE__ */ jsxRuntime.jsx(
7941
- SGhostButton,
7942
- {
7943
- icon: expanded ? "caretDown" : "caretRight",
7944
- size: "xxs",
7945
- ariaLabel: expanded ? "collapse tree item" : "expand tree item",
7946
- ariaExpanded: expanded,
7947
- onClick: (event) => {
7948
- event.stopPropagation();
7949
- onToggle?.(node, event);
7950
- }
7951
- }
7952
- ) : (
7953
- // 자식이 없는 줄은 펼침 버튼 자리를 빈 칸으로 남겨 라벨의 시작점을 맞춘다.
7954
- // 크기는 자리를 대신하는 그 버튼(`SGhostButton size="xxs"`)의 토큰을 그대로 쓴다 —
7955
- // 버튼 크기가 바뀌면 빈 칸도 같이 따라가야 줄이 어긋나지 않는다.
7956
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-(--cmp-ghostButton-xxs-size) shrink-0", "aria-hidden": "true" })
7957
- ),
8002
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: TREE_CONTENT_CLASS, children: [
8003
+ hasChildren ? /* @__PURE__ */ jsxRuntime.jsx(TreeCaret, { expanded, onToggle: (event) => onToggle?.(node, event) }) : /* @__PURE__ */ jsxRuntime.jsx(TreeCaretSpacer, {}),
7958
8004
  leadingNode,
7959
8005
  selectable && /* @__PURE__ */ jsxRuntime.jsx(
7960
8006
  "span",
@@ -7976,12 +8022,11 @@ var STreeItemImpl = /* @__PURE__ */ react.forwardRef(function STreeItem({
7976
8022
  "span",
7977
8023
  {
7978
8024
  "data-tree-label": true,
7979
- className: cn(
7980
- "block min-w-0 flex-1 truncate py-(--cmp-tree-item-paddingY) text-(--cmp-tree-item-text-default)",
7981
- labelTypo,
7982
- node.disabled && "opacity-60",
7983
- node.disabled ? "cursor-not-allowed" : selectable ? "cursor-pointer" : void 0
7984
- ),
8025
+ className: treeLabelClass(size, {
8026
+ emphasized: selected || indeterminate,
8027
+ disabled: Boolean(node.disabled),
8028
+ selectable
8029
+ }),
7985
8030
  children: node.label
7986
8031
  }
7987
8032
  ),
@@ -15773,13 +15818,13 @@ var SSearchInput = /* @__PURE__ */ react.forwardRef(function SSearchInput2({
15773
15818
  });
15774
15819
 
15775
15820
  // src/components/SSelect/select.config.ts
15776
- function flattenOptions(options, depth = 1) {
15821
+ function flattenOptions(options, depth = 1, inheritDisabled = true) {
15777
15822
  const out = [];
15778
15823
  for (const o of options) {
15779
15824
  const hasChildren = Array.isArray(o.children) && o.children.length > 0;
15780
15825
  if (hasChildren) {
15781
- const childFlat = flattenOptions(o.children, depth + 1);
15782
- const allChildrenDisabled = childFlat.every((c) => c.disabled);
15826
+ const childFlat = flattenOptions(o.children, depth + 1, inheritDisabled);
15827
+ const allChildrenDisabled = inheritDisabled && childFlat.every((c) => c.disabled);
15783
15828
  out.push({
15784
15829
  value: String(o.value),
15785
15830
  label: o.label,
@@ -15813,12 +15858,71 @@ function collectGroupLeafValues(flat, groupIndex) {
15813
15858
  }
15814
15859
  return values;
15815
15860
  }
15861
+ function visibleTreeRows(rows, isExpanded) {
15862
+ const out = [];
15863
+ const sourceIndex = [];
15864
+ let hiddenBelow = null;
15865
+ rows.forEach((row, i) => {
15866
+ if (hiddenBelow != null && row.depth > hiddenBelow) return;
15867
+ hiddenBelow = null;
15868
+ out.push(row);
15869
+ sourceIndex.push(i);
15870
+ if (row.isGroup && !isExpanded(row)) hiddenBelow = row.depth;
15871
+ });
15872
+ return { rows: out, sourceIndex };
15873
+ }
15874
+ function treeLineInfo(rows) {
15875
+ const isLast = new Array(rows.length);
15876
+ const seen = [];
15877
+ for (let i = rows.length - 1; i >= 0; i--) {
15878
+ const depth = rows[i].depth;
15879
+ isLast[i] = !seen[depth];
15880
+ seen[depth] = true;
15881
+ seen.length = depth + 1;
15882
+ }
15883
+ const ancestors = [];
15884
+ const parents = [];
15885
+ const parentOf = new Array(rows.length);
15886
+ const posInSet = new Array(rows.length);
15887
+ const siblingCount = /* @__PURE__ */ new Map();
15888
+ rows.forEach((row, i) => {
15889
+ const parent = row.depth > 1 ? parents[row.depth - 2] ?? -1 : -1;
15890
+ parentOf[i] = parent;
15891
+ const count = (siblingCount.get(parent) ?? 0) + 1;
15892
+ siblingCount.set(parent, count);
15893
+ posInSet[i] = count;
15894
+ parents.length = row.depth - 1;
15895
+ parents[row.depth - 1] = i;
15896
+ });
15897
+ return rows.map((row, i) => {
15898
+ ancestors.length = row.depth - 1;
15899
+ const ancestorLast = ancestors.slice();
15900
+ ancestors[row.depth - 1] = isLast[i];
15901
+ return {
15902
+ isLast: isLast[i],
15903
+ ancestorLast,
15904
+ posInSet: posInSet[i],
15905
+ setSize: siblingCount.get(parentOf[i]) ?? 1
15906
+ };
15907
+ });
15908
+ }
15909
+ function ancestorGroupValues(flat, selected) {
15910
+ const out = /* @__PURE__ */ new Set();
15911
+ const stack = [];
15912
+ for (const row of flat) {
15913
+ while (stack.length > 0 && stack[stack.length - 1].depth >= row.depth) stack.pop();
15914
+ if (selected.has(row.value)) for (const group of stack) out.add(group.value);
15915
+ if (row.isGroup) stack.push(row);
15916
+ }
15917
+ return out;
15918
+ }
15816
15919
  var isHtmlLabel = (label) => /<[a-z][\s\S]*>/i.test(label);
15817
15920
  var extractText = (html) => html.replace(/<[^>]*>/g, "");
15818
15921
  var OPTION_ROW_HEIGHT = 28;
15819
15922
  var DEFAULT_VIRTUAL_BUFFER = 10;
15820
15923
  var DEFAULT_REACH_END_THRESHOLD = 10;
15821
15924
  var DEFAULT_SEARCH_DEBOUNCE = 300;
15925
+ var NO_SEARCH_COLLAPSED = /* @__PURE__ */ new Set();
15822
15926
  var SEARCH_THRESHOLD = 11;
15823
15927
  function isListSized(el) {
15824
15928
  return el.clientHeight > 0 && getComputedStyle(el).maxHeight !== "none";
@@ -15873,8 +15977,9 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15873
15977
  className,
15874
15978
  style
15875
15979
  }, ref) {
15876
- const multi = type === "multi" || type === "multi_depth";
15980
+ const multi = type === "multi" || type === "multi_depth" || type === "multi_tree";
15877
15981
  const isDepth = type === "default_depth" || type === "multi_depth";
15982
+ const isTree = type === "tree" || type === "multi_tree";
15878
15983
  const pagingEnabled = onReachEnd != null;
15879
15984
  const partialOptions = pagingEnabled || serverSearch;
15880
15985
  const portalContainer = usePortalContainer();
@@ -15884,6 +15989,8 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15884
15989
  const [open, setOpen] = react.useState(false);
15885
15990
  const [pendingOpen, setPendingOpen] = react.useState(false);
15886
15991
  const [focusedIndex, setFocusedIndex] = react.useState(-1);
15992
+ const [expanded, setExpanded] = react.useState(() => /* @__PURE__ */ new Set());
15993
+ const [searchCollapsed, setSearchCollapsed] = react.useState(NO_SEARCH_COLLAPSED);
15887
15994
  const baseId = react.useId();
15888
15995
  const listboxId = `${baseId}-listbox`;
15889
15996
  const activeOptionId = `${baseId}-active-option`;
@@ -15907,10 +16014,11 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15907
16014
  const alignSelectedTopRef = react.useRef(false);
15908
16015
  const selectedAnchorRef = react.useRef(-1);
15909
16016
  const rangeAnchorRef = react.useRef(null);
15910
- const flat = react.useMemo(() => flattenOptions(options), [options]);
16017
+ const groupIsValue = type === "tree";
16018
+ const flat = react.useMemo(() => flattenOptions(options, 1, !groupIsValue), [groupIsValue, options]);
15911
16019
  const selectable = react.useMemo(() => selectableOptions(flat), [flat]);
15912
16020
  const hasLeaf = react.useMemo(() => flat.some((o) => !o.isGroup), [flat]);
15913
- const allOptionsDisabled = hasLeaf && selectable.length === 0;
16021
+ const allOptionsDisabled = groupIsValue ? flat.length > 0 && flat.every((o) => o.disabled) : hasLeaf && selectable.length === 0;
15914
16022
  const effectiveDisabled = disabled || allOptionsDisabled;
15915
16023
  const searchVisible = showSearch && (partialOptions || selectable.length >= SEARCH_THRESHOLD);
15916
16024
  const labelByValue = react.useMemo(() => new Map(flat.map((o) => [o.value, o.label])), [flat]);
@@ -15953,6 +16061,10 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15953
16061
  if (next) {
15954
16062
  dismissedByPointerRef.current = false;
15955
16063
  alignSelectedTopRef.current = true;
16064
+ if (isTree) {
16065
+ setExpanded(ancestorGroupValues(flat, new Set(selectedRef.current)));
16066
+ setSearchCollapsed(NO_SEARCH_COLLAPSED);
16067
+ }
15956
16068
  } else {
15957
16069
  setPendingOpen(false);
15958
16070
  setSearch("");
@@ -15995,7 +16107,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15995
16107
  },
15996
16108
  resetValidation: () => setRuleError("")
15997
16109
  });
15998
- const filtered = react.useMemo(() => {
16110
+ const searched = react.useMemo(() => {
15999
16111
  if (serverSearch || !searchVisible || search.trim() === "") return flat;
16000
16112
  const q = search.trim().toLowerCase();
16001
16113
  return filterDepthRows(
@@ -16003,23 +16115,53 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16003
16115
  (o) => (isHtmlLabel(o.label) ? extractText(o.label) : o.label).toLowerCase().includes(q)
16004
16116
  );
16005
16117
  }, [flat, search, searchVisible, serverSearch]);
16118
+ const treeSearching = isTree && search.trim() !== "";
16119
+ const isGroupExpanded = react.useCallback(
16120
+ (groupValue) => treeSearching ? !searchCollapsed.has(groupValue) : expanded.has(groupValue),
16121
+ [expanded, searchCollapsed, treeSearching]
16122
+ );
16123
+ const treeRows = react.useMemo(
16124
+ () => isTree ? visibleTreeRows(searched, (row) => isGroupExpanded(row.value)) : null,
16125
+ [isGroupExpanded, isTree, searched]
16126
+ );
16127
+ const filtered = treeRows?.rows ?? searched;
16128
+ const treeLines = react.useMemo(() => treeRows ? treeLineInfo(treeRows.rows) : [], [treeRows]);
16006
16129
  const allValues = selectable.map((o) => o.value);
16007
16130
  const allSelected = !partialOptions && allValues.length > 0 && allValues.every((v) => selectedSet.has(v));
16008
16131
  const selectAllVisible = multi && showSelectAll && !partialOptions && allValues.length > 0;
16009
- const hasItems = filtered.some((o) => !o.isGroup);
16132
+ const hasItems = isTree ? filtered.length > 0 : filtered.some((o) => !o.isGroup);
16010
16133
  const groupLeafValues = react.useMemo(() => {
16011
16134
  const map = /* @__PURE__ */ new Map();
16012
16135
  filtered.forEach((o, i) => {
16013
- if (o.isGroup) map.set(i, collectGroupLeafValues(filtered, i));
16136
+ if (!o.isGroup) return;
16137
+ map.set(
16138
+ i,
16139
+ treeRows ? collectGroupLeafValues(searched, treeRows.sourceIndex[i]) : collectGroupLeafValues(filtered, i)
16140
+ );
16014
16141
  });
16015
16142
  return map;
16016
- }, [filtered]);
16143
+ }, [filtered, searched, treeRows]);
16017
16144
  const navigableItems = react.useMemo(() => {
16018
16145
  const items = [];
16019
16146
  if (selectAllVisible) {
16020
16147
  items.push({ key: "__select_all__", isSelectAll: true });
16021
16148
  }
16022
16149
  filtered.forEach((o, i) => {
16150
+ if (o.isGroup && isTree) {
16151
+ if (multi) {
16152
+ const leafValues = groupLeafValues.get(i) ?? [];
16153
+ items.push({
16154
+ key: `__group_${i}__`,
16155
+ isGroup: true,
16156
+ index: i,
16157
+ leafValues,
16158
+ disabled: o.disabled || leafValues.length === 0
16159
+ });
16160
+ } else {
16161
+ items.push({ key: o.value, value: o.value, index: i, disabled: o.disabled });
16162
+ }
16163
+ return;
16164
+ }
16023
16165
  if (o.disabled) return;
16024
16166
  if (o.isGroup) {
16025
16167
  if (!multi) return;
@@ -16031,7 +16173,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16031
16173
  }
16032
16174
  });
16033
16175
  return items;
16034
- }, [filtered, groupLeafValues, multi, selectAllVisible]);
16176
+ }, [filtered, groupLeafValues, isTree, multi, selectAllVisible]);
16035
16177
  const selectedAnchorIndex = react.useMemo(() => {
16036
16178
  if (selectedSet.size === 0) return -1;
16037
16179
  return navigableItems.findIndex((item4) => item4.value != null && selectedSet.has(item4.value));
@@ -16042,7 +16184,10 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16042
16184
  const rowCount = filtered.length;
16043
16185
  const metrics = metricsRef.current;
16044
16186
  const lastSearchRef = react.useRef(search);
16045
- if (lastSearchRef.current !== search) {
16187
+ const lastExpansionRef = react.useRef({ expanded, searchCollapsed });
16188
+ const expansionChanged = lastExpansionRef.current.expanded !== expanded || lastExpansionRef.current.searchCollapsed !== searchCollapsed;
16189
+ lastExpansionRef.current = { expanded, searchCollapsed };
16190
+ if (lastSearchRef.current !== search || isTree && expansionChanged) {
16046
16191
  lastSearchRef.current = search;
16047
16192
  resetRowMetrics(metrics, rowCount);
16048
16193
  measuredKeyRef.current = "";
@@ -16182,7 +16327,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16182
16327
  );
16183
16328
  const fullGroupByLeaf = react.useMemo(() => {
16184
16329
  const map = /* @__PURE__ */ new Map();
16185
- if (!showChips || type !== "multi_depth") return map;
16330
+ if (!showChips || type !== "multi_depth" && type !== "multi_tree") return map;
16186
16331
  flat.forEach((o, i) => {
16187
16332
  if (!o.isGroup) return;
16188
16333
  const leafValues = collectGroupLeafValues(flat, i);
@@ -16260,6 +16405,21 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16260
16405
  rangeAnchorRef.current = null;
16261
16406
  emit3(allSelected ? [] : allValues);
16262
16407
  };
16408
+ const handleSearchChange = (next) => {
16409
+ setSearch(next);
16410
+ setSearchCollapsed(NO_SEARCH_COLLAPSED);
16411
+ };
16412
+ const handleToggleExpand = (groupValue) => {
16413
+ rangeAnchorRef.current = null;
16414
+ const toggle = (prev) => {
16415
+ const next = new Set(prev);
16416
+ if (next.has(groupValue)) next.delete(groupValue);
16417
+ else next.add(groupValue);
16418
+ return next;
16419
+ };
16420
+ if (treeSearching) setSearchCollapsed(toggle);
16421
+ else setExpanded(toggle);
16422
+ };
16263
16423
  const handleToggleGroup = (leafValues) => {
16264
16424
  if (leafValues.length === 0) return;
16265
16425
  rangeAnchorRef.current = null;
@@ -16332,6 +16492,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16332
16492
  const selectFocusedItem = (extend = false) => {
16333
16493
  if (focusedIndex < 0 || focusedIndex >= navigableItems.length) return;
16334
16494
  const focused = navigableItems[focusedIndex];
16495
+ if (focused.disabled) return;
16335
16496
  if (focused.isSelectAll) {
16336
16497
  handleSelectAll();
16337
16498
  return;
@@ -16360,6 +16521,41 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16360
16521
  moveFocusedIndex("prev");
16361
16522
  return;
16362
16523
  }
16524
+ const input = event.target instanceof HTMLInputElement ? event.target : null;
16525
+ const caretAtEdge = input == null || input.selectionStart === input.selectionEnd && input.selectionStart === (event.key === "ArrowRight" ? input.value.length : 0);
16526
+ if (isTree && (event.key === "ArrowRight" || event.key === "ArrowLeft") && caretAtEdge) {
16527
+ const rowIndex = navigableItems[focusedIndex]?.index;
16528
+ const row = rowIndex != null ? filtered[rowIndex] : void 0;
16529
+ if (rowIndex == null || row == null) return;
16530
+ event.preventDefault();
16531
+ const rowExpanded = row.isGroup && isGroupExpanded(row.value);
16532
+ if (event.key === "ArrowRight") {
16533
+ if (!row.isGroup) return;
16534
+ if (!rowExpanded) {
16535
+ handleToggleExpand(row.value);
16536
+ return;
16537
+ }
16538
+ for (let i = rowIndex + 1; i < filtered.length && filtered[i].depth > row.depth; i++) {
16539
+ const child = navigableItems.findIndex((item4) => item4.index === i);
16540
+ if (child >= 0) {
16541
+ setFocusedIndex(child);
16542
+ break;
16543
+ }
16544
+ }
16545
+ return;
16546
+ }
16547
+ if (rowExpanded) {
16548
+ handleToggleExpand(row.value);
16549
+ return;
16550
+ }
16551
+ for (let i = rowIndex - 1; i >= 0; i--) {
16552
+ if (filtered[i].depth >= row.depth) continue;
16553
+ const parent = navigableItems.findIndex((item4) => item4.index === i);
16554
+ if (parent >= 0) setFocusedIndex(parent);
16555
+ return;
16556
+ }
16557
+ return;
16558
+ }
16363
16559
  if (event.key === "Enter") {
16364
16560
  event.preventDefault();
16365
16561
  selectFocusedItem(event.shiftKey);
@@ -16404,7 +16600,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16404
16600
  type: "button",
16405
16601
  disabled: effectiveDisabled,
16406
16602
  "aria-labelledby": label ? `${fieldLabelId(triggerId)} ${triggerId}` : void 0,
16407
- "aria-haspopup": "listbox",
16603
+ "aria-haspopup": isTree ? "tree" : "listbox",
16408
16604
  "aria-expanded": open,
16409
16605
  "aria-controls": open ? listboxId : void 0,
16410
16606
  "aria-activedescendant": open && focusedIndex >= 0 ? activeOptionId : void 0,
@@ -16541,7 +16737,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16541
16737
  {
16542
16738
  ref: searchInputRef,
16543
16739
  value: search,
16544
- onValueChange: setSearch,
16740
+ onValueChange: handleSearchChange,
16545
16741
  onKeyDown: handleKeyDown,
16546
16742
  placeholder: "\uAC80\uC0C9",
16547
16743
  clearable: true,
@@ -16560,8 +16756,12 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16560
16756
  indexOffset: windowStart,
16561
16757
  topSpacer: win.top,
16562
16758
  bottomSpacer: win.bottom,
16563
- setSize: isDepth ? void 0 : rowCount,
16759
+ setSize: isDepth || isTree ? void 0 : rowCount,
16564
16760
  multi,
16761
+ tree: isTree,
16762
+ isExpanded: isGroupExpanded,
16763
+ onToggleExpand: handleToggleExpand,
16764
+ treeLines,
16565
16765
  selected,
16566
16766
  groupLeafValues,
16567
16767
  focusedValue: navigableItems[focusedIndex]?.key,
@@ -16615,6 +16815,10 @@ function ItemList({
16615
16815
  bottomSpacer,
16616
16816
  setSize,
16617
16817
  multi,
16818
+ tree,
16819
+ isExpanded,
16820
+ onToggleExpand,
16821
+ treeLines,
16618
16822
  selected,
16619
16823
  groupLeafValues,
16620
16824
  focusedValue,
@@ -16628,14 +16832,34 @@ function ItemList({
16628
16832
  onHoverValue
16629
16833
  }) {
16630
16834
  const selectedSet = react.useMemo(() => new Set(selected), [selected]);
16835
+ const checkboxShiftRef = react.useRef(false);
16631
16836
  const itemBase = cn(
16632
16837
  "group/select-item flex w-full cursor-pointer select-none items-center gap-[8px] border-none bg-transparent text-left text-[12px] leading-[20px] outline-none",
16633
16838
  "text-[color:var(--cmp-listBoxItem-content-default)]",
16634
16839
  "disabled:cursor-not-allowed disabled:text-[color:var(--cmp-listBoxItem-content-disabled)]"
16635
16840
  );
16636
16841
  const activeClass = "hover:bg-[var(--cmp-listBoxItem-bg-hover)] hover:text-[color:var(--cmp-listBoxItem-content-hover)] focus-visible:bg-[var(--cmp-listBoxItem-bg-hover)] focus-visible:text-[color:var(--cmp-listBoxItem-content-hover)]";
16637
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { id: listboxId, role: "listbox", "aria-multiselectable": multi || void 0, children: [
16638
- showSelectAll && /* @__PURE__ */ jsxRuntime.jsxs(
16842
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { id: listboxId, role: tree ? "tree" : "listbox", "aria-multiselectable": multi || void 0, children: [
16843
+ showSelectAll && (tree ? (
16844
+ // 트리의 전체선택 줄은 STree 의 "전체" 줄 그대로다 (lib/tree-row).
16845
+ /* @__PURE__ */ jsxRuntime.jsx(
16846
+ TreeAllRow,
16847
+ {
16848
+ "data-select-all-row": true,
16849
+ id: focusedValue === "__select_all__" ? activeOptionId : void 0,
16850
+ "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
16851
+ "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
16852
+ tabIndex: -1,
16853
+ label: selectAllLabel,
16854
+ selected: selectAllSelected,
16855
+ active: focusedValue === "__select_all__",
16856
+ className: "select-none",
16857
+ onMouseEnter: () => onHoverValue("__select_all__"),
16858
+ onMouseLeave: () => onHoverValue(null),
16859
+ onToggle: onSelectAll
16860
+ }
16861
+ )
16862
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
16639
16863
  "button",
16640
16864
  {
16641
16865
  type: "button",
@@ -16656,12 +16880,109 @@ function ItemList({
16656
16880
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate", children: selectAllLabel })
16657
16881
  ]
16658
16882
  }
16659
- ),
16883
+ )),
16660
16884
  topSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: { height: topSpacer } }),
16661
16885
  items.map((o, windowIndex) => {
16662
16886
  const idx = windowIndex + indexOffset;
16663
16887
  const padLeft = o.depth === 1 ? 12 : o.depth === 2 ? 20 : 28;
16664
16888
  const rowKey = `${o.value}-${idx}`;
16889
+ if (tree) {
16890
+ const togglesLeaves = multi && o.isGroup;
16891
+ const leafValues = togglesLeaves ? groupLeafValues.get(idx) ?? [] : [];
16892
+ const total = leafValues.length;
16893
+ const selectedCount = leafValues.reduce((n, v) => selectedSet.has(v) ? n + 1 : n, 0);
16894
+ const isSelected2 = selectedSet.has(o.value);
16895
+ const checkState = togglesLeaves ? total === 0 || selectedCount === 0 ? false : selectedCount === total ? true : null : isSelected2;
16896
+ const rowDisabled = o.disabled || togglesLeaves && total === 0;
16897
+ const focusKey = togglesLeaves ? `__group_${idx}__` : o.value;
16898
+ const focused = focusedValue === focusKey;
16899
+ const expanded = o.isGroup && isExpanded(o.value);
16900
+ const line = treeLines[idx];
16901
+ const toggle = () => {
16902
+ if (rowDisabled) return;
16903
+ if (togglesLeaves) onToggleGroup(leafValues);
16904
+ else onSelect(o.value, idx, checkboxShiftRef.current);
16905
+ };
16906
+ return (
16907
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events -- 키보드는 트리거(또는 검색 입력)가 aria-activedescendant 로 맡는다. 줄로 포커스를 옮기지 않는다
16908
+ /* @__PURE__ */ jsxRuntime.jsxs(
16909
+ "div",
16910
+ {
16911
+ tabIndex: -1,
16912
+ "data-select-row": true,
16913
+ "data-index": idx,
16914
+ id: focused ? activeOptionId : void 0,
16915
+ role: "treeitem",
16916
+ "aria-level": o.depth,
16917
+ "aria-posinset": line?.posInSet,
16918
+ "aria-setsize": line?.setSize,
16919
+ "aria-expanded": o.isGroup ? expanded : void 0,
16920
+ "aria-selected": checkState === true,
16921
+ "aria-disabled": rowDisabled || void 0,
16922
+ "data-select-focused": focused ? "true" : void 0,
16923
+ "data-select-active": focused ? "true" : void 0,
16924
+ className: cn(
16925
+ TREE_ROW_CLASS,
16926
+ TREE_LABEL_TYPOGRAPHY.sm.rowHeight,
16927
+ TREE_ROW_BORDER_CLASS,
16928
+ "select-none",
16929
+ rowDisabled ? "cursor-not-allowed" : "cursor-pointer",
16930
+ focused && TREE_ROW_ACTIVE_CLASS
16931
+ ),
16932
+ onMouseEnter: () => onHoverValue(focusKey),
16933
+ onMouseLeave: () => onHoverValue(null),
16934
+ onClick: (event) => {
16935
+ if (isRowControlClick(event) || rowDisabled) return;
16936
+ if (togglesLeaves) onToggleGroup(leafValues);
16937
+ else onSelect(o.value, idx, event.shiftKey);
16938
+ },
16939
+ children: [
16940
+ /* @__PURE__ */ jsxRuntime.jsx(
16941
+ TreeConnector,
16942
+ {
16943
+ guideline: true,
16944
+ size: "sm",
16945
+ depth: o.depth,
16946
+ hasChildren: o.isGroup,
16947
+ isLast: line?.isLast ?? true,
16948
+ ancestorLast: line?.ancestorLast ?? []
16949
+ }
16950
+ ),
16951
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: TREE_CONTENT_CLASS, children: [
16952
+ o.isGroup ? (
16953
+ // 비활성 그룹도 접고 편다 — 그룹이 비활성이어도 아래에 고를 수 있는 자식이 있을 수 있다.
16954
+ /* @__PURE__ */ jsxRuntime.jsx(TreeCaret, { expanded, onToggle: () => onToggleExpand(o.value) })
16955
+ ) : /* @__PURE__ */ jsxRuntime.jsx(TreeCaretSpacer, {}),
16956
+ multi && /* @__PURE__ */ jsxRuntime.jsx(
16957
+ "span",
16958
+ {
16959
+ "data-tree-control": true,
16960
+ className: "flex shrink-0",
16961
+ onPointerDownCapture: (event) => {
16962
+ checkboxShiftRef.current = event.shiftKey;
16963
+ },
16964
+ children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: checkState, disabled: rowDisabled, onValueChange: toggle })
16965
+ }
16966
+ ),
16967
+ /* @__PURE__ */ jsxRuntime.jsx(
16968
+ OptionLabel,
16969
+ {
16970
+ label: o.label,
16971
+ className: treeLabelClass("sm", {
16972
+ // 고른 줄은 굵게 — multi_tree 는 일부만 고른 그룹도 굵다 (STree 와 같다)
16973
+ emphasized: checkState !== false,
16974
+ disabled: rowDisabled,
16975
+ selectable: true
16976
+ })
16977
+ }
16978
+ )
16979
+ ] })
16980
+ ]
16981
+ },
16982
+ rowKey
16983
+ )
16984
+ );
16985
+ }
16665
16986
  if (o.isGroup) {
16666
16987
  const isDepth1 = o.depth === 1;
16667
16988
  const isFirstRow = idx === 0 && !showSelectAll && !searchVisible;
@@ -16702,13 +17023,7 @@ function ItemList({
16702
17023
  onClick: () => onToggleGroup(leafValues),
16703
17024
  children: [
16704
17025
  /* @__PURE__ */ jsxRuntime.jsx(CheckboxBox, { state: groupState, disabled: groupDisabled, focused: groupFocused }),
16705
- isHtmlLabel(o.label) ? /* @__PURE__ */ jsxRuntime.jsx(
16706
- "span",
16707
- {
16708
- className: "min-w-0 truncate",
16709
- dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(o.label) }
16710
- }
16711
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: o.label }),
17026
+ /* @__PURE__ */ jsxRuntime.jsx(OptionLabel, { label: o.label, className: "min-w-0 truncate" }),
16712
17027
  total > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
16713
17028
  "span",
16714
17029
  {
@@ -16785,13 +17100,7 @@ function ItemList({
16785
17100
  focused: focusedValue === o.value
16786
17101
  }
16787
17102
  ),
16788
- isHtmlLabel(o.label) ? /* @__PURE__ */ jsxRuntime.jsx(
16789
- "span",
16790
- {
16791
- className: "flex-1 truncate",
16792
- dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(o.label) }
16793
- }
16794
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate", children: o.label })
17103
+ /* @__PURE__ */ jsxRuntime.jsx(OptionLabel, { label: o.label, className: "flex-1 truncate" })
16795
17104
  ]
16796
17105
  },
16797
17106
  rowKey
@@ -16800,8 +17109,11 @@ function ItemList({
16800
17109
  bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: { height: bottomSpacer } })
16801
17110
  ] });
16802
17111
  }
17112
+ function OptionLabel({ label, className }) {
17113
+ return isHtmlLabel(label) ? /* @__PURE__ */ jsxRuntime.jsx("span", { className, dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className, children: label });
17114
+ }
16803
17115
  function TriggerLabel({ label }) {
16804
- return isHtmlLabel(label) ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: label });
17116
+ return /* @__PURE__ */ jsxRuntime.jsx(OptionLabel, { label, className: "truncate" });
16805
17117
  }
16806
17118
  function TriggerRemoveButton({ ariaLabel, onRemove }) {
16807
17119
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -17056,8 +17368,8 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17056
17368
  const [rowCountOverride, setRowCountOverride] = react.useState(null);
17057
17369
  const rowCount = rowCountOverride ?? rows.length;
17058
17370
  const scrollRef = react.useRef(null);
17059
- const headerScrollRef = react.useRef(null);
17060
- const [headerGutter, setHeaderGutter] = react.useState(0);
17371
+ const headerRef = react.useRef(null);
17372
+ const [headerHeight, setHeaderHeight] = react.useState(0);
17061
17373
  const [vsRange, setVsRange] = react.useState({ start: 0, end: 0 });
17062
17374
  const lastReachEndRef = react.useRef(-1);
17063
17375
  const cbRef = react.useRef({ onVirtualUpdate, onVirtualReachEnd, onScroll });
@@ -17099,17 +17411,13 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17099
17411
  );
17100
17412
  const handleScroll = react.useCallback(() => {
17101
17413
  const el = scrollRef.current;
17102
- if (!el) {
17103
- setHeaderGutter(0);
17104
- return;
17105
- }
17106
- const header = headerScrollRef.current;
17107
- if (header) header.scrollLeft = el.scrollLeft;
17108
- setHeaderGutter(el.offsetWidth - el.clientWidth);
17414
+ if (!el) return;
17415
+ const headerH = headerRef.current?.offsetHeight ?? 0;
17416
+ setHeaderHeight(headerH);
17109
17417
  setScrolledLeft(el.scrollLeft > 0);
17110
17418
  setScrolledRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
17111
17419
  if (virtualScroll) {
17112
- const { start, end } = calcRange(el.scrollTop, el.clientHeight);
17420
+ const { start, end } = calcRange(el.scrollTop, el.clientHeight - headerH);
17113
17421
  setVsRange((prev) => prev.start === start && prev.end === end ? prev : { start, end });
17114
17422
  }
17115
17423
  }, [virtualScroll, calcRange]);
@@ -17506,7 +17814,7 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17506
17814
  );
17507
17815
  };
17508
17816
  const bodyScrollClass = cn(
17509
- "min-h-0 flex-1 overflow-auto border-t border-solid border-[color:var(--cmp-table-border-color)]",
17817
+ "min-h-0 flex-1 overflow-auto",
17510
17818
  SCROLLBAR_CLASS,
17511
17819
  SCROLLBAR_TRACK_BG_CLASS,
17512
17820
  SCROLLBAR_TRACK_BORDER_CLASS
@@ -17523,28 +17831,226 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17523
17831
  selectable && /* @__PURE__ */ jsxRuntime.jsx("col", { style: { width: SELECTABLE_COLUMN_WIDTH } }),
17524
17832
  visibleCols.map((col, i) => /* @__PURE__ */ jsxRuntime.jsx("col", { style: col.autoWidth ? void 0 : { width: columnWidths[i] } }, col.name))
17525
17833
  ] });
17526
- return /* @__PURE__ */ jsxRuntime.jsxs(
17527
- "div",
17834
+ const headerTable = /* @__PURE__ */ jsxRuntime.jsxs(
17835
+ "table",
17528
17836
  {
17529
- className: cn("flex flex-col", className),
17530
- style: { width, ...style },
17531
- "data-table-id": resolvedTableId,
17837
+ className: "w-full table-fixed border-separate border-spacing-0 border-b border-solid border-[color:var(--cmp-table-border-color)] text-left",
17838
+ style: { fontSize: 12 },
17532
17839
  children: [
17533
- /* @__PURE__ */ jsxRuntime.jsxs(
17534
- "div",
17535
- {
17536
- className: cn(
17537
- // isolate: no-data/loading 오버레이(z-30·31)를 테이블 안에서 끝낸다. relative + z-auto
17538
- // 만으로는 스태킹 컨텍스트가 생기지 않아, 모달 안에 들어가면 30·31이 모달 Content
17539
- // 레벨까지 올라가 같은 컨텍스트의 floating portal 밴드(z-2)를 덮는다.
17540
- // 자손은 이미 overflow-hidden으로 클리핑되므로 테이블 내부 순서는 달라지지 않는다.
17541
- "bg-white relative isolate flex flex-col overflow-hidden",
17542
- bordered && "border border-solid border-[color:var(--cmp-table-border-color)]",
17543
- // height가 없으면 루트가 받은 높이를 끝까지 채운다. 호스트가 className으로
17544
- // `flex-1 min-h-0` 등을 줘 루트 높이가 확정된 경우, 행이 적어도 테두리 박스가
17545
- // 남은 영역을 그대로 차지한다. basis는 auto로 둬 높이가 확정되지 않은
17546
- // 기본 사용(내용만큼 커지는 케이스)에서는 동작이 바뀌지 않는다.
17547
- !height && "min-h-0 grow",
17840
+ colgroup,
17841
+ /* @__PURE__ */ jsxRuntime.jsxs("thead", { className: cn(isNoData && "opacity-40"), children: [
17842
+ headerGroupInfo && /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17843
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(
17844
+ "th",
17845
+ {
17846
+ rowSpan: 2,
17847
+ className: cn(
17848
+ "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17849
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17850
+ ),
17851
+ style: {
17852
+ width: SELECTABLE_COLUMN_WIDTH,
17853
+ minWidth: SELECTABLE_COLUMN_WIDTH,
17854
+ maxWidth: SELECTABLE_COLUMN_WIDTH
17855
+ },
17856
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
17857
+ SCheckbox,
17858
+ {
17859
+ value: headerCheck,
17860
+ disabled: !hasSelectableRow,
17861
+ onValueChange: toggleAll
17862
+ }
17863
+ ) })
17864
+ }
17865
+ ),
17866
+ headerGroupInfo.cells.map((group) => /* @__PURE__ */ jsxRuntime.jsxs(
17867
+ "th",
17868
+ {
17869
+ colSpan: group.colSpan,
17870
+ rowSpan: group.rowSpan,
17871
+ className: cn(
17872
+ "relative border-b border-r border-solid border-[color:var(--cmp-table-border-color)] bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[20px] text-[color:var(--sys-color-fg-primary)]",
17873
+ group.firstIndex + group.colSpan === visibleCols.length && "border-r-0",
17874
+ group.rowSpan === 2 && "border-b-0",
17875
+ group.className
17876
+ ),
17877
+ style: { textAlign: group.align ?? "left", ...group.style },
17878
+ children: [
17879
+ group.render ? group.render() : group.label,
17880
+ group.rowSpan === 2 && group.colSpan === 1 ? headerResizeHitArea(group.firstIndex, `group-resizer-${group.columnNames[0]}`) : null
17881
+ ]
17882
+ },
17883
+ group.columnNames.join("|")
17884
+ ))
17885
+ ] }),
17886
+ /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17887
+ selectable && !headerGroupInfo && /* @__PURE__ */ jsxRuntime.jsxs(
17888
+ "th",
17889
+ {
17890
+ className: cn(
17891
+ // 원본 .th--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
17892
+ // 하단 border는 헤더 표가 연속으로 그리므로 셀에는 두지 않는다.
17893
+ "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17894
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17895
+ ),
17896
+ style: {
17897
+ width: SELECTABLE_COLUMN_WIDTH,
17898
+ minWidth: SELECTABLE_COLUMN_WIDTH,
17899
+ maxWidth: SELECTABLE_COLUMN_WIDTH
17900
+ },
17901
+ children: [
17902
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
17903
+ SCheckbox,
17904
+ {
17905
+ value: headerCheck,
17906
+ disabled: !hasSelectableRow,
17907
+ onValueChange: toggleAll
17908
+ }
17909
+ ) }),
17910
+ /* @__PURE__ */ jsxRuntime.jsx(
17911
+ "span",
17912
+ {
17913
+ "aria-hidden": true,
17914
+ className: "pointer-events-none absolute right-0 top-1/2 h-[16px] w-px -translate-y-1/2 bg-[var(--cmp-table-header-resizingBar-color)]"
17915
+ }
17916
+ )
17917
+ ]
17918
+ }
17919
+ ),
17920
+ visibleCols.map((col, i) => {
17921
+ if (headerGroupInfo?.rowSpanNames.has(col.name)) return null;
17922
+ const isSortable = !!col.sortable && !col.renderHeader;
17923
+ const sortDir = isSortable && sort?.name === col.name ? sort.dir : void 0;
17924
+ return /* @__PURE__ */ jsxRuntime.jsxs(
17925
+ "th",
17926
+ {
17927
+ className: cn(
17928
+ // 하단 border는 헤더 표가 연속으로 그린다.
17929
+ // leading-[0]: th 의 스트럿(line-height 만큼의 빈 인라인 박스)을 없앤다.
17930
+ // 내용이 inline-flex 라 스트럿이 남아 있으면 라벨보다 큰 컨트롤(정렬 버튼 등)이
17931
+ // 들어올 때 줄 상자가 베이스라인 기준으로 늘어나 헤더 높이를 넘기고
17932
+ // 세로 가운데도 어긋난다. 글자 줄높이는 아래 th__content 가 다시 준다.
17933
+ headerGroupInfo && "border-r border-solid border-[color:var(--cmp-table-border-color)]",
17934
+ headerGroupInfo && i === visibleCols.length - 1 && "border-r-0",
17935
+ "relative bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[0] text-[color:var(--sys-color-fg-primary)]",
17936
+ stickyInfo(i).isSticky && "z-[6]",
17937
+ col.thClass
17938
+ ),
17939
+ style: {
17940
+ textAlign: col.align ?? "left",
17941
+ boxShadow: stickyShadowValue(i) ?? void 0,
17942
+ ...cellStyle(i),
17943
+ ...col.thStyle
17944
+ },
17945
+ "aria-sort": isSortable ? sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none" : void 0,
17946
+ children: [
17947
+ /* @__PURE__ */ jsxRuntime.jsx(
17948
+ "span",
17949
+ {
17950
+ className: cn(
17951
+ // 원본 th__content: align에 따라 justify-content 적용 (th도 정렬 반영)
17952
+ // th 가 leading-[0] 이므로 글자 줄높이는 여기서 준다 (truncate 가
17953
+ // overflow-hidden 이라 줄높이가 0이면 라벨이 잘려 사라진다).
17954
+ "inline-flex w-full min-w-0 items-center gap-[4px] leading-[20px]",
17955
+ col.align === "center" && "justify-center",
17956
+ col.align === "right" && "justify-end"
17957
+ ),
17958
+ children: col.renderHeader ? col.renderHeader(col, i) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
17959
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: col.label }),
17960
+ col.helpText && col.helpText.length > 0 && // 라벨이 좁아져 truncate 되더라도 아이콘까지 함께 줄어들면 안 되므로
17961
+ // shrink-0 래퍼로 감싼다 (STooltip 의 className 은 본문 쪽으로 간다).
17962
+ // 아이콘·크기·색은 SKeyValueTable 헤더 도움말과 동일하게 맞춘다.
17963
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
17964
+ STooltip2,
17965
+ {
17966
+ trigger: "hover",
17967
+ icon: "helpOutline",
17968
+ iconSize: 12,
17969
+ color: "var(--sys-color-fg-success)",
17970
+ placement: "top",
17971
+ message: col.helpText
17972
+ }
17973
+ ) }),
17974
+ col.editable && /* @__PURE__ */ jsxRuntime.jsx(
17975
+ SIcon,
17976
+ {
17977
+ name: "pageEdit",
17978
+ size: 12,
17979
+ color: "var(--sys-color-link-accent)",
17980
+ label: "\uC218\uC815 \uAC00\uB2A5",
17981
+ className: "shrink-0"
17982
+ }
17983
+ ),
17984
+ col.navigable && /* @__PURE__ */ jsxRuntime.jsx(
17985
+ SIcon,
17986
+ {
17987
+ name: "pageMove",
17988
+ size: 12,
17989
+ color: "var(--sys-color-link-accent)",
17990
+ label: "\uC0C8 \uD654\uBA74\uC5D0\uC11C \uC5F4\uAE30",
17991
+ className: "shrink-0"
17992
+ }
17993
+ ),
17994
+ col.required && // 라벨이 truncate 돼도 필수 표시는 남아야 하므로 shrink-0.
17995
+ // 아이콘·크기·색은 SKeyValueTable 헤더 필수 표시와 동일하게 맞춘다.
17996
+ /* @__PURE__ */ jsxRuntime.jsx(
17997
+ SIcon,
17998
+ {
17999
+ name: "asterisk",
18000
+ size: 12,
18001
+ color: "var(--sys-color-fg-accent)",
18002
+ className: "shrink-0"
18003
+ }
18004
+ ),
18005
+ isSortable && // 미정렬은 updown 을 default(회색)로 — 정렬할 수 있다는 것 자체가
18006
+ // 헤더에 보여야 한다. 정렬 중이면 action 색으로 올려 활성임을 알린다.
18007
+ // 라벨이 truncate 돼도 버튼은 남도록 shrink-0.
18008
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
18009
+ SGhostButton,
18010
+ {
18011
+ size: "xxs",
18012
+ intent: sortDir ? "action" : "default",
18013
+ icon: sortDir === "asc" ? "arrowUp" : sortDir === "desc" ? "arrowDown" : "updown",
18014
+ ariaLabel: `${col.label} \uC815\uB82C`,
18015
+ onClick: () => toggleSort(col)
18016
+ }
18017
+ ) })
18018
+ ] })
18019
+ }
18020
+ ),
18021
+ headerGroupInfo ? headerResizeHitArea(i, `resizer-${col.name}`) : headerResizer(i, `resizer-${col.name}`)
18022
+ ]
18023
+ },
18024
+ col.name
18025
+ );
18026
+ })
18027
+ ] })
18028
+ ] })
18029
+ ]
18030
+ }
18031
+ );
18032
+ return /* @__PURE__ */ jsxRuntime.jsxs(
18033
+ "div",
18034
+ {
18035
+ className: cn("flex flex-col", className),
18036
+ style: { width, ...style },
18037
+ "data-table-id": resolvedTableId,
18038
+ children: [
18039
+ /* @__PURE__ */ jsxRuntime.jsxs(
18040
+ "div",
18041
+ {
18042
+ className: cn(
18043
+ // isolate: no-data/loading 오버레이(z-30·31)를 테이블 안에서 끝낸다. relative + z-auto
18044
+ // 만으로는 스태킹 컨텍스트가 생기지 않아, 모달 안에 들어가면 30·31이 모달 Content
18045
+ // 레벨까지 올라가 같은 컨텍스트의 floating portal 밴드(z-2)를 덮는다.
18046
+ // 자손은 이미 overflow-hidden으로 클리핑되므로 테이블 내부 순서는 달라지지 않는다.
18047
+ "bg-white relative isolate flex flex-col overflow-hidden",
18048
+ bordered && "border border-solid border-[color:var(--cmp-table-border-color)]",
18049
+ // height가 없으면 루트가 받은 높이를 끝까지 채운다. 호스트가 className으로
18050
+ // `flex-1 min-h-0` 등을 줘 루트 높이가 확정된 경우, 행이 적어도 테두리 박스가
18051
+ // 남은 영역을 그대로 차지한다. basis는 auto로 둬 높이가 확정되지 않은
18052
+ // 기본 사용(내용만큼 커지는 케이스)에서는 동작이 바뀌지 않는다.
18053
+ !height && "min-h-0 grow",
17548
18054
  radiusClass,
17549
18055
  // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 인터랙션 차단
17550
18056
  (loading2 || rowCount === 0) && "pointer-events-none"
@@ -17554,220 +18060,20 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17554
18060
  minHeight: isNoData ? "calc(var(--cmp-table-header-height) + 60px)" : void 0
17555
18061
  },
17556
18062
  children: [
17557
- /* @__PURE__ */ jsxRuntime.jsx(
17558
- "div",
17559
- {
17560
- ref: headerScrollRef,
17561
- className: "flex-none overflow-hidden",
17562
- style: {
17563
- borderRight: headerGutter ? `${headerGutter}px solid var(--cmp-table-header-bg)` : void 0
17564
- },
17565
- children: /* @__PURE__ */ jsxRuntime.jsxs(
17566
- "table",
17567
- {
17568
- className: "w-full table-fixed border-separate border-spacing-0 text-left",
17569
- style: { fontSize: 12 },
17570
- children: [
17571
- colgroup,
17572
- /* @__PURE__ */ jsxRuntime.jsxs("thead", { className: cn(isNoData && "opacity-40"), children: [
17573
- headerGroupInfo && /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17574
- selectable && /* @__PURE__ */ jsxRuntime.jsx(
17575
- "th",
17576
- {
17577
- rowSpan: 2,
17578
- className: cn(
17579
- "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17580
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17581
- ),
17582
- style: {
17583
- width: SELECTABLE_COLUMN_WIDTH,
17584
- minWidth: SELECTABLE_COLUMN_WIDTH,
17585
- maxWidth: SELECTABLE_COLUMN_WIDTH
17586
- },
17587
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
17588
- SCheckbox,
17589
- {
17590
- value: headerCheck,
17591
- disabled: !hasSelectableRow,
17592
- onValueChange: toggleAll
17593
- }
17594
- ) })
17595
- }
17596
- ),
17597
- headerGroupInfo.cells.map((group) => /* @__PURE__ */ jsxRuntime.jsxs(
17598
- "th",
17599
- {
17600
- colSpan: group.colSpan,
17601
- rowSpan: group.rowSpan,
17602
- className: cn(
17603
- "relative border-b border-r border-solid border-[color:var(--cmp-table-border-color)] bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[20px] text-[color:var(--sys-color-fg-primary)]",
17604
- group.firstIndex + group.colSpan === visibleCols.length && "border-r-0",
17605
- group.rowSpan === 2 && "border-b-0",
17606
- group.className
17607
- ),
17608
- style: { textAlign: group.align ?? "left", ...group.style },
17609
- children: [
17610
- group.render ? group.render() : group.label,
17611
- group.rowSpan === 2 && group.colSpan === 1 ? headerResizeHitArea(
17612
- group.firstIndex,
17613
- `group-resizer-${group.columnNames[0]}`
17614
- ) : null
17615
- ]
17616
- },
17617
- group.columnNames.join("|")
17618
- ))
17619
- ] }),
17620
- /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17621
- selectable && !headerGroupInfo && /* @__PURE__ */ jsxRuntime.jsxs(
17622
- "th",
17623
- {
17624
- className: cn(
17625
- // 원본 .th--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
17626
- // 하단 border는 헤더 래퍼가 연속으로 그리므로 셀에는 두지 않는다.
17627
- "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17628
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17629
- ),
17630
- style: {
17631
- width: SELECTABLE_COLUMN_WIDTH,
17632
- minWidth: SELECTABLE_COLUMN_WIDTH,
17633
- maxWidth: SELECTABLE_COLUMN_WIDTH
17634
- },
17635
- children: [
17636
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
17637
- SCheckbox,
17638
- {
17639
- value: headerCheck,
17640
- disabled: !hasSelectableRow,
17641
- onValueChange: toggleAll
17642
- }
17643
- ) }),
17644
- /* @__PURE__ */ jsxRuntime.jsx(
17645
- "span",
17646
- {
17647
- "aria-hidden": true,
17648
- className: "pointer-events-none absolute right-0 top-1/2 h-[16px] w-px -translate-y-1/2 bg-[var(--cmp-table-header-resizingBar-color)]"
17649
- }
17650
- )
17651
- ]
17652
- }
17653
- ),
17654
- visibleCols.map((col, i) => {
17655
- if (headerGroupInfo?.rowSpanNames.has(col.name)) return null;
17656
- const isSortable = !!col.sortable && !col.renderHeader;
17657
- const sortDir = isSortable && sort?.name === col.name ? sort.dir : void 0;
17658
- return /* @__PURE__ */ jsxRuntime.jsxs(
17659
- "th",
17660
- {
17661
- className: cn(
17662
- // 하단 border는 헤더 래퍼가 연속으로 그린다 (거터 영역까지 이어짐).
17663
- // leading-[0]: th 의 스트럿(line-height 만큼의 빈 인라인 박스)을 없앤다.
17664
- // 내용이 inline-flex 라 스트럿이 남아 있으면 라벨보다 큰 컨트롤(정렬 버튼 등)이
17665
- // 들어올 때 줄 상자가 베이스라인 기준으로 늘어나 헤더 높이를 넘기고
17666
- // 세로 가운데도 어긋난다. 글자 줄높이는 아래 th__content 가 다시 준다.
17667
- headerGroupInfo && "border-r border-solid border-[color:var(--cmp-table-border-color)]",
17668
- headerGroupInfo && i === visibleCols.length - 1 && "border-r-0",
17669
- "relative bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[0] text-[color:var(--sys-color-fg-primary)]",
17670
- stickyInfo(i).isSticky && "z-[6]",
17671
- col.thClass
17672
- ),
17673
- style: {
17674
- textAlign: col.align ?? "left",
17675
- boxShadow: stickyShadowValue(i) ?? void 0,
17676
- ...cellStyle(i),
17677
- ...col.thStyle
17678
- },
17679
- "aria-sort": isSortable ? sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none" : void 0,
17680
- children: [
17681
- /* @__PURE__ */ jsxRuntime.jsx(
17682
- "span",
17683
- {
17684
- className: cn(
17685
- // 원본 th__content: align에 따라 justify-content 적용 (th도 정렬 반영)
17686
- // th 가 leading-[0] 이므로 글자 줄높이는 여기서 준다 (truncate 가
17687
- // overflow-hidden 이라 줄높이가 0이면 라벨이 잘려 사라진다).
17688
- "inline-flex w-full min-w-0 items-center gap-[4px] leading-[20px]",
17689
- col.align === "center" && "justify-center",
17690
- col.align === "right" && "justify-end"
17691
- ),
17692
- children: col.renderHeader ? col.renderHeader(col, i) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
17693
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: col.label }),
17694
- col.helpText && col.helpText.length > 0 && // 라벨이 좁아져 truncate 되더라도 아이콘까지 함께 줄어들면 안 되므로
17695
- // shrink-0 래퍼로 감싼다 (STooltip 의 className 은 본문 쪽으로 간다).
17696
- // 아이콘·크기·색은 SKeyValueTable 헤더 도움말과 동일하게 맞춘다.
17697
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
17698
- STooltip2,
17699
- {
17700
- trigger: "hover",
17701
- icon: "helpOutline",
17702
- iconSize: 12,
17703
- color: "var(--sys-color-fg-success)",
17704
- placement: "top",
17705
- message: col.helpText
17706
- }
17707
- ) }),
17708
- col.editable && /* @__PURE__ */ jsxRuntime.jsx(
17709
- SIcon,
17710
- {
17711
- name: "pageEdit",
17712
- size: 12,
17713
- color: "var(--sys-color-link-accent)",
17714
- label: "\uC218\uC815 \uAC00\uB2A5",
17715
- className: "shrink-0"
17716
- }
17717
- ),
17718
- col.navigable && /* @__PURE__ */ jsxRuntime.jsx(
17719
- SIcon,
17720
- {
17721
- name: "pageMove",
17722
- size: 12,
17723
- color: "var(--sys-color-link-accent)",
17724
- label: "\uC0C8 \uD654\uBA74\uC5D0\uC11C \uC5F4\uAE30",
17725
- className: "shrink-0"
17726
- }
17727
- ),
17728
- col.required && // 라벨이 truncate 돼도 필수 표시는 남아야 하므로 shrink-0.
17729
- // 아이콘·크기·색은 SKeyValueTable 헤더 필수 표시와 동일하게 맞춘다.
17730
- /* @__PURE__ */ jsxRuntime.jsx(
17731
- SIcon,
17732
- {
17733
- name: "asterisk",
17734
- size: 12,
17735
- color: "var(--sys-color-fg-accent)",
17736
- className: "shrink-0"
17737
- }
17738
- ),
17739
- isSortable && // 미정렬은 updown 을 default(회색)로 — 정렬할 수 있다는 것 자체가
17740
- // 헤더에 보여야 한다. 정렬 중이면 action 색으로 올려 활성임을 알린다.
17741
- // 라벨이 truncate 돼도 버튼은 남도록 shrink-0.
17742
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
17743
- SGhostButton,
17744
- {
17745
- size: "xxs",
17746
- intent: sortDir ? "action" : "default",
17747
- icon: sortDir === "asc" ? "arrowUp" : sortDir === "desc" ? "arrowDown" : "updown",
17748
- ariaLabel: `${col.label} \uC815\uB82C`,
17749
- onClick: () => toggleSort(col)
17750
- }
17751
- ) })
17752
- ] })
17753
- }
17754
- ),
17755
- headerGroupInfo ? headerResizeHitArea(i, `resizer-${col.name}`) : headerResizer(i, `resizer-${col.name}`)
17756
- ]
17757
- },
17758
- col.name
17759
- );
17760
- })
17761
- ] })
17762
- ] })
17763
- ]
17764
- }
17765
- )
17766
- }
17767
- ),
17768
- showNoDataSlot ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(bodyScrollClass, "pointer-events-auto bg-white"), children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full min-h-[60px] w-full items-center justify-center", children: emptySlot }) }) : (
17769
- /* 바디: 세로 스크롤은 이 컨테이너에만 생긴다 (헤더 제외) */
17770
- /* @__PURE__ */ jsxRuntime.jsx(
18063
+ showNoDataSlot ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
18064
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-none overflow-hidden", children: headerTable }),
18065
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(bodyScrollClass, "pointer-events-auto bg-white"), children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full min-h-[60px] w-full items-center justify-center", children: emptySlot }) })
18066
+ ] }) : (
18067
+ /**
18068
+ * 헤더와 body 가 한 스크롤 컨테이너에 있다 — 헤더는 위에 붙어(sticky) 세로로는 서 있고
18069
+ * 가로로는 body 와 같은 스크롤로 움직인다. 헤더를 따로 두고 scroll 이벤트에서
18070
+ * scrollLeft 를 맞추면, 브라우저가 body 스크롤을 먼저 그리고 이벤트를 나중에 돌려
18071
+ * 헤더가 한 프레임 이상 늦게 따라온다.
18072
+ *
18073
+ * 행 좌표는 body 표 기준 그대로다 — `scrollTop` 이 y 이면 y 에 있는 행이 헤더 바로
18074
+ * 아래에 선다. 보이는 높이만 헤더 높이만큼 줄어든다(`headerHeight`).
18075
+ */
18076
+ /* @__PURE__ */ jsxRuntime.jsxs(
17771
18077
  "div",
17772
18078
  {
17773
18079
  ref: scrollRef,
@@ -17777,136 +18083,140 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17777
18083
  // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 스크롤 차단
17778
18084
  (loading2 || rowCount === 0) && "overflow-hidden"
17779
18085
  ),
17780
- children: /* @__PURE__ */ jsxRuntime.jsxs(
17781
- "table",
17782
- {
17783
- className: "w-full table-fixed border-separate border-spacing-0 text-left",
17784
- style: { fontSize: 12 },
17785
- children: [
17786
- colgroup,
17787
- /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
17788
- virtualScroll && topSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: topSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) }),
17789
- rowCount === 0 ? null : bodyRows.map((row, ri) => {
17790
- const rowIndex = rowOffset + ri;
17791
- const isSel = isViewSelected(row);
17792
- const dragSel = dragSelectOn && isSel;
17793
- const dragTop = dragSel && !isViewSelected(rows[rowIndex - 1]);
17794
- const dragBottom = dragSel && !isViewSelected(rows[rowIndex + 1]);
17795
- const rowLocked = (selectable || dragSelectable) && !canSelectRow(row);
17796
- const rowBg = "bg-white";
17797
- const rowHover = hoverable && !rowLocked;
17798
- return /* @__PURE__ */ jsxRuntime.jsxs(
17799
- "tr",
17800
- {
17801
- "data-row-index": rowIndex,
17802
- onClick: () => onRowClick?.(row),
17803
- onDoubleClick: onRowDoubleClick ? (event) => {
17804
- const target = event.target;
17805
- if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
17806
- onRowDoubleClick(row);
17807
- } : void 0,
17808
- onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
17809
- onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
17810
- onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
17811
- className: cn(
17812
- "group/table-row",
17813
- rowHover && "hover:bg-[var(--color-grey-05)]",
17814
- (onRowClick || onRowDoubleClick) && "cursor-pointer",
17815
- dragSelectOn && "select-none",
17816
- rowClassName?.(row)
17817
- ),
17818
- style: { height: virtualScroll ? effectiveRowHeight : rowH },
17819
- children: [
17820
- selectable && /* @__PURE__ */ jsxRuntime.jsx(
17821
- "td",
17822
- {
17823
- className: cn(
17824
- // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
17825
- "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
18086
+ style: { scrollPaddingTop: headerHeight },
18087
+ children: [
18088
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: headerRef, className: "sticky top-0 z-[8]", children: headerTable }),
18089
+ /* @__PURE__ */ jsxRuntime.jsxs(
18090
+ "table",
18091
+ {
18092
+ className: "w-full table-fixed border-separate border-spacing-0 text-left",
18093
+ style: { fontSize: 12 },
18094
+ children: [
18095
+ colgroup,
18096
+ /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
18097
+ virtualScroll && topSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: topSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) }),
18098
+ rowCount === 0 ? null : bodyRows.map((row, ri) => {
18099
+ const rowIndex = rowOffset + ri;
18100
+ const isSel = isViewSelected(row);
18101
+ const dragSel = dragSelectOn && isSel;
18102
+ const dragTop = dragSel && !isViewSelected(rows[rowIndex - 1]);
18103
+ const dragBottom = dragSel && !isViewSelected(rows[rowIndex + 1]);
18104
+ const rowLocked = (selectable || dragSelectable) && !canSelectRow(row);
18105
+ const rowBg = "bg-white";
18106
+ const rowHover = hoverable && !rowLocked;
18107
+ return /* @__PURE__ */ jsxRuntime.jsxs(
18108
+ "tr",
18109
+ {
18110
+ "data-row-index": rowIndex,
18111
+ onClick: () => onRowClick?.(row),
18112
+ onDoubleClick: onRowDoubleClick ? (event) => {
18113
+ const target = event.target;
18114
+ if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
18115
+ onRowDoubleClick(row);
18116
+ } : void 0,
18117
+ onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
18118
+ onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
18119
+ onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
18120
+ className: cn(
18121
+ "group/table-row",
18122
+ rowHover && "hover:bg-[var(--color-grey-05)]",
18123
+ (onRowClick || onRowDoubleClick) && "cursor-pointer",
18124
+ dragSelectOn && "select-none",
18125
+ rowClassName?.(row)
18126
+ ),
18127
+ style: { height: virtualScroll ? effectiveRowHeight : rowH },
18128
+ children: [
18129
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(
18130
+ "td",
18131
+ {
18132
+ className: cn(
18133
+ // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
18134
+ "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
18135
+ rowBg,
18136
+ rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
18137
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
18138
+ ),
18139
+ style: {
18140
+ width: SELECTABLE_COLUMN_WIDTH,
18141
+ minWidth: SELECTABLE_COLUMN_WIDTH,
18142
+ maxWidth: SELECTABLE_COLUMN_WIDTH,
18143
+ // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
18144
+ background: rowLocked ? LOCKED_BG : void 0
18145
+ },
18146
+ onClick: (e) => e.stopPropagation(),
18147
+ onDoubleClick: (e) => e.stopPropagation(),
18148
+ onClickCapture: (e) => {
18149
+ shiftPressedRef.current = e.shiftKey;
18150
+ if (e.shiftKey) window.getSelection()?.removeAllRanges();
18151
+ },
18152
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
18153
+ SCheckbox,
18154
+ {
18155
+ value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
18156
+ disabled: !canSelectRow(row),
18157
+ onValueChange: () => toggleRow(row, rowOffset + ri)
18158
+ }
18159
+ ) })
18160
+ }
18161
+ ),
18162
+ visibleCols.map((col, i) => {
18163
+ const tdClassName = cn(
18164
+ // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
18165
+ // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
18166
+ "overflow-hidden text-ellipsis whitespace-nowrap border-b border-solid border-[color:var(--cmp-table-border-color)] text-[12px] leading-[20px] text-[color:var(--sys-color-fg-primary)]",
17826
18167
  rowBg,
17827
18168
  rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
17828
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17829
- ),
17830
- style: {
17831
- width: SELECTABLE_COLUMN_WIDTH,
17832
- minWidth: SELECTABLE_COLUMN_WIDTH,
17833
- maxWidth: SELECTABLE_COLUMN_WIDTH,
17834
- // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
17835
- background: rowLocked ? LOCKED_BG : void 0
17836
- },
17837
- onClick: (e) => e.stopPropagation(),
17838
- onDoubleClick: (e) => e.stopPropagation(),
17839
- onClickCapture: (e) => {
17840
- shiftPressedRef.current = e.shiftKey;
17841
- if (e.shiftKey) window.getSelection()?.removeAllRanges();
17842
- },
17843
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
17844
- SCheckbox,
17845
- {
17846
- value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
17847
- disabled: !canSelectRow(row),
17848
- onValueChange: () => toggleRow(row, rowOffset + ri)
17849
- }
17850
- ) })
17851
- }
17852
- ),
17853
- visibleCols.map((col, i) => {
17854
- const tdClassName = cn(
17855
- // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
17856
- // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
17857
- "overflow-hidden text-ellipsis whitespace-nowrap border-b border-solid border-[color:var(--cmp-table-border-color)] text-[12px] leading-[20px] text-[color:var(--sys-color-fg-primary)]",
17858
- rowBg,
17859
- rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
17860
- typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
17861
- );
17862
- const tdPadX = cellPadXOf(col);
17863
- const dragEdges = dragSel ? [
17864
- dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
17865
- dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
17866
- i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
17867
- i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
17868
- ].filter(Boolean) : [];
17869
- const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
17870
- const tdStyle = {
17871
- paddingLeft: tdPadX,
17872
- paddingRight: tdPadX,
17873
- paddingTop: cellPadY,
17874
- paddingBottom: cellPadY,
17875
- textAlign: col.align ?? "left",
17876
- // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
17877
- background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
17878
- // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
17879
- // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
17880
- color: rowLocked ? LOCKED_CONTENT : void 0,
17881
- boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
17882
- ...cellStyle(i)
17883
- };
17884
- if (col.renderCell) {
17885
- const cell = col.renderCell({
17886
- row,
17887
- value: cellValue(col, row),
17888
- column: col,
17889
- colIndex: i,
17890
- rowIndex: ri,
17891
- isSelected: isSel,
17892
- className: tdClassName,
17893
- style: tdStyle
17894
- });
17895
- if (cell == null || cell === false) return null;
17896
- return /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: cell }, col.name);
17897
- }
17898
- return /* @__PURE__ */ jsxRuntime.jsx("td", { className: tdClassName, style: tdStyle, children: col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row) }, col.name);
17899
- })
17900
- ]
17901
- },
17902
- row[rowKey] ?? ri
17903
- );
17904
- }),
17905
- virtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
17906
- ] })
17907
- ]
17908
- }
17909
- )
18169
+ typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
18170
+ );
18171
+ const tdPadX = cellPadXOf(col);
18172
+ const dragEdges = dragSel ? [
18173
+ dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
18174
+ dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
18175
+ i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
18176
+ i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
18177
+ ].filter(Boolean) : [];
18178
+ const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
18179
+ const tdStyle = {
18180
+ paddingLeft: tdPadX,
18181
+ paddingRight: tdPadX,
18182
+ paddingTop: cellPadY,
18183
+ paddingBottom: cellPadY,
18184
+ textAlign: col.align ?? "left",
18185
+ // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
18186
+ background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
18187
+ // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
18188
+ // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
18189
+ color: rowLocked ? LOCKED_CONTENT : void 0,
18190
+ boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
18191
+ ...cellStyle(i)
18192
+ };
18193
+ if (col.renderCell) {
18194
+ const cell = col.renderCell({
18195
+ row,
18196
+ value: cellValue(col, row),
18197
+ column: col,
18198
+ colIndex: i,
18199
+ rowIndex: ri,
18200
+ isSelected: isSel,
18201
+ className: tdClassName,
18202
+ style: tdStyle
18203
+ });
18204
+ if (cell == null || cell === false) return null;
18205
+ return /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: cell }, col.name);
18206
+ }
18207
+ return /* @__PURE__ */ jsxRuntime.jsx("td", { className: tdClassName, style: tdStyle, children: col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row) }, col.name);
18208
+ })
18209
+ ]
18210
+ },
18211
+ row[rowKey] ?? ri
18212
+ );
18213
+ }),
18214
+ virtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
18215
+ ] })
18216
+ ]
18217
+ }
18218
+ )
18219
+ ]
17910
18220
  }
17911
18221
  )
17912
18222
  ),
@@ -21183,6 +21493,169 @@ function ChartAxisLabels({
21183
21493
  }
21184
21494
  );
21185
21495
  }
21496
+ function useElementSize(target) {
21497
+ const ref = react.useRef(null);
21498
+ const [size, setSize] = react.useState({ width: 0, height: 0 });
21499
+ react.useEffect(() => {
21500
+ const element = ref.current;
21501
+ if (!element) return;
21502
+ const update = () => {
21503
+ const { width, height } = element.getBoundingClientRect();
21504
+ setSize(
21505
+ (prev) => Math.abs(prev.width - width) < 0.5 && Math.abs(prev.height - height) < 0.5 ? prev : { width, height }
21506
+ );
21507
+ };
21508
+ update();
21509
+ if (typeof ResizeObserver === "undefined") return;
21510
+ const observer = new ResizeObserver(update);
21511
+ observer.observe(element);
21512
+ return () => observer.disconnect();
21513
+ }, [target]);
21514
+ return { ref, size };
21515
+ }
21516
+ function useSvgTextMetrics() {
21517
+ const ref = react.useRef(null);
21518
+ const [metrics, setMetrics] = react.useState({ top: 0, width: 0 });
21519
+ react.useEffect(() => {
21520
+ const element = ref.current;
21521
+ if (!element) return;
21522
+ let box3;
21523
+ try {
21524
+ box3 = element.getBBox();
21525
+ } catch {
21526
+ return;
21527
+ }
21528
+ setMetrics(
21529
+ (prev) => Math.abs(prev.top - box3.y) < 0.5 && Math.abs(prev.width - box3.width) < 0.5 ? prev : { top: box3.y, width: box3.width }
21530
+ );
21531
+ });
21532
+ return { ref, metrics };
21533
+ }
21534
+ var CATEGORY_LABEL_ROTATED_MAX = 120;
21535
+ var DIAGONAL = Math.SQRT1_2;
21536
+ function ChartRotatedAxisLayout({
21537
+ axis,
21538
+ axisStyle,
21539
+ plot,
21540
+ minPlotWidth,
21541
+ labels,
21542
+ xAt,
21543
+ labelsRef
21544
+ }) {
21545
+ const scrollRef = react.useRef(null);
21546
+ const { ref: axisRef, size: axisBox } = useElementSize();
21547
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: scrollRef, className: "min-w-0 overflow-x-auto", style: { gridColumn: "1 / -1" }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { width: `max(100%, ${axisBox.width + minPlotWidth}px)` }, children: [
21548
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex", children: [
21549
+ /* @__PURE__ */ jsxRuntime.jsx(
21550
+ "div",
21551
+ {
21552
+ ref: axisRef,
21553
+ className: "sticky left-0 z-1 shrink-0 bg-(--cmp-chart-bg)",
21554
+ style: { ...axisStyle, paddingRight: AXIS_LABEL_GAP },
21555
+ children: axis
21556
+ }
21557
+ ),
21558
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children: plot })
21559
+ ] }),
21560
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: labelsRef, children: /* @__PURE__ */ jsxRuntime.jsx(
21561
+ ChartRotatedCategoryLabels,
21562
+ {
21563
+ labels,
21564
+ xAt,
21565
+ gutter: axisBox.width,
21566
+ scrollRef
21567
+ }
21568
+ ) })
21569
+ ] }) });
21570
+ }
21571
+ function ChartRotatedCategoryLabels({
21572
+ labels,
21573
+ xAt,
21574
+ gutter,
21575
+ scrollRef
21576
+ }) {
21577
+ const sizerRef = react.useRef(null);
21578
+ const maskRef = react.useRef(null);
21579
+ const [box3, setBox] = react.useState({ line: 0, widest: 0 });
21580
+ react.useEffect(() => {
21581
+ const sizer = sizerRef.current;
21582
+ if (!sizer) return;
21583
+ const measure = () => {
21584
+ const { width, height: height2 } = sizer.getBoundingClientRect();
21585
+ const line = labels.length > 0 ? height2 / labels.length : 0;
21586
+ setBox(
21587
+ (prev) => Math.abs(prev.line - line) < 0.5 && Math.abs(prev.widest - width) < 0.5 ? prev : { line, widest: width }
21588
+ );
21589
+ };
21590
+ measure();
21591
+ if (typeof ResizeObserver === "undefined") return;
21592
+ const observer = new ResizeObserver(measure);
21593
+ observer.observe(sizer);
21594
+ return () => observer.disconnect();
21595
+ });
21596
+ react.useEffect(() => {
21597
+ const scroller = scrollRef.current;
21598
+ const mask = maskRef.current;
21599
+ if (!scroller || !mask) return;
21600
+ const sync2 = () => {
21601
+ mask.style.visibility = scroller.scrollLeft > 0 ? "visible" : "hidden";
21602
+ };
21603
+ sync2();
21604
+ scroller.addEventListener("scroll", sync2, { passive: true });
21605
+ return () => scroller.removeEventListener("scroll", sync2);
21606
+ }, [scrollRef]);
21607
+ const maxWidthAt = (index) => Math.max(
21608
+ 0,
21609
+ Math.min(CATEGORY_LABEL_ROTATED_MAX, (xAt(index) + gutter) / DIAGONAL - box3.line / 2)
21610
+ );
21611
+ const height = Math.ceil(box3.line / 2 + (box3.widest + box3.line / 2) * DIAGONAL);
21612
+ return (
21613
+ // overflow 는 clip 이어야 한다 — hidden 이면 이 행이 스크롤 상자로 잡혀 가림막의
21614
+ // sticky 가 스크롤을 따라가지 않는다.
21615
+ /* @__PURE__ */ jsxRuntime.jsxs(
21616
+ "div",
21617
+ {
21618
+ "aria-hidden": true,
21619
+ className: "relative overflow-clip typo-body-sm-medium text-(--cmp-chart-axis-color)",
21620
+ style: { height },
21621
+ children: [
21622
+ /* @__PURE__ */ jsxRuntime.jsx(
21623
+ "div",
21624
+ {
21625
+ ref: maskRef,
21626
+ className: "invisible sticky left-0 z-1 h-full bg-(--cmp-chart-bg)",
21627
+ style: { width: gutter }
21628
+ }
21629
+ ),
21630
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-y-0 right-0", style: { left: gutter }, children: labels.map((label, index) => /* @__PURE__ */ jsxRuntime.jsx(
21631
+ "span",
21632
+ {
21633
+ className: "absolute top-0 truncate",
21634
+ style: {
21635
+ left: xAt(index),
21636
+ maxWidth: maxWidthAt(index),
21637
+ // 글자 끝(오른쪽 가운데)을 눈금에 맞춘 뒤 그 점을 축으로 기울인다.
21638
+ transform: "translateX(-100%) rotate(-45deg)",
21639
+ transformOrigin: "right center"
21640
+ },
21641
+ children: label
21642
+ },
21643
+ `${label}-${index}`
21644
+ )) }),
21645
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: sizerRef, className: "invisible absolute top-0 left-0 flex flex-col items-start", children: labels.map((label, index) => /* @__PURE__ */ jsxRuntime.jsx(
21646
+ "span",
21647
+ {
21648
+ className: "block truncate",
21649
+ style: { maxWidth: maxWidthAt(index) },
21650
+ children: label
21651
+ },
21652
+ `${label}-${index}`
21653
+ )) })
21654
+ ]
21655
+ }
21656
+ )
21657
+ );
21658
+ }
21186
21659
  var ChartFrame = /* @__PURE__ */ react.forwardRef(function ChartFrame2({ title, unit, legend, legendPosition = "top", children, className, style, ...props }, ref) {
21187
21660
  return /* @__PURE__ */ jsxRuntime.jsxs(
21188
21661
  "div",
@@ -21374,44 +21847,6 @@ function linePointDimmedColor(palette, index, colors) {
21374
21847
  function dataValueColor(palette, index, colors) {
21375
21848
  return pick(DATA_VALUE_COLORS, DATA_VALUE_PALETTES, palette, index, colors);
21376
21849
  }
21377
- function useElementSize() {
21378
- const ref = react.useRef(null);
21379
- const [size, setSize] = react.useState({ width: 0, height: 0 });
21380
- react.useEffect(() => {
21381
- const element = ref.current;
21382
- if (!element) return;
21383
- const update = () => {
21384
- const { width, height } = element.getBoundingClientRect();
21385
- setSize(
21386
- (prev) => Math.abs(prev.width - width) < 0.5 && Math.abs(prev.height - height) < 0.5 ? prev : { width, height }
21387
- );
21388
- };
21389
- update();
21390
- if (typeof ResizeObserver === "undefined") return;
21391
- const observer = new ResizeObserver(update);
21392
- observer.observe(element);
21393
- return () => observer.disconnect();
21394
- }, []);
21395
- return { ref, size };
21396
- }
21397
- function useSvgTextMetrics() {
21398
- const ref = react.useRef(null);
21399
- const [metrics, setMetrics] = react.useState({ top: 0, width: 0 });
21400
- react.useEffect(() => {
21401
- const element = ref.current;
21402
- if (!element) return;
21403
- let box3;
21404
- try {
21405
- box3 = element.getBBox();
21406
- } catch {
21407
- return;
21408
- }
21409
- setMetrics(
21410
- (prev) => Math.abs(prev.top - box3.y) < 0.5 && Math.abs(prev.width - box3.width) < 0.5 ? prev : { top: box3.y, width: box3.width }
21411
- );
21412
- });
21413
- return { ref, metrics };
21414
- }
21415
21850
  var DOT_SIZE2 = 6;
21416
21851
  var DEFAULT_GAP_X = { after: 4, before: 4 };
21417
21852
  var ANCHOR_GAP_Y = 0;
@@ -21573,6 +22008,7 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
21573
22008
  tickCount = 8,
21574
22009
  barSize = BAR_SIZE_MAX,
21575
22010
  showValueLabel = true,
22011
+ rotateCategoryLabels = false,
21576
22012
  showLegend = true,
21577
22013
  legendPosition = "top",
21578
22014
  showTooltip = true,
@@ -21584,9 +22020,10 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
21584
22020
  style
21585
22021
  }, ref) {
21586
22022
  const vertical = direction === "vertical";
22023
+ const rotateLabels = rotateCategoryLabels && vertical;
21587
22024
  const { ref: plotRef, size } = useElementSize();
21588
- const { ref: axisTextRef, size: axisText } = useElementSize();
21589
- const { ref: axisSampleRef, size: axisSample } = useElementSize();
22025
+ const { ref: axisTextRef, size: axisText } = useElementSize(rotateLabels);
22026
+ const { ref: axisSampleRef, size: axisSample } = useElementSize(rotateLabels);
21590
22027
  const clipId = react.useId();
21591
22028
  const [hoveredCategory, setHoveredCategory] = react.useState(null);
21592
22029
  const perCategory = series.length === 1;
@@ -22045,7 +22482,29 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
22045
22482
  // 눈금은 왼쪽 한 줄뿐이다 — 오른쪽에도 적으면 서로 다른 단위처럼 읽힌다.
22046
22483
  gridTemplateColumns: "max-content 1fr"
22047
22484
  },
22048
- children: vertical ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
22485
+ children: rotateLabels ? (
22486
+ // 기울인 이름은 눈금 열 아래까지 뻗으므로 스크롤 상자가 눈금 열까지 감싼다.
22487
+ /* @__PURE__ */ jsxRuntime.jsx(
22488
+ ChartRotatedAxisLayout,
22489
+ {
22490
+ axis: /* @__PURE__ */ jsxRuntime.jsx(
22491
+ ChartAxisLabels,
22492
+ {
22493
+ scale,
22494
+ direction,
22495
+ format: formatValue,
22496
+ sampleRef: axisSampleRef
22497
+ }
22498
+ ),
22499
+ axisStyle: { height: plotHeight, ...inset },
22500
+ plot,
22501
+ minPlotWidth,
22502
+ labels: categories,
22503
+ xAt: (index) => layout.offset + (index + 0.5) * layout.band,
22504
+ labelsRef: axisTextRef
22505
+ }
22506
+ )
22507
+ ) : vertical ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
22049
22508
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: plotHeight, ...inset }, children: /* @__PURE__ */ jsxRuntime.jsx(
22050
22509
  ChartAxisLabels,
22051
22510
  {
@@ -22176,6 +22635,7 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22176
22635
  tickCount = 8,
22177
22636
  showPoint = true,
22178
22637
  showValueLabel = false,
22638
+ rotateCategoryLabels = false,
22179
22639
  showLegend = true,
22180
22640
  legendPosition = "top",
22181
22641
  showTooltip = true,
@@ -22188,8 +22648,8 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22188
22648
  }, ref) {
22189
22649
  const smooth = curve === "smooth";
22190
22650
  const { ref: plotRef, size } = useElementSize();
22191
- const { ref: axisTextRef, size: axisText } = useElementSize();
22192
- const { ref: axisSampleRef, size: axisSample } = useElementSize();
22651
+ const { ref: axisTextRef, size: axisText } = useElementSize(rotateCategoryLabels);
22652
+ const { ref: axisSampleRef, size: axisSample } = useElementSize(rotateCategoryLabels);
22193
22653
  const gradientId = react.useId();
22194
22654
  const [hoveredCategory, setHoveredCategory] = react.useState(null);
22195
22655
  const colorAt = react.useCallback(
@@ -22223,23 +22683,16 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22223
22683
  [rows, tickCount, max]
22224
22684
  );
22225
22685
  const count = categories.length;
22226
- const step = count > 1 ? size.width / (count - 1) : 0;
22227
- const xAt = react.useCallback(
22228
- (index) => count > 1 ? index * step : size.width / 2,
22229
- [count, step, size.width]
22230
- );
22686
+ const band = Math.max(0, size.width - AXIS_EDGE_PADDING * 2) / Math.max(1, count);
22687
+ const xAt = react.useCallback((index) => AXIS_EDGE_PADDING + (index + 0.5) * band, [band]);
22231
22688
  const cellOf = react.useCallback(
22232
- (index) => count > 1 ? {
22233
- left: Math.max(0, xAt(index) - step / 2),
22234
- right: Math.min(size.width, xAt(index) + step / 2)
22235
- } : { left: 0, right: size.width },
22236
- [count, step, xAt, size.width]
22237
- );
22238
- const edgeOf = react.useCallback(
22239
- (index) => count > 1 && index === 0 ? "start" : count > 1 && index === count - 1 ? "end" : null,
22240
- [count]
22689
+ (index) => ({
22690
+ left: AXIS_EDGE_PADDING + index * band,
22691
+ right: AXIS_EDGE_PADDING + (index + 1) * band
22692
+ }),
22693
+ [band]
22241
22694
  );
22242
- const minPlotWidth = categories.length * BAND_MIN;
22695
+ const minPlotWidth = count * BAND_MIN + AXIS_EDGE_PADDING * 2;
22243
22696
  const totalHeight = height ?? 320;
22244
22697
  const plotHeight = typeof totalHeight === "number" ? Math.max(0, totalHeight - axisText.height) : `calc(${totalHeight} - ${axisText.height}px)`;
22245
22698
  const plotted = react.useMemo(() => {
@@ -22253,10 +22706,10 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22253
22706
  }, [rows, size.width, size.height, xAt, scale]);
22254
22707
  const legend = showLegend ? /* @__PURE__ */ jsxRuntime.jsx(ChartLegend, { items: series.map((s, i) => ({ name: s.name, color: colorAt(i) })) }) : null;
22255
22708
  const handleMove = (event) => {
22256
- if (!showTooltip || count === 0 || size.width <= 0 || series.length === 0) return;
22709
+ if (!showTooltip || count === 0 || band <= 0 || series.length === 0) return;
22257
22710
  const rect = event.currentTarget.getBoundingClientRect();
22258
- const along = event.clientX - rect.left;
22259
- const category = count > 1 ? Math.round(along / step) : 0;
22711
+ const along = event.clientX - rect.left - AXIS_EDGE_PADDING;
22712
+ const category = Math.floor(along / band);
22260
22713
  setHoveredCategory(category >= 0 && category < categories.length ? category : null);
22261
22714
  };
22262
22715
  const tooltipItems = hoveredCategory == null ? [] : rows.flatMap((row, seriesIndex) => {
@@ -22295,10 +22748,7 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22295
22748
  categories.map((label, index) => /* @__PURE__ */ jsxRuntime.jsx(
22296
22749
  "span",
22297
22750
  {
22298
- className: cn(
22299
- "absolute top-0 whitespace-nowrap",
22300
- edgeOf(index) === "start" ? "translate-x-0" : edgeOf(index) === "end" ? "-translate-x-full" : "-translate-x-1/2"
22301
- ),
22751
+ className: "absolute top-0 -translate-x-1/2 whitespace-nowrap",
22302
22752
  style: { left: xAt(index) },
22303
22753
  children: label
22304
22754
  },
@@ -22479,7 +22929,7 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22479
22929
  y: point.y - POINT_RADIUS - VALUE_LABEL_GAP2,
22480
22930
  className: hoveredCategory === categoryIndex ? LINE_DATA_LABEL_TYPO_HOVER : LINE_DATA_LABEL_TYPO,
22481
22931
  style: { fill: LINE_DATA_LABEL_COLOR },
22482
- textAnchor: edgeOf(categoryIndex) === "start" ? "start" : edgeOf(categoryIndex) === "end" ? "end" : "middle",
22932
+ textAnchor: "middle",
22483
22933
  dominantBaseline: "auto",
22484
22934
  children: formatValue(point.own)
22485
22935
  },
@@ -22487,9 +22937,106 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22487
22937
  )
22488
22938
  )
22489
22939
  ) : [],
22490
- [showValueLabel, plotted, hoveredCategory, formatValue, edgeOf]
22940
+ [showValueLabel, plotted, hoveredCategory, formatValue]
22491
22941
  );
22492
22942
  const inset = { paddingTop: axisSample.height / 2, paddingBottom: axisSample.height / 2 };
22943
+ const axisLabels = /* @__PURE__ */ jsxRuntime.jsx(
22944
+ ChartAxisLabels,
22945
+ {
22946
+ scale,
22947
+ direction: "vertical",
22948
+ format: formatValue,
22949
+ sampleRef: axisSampleRef
22950
+ }
22951
+ );
22952
+ const plot = (
22953
+ // 바깥은 자리(높이·물러남)를, 안쪽은 그림을 맡는다. minWidth 는 바깥에 둬야
22954
+ // 스크롤 컨테이너가 넘치는 폭을 알아본다.
22955
+ /* @__PURE__ */ jsxRuntime.jsx(
22956
+ "div",
22957
+ {
22958
+ className: "relative min-w-0",
22959
+ style: { height: plotHeight, minWidth: minPlotWidth || void 0, ...inset },
22960
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
22961
+ "div",
22962
+ {
22963
+ ref: plotRef,
22964
+ className: "relative h-full w-full",
22965
+ onMouseMove: handleMove,
22966
+ onMouseLeave: () => setHoveredCategory(null),
22967
+ children: [
22968
+ size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
22969
+ "svg",
22970
+ {
22971
+ width: size.width,
22972
+ height: size.height,
22973
+ overflow: "visible",
22974
+ role: "img",
22975
+ "aria-label": `${categories.length}\uAC1C \uD56D\uBAA9, ${series.length}\uAC1C \uACC4\uC5F4 ${stacked ? "\uB204\uC801 " : ""}\uAEBE\uC740\uC120 \uADF8\uB798\uD504`,
22976
+ children: [
22977
+ /* @__PURE__ */ jsxRuntime.jsx(
22978
+ "text",
22979
+ {
22980
+ ref: valueLabelRef,
22981
+ "aria-hidden": true,
22982
+ x: 0,
22983
+ y: 0,
22984
+ className: LINE_DATA_LABEL_TYPO_HOVER,
22985
+ dominantBaseline: "auto",
22986
+ style: { visibility: "hidden" },
22987
+ children: "0"
22988
+ }
22989
+ ),
22990
+ /* @__PURE__ */ jsxRuntime.jsx(ChartGrid, { scale, direction: "vertical", width: size.width, height: size.height }),
22991
+ hoveredCategory != null && /* @__PURE__ */ jsxRuntime.jsx(
22992
+ "rect",
22993
+ {
22994
+ "aria-hidden": true,
22995
+ x: cellOf(hoveredCategory).left,
22996
+ y: 0,
22997
+ width: cellOf(hoveredCategory).right - cellOf(hoveredCategory).left,
22998
+ height: size.height,
22999
+ style: {
23000
+ fill: "var(--cmp-chart-barContainer-hover-bg)",
23001
+ fillOpacity: "var(--cmp-chart-barContainer-hover-opacity)"
23002
+ }
23003
+ }
23004
+ ),
23005
+ areaGradients,
23006
+ areaMask,
23007
+ area && /* @__PURE__ */ jsxRuntime.jsx("g", { mask: `url(#${areaMaskId})`, children: areas }),
23008
+ lines,
23009
+ pointHalos,
23010
+ pointDots,
23011
+ valueLabels
23012
+ ]
23013
+ }
23014
+ ),
23015
+ showTooltip && hoveredCategory != null && tooltipAnchor != null && tooltipItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
23016
+ ChartTooltip,
23017
+ {
23018
+ title: categories[hoveredCategory] ?? "",
23019
+ items: tooltipItems,
23020
+ anchor: tooltipAnchor,
23021
+ bounds: size,
23022
+ gapX: { after: -4, before: -4 },
23023
+ footer: tooltipFooter?.({
23024
+ categoryIndex: hoveredCategory,
23025
+ name: categories[hoveredCategory] ?? "",
23026
+ points: rows.flatMap((row, seriesIndex) => {
23027
+ const point = row[hoveredCategory];
23028
+ if (point == null) return [];
23029
+ return [{ seriesIndex, categoryIndex: hoveredCategory, value: point.own }];
23030
+ })
23031
+ })
23032
+ }
23033
+ )
23034
+ ]
23035
+ }
23036
+ )
23037
+ }
23038
+ )
23039
+ );
22493
23040
  return /* @__PURE__ */ jsxRuntime.jsx(
22494
23041
  ChartFrame,
22495
23042
  {
@@ -22500,7 +23047,7 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22500
23047
  legendPosition,
22501
23048
  className,
22502
23049
  style,
22503
- children: /* @__PURE__ */ jsxRuntime.jsxs(
23050
+ children: /* @__PURE__ */ jsxRuntime.jsx(
22504
23051
  "div",
22505
23052
  {
22506
23053
  className: "grid",
@@ -22509,119 +23056,27 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22509
23056
  // 눈금은 왼쪽 한 줄뿐이다 — 오른쪽에도 적으면 서로 다른 단위처럼 읽힌다.
22510
23057
  gridTemplateColumns: "max-content 1fr"
22511
23058
  },
22512
- children: [
22513
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: plotHeight, ...inset }, children: /* @__PURE__ */ jsxRuntime.jsx(
22514
- ChartAxisLabels,
22515
- {
22516
- scale,
22517
- direction: "vertical",
22518
- format: formatValue,
22519
- sampleRef: axisSampleRef
22520
- }
22521
- ) }),
22522
- /* @__PURE__ */ jsxRuntime.jsxs(
22523
- "div",
23059
+ children: rotateCategoryLabels ? (
23060
+ // 기울인 이름은 눈금 열 아래까지 뻗으므로 스크롤 상자가 눈금 열까지 감싼다.
23061
+ /* @__PURE__ */ jsxRuntime.jsx(
23062
+ ChartRotatedAxisLayout,
22524
23063
  {
22525
- className: "min-w-0 overflow-x-auto",
22526
- style: { marginInline: -POINT_HOVER_EXTENT, paddingInline: POINT_HOVER_EXTENT },
22527
- children: [
22528
- /* @__PURE__ */ jsxRuntime.jsx(
22529
- "div",
22530
- {
22531
- className: "relative min-w-0",
22532
- style: { height: plotHeight, minWidth: minPlotWidth || void 0, ...inset },
22533
- children: /* @__PURE__ */ jsxRuntime.jsxs(
22534
- "div",
22535
- {
22536
- ref: plotRef,
22537
- className: "relative h-full w-full",
22538
- onMouseMove: handleMove,
22539
- onMouseLeave: () => setHoveredCategory(null),
22540
- children: [
22541
- size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
22542
- "svg",
22543
- {
22544
- width: size.width,
22545
- height: size.height,
22546
- overflow: "visible",
22547
- role: "img",
22548
- "aria-label": `${categories.length}\uAC1C \uD56D\uBAA9, ${series.length}\uAC1C \uACC4\uC5F4 ${stacked ? "\uB204\uC801 " : ""}\uAEBE\uC740\uC120 \uADF8\uB798\uD504`,
22549
- children: [
22550
- /* @__PURE__ */ jsxRuntime.jsx(
22551
- "text",
22552
- {
22553
- ref: valueLabelRef,
22554
- "aria-hidden": true,
22555
- x: 0,
22556
- y: 0,
22557
- className: LINE_DATA_LABEL_TYPO_HOVER,
22558
- dominantBaseline: "auto",
22559
- style: { visibility: "hidden" },
22560
- children: "0"
22561
- }
22562
- ),
22563
- /* @__PURE__ */ jsxRuntime.jsx(
22564
- ChartGrid,
22565
- {
22566
- scale,
22567
- direction: "vertical",
22568
- width: size.width,
22569
- height: size.height
22570
- }
22571
- ),
22572
- hoveredCategory != null && /* @__PURE__ */ jsxRuntime.jsx(
22573
- "rect",
22574
- {
22575
- "aria-hidden": true,
22576
- x: cellOf(hoveredCategory).left,
22577
- y: 0,
22578
- width: cellOf(hoveredCategory).right - cellOf(hoveredCategory).left,
22579
- height: size.height,
22580
- style: {
22581
- fill: "var(--cmp-chart-barContainer-hover-bg)",
22582
- fillOpacity: "var(--cmp-chart-barContainer-hover-opacity)"
22583
- }
22584
- }
22585
- ),
22586
- areaGradients,
22587
- areaMask,
22588
- area && /* @__PURE__ */ jsxRuntime.jsx("g", { mask: `url(#${areaMaskId})`, children: areas }),
22589
- lines,
22590
- pointHalos,
22591
- pointDots,
22592
- valueLabels
22593
- ]
22594
- }
22595
- ),
22596
- showTooltip && hoveredCategory != null && tooltipAnchor != null && tooltipItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
22597
- ChartTooltip,
22598
- {
22599
- title: categories[hoveredCategory] ?? "",
22600
- items: tooltipItems,
22601
- anchor: tooltipAnchor,
22602
- bounds: size,
22603
- gapX: { after: -4, before: -4 },
22604
- footer: tooltipFooter?.({
22605
- categoryIndex: hoveredCategory,
22606
- name: categories[hoveredCategory] ?? "",
22607
- points: rows.flatMap((row, seriesIndex) => {
22608
- const point = row[hoveredCategory];
22609
- if (point == null) return [];
22610
- return [{ seriesIndex, categoryIndex: hoveredCategory, value: point.own }];
22611
- })
22612
- })
22613
- }
22614
- )
22615
- ]
22616
- }
22617
- )
22618
- }
22619
- ),
22620
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: axisTextRef, children: categoryLabels })
22621
- ]
23064
+ axis: axisLabels,
23065
+ axisStyle: { height: plotHeight, ...inset },
23066
+ plot,
23067
+ minPlotWidth,
23068
+ labels: categories,
23069
+ xAt,
23070
+ labelsRef: axisTextRef
22622
23071
  }
22623
23072
  )
22624
- ]
23073
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
23074
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: plotHeight, ...inset }, children: axisLabels }),
23075
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 overflow-x-auto", children: [
23076
+ plot,
23077
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: axisTextRef, children: categoryLabels })
23078
+ ] })
23079
+ ] })
22625
23080
  }
22626
23081
  )
22627
23082
  }