refactor(frontend): remove classic dashboard chrome (topbar/navrail/mobilenav)

This commit is contained in:
asepharyana
2026-08-24 15:07:40 +07:00
parent a9e09c38e9
commit 1fafebb16d
6 changed files with 3 additions and 212 deletions
@@ -1,30 +0,0 @@
import { MiniPlayer } from "@/components/media/mini-player";
import { MobileNav } from "./mobile-nav";
import { NavRail } from "./nav-rail";
import { TopBar } from "./topbar";
/**
* App chrome: slim nav rail + sticky top bar + scrollable content region.
* Sits above the fixed AmbientCanvas. Providers (Ambient + WS) are mounted in
* the route layout so every page shares one live link and signal context.
*
* < md the side rail collapses (hidden) and a bottom tab bar (MobileNav)
* takes over navigation; the content region gains bottom padding so the last
* panel never hides behind the dock. A persistent MiniPlayer floats at the
* bottom-right whenever a media track is loaded outside /media.
*/
export function AppFrame({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-dvh w-full overflow-hidden">
<NavRail />
<div className="flex min-w-0 flex-1 flex-col">
<TopBar />
<main className="min-h-0 flex-1 overflow-y-auto px-4 pb-[calc(2rem+env(safe-area-inset-bottom))] pt-4 sm:px-5 sm:pb-[calc(2.5rem+env(safe-area-inset-bottom))]">
{children}
</main>
</div>
<MobileNav />
<MiniPlayer />
</div>
);
}
@@ -4,10 +4,11 @@
* ConstellationFrame — replaces the classic AppFrame chrome.
* The stage canvas sits fixed behind everything; page content is an
* overlay layer (absolute, no top bar / nav rail / scroll shell).
* MiniPlayer + Chatbot + CommandPalette keep mounting at the layout level.
* Chatbot + CommandPalette keep mounting at the layout level.
*/
import { usePathname } from "next/navigation";
import { type ReactNode, useMemo } from "react";
import { MiniPlayer } from "@/components/media/mini-player";
import { ConstellationStage } from "@/components/shell/constellation-stage";
import { FloatingChrome } from "@/components/shell/floating-chrome";
import { resolveScene, type SceneSeed } from "@/components/shell/route-scenes";
@@ -38,6 +39,7 @@ export function ConstellationFrame({
}}
/>
<FloatingChrome />
<MiniPlayer />
{/* Overlay content region — scenes place floating panels inside. */}
<main className="pointer-events-none absolute inset-0 z-10 overflow-y-auto overscroll-contain">
<div className="pointer-events-auto min-h-full">{children}</div>
@@ -1,3 +1,2 @@
export { AppFrame } from "./ambient-app";
export { ConstellationFrame } from "./constellation-frame";
export { ConnectionStatus } from "./status-dot";
@@ -1,43 +0,0 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { isActivePath, mobileNavItems } from "@/lib/navigation";
import { cn } from "@/lib/utils";
/**
* Mobile bottom tab bar. Shown only < md (the side NavRail is hidden there).
* Mirrors the desktop nav items but as a thumb-friendly dock with labels and a
* top active indicator. Safe-area aware for notched devices.
*/
export function MobileNav() {
const path = usePathname() ?? "/";
return (
<nav className="glass fixed inset-x-0 bottom-0 z-40 flex items-stretch justify-around rounded-t-[20px] px-2 pb-[calc(0.4rem+env(safe-area-inset-bottom))] pt-2 md:hidden">
{mobileNavItems.map((item) => {
const active = isActivePath(path, item.matchPrefix);
return (
<Link
key={item.href}
href={item.href}
aria-label={item.label}
aria-current={active ? "page" : undefined}
className={cn(
"relative flex flex-1 flex-col items-center justify-center rounded-[12px] rounded-t-[20px] px-2 pb-[calc(0.4rem+env(safe-area-inset-bottom))] pt-2 md:hidden",
active
? "bg-signal/15 text-signal"
: "text-ink-faint hover:bg-white/5 hover:text-ink-soft",
)}
>
{active && (
<span className="absolute -top-2 h-8 w-8 rounded-full bg-signal shadow-[0_0_12px_var(--color-signal-glow)]" />
)}
<item.icon className="size-[20px]" strokeWidth={active ? 2.4 : 2} />
{item.label}
</Link>
);
})}
</nav>
);
}
@@ -1,61 +0,0 @@
"use client";
import { usePathname } from "next/navigation";
import { isActivePath, navItems } from "@/lib/navigation";
import { cn } from "@/lib/utils";
function NavItem({
href,
label,
active,
Icon,
}: {
href: string;
label: string;
active: boolean;
Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
}) {
return (
<a
href={href}
aria-label={label}
aria-current={active ? "page" : undefined}
className={cn(
"group relative flex size-11 items-center justify-center rounded-[13px] transition-all",
active
? "bg-signal/15 text-signal"
: "text-ink-faint hover:bg-white/5 hover:text-ink-soft",
)}
>
{active && (
<span className="absolute -left-3 h-6 w-1 rounded-full bg-signal shadow-[0_0_12px_var(--color-signal-glow)]" />
)}
<Icon className="size-[18px]" strokeWidth={active ? 2.4 : 2} />
{/* hover tooltip — labels are hidden in the rail, so surface on hover */}
<span className="pointer-events-none absolute left-full z-50 ml-3 hidden whitespace-nowrap rounded-[9px] border border-hairline bg-canvas-2 px-2.5 py-1.5 text-xs font-medium text-ink-soft opacity-0 shadow-lg transition-opacity group-hover:opacity-100 md:block">
{label}
</span>
</a>
);
}
export function NavRail() {
const pathname = usePathname();
const path = pathname ?? "/";
return (
<nav className="glass mb-[calc(0.75rem+env(safe-area-inset-bottom))] ml-[calc(0.75rem+env(safe-area-inset-left))] mt-[calc(0.75rem+env(safe-area-inset-top))] hidden w-[68px] flex-col items-center gap-1 rounded-[18px] py-4 md:flex">
<div className="flex flex-1 flex-col gap-1">
{navItems.map((item) => (
<NavItem
key={item.href}
href={item.href}
label={item.label}
active={isActivePath(path, item.matchPrefix)}
Icon={item.icon}
/>
))}
</div>
</nav>
);
}
@@ -1,76 +0,0 @@
"use client";
import { Moon, Sun } from "lucide-react";
import { usePathname } from "next/navigation";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { useAmbient } from "@/components/ambient/ambient-context";
import { navItems } from "@/lib/navigation";
import { cn } from "@/lib/utils";
import { ConnectionStatus } from "./status-dot";
function useActiveLabel() {
const pathname = usePathname();
const item = [...navItems]
.sort((a, b) => b.matchPrefix.length - a.matchPrefix.length)
.find((i) => pathname.startsWith(i.matchPrefix));
return item?.label ?? "Console";
}
export function TopBar() {
const label = useActiveLabel();
const { state } = useAmbient();
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const signalTone =
state.tone === "vermilion"
? "text-vermilion"
: state.tone === "amber"
? "text-amber"
: "text-signal";
return (
<header className="sticky top-0 z-40 flex items-center gap-3 px-4 py-3 pt-[calc(0.75rem+env(safe-area-inset-top))] sm:gap-4 sm:px-5 sm:py-3.5">
<div className="flex min-w-0 items-baseline gap-2 sm:gap-3">
<span className="eyebrow hidden sm:inline">GMW</span>
<h1 className="display truncate text-[1.25rem] text-ink sm:text-[1.5rem]">
{label}
</h1>
</div>
<div className="ml-auto flex items-center gap-2 sm:gap-3">
<span className={cn("pill hidden sm:flex", signalTone)}>
<span
className={cn("size-1.5 rounded-full bg-current animate-breathe")}
/>
{state.label ?? "nominal"}
</span>
<ConnectionStatus compact />
<button
type="button"
aria-label="Open command palette"
onClick={() =>
window.dispatchEvent(new Event("command-palette:open"))
}
className="hidden items-center gap-1.5 rounded-[11px] border border-hairline bg-white/5 px-2.5 py-1.5 text-xs text-ink-soft transition-colors hover:text-ink hover:border-signal/40 sm:flex"
>
<span className="mono text-[0.65rem]">K</span>
</button>
<button
type="button"
aria-label="Toggle theme"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
className="flex size-9 items-center justify-center rounded-[11px] border border-hairline bg-white/5 text-ink-soft transition-colors hover:text-ink hover:border-signal/40"
>
{mounted && theme === "light" ? (
<Moon className="size-4" />
) : (
<Sun className="size-4" />
)}
</button>
</div>
</header>
);
}