import * as React from "react" import { Outlet, useLocation } from "react-router" import { AppSidebar, SidebarShell, SecondaryPanelProvider, SecondaryPanel, } from "@/components/sidebar" import { isExamLockPath } from "@/lib/exam-lock-shell" import { isPreAuthPath } from "@/lib/pre-auth-shell" import { SIDEBAR_STATE_COOKIE_NAME, sidebarDefaultOpenFromCookie, } from "@/lib/sidebar-state-cookie" import { ThemeProvider } from "@/components/theme-provider" import { AppThemeSync } from "@/hooks/use-app-theme" import { TabVisibilitySync } from "@/hooks/use-page-visible" import { TooltipProvider } from "@/components/ui/tooltip" import { ProductProviderRoot } from "@/components/product-provider-root" import { ProductRouteSync, useProductDashboardHref, useProductOrganizationSettingsHref, } from "@/contexts/product-route-sync" import { ProductSwitchOverlay } from "@/components/product-switch-overlay" import { ProductSwitchDialog, ProductSwitchProvider, } from "@/components/product-switch-dialog" import { ThemeColorSync } from "@/components/theme-color-sync" import { AppShellWiring } from "@/components/app-shell-wiring" import { AppShellContextProviders } from "./app-shell-providers" import { SystemBannerSlot } from "@/components/system-banner-slot" import { UtilityBarSlot } from "@/components/utility-bar-slot" import { ShellLayoutProvider, useShellLayout } from "@/contexts/shell-layout-context" import { hasFlushSidebar, isFullWidthUtilityBar, showsUtilityBar, } from "@/lib/shell-layout" import { CommandMenu } from "@/components/command-menu" import { buildCommandMenuConfig, type CommandMenuConfig } from "@/lib/command-menu-config" import { COMMAND_MENU_SEARCH_DATA_GROUPS } from "@/lib/command-menu-search-data" import { shouldShowOnboardingSidebarPreview, useBuilderOnboardingChrome, } from "@/lib/builder-onboarding-chrome" import { AskLeoDockedSlot, AskLeoFloatingSlot } from "@/components/ask-leo-shell" import { ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable" import { useRailLayoutTransition } from "@exxatdesignux/ui/hooks/use-rail-layout-transition" import { useAskLeoDockedPanelOpen } from "@/hooks/use-ask-leo-docked-panel-open" import { isProductsHomePath } from "@/lib/product-home" import { isSignInFlowsPath } from "@/lib/sign-in-flows-shell" import { isStudentHomePath } from "@/lib/student-shell" import { cn } from "@/lib/utils" /** * Vite-side root layout — replaces both `app/layout.tsx` (Next root) and * `app/(app)/layout.tsx` (Next signed-in app shell). * * Cookie reading: Next reads `cookies()` server-side. In Vite/SPA we read * `document.cookie` synchronously on first render — the API is identical * (default `true` if absent, otherwise the persisted state). No * hydration mismatch because there is no server render to mismatch * against. * * Provider order is preserved verbatim from the Next layout — these * contexts are tightly coupled to DS components (KeyMetrics ↔ Ask Leo, * SecondaryPanel ↔ Sidebar) and reorder would break runtime behaviour. * * The `` in the centre is where each route's element renders. */ function readCookie(name: string): string | undefined { if (typeof document === "undefined") return undefined const match = document.cookie .split("; ") .find(c => c.startsWith(`${name}=`)) return match ? decodeURIComponent(match.split("=")[1]) : undefined } function patchOrganizationSettingsHref( config: CommandMenuConfig, organizationSettingsHref: string, ): CommandMenuConfig { return { ...config, groups: config.groups.map(group => ({ ...group, items: group.items?.map(item => item.id === "nav-settings-organization" ? { ...item, href: organizationSettingsHref } : item, ), })), } } function AppShellLayout({ sidebarDefaultOpen }: { sidebarDefaultOpen: boolean }) { const { pathname } = useLocation() // Docked panel only — floating window must not reflow main (padding / flex). // Which shell mounts is still `AskLeoShell`'s call. const askLeoDockedPanelOpen = useAskLeoDockedPanelOpen() // Arms the rails' width transition, and steps out of the way for drags and // window resizes. Must be spread on the group, not a child. const railTransition = useRailLayoutTransition() const { variant: shellLayoutVariant } = useShellLayout() // Two reasons to render nothing but the route. Exam lock hides chrome that // would distract; pre-auth hides chrome that cannot work yet — no session // means the nav has nowhere to go, ⌘K has nothing to search, Leo has no // records to answer about, and the profile menu has no identity to show. const examLock = isExamLockPath(pathname) // The student landing joins these two. A student holds no product, so the // product switcher would name an app that is not theirs, search and Ask Leo // would reach records they cannot see, and the workspace promo strip would // sell them a school licence. It carries its own slim header instead. const chromeless = examLock || isPreAuthPath(pathname) || isStudentHomePath(pathname) const onboardingSidebarPreview = useBuilderOnboardingChrome(shouldShowOnboardingSidebarPreview) // The products home has no active product, so a product sidebar would be // navigating somewhere the user has not chosen yet. Same reasoning as // onboarding: keep the utility bar (identity, search, sign-out) and drop the // product nav. const productsHome = isProductsHomePath(pathname) // The sign-in flow builder for the same reason: a flow decides which product // you land in, so it belongs to none of them, and the active product's nav // beside it made authoring one look like a Clinical Education feature. const hidePrimarySidebar = productsHome || isSignInFlowsPath(pathname) || (pathname.startsWith("/builder/onboarding") && !onboardingSidebarPreview) // "utility-bar" layout variant: the bar moves out of the workspace column // and becomes a full-width row above the sidebar+workspace row (see // UtilityBarSlot's doc comment for how the sidebar accommodates this). const fullWidthUtilityBar = !chromeless && isFullWidthUtilityBar(shellLayoutVariant) const mountUtilityBar = !chromeless && showsUtilityBar(shellLayoutVariant) // Flush sidebar drops `variant="inset"`, which is what was drawing the // floating card and the gutter around it. `SidebarInset` reads the peer's // `data-variant`, so the canvas goes edge-to-edge on its own once the // sidebar stops claiming inset. const flushSidebar = !chromeless && hasFlushSidebar(shellLayoutVariant) return ( {!chromeless ? : null} {/* Full-bleed workspace promo / status — above the utility bar so it spans the viewport, not the inset workspace card. */} {!chromeless && !examLock && !productsHome ? : null} {fullWidthUtilityBar ? : null} {chromeless ? (
) : (
{!hidePrimarySidebar ? ( ) : null}
{mountUtilityBar && !fullWidthUtilityBar ? : null}
)}
) } function AppShell({ sidebarDefaultOpen }: { sidebarDefaultOpen: boolean }) { const dashboardHref = useProductDashboardHref() const libraryHref = dashboardHref.replace(/\/dashboard$/, "/library") const organizationSettingsHref = useProductOrganizationSettingsHref() const commandMenuConfig = React.useMemo( () => patchOrganizationSettingsHref( patchCommandMenuHrefs( buildCommandMenuConfig({ dataGroups: COMMAND_MENU_SEARCH_DATA_GROUPS }), { "nav-dashboard": dashboardHref, "nav-library": libraryHref, "nav-library-all": `${libraryHref}/all`, }, ), organizationSettingsHref, ), [dashboardHref, libraryHref, organizationSettingsHref], ) return ( {/* Above the shell so it survives the destination mounting underneath it: the switch navigates while the dialog is still up. */} ) } function patchCommandMenuHrefs( config: CommandMenuConfig, hrefs: Record, ): CommandMenuConfig { return { ...config, groups: config.groups.map(group => ({ ...group, items: group.items?.map(item => item.id in hrefs ? { ...item, href: hrefs[item.id] } : item, ), })), } } export function App() { const sidebarDefaultOpen = sidebarDefaultOpenFromCookie( readCookie(SIDEBAR_STATE_COOKIE_NAME), ) return (
) }