feat(ui): the five pages + shell — console-grade @unom/app-ui chrome
PluginShell over the kit router (flat routes, console deep-link bridge), standalone header only outside the iframe. Every subscription renders through a shared Gate (kit ResultGate + colocated PageSkeleton + retry error EmptyState). Overview: first-run hero, tone-coded StatCards, sync button that tracks engine state, warnings accordion, live EventFeed. Library: @container-responsive cover grid with whileInView stagger, include Switches saving gameOverrides immediately, skipped/excluded card with re-include. Sources: PathListEditor over draft.roots with platform Select + excludes globs. Emulators: detected roster with via/ contested/cores badges + re-detect, per-platform launch overrides. Settings: artwork/sync/files groups over resolved defaults, sticky SaveBar (motion slide-up on dirty; entry variants neutralized inside the object-form motion wrapper so the button isn't stuck at opacity 0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// The app frame: kit router (flat routes, console deep-link bridge) inside the shared
|
||||
// PluginShell chrome. Pages own their data; the shell owns navigation + the toaster.
|
||||
|
||||
import { createPluginRouter, useIsEmbedded } from "@punktfunk/plugin-kit/react";
|
||||
import {
|
||||
PluginShell,
|
||||
type PluginShellNavItem,
|
||||
} from "@unom/app-ui/plugin-shell";
|
||||
import {
|
||||
FolderOpen,
|
||||
Gamepad2,
|
||||
LayoutDashboard,
|
||||
Library,
|
||||
Settings2,
|
||||
} from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { EmulatorsPage } from "@/pages/emulators";
|
||||
import { LibraryPage } from "@/pages/library";
|
||||
import { OverviewPage } from "@/pages/overview";
|
||||
import { SettingsPage } from "@/pages/settings";
|
||||
import { SourcesPage } from "@/pages/sources";
|
||||
import { type PageProps, ROUTES, type Route } from "@/routes";
|
||||
|
||||
const { usePluginRoute } = createPluginRouter(ROUTES, "overview");
|
||||
|
||||
const NAV: ReadonlyArray<PluginShellNavItem> = [
|
||||
{ id: "overview", label: "Overview", icon: LayoutDashboard },
|
||||
{ id: "library", label: "Library", icon: Library },
|
||||
{ id: "sources", label: "Sources", icon: FolderOpen },
|
||||
{ id: "emulators", label: "Emulators", icon: Gamepad2 },
|
||||
{ id: "settings", label: "Settings", icon: Settings2 },
|
||||
];
|
||||
|
||||
const PAGES: Record<Route, FC<PageProps>> = {
|
||||
overview: OverviewPage,
|
||||
library: LibraryPage,
|
||||
sources: SourcesPage,
|
||||
emulators: EmulatorsPage,
|
||||
settings: SettingsPage,
|
||||
};
|
||||
|
||||
/** Only shown in a standalone tab — embedded in the console, the console is the chrome. */
|
||||
const StandaloneHeader = () => (
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
||||
<Gamepad2 className="size-4 text-primary" />
|
||||
ROM Manager
|
||||
</div>
|
||||
);
|
||||
|
||||
export const App = () => {
|
||||
const { route, navigate } = usePluginRoute();
|
||||
const embedded = useIsEmbedded();
|
||||
const Page = PAGES[route];
|
||||
return (
|
||||
<PluginShell
|
||||
nav={NAV}
|
||||
active={route}
|
||||
onNavigate={(id) => navigate(id as Route)}
|
||||
standaloneHeader={embedded ? undefined : <StandaloneHeader />}
|
||||
>
|
||||
<Page navigate={navigate} />
|
||||
</PluginShell>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user