sellmate-design-system-react 9.0.0-beta.43 → 9.0.0-beta.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3689,14 +3689,14 @@ function SFooter({
3689
3689
  ]
3690
3690
  }
3691
3691
  ),
3692
- hasButton && /* @__PURE__ */ jsx(
3692
+ hasButton && // 스프레드를 먼저 두고 기본값을 뒤에 덮는다 — 순서를 뒤집으면 `{ color: undefined }`
3693
+ // 처럼 값 없이 넘어온 키가 footer 기본값을 지우고 SButton 기본값으로 떨어진다.
3694
+ /* @__PURE__ */ jsx(
3693
3695
  SButton,
3694
3696
  {
3697
+ ...button,
3698
+ size: "md",
3695
3699
  color: button.color ?? "primary",
3696
- outline: button.outline,
3697
- size: button.size ?? "md",
3698
- disabled: button.disabled,
3699
- onClick: () => button.onClick?.(),
3700
3700
  label: button.label ?? "\uD655\uC778"
3701
3701
  }
3702
3702
  )
@@ -4265,6 +4265,13 @@ function SDraggableProvider({ children }) {
4265
4265
  function SDraggableGroup({ children, onMove, ...rest }) {
4266
4266
  return /* @__PURE__ */ jsx(SDraggableProviderInternal, { defaultGroup: "__legacy", legacyOnMove: onMove, children: /* @__PURE__ */ jsx("div", { ...rest, children }) });
4267
4267
  }
4268
+ var INTERACTIVE_DESCENDANT_SELECTOR = 'input,textarea,select,button,a[href],label,[contenteditable="true"],[role="button"],[role="checkbox"],[role="radio"],[role="switch"],[role="tab"],[role="menuitem"]';
4269
+ function isFromInteractiveDescendant(event) {
4270
+ const target = event.target;
4271
+ if (target == null || target === event.currentTarget) return false;
4272
+ const interactive = target.closest?.(INTERACTIVE_DESCENDANT_SELECTOR);
4273
+ return interactive != null && interactive !== event.currentTarget;
4274
+ }
4268
4275
  function reorder(items, fromIndex, toIndex) {
4269
4276
  if (fromIndex === toIndex) return items;
4270
4277
  const nextItems = [...items];
@@ -4348,6 +4355,7 @@ function SDraggableListInner({
4348
4355
  const listRef = useRef(null);
4349
4356
  const fromIndexRef = useRef(null);
4350
4357
  const currentSelectedKey = selectedKey ?? internalSelectedKey;
4358
+ const selectable = selectedKey !== void 0 || defaultSelectedKey !== void 0 || onSelectedKeyChange != null;
4351
4359
  const usesDepth = getDepth != null;
4352
4360
  const depthEnabled = usesDepth && !gap;
4353
4361
  const groupDragging = dragContext?.dragging;
@@ -4507,20 +4515,28 @@ function SDraggableListInner({
4507
4515
  setInternalSelectedKey(itemKey);
4508
4516
  onSelectedKeyChange?.(itemKey);
4509
4517
  };
4518
+ const handleItemClick = (event) => {
4519
+ if (isFromInteractiveDescendant(event)) return;
4520
+ selectItem();
4521
+ };
4510
4522
  const handleItemKeyDown = (event) => {
4523
+ if (event.target !== event.currentTarget) return;
4511
4524
  if (event.key !== "Enter" && event.key !== " ") return;
4512
4525
  event.preventDefault();
4513
4526
  selectItem();
4514
4527
  };
4528
+ const selectionProps = selectable ? {
4529
+ role: "button",
4530
+ tabIndex: disabled ? -1 : 0,
4531
+ onClick: handleItemClick,
4532
+ onKeyDown: handleItemKeyDown
4533
+ } : {};
4515
4534
  return /* @__PURE__ */ jsxs(
4516
4535
  "div",
4517
4536
  {
4518
4537
  "data-draggable-list-item": true,
4519
- role: "button",
4520
- tabIndex: disabled ? -1 : 0,
4521
4538
  "aria-disabled": disabled || void 0,
4522
- onClick: selectItem,
4523
- onKeyDown: handleItemKeyDown,
4539
+ ...selectionProps,
4524
4540
  className: "relative",
4525
4541
  children: [
4526
4542
  showBefore && /* @__PURE__ */ jsx(
@@ -12708,6 +12724,130 @@ function collectGroupLeafValues(flat, groupIndex) {
12708
12724
  }
12709
12725
  var isHtmlLabel = (label) => /<[a-z][\s\S]*>/i.test(label);
12710
12726
  var extractText = (html) => html.replace(/<[^>]*>/g, "");
12727
+ var OPTION_ROW_HEIGHT = 28;
12728
+ var DEFAULT_VIRTUAL_BUFFER = 10;
12729
+ var DEFAULT_REACH_END_THRESHOLD = 10;
12730
+ var DEFAULT_SEARCH_DEBOUNCE = 300;
12731
+
12732
+ // src/components/SSelect/row-metrics.ts
12733
+ function createRowMetrics(count, estimate) {
12734
+ const safeCount = Math.max(0, Math.floor(count));
12735
+ const safeEstimate = Math.max(1, estimate);
12736
+ const metrics = {
12737
+ sizes: new Array(safeCount).fill(safeEstimate),
12738
+ measured: new Array(safeCount).fill(false),
12739
+ offsets: [],
12740
+ estimate: safeEstimate,
12741
+ measuredCount: 0,
12742
+ measuredTotal: 0,
12743
+ dirtyFrom: 0
12744
+ };
12745
+ rebuildOffsets(metrics);
12746
+ return metrics;
12747
+ }
12748
+ function resizeRowMetrics(metrics, count) {
12749
+ const next = Math.max(0, Math.floor(count));
12750
+ const prev = metrics.sizes.length;
12751
+ if (next === prev) return;
12752
+ if (next < prev) {
12753
+ for (let i = next; i < prev; i++) {
12754
+ if (metrics.measured[i]) {
12755
+ metrics.measuredCount--;
12756
+ metrics.measuredTotal -= metrics.sizes[i];
12757
+ }
12758
+ }
12759
+ metrics.sizes.length = next;
12760
+ metrics.measured.length = next;
12761
+ } else {
12762
+ for (let i = prev; i < next; i++) {
12763
+ metrics.sizes.push(metrics.estimate);
12764
+ metrics.measured.push(false);
12765
+ }
12766
+ }
12767
+ markDirty(metrics, Math.min(prev, next));
12768
+ }
12769
+ function resetRowMetrics(metrics, count) {
12770
+ const fresh = createRowMetrics(count, metrics.estimate);
12771
+ metrics.sizes = fresh.sizes;
12772
+ metrics.measured = fresh.measured;
12773
+ metrics.offsets = fresh.offsets;
12774
+ metrics.measuredCount = 0;
12775
+ metrics.measuredTotal = 0;
12776
+ metrics.dirtyFrom = Infinity;
12777
+ }
12778
+ function setRowSize(metrics, index, size) {
12779
+ if (index < 0 || index >= metrics.sizes.length) return false;
12780
+ const next = Math.max(1, Math.round(size));
12781
+ const wasMeasured = metrics.measured[index];
12782
+ const prev = metrics.sizes[index];
12783
+ if (wasMeasured && prev === next) return false;
12784
+ if (wasMeasured) {
12785
+ metrics.measuredTotal += next - prev;
12786
+ } else {
12787
+ metrics.measured[index] = true;
12788
+ metrics.measuredCount++;
12789
+ metrics.measuredTotal += next;
12790
+ }
12791
+ metrics.sizes[index] = next;
12792
+ let changedFrom = prev === next ? Infinity : index;
12793
+ const average = Math.max(1, Math.round(metrics.measuredTotal / metrics.measuredCount));
12794
+ if (average !== metrics.estimate) {
12795
+ metrics.estimate = average;
12796
+ for (let i = 0; i < metrics.sizes.length; i++) {
12797
+ if (metrics.measured[i] || metrics.sizes[i] === average) continue;
12798
+ metrics.sizes[i] = average;
12799
+ if (i < changedFrom) changedFrom = i;
12800
+ }
12801
+ }
12802
+ if (changedFrom === Infinity) return false;
12803
+ markDirty(metrics, changedFrom);
12804
+ return true;
12805
+ }
12806
+ function markDirty(metrics, from) {
12807
+ metrics.dirtyFrom = Math.min(metrics.dirtyFrom, Math.max(0, from));
12808
+ }
12809
+ function rebuildOffsets(metrics) {
12810
+ const count = metrics.sizes.length;
12811
+ const stale = metrics.offsets.length !== count + 1;
12812
+ if (!stale && metrics.dirtyFrom === Infinity) return;
12813
+ const start = stale ? 0 : Math.min(metrics.dirtyFrom, count);
12814
+ const acc0 = start === 0 ? 0 : metrics.offsets[start] ?? 0;
12815
+ metrics.offsets.length = count + 1;
12816
+ let acc = acc0;
12817
+ for (let i = start; i < count; i++) {
12818
+ metrics.offsets[i] = acc;
12819
+ acc += metrics.sizes[i];
12820
+ }
12821
+ metrics.offsets[count] = acc;
12822
+ metrics.dirtyFrom = Infinity;
12823
+ }
12824
+ function totalSize(metrics) {
12825
+ rebuildOffsets(metrics);
12826
+ return metrics.offsets[metrics.sizes.length] ?? 0;
12827
+ }
12828
+ function offsetOf(metrics, index) {
12829
+ rebuildOffsets(metrics);
12830
+ const clamped = Math.max(0, Math.min(Math.floor(index), metrics.sizes.length));
12831
+ return metrics.offsets[clamped] ?? 0;
12832
+ }
12833
+ function sizeOf(metrics, index) {
12834
+ if (index < 0 || index >= metrics.sizes.length) return metrics.estimate;
12835
+ return metrics.sizes[index];
12836
+ }
12837
+ function indexAtOffset(metrics, y) {
12838
+ rebuildOffsets(metrics);
12839
+ const count = metrics.sizes.length;
12840
+ if (count === 0) return 0;
12841
+ const target = Math.max(0, y);
12842
+ let lo = 0;
12843
+ let hi = count - 1;
12844
+ while (lo < hi) {
12845
+ const mid = lo + hi + 1 >> 1;
12846
+ if (metrics.offsets[mid] <= target) lo = mid;
12847
+ else hi = mid - 1;
12848
+ }
12849
+ return lo;
12850
+ }
12711
12851
  var SEARCH_THRESHOLD = 11;
12712
12852
  var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12713
12853
  value,
@@ -12729,6 +12869,14 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12729
12869
  dropdownHeight = "640px",
12730
12870
  dropdownWidth,
12731
12871
  maxDropdownWidth = "640px",
12872
+ virtualBuffer = DEFAULT_VIRTUAL_BUFFER,
12873
+ onReachEnd,
12874
+ reachEndThreshold = DEFAULT_REACH_END_THRESHOLD,
12875
+ hasMore = false,
12876
+ loading: loading2 = false,
12877
+ serverSearch = false,
12878
+ onSearchChange,
12879
+ searchDebounce = DEFAULT_SEARCH_DEBOUNCE,
12732
12880
  label,
12733
12881
  labelWidth,
12734
12882
  icon,
@@ -12743,6 +12891,9 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12743
12891
  style
12744
12892
  }, ref) {
12745
12893
  const multi = type === "multi" || type === "multi_depth";
12894
+ const isDepth = type === "default_depth" || type === "multi_depth";
12895
+ const pagingEnabled = onReachEnd != null;
12896
+ const partialOptions = pagingEnabled || serverSearch;
12746
12897
  const portalContainer = usePortalContainer();
12747
12898
  const insideModal = useInsideModal();
12748
12899
  const [search, setSearch] = useState("");
@@ -12750,6 +12901,15 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12750
12901
  const [open, setOpen] = useState(false);
12751
12902
  const [pendingOpen, setPendingOpen] = useState(false);
12752
12903
  const [focusedIndex, setFocusedIndex] = useState(-1);
12904
+ const [win, setWin] = useState({ start: 0, end: 0, top: 0, bottom: 0 });
12905
+ const winRef = useRef(win);
12906
+ winRef.current = win;
12907
+ const metricsRef = useRef(null);
12908
+ metricsRef.current ?? (metricsRef.current = createRowMetrics(0, OPTION_ROW_HEIGHT));
12909
+ const [searchBarHeight, setSearchBarHeight] = useState(0);
12910
+ const [selectAllHeight, setSelectAllHeight] = useState(0);
12911
+ const measuredKeyRef = useRef("");
12912
+ const lastReachEndCountRef = useRef(-1);
12753
12913
  const triggerRef = useRef(null);
12754
12914
  const searchInputRef = useRef(null);
12755
12915
  const searchBarRef = useRef(null);
@@ -12764,7 +12924,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12764
12924
  const hasLeaf = useMemo(() => flat.some((o) => !o.isGroup), [flat]);
12765
12925
  const allOptionsDisabled = hasLeaf && selectable.length === 0;
12766
12926
  const effectiveDisabled = disabled || allOptionsDisabled;
12767
- const searchVisible = showSearch && selectable.length >= SEARCH_THRESHOLD;
12927
+ const searchVisible = showSearch && (partialOptions || selectable.length >= SEARCH_THRESHOLD);
12768
12928
  const labelByValue = useMemo(() => new Map(flat.map((o) => [o.value, o.label])), [flat]);
12769
12929
  const rawByValue = useMemo(() => new Map(flat.map((o) => [o.value, o.raw])), [flat]);
12770
12930
  const optionByValue = useMemo(
@@ -12776,12 +12936,26 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12776
12936
  const arr = Array.isArray(value) ? value : [value];
12777
12937
  return arr.map((v) => v != null && typeof v === "object" ? v.value : v).filter((v) => v != null && v !== "").map(String);
12778
12938
  }, [value]);
12939
+ const valueOptions = useMemo(() => {
12940
+ const map = /* @__PURE__ */ new Map();
12941
+ if (value == null) return map;
12942
+ for (const v of Array.isArray(value) ? value : [value]) {
12943
+ if (v != null && typeof v === "object") {
12944
+ map.set(String(v.value), { value: v.value, label: v.label });
12945
+ }
12946
+ }
12947
+ return map;
12948
+ }, [value]);
12779
12949
  const selectedRef = useRef(selected);
12780
12950
  selectedRef.current = selected;
12951
+ const selectedSet = useMemo(() => new Set(selected), [selected]);
12952
+ const cbRef = useRef({ onReachEnd, onSearchChange });
12953
+ cbRef.current = { onReachEnd, onSearchChange };
12954
+ const emittedQueryRef = useRef("");
12781
12955
  const emit3 = (next) => {
12782
12956
  if (!onValueChange) return;
12783
12957
  const mapped = next.map(
12784
- (v) => valueAsPrimitive ? rawByValue.get(v) ?? v : optionByValue.get(v) ?? { value: v, label: v }
12958
+ (v) => valueAsPrimitive ? rawByValue.get(v) ?? valueOptions.get(v)?.value ?? v : optionByValue.get(v) ?? valueOptions.get(v) ?? { value: v, label: v }
12785
12959
  );
12786
12960
  onValueChange(multi ? mapped : mapped[0] ?? (valueAsPrimitive ? "" : null));
12787
12961
  };
@@ -12830,14 +13004,15 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12830
13004
  resetValidation: () => setRuleError("")
12831
13005
  });
12832
13006
  const filtered = useMemo(() => {
12833
- if (!searchVisible || search.trim() === "") return flat;
13007
+ if (serverSearch || !searchVisible || search.trim() === "") return flat;
12834
13008
  const q = search.trim().toLowerCase();
12835
13009
  return flat.filter(
12836
13010
  (o) => o.isGroup || (isHtmlLabel(o.label) ? extractText(o.label) : o.label).toLowerCase().includes(q)
12837
13011
  );
12838
- }, [flat, search, searchVisible]);
13012
+ }, [flat, search, searchVisible, serverSearch]);
12839
13013
  const allValues = selectable.map((o) => o.value);
12840
- const allSelected = allValues.length > 0 && allValues.every((v) => selected.includes(v));
13014
+ const allSelected = !partialOptions && allValues.length > 0 && allValues.every((v) => selectedSet.has(v));
13015
+ const selectAllVisible = multi && showSelectAll && !partialOptions && allValues.length > 0;
12841
13016
  const hasItems = filtered.some((o) => !o.isGroup);
12842
13017
  const groupLeafValues = useMemo(() => {
12843
13018
  const map = /* @__PURE__ */ new Map();
@@ -12848,7 +13023,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12848
13023
  }, [filtered]);
12849
13024
  const navigableItems = useMemo(() => {
12850
13025
  const items = [];
12851
- if (multi && showSelectAll && allValues.length > 0) {
13026
+ if (selectAllVisible) {
12852
13027
  items.push({ key: "__select_all__", isSelectAll: true });
12853
13028
  }
12854
13029
  filtered.forEach((o, i) => {
@@ -12863,20 +13038,129 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12863
13038
  }
12864
13039
  });
12865
13040
  return items;
12866
- }, [allValues.length, filtered, groupLeafValues, multi, showSelectAll]);
13041
+ }, [filtered, groupLeafValues, multi, selectAllVisible]);
12867
13042
  const selectedAnchorIndex = useMemo(() => {
12868
- if (selected.length === 0) return -1;
12869
- const selectedSet = new Set(selected);
13043
+ if (selectedSet.size === 0) return -1;
12870
13044
  return navigableItems.findIndex((item4) => item4.value != null && selectedSet.has(item4.value));
12871
- }, [navigableItems, selected]);
13045
+ }, [navigableItems, selectedSet]);
12872
13046
  selectedAnchorRef.current = selectedAnchorIndex;
13047
+ const navigableItemsRef = useRef(navigableItems);
13048
+ navigableItemsRef.current = navigableItems;
13049
+ const rowCount = filtered.length;
13050
+ const metrics = metricsRef.current;
13051
+ const lastSearchRef = useRef(search);
13052
+ if (lastSearchRef.current !== search) {
13053
+ lastSearchRef.current = search;
13054
+ resetRowMetrics(metrics, rowCount);
13055
+ measuredKeyRef.current = "";
13056
+ } else {
13057
+ resizeRowMetrics(metrics, rowCount);
13058
+ }
13059
+ const rowsOffset = searchBarHeight + selectAllHeight;
13060
+ const syncScrollState = useCallback(() => {
13061
+ if (!listEl) return;
13062
+ const m = metricsRef.current;
13063
+ if (m == null) return;
13064
+ const buffer = Math.max(0, Math.floor(virtualBuffer));
13065
+ const viewTop = Math.max(0, listEl.scrollTop - rowsOffset);
13066
+ const viewBottom = viewTop + Math.max(0, listEl.clientHeight);
13067
+ const start = Math.max(0, indexAtOffset(m, viewTop) - buffer);
13068
+ const end = Math.min(rowCount, indexAtOffset(m, viewBottom) + 1 + buffer);
13069
+ const top = offsetOf(m, start);
13070
+ const bottom = Math.max(0, totalSize(m) - offsetOf(m, end));
13071
+ setWin(
13072
+ (prev) => prev.start === start && prev.end === end && prev.top === top && prev.bottom === bottom ? prev : { start, end, top, bottom }
13073
+ );
13074
+ if (!pagingEnabled || !hasMore || loading2) return;
13075
+ const contentHeight = rowsOffset + totalSize(m);
13076
+ const remaining = contentHeight - listEl.scrollTop - listEl.clientHeight;
13077
+ if (remaining > Math.max(1, reachEndThreshold) * m.estimate) return;
13078
+ if (lastReachEndCountRef.current === rowCount) return;
13079
+ lastReachEndCountRef.current = rowCount;
13080
+ cbRef.current.onReachEnd?.();
13081
+ }, [
13082
+ hasMore,
13083
+ listEl,
13084
+ loading2,
13085
+ pagingEnabled,
13086
+ reachEndThreshold,
13087
+ rowCount,
13088
+ rowsOffset,
13089
+ virtualBuffer
13090
+ ]);
13091
+ const syncScrollStateRef = useRef(syncScrollState);
13092
+ syncScrollStateRef.current = syncScrollState;
13093
+ useLayoutEffect(() => {
13094
+ if (!open) return;
13095
+ syncScrollState();
13096
+ }, [open, syncScrollState]);
13097
+ useLayoutEffect(() => {
13098
+ if (!open || !listEl) {
13099
+ setSearchBarHeight(0);
13100
+ setSelectAllHeight(0);
13101
+ return;
13102
+ }
13103
+ const nextBar = searchVisible ? searchBarRef.current?.offsetHeight ?? 0 : 0;
13104
+ setSearchBarHeight((prev) => prev === nextBar ? prev : nextBar);
13105
+ const nextSelectAll = selectAllVisible ? listEl.querySelector("[data-select-all-row]")?.offsetHeight ?? 0 : 0;
13106
+ setSelectAllHeight((prev) => prev === nextSelectAll ? prev : nextSelectAll);
13107
+ }, [listEl, open, searchVisible, selectAllVisible]);
13108
+ useLayoutEffect(() => {
13109
+ if (!open || !listEl) return;
13110
+ const m = metricsRef.current;
13111
+ if (m == null) return;
13112
+ const key = `${winRef.current.start}:${winRef.current.end}:${rowCount}`;
13113
+ if (measuredKeyRef.current === key) return;
13114
+ measuredKeyRef.current = key;
13115
+ const anchor = winRef.current.start;
13116
+ const anchorBefore = offsetOf(m, anchor);
13117
+ let changed = false;
13118
+ for (const row of listEl.querySelectorAll("[data-select-row]")) {
13119
+ const index = Number(row.dataset.index);
13120
+ if (!Number.isInteger(index)) continue;
13121
+ if (setRowSize(m, index, row.offsetHeight)) changed = true;
13122
+ }
13123
+ if (!changed) return;
13124
+ const delta = offsetOf(m, anchor) - anchorBefore;
13125
+ if (delta !== 0 && listEl.scrollTop > 0) listEl.scrollTop += delta;
13126
+ syncScrollStateRef.current();
13127
+ });
13128
+ useEffect(() => {
13129
+ if (cbRef.current.onSearchChange == null) return;
13130
+ if (search === emittedQueryRef.current) return;
13131
+ if (!serverSearch) {
13132
+ emittedQueryRef.current = search;
13133
+ cbRef.current.onSearchChange(search);
13134
+ return;
13135
+ }
13136
+ const timer = setTimeout(
13137
+ () => {
13138
+ emittedQueryRef.current = search;
13139
+ cbRef.current.onSearchChange?.(search);
13140
+ },
13141
+ Math.max(0, searchDebounce)
13142
+ );
13143
+ return () => clearTimeout(timer);
13144
+ }, [search, searchDebounce, serverSearch]);
13145
+ const selectAllMisuse = multi && showSelectAll && partialOptions;
13146
+ useEffect(() => {
13147
+ if (!isDev()) return;
13148
+ if (selectAllMisuse) {
13149
+ console.warn(
13150
+ '[SSelect] onReachEnd\xB7serverSearch \uC640 \uD568\uAED8 \uC900 showSelectAll \uC744 \uBB34\uC2DC\uD569\uB2C8\uB2E4. options \uAC00 \uBAA9\uB85D\uC758 \uC870\uAC01\uC77C \uBFD0\uC774\uB77C \uC544\uC9C1 \uBC1B\uC9C0 \uC54A\uC740 \uC635\uC158\uC740 \uACE0\uB97C \uC218 \uC5C6\uACE0, \uBC1B\uC544 \uC628 \uB9CC\uD07C\uB9CC \uACE0\uB978 \uC0C1\uD0DC\uB97C "\uC804\uCCB4"\uB77C\uACE0 \uD45C\uC2DC\uD558\uBA74 \uAC70\uC9D3\uC774 \uB429\uB2C8\uB2E4.'
13151
+ );
13152
+ }
13153
+ }, [selectAllMisuse]);
13154
+ const windowStart = Math.min(win.start, rowCount);
13155
+ const windowEnd = Math.max(windowStart, Math.min(win.end, rowCount));
13156
+ const visibleItems = filtered.slice(windowStart, windowEnd);
12873
13157
  const triggerText = (() => {
12874
13158
  if (selected.length === 0) return placeholder;
12875
13159
  if (multi) {
12876
13160
  if (allSelected) return allSelectedLabel;
12877
- return selected.map((v) => labelByValue.get(v) ?? v).filter(Boolean).join(", ");
13161
+ return selected.map((v) => labelByValue.get(v) ?? valueOptions.get(v)?.label ?? v).filter(Boolean).join(", ");
12878
13162
  }
12879
- return labelByValue.get(selected[0]) ?? selected[0];
13163
+ return labelByValue.get(selected[0]) ?? valueOptions.get(selected[0])?.label ?? selected[0];
12880
13164
  })();
12881
13165
  const isPlaceholder = selected.length === 0;
12882
13166
  const showClear = clearable && !effectiveDisabled && !isPlaceholder;
@@ -12889,7 +13173,6 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12889
13173
  const anchor = rangeAnchorRef.current;
12890
13174
  if (extend && anchor != null && anchor !== index && filtered[anchor]?.isGroup === false) {
12891
13175
  const [from, to] = anchor < index ? [anchor, index] : [index, anchor];
12892
- const selectedSet = new Set(selected);
12893
13176
  const shouldSelect = selectedSet.has(filtered[anchor].value);
12894
13177
  const rangeValues = filtered.slice(from, to + 1).filter((o) => !o.isGroup && !o.disabled).map((o) => o.value);
12895
13178
  const inRange = new Set(rangeValues);
@@ -12914,7 +13197,6 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12914
13197
  const handleToggleGroup = (leafValues) => {
12915
13198
  if (leafValues.length === 0) return;
12916
13199
  rangeAnchorRef.current = null;
12917
- const selectedSet = new Set(selected);
12918
13200
  const allSelectedInGroup = leafValues.every((v) => selectedSet.has(v));
12919
13201
  if (allSelectedInGroup) {
12920
13202
  const remove = new Set(leafValues);
@@ -12923,35 +13205,51 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12923
13205
  emit3([...selected, ...leafValues.filter((v) => !selectedSet.has(v))]);
12924
13206
  }
12925
13207
  };
13208
+ const hasNavigableItems = navigableItems.length > 0;
12926
13209
  useEffect(() => {
12927
- if (!open || navigableItems.length === 0) {
13210
+ if (!open || !hasNavigableItems) {
12928
13211
  setFocusedIndex(-1);
12929
13212
  return;
12930
13213
  }
12931
13214
  const anchor = alignSelectedTopRef.current ? selectedAnchorRef.current : -1;
12932
13215
  setFocusedIndex(anchor >= 0 ? anchor : 0);
12933
- }, [open, search, navigableItems.length]);
13216
+ }, [open, search, hasNavigableItems]);
13217
+ const lastResetSearchRef = useRef(search);
13218
+ useLayoutEffect(() => {
13219
+ if (lastResetSearchRef.current === search) return;
13220
+ lastResetSearchRef.current = search;
13221
+ if (!open || !listEl) return;
13222
+ listEl.scrollTop = 0;
13223
+ syncScrollStateRef.current();
13224
+ }, [listEl, open, search]);
12934
13225
  useEffect(() => {
12935
13226
  rangeAnchorRef.current = null;
13227
+ lastReachEndCountRef.current = -1;
12936
13228
  }, [open, search]);
12937
13229
  useLayoutEffect(() => {
12938
13230
  if (!open || focusedIndex < 0 || !listEl) return;
12939
- const focusedEl = listEl.querySelector('[data-select-focused="true"]');
12940
- if (!focusedEl) return;
12941
- const listRect = listEl.getBoundingClientRect();
12942
- const itemRect = focusedEl.getBoundingClientRect();
12943
- if (alignSelectedTopRef.current) {
13231
+ const m = metricsRef.current;
13232
+ if (m == null) return;
13233
+ const stickyOffset = searchBarRef.current?.offsetHeight ?? 0;
13234
+ const rowIndex = navigableItemsRef.current[focusedIndex]?.index;
13235
+ if (rowIndex == null) {
12944
13236
  alignSelectedTopRef.current = false;
12945
- const stickyOffset = searchBarRef.current?.offsetHeight ?? 0;
12946
- listEl.scrollTop += itemRect.top - listRect.top - stickyOffset;
13237
+ listEl.scrollTop = 0;
13238
+ syncScrollStateRef.current();
12947
13239
  return;
12948
13240
  }
12949
- if (itemRect.top < listRect.top) {
12950
- listEl.scrollTop += itemRect.top - listRect.top;
12951
- } else if (itemRect.bottom > listRect.bottom) {
12952
- listEl.scrollTop += itemRect.bottom - listRect.bottom;
12953
- }
12954
- }, [focusedIndex, open, listEl]);
13241
+ const top = rowsOffset + offsetOf(m, rowIndex);
13242
+ const height = sizeOf(m, rowIndex);
13243
+ if (alignSelectedTopRef.current) {
13244
+ alignSelectedTopRef.current = false;
13245
+ listEl.scrollTop = top - stickyOffset;
13246
+ } else if (top < listEl.scrollTop + stickyOffset) {
13247
+ listEl.scrollTop = top - stickyOffset;
13248
+ } else if (top + height > listEl.scrollTop + listEl.clientHeight) {
13249
+ listEl.scrollTop = top + height - listEl.clientHeight;
13250
+ }
13251
+ syncScrollStateRef.current();
13252
+ }, [focusedIndex, listEl, open, rowsOffset]);
12955
13253
  const moveFocusedIndex = (direction) => {
12956
13254
  if (!open || navigableItems.length === 0) return;
12957
13255
  setFocusedIndex((current) => {
@@ -13141,6 +13439,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
13141
13439
  SCROLLBAR_TRACK_BORDER_CLASS
13142
13440
  ),
13143
13441
  style: { maxHeight: clampToAvailableHeight(dropdownHeight, POPOVER_AVAILABLE_HEIGHT) },
13442
+ onScroll: syncScrollState,
13144
13443
  onMouseLeave: () => setFocusedIndex(-1),
13145
13444
  children: [
13146
13445
  searchVisible && /* @__PURE__ */ jsx(
@@ -13164,12 +13463,16 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
13164
13463
  hasItems ? /* @__PURE__ */ jsx(
13165
13464
  ItemList,
13166
13465
  {
13167
- items: filtered,
13466
+ items: visibleItems,
13467
+ indexOffset: windowStart,
13468
+ topSpacer: win.top,
13469
+ bottomSpacer: win.bottom,
13470
+ setSize: isDepth ? void 0 : rowCount,
13168
13471
  multi,
13169
13472
  selected,
13170
13473
  groupLeafValues,
13171
13474
  focusedValue: navigableItems[focusedIndex]?.key,
13172
- showSelectAll: multi && showSelectAll && allValues.length > 0,
13475
+ showSelectAll: selectAllVisible,
13173
13476
  selectAllLabel: allSelectedLabel,
13174
13477
  selectAllSelected: allSelected,
13175
13478
  onSelectAll: handleSelectAll,
@@ -13184,7 +13487,18 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
13184
13487
  if (index >= 0) setFocusedIndex(index);
13185
13488
  }
13186
13489
  }
13187
- ) : /* @__PURE__ */ jsx("div", { className: "px-[12px] py-[12px] text-left text-[12px] leading-[20px] text-[color:var(--cmp-listBoxItem-content-disabled)]", children: "\uD45C\uC2DC\uD560 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." })
13490
+ ) : loading2 ? null : (
13491
+ // 첫 페이지를 받는 중에는 "없다"고 단정하지 않는다 — 아래 로딩 표시가 대신 나온다.
13492
+ /* @__PURE__ */ jsx("div", { className: "px-[12px] py-[12px] text-left text-[12px] leading-[20px] text-[color:var(--cmp-listBoxItem-content-disabled)]", children: "\uD45C\uC2DC\uD560 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." })
13493
+ ),
13494
+ loading2 && /* @__PURE__ */ jsx(
13495
+ "div",
13496
+ {
13497
+ role: "status",
13498
+ className: "px-[12px] py-[12px] text-center text-[12px] leading-[20px] text-[color:var(--cmp-listBoxItem-content-disabled)]",
13499
+ children: "\uBD88\uB7EC\uC624\uB294 \uC911..."
13500
+ }
13501
+ )
13188
13502
  ]
13189
13503
  }
13190
13504
  )
@@ -13197,6 +13511,10 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
13197
13511
  var SSelect2 = /* @__PURE__ */ withDisplayName(SSelectImpl, "SSelect");
13198
13512
  function ItemList({
13199
13513
  items,
13514
+ indexOffset,
13515
+ topSpacer,
13516
+ bottomSpacer,
13517
+ setSize,
13200
13518
  multi,
13201
13519
  selected,
13202
13520
  groupLeafValues,
@@ -13221,6 +13539,7 @@ function ItemList({
13221
13539
  "button",
13222
13540
  {
13223
13541
  type: "button",
13542
+ "data-select-all-row": true,
13224
13543
  "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
13225
13544
  "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
13226
13545
  className: cn(
@@ -13237,7 +13556,9 @@ function ItemList({
13237
13556
  ]
13238
13557
  }
13239
13558
  ),
13240
- items.map((o, idx) => {
13559
+ topSpacer > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: { height: topSpacer } }),
13560
+ items.map((o, windowIndex) => {
13561
+ const idx = windowIndex + indexOffset;
13241
13562
  const padLeft = o.depth === 1 ? 12 : o.depth === 2 ? 20 : 28;
13242
13563
  const rowKey = `${o.value}-${idx}`;
13243
13564
  if (o.isGroup) {
@@ -13254,6 +13575,8 @@ function ItemList({
13254
13575
  "button",
13255
13576
  {
13256
13577
  type: "button",
13578
+ "data-select-row": true,
13579
+ "data-index": idx,
13257
13580
  role: "option",
13258
13581
  "aria-selected": groupState === true,
13259
13582
  disabled: groupDisabled,
@@ -13304,6 +13627,8 @@ function ItemList({
13304
13627
  return /* @__PURE__ */ jsx(
13305
13628
  "div",
13306
13629
  {
13630
+ "data-select-row": true,
13631
+ "data-index": idx,
13307
13632
  className: cn(
13308
13633
  "select-none py-[4px] pr-[12px] text-[12px] leading-[20px] text-[color:var(--cmp-listBoxItem-content-default)]",
13309
13634
  groupTone,
@@ -13323,9 +13648,13 @@ function ItemList({
13323
13648
  "button",
13324
13649
  {
13325
13650
  type: "button",
13651
+ "data-select-row": true,
13652
+ "data-index": idx,
13326
13653
  disabled: o.disabled,
13327
13654
  role: "option",
13328
13655
  "aria-selected": isSelected,
13656
+ "aria-setsize": setSize,
13657
+ "aria-posinset": setSize != null ? idx + 1 : void 0,
13329
13658
  "data-select-focused": focusedValue === o.value ? "true" : void 0,
13330
13659
  "data-select-active": focusedValue === o.value ? "true" : void 0,
13331
13660
  className: cn(
@@ -13360,7 +13689,8 @@ function ItemList({
13360
13689
  },
13361
13690
  rowKey
13362
13691
  );
13363
- })
13692
+ }),
13693
+ bottomSpacer > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: { height: bottomSpacer } })
13364
13694
  ] });
13365
13695
  }
13366
13696
  function CheckboxBox({
@@ -14521,12 +14851,29 @@ var SInput = /* @__PURE__ */ forwardRef(function SInput2({
14521
14851
  }
14522
14852
  );
14523
14853
  });
14524
- var PAD2 = { fontSize: 12, paddingX: 12, paddingY: 4 };
14854
+ var TEXTAREA_SIZE = {
14855
+ sm: {
14856
+ fontSize: 12,
14857
+ lineHeight: 20,
14858
+ paddingX: "var(--cmp-textinput-sm-paddingX)",
14859
+ paddingY: "var(--cmp-textinput-sm-paddingY)"
14860
+ },
14861
+ md: {
14862
+ fontSize: 14,
14863
+ lineHeight: 24,
14864
+ paddingX: "var(--cmp-textinput-md-paddingX)",
14865
+ paddingY: "var(--cmp-textinput-md-paddingY)"
14866
+ }
14867
+ };
14868
+ var MIN_ROWS = 2;
14525
14869
  var STextarea = /* @__PURE__ */ forwardRef(function STextarea2({
14526
14870
  value,
14527
14871
  onValueChange,
14528
14872
  onChange,
14529
- rows = 3,
14873
+ size = "sm",
14874
+ rows = MIN_ROWS,
14875
+ autogrow = false,
14876
+ maxRows,
14530
14877
  rules,
14531
14878
  status,
14532
14879
  focused,
@@ -14553,8 +14900,30 @@ var STextarea = /* @__PURE__ */ forwardRef(function STextarea2({
14553
14900
  style,
14554
14901
  ...rest
14555
14902
  }, ref) {
14556
- const p = PAD2;
14903
+ const p = TEXTAREA_SIZE[size];
14904
+ const heightForRows = (n) => `calc(${p.lineHeight * n}px + ${p.paddingY} * 2)`;
14905
+ const minHeight = heightForRows(MIN_ROWS);
14906
+ const maxHeight = autogrow && maxRows != null ? heightForRows(maxRows) : void 0;
14557
14907
  const [ruleError, setRuleError] = useState("");
14908
+ const innerRef = useRef(null);
14909
+ const setRefs2 = useCallback(
14910
+ (node) => {
14911
+ innerRef.current = node;
14912
+ if (typeof ref === "function") ref(node);
14913
+ else if (ref) ref.current = node;
14914
+ },
14915
+ [ref]
14916
+ );
14917
+ useLayoutEffect(() => {
14918
+ const el = innerRef.current;
14919
+ if (el == null) return;
14920
+ if (!autogrow) {
14921
+ el.style.height = "";
14922
+ return;
14923
+ }
14924
+ el.style.height = "auto";
14925
+ el.style.height = `${el.scrollHeight}px`;
14926
+ }, [autogrow, maxHeight, minHeight, rows, value]);
14558
14927
  useFormField({
14559
14928
  name: rest.name,
14560
14929
  validate: () => {
@@ -14579,6 +14948,7 @@ var STextarea = /* @__PURE__ */ forwardRef(function STextarea2({
14579
14948
  addonLabel,
14580
14949
  addonAlign,
14581
14950
  hint,
14951
+ size,
14582
14952
  error: resolvedError,
14583
14953
  status: resolvedStatus,
14584
14954
  focused,
@@ -14593,7 +14963,7 @@ var STextarea = /* @__PURE__ */ forwardRef(function STextarea2({
14593
14963
  children: /* @__PURE__ */ jsx(
14594
14964
  "textarea",
14595
14965
  {
14596
- ref,
14966
+ ref: setRefs2,
14597
14967
  value: value ?? "",
14598
14968
  rows,
14599
14969
  disabled,
@@ -14609,15 +14979,21 @@ var STextarea = /* @__PURE__ */ forwardRef(function STextarea2({
14609
14979
  if (rules && rules.length > 0) setRuleError(runRules(e.target.value, rules));
14610
14980
  },
14611
14981
  className: cn(
14612
- "w-full resize-y border-0 bg-transparent outline-none placeholder:text-[color:var(--sys-color-field-text-placeholder)] disabled:cursor-not-allowed",
14982
+ "w-full border-0 bg-transparent outline-none placeholder:text-[color:var(--sys-color-field-text-placeholder)] disabled:cursor-not-allowed",
14983
+ // autogrow 와 끌어서 늘리기는 같은 높이를 두고 다투므로 함께 두지 않는다
14984
+ autogrow ? "resize-none" : "resize-y",
14613
14985
  AUTOFILL_RESET_CLASS,
14614
14986
  textareaClassName
14615
14987
  ),
14616
14988
  style: {
14617
- minHeight: 100,
14618
- padding: `${p.paddingY}px ${p.paddingX}px`,
14989
+ minHeight,
14990
+ maxHeight,
14991
+ // 상한이 있으면 거기 닿았을 때 안에서 스크롤한다. 상한이 없으면 높이가 늘 내용과
14992
+ // 같으므로 스크롤바가 나올 일이 없고, 반올림으로 1px 이 넘쳐 깜빡이는 것만 막는다.
14993
+ overflowY: autogrow ? maxRows != null ? "auto" : "hidden" : void 0,
14994
+ padding: `${p.paddingY} ${p.paddingX}`,
14619
14995
  fontSize: p.fontSize,
14620
- lineHeight: "20px",
14996
+ lineHeight: `${p.lineHeight}px`,
14621
14997
  color: disabled ? "var(--cmp-textinput-text-disabled)" : "var(--sys-color-field-text-default)",
14622
14998
  ...textareaStyle
14623
14999
  },
@@ -14627,7 +15003,7 @@ var STextarea = /* @__PURE__ */ forwardRef(function STextarea2({
14627
15003
  }
14628
15004
  );
14629
15005
  });
14630
- var PAD3 = {
15006
+ var PAD2 = {
14631
15007
  sm: { fontSize: 12, lineHeight: 20, stepper: 20, icon: 12 },
14632
15008
  md: { fontSize: 14, lineHeight: 24, stepper: 24, icon: 14 }
14633
15009
  };
@@ -14656,6 +15032,16 @@ var formatInputWithCommas = (value) => {
14656
15032
  const formattedInt = addCommas(intPart === "" ? "0" : intPart);
14657
15033
  return `${isNeg ? "-" : ""}${formattedInt}${hasDecimal ? `.${decPart}` : ""}`;
14658
15034
  };
15035
+ var getRangeError = (value, min, max) => {
15036
+ if (value === null) return "";
15037
+ const belowMin = value < min;
15038
+ const aboveMax = value > max;
15039
+ if (!belowMin && !aboveMax) return "";
15040
+ if (Number.isFinite(min) && Number.isFinite(max)) {
15041
+ return `${formatWithCommas(min)}~${formatWithCommas(max)} \uC0AC\uC774\uB85C \uC785\uB825\uD574 \uC8FC\uC138\uC694.`;
15042
+ }
15043
+ return belowMin ? `${formatWithCommas(min)} \uC774\uC0C1\uC73C\uB85C \uC785\uB825\uD574 \uC8FC\uC138\uC694.` : `${formatWithCommas(max)} \uC774\uD558\uB85C \uC785\uB825\uD574 \uC8FC\uC138\uC694.`;
15044
+ };
14659
15045
  var getDecimalLimit = (allowDecimal, maxDecimalLength) => {
14660
15046
  if (!allowDecimal || maxDecimalLength === void 0 || !Number.isFinite(maxDecimalLength)) {
14661
15047
  return void 0;
@@ -14686,6 +15072,7 @@ var SNumberInput = /* @__PURE__ */ forwardRef(function SNumberInput2({
14686
15072
  min = Number.NEGATIVE_INFINITY,
14687
15073
  max = Number.POSITIVE_INFINITY,
14688
15074
  step = 1,
15075
+ clampOnBlur = false,
14689
15076
  showButton = false,
14690
15077
  allowDecimal = false,
14691
15078
  maxDecimalLength,
@@ -14719,12 +15106,13 @@ var SNumberInput = /* @__PURE__ */ forwardRef(function SNumberInput2({
14719
15106
  className,
14720
15107
  style
14721
15108
  }, ref) {
14722
- const p = PAD3[size];
15109
+ const p = PAD2[size];
14723
15110
  const padX = `var(--cmp-numberInput-${size}-paddingX)`;
14724
15111
  const decimalLimit = getDecimalLimit(allowDecimal, maxDecimalLength);
14725
15112
  const numValue = applyDecimalLimit(toNumber(value), decimalLimit);
14726
15113
  const [display, setDisplay] = useState(() => formatWithCommas(numValue));
14727
15114
  const [ruleError, setRuleError] = useState("");
15115
+ const [rangeError, setRangeError] = useState("");
14728
15116
  const innerRef = useRef(null);
14729
15117
  const valueTooltip = useTruncatedValueTooltip({ enabled: readOnly && !disabled, text: display });
14730
15118
  const setRefs2 = (el) => {
@@ -14742,12 +15130,19 @@ var SNumberInput = /* @__PURE__ */ forwardRef(function SNumberInput2({
14742
15130
  validate: () => {
14743
15131
  const msg = runRules(numValue, rules);
14744
15132
  setRuleError(msg);
14745
- return msg === "";
15133
+ const range = clampOnBlur ? "" : getRangeError(numValue, min, max);
15134
+ setRangeError(range);
15135
+ return msg === "" && range === "";
14746
15136
  },
14747
- resetValidation: () => setRuleError("")
15137
+ resetValidation: () => {
15138
+ setRuleError("");
15139
+ setRangeError("");
15140
+ }
14748
15141
  });
14749
- const resolvedError = error || ruleError !== "";
14750
- const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
15142
+ const autoError = ruleError !== "" ? ruleError : rangeError;
15143
+ const resolvedError = error || autoError !== "";
15144
+ const resolvedStatus = status ?? (autoError !== "" ? "error" : void 0);
15145
+ const resolvedErrorMessage = ruleError !== "" ? ruleError : (errorMessage ?? "") !== "" ? errorMessage : rangeError;
14751
15146
  const restoreCaret = (nextDisplay, logicalCaret) => {
14752
15147
  const input = innerRef.current;
14753
15148
  if (logicalCaret === void 0 || !input || document.activeElement !== input) return;
@@ -14879,7 +15274,7 @@ var SNumberInput = /* @__PURE__ */ forwardRef(function SNumberInput2({
14879
15274
  status: resolvedStatus,
14880
15275
  focused,
14881
15276
  hovered,
14882
- errorMessage: ruleError !== "" ? ruleError : errorMessage,
15277
+ errorMessage: resolvedErrorMessage,
14883
15278
  width,
14884
15279
  disabled,
14885
15280
  readOnly,
@@ -14925,10 +15320,11 @@ var SNumberInput = /* @__PURE__ */ forwardRef(function SNumberInput2({
14925
15320
  onBlur: (e) => {
14926
15321
  const cleaned = display.replace(/,/g, "");
14927
15322
  const n = toNumber(cleaned);
14928
- const clamped = n === null ? null : Math.min(Math.max(n, min), max);
15323
+ const clamped = !clampOnBlur || n === null ? n : Math.min(Math.max(n, min), max);
14929
15324
  if (clamped !== n) emit3(clamped);
14930
15325
  else setDisplay(n === null ? "" : formatInputWithCommas(cleaned));
14931
15326
  if (rules && rules.length > 0) setRuleError(runRules(clamped, rules));
15327
+ setRangeError(clampOnBlur ? "" : getRangeError(clamped, min, max));
14932
15328
  onBlur?.(e);
14933
15329
  },
14934
15330
  className: cn(