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.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
  )
@@ -12752,6 +12752,130 @@ function collectGroupLeafValues(flat, groupIndex) {
12752
12752
  }
12753
12753
  var isHtmlLabel = (label) => /<[a-z][\s\S]*>/i.test(label);
12754
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
+ }
12755
12879
  var SEARCH_THRESHOLD = 11;
12756
12880
  var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12757
12881
  value,
@@ -12773,6 +12897,14 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12773
12897
  dropdownHeight = "640px",
12774
12898
  dropdownWidth,
12775
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,
12776
12908
  label,
12777
12909
  labelWidth,
12778
12910
  icon,
@@ -12787,6 +12919,9 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12787
12919
  style
12788
12920
  }, ref) {
12789
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;
12790
12925
  const portalContainer = usePortalContainer();
12791
12926
  const insideModal = useInsideModal();
12792
12927
  const [search, setSearch] = react.useState("");
@@ -12794,6 +12929,15 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12794
12929
  const [open, setOpen] = react.useState(false);
12795
12930
  const [pendingOpen, setPendingOpen] = react.useState(false);
12796
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);
12797
12941
  const triggerRef = react.useRef(null);
12798
12942
  const searchInputRef = react.useRef(null);
12799
12943
  const searchBarRef = react.useRef(null);
@@ -12808,7 +12952,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12808
12952
  const hasLeaf = react.useMemo(() => flat.some((o) => !o.isGroup), [flat]);
12809
12953
  const allOptionsDisabled = hasLeaf && selectable.length === 0;
12810
12954
  const effectiveDisabled = disabled || allOptionsDisabled;
12811
- const searchVisible = showSearch && selectable.length >= SEARCH_THRESHOLD;
12955
+ const searchVisible = showSearch && (partialOptions || selectable.length >= SEARCH_THRESHOLD);
12812
12956
  const labelByValue = react.useMemo(() => new Map(flat.map((o) => [o.value, o.label])), [flat]);
12813
12957
  const rawByValue = react.useMemo(() => new Map(flat.map((o) => [o.value, o.raw])), [flat]);
12814
12958
  const optionByValue = react.useMemo(
@@ -12820,12 +12964,26 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12820
12964
  const arr = Array.isArray(value) ? value : [value];
12821
12965
  return arr.map((v) => v != null && typeof v === "object" ? v.value : v).filter((v) => v != null && v !== "").map(String);
12822
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]);
12823
12977
  const selectedRef = react.useRef(selected);
12824
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("");
12825
12983
  const emit3 = (next) => {
12826
12984
  if (!onValueChange) return;
12827
12985
  const mapped = next.map(
12828
- (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 }
12829
12987
  );
12830
12988
  onValueChange(multi ? mapped : mapped[0] ?? (valueAsPrimitive ? "" : null));
12831
12989
  };
@@ -12874,14 +13032,15 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12874
13032
  resetValidation: () => setRuleError("")
12875
13033
  });
12876
13034
  const filtered = react.useMemo(() => {
12877
- if (!searchVisible || search.trim() === "") return flat;
13035
+ if (serverSearch || !searchVisible || search.trim() === "") return flat;
12878
13036
  const q = search.trim().toLowerCase();
12879
13037
  return flat.filter(
12880
13038
  (o) => o.isGroup || (isHtmlLabel(o.label) ? extractText(o.label) : o.label).toLowerCase().includes(q)
12881
13039
  );
12882
- }, [flat, search, searchVisible]);
13040
+ }, [flat, search, searchVisible, serverSearch]);
12883
13041
  const allValues = selectable.map((o) => o.value);
12884
- 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;
12885
13044
  const hasItems = filtered.some((o) => !o.isGroup);
12886
13045
  const groupLeafValues = react.useMemo(() => {
12887
13046
  const map = /* @__PURE__ */ new Map();
@@ -12892,7 +13051,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12892
13051
  }, [filtered]);
12893
13052
  const navigableItems = react.useMemo(() => {
12894
13053
  const items = [];
12895
- if (multi && showSelectAll && allValues.length > 0) {
13054
+ if (selectAllVisible) {
12896
13055
  items.push({ key: "__select_all__", isSelectAll: true });
12897
13056
  }
12898
13057
  filtered.forEach((o, i) => {
@@ -12907,20 +13066,129 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12907
13066
  }
12908
13067
  });
12909
13068
  return items;
12910
- }, [allValues.length, filtered, groupLeafValues, multi, showSelectAll]);
13069
+ }, [filtered, groupLeafValues, multi, selectAllVisible]);
12911
13070
  const selectedAnchorIndex = react.useMemo(() => {
12912
- if (selected.length === 0) return -1;
12913
- const selectedSet = new Set(selected);
13071
+ if (selectedSet.size === 0) return -1;
12914
13072
  return navigableItems.findIndex((item4) => item4.value != null && selectedSet.has(item4.value));
12915
- }, [navigableItems, selected]);
13073
+ }, [navigableItems, selectedSet]);
12916
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);
12917
13185
  const triggerText = (() => {
12918
13186
  if (selected.length === 0) return placeholder;
12919
13187
  if (multi) {
12920
13188
  if (allSelected) return allSelectedLabel;
12921
- 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(", ");
12922
13190
  }
12923
- return labelByValue.get(selected[0]) ?? selected[0];
13191
+ return labelByValue.get(selected[0]) ?? valueOptions.get(selected[0])?.label ?? selected[0];
12924
13192
  })();
12925
13193
  const isPlaceholder = selected.length === 0;
12926
13194
  const showClear = clearable && !effectiveDisabled && !isPlaceholder;
@@ -12933,7 +13201,6 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12933
13201
  const anchor = rangeAnchorRef.current;
12934
13202
  if (extend && anchor != null && anchor !== index && filtered[anchor]?.isGroup === false) {
12935
13203
  const [from, to] = anchor < index ? [anchor, index] : [index, anchor];
12936
- const selectedSet = new Set(selected);
12937
13204
  const shouldSelect = selectedSet.has(filtered[anchor].value);
12938
13205
  const rangeValues = filtered.slice(from, to + 1).filter((o) => !o.isGroup && !o.disabled).map((o) => o.value);
12939
13206
  const inRange = new Set(rangeValues);
@@ -12958,7 +13225,6 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12958
13225
  const handleToggleGroup = (leafValues) => {
12959
13226
  if (leafValues.length === 0) return;
12960
13227
  rangeAnchorRef.current = null;
12961
- const selectedSet = new Set(selected);
12962
13228
  const allSelectedInGroup = leafValues.every((v) => selectedSet.has(v));
12963
13229
  if (allSelectedInGroup) {
12964
13230
  const remove = new Set(leafValues);
@@ -12967,35 +13233,51 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12967
13233
  emit3([...selected, ...leafValues.filter((v) => !selectedSet.has(v))]);
12968
13234
  }
12969
13235
  };
13236
+ const hasNavigableItems = navigableItems.length > 0;
12970
13237
  react.useEffect(() => {
12971
- if (!open || navigableItems.length === 0) {
13238
+ if (!open || !hasNavigableItems) {
12972
13239
  setFocusedIndex(-1);
12973
13240
  return;
12974
13241
  }
12975
13242
  const anchor = alignSelectedTopRef.current ? selectedAnchorRef.current : -1;
12976
13243
  setFocusedIndex(anchor >= 0 ? anchor : 0);
12977
- }, [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]);
12978
13253
  react.useEffect(() => {
12979
13254
  rangeAnchorRef.current = null;
13255
+ lastReachEndCountRef.current = -1;
12980
13256
  }, [open, search]);
12981
13257
  react.useLayoutEffect(() => {
12982
13258
  if (!open || focusedIndex < 0 || !listEl) return;
12983
- const focusedEl = listEl.querySelector('[data-select-focused="true"]');
12984
- if (!focusedEl) return;
12985
- const listRect = listEl.getBoundingClientRect();
12986
- const itemRect = focusedEl.getBoundingClientRect();
12987
- 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) {
12988
13264
  alignSelectedTopRef.current = false;
12989
- const stickyOffset = searchBarRef.current?.offsetHeight ?? 0;
12990
- listEl.scrollTop += itemRect.top - listRect.top - stickyOffset;
13265
+ listEl.scrollTop = 0;
13266
+ syncScrollStateRef.current();
12991
13267
  return;
12992
13268
  }
12993
- if (itemRect.top < listRect.top) {
12994
- listEl.scrollTop += itemRect.top - listRect.top;
12995
- } else if (itemRect.bottom > listRect.bottom) {
12996
- listEl.scrollTop += itemRect.bottom - listRect.bottom;
12997
- }
12998
- }, [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]);
12999
13281
  const moveFocusedIndex = (direction) => {
13000
13282
  if (!open || navigableItems.length === 0) return;
13001
13283
  setFocusedIndex((current) => {
@@ -13185,6 +13467,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
13185
13467
  SCROLLBAR_TRACK_BORDER_CLASS
13186
13468
  ),
13187
13469
  style: { maxHeight: clampToAvailableHeight(dropdownHeight, POPOVER_AVAILABLE_HEIGHT) },
13470
+ onScroll: syncScrollState,
13188
13471
  onMouseLeave: () => setFocusedIndex(-1),
13189
13472
  children: [
13190
13473
  searchVisible && /* @__PURE__ */ jsxRuntime.jsx(
@@ -13208,12 +13491,16 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
13208
13491
  hasItems ? /* @__PURE__ */ jsxRuntime.jsx(
13209
13492
  ItemList,
13210
13493
  {
13211
- items: filtered,
13494
+ items: visibleItems,
13495
+ indexOffset: windowStart,
13496
+ topSpacer: win.top,
13497
+ bottomSpacer: win.bottom,
13498
+ setSize: isDepth ? void 0 : rowCount,
13212
13499
  multi,
13213
13500
  selected,
13214
13501
  groupLeafValues,
13215
13502
  focusedValue: navigableItems[focusedIndex]?.key,
13216
- showSelectAll: multi && showSelectAll && allValues.length > 0,
13503
+ showSelectAll: selectAllVisible,
13217
13504
  selectAllLabel: allSelectedLabel,
13218
13505
  selectAllSelected: allSelected,
13219
13506
  onSelectAll: handleSelectAll,
@@ -13228,7 +13515,18 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
13228
13515
  if (index >= 0) setFocusedIndex(index);
13229
13516
  }
13230
13517
  }
13231
- ) : /* @__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
+ )
13232
13530
  ]
13233
13531
  }
13234
13532
  )
@@ -13241,6 +13539,10 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
13241
13539
  var SSelect2 = /* @__PURE__ */ withDisplayName(SSelectImpl, "SSelect");
13242
13540
  function ItemList({
13243
13541
  items,
13542
+ indexOffset,
13543
+ topSpacer,
13544
+ bottomSpacer,
13545
+ setSize,
13244
13546
  multi,
13245
13547
  selected,
13246
13548
  groupLeafValues,
@@ -13265,6 +13567,7 @@ function ItemList({
13265
13567
  "button",
13266
13568
  {
13267
13569
  type: "button",
13570
+ "data-select-all-row": true,
13268
13571
  "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
13269
13572
  "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
13270
13573
  className: cn(
@@ -13281,7 +13584,9 @@ function ItemList({
13281
13584
  ]
13282
13585
  }
13283
13586
  ),
13284
- 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;
13285
13590
  const padLeft = o.depth === 1 ? 12 : o.depth === 2 ? 20 : 28;
13286
13591
  const rowKey = `${o.value}-${idx}`;
13287
13592
  if (o.isGroup) {
@@ -13298,6 +13603,8 @@ function ItemList({
13298
13603
  "button",
13299
13604
  {
13300
13605
  type: "button",
13606
+ "data-select-row": true,
13607
+ "data-index": idx,
13301
13608
  role: "option",
13302
13609
  "aria-selected": groupState === true,
13303
13610
  disabled: groupDisabled,
@@ -13348,6 +13655,8 @@ function ItemList({
13348
13655
  return /* @__PURE__ */ jsxRuntime.jsx(
13349
13656
  "div",
13350
13657
  {
13658
+ "data-select-row": true,
13659
+ "data-index": idx,
13351
13660
  className: cn(
13352
13661
  "select-none py-[4px] pr-[12px] text-[12px] leading-[20px] text-[color:var(--cmp-listBoxItem-content-default)]",
13353
13662
  groupTone,
@@ -13367,9 +13676,13 @@ function ItemList({
13367
13676
  "button",
13368
13677
  {
13369
13678
  type: "button",
13679
+ "data-select-row": true,
13680
+ "data-index": idx,
13370
13681
  disabled: o.disabled,
13371
13682
  role: "option",
13372
13683
  "aria-selected": isSelected,
13684
+ "aria-setsize": setSize,
13685
+ "aria-posinset": setSize != null ? idx + 1 : void 0,
13373
13686
  "data-select-focused": focusedValue === o.value ? "true" : void 0,
13374
13687
  "data-select-active": focusedValue === o.value ? "true" : void 0,
13375
13688
  className: cn(
@@ -13404,7 +13717,8 @@ function ItemList({
13404
13717
  },
13405
13718
  rowKey
13406
13719
  );
13407
- })
13720
+ }),
13721
+ bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: { height: bottomSpacer } })
13408
13722
  ] });
13409
13723
  }
13410
13724
  function CheckboxBox({
@@ -14565,12 +14879,29 @@ var SInput = /* @__PURE__ */ react.forwardRef(function SInput2({
14565
14879
  }
14566
14880
  );
14567
14881
  });
14568
- 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;
14569
14897
  var STextarea = /* @__PURE__ */ react.forwardRef(function STextarea2({
14570
14898
  value,
14571
14899
  onValueChange,
14572
14900
  onChange,
14573
- rows = 3,
14901
+ size = "sm",
14902
+ rows = MIN_ROWS,
14903
+ autogrow = false,
14904
+ maxRows,
14574
14905
  rules,
14575
14906
  status,
14576
14907
  focused,
@@ -14597,8 +14928,30 @@ var STextarea = /* @__PURE__ */ react.forwardRef(function STextarea2({
14597
14928
  style,
14598
14929
  ...rest
14599
14930
  }, ref) {
14600
- 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;
14601
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]);
14602
14955
  useFormField({
14603
14956
  name: rest.name,
14604
14957
  validate: () => {
@@ -14623,6 +14976,7 @@ var STextarea = /* @__PURE__ */ react.forwardRef(function STextarea2({
14623
14976
  addonLabel,
14624
14977
  addonAlign,
14625
14978
  hint,
14979
+ size,
14626
14980
  error: resolvedError,
14627
14981
  status: resolvedStatus,
14628
14982
  focused,
@@ -14637,7 +14991,7 @@ var STextarea = /* @__PURE__ */ react.forwardRef(function STextarea2({
14637
14991
  children: /* @__PURE__ */ jsxRuntime.jsx(
14638
14992
  "textarea",
14639
14993
  {
14640
- ref,
14994
+ ref: setRefs2,
14641
14995
  value: value ?? "",
14642
14996
  rows,
14643
14997
  disabled,
@@ -14653,15 +15007,21 @@ var STextarea = /* @__PURE__ */ react.forwardRef(function STextarea2({
14653
15007
  if (rules && rules.length > 0) setRuleError(runRules(e.target.value, rules));
14654
15008
  },
14655
15009
  className: cn(
14656
- "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",
14657
15013
  AUTOFILL_RESET_CLASS,
14658
15014
  textareaClassName
14659
15015
  ),
14660
15016
  style: {
14661
- minHeight: 100,
14662
- 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}`,
14663
15023
  fontSize: p.fontSize,
14664
- lineHeight: "20px",
15024
+ lineHeight: `${p.lineHeight}px`,
14665
15025
  color: disabled ? "var(--cmp-textinput-text-disabled)" : "var(--sys-color-field-text-default)",
14666
15026
  ...textareaStyle
14667
15027
  },
@@ -14671,7 +15031,7 @@ var STextarea = /* @__PURE__ */ react.forwardRef(function STextarea2({
14671
15031
  }
14672
15032
  );
14673
15033
  });
14674
- var PAD3 = {
15034
+ var PAD2 = {
14675
15035
  sm: { fontSize: 12, lineHeight: 20, stepper: 20, icon: 12 },
14676
15036
  md: { fontSize: 14, lineHeight: 24, stepper: 24, icon: 14 }
14677
15037
  };
@@ -14774,7 +15134,7 @@ var SNumberInput = /* @__PURE__ */ react.forwardRef(function SNumberInput2({
14774
15134
  className,
14775
15135
  style
14776
15136
  }, ref) {
14777
- const p = PAD3[size];
15137
+ const p = PAD2[size];
14778
15138
  const padX = `var(--cmp-numberInput-${size}-paddingX)`;
14779
15139
  const decimalLimit = getDecimalLimit(allowDecimal, maxDecimalLength);
14780
15140
  const numValue = applyDecimalLimit(toNumber(value), decimalLimit);