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
@@ -1,6 +1,5 @@
1
1
  "use client";
2
- import { createContext, forwardRef, useState, useRef, useImperativeHandle, useMemo, useContext, useEffect } from 'react';
3
- import * as Ariakit from '@ariakit/react';
2
+ import { createContext, forwardRef, useState, useRef, useImperativeHandle, useMemo, useEffect, useContext } from 'react';
4
3
  import { clsx } from 'clsx';
5
4
  import { extendTailwindMerge } from 'tailwind-merge';
6
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -17,10 +16,110 @@ function cn(...inputs) {
17
16
  return twMerge(clsx(inputs));
18
17
  }
19
18
  var PortalContainerContext = createContext(null);
19
+ var InsideModalContext = createContext(false);
20
20
  PortalContainerContext.Provider;
21
+ InsideModalContext.Provider;
21
22
  function usePortalContainer() {
22
23
  return useContext(PortalContainerContext) ?? void 0;
23
24
  }
25
+ function useInsideModal() {
26
+ return useContext(InsideModalContext);
27
+ }
28
+ function runRules(value, rules) {
29
+ if (!rules || rules.length === 0) return "";
30
+ for (const rule of rules) {
31
+ const result = rule(value ?? "");
32
+ if (result !== true) return typeof result === "string" ? result : "\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC12\uC785\uB2C8\uB2E4.";
33
+ }
34
+ return "";
35
+ }
36
+ var FormContext = createContext(null);
37
+ function useFormContext() {
38
+ return useContext(FormContext);
39
+ }
40
+ function useFormField(field) {
41
+ const ctx = useFormContext();
42
+ const cbRef = useRef(field);
43
+ cbRef.current = field;
44
+ const name = field.name ?? "";
45
+ useEffect(() => {
46
+ if (!ctx) return;
47
+ return ctx.register({
48
+ name,
49
+ validate: () => cbRef.current.validate(),
50
+ resetValidation: () => cbRef.current.resetValidation()
51
+ });
52
+ }, [ctx, name]);
53
+ }
54
+
55
+ // src/lib/sanitize-inline-html.ts
56
+ var ALLOWED_INLINE_TAGS = /* @__PURE__ */ new Set(["A", "B", "STRONG", "I", "EM", "BR", "SPAN"]);
57
+ var ALLOWED_ATTRS_BY_TAG = {
58
+ A: /* @__PURE__ */ new Set(["href", "target", "rel", "class"])
59
+ };
60
+ var COMMON_ALLOWED_ATTRS = /* @__PURE__ */ new Set(["class"]);
61
+ var SAFE_HREF_RE = /^(https?:|mailto:|tel:|\/|#|\.)/i;
62
+ var DROP_CONTENT_TAGS = /* @__PURE__ */ new Set([
63
+ "SCRIPT",
64
+ "STYLE",
65
+ "IFRAME",
66
+ "OBJECT",
67
+ "EMBED",
68
+ "META",
69
+ "LINK",
70
+ "BASE",
71
+ "NOSCRIPT",
72
+ // SVG/MATH 는 foreign-content 파싱 모드로 전환되어 재직렬화 시 다른 트리로
73
+ // 재파싱될 수 있다(mXSS 벡터). 인라인 라벨에 필요 없으므로 통째로 제거한다.
74
+ "SVG",
75
+ "MATH",
76
+ "TEMPLATE"
77
+ ]);
78
+ var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
79
+ var sanitizeNode = (node) => {
80
+ if (node.nodeType === Node.COMMENT_NODE) {
81
+ node.remove();
82
+ return;
83
+ }
84
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
85
+ const element = node;
86
+ const tagName = element.tagName;
87
+ if (DROP_CONTENT_TAGS.has(tagName)) {
88
+ element.remove();
89
+ return;
90
+ }
91
+ Array.from(element.childNodes).forEach((child) => sanitizeNode(child));
92
+ if (ALLOWED_INLINE_TAGS.has(tagName)) {
93
+ if (element.hasAttribute("className")) {
94
+ const val = element.getAttribute("className");
95
+ element.removeAttribute("className");
96
+ if (!element.hasAttribute("class")) element.setAttribute("class", val);
97
+ }
98
+ const allowedAttrs = ALLOWED_ATTRS_BY_TAG[tagName] ?? COMMON_ALLOWED_ATTRS;
99
+ Array.from(element.attributes).forEach((attr) => {
100
+ if (!allowedAttrs.has(attr.name)) element.removeAttribute(attr.name);
101
+ });
102
+ if (tagName === "A") {
103
+ const href = element.getAttribute("href");
104
+ if (href !== null && href !== "" && !SAFE_HREF_RE.test(href)) element.removeAttribute("href");
105
+ if (element.hasAttribute("target")) {
106
+ element.setAttribute("rel", "noopener noreferrer");
107
+ }
108
+ }
109
+ return;
110
+ }
111
+ const parent = element.parentNode;
112
+ if (parent == null) return;
113
+ while (element.firstChild) parent.insertBefore(element.firstChild, element);
114
+ parent.removeChild(element);
115
+ };
116
+ var sanitizeInlineHtml = (value) => {
117
+ if (typeof document === "undefined") return escapeHtml(value);
118
+ const template = document.createElement("template");
119
+ template.innerHTML = value;
120
+ Array.from(template.content.childNodes).forEach((child) => sanitizeNode(child));
121
+ return template.innerHTML;
122
+ };
24
123
 
25
124
  // src/lib/color.ts
26
125
  var COLOR_KEYS = [
@@ -2973,6 +3072,8 @@ function SGhostButton({
2973
3072
  tooltipText,
2974
3073
  disabled = false,
2975
3074
  onClick,
3075
+ anchorClassName,
3076
+ anchorStyle,
2976
3077
  className,
2977
3078
  style
2978
3079
  }) {
@@ -2995,6 +3096,8 @@ function SGhostButton({
2995
3096
  };
2996
3097
  const s = SIZE[size];
2997
3098
  const contentColor = disabled ? intent === "inverse" ? "var(--cmp-ghostButton-inverse-disabled)" : "var(--cmp-ghostButton-icon-disabled)" : `var(--cmp-ghostButton-${intent}-content)`;
3099
+ const text = (tooltipText ?? "").trim();
3100
+ const hasTooltip = text !== "";
2998
3101
  const btn = /* @__PURE__ */ jsxs(
2999
3102
  "button",
3000
3103
  {
@@ -3035,13 +3138,13 @@ function SGhostButton({
3035
3138
  ]
3036
3139
  }
3037
3140
  );
3038
- const text = (tooltipText ?? "").trim();
3039
- if (text === "") return btn;
3141
+ if (!hasTooltip) return btn;
3040
3142
  return /* @__PURE__ */ jsxs(RPopover2.Root, { open, onOpenChange: setOpen, children: [
3041
3143
  /* @__PURE__ */ jsx(RPopover2.Anchor, { asChild: true, children: /* @__PURE__ */ jsx(
3042
3144
  "span",
3043
3145
  {
3044
- className: "inline-flex",
3146
+ className: cn("inline-flex", anchorClassName),
3147
+ style: anchorStyle,
3045
3148
  onMouseEnter: openNow,
3046
3149
  onMouseLeave: startHide,
3047
3150
  onFocus: openNow,
@@ -3193,32 +3296,6 @@ var STooltip = forwardRef(function STooltip2({
3193
3296
  ] });
3194
3297
  });
3195
3298
  STooltip.displayName = "STooltip";
3196
- function runRules(value, rules) {
3197
- if (!rules || rules.length === 0) return "";
3198
- for (const rule of rules) {
3199
- const result = rule(value ?? "");
3200
- if (result !== true) return typeof result === "string" ? result : "\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC12\uC785\uB2C8\uB2E4.";
3201
- }
3202
- return "";
3203
- }
3204
- var FormContext = createContext(null);
3205
- function useFormContext() {
3206
- return useContext(FormContext);
3207
- }
3208
- function useFormField(field) {
3209
- const ctx = useFormContext();
3210
- const cbRef = useRef(field);
3211
- cbRef.current = field;
3212
- const name = field.name ?? "";
3213
- useEffect(() => {
3214
- if (!ctx) return;
3215
- return ctx.register({
3216
- name,
3217
- validate: () => cbRef.current.validate(),
3218
- resetValidation: () => cbRef.current.resetValidation()
3219
- });
3220
- }, [ctx, name]);
3221
- }
3222
3299
  var FIELD_SIZE = {
3223
3300
  sm: { height: 28, radius: 4, fieldGap: 12, labelGap: 6, fontSize: 12, lineHeight: 20, icon: 16 },
3224
3301
  md: { height: 36, radius: 6, fieldGap: 16, labelGap: 8, fontSize: 14, lineHeight: 24, icon: 16 }
@@ -3233,6 +3310,7 @@ var SField = forwardRef(function SField2({
3233
3310
  name,
3234
3311
  label = "",
3235
3312
  icon,
3313
+ iconColor,
3236
3314
  labelTooltip = "",
3237
3315
  labelTooltipProps,
3238
3316
  labelWidth,
@@ -3310,7 +3388,15 @@ var SField = forwardRef(function SField2({
3310
3388
  lineHeight: `${s.lineHeight}px`
3311
3389
  },
3312
3390
  children: [
3313
- icon && /* @__PURE__ */ jsx(SIcon, { name: icon, size: s.icon, style: { marginRight: s.labelGap } }),
3391
+ icon && /* @__PURE__ */ jsx(
3392
+ SIcon,
3393
+ {
3394
+ name: icon,
3395
+ size: s.icon,
3396
+ color: iconColor,
3397
+ style: { marginRight: s.labelGap }
3398
+ }
3399
+ ),
3314
3400
  /* @__PURE__ */ jsx("span", { className: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", children: label }),
3315
3401
  (labelTooltip !== "" || labelTooltipProps) && /* @__PURE__ */ jsx("span", { className: "shrink-0", style: { marginLeft: s.labelGap }, children: /* @__PURE__ */ jsx(
3316
3402
  STooltip,
@@ -3360,7 +3446,7 @@ var SField = forwardRef(function SField2({
3360
3446
  borderBottomLeftRadius: s.radius - 1
3361
3447
  },
3362
3448
  children: [
3363
- icon && !label && /* @__PURE__ */ jsx(SIcon, { name: icon, size: s.icon }),
3449
+ icon && !label && /* @__PURE__ */ jsx(SIcon, { name: icon, size: s.icon, color: iconColor }),
3364
3450
  effectiveAddonLabel
3365
3451
  ]
3366
3452
  }
@@ -3375,6 +3461,77 @@ var SField = forwardRef(function SField2({
3375
3461
  }
3376
3462
  );
3377
3463
  });
3464
+ function SPortal({
3465
+ open,
3466
+ onClose,
3467
+ onOpenChange,
3468
+ anchorRef,
3469
+ placement = "bottom",
3470
+ align = "start",
3471
+ offset = 4,
3472
+ alignOffset = 0,
3473
+ fit = false,
3474
+ maxHeight,
3475
+ autoClose = false,
3476
+ persistent = false,
3477
+ viewportPadding = 0,
3478
+ onOpenAutoFocus,
3479
+ onCloseAutoFocus,
3480
+ children,
3481
+ className,
3482
+ style,
3483
+ ariaLabel
3484
+ }) {
3485
+ const portalContainer = usePortalContainer();
3486
+ return /* @__PURE__ */ jsxs(
3487
+ RPopover2.Root,
3488
+ {
3489
+ open,
3490
+ onOpenChange: (next) => {
3491
+ onOpenChange?.(next);
3492
+ if (!next) onClose?.();
3493
+ },
3494
+ children: [
3495
+ /* @__PURE__ */ jsx(RPopover2.Anchor, { virtualRef: anchorRef }),
3496
+ /* @__PURE__ */ jsx(RPopover2.Portal, { container: portalContainer, children: /* @__PURE__ */ jsx(
3497
+ RPopover2.Content,
3498
+ {
3499
+ side: placement,
3500
+ align,
3501
+ sideOffset: offset,
3502
+ alignOffset,
3503
+ collisionPadding: viewportPadding,
3504
+ "aria-label": ariaLabel,
3505
+ onPointerDownOutside: (e) => {
3506
+ if (persistent || anchorRef.current?.contains(e.target)) {
3507
+ e.preventDefault();
3508
+ }
3509
+ },
3510
+ onFocusOutside: (e) => {
3511
+ if (persistent || anchorRef.current?.contains(e.target)) {
3512
+ e.preventDefault();
3513
+ }
3514
+ },
3515
+ onEscapeKeyDown: persistent ? (e) => e.preventDefault() : void 0,
3516
+ onOpenAutoFocus,
3517
+ onCloseAutoFocus,
3518
+ onClick: autoClose ? () => onClose?.() : void 0,
3519
+ className: cn(
3520
+ "z-50 overflow-hidden rounded-(--cmp-listBox-radius) bg-(--cmp-listBox-bg) shadow-normal-sm",
3521
+ maxHeight != null && "overflow-y-auto",
3522
+ fit && "min-w-(--radix-popover-trigger-width)",
3523
+ FLOATING_SLIDE_ANIM,
3524
+ className
3525
+ ),
3526
+ style: { maxHeight, ...style },
3527
+ children
3528
+ }
3529
+ ) })
3530
+ ]
3531
+ }
3532
+ );
3533
+ }
3534
+ SPortal.displayName = "SPortal";
3378
3535
 
3379
3536
  // src/components/SSelect/select.config.ts
3380
3537
  function flattenOptions(options, depth = 1) {
@@ -3409,75 +3566,6 @@ function flattenOptions(options, depth = 1) {
3409
3566
  var selectableOptions = (flat) => flat.filter((o) => !o.isGroup && !o.disabled);
3410
3567
  var isHtmlLabel = (label) => /<[a-z][\s\S]*>/i.test(label);
3411
3568
  var extractText = (html) => html.replace(/<[^>]*>/g, "");
3412
-
3413
- // src/lib/sanitize-inline-html.ts
3414
- var ALLOWED_INLINE_TAGS = /* @__PURE__ */ new Set(["A", "B", "STRONG", "I", "EM", "BR", "SPAN"]);
3415
- var ALLOWED_ATTRS_BY_TAG = {
3416
- A: /* @__PURE__ */ new Set(["href", "target", "rel", "class"])
3417
- };
3418
- var COMMON_ALLOWED_ATTRS = /* @__PURE__ */ new Set(["class"]);
3419
- var SAFE_HREF_RE = /^(https?:|mailto:|tel:|\/|#|\.)/i;
3420
- var DROP_CONTENT_TAGS = /* @__PURE__ */ new Set([
3421
- "SCRIPT",
3422
- "STYLE",
3423
- "IFRAME",
3424
- "OBJECT",
3425
- "EMBED",
3426
- "META",
3427
- "LINK",
3428
- "BASE",
3429
- "NOSCRIPT",
3430
- // SVG/MATH 는 foreign-content 파싱 모드로 전환되어 재직렬화 시 다른 트리로
3431
- // 재파싱될 수 있다(mXSS 벡터). 인라인 라벨에 필요 없으므로 통째로 제거한다.
3432
- "SVG",
3433
- "MATH",
3434
- "TEMPLATE"
3435
- ]);
3436
- var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3437
- var sanitizeNode = (node) => {
3438
- if (node.nodeType === Node.COMMENT_NODE) {
3439
- node.remove();
3440
- return;
3441
- }
3442
- if (node.nodeType !== Node.ELEMENT_NODE) return;
3443
- const element = node;
3444
- const tagName = element.tagName;
3445
- if (DROP_CONTENT_TAGS.has(tagName)) {
3446
- element.remove();
3447
- return;
3448
- }
3449
- Array.from(element.childNodes).forEach((child) => sanitizeNode(child));
3450
- if (ALLOWED_INLINE_TAGS.has(tagName)) {
3451
- if (element.hasAttribute("className")) {
3452
- const val = element.getAttribute("className");
3453
- element.removeAttribute("className");
3454
- if (!element.hasAttribute("class")) element.setAttribute("class", val);
3455
- }
3456
- const allowedAttrs = ALLOWED_ATTRS_BY_TAG[tagName] ?? COMMON_ALLOWED_ATTRS;
3457
- Array.from(element.attributes).forEach((attr) => {
3458
- if (!allowedAttrs.has(attr.name)) element.removeAttribute(attr.name);
3459
- });
3460
- if (tagName === "A") {
3461
- const href = element.getAttribute("href");
3462
- if (href !== null && href !== "" && !SAFE_HREF_RE.test(href)) element.removeAttribute("href");
3463
- if (element.hasAttribute("target")) {
3464
- element.setAttribute("rel", "noopener noreferrer");
3465
- }
3466
- }
3467
- return;
3468
- }
3469
- const parent = element.parentNode;
3470
- if (parent == null) return;
3471
- while (element.firstChild) parent.insertBefore(element.firstChild, element);
3472
- parent.removeChild(element);
3473
- };
3474
- var sanitizeInlineHtml = (value) => {
3475
- if (typeof document === "undefined") return escapeHtml(value);
3476
- const template = document.createElement("template");
3477
- template.innerHTML = value;
3478
- Array.from(template.content.childNodes).forEach((child) => sanitizeNode(child));
3479
- return template.innerHTML;
3480
- };
3481
3569
  var SEARCH_THRESHOLD = 11;
3482
3570
  var SSelect = forwardRef(function SSelect2({
3483
3571
  value,
@@ -3501,6 +3589,7 @@ var SSelect = forwardRef(function SSelect2({
3501
3589
  label,
3502
3590
  labelWidth,
3503
3591
  icon,
3592
+ iconColor,
3504
3593
  labelTooltip,
3505
3594
  addonLabel,
3506
3595
  addonAlign,
@@ -3512,18 +3601,15 @@ var SSelect = forwardRef(function SSelect2({
3512
3601
  }, ref) {
3513
3602
  const multi = type === "multi" || type === "multi_depth";
3514
3603
  const portalContainer = usePortalContainer();
3604
+ const insideModal = useInsideModal();
3515
3605
  const [search, setSearch] = useState("");
3516
3606
  const [ruleError, setRuleError] = useState("");
3517
3607
  const [open, setOpen] = useState(false);
3608
+ const [pendingOpen, setPendingOpen] = useState(false);
3609
+ const [focusedIndex, setFocusedIndex] = useState(-1);
3518
3610
  const triggerRef = useRef(null);
3519
- useImperativeHandle(
3520
- ref,
3521
- () => ({
3522
- focus: () => triggerRef.current?.focus(),
3523
- open: () => setOpen(true)
3524
- }),
3525
- []
3526
- );
3611
+ const searchInputRef = useRef(null);
3612
+ const listRef = useRef(null);
3527
3613
  const flat = useMemo(() => flattenOptions(options), [options]);
3528
3614
  const selectable = useMemo(() => selectableOptions(flat), [flat]);
3529
3615
  const hasLeaf = useMemo(() => flat.some((o) => !o.isGroup), [flat]);
@@ -3541,6 +3627,46 @@ var SSelect = forwardRef(function SSelect2({
3541
3627
  const arr = Array.isArray(value) ? value : [value];
3542
3628
  return arr.map((v) => v != null && typeof v === "object" ? v.value : v).filter((v) => v != null && v !== "").map(String);
3543
3629
  }, [value]);
3630
+ const selectedRef = useRef(selected);
3631
+ selectedRef.current = selected;
3632
+ const emit = (next) => {
3633
+ if (!onValueChange) return;
3634
+ const mapped = next.map(
3635
+ (v) => emitValue ? rawByValue.get(v) ?? v : optionByValue.get(v) ?? { value: v, label: v }
3636
+ );
3637
+ onValueChange(multi ? mapped : mapped[0] ?? (emitValue ? "" : null));
3638
+ };
3639
+ const commitOpen = (next) => {
3640
+ setOpen(next);
3641
+ onOpenChange?.(next);
3642
+ if (!next) {
3643
+ setPendingOpen(false);
3644
+ setSearch("");
3645
+ if (rules && rules.length > 0) {
3646
+ setRuleError(runRules(multi ? selectedRef.current : selectedRef.current[0] ?? "", rules));
3647
+ }
3648
+ }
3649
+ };
3650
+ const requestOpen = (next) => {
3651
+ if (next && insideModal && portalContainer == null) {
3652
+ setPendingOpen(true);
3653
+ return;
3654
+ }
3655
+ commitOpen(next);
3656
+ };
3657
+ useEffect(() => {
3658
+ if (!insideModal || portalContainer == null || !pendingOpen) return;
3659
+ setPendingOpen(false);
3660
+ commitOpen(true);
3661
+ }, [insideModal, pendingOpen, portalContainer]);
3662
+ useImperativeHandle(
3663
+ ref,
3664
+ () => ({
3665
+ focus: () => triggerRef.current?.focus(),
3666
+ open: () => requestOpen(true)
3667
+ }),
3668
+ [insideModal, portalContainer]
3669
+ );
3544
3670
  useFormField({
3545
3671
  name,
3546
3672
  validate: () => {
@@ -3550,13 +3676,6 @@ var SSelect = forwardRef(function SSelect2({
3550
3676
  },
3551
3677
  resetValidation: () => setRuleError("")
3552
3678
  });
3553
- const emit = (next) => {
3554
- if (!onValueChange) return;
3555
- const mapped = next.map(
3556
- (v) => emitValue ? rawByValue.get(v) ?? v : optionByValue.get(v) ?? { value: v, label: v }
3557
- );
3558
- onValueChange(multi ? mapped : mapped[0] ?? (emitValue ? "" : null));
3559
- };
3560
3679
  const filtered = useMemo(() => {
3561
3680
  if (!showSearch || search.trim() === "") return flat;
3562
3681
  const q = search.trim().toLowerCase();
@@ -3567,6 +3686,16 @@ var SSelect = forwardRef(function SSelect2({
3567
3686
  const allValues = selectable.map((o) => o.value);
3568
3687
  const allSelected = allValues.length > 0 && allValues.every((v) => selected.includes(v));
3569
3688
  const hasItems = filtered.some((o) => !o.isGroup);
3689
+ const navigableItems = useMemo(() => {
3690
+ const items = [];
3691
+ if (multi && useSelectAll && allValues.length > 0) {
3692
+ items.push({ value: "__select_all__", isSelectAll: true });
3693
+ }
3694
+ filtered.forEach((o) => {
3695
+ if (!o.isGroup && !o.disabled) items.push({ value: o.value });
3696
+ });
3697
+ return items;
3698
+ }, [allValues.length, filtered, multi, useSelectAll]);
3570
3699
  const triggerText = (() => {
3571
3700
  if (selected.length === 0) return placeholder;
3572
3701
  if (multi) {
@@ -3576,26 +3705,119 @@ var SSelect = forwardRef(function SSelect2({
3576
3705
  return labelByValue.get(selected[0]) ?? selected[0];
3577
3706
  })();
3578
3707
  const isPlaceholder = selected.length === 0;
3579
- const selectStore = /* @__PURE__ */ jsxs(
3580
- Ariakit.SelectProvider,
3708
+ const handleSelect = (nextValue) => {
3709
+ if (multi) {
3710
+ const next = selected.includes(nextValue) ? selected.filter((v) => v !== nextValue) : [...selected, nextValue];
3711
+ emit(next);
3712
+ return;
3713
+ }
3714
+ emit([nextValue]);
3715
+ commitOpen(false);
3716
+ };
3717
+ const handleSelectAll = () => {
3718
+ emit(allSelected ? [] : allValues);
3719
+ };
3720
+ useEffect(() => {
3721
+ if (!open) {
3722
+ setFocusedIndex(-1);
3723
+ return;
3724
+ }
3725
+ setFocusedIndex(navigableItems.length > 0 ? 0 : -1);
3726
+ }, [open, navigableItems.length]);
3727
+ useEffect(() => {
3728
+ if (!open) return;
3729
+ setFocusedIndex(navigableItems.length > 0 ? 0 : -1);
3730
+ }, [search, open, navigableItems.length]);
3731
+ useEffect(() => {
3732
+ if (!open || focusedIndex < 0) return;
3733
+ const list = listRef.current;
3734
+ const focusedEl = list?.querySelector('[data-select-focused="true"]');
3735
+ if (!list || !focusedEl) return;
3736
+ const listRect = list.getBoundingClientRect();
3737
+ const itemRect = focusedEl.getBoundingClientRect();
3738
+ if (itemRect.top < listRect.top) {
3739
+ list.scrollTop += itemRect.top - listRect.top;
3740
+ } else if (itemRect.bottom > listRect.bottom) {
3741
+ list.scrollTop += itemRect.bottom - listRect.bottom;
3742
+ }
3743
+ }, [focusedIndex, open]);
3744
+ const moveFocusedIndex = (direction) => {
3745
+ if (!open || navigableItems.length === 0) return;
3746
+ setFocusedIndex((current) => {
3747
+ if (direction === "next") {
3748
+ return current < 0 ? 0 : (current + 1) % navigableItems.length;
3749
+ }
3750
+ return current <= 0 ? navigableItems.length - 1 : current - 1;
3751
+ });
3752
+ };
3753
+ const selectFocusedItem = () => {
3754
+ if (focusedIndex < 0 || focusedIndex >= navigableItems.length) return;
3755
+ const focused = navigableItems[focusedIndex];
3756
+ if (focused.isSelectAll) {
3757
+ handleSelectAll();
3758
+ return;
3759
+ }
3760
+ handleSelect(focused.value);
3761
+ };
3762
+ const handleKeyDown = (event) => {
3763
+ if (!open) {
3764
+ if ((event.key === "ArrowDown" || event.key === "ArrowUp") && !effectiveDisabled) {
3765
+ event.preventDefault();
3766
+ requestOpen(true);
3767
+ }
3768
+ return;
3769
+ }
3770
+ if (event.key === "ArrowDown") {
3771
+ event.preventDefault();
3772
+ moveFocusedIndex("next");
3773
+ return;
3774
+ }
3775
+ if (event.key === "ArrowUp") {
3776
+ event.preventDefault();
3777
+ moveFocusedIndex("prev");
3778
+ return;
3779
+ }
3780
+ if (event.key === "Enter") {
3781
+ event.preventDefault();
3782
+ selectFocusedItem();
3783
+ return;
3784
+ }
3785
+ if (event.key === "Escape") {
3786
+ event.preventDefault();
3787
+ commitOpen(false);
3788
+ }
3789
+ };
3790
+ return /* @__PURE__ */ jsxs(
3791
+ SField,
3581
3792
  {
3582
- animated: portalContainer == null ? 150 : false,
3583
- value: multi ? selected : selected[0] ?? "",
3584
- setValue: (v) => emit(Array.isArray(v) ? v : v === "" ? [] : [v]),
3585
- open,
3586
- setOpen: (o) => {
3587
- setOpen(o);
3588
- onOpenChange?.(o);
3589
- if (!o && rules && rules.length > 0) {
3590
- setRuleError(runRules(multi ? selected : selected[0] ?? "", rules));
3591
- }
3592
- },
3793
+ label,
3794
+ labelWidth,
3795
+ icon,
3796
+ iconColor,
3797
+ labelTooltip,
3798
+ labelTooltipProps,
3799
+ addonLabel,
3800
+ addonAlign,
3801
+ hint,
3802
+ error: error || ruleError !== "",
3803
+ status: rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0,
3804
+ errorMessage: ruleError !== "" ? ruleError : errorMessage,
3805
+ disabled: effectiveDisabled,
3806
+ width,
3807
+ className,
3808
+ style,
3593
3809
  children: [
3810
+ name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value: selected.join(",") }),
3594
3811
  /* @__PURE__ */ jsxs(
3595
- Ariakit.Select,
3812
+ "button",
3596
3813
  {
3597
3814
  ref: triggerRef,
3815
+ type: "button",
3598
3816
  disabled: effectiveDisabled,
3817
+ "aria-haspopup": "listbox",
3818
+ "aria-expanded": open,
3819
+ onClick: () => requestOpen(!open),
3820
+ onKeyDown: handleKeyDown,
3599
3821
  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",
3600
3822
  style: {
3601
3823
  color: effectiveDisabled ? "var(--cmp-select-text-disabled)" : isPlaceholder ? "var(--cmp-select-icon-default)" : "var(--cmp-select-text-default)"
@@ -3620,178 +3842,237 @@ var SSelect = forwardRef(function SSelect2({
3620
3842
  ]
3621
3843
  }
3622
3844
  ),
3623
- /* @__PURE__ */ jsxs(
3624
- Ariakit.SelectPopover,
3845
+ /* @__PURE__ */ jsx(
3846
+ SPortal,
3625
3847
  {
3626
- portal: true,
3627
- portalElement: portalContainer,
3628
- gutter: 4,
3629
- sameWidth: !dropdownWidth,
3630
- 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",
3848
+ open,
3849
+ onClose: () => commitOpen(false),
3850
+ onOpenChange: (next) => {
3851
+ if (next) requestOpen(true);
3852
+ },
3853
+ anchorRef: triggerRef,
3854
+ placement: "bottom",
3855
+ align: "start",
3856
+ offset: 4,
3857
+ fit: !dropdownWidth,
3858
+ onOpenAutoFocus: (event) => {
3859
+ if (!showSearch) {
3860
+ event.preventDefault();
3861
+ return;
3862
+ }
3863
+ event.preventDefault();
3864
+ requestAnimationFrame(() => searchInputRef.current?.focus());
3865
+ },
3866
+ onCloseAutoFocus: (event) => {
3867
+ event.preventDefault();
3868
+ triggerRef.current?.focus();
3869
+ },
3870
+ className: "rounded-[var(--cmp-listBox-radius)] bg-[var(--cmp-listBox-bg)] shadow-[2px_2px_12px_2px_rgba(0,0,0,0.1)]",
3631
3871
  style: {
3632
- maxHeight: dropdownHeight,
3633
3872
  ...dropdownWidth ? { width: dropdownWidth } : {},
3634
3873
  maxWidth: maxDropdownWidth
3635
3874
  },
3636
- children: [
3637
- showSearch && // 원본 sd-select-list-item-search: sticky + 검색 아이콘 + 테두리 input + focus-within + clear
3638
- /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-[1] bg-[var(--cmp-listBox-bg)] px-[8px] py-[4px]", children: /* @__PURE__ */ 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: [
3639
- /* @__PURE__ */ jsx(SIcon, { name: "search", size: 16, color: "var(--color-grey-70)", className: "shrink-0" }),
3640
- /* @__PURE__ */ jsx(
3641
- Ariakit.Combobox,
3642
- {
3643
- autoSelect: true,
3644
- placeholder: "\uAC80\uC0C9",
3645
- 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)]"
3646
- }
3647
- ),
3648
- search !== "" && /* @__PURE__ */ jsx(
3649
- SGhostButton,
3650
- {
3651
- icon: "close",
3652
- size: "xxs",
3653
- intent: "default",
3654
- ariaLabel: "\uAC80\uC0C9\uC5B4 \uC9C0\uC6B0\uAE30",
3655
- onClick: () => setSearch(""),
3656
- className: "shrink-0"
3657
- }
3658
- )
3659
- ] }) }),
3660
- multi && useSelectAll && /* @__PURE__ */ jsx(
3661
- "button",
3662
- {
3663
- type: "button",
3664
- 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)]",
3665
- onClick: () => emit(allSelected ? [] : allValues),
3666
- children: allSelectedLabel
3667
- }
3668
- ),
3669
- hasItems ? /* @__PURE__ */ jsx(ItemList, { items: filtered, multi, selected, useSearch: showSearch }) : (
3670
- // 원본 sd-select-listbox__empty (표시할 항목이 없을 때)
3671
- /* @__PURE__ */ 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." })
3672
- )
3673
- ]
3875
+ children: /* @__PURE__ */ jsxs(
3876
+ "div",
3877
+ {
3878
+ ref: listRef,
3879
+ 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)]",
3880
+ style: { maxHeight: dropdownHeight },
3881
+ onMouseLeave: () => setFocusedIndex(-1),
3882
+ children: [
3883
+ showSearch && /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-[1] bg-[var(--cmp-listBox-bg)] px-[8px] py-[4px]", children: /* @__PURE__ */ 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: [
3884
+ /* @__PURE__ */ jsx(SIcon, { name: "search", size: 16, color: "var(--color-grey-70)", className: "shrink-0" }),
3885
+ /* @__PURE__ */ jsx(
3886
+ "input",
3887
+ {
3888
+ ref: searchInputRef,
3889
+ value: search,
3890
+ onChange: (e) => setSearch(e.target.value),
3891
+ onKeyDown: handleKeyDown,
3892
+ placeholder: "\uAC80\uC0C9",
3893
+ 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)]"
3894
+ }
3895
+ ),
3896
+ search !== "" && /* @__PURE__ */ jsx(
3897
+ SGhostButton,
3898
+ {
3899
+ icon: "close",
3900
+ size: "xxs",
3901
+ intent: "default",
3902
+ ariaLabel: "\uAC80\uC0C9\uC5B4 \uC9C0\uC6B0\uAE30",
3903
+ onClick: () => setSearch(""),
3904
+ className: "shrink-0"
3905
+ }
3906
+ )
3907
+ ] }) }),
3908
+ hasItems ? /* @__PURE__ */ jsx(
3909
+ ItemList,
3910
+ {
3911
+ items: filtered,
3912
+ multi,
3913
+ selected,
3914
+ focusedValue: navigableItems[focusedIndex]?.isSelectAll ? "__select_all__" : navigableItems[focusedIndex]?.value,
3915
+ showSelectAll: multi && useSelectAll && allValues.length > 0,
3916
+ selectAllLabel: allSelectedLabel,
3917
+ selectAllSelected: allSelected,
3918
+ onSelectAll: handleSelectAll,
3919
+ onSelect: handleSelect,
3920
+ onHoverValue: (value2) => {
3921
+ if (value2 == null) {
3922
+ setFocusedIndex(-1);
3923
+ return;
3924
+ }
3925
+ const index = navigableItems.findIndex(
3926
+ (item) => item.isSelectAll ? value2 === "__select_all__" : item.value === value2
3927
+ );
3928
+ if (index >= 0) setFocusedIndex(index);
3929
+ }
3930
+ }
3931
+ ) : /* @__PURE__ */ 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." })
3932
+ ]
3933
+ }
3934
+ )
3674
3935
  }
3675
3936
  )
3676
3937
  ]
3677
3938
  }
3678
3939
  );
3679
- const control = showSearch ? /* @__PURE__ */ jsx(Ariakit.ComboboxProvider, { value: search, setValue: setSearch, resetValueOnHide: true, children: selectStore }) : selectStore;
3680
- return /* @__PURE__ */ jsxs(
3681
- SField,
3682
- {
3683
- label,
3684
- labelWidth,
3685
- icon,
3686
- labelTooltip,
3687
- labelTooltipProps,
3688
- addonLabel,
3689
- addonAlign,
3690
- hint,
3691
- error: error || ruleError !== "",
3692
- status: rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0,
3693
- errorMessage: ruleError !== "" ? ruleError : errorMessage,
3694
- disabled: effectiveDisabled,
3695
- width,
3696
- className,
3697
- style,
3698
- children: [
3699
- name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value: selected.join(",") }),
3700
- control
3701
- ]
3702
- }
3703
- );
3704
3940
  });
3705
3941
  SSelect.displayName = "SSelect";
3706
3942
  function ItemList({
3707
3943
  items,
3708
3944
  multi,
3709
3945
  selected,
3710
- useSearch
3946
+ focusedValue,
3947
+ showSelectAll,
3948
+ selectAllLabel,
3949
+ selectAllSelected,
3950
+ onSelectAll,
3951
+ onSelect,
3952
+ onHoverValue
3711
3953
  }) {
3712
3954
  const itemBase = cn(
3713
- // group: 체크박스 inverse(group-data-[active-item])를 위해 필요
3714
- "group flex cursor-pointer select-none items-center gap-[8px] text-[12px] leading-[20px] outline-none",
3955
+ "group flex w-full cursor-pointer select-none items-center gap-[8px] border-none bg-transparent text-left text-[12px] leading-[20px] outline-none",
3715
3956
  "text-[color:var(--cmp-listItem-content-default)]",
3716
- // Ariakit은 disabled 항목에 aria-disabled를 붙이므로 aria-disabled 변형으로 스타일 (data-disabled 아님)
3717
- "aria-disabled:cursor-not-allowed aria-disabled:text-[color:var(--cmp-listItem-content-disabled)]"
3718
- );
3719
- const activeClass = "data-[active-item]:bg-[var(--cmp-listItem-bg-hover)] data-[active-item]:text-[color:var(--cmp-listItem-content-hover)]";
3720
- return /* @__PURE__ */ jsx(Fragment, { children: items.map((o) => {
3721
- const padLeft = o.depth === 1 ? 12 : o.depth === 2 ? 20 : 28;
3722
- if (o.isGroup) {
3723
- const isDepth1 = o.depth === 1;
3724
- return /* @__PURE__ */ jsx(
3725
- "div",
3726
- {
3727
- className: cn(
3728
- "select-none py-[4px] pr-[12px] text-[12px] leading-[20px] text-[color:var(--cmp-listItem-content-default)]",
3729
- 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",
3730
- // 하위가 전부 비활성이면(select.config 에서 bubble-up) 어미 그룹도 비활성 처리:
3731
- // 텍스트는 회색(#888), depth1 그룹은 파란 배경 대신 회색(#EEE)으로 (depth2 는 #F6F6F6 유지)
3732
- o.disabled && "cursor-not-allowed text-[color:var(--cmp-listItem-content-disabled)]",
3733
- o.disabled && isDepth1 && "bg-[var(--cmp-listItem-depth1-bg-disabled)]"
3734
- ),
3735
- style: { paddingLeft: padLeft },
3736
- children: o.label
3737
- },
3738
- o.value
3739
- );
3740
- }
3741
- const isSelected = selected.includes(o.value);
3742
- return /* @__PURE__ */ jsxs(
3743
- Ariakit.SelectItem,
3957
+ "disabled:cursor-not-allowed disabled:text-[color:var(--cmp-listItem-content-disabled)]"
3958
+ );
3959
+ 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)]";
3960
+ return /* @__PURE__ */ jsxs("div", { role: "listbox", "aria-multiselectable": multi || void 0, children: [
3961
+ showSelectAll && /* @__PURE__ */ jsxs(
3962
+ "button",
3744
3963
  {
3745
- value: o.value,
3746
- disabled: o.disabled,
3747
- accessibleWhenDisabled: true,
3964
+ type: "button",
3965
+ "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
3966
+ "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
3748
3967
  className: cn(
3749
3968
  itemBase,
3750
- isSelected && !multi && "font-bold text-[color:var(--cmp-listItem-content-selected)]",
3751
- // Ariakit은 disabled 항목도 hover 시 active(data-active-item) 처리하므로,
3752
- // 비활성 항목에는 hover 하이라이트를 적용하지 않아 회색 비활성 스타일을 유지한다.
3753
- // (원본 Stencil의 `--focused...:not(--disabled)` 규칙과 동일한 의도)
3754
- !o.disabled && activeClass
3755
- ),
3756
- style: { paddingTop: 4, paddingBottom: 4, paddingRight: 12, paddingLeft: padLeft },
3757
- render: useSearch ? /* @__PURE__ */ jsx(Ariakit.ComboboxItem, {}) : void 0,
3969
+ focusedValue === "__select_all__" && "bg-[var(--cmp-listItem-bg-hover)] text-[color:var(--cmp-listItem-content-hover)]"
3970
+ ),
3971
+ style: { paddingTop: 4, paddingBottom: 4, paddingRight: 12, paddingLeft: 12 },
3972
+ onMouseEnter: () => onHoverValue("__select_all__"),
3973
+ onMouseLeave: () => onHoverValue(null),
3974
+ onClick: onSelectAll,
3758
3975
  children: [
3759
- multi && // hover(active) 시 checkbox inverse — 파란 hover 배경 위에서 checked/unchecked 모두 흰 테두리 + 투명 배경
3760
3976
  /* @__PURE__ */ jsx(
3761
3977
  "span",
3762
3978
  {
3763
3979
  className: cn(
3764
3980
  "flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[2px] border border-solid",
3765
- o.disabled ? (
3766
- // 비활성: 회색 채움(#E1E1E1) + 회색 테두리(#CCCCCC)
3767
- "cursor-not-allowed border-[color:var(--cmp-checkbox-border-disabled)] bg-[var(--cmp-checkbox-bg-disabled)]"
3768
- ) : cn(
3769
- "group-data-[active-item]:border-[color:var(--cmp-checkbox-checked-border-inverse)]",
3770
- 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"
3771
- )
3981
+ 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)]"
3772
3982
  ),
3773
- children: isSelected && /* @__PURE__ */ jsx(
3983
+ children: selectAllSelected && /* @__PURE__ */ jsx(
3774
3984
  SIcon,
3775
3985
  {
3776
3986
  name: "checkboxCheck",
3777
3987
  size: 12,
3778
- color: o.disabled ? "var(--cmp-checkbox-checked-icon-disabled)" : "var(--cmp-checkbox-checked-icon-default)"
3988
+ color: "var(--cmp-checkbox-checked-icon-default)"
3779
3989
  }
3780
3990
  )
3781
3991
  }
3782
3992
  ),
3783
- isHtmlLabel(o.label) ? /* @__PURE__ */ jsx(
3784
- "span",
3785
- {
3786
- className: "flex-1 truncate",
3787
- dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(o.label) }
3788
- }
3789
- ) : /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: o.label })
3993
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: selectAllLabel })
3790
3994
  ]
3791
- },
3792
- o.value
3793
- );
3794
- }) });
3995
+ }
3996
+ ),
3997
+ items.map((o) => {
3998
+ const padLeft = o.depth === 1 ? 12 : o.depth === 2 ? 20 : 28;
3999
+ if (o.isGroup) {
4000
+ const isDepth1 = o.depth === 1;
4001
+ return /* @__PURE__ */ jsx(
4002
+ "div",
4003
+ {
4004
+ className: cn(
4005
+ "select-none py-[4px] pr-[12px] text-[12px] leading-[20px] text-[color:var(--cmp-listItem-content-default)]",
4006
+ 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",
4007
+ // 하위가 전부 비활성이면(select.config 에서 bubble-up) 어미 그룹도 비활성 처리:
4008
+ // 텍스트는 회색(#888), depth1 그룹은 파란 배경 대신 회색(#EEE)으로 (depth2 는 #F6F6F6 유지)
4009
+ o.disabled && "cursor-not-allowed text-[color:var(--cmp-listItem-content-disabled)]",
4010
+ o.disabled && isDepth1 && "bg-[var(--cmp-listItem-depth1-bg-disabled)]"
4011
+ ),
4012
+ style: { paddingLeft: padLeft },
4013
+ children: o.label
4014
+ },
4015
+ o.value
4016
+ );
4017
+ }
4018
+ const isSelected = selected.includes(o.value);
4019
+ return /* @__PURE__ */ jsxs(
4020
+ "button",
4021
+ {
4022
+ type: "button",
4023
+ disabled: o.disabled,
4024
+ role: "option",
4025
+ "aria-selected": isSelected,
4026
+ "data-select-focused": focusedValue === o.value ? "true" : void 0,
4027
+ "data-select-active": focusedValue === o.value ? "true" : void 0,
4028
+ className: cn(
4029
+ itemBase,
4030
+ isSelected && !multi && "font-bold text-[color:var(--cmp-listItem-content-selected)]",
4031
+ focusedValue === o.value && "bg-[var(--cmp-listItem-bg-hover)] text-[color:var(--cmp-listItem-content-hover)]",
4032
+ // 비활성 항목은 hover/focus 스타일 대상에서 제외한다.
4033
+ // (:hover 는 disabled 버튼에도 매칭되므로 클래스 자체를 붙이지 않아야 한다)
4034
+ !o.disabled && activeClass
4035
+ ),
4036
+ style: { paddingTop: 4, paddingBottom: 4, paddingRight: 12, paddingLeft: padLeft },
4037
+ onMouseEnter: () => onHoverValue(o.value),
4038
+ onMouseLeave: () => onHoverValue(null),
4039
+ onClick: () => onSelect(o.value),
4040
+ children: [
4041
+ multi && /* @__PURE__ */ jsx(
4042
+ "span",
4043
+ {
4044
+ className: cn(
4045
+ "flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[2px] border border-solid",
4046
+ o.disabled ? (
4047
+ // 비활성: 회색 채움(#E1E1E1) + 회색 테두리(#CCCCCC)
4048
+ "cursor-not-allowed border-[color:var(--cmp-checkbox-border-disabled)] bg-[var(--cmp-checkbox-bg-disabled)]"
4049
+ ) : cn(
4050
+ 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)]"
4051
+ )
4052
+ ),
4053
+ children: isSelected && /* @__PURE__ */ jsx(
4054
+ SIcon,
4055
+ {
4056
+ name: "checkboxCheck",
4057
+ size: 12,
4058
+ color: o.disabled ? "var(--cmp-checkbox-checked-icon-disabled)" : "var(--cmp-checkbox-checked-icon-default)"
4059
+ }
4060
+ )
4061
+ }
4062
+ ),
4063
+ isHtmlLabel(o.label) ? /* @__PURE__ */ jsx(
4064
+ "span",
4065
+ {
4066
+ className: "flex-1 truncate",
4067
+ dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(o.label) }
4068
+ }
4069
+ ) : /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: o.label })
4070
+ ]
4071
+ },
4072
+ o.value
4073
+ );
4074
+ })
4075
+ ] });
3795
4076
  }
3796
4077
 
3797
4078
  export { SSelect };