BRIEFS
Design System & UI Inventory
REFERENCE / Design System & UI Inventory
A catalog of every styled / interactive front-end element: what it is, where it lives, the classes + tokens it consumes, its variants/states, and — where relevant — a 🎯 flagged standardization note. This is the component & pattern reference; its companions are:
docs/STYLE-MAP.md— the token reference: every theme knob → CSS var → on-screen element.docs/CIRCUIT-SPEC.md— the circuit subsystem spec.AUTHORING.md— adding content.
Scope note: this is a descriptive + recommendation doc. The naming-convention section (§13) has been applied — the component/class names below are live. The remaining 🎯 items in the standardization section (§14) are still proposals for a later pass.
1. Purpose & ethos
panicOS is an open-source artist-portfolio framework with a 90s DOS/CRT aesthetic. The design goal: a forker should be able to take the source, change (or keep) the colours, drop in their own images + markdown posts, and ship — without touching component code. That is why:
- Almost nothing is hard-coded. Colours, type, dash geometry, motion, and the OS-home
surfaces all come from markdown/JSON config (
src/content/site/*) compiled to CSS variables. - Content is markdown. Projects/briefs/assets/links/about are
.mdfiles. - Components consume tokens, not literals. Reach for
var(--c-*)/var(--os-*)/var(--text-*)/var(--font-*)— never a raw hex.
The two themeability gotchas to keep in mind:
- OS-home colours are literal copies, not palette aliases.
os.*intheme.mdduplicates the palette; changingpalette.textwon’t move the homepage unless you also changeos.*.text. Intentional but the #1 “my change didn’t apply” cause. - Marching-dash borders are SVG, drawn per-element by
DashedBorderFull— not CSSborder. Geometry comes fromcircuits.md.
2. Design tokens (summary)
Token families emitted by lib/theme-css.ts into :root:
| Family | Prefix | Drives | Source knobs |
|---|---|---|---|
| Palette | --c-* | colours everywhere (text, accent, borders, glow, links, CRT) | theme.md → palette |
| Chrome | --c-top-accent, --tray-* | top strip, sidebar tray | theme.md → chrome |
| Type sizes | --text-*, --font-size, --prose-size | every font-size | theme.md → typography.sizes |
| Font roles | --font-* | font-family per role | theme.md → typography.fonts/faces |
| Backgrounds | --bg-<layer>-* | surface image layers | theme.md → backgrounds |
| Circuits | --c-circuit, --c-border-dashed, --dash-*, --circuit-width | marching-dash borders | circuits.md |
| OS-home | --os-* | the / shell surfaces only | theme.md → os + os-circuits |
| Effects | --flicker-speed, --stagger-step, --crt-speed, --boot-duration, --glitch-duration | motion timing | effects.md |
Rule: the markdown key names are the contract. Reorder/rename comments freely; never rename or move a key across top-level groups.
3. Typography & font roles
Seven semantic roles, each emitting a .font-<role> class + --font-<role> var, plus the
console’s --font-terminal (used directly).
| Role | Class | Used for |
|---|---|---|
title | .font-title | card / hero / section titles |
heading | .font-heading | section labels, eyebrows, metadata headers, prose h2/h3 |
nav | .font-nav | sidebar nav, filter bar, home links, buttons, filter chips |
badge | .font-badge | badges, tag pills, status chips |
body | .font-body | prose, descriptions, excerpts, metadata values; body{} default |
meta | .font-meta | dates, CTA links, captions |
terminal | var(--font-terminal) | the whole console (Terminal, Motd, IdentityModal, Radio, Comments) |
Sizes are --text-*. Mobile shrinks --text-*/--prose-size via global.css @media blocks.
faces[] in theme.md maps family↔font-file (@font-face).
4. Buttons & controls
Six visually-distinct button systems grew up independently:
| # | Component / class | Where | Surface | Border | Font | Key tokens |
|---|---|---|---|---|---|---|
| 1 | Button.astro bevel (via NavButton) | Sidebar nav | beveled raised face (.nav-btn-face light/dark key edges) | none (active adds dashed inset) | .font-nav | --c-accent / -bright / -deep, --c-shadow |
| 2 | LauncherButton.astro (.launcher-btn) | OS-home launcher rail | .card-gradient | DashedBorderFull (marching) | .font-nav | --c-text, --c-link (► CTA), --c-shadow |
| 3 | Button.astro rail (.console-tab) | CLI view rail (ConsoleNav) | transparent (icon + label) | none (dashed rail divider) | --font-terminal | --os-term-text/-accent/-name |
| 4 | Button.astro bevel/ghost | Console form/action (SEND, SAVE…) | bevel raised face (shares §1) / transparent (ghost guest) | bevel edges / 1px accent (ghost) | .font-nav/term | --c-accent* (bevel), --os-term-accent (ghost) |
| 5 | LinkCard.astro (.link-card) | Home quick-links + /links | .link-card | DashedBorderFull (link circuit) | .font-nav | --c-link-hover*, --c-text-neutral |
| 6 | FilterBar chips | Listing filter dropdown | square dashed | border-dashed --c-border | .font-nav | --c-accent (active), --c-border |
Other interactive controls: radio transport (⏮ ▶/⏸ ⏭, seek, volume — --os-term-*), slideshow
dots ([data-dot]), the hub (HubNode, a focusable role=button hotspot), the colour-swatch
grid (IdentityModal / profile view).
🎯 Standardization (§14.1 — in progress): systems 1, 3 and 4 now share one
Button.astroprimitive (variant: bevel|ghost|rail) — the sidebar bevel was reused for the console form actions so the CLI reads as part of the same OS, and the CLI view rail is therailvariant. Only bespoke left: launcher (2,card). See §13 (naming) + §14 (plan).
5. Primitives
| Component | What it is | Notes / tokens |
|---|---|---|
DashedBorderFull.astro | the marching-dash “circuit” border on every framed box | two SVG rects (solid --c-circuit + dashed --c-border-dashed); animated → .dash-animated; geometry from --dash-*. |
Badge.astro | every small pill/label (COMMISSION flag, price, tag) | variant: solid|outline, tone: accent|accentDark|tag, .font-badge |
CategoryTitle.astro | [category] / [title] — category coloured, / grey, title white | --c-category, --c-category-sep, --c-text |
SectionLabel.astro | heading row (slot) above a solid Rule | wraps Rule |
Rule.astro | horizontal solid divider (never animated) | --c-rule |
Logo.astro / LogoMark.astro | brand wordmark / mark | brand SVGs |
.t-line / .t-screen | console “type-in” stagger cascade for lists/cards | --stagger-step; OS shell reuses it for the launcher rail |
6. Cards & media
All cards share .card-gradient + DashedBorderFull + drop-shadow-…var(--c-shadow), and
compose CategoryTitle / Badge.
| Component | Where | Composition |
|---|---|---|
ProjectCard.astro | /projects, home floor | .card-gradient row + ProjectMedia 16:9 thumb + CategoryTitle |
BriefCard.astro | /briefs, home floor | .card-gradient block + CategoryTitle + date (.font-meta) |
AssetCard.astro | /assets | .card-gradient column + Badge (price) |
SpotlightHero.astro | Home floor hero | rotating pinned-project slideshow + CRT glitch (retro.ts) |
OsSpotlight.astro | OS-home left column | compact spotlight card, console-width; wired to the hub (data-os-wire) |
ProjectMedia.astro | Inside cards / detail | image/video cover; autoplay per effects toggles |
CanvasEmbed.astro | Project detail | embedded interactive canvas |
7. Forms & inputs
| Surface | Where | Fields / classes |
|---|---|---|
| Contact form | Console contact view (Terminal.astro) | .os-view-select (reason) + .os-view-input (name/email) + .os-view-textarea (message) + Button (bevel SEND) |
| Identity modal | IdentityModal.astro + identity-modal.ts | email/password/display-name inputs; status input; 36-swatch colour grid |
| Profile view | Console profile view | status input + colour grid + sign-out (Button bevel); sign-in/register bevel, guest ghost |
| Comments | Comments.astro + comments-page.ts | terminal-styled message input |
| Filter bar | FilterBar.astro + filter.ts | search input + FILTERS ▾ chip dropdown |
Shared field look: dashed border on --os-term-surface-deep, --font-terminal, focus → accent
border. Always set font-size:16px on mobile inputs (stops iOS focus-zoom; the contact +
identity inputs already do this).
🎯 Standardization: the console field styles (
.os-view-input/-select/-textarea) and the identity-modal inputs are near-duplicates — candidate for one shared field component/class.
8. Console / CLI (the / + /console shell)
Built from Terminal.astro (markup) + os-terminal.ts (behaviour), driven by terminal.md.
- View shell: a left icon rail (
ConsoleNav.astro, desktop) / bottom bar (mobile) switches the body between views:chat(default) ·radio·contact·help(commands) ·inventory(locked) ·profile. Adding a view = oneNAVentry + a matching[data-view-panel]. State machine:setView(id)togglesdata-view+ panelhidden. - Persistent prompt: the input line (red accent outline) +
/palette sit in.os-term-promptbelow the views, so the command line stays on every view (type/help, still/chatback). Chatviewholds only the scrollback log. - Chat-row vocabulary (styled inline — Astro scoped CSS can’t reach JS-built rows):
name: message(name innameColor),[system]sender (dim, non-clickable.os-term-sysname),[x]admin-delete chip (.os-term-del, admin-only viahidden), guest lines dimmed +[guest]tag,@mentionhighlight, pinned[announce]. - User bar:
● name ▾at the bottom ([data-term-userbar]). /palette + commands view: typeahead command menu fromterminal.md(palette/commands/redirects); built-in behaviour keyed by id inos-terminal.ts. Bare page names route to sections. JS-built rows → styled via:globalunder.os-term-window.- Boot blurb: ASCII header printed once per device (localStorage) at top of chat.
- MOTD (
Motd.astro): bare scrolling ticker across the header (config interminal.md → motd). No box/frame, pure CSS marquee. - Per-page variant:
Comments.astroreuses the same chrome (.cmt-*,--os-term-*, identity modal) minus radio/nav.
Tokens: everything here is --os-term-* (fill, titlebar, text, accent, name) + --font-terminal.
9. Overlays & feedback
| Overlay | File | Notes |
|---|---|---|
| Identity modal | IdentityModal.astro / identity-modal.ts | Login/register/guest + colour. Profile moved to the console view (not this modal). |
| Profile popover | ProfilePopover.astro / profile-popover.ts | Click another user’s name → small popover (role/status). |
| Toasts | notification-toast.ts | Transient notices/announcements. |
10. Layouts & shells
| Shell | File | Used by | Chrome |
|---|---|---|---|
| ShellLayout | ShellLayout.astro | / + /console | full-bleed os-shell; no sidebar/header; owns the viewport; scanlines |
| BaseLayout | BaseLayout.astro | every other page | desktop sidebar (190px, sticky ≥768px) + mobile top bar (48px) |
- OS-home (
/) is an absolute-positioned scene inindex.astro: header row (wordmark + MOTD), NAVIGATION panel (ActionCenterBar), launcher rail (ButtonRail/LauncherButton),OsSpotlight,HubNode,Terminal. Shared layout vars:--os-row-top,--os-col-w(= console width; left column mirrors it),--os-rail-bottom,--os-spot-h. Below 768px it reflows to a vertical stack; the NAVIGATION panel + hero hidden. (No social row on/.) Panel glow halo =os.glow→--os-glow-*(box-shadowon the 4 panels). - Circuit recolour: nav→button
tree/tree-stubcircuits use the NAVIGATION yellow/orange (--os-ac-*); launcher buttons stay RED. OS-page raster images →src/assets/os/+<Image>. - Region split:
/= the only consumer ofos.*+ os-circuits +--font-terminal+ boot +os-parallax. Everything else = global palette/circuits + the sidebar shell.
11. Motion, states & accessibility
- Flicker (
--flicker-speed):data-flickerCRT click-flash on buttons/links (retro.ts). - Stagger (
--stagger-step):.t-lineitems type-in within a.t-screen. - CRT / boot / route transitions: page CRT phases (
--crt-speed), OS power-on (--boot-duration), and the circuit “route via the bus” lead-in (os-route.ts). - State conventions: hover = brighten/tint; active = deepen + (often) translate 1px;
disabled/locked = reduced opacity +
cursor:not-allowed; active nav =.is-active. - Reduced motion: every animation has a
@media (prefers-reduced-motion: reduce)off-ramp. - Toggles:
effects.md→html[data-*]/ runtime flags:scanlines,flicker,marchingAnts,coverAutoplay,videoAutoplay,crtTransition,sidebarParallax,osParallax.
12. Interactive features map
| Feature | UI (components) | Script(s) | Backend |
|---|---|---|---|
| Global chat | Terminal chat view | os-terminal.ts, pb-chat.ts | PocketBase messages (thread chat) |
| Per-page comments | Comments | comments-page.ts, pb-chat.ts | PocketBase messages (per url thread) |
| Identity / auth | IdentityModal | identity-modal.ts, pb-auth.ts | PocketBase users |
| Profile | Console profile view, ProfilePopover | os-terminal.ts, profile-popover.ts | PocketBase users |
| Radio | Radio (radio view) | os-terminal.ts | radio.md (PB streaming later) |
| Contact | Console contact view | os-terminal.ts, pb-auth.ts | PocketBase contact / bug_reports / wishlist |
| Inventory | Locked view | — | PocketBase items/user_inventory (scaffold) |
| Filter/search | FilterBar | filter.ts | static (client) |
| Theme editor | /dev/theme | dev-theme.ts | dev-only live preview |
| Circuit wiring | CircuitTraces, HubNode | os-circuit.ts, os-route.ts | — (see docs/CIRCUIT-SPEC.md) |
13. Control-naming convention (APPLIED)
Goal: a control’s name says what it controls / does, not how it looked in the original
design. This naming pass is applied in code — the table records each rename (earlier name → the
name now live). E.g. .bevel-face described a bevel (a visual), not a purpose.
Rule: <Domain><Role> for components, <scope>-<role> for classes.
| Earlier name | Component (now) | Class (now) | Controls / does |
|---|---|---|---|
BevelButton.astro / .bevel-face | Button.astro (via NavButton) | .nav-btn-face / -active | navigates to a site section (sidebar) |
RailButton.astro / .os-rail-btn | LauncherButton.astro | .launcher-btn | launches a section from the OS home |
.os-nav-item (ConsoleNav) | Button.astro rail (in ConsoleNav) | .console-tab | switches the active console view |
.os-view-btn / .console-action | Terminal.astro views | Button bevel/ghost | submits/acts inside a console view |
HomeLink.astro / .link-gradient | LinkCard.astro | .link-card | opens an external link |
FilterBar chip | FilterBar.astro | .filter-chip | toggles a category/tag filter |
Badge.astro | Badge.astro | .badge | already intent-named ✅ |
Note:
FilterBarchips already used.filter-chip(no change needed); the container classes (.os-nav,.os-rail) and--os-*tokens stay as theme contract, not renamed.
14. 🎯 Standardization opportunities (PROPOSALS — not yet applied)
- Unify the button systems into one
Buttonprimitive with avariant(bevel/card/ghost/solid) and a token group, then reskin systems 1–4 (§4) through it. - One shared form-field component/class for
.os-view-input/-select/-textarea+ the identity-modal inputs (§7), with the 16px mobile guard built in. - Token de-duplication — the “twinned knobs” (STYLE-MAP) hold equal values today; decide which should stay independent vs collapse.
- Consistent CTA glyph — finish the literal-
>→ red chevron (►) cleanup. Apply the §13 naming convention in a focused rename pass.Done — see the §13 table for the applied renames.
Sequencing: §13 names are done. The §14.1 button primitive (same files) remains — do it behind a green
check/build, one button system at a time.
15. Forker / themeability guide
To make this your own portfolio:
- Colours/type/motion: edit
src/content/site/theme.md(+circuits.md,effects.md). Use/dev/themeto preview, then Export YAML. Rememberos.*colours are separate frompalette.*(recolour both for the homepage). - Brand: swap the logo SVGs (
Logo/LogoMark,/brand/*) and the hub mark (/brand/dp-logo.svg) + ring (public/media/site-media/hub_ring.svg). - Content: drop markdown into
src/content/{projects,briefs,assets,links}and editabout/about.md; seeAUTHORING.md. Console copy + commands live interminal.md; radio inradio.md. - Backends (optional): chat/auth/comments need PocketBase
(
scripts/pb-setup.mjsprovisions collections). Without them the static site still builds. - Don’t: hard-code hex/px in components, rename a theme key across groups, or add a DB/server runtime — keep it static + token-driven.
Create a PanicOS account (email + password) to hold a verified name across devices and keep your message streak. Or continue as a guest — a guest name is set on THIS device only and shows as @guest.
CHOOSE A DISPLAY NAME
STATUS