sellmate-design-system-react 9.0.0-beta.91 → 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.
Files changed (41) hide show
  1. package/AGENTS.md +27 -2
  2. package/dist/components/SBarChart/README.md +1 -0
  3. package/dist/components/SBarChart/SBarChart.d.ts +7 -0
  4. package/dist/components/SBarcodeInput/README.md +2 -0
  5. package/dist/components/SCheckbox/README.md +2 -0
  6. package/dist/components/SGhostButton/README.md +2 -0
  7. package/dist/components/SGnbSystem/README.md +6 -2
  8. package/dist/components/SGnbSystem/SGnbSystem.d.ts +3 -3
  9. package/dist/components/SGnbSystem/gnbSystem.config.d.ts +5 -3
  10. package/dist/components/SIcon/README.md +2 -0
  11. package/dist/components/SInput/README.md +2 -0
  12. package/dist/components/SLineChart/README.md +1 -0
  13. package/dist/components/SLineChart/SLineChart.d.ts +5 -0
  14. package/dist/components/SLineChart/lineChart.config.d.ts +1 -2
  15. package/dist/components/SMeatballButton/README.md +2 -0
  16. package/dist/components/SNumberInput/README.md +2 -0
  17. package/dist/components/SSelect/README.md +8 -3
  18. package/dist/components/SSelect/SSelect.d.ts +17 -3
  19. package/dist/components/SSelect/select.config.d.ts +34 -4
  20. package/dist/components/STable/README.md +1 -1
  21. package/dist/components/STable/STable.d.ts +3 -0
  22. package/dist/components/STag/README.md +8 -0
  23. package/dist/components/STimePicker/README.md +1 -1
  24. package/dist/components/STimePicker/STimePicker.d.ts +3 -7
  25. package/dist/components/STimePicker/TimeListbox.d.ts +21 -0
  26. package/dist/components/STimeRangePicker/README.md +2 -2
  27. package/dist/components/STimeRangePicker/STimeRangePicker.d.ts +5 -1
  28. package/dist/components/STreeTable/STreeTable.examples.d.ts +7 -0
  29. package/dist/index.cjs +1339 -1128
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +1339 -1128
  32. package/dist/index.js.map +1 -1
  33. package/dist/lib/chart/cartesian.d.ts +1 -3
  34. package/dist/lib/chart/category-labels.d.ts +37 -0
  35. package/dist/lib/chart/use-element-size.d.ts +4 -1
  36. package/dist/lib/mode-listbox.d.ts +10 -0
  37. package/dist/lib/tree-row.d.ts +74 -0
  38. package/dist/llms-full.txt +55 -11
  39. package/dist/llms.txt +27 -2
  40. package/dist/styles.css +12 -37
  41. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -7711,32 +7711,47 @@ var TREE_LABEL_TYPOGRAPHY = {
7711
7711
  rowHeight: "h-[32px]"
7712
7712
  }
7713
7713
  };
7714
- function collectIds(nodes) {
7715
- return nodes.flatMap((node) => [node.id, ...collectIds(node.children ?? [])]);
7716
- }
7717
- function collectSelectableIds(node) {
7718
- if (node.disabled) return [];
7719
- return [node.id, ...(node.children ?? []).flatMap(collectSelectableIds)];
7714
+ var TREE_ROW_CLASS = "group/tree-item box-border flex w-full items-center gap-(--cmp-tree-item-rowGap) overflow-hidden bg-(--cmp-tree-item-bg-default) px-(--cmp-tree-item-paddingX) text-left hover:bg-(--cmp-tree-item-bg-hover)";
7715
+ var TREE_ROW_BORDER_CLASS = "border-b border-(--cmp-tree-item-border-default)";
7716
+ var TREE_ROW_ACTIVE_CLASS = "bg-(--cmp-tree-item-bg-hover)";
7717
+ var TREE_CONTENT_CLASS = "box-border flex min-w-0 flex-1 items-center gap-(--cmp-tree-item-contentGap)";
7718
+ function treeLabelClass(size, {
7719
+ emphasized,
7720
+ disabled,
7721
+ selectable
7722
+ }) {
7723
+ const typography = TREE_LABEL_TYPOGRAPHY[size];
7724
+ return cn(
7725
+ "block min-w-0 flex-1 truncate py-(--cmp-tree-item-paddingY) text-(--cmp-tree-item-text-default)",
7726
+ emphasized ? typography.selected : typography.default,
7727
+ disabled && "opacity-60",
7728
+ disabled ? "cursor-not-allowed" : selectable ? "cursor-pointer" : void 0
7729
+ );
7720
7730
  }
7721
- function toSelectedSet(value) {
7722
- return new Set(value ?? []);
7731
+ function isRowControlClick(event) {
7732
+ const control = event.target.closest("[data-tree-control]");
7733
+ return control != null && event.currentTarget.contains(control);
7723
7734
  }
7724
- var TREE_ALL_NODE = { id: "__all__", label: "\uC804\uCCB4" };
7725
- function getSelectionState(node, selectedSet) {
7726
- const selectableChildren = (node.children ?? []).filter(
7727
- (child) => collectSelectableIds(child).length > 0
7728
- );
7729
- if (selectableChildren.length === 0) {
7730
- return { selected: selectedSet.has(node.id), indeterminate: false };
7731
- }
7732
- const childStates = selectableChildren.map(
7733
- (child) => getSelectionState(child, selectedSet)
7735
+ function TreeCaret({
7736
+ expanded,
7737
+ onToggle
7738
+ }) {
7739
+ return /* @__PURE__ */ jsxRuntime.jsx(
7740
+ SGhostButton,
7741
+ {
7742
+ icon: expanded ? "caretDown" : "caretRight",
7743
+ size: "xxs",
7744
+ ariaLabel: expanded ? "collapse tree item" : "expand tree item",
7745
+ ariaExpanded: expanded,
7746
+ onClick: (event) => {
7747
+ event.stopPropagation();
7748
+ onToggle(event);
7749
+ }
7750
+ }
7734
7751
  );
7735
- const selectedCount = childStates.filter((state2) => state2.selected).length;
7736
- return {
7737
- selected: selectedCount === childStates.length,
7738
- indeterminate: childStates.some((state2) => state2.indeterminate) || selectedCount > 0 && selectedCount < childStates.length
7739
- };
7752
+ }
7753
+ function TreeCaretSpacer() {
7754
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-(--cmp-ghostButton-xxs-size) shrink-0", "aria-hidden": "true" });
7740
7755
  }
7741
7756
  function TreeConnector({
7742
7757
  guideline,
@@ -7787,6 +7802,83 @@ function TreeConnector({
7787
7802
  ] })
7788
7803
  ] });
7789
7804
  }
7805
+ function TreeAllRow({
7806
+ label = "\uC804\uCCB4",
7807
+ selected,
7808
+ indeterminate = false,
7809
+ size = "sm",
7810
+ border = true,
7811
+ active = false,
7812
+ tabIndex,
7813
+ onKeyDown,
7814
+ onToggle,
7815
+ className,
7816
+ ...rest
7817
+ }) {
7818
+ return /* @__PURE__ */ jsxRuntime.jsx(
7819
+ "div",
7820
+ {
7821
+ role: "treeitem",
7822
+ "aria-level": 1,
7823
+ "aria-selected": selected,
7824
+ tabIndex,
7825
+ onKeyDown,
7826
+ onClick: (event) => {
7827
+ if (!isRowControlClick(event)) onToggle(event);
7828
+ },
7829
+ className: cn(
7830
+ TREE_ROW_CLASS,
7831
+ "cursor-pointer",
7832
+ TREE_LABEL_TYPOGRAPHY[size].rowHeight,
7833
+ border && TREE_ROW_BORDER_CLASS,
7834
+ active && TREE_ROW_ACTIVE_CLASS,
7835
+ className
7836
+ ),
7837
+ ...rest,
7838
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: TREE_CONTENT_CLASS, children: [
7839
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-tree-control": true, className: "flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: indeterminate ? null : selected, onValueChange: () => onToggle() }) }),
7840
+ /* @__PURE__ */ jsxRuntime.jsx(
7841
+ "span",
7842
+ {
7843
+ className: treeLabelClass(size, {
7844
+ emphasized: selected || indeterminate,
7845
+ disabled: false,
7846
+ selectable: false
7847
+ }),
7848
+ children: label
7849
+ }
7850
+ )
7851
+ ] })
7852
+ }
7853
+ );
7854
+ }
7855
+ function collectIds(nodes) {
7856
+ return nodes.flatMap((node) => [node.id, ...collectIds(node.children ?? [])]);
7857
+ }
7858
+ function collectSelectableIds(node) {
7859
+ if (node.disabled) return [];
7860
+ return [node.id, ...(node.children ?? []).flatMap(collectSelectableIds)];
7861
+ }
7862
+ function toSelectedSet(value) {
7863
+ return new Set(value ?? []);
7864
+ }
7865
+ var TREE_ALL_NODE = { id: "__all__", label: "\uC804\uCCB4" };
7866
+ function getSelectionState(node, selectedSet) {
7867
+ const selectableChildren = (node.children ?? []).filter(
7868
+ (child) => collectSelectableIds(child).length > 0
7869
+ );
7870
+ if (selectableChildren.length === 0) {
7871
+ return { selected: selectedSet.has(node.id), indeterminate: false };
7872
+ }
7873
+ const childStates = selectableChildren.map(
7874
+ (child) => getSelectionState(child, selectedSet)
7875
+ );
7876
+ const selectedCount = childStates.filter((state2) => state2.selected).length;
7877
+ return {
7878
+ selected: selectedCount === childStates.length,
7879
+ indeterminate: childStates.some((state2) => state2.indeterminate) || selectedCount > 0 && selectedCount < childStates.length
7880
+ };
7881
+ }
7790
7882
  function TreeBranch({ expanded, children }) {
7791
7883
  const branchRef = useInertRef(!expanded);
7792
7884
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -7802,10 +7894,6 @@ function TreeBranch({ expanded, children }) {
7802
7894
  }
7803
7895
  );
7804
7896
  }
7805
- function isRowControlClick(event) {
7806
- const control = event.target.closest("[data-tree-control]");
7807
- return control != null && event.currentTarget.contains(control);
7808
- }
7809
7897
  function STreeAllItem({
7810
7898
  selected,
7811
7899
  indeterminate,
@@ -7816,39 +7904,17 @@ function STreeAllItem({
7816
7904
  onKeyDown,
7817
7905
  onSelect
7818
7906
  }) {
7819
- const checkboxValue = indeterminate ? null : selected;
7820
- const typography = TREE_LABEL_TYPOGRAPHY[size];
7821
- const labelTypo = selected || indeterminate ? typography.selected : typography.default;
7822
7907
  return /* @__PURE__ */ jsxRuntime.jsx(
7823
- "div",
7908
+ TreeAllRow,
7824
7909
  {
7825
- role: "treeitem",
7826
- "aria-level": 1,
7827
- "aria-selected": selected,
7828
7910
  "data-tree-row": rowId,
7829
7911
  tabIndex,
7830
7912
  onKeyDown,
7831
- onClick: (event) => {
7832
- if (!isRowControlClick(event)) onSelect(event);
7833
- },
7834
- className: cn(
7835
- "group/tree-item box-border flex w-full cursor-pointer items-center overflow-hidden bg-(--cmp-tree-item-bg-default) px-(--cmp-tree-item-paddingX) text-left hover:bg-(--cmp-tree-item-bg-hover)",
7836
- typography.rowHeight,
7837
- border && "border-b border-(--cmp-tree-item-border-default)"
7838
- ),
7839
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "box-border flex min-w-0 flex-1 items-center gap-(--cmp-tree-item-contentGap)", children: [
7840
- /* @__PURE__ */ jsxRuntime.jsx("span", { "data-tree-control": true, className: "flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: checkboxValue, onValueChange: () => onSelect() }) }),
7841
- /* @__PURE__ */ jsxRuntime.jsx(
7842
- "span",
7843
- {
7844
- className: cn(
7845
- "block min-w-0 flex-1 truncate py-(--cmp-tree-item-paddingY) text-(--cmp-tree-item-text-default)",
7846
- labelTypo
7847
- ),
7848
- children: "\uC804\uCCB4"
7849
- }
7850
- )
7851
- ] })
7913
+ selected,
7914
+ indeterminate,
7915
+ border,
7916
+ size,
7917
+ onToggle: onSelect
7852
7918
  }
7853
7919
  );
7854
7920
  }
@@ -7889,8 +7955,6 @@ var STreeItemImpl = /* @__PURE__ */ react.forwardRef(function STreeItem({
7889
7955
  const leadingNode = typeof leading === "function" ? leading(renderState) : leading;
7890
7956
  const trailingNode = typeof trailing === "function" ? trailing(renderState) : trailing;
7891
7957
  const checkboxValue = indeterminate ? null : selected;
7892
- const typography = TREE_LABEL_TYPOGRAPHY[size];
7893
- const labelTypo = selected || indeterminate ? typography.selected : typography.default;
7894
7958
  const handleSelect = (event) => {
7895
7959
  if (node.disabled) return;
7896
7960
  onTreeSelect?.(node, event);
@@ -7916,10 +7980,9 @@ var STreeItemImpl = /* @__PURE__ */ react.forwardRef(function STreeItem({
7916
7980
  onKeyDown,
7917
7981
  onClick: handleRowClick,
7918
7982
  className: cn(
7919
- "group/tree-item box-border flex w-full items-center gap-(--cmp-tree-item-rowGap) overflow-hidden bg-(--cmp-tree-item-bg-default) px-(--cmp-tree-item-paddingX) text-left",
7920
- typography.rowHeight,
7921
- border && "border-b border-(--cmp-tree-item-border-default)",
7922
- "hover:bg-(--cmp-tree-item-bg-hover)",
7983
+ TREE_ROW_CLASS,
7984
+ TREE_LABEL_TYPOGRAPHY[size].rowHeight,
7985
+ border && TREE_ROW_BORDER_CLASS,
7923
7986
  hasChildren && "cursor-pointer",
7924
7987
  className
7925
7988
  ),
@@ -7936,25 +7999,8 @@ var STreeItemImpl = /* @__PURE__ */ react.forwardRef(function STreeItem({
7936
7999
  ancestorLast
7937
8000
  }
7938
8001
  ),
7939
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "box-border flex min-w-0 flex-1 items-center gap-(--cmp-tree-item-contentGap)", children: [
7940
- hasChildren ? /* @__PURE__ */ jsxRuntime.jsx(
7941
- SGhostButton,
7942
- {
7943
- icon: expanded ? "caretDown" : "caretRight",
7944
- size: "xxs",
7945
- ariaLabel: expanded ? "collapse tree item" : "expand tree item",
7946
- ariaExpanded: expanded,
7947
- onClick: (event) => {
7948
- event.stopPropagation();
7949
- onToggle?.(node, event);
7950
- }
7951
- }
7952
- ) : (
7953
- // 자식이 없는 줄은 펼침 버튼 자리를 빈 칸으로 남겨 라벨의 시작점을 맞춘다.
7954
- // 크기는 자리를 대신하는 그 버튼(`SGhostButton size="xxs"`)의 토큰을 그대로 쓴다 —
7955
- // 버튼 크기가 바뀌면 빈 칸도 같이 따라가야 줄이 어긋나지 않는다.
7956
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-(--cmp-ghostButton-xxs-size) shrink-0", "aria-hidden": "true" })
7957
- ),
8002
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: TREE_CONTENT_CLASS, children: [
8003
+ hasChildren ? /* @__PURE__ */ jsxRuntime.jsx(TreeCaret, { expanded, onToggle: (event) => onToggle?.(node, event) }) : /* @__PURE__ */ jsxRuntime.jsx(TreeCaretSpacer, {}),
7958
8004
  leadingNode,
7959
8005
  selectable && /* @__PURE__ */ jsxRuntime.jsx(
7960
8006
  "span",
@@ -7976,12 +8022,11 @@ var STreeItemImpl = /* @__PURE__ */ react.forwardRef(function STreeItem({
7976
8022
  "span",
7977
8023
  {
7978
8024
  "data-tree-label": true,
7979
- className: cn(
7980
- "block min-w-0 flex-1 truncate py-(--cmp-tree-item-paddingY) text-(--cmp-tree-item-text-default)",
7981
- labelTypo,
7982
- node.disabled && "opacity-60",
7983
- node.disabled ? "cursor-not-allowed" : selectable ? "cursor-pointer" : void 0
7984
- ),
8025
+ className: treeLabelClass(size, {
8026
+ emphasized: selected || indeterminate,
8027
+ disabled: Boolean(node.disabled),
8028
+ selectable
8029
+ }),
7985
8030
  children: node.label
7986
8031
  }
7987
8032
  ),
@@ -13423,9 +13468,20 @@ var SystemButton = /* @__PURE__ */ react.forwardRef(function SystemButton2({
13423
13468
  }
13424
13469
  ),
13425
13470
  label && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-body-sm-semibold min-w-0 truncate", children: label }),
13426
- count != null && count > 0 && // 개수 오른쪽은 버튼 여백(alertButton.paddingX)만으로는 좁다 — 시안이 4px 을 더 준다.
13471
+ count != null && count > 0 && // 개수는 채운 배지로 단다 — 배경·글자색·치수는 xs deep 태그 토큰이 쥔다(시안).
13472
+ // 배지 색은 고정이다: 흰 면은 파랑, 어두운 면(dark·darker)은 노랑 바탕에 검은 글자.
13473
+ // 배지 오른쪽은 버튼 여백(alertButton.paddingX)만으로는 좁다 — 시안이 4px 을 더 준다.
13427
13474
  // 대응 토큰이 없어 디자인 값을 그대로 쓴다.
13428
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-body-md-bold me-[4px] shrink-0", style: { color: accentColor }, children: count.toLocaleString() })
13475
+ /* @__PURE__ */ jsxRuntime.jsx(
13476
+ STag,
13477
+ {
13478
+ size: "xs",
13479
+ deep: true,
13480
+ color: color === "light" ? "blue" : "yellow",
13481
+ label: count.toLocaleString(),
13482
+ className: "me-[4px] shrink-0"
13483
+ }
13484
+ )
13429
13485
  ]
13430
13486
  }
13431
13487
  );
@@ -14304,7 +14360,7 @@ var SStepper = /* @__PURE__ */ react.forwardRef(function SStepper2({
14304
14360
  items.findIndex((item4, index) => getItemValue(item4, index) === value)
14305
14361
  );
14306
14362
  const lastCompletedIndex = items.reduce(
14307
- (lastIndex, item4, index) => item4.completed === true ? index : lastIndex,
14363
+ (lastIndex2, item4, index) => item4.completed === true ? index : lastIndex2,
14308
14364
  -1
14309
14365
  );
14310
14366
  const resolvedClickableIndex = lastCompletedIndex + 1;
@@ -15762,13 +15818,13 @@ var SSearchInput = /* @__PURE__ */ react.forwardRef(function SSearchInput2({
15762
15818
  });
15763
15819
 
15764
15820
  // src/components/SSelect/select.config.ts
15765
- function flattenOptions(options, depth = 1) {
15821
+ function flattenOptions(options, depth = 1, inheritDisabled = true) {
15766
15822
  const out = [];
15767
15823
  for (const o of options) {
15768
15824
  const hasChildren = Array.isArray(o.children) && o.children.length > 0;
15769
15825
  if (hasChildren) {
15770
- const childFlat = flattenOptions(o.children, depth + 1);
15771
- const allChildrenDisabled = childFlat.every((c) => c.disabled);
15826
+ const childFlat = flattenOptions(o.children, depth + 1, inheritDisabled);
15827
+ const allChildrenDisabled = inheritDisabled && childFlat.every((c) => c.disabled);
15772
15828
  out.push({
15773
15829
  value: String(o.value),
15774
15830
  label: o.label,
@@ -15802,12 +15858,71 @@ function collectGroupLeafValues(flat, groupIndex) {
15802
15858
  }
15803
15859
  return values;
15804
15860
  }
15861
+ function visibleTreeRows(rows, isExpanded) {
15862
+ const out = [];
15863
+ const sourceIndex = [];
15864
+ let hiddenBelow = null;
15865
+ rows.forEach((row, i) => {
15866
+ if (hiddenBelow != null && row.depth > hiddenBelow) return;
15867
+ hiddenBelow = null;
15868
+ out.push(row);
15869
+ sourceIndex.push(i);
15870
+ if (row.isGroup && !isExpanded(row)) hiddenBelow = row.depth;
15871
+ });
15872
+ return { rows: out, sourceIndex };
15873
+ }
15874
+ function treeLineInfo(rows) {
15875
+ const isLast = new Array(rows.length);
15876
+ const seen = [];
15877
+ for (let i = rows.length - 1; i >= 0; i--) {
15878
+ const depth = rows[i].depth;
15879
+ isLast[i] = !seen[depth];
15880
+ seen[depth] = true;
15881
+ seen.length = depth + 1;
15882
+ }
15883
+ const ancestors = [];
15884
+ const parents = [];
15885
+ const parentOf = new Array(rows.length);
15886
+ const posInSet = new Array(rows.length);
15887
+ const siblingCount = /* @__PURE__ */ new Map();
15888
+ rows.forEach((row, i) => {
15889
+ const parent = row.depth > 1 ? parents[row.depth - 2] ?? -1 : -1;
15890
+ parentOf[i] = parent;
15891
+ const count = (siblingCount.get(parent) ?? 0) + 1;
15892
+ siblingCount.set(parent, count);
15893
+ posInSet[i] = count;
15894
+ parents.length = row.depth - 1;
15895
+ parents[row.depth - 1] = i;
15896
+ });
15897
+ return rows.map((row, i) => {
15898
+ ancestors.length = row.depth - 1;
15899
+ const ancestorLast = ancestors.slice();
15900
+ ancestors[row.depth - 1] = isLast[i];
15901
+ return {
15902
+ isLast: isLast[i],
15903
+ ancestorLast,
15904
+ posInSet: posInSet[i],
15905
+ setSize: siblingCount.get(parentOf[i]) ?? 1
15906
+ };
15907
+ });
15908
+ }
15909
+ function ancestorGroupValues(flat, selected) {
15910
+ const out = /* @__PURE__ */ new Set();
15911
+ const stack = [];
15912
+ for (const row of flat) {
15913
+ while (stack.length > 0 && stack[stack.length - 1].depth >= row.depth) stack.pop();
15914
+ if (selected.has(row.value)) for (const group of stack) out.add(group.value);
15915
+ if (row.isGroup) stack.push(row);
15916
+ }
15917
+ return out;
15918
+ }
15805
15919
  var isHtmlLabel = (label) => /<[a-z][\s\S]*>/i.test(label);
15806
15920
  var extractText = (html) => html.replace(/<[^>]*>/g, "");
15807
15921
  var OPTION_ROW_HEIGHT = 28;
15808
15922
  var DEFAULT_VIRTUAL_BUFFER = 10;
15809
15923
  var DEFAULT_REACH_END_THRESHOLD = 10;
15810
15924
  var DEFAULT_SEARCH_DEBOUNCE = 300;
15925
+ var NO_SEARCH_COLLAPSED = /* @__PURE__ */ new Set();
15811
15926
  var SEARCH_THRESHOLD = 11;
15812
15927
  function isListSized(el) {
15813
15928
  return el.clientHeight > 0 && getComputedStyle(el).maxHeight !== "none";
@@ -15862,8 +15977,9 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15862
15977
  className,
15863
15978
  style
15864
15979
  }, ref) {
15865
- const multi = type === "multi" || type === "multi_depth";
15980
+ const multi = type === "multi" || type === "multi_depth" || type === "multi_tree";
15866
15981
  const isDepth = type === "default_depth" || type === "multi_depth";
15982
+ const isTree = type === "tree" || type === "multi_tree";
15867
15983
  const pagingEnabled = onReachEnd != null;
15868
15984
  const partialOptions = pagingEnabled || serverSearch;
15869
15985
  const portalContainer = usePortalContainer();
@@ -15873,6 +15989,8 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15873
15989
  const [open, setOpen] = react.useState(false);
15874
15990
  const [pendingOpen, setPendingOpen] = react.useState(false);
15875
15991
  const [focusedIndex, setFocusedIndex] = react.useState(-1);
15992
+ const [expanded, setExpanded] = react.useState(() => /* @__PURE__ */ new Set());
15993
+ const [searchCollapsed, setSearchCollapsed] = react.useState(NO_SEARCH_COLLAPSED);
15876
15994
  const baseId = react.useId();
15877
15995
  const listboxId = `${baseId}-listbox`;
15878
15996
  const activeOptionId = `${baseId}-active-option`;
@@ -15896,10 +16014,11 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15896
16014
  const alignSelectedTopRef = react.useRef(false);
15897
16015
  const selectedAnchorRef = react.useRef(-1);
15898
16016
  const rangeAnchorRef = react.useRef(null);
15899
- const flat = react.useMemo(() => flattenOptions(options), [options]);
16017
+ const groupIsValue = type === "tree";
16018
+ const flat = react.useMemo(() => flattenOptions(options, 1, !groupIsValue), [groupIsValue, options]);
15900
16019
  const selectable = react.useMemo(() => selectableOptions(flat), [flat]);
15901
16020
  const hasLeaf = react.useMemo(() => flat.some((o) => !o.isGroup), [flat]);
15902
- const allOptionsDisabled = hasLeaf && selectable.length === 0;
16021
+ const allOptionsDisabled = groupIsValue ? flat.length > 0 && flat.every((o) => o.disabled) : hasLeaf && selectable.length === 0;
15903
16022
  const effectiveDisabled = disabled || allOptionsDisabled;
15904
16023
  const searchVisible = showSearch && (partialOptions || selectable.length >= SEARCH_THRESHOLD);
15905
16024
  const labelByValue = react.useMemo(() => new Map(flat.map((o) => [o.value, o.label])), [flat]);
@@ -15942,6 +16061,10 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15942
16061
  if (next) {
15943
16062
  dismissedByPointerRef.current = false;
15944
16063
  alignSelectedTopRef.current = true;
16064
+ if (isTree) {
16065
+ setExpanded(ancestorGroupValues(flat, new Set(selectedRef.current)));
16066
+ setSearchCollapsed(NO_SEARCH_COLLAPSED);
16067
+ }
15945
16068
  } else {
15946
16069
  setPendingOpen(false);
15947
16070
  setSearch("");
@@ -15984,7 +16107,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15984
16107
  },
15985
16108
  resetValidation: () => setRuleError("")
15986
16109
  });
15987
- const filtered = react.useMemo(() => {
16110
+ const searched = react.useMemo(() => {
15988
16111
  if (serverSearch || !searchVisible || search.trim() === "") return flat;
15989
16112
  const q = search.trim().toLowerCase();
15990
16113
  return filterDepthRows(
@@ -15992,23 +16115,53 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15992
16115
  (o) => (isHtmlLabel(o.label) ? extractText(o.label) : o.label).toLowerCase().includes(q)
15993
16116
  );
15994
16117
  }, [flat, search, searchVisible, serverSearch]);
16118
+ const treeSearching = isTree && search.trim() !== "";
16119
+ const isGroupExpanded = react.useCallback(
16120
+ (groupValue) => treeSearching ? !searchCollapsed.has(groupValue) : expanded.has(groupValue),
16121
+ [expanded, searchCollapsed, treeSearching]
16122
+ );
16123
+ const treeRows = react.useMemo(
16124
+ () => isTree ? visibleTreeRows(searched, (row) => isGroupExpanded(row.value)) : null,
16125
+ [isGroupExpanded, isTree, searched]
16126
+ );
16127
+ const filtered = treeRows?.rows ?? searched;
16128
+ const treeLines = react.useMemo(() => treeRows ? treeLineInfo(treeRows.rows) : [], [treeRows]);
15995
16129
  const allValues = selectable.map((o) => o.value);
15996
16130
  const allSelected = !partialOptions && allValues.length > 0 && allValues.every((v) => selectedSet.has(v));
15997
16131
  const selectAllVisible = multi && showSelectAll && !partialOptions && allValues.length > 0;
15998
- const hasItems = filtered.some((o) => !o.isGroup);
16132
+ const hasItems = isTree ? filtered.length > 0 : filtered.some((o) => !o.isGroup);
15999
16133
  const groupLeafValues = react.useMemo(() => {
16000
16134
  const map = /* @__PURE__ */ new Map();
16001
16135
  filtered.forEach((o, i) => {
16002
- if (o.isGroup) map.set(i, collectGroupLeafValues(filtered, i));
16136
+ if (!o.isGroup) return;
16137
+ map.set(
16138
+ i,
16139
+ treeRows ? collectGroupLeafValues(searched, treeRows.sourceIndex[i]) : collectGroupLeafValues(filtered, i)
16140
+ );
16003
16141
  });
16004
16142
  return map;
16005
- }, [filtered]);
16143
+ }, [filtered, searched, treeRows]);
16006
16144
  const navigableItems = react.useMemo(() => {
16007
16145
  const items = [];
16008
16146
  if (selectAllVisible) {
16009
16147
  items.push({ key: "__select_all__", isSelectAll: true });
16010
16148
  }
16011
16149
  filtered.forEach((o, i) => {
16150
+ if (o.isGroup && isTree) {
16151
+ if (multi) {
16152
+ const leafValues = groupLeafValues.get(i) ?? [];
16153
+ items.push({
16154
+ key: `__group_${i}__`,
16155
+ isGroup: true,
16156
+ index: i,
16157
+ leafValues,
16158
+ disabled: o.disabled || leafValues.length === 0
16159
+ });
16160
+ } else {
16161
+ items.push({ key: o.value, value: o.value, index: i, disabled: o.disabled });
16162
+ }
16163
+ return;
16164
+ }
16012
16165
  if (o.disabled) return;
16013
16166
  if (o.isGroup) {
16014
16167
  if (!multi) return;
@@ -16020,7 +16173,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16020
16173
  }
16021
16174
  });
16022
16175
  return items;
16023
- }, [filtered, groupLeafValues, multi, selectAllVisible]);
16176
+ }, [filtered, groupLeafValues, isTree, multi, selectAllVisible]);
16024
16177
  const selectedAnchorIndex = react.useMemo(() => {
16025
16178
  if (selectedSet.size === 0) return -1;
16026
16179
  return navigableItems.findIndex((item4) => item4.value != null && selectedSet.has(item4.value));
@@ -16031,7 +16184,10 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16031
16184
  const rowCount = filtered.length;
16032
16185
  const metrics = metricsRef.current;
16033
16186
  const lastSearchRef = react.useRef(search);
16034
- if (lastSearchRef.current !== search) {
16187
+ const lastExpansionRef = react.useRef({ expanded, searchCollapsed });
16188
+ const expansionChanged = lastExpansionRef.current.expanded !== expanded || lastExpansionRef.current.searchCollapsed !== searchCollapsed;
16189
+ lastExpansionRef.current = { expanded, searchCollapsed };
16190
+ if (lastSearchRef.current !== search || isTree && expansionChanged) {
16035
16191
  lastSearchRef.current = search;
16036
16192
  resetRowMetrics(metrics, rowCount);
16037
16193
  measuredKeyRef.current = "";
@@ -16171,7 +16327,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16171
16327
  );
16172
16328
  const fullGroupByLeaf = react.useMemo(() => {
16173
16329
  const map = /* @__PURE__ */ new Map();
16174
- if (!showChips || type !== "multi_depth") return map;
16330
+ if (!showChips || type !== "multi_depth" && type !== "multi_tree") return map;
16175
16331
  flat.forEach((o, i) => {
16176
16332
  if (!o.isGroup) return;
16177
16333
  const leafValues = collectGroupLeafValues(flat, i);
@@ -16249,6 +16405,21 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16249
16405
  rangeAnchorRef.current = null;
16250
16406
  emit3(allSelected ? [] : allValues);
16251
16407
  };
16408
+ const handleSearchChange = (next) => {
16409
+ setSearch(next);
16410
+ setSearchCollapsed(NO_SEARCH_COLLAPSED);
16411
+ };
16412
+ const handleToggleExpand = (groupValue) => {
16413
+ rangeAnchorRef.current = null;
16414
+ const toggle = (prev) => {
16415
+ const next = new Set(prev);
16416
+ if (next.has(groupValue)) next.delete(groupValue);
16417
+ else next.add(groupValue);
16418
+ return next;
16419
+ };
16420
+ if (treeSearching) setSearchCollapsed(toggle);
16421
+ else setExpanded(toggle);
16422
+ };
16252
16423
  const handleToggleGroup = (leafValues) => {
16253
16424
  if (leafValues.length === 0) return;
16254
16425
  rangeAnchorRef.current = null;
@@ -16321,6 +16492,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16321
16492
  const selectFocusedItem = (extend = false) => {
16322
16493
  if (focusedIndex < 0 || focusedIndex >= navigableItems.length) return;
16323
16494
  const focused = navigableItems[focusedIndex];
16495
+ if (focused.disabled) return;
16324
16496
  if (focused.isSelectAll) {
16325
16497
  handleSelectAll();
16326
16498
  return;
@@ -16349,6 +16521,41 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16349
16521
  moveFocusedIndex("prev");
16350
16522
  return;
16351
16523
  }
16524
+ const input = event.target instanceof HTMLInputElement ? event.target : null;
16525
+ const caretAtEdge = input == null || input.selectionStart === input.selectionEnd && input.selectionStart === (event.key === "ArrowRight" ? input.value.length : 0);
16526
+ if (isTree && (event.key === "ArrowRight" || event.key === "ArrowLeft") && caretAtEdge) {
16527
+ const rowIndex = navigableItems[focusedIndex]?.index;
16528
+ const row = rowIndex != null ? filtered[rowIndex] : void 0;
16529
+ if (rowIndex == null || row == null) return;
16530
+ event.preventDefault();
16531
+ const rowExpanded = row.isGroup && isGroupExpanded(row.value);
16532
+ if (event.key === "ArrowRight") {
16533
+ if (!row.isGroup) return;
16534
+ if (!rowExpanded) {
16535
+ handleToggleExpand(row.value);
16536
+ return;
16537
+ }
16538
+ for (let i = rowIndex + 1; i < filtered.length && filtered[i].depth > row.depth; i++) {
16539
+ const child = navigableItems.findIndex((item4) => item4.index === i);
16540
+ if (child >= 0) {
16541
+ setFocusedIndex(child);
16542
+ break;
16543
+ }
16544
+ }
16545
+ return;
16546
+ }
16547
+ if (rowExpanded) {
16548
+ handleToggleExpand(row.value);
16549
+ return;
16550
+ }
16551
+ for (let i = rowIndex - 1; i >= 0; i--) {
16552
+ if (filtered[i].depth >= row.depth) continue;
16553
+ const parent = navigableItems.findIndex((item4) => item4.index === i);
16554
+ if (parent >= 0) setFocusedIndex(parent);
16555
+ return;
16556
+ }
16557
+ return;
16558
+ }
16352
16559
  if (event.key === "Enter") {
16353
16560
  event.preventDefault();
16354
16561
  selectFocusedItem(event.shiftKey);
@@ -16393,7 +16600,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16393
16600
  type: "button",
16394
16601
  disabled: effectiveDisabled,
16395
16602
  "aria-labelledby": label ? `${fieldLabelId(triggerId)} ${triggerId}` : void 0,
16396
- "aria-haspopup": "listbox",
16603
+ "aria-haspopup": isTree ? "tree" : "listbox",
16397
16604
  "aria-expanded": open,
16398
16605
  "aria-controls": open ? listboxId : void 0,
16399
16606
  "aria-activedescendant": open && focusedIndex >= 0 ? activeOptionId : void 0,
@@ -16530,7 +16737,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16530
16737
  {
16531
16738
  ref: searchInputRef,
16532
16739
  value: search,
16533
- onValueChange: setSearch,
16740
+ onValueChange: handleSearchChange,
16534
16741
  onKeyDown: handleKeyDown,
16535
16742
  placeholder: "\uAC80\uC0C9",
16536
16743
  clearable: true,
@@ -16549,8 +16756,12 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16549
16756
  indexOffset: windowStart,
16550
16757
  topSpacer: win.top,
16551
16758
  bottomSpacer: win.bottom,
16552
- setSize: isDepth ? void 0 : rowCount,
16759
+ setSize: isDepth || isTree ? void 0 : rowCount,
16553
16760
  multi,
16761
+ tree: isTree,
16762
+ isExpanded: isGroupExpanded,
16763
+ onToggleExpand: handleToggleExpand,
16764
+ treeLines,
16554
16765
  selected,
16555
16766
  groupLeafValues,
16556
16767
  focusedValue: navigableItems[focusedIndex]?.key,
@@ -16604,6 +16815,10 @@ function ItemList({
16604
16815
  bottomSpacer,
16605
16816
  setSize,
16606
16817
  multi,
16818
+ tree,
16819
+ isExpanded,
16820
+ onToggleExpand,
16821
+ treeLines,
16607
16822
  selected,
16608
16823
  groupLeafValues,
16609
16824
  focusedValue,
@@ -16617,14 +16832,34 @@ function ItemList({
16617
16832
  onHoverValue
16618
16833
  }) {
16619
16834
  const selectedSet = react.useMemo(() => new Set(selected), [selected]);
16835
+ const checkboxShiftRef = react.useRef(false);
16620
16836
  const itemBase = cn(
16621
16837
  "group/select-item flex w-full cursor-pointer select-none items-center gap-[8px] border-none bg-transparent text-left text-[12px] leading-[20px] outline-none",
16622
16838
  "text-[color:var(--cmp-listBoxItem-content-default)]",
16623
16839
  "disabled:cursor-not-allowed disabled:text-[color:var(--cmp-listBoxItem-content-disabled)]"
16624
16840
  );
16625
16841
  const activeClass = "hover:bg-[var(--cmp-listBoxItem-bg-hover)] hover:text-[color:var(--cmp-listBoxItem-content-hover)] focus-visible:bg-[var(--cmp-listBoxItem-bg-hover)] focus-visible:text-[color:var(--cmp-listBoxItem-content-hover)]";
16626
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { id: listboxId, role: "listbox", "aria-multiselectable": multi || void 0, children: [
16627
- showSelectAll && /* @__PURE__ */ jsxRuntime.jsxs(
16842
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { id: listboxId, role: tree ? "tree" : "listbox", "aria-multiselectable": multi || void 0, children: [
16843
+ showSelectAll && (tree ? (
16844
+ // 트리의 전체선택 줄은 STree 의 "전체" 줄 그대로다 (lib/tree-row).
16845
+ /* @__PURE__ */ jsxRuntime.jsx(
16846
+ TreeAllRow,
16847
+ {
16848
+ "data-select-all-row": true,
16849
+ id: focusedValue === "__select_all__" ? activeOptionId : void 0,
16850
+ "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
16851
+ "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
16852
+ tabIndex: -1,
16853
+ label: selectAllLabel,
16854
+ selected: selectAllSelected,
16855
+ active: focusedValue === "__select_all__",
16856
+ className: "select-none",
16857
+ onMouseEnter: () => onHoverValue("__select_all__"),
16858
+ onMouseLeave: () => onHoverValue(null),
16859
+ onToggle: onSelectAll
16860
+ }
16861
+ )
16862
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
16628
16863
  "button",
16629
16864
  {
16630
16865
  type: "button",
@@ -16645,12 +16880,109 @@ function ItemList({
16645
16880
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate", children: selectAllLabel })
16646
16881
  ]
16647
16882
  }
16648
- ),
16883
+ )),
16649
16884
  topSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: { height: topSpacer } }),
16650
16885
  items.map((o, windowIndex) => {
16651
16886
  const idx = windowIndex + indexOffset;
16652
16887
  const padLeft = o.depth === 1 ? 12 : o.depth === 2 ? 20 : 28;
16653
16888
  const rowKey = `${o.value}-${idx}`;
16889
+ if (tree) {
16890
+ const togglesLeaves = multi && o.isGroup;
16891
+ const leafValues = togglesLeaves ? groupLeafValues.get(idx) ?? [] : [];
16892
+ const total = leafValues.length;
16893
+ const selectedCount = leafValues.reduce((n, v) => selectedSet.has(v) ? n + 1 : n, 0);
16894
+ const isSelected2 = selectedSet.has(o.value);
16895
+ const checkState = togglesLeaves ? total === 0 || selectedCount === 0 ? false : selectedCount === total ? true : null : isSelected2;
16896
+ const rowDisabled = o.disabled || togglesLeaves && total === 0;
16897
+ const focusKey = togglesLeaves ? `__group_${idx}__` : o.value;
16898
+ const focused = focusedValue === focusKey;
16899
+ const expanded = o.isGroup && isExpanded(o.value);
16900
+ const line = treeLines[idx];
16901
+ const toggle = () => {
16902
+ if (rowDisabled) return;
16903
+ if (togglesLeaves) onToggleGroup(leafValues);
16904
+ else onSelect(o.value, idx, checkboxShiftRef.current);
16905
+ };
16906
+ return (
16907
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events -- 키보드는 트리거(또는 검색 입력)가 aria-activedescendant 로 맡는다. 줄로 포커스를 옮기지 않는다
16908
+ /* @__PURE__ */ jsxRuntime.jsxs(
16909
+ "div",
16910
+ {
16911
+ tabIndex: -1,
16912
+ "data-select-row": true,
16913
+ "data-index": idx,
16914
+ id: focused ? activeOptionId : void 0,
16915
+ role: "treeitem",
16916
+ "aria-level": o.depth,
16917
+ "aria-posinset": line?.posInSet,
16918
+ "aria-setsize": line?.setSize,
16919
+ "aria-expanded": o.isGroup ? expanded : void 0,
16920
+ "aria-selected": checkState === true,
16921
+ "aria-disabled": rowDisabled || void 0,
16922
+ "data-select-focused": focused ? "true" : void 0,
16923
+ "data-select-active": focused ? "true" : void 0,
16924
+ className: cn(
16925
+ TREE_ROW_CLASS,
16926
+ TREE_LABEL_TYPOGRAPHY.sm.rowHeight,
16927
+ TREE_ROW_BORDER_CLASS,
16928
+ "select-none",
16929
+ rowDisabled ? "cursor-not-allowed" : "cursor-pointer",
16930
+ focused && TREE_ROW_ACTIVE_CLASS
16931
+ ),
16932
+ onMouseEnter: () => onHoverValue(focusKey),
16933
+ onMouseLeave: () => onHoverValue(null),
16934
+ onClick: (event) => {
16935
+ if (isRowControlClick(event) || rowDisabled) return;
16936
+ if (togglesLeaves) onToggleGroup(leafValues);
16937
+ else onSelect(o.value, idx, event.shiftKey);
16938
+ },
16939
+ children: [
16940
+ /* @__PURE__ */ jsxRuntime.jsx(
16941
+ TreeConnector,
16942
+ {
16943
+ guideline: true,
16944
+ size: "sm",
16945
+ depth: o.depth,
16946
+ hasChildren: o.isGroup,
16947
+ isLast: line?.isLast ?? true,
16948
+ ancestorLast: line?.ancestorLast ?? []
16949
+ }
16950
+ ),
16951
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: TREE_CONTENT_CLASS, children: [
16952
+ o.isGroup ? (
16953
+ // 비활성 그룹도 접고 편다 — 그룹이 비활성이어도 아래에 고를 수 있는 자식이 있을 수 있다.
16954
+ /* @__PURE__ */ jsxRuntime.jsx(TreeCaret, { expanded, onToggle: () => onToggleExpand(o.value) })
16955
+ ) : /* @__PURE__ */ jsxRuntime.jsx(TreeCaretSpacer, {}),
16956
+ multi && /* @__PURE__ */ jsxRuntime.jsx(
16957
+ "span",
16958
+ {
16959
+ "data-tree-control": true,
16960
+ className: "flex shrink-0",
16961
+ onPointerDownCapture: (event) => {
16962
+ checkboxShiftRef.current = event.shiftKey;
16963
+ },
16964
+ children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: checkState, disabled: rowDisabled, onValueChange: toggle })
16965
+ }
16966
+ ),
16967
+ /* @__PURE__ */ jsxRuntime.jsx(
16968
+ OptionLabel,
16969
+ {
16970
+ label: o.label,
16971
+ className: treeLabelClass("sm", {
16972
+ // 고른 줄은 굵게 — multi_tree 는 일부만 고른 그룹도 굵다 (STree 와 같다)
16973
+ emphasized: checkState !== false,
16974
+ disabled: rowDisabled,
16975
+ selectable: true
16976
+ })
16977
+ }
16978
+ )
16979
+ ] })
16980
+ ]
16981
+ },
16982
+ rowKey
16983
+ )
16984
+ );
16985
+ }
16654
16986
  if (o.isGroup) {
16655
16987
  const isDepth1 = o.depth === 1;
16656
16988
  const isFirstRow = idx === 0 && !showSelectAll && !searchVisible;
@@ -16691,13 +17023,7 @@ function ItemList({
16691
17023
  onClick: () => onToggleGroup(leafValues),
16692
17024
  children: [
16693
17025
  /* @__PURE__ */ jsxRuntime.jsx(CheckboxBox, { state: groupState, disabled: groupDisabled, focused: groupFocused }),
16694
- isHtmlLabel(o.label) ? /* @__PURE__ */ jsxRuntime.jsx(
16695
- "span",
16696
- {
16697
- className: "min-w-0 truncate",
16698
- dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(o.label) }
16699
- }
16700
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: o.label }),
17026
+ /* @__PURE__ */ jsxRuntime.jsx(OptionLabel, { label: o.label, className: "min-w-0 truncate" }),
16701
17027
  total > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
16702
17028
  "span",
16703
17029
  {
@@ -16774,13 +17100,7 @@ function ItemList({
16774
17100
  focused: focusedValue === o.value
16775
17101
  }
16776
17102
  ),
16777
- isHtmlLabel(o.label) ? /* @__PURE__ */ jsxRuntime.jsx(
16778
- "span",
16779
- {
16780
- className: "flex-1 truncate",
16781
- dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(o.label) }
16782
- }
16783
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate", children: o.label })
17103
+ /* @__PURE__ */ jsxRuntime.jsx(OptionLabel, { label: o.label, className: "flex-1 truncate" })
16784
17104
  ]
16785
17105
  },
16786
17106
  rowKey
@@ -16789,8 +17109,11 @@ function ItemList({
16789
17109
  bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: { height: bottomSpacer } })
16790
17110
  ] });
16791
17111
  }
17112
+ function OptionLabel({ label, className }) {
17113
+ return isHtmlLabel(label) ? /* @__PURE__ */ jsxRuntime.jsx("span", { className, dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className, children: label });
17114
+ }
16792
17115
  function TriggerLabel({ label }) {
16793
- return isHtmlLabel(label) ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: label });
17116
+ return /* @__PURE__ */ jsxRuntime.jsx(OptionLabel, { label, className: "truncate" });
16794
17117
  }
16795
17118
  function TriggerRemoveButton({ ariaLabel, onRemove }) {
16796
17119
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -17045,8 +17368,8 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17045
17368
  const [rowCountOverride, setRowCountOverride] = react.useState(null);
17046
17369
  const rowCount = rowCountOverride ?? rows.length;
17047
17370
  const scrollRef = react.useRef(null);
17048
- const headerScrollRef = react.useRef(null);
17049
- const [headerGutter, setHeaderGutter] = react.useState(0);
17371
+ const headerRef = react.useRef(null);
17372
+ const [headerHeight, setHeaderHeight] = react.useState(0);
17050
17373
  const [vsRange, setVsRange] = react.useState({ start: 0, end: 0 });
17051
17374
  const lastReachEndRef = react.useRef(-1);
17052
17375
  const cbRef = react.useRef({ onVirtualUpdate, onVirtualReachEnd, onScroll });
@@ -17088,17 +17411,13 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17088
17411
  );
17089
17412
  const handleScroll = react.useCallback(() => {
17090
17413
  const el = scrollRef.current;
17091
- if (!el) {
17092
- setHeaderGutter(0);
17093
- return;
17094
- }
17095
- const header = headerScrollRef.current;
17096
- if (header) header.scrollLeft = el.scrollLeft;
17097
- setHeaderGutter(el.offsetWidth - el.clientWidth);
17414
+ if (!el) return;
17415
+ const headerH = headerRef.current?.offsetHeight ?? 0;
17416
+ setHeaderHeight(headerH);
17098
17417
  setScrolledLeft(el.scrollLeft > 0);
17099
17418
  setScrolledRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
17100
17419
  if (virtualScroll) {
17101
- const { start, end } = calcRange(el.scrollTop, el.clientHeight);
17420
+ const { start, end } = calcRange(el.scrollTop, el.clientHeight - headerH);
17102
17421
  setVsRange((prev) => prev.start === start && prev.end === end ? prev : { start, end });
17103
17422
  }
17104
17423
  }, [virtualScroll, calcRange]);
@@ -17495,7 +17814,7 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17495
17814
  );
17496
17815
  };
17497
17816
  const bodyScrollClass = cn(
17498
- "min-h-0 flex-1 overflow-auto border-t border-solid border-[color:var(--cmp-table-border-color)]",
17817
+ "min-h-0 flex-1 overflow-auto",
17499
17818
  SCROLLBAR_CLASS,
17500
17819
  SCROLLBAR_TRACK_BG_CLASS,
17501
17820
  SCROLLBAR_TRACK_BORDER_CLASS
@@ -17512,23 +17831,221 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17512
17831
  selectable && /* @__PURE__ */ jsxRuntime.jsx("col", { style: { width: SELECTABLE_COLUMN_WIDTH } }),
17513
17832
  visibleCols.map((col, i) => /* @__PURE__ */ jsxRuntime.jsx("col", { style: col.autoWidth ? void 0 : { width: columnWidths[i] } }, col.name))
17514
17833
  ] });
17515
- return /* @__PURE__ */ jsxRuntime.jsxs(
17516
- "div",
17834
+ const headerTable = /* @__PURE__ */ jsxRuntime.jsxs(
17835
+ "table",
17517
17836
  {
17518
- className: cn("flex flex-col", className),
17519
- style: { width, ...style },
17520
- "data-table-id": resolvedTableId,
17837
+ className: "w-full table-fixed border-separate border-spacing-0 border-b border-solid border-[color:var(--cmp-table-border-color)] text-left",
17838
+ style: { fontSize: 12 },
17521
17839
  children: [
17522
- /* @__PURE__ */ jsxRuntime.jsxs(
17523
- "div",
17524
- {
17525
- className: cn(
17526
- // isolate: no-data/loading 오버레이(z-30·31)를 테이블 안에서 끝낸다. relative + z-auto
17527
- // 만으로는 스태킹 컨텍스트가 생기지 않아, 모달 안에 들어가면 30·31이 모달 Content
17528
- // 레벨까지 올라가 같은 컨텍스트의 floating portal 밴드(z-2)를 덮는다.
17529
- // 자손은 이미 overflow-hidden으로 클리핑되므로 테이블 내부 순서는 달라지지 않는다.
17530
- "bg-white relative isolate flex flex-col overflow-hidden",
17531
- bordered && "border border-solid border-[color:var(--cmp-table-border-color)]",
17840
+ colgroup,
17841
+ /* @__PURE__ */ jsxRuntime.jsxs("thead", { className: cn(isNoData && "opacity-40"), children: [
17842
+ headerGroupInfo && /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17843
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(
17844
+ "th",
17845
+ {
17846
+ rowSpan: 2,
17847
+ className: cn(
17848
+ "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17849
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17850
+ ),
17851
+ style: {
17852
+ width: SELECTABLE_COLUMN_WIDTH,
17853
+ minWidth: SELECTABLE_COLUMN_WIDTH,
17854
+ maxWidth: SELECTABLE_COLUMN_WIDTH
17855
+ },
17856
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
17857
+ SCheckbox,
17858
+ {
17859
+ value: headerCheck,
17860
+ disabled: !hasSelectableRow,
17861
+ onValueChange: toggleAll
17862
+ }
17863
+ ) })
17864
+ }
17865
+ ),
17866
+ headerGroupInfo.cells.map((group) => /* @__PURE__ */ jsxRuntime.jsxs(
17867
+ "th",
17868
+ {
17869
+ colSpan: group.colSpan,
17870
+ rowSpan: group.rowSpan,
17871
+ className: cn(
17872
+ "relative border-b border-r border-solid border-[color:var(--cmp-table-border-color)] bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[20px] text-[color:var(--sys-color-fg-primary)]",
17873
+ group.firstIndex + group.colSpan === visibleCols.length && "border-r-0",
17874
+ group.rowSpan === 2 && "border-b-0",
17875
+ group.className
17876
+ ),
17877
+ style: { textAlign: group.align ?? "left", ...group.style },
17878
+ children: [
17879
+ group.render ? group.render() : group.label,
17880
+ group.rowSpan === 2 && group.colSpan === 1 ? headerResizeHitArea(group.firstIndex, `group-resizer-${group.columnNames[0]}`) : null
17881
+ ]
17882
+ },
17883
+ group.columnNames.join("|")
17884
+ ))
17885
+ ] }),
17886
+ /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17887
+ selectable && !headerGroupInfo && /* @__PURE__ */ jsxRuntime.jsxs(
17888
+ "th",
17889
+ {
17890
+ className: cn(
17891
+ // 원본 .th--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
17892
+ // 하단 border는 헤더 표가 연속으로 그리므로 셀에는 두지 않는다.
17893
+ "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17894
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17895
+ ),
17896
+ style: {
17897
+ width: SELECTABLE_COLUMN_WIDTH,
17898
+ minWidth: SELECTABLE_COLUMN_WIDTH,
17899
+ maxWidth: SELECTABLE_COLUMN_WIDTH
17900
+ },
17901
+ children: [
17902
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
17903
+ SCheckbox,
17904
+ {
17905
+ value: headerCheck,
17906
+ disabled: !hasSelectableRow,
17907
+ onValueChange: toggleAll
17908
+ }
17909
+ ) }),
17910
+ /* @__PURE__ */ jsxRuntime.jsx(
17911
+ "span",
17912
+ {
17913
+ "aria-hidden": true,
17914
+ className: "pointer-events-none absolute right-0 top-1/2 h-[16px] w-px -translate-y-1/2 bg-[var(--cmp-table-header-resizingBar-color)]"
17915
+ }
17916
+ )
17917
+ ]
17918
+ }
17919
+ ),
17920
+ visibleCols.map((col, i) => {
17921
+ if (headerGroupInfo?.rowSpanNames.has(col.name)) return null;
17922
+ const isSortable = !!col.sortable && !col.renderHeader;
17923
+ const sortDir = isSortable && sort?.name === col.name ? sort.dir : void 0;
17924
+ return /* @__PURE__ */ jsxRuntime.jsxs(
17925
+ "th",
17926
+ {
17927
+ className: cn(
17928
+ // 하단 border는 헤더 표가 연속으로 그린다.
17929
+ // leading-[0]: th 의 스트럿(line-height 만큼의 빈 인라인 박스)을 없앤다.
17930
+ // 내용이 inline-flex 라 스트럿이 남아 있으면 라벨보다 큰 컨트롤(정렬 버튼 등)이
17931
+ // 들어올 때 줄 상자가 베이스라인 기준으로 늘어나 헤더 높이를 넘기고
17932
+ // 세로 가운데도 어긋난다. 글자 줄높이는 아래 th__content 가 다시 준다.
17933
+ headerGroupInfo && "border-r border-solid border-[color:var(--cmp-table-border-color)]",
17934
+ headerGroupInfo && i === visibleCols.length - 1 && "border-r-0",
17935
+ "relative bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[0] text-[color:var(--sys-color-fg-primary)]",
17936
+ stickyInfo(i).isSticky && "z-[6]",
17937
+ col.thClass
17938
+ ),
17939
+ style: {
17940
+ textAlign: col.align ?? "left",
17941
+ boxShadow: stickyShadowValue(i) ?? void 0,
17942
+ ...cellStyle(i),
17943
+ ...col.thStyle
17944
+ },
17945
+ "aria-sort": isSortable ? sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none" : void 0,
17946
+ children: [
17947
+ /* @__PURE__ */ jsxRuntime.jsx(
17948
+ "span",
17949
+ {
17950
+ className: cn(
17951
+ // 원본 th__content: align에 따라 justify-content 적용 (th도 정렬 반영)
17952
+ // th 가 leading-[0] 이므로 글자 줄높이는 여기서 준다 (truncate 가
17953
+ // overflow-hidden 이라 줄높이가 0이면 라벨이 잘려 사라진다).
17954
+ "inline-flex w-full min-w-0 items-center gap-[4px] leading-[20px]",
17955
+ col.align === "center" && "justify-center",
17956
+ col.align === "right" && "justify-end"
17957
+ ),
17958
+ children: col.renderHeader ? col.renderHeader(col, i) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
17959
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: col.label }),
17960
+ col.helpText && col.helpText.length > 0 && // 라벨이 좁아져 truncate 되더라도 아이콘까지 함께 줄어들면 안 되므로
17961
+ // shrink-0 래퍼로 감싼다 (STooltip 의 className 은 본문 쪽으로 간다).
17962
+ // 아이콘·크기·색은 SKeyValueTable 헤더 도움말과 동일하게 맞춘다.
17963
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
17964
+ STooltip2,
17965
+ {
17966
+ trigger: "hover",
17967
+ icon: "helpOutline",
17968
+ iconSize: 12,
17969
+ color: "var(--sys-color-fg-success)",
17970
+ placement: "top",
17971
+ message: col.helpText
17972
+ }
17973
+ ) }),
17974
+ col.editable && /* @__PURE__ */ jsxRuntime.jsx(
17975
+ SIcon,
17976
+ {
17977
+ name: "pageEdit",
17978
+ size: 12,
17979
+ color: "var(--sys-color-link-accent)",
17980
+ label: "\uC218\uC815 \uAC00\uB2A5",
17981
+ className: "shrink-0"
17982
+ }
17983
+ ),
17984
+ col.navigable && /* @__PURE__ */ jsxRuntime.jsx(
17985
+ SIcon,
17986
+ {
17987
+ name: "pageMove",
17988
+ size: 12,
17989
+ color: "var(--sys-color-link-accent)",
17990
+ label: "\uC0C8 \uD654\uBA74\uC5D0\uC11C \uC5F4\uAE30",
17991
+ className: "shrink-0"
17992
+ }
17993
+ ),
17994
+ col.required && // 라벨이 truncate 돼도 필수 표시는 남아야 하므로 shrink-0.
17995
+ // 아이콘·크기·색은 SKeyValueTable 헤더 필수 표시와 동일하게 맞춘다.
17996
+ /* @__PURE__ */ jsxRuntime.jsx(
17997
+ SIcon,
17998
+ {
17999
+ name: "asterisk",
18000
+ size: 12,
18001
+ color: "var(--sys-color-fg-accent)",
18002
+ className: "shrink-0"
18003
+ }
18004
+ ),
18005
+ isSortable && // 미정렬은 updown 을 default(회색)로 — 정렬할 수 있다는 것 자체가
18006
+ // 헤더에 보여야 한다. 정렬 중이면 action 색으로 올려 활성임을 알린다.
18007
+ // 라벨이 truncate 돼도 버튼은 남도록 shrink-0.
18008
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
18009
+ SGhostButton,
18010
+ {
18011
+ size: "xxs",
18012
+ intent: sortDir ? "action" : "default",
18013
+ icon: sortDir === "asc" ? "arrowUp" : sortDir === "desc" ? "arrowDown" : "updown",
18014
+ ariaLabel: `${col.label} \uC815\uB82C`,
18015
+ onClick: () => toggleSort(col)
18016
+ }
18017
+ ) })
18018
+ ] })
18019
+ }
18020
+ ),
18021
+ headerGroupInfo ? headerResizeHitArea(i, `resizer-${col.name}`) : headerResizer(i, `resizer-${col.name}`)
18022
+ ]
18023
+ },
18024
+ col.name
18025
+ );
18026
+ })
18027
+ ] })
18028
+ ] })
18029
+ ]
18030
+ }
18031
+ );
18032
+ return /* @__PURE__ */ jsxRuntime.jsxs(
18033
+ "div",
18034
+ {
18035
+ className: cn("flex flex-col", className),
18036
+ style: { width, ...style },
18037
+ "data-table-id": resolvedTableId,
18038
+ children: [
18039
+ /* @__PURE__ */ jsxRuntime.jsxs(
18040
+ "div",
18041
+ {
18042
+ className: cn(
18043
+ // isolate: no-data/loading 오버레이(z-30·31)를 테이블 안에서 끝낸다. relative + z-auto
18044
+ // 만으로는 스태킹 컨텍스트가 생기지 않아, 모달 안에 들어가면 30·31이 모달 Content
18045
+ // 레벨까지 올라가 같은 컨텍스트의 floating portal 밴드(z-2)를 덮는다.
18046
+ // 자손은 이미 overflow-hidden으로 클리핑되므로 테이블 내부 순서는 달라지지 않는다.
18047
+ "bg-white relative isolate flex flex-col overflow-hidden",
18048
+ bordered && "border border-solid border-[color:var(--cmp-table-border-color)]",
17532
18049
  // height가 없으면 루트가 받은 높이를 끝까지 채운다. 호스트가 className으로
17533
18050
  // `flex-1 min-h-0` 등을 줘 루트 높이가 확정된 경우, 행이 적어도 테두리 박스가
17534
18051
  // 남은 영역을 그대로 차지한다. basis는 auto로 둬 높이가 확정되지 않은
@@ -17543,220 +18060,20 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17543
18060
  minHeight: isNoData ? "calc(var(--cmp-table-header-height) + 60px)" : void 0
17544
18061
  },
17545
18062
  children: [
17546
- /* @__PURE__ */ jsxRuntime.jsx(
17547
- "div",
17548
- {
17549
- ref: headerScrollRef,
17550
- className: "flex-none overflow-hidden",
17551
- style: {
17552
- borderRight: headerGutter ? `${headerGutter}px solid var(--cmp-table-header-bg)` : void 0
17553
- },
17554
- children: /* @__PURE__ */ jsxRuntime.jsxs(
17555
- "table",
17556
- {
17557
- className: "w-full table-fixed border-separate border-spacing-0 text-left",
17558
- style: { fontSize: 12 },
17559
- children: [
17560
- colgroup,
17561
- /* @__PURE__ */ jsxRuntime.jsxs("thead", { className: cn(isNoData && "opacity-40"), children: [
17562
- headerGroupInfo && /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17563
- selectable && /* @__PURE__ */ jsxRuntime.jsx(
17564
- "th",
17565
- {
17566
- rowSpan: 2,
17567
- className: cn(
17568
- "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17569
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17570
- ),
17571
- style: {
17572
- width: SELECTABLE_COLUMN_WIDTH,
17573
- minWidth: SELECTABLE_COLUMN_WIDTH,
17574
- maxWidth: SELECTABLE_COLUMN_WIDTH
17575
- },
17576
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
17577
- SCheckbox,
17578
- {
17579
- value: headerCheck,
17580
- disabled: !hasSelectableRow,
17581
- onValueChange: toggleAll
17582
- }
17583
- ) })
17584
- }
17585
- ),
17586
- headerGroupInfo.cells.map((group) => /* @__PURE__ */ jsxRuntime.jsxs(
17587
- "th",
17588
- {
17589
- colSpan: group.colSpan,
17590
- rowSpan: group.rowSpan,
17591
- className: cn(
17592
- "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)]",
17593
- group.firstIndex + group.colSpan === visibleCols.length && "border-r-0",
17594
- group.rowSpan === 2 && "border-b-0",
17595
- group.className
17596
- ),
17597
- style: { textAlign: group.align ?? "left", ...group.style },
17598
- children: [
17599
- group.render ? group.render() : group.label,
17600
- group.rowSpan === 2 && group.colSpan === 1 ? headerResizeHitArea(
17601
- group.firstIndex,
17602
- `group-resizer-${group.columnNames[0]}`
17603
- ) : null
17604
- ]
17605
- },
17606
- group.columnNames.join("|")
17607
- ))
17608
- ] }),
17609
- /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17610
- selectable && !headerGroupInfo && /* @__PURE__ */ jsxRuntime.jsxs(
17611
- "th",
17612
- {
17613
- className: cn(
17614
- // 원본 .th--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
17615
- // 하단 border는 헤더 래퍼가 연속으로 그리므로 셀에는 두지 않는다.
17616
- "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17617
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17618
- ),
17619
- style: {
17620
- width: SELECTABLE_COLUMN_WIDTH,
17621
- minWidth: SELECTABLE_COLUMN_WIDTH,
17622
- maxWidth: SELECTABLE_COLUMN_WIDTH
17623
- },
17624
- children: [
17625
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
17626
- SCheckbox,
17627
- {
17628
- value: headerCheck,
17629
- disabled: !hasSelectableRow,
17630
- onValueChange: toggleAll
17631
- }
17632
- ) }),
17633
- /* @__PURE__ */ jsxRuntime.jsx(
17634
- "span",
17635
- {
17636
- "aria-hidden": true,
17637
- 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)]"
17638
- }
17639
- )
17640
- ]
17641
- }
17642
- ),
17643
- visibleCols.map((col, i) => {
17644
- if (headerGroupInfo?.rowSpanNames.has(col.name)) return null;
17645
- const isSortable = !!col.sortable && !col.renderHeader;
17646
- const sortDir = isSortable && sort?.name === col.name ? sort.dir : void 0;
17647
- return /* @__PURE__ */ jsxRuntime.jsxs(
17648
- "th",
17649
- {
17650
- className: cn(
17651
- // 하단 border는 헤더 래퍼가 연속으로 그린다 (거터 영역까지 이어짐).
17652
- // leading-[0]: th 의 스트럿(line-height 만큼의 빈 인라인 박스)을 없앤다.
17653
- // 내용이 inline-flex 라 스트럿이 남아 있으면 라벨보다 큰 컨트롤(정렬 버튼 등)이
17654
- // 들어올 때 줄 상자가 베이스라인 기준으로 늘어나 헤더 높이를 넘기고
17655
- // 세로 가운데도 어긋난다. 글자 줄높이는 아래 th__content 가 다시 준다.
17656
- headerGroupInfo && "border-r border-solid border-[color:var(--cmp-table-border-color)]",
17657
- headerGroupInfo && i === visibleCols.length - 1 && "border-r-0",
17658
- "relative bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[0] text-[color:var(--sys-color-fg-primary)]",
17659
- stickyInfo(i).isSticky && "z-[6]",
17660
- col.thClass
17661
- ),
17662
- style: {
17663
- textAlign: col.align ?? "left",
17664
- boxShadow: stickyShadowValue(i) ?? void 0,
17665
- ...cellStyle(i),
17666
- ...col.thStyle
17667
- },
17668
- "aria-sort": isSortable ? sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none" : void 0,
17669
- children: [
17670
- /* @__PURE__ */ jsxRuntime.jsx(
17671
- "span",
17672
- {
17673
- className: cn(
17674
- // 원본 th__content: align에 따라 justify-content 적용 (th도 정렬 반영)
17675
- // th 가 leading-[0] 이므로 글자 줄높이는 여기서 준다 (truncate 가
17676
- // overflow-hidden 이라 줄높이가 0이면 라벨이 잘려 사라진다).
17677
- "inline-flex w-full min-w-0 items-center gap-[4px] leading-[20px]",
17678
- col.align === "center" && "justify-center",
17679
- col.align === "right" && "justify-end"
17680
- ),
17681
- children: col.renderHeader ? col.renderHeader(col, i) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
17682
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: col.label }),
17683
- col.helpText && col.helpText.length > 0 && // 라벨이 좁아져 truncate 되더라도 아이콘까지 함께 줄어들면 안 되므로
17684
- // shrink-0 래퍼로 감싼다 (STooltip 의 className 은 본문 쪽으로 간다).
17685
- // 아이콘·크기·색은 SKeyValueTable 헤더 도움말과 동일하게 맞춘다.
17686
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
17687
- STooltip2,
17688
- {
17689
- trigger: "hover",
17690
- icon: "helpOutline",
17691
- iconSize: 12,
17692
- color: "var(--sys-color-fg-success)",
17693
- placement: "top",
17694
- message: col.helpText
17695
- }
17696
- ) }),
17697
- col.editable && /* @__PURE__ */ jsxRuntime.jsx(
17698
- SIcon,
17699
- {
17700
- name: "pageEdit",
17701
- size: 12,
17702
- color: "var(--sys-color-link-accent)",
17703
- label: "\uC218\uC815 \uAC00\uB2A5",
17704
- className: "shrink-0"
17705
- }
17706
- ),
17707
- col.navigable && /* @__PURE__ */ jsxRuntime.jsx(
17708
- SIcon,
17709
- {
17710
- name: "pageMove",
17711
- size: 12,
17712
- color: "var(--sys-color-link-accent)",
17713
- label: "\uC0C8 \uD654\uBA74\uC5D0\uC11C \uC5F4\uAE30",
17714
- className: "shrink-0"
17715
- }
17716
- ),
17717
- col.required && // 라벨이 truncate 돼도 필수 표시는 남아야 하므로 shrink-0.
17718
- // 아이콘·크기·색은 SKeyValueTable 헤더 필수 표시와 동일하게 맞춘다.
17719
- /* @__PURE__ */ jsxRuntime.jsx(
17720
- SIcon,
17721
- {
17722
- name: "asterisk",
17723
- size: 12,
17724
- color: "var(--sys-color-fg-accent)",
17725
- className: "shrink-0"
17726
- }
17727
- ),
17728
- isSortable && // 미정렬은 updown 을 default(회색)로 — 정렬할 수 있다는 것 자체가
17729
- // 헤더에 보여야 한다. 정렬 중이면 action 색으로 올려 활성임을 알린다.
17730
- // 라벨이 truncate 돼도 버튼은 남도록 shrink-0.
17731
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
17732
- SGhostButton,
17733
- {
17734
- size: "xxs",
17735
- intent: sortDir ? "action" : "default",
17736
- icon: sortDir === "asc" ? "arrowUp" : sortDir === "desc" ? "arrowDown" : "updown",
17737
- ariaLabel: `${col.label} \uC815\uB82C`,
17738
- onClick: () => toggleSort(col)
17739
- }
17740
- ) })
17741
- ] })
17742
- }
17743
- ),
17744
- headerGroupInfo ? headerResizeHitArea(i, `resizer-${col.name}`) : headerResizer(i, `resizer-${col.name}`)
17745
- ]
17746
- },
17747
- col.name
17748
- );
17749
- })
17750
- ] })
17751
- ] })
17752
- ]
17753
- }
17754
- )
17755
- }
17756
- ),
17757
- showNoDataSlot ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(bodyScrollClass, "pointer-events-auto bg-white"), children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full min-h-[60px] w-full items-center justify-center", children: emptySlot }) }) : (
17758
- /* 바디: 세로 스크롤은 이 컨테이너에만 생긴다 (헤더 제외) */
17759
- /* @__PURE__ */ jsxRuntime.jsx(
18063
+ showNoDataSlot ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
18064
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-none overflow-hidden", children: headerTable }),
18065
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(bodyScrollClass, "pointer-events-auto bg-white"), children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full min-h-[60px] w-full items-center justify-center", children: emptySlot }) })
18066
+ ] }) : (
18067
+ /**
18068
+ * 헤더와 body 가 한 스크롤 컨테이너에 있다 — 헤더는 위에 붙어(sticky) 세로로는 서 있고
18069
+ * 가로로는 body 와 같은 스크롤로 움직인다. 헤더를 따로 두고 scroll 이벤트에서
18070
+ * scrollLeft 를 맞추면, 브라우저가 body 스크롤을 먼저 그리고 이벤트를 나중에 돌려
18071
+ * 헤더가 한 프레임 이상 늦게 따라온다.
18072
+ *
18073
+ * 행 좌표는 body 표 기준 그대로다 — `scrollTop` 이 y 이면 y 에 있는 행이 헤더 바로
18074
+ * 아래에 선다. 보이는 높이만 헤더 높이만큼 줄어든다(`headerHeight`).
18075
+ */
18076
+ /* @__PURE__ */ jsxRuntime.jsxs(
17760
18077
  "div",
17761
18078
  {
17762
18079
  ref: scrollRef,
@@ -17766,136 +18083,140 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17766
18083
  // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 스크롤 차단
17767
18084
  (loading2 || rowCount === 0) && "overflow-hidden"
17768
18085
  ),
17769
- children: /* @__PURE__ */ jsxRuntime.jsxs(
17770
- "table",
17771
- {
17772
- className: "w-full table-fixed border-separate border-spacing-0 text-left",
17773
- style: { fontSize: 12 },
17774
- children: [
17775
- colgroup,
17776
- /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
17777
- virtualScroll && topSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: topSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) }),
17778
- rowCount === 0 ? null : bodyRows.map((row, ri) => {
17779
- const rowIndex = rowOffset + ri;
17780
- const isSel = isViewSelected(row);
17781
- const dragSel = dragSelectOn && isSel;
17782
- const dragTop = dragSel && !isViewSelected(rows[rowIndex - 1]);
17783
- const dragBottom = dragSel && !isViewSelected(rows[rowIndex + 1]);
17784
- const rowLocked = (selectable || dragSelectable) && !canSelectRow(row);
17785
- const rowBg = "bg-white";
17786
- const rowHover = hoverable && !rowLocked;
17787
- return /* @__PURE__ */ jsxRuntime.jsxs(
17788
- "tr",
17789
- {
17790
- "data-row-index": rowIndex,
17791
- onClick: () => onRowClick?.(row),
17792
- onDoubleClick: onRowDoubleClick ? (event) => {
17793
- const target = event.target;
17794
- if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
17795
- onRowDoubleClick(row);
17796
- } : void 0,
17797
- onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
17798
- onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
17799
- onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
17800
- className: cn(
17801
- "group/table-row",
17802
- rowHover && "hover:bg-[var(--color-grey-05)]",
17803
- (onRowClick || onRowDoubleClick) && "cursor-pointer",
17804
- dragSelectOn && "select-none",
17805
- rowClassName?.(row)
17806
- ),
17807
- style: { height: virtualScroll ? effectiveRowHeight : rowH },
17808
- children: [
17809
- selectable && /* @__PURE__ */ jsxRuntime.jsx(
17810
- "td",
17811
- {
17812
- className: cn(
17813
- // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
17814
- "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
18086
+ style: { scrollPaddingTop: headerHeight },
18087
+ children: [
18088
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: headerRef, className: "sticky top-0 z-[8]", children: headerTable }),
18089
+ /* @__PURE__ */ jsxRuntime.jsxs(
18090
+ "table",
18091
+ {
18092
+ className: "w-full table-fixed border-separate border-spacing-0 text-left",
18093
+ style: { fontSize: 12 },
18094
+ children: [
18095
+ colgroup,
18096
+ /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
18097
+ virtualScroll && topSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: topSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) }),
18098
+ rowCount === 0 ? null : bodyRows.map((row, ri) => {
18099
+ const rowIndex = rowOffset + ri;
18100
+ const isSel = isViewSelected(row);
18101
+ const dragSel = dragSelectOn && isSel;
18102
+ const dragTop = dragSel && !isViewSelected(rows[rowIndex - 1]);
18103
+ const dragBottom = dragSel && !isViewSelected(rows[rowIndex + 1]);
18104
+ const rowLocked = (selectable || dragSelectable) && !canSelectRow(row);
18105
+ const rowBg = "bg-white";
18106
+ const rowHover = hoverable && !rowLocked;
18107
+ return /* @__PURE__ */ jsxRuntime.jsxs(
18108
+ "tr",
18109
+ {
18110
+ "data-row-index": rowIndex,
18111
+ onClick: () => onRowClick?.(row),
18112
+ onDoubleClick: onRowDoubleClick ? (event) => {
18113
+ const target = event.target;
18114
+ if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
18115
+ onRowDoubleClick(row);
18116
+ } : void 0,
18117
+ onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
18118
+ onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
18119
+ onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
18120
+ className: cn(
18121
+ "group/table-row",
18122
+ rowHover && "hover:bg-[var(--color-grey-05)]",
18123
+ (onRowClick || onRowDoubleClick) && "cursor-pointer",
18124
+ dragSelectOn && "select-none",
18125
+ rowClassName?.(row)
18126
+ ),
18127
+ style: { height: virtualScroll ? effectiveRowHeight : rowH },
18128
+ children: [
18129
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(
18130
+ "td",
18131
+ {
18132
+ className: cn(
18133
+ // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
18134
+ "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
18135
+ rowBg,
18136
+ rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
18137
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
18138
+ ),
18139
+ style: {
18140
+ width: SELECTABLE_COLUMN_WIDTH,
18141
+ minWidth: SELECTABLE_COLUMN_WIDTH,
18142
+ maxWidth: SELECTABLE_COLUMN_WIDTH,
18143
+ // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
18144
+ background: rowLocked ? LOCKED_BG : void 0
18145
+ },
18146
+ onClick: (e) => e.stopPropagation(),
18147
+ onDoubleClick: (e) => e.stopPropagation(),
18148
+ onClickCapture: (e) => {
18149
+ shiftPressedRef.current = e.shiftKey;
18150
+ if (e.shiftKey) window.getSelection()?.removeAllRanges();
18151
+ },
18152
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
18153
+ SCheckbox,
18154
+ {
18155
+ value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
18156
+ disabled: !canSelectRow(row),
18157
+ onValueChange: () => toggleRow(row, rowOffset + ri)
18158
+ }
18159
+ ) })
18160
+ }
18161
+ ),
18162
+ visibleCols.map((col, i) => {
18163
+ const tdClassName = cn(
18164
+ // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
18165
+ // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
18166
+ "overflow-hidden text-ellipsis whitespace-nowrap border-b border-solid border-[color:var(--cmp-table-border-color)] text-[12px] leading-[20px] text-[color:var(--sys-color-fg-primary)]",
17815
18167
  rowBg,
17816
18168
  rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
17817
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17818
- ),
17819
- style: {
17820
- width: SELECTABLE_COLUMN_WIDTH,
17821
- minWidth: SELECTABLE_COLUMN_WIDTH,
17822
- maxWidth: SELECTABLE_COLUMN_WIDTH,
17823
- // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
17824
- background: rowLocked ? LOCKED_BG : void 0
17825
- },
17826
- onClick: (e) => e.stopPropagation(),
17827
- onDoubleClick: (e) => e.stopPropagation(),
17828
- onClickCapture: (e) => {
17829
- shiftPressedRef.current = e.shiftKey;
17830
- if (e.shiftKey) window.getSelection()?.removeAllRanges();
17831
- },
17832
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
17833
- SCheckbox,
17834
- {
17835
- value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
17836
- disabled: !canSelectRow(row),
17837
- onValueChange: () => toggleRow(row, rowOffset + ri)
17838
- }
17839
- ) })
17840
- }
17841
- ),
17842
- visibleCols.map((col, i) => {
17843
- const tdClassName = cn(
17844
- // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
17845
- // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
17846
- "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)]",
17847
- rowBg,
17848
- rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
17849
- typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
17850
- );
17851
- const tdPadX = cellPadXOf(col);
17852
- const dragEdges = dragSel ? [
17853
- dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
17854
- dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
17855
- i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
17856
- i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
17857
- ].filter(Boolean) : [];
17858
- const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
17859
- const tdStyle = {
17860
- paddingLeft: tdPadX,
17861
- paddingRight: tdPadX,
17862
- paddingTop: cellPadY,
17863
- paddingBottom: cellPadY,
17864
- textAlign: col.align ?? "left",
17865
- // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
17866
- background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
17867
- // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
17868
- // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
17869
- color: rowLocked ? LOCKED_CONTENT : void 0,
17870
- boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
17871
- ...cellStyle(i)
17872
- };
17873
- if (col.renderCell) {
17874
- const cell = col.renderCell({
17875
- row,
17876
- value: cellValue(col, row),
17877
- column: col,
17878
- colIndex: i,
17879
- rowIndex: ri,
17880
- isSelected: isSel,
17881
- className: tdClassName,
17882
- style: tdStyle
17883
- });
17884
- if (cell == null || cell === false) return null;
17885
- return /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: cell }, col.name);
17886
- }
17887
- return /* @__PURE__ */ jsxRuntime.jsx("td", { className: tdClassName, style: tdStyle, children: col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row) }, col.name);
17888
- })
17889
- ]
17890
- },
17891
- row[rowKey] ?? ri
17892
- );
17893
- }),
17894
- virtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
17895
- ] })
17896
- ]
17897
- }
17898
- )
18169
+ typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
18170
+ );
18171
+ const tdPadX = cellPadXOf(col);
18172
+ const dragEdges = dragSel ? [
18173
+ dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
18174
+ dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
18175
+ i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
18176
+ i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
18177
+ ].filter(Boolean) : [];
18178
+ const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
18179
+ const tdStyle = {
18180
+ paddingLeft: tdPadX,
18181
+ paddingRight: tdPadX,
18182
+ paddingTop: cellPadY,
18183
+ paddingBottom: cellPadY,
18184
+ textAlign: col.align ?? "left",
18185
+ // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
18186
+ background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
18187
+ // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
18188
+ // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
18189
+ color: rowLocked ? LOCKED_CONTENT : void 0,
18190
+ boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
18191
+ ...cellStyle(i)
18192
+ };
18193
+ if (col.renderCell) {
18194
+ const cell = col.renderCell({
18195
+ row,
18196
+ value: cellValue(col, row),
18197
+ column: col,
18198
+ colIndex: i,
18199
+ rowIndex: ri,
18200
+ isSelected: isSel,
18201
+ className: tdClassName,
18202
+ style: tdStyle
18203
+ });
18204
+ if (cell == null || cell === false) return null;
18205
+ return /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: cell }, col.name);
18206
+ }
18207
+ return /* @__PURE__ */ jsxRuntime.jsx("td", { className: tdClassName, style: tdStyle, children: col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row) }, col.name);
18208
+ })
18209
+ ]
18210
+ },
18211
+ row[rowKey] ?? ri
18212
+ );
18213
+ }),
18214
+ virtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
18215
+ ] })
18216
+ ]
18217
+ }
18218
+ )
18219
+ ]
17899
18220
  }
17900
18221
  )
17901
18222
  ),
@@ -19403,13 +19724,21 @@ function SCalendar({
19403
19724
  }
19404
19725
  );
19405
19726
  }
19727
+
19728
+ // src/lib/mode-listbox.ts
19729
+ var MODE_LIST_HEIGHT = 240;
19730
+ var MODE_ITEM_HEIGHT = 28;
19731
+ var modeListItemClass = (selected, disabled) => cn(
19732
+ "flex w-full items-center justify-center gap-[var(--cmp-datepicker-mode-item-gap)] border-0 px-[var(--cmp-datepicker-mode-item-paddingX)] py-[var(--cmp-datepicker-mode-item-paddingY)] text-center font-base text-12 leading-[20px]",
19733
+ disabled && "cursor-not-allowed bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-disabled)]",
19734
+ !disabled && !selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-default)] hover:bg-[var(--cmp-datepicker-mode-item-bg-hover)] hover:text-[color:var(--cmp-datepicker-mode-item-content-hover)]",
19735
+ !disabled && selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-bold text-[color:var(--cmp-datepicker-mode-item-content-selected)]"
19736
+ );
19406
19737
  var MODE_NOUN = {
19407
19738
  date: "\uB0A0\uC9DC",
19408
19739
  month: "\uC6D4",
19409
19740
  year: "\uC5F0\uB3C4"
19410
19741
  };
19411
- var DATEPICKER_MODE_LIST_HEIGHT = 240;
19412
- var DATEPICKER_MODE_ITEM_HEIGHT = 28;
19413
19742
  var YEAR_LIST_EDGE_BUFFER = 24;
19414
19743
  var YEAR_LIST_EXPAND_SIZE = 80;
19415
19744
  var MONTHS = Array.from({ length: 12 }, (_, i) => i + 1);
@@ -19460,7 +19789,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19460
19789
  if (!list) return;
19461
19790
  const index = anchorYearRef.current - rangeStartRef.current;
19462
19791
  syncingScrollRef.current = true;
19463
- list.scrollTop = index * DATEPICKER_MODE_ITEM_HEIGHT;
19792
+ list.scrollTop = index * MODE_ITEM_HEIGHT;
19464
19793
  requestAnimationFrame(() => {
19465
19794
  syncingScrollRef.current = false;
19466
19795
  });
@@ -19469,7 +19798,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19469
19798
  const list = listRef.current;
19470
19799
  if (!list) return;
19471
19800
  if (syncingScrollRef.current) return;
19472
- if (list.scrollTop < DATEPICKER_MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19801
+ if (list.scrollTop < MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19473
19802
  const previousHeight = list.scrollHeight;
19474
19803
  setRange((current) => ({
19475
19804
  start: current.start - YEAR_LIST_EXPAND_SIZE,
@@ -19485,7 +19814,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19485
19814
  });
19486
19815
  return;
19487
19816
  }
19488
- if (list.scrollHeight - list.scrollTop - list.clientHeight < DATEPICKER_MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19817
+ if (list.scrollHeight - list.scrollTop - list.clientHeight < MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19489
19818
  setRange((current) => ({
19490
19819
  start: current.start,
19491
19820
  end: current.end + YEAR_LIST_EXPAND_SIZE
@@ -19500,8 +19829,8 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19500
19829
  "aria-label": "year",
19501
19830
  className: "h-full overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
19502
19831
  style: {
19503
- paddingTop: `calc((${DATEPICKER_MODE_LIST_HEIGHT}px - ${DATEPICKER_MODE_ITEM_HEIGHT}px) / 2)`,
19504
- paddingBottom: `calc((${DATEPICKER_MODE_LIST_HEIGHT}px - ${DATEPICKER_MODE_ITEM_HEIGHT}px) / 2)`
19832
+ paddingTop: `calc((${MODE_LIST_HEIGHT}px - ${MODE_ITEM_HEIGHT}px) / 2)`,
19833
+ paddingBottom: `calc((${MODE_LIST_HEIGHT}px - ${MODE_ITEM_HEIGHT}px) / 2)`
19505
19834
  },
19506
19835
  onScroll: handleScroll,
19507
19836
  children: years.map((year) => {
@@ -19514,12 +19843,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19514
19843
  role: "option",
19515
19844
  "aria-selected": selected,
19516
19845
  disabled,
19517
- className: cn(
19518
- "flex w-full items-center justify-center gap-[var(--cmp-datepicker-mode-item-gap)] border-0 px-[var(--cmp-datepicker-mode-item-paddingX)] py-[var(--cmp-datepicker-mode-item-paddingY)] text-center font-base text-12 leading-[20px]",
19519
- disabled && "cursor-not-allowed bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-disabled)]",
19520
- !disabled && !selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-default)] hover:bg-[var(--cmp-datepicker-mode-item-bg-hover)] hover:text-[color:var(--cmp-datepicker-mode-item-content-hover)]",
19521
- !disabled && selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-bold text-[color:var(--cmp-datepicker-mode-item-content-selected)]"
19522
- ),
19846
+ className: modeListItemClass(selected, disabled),
19523
19847
  onClick: () => !disabled && onCommit(year),
19524
19848
  children: year
19525
19849
  },
@@ -19575,7 +19899,7 @@ function MonthWheel({ year, value, selectable, onCommit }) {
19575
19899
  if (!list) return;
19576
19900
  list.scrollTop = Math.max(
19577
19901
  0,
19578
- (initialValueRef.current - 1) * DATEPICKER_MODE_ITEM_HEIGHT - (list.clientHeight - DATEPICKER_MODE_ITEM_HEIGHT) / 2
19902
+ (initialValueRef.current - 1) * MODE_ITEM_HEIGHT - (list.clientHeight - MODE_ITEM_HEIGHT) / 2
19579
19903
  );
19580
19904
  }, []);
19581
19905
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full w-full overflow-hidden bg-[var(--cmp-datepicker-mode-bg)]", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -19595,12 +19919,7 @@ function MonthWheel({ year, value, selectable, onCommit }) {
19595
19919
  role: "option",
19596
19920
  "aria-selected": selected,
19597
19921
  disabled,
19598
- className: cn(
19599
- "flex w-full items-center justify-center gap-[var(--cmp-datepicker-mode-item-gap)] border-0 px-[var(--cmp-datepicker-mode-item-paddingX)] py-[var(--cmp-datepicker-mode-item-paddingY)] text-center font-base text-12 leading-[20px]",
19600
- disabled && "cursor-not-allowed bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-disabled)]",
19601
- !disabled && !selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-default)] hover:bg-[var(--cmp-datepicker-mode-item-bg-hover)] hover:text-[color:var(--cmp-datepicker-mode-item-content-hover)]",
19602
- !disabled && selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-bold text-[color:var(--cmp-datepicker-mode-item-content-selected)]"
19603
- ),
19922
+ className: modeListItemClass(selected, disabled),
19604
19923
  onClick: () => !disabled && onCommit(month),
19605
19924
  children: [
19606
19925
  month,
@@ -21174,6 +21493,169 @@ function ChartAxisLabels({
21174
21493
  }
21175
21494
  );
21176
21495
  }
21496
+ function useElementSize(target) {
21497
+ const ref = react.useRef(null);
21498
+ const [size, setSize] = react.useState({ width: 0, height: 0 });
21499
+ react.useEffect(() => {
21500
+ const element = ref.current;
21501
+ if (!element) return;
21502
+ const update = () => {
21503
+ const { width, height } = element.getBoundingClientRect();
21504
+ setSize(
21505
+ (prev) => Math.abs(prev.width - width) < 0.5 && Math.abs(prev.height - height) < 0.5 ? prev : { width, height }
21506
+ );
21507
+ };
21508
+ update();
21509
+ if (typeof ResizeObserver === "undefined") return;
21510
+ const observer = new ResizeObserver(update);
21511
+ observer.observe(element);
21512
+ return () => observer.disconnect();
21513
+ }, [target]);
21514
+ return { ref, size };
21515
+ }
21516
+ function useSvgTextMetrics() {
21517
+ const ref = react.useRef(null);
21518
+ const [metrics, setMetrics] = react.useState({ top: 0, width: 0 });
21519
+ react.useEffect(() => {
21520
+ const element = ref.current;
21521
+ if (!element) return;
21522
+ let box3;
21523
+ try {
21524
+ box3 = element.getBBox();
21525
+ } catch {
21526
+ return;
21527
+ }
21528
+ setMetrics(
21529
+ (prev) => Math.abs(prev.top - box3.y) < 0.5 && Math.abs(prev.width - box3.width) < 0.5 ? prev : { top: box3.y, width: box3.width }
21530
+ );
21531
+ });
21532
+ return { ref, metrics };
21533
+ }
21534
+ var CATEGORY_LABEL_ROTATED_MAX = 120;
21535
+ var DIAGONAL = Math.SQRT1_2;
21536
+ function ChartRotatedAxisLayout({
21537
+ axis,
21538
+ axisStyle,
21539
+ plot,
21540
+ minPlotWidth,
21541
+ labels,
21542
+ xAt,
21543
+ labelsRef
21544
+ }) {
21545
+ const scrollRef = react.useRef(null);
21546
+ const { ref: axisRef, size: axisBox } = useElementSize();
21547
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: scrollRef, className: "min-w-0 overflow-x-auto", style: { gridColumn: "1 / -1" }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { width: `max(100%, ${axisBox.width + minPlotWidth}px)` }, children: [
21548
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex", children: [
21549
+ /* @__PURE__ */ jsxRuntime.jsx(
21550
+ "div",
21551
+ {
21552
+ ref: axisRef,
21553
+ className: "sticky left-0 z-1 shrink-0 bg-(--cmp-chart-bg)",
21554
+ style: { ...axisStyle, paddingRight: AXIS_LABEL_GAP },
21555
+ children: axis
21556
+ }
21557
+ ),
21558
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children: plot })
21559
+ ] }),
21560
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: labelsRef, children: /* @__PURE__ */ jsxRuntime.jsx(
21561
+ ChartRotatedCategoryLabels,
21562
+ {
21563
+ labels,
21564
+ xAt,
21565
+ gutter: axisBox.width,
21566
+ scrollRef
21567
+ }
21568
+ ) })
21569
+ ] }) });
21570
+ }
21571
+ function ChartRotatedCategoryLabels({
21572
+ labels,
21573
+ xAt,
21574
+ gutter,
21575
+ scrollRef
21576
+ }) {
21577
+ const sizerRef = react.useRef(null);
21578
+ const maskRef = react.useRef(null);
21579
+ const [box3, setBox] = react.useState({ line: 0, widest: 0 });
21580
+ react.useEffect(() => {
21581
+ const sizer = sizerRef.current;
21582
+ if (!sizer) return;
21583
+ const measure = () => {
21584
+ const { width, height: height2 } = sizer.getBoundingClientRect();
21585
+ const line = labels.length > 0 ? height2 / labels.length : 0;
21586
+ setBox(
21587
+ (prev) => Math.abs(prev.line - line) < 0.5 && Math.abs(prev.widest - width) < 0.5 ? prev : { line, widest: width }
21588
+ );
21589
+ };
21590
+ measure();
21591
+ if (typeof ResizeObserver === "undefined") return;
21592
+ const observer = new ResizeObserver(measure);
21593
+ observer.observe(sizer);
21594
+ return () => observer.disconnect();
21595
+ });
21596
+ react.useEffect(() => {
21597
+ const scroller = scrollRef.current;
21598
+ const mask = maskRef.current;
21599
+ if (!scroller || !mask) return;
21600
+ const sync2 = () => {
21601
+ mask.style.visibility = scroller.scrollLeft > 0 ? "visible" : "hidden";
21602
+ };
21603
+ sync2();
21604
+ scroller.addEventListener("scroll", sync2, { passive: true });
21605
+ return () => scroller.removeEventListener("scroll", sync2);
21606
+ }, [scrollRef]);
21607
+ const maxWidthAt = (index) => Math.max(
21608
+ 0,
21609
+ Math.min(CATEGORY_LABEL_ROTATED_MAX, (xAt(index) + gutter) / DIAGONAL - box3.line / 2)
21610
+ );
21611
+ const height = Math.ceil(box3.line / 2 + (box3.widest + box3.line / 2) * DIAGONAL);
21612
+ return (
21613
+ // overflow 는 clip 이어야 한다 — hidden 이면 이 행이 스크롤 상자로 잡혀 가림막의
21614
+ // sticky 가 스크롤을 따라가지 않는다.
21615
+ /* @__PURE__ */ jsxRuntime.jsxs(
21616
+ "div",
21617
+ {
21618
+ "aria-hidden": true,
21619
+ className: "relative overflow-clip typo-body-sm-medium text-(--cmp-chart-axis-color)",
21620
+ style: { height },
21621
+ children: [
21622
+ /* @__PURE__ */ jsxRuntime.jsx(
21623
+ "div",
21624
+ {
21625
+ ref: maskRef,
21626
+ className: "invisible sticky left-0 z-1 h-full bg-(--cmp-chart-bg)",
21627
+ style: { width: gutter }
21628
+ }
21629
+ ),
21630
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-y-0 right-0", style: { left: gutter }, children: labels.map((label, index) => /* @__PURE__ */ jsxRuntime.jsx(
21631
+ "span",
21632
+ {
21633
+ className: "absolute top-0 truncate",
21634
+ style: {
21635
+ left: xAt(index),
21636
+ maxWidth: maxWidthAt(index),
21637
+ // 글자 끝(오른쪽 가운데)을 눈금에 맞춘 뒤 그 점을 축으로 기울인다.
21638
+ transform: "translateX(-100%) rotate(-45deg)",
21639
+ transformOrigin: "right center"
21640
+ },
21641
+ children: label
21642
+ },
21643
+ `${label}-${index}`
21644
+ )) }),
21645
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: sizerRef, className: "invisible absolute top-0 left-0 flex flex-col items-start", children: labels.map((label, index) => /* @__PURE__ */ jsxRuntime.jsx(
21646
+ "span",
21647
+ {
21648
+ className: "block truncate",
21649
+ style: { maxWidth: maxWidthAt(index) },
21650
+ children: label
21651
+ },
21652
+ `${label}-${index}`
21653
+ )) })
21654
+ ]
21655
+ }
21656
+ )
21657
+ );
21658
+ }
21177
21659
  var ChartFrame = /* @__PURE__ */ react.forwardRef(function ChartFrame2({ title, unit, legend, legendPosition = "top", children, className, style, ...props }, ref) {
21178
21660
  return /* @__PURE__ */ jsxRuntime.jsxs(
21179
21661
  "div",
@@ -21365,44 +21847,6 @@ function linePointDimmedColor(palette, index, colors) {
21365
21847
  function dataValueColor(palette, index, colors) {
21366
21848
  return pick(DATA_VALUE_COLORS, DATA_VALUE_PALETTES, palette, index, colors);
21367
21849
  }
21368
- function useElementSize() {
21369
- const ref = react.useRef(null);
21370
- const [size, setSize] = react.useState({ width: 0, height: 0 });
21371
- react.useEffect(() => {
21372
- const element = ref.current;
21373
- if (!element) return;
21374
- const update = () => {
21375
- const { width, height } = element.getBoundingClientRect();
21376
- setSize(
21377
- (prev) => Math.abs(prev.width - width) < 0.5 && Math.abs(prev.height - height) < 0.5 ? prev : { width, height }
21378
- );
21379
- };
21380
- update();
21381
- if (typeof ResizeObserver === "undefined") return;
21382
- const observer = new ResizeObserver(update);
21383
- observer.observe(element);
21384
- return () => observer.disconnect();
21385
- }, []);
21386
- return { ref, size };
21387
- }
21388
- function useSvgTextMetrics() {
21389
- const ref = react.useRef(null);
21390
- const [metrics, setMetrics] = react.useState({ top: 0, width: 0 });
21391
- react.useEffect(() => {
21392
- const element = ref.current;
21393
- if (!element) return;
21394
- let box3;
21395
- try {
21396
- box3 = element.getBBox();
21397
- } catch {
21398
- return;
21399
- }
21400
- setMetrics(
21401
- (prev) => Math.abs(prev.top - box3.y) < 0.5 && Math.abs(prev.width - box3.width) < 0.5 ? prev : { top: box3.y, width: box3.width }
21402
- );
21403
- });
21404
- return { ref, metrics };
21405
- }
21406
21850
  var DOT_SIZE2 = 6;
21407
21851
  var DEFAULT_GAP_X = { after: 4, before: 4 };
21408
21852
  var ANCHOR_GAP_Y = 0;
@@ -21564,6 +22008,7 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
21564
22008
  tickCount = 8,
21565
22009
  barSize = BAR_SIZE_MAX,
21566
22010
  showValueLabel = true,
22011
+ rotateCategoryLabels = false,
21567
22012
  showLegend = true,
21568
22013
  legendPosition = "top",
21569
22014
  showTooltip = true,
@@ -21575,9 +22020,10 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
21575
22020
  style
21576
22021
  }, ref) {
21577
22022
  const vertical = direction === "vertical";
22023
+ const rotateLabels = rotateCategoryLabels && vertical;
21578
22024
  const { ref: plotRef, size } = useElementSize();
21579
- const { ref: axisTextRef, size: axisText } = useElementSize();
21580
- const { ref: axisSampleRef, size: axisSample } = useElementSize();
22025
+ const { ref: axisTextRef, size: axisText } = useElementSize(rotateLabels);
22026
+ const { ref: axisSampleRef, size: axisSample } = useElementSize(rotateLabels);
21581
22027
  const clipId = react.useId();
21582
22028
  const [hoveredCategory, setHoveredCategory] = react.useState(null);
21583
22029
  const perCategory = series.length === 1;
@@ -22036,7 +22482,29 @@ var SBarChart = /* @__PURE__ */ react.forwardRef(function SBarChart2({
22036
22482
  // 눈금은 왼쪽 한 줄뿐이다 — 오른쪽에도 적으면 서로 다른 단위처럼 읽힌다.
22037
22483
  gridTemplateColumns: "max-content 1fr"
22038
22484
  },
22039
- children: vertical ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
22485
+ children: rotateLabels ? (
22486
+ // 기울인 이름은 눈금 열 아래까지 뻗으므로 스크롤 상자가 눈금 열까지 감싼다.
22487
+ /* @__PURE__ */ jsxRuntime.jsx(
22488
+ ChartRotatedAxisLayout,
22489
+ {
22490
+ axis: /* @__PURE__ */ jsxRuntime.jsx(
22491
+ ChartAxisLabels,
22492
+ {
22493
+ scale,
22494
+ direction,
22495
+ format: formatValue,
22496
+ sampleRef: axisSampleRef
22497
+ }
22498
+ ),
22499
+ axisStyle: { height: plotHeight, ...inset },
22500
+ plot,
22501
+ minPlotWidth,
22502
+ labels: categories,
22503
+ xAt: (index) => layout.offset + (index + 0.5) * layout.band,
22504
+ labelsRef: axisTextRef
22505
+ }
22506
+ )
22507
+ ) : vertical ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
22040
22508
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: plotHeight, ...inset }, children: /* @__PURE__ */ jsxRuntime.jsx(
22041
22509
  ChartAxisLabels,
22042
22510
  {
@@ -22167,6 +22635,7 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22167
22635
  tickCount = 8,
22168
22636
  showPoint = true,
22169
22637
  showValueLabel = false,
22638
+ rotateCategoryLabels = false,
22170
22639
  showLegend = true,
22171
22640
  legendPosition = "top",
22172
22641
  showTooltip = true,
@@ -22179,8 +22648,8 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22179
22648
  }, ref) {
22180
22649
  const smooth = curve === "smooth";
22181
22650
  const { ref: plotRef, size } = useElementSize();
22182
- const { ref: axisTextRef, size: axisText } = useElementSize();
22183
- const { ref: axisSampleRef, size: axisSample } = useElementSize();
22651
+ const { ref: axisTextRef, size: axisText } = useElementSize(rotateCategoryLabels);
22652
+ const { ref: axisSampleRef, size: axisSample } = useElementSize(rotateCategoryLabels);
22184
22653
  const gradientId = react.useId();
22185
22654
  const [hoveredCategory, setHoveredCategory] = react.useState(null);
22186
22655
  const colorAt = react.useCallback(
@@ -22214,23 +22683,16 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22214
22683
  [rows, tickCount, max]
22215
22684
  );
22216
22685
  const count = categories.length;
22217
- const step = count > 1 ? size.width / (count - 1) : 0;
22218
- const xAt = react.useCallback(
22219
- (index) => count > 1 ? index * step : size.width / 2,
22220
- [count, step, size.width]
22221
- );
22686
+ const band = Math.max(0, size.width - AXIS_EDGE_PADDING * 2) / Math.max(1, count);
22687
+ const xAt = react.useCallback((index) => AXIS_EDGE_PADDING + (index + 0.5) * band, [band]);
22222
22688
  const cellOf = react.useCallback(
22223
- (index) => count > 1 ? {
22224
- left: Math.max(0, xAt(index) - step / 2),
22225
- right: Math.min(size.width, xAt(index) + step / 2)
22226
- } : { left: 0, right: size.width },
22227
- [count, step, xAt, size.width]
22228
- );
22229
- const edgeOf = react.useCallback(
22230
- (index) => count > 1 && index === 0 ? "start" : count > 1 && index === count - 1 ? "end" : null,
22231
- [count]
22689
+ (index) => ({
22690
+ left: AXIS_EDGE_PADDING + index * band,
22691
+ right: AXIS_EDGE_PADDING + (index + 1) * band
22692
+ }),
22693
+ [band]
22232
22694
  );
22233
- const minPlotWidth = categories.length * BAND_MIN;
22695
+ const minPlotWidth = count * BAND_MIN + AXIS_EDGE_PADDING * 2;
22234
22696
  const totalHeight = height ?? 320;
22235
22697
  const plotHeight = typeof totalHeight === "number" ? Math.max(0, totalHeight - axisText.height) : `calc(${totalHeight} - ${axisText.height}px)`;
22236
22698
  const plotted = react.useMemo(() => {
@@ -22244,10 +22706,10 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22244
22706
  }, [rows, size.width, size.height, xAt, scale]);
22245
22707
  const legend = showLegend ? /* @__PURE__ */ jsxRuntime.jsx(ChartLegend, { items: series.map((s, i) => ({ name: s.name, color: colorAt(i) })) }) : null;
22246
22708
  const handleMove = (event) => {
22247
- if (!showTooltip || count === 0 || size.width <= 0 || series.length === 0) return;
22709
+ if (!showTooltip || count === 0 || band <= 0 || series.length === 0) return;
22248
22710
  const rect = event.currentTarget.getBoundingClientRect();
22249
- const along = event.clientX - rect.left;
22250
- const category = count > 1 ? Math.round(along / step) : 0;
22711
+ const along = event.clientX - rect.left - AXIS_EDGE_PADDING;
22712
+ const category = Math.floor(along / band);
22251
22713
  setHoveredCategory(category >= 0 && category < categories.length ? category : null);
22252
22714
  };
22253
22715
  const tooltipItems = hoveredCategory == null ? [] : rows.flatMap((row, seriesIndex) => {
@@ -22286,10 +22748,7 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22286
22748
  categories.map((label, index) => /* @__PURE__ */ jsxRuntime.jsx(
22287
22749
  "span",
22288
22750
  {
22289
- className: cn(
22290
- "absolute top-0 whitespace-nowrap",
22291
- edgeOf(index) === "start" ? "translate-x-0" : edgeOf(index) === "end" ? "-translate-x-full" : "-translate-x-1/2"
22292
- ),
22751
+ className: "absolute top-0 -translate-x-1/2 whitespace-nowrap",
22293
22752
  style: { left: xAt(index) },
22294
22753
  children: label
22295
22754
  },
@@ -22470,17 +22929,114 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22470
22929
  y: point.y - POINT_RADIUS - VALUE_LABEL_GAP2,
22471
22930
  className: hoveredCategory === categoryIndex ? LINE_DATA_LABEL_TYPO_HOVER : LINE_DATA_LABEL_TYPO,
22472
22931
  style: { fill: LINE_DATA_LABEL_COLOR },
22473
- textAnchor: edgeOf(categoryIndex) === "start" ? "start" : edgeOf(categoryIndex) === "end" ? "end" : "middle",
22932
+ textAnchor: "middle",
22474
22933
  dominantBaseline: "auto",
22475
22934
  children: formatValue(point.own)
22476
22935
  },
22477
22936
  `label-${seriesIndex}-${categoryIndex}`
22478
22937
  )
22479
- )
22480
- ) : [],
22481
- [showValueLabel, plotted, hoveredCategory, formatValue, edgeOf]
22938
+ )
22939
+ ) : [],
22940
+ [showValueLabel, plotted, hoveredCategory, formatValue]
22941
+ );
22942
+ const inset = { paddingTop: axisSample.height / 2, paddingBottom: axisSample.height / 2 };
22943
+ const axisLabels = /* @__PURE__ */ jsxRuntime.jsx(
22944
+ ChartAxisLabels,
22945
+ {
22946
+ scale,
22947
+ direction: "vertical",
22948
+ format: formatValue,
22949
+ sampleRef: axisSampleRef
22950
+ }
22951
+ );
22952
+ const plot = (
22953
+ // 바깥은 자리(높이·물러남)를, 안쪽은 그림을 맡는다. minWidth 는 바깥에 둬야
22954
+ // 스크롤 컨테이너가 넘치는 폭을 알아본다.
22955
+ /* @__PURE__ */ jsxRuntime.jsx(
22956
+ "div",
22957
+ {
22958
+ className: "relative min-w-0",
22959
+ style: { height: plotHeight, minWidth: minPlotWidth || void 0, ...inset },
22960
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
22961
+ "div",
22962
+ {
22963
+ ref: plotRef,
22964
+ className: "relative h-full w-full",
22965
+ onMouseMove: handleMove,
22966
+ onMouseLeave: () => setHoveredCategory(null),
22967
+ children: [
22968
+ size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
22969
+ "svg",
22970
+ {
22971
+ width: size.width,
22972
+ height: size.height,
22973
+ overflow: "visible",
22974
+ role: "img",
22975
+ "aria-label": `${categories.length}\uAC1C \uD56D\uBAA9, ${series.length}\uAC1C \uACC4\uC5F4 ${stacked ? "\uB204\uC801 " : ""}\uAEBE\uC740\uC120 \uADF8\uB798\uD504`,
22976
+ children: [
22977
+ /* @__PURE__ */ jsxRuntime.jsx(
22978
+ "text",
22979
+ {
22980
+ ref: valueLabelRef,
22981
+ "aria-hidden": true,
22982
+ x: 0,
22983
+ y: 0,
22984
+ className: LINE_DATA_LABEL_TYPO_HOVER,
22985
+ dominantBaseline: "auto",
22986
+ style: { visibility: "hidden" },
22987
+ children: "0"
22988
+ }
22989
+ ),
22990
+ /* @__PURE__ */ jsxRuntime.jsx(ChartGrid, { scale, direction: "vertical", width: size.width, height: size.height }),
22991
+ hoveredCategory != null && /* @__PURE__ */ jsxRuntime.jsx(
22992
+ "rect",
22993
+ {
22994
+ "aria-hidden": true,
22995
+ x: cellOf(hoveredCategory).left,
22996
+ y: 0,
22997
+ width: cellOf(hoveredCategory).right - cellOf(hoveredCategory).left,
22998
+ height: size.height,
22999
+ style: {
23000
+ fill: "var(--cmp-chart-barContainer-hover-bg)",
23001
+ fillOpacity: "var(--cmp-chart-barContainer-hover-opacity)"
23002
+ }
23003
+ }
23004
+ ),
23005
+ areaGradients,
23006
+ areaMask,
23007
+ area && /* @__PURE__ */ jsxRuntime.jsx("g", { mask: `url(#${areaMaskId})`, children: areas }),
23008
+ lines,
23009
+ pointHalos,
23010
+ pointDots,
23011
+ valueLabels
23012
+ ]
23013
+ }
23014
+ ),
23015
+ showTooltip && hoveredCategory != null && tooltipAnchor != null && tooltipItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
23016
+ ChartTooltip,
23017
+ {
23018
+ title: categories[hoveredCategory] ?? "",
23019
+ items: tooltipItems,
23020
+ anchor: tooltipAnchor,
23021
+ bounds: size,
23022
+ gapX: { after: -4, before: -4 },
23023
+ footer: tooltipFooter?.({
23024
+ categoryIndex: hoveredCategory,
23025
+ name: categories[hoveredCategory] ?? "",
23026
+ points: rows.flatMap((row, seriesIndex) => {
23027
+ const point = row[hoveredCategory];
23028
+ if (point == null) return [];
23029
+ return [{ seriesIndex, categoryIndex: hoveredCategory, value: point.own }];
23030
+ })
23031
+ })
23032
+ }
23033
+ )
23034
+ ]
23035
+ }
23036
+ )
23037
+ }
23038
+ )
22482
23039
  );
22483
- const inset = { paddingTop: axisSample.height / 2, paddingBottom: axisSample.height / 2 };
22484
23040
  return /* @__PURE__ */ jsxRuntime.jsx(
22485
23041
  ChartFrame,
22486
23042
  {
@@ -22491,7 +23047,7 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22491
23047
  legendPosition,
22492
23048
  className,
22493
23049
  style,
22494
- children: /* @__PURE__ */ jsxRuntime.jsxs(
23050
+ children: /* @__PURE__ */ jsxRuntime.jsx(
22495
23051
  "div",
22496
23052
  {
22497
23053
  className: "grid",
@@ -22500,119 +23056,27 @@ var SLineChart = /* @__PURE__ */ react.forwardRef(function SLineChart2({
22500
23056
  // 눈금은 왼쪽 한 줄뿐이다 — 오른쪽에도 적으면 서로 다른 단위처럼 읽힌다.
22501
23057
  gridTemplateColumns: "max-content 1fr"
22502
23058
  },
22503
- children: [
22504
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: plotHeight, ...inset }, children: /* @__PURE__ */ jsxRuntime.jsx(
22505
- ChartAxisLabels,
22506
- {
22507
- scale,
22508
- direction: "vertical",
22509
- format: formatValue,
22510
- sampleRef: axisSampleRef
22511
- }
22512
- ) }),
22513
- /* @__PURE__ */ jsxRuntime.jsxs(
22514
- "div",
23059
+ children: rotateCategoryLabels ? (
23060
+ // 기울인 이름은 눈금 열 아래까지 뻗으므로 스크롤 상자가 눈금 열까지 감싼다.
23061
+ /* @__PURE__ */ jsxRuntime.jsx(
23062
+ ChartRotatedAxisLayout,
22515
23063
  {
22516
- className: "min-w-0 overflow-x-auto",
22517
- style: { marginInline: -POINT_HOVER_EXTENT, paddingInline: POINT_HOVER_EXTENT },
22518
- children: [
22519
- /* @__PURE__ */ jsxRuntime.jsx(
22520
- "div",
22521
- {
22522
- className: "relative min-w-0",
22523
- style: { height: plotHeight, minWidth: minPlotWidth || void 0, ...inset },
22524
- children: /* @__PURE__ */ jsxRuntime.jsxs(
22525
- "div",
22526
- {
22527
- ref: plotRef,
22528
- className: "relative h-full w-full",
22529
- onMouseMove: handleMove,
22530
- onMouseLeave: () => setHoveredCategory(null),
22531
- children: [
22532
- size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
22533
- "svg",
22534
- {
22535
- width: size.width,
22536
- height: size.height,
22537
- overflow: "visible",
22538
- role: "img",
22539
- "aria-label": `${categories.length}\uAC1C \uD56D\uBAA9, ${series.length}\uAC1C \uACC4\uC5F4 ${stacked ? "\uB204\uC801 " : ""}\uAEBE\uC740\uC120 \uADF8\uB798\uD504`,
22540
- children: [
22541
- /* @__PURE__ */ jsxRuntime.jsx(
22542
- "text",
22543
- {
22544
- ref: valueLabelRef,
22545
- "aria-hidden": true,
22546
- x: 0,
22547
- y: 0,
22548
- className: LINE_DATA_LABEL_TYPO_HOVER,
22549
- dominantBaseline: "auto",
22550
- style: { visibility: "hidden" },
22551
- children: "0"
22552
- }
22553
- ),
22554
- /* @__PURE__ */ jsxRuntime.jsx(
22555
- ChartGrid,
22556
- {
22557
- scale,
22558
- direction: "vertical",
22559
- width: size.width,
22560
- height: size.height
22561
- }
22562
- ),
22563
- hoveredCategory != null && /* @__PURE__ */ jsxRuntime.jsx(
22564
- "rect",
22565
- {
22566
- "aria-hidden": true,
22567
- x: cellOf(hoveredCategory).left,
22568
- y: 0,
22569
- width: cellOf(hoveredCategory).right - cellOf(hoveredCategory).left,
22570
- height: size.height,
22571
- style: {
22572
- fill: "var(--cmp-chart-barContainer-hover-bg)",
22573
- fillOpacity: "var(--cmp-chart-barContainer-hover-opacity)"
22574
- }
22575
- }
22576
- ),
22577
- areaGradients,
22578
- areaMask,
22579
- area && /* @__PURE__ */ jsxRuntime.jsx("g", { mask: `url(#${areaMaskId})`, children: areas }),
22580
- lines,
22581
- pointHalos,
22582
- pointDots,
22583
- valueLabels
22584
- ]
22585
- }
22586
- ),
22587
- showTooltip && hoveredCategory != null && tooltipAnchor != null && tooltipItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
22588
- ChartTooltip,
22589
- {
22590
- title: categories[hoveredCategory] ?? "",
22591
- items: tooltipItems,
22592
- anchor: tooltipAnchor,
22593
- bounds: size,
22594
- gapX: { after: -4, before: -4 },
22595
- footer: tooltipFooter?.({
22596
- categoryIndex: hoveredCategory,
22597
- name: categories[hoveredCategory] ?? "",
22598
- points: rows.flatMap((row, seriesIndex) => {
22599
- const point = row[hoveredCategory];
22600
- if (point == null) return [];
22601
- return [{ seriesIndex, categoryIndex: hoveredCategory, value: point.own }];
22602
- })
22603
- })
22604
- }
22605
- )
22606
- ]
22607
- }
22608
- )
22609
- }
22610
- ),
22611
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: axisTextRef, children: categoryLabels })
22612
- ]
23064
+ axis: axisLabels,
23065
+ axisStyle: { height: plotHeight, ...inset },
23066
+ plot,
23067
+ minPlotWidth,
23068
+ labels: categories,
23069
+ xAt,
23070
+ labelsRef: axisTextRef
22613
23071
  }
22614
23072
  )
22615
- ]
23073
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
23074
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: plotHeight, ...inset }, children: axisLabels }),
23075
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 overflow-x-auto", children: [
23076
+ plot,
23077
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: axisTextRef, children: categoryLabels })
23078
+ ] })
23079
+ ] })
22616
23080
  }
22617
23081
  )
22618
23082
  }
@@ -28687,212 +29151,171 @@ function formatTimeDisplay(value, type) {
28687
29151
  const displayHour = hour % 12 === 0 ? 12 : hour % 12;
28688
29152
  return `${meridiem} ${padTimePart(displayHour)}:${padTimePart(minute)}`;
28689
29153
  }
28690
- var TIME_SELECTOR_WIDTH = 136;
28691
- var MERIDIEM_TIME_SELECTOR_WIDTH = 154;
28692
- function TimeSelector({
29154
+ function scrollSelectedIntoView(list, index, center) {
29155
+ const top = index * MODE_ITEM_HEIGHT;
29156
+ if (center) {
29157
+ list.scrollTop = Math.max(0, top - (list.clientHeight - MODE_ITEM_HEIGHT) / 2);
29158
+ return;
29159
+ }
29160
+ if (top < list.scrollTop) list.scrollTop = top;
29161
+ else if (top + MODE_ITEM_HEIGHT > list.scrollTop + list.clientHeight)
29162
+ list.scrollTop = top + MODE_ITEM_HEIGHT - list.clientHeight;
29163
+ }
29164
+ function lastIndex(arr, pred) {
29165
+ for (let i = arr.length - 1; i >= 0; i--) if (pred(arr[i], i)) return i;
29166
+ return -1;
29167
+ }
29168
+ function ListColumn({ ariaLabel, items }) {
29169
+ const listRef = react.useRef(null);
29170
+ const mountedRef = react.useRef(false);
29171
+ const selectedIndex = items.findIndex((item4) => item4.selected);
29172
+ const tabIndexTarget = selectedIndex >= 0 ? selectedIndex : items.findIndex((item4) => !item4.disabled);
29173
+ react.useLayoutEffect(() => {
29174
+ const list = listRef.current;
29175
+ if (!list || selectedIndex < 0) return;
29176
+ scrollSelectedIntoView(list, selectedIndex, !mountedRef.current);
29177
+ mountedRef.current = true;
29178
+ }, [selectedIndex]);
29179
+ const onKeyDown = (event) => {
29180
+ const buttons = [...listRef.current?.querySelectorAll("button") ?? []];
29181
+ const current = buttons.indexOf(document.activeElement);
29182
+ if (current < 0) return;
29183
+ const enabled = (item4, i) => !item4.disabled && i !== current;
29184
+ let next;
29185
+ if (event.key === "ArrowDown") next = items.findIndex((it, i) => i > current && enabled(it, i));
29186
+ else if (event.key === "ArrowUp")
29187
+ next = lastIndex(items, (it, i) => i < current && enabled(it, i));
29188
+ else if (event.key === "Home") next = items.findIndex((it) => !it.disabled);
29189
+ else if (event.key === "End") next = lastIndex(items, (it) => !it.disabled);
29190
+ else return;
29191
+ event.preventDefault();
29192
+ if (next < 0) return;
29193
+ buttons[next]?.focus({ preventScroll: true });
29194
+ if (listRef.current) scrollSelectedIntoView(listRef.current, next, false);
29195
+ };
29196
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full w-[var(--cmp-datepicker-mode-yearMonth-column-width)] min-w-0 overflow-hidden bg-[var(--cmp-datepicker-mode-bg)]", children: /* @__PURE__ */ jsxRuntime.jsx(
29197
+ "div",
29198
+ {
29199
+ ref: listRef,
29200
+ role: "listbox",
29201
+ "aria-label": ariaLabel,
29202
+ tabIndex: -1,
29203
+ className: "h-full overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
29204
+ onKeyDown,
29205
+ children: items.map((item4, index) => /* @__PURE__ */ jsxRuntime.jsx(
29206
+ "button",
29207
+ {
29208
+ type: "button",
29209
+ role: "option",
29210
+ "aria-selected": item4.selected,
29211
+ disabled: item4.disabled,
29212
+ tabIndex: index === tabIndexTarget ? 0 : -1,
29213
+ className: cn(modeListItemClass(item4.selected, item4.disabled), "outline-offset-[-2px]"),
29214
+ onClick: () => !item4.disabled && item4.onSelect(),
29215
+ children: item4.label
29216
+ },
29217
+ item4.key
29218
+ ))
29219
+ }
29220
+ ) });
29221
+ }
29222
+ var HOURS_24 = Array.from({ length: 24 }, (_, i) => i);
29223
+ var HOURS_12 = [12, ...Array.from({ length: 11 }, (_, i) => i + 1)];
29224
+ function TimeListbox({
28693
29225
  value,
28694
29226
  showMeridiem,
28695
29227
  minuteStep,
28696
- onChange
29228
+ isDisabled,
29229
+ onChange,
29230
+ labelPrefix,
29231
+ elevated = true
28697
29232
  }) {
28698
- const parsed = react.useMemo(() => parseTimeValue(value), [value]);
28699
- const [hour, setHour] = react.useState(parsed.hour);
28700
- const [minute, setMinute] = react.useState(parsed.minute);
28701
- const [editingPart, setEditingPart] = react.useState(null);
28702
- const [inputDraft, setInputDraft] = react.useState({});
29233
+ const toState = (v) => v ? parseTimeValue(v) : null;
29234
+ const [time, setTime] = react.useState(() => toState(value));
28703
29235
  react.useEffect(() => {
28704
- setHour(parsed.hour);
28705
- setMinute(parsed.minute);
28706
- }, [parsed.hour, parsed.minute]);
28707
- const meridiem = hour >= 12 ? "PM" : "AM";
28708
- const displayHour = showMeridiem ? hour % 12 === 0 ? 12 : hour % 12 : hour;
28709
- const commit = (nextHour, nextMinute) => {
28710
- const h = clampTimePart(nextHour, 0, 23);
28711
- const m = clampTimePart(nextMinute, 0, 59);
28712
- setHour(h);
28713
- setMinute(m);
28714
- onChange(formatTimeValue(h, m));
28715
- };
28716
- const setMeridiem = (next) => {
28717
- if (next === meridiem) return;
28718
- commit(next === "AM" ? hour - 12 : hour + 12, minute);
28719
- };
28720
- const changeMinute = (delta) => {
28721
- const next = minute + delta;
28722
- if (next > 59) {
28723
- commit((hour + 1) % 24, next - 60);
28724
- return;
28725
- }
28726
- if (next < 0) {
28727
- commit((hour + 23) % 24, next + 60);
28728
- return;
28729
- }
28730
- commit(hour, next);
28731
- };
28732
- const changeHour = (delta) => commit((hour + delta + 24) % 24, minute);
28733
- const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
28734
- const beginEdit = (part, currentValue) => {
28735
- setEditingPart(part);
28736
- setInputDraft((prev) => ({ ...prev, [part]: String(currentValue).padStart(2, "0") }));
28737
- };
28738
- const commitHourDraft = (draft) => {
28739
- const clamped = clampTimePart(Number(draft), showMeridiem ? 1 : 0, showMeridiem ? 12 : 23);
28740
- const nextHour = showMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
28741
- commit(nextHour, minute);
28742
- return clamped;
28743
- };
28744
- const commitMinuteDraft = (draft) => {
28745
- const clamped = clampTimePart(Number(draft), 0, 59);
28746
- commit(hour, clamped);
28747
- return clamped;
28748
- };
28749
- const endEdit = (part) => {
28750
- const draft = inputDraft[part];
28751
- if (part === "hour" && draft != null && draft !== "") {
28752
- commitHourDraft(draft);
28753
- }
28754
- if (part === "minute" && draft != null && draft !== "") {
28755
- commitMinuteDraft(draft);
28756
- }
28757
- setEditingPart(null);
28758
- setInputDraft({});
28759
- };
28760
- const inputStyle = {
28761
- width: "var(--cmp-timepicker-selector-time-input-width)",
28762
- height: "var(--cmp-textinput-sm-height)",
28763
- borderRadius: "var(--cmp-textinput-sm-radius)",
28764
- border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
28765
- color: "var(--cmp-textinput-text-default)",
28766
- fontSize: 12,
28767
- lineHeight: "20px"
29236
+ setTime(toState(value));
29237
+ }, [value]);
29238
+ const step = Math.max(1, Math.floor(minuteStep));
29239
+ const minutes = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step);
29240
+ const blocked = (h, m) => isDisabled?.(h * 60 + m) ?? false;
29241
+ const hourBlocked = (h) => minutes.every((m) => blocked(h, m));
29242
+ const commit = (hours, preferred, part) => {
29243
+ let next = blocked(preferred.hour, preferred.minute) ? null : preferred;
29244
+ for (const h of hours) {
29245
+ if (next) break;
29246
+ const m = minutes.find((mm) => !blocked(h, mm));
29247
+ if (m !== void 0) next = { hour: h, minute: m };
29248
+ }
29249
+ if (!next) return;
29250
+ setTime(next);
29251
+ onChange(formatTimeValue(next.hour, next.minute), part);
28768
29252
  };
28769
- const renderColumn = (type) => {
28770
- const isHour = type === "hour";
28771
- const val = isHour ? displayHour : minute;
28772
- const displayValue2 = editingPart === type ? inputDraft[type] ?? "" : String(val).padStart(2, "0");
28773
- return /* @__PURE__ */ jsxRuntime.jsxs(
28774
- "div",
28775
- {
28776
- className: "flex flex-col items-center justify-center",
28777
- style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
28778
- children: [
28779
- /* @__PURE__ */ jsxRuntime.jsx(
28780
- SGhostButton,
28781
- {
28782
- icon: "chevronUp",
28783
- size: "xs",
28784
- ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
28785
- onClick: () => isHour ? changeHour(1) : changeMinute(minuteStep)
28786
- }
28787
- ),
28788
- /* @__PURE__ */ jsxRuntime.jsx(
28789
- "input",
28790
- {
28791
- "aria-label": isHour ? "\uC2DC" : "\uBD84",
28792
- inputMode: "numeric",
28793
- value: displayValue2,
28794
- placeholder: "-",
28795
- onFocus: () => beginEdit(type, val),
28796
- onBlur: () => endEdit(type),
28797
- onChange: (event) => {
28798
- const draft = sanitizeInput(event.target.value);
28799
- setInputDraft((prev) => ({ ...prev, [type]: draft }));
28800
- if (draft === "") return;
28801
- const raw = Number(draft);
28802
- if (isHour) {
28803
- if (draft.length < 2) return;
28804
- const clamped2 = commitHourDraft(draft);
28805
- const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
28806
- setInputDraft((prev) => ({ ...prev, [type]: displayDraft2 }));
28807
- return;
28808
- }
28809
- if (draft.length < 2) return;
28810
- const clamped = commitMinuteDraft(draft);
28811
- const displayDraft = raw === clamped ? draft : String(clamped);
28812
- setInputDraft((prev) => ({ ...prev, [type]: displayDraft }));
28813
- },
28814
- className: "box-border bg-white text-center outline-none",
28815
- style: inputStyle
28816
- }
28817
- ),
28818
- /* @__PURE__ */ jsxRuntime.jsx(
28819
- SGhostButton,
28820
- {
28821
- icon: "chevronDown",
28822
- size: "xs",
28823
- ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
28824
- onClick: () => isHour ? changeHour(-1) : changeMinute(-minuteStep)
28825
- }
28826
- )
28827
- ]
29253
+ const hour = time?.hour;
29254
+ const minute = time?.minute ?? 0;
29255
+ const meridiem = hour === void 0 ? void 0 : hour >= 12 ? "PM" : "AM";
29256
+ const prefix = labelPrefix ? `${labelPrefix} ` : "";
29257
+ const meridiemItems = ["AM", "PM"].map((item4) => {
29258
+ const base = item4 === "AM" ? 0 : 12;
29259
+ const range = HOURS_24.slice(base, base + 12);
29260
+ return {
29261
+ key: item4,
29262
+ label: item4 === "AM" ? "\uC624\uC804" : "\uC624\uD6C4",
29263
+ selected: meridiem === item4,
29264
+ disabled: range.every(hourBlocked),
29265
+ onSelect: () => {
29266
+ if (meridiem === item4) return;
29267
+ const h = hour === void 0 ? base : hour % 12 + base;
29268
+ commit(range, { hour: h, minute }, "meridiem");
28828
29269
  }
28829
- );
28830
- };
29270
+ };
29271
+ });
29272
+ const hourItems = showMeridiem ? HOURS_12.map((display) => {
29273
+ const h = display % 12 + (meridiem === "PM" ? 12 : 0);
29274
+ return {
29275
+ key: String(display),
29276
+ label: `${padTimePart(display)}\uC2DC`,
29277
+ selected: hour === h,
29278
+ disabled: hourBlocked(h),
29279
+ onSelect: () => commit([h], { hour: h, minute }, "hour")
29280
+ };
29281
+ }) : HOURS_24.map((h) => ({
29282
+ key: String(h),
29283
+ label: `${padTimePart(h)}\uC2DC`,
29284
+ selected: hour === h,
29285
+ disabled: hourBlocked(h),
29286
+ onSelect: () => commit([h], { hour: h, minute }, "hour")
29287
+ }));
29288
+ const minuteHour = hour ?? (meridiem === "PM" ? 12 : 0);
29289
+ const minuteItems = minutes.map((m) => ({
29290
+ key: String(m),
29291
+ label: `${padTimePart(m)}\uBD84`,
29292
+ selected: time != null && minute === m,
29293
+ disabled: blocked(minuteHour, m),
29294
+ onSelect: () => commit([minuteHour], { hour: minuteHour, minute: m }, "minute")
29295
+ }));
29296
+ const divider = /* @__PURE__ */ jsxRuntime.jsx(
29297
+ "div",
29298
+ {
29299
+ "aria-hidden": true,
29300
+ className: "w-[var(--border-width-1)] self-stretch bg-[var(--sys-color-divider-default)]"
29301
+ }
29302
+ );
28831
29303
  return /* @__PURE__ */ jsxRuntime.jsxs(
28832
29304
  "div",
28833
29305
  {
28834
- className: "flex flex-col items-stretch justify-center overflow-hidden",
28835
- style: {
28836
- background: "var(--cmp-timepicker-selector-bg)",
28837
- borderRadius: "var(--cmp-timepicker-selector-radius)",
28838
- boxShadow: "4px 8px 16px -4px rgba(34,34,34,0.1), 0 0 24px -6px rgba(34,34,34,0.12)"
28839
- },
29306
+ className: cn(
29307
+ "inline-flex gap-[var(--cmp-datepicker-mode-yearMonth-gap)] overflow-hidden bg-[var(--cmp-datepicker-mode-bg)]",
29308
+ elevated && "rounded-[var(--cmp-datepicker-mode-radius)] shadow-normal-sm"
29309
+ ),
29310
+ style: { height: MODE_LIST_HEIGHT },
28840
29311
  children: [
28841
- showMeridiem && /* @__PURE__ */ jsxRuntime.jsx(
28842
- "div",
28843
- {
28844
- className: "flex items-center justify-center",
28845
- style: {
28846
- padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
28847
- },
28848
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item4, index) => {
28849
- const selected = item4 === meridiem;
28850
- return /* @__PURE__ */ jsxRuntime.jsx(
28851
- "button",
28852
- {
28853
- type: "button",
28854
- onClick: () => setMeridiem(item4),
28855
- className: cn(
28856
- "box-border h-[var(--cmp-textinput-sm-height)] w-[61px] cursor-pointer border border-solid bg-white text-center text-[12px] font-medium leading-[20px]",
28857
- index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)]" : "rounded-r-[var(--cmp-select-sm-radius)]",
28858
- index === 0 ? "-mr-px" : "",
28859
- selected ? "z-[1] border-[var(--cmp-timepicker-border-focus)] text-[var(--cmp-timepicker-border-focus)]" : "border-[var(--cmp-timepicker-border-default)] text-[var(--cmp-field-hint-color)]"
28860
- ),
28861
- children: item4 === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
28862
- },
28863
- item4
28864
- );
28865
- }) })
28866
- }
28867
- ),
28868
- showMeridiem && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px w-full bg-[var(--sys-color-divider-default)]" }),
28869
- /* @__PURE__ */ jsxRuntime.jsxs(
28870
- "div",
28871
- {
28872
- className: "flex items-center justify-center",
28873
- style: {
28874
- gap: "var(--cmp-timepicker-selector-time-gap)",
28875
- padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
28876
- },
28877
- children: [
28878
- renderColumn("hour"),
28879
- /* @__PURE__ */ jsxRuntime.jsx(
28880
- "span",
28881
- {
28882
- className: "text-center",
28883
- style: {
28884
- width: "var(--cmp-timepicker-selector-colon-size)",
28885
- color: "var(--cmp-field-hint-color)",
28886
- fontSize: "var(--cmp-timepicker-selector-colon-size)",
28887
- lineHeight: "var(--cmp-timepicker-selector-colon-size)"
28888
- },
28889
- children: ":"
28890
- }
28891
- ),
28892
- renderColumn("minute")
28893
- ]
28894
- }
28895
- )
29312
+ showMeridiem && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
29313
+ /* @__PURE__ */ jsxRuntime.jsx(ListColumn, { ariaLabel: `${prefix}\uC624\uC804/\uC624\uD6C4`, items: meridiemItems }),
29314
+ divider
29315
+ ] }),
29316
+ /* @__PURE__ */ jsxRuntime.jsx(ListColumn, { ariaLabel: `${prefix}\uC2DC`, items: hourItems }),
29317
+ divider,
29318
+ /* @__PURE__ */ jsxRuntime.jsx(ListColumn, { ariaLabel: `${prefix}\uBD84`, items: minuteItems })
28896
29319
  ]
28897
29320
  }
28898
29321
  );
@@ -28930,11 +29353,9 @@ function STimePicker({
28930
29353
  style
28931
29354
  }) {
28932
29355
  const portalContainer = usePortalContainer();
28933
- const triggerRef = react.useRef(null);
28934
29356
  const [open, setOpen] = react.useState(false);
28935
29357
  const triggerId = useFieldControlId();
28936
29358
  const triggerTextId = `${triggerId}-text`;
28937
- const [popoverAlignOffset, setPopoverAlignOffset] = react.useState(0);
28938
29359
  const [focused, setFocused] = react.useState(false);
28939
29360
  const [ruleError, setRuleError] = react.useState("");
28940
29361
  const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
@@ -28955,11 +29376,6 @@ function STimePicker({
28955
29376
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
28956
29377
  const commitOpen = (next) => {
28957
29378
  if (disabled) return;
28958
- if (next) {
28959
- const triggerWidth = triggerRef.current?.getBoundingClientRect().width ?? 0;
28960
- const selectorWidth = effectiveShowMeridiem ? MERIDIEM_TIME_SELECTOR_WIDTH : TIME_SELECTOR_WIDTH;
28961
- setPopoverAlignOffset(Math.max(0, (triggerWidth - selectorWidth) / 2));
28962
- }
28963
29379
  setOpen(next);
28964
29380
  onOpenChange?.(next);
28965
29381
  if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
@@ -28998,7 +29414,6 @@ function STimePicker({
28998
29414
  /* @__PURE__ */ jsxRuntime.jsx(RPopover13__namespace.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
28999
29415
  "div",
29000
29416
  {
29001
- ref: triggerRef,
29002
29417
  id: triggerId,
29003
29418
  role: "button",
29004
29419
  "aria-disabled": disabled,
@@ -29062,20 +29477,22 @@ function STimePicker({
29062
29477
  RPopover13__namespace.Content,
29063
29478
  {
29064
29479
  side: "bottom",
29065
- align: "start",
29480
+ align: "center",
29066
29481
  sideOffset: 4,
29067
- alignOffset: popoverAlignOffset,
29068
29482
  className: cn(
29069
29483
  "z-50 max-w-[var(--radix-popover-content-available-width)]",
29070
29484
  FLOATING_SLIDE_ANIM
29071
29485
  ),
29072
29486
  children: /* @__PURE__ */ jsxRuntime.jsx(
29073
- TimeSelector,
29487
+ TimeListbox,
29074
29488
  {
29075
29489
  value: normalized,
29076
29490
  showMeridiem: effectiveShowMeridiem,
29077
29491
  minuteStep,
29078
- onChange: commitValue
29492
+ onChange: (time, part) => {
29493
+ commitValue(time);
29494
+ if (part === "minute") commitOpen(false);
29495
+ }
29079
29496
  }
29080
29497
  )
29081
29498
  }
@@ -29105,7 +29522,6 @@ function formatTimeRangeDisplay(value, type) {
29105
29522
  if (value == null || value[0] === "" || value[1] === "") return "";
29106
29523
  return `${formatTimeDisplay(value[0], type)} ~ ${formatTimeDisplay(value[1], type)}`;
29107
29524
  }
29108
- var TIME_RANGE_SELECTOR_WIDTH = 345;
29109
29525
  var MIDDAY_FIELD_MIN_WIDTH = {
29110
29526
  sm: 178,
29111
29527
  md: 216
@@ -29114,6 +29530,10 @@ var MIDDAY_FIELD_MAX_WIDTH = {
29114
29530
  sm: "lg",
29115
29531
  md: "lg"
29116
29532
  };
29533
+ var toMinutes = (time) => {
29534
+ const { hour, minute } = parseTimeValue(time);
29535
+ return hour * 60 + minute;
29536
+ };
29117
29537
  function RangeTimeSelector({
29118
29538
  value,
29119
29539
  showMeridiem,
@@ -29121,233 +29541,32 @@ function RangeTimeSelector({
29121
29541
  rangeOrder,
29122
29542
  onChange
29123
29543
  }) {
29124
- const range = react.useMemo(() => value ?? ["00:00", "23:59"], [value]);
29125
- const [editingKey, setEditingKey] = react.useState(null);
29126
- const [inputDraft, setInputDraft] = react.useState({});
29127
- const autoAdjustedRef = react.useRef(null);
29128
- const getParts = (side) => parseTimeValue(side === "start" ? range[0] : range[1]);
29129
- const getMeridiem = (side) => getParts(side).hour >= 12 ? "PM" : "AM";
29130
- const getDisplayHour = (side) => {
29131
- const { hour } = getParts(side);
29132
- if (!showMeridiem) return hour;
29133
- const displayHour = hour % 12;
29134
- return displayHour === 0 ? 12 : displayHour;
29135
- };
29136
- const commit = (side, hour, minute, direction = "unknown") => {
29137
- const next = formatTimeValue(hour, minute);
29138
- onChange(
29139
- normalizeNextRange(side === "start" ? [next, range[1]] : [range[0], next], side, direction)
29140
- );
29141
- };
29142
- const getTimeMinutes2 = (time) => {
29143
- const { hour, minute } = parseTimeValue(time);
29144
- return hour * 60 + minute;
29145
- };
29146
- const normalizeNextRange = (nextRange, changedSide, direction) => {
29147
- if (rangeOrder === "allow-cross-day") return nextRange;
29148
- const [start, end] = nextRange;
29149
- const [currentStart, currentEnd] = range;
29150
- if (getTimeMinutes2(start) <= getTimeMinutes2(end)) {
29151
- const shouldKeepSynced = autoAdjustedRef.current?.side === changedSide && currentStart === currentEnd && start !== end && (direction === "unknown" || autoAdjustedRef.current.direction === direction);
29152
- if (shouldKeepSynced) {
29153
- return changedSide === "start" ? [start, start] : [end, end];
29154
- }
29155
- autoAdjustedRef.current = null;
29156
- return nextRange;
29157
- }
29158
- autoAdjustedRef.current = {
29159
- side: changedSide,
29160
- direction: changedSide === "start" ? "increase" : "decrease"
29161
- };
29162
- return changedSide === "start" ? [start, start] : [end, end];
29163
- };
29164
- const changeHour = (side, delta) => {
29165
- const { hour, minute } = getParts(side);
29166
- commit(side, (hour + delta + 24) % 24, minute, delta > 0 ? "increase" : "decrease");
29167
- };
29168
- const changeMinute = (side, delta) => {
29169
- const { hour, minute } = getParts(side);
29170
- const next = minute + delta;
29171
- if (next > 59) {
29172
- commit(side, (hour + 1) % 24, next - 60, delta > 0 ? "increase" : "decrease");
29173
- return;
29174
- }
29175
- if (next < 0) {
29176
- commit(side, (hour + 23) % 24, next + 60, delta > 0 ? "increase" : "decrease");
29544
+ const range = value ?? ["00:00", "23:59"];
29545
+ const strict = rangeOrder !== "allow-cross-day";
29546
+ const change = (side, time) => {
29547
+ if (side === "end") {
29548
+ onChange([range[0], time]);
29177
29549
  return;
29178
29550
  }
29179
- commit(side, hour, next, delta > 0 ? "increase" : "decrease");
29180
- };
29181
- const setMeridiem = (side, next) => {
29182
- const current = getMeridiem(side);
29183
- if (current === next) return;
29184
- const { hour, minute } = getParts(side);
29185
- commit(side, next === "AM" ? hour - 12 : hour + 12, minute);
29186
- };
29187
- const getEditingKey = (side, part) => `${side}-${part}`;
29188
- const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
29189
- const beginEdit = (side, part, currentValue) => {
29190
- const key = getEditingKey(side, part);
29191
- setEditingKey(key);
29192
- setInputDraft((prev) => ({ ...prev, [key]: String(currentValue).padStart(2, "0") }));
29193
- };
29194
- const commitHourDraft = (side, draft) => {
29195
- const clamped = clampTimePart(Number(draft), showMeridiem ? 1 : 0, showMeridiem ? 12 : 23);
29196
- const parts = getParts(side);
29197
- const meridiem = getMeridiem(side);
29198
- const nextHour = showMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
29199
- commit(side, nextHour, parts.minute);
29200
- return clamped;
29201
- };
29202
- const commitMinuteDraft = (side, draft) => {
29203
- const { hour } = getParts(side);
29204
- const clamped = clampTimePart(Number(draft), 0, 59);
29205
- commit(side, hour, clamped);
29206
- return clamped;
29207
- };
29208
- const endEdit = (side, part) => {
29209
- const key = getEditingKey(side, part);
29210
- const draft = inputDraft[key];
29211
- if (part === "hour" && draft != null && draft !== "") {
29212
- commitHourDraft(side, draft);
29213
- }
29214
- if (part === "minute" && draft != null && draft !== "") {
29215
- commitMinuteDraft(side, draft);
29216
- }
29217
- setEditingKey(null);
29218
- setInputDraft({});
29219
- };
29220
- const inputStyle = {
29221
- width: "var(--cmp-timepicker-selector-time-input-width)",
29222
- height: "var(--cmp-textinput-sm-height)",
29223
- borderRadius: "var(--cmp-textinput-sm-radius)",
29224
- border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
29225
- color: "var(--cmp-textinput-text-default)",
29226
- fontSize: 12,
29227
- lineHeight: "20px"
29228
- };
29229
- const renderColumn = (side, kind) => {
29230
- const isHour = kind === "hour";
29231
- const { minute } = getParts(side);
29232
- const val = isHour ? getDisplayHour(side) : minute;
29233
- const currentEditingKey = getEditingKey(side, kind);
29234
- const displayValue2 = editingKey === currentEditingKey ? inputDraft[currentEditingKey] ?? "" : String(val).padStart(2, "0");
29235
- return /* @__PURE__ */ jsxRuntime.jsxs(
29236
- "div",
29237
- {
29238
- className: "flex flex-col items-center justify-center",
29239
- style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
29240
- children: [
29241
- /* @__PURE__ */ jsxRuntime.jsx(
29242
- SGhostButton,
29243
- {
29244
- icon: "chevronUp",
29245
- size: "xs",
29246
- ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
29247
- onClick: () => isHour ? changeHour(side, 1) : changeMinute(side, minuteStep)
29248
- }
29249
- ),
29250
- /* @__PURE__ */ jsxRuntime.jsx(
29251
- "input",
29252
- {
29253
- "aria-label": `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"}`,
29254
- inputMode: "numeric",
29255
- value: displayValue2,
29256
- placeholder: "-",
29257
- onFocus: () => beginEdit(side, kind, val),
29258
- onBlur: () => endEdit(side, kind),
29259
- onChange: (event) => {
29260
- const draft = sanitizeInput(event.target.value);
29261
- setInputDraft((prev) => ({ ...prev, [currentEditingKey]: draft }));
29262
- if (draft === "") return;
29263
- const raw = Number(draft);
29264
- if (isHour) {
29265
- if (draft.length < 2) return;
29266
- const clamped2 = commitHourDraft(side, draft);
29267
- const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
29268
- setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft2 }));
29269
- return;
29270
- }
29271
- if (draft.length < 2) return;
29272
- const clamped = commitMinuteDraft(side, draft);
29273
- const displayDraft = raw === clamped ? draft : String(clamped);
29274
- setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft }));
29275
- },
29276
- className: "box-border bg-white text-center outline-none",
29277
- style: inputStyle
29278
- }
29279
- ),
29280
- /* @__PURE__ */ jsxRuntime.jsx(
29281
- SGhostButton,
29282
- {
29283
- icon: "chevronDown",
29284
- size: "xs",
29285
- ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
29286
- onClick: () => isHour ? changeHour(side, -1) : changeMinute(side, -minuteStep)
29287
- }
29288
- )
29289
- ]
29290
- }
29291
- );
29551
+ onChange([time, strict && toMinutes(time) > toMinutes(range[1]) ? time : range[1]]);
29292
29552
  };
29293
29553
  const renderPanel = (side, title) => /* @__PURE__ */ jsxRuntime.jsxs(
29294
29554
  "div",
29295
29555
  {
29296
- className: "flex flex-col items-center justify-center",
29297
- style: { gap: "var(--cmp-timepicker-selector-range-section-gap)" },
29556
+ className: "flex flex-col items-center",
29557
+ style: { gap: "var(--cmp-timepicker-selector-range-heading-gap)" },
29298
29558
  children: [
29299
- /* @__PURE__ */ jsxRuntime.jsxs(
29300
- "div",
29301
- {
29302
- className: "flex flex-col items-center",
29303
- style: { gap: "var(--cmp-timepicker-selector-range-heading-gap)" },
29304
- children: [
29305
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-nowrap text-center text-[12px] font-bold leading-[20px] text-[var(--cmp-timepicker-selector-range-heading-color)]", children: title }),
29306
- showMeridiem && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item4, index) => {
29307
- const selected = item4 === getMeridiem(side);
29308
- return /* @__PURE__ */ jsxRuntime.jsx(
29309
- "button",
29310
- {
29311
- type: "button",
29312
- onClick: () => setMeridiem(side, item4),
29313
- className: cn(
29314
- "box-border h-[var(--cmp-textinput-sm-height)] w-[61px] cursor-pointer border border-solid bg-white text-center text-[12px] font-medium leading-[20px]",
29315
- index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)] -mr-px" : "rounded-r-[var(--cmp-select-sm-radius)]",
29316
- selected ? "z-[1] border-[var(--cmp-timepicker-border-focus)] text-[var(--cmp-timepicker-border-focus)]" : "border-[var(--cmp-timepicker-border-default)] text-[var(--cmp-field-hint-color)]"
29317
- ),
29318
- children: item4 === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
29319
- },
29320
- item4
29321
- );
29322
- }) })
29323
- ]
29324
- }
29325
- ),
29326
- /* @__PURE__ */ jsxRuntime.jsxs(
29327
- "div",
29559
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-nowrap text-center text-[12px] font-bold leading-[20px] text-[var(--cmp-timepicker-selector-range-heading-color)]", children: title }),
29560
+ /* @__PURE__ */ jsxRuntime.jsx(
29561
+ TimeListbox,
29328
29562
  {
29329
- className: "flex items-center justify-center",
29330
- style: {
29331
- gap: "var(--cmp-timepicker-selector-time-gap)",
29332
- padding: "0 var(--cmp-timepicker-selector-section-paddingX)"
29333
- },
29334
- children: [
29335
- renderColumn(side, "hour"),
29336
- /* @__PURE__ */ jsxRuntime.jsx(
29337
- "span",
29338
- {
29339
- className: "text-center",
29340
- style: {
29341
- width: "var(--cmp-timepicker-selector-colon-size)",
29342
- color: "var(--cmp-field-hint-color)",
29343
- fontSize: "var(--cmp-timepicker-selector-colon-size)",
29344
- lineHeight: "var(--cmp-timepicker-selector-colon-size)"
29345
- },
29346
- children: ":"
29347
- }
29348
- ),
29349
- renderColumn(side, "minute")
29350
- ]
29563
+ value: side === "start" ? range[0] : range[1],
29564
+ showMeridiem,
29565
+ minuteStep,
29566
+ labelPrefix: side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC",
29567
+ elevated: false,
29568
+ isDisabled: strict && side === "end" ? (minutes) => minutes < toMinutes(range[0]) : void 0,
29569
+ onChange: (time) => change(side, time)
29351
29570
  }
29352
29571
  )
29353
29572
  ]
@@ -29406,11 +29625,9 @@ function STimeRangePicker({
29406
29625
  style
29407
29626
  }) {
29408
29627
  const portalContainer = usePortalContainer();
29409
- const triggerRef = react.useRef(null);
29410
29628
  const [open, setOpen] = react.useState(false);
29411
29629
  const triggerId = useFieldControlId();
29412
29630
  const triggerTextId = `${triggerId}-text`;
29413
- const [popoverAlignOffset, setPopoverAlignOffset] = react.useState(0);
29414
29631
  const [focused, setFocused] = react.useState(false);
29415
29632
  const [ruleError, setRuleError] = react.useState("");
29416
29633
  const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
@@ -29434,10 +29651,6 @@ function STimeRangePicker({
29434
29651
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
29435
29652
  const commitOpen = (next) => {
29436
29653
  if (disabled) return;
29437
- if (next) {
29438
- const triggerWidth = triggerRef.current?.getBoundingClientRect().width ?? 0;
29439
- setPopoverAlignOffset(Math.max(0, (triggerWidth - TIME_RANGE_SELECTOR_WIDTH) / 2));
29440
- }
29441
29654
  setOpen(next);
29442
29655
  onOpenChange?.(next);
29443
29656
  if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
@@ -29476,7 +29689,6 @@ function STimeRangePicker({
29476
29689
  /* @__PURE__ */ jsxRuntime.jsx(RPopover13__namespace.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
29477
29690
  "div",
29478
29691
  {
29479
- ref: triggerRef,
29480
29692
  id: triggerId,
29481
29693
  role: "button",
29482
29694
  "aria-disabled": disabled,
@@ -29540,9 +29752,8 @@ function STimeRangePicker({
29540
29752
  RPopover13__namespace.Content,
29541
29753
  {
29542
29754
  side: "bottom",
29543
- align: "start",
29755
+ align: "center",
29544
29756
  sideOffset: 4,
29545
- alignOffset: popoverAlignOffset,
29546
29757
  className: cn(
29547
29758
  "z-50 max-w-[var(--radix-popover-content-available-width)]",
29548
29759
  FLOATING_SLIDE_ANIM