@camox/ui 0.1.1-alpha.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.
- package/LICENSE.md +110 -0
- package/package.json +185 -0
- package/src/components/accordion.tsx +58 -0
- package/src/components/alert-dialog.tsx +133 -0
- package/src/components/alert.tsx +60 -0
- package/src/components/avatar.tsx +94 -0
- package/src/components/badge.tsx +39 -0
- package/src/components/breadcrumb.tsx +102 -0
- package/src/components/button-group.tsx +78 -0
- package/src/components/button.tsx +58 -0
- package/src/components/checkbox.tsx +27 -0
- package/src/components/command.tsx +168 -0
- package/src/components/control-group.tsx +58 -0
- package/src/components/dialog.tsx +127 -0
- package/src/components/dropdown-menu.tsx +226 -0
- package/src/components/floating-toolbar.tsx +17 -0
- package/src/components/frame.tsx +146 -0
- package/src/components/input-base.tsx +189 -0
- package/src/components/input.tsx +21 -0
- package/src/components/kbd.tsx +28 -0
- package/src/components/label.tsx +21 -0
- package/src/components/panel.tsx +78 -0
- package/src/components/popover.tsx +40 -0
- package/src/components/resizable.tsx +46 -0
- package/src/components/select.tsx +169 -0
- package/src/components/separator.tsx +26 -0
- package/src/components/sheet.tsx +130 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/spinner.tsx +16 -0
- package/src/components/switch.tsx +26 -0
- package/src/components/tabs.tsx +52 -0
- package/src/components/textarea.tsx +20 -0
- package/src/components/toaster.tsx +22 -0
- package/src/components/toggle.tsx +45 -0
- package/src/components/tooltip.tsx +55 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/lib/utils.ts +15 -0
- package/src/styles/globals.css +120 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
2
|
+
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
|
8
|
+
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function DropdownMenuPortal({
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
|
14
|
+
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function DropdownMenuTrigger({
|
|
18
|
+
...props
|
|
19
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
|
20
|
+
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function DropdownMenuContent({
|
|
24
|
+
className,
|
|
25
|
+
sideOffset = 4,
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
|
28
|
+
return (
|
|
29
|
+
<DropdownMenuPrimitive.Portal>
|
|
30
|
+
<DropdownMenuPrimitive.Content
|
|
31
|
+
data-slot="dropdown-menu-content"
|
|
32
|
+
sideOffset={sideOffset}
|
|
33
|
+
className={cn(
|
|
34
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
</DropdownMenuPrimitive.Portal>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
|
44
|
+
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function DropdownMenuItem({
|
|
48
|
+
className,
|
|
49
|
+
inset,
|
|
50
|
+
variant = "default",
|
|
51
|
+
...props
|
|
52
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
|
53
|
+
inset?: boolean;
|
|
54
|
+
variant?: "default" | "destructive";
|
|
55
|
+
}) {
|
|
56
|
+
return (
|
|
57
|
+
<DropdownMenuPrimitive.Item
|
|
58
|
+
data-slot="dropdown-menu-item"
|
|
59
|
+
data-inset={inset}
|
|
60
|
+
data-variant={variant}
|
|
61
|
+
className={cn(
|
|
62
|
+
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
63
|
+
className,
|
|
64
|
+
)}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function DropdownMenuCheckboxItem({
|
|
71
|
+
className,
|
|
72
|
+
children,
|
|
73
|
+
checked,
|
|
74
|
+
...props
|
|
75
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
|
76
|
+
return (
|
|
77
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
78
|
+
data-slot="dropdown-menu-checkbox-item"
|
|
79
|
+
className={cn(
|
|
80
|
+
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
checked={checked}
|
|
84
|
+
{...props}
|
|
85
|
+
>
|
|
86
|
+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
87
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
88
|
+
<CheckIcon className="size-4" />
|
|
89
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
90
|
+
</span>
|
|
91
|
+
{children}
|
|
92
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function DropdownMenuRadioGroup({
|
|
97
|
+
...props
|
|
98
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
|
99
|
+
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function DropdownMenuRadioItem({
|
|
103
|
+
className,
|
|
104
|
+
children,
|
|
105
|
+
...props
|
|
106
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
|
107
|
+
return (
|
|
108
|
+
<DropdownMenuPrimitive.RadioItem
|
|
109
|
+
data-slot="dropdown-menu-radio-item"
|
|
110
|
+
className={cn(
|
|
111
|
+
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
112
|
+
className,
|
|
113
|
+
)}
|
|
114
|
+
{...props}
|
|
115
|
+
>
|
|
116
|
+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
117
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
118
|
+
<CircleIcon className="size-2 fill-current" />
|
|
119
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
120
|
+
</span>
|
|
121
|
+
{children}
|
|
122
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function DropdownMenuLabel({
|
|
127
|
+
className,
|
|
128
|
+
inset,
|
|
129
|
+
...props
|
|
130
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
|
131
|
+
inset?: boolean;
|
|
132
|
+
}) {
|
|
133
|
+
return (
|
|
134
|
+
<DropdownMenuPrimitive.Label
|
|
135
|
+
data-slot="dropdown-menu-label"
|
|
136
|
+
data-inset={inset}
|
|
137
|
+
className={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
|
|
138
|
+
{...props}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function DropdownMenuSeparator({
|
|
144
|
+
className,
|
|
145
|
+
...props
|
|
146
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
|
147
|
+
return (
|
|
148
|
+
<DropdownMenuPrimitive.Separator
|
|
149
|
+
data-slot="dropdown-menu-separator"
|
|
150
|
+
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
151
|
+
{...props}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
|
157
|
+
return (
|
|
158
|
+
<span
|
|
159
|
+
data-slot="dropdown-menu-shortcut"
|
|
160
|
+
className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
|
161
|
+
{...props}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
|
167
|
+
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function DropdownMenuSubTrigger({
|
|
171
|
+
className,
|
|
172
|
+
inset,
|
|
173
|
+
children,
|
|
174
|
+
...props
|
|
175
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
176
|
+
inset?: boolean;
|
|
177
|
+
}) {
|
|
178
|
+
return (
|
|
179
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
180
|
+
data-slot="dropdown-menu-sub-trigger"
|
|
181
|
+
data-inset={inset}
|
|
182
|
+
className={cn(
|
|
183
|
+
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
|
184
|
+
className,
|
|
185
|
+
)}
|
|
186
|
+
{...props}
|
|
187
|
+
>
|
|
188
|
+
{children}
|
|
189
|
+
<ChevronRightIcon className="text-muted-foreground ml-auto size-4" />
|
|
190
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function DropdownMenuSubContent({
|
|
195
|
+
className,
|
|
196
|
+
...props
|
|
197
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
|
198
|
+
return (
|
|
199
|
+
<DropdownMenuPrimitive.SubContent
|
|
200
|
+
data-slot="dropdown-menu-sub-content"
|
|
201
|
+
className={cn(
|
|
202
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
|
203
|
+
className,
|
|
204
|
+
)}
|
|
205
|
+
{...props}
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
DropdownMenu,
|
|
212
|
+
DropdownMenuPortal,
|
|
213
|
+
DropdownMenuTrigger,
|
|
214
|
+
DropdownMenuContent,
|
|
215
|
+
DropdownMenuGroup,
|
|
216
|
+
DropdownMenuLabel,
|
|
217
|
+
DropdownMenuItem,
|
|
218
|
+
DropdownMenuCheckboxItem,
|
|
219
|
+
DropdownMenuRadioGroup,
|
|
220
|
+
DropdownMenuRadioItem,
|
|
221
|
+
DropdownMenuSeparator,
|
|
222
|
+
DropdownMenuShortcut,
|
|
223
|
+
DropdownMenuSub,
|
|
224
|
+
DropdownMenuSubTrigger,
|
|
225
|
+
DropdownMenuSubContent,
|
|
226
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
|
|
3
|
+
function FloatingToolbar({ className, ...props }: React.ComponentProps<"menu">) {
|
|
4
|
+
return (
|
|
5
|
+
<menu
|
|
6
|
+
role="toolbar"
|
|
7
|
+
data-slot="floating-toolbar"
|
|
8
|
+
className={cn(
|
|
9
|
+
"absolute left-[50%] z-30 flex translate-x-[-50%] items-center rounded-xl border-1 bg-background/95 p-2 shadow-2xl backdrop-blur-lg transition-all duration-200",
|
|
10
|
+
className,
|
|
11
|
+
)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { FloatingToolbar };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
interface FrameContextValue {
|
|
7
|
+
window: Window | null;
|
|
8
|
+
iframeElement: HTMLIFrameElement | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const FrameContext = React.createContext<FrameContextValue>({
|
|
12
|
+
window: null,
|
|
13
|
+
iframeElement: null,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function useFrame() {
|
|
17
|
+
const context = React.use(FrameContext);
|
|
18
|
+
if (!context) {
|
|
19
|
+
throw new Error("useFrame must be used within a Frame");
|
|
20
|
+
}
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FrameProps {
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
/** Optional className for the iframe element */
|
|
27
|
+
className?: string;
|
|
28
|
+
/** Optional inline styles for the iframe element */
|
|
29
|
+
style?: React.CSSProperties;
|
|
30
|
+
/** Whether to copy parent document styles into the iframe (default: true) */
|
|
31
|
+
copyStyles?: boolean;
|
|
32
|
+
/** Callback when iframe is ready, receives the iframe element */
|
|
33
|
+
onIframeReady?: (iframe: HTMLIFrameElement) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const Frame = ({
|
|
37
|
+
children,
|
|
38
|
+
className,
|
|
39
|
+
style,
|
|
40
|
+
copyStyles = true,
|
|
41
|
+
onIframeReady,
|
|
42
|
+
}: FrameProps) => {
|
|
43
|
+
const iframeRef = React.useRef<HTMLIFrameElement>(null);
|
|
44
|
+
const [iframeWindow, setIframeWindow] = React.useState<Window | null>(null);
|
|
45
|
+
const [iframeElement, setIframeElement] = React.useState<HTMLIFrameElement | null>(null);
|
|
46
|
+
const [mountNode, setMountNode] = React.useState<HTMLElement | null>(null);
|
|
47
|
+
const [hasRadixPopper, setHasRadixPopper] = React.useState(false);
|
|
48
|
+
|
|
49
|
+
React.useEffect(() => {
|
|
50
|
+
const iframe = iframeRef.current;
|
|
51
|
+
if (!iframe) return;
|
|
52
|
+
|
|
53
|
+
const handleLoad = () => {
|
|
54
|
+
const iframeDoc = iframe.contentDocument;
|
|
55
|
+
const iframeWin = iframe.contentWindow;
|
|
56
|
+
|
|
57
|
+
if (!iframeDoc || !iframeWin) return;
|
|
58
|
+
|
|
59
|
+
// Set up basic document structure
|
|
60
|
+
iframeDoc.open();
|
|
61
|
+
iframeDoc.write(
|
|
62
|
+
"<!DOCTYPE html><html><head></head><body style='background: transparent;'></body></html>",
|
|
63
|
+
);
|
|
64
|
+
iframeDoc.close();
|
|
65
|
+
|
|
66
|
+
// Navigate the top-level window when a native <a> is clicked inside the
|
|
67
|
+
// iframe. Links managed by a client-side router (e.g. TanStack Router's
|
|
68
|
+
// <Link>) call e.preventDefault() themselves, so we skip those.
|
|
69
|
+
// We listen on `iframeWin` (not `iframeDoc`) so that this handler fires
|
|
70
|
+
// AFTER React's event delegation (which is on the document/body), giving
|
|
71
|
+
// React a chance to call preventDefault() first.
|
|
72
|
+
iframeWin.addEventListener("click", (e) => {
|
|
73
|
+
if (e.defaultPrevented) return;
|
|
74
|
+
const anchor = (e.target as Element).closest("a");
|
|
75
|
+
if (!anchor?.href) return;
|
|
76
|
+
if (anchor.target === "_blank") return;
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
window.top?.location.assign(anchor.href);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Copy styles from parent document if requested
|
|
82
|
+
if (copyStyles) {
|
|
83
|
+
const headStyles = Array.from(
|
|
84
|
+
document.head.querySelectorAll('style, link[rel="stylesheet"]'),
|
|
85
|
+
);
|
|
86
|
+
headStyles.forEach((style) => {
|
|
87
|
+
const clonedStyle = style.cloneNode(true);
|
|
88
|
+
iframeDoc.head.appendChild(clonedStyle);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Set the mount node to the iframe's body
|
|
93
|
+
setMountNode(iframeDoc.body);
|
|
94
|
+
setIframeWindow(iframeWin);
|
|
95
|
+
setIframeElement(iframe);
|
|
96
|
+
onIframeReady?.(iframe);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Add load event listener
|
|
100
|
+
iframe.addEventListener("load", handleLoad);
|
|
101
|
+
|
|
102
|
+
// Trigger load if iframe is already loaded
|
|
103
|
+
if (iframe.contentDocument?.readyState === "complete") {
|
|
104
|
+
handleLoad();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return () => {
|
|
108
|
+
iframe.removeEventListener("load", handleLoad);
|
|
109
|
+
};
|
|
110
|
+
}, [copyStyles, onIframeReady]);
|
|
111
|
+
|
|
112
|
+
// Monitor for Radix popper content wrapper in body
|
|
113
|
+
React.useEffect(() => {
|
|
114
|
+
const checkForRadixPopper = () => {
|
|
115
|
+
const hasPopper =
|
|
116
|
+
document.body.querySelector(":scope > [data-radix-popper-content-wrapper]") !== null;
|
|
117
|
+
setHasRadixPopper(hasPopper);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Initial check
|
|
121
|
+
checkForRadixPopper();
|
|
122
|
+
|
|
123
|
+
// Set up MutationObserver to watch for changes
|
|
124
|
+
const observer = new MutationObserver(checkForRadixPopper);
|
|
125
|
+
observer.observe(document.body, {
|
|
126
|
+
childList: true,
|
|
127
|
+
subtree: false, // Only watch direct children of body
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return () => {
|
|
131
|
+
observer.disconnect();
|
|
132
|
+
};
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className={cn("relative w-full h-full", className)} style={style}>
|
|
137
|
+
{/* Display an overlay to properly close close Radix portals (modals, popovers...) */}
|
|
138
|
+
{/* because otherwise Radix wouldn't detect pointer events that would happen on the iframe */}
|
|
139
|
+
{hasRadixPopper && <div className="absolute top-0 left-0 h-full w-full" />}
|
|
140
|
+
<FrameContext.Provider value={{ window: iframeWindow, iframeElement }}>
|
|
141
|
+
<iframe ref={iframeRef} className={cn("w-full h-full")} />
|
|
142
|
+
{mountNode && createPortal(children, mountNode)}
|
|
143
|
+
</FrameContext.Provider>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { composeEventHandlers } from "@radix-ui/primitive";
|
|
4
|
+
import { useComposedRefs } from "@radix-ui/react-compose-refs";
|
|
5
|
+
import { Primitive } from "@radix-ui/react-primitive";
|
|
6
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
|
|
9
|
+
import { cn } from "../lib/utils";
|
|
10
|
+
import { Button } from "./button";
|
|
11
|
+
|
|
12
|
+
export type InputBaseContextProps = Pick<InputBaseProps, "autoFocus" | "disabled"> & {
|
|
13
|
+
controlRef: React.RefObject<HTMLElement | null>;
|
|
14
|
+
onFocusedChange: (focused: boolean) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const InputBaseContext = React.createContext<InputBaseContextProps>({
|
|
18
|
+
autoFocus: false,
|
|
19
|
+
controlRef: { current: null },
|
|
20
|
+
disabled: false,
|
|
21
|
+
onFocusedChange: () => {},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function useInputBase() {
|
|
25
|
+
const context = React.useContext(InputBaseContext);
|
|
26
|
+
if (!context) {
|
|
27
|
+
throw new Error("useInputBase must be used within a <InputBase />.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return context;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface InputBaseProps extends React.ComponentProps<typeof Primitive.div> {
|
|
34
|
+
autoFocus?: boolean;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
error?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function InputBase({ autoFocus, disabled, className, onClick, error, ...props }: InputBaseProps) {
|
|
40
|
+
const [focused, setFocused] = React.useState(false);
|
|
41
|
+
const controlRef = React.useRef<HTMLElement>(null);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<InputBaseContext.Provider
|
|
45
|
+
value={{
|
|
46
|
+
autoFocus,
|
|
47
|
+
controlRef,
|
|
48
|
+
disabled,
|
|
49
|
+
onFocusedChange: setFocused,
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<Primitive.div
|
|
53
|
+
data-slot="input-base"
|
|
54
|
+
// Based on MUI's <InputBase /> implementation.
|
|
55
|
+
// https://github.com/mui/material-ui/blob/master/packages/mui-material/src/InputBase/InputBase.js#L458~L460
|
|
56
|
+
onClick={composeEventHandlers(onClick, (event) => {
|
|
57
|
+
if (controlRef.current && event.currentTarget === event.target) {
|
|
58
|
+
controlRef.current.focus();
|
|
59
|
+
}
|
|
60
|
+
})}
|
|
61
|
+
className={cn(
|
|
62
|
+
"border-input selection:bg-primary selection:text-primary-foreground dark:bg-input/30 flex min-h-9 cursor-text items-center gap-2 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none md:text-sm",
|
|
63
|
+
disabled && "pointer-events-none cursor-not-allowed opacity-50",
|
|
64
|
+
focused && "border-ring ring-ring/50 ring-[3px]",
|
|
65
|
+
error && "ring-destructive/20 dark:ring-destructive/40 border-destructive",
|
|
66
|
+
className,
|
|
67
|
+
)}
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
</InputBaseContext.Provider>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function InputBaseFlexWrapper({ className, ...props }: React.ComponentProps<typeof Primitive.div>) {
|
|
75
|
+
return (
|
|
76
|
+
<Primitive.div
|
|
77
|
+
data-slot="input-base-flex-wrapper"
|
|
78
|
+
className={cn("flex flex-1 flex-wrap", className)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function InputBaseControl({ ref, onFocus, onBlur, ...props }: React.ComponentProps<typeof Slot>) {
|
|
85
|
+
const { controlRef, autoFocus, disabled, onFocusedChange } = useInputBase();
|
|
86
|
+
|
|
87
|
+
const composedRefs = useComposedRefs(controlRef, ref);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Slot
|
|
91
|
+
data-slot="input-base-control"
|
|
92
|
+
ref={composedRefs}
|
|
93
|
+
autoFocus={autoFocus}
|
|
94
|
+
onFocus={composeEventHandlers(onFocus, () => onFocusedChange(true))}
|
|
95
|
+
onBlur={composeEventHandlers(onBlur, () => onFocusedChange(false))}
|
|
96
|
+
{...{ disabled }}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface InputBaseAdornmentProps extends React.ComponentProps<"div"> {
|
|
103
|
+
asChild?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function InputBaseAdornment({ className, asChild, children, ...props }: InputBaseAdornmentProps) {
|
|
107
|
+
let Comp: typeof Slot | "p" | "div";
|
|
108
|
+
if (asChild) {
|
|
109
|
+
Comp = Slot;
|
|
110
|
+
} else if (typeof children === "string") {
|
|
111
|
+
Comp = "p";
|
|
112
|
+
} else {
|
|
113
|
+
Comp = "div";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Comp
|
|
118
|
+
data-slot="input-base-adornment"
|
|
119
|
+
className={cn(
|
|
120
|
+
"text-muted-foreground flex items-center [&_svg:not([class*='size-'])]:size-4",
|
|
121
|
+
"[&:not(:has(button))]:pointer-events-none",
|
|
122
|
+
className,
|
|
123
|
+
)}
|
|
124
|
+
{...props}
|
|
125
|
+
>
|
|
126
|
+
{children}
|
|
127
|
+
</Comp>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function InputBaseAdornmentButton({
|
|
132
|
+
type = "button",
|
|
133
|
+
variant = "ghost",
|
|
134
|
+
size = "icon",
|
|
135
|
+
disabled: disabledProp,
|
|
136
|
+
className,
|
|
137
|
+
...props
|
|
138
|
+
}: React.ComponentProps<typeof Button>) {
|
|
139
|
+
const { disabled } = useInputBase();
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Button
|
|
143
|
+
data-slot="input-base-adornment-button"
|
|
144
|
+
type={type}
|
|
145
|
+
variant={variant}
|
|
146
|
+
size={size}
|
|
147
|
+
disabled={disabled || disabledProp}
|
|
148
|
+
className={cn("size-6", className)}
|
|
149
|
+
{...props}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function InputBaseInput({ className, ...props }: React.ComponentProps<typeof Primitive.input>) {
|
|
155
|
+
return (
|
|
156
|
+
<Primitive.input
|
|
157
|
+
data-slot="input-base-input"
|
|
158
|
+
className={cn(
|
|
159
|
+
"placeholder:text-muted-foreground file:text-foreground w-full flex-1 bg-transparent file:border-0 file:bg-transparent file:text-sm file:font-medium focus:outline-none disabled:pointer-events-none",
|
|
160
|
+
className,
|
|
161
|
+
)}
|
|
162
|
+
{...props}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function InputBaseTextarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
168
|
+
return (
|
|
169
|
+
<textarea
|
|
170
|
+
data-slot="input-base-textarea"
|
|
171
|
+
className={cn(
|
|
172
|
+
"placeholder:text-muted-foreground min-h-16 flex-1 bg-transparent focus:outline-none disabled:pointer-events-none",
|
|
173
|
+
className,
|
|
174
|
+
)}
|
|
175
|
+
{...props}
|
|
176
|
+
/>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export {
|
|
181
|
+
InputBase,
|
|
182
|
+
InputBaseFlexWrapper,
|
|
183
|
+
InputBaseControl,
|
|
184
|
+
InputBaseAdornment,
|
|
185
|
+
InputBaseAdornmentButton,
|
|
186
|
+
InputBaseInput,
|
|
187
|
+
InputBaseTextarea,
|
|
188
|
+
useInputBase,
|
|
189
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { INPUT_BASE_STYLES, INPUT_FOCUS_STYLES, cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
6
|
+
return (
|
|
7
|
+
<input
|
|
8
|
+
type={type}
|
|
9
|
+
data-slot="input"
|
|
10
|
+
className={cn(
|
|
11
|
+
INPUT_BASE_STYLES,
|
|
12
|
+
INPUT_FOCUS_STYLES,
|
|
13
|
+
"file:text-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 h-9 w-full min-w-0 px-3 py-1 file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Input };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
|
|
3
|
+
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
|
4
|
+
return (
|
|
5
|
+
<kbd
|
|
6
|
+
data-slot="kbd"
|
|
7
|
+
className={cn(
|
|
8
|
+
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
|
|
9
|
+
"[&_svg:not([class*='size-'])]:size-3",
|
|
10
|
+
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
|
|
11
|
+
className,
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
19
|
+
return (
|
|
20
|
+
<kbd
|
|
21
|
+
data-slot="kbd-group"
|
|
22
|
+
className={cn("inline-flex items-center gap-1", className)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { Kbd, KbdGroup };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
9
|
+
return (
|
|
10
|
+
<LabelPrimitive.Root
|
|
11
|
+
data-slot="label"
|
|
12
|
+
className={cn(
|
|
13
|
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Label };
|