import { Suspense, lazy, useEffect, useState, type ReactNode } from "react"; import { TenantProvider } from "@/components/tenant-provider"; import { QueryCachePersister } from "@/components/query-cache-persister"; import { OutboxSyncRunner } from "@/components/outbox-sync-runner"; import { OfflineMutationRunner } from "@/components/offline-mutation-runner"; import { ConflictNotice } from "@/components/conflict-notice"; import { ConflictBanner } from "@/components/conflict-banner"; import { ConflictResolutionDialogHost } from "@/components/conflict-resolution-dialog"; import { OfflinePassphraseGate } from "@/components/offline-passphrase-gate"; import { useLocationCollector } from "@/lib/geolocation"; import { CimbarRoot } from "@/components/cimbar"; import { AccessibilityRoot } from "@/components/accessibility/accessibility-root"; import { SkipToContent } from "@/components/accessibility/skip-to-content"; import { AccessibilityOnboardingNudge } from "@/components/accessibility/accessibility-onboarding-nudge"; function LocationCollectorRunner() { useLocationCollector(); return null; } /** * Task #1448 — defers mounting non-essential post-auth runners until * `requestIdleCallback` (with a `setTimeout` fallback) fires. The * outbox sync runner, snapshot primer, location collector, and * accessibility onboarding nudge all start work as soon as they're * mounted, contending with the dashboard's own initial queries. * Holding them back one idle tick gives the destination route's * first paint clear bandwidth. */ function IdleMount({ children }: { children: ReactNode }) { const [mounted, setMounted] = useState(false); useEffect(() => { type RICWindow = Window & { requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => number; cancelIdleCallback?: (id: number) => void; }; const w = window as RICWindow; if (typeof w.requestIdleCallback === "function") { const id = w.requestIdleCallback(() => setMounted(true), { timeout: 1500 }); return () => w.cancelIdleCallback?.(id); } const t = window.setTimeout(() => setMounted(true), 1); return () => window.clearTimeout(t); }, []); if (!mounted) return null; return <>{children}; } // Snapshot priming sentinel (Task #390). Lazy-loaded so it doesn't // inflate the authed-shell chunk for tenants that have already primed // their caches in a previous session. The progress indicator has been // moved into AppLayout's status bar (SnapshotProgressInline) so it // appears as inline text rather than a floating pill. const SnapshotPrimer = lazy(() => import("@/components/snapshot-primer").then((m) => ({ default: m.SnapshotPrimer, })), ); /** * Single lazy-loaded "authed shell" boundary. * * Bundles every provider and sentinel that is only meaningful once * the user has signed in: * * - {@link TenantProvider} — fetches `/api/tenants/me` and exposes * the active tenant context. * - {@link QueryCachePersister} — wires React Query to IndexedDB, * scoped per (user, tenant). * - {@link OutboxSyncRunner} — drains the offline mutation outbox. * - {@link ConflictNotice} — surfaces the toast when a conflict is * detected. * - {@link ConflictResolutionDialogHost} — owns the side-by-side * conflict resolution dialog. * - {@link SnapshotPrimer} — primes detail-view caches for matters, * contacts, firms, and interactions on first sign-in per (user, * tenant) (Task #390). * * All of these used to be either eagerly imported (TenantProvider) or * lazy-loaded as separate chunks. Folding them into one chunk that's * only fetched on the first signed-in render means a not-yet- * authenticated visitor's cold load doesn't pay for any of them, and * a signed-in user only pays the cost once per device per release. * * The inner `` is a defence-in-depth fallback in case any * of the children themselves trigger a follow-on lazy import (the * snapshot sentinels are themselves lazy()). */ export function AuthedShell({ children }: { children: React.ReactNode }) { return ( {/* QueryCachePersister stays outside the gate because it owns the unlock side-effect: when the gate calls unlockWithPassphrase, the persister must be mounted to react to the resulting state change and attach the DEK. */} {/* ConflictNotice / ConflictResolutionDialogHost must mount eagerly because they own toast surfaces that could fire from a server-side conflict the moment the dashboard's first round of queries returns. */} {/* Task #1448 — everything below runs after the destination route's first paint so its initial queries don't fight the outbox / snapshot / nudges for bandwidth and main-thread time. */} {children} ); } export default AuthedShell;