Panels & Surfaces
The two codegen-backed overlay systems — slide-out panels and responsive modal surfaces — and when to use each.
Almost every screen in the Education Hub eventually needs to show you more without throwing away where you were. You click a staff member and want their full profile; you delete a class and need to confirm; you create a department and a quick form should appear. The app answers all of these with two overlay systems — and the first skill to learn here is simply telling them apart.
- A Side Panel slides out alongside the main content, splitting the screen into a main pane and a side pane. You can still see and use the rest of the UI. It's the home for inspectors, detail views, and in-place editing.
- A Surface is a modal overlay. It renders as a centered Dialog on desktop and a bottom Drawer on mobile, and it interrupts the workflow — perfect for confirmations, alerts, and short forms.
Both are codegen-backed registries: you drop a file in a folder, and a Vite plugin discovers it, scaffolds it if it's empty, and exposes it to the rest of the app through a typed handle. You never wire up imports or routing by hand.
One decision, every time
Supplementary information you want to work alongside — keep the main UI visible — is a panel. Something that needs immediate action, or must block the task until it's resolved, is a surface. When in doubt, ask whether the user should still be able to touch the page behind it. Yes means panel; no means surface.
How both registries work
The two systems are near-mirror images, so once you understand one you understand both. Each has a Vite plugin in .vite-plugins/ that watches a folder of files named by convention:
- Panels live in
src/components/side-panels/as*.panel.tsx, each exporting adefinePanel(...). - Surfaces live in
src/components/surfaces/as*.surface.tsx, each exporting adefineSurface(...).
The plugin walks the folder, finds the exported constant in every file with a regex, and writes a generated barrel into the _registry subfolder. That barrel is the single thing the rest of the app imports.
const PANEL_FILE_PATTERN = /\.panel\.tsx$/
const DEFINE_PANEL_REGEX = /export\s+const\s+(\w+)\s*=\s*definePanel\s*\(/
const GEN_FILENAME = 'panel-definitions.gen.ts'The plugin runs on buildStart and again on every hotUpdate, so adding, renaming, or deleting a panel updates the registry live while the dev server runs — no restart needed.
The generated barrel
The output is one typed object. Every panel becomes a named member, so you open one by a handle the editor can autocomplete rather than a stringly-typed name:
// AUTO-GENERATED by sidepanels-codegen — DO NOT EDIT
import { ManageRole } from '../manage-role.panel'
import { StaffProfile } from '../staff-profile.panel'
// ...
export const Panel = {
ManageRole,
StaffProfile,
// ...
} as constSurfaces generate the exact same shape under a Surface object — Surface.Alert, Surface.CreateDepartment, Surface.EditStaff, and so on. Reach for Panel.StaffProfile to open a panel, Surface.Alert to open a surface, and TypeScript tells you immediately if the handle doesn't exist.
Never edit the .gen.ts files
The barrel carries a // DO NOT EDIT banner because the plugin overwrites it on every change. Edit a *.panel.tsx or *.surface.tsx file instead and let codegen regenerate. To keep HMR sane, the plugin only rewrites the barrel when the content actually changed — so a no-op save won't trigger an infinite reload loop.
Empty files scaffold themselves
There's a small touch that makes adding a new overlay almost free. If the plugin finds a *.panel.tsx or *.surface.tsx file that's completely empty, it fills it in with working boilerplate — a component, a definePanel/defineSurface call with a zod context schema, and a commented-out permission gate to copy from.
if (content.trim().length === 0) {
const panelName = getPanelName(path.basename(relPath))
fs.writeFileSync(filepath, buildScaffold(panelName, relPath), 'utf-8')
}So the real "create a panel" workflow is just two steps: make an empty file named for what it does — create-term.surface.tsx, staff-profile.panel.tsx — then save it. The kebab-case filename becomes the PascalCase handle (create-term to CreateTerm) and a Title Case heading, and you start editing from a scaffold rather than a blank page.
Side by side
Both overlays gate on permissions and both are summoned by a typed handle, so the choice really does come down to how much they interrupt.
| Side Panel | Surface | |
|---|---|---|
| Layout | Split pane, beside the content | Modal over the content |
| On mobile | Slides over | Bottom drawer |
| Main UI | Still visible and usable | Blocked until dismissed |
| Best for | Inspectors, detail views, editing | Confirms, alerts, quick forms |
| Handle | Panel.StaffProfile | Surface.Alert |
Once you've placed your overlay on the right side of that line, the two sub-pages below cover how to build and drive each one — panel sizing, context params, and the responsive SurfaceContainer that swaps Dialog for Drawer.
Where to go next
Side Panels
Build and open a slide-out panel — context params, sizing, and the split-pane layout.
Surfaces
The responsive modal system that becomes a Dialog on desktop and a Drawer on mobile.
Permissions
How the optional permission gate on a panel or surface renders a denied view.
Forms
The form patterns most create and edit surfaces are built around.