diff --git a/app/(dashboard)/devices/page.tsx b/app/(dashboard)/devices/page.tsx
index 8874727..b29e715 100644
--- a/app/(dashboard)/devices/page.tsx
+++ b/app/(dashboard)/devices/page.tsx
@@ -1,5 +1,6 @@
import { authOptions } from "@/app/auth";
import { DevicesTable } from "@/components/devices-table";
+import DeviceFilter from "@/components/devices/device-filter";
import Search from "@/components/search";
import AddDeviceDialogForm from "@/components/user/add-device-dialog";
import { getServerSession } from "next-auth";
@@ -27,9 +28,9 @@ export default async function Devices({
-
+
}>
diff --git a/app/globals.css b/app/globals.css
index 01b8e1e..37c34a7 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -2,31 +2,27 @@
@tailwind components;
@tailwind utilities;
-body {
- font-family: Arial, Helvetica, sans-serif;
-}
-
@layer base {
:root {
--background: 0 0% 100%;
- --foreground: 240 10% 3.9%;
+ --foreground: 0 0% 3.9%;
--card: 0 0% 100%;
- --card-foreground: 240 10% 3.9%;
+ --card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
- --popover-foreground: 240 10% 3.9%;
- --primary: 240 5.9% 10%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
- --secondary: 240 4.8% 95.9%;
- --secondary-foreground: 240 5.9% 10%;
- --muted: 240 4.8% 95.9%;
- --muted-foreground: 240 3.8% 46.1%;
- --accent: 240 4.8% 95.9%;
- --accent-foreground: 240 5.9% 10%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
- --border: 240 5.9% 90%;
- --input: 240 5.9% 90%;
- --ring: 240 10% 3.9%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
@@ -43,25 +39,25 @@ body {
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
- --background: 240 10% 3.9%;
+ --background: 0 0% 3.9%;
--foreground: 0 0% 98%;
- --card: 240 10% 3.9%;
+ --card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
- --popover: 240 10% 3.9%;
+ --popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
- --primary-foreground: 240 5.9% 10%;
- --secondary: 240 3.7% 15.9%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
- --muted: 240 3.7% 15.9%;
- --muted-foreground: 240 5% 64.9%;
- --accent: 240 3.7% 15.9%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
- --border: 240 3.7% 15.9%;
- --input: 240 3.7% 15.9%;
- --ring: 240 4.9% 83.9%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
diff --git a/components.json b/components.json
index a312865..dea737b 100644
--- a/components.json
+++ b/components.json
@@ -6,7 +6,7 @@
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
- "baseColor": "zinc",
+ "baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
diff --git a/components/auth/application-layout.tsx b/components/auth/application-layout.tsx
index 82e97e8..573ccfc 100644
--- a/components/auth/application-layout.tsx
+++ b/components/auth/application-layout.tsx
@@ -1,5 +1,6 @@
import { DeviceCartDrawer } from "@/components/device-cart";
import { Wallet } from "@/components/wallet";
+import { NuqsAdapter } from 'nuqs/adapters/next/app'
import { ModeToggle } from "@/components/theme-toggle";
import { AppSidebar } from "@/components/ui/app-sidebar";
@@ -15,8 +16,8 @@ import {
import { tryCatch } from "@/utils/tryCatch";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
-import { AccountPopover } from "./account-popver";
import { WelcomeBanner } from "../welcome-banner";
+import { AccountPopover } from "./account-popver";
export async function ApplicationLayout({
children,
@@ -51,7 +52,9 @@ export async function ApplicationLayout({
/>
- {children}
+
+ {children}
+
diff --git a/components/devices-table.tsx b/components/devices-table.tsx
index 410b5d4..123c113 100644
--- a/components/devices-table.tsx
+++ b/components/devices-table.tsx
@@ -23,21 +23,30 @@ export async function DevicesTable({
parentalControl,
}: {
searchParams: Promise<{
- query: string;
- page: number;
+ [key: string]: unknown;
}>;
parentalControl?: boolean;
}) {
+ const resolvedParams = await searchParams;
const session = await getServerSession(authOptions);
const isAdmin = session?.user?.is_superuser;
- const query = (await searchParams)?.query || "";
- const page = (await searchParams)?.page || 1;
- const limit = 10; // Items per page
- const offset = (page - 1) * limit; // Calculate offset based on page
+ const page = Number.parseInt(resolvedParams.page as string) || 1;
+ const limit = 10;
+ const offset = (page - 1) * limit;
+
+ // Build params object for getDevices
+ const apiParams: Record = {};
+ for (const [key, value] of Object.entries(resolvedParams)) {
+ if (value !== undefined && value !== "") {
+ apiParams[key] = typeof value === "number" ? value : String(value);
+ }
+ }
+ apiParams.limit = limit;
+ apiParams.offset = offset;
const [error, devices] = await tryCatch(
- getDevices({ query: query, limit: limit, offset: offset }),
+ getDevices(apiParams),
);
if (error) {
if (error.message === "UNAUTHORIZED") {
@@ -50,8 +59,8 @@ export async function DevicesTable({
return (
{data?.length === 0 ? (
-
-
No devices yet.
+
+
No devices.
) : (
<>
@@ -78,17 +87,17 @@ export async function DevicesTable({
-
- {query?.length > 0 && (
-
- Showing {meta?.total} devices for "{query}
- "
+
+ {meta?.total === 1 ? (
+
+ Total {meta?.total} device.
+
+ ) : (
+
+ Total {meta?.total} devices.
)}
-
- {meta?.total} devices
-
@@ -107,7 +116,8 @@ export async function DevicesTable({
currentPage={meta?.current_page}
/>
>
- )}
-
+ )
+ }
+
);
}
diff --git a/components/devices/device-filter.tsx b/components/devices/device-filter.tsx
new file mode 100644
index 0000000..df34188
--- /dev/null
+++ b/components/devices/device-filter.tsx
@@ -0,0 +1,183 @@
+"use client"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button";
+import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer";
+import { Input } from "@/components/ui/input";
+import { cn } from "@/lib/utils";
+import { ListFilter, Loader2, X } from "lucide-react";
+import { usePathname, useRouter, useSearchParams } from "next/navigation";
+import { useQueryState } from "nuqs";
+import { useState, useTransition } from 'react';
+export default function DeviceFilter() {
+ const { replace } = useRouter();
+
+ const [isOpen, setIsOpen] = useState(false);
+ const [disabled, startTransition] = useTransition();
+ const searchParams = useSearchParams();
+ const pathname = usePathname();
+ const params = new URLSearchParams(searchParams.toString());
+
+
+ const [urlInputName] = useQueryState("name", {
+ clearOnDefault: true
+ })
+ const [urlInputMac] = useQueryState("mac", {
+ clearOnDefault: true
+ })
+ const [urlInputVendor] = useQueryState("vendor", {
+ clearOnDefault: true
+ })
+
+ // Local state for input fields
+ const [inputName, setInputName] = useState(urlInputName ?? "");
+ const [inputMac, setInputMac] = useState(urlInputMac ?? "");
+ const [inputVendor, setInputVendor] = useState(urlInputVendor ?? "");
+
+ // Map filter keys to their state setters
+ const filterSetters: Record>> = {
+ name: setInputName,
+ mac: setInputMac,
+ vendor: setInputVendor,
+ };
+ function handleSearch({ name, mac, vendor }: { name: string; mac: string; vendor: string }) {
+
+ if (name) params.set("name", name); else params.delete("name");
+ if (mac) params.set("mac", mac); else params.delete("mac");
+ if (vendor) params.set("vendor", vendor); else params.delete("vendor");
+ params.set("page", "1");
+
+ startTransition(() => {
+ replace(`${pathname}?${params.toString()}`);
+ });
+ }
+
+ const appliedFilters = searchParams
+ .toString()
+ .split("&")
+ .filter((filter) => !filter.startsWith("page=") && filter)
+ return (
+
+
+
+
+
+
+
+
+
+
+ {appliedFilters.map((filter) => (
+
+ {prettyPrintFilter(filter)}
+ {
+ disabled ? (
+
+ ) : (
+ {
+ const key = filter.split("=")[0];
+ params.delete(key);
+
+ // Use the mapping to clear the correct input state
+ filterSetters[key]?.("");
+
+ startTransition(() => {
+ replace(`${pathname}?${params.toString()}`);
+ });
+ }}
+ >
+ Remove
+
+ )
+ }
+
+ ))}
+
+
+ );
+}
+
+
+function prettyPrintFilter(filter: string) {
+ const [key, value] = filter.split("=");
+ switch (key) {
+ case "name":
+ return Device Name: {value}
;
+ case "mac":
+ return MAC Address: {value}
;
+ case "vendor":
+ return Vendor: {value}
;
+ default:
+ return filter;
+ }
+}
+
diff --git a/components/search.tsx b/components/search.tsx
index 21c7a66..e0caa5f 100644
--- a/components/search.tsx
+++ b/components/search.tsx
@@ -34,7 +34,7 @@ export default function Search({ disabled }: { disabled?: boolean }) {
ref={inputRef}
placeholder="Search..."
type="search"
- className={cn("w-fit", isPending && "animate-pulse")}
+ className={cn("sm:w-fit", isPending && "animate-pulse")}
name="search"
id="search"
defaultValue={searchQuery ? searchQuery : ""}
diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx
index 4a8cca4..2f55a32 100644
--- a/components/ui/accordion.tsx
+++ b/components/ui/accordion.tsx
@@ -2,65 +2,56 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
-import { ChevronDownIcon } from "lucide-react"
+import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
-function Accordion({
- ...props
-}: React.ComponentProps) {
- return
-}
+const Accordion = AccordionPrimitive.Root
-function AccordionItem({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
-function AccordionTrigger({
- className,
- children,
- ...props
-}: React.ComponentProps) {
- return (
-
- svg]:rotate-180",
- className
- )}
- {...props}
- >
- {children}
-
-
-
- )
-}
-
-function AccordionContent({
- className,
- children,
- ...props
-}: React.ComponentProps) {
- return (
- ,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
{...props}
>
- {children}
-
- )
-}
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx
new file mode 100644
index 0000000..5afd41d
--- /dev/null
+++ b/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..d6a5226
--- /dev/null
+++ b/components/ui/aspect-ratio.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 0000000..51e507b
--- /dev/null
+++ b/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
index e87d62b..b21ec24 100644
--- a/components/ui/badge.tsx
+++ b/components/ui/badge.tsx
@@ -1,5 +1,5 @@
+import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
@@ -25,7 +25,7 @@ const badgeVariants = cva(
export interface BadgeProps
extends React.HTMLAttributes,
- VariantProps {}
+ VariantProps { }
function Badge({ className, variant, ...props }: BadgeProps) {
return (
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
index 65d4fcd..12ae757 100644
--- a/components/ui/button.tsx
+++ b/components/ui/button.tsx
@@ -1,30 +1,31 @@
-import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
+import { type VariantProps, cva } from "class-variance-authority"
+import * as React from "react"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ "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 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_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",
{
variants: {
variant: {
default:
- "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
- "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
- "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
- "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
- ghost: "hover:bg-accent hover:text-accent-foreground",
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
- default: "h-9 px-4 py-2",
- sm: "h-8 rounded-md px-3 text-xs",
- lg: "h-10 rounded-md px-8",
- icon: "h-9 w-9",
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
},
},
defaultVariants: {
@@ -34,24 +35,25 @@ const buttonVariants = cva(
}
)
-export interface ButtonProps
- extends React.ButtonHTMLAttributes,
- VariantProps {
- asChild?: boolean
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
}
-const Button = React.forwardRef(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button"
- return (
-
- )
- }
-)
-Button.displayName = "Button"
-
export { Button, buttonVariants }
diff --git a/components/ui/calendar.tsx b/components/ui/calendar.tsx
index b009680..22d7b57 100644
--- a/components/ui/calendar.tsx
+++ b/components/ui/calendar.tsx
@@ -1,72 +1,210 @@
-"use client";
+"use client"
-import { ChevronLeft, ChevronRight } from "lucide-react";
-import { DayPicker } from "react-day-picker";
+import * as React from "react"
+import {
+ ChevronDownIcon,
+ ChevronLeftIcon,
+ ChevronRightIcon,
+} from "lucide-react"
+import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
-import { buttonVariants } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-
-export type CalendarProps = React.ComponentProps;
+import { cn } from "@/lib/utils"
+import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- ...props
-}: CalendarProps) {
- return (
- .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
- : "[&:has([aria-selected])]:rounded-md",
- ),
- day: cn(
- buttonVariants({ variant: "ghost" }),
- "h-8 w-8 p-0 font-normal aria-selected:opacity-100",
- ),
- day_range_start: "day-range-start",
- day_range_end: "day-range-end",
- day_selected:
- "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
- day_today: "bg-accent text-accent-foreground",
- day_outside:
- "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
- day_disabled: "text-muted-foreground opacity-50",
- day_range_middle:
- "aria-selected:bg-accent aria-selected:text-accent-foreground",
- day_hidden: "invisible",
- ...classNames,
- }}
- components={{
- // @ts-expect-error this works but types are not correct
- IconLeft: () => ,
- IconRight: () => ,
- }}
- {...props}
- />
- );
-}
-Calendar.displayName = "Calendar";
+ className,
+ classNames,
+ showOutsideDays = true,
+ captionLayout = "label",
+ buttonVariant = "ghost",
+ formatters,
+ components,
+ ...props
+}: React.ComponentProps & {
+ buttonVariant?: React.ComponentProps["variant"]
+}) {
+ const defaultClassNames = getDefaultClassNames()
-export { Calendar };
+ return (
+ svg]:rotate-180`,
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+ className
+ )}
+ captionLayout={captionLayout}
+ formatters={{
+ formatMonthDropdown: (date) =>
+ date.toLocaleString("default", { month: "short" }),
+ ...formatters,
+ }}
+ classNames={{
+ root: cn("w-fit", defaultClassNames.root),
+ months: cn(
+ "relative flex flex-col gap-4 md:flex-row",
+ defaultClassNames.months
+ ),
+ month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
+ nav: cn(
+ "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
+ defaultClassNames.nav
+ ),
+ button_previous: cn(
+ buttonVariants({ variant: buttonVariant }),
+ "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
+ defaultClassNames.button_previous
+ ),
+ button_next: cn(
+ buttonVariants({ variant: buttonVariant }),
+ "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
+ defaultClassNames.button_next
+ ),
+ month_caption: cn(
+ "flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
+ defaultClassNames.month_caption
+ ),
+ dropdowns: cn(
+ "flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
+ defaultClassNames.dropdowns
+ ),
+ dropdown_root: cn(
+ "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
+ defaultClassNames.dropdown_root
+ ),
+ dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
+ caption_label: cn(
+ "select-none font-medium",
+ captionLayout === "label"
+ ? "text-sm"
+ : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
+ defaultClassNames.caption_label
+ ),
+ table: "w-full border-collapse",
+ weekdays: cn("flex", defaultClassNames.weekdays),
+ weekday: cn(
+ "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
+ defaultClassNames.weekday
+ ),
+ week: cn("mt-2 flex w-full", defaultClassNames.week),
+ week_number_header: cn(
+ "w-[--cell-size] select-none",
+ defaultClassNames.week_number_header
+ ),
+ week_number: cn(
+ "text-muted-foreground select-none text-[0.8rem]",
+ defaultClassNames.week_number
+ ),
+ day: cn(
+ "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
+ defaultClassNames.day
+ ),
+ range_start: cn(
+ "bg-accent rounded-l-md",
+ defaultClassNames.range_start
+ ),
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
+ range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
+ today: cn(
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
+ defaultClassNames.today
+ ),
+ outside: cn(
+ "text-muted-foreground aria-selected:text-muted-foreground",
+ defaultClassNames.outside
+ ),
+ disabled: cn(
+ "text-muted-foreground opacity-50",
+ defaultClassNames.disabled
+ ),
+ hidden: cn("invisible", defaultClassNames.hidden),
+ ...classNames,
+ }}
+ components={{
+ Root: ({ className, rootRef, ...props }) => {
+ return (
+
+ )
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === "left") {
+ return (
+
+ )
+ }
+
+ if (orientation === "right") {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+ },
+ DayButton: CalendarDayButton,
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+
+ {children}
+
+ |
+ )
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ )
+}
+
+function CalendarDayButton({
+ className,
+ day,
+ modifiers,
+ ...props
+}: React.ComponentProps) {
+ const defaultClassNames = getDefaultClassNames()
+
+ const ref = React.useRef(null)
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus()
+ }, [modifiers.focused])
+
+ return (
+