.cursor/skills/ui-guidelines/SKILL.md
UI design guidelines for WOD Brains app - mobile-first, app-like design with consistent patterns. Use when making UI changes.
npx skillsauth add jdconley/wodbrains ui-guidelinesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Mobile-first, app-like design:
All colors are defined as CSS variables in apps/web/src/style.css:
/* Backgrounds */
--bg-deep: #0a0a0a; /* Deepest background, full-screen shells */
--bg-surface: #141414; /* Cards, elevated surfaces */
--bg-elevated: #1a1a1a; /* Hover states, inputs */
--bg-overlay: rgba(0, 0, 0, 0.85); /* All overlay backgrounds */
/* Text */
--text: #ffffff; /* Primary text */
--text-muted: #737373; /* Secondary text */
--text-inverse: #000000; /* Text on accent backgrounds */
/* Accent */
--accent: #ff10f0; /* Primary accent (neon pink) */
--accent-dim: #cc0dc0; /* Hover/pressed state */
--accent-glow: rgba(255, 16, 240, 0.4); /* Glow effects */
--accent-subtle: rgba(255, 16, 240, 0.1); /* Subtle accent backgrounds */
/* Status */
--danger: #ff3b3b; /* Destructive actions */
--danger-subtle: rgba(255, 59, 59, 0.1); /* Subtle danger backgrounds */
/* Border (for inputs only) */
--border: #262626;
/* Separators + list highlights */
--separator: rgba(255, 255, 255, 0.08);
--list-highlight: rgba(255, 255, 255, 0.04);
Rules:
--bg-overlay for all overlay backgrounds (standardized to 0.85 opacity)--text-inverse for text on accent-colored buttons--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
--radius-sm: 8px; /* Inputs, small controls */
--radius-md: 16px; /* Cards, overlays */
--radius-full: 999px; /* Pill label buttons + circular buttons */
Font families:
Inter, system-ui, sans-serifRubik, Inter, system-ui, sans-serif"JetBrains Mono", "SF Mono", "Fira Code", monospaceFont weights (standard values only):
Never use: 450, 650, 750, or other non-standard weights.
The app uses stack-based navigation like iOS:
/ (import) ← ROOT (no back button)
├── /workouts ← back to /
└── /w/{id} (definition) ← back to /
├── /w/{id}/edit (timer-edit) ← back to /w/{id}
└── /r/{id} (run) ← back to /w/{id}
Use the shared header component (apps/web/src/components/header.ts):
import { appHeader, setupAppHeader } from '../components/header';
// In your page render:
root.innerHTML = `
<div class="PageShell">
${appHeader()}
<main class="PageContent">
...
</main>
</div>
`;
setupAppHeader(root);
With custom back target:
appHeader({ backTarget: `/w/${definitionId}` });
setupAppHeader(root, {
backTarget: `/w/${definitionId}`,
onBeforeBack: () => {
// Return false to cancel navigation
if (hasUnsavedChanges()) {
showConfirmDialog();
return false;
}
return true;
},
});
Header visibility (run page):
import { setAppHeaderVisible } from '../components/header';
// Hide header when timer is running
setAppHeaderVisible(root, status !== 'running');
All pages use full-screen shells with min-height: 100dvh:
ImportShell - Centered content, import pagePageShell - Standard pages (workouts, timer-edit)DefinitionShell - Definition view with actions footerRunShell - Timer run page (grid layout)max-width: 480px--content-pad-x (left/right)--content-pad-y (top/bottom)PageContent and DefinitionContent so all content pages share the same inset.PageContent as the base container class for content pages. This is the “source of truth” for shared width/insets, and for the desktop card container styling.class="PageContent ImportContent") and keep the modifier limited to the delta (centering behavior, special gaps, etc.)..ImportContent). If a page needs the same desktop card as other pages, it should opt into it by using PageContent./). This applies to both mobile and desktop.max-width as PageContent on desktop.centerSlot: 'logo' for the default brand header (mobile shows mascot logo, desktop shows “WOD Brains” brand).centerSlot: 'title' for pages that should show a title in the header (e.g. About: “About WOD Brains”).centerSlot: 'titleInput' for editable header titles (timer edit).All shells have a fade-in animation:
@keyframes pageIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.PageShell {
animation: pageIn 0.2s ease-out;
}
Default list style across the app:
--separator)--list-highlight)Markup guidance:
div containers, set role="list" and role="listitem" on rows.List pattern (flat, no bullets)CSS sketch (reference only):
.MyList {
display: flex;
flex-direction: column;
gap: 0;
}
.MyListItem {
padding: 10px 0;
border-bottom: 1px solid var(--separator);
background: transparent;
}
@media (hover: hover) {
.MyListItem:hover {
background: var(--list-highlight);
}
}
.MyListItem:active {
background: var(--list-highlight);
}
Labeled buttons are pill-like and flat (no shadows). Buttons do not have borders. Use background color for states:
/* Labeled buttons are pill-shaped */
.PrimaryBtn,
.SecondaryBtn,
.GhostBtn,
.DangerBtn {
border-radius: var(--radius-full);
}
/* Primary - accent background */
.PrimaryBtn {
background: var(--accent);
color: var(--text-inverse);
}
/* Secondary - elevated background */
.SecondaryBtn {
background: var(--bg-elevated);
color: var(--text);
}
/* Ghost - transparent, subtle hover */
.GhostBtn {
background: transparent;
color: var(--text-muted);
}
.GhostBtn:hover {
background: var(--bg-elevated);
color: var(--text);
}
/* Danger - destructive action */
.DangerBtn {
background: var(--danger);
color: var(--text-inverse);
}
Notes:
.DangerBtn for destructive labeled actions (e.g., Discard).<button> or <a>; both should render with a 44px min height.For action buttons, prefer icons over text labels. Example from definition page:
<button class="DefinitionAction DefinitionAction--start">
<svg><!-- play icon --></svg>
</button>
Full-screen overlays with semi-transparent backdrop:
.MyOverlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-overlay);
z-index: 120;
}
Overlay cards have no borders:
.OverlayCard {
border: none;
border-radius: var(--radius-md);
background: var(--bg-surface);
}
Special behaviors:
pageIn - Page entrance (0.2s ease-out)repBump - Rep count bump (0.18s ease)countdownPulse - Countdown numbers (0.8s ease-out)repCelebrationFade - Rep celebration (2.5s ease-out)logoBounce - Generate loading (1.2s infinite)sparkle - Sparkle effects (1.5s ease-out)0.15s0.2s-0.3s ease-out0.5s ease-outRule: Never interpolate user data into innerHTML.
document.createElement + textContent for all dynamic contentinnerHTML is only allowed for static templates (no user data) or for clearing elements (innerHTML = '')apps/web/src/utils/dom.tsExample:
const title = document.createElement('h2');
title.textContent = workoutTitle; // always safe
Use the shared meta helper for every page:
apps/web/src/meta.ts → updateMeta({ title, description, url })import { appHeader, setupAppHeader } from '../components/header';
import { updateMeta } from '../meta';
export function renderMyPage(root: HTMLElement) {
updateMeta({
title: 'My Page - WOD Brains',
description: 'Describe what this page does for search and sharing.',
url: new URL('/my-page', window.location.origin).toString(),
});
root.innerHTML = `
<div class="PageShell">
${appHeader()}
<main class="PageContent">
<h1 class="PageTitle">My Page</h1>
<!-- Content here -->
</main>
</div>
`;
setupAppHeader(root);
// Your page logic...
}
--bg-overlay and have no card bordersinnerHTML)testing
Always create and run tests affected by changes, including Playwright for UI changes. Use when modifying Wodbrains features or UI.
development
Run Wodbrains worker parse evals and live Gemini tests locally using Wrangler `.dev.vars` keys. Use when you need to run `parse.evals.test.ts` / `parse.gemini.test.ts` against the real model (RUN_LIVE_AI_TESTS=1).
documentation
Regenerate README screenshots and the demo video for WOD Brains.
testing
Generate and update the WOD Brains OG image PNG/JPG using the mascot SVG and a Playwright-rendered layout. Use when changing OG image copy or layout.