Files
loyaly-merchant/src/app/(workspace)/layout.tsx
2026-08-05 15:26:50 +05:30

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>
);
}