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.js CHANGED
@@ -7682,32 +7682,47 @@ var TREE_LABEL_TYPOGRAPHY = {
7682
7682
  rowHeight: "h-[32px]"
7683
7683
  }
7684
7684
  };
7685
- function collectIds(nodes) {
7686
- return nodes.flatMap((node) => [node.id, ...collectIds(node.children ?? [])]);
7687
- }
7688
- function collectSelectableIds(node) {
7689
- if (node.disabled) return [];
7690
- return [node.id, ...(node.children ?? []).flatMap(collectSelectableIds)];
7685
+ var TREE_ROW_CLASS = "group/tree-item box-border flex w-full items-center gap-(--cmp-tree-item-rowGap) overflow-hidden bg-(--cmp-tree-item-bg-default) px-(--cmp-tree-item-paddingX) text-left hover:bg-(--cmp-tree-item-bg-hover)";
7686
+ var TREE_ROW_BORDER_CLASS = "border-b border-(--cmp-tree-item-border-default)";
7687
+ var TREE_ROW_ACTIVE_CLASS = "bg-(--cmp-tree-item-bg-hover)";
7688
+ var TREE_CONTENT_CLASS = "box-border flex min-w-0 flex-1 items-center gap-(--cmp-tree-item-contentGap)";
7689
+ function treeLabelClass(size, {
7690
+ emphasized,
7691
+ disabled,
7692
+ selectable
7693
+ }) {
7694
+ const typography = TREE_LABEL_TYPOGRAPHY[size];
7695
+ return cn(
7696
+ "block min-w-0 flex-1 truncate py-(--cmp-tree-item-paddingY) text-(--cmp-tree-item-text-default)",
7697
+ emphasized ? typography.selected : typography.default,
7698
+ disabled && "opacity-60",
7699
+ disabled ? "cursor-not-allowed" : selectable ? "cursor-pointer" : void 0
7700
+ );
7691
7701
  }
7692
- function toSelectedSet(value) {
7693
- return new Set(value ?? []);
7702
+ function isRowControlClick(event) {
7703
+ const control = event.target.closest("[data-tree-control]");
7704
+ return control != null && event.currentTarget.contains(control);
7694
7705
  }
7695
- var TREE_ALL_NODE = { id: "__all__", label: "\uC804\uCCB4" };
7696
- function getSelectionState(node, selectedSet) {
7697
- const selectableChildren = (node.children ?? []).filter(
7698
- (child) => collectSelectableIds(child).length > 0
7699
- );
7700
- if (selectableChildren.length === 0) {
7701
- return { selected: selectedSet.has(node.id), indeterminate: false };
7702
- }
7703
- const childStates = selectableChildren.map(
7704
- (child) => getSelectionState(child, selectedSet)
7706
+ function TreeCaret({
7707
+ expanded,
7708
+ onToggle
7709
+ }) {
7710
+ return /* @__PURE__ */ jsx(
7711
+ SGhostButton,
7712
+ {
7713
+ icon: expanded ? "caretDown" : "caretRight",
7714
+ size: "xxs",
7715
+ ariaLabel: expanded ? "collapse tree item" : "expand tree item",
7716
+ ariaExpanded: expanded,
7717
+ onClick: (event) => {
7718
+ event.stopPropagation();
7719
+ onToggle(event);
7720
+ }
7721
+ }
7705
7722
  );
7706
- const selectedCount = childStates.filter((state2) => state2.selected).length;
7707
- return {
7708
- selected: selectedCount === childStates.length,
7709
- indeterminate: childStates.some((state2) => state2.indeterminate) || selectedCount > 0 && selectedCount < childStates.length
7710
- };
7723
+ }
7724
+ function TreeCaretSpacer() {
7725
+ return /* @__PURE__ */ jsx("span", { className: "size-(--cmp-ghostButton-xxs-size) shrink-0", "aria-hidden": "true" });
7711
7726
  }
7712
7727
  function TreeConnector({
7713
7728
  guideline,
@@ -7758,6 +7773,83 @@ function TreeConnector({
7758
7773
  ] })
7759
7774
  ] });
7760
7775
  }
7776
+ function TreeAllRow({
7777
+ label = "\uC804\uCCB4",
7778
+ selected,
7779
+ indeterminate = false,
7780
+ size = "sm",
7781
+ border = true,
7782
+ active = false,
7783
+ tabIndex,
7784
+ onKeyDown,
7785
+ onToggle,
7786
+ className,
7787
+ ...rest
7788
+ }) {
7789
+ return /* @__PURE__ */ jsx(
7790
+ "div",
7791
+ {
7792
+ role: "treeitem",
7793
+ "aria-level": 1,
7794
+ "aria-selected": selected,
7795
+ tabIndex,
7796
+ onKeyDown,
7797
+ onClick: (event) => {
7798
+ if (!isRowControlClick(event)) onToggle(event);
7799
+ },
7800
+ className: cn(
7801
+ TREE_ROW_CLASS,
7802
+ "cursor-pointer",
7803
+ TREE_LABEL_TYPOGRAPHY[size].rowHeight,
7804
+ border && TREE_ROW_BORDER_CLASS,
7805
+ active && TREE_ROW_ACTIVE_CLASS,
7806
+ className
7807
+ ),
7808
+ ...rest,
7809
+ children: /* @__PURE__ */ jsxs("div", { className: TREE_CONTENT_CLASS, children: [
7810
+ /* @__PURE__ */ jsx("span", { "data-tree-control": true, className: "flex shrink-0", children: /* @__PURE__ */ jsx(SCheckbox, { value: indeterminate ? null : selected, onValueChange: () => onToggle() }) }),
7811
+ /* @__PURE__ */ jsx(
7812
+ "span",
7813
+ {
7814
+ className: treeLabelClass(size, {
7815
+ emphasized: selected || indeterminate,
7816
+ disabled: false,
7817
+ selectable: false
7818
+ }),
7819
+ children: label
7820
+ }
7821
+ )
7822
+ ] })
7823
+ }
7824
+ );
7825
+ }
7826
+ function collectIds(nodes) {
7827
+ return nodes.flatMap((node) => [node.id, ...collectIds(node.children ?? [])]);
7828
+ }
7829
+ function collectSelectableIds(node) {
7830
+ if (node.disabled) return [];
7831
+ return [node.id, ...(node.children ?? []).flatMap(collectSelectableIds)];
7832
+ }
7833
+ function toSelectedSet(value) {
7834
+ return new Set(value ?? []);
7835
+ }
7836
+ var TREE_ALL_NODE = { id: "__all__", label: "\uC804\uCCB4" };
7837
+ function getSelectionState(node, selectedSet) {
7838
+ const selectableChildren = (node.children ?? []).filter(
7839
+ (child) => collectSelectableIds(child).length > 0
7840
+ );
7841
+ if (selectableChildren.length === 0) {
7842
+ return { selected: selectedSet.has(node.id), indeterminate: false };
7843
+ }
7844
+ const childStates = selectableChildren.map(
7845
+ (child) => getSelectionState(child, selectedSet)
7846
+ );
7847
+ const selectedCount = childStates.filter((state2) => state2.selected).length;
7848
+ return {
7849
+ selected: selectedCount === childStates.length,
7850
+ indeterminate: childStates.some((state2) => state2.indeterminate) || selectedCount > 0 && selectedCount < childStates.length
7851
+ };
7852
+ }
7761
7853
  function TreeBranch({ expanded, children }) {
7762
7854
  const branchRef = useInertRef(!expanded);
7763
7855
  return /* @__PURE__ */ jsx(
@@ -7773,10 +7865,6 @@ function TreeBranch({ expanded, children }) {
7773
7865
  }
7774
7866
  );
7775
7867
  }
7776
- function isRowControlClick(event) {
7777
- const control = event.target.closest("[data-tree-control]");
7778
- return control != null && event.currentTarget.contains(control);
7779
- }
7780
7868
  function STreeAllItem({
7781
7869
  selected,
7782
7870
  indeterminate,
@@ -7787,39 +7875,17 @@ function STreeAllItem({
7787
7875
  onKeyDown,
7788
7876
  onSelect
7789
7877
  }) {
7790
- const checkboxValue = indeterminate ? null : selected;
7791
- const typography = TREE_LABEL_TYPOGRAPHY[size];
7792
- const labelTypo = selected || indeterminate ? typography.selected : typography.default;
7793
7878
  return /* @__PURE__ */ jsx(
7794
- "div",
7879
+ TreeAllRow,
7795
7880
  {
7796
- role: "treeitem",
7797
- "aria-level": 1,
7798
- "aria-selected": selected,
7799
7881
  "data-tree-row": rowId,
7800
7882
  tabIndex,
7801
7883
  onKeyDown,
7802
- onClick: (event) => {
7803
- if (!isRowControlClick(event)) onSelect(event);
7804
- },
7805
- className: cn(
7806
- "group/tree-item box-border flex w-full cursor-pointer items-center overflow-hidden bg-(--cmp-tree-item-bg-default) px-(--cmp-tree-item-paddingX) text-left hover:bg-(--cmp-tree-item-bg-hover)",
7807
- typography.rowHeight,
7808
- border && "border-b border-(--cmp-tree-item-border-default)"
7809
- ),
7810
- children: /* @__PURE__ */ jsxs("div", { className: "box-border flex min-w-0 flex-1 items-center gap-(--cmp-tree-item-contentGap)", children: [
7811
- /* @__PURE__ */ jsx("span", { "data-tree-control": true, className: "flex shrink-0", children: /* @__PURE__ */ jsx(SCheckbox, { value: checkboxValue, onValueChange: () => onSelect() }) }),
7812
- /* @__PURE__ */ jsx(
7813
- "span",
7814
- {
7815
- className: cn(
7816
- "block min-w-0 flex-1 truncate py-(--cmp-tree-item-paddingY) text-(--cmp-tree-item-text-default)",
7817
- labelTypo
7818
- ),
7819
- children: "\uC804\uCCB4"
7820
- }
7821
- )
7822
- ] })
7884
+ selected,
7885
+ indeterminate,
7886
+ border,
7887
+ size,
7888
+ onToggle: onSelect
7823
7889
  }
7824
7890
  );
7825
7891
  }
@@ -7860,8 +7926,6 @@ var STreeItemImpl = /* @__PURE__ */ forwardRef(function STreeItem({
7860
7926
  const leadingNode = typeof leading === "function" ? leading(renderState) : leading;
7861
7927
  const trailingNode = typeof trailing === "function" ? trailing(renderState) : trailing;
7862
7928
  const checkboxValue = indeterminate ? null : selected;
7863
- const typography = TREE_LABEL_TYPOGRAPHY[size];
7864
- const labelTypo = selected || indeterminate ? typography.selected : typography.default;
7865
7929
  const handleSelect = (event) => {
7866
7930
  if (node.disabled) return;
7867
7931
  onTreeSelect?.(node, event);
@@ -7887,10 +7951,9 @@ var STreeItemImpl = /* @__PURE__ */ forwardRef(function STreeItem({
7887
7951
  onKeyDown,
7888
7952
  onClick: handleRowClick,
7889
7953
  className: cn(
7890
- "group/tree-item box-border flex w-full items-center gap-(--cmp-tree-item-rowGap) overflow-hidden bg-(--cmp-tree-item-bg-default) px-(--cmp-tree-item-paddingX) text-left",
7891
- typography.rowHeight,
7892
- border && "border-b border-(--cmp-tree-item-border-default)",
7893
- "hover:bg-(--cmp-tree-item-bg-hover)",
7954
+ TREE_ROW_CLASS,
7955
+ TREE_LABEL_TYPOGRAPHY[size].rowHeight,
7956
+ border && TREE_ROW_BORDER_CLASS,
7894
7957
  hasChildren && "cursor-pointer",
7895
7958
  className
7896
7959
  ),
@@ -7907,25 +7970,8 @@ var STreeItemImpl = /* @__PURE__ */ forwardRef(function STreeItem({
7907
7970
  ancestorLast
7908
7971
  }
7909
7972
  ),
7910
- /* @__PURE__ */ jsxs("div", { className: "box-border flex min-w-0 flex-1 items-center gap-(--cmp-tree-item-contentGap)", children: [
7911
- hasChildren ? /* @__PURE__ */ jsx(
7912
- SGhostButton,
7913
- {
7914
- icon: expanded ? "caretDown" : "caretRight",
7915
- size: "xxs",
7916
- ariaLabel: expanded ? "collapse tree item" : "expand tree item",
7917
- ariaExpanded: expanded,
7918
- onClick: (event) => {
7919
- event.stopPropagation();
7920
- onToggle?.(node, event);
7921
- }
7922
- }
7923
- ) : (
7924
- // 자식이 없는 줄은 펼침 버튼 자리를 빈 칸으로 남겨 라벨의 시작점을 맞춘다.
7925
- // 크기는 자리를 대신하는 그 버튼(`SGhostButton size="xxs"`)의 토큰을 그대로 쓴다 —
7926
- // 버튼 크기가 바뀌면 빈 칸도 같이 따라가야 줄이 어긋나지 않는다.
7927
- /* @__PURE__ */ jsx("span", { className: "size-(--cmp-ghostButton-xxs-size) shrink-0", "aria-hidden": "true" })
7928
- ),
7973
+ /* @__PURE__ */ jsxs("div", { className: TREE_CONTENT_CLASS, children: [
7974
+ hasChildren ? /* @__PURE__ */ jsx(TreeCaret, { expanded, onToggle: (event) => onToggle?.(node, event) }) : /* @__PURE__ */ jsx(TreeCaretSpacer, {}),
7929
7975
  leadingNode,
7930
7976
  selectable && /* @__PURE__ */ jsx(
7931
7977
  "span",
@@ -7947,12 +7993,11 @@ var STreeItemImpl = /* @__PURE__ */ forwardRef(function STreeItem({
7947
7993
  "span",
7948
7994
  {
7949
7995
  "data-tree-label": true,
7950
- className: cn(
7951
- "block min-w-0 flex-1 truncate py-(--cmp-tree-item-paddingY) text-(--cmp-tree-item-text-default)",
7952
- labelTypo,
7953
- node.disabled && "opacity-60",
7954
- node.disabled ? "cursor-not-allowed" : selectable ? "cursor-pointer" : void 0
7955
- ),
7996
+ className: treeLabelClass(size, {
7997
+ emphasized: selected || indeterminate,
7998
+ disabled: Boolean(node.disabled),
7999
+ selectable
8000
+ }),
7956
8001
  children: node.label
7957
8002
  }
7958
8003
  ),
@@ -13394,9 +13439,20 @@ var SystemButton = /* @__PURE__ */ forwardRef(function SystemButton2({
13394
13439
  }
13395
13440
  ),
13396
13441
  label && /* @__PURE__ */ jsx("span", { className: "typo-body-sm-semibold min-w-0 truncate", children: label }),
13397
- count != null && count > 0 && // 개수 오른쪽은 버튼 여백(alertButton.paddingX)만으로는 좁다 — 시안이 4px 을 더 준다.
13442
+ count != null && count > 0 && // 개수는 채운 배지로 단다 — 배경·글자색·치수는 xs deep 태그 토큰이 쥔다(시안).
13443
+ // 배지 색은 고정이다: 흰 면은 파랑, 어두운 면(dark·darker)은 노랑 바탕에 검은 글자.
13444
+ // 배지 오른쪽은 버튼 여백(alertButton.paddingX)만으로는 좁다 — 시안이 4px 을 더 준다.
13398
13445
  // 대응 토큰이 없어 디자인 값을 그대로 쓴다.
13399
- /* @__PURE__ */ jsx("span", { className: "typo-body-md-bold me-[4px] shrink-0", style: { color: accentColor }, children: count.toLocaleString() })
13446
+ /* @__PURE__ */ jsx(
13447
+ STag,
13448
+ {
13449
+ size: "xs",
13450
+ deep: true,
13451
+ color: color === "light" ? "blue" : "yellow",
13452
+ label: count.toLocaleString(),
13453
+ className: "me-[4px] shrink-0"
13454
+ }
13455
+ )
13400
13456
  ]
13401
13457
  }
13402
13458
  );
@@ -14275,7 +14331,7 @@ var SStepper = /* @__PURE__ */ forwardRef(function SStepper2({
14275
14331
  items.findIndex((item4, index) => getItemValue(item4, index) === value)
14276
14332
  );
14277
14333
  const lastCompletedIndex = items.reduce(
14278
- (lastIndex, item4, index) => item4.completed === true ? index : lastIndex,
14334
+ (lastIndex2, item4, index) => item4.completed === true ? index : lastIndex2,
14279
14335
  -1
14280
14336
  );
14281
14337
  const resolvedClickableIndex = lastCompletedIndex + 1;
@@ -15733,13 +15789,13 @@ var SSearchInput = /* @__PURE__ */ forwardRef(function SSearchInput2({
15733
15789
  });
15734
15790
 
15735
15791
  // src/components/SSelect/select.config.ts
15736
- function flattenOptions(options, depth = 1) {
15792
+ function flattenOptions(options, depth = 1, inheritDisabled = true) {
15737
15793
  const out = [];
15738
15794
  for (const o of options) {
15739
15795
  const hasChildren = Array.isArray(o.children) && o.children.length > 0;
15740
15796
  if (hasChildren) {
15741
- const childFlat = flattenOptions(o.children, depth + 1);
15742
- const allChildrenDisabled = childFlat.every((c) => c.disabled);
15797
+ const childFlat = flattenOptions(o.children, depth + 1, inheritDisabled);
15798
+ const allChildrenDisabled = inheritDisabled && childFlat.every((c) => c.disabled);
15743
15799
  out.push({
15744
15800
  value: String(o.value),
15745
15801
  label: o.label,
@@ -15773,12 +15829,71 @@ function collectGroupLeafValues(flat, groupIndex) {
15773
15829
  }
15774
15830
  return values;
15775
15831
  }
15832
+ function visibleTreeRows(rows, isExpanded) {
15833
+ const out = [];
15834
+ const sourceIndex = [];
15835
+ let hiddenBelow = null;
15836
+ rows.forEach((row, i) => {
15837
+ if (hiddenBelow != null && row.depth > hiddenBelow) return;
15838
+ hiddenBelow = null;
15839
+ out.push(row);
15840
+ sourceIndex.push(i);
15841
+ if (row.isGroup && !isExpanded(row)) hiddenBelow = row.depth;
15842
+ });
15843
+ return { rows: out, sourceIndex };
15844
+ }
15845
+ function treeLineInfo(rows) {
15846
+ const isLast = new Array(rows.length);
15847
+ const seen = [];
15848
+ for (let i = rows.length - 1; i >= 0; i--) {
15849
+ const depth = rows[i].depth;
15850
+ isLast[i] = !seen[depth];
15851
+ seen[depth] = true;
15852
+ seen.length = depth + 1;
15853
+ }
15854
+ const ancestors = [];
15855
+ const parents = [];
15856
+ const parentOf = new Array(rows.length);
15857
+ const posInSet = new Array(rows.length);
15858
+ const siblingCount = /* @__PURE__ */ new Map();
15859
+ rows.forEach((row, i) => {
15860
+ const parent = row.depth > 1 ? parents[row.depth - 2] ?? -1 : -1;
15861
+ parentOf[i] = parent;
15862
+ const count = (siblingCount.get(parent) ?? 0) + 1;
15863
+ siblingCount.set(parent, count);
15864
+ posInSet[i] = count;
15865
+ parents.length = row.depth - 1;
15866
+ parents[row.depth - 1] = i;
15867
+ });
15868
+ return rows.map((row, i) => {
15869
+ ancestors.length = row.depth - 1;
15870
+ const ancestorLast = ancestors.slice();
15871
+ ancestors[row.depth - 1] = isLast[i];
15872
+ return {
15873
+ isLast: isLast[i],
15874
+ ancestorLast,
15875
+ posInSet: posInSet[i],
15876
+ setSize: siblingCount.get(parentOf[i]) ?? 1
15877
+ };
15878
+ });
15879
+ }
15880
+ function ancestorGroupValues(flat, selected) {
15881
+ const out = /* @__PURE__ */ new Set();
15882
+ const stack = [];
15883
+ for (const row of flat) {
15884
+ while (stack.length > 0 && stack[stack.length - 1].depth >= row.depth) stack.pop();
15885
+ if (selected.has(row.value)) for (const group of stack) out.add(group.value);
15886
+ if (row.isGroup) stack.push(row);
15887
+ }
15888
+ return out;
15889
+ }
15776
15890
  var isHtmlLabel = (label) => /<[a-z][\s\S]*>/i.test(label);
15777
15891
  var extractText = (html) => html.replace(/<[^>]*>/g, "");
15778
15892
  var OPTION_ROW_HEIGHT = 28;
15779
15893
  var DEFAULT_VIRTUAL_BUFFER = 10;
15780
15894
  var DEFAULT_REACH_END_THRESHOLD = 10;
15781
15895
  var DEFAULT_SEARCH_DEBOUNCE = 300;
15896
+ var NO_SEARCH_COLLAPSED = /* @__PURE__ */ new Set();
15782
15897
  var SEARCH_THRESHOLD = 11;
15783
15898
  function isListSized(el) {
15784
15899
  return el.clientHeight > 0 && getComputedStyle(el).maxHeight !== "none";
@@ -15833,8 +15948,9 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15833
15948
  className,
15834
15949
  style
15835
15950
  }, ref) {
15836
- const multi = type === "multi" || type === "multi_depth";
15951
+ const multi = type === "multi" || type === "multi_depth" || type === "multi_tree";
15837
15952
  const isDepth = type === "default_depth" || type === "multi_depth";
15953
+ const isTree = type === "tree" || type === "multi_tree";
15838
15954
  const pagingEnabled = onReachEnd != null;
15839
15955
  const partialOptions = pagingEnabled || serverSearch;
15840
15956
  const portalContainer = usePortalContainer();
@@ -15844,6 +15960,8 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15844
15960
  const [open, setOpen] = useState(false);
15845
15961
  const [pendingOpen, setPendingOpen] = useState(false);
15846
15962
  const [focusedIndex, setFocusedIndex] = useState(-1);
15963
+ const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
15964
+ const [searchCollapsed, setSearchCollapsed] = useState(NO_SEARCH_COLLAPSED);
15847
15965
  const baseId = useId();
15848
15966
  const listboxId = `${baseId}-listbox`;
15849
15967
  const activeOptionId = `${baseId}-active-option`;
@@ -15867,10 +15985,11 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15867
15985
  const alignSelectedTopRef = useRef(false);
15868
15986
  const selectedAnchorRef = useRef(-1);
15869
15987
  const rangeAnchorRef = useRef(null);
15870
- const flat = useMemo(() => flattenOptions(options), [options]);
15988
+ const groupIsValue = type === "tree";
15989
+ const flat = useMemo(() => flattenOptions(options, 1, !groupIsValue), [groupIsValue, options]);
15871
15990
  const selectable = useMemo(() => selectableOptions(flat), [flat]);
15872
15991
  const hasLeaf = useMemo(() => flat.some((o) => !o.isGroup), [flat]);
15873
- const allOptionsDisabled = hasLeaf && selectable.length === 0;
15992
+ const allOptionsDisabled = groupIsValue ? flat.length > 0 && flat.every((o) => o.disabled) : hasLeaf && selectable.length === 0;
15874
15993
  const effectiveDisabled = disabled || allOptionsDisabled;
15875
15994
  const searchVisible = showSearch && (partialOptions || selectable.length >= SEARCH_THRESHOLD);
15876
15995
  const labelByValue = useMemo(() => new Map(flat.map((o) => [o.value, o.label])), [flat]);
@@ -15913,6 +16032,10 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15913
16032
  if (next) {
15914
16033
  dismissedByPointerRef.current = false;
15915
16034
  alignSelectedTopRef.current = true;
16035
+ if (isTree) {
16036
+ setExpanded(ancestorGroupValues(flat, new Set(selectedRef.current)));
16037
+ setSearchCollapsed(NO_SEARCH_COLLAPSED);
16038
+ }
15916
16039
  } else {
15917
16040
  setPendingOpen(false);
15918
16041
  setSearch("");
@@ -15955,7 +16078,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15955
16078
  },
15956
16079
  resetValidation: () => setRuleError("")
15957
16080
  });
15958
- const filtered = useMemo(() => {
16081
+ const searched = useMemo(() => {
15959
16082
  if (serverSearch || !searchVisible || search.trim() === "") return flat;
15960
16083
  const q = search.trim().toLowerCase();
15961
16084
  return filterDepthRows(
@@ -15963,23 +16086,53 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15963
16086
  (o) => (isHtmlLabel(o.label) ? extractText(o.label) : o.label).toLowerCase().includes(q)
15964
16087
  );
15965
16088
  }, [flat, search, searchVisible, serverSearch]);
16089
+ const treeSearching = isTree && search.trim() !== "";
16090
+ const isGroupExpanded = useCallback(
16091
+ (groupValue) => treeSearching ? !searchCollapsed.has(groupValue) : expanded.has(groupValue),
16092
+ [expanded, searchCollapsed, treeSearching]
16093
+ );
16094
+ const treeRows = useMemo(
16095
+ () => isTree ? visibleTreeRows(searched, (row) => isGroupExpanded(row.value)) : null,
16096
+ [isGroupExpanded, isTree, searched]
16097
+ );
16098
+ const filtered = treeRows?.rows ?? searched;
16099
+ const treeLines = useMemo(() => treeRows ? treeLineInfo(treeRows.rows) : [], [treeRows]);
15966
16100
  const allValues = selectable.map((o) => o.value);
15967
16101
  const allSelected = !partialOptions && allValues.length > 0 && allValues.every((v) => selectedSet.has(v));
15968
16102
  const selectAllVisible = multi && showSelectAll && !partialOptions && allValues.length > 0;
15969
- const hasItems = filtered.some((o) => !o.isGroup);
16103
+ const hasItems = isTree ? filtered.length > 0 : filtered.some((o) => !o.isGroup);
15970
16104
  const groupLeafValues = useMemo(() => {
15971
16105
  const map = /* @__PURE__ */ new Map();
15972
16106
  filtered.forEach((o, i) => {
15973
- if (o.isGroup) map.set(i, collectGroupLeafValues(filtered, i));
16107
+ if (!o.isGroup) return;
16108
+ map.set(
16109
+ i,
16110
+ treeRows ? collectGroupLeafValues(searched, treeRows.sourceIndex[i]) : collectGroupLeafValues(filtered, i)
16111
+ );
15974
16112
  });
15975
16113
  return map;
15976
- }, [filtered]);
16114
+ }, [filtered, searched, treeRows]);
15977
16115
  const navigableItems = useMemo(() => {
15978
16116
  const items = [];
15979
16117
  if (selectAllVisible) {
15980
16118
  items.push({ key: "__select_all__", isSelectAll: true });
15981
16119
  }
15982
16120
  filtered.forEach((o, i) => {
16121
+ if (o.isGroup && isTree) {
16122
+ if (multi) {
16123
+ const leafValues = groupLeafValues.get(i) ?? [];
16124
+ items.push({
16125
+ key: `__group_${i}__`,
16126
+ isGroup: true,
16127
+ index: i,
16128
+ leafValues,
16129
+ disabled: o.disabled || leafValues.length === 0
16130
+ });
16131
+ } else {
16132
+ items.push({ key: o.value, value: o.value, index: i, disabled: o.disabled });
16133
+ }
16134
+ return;
16135
+ }
15983
16136
  if (o.disabled) return;
15984
16137
  if (o.isGroup) {
15985
16138
  if (!multi) return;
@@ -15991,7 +16144,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
15991
16144
  }
15992
16145
  });
15993
16146
  return items;
15994
- }, [filtered, groupLeafValues, multi, selectAllVisible]);
16147
+ }, [filtered, groupLeafValues, isTree, multi, selectAllVisible]);
15995
16148
  const selectedAnchorIndex = useMemo(() => {
15996
16149
  if (selectedSet.size === 0) return -1;
15997
16150
  return navigableItems.findIndex((item4) => item4.value != null && selectedSet.has(item4.value));
@@ -16002,7 +16155,10 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16002
16155
  const rowCount = filtered.length;
16003
16156
  const metrics = metricsRef.current;
16004
16157
  const lastSearchRef = useRef(search);
16005
- if (lastSearchRef.current !== search) {
16158
+ const lastExpansionRef = useRef({ expanded, searchCollapsed });
16159
+ const expansionChanged = lastExpansionRef.current.expanded !== expanded || lastExpansionRef.current.searchCollapsed !== searchCollapsed;
16160
+ lastExpansionRef.current = { expanded, searchCollapsed };
16161
+ if (lastSearchRef.current !== search || isTree && expansionChanged) {
16006
16162
  lastSearchRef.current = search;
16007
16163
  resetRowMetrics(metrics, rowCount);
16008
16164
  measuredKeyRef.current = "";
@@ -16142,7 +16298,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16142
16298
  );
16143
16299
  const fullGroupByLeaf = useMemo(() => {
16144
16300
  const map = /* @__PURE__ */ new Map();
16145
- if (!showChips || type !== "multi_depth") return map;
16301
+ if (!showChips || type !== "multi_depth" && type !== "multi_tree") return map;
16146
16302
  flat.forEach((o, i) => {
16147
16303
  if (!o.isGroup) return;
16148
16304
  const leafValues = collectGroupLeafValues(flat, i);
@@ -16220,6 +16376,21 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16220
16376
  rangeAnchorRef.current = null;
16221
16377
  emit3(allSelected ? [] : allValues);
16222
16378
  };
16379
+ const handleSearchChange = (next) => {
16380
+ setSearch(next);
16381
+ setSearchCollapsed(NO_SEARCH_COLLAPSED);
16382
+ };
16383
+ const handleToggleExpand = (groupValue) => {
16384
+ rangeAnchorRef.current = null;
16385
+ const toggle = (prev) => {
16386
+ const next = new Set(prev);
16387
+ if (next.has(groupValue)) next.delete(groupValue);
16388
+ else next.add(groupValue);
16389
+ return next;
16390
+ };
16391
+ if (treeSearching) setSearchCollapsed(toggle);
16392
+ else setExpanded(toggle);
16393
+ };
16223
16394
  const handleToggleGroup = (leafValues) => {
16224
16395
  if (leafValues.length === 0) return;
16225
16396
  rangeAnchorRef.current = null;
@@ -16292,6 +16463,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16292
16463
  const selectFocusedItem = (extend = false) => {
16293
16464
  if (focusedIndex < 0 || focusedIndex >= navigableItems.length) return;
16294
16465
  const focused = navigableItems[focusedIndex];
16466
+ if (focused.disabled) return;
16295
16467
  if (focused.isSelectAll) {
16296
16468
  handleSelectAll();
16297
16469
  return;
@@ -16320,6 +16492,41 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16320
16492
  moveFocusedIndex("prev");
16321
16493
  return;
16322
16494
  }
16495
+ const input = event.target instanceof HTMLInputElement ? event.target : null;
16496
+ const caretAtEdge = input == null || input.selectionStart === input.selectionEnd && input.selectionStart === (event.key === "ArrowRight" ? input.value.length : 0);
16497
+ if (isTree && (event.key === "ArrowRight" || event.key === "ArrowLeft") && caretAtEdge) {
16498
+ const rowIndex = navigableItems[focusedIndex]?.index;
16499
+ const row = rowIndex != null ? filtered[rowIndex] : void 0;
16500
+ if (rowIndex == null || row == null) return;
16501
+ event.preventDefault();
16502
+ const rowExpanded = row.isGroup && isGroupExpanded(row.value);
16503
+ if (event.key === "ArrowRight") {
16504
+ if (!row.isGroup) return;
16505
+ if (!rowExpanded) {
16506
+ handleToggleExpand(row.value);
16507
+ return;
16508
+ }
16509
+ for (let i = rowIndex + 1; i < filtered.length && filtered[i].depth > row.depth; i++) {
16510
+ const child = navigableItems.findIndex((item4) => item4.index === i);
16511
+ if (child >= 0) {
16512
+ setFocusedIndex(child);
16513
+ break;
16514
+ }
16515
+ }
16516
+ return;
16517
+ }
16518
+ if (rowExpanded) {
16519
+ handleToggleExpand(row.value);
16520
+ return;
16521
+ }
16522
+ for (let i = rowIndex - 1; i >= 0; i--) {
16523
+ if (filtered[i].depth >= row.depth) continue;
16524
+ const parent = navigableItems.findIndex((item4) => item4.index === i);
16525
+ if (parent >= 0) setFocusedIndex(parent);
16526
+ return;
16527
+ }
16528
+ return;
16529
+ }
16323
16530
  if (event.key === "Enter") {
16324
16531
  event.preventDefault();
16325
16532
  selectFocusedItem(event.shiftKey);
@@ -16364,7 +16571,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16364
16571
  type: "button",
16365
16572
  disabled: effectiveDisabled,
16366
16573
  "aria-labelledby": label ? `${fieldLabelId(triggerId)} ${triggerId}` : void 0,
16367
- "aria-haspopup": "listbox",
16574
+ "aria-haspopup": isTree ? "tree" : "listbox",
16368
16575
  "aria-expanded": open,
16369
16576
  "aria-controls": open ? listboxId : void 0,
16370
16577
  "aria-activedescendant": open && focusedIndex >= 0 ? activeOptionId : void 0,
@@ -16501,7 +16708,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16501
16708
  {
16502
16709
  ref: searchInputRef,
16503
16710
  value: search,
16504
- onValueChange: setSearch,
16711
+ onValueChange: handleSearchChange,
16505
16712
  onKeyDown: handleKeyDown,
16506
16713
  placeholder: "\uAC80\uC0C9",
16507
16714
  clearable: true,
@@ -16520,8 +16727,12 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16520
16727
  indexOffset: windowStart,
16521
16728
  topSpacer: win.top,
16522
16729
  bottomSpacer: win.bottom,
16523
- setSize: isDepth ? void 0 : rowCount,
16730
+ setSize: isDepth || isTree ? void 0 : rowCount,
16524
16731
  multi,
16732
+ tree: isTree,
16733
+ isExpanded: isGroupExpanded,
16734
+ onToggleExpand: handleToggleExpand,
16735
+ treeLines,
16525
16736
  selected,
16526
16737
  groupLeafValues,
16527
16738
  focusedValue: navigableItems[focusedIndex]?.key,
@@ -16575,6 +16786,10 @@ function ItemList({
16575
16786
  bottomSpacer,
16576
16787
  setSize,
16577
16788
  multi,
16789
+ tree,
16790
+ isExpanded,
16791
+ onToggleExpand,
16792
+ treeLines,
16578
16793
  selected,
16579
16794
  groupLeafValues,
16580
16795
  focusedValue,
@@ -16588,14 +16803,34 @@ function ItemList({
16588
16803
  onHoverValue
16589
16804
  }) {
16590
16805
  const selectedSet = useMemo(() => new Set(selected), [selected]);
16806
+ const checkboxShiftRef = useRef(false);
16591
16807
  const itemBase = cn(
16592
16808
  "group/select-item flex w-full cursor-pointer select-none items-center gap-[8px] border-none bg-transparent text-left text-[12px] leading-[20px] outline-none",
16593
16809
  "text-[color:var(--cmp-listBoxItem-content-default)]",
16594
16810
  "disabled:cursor-not-allowed disabled:text-[color:var(--cmp-listBoxItem-content-disabled)]"
16595
16811
  );
16596
16812
  const activeClass = "hover:bg-[var(--cmp-listBoxItem-bg-hover)] hover:text-[color:var(--cmp-listBoxItem-content-hover)] focus-visible:bg-[var(--cmp-listBoxItem-bg-hover)] focus-visible:text-[color:var(--cmp-listBoxItem-content-hover)]";
16597
- return /* @__PURE__ */ jsxs("div", { id: listboxId, role: "listbox", "aria-multiselectable": multi || void 0, children: [
16598
- showSelectAll && /* @__PURE__ */ jsxs(
16813
+ return /* @__PURE__ */ jsxs("div", { id: listboxId, role: tree ? "tree" : "listbox", "aria-multiselectable": multi || void 0, children: [
16814
+ showSelectAll && (tree ? (
16815
+ // 트리의 전체선택 줄은 STree 의 "전체" 줄 그대로다 (lib/tree-row).
16816
+ /* @__PURE__ */ jsx(
16817
+ TreeAllRow,
16818
+ {
16819
+ "data-select-all-row": true,
16820
+ id: focusedValue === "__select_all__" ? activeOptionId : void 0,
16821
+ "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
16822
+ "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
16823
+ tabIndex: -1,
16824
+ label: selectAllLabel,
16825
+ selected: selectAllSelected,
16826
+ active: focusedValue === "__select_all__",
16827
+ className: "select-none",
16828
+ onMouseEnter: () => onHoverValue("__select_all__"),
16829
+ onMouseLeave: () => onHoverValue(null),
16830
+ onToggle: onSelectAll
16831
+ }
16832
+ )
16833
+ ) : /* @__PURE__ */ jsxs(
16599
16834
  "button",
16600
16835
  {
16601
16836
  type: "button",
@@ -16616,12 +16851,109 @@ function ItemList({
16616
16851
  /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: selectAllLabel })
16617
16852
  ]
16618
16853
  }
16619
- ),
16854
+ )),
16620
16855
  topSpacer > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: { height: topSpacer } }),
16621
16856
  items.map((o, windowIndex) => {
16622
16857
  const idx = windowIndex + indexOffset;
16623
16858
  const padLeft = o.depth === 1 ? 12 : o.depth === 2 ? 20 : 28;
16624
16859
  const rowKey = `${o.value}-${idx}`;
16860
+ if (tree) {
16861
+ const togglesLeaves = multi && o.isGroup;
16862
+ const leafValues = togglesLeaves ? groupLeafValues.get(idx) ?? [] : [];
16863
+ const total = leafValues.length;
16864
+ const selectedCount = leafValues.reduce((n, v) => selectedSet.has(v) ? n + 1 : n, 0);
16865
+ const isSelected2 = selectedSet.has(o.value);
16866
+ const checkState = togglesLeaves ? total === 0 || selectedCount === 0 ? false : selectedCount === total ? true : null : isSelected2;
16867
+ const rowDisabled = o.disabled || togglesLeaves && total === 0;
16868
+ const focusKey = togglesLeaves ? `__group_${idx}__` : o.value;
16869
+ const focused = focusedValue === focusKey;
16870
+ const expanded = o.isGroup && isExpanded(o.value);
16871
+ const line = treeLines[idx];
16872
+ const toggle = () => {
16873
+ if (rowDisabled) return;
16874
+ if (togglesLeaves) onToggleGroup(leafValues);
16875
+ else onSelect(o.value, idx, checkboxShiftRef.current);
16876
+ };
16877
+ return (
16878
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events -- 키보드는 트리거(또는 검색 입력)가 aria-activedescendant 로 맡는다. 줄로 포커스를 옮기지 않는다
16879
+ /* @__PURE__ */ jsxs(
16880
+ "div",
16881
+ {
16882
+ tabIndex: -1,
16883
+ "data-select-row": true,
16884
+ "data-index": idx,
16885
+ id: focused ? activeOptionId : void 0,
16886
+ role: "treeitem",
16887
+ "aria-level": o.depth,
16888
+ "aria-posinset": line?.posInSet,
16889
+ "aria-setsize": line?.setSize,
16890
+ "aria-expanded": o.isGroup ? expanded : void 0,
16891
+ "aria-selected": checkState === true,
16892
+ "aria-disabled": rowDisabled || void 0,
16893
+ "data-select-focused": focused ? "true" : void 0,
16894
+ "data-select-active": focused ? "true" : void 0,
16895
+ className: cn(
16896
+ TREE_ROW_CLASS,
16897
+ TREE_LABEL_TYPOGRAPHY.sm.rowHeight,
16898
+ TREE_ROW_BORDER_CLASS,
16899
+ "select-none",
16900
+ rowDisabled ? "cursor-not-allowed" : "cursor-pointer",
16901
+ focused && TREE_ROW_ACTIVE_CLASS
16902
+ ),
16903
+ onMouseEnter: () => onHoverValue(focusKey),
16904
+ onMouseLeave: () => onHoverValue(null),
16905
+ onClick: (event) => {
16906
+ if (isRowControlClick(event) || rowDisabled) return;
16907
+ if (togglesLeaves) onToggleGroup(leafValues);
16908
+ else onSelect(o.value, idx, event.shiftKey);
16909
+ },
16910
+ children: [
16911
+ /* @__PURE__ */ jsx(
16912
+ TreeConnector,
16913
+ {
16914
+ guideline: true,
16915
+ size: "sm",
16916
+ depth: o.depth,
16917
+ hasChildren: o.isGroup,
16918
+ isLast: line?.isLast ?? true,
16919
+ ancestorLast: line?.ancestorLast ?? []
16920
+ }
16921
+ ),
16922
+ /* @__PURE__ */ jsxs("div", { className: TREE_CONTENT_CLASS, children: [
16923
+ o.isGroup ? (
16924
+ // 비활성 그룹도 접고 편다 — 그룹이 비활성이어도 아래에 고를 수 있는 자식이 있을 수 있다.
16925
+ /* @__PURE__ */ jsx(TreeCaret, { expanded, onToggle: () => onToggleExpand(o.value) })
16926
+ ) : /* @__PURE__ */ jsx(TreeCaretSpacer, {}),
16927
+ multi && /* @__PURE__ */ jsx(
16928
+ "span",
16929
+ {
16930
+ "data-tree-control": true,
16931
+ className: "flex shrink-0",
16932
+ onPointerDownCapture: (event) => {
16933
+ checkboxShiftRef.current = event.shiftKey;
16934
+ },
16935
+ children: /* @__PURE__ */ jsx(SCheckbox, { value: checkState, disabled: rowDisabled, onValueChange: toggle })
16936
+ }
16937
+ ),
16938
+ /* @__PURE__ */ jsx(
16939
+ OptionLabel,
16940
+ {
16941
+ label: o.label,
16942
+ className: treeLabelClass("sm", {
16943
+ // 고른 줄은 굵게 — multi_tree 는 일부만 고른 그룹도 굵다 (STree 와 같다)
16944
+ emphasized: checkState !== false,
16945
+ disabled: rowDisabled,
16946
+ selectable: true
16947
+ })
16948
+ }
16949
+ )
16950
+ ] })
16951
+ ]
16952
+ },
16953
+ rowKey
16954
+ )
16955
+ );
16956
+ }
16625
16957
  if (o.isGroup) {
16626
16958
  const isDepth1 = o.depth === 1;
16627
16959
  const isFirstRow = idx === 0 && !showSelectAll && !searchVisible;
@@ -16662,13 +16994,7 @@ function ItemList({
16662
16994
  onClick: () => onToggleGroup(leafValues),
16663
16995
  children: [
16664
16996
  /* @__PURE__ */ jsx(CheckboxBox, { state: groupState, disabled: groupDisabled, focused: groupFocused }),
16665
- isHtmlLabel(o.label) ? /* @__PURE__ */ jsx(
16666
- "span",
16667
- {
16668
- className: "min-w-0 truncate",
16669
- dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(o.label) }
16670
- }
16671
- ) : /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: o.label }),
16997
+ /* @__PURE__ */ jsx(OptionLabel, { label: o.label, className: "min-w-0 truncate" }),
16672
16998
  total > 0 && /* @__PURE__ */ jsxs(
16673
16999
  "span",
16674
17000
  {
@@ -16745,13 +17071,7 @@ function ItemList({
16745
17071
  focused: focusedValue === o.value
16746
17072
  }
16747
17073
  ),
16748
- isHtmlLabel(o.label) ? /* @__PURE__ */ jsx(
16749
- "span",
16750
- {
16751
- className: "flex-1 truncate",
16752
- dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(o.label) }
16753
- }
16754
- ) : /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: o.label })
17074
+ /* @__PURE__ */ jsx(OptionLabel, { label: o.label, className: "flex-1 truncate" })
16755
17075
  ]
16756
17076
  },
16757
17077
  rowKey
@@ -16760,8 +17080,11 @@ function ItemList({
16760
17080
  bottomSpacer > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: { height: bottomSpacer } })
16761
17081
  ] });
16762
17082
  }
17083
+ function OptionLabel({ label, className }) {
17084
+ return isHtmlLabel(label) ? /* @__PURE__ */ jsx("span", { className, dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsx("span", { className, children: label });
17085
+ }
16763
17086
  function TriggerLabel({ label }) {
16764
- return isHtmlLabel(label) ? /* @__PURE__ */ jsx("span", { className: "truncate", dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsx("span", { className: "truncate", children: label });
17087
+ return /* @__PURE__ */ jsx(OptionLabel, { label, className: "truncate" });
16765
17088
  }
16766
17089
  function TriggerRemoveButton({ ariaLabel, onRemove }) {
16767
17090
  return /* @__PURE__ */ jsxs(
@@ -17016,8 +17339,8 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17016
17339
  const [rowCountOverride, setRowCountOverride] = useState(null);
17017
17340
  const rowCount = rowCountOverride ?? rows.length;
17018
17341
  const scrollRef = useRef(null);
17019
- const headerScrollRef = useRef(null);
17020
- const [headerGutter, setHeaderGutter] = useState(0);
17342
+ const headerRef = useRef(null);
17343
+ const [headerHeight, setHeaderHeight] = useState(0);
17021
17344
  const [vsRange, setVsRange] = useState({ start: 0, end: 0 });
17022
17345
  const lastReachEndRef = useRef(-1);
17023
17346
  const cbRef = useRef({ onVirtualUpdate, onVirtualReachEnd, onScroll });
@@ -17059,17 +17382,13 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17059
17382
  );
17060
17383
  const handleScroll = useCallback(() => {
17061
17384
  const el = scrollRef.current;
17062
- if (!el) {
17063
- setHeaderGutter(0);
17064
- return;
17065
- }
17066
- const header = headerScrollRef.current;
17067
- if (header) header.scrollLeft = el.scrollLeft;
17068
- setHeaderGutter(el.offsetWidth - el.clientWidth);
17385
+ if (!el) return;
17386
+ const headerH = headerRef.current?.offsetHeight ?? 0;
17387
+ setHeaderHeight(headerH);
17069
17388
  setScrolledLeft(el.scrollLeft > 0);
17070
17389
  setScrolledRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
17071
17390
  if (virtualScroll) {
17072
- const { start, end } = calcRange(el.scrollTop, el.clientHeight);
17391
+ const { start, end } = calcRange(el.scrollTop, el.clientHeight - headerH);
17073
17392
  setVsRange((prev) => prev.start === start && prev.end === end ? prev : { start, end });
17074
17393
  }
17075
17394
  }, [virtualScroll, calcRange]);
@@ -17466,7 +17785,7 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17466
17785
  );
17467
17786
  };
17468
17787
  const bodyScrollClass = cn(
17469
- "min-h-0 flex-1 overflow-auto border-t border-solid border-[color:var(--cmp-table-border-color)]",
17788
+ "min-h-0 flex-1 overflow-auto",
17470
17789
  SCROLLBAR_CLASS,
17471
17790
  SCROLLBAR_TRACK_BG_CLASS,
17472
17791
  SCROLLBAR_TRACK_BORDER_CLASS
@@ -17483,23 +17802,221 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17483
17802
  selectable && /* @__PURE__ */ jsx("col", { style: { width: SELECTABLE_COLUMN_WIDTH } }),
17484
17803
  visibleCols.map((col, i) => /* @__PURE__ */ jsx("col", { style: col.autoWidth ? void 0 : { width: columnWidths[i] } }, col.name))
17485
17804
  ] });
17486
- return /* @__PURE__ */ jsxs(
17487
- "div",
17805
+ const headerTable = /* @__PURE__ */ jsxs(
17806
+ "table",
17488
17807
  {
17489
- className: cn("flex flex-col", className),
17490
- style: { width, ...style },
17491
- "data-table-id": resolvedTableId,
17808
+ className: "w-full table-fixed border-separate border-spacing-0 border-b border-solid border-[color:var(--cmp-table-border-color)] text-left",
17809
+ style: { fontSize: 12 },
17492
17810
  children: [
17493
- /* @__PURE__ */ jsxs(
17494
- "div",
17495
- {
17496
- className: cn(
17497
- // isolate: no-data/loading 오버레이(z-30·31)를 테이블 안에서 끝낸다. relative + z-auto
17498
- // 만으로는 스태킹 컨텍스트가 생기지 않아, 모달 안에 들어가면 30·31이 모달 Content
17499
- // 레벨까지 올라가 같은 컨텍스트의 floating portal 밴드(z-2)를 덮는다.
17500
- // 자손은 이미 overflow-hidden으로 클리핑되므로 테이블 내부 순서는 달라지지 않는다.
17501
- "bg-white relative isolate flex flex-col overflow-hidden",
17502
- bordered && "border border-solid border-[color:var(--cmp-table-border-color)]",
17811
+ colgroup,
17812
+ /* @__PURE__ */ jsxs("thead", { className: cn(isNoData && "opacity-40"), children: [
17813
+ headerGroupInfo && /* @__PURE__ */ jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17814
+ selectable && /* @__PURE__ */ jsx(
17815
+ "th",
17816
+ {
17817
+ rowSpan: 2,
17818
+ className: cn(
17819
+ "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17820
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17821
+ ),
17822
+ style: {
17823
+ width: SELECTABLE_COLUMN_WIDTH,
17824
+ minWidth: SELECTABLE_COLUMN_WIDTH,
17825
+ maxWidth: SELECTABLE_COLUMN_WIDTH
17826
+ },
17827
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(
17828
+ SCheckbox,
17829
+ {
17830
+ value: headerCheck,
17831
+ disabled: !hasSelectableRow,
17832
+ onValueChange: toggleAll
17833
+ }
17834
+ ) })
17835
+ }
17836
+ ),
17837
+ headerGroupInfo.cells.map((group) => /* @__PURE__ */ jsxs(
17838
+ "th",
17839
+ {
17840
+ colSpan: group.colSpan,
17841
+ rowSpan: group.rowSpan,
17842
+ className: cn(
17843
+ "relative border-b border-r border-solid border-[color:var(--cmp-table-border-color)] bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[20px] text-[color:var(--sys-color-fg-primary)]",
17844
+ group.firstIndex + group.colSpan === visibleCols.length && "border-r-0",
17845
+ group.rowSpan === 2 && "border-b-0",
17846
+ group.className
17847
+ ),
17848
+ style: { textAlign: group.align ?? "left", ...group.style },
17849
+ children: [
17850
+ group.render ? group.render() : group.label,
17851
+ group.rowSpan === 2 && group.colSpan === 1 ? headerResizeHitArea(group.firstIndex, `group-resizer-${group.columnNames[0]}`) : null
17852
+ ]
17853
+ },
17854
+ group.columnNames.join("|")
17855
+ ))
17856
+ ] }),
17857
+ /* @__PURE__ */ jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17858
+ selectable && !headerGroupInfo && /* @__PURE__ */ jsxs(
17859
+ "th",
17860
+ {
17861
+ className: cn(
17862
+ // 원본 .th--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
17863
+ // 하단 border는 헤더 표가 연속으로 그리므로 셀에는 두지 않는다.
17864
+ "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17865
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17866
+ ),
17867
+ style: {
17868
+ width: SELECTABLE_COLUMN_WIDTH,
17869
+ minWidth: SELECTABLE_COLUMN_WIDTH,
17870
+ maxWidth: SELECTABLE_COLUMN_WIDTH
17871
+ },
17872
+ children: [
17873
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(
17874
+ SCheckbox,
17875
+ {
17876
+ value: headerCheck,
17877
+ disabled: !hasSelectableRow,
17878
+ onValueChange: toggleAll
17879
+ }
17880
+ ) }),
17881
+ /* @__PURE__ */ jsx(
17882
+ "span",
17883
+ {
17884
+ "aria-hidden": true,
17885
+ className: "pointer-events-none absolute right-0 top-1/2 h-[16px] w-px -translate-y-1/2 bg-[var(--cmp-table-header-resizingBar-color)]"
17886
+ }
17887
+ )
17888
+ ]
17889
+ }
17890
+ ),
17891
+ visibleCols.map((col, i) => {
17892
+ if (headerGroupInfo?.rowSpanNames.has(col.name)) return null;
17893
+ const isSortable = !!col.sortable && !col.renderHeader;
17894
+ const sortDir = isSortable && sort?.name === col.name ? sort.dir : void 0;
17895
+ return /* @__PURE__ */ jsxs(
17896
+ "th",
17897
+ {
17898
+ className: cn(
17899
+ // 하단 border는 헤더 표가 연속으로 그린다.
17900
+ // leading-[0]: th 의 스트럿(line-height 만큼의 빈 인라인 박스)을 없앤다.
17901
+ // 내용이 inline-flex 라 스트럿이 남아 있으면 라벨보다 큰 컨트롤(정렬 버튼 등)이
17902
+ // 들어올 때 줄 상자가 베이스라인 기준으로 늘어나 헤더 높이를 넘기고
17903
+ // 세로 가운데도 어긋난다. 글자 줄높이는 아래 th__content 가 다시 준다.
17904
+ headerGroupInfo && "border-r border-solid border-[color:var(--cmp-table-border-color)]",
17905
+ headerGroupInfo && i === visibleCols.length - 1 && "border-r-0",
17906
+ "relative bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[0] text-[color:var(--sys-color-fg-primary)]",
17907
+ stickyInfo(i).isSticky && "z-[6]",
17908
+ col.thClass
17909
+ ),
17910
+ style: {
17911
+ textAlign: col.align ?? "left",
17912
+ boxShadow: stickyShadowValue(i) ?? void 0,
17913
+ ...cellStyle(i),
17914
+ ...col.thStyle
17915
+ },
17916
+ "aria-sort": isSortable ? sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none" : void 0,
17917
+ children: [
17918
+ /* @__PURE__ */ jsx(
17919
+ "span",
17920
+ {
17921
+ className: cn(
17922
+ // 원본 th__content: align에 따라 justify-content 적용 (th도 정렬 반영)
17923
+ // th 가 leading-[0] 이므로 글자 줄높이는 여기서 준다 (truncate 가
17924
+ // overflow-hidden 이라 줄높이가 0이면 라벨이 잘려 사라진다).
17925
+ "inline-flex w-full min-w-0 items-center gap-[4px] leading-[20px]",
17926
+ col.align === "center" && "justify-center",
17927
+ col.align === "right" && "justify-end"
17928
+ ),
17929
+ children: col.renderHeader ? col.renderHeader(col, i) : /* @__PURE__ */ jsxs(Fragment, { children: [
17930
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: col.label }),
17931
+ col.helpText && col.helpText.length > 0 && // 라벨이 좁아져 truncate 되더라도 아이콘까지 함께 줄어들면 안 되므로
17932
+ // shrink-0 래퍼로 감싼다 (STooltip 의 className 은 본문 쪽으로 간다).
17933
+ // 아이콘·크기·색은 SKeyValueTable 헤더 도움말과 동일하게 맞춘다.
17934
+ /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx(
17935
+ STooltip2,
17936
+ {
17937
+ trigger: "hover",
17938
+ icon: "helpOutline",
17939
+ iconSize: 12,
17940
+ color: "var(--sys-color-fg-success)",
17941
+ placement: "top",
17942
+ message: col.helpText
17943
+ }
17944
+ ) }),
17945
+ col.editable && /* @__PURE__ */ jsx(
17946
+ SIcon,
17947
+ {
17948
+ name: "pageEdit",
17949
+ size: 12,
17950
+ color: "var(--sys-color-link-accent)",
17951
+ label: "\uC218\uC815 \uAC00\uB2A5",
17952
+ className: "shrink-0"
17953
+ }
17954
+ ),
17955
+ col.navigable && /* @__PURE__ */ jsx(
17956
+ SIcon,
17957
+ {
17958
+ name: "pageMove",
17959
+ size: 12,
17960
+ color: "var(--sys-color-link-accent)",
17961
+ label: "\uC0C8 \uD654\uBA74\uC5D0\uC11C \uC5F4\uAE30",
17962
+ className: "shrink-0"
17963
+ }
17964
+ ),
17965
+ col.required && // 라벨이 truncate 돼도 필수 표시는 남아야 하므로 shrink-0.
17966
+ // 아이콘·크기·색은 SKeyValueTable 헤더 필수 표시와 동일하게 맞춘다.
17967
+ /* @__PURE__ */ jsx(
17968
+ SIcon,
17969
+ {
17970
+ name: "asterisk",
17971
+ size: 12,
17972
+ color: "var(--sys-color-fg-accent)",
17973
+ className: "shrink-0"
17974
+ }
17975
+ ),
17976
+ isSortable && // 미정렬은 updown 을 default(회색)로 — 정렬할 수 있다는 것 자체가
17977
+ // 헤더에 보여야 한다. 정렬 중이면 action 색으로 올려 활성임을 알린다.
17978
+ // 라벨이 truncate 돼도 버튼은 남도록 shrink-0.
17979
+ /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx(
17980
+ SGhostButton,
17981
+ {
17982
+ size: "xxs",
17983
+ intent: sortDir ? "action" : "default",
17984
+ icon: sortDir === "asc" ? "arrowUp" : sortDir === "desc" ? "arrowDown" : "updown",
17985
+ ariaLabel: `${col.label} \uC815\uB82C`,
17986
+ onClick: () => toggleSort(col)
17987
+ }
17988
+ ) })
17989
+ ] })
17990
+ }
17991
+ ),
17992
+ headerGroupInfo ? headerResizeHitArea(i, `resizer-${col.name}`) : headerResizer(i, `resizer-${col.name}`)
17993
+ ]
17994
+ },
17995
+ col.name
17996
+ );
17997
+ })
17998
+ ] })
17999
+ ] })
18000
+ ]
18001
+ }
18002
+ );
18003
+ return /* @__PURE__ */ jsxs(
18004
+ "div",
18005
+ {
18006
+ className: cn("flex flex-col", className),
18007
+ style: { width, ...style },
18008
+ "data-table-id": resolvedTableId,
18009
+ children: [
18010
+ /* @__PURE__ */ jsxs(
18011
+ "div",
18012
+ {
18013
+ className: cn(
18014
+ // isolate: no-data/loading 오버레이(z-30·31)를 테이블 안에서 끝낸다. relative + z-auto
18015
+ // 만으로는 스태킹 컨텍스트가 생기지 않아, 모달 안에 들어가면 30·31이 모달 Content
18016
+ // 레벨까지 올라가 같은 컨텍스트의 floating portal 밴드(z-2)를 덮는다.
18017
+ // 자손은 이미 overflow-hidden으로 클리핑되므로 테이블 내부 순서는 달라지지 않는다.
18018
+ "bg-white relative isolate flex flex-col overflow-hidden",
18019
+ bordered && "border border-solid border-[color:var(--cmp-table-border-color)]",
17503
18020
  // height가 없으면 루트가 받은 높이를 끝까지 채운다. 호스트가 className으로
17504
18021
  // `flex-1 min-h-0` 등을 줘 루트 높이가 확정된 경우, 행이 적어도 테두리 박스가
17505
18022
  // 남은 영역을 그대로 차지한다. basis는 auto로 둬 높이가 확정되지 않은
@@ -17514,220 +18031,20 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17514
18031
  minHeight: isNoData ? "calc(var(--cmp-table-header-height) + 60px)" : void 0
17515
18032
  },
17516
18033
  children: [
17517
- /* @__PURE__ */ jsx(
17518
- "div",
17519
- {
17520
- ref: headerScrollRef,
17521
- className: "flex-none overflow-hidden",
17522
- style: {
17523
- borderRight: headerGutter ? `${headerGutter}px solid var(--cmp-table-header-bg)` : void 0
17524
- },
17525
- children: /* @__PURE__ */ jsxs(
17526
- "table",
17527
- {
17528
- className: "w-full table-fixed border-separate border-spacing-0 text-left",
17529
- style: { fontSize: 12 },
17530
- children: [
17531
- colgroup,
17532
- /* @__PURE__ */ jsxs("thead", { className: cn(isNoData && "opacity-40"), children: [
17533
- headerGroupInfo && /* @__PURE__ */ jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17534
- selectable && /* @__PURE__ */ jsx(
17535
- "th",
17536
- {
17537
- rowSpan: 2,
17538
- className: cn(
17539
- "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17540
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17541
- ),
17542
- style: {
17543
- width: SELECTABLE_COLUMN_WIDTH,
17544
- minWidth: SELECTABLE_COLUMN_WIDTH,
17545
- maxWidth: SELECTABLE_COLUMN_WIDTH
17546
- },
17547
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(
17548
- SCheckbox,
17549
- {
17550
- value: headerCheck,
17551
- disabled: !hasSelectableRow,
17552
- onValueChange: toggleAll
17553
- }
17554
- ) })
17555
- }
17556
- ),
17557
- headerGroupInfo.cells.map((group) => /* @__PURE__ */ jsxs(
17558
- "th",
17559
- {
17560
- colSpan: group.colSpan,
17561
- rowSpan: group.rowSpan,
17562
- className: cn(
17563
- "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)]",
17564
- group.firstIndex + group.colSpan === visibleCols.length && "border-r-0",
17565
- group.rowSpan === 2 && "border-b-0",
17566
- group.className
17567
- ),
17568
- style: { textAlign: group.align ?? "left", ...group.style },
17569
- children: [
17570
- group.render ? group.render() : group.label,
17571
- group.rowSpan === 2 && group.colSpan === 1 ? headerResizeHitArea(
17572
- group.firstIndex,
17573
- `group-resizer-${group.columnNames[0]}`
17574
- ) : null
17575
- ]
17576
- },
17577
- group.columnNames.join("|")
17578
- ))
17579
- ] }),
17580
- /* @__PURE__ */ jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
17581
- selectable && !headerGroupInfo && /* @__PURE__ */ jsxs(
17582
- "th",
17583
- {
17584
- className: cn(
17585
- // 원본 .th--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
17586
- // 하단 border는 헤더 래퍼가 연속으로 그리므로 셀에는 두지 않는다.
17587
- "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
17588
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17589
- ),
17590
- style: {
17591
- width: SELECTABLE_COLUMN_WIDTH,
17592
- minWidth: SELECTABLE_COLUMN_WIDTH,
17593
- maxWidth: SELECTABLE_COLUMN_WIDTH
17594
- },
17595
- children: [
17596
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(
17597
- SCheckbox,
17598
- {
17599
- value: headerCheck,
17600
- disabled: !hasSelectableRow,
17601
- onValueChange: toggleAll
17602
- }
17603
- ) }),
17604
- /* @__PURE__ */ jsx(
17605
- "span",
17606
- {
17607
- "aria-hidden": true,
17608
- 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)]"
17609
- }
17610
- )
17611
- ]
17612
- }
17613
- ),
17614
- visibleCols.map((col, i) => {
17615
- if (headerGroupInfo?.rowSpanNames.has(col.name)) return null;
17616
- const isSortable = !!col.sortable && !col.renderHeader;
17617
- const sortDir = isSortable && sort?.name === col.name ? sort.dir : void 0;
17618
- return /* @__PURE__ */ jsxs(
17619
- "th",
17620
- {
17621
- className: cn(
17622
- // 하단 border는 헤더 래퍼가 연속으로 그린다 (거터 영역까지 이어짐).
17623
- // leading-[0]: th 의 스트럿(line-height 만큼의 빈 인라인 박스)을 없앤다.
17624
- // 내용이 inline-flex 라 스트럿이 남아 있으면 라벨보다 큰 컨트롤(정렬 버튼 등)이
17625
- // 들어올 때 줄 상자가 베이스라인 기준으로 늘어나 헤더 높이를 넘기고
17626
- // 세로 가운데도 어긋난다. 글자 줄높이는 아래 th__content 가 다시 준다.
17627
- headerGroupInfo && "border-r border-solid border-[color:var(--cmp-table-border-color)]",
17628
- headerGroupInfo && i === visibleCols.length - 1 && "border-r-0",
17629
- "relative bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[0] text-[color:var(--sys-color-fg-primary)]",
17630
- stickyInfo(i).isSticky && "z-[6]",
17631
- col.thClass
17632
- ),
17633
- style: {
17634
- textAlign: col.align ?? "left",
17635
- boxShadow: stickyShadowValue(i) ?? void 0,
17636
- ...cellStyle(i),
17637
- ...col.thStyle
17638
- },
17639
- "aria-sort": isSortable ? sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none" : void 0,
17640
- children: [
17641
- /* @__PURE__ */ jsx(
17642
- "span",
17643
- {
17644
- className: cn(
17645
- // 원본 th__content: align에 따라 justify-content 적용 (th도 정렬 반영)
17646
- // th 가 leading-[0] 이므로 글자 줄높이는 여기서 준다 (truncate 가
17647
- // overflow-hidden 이라 줄높이가 0이면 라벨이 잘려 사라진다).
17648
- "inline-flex w-full min-w-0 items-center gap-[4px] leading-[20px]",
17649
- col.align === "center" && "justify-center",
17650
- col.align === "right" && "justify-end"
17651
- ),
17652
- children: col.renderHeader ? col.renderHeader(col, i) : /* @__PURE__ */ jsxs(Fragment, { children: [
17653
- /* @__PURE__ */ jsx("span", { className: "truncate", children: col.label }),
17654
- col.helpText && col.helpText.length > 0 && // 라벨이 좁아져 truncate 되더라도 아이콘까지 함께 줄어들면 안 되므로
17655
- // shrink-0 래퍼로 감싼다 (STooltip 의 className 은 본문 쪽으로 간다).
17656
- // 아이콘·크기·색은 SKeyValueTable 헤더 도움말과 동일하게 맞춘다.
17657
- /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx(
17658
- STooltip2,
17659
- {
17660
- trigger: "hover",
17661
- icon: "helpOutline",
17662
- iconSize: 12,
17663
- color: "var(--sys-color-fg-success)",
17664
- placement: "top",
17665
- message: col.helpText
17666
- }
17667
- ) }),
17668
- col.editable && /* @__PURE__ */ jsx(
17669
- SIcon,
17670
- {
17671
- name: "pageEdit",
17672
- size: 12,
17673
- color: "var(--sys-color-link-accent)",
17674
- label: "\uC218\uC815 \uAC00\uB2A5",
17675
- className: "shrink-0"
17676
- }
17677
- ),
17678
- col.navigable && /* @__PURE__ */ jsx(
17679
- SIcon,
17680
- {
17681
- name: "pageMove",
17682
- size: 12,
17683
- color: "var(--sys-color-link-accent)",
17684
- label: "\uC0C8 \uD654\uBA74\uC5D0\uC11C \uC5F4\uAE30",
17685
- className: "shrink-0"
17686
- }
17687
- ),
17688
- col.required && // 라벨이 truncate 돼도 필수 표시는 남아야 하므로 shrink-0.
17689
- // 아이콘·크기·색은 SKeyValueTable 헤더 필수 표시와 동일하게 맞춘다.
17690
- /* @__PURE__ */ jsx(
17691
- SIcon,
17692
- {
17693
- name: "asterisk",
17694
- size: 12,
17695
- color: "var(--sys-color-fg-accent)",
17696
- className: "shrink-0"
17697
- }
17698
- ),
17699
- isSortable && // 미정렬은 updown 을 default(회색)로 — 정렬할 수 있다는 것 자체가
17700
- // 헤더에 보여야 한다. 정렬 중이면 action 색으로 올려 활성임을 알린다.
17701
- // 라벨이 truncate 돼도 버튼은 남도록 shrink-0.
17702
- /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx(
17703
- SGhostButton,
17704
- {
17705
- size: "xxs",
17706
- intent: sortDir ? "action" : "default",
17707
- icon: sortDir === "asc" ? "arrowUp" : sortDir === "desc" ? "arrowDown" : "updown",
17708
- ariaLabel: `${col.label} \uC815\uB82C`,
17709
- onClick: () => toggleSort(col)
17710
- }
17711
- ) })
17712
- ] })
17713
- }
17714
- ),
17715
- headerGroupInfo ? headerResizeHitArea(i, `resizer-${col.name}`) : headerResizer(i, `resizer-${col.name}`)
17716
- ]
17717
- },
17718
- col.name
17719
- );
17720
- })
17721
- ] })
17722
- ] })
17723
- ]
17724
- }
17725
- )
17726
- }
17727
- ),
17728
- showNoDataSlot ? /* @__PURE__ */ jsx("div", { className: cn(bodyScrollClass, "pointer-events-auto bg-white"), children: /* @__PURE__ */ jsx("div", { className: "flex h-full min-h-[60px] w-full items-center justify-center", children: emptySlot }) }) : (
17729
- /* 바디: 세로 스크롤은 이 컨테이너에만 생긴다 (헤더 제외) */
17730
- /* @__PURE__ */ jsx(
18034
+ showNoDataSlot ? /* @__PURE__ */ jsxs(Fragment, { children: [
18035
+ /* @__PURE__ */ jsx("div", { className: "flex-none overflow-hidden", children: headerTable }),
18036
+ /* @__PURE__ */ jsx("div", { className: cn(bodyScrollClass, "pointer-events-auto bg-white"), children: /* @__PURE__ */ jsx("div", { className: "flex h-full min-h-[60px] w-full items-center justify-center", children: emptySlot }) })
18037
+ ] }) : (
18038
+ /**
18039
+ * 헤더와 body 가 한 스크롤 컨테이너에 있다 — 헤더는 위에 붙어(sticky) 세로로는 서 있고
18040
+ * 가로로는 body 와 같은 스크롤로 움직인다. 헤더를 따로 두고 scroll 이벤트에서
18041
+ * scrollLeft 를 맞추면, 브라우저가 body 스크롤을 먼저 그리고 이벤트를 나중에 돌려
18042
+ * 헤더가 한 프레임 이상 늦게 따라온다.
18043
+ *
18044
+ * 행 좌표는 body 표 기준 그대로다 — `scrollTop` 이 y 이면 y 에 있는 행이 헤더 바로
18045
+ * 아래에 선다. 보이는 높이만 헤더 높이만큼 줄어든다(`headerHeight`).
18046
+ */
18047
+ /* @__PURE__ */ jsxs(
17731
18048
  "div",
17732
18049
  {
17733
18050
  ref: scrollRef,
@@ -17737,136 +18054,140 @@ var STableImpl = /* @__PURE__ */ forwardRef(function STable({
17737
18054
  // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 스크롤 차단
17738
18055
  (loading2 || rowCount === 0) && "overflow-hidden"
17739
18056
  ),
17740
- children: /* @__PURE__ */ jsxs(
17741
- "table",
17742
- {
17743
- className: "w-full table-fixed border-separate border-spacing-0 text-left",
17744
- style: { fontSize: 12 },
17745
- children: [
17746
- colgroup,
17747
- /* @__PURE__ */ jsxs("tbody", { children: [
17748
- virtualScroll && topSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: topSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) }),
17749
- rowCount === 0 ? null : bodyRows.map((row, ri) => {
17750
- const rowIndex = rowOffset + ri;
17751
- const isSel = isViewSelected(row);
17752
- const dragSel = dragSelectOn && isSel;
17753
- const dragTop = dragSel && !isViewSelected(rows[rowIndex - 1]);
17754
- const dragBottom = dragSel && !isViewSelected(rows[rowIndex + 1]);
17755
- const rowLocked = (selectable || dragSelectable) && !canSelectRow(row);
17756
- const rowBg = "bg-white";
17757
- const rowHover = hoverable && !rowLocked;
17758
- return /* @__PURE__ */ jsxs(
17759
- "tr",
17760
- {
17761
- "data-row-index": rowIndex,
17762
- onClick: () => onRowClick?.(row),
17763
- onDoubleClick: onRowDoubleClick ? (event) => {
17764
- const target = event.target;
17765
- if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
17766
- onRowDoubleClick(row);
17767
- } : void 0,
17768
- onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
17769
- onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
17770
- onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
17771
- className: cn(
17772
- "group/table-row",
17773
- rowHover && "hover:bg-[var(--color-grey-05)]",
17774
- (onRowClick || onRowDoubleClick) && "cursor-pointer",
17775
- dragSelectOn && "select-none",
17776
- rowClassName?.(row)
17777
- ),
17778
- style: { height: virtualScroll ? effectiveRowHeight : rowH },
17779
- children: [
17780
- selectable && /* @__PURE__ */ jsx(
17781
- "td",
17782
- {
17783
- className: cn(
17784
- // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
17785
- "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
18057
+ style: { scrollPaddingTop: headerHeight },
18058
+ children: [
18059
+ /* @__PURE__ */ jsx("div", { ref: headerRef, className: "sticky top-0 z-[8]", children: headerTable }),
18060
+ /* @__PURE__ */ jsxs(
18061
+ "table",
18062
+ {
18063
+ className: "w-full table-fixed border-separate border-spacing-0 text-left",
18064
+ style: { fontSize: 12 },
18065
+ children: [
18066
+ colgroup,
18067
+ /* @__PURE__ */ jsxs("tbody", { children: [
18068
+ virtualScroll && topSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: topSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) }),
18069
+ rowCount === 0 ? null : bodyRows.map((row, ri) => {
18070
+ const rowIndex = rowOffset + ri;
18071
+ const isSel = isViewSelected(row);
18072
+ const dragSel = dragSelectOn && isSel;
18073
+ const dragTop = dragSel && !isViewSelected(rows[rowIndex - 1]);
18074
+ const dragBottom = dragSel && !isViewSelected(rows[rowIndex + 1]);
18075
+ const rowLocked = (selectable || dragSelectable) && !canSelectRow(row);
18076
+ const rowBg = "bg-white";
18077
+ const rowHover = hoverable && !rowLocked;
18078
+ return /* @__PURE__ */ jsxs(
18079
+ "tr",
18080
+ {
18081
+ "data-row-index": rowIndex,
18082
+ onClick: () => onRowClick?.(row),
18083
+ onDoubleClick: onRowDoubleClick ? (event) => {
18084
+ const target = event.target;
18085
+ if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
18086
+ onRowDoubleClick(row);
18087
+ } : void 0,
18088
+ onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
18089
+ onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
18090
+ onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
18091
+ className: cn(
18092
+ "group/table-row",
18093
+ rowHover && "hover:bg-[var(--color-grey-05)]",
18094
+ (onRowClick || onRowDoubleClick) && "cursor-pointer",
18095
+ dragSelectOn && "select-none",
18096
+ rowClassName?.(row)
18097
+ ),
18098
+ style: { height: virtualScroll ? effectiveRowHeight : rowH },
18099
+ children: [
18100
+ selectable && /* @__PURE__ */ jsx(
18101
+ "td",
18102
+ {
18103
+ className: cn(
18104
+ // 원본 .td--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
18105
+ "sticky left-0 z-[2] border-b border-solid border-[color:var(--cmp-table-border-color)] align-middle",
18106
+ rowBg,
18107
+ rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
18108
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
18109
+ ),
18110
+ style: {
18111
+ width: SELECTABLE_COLUMN_WIDTH,
18112
+ minWidth: SELECTABLE_COLUMN_WIDTH,
18113
+ maxWidth: SELECTABLE_COLUMN_WIDTH,
18114
+ // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
18115
+ background: rowLocked ? LOCKED_BG : void 0
18116
+ },
18117
+ onClick: (e) => e.stopPropagation(),
18118
+ onDoubleClick: (e) => e.stopPropagation(),
18119
+ onClickCapture: (e) => {
18120
+ shiftPressedRef.current = e.shiftKey;
18121
+ if (e.shiftKey) window.getSelection()?.removeAllRanges();
18122
+ },
18123
+ children: /* @__PURE__ */ jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsx(
18124
+ SCheckbox,
18125
+ {
18126
+ value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
18127
+ disabled: !canSelectRow(row),
18128
+ onValueChange: () => toggleRow(row, rowOffset + ri)
18129
+ }
18130
+ ) })
18131
+ }
18132
+ ),
18133
+ visibleCols.map((col, i) => {
18134
+ const tdClassName = cn(
18135
+ // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
18136
+ // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
18137
+ "overflow-hidden text-ellipsis whitespace-nowrap border-b border-solid border-[color:var(--cmp-table-border-color)] text-[12px] leading-[20px] text-[color:var(--sys-color-fg-primary)]",
17786
18138
  rowBg,
17787
18139
  rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
17788
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
17789
- ),
17790
- style: {
17791
- width: SELECTABLE_COLUMN_WIDTH,
17792
- minWidth: SELECTABLE_COLUMN_WIDTH,
17793
- maxWidth: SELECTABLE_COLUMN_WIDTH,
17794
- // 선택 열도 행의 일부다 — 여기만 희지 않으면 회색 줄이 끊긴다
17795
- background: rowLocked ? LOCKED_BG : void 0
17796
- },
17797
- onClick: (e) => e.stopPropagation(),
17798
- onDoubleClick: (e) => e.stopPropagation(),
17799
- onClickCapture: (e) => {
17800
- shiftPressedRef.current = e.shiftKey;
17801
- if (e.shiftKey) window.getSelection()?.removeAllRanges();
17802
- },
17803
- children: /* @__PURE__ */ jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsx(
17804
- SCheckbox,
17805
- {
17806
- value: isSel || !isRowIndeterminate?.(row) ? isSel : null,
17807
- disabled: !canSelectRow(row),
17808
- onValueChange: () => toggleRow(row, rowOffset + ri)
17809
- }
17810
- ) })
17811
- }
17812
- ),
17813
- visibleCols.map((col, i) => {
17814
- const tdClassName = cn(
17815
- // 원본 .td: 컬럼 폭에 맞춰 내용 말줄임(overflow hidden + ellipsis) + 행 구분선(border-b)
17816
- // 셀 배경은 흰색으로 채워 뒤 배경이 비치지 않게 하고, hover 시 행 배경(grey_05)을 따른다.
17817
- "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)]",
17818
- rowBg,
17819
- rowHover && "group-hover/table-row:bg-[var(--color-grey-05)]",
17820
- typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
17821
- );
17822
- const tdPadX = cellPadXOf(col);
17823
- const dragEdges = dragSel ? [
17824
- dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
17825
- dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
17826
- i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
17827
- i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
17828
- ].filter(Boolean) : [];
17829
- const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
17830
- const tdStyle = {
17831
- paddingLeft: tdPadX,
17832
- paddingRight: tdPadX,
17833
- paddingTop: cellPadY,
17834
- paddingBottom: cellPadY,
17835
- textAlign: col.align ?? "left",
17836
- // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
17837
- background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
17838
- // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
17839
- // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
17840
- color: rowLocked ? LOCKED_CONTENT : void 0,
17841
- boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
17842
- ...cellStyle(i)
17843
- };
17844
- if (col.renderCell) {
17845
- const cell = col.renderCell({
17846
- row,
17847
- value: cellValue(col, row),
17848
- column: col,
17849
- colIndex: i,
17850
- rowIndex: ri,
17851
- isSelected: isSel,
17852
- className: tdClassName,
17853
- style: tdStyle
17854
- });
17855
- if (cell == null || cell === false) return null;
17856
- return /* @__PURE__ */ jsx(Fragment$1, { children: cell }, col.name);
17857
- }
17858
- return /* @__PURE__ */ jsx("td", { className: tdClassName, style: tdStyle, children: col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row) }, col.name);
17859
- })
17860
- ]
17861
- },
17862
- row[rowKey] ?? ri
17863
- );
17864
- }),
17865
- virtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
17866
- ] })
17867
- ]
17868
- }
17869
- )
18140
+ typeof col.tdClass === "function" ? col.tdClass(row) : col.tdClass
18141
+ );
18142
+ const tdPadX = cellPadXOf(col);
18143
+ const dragEdges = dragSel ? [
18144
+ dragTop && `inset 0 ${DRAG_EDGE_W} 0 0 ${DRAG_EDGE_C}`,
18145
+ dragBottom && `inset 0 ${DRAG_EDGE_NEG_W} 0 0 ${DRAG_EDGE_C}`,
18146
+ i === 0 && `inset ${DRAG_EDGE_W} 0 0 0 ${DRAG_EDGE_C}`,
18147
+ i === visibleCols.length - 1 && `inset ${DRAG_EDGE_NEG_W} 0 0 0 ${DRAG_EDGE_C}`
18148
+ ].filter(Boolean) : [];
18149
+ const shadows = [...dragEdges, stickyShadowValue(i)].filter(Boolean);
18150
+ const tdStyle = {
18151
+ paddingLeft: tdPadX,
18152
+ paddingRight: tdPadX,
18153
+ paddingTop: cellPadY,
18154
+ paddingBottom: cellPadY,
18155
+ textAlign: col.align ?? "left",
18156
+ // 인라인이라 hover 배경보다 세다 — 고른 행은 hover 로 흐려지지 않는다.
18157
+ background: dragSel ? DRAG_BG : rowLocked ? LOCKED_BG : void 0,
18158
+ // 클래스의 기본 글자색을 덮는다. 셀이 자기 색을 정한 내용(SIcon 의
18159
+ // color, STag 등)까지는 닿지 않는다 — 그건 소비 앱 몫이다.
18160
+ color: rowLocked ? LOCKED_CONTENT : void 0,
18161
+ boxShadow: shadows.length > 0 ? shadows.join(", ") : void 0,
18162
+ ...cellStyle(i)
18163
+ };
18164
+ if (col.renderCell) {
18165
+ const cell = col.renderCell({
18166
+ row,
18167
+ value: cellValue(col, row),
18168
+ column: col,
18169
+ colIndex: i,
18170
+ rowIndex: ri,
18171
+ isSelected: isSel,
18172
+ className: tdClassName,
18173
+ style: tdStyle
18174
+ });
18175
+ if (cell == null || cell === false) return null;
18176
+ return /* @__PURE__ */ jsx(Fragment$1, { children: cell }, col.name);
18177
+ }
18178
+ return /* @__PURE__ */ jsx("td", { className: tdClassName, style: tdStyle, children: col.render ? col.render(row, cellValue(col, row)) : cellValue(col, row) }, col.name);
18179
+ })
18180
+ ]
18181
+ },
18182
+ row[rowKey] ?? ri
18183
+ );
18184
+ }),
18185
+ virtualScroll && bottomSpacer > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: bottomSpacer }, children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0" }) })
18186
+ ] })
18187
+ ]
18188
+ }
18189
+ )
18190
+ ]
17870
18191
  }
17871
18192
  )
17872
18193
  ),
@@ -19374,13 +19695,21 @@ function SCalendar({
19374
19695
  }
19375
19696
  );
19376
19697
  }
19698
+
19699
+ // src/lib/mode-listbox.ts
19700
+ var MODE_LIST_HEIGHT = 240;
19701
+ var MODE_ITEM_HEIGHT = 28;
19702
+ var modeListItemClass = (selected, disabled) => cn(
19703
+ "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]",
19704
+ disabled && "cursor-not-allowed bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-disabled)]",
19705
+ !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)]",
19706
+ !disabled && selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-bold text-[color:var(--cmp-datepicker-mode-item-content-selected)]"
19707
+ );
19377
19708
  var MODE_NOUN = {
19378
19709
  date: "\uB0A0\uC9DC",
19379
19710
  month: "\uC6D4",
19380
19711
  year: "\uC5F0\uB3C4"
19381
19712
  };
19382
- var DATEPICKER_MODE_LIST_HEIGHT = 240;
19383
- var DATEPICKER_MODE_ITEM_HEIGHT = 28;
19384
19713
  var YEAR_LIST_EDGE_BUFFER = 24;
19385
19714
  var YEAR_LIST_EXPAND_SIZE = 80;
19386
19715
  var MONTHS = Array.from({ length: 12 }, (_, i) => i + 1);
@@ -19431,7 +19760,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19431
19760
  if (!list) return;
19432
19761
  const index = anchorYearRef.current - rangeStartRef.current;
19433
19762
  syncingScrollRef.current = true;
19434
- list.scrollTop = index * DATEPICKER_MODE_ITEM_HEIGHT;
19763
+ list.scrollTop = index * MODE_ITEM_HEIGHT;
19435
19764
  requestAnimationFrame(() => {
19436
19765
  syncingScrollRef.current = false;
19437
19766
  });
@@ -19440,7 +19769,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19440
19769
  const list = listRef.current;
19441
19770
  if (!list) return;
19442
19771
  if (syncingScrollRef.current) return;
19443
- if (list.scrollTop < DATEPICKER_MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19772
+ if (list.scrollTop < MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19444
19773
  const previousHeight = list.scrollHeight;
19445
19774
  setRange((current) => ({
19446
19775
  start: current.start - YEAR_LIST_EXPAND_SIZE,
@@ -19456,7 +19785,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19456
19785
  });
19457
19786
  return;
19458
19787
  }
19459
- if (list.scrollHeight - list.scrollTop - list.clientHeight < DATEPICKER_MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19788
+ if (list.scrollHeight - list.scrollTop - list.clientHeight < MODE_ITEM_HEIGHT * YEAR_LIST_EDGE_BUFFER) {
19460
19789
  setRange((current) => ({
19461
19790
  start: current.start,
19462
19791
  end: current.end + YEAR_LIST_EXPAND_SIZE
@@ -19471,8 +19800,8 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19471
19800
  "aria-label": "year",
19472
19801
  className: "h-full overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
19473
19802
  style: {
19474
- paddingTop: `calc((${DATEPICKER_MODE_LIST_HEIGHT}px - ${DATEPICKER_MODE_ITEM_HEIGHT}px) / 2)`,
19475
- paddingBottom: `calc((${DATEPICKER_MODE_LIST_HEIGHT}px - ${DATEPICKER_MODE_ITEM_HEIGHT}px) / 2)`
19803
+ paddingTop: `calc((${MODE_LIST_HEIGHT}px - ${MODE_ITEM_HEIGHT}px) / 2)`,
19804
+ paddingBottom: `calc((${MODE_LIST_HEIGHT}px - ${MODE_ITEM_HEIGHT}px) / 2)`
19476
19805
  },
19477
19806
  onScroll: handleScroll,
19478
19807
  children: years.map((year) => {
@@ -19485,12 +19814,7 @@ function YearWheel({ value, anchorYear, selectable, onCommit }) {
19485
19814
  role: "option",
19486
19815
  "aria-selected": selected,
19487
19816
  disabled,
19488
- className: cn(
19489
- "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]",
19490
- disabled && "cursor-not-allowed bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-disabled)]",
19491
- !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)]",
19492
- !disabled && selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-bold text-[color:var(--cmp-datepicker-mode-item-content-selected)]"
19493
- ),
19817
+ className: modeListItemClass(selected, disabled),
19494
19818
  onClick: () => !disabled && onCommit(year),
19495
19819
  children: year
19496
19820
  },
@@ -19546,7 +19870,7 @@ function MonthWheel({ year, value, selectable, onCommit }) {
19546
19870
  if (!list) return;
19547
19871
  list.scrollTop = Math.max(
19548
19872
  0,
19549
- (initialValueRef.current - 1) * DATEPICKER_MODE_ITEM_HEIGHT - (list.clientHeight - DATEPICKER_MODE_ITEM_HEIGHT) / 2
19873
+ (initialValueRef.current - 1) * MODE_ITEM_HEIGHT - (list.clientHeight - MODE_ITEM_HEIGHT) / 2
19550
19874
  );
19551
19875
  }, []);
19552
19876
  return /* @__PURE__ */ jsx("div", { className: "h-full w-full overflow-hidden bg-[var(--cmp-datepicker-mode-bg)]", children: /* @__PURE__ */ jsx(
@@ -19566,12 +19890,7 @@ function MonthWheel({ year, value, selectable, onCommit }) {
19566
19890
  role: "option",
19567
19891
  "aria-selected": selected,
19568
19892
  disabled,
19569
- className: cn(
19570
- "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]",
19571
- disabled && "cursor-not-allowed bg-[var(--cmp-datepicker-mode-item-bg-default)] font-medium text-[color:var(--cmp-datepicker-mode-item-content-disabled)]",
19572
- !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)]",
19573
- !disabled && selected && "cursor-pointer bg-[var(--cmp-datepicker-mode-item-bg-default)] font-bold text-[color:var(--cmp-datepicker-mode-item-content-selected)]"
19574
- ),
19893
+ className: modeListItemClass(selected, disabled),
19575
19894
  onClick: () => !disabled && onCommit(month),
19576
19895
  children: [
19577
19896
  month,
@@ -21145,6 +21464,169 @@ function ChartAxisLabels({
21145
21464
  }
21146
21465
  );
21147
21466
  }
21467
+ function useElementSize(target) {
21468
+ const ref = useRef(null);
21469
+ const [size, setSize] = useState({ width: 0, height: 0 });
21470
+ useEffect(() => {
21471
+ const element = ref.current;
21472
+ if (!element) return;
21473
+ const update = () => {
21474
+ const { width, height } = element.getBoundingClientRect();
21475
+ setSize(
21476
+ (prev) => Math.abs(prev.width - width) < 0.5 && Math.abs(prev.height - height) < 0.5 ? prev : { width, height }
21477
+ );
21478
+ };
21479
+ update();
21480
+ if (typeof ResizeObserver === "undefined") return;
21481
+ const observer = new ResizeObserver(update);
21482
+ observer.observe(element);
21483
+ return () => observer.disconnect();
21484
+ }, [target]);
21485
+ return { ref, size };
21486
+ }
21487
+ function useSvgTextMetrics() {
21488
+ const ref = useRef(null);
21489
+ const [metrics, setMetrics] = useState({ top: 0, width: 0 });
21490
+ useEffect(() => {
21491
+ const element = ref.current;
21492
+ if (!element) return;
21493
+ let box3;
21494
+ try {
21495
+ box3 = element.getBBox();
21496
+ } catch {
21497
+ return;
21498
+ }
21499
+ setMetrics(
21500
+ (prev) => Math.abs(prev.top - box3.y) < 0.5 && Math.abs(prev.width - box3.width) < 0.5 ? prev : { top: box3.y, width: box3.width }
21501
+ );
21502
+ });
21503
+ return { ref, metrics };
21504
+ }
21505
+ var CATEGORY_LABEL_ROTATED_MAX = 120;
21506
+ var DIAGONAL = Math.SQRT1_2;
21507
+ function ChartRotatedAxisLayout({
21508
+ axis,
21509
+ axisStyle,
21510
+ plot,
21511
+ minPlotWidth,
21512
+ labels,
21513
+ xAt,
21514
+ labelsRef
21515
+ }) {
21516
+ const scrollRef = useRef(null);
21517
+ const { ref: axisRef, size: axisBox } = useElementSize();
21518
+ return /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "min-w-0 overflow-x-auto", style: { gridColumn: "1 / -1" }, children: /* @__PURE__ */ jsxs("div", { style: { width: `max(100%, ${axisBox.width + minPlotWidth}px)` }, children: [
21519
+ /* @__PURE__ */ jsxs("div", { className: "flex", children: [
21520
+ /* @__PURE__ */ jsx(
21521
+ "div",
21522
+ {
21523
+ ref: axisRef,
21524
+ className: "sticky left-0 z-1 shrink-0 bg-(--cmp-chart-bg)",
21525
+ style: { ...axisStyle, paddingRight: AXIS_LABEL_GAP },
21526
+ children: axis
21527
+ }
21528
+ ),
21529
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: plot })
21530
+ ] }),
21531
+ /* @__PURE__ */ jsx("div", { ref: labelsRef, children: /* @__PURE__ */ jsx(
21532
+ ChartRotatedCategoryLabels,
21533
+ {
21534
+ labels,
21535
+ xAt,
21536
+ gutter: axisBox.width,
21537
+ scrollRef
21538
+ }
21539
+ ) })
21540
+ ] }) });
21541
+ }
21542
+ function ChartRotatedCategoryLabels({
21543
+ labels,
21544
+ xAt,
21545
+ gutter,
21546
+ scrollRef
21547
+ }) {
21548
+ const sizerRef = useRef(null);
21549
+ const maskRef = useRef(null);
21550
+ const [box3, setBox] = useState({ line: 0, widest: 0 });
21551
+ useEffect(() => {
21552
+ const sizer = sizerRef.current;
21553
+ if (!sizer) return;
21554
+ const measure = () => {
21555
+ const { width, height: height2 } = sizer.getBoundingClientRect();
21556
+ const line = labels.length > 0 ? height2 / labels.length : 0;
21557
+ setBox(
21558
+ (prev) => Math.abs(prev.line - line) < 0.5 && Math.abs(prev.widest - width) < 0.5 ? prev : { line, widest: width }
21559
+ );
21560
+ };
21561
+ measure();
21562
+ if (typeof ResizeObserver === "undefined") return;
21563
+ const observer = new ResizeObserver(measure);
21564
+ observer.observe(sizer);
21565
+ return () => observer.disconnect();
21566
+ });
21567
+ useEffect(() => {
21568
+ const scroller = scrollRef.current;
21569
+ const mask = maskRef.current;
21570
+ if (!scroller || !mask) return;
21571
+ const sync2 = () => {
21572
+ mask.style.visibility = scroller.scrollLeft > 0 ? "visible" : "hidden";
21573
+ };
21574
+ sync2();
21575
+ scroller.addEventListener("scroll", sync2, { passive: true });
21576
+ return () => scroller.removeEventListener("scroll", sync2);
21577
+ }, [scrollRef]);
21578
+ const maxWidthAt = (index) => Math.max(
21579
+ 0,
21580
+ Math.min(CATEGORY_LABEL_ROTATED_MAX, (xAt(index) + gutter) / DIAGONAL - box3.line / 2)
21581
+ );
21582
+ const height = Math.ceil(box3.line / 2 + (box3.widest + box3.line / 2) * DIAGONAL);
21583
+ return (
21584
+ // overflow 는 clip 이어야 한다 — hidden 이면 이 행이 스크롤 상자로 잡혀 가림막의
21585
+ // sticky 가 스크롤을 따라가지 않는다.
21586
+ /* @__PURE__ */ jsxs(
21587
+ "div",
21588
+ {
21589
+ "aria-hidden": true,
21590
+ className: "relative overflow-clip typo-body-sm-medium text-(--cmp-chart-axis-color)",
21591
+ style: { height },
21592
+ children: [
21593
+ /* @__PURE__ */ jsx(
21594
+ "div",
21595
+ {
21596
+ ref: maskRef,
21597
+ className: "invisible sticky left-0 z-1 h-full bg-(--cmp-chart-bg)",
21598
+ style: { width: gutter }
21599
+ }
21600
+ ),
21601
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 right-0", style: { left: gutter }, children: labels.map((label, index) => /* @__PURE__ */ jsx(
21602
+ "span",
21603
+ {
21604
+ className: "absolute top-0 truncate",
21605
+ style: {
21606
+ left: xAt(index),
21607
+ maxWidth: maxWidthAt(index),
21608
+ // 글자 끝(오른쪽 가운데)을 눈금에 맞춘 뒤 그 점을 축으로 기울인다.
21609
+ transform: "translateX(-100%) rotate(-45deg)",
21610
+ transformOrigin: "right center"
21611
+ },
21612
+ children: label
21613
+ },
21614
+ `${label}-${index}`
21615
+ )) }),
21616
+ /* @__PURE__ */ jsx("div", { ref: sizerRef, className: "invisible absolute top-0 left-0 flex flex-col items-start", children: labels.map((label, index) => /* @__PURE__ */ jsx(
21617
+ "span",
21618
+ {
21619
+ className: "block truncate",
21620
+ style: { maxWidth: maxWidthAt(index) },
21621
+ children: label
21622
+ },
21623
+ `${label}-${index}`
21624
+ )) })
21625
+ ]
21626
+ }
21627
+ )
21628
+ );
21629
+ }
21148
21630
  var ChartFrame = /* @__PURE__ */ forwardRef(function ChartFrame2({ title, unit, legend, legendPosition = "top", children, className, style, ...props }, ref) {
21149
21631
  return /* @__PURE__ */ jsxs(
21150
21632
  "div",
@@ -21336,44 +21818,6 @@ function linePointDimmedColor(palette, index, colors) {
21336
21818
  function dataValueColor(palette, index, colors) {
21337
21819
  return pick(DATA_VALUE_COLORS, DATA_VALUE_PALETTES, palette, index, colors);
21338
21820
  }
21339
- function useElementSize() {
21340
- const ref = useRef(null);
21341
- const [size, setSize] = useState({ width: 0, height: 0 });
21342
- useEffect(() => {
21343
- const element = ref.current;
21344
- if (!element) return;
21345
- const update = () => {
21346
- const { width, height } = element.getBoundingClientRect();
21347
- setSize(
21348
- (prev) => Math.abs(prev.width - width) < 0.5 && Math.abs(prev.height - height) < 0.5 ? prev : { width, height }
21349
- );
21350
- };
21351
- update();
21352
- if (typeof ResizeObserver === "undefined") return;
21353
- const observer = new ResizeObserver(update);
21354
- observer.observe(element);
21355
- return () => observer.disconnect();
21356
- }, []);
21357
- return { ref, size };
21358
- }
21359
- function useSvgTextMetrics() {
21360
- const ref = useRef(null);
21361
- const [metrics, setMetrics] = useState({ top: 0, width: 0 });
21362
- useEffect(() => {
21363
- const element = ref.current;
21364
- if (!element) return;
21365
- let box3;
21366
- try {
21367
- box3 = element.getBBox();
21368
- } catch {
21369
- return;
21370
- }
21371
- setMetrics(
21372
- (prev) => Math.abs(prev.top - box3.y) < 0.5 && Math.abs(prev.width - box3.width) < 0.5 ? prev : { top: box3.y, width: box3.width }
21373
- );
21374
- });
21375
- return { ref, metrics };
21376
- }
21377
21821
  var DOT_SIZE2 = 6;
21378
21822
  var DEFAULT_GAP_X = { after: 4, before: 4 };
21379
21823
  var ANCHOR_GAP_Y = 0;
@@ -21535,6 +21979,7 @@ var SBarChart = /* @__PURE__ */ forwardRef(function SBarChart2({
21535
21979
  tickCount = 8,
21536
21980
  barSize = BAR_SIZE_MAX,
21537
21981
  showValueLabel = true,
21982
+ rotateCategoryLabels = false,
21538
21983
  showLegend = true,
21539
21984
  legendPosition = "top",
21540
21985
  showTooltip = true,
@@ -21546,9 +21991,10 @@ var SBarChart = /* @__PURE__ */ forwardRef(function SBarChart2({
21546
21991
  style
21547
21992
  }, ref) {
21548
21993
  const vertical = direction === "vertical";
21994
+ const rotateLabels = rotateCategoryLabels && vertical;
21549
21995
  const { ref: plotRef, size } = useElementSize();
21550
- const { ref: axisTextRef, size: axisText } = useElementSize();
21551
- const { ref: axisSampleRef, size: axisSample } = useElementSize();
21996
+ const { ref: axisTextRef, size: axisText } = useElementSize(rotateLabels);
21997
+ const { ref: axisSampleRef, size: axisSample } = useElementSize(rotateLabels);
21552
21998
  const clipId = useId();
21553
21999
  const [hoveredCategory, setHoveredCategory] = useState(null);
21554
22000
  const perCategory = series.length === 1;
@@ -22007,7 +22453,29 @@ var SBarChart = /* @__PURE__ */ forwardRef(function SBarChart2({
22007
22453
  // 눈금은 왼쪽 한 줄뿐이다 — 오른쪽에도 적으면 서로 다른 단위처럼 읽힌다.
22008
22454
  gridTemplateColumns: "max-content 1fr"
22009
22455
  },
22010
- children: vertical ? /* @__PURE__ */ jsxs(Fragment, { children: [
22456
+ children: rotateLabels ? (
22457
+ // 기울인 이름은 눈금 열 아래까지 뻗으므로 스크롤 상자가 눈금 열까지 감싼다.
22458
+ /* @__PURE__ */ jsx(
22459
+ ChartRotatedAxisLayout,
22460
+ {
22461
+ axis: /* @__PURE__ */ jsx(
22462
+ ChartAxisLabels,
22463
+ {
22464
+ scale,
22465
+ direction,
22466
+ format: formatValue,
22467
+ sampleRef: axisSampleRef
22468
+ }
22469
+ ),
22470
+ axisStyle: { height: plotHeight, ...inset },
22471
+ plot,
22472
+ minPlotWidth,
22473
+ labels: categories,
22474
+ xAt: (index) => layout.offset + (index + 0.5) * layout.band,
22475
+ labelsRef: axisTextRef
22476
+ }
22477
+ )
22478
+ ) : vertical ? /* @__PURE__ */ jsxs(Fragment, { children: [
22011
22479
  /* @__PURE__ */ jsx("div", { style: { height: plotHeight, ...inset }, children: /* @__PURE__ */ jsx(
22012
22480
  ChartAxisLabels,
22013
22481
  {
@@ -22138,6 +22606,7 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22138
22606
  tickCount = 8,
22139
22607
  showPoint = true,
22140
22608
  showValueLabel = false,
22609
+ rotateCategoryLabels = false,
22141
22610
  showLegend = true,
22142
22611
  legendPosition = "top",
22143
22612
  showTooltip = true,
@@ -22150,8 +22619,8 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22150
22619
  }, ref) {
22151
22620
  const smooth = curve === "smooth";
22152
22621
  const { ref: plotRef, size } = useElementSize();
22153
- const { ref: axisTextRef, size: axisText } = useElementSize();
22154
- const { ref: axisSampleRef, size: axisSample } = useElementSize();
22622
+ const { ref: axisTextRef, size: axisText } = useElementSize(rotateCategoryLabels);
22623
+ const { ref: axisSampleRef, size: axisSample } = useElementSize(rotateCategoryLabels);
22155
22624
  const gradientId = useId();
22156
22625
  const [hoveredCategory, setHoveredCategory] = useState(null);
22157
22626
  const colorAt = useCallback(
@@ -22185,23 +22654,16 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22185
22654
  [rows, tickCount, max]
22186
22655
  );
22187
22656
  const count = categories.length;
22188
- const step = count > 1 ? size.width / (count - 1) : 0;
22189
- const xAt = useCallback(
22190
- (index) => count > 1 ? index * step : size.width / 2,
22191
- [count, step, size.width]
22192
- );
22657
+ const band = Math.max(0, size.width - AXIS_EDGE_PADDING * 2) / Math.max(1, count);
22658
+ const xAt = useCallback((index) => AXIS_EDGE_PADDING + (index + 0.5) * band, [band]);
22193
22659
  const cellOf = useCallback(
22194
- (index) => count > 1 ? {
22195
- left: Math.max(0, xAt(index) - step / 2),
22196
- right: Math.min(size.width, xAt(index) + step / 2)
22197
- } : { left: 0, right: size.width },
22198
- [count, step, xAt, size.width]
22199
- );
22200
- const edgeOf = useCallback(
22201
- (index) => count > 1 && index === 0 ? "start" : count > 1 && index === count - 1 ? "end" : null,
22202
- [count]
22660
+ (index) => ({
22661
+ left: AXIS_EDGE_PADDING + index * band,
22662
+ right: AXIS_EDGE_PADDING + (index + 1) * band
22663
+ }),
22664
+ [band]
22203
22665
  );
22204
- const minPlotWidth = categories.length * BAND_MIN;
22666
+ const minPlotWidth = count * BAND_MIN + AXIS_EDGE_PADDING * 2;
22205
22667
  const totalHeight = height ?? 320;
22206
22668
  const plotHeight = typeof totalHeight === "number" ? Math.max(0, totalHeight - axisText.height) : `calc(${totalHeight} - ${axisText.height}px)`;
22207
22669
  const plotted = useMemo(() => {
@@ -22215,10 +22677,10 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22215
22677
  }, [rows, size.width, size.height, xAt, scale]);
22216
22678
  const legend = showLegend ? /* @__PURE__ */ jsx(ChartLegend, { items: series.map((s, i) => ({ name: s.name, color: colorAt(i) })) }) : null;
22217
22679
  const handleMove = (event) => {
22218
- if (!showTooltip || count === 0 || size.width <= 0 || series.length === 0) return;
22680
+ if (!showTooltip || count === 0 || band <= 0 || series.length === 0) return;
22219
22681
  const rect = event.currentTarget.getBoundingClientRect();
22220
- const along = event.clientX - rect.left;
22221
- const category = count > 1 ? Math.round(along / step) : 0;
22682
+ const along = event.clientX - rect.left - AXIS_EDGE_PADDING;
22683
+ const category = Math.floor(along / band);
22222
22684
  setHoveredCategory(category >= 0 && category < categories.length ? category : null);
22223
22685
  };
22224
22686
  const tooltipItems = hoveredCategory == null ? [] : rows.flatMap((row, seriesIndex) => {
@@ -22257,10 +22719,7 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22257
22719
  categories.map((label, index) => /* @__PURE__ */ jsx(
22258
22720
  "span",
22259
22721
  {
22260
- className: cn(
22261
- "absolute top-0 whitespace-nowrap",
22262
- edgeOf(index) === "start" ? "translate-x-0" : edgeOf(index) === "end" ? "-translate-x-full" : "-translate-x-1/2"
22263
- ),
22722
+ className: "absolute top-0 -translate-x-1/2 whitespace-nowrap",
22264
22723
  style: { left: xAt(index) },
22265
22724
  children: label
22266
22725
  },
@@ -22441,17 +22900,114 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22441
22900
  y: point.y - POINT_RADIUS - VALUE_LABEL_GAP2,
22442
22901
  className: hoveredCategory === categoryIndex ? LINE_DATA_LABEL_TYPO_HOVER : LINE_DATA_LABEL_TYPO,
22443
22902
  style: { fill: LINE_DATA_LABEL_COLOR },
22444
- textAnchor: edgeOf(categoryIndex) === "start" ? "start" : edgeOf(categoryIndex) === "end" ? "end" : "middle",
22903
+ textAnchor: "middle",
22445
22904
  dominantBaseline: "auto",
22446
22905
  children: formatValue(point.own)
22447
22906
  },
22448
22907
  `label-${seriesIndex}-${categoryIndex}`
22449
22908
  )
22450
- )
22451
- ) : [],
22452
- [showValueLabel, plotted, hoveredCategory, formatValue, edgeOf]
22909
+ )
22910
+ ) : [],
22911
+ [showValueLabel, plotted, hoveredCategory, formatValue]
22912
+ );
22913
+ const inset = { paddingTop: axisSample.height / 2, paddingBottom: axisSample.height / 2 };
22914
+ const axisLabels = /* @__PURE__ */ jsx(
22915
+ ChartAxisLabels,
22916
+ {
22917
+ scale,
22918
+ direction: "vertical",
22919
+ format: formatValue,
22920
+ sampleRef: axisSampleRef
22921
+ }
22922
+ );
22923
+ const plot = (
22924
+ // 바깥은 자리(높이·물러남)를, 안쪽은 그림을 맡는다. minWidth 는 바깥에 둬야
22925
+ // 스크롤 컨테이너가 넘치는 폭을 알아본다.
22926
+ /* @__PURE__ */ jsx(
22927
+ "div",
22928
+ {
22929
+ className: "relative min-w-0",
22930
+ style: { height: plotHeight, minWidth: minPlotWidth || void 0, ...inset },
22931
+ children: /* @__PURE__ */ jsxs(
22932
+ "div",
22933
+ {
22934
+ ref: plotRef,
22935
+ className: "relative h-full w-full",
22936
+ onMouseMove: handleMove,
22937
+ onMouseLeave: () => setHoveredCategory(null),
22938
+ children: [
22939
+ size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxs(
22940
+ "svg",
22941
+ {
22942
+ width: size.width,
22943
+ height: size.height,
22944
+ overflow: "visible",
22945
+ role: "img",
22946
+ "aria-label": `${categories.length}\uAC1C \uD56D\uBAA9, ${series.length}\uAC1C \uACC4\uC5F4 ${stacked ? "\uB204\uC801 " : ""}\uAEBE\uC740\uC120 \uADF8\uB798\uD504`,
22947
+ children: [
22948
+ /* @__PURE__ */ jsx(
22949
+ "text",
22950
+ {
22951
+ ref: valueLabelRef,
22952
+ "aria-hidden": true,
22953
+ x: 0,
22954
+ y: 0,
22955
+ className: LINE_DATA_LABEL_TYPO_HOVER,
22956
+ dominantBaseline: "auto",
22957
+ style: { visibility: "hidden" },
22958
+ children: "0"
22959
+ }
22960
+ ),
22961
+ /* @__PURE__ */ jsx(ChartGrid, { scale, direction: "vertical", width: size.width, height: size.height }),
22962
+ hoveredCategory != null && /* @__PURE__ */ jsx(
22963
+ "rect",
22964
+ {
22965
+ "aria-hidden": true,
22966
+ x: cellOf(hoveredCategory).left,
22967
+ y: 0,
22968
+ width: cellOf(hoveredCategory).right - cellOf(hoveredCategory).left,
22969
+ height: size.height,
22970
+ style: {
22971
+ fill: "var(--cmp-chart-barContainer-hover-bg)",
22972
+ fillOpacity: "var(--cmp-chart-barContainer-hover-opacity)"
22973
+ }
22974
+ }
22975
+ ),
22976
+ areaGradients,
22977
+ areaMask,
22978
+ area && /* @__PURE__ */ jsx("g", { mask: `url(#${areaMaskId})`, children: areas }),
22979
+ lines,
22980
+ pointHalos,
22981
+ pointDots,
22982
+ valueLabels
22983
+ ]
22984
+ }
22985
+ ),
22986
+ showTooltip && hoveredCategory != null && tooltipAnchor != null && tooltipItems.length > 0 && /* @__PURE__ */ jsx(
22987
+ ChartTooltip,
22988
+ {
22989
+ title: categories[hoveredCategory] ?? "",
22990
+ items: tooltipItems,
22991
+ anchor: tooltipAnchor,
22992
+ bounds: size,
22993
+ gapX: { after: -4, before: -4 },
22994
+ footer: tooltipFooter?.({
22995
+ categoryIndex: hoveredCategory,
22996
+ name: categories[hoveredCategory] ?? "",
22997
+ points: rows.flatMap((row, seriesIndex) => {
22998
+ const point = row[hoveredCategory];
22999
+ if (point == null) return [];
23000
+ return [{ seriesIndex, categoryIndex: hoveredCategory, value: point.own }];
23001
+ })
23002
+ })
23003
+ }
23004
+ )
23005
+ ]
23006
+ }
23007
+ )
23008
+ }
23009
+ )
22453
23010
  );
22454
- const inset = { paddingTop: axisSample.height / 2, paddingBottom: axisSample.height / 2 };
22455
23011
  return /* @__PURE__ */ jsx(
22456
23012
  ChartFrame,
22457
23013
  {
@@ -22462,7 +23018,7 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22462
23018
  legendPosition,
22463
23019
  className,
22464
23020
  style,
22465
- children: /* @__PURE__ */ jsxs(
23021
+ children: /* @__PURE__ */ jsx(
22466
23022
  "div",
22467
23023
  {
22468
23024
  className: "grid",
@@ -22471,119 +23027,27 @@ var SLineChart = /* @__PURE__ */ forwardRef(function SLineChart2({
22471
23027
  // 눈금은 왼쪽 한 줄뿐이다 — 오른쪽에도 적으면 서로 다른 단위처럼 읽힌다.
22472
23028
  gridTemplateColumns: "max-content 1fr"
22473
23029
  },
22474
- children: [
22475
- /* @__PURE__ */ jsx("div", { style: { height: plotHeight, ...inset }, children: /* @__PURE__ */ jsx(
22476
- ChartAxisLabels,
22477
- {
22478
- scale,
22479
- direction: "vertical",
22480
- format: formatValue,
22481
- sampleRef: axisSampleRef
22482
- }
22483
- ) }),
22484
- /* @__PURE__ */ jsxs(
22485
- "div",
23030
+ children: rotateCategoryLabels ? (
23031
+ // 기울인 이름은 눈금 열 아래까지 뻗으므로 스크롤 상자가 눈금 열까지 감싼다.
23032
+ /* @__PURE__ */ jsx(
23033
+ ChartRotatedAxisLayout,
22486
23034
  {
22487
- className: "min-w-0 overflow-x-auto",
22488
- style: { marginInline: -POINT_HOVER_EXTENT, paddingInline: POINT_HOVER_EXTENT },
22489
- children: [
22490
- /* @__PURE__ */ jsx(
22491
- "div",
22492
- {
22493
- className: "relative min-w-0",
22494
- style: { height: plotHeight, minWidth: minPlotWidth || void 0, ...inset },
22495
- children: /* @__PURE__ */ jsxs(
22496
- "div",
22497
- {
22498
- ref: plotRef,
22499
- className: "relative h-full w-full",
22500
- onMouseMove: handleMove,
22501
- onMouseLeave: () => setHoveredCategory(null),
22502
- children: [
22503
- size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxs(
22504
- "svg",
22505
- {
22506
- width: size.width,
22507
- height: size.height,
22508
- overflow: "visible",
22509
- role: "img",
22510
- "aria-label": `${categories.length}\uAC1C \uD56D\uBAA9, ${series.length}\uAC1C \uACC4\uC5F4 ${stacked ? "\uB204\uC801 " : ""}\uAEBE\uC740\uC120 \uADF8\uB798\uD504`,
22511
- children: [
22512
- /* @__PURE__ */ jsx(
22513
- "text",
22514
- {
22515
- ref: valueLabelRef,
22516
- "aria-hidden": true,
22517
- x: 0,
22518
- y: 0,
22519
- className: LINE_DATA_LABEL_TYPO_HOVER,
22520
- dominantBaseline: "auto",
22521
- style: { visibility: "hidden" },
22522
- children: "0"
22523
- }
22524
- ),
22525
- /* @__PURE__ */ jsx(
22526
- ChartGrid,
22527
- {
22528
- scale,
22529
- direction: "vertical",
22530
- width: size.width,
22531
- height: size.height
22532
- }
22533
- ),
22534
- hoveredCategory != null && /* @__PURE__ */ jsx(
22535
- "rect",
22536
- {
22537
- "aria-hidden": true,
22538
- x: cellOf(hoveredCategory).left,
22539
- y: 0,
22540
- width: cellOf(hoveredCategory).right - cellOf(hoveredCategory).left,
22541
- height: size.height,
22542
- style: {
22543
- fill: "var(--cmp-chart-barContainer-hover-bg)",
22544
- fillOpacity: "var(--cmp-chart-barContainer-hover-opacity)"
22545
- }
22546
- }
22547
- ),
22548
- areaGradients,
22549
- areaMask,
22550
- area && /* @__PURE__ */ jsx("g", { mask: `url(#${areaMaskId})`, children: areas }),
22551
- lines,
22552
- pointHalos,
22553
- pointDots,
22554
- valueLabels
22555
- ]
22556
- }
22557
- ),
22558
- showTooltip && hoveredCategory != null && tooltipAnchor != null && tooltipItems.length > 0 && /* @__PURE__ */ jsx(
22559
- ChartTooltip,
22560
- {
22561
- title: categories[hoveredCategory] ?? "",
22562
- items: tooltipItems,
22563
- anchor: tooltipAnchor,
22564
- bounds: size,
22565
- gapX: { after: -4, before: -4 },
22566
- footer: tooltipFooter?.({
22567
- categoryIndex: hoveredCategory,
22568
- name: categories[hoveredCategory] ?? "",
22569
- points: rows.flatMap((row, seriesIndex) => {
22570
- const point = row[hoveredCategory];
22571
- if (point == null) return [];
22572
- return [{ seriesIndex, categoryIndex: hoveredCategory, value: point.own }];
22573
- })
22574
- })
22575
- }
22576
- )
22577
- ]
22578
- }
22579
- )
22580
- }
22581
- ),
22582
- /* @__PURE__ */ jsx("div", { ref: axisTextRef, children: categoryLabels })
22583
- ]
23035
+ axis: axisLabels,
23036
+ axisStyle: { height: plotHeight, ...inset },
23037
+ plot,
23038
+ minPlotWidth,
23039
+ labels: categories,
23040
+ xAt,
23041
+ labelsRef: axisTextRef
22584
23042
  }
22585
23043
  )
22586
- ]
23044
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
23045
+ /* @__PURE__ */ jsx("div", { style: { height: plotHeight, ...inset }, children: axisLabels }),
23046
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 overflow-x-auto", children: [
23047
+ plot,
23048
+ /* @__PURE__ */ jsx("div", { ref: axisTextRef, children: categoryLabels })
23049
+ ] })
23050
+ ] })
22587
23051
  }
22588
23052
  )
22589
23053
  }
@@ -28658,212 +29122,171 @@ function formatTimeDisplay(value, type) {
28658
29122
  const displayHour = hour % 12 === 0 ? 12 : hour % 12;
28659
29123
  return `${meridiem} ${padTimePart(displayHour)}:${padTimePart(minute)}`;
28660
29124
  }
28661
- var TIME_SELECTOR_WIDTH = 136;
28662
- var MERIDIEM_TIME_SELECTOR_WIDTH = 154;
28663
- function TimeSelector({
29125
+ function scrollSelectedIntoView(list, index, center) {
29126
+ const top = index * MODE_ITEM_HEIGHT;
29127
+ if (center) {
29128
+ list.scrollTop = Math.max(0, top - (list.clientHeight - MODE_ITEM_HEIGHT) / 2);
29129
+ return;
29130
+ }
29131
+ if (top < list.scrollTop) list.scrollTop = top;
29132
+ else if (top + MODE_ITEM_HEIGHT > list.scrollTop + list.clientHeight)
29133
+ list.scrollTop = top + MODE_ITEM_HEIGHT - list.clientHeight;
29134
+ }
29135
+ function lastIndex(arr, pred) {
29136
+ for (let i = arr.length - 1; i >= 0; i--) if (pred(arr[i], i)) return i;
29137
+ return -1;
29138
+ }
29139
+ function ListColumn({ ariaLabel, items }) {
29140
+ const listRef = useRef(null);
29141
+ const mountedRef = useRef(false);
29142
+ const selectedIndex = items.findIndex((item4) => item4.selected);
29143
+ const tabIndexTarget = selectedIndex >= 0 ? selectedIndex : items.findIndex((item4) => !item4.disabled);
29144
+ useLayoutEffect(() => {
29145
+ const list = listRef.current;
29146
+ if (!list || selectedIndex < 0) return;
29147
+ scrollSelectedIntoView(list, selectedIndex, !mountedRef.current);
29148
+ mountedRef.current = true;
29149
+ }, [selectedIndex]);
29150
+ const onKeyDown = (event) => {
29151
+ const buttons = [...listRef.current?.querySelectorAll("button") ?? []];
29152
+ const current = buttons.indexOf(document.activeElement);
29153
+ if (current < 0) return;
29154
+ const enabled = (item4, i) => !item4.disabled && i !== current;
29155
+ let next;
29156
+ if (event.key === "ArrowDown") next = items.findIndex((it, i) => i > current && enabled(it, i));
29157
+ else if (event.key === "ArrowUp")
29158
+ next = lastIndex(items, (it, i) => i < current && enabled(it, i));
29159
+ else if (event.key === "Home") next = items.findIndex((it) => !it.disabled);
29160
+ else if (event.key === "End") next = lastIndex(items, (it) => !it.disabled);
29161
+ else return;
29162
+ event.preventDefault();
29163
+ if (next < 0) return;
29164
+ buttons[next]?.focus({ preventScroll: true });
29165
+ if (listRef.current) scrollSelectedIntoView(listRef.current, next, false);
29166
+ };
29167
+ return /* @__PURE__ */ 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__ */ jsx(
29168
+ "div",
29169
+ {
29170
+ ref: listRef,
29171
+ role: "listbox",
29172
+ "aria-label": ariaLabel,
29173
+ tabIndex: -1,
29174
+ className: "h-full overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
29175
+ onKeyDown,
29176
+ children: items.map((item4, index) => /* @__PURE__ */ jsx(
29177
+ "button",
29178
+ {
29179
+ type: "button",
29180
+ role: "option",
29181
+ "aria-selected": item4.selected,
29182
+ disabled: item4.disabled,
29183
+ tabIndex: index === tabIndexTarget ? 0 : -1,
29184
+ className: cn(modeListItemClass(item4.selected, item4.disabled), "outline-offset-[-2px]"),
29185
+ onClick: () => !item4.disabled && item4.onSelect(),
29186
+ children: item4.label
29187
+ },
29188
+ item4.key
29189
+ ))
29190
+ }
29191
+ ) });
29192
+ }
29193
+ var HOURS_24 = Array.from({ length: 24 }, (_, i) => i);
29194
+ var HOURS_12 = [12, ...Array.from({ length: 11 }, (_, i) => i + 1)];
29195
+ function TimeListbox({
28664
29196
  value,
28665
29197
  showMeridiem,
28666
29198
  minuteStep,
28667
- onChange
29199
+ isDisabled,
29200
+ onChange,
29201
+ labelPrefix,
29202
+ elevated = true
28668
29203
  }) {
28669
- const parsed = useMemo(() => parseTimeValue(value), [value]);
28670
- const [hour, setHour] = useState(parsed.hour);
28671
- const [minute, setMinute] = useState(parsed.minute);
28672
- const [editingPart, setEditingPart] = useState(null);
28673
- const [inputDraft, setInputDraft] = useState({});
29204
+ const toState = (v) => v ? parseTimeValue(v) : null;
29205
+ const [time, setTime] = useState(() => toState(value));
28674
29206
  useEffect(() => {
28675
- setHour(parsed.hour);
28676
- setMinute(parsed.minute);
28677
- }, [parsed.hour, parsed.minute]);
28678
- const meridiem = hour >= 12 ? "PM" : "AM";
28679
- const displayHour = showMeridiem ? hour % 12 === 0 ? 12 : hour % 12 : hour;
28680
- const commit = (nextHour, nextMinute) => {
28681
- const h = clampTimePart(nextHour, 0, 23);
28682
- const m = clampTimePart(nextMinute, 0, 59);
28683
- setHour(h);
28684
- setMinute(m);
28685
- onChange(formatTimeValue(h, m));
28686
- };
28687
- const setMeridiem = (next) => {
28688
- if (next === meridiem) return;
28689
- commit(next === "AM" ? hour - 12 : hour + 12, minute);
28690
- };
28691
- const changeMinute = (delta) => {
28692
- const next = minute + delta;
28693
- if (next > 59) {
28694
- commit((hour + 1) % 24, next - 60);
28695
- return;
28696
- }
28697
- if (next < 0) {
28698
- commit((hour + 23) % 24, next + 60);
28699
- return;
28700
- }
28701
- commit(hour, next);
28702
- };
28703
- const changeHour = (delta) => commit((hour + delta + 24) % 24, minute);
28704
- const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
28705
- const beginEdit = (part, currentValue) => {
28706
- setEditingPart(part);
28707
- setInputDraft((prev) => ({ ...prev, [part]: String(currentValue).padStart(2, "0") }));
28708
- };
28709
- const commitHourDraft = (draft) => {
28710
- const clamped = clampTimePart(Number(draft), showMeridiem ? 1 : 0, showMeridiem ? 12 : 23);
28711
- const nextHour = showMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
28712
- commit(nextHour, minute);
28713
- return clamped;
28714
- };
28715
- const commitMinuteDraft = (draft) => {
28716
- const clamped = clampTimePart(Number(draft), 0, 59);
28717
- commit(hour, clamped);
28718
- return clamped;
28719
- };
28720
- const endEdit = (part) => {
28721
- const draft = inputDraft[part];
28722
- if (part === "hour" && draft != null && draft !== "") {
28723
- commitHourDraft(draft);
28724
- }
28725
- if (part === "minute" && draft != null && draft !== "") {
28726
- commitMinuteDraft(draft);
28727
- }
28728
- setEditingPart(null);
28729
- setInputDraft({});
28730
- };
28731
- const inputStyle = {
28732
- width: "var(--cmp-timepicker-selector-time-input-width)",
28733
- height: "var(--cmp-textinput-sm-height)",
28734
- borderRadius: "var(--cmp-textinput-sm-radius)",
28735
- border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
28736
- color: "var(--cmp-textinput-text-default)",
28737
- fontSize: 12,
28738
- lineHeight: "20px"
29207
+ setTime(toState(value));
29208
+ }, [value]);
29209
+ const step = Math.max(1, Math.floor(minuteStep));
29210
+ const minutes = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step);
29211
+ const blocked = (h, m) => isDisabled?.(h * 60 + m) ?? false;
29212
+ const hourBlocked = (h) => minutes.every((m) => blocked(h, m));
29213
+ const commit = (hours, preferred, part) => {
29214
+ let next = blocked(preferred.hour, preferred.minute) ? null : preferred;
29215
+ for (const h of hours) {
29216
+ if (next) break;
29217
+ const m = minutes.find((mm) => !blocked(h, mm));
29218
+ if (m !== void 0) next = { hour: h, minute: m };
29219
+ }
29220
+ if (!next) return;
29221
+ setTime(next);
29222
+ onChange(formatTimeValue(next.hour, next.minute), part);
28739
29223
  };
28740
- const renderColumn = (type) => {
28741
- const isHour = type === "hour";
28742
- const val = isHour ? displayHour : minute;
28743
- const displayValue2 = editingPart === type ? inputDraft[type] ?? "" : String(val).padStart(2, "0");
28744
- return /* @__PURE__ */ jsxs(
28745
- "div",
28746
- {
28747
- className: "flex flex-col items-center justify-center",
28748
- style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
28749
- children: [
28750
- /* @__PURE__ */ jsx(
28751
- SGhostButton,
28752
- {
28753
- icon: "chevronUp",
28754
- size: "xs",
28755
- ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
28756
- onClick: () => isHour ? changeHour(1) : changeMinute(minuteStep)
28757
- }
28758
- ),
28759
- /* @__PURE__ */ jsx(
28760
- "input",
28761
- {
28762
- "aria-label": isHour ? "\uC2DC" : "\uBD84",
28763
- inputMode: "numeric",
28764
- value: displayValue2,
28765
- placeholder: "-",
28766
- onFocus: () => beginEdit(type, val),
28767
- onBlur: () => endEdit(type),
28768
- onChange: (event) => {
28769
- const draft = sanitizeInput(event.target.value);
28770
- setInputDraft((prev) => ({ ...prev, [type]: draft }));
28771
- if (draft === "") return;
28772
- const raw = Number(draft);
28773
- if (isHour) {
28774
- if (draft.length < 2) return;
28775
- const clamped2 = commitHourDraft(draft);
28776
- const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
28777
- setInputDraft((prev) => ({ ...prev, [type]: displayDraft2 }));
28778
- return;
28779
- }
28780
- if (draft.length < 2) return;
28781
- const clamped = commitMinuteDraft(draft);
28782
- const displayDraft = raw === clamped ? draft : String(clamped);
28783
- setInputDraft((prev) => ({ ...prev, [type]: displayDraft }));
28784
- },
28785
- className: "box-border bg-white text-center outline-none",
28786
- style: inputStyle
28787
- }
28788
- ),
28789
- /* @__PURE__ */ jsx(
28790
- SGhostButton,
28791
- {
28792
- icon: "chevronDown",
28793
- size: "xs",
28794
- ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
28795
- onClick: () => isHour ? changeHour(-1) : changeMinute(-minuteStep)
28796
- }
28797
- )
28798
- ]
29224
+ const hour = time?.hour;
29225
+ const minute = time?.minute ?? 0;
29226
+ const meridiem = hour === void 0 ? void 0 : hour >= 12 ? "PM" : "AM";
29227
+ const prefix = labelPrefix ? `${labelPrefix} ` : "";
29228
+ const meridiemItems = ["AM", "PM"].map((item4) => {
29229
+ const base = item4 === "AM" ? 0 : 12;
29230
+ const range = HOURS_24.slice(base, base + 12);
29231
+ return {
29232
+ key: item4,
29233
+ label: item4 === "AM" ? "\uC624\uC804" : "\uC624\uD6C4",
29234
+ selected: meridiem === item4,
29235
+ disabled: range.every(hourBlocked),
29236
+ onSelect: () => {
29237
+ if (meridiem === item4) return;
29238
+ const h = hour === void 0 ? base : hour % 12 + base;
29239
+ commit(range, { hour: h, minute }, "meridiem");
28799
29240
  }
28800
- );
28801
- };
29241
+ };
29242
+ });
29243
+ const hourItems = showMeridiem ? HOURS_12.map((display) => {
29244
+ const h = display % 12 + (meridiem === "PM" ? 12 : 0);
29245
+ return {
29246
+ key: String(display),
29247
+ label: `${padTimePart(display)}\uC2DC`,
29248
+ selected: hour === h,
29249
+ disabled: hourBlocked(h),
29250
+ onSelect: () => commit([h], { hour: h, minute }, "hour")
29251
+ };
29252
+ }) : HOURS_24.map((h) => ({
29253
+ key: String(h),
29254
+ label: `${padTimePart(h)}\uC2DC`,
29255
+ selected: hour === h,
29256
+ disabled: hourBlocked(h),
29257
+ onSelect: () => commit([h], { hour: h, minute }, "hour")
29258
+ }));
29259
+ const minuteHour = hour ?? (meridiem === "PM" ? 12 : 0);
29260
+ const minuteItems = minutes.map((m) => ({
29261
+ key: String(m),
29262
+ label: `${padTimePart(m)}\uBD84`,
29263
+ selected: time != null && minute === m,
29264
+ disabled: blocked(minuteHour, m),
29265
+ onSelect: () => commit([minuteHour], { hour: minuteHour, minute: m }, "minute")
29266
+ }));
29267
+ const divider = /* @__PURE__ */ jsx(
29268
+ "div",
29269
+ {
29270
+ "aria-hidden": true,
29271
+ className: "w-[var(--border-width-1)] self-stretch bg-[var(--sys-color-divider-default)]"
29272
+ }
29273
+ );
28802
29274
  return /* @__PURE__ */ jsxs(
28803
29275
  "div",
28804
29276
  {
28805
- className: "flex flex-col items-stretch justify-center overflow-hidden",
28806
- style: {
28807
- background: "var(--cmp-timepicker-selector-bg)",
28808
- borderRadius: "var(--cmp-timepicker-selector-radius)",
28809
- boxShadow: "4px 8px 16px -4px rgba(34,34,34,0.1), 0 0 24px -6px rgba(34,34,34,0.12)"
28810
- },
29277
+ className: cn(
29278
+ "inline-flex gap-[var(--cmp-datepicker-mode-yearMonth-gap)] overflow-hidden bg-[var(--cmp-datepicker-mode-bg)]",
29279
+ elevated && "rounded-[var(--cmp-datepicker-mode-radius)] shadow-normal-sm"
29280
+ ),
29281
+ style: { height: MODE_LIST_HEIGHT },
28811
29282
  children: [
28812
- showMeridiem && /* @__PURE__ */ jsx(
28813
- "div",
28814
- {
28815
- className: "flex items-center justify-center",
28816
- style: {
28817
- padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
28818
- },
28819
- children: /* @__PURE__ */ jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item4, index) => {
28820
- const selected = item4 === meridiem;
28821
- return /* @__PURE__ */ jsx(
28822
- "button",
28823
- {
28824
- type: "button",
28825
- onClick: () => setMeridiem(item4),
28826
- className: cn(
28827
- "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]",
28828
- index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)]" : "rounded-r-[var(--cmp-select-sm-radius)]",
28829
- index === 0 ? "-mr-px" : "",
28830
- 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)]"
28831
- ),
28832
- children: item4 === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
28833
- },
28834
- item4
28835
- );
28836
- }) })
28837
- }
28838
- ),
28839
- showMeridiem && /* @__PURE__ */ jsx("div", { className: "h-px w-full bg-[var(--sys-color-divider-default)]" }),
28840
- /* @__PURE__ */ jsxs(
28841
- "div",
28842
- {
28843
- className: "flex items-center justify-center",
28844
- style: {
28845
- gap: "var(--cmp-timepicker-selector-time-gap)",
28846
- padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
28847
- },
28848
- children: [
28849
- renderColumn("hour"),
28850
- /* @__PURE__ */ jsx(
28851
- "span",
28852
- {
28853
- className: "text-center",
28854
- style: {
28855
- width: "var(--cmp-timepicker-selector-colon-size)",
28856
- color: "var(--cmp-field-hint-color)",
28857
- fontSize: "var(--cmp-timepicker-selector-colon-size)",
28858
- lineHeight: "var(--cmp-timepicker-selector-colon-size)"
28859
- },
28860
- children: ":"
28861
- }
28862
- ),
28863
- renderColumn("minute")
28864
- ]
28865
- }
28866
- )
29283
+ showMeridiem && /* @__PURE__ */ jsxs(Fragment, { children: [
29284
+ /* @__PURE__ */ jsx(ListColumn, { ariaLabel: `${prefix}\uC624\uC804/\uC624\uD6C4`, items: meridiemItems }),
29285
+ divider
29286
+ ] }),
29287
+ /* @__PURE__ */ jsx(ListColumn, { ariaLabel: `${prefix}\uC2DC`, items: hourItems }),
29288
+ divider,
29289
+ /* @__PURE__ */ jsx(ListColumn, { ariaLabel: `${prefix}\uBD84`, items: minuteItems })
28867
29290
  ]
28868
29291
  }
28869
29292
  );
@@ -28901,11 +29324,9 @@ function STimePicker({
28901
29324
  style
28902
29325
  }) {
28903
29326
  const portalContainer = usePortalContainer();
28904
- const triggerRef = useRef(null);
28905
29327
  const [open, setOpen] = useState(false);
28906
29328
  const triggerId = useFieldControlId();
28907
29329
  const triggerTextId = `${triggerId}-text`;
28908
- const [popoverAlignOffset, setPopoverAlignOffset] = useState(0);
28909
29330
  const [focused, setFocused] = useState(false);
28910
29331
  const [ruleError, setRuleError] = useState("");
28911
29332
  const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
@@ -28926,11 +29347,6 @@ function STimePicker({
28926
29347
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
28927
29348
  const commitOpen = (next) => {
28928
29349
  if (disabled) return;
28929
- if (next) {
28930
- const triggerWidth = triggerRef.current?.getBoundingClientRect().width ?? 0;
28931
- const selectorWidth = effectiveShowMeridiem ? MERIDIEM_TIME_SELECTOR_WIDTH : TIME_SELECTOR_WIDTH;
28932
- setPopoverAlignOffset(Math.max(0, (triggerWidth - selectorWidth) / 2));
28933
- }
28934
29350
  setOpen(next);
28935
29351
  onOpenChange?.(next);
28936
29352
  if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
@@ -28969,7 +29385,6 @@ function STimePicker({
28969
29385
  /* @__PURE__ */ jsx(RPopover13.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
28970
29386
  "div",
28971
29387
  {
28972
- ref: triggerRef,
28973
29388
  id: triggerId,
28974
29389
  role: "button",
28975
29390
  "aria-disabled": disabled,
@@ -29033,20 +29448,22 @@ function STimePicker({
29033
29448
  RPopover13.Content,
29034
29449
  {
29035
29450
  side: "bottom",
29036
- align: "start",
29451
+ align: "center",
29037
29452
  sideOffset: 4,
29038
- alignOffset: popoverAlignOffset,
29039
29453
  className: cn(
29040
29454
  "z-50 max-w-[var(--radix-popover-content-available-width)]",
29041
29455
  FLOATING_SLIDE_ANIM
29042
29456
  ),
29043
29457
  children: /* @__PURE__ */ jsx(
29044
- TimeSelector,
29458
+ TimeListbox,
29045
29459
  {
29046
29460
  value: normalized,
29047
29461
  showMeridiem: effectiveShowMeridiem,
29048
29462
  minuteStep,
29049
- onChange: commitValue
29463
+ onChange: (time, part) => {
29464
+ commitValue(time);
29465
+ if (part === "minute") commitOpen(false);
29466
+ }
29050
29467
  }
29051
29468
  )
29052
29469
  }
@@ -29076,7 +29493,6 @@ function formatTimeRangeDisplay(value, type) {
29076
29493
  if (value == null || value[0] === "" || value[1] === "") return "";
29077
29494
  return `${formatTimeDisplay(value[0], type)} ~ ${formatTimeDisplay(value[1], type)}`;
29078
29495
  }
29079
- var TIME_RANGE_SELECTOR_WIDTH = 345;
29080
29496
  var MIDDAY_FIELD_MIN_WIDTH = {
29081
29497
  sm: 178,
29082
29498
  md: 216
@@ -29085,6 +29501,10 @@ var MIDDAY_FIELD_MAX_WIDTH = {
29085
29501
  sm: "lg",
29086
29502
  md: "lg"
29087
29503
  };
29504
+ var toMinutes = (time) => {
29505
+ const { hour, minute } = parseTimeValue(time);
29506
+ return hour * 60 + minute;
29507
+ };
29088
29508
  function RangeTimeSelector({
29089
29509
  value,
29090
29510
  showMeridiem,
@@ -29092,233 +29512,32 @@ function RangeTimeSelector({
29092
29512
  rangeOrder,
29093
29513
  onChange
29094
29514
  }) {
29095
- const range = useMemo(() => value ?? ["00:00", "23:59"], [value]);
29096
- const [editingKey, setEditingKey] = useState(null);
29097
- const [inputDraft, setInputDraft] = useState({});
29098
- const autoAdjustedRef = useRef(null);
29099
- const getParts = (side) => parseTimeValue(side === "start" ? range[0] : range[1]);
29100
- const getMeridiem = (side) => getParts(side).hour >= 12 ? "PM" : "AM";
29101
- const getDisplayHour = (side) => {
29102
- const { hour } = getParts(side);
29103
- if (!showMeridiem) return hour;
29104
- const displayHour = hour % 12;
29105
- return displayHour === 0 ? 12 : displayHour;
29106
- };
29107
- const commit = (side, hour, minute, direction = "unknown") => {
29108
- const next = formatTimeValue(hour, minute);
29109
- onChange(
29110
- normalizeNextRange(side === "start" ? [next, range[1]] : [range[0], next], side, direction)
29111
- );
29112
- };
29113
- const getTimeMinutes2 = (time) => {
29114
- const { hour, minute } = parseTimeValue(time);
29115
- return hour * 60 + minute;
29116
- };
29117
- const normalizeNextRange = (nextRange, changedSide, direction) => {
29118
- if (rangeOrder === "allow-cross-day") return nextRange;
29119
- const [start, end] = nextRange;
29120
- const [currentStart, currentEnd] = range;
29121
- if (getTimeMinutes2(start) <= getTimeMinutes2(end)) {
29122
- const shouldKeepSynced = autoAdjustedRef.current?.side === changedSide && currentStart === currentEnd && start !== end && (direction === "unknown" || autoAdjustedRef.current.direction === direction);
29123
- if (shouldKeepSynced) {
29124
- return changedSide === "start" ? [start, start] : [end, end];
29125
- }
29126
- autoAdjustedRef.current = null;
29127
- return nextRange;
29128
- }
29129
- autoAdjustedRef.current = {
29130
- side: changedSide,
29131
- direction: changedSide === "start" ? "increase" : "decrease"
29132
- };
29133
- return changedSide === "start" ? [start, start] : [end, end];
29134
- };
29135
- const changeHour = (side, delta) => {
29136
- const { hour, minute } = getParts(side);
29137
- commit(side, (hour + delta + 24) % 24, minute, delta > 0 ? "increase" : "decrease");
29138
- };
29139
- const changeMinute = (side, delta) => {
29140
- const { hour, minute } = getParts(side);
29141
- const next = minute + delta;
29142
- if (next > 59) {
29143
- commit(side, (hour + 1) % 24, next - 60, delta > 0 ? "increase" : "decrease");
29144
- return;
29145
- }
29146
- if (next < 0) {
29147
- commit(side, (hour + 23) % 24, next + 60, delta > 0 ? "increase" : "decrease");
29515
+ const range = value ?? ["00:00", "23:59"];
29516
+ const strict = rangeOrder !== "allow-cross-day";
29517
+ const change = (side, time) => {
29518
+ if (side === "end") {
29519
+ onChange([range[0], time]);
29148
29520
  return;
29149
29521
  }
29150
- commit(side, hour, next, delta > 0 ? "increase" : "decrease");
29151
- };
29152
- const setMeridiem = (side, next) => {
29153
- const current = getMeridiem(side);
29154
- if (current === next) return;
29155
- const { hour, minute } = getParts(side);
29156
- commit(side, next === "AM" ? hour - 12 : hour + 12, minute);
29157
- };
29158
- const getEditingKey = (side, part) => `${side}-${part}`;
29159
- const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
29160
- const beginEdit = (side, part, currentValue) => {
29161
- const key = getEditingKey(side, part);
29162
- setEditingKey(key);
29163
- setInputDraft((prev) => ({ ...prev, [key]: String(currentValue).padStart(2, "0") }));
29164
- };
29165
- const commitHourDraft = (side, draft) => {
29166
- const clamped = clampTimePart(Number(draft), showMeridiem ? 1 : 0, showMeridiem ? 12 : 23);
29167
- const parts = getParts(side);
29168
- const meridiem = getMeridiem(side);
29169
- const nextHour = showMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
29170
- commit(side, nextHour, parts.minute);
29171
- return clamped;
29172
- };
29173
- const commitMinuteDraft = (side, draft) => {
29174
- const { hour } = getParts(side);
29175
- const clamped = clampTimePart(Number(draft), 0, 59);
29176
- commit(side, hour, clamped);
29177
- return clamped;
29178
- };
29179
- const endEdit = (side, part) => {
29180
- const key = getEditingKey(side, part);
29181
- const draft = inputDraft[key];
29182
- if (part === "hour" && draft != null && draft !== "") {
29183
- commitHourDraft(side, draft);
29184
- }
29185
- if (part === "minute" && draft != null && draft !== "") {
29186
- commitMinuteDraft(side, draft);
29187
- }
29188
- setEditingKey(null);
29189
- setInputDraft({});
29190
- };
29191
- const inputStyle = {
29192
- width: "var(--cmp-timepicker-selector-time-input-width)",
29193
- height: "var(--cmp-textinput-sm-height)",
29194
- borderRadius: "var(--cmp-textinput-sm-radius)",
29195
- border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
29196
- color: "var(--cmp-textinput-text-default)",
29197
- fontSize: 12,
29198
- lineHeight: "20px"
29199
- };
29200
- const renderColumn = (side, kind) => {
29201
- const isHour = kind === "hour";
29202
- const { minute } = getParts(side);
29203
- const val = isHour ? getDisplayHour(side) : minute;
29204
- const currentEditingKey = getEditingKey(side, kind);
29205
- const displayValue2 = editingKey === currentEditingKey ? inputDraft[currentEditingKey] ?? "" : String(val).padStart(2, "0");
29206
- return /* @__PURE__ */ jsxs(
29207
- "div",
29208
- {
29209
- className: "flex flex-col items-center justify-center",
29210
- style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
29211
- children: [
29212
- /* @__PURE__ */ jsx(
29213
- SGhostButton,
29214
- {
29215
- icon: "chevronUp",
29216
- size: "xs",
29217
- ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
29218
- onClick: () => isHour ? changeHour(side, 1) : changeMinute(side, minuteStep)
29219
- }
29220
- ),
29221
- /* @__PURE__ */ jsx(
29222
- "input",
29223
- {
29224
- "aria-label": `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"}`,
29225
- inputMode: "numeric",
29226
- value: displayValue2,
29227
- placeholder: "-",
29228
- onFocus: () => beginEdit(side, kind, val),
29229
- onBlur: () => endEdit(side, kind),
29230
- onChange: (event) => {
29231
- const draft = sanitizeInput(event.target.value);
29232
- setInputDraft((prev) => ({ ...prev, [currentEditingKey]: draft }));
29233
- if (draft === "") return;
29234
- const raw = Number(draft);
29235
- if (isHour) {
29236
- if (draft.length < 2) return;
29237
- const clamped2 = commitHourDraft(side, draft);
29238
- const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
29239
- setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft2 }));
29240
- return;
29241
- }
29242
- if (draft.length < 2) return;
29243
- const clamped = commitMinuteDraft(side, draft);
29244
- const displayDraft = raw === clamped ? draft : String(clamped);
29245
- setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft }));
29246
- },
29247
- className: "box-border bg-white text-center outline-none",
29248
- style: inputStyle
29249
- }
29250
- ),
29251
- /* @__PURE__ */ jsx(
29252
- SGhostButton,
29253
- {
29254
- icon: "chevronDown",
29255
- size: "xs",
29256
- ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
29257
- onClick: () => isHour ? changeHour(side, -1) : changeMinute(side, -minuteStep)
29258
- }
29259
- )
29260
- ]
29261
- }
29262
- );
29522
+ onChange([time, strict && toMinutes(time) > toMinutes(range[1]) ? time : range[1]]);
29263
29523
  };
29264
29524
  const renderPanel = (side, title) => /* @__PURE__ */ jsxs(
29265
29525
  "div",
29266
29526
  {
29267
- className: "flex flex-col items-center justify-center",
29268
- style: { gap: "var(--cmp-timepicker-selector-range-section-gap)" },
29527
+ className: "flex flex-col items-center",
29528
+ style: { gap: "var(--cmp-timepicker-selector-range-heading-gap)" },
29269
29529
  children: [
29270
- /* @__PURE__ */ jsxs(
29271
- "div",
29272
- {
29273
- className: "flex flex-col items-center",
29274
- style: { gap: "var(--cmp-timepicker-selector-range-heading-gap)" },
29275
- children: [
29276
- /* @__PURE__ */ jsx("div", { className: "whitespace-nowrap text-center text-[12px] font-bold leading-[20px] text-[var(--cmp-timepicker-selector-range-heading-color)]", children: title }),
29277
- showMeridiem && /* @__PURE__ */ jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item4, index) => {
29278
- const selected = item4 === getMeridiem(side);
29279
- return /* @__PURE__ */ jsx(
29280
- "button",
29281
- {
29282
- type: "button",
29283
- onClick: () => setMeridiem(side, item4),
29284
- className: cn(
29285
- "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]",
29286
- index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)] -mr-px" : "rounded-r-[var(--cmp-select-sm-radius)]",
29287
- 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)]"
29288
- ),
29289
- children: item4 === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
29290
- },
29291
- item4
29292
- );
29293
- }) })
29294
- ]
29295
- }
29296
- ),
29297
- /* @__PURE__ */ jsxs(
29298
- "div",
29530
+ /* @__PURE__ */ jsx("div", { className: "whitespace-nowrap text-center text-[12px] font-bold leading-[20px] text-[var(--cmp-timepicker-selector-range-heading-color)]", children: title }),
29531
+ /* @__PURE__ */ jsx(
29532
+ TimeListbox,
29299
29533
  {
29300
- className: "flex items-center justify-center",
29301
- style: {
29302
- gap: "var(--cmp-timepicker-selector-time-gap)",
29303
- padding: "0 var(--cmp-timepicker-selector-section-paddingX)"
29304
- },
29305
- children: [
29306
- renderColumn(side, "hour"),
29307
- /* @__PURE__ */ jsx(
29308
- "span",
29309
- {
29310
- className: "text-center",
29311
- style: {
29312
- width: "var(--cmp-timepicker-selector-colon-size)",
29313
- color: "var(--cmp-field-hint-color)",
29314
- fontSize: "var(--cmp-timepicker-selector-colon-size)",
29315
- lineHeight: "var(--cmp-timepicker-selector-colon-size)"
29316
- },
29317
- children: ":"
29318
- }
29319
- ),
29320
- renderColumn(side, "minute")
29321
- ]
29534
+ value: side === "start" ? range[0] : range[1],
29535
+ showMeridiem,
29536
+ minuteStep,
29537
+ labelPrefix: side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC",
29538
+ elevated: false,
29539
+ isDisabled: strict && side === "end" ? (minutes) => minutes < toMinutes(range[0]) : void 0,
29540
+ onChange: (time) => change(side, time)
29322
29541
  }
29323
29542
  )
29324
29543
  ]
@@ -29377,11 +29596,9 @@ function STimeRangePicker({
29377
29596
  style
29378
29597
  }) {
29379
29598
  const portalContainer = usePortalContainer();
29380
- const triggerRef = useRef(null);
29381
29599
  const [open, setOpen] = useState(false);
29382
29600
  const triggerId = useFieldControlId();
29383
29601
  const triggerTextId = `${triggerId}-text`;
29384
- const [popoverAlignOffset, setPopoverAlignOffset] = useState(0);
29385
29602
  const [focused, setFocused] = useState(false);
29386
29603
  const [ruleError, setRuleError] = useState("");
29387
29604
  const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
@@ -29405,10 +29622,6 @@ function STimeRangePicker({
29405
29622
  const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
29406
29623
  const commitOpen = (next) => {
29407
29624
  if (disabled) return;
29408
- if (next) {
29409
- const triggerWidth = triggerRef.current?.getBoundingClientRect().width ?? 0;
29410
- setPopoverAlignOffset(Math.max(0, (triggerWidth - TIME_RANGE_SELECTOR_WIDTH) / 2));
29411
- }
29412
29625
  setOpen(next);
29413
29626
  onOpenChange?.(next);
29414
29627
  if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
@@ -29447,7 +29660,6 @@ function STimeRangePicker({
29447
29660
  /* @__PURE__ */ jsx(RPopover13.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
29448
29661
  "div",
29449
29662
  {
29450
- ref: triggerRef,
29451
29663
  id: triggerId,
29452
29664
  role: "button",
29453
29665
  "aria-disabled": disabled,
@@ -29511,9 +29723,8 @@ function STimeRangePicker({
29511
29723
  RPopover13.Content,
29512
29724
  {
29513
29725
  side: "bottom",
29514
- align: "start",
29726
+ align: "center",
29515
29727
  sideOffset: 4,
29516
- alignOffset: popoverAlignOffset,
29517
29728
  className: cn(
29518
29729
  "z-50 max-w-[var(--radix-popover-content-available-width)]",
29519
29730
  FLOATING_SLIDE_ANIM