first commit
This commit is contained in:
81
src/app/(workspace)/layout.tsx
Normal file
81
src/app/(workspace)/layout.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user