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"; "use client";
import { useTheme } from "@wrksz/themes/client"; import { useTheme } from "@wrksz/themes/client";
import { useSyncExternalStore } from "react"; 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 { theme, setTheme } = useTheme();
const mounted = useSyncExternalStore( const mounted = useSyncExternalStore(
() => () => undefined, () => () => undefined,
@ -11,18 +20,33 @@ export const ThemeChanger = () => {
); );
if (!mounted) { 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 ( return (
<div> <div data-testid="theme-switcher" className={cn("flex items-center gap-1", className)}>
<p>The current theme is: {theme}</p> <Button
<button type="button" onClick={() => setTheme("light")}> variant={theme === "light" ? "default" : "ghost"}
Light Mode size="icon"
</button> onClick={() => setTheme("light")}
<button type="button" onClick={() => setTheme("dark")}> aria-label="Light mode"
Dark Mode >
</button> <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> </div>
); );
}; }