Arreglos layout

This commit is contained in:
David Arranz 2026-04-12 19:28:26 +02:00
parent 55a6b1fbaf
commit 1bbaf0aaee
21 changed files with 371 additions and 1728 deletions

View File

@ -21,7 +21,7 @@ export function PageHeader({
className,
}: PageHeaderProps) {
return (
<div className={cn("flex flex-row items-center justify-between", className)}>
<div className={cn("flex flex-row items-center justify-between mb-6", className)}>
{/* Lado izquierdo */}
<div className="min-w-0 flex-1">
<div className="flex items-start gap-4">
@ -36,7 +36,7 @@ export function PageHeader({
</Button>
)}
<div>
<div className="space-y-2">
<h1 className="text-xl font-semibold tracking-tight lg:text-2xl h-8 text-foreground sm:truncate sm:tracking-tight">
{title}
</h1>

View File

@ -2,7 +2,6 @@ import {
Field,
FieldDescription,
FieldError,
FormControl,
Select,
SelectContent,
SelectItem,
@ -90,25 +89,23 @@ export function SelectField<TFormValues extends FieldValues>({
onValueChange={field.onChange}
value={field.value ?? undefined}
>
<FormControl>
<SelectTrigger
aria-invalid={fieldState.invalid}
aria-required={required}
className={cn(
"bg-muted/50 font-medium",
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
"placeholder:text-muted-foreground/50",
inputClassName
)}
id={triggerId}
>
<SelectValue
className={"placeholder:font-normal placeholder:italic"}
placeholder={placeholder}
/>
</SelectTrigger>
</FormControl>
<SelectTrigger
aria-invalid={fieldState.invalid}
aria-required={required}
className={cn(
"bg-muted/50 font-medium",
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
"placeholder:text-muted-foreground/50",
inputClassName
)}
id={triggerId}
>
<SelectValue
className={"placeholder:font-normal placeholder:italic"}
placeholder={placeholder}
/>
</SelectTrigger>
<SelectContent>
{normalizedItems.map((item) => (
<SelectItem key={`key-${item.value}`} value={item.value}>

View File

@ -7,6 +7,7 @@ export * from "./error-overlay.tsx";
export * from "./form/index.ts";
export * from "./full-screen-modal.tsx";
export * from "./grid/index.ts";
export * from "./initials-avatar.tsx";
export * from "./layout/index.ts";
export * from "./loading-overlay/index.ts";
export * from "./logo-verifactu.tsx";

View File

@ -0,0 +1,65 @@
import { Avatar, AvatarFallback } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import type * as React from "react";
type InitialsProps = {
name?: string | null;
maxParts?: number;
fallback?: string;
};
export const getInitials = ({ name, maxParts = 2, fallback = "?" }: InitialsProps): string => {
if (!name?.trim()) return fallback;
return (
name
.trim()
.split(/\s+/)
.filter(Boolean)
.slice(0, maxParts)
.map((p) => p[0]?.toUpperCase() ?? "")
.join("") || fallback
);
};
export const Initials = (props: InitialsProps) => {
return <>{getInitials(props)}</>;
};
const VARIANT_CLASSES: Record<string, string> = {
primary: "bg-primary/10 text-primary",
muted: "bg-muted text-muted-foreground",
};
type InitialsAvatarProps = React.ComponentProps<typeof Avatar> & {
name?: string | null;
variant?: "primary" | "muted";
maxParts?: number;
fallback?: string;
fallbackClassName?: string;
};
export const InitialsAvatar = ({
name,
variant = "muted",
maxParts = 2,
fallback = "?",
className,
fallbackClassName,
...props
}: InitialsAvatarProps) => {
return (
<Avatar
className={cn(
"size-10 border-2 shadow-sm",
variant === "primary" && "border-primary",
className
)}
{...props}
>
<AvatarFallback className={cn(VARIANT_CLASSES[variant], fallbackClassName)}>
<Initials fallback={fallback} maxParts={maxParts} name={name} />
</AvatarFallback>
</Avatar>
);
};

View File

@ -5,28 +5,20 @@ import {
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Separator,
SidebarTrigger,
} from "@repo/shadcn-ui/components";
export const AppBreadcrumb = () => {
return (
<header className="app-breadcrumb flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-6">
<SidebarTrigger className="-ml-1" />
<Separator className="mr-2 h-4" orientation="vertical" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">Building Your Application</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">Building Your Application</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);
};

View File

@ -7,8 +7,8 @@ export const AppHeader = ({
...props
}: PropsWithChildren<{ className?: string }>) => {
return (
<div className={cn("app-header", className)} {...props}>
<header className={cn("app-header space-y-3", className)} {...props}>
{children}
</div>
</header>
);
};

View File

@ -2,26 +2,24 @@ import { SidebarInset, SidebarProvider } from "@repo/shadcn-ui/components";
import { Outlet } from "react-router";
import { AppSidebar } from "./app-sidebar.tsx";
import { SiteHeader } from "./site-header.tsx";
import { AppTopbar } from "./app-topbar.tsx";
export const AppLayout = () => {
return (
<SidebarProvider
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
"--header-height": "calc(var(--spacing) * 12)",
"--sidebar-width": "16rem",
"--sidebar-width-mobile": "18rem",
} as React.CSSProperties
}
>
<AppSidebar className="bg-sidebar" variant="inset" />
<AppSidebar className="bg-sidebar" />
{/* Aquí está el MAIN */}
<SidebarInset className="app-main bg-muted ">
<SiteHeader />
<div className="flex flex-1 flex-col">
<div className="@container/main p-(--content-padding) xl:group-data-[theme-content-layout=centered]/layout:container xl:group-data-[theme-content-layout=centered]/layout:mx-auto">
<Outlet />
</div>
<AppTopbar />
<div className="flex flex-1 flex-col px-4 py-4 sm:px-6 sm:py-6">
<Outlet />
</div>
</SidebarInset>
</SidebarProvider>

View File

@ -1,4 +1,10 @@
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from "@repo/shadcn-ui/components";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarRail,
} from "@repo/shadcn-ui/components";
import {
CameraIcon,
CircleIcon,
@ -140,8 +146,8 @@ const data = {
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader className="mb-3">
<Sidebar collapsible="icon" variant="sidebar" {...props}>
<SidebarHeader>
<TeamSwitcher teams={data.teams} />
</SidebarHeader>
<SidebarContent>
@ -151,6 +157,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
}

View File

@ -0,0 +1,37 @@
import {
Avatar,
AvatarFallback,
AvatarImage,
Button,
Separator,
SidebarTrigger,
} from "@repo/shadcn-ui/components";
import { AppBreadcrumb } from "./app-breadcrumb.tsx";
import { ProfileDropdown } from "./dropdown-profile.tsx";
export function AppTopbar() {
return (
<header className="bg-card sticky top-0 z-50 border-b">
<div className="flex items-center justify-between gap-6 px-4 py-2 sm:px-6">
<div className="flex items-center gap-4 align-middle">
<SidebarTrigger className="[&_svg]:size-5!" />
<Separator className="hidden h-8! sm:block" orientation="vertical" />
<AppBreadcrumb />
</div>
<div className="flex items-center gap-1.5">
<ProfileDropdown
trigger={
<Button className="size-9.5" size="icon" variant="ghost">
<Avatar className="size-9.5 rounded-md">
<AvatarImage src="https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
</Button>
}
/>
</div>
</div>
</header>
);
}

View File

@ -1,263 +0,0 @@
"use client";
import * as React from "react";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
ToggleGroup,
ToggleGroupItem,
} from "@repo/shadcn-ui/components";
import { useIsMobile } from "@repo/shadcn-ui/hooks/";
const chartData = [
{ date: "2024-04-01", desktop: 222, mobile: 150 },
{ date: "2024-04-02", desktop: 97, mobile: 180 },
{ date: "2024-04-03", desktop: 167, mobile: 120 },
{ date: "2024-04-04", desktop: 242, mobile: 260 },
{ date: "2024-04-05", desktop: 373, mobile: 290 },
{ date: "2024-04-06", desktop: 301, mobile: 340 },
{ date: "2024-04-07", desktop: 245, mobile: 180 },
{ date: "2024-04-08", desktop: 409, mobile: 320 },
{ date: "2024-04-09", desktop: 59, mobile: 110 },
{ date: "2024-04-10", desktop: 261, mobile: 190 },
{ date: "2024-04-11", desktop: 327, mobile: 350 },
{ date: "2024-04-12", desktop: 292, mobile: 210 },
{ date: "2024-04-13", desktop: 342, mobile: 380 },
{ date: "2024-04-14", desktop: 137, mobile: 220 },
{ date: "2024-04-15", desktop: 120, mobile: 170 },
{ date: "2024-04-16", desktop: 138, mobile: 190 },
{ date: "2024-04-17", desktop: 446, mobile: 360 },
{ date: "2024-04-18", desktop: 364, mobile: 410 },
{ date: "2024-04-19", desktop: 243, mobile: 180 },
{ date: "2024-04-20", desktop: 89, mobile: 150 },
{ date: "2024-04-21", desktop: 137, mobile: 200 },
{ date: "2024-04-22", desktop: 224, mobile: 170 },
{ date: "2024-04-23", desktop: 138, mobile: 230 },
{ date: "2024-04-24", desktop: 387, mobile: 290 },
{ date: "2024-04-25", desktop: 215, mobile: 250 },
{ date: "2024-04-26", desktop: 75, mobile: 130 },
{ date: "2024-04-27", desktop: 383, mobile: 420 },
{ date: "2024-04-28", desktop: 122, mobile: 180 },
{ date: "2024-04-29", desktop: 315, mobile: 240 },
{ date: "2024-04-30", desktop: 454, mobile: 380 },
{ date: "2024-05-01", desktop: 165, mobile: 220 },
{ date: "2024-05-02", desktop: 293, mobile: 310 },
{ date: "2024-05-03", desktop: 247, mobile: 190 },
{ date: "2024-05-04", desktop: 385, mobile: 420 },
{ date: "2024-05-05", desktop: 481, mobile: 390 },
{ date: "2024-05-06", desktop: 498, mobile: 520 },
{ date: "2024-05-07", desktop: 388, mobile: 300 },
{ date: "2024-05-08", desktop: 149, mobile: 210 },
{ date: "2024-05-09", desktop: 227, mobile: 180 },
{ date: "2024-05-10", desktop: 293, mobile: 330 },
{ date: "2024-05-11", desktop: 335, mobile: 270 },
{ date: "2024-05-12", desktop: 197, mobile: 240 },
{ date: "2024-05-13", desktop: 197, mobile: 160 },
{ date: "2024-05-14", desktop: 448, mobile: 490 },
{ date: "2024-05-15", desktop: 473, mobile: 380 },
{ date: "2024-05-16", desktop: 338, mobile: 400 },
{ date: "2024-05-17", desktop: 499, mobile: 420 },
{ date: "2024-05-18", desktop: 315, mobile: 350 },
{ date: "2024-05-19", desktop: 235, mobile: 180 },
{ date: "2024-05-20", desktop: 177, mobile: 230 },
{ date: "2024-05-21", desktop: 82, mobile: 140 },
{ date: "2024-05-22", desktop: 81, mobile: 120 },
{ date: "2024-05-23", desktop: 252, mobile: 290 },
{ date: "2024-05-24", desktop: 294, mobile: 220 },
{ date: "2024-05-25", desktop: 201, mobile: 250 },
{ date: "2024-05-26", desktop: 213, mobile: 170 },
{ date: "2024-05-27", desktop: 420, mobile: 460 },
{ date: "2024-05-28", desktop: 233, mobile: 190 },
{ date: "2024-05-29", desktop: 78, mobile: 130 },
{ date: "2024-05-30", desktop: 340, mobile: 280 },
{ date: "2024-05-31", desktop: 178, mobile: 230 },
{ date: "2024-06-01", desktop: 178, mobile: 200 },
{ date: "2024-06-02", desktop: 470, mobile: 410 },
{ date: "2024-06-03", desktop: 103, mobile: 160 },
{ date: "2024-06-04", desktop: 439, mobile: 380 },
{ date: "2024-06-05", desktop: 88, mobile: 140 },
{ date: "2024-06-06", desktop: 294, mobile: 250 },
{ date: "2024-06-07", desktop: 323, mobile: 370 },
{ date: "2024-06-08", desktop: 385, mobile: 320 },
{ date: "2024-06-09", desktop: 438, mobile: 480 },
{ date: "2024-06-10", desktop: 155, mobile: 200 },
{ date: "2024-06-11", desktop: 92, mobile: 150 },
{ date: "2024-06-12", desktop: 492, mobile: 420 },
{ date: "2024-06-13", desktop: 81, mobile: 130 },
{ date: "2024-06-14", desktop: 426, mobile: 380 },
{ date: "2024-06-15", desktop: 307, mobile: 350 },
{ date: "2024-06-16", desktop: 371, mobile: 310 },
{ date: "2024-06-17", desktop: 475, mobile: 520 },
{ date: "2024-06-18", desktop: 107, mobile: 170 },
{ date: "2024-06-19", desktop: 341, mobile: 290 },
{ date: "2024-06-20", desktop: 408, mobile: 450 },
{ date: "2024-06-21", desktop: 169, mobile: 210 },
{ date: "2024-06-22", desktop: 317, mobile: 270 },
{ date: "2024-06-23", desktop: 480, mobile: 530 },
{ date: "2024-06-24", desktop: 132, mobile: 180 },
{ date: "2024-06-25", desktop: 141, mobile: 190 },
{ date: "2024-06-26", desktop: 434, mobile: 380 },
{ date: "2024-06-27", desktop: 448, mobile: 490 },
{ date: "2024-06-28", desktop: 149, mobile: 200 },
{ date: "2024-06-29", desktop: 103, mobile: 160 },
{ date: "2024-06-30", desktop: 446, mobile: 400 },
];
const chartConfig = {
visitors: {
label: "Visitors",
},
desktop: {
label: "Desktop",
color: "hsl(var(--chart-1))",
},
mobile: {
label: "Mobile",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
export function ChartAreaInteractive() {
const isMobile = useIsMobile();
const [timeRange, setTimeRange] = React.useState("30d");
React.useEffect(() => {
if (isMobile) {
setTimeRange("7d");
}
}, [isMobile]);
const filteredData = chartData.filter((item) => {
const date = new Date(item.date);
const referenceDate = new Date("2024-06-30");
let daysToSubtract = 90;
if (timeRange === "30d") {
daysToSubtract = 30;
} else if (timeRange === "7d") {
daysToSubtract = 7;
}
const startDate = new Date(referenceDate);
startDate.setDate(startDate.getDate() - daysToSubtract);
return date >= startDate;
});
return (
<Card className='@container/card'>
<CardHeader className='relative'>
<CardTitle>Total Visitors</CardTitle>
<CardDescription>
<span className='@[540px]/card:block hidden'>Total for the last 3 months</span>
<span className='@[540px]/card:hidden'>Last 3 months</span>
</CardDescription>
<div className='absolute right-4 top-4'>
<ToggleGroup
type='single'
value={timeRange}
onValueChange={setTimeRange}
variant='outline'
className='@[767px]/card:flex hidden'
>
<ToggleGroupItem value='90d' className='h-8 px-2.5'>
Last 3 months
</ToggleGroupItem>
<ToggleGroupItem value='30d' className='h-8 px-2.5'>
Last 30 days
</ToggleGroupItem>
<ToggleGroupItem value='7d' className='h-8 px-2.5'>
Last 7 days
</ToggleGroupItem>
</ToggleGroup>
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className='@[767px]/card:hidden flex w-40' aria-label='Select a value'>
<SelectValue placeholder='Last 3 months' />
</SelectTrigger>
<SelectContent className='rounded-xl'>
<SelectItem value='90d' className='rounded-lg'>
Last 3 months
</SelectItem>
<SelectItem value='30d' className='rounded-lg'>
Last 30 days
</SelectItem>
<SelectItem value='7d' className='rounded-lg'>
Last 7 days
</SelectItem>
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent className='px-2 pt-4 sm:px-6 sm:pt-6'>
<ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>
<AreaChart data={filteredData}>
<defs>
<linearGradient id='fillDesktop' x1='0' y1='0' x2='0' y2='1'>
<stop offset='5%' stopColor='var(--color-desktop)' stopOpacity={1.0} />
<stop offset='95%' stopColor='var(--color-desktop)' stopOpacity={0.1} />
</linearGradient>
<linearGradient id='fillMobile' x1='0' y1='0' x2='0' y2='1'>
<stop offset='5%' stopColor='var(--color-mobile)' stopOpacity={0.8} />
<stop offset='95%' stopColor='var(--color-mobile)' stopOpacity={0.1} />
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey='date'
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={(value) => {
return new Date(value).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}}
indicator='dot'
/>
}
/>
<Area
dataKey='mobile'
type='natural'
fill='url(#fillMobile)'
stroke='var(--color-mobile)'
stackId='a'
/>
<Area
dataKey='desktop'
type='natural'
fill='url(#fillDesktop)'
stroke='var(--color-desktop)'
stackId='a'
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@ -1,751 +0,0 @@
"use client";
import {
DndContext,
type DragEndEvent,
KeyboardSensor,
MouseSensor,
TouchSensor,
type UniqueIdentifier,
closestCenter,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import {
SortableContext,
arrayMove,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Badge } from "@repo/shadcn-ui/components/badge";
import { Button } from "@repo/shadcn-ui/components/button";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@repo/shadcn-ui/components/chart";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@repo/shadcn-ui/components/dropdown-menu";
import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@repo/shadcn-ui/components/select";
import { Separator } from "@repo/shadcn-ui/components/separator";
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@repo/shadcn-ui/components/sheet";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@repo/shadcn-ui/components/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@repo/shadcn-ui/components/tabs";
import { useIsMobile } from "@repo/shadcn-ui/hooks";
import {
type ColumnDef,
type ColumnFiltersState,
type Row,
type SortingState,
type VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
CheckCircle2Icon,
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
ChevronsLeftIcon,
ChevronsRightIcon,
ColumnsIcon,
GripVerticalIcon,
LoaderIcon,
MoreVerticalIcon,
PlusIcon,
TrendingUpIcon,
} from "lucide-react";
import * as React from "react";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import { toast } from "sonner";
import { z } from "zod/v4";
export const schema = z.object({
id: z.number(),
header: z.string(),
type: z.string(),
status: z.string(),
target: z.string(),
limit: z.string(),
reviewer: z.string(),
});
// Create a separate component for the drag handle
function DragHandle({ id }: { id: number }) {
const { attributes, listeners } = useSortable({
id,
});
return (
<Button
{...attributes}
{...listeners}
className="size-7 text-muted-foreground hover:bg-transparent"
size="icon"
variant="ghost"
>
<GripVerticalIcon className="size-3 text-muted-foreground" />
<span className="sr-only">Drag to reorder</span>
</Button>
);
}
const columns: ColumnDef<z.infer<typeof schema>>[] = [
{
id: "drag",
header: () => null,
cell: ({ row }) => <DragHandle id={row.original.id} />,
},
{
id: "select",
header: ({ table }) => (
<div className="flex items-center justify-center">
<Checkbox
aria-label="Select all"
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
/>
</div>
),
cell: ({ row }) => (
<div className="flex items-center justify-center">
<Checkbox
aria-label="Select row"
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
/>
</div>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "header",
header: "Header",
cell: ({ row }) => {
return <TableCellViewer item={row.original} />;
},
enableHiding: false,
},
{
accessorKey: "type",
header: "Section Type",
cell: ({ row }) => (
<div className="w-32">
<Badge className="px-1.5 text-muted-foreground" variant="outline">
{row.original.type}
</Badge>
</div>
),
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => (
<Badge className="flex gap-1 px-1.5 text-muted-foreground [&_svg]:size-3" variant="outline">
{row.original.status === "Done" ? (
<CheckCircle2Icon className="text-green-500 dark:text-green-400" />
) : (
<LoaderIcon />
)}
{row.original.status}
</Badge>
),
},
{
accessorKey: "target",
header: () => <div className="w-full text-right">Target</div>,
cell: ({ row }) => (
<form
onSubmit={(e) => {
e.preventDefault();
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
loading: `Saving ${row.original.header}`,
success: "Done",
error: "Error",
});
}}
>
<Label className="sr-only" htmlFor={`${row.original.id}-target`}>
Target
</Label>
<Input
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background"
defaultValue={row.original.target}
id={`${row.original.id}-target`}
/>
</form>
),
},
{
accessorKey: "limit",
header: () => <div className="w-full text-right">Limit</div>,
cell: ({ row }) => (
<form
onSubmit={(e) => {
e.preventDefault();
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
loading: `Saving ${row.original.header}`,
success: "Done",
error: "Error",
});
}}
>
<Label className="sr-only" htmlFor={`${row.original.id}-limit`}>
Limit
</Label>
<Input
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background"
defaultValue={row.original.limit}
id={`${row.original.id}-limit`}
/>
</form>
),
},
{
accessorKey: "reviewer",
header: "Reviewer",
cell: ({ row }) => {
const isAssigned = row.original.reviewer !== "Assign reviewer";
if (isAssigned) {
return row.original.reviewer;
}
return (
<>
<Label className="sr-only" htmlFor={`${row.original.id}-reviewer`}>
Reviewer
</Label>
<Select>
<SelectTrigger className="h-8 w-40" id={`${row.original.id}-reviewer`}>
<SelectValue placeholder="Assign reviewer" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
<SelectItem value="Jamik Tashpulatov">Jamik Tashpulatov</SelectItem>
</SelectContent>
</Select>
</>
);
},
},
{
id: "actions",
cell: () => (
<DropdownMenu>
<DropdownMenuTrigger>
<Button
className="flex size-8 text-muted-foreground data-[state=open]:bg-muted"
size="icon"
variant="ghost"
>
<MoreVerticalIcon />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Make a copy</DropdownMenuItem>
<DropdownMenuItem>Favorite</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({
id: row.original.id,
});
return (
<TableRow
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
data-dragging={isDragging}
data-state={row.getIsSelected() && "selected"}
ref={setNodeRef}
style={{
transform: CSS.Transform.toString(transform),
transition: transition,
}}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
);
}
export function DataTable({ data: initialData }: { data: z.infer<typeof schema>[] }) {
const [data, setData] = React.useState(() => initialData);
const [rowSelection, setRowSelection] = React.useState({});
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 10,
});
const sortableId = React.useId();
const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {})
);
const dataIds = React.useMemo<UniqueIdentifier[]>(() => data?.map(({ id }) => id) || [], [data]);
const table = useReactTable({
data,
columns,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
pagination,
},
getRowId: (row) => row.id.toString(),
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
});
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (active && over && active.id !== over.id) {
setData((data) => {
const oldIndex = dataIds.indexOf(active.id);
const newIndex = dataIds.indexOf(over.id);
return arrayMove(data, oldIndex, newIndex);
});
}
}
return (
<Tabs className="flex w-full flex-col justify-start gap-6" defaultValue="outline">
<div className="flex items-center justify-between px-4 lg:px-6">
<Label className="sr-only" htmlFor="view-selector">
View
</Label>
<Select defaultValue="outline">
<SelectTrigger className="@4xl/main:hidden flex w-fit" id="view-selector">
<SelectValue placeholder="Select a view" />
</SelectTrigger>
<SelectContent>
<SelectItem value="outline">Outline</SelectItem>
<SelectItem value="past-performance">Past Performance</SelectItem>
<SelectItem value="key-personnel">Key Personnel</SelectItem>
<SelectItem value="focus-documents">Focus Documents</SelectItem>
</SelectContent>
</Select>
<TabsList className="@4xl/main:flex hidden">
<TabsTrigger value="outline">Outline</TabsTrigger>
<TabsTrigger className="gap-1" value="past-performance">
Past Performance{" "}
<Badge
className="flex size-5 items-center justify-center rounded-full bg-muted-foreground/30"
variant="secondary"
>
3
</Badge>
</TabsTrigger>
<TabsTrigger className="gap-1" value="key-personnel">
Key Personnel{" "}
<Badge
className="flex size-5 items-center justify-center rounded-full bg-muted-foreground/30"
variant="secondary"
>
2
</Badge>
</TabsTrigger>
<TabsTrigger value="focus-documents">Focus Documents</TabsTrigger>
</TabsList>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger>
<Button size="sm" variant="outline">
<ColumnsIcon />
<span className="hidden lg:inline">Customize Columns</span>
<span className="lg:hidden">Columns</span>
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{table
.getAllColumns()
.filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
checked={column.getIsVisible()}
className="capitalize"
key={column.id}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
<Button size="sm" variant="outline">
<PlusIcon />
<span className="hidden lg:inline">Add Section</span>
</Button>
</div>
</div>
<TabsContent
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
value="outline"
>
<div className="overflow-hidden rounded-lg border">
<DndContext
collisionDetection={closestCenter}
id={sortableId}
modifiers={[restrictToVerticalAxis]}
onDragEnd={handleDragEnd}
sensors={sensors}
>
<Table>
<TableHeader className="sticky top-0 z-10 bg-muted">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead colSpan={header.colSpan} key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody className="**:data-[slot=table-cell]:first:w-8">
{table.getRowModel().rows?.length ? (
<SortableContext items={dataIds} strategy={verticalListSortingStrategy}>
{table.getRowModel().rows.map((row) => (
<DraggableRow key={row.id} row={row} />
))}
</SortableContext>
) : (
<TableRow>
<TableCell className="h-24 text-center" colSpan={columns.length}>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</DndContext>
</div>
<div className="flex items-center justify-between px-4">
<div className="hidden flex-1 text-sm text-muted-foreground lg:flex">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex w-full items-center gap-8 lg:w-fit">
<div className="hidden items-center gap-2 lg:flex">
<Label className="text-sm font-medium" htmlFor="rows-per-page">
Rows per page
</Label>
<Select
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
value={`${table.getState().pagination.pageSize}`}
>
<SelectTrigger className="w-20" id="rows-per-page">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
</div>
<div className="ml-auto flex items-center gap-2 lg:ml-0">
<Button
className="hidden size-8 p-0 lg:flex"
disabled={!table.getCanPreviousPage()}
onClick={() => table.setPageIndex(0)}
variant="outline"
>
<span className="sr-only">Go to first page</span>
<ChevronsLeftIcon />
</Button>
<Button
className="size-8"
disabled={!table.getCanPreviousPage()}
onClick={() => table.previousPage()}
size="icon"
variant="outline"
>
<span className="sr-only">Go to previous page</span>
<ChevronLeftIcon />
</Button>
<Button
className="size-8"
disabled={!table.getCanNextPage()}
onClick={() => table.nextPage()}
size="icon"
variant="outline"
>
<span className="sr-only">Go to next page</span>
<ChevronRightIcon />
</Button>
<Button
className="hidden size-8 lg:flex"
disabled={!table.getCanNextPage()}
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
size="icon"
variant="outline"
>
<span className="sr-only">Go to last page</span>
<ChevronsRightIcon />
</Button>
</div>
</div>
</div>
</TabsContent>
<TabsContent className="flex flex-col px-4 lg:px-6" value="past-performance">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed" />
</TabsContent>
<TabsContent className="flex flex-col px-4 lg:px-6" value="key-personnel">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed" />
</TabsContent>
<TabsContent className="flex flex-col px-4 lg:px-6" value="focus-documents">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed" />
</TabsContent>
</Tabs>
);
}
const chartData = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "June", desktop: 214, mobile: 140 },
];
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--primary)",
},
mobile: {
label: "Mobile",
color: "var(--primary)",
},
} satisfies ChartConfig;
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
const isMobile = useIsMobile();
return (
<Sheet>
<SheetTrigger>
<Button className="w-fit px-0 text-left text-foreground" variant="link">
{item.header}
</Button>
</SheetTrigger>
<SheetContent className="flex flex-col" side="right">
<SheetHeader className="gap-1">
<SheetTitle>{item.header}</SheetTitle>
<SheetDescription>Showing total visitors for the last 6 months</SheetDescription>
</SheetHeader>
<div className="flex flex-1 flex-col gap-4 overflow-y-auto py-4 text-sm">
{!isMobile && (
<>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 0,
right: 10,
}}
>
<CartesianGrid vertical={false} />
<XAxis
axisLine={false}
dataKey="month"
hide
tickFormatter={(value) => value.slice(0, 3)}
tickLine={false}
tickMargin={8}
/>
<ChartTooltip content={<ChartTooltipContent indicator="dot" />} cursor={false} />
<Area
dataKey="mobile"
fill="var(--color-mobile)"
fillOpacity={0.6}
stackId="a"
stroke="var(--color-mobile)"
type="natural"
/>
<Area
dataKey="desktop"
fill="var(--color-desktop)"
fillOpacity={0.4}
stackId="a"
stroke="var(--color-desktop)"
type="natural"
/>
</AreaChart>
</ChartContainer>
<Separator />
<div className="grid gap-2">
<div className="flex gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUpIcon className="size-4" />
</div>
<div className="text-muted-foreground">
Showing total visitors for the last 6 months. This is just some random text to
test the layout. It spans multiple lines and should wrap around.
</div>
</div>
<Separator />
</>
)}
<form className="flex flex-col gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="header">Header</Label>
<Input defaultValue={item.header} id="header" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="type">Type</Label>
<Select defaultValue={item.type}>
<SelectTrigger className="w-full" id="type">
<SelectValue placeholder="Select a type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Table of Contents">Table of Contents</SelectItem>
<SelectItem value="Executive Summary">Executive Summary</SelectItem>
<SelectItem value="Technical Approach">Technical Approach</SelectItem>
<SelectItem value="Design">Design</SelectItem>
<SelectItem value="Capabilities">Capabilities</SelectItem>
<SelectItem value="Focus Documents">Focus Documents</SelectItem>
<SelectItem value="Narrative">Narrative</SelectItem>
<SelectItem value="Cover Page">Cover Page</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="status">Status</Label>
<Select defaultValue={item.status}>
<SelectTrigger className="w-full" id="status">
<SelectValue placeholder="Select a status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Done">Done</SelectItem>
<SelectItem value="In Progress">In Progress</SelectItem>
<SelectItem value="Not Started">Not Started</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="target">Target</Label>
<Input defaultValue={item.target} id="target" />
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="limit">Limit</Label>
<Input defaultValue={item.limit} id="limit" />
</div>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="reviewer">Reviewer</Label>
<Select defaultValue={item.reviewer}>
<SelectTrigger className="w-full" id="reviewer">
<SelectValue placeholder="Select a reviewer" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
<SelectItem value="Jamik Tashpulatov">Jamik Tashpulatov</SelectItem>
<SelectItem value="Emily Whalen">Emily Whalen</SelectItem>
</SelectContent>
</Select>
</div>
</form>
</div>
<SheetFooter className="mt-auto flex gap-2 sm:flex-col sm:space-x-0">
<Button className="w-full">Submit</Button>
<SheetClose>
<Button className="w-full" variant="outline">
Done
</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,95 @@
import {
Avatar,
AvatarFallback,
AvatarImage,
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@repo/shadcn-ui/components";
import {
CirclePlusIcon,
CreditCardIcon,
LogOutIcon,
SettingsIcon,
SquarePenIcon,
UserIcon,
UsersIcon,
} from "lucide-react";
import type { ReactNode } from "react";
type Props = {
trigger: ReactNode;
defaultOpen?: boolean;
align?: "start" | "center" | "end";
};
export const ProfileDropdown = ({ trigger, defaultOpen, align = "end" }: Props) => {
return (
<DropdownMenu defaultOpen={defaultOpen}>
<DropdownMenuTrigger>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent align={align || "end"} className="w-80">
<DropdownMenuGroup>
<DropdownMenuLabel className="flex items-center gap-4 px-4 py-2.5 font-normal">
<div className="relative">
<Avatar className="size-10">
<AvatarImage
alt="John Doe"
src="https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png"
/>
<AvatarFallback>JD</AvatarFallback>
</Avatar>
<span className="ring-card absolute right-0 bottom-0 block size-2 rounded-full bg-green-600 ring-2" />
</div>
<div className="flex flex-1 flex-col items-start">
<span className="text-foreground text-lg font-semibold">John Doe</span>
<span className="text-muted-foreground text-base">john.doe@example.com</span>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem className="px-4 py-2.5 text-base">
<UserIcon className="text-foreground size-5" />
<span>My account</span>
</DropdownMenuItem>
<DropdownMenuItem className="px-4 py-2.5 text-base">
<SettingsIcon className="text-foreground size-5" />
<span>Settings</span>
</DropdownMenuItem>
<DropdownMenuItem className="px-4 py-2.5 text-base">
<CreditCardIcon className="text-foreground size-5" />
<span>Billing</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem className="px-4 py-2.5 text-base">
<UsersIcon className="text-foreground size-5" />
<span>Manage team</span>
</DropdownMenuItem>
<DropdownMenuItem className="px-4 py-2.5 text-base">
<SquarePenIcon className="text-foreground size-5" />
<span>Customization</span>
</DropdownMenuItem>
<DropdownMenuItem className="px-4 py-2.5 text-base">
<CirclePlusIcon className="text-foreground size-5" />
<span>Add team account</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem className="px-4 py-2.5 text-base" variant="destructive">
<LogOutIcon className="size-5" />
<span>Logout</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@ -1,4 +1,3 @@
export * from "./app-breadcrumb.tsx";
export * from "./app-content.tsx";
export * from "./app-header.tsx";
export * from "./app-layout.tsx";

View File

@ -1,76 +0,0 @@
"use client";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@repo/shadcn-ui/components/dropdown-menu";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@repo/shadcn-ui/components/sidebar";
import { FolderIcon, type LucideIcon, MoreHorizontalIcon, ShareIcon } from "lucide-react";
export function NavDocuments({
items,
}: {
items: {
name: string;
url: string;
icon: LucideIcon;
}[];
}) {
const { isMobile } = useSidebar();
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Documents</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger>
<SidebarMenuAction className="rounded-sm data-[state=open]:bg-accent" showOnHover>
<MoreHorizontalIcon />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
align={isMobile ? "end" : "start"}
className="w-24 rounded-lg"
side={isMobile ? "bottom" : "right"}
>
<DropdownMenuItem>
<FolderIcon />
<span>Open</span>
</DropdownMenuItem>
<DropdownMenuItem>
<ShareIcon />
<span>Share</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontalIcon className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
}

View File

@ -3,7 +3,7 @@ import {
CollapsibleContent,
CollapsibleTrigger,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
@ -11,66 +11,53 @@ import {
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@repo/shadcn-ui/components";
import { ChevronRightIcon, type LucideIcon, PlusCircleIcon } from "lucide-react";
import { useNavigate } from "react-router";
import { ChevronRightIcon, type LucideIcon } from "lucide-react";
type NavMainItem = {
title: string;
url?: string;
icon?: LucideIcon;
isActive?: boolean;
items?: {
export function NavMain({
items,
}: {
items: {
title: string;
url: string;
url?: string;
icon?: LucideIcon;
isActive?: boolean;
items?: {
title: string;
url: string;
}[];
}[];
};
export function NavMain({ items }: { items: NavMainItem[] }) {
const navigate = useNavigate();
}) {
return (
<SidebarGroup>
<SidebarGroupContent className="flex flex-col gap-2">
<SidebarMenu>
<SidebarMenuItem className="flex items-center gap-2">
<SidebarMenuButton
className="hidden min-w-8 bg-primary text-primary-foreground duration-200 ease-linear hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground"
tooltip="Quick Create"
>
<PlusCircleIcon />
<span>Quick Create</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
<SidebarMenu>
{items.map((item) => (
<Collapsible asChild className="group/collapsible" defaultOpen={true} key={item.title}>
<SidebarMenuItem className="mb-6">
<CollapsibleTrigger>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span className="font-semibold">{item.title}</span>
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton>
<a href={subItem.url}>
<SidebarGroupLabel>Módulos</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible className="group/collapsible" defaultOpen={item.isActive} key={item.title}>
<SidebarMenuItem>
<CollapsibleTrigger render={<SidebarMenuButton tooltip={item.title} />}>
{item.icon && <item.icon />}
<span>{item.title}</span>
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
render={
<a href={subItem.url} title={subItem.title}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroupContent>
}
/>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
);
}

View File

@ -1,82 +0,0 @@
"use client";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@repo/shadcn-ui/components/dropdown-menu";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@repo/shadcn-ui/components/sidebar";
import { Folder, Forward, type LucideIcon, MoreHorizontal, Trash2 } from "lucide-react";
export function NavProjects({
projects,
}: {
projects: {
name: string;
url: string;
icon: LucideIcon;
}[];
}) {
const { isMobile } = useSidebar();
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
align={isMobile ? "end" : "start"}
className="w-48 rounded-lg"
side={isMobile ? "bottom" : "right"}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward className="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontal className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
}

View File

@ -16,13 +16,7 @@ import {
SidebarMenuItem,
useSidebar,
} from "@repo/shadcn-ui/components/sidebar";
import {
BellIcon,
CreditCardIcon,
LogOutIcon,
MoreVerticalIcon,
UserCircleIcon,
} from "lucide-react";
import { BadgeCheck, Bell, ChevronsUpDownIcon, CreditCard, LogOut, Sparkles } from "lucide-react";
export function NavUser({
user,
@ -34,63 +28,72 @@ export function NavUser({
};
}) {
const { isMobile } = useSidebar();
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger>
<SidebarMenuButton
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
size="lg"
>
<Avatar className="size-8 rounded-lg grayscale">
<AvatarImage alt={user.name} src={user.avatar} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs text-muted-foreground">{user.email}</span>
</div>
<MoreVerticalIcon className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="size-8 rounded-lg">
<DropdownMenuTrigger
render={
<SidebarMenuButton
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
size="lg"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage alt={user.name} src={user.avatar} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs text-muted-foreground">{user.email}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<ChevronsUpDownIcon className="ml-auto size-4" />
</SidebarMenuButton>
}
/>
<DropdownMenuContent
align="end"
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuGroup>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage alt={user.name} src={user.avatar} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<UserCircleIcon />
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCardIcon />
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<BellIcon />
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOutIcon />
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>

View File

@ -1,38 +0,0 @@
import {
Button,
Label,
SidebarGroup,
SidebarGroupContent,
SidebarInput,
SidebarMenu,
SidebarMenuItem,
} from "@repo/shadcn-ui/components";
import { BinocularsIcon, SearchIcon } from "lucide-react";
export function SearchForm({ ...props }: React.ComponentProps<"form">) {
return (
<form {...props}>
<SidebarGroup className="py-0">
<SidebarGroupContent className="relative">
<SidebarMenu>
<SidebarMenuItem className="flex items-center gap-2">
<Label className="sr-only" htmlFor="search">
Searchsss
</Label>
<SidebarInput className="pl-8 h-9" id="search" placeholder="Search..." />
<SearchIcon className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none " />
<Button
className="h-9 w-9 shrink-0 group-data-[collapsible=icon]:opacity-0"
size="icon"
variant="outline"
>
<BinocularsIcon />
<span className="sr-only">Advanced search</span>
</Button>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</form>
);
}

View File

@ -1,97 +0,0 @@
import { TrendingDownIcon, TrendingUpIcon } from "lucide-react";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@repo/shadcn-ui/components";
import { Badge } from "@repo/shadcn-ui/components/badge";
export function SectionCards() {
return (
<div className='*:data-[slot=card]:shadow-xs @xl/main:grid-cols-2 @5xl/main:grid-cols-4 grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card lg:px-6'>
<Card className='@container/card'>
<CardHeader className='relative'>
<CardDescription>Total Revenue</CardDescription>
<CardTitle className='@[250px]/card:text-3xl text-2xl font-semibold tabular-nums'>
$1,250.00
</CardTitle>
<div className='absolute right-4 top-4'>
<Badge variant='outline' className='flex gap-1 rounded-lg text-xs'>
<TrendingUpIcon className='size-3' />
+12.5%
</Badge>
</div>
</CardHeader>
<CardFooter className='flex-col items-start gap-1 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Trending up this month <TrendingUpIcon className='size-4' />
</div>
<div className='text-muted-foreground'>Visitors for the last 6 months</div>
</CardFooter>
</Card>
<Card className='@container/card'>
<CardHeader className='relative'>
<CardDescription>New Customers</CardDescription>
<CardTitle className='@[250px]/card:text-3xl text-2xl font-semibold tabular-nums'>
1,234
</CardTitle>
<div className='absolute right-4 top-4'>
<Badge variant='outline' className='flex gap-1 rounded-lg text-xs'>
<TrendingDownIcon className='size-3' />
-20%
</Badge>
</div>
</CardHeader>
<CardFooter className='flex-col items-start gap-1 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Down 20% this period <TrendingDownIcon className='size-4' />
</div>
<div className='text-muted-foreground'>Acquisition needs attention</div>
</CardFooter>
</Card>
<Card className='@container/card'>
<CardHeader className='relative'>
<CardDescription>Active Accounts</CardDescription>
<CardTitle className='@[250px]/card:text-3xl text-2xl font-semibold tabular-nums'>
45,678
</CardTitle>
<div className='absolute right-4 top-4'>
<Badge variant='outline' className='flex gap-1 rounded-lg text-xs'>
<TrendingUpIcon className='size-3' />
+12.5%
</Badge>
</div>
</CardHeader>
<CardFooter className='flex-col items-start gap-1 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Strong user retention <TrendingUpIcon className='size-4' />
</div>
<div className='text-muted-foreground'>Engagement exceed targets</div>
</CardFooter>
</Card>
<Card className='@container/card'>
<CardHeader className='relative'>
<CardDescription>Growth Rate</CardDescription>
<CardTitle className='@[250px]/card:text-3xl text-2xl font-semibold tabular-nums'>
4.5%
</CardTitle>
<div className='absolute right-4 top-4'>
<Badge variant='outline' className='flex gap-1 rounded-lg text-xs'>
<TrendingUpIcon className='size-3' />
+4.5%
</Badge>
</div>
</CardHeader>
<CardFooter className='flex-col items-start gap-1 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Steady performance <TrendingUpIcon className='size-4' />
</div>
<div className='text-muted-foreground'>Meets growth projections</div>
</CardFooter>
</Card>
</div>
);
}

View File

@ -1,237 +0,0 @@
import { Button, Separator, SidebarTrigger } from "@repo/shadcn-ui/components";
import { Link } from "react-router-dom";
export function SiteHeader() {
return (
<header className="bg-background sticky top-0 z-50 flex h-(--header-height) shrink-0 items-center gap-2 border-b backdrop-blur-md transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height) md:rounded-tl-xl md:rounded-tr-xl">
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
<SidebarTrigger className="-ml-1" />
<Separator className="mx-2 data-[orientation=vertical]:h-4" orientation="vertical" />
<div className="lg:flex-1">
<div className="relative hidden max-w-sm flex-1 lg:block">
<svg
aria-hidden="true"
className="lucide lucide-search text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="m21 21-4.34-4.34" />
<circle cx="11" cy="11" r="8" />
</svg>
<input
className="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input min-w-0 bg-transparent px-3 py-1 transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-9 w-full cursor-pointer rounded-md border pr-4 pl-10 text-sm shadow-xs"
data-slot="input"
placeholder="Search..."
type="search"
/>
<div className="absolute top-1/2 right-2 hidden -translate-y-1/2 items-center gap-0.5 rounded-sm bg-zinc-200 p-1 font-mono text-xs font-medium sm:flex dark:bg-neutral-700">
<svg
aria-hidden="true"
className="lucide lucide-command size-3"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
</svg>
<span>k</span>
</div>
</div>
<div className="block lg:hidden">
<button
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg:not([class*='size-'])]:size-4 shrink-0 [&amp;_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-9"
data-size="icon"
data-slot="button"
data-variant="ghost"
type="button"
>
<svg
aria-hidden="true"
className="lucide lucide-search"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="m21 21-4.34-4.34" />
<circle cx="11" cy="11" r="8" />
</svg>
</button>
</div>
<div
className="flex flex-col gap-2 text-center sm:text-left sr-only"
data-slot="dialog-header"
>
<h2
className="text-lg leading-none font-semibold"
data-slot="dialog-title"
id="radix-_R_rd5ubplbH1_"
>
Command Palette
</h2>
<p
className="text-muted-foreground text-sm"
data-slot="dialog-description"
id="radix-_R_rd5ubplbH2_"
>
Search for a command to run...
</p>
</div>
</div>
<div className="ml-auto flex items-center gap-2">
<Button asChild className="hidden sm:flex" size="sm">
<Link
className="dark:text-foreground"
rel="noopener noreferrer"
target="_blank"
to="https://shadcnuikit.com/"
>
Get Pro
</Link>
</Button>
</div>
<div className="ml-auto flex items-center gap-2">
<a
className="inline-flex items-center justify-center whitespace-nowrap text-sm transition-all disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg:not([class*='size-'])]:size-4 shrink-0 [&amp;_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive underline-offset-4 hover:underline h-8 rounded-md gap-1.5 px-3 has-[&gt;svg]:px-2.5 relative animate-pulse bg-linear-to-r from-violet-600 via-fuchsia-600 to-cyan-600 bg-clip-text font-medium text-transparent hover:bg-transparent"
data-size="sm"
data-slot="button"
data-variant="link"
href="https://shadcnuikit.com/pricing"
rel="noopener"
target="_blank"
>
Get Pro
</a>
<button
aria-expanded="false"
aria-haspopup="menu"
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg:not([class*='size-'])]:size-4 shrink-0 [&amp;_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-8 relative"
data-size="icon-sm"
data-slot="dropdown-menu-trigger"
data-state="closed"
data-variant="ghost"
id="radix-_R_kd5ubplb_"
type="button"
>
<svg
aria-hidden="true"
className="lucide lucide-bell"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10.268 21a2 2 0 0 0 3.464 0" />
<path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" />
</svg>
<span className="bg-destructive absolute end-0.5 top-0.5 block size-1.5 shrink-0 rounded-full" />
</button>
<button
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg:not([class*='size-'])]:size-4 shrink-0 [&amp;_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-8 relative"
data-size="icon-sm"
data-slot="button"
data-variant="ghost"
type="button"
>
<svg
aria-hidden="true"
className="lucide lucide-moon"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
<span className="sr-only">Toggle theme</span>
</button>
<button
aria-expanded="false"
aria-haspopup="menu"
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg:not([class*='size-'])]:size-4 shrink-0 [&amp;_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-8"
data-size="icon-sm"
data-slot="dropdown-menu-trigger"
data-state="closed"
data-variant="ghost"
id="radix-_R_14d5ubplb_"
type="button"
>
<svg
aria-hidden="true"
className="lucide lucide-palette"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" />
<circle cx="13.5" cy="6.5" fill="currentColor" r=".5" />
<circle cx="17.5" cy="10.5" fill="currentColor" r=".5" />
<circle cx="6.5" cy="12.5" fill="currentColor" r=".5" />
<circle cx="8.5" cy="7.5" fill="currentColor" r=".5" />
</svg>
</button>
<div
className="bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px mx-2 data-[orientation=vertical]:h-4"
data-orientation="vertical"
data-slot="separator"
role="none"
/>
<button
aria-expanded="false"
aria-haspopup="menu"
className="group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6"
data-size="default"
data-slot="dropdown-menu-trigger"
data-state="closed"
id="radix-_R_1kd5ubplb_"
type="button"
>
<img
alt="shadcn ui kit"
className="aspect-square size-full object-cover"
data-slot="avatar-image"
src="/images/avatars/01.png"
/>
</button>
</div>
</div>
</header>
);
}

View File

@ -3,6 +3,7 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
@ -36,48 +37,53 @@ export function TeamSwitcher({
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger>
<SidebarMenuButton
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
size="lg"
>
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{activeTeam.name}</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuTrigger
render={
<SidebarMenuButton
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
size="lg"
>
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{activeTeam.name}</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
}
/>
<DropdownMenuContent
align="start"
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
className="gap-2 p-2"
key={team.name}
onClick={() => setActiveTeam(team)}
>
<div className="flex size-6 items-center justify-center rounded-md border">
<team.logo className="size-3.5 shrink-0" />
<DropdownMenuGroup>
<DropdownMenuLabel className="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
className="gap-2 p-2"
key={team.name}
onClick={() => setActiveTeam(team)}
>
<div className="flex size-6 items-center justify-center rounded-md border">
<team.logo className="size-3.5 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
<Plus className="size-4" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
<div className="text-muted-foreground font-medium">Add team</div>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
<Plus className="size-4" />
</div>
<div className="text-muted-foreground font-medium">Add team</div>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>