feat: add boards and tasks management endpoints

This commit is contained in:
Abhimanyu Saharan
2026-02-04 02:28:51 +05:30
parent 23faa0865b
commit 1abc8f68f3
170 changed files with 6860 additions and 10706 deletions

View File

@@ -0,0 +1,17 @@
export function BrandMark() {
return (
<div className="flex items-center gap-3">
<div className="grid h-10 w-10 place-items-center rounded-lg border-2 border-gray-200 bg-white text-sm font-bold text-gray-900 shadow-lush">
<span className="font-heading tracking-[0.2em]">
OC
</span>
</div>
<div className="leading-tight">
<div className="font-heading text-sm uppercase tracking-[0.28em] text-gray-600">
OpenClaw
</div>
<div className="text-[11px] font-medium text-gray-500">Mission Control</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import type { ReactNode } from "react";
export function HeroKicker({ children }: { children: ReactNode }) {
return (
<span className="inline-flex items-center rounded-full bg-gray-100 px-4 py-1 text-[11px] font-semibold uppercase tracking-[0.35em] text-gray-600">
{children}
</span>
);
}

View File

@@ -0,0 +1,21 @@
import { Badge } from "@/components/ui/badge";
const STATUS_STYLES: Record<string, "default" | "outline" | "ember"> = {
inbox: "outline",
assigned: "default",
in_progress: "ember",
testing: "outline",
review: "default",
done: "default",
online: "default",
busy: "ember",
offline: "outline",
};
export function StatusPill({ status }: { status: string }) {
return (
<Badge variant={STATUS_STYLES[status] ?? "default"}>
{status.replace("_", " ")}
</Badge>
);
}

View File

@@ -0,0 +1,20 @@
import { HeroKicker } from "@/components/atoms/HeroKicker";
export function HeroCopy() {
return (
<div className="space-y-6">
<HeroKicker>Mission Control</HeroKicker>
<div className="space-y-4">
<h1 className="font-heading text-4xl font-bold leading-tight text-gray-900 sm:text-5xl lg:text-6xl">
Orchestrate work without
<br />
the daily status chase.
</h1>
<p className="max-w-xl text-base text-gray-600 sm:text-lg">
OpenClaw keeps every task, agent, and delivery signal in one place so
teams can spot momentum shifts fast.
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import { CalendarClock, UserCircle } from "lucide-react";
import { StatusPill } from "@/components/atoms/StatusPill";
import { Card, CardContent } from "@/components/ui/card";
interface TaskCardProps {
title: string;
status: string;
assignee?: string;
due?: string;
}
export function TaskCard({ title, status, assignee, due }: TaskCardProps) {
return (
<Card className="border-gray-200 bg-white">
<CardContent className="space-y-4">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<p className="text-sm font-semibold text-gray-900">{title}</p>
<StatusPill status={status} />
</div>
</div>
<div className="flex items-center justify-between text-xs text-gray-600">
<div className="flex items-center gap-2">
<UserCircle className="h-4 w-4" />
<span>{assignee ?? "Unassigned"}</span>
</div>
{due ? (
<div className="flex items-center gap-2">
<CalendarClock className="h-4 w-4" />
<span>{due}</span>
</div>
) : null}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,42 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
export function DashboardSidebar() {
const pathname = usePathname();
return (
<aside className="flex h-full flex-col gap-6 rounded-xl border-2 border-gray-200 bg-white p-6 shadow-lush">
<div className="space-y-2">
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-gray-500">
Work
</p>
<nav className="space-y-1 text-sm">
<Link
href="/agents"
className={cn(
"block rounded-lg border border-transparent px-3 py-2 font-medium text-gray-700 hover:border-gray-200 hover:bg-gray-50",
pathname.startsWith("/agents") &&
"border-gray-200 bg-gray-50 text-gray-900"
)}
>
Agents
</Link>
<Link
href="/boards"
className={cn(
"block rounded-lg border border-transparent px-3 py-2 font-medium text-gray-700 hover:border-gray-200 hover:bg-gray-50",
pathname.startsWith("/boards") &&
"border-gray-200 bg-gray-50 text-gray-900"
)}
>
Boards
</Link>
</nav>
</div>
</aside>
);
}

View File

@@ -0,0 +1,88 @@
"use client";
import { SignInButton, SignedIn, SignedOut } from "@clerk/nextjs";
import { HeroCopy } from "@/components/molecules/HeroCopy";
import { Button } from "@/components/ui/button";
export function LandingHero() {
return (
<section className="grid w-full items-center gap-10 lg:grid-cols-[1.05fr_0.95fr]">
<div
className="space-y-8 animate-fade-in-up"
style={{ animationDelay: "0.05s" }}
>
<HeroCopy />
<div
className="flex flex-col gap-3 sm:flex-row sm:items-center animate-fade-in-up"
style={{ animationDelay: "0.12s" }}
>
<SignedOut>
<SignInButton
mode="modal"
afterSignInUrl="/boards"
afterSignUpUrl="/boards"
forceRedirectUrl="/boards"
signUpForceRedirectUrl="/boards"
>
<Button
size="lg"
className="w-full sm:w-auto border-2 border-gray-900 bg-gray-900 text-white hover:bg-gray-900/90"
>
Sign in to open mission control
</Button>
</SignInButton>
</SignedOut>
<SignedIn>
<div className="text-sm text-gray-600">
You&apos;re signed in. Open your boards when you&apos;re ready.
</div>
</SignedIn>
</div>
<p
className="text-xs uppercase tracking-[0.3em] text-gray-500 animate-fade-in-up"
style={{ animationDelay: "0.18s" }}
>
One login · clear ownership · faster decisions
</p>
</div>
<div
className="relative animate-fade-in-up"
style={{ animationDelay: "0.3s" }}
>
<div className="glass-panel rounded-2xl bg-white p-6">
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between text-xs font-semibold uppercase tracking-[0.3em] text-gray-500">
<span>Status</span>
<span className="rounded-full border border-gray-200 px-2 py-1 text-[10px]">
Live
</span>
</div>
<div className="space-y-2">
<p className="text-lg font-semibold text-gray-900">
Tasks claimed automatically
</p>
<p className="text-sm text-gray-600">
Agents pick the next task in queue, report progress, and ship
deliverables back to you.
</p>
</div>
<div className="grid grid-cols-2 gap-3">
{["Assignments", "In review", "Delivered", "Signals"].map(
(label) => (
<div
key={label}
className="rounded-xl border-2 border-gray-200 bg-white p-4 text-sm font-semibold text-gray-900 soft-shadow-sm"
>
{label}
</div>
)
)}
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,99 @@
"use client";
import { useMemo } from "react";
import { TaskCard } from "@/components/molecules/TaskCard";
import { cn } from "@/lib/utils";
type Task = {
id: string;
title: string;
status: string;
due_at?: string | null;
};
type TaskBoardProps = {
tasks: Task[];
onCreateTask: () => void;
isCreateDisabled?: boolean;
};
const columns = [
{ title: "Inbox", status: "inbox" },
{ title: "Assigned", status: "assigned" },
{ title: "In Progress", status: "in_progress" },
{ title: "Testing", status: "testing" },
{ title: "Review", status: "review" },
{ title: "Done", status: "done" },
];
const formatDueDate = (value?: string | null) => {
if (!value) return undefined;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return undefined;
return date.toLocaleDateString(undefined, {
month: "short",
day: "numeric",
});
};
export function TaskBoard({
tasks,
onCreateTask,
isCreateDisabled = false,
}: TaskBoardProps) {
const grouped = useMemo(() => {
const buckets: Record<string, Task[]> = {};
for (const column of columns) {
buckets[column.status] = [];
}
tasks.forEach((task) => {
const bucket = buckets[task.status] ?? buckets.inbox;
bucket.push(task);
});
return buckets;
}, [tasks]);
return (
<div className="grid grid-flow-col auto-cols-[minmax(260px,320px)] gap-6 overflow-x-auto pb-2">
{columns.map((column) => {
const columnTasks = grouped[column.status] ?? [];
return (
<div key={column.title} className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-900">
{column.title}
</h3>
<span className="text-xs text-gray-500">
{columnTasks.length}
</span>
</div>
<div className="space-y-3">
{column.status === "inbox" ? (
<button
type="button"
onClick={onCreateTask}
disabled={isCreateDisabled}
className={cn(
"flex w-full items-center justify-center rounded-xl border-2 border-dashed border-gray-200 bg-gray-50 px-4 py-6 text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 transition hover:border-gray-300 hover:bg-white",
isCreateDisabled && "cursor-not-allowed opacity-60"
)}
>
New task
</button>
) : null}
{columnTasks.map((task) => (
<TaskCard
key={task.id}
title={task.title}
status={column.status}
due={formatDueDate(task.due_at)}
/>
))}
</div>
</div>
);
})}
</div>
);
}

View File

@@ -0,0 +1,31 @@
"use client";
import type { ReactNode } from "react";
import { SignedIn, UserButton } from "@clerk/nextjs";
import { BrandMark } from "@/components/atoms/BrandMark";
export function DashboardShell({ children }: { children: ReactNode }) {
return (
<div className="relative min-h-screen bg-white text-gray-900">
<div
className="absolute inset-0 bg-landing-grid opacity-[0.35] pointer-events-none"
aria-hidden="true"
/>
<div className="relative flex min-h-screen w-full flex-col gap-8 px-6 pb-10 pt-8">
<header className="flex flex-wrap items-center justify-between gap-4">
<BrandMark />
<SignedIn>
<div className="rounded-lg border-2 border-gray-200 bg-white px-2 py-1">
<UserButton />
</div>
</SignedIn>
</header>
<div className="grid flex-1 gap-6 lg:grid-cols-[320px_1fr]">
{children}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
"use client";
import type { ReactNode } from "react";
import { SignedIn, UserButton } from "@clerk/nextjs";
import { BrandMark } from "@/components/atoms/BrandMark";
export function LandingShell({ children }: { children: ReactNode }) {
return (
<div className="landing-page bg-white text-gray-900">
<section className="relative overflow-hidden pt-24 pb-16 px-4 sm:px-6 lg:px-8">
<div
className="absolute inset-0 bg-landing-grid opacity-[0.35] pointer-events-none"
aria-hidden="true"
/>
<div
className="absolute -top-28 right-0 h-64 w-64 rounded-full bg-gray-100 blur-3xl pointer-events-none"
aria-hidden="true"
/>
<div
className="absolute -bottom-32 left-0 h-72 w-72 rounded-full bg-gray-100 blur-3xl pointer-events-none"
aria-hidden="true"
/>
<div className="relative w-full">
<header className="flex items-center justify-between pb-12">
<BrandMark />
<SignedIn>
<div className="rounded-lg border-2 border-gray-200 bg-white px-2 py-1">
<UserButton />
</div>
</SignedIn>
</header>
<main>{children}</main>
</div>
</section>
</div>
);
}

View File

@@ -4,19 +4,19 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors",
"inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground",
secondary: "border-transparent bg-secondary text-secondary-foreground",
outline: "text-foreground",
default: "bg-gray-100 text-gray-800",
outline: "border border-gray-300 text-gray-800",
ember: "bg-gray-900 text-white",
},
},
defaultVariants: {
variant: "default",
},
},
}
);
export interface BadgeProps

View File

@@ -1,30 +1,30 @@
"use client";
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 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center gap-2 rounded-lg text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:opacity-90",
secondary: "bg-secondary text-secondary-foreground hover:opacity-90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
destructive: "bg-destructive text-destructive-foreground hover:opacity-90",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
primary:
"border-2 border-gray-900 bg-gray-900 text-white hover:bg-gray-900/90",
secondary:
"border-2 border-gray-200 bg-white text-gray-900 hover:border-gray-900 hover:bg-gray-900 hover:text-white",
ghost: "bg-transparent text-gray-900 hover:bg-black/5",
},
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",
sm: "h-9 px-4",
md: "h-11 px-5",
lg: "h-12 px-6 text-base",
},
},
defaultVariants: {
variant: "default",
size: "default",
variant: "primary",
size: "md",
},
}
);
@@ -34,15 +34,13 @@ export interface ButtonProps
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
({ className, variant, size, ...props }, ref) => (
<button
ref={ref}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
);
Button.displayName = "Button";

View File

@@ -6,46 +6,30 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
className={cn(
"rounded-xl border-2 border-gray-200 bg-white soft-shadow-sm",
className
)}
{...props}
/>
)
);
Card.displayName = "Card";
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} />
)
);
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("px-6 pt-6", className)} {...props} />
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
)
);
CardTitle.displayName = "CardTitle";
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";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
)
);
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("px-6 pb-6", className)} {...props} />
));
CardContent.displayName = "CardContent";
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";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
export { Card, CardHeader, CardContent };

View File

@@ -0,0 +1,102 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 w-[90vw] max-w-2xl translate-x-[-50%] translate-y-[-50%] rounded-2xl border-2 border-gray-200 bg-white p-6 shadow-lush focus:outline-none 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",
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col gap-2", className)} {...props} />
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-wrap items-center justify-end gap-2", className)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-gray-900", className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-gray-600", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@@ -2,22 +2,18 @@ import * as React from "react";
import { cn } from "@/lib/utils";
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium 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,
)}
ref={ref}
{...props}
/>
);
},
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
({ className, type, ...props }, ref) => (
<input
ref={ref}
type={type}
className={cn(
"flex h-11 w-full rounded-lg border-2 border-gray-200 bg-white px-4 text-sm text-gray-900 shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black",
className
)}
{...props}
/>
)
);
Input.displayName = "Input";

View File

@@ -1,20 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
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";
export { Label };

View File

@@ -1,23 +1,151 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
export type SelectProps = React.SelectHTMLAttributes<HTMLSelectElement>
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, children, ...props }, ref) => (
<select
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
type="button"
className={cn(
"flex h-11 w-full cursor-pointer items-center justify-between rounded-lg border-2 border-gray-200 bg-white px-4 text-sm text-gray-900 shadow-sm focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 text-gray-500" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = "SelectTrigger";
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-pointer items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = "SelectScrollUpButton";
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-pointer items-center justify-center py-1", className)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = "SelectScrollDownButton";
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
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",
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-xl border-2 border-gray-200 bg-white 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}
>
{children}
</select>
),
);
Select.displayName = "Select";
<SelectScrollUpButton />
<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>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = "SelectContent";
export { Select };
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = "SelectLabel";
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm text-gray-800 outline-none focus:bg-black/5 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = "SelectItem";
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-gray-200", className)}
{...props}
/>
));
SelectSeparator.displayName = "SelectSeparator";
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};

View File

@@ -0,0 +1,52 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex items-center gap-2 rounded-full border-2 border-gray-200 bg-white p-1",
className
)}
{...props}
/>
));
TabsList.displayName = "TabsList";
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"rounded-full px-4 py-2 text-xs font-semibold text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white",
className
)}
{...props}
/>
));
TabsTrigger.displayName = "TabsTrigger";
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn("mt-4 focus-visible:outline-none", className)}
{...props}
/>
));
TabsContent.displayName = "TabsContent";
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -2,22 +2,19 @@ import * as React from "react";
import { cn } from "@/lib/utils";
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
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,
)}
ref={ref}
{...props}
/>
);
},
);
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.TextareaHTMLAttributes<HTMLTextAreaElement>
>(({ className, ...props }, ref) => (
<textarea
ref={ref}
className={cn(
"min-h-[120px] w-full rounded-lg border-2 border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black",
className
)}
{...props}
/>
));
Textarea.displayName = "Textarea";
export { Textarea };

View File

@@ -0,0 +1,28 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 6, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"rounded-lg bg-gray-900 px-3 py-2 text-xs font-semibold text-white soft-shadow-sm",
className
)}
{...props}
/>
));
TooltipContent.displayName = "TooltipContent";
export { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent };