"use client"
/**
* Every Leo surface the app shell mounts, and the rules for which one shows.
*
* Leo has grown three shells — the docked panel, the floating window, and the
* corner FAB — and the choice between them depends on the persisted `dock`
* preference, whether the window is minimised, the route, and whether the
* viewport can carry a draggable window at all. That policy used to sit inline
* in `App.tsx`, which meant the app shell imported four Leo modules and two
* viewport hooks to answer a question that is entirely Leo's business, and any
* other host (the generated starter, a consumer app) had to copy the same
* fifteen lines correctly to get the same behaviour.
*
* The shells themselves stay lazy: the panel and the window are only fetched
* once someone opens Leo, and the FAB is small enough to ship eagerly because
* it is the thing that opens him. Leo appearance mounts as a **sibling** of
* Ask Leo (not inside its toolbar tree) so portal pointer events cannot bubble
* through React and drag both windows together.
*
* Window mode must not reflow the hub: it portals as a fixed overlay. Only the
* docked panel claims flex width and may collapse the primary sidebar. The app
* row's `data-ask-leo-open` hook follows {@link useAskLeoDockedPanelOpen}.
*
* @see components/ask-leo-context.tsx — `open`, `dock`, `minimized`
* @see docs/ask-leo-pattern.md — view modes and their entry points
*/
import * as React from "react"
import { useLocation } from "react-router"
import { useAskLeo } from "@/components/ask-leo-context"
import { useLeoAmbience } from "@/components/leo-ambience-context"
import { LeoLauncherFab } from "@/components/leo-launcher-fab"
import { useSidebar } from "@/components/ui/sidebar"
import { useIsMobile } from "@/hooks/use-mobile"
import { useSidebarReflowZoom } from "@/hooks/use-sidebar-reflow-zoom"
import { isProductsHomePath } from "@/lib/product-home"
const AskLeoSidebar = React.lazy(() =>
import("@/components/ask-leo-sidebar").then(m => ({
default: m.AskLeoSidebar,
})),
)
const AskLeoWindow = React.lazy(() =>
import("@/components/ask-leo-window").then(m => ({
default: m.AskLeoWindow,
})),
)
const LeoAmbienceWindow = React.lazy(() =>
import("@/components/leo-ambience-window").then(m => ({
default: m.LeoAmbienceWindow,
})),
)
/**
* True when Leo is open as the docked right rail (or a panel fallback).
* False for the floating window — that shell must leave hub layout alone.
*/
export function useAskLeoDockedPanelOpen() {
const { open, dock } = useAskLeo()
const isMobile = useIsMobile()
const reflowZoom = useSidebarReflowZoom()
const windowMode = dock === "window" && !isMobile && !reflowZoom
return open && !windowMode
}
/** Collapse the primary nav only while the docked panel claims layout space. */
function AskLeoPrimarySidebarCollapse({ panelOpen }: { panelOpen: boolean }) {
const { setOpen: setSidebarOpen } = useSidebar()
const prevOpen = React.useRef(panelOpen)
React.useEffect(() => {
if (panelOpen && !prevOpen.current) setSidebarOpen(false)
if (!panelOpen && prevOpen.current) setSidebarOpen(true)
prevOpen.current = panelOpen
}, [panelOpen, setSidebarOpen])
return null
}
export function AskLeoShell() {
const { pathname } = useLocation()
const { open, dock, minimized } = useAskLeo()
const { settingsOpen } = useLeoAmbience()
const isMobile = useIsMobile()
const reflowZoom = useSidebarReflowZoom()
// No floating window on touch or at reflow zoom: dragging a window there
// fights page scroll, and at that size it would cover the page it is meant to
// sit beside. Both fall back to the panel, which already has a flyout mode.
const windowMode = dock === "window" && !isMobile && !reflowZoom
const dockedPanelOpen = open && !windowMode
// The products home strips the utility bar's Leo toggle, and a minimised
// window needs somewhere visible to come back from. One button, two jobs.
const salesFraming = isProductsHomePath(pathname)
const showFab = salesFraming || (windowMode && minimized)
return (
<>
{open ? (
{windowMode ? : }
) : null}
{settingsOpen ? (
) : null}
{showFab ? : null}
>
)
}