sellmate-design-system-react 9.0.0-beta.44 → 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
  )
@@ -12724,6 +12724,130 @@ function collectGroupLeafValues(flat, groupIndex) {
12724
12724
  }
12725
12725
  var isHtmlLabel = (label) => /<[a-z][\s\S]*>/i.test(label);
12726
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
+ }
12727
12851
  var SEARCH_THRESHOLD = 11;
12728
12852
  var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12729
12853
  value,
@@ -12745,6 +12869,14 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12745
12869
  dropdownHeight = "640px",
12746
12870
  dropdownWidth,
12747
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,
12748
12880
  label,
12749
12881
  labelWidth,
12750
12882
  icon,
@@ -12759,6 +12891,9 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12759
12891
  style
12760
12892
  }, ref) {
12761
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;
12762
12897
  const portalContainer = usePortalContainer();
12763
12898
  const insideModal = useInsideModal();
12764
12899
  const [search, setSearch] = useState("");
@@ -12766,6 +12901,15 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12766
12901
  const [open, setOpen] = useState(false);
12767
12902
  const [pendingOpen, setPendingOpen] = useState(false);
12768
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);
12769
12913
  const triggerRef = useRef(null);
12770
12914
  const searchInputRef = useRef(null);
12771
12915
  const searchBarRef = useRef(null);
@@ -12780,7 +12924,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12780
12924
  const hasLeaf = useMemo(() => flat.some((o) => !o.isGroup), [flat]);
12781
12925
  const allOptionsDisabled = hasLeaf && selectable.length === 0;
12782
12926
  const effectiveDisabled = disabled || allOptionsDisabled;
12783
- const searchVisible = showSearch && selectable.length >= SEARCH_THRESHOLD;
12927
+ const searchVisible = showSearch && (partialOptions || selectable.length >= SEARCH_THRESHOLD);
12784
12928
  const labelByValue = useMemo(() => new Map(flat.map((o) => [o.value, o.label])), [flat]);
12785
12929
  const rawByValue = useMemo(() => new Map(flat.map((o) => [o.value, o.raw])), [flat]);
12786
12930
  const optionByValue = useMemo(
@@ -12792,12 +12936,26 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12792
12936
  const arr = Array.isArray(value) ? value : [value];
12793
12937
  return arr.map((v) => v != null && typeof v === "object" ? v.value : v).filter((v) => v != null && v !== "").map(String);
12794
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]);
12795
12949
  const selectedRef = useRef(selected);
12796
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("");
12797
12955
  const emit3 = (next) => {
12798
12956
  if (!onValueChange) return;
12799
12957
  const mapped = next.map(
12800
- (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 }
12801
12959
  );
12802
12960
  onValueChange(multi ? mapped : mapped[0] ?? (valueAsPrimitive ? "" : null));
12803
12961
  };
@@ -12846,14 +13004,15 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12846
13004
  resetValidation: () => setRuleError("")
12847
13005
  });
12848
13006
  const filtered = useMemo(() => {
12849
- if (!searchVisible || search.trim() === "") return flat;
13007
+ if (serverSearch || !searchVisible || search.trim() === "") return flat;
12850
13008
  const q = search.trim().toLowerCase();
12851
13009
  return flat.filter(
12852
13010
  (o) => o.isGroup || (isHtmlLabel(o.label) ? extractText(o.label) : o.label).toLowerCase().includes(q)
12853
13011
  );
12854
- }, [flat, search, searchVisible]);
13012
+ }, [flat, search, searchVisible, serverSearch]);
12855
13013
  const allValues = selectable.map((o) => o.value);
12856
- 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;
12857
13016
  const hasItems = filtered.some((o) => !o.isGroup);
12858
13017
  const groupLeafValues = useMemo(() => {
12859
13018
  const map = /* @__PURE__ */ new Map();
@@ -12864,7 +13023,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12864
13023
  }, [filtered]);
12865
13024
  const navigableItems = useMemo(() => {
12866
13025
  const items = [];
12867
- if (multi && showSelectAll && allValues.length > 0) {
13026
+ if (selectAllVisible) {
12868
13027
  items.push({ key: "__select_all__", isSelectAll: true });
12869
13028
  }
12870
13029
  filtered.forEach((o, i) => {
@@ -12879,20 +13038,129 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12879
13038
  }
12880
13039
  });
12881
13040
  return items;
12882
- }, [allValues.length, filtered, groupLeafValues, multi, showSelectAll]);
13041
+ }, [filtered, groupLeafValues, multi, selectAllVisible]);
12883
13042
  const selectedAnchorIndex = useMemo(() => {
12884
- if (selected.length === 0) return -1;
12885
- const selectedSet = new Set(selected);
13043
+ if (selectedSet.size === 0) return -1;
12886
13044
  return navigableItems.findIndex((item4) => item4.value != null && selectedSet.has(item4.value));
12887
- }, [navigableItems, selected]);
13045
+ }, [navigableItems, selectedSet]);
12888
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);
12889
13157
  const triggerText = (() => {
12890
13158
  if (selected.length === 0) return placeholder;
12891
13159
  if (multi) {
12892
13160
  if (allSelected) return allSelectedLabel;
12893
- 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(", ");
12894
13162
  }
12895
- return labelByValue.get(selected[0]) ?? selected[0];
13163
+ return labelByValue.get(selected[0]) ?? valueOptions.get(selected[0])?.label ?? selected[0];
12896
13164
  })();
12897
13165
  const isPlaceholder = selected.length === 0;
12898
13166
  const showClear = clearable && !effectiveDisabled && !isPlaceholder;
@@ -12905,7 +13173,6 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12905
13173
  const anchor = rangeAnchorRef.current;
12906
13174
  if (extend && anchor != null && anchor !== index && filtered[anchor]?.isGroup === false) {
12907
13175
  const [from, to] = anchor < index ? [anchor, index] : [index, anchor];
12908
- const selectedSet = new Set(selected);
12909
13176
  const shouldSelect = selectedSet.has(filtered[anchor].value);
12910
13177
  const rangeValues = filtered.slice(from, to + 1).filter((o) => !o.isGroup && !o.disabled).map((o) => o.value);
12911
13178
  const inRange = new Set(rangeValues);
@@ -12930,7 +13197,6 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12930
13197
  const handleToggleGroup = (leafValues) => {
12931
13198
  if (leafValues.length === 0) return;
12932
13199
  rangeAnchorRef.current = null;
12933
- const selectedSet = new Set(selected);
12934
13200
  const allSelectedInGroup = leafValues.every((v) => selectedSet.has(v));
12935
13201
  if (allSelectedInGroup) {
12936
13202
  const remove = new Set(leafValues);
@@ -12939,35 +13205,51 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
12939
13205
  emit3([...selected, ...leafValues.filter((v) => !selectedSet.has(v))]);
12940
13206
  }
12941
13207
  };
13208
+ const hasNavigableItems = navigableItems.length > 0;
12942
13209
  useEffect(() => {
12943
- if (!open || navigableItems.length === 0) {
13210
+ if (!open || !hasNavigableItems) {
12944
13211
  setFocusedIndex(-1);
12945
13212
  return;
12946
13213
  }
12947
13214
  const anchor = alignSelectedTopRef.current ? selectedAnchorRef.current : -1;
12948
13215
  setFocusedIndex(anchor >= 0 ? anchor : 0);
12949
- }, [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]);
12950
13225
  useEffect(() => {
12951
13226
  rangeAnchorRef.current = null;
13227
+ lastReachEndCountRef.current = -1;
12952
13228
  }, [open, search]);
12953
13229
  useLayoutEffect(() => {
12954
13230
  if (!open || focusedIndex < 0 || !listEl) return;
12955
- const focusedEl = listEl.querySelector('[data-select-focused="true"]');
12956
- if (!focusedEl) return;
12957
- const listRect = listEl.getBoundingClientRect();
12958
- const itemRect = focusedEl.getBoundingClientRect();
12959
- 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) {
12960
13236
  alignSelectedTopRef.current = false;
12961
- const stickyOffset = searchBarRef.current?.offsetHeight ?? 0;
12962
- listEl.scrollTop += itemRect.top - listRect.top - stickyOffset;
13237
+ listEl.scrollTop = 0;
13238
+ syncScrollStateRef.current();
12963
13239
  return;
12964
13240
  }
12965
- if (itemRect.top < listRect.top) {
12966
- listEl.scrollTop += itemRect.top - listRect.top;
12967
- } else if (itemRect.bottom > listRect.bottom) {
12968
- listEl.scrollTop += itemRect.bottom - listRect.bottom;
12969
- }
12970
- }, [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]);
12971
13253
  const moveFocusedIndex = (direction) => {
12972
13254
  if (!open || navigableItems.length === 0) return;
12973
13255
  setFocusedIndex((current) => {
@@ -13157,6 +13439,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
13157
13439
  SCROLLBAR_TRACK_BORDER_CLASS
13158
13440
  ),
13159
13441
  style: { maxHeight: clampToAvailableHeight(dropdownHeight, POPOVER_AVAILABLE_HEIGHT) },
13442
+ onScroll: syncScrollState,
13160
13443
  onMouseLeave: () => setFocusedIndex(-1),
13161
13444
  children: [
13162
13445
  searchVisible && /* @__PURE__ */ jsx(
@@ -13180,12 +13463,16 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
13180
13463
  hasItems ? /* @__PURE__ */ jsx(
13181
13464
  ItemList,
13182
13465
  {
13183
- items: filtered,
13466
+ items: visibleItems,
13467
+ indexOffset: windowStart,
13468
+ topSpacer: win.top,
13469
+ bottomSpacer: win.bottom,
13470
+ setSize: isDepth ? void 0 : rowCount,
13184
13471
  multi,
13185
13472
  selected,
13186
13473
  groupLeafValues,
13187
13474
  focusedValue: navigableItems[focusedIndex]?.key,
13188
- showSelectAll: multi && showSelectAll && allValues.length > 0,
13475
+ showSelectAll: selectAllVisible,
13189
13476
  selectAllLabel: allSelectedLabel,
13190
13477
  selectAllSelected: allSelected,
13191
13478
  onSelectAll: handleSelectAll,
@@ -13200,7 +13487,18 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
13200
13487
  if (index >= 0) setFocusedIndex(index);
13201
13488
  }
13202
13489
  }
13203
- ) : /* @__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
+ )
13204
13502
  ]
13205
13503
  }
13206
13504
  )
@@ -13213,6 +13511,10 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
13213
13511
  var SSelect2 = /* @__PURE__ */ withDisplayName(SSelectImpl, "SSelect");
13214
13512
  function ItemList({
13215
13513
  items,
13514
+ indexOffset,
13515
+ topSpacer,
13516
+ bottomSpacer,
13517
+ setSize,
13216
13518
  multi,
13217
13519
  selected,
13218
13520
  groupLeafValues,
@@ -13237,6 +13539,7 @@ function ItemList({
13237
13539
  "button",
13238
13540
  {
13239
13541
  type: "button",
13542
+ "data-select-all-row": true,
13240
13543
  "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
13241
13544
  "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
13242
13545
  className: cn(
@@ -13253,7 +13556,9 @@ function ItemList({
13253
13556
  ]
13254
13557
  }
13255
13558
  ),
13256
- 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;
13257
13562
  const padLeft = o.depth === 1 ? 12 : o.depth === 2 ? 20 : 28;
13258
13563
  const rowKey = `${o.value}-${idx}`;
13259
13564
  if (o.isGroup) {
@@ -13270,6 +13575,8 @@ function ItemList({
13270
13575
  "button",
13271
13576
  {
13272
13577
  type: "button",
13578
+ "data-select-row": true,
13579
+ "data-index": idx,
13273
13580
  role: "option",
13274
13581
  "aria-selected": groupState === true,
13275
13582
  disabled: groupDisabled,
@@ -13320,6 +13627,8 @@ function ItemList({
13320
13627
  return /* @__PURE__ */ jsx(
13321
13628
  "div",
13322
13629
  {
13630
+ "data-select-row": true,
13631
+ "data-index": idx,
13323
13632
  className: cn(
13324
13633
  "select-none py-[4px] pr-[12px] text-[12px] leading-[20px] text-[color:var(--cmp-listBoxItem-content-default)]",
13325
13634
  groupTone,
@@ -13339,9 +13648,13 @@ function ItemList({
13339
13648
  "button",
13340
13649
  {
13341
13650
  type: "button",
13651
+ "data-select-row": true,
13652
+ "data-index": idx,
13342
13653
  disabled: o.disabled,
13343
13654
  role: "option",
13344
13655
  "aria-selected": isSelected,
13656
+ "aria-setsize": setSize,
13657
+ "aria-posinset": setSize != null ? idx + 1 : void 0,
13345
13658
  "data-select-focused": focusedValue === o.value ? "true" : void 0,
13346
13659
  "data-select-active": focusedValue === o.value ? "true" : void 0,
13347
13660
  className: cn(
@@ -13376,7 +13689,8 @@ function ItemList({
13376
13689
  },
13377
13690
  rowKey
13378
13691
  );
13379
- })
13692
+ }),
13693
+ bottomSpacer > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: { height: bottomSpacer } })
13380
13694
  ] });
13381
13695
  }
13382
13696
  function CheckboxBox({
@@ -14537,12 +14851,29 @@ var SInput = /* @__PURE__ */ forwardRef(function SInput2({
14537
14851
  }
14538
14852
  );
14539
14853
  });
14540
- 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;
14541
14869
  var STextarea = /* @__PURE__ */ forwardRef(function STextarea2({
14542
14870
  value,
14543
14871
  onValueChange,
14544
14872
  onChange,
14545
- rows = 3,
14873
+ size = "sm",
14874
+ rows = MIN_ROWS,
14875
+ autogrow = false,
14876
+ maxRows,
14546
14877
  rules,
14547
14878
  status,
14548
14879
  focused,
@@ -14569,8 +14900,30 @@ var STextarea = /* @__PURE__ */ forwardRef(function STextarea2({
14569
14900
  style,
14570
14901
  ...rest
14571
14902
  }, ref) {
14572
- 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;
14573
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]);
14574
14927
  useFormField({
14575
14928
  name: rest.name,
14576
14929
  validate: () => {
@@ -14595,6 +14948,7 @@ var STextarea = /* @__PURE__ */ forwardRef(function STextarea2({
14595
14948
  addonLabel,
14596
14949
  addonAlign,
14597
14950
  hint,
14951
+ size,
14598
14952
  error: resolvedError,
14599
14953
  status: resolvedStatus,
14600
14954
  focused,
@@ -14609,7 +14963,7 @@ var STextarea = /* @__PURE__ */ forwardRef(function STextarea2({
14609
14963
  children: /* @__PURE__ */ jsx(
14610
14964
  "textarea",
14611
14965
  {
14612
- ref,
14966
+ ref: setRefs2,
14613
14967
  value: value ?? "",
14614
14968
  rows,
14615
14969
  disabled,
@@ -14625,15 +14979,21 @@ var STextarea = /* @__PURE__ */ forwardRef(function STextarea2({
14625
14979
  if (rules && rules.length > 0) setRuleError(runRules(e.target.value, rules));
14626
14980
  },
14627
14981
  className: cn(
14628
- "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",
14629
14985
  AUTOFILL_RESET_CLASS,
14630
14986
  textareaClassName
14631
14987
  ),
14632
14988
  style: {
14633
- minHeight: 100,
14634
- 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}`,
14635
14995
  fontSize: p.fontSize,
14636
- lineHeight: "20px",
14996
+ lineHeight: `${p.lineHeight}px`,
14637
14997
  color: disabled ? "var(--cmp-textinput-text-disabled)" : "var(--sys-color-field-text-default)",
14638
14998
  ...textareaStyle
14639
14999
  },
@@ -14643,7 +15003,7 @@ var STextarea = /* @__PURE__ */ forwardRef(function STextarea2({
14643
15003
  }
14644
15004
  );
14645
15005
  });
14646
- var PAD3 = {
15006
+ var PAD2 = {
14647
15007
  sm: { fontSize: 12, lineHeight: 20, stepper: 20, icon: 12 },
14648
15008
  md: { fontSize: 14, lineHeight: 24, stepper: 24, icon: 14 }
14649
15009
  };
@@ -14746,7 +15106,7 @@ var SNumberInput = /* @__PURE__ */ forwardRef(function SNumberInput2({
14746
15106
  className,
14747
15107
  style
14748
15108
  }, ref) {
14749
- const p = PAD3[size];
15109
+ const p = PAD2[size];
14750
15110
  const padX = `var(--cmp-numberInput-${size}-paddingX)`;
14751
15111
  const decimalLimit = getDecimalLimit(allowDecimal, maxDecimalLength);
14752
15112
  const numValue = applyDecimalLimit(toNumber(value), decimalLimit);