Skip to content

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:

apps/admin/app/my-feature/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:

apps/site/app/my-page/page.js
"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_id cookies 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:

  1. Find the relevant group in the SECTIONS array in sidebar.tsx
  2. Add an entry:
{
  title: "My Feature",
  href: "/my-feature",
  icon: I("M3 12h18"),  // Lucide-style SVG path, 14x14
  status: "live"         // or "planned"
}
  1. 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:

  1. Nav wordmark (top-left) — always links to / (home)
  2. Left chevron (fixed left edge) — navigate to parent context
  3. 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" and aria-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