clean
This commit is contained in:
38
internal/api-harness/src/components/ApiResponse.jsx
Normal file
38
internal/api-harness/src/components/ApiResponse.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
const ApiResponse = ({ response, error, loading }) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-2">Response</h3>
|
||||
<div className="bg-slate-100 border border-slate-200 rounded-lg p-4">
|
||||
<p className="text-slate-500">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<h3 className="text-lg font-semibold text-red-800 mb-2">Error</h3>
|
||||
<pre className="bg-red-50 border border-red-200 text-red-700 rounded-lg p-4 text-sm whitespace-pre-wrap">
|
||||
{JSON.stringify(error, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (response) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-2">Response</h3>
|
||||
<pre className="bg-slate-100 border border-slate-200 rounded-lg p-4 text-sm whitespace-pre-wrap">
|
||||
{JSON.stringify(response, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ApiResponse;
|
||||
125
internal/api-harness/src/components/Layout.jsx
Normal file
125
internal/api-harness/src/components/Layout.jsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Link, Navigate, Outlet, useLocation } from "react-router-dom";
|
||||
import { useAuthState } from "react-firebase-hooks/auth";
|
||||
import { auth } from "../firebase";
|
||||
import { krowSDK } from "@/api/krowSDK";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const navLinks = {
|
||||
auth: [
|
||||
{ path: "/auth/me", title: "Get Me" },
|
||||
{ path: "/auth/update-me", title: "Update Me" },
|
||||
],
|
||||
core: [
|
||||
{ path: "/core/send-email", title: "Send Email" },
|
||||
{ path: "/core/invoke-llm", title: "Invoke LLM" },
|
||||
{ path: "/core/upload-file", title: "Upload File" },
|
||||
{ path: "/core/upload-private-file", title: "Upload Private File" },
|
||||
{ path: "/core/create-signed-url", title: "Create Signed URL" },
|
||||
{ path: "/core/extract-data", title: "Extract Data from File" },
|
||||
{ path: "/core/generate-image", title: "Generate Image" },
|
||||
],
|
||||
entities: [
|
||||
{ path: "/entities", title: "Entity API Tester" }
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
const NavSection = ({ title, links }) => {
|
||||
const location = useLocation();
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const navLinkClasses = (path) =>
|
||||
`flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
location.pathname === path
|
||||
? "bg-slate-200 text-slate-900"
|
||||
: "text-slate-600 hover:bg-slate-100"
|
||||
}`;
|
||||
|
||||
return (
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<CollapsibleTrigger className="w-full">
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<h2 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
{title}
|
||||
</h2>
|
||||
<ChevronDown className={`w-4 h-4 text-slate-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-1">
|
||||
{links.map((api) => (
|
||||
<Link key={api.path} to={api.path} className={navLinkClasses(api.path)}>
|
||||
{api.title}
|
||||
</Link>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const Layout = () => {
|
||||
const [user, loading] = useAuthState(auth);
|
||||
const location = useLocation();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
krowSDK.auth.logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-slate-50">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-72 bg-white border-r border-slate-200 flex flex-col">
|
||||
<div className="p-6 border-b border-slate-200">
|
||||
<div className="flex items-center space-x-3">
|
||||
<img src="/logo.svg" alt="Krow Logo" className="h-12 w-12" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-900">KROW</h1>
|
||||
<p className="text-xs text-slate-500">API Test Harness ({import.meta.env.VITE_HARNESS_ENVIRONMENT})</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 overflow-y-auto p-4 space-y-2">
|
||||
<NavSection title="Auth" links={navLinks.auth} />
|
||||
<NavSection title="Core Integrations" links={navLinks.core} />
|
||||
<NavSection title="Entities" links={navLinks.entities} />
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-slate-200">
|
||||
<div className="text-sm text-slate-600 truncate mb-2" title={user.email}>{user.email}</div>
|
||||
<Button onClick={handleLogout} className="w-full">
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<div className="p-8">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
120
internal/api-harness/src/components/ServiceTester.jsx
Normal file
120
internal/api-harness/src/components/ServiceTester.jsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useState } from "react";
|
||||
import apiClient from "../api/client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const ServiceTester = ({ serviceName, serviceDescription, endpoint, fields }) => {
|
||||
const [formData, setFormData] = useState({});
|
||||
const [response, setResponse] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value, files } = e.target;
|
||||
if (files) {
|
||||
setFormData({ ...formData, [name]: files[0] });
|
||||
} else {
|
||||
setFormData({ ...formData, [name]: value });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResponse(null);
|
||||
|
||||
const data = new FormData();
|
||||
for (const key in formData) {
|
||||
data.append(key, formData[key]);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await apiClient.post(endpoint, data);
|
||||
setResponse(res.data);
|
||||
} catch (err) {
|
||||
setError(err.response?.data || err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-slate-900">{serviceName}</h1>
|
||||
<p className="text-slate-600 mt-1">{serviceDescription}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Parameters</CardTitle>
|
||||
<CardDescription>
|
||||
Fill in the required parameters to execute the service.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{fields.map((field) => (
|
||||
<div key={field.name} className="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Label htmlFor={field.name}>{field.label}</Label>
|
||||
{field.type === "textarea" ? (
|
||||
<Textarea
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
onChange={handleChange}
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type={field.type}
|
||||
onChange={handleChange}
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Executing..." : "Execute"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
|
||||
{response && (
|
||||
<div className="mt-8">
|
||||
<h2 className="text-xl font-bold text-slate-900 mb-2">Response</h2>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<pre className="text-sm overflow-x-auto bg-slate-900 text-white p-4 rounded-md">
|
||||
{JSON.stringify(response, null, 2)}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mt-8">
|
||||
<h2 className="text-xl font-bold text-red-600 mb-2">Error</h2>
|
||||
<Card className="border-red-500">
|
||||
<CardContent className="p-4">
|
||||
<pre className="text-sm overflow-x-auto bg-red-50 text-red-800 p-4 rounded-md">
|
||||
{JSON.stringify(error, null, 2)}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceTester;
|
||||
48
internal/api-harness/src/components/ui/button.jsx
Normal file
48
internal/api-harness/src/components/ui/button.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
50
internal/api-harness/src/components/ui/card.jsx
Normal file
50
internal/api-harness/src/components/ui/card.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
|
||||
{...props} />
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
11
internal/api-harness/src/components/ui/collapsible.jsx
Normal file
11
internal/api-harness/src/components/ui/collapsible.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
19
internal/api-harness/src/components/ui/input.jsx
Normal file
19
internal/api-harness/src/components/ui/input.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
16
internal/api-harness/src/components/ui/label.jsx
Normal file
16
internal/api-harness/src/components/ui/label.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
105
internal/api-harness/src/components/ui/select.jsx
Normal file
105
internal/api-harness/src/components/ui/select.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
}
|
||||
18
internal/api-harness/src/components/ui/textarea.jsx
Normal file
18
internal/api-harness/src/components/ui/textarea.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
Reference in New Issue
Block a user