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.js CHANGED
@@ -7682,32 +7682,47 @@ var TREE_LABEL_TYPOGRAPHY = {
7682
7682
  rowHeight: "h-[32px]"
7683
7683
  }
7684
7684
  };
7685
- function collectIds(nodes) {
7686
- return nodes.flatMap((node) => [node.id, ...collectIds(node.children ?? [])]);
7687
- }
7688
- function collectSelectableIds(node) {
7689
- if (node.disabled) return [];
7690
- return [node.id, ...(node.children ?? []).flatMap(collectSelectableIds)];
7685
+ 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)";
7686
+ var TREE_ROW_BORDER_CLASS = "border-b border-(--cmp-tree-item-border-default)";
7687
+ var TREE_ROW_ACTIVE_CLASS = "bg-(--cmp-tree-item-bg-hover)";
7688
+ var TREE_CONTENT_CLASS = "box-border flex min-w-0 flex-1 items-center gap-(--cmp-tree-item-contentGap)";
7689
+ function treeLabelClass(size, {
7690
+ emphasized,
7691
+ disabled,
7692
+ selectable
7693
+ }) {
7694
+ const typography = TREE_LABEL_TYPOGRAPHY[size];
7695
+ return cn(
7696
+ "block min-w-0 flex-1 truncate py-(--cmp-tree-item-paddingY) text-(--cmp-tree-item-text-default)",
7697
+ emphasized ? typography.selected : typography.default,
7698
+ disabled && "opacity-60",
7699
+ disabled ? "cursor-not-allowed" : selectable ? "cursor-pointer" : void 0
7700
+ );
7691
7701
  }
7692
- function toSelectedSet(value) {
7693
- return new Set(value ?? []);
7702
+ function isRowControlClick(event) {
7703
+ const control = event.target.closest("[data-tree-control]");
7704
+ return control != null && event.currentTarget.contains(control);
7694
7705
  }
7695
- var TREE_ALL_NODE = { id: "__all__", label: "\uC804\uCCB4" };
7696
- function getSelectionState(node, selectedSet) {
7697
- const selectableChildren = (node.children ?? []).filter(
7698
- (child) => collectSelectableIds(child).length > 0
7699
- );
7700
- if (selectableChildren.length === 0) {
7701
- return { selected: selectedSet.has(node.id), indeterminate: false };
7702
- }
7703
- const childStates = selectableChildren.map(
7704
- (child) => getSelectionState(child, selectedSet)
7706
+ function TreeCaret({
7707
+ expanded,
7708
+ onToggle
7709
+ }) {
7710
+ return /* @__PURE__ */ jsx(
7711
+ SGhostButton,
7712
+ {
7713
+ icon: expanded ? "caretDown" : "caretRight",
7714
+ size: "xxs",
7715
+ ariaLabel: expanded ? "collapse tree item" : "expand tree item",
7716
+ ariaExpanded: expanded,
7717
+ onClick: (event) => {
7718
+ event.stopPropagation();
7719
+ onToggle(event);
7720
+ }
7721
+ }
7705
7722
  );
7706
- const selectedCount = childStates.filter((state2) => state2.selected).length;
7707
- return {
7708
- selected: selectedCount === childStates.length,
7709
- indeterminate: childStates.some((state2) => state2.indeterminate) || selectedCount > 0 && selectedCount < childStates.length
7710
- };
7723
+ }
7724
+ function TreeCaretSpacer() {
7725
+ return /* @__PURE__ */ jsx("span", { className: "size-(--cmp-ghostButton-xxs-size) shrink-0", "aria-hidden": "true" });
7711
7726
  }
7712
7727
  function TreeConnector({
7713
7728
  guideline,
@@ -7758,6 +7773,83 @@ function TreeConnector({
7758
7773
  ] })
7759
7774
  ] });
7760
7775
  }
7776
+ function TreeAllRow({
7777
+ label = "\uC804\uCCB4",
7778
+ selected,
7779
+ indeterminate = false,
7780
+ size = "sm",
7781
+ border = true,
7782
+ active = false,
7783
+ tabIndex,
7784
+ onKeyDown,
7785
+ onToggle,
7786
+ className,
7787
+ ...rest
7788
+ }) {
7789
+ return /* @__PURE__ */ jsx(
7790
+ "div",
7791
+ {
7792
+ role: "treeitem",
7793
+ "aria-level": 1,
7794
+ "aria-selected": selected,
7795
+ tabIndex,
7796
+ onKeyDown,
7797
+ onClick: (event) => {
7798
+ if (!isRowControlClick(event)) onToggle(event);
7799
+ },
7800
+ className: cn(
7801
+ TREE_ROW_CLASS,
7802
+ "cursor-pointer",
7803
+ TREE_LABEL_TYPOGRAPHY[size].rowHeight,
7804
+ border && TREE_ROW_BORDER_CLASS,
7805
+ active && TREE_ROW_ACTIVE_CLASS,
7806
+ className
7807
+ ),
7808
+ ...rest,
7809
+ children: /* @__PURE__ */ jsxs("div", { className: TREE_CONTENT_CLASS, children: [
7810
+ /* @__PURE__ */ jsx("span", { "data-tree-control": true, className: "flex shrink-0", children: /* @__PURE__ */ jsx(SCheckbox, { value: indeterminate ? null : selected, onValueChange: () => onToggle() }) }),
7811
+ /* @__PURE__ */ jsx(
7812
+ "span",
7813
+ {
7814
+ className: treeLabelClass(size, {
7815
+ emphasized: selected || indeterminate,
7816
+ disabled: false,
7817
+ selectable: false
7818
+ }),
7819
+ children: label
7820
+ }
7821
+ )
7822
+ ] })
7823
+ }
7824
+ );
7825
+ }
7826
+ function collectIds(nodes) {
7827
+ return nodes.flatMap((node) => [node.id, ...collectIds(node.children ?? [])]);
7828
+ }
7829
+ function collectSelectableIds(node) {
7830
+ if (node.disabled) return [];
7831
+ return [node.id, ...(node.children ?? []).flatMap(collectSelectableIds)];
7832
+ }
7833
+ function toSelectedSet(value) {
7834
+ return new Set(value ?? []);
7835
+ }
7836
+ var TREE_ALL_NODE = { id: "__all__", label: "\uC804\uCCB4" };
7837
+ function getSelectionState(node, selectedSet) {
7838
+ const selectableChildren = (node.children ?? []).filter(
7839
+ (child) => collectSelectableIds(child).length > 0
7840
+ );
7841
+ if (selectableChildren.length === 0) {
7842
+ return { selected: selectedSet.has(node.id), indeterminate: false };
7843
+ }
7844
+ const childStates = selectableChildren.map(
7845
+ (child) => getSelectionState(child, selectedSet)
7846
+ );
7847
+ const selectedCount = childStates.filter((state2) => state2.selected).length;
7848
+ return {
7849
+ selected: selectedCount === childStates.length,
7850
+ indeterminate: childStates.some((state2) => state2.indeterminate) || selectedCount > 0 && selectedCount < childStates.length
7851
+ };
7852
+ }
7761
7853
  function TreeBranch({ expanded, children }) {
7762
7854
  const branchRef = useInertRef(!expanded);
7763
7855
  return /* @__PURE__ */ jsx(
@@ -7773,10 +7865,6 @@ function TreeBranch({ expanded, children }) {
7773
7865
  }
7774
7866
  );
7775
7867
  }
7776
- function isRowControlClick(event) {
7777
- const control = event.target.closest("[data-tree-control]");
7778
- return control != null && event.currentTarget.contains(control);
7779
- }
7780
7868
  function STreeAllItem({
7781
7869
  selected,
7782
7870
  indeterminate,
@@ -7787,39 +7875,17 @@ function STreeAllItem({
7787
7875
  onKeyDown,
7788
7876
  onSelect
7789
7877
  }) {
7790
- const checkboxValue = indeterminate ? null : selected;
7791
- const typography = TREE_LABEL_TYPOGRAPHY[size];
7792
- const labelTypo = selected || indeterminate ? typography.selected : typography.default;
7793
7878
  return /* @__PURE__ */ jsx(
7794
- "div",
7879
+ TreeAllRow,
7795
7880
  {
7796
- role: "treeitem",
7797
- "aria-level": 1,
7798
- "aria-selected": selected,
7799
7881
  "data-tree-row": rowId,
7800
7882
  tabIndex,
7801
7883
  onKeyDown,
7802
- onClick: (event) => {
7803
- if (!isRowControlClick(event)) onSelect(event);
7804
- },
7805
- className: cn(
7806
- "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)",
7807
- typography.rowHeight,
7808
- border && "border-b border-(--cmp-tree-item-border-default)"
7809
- ),
7810
- children: /* @__PURE__ */ jsxs("div", { className: "box-border flex min-w-0 flex-1 items-center gap-(--cmp-tree-item-contentGap)", children: [
7811
- /* @__PURE__ */ jsx("span", { "data-tree-control": true, className: "flex shrink-0", children: /* @__PURE__ */ jsx(SCheckbox, { value: checkboxValue, onValueChange: () => onSelect() }) }),
7812
- /* @__PURE__ */ jsx(
7813
- "span",
7814
- {
7815
- className: cn(
7816
- "block min-w-0 flex-1 truncate py-(--cmp-tree-item-paddingY) text-(--cmp-tree-item-text-default)",
7817
- labelTypo
7818
- ),
7819
- children: "\uC804\uCCB4"
7820
- }
7821
- )
7822
- ] })
7884
+ selected,
7885
+ indeterminate,
7886
+ border,
7887
+ size,
7888
+ onToggle: onSelect
7823
7889
  }
7824
7890
  );
7825
7891
  }
@@ -7860,8 +7926,6 @@ var STreeItemImpl = /* @__PURE__ */ forwardRef(function STreeItem({
7860
7926
  const leadingNode = typeof leading === "function" ? leading(renderState) : leading;
7861
7927
  const trailingNode = typeof trailing === "function" ? trailing(renderState) : trailing;
7862
7928
  const checkboxValue = indeterminate ? null : selected;
7863
- const typography = TREE_LABEL_TYPOGRAPHY[size];
7864
- const labelTypo = selected || indeterminate ? typography.selected : typography.default;
7865
7929
  const handleSelect = (event) => {
7866
7930
  if (node.disabled) return;
7867
7931
  onTreeSelect?.(node, event);
@@ -7887,10 +7951,9 @@ var STreeItemImpl = /* @__PURE__ */ forwardRef(function STreeItem({
7887
7951
  onKeyDown,
7888
7952
  onClick: handleRowClick,
7889
7953
  className: cn(
7890
- "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",
7891
- typography.rowHeight,
7892
- border && "border-b border-(--cmp-tree-item-border-default)",
7893
- "hover:bg-(--cmp-tree-item-bg-hover)",
7954
+ TREE_ROW_CLASS,
7955
+ TREE_LABEL_TYPOGRAPHY[size].rowHeight,
7956
+ border && TREE_ROW_BORDER_CLASS,
7894
7957
  hasChildren && "cursor-pointer",
7895
7958
  className
7896
7959
  ),
@@ -7907,25 +7970,8 @@ var STreeItemImpl = /* @__PURE__ */ forwardRef(function STreeItem({
7907
7970
  ancestorLast
7908
7971
  }
7909
7972
  ),
7910
- /* @__PURE__ */ jsxs("div", { className: "box-border flex min-w-0 flex-1 items-center gap-(--cmp-tree-item-contentGap)", children: [
7911
- hasChildren ? /* @__PURE__ */ jsx(
7912
- SGhostButton,
7913
- {
7914
- icon: expanded ? "caretDown" : "caretRight",
7915
- size: "xxs",
7916
- ariaLabel: expanded ? "collapse tree item" : "expand tree item",
7917
- ariaExpanded: expanded,
7918
- onClick: (event) => {
7919
- event.stopPropagation();
7920
- onToggle?.(node, event);
7921
- }
7922
- }
7923
- ) : (
7924
- // 자식이 없는 줄은 펼침 버튼 자리를 빈 칸으로 남겨 라벨의 시작점을 맞춘다.
7925
- // 크기는 자리를 대신하는 그 버튼(`SGhostButton size="xxs"`)의 토큰을 그대로 쓴다 —
7926
- // 버튼 크기가 바뀌면 빈 칸도 같이 따라가야 줄이 어긋나지 않는다.
7927
- /* @__PURE__ */ jsx("span", { className: "size-(--cmp-ghostButton-xxs-size) shrink-0", "aria-hidden": "true" })
7928
- ),
7973
+ /* @__PURE__ */ jsxs("div", { className: TREE_CONTENT_CLASS, children: [
7974
+ hasChildren ? /* @__PURE__ */ jsx(TreeCaret, { expanded, onToggle: (event) => onToggle?.(node, event) }) : /* @__PURE__ */ jsx(TreeCaretSpacer, {}),
7929
7975
  leadingNode,
7930
7976
  selectable && /* @__PURE__ */ jsx(
7931
7977
  "span",
@@ -7947,12 +7993,11 @@ var STreeItemImpl = /* @__PURE__ */ forwardRef(function STreeItem({
7947
7993
  "span",
7948
7994
  {
7949
7995
  "data-tree-label": true,
7950
- className: cn(
7951
- "block min-w-0 flex-1 truncate py-(--cmp-tree-item-paddingY) text-(--cmp-tree-item-text-default)",
7952
- labelTypo,
7953
- node.disabled && "opacity-60",
7954
- node.disabled ? "cursor-not-allowed" : selectable ? "cursor-pointer" : void 0
7955
- ),
7996
+ className: treeLabelClass(size, {
7997
+ emphasized: selected || indeterminate,
7998
+ disabled: Boolean(node.disabled),
7999
+ selectable
8000
+ }),
7956
8001
  children: node.label
7957
8002
  }
7958
8003
  ),
@@ -15744,13 +15789,13 @@ var SSearchInput = /* @__PURE__ */ forwardRef(function SSearchInput2({
15744
15789
  });
15745
15790
 
15746
15791
  // src/components/SSelect/select.config.ts
15747
- function flattenOptions(options, depth = 1) {
15792
+ function flattenOptions(options, depth = 1, inheritDisabled = true) {
15748
15793
  const out = [];
15749
15794
  for (const o of options) {
15750
15795
  const hasChildren = Array.isArray(o.children) && o.children.length > 0;
15751
15796
  if (hasChildren) {
15752
- const childFlat = flattenOptions(o.children, depth + 1);
15753
- const allChildrenDisabled = childFlat.every((c) => c.disabled);
15797
+ const childFlat = flattenOptions(o.children, depth + 1, inheritDisabled);
15798
+ const allChildrenDisabled = inheritDisabled && childFlat.every((c) => c.disabled);
15754
15799
  out.push({
15755
15800
  value: String(o.value),
15756
15801
  label: o.label,
@@ -15784,12 +15829,71 @@ function collectGroupLeafValues(flat, groupIndex) {
15784
15829
  }
15785
15830
  return values;
15786
15831
  }
15832
+ function visibleTreeRows(rows, isExpanded) {
15833
+ const out = [];
15834
+ const sourceIndex = [];
15835
+ let hiddenBelow = null;
15836
+ rows.forEach((row, i) => {
15837
+ if (hiddenBelow != null && row.depth > hiddenBelow) return;
15838
+ hiddenBelow = null;
15839
+ out.push(row);
15840
+ sourceIndex.push(i);
15841
+ if (row.isGroup && !isExpanded(row)) hiddenBelow = row.depth;
15842
+ });
15843
+ return { rows: out, sourceIndex };
15844
+ }
15845
+ function treeLineInfo(rows) {
15846
+ const isLast = new Array(rows.length);
15847
+ const seen = [];
15848
+ for (let i = rows.length - 1; i >= 0; i--) {
15849
+ const depth = rows[i].depth;
15850
+ isLast[i] = !seen[depth];
15851
+ seen[depth] = true;
15852
+ seen.length = depth + 1;
15853
+ }
15854
+ const ancestors = [];
15855
+ const parents = [];
15856
+ const parentOf = new Array(rows.length);
15857
+ const posInSet = new Array(rows.length);
15858
+ const siblingCount = /* @__PURE__ */ new Map();
15859
+ rows.forEach((row, i) => {
15860
+ const parent = row.depth > 1 ? parents[row.depth - 2] ?? -1 : -1;
15861
+ parentOf[i] = parent;
15862
+ const count = (siblingCount.get(parent) ?? 0) + 1;
15863
+ siblingCount.set(parent, count);
15864
+ posInSet[i] = count;
15865
+ parents.length = row.depth - 1;
15866
+ parents[row.depth - 1] = i;
15867
+ });
15868
+ return rows.map((row, i) => {
15869
+ ancestors.length = row.depth - 1;
15870
+ const ancestorLast = ancestors.slice();
15871
+ ancestors[row.depth - 1] = isLast[i];
15872
+ return {
15873
+ isLast: isLast[i],
15874
+ ancestorLast,
15875
+ posInSet: posInSet[i],
15876
+ setSize: siblingCount.get(parentOf[i]) ?? 1
15877
+ };
15878
+ });
15879
+ }
15880
+ function ancestorGroupValues(flat, selected) {
15881
+ const out = /* @__PURE__ */ new Set();
15882
+ const stack = [];
15883
+ for (const row of flat) {
15884
+ while (stack.length > 0 && stack[stack.length - 1].depth >= row.depth) stack.pop();
15885
+ if (selected.has(row.value)) for (const group of stack) out.add(group.value);
15886
+ if (row.isGroup) stack.push(row);
15887
+ }
15888
+ return out;
15889
+ }
15787
15890
  var isHtmlLabel = (label) => /<[a-z][\s\S]*>/i.test(label);
15788
15891
  var extractText = (html) => html.replace(/<[^>]*>/g, "");
15789
15892
  var OPTION_ROW_HEIGHT = 28;
15790
15893
  var DEFAULT_VIRTUAL_BUFFER = 10;
15791
15894
  var DEFAULT_REACH_END_THRESHOLD = 10;
15792
15895
  var DEFAULT_SEARCH_DEBOUNCE = 300;
15896
+ var NO_SEARCH_COLLAPSED = /* @__PURE__ */ new Set();
15793
15897
  var SEARCH_THRESHOLD = 11;
15794
15898
  function isListSized(el) {
15795
15899
  return el.clientHeight > 0 && getComputedStyle(el).maxHeight !== "none";
@@ -15844,8 +15948,9 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15844
15948
  className,
15845
15949
  style
15846
15950
  }, ref) {
15847
- const multi = type === "multi" || type === "multi_depth";
15951
+ const multi = type === "multi" || type === "multi_depth" || type === "multi_tree";
15848
15952
  const isDepth = type === "default_depth" || type === "multi_depth";
15953
+ const isTree = type === "tree" || type === "multi_tree";
15849
15954
  const pagingEnabled = onReachEnd != null;
15850
15955
  const partialOptions = pagingEnabled || serverSearch;
15851
15956
  const portalContainer = usePortalContainer();
@@ -15855,6 +15960,8 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15855
15960
  const [open, setOpen] = useState(false);
15856
15961
  const [pendingOpen, setPendingOpen] = useState(false);
15857
15962
  const [focusedIndex, setFocusedIndex] = useState(-1);
15963
+ const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
15964
+ const [searchCollapsed, setSearchCollapsed] = useState(NO_SEARCH_COLLAPSED);
15858
15965
  const baseId = useId();
15859
15966
  const listboxId = `${baseId}-listbox`;
15860
15967
  const activeOptionId = `${baseId}-active-option`;
@@ -15878,10 +15985,11 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15878
15985
  const alignSelectedTopRef = useRef(false);
15879
15986
  const selectedAnchorRef = useRef(-1);
15880
15987
  const rangeAnchorRef = useRef(null);
15881
- const flat = useMemo(() => flattenOptions(options), [options]);
15988
+ const groupIsValue = type === "tree";
15989
+ const flat = useMemo(() => flattenOptions(options, 1, !groupIsValue), [groupIsValue, options]);
15882
15990
  const selectable = useMemo(() => selectableOptions(flat), [flat]);
15883
15991
  const hasLeaf = useMemo(() => flat.some((o) => !o.isGroup), [flat]);
15884
- const allOptionsDisabled = hasLeaf && selectable.length === 0;
15992
+ const allOptionsDisabled = groupIsValue ? flat.length > 0 && flat.every((o) => o.disabled) : hasLeaf && selectable.length === 0;
15885
15993
  const effectiveDisabled = disabled || allOptionsDisabled;
15886
15994
  const searchVisible = showSearch && (partialOptions || selectable.length >= SEARCH_THRESHOLD);
15887
15995
  const labelByValue = useMemo(() => new Map(flat.map((o) => [o.value, o.label])), [flat]);
@@ -15924,6 +16032,10 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15924
16032
  if (next) {
15925
16033
  dismissedByPointerRef.current = false;
15926
16034
  alignSelectedTopRef.current = true;
16035
+ if (isTree) {
16036
+ setExpanded(ancestorGroupValues(flat, new Set(selectedRef.current)));
16037
+ setSearchCollapsed(NO_SEARCH_COLLAPSED);
16038
+ }
15927
16039
  } else {
15928
16040
  setPendingOpen(false);
15929
16041
  setSearch("");
@@ -15966,7 +16078,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15966
16078
  },
15967
16079
  resetValidation: () => setRuleError("")
15968
16080
  });
15969
- const filtered = useMemo(() => {
16081
+ const searched = useMemo(() => {
15970
16082
  if (serverSearch || !searchVisible || search.trim() === "") return flat;
15971
16083
  const q = search.trim().toLowerCase();
15972
16084
  return filterDepthRows(
@@ -15974,23 +16086,53 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15974
16086
  (o) => (isHtmlLabel(o.label) ? extractText(o.label) : o.label).toLowerCase().includes(q)
15975
16087
  );
15976
16088
  }, [flat, search, searchVisible, serverSearch]);
16089
+ const treeSearching = isTree && search.trim() !== "";
16090
+ const isGroupExpanded = useCallback(
16091
+ (groupValue) => treeSearching ? !searchCollapsed.has(groupValue) : expanded.has(groupValue),
16092
+ [expanded, searchCollapsed, treeSearching]
16093
+ );
16094
+ const treeRows = useMemo(
16095
+ () => isTree ? visibleTreeRows(searched, (row) => isGroupExpanded(row.value)) : null,
16096
+ [isGroupExpanded, isTree, searched]
16097
+ );
16098
+ const filtered = treeRows?.rows ?? searched;
16099
+ const treeLines = useMemo(() => treeRows ? treeLineInfo(treeRows.rows) : [], [treeRows]);
15977
16100
  const allValues = selectable.map((o) => o.value);
15978
16101
  const allSelected = !partialOptions && allValues.length > 0 && allValues.every((v) => selectedSet.has(v));
15979
16102
  const selectAllVisible = multi && showSelectAll && !partialOptions && allValues.length > 0;
15980
- const hasItems = filtered.some((o) => !o.isGroup);
16103
+ const hasItems = isTree ? filtered.length > 0 : filtered.some((o) => !o.isGroup);
15981
16104
  const groupLeafValues = useMemo(() => {
15982
16105
  const map = /* @__PURE__ */ new Map();
15983
16106
  filtered.forEach((o, i) => {
15984
- if (o.isGroup) map.set(i, collectGroupLeafValues(filtered, i));
16107
+ if (!o.isGroup) return;
16108
+ map.set(
16109
+ i,
16110
+ treeRows ? collectGroupLeafValues(searched, treeRows.sourceIndex[i]) : collectGroupLeafValues(filtered, i)
16111
+ );
15985
16112
  });
15986
16113
  return map;
15987
- }, [filtered]);
16114
+ }, [filtered, searched, treeRows]);
15988
16115
  const navigableItems = useMemo(() => {
15989
16116
  const items = [];
15990
16117
  if (selectAllVisible) {
15991
16118
  items.push({ key: "__select_all__", isSelectAll: true });
15992
16119
  }
15993
16120
  filtered.forEach((o, i) => {
16121
+ if (o.isGroup && isTree) {
16122
+ if (multi) {
16123
+ const leafValues = groupLeafValues.get(i) ?? [];
16124
+ items.push({
16125
+ key: `__group_${i}__`,
16126
+ isGroup: true,
16127
+ index: i,
16128
+ leafValues,
16129
+ disabled: o.disabled || leafValues.length === 0
16130
+ });
16131
+ } else {
16132
+ items.push({ key: o.value, value: o.value, index: i, disabled: o.disabled });
16133
+ }
16134
+ return;
16135
+ }
15994
16136
  if (o.disabled) return;
15995
16137
  if (o.isGroup) {
15996
16138
  if (!multi) return;
@@ -16002,7 +16144,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16002
16144
  }
16003
16145
  });
16004
16146
  return items;
16005
- }, [filtered, groupLeafValues, multi, selectAllVisible]);
16147
+ }, [filtered, groupLeafValues, isTree, multi, selectAllVisible]);
16006
16148
  const selectedAnchorIndex = useMemo(() => {
16007
16149
  if (selectedSet.size === 0) return -1;
16008
16150
  return navigableItems.findIndex((item4) => item4.value != null && selectedSet.has(item4.value));
@@ -16013,7 +16155,10 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16013
16155
  const rowCount = filtered.length;
16014
16156
  const metrics = metricsRef.current;
16015
16157
  const lastSearchRef = useRef(search);
16016
- if (lastSearchRef.current !== search) {
16158
+ const lastExpansionRef = useRef({ expanded, searchCollapsed });
16159
+ const expansionChanged = lastExpansionRef.current.expanded !== expanded || lastExpansionRef.current.searchCollapsed !== searchCollapsed;
16160
+ lastExpansionRef.current = { expanded, searchCollapsed };
16161
+ if (lastSearchRef.current !== search || isTree && expansionChanged) {
16017
16162
  lastSearchRef.current = search;
16018
16163
  resetRowMetrics(metrics, rowCount);
16019
16164
  measuredKeyRef.current = "";
@@ -16153,7 +16298,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16153
16298
  );
16154
16299
  const fullGroupByLeaf = useMemo(() => {
16155
16300
  const map = /* @__PURE__ */ new Map();
16156
- if (!showChips || type !== "multi_depth") return map;
16301
+ if (!showChips || type !== "multi_depth" && type !== "multi_tree") return map;
16157
16302
  flat.forEach((o, i) => {
16158
16303
  if (!o.isGroup) return;
16159
16304
  const leafValues = collectGroupLeafValues(flat, i);
@@ -16231,6 +16376,21 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16231
16376
  rangeAnchorRef.current = null;
16232
16377
  emit3(allSelected ? [] : allValues);
16233
16378
  };
16379
+ const handleSearchChange = (next) => {
16380
+ setSearch(next);
16381
+ setSearchCollapsed(NO_SEARCH_COLLAPSED);
16382
+ };
16383
+ const handleToggleExpand = (groupValue) => {
16384
+ rangeAnchorRef.current = null;
16385
+ const toggle = (prev) => {
16386
+ const next = new Set(prev);
16387
+ if (next.has(groupValue)) next.delete(groupValue);
16388
+ else next.add(groupValue);
16389
+ return next;
16390
+ };
16391
+ if (treeSearching) setSearchCollapsed(toggle);
16392
+ else setExpanded(toggle);
16393
+ };
16234
16394
  const handleToggleGroup = (leafValues) => {
16235
16395
  if (leafValues.length === 0) return;
16236
16396
  rangeAnchorRef.current = null;
@@ -16303,6 +16463,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16303
16463
  const selectFocusedItem = (extend = false) => {
16304
16464
  if (focusedIndex < 0 || focusedIndex >= navigableItems.length) return;
16305
16465
  const focused = navigableItems[focusedIndex];
16466
+ if (focused.disabled) return;
16306
16467
  if (focused.isSelectAll) {
16307
16468
  handleSelectAll();
16308
16469
  return;
@@ -16331,6 +16492,41 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16331
16492
  moveFocusedIndex("prev");
16332
16493
  return;
16333
16494
  }
16495
+ const input = event.target instanceof HTMLInputElement ? event.target : null;
16496
+ const caretAtEdge = input == null || input.selectionStart === input.selectionEnd && input.selectionStart === (event.key === "ArrowRight" ? input.value.length : 0);
16497
+ if (isTree && (event.key === "ArrowRight" || event.key === "ArrowLeft") && caretAtEdge) {
16498
+ const rowIndex = navigableItems[focusedIndex]?.index;
16499
+ const row = rowIndex != null ? filtered[rowIndex] : void 0;
16500
+ if (rowIndex == null || row == null) return;
16501
+ event.preventDefault();
16502
+ const rowExpanded = row.isGroup && isGroupExpanded(row.value);
16503
+ if (event.key === "ArrowRight") {
16504
+ if (!row.isGroup) return;
16505
+ if (!rowExpanded) {
16506
+ handleToggleExpand(row.value);
16507
+ return;
16508
+ }
16509
+ for (let i = rowIndex + 1; i < filtered.length && filtered[i].depth > row.depth; i++) {
16510
+ const child = navigableItems.findIndex((item4) => item4.index === i);
16511
+ if (child >= 0) {
16512
+ setFocusedIndex(child);
16513
+ break;
16514
+ }
16515
+ }
16516
+ return;
16517
+ }
16518
+ if (rowExpanded) {
16519
+ handleToggleExpand(row.value);
16520
+ return;
16521
+ }
16522
+ for (let i = rowIndex - 1; i >= 0; i--) {
16523
+ if (filtered[i].depth >= row.depth) continue;
16524
+ const parent = navigableItems.findIndex((item4) => item4.index === i);
16525
+ if (parent >= 0) setFocusedIndex(parent);
16526
+ return;
16527
+ }
16528
+ return;
16529
+ }
16334
16530
  if (event.key === "Enter") {
16335
16531
  event.preventDefault();
16336
16532
  selectFocusedItem(event.shiftKey);
@@ -16375,7 +16571,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16375
16571
  type: "button",
16376
16572
  disabled: effectiveDisabled,
16377
16573
  "aria-labelledby": label ? `${fieldLabelId(triggerId)} ${triggerId}` : void 0,
16378
- "aria-haspopup": "listbox",
16574
+ "aria-haspopup": isTree ? "tree" : "listbox",
16379
16575
  "aria-expanded": open,
16380
16576
  "aria-controls": open ? listboxId : void 0,
16381
16577
  "aria-activedescendant": open && focusedIndex >= 0 ? activeOptionId : void 0,
@@ -16512,7 +16708,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16512
16708
  {
16513
16709
  ref: searchInputRef,
16514
16710
  value: search,
16515
- onValueChange: setSearch,
16711
+ onValueChange: handleSearchChange,
16516
16712
  onKeyDown: handleKeyDown,
16517
16713
  placeholder: "\uAC80\uC0C9",
16518
16714
  clearable: true,
@@ -16531,8 +16727,12 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16531
16727
  indexOffset: windowStart,
16532
16728
  topSpacer: win.top,
16533
16729
  bottomSpacer: win.bottom,
16534
- setSize: isDepth ? void 0 : rowCount,
16730
+ setSize: isDepth || isTree ? void 0 : rowCount,
16535
16731
  multi,
16732
+ tree: isTree,
16733
+ isExpanded: isGroupExpanded,
16734
+ onToggleExpand: handleToggleExpand,
16735
+ treeLines,
16536
16736
  selected,
16537
16737
  groupLeafValues,
16538
16738
  focusedValue: navigableItems[focusedIndex]?.key,
@@ -16586,6 +16786,10 @@ function ItemList({
16586
16786
  bottomSpacer,
16587
16787
  setSize,
16588
16788
  multi,
16789
+ tree,
16790
+ isExpanded,
16791
+ onToggleExpand,
16792
+ treeLines,
16589
16793
  selected,
16590
16794
  groupLeafValues,
16591
16795
  focusedValue,
@@ -16599,14 +16803,34 @@ function ItemList({
16599
16803
  onHoverValue
16600
16804
  }) {
16601
16805
  const selectedSet = useMemo(() => new Set(selected), [selected]);
16806
+ const checkboxShiftRef = useRef(false);
16602
16807
  const itemBase = cn(
16603
16808
  "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",
16604
16809
  "text-[color:var(--cmp-listBoxItem-content-default)]",
16605
16810
  "disabled:cursor-not-allowed disabled:text-[color:var(--cmp-listBoxItem-content-disabled)]"
16606
16811
  );
16607
16812
  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)]";
16608
- return /* @__PURE__ */ jsxs("div", { id: listboxId, role: "listbox", "aria-multiselectable": multi || void 0, children: [
16609
- showSelectAll && /* @__PURE__ */ jsxs(
16813
+ return /* @__PURE__ */ jsxs("div", { id: listboxId, role: tree ? "tree" : "listbox", "aria-multiselectable": multi || void 0, children: [
16814
+ showSelectAll && (tree ? (
16815
+ // 트리의 전체선택 줄은 STree 의 "전체" 줄 그대로다 (lib/tree-row).
16816
+ /* @__PURE__ */ jsx(
16817
+ TreeAllRow,
16818
+ {
16819
+ "data-select-all-row": true,
16820
+ id: focusedValue === "__select_all__" ? activeOptionId : void 0,
16821
+ "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
16822
+ "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
16823
+ tabIndex: -1,
16824
+ label: selectAllLabel,
16825
+ selected: selectAllSelected,
16826
+ active: focusedValue === "__select_all__",
16827
+ className: "select-none",
16828
+ onMouseEnter: () => onHoverValue("__select_all__"),
16829
+ onMouseLeave: () => onHoverValue(null),
16830
+ onToggle: onSelectAll
16831
+ }
16832
+ )
16833
+ ) : /* @__PURE__ */ jsxs(
16610
16834
  "button",
16611
16835
  {
16612
16836
  type: "button",
@@ -16627,12 +16851,109 @@ function ItemList({
16627
16851
  /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: selectAllLabel })
16628
16852
  ]
16629
16853
  }
16630
- ),
16854
+ )),
16631
16855
  topSpacer > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: { height: topSpacer } }),
16632
16856
  items.map((o, windowIndex) => {
16633
16857
  const idx = windowIndex + indexOffset;
16634
16858
  const padLeft = o.depth === 1 ? 12 : o.depth === 2 ? 20 : 28;
16635
16859
  const rowKey = `${o.value}-${idx}`;
16860
+ if (tree) {
16861
+ const togglesLeaves = multi && o.isGroup;
16862
+ const leafValues = togglesLeaves ? groupLeafValues.get(idx) ?? [] : [];
16863
+ const total = leafValues.length;
16864
+ const selectedCount = leafValues.reduce((n, v) => selectedSet.has(v) ? n + 1 : n, 0);
16865
+ const isSelected2 = selectedSet.has(o.value);
16866
+ const checkState = togglesLeaves ? total === 0 || selectedCount === 0 ? false : selectedCount === total ? true : null : isSelected2;
16867
+ const rowDisabled = o.disabled || togglesLeaves && total === 0;
16868
+ const focusKey = togglesLeaves ? `__group_${idx}__` : o.value;
16869
+ const focused = focusedValue === focusKey;
16870
+ const expanded = o.isGroup && isExpanded(o.value);
16871
+ const line = treeLines[idx];
16872
+ const toggle = () => {
16873
+ if (rowDisabled) return;
16874
+ if (togglesLeaves) onToggleGroup(leafValues);
16875
+ else onSelect(o.value, idx, checkboxShiftRef.current);
16876
+ };
16877
+ return (
16878
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events -- 키보드는 트리거(또는 검색 입력)가 aria-activedescendant 로 맡는다. 줄로 포커스를 옮기지 않는다
16879
+ /* @__PURE__ */ jsxs(
16880
+ "div",
16881
+ {
16882
+ tabIndex: -1,
16883
+ "data-select-row": true,
16884
+ "data-index": idx,
16885
+ id: focused ? activeOptionId : void 0,
16886
+ role: "treeitem",
16887
+ "aria-level": o.depth,
16888
+ "aria-posinset": line?.posInSet,
16889
+ "aria-setsize": line?.setSize,
16890
+ "aria-expanded": o.isGroup ? expanded : void 0,
16891
+ "aria-selected": checkState === true,
16892
+ "aria-disabled": rowDisabled || void 0,
16893
+ "data-select-focused": focused ? "true" : void 0,
16894
+ "data-select-active": focused ? "true" : void 0,
16895
+ className: cn(
16896
+ TREE_ROW_CLASS,
16897
+ TREE_LABEL_TYPOGRAPHY.sm.rowHeight,
16898
+ TREE_ROW_BORDER_CLASS,
16899
+ "select-none",
16900
+ rowDisabled ? "cursor-not-allowed" : "cursor-pointer",
16901
+ focused && TREE_ROW_ACTIVE_CLASS
16902
+ ),
16903
+ onMouseEnter: () => onHoverValue(focusKey),
16904
+ onMouseLeave: () => onHoverValue(null),
16905
+ onClick: (event) => {
16906
+ if (isRowControlClick(event) || rowDisabled) return;
16907
+ if (togglesLeaves) onToggleGroup(leafValues);
16908
+ else onSelect(o.value, idx, event.shiftKey);
16909
+ },
16910
+ children: [
16911
+ /* @__PURE__ */ jsx(
16912
+ TreeConnector,
16913
+ {
16914
+ guideline: true,
16915
+ size: "sm",
16916
+ depth: o.depth,
16917
+ hasChildren: o.isGroup,
16918
+ isLast: line?.isLast ?? true,
16919
+ ancestorLast: line?.ancestorLast ?? []
16920
+ }
16921
+ ),
16922
+ /* @__PURE__ */ jsxs("div", { className: TREE_CONTENT_CLASS, children: [
16923
+ o.isGroup ? (
16924
+ // 비활성 그룹도 접고 편다 — 그룹이 비활성이어도 아래에 고를 수 있는 자식이 있을 수 있다.
16925
+ /* @__PURE__ */ jsx(TreeCaret, { expanded, onToggle: () => onToggleExpand(o.value) })
16926
+ ) : /* @__PURE__ */ jsx(TreeCaretSpacer, {}),
16927
+ multi && /* @__PURE__ */ jsx(
16928
+ "span",
16929
+ {
16930
+ "data-tree-control": true,
16931
+ className: "flex shrink-0",
16932
+ onPointerDownCapture: (event) => {
16933
+ checkboxShiftRef.current = event.shiftKey;
16934
+ },
16935
+ children: /* @__PURE__ */ jsx(SCheckbox, { value: checkState, disabled: rowDisabled, onValueChange: toggle })
16936
+ }
16937
+ ),
16938
+ /* @__PURE__ */ jsx(
16939
+ OptionLabel,
16940
+ {
16941
+ label: o.label,
16942
+ className: treeLabelClass("sm", {
16943
+ // 고른 줄은 굵게 — multi_tree 는 일부만 고른 그룹도 굵다 (STree 와 같다)
16944
+ emphasized: checkState !== false,
16945
+ disabled: rowDisabled,
16946
+ selectable: true
16947
+ })
16948
+ }
16949
+ )
16950
+ ] })
16951
+ ]
16952
+ },
16953
+ rowKey
16954
+ )
16955
+ );
16956
+ }
16636
16957
  if (o.isGroup) {
16637
16958
  const isDepth1 = o.depth === 1;
16638
16959
  const isFirstRow = idx === 0 && !showSelectAll && !searchVisible;
@@ -16673,13 +16994,7 @@ function ItemList({
16673
16994
  onClick: () => onToggleGroup(leafValues),
16674
16995
  children: [
16675
16996
  /* @__PURE__ */ jsx(CheckboxBox, { state: groupState, disabled: groupDisabled, focused: groupFocused }),
16676
- isHtmlLabel(o.label) ? /* @__PURE__ */ jsx(
16677
- "span",
16678
- {
16679
- className: "min-w-0 truncate",
16680
- dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(o.label) }
16681
- }
16682
- ) : /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: o.label }),
16997
+ /* @__PURE__ */ jsx(OptionLabel, { label: o.label, className: "min-w-0 truncate" }),
16683
16998
  total > 0 && /* @__PURE__ */ jsxs(
16684
16999
  "span",
16685
17000
  {
@@ -16756,13 +17071,7 @@ function ItemList({
16756
17071
  focused: focusedValue === o.value
16757
17072
  }
16758
17073
  ),
16759
- isHtmlLabel(o.label) ? /* @__PURE__ */ jsx(
16760
- "span",
16761
- {
16762
- className: "flex-1 truncate",
16763
- dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(o.label) }
16764
- }
16765
- ) : /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: o.label })
17074
+ /* @__PURE__ */ jsx(OptionLabel, { label: o.label, className: "flex-1 truncate" })
16766
17075
  ]
16767
17076
  },
16768
17077
  rowKey
@@ -16771,8 +17080,11 @@ function ItemList({
16771
17080
  bottomSpacer > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: { height: bottomSpacer } })
16772
17081
  ] });
16773
17082
  }
17083
+ function OptionLabel({ label, className }) {
17084
+ return isHtmlLabel(label) ? /* @__PURE__ */ jsx("span", { className, dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsx("span", { className, children: label });
17085
+ }
16774
17086
  function TriggerLabel({ label }) {
16775
- return isHtmlLabel(label) ? /* @__PURE__ */ jsx("span", { className: "truncate", dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsx("span", { className: "truncate", children: label });
17087
+ return /* @__PURE__ */ jsx(OptionLabel, { label, className: "truncate" });
16776
17088
  }
16777
17089
  function TriggerRemoveButton({ ariaLabel, onRemove }) {
16778
17090
  return /* @__PURE__ */ jsxs(
@@ -17027,8 +17339,8 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17027
17339
  const [rowCountOverride, setRowCountOverride] = useState(null);
17028
17340
  const rowCount = rowCountOverride ?? rows.length;
17029
17341
  const scrollRef = useRef(null);
17030
- const headerScrollRef = useRef(null);
17031
- const [headerGutter, setHeaderGutter] = useState(0);
17342
+ const headerRef = useRef(null);
17343
+ const [headerHeight, setHeaderHeight] = useState(0);
17032
17344
  const [vsRange, setVsRange] = useState({ start: 0, end: 0 });
17033
17345
  const lastReachEndRef = useRef(-1);
17034
17346
  const cbRef = useRef({ onVirtualUpdate, onVirtualReachEnd, onScroll });
@@ -17070,17 +17382,13 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17070
17382
  );
17071
17383
  const handleScroll = useCallback(() => {
17072
17384
  const el = scrollRef.current;
17073
- if (!el) {
17074
- setHeaderGutter(0);
17075
- return;
17076
- }
17077
- const header = headerScrollRef.current;
17078
- if (header) header.scrollLeft = el.scrollLeft;
17079
- setHeaderGutter(el.offsetWidth - el.clientWidth);
17385
+ if (!el) return;
17386
+ const headerH = headerRef.current?.offsetHeight ?? 0;
17387
+ setHeaderHeight(headerH);
17080
17388
  setScrolledLeft(el.scrollLeft > 0);
17081
17389
  setScrolledRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
17082
17390
  if (virtualScroll) {
17083
- const { start, end } = calcRange(el.scrollTop, el.clientHeight);
17391
+ const { start, end } = calcRange(el.scrollTop, el.clientHeight - headerH);
17084
17392
  setVsRange((prev) => prev.start === start && prev.end === end ? prev : { start, end });
17085
17393
  }
17086
17394
  }, [virtualScroll, calcRange]);
@@ -17477,7 +17785,7 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17477
17785
  );
17478
17786
  };
17479
17787
  const bodyScrollClass = cn(
17480
- "min-h-0 flex-1 overflow-auto border-t border-solid border-[color:var(--cmp-table-border-color)]",
17788
+ "min-h-0 flex-1 overflow-auto",
17481
17789
  SCROLLBAR_CLASS,
17482
17790
  SCROLLBAR_TRACK_BG_CLASS,
17483
17791
  SCROLLBAR_TRACK_BORDER_CLASS
@@ -17494,28 +17802,226 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17494
17802
  selectable && /* @__PURE__ */ jsx("col", { style: { width: SELECTABLE_COLUMN_WIDTH } }),
17495
17803
  visibleCols.map((col, i) => /* @__PURE__ */ jsx("col", { style: col.autoWidth ? void 0 : { width: columnWidths[i] } }, col.name))
17496
17804
  ] });
17497
- return /* @__PURE__ */ jsxs(
17498
- "div",
17805
+ const headerTable = /* @__PURE__ */ jsxs(
17806
+ "table",
17499
17807
  {
17500
- className: cn("flex flex-col", className),
17501
- style: { width, ...style },
17502
- "data-table-id": resolvedTableId,
17808
+ className: "w-full table-fixed border-separate border-spacing-0 border-b border-solid border-[color:var(--cmp-table-border-color)] text-left",
17809
+ style: { fontSize: 12 },
17503
17810
  children: [
17504
- /* @__PURE__ */ jsxs(
17505
- "div",
17506
- {
17507
- className: cn(
17508
- // isolate: no-data/loading 오버레이(z-30·31)를 테이블 안에서 끝낸다. relative + z-auto
17509
- // 만으로는 스태킹 컨텍스트가 생기지 않아, 모달 안에 들어가면 30·31이 모달 Content
17510
- // 레벨까지 올라가 같은 컨텍스트의 floating portal 밴드(z-2)를 덮는다.
17511
- // 자손은 이미 overflow-hidden으로 클리핑되므로 테이블 내부 순서는 달라지지 않는다.
17512
- "bg-white relative isolate flex flex-col overflow-hidden",
17513
- bordered && "border border-solid border-[color:var(--cmp-table-border-color)]",
17514
- // height가 없으면 루트가 받은 높이를 끝까지 채운다. 호스트가 className으로
17515
- // `flex-1 min-h-0` 등을 줘 루트 높이가 확정된 경우, 행이 적어도 테두리 박스가
17516
- // 남은 영역을 그대로 차지한다. basis는 auto로 둬 높이가 확정되지 않은
17517
- // 기본 사용(내용만큼 커지는 케이스)에서는 동작이 바뀌지 않는다.
17518
- !height && "min-h-0 grow",
17811
+ colgroup,
17812
+ /* @__PURE__ */ jsxs("thead", { className: cn(isNoData && "opacity-40"), children: [
17813
+ headerGroupInfo && /* @__PURE__ */ jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17814
+ selectable && /* @__PURE__ */ jsx(
17815
+ "th",
17816
+ {
17817
+ rowSpan: 2,
17818
+ className: cn(
17819
+ "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17820
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17821
+ ),
17822
+ style: {
17823
+ width: SELECTABLE_COLUMN_WIDTH,
17824
+ minWidth: SELECTABLE_COLUMN_WIDTH,
17825
+ maxWidth: SELECTABLE_COLUMN_WIDTH
17826
+ },
17827
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(
17828
+ SCheckbox,
17829
+ {
17830
+ value: headerCheck,
17831
+ disabled: !hasSelectableRow,
17832
+ onValueChange: toggleAll
17833
+ }
17834
+ ) })
17835
+ }
17836
+ ),
17837
+ headerGroupInfo.cells.map((group) => /* @__PURE__ */ jsxs(
17838
+ "th",
17839
+ {
17840
+ colSpan: group.colSpan,
17841
+ rowSpan: group.rowSpan,
17842
+ className: cn(
17843
+ "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)]",
17844
+ group.firstIndex + group.colSpan === visibleCols.length && "border-r-0",
17845
+ group.rowSpan === 2 && "border-b-0",
17846
+ group.className
17847
+ ),
17848
+ style: { textAlign: group.align ?? "left", ...group.style },
17849
+ children: [
17850
+ group.render ? group.render() : group.label,
17851
+ group.rowSpan === 2 && group.colSpan === 1 ? headerResizeHitArea(group.firstIndex, `group-resizer-${group.columnNames[0]}`) : null
17852
+ ]
17853
+ },
17854
+ group.columnNames.join("|")
17855
+ ))
17856
+ ] }),
17857
+ /* @__PURE__ */ jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17858
+ selectable && !headerGroupInfo && /* @__PURE__ */ jsxs(
17859
+ "th",
17860
+ {
17861
+ className: cn(
17862
+ // 원본 .th--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
17863
+ // 하단 border는 헤더 표가 연속으로 그리므로 셀에는 두지 않는다.
17864
+ "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17865
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17866
+ ),
17867
+ style: {
17868
+ width: SELECTABLE_COLUMN_WIDTH,
17869
+ minWidth: SELECTABLE_COLUMN_WIDTH,
17870
+ maxWidth: SELECTABLE_COLUMN_WIDTH
17871
+ },
17872
+ children: [
17873
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(
17874
+ SCheckbox,
17875
+ {
17876
+ value: headerCheck,
17877
+ disabled: !hasSelectableRow,
17878
+ onValueChange: toggleAll
17879
+ }
17880
+ ) }),
17881
+ /* @__PURE__ */ jsx(
17882
+ "span",
17883
+ {
17884
+ "aria-hidden": true,
17885
+ 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)]"
17886
+ }
17887
+ )
17888
+ ]
17889
+ }
17890
+ ),
17891
+ visibleCols.map((col, i) => {
17892
+ if (headerGroupInfo?.rowSpanNames.has(col.name)) return null;
17893
+ const isSortable = !!col.sortable && !col.renderHeader;
17894
+ const sortDir = isSortable && sort?.name === col.name ? sort.dir : void 0;
17895
+ return /* @__PURE__ */ jsxs(
17896
+ "th",
17897
+ {
17898
+ className: cn(
17899
+ // 하단 border는 헤더 표가 연속으로 그린다.
17900
+ // leading-[0]: th 의 스트럿(line-height 만큼의 빈 인라인 박스)을 없앤다.
17901
+ // 내용이 inline-flex 라 스트럿이 남아 있으면 라벨보다 큰 컨트롤(정렬 버튼 등)이
17902
+ // 들어올 때 줄 상자가 베이스라인 기준으로 늘어나 헤더 높이를 넘기고
17903
+ // 세로 가운데도 어긋난다. 글자 줄높이는 아래 th__content 가 다시 준다.
17904
+ headerGroupInfo && "border-r border-solid border-[color:var(--cmp-table-border-color)]",
17905
+ headerGroupInfo && i === visibleCols.length - 1 && "border-r-0",
17906
+ "relative bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[0] text-[color:var(--sys-color-fg-primary)]",
17907
+ stickyInfo(i).isSticky && "z-[6]",
17908
+ col.thClass
17909
+ ),
17910
+ style: {
17911
+ textAlign: col.align ?? "left",
17912
+ boxShadow: stickyShadowValue(i) ?? void 0,
17913
+ ...cellStyle(i),
17914
+ ...col.thStyle
17915
+ },
17916
+ "aria-sort": isSortable ? sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none" : void 0,
17917
+ children: [
17918
+ /* @__PURE__ */ jsx(
17919
+ "span",
17920
+ {
17921
+ className: cn(
17922
+ // 원본 th__content: align에 따라 justify-content 적용 (th도 정렬 반영)
17923
+ // th 가 leading-[0] 이므로 글자 줄높이는 여기서 준다 (truncate 가
17924
+ // overflow-hidden 이라 줄높이가 0이면 라벨이 잘려 사라진다).
17925
+ "inline-flex w-full min-w-0 items-center gap-[4px] leading-[20px]",
17926
+ col.align === "center" && "justify-center",
17927
+ col.align === "right" && "justify-end"
17928
+ ),
17929
+ children: col.renderHeader ? col.renderHeader(col, i) : /* @__PURE__ */ jsxs(Fragment, { children: [
17930
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: col.label }),
17931
+ col.helpText && col.helpText.length > 0 && // 라벨이 좁아져 truncate 되더라도 아이콘까지 함께 줄어들면 안 되므로
17932
+ // shrink-0 래퍼로 감싼다 (STooltip 의 className 은 본문 쪽으로 간다).
17933
+ // 아이콘·크기·색은 SKeyValueTable 헤더 도움말과 동일하게 맞춘다.
17934
+ /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx(
17935
+ STooltip2,
17936
+ {
17937
+ trigger: "hover",
17938
+ icon: "helpOutline",
17939
+ iconSize: 12,
17940
+ color: "var(--sys-color-fg-success)",
17941
+ placement: "top",
17942
+ message: col.helpText
17943
+ }
17944
+ ) }),
17945
+ col.editable && /* @__PURE__ */ jsx(
17946
+ SIcon,
17947
+ {
17948
+ name: "pageEdit",
17949
+ size: 12,
17950
+ color: "var(--sys-color-link-accent)",
17951
+ label: "\uC218\uC815 \uAC00\uB2A5",
17952
+ className: "shrink-0"
17953
+ }
17954
+ ),
17955
+ col.navigable && /* @__PURE__ */ jsx(
17956
+ SIcon,
17957
+ {
17958
+ name: "pageMove",
17959
+ size: 12,
17960
+ color: "var(--sys-color-link-accent)",
17961
+ label: "\uC0C8 \uD654\uBA74\uC5D0\uC11C \uC5F4\uAE30",
17962
+ className: "shrink-0"
17963
+ }
17964
+ ),
17965
+ col.required && // 라벨이 truncate 돼도 필수 표시는 남아야 하므로 shrink-0.
17966
+ // 아이콘·크기·색은 SKeyValueTable 헤더 필수 표시와 동일하게 맞춘다.
17967
+ /* @__PURE__ */ jsx(
17968
+ SIcon,
17969
+ {
17970
+ name: "asterisk",
17971
+ size: 12,
17972
+ color: "var(--sys-color-fg-accent)",
17973
+ className: "shrink-0"
17974
+ }
17975
+ ),
17976
+ isSortable && // 미정렬은 updown 을 default(회색)로 — 정렬할 수 있다는 것 자체가
17977
+ // 헤더에 보여야 한다. 정렬 중이면 action 색으로 올려 활성임을 알린다.
17978
+ // 라벨이 truncate 돼도 버튼은 남도록 shrink-0.
17979
+ /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx(
17980
+ SGhostButton,
17981
+ {
17982
+ size: "xxs",
17983
+ intent: sortDir ? "action" : "default",
17984
+ icon: sortDir === "asc" ? "arrowUp" : sortDir === "desc" ? "arrowDown" : "updown",
17985
+ ariaLabel: `${col.label} \uC815\uB82C`,
17986
+ onClick: () => toggleSort(col)
17987
+ }
17988
+ ) })
17989
+ ] })
17990
+ }
17991
+ ),
17992
+ headerGroupInfo ? headerResizeHitArea(i, `resizer-${col.name}`) : headerResizer(i, `resizer-${col.name}`)
17993
+ ]
17994
+ },
17995
+ col.name
17996
+ );
17997
+ })
17998
+ ] })
17999
+ ] })
18000
+ ]
18001
+ }
18002
+ );
18003
+ return /* @__PURE__ */ jsxs(
18004
+ "div",
18005
+ {
18006
+ className: cn("flex flex-col", className),
18007
+ style: { width, ...style },
18008
+ "data-table-id": resolvedTableId,
18009
+ children: [
18010
+ /* @__PURE__ */ jsxs(
18011
+ "div",
18012
+ {
18013
+ className: cn(
18014
+ // isolate: no-data/loading 오버레이(z-30·31)를 테이블 안에서 끝낸다. relative + z-auto
18015
+ // 만으로는 스태킹 컨텍스트가 생기지 않아, 모달 안에 들어가면 30·31이 모달 Content
18016
+ // 레벨까지 올라가 같은 컨텍스트의 floating portal 밴드(z-2)를 덮는다.
18017
+ // 자손은 이미 overflow-hidden으로 클리핑되므로 테이블 내부 순서는 달라지지 않는다.
18018
+ "bg-white relative isolate flex flex-col overflow-hidden",
18019
+ bordered && "border border-solid border-[color:var(--cmp-table-border-color)]",
18020
+ // height가 없으면 루트가 받은 높이를 끝까지 채운다. 호스트가 className으로
18021
+ // `flex-1 min-h-0` 등을 줘 루트 높이가 확정된 경우, 행이 적어도 테두리 박스가
18022
+ // 남은 영역을 그대로 차지한다. basis는 auto로 둬 높이가 확정되지 않은
18023
+ // 기본 사용(내용만큼 커지는 케이스)에서는 동작이 바뀌지 않는다.
18024
+ !height && "min-h-0 grow",
17519
18025
  radiusClass,
17520
18026
  // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 인터랙션 차단
17521
18027
  (loading2 || rowCount === 0) && "pointer-events-none"
@@ -17525,220 +18031,20 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17525
18031
  minHeight: isNoData ? "calc(var(--cmp-table-header-height) + 60px)" : void 0
17526
18032
  },
17527
18033
  children: [
17528
- /* @__PURE__ */ jsx(
17529
- "div",
17530
- {
17531
- ref: headerScrollRef,
17532
- className: "flex-none overflow-hidden",
17533
- style: {
17534
- borderRight: headerGutter ? `${headerGutter}px solid var(--cmp-table-header-bg)` : void 0
17535
- },
17536
- children: /* @__PURE__ */ jsxs(
17537
- "table",
17538
- {
17539
- className: "w-full table-fixed border-separate border-spacing-0 text-left",
17540
- style: { fontSize: 12 },
17541
- children: [
17542
- colgroup,
17543
- /* @__PURE__ */ jsxs("thead", { className: cn(isNoData && "opacity-40"), children: [
17544
- headerGroupInfo && /* @__PURE__ */ jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17545
- selectable && /* @__PURE__ */ jsx(
17546
- "th",
17547
- {
17548
- rowSpan: 2,
17549
- className: cn(
17550
- "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17551
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17552
- ),
17553
- style: {
17554
- width: SELECTABLE_COLUMN_WIDTH,
17555
- minWidth: SELECTABLE_COLUMN_WIDTH,
17556
- maxWidth: SELECTABLE_COLUMN_WIDTH
17557
- },
17558
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(
17559
- SCheckbox,
17560
- {
17561
- value: headerCheck,
17562
- disabled: !hasSelectableRow,
17563
- onValueChange: toggleAll
17564
- }
17565
- ) })
17566
- }
17567
- ),
17568
- headerGroupInfo.cells.map((group) => /* @__PURE__ */ jsxs(
17569
- "th",
17570
- {
17571
- colSpan: group.colSpan,
17572
- rowSpan: group.rowSpan,
17573
- className: cn(
17574
- "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)]",
17575
- group.firstIndex + group.colSpan === visibleCols.length && "border-r-0",
17576
- group.rowSpan === 2 && "border-b-0",
17577
- group.className
17578
- ),
17579
- style: { textAlign: group.align ?? "left", ...group.style },
17580
- children: [
17581
- group.render ? group.render() : group.label,
17582
- group.rowSpan === 2 && group.colSpan === 1 ? headerResizeHitArea(
17583
- group.firstIndex,
17584
- `group-resizer-${group.columnNames[0]}`
17585
- ) : null
17586
- ]
17587
- },
17588
- group.columnNames.join("|")
17589
- ))
17590
- ] }),
17591
- /* @__PURE__ */ jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17592
- selectable && !headerGroupInfo && /* @__PURE__ */ jsxs(
17593
- "th",
17594
- {
17595
- className: cn(
17596
- // 원본 .th--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
17597
- // 하단 border는 헤더 래퍼가 연속으로 그리므로 셀에는 두지 않는다.
17598
- "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17599
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17600
- ),
17601
- style: {
17602
- width: SELECTABLE_COLUMN_WIDTH,
17603
- minWidth: SELECTABLE_COLUMN_WIDTH,
17604
- maxWidth: SELECTABLE_COLUMN_WIDTH
17605
- },
17606
- children: [
17607
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(
17608
- SCheckbox,
17609
- {
17610
- value: headerCheck,
17611
- disabled: !hasSelectableRow,
17612
- onValueChange: toggleAll
17613
- }
17614
- ) }),
17615
- /* @__PURE__ */ jsx(
17616
- "span",
17617
- {
17618
- "aria-hidden": true,
17619
- 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)]"
17620
- }
17621
- )
17622
- ]
17623
- }
17624
- ),
17625
- visibleCols.map((col, i) => {
17626
- if (headerGroupInfo?.rowSpanNames.has(col.name)) return null;
17627
- const isSortable = !!col.sortable && !col.renderHeader;
17628
- const sortDir = isSortable && sort?.name === col.name ? sort.dir : void 0;
17629
- return /* @__PURE__ */ jsxs(
17630
- "th",
17631
- {
17632
- className: cn(
17633
- // 하단 border는 헤더 래퍼가 연속으로 그린다 (거터 영역까지 이어짐).
17634
- // leading-[0]: th 의 스트럿(line-height 만큼의 빈 인라인 박스)을 없앤다.
17635
- // 내용이 inline-flex 라 스트럿이 남아 있으면 라벨보다 큰 컨트롤(정렬 버튼 등)이
17636
- // 들어올 때 줄 상자가 베이스라인 기준으로 늘어나 헤더 높이를 넘기고
17637
- // 세로 가운데도 어긋난다. 글자 줄높이는 아래 th__content 가 다시 준다.
17638
- headerGroupInfo && "border-r border-solid border-[color:var(--cmp-table-border-color)]",
17639
- headerGroupInfo && i === visibleCols.length - 1 && "border-r-0",
17640
- "relative bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[0] text-[color:var(--sys-color-fg-primary)]",
17641
- stickyInfo(i).isSticky && "z-[6]",
17642
- col.thClass
17643
- ),
17644
- style: {
17645
- textAlign: col.align ?? "left",
17646
- boxShadow: stickyShadowValue(i) ?? void 0,
17647
- ...cellStyle(i),
17648
- ...col.thStyle
17649
- },
17650
- "aria-sort": isSortable ? sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none" : void 0,
17651
- children: [
17652
- /* @__PURE__ */ jsx(
17653
- "span",
17654
- {
17655
- className: cn(
17656
- // 원본 th__content: align에 따라 justify-content 적용 (th도 정렬 반영)
17657
- // th 가 leading-[0] 이므로 글자 줄높이는 여기서 준다 (truncate 가
17658
- // overflow-hidden 이라 줄높이가 0이면 라벨이 잘려 사라진다).
17659
- "inline-flex w-full min-w-0 items-center gap-[4px] leading-[20px]",
17660
- col.align === "center" && "justify-center",
17661
- col.align === "right" && "justify-end"
17662
- ),
17663
- children: col.renderHeader ? col.renderHeader(col, i) : /* @__PURE__ */ jsxs(Fragment, { children: [
17664
- /* @__PURE__ */ jsx("span", { className: "truncate", children: col.label }),
17665
- col.helpText && col.helpText.length > 0 && // 라벨이 좁아져 truncate 되더라도 아이콘까지 함께 줄어들면 안 되므로
17666
- // shrink-0 래퍼로 감싼다 (STooltip 의 className 은 본문 쪽으로 간다).
17667
- // 아이콘·크기·색은 SKeyValueTable 헤더 도움말과 동일하게 맞춘다.
17668
- /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx(
17669
- STooltip2,
17670
- {
17671
- trigger: "hover",
17672
- icon: "helpOutline",
17673
- iconSize: 12,
17674
- color: "var(--sys-color-fg-success)",
17675
- placement: "top",
17676
- message: col.helpText
17677
- }
17678
- ) }),
17679
- col.editable && /* @__PURE__ */ jsx(
17680
- SIcon,
17681
- {
17682
- name: "pageEdit",
17683
- size: 12,
17684
- color: "var(--sys-color-link-accent)",
17685
- label: "\uC218\uC815 \uAC00\uB2A5",
17686
- className: "shrink-0"
17687
- }
17688
- ),
17689
- col.navigable && /* @__PURE__ */ jsx(
17690
- SIcon,
17691
- {
17692
- name: "pageMove",
17693
- size: 12,
17694
- color: "var(--sys-color-link-accent)",
17695
- label: "\uC0C8 \uD654\uBA74\uC5D0\uC11C \uC5F4\uAE30",
17696
- className: "shrink-0"
17697
- }
17698
- ),
17699
- col.required && // 라벨이 truncate 돼도 필수 표시는 남아야 하므로 shrink-0.
17700
- // 아이콘·크기·색은 SKeyValueTable 헤더 필수 표시와 동일하게 맞춘다.
17701
- /* @__PURE__ */ jsx(
17702
- SIcon,
17703
- {
17704
- name: "asterisk",
17705
- size: 12,
17706
- color: "var(--sys-color-fg-accent)",
17707
- className: "shrink-0"
17708
- }
17709
- ),
17710
- isSortable && // 미정렬은 updown 을 default(회색)로 — 정렬할 수 있다는 것 자체가
17711
- // 헤더에 보여야 한다. 정렬 중이면 action 색으로 올려 활성임을 알린다.
17712
- // 라벨이 truncate 돼도 버튼은 남도록 shrink-0.
17713
- /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx(
17714
- SGhostButton,
17715
- {
17716
- size: "xxs",
17717
- intent: sortDir ? "action" : "default",
17718
- icon: sortDir === "asc" ? "arrowUp" : sortDir === "desc" ? "arrowDown" : "updown",
17719
- ariaLabel: `${col.label} \uC815\uB82C`,
17720
- onClick: () => toggleSort(col)
17721
- }
17722
- ) })
17723
- ] })
17724
- }
17725
- ),
17726
- headerGroupInfo ? headerResizeHitArea(i, `resizer-${col.name}`) : headerResizer(i, `resizer-${col.name}`)
17727
- ]
17728
- },
17729
- col.name
17730
- );
17731
- })
17732
- ] })
17733
- ] })
17734
- ]
17735
- }
17736
- )
17737
- }
17738
- ),
17739
- showNoDataSlot ? /* @__PURE__ */ jsx("div", { className: cn(bodyScrollClass, "pointer-events-auto bg-white"), children: /* @__PURE__ */ jsx("div", { className: "flex h-full min-h-[60px] w-full items-center justify-center", children: emptySlot }) }) : (
17740
- /* 바디: 세로 스크롤은 이 컨테이너에만 생긴다 (헤더 제외) */
17741
- /* @__PURE__ */ jsx(
18034
+ showNoDataSlot ? /* @__PURE__ */ jsxs(Fragment, { children: [
18035
+ /* @__PURE__ */ jsx("div", { className: "flex-none overflow-hidden", children: headerTable }),
18036
+ /* @__PURE__ */ jsx("div", { className: cn(bodyScrollClass, "pointer-events-auto bg-white"), children: /* @__PURE__ */ jsx("div", { className: "flex h-full min-h-[60px] w-full items-center justify-center", children: emptySlot }) })
18037
+ ] }) : (
18038
+ /**
18039
+ * 헤더와 body 가 한 스크롤 컨테이너에 있다 — 헤더는 위에 붙어(sticky) 세로로는 서 있고
18040
+ * 가로로는 body 와 같은 스크롤로 움직인다. 헤더를 따로 두고 scroll 이벤트에서
18041
+ * scrollLeft 를 맞추면, 브라우저가 body 스크롤을 먼저 그리고 이벤트를 나중에 돌려
18042
+ * 헤더가 한 프레임 이상 늦게 따라온다.
18043
+ *
18044
+ * 행 좌표는 body 표 기준 그대로다 — `scrollTop` 이 y 이면 y 에 있는 행이 헤더 바로
18045
+ * 아래에 선다. 보이는 높이만 헤더 높이만큼 줄어든다(`headerHeight`).
18046
+ */
18047
+ /* @__PURE__ */ jsxs(
17742
18048
  "div",
17743
18049
  {
17744
18050
  ref: scrollRef,
@@ -17748,136 +18054,140 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17748
18054
  // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 스크롤 차단
17749
18055
  (loading2 || rowCount === 0) && "overflow-hidden"
17750
18056
  ),
17751
- children: /* @__PURE__ */ jsxs(
17752
- "table",
17753
- {
17754
- className: "w-full table-fixed border-separate border-spacing-0 text-left",
17755
- style: { fontSize: 12 },
17756
- children: [
17757
- colgroup,
17758
- /* @__PURE__ */ jsxs("tbody", { children: [
17759
- virtualScroll && topSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: topSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) }),
17760
- rowCount === 0 ? null : bodyRows.map((row, ri) => {
17761
- const rowIndex = rowOffset + ri;
17762
- const isSel = isViewSelected(row);
17763
- const dragSel = dragSelectOn && isSel;
17764
- const dragTop = dragSel && !isViewSelected(rows[rowIndex - 1]);
17765
- const dragBottom = dragSel && !isViewSelected(rows[rowIndex + 1]);
17766
- const rowLocked = (selectable || dragSelectable) && !canSelectRow(row);
17767
- const rowBg = "bg-white";
17768
- const rowHover = hoverable && !rowLocked;
17769
- return /* @__PURE__ */ jsxs(
17770
- "tr",
17771
- {
17772
- "data-row-index": rowIndex,
17773
- onClick: () => onRowClick?.(row),
17774
- onDoubleClick: onRowDoubleClick ? (event) => {
17775
- const target = event.target;
17776
- if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
17777
- onRowDoubleClick(row);
17778
- } : void 0,
17779
- onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
17780
- onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
17781
- onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
17782
- className: cn(
17783
- "group/table-row",
17784
- rowHover && "hover:bg-[var(--color-grey-05)]",
17785
- (onRowClick || onRowDoubleClick) && "cursor-pointer",
17786
- dragSelectOn && "select-none",
17787
- rowClassName?.(row)
17788
- ),
17789
- style: { height: virtualScroll ? effectiveRowHeight : rowH },
17790
- children: [
17791
- selectable && /* @__PURE__ */ jsx(
17792
- "td",
17793
- {
17794
- className: cn(
17795
- // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
17796
- "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
18057
+ style: { scrollPaddingTop: headerHeight },
18058
+ children: [
18059
+ /* @__PURE__ */ jsx("div", { ref: headerRef, className: "sticky top-0 z-[8]", children: headerTable }),
18060
+ /* @__PURE__ */ jsxs(
18061
+ "table",
18062
+ {
18063
+ className: "w-full table-fixed border-separate border-spacing-0 text-left",
18064
+ style: { fontSize: 12 },
18065
+ children: [
18066
+ colgroup,
18067
+ /* @__PURE__ */ jsxs("tbody", { children: [
18068
+ virtualScroll && topSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: topSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) }),
18069
+ rowCount === 0 ? null : bodyRows.map((row, ri) => {
18070
+ const rowIndex = rowOffset + ri;
18071
+ const isSel = isViewSelected(row);
18072
+ const dragSel = dragSelectOn && isSel;
18073
+ const dragTop = dragSel && !isViewSelected(rows[rowIndex - 1]);
18074
+ const dragBottom = dragSel && !isViewSelected(rows[rowIndex + 1]);
18075
+ const rowLocked = (selectable || dragSelectable) && !canSelectRow(row);
18076
+ const rowBg = "bg-white";
18077
+ const rowHover = hoverable && !rowLocked;
18078
+ return /* @__PURE__ */ jsxs(
18079
+ "tr",
18080
+ {
18081
+ "data-row-index": rowIndex,
18082
+ onClick: () => onRowClick?.(row),
18083
+ onDoubleClick: onRowDoubleClick ? (event) => {
18084
+ const target = event.target;
18085
+ if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
18086
+ onRowDoubleClick(row);
18087
+ } : void 0,
18088
+ onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
18089
+ onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
18090
+ onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
18091
+ className: cn(
18092
+ "group/table-row",
18093
+ rowHover && "hover:bg-[var(--color-grey-05)]",
18094
+ (onRowClick || onRowDoubleClick) && "cursor-pointer",
18095
+ dragSelectOn && "select-none",
18096
+ rowClassName?.(row)
18097
+ ),
18098
+ style: { height: virtualScroll ? effectiveRowHeight : rowH },
18099
+ children: [
18100
+ selectable && /* @__PURE__ */ jsx(
18101
+ "td",
18102
+ {
18103
+ className: cn(
18104
+ // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
18105
+ "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
18106
+ rowBg,
18107
+ rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
18108
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
18109
+ ),
18110
+ style: {
18111
+ width: SELECTABLE_COLUMN_WIDTH,
18112
+ minWidth: SELECTABLE_COLUMN_WIDTH,
18113
+ maxWidth: SELECTABLE_COLUMN_WIDTH,
18114
+ // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
18115
+ background: rowLocked ? LOCKED_BG : void 0
18116
+ },
18117
+ onClick: (e) => e.stopPropagation(),
18118
+ onDoubleClick: (e) => e.stopPropagation(),
18119
+ onClickCapture: (e) => {
18120
+ shiftPressedRef.current = e.shiftKey;
18121
+ if (e.shiftKey) window.getSelection()?.removeAllRanges();
18122
+ },
18123
+ children: /* @__PURE__ */ jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsx(
18124
+ SCheckbox,
18125
+ {
18126
+ value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
18127
+ disabled: !canSelectRow(row),
18128
+ onValueChange: () => toggleRow(row, rowOffset + ri)
18129
+ }
18130
+ ) })
18131
+ }
18132
+ ),
18133
+ visibleCols.map((col, i) => {
18134
+ const tdClassName = cn(
18135
+ // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
18136
+ // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
18137
+ "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)]",
17797
18138
  rowBg,
17798
18139
  rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
17799
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17800
- ),
17801
- style: {
17802
- width: SELECTABLE_COLUMN_WIDTH,
17803
- minWidth: SELECTABLE_COLUMN_WIDTH,
17804
- maxWidth: SELECTABLE_COLUMN_WIDTH,
17805
- // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
17806
- background: rowLocked ? LOCKED_BG : void 0
17807
- },
17808
- onClick: (e) => e.stopPropagation(),
17809
- onDoubleClick: (e) => e.stopPropagation(),
17810
- onClickCapture: (e) => {
17811
- shiftPressedRef.current = e.shiftKey;
17812
- if (e.shiftKey) window.getSelection()?.removeAllRanges();
17813
- },
17814
- children: /* @__PURE__ */ jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsx(
17815
- SCheckbox,
17816
- {
17817
- value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
17818
- disabled: !canSelectRow(row),
17819
- onValueChange: () => toggleRow(row, rowOffset + ri)
17820
- }
17821
- ) })
17822
- }
17823
- ),
17824
- visibleCols.map((col, i) => {
17825
- const tdClassName = cn(
17826
- // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
17827
- // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
17828
- "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)]",
17829
- rowBg,
17830
- rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
17831
- typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
17832
- );
17833
- const tdPadX = cellPadXOf(col);
17834
- const dragEdges = dragSel ? [
17835
- dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
17836
- dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
17837
- i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
17838
- i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
17839
- ].filter(Boolean) : [];
17840
- const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
17841
- const tdStyle = {
17842
- paddingLeft: tdPadX,
17843
- paddingRight: tdPadX,
17844
- paddingTop: cellPadY,
17845
- paddingBottom: cellPadY,
17846
- textAlign: col.align ?? "left",
17847
- // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
17848
- background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
17849
- // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
17850
- // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
17851
- color: rowLocked ? LOCKED_CONTENT : void 0,
17852
- boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
17853
- ...cellStyle(i)
17854
- };
17855
- if (col.renderCell) {
17856
- const cell = col.renderCell({
17857
- row,
17858
- value: cellValue(col, row),
17859
- column: col,
17860
- colIndex: i,
17861
- rowIndex: ri,
17862
- isSelected: isSel,
17863
- className: tdClassName,
17864
- style: tdStyle
17865
- });
17866
- if (cell == null || cell === false) return null;
17867
- return /* @__PURE__ */ jsx(Fragment$1, { children: cell }, col.name);
17868
- }
17869
- return /* @__PURE__ */ jsx("td", { className: tdClassName, style: tdStyle, children: col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row) }, col.name);
17870
- })
17871
- ]
17872
- },
17873
- row[rowKey] ?? ri
17874
- );
17875
- }),
17876
- virtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
17877
- ] })
17878
- ]
17879
- }
17880
- )
18140
+ typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
18141
+ );
18142
+ const tdPadX = cellPadXOf(col);
18143
+ const dragEdges = dragSel ? [
18144
+ dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
18145
+ dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
18146
+ i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
18147
+ i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
18148
+ ].filter(Boolean) : [];
18149
+ const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
18150
+ const tdStyle = {
18151
+ paddingLeft: tdPadX,
18152
+ paddingRight: tdPadX,
18153
+ paddingTop: cellPadY,
18154
+ paddingBottom: cellPadY,
18155
+ textAlign: col.align ?? "left",
18156
+ // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
18157
+ background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
18158
+ // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
18159
+ // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
18160
+ color: rowLocked ? LOCKED_CONTENT : void 0,
18161
+ boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
18162
+ ...cellStyle(i)
18163
+ };
18164
+ if (col.renderCell) {
18165
+ const cell = col.renderCell({
18166
+ row,
18167
+ value: cellValue(col, row),
18168
+ column: col,
18169
+ colIndex: i,
18170
+ rowIndex: ri,
18171
+ isSelected: isSel,
18172
+ className: tdClassName,
18173
+ style: tdStyle
18174
+ });
18175
+ if (cell == null || cell === false) return null;
18176
+ return /* @__PURE__ */ jsx(Fragment$1, { children: cell }, col.name);
18177
+ }
18178
+ return /* @__PURE__ */ jsx("td", { className: tdClassName, style: tdStyle, children: col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row) }, col.name);
18179
+ })
18180
+ ]
18181
+ },
18182
+ row[rowKey] ?? ri
18183
+ );
18184
+ }),
18185
+ virtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
18186
+ ] })
18187
+ ]
18188
+ }
18189
+ )
18190
+ ]
17881
18191
  }
17882
18192
  )
17883
18193
  ),
@@ -21154,6 +21464,169 @@ function ChartAxisLabels({
21154
21464
  }
21155
21465
  );
21156
21466
  }
21467
+ function useElementSize(target) {
21468
+ const ref = useRef(null);
21469
+ const [size, setSize] = useState({ width: 0, height: 0 });
21470
+ useEffect(() => {
21471
+ const element = ref.current;
21472
+ if (!element) return;
21473
+ const update = () => {
21474
+ const { width, height } = element.getBoundingClientRect();
21475
+ setSize(
21476
+ (prev) => Math.abs(prev.width - width) < 0.5 && Math.abs(prev.height - height) < 0.5 ? prev : { width, height }
21477
+ );
21478
+ };
21479
+ update();
21480
+ if (typeof ResizeObserver === "undefined") return;
21481
+ const observer = new ResizeObserver(update);
21482
+ observer.observe(element);
21483
+ return () => observer.disconnect();
21484
+ }, [target]);
21485
+ return { ref, size };
21486
+ }
21487
+ function useSvgTextMetrics() {
21488
+ const ref = useRef(null);
21489
+ const [metrics, setMetrics] = useState({ top: 0, width: 0 });
21490
+ useEffect(() => {
21491
+ const element = ref.current;
21492
+ if (!element) return;
21493
+ let box3;
21494
+ try {
21495
+ box3 = element.getBBox();
21496
+ } catch {
21497
+ return;
21498
+ }
21499
+ setMetrics(
21500
+ (prev) => Math.abs(prev.top - box3.y) < 0.5 && Math.abs(prev.width - box3.width) < 0.5 ? prev : { top: box3.y, width: box3.width }
21501
+ );
21502
+ });
21503
+ return { ref, metrics };
21504
+ }
21505
+ var CATEGORY_LABEL_ROTATED_MAX = 120;
21506
+ var DIAGONAL = Math.SQRT1_2;
21507
+ function ChartRotatedAxisLayout({
21508
+ axis,
21509
+ axisStyle,
21510
+ plot,
21511
+ minPlotWidth,
21512
+ labels,
21513
+ xAt,
21514
+ labelsRef
21515
+ }) {
21516
+ const scrollRef = useRef(null);
21517
+ const { ref: axisRef, size: axisBox } = useElementSize();
21518
+ return /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "min-w-0 overflow-x-auto", style: { gridColumn: "1 / -1" }, children: /* @__PURE__ */ jsxs("div", { style: { width: `max(100%, ${axisBox.width + minPlotWidth}px)` }, children: [
21519
+ /* @__PURE__ */ jsxs("div", { className: "flex", children: [
21520
+ /* @__PURE__ */ jsx(
21521
+ "div",
21522
+ {
21523
+ ref: axisRef,
21524
+ className: "sticky left-0 z-1 shrink-0 bg-(--cmp-chart-bg)",
21525
+ style: { ...axisStyle, paddingRight: AXIS_LABEL_GAP },
21526
+ children: axis
21527
+ }
21528
+ ),
21529
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: plot })
21530
+ ] }),
21531
+ /* @__PURE__ */ jsx("div", { ref: labelsRef, children: /* @__PURE__ */ jsx(
21532
+ ChartRotatedCategoryLabels,
21533
+ {
21534
+ labels,
21535
+ xAt,
21536
+ gutter: axisBox.width,
21537
+ scrollRef
21538
+ }
21539
+ ) })
21540
+ ] }) });
21541
+ }
21542
+ function ChartRotatedCategoryLabels({
21543
+ labels,
21544
+ xAt,
21545
+ gutter,
21546
+ scrollRef
21547
+ }) {
21548
+ const sizerRef = useRef(null);
21549
+ const maskRef = useRef(null);
21550
+ const [box3, setBox] = useState({ line: 0, widest: 0 });
21551
+ useEffect(() => {
21552
+ const sizer = sizerRef.current;
21553
+ if (!sizer) return;
21554
+ const measure = () => {
21555
+ const { width, height: height2 } = sizer.getBoundingClientRect();
21556
+ const line = labels.length > 0 ? height2 / labels.length : 0;
21557
+ setBox(
21558
+ (prev) => Math.abs(prev.line - line) < 0.5 && Math.abs(prev.widest - width) < 0.5 ? prev : { line, widest: width }
21559
+ );
21560
+ };
21561
+ measure();
21562
+ if (typeof ResizeObserver === "undefined") return;
21563
+ const observer = new ResizeObserver(measure);
21564
+ observer.observe(sizer);
21565
+ return () => observer.disconnect();
21566
+ });
21567
+ useEffect(() => {
21568
+ const scroller = scrollRef.current;
21569
+ const mask = maskRef.current;
21570
+ if (!scroller || !mask) return;
21571
+ const sync2 = () => {
21572
+ mask.style.visibility = scroller.scrollLeft > 0 ? "visible" : "hidden";
21573
+ };
21574
+ sync2();
21575
+ scroller.addEventListener("scroll", sync2, { passive: true });
21576
+ return () => scroller.removeEventListener("scroll", sync2);
21577
+ }, [scrollRef]);
21578
+ const maxWidthAt = (index) => Math.max(
21579
+ 0,
21580
+ Math.min(CATEGORY_LABEL_ROTATED_MAX, (xAt(index) + gutter) / DIAGONAL - box3.line / 2)
21581
+ );
21582
+ const height = Math.ceil(box3.line / 2 + (box3.widest + box3.line / 2) * DIAGONAL);
21583
+ return (
21584
+ // overflow 는 clip 이어야 한다 — hidden 이면 이 행이 스크롤 상자로 잡혀 가림막의
21585
+ // sticky 가 스크롤을 따라가지 않는다.
21586
+ /* @__PURE__ */ jsxs(
21587
+ "div",
21588
+ {
21589
+ "aria-hidden": true,
21590
+ className: "relative overflow-clip typo-body-sm-medium text-(--cmp-chart-axis-color)",
21591
+ style: { height },
21592
+ children: [
21593
+ /* @__PURE__ */ jsx(
21594
+ "div",
21595
+ {
21596
+ ref: maskRef,
21597
+ className: "invisible sticky left-0 z-1 h-full bg-(--cmp-chart-bg)",
21598
+ style: { width: gutter }
21599
+ }
21600
+ ),
21601
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 right-0", style: { left: gutter }, children: labels.map((label, index) => /* @__PURE__ */ jsx(
21602
+ "span",
21603
+ {
21604
+ className: "absolute top-0 truncate",
21605
+ style: {
21606
+ left: xAt(index),
21607
+ maxWidth: maxWidthAt(index),
21608
+ // 글자 끝(오른쪽 가운데)을 눈금에 맞춘 뒤 그 점을 축으로 기울인다.
21609
+ transform: "translateX(-100%) rotate(-45deg)",
21610
+ transformOrigin: "right center"
21611
+ },
21612
+ children: label
21613
+ },
21614
+ `${label}-${index}`
21615
+ )) }),
21616
+ /* @__PURE__ */ jsx("div", { ref: sizerRef, className: "invisible absolute top-0 left-0 flex flex-col items-start", children: labels.map((label, index) => /* @__PURE__ */ jsx(
21617
+ "span",
21618
+ {
21619
+ className: "block truncate",
21620
+ style: { maxWidth: maxWidthAt(index) },
21621
+ children: label
21622
+ },
21623
+ `${label}-${index}`
21624
+ )) })
21625
+ ]
21626
+ }
21627
+ )
21628
+ );
21629
+ }
21157
21630
  var ChartFrame = /* @__PURE__ */ forwardRef(function ChartFrame2({ title, unit, legend, legendPosition = "top", children, className, style, ...props }, ref) {
21158
21631
  return /* @__PURE__ */ jsxs(
21159
21632
  "div",
@@ -21345,44 +21818,6 @@ function linePointDimmedColor(palette, index, colors) {
21345
21818
  function dataValueColor(palette, index, colors) {
21346
21819
  return pick(DATA_VALUE_COLORS, DATA_VALUE_PALETTES, palette, index, colors);
21347
21820
  }
21348
- function useElementSize() {
21349
- const ref = useRef(null);
21350
- const [size, setSize] = useState({ width: 0, height: 0 });
21351
- useEffect(() => {
21352
- const element = ref.current;
21353
- if (!element) return;
21354
- const update = () => {
21355
- const { width, height } = element.getBoundingClientRect();
21356
- setSize(
21357
- (prev) => Math.abs(prev.width - width) < 0.5 && Math.abs(prev.height - height) < 0.5 ? prev : { width, height }
21358
- );
21359
- };
21360
- update();
21361
- if (typeof ResizeObserver === "undefined") return;
21362
- const observer = new ResizeObserver(update);
21363
- observer.observe(element);
21364
- return () => observer.disconnect();
21365
- }, []);
21366
- return { ref, size };
21367
- }
21368
- function useSvgTextMetrics() {
21369
- const ref = useRef(null);
21370
- const [metrics, setMetrics] = useState({ top: 0, width: 0 });
21371
- useEffect(() => {
21372
- const element = ref.current;
21373
- if (!element) return;
21374
- let box3;
21375
- try {
21376
- box3 = element.getBBox();
21377
- } catch {
21378
- return;
21379
- }
21380
- setMetrics(
21381
- (prev) => Math.abs(prev.top - box3.y) < 0.5 && Math.abs(prev.width - box3.width) < 0.5 ? prev : { top: box3.y, width: box3.width }
21382
- );
21383
- });
21384
- return { ref, metrics };
21385
- }
21386
21821
  var DOT_SIZE2 = 6;
21387
21822
  var DEFAULT_GAP_X = { after: 4, before: 4 };
21388
21823
  var ANCHOR_GAP_Y = 0;
@@ -21544,6 +21979,7 @@ var SBarChart = /* @__PURE__ */ forwardRef(function SBarChart2({
21544
21979
  tickCount = 8,
21545
21980
  barSize = BAR_SIZE_MAX,
21546
21981
  showValueLabel = true,
21982
+ rotateCategoryLabels = false,
21547
21983
  showLegend = true,
21548
21984
  legendPosition = "top",
21549
21985
  showTooltip = true,
@@ -21555,9 +21991,10 @@ var SBarChart = /* @__PURE__ */ forwardRef(function SBarChart2({
21555
21991
  style
21556
21992
  }, ref) {
21557
21993
  const vertical = direction === "vertical";
21994
+ const rotateLabels = rotateCategoryLabels && vertical;
21558
21995
  const { ref: plotRef, size } = useElementSize();
21559
- const { ref: axisTextRef, size: axisText } = useElementSize();
21560
- const { ref: axisSampleRef, size: axisSample } = useElementSize();
21996
+ const { ref: axisTextRef, size: axisText } = useElementSize(rotateLabels);
21997
+ const { ref: axisSampleRef, size: axisSample } = useElementSize(rotateLabels);
21561
21998
  const clipId = useId();
21562
21999
  const [hoveredCategory, setHoveredCategory] = useState(null);
21563
22000
  const perCategory = series.length === 1;
@@ -22016,7 +22453,29 @@ var SBarChart = /* @__PURE__ */ forwardRef(function SBarChart2({
22016
22453
  // 눈금은 왼쪽 한 줄뿐이다 — 오른쪽에도 적으면 서로 다른 단위처럼 읽힌다.
22017
22454
  gridTemplateColumns: "max-content 1fr"
22018
22455
  },
22019
- children: vertical ? /* @__PURE__ */ jsxs(Fragment, { children: [
22456
+ children: rotateLabels ? (
22457
+ // 기울인 이름은 눈금 열 아래까지 뻗으므로 스크롤 상자가 눈금 열까지 감싼다.
22458
+ /* @__PURE__ */ jsx(
22459
+ ChartRotatedAxisLayout,
22460
+ {
22461
+ axis: /* @__PURE__ */ jsx(
22462
+ ChartAxisLabels,
22463
+ {
22464
+ scale,
22465
+ direction,
22466
+ format: formatValue,
22467
+ sampleRef: axisSampleRef
22468
+ }
22469
+ ),
22470
+ axisStyle: { height: plotHeight, ...inset },
22471
+ plot,
22472
+ minPlotWidth,
22473
+ labels: categories,
22474
+ xAt: (index) => layout.offset + (index + 0.5) * layout.band,
22475
+ labelsRef: axisTextRef
22476
+ }
22477
+ )
22478
+ ) : vertical ? /* @__PURE__ */ jsxs(Fragment, { children: [
22020
22479
  /* @__PURE__ */ jsx("div", { style: { height: plotHeight, ...inset }, children: /* @__PURE__ */ jsx(
22021
22480
  ChartAxisLabels,
22022
22481
  {
@@ -22147,6 +22606,7 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22147
22606
  tickCount = 8,
22148
22607
  showPoint = true,
22149
22608
  showValueLabel = false,
22609
+ rotateCategoryLabels = false,
22150
22610
  showLegend = true,
22151
22611
  legendPosition = "top",
22152
22612
  showTooltip = true,
@@ -22159,8 +22619,8 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22159
22619
  }, ref) {
22160
22620
  const smooth = curve === "smooth";
22161
22621
  const { ref: plotRef, size } = useElementSize();
22162
- const { ref: axisTextRef, size: axisText } = useElementSize();
22163
- const { ref: axisSampleRef, size: axisSample } = useElementSize();
22622
+ const { ref: axisTextRef, size: axisText } = useElementSize(rotateCategoryLabels);
22623
+ const { ref: axisSampleRef, size: axisSample } = useElementSize(rotateCategoryLabels);
22164
22624
  const gradientId = useId();
22165
22625
  const [hoveredCategory, setHoveredCategory] = useState(null);
22166
22626
  const colorAt = useCallback(
@@ -22194,23 +22654,16 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22194
22654
  [rows, tickCount, max]
22195
22655
  );
22196
22656
  const count = categories.length;
22197
- const step = count > 1 ? size.width / (count - 1) : 0;
22198
- const xAt = useCallback(
22199
- (index) => count > 1 ? index * step : size.width / 2,
22200
- [count, step, size.width]
22201
- );
22657
+ const band = Math.max(0, size.width - AXIS_EDGE_PADDING * 2) / Math.max(1, count);
22658
+ const xAt = useCallback((index) => AXIS_EDGE_PADDING + (index + 0.5) * band, [band]);
22202
22659
  const cellOf = useCallback(
22203
- (index) => count > 1 ? {
22204
- left: Math.max(0, xAt(index) - step / 2),
22205
- right: Math.min(size.width, xAt(index) + step / 2)
22206
- } : { left: 0, right: size.width },
22207
- [count, step, xAt, size.width]
22208
- );
22209
- const edgeOf = useCallback(
22210
- (index) => count > 1 && index === 0 ? "start" : count > 1 && index === count - 1 ? "end" : null,
22211
- [count]
22660
+ (index) => ({
22661
+ left: AXIS_EDGE_PADDING + index * band,
22662
+ right: AXIS_EDGE_PADDING + (index + 1) * band
22663
+ }),
22664
+ [band]
22212
22665
  );
22213
- const minPlotWidth = categories.length * BAND_MIN;
22666
+ const minPlotWidth = count * BAND_MIN + AXIS_EDGE_PADDING * 2;
22214
22667
  const totalHeight = height ?? 320;
22215
22668
  const plotHeight = typeof totalHeight === "number" ? Math.max(0, totalHeight - axisText.height) : `calc(${totalHeight} - ${axisText.height}px)`;
22216
22669
  const plotted = useMemo(() => {
@@ -22224,10 +22677,10 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22224
22677
  }, [rows, size.width, size.height, xAt, scale]);
22225
22678
  const legend = showLegend ? /* @__PURE__ */ jsx(ChartLegend, { items: series.map((s, i) => ({ name: s.name, color: colorAt(i) })) }) : null;
22226
22679
  const handleMove = (event) => {
22227
- if (!showTooltip || count === 0 || size.width <= 0 || series.length === 0) return;
22680
+ if (!showTooltip || count === 0 || band <= 0 || series.length === 0) return;
22228
22681
  const rect = event.currentTarget.getBoundingClientRect();
22229
- const along = event.clientX - rect.left;
22230
- const category = count > 1 ? Math.round(along / step) : 0;
22682
+ const along = event.clientX - rect.left - AXIS_EDGE_PADDING;
22683
+ const category = Math.floor(along / band);
22231
22684
  setHoveredCategory(category >= 0 && category < categories.length ? category : null);
22232
22685
  };
22233
22686
  const tooltipItems = hoveredCategory == null ? [] : rows.flatMap((row, seriesIndex) => {
@@ -22266,10 +22719,7 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22266
22719
  categories.map((label, index) => /* @__PURE__ */ jsx(
22267
22720
  "span",
22268
22721
  {
22269
- className: cn(
22270
- "absolute top-0 whitespace-nowrap",
22271
- edgeOf(index) === "start" ? "translate-x-0" : edgeOf(index) === "end" ? "-translate-x-full" : "-translate-x-1/2"
22272
- ),
22722
+ className: "absolute top-0 -translate-x-1/2 whitespace-nowrap",
22273
22723
  style: { left: xAt(index) },
22274
22724
  children: label
22275
22725
  },
@@ -22450,7 +22900,7 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22450
22900
  y: point.y - POINT_RADIUS - VALUE_LABEL_GAP2,
22451
22901
  className: hoveredCategory === categoryIndex ? LINE_DATA_LABEL_TYPO_HOVER : LINE_DATA_LABEL_TYPO,
22452
22902
  style: { fill: LINE_DATA_LABEL_COLOR },
22453
- textAnchor: edgeOf(categoryIndex) === "start" ? "start" : edgeOf(categoryIndex) === "end" ? "end" : "middle",
22903
+ textAnchor: "middle",
22454
22904
  dominantBaseline: "auto",
22455
22905
  children: formatValue(point.own)
22456
22906
  },
@@ -22458,9 +22908,106 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22458
22908
  )
22459
22909
  )
22460
22910
  ) : [],
22461
- [showValueLabel, plotted, hoveredCategory, formatValue, edgeOf]
22911
+ [showValueLabel, plotted, hoveredCategory, formatValue]
22462
22912
  );
22463
22913
  const inset = { paddingTop: axisSample.height / 2, paddingBottom: axisSample.height / 2 };
22914
+ const axisLabels = /* @__PURE__ */ jsx(
22915
+ ChartAxisLabels,
22916
+ {
22917
+ scale,
22918
+ direction: "vertical",
22919
+ format: formatValue,
22920
+ sampleRef: axisSampleRef
22921
+ }
22922
+ );
22923
+ const plot = (
22924
+ // 바깥은 자리(높이·물러남)를, 안쪽은 그림을 맡는다. minWidth 는 바깥에 둬야
22925
+ // 스크롤 컨테이너가 넘치는 폭을 알아본다.
22926
+ /* @__PURE__ */ jsx(
22927
+ "div",
22928
+ {
22929
+ className: "relative min-w-0",
22930
+ style: { height: plotHeight, minWidth: minPlotWidth || void 0, ...inset },
22931
+ children: /* @__PURE__ */ jsxs(
22932
+ "div",
22933
+ {
22934
+ ref: plotRef,
22935
+ className: "relative h-full w-full",
22936
+ onMouseMove: handleMove,
22937
+ onMouseLeave: () => setHoveredCategory(null),
22938
+ children: [
22939
+ size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxs(
22940
+ "svg",
22941
+ {
22942
+ width: size.width,
22943
+ height: size.height,
22944
+ overflow: "visible",
22945
+ role: "img",
22946
+ "aria-label": `${categories.length}\uAC1C \uD56D\uBAA9, ${series.length}\uAC1C \uACC4\uC5F4 ${stacked ? "\uB204\uC801 " : ""}\uAEBE\uC740\uC120 \uADF8\uB798\uD504`,
22947
+ children: [
22948
+ /* @__PURE__ */ jsx(
22949
+ "text",
22950
+ {
22951
+ ref: valueLabelRef,
22952
+ "aria-hidden": true,
22953
+ x: 0,
22954
+ y: 0,
22955
+ className: LINE_DATA_LABEL_TYPO_HOVER,
22956
+ dominantBaseline: "auto",
22957
+ style: { visibility: "hidden" },
22958
+ children: "0"
22959
+ }
22960
+ ),
22961
+ /* @__PURE__ */ jsx(ChartGrid, { scale, direction: "vertical", width: size.width, height: size.height }),
22962
+ hoveredCategory != null && /* @__PURE__ */ jsx(
22963
+ "rect",
22964
+ {
22965
+ "aria-hidden": true,
22966
+ x: cellOf(hoveredCategory).left,
22967
+ y: 0,
22968
+ width: cellOf(hoveredCategory).right - cellOf(hoveredCategory).left,
22969
+ height: size.height,
22970
+ style: {
22971
+ fill: "var(--cmp-chart-barContainer-hover-bg)",
22972
+ fillOpacity: "var(--cmp-chart-barContainer-hover-opacity)"
22973
+ }
22974
+ }
22975
+ ),
22976
+ areaGradients,
22977
+ areaMask,
22978
+ area && /* @__PURE__ */ jsx("g", { mask: `url(#${areaMaskId})`, children: areas }),
22979
+ lines,
22980
+ pointHalos,
22981
+ pointDots,
22982
+ valueLabels
22983
+ ]
22984
+ }
22985
+ ),
22986
+ showTooltip && hoveredCategory != null && tooltipAnchor != null && tooltipItems.length > 0 && /* @__PURE__ */ jsx(
22987
+ ChartTooltip,
22988
+ {
22989
+ title: categories[hoveredCategory] ?? "",
22990
+ items: tooltipItems,
22991
+ anchor: tooltipAnchor,
22992
+ bounds: size,
22993
+ gapX: { after: -4, before: -4 },
22994
+ footer: tooltipFooter?.({
22995
+ categoryIndex: hoveredCategory,
22996
+ name: categories[hoveredCategory] ?? "",
22997
+ points: rows.flatMap((row, seriesIndex) => {
22998
+ const point = row[hoveredCategory];
22999
+ if (point == null) return [];
23000
+ return [{ seriesIndex, categoryIndex: hoveredCategory, value: point.own }];
23001
+ })
23002
+ })
23003
+ }
23004
+ )
23005
+ ]
23006
+ }
23007
+ )
23008
+ }
23009
+ )
23010
+ );
22464
23011
  return /* @__PURE__ */ jsx(
22465
23012
  ChartFrame,
22466
23013
  {
@@ -22471,7 +23018,7 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22471
23018
  legendPosition,
22472
23019
  className,
22473
23020
  style,
22474
- children: /* @__PURE__ */ jsxs(
23021
+ children: /* @__PURE__ */ jsx(
22475
23022
  "div",
22476
23023
  {
22477
23024
  className: "grid",
@@ -22480,119 +23027,27 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22480
23027
  // 눈금은 왼쪽 한 줄뿐이다 — 오른쪽에도 적으면 서로 다른 단위처럼 읽힌다.
22481
23028
  gridTemplateColumns: "max-content 1fr"
22482
23029
  },
22483
- children: [
22484
- /* @__PURE__ */ jsx("div", { style: { height: plotHeight, ...inset }, children: /* @__PURE__ */ jsx(
22485
- ChartAxisLabels,
22486
- {
22487
- scale,
22488
- direction: "vertical",
22489
- format: formatValue,
22490
- sampleRef: axisSampleRef
22491
- }
22492
- ) }),
22493
- /* @__PURE__ */ jsxs(
22494
- "div",
23030
+ children: rotateCategoryLabels ? (
23031
+ // 기울인 이름은 눈금 열 아래까지 뻗으므로 스크롤 상자가 눈금 열까지 감싼다.
23032
+ /* @__PURE__ */ jsx(
23033
+ ChartRotatedAxisLayout,
22495
23034
  {
22496
- className: "min-w-0 overflow-x-auto",
22497
- style: { marginInline: -POINT_HOVER_EXTENT, paddingInline: POINT_HOVER_EXTENT },
22498
- children: [
22499
- /* @__PURE__ */ jsx(
22500
- "div",
22501
- {
22502
- className: "relative min-w-0",
22503
- style: { height: plotHeight, minWidth: minPlotWidth || void 0, ...inset },
22504
- children: /* @__PURE__ */ jsxs(
22505
- "div",
22506
- {
22507
- ref: plotRef,
22508
- className: "relative h-full w-full",
22509
- onMouseMove: handleMove,
22510
- onMouseLeave: () => setHoveredCategory(null),
22511
- children: [
22512
- size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxs(
22513
- "svg",
22514
- {
22515
- width: size.width,
22516
- height: size.height,
22517
- overflow: "visible",
22518
- role: "img",
22519
- "aria-label": `${categories.length}\uAC1C \uD56D\uBAA9, ${series.length}\uAC1C \uACC4\uC5F4 ${stacked ? "\uB204\uC801 " : ""}\uAEBE\uC740\uC120 \uADF8\uB798\uD504`,
22520
- children: [
22521
- /* @__PURE__ */ jsx(
22522
- "text",
22523
- {
22524
- ref: valueLabelRef,
22525
- "aria-hidden": true,
22526
- x: 0,
22527
- y: 0,
22528
- className: LINE_DATA_LABEL_TYPO_HOVER,
22529
- dominantBaseline: "auto",
22530
- style: { visibility: "hidden" },
22531
- children: "0"
22532
- }
22533
- ),
22534
- /* @__PURE__ */ jsx(
22535
- ChartGrid,
22536
- {
22537
- scale,
22538
- direction: "vertical",
22539
- width: size.width,
22540
- height: size.height
22541
- }
22542
- ),
22543
- hoveredCategory != null && /* @__PURE__ */ jsx(
22544
- "rect",
22545
- {
22546
- "aria-hidden": true,
22547
- x: cellOf(hoveredCategory).left,
22548
- y: 0,
22549
- width: cellOf(hoveredCategory).right - cellOf(hoveredCategory).left,
22550
- height: size.height,
22551
- style: {
22552
- fill: "var(--cmp-chart-barContainer-hover-bg)",
22553
- fillOpacity: "var(--cmp-chart-barContainer-hover-opacity)"
22554
- }
22555
- }
22556
- ),
22557
- areaGradients,
22558
- areaMask,
22559
- area && /* @__PURE__ */ jsx("g", { mask: `url(#${areaMaskId})`, children: areas }),
22560
- lines,
22561
- pointHalos,
22562
- pointDots,
22563
- valueLabels
22564
- ]
22565
- }
22566
- ),
22567
- showTooltip && hoveredCategory != null && tooltipAnchor != null && tooltipItems.length > 0 && /* @__PURE__ */ jsx(
22568
- ChartTooltip,
22569
- {
22570
- title: categories[hoveredCategory] ?? "",
22571
- items: tooltipItems,
22572
- anchor: tooltipAnchor,
22573
- bounds: size,
22574
- gapX: { after: -4, before: -4 },
22575
- footer: tooltipFooter?.({
22576
- categoryIndex: hoveredCategory,
22577
- name: categories[hoveredCategory] ?? "",
22578
- points: rows.flatMap((row, seriesIndex) => {
22579
- const point = row[hoveredCategory];
22580
- if (point == null) return [];
22581
- return [{ seriesIndex, categoryIndex: hoveredCategory, value: point.own }];
22582
- })
22583
- })
22584
- }
22585
- )
22586
- ]
22587
- }
22588
- )
22589
- }
22590
- ),
22591
- /* @__PURE__ */ jsx("div", { ref: axisTextRef, children: categoryLabels })
22592
- ]
23035
+ axis: axisLabels,
23036
+ axisStyle: { height: plotHeight, ...inset },
23037
+ plot,
23038
+ minPlotWidth,
23039
+ labels: categories,
23040
+ xAt,
23041
+ labelsRef: axisTextRef
22593
23042
  }
22594
23043
  )
22595
- ]
23044
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
23045
+ /* @__PURE__ */ jsx("div", { style: { height: plotHeight, ...inset }, children: axisLabels }),
23046
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 overflow-x-auto", children: [
23047
+ plot,
23048
+ /* @__PURE__ */ jsx("div", { ref: axisTextRef, children: categoryLabels })
23049
+ ] })
23050
+ ] })
22596
23051
  }
22597
23052
  )
22598
23053
  }