sh-ui-cli 0.45.3 → 0.47.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 (93) hide show
  1. package/data/changelog/versions.json +26 -0
  2. package/data/registry/react/components/accordion/index.module.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.module.css +111 -0
  4. package/data/registry/react/components/avatar/index.module.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.module.css +36 -0
  6. package/data/registry/react/components/badge/index.module.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.module.css +57 -0
  8. package/data/registry/react/components/breadcrumb/index.module.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.module.css +82 -0
  10. package/data/registry/react/components/button/index.module.tsx +45 -0
  11. package/data/registry/react/components/button/styles.module.css +92 -0
  12. package/data/registry/react/components/calendar/index.module.tsx +806 -0
  13. package/data/registry/react/components/calendar/styles.module.css +213 -0
  14. package/data/registry/react/components/card/index.module.tsx +63 -0
  15. package/data/registry/react/components/card/styles.module.css +73 -0
  16. package/data/registry/react/components/carousel/index.module.tsx +430 -0
  17. package/data/registry/react/components/carousel/styles.module.css +155 -0
  18. package/data/registry/react/components/checkbox/index.module.tsx +96 -0
  19. package/data/registry/react/components/checkbox/styles.module.css +75 -0
  20. package/data/registry/react/components/code-editor/index.module.tsx +230 -0
  21. package/data/registry/react/components/code-editor/styles.module.css +76 -0
  22. package/data/registry/react/components/code-panel/index.module.tsx +191 -0
  23. package/data/registry/react/components/code-panel/styles.module.css +124 -0
  24. package/data/registry/react/components/color-picker/index.module.tsx +467 -0
  25. package/data/registry/react/components/color-picker/styles.module.css +166 -0
  26. package/data/registry/react/components/combobox/index.module.tsx +165 -0
  27. package/data/registry/react/components/combobox/styles.module.css +151 -0
  28. package/data/registry/react/components/context-menu/index.module.tsx +251 -0
  29. package/data/registry/react/components/context-menu/styles.module.css +140 -0
  30. package/data/registry/react/components/date-picker/index.module.tsx +520 -0
  31. package/data/registry/react/components/date-picker/styles.module.css +103 -0
  32. package/data/registry/react/components/dialog/index.module.tsx +95 -0
  33. package/data/registry/react/components/dialog/styles.module.css +127 -0
  34. package/data/registry/react/components/dropdown-menu/index.module.tsx +255 -0
  35. package/data/registry/react/components/dropdown-menu/styles.module.css +150 -0
  36. package/data/registry/react/components/file-upload/index.module.tsx +487 -0
  37. package/data/registry/react/components/file-upload/styles.module.css +170 -0
  38. package/data/registry/react/components/form/index.module.tsx +61 -0
  39. package/data/registry/react/components/form/styles.module.css +47 -0
  40. package/data/registry/react/components/header/index.module.tsx +805 -0
  41. package/data/registry/react/components/header/styles.module.css +350 -0
  42. package/data/registry/react/components/input/index.module.tsx +486 -0
  43. package/data/registry/react/components/input/styles.module.css +200 -0
  44. package/data/registry/react/components/label/index.module.tsx +52 -0
  45. package/data/registry/react/components/label/styles.module.css +90 -0
  46. package/data/registry/react/components/markdown-editor/index.module.tsx +119 -0
  47. package/data/registry/react/components/markdown-editor/styles.module.css +160 -0
  48. package/data/registry/react/components/menubar/index.module.tsx +32 -0
  49. package/data/registry/react/components/menubar/styles.module.css +45 -0
  50. package/data/registry/react/components/numeric-input/index.module.tsx +148 -0
  51. package/data/registry/react/components/numeric-input/styles.module.css +56 -0
  52. package/data/registry/react/components/page-toc/index.module.tsx +174 -0
  53. package/data/registry/react/components/page-toc/styles.module.css +82 -0
  54. package/data/registry/react/components/pagination/index.module.tsx +269 -0
  55. package/data/registry/react/components/pagination/styles.module.css +105 -0
  56. package/data/registry/react/components/popover/index.module.tsx +113 -0
  57. package/data/registry/react/components/popover/styles.module.css +65 -0
  58. package/data/registry/react/components/progress/index.module.tsx +54 -0
  59. package/data/registry/react/components/progress/styles.module.css +41 -0
  60. package/data/registry/react/components/radio/index.module.tsx +65 -0
  61. package/data/registry/react/components/radio/styles.module.css +80 -0
  62. package/data/registry/react/components/rich-text-editor/index.module.tsx +348 -0
  63. package/data/registry/react/components/rich-text-editor/styles.module.css +196 -0
  64. package/data/registry/react/components/select/index.module.tsx +234 -0
  65. package/data/registry/react/components/select/styles.module.css +193 -0
  66. package/data/registry/react/components/separator/index.module.tsx +46 -0
  67. package/data/registry/react/components/separator/styles.module.css +15 -0
  68. package/data/registry/react/components/sidebar/index.module.tsx +1067 -0
  69. package/data/registry/react/components/sidebar/styles.module.css +502 -0
  70. package/data/registry/react/components/skeleton/index.module.tsx +22 -0
  71. package/data/registry/react/components/skeleton/styles.module.css +24 -0
  72. package/data/registry/react/components/slider/index.module.tsx +298 -0
  73. package/data/registry/react/components/slider/styles.module.css +64 -0
  74. package/data/registry/react/components/spinner/index.module.tsx +38 -0
  75. package/data/registry/react/components/spinner/styles.module.css +37 -0
  76. package/data/registry/react/components/switch/index.module.tsx +39 -0
  77. package/data/registry/react/components/switch/styles.module.css +83 -0
  78. package/data/registry/react/components/tabs/index.module.tsx +91 -0
  79. package/data/registry/react/components/tabs/styles.module.css +148 -0
  80. package/data/registry/react/components/textarea/index.module.tsx +23 -0
  81. package/data/registry/react/components/textarea/styles.module.css +54 -0
  82. package/data/registry/react/components/toast/index.module.tsx +258 -0
  83. package/data/registry/react/components/toast/styles.module.css +290 -0
  84. package/data/registry/react/components/toggle/index.module.tsx +131 -0
  85. package/data/registry/react/components/toggle/styles.module.css +85 -0
  86. package/data/registry/react/components/tooltip/index.module.tsx +83 -0
  87. package/data/registry/react/components/tooltip/styles.module.css +44 -0
  88. package/data/registry/react/registry.json +604 -1
  89. package/data/tokens/build.mjs +4 -0
  90. package/package.json +1 -1
  91. package/src/add.mjs +12 -12
  92. package/src/api.d.ts +4 -3
  93. package/src/constants.js +4 -3
@@ -0,0 +1,148 @@
1
+ .tabs {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--space-3);
5
+ width: 100%;
6
+ }
7
+
8
+ .tabs[data-orientation="vertical"] {
9
+ flex-direction: row;
10
+ }
11
+
12
+ .tabs__list {
13
+ position: relative;
14
+ display: inline-flex;
15
+ align-items: center;
16
+ gap: var(--space-1);
17
+ width: fit-content;
18
+ }
19
+
20
+ .tabs[data-orientation="vertical"] > .tabs__list {
21
+ flex-direction: column;
22
+ align-items: stretch;
23
+ }
24
+
25
+ .tabs__trigger {
26
+ position: relative;
27
+ z-index: 1;
28
+ display: inline-flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ gap: 0.375rem;
32
+ padding: var(--space-2) var(--space-3);
33
+ background: transparent;
34
+ color: var(--foreground-muted, var(--foreground));
35
+ border: 0;
36
+ font-size: var(--text-sm);
37
+ font-weight: var(--weight-medium);
38
+ line-height: 1;
39
+ cursor: pointer;
40
+ transition: color var(--duration-fast), background-color var(--duration-fast);
41
+ user-select: none;
42
+ -webkit-tap-highlight-color: transparent;
43
+ white-space: nowrap;
44
+ }
45
+
46
+ .tabs__trigger:hover:not(:disabled):not([data-selected]) {
47
+ color: var(--foreground);
48
+ }
49
+
50
+ .tabs__trigger:focus-visible {
51
+ outline: var(--border-width-strong) solid var(--foreground);
52
+ outline-offset: 2px;
53
+ }
54
+
55
+ .tabs__trigger[data-selected] {
56
+ color: var(--foreground);
57
+ }
58
+
59
+ .tabs__trigger:disabled {
60
+ opacity: var(--opacity-disabled);
61
+ cursor: not-allowed;
62
+ }
63
+
64
+ .tabs__indicator {
65
+ position: absolute;
66
+ transition: top 180ms, left 180ms, width 180ms, height 180ms;
67
+ z-index: 0;
68
+ pointer-events: none;
69
+ }
70
+
71
+ .tabs__indicator[data-activation-direction="none"] {
72
+ transition: none;
73
+ }
74
+
75
+ .tabs__content {
76
+ outline: none;
77
+ }
78
+
79
+ .tabs__content:focus-visible {
80
+ outline: var(--border-width-strong) solid var(--foreground);
81
+ outline-offset: 2px;
82
+ border-radius: var(--radius);
83
+ }
84
+
85
+ /* ─────────────── variant: underline (기본) ─────────────── */
86
+ .tabs[data-variant="underline"] > .tabs__list {
87
+ width: 100%;
88
+ gap: 0;
89
+ box-shadow: inset 0 -1px 0 var(--border);
90
+ }
91
+
92
+ .tabs[data-variant="underline"] .tabs__trigger {
93
+ padding: 0.625rem var(--space-4);
94
+ }
95
+
96
+ .tabs[data-variant="underline"] .tabs__indicator {
97
+ top: var(--active-tab-top);
98
+ left: var(--active-tab-left);
99
+ width: var(--active-tab-width);
100
+ height: var(--active-tab-height);
101
+ box-shadow: inset 0 -2px 0 var(--foreground);
102
+ }
103
+
104
+ .tabs[data-variant="underline"][data-orientation="vertical"] > .tabs__list {
105
+ width: auto;
106
+ box-shadow: inset -1px 0 0 var(--border);
107
+ }
108
+
109
+ .tabs[data-variant="underline"][data-orientation="vertical"] .tabs__indicator {
110
+ box-shadow: inset -2px 0 0 var(--foreground);
111
+ }
112
+
113
+ /* ─────────────── variant: pill (세그먼트) ─────────────── */
114
+ .tabs[data-variant="pill"] > .tabs__list {
115
+ padding: var(--space-1);
116
+ background: var(--background-muted, var(--background));
117
+ border: 1px solid var(--border);
118
+ border-radius: var(--radius);
119
+ }
120
+
121
+ .tabs[data-variant="pill"] .tabs__trigger {
122
+ padding: 0.375rem var(--space-3);
123
+ border-radius: calc(var(--radius) - 2px);
124
+ }
125
+
126
+ .tabs[data-variant="pill"] .tabs__indicator {
127
+ top: var(--active-tab-top);
128
+ left: var(--active-tab-left);
129
+ width: var(--active-tab-width);
130
+ height: var(--active-tab-height);
131
+ background: var(--background);
132
+ border-radius: calc(var(--radius) - 2px);
133
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
134
+ }
135
+
136
+ /* ─────────────── variant: plain (컨테이너 없음) ─────────────── */
137
+ .tabs[data-variant="plain"] .tabs__trigger {
138
+ padding: 0.375rem var(--space-2);
139
+ border-radius: calc(var(--radius) - 2px);
140
+ }
141
+
142
+ .tabs[data-variant="plain"] .tabs__trigger[data-selected] {
143
+ background: var(--background-muted, transparent);
144
+ }
145
+
146
+ .tabs[data-variant="plain"] .tabs__indicator {
147
+ display: none;
148
+ }
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ import styles from "./styles.module.css";
3
+
4
+ import { cn } from "@SH_UI_UTILS@";
5
+ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
6
+
7
+
8
+ /**
9
+ * 여러 줄 텍스트 입력. 네이티브 `<textarea>` 위에 sh-ui 토큰 스타일만 입혔다.
10
+ * 라벨/오류 메시지는 외부에서 조립한다 — 단독 사용 시 `<Label>`과 `aria-describedby` 연결 권장.
11
+ */
12
+ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
13
+ ({ className, ...props }, ref) => {
14
+ return (
15
+ <textarea
16
+ ref={ref}
17
+ className={cn(styles.textarea, className)}
18
+ {...props}
19
+ />
20
+ );
21
+ },
22
+ );
23
+ Textarea.displayName = "Textarea";
@@ -0,0 +1,54 @@
1
+ .textarea {
2
+ display: block;
3
+ width: 100%;
4
+ min-height: 5rem;
5
+ padding: var(--space-2) var(--space-3);
6
+ background: var(--background);
7
+ color: var(--foreground);
8
+ border: 1px solid var(--border);
9
+ border-radius: var(--radius);
10
+ font-family: inherit;
11
+ font-size: var(--text-sm);
12
+ line-height: 1.5;
13
+ resize: vertical;
14
+ transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
15
+ -webkit-tap-highlight-color: transparent;
16
+ }
17
+
18
+ @media (hover: none) and (pointer: coarse) {
19
+ .textarea {
20
+ font-size: var(--text-base);
21
+ }
22
+ }
23
+
24
+ .textarea::placeholder {
25
+ color: var(--foreground-subtle);
26
+ }
27
+
28
+ .textarea:hover:not(:disabled):not(:focus) {
29
+ border-color: var(--border-strong);
30
+ }
31
+
32
+ .textarea:focus {
33
+ outline: none;
34
+ border-color: var(--foreground);
35
+ box-shadow: 0 0 0 1px var(--foreground);
36
+ }
37
+
38
+ .textarea:disabled {
39
+ opacity: var(--opacity-disabled);
40
+ cursor: not-allowed;
41
+ background: var(--background-subtle);
42
+ }
43
+
44
+ .textarea:read-only {
45
+ background: var(--background-subtle);
46
+ }
47
+
48
+ /* 에러 상태 — aria-invalid="true" 기반 */
49
+ .textarea[aria-invalid="true"] {
50
+ border-color: var(--danger);
51
+ }
52
+ .textarea[aria-invalid="true"]:focus {
53
+ box-shadow: 0 0 0 1px var(--danger);
54
+ }
@@ -0,0 +1,258 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { cn } from "@SH_UI_UTILS@";
6
+ import styles from "./styles.module.css";
7
+
8
+ /* ───────── Types ───────── */
9
+
10
+ type ToastVariant = "default" | "success" | "danger" | "warning";
11
+
12
+ interface ToastItem {
13
+ id: string;
14
+ /** 짧은 제목. 한 줄 굵게 표시. */
15
+ title?: React.ReactNode;
16
+ /** 본문 설명. 줄바꿈 가능. */
17
+ description?: React.ReactNode;
18
+ variant: ToastVariant;
19
+ duration: number;
20
+ /** 우측에 표시될 액션 노드(예: "다시 시도" 버튼). */
21
+ action?: React.ReactNode;
22
+ }
23
+
24
+ type ToastInput = Omit<ToastItem, "id" | "variant" | "duration"> & {
25
+ /**
26
+ * 토스트 종류.
27
+ * - `default` — 정보성 알림
28
+ * - `success` — 성공
29
+ * - `warning` — 주의
30
+ * - `danger` — 오류 (스크린리더에 즉시 읽힘)
31
+ *
32
+ * @default "default"
33
+ */
34
+ variant?: ToastVariant;
35
+ /**
36
+ * 자동 닫힘 시간(ms). `0` 이하면 자동 닫힘 비활성.
37
+ * @default 4000
38
+ */
39
+ duration?: number;
40
+ };
41
+
42
+ /* ───────── Store (외부 상태) ───────── */
43
+
44
+ type Listener = () => void;
45
+
46
+ let toasts: ToastItem[] = [];
47
+ const listeners = new Set<Listener>();
48
+
49
+ const notify = () => listeners.forEach((l) => l());
50
+
51
+ let counter = 0;
52
+ const genId = () => `sh-toast-${++counter}`;
53
+
54
+ function addToast(input: ToastInput): string {
55
+ const id = genId();
56
+ toasts = [
57
+ ...toasts,
58
+ {
59
+ id,
60
+ variant: input.variant ?? "default",
61
+ duration: input.duration ?? 4000,
62
+ title: input.title,
63
+ description: input.description,
64
+ action: input.action,
65
+ },
66
+ ];
67
+ notify();
68
+ return id;
69
+ }
70
+
71
+ function removeToast(id: string) {
72
+ toasts = toasts.filter((t) => t.id !== id);
73
+ notify();
74
+ }
75
+
76
+ function useToastStore() {
77
+ return React.useSyncExternalStore(
78
+ (cb) => {
79
+ listeners.add(cb);
80
+ return () => listeners.delete(cb);
81
+ },
82
+ () => toasts,
83
+ () => toasts,
84
+ );
85
+ }
86
+
87
+ /* ───────── Public API ───────── */
88
+
89
+ /**
90
+ * 토스트 알림을 발생시키는 명령형 API. 문자열만 전달하면 description으로 사용된다.
91
+ * variant별 헬퍼(`toast.success` 등)와 `toast.dismiss(id)`도 함께 노출된다.
92
+ * 사용 전에 앱 어딘가에 `<Toaster />`가 마운트되어 있어야 화면에 보인다.
93
+ *
94
+ * @param input - 문자열(빠른 알림) 또는 `{ title, description, variant, duration, action }`.
95
+ * @returns 생성된 토스트 id. `toast.dismiss(id)`로 수동 닫을 때 사용.
96
+ * @example
97
+ * toast.success("저장 완료");
98
+ * toast({ title: "오류", description: e.message, variant: "danger" });
99
+ */
100
+ export function toast(input: ToastInput | string): string {
101
+ if (typeof input === "string") return addToast({ description: input });
102
+ return addToast(input);
103
+ }
104
+
105
+ toast.success = (input: ToastInput | string) =>
106
+ toast(typeof input === "string" ? { description: input, variant: "success" } : { ...input, variant: "success" });
107
+
108
+ toast.danger = (input: ToastInput | string) =>
109
+ toast(typeof input === "string" ? { description: input, variant: "danger" } : { ...input, variant: "danger" });
110
+
111
+ toast.warning = (input: ToastInput | string) =>
112
+ toast(typeof input === "string" ? { description: input, variant: "warning" } : { ...input, variant: "warning" });
113
+
114
+ toast.dismiss = removeToast;
115
+
116
+ /* ───────── Icons ───────── */
117
+
118
+ function CheckIcon() {
119
+ return (
120
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
121
+ <circle cx="8" cy="8" r="7.25" stroke="currentColor" strokeWidth="1.5" />
122
+ <path d="M5 8.5 7 10.5 11 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
123
+ </svg>
124
+ );
125
+ }
126
+
127
+ function AlertIcon() {
128
+ return (
129
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
130
+ <circle cx="8" cy="8" r="7.25" stroke="currentColor" strokeWidth="1.5" />
131
+ <path d="M8 5v3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
132
+ <circle cx="8" cy="11" r="0.75" fill="currentColor" />
133
+ </svg>
134
+ );
135
+ }
136
+
137
+ function XIcon() {
138
+ return (
139
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
140
+ <circle cx="8" cy="8" r="7.25" stroke="currentColor" strokeWidth="1.5" />
141
+ <path d="M6 6l4 4M10 6l-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
142
+ </svg>
143
+ );
144
+ }
145
+
146
+ const VARIANT_ICON: Record<ToastVariant, React.ReactNode> = {
147
+ default: null,
148
+ success: <CheckIcon />,
149
+ danger: <XIcon />,
150
+ warning: <AlertIcon />,
151
+ };
152
+
153
+ /* ───────── Single Toast ───────── */
154
+
155
+ function ToastCard({ item, onDismiss }: { item: ToastItem; onDismiss: () => void }) {
156
+ const [exiting, setExiting] = React.useState(false);
157
+
158
+ React.useEffect(() => {
159
+ if (item.duration <= 0) return;
160
+ const timer = setTimeout(() => setExiting(true), item.duration);
161
+ return () => clearTimeout(timer);
162
+ }, [item.duration]);
163
+
164
+ const handleAnimationEnd = () => {
165
+ if (exiting) onDismiss();
166
+ };
167
+
168
+ const icon = VARIANT_ICON[item.variant];
169
+
170
+ return (
171
+ <div
172
+ className={cn(styles.toast, styles[`toast--${item.variant}`])}
173
+ role={item.variant === "danger" ? "alert" : "status"}
174
+ aria-live={item.variant === "danger" ? "assertive" : "polite"}
175
+ data-exiting={exiting || undefined}
176
+ onAnimationEnd={handleAnimationEnd}
177
+ >
178
+ {icon && <span className={styles.toast__icon}>{icon}</span>}
179
+ <div className={styles.toast__body}>
180
+ {item.title && <p className={styles.toast__title}>{item.title}</p>}
181
+ {item.description && <p className={styles.toast__description}>{item.description}</p>}
182
+ </div>
183
+ {item.action && <div className={styles.toast__action}>{item.action}</div>}
184
+ <button
185
+ type="button"
186
+ className={styles.toast__close}
187
+ onClick={() => setExiting(true)}
188
+ aria-label="닫기"
189
+ >
190
+ ×
191
+ </button>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ /* ───────── Toaster (Provider) ───────── */
197
+
198
+ export type ToastPosition =
199
+ | "top-left"
200
+ | "top-right"
201
+ | "top-center"
202
+ | "bottom-left"
203
+ | "bottom-right"
204
+ | "bottom-center";
205
+
206
+ export interface ToasterProps {
207
+ /**
208
+ * 토스트 표시 위치.
209
+ * `top-*` 위치는 새 토스트가 위에 쌓이고, `bottom-*` 위치는 아래에 쌓인다.
210
+ *
211
+ * @default "bottom-right"
212
+ */
213
+ position?: ToastPosition;
214
+ /**
215
+ * Portal이 마운트될 DOM 노드.
216
+ * @default document.body
217
+ */
218
+ container?: Element | null;
219
+ }
220
+
221
+ /**
222
+ * `toast()`로 발생한 알림을 그리는 viewport. 앱 루트에 한 번만 마운트한다.
223
+ * `danger` variant는 `role="alert"`+`aria-live="assertive"`로 즉시 읽히고, 그 외는 `polite`로 큐잉된다.
224
+ */
225
+ export function Toaster({ position = "bottom-right", container }: ToasterProps) {
226
+ const items = useToastStore();
227
+ const [mounted, setMounted] = React.useState(false);
228
+
229
+ React.useEffect(() => setMounted(true), []);
230
+
231
+ if (!mounted || items.length === 0) return null;
232
+
233
+ const isBottom = position.startsWith("bottom");
234
+
235
+ const el = (
236
+ <div
237
+ className={styles["toast-viewport"]}
238
+ data-position={position}
239
+ aria-label="알림"
240
+ >
241
+ {isBottom ? items.map((item) => (
242
+ <ToastCard
243
+ key={item.id}
244
+ item={item}
245
+ onDismiss={() => removeToast(item.id)}
246
+ />
247
+ )) : [...items].reverse().map((item) => (
248
+ <ToastCard
249
+ key={item.id}
250
+ item={item}
251
+ onDismiss={() => removeToast(item.id)}
252
+ />
253
+ ))}
254
+ </div>
255
+ );
256
+
257
+ return createPortal(el, container ?? document.body);
258
+ }