sh-ui-cli 0.43.0 → 0.45.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 (46) hide show
  1. package/data/changelog/versions.json +24 -0
  2. package/data/registry/react/components/accordion/index.tailwind.tsx +88 -0
  3. package/data/registry/react/components/avatar/index.tailwind.tsx +74 -0
  4. package/data/registry/react/components/badge/index.tailwind.tsx +47 -0
  5. package/data/registry/react/components/breadcrumb/index.tailwind.tsx +138 -0
  6. package/data/registry/react/components/calendar/index.tailwind.tsx +498 -0
  7. package/data/registry/react/components/carousel/index.tailwind.tsx +309 -0
  8. package/data/registry/react/components/checkbox/index.tailwind.tsx +72 -0
  9. package/data/registry/react/components/code-editor/index.tailwind.tsx +168 -0
  10. package/data/registry/react/components/code-panel/index.tailwind.tsx +107 -0
  11. package/data/registry/react/components/color-picker/index.tailwind.tsx +309 -0
  12. package/data/registry/react/components/combobox/index.tailwind.tsx +160 -0
  13. package/data/registry/react/components/context-menu/index.tailwind.tsx +170 -0
  14. package/data/registry/react/components/date-picker/index.tailwind.tsx +294 -0
  15. package/data/registry/react/components/dialog/index.tailwind.tsx +96 -0
  16. package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +205 -0
  17. package/data/registry/react/components/file-upload/index.tailwind.tsx +290 -0
  18. package/data/registry/react/components/form/field.tailwind.tsx +165 -0
  19. package/data/registry/react/components/form/form.tailwind.tsx +129 -0
  20. package/data/registry/react/components/form/index.tailwind.tsx +49 -0
  21. package/data/registry/react/components/header/index.tailwind.tsx +550 -0
  22. package/data/registry/react/components/label/index.tailwind.tsx +78 -0
  23. package/data/registry/react/components/markdown-editor/index.tailwind.tsx +118 -0
  24. package/data/registry/react/components/menubar/index.tailwind.tsx +32 -0
  25. package/data/registry/react/components/numeric-input/index.tailwind.tsx +113 -0
  26. package/data/registry/react/components/page-toc/index.tailwind.tsx +149 -0
  27. package/data/registry/react/components/pagination/index.tailwind.tsx +148 -0
  28. package/data/registry/react/components/popover/index.tailwind.tsx +77 -0
  29. package/data/registry/react/components/progress/index.tailwind.tsx +60 -0
  30. package/data/registry/react/components/radio/index.tailwind.tsx +54 -0
  31. package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +211 -0
  32. package/data/registry/react/components/select/index.tailwind.tsx +199 -0
  33. package/data/registry/react/components/separator/index.tailwind.tsx +42 -0
  34. package/data/registry/react/components/sidebar/index.tailwind.tsx +635 -0
  35. package/data/registry/react/components/skeleton/index.tailwind.tsx +39 -0
  36. package/data/registry/react/components/slider/index.tailwind.tsx +255 -0
  37. package/data/registry/react/components/spinner/index.tailwind.tsx +63 -0
  38. package/data/registry/react/components/switch/index.tailwind.tsx +62 -0
  39. package/data/registry/react/components/tabs/index.tailwind.tsx +113 -0
  40. package/data/registry/react/components/textarea/index.tailwind.tsx +21 -0
  41. package/data/registry/react/components/toast/index.tailwind.tsx +215 -0
  42. package/data/registry/react/components/toggle/index.tailwind.tsx +111 -0
  43. package/data/registry/react/components/tooltip/index.tailwind.tsx +55 -0
  44. package/data/registry/react/registry.json +696 -98
  45. package/package.json +1 -1
  46. package/src/mcp.mjs +1 -1
@@ -0,0 +1,215 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { createPortal } from "react-dom";
5
+
6
+ type ToastVariant = "default" | "success" | "danger" | "warning";
7
+
8
+ interface ToastItem {
9
+ id: string;
10
+ title?: React.ReactNode;
11
+ description?: React.ReactNode;
12
+ variant: ToastVariant;
13
+ duration: number;
14
+ action?: React.ReactNode;
15
+ }
16
+
17
+ type ToastInput = Omit<ToastItem, "id" | "variant" | "duration"> & {
18
+ variant?: ToastVariant;
19
+ duration?: number;
20
+ };
21
+
22
+ type Listener = () => void;
23
+ let toasts: ToastItem[] = [];
24
+ const listeners = new Set<Listener>();
25
+ const notify = () => listeners.forEach((l) => l());
26
+ let counter = 0;
27
+ const genId = () => `sh-toast-${++counter}`;
28
+
29
+ function addToast(input: ToastInput): string {
30
+ const id = genId();
31
+ toasts = [...toasts, {
32
+ id, variant: input.variant ?? "default", duration: input.duration ?? 4000,
33
+ title: input.title, description: input.description, action: input.action,
34
+ }];
35
+ notify();
36
+ return id;
37
+ }
38
+ function removeToast(id: string) { toasts = toasts.filter((t) => t.id !== id); notify(); }
39
+
40
+ function useToastStore() {
41
+ return React.useSyncExternalStore(
42
+ (cb) => { listeners.add(cb); return () => listeners.delete(cb); },
43
+ () => toasts,
44
+ () => toasts,
45
+ );
46
+ }
47
+
48
+ export function toast(input: ToastInput | string): string {
49
+ if (typeof input === "string") return addToast({ description: input });
50
+ return addToast(input);
51
+ }
52
+ toast.success = (input: ToastInput | string) =>
53
+ toast(typeof input === "string" ? { description: input, variant: "success" } : { ...input, variant: "success" });
54
+ toast.danger = (input: ToastInput | string) =>
55
+ toast(typeof input === "string" ? { description: input, variant: "danger" } : { ...input, variant: "danger" });
56
+ toast.warning = (input: ToastInput | string) =>
57
+ toast(typeof input === "string" ? { description: input, variant: "warning" } : { ...input, variant: "warning" });
58
+ toast.dismiss = removeToast;
59
+
60
+ function CheckIcon() {
61
+ return (
62
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
63
+ <circle cx="8" cy="8" r="7.25" stroke="currentColor" strokeWidth="1.5" />
64
+ <path d="M5 8.5 7 10.5 11 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
65
+ </svg>
66
+ );
67
+ }
68
+ function AlertIcon() {
69
+ return (
70
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
71
+ <circle cx="8" cy="8" r="7.25" stroke="currentColor" strokeWidth="1.5" />
72
+ <path d="M8 5v3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
73
+ <circle cx="8" cy="11" r="0.75" fill="currentColor" />
74
+ </svg>
75
+ );
76
+ }
77
+ function XIcon() {
78
+ return (
79
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
80
+ <circle cx="8" cy="8" r="7.25" stroke="currentColor" strokeWidth="1.5" />
81
+ <path d="M6 6l4 4M10 6l-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
82
+ </svg>
83
+ );
84
+ }
85
+
86
+ const VARIANT_ICON: Record<ToastVariant, React.ReactNode> = {
87
+ default: null, success: <CheckIcon />, danger: <XIcon />, warning: <AlertIcon />,
88
+ };
89
+ const VARIANT_ICON_COLOR: Record<ToastVariant, string> = {
90
+ default: "",
91
+ success: "text-[var(--success,#16a34a)]",
92
+ danger: "text-danger",
93
+ warning: "text-[var(--warning,#d97706)]",
94
+ };
95
+
96
+ function ToastCard({ item, onDismiss }: { item: ToastItem; onDismiss: () => void }) {
97
+ const [exiting, setExiting] = React.useState(false);
98
+
99
+ React.useEffect(() => {
100
+ if (item.duration <= 0) return;
101
+ const timer = setTimeout(() => setExiting(true), item.duration);
102
+ return () => clearTimeout(timer);
103
+ }, [item.duration]);
104
+
105
+ const handleAnimationEnd = () => { if (exiting) onDismiss(); };
106
+ const icon = VARIANT_ICON[item.variant];
107
+
108
+ return (
109
+ <div
110
+ className="sh-ui-toast relative flex items-start gap-2.5 w-full pl-[var(--space-3)] pr-9 py-[var(--space-3)] bg-background text-foreground border border-border rounded-[var(--radius)] shadow-[0_4px_16px_rgba(0,0,0,0.12)] pointer-events-auto motion-reduce:!animate-none"
111
+ role={item.variant === "danger" ? "alert" : "status"}
112
+ aria-live={item.variant === "danger" ? "assertive" : "polite"}
113
+ data-exiting={exiting || undefined}
114
+ onAnimationEnd={handleAnimationEnd}
115
+ >
116
+ {icon && (
117
+ <span className={`shrink-0 inline-flex items-center mt-0.5 ${VARIANT_ICON_COLOR[item.variant]}`}>
118
+ {icon}
119
+ </span>
120
+ )}
121
+ <div className="flex-1 min-w-0">
122
+ {item.title && (
123
+ <p className="m-0 text-[length:var(--text-sm)] font-semibold leading-snug">{item.title}</p>
124
+ )}
125
+ {item.description && (
126
+ <p className="m-0 text-[0.8125rem] leading-snug text-foreground-muted [&:not(:first-child)]:mt-0.5">
127
+ {item.description}
128
+ </p>
129
+ )}
130
+ </div>
131
+ {item.action && (
132
+ <div className="shrink-0 inline-flex items-center ml-auto">{item.action}</div>
133
+ )}
134
+ <button
135
+ type="button"
136
+ className="absolute top-1.5 right-1.5 inline-flex items-center justify-center w-6 h-6 p-0 border-none rounded-[calc(var(--radius)-2px)] bg-transparent text-foreground-muted text-[length:var(--text-sm)] leading-none cursor-pointer transition-[background-color,color] duration-[var(--duration-fast)] hover:bg-background-muted hover:text-foreground focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 motion-reduce:transition-none"
137
+ onClick={() => setExiting(true)}
138
+ aria-label="닫기"
139
+ >
140
+ ×
141
+ </button>
142
+ </div>
143
+ );
144
+ }
145
+
146
+ export type ToastPosition =
147
+ | "top-left" | "top-right" | "top-center"
148
+ | "bottom-left" | "bottom-right" | "bottom-center";
149
+
150
+ export interface ToasterProps {
151
+ position?: ToastPosition;
152
+ container?: Element | null;
153
+ }
154
+
155
+ const POSITION_CLASSES: Record<ToastPosition, string> = {
156
+ "bottom-right": "bottom-[var(--space-4)] right-[var(--space-4)] flex-col-reverse max-sm:right-0 max-sm:left-0 max-sm:bottom-0",
157
+ "bottom-left": "bottom-[var(--space-4)] left-[var(--space-4)] flex-col-reverse max-sm:right-0 max-sm:left-0 max-sm:bottom-0",
158
+ "bottom-center": "bottom-[var(--space-4)] left-1/2 -translate-x-1/2 flex-col-reverse max-sm:right-0 max-sm:left-0 max-sm:bottom-0 max-sm:translate-x-0",
159
+ "top-right": "top-[var(--space-4)] right-[var(--space-4)] max-sm:right-0 max-sm:left-0 max-sm:top-0",
160
+ "top-left": "top-[var(--space-4)] left-[var(--space-4)] max-sm:right-0 max-sm:left-0 max-sm:top-0",
161
+ "top-center": "top-[var(--space-4)] left-1/2 -translate-x-1/2 max-sm:right-0 max-sm:left-0 max-sm:top-0 max-sm:translate-x-0",
162
+ };
163
+
164
+ export function Toaster({ position = "bottom-right", container }: ToasterProps) {
165
+ const items = useToastStore();
166
+ const [mounted, setMounted] = React.useState(false);
167
+
168
+ React.useEffect(() => setMounted(true), []);
169
+ if (!mounted || items.length === 0) return null;
170
+ const isBottom = position.startsWith("bottom");
171
+
172
+ const el = (
173
+ <div
174
+ className={`fixed z-[var(--z-toast)] flex gap-[var(--space-2)] max-w-96 w-full pointer-events-none max-sm:max-w-full max-sm:p-[var(--space-4)] flex-col ${POSITION_CLASSES[position]}`}
175
+ data-position={position}
176
+ aria-label="알림"
177
+ >
178
+ {(isBottom ? items : [...items].reverse()).map((item) => (
179
+ <ToastCard key={item.id} item={item} onDismiss={() => removeToast(item.id)} />
180
+ ))}
181
+ </div>
182
+ );
183
+
184
+ return createPortal(el, container ?? document.body);
185
+ }
186
+
187
+ if (typeof document !== "undefined" && !document.querySelector("style[data-sh-ui-toast]")) {
188
+ const style = document.createElement("style");
189
+ style.setAttribute("data-sh-ui-toast", "");
190
+ style.textContent = `
191
+ [data-position="bottom-right"] .sh-ui-toast, [data-position="top-right"] .sh-ui-toast { animation: sh-ui-toast-enter-right var(--duration-slow) cubic-bezier(0.16,1,0.3,1) forwards; }
192
+ [data-position="bottom-right"] .sh-ui-toast[data-exiting], [data-position="top-right"] .sh-ui-toast[data-exiting] { animation: sh-ui-toast-exit-right 150ms ease-in forwards; }
193
+ [data-position="bottom-left"] .sh-ui-toast, [data-position="top-left"] .sh-ui-toast { animation: sh-ui-toast-enter-left var(--duration-slow) cubic-bezier(0.16,1,0.3,1) forwards; }
194
+ [data-position="bottom-left"] .sh-ui-toast[data-exiting], [data-position="top-left"] .sh-ui-toast[data-exiting] { animation: sh-ui-toast-exit-left 150ms ease-in forwards; }
195
+ [data-position="bottom-center"] .sh-ui-toast { animation: sh-ui-toast-enter-bottom var(--duration-slow) cubic-bezier(0.16,1,0.3,1) forwards; }
196
+ [data-position="bottom-center"] .sh-ui-toast[data-exiting] { animation: sh-ui-toast-exit-bottom 150ms ease-in forwards; }
197
+ [data-position="top-center"] .sh-ui-toast { animation: sh-ui-toast-enter-top var(--duration-slow) cubic-bezier(0.16,1,0.3,1) forwards; }
198
+ [data-position="top-center"] .sh-ui-toast[data-exiting] { animation: sh-ui-toast-exit-top 150ms ease-in forwards; }
199
+ @keyframes sh-ui-toast-enter-right { from { opacity:0; transform: translateX(100%) } to { opacity:1; transform: translateX(0) } }
200
+ @keyframes sh-ui-toast-exit-right { from { opacity:1; transform: translateX(0) } to { opacity:0; transform: translateX(100%) } }
201
+ @keyframes sh-ui-toast-enter-left { from { opacity:0; transform: translateX(-100%) } to { opacity:1; transform: translateX(0) } }
202
+ @keyframes sh-ui-toast-exit-left { from { opacity:1; transform: translateX(0) } to { opacity:0; transform: translateX(-100%) } }
203
+ @keyframes sh-ui-toast-enter-bottom { from { opacity:0; transform: translateY(100%) } to { opacity:1; transform: translateY(0) } }
204
+ @keyframes sh-ui-toast-exit-bottom { from { opacity:1; transform: translateY(0) } to { opacity:0; transform: translateY(100%) } }
205
+ @keyframes sh-ui-toast-enter-top { from { opacity:0; transform: translateY(-100%) } to { opacity:1; transform: translateY(0) } }
206
+ @keyframes sh-ui-toast-exit-top { from { opacity:1; transform: translateY(0) } to { opacity:0; transform: translateY(-100%) } }
207
+ @media (max-width: 40rem) {
208
+ [data-position] .sh-ui-toast { animation-name: sh-ui-toast-enter-bottom !important }
209
+ [data-position^="top-"] .sh-ui-toast { animation-name: sh-ui-toast-enter-top !important }
210
+ [data-position] .sh-ui-toast[data-exiting] { animation-name: sh-ui-toast-exit-bottom !important }
211
+ [data-position^="top-"] .sh-ui-toast[data-exiting] { animation-name: sh-ui-toast-exit-top !important }
212
+ }
213
+ `;
214
+ document.head.appendChild(style);
215
+ }
@@ -0,0 +1,111 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Toggle as BaseToggle } from "@base-ui/react/toggle";
5
+ import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group";
6
+ import { cva, type VariantProps } from "class-variance-authority";
7
+
8
+ function cx(...args: (string | undefined | false)[]) {
9
+ return args.filter(Boolean).join(" ");
10
+ }
11
+
12
+ const toggleVariants = cva(
13
+ "inline-flex items-center justify-center gap-1.5 border border-transparent rounded-[var(--radius)] font-medium leading-none cursor-pointer text-foreground-muted bg-transparent transition-[background-color,color,border-color] duration-[var(--duration-fast)] select-none focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 disabled:opacity-[var(--opacity-disabled)] disabled:pointer-events-none data-[pressed]:bg-background-muted data-[pressed]:text-foreground motion-reduce:transition-none",
14
+ {
15
+ variants: {
16
+ variant: {
17
+ outline:
18
+ "border-border hover:not-disabled:not-data-[pressed]:bg-background-muted hover:not-disabled:not-data-[pressed]:text-foreground data-[pressed]:border-border-strong",
19
+ ghost:
20
+ "hover:not-disabled:not-data-[pressed]:bg-background-muted hover:not-disabled:not-data-[pressed]:text-foreground",
21
+ },
22
+ size: {
23
+ sm: "h-[var(--control-sm)] px-2.5 text-[length:var(--text-sm)] [@media(hover:none)_and_(pointer:coarse)]:h-9",
24
+ md: "h-[var(--control-md)] px-[var(--space-3)] text-[length:var(--text-sm)] [@media(hover:none)_and_(pointer:coarse)]:h-11",
25
+ lg: "h-[var(--control-lg)] px-[var(--space-4)] text-[length:var(--text-base)]",
26
+ },
27
+ },
28
+ defaultVariants: { variant: "ghost", size: "md" },
29
+ },
30
+ );
31
+
32
+ export type ToggleVariant = NonNullable<VariantProps<typeof toggleVariants>["variant"]>;
33
+ export type ToggleSize = NonNullable<VariantProps<typeof toggleVariants>["size"]>;
34
+
35
+ export type ToggleProps = Omit<
36
+ React.ComponentPropsWithoutRef<typeof BaseToggle>,
37
+ "className"
38
+ > & {
39
+ className?: string;
40
+ variant?: ToggleVariant;
41
+ size?: ToggleSize;
42
+ };
43
+
44
+ export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
45
+ ({ className, variant = "ghost", size = "md", ...props }, ref) => (
46
+ <BaseToggle
47
+ ref={ref}
48
+ className={cx(toggleVariants({ variant, size }), className)}
49
+ {...props}
50
+ />
51
+ ),
52
+ );
53
+ Toggle.displayName = "Toggle";
54
+
55
+ export type ToggleGroupProps = Omit<
56
+ React.ComponentPropsWithoutRef<typeof BaseToggleGroup>,
57
+ "className"
58
+ > & {
59
+ className?: string;
60
+ variant?: ToggleVariant;
61
+ size?: ToggleSize;
62
+ };
63
+
64
+ interface ToggleGroupContextValue {
65
+ variant: ToggleVariant;
66
+ size: ToggleSize;
67
+ }
68
+
69
+ const ToggleGroupContext = React.createContext<ToggleGroupContextValue>({
70
+ variant: "ghost",
71
+ size: "md",
72
+ });
73
+
74
+ export const useToggleGroupStyle = () => React.useContext(ToggleGroupContext);
75
+
76
+ export const ToggleGroup = React.forwardRef<HTMLDivElement, ToggleGroupProps>(
77
+ ({ className, variant = "ghost", size = "md", ...props }, ref) => (
78
+ <ToggleGroupContext.Provider value={{ variant, size }}>
79
+ <BaseToggleGroup
80
+ ref={ref}
81
+ className={cx(
82
+ "inline-flex items-center gap-[var(--space-1)] data-[orientation=vertical]:flex-col",
83
+ className,
84
+ )}
85
+ {...props}
86
+ />
87
+ </ToggleGroupContext.Provider>
88
+ ),
89
+ );
90
+ ToggleGroup.displayName = "ToggleGroup";
91
+
92
+ export type ToggleGroupItemProps = Omit<
93
+ React.ComponentPropsWithoutRef<typeof BaseToggle>,
94
+ "className"
95
+ > & {
96
+ className?: string;
97
+ };
98
+
99
+ export const ToggleGroupItem = React.forwardRef<HTMLButtonElement, ToggleGroupItemProps>(
100
+ ({ className, ...props }, ref) => {
101
+ const { variant, size } = useToggleGroupStyle();
102
+ return (
103
+ <BaseToggle
104
+ ref={ref}
105
+ className={cx(toggleVariants({ variant, size }), className)}
106
+ {...props}
107
+ />
108
+ );
109
+ },
110
+ );
111
+ ToggleGroupItem.displayName = "ToggleGroupItem";
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
3
+
4
+ type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
5
+
6
+ function cx(...args: (string | undefined | false | null)[]) {
7
+ return args.filter(Boolean).join(" ");
8
+ }
9
+
10
+ export const TooltipProvider = BaseTooltip.Provider;
11
+ export const Tooltip = BaseTooltip.Root;
12
+ export const TooltipTrigger = BaseTooltip.Trigger;
13
+
14
+ export interface TooltipContentProps
15
+ extends WithStringClassName<
16
+ React.ComponentPropsWithoutRef<typeof BaseTooltip.Popup>
17
+ > {
18
+ side?: "top" | "right" | "bottom" | "left";
19
+ align?: "start" | "center" | "end";
20
+ sideOffset?: number;
21
+ showArrow?: boolean;
22
+ container?: React.ComponentPropsWithoutRef<
23
+ typeof BaseTooltip.Portal
24
+ >["container"];
25
+ }
26
+
27
+ export const TooltipContent = React.forwardRef<HTMLDivElement, TooltipContentProps>(
28
+ function TooltipContent(
29
+ { className, children, side, align, sideOffset = 6, showArrow, container, ...props },
30
+ ref,
31
+ ) {
32
+ return (
33
+ <BaseTooltip.Portal container={container}>
34
+ <BaseTooltip.Positioner
35
+ className="z-[var(--z-tooltip,var(--z-popover))] outline-none"
36
+ side={side}
37
+ align={align}
38
+ sideOffset={sideOffset}
39
+ >
40
+ <BaseTooltip.Popup
41
+ ref={ref}
42
+ className={cx(
43
+ "px-2.5 py-1.5 bg-foreground text-background rounded-[calc(var(--radius)-2px)] text-[length:var(--text-xs)] leading-snug max-w-xs shadow-[0_4px_12px_rgba(0,0,0,0.12)] origin-[var(--transform-origin)] outline-none transition-[opacity,transform] duration-[120ms] ease-out motion-reduce:transition-none data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 motion-reduce:data-[starting-style]:scale-100 motion-reduce:data-[ending-style]:scale-100",
44
+ className,
45
+ )}
46
+ {...props}
47
+ >
48
+ {showArrow && <BaseTooltip.Arrow className="text-foreground [&_svg]:block" />}
49
+ {children}
50
+ </BaseTooltip.Popup>
51
+ </BaseTooltip.Positioner>
52
+ </BaseTooltip.Portal>
53
+ );
54
+ },
55
+ );