dashboard design changes
This commit is contained in:
@@ -1,14 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import {AppShell} from '@astryxdesign/core/AppShell';
|
||||
import {Layout, LayoutContent, LayoutPanel} from '@astryxdesign/core/Layout';
|
||||
import {
|
||||
Layout,
|
||||
LayoutContent,
|
||||
LayoutHeader,
|
||||
LayoutPanel,
|
||||
VStack,
|
||||
} from '@astryxdesign/core/Layout';
|
||||
import {AppSideNav} from '@/components/shell/AppSideNav';
|
||||
import {AppTopNav} from '@/components/shell/AppTopNav';
|
||||
import {MobileScopeNav} from '@/components/shell/MobileScopeNav';
|
||||
import {MobileMenu} from '@/components/shell/MobileMenu';
|
||||
import {SidebarProvider, useSidebar} from '@/components/shell/SidebarProvider';
|
||||
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';
|
||||
import {
|
||||
useBreakpoint,
|
||||
isPanelInline,
|
||||
isSideNavInline,
|
||||
copilotWidth,
|
||||
contentMaxWidth,
|
||||
} from '@/lib/breakpoints';
|
||||
|
||||
/**
|
||||
* The three-column shell, and the reason the Copilot survives navigation.
|
||||
@@ -18,28 +31,61 @@ import {useBreakpoint, isPanelInline, copilotWidth} from '@/lib/breakpoints';
|
||||
* 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.
|
||||
* Layout{header, start, content} internally and exposes no end/panel slot. The
|
||||
* Copilot column therefore comes from a NESTED Layout inside its children.
|
||||
*
|
||||
* AppShell(topNav, sideNav)
|
||||
* └─ Layout(content = page, end = LayoutPanel > CopilotPanel)
|
||||
* AppShell(sideNav)
|
||||
* └─ Layout(header = TopNav, content = page, end = LayoutPanel > Copilot)
|
||||
*
|
||||
* 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.
|
||||
* The top nav is deliberately NOT passed to AppShell's `topNav` slot. That slot
|
||||
* renders Layout{header} at the shell root, which spans the full viewport width
|
||||
* and pushes the sidebar — and therefore the branding — 48px down the screen.
|
||||
* Measured in the browser, the logo sat at y=64 with the top-left 260x48 region
|
||||
* empty. Moving the bar into the content column's own header starts the sidebar
|
||||
* at y=0, so the brand anchors in the window's top-left corner and the bar
|
||||
* begins where the content does. This is the arrangement Linear, Cursor and
|
||||
* Stripe use, and no amount of padding inside the sidebar could produce it.
|
||||
*
|
||||
* The one thing given up: with no `topNav`, AppShell drops the --radius-page
|
||||
* corner it draws where the top bar meets the sidebar. The content area still
|
||||
* paints elevated --color-background-surface (#0F0F10) against the #000 rails,
|
||||
* and with a full-height sidebar that corner had nothing left to round.
|
||||
*/
|
||||
export default function WorkspaceLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<WorkspaceShell>{children}</WorkspaceShell>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split out from the layout only so it can READ the sidebar context that the
|
||||
* layout mounts — a provider's own component sits above its value.
|
||||
*/
|
||||
function WorkspaceShell({children}: {children: React.ReactNode}) {
|
||||
const bp = useBreakpoint();
|
||||
const {isOpen} = useCopilot();
|
||||
const inline = isPanelInline(bp);
|
||||
const showInlinePanel = inline && isOpen;
|
||||
|
||||
// Both flags come from SidebarProvider now. AppShell keeps a copy of the
|
||||
// open state for its own context (MobileNav reads it for aria wiring), but
|
||||
// ours is the one that decides — uncontrolled, there would be no way to
|
||||
// dismiss the drawer when a nav item is selected.
|
||||
const {isDrawerOpen, setDrawerOpen} = useSidebar();
|
||||
|
||||
// Below the breakpoint the rail is gone — it lives in the drawer — so there
|
||||
// is no top-left corner to anchor the brand to and nothing for the bar to
|
||||
// start after. Hand the top nav back to AppShell there. Keeping it in the
|
||||
// content column instead pushed the SideNav into AppShell's
|
||||
// `autoMobileTopBar` fallback, which crops the lockup into a 48px bar.
|
||||
const sideNavInline = isSideNavInline(bp);
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
variant="elevated"
|
||||
@@ -47,16 +93,49 @@ export default function WorkspaceLayout({
|
||||
// Layout/LayoutContent own the padding so the Copilot panel can sit
|
||||
// flush against the workspace edge.
|
||||
contentPadding={0}
|
||||
topNav={<AppTopNav />}
|
||||
topNav={sideNavInline ? undefined : <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 />}}
|
||||
// 'sm' (640px) matches the mobile/tablet line in lib/breakpoints —
|
||||
// AppShell's own threshold and ours have to agree or the hamburger and
|
||||
// the rail are both visible, or neither is.
|
||||
mobileNav={{
|
||||
breakpoint: 'sm',
|
||||
isOpen: isDrawerOpen,
|
||||
onOpenChange: setDrawerOpen,
|
||||
// hasToggle: false — AppShell would otherwise inject a SECOND
|
||||
// hamburger into TopNav's mobile bar, next to the one in `heading`
|
||||
// that works at every width. See AppTopNav.
|
||||
hasToggle: false,
|
||||
content: <MobileMenu />,
|
||||
}}
|
||||
>
|
||||
<Layout
|
||||
height="fill"
|
||||
content={<LayoutContent padding={5}>{children}</LayoutContent>}
|
||||
header={
|
||||
sideNavInline ? (
|
||||
<LayoutHeader padding={0}>
|
||||
<AppTopNav />
|
||||
</LayoutHeader>
|
||||
) : undefined
|
||||
}
|
||||
content={
|
||||
<LayoutContent padding={5}>
|
||||
{/*
|
||||
Above 1920 the workspace is capped and centred. Uncapped, a
|
||||
30-day line chart stretches across ~1900px, which flattens every
|
||||
trend it exists to show, and body copy runs well past the ~90ch
|
||||
where reading breaks down. Below the cap this is a no-op —
|
||||
maxWidth only binds once there is more width than content wants.
|
||||
*/}
|
||||
<VStack
|
||||
width="100%"
|
||||
maxWidth={contentMaxWidth(bp)}
|
||||
className={bp === 'ultrawide' ? 'mx-auto' : undefined}
|
||||
>
|
||||
{children}
|
||||
</VStack>
|
||||
</LayoutContent>
|
||||
}
|
||||
end={
|
||||
showInlinePanel ? (
|
||||
<LayoutPanel
|
||||
@@ -65,7 +144,7 @@ export default function WorkspaceLayout({
|
||||
// CopilotPanel supplies its own header and tab chrome.
|
||||
padding={0}
|
||||
role="complementary"
|
||||
label="AI Copilot"
|
||||
label="Loyaly AI"
|
||||
>
|
||||
<CopilotPanel />
|
||||
</LayoutPanel>
|
||||
|
||||
Reference in New Issue
Block a user