import { AgentSidebar, useGuidedQuestionFlow, } from "@agent-native/core/client/agent-chat"; import { getBrowserTabId, useSession } from "@agent-native/core/client/hooks"; import { isEmbedAuthActive } from "@agent-native/core/client/host"; import { useT } from "@agent-native/core/client/i18n"; import { CreativeContextComposerChip } from "@agent-native/creative-context/client"; import { HeaderActionsProvider } from "@agent-native/toolkit/app-shell"; import { IconMenu2 } from "@tabler/icons-react"; import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react"; import { useLocation } from "react-router"; import { useNavigationState } from "@/hooks/use-navigation-state"; import { DESIGN_CHAT_STORAGE_KEY } from "@/lib/agent-chat"; import { cn } from "@/lib/utils"; import { FigmaLinkComposerBubble, useDetectedFigmaComposerLink, } from "../editor/FigmaLinkComposerBubble"; import { Header } from "./Header"; import { Sidebar } from "./Sidebar"; interface LayoutProps { children: React.ReactNode; } const MobileSidebarContext = createContext<(() => void) | null>(null); export function useOpenMobileSidebar() { return useContext(MobileSidebarContext); } /** Routes that render with no app shell at all (no sidebar, no header). */ const BARE_PREFIXES = ["/present/"]; /** * Routes where the page renders its own toolbar instead of the global Header. * The Header is hidden so the page can supply richer custom chrome (e.g. * DesignEditor mode/zoom/device, shared ExtensionViewer / ExtensionsListPage * chrome). The editor owns its agent surface inside its Figma-style left rail. */ const EDITOR_PREFIXES = ["/design/", "/extensions"]; export function Layout({ children }: LayoutProps) { const location = useLocation(); const t = useT(); const { session } = useSession(); const hasSession = Boolean(session?.email); const embedded = isEmbedAuthActive(); useNavigationState(hasSession); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const openMobileSidebar = useCallback(() => setMobileSidebarOpen(true), []); const isDesignEditor = location.pathname.startsWith("/design/"); const showMobileTopBar = !isDesignEditor; const browserTabId = getBrowserTabId(); const { link: detectedFigmaComposerLink, onComposerTextChange: handleComposerTextChange, } = useDetectedFigmaComposerLink(); // Bind chat to the currently-open design. Same pattern as slides — the // route is `/design/:id` for the editor and `/present/:id` for preview // (which we already short-circuit as BARE). Anywhere else (list, // design-systems, settings) leaves scope null so general chats keep working. const designScope = useMemo(() => { const match = location.pathname.match(/^\/design\/([^/]+)/); const designId = match?.[1]; if (!designId) return null; return { type: "design" as const, id: designId }; }, [location.pathname]); const designQuestionStateKey = designScope ? `show-questions:${designScope.id}` : "show-questions"; const { questions: pendingDesignQuestions } = useGuidedQuestionFlow({ enabled: hasSession, stateKey: designQuestionStateKey, queryKey: [designQuestionStateKey], browserTabId, refetchInterval: embedded || !isDesignEditor || !hasSession ? false : 2000, }); const designQuestionsWaitingSlot = isDesignEditor && pendingDesignQuestions?.length ? (
{"Waiting for your answers in the canvas." /* i18n-ignore */}
) : null; useEffect(() => { setMobileSidebarOpen(false); }, [location.pathname]); const isBare = BARE_PREFIXES.some((p) => location.pathname.startsWith(p)); if (isBare) { return <>{children}; } const hideHeader = EDITOR_PREFIXES.some((p) => location.pathname.startsWith(p), ); if (embedded || (isDesignEditor && !hasSession)) { return (
{children}
); } if (isDesignEditor) { return (
{children}
); } return ( {detectedFigmaComposerLink ? ( ) : null} } >
{!isDesignEditor && mobileSidebarOpen && (
setMobileSidebarOpen(false)} /> )} {!isDesignEditor && (
)}
{/* Mobile-only top bar with hamburger */} {showMobileTopBar && (
{t("navigation.brand")}
)} {!hideHeader &&
}
{children}
); }