275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
import {
|
|
AppShell,
|
|
AppShellBody,
|
|
AppShellFooter,
|
|
AppShellHeader,
|
|
AppShellMain,
|
|
AppShellSidebar,
|
|
Badge,
|
|
Button,
|
|
PageFooter,
|
|
PageFooterActions,
|
|
PageFooterDescription,
|
|
PageFooterLeading,
|
|
PageFooterTitle,
|
|
PageHeader,
|
|
PageHeaderActions,
|
|
PageHeaderDescription,
|
|
PageHeaderEyebrow,
|
|
PageHeaderLeading,
|
|
PageHeaderTitle,
|
|
SidebarNav,
|
|
SidebarNavContent,
|
|
SidebarNavHeader,
|
|
SidebarNavItem,
|
|
SidebarNavItemIcon,
|
|
SidebarNavItemLabel,
|
|
SidebarNavItems,
|
|
SidebarNavSection,
|
|
SidebarNavSectionLabel,
|
|
StatCard,
|
|
StatCardDelta,
|
|
StatCardDescription,
|
|
StatCardHeader,
|
|
StatCardLabel,
|
|
StatCardMetric,
|
|
StatCardValue
|
|
} from "@ai-ui/ui";
|
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
|
|
function ShellMark() {
|
|
return (
|
|
<span className="flex size-10 items-center justify-center rounded-[1rem] bg-[linear-gradient(145deg,var(--color-foreground),color-mix(in_oklch,var(--color-foreground)_74%,var(--color-primary)_26%))] text-[var(--color-background)]">
|
|
<svg aria-hidden="true" className="size-5" fill="none" viewBox="0 0 24 24">
|
|
<path
|
|
d="M5.25 7.2c0-1.08.87-1.95 1.95-1.95h9.6c1.08 0 1.95.87 1.95 1.95v9.6c0 1.08-.87 1.95-1.95 1.95H7.2a1.95 1.95 0 0 1-1.95-1.95V7.2Z"
|
|
fill="currentColor"
|
|
opacity="0.24"
|
|
/>
|
|
<path
|
|
d="m8.25 8.85 3.2 3.2-3.2 3.2M11.55 12.05h4.2"
|
|
stroke="currentColor"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="1.9"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function RailIcon({ kind }: { kind: "overview" | "pipeline" | "team" }) {
|
|
switch (kind) {
|
|
case "pipeline":
|
|
return (
|
|
<svg aria-hidden="true" className="size-4" fill="none" viewBox="0 0 24 24">
|
|
<path d="M6 7.5h12M6 12h8m-8 4.5h12" stroke="currentColor" strokeLinecap="round" strokeWidth="1.8" />
|
|
</svg>
|
|
);
|
|
case "team":
|
|
return (
|
|
<svg aria-hidden="true" className="size-4" fill="none" viewBox="0 0 24 24">
|
|
<circle cx="9" cy="9" r="2.25" stroke="currentColor" strokeWidth="1.7" />
|
|
<circle cx="16" cy="10" r="1.9" stroke="currentColor" strokeWidth="1.7" opacity="0.7" />
|
|
<path
|
|
d="M5.5 18c.35-2.55 2.22-4 4.5-4s4.15 1.45 4.5 4M14.2 18c.16-1.36.93-2.28 2.25-2.9"
|
|
stroke="currentColor"
|
|
strokeLinecap="round"
|
|
strokeWidth="1.7"
|
|
/>
|
|
</svg>
|
|
);
|
|
case "overview":
|
|
default:
|
|
return (
|
|
<svg aria-hidden="true" className="size-4" fill="none" viewBox="0 0 24 24">
|
|
<rect x="4" y="4" width="6" height="6" rx="1.5" fill="currentColor" />
|
|
<rect x="14" y="4" width="6" height="10" rx="1.5" fill="currentColor" opacity="0.75" />
|
|
<rect x="4" y="14" width="10" height="6" rx="1.5" fill="currentColor" opacity="0.65" />
|
|
</svg>
|
|
);
|
|
}
|
|
}
|
|
|
|
function AppShellExample() {
|
|
return (
|
|
<div className="w-full max-w-[1320px]">
|
|
<AppShell layout="sidebar" surface="panel">
|
|
<AppShellSidebar>
|
|
<SidebarNav className="h-full">
|
|
<SidebarNavHeader>
|
|
<div className="flex items-center gap-3">
|
|
<ShellMark />
|
|
<div>
|
|
<p
|
|
className="text-[1.35rem] font-semibold tracking-[-0.04em] text-[var(--color-foreground)]"
|
|
style={{ fontFamily: "var(--font-display)" }}
|
|
>
|
|
ScaleOps
|
|
</p>
|
|
<p className="text-sm text-[var(--color-muted-foreground)]">Workspace shell</p>
|
|
</div>
|
|
</div>
|
|
</SidebarNavHeader>
|
|
|
|
<SidebarNavContent>
|
|
<SidebarNavSection>
|
|
<SidebarNavSectionLabel>Main</SidebarNavSectionLabel>
|
|
<SidebarNavItems>
|
|
<SidebarNavItem active>
|
|
<SidebarNavItemIcon>
|
|
<RailIcon kind="overview" />
|
|
</SidebarNavItemIcon>
|
|
<SidebarNavItemLabel>Overview</SidebarNavItemLabel>
|
|
</SidebarNavItem>
|
|
<SidebarNavItem>
|
|
<SidebarNavItemIcon>
|
|
<RailIcon kind="pipeline" />
|
|
</SidebarNavItemIcon>
|
|
<SidebarNavItemLabel>Pipeline</SidebarNavItemLabel>
|
|
</SidebarNavItem>
|
|
<SidebarNavItem>
|
|
<SidebarNavItemIcon>
|
|
<RailIcon kind="team" />
|
|
</SidebarNavItemIcon>
|
|
<SidebarNavItemLabel>Team</SidebarNavItemLabel>
|
|
</SidebarNavItem>
|
|
</SidebarNavItems>
|
|
</SidebarNavSection>
|
|
</SidebarNavContent>
|
|
</SidebarNav>
|
|
</AppShellSidebar>
|
|
|
|
<AppShellBody>
|
|
<AppShellHeader>
|
|
<PageHeader density="compact" variant="default">
|
|
<PageHeaderLeading>
|
|
<PageHeaderEyebrow>Workspace shell</PageHeaderEyebrow>
|
|
<PageHeaderTitle>One shared frame for navigation, header, and page body.</PageHeaderTitle>
|
|
<PageHeaderDescription>
|
|
AppShell owns the outer layout contract so product pages can swap content without
|
|
reinventing the sidebar and body structure.
|
|
</PageHeaderDescription>
|
|
</PageHeaderLeading>
|
|
<PageHeaderActions>
|
|
<Button>Primary action</Button>
|
|
<Button variant="secondary">Secondary action</Button>
|
|
</PageHeaderActions>
|
|
</PageHeader>
|
|
</AppShellHeader>
|
|
|
|
<AppShellMain>
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
<StatCard>
|
|
<StatCardHeader>
|
|
<StatCardLabel>Live revenue pulse</StatCardLabel>
|
|
</StatCardHeader>
|
|
<StatCardMetric>
|
|
<StatCardValue>$101,820</StatCardValue>
|
|
<StatCardDelta tone="success">+84</StatCardDelta>
|
|
</StatCardMetric>
|
|
<StatCardDescription>
|
|
AI-assisted follow-up is still the strongest lift across the board window.
|
|
</StatCardDescription>
|
|
</StatCard>
|
|
|
|
<StatCard>
|
|
<StatCardHeader>
|
|
<StatCardLabel>Forecast confidence</StatCardLabel>
|
|
</StatCardHeader>
|
|
<StatCardMetric>
|
|
<StatCardValue>31%</StatCardValue>
|
|
<StatCardDelta tone="primary">+9.2%</StatCardDelta>
|
|
</StatCardMetric>
|
|
<StatCardDescription>
|
|
Strong enough for planning, but still worth watching through the anomaly lane.
|
|
</StatCardDescription>
|
|
</StatCard>
|
|
</div>
|
|
</AppShellMain>
|
|
|
|
<AppShellFooter>
|
|
<PageFooter tone="subtle">
|
|
<PageFooterLeading>
|
|
<PageFooterTitle>Shell state is synchronized across the workspace.</PageFooterTitle>
|
|
<PageFooterDescription>
|
|
The page footer closes the shell with status and low-emphasis follow-up actions.
|
|
</PageFooterDescription>
|
|
</PageFooterLeading>
|
|
<PageFooterActions>
|
|
<Badge tone="primary" variant="subtle">
|
|
Ready for review
|
|
</Badge>
|
|
</PageFooterActions>
|
|
</PageFooter>
|
|
</AppShellFooter>
|
|
</AppShellBody>
|
|
</AppShell>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const meta = {
|
|
title: "Patterns/AppShell",
|
|
component: AppShellExample,
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
component:
|
|
"AppShell is the shared outer layout pattern for workspace-style screens. Use it when a product view needs a repeatable sidebar/body frame that can host `SidebarNav`, `PageHeader`, `PageFooter`, and arbitrary content panels without each scene redefining the same shell grid. The shell should feel quietly awake through soft surface motion and focus-within depth changes, while leaving stronger animation work to the content living inside it."
|
|
}
|
|
},
|
|
layout: "centered"
|
|
},
|
|
tags: ["autodocs"]
|
|
} satisfies Meta<typeof AppShellExample>;
|
|
|
|
export default meta;
|
|
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
export const Playground: Story = {};
|
|
|
|
export const Anatomy: Story = {
|
|
render: () => (
|
|
<div className="grid w-[1320px] gap-5 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
|
|
<div className="grid gap-2">
|
|
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
|
App shell anatomy
|
|
</p>
|
|
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
|
|
The stable shell regions are the sidebar, body, header, main content lane, and footer.
|
|
That keeps full-page scenes aligned around one layout contract instead of local CSS grids.
|
|
</p>
|
|
</div>
|
|
|
|
<AppShellExample />
|
|
</div>
|
|
)
|
|
};
|
|
|
|
export const Motion: Story = {
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
"The panel shell should feel a little more alive than a static CSS grid, but it should still read as infrastructure. Hover and focus inside the shell to check the subtle surface wake-up without competing with the contained components."
|
|
}
|
|
}
|
|
},
|
|
render: () => (
|
|
<div className="grid w-[1320px] gap-5 rounded-[var(--radius-xl)] border border-[color-mix(in_oklch,var(--color-border)_72%,transparent)] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_80%,white_20%),color-mix(in_oklch,var(--color-surface-container-low)_88%,white_12%))] p-6 shadow-[var(--shadow-sm)]">
|
|
<div className="grid gap-2">
|
|
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-primary)]">
|
|
Motion review
|
|
</p>
|
|
<p className="max-w-3xl text-sm leading-6 text-[var(--color-muted-foreground)]">
|
|
AppShell now carries a restrained surface glow and depth response so the workspace frame
|
|
feels staged, not inert. The motion should stay in the background behind the content.
|
|
</p>
|
|
</div>
|
|
<AppShellExample />
|
|
</div>
|
|
)
|
|
};
|