Files
multica/apps/web/platform/navigation.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

49 lines
1.3 KiB
TypeScript

"use client";
import { Suspense } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import {
NavigationProvider,
type NavigationAdapter,
} from "@multica/views/navigation";
function NavigationProviderInner({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const adapter: NavigationAdapter = {
push: router.push,
replace: router.replace,
back: router.back,
pathname,
searchParams: new URLSearchParams(searchParams.toString()),
getShareableUrl: (path: string) =>
typeof window === "undefined" ? path : window.location.origin + path,
// router.prefetch is a no-op in dev mode by Next.js design; in production
// it warms the RSC payload + route chunk so the next push() commits with
// no network round-trip. Safe to call repeatedly — Next dedupes internally.
prefetch: (path: string) => {
router.prefetch(path);
},
};
return <NavigationProvider value={adapter}>{children}</NavigationProvider>;
}
export function WebNavigationProvider({
children,
}: {
children: React.ReactNode;
}) {
return (
<Suspense>
<NavigationProviderInner>{children}</NavigationProviderInner>
</Suspense>
);
}