- Archive dormant apps (daemon, desktop, native, tui) and superseded packages/server into repos/ for reference - Archive 21 dead web components (chat page shell, work-os cluster, strays) into repos/web/; keep reused message-rendering primitives in components/chat - Merge @code/work-os into @code/primitives; work.ts absorbed via ./work subpath and barrel export; update all four importers - Add docs/futures.md tracking audio/video (blocked at Flue + provider), web layout cleanup, and message rendering quality - Repoint dev:zopu script to @code/agents (the live Flue server) - Note repos/ reference-code convention in AGENTS.md
113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
import { Button } from "@code/ui/components/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@code/ui/components/dropdown-menu";
|
|
import { ChevronDown, Circle, FolderGit2, Zap } from "lucide-react";
|
|
|
|
import UserMenu from "@/components/user-menu";
|
|
|
|
interface ProjectOption {
|
|
readonly id: string;
|
|
readonly name: string;
|
|
readonly host?: string;
|
|
}
|
|
|
|
interface WorkOsHeaderProps {
|
|
readonly projects: readonly ProjectOption[] | undefined;
|
|
readonly selectedProject: ProjectOption | null;
|
|
readonly onSelectProject: (id: string) => void;
|
|
readonly connected: boolean;
|
|
readonly onBack?: () => void;
|
|
readonly showBack: boolean;
|
|
}
|
|
|
|
const StatusDot = ({ connected }: { readonly connected: boolean }) => (
|
|
<span className="inline-flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
<Circle
|
|
className={`size-2 ${connected ? "fill-emerald-500 text-emerald-500" : "fill-amber-500 text-amber-500"}`}
|
|
/>
|
|
{connected ? "Connected" : "Connecting"}
|
|
</span>
|
|
);
|
|
|
|
export const WorkOsHeader = ({
|
|
connected,
|
|
onBack,
|
|
onSelectProject,
|
|
projects,
|
|
selectedProject,
|
|
showBack,
|
|
}: WorkOsHeaderProps) => (
|
|
<header className="flex items-center justify-between gap-3 border-b border-border/40 bg-background/80 px-4 py-3 backdrop-blur-xl sm:px-6">
|
|
<div className="flex min-w-0 items-center gap-3">
|
|
{showBack && onBack ? (
|
|
<Button
|
|
className="shrink-0"
|
|
onClick={onBack}
|
|
size="sm"
|
|
type="button"
|
|
variant="ghost"
|
|
>
|
|
<ChevronDown className="size-4 rotate-90" />
|
|
</Button>
|
|
) : null}
|
|
<div className="grid size-9 shrink-0 place-items-center rounded-xl bg-foreground text-background">
|
|
<Zap className="size-4 fill-current" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<p className="text-sm font-semibold tracking-tight">Zopu</p>
|
|
<StatusDot connected={connected} />
|
|
</div>
|
|
{selectedProject ? (
|
|
<p className="truncate text-[11px] text-muted-foreground">
|
|
{selectedProject.name}
|
|
</p>
|
|
) : (
|
|
<p className="text-[11px] text-muted-foreground">No project</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{projects && projects.length > 1 ? (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger>
|
|
<Button size="sm" type="button" variant="ghost">
|
|
<FolderGit2 className="size-3.5" />
|
|
<span className="max-w-32 truncate">
|
|
{selectedProject?.name ?? "Select"}
|
|
</span>
|
|
<ChevronDown className="size-3.5 opacity-50" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuLabel>Projects</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
{projects.map((project) => (
|
|
<DropdownMenuItem
|
|
key={project.id}
|
|
onClick={() => onSelectProject(project.id)}
|
|
>
|
|
<FolderGit2 className="size-3.5" />
|
|
<span className="truncate">{project.name}</span>
|
|
{project.host ? (
|
|
<span className="ml-auto text-[10px] text-muted-foreground">
|
|
{project.host}
|
|
</span>
|
|
) : null}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : null}
|
|
<UserMenu />
|
|
</div>
|
|
</header>
|
|
);
|