sellmate-design-system-react 9.0.0-beta.77 → 9.0.0-beta.79

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` 을 보여준다. `multi_depth` 에서 그룹의 하위 리프를 전부 고르면 리프 칩 대신 그룹명 칩 하나로 묶고, 그 칩의 × 는 그룹의 리프를 한꺼번에 뺀다. 비활성(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,19 @@ 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
+ * `multi_depth` 에서 그룹의 하위 리프를 전부 고르면 리프 칩 대신 그룹명 칩 하나로 묶고,
56
+ * 그 칩의 × 는 그룹의 리프를 한꺼번에 뺀다.
57
+ * 비활성(select 자체나 해당 옵션)인 칩에는 × 를 내지 않는다.
58
+ */
59
+ valueDisplay?: SSelectValueDisplay;
47
60
  /** 검색 입력 사용 */
48
61
  showSearch?: boolean;
49
62
  /** 전체선택 옵션 사용 (multi 전용) */
@@ -158,6 +171,12 @@ export interface SSelectProps<TValue extends SSelectChangeValue = SSelectChangeV
158
171
  * 셀렉트는 `maxLength` 개념이 없어 목록 최장값을 담는 등급으로 고른다.
159
172
  */
160
173
  width?: SFieldWidth;
174
+ /**
175
+ * 트리거 크기 (기본값 `'sm'`). 높이·패딩·간격·글자 크기를 `--cmp-select-{size}-*` 토큰으로 바꾼다.
176
+ * 드롭다운 목록은 크기와 무관하게 같다.
177
+ * `valueDisplay="chip"` 이면 `md` 는 SChip 과 같은 칩을, `sm` 은 28px 트리거에 맞춘 낮은 칩을 낸다.
178
+ */
179
+ size?: SFieldSize;
161
180
  className?: string;
162
181
  style?: CSSProperties;
163
182
  }
@@ -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;
@@ -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,51 @@ 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 fullGroupByLeaf = react.useMemo(() => {
15962
+ const map = /* @__PURE__ */ new Map();
15963
+ if (!showChips || type !== "multi_depth") return map;
15964
+ flat.forEach((o, i) => {
15965
+ if (!o.isGroup) return;
15966
+ const leafValues = collectGroupLeafValues(flat, i);
15967
+ if (leafValues.length === 0 || leafValues.some((v) => !selectedSet.has(v))) return;
15968
+ if (map.has(leafValues[0])) return;
15969
+ const group = { value: o.value, label: o.label, leafValues };
15970
+ for (let j = i + 1; j < flat.length && flat[j].depth > o.depth; j++) {
15971
+ if (!flat[j].isGroup && selectedSet.has(flat[j].value)) map.set(flat[j].value, group);
15972
+ }
15973
+ });
15974
+ return map;
15975
+ }, [flat, selectedSet, showChips, type]);
15976
+ const chips = [];
15977
+ if (showChips) {
15978
+ const placedGroups = /* @__PURE__ */ new Set();
15979
+ for (const v of selected) {
15980
+ const group = fullGroupByLeaf.get(v);
15981
+ if (group != null) {
15982
+ if (placedGroups.has(group.value)) continue;
15983
+ placedGroups.add(group.value);
15984
+ chips.push({
15985
+ // 그룹 value 와 리프 value 가 겹쳐도 key 가 부딪히지 않게 구분한다.
15986
+ key: `__group_${group.value}__`,
15987
+ label: group.label,
15988
+ values: group.leafValues,
15989
+ removable: !effectiveDisabled
15990
+ });
15991
+ continue;
15992
+ }
15993
+ chips.push({
15994
+ key: v,
15995
+ label: labelByValue.get(v) ?? valueOptions.get(v)?.label ?? v,
15996
+ values: [v],
15997
+ removable: !effectiveDisabled && !disabledValues.has(v)
15998
+ });
15999
+ }
16000
+ }
15954
16001
  const isPlaceholder = selected.length === 0;
15955
16002
  const showTriggerSpinner = loading2 && !effectiveDisabled;
15956
16003
  const showClear = clearable && !effectiveDisabled && !isPlaceholder;
@@ -15958,6 +16005,13 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
15958
16005
  emit3([]);
15959
16006
  if (rules && rules.length > 0) setRuleError(runRules(multi ? [] : "", rules));
15960
16007
  };
16008
+ const handleRemoveChip = (targets) => {
16009
+ const remove = new Set(targets);
16010
+ const next = selected.filter((v) => !remove.has(v));
16011
+ emit3(next);
16012
+ if (rules && rules.length > 0) setRuleError(runRules(next, rules));
16013
+ triggerRef.current?.focus();
16014
+ };
15961
16015
  const handleSelect = (nextValue, index, extend = false) => {
15962
16016
  if (multi) {
15963
16017
  const anchor = rangeAnchorRef.current;
@@ -16113,6 +16167,8 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16113
16167
  errorMessage: ruleError !== "" ? ruleError : errorMessage,
16114
16168
  disabled: effectiveDisabled,
16115
16169
  width,
16170
+ size,
16171
+ multiline: showChips,
16116
16172
  className,
16117
16173
  style,
16118
16174
  children: [
@@ -16130,55 +16186,59 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16130
16186
  "aria-busy": showTriggerSpinner || void 0,
16131
16187
  onClick: () => requestOpen(!open),
16132
16188
  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",
16189
+ className: cn(
16190
+ "flex h-full w-full cursor-pointer items-center justify-between bg-transparent text-left outline-none disabled:cursor-not-allowed",
16191
+ 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)"
16192
+ ),
16134
16193
  style: {
16194
+ // 칩 한 줄이 기본 높이 안에서 세로 가운데에 오는 여백. 줄이 늘면 같은 여백을 유지한 채 트리거가 커진다.
16195
+ // sm 칩은 높이가 라벨 줄높이(typo-body-xs-medium)라 남는 높이가 4px 로 떨어진다.
16196
+ 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
16197
  color: effectiveDisabled ? "var(--cmp-select-text-disabled)" : isPlaceholder ? "var(--cmp-select-icon-default)" : "var(--cmp-select-text-default)"
16136
16198
  },
16137
16199
  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(
16200
+ showChips ? (
16201
+ // md 는 SChip 과 같은 모양(높이 --cmp-chip-height)이다. SChip 을 그대로 넣지 않는 것은
16202
+ // SChip 의 × 가 <button> 이라 트리거 button 안에 중첩할 수 없어서다 — 모양만 같은 토큰으로 맞춘다.
16203
+ // sm 은 28px 트리거에 들어가도록 낮춘 칩 — 높이를 주지 않고 라벨 줄높이(typo-body-xs-medium)가 정한다.
16204
+ // 두 크기 모두 패딩·라벨↔× 간격은 chip 토큰 그대로.
16205
+ /* @__PURE__ */ jsxRuntime.jsx(
16147
16206
  "span",
16148
16207
  {
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
- ]
16208
+ className: cn(
16209
+ "flex min-w-0 flex-1 flex-wrap items-center",
16210
+ size === "md" ? "gap-(--cmp-chipInput-chip-gap)" : "gap-sd-6"
16211
+ ),
16212
+ children: chips.map((chip) => /* @__PURE__ */ jsxRuntime.jsxs(
16213
+ "span",
16214
+ {
16215
+ className: cn(
16216
+ "box-border inline-flex min-w-0 max-w-full items-center gap-(--cmp-chip-gap) overflow-hidden whitespace-nowrap px-(--cmp-chip-paddingX)",
16217
+ size === "md" ? "h-(--cmp-chip-height) text-[12px] font-medium leading-[20px]" : "typo-body-xs-medium"
16218
+ ),
16219
+ style: {
16220
+ borderRadius: "var(--cmp-chip-radius)",
16221
+ background: chip.removable ? "var(--cmp-chip-bg-default)" : "var(--cmp-chip-bg-disabled)",
16222
+ color: chip.removable ? "var(--cmp-chip-content-default)" : "var(--cmp-chip-content-disabled)"
16223
+ },
16224
+ children: [
16225
+ /* @__PURE__ */ jsxRuntime.jsx(TriggerLabel, { label: chip.label }),
16226
+ chip.removable && /* @__PURE__ */ jsxRuntime.jsx(
16227
+ TriggerRemoveButton,
16228
+ {
16229
+ ariaLabel: `${isHtmlLabel(chip.label) ? extractText(chip.label) : chip.label} \uC120\uD0DD \uD574\uC81C`,
16230
+ onRemove: () => handleRemoveChip(chip.values)
16231
+ }
16232
+ )
16233
+ ]
16234
+ },
16235
+ chip.key
16236
+ ))
16180
16237
  }
16181
- ),
16238
+ )
16239
+ ) : /* @__PURE__ */ jsxRuntime.jsx(TriggerLabel, { label: triggerText }),
16240
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex shrink-0 items-center gap-[8px]", children: [
16241
+ showClear && /* @__PURE__ */ jsxRuntime.jsx(TriggerRemoveButton, { ariaLabel: "\uC120\uD0DD \uC9C0\uC6B0\uAE30", onRemove: handleClear }),
16182
16242
  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
16243
  SIcon,
16184
16244
  {
@@ -16510,6 +16570,47 @@ function ItemList({
16510
16570
  bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: { height: bottomSpacer } })
16511
16571
  ] });
16512
16572
  }
16573
+ function TriggerLabel({ label }) {
16574
+ return isHtmlLabel(label) ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: label });
16575
+ }
16576
+ function TriggerRemoveButton({ ariaLabel, onRemove }) {
16577
+ return /* @__PURE__ */ jsxRuntime.jsxs(
16578
+ "span",
16579
+ {
16580
+ role: "button",
16581
+ tabIndex: 0,
16582
+ "aria-label": ariaLabel,
16583
+ className: "group/select-remove relative inline-flex shrink-0 cursor-pointer items-center justify-center",
16584
+ style: {
16585
+ width: "var(--cmp-ghostButton-xxs-size)",
16586
+ height: "var(--cmp-ghostButton-xxs-size)",
16587
+ borderRadius: "var(--cmp-ghostButton-radius-sm)"
16588
+ },
16589
+ onClick: (event) => {
16590
+ event.preventDefault();
16591
+ event.stopPropagation();
16592
+ onRemove();
16593
+ },
16594
+ onKeyDown: (event) => {
16595
+ if (event.key !== "Enter" && event.key !== " ") return;
16596
+ event.preventDefault();
16597
+ event.stopPropagation();
16598
+ onRemove();
16599
+ },
16600
+ children: [
16601
+ /* @__PURE__ */ jsxRuntime.jsx(
16602
+ "span",
16603
+ {
16604
+ "aria-hidden": true,
16605
+ className: "pointer-events-none absolute inset-0 rounded-[inherit] opacity-0 transition-opacity duration-200 group-hover/select-remove:opacity-5",
16606
+ style: { background: "var(--cmp-ghostButton-default-hover-bg)" }
16607
+ }
16608
+ ),
16609
+ /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: "close", size: 12, color: "var(--cmp-ghostButton-default-content)" })
16610
+ ]
16611
+ }
16612
+ );
16613
+ }
16513
16614
  function CheckboxBox({
16514
16615
  state: state2,
16515
16616
  disabled = false,
@@ -22573,71 +22674,76 @@ function SRadioButton({
22573
22674
  }) {
22574
22675
  const s = SIZE_CONFIG2[size];
22575
22676
  const [hoveredValue, setHoveredValue] = react.useState(null);
22576
- return /* @__PURE__ */ jsxRuntime.jsx(
22577
- "div",
22578
- {
22579
- role: "radiogroup",
22580
- "aria-disabled": disabled,
22581
- className: cn("inline-flex", className),
22582
- style,
22583
- children: options.map((o, i) => {
22584
- const selected = o.value === value;
22585
- const od = disabled || o.disabled;
22586
- const first = i === 0;
22587
- const last = i === options.length - 1;
22588
- const tooltip = showTooltip && o.tooltip ? o.tooltip : void 0;
22589
- const button = /* @__PURE__ */ jsxRuntime.jsx(
22590
- "button",
22591
- {
22592
- type: "button",
22593
- role: "radio",
22594
- name,
22595
- "aria-checked": selected,
22596
- disabled: od,
22597
- onClick: () => !od && !selected && onValueChange?.(o.value),
22598
- onMouseEnter: tooltip ? () => setHoveredValue(o.value) : void 0,
22599
- onMouseLeave: tooltip ? () => setHoveredValue(null) : void 0,
22600
- onFocus: tooltip ? () => setHoveredValue(o.value) : void 0,
22601
- onBlur: tooltip ? () => setHoveredValue(null) : void 0,
22602
- className: cn(
22603
- "relative box-border grow inline-flex items-center justify-center whitespace-nowrap border border-solid font-medium transition-colors",
22604
- first ? "" : "-ml-px",
22605
- selected ? "z-10" : "",
22606
- !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",
22607
- od ? "cursor-not-allowed" : "cursor-pointer"
22608
- ),
22609
- style: {
22610
- height: `var(--cmp-radioButton-${size}-height)`,
22611
- padding: `0 var(--cmp-radioButton-${size}-paddingX)`,
22612
- fontSize: `${s.fontSize}px`,
22613
- lineHeight: `${s.lineHeight}px`,
22614
- borderRadius: [
22615
- first ? s.radius : "0",
22616
- last ? s.radius : "0",
22617
- last ? s.radius : "0",
22618
- first ? s.radius : "0"
22619
- ].join(" "),
22620
- background: od ? "var(--cmp-radioButton-bg-disabled)" : "var(--cmp-radioButton-bg-default)",
22621
- borderColor: od ? "var(--cmp-radioButton-border-disabled)" : selected ? "var(--cmp-radioButton-border-select)" : "var(--cmp-radioButton-border-default)",
22622
- color: od ? "var(--cmp-radioButton-content-disabled)" : selected ? "var(--cmp-radioButton-content-select)" : "var(--cmp-radioButton-content-default)"
22677
+ return (
22678
+ // isolate: 선택·hover 버튼의 z-10 은 겹친 테두리(-ml-px) 위로 올리기 위한 값이라 그룹 안에서
22679
+ // 끝나야 한다. 빼면 상위 컨텍스트까지 올라가 모달 안 floating 밴드(z-2)를 이겨, 위에서 연
22680
+ // SSelect listbox 위로 선택된 버튼만 뚫고 그려진다.
22681
+ /* @__PURE__ */ jsxRuntime.jsx(
22682
+ "div",
22683
+ {
22684
+ role: "radiogroup",
22685
+ "aria-disabled": disabled,
22686
+ className: cn("inline-flex isolate", className),
22687
+ style,
22688
+ children: options.map((o, i) => {
22689
+ const selected = o.value === value;
22690
+ const od = disabled || o.disabled;
22691
+ const first = i === 0;
22692
+ const last = i === options.length - 1;
22693
+ const tooltip = showTooltip && o.tooltip ? o.tooltip : void 0;
22694
+ const button = /* @__PURE__ */ jsxRuntime.jsx(
22695
+ "button",
22696
+ {
22697
+ type: "button",
22698
+ role: "radio",
22699
+ name,
22700
+ "aria-checked": selected,
22701
+ disabled: od,
22702
+ onClick: () => !od && !selected && onValueChange?.(o.value),
22703
+ onMouseEnter: tooltip ? () => setHoveredValue(o.value) : void 0,
22704
+ onMouseLeave: tooltip ? () => setHoveredValue(null) : void 0,
22705
+ onFocus: tooltip ? () => setHoveredValue(o.value) : void 0,
22706
+ onBlur: tooltip ? () => setHoveredValue(null) : void 0,
22707
+ className: cn(
22708
+ "relative box-border grow inline-flex items-center justify-center whitespace-nowrap border border-solid font-medium transition-colors",
22709
+ first ? "" : "-ml-px",
22710
+ selected ? "z-10" : "",
22711
+ !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",
22712
+ od ? "cursor-not-allowed" : "cursor-pointer"
22713
+ ),
22714
+ style: {
22715
+ height: `var(--cmp-radioButton-${size}-height)`,
22716
+ padding: `0 var(--cmp-radioButton-${size}-paddingX)`,
22717
+ fontSize: `${s.fontSize}px`,
22718
+ lineHeight: `${s.lineHeight}px`,
22719
+ borderRadius: [
22720
+ first ? s.radius : "0",
22721
+ last ? s.radius : "0",
22722
+ last ? s.radius : "0",
22723
+ first ? s.radius : "0"
22724
+ ].join(" "),
22725
+ background: od ? "var(--cmp-radioButton-bg-disabled)" : "var(--cmp-radioButton-bg-default)",
22726
+ borderColor: od ? "var(--cmp-radioButton-border-disabled)" : selected ? "var(--cmp-radioButton-border-select)" : "var(--cmp-radioButton-border-default)",
22727
+ color: od ? "var(--cmp-radioButton-content-disabled)" : selected ? "var(--cmp-radioButton-content-select)" : "var(--cmp-radioButton-content-default)"
22728
+ },
22729
+ children: o.label
22623
22730
  },
22624
- children: o.label
22625
- },
22626
- o.value
22627
- );
22628
- return tooltip ? /* @__PURE__ */ jsxRuntime.jsx(
22629
- STooltip2,
22630
- {
22631
- trigger: "none",
22632
- open: hoveredValue === o.value,
22633
- tooltipType: "accent",
22634
- message: [tooltip],
22635
- children: button
22636
- },
22637
- o.value
22638
- ) : button;
22639
- })
22640
- }
22731
+ o.value
22732
+ );
22733
+ return tooltip ? /* @__PURE__ */ jsxRuntime.jsx(
22734
+ STooltip2,
22735
+ {
22736
+ trigger: "none",
22737
+ open: hoveredValue === o.value,
22738
+ tooltipType: "accent",
22739
+ message: [tooltip],
22740
+ children: button
22741
+ },
22742
+ o.value
22743
+ ) : button;
22744
+ })
22745
+ }
22746
+ )
22641
22747
  );
22642
22748
  }
22643
22749
  function SRadioCard({