Add animated glass background, breadcrumbs, and brand mark polish

Cards now sit on an animated signature-gradient background and use a
translucent, blurred bp-glass surface so it shines through; extended
bp-glass to every stat surface on the Stats page and to its tab switcher.
Adds a Breadcrumbs component below the header on all app pages.

Also: drop the gradient text fill from the wordmark/heading, animate the
BrandMark's heart and curve on the dashboard hero instead of a plain pulse
ring, recenter the heart/curve artwork in icon.svg and BrandMark, close the
mobile nav menu on link click, remove the redundant "back to recordings"
button, and match the "Start session"/"Scan for devices" button sizes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 18:47:30 +02:00
co-authored by Claude Sonnet 5
parent 9e0380ec75
commit c46adf788f
14 changed files with 248 additions and 93 deletions
+19 -13
View File
@@ -3,10 +3,12 @@ import { cn } from "@/lib/utils";
interface BrandMarkProps {
size?: number;
className?: string;
/** Beats the heart glyph and makes the curve glow - used for the dashboard hero mark. */
animated?: boolean;
}
/** Inline twin of app/icon.svg (the favicon) - kept as markup, not an <img>, so it can be sized/animated with CSS. */
export function BrandMark({ size = 28, className }: BrandMarkProps) {
export function BrandMark({ size = 28, className, animated = false }: BrandMarkProps) {
return (
<svg
width={size}
@@ -25,18 +27,22 @@ export function BrandMark({ size = 28, className }: BrandMarkProps) {
</linearGradient>
</defs>
<rect x="1" y="1" width="30" height="30" rx="9" fill="url(#bpGradMark)" />
<path
d="M16 22.6c-.3 0-.5-.1-.7-.3C11.7 19 8 15.6 8 12.3 8 9.9 9.9 8 12.3 8c1.3 0 2.6.6 3.4 1.7.1.1.2.1.3 0 .8-1.1 2.1-1.7 3.4-1.7C21.8 8 23.7 9.9 23.7 12.3c0 3.3-3.7 6.7-7.3 10-.2.2-.4.3-.7.3z"
fill="rgba(255,255,255,0.22)"
/>
<polyline
points="6,15 10,15 12,9 15,21 18,13 20,15 26,15"
fill="none"
stroke="#ffffff"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
/>
<g transform="translate(0 0.7)">
<path
d="M16 22.6c-.3 0-.5-.1-.7-.3C11.7 19 8 15.6 8 12.3 8 9.9 9.9 8 12.3 8c1.3 0 2.6.6 3.4 1.7.1.1.2.1.3 0 .8-1.1 2.1-1.7 3.4-1.7C21.8 8 23.7 9.9 23.7 12.3c0 3.3-3.7 6.7-7.3 10-.2.2-.4.3-.7.3z"
fill="rgba(255,255,255,0.22)"
className={cn(animated && "bp-mark-heart")}
/>
<polyline
points="6,15 10,15 12,9 15,21 18,13 20,15 26,15"
fill="none"
stroke="#ffffff"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
className={cn(animated && "bp-mark-curve")}
/>
</g>
</svg>
);
}
+55
View File
@@ -0,0 +1,55 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ChevronRight, House } from "lucide-react";
const SECTION_LABELS: Record<string, string> = {
control: "Control",
recordings: "Recordings",
sessions: "Sessions",
stats: "Stats",
devices: "Devices",
};
const LEAF_LABELS: Record<string, string> = {
replay: "Replay",
};
export function Breadcrumbs() {
const pathname = usePathname();
const segments = pathname.split("/").filter(Boolean);
if (segments.length === 0) {
return null;
}
const crumbs = segments.reduce<{ label: string; href: string }[]>((acc, segment) => {
const href = `${acc.at(-1)?.href ?? ""}/${segment}`;
const label = /^\d+$/.test(segment) ? `#${segment}` : (LEAF_LABELS[segment] ?? SECTION_LABELS[segment] ?? segment);
return [...acc, { label, href }];
}, []);
return (
<nav aria-label="Breadcrumb" className="bp-glass mb-6 flex w-fit items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm text-muted-foreground">
<Link href="/" className="flex items-center hover:text-foreground" aria-label="Dashboard">
<House className="size-3.5" />
</Link>
{crumbs.map((crumb, i) => {
const isLast = i === crumbs.length - 1;
return (
<span key={crumb.href} className="flex items-center gap-1.5">
<ChevronRight className="size-3.5 text-muted-foreground/50" />
{isLast ? (
<span className="font-medium text-foreground">{crumb.label}</span>
) : (
<Link href={crumb.href} className="hover:text-foreground">
{crumb.label}
</Link>
)}
</span>
);
})}
</nav>
);
}
+5 -3
View File
@@ -1,5 +1,6 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useTheme } from "next-themes";
@@ -45,6 +46,7 @@ function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
export function NavBar() {
const { theme, setTheme } = useTheme();
const router = useRouter();
const [menuOpen, setMenuOpen] = useState(false);
async function handleLogout() {
await fetch("/api/auth/logout", { method: "POST" });
@@ -57,7 +59,7 @@ export function NavBar() {
<div className="mx-auto flex h-14 max-w-6xl items-center gap-3 px-4">
<Link href="/" className="mr-2 flex items-center gap-2">
<BrandMark size={26} />
<span className="font-heading bp-gradient-text text-lg font-semibold">Sexy</span>
<span className="font-heading text-lg font-semibold">Sexy</span>
</Link>
<nav className="hidden items-center gap-1 md:flex">
@@ -79,7 +81,7 @@ export function NavBar() {
<LogOut className="size-4" />
</Button>
<Sheet>
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden" aria-label="Open menu">
<Menu className="size-4" />
@@ -88,7 +90,7 @@ export function NavBar() {
<SheetContent side="right" className="w-64">
<SheetTitle className="px-4 pt-4">Menu</SheetTitle>
<nav className="flex flex-col gap-1 p-4">
<NavLinks />
<NavLinks onNavigate={() => setMenuOpen(false)} />
</nav>
</SheetContent>
</Sheet>