82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
'use client';
|
|
|
|
import {AppShell} from '@astryxdesign/core/AppShell';
|
|
import {Layout, LayoutContent, LayoutPanel} from '@astryxdesign/core/Layout';
|
|
import {AppSideNav} from '@/components/shell/AppSideNav';
|
|
import {AppTopNav} from '@/components/shell/AppTopNav';
|
|
import {MobileScopeNav} from '@/components/shell/MobileScopeNav';
|
|
import {CopilotPanel} from '@/features/copilot/CopilotPanel';
|
|
import {CopilotSlideOver} from '@/features/copilot/CopilotSlideOver';
|
|
import {useCopilot} from '@/features/copilot/CopilotProvider';
|
|
import {useBreakpoint, isPanelInline, copilotWidth} from '@/lib/breakpoints';
|
|
|
|
/**
|
|
* The three-column shell, and the reason the Copilot survives navigation.
|
|
*
|
|
* Next's App Router preserves a layout's element identity across sibling route
|
|
* changes, so /dashboard → /staff swaps only {children}. Everything else here —
|
|
* nav, scroll position, the Copilot — is untouched.
|
|
*
|
|
* Structure, and why AppShell alone is not enough: AppShell renders
|
|
* Layout{start, content} internally and exposes no end/panel slot. The Copilot
|
|
* column therefore comes from a NESTED Layout inside AppShell's children,
|
|
* using its `end` slot.
|
|
*
|
|
* AppShell(topNav, sideNav)
|
|
* └─ Layout(content = page, end = LayoutPanel > CopilotPanel)
|
|
*
|
|
* variant="elevated" is what produces the described visual for free: with both
|
|
* navs present the shell root and rails paint --color-background-body (#000),
|
|
* and the content area sits on an elevated --color-background-surface (#0F0F10)
|
|
* backdrop with --radius-page on the start corner.
|
|
*/
|
|
export default function WorkspaceLayout({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
const bp = useBreakpoint();
|
|
const {isOpen} = useCopilot();
|
|
const inline = isPanelInline(bp);
|
|
const showInlinePanel = inline && isOpen;
|
|
|
|
return (
|
|
<AppShell
|
|
variant="elevated"
|
|
height="fill"
|
|
// Layout/LayoutContent own the padding so the Copilot panel can sit
|
|
// flush against the workspace edge.
|
|
contentPadding={0}
|
|
topNav={<AppTopNav />}
|
|
sideNav={<AppSideNav />}
|
|
// Supplied explicitly rather than auto-generated: the default drawer
|
|
// mirrors sideNav only, which would leave the store and range controls
|
|
// unreachable below 768px (they live in TopNav's collapsing start slot).
|
|
mobileNav={{breakpoint: 'md', content: <MobileScopeNav />}}
|
|
>
|
|
<Layout
|
|
height="fill"
|
|
content={<LayoutContent padding={5}>{children}</LayoutContent>}
|
|
end={
|
|
showInlinePanel ? (
|
|
<LayoutPanel
|
|
hasDivider
|
|
width={copilotWidth(bp)}
|
|
// CopilotPanel supplies its own header and tab chrome.
|
|
padding={0}
|
|
role="complementary"
|
|
label="AI Copilot"
|
|
>
|
|
<CopilotPanel />
|
|
</LayoutPanel>
|
|
) : undefined
|
|
}
|
|
/>
|
|
{/* Mounted only below the inline breakpoint. Because every piece of
|
|
Copilot state lives in CopilotProvider (above the route tree), moving
|
|
between these two presentations is lossless. */}
|
|
{!inline ? <CopilotSlideOver /> : null}
|
|
</AppShell>
|
|
);
|
|
}
|