`
- Include `aria-label` on icon-only buttons
- Use `lucide-react` for icons (already a dev dependency)
+- Compose pages using AppShell (TopBar + SideNav + content) as the outer layout
+- Use the spacing conventions (p-6 content padding, gap-6 between sections)
**Don't:**
- Hardcode colour hex values in component code
@@ -584,3 +1089,4 @@ import { Popover, PopoverTrigger, PopoverContent } from '@/components/molecules/
- Use CSS modules or styled-components
- Skip the `label` prop on form controls — all inputs must have visible labels
- Nest interactive elements (button inside button, link inside button)
+- Reference palette tokens (`text-blue-01`, `border-grey-03`) in component code — add a semantic token if one doesn't exist
diff --git a/src/components/organisms/SideNav/SideNav.tsx b/src/components/organisms/SideNav/SideNav.tsx
index 3c72551..e16d087 100644
--- a/src/components/organisms/SideNav/SideNav.tsx
+++ b/src/components/organisms/SideNav/SideNav.tsx
@@ -182,7 +182,7 @@ export const SideNavItem = forwardRef
(
const toggle = useCallback(() => setOpen((prev) => !prev), [])
const triggerStyles = cn(
- 'relative flex h-14 w-full items-center rounded-full transition-colors',
+ 'relative flex h-14 w-full items-center rounded-full text-left transition-colors',
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info',
collapsed ? 'size-14 justify-center' : 'pl-4 pr-6',
active && collapsed
diff --git a/src/components/templates/AppShell/AppShell.stories.tsx b/src/components/templates/AppShell/AppShell.stories.tsx
new file mode 100644
index 0000000..9668cc2
--- /dev/null
+++ b/src/components/templates/AppShell/AppShell.stories.tsx
@@ -0,0 +1,95 @@
+import type { Meta, StoryObj } from '@storybook/react'
+import { useState } from 'react'
+import { AppShell } from './AppShell'
+import { TopBar } from '@/components/organisms/TopBar/TopBar'
+import { SideNav, SideNavItem, SideNavGroup, SideNavDivider } from '@/components/organisms/SideNav/SideNav'
+import { Avatar } from '@/components/atoms/Avatar/Avatar'
+import { IconButton } from '@/components/atoms/IconButton/IconButton'
+import { PageHeader } from '@/components/organisms/PageHeader/PageHeader'
+import { Menu, Search, Bell, Home, FileText, LayoutGrid, Settings, Users, Link } from 'lucide-react'
+
+const NswLogo = () => (
+ NSW
+)
+
+const meta: Meta = {
+ title: 'Templates/AppShell',
+ component: AppShell,
+ tags: ['autodocs', 'template'],
+ parameters: {
+ layout: 'fullscreen',
+ docs: {
+ description: {
+ component: 'Application shell layout that composes TopBar + SideNav + scrollable content area. All page templates should be rendered inside an AppShell.',
+ },
+ },
+ },
+}
+export default meta
+
+type Story = StoryObj
+
+const SampleTopBar = ({ onMenuClick }: { onMenuClick?: () => void }) => (
+ } aria-label="Toggle menu" variant="tertiary" onClick={onMenuClick} />}
+ logo={}
+ >
+ } aria-label="Search" variant="tertiary" />
+ } aria-label="Notifications" variant="tertiary" />
+
+
+)
+
+const SampleSideNav = ({ collapsed }: { collapsed: boolean }) => (
+
+ } active>My status
+ }>My details
+ }>Workspace
+
+ } label="PDP" defaultOpen>
+ My PDP
+ PDP guide
+ Management
+
+ }>Resources
+ }>Settings
+
+)
+
+export const Default: Story = {
+ render: () => {
+ const [collapsed, setCollapsed] = useState(false)
+ return (
+ setCollapsed(!collapsed)} />}
+ sideNav={}
+ sideNavCollapsed={collapsed}
+ >
+
+
+
+ Page content goes here
+
+
+
+ )
+ },
+}
+
+export const Collapsed: Story = {
+ render: () => (
+ }
+ sideNav={}
+ sideNavCollapsed
+ >
+
+
+
+ Content area is wider with collapsed sidebar
+
+
+
+ ),
+}
diff --git a/src/components/templates/AppShell/AppShell.tsx b/src/components/templates/AppShell/AppShell.tsx
new file mode 100644
index 0000000..0760c99
--- /dev/null
+++ b/src/components/templates/AppShell/AppShell.tsx
@@ -0,0 +1,30 @@
+import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
+import { cn } from '@/lib/utils'
+
+export interface AppShellProps extends HTMLAttributes {
+ /** TopBar component rendered fixed at the top */
+ topBar: ReactNode
+ /** SideNav component rendered in the left rail */
+ sideNav: ReactNode
+ /** Whether the SideNav is in collapsed (icon-only) mode */
+ sideNavCollapsed?: boolean
+}
+
+export const AppShell = forwardRef(
+ ({ topBar, sideNav, sideNavCollapsed = false, className, children, ...props }, ref) => {
+ return (
+
+ )
+ },
+)
+AppShell.displayName = 'AppShell'
diff --git a/src/components/templates/AppShell/index.ts b/src/components/templates/AppShell/index.ts
new file mode 100644
index 0000000..03c5871
--- /dev/null
+++ b/src/components/templates/AppShell/index.ts
@@ -0,0 +1,2 @@
+export { AppShell } from './AppShell'
+export type { AppShellProps } from './AppShell'
diff --git a/src/components/templates/DashboardPage/DashboardPage.stories.tsx b/src/components/templates/DashboardPage/DashboardPage.stories.tsx
new file mode 100644
index 0000000..28bd8bd
--- /dev/null
+++ b/src/components/templates/DashboardPage/DashboardPage.stories.tsx
@@ -0,0 +1,193 @@
+import type { Meta, StoryObj } from '@storybook/react'
+import { useState } from 'react'
+import { DashboardPage } from './DashboardPage'
+import { AppShell } from '@/components/templates/AppShell/AppShell'
+import { TopBar } from '@/components/organisms/TopBar/TopBar'
+import { SideNav, SideNavItem, SideNavDivider } from '@/components/organisms/SideNav/SideNav'
+import { PageHeader } from '@/components/organisms/PageHeader/PageHeader'
+import { Card, CardHeader, CardTitle, CardContent } from '@/components/molecules/Card/Card'
+import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/molecules/Accordion/Accordion'
+import { Badge } from '@/components/atoms/Badge/Badge'
+import { Avatar } from '@/components/atoms/Avatar/Avatar'
+import { IconButton } from '@/components/atoms/IconButton/IconButton'
+import { Menu, Bell, Home, FileText, LayoutGrid, Users, CheckCircle, Clock, Info } from 'lucide-react'
+
+const NswLogo = () => (
+ NSW
+)
+
+const meta: Meta = {
+ title: 'Templates/DashboardPage',
+ component: DashboardPage,
+ tags: ['autodocs', 'template'],
+ parameters: {
+ layout: 'fullscreen',
+ docs: {
+ description: {
+ component: 'Dashboard page template with stat summary row and a responsive 2-column content grid. Use inside AppShell for the full page layout.',
+ },
+ },
+ },
+}
+export default meta
+
+type Story = StoryObj
+
+export const ProfessionalPathway: Story = {
+ name: 'Professional Pathway Dashboard',
+ render: () => {
+ const [collapsed, setCollapsed] = useState(false)
+ return (
+ } aria-label="Menu" variant="tertiary" onClick={() => setCollapsed(!collapsed)} />}
+ logo={}
+ >
+ } aria-label="Notifications" variant="tertiary" />
+
+
+ }
+ sideNav={
+
+ } active>My status
+ }>My details
+ }>Workspace
+ }>Resources
+
+ }>Accreditation
+
+ }
+ sideNavCollapsed={collapsed}
+ >
+
+
+ Maroubra Junction Public School
+
+
+ }
+ >
+
+
+ Steps to be taken
+
+
+
+
+ Ensure you have completed the minimum requirements of your teaching degree as stated by NESA.
+ Details about teaching degree requirements.
+
+
+ Apply for your Working With Children Check (WWCC).
+ Information about WWCC application.
+
+
+ Create an eTAMS account and submit required documentation to NESA.
+ Steps for eTAMS registration.
+
+
+ Pay your NESA fee.
+ Payment details.
+
+
+ Complete Mandatory Training.
+ Training module details.
+
+
+
+
+
+
+
+
+ Mandatory Training Reminders
+
+
+
+ Aboriginal Cultural Education
+ }>Certified
+
+
+ Please consult the Mandatory Training Hub for role specific training, or contact the MyPL Helpdesk for queries regarding training.
+
+
+
+
+
+
+
+
+
+ Hi I am Martha. I got my conditional accreditation recently through NESA. These links really helped me through the process.
+
+
+
+
+
+
+
+
+ )
+ },
+}
+
+export const Standalone: Story = {
+ name: 'Without AppShell',
+ render: () => (
+ }
+ stats={
+ <>
+
+
+
+
+
+
+
21h
+
Total hours logged
+
Target 100h
+
+
+
+
+
+
+
+
+
+
18h
+
NESA Registered PD
+
Target 60h
+
+
+
+
+
+
+
+
+
+
5
+
Activities logged
+
+
+
+ >
+ }
+ >
+
+ Left column content
+
+
+ Right column content
+
+
+ ),
+}
diff --git a/src/components/templates/DashboardPage/DashboardPage.tsx b/src/components/templates/DashboardPage/DashboardPage.tsx
new file mode 100644
index 0000000..a87b81e
--- /dev/null
+++ b/src/components/templates/DashboardPage/DashboardPage.tsx
@@ -0,0 +1,30 @@
+import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
+import { cn } from '@/lib/utils'
+
+export interface DashboardPageProps extends HTMLAttributes {
+ /** PageHeader or custom header section */
+ header?: ReactNode
+ /** Row of stat cards or summary widgets displayed above the content grid */
+ stats?: ReactNode
+ /** Two-column responsive content grid area */
+ children: ReactNode
+}
+
+export const DashboardPage = forwardRef(
+ ({ header, stats, className, children, ...props }, ref) => {
+ return (
+
+ {header}
+
+
+ {stats &&
{stats}
}
+
+
+ {children}
+
+
+
+ )
+ },
+)
+DashboardPage.displayName = 'DashboardPage'
diff --git a/src/components/templates/DashboardPage/index.ts b/src/components/templates/DashboardPage/index.ts
new file mode 100644
index 0000000..a0af369
--- /dev/null
+++ b/src/components/templates/DashboardPage/index.ts
@@ -0,0 +1,2 @@
+export { DashboardPage } from './DashboardPage'
+export type { DashboardPageProps } from './DashboardPage'
diff --git a/src/components/templates/FormPage/FormPage.stories.tsx b/src/components/templates/FormPage/FormPage.stories.tsx
new file mode 100644
index 0000000..076c5df
--- /dev/null
+++ b/src/components/templates/FormPage/FormPage.stories.tsx
@@ -0,0 +1,198 @@
+import type { Meta, StoryObj } from '@storybook/react'
+import { useState } from 'react'
+import { FormPage } from './FormPage'
+import { AppShell } from '@/components/templates/AppShell/AppShell'
+import { TopBar } from '@/components/organisms/TopBar/TopBar'
+import { SideNav, SideNavItem, SideNavGroup, SideNavDivider } from '@/components/organisms/SideNav/SideNav'
+import { PageHeader } from '@/components/organisms/PageHeader/PageHeader'
+import { Card, CardContent } from '@/components/molecules/Card/Card'
+import { Alert } from '@/components/molecules/Alert/Alert'
+import { Input } from '@/components/atoms/Input/Input'
+import { Select } from '@/components/atoms/Select/Select'
+import { Button } from '@/components/atoms/Button/Button'
+import { Badge } from '@/components/atoms/Badge/Badge'
+import { Avatar } from '@/components/atoms/Avatar/Avatar'
+import { IconButton } from '@/components/atoms/IconButton/IconButton'
+import { Menu, Bell, Home, FileText, LayoutGrid, Users, Link, ArrowRight } from 'lucide-react'
+
+const NswLogo = () => (
+ NSW
+)
+
+const meta: Meta = {
+ title: 'Templates/FormPage',
+ component: FormPage,
+ tags: ['autodocs', 'template'],
+ parameters: {
+ layout: 'fullscreen',
+ docs: {
+ description: {
+ component: 'Form page template with optional vertical stepper and constrained-width form content. Use inside AppShell for the full page layout.',
+ },
+ },
+ },
+}
+export default meta
+
+type Story = StoryObj
+
+export const PDPDetails: Story = {
+ name: 'PDP Form',
+ render: () => {
+ const [collapsed, setCollapsed] = useState(false)
+ return (
+ } aria-label="Menu" variant="tertiary" onClick={() => setCollapsed(!collapsed)} />}
+ logo={}
+ >
+ } aria-label="Notifications" variant="tertiary" />
+
+
+ }
+ sideNav={
+
+ }>My status
+ }>My details
+ }>Workspace
+ }>Resources
+ }>My documents & links
+
+ } label="PDP" defaultOpen active>
+ My PDP
+ PDP guide
+ Management
+ Useful links
+ Support
+
+
+ }
+ sideNavCollapsed={collapsed}
+ >
+
+
+ Plan - In progress
+ Date commenced: dd-mm-yyyy
+
+
+ }
+ actions={
+ <>
+
+
+ >
+ }
+ steps={[
+ { label: 'Your PDP details', status: 'current' },
+ { label: 'Create your PDP', status: 'upcoming' },
+ { label: 'Notify your PDP supervisor', status: 'upcoming' },
+ ]}
+ >
+
+
+
+
Welcome to your Performance and Development Plan (PDP)
+
+ Once your goals are drafted and you're ready to share them, you can notify your PDP supervisor. Head to the Digital PDP page on the intranet to find key resources to help you complete your PDP.
+
+
+
+
+ Fill in the details below to get started with your PDP.
+
+
+
+
+
+
+
Middle leader role(s)
+
Some text about middle leader roles
+
+
+
+
+
Add your PDP supervisor's details here
+
+ Note: if your supervisor's name does not appear when you search for them, ask them to access the Digital PDP using their credentials, then try again.
+
+
+
+
+
+
+
+
+
Add your school or work location.
+
+ If you don't work in a school, add 'Education Office' as your work location.
+
+
+
+
+
+ Note: As the school leader, your principal can view all the POPs in the school.
+
+
+
+ }>Proceed
+
+
+
+
+
+
+ )
+ },
+}
+
+export const SimpleForm: Story = {
+ name: 'Simple Form (no stepper)',
+ render: () => (
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+}
diff --git a/src/components/templates/FormPage/FormPage.tsx b/src/components/templates/FormPage/FormPage.tsx
new file mode 100644
index 0000000..e4e7e16
--- /dev/null
+++ b/src/components/templates/FormPage/FormPage.tsx
@@ -0,0 +1,81 @@
+import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
+import { cn } from '@/lib/utils'
+
+export interface FormPageStep {
+ label: string
+ status: 'complete' | 'current' | 'upcoming'
+}
+
+export interface FormPageProps extends HTMLAttributes {
+ /** PageHeader or custom header section */
+ header?: ReactNode
+ /** Action bar above the form content (e.g. dropdowns, buttons) */
+ actions?: ReactNode
+ /** Vertical stepper steps — renders a progress indicator alongside the form */
+ steps?: FormPageStep[]
+ /** Form content area */
+ children: ReactNode
+}
+
+const StepIndicator = ({ step, index }: { step: FormPageStep; index: number }) => {
+ const base = 'flex size-8 shrink-0 items-center justify-center rounded-full text-small font-bold'
+ const styles = {
+ complete: 'bg-success text-white',
+ current: 'bg-info text-white',
+ upcoming: 'bg-grey-04 text-text-secondary',
+ }
+
+ return (
+
+
+ {step.status === 'complete' ? (
+
+ ) : (
+ index + 1
+ )}
+
+
+ {step.label}
+
+
+ )
+}
+
+export const FormPage = forwardRef(
+ ({ header, actions, steps, className, children, ...props }, ref) => {
+ return (
+
+ {header}
+
+ {actions && (
+
+ {actions}
+
+ )}
+
+
+
+ {steps ? (
+
+
+
{children}
+
+ ) : (
+ children
+ )}
+
+
+
+ )
+ },
+)
+FormPage.displayName = 'FormPage'
diff --git a/src/components/templates/FormPage/index.ts b/src/components/templates/FormPage/index.ts
new file mode 100644
index 0000000..fb7d1ac
--- /dev/null
+++ b/src/components/templates/FormPage/index.ts
@@ -0,0 +1,2 @@
+export { FormPage } from './FormPage'
+export type { FormPageProps, FormPageStep } from './FormPage'
diff --git a/src/components/templates/ListPage/ListPage.stories.tsx b/src/components/templates/ListPage/ListPage.stories.tsx
new file mode 100644
index 0000000..67f6ebc
--- /dev/null
+++ b/src/components/templates/ListPage/ListPage.stories.tsx
@@ -0,0 +1,169 @@
+import type { Meta, StoryObj } from '@storybook/react'
+import { useState } from 'react'
+import { ListPage } from './ListPage'
+import { AppShell } from '@/components/templates/AppShell/AppShell'
+import { TopBar } from '@/components/organisms/TopBar/TopBar'
+import { SideNav, SideNavItem, SideNavDivider } from '@/components/organisms/SideNav/SideNav'
+import { PageHeader } from '@/components/organisms/PageHeader/PageHeader'
+import { Card, CardContent } from '@/components/molecules/Card/Card'
+import { Badge } from '@/components/atoms/Badge/Badge'
+import { Tag } from '@/components/atoms/Tag/Tag'
+import { Button } from '@/components/atoms/Button/Button'
+import { Avatar } from '@/components/atoms/Avatar/Avatar'
+import { IconButton } from '@/components/atoms/IconButton/IconButton'
+import { Menu, Bell, Home, LayoutGrid, FileText, Users, Clock, BarChart3, Plus, Check } from 'lucide-react'
+
+const NswLogo = () => (
+ NSW
+)
+
+const ActivityItem = ({ title, hours, date }: { title: string; hours: string; date: string }) => (
+
+
+
+ NSW DoE
+ }>Registered
+ S1
+ s4
+ S6
+
+
+ Lorem dolor sit amet, consectetur adipiscing elit. Donec condimentum nulla gravida pretium libero. Proin in felis consectetur, laoreet est eu, consectetur mi.
+
+
+)
+
+const meta: Meta = {
+ title: 'Templates/ListPage',
+ component: ListPage,
+ tags: ['autodocs', 'template'],
+ parameters: {
+ layout: 'fullscreen',
+ docs: {
+ description: {
+ component: 'List page template with stat summary row, list header with actions, and a scrollable item list. Use inside AppShell for the full page layout.',
+ },
+ },
+ },
+}
+export default meta
+
+type Story = StoryObj
+
+export const PDLog: Story = {
+ name: 'PD Log',
+ render: () => {
+ const [collapsed, setCollapsed] = useState(false)
+ return (
+ } aria-label="Menu" variant="tertiary" onClick={() => setCollapsed(!collapsed)} />}
+ logo={}
+ >
+ } aria-label="Notifications" variant="tertiary" />
+
+
+ }
+ sideNav={
+
+ } active>My status
+ }>Workspace
+ }>My details
+
+ }>Accreditation
+
+ }
+ sideNavCollapsed={collapsed}
+ >
+
+
+ Maroubra Junction Public School
+
+
+ }
+ stats={
+ <>
+
+
+
+
+
+
+
21h
+
Total hours logged
+
Target 100h
+
+
+
+
+
+
+
+
+
+
18h
+
NESA Registered PD
+
Target 60h
+
+
+
+
+
+
+
+
+
+
5
+
Activities logged
+
+
+
+ >
+ }
+ listHeader={
+
+
+
My PD Log
+
Log every professional learning activity — NESA Registered and school-based.
+
+
}>Add Activity
+
+ }
+ >
+
+
+
+
+
+
+ )
+ },
+}
+
+export const Standalone: Story = {
+ name: 'Without AppShell',
+ render: () => (
+ }
+ listHeader={
+
+
Activities
+ }>Add
+
+ }
+ >
+
+
+
+ ),
+}
diff --git a/src/components/templates/ListPage/ListPage.tsx b/src/components/templates/ListPage/ListPage.tsx
new file mode 100644
index 0000000..2ca7301
--- /dev/null
+++ b/src/components/templates/ListPage/ListPage.tsx
@@ -0,0 +1,36 @@
+import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
+import { cn } from '@/lib/utils'
+
+export interface ListPageProps extends HTMLAttributes {
+ /** PageHeader or custom header section */
+ header?: ReactNode
+ /** Row of stat cards or summary widgets */
+ stats?: ReactNode
+ /** Section header area with title and optional action (e.g. "Add Activity" button) */
+ listHeader?: ReactNode
+ /** Scrollable list content area */
+ children: ReactNode
+}
+
+export const ListPage = forwardRef(
+ ({ header, stats, listHeader, className, children, ...props }, ref) => {
+ return (
+
+ {header}
+
+
+ {stats &&
{stats}
}
+
+
+ {listHeader}
+
+
+ {children}
+
+
+
+
+ )
+ },
+)
+ListPage.displayName = 'ListPage'
diff --git a/src/components/templates/ListPage/index.ts b/src/components/templates/ListPage/index.ts
new file mode 100644
index 0000000..e19adcb
--- /dev/null
+++ b/src/components/templates/ListPage/index.ts
@@ -0,0 +1,2 @@
+export { ListPage } from './ListPage'
+export type { ListPageProps } from './ListPage'