feat(shell): add shared app shell primitives

Add AppShell, AppNav components and refactor ThemeChanger for header use

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
nxtkofi 2026-04-21 23:26:21 +02:00
parent c8cf9b1573
commit e8d557b0d2
3 changed files with 93 additions and 11 deletions

View file

@ -0,0 +1,44 @@
"use client";
import Link from "next/link";
import { useTranslations } from "next-intl";
import { routes } from "@/lib/routes";
import { ThemeChanger } from "./ThemeChanger";
import { AuthNavActions } from "./AuthNavActions";
import { cn } from "@/lib/utils";
interface AppNavProps {
className?: string;
}
export function AppNav({ className }: AppNavProps) {
const t = useTranslations("Navigation");
return (
<header
data-testid="app-nav"
className={cn(
"border-b bg-background sticky top-0 z-50",
className
)}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
<div className="flex items-center">
<Link
href={routes.public.home}
className="text-xl font-bold text-foreground hover:text-foreground/80 transition-colors"
>
{t("Home")}
</Link>
</div>
<div className="flex items-center gap-4">
<AuthNavActions />
<ThemeChanger />
</div>
</div>
</div>
</header>
);
}

View file

@ -0,0 +1,14 @@
import { cn } from "@/lib/utils";
interface AppShellProps {
children: React.ReactNode;
className?: string;
}
export function AppShell({ children, className }: AppShellProps) {
return (
<div data-testid="app-shell" className={cn("min-h-screen flex flex-col", className)}>
{children}
</div>
);
}

View file

@ -1,8 +1,17 @@
"use client";
import { useTheme } from "@wrksz/themes/client";
import { useSyncExternalStore } from "react";
import { Button } from "@/components/ui/button";
import { HugeiconsIcon } from "@hugeicons/react";
import { Sun01Icon, Moon02Icon } from "@hugeicons/core-free-icons";
import { cn } from "@/lib/utils";
export const ThemeChanger = () => {
interface ThemeChangerProps {
className?: string;
}
export function ThemeChanger({ className }: ThemeChangerProps) {
const { theme, setTheme } = useTheme();
const mounted = useSyncExternalStore(
() => () => undefined,
@ -11,18 +20,33 @@ export const ThemeChanger = () => {
);
if (!mounted) {
return <p>Loading theme...</p>;
return (
<div data-testid="theme-switcher" className={cn("flex items-center gap-1", className)}>
<Button variant="ghost" size="icon" disabled>
<HugeiconsIcon icon={Sun01Icon} className="h-4 w-4" />
</Button>
</div>
);
}
return (
<div>
<p>The current theme is: {theme}</p>
<button type="button" onClick={() => setTheme("light")}>
Light Mode
</button>
<button type="button" onClick={() => setTheme("dark")}>
Dark Mode
</button>
<div data-testid="theme-switcher" className={cn("flex items-center gap-1", className)}>
<Button
variant={theme === "light" ? "default" : "ghost"}
size="icon"
onClick={() => setTheme("light")}
aria-label="Light mode"
>
<HugeiconsIcon icon={Sun01Icon} className="h-4 w-4" />
</Button>
<Button
variant={theme === "dark" ? "default" : "ghost"}
size="icon"
onClick={() => setTheme("dark")}
aria-label="Dark mode"
>
<HugeiconsIcon icon={Moon02Icon} className="h-4 w-4" />
</Button>
</div>
);
};
}