refactor(frontend): remove classic dashboard chrome (topbar/navrail/mobilenav)
This commit is contained in:
@@ -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.
|
* ConstellationFrame — replaces the classic AppFrame chrome.
|
||||||
* The stage canvas sits fixed behind everything; page content is an
|
* The stage canvas sits fixed behind everything; page content is an
|
||||||
* overlay layer (absolute, no top bar / nav rail / scroll shell).
|
* 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 { usePathname } from "next/navigation";
|
||||||
import { type ReactNode, useMemo } from "react";
|
import { type ReactNode, useMemo } from "react";
|
||||||
|
import { MiniPlayer } from "@/components/media/mini-player";
|
||||||
import { ConstellationStage } from "@/components/shell/constellation-stage";
|
import { ConstellationStage } from "@/components/shell/constellation-stage";
|
||||||
import { FloatingChrome } from "@/components/shell/floating-chrome";
|
import { FloatingChrome } from "@/components/shell/floating-chrome";
|
||||||
import { resolveScene, type SceneSeed } from "@/components/shell/route-scenes";
|
import { resolveScene, type SceneSeed } from "@/components/shell/route-scenes";
|
||||||
@@ -38,6 +39,7 @@ export function ConstellationFrame({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FloatingChrome />
|
<FloatingChrome />
|
||||||
|
<MiniPlayer />
|
||||||
{/* Overlay content region — scenes place floating panels inside. */}
|
{/* Overlay content region — scenes place floating panels inside. */}
|
||||||
<main className="pointer-events-none absolute inset-0 z-10 overflow-y-auto overscroll-contain">
|
<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>
|
<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 { ConstellationFrame } from "./constellation-frame";
|
||||||
export { ConnectionStatus } from "./status-dot";
|
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user