
Some checks failed
GBCI / build (push) Failing after 10m45s
- Introduced a new CSS theme for Orange, featuring a modern color palette with distinct foreground and background colors. - Added an XTree Gold theme that emulates the classic 1980s DOS interface, complete with authentic colors and styles for file management elements. - Both themes include variables for customization and specific styles for various UI components such as cards, popovers, and menus.
565 lines
22 KiB
TypeScript
565 lines
22 KiB
TypeScript
"use client"
|
|
import { accounts, mails } from "./data"
|
|
|
|
import * as React from "react"
|
|
import {
|
|
AlertCircle, Archive, ArchiveX, Clock, File, Forward, Inbox, Mail as MailIcon,
|
|
MessagesSquare, MoreVertical, Reply, ReplyAll, Search, Send, ShoppingCart,
|
|
Trash2, Users2, LucideIcon
|
|
} from "lucide-react"
|
|
import { useMail } from "./use-mail"
|
|
import { cn } from "@/lib/utils"
|
|
import { format, formatDistanceToNow, addDays, addHours, nextSaturday } from "date-fns"
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Calendar } from "@/components/ui/calendar"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
|
import {
|
|
ResizableHandle,
|
|
ResizablePanel,
|
|
ResizablePanelGroup,
|
|
} from "@/components/ui/resizable"
|
|
import Footer from '../footer';
|
|
|
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import { Separator } from "@/components/ui/separator"
|
|
import { Switch } from "@/components/ui/switch"
|
|
import {
|
|
Tabs,
|
|
TabsContent,
|
|
TabsList,
|
|
TabsTrigger,
|
|
} from "@/components/ui/tabs"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
TooltipProvider,
|
|
} from "@/components/ui/tooltip"
|
|
|
|
interface Account {
|
|
label: string
|
|
email: string
|
|
icon: React.ReactNode
|
|
}
|
|
|
|
interface Mail {
|
|
id: string
|
|
name: string
|
|
email: string
|
|
subject: string
|
|
text: string
|
|
date: string
|
|
read: boolean
|
|
labels: string[]
|
|
}
|
|
|
|
interface NavLink {
|
|
title: string
|
|
label?: string
|
|
icon: LucideIcon
|
|
variant: "default" | "ghost"
|
|
}
|
|
|
|
interface MailProps {
|
|
accounts: Account[]
|
|
mails: Mail[]
|
|
defaultLayout?: number[]
|
|
defaultCollapsed?: boolean
|
|
navCollapsedSize: number
|
|
}
|
|
|
|
|
|
function MailComponent({
|
|
accounts,
|
|
mails,
|
|
defaultLayout = [20, 32, 48],
|
|
defaultCollapsed = false,
|
|
navCollapsedSize,
|
|
}: MailProps) {
|
|
const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed)
|
|
const [mail, setMail] = useMail()
|
|
|
|
const navLinks: NavLink[] = [
|
|
{ title: "Inbox", label: "128", icon: Inbox, variant: "default" },
|
|
{ title: "Drafts", label: "9", icon: File, variant: "ghost" },
|
|
{ title: "Sent", label: "", icon: Send, variant: "ghost" },
|
|
{ title: "Junk", label: "23", icon: ArchiveX, variant: "ghost" },
|
|
{ title: "Trash", label: "", icon: Trash2, variant: "ghost" },
|
|
{ title: "Archive", label: "", icon: Archive, variant: "ghost" },
|
|
{ title: "Social", label: "972", icon: Users2, variant: "ghost" },
|
|
{ title: "Updates", label: "342", icon: AlertCircle, variant: "ghost" },
|
|
{ title: "Forums", label: "128", icon: MessagesSquare, variant: "ghost" },
|
|
{ title: "Shopping", label: "8", icon: ShoppingCart, variant: "ghost" },
|
|
{ title: "Promotions", label: "21", icon: Archive, variant: "ghost" }
|
|
]
|
|
|
|
return (
|
|
<TooltipProvider delayDuration={0}>
|
|
<ResizablePanelGroup
|
|
direction="horizontal"
|
|
className="h-full max-h-[800px] items-stretch"
|
|
>
|
|
{/* Left Sidebar */}
|
|
<ResizablePanel
|
|
defaultSize={defaultLayout[0]}
|
|
collapsedSize={navCollapsedSize}
|
|
collapsible={true}
|
|
minSize={15}
|
|
maxSize={20}
|
|
onCollapse={() => setIsCollapsed(true)}
|
|
onResize={() => setIsCollapsed(false)}
|
|
className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")}
|
|
>
|
|
<div className={cn("flex h-[52px] items-center justify-center", isCollapsed ? "h-[52px]" : "px-2")}>
|
|
<AccountSwitcher isCollapsed={isCollapsed} accounts={accounts} />
|
|
</div>
|
|
<Separator />
|
|
<Nav links={navLinks.slice(0, 6)} isCollapsed={isCollapsed} />
|
|
<Separator />
|
|
<Nav links={navLinks.slice(6)} isCollapsed={isCollapsed} />
|
|
</ResizablePanel>
|
|
|
|
<ResizableHandle withHandle />
|
|
|
|
{/* Middle Mail List */}
|
|
<ResizablePanel defaultSize={defaultLayout[1]} minSize={30}>
|
|
<Tabs defaultValue="all">
|
|
<div className="flex items-center px-4 py-2">
|
|
<h1 className="text-xl font-bold">Inbox</h1>
|
|
<TabsList className="ml-auto">
|
|
<TabsTrigger value="all" className="text-foreground/80">All mail</TabsTrigger>
|
|
<TabsTrigger value="unread" className="text-foreground/80">Unread</TabsTrigger>
|
|
</TabsList>
|
|
</div>
|
|
<Separator />
|
|
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<form>
|
|
<div className="relative">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input placeholder="Search" className="pl-8" />
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<TabsContent value="all" className="m-0">
|
|
<MailList items={mails} />
|
|
</TabsContent>
|
|
<TabsContent value="unread" className="m-0">
|
|
<MailList items={mails.filter((item) => !item.read)} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</ResizablePanel>
|
|
|
|
<ResizableHandle withHandle />
|
|
|
|
{/* Right Mail Display */}
|
|
<ResizablePanel defaultSize={defaultLayout[2]} minSize={30}>
|
|
<MailDisplay mail={mails.find((item) => item.id === mail.selected) || null} />
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</TooltipProvider>
|
|
)
|
|
}
|
|
|
|
function AccountSwitcher({ isCollapsed, accounts }: { isCollapsed: boolean, accounts: Account[] }) {
|
|
const [selectedAccount, setSelectedAccount] = React.useState(accounts[0].email)
|
|
|
|
return (
|
|
<Select defaultValue={selectedAccount} onValueChange={setSelectedAccount}>
|
|
<SelectTrigger
|
|
className={cn(
|
|
"flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0",
|
|
isCollapsed && "flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden"
|
|
)}
|
|
aria-label="Select account"
|
|
>
|
|
<SelectValue placeholder="Select an account">
|
|
{accounts.find((account) => account.email === selectedAccount)?.icon}
|
|
<span className={cn("ml-2", isCollapsed && "hidden")}>
|
|
{accounts.find((account) => account.email === selectedAccount)?.label}
|
|
</span>
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{accounts.map((account) => (
|
|
<SelectItem key={account.email} value={account.email}>
|
|
<div className="flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground">
|
|
{account.icon}
|
|
{account.email}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)
|
|
}
|
|
|
|
function Nav({ links, isCollapsed }: { links: NavLink[], isCollapsed: boolean }) {
|
|
return (
|
|
<div
|
|
data-collapsed={isCollapsed}
|
|
className="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2"
|
|
>
|
|
<nav className="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
|
|
{links.map((link, index) =>
|
|
isCollapsed ? (
|
|
<Tooltip key={index} delayDuration={0}>
|
|
<TooltipTrigger asChild>
|
|
<a
|
|
href="#"
|
|
className={cn(
|
|
"h-9 w-9 inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
|
|
link.variant === "default"
|
|
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
: "hover:bg-secondary hover:text-secondary-foreground",
|
|
link.variant === "default" && "dark:bg-primary/10 dark:text-primary-foreground"
|
|
)}
|
|
>
|
|
<link.icon className="h-4 w-4" />
|
|
<span className="sr-only">{link.title}</span>
|
|
</a>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right" className="flex items-center gap-4">
|
|
{link.title}
|
|
{link.label && <span className="ml-auto text-muted-foreground">{link.label}</span>}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
) : (
|
|
<a
|
|
key={index}
|
|
href="#"
|
|
className={cn(
|
|
"inline-flex items-center h-9 px-3 rounded-md text-sm font-medium transition-colors",
|
|
link.variant === "default"
|
|
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
: "hover:bg-secondary hover:text-secondary-foreground",
|
|
link.variant === "default" && "dark:bg-primary/10 dark:text-primary-foreground",
|
|
"justify-start"
|
|
)}
|
|
>
|
|
<link.icon className="mr-2 h-4 w-4" />
|
|
{link.title}
|
|
{link.label && (
|
|
<span className={cn("ml-auto", link.variant === "default" && "text-primary-foreground")}>
|
|
{link.label}
|
|
</span>
|
|
)}
|
|
</a>
|
|
)
|
|
)}
|
|
</nav>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function MailList({ items }: { items: Mail[] }) {
|
|
const [mail, setMail] = useMail()
|
|
|
|
return (
|
|
<ScrollArea className="h-screen">
|
|
<div className="flex flex-col gap-2 p-4 pt-0">
|
|
{items.map((item) => (
|
|
<button
|
|
key={item.id}
|
|
className={cn(
|
|
"flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent",
|
|
mail.selected === item.id && "bg-muted"
|
|
)}
|
|
onClick={() => setMail({ ...mail, selected: item.id })}
|
|
>
|
|
<div className="flex w-full flex-col gap-1">
|
|
<div className="flex items-center">
|
|
<div className="flex items-center gap-2">
|
|
<div className="font-semibold">{item.name}</div>
|
|
{!item.read && <span className="flex h-2 w-2 rounded-full bg-primary" />}
|
|
</div>
|
|
<div className={cn("ml-auto text-xs", mail.selected === item.id ? "text-foreground" : "text-muted-foreground")}>
|
|
{formatDistanceToNow(new Date(item.date), { addSuffix: true })}
|
|
</div>
|
|
</div>
|
|
<div className="text-xs font-medium">{item.subject}</div>
|
|
</div>
|
|
<div className="line-clamp-2 text-xs text-muted-foreground">
|
|
{item.text.substring(0, 300)}
|
|
</div>
|
|
{item.labels.length ? (
|
|
<div className="flex items-center gap-2">
|
|
{item.labels.map((label) => (
|
|
<Badge key={label} variant={getBadgeVariantFromLabel(label)}>
|
|
{label}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
)
|
|
}
|
|
|
|
function MailDisplay({ mail }: { mail: Mail | null }) {
|
|
const today = new Date()
|
|
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
<div className="flex items-center p-2">
|
|
<div className="flex items-center gap-2">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon" disabled={!mail}>
|
|
<Archive className="h-4 w-4" />
|
|
<span className="sr-only">Archive</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Archive</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon" disabled={!mail}>
|
|
<ArchiveX className="h-4 w-4" />
|
|
<span className="sr-only">Move to junk</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Move to junk</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon" disabled={!mail}>
|
|
<Trash2 className="h-4 w-4" />
|
|
<span className="sr-only">Move to trash</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Move to trash</TooltipContent>
|
|
</Tooltip>
|
|
<Separator orientation="vertical" className="mx-1 h-6" />
|
|
<Tooltip>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon" disabled={!mail}>
|
|
<Clock className="h-4 w-4" />
|
|
<span className="sr-only">Snooze</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="flex w-[535px] p-0">
|
|
<div className="flex flex-col gap-2 border-r px-2 py-4">
|
|
<div className="px-4 text-sm font-medium">Snooze until</div>
|
|
<div className="grid min-w-[250px] gap-1">
|
|
<Button variant="ghost" className="justify-start font-normal">
|
|
Later today <span className="ml-auto text-muted-foreground">
|
|
{format(addHours(today, 4), "E, h:m b")}
|
|
</span>
|
|
</Button>
|
|
<Button variant="ghost" className="justify-start font-normal">
|
|
Tomorrow<span className="ml-auto text-muted-foreground">
|
|
{format(addDays(today, 1), "E, h:m b")}
|
|
</span>
|
|
</Button>
|
|
<Button variant="ghost" className="justify-start font-normal">
|
|
This weekend<span className="ml-auto text-muted-foreground">
|
|
{format(nextSaturday(today), "E, h:m b")}
|
|
</span>
|
|
</Button>
|
|
<Button variant="ghost" className="justify-start font-normal">
|
|
Next week<span className="ml-auto text-muted-foreground">
|
|
{format(addDays(today, 7), "E, h:m b")}
|
|
</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="p-2">
|
|
<Calendar />
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<TooltipContent>Snooze</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
<div className="ml-auto flex items-center gap-2">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon" disabled={!mail}>
|
|
<Reply className="h-4 w-4" />
|
|
<span className="sr-only">Reply</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Reply</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon" disabled={!mail}>
|
|
<ReplyAll className="h-4 w-4" />
|
|
<span className="sr-only">Reply all</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Reply all</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon" disabled={!mail}>
|
|
<Forward className="h-4 w-4" />
|
|
<span className="sr-only">Forward</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Forward</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
<Separator orientation="vertical" className="mx-2 h-6" />
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" disabled={!mail}>
|
|
<MoreVertical className="h-4 w-4" />
|
|
<span className="sr-only">More</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem>Mark as unread</DropdownMenuItem>
|
|
<DropdownMenuItem>Star thread</DropdownMenuItem>
|
|
<DropdownMenuItem>Add label</DropdownMenuItem>
|
|
<DropdownMenuItem>Mute thread</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
<Separator />
|
|
{mail ? (
|
|
<div className="flex flex-1 flex-col">
|
|
<div className="flex items-start p-4">
|
|
<div className="flex items-start gap-4 text-sm">
|
|
<Avatar>
|
|
<AvatarImage alt={mail.name} />
|
|
<AvatarFallback>
|
|
{mail.name.split(" ").map((chunk) => chunk[0]).join("")}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="grid gap-1">
|
|
<div className="font-semibold">{mail.name}</div>
|
|
<div className="line-clamp-1 text-xs">{mail.subject}</div>
|
|
<div className="line-clamp-1 text-xs">
|
|
<span className="font-medium">Reply-To:</span> {mail.email}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{mail.date && (
|
|
<div className="ml-auto text-xs text-muted-foreground">
|
|
{format(new Date(mail.date), "PPpp")}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Separator />
|
|
<div className="flex-1 whitespace-pre-wrap p-4 text-sm">
|
|
{mail.text}
|
|
</div>
|
|
<Separator className="mt-auto" />
|
|
<div className="p-4">
|
|
<form>
|
|
<div className="grid gap-4">
|
|
<Textarea className="p-4" placeholder={`Reply ${mail.name}...`} />
|
|
<div className="flex items-center">
|
|
<Label htmlFor="mute" className="flex items-center gap-2 text-xs font-normal">
|
|
<Switch id="mute" aria-label="Mute thread" /> Mute this thread
|
|
</Label>
|
|
<Button onClick={(e) => e.preventDefault()} size="sm" className="ml-auto">
|
|
Send
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="p-8 text-center text-muted-foreground">
|
|
No message selected
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
|
|
function getBadgeVariantFromLabel(label: string) {
|
|
if (["work"].includes(label.toLowerCase())) return "default"
|
|
if (["personal"].includes(label.toLowerCase())) return "outline"
|
|
return "secondary"
|
|
}
|
|
|
|
|
|
|
|
export default function MailPage() {
|
|
const layout = [20, 32, 48]; //cookies().get("react-resizable-panels:layout:mail")
|
|
const collapsed = false; //cookies().get("react-resizable-panels:collapsed")
|
|
|
|
const defaultLayout = layout// ? JSON.parse(layout.value) : undefined
|
|
const defaultCollapsed = collapsed //? JSON.parse(collapsed.value) : undefined
|
|
|
|
|
|
// Drive-specific keyboard shortcuts
|
|
// XTreeGold classic shortcut layout - two rows
|
|
// XTree/NC-style: only unique, non-browser, non-reserved keys for file ops
|
|
// No F1-F12, Ctrl+F, Ctrl+T, Ctrl+W, Ctrl+N, Ctrl+R, Ctrl+P, etc.
|
|
// Use Q, W, E, R, T, Y, U, I, O, P, A, S, D, G, H, J, K, L, Z, X, C, V, B, M with Ctrl/Shift if needed
|
|
const shortcuts = [
|
|
// File operations row (unique qkeys)
|
|
[
|
|
{ key: 'Q', label: 'Rename', action: () => console.log('Rename') },
|
|
{ key: 'W', label: 'View', action: () => console.log('View') },
|
|
{ key: 'E', label: 'Edit', action: () => console.log('Edit') },
|
|
{ key: 'R', label: 'Move', action: () => console.log('Move') },
|
|
{ key: 'T', label: 'MkDir', action: () => console.log('Make Directory') },
|
|
{ key: 'Y', label: 'Delete', action: () => console.log('Delete') },
|
|
{ key: 'U', label: 'Copy', action: () => console.log('Copy') },
|
|
{ key: 'I', label: 'Cut', action: () => console.log('Cut') },
|
|
{ key: 'O', label: 'Paste', action: () => console.log('Paste') },
|
|
{ key: 'P', label: 'Duplicate', action: () => console.log('Duplicate') },
|
|
],
|
|
// Navigation/info row (unique qkeys)
|
|
[
|
|
{ key: 'A', label: 'Select', action: () => console.log('Select') },
|
|
{ key: 'S', label: 'Select All', action: () => console.log('Select All') },
|
|
{ key: 'D', label: 'Deselect', action: () => console.log('Deselect') },
|
|
{ key: 'G', label: 'Details', action: () => console.log('Details') },
|
|
{ key: 'H', label: 'History', action: () => console.log('History') },
|
|
{ key: 'J', label: 'Share', action: () => console.log('Share') },
|
|
{ key: 'K', label: 'Star', action: () => console.log('Star') },
|
|
{ key: 'L', label: 'Download', action: () => console.log('Download') },
|
|
{ key: 'Z', label: 'Upload', action: () => console.log('Upload') },
|
|
{ key: 'X', label: 'Refresh', action: () => console.log('Refresh') },
|
|
]
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<div className="flex-col md:flex">
|
|
<MailComponent
|
|
accounts={accounts}
|
|
mails={mails}
|
|
defaultLayout={defaultLayout}
|
|
defaultCollapsed={defaultCollapsed}
|
|
navCollapsedSize={4}
|
|
/>
|
|
|
|
</div>
|
|
{/* Footer with Status Bar */}
|
|
<Footer shortcuts={shortcuts} />
|
|
|
|
</>
|
|
)
|
|
}
|