Add CenteredPage template and fix SideNav collapse animation

Add CenteredPage template for no-sidebar, centered-content layouts
(login, error, onboarding). Fix SideNav collapse animation where items
slid right — removed nav-level items-center (each item already handles
its own centering) and added overflow-hidden to clip text during the
width transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 15:38:03 +10:00
parent b8fb8c63c6
commit d36330084a
6 changed files with 168 additions and 2 deletions

View File

@@ -86,6 +86,7 @@ Page-level layout components that define the shell and content structure. Templa
- **ListPage** — PageHeader + stat cards + list header with actions + scrollable item list - **ListPage** — PageHeader + stat cards + list header with actions + scrollable item list
- **FormPage** — PageHeader + optional action bar + optional vertical stepper + constrained-width form content - **FormPage** — PageHeader + optional action bar + optional vertical stepper + constrained-width form content
- **DetailPage** — PageHeader + optional action bar (e.g. tabs) + single-column constrained content for viewing records/profiles/documents - **DetailPage** — PageHeader + optional action bar (e.g. tabs) + single-column constrained content for viewing records/profiles/documents
- **CenteredPage** — TopBar (optional) + horizontally/vertically centered content, no sidebar. For login, error, onboarding flows
Templates have Storybook stories tagged `['autodocs', 'template']` that show realistic "recipe" compositions — full pages built from real components with sample data. These serve as reference implementations for AI coding agents. Templates have Storybook stories tagged `['autodocs', 'template']` that show realistic "recipe" compositions — full pages built from real components with sample data. These serve as reference implementations for AI coding agents.

View File

@@ -1092,6 +1092,26 @@ import { DetailPage } from '@/components/templates/DetailPage'
</DetailPage> </DetailPage>
``` ```
#### CenteredPage
Full-page layout with no sidebar and centered content. Use for login, sign-up, error pages, onboarding, or any focused single-task flow.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `topBar` | `ReactNode` | — | Optional TopBar |
| `maxWidth` | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Content max width |
| `children` | `ReactNode` | — | **Required.** Centered content |
```tsx
import { CenteredPage } from '@/components/templates/CenteredPage'
<CenteredPage topBar={<TopBar title="" logo={<NswLogo />} />} maxWidth="md">
<Card variant="elevated">
<CardContent>Login form here</CardContent>
</Card>
</CenteredPage>
```
--- ---
## Do's and Don'ts ## Do's and Don'ts

View File

@@ -66,8 +66,8 @@ export const SideNav = forwardRef<HTMLElement, SideNavProps>(
ref={ref} ref={ref}
aria-label="Side navigation" aria-label="Side navigation"
className={cn( className={cn(
'flex flex-col bg-nav-bg px-2 py-2 transition-[width] duration-200', 'flex flex-col overflow-hidden bg-nav-bg px-2 py-2 transition-[width] duration-200',
collapsed ? 'w-20 items-center' : 'w-[360px]', collapsed ? 'w-20' : 'w-[360px]',
className, className,
)} )}
{...props} {...props}

View File

@@ -0,0 +1,108 @@
import type { Meta, StoryObj } from '@storybook/react'
import { CenteredPage } from './CenteredPage'
import { TopBar } from '@/components/organisms/TopBar/TopBar'
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/molecules/Card/Card'
import { Input } from '@/components/atoms/Input/Input'
import { Button } from '@/components/atoms/Button/Button'
import { Checkbox } from '@/components/atoms/Checkbox/Checkbox'
import { Alert } from '@/components/molecules/Alert/Alert'
import { NswLogo } from '@/components/templates/_story-helpers'
const meta: Meta<typeof CenteredPage> = {
title: 'Templates/CenteredPage',
component: CenteredPage,
tags: ['autodocs', 'template'],
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Full-page layout with no sidebar and horizontally/vertically centered content. Use for login, sign-up, error pages, onboarding, or any focused single-task flow.',
},
},
},
}
export default meta
type Story = StoryObj<typeof CenteredPage>
export const Login: Story = {
name: 'Login page',
render: () => (
<CenteredPage
topBar={<TopBar title="" leading={<div className="flex size-14 items-center justify-center"><NswLogo /></div>} />}
>
<Card variant="elevated">
<CardHeader>
<CardTitle>Sign in</CardTitle>
<CardDescription>Enter your credentials to access your account.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Input label="Email address" type="email" placeholder="you@example.com" />
<Input label="Password" type="password" placeholder="Enter your password" />
<div className="flex items-center justify-between">
<Checkbox label="Remember me" />
<a href="#" className="text-small text-info hover:underline">Forgot password?</a>
</div>
</CardContent>
<CardFooter className="flex-col gap-3">
<Button className="w-full">Sign in</Button>
<p className="text-center text-small text-text-secondary">
Don't have an account? <a href="#" className="text-info hover:underline">Create one</a>
</p>
</CardFooter>
</Card>
</CenteredPage>
),
}
export const ErrorPage: Story = {
name: 'Error page',
render: () => (
<CenteredPage
topBar={<TopBar title="" leading={<div className="flex size-14 items-center justify-center"><NswLogo /></div>} />}
maxWidth="sm"
>
<div className="flex flex-col items-center text-center">
<p className="text-[72px] font-bold leading-none text-primary">404</p>
<h1 className="mt-4 text-h2 font-bold text-text">Page not found</h1>
<p className="mt-2 text-body text-text-secondary">
The page you're looking for doesn't exist or has been moved.
</p>
<div className="mt-6 flex gap-3">
<Button variant="secondary">Go back</Button>
<Button>Home</Button>
</div>
</div>
</CenteredPage>
),
}
export const Onboarding: Story = {
name: 'Onboarding step',
render: () => (
<CenteredPage
topBar={<TopBar title="Getting Started" leading={<div className="flex size-14 items-center justify-center"><NswLogo /></div>} />}
maxWidth="lg"
>
<Card variant="surface">
<CardHeader>
<CardTitle>Welcome to the platform</CardTitle>
<CardDescription>Let's set up your workspace. This will only take a minute.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert variant="info" title="Step 1 of 3">
Tell us about your organisation so we can customise your experience.
</Alert>
<Input label="Organisation name" placeholder="Enter your organisation name" />
<Input label="Your role" placeholder="e.g. Manager, Coordinator" />
</CardContent>
<CardFooter>
<div className="flex w-full justify-between">
<Button variant="tertiary">Skip for now</Button>
<Button>Continue</Button>
</div>
</CardFooter>
</Card>
</CenteredPage>
),
}

View File

@@ -0,0 +1,35 @@
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
import { cn } from '@/lib/utils'
export interface CenteredPageProps extends HTMLAttributes<HTMLDivElement> {
/** TopBar component rendered fixed at the top */
topBar?: ReactNode
/** Horizontally and vertically centered content */
children: ReactNode
/** Max width of the content area */
maxWidth?: 'sm' | 'md' | 'lg' | 'xl'
}
const maxWidthStyles = {
sm: 'max-w-md',
md: 'max-w-xl',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
}
export const CenteredPage = forwardRef<HTMLDivElement, CenteredPageProps>(
({ topBar, maxWidth = 'md', className, children, ...props }, ref) => {
return (
<div ref={ref} className={cn('flex h-screen flex-col bg-bg', className)} {...props}>
{topBar && <div className="shrink-0">{topBar}</div>}
<main className="flex flex-1 items-center justify-center overflow-y-auto p-6">
<div className={cn('w-full', maxWidthStyles[maxWidth])}>
{children}
</div>
</main>
</div>
)
},
)
CenteredPage.displayName = 'CenteredPage'

View File

@@ -0,0 +1,2 @@
export { CenteredPage } from './CenteredPage'
export type { CenteredPageProps } from './CenteredPage'