[crawler_platform 삭제]
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
matchPath,
|
||||
NavLink,
|
||||
Outlet,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
} from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
UploadCloud,
|
||||
Settings2,
|
||||
Activity,
|
||||
Brain,
|
||||
Code2,
|
||||
GitBranch,
|
||||
Network,
|
||||
ListChecks,
|
||||
Menu,
|
||||
SearchCheck,
|
||||
ShieldCheck,
|
||||
ClipboardCheck,
|
||||
Layers3,
|
||||
} from "lucide-react";
|
||||
import { RootState } from "@/stores";
|
||||
import { toggleSidebar } from "@/stores/slices/uiSlice";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProjects } from "@/hooks/useProjects";
|
||||
|
||||
interface NavItem {
|
||||
to?: string;
|
||||
projectPath?: string;
|
||||
labelKey: string;
|
||||
label: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ to: "/", labelKey: "nav.dashboard", label: "Dashboard", icon: LayoutDashboard },
|
||||
{ to: "/onboard", labelKey: "nav.onboard", label: "New Project", icon: UploadCloud },
|
||||
{ projectPath: "sources", labelKey: "nav.sources", label: "Source Explorer", icon: Settings2 },
|
||||
{ projectPath: "crawl", labelKey: "nav.crawl", label: "Seed Crawl", icon: Activity },
|
||||
{ projectPath: "pipeline", labelKey: "nav.pipeline", label: "Build Pipeline", icon: Layers3 },
|
||||
{ projectPath: "analysis", labelKey: "nav.analysis", label: "Page Analysis", icon: SearchCheck },
|
||||
{ projectPath: "schema", labelKey: "nav.schema", label: "Schema Designer", icon: ShieldCheck },
|
||||
{ projectPath: "editor", labelKey: "nav.editor", label: "Entity Manager", icon: Network },
|
||||
{ projectPath: "review", labelKey: "nav.review", label: "Claim Review", icon: ListChecks },
|
||||
{ projectPath: "quality", labelKey: "nav.quality", label: "Quality Inspector", icon: ClipboardCheck },
|
||||
{ projectPath: "graph", labelKey: "nav.graph", label: "Graph View", icon: GitBranch },
|
||||
{ projectPath: "export", labelKey: "nav.export", label: "Export / API", icon: Code2 },
|
||||
{ projectPath: "research", labelKey: "nav.research", label: "Graph Research", icon: Brain },
|
||||
];
|
||||
|
||||
const projectRoutePatterns = [
|
||||
"/sources/:projectId",
|
||||
"/crawl/:projectId",
|
||||
"/pipeline/:projectId",
|
||||
"/analysis/:projectId",
|
||||
"/schema/:projectId",
|
||||
"/research/:projectId",
|
||||
"/editor/:projectId",
|
||||
"/review/:projectId",
|
||||
"/quality/:projectId",
|
||||
"/graph/:projectId",
|
||||
"/export/:projectId",
|
||||
];
|
||||
|
||||
function projectIdFromPathname(pathname: string): string | undefined {
|
||||
for (const pattern of projectRoutePatterns) {
|
||||
const match = matchPath({ path: pattern, end: false }, pathname);
|
||||
if (match?.params.projectId) return match.params.projectId;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function workspaceSectionFromPathname(pathname: string): string | undefined {
|
||||
for (const pattern of projectRoutePatterns) {
|
||||
const match = matchPath({ path: pattern, end: false }, pathname);
|
||||
if (match?.params.projectId) return pattern.split("/")[1];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default function AppShell() {
|
||||
const { t } = useTranslation();
|
||||
const sidebarOpen = useSelector((s: RootState) => s.ui.sidebarOpen);
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const routeProjectId = projectIdFromPathname(location.pathname);
|
||||
const workspaceSection = workspaceSectionFromPathname(location.pathname);
|
||||
const { data: projects } = useProjects();
|
||||
const fallbackProjectId = projects?.[0]?.name;
|
||||
const routeProjectExists =
|
||||
!routeProjectId ||
|
||||
!projects ||
|
||||
projects.some((project) => project.name === routeProjectId);
|
||||
const currentProjectId = routeProjectExists
|
||||
? routeProjectId ?? fallbackProjectId
|
||||
: fallbackProjectId;
|
||||
|
||||
useEffect(() => {
|
||||
if (routeProjectId && projects && !routeProjectExists) {
|
||||
const nextPath =
|
||||
workspaceSection && fallbackProjectId
|
||||
? `/${workspaceSection}/${encodeURIComponent(fallbackProjectId)}`
|
||||
: "/";
|
||||
navigate(nextPath, { replace: true });
|
||||
}
|
||||
}, [
|
||||
fallbackProjectId,
|
||||
navigate,
|
||||
projects,
|
||||
routeProjectExists,
|
||||
routeProjectId,
|
||||
workspaceSection,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-background text-foreground">
|
||||
<aside
|
||||
className={cn(
|
||||
"border-r bg-card transition-all duration-200 ease-out",
|
||||
sidebarOpen ? "w-60" : "w-16",
|
||||
)}
|
||||
>
|
||||
<div className="flex h-14 items-center justify-between px-4 border-b">
|
||||
{sidebarOpen && (
|
||||
<span className="text-sm font-semibold">
|
||||
{t("app.title", "Ontology Builder")}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => dispatch(toggleSidebar())}
|
||||
aria-label={t("nav.toggleSidebar", "Toggle sidebar")}
|
||||
>
|
||||
<Menu className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{sidebarOpen && (
|
||||
<div className="border-b px-4 py-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("nav.currentProject", "Current project")}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-sm font-medium">
|
||||
{currentProjectId ??
|
||||
t("nav.noProjectSelected", "Select a project")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<nav className="flex flex-col gap-1 p-2">
|
||||
{navItems.map(({ to, projectPath, labelKey, label, icon: Icon }) => {
|
||||
const href =
|
||||
to ??
|
||||
(currentProjectId
|
||||
? `/${projectPath}/${encodeURIComponent(currentProjectId)}`
|
||||
: undefined);
|
||||
|
||||
if (!href) {
|
||||
return (
|
||||
<div
|
||||
key={labelKey}
|
||||
className="flex cursor-not-allowed items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground/50"
|
||||
title={t(
|
||||
"nav.selectProjectFirst",
|
||||
"Select a project from the dashboard first",
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||
{sidebarOpen && <span>{t(labelKey, label)}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={labelKey}
|
||||
to={href}
|
||||
end={href === "/"}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
|
||||
isActive
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||
{sidebarOpen && <span>{t(labelKey, label)}</span>}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div className="flex flex-1 flex-col">
|
||||
<header className="flex h-14 items-center justify-between border-b bg-card px-6">
|
||||
<h1 className="text-sm font-medium text-muted-foreground">
|
||||
{t("app.subtitle", "AI-powered ontology construction")}
|
||||
</h1>
|
||||
</header>
|
||||
<main className="flex-1 overflow-auto">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
ontology_platform/web/frontend/src/components/ui/badge.tsx
Normal file
36
ontology_platform/web/frontend/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary text-primary-foreground",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground",
|
||||
outline: "text-foreground",
|
||||
success:
|
||||
"border-transparent bg-green-100 text-green-800",
|
||||
warning:
|
||||
"border-transparent bg-yellow-100 text-yellow-800",
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: "default" },
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLSpanElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
export function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<span className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { badgeVariants };
|
||||
51
ontology_platform/web/frontend/src/components/ui/button.tsx
Normal file
51
ontology_platform/web/frontend/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } 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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { buttonVariants };
|
||||
76
ontology_platform/web/frontend/src/components/ui/card.tsx
Normal file
76
ontology_platform/web/frontend/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
export const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
export const CardTitle = React.forwardRef<
|
||||
HTMLHeadingElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
export const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
export const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
export const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
21
ontology_platform/web/frontend/src/components/ui/input.tsx
Normal file
21
ontology_platform/web/frontend/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type = "text", ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
17
ontology_platform/web/frontend/src/components/ui/label.tsx
Normal file
17
ontology_platform/web/frontend/src/components/ui/label.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const Label = React.forwardRef<
|
||||
HTMLLabelElement,
|
||||
React.LabelHTMLAttributes<HTMLLabelElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = "Label";
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
value?: number;
|
||||
max?: number;
|
||||
indeterminate?: boolean;
|
||||
}
|
||||
|
||||
export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
|
||||
({ className, value = 0, max = 100, indeterminate, ...props }, ref) => {
|
||||
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={max}
|
||||
aria-valuenow={indeterminate ? undefined : value}
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-secondary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full bg-primary transition-all",
|
||||
indeterminate && "w-1/3 animate-pulse",
|
||||
)}
|
||||
style={indeterminate ? undefined : { width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Progress.displayName = "Progress";
|
||||
22
ontology_platform/web/frontend/src/components/ui/select.tsx
Normal file
22
ontology_platform/web/frontend/src/components/ui/select.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type SelectProps = React.SelectHTMLAttributes<HTMLSelectElement>;
|
||||
|
||||
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<select
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
);
|
||||
},
|
||||
);
|
||||
Select.displayName = "Select";
|
||||
@@ -0,0 +1,13 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
98
ontology_platform/web/frontend/src/components/ui/tabs.tsx
Normal file
98
ontology_platform/web/frontend/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface TabsContextValue {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const TabsContext = React.createContext<TabsContextValue | null>(null);
|
||||
|
||||
function useTabs() {
|
||||
const ctx = React.useContext(TabsContext);
|
||||
if (!ctx) throw new Error("Tabs primitives must be used within <Tabs>");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
interface TabsProps {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Tabs({ value, onValueChange, className, children }: TabsProps) {
|
||||
return (
|
||||
<TabsContext.Provider value={{ value, onChange: onValueChange }}>
|
||||
<div className={cn("flex flex-col gap-3", className)}>{children}</div>
|
||||
</TabsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabsList({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role="tablist"
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-start gap-1 rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabsTrigger({
|
||||
value,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const ctx = useTabs();
|
||||
const active = ctx.value === value;
|
||||
return (
|
||||
<button
|
||||
role="tab"
|
||||
type="button"
|
||||
aria-selected={active}
|
||||
onClick={() => ctx.onChange(value)}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
active
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "hover:text-foreground",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabsContent({
|
||||
value,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const ctx = useTabs();
|
||||
if (ctx.value !== value) return null;
|
||||
return (
|
||||
<div role="tabpanel" className={cn("focus-visible:outline-none", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||
|
||||
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Textarea.displayName = "Textarea";
|
||||
Reference in New Issue
Block a user