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:
parent
c8cf9b1573
commit
e8d557b0d2
3 changed files with 93 additions and 11 deletions
44
src/components/core/AppNav.tsx
Normal file
44
src/components/core/AppNav.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/components/core/AppShell.tsx
Normal file
14
src/components/core/AppShell.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue