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.cjs CHANGED
@@ -3717,14 +3717,14 @@ function SFooter({
3717
3717
  ]
3718
3718
  }
3719
3719
  ),
3720
- hasButton && /* @__PURE__ */ jsxRuntime.jsx(
3720
+ hasButton && // 스프레드를 먼저 두고 기본값을 뒤에 덮는다 — 순서를 뒤집으면 `{ color: undefined }`
3721
+ // 처럼 값 없이 넘어온 키가 footer 기본값을 지우고 SButton 기본값으로 떨어진다.
3722
+ /* @__PURE__ */ jsxRuntime.jsx(
3721
3723
  SButton,
3722
3724
  {
3725
+ ...button,
3726
+ size: "md",
3723
3727
  color: button.color ?? "primary",
3724
- outline: button.outline,
3725
- size: button.size ?? "md",
3726
- disabled: button.disabled,
3727
- onClick: () => button.onClick?.(),
3728
3728
  label: button.label ?? "\uD655\uC778"
3729
3729
  }
3730
3730
  )
@@ -4293,6 +4293,13 @@ function SDraggableProvider({ children }) {
4293
4293
  function SDraggableGroup({ children, onMove, ...rest }) {
4294
4294
  return /* @__PURE__ */ jsxRuntime.jsx(SDraggableProviderInternal, { defaultGroup: "__legacy", legacyOnMove: onMove, children: /* @__PURE__ */ jsxRuntime.jsx("div", { ...rest, children }) });
4295
4295
  }
4296
+ 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"]';
4297
+ function isFromInteractiveDescendant(event) {
4298
+ const target = event.target;
4299
+ if (target == null || target === event.currentTarget) return false;
4300
+ const interactive = target.closest?.(INTERACTIVE_DESCENDANT_SELECTOR);
4301
+ return interactive != null && interactive !== event.currentTarget;
4302
+ }
4296
4303
  function reorder(items, fromIndex, toIndex) {
4297
4304
  if (fromIndex === toIndex) return items;
4298
4305
  const nextItems = [...items];
@@ -4376,6 +4383,7 @@ function SDraggableListInner({
4376
4383
  const listRef = react.useRef(null);
4377
4384
  const fromIndexRef = react.useRef(null);
4378
4385
  const currentSelectedKey = selectedKey ?? internalSelectedKey;
4386
+ const selectable = selectedKey !== void 0 || defaultSelectedKey !== void 0 || onSelectedKeyChange != null;
4379
4387
  const usesDepth = getDepth != null;
4380
4388
  const depthEnabled = usesDepth && !gap;
4381
4389
  const groupDragging = dragContext?.dragging;
@@ -4535,20 +4543,28 @@ function SDraggableListInner({
4535
4543
  setInternalSelectedKey(itemKey);
4536
4544
  onSelectedKeyChange?.(itemKey);
4537
4545
  };
4546
+ const handleItemClick = (event) => {
4547
+ if (isFromInteractiveDescendant(event)) return;
4548
+ selectItem();
4549
+ };
4538
4550
  const handleItemKeyDown = (event) => {
4551
+ if (event.target !== event.currentTarget) return;
4539
4552
  if (event.key !== "Enter" && event.key !== " ") return;
4540
4553
  event.preventDefault();
4541
4554
  selectItem();
4542
4555
  };
4556
+ const selectionProps = selectable ? {
4557
+ role: "button",
4558
+ tabIndex: disabled ? -1 : 0,
4559
+ onClick: handleItemClick,
4560
+ onKeyDown: handleItemKeyDown
4561
+ } : {};
4543
4562
  return /* @__PURE__ */ jsxRuntime.jsxs(
4544
4563
  "div",
4545
4564
  {
4546
4565
  "data-draggable-list-item": true,
4547
- role: "button",
4548
- tabIndex: disabled ? -1 : 0,
4549
4566
  "aria-disabled": disabled || void 0,
4550
- onClick: selectItem,
4551
- onKeyDown: handleItemKeyDown,
4567
+ ...selectionProps,
4552
4568
  className: "relative",
4553
4569
  children: [
4554
4570
  showBefore && /* @__PURE__ */ jsxRuntime.jsx(
@@ -12736,6 +12752,130 @@ function collectGroupLeafValues(flat, groupIndex) {
12736
12752
  }
12737
12753
  var isHtmlLabel = (label) => /<[a-z][\s\S]*>/i.test(label);
12738
12754
  var extractText = (html) => html.replace(/<[^>]*>/g, "");
12755
+ var OPTION_ROW_HEIGHT = 28;
12756
+ var DEFAULT_VIRTUAL_BUFFER = 10;
12757
+ var DEFAULT_REACH_END_THRESHOLD = 10;
12758
+ var DEFAULT_SEARCH_DEBOUNCE = 300;
12759
+
12760
+ // src/components/SSelect/row-metrics.ts
12761
+ function createRowMetrics(count, estimate) {
12762
+ const safeCount = Math.max(0, Math.floor(count));
12763
+ const safeEstimate = Math.max(1, estimate);
12764
+ const metrics = {
12765
+ sizes: new Array(safeCount).fill(safeEstimate),
12766
+ measured: new Array(safeCount).fill(false),
12767
+ offsets: [],
12768
+ estimate: safeEstimate,
12769
+ measuredCount: 0,
12770
+ measuredTotal: 0,
12771
+ dirtyFrom: 0
12772
+ };
12773
+ rebuildOffsets(metrics);
12774
+ return metrics;
12775
+ }
12776
+ function resizeRowMetrics(metrics, count) {
12777
+ const next = Math.max(0, Math.floor(count));
12778
+ const prev = metrics.sizes.length;
12779
+ if (next === prev) return;
12780
+ if (next < prev) {
12781
+ for (let i = next; i < prev; i++) {
12782
+ if (metrics.measured[i]) {
12783
+ metrics.measuredCount--;
12784
+ metrics.measuredTotal -= metrics.sizes[i];
12785
+ }
12786
+ }
12787
+ metrics.sizes.length = next;
12788
+ metrics.measured.length = next;
12789
+ } else {
12790
+ for (let i = prev; i < next; i++) {
12791
+ metrics.sizes.push(metrics.estimate);
12792
+ metrics.measured.push(false);
12793
+ }
12794
+ }
12795
+ markDirty(metrics, Math.min(prev, next));
12796
+ }
12797
+ function resetRowMetrics(metrics, count) {
12798
+ const fresh = createRowMetrics(count, metrics.estimate);
12799
+ metrics.sizes = fresh.sizes;
12800
+ metrics.measured = fresh.measured;
12801
+ metrics.offsets = fresh.offsets;
12802
+ metrics.measuredCount = 0;
12803
+ metrics.measuredTotal = 0;
12804
+ metrics.dirtyFrom = Infinity;
12805
+ }
12806
+ function setRowSize(metrics, index, size) {
12807
+ if (index < 0 || index >= metrics.sizes.length) return false;
12808
+ const next = Math.max(1, Math.round(size));
12809
+ const wasMeasured = metrics.measured[index];
12810
+ const prev = metrics.sizes[index];
12811
+ if (wasMeasured && prev === next) return false;
12812
+ if (wasMeasured) {
12813
+ metrics.measuredTotal += next - prev;
12814
+ } else {
12815
+ metrics.measured[index] = true;
12816
+ metrics.measuredCount++;
12817
+ metrics.measuredTotal += next;
12818
+ }
12819
+ metrics.sizes[index] = next;
12820
+ let changedFrom = prev === next ? Infinity : index;
12821
+ const average = Math.max(1, Math.round(metrics.measuredTotal / metrics.measuredCount));
12822
+ if (average !== metrics.estimate) {
12823
+ metrics.estimate = average;
12824
+ for (let i = 0; i < metrics.sizes.length; i++) {
12825
+ if (metrics.measured[i] || metrics.sizes[i] === average) continue;
12826
+ metrics.sizes[i] = average;
12827
+ if (i < changedFrom) changedFrom = i;
12828
+ }
12829
+ }
12830
+ if (changedFrom === Infinity) return false;
12831
+ markDirty(metrics, changedFrom);
12832
+ return true;
12833
+ }
12834
+ function markDirty(metrics, from) {
12835
+ metrics.dirtyFrom = Math.min(metrics.dirtyFrom, Math.max(0, from));
12836
+ }
12837
+ function rebuildOffsets(metrics) {
12838
+ const count = metrics.sizes.length;
12839
+ const stale = metrics.offsets.length !== count + 1;
12840
+ if (!stale && metrics.dirtyFrom === Infinity) return;
12841
+ const start = stale ? 0 : Math.min(metrics.dirtyFrom, count);
12842
+ const acc0 = start === 0 ? 0 : metrics.offsets[start] ?? 0;
12843
+ metrics.offsets.length = count + 1;
12844
+ let acc = acc0;
12845
+ for (let i = start; i < count; i++) {
12846
+ metrics.offsets[i] = acc;
12847
+ acc += metrics.sizes[i];
12848
+ }
12849
+ metrics.offsets[count] = acc;
12850
+ metrics.dirtyFrom = Infinity;
12851
+ }
12852
+ function totalSize(metrics) {
12853
+ rebuildOffsets(metrics);
12854
+ return metrics.offsets[metrics.sizes.length] ?? 0;
12855
+ }
12856
+ function offsetOf(metrics, index) {
12857
+ rebuildOffsets(metrics);
12858
+ const clamped = Math.max(0, Math.min(Math.floor(index), metrics.sizes.length));
12859
+ return metrics.offsets[clamped] ?? 0;
12860
+ }
12861
+ function sizeOf(metrics, index) {
12862
+ if (index < 0 || index >= metrics.sizes.length) return metrics.estimate;
12863
+ return metrics.sizes[index];
12864
+ }
12865
+ function indexAtOffset(metrics, y) {
12866
+ rebuildOffsets(metrics);
12867
+ const count = metrics.sizes.length;
12868
+ if (count === 0) return 0;
12869
+ const target = Math.max(0, y);
12870
+ let lo = 0;
12871
+ let hi = count - 1;
12872
+ while (lo < hi) {
12873
+ const mid = lo + hi + 1 >> 1;
12874
+ if (metrics.offsets[mid] <= target) lo = mid;
12875
+ else hi = mid - 1;
12876
+ }
12877
+ return lo;
12878
+ }
12739
12879
  var SEARCH_THRESHOLD = 11;
12740
12880
  var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12741
12881
  value,
@@ -12757,6 +12897,14 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12757
12897
  dropdownHeight = "640px",
12758
12898
  dropdownWidth,
12759
12899
  maxDropdownWidth = "640px",
12900
+ virtualBuffer = DEFAULT_VIRTUAL_BUFFER,
12901
+ onReachEnd,
12902
+ reachEndThreshold = DEFAULT_REACH_END_THRESHOLD,
12903
+ hasMore = false,
12904
+ loading: loading2 = false,
12905
+ serverSearch = false,
12906
+ onSearchChange,
12907
+ searchDebounce = DEFAULT_SEARCH_DEBOUNCE,
12760
12908
  label,
12761
12909
  labelWidth,
12762
12910
  icon,
@@ -12771,6 +12919,9 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12771
12919
  style
12772
12920
  }, ref) {
12773
12921
  const multi = type === "multi" || type === "multi_depth";
12922
+ const isDepth = type === "default_depth" || type === "multi_depth";
12923
+ const pagingEnabled = onReachEnd != null;
12924
+ const partialOptions = pagingEnabled || serverSearch;
12774
12925
  const portalContainer = usePortalContainer();
12775
12926
  const insideModal = useInsideModal();
12776
12927
  const [search, setSearch] = react.useState("");
@@ -12778,6 +12929,15 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12778
12929
  const [open, setOpen] = react.useState(false);
12779
12930
  const [pendingOpen, setPendingOpen] = react.useState(false);
12780
12931
  const [focusedIndex, setFocusedIndex] = react.useState(-1);
12932
+ const [win, setWin] = react.useState({ start: 0, end: 0, top: 0, bottom: 0 });
12933
+ const winRef = react.useRef(win);
12934
+ winRef.current = win;
12935
+ const metricsRef = react.useRef(null);
12936
+ metricsRef.current ?? (metricsRef.current = createRowMetrics(0, OPTION_ROW_HEIGHT));
12937
+ const [searchBarHeight, setSearchBarHeight] = react.useState(0);
12938
+ const [selectAllHeight, setSelectAllHeight] = react.useState(0);
12939
+ const measuredKeyRef = react.useRef("");
12940
+ const lastReachEndCountRef = react.useRef(-1);
12781
12941
  const triggerRef = react.useRef(null);
12782
12942
  const searchInputRef = react.useRef(null);
12783
12943
  const searchBarRef = react.useRef(null);
@@ -12792,7 +12952,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12792
12952
  const hasLeaf = react.useMemo(() => flat.some((o) => !o.isGroup), [flat]);
12793
12953
  const allOptionsDisabled = hasLeaf && selectable.length === 0;
12794
12954
  const effectiveDisabled = disabled || allOptionsDisabled;
12795
- const searchVisible = showSearch && selectable.length >= SEARCH_THRESHOLD;
12955
+ const searchVisible = showSearch && (partialOptions || selectable.length >= SEARCH_THRESHOLD);
12796
12956
  const labelByValue = react.useMemo(() => new Map(flat.map((o) => [o.value, o.label])), [flat]);
12797
12957
  const rawByValue = react.useMemo(() => new Map(flat.map((o) => [o.value, o.raw])), [flat]);
12798
12958
  const optionByValue = react.useMemo(
@@ -12804,12 +12964,26 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12804
12964
  const arr = Array.isArray(value) ? value : [value];
12805
12965
  return arr.map((v) => v != null && typeof v === "object" ? v.value : v).filter((v) => v != null && v !== "").map(String);
12806
12966
  }, [value]);
12967
+ const valueOptions = react.useMemo(() => {
12968
+ const map = /* @__PURE__ */ new Map();
12969
+ if (value == null) return map;
12970
+ for (const v of Array.isArray(value) ? value : [value]) {
12971
+ if (v != null && typeof v === "object") {
12972
+ map.set(String(v.value), { value: v.value, label: v.label });
12973
+ }
12974
+ }
12975
+ return map;
12976
+ }, [value]);
12807
12977
  const selectedRef = react.useRef(selected);
12808
12978
  selectedRef.current = selected;
12979
+ const selectedSet = react.useMemo(() => new Set(selected), [selected]);
12980
+ const cbRef = react.useRef({ onReachEnd, onSearchChange });
12981
+ cbRef.current = { onReachEnd, onSearchChange };
12982
+ const emittedQueryRef = react.useRef("");
12809
12983
  const emit3 = (next) => {
12810
12984
  if (!onValueChange) return;
12811
12985
  const mapped = next.map(
12812
- (v) => valueAsPrimitive ? rawByValue.get(v) ?? v : optionByValue.get(v) ?? { value: v, label: v }
12986
+ (v) => valueAsPrimitive ? rawByValue.get(v) ?? valueOptions.get(v)?.value ?? v : optionByValue.get(v) ?? valueOptions.get(v) ?? { value: v, label: v }
12813
12987
  );
12814
12988
  onValueChange(multi ? mapped : mapped[0] ?? (valueAsPrimitive ? "" : null));
12815
12989
  };
@@ -12858,14 +13032,15 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12858
13032
  resetValidation: () => setRuleError("")
12859
13033
  });
12860
13034
  const filtered = react.useMemo(() => {
12861
- if (!searchVisible || search.trim() === "") return flat;
13035
+ if (serverSearch || !searchVisible || search.trim() === "") return flat;
12862
13036
  const q = search.trim().toLowerCase();
12863
13037
  return flat.filter(
12864
13038
  (o) => o.isGroup || (isHtmlLabel(o.label) ? extractText(o.label) : o.label).toLowerCase().includes(q)
12865
13039
  );
12866
- }, [flat, search, searchVisible]);
13040
+ }, [flat, search, searchVisible, serverSearch]);
12867
13041
  const allValues = selectable.map((o) => o.value);
12868
- const allSelected = allValues.length > 0 && allValues.every((v) => selected.includes(v));
13042
+ const allSelected = !partialOptions && allValues.length > 0 && allValues.every((v) => selectedSet.has(v));
13043
+ const selectAllVisible = multi && showSelectAll && !partialOptions && allValues.length > 0;
12869
13044
  const hasItems = filtered.some((o) => !o.isGroup);
12870
13045
  const groupLeafValues = react.useMemo(() => {
12871
13046
  const map = /* @__PURE__ */ new Map();
@@ -12876,7 +13051,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12876
13051
  }, [filtered]);
12877
13052
  const navigableItems = react.useMemo(() => {
12878
13053
  const items = [];
12879
- if (multi && showSelectAll && allValues.length > 0) {
13054
+ if (selectAllVisible) {
12880
13055
  items.push({ key: "__select_all__", isSelectAll: true });
12881
13056
  }
12882
13057
  filtered.forEach((o, i) => {
@@ -12891,20 +13066,129 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12891
13066
  }
12892
13067
  });
12893
13068
  return items;
12894
- }, [allValues.length, filtered, groupLeafValues, multi, showSelectAll]);
13069
+ }, [filtered, groupLeafValues, multi, selectAllVisible]);
12895
13070
  const selectedAnchorIndex = react.useMemo(() => {
12896
- if (selected.length === 0) return -1;
12897
- const selectedSet = new Set(selected);
13071
+ if (selectedSet.size === 0) return -1;
12898
13072
  return navigableItems.findIndex((item4) => item4.value != null && selectedSet.has(item4.value));
12899
- }, [navigableItems, selected]);
13073
+ }, [navigableItems, selectedSet]);
12900
13074
  selectedAnchorRef.current = selectedAnchorIndex;
13075
+ const navigableItemsRef = react.useRef(navigableItems);
13076
+ navigableItemsRef.current = navigableItems;
13077
+ const rowCount = filtered.length;
13078
+ const metrics = metricsRef.current;
13079
+ const lastSearchRef = react.useRef(search);
13080
+ if (lastSearchRef.current !== search) {
13081
+ lastSearchRef.current = search;
13082
+ resetRowMetrics(metrics, rowCount);
13083
+ measuredKeyRef.current = "";
13084
+ } else {
13085
+ resizeRowMetrics(metrics, rowCount);
13086
+ }
13087
+ const rowsOffset = searchBarHeight + selectAllHeight;
13088
+ const syncScrollState = react.useCallback(() => {
13089
+ if (!listEl) return;
13090
+ const m = metricsRef.current;
13091
+ if (m == null) return;
13092
+ const buffer = Math.max(0, Math.floor(virtualBuffer));
13093
+ const viewTop = Math.max(0, listEl.scrollTop - rowsOffset);
13094
+ const viewBottom = viewTop + Math.max(0, listEl.clientHeight);
13095
+ const start = Math.max(0, indexAtOffset(m, viewTop) - buffer);
13096
+ const end = Math.min(rowCount, indexAtOffset(m, viewBottom) + 1 + buffer);
13097
+ const top = offsetOf(m, start);
13098
+ const bottom = Math.max(0, totalSize(m) - offsetOf(m, end));
13099
+ setWin(
13100
+ (prev) => prev.start === start && prev.end === end && prev.top === top && prev.bottom === bottom ? prev : { start, end, top, bottom }
13101
+ );
13102
+ if (!pagingEnabled || !hasMore || loading2) return;
13103
+ const contentHeight = rowsOffset + totalSize(m);
13104
+ const remaining = contentHeight - listEl.scrollTop - listEl.clientHeight;
13105
+ if (remaining > Math.max(1, reachEndThreshold) * m.estimate) return;
13106
+ if (lastReachEndCountRef.current === rowCount) return;
13107
+ lastReachEndCountRef.current = rowCount;
13108
+ cbRef.current.onReachEnd?.();
13109
+ }, [
13110
+ hasMore,
13111
+ listEl,
13112
+ loading2,
13113
+ pagingEnabled,
13114
+ reachEndThreshold,
13115
+ rowCount,
13116
+ rowsOffset,
13117
+ virtualBuffer
13118
+ ]);
13119
+ const syncScrollStateRef = react.useRef(syncScrollState);
13120
+ syncScrollStateRef.current = syncScrollState;
13121
+ react.useLayoutEffect(() => {
13122
+ if (!open) return;
13123
+ syncScrollState();
13124
+ }, [open, syncScrollState]);
13125
+ react.useLayoutEffect(() => {
13126
+ if (!open || !listEl) {
13127
+ setSearchBarHeight(0);
13128
+ setSelectAllHeight(0);
13129
+ return;
13130
+ }
13131
+ const nextBar = searchVisible ? searchBarRef.current?.offsetHeight ?? 0 : 0;
13132
+ setSearchBarHeight((prev) => prev === nextBar ? prev : nextBar);
13133
+ const nextSelectAll = selectAllVisible ? listEl.querySelector("[data-select-all-row]")?.offsetHeight ?? 0 : 0;
13134
+ setSelectAllHeight((prev) => prev === nextSelectAll ? prev : nextSelectAll);
13135
+ }, [listEl, open, searchVisible, selectAllVisible]);
13136
+ react.useLayoutEffect(() => {
13137
+ if (!open || !listEl) return;
13138
+ const m = metricsRef.current;
13139
+ if (m == null) return;
13140
+ const key = `${winRef.current.start}:${winRef.current.end}:${rowCount}`;
13141
+ if (measuredKeyRef.current === key) return;
13142
+ measuredKeyRef.current = key;
13143
+ const anchor = winRef.current.start;
13144
+ const anchorBefore = offsetOf(m, anchor);
13145
+ let changed = false;
13146
+ for (const row of listEl.querySelectorAll("[data-select-row]")) {
13147
+ const index = Number(row.dataset.index);
13148
+ if (!Number.isInteger(index)) continue;
13149
+ if (setRowSize(m, index, row.offsetHeight)) changed = true;
13150
+ }
13151
+ if (!changed) return;
13152
+ const delta = offsetOf(m, anchor) - anchorBefore;
13153
+ if (delta !== 0 && listEl.scrollTop > 0) listEl.scrollTop += delta;
13154
+ syncScrollStateRef.current();
13155
+ });
13156
+ react.useEffect(() => {
13157
+ if (cbRef.current.onSearchChange == null) return;
13158
+ if (search === emittedQueryRef.current) return;
13159
+ if (!serverSearch) {
13160
+ emittedQueryRef.current = search;
13161
+ cbRef.current.onSearchChange(search);
13162
+ return;
13163
+ }
13164
+ const timer = setTimeout(
13165
+ () => {
13166
+ emittedQueryRef.current = search;
13167
+ cbRef.current.onSearchChange?.(search);
13168
+ },
13169
+ Math.max(0, searchDebounce)
13170
+ );
13171
+ return () => clearTimeout(timer);
13172
+ }, [search, searchDebounce, serverSearch]);
13173
+ const selectAllMisuse = multi && showSelectAll && partialOptions;
13174
+ react.useEffect(() => {
13175
+ if (!isDev()) return;
13176
+ if (selectAllMisuse) {
13177
+ console.warn(
13178
+ '[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.'
13179
+ );
13180
+ }
13181
+ }, [selectAllMisuse]);
13182
+ const windowStart = Math.min(win.start, rowCount);
13183
+ const windowEnd = Math.max(windowStart, Math.min(win.end, rowCount));
13184
+ const visibleItems = filtered.slice(windowStart, windowEnd);
12901
13185
  const triggerText = (() => {
12902
13186
  if (selected.length === 0) return placeholder;
12903
13187
  if (multi) {
12904
13188
  if (allSelected) return allSelectedLabel;
12905
- return selected.map((v) => labelByValue.get(v) ?? v).filter(Boolean).join(", ");
13189
+ return selected.map((v) => labelByValue.get(v) ?? valueOptions.get(v)?.label ?? v).filter(Boolean).join(", ");
12906
13190
  }
12907
- return labelByValue.get(selected[0]) ?? selected[0];
13191
+ return labelByValue.get(selected[0]) ?? valueOptions.get(selected[0])?.label ?? selected[0];
12908
13192
  })();
12909
13193
  const isPlaceholder = selected.length === 0;
12910
13194
  const showClear = clearable && !effectiveDisabled && !isPlaceholder;
@@ -12917,7 +13201,6 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12917
13201
  const anchor = rangeAnchorRef.current;
12918
13202
  if (extend && anchor != null && anchor !== index && filtered[anchor]?.isGroup === false) {
12919
13203
  const [from, to] = anchor < index ? [anchor, index] : [index, anchor];
12920
- const selectedSet = new Set(selected);
12921
13204
  const shouldSelect = selectedSet.has(filtered[anchor].value);
12922
13205
  const rangeValues = filtered.slice(from, to + 1).filter((o) => !o.isGroup && !o.disabled).map((o) => o.value);
12923
13206
  const inRange = new Set(rangeValues);
@@ -12942,7 +13225,6 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12942
13225
  const handleToggleGroup = (leafValues) => {
12943
13226
  if (leafValues.length === 0) return;
12944
13227
  rangeAnchorRef.current = null;
12945
- const selectedSet = new Set(selected);
12946
13228
  const allSelectedInGroup = leafValues.every((v) => selectedSet.has(v));
12947
13229
  if (allSelectedInGroup) {
12948
13230
  const remove = new Set(leafValues);
@@ -12951,35 +13233,51 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12951
13233
  emit3([...selected, ...leafValues.filter((v) => !selectedSet.has(v))]);
12952
13234
  }
12953
13235
  };
13236
+ const hasNavigableItems = navigableItems.length > 0;
12954
13237
  react.useEffect(() => {
12955
- if (!open || navigableItems.length === 0) {
13238
+ if (!open || !hasNavigableItems) {
12956
13239
  setFocusedIndex(-1);
12957
13240
  return;
12958
13241
  }
12959
13242
  const anchor = alignSelectedTopRef.current ? selectedAnchorRef.current : -1;
12960
13243
  setFocusedIndex(anchor >= 0 ? anchor : 0);
12961
- }, [open, search, navigableItems.length]);
13244
+ }, [open, search, hasNavigableItems]);
13245
+ const lastResetSearchRef = react.useRef(search);
13246
+ react.useLayoutEffect(() => {
13247
+ if (lastResetSearchRef.current === search) return;
13248
+ lastResetSearchRef.current = search;
13249
+ if (!open || !listEl) return;
13250
+ listEl.scrollTop = 0;
13251
+ syncScrollStateRef.current();
13252
+ }, [listEl, open, search]);
12962
13253
  react.useEffect(() => {
12963
13254
  rangeAnchorRef.current = null;
13255
+ lastReachEndCountRef.current = -1;
12964
13256
  }, [open, search]);
12965
13257
  react.useLayoutEffect(() => {
12966
13258
  if (!open || focusedIndex < 0 || !listEl) return;
12967
- const focusedEl = listEl.querySelector('[data-select-focused="true"]');
12968
- if (!focusedEl) return;
12969
- const listRect = listEl.getBoundingClientRect();
12970
- const itemRect = focusedEl.getBoundingClientRect();
12971
- if (alignSelectedTopRef.current) {
13259
+ const m = metricsRef.current;
13260
+ if (m == null) return;
13261
+ const stickyOffset = searchBarRef.current?.offsetHeight ?? 0;
13262
+ const rowIndex = navigableItemsRef.current[focusedIndex]?.index;
13263
+ if (rowIndex == null) {
12972
13264
  alignSelectedTopRef.current = false;
12973
- const stickyOffset = searchBarRef.current?.offsetHeight ?? 0;
12974
- listEl.scrollTop += itemRect.top - listRect.top - stickyOffset;
13265
+ listEl.scrollTop = 0;
13266
+ syncScrollStateRef.current();
12975
13267
  return;
12976
13268
  }
12977
- if (itemRect.top < listRect.top) {
12978
- listEl.scrollTop += itemRect.top - listRect.top;
12979
- } else if (itemRect.bottom > listRect.bottom) {
12980
- listEl.scrollTop += itemRect.bottom - listRect.bottom;
12981
- }
12982
- }, [focusedIndex, open, listEl]);
13269
+ const top = rowsOffset + offsetOf(m, rowIndex);
13270
+ const height = sizeOf(m, rowIndex);
13271
+ if (alignSelectedTopRef.current) {
13272
+ alignSelectedTopRef.current = false;
13273
+ listEl.scrollTop = top - stickyOffset;
13274
+ } else if (top < listEl.scrollTop + stickyOffset) {
13275
+ listEl.scrollTop = top - stickyOffset;
13276
+ } else if (top + height > listEl.scrollTop + listEl.clientHeight) {
13277
+ listEl.scrollTop = top + height - listEl.clientHeight;
13278
+ }
13279
+ syncScrollStateRef.current();
13280
+ }, [focusedIndex, listEl, open, rowsOffset]);
12983
13281
  const moveFocusedIndex = (direction) => {
12984
13282
  if (!open || navigableItems.length === 0) return;
12985
13283
  setFocusedIndex((current) => {
@@ -13169,6 +13467,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
13169
13467
  SCROLLBAR_TRACK_BORDER_CLASS
13170
13468
  ),
13171
13469
  style: { maxHeight: clampToAvailableHeight(dropdownHeight, POPOVER_AVAILABLE_HEIGHT) },
13470
+ onScroll: syncScrollState,
13172
13471
  onMouseLeave: () => setFocusedIndex(-1),
13173
13472
  children: [
13174
13473
  searchVisible && /* @__PURE__ */ jsxRuntime.jsx(
@@ -13192,12 +13491,16 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
13192
13491
  hasItems ? /* @__PURE__ */ jsxRuntime.jsx(
13193
13492
  ItemList,
13194
13493
  {
13195
- items: filtered,
13494
+ items: visibleItems,
13495
+ indexOffset: windowStart,
13496
+ topSpacer: win.top,
13497
+ bottomSpacer: win.bottom,
13498
+ setSize: isDepth ? void 0 : rowCount,
13196
13499
  multi,
13197
13500
  selected,
13198
13501
  groupLeafValues,
13199
13502
  focusedValue: navigableItems[focusedIndex]?.key,
13200
- showSelectAll: multi && showSelectAll && allValues.length > 0,
13503
+ showSelectAll: selectAllVisible,
13201
13504
  selectAllLabel: allSelectedLabel,
13202
13505
  selectAllSelected: allSelected,
13203
13506
  onSelectAll: handleSelectAll,
@@ -13212,7 +13515,18 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
13212
13515
  if (index >= 0) setFocusedIndex(index);
13213
13516
  }
13214
13517
  }
13215
- ) : /* @__PURE__ */ jsxRuntime.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." })
13518
+ ) : loading2 ? null : (
13519
+ // 첫 페이지를 받는 중에는 "없다"고 단정하지 않는다 — 아래 로딩 표시가 대신 나온다.
13520
+ /* @__PURE__ */ jsxRuntime.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." })
13521
+ ),
13522
+ loading2 && /* @__PURE__ */ jsxRuntime.jsx(
13523
+ "div",
13524
+ {
13525
+ role: "status",
13526
+ className: "px-[12px] py-[12px] text-center text-[12px] leading-[20px] text-[color:var(--cmp-listBoxItem-content-disabled)]",
13527
+ children: "\uBD88\uB7EC\uC624\uB294 \uC911..."
13528
+ }
13529
+ )
13216
13530
  ]
13217
13531
  }
13218
13532
  )
@@ -13225,6 +13539,10 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
13225
13539
  var SSelect2 = /* @__PURE__ */ withDisplayName(SSelectImpl, "SSelect");
13226
13540
  function ItemList({
13227
13541
  items,
13542
+ indexOffset,
13543
+ topSpacer,
13544
+ bottomSpacer,
13545
+ setSize,
13228
13546
  multi,
13229
13547
  selected,
13230
13548
  groupLeafValues,
@@ -13249,6 +13567,7 @@ function ItemList({
13249
13567
  "button",
13250
13568
  {
13251
13569
  type: "button",
13570
+ "data-select-all-row": true,
13252
13571
  "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
13253
13572
  "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
13254
13573
  className: cn(
@@ -13265,7 +13584,9 @@ function ItemList({
13265
13584
  ]
13266
13585
  }
13267
13586
  ),
13268
- items.map((o, idx) => {
13587
+ topSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: { height: topSpacer } }),
13588
+ items.map((o, windowIndex) => {
13589
+ const idx = windowIndex + indexOffset;
13269
13590
  const padLeft = o.depth === 1 ? 12 : o.depth === 2 ? 20 : 28;
13270
13591
  const rowKey = `${o.value}-${idx}`;
13271
13592
  if (o.isGroup) {
@@ -13282,6 +13603,8 @@ function ItemList({
13282
13603
  "button",
13283
13604
  {
13284
13605
  type: "button",
13606
+ "data-select-row": true,
13607
+ "data-index": idx,
13285
13608
  role: "option",
13286
13609
  "aria-selected": groupState === true,
13287
13610
  disabled: groupDisabled,
@@ -13332,6 +13655,8 @@ function ItemList({
13332
13655
  return /* @__PURE__ */ jsxRuntime.jsx(
13333
13656
  "div",
13334
13657
  {
13658
+ "data-select-row": true,
13659
+ "data-index": idx,
13335
13660
  className: cn(
13336
13661
  "select-none py-[4px] pr-[12px] text-[12px] leading-[20px] text-[color:var(--cmp-listBoxItem-content-default)]",
13337
13662
  groupTone,
@@ -13351,9 +13676,13 @@ function ItemList({
13351
13676
  "button",
13352
13677
  {
13353
13678
  type: "button",
13679
+ "data-select-row": true,
13680
+ "data-index": idx,
13354
13681
  disabled: o.disabled,
13355
13682
  role: "option",
13356
13683
  "aria-selected": isSelected,
13684
+ "aria-setsize": setSize,
13685
+ "aria-posinset": setSize != null ? idx + 1 : void 0,
13357
13686
  "data-select-focused": focusedValue === o.value ? "true" : void 0,
13358
13687
  "data-select-active": focusedValue === o.value ? "true" : void 0,
13359
13688
  className: cn(
@@ -13388,7 +13717,8 @@ function ItemList({
13388
13717
  },
13389
13718
  rowKey
13390
13719
  );
13391
- })
13720
+ }),
13721
+ bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: { height: bottomSpacer } })
13392
13722
  ] });
13393
13723
  }
13394
13724
  function CheckboxBox({
@@ -14549,12 +14879,29 @@ var SInput = /* @__PURE__ */ react.forwardRef(function SInput2({
14549
14879
  }
14550
14880
  );
14551
14881
  });
14552
- var PAD2 = { fontSize: 12, paddingX: 12, paddingY: 4 };
14882
+ var TEXTAREA_SIZE = {
14883
+ sm: {
14884
+ fontSize: 12,
14885
+ lineHeight: 20,
14886
+ paddingX: "var(--cmp-textinput-sm-paddingX)",
14887
+ paddingY: "var(--cmp-textinput-sm-paddingY)"
14888
+ },
14889
+ md: {
14890
+ fontSize: 14,
14891
+ lineHeight: 24,
14892
+ paddingX: "var(--cmp-textinput-md-paddingX)",
14893
+ paddingY: "var(--cmp-textinput-md-paddingY)"
14894
+ }
14895
+ };
14896
+ var MIN_ROWS = 2;
14553
14897
  var STextarea = /* @__PURE__ */ react.forwardRef(function STextarea2({
14554
14898
  value,
14555
14899
  onValueChange,
14556
14900
  onChange,
14557
- rows = 3,
14901
+ size = "sm",
14902
+ rows = MIN_ROWS,
14903
+ autogrow = false,
14904
+ maxRows,
14558
14905
  rules,
14559
14906
  status,
14560
14907
  focused,
@@ -14581,8 +14928,30 @@ var STextarea = /* @__PURE__ */ react.forwardRef(function STextarea2({
14581
14928
  style,
14582
14929
  ...rest
14583
14930
  }, ref) {
14584
- const p = PAD2;
14931
+ const p = TEXTAREA_SIZE[size];
14932
+ const heightForRows = (n) => `calc(${p.lineHeight * n}px + ${p.paddingY} * 2)`;
14933
+ const minHeight = heightForRows(MIN_ROWS);
14934
+ const maxHeight = autogrow && maxRows != null ? heightForRows(maxRows) : void 0;
14585
14935
  const [ruleError, setRuleError] = react.useState("");
14936
+ const innerRef = react.useRef(null);
14937
+ const setRefs2 = react.useCallback(
14938
+ (node) => {
14939
+ innerRef.current = node;
14940
+ if (typeof ref === "function") ref(node);
14941
+ else if (ref) ref.current = node;
14942
+ },
14943
+ [ref]
14944
+ );
14945
+ react.useLayoutEffect(() => {
14946
+ const el = innerRef.current;
14947
+ if (el == null) return;
14948
+ if (!autogrow) {
14949
+ el.style.height = "";
14950
+ return;
14951
+ }
14952
+ el.style.height = "auto";
14953
+ el.style.height = `${el.scrollHeight}px`;
14954
+ }, [autogrow, maxHeight, minHeight, rows, value]);
14586
14955
  useFormField({
14587
14956
  name: rest.name,
14588
14957
  validate: () => {
@@ -14607,6 +14976,7 @@ var STextarea = /* @__PURE__ */ react.forwardRef(function STextarea2({
14607
14976
  addonLabel,
14608
14977
  addonAlign,
14609
14978
  hint,
14979
+ size,
14610
14980
  error: resolvedError,
14611
14981
  status: resolvedStatus,
14612
14982
  focused,
@@ -14621,7 +14991,7 @@ var STextarea = /* @__PURE__ */ react.forwardRef(function STextarea2({
14621
14991
  children: /* @__PURE__ */ jsxRuntime.jsx(
14622
14992
  "textarea",
14623
14993
  {
14624
- ref,
14994
+ ref: setRefs2,
14625
14995
  value: value ?? "",
14626
14996
  rows,
14627
14997
  disabled,
@@ -14637,15 +15007,21 @@ var STextarea = /* @__PURE__ */ react.forwardRef(function STextarea2({
14637
15007
  if (rules && rules.length > 0) setRuleError(runRules(e.target.value, rules));
14638
15008
  },
14639
15009
  className: cn(
14640
- "w-full resize-y border-0 bg-transparent outline-none placeholder:text-[color:var(--sys-color-field-text-placeholder)] disabled:cursor-not-allowed",
15010
+ "w-full border-0 bg-transparent outline-none placeholder:text-[color:var(--sys-color-field-text-placeholder)] disabled:cursor-not-allowed",
15011
+ // autogrow 와 끌어서 늘리기는 같은 높이를 두고 다투므로 함께 두지 않는다
15012
+ autogrow ? "resize-none" : "resize-y",
14641
15013
  AUTOFILL_RESET_CLASS,
14642
15014
  textareaClassName
14643
15015
  ),
14644
15016
  style: {
14645
- minHeight: 100,
14646
- padding: `${p.paddingY}px ${p.paddingX}px`,
15017
+ minHeight,
15018
+ maxHeight,
15019
+ // 상한이 있으면 거기 닿았을 때 안에서 스크롤한다. 상한이 없으면 높이가 늘 내용과
15020
+ // 같으므로 스크롤바가 나올 일이 없고, 반올림으로 1px 이 넘쳐 깜빡이는 것만 막는다.
15021
+ overflowY: autogrow ? maxRows != null ? "auto" : "hidden" : void 0,
15022
+ padding: `${p.paddingY} ${p.paddingX}`,
14647
15023
  fontSize: p.fontSize,
14648
- lineHeight: "20px",
15024
+ lineHeight: `${p.lineHeight}px`,
14649
15025
  color: disabled ? "var(--cmp-textinput-text-disabled)" : "var(--sys-color-field-text-default)",
14650
15026
  ...textareaStyle
14651
15027
  },
@@ -14655,7 +15031,7 @@ var STextarea = /* @__PURE__ */ react.forwardRef(function STextarea2({
14655
15031
  }
14656
15032
  );
14657
15033
  });
14658
- var PAD3 = {
15034
+ var PAD2 = {
14659
15035
  sm: { fontSize: 12, lineHeight: 20, stepper: 20, icon: 12 },
14660
15036
  md: { fontSize: 14, lineHeight: 24, stepper: 24, icon: 14 }
14661
15037
  };
@@ -14684,6 +15060,16 @@ var formatInputWithCommas = (value) => {
14684
15060
  const formattedInt = addCommas(intPart === "" ? "0" : intPart);
14685
15061
  return `${isNeg ? "-" : ""}${formattedInt}${hasDecimal ? `.${decPart}` : ""}`;
14686
15062
  };
15063
+ var getRangeError = (value, min, max) => {
15064
+ if (value === null) return "";
15065
+ const belowMin = value < min;
15066
+ const aboveMax = value > max;
15067
+ if (!belowMin && !aboveMax) return "";
15068
+ if (Number.isFinite(min) && Number.isFinite(max)) {
15069
+ return `${formatWithCommas(min)}~${formatWithCommas(max)} \uC0AC\uC774\uB85C \uC785\uB825\uD574 \uC8FC\uC138\uC694.`;
15070
+ }
15071
+ 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.`;
15072
+ };
14687
15073
  var getDecimalLimit = (allowDecimal, maxDecimalLength) => {
14688
15074
  if (!allowDecimal || maxDecimalLength === void 0 || !Number.isFinite(maxDecimalLength)) {
14689
15075
  return void 0;
@@ -14714,6 +15100,7 @@ var SNumberInput = /* @__PURE__ */ react.forwardRef(function SNumberInput2({
14714
15100
  min = Number.NEGATIVE_INFINITY,
14715
15101
  max = Number.POSITIVE_INFINITY,
14716
15102
  step = 1,
15103
+ clampOnBlur = false,
14717
15104
  showButton = false,
14718
15105
  allowDecimal = false,
14719
15106
  maxDecimalLength,
@@ -14747,12 +15134,13 @@ var SNumberInput = /* @__PURE__ */ react.forwardRef(function SNumberInput2({
14747
15134
  className,
14748
15135
  style
14749
15136
  }, ref) {
14750
- const p = PAD3[size];
15137
+ const p = PAD2[size];
14751
15138
  const padX = `var(--cmp-numberInput-${size}-paddingX)`;
14752
15139
  const decimalLimit = getDecimalLimit(allowDecimal, maxDecimalLength);
14753
15140
  const numValue = applyDecimalLimit(toNumber(value), decimalLimit);
14754
15141
  const [display, setDisplay] = react.useState(() => formatWithCommas(numValue));
14755
15142
  const [ruleError, setRuleError] = react.useState("");
15143
+ const [rangeError, setRangeError] = react.useState("");
14756
15144
  const innerRef = react.useRef(null);
14757
15145
  const valueTooltip = useTruncatedValueTooltip({ enabled: readOnly && !disabled, text: display });
14758
15146
  const setRefs2 = (el) => {
@@ -14770,12 +15158,19 @@ var SNumberInput = /* @__PURE__ */ react.forwardRef(function SNumberInput2({
14770
15158
  validate: () => {
14771
15159
  const msg = runRules(numValue, rules);
14772
15160
  setRuleError(msg);
14773
- return msg === "";
15161
+ const range = clampOnBlur ? "" : getRangeError(numValue, min, max);
15162
+ setRangeError(range);
15163
+ return msg === "" && range === "";
14774
15164
  },
14775
- resetValidation: () => setRuleError("")
15165
+ resetValidation: () => {
15166
+ setRuleError("");
15167
+ setRangeError("");
15168
+ }
14776
15169
  });
14777
- const resolvedError = error || ruleError !== "";
14778
- const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
15170
+ const autoError = ruleError !== "" ? ruleError : rangeError;
15171
+ const resolvedError = error || autoError !== "";
15172
+ const resolvedStatus = status ?? (autoError !== "" ? "error" : void 0);
15173
+ const resolvedErrorMessage = ruleError !== "" ? ruleError : (errorMessage ?? "") !== "" ? errorMessage : rangeError;
14779
15174
  const restoreCaret = (nextDisplay, logicalCaret) => {
14780
15175
  const input = innerRef.current;
14781
15176
  if (logicalCaret === void 0 || !input || document.activeElement !== input) return;
@@ -14907,7 +15302,7 @@ var SNumberInput = /* @__PURE__ */ react.forwardRef(function SNumberInput2({
14907
15302
  status: resolvedStatus,
14908
15303
  focused,
14909
15304
  hovered,
14910
- errorMessage: ruleError !== "" ? ruleError : errorMessage,
15305
+ errorMessage: resolvedErrorMessage,
14911
15306
  width,
14912
15307
  disabled,
14913
15308
  readOnly,
@@ -14953,10 +15348,11 @@ var SNumberInput = /* @__PURE__ */ react.forwardRef(function SNumberInput2({
14953
15348
  onBlur: (e) => {
14954
15349
  const cleaned = display.replace(/,/g, "");
14955
15350
  const n = toNumber(cleaned);
14956
- const clamped = n === null ? null : Math.min(Math.max(n, min), max);
15351
+ const clamped = !clampOnBlur || n === null ? n : Math.min(Math.max(n, min), max);
14957
15352
  if (clamped !== n) emit3(clamped);
14958
15353
  else setDisplay(n === null ? "" : formatInputWithCommas(cleaned));
14959
15354
  if (rules && rules.length > 0) setRuleError(runRules(clamped, rules));
15355
+ setRangeError(clampOnBlur ? "" : getRangeError(clamped, min, max));
14960
15356
  onBlur?.(e);
14961
15357
  },
14962
15358
  className: cn(