Improve dashboard sidebar layout
This commit is contained in:
@@ -36,7 +36,6 @@
|
|||||||
"@dnd-kit/modifiers": "9.0.0",
|
"@dnd-kit/modifiers": "9.0.0",
|
||||||
"@dnd-kit/sortable": "10.0.0",
|
"@dnd-kit/sortable": "10.0.0",
|
||||||
"@dnd-kit/utilities": "3.2.2",
|
"@dnd-kit/utilities": "3.2.2",
|
||||||
"@fontsource-variable/playfair-display": "5.2.8",
|
|
||||||
"@radix-ui/react-alert-dialog": "1.1.16",
|
"@radix-ui/react-alert-dialog": "1.1.16",
|
||||||
"@radix-ui/react-avatar": "1.1.12",
|
"@radix-ui/react-avatar": "1.1.12",
|
||||||
"@radix-ui/react-checkbox": "1.3.4",
|
"@radix-ui/react-checkbox": "1.3.4",
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ export function ActiveTimerWidget({
|
|||||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||||
if (running) {
|
if (running) {
|
||||||
const tick = () =>
|
const tick = () =>
|
||||||
setElapsed(Math.floor((Date.now() - new Date(running.startedAt).getTime()) / 1000));
|
setElapsed(
|
||||||
|
Math.floor(
|
||||||
|
(Date.now() - new Date(running.startedAt).getTime()) / 1000,
|
||||||
|
),
|
||||||
|
);
|
||||||
tick();
|
tick();
|
||||||
intervalRef.current = setInterval(tick, 1000);
|
intervalRef.current = setInterval(tick, 1000);
|
||||||
}
|
}
|
||||||
@@ -73,7 +77,10 @@ export function ActiveTimerWidget({
|
|||||||
window.location.assign(`/dashboard/invoices/${data.invoice!.id}`),
|
window.location.assign(`/dashboard/invoices/${data.invoice!.id}`),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (data.outcome === "saved_no_invoice" || data.outcome === "saved_no_client") {
|
} else if (
|
||||||
|
data.outcome === "saved_no_invoice" ||
|
||||||
|
data.outcome === "saved_no_client"
|
||||||
|
) {
|
||||||
toast.warning("Time saved", { description: message });
|
toast.warning("Time saved", { description: message });
|
||||||
} else {
|
} else {
|
||||||
toast.success(message);
|
toast.success(message);
|
||||||
@@ -96,12 +103,19 @@ export function ActiveTimerWidget({
|
|||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
aria-label={compact ? "Stop timer" : undefined}
|
||||||
onClick={() => clockOut.mutate({})}
|
onClick={() => clockOut.mutate({})}
|
||||||
disabled={clockOut.isPending}
|
disabled={clockOut.isPending}
|
||||||
className={cn(compact && "h-8 px-2", className)}
|
className={cn(compact && "size-8 p-0", className)}
|
||||||
>
|
>
|
||||||
<Square className={cn("h-3.5 w-3.5", !compact && "mr-1.5")} />
|
<Square data-icon={compact ? undefined : "inline-start"} />
|
||||||
{!compact && (clockOut.isPending ? "Stopping…" : "Stop")}
|
{compact ? (
|
||||||
|
<span className="sr-only">Stop timer</span>
|
||||||
|
) : clockOut.isPending ? (
|
||||||
|
"Stopping…"
|
||||||
|
) : (
|
||||||
|
"Stop"
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -133,18 +147,18 @@ export function ActiveTimerWidget({
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/time-clock"
|
href="/dashboard/time-clock"
|
||||||
className="border-primary/30 bg-primary/5 relative flex h-10 w-10 items-center justify-center rounded-md border transition-colors hover:bg-primary/10"
|
className="border-primary/30 bg-primary/5 hover:bg-primary/10 relative flex size-10 items-center justify-center rounded-xl border transition-colors"
|
||||||
>
|
>
|
||||||
<Clock className="text-primary h-5 w-5" />
|
<Clock className="text-primary size-5" />
|
||||||
<span className="absolute top-1 right-1 flex h-2 w-2">
|
<span className="absolute top-1 right-1 flex size-2">
|
||||||
<span className="bg-primary absolute inline-flex h-full w-full animate-ping rounded-full opacity-75" />
|
<span className="bg-primary absolute inline-flex size-full animate-ping rounded-full opacity-75" />
|
||||||
<span className="bg-primary relative inline-flex h-2 w-2 rounded-full" />
|
<span className="bg-primary relative inline-flex size-2 rounded-full" />
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
side="right"
|
side="right"
|
||||||
className="bg-popover text-popover-foreground border-border max-w-56 space-y-2 border p-3 text-sm [&>svg]:bg-popover [&>svg]:fill-popover"
|
className="bg-popover text-popover-foreground border-border [&>svg]:bg-popover [&>svg]:fill-popover max-w-56 space-y-2 border p-3 text-sm"
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium">
|
<p className="text-sm font-medium">
|
||||||
{description}
|
{description}
|
||||||
@@ -169,10 +183,17 @@ export function ActiveTimerWidget({
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground text-xs">No invoice selected</p>
|
<p className="text-muted-foreground text-xs">
|
||||||
|
No invoice selected
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-2 pt-1">
|
<div className="flex gap-2 pt-1">
|
||||||
<Button variant="outline" size="sm" asChild className="h-8 flex-1">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
asChild
|
||||||
|
className="h-8 flex-1"
|
||||||
|
>
|
||||||
<Link href="/dashboard/time-clock">Open</Link>
|
<Link href="/dashboard/time-clock">Open</Link>
|
||||||
</Button>
|
</Button>
|
||||||
{renderStopButton()}
|
{renderStopButton()}
|
||||||
@@ -185,58 +206,41 @@ export function ActiveTimerWidget({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border-primary/30 bg-primary/5">
|
<Card className="border-primary/30 bg-primary/5 shadow-none">
|
||||||
<CardContent className="flex flex-col gap-3 p-3">
|
<CardContent className="flex flex-col gap-2.5 p-3">
|
||||||
<div className="flex items-start gap-2">
|
<Link
|
||||||
<span className="relative mt-1 flex h-2.5 w-2.5 flex-shrink-0">
|
href="/dashboard/time-clock"
|
||||||
<span className="bg-primary absolute inline-flex h-full w-full animate-ping rounded-full opacity-75" />
|
className="focus-visible:ring-ring flex items-center justify-between gap-3 rounded-md outline-hidden focus-visible:ring-2"
|
||||||
<span className="bg-primary relative inline-flex h-2.5 w-2.5 rounded-full" />
|
>
|
||||||
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
|
<span className="relative flex size-2.5 shrink-0">
|
||||||
|
<span className="bg-primary absolute inline-flex size-full animate-ping rounded-full opacity-75" />
|
||||||
|
<span className="bg-primary relative inline-flex size-2.5 rounded-full" />
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">Timer running</span>
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<span className="text-primary shrink-0 font-mono text-sm font-bold tabular-nums">
|
||||||
<p className="text-sm leading-snug font-medium">
|
|
||||||
{description}
|
|
||||||
{running.client && (
|
|
||||||
<span className="text-muted-foreground font-normal">
|
|
||||||
{" "}
|
|
||||||
· {running.client.name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground mt-1 text-xs leading-snug">
|
|
||||||
{invoiceLabel ? (
|
|
||||||
<>
|
|
||||||
Billing to{" "}
|
|
||||||
<Link
|
|
||||||
href={`/dashboard/invoices/${running.invoice!.id}`}
|
|
||||||
className="text-primary hover:underline"
|
|
||||||
>
|
|
||||||
{invoiceLabel}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>No invoice selected — open time clock to assign</>
|
|
||||||
)}
|
|
||||||
{" · "}
|
|
||||||
<Link href="/dashboard/time-clock" className="text-primary hover:underline">
|
|
||||||
Time clock
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
|
||||||
<span className="text-primary text-center font-mono text-xl font-bold tabular-nums">
|
|
||||||
{formatElapsedSeconds(elapsed)}
|
{formatElapsedSeconds(elapsed)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex w-full flex-col gap-1.5">
|
</Link>
|
||||||
<Button variant="outline" size="sm" asChild className="h-8 w-full">
|
|
||||||
<Link href="/dashboard/time-clock">
|
<p
|
||||||
<Clock className="mr-1 h-3.5 w-3.5" />
|
className="text-muted-foreground truncate text-xs"
|
||||||
Open
|
title={description}
|
||||||
</Link>
|
>
|
||||||
</Button>
|
{description}
|
||||||
{renderStopButton("w-full")}
|
{running.client ? ` · ${running.client.name}` : ""}
|
||||||
</div>
|
{invoiceLabel ? ` · ${invoiceLabel}` : ""}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" size="sm" asChild className="flex-1">
|
||||||
|
<Link href="/dashboard/time-clock">
|
||||||
|
<Clock data-icon="inline-start" />
|
||||||
|
Open
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
{renderStopButton("flex-1")}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -41,12 +41,6 @@ const geistSans = localFont({
|
|||||||
display: "swap",
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
||||||
const playfair = localFont({
|
|
||||||
src: "../../node_modules/@fontsource-variable/playfair-display/files/playfair-display-latin-wght-normal.woff2",
|
|
||||||
variable: "--font-playfair",
|
|
||||||
display: "swap",
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = localFont({
|
const geistMono = localFont({
|
||||||
src: "../../public/fonts/geist/mono/GeistMono-VariableFont_wght.ttf",
|
src: "../../public/fonts/geist/mono/GeistMono-VariableFont_wght.ttf",
|
||||||
variable: "--font-geist-mono",
|
variable: "--font-geist-mono",
|
||||||
@@ -61,7 +55,7 @@ export default function RootLayout({
|
|||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
lang="en"
|
lang="en"
|
||||||
data-color-mode="system"
|
data-color-mode="system"
|
||||||
className={`${geistSans.variable} ${playfair.variable} ${geistMono.variable}`}
|
className={`${geistSans.variable} ${geistMono.variable}`}
|
||||||
>
|
>
|
||||||
<head>
|
<head>
|
||||||
<AppearanceInitScript />
|
<AppearanceInitScript />
|
||||||
|
|||||||
@@ -1,21 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { BusinessBrandImage } from "~/components/branding/business-brand-image";
|
|
||||||
import { Logo } from "~/components/branding/logo";
|
import { Logo } from "~/components/branding/logo";
|
||||||
import { hasBusinessBrandAsset } from "~/lib/business-branding";
|
|
||||||
import { api } from "~/trpc/react";
|
|
||||||
|
|
||||||
export function DashboardBrand({ compact = false }: { compact?: boolean }) {
|
export function DashboardBrand({ compact = false }: { compact?: boolean }) {
|
||||||
const { data: business } = api.businesses.getDefault.useQuery();
|
|
||||||
if (business && hasBusinessBrandAsset(business)) {
|
|
||||||
return (
|
|
||||||
<BusinessBrandImage
|
|
||||||
business={business}
|
|
||||||
kind={compact ? "icon" : "wordmark"}
|
|
||||||
decorative
|
|
||||||
className={compact ? "h-8 w-8" : "h-8 w-36"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <Logo size={compact ? "icon" : "sm"} />;
|
return <Logo size={compact ? "icon" : "sm"} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ import { cn } from "~/lib/utils";
|
|||||||
import { Menu } from "lucide-react";
|
import { Menu } from "lucide-react";
|
||||||
import { DashboardBrand } from "~/components/branding/dashboard-brand";
|
import { DashboardBrand } from "~/components/branding/dashboard-brand";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet";
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "~/components/ui/sheet";
|
||||||
import { ActiveTimerWidget } from "~/app/dashboard/_components/active-timer-widget";
|
import { ActiveTimerWidget } from "~/app/dashboard/_components/active-timer-widget";
|
||||||
import { OnboardingGuard } from "~/components/layout/onboarding-guard";
|
import { OnboardingGuard } from "~/components/layout/onboarding-guard";
|
||||||
|
|
||||||
@@ -40,10 +45,10 @@ function DashboardContent({ children }: { children: React.ReactNode }) {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="bg-background h-10 w-10 shadow-sm"
|
className="bg-background size-10 shadow-sm"
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<Menu className="h-5 w-5" />
|
<Menu />
|
||||||
<span className="sr-only">Toggle menu</span>
|
<span className="sr-only">Toggle menu</span>
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
@@ -51,10 +56,12 @@ function DashboardContent({ children }: { children: React.ReactNode }) {
|
|||||||
<DashboardBrand />
|
<DashboardBrand />
|
||||||
<ActiveTimerWidget compact />
|
<ActiveTimerWidget compact />
|
||||||
</div>
|
</div>
|
||||||
<SheetContent side="left" className="w-72 p-0">
|
<SheetContent
|
||||||
<div className="sr-only">
|
side="left"
|
||||||
<h2 id="mobile-nav-title">Navigation Menu</h2>
|
className="w-80 max-w-[90vw] gap-0 p-0"
|
||||||
</div>
|
aria-describedby={undefined}
|
||||||
|
>
|
||||||
|
<SheetTitle className="sr-only">Navigation menu</SheetTitle>
|
||||||
<Sidebar mobile onClose={() => setIsMobileOpen(false)} />
|
<Sidebar mobile onClose={() => setIsMobileOpen(false)} />
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
@@ -64,7 +71,7 @@ function DashboardContent({ children }: { children: React.ReactNode }) {
|
|||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-h-screen min-w-0 flex-1 transition-all duration-300 ease-in-out md:ml-0",
|
"min-h-screen min-w-0 flex-1 transition-all duration-300 ease-in-out md:ml-0",
|
||||||
!isOnboarding && (isCollapsed ? "md:ml-16" : "md:ml-64"),
|
!isOnboarding && (isCollapsed ? "md:ml-20" : "md:ml-72"),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isOnboarding ? (
|
{isOnboarding ? (
|
||||||
|
|||||||
@@ -13,29 +13,52 @@ const SidebarContext = React.createContext<SidebarContextType | undefined>(
|
|||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SIDEBAR_STORAGE_KEY = "sidebar-collapsed";
|
||||||
|
const SIDEBAR_CHANGE_EVENT = "beenvoice:sidebar-change";
|
||||||
|
|
||||||
|
function getSidebarSnapshot() {
|
||||||
|
return localStorage.getItem(SIDEBAR_STORAGE_KEY) === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServerSidebarSnapshot() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeToSidebar(callback: () => void) {
|
||||||
|
const handleStorage = (event: StorageEvent) => {
|
||||||
|
if (event.key === SIDEBAR_STORAGE_KEY) callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("storage", handleStorage);
|
||||||
|
window.addEventListener(SIDEBAR_CHANGE_EVENT, callback);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("storage", handleStorage);
|
||||||
|
window.removeEventListener(SIDEBAR_CHANGE_EVENT, callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSidebarState(collapsed: boolean) {
|
||||||
|
localStorage.setItem(SIDEBAR_STORAGE_KEY, String(collapsed));
|
||||||
|
window.dispatchEvent(new Event(SIDEBAR_CHANGE_EVENT));
|
||||||
|
}
|
||||||
|
|
||||||
export function SidebarProvider({ children }: { children: React.ReactNode }) {
|
export function SidebarProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [isCollapsed, setIsCollapsed] = React.useState(() => {
|
const isCollapsed = React.useSyncExternalStore(
|
||||||
if (typeof window === "undefined") return false;
|
subscribeToSidebar,
|
||||||
const saved = localStorage.getItem("sidebar-collapsed");
|
getSidebarSnapshot,
|
||||||
return saved ? (JSON.parse(saved) as boolean) : false;
|
getServerSidebarSnapshot,
|
||||||
});
|
);
|
||||||
|
|
||||||
const toggleCollapse = React.useCallback(() => {
|
const toggleCollapse = React.useCallback(() => {
|
||||||
setIsCollapsed((prev) => {
|
saveSidebarState(!isCollapsed);
|
||||||
const next = !prev;
|
}, [isCollapsed]);
|
||||||
localStorage.setItem("sidebar-collapsed", JSON.stringify(next));
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const expand = React.useCallback(() => {
|
const expand = React.useCallback(() => {
|
||||||
setIsCollapsed(false);
|
saveSidebarState(false);
|
||||||
localStorage.setItem("sidebar-collapsed", JSON.stringify(false));
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const collapse = React.useCallback(() => {
|
const collapse = React.useCallback(() => {
|
||||||
setIsCollapsed(true);
|
saveSidebarState(true);
|
||||||
localStorage.setItem("sidebar-collapsed", JSON.stringify(true));
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,279 +1,358 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import type { ComponentType } from "react";
|
||||||
|
import {
|
||||||
|
ChevronsUpDown,
|
||||||
|
LogOut,
|
||||||
|
PanelLeftClose,
|
||||||
|
PanelLeftOpen,
|
||||||
|
Plus,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { authClient } from "~/lib/auth-client";
|
|
||||||
import { Skeleton } from "~/components/ui/skeleton";
|
import { ActiveTimerWidget } from "~/app/dashboard/_components/active-timer-widget";
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { LogOut, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
|
||||||
import { getNavigationForUser, isNavLinkActive } from "~/lib/navigation";
|
|
||||||
import { useSidebar } from "./sidebar-provider";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { DashboardBrand } from "~/components/branding/dashboard-brand";
|
import { DashboardBrand } from "~/components/branding/dashboard-brand";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "~/components/ui/dropdown-menu";
|
||||||
|
import { Separator } from "~/components/ui/separator";
|
||||||
|
import { Skeleton } from "~/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "~/components/ui/tooltip";
|
} from "~/components/ui/tooltip";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "~/components/ui/dropdown-menu";
|
|
||||||
import { getGravatarUrl } from "~/lib/gravatar";
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
|
|
||||||
import { useAuthSession } from "~/hooks/use-auth-session";
|
import { useAuthSession } from "~/hooks/use-auth-session";
|
||||||
import { useDashboardUser } from "~/components/layout/dashboard-user-context";
|
import { authClient } from "~/lib/auth-client";
|
||||||
import { ActiveTimerWidget } from "~/app/dashboard/_components/active-timer-widget";
|
import { getGravatarUrl } from "~/lib/gravatar";
|
||||||
|
import { getNavigationForUser, isNavLinkActive } from "~/lib/navigation";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { useDashboardUser } from "./dashboard-user-context";
|
||||||
|
import { useSidebar } from "./sidebar-provider";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ mobile, onClose }: SidebarProps) {
|
interface SidebarLinkProps {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
icon: ComponentType<{ className?: string }>;
|
||||||
|
active: boolean;
|
||||||
|
collapsed: boolean;
|
||||||
|
mobile: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarLink({
|
||||||
|
name,
|
||||||
|
href,
|
||||||
|
icon: Icon,
|
||||||
|
active,
|
||||||
|
collapsed,
|
||||||
|
mobile,
|
||||||
|
onClose,
|
||||||
|
}: SidebarLinkProps) {
|
||||||
|
const link = (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
aria-current={active ? "page" : undefined}
|
||||||
|
onClick={mobile ? onClose : undefined}
|
||||||
|
className={cn(
|
||||||
|
"group focus-visible:ring-ring relative flex h-10 items-center rounded-xl text-sm font-medium outline-hidden transition-colors focus-visible:ring-2",
|
||||||
|
collapsed ? "w-11 justify-center" : "gap-3 px-3",
|
||||||
|
active
|
||||||
|
? "bg-accent text-accent-foreground shadow-sm"
|
||||||
|
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="size-4.5 shrink-0" />
|
||||||
|
{!collapsed ? (
|
||||||
|
<>
|
||||||
|
<span className="min-w-0 flex-1 truncate">{name}</span>
|
||||||
|
{active ? (
|
||||||
|
<span className="bg-primary size-1.5 shrink-0 rounded-full" />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!collapsed) return link;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{link}</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
|
{name}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sidebar({ mobile = false, onClose }: SidebarProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { data: session, isPending } = useAuthSession();
|
const { data: session, isPending } = useAuthSession();
|
||||||
const { isAdmin } = useDashboardUser();
|
const { isAdmin } = useDashboardUser();
|
||||||
const { isCollapsed, toggleCollapse } = useSidebar();
|
const { isCollapsed, toggleCollapse } = useSidebar();
|
||||||
const navSections = getNavigationForUser(isAdmin);
|
const navSections = getNavigationForUser(isAdmin);
|
||||||
|
|
||||||
// If mobile, always expanded
|
|
||||||
const collapsed = mobile ? false : isCollapsed;
|
const collapsed = mobile ? false : isCollapsed;
|
||||||
|
|
||||||
const SidebarContent = (
|
const sidebarContent = (
|
||||||
<div className="flex h-full flex-col justify-between">
|
<TooltipProvider delayDuration={150}>
|
||||||
<div>
|
<div className="flex h-full min-h-0 flex-col">
|
||||||
{/* Header / Logo */}
|
<header
|
||||||
<div
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-2 flex h-14 items-center px-4",
|
"border-border/70 flex h-20 shrink-0 items-center border-b",
|
||||||
collapsed ? "justify-center px-2" : "justify-between",
|
collapsed ? "justify-center px-2" : "px-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!collapsed && (
|
<Link
|
||||||
<div className="flex items-center gap-2">
|
href="/dashboard"
|
||||||
<DashboardBrand />
|
onClick={mobile ? onClose : undefined}
|
||||||
</div>
|
aria-label="Beenvoice dashboard"
|
||||||
)}
|
|
||||||
{collapsed && <DashboardBrand compact />}
|
|
||||||
|
|
||||||
{!mobile && !collapsed && (
|
|
||||||
<div className="h-8 w-8" /> // Spacer to keep alignment if needed, or just remove
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
|
||||||
<nav
|
|
||||||
className={cn(
|
|
||||||
"mt-4 flex flex-col gap-6 px-2",
|
|
||||||
collapsed && "items-center",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{navSections.map((section) => (
|
|
||||||
<div key={section.title}>
|
|
||||||
{!collapsed && (
|
|
||||||
<div className="text-muted-foreground/60 mb-2 px-2 text-xs font-semibold tracking-wider uppercase">
|
|
||||||
{section.title}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{section.links.map((link) => {
|
|
||||||
const Icon = link.icon;
|
|
||||||
const isActive = isNavLinkActive(pathname, link.href);
|
|
||||||
|
|
||||||
if (collapsed) {
|
|
||||||
return (
|
|
||||||
<TooltipProvider key={link.href} delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Link
|
|
||||||
href={link.href}
|
|
||||||
data-active={isActive ? "true" : undefined}
|
|
||||||
className={cn(
|
|
||||||
"flex h-10 w-10 items-center justify-center rounded-md transition-colors",
|
|
||||||
isActive
|
|
||||||
? "bg-primary text-primary-foreground shadow-sm"
|
|
||||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="h-5 w-5" />
|
|
||||||
</Link>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="right"
|
|
||||||
className="font-medium"
|
|
||||||
>
|
|
||||||
{link.name}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={link.href}
|
|
||||||
href={link.href}
|
|
||||||
data-active={isActive ? "true" : undefined}
|
|
||||||
onClick={mobile ? onClose : undefined}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
|
||||||
isActive
|
|
||||||
? "bg-primary/10 text-primary"
|
|
||||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="h-4 w-4" />
|
|
||||||
{link.name}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer / User */}
|
|
||||||
<div className="mt-auto space-y-2 p-2">
|
|
||||||
{!mobile && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex",
|
"focus-visible:ring-ring flex min-w-0 items-center outline-hidden focus-visible:ring-2",
|
||||||
collapsed ? "justify-center" : "justify-end px-2",
|
collapsed ? "justify-center rounded-lg" : "flex-1",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Button
|
<DashboardBrand compact={collapsed} />
|
||||||
variant="ghost"
|
</Link>
|
||||||
size="icon"
|
</header>
|
||||||
className="text-muted-foreground h-8 w-8"
|
|
||||||
onClick={toggleCollapse}
|
|
||||||
>
|
|
||||||
{collapsed ? (
|
|
||||||
<PanelLeftOpen className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<PanelLeftClose className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ActiveTimerWidget collapsed={collapsed} />
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
|
<nav
|
||||||
|
id="dashboard-sidebar-navigation"
|
||||||
|
aria-label="Dashboard navigation"
|
||||||
|
className="min-h-0 flex-1 overflow-y-auto px-3 py-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<div className={cn("flex", collapsed && "justify-center")}>
|
||||||
|
{collapsed ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
size="icon"
|
||||||
|
className="size-11 rounded-xl"
|
||||||
|
>
|
||||||
|
<Link href="/dashboard/invoices/new">
|
||||||
|
<Plus />
|
||||||
|
<span className="sr-only">New invoice</span>
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
|
New invoice
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Button asChild className="w-full justify-start">
|
||||||
|
<Link
|
||||||
|
href="/dashboard/invoices/new"
|
||||||
|
onClick={mobile ? onClose : undefined}
|
||||||
|
>
|
||||||
|
<Plus data-icon="inline-start" />
|
||||||
|
New invoice
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
{navSections.map((section, sectionIndex) => (
|
||||||
className={cn(
|
<section
|
||||||
"border-border/50 border-t pt-4",
|
key={section.title}
|
||||||
collapsed ? "flex flex-col items-center gap-2" : "px-2",
|
aria-label={collapsed ? section.title : undefined}
|
||||||
)}
|
aria-labelledby={
|
||||||
>
|
collapsed ? undefined : `sidebar-section-${sectionIndex}`
|
||||||
{isPending ? (
|
}
|
||||||
<div
|
className="flex flex-col gap-2"
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-3",
|
|
||||||
collapsed ? "justify-center" : "px-2",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Skeleton className="h-9 w-9 rounded-full" />
|
|
||||||
{!collapsed && (
|
|
||||||
<div className="flex-1 space-y-1">
|
|
||||||
<Skeleton className="h-3 w-20" />
|
|
||||||
<Skeleton className="h-2 w-24" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : session?.user ? (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className={cn(
|
|
||||||
"w-full justify-start p-0 hover:bg-transparent",
|
|
||||||
collapsed && "justify-center",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{/* FIXED: Changed div to span to prevent hydration error */}
|
{collapsed ? (
|
||||||
<span
|
sectionIndex > 0 ? (
|
||||||
|
<Separator className="mx-auto w-8" />
|
||||||
|
) : null
|
||||||
|
) : (
|
||||||
|
<h2
|
||||||
|
id={`sidebar-section-${sectionIndex}`}
|
||||||
|
className="text-muted-foreground px-3 text-[0.6875rem] font-semibold tracking-[0.12em] uppercase"
|
||||||
|
>
|
||||||
|
{section.title}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3",
|
"flex flex-col gap-1",
|
||||||
collapsed ? "justify-center" : "w-full",
|
collapsed && "items-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Avatar className="border-border h-9 w-9 border">
|
{section.links.map((link) => (
|
||||||
|
<SidebarLink
|
||||||
|
key={link.href}
|
||||||
|
{...link}
|
||||||
|
active={isNavLinkActive(pathname, link.href)}
|
||||||
|
collapsed={collapsed}
|
||||||
|
mobile={mobile}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{!mobile ? (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 px-3 pb-3",
|
||||||
|
collapsed && "flex justify-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size={collapsed ? "icon" : "default"}
|
||||||
|
aria-label={
|
||||||
|
collapsed ? "Expand sidebar" : "Collapse sidebar"
|
||||||
|
}
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
aria-controls="dashboard-sidebar-navigation"
|
||||||
|
onClick={toggleCollapse}
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground h-10 rounded-xl",
|
||||||
|
collapsed
|
||||||
|
? "mx-auto size-11"
|
||||||
|
: "w-full justify-start gap-3 px-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{collapsed ? <PanelLeftOpen /> : <PanelLeftClose />}
|
||||||
|
{!collapsed ? <span>Collapse sidebar</span> : null}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
|
{collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<footer className="border-border/70 flex shrink-0 flex-col gap-2 border-t p-3">
|
||||||
|
<ActiveTimerWidget collapsed={collapsed} />
|
||||||
|
|
||||||
|
{isPending ? (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-12 items-center gap-3 px-2",
|
||||||
|
collapsed && "justify-center px-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Skeleton className="size-9 rounded-full" />
|
||||||
|
{!collapsed ? (
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||||
|
<Skeleton className="h-3 w-20" />
|
||||||
|
<Skeleton className="h-2.5 w-28" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : session?.user ? (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size={collapsed ? "icon" : "default"}
|
||||||
|
aria-label={collapsed ? "Open account menu" : undefined}
|
||||||
|
className={cn(
|
||||||
|
"h-auto min-h-12 rounded-xl",
|
||||||
|
collapsed
|
||||||
|
? "mx-auto size-11 p-0"
|
||||||
|
: "w-full justify-start gap-3 px-2 py-1.5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Avatar className="border-border size-9 shrink-0 border">
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
src={getGravatarUrl(session.user.email)}
|
src={getGravatarUrl(session.user.email)}
|
||||||
alt={session.user.name ?? "User"}
|
alt=""
|
||||||
/>
|
/>
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{session.user.name?.[0] ?? "U"}
|
{session.user.name?.[0] ?? "U"}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
{!collapsed && (
|
{!collapsed ? (
|
||||||
<span className="min-w-0 flex-1 text-left">
|
<>
|
||||||
<span className="block truncate text-sm font-medium">
|
<span className="min-w-0 flex-1 text-left">
|
||||||
{session.user.name}
|
<span className="block truncate text-sm font-medium">
|
||||||
|
{session.user.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground block truncate text-xs">
|
||||||
|
{session.user.email}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground block truncate text-xs">
|
<ChevronsUpDown className="text-muted-foreground" />
|
||||||
{session.user.email}
|
</>
|
||||||
</span>
|
) : null}
|
||||||
</span>
|
</Button>
|
||||||
)}
|
</DropdownMenuTrigger>
|
||||||
</span>
|
<DropdownMenuContent
|
||||||
</Button>
|
side={mobile ? "top" : "right"}
|
||||||
</DropdownMenuTrigger>
|
align="end"
|
||||||
<DropdownMenuContent
|
sideOffset={8}
|
||||||
side="right"
|
className="border-border w-60 border"
|
||||||
align="end"
|
|
||||||
className="bg-background/80 border-border/50 w-56 backdrop-blur-xl"
|
|
||||||
sideOffset={10}
|
|
||||||
>
|
|
||||||
<DropdownMenuLabel>
|
|
||||||
<div className="flex flex-col space-y-1">
|
|
||||||
<p className="text-sm leading-none font-medium">
|
|
||||||
{session.user.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-xs leading-none">
|
|
||||||
{session.user.email}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={async () => {
|
|
||||||
await authClient.signOut();
|
|
||||||
window.location.href = "/";
|
|
||||||
}}
|
|
||||||
className="text-red-600 focus:bg-red-100/50 focus:text-red-600 dark:focus:bg-red-900/20"
|
|
||||||
>
|
>
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<DropdownMenuLabel className="flex flex-col gap-1">
|
||||||
Sign Out
|
<span className="truncate">{session.user.name}</span>
|
||||||
</DropdownMenuItem>
|
<span className="text-muted-foreground truncate text-xs font-normal">
|
||||||
</DropdownMenuContent>
|
{session.user.email}
|
||||||
</DropdownMenu>
|
</span>
|
||||||
) : null}
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
variant="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await authClient.signOut();
|
||||||
|
window.location.href = "/";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LogOut />
|
||||||
|
Sign out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mobile) {
|
if (mobile) {
|
||||||
return <div className="bg-background h-full">{SidebarContent}</div>;
|
return <div className="bg-background h-full">{sidebarContent}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-border bg-background fixed top-0 bottom-0 left-0 z-30 hidden flex-col rounded-none border-r shadow-none transition-all duration-300 ease-in-out md:flex",
|
"border-border bg-background fixed inset-y-0 left-0 z-30 hidden border-r transition-[width] duration-200 ease-out md:block",
|
||||||
isCollapsed ? "w-16" : "w-64",
|
isCollapsed ? "w-20" : "w-72",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{SidebarContent}
|
{sidebarContent}
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--app-font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
--app-font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||||
--app-font-heading: var(--font-playfair), ui-serif, Georgia, serif;
|
--app-font-heading: var(--font-geist-sans), ui-sans-serif, system-ui,
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 240 10% 3.9%;
|
--foreground: 240 10% 3.9%;
|
||||||
@@ -100,7 +101,7 @@
|
|||||||
--color-ring: hsl(var(--ring));
|
--color-ring: hsl(var(--ring));
|
||||||
|
|
||||||
--font-sans: var(--app-font-sans), ui-sans-serif, system-ui, sans-serif;
|
--font-sans: var(--app-font-sans), ui-sans-serif, system-ui, sans-serif;
|
||||||
--font-heading: var(--app-font-heading), ui-serif, Georgia, serif;
|
--font-heading: var(--app-font-heading), ui-sans-serif, system-ui, sans-serif;
|
||||||
--font-mono: var(--font-geist-mono), ui-monospace, monospace;
|
--font-mono: var(--font-geist-mono), ui-monospace, monospace;
|
||||||
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
|||||||
@@ -86,7 +86,6 @@
|
|||||||
"@dnd-kit/modifiers": "9.0.0",
|
"@dnd-kit/modifiers": "9.0.0",
|
||||||
"@dnd-kit/sortable": "10.0.0",
|
"@dnd-kit/sortable": "10.0.0",
|
||||||
"@dnd-kit/utilities": "3.2.2",
|
"@dnd-kit/utilities": "3.2.2",
|
||||||
"@fontsource-variable/playfair-display": "5.2.8",
|
|
||||||
"@radix-ui/react-alert-dialog": "1.1.16",
|
"@radix-ui/react-alert-dialog": "1.1.16",
|
||||||
"@radix-ui/react-avatar": "1.1.12",
|
"@radix-ui/react-avatar": "1.1.12",
|
||||||
"@radix-ui/react-checkbox": "1.3.4",
|
"@radix-ui/react-checkbox": "1.3.4",
|
||||||
@@ -664,8 +663,6 @@
|
|||||||
|
|
||||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
|
||||||
|
|
||||||
"@fontsource-variable/playfair-display": ["@fontsource-variable/playfair-display@5.2.8", "", {}, "sha512-ZzVIXPOrL85yyOvZYoBzUszIJM+xKkHqni4IYn2CVLaGQQdJR8sBeC8yFNgjxSJ7ludTwta8qpULeOFuk5X75A=="],
|
|
||||||
|
|
||||||
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
||||||
|
|
||||||
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user