dashboard design changes

This commit is contained in:
2026-08-05 18:34:37 +05:30
parent 2c8c394795
commit 9d7bbad32c
92 changed files with 5104 additions and 608 deletions

View File

@@ -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>