sellmate-design-system-react 9.0.0-beta.25 → 9.0.0-beta.27

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
@@ -10416,6 +10416,14 @@ var SStepper = /* @__PURE__ */ forwardRef(function SStepper2({
10416
10416
  }
10417
10417
  );
10418
10418
  });
10419
+
10420
+ // src/lib/floating-height.ts
10421
+ var POPOVER_AVAILABLE_HEIGHT = "var(--radix-popover-content-available-height)";
10422
+ function clampToAvailableHeight(value, availableHeight) {
10423
+ if (value == null || value === "") return availableHeight;
10424
+ const css = typeof value === "number" ? `${value}px` : value;
10425
+ return `min(${css}, ${availableHeight})`;
10426
+ }
10419
10427
  function runRules(value, rules) {
10420
10428
  if (!rules || rules.length === 0) return "";
10421
10429
  for (const rule of rules) {
@@ -10813,7 +10821,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
10813
10821
  error = false,
10814
10822
  rules,
10815
10823
  labelTooltipProps,
10816
- dropdownHeight = "260px",
10824
+ dropdownHeight = "640px",
10817
10825
  dropdownWidth,
10818
10826
  maxDropdownWidth = "640px",
10819
10827
  label,
@@ -11174,6 +11182,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
11174
11182
  placement: "bottom",
11175
11183
  align: "start",
11176
11184
  offset: 4,
11185
+ viewportPadding: 24,
11177
11186
  fit: !dropdownWidth,
11178
11187
  onOpenAutoFocus: (event) => {
11179
11188
  if (!showSearch) {
@@ -11206,7 +11215,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
11206
11215
  SCROLLBAR_TRACK_BG_CLASS,
11207
11216
  SCROLLBAR_TRACK_BORDER_CLASS
11208
11217
  ),
11209
- style: { maxHeight: dropdownHeight },
11218
+ style: { maxHeight: clampToAvailableHeight(dropdownHeight, POPOVER_AVAILABLE_HEIGHT) },
11210
11219
  onMouseLeave: () => setFocusedIndex(-1),
11211
11220
  children: [
11212
11221
  showSearch && /* @__PURE__ */ jsx(
@@ -11478,6 +11487,7 @@ var colWidthFromDef = (c) => {
11478
11487
  };
11479
11488
  var STable = /* @__PURE__ */ forwardRef(function STable2({
11480
11489
  columns = [],
11490
+ headerGroups,
11481
11491
  rows = [],
11482
11492
  rowKey = "id",
11483
11493
  selectable = false,
@@ -11525,6 +11535,49 @@ var STable = /* @__PURE__ */ forwardRef(function STable2({
11525
11535
  onDenseChange?.(next);
11526
11536
  };
11527
11537
  const visibleCols = useMemo(() => columns.filter((c) => c.visible !== false), [columns]);
11538
+ const headerGroupInfo = useMemo(() => {
11539
+ if (!headerGroups || headerGroups.length === 0) return null;
11540
+ const indexByName = new Map(visibleCols.map((col, index) => [col.name, index]));
11541
+ const covered = /* @__PURE__ */ new Set();
11542
+ let previousLast = -1;
11543
+ const cells = headerGroups.map((group) => {
11544
+ if (group.columnNames.length === 0) {
11545
+ throw new Error("[STable] headerGroups\uC758 columnNames\uB294 \uBE44\uC5B4 \uC788\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
11546
+ }
11547
+ const indexes = group.columnNames.map((name) => {
11548
+ const index = indexByName.get(name);
11549
+ if (index == null) {
11550
+ throw new Error(`[STable] headerGroups\uC5D0 \uC874\uC7AC\uD558\uC9C0 \uC54A\uB294 \uCEEC\uB7FC\uC774 \uC9C0\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${name}`);
11551
+ }
11552
+ if (covered.has(name)) {
11553
+ throw new Error(`[STable] headerGroups\uC5D0\uC11C \uCEEC\uB7FC\uC774 \uC911\uBCF5 \uC9C0\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${name}`);
11554
+ }
11555
+ covered.add(name);
11556
+ return index;
11557
+ });
11558
+ const first = indexes[0];
11559
+ const last = indexes[indexes.length - 1];
11560
+ if (last - first + 1 !== indexes.length || indexes.some((index, i) => index !== first + i)) {
11561
+ throw new Error(
11562
+ "[STable] headerGroups\uC758 columnNames\uB294 \uC778\uC811\uD55C \uCEEC\uB7FC \uC21C\uC11C\uB85C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4."
11563
+ );
11564
+ }
11565
+ if (first <= previousLast) {
11566
+ throw new Error("[STable] headerGroups\uB294 \uCEEC\uB7FC \uC21C\uC11C\uB300\uB85C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.");
11567
+ }
11568
+ previousLast = last;
11569
+ return { ...group, firstIndex: first, colSpan: indexes.length };
11570
+ });
11571
+ if (covered.size !== visibleCols.length) {
11572
+ throw new Error("[STable] headerGroups\uC758 columnNames\uAC00 \uBAA8\uB4E0 visible \uCEEC\uB7FC\uC744 \uD3EC\uD568\uD574\uC57C \uD569\uB2C8\uB2E4.");
11573
+ }
11574
+ return {
11575
+ cells,
11576
+ rowSpanNames: new Set(
11577
+ cells.filter((cell) => cell.rowSpan === 2).flatMap((cell) => cell.columnNames)
11578
+ )
11579
+ };
11580
+ }, [headerGroups, visibleCols]);
11528
11581
  const stickyLeft = stickyColumn?.left ?? 0;
11529
11582
  const stickyRight = stickyColumn?.right ?? 0;
11530
11583
  const effectiveRowHeight = rowHeight ?? (innerDense ? DENSE_ROW_HEIGHT : DEFAULT_ROW_HEIGHT);
@@ -11787,6 +11840,35 @@ var STable = /* @__PURE__ */ forwardRef(function STable2({
11787
11840
  isRightEdge && scrolledRight && "shadow-[-5px_0_8px_0_rgba(34,34,34,0.1)]"
11788
11841
  );
11789
11842
  };
11843
+ const headerResizeHitArea = (i, key) => {
11844
+ const col = visibleCols[i];
11845
+ if (!headerGroupInfo || !resizable || !col || col.autoWidth || i >= visibleCols.length - 1) {
11846
+ return null;
11847
+ }
11848
+ return /* @__PURE__ */ jsx(
11849
+ "span",
11850
+ {
11851
+ onMouseDown: (e) => startResize(i, e),
11852
+ className: "absolute inset-y-0 right-0 z-[5] w-[9px] cursor-col-resize",
11853
+ "aria-hidden": true
11854
+ },
11855
+ key
11856
+ );
11857
+ };
11858
+ const headerResizer = (i, key) => {
11859
+ const col = visibleCols[i];
11860
+ if (!resizable || !col || col.autoWidth || i >= visibleCols.length - 1) return null;
11861
+ return /* @__PURE__ */ jsx(
11862
+ "span",
11863
+ {
11864
+ onMouseDown: (e) => startResize(i, e),
11865
+ className: "absolute right-0 top-1/2 z-[5] flex h-[16px] w-[9px] -translate-y-1/2 cursor-col-resize items-center justify-end",
11866
+ "aria-hidden": true,
11867
+ children: /* @__PURE__ */ jsx("span", { className: "h-full w-[4px] border-x border-solid border-[color:var(--cmp-table-header-resizingBar-color)]" })
11868
+ },
11869
+ key
11870
+ );
11871
+ };
11790
11872
  const bodyScrollClass = cn(
11791
11873
  "min-h-0 flex-1 overflow-auto border-t border-solid border-[color:var(--cmp-table-border-color)]",
11792
11874
  SCROLLBAR_CLASS,
@@ -11851,145 +11933,179 @@ var STable = /* @__PURE__ */ forwardRef(function STable2({
11851
11933
  style: { fontSize: 12 },
11852
11934
  children: [
11853
11935
  colgroup,
11854
- /* @__PURE__ */ jsx("thead", { className: cn(isNoData && "opacity-40"), children: /* @__PURE__ */ jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
11855
- selectable && /* @__PURE__ */ jsxs(
11856
- "th",
11857
- {
11858
- className: cn(
11859
- // 원본 .th--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
11860
- // 하단 border는 헤더 래퍼가 연속으로 그리므로 셀에는 두지 않는다.
11861
- "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
11862
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
11863
- ),
11864
- style: {
11865
- width: SELECTABLE_COLUMN_WIDTH,
11866
- minWidth: SELECTABLE_COLUMN_WIDTH,
11867
- maxWidth: SELECTABLE_COLUMN_WIDTH
11936
+ /* @__PURE__ */ jsxs("thead", { className: cn(isNoData && "opacity-40"), children: [
11937
+ headerGroupInfo && /* @__PURE__ */ jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
11938
+ selectable && /* @__PURE__ */ jsx(
11939
+ "th",
11940
+ {
11941
+ rowSpan: 2,
11942
+ className: cn(
11943
+ "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
11944
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
11945
+ ),
11946
+ style: {
11947
+ width: SELECTABLE_COLUMN_WIDTH,
11948
+ minWidth: SELECTABLE_COLUMN_WIDTH,
11949
+ maxWidth: SELECTABLE_COLUMN_WIDTH
11950
+ },
11951
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(SCheckbox, { value: headerCheck, onValueChange: toggleAll }) })
11952
+ }
11953
+ ),
11954
+ headerGroupInfo.cells.map((group) => /* @__PURE__ */ jsxs(
11955
+ "th",
11956
+ {
11957
+ colSpan: group.colSpan,
11958
+ rowSpan: group.rowSpan,
11959
+ className: cn(
11960
+ "relative border-b border-r border-solid border-[color:var(--cmp-table-border-color)] bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[20px] text-[color:var(--sys-color-fg-primary)]",
11961
+ group.firstIndex + group.colSpan === visibleCols.length && "border-r-0",
11962
+ group.rowSpan === 2 && "border-b-0",
11963
+ group.className
11964
+ ),
11965
+ style: { textAlign: group.align ?? "left", ...group.style },
11966
+ children: [
11967
+ group.render ? group.render() : group.label,
11968
+ group.rowSpan === 2 && group.colSpan === 1 ? headerResizeHitArea(
11969
+ group.firstIndex,
11970
+ `group-resizer-${group.columnNames[0]}`
11971
+ ) : null
11972
+ ]
11868
11973
  },
11869
- children: [
11870
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(SCheckbox, { value: headerCheck, onValueChange: toggleAll }) }),
11871
- /* @__PURE__ */ jsx(
11872
- "span",
11873
- {
11874
- "aria-hidden": true,
11875
- className: "pointer-events-none absolute right-0 top-1/2 h-[16px] w-px -translate-y-1/2 bg-[var(--cmp-table-header-resizingBar-color)]"
11876
- }
11877
- )
11878
- ]
11879
- }
11880
- ),
11881
- visibleCols.map((col, i) => {
11882
- const isSortable = !!col.sortable && !col.renderHeader;
11883
- const sortDir = isSortable && sort?.name === col.name ? sort.dir : void 0;
11884
- return /* @__PURE__ */ jsxs(
11974
+ group.columnNames.join("|")
11975
+ ))
11976
+ ] }),
11977
+ /* @__PURE__ */ jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
11978
+ selectable && !headerGroupInfo && /* @__PURE__ */ jsxs(
11885
11979
  "th",
11886
11980
  {
11887
11981
  className: cn(
11888
- // 하단 border는 헤더 래퍼가 연속으로 그린다 (거터 영역까지 이어짐).
11889
- // leading-[0]: th 의 스트럿(line-height 만큼의 빈 인라인 박스)을 없앤다.
11890
- // 내용이 inline-flex 라 스트럿이 남아 있으면 라벨보다 큰 컨트롤(정렬 버튼 등)이
11891
- // 들어올 때 줄 상자가 베이스라인 기준으로 늘어나 헤더 높이를 넘기고
11892
- // 세로 가운데도 어긋난다. 글자 줄높이는 아래 th__content 가 다시 준다.
11893
- "relative bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[0] text-[color:var(--sys-color-fg-primary)]",
11894
- stickyInfo(i).isSticky && "z-[6]",
11895
- stickyShadowClass(i),
11896
- col.thClass
11982
+ // 원본 .th--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
11983
+ // 하단 border는 헤더 래퍼가 연속으로 그리므로 셀에는 두지 않는다.
11984
+ "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
11985
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
11897
11986
  ),
11898
- style: { textAlign: col.align ?? "left", ...cellStyle(i), ...col.thStyle },
11899
- "aria-sort": isSortable ? sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none" : void 0,
11987
+ style: {
11988
+ width: SELECTABLE_COLUMN_WIDTH,
11989
+ minWidth: SELECTABLE_COLUMN_WIDTH,
11990
+ maxWidth: SELECTABLE_COLUMN_WIDTH
11991
+ },
11900
11992
  children: [
11993
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(SCheckbox, { value: headerCheck, onValueChange: toggleAll }) }),
11901
11994
  /* @__PURE__ */ jsx(
11902
11995
  "span",
11903
11996
  {
11904
- className: cn(
11905
- // 원본 th__content: align에 따라 justify-content 적용 (th도 정렬 반영)
11906
- // th 가 leading-[0] 이므로 글자 줄높이는 여기서 준다 (truncate 가
11907
- // overflow-hidden 이라 줄높이가 0이면 라벨이 잘려 사라진다).
11908
- "inline-flex w-full min-w-0 items-center gap-[4px] leading-[20px]",
11909
- col.align === "center" && "justify-center",
11910
- col.align === "right" && "justify-end"
11911
- ),
11912
- children: col.renderHeader ? col.renderHeader(col, i) : /* @__PURE__ */ jsxs(Fragment, { children: [
11913
- /* @__PURE__ */ jsx("span", { className: "truncate", children: col.label }),
11914
- col.helpText && col.helpText.length > 0 && // 라벨이 좁아져 truncate 되더라도 아이콘까지 함께 줄어들면 안 되므로
11915
- // shrink-0 래퍼로 감싼다 (STooltip 의 className 은 본문 쪽으로 간다).
11916
- // 아이콘·크기·색은 SKeyValueTable 헤더 도움말과 동일하게 맞춘다.
11917
- /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx(
11918
- STooltip2,
11919
- {
11920
- trigger: "hover",
11921
- icon: "helpOutline",
11922
- iconSize: 12,
11923
- color: "var(--sys-color-fg-success)",
11924
- placement: "top",
11925
- message: col.helpText
11926
- }
11927
- ) }),
11928
- col.editable && /* @__PURE__ */ jsx(
11929
- SIcon,
11930
- {
11931
- name: "pageEdit",
11932
- size: 12,
11933
- color: "var(--sys-color-link-accent)",
11934
- label: "\uC218\uC815 \uAC00\uB2A5",
11935
- className: "shrink-0"
11936
- }
11937
- ),
11938
- col.navigable && /* @__PURE__ */ jsx(
11939
- SIcon,
11940
- {
11941
- name: "pageMove",
11942
- size: 12,
11943
- color: "var(--sys-color-link-accent)",
11944
- label: "\uB2E4\uB978 \uD654\uBA74\uC73C\uB85C \uC774\uB3D9",
11945
- className: "shrink-0"
11946
- }
11947
- ),
11948
- col.required && // 라벨이 truncate 돼도 필수 표시는 남아야 하므로 shrink-0.
11949
- // 아이콘·크기·색은 SKeyValueTable 헤더 필수 표시와 동일하게 맞춘다.
11950
- /* @__PURE__ */ jsx(
11951
- SIcon,
11952
- {
11953
- name: "asterisk",
11954
- size: 12,
11955
- color: "var(--sys-color-fg-accent)",
11956
- className: "shrink-0"
11957
- }
11958
- ),
11959
- isSortable && // 미정렬은 updown 을 default(회색)로 — 정렬할 수 있다는 것 자체가
11960
- // 헤더에 보여야 한다. 정렬 중이면 action 색으로 올려 활성임을 알린다.
11961
- // 라벨이 truncate 돼도 버튼은 남도록 shrink-0.
11962
- /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx(
11963
- SGhostButton,
11964
- {
11965
- size: "xxs",
11966
- intent: sortDir ? "action" : "default",
11967
- icon: sortDir === "asc" ? "arrowUp" : sortDir === "desc" ? "arrowDown" : "updown",
11968
- ariaLabel: `${col.label} \uC815\uB82C`,
11969
- onClick: () => toggleSort(col)
11970
- }
11971
- ) })
11972
- ] })
11973
- }
11974
- ),
11975
- resizable && !col.autoWidth && i < visibleCols.length - 1 && // 원본 th__resizer: 셀 우측 경계 안쪽 4px 그립(좌우 1px #CCCCCC 라인, 세로 16px).
11976
- // 셀 밖으로 넘치면 다음 셀에 가려져 클릭이 안 먹으므로 translate 없이 셀 내부에 배치.
11977
- // 겉보기 그립은 4px(원본과 동일), 클릭 영역은 9px로 넓혀 사용성 확보.
11978
- /* @__PURE__ */ jsx(
11979
- "span",
11980
- {
11981
- onMouseDown: (e) => startResize(i, e),
11982
- className: "absolute right-0 top-1/2 z-[5] flex h-[16px] w-[9px] -translate-y-1/2 cursor-col-resize items-center justify-end",
11983
11997
  "aria-hidden": true,
11984
- children: /* @__PURE__ */ jsx("span", { className: "h-full w-[4px] border-x border-solid border-[color:var(--cmp-table-header-resizingBar-color)]" })
11998
+ className: "pointer-events-none absolute right-0 top-1/2 h-[16px] w-px -translate-y-1/2 bg-[var(--cmp-table-header-resizingBar-color)]"
11985
11999
  }
11986
12000
  )
11987
12001
  ]
11988
- },
11989
- col.name
11990
- );
11991
- })
11992
- ] }) })
12002
+ }
12003
+ ),
12004
+ visibleCols.map((col, i) => {
12005
+ if (headerGroupInfo?.rowSpanNames.has(col.name)) return null;
12006
+ const isSortable = !!col.sortable && !col.renderHeader;
12007
+ const sortDir = isSortable && sort?.name === col.name ? sort.dir : void 0;
12008
+ return /* @__PURE__ */ jsxs(
12009
+ "th",
12010
+ {
12011
+ className: cn(
12012
+ // 하단 border는 헤더 래퍼가 연속으로 그린다 (거터 영역까지 이어짐).
12013
+ // leading-[0]: th 의 스트럿(line-height 만큼의 빈 인라인 박스)을 없앤다.
12014
+ // 내용이 inline-flex 라 스트럿이 남아 있으면 라벨보다 큰 컨트롤(정렬 버튼 등)이
12015
+ // 들어올 때 줄 상자가 베이스라인 기준으로 늘어나 헤더 높이를 넘기고
12016
+ // 세로 가운데도 어긋난다. 글자 줄높이는 아래 th__content 가 다시 준다.
12017
+ headerGroupInfo && "border-r border-solid border-[color:var(--cmp-table-border-color)]",
12018
+ headerGroupInfo && i === visibleCols.length - 1 && "border-r-0",
12019
+ "relative bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[0] text-[color:var(--sys-color-fg-primary)]",
12020
+ stickyInfo(i).isSticky && "z-[6]",
12021
+ stickyShadowClass(i),
12022
+ col.thClass
12023
+ ),
12024
+ style: { textAlign: col.align ?? "left", ...cellStyle(i), ...col.thStyle },
12025
+ "aria-sort": isSortable ? sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none" : void 0,
12026
+ children: [
12027
+ /* @__PURE__ */ jsx(
12028
+ "span",
12029
+ {
12030
+ className: cn(
12031
+ // 원본 th__content: align에 따라 justify-content 적용 (th도 정렬 반영)
12032
+ // th 가 leading-[0] 이므로 글자 줄높이는 여기서 준다 (truncate 가
12033
+ // overflow-hidden 이라 줄높이가 0이면 라벨이 잘려 사라진다).
12034
+ "inline-flex w-full min-w-0 items-center gap-[4px] leading-[20px]",
12035
+ col.align === "center" && "justify-center",
12036
+ col.align === "right" && "justify-end"
12037
+ ),
12038
+ children: col.renderHeader ? col.renderHeader(col, i) : /* @__PURE__ */ jsxs(Fragment, { children: [
12039
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: col.label }),
12040
+ col.helpText && col.helpText.length > 0 && // 라벨이 좁아져 truncate 되더라도 아이콘까지 함께 줄어들면 안 되므로
12041
+ // shrink-0 래퍼로 감싼다 (STooltip 의 className 은 본문 쪽으로 간다).
12042
+ // 아이콘·크기·색은 SKeyValueTable 헤더 도움말과 동일하게 맞춘다.
12043
+ /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx(
12044
+ STooltip2,
12045
+ {
12046
+ trigger: "hover",
12047
+ icon: "helpOutline",
12048
+ iconSize: 12,
12049
+ color: "var(--sys-color-fg-success)",
12050
+ placement: "top",
12051
+ message: col.helpText
12052
+ }
12053
+ ) }),
12054
+ col.editable && /* @__PURE__ */ jsx(
12055
+ SIcon,
12056
+ {
12057
+ name: "pageEdit",
12058
+ size: 12,
12059
+ color: "var(--sys-color-link-accent)",
12060
+ label: "\uC218\uC815 \uAC00\uB2A5",
12061
+ className: "shrink-0"
12062
+ }
12063
+ ),
12064
+ col.navigable && /* @__PURE__ */ jsx(
12065
+ SIcon,
12066
+ {
12067
+ name: "pageMove",
12068
+ size: 12,
12069
+ color: "var(--sys-color-link-accent)",
12070
+ label: "\uB2E4\uB978 \uD654\uBA74\uC73C\uB85C \uC774\uB3D9",
12071
+ className: "shrink-0"
12072
+ }
12073
+ ),
12074
+ col.required && // 라벨이 truncate 돼도 필수 표시는 남아야 하므로 shrink-0.
12075
+ // 아이콘·크기·색은 SKeyValueTable 헤더 필수 표시와 동일하게 맞춘다.
12076
+ /* @__PURE__ */ jsx(
12077
+ SIcon,
12078
+ {
12079
+ name: "asterisk",
12080
+ size: 12,
12081
+ color: "var(--sys-color-fg-accent)",
12082
+ className: "shrink-0"
12083
+ }
12084
+ ),
12085
+ isSortable && // 미정렬은 updown 을 default(회색)로 — 정렬할 수 있다는 것 자체가
12086
+ // 헤더에 보여야 한다. 정렬 중이면 action 색으로 올려 활성임을 알린다.
12087
+ // 라벨이 truncate 돼도 버튼은 남도록 shrink-0.
12088
+ /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsx(
12089
+ SGhostButton,
12090
+ {
12091
+ size: "xxs",
12092
+ intent: sortDir ? "action" : "default",
12093
+ icon: sortDir === "asc" ? "arrowUp" : sortDir === "desc" ? "arrowDown" : "updown",
12094
+ ariaLabel: `${col.label} \uC815\uB82C`,
12095
+ onClick: () => toggleSort(col)
12096
+ }
12097
+ ) })
12098
+ ] })
12099
+ }
12100
+ ),
12101
+ headerGroupInfo ? headerResizeHitArea(i, `resizer-${col.name}`) : headerResizer(i, `resizer-${col.name}`)
12102
+ ]
12103
+ },
12104
+ col.name
12105
+ );
12106
+ })
12107
+ ] })
12108
+ ] })
11993
12109
  ]
11994
12110
  }
11995
12111
  )
@@ -17252,6 +17368,8 @@ var SEditor = /* @__PURE__ */ forwardRef(function SEditor2({
17252
17368
  });
17253
17369
  var CHIPFILTER_LISTBOX_WIDTH = 240;
17254
17370
  var CHIPFILTER_DATE_PRESET_LISTBOX_WIDTH = 246;
17371
+ var EDITOR_LISTBOX_MAX_HEIGHT = "min(640px, var(--radix-popover-content-available-height))";
17372
+ var EDITOR_POPOVER_VIEWPORT_PADDING = 24;
17255
17373
  var CHIPFILTER = {
17256
17374
  barBg: "var(--cmp-chipFilter-bar-bg)",
17257
17375
  barBorder: "var(--cmp-chipFilter-bar-border)",
@@ -17291,6 +17409,7 @@ var CHIPFILTER = {
17291
17409
  itemTextDefault: "var(--cmp-chipFilter-item-text-default)",
17292
17410
  itemTextSelected: "var(--cmp-chipFilter-item-text-selected)"
17293
17411
  };
17412
+ var DEFAULT_CUSTOM_MAX_RANGE = 90;
17294
17413
  var DEFAULT_PERIOD_PRESETS = [
17295
17414
  { value: "day", label: "\uC77C\uBCC4" },
17296
17415
  { value: "month", label: "\uC6D4\uBCC4" },
@@ -17299,6 +17418,20 @@ var DEFAULT_PERIOD_PRESETS = [
17299
17418
  { value: "year", label: "\uC5F0\uB3C4\uBCC4" },
17300
17419
  { value: "custom", label: "\uC0AC\uC6A9\uC790 \uC9C0\uC815", custom: true }
17301
17420
  ];
17421
+ var DEFAULT_DATE_RANGE_PRESETS = [
17422
+ { value: "today", label: "\uC624\uB298", resolve: () => todayIso() },
17423
+ {
17424
+ value: "last7",
17425
+ label: "\uC9C0\uB09C 7\uC77C",
17426
+ resolve: () => [addDaysIso(todayIso(), -7), todayIso()]
17427
+ },
17428
+ {
17429
+ value: "last30",
17430
+ label: "\uC9C0\uB09C 30\uC77C",
17431
+ resolve: () => [addDaysIso(todayIso(), -30), todayIso()]
17432
+ },
17433
+ { value: "custom", label: "\uC0AC\uC6A9\uC790 \uC9C0\uC815", custom: true }
17434
+ ];
17302
17435
  var isKeywordValue = (value) => typeof value === "object" && value != null && !Array.isArray(value) && "keywords" in value;
17303
17436
  var isPeriodValue = (value) => typeof value === "object" && value != null && !Array.isArray(value) && "unit" in value;
17304
17437
  var keywordEntries = (value) => {
@@ -17323,23 +17456,39 @@ var todayIso = () => {
17323
17456
  const pad = (n) => String(n).padStart(2, "0");
17324
17457
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
17325
17458
  };
17459
+ var addDaysIso = (iso, n) => {
17460
+ const d = /* @__PURE__ */ new Date(`${iso}T00:00:00`);
17461
+ d.setDate(d.getDate() + n);
17462
+ const pad = (v) => String(v).padStart(2, "0");
17463
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
17464
+ };
17326
17465
  var isInlineDateField = (field) => field.type === "datepicker-range" && field.radioButton === true && (field.presets?.length ?? 0) > 0;
17327
17466
  var getFieldPresets = (field) => {
17328
17467
  if (field.type === "datepicker-statistics") return field.presets ?? DEFAULT_PERIOD_PRESETS;
17329
- return field.presets ?? [];
17468
+ return field.presets ?? DEFAULT_DATE_RANGE_PRESETS;
17469
+ };
17470
+ var getEffectiveMaxRange = (field) => {
17471
+ if (field.maxRange !== void 0) return field.maxRange;
17472
+ const hasCustomPreset = getFieldPresets(field).some((preset) => preset.custom);
17473
+ return hasCustomPreset ? DEFAULT_CUSTOM_MAX_RANGE : void 0;
17330
17474
  };
17331
- var getDefaultPeriodInputValue = (unit) => {
17475
+ var getPresetLabel = (preset, maxRange) => {
17476
+ if (!preset.custom || !maxRange || preset.label.includes("\uCD5C\uB300")) return preset.label;
17477
+ return `${preset.label} (\uCD5C\uB300 ${maxRange}\uC77C\uC529)`;
17478
+ };
17479
+ var getDefaultPeriodInputValue = (unit, maxRange) => {
17332
17480
  const today = todayIso();
17333
17481
  if (unit === "day") return today;
17334
17482
  if (unit === "month") return today.slice(0, 7);
17335
17483
  if (unit === "quarter") return 1;
17336
17484
  if (unit === "half") return "first";
17337
17485
  if (unit === "year") return today.slice(0, 4);
17338
- return [today, today];
17486
+ const days = maxRange && maxRange > 0 ? maxRange : DEFAULT_CUSTOM_MAX_RANGE;
17487
+ return [addDaysIso(today, -days), today];
17339
17488
  };
17340
- var createPeriodValue = (unit) => ({
17489
+ var createPeriodValue = (unit, maxRange) => ({
17341
17490
  unit,
17342
- value: getDefaultPeriodInputValue(unit)
17491
+ value: getDefaultPeriodInputValue(unit, maxRange)
17343
17492
  });
17344
17493
  var resolveFieldDefault = (field) => {
17345
17494
  if (field.defaultValue !== void 0 && isFixedField(field)) return field.defaultValue;
@@ -17352,7 +17501,8 @@ var resolveFieldDefault = (field) => {
17352
17501
  if (field.type === "datepicker-day") return todayIso();
17353
17502
  if (field.type === "select") return field.options?.[0]?.value;
17354
17503
  if (field.type === "datepicker-statistics") {
17355
- const unit = getFieldPresets(field).find((preset) => !preset.custom)?.value;
17504
+ const presets = getFieldPresets(field).filter((preset) => !preset.custom);
17505
+ const unit = presets.some((preset) => preset.value === "day") ? "day" : presets[0]?.value;
17356
17506
  return unit ? createPeriodValue(unit) : void 0;
17357
17507
  }
17358
17508
  return void 0;
@@ -17482,8 +17632,7 @@ var SChipFilter = /* @__PURE__ */ forwardRef(function SChipFilter2({
17482
17632
  onActiveKeysChange,
17483
17633
  onReset,
17484
17634
  onAddFilter,
17485
- label = "\uAC80\uC0C9 \uD544\uD130",
17486
- showLabel = false,
17635
+ label,
17487
17636
  showReset = true,
17488
17637
  disabled = false,
17489
17638
  className,
@@ -17500,6 +17649,7 @@ var SChipFilter = /* @__PURE__ */ forwardRef(function SChipFilter2({
17500
17649
  const pendingOpenKeyRef = useRef(null);
17501
17650
  const triggerRefs = useRef(/* @__PURE__ */ new Map());
17502
17651
  const initializedDefaultsRef = useRef(/* @__PURE__ */ new Set());
17652
+ const misuseWarningSignatureRef = useRef(null);
17503
17653
  const prevOpenKeyRef = useRef(null);
17504
17654
  const valuesRef = useRef(values);
17505
17655
  valuesRef.current = values;
@@ -17515,14 +17665,19 @@ var SChipFilter = /* @__PURE__ */ forwardRef(function SChipFilter2({
17515
17665
  (current) => groups.every((group) => isGroupSatisfied(group, current)),
17516
17666
  [groups]
17517
17667
  );
17668
+ const isRequiredSatisfied = useCallback(
17669
+ (current) => fields.every((field) => !field.required || !isEmptyValue(current[field.key])),
17670
+ [fields]
17671
+ );
17518
17672
  const runSearch = useCallback(
17519
17673
  (next) => {
17520
17674
  if (valuesMapEqual(lastSearchedValuesRef.current, next)) return;
17521
17675
  if (!isGroupsValid(next)) return;
17676
+ if (!isRequiredSatisfied(next)) return;
17522
17677
  lastSearchedValuesRef.current = next;
17523
17678
  onSearch?.(next);
17524
17679
  },
17525
- [isGroupsValid, onSearch]
17680
+ [isGroupsValid, isRequiredSatisfied, onSearch]
17526
17681
  );
17527
17682
  useEffect(() => {
17528
17683
  const prevKey = prevOpenKeyRef.current;
@@ -17557,14 +17712,24 @@ var SChipFilter = /* @__PURE__ */ forwardRef(function SChipFilter2({
17557
17712
  });
17558
17713
  if (changed) onValueChange?.({ ...values, ...patch });
17559
17714
  }, [fields, values, onValueChange]);
17560
- if (typeof console !== "undefined") {
17561
- const misused = fields.filter((f) => f.defaultValue !== void 0 && !isFixedField(f));
17562
- if (misused.length > 0) {
17715
+ useEffect(() => {
17716
+ if (typeof console === "undefined") return;
17717
+ const misusedKeys = fields.filter((f) => f.defaultValue !== void 0 && !isFixedField(f)).map((f) => f.key);
17718
+ const requiredWithoutDefaultKeys = fields.filter((f) => f.required === true && resolveFieldDefault(f) === void 0).map((f) => f.key);
17719
+ const signature = JSON.stringify([misusedKeys, requiredWithoutDefaultKeys]);
17720
+ if (signature === misuseWarningSignatureRef.current) return;
17721
+ misuseWarningSignatureRef.current = signature;
17722
+ if (misusedKeys.length > 0) {
17563
17723
  console.warn(
17564
- `[SChipFilter] defaultValue \uB294 fixed\xB7required \uD544\uD130\uC5D0\uC11C\uB9CC \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uBB34\uC2DC\uD569\uB2C8\uB2E4. (${misused.map((f) => f.key).join(", ")})`
17724
+ `[SChipFilter] defaultValue \uB294 fixed\xB7required \uD544\uD130\uC5D0\uC11C\uB9CC \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uBB34\uC2DC\uD569\uB2C8\uB2E4. (${misusedKeys.join(", ")})`
17565
17725
  );
17566
17726
  }
17567
- }
17727
+ if (requiredWithoutDefaultKeys.length > 0) {
17728
+ console.warn(
17729
+ `[SChipFilter] required \uD544\uD130\uC5D0 defaultValue \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uC774 \uD0C0\uC785\uC740 \uB0B4\uC7A5 \uAE30\uBCF8\uAC12\uC774 \uC5C6\uC5B4 "\uAC80\uC0C9 \uCD08\uAE30\uD654" \uC2DC \uAC12\uC774 \uBE44\uC6CC\uC9C0\uACE0 \uAC80\uC0C9\uB3C4 \uB9C9\uD799\uB2C8\uB2E4. defaultValue \uB97C \uC9C0\uC815\uD558\uC138\uC694. (${requiredWithoutDefaultKeys.join(", ")})`
17730
+ );
17731
+ }
17732
+ }, [fields]);
17568
17733
  const fixedKeys = fields.filter(isFixedField).map((field) => field.key);
17569
17734
  const resolvedActiveKeys = Array.from(
17570
17735
  /* @__PURE__ */ new Set([...activeKeys ?? internalActiveKeys, ...fixedKeys])
@@ -17632,7 +17797,10 @@ var SChipFilter = /* @__PURE__ */ forwardRef(function SChipFilter2({
17632
17797
  },
17633
17798
  [fields]
17634
17799
  );
17635
- const validate = useCallback(() => invalidGroups.size === 0, [invalidGroups]);
17800
+ const validate = useCallback(
17801
+ () => invalidGroups.size === 0 && isRequiredSatisfied(values),
17802
+ [invalidGroups, isRequiredSatisfied, values]
17803
+ );
17636
17804
  useImperativeHandle(ref, () => ({ open, reset, validate }), [open, reset, validate]);
17637
17805
  const addField = (key) => {
17638
17806
  const next = [...resolvedActiveKeys, key];
@@ -17669,7 +17837,7 @@ var SChipFilter = /* @__PURE__ */ forwardRef(function SChipFilter2({
17669
17837
  ),
17670
17838
  style: { ...rootStyle, borderColor: "var(--chipfilter-bar-border)" },
17671
17839
  children: [
17672
- showLabel && /* @__PURE__ */ jsxs(Fragment, { children: [
17840
+ label && /* @__PURE__ */ jsxs(Fragment, { children: [
17673
17841
  /* @__PURE__ */ jsx(STag, { className: "shrink-0", shape: "square", size: "sm", color: "darkblue", label }),
17674
17842
  /* @__PURE__ */ jsx(Divider, { top: true })
17675
17843
  ] }),
@@ -17869,6 +18037,7 @@ var SChipFilter = /* @__PURE__ */ forwardRef(function SChipFilter2({
17869
18037
  side: "bottom",
17870
18038
  align: "start",
17871
18039
  sideOffset: 4,
18040
+ collisionPadding: EDITOR_POPOVER_VIEWPORT_PADDING,
17872
18041
  onOpenAutoFocus: (event) => event.preventDefault(),
17873
18042
  className: cn("z-50", FLOATING_SLIDE_ANIM),
17874
18043
  style: editorPopoverStyle,
@@ -18034,6 +18203,7 @@ function EditorPanel({
18034
18203
  onClose
18035
18204
  }) {
18036
18205
  const wrapsInPaddedTop = field.type === "datepicker-day" || field.type === "datepicker-range" || field.type === "datepicker-statistics";
18206
+ const isScrollableListField = field.type === "select" || field.type === "select-multi" || field.type === "keyword";
18037
18207
  const isMulti = field.type === "select-multi";
18038
18208
  const selectableValues = isMulti ? (field.options ?? []).filter((option) => !option.disabled).map((option) => option.value) : [];
18039
18209
  const selectedValues = isMulti ? toArrayValue(value) : [];
@@ -18043,7 +18213,7 @@ function EditorPanel({
18043
18213
  allSelected ? selectedValues.filter((v) => !selectableValues.includes(v)) : [...selectedValues, ...selectableValues.filter((v) => !selectedValues.includes(v))]
18044
18214
  );
18045
18215
  };
18046
- return /* @__PURE__ */ jsxs(ListBox, { children: [
18216
+ return /* @__PURE__ */ jsxs(ListBox, { maxHeight: isScrollableListField ? EDITOR_LISTBOX_MAX_HEIGHT : void 0, children: [
18047
18217
  wrapsInPaddedTop ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-[var(--cmp-chipFilter-listbox-top-gap)] px-[var(--cmp-chipFilter-listbox-top-paddingX)] py-[var(--cmp-chipFilter-listbox-top-paddingY)]", children: /* @__PURE__ */ jsx(
18048
18218
  FilterEditor,
18049
18219
  {
@@ -18066,7 +18236,7 @@ function EditorPanel({
18066
18236
  /* @__PURE__ */ jsxs(
18067
18237
  "div",
18068
18238
  {
18069
- className: "flex items-center justify-end gap-[var(--cmp-chipFilter-listbox-bottom-gap)] border-t border-solid px-[var(--cmp-chipFilter-listbox-bottom-paddingX)] py-[var(--cmp-chipFilter-listbox-bottom-paddingY)]",
18239
+ className: "flex shrink-0 items-center justify-end gap-[var(--cmp-chipFilter-listbox-bottom-gap)] border-t border-solid px-[var(--cmp-chipFilter-listbox-bottom-paddingX)] py-[var(--cmp-chipFilter-listbox-bottom-paddingY)]",
18070
18240
  style: { borderColor: CHIPFILTER.listboxBorder },
18071
18241
  children: [
18072
18242
  isMulti && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -18181,6 +18351,7 @@ function DatePresetEditor({
18181
18351
  onValueChange
18182
18352
  }) {
18183
18353
  const presets = getFieldPresets(field);
18354
+ const maxRange = getEffectiveMaxRange(field);
18184
18355
  const [selectedId, setSelectedId] = useState(() => matchPresetId(presets, value));
18185
18356
  const syncedInitialValueRef = useRef(!isEmptyValue(value));
18186
18357
  useEffect(() => {
@@ -18191,7 +18362,13 @@ function DatePresetEditor({
18191
18362
  const selectedPreset = presets.find((preset) => preset.value === selectedId);
18192
18363
  const selectPreset = (preset) => {
18193
18364
  setSelectedId(preset.value);
18194
- if (!preset.custom) onValueChange(preset.resolve ? preset.resolve() : preset.value);
18365
+ if (!preset.custom) {
18366
+ onValueChange(preset.resolve ? preset.resolve() : preset.value);
18367
+ return;
18368
+ }
18369
+ const today = todayIso();
18370
+ const days = maxRange && maxRange > 0 ? maxRange : DEFAULT_CUSTOM_MAX_RANGE;
18371
+ onValueChange([addDaysIso(today, -days), today]);
18195
18372
  };
18196
18373
  if (presets.length === 0) {
18197
18374
  return /* @__PURE__ */ jsx(
@@ -18200,7 +18377,7 @@ function DatePresetEditor({
18200
18377
  value: Array.isArray(value) && value.length === 2 ? value : null,
18201
18378
  onValueChange,
18202
18379
  selectable: field.selectable,
18203
- maxRange: field.maxRange,
18380
+ maxRange,
18204
18381
  disabled,
18205
18382
  width: "100%"
18206
18383
  }
@@ -18212,7 +18389,7 @@ function DatePresetEditor({
18212
18389
  {
18213
18390
  val: preset.value,
18214
18391
  value: selectedId,
18215
- label: preset.label,
18392
+ label: getPresetLabel(preset, maxRange),
18216
18393
  disabled,
18217
18394
  onValueChange: () => selectPreset(preset)
18218
18395
  },
@@ -18224,7 +18401,7 @@ function DatePresetEditor({
18224
18401
  value: Array.isArray(value) && value.length === 2 ? value : null,
18225
18402
  onValueChange,
18226
18403
  selectable: field.selectable,
18227
- maxRange: field.maxRange,
18404
+ maxRange,
18228
18405
  disabled
18229
18406
  }
18230
18407
  )
@@ -18237,9 +18414,10 @@ function PeriodPresetEditor({
18237
18414
  onValueChange
18238
18415
  }) {
18239
18416
  const presets = getFieldPresets(field);
18417
+ const maxRange = getEffectiveMaxRange(field);
18240
18418
  const firstUnit = presets.find((preset) => !preset.custom)?.value ?? "day";
18241
18419
  const [selectedId, setSelectedId] = useState(() => matchPresetId(presets, value) ?? firstUnit);
18242
- const periodValue = isPeriodValue(value) ? value : createPeriodValue(selectedId ?? firstUnit);
18420
+ const periodValue = isPeriodValue(value) ? value : createPeriodValue(selectedId ?? firstUnit, maxRange);
18243
18421
  const selectedUnit = selectedId ?? periodValue.unit;
18244
18422
  useEffect(() => {
18245
18423
  if (isEmptyValue(value)) return;
@@ -18248,7 +18426,7 @@ function PeriodPresetEditor({
18248
18426
  const selectPreset = (preset) => {
18249
18427
  const nextUnit = preset.value;
18250
18428
  setSelectedId(nextUnit);
18251
- onValueChange(createPeriodValue(nextUnit));
18429
+ onValueChange(createPeriodValue(nextUnit, maxRange));
18252
18430
  };
18253
18431
  const updateInputValue = (nextValue) => {
18254
18432
  onValueChange({ unit: selectedUnit, value: nextValue });
@@ -18331,7 +18509,7 @@ function PeriodPresetEditor({
18331
18509
  value: Array.isArray(periodValue.value) && periodValue.value.length === 2 ? periodValue.value : null,
18332
18510
  onValueChange: updateInputValue,
18333
18511
  selectable: field.selectable,
18334
- maxRange: field.maxRange,
18512
+ maxRange,
18335
18513
  disabled,
18336
18514
  width: "100%"
18337
18515
  }
@@ -18343,7 +18521,7 @@ function PeriodPresetEditor({
18343
18521
  {
18344
18522
  val: preset.value,
18345
18523
  value: selectedId,
18346
- label: preset.label,
18524
+ label: getPresetLabel(preset, maxRange),
18347
18525
  disabled,
18348
18526
  onValueChange: () => selectPreset(preset)
18349
18527
  },
@@ -18379,6 +18557,10 @@ function InlineDatePresetFilter({
18379
18557
  setSelectedId(preset.value);
18380
18558
  onValueChange(preset.resolve ? preset.resolve() : null);
18381
18559
  };
18560
+ const badgeBorder = disabled ? CHIPFILTER.chipDisabledBorder : warning ? CHIPFILTER.chipWarningBorder : CHIPFILTER.chipAppliedBorder;
18561
+ const badgeBackground = disabled ? CHIPFILTER.chipDisabledBg : CHIPFILTER.chipAppliedBg;
18562
+ const badgeLabelColor = disabled ? CHIPFILTER.chipDisabledText : CHIPFILTER.chipAppliedLabel;
18563
+ const badgeValueColor = disabled ? CHIPFILTER.chipDisabledText : CHIPFILTER.chipAppliedValue;
18382
18564
  const badge = applied && /* @__PURE__ */ jsxs(
18383
18565
  "span",
18384
18566
  {
@@ -18386,17 +18568,17 @@ function InlineDatePresetFilter({
18386
18568
  style: {
18387
18569
  minHeight: CHIPFILTER.chipHeight,
18388
18570
  borderRadius: CHIPFILTER.chipRadius,
18389
- borderColor: warning ? CHIPFILTER.chipWarningBorder : CHIPFILTER.chipAppliedBorder,
18390
- background: CHIPFILTER.chipAppliedBg,
18391
- color: CHIPFILTER.chipAppliedLabel,
18392
- cursor: rangeValue ? "pointer" : void 0
18571
+ borderColor: badgeBorder,
18572
+ background: badgeBackground,
18573
+ color: badgeLabelColor,
18574
+ cursor: rangeValue && !disabled ? "pointer" : void 0
18393
18575
  },
18394
18576
  children: [
18395
18577
  /* @__PURE__ */ jsxs("span", { className: "font-bold", children: [
18396
18578
  field.label,
18397
18579
  ":"
18398
18580
  ] }),
18399
- /* @__PURE__ */ jsx("span", { className: "font-bold", style: { color: CHIPFILTER.chipAppliedValue }, children: displayValue(field, value) }),
18581
+ /* @__PURE__ */ jsx("span", { className: "font-bold", style: { color: badgeValueColor }, children: displayValue(field, value) }),
18400
18582
  !disabled && showClear && /* @__PURE__ */ jsx(
18401
18583
  "span",
18402
18584
  {
@@ -18551,7 +18733,7 @@ function KeywordEditor({
18551
18733
  }
18552
18734
  };
18553
18735
  return /* @__PURE__ */ jsxs(Fragment, { children: [
18554
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--cmp-chipFilter-listbox-top-gap)] px-[var(--cmp-chipFilter-listbox-top-paddingX)] py-[var(--cmp-chipFilter-listbox-top-paddingY)]", children: [
18736
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-col gap-[var(--cmp-chipFilter-listbox-top-gap)] px-[var(--cmp-chipFilter-listbox-top-paddingX)] py-[var(--cmp-chipFilter-listbox-top-paddingY)]", children: [
18555
18737
  field.matchModes && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
18556
18738
  /* @__PURE__ */ jsx(
18557
18739
  "span",
@@ -18593,7 +18775,7 @@ function KeywordEditor({
18593
18775
  entries3.length > 0 && /* @__PURE__ */ jsx(
18594
18776
  "div",
18595
18777
  {
18596
- className: "max-h-[220px] overflow-auto border-t border-solid py-[var(--cmp-chipFilter-listbox-list-paddingY)]",
18778
+ className: "min-h-0 flex-1 overflow-auto border-t border-solid py-[var(--cmp-chipFilter-listbox-list-paddingY)]",
18597
18779
  style: { borderColor: CHIPFILTER.listboxBorder },
18598
18780
  children: entries3.map((entry) => /* @__PURE__ */ jsx(
18599
18781
  KeywordEntryRow,
@@ -18651,6 +18833,7 @@ function OptionList({
18651
18833
  const selected = multiple ? toArrayValue(value) : [];
18652
18834
  const singleValue = !multiple && (typeof value === "string" || typeof value === "number") ? value : null;
18653
18835
  const [search, setSearch] = useState("");
18836
+ const searchInputRef = useRef(null);
18654
18837
  const options = field.options ?? [];
18655
18838
  const showSearch = options.length >= OPTION_SEARCH_THRESHOLD;
18656
18839
  const filteredOptions = useMemo(() => {
@@ -18658,74 +18841,70 @@ function OptionList({
18658
18841
  const query = search.trim().toLowerCase();
18659
18842
  return options.filter((option) => option.label.toLowerCase().includes(query));
18660
18843
  }, [options, search, showSearch]);
18844
+ useEffect(() => {
18845
+ if (!showSearch || disabled) return;
18846
+ searchInputRef.current?.focus();
18847
+ }, [showSearch, disabled]);
18661
18848
  if (options.length === 0) return /* @__PURE__ */ jsx(EmptyItem, { children: "\uD45C\uC2DC\uD560 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." });
18662
- return /* @__PURE__ */ jsxs(
18663
- "div",
18664
- {
18665
- className: cn(
18666
- "max-h-[240px] overflow-auto",
18667
- !showSearch && "pt-[var(--cmp-chipFilter-listbox-list-paddingY)]",
18668
- "pb-[var(--cmp-chipFilter-listbox-list-paddingY)]"
18669
- ),
18670
- children: [
18671
- showSearch && /* @__PURE__ */ jsx(
18672
- "div",
18849
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [
18850
+ showSearch && /* @__PURE__ */ jsx(
18851
+ "div",
18852
+ {
18853
+ className: "shrink-0 border-b border-solid px-[8px] pb-[4px] pt-[var(--cmp-chipFilter-listbox-list-paddingY)]",
18854
+ style: { borderColor: CHIPFILTER.listboxBorder },
18855
+ children: /* @__PURE__ */ jsx(
18856
+ SSearchInput,
18673
18857
  {
18674
- className: "sticky top-0 z-[1] border-b border-solid px-[8px] pb-[4px] pt-[var(--cmp-chipFilter-listbox-list-paddingY)]",
18675
- style: { background: CHIPFILTER.listboxBg, borderColor: CHIPFILTER.listboxBorder },
18676
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-[6px] rounded-[4px] border border-solid border-[color:var(--sys-color-field-border-default)] bg-[var(--sys-color-field-bg-default)] px-[8px] py-[4px] focus-within:border-[color:var(--sys-color-field-border-focus)]", children: [
18677
- /* @__PURE__ */ jsx(SIcon, { name: "search", size: 16, color: "var(--sys-color-field-icon-default)" }),
18678
- /* @__PURE__ */ jsx(
18679
- "input",
18680
- {
18681
- value: search,
18682
- disabled,
18683
- placeholder: "\uAC80\uC0C9",
18684
- onChange: (event) => setSearch(event.target.value),
18685
- className: "min-w-0 flex-1 border-none bg-transparent text-[12px] leading-[20px] text-[color:var(--sys-color-field-text-default)] outline-none placeholder:text-[color:var(--sys-color-field-text-placeholder)] disabled:cursor-not-allowed"
18686
- }
18687
- ),
18688
- search !== "" && !disabled && /* @__PURE__ */ jsx(
18689
- SGhostButton,
18690
- {
18691
- icon: "close",
18692
- size: "xxs",
18693
- ariaLabel: "\uAC80\uC0C9\uC5B4 \uC9C0\uC6B0\uAE30",
18694
- className: "flex-shrink-0",
18695
- onClick: () => setSearch("")
18696
- }
18697
- )
18698
- ] })
18858
+ ref: searchInputRef,
18859
+ size: "sm",
18860
+ value: search,
18861
+ onValueChange: setSearch,
18862
+ disabled,
18863
+ placeholder: "\uAC80\uC0C9",
18864
+ clearable: true,
18865
+ width: "100%"
18699
18866
  }
18867
+ )
18868
+ }
18869
+ ),
18870
+ /* @__PURE__ */ jsxs(
18871
+ "div",
18872
+ {
18873
+ className: cn(
18874
+ "min-h-0 flex-1 overflow-auto",
18875
+ !showSearch && "pt-[var(--cmp-chipFilter-listbox-list-paddingY)]",
18876
+ "pb-[var(--cmp-chipFilter-listbox-list-paddingY)]"
18700
18877
  ),
18701
- showSearch && filteredOptions.length === 0 && /* @__PURE__ */ jsx(EmptyItem, { children: "\uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }),
18702
- filteredOptions.map((option) => {
18703
- const checked = multiple ? selected.includes(option.value) : singleValue === option.value;
18704
- return /* @__PURE__ */ jsxs(
18705
- MenuItem,
18706
- {
18707
- selected: checked,
18708
- disabled: disabled || option.disabled,
18709
- multi: multiple,
18710
- onClick: () => {
18711
- if (multiple) {
18712
- const next = checked ? selected.filter((v) => v !== option.value) : [...selected, option.value];
18713
- onValueChange(next);
18714
- } else {
18715
- onValueChange(option.value);
18716
- }
18878
+ children: [
18879
+ showSearch && filteredOptions.length === 0 && /* @__PURE__ */ jsx(EmptyItem, { children: "\uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }),
18880
+ filteredOptions.map((option) => {
18881
+ const checked = multiple ? selected.includes(option.value) : singleValue === option.value;
18882
+ return /* @__PURE__ */ jsxs(
18883
+ MenuItem,
18884
+ {
18885
+ selected: checked,
18886
+ disabled: disabled || option.disabled,
18887
+ multi: multiple,
18888
+ onClick: () => {
18889
+ if (multiple) {
18890
+ const next = checked ? selected.filter((v) => v !== option.value) : [...selected, option.value];
18891
+ onValueChange(next);
18892
+ } else {
18893
+ onValueChange(option.value);
18894
+ }
18895
+ },
18896
+ children: [
18897
+ multiple && /* @__PURE__ */ jsx(OptionCheckbox, { checked }),
18898
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: option.label })
18899
+ ]
18717
18900
  },
18718
- children: [
18719
- multiple && /* @__PURE__ */ jsx(OptionCheckbox, { checked }),
18720
- /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: option.label })
18721
- ]
18722
- },
18723
- String(option.value)
18724
- );
18725
- })
18726
- ]
18727
- }
18728
- );
18901
+ String(option.value)
18902
+ );
18903
+ })
18904
+ ]
18905
+ }
18906
+ )
18907
+ ] });
18729
18908
  }
18730
18909
  function OptionCheckbox({ checked }) {
18731
18910
  return /* @__PURE__ */ jsx(
@@ -18739,15 +18918,22 @@ function OptionCheckbox({ checked }) {
18739
18918
  }
18740
18919
  );
18741
18920
  }
18742
- function ListBox({ children }) {
18921
+ function ListBox({
18922
+ children,
18923
+ maxHeight
18924
+ }) {
18743
18925
  return /* @__PURE__ */ jsx(
18744
18926
  "div",
18745
18927
  {
18746
- className: "overflow-hidden border border-solid shadow-[var(--shadow-overlay)]",
18928
+ className: cn(
18929
+ "overflow-hidden border border-solid shadow-[var(--shadow-overlay)]",
18930
+ maxHeight && "flex flex-col"
18931
+ ),
18747
18932
  style: {
18748
18933
  borderRadius: CHIPFILTER.listboxRadius,
18749
18934
  background: CHIPFILTER.listboxBg,
18750
- borderColor: CHIPFILTER.listboxBorder
18935
+ borderColor: CHIPFILTER.listboxBorder,
18936
+ maxHeight
18751
18937
  },
18752
18938
  children
18753
18939
  }