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.cjs CHANGED
@@ -10444,6 +10444,14 @@ var SStepper = /* @__PURE__ */ react.forwardRef(function SStepper2({
10444
10444
  }
10445
10445
  );
10446
10446
  });
10447
+
10448
+ // src/lib/floating-height.ts
10449
+ var POPOVER_AVAILABLE_HEIGHT = "var(--radix-popover-content-available-height)";
10450
+ function clampToAvailableHeight(value, availableHeight) {
10451
+ if (value == null || value === "") return availableHeight;
10452
+ const css = typeof value === "number" ? `${value}px` : value;
10453
+ return `min(${css}, ${availableHeight})`;
10454
+ }
10447
10455
  function runRules(value, rules) {
10448
10456
  if (!rules || rules.length === 0) return "";
10449
10457
  for (const rule of rules) {
@@ -10841,7 +10849,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
10841
10849
  error = false,
10842
10850
  rules,
10843
10851
  labelTooltipProps,
10844
- dropdownHeight = "260px",
10852
+ dropdownHeight = "640px",
10845
10853
  dropdownWidth,
10846
10854
  maxDropdownWidth = "640px",
10847
10855
  label,
@@ -11202,6 +11210,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
11202
11210
  placement: "bottom",
11203
11211
  align: "start",
11204
11212
  offset: 4,
11213
+ viewportPadding: 24,
11205
11214
  fit: !dropdownWidth,
11206
11215
  onOpenAutoFocus: (event) => {
11207
11216
  if (!showSearch) {
@@ -11234,7 +11243,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
11234
11243
  SCROLLBAR_TRACK_BG_CLASS,
11235
11244
  SCROLLBAR_TRACK_BORDER_CLASS
11236
11245
  ),
11237
- style: { maxHeight: dropdownHeight },
11246
+ style: { maxHeight: clampToAvailableHeight(dropdownHeight, POPOVER_AVAILABLE_HEIGHT) },
11238
11247
  onMouseLeave: () => setFocusedIndex(-1),
11239
11248
  children: [
11240
11249
  showSearch && /* @__PURE__ */ jsxRuntime.jsx(
@@ -11506,6 +11515,7 @@ var colWidthFromDef = (c) => {
11506
11515
  };
11507
11516
  var STable = /* @__PURE__ */ react.forwardRef(function STable2({
11508
11517
  columns = [],
11518
+ headerGroups,
11509
11519
  rows = [],
11510
11520
  rowKey = "id",
11511
11521
  selectable = false,
@@ -11553,6 +11563,49 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
11553
11563
  onDenseChange?.(next);
11554
11564
  };
11555
11565
  const visibleCols = react.useMemo(() => columns.filter((c) => c.visible !== false), [columns]);
11566
+ const headerGroupInfo = react.useMemo(() => {
11567
+ if (!headerGroups || headerGroups.length === 0) return null;
11568
+ const indexByName = new Map(visibleCols.map((col, index) => [col.name, index]));
11569
+ const covered = /* @__PURE__ */ new Set();
11570
+ let previousLast = -1;
11571
+ const cells = headerGroups.map((group) => {
11572
+ if (group.columnNames.length === 0) {
11573
+ throw new Error("[STable] headerGroups\uC758 columnNames\uB294 \uBE44\uC5B4 \uC788\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
11574
+ }
11575
+ const indexes = group.columnNames.map((name) => {
11576
+ const index = indexByName.get(name);
11577
+ if (index == null) {
11578
+ throw new Error(`[STable] headerGroups\uC5D0 \uC874\uC7AC\uD558\uC9C0 \uC54A\uB294 \uCEEC\uB7FC\uC774 \uC9C0\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${name}`);
11579
+ }
11580
+ if (covered.has(name)) {
11581
+ throw new Error(`[STable] headerGroups\uC5D0\uC11C \uCEEC\uB7FC\uC774 \uC911\uBCF5 \uC9C0\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${name}`);
11582
+ }
11583
+ covered.add(name);
11584
+ return index;
11585
+ });
11586
+ const first = indexes[0];
11587
+ const last = indexes[indexes.length - 1];
11588
+ if (last - first + 1 !== indexes.length || indexes.some((index, i) => index !== first + i)) {
11589
+ throw new Error(
11590
+ "[STable] headerGroups\uC758 columnNames\uB294 \uC778\uC811\uD55C \uCEEC\uB7FC \uC21C\uC11C\uB85C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4."
11591
+ );
11592
+ }
11593
+ if (first <= previousLast) {
11594
+ throw new Error("[STable] headerGroups\uB294 \uCEEC\uB7FC \uC21C\uC11C\uB300\uB85C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.");
11595
+ }
11596
+ previousLast = last;
11597
+ return { ...group, firstIndex: first, colSpan: indexes.length };
11598
+ });
11599
+ if (covered.size !== visibleCols.length) {
11600
+ throw new Error("[STable] headerGroups\uC758 columnNames\uAC00 \uBAA8\uB4E0 visible \uCEEC\uB7FC\uC744 \uD3EC\uD568\uD574\uC57C \uD569\uB2C8\uB2E4.");
11601
+ }
11602
+ return {
11603
+ cells,
11604
+ rowSpanNames: new Set(
11605
+ cells.filter((cell) => cell.rowSpan === 2).flatMap((cell) => cell.columnNames)
11606
+ )
11607
+ };
11608
+ }, [headerGroups, visibleCols]);
11556
11609
  const stickyLeft = stickyColumn?.left ?? 0;
11557
11610
  const stickyRight = stickyColumn?.right ?? 0;
11558
11611
  const effectiveRowHeight = rowHeight ?? (innerDense ? DENSE_ROW_HEIGHT : DEFAULT_ROW_HEIGHT);
@@ -11815,6 +11868,35 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
11815
11868
  isRightEdge && scrolledRight && "shadow-[-5px_0_8px_0_rgba(34,34,34,0.1)]"
11816
11869
  );
11817
11870
  };
11871
+ const headerResizeHitArea = (i, key) => {
11872
+ const col = visibleCols[i];
11873
+ if (!headerGroupInfo || !resizable || !col || col.autoWidth || i >= visibleCols.length - 1) {
11874
+ return null;
11875
+ }
11876
+ return /* @__PURE__ */ jsxRuntime.jsx(
11877
+ "span",
11878
+ {
11879
+ onMouseDown: (e) => startResize(i, e),
11880
+ className: "absolute inset-y-0 right-0 z-[5] w-[9px] cursor-col-resize",
11881
+ "aria-hidden": true
11882
+ },
11883
+ key
11884
+ );
11885
+ };
11886
+ const headerResizer = (i, key) => {
11887
+ const col = visibleCols[i];
11888
+ if (!resizable || !col || col.autoWidth || i >= visibleCols.length - 1) return null;
11889
+ return /* @__PURE__ */ jsxRuntime.jsx(
11890
+ "span",
11891
+ {
11892
+ onMouseDown: (e) => startResize(i, e),
11893
+ 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",
11894
+ "aria-hidden": true,
11895
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-full w-[4px] border-x border-solid border-[color:var(--cmp-table-header-resizingBar-color)]" })
11896
+ },
11897
+ key
11898
+ );
11899
+ };
11818
11900
  const bodyScrollClass = cn(
11819
11901
  "min-h-0 flex-1 overflow-auto border-t border-solid border-[color:var(--cmp-table-border-color)]",
11820
11902
  SCROLLBAR_CLASS,
@@ -11879,145 +11961,179 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
11879
11961
  style: { fontSize: 12 },
11880
11962
  children: [
11881
11963
  colgroup,
11882
- /* @__PURE__ */ jsxRuntime.jsx("thead", { className: cn(isNoData && "opacity-40"), children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
11883
- selectable && /* @__PURE__ */ jsxRuntime.jsxs(
11884
- "th",
11885
- {
11886
- className: cn(
11887
- // 원본 .th--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
11888
- // 하단 border는 헤더 래퍼가 연속으로 그리므로 셀에는 두지 않는다.
11889
- "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
11890
- stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
11891
- ),
11892
- style: {
11893
- width: SELECTABLE_COLUMN_WIDTH,
11894
- minWidth: SELECTABLE_COLUMN_WIDTH,
11895
- maxWidth: SELECTABLE_COLUMN_WIDTH
11964
+ /* @__PURE__ */ jsxRuntime.jsxs("thead", { className: cn(isNoData && "opacity-40"), children: [
11965
+ headerGroupInfo && /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
11966
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(
11967
+ "th",
11968
+ {
11969
+ rowSpan: 2,
11970
+ className: cn(
11971
+ "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
11972
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
11973
+ ),
11974
+ style: {
11975
+ width: SELECTABLE_COLUMN_WIDTH,
11976
+ minWidth: SELECTABLE_COLUMN_WIDTH,
11977
+ maxWidth: SELECTABLE_COLUMN_WIDTH
11978
+ },
11979
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: headerCheck, onValueChange: toggleAll }) })
11980
+ }
11981
+ ),
11982
+ headerGroupInfo.cells.map((group) => /* @__PURE__ */ jsxRuntime.jsxs(
11983
+ "th",
11984
+ {
11985
+ colSpan: group.colSpan,
11986
+ rowSpan: group.rowSpan,
11987
+ className: cn(
11988
+ "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)]",
11989
+ group.firstIndex + group.colSpan === visibleCols.length && "border-r-0",
11990
+ group.rowSpan === 2 && "border-b-0",
11991
+ group.className
11992
+ ),
11993
+ style: { textAlign: group.align ?? "left", ...group.style },
11994
+ children: [
11995
+ group.render ? group.render() : group.label,
11996
+ group.rowSpan === 2 && group.colSpan === 1 ? headerResizeHitArea(
11997
+ group.firstIndex,
11998
+ `group-resizer-${group.columnNames[0]}`
11999
+ ) : null
12000
+ ]
11896
12001
  },
11897
- children: [
11898
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: headerCheck, onValueChange: toggleAll }) }),
11899
- /* @__PURE__ */ jsxRuntime.jsx(
11900
- "span",
11901
- {
11902
- "aria-hidden": true,
11903
- 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)]"
11904
- }
11905
- )
11906
- ]
11907
- }
11908
- ),
11909
- visibleCols.map((col, i) => {
11910
- const isSortable = !!col.sortable && !col.renderHeader;
11911
- const sortDir = isSortable && sort?.name === col.name ? sort.dir : void 0;
11912
- return /* @__PURE__ */ jsxRuntime.jsxs(
12002
+ group.columnNames.join("|")
12003
+ ))
12004
+ ] }),
12005
+ /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "h-[var(--cmp-table-header-height)] bg-[var(--cmp-table-header-bg)]", children: [
12006
+ selectable && !headerGroupInfo && /* @__PURE__ */ jsxRuntime.jsxs(
11913
12007
  "th",
11914
12008
  {
11915
12009
  className: cn(
11916
- // 하단 border는 헤더 래퍼가 연속으로 그린다 (거터 영역까지 이어짐).
11917
- // leading-[0]: th 의 스트럿(line-height 만큼의 빈 인라인 박스)을 없앤다.
11918
- // 내용이 inline-flex 라 스트럿이 남아 있으면 라벨보다 큰 컨트롤(정렬 버튼 등)이
11919
- // 들어올 때 줄 상자가 베이스라인 기준으로 늘어나 헤더 높이를 넘기고
11920
- // 세로 가운데도 어긋난다. 글자 줄높이는 아래 th__content 가 다시 준다.
11921
- "relative bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[0] text-[color:var(--sys-color-fg-primary)]",
11922
- stickyInfo(i).isSticky && "z-[6]",
11923
- stickyShadowClass(i),
11924
- col.thClass
12010
+ // 원본 .th--selected: 48px 고정폭, 항상 left sticky, 체크박스 중앙정렬
12011
+ // 하단 border는 헤더 래퍼가 연속으로 그리므로 셀에는 두지 않는다.
12012
+ "sticky left-0 z-[7] bg-[var(--cmp-table-header-bg)] text-center align-middle",
12013
+ stickyLeft === 0 && scrolledLeft && "shadow-[5px_0_8px_0_rgba(34,34,34,0.1)]"
11925
12014
  ),
11926
- style: { textAlign: col.align ?? "left", ...cellStyle(i), ...col.thStyle },
11927
- "aria-sort": isSortable ? sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none" : void 0,
12015
+ style: {
12016
+ width: SELECTABLE_COLUMN_WIDTH,
12017
+ minWidth: SELECTABLE_COLUMN_WIDTH,
12018
+ maxWidth: SELECTABLE_COLUMN_WIDTH
12019
+ },
11928
12020
  children: [
12021
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: headerCheck, onValueChange: toggleAll }) }),
11929
12022
  /* @__PURE__ */ jsxRuntime.jsx(
11930
12023
  "span",
11931
12024
  {
11932
- className: cn(
11933
- // 원본 th__content: align에 따라 justify-content 적용 (th도 정렬 반영)
11934
- // th 가 leading-[0] 이므로 글자 줄높이는 여기서 준다 (truncate 가
11935
- // overflow-hidden 이라 줄높이가 0이면 라벨이 잘려 사라진다).
11936
- "inline-flex w-full min-w-0 items-center gap-[4px] leading-[20px]",
11937
- col.align === "center" && "justify-center",
11938
- col.align === "right" && "justify-end"
11939
- ),
11940
- children: col.renderHeader ? col.renderHeader(col, i) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11941
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: col.label }),
11942
- col.helpText && col.helpText.length > 0 && // 라벨이 좁아져 truncate 되더라도 아이콘까지 함께 줄어들면 안 되므로
11943
- // shrink-0 래퍼로 감싼다 (STooltip 의 className 은 본문 쪽으로 간다).
11944
- // 아이콘·크기·색은 SKeyValueTable 헤더 도움말과 동일하게 맞춘다.
11945
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
11946
- STooltip2,
11947
- {
11948
- trigger: "hover",
11949
- icon: "helpOutline",
11950
- iconSize: 12,
11951
- color: "var(--sys-color-fg-success)",
11952
- placement: "top",
11953
- message: col.helpText
11954
- }
11955
- ) }),
11956
- col.editable && /* @__PURE__ */ jsxRuntime.jsx(
11957
- SIcon,
11958
- {
11959
- name: "pageEdit",
11960
- size: 12,
11961
- color: "var(--sys-color-link-accent)",
11962
- label: "\uC218\uC815 \uAC00\uB2A5",
11963
- className: "shrink-0"
11964
- }
11965
- ),
11966
- col.navigable && /* @__PURE__ */ jsxRuntime.jsx(
11967
- SIcon,
11968
- {
11969
- name: "pageMove",
11970
- size: 12,
11971
- color: "var(--sys-color-link-accent)",
11972
- label: "\uB2E4\uB978 \uD654\uBA74\uC73C\uB85C \uC774\uB3D9",
11973
- className: "shrink-0"
11974
- }
11975
- ),
11976
- col.required && // 라벨이 truncate 돼도 필수 표시는 남아야 하므로 shrink-0.
11977
- // 아이콘·크기·색은 SKeyValueTable 헤더 필수 표시와 동일하게 맞춘다.
11978
- /* @__PURE__ */ jsxRuntime.jsx(
11979
- SIcon,
11980
- {
11981
- name: "asterisk",
11982
- size: 12,
11983
- color: "var(--sys-color-fg-accent)",
11984
- className: "shrink-0"
11985
- }
11986
- ),
11987
- isSortable && // 미정렬은 updown 을 default(회색)로 — 정렬할 수 있다는 것 자체가
11988
- // 헤더에 보여야 한다. 정렬 중이면 action 색으로 올려 활성임을 알린다.
11989
- // 라벨이 truncate 돼도 버튼은 남도록 shrink-0.
11990
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
11991
- SGhostButton,
11992
- {
11993
- size: "xxs",
11994
- intent: sortDir ? "action" : "default",
11995
- icon: sortDir === "asc" ? "arrowUp" : sortDir === "desc" ? "arrowDown" : "updown",
11996
- ariaLabel: `${col.label} \uC815\uB82C`,
11997
- onClick: () => toggleSort(col)
11998
- }
11999
- ) })
12000
- ] })
12001
- }
12002
- ),
12003
- resizable && !col.autoWidth && i < visibleCols.length - 1 && // 원본 th__resizer: 셀 우측 경계 안쪽 4px 그립(좌우 1px #CCCCCC 라인, 세로 16px).
12004
- // 셀 밖으로 넘치면 다음 셀에 가려져 클릭이 안 먹으므로 translate 없이 셀 내부에 배치.
12005
- // 겉보기 그립은 4px(원본과 동일), 클릭 영역은 9px로 넓혀 사용성 확보.
12006
- /* @__PURE__ */ jsxRuntime.jsx(
12007
- "span",
12008
- {
12009
- onMouseDown: (e) => startResize(i, e),
12010
- 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",
12011
12025
  "aria-hidden": true,
12012
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-full w-[4px] border-x border-solid border-[color:var(--cmp-table-header-resizingBar-color)]" })
12026
+ 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)]"
12013
12027
  }
12014
12028
  )
12015
12029
  ]
12016
- },
12017
- col.name
12018
- );
12019
- })
12020
- ] }) })
12030
+ }
12031
+ ),
12032
+ visibleCols.map((col, i) => {
12033
+ if (headerGroupInfo?.rowSpanNames.has(col.name)) return null;
12034
+ const isSortable = !!col.sortable && !col.renderHeader;
12035
+ const sortDir = isSortable && sort?.name === col.name ? sort.dir : void 0;
12036
+ return /* @__PURE__ */ jsxRuntime.jsxs(
12037
+ "th",
12038
+ {
12039
+ className: cn(
12040
+ // 하단 border는 헤더 래퍼가 연속으로 그린다 (거터 영역까지 이어짐).
12041
+ // leading-[0]: th 의 스트럿(line-height 만큼의 빈 인라인 박스)을 없앤다.
12042
+ // 내용이 inline-flex 라 스트럿이 남아 있으면 라벨보다 큰 컨트롤(정렬 버튼 등)이
12043
+ // 들어올 때 줄 상자가 베이스라인 기준으로 늘어나 헤더 높이를 넘기고
12044
+ // 세로 가운데도 어긋난다. 글자 줄높이는 아래 th__content 가 다시 준다.
12045
+ headerGroupInfo && "border-r border-solid border-[color:var(--cmp-table-border-color)]",
12046
+ headerGroupInfo && i === visibleCols.length - 1 && "border-r-0",
12047
+ "relative bg-[var(--cmp-table-header-bg)] px-[16px] text-[12px] font-medium leading-[0] text-[color:var(--sys-color-fg-primary)]",
12048
+ stickyInfo(i).isSticky && "z-[6]",
12049
+ stickyShadowClass(i),
12050
+ col.thClass
12051
+ ),
12052
+ style: { textAlign: col.align ?? "left", ...cellStyle(i), ...col.thStyle },
12053
+ "aria-sort": isSortable ? sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none" : void 0,
12054
+ children: [
12055
+ /* @__PURE__ */ jsxRuntime.jsx(
12056
+ "span",
12057
+ {
12058
+ className: cn(
12059
+ // 원본 th__content: align에 따라 justify-content 적용 (th도 정렬 반영)
12060
+ // th 가 leading-[0] 이므로 글자 줄높이는 여기서 준다 (truncate 가
12061
+ // overflow-hidden 이라 줄높이가 0이면 라벨이 잘려 사라진다).
12062
+ "inline-flex w-full min-w-0 items-center gap-[4px] leading-[20px]",
12063
+ col.align === "center" && "justify-center",
12064
+ col.align === "right" && "justify-end"
12065
+ ),
12066
+ children: col.renderHeader ? col.renderHeader(col, i) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
12067
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: col.label }),
12068
+ col.helpText && col.helpText.length > 0 && // 라벨이 좁아져 truncate 되더라도 아이콘까지 함께 줄어들면 안 되므로
12069
+ // shrink-0 래퍼로 감싼다 (STooltip 의 className 은 본문 쪽으로 간다).
12070
+ // 아이콘·크기·색은 SKeyValueTable 헤더 도움말과 동일하게 맞춘다.
12071
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
12072
+ STooltip2,
12073
+ {
12074
+ trigger: "hover",
12075
+ icon: "helpOutline",
12076
+ iconSize: 12,
12077
+ color: "var(--sys-color-fg-success)",
12078
+ placement: "top",
12079
+ message: col.helpText
12080
+ }
12081
+ ) }),
12082
+ col.editable && /* @__PURE__ */ jsxRuntime.jsx(
12083
+ SIcon,
12084
+ {
12085
+ name: "pageEdit",
12086
+ size: 12,
12087
+ color: "var(--sys-color-link-accent)",
12088
+ label: "\uC218\uC815 \uAC00\uB2A5",
12089
+ className: "shrink-0"
12090
+ }
12091
+ ),
12092
+ col.navigable && /* @__PURE__ */ jsxRuntime.jsx(
12093
+ SIcon,
12094
+ {
12095
+ name: "pageMove",
12096
+ size: 12,
12097
+ color: "var(--sys-color-link-accent)",
12098
+ label: "\uB2E4\uB978 \uD654\uBA74\uC73C\uB85C \uC774\uB3D9",
12099
+ className: "shrink-0"
12100
+ }
12101
+ ),
12102
+ col.required && // 라벨이 truncate 돼도 필수 표시는 남아야 하므로 shrink-0.
12103
+ // 아이콘·크기·색은 SKeyValueTable 헤더 필수 표시와 동일하게 맞춘다.
12104
+ /* @__PURE__ */ jsxRuntime.jsx(
12105
+ SIcon,
12106
+ {
12107
+ name: "asterisk",
12108
+ size: 12,
12109
+ color: "var(--sys-color-fg-accent)",
12110
+ className: "shrink-0"
12111
+ }
12112
+ ),
12113
+ isSortable && // 미정렬은 updown 을 default(회색)로 — 정렬할 수 있다는 것 자체가
12114
+ // 헤더에 보여야 한다. 정렬 중이면 action 색으로 올려 활성임을 알린다.
12115
+ // 라벨이 truncate 돼도 버튼은 남도록 shrink-0.
12116
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
12117
+ SGhostButton,
12118
+ {
12119
+ size: "xxs",
12120
+ intent: sortDir ? "action" : "default",
12121
+ icon: sortDir === "asc" ? "arrowUp" : sortDir === "desc" ? "arrowDown" : "updown",
12122
+ ariaLabel: `${col.label} \uC815\uB82C`,
12123
+ onClick: () => toggleSort(col)
12124
+ }
12125
+ ) })
12126
+ ] })
12127
+ }
12128
+ ),
12129
+ headerGroupInfo ? headerResizeHitArea(i, `resizer-${col.name}`) : headerResizer(i, `resizer-${col.name}`)
12130
+ ]
12131
+ },
12132
+ col.name
12133
+ );
12134
+ })
12135
+ ] })
12136
+ ] })
12021
12137
  ]
12022
12138
  }
12023
12139
  )
@@ -17280,6 +17396,8 @@ var SEditor = /* @__PURE__ */ react.forwardRef(function SEditor2({
17280
17396
  });
17281
17397
  var CHIPFILTER_LISTBOX_WIDTH = 240;
17282
17398
  var CHIPFILTER_DATE_PRESET_LISTBOX_WIDTH = 246;
17399
+ var EDITOR_LISTBOX_MAX_HEIGHT = "min(640px, var(--radix-popover-content-available-height))";
17400
+ var EDITOR_POPOVER_VIEWPORT_PADDING = 24;
17283
17401
  var CHIPFILTER = {
17284
17402
  barBg: "var(--cmp-chipFilter-bar-bg)",
17285
17403
  barBorder: "var(--cmp-chipFilter-bar-border)",
@@ -17319,6 +17437,7 @@ var CHIPFILTER = {
17319
17437
  itemTextDefault: "var(--cmp-chipFilter-item-text-default)",
17320
17438
  itemTextSelected: "var(--cmp-chipFilter-item-text-selected)"
17321
17439
  };
17440
+ var DEFAULT_CUSTOM_MAX_RANGE = 90;
17322
17441
  var DEFAULT_PERIOD_PRESETS = [
17323
17442
  { value: "day", label: "\uC77C\uBCC4" },
17324
17443
  { value: "month", label: "\uC6D4\uBCC4" },
@@ -17327,6 +17446,20 @@ var DEFAULT_PERIOD_PRESETS = [
17327
17446
  { value: "year", label: "\uC5F0\uB3C4\uBCC4" },
17328
17447
  { value: "custom", label: "\uC0AC\uC6A9\uC790 \uC9C0\uC815", custom: true }
17329
17448
  ];
17449
+ var DEFAULT_DATE_RANGE_PRESETS = [
17450
+ { value: "today", label: "\uC624\uB298", resolve: () => todayIso() },
17451
+ {
17452
+ value: "last7",
17453
+ label: "\uC9C0\uB09C 7\uC77C",
17454
+ resolve: () => [addDaysIso(todayIso(), -7), todayIso()]
17455
+ },
17456
+ {
17457
+ value: "last30",
17458
+ label: "\uC9C0\uB09C 30\uC77C",
17459
+ resolve: () => [addDaysIso(todayIso(), -30), todayIso()]
17460
+ },
17461
+ { value: "custom", label: "\uC0AC\uC6A9\uC790 \uC9C0\uC815", custom: true }
17462
+ ];
17330
17463
  var isKeywordValue = (value) => typeof value === "object" && value != null && !Array.isArray(value) && "keywords" in value;
17331
17464
  var isPeriodValue = (value) => typeof value === "object" && value != null && !Array.isArray(value) && "unit" in value;
17332
17465
  var keywordEntries = (value) => {
@@ -17351,23 +17484,39 @@ var todayIso = () => {
17351
17484
  const pad = (n) => String(n).padStart(2, "0");
17352
17485
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
17353
17486
  };
17487
+ var addDaysIso = (iso, n) => {
17488
+ const d = /* @__PURE__ */ new Date(`${iso}T00:00:00`);
17489
+ d.setDate(d.getDate() + n);
17490
+ const pad = (v) => String(v).padStart(2, "0");
17491
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
17492
+ };
17354
17493
  var isInlineDateField = (field) => field.type === "datepicker-range" && field.radioButton === true && (field.presets?.length ?? 0) > 0;
17355
17494
  var getFieldPresets = (field) => {
17356
17495
  if (field.type === "datepicker-statistics") return field.presets ?? DEFAULT_PERIOD_PRESETS;
17357
- return field.presets ?? [];
17496
+ return field.presets ?? DEFAULT_DATE_RANGE_PRESETS;
17497
+ };
17498
+ var getEffectiveMaxRange = (field) => {
17499
+ if (field.maxRange !== void 0) return field.maxRange;
17500
+ const hasCustomPreset = getFieldPresets(field).some((preset) => preset.custom);
17501
+ return hasCustomPreset ? DEFAULT_CUSTOM_MAX_RANGE : void 0;
17358
17502
  };
17359
- var getDefaultPeriodInputValue = (unit) => {
17503
+ var getPresetLabel = (preset, maxRange) => {
17504
+ if (!preset.custom || !maxRange || preset.label.includes("\uCD5C\uB300")) return preset.label;
17505
+ return `${preset.label} (\uCD5C\uB300 ${maxRange}\uC77C\uC529)`;
17506
+ };
17507
+ var getDefaultPeriodInputValue = (unit, maxRange) => {
17360
17508
  const today = todayIso();
17361
17509
  if (unit === "day") return today;
17362
17510
  if (unit === "month") return today.slice(0, 7);
17363
17511
  if (unit === "quarter") return 1;
17364
17512
  if (unit === "half") return "first";
17365
17513
  if (unit === "year") return today.slice(0, 4);
17366
- return [today, today];
17514
+ const days = maxRange && maxRange > 0 ? maxRange : DEFAULT_CUSTOM_MAX_RANGE;
17515
+ return [addDaysIso(today, -days), today];
17367
17516
  };
17368
- var createPeriodValue = (unit) => ({
17517
+ var createPeriodValue = (unit, maxRange) => ({
17369
17518
  unit,
17370
- value: getDefaultPeriodInputValue(unit)
17519
+ value: getDefaultPeriodInputValue(unit, maxRange)
17371
17520
  });
17372
17521
  var resolveFieldDefault = (field) => {
17373
17522
  if (field.defaultValue !== void 0 && isFixedField(field)) return field.defaultValue;
@@ -17380,7 +17529,8 @@ var resolveFieldDefault = (field) => {
17380
17529
  if (field.type === "datepicker-day") return todayIso();
17381
17530
  if (field.type === "select") return field.options?.[0]?.value;
17382
17531
  if (field.type === "datepicker-statistics") {
17383
- const unit = getFieldPresets(field).find((preset) => !preset.custom)?.value;
17532
+ const presets = getFieldPresets(field).filter((preset) => !preset.custom);
17533
+ const unit = presets.some((preset) => preset.value === "day") ? "day" : presets[0]?.value;
17384
17534
  return unit ? createPeriodValue(unit) : void 0;
17385
17535
  }
17386
17536
  return void 0;
@@ -17510,8 +17660,7 @@ var SChipFilter = /* @__PURE__ */ react.forwardRef(function SChipFilter2({
17510
17660
  onActiveKeysChange,
17511
17661
  onReset,
17512
17662
  onAddFilter,
17513
- label = "\uAC80\uC0C9 \uD544\uD130",
17514
- showLabel = false,
17663
+ label,
17515
17664
  showReset = true,
17516
17665
  disabled = false,
17517
17666
  className,
@@ -17528,6 +17677,7 @@ var SChipFilter = /* @__PURE__ */ react.forwardRef(function SChipFilter2({
17528
17677
  const pendingOpenKeyRef = react.useRef(null);
17529
17678
  const triggerRefs = react.useRef(/* @__PURE__ */ new Map());
17530
17679
  const initializedDefaultsRef = react.useRef(/* @__PURE__ */ new Set());
17680
+ const misuseWarningSignatureRef = react.useRef(null);
17531
17681
  const prevOpenKeyRef = react.useRef(null);
17532
17682
  const valuesRef = react.useRef(values);
17533
17683
  valuesRef.current = values;
@@ -17543,14 +17693,19 @@ var SChipFilter = /* @__PURE__ */ react.forwardRef(function SChipFilter2({
17543
17693
  (current) => groups.every((group) => isGroupSatisfied(group, current)),
17544
17694
  [groups]
17545
17695
  );
17696
+ const isRequiredSatisfied = react.useCallback(
17697
+ (current) => fields.every((field) => !field.required || !isEmptyValue(current[field.key])),
17698
+ [fields]
17699
+ );
17546
17700
  const runSearch = react.useCallback(
17547
17701
  (next) => {
17548
17702
  if (valuesMapEqual(lastSearchedValuesRef.current, next)) return;
17549
17703
  if (!isGroupsValid(next)) return;
17704
+ if (!isRequiredSatisfied(next)) return;
17550
17705
  lastSearchedValuesRef.current = next;
17551
17706
  onSearch?.(next);
17552
17707
  },
17553
- [isGroupsValid, onSearch]
17708
+ [isGroupsValid, isRequiredSatisfied, onSearch]
17554
17709
  );
17555
17710
  react.useEffect(() => {
17556
17711
  const prevKey = prevOpenKeyRef.current;
@@ -17585,14 +17740,24 @@ var SChipFilter = /* @__PURE__ */ react.forwardRef(function SChipFilter2({
17585
17740
  });
17586
17741
  if (changed) onValueChange?.({ ...values, ...patch });
17587
17742
  }, [fields, values, onValueChange]);
17588
- if (typeof console !== "undefined") {
17589
- const misused = fields.filter((f) => f.defaultValue !== void 0 && !isFixedField(f));
17590
- if (misused.length > 0) {
17743
+ react.useEffect(() => {
17744
+ if (typeof console === "undefined") return;
17745
+ const misusedKeys = fields.filter((f) => f.defaultValue !== void 0 && !isFixedField(f)).map((f) => f.key);
17746
+ const requiredWithoutDefaultKeys = fields.filter((f) => f.required === true && resolveFieldDefault(f) === void 0).map((f) => f.key);
17747
+ const signature = JSON.stringify([misusedKeys, requiredWithoutDefaultKeys]);
17748
+ if (signature === misuseWarningSignatureRef.current) return;
17749
+ misuseWarningSignatureRef.current = signature;
17750
+ if (misusedKeys.length > 0) {
17591
17751
  console.warn(
17592
- `[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(", ")})`
17752
+ `[SChipFilter] defaultValue \uB294 fixed\xB7required \uD544\uD130\uC5D0\uC11C\uB9CC \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uBB34\uC2DC\uD569\uB2C8\uB2E4. (${misusedKeys.join(", ")})`
17593
17753
  );
17594
17754
  }
17595
- }
17755
+ if (requiredWithoutDefaultKeys.length > 0) {
17756
+ console.warn(
17757
+ `[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(", ")})`
17758
+ );
17759
+ }
17760
+ }, [fields]);
17596
17761
  const fixedKeys = fields.filter(isFixedField).map((field) => field.key);
17597
17762
  const resolvedActiveKeys = Array.from(
17598
17763
  /* @__PURE__ */ new Set([...activeKeys ?? internalActiveKeys, ...fixedKeys])
@@ -17660,7 +17825,10 @@ var SChipFilter = /* @__PURE__ */ react.forwardRef(function SChipFilter2({
17660
17825
  },
17661
17826
  [fields]
17662
17827
  );
17663
- const validate = react.useCallback(() => invalidGroups.size === 0, [invalidGroups]);
17828
+ const validate = react.useCallback(
17829
+ () => invalidGroups.size === 0 && isRequiredSatisfied(values),
17830
+ [invalidGroups, isRequiredSatisfied, values]
17831
+ );
17664
17832
  react.useImperativeHandle(ref, () => ({ open, reset, validate }), [open, reset, validate]);
17665
17833
  const addField = (key) => {
17666
17834
  const next = [...resolvedActiveKeys, key];
@@ -17697,7 +17865,7 @@ var SChipFilter = /* @__PURE__ */ react.forwardRef(function SChipFilter2({
17697
17865
  ),
17698
17866
  style: { ...rootStyle, borderColor: "var(--chipfilter-bar-border)" },
17699
17867
  children: [
17700
- showLabel && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
17868
+ label && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
17701
17869
  /* @__PURE__ */ jsxRuntime.jsx(STag, { className: "shrink-0", shape: "square", size: "sm", color: "darkblue", label }),
17702
17870
  /* @__PURE__ */ jsxRuntime.jsx(Divider, { top: true })
17703
17871
  ] }),
@@ -17897,6 +18065,7 @@ var SChipFilter = /* @__PURE__ */ react.forwardRef(function SChipFilter2({
17897
18065
  side: "bottom",
17898
18066
  align: "start",
17899
18067
  sideOffset: 4,
18068
+ collisionPadding: EDITOR_POPOVER_VIEWPORT_PADDING,
17900
18069
  onOpenAutoFocus: (event) => event.preventDefault(),
17901
18070
  className: cn("z-50", FLOATING_SLIDE_ANIM),
17902
18071
  style: editorPopoverStyle,
@@ -18062,6 +18231,7 @@ function EditorPanel({
18062
18231
  onClose
18063
18232
  }) {
18064
18233
  const wrapsInPaddedTop = field.type === "datepicker-day" || field.type === "datepicker-range" || field.type === "datepicker-statistics";
18234
+ const isScrollableListField = field.type === "select" || field.type === "select-multi" || field.type === "keyword";
18065
18235
  const isMulti = field.type === "select-multi";
18066
18236
  const selectableValues = isMulti ? (field.options ?? []).filter((option) => !option.disabled).map((option) => option.value) : [];
18067
18237
  const selectedValues = isMulti ? toArrayValue(value) : [];
@@ -18071,7 +18241,7 @@ function EditorPanel({
18071
18241
  allSelected ? selectedValues.filter((v) => !selectableValues.includes(v)) : [...selectedValues, ...selectableValues.filter((v) => !selectedValues.includes(v))]
18072
18242
  );
18073
18243
  };
18074
- return /* @__PURE__ */ jsxRuntime.jsxs(ListBox, { children: [
18244
+ return /* @__PURE__ */ jsxRuntime.jsxs(ListBox, { maxHeight: isScrollableListField ? EDITOR_LISTBOX_MAX_HEIGHT : void 0, children: [
18075
18245
  wrapsInPaddedTop ? /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(
18076
18246
  FilterEditor,
18077
18247
  {
@@ -18094,7 +18264,7 @@ function EditorPanel({
18094
18264
  /* @__PURE__ */ jsxRuntime.jsxs(
18095
18265
  "div",
18096
18266
  {
18097
- 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)]",
18267
+ 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)]",
18098
18268
  style: { borderColor: CHIPFILTER.listboxBorder },
18099
18269
  children: [
18100
18270
  isMulti && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -18209,6 +18379,7 @@ function DatePresetEditor({
18209
18379
  onValueChange
18210
18380
  }) {
18211
18381
  const presets = getFieldPresets(field);
18382
+ const maxRange = getEffectiveMaxRange(field);
18212
18383
  const [selectedId, setSelectedId] = react.useState(() => matchPresetId(presets, value));
18213
18384
  const syncedInitialValueRef = react.useRef(!isEmptyValue(value));
18214
18385
  react.useEffect(() => {
@@ -18219,7 +18390,13 @@ function DatePresetEditor({
18219
18390
  const selectedPreset = presets.find((preset) => preset.value === selectedId);
18220
18391
  const selectPreset = (preset) => {
18221
18392
  setSelectedId(preset.value);
18222
- if (!preset.custom) onValueChange(preset.resolve ? preset.resolve() : preset.value);
18393
+ if (!preset.custom) {
18394
+ onValueChange(preset.resolve ? preset.resolve() : preset.value);
18395
+ return;
18396
+ }
18397
+ const today = todayIso();
18398
+ const days = maxRange && maxRange > 0 ? maxRange : DEFAULT_CUSTOM_MAX_RANGE;
18399
+ onValueChange([addDaysIso(today, -days), today]);
18223
18400
  };
18224
18401
  if (presets.length === 0) {
18225
18402
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -18228,7 +18405,7 @@ function DatePresetEditor({
18228
18405
  value: Array.isArray(value) && value.length === 2 ? value : null,
18229
18406
  onValueChange,
18230
18407
  selectable: field.selectable,
18231
- maxRange: field.maxRange,
18408
+ maxRange,
18232
18409
  disabled,
18233
18410
  width: "100%"
18234
18411
  }
@@ -18240,7 +18417,7 @@ function DatePresetEditor({
18240
18417
  {
18241
18418
  val: preset.value,
18242
18419
  value: selectedId,
18243
- label: preset.label,
18420
+ label: getPresetLabel(preset, maxRange),
18244
18421
  disabled,
18245
18422
  onValueChange: () => selectPreset(preset)
18246
18423
  },
@@ -18252,7 +18429,7 @@ function DatePresetEditor({
18252
18429
  value: Array.isArray(value) && value.length === 2 ? value : null,
18253
18430
  onValueChange,
18254
18431
  selectable: field.selectable,
18255
- maxRange: field.maxRange,
18432
+ maxRange,
18256
18433
  disabled
18257
18434
  }
18258
18435
  )
@@ -18265,9 +18442,10 @@ function PeriodPresetEditor({
18265
18442
  onValueChange
18266
18443
  }) {
18267
18444
  const presets = getFieldPresets(field);
18445
+ const maxRange = getEffectiveMaxRange(field);
18268
18446
  const firstUnit = presets.find((preset) => !preset.custom)?.value ?? "day";
18269
18447
  const [selectedId, setSelectedId] = react.useState(() => matchPresetId(presets, value) ?? firstUnit);
18270
- const periodValue = isPeriodValue(value) ? value : createPeriodValue(selectedId ?? firstUnit);
18448
+ const periodValue = isPeriodValue(value) ? value : createPeriodValue(selectedId ?? firstUnit, maxRange);
18271
18449
  const selectedUnit = selectedId ?? periodValue.unit;
18272
18450
  react.useEffect(() => {
18273
18451
  if (isEmptyValue(value)) return;
@@ -18276,7 +18454,7 @@ function PeriodPresetEditor({
18276
18454
  const selectPreset = (preset) => {
18277
18455
  const nextUnit = preset.value;
18278
18456
  setSelectedId(nextUnit);
18279
- onValueChange(createPeriodValue(nextUnit));
18457
+ onValueChange(createPeriodValue(nextUnit, maxRange));
18280
18458
  };
18281
18459
  const updateInputValue = (nextValue) => {
18282
18460
  onValueChange({ unit: selectedUnit, value: nextValue });
@@ -18359,7 +18537,7 @@ function PeriodPresetEditor({
18359
18537
  value: Array.isArray(periodValue.value) && periodValue.value.length === 2 ? periodValue.value : null,
18360
18538
  onValueChange: updateInputValue,
18361
18539
  selectable: field.selectable,
18362
- maxRange: field.maxRange,
18540
+ maxRange,
18363
18541
  disabled,
18364
18542
  width: "100%"
18365
18543
  }
@@ -18371,7 +18549,7 @@ function PeriodPresetEditor({
18371
18549
  {
18372
18550
  val: preset.value,
18373
18551
  value: selectedId,
18374
- label: preset.label,
18552
+ label: getPresetLabel(preset, maxRange),
18375
18553
  disabled,
18376
18554
  onValueChange: () => selectPreset(preset)
18377
18555
  },
@@ -18407,6 +18585,10 @@ function InlineDatePresetFilter({
18407
18585
  setSelectedId(preset.value);
18408
18586
  onValueChange(preset.resolve ? preset.resolve() : null);
18409
18587
  };
18588
+ const badgeBorder = disabled ? CHIPFILTER.chipDisabledBorder : warning ? CHIPFILTER.chipWarningBorder : CHIPFILTER.chipAppliedBorder;
18589
+ const badgeBackground = disabled ? CHIPFILTER.chipDisabledBg : CHIPFILTER.chipAppliedBg;
18590
+ const badgeLabelColor = disabled ? CHIPFILTER.chipDisabledText : CHIPFILTER.chipAppliedLabel;
18591
+ const badgeValueColor = disabled ? CHIPFILTER.chipDisabledText : CHIPFILTER.chipAppliedValue;
18410
18592
  const badge = applied && /* @__PURE__ */ jsxRuntime.jsxs(
18411
18593
  "span",
18412
18594
  {
@@ -18414,17 +18596,17 @@ function InlineDatePresetFilter({
18414
18596
  style: {
18415
18597
  minHeight: CHIPFILTER.chipHeight,
18416
18598
  borderRadius: CHIPFILTER.chipRadius,
18417
- borderColor: warning ? CHIPFILTER.chipWarningBorder : CHIPFILTER.chipAppliedBorder,
18418
- background: CHIPFILTER.chipAppliedBg,
18419
- color: CHIPFILTER.chipAppliedLabel,
18420
- cursor: rangeValue ? "pointer" : void 0
18599
+ borderColor: badgeBorder,
18600
+ background: badgeBackground,
18601
+ color: badgeLabelColor,
18602
+ cursor: rangeValue && !disabled ? "pointer" : void 0
18421
18603
  },
18422
18604
  children: [
18423
18605
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-bold", children: [
18424
18606
  field.label,
18425
18607
  ":"
18426
18608
  ] }),
18427
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-bold", style: { color: CHIPFILTER.chipAppliedValue }, children: displayValue(field, value) }),
18609
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-bold", style: { color: badgeValueColor }, children: displayValue(field, value) }),
18428
18610
  !disabled && showClear && /* @__PURE__ */ jsxRuntime.jsx(
18429
18611
  "span",
18430
18612
  {
@@ -18579,7 +18761,7 @@ function KeywordEditor({
18579
18761
  }
18580
18762
  };
18581
18763
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
18582
- /* @__PURE__ */ jsxRuntime.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: [
18764
+ /* @__PURE__ */ jsxRuntime.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: [
18583
18765
  field.matchModes && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
18584
18766
  /* @__PURE__ */ jsxRuntime.jsx(
18585
18767
  "span",
@@ -18621,7 +18803,7 @@ function KeywordEditor({
18621
18803
  entries3.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
18622
18804
  "div",
18623
18805
  {
18624
- className: "max-h-[220px] overflow-auto border-t border-solid py-[var(--cmp-chipFilter-listbox-list-paddingY)]",
18806
+ className: "min-h-0 flex-1 overflow-auto border-t border-solid py-[var(--cmp-chipFilter-listbox-list-paddingY)]",
18625
18807
  style: { borderColor: CHIPFILTER.listboxBorder },
18626
18808
  children: entries3.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(
18627
18809
  KeywordEntryRow,
@@ -18679,6 +18861,7 @@ function OptionList({
18679
18861
  const selected = multiple ? toArrayValue(value) : [];
18680
18862
  const singleValue = !multiple && (typeof value === "string" || typeof value === "number") ? value : null;
18681
18863
  const [search, setSearch] = react.useState("");
18864
+ const searchInputRef = react.useRef(null);
18682
18865
  const options = field.options ?? [];
18683
18866
  const showSearch = options.length >= OPTION_SEARCH_THRESHOLD;
18684
18867
  const filteredOptions = react.useMemo(() => {
@@ -18686,74 +18869,70 @@ function OptionList({
18686
18869
  const query = search.trim().toLowerCase();
18687
18870
  return options.filter((option) => option.label.toLowerCase().includes(query));
18688
18871
  }, [options, search, showSearch]);
18872
+ react.useEffect(() => {
18873
+ if (!showSearch || disabled) return;
18874
+ searchInputRef.current?.focus();
18875
+ }, [showSearch, disabled]);
18689
18876
  if (options.length === 0) return /* @__PURE__ */ jsxRuntime.jsx(EmptyItem, { children: "\uD45C\uC2DC\uD560 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." });
18690
- return /* @__PURE__ */ jsxRuntime.jsxs(
18691
- "div",
18692
- {
18693
- className: cn(
18694
- "max-h-[240px] overflow-auto",
18695
- !showSearch && "pt-[var(--cmp-chipFilter-listbox-list-paddingY)]",
18696
- "pb-[var(--cmp-chipFilter-listbox-list-paddingY)]"
18697
- ),
18698
- children: [
18699
- showSearch && /* @__PURE__ */ jsxRuntime.jsx(
18700
- "div",
18877
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [
18878
+ showSearch && /* @__PURE__ */ jsxRuntime.jsx(
18879
+ "div",
18880
+ {
18881
+ className: "shrink-0 border-b border-solid px-[8px] pb-[4px] pt-[var(--cmp-chipFilter-listbox-list-paddingY)]",
18882
+ style: { borderColor: CHIPFILTER.listboxBorder },
18883
+ children: /* @__PURE__ */ jsxRuntime.jsx(
18884
+ SSearchInput,
18701
18885
  {
18702
- className: "sticky top-0 z-[1] border-b border-solid px-[8px] pb-[4px] pt-[var(--cmp-chipFilter-listbox-list-paddingY)]",
18703
- style: { background: CHIPFILTER.listboxBg, borderColor: CHIPFILTER.listboxBorder },
18704
- children: /* @__PURE__ */ jsxRuntime.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: [
18705
- /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: "search", size: 16, color: "var(--sys-color-field-icon-default)" }),
18706
- /* @__PURE__ */ jsxRuntime.jsx(
18707
- "input",
18708
- {
18709
- value: search,
18710
- disabled,
18711
- placeholder: "\uAC80\uC0C9",
18712
- onChange: (event) => setSearch(event.target.value),
18713
- 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"
18714
- }
18715
- ),
18716
- search !== "" && !disabled && /* @__PURE__ */ jsxRuntime.jsx(
18717
- SGhostButton,
18718
- {
18719
- icon: "close",
18720
- size: "xxs",
18721
- ariaLabel: "\uAC80\uC0C9\uC5B4 \uC9C0\uC6B0\uAE30",
18722
- className: "flex-shrink-0",
18723
- onClick: () => setSearch("")
18724
- }
18725
- )
18726
- ] })
18886
+ ref: searchInputRef,
18887
+ size: "sm",
18888
+ value: search,
18889
+ onValueChange: setSearch,
18890
+ disabled,
18891
+ placeholder: "\uAC80\uC0C9",
18892
+ clearable: true,
18893
+ width: "100%"
18727
18894
  }
18895
+ )
18896
+ }
18897
+ ),
18898
+ /* @__PURE__ */ jsxRuntime.jsxs(
18899
+ "div",
18900
+ {
18901
+ className: cn(
18902
+ "min-h-0 flex-1 overflow-auto",
18903
+ !showSearch && "pt-[var(--cmp-chipFilter-listbox-list-paddingY)]",
18904
+ "pb-[var(--cmp-chipFilter-listbox-list-paddingY)]"
18728
18905
  ),
18729
- showSearch && filteredOptions.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(EmptyItem, { children: "\uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }),
18730
- filteredOptions.map((option) => {
18731
- const checked = multiple ? selected.includes(option.value) : singleValue === option.value;
18732
- return /* @__PURE__ */ jsxRuntime.jsxs(
18733
- MenuItem,
18734
- {
18735
- selected: checked,
18736
- disabled: disabled || option.disabled,
18737
- multi: multiple,
18738
- onClick: () => {
18739
- if (multiple) {
18740
- const next = checked ? selected.filter((v) => v !== option.value) : [...selected, option.value];
18741
- onValueChange(next);
18742
- } else {
18743
- onValueChange(option.value);
18744
- }
18906
+ children: [
18907
+ showSearch && filteredOptions.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(EmptyItem, { children: "\uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }),
18908
+ filteredOptions.map((option) => {
18909
+ const checked = multiple ? selected.includes(option.value) : singleValue === option.value;
18910
+ return /* @__PURE__ */ jsxRuntime.jsxs(
18911
+ MenuItem,
18912
+ {
18913
+ selected: checked,
18914
+ disabled: disabled || option.disabled,
18915
+ multi: multiple,
18916
+ onClick: () => {
18917
+ if (multiple) {
18918
+ const next = checked ? selected.filter((v) => v !== option.value) : [...selected, option.value];
18919
+ onValueChange(next);
18920
+ } else {
18921
+ onValueChange(option.value);
18922
+ }
18923
+ },
18924
+ children: [
18925
+ multiple && /* @__PURE__ */ jsxRuntime.jsx(OptionCheckbox, { checked }),
18926
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: option.label })
18927
+ ]
18745
18928
  },
18746
- children: [
18747
- multiple && /* @__PURE__ */ jsxRuntime.jsx(OptionCheckbox, { checked }),
18748
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: option.label })
18749
- ]
18750
- },
18751
- String(option.value)
18752
- );
18753
- })
18754
- ]
18755
- }
18756
- );
18929
+ String(option.value)
18930
+ );
18931
+ })
18932
+ ]
18933
+ }
18934
+ )
18935
+ ] });
18757
18936
  }
18758
18937
  function OptionCheckbox({ checked }) {
18759
18938
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -18767,15 +18946,22 @@ function OptionCheckbox({ checked }) {
18767
18946
  }
18768
18947
  );
18769
18948
  }
18770
- function ListBox({ children }) {
18949
+ function ListBox({
18950
+ children,
18951
+ maxHeight
18952
+ }) {
18771
18953
  return /* @__PURE__ */ jsxRuntime.jsx(
18772
18954
  "div",
18773
18955
  {
18774
- className: "overflow-hidden border border-solid shadow-[var(--shadow-overlay)]",
18956
+ className: cn(
18957
+ "overflow-hidden border border-solid shadow-[var(--shadow-overlay)]",
18958
+ maxHeight && "flex flex-col"
18959
+ ),
18775
18960
  style: {
18776
18961
  borderRadius: CHIPFILTER.listboxRadius,
18777
18962
  background: CHIPFILTER.listboxBg,
18778
- borderColor: CHIPFILTER.listboxBorder
18963
+ borderColor: CHIPFILTER.listboxBorder,
18964
+ maxHeight
18779
18965
  },
18780
18966
  children
18781
18967
  }