Files
multica/packages/views/layout/dashboard-layout.tsx
Naiyuan Qing e8d6c912c4 feat(views): prefetch + transition + skeleton for snappy web navigation (MUL-2269) (#2677)
Internal navigation on web feels laggy because clicking a sidebar link blocks
0.2–0.6s with zero visual feedback — no prefetch, no Suspense fallback in the
dashboard segment, and no React transition to mark the route commit as pending.

This change adds the three pieces App Router needs to make the click→commit
window feel instant, scoped to the (dashboard) segment so auth/landing keep
their existing chrome:

- NavigationAdapter gains an optional prefetch(path). The web adapter wires
  it to router.prefetch; desktop leaves it undefined (react-router has no
  equivalent and doesn't need one). AppLink prefetches on hover/focus and
  preserves caller-supplied onMouseEnter/onFocus/onClick.
- NavigationProvider wraps push/replace in useTransition and exposes the
  pending flag via useIsNavigating(). Every useNavigation().push caller —
  sidebar AppLink, command palette, post-create modal jumps — picks this up
  automatically.
- New apps/web/app/[workspaceSlug]/(dashboard)/loading.tsx renders a minimal
  skeleton during cold transitions inside the dashboard segment only.
- DashboardLayout renders a 1px top progress bar driven by useIsNavigating.

packages/views remains free of next/* imports; desktop is unaffected by
construction (no prefetch, transition flips quickly, no loading.tsx).

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 17:01:42 +08:00

48 lines
1.4 KiB
TypeScript

"use client";
import type { ReactNode } from "react";
import { SidebarProvider, SidebarInset } from "@multica/ui/components/ui/sidebar";
import { ModalRegistry } from "../modals/registry";
import { AppSidebar } from "./app-sidebar";
import { DashboardGuard } from "./dashboard-guard";
import { NavigationProgress } from "./navigation-progress";
import { WorkspacePresencePrefetch } from "./workspace-presence-prefetch";
interface DashboardLayoutProps {
children: ReactNode;
/** Rendered inside SidebarInset (e.g. ChatWindow, ChatFab — absolute-positioned overlays) */
extra?: ReactNode;
/** Rendered inside sidebar header as a search trigger */
searchSlot?: ReactNode;
/** Loading indicator */
loadingIndicator?: ReactNode;
}
export function DashboardLayout({
children,
extra,
searchSlot,
loadingIndicator,
}: DashboardLayoutProps) {
return (
<DashboardGuard
loadingFallback={
<div className="flex h-svh items-center justify-center">
{loadingIndicator}
</div>
}
>
<SidebarProvider className="h-svh">
<WorkspacePresencePrefetch />
<AppSidebar searchSlot={searchSlot} />
<SidebarInset className="relative overflow-hidden">
<NavigationProgress />
{children}
<ModalRegistry />
{extra}
</SidebarInset>
</SidebarProvider>
</DashboardGuard>
);
}