sellmate-design-system-react 0.1.10 → 0.2.0

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.
Files changed (163) hide show
  1. package/dist/components/SActionModal/SActionModal.cjs +10 -4
  2. package/dist/components/SActionModal/SActionModal.cjs.map +1 -1
  3. package/dist/components/SActionModal/SActionModal.js +10 -4
  4. package/dist/components/SActionModal/SActionModal.js.map +1 -1
  5. package/dist/components/SBarcodeInput/SBarcodeInput.cjs +22 -5
  6. package/dist/components/SBarcodeInput/SBarcodeInput.cjs.map +1 -1
  7. package/dist/components/SBarcodeInput/SBarcodeInput.d.ts +3 -1
  8. package/dist/components/SBarcodeInput/SBarcodeInput.js +22 -5
  9. package/dist/components/SBarcodeInput/SBarcodeInput.js.map +1 -1
  10. package/dist/components/SCalendar/SCalendar.cjs +9 -3
  11. package/dist/components/SCalendar/SCalendar.cjs.map +1 -1
  12. package/dist/components/SCalendar/SCalendar.js +9 -3
  13. package/dist/components/SCalendar/SCalendar.js.map +1 -1
  14. package/dist/components/SChip/SChip.cjs +9 -3
  15. package/dist/components/SChip/SChip.cjs.map +1 -1
  16. package/dist/components/SChip/SChip.js +9 -3
  17. package/dist/components/SChip/SChip.js.map +1 -1
  18. package/dist/components/SChipInput/SChipInput.cjs +44 -13
  19. package/dist/components/SChipInput/SChipInput.cjs.map +1 -1
  20. package/dist/components/SChipInput/SChipInput.d.ts +5 -0
  21. package/dist/components/SChipInput/SChipInput.js +44 -13
  22. package/dist/components/SChipInput/SChipInput.js.map +1 -1
  23. package/dist/components/SChipInput/index.d.ts +1 -1
  24. package/dist/components/SCircleProgress/SCircleProgress.cjs +6 -2
  25. package/dist/components/SCircleProgress/SCircleProgress.cjs.map +1 -1
  26. package/dist/components/SCircleProgress/SCircleProgress.js +6 -2
  27. package/dist/components/SCircleProgress/SCircleProgress.js.map +1 -1
  28. package/dist/components/SConfirmModal/SConfirmModal.cjs +20 -13
  29. package/dist/components/SConfirmModal/SConfirmModal.cjs.map +1 -1
  30. package/dist/components/SConfirmModal/SConfirmModal.d.ts +7 -4
  31. package/dist/components/SConfirmModal/SConfirmModal.js +20 -13
  32. package/dist/components/SConfirmModal/SConfirmModal.js.map +1 -1
  33. package/dist/components/SDatePicker/SDatePicker.cjs +22 -5
  34. package/dist/components/SDatePicker/SDatePicker.cjs.map +1 -1
  35. package/dist/components/SDatePicker/SDatePicker.d.ts +3 -1
  36. package/dist/components/SDatePicker/SDatePicker.js +22 -5
  37. package/dist/components/SDatePicker/SDatePicker.js.map +1 -1
  38. package/dist/components/SDateRangePicker/SDateRangePicker.cjs +22 -5
  39. package/dist/components/SDateRangePicker/SDateRangePicker.cjs.map +1 -1
  40. package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +3 -1
  41. package/dist/components/SDateRangePicker/SDateRangePicker.js +22 -5
  42. package/dist/components/SDateRangePicker/SDateRangePicker.js.map +1 -1
  43. package/dist/components/SDropdownButton/SDropdownButton.cjs +3 -1
  44. package/dist/components/SDropdownButton/SDropdownButton.cjs.map +1 -1
  45. package/dist/components/SDropdownButton/SDropdownButton.js +3 -1
  46. package/dist/components/SDropdownButton/SDropdownButton.js.map +1 -1
  47. package/dist/components/SField/SField.cjs +20 -5
  48. package/dist/components/SField/SField.cjs.map +1 -1
  49. package/dist/components/SField/SField.d.ts +4 -1
  50. package/dist/components/SField/SField.js +20 -5
  51. package/dist/components/SField/SField.js.map +1 -1
  52. package/dist/components/SFilePicker/SFilePicker.cjs +23 -6
  53. package/dist/components/SFilePicker/SFilePicker.cjs.map +1 -1
  54. package/dist/components/SFilePicker/SFilePicker.d.ts +3 -1
  55. package/dist/components/SFilePicker/SFilePicker.js +23 -6
  56. package/dist/components/SFilePicker/SFilePicker.js.map +1 -1
  57. package/dist/components/SGhostButton/SGhostButton.cjs +9 -3
  58. package/dist/components/SGhostButton/SGhostButton.cjs.map +1 -1
  59. package/dist/components/SGhostButton/SGhostButton.d.ts +5 -1
  60. package/dist/components/SGhostButton/SGhostButton.js +9 -3
  61. package/dist/components/SGhostButton/SGhostButton.js.map +1 -1
  62. package/dist/components/SGnb/SGnb.cjs +3297 -0
  63. package/dist/components/SGnb/SGnb.cjs.map +1 -0
  64. package/dist/components/SGnb/SGnb.d.ts +35 -0
  65. package/dist/components/SGnb/SGnb.js +3295 -0
  66. package/dist/components/SGnb/SGnb.js.map +1 -0
  67. package/dist/components/SGnb/gnb.config.d.ts +63 -0
  68. package/dist/components/SGnb/index.d.ts +2 -0
  69. package/dist/components/SGuide/SGuide.cjs +9 -3
  70. package/dist/components/SGuide/SGuide.cjs.map +1 -1
  71. package/dist/components/SGuide/SGuide.js +9 -3
  72. package/dist/components/SGuide/SGuide.js.map +1 -1
  73. package/dist/components/SInput/SInput.cjs +22 -5
  74. package/dist/components/SInput/SInput.cjs.map +1 -1
  75. package/dist/components/SInput/SInput.d.ts +4 -1
  76. package/dist/components/SInput/SInput.js +22 -5
  77. package/dist/components/SInput/SInput.js.map +1 -1
  78. package/dist/components/SKeyValueTable/SKeyValueTable.cjs +511 -209
  79. package/dist/components/SKeyValueTable/SKeyValueTable.cjs.map +1 -1
  80. package/dist/components/SKeyValueTable/SKeyValueTable.d.ts +27 -6
  81. package/dist/components/SKeyValueTable/SKeyValueTable.js +510 -208
  82. package/dist/components/SKeyValueTable/SKeyValueTable.js.map +1 -1
  83. package/dist/components/SLayout/SLayout.cjs +79 -0
  84. package/dist/components/SLayout/SLayout.cjs.map +1 -0
  85. package/dist/components/SLayout/SLayout.d.ts +41 -0
  86. package/dist/components/SLayout/SLayout.js +75 -0
  87. package/dist/components/SLayout/SLayout.js.map +1 -0
  88. package/dist/components/SLayout/index.d.ts +1 -0
  89. package/dist/components/SLoadingContainer/SLoadingContainer.cjs +6 -2
  90. package/dist/components/SLoadingContainer/SLoadingContainer.cjs.map +1 -1
  91. package/dist/components/SLoadingContainer/SLoadingContainer.js +6 -2
  92. package/dist/components/SLoadingContainer/SLoadingContainer.js.map +1 -1
  93. package/dist/components/SLoadingContainer/loading.cjs +6 -2
  94. package/dist/components/SLoadingContainer/loading.cjs.map +1 -1
  95. package/dist/components/SLoadingContainer/loading.js +6 -2
  96. package/dist/components/SLoadingContainer/loading.js.map +1 -1
  97. package/dist/components/SLoadingModal/SLoadingModal.cjs +16 -6
  98. package/dist/components/SLoadingModal/SLoadingModal.cjs.map +1 -1
  99. package/dist/components/SLoadingModal/SLoadingModal.js +16 -6
  100. package/dist/components/SLoadingModal/SLoadingModal.js.map +1 -1
  101. package/dist/components/SModalContainer/SModalContainer.cjs +10 -4
  102. package/dist/components/SModalContainer/SModalContainer.cjs.map +1 -1
  103. package/dist/components/SModalContainer/SModalContainer.d.ts +10 -0
  104. package/dist/components/SModalContainer/SModalContainer.js +10 -4
  105. package/dist/components/SModalContainer/SModalContainer.js.map +1 -1
  106. package/dist/components/SNumberInput/SNumberInput.cjs +22 -5
  107. package/dist/components/SNumberInput/SNumberInput.cjs.map +1 -1
  108. package/dist/components/SNumberInput/SNumberInput.d.ts +2 -0
  109. package/dist/components/SNumberInput/SNumberInput.js +22 -5
  110. package/dist/components/SNumberInput/SNumberInput.js.map +1 -1
  111. package/dist/components/SPage/SPage.cjs +86 -0
  112. package/dist/components/SPage/SPage.cjs.map +1 -0
  113. package/dist/components/SPage/SPage.d.ts +13 -0
  114. package/dist/components/SPage/SPage.js +84 -0
  115. package/dist/components/SPage/SPage.js.map +1 -0
  116. package/dist/components/SPage/index.d.ts +1 -0
  117. package/dist/components/SPopover/SPopover.cjs +9 -3
  118. package/dist/components/SPopover/SPopover.cjs.map +1 -1
  119. package/dist/components/SPopover/SPopover.js +9 -3
  120. package/dist/components/SPopover/SPopover.js.map +1 -1
  121. package/dist/components/SPortal/SPortal.cjs +3 -1
  122. package/dist/components/SPortal/SPortal.cjs.map +1 -1
  123. package/dist/components/SPortal/SPortal.js +3 -1
  124. package/dist/components/SPortal/SPortal.js.map +1 -1
  125. package/dist/components/SSelect/SSelect.cjs +545 -265
  126. package/dist/components/SSelect/SSelect.cjs.map +1 -1
  127. package/dist/components/SSelect/SSelect.d.ts +4 -2
  128. package/dist/components/SSelect/SSelect.js +546 -265
  129. package/dist/components/SSelect/SSelect.js.map +1 -1
  130. package/dist/components/STable/STable.cjs +552 -268
  131. package/dist/components/STable/STable.cjs.map +1 -1
  132. package/dist/components/STable/STable.js +553 -268
  133. package/dist/components/STable/STable.js.map +1 -1
  134. package/dist/components/STextLink/STextLink.cjs +2 -0
  135. package/dist/components/STextLink/STextLink.cjs.map +1 -1
  136. package/dist/components/STextLink/STextLink.js +2 -0
  137. package/dist/components/STextLink/STextLink.js.map +1 -1
  138. package/dist/components/STextarea/STextarea.cjs +22 -5
  139. package/dist/components/STextarea/STextarea.cjs.map +1 -1
  140. package/dist/components/STextarea/STextarea.d.ts +2 -0
  141. package/dist/components/STextarea/STextarea.js +22 -5
  142. package/dist/components/STextarea/STextarea.js.map +1 -1
  143. package/dist/components/SToast/SToast.cjs +9 -3
  144. package/dist/components/SToast/SToast.cjs.map +1 -1
  145. package/dist/components/SToast/SToast.js +9 -3
  146. package/dist/components/SToast/SToast.js.map +1 -1
  147. package/dist/components/SToast/SToastContainer.cjs +9 -3
  148. package/dist/components/SToast/SToastContainer.cjs.map +1 -1
  149. package/dist/components/SToast/SToastContainer.js +9 -3
  150. package/dist/components/SToast/SToastContainer.js.map +1 -1
  151. package/dist/components/STooltip/STooltip.cjs +9 -3
  152. package/dist/components/STooltip/STooltip.cjs.map +1 -1
  153. package/dist/components/STooltip/STooltip.js +9 -3
  154. package/dist/components/STooltip/STooltip.js.map +1 -1
  155. package/dist/index.cjs +840 -199
  156. package/dist/index.cjs.map +1 -1
  157. package/dist/index.d.ts +3 -1
  158. package/dist/index.js +832 -200
  159. package/dist/index.js.map +1 -1
  160. package/dist/lib/portal-container.d.ts +7 -0
  161. package/dist/styles.css +253 -63
  162. package/dist/theme.css +66 -13
  163. package/package.json +1 -1
@@ -2,7 +2,6 @@
2
2
  'use strict';
3
3
 
4
4
  var react = require('react');
5
- var Ariakit = require('@ariakit/react');
6
5
  var clsx = require('clsx');
7
6
  var tailwindMerge = require('tailwind-merge');
8
7
  var jsxRuntime = require('react/jsx-runtime');
@@ -26,7 +25,6 @@ function _interopNamespace(e) {
26
25
  return Object.freeze(n);
27
26
  }
28
27
 
29
- var Ariakit__namespace = /*#__PURE__*/_interopNamespace(Ariakit);
30
28
  var RPopover2__namespace = /*#__PURE__*/_interopNamespace(RPopover2);
31
29
 
32
30
  var twMerge = tailwindMerge.extendTailwindMerge({
@@ -40,10 +38,110 @@ function cn(...inputs) {
40
38
  return twMerge(clsx.clsx(inputs));
41
39
  }
42
40
  var PortalContainerContext = react.createContext(null);
41
+ var InsideModalContext = react.createContext(false);
43
42
  PortalContainerContext.Provider;
43
+ InsideModalContext.Provider;
44
44
  function usePortalContainer() {
45
45
  return react.useContext(PortalContainerContext) ?? void 0;
46
46
  }
47
+ function useInsideModal() {
48
+ return react.useContext(InsideModalContext);
49
+ }
50
+ function runRules(value, rules) {
51
+ if (!rules || rules.length === 0) return "";
52
+ for (const rule of rules) {
53
+ const result = rule(value ?? "");
54
+ if (result !== true) return typeof result === "string" ? result : "\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC12\uC785\uB2C8\uB2E4.";
55
+ }
56
+ return "";
57
+ }
58
+ var FormContext = react.createContext(null);
59
+ function useFormContext() {
60
+ return react.useContext(FormContext);
61
+ }
62
+ function useFormField(field) {
63
+ const ctx = useFormContext();
64
+ const cbRef = react.useRef(field);
65
+ cbRef.current = field;
66
+ const name = field.name ?? "";
67
+ react.useEffect(() => {
68
+ if (!ctx) return;
69
+ return ctx.register({
70
+ name,
71
+ validate: () => cbRef.current.validate(),
72
+ resetValidation: () => cbRef.current.resetValidation()
73
+ });
74
+ }, [ctx, name]);
75
+ }
76
+
77
+ // src/lib/sanitize-inline-html.ts
78
+ var ALLOWED_INLINE_TAGS = /* @__PURE__ */ new Set(["A", "B", "STRONG", "I", "EM", "BR", "SPAN"]);
79
+ var ALLOWED_ATTRS_BY_TAG = {
80
+ A: /* @__PURE__ */ new Set(["href", "target", "rel", "class"])
81
+ };
82
+ var COMMON_ALLOWED_ATTRS = /* @__PURE__ */ new Set(["class"]);
83
+ var SAFE_HREF_RE = /^(https?:|mailto:|tel:|\/|#|\.)/i;
84
+ var DROP_CONTENT_TAGS = /* @__PURE__ */ new Set([
85
+ "SCRIPT",
86
+ "STYLE",
87
+ "IFRAME",
88
+ "OBJECT",
89
+ "EMBED",
90
+ "META",
91
+ "LINK",
92
+ "BASE",
93
+ "NOSCRIPT",
94
+ // SVG/MATH 는 foreign-content 파싱 모드로 전환되어 재직렬화 시 다른 트리로
95
+ // 재파싱될 수 있다(mXSS 벡터). 인라인 라벨에 필요 없으므로 통째로 제거한다.
96
+ "SVG",
97
+ "MATH",
98
+ "TEMPLATE"
99
+ ]);
100
+ var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
101
+ var sanitizeNode = (node) => {
102
+ if (node.nodeType === Node.COMMENT_NODE) {
103
+ node.remove();
104
+ return;
105
+ }
106
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
107
+ const element = node;
108
+ const tagName = element.tagName;
109
+ if (DROP_CONTENT_TAGS.has(tagName)) {
110
+ element.remove();
111
+ return;
112
+ }
113
+ Array.from(element.childNodes).forEach((child) => sanitizeNode(child));
114
+ if (ALLOWED_INLINE_TAGS.has(tagName)) {
115
+ if (element.hasAttribute("className")) {
116
+ const val = element.getAttribute("className");
117
+ element.removeAttribute("className");
118
+ if (!element.hasAttribute("class")) element.setAttribute("class", val);
119
+ }
120
+ const allowedAttrs = ALLOWED_ATTRS_BY_TAG[tagName] ?? COMMON_ALLOWED_ATTRS;
121
+ Array.from(element.attributes).forEach((attr) => {
122
+ if (!allowedAttrs.has(attr.name)) element.removeAttribute(attr.name);
123
+ });
124
+ if (tagName === "A") {
125
+ const href = element.getAttribute("href");
126
+ if (href !== null && href !== "" && !SAFE_HREF_RE.test(href)) element.removeAttribute("href");
127
+ if (element.hasAttribute("target")) {
128
+ element.setAttribute("rel", "noopener noreferrer");
129
+ }
130
+ }
131
+ return;
132
+ }
133
+ const parent = element.parentNode;
134
+ if (parent == null) return;
135
+ while (element.firstChild) parent.insertBefore(element.firstChild, element);
136
+ parent.removeChild(element);
137
+ };
138
+ var sanitizeInlineHtml = (value) => {
139
+ if (typeof document === "undefined") return escapeHtml(value);
140
+ const template = document.createElement("template");
141
+ template.innerHTML = value;
142
+ Array.from(template.content.childNodes).forEach((child) => sanitizeNode(child));
143
+ return template.innerHTML;
144
+ };
47
145
 
48
146
  // src/lib/color.ts
49
147
  var COLOR_KEYS = [
@@ -2996,6 +3094,8 @@ function SGhostButton({
2996
3094
  tooltipText,
2997
3095
  disabled = false,
2998
3096
  onClick,
3097
+ anchorClassName,
3098
+ anchorStyle,
2999
3099
  className,
3000
3100
  style
3001
3101
  }) {
@@ -3018,6 +3118,8 @@ function SGhostButton({
3018
3118
  };
3019
3119
  const s = SIZE[size];
3020
3120
  const contentColor = disabled ? intent === "inverse" ? "var(--cmp-ghostButton-inverse-disabled)" : "var(--cmp-ghostButton-icon-disabled)" : `var(--cmp-ghostButton-${intent}-content)`;
3121
+ const text = (tooltipText ?? "").trim();
3122
+ const hasTooltip = text !== "";
3021
3123
  const btn = /* @__PURE__ */ jsxRuntime.jsxs(
3022
3124
  "button",
3023
3125
  {
@@ -3058,13 +3160,13 @@ function SGhostButton({
3058
3160
  ]
3059
3161
  }
3060
3162
  );
3061
- const text = (tooltipText ?? "").trim();
3062
- if (text === "") return btn;
3163
+ if (!hasTooltip) return btn;
3063
3164
  return /* @__PURE__ */ jsxRuntime.jsxs(RPopover2__namespace.Root, { open, onOpenChange: setOpen, children: [
3064
3165
  /* @__PURE__ */ jsxRuntime.jsx(RPopover2__namespace.Anchor, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
3065
3166
  "span",
3066
3167
  {
3067
- className: "inline-flex",
3168
+ className: cn("inline-flex", anchorClassName),
3169
+ style: anchorStyle,
3068
3170
  onMouseEnter: openNow,
3069
3171
  onMouseLeave: startHide,
3070
3172
  onFocus: openNow,
@@ -3216,32 +3318,6 @@ var STooltip = react.forwardRef(function STooltip2({
3216
3318
  ] });
3217
3319
  });
3218
3320
  STooltip.displayName = "STooltip";
3219
- function runRules(value, rules) {
3220
- if (!rules || rules.length === 0) return "";
3221
- for (const rule of rules) {
3222
- const result = rule(value ?? "");
3223
- if (result !== true) return typeof result === "string" ? result : "\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC12\uC785\uB2C8\uB2E4.";
3224
- }
3225
- return "";
3226
- }
3227
- var FormContext = react.createContext(null);
3228
- function useFormContext() {
3229
- return react.useContext(FormContext);
3230
- }
3231
- function useFormField(field) {
3232
- const ctx = useFormContext();
3233
- const cbRef = react.useRef(field);
3234
- cbRef.current = field;
3235
- const name = field.name ?? "";
3236
- react.useEffect(() => {
3237
- if (!ctx) return;
3238
- return ctx.register({
3239
- name,
3240
- validate: () => cbRef.current.validate(),
3241
- resetValidation: () => cbRef.current.resetValidation()
3242
- });
3243
- }, [ctx, name]);
3244
- }
3245
3321
  var FIELD_SIZE = {
3246
3322
  sm: { height: 28, radius: 4, fieldGap: 12, labelGap: 6, fontSize: 12, lineHeight: 20, icon: 16 },
3247
3323
  md: { height: 36, radius: 6, fieldGap: 16, labelGap: 8, fontSize: 14, lineHeight: 24, icon: 16 }
@@ -3256,6 +3332,7 @@ var SField = react.forwardRef(function SField2({
3256
3332
  name,
3257
3333
  label = "",
3258
3334
  icon,
3335
+ iconColor,
3259
3336
  labelTooltip = "",
3260
3337
  labelTooltipProps,
3261
3338
  labelWidth,
@@ -3333,7 +3410,15 @@ var SField = react.forwardRef(function SField2({
3333
3410
  lineHeight: `${s.lineHeight}px`
3334
3411
  },
3335
3412
  children: [
3336
- icon && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: icon, size: s.icon, style: { marginRight: s.labelGap } }),
3413
+ icon && /* @__PURE__ */ jsxRuntime.jsx(
3414
+ SIcon,
3415
+ {
3416
+ name: icon,
3417
+ size: s.icon,
3418
+ color: iconColor,
3419
+ style: { marginRight: s.labelGap }
3420
+ }
3421
+ ),
3337
3422
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", children: label }),
3338
3423
  (labelTooltip !== "" || labelTooltipProps) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0", style: { marginLeft: s.labelGap }, children: /* @__PURE__ */ jsxRuntime.jsx(
3339
3424
  STooltip,
@@ -3383,7 +3468,7 @@ var SField = react.forwardRef(function SField2({
3383
3468
  borderBottomLeftRadius: s.radius - 1
3384
3469
  },
3385
3470
  children: [
3386
- icon && !label && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: icon, size: s.icon }),
3471
+ icon && !label && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: icon, size: s.icon, color: iconColor }),
3387
3472
  effectiveAddonLabel
3388
3473
  ]
3389
3474
  }
@@ -3398,6 +3483,77 @@ var SField = react.forwardRef(function SField2({
3398
3483
  }
3399
3484
  );
3400
3485
  });
3486
+ function SPortal({
3487
+ open,
3488
+ onClose,
3489
+ onOpenChange,
3490
+ anchorRef,
3491
+ placement = "bottom",
3492
+ align = "start",
3493
+ offset = 4,
3494
+ alignOffset = 0,
3495
+ fit = false,
3496
+ maxHeight,
3497
+ autoClose = false,
3498
+ persistent = false,
3499
+ viewportPadding = 0,
3500
+ onOpenAutoFocus,
3501
+ onCloseAutoFocus,
3502
+ children,
3503
+ className,
3504
+ style,
3505
+ ariaLabel
3506
+ }) {
3507
+ const portalContainer = usePortalContainer();
3508
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3509
+ RPopover2__namespace.Root,
3510
+ {
3511
+ open,
3512
+ onOpenChange: (next) => {
3513
+ onOpenChange?.(next);
3514
+ if (!next) onClose?.();
3515
+ },
3516
+ children: [
3517
+ /* @__PURE__ */ jsxRuntime.jsx(RPopover2__namespace.Anchor, { virtualRef: anchorRef }),
3518
+ /* @__PURE__ */ jsxRuntime.jsx(RPopover2__namespace.Portal, { container: portalContainer, children: /* @__PURE__ */ jsxRuntime.jsx(
3519
+ RPopover2__namespace.Content,
3520
+ {
3521
+ side: placement,
3522
+ align,
3523
+ sideOffset: offset,
3524
+ alignOffset,
3525
+ collisionPadding: viewportPadding,
3526
+ "aria-label": ariaLabel,
3527
+ onPointerDownOutside: (e) => {
3528
+ if (persistent || anchorRef.current?.contains(e.target)) {
3529
+ e.preventDefault();
3530
+ }
3531
+ },
3532
+ onFocusOutside: (e) => {
3533
+ if (persistent || anchorRef.current?.contains(e.target)) {
3534
+ e.preventDefault();
3535
+ }
3536
+ },
3537
+ onEscapeKeyDown: persistent ? (e) => e.preventDefault() : void 0,
3538
+ onOpenAutoFocus,
3539
+ onCloseAutoFocus,
3540
+ onClick: autoClose ? () => onClose?.() : void 0,
3541
+ className: cn(
3542
+ "z-50 overflow-hidden rounded-(--cmp-listBox-radius) bg-(--cmp-listBox-bg) shadow-normal-sm",
3543
+ maxHeight != null && "overflow-y-auto",
3544
+ fit && "min-w-(--radix-popover-trigger-width)",
3545
+ FLOATING_SLIDE_ANIM,
3546
+ className
3547
+ ),
3548
+ style: { maxHeight, ...style },
3549
+ children
3550
+ }
3551
+ ) })
3552
+ ]
3553
+ }
3554
+ );
3555
+ }
3556
+ SPortal.displayName = "SPortal";
3401
3557
 
3402
3558
  // src/components/SSelect/select.config.ts
3403
3559
  function flattenOptions(options, depth = 1) {
@@ -3432,75 +3588,6 @@ function flattenOptions(options, depth = 1) {
3432
3588
  var selectableOptions = (flat) => flat.filter((o) => !o.isGroup && !o.disabled);
3433
3589
  var isHtmlLabel = (label) => /<[a-z][\s\S]*>/i.test(label);
3434
3590
  var extractText = (html) => html.replace(/<[^>]*>/g, "");
3435
-
3436
- // src/lib/sanitize-inline-html.ts
3437
- var ALLOWED_INLINE_TAGS = /* @__PURE__ */ new Set(["A", "B", "STRONG", "I", "EM", "BR", "SPAN"]);
3438
- var ALLOWED_ATTRS_BY_TAG = {
3439
- A: /* @__PURE__ */ new Set(["href", "target", "rel", "class"])
3440
- };
3441
- var COMMON_ALLOWED_ATTRS = /* @__PURE__ */ new Set(["class"]);
3442
- var SAFE_HREF_RE = /^(https?:|mailto:|tel:|\/|#|\.)/i;
3443
- var DROP_CONTENT_TAGS = /* @__PURE__ */ new Set([
3444
- "SCRIPT",
3445
- "STYLE",
3446
- "IFRAME",
3447
- "OBJECT",
3448
- "EMBED",
3449
- "META",
3450
- "LINK",
3451
- "BASE",
3452
- "NOSCRIPT",
3453
- // SVG/MATH 는 foreign-content 파싱 모드로 전환되어 재직렬화 시 다른 트리로
3454
- // 재파싱될 수 있다(mXSS 벡터). 인라인 라벨에 필요 없으므로 통째로 제거한다.
3455
- "SVG",
3456
- "MATH",
3457
- "TEMPLATE"
3458
- ]);
3459
- var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3460
- var sanitizeNode = (node) => {
3461
- if (node.nodeType === Node.COMMENT_NODE) {
3462
- node.remove();
3463
- return;
3464
- }
3465
- if (node.nodeType !== Node.ELEMENT_NODE) return;
3466
- const element = node;
3467
- const tagName = element.tagName;
3468
- if (DROP_CONTENT_TAGS.has(tagName)) {
3469
- element.remove();
3470
- return;
3471
- }
3472
- Array.from(element.childNodes).forEach((child) => sanitizeNode(child));
3473
- if (ALLOWED_INLINE_TAGS.has(tagName)) {
3474
- if (element.hasAttribute("className")) {
3475
- const val = element.getAttribute("className");
3476
- element.removeAttribute("className");
3477
- if (!element.hasAttribute("class")) element.setAttribute("class", val);
3478
- }
3479
- const allowedAttrs = ALLOWED_ATTRS_BY_TAG[tagName] ?? COMMON_ALLOWED_ATTRS;
3480
- Array.from(element.attributes).forEach((attr) => {
3481
- if (!allowedAttrs.has(attr.name)) element.removeAttribute(attr.name);
3482
- });
3483
- if (tagName === "A") {
3484
- const href = element.getAttribute("href");
3485
- if (href !== null && href !== "" && !SAFE_HREF_RE.test(href)) element.removeAttribute("href");
3486
- if (element.hasAttribute("target")) {
3487
- element.setAttribute("rel", "noopener noreferrer");
3488
- }
3489
- }
3490
- return;
3491
- }
3492
- const parent = element.parentNode;
3493
- if (parent == null) return;
3494
- while (element.firstChild) parent.insertBefore(element.firstChild, element);
3495
- parent.removeChild(element);
3496
- };
3497
- var sanitizeInlineHtml = (value) => {
3498
- if (typeof document === "undefined") return escapeHtml(value);
3499
- const template = document.createElement("template");
3500
- template.innerHTML = value;
3501
- Array.from(template.content.childNodes).forEach((child) => sanitizeNode(child));
3502
- return template.innerHTML;
3503
- };
3504
3591
  var SEARCH_THRESHOLD = 11;
3505
3592
  var SSelect = react.forwardRef(function SSelect2({
3506
3593
  value,
@@ -3524,6 +3611,7 @@ var SSelect = react.forwardRef(function SSelect2({
3524
3611
  label,
3525
3612
  labelWidth,
3526
3613
  icon,
3614
+ iconColor,
3527
3615
  labelTooltip,
3528
3616
  addonLabel,
3529
3617
  addonAlign,
@@ -3535,18 +3623,15 @@ var SSelect = react.forwardRef(function SSelect2({
3535
3623
  }, ref) {
3536
3624
  const multi = type === "multi" || type === "multi_depth";
3537
3625
  const portalContainer = usePortalContainer();
3626
+ const insideModal = useInsideModal();
3538
3627
  const [search, setSearch] = react.useState("");
3539
3628
  const [ruleError, setRuleError] = react.useState("");
3540
3629
  const [open, setOpen] = react.useState(false);
3630
+ const [pendingOpen, setPendingOpen] = react.useState(false);
3631
+ const [focusedIndex, setFocusedIndex] = react.useState(-1);
3541
3632
  const triggerRef = react.useRef(null);
3542
- react.useImperativeHandle(
3543
- ref,
3544
- () => ({
3545
- focus: () => triggerRef.current?.focus(),
3546
- open: () => setOpen(true)
3547
- }),
3548
- []
3549
- );
3633
+ const searchInputRef = react.useRef(null);
3634
+ const listRef = react.useRef(null);
3550
3635
  const flat = react.useMemo(() => flattenOptions(options), [options]);
3551
3636
  const selectable = react.useMemo(() => selectableOptions(flat), [flat]);
3552
3637
  const hasLeaf = react.useMemo(() => flat.some((o) => !o.isGroup), [flat]);
@@ -3564,6 +3649,46 @@ var SSelect = react.forwardRef(function SSelect2({
3564
3649
  const arr = Array.isArray(value) ? value : [value];
3565
3650
  return arr.map((v) => v != null && typeof v === "object" ? v.value : v).filter((v) => v != null && v !== "").map(String);
3566
3651
  }, [value]);
3652
+ const selectedRef = react.useRef(selected);
3653
+ selectedRef.current = selected;
3654
+ const emit = (next) => {
3655
+ if (!onValueChange) return;
3656
+ const mapped = next.map(
3657
+ (v) => emitValue ? rawByValue.get(v) ?? v : optionByValue.get(v) ?? { value: v, label: v }
3658
+ );
3659
+ onValueChange(multi ? mapped : mapped[0] ?? (emitValue ? "" : null));
3660
+ };
3661
+ const commitOpen = (next) => {
3662
+ setOpen(next);
3663
+ onOpenChange?.(next);
3664
+ if (!next) {
3665
+ setPendingOpen(false);
3666
+ setSearch("");
3667
+ if (rules && rules.length > 0) {
3668
+ setRuleError(runRules(multi ? selectedRef.current : selectedRef.current[0] ?? "", rules));
3669
+ }
3670
+ }
3671
+ };
3672
+ const requestOpen = (next) => {
3673
+ if (next && insideModal && portalContainer == null) {
3674
+ setPendingOpen(true);
3675
+ return;
3676
+ }
3677
+ commitOpen(next);
3678
+ };
3679
+ react.useEffect(() => {
3680
+ if (!insideModal || portalContainer == null || !pendingOpen) return;
3681
+ setPendingOpen(false);
3682
+ commitOpen(true);
3683
+ }, [insideModal, pendingOpen, portalContainer]);
3684
+ react.useImperativeHandle(
3685
+ ref,
3686
+ () => ({
3687
+ focus: () => triggerRef.current?.focus(),
3688
+ open: () => requestOpen(true)
3689
+ }),
3690
+ [insideModal, portalContainer]
3691
+ );
3567
3692
  useFormField({
3568
3693
  name,
3569
3694
  validate: () => {
@@ -3573,13 +3698,6 @@ var SSelect = react.forwardRef(function SSelect2({
3573
3698
  },
3574
3699
  resetValidation: () => setRuleError("")
3575
3700
  });
3576
- const emit = (next) => {
3577
- if (!onValueChange) return;
3578
- const mapped = next.map(
3579
- (v) => emitValue ? rawByValue.get(v) ?? v : optionByValue.get(v) ?? { value: v, label: v }
3580
- );
3581
- onValueChange(multi ? mapped : mapped[0] ?? (emitValue ? "" : null));
3582
- };
3583
3701
  const filtered = react.useMemo(() => {
3584
3702
  if (!showSearch || search.trim() === "") return flat;
3585
3703
  const q = search.trim().toLowerCase();
@@ -3590,6 +3708,16 @@ var SSelect = react.forwardRef(function SSelect2({
3590
3708
  const allValues = selectable.map((o) => o.value);
3591
3709
  const allSelected = allValues.length > 0 && allValues.every((v) => selected.includes(v));
3592
3710
  const hasItems = filtered.some((o) => !o.isGroup);
3711
+ const navigableItems = react.useMemo(() => {
3712
+ const items = [];
3713
+ if (multi && useSelectAll && allValues.length > 0) {
3714
+ items.push({ value: "__select_all__", isSelectAll: true });
3715
+ }
3716
+ filtered.forEach((o) => {
3717
+ if (!o.isGroup && !o.disabled) items.push({ value: o.value });
3718
+ });
3719
+ return items;
3720
+ }, [allValues.length, filtered, multi, useSelectAll]);
3593
3721
  const triggerText = (() => {
3594
3722
  if (selected.length === 0) return placeholder;
3595
3723
  if (multi) {
@@ -3599,26 +3727,119 @@ var SSelect = react.forwardRef(function SSelect2({
3599
3727
  return labelByValue.get(selected[0]) ?? selected[0];
3600
3728
  })();
3601
3729
  const isPlaceholder = selected.length === 0;
3602
- const selectStore = /* @__PURE__ */ jsxRuntime.jsxs(
3603
- Ariakit__namespace.SelectProvider,
3730
+ const handleSelect = (nextValue) => {
3731
+ if (multi) {
3732
+ const next = selected.includes(nextValue) ? selected.filter((v) => v !== nextValue) : [...selected, nextValue];
3733
+ emit(next);
3734
+ return;
3735
+ }
3736
+ emit([nextValue]);
3737
+ commitOpen(false);
3738
+ };
3739
+ const handleSelectAll = () => {
3740
+ emit(allSelected ? [] : allValues);
3741
+ };
3742
+ react.useEffect(() => {
3743
+ if (!open) {
3744
+ setFocusedIndex(-1);
3745
+ return;
3746
+ }
3747
+ setFocusedIndex(navigableItems.length > 0 ? 0 : -1);
3748
+ }, [open, navigableItems.length]);
3749
+ react.useEffect(() => {
3750
+ if (!open) return;
3751
+ setFocusedIndex(navigableItems.length > 0 ? 0 : -1);
3752
+ }, [search, open, navigableItems.length]);
3753
+ react.useEffect(() => {
3754
+ if (!open || focusedIndex < 0) return;
3755
+ const list = listRef.current;
3756
+ const focusedEl = list?.querySelector('[data-select-focused="true"]');
3757
+ if (!list || !focusedEl) return;
3758
+ const listRect = list.getBoundingClientRect();
3759
+ const itemRect = focusedEl.getBoundingClientRect();
3760
+ if (itemRect.top < listRect.top) {
3761
+ list.scrollTop += itemRect.top - listRect.top;
3762
+ } else if (itemRect.bottom > listRect.bottom) {
3763
+ list.scrollTop += itemRect.bottom - listRect.bottom;
3764
+ }
3765
+ }, [focusedIndex, open]);
3766
+ const moveFocusedIndex = (direction) => {
3767
+ if (!open || navigableItems.length === 0) return;
3768
+ setFocusedIndex((current) => {
3769
+ if (direction === "next") {
3770
+ return current < 0 ? 0 : (current + 1) % navigableItems.length;
3771
+ }
3772
+ return current <= 0 ? navigableItems.length - 1 : current - 1;
3773
+ });
3774
+ };
3775
+ const selectFocusedItem = () => {
3776
+ if (focusedIndex < 0 || focusedIndex >= navigableItems.length) return;
3777
+ const focused = navigableItems[focusedIndex];
3778
+ if (focused.isSelectAll) {
3779
+ handleSelectAll();
3780
+ return;
3781
+ }
3782
+ handleSelect(focused.value);
3783
+ };
3784
+ const handleKeyDown = (event) => {
3785
+ if (!open) {
3786
+ if ((event.key === "ArrowDown" || event.key === "ArrowUp") && !effectiveDisabled) {
3787
+ event.preventDefault();
3788
+ requestOpen(true);
3789
+ }
3790
+ return;
3791
+ }
3792
+ if (event.key === "ArrowDown") {
3793
+ event.preventDefault();
3794
+ moveFocusedIndex("next");
3795
+ return;
3796
+ }
3797
+ if (event.key === "ArrowUp") {
3798
+ event.preventDefault();
3799
+ moveFocusedIndex("prev");
3800
+ return;
3801
+ }
3802
+ if (event.key === "Enter") {
3803
+ event.preventDefault();
3804
+ selectFocusedItem();
3805
+ return;
3806
+ }
3807
+ if (event.key === "Escape") {
3808
+ event.preventDefault();
3809
+ commitOpen(false);
3810
+ }
3811
+ };
3812
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3813
+ SField,
3604
3814
  {
3605
- animated: portalContainer == null ? 150 : false,
3606
- value: multi ? selected : selected[0] ?? "",
3607
- setValue: (v) => emit(Array.isArray(v) ? v : v === "" ? [] : [v]),
3608
- open,
3609
- setOpen: (o) => {
3610
- setOpen(o);
3611
- onOpenChange?.(o);
3612
- if (!o && rules && rules.length > 0) {
3613
- setRuleError(runRules(multi ? selected : selected[0] ?? "", rules));
3614
- }
3615
- },
3815
+ label,
3816
+ labelWidth,
3817
+ icon,
3818
+ iconColor,
3819
+ labelTooltip,
3820
+ labelTooltipProps,
3821
+ addonLabel,
3822
+ addonAlign,
3823
+ hint,
3824
+ error: error || ruleError !== "",
3825
+ status: rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0,
3826
+ errorMessage: ruleError !== "" ? ruleError : errorMessage,
3827
+ disabled: effectiveDisabled,
3828
+ width,
3829
+ className,
3830
+ style,
3616
3831
  children: [
3832
+ name && /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name, value: selected.join(",") }),
3617
3833
  /* @__PURE__ */ jsxRuntime.jsxs(
3618
- Ariakit__namespace.Select,
3834
+ "button",
3619
3835
  {
3620
3836
  ref: triggerRef,
3837
+ type: "button",
3621
3838
  disabled: effectiveDisabled,
3839
+ "aria-haspopup": "listbox",
3840
+ "aria-expanded": open,
3841
+ onClick: () => requestOpen(!open),
3842
+ onKeyDown: handleKeyDown,
3622
3843
  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",
3623
3844
  style: {
3624
3845
  color: effectiveDisabled ? "var(--cmp-select-text-disabled)" : isPlaceholder ? "var(--cmp-select-icon-default)" : "var(--cmp-select-text-default)"
@@ -3643,178 +3864,237 @@ var SSelect = react.forwardRef(function SSelect2({
3643
3864
  ]
3644
3865
  }
3645
3866
  ),
3646
- /* @__PURE__ */ jsxRuntime.jsxs(
3647
- Ariakit__namespace.SelectPopover,
3867
+ /* @__PURE__ */ jsxRuntime.jsx(
3868
+ SPortal,
3648
3869
  {
3649
- portal: true,
3650
- portalElement: portalContainer,
3651
- gutter: 4,
3652
- sameWidth: !dropdownWidth,
3653
- className: "z-50 overflow-auto rounded-[var(--cmp-listBox-radius)] bg-[var(--cmp-listBox-bg)] shadow-[2px_2px_12px_2px_rgba(0,0,0,0.1)] data-[enter]:animate-floating-in-bottom data-[leave]:animate-floating-out-bottom",
3870
+ open,
3871
+ onClose: () => commitOpen(false),
3872
+ onOpenChange: (next) => {
3873
+ if (next) requestOpen(true);
3874
+ },
3875
+ anchorRef: triggerRef,
3876
+ placement: "bottom",
3877
+ align: "start",
3878
+ offset: 4,
3879
+ fit: !dropdownWidth,
3880
+ onOpenAutoFocus: (event) => {
3881
+ if (!showSearch) {
3882
+ event.preventDefault();
3883
+ return;
3884
+ }
3885
+ event.preventDefault();
3886
+ requestAnimationFrame(() => searchInputRef.current?.focus());
3887
+ },
3888
+ onCloseAutoFocus: (event) => {
3889
+ event.preventDefault();
3890
+ triggerRef.current?.focus();
3891
+ },
3892
+ className: "rounded-[var(--cmp-listBox-radius)] bg-[var(--cmp-listBox-bg)] shadow-[2px_2px_12px_2px_rgba(0,0,0,0.1)]",
3654
3893
  style: {
3655
- maxHeight: dropdownHeight,
3656
3894
  ...dropdownWidth ? { width: dropdownWidth } : {},
3657
3895
  maxWidth: maxDropdownWidth
3658
3896
  },
3659
- children: [
3660
- showSearch && // 원본 sd-select-list-item-search: sticky + 검색 아이콘 + 테두리 input + focus-within + clear
3661
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-[1] bg-[var(--cmp-listBox-bg)] px-[8px] py-[4px]", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-[6px] rounded-[4px] border border-solid border-[color:var(--color-grey-45)] bg-white px-[8px] py-[4px] focus-within:border-[color:var(--sys-color-field-border-focus)]", children: [
3662
- /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: "search", size: 16, color: "var(--color-grey-70)", className: "shrink-0" }),
3663
- /* @__PURE__ */ jsxRuntime.jsx(
3664
- Ariakit__namespace.Combobox,
3665
- {
3666
- autoSelect: true,
3667
- placeholder: "\uAC80\uC0C9",
3668
- className: "min-w-0 flex-1 border-none bg-transparent text-[12px] leading-[20px] text-[color:var(--cmp-select-text-default)] outline-none placeholder:text-[color:var(--color-grey-55)]"
3669
- }
3670
- ),
3671
- search !== "" && /* @__PURE__ */ jsxRuntime.jsx(
3672
- SGhostButton,
3673
- {
3674
- icon: "close",
3675
- size: "xxs",
3676
- intent: "default",
3677
- ariaLabel: "\uAC80\uC0C9\uC5B4 \uC9C0\uC6B0\uAE30",
3678
- onClick: () => setSearch(""),
3679
- className: "shrink-0"
3680
- }
3681
- )
3682
- ] }) }),
3683
- multi && useSelectAll && /* @__PURE__ */ jsxRuntime.jsx(
3684
- "button",
3685
- {
3686
- type: "button",
3687
- className: "flex w-full items-center px-[12px] py-[4px] text-left text-[12px] leading-[20px] text-[color:var(--cmp-listItem-content-default)] hover:bg-[var(--cmp-listItem-bg-hover)] hover:text-[color:var(--cmp-listItem-content-hover)]",
3688
- onClick: () => emit(allSelected ? [] : allValues),
3689
- children: allSelectedLabel
3690
- }
3691
- ),
3692
- hasItems ? /* @__PURE__ */ jsxRuntime.jsx(ItemList, { items: filtered, multi, selected, useSearch: showSearch }) : (
3693
- // 원본 sd-select-listbox__empty (표시할 항목이 없을 때)
3694
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-[12px] py-[12px] text-left text-[12px] leading-[20px] text-[color:var(--cmp-listItem-content-disabled)]", children: "\uD45C\uC2DC\uD560 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." })
3695
- )
3696
- ]
3897
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
3898
+ "div",
3899
+ {
3900
+ ref: listRef,
3901
+ className: "overflow-auto [scrollbar-width:auto] [&::-webkit-scrollbar]:w-[8px] [&::-webkit-scrollbar]:bg-[var(--color-grey-25)] [&::-webkit-scrollbar:horizontal]:h-[8px] [&::-webkit-scrollbar-thumb]:rounded-[4px] [&::-webkit-scrollbar-thumb]:bg-[var(--color-grey-45)] [&::-webkit-scrollbar-track]:bg-[var(--color-grey-25)]",
3902
+ style: { maxHeight: dropdownHeight },
3903
+ onMouseLeave: () => setFocusedIndex(-1),
3904
+ children: [
3905
+ showSearch && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-[1] bg-[var(--cmp-listBox-bg)] px-[8px] py-[4px]", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-[6px] rounded-[4px] border border-solid border-[color:var(--color-grey-45)] bg-white px-[8px] py-[4px] focus-within:border-[color:var(--sys-color-field-border-focus)]", children: [
3906
+ /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: "search", size: 16, color: "var(--color-grey-70)", className: "shrink-0" }),
3907
+ /* @__PURE__ */ jsxRuntime.jsx(
3908
+ "input",
3909
+ {
3910
+ ref: searchInputRef,
3911
+ value: search,
3912
+ onChange: (e) => setSearch(e.target.value),
3913
+ onKeyDown: handleKeyDown,
3914
+ placeholder: "\uAC80\uC0C9",
3915
+ className: "min-w-0 flex-1 border-none bg-transparent text-[12px] leading-[20px] text-[color:var(--cmp-select-text-default)] outline-none placeholder:text-[color:var(--color-grey-55)]"
3916
+ }
3917
+ ),
3918
+ search !== "" && /* @__PURE__ */ jsxRuntime.jsx(
3919
+ SGhostButton,
3920
+ {
3921
+ icon: "close",
3922
+ size: "xxs",
3923
+ intent: "default",
3924
+ ariaLabel: "\uAC80\uC0C9\uC5B4 \uC9C0\uC6B0\uAE30",
3925
+ onClick: () => setSearch(""),
3926
+ className: "shrink-0"
3927
+ }
3928
+ )
3929
+ ] }) }),
3930
+ hasItems ? /* @__PURE__ */ jsxRuntime.jsx(
3931
+ ItemList,
3932
+ {
3933
+ items: filtered,
3934
+ multi,
3935
+ selected,
3936
+ focusedValue: navigableItems[focusedIndex]?.isSelectAll ? "__select_all__" : navigableItems[focusedIndex]?.value,
3937
+ showSelectAll: multi && useSelectAll && allValues.length > 0,
3938
+ selectAllLabel: allSelectedLabel,
3939
+ selectAllSelected: allSelected,
3940
+ onSelectAll: handleSelectAll,
3941
+ onSelect: handleSelect,
3942
+ onHoverValue: (value2) => {
3943
+ if (value2 == null) {
3944
+ setFocusedIndex(-1);
3945
+ return;
3946
+ }
3947
+ const index = navigableItems.findIndex(
3948
+ (item) => item.isSelectAll ? value2 === "__select_all__" : item.value === value2
3949
+ );
3950
+ if (index >= 0) setFocusedIndex(index);
3951
+ }
3952
+ }
3953
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-[12px] py-[12px] text-left text-[12px] leading-[20px] text-[color:var(--cmp-listItem-content-disabled)]", children: "\uD45C\uC2DC\uD560 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." })
3954
+ ]
3955
+ }
3956
+ )
3697
3957
  }
3698
3958
  )
3699
3959
  ]
3700
3960
  }
3701
3961
  );
3702
- const control = showSearch ? /* @__PURE__ */ jsxRuntime.jsx(Ariakit__namespace.ComboboxProvider, { value: search, setValue: setSearch, resetValueOnHide: true, children: selectStore }) : selectStore;
3703
- return /* @__PURE__ */ jsxRuntime.jsxs(
3704
- SField,
3705
- {
3706
- label,
3707
- labelWidth,
3708
- icon,
3709
- labelTooltip,
3710
- labelTooltipProps,
3711
- addonLabel,
3712
- addonAlign,
3713
- hint,
3714
- error: error || ruleError !== "",
3715
- status: rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0,
3716
- errorMessage: ruleError !== "" ? ruleError : errorMessage,
3717
- disabled: effectiveDisabled,
3718
- width,
3719
- className,
3720
- style,
3721
- children: [
3722
- name && /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name, value: selected.join(",") }),
3723
- control
3724
- ]
3725
- }
3726
- );
3727
3962
  });
3728
3963
  SSelect.displayName = "SSelect";
3729
3964
  function ItemList({
3730
3965
  items,
3731
3966
  multi,
3732
3967
  selected,
3733
- useSearch
3968
+ focusedValue,
3969
+ showSelectAll,
3970
+ selectAllLabel,
3971
+ selectAllSelected,
3972
+ onSelectAll,
3973
+ onSelect,
3974
+ onHoverValue
3734
3975
  }) {
3735
3976
  const itemBase = cn(
3736
- // group: 체크박스 inverse(group-data-[active-item])를 위해 필요
3737
- "group flex cursor-pointer select-none items-center gap-[8px] text-[12px] leading-[20px] outline-none",
3977
+ "group flex w-full cursor-pointer select-none items-center gap-[8px] border-none bg-transparent text-left text-[12px] leading-[20px] outline-none",
3738
3978
  "text-[color:var(--cmp-listItem-content-default)]",
3739
- // Ariakit은 disabled 항목에 aria-disabled를 붙이므로 aria-disabled 변형으로 스타일 (data-disabled 아님)
3740
- "aria-disabled:cursor-not-allowed aria-disabled:text-[color:var(--cmp-listItem-content-disabled)]"
3741
- );
3742
- const activeClass = "data-[active-item]:bg-[var(--cmp-listItem-bg-hover)] data-[active-item]:text-[color:var(--cmp-listItem-content-hover)]";
3743
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: items.map((o) => {
3744
- const padLeft = o.depth === 1 ? 12 : o.depth === 2 ? 20 : 28;
3745
- if (o.isGroup) {
3746
- const isDepth1 = o.depth === 1;
3747
- return /* @__PURE__ */ jsxRuntime.jsx(
3748
- "div",
3749
- {
3750
- className: cn(
3751
- "select-none py-[4px] pr-[12px] text-[12px] leading-[20px] text-[color:var(--cmp-listItem-content-default)]",
3752
- isDepth1 ? "border-t border-solid border-[color:var(--cmp-listItem-depth1-border-color)] bg-[var(--cmp-listItem-depth1-bg-default)] font-bold" : "bg-[var(--cmp-listItem-depth2-middle-bg)] font-medium",
3753
- // 하위가 전부 비활성이면(select.config 에서 bubble-up) 어미 그룹도 비활성 처리:
3754
- // 텍스트는 회색(#888), depth1 그룹은 파란 배경 대신 회색(#EEE)으로 (depth2 는 #F6F6F6 유지)
3755
- o.disabled && "cursor-not-allowed text-[color:var(--cmp-listItem-content-disabled)]",
3756
- o.disabled && isDepth1 && "bg-[var(--cmp-listItem-depth1-bg-disabled)]"
3757
- ),
3758
- style: { paddingLeft: padLeft },
3759
- children: o.label
3760
- },
3761
- o.value
3762
- );
3763
- }
3764
- const isSelected = selected.includes(o.value);
3765
- return /* @__PURE__ */ jsxRuntime.jsxs(
3766
- Ariakit__namespace.SelectItem,
3979
+ "disabled:cursor-not-allowed disabled:text-[color:var(--cmp-listItem-content-disabled)]"
3980
+ );
3981
+ const activeClass = "hover:bg-[var(--cmp-listItem-bg-hover)] hover:text-[color:var(--cmp-listItem-content-hover)] focus:bg-[var(--cmp-listItem-bg-hover)] focus:text-[color:var(--cmp-listItem-content-hover)]";
3982
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "listbox", "aria-multiselectable": multi || void 0, children: [
3983
+ showSelectAll && /* @__PURE__ */ jsxRuntime.jsxs(
3984
+ "button",
3767
3985
  {
3768
- value: o.value,
3769
- disabled: o.disabled,
3770
- accessibleWhenDisabled: true,
3986
+ type: "button",
3987
+ "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
3988
+ "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
3771
3989
  className: cn(
3772
3990
  itemBase,
3773
- isSelected && !multi && "font-bold text-[color:var(--cmp-listItem-content-selected)]",
3774
- // Ariakit은 disabled 항목도 hover 시 active(data-active-item) 처리하므로,
3775
- // 비활성 항목에는 hover 하이라이트를 적용하지 않아 회색 비활성 스타일을 유지한다.
3776
- // (원본 Stencil의 `--focused...:not(--disabled)` 규칙과 동일한 의도)
3777
- !o.disabled && activeClass
3778
- ),
3779
- style: { paddingTop: 4, paddingBottom: 4, paddingRight: 12, paddingLeft: padLeft },
3780
- render: useSearch ? /* @__PURE__ */ jsxRuntime.jsx(Ariakit__namespace.ComboboxItem, {}) : void 0,
3991
+ focusedValue === "__select_all__" && "bg-[var(--cmp-listItem-bg-hover)] text-[color:var(--cmp-listItem-content-hover)]"
3992
+ ),
3993
+ style: { paddingTop: 4, paddingBottom: 4, paddingRight: 12, paddingLeft: 12 },
3994
+ onMouseEnter: () => onHoverValue("__select_all__"),
3995
+ onMouseLeave: () => onHoverValue(null),
3996
+ onClick: onSelectAll,
3781
3997
  children: [
3782
- multi && // hover(active) 시 checkbox inverse — 파란 hover 배경 위에서 checked/unchecked 모두 흰 테두리 + 투명 배경
3783
3998
  /* @__PURE__ */ jsxRuntime.jsx(
3784
3999
  "span",
3785
4000
  {
3786
4001
  className: cn(
3787
4002
  "flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[2px] border border-solid",
3788
- o.disabled ? (
3789
- // 비활성: 회색 채움(#E1E1E1) + 회색 테두리(#CCCCCC)
3790
- "cursor-not-allowed border-[color:var(--cmp-checkbox-border-disabled)] bg-[var(--cmp-checkbox-bg-disabled)]"
3791
- ) : cn(
3792
- "group-data-[active-item]:border-[color:var(--cmp-checkbox-checked-border-inverse)]",
3793
- isSelected ? "border-[color:var(--cmp-checkbox-checked-bg-default)] bg-[var(--cmp-checkbox-checked-bg-default)] group-data-[active-item]:bg-transparent" : "border-[color:var(--cmp-checkbox-unchecked-border-default)] bg-transparent"
3794
- )
4003
+ selectAllSelected ? focusedValue === "__select_all__" ? "border-[color:var(--cmp-checkbox-checked-border-inverse)] bg-[var(--cmp-checkbox-checked-bg-default)]" : "border-[color:var(--cmp-checkbox-checked-bg-default)] bg-[var(--cmp-checkbox-checked-bg-default)]" : "border-[color:var(--cmp-checkbox-unchecked-border-default)] bg-[var(--cmp-checkbox-unchecked-bg-default)]"
3795
4004
  ),
3796
- children: isSelected && /* @__PURE__ */ jsxRuntime.jsx(
4005
+ children: selectAllSelected && /* @__PURE__ */ jsxRuntime.jsx(
3797
4006
  SIcon,
3798
4007
  {
3799
4008
  name: "checkboxCheck",
3800
4009
  size: 12,
3801
- color: o.disabled ? "var(--cmp-checkbox-checked-icon-disabled)" : "var(--cmp-checkbox-checked-icon-default)"
4010
+ color: "var(--cmp-checkbox-checked-icon-default)"
3802
4011
  }
3803
4012
  )
3804
4013
  }
3805
4014
  ),
3806
- isHtmlLabel(o.label) ? /* @__PURE__ */ jsxRuntime.jsx(
3807
- "span",
3808
- {
3809
- className: "flex-1 truncate",
3810
- dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(o.label) }
3811
- }
3812
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate", children: o.label })
4015
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate", children: selectAllLabel })
3813
4016
  ]
3814
- },
3815
- o.value
3816
- );
3817
- }) });
4017
+ }
4018
+ ),
4019
+ items.map((o) => {
4020
+ const padLeft = o.depth === 1 ? 12 : o.depth === 2 ? 20 : 28;
4021
+ if (o.isGroup) {
4022
+ const isDepth1 = o.depth === 1;
4023
+ return /* @__PURE__ */ jsxRuntime.jsx(
4024
+ "div",
4025
+ {
4026
+ className: cn(
4027
+ "select-none py-[4px] pr-[12px] text-[12px] leading-[20px] text-[color:var(--cmp-listItem-content-default)]",
4028
+ isDepth1 ? "border-t border-solid border-[color:var(--cmp-listItem-depth1-border-color)] bg-[var(--cmp-listItem-depth1-bg-default)] font-bold" : "bg-[var(--cmp-listItem-depth2-middle-bg)] font-medium",
4029
+ // 하위가 전부 비활성이면(select.config 에서 bubble-up) 어미 그룹도 비활성 처리:
4030
+ // 텍스트는 회색(#888), depth1 그룹은 파란 배경 대신 회색(#EEE)으로 (depth2 는 #F6F6F6 유지)
4031
+ o.disabled && "cursor-not-allowed text-[color:var(--cmp-listItem-content-disabled)]",
4032
+ o.disabled && isDepth1 && "bg-[var(--cmp-listItem-depth1-bg-disabled)]"
4033
+ ),
4034
+ style: { paddingLeft: padLeft },
4035
+ children: o.label
4036
+ },
4037
+ o.value
4038
+ );
4039
+ }
4040
+ const isSelected = selected.includes(o.value);
4041
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4042
+ "button",
4043
+ {
4044
+ type: "button",
4045
+ disabled: o.disabled,
4046
+ role: "option",
4047
+ "aria-selected": isSelected,
4048
+ "data-select-focused": focusedValue === o.value ? "true" : void 0,
4049
+ "data-select-active": focusedValue === o.value ? "true" : void 0,
4050
+ className: cn(
4051
+ itemBase,
4052
+ isSelected && !multi && "font-bold text-[color:var(--cmp-listItem-content-selected)]",
4053
+ focusedValue === o.value && "bg-[var(--cmp-listItem-bg-hover)] text-[color:var(--cmp-listItem-content-hover)]",
4054
+ // 비활성 항목은 hover/focus 스타일 대상에서 제외한다.
4055
+ // (:hover 는 disabled 버튼에도 매칭되므로 클래스 자체를 붙이지 않아야 한다)
4056
+ !o.disabled && activeClass
4057
+ ),
4058
+ style: { paddingTop: 4, paddingBottom: 4, paddingRight: 12, paddingLeft: padLeft },
4059
+ onMouseEnter: () => onHoverValue(o.value),
4060
+ onMouseLeave: () => onHoverValue(null),
4061
+ onClick: () => onSelect(o.value),
4062
+ children: [
4063
+ multi && /* @__PURE__ */ jsxRuntime.jsx(
4064
+ "span",
4065
+ {
4066
+ className: cn(
4067
+ "flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[2px] border border-solid",
4068
+ o.disabled ? (
4069
+ // 비활성: 회색 채움(#E1E1E1) + 회색 테두리(#CCCCCC)
4070
+ "cursor-not-allowed border-[color:var(--cmp-checkbox-border-disabled)] bg-[var(--cmp-checkbox-bg-disabled)]"
4071
+ ) : cn(
4072
+ isSelected ? focusedValue === o.value ? "border-[color:var(--cmp-checkbox-checked-border-inverse)] bg-[var(--cmp-checkbox-checked-bg-default)]" : "border-[color:var(--cmp-checkbox-checked-bg-default)] bg-[var(--cmp-checkbox-checked-bg-default)]" : "border-[color:var(--cmp-checkbox-unchecked-border-default)] bg-[var(--cmp-checkbox-unchecked-bg-default)]"
4073
+ )
4074
+ ),
4075
+ children: isSelected && /* @__PURE__ */ jsxRuntime.jsx(
4076
+ SIcon,
4077
+ {
4078
+ name: "checkboxCheck",
4079
+ size: 12,
4080
+ color: o.disabled ? "var(--cmp-checkbox-checked-icon-disabled)" : "var(--cmp-checkbox-checked-icon-default)"
4081
+ }
4082
+ )
4083
+ }
4084
+ ),
4085
+ isHtmlLabel(o.label) ? /* @__PURE__ */ jsxRuntime.jsx(
4086
+ "span",
4087
+ {
4088
+ className: "flex-1 truncate",
4089
+ dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(o.label) }
4090
+ }
4091
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate", children: o.label })
4092
+ ]
4093
+ },
4094
+ o.value
4095
+ );
4096
+ })
4097
+ ] });
3818
4098
  }
3819
4099
 
3820
4100
  exports.SSelect = SSelect;