Files
wehub-resource-sync e30e75b5d4
Code Quality / Oxlint + Oxfmt (push) Waiting to run
Code Quality / Template Sync (push) Waiting to run
Code Quality / Build Changed Packages (push) Waiting to run
Code Quality / Test Changed Packages (push) Waiting to run
Deploy Expo Example / Deploy Production (push) Waiting to run
Deploy Ink Example / Deploy Production (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.12) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Waiting to run
Deploy Shadcn Registry / Deploy Production (push) Waiting to run
Template Metrics / LOC + Bundle Size (push) Waiting to run
Changesets / Create Version PR (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

167 lines
5.1 KiB
TypeScript

"use client";
import { type ReactNode, useMemo } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { GitHubIcon } from "@/components/icons/github";
import { Select } from "@/components/assistant-ui/select";
import { SUB_PROJECTS } from "@/lib/constants";
import { ThemeToggle } from "./theme-toggle";
import { HeaderBrandLink } from "./header-brand-link";
interface BreadcrumbItem {
label: string;
href: string;
shimmer?: boolean;
}
interface SubProjectLayoutProps {
name: string;
githubPath: string;
breadcrumbs?: BreadcrumbItem[];
children: ReactNode;
hideFooter?: boolean;
fullHeight?: boolean;
}
export function SubProjectLayout({
name,
githubPath,
breadcrumbs: breadcrumbsOverride,
children,
hideFooter = false,
fullHeight = false,
}: SubProjectLayoutProps): React.ReactElement {
const pathname = usePathname();
const router = useRouter();
const breadcrumbs = useMemo(() => {
if (breadcrumbsOverride) {
return breadcrumbsOverride;
}
const basePath = `/${name}`;
if (!pathname.startsWith(basePath) || pathname === basePath) {
return [];
}
const subPath = pathname.slice(basePath.length);
const segments = subPath.split("/").filter(Boolean);
return segments.map((segment, index) => ({
label: segment,
href: `${basePath}/${segments.slice(0, index + 1).join("/")}`,
shimmer: false,
}));
}, [pathname, name, breadcrumbsOverride]);
return (
<div
className={cn(
"flex flex-col",
fullHeight ? "h-svh overflow-hidden" : "min-h-screen",
)}
>
<header
className={cn("z-50 w-full shrink-0", !fullHeight && "sticky top-0")}
>
{!fullHeight && (
<div className="from-background pointer-events-none absolute inset-x-0 top-0 h-14 bg-linear-to-b to-transparent mask-[linear-gradient(to_bottom,black_75%,transparent)] backdrop-blur-xl" />
)}
<div
className={cn(
"relative flex h-12 w-full items-center justify-between px-4",
!fullHeight && "mx-auto max-w-7xl",
)}
>
<div className="flex min-w-0 items-center">
<HeaderBrandLink labelClassName="hidden sm:inline" />
<span className="text-muted-foreground/40 ml-3">/</span>
<Select
value={name}
onValueChange={(value) => router.push(`/${value}`)}
options={SUB_PROJECTS.toSorted((a, b) =>
a.slug.localeCompare(b.slug),
).map((p) => ({
value: p.slug,
label:
p.slug === "tw-shimmer" ? (
<span className="shimmer">{p.label}</span>
) : (
p.label
),
textValue: p.slug,
}))}
/>
<span className="hidden sm:contents">
{breadcrumbs?.map((item, index) => (
<span key={item.href} className="contents">
<span className="text-muted-foreground/40 mr-3 ml-1">/</span>
<Link
href={item.href}
className={cn(
"hover:text-foreground mr-2 text-sm transition-colors",
index === breadcrumbs.length - 1
? "text-foreground"
: "text-muted-foreground",
item.shimmer && "shimmer",
)}
>
{item.label}
</Link>
</span>
))}
</span>
</div>
<div className="flex items-center gap-2">
<div
data-sub-project-header-portal
className="flex items-center gap-1"
/>
<a
href={githubPath}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground flex size-8 items-center justify-center transition-colors"
aria-label="View on GitHub"
>
<GitHubIcon className="size-4" />
</a>
<ThemeToggle />
</div>
</div>
</header>
<main className={cn("flex-1", fullHeight && "min-h-0 overflow-hidden")}>
{children}
</main>
{!hideFooter && (
<footer className="relative px-4 py-8">
<div className="text-muted-foreground mx-auto flex max-w-7xl items-center justify-between text-sm">
<p>
By{" "}
<Link
href="/"
className="hover:text-foreground transition-colors"
>
assistant-ui
</Link>
</p>
<a
href="https://agentbase.dev"
target="_blank"
rel="noopener noreferrer"
className="text-foreground/30 hover:text-foreground text-xs transition-colors"
>
&copy; {new Date().getFullYear()} AgentbaseAI Inc.
</a>
</div>
</footer>
)}
</div>
);
}