sellmate-design-system-react 9.0.0-beta.76 → 9.0.0-beta.78

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/AGENTS.md CHANGED
@@ -670,6 +670,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
670
670
  | **상태·분류를 라벨로 표시 (기본값)** | **`STag`** — `<STag size="sm" color="..." label="판매중" />`. 목록의 상태 컬럼, 상세의 분류 태그 등 대부분이 여기 해당한다 |
671
671
  | 라벨 없이 **색 점만**으로 상태를 찍을 때 | `SBadge` — 점(dot)만 그리는 인디케이터. 텍스트가 이미 있고 앞에 점만 붙이는 좁은 경우에만 |
672
672
  | 사용자가 입력·삭제·편집하는 **토큰** | `SChip` (단독) / `SChipInput` (입력 필드 안에서) |
673
+ | 목록에서 **여러 개 고른 값**을 칩으로 보여주고 하나씩 빼게 할 때 | `SSelect` `type="multi"` + `valueDisplay="chip"` (§3-7-2) — `SChip` 을 따로 나열하지 않는다 |
673
674
 
674
675
  > 상태 표시는 **`STag size="sm"` 이 기본**이다. 색 점 + 텍스트 조합(`SBadge`)을 기본으로 쓰지 않는다.
675
676
 
@@ -1752,6 +1753,14 @@ tableRef.current.scrollToRow(row); // 복원
1752
1753
  - 카드 안에 상태 배지를 넣으려면 `tag` 슬롯에 `STag` 를 준다. 라벨 문자열에 "(추천)" 처럼 섞어 쓰지 않는다.
1753
1754
  - **카드처럼 생겼다고 `SCard`/`SSectionHeaderCard` 로 감싸지 않는다.** `SRadioCard` 자체가 완결된 요소이고, 나열 간격은 `SRadioCardGroup` 의 `direction` 이 맞춘다 (§2-2).
1754
1755
 
1756
+ ##### 여러 개 고른 값을 칩으로 보여줄 때 — `valueDisplay="chip"`
1757
+
1758
+ - **`type="multi"`·`"multi_depth"` 에서 `valueDisplay="chip"` 을 주면** 선택값이 트리거에 칩으로 뜨고, 칩의 × 로 하나씩 뺄 수 있다. 기본(`text`)은 라벨을 쉼표로 이어 한 줄로 말줄임한다. 고른 값 하나하나를 확인하고 바로 빼야 하는 자리면 `chip`, 몇 개 골랐는지만 보이면 되는 조회 필터면 `text` 를 쓴다.
1759
+ - **후보가 정해진 목록에서 고르는 것이면 `SSelect` 의 `chip` 이고, 사용자가 값을 직접 타이핑해 쌓는 것이면 `SChipInput` 이다.** 목록에서 고른 값을 `SChipInput` 에 옮겨 담거나, `SSelect` 옆에 `SChip` 을 따로 나열해 선택값을 흉내 내지 않는다.
1760
+ - **단일 선택 type 에는 주지 않는다.** 무시되고 `text` 로 보인다. 단일 선택의 값 비우기는 `clearable` 이 맡는다.
1761
+ - 칩이 한 줄을 넘으면 줄바꿈하고 트리거가 그만큼 높아진다. 높이·칩 간격은 `SSelect` 가 토큰으로 넣는다 — 직접 주지 않는다.
1762
+ - 전체 선택이면 칩 대신 `allSelectedLabel` 이 뜬다. 비활성 옵션의 칩과 비활성 셀렉트의 칩에는 × 가 없다.
1763
+
1755
1764
  ##### 옵션을 서버에서 받는 동안 — `loading`
1756
1765
 
1757
1766
  - **받는 동안 `loading` 을 켠다.** 트리거의 펼침 아이콘 자리에 스피너가, 드롭다운에 로딩 행이 뜬다. 첫 조회든 다음 페이지든 같은 prop 이다.
@@ -13,6 +13,7 @@
13
13
  | `name?` | `string` | — | 폼 연동용 name 속성 |
14
14
  | `options?` | `SSelectOption[]` | `[]` | 옵션 목록 (children으로 계층 표현) |
15
15
  | `type?` | `SSelectType` | `'default'` | 타입: default | multi | default_depth | multi_depth |
16
+ | `valueDisplay?` | `SSelectValueDisplay` | `'text'` | 트리거에 선택값을 보여주는 방식. - `text`: 선택 라벨을 쉼표로 이어 한 줄로 보여준다. 넘치면 말줄임한다. - `chip`: 선택값마다 칩을 낸다. 칩의 × 를 누르면 그 값만 선택에서 빠지고, 드롭다운은 열리지 않는다. 칩이 한 줄을 넘으면 줄바꿈하고 트리거가 그만큼 높아진다. `chip` 은 `multi`·`multi_depth` 에서만 동작한다. 단일 선택 type 에서는 무시하고 `text` 로 보여준다. 전체 선택이면 칩 대신 `allSelectedLabel` 을 보여준다. 비활성(select 자체나 해당 옵션)인 칩에는 × 를 내지 않는다. |
16
17
  | `showSearch?` | `boolean` | `false` | 검색 입력 사용 |
17
18
  | `showSelectAll?` | `boolean` | `false` | 전체선택 옵션 사용 (multi 전용) |
18
19
  | `allSelectedLabel?` | `string` | `'전체'` | 전체선택 레이블 |
@@ -43,6 +44,7 @@
43
44
  | `hint?` | `string` | — | |
44
45
  | `errorMessage?` | `string` | — | |
45
46
  | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. 셀렉트는 `maxLength` 개념이 없어 목록 최장값을 담는 등급으로 고른다. |
47
+ | `size?` | `SFieldSize` | `'sm'` | 트리거 크기 (기본값 `'sm'`). 높이·패딩·간격·글자 크기를 `--cmp-select-{size}-*` 토큰으로 바꾼다. 드롭다운 목록은 크기와 무관하게 같다. `valueDisplay="chip"` 이면 `md` 는 SChip 과 같은 칩을, `sm` 은 28px 트리거에 맞춘 낮은 칩을 낸다. |
46
48
  | `className?` | `string` | — | |
47
49
  | `style?` | `CSSProperties` | — | |
48
50
 
@@ -95,6 +97,13 @@ export interface SSelectOption {
95
97
  export type SSelectType = 'default' | 'multi' | 'default_depth' | 'multi_depth';
96
98
  ```
97
99
 
100
+ ### SSelectValueDisplay
101
+
102
+ ```ts
103
+ /** 트리거에 선택값을 보여주는 방식 — 쉼표로 이은 텍스트 | 값마다 칩 */
104
+ export type SSelectValueDisplay = 'text' | 'chip';
105
+ ```
106
+
98
107
  ## Dependencies
99
108
 
100
109
  ### Used by
@@ -1,12 +1,12 @@
1
1
  import { type CSSProperties, type ForwardedRef, type ReactElement } from 'react';
2
2
  import { type Rule } from '../../lib/form';
3
- import { type SFieldAddonAlign, type SFieldLabelPosition } from '../SField';
3
+ import { type SFieldAddonAlign, type SFieldLabelPosition, type SFieldSize } from '../SField';
4
4
  import { type SIconName } from '../SIcon';
5
5
  import { type SColor } from '../../lib/color';
6
6
  import { type STooltipProps } from '../STooltip';
7
- import { type SSelectOption, type SSelectType } from './select.config';
7
+ import { type SSelectOption, type SSelectType, type SSelectValueDisplay } from './select.config';
8
8
  import { type SFieldWidth } from '../../lib/field-width';
9
- export type { SSelectOption, SSelectType } from './select.config';
9
+ export type { SSelectOption, SSelectType, SSelectValueDisplay } from './select.config';
10
10
  export interface SSelectHandle {
11
11
  /** 트리거 버튼에 포커스 (sdFocus) */
12
12
  focus: () => void;
@@ -44,6 +44,17 @@ export interface SSelectProps<TValue extends SSelectChangeValue = SSelectChangeV
44
44
  options?: SSelectOption[];
45
45
  /** 타입: default | multi | default_depth | multi_depth */
46
46
  type?: SSelectType;
47
+ /**
48
+ * 트리거에 선택값을 보여주는 방식.
49
+ * - `text`: 선택 라벨을 쉼표로 이어 한 줄로 보여준다. 넘치면 말줄임한다.
50
+ * - `chip`: 선택값마다 칩을 낸다. 칩의 × 를 누르면 그 값만 선택에서 빠지고, 드롭다운은 열리지 않는다.
51
+ * 칩이 한 줄을 넘으면 줄바꿈하고 트리거가 그만큼 높아진다.
52
+ *
53
+ * `chip` 은 `multi`·`multi_depth` 에서만 동작한다. 단일 선택 type 에서는 무시하고 `text` 로 보여준다.
54
+ * 전체 선택이면 칩 대신 `allSelectedLabel` 을 보여준다.
55
+ * 비활성(select 자체나 해당 옵션)인 칩에는 × 를 내지 않는다.
56
+ */
57
+ valueDisplay?: SSelectValueDisplay;
47
58
  /** 검색 입력 사용 */
48
59
  showSearch?: boolean;
49
60
  /** 전체선택 옵션 사용 (multi 전용) */
@@ -158,6 +169,12 @@ export interface SSelectProps<TValue extends SSelectChangeValue = SSelectChangeV
158
169
  * 셀렉트는 `maxLength` 개념이 없어 목록 최장값을 담는 등급으로 고른다.
159
170
  */
160
171
  width?: SFieldWidth;
172
+ /**
173
+ * 트리거 크기 (기본값 `'sm'`). 높이·패딩·간격·글자 크기를 `--cmp-select-{size}-*` 토큰으로 바꾼다.
174
+ * 드롭다운 목록은 크기와 무관하게 같다.
175
+ * `valueDisplay="chip"` 이면 `md` 는 SChip 과 같은 칩을, `sm` 은 28px 트리거에 맞춘 낮은 칩을 낸다.
176
+ */
177
+ size?: SFieldSize;
161
178
  className?: string;
162
179
  style?: CSSProperties;
163
180
  }
@@ -1 +1 @@
1
- export { SSelect, type SSelectHandle, type SSelectProps, type SSelectChangeValue, type SSelectOption, type SSelectType, } from './SSelect';
1
+ export { SSelect, type SSelectHandle, type SSelectProps, type SSelectChangeValue, type SSelectOption, type SSelectType, type SSelectValueDisplay, } from './SSelect';
@@ -5,6 +5,8 @@ export interface SSelectOption {
5
5
  children?: SSelectOption[];
6
6
  }
7
7
  export type SSelectType = 'default' | 'multi' | 'default_depth' | 'multi_depth';
8
+ /** 트리거에 선택값을 보여주는 방식 — 쉼표로 이은 텍스트 | 값마다 칩 */
9
+ export type SSelectValueDisplay = 'text' | 'chip';
8
10
  export interface FlatOption {
9
11
  value: string;
10
12
  label: string;
@@ -55,6 +55,7 @@
55
55
  | `onVirtualUpdate` | `(range: { from: number; to: number }) => void` | |
56
56
  | `onVirtualReachEnd` | `(info: { from: number; to: number; rowCount: number; threshold: number; }) =>…` | |
57
57
  | `onRowClick` | `(row: TRow) => void` | 행 클릭 |
58
+ | `onRowDoubleClick` | `(row: TRow) => void` | 행 더블클릭. 목록에서 상세를 여는 것처럼 **한 번 누르는 것과 다른 일**을 걸 때 쓴다. 더블클릭도 클릭 두 번이라 `onRowClick` 과 함께 주면 **`onRowClick` 이 두 번 먼저 울린다** — 둘을 함께 쓰려면 `onRowClick` 은 두 번 울려도 결과가 같은 일(행 강조 등)이어야 한다. 선택 체크박스 열과 셀 안의 버튼·링크·입력 위에서 한 더블클릭은 행 더블클릭이 아니다 — 체크박스를 빠르게 두 번 누르거나 입력의 글자를 더블클릭으로 고르다 상세가 열리지 않게 한다. |
58
59
 
59
60
  #### Methods (ref)
60
61
 
@@ -400,6 +400,16 @@ export interface STableProps<TRow extends SRow = SRow> {
400
400
  tableId?: string;
401
401
  /** 행 클릭 */
402
402
  onRowClick?: (row: TRow) => void;
403
+ /**
404
+ * 행 더블클릭. 목록에서 상세를 여는 것처럼 **한 번 누르는 것과 다른 일**을 걸 때 쓴다.
405
+ *
406
+ * 더블클릭도 클릭 두 번이라 `onRowClick` 과 함께 주면 **`onRowClick` 이 두 번 먼저 울린다** —
407
+ * 둘을 함께 쓰려면 `onRowClick` 은 두 번 울려도 결과가 같은 일(행 강조 등)이어야 한다.
408
+ *
409
+ * 선택 체크박스 열과 셀 안의 버튼·링크·입력 위에서 한 더블클릭은 행 더블클릭이 아니다 —
410
+ * 체크박스를 빠르게 두 번 누르거나 입력의 글자를 더블클릭으로 고르다 상세가 열리지 않게 한다.
411
+ */
412
+ onRowDoubleClick?: (row: TRow) => void;
403
413
  className?: string;
404
414
  style?: CSSProperties;
405
415
  }
@@ -68,6 +68,7 @@ export type STooltipType = 'default' | 'danger' | 'warning' | 'accent';
68
68
  - [SField](../SField)
69
69
  - [SGnbSystem](../SGnbSystem)
70
70
  - [SKeyValueTable](../SKeyValueTable)
71
+ - [SRadioButton](../SRadioButton)
71
72
  - [SSectionHeaderCard](../SSectionHeaderCard)
72
73
  - [SStepper](../SStepper)
73
74
  - [STable](../STable)
@@ -87,6 +88,7 @@ graph TD;
87
88
  SField --> STooltip
88
89
  SGnbSystem --> STooltip
89
90
  SKeyValueTable --> STooltip
91
+ SRadioButton --> STooltip
90
92
  SSectionHeaderCard --> STooltip
91
93
  SStepper --> STooltip
92
94
  STable --> STooltip
package/dist/index.cjs CHANGED
@@ -15615,6 +15615,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15615
15615
  name,
15616
15616
  options = [],
15617
15617
  type = "default",
15618
+ valueDisplay = "text",
15618
15619
  showSearch = false,
15619
15620
  showSelectAll = false,
15620
15621
  allSelectedLabel = "\uC804\uCCB4",
@@ -15647,6 +15648,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15647
15648
  hint,
15648
15649
  errorMessage,
15649
15650
  width,
15651
+ size = "sm",
15650
15652
  className,
15651
15653
  style
15652
15654
  }, ref) {
@@ -15951,6 +15953,16 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15951
15953
  }
15952
15954
  return labelByValue.get(selected[0]) ?? valueOptions.get(selected[0])?.label ?? selected[0];
15953
15955
  })();
15956
+ const showChips = valueDisplay === "chip" && multi && selected.length > 0 && !allSelected;
15957
+ const disabledValues = react.useMemo(
15958
+ () => new Set(flat.filter((o) => !o.isGroup && o.disabled).map((o) => o.value)),
15959
+ [flat]
15960
+ );
15961
+ const chips = showChips ? selected.map((v) => ({
15962
+ value: v,
15963
+ label: labelByValue.get(v) ?? valueOptions.get(v)?.label ?? v,
15964
+ removable: !effectiveDisabled && !disabledValues.has(v)
15965
+ })) : [];
15954
15966
  const isPlaceholder = selected.length === 0;
15955
15967
  const showTriggerSpinner = loading2 && !effectiveDisabled;
15956
15968
  const showClear = clearable && !effectiveDisabled && !isPlaceholder;
@@ -15958,6 +15970,12 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15958
15970
  emit3([]);
15959
15971
  if (rules && rules.length > 0) setRuleError(runRules(multi ? [] : "", rules));
15960
15972
  };
15973
+ const handleRemoveChip = (target) => {
15974
+ const next = selected.filter((v) => v !== target);
15975
+ emit3(next);
15976
+ if (rules && rules.length > 0) setRuleError(runRules(next, rules));
15977
+ triggerRef.current?.focus();
15978
+ };
15961
15979
  const handleSelect = (nextValue, index, extend = false) => {
15962
15980
  if (multi) {
15963
15981
  const anchor = rangeAnchorRef.current;
@@ -16113,6 +16131,8 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16113
16131
  errorMessage: ruleError !== "" ? ruleError : errorMessage,
16114
16132
  disabled: effectiveDisabled,
16115
16133
  width,
16134
+ size,
16135
+ multiline: showChips,
16116
16136
  className,
16117
16137
  style,
16118
16138
  children: [
@@ -16130,55 +16150,59 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16130
16150
  "aria-busy": showTriggerSpinner || void 0,
16131
16151
  onClick: () => requestOpen(!open),
16132
16152
  onKeyDown: handleKeyDown,
16133
- className: "flex h-full w-full cursor-pointer items-center justify-between gap-[8px] bg-transparent px-[12px] text-left text-[12px] leading-[20px] outline-none disabled:cursor-not-allowed",
16153
+ className: cn(
16154
+ "flex h-full w-full cursor-pointer items-center justify-between bg-transparent text-left outline-none disabled:cursor-not-allowed",
16155
+ size === "md" ? "typo-body-md-default gap-(--cmp-select-md-gap) px-(--cmp-select-md-paddingX)" : "typo-body-sm-default gap-(--cmp-select-sm-gap) px-(--cmp-select-sm-paddingX)"
16156
+ ),
16134
16157
  style: {
16158
+ // 칩 한 줄이 기본 높이 안에서 세로 가운데에 오는 여백. 줄이 늘면 같은 여백을 유지한 채 트리거가 커진다.
16159
+ // sm 칩은 높이가 라벨 줄높이(typo-body-xs-medium)라 남는 높이가 4px 로 떨어진다.
16160
+ paddingBlock: showChips ? size === "md" ? "calc((var(--cmp-select-md-height) - var(--cmp-select-md-borderWidth) * 2 - var(--cmp-chip-height)) / 2)" : "var(--spacing-sd-4)" : void 0,
16135
16161
  color: effectiveDisabled ? "var(--cmp-select-text-disabled)" : isPlaceholder ? "var(--cmp-select-icon-default)" : "var(--cmp-select-text-default)"
16136
16162
  },
16137
16163
  children: [
16138
- isHtmlLabel(triggerText) ? /* @__PURE__ */ jsxRuntime.jsx(
16139
- "span",
16140
- {
16141
- className: "truncate",
16142
- dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(triggerText) }
16143
- }
16144
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: triggerText }),
16145
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex shrink-0 items-center gap-[8px]", children: [
16146
- showClear && /* @__PURE__ */ jsxRuntime.jsxs(
16164
+ showChips ? (
16165
+ // md 는 SChip 과 같은 모양(높이 --cmp-chip-height)이다. SChip 을 그대로 넣지 않는 것은
16166
+ // SChip 의 × 가 <button> 이라 트리거 button 안에 중첩할 수 없어서다 — 모양만 같은 토큰으로 맞춘다.
16167
+ // sm 은 28px 트리거에 들어가도록 낮춘 칩 — 높이를 주지 않고 라벨 줄높이(typo-body-xs-medium)가 정한다.
16168
+ // 두 크기 모두 패딩·라벨↔× 간격은 chip 토큰 그대로.
16169
+ /* @__PURE__ */ jsxRuntime.jsx(
16147
16170
  "span",
16148
16171
  {
16149
- role: "button",
16150
- tabIndex: 0,
16151
- "aria-label": "\uC120\uD0DD \uC9C0\uC6B0\uAE30",
16152
- className: "group/select-clear relative inline-flex shrink-0 cursor-pointer items-center justify-center",
16153
- style: {
16154
- width: "var(--cmp-ghostButton-xxs-size)",
16155
- height: "var(--cmp-ghostButton-xxs-size)",
16156
- borderRadius: "var(--cmp-ghostButton-radius-sm)"
16157
- },
16158
- onClick: (event) => {
16159
- event.preventDefault();
16160
- event.stopPropagation();
16161
- handleClear();
16162
- },
16163
- onKeyDown: (event) => {
16164
- if (event.key !== "Enter" && event.key !== " ") return;
16165
- event.preventDefault();
16166
- event.stopPropagation();
16167
- handleClear();
16168
- },
16169
- children: [
16170
- /* @__PURE__ */ jsxRuntime.jsx(
16171
- "span",
16172
- {
16173
- "aria-hidden": true,
16174
- className: "pointer-events-none absolute inset-0 rounded-[inherit] opacity-0 transition-opacity duration-200 group-hover/select-clear:opacity-5",
16175
- style: { background: "var(--cmp-ghostButton-default-hover-bg)" }
16176
- }
16177
- ),
16178
- /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: "close", size: 12, color: "var(--cmp-ghostButton-default-content)" })
16179
- ]
16172
+ className: cn(
16173
+ "flex min-w-0 flex-1 flex-wrap items-center",
16174
+ size === "md" ? "gap-(--cmp-chipInput-chip-gap)" : "gap-sd-6"
16175
+ ),
16176
+ children: chips.map((chip) => /* @__PURE__ */ jsxRuntime.jsxs(
16177
+ "span",
16178
+ {
16179
+ className: cn(
16180
+ "box-border inline-flex min-w-0 max-w-full items-center gap-(--cmp-chip-gap) overflow-hidden whitespace-nowrap px-(--cmp-chip-paddingX)",
16181
+ size === "md" ? "h-(--cmp-chip-height) text-[12px] font-medium leading-[20px]" : "typo-body-xs-medium"
16182
+ ),
16183
+ style: {
16184
+ borderRadius: "var(--cmp-chip-radius)",
16185
+ background: chip.removable ? "var(--cmp-chip-bg-default)" : "var(--cmp-chip-bg-disabled)",
16186
+ color: chip.removable ? "var(--cmp-chip-content-default)" : "var(--cmp-chip-content-disabled)"
16187
+ },
16188
+ children: [
16189
+ /* @__PURE__ */ jsxRuntime.jsx(TriggerLabel, { label: chip.label }),
16190
+ chip.removable && /* @__PURE__ */ jsxRuntime.jsx(
16191
+ TriggerRemoveButton,
16192
+ {
16193
+ ariaLabel: `${isHtmlLabel(chip.label) ? extractText(chip.label) : chip.label} \uC120\uD0DD \uD574\uC81C`,
16194
+ onRemove: () => handleRemoveChip(chip.value)
16195
+ }
16196
+ )
16197
+ ]
16198
+ },
16199
+ chip.value
16200
+ ))
16180
16201
  }
16181
- ),
16202
+ )
16203
+ ) : /* @__PURE__ */ jsxRuntime.jsx(TriggerLabel, { label: triggerText }),
16204
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex shrink-0 items-center gap-[8px]", children: [
16205
+ showClear && /* @__PURE__ */ jsxRuntime.jsx(TriggerRemoveButton, { ariaLabel: "\uC120\uD0DD \uC9C0\uC6B0\uAE30", onRemove: handleClear }),
16182
16206
  showTriggerSpinner ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "inline-flex shrink-0", "data-select-trigger-spinner": true, children: /* @__PURE__ */ jsxRuntime.jsx(SCircleProgress, { indeterminate: true, type: "neutral", size: 12 }) }) : /* @__PURE__ */ jsxRuntime.jsx(
16183
16207
  SIcon,
16184
16208
  {
@@ -16510,6 +16534,47 @@ function ItemList({
16510
16534
  bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: { height: bottomSpacer } })
16511
16535
  ] });
16512
16536
  }
16537
+ function TriggerLabel({ label }) {
16538
+ return isHtmlLabel(label) ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: label });
16539
+ }
16540
+ function TriggerRemoveButton({ ariaLabel, onRemove }) {
16541
+ return /* @__PURE__ */ jsxRuntime.jsxs(
16542
+ "span",
16543
+ {
16544
+ role: "button",
16545
+ tabIndex: 0,
16546
+ "aria-label": ariaLabel,
16547
+ className: "group/select-remove relative inline-flex shrink-0 cursor-pointer items-center justify-center",
16548
+ style: {
16549
+ width: "var(--cmp-ghostButton-xxs-size)",
16550
+ height: "var(--cmp-ghostButton-xxs-size)",
16551
+ borderRadius: "var(--cmp-ghostButton-radius-sm)"
16552
+ },
16553
+ onClick: (event) => {
16554
+ event.preventDefault();
16555
+ event.stopPropagation();
16556
+ onRemove();
16557
+ },
16558
+ onKeyDown: (event) => {
16559
+ if (event.key !== "Enter" && event.key !== " ") return;
16560
+ event.preventDefault();
16561
+ event.stopPropagation();
16562
+ onRemove();
16563
+ },
16564
+ children: [
16565
+ /* @__PURE__ */ jsxRuntime.jsx(
16566
+ "span",
16567
+ {
16568
+ "aria-hidden": true,
16569
+ className: "pointer-events-none absolute inset-0 rounded-[inherit] opacity-0 transition-opacity duration-200 group-hover/select-remove:opacity-5",
16570
+ style: { background: "var(--cmp-ghostButton-default-hover-bg)" }
16571
+ }
16572
+ ),
16573
+ /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: "close", size: 12, color: "var(--cmp-ghostButton-default-content)" })
16574
+ ]
16575
+ }
16576
+ );
16577
+ }
16513
16578
  function CheckboxBox({
16514
16579
  state: state2,
16515
16580
  disabled = false,
@@ -16614,6 +16679,7 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
16614
16679
  onVirtualReachEnd,
16615
16680
  tableId,
16616
16681
  onRowClick,
16682
+ onRowDoubleClick,
16617
16683
  className,
16618
16684
  style
16619
16685
  }, ref) {
@@ -17466,13 +17532,18 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17466
17532
  {
17467
17533
  "data-row-index": rowIndex,
17468
17534
  onClick: () => onRowClick?.(row),
17535
+ onDoubleClick: onRowDoubleClick ? (event) => {
17536
+ const target = event.target;
17537
+ if (target.closest(ROW_INTERACTIVE_SELECTOR)) return;
17538
+ onRowDoubleClick(row);
17539
+ } : void 0,
17469
17540
  onMouseDown: dragSelectOn ? (event) => startDragSelect(rowIndex, event) : void 0,
17470
17541
  onMouseEnter: dragSelectOn ? () => extendDragSelect(rowIndex) : void 0,
17471
17542
  onContextMenu: contextMenuOn ? (event) => openContextMenu(row, event) : void 0,
17472
17543
  className: cn(
17473
17544
  "group/table-row",
17474
17545
  rowHover && "hover:bg-[var(--color-grey-05)]",
17475
- onRowClick && "cursor-pointer",
17546
+ (onRowClick || onRowDoubleClick) && "cursor-pointer",
17476
17547
  dragSelectOn && "select-none"
17477
17548
  ),
17478
17549
  style: { height: virtualScroll ? effectiveRowHeight : rowH },
@@ -17495,6 +17566,7 @@ var STableImpl = /* @__PURE__ */ react.forwardRef(function STable({
17495
17566
  background: rowLocked ? LOCKED_BG : void 0
17496
17567
  },
17497
17568
  onClick: (e) => e.stopPropagation(),
17569
+ onDoubleClick: (e) => e.stopPropagation(),
17498
17570
  onClickCapture: (e) => {
17499
17571
  shiftPressedRef.current = e.shiftKey;
17500
17572
  if (e.shiftKey) window.getSelection()?.removeAllRanges();
@@ -22566,71 +22638,76 @@ function SRadioButton({
22566
22638
  }) {
22567
22639
  const s = SIZE_CONFIG2[size];
22568
22640
  const [hoveredValue, setHoveredValue] = react.useState(null);
22569
- return /* @__PURE__ */ jsxRuntime.jsx(
22570
- "div",
22571
- {
22572
- role: "radiogroup",
22573
- "aria-disabled": disabled,
22574
- className: cn("inline-flex", className),
22575
- style,
22576
- children: options.map((o, i) => {
22577
- const selected = o.value === value;
22578
- const od = disabled || o.disabled;
22579
- const first = i === 0;
22580
- const last = i === options.length - 1;
22581
- const tooltip = showTooltip && o.tooltip ? o.tooltip : void 0;
22582
- const button = /* @__PURE__ */ jsxRuntime.jsx(
22583
- "button",
22584
- {
22585
- type: "button",
22586
- role: "radio",
22587
- name,
22588
- "aria-checked": selected,
22589
- disabled: od,
22590
- onClick: () => !od && !selected && onValueChange?.(o.value),
22591
- onMouseEnter: tooltip ? () => setHoveredValue(o.value) : void 0,
22592
- onMouseLeave: tooltip ? () => setHoveredValue(null) : void 0,
22593
- onFocus: tooltip ? () => setHoveredValue(o.value) : void 0,
22594
- onBlur: tooltip ? () => setHoveredValue(null) : void 0,
22595
- className: cn(
22596
- "relative box-border grow inline-flex items-center justify-center whitespace-nowrap border border-solid font-medium transition-colors",
22597
- first ? "" : "-ml-px",
22598
- selected ? "z-10" : "",
22599
- !od && !selected && "hover:!bg-[var(--cmp-radioButton-bg-hover)] hover:!text-[color:var(--cmp-radioButton-content-hover)] hover:!border-[color:var(--cmp-radioButton-bg-hover)] hover:z-10",
22600
- od ? "cursor-not-allowed" : "cursor-pointer"
22601
- ),
22602
- style: {
22603
- height: `var(--cmp-radioButton-${size}-height)`,
22604
- padding: `0 var(--cmp-radioButton-${size}-paddingX)`,
22605
- fontSize: `${s.fontSize}px`,
22606
- lineHeight: `${s.lineHeight}px`,
22607
- borderRadius: [
22608
- first ? s.radius : "0",
22609
- last ? s.radius : "0",
22610
- last ? s.radius : "0",
22611
- first ? s.radius : "0"
22612
- ].join(" "),
22613
- background: od ? "var(--cmp-radioButton-bg-disabled)" : "var(--cmp-radioButton-bg-default)",
22614
- borderColor: od ? "var(--cmp-radioButton-border-disabled)" : selected ? "var(--cmp-radioButton-border-select)" : "var(--cmp-radioButton-border-default)",
22615
- color: od ? "var(--cmp-radioButton-content-disabled)" : selected ? "var(--cmp-radioButton-content-select)" : "var(--cmp-radioButton-content-default)"
22641
+ return (
22642
+ // isolate: 선택·hover 버튼의 z-10 은 겹친 테두리(-ml-px) 위로 올리기 위한 값이라 그룹 안에서
22643
+ // 끝나야 한다. 빼면 상위 컨텍스트까지 올라가 모달 안 floating 밴드(z-2)를 이겨, 위에서 연
22644
+ // SSelect listbox 위로 선택된 버튼만 뚫고 그려진다.
22645
+ /* @__PURE__ */ jsxRuntime.jsx(
22646
+ "div",
22647
+ {
22648
+ role: "radiogroup",
22649
+ "aria-disabled": disabled,
22650
+ className: cn("inline-flex isolate", className),
22651
+ style,
22652
+ children: options.map((o, i) => {
22653
+ const selected = o.value === value;
22654
+ const od = disabled || o.disabled;
22655
+ const first = i === 0;
22656
+ const last = i === options.length - 1;
22657
+ const tooltip = showTooltip && o.tooltip ? o.tooltip : void 0;
22658
+ const button = /* @__PURE__ */ jsxRuntime.jsx(
22659
+ "button",
22660
+ {
22661
+ type: "button",
22662
+ role: "radio",
22663
+ name,
22664
+ "aria-checked": selected,
22665
+ disabled: od,
22666
+ onClick: () => !od && !selected && onValueChange?.(o.value),
22667
+ onMouseEnter: tooltip ? () => setHoveredValue(o.value) : void 0,
22668
+ onMouseLeave: tooltip ? () => setHoveredValue(null) : void 0,
22669
+ onFocus: tooltip ? () => setHoveredValue(o.value) : void 0,
22670
+ onBlur: tooltip ? () => setHoveredValue(null) : void 0,
22671
+ className: cn(
22672
+ "relative box-border grow inline-flex items-center justify-center whitespace-nowrap border border-solid font-medium transition-colors",
22673
+ first ? "" : "-ml-px",
22674
+ selected ? "z-10" : "",
22675
+ !od && !selected && "hover:!bg-[var(--cmp-radioButton-bg-hover)] hover:!text-[color:var(--cmp-radioButton-content-hover)] hover:!border-[color:var(--cmp-radioButton-bg-hover)] hover:z-10",
22676
+ od ? "cursor-not-allowed" : "cursor-pointer"
22677
+ ),
22678
+ style: {
22679
+ height: `var(--cmp-radioButton-${size}-height)`,
22680
+ padding: `0 var(--cmp-radioButton-${size}-paddingX)`,
22681
+ fontSize: `${s.fontSize}px`,
22682
+ lineHeight: `${s.lineHeight}px`,
22683
+ borderRadius: [
22684
+ first ? s.radius : "0",
22685
+ last ? s.radius : "0",
22686
+ last ? s.radius : "0",
22687
+ first ? s.radius : "0"
22688
+ ].join(" "),
22689
+ background: od ? "var(--cmp-radioButton-bg-disabled)" : "var(--cmp-radioButton-bg-default)",
22690
+ borderColor: od ? "var(--cmp-radioButton-border-disabled)" : selected ? "var(--cmp-radioButton-border-select)" : "var(--cmp-radioButton-border-default)",
22691
+ color: od ? "var(--cmp-radioButton-content-disabled)" : selected ? "var(--cmp-radioButton-content-select)" : "var(--cmp-radioButton-content-default)"
22692
+ },
22693
+ children: o.label
22616
22694
  },
22617
- children: o.label
22618
- },
22619
- o.value
22620
- );
22621
- return tooltip ? /* @__PURE__ */ jsxRuntime.jsx(
22622
- STooltip2,
22623
- {
22624
- trigger: "none",
22625
- open: hoveredValue === o.value,
22626
- tooltipType: "accent",
22627
- message: [tooltip],
22628
- children: button
22629
- },
22630
- o.value
22631
- ) : button;
22632
- })
22633
- }
22695
+ o.value
22696
+ );
22697
+ return tooltip ? /* @__PURE__ */ jsxRuntime.jsx(
22698
+ STooltip2,
22699
+ {
22700
+ trigger: "none",
22701
+ open: hoveredValue === o.value,
22702
+ tooltipType: "accent",
22703
+ message: [tooltip],
22704
+ children: button
22705
+ },
22706
+ o.value
22707
+ ) : button;
22708
+ })
22709
+ }
22710
+ )
22634
22711
  );
22635
22712
  }
22636
22713
  function SRadioCard({