RTOpacks Page Build Guide¶
Version: 1.0
Status: Canonical
Companion to: Design Foundation (v1.22)
Usage: Read this document alongside the Design Foundation. The Foundation defines what the design system is (colours, typography, spacing, motion, accessibility). This guide defines how to build a page using that system. Do not build a page without consulting both documents.
0. Purpose¶
This document is the practical reference for building any page on any RTOpacks surface. It covers the layout hierarchy, component patterns, navigation wiring, and file structure conventions that turn the Design Foundation's constraints into working code.
If the Foundation says "use --color-bg-elevated for panels," this guide tells you which component renders that panel and where the file lives.
1. Project Structure¶
RTOpacks is a monorepo with three production apps and shared packages:
rtopacks-project/
├── apps/
│ ├── site/ → rtopacks.com.au (public)
│ ├── admin/ → admin.rtopacks.com.au (internal)
│ └── workspace/ → my.rtopacks.com.au (authenticated RTO)
├── packages/
│ ├── ui/ → shared component library + design system
│ ├── config/ → configuration utilities
│ └── data/ → data models and schemas
├── workers/ → Cloudflare Worker APIs
└── docs/ → documentation (you are here)
All three apps use Next.js 16 with App Router, React 19, TypeScript, and Tailwind CSS 4. Deployment is to Cloudflare Workers via OpenNext.js.
2. Layout Hierarchy¶
Every page renders inside a nesting of layouts. Understanding this hierarchy is essential.
2.1 Site App (apps/site)¶
app/layout.js ← Root: fonts, metadata, ModeWrapper, GlobalNav, Footer
└── app/[route]/page.js ← Page content
└── app/[route]/layout.js ← Optional nested layout (rare)
Root layout provides:
- Font loading (Inter + JetBrains Mono via Google Fonts)
- ModeWrapper — theme context for the entire app
- GlobalNav — fixed top nav bar (56px, z-200)
- SiteFooter
- ExperiencePanel — Knowledge Navigator UI
- SearchOverlay — global search
You do not need to add any of these to a new page. They wrap automatically via the root layout.
2.2 Admin App (apps/admin)¶
app/layout.tsx ← Root: responsive media queries, Shell wrapper
└── Shell ← Sidebar + main content area
└── ContentHeader ← Top bar
└── <main> ← Your page content renders here
└── app/[route]/page.tsx
Root layout provides:
- Responsive CSS in <head> — hides desktop sidebar below 1280px
- Shell component — manages sidebar visibility, impersonation state, and flex layout
Shell provides:
- SharedModeProvider with surface="admin"
- Desktop sidebar (#desktop-sidebar) — fixed left, hidden on mobile via CSS
- Mobile sidebar — fixed overlay with backdrop scrim, toggled by hamburger
- ContentHeader — top bar
- <main> element — your page content
You do not add Shell, sidebar, or header to a new page. Just export your page component from page.tsx.
2.3 Workspace App (apps/workspace)¶
app/layout.tsx ← Root: dark theme enforced, PWA metadata
└── app/(workspace)/layout.tsx ← Workspace layout (auth, sidebar)
└── app/(workspace)/[route]/page.tsx
Root layout provides:
- data-theme="dark" on <html> — workspace is always dark mode
- PWA manifest, touch icons, theme-color meta
- Inline styles for background and text using CSS custom properties
3. Creating a New Page¶
3.1 Admin Page (most common)¶
Create a new directory under apps/admin/app/ with a page.tsx:
Minimal page structure:
"use client"
export default function MyFeaturePage() {
return (
<div style={{ padding: "24px" }}>
<h1 style={{
fontSize: "1.2rem",
fontWeight: 700,
lineHeight: 1.3,
letterSpacing: "-0.01em",
color: "var(--color-text-primary)",
marginBottom: "24px"
}}>
Page Title
</h1>
<div style={{
backgroundColor: "var(--color-bg-card)",
border: "1px solid var(--color-border)",
borderRadius: "8px",
padding: "16px"
}}>
{/* Content */}
</div>
</div>
)
}
Key rules:
- Always "use client" if the page has any interactivity, state, or hooks
- Padding on the outer div: 24px (xl spacing — see Foundation §5)
- All colours via CSS custom properties — never hardcoded hex
- Typography roles from Foundation §2 — title is 1.2rem/700, body is 0.875rem/400
3.2 Admin Page with Data Table¶
Most admin pages follow this pattern (see apps/admin/app/clients/page.tsx for reference):
"use client"
import { useState, useEffect } from "react"
export default function MyListPage() {
const [data, setData] = useState([])
useEffect(() => {
fetch("/api/admin/my-endpoint")
.then(r => r.json())
.then(setData)
}, [])
return (
<div style={{ padding: "24px" }}>
{/* Page header */}
<div style={{ marginBottom: "24px" }}>
<h1 style={{
fontSize: "1.2rem",
fontWeight: 700,
color: "var(--color-text-primary)"
}}>
My Feature
</h1>
<p style={{
fontSize: "0.875rem",
color: "var(--color-text-secondary)",
marginTop: "4px"
}}>
Supporting description text.
</p>
</div>
{/* Data table */}
<table style={{
width: "100%",
borderCollapse: "collapse",
fontSize: "0.875rem"
}}>
<thead>
<tr style={{ borderBottom: "1px solid var(--color-border)" }}>
<th style={{
textAlign: "left",
padding: "12px 16px",
fontSize: "0.65rem",
fontWeight: 600,
letterSpacing: "0.08em",
textTransform: "uppercase",
color: "var(--color-text-secondary)"
}}>
Column Name
</th>
</tr>
</thead>
<tbody>
{data.map(item => (
<tr
key={item.id}
style={{
borderBottom: "1px solid var(--color-border-subtle)",
cursor: "pointer"
}}
onMouseEnter={e => e.currentTarget.style.backgroundColor = "var(--color-bg-hover)"}
onMouseLeave={e => e.currentTarget.style.backgroundColor = "transparent"}
>
<td style={{
padding: "12px 16px",
color: "var(--color-text-primary)"
}}>
{item.name}
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
Table column headers use the label typography role: 0.65rem, weight 600, uppercase, letter-spacing 0.08em.
3.3 Site Page¶
Site pages follow the same pattern but render inside GlobalNav + ModeWrapper:
"use client"
export default function MyPage() {
return (
<div style={{
maxWidth: "1200px",
margin: "0 auto",
padding: "64px 24px"
}}>
{/* Content */}
</div>
)
}
Site-specific notes:
- GlobalNav is hidden on the homepage (pathname /)
- Max content width: typically 1200px, centered
- Page-level top padding: 64px (4xl) to clear the fixed nav
4. Navigation¶
4.1 Site — GlobalNav¶
File: apps/site/app/components/GlobalNav.js
- Fixed top bar, 56px height, z-index 200
- Left: RP badge + "rtopacks" wordmark (links to
/) - Right: Search, Explore, Sign up (hidden when signed in)
- Checks
ucca_verified_session/ucca_tenant_idcookies for auth state - Hidden on homepage
To add a nav link: Edit the right-side link list in GlobalNav.js. Follow existing link patterns.
4.2 Admin — Sidebar¶
File: apps/admin/app/components/sidebar.tsx
The sidebar is organised into five collapsible groups:
| Group | Purpose |
|---|---|
| MARKET | RTO clients, vendors, calendar |
| OUTREACH | Contacts, marketing |
| CORPUS | Units, qualifications, KN output, LLM |
| FINANCE | Stripe, QuickBooks, exports |
| PLATFORM | Preferences, access control |
Dashboard is pinned above all groups.
To add a nav item:
- Find the relevant group in the
SECTIONSarray insidebar.tsx - Add an entry:
{
title: "My Feature",
href: "/my-feature",
icon: I("M3 12h18"), // Lucide-style SVG path, 14x14
status: "live" // or "planned"
}
- The active state highlights automatically via
usePathname()
Icon pattern: The sidebar uses a mini SVG builder function I() that takes a Lucide-style path string and returns a 14x14 SVG element with currentColor. Do not use Phosphor icons here.
Drag-and-drop: Users can reorder the five groups. Order persists to /api/admin/nav-prefs and is cached for 60 seconds.
4.3 Navigation Chevrons (Foundation §4.4)¶
All apps use a three-element navigation system:
- Nav wordmark (top-left) — always links to
/(home) - Left chevron (fixed left edge) — navigate to parent context
- Right chevron (fixed right edge) — navigate to next item
These are the only navigation affordances. No pulsing, no looping animations on nav elements.
5. Styling Tokens¶
All styling flows from CSS custom properties defined in each app's globals.css. These implement the values specified in the Design Foundation.
5.1 Where Tokens Are Defined¶
| File | Scope |
|---|---|
apps/site/app/globals.css |
Site tokens — full foundation palette, motion, entity colours |
apps/admin/app/globals.css |
Admin tokens — light mode palette, tag colours, vendor colours |
apps/workspace/app/globals.css |
Workspace tokens — dark + light mode |
5.2 Using Tokens in Components¶
Always use var() references. Never hardcode hex values:
// Correct
style={{ color: "var(--color-text-primary)" }}
style={{ backgroundColor: "var(--color-bg-card)" }}
style={{ border: "1px solid var(--color-border)" }}
// Wrong — never do this
style={{ color: "#e6edf3" }}
style={{ backgroundColor: "#1c2128" }}
5.3 Key Token Groups¶
Backgrounds (three-level depth stack — see Foundation §1):
- --color-bg-base — page background (deepest)
- --color-bg-elevated — panels, drawers, modals (one step up)
- --color-bg-card — cards, inset sections (top level)
- --color-bg-hover — hover state
Text:
- --color-text-primary — main body text
- --color-text-secondary — supporting text, captions
- --color-text-disabled — disabled state
Interactive:
- --color-interactive — primary blue for links, buttons, focus rings
- --color-interactive-hover — hover state on interactive elements
Borders:
- --color-border — standard borders
- --color-border-subtle — hairline separators
Status:
- --color-success, --color-warning, --color-danger, --color-info
Knowledge Navigator (teal — KN UI only, never for general use):
- --color-kn-accent, --color-kn-bg, --color-kn-border, --color-kn-text
5.4 Motion Tokens¶
Transitions use Foundation §6 duration and easing tokens:
/* Hover state */
transition: background-color var(--duration-fast) var(--ease-default);
/* Panel open */
transition: transform var(--duration-panel) var(--ease-out);
/* Panel close */
transition: transform var(--duration-runner) var(--ease-in);
6. Shared Component Library¶
The @rtopacks/ui package exports shared components used across all three apps.
Location: packages/ui/
Key exports:
| Export | Purpose |
|---|---|
badges |
Status badges, tier badges, entity type badges |
formatters |
Date, number, and text formatting utilities |
primitives |
Basic UI building blocks |
ExperiencePanel |
Knowledge Navigator visual interface |
ModeProvider / useModeContext |
Session-level mode state (LIVE/GUIDED/COMPLIANCE) |
useEnvironment / applyPrefs |
Theme and accessibility preferences |
SectionAnnotation / NodeAnnotation |
KN annotation block rendering |
print |
Print-optimised components |
video |
Video player components |
search |
Search UI components |
qual |
Qualification display components |
unit |
Unit display components |
Importing:
import { ModeProvider, useModeContext } from "@rtopacks/ui"
import { ExperiencePanel } from "@rtopacks/ui"
import { StatusBadge, TierBadge } from "@rtopacks/ui"
7. Responsive Behaviour¶
7.1 Admin Sidebar Breakpoint¶
The admin sidebar visibility is controlled by a media query injected in layout.tsx:
- Desktop (>= 1280px): Sidebar visible as fixed left column
- Mobile (< 1280px): Sidebar hidden, replaced by hamburger menu that opens a full-screen overlay
This is handled by CSS on #desktop-sidebar — you do not need to manage this in page components.
7.2 Content Area¶
The main content area fills the remaining space after the sidebar. On mobile, it takes the full viewport width. Standard page padding is 24px on all sides.
7.3 Touch Targets¶
Minimum touch target: 44px x 44px (Apple HIG). Minimum control padding: 12px around the visible edge. See Foundation §5.
8. Accessibility Checklist¶
Every new page must meet these requirements (see Foundation §7 for full specification):
- All interactive elements have visible label or
aria-label - Focus ring:
box-shadow: 0 0 0 3px var(--color-interactive)via:focus-visible - Body text contrast: 4.5:1 minimum (7:1 in high contrast mode)
- Large text contrast: 3:1 minimum
- Touch targets: 44px x 44px minimum
- Semantic HTML — headings, lists, buttons (not divs with click handlers)
- Reduced motion respected — no stagger or animation when
data-motion="reduced" - Tab order follows visual order
9. Common Patterns¶
9.1 Card¶
<div style={{
backgroundColor: "var(--color-bg-card)",
border: "1px solid var(--color-border)",
borderRadius: "8px",
padding: "16px"
}}>
{/* Card content */}
</div>
9.2 Section Label (uppercase)¶
<h3 style={{
fontSize: "0.65rem",
fontWeight: 600,
lineHeight: 1.2,
letterSpacing: "0.08em",
textTransform: "uppercase",
color: "var(--color-text-secondary)",
marginBottom: "12px"
}}>
SECTION NAME
</h3>
9.3 Status Badge¶
import { StatusBadge } from "@rtopacks/ui"
<StatusBadge status="live" /> // green
<StatusBadge status="planned" /> // muted
9.4 Empty State¶
<div style={{
textAlign: "center",
padding: "48px 24px",
color: "var(--color-text-secondary)",
fontSize: "0.875rem"
}}>
No items found.
</div>
Centered text is only permitted for empty states.
9.5 Panel / Drawer¶
Panels slide in from the right. Use Foundation §6 motion tokens:
- Open:
translateX(100%) → translateX(0), 280ms, ease-out - Close:
translateX(0) → translateX(100%), 220ms, ease-in - Must have
role="dialog"andaria-label - Escape key dismisses
- Focus trapped while open
10. Checklist — Before You Ship¶
- All colours use CSS custom properties (no hardcoded hex)
- Typography uses one of the six Foundation roles (title, subhead, label, body, caption, meta)
- Font sizes in rem only (no px for font sizes)
- Spacing uses 4px-base multiples (4, 8, 12, 16, 24, 32, 48, 64)
- Icons are SF Symbols SVG (not Phosphor)
- Sidebar nav item added (if page is a new section)
- Responsive behaviour tested at < 1280px
- Accessibility checklist (§8) complete
- No looping animations on navigation elements
- Dark mode and light mode both tested
- Maximum line length for body text: 72 characters