ubiquitous-invention/apps/web/components/layout/top-header.tsx

288 lines
9.9 KiB
TypeScript
Raw Permalink Normal View History

"use client";
import { useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { signOut } from "next-auth/react";
import {
Activity,
AppWindow,
Calendar,
CheckSquare,
ChevronDown,
FileText,
LayoutGrid,
LayoutTemplate,
LogOut,
Pencil,
Plus,
Presentation,
ScrollText,
Search,
Settings,
User,
Users,
Zap,
} from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { CreateWorkspaceDialog } from "@/components/workspaces/create-workspace-dialog";
import { useWorkspaceStore } from "@/lib/stores/workspace-store";
import { cn } from "@/lib/utils";
export interface TopHeaderProps {
onOpenSearch?: () => void;
onQuickAction?: (type: string) => void;
}
export function TopHeader({ onOpenSearch, onQuickAction }: TopHeaderProps) {
const pathname = usePathname();
const router = useRouter();
const workspace = useWorkspaceStore((s) => s.currentWorkspace);
const workspaceName = workspace?.name ?? "Workspace";
const workspaceId = workspace?.id;
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
const initials = workspaceName.trim().slice(0, 2).toUpperCase() || "WS";
return (
<header
className={cn(
"sticky top-0 z-10 flex h-12 w-full shrink-0 items-center gap-4 border-b border-border bg-background px-4",
)}
>
{/* Workspace dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex shrink-0 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<div className="flex h-7 w-7 items-center justify-center rounded-md bg-primary/15 text-[10px] font-bold text-primary">
{initials}
</div>
<span className="max-w-[180px] truncate">{workspaceName}</span>
<ChevronDown className="size-3.5 opacity-50" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-64 p-0">
{/* Header */}
<div className="flex items-start gap-3 px-4 py-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/15 text-sm font-bold text-primary">
{initials}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-semibold">{workspaceName}</p>
<p className="text-xs text-muted-foreground">1 member</p>
</div>
</div>
{/* Settings + People row */}
<div className="flex gap-2 px-4 pb-3">
<Button
variant="outline"
size="sm"
className="flex-1 gap-1.5 text-xs"
onClick={() => workspaceId && router.push(`/${workspaceId}/settings`)}
>
<Settings className="size-3.5" />
Settings
</Button>
<Button
variant="outline"
size="sm"
className="flex-1 gap-1.5 text-xs"
>
<Users className="size-3.5" />
People
</Button>
</div>
<DropdownMenuSeparator className="my-0" />
{/* Manage section */}
<DropdownMenuLabel className="px-4 pb-1 pt-3 text-xs font-medium text-muted-foreground">
Manage
</DropdownMenuLabel>
<DropdownMenuItem className="gap-2.5 px-4">
<AppWindow className="size-4 text-muted-foreground" />
Apps
</DropdownMenuItem>
<DropdownMenuItem
className="gap-2.5 px-4"
onSelect={() =>
workspaceId && router.push(`/${workspaceId}/settings/templates`)
}
>
<LayoutTemplate className="size-4 text-muted-foreground" />
Templates
</DropdownMenuItem>
<DropdownMenuItem
className="gap-2.5 px-4"
onSelect={() =>
workspaceId && router.push(`/${workspaceId}/settings/types`)
}
>
<Pencil className="size-4 text-muted-foreground" />
Custom Fields
</DropdownMenuItem>
<DropdownMenuItem className="gap-2.5 px-4">
<Zap className="size-4 text-muted-foreground" />
Automations
</DropdownMenuItem>
<DropdownMenuItem
className="gap-2.5 px-4"
onSelect={() =>
workspaceId && router.push(`/${workspaceId}/settings/audit`)
}
>
<ScrollText className="size-4 text-muted-foreground" />
Audit log
</DropdownMenuItem>
<DropdownMenuItem
className="gap-2.5 px-4"
onSelect={() =>
workspaceId && router.push(`/${workspaceId}/settings/runs`)
}
>
<Activity className="size-4 text-muted-foreground" />
Agent runs
</DropdownMenuItem>
<DropdownMenuSeparator />
{/* Create Workspace */}
<div className="px-3 py-2">
<Button
variant="outline"
size="sm"
className="w-full gap-1.5 text-xs"
onClick={() => setCreateWorkspaceOpen(true)}
>
<Plus className="size-3.5" />
Create Workspace
</Button>
</div>
</DropdownMenuContent>
</DropdownMenu>
<CreateWorkspaceDialog
open={createWorkspaceOpen}
onOpenChange={setCreateWorkspaceOpen}
/>
{/* Center search bar */}
<div className="flex flex-1 justify-center px-2">
<div
role="button"
tabIndex={0}
className={cn(
"flex h-9 w-full max-w-[400px] cursor-pointer items-center gap-2 rounded-lg border border-border bg-muted/50 px-3 text-sm text-muted-foreground transition-colors hover:bg-muted/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
)}
onClick={() => onOpenSearch?.()}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onOpenSearch?.();
}
}}
>
<Search className="size-4 shrink-0 opacity-70" aria-hidden />
<span className="min-w-0 flex-1 truncate text-left">Search...</span>
<kbd className="pointer-events-none shrink-0 rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[10px] font-medium">
Cmd+/
</kbd>
</div>
</div>
{/* Right actions */}
<div className="flex shrink-0 items-center gap-1.5">
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8"
aria-label="Calendar"
>
<Calendar className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Calendar</TooltipContent>
</Tooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
className="h-8 w-8"
aria-label="Quick actions"
>
<Plus className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuItem onClick={() => onQuickAction?.("task")}>
<CheckSquare className="size-4" />
New Task
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onQuickAction?.("doc")}>
<FileText className="size-4" />
New Doc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onQuickAction?.("whiteboard")}>
<Presentation className="size-4" />
New Whiteboard
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onQuickAction?.("space")}>
<LayoutGrid className="size-4" />
New Space
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
className="relative h-8 w-8 rounded-full p-0"
aria-label="Account menu"
>
<Avatar className="h-8 w-8">
<AvatarFallback className="text-xs">U</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onSelect={() => {}}>
<User className="size-4" />
Account
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() =>
workspaceId && router.push(`/${workspaceId}/settings`)
}
>
<Settings className="size-4" />
Settings
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => signOut({ callbackUrl: "/sign-in" })}
>
<LogOut className="size-4" />
Sign Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
);
}