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 ? (