Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 39 additions & 20 deletions docs/DESIGN_SYSTEM.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Design system

**Status:** current
**Last verified:** 2026-10-16 (theme architecture + tokens)
**Last verified:** 2026-10-29 (diff review language for public pages; theme architecture + tokens)

StackPulse **UI design system** and application architecture. Read this before adding pages, components, or visual changes so new work matches the terminal-inspired product shell.

Expand Down Expand Up @@ -89,9 +89,24 @@ flowchart TB

- Dark void or off-white background with dotted grid + subtle noise (`globals.css` `body::before/after`)
- Monospace voice for headings, labels, CLI copy, errors
- “Window chrome” panels: traffic-light dots, title bars, faux shell paths (`~/dashboard/feed`)
- Syntax-inspired accents on semantic meaning (lime = primary action, rose = danger/breaking)
- Lowercase headings with a **lime full stop** after the title: `your feed<span class="text-lime">.</span>`

**Public pages use the "diff review" language** (homepage, stacks, skills, status, legal,
sign-in, 404/error, digest unsubscribe): every page reads like a pull request.

- Page intro: state badge + breadcrumb path (`stackpulse / stacks / react`), then a sentence-case
mono title, lede and actions (`PageIntro`).
- Panels are changed files: a file-path header that sticks under the site header, then content
(`FileCard`). Diff gutters (`DiffLine`: `+` emerald, `-` rose, `@@` hunk cyan) are used where the
content really is a list of changes or a file.
- Risky release notes are review comments from `stackpulse` with a semantic badge (`ReviewComment`).
- Real counts render as CI-style checks or stat tiles (`Checks`, `StatGrid`); never invent numbers.
Example data is labelled `example`.
- Calls to action are a merge box (`MergeBox`).

The signed-in app (dashboard, onboarding, settings) still uses the original window-chrome panels
(`.frame` + `.frame-titlebar` + `win-dots`) and lowercase headings with a **lime full stop**
(`your feed<span class="text-lime">.</span>`). New public surfaces use the diff review language.

**Do not introduce:**

Expand Down Expand Up @@ -195,12 +210,12 @@ Fonts loaded in `src/app/layout.tsx` (`Geist`, `Geist_Mono`).

| Pattern | Usage |
|---------|--------|
| **Sticky header** | `sticky top-0 z-30 border-b border-line bg-void/80 backdrop-blur`, height `h-14`, `max-w-7xl px-6` |
| **Content width** | Landing/stacks: `max-w-7xl`; dashboard feed: `max-w-4xl`; legal: `max-w-3xl`; auth/errors: `max-w-md`–`max-w-lg` |
| **Page intro block** | Kicker → `h1` → `p` with `animate-fade-up` |
| **Frame panel** | `.frame` + optional `.frame-titlebar` + `win-dots` — cards, auth, errors, empty states |
| **Primary button** | `rounded-md bg-lime px-4 py-2.5 font-mono font-semibold text-void hover:bg-lime/85` with optional `$` prefix |
| **Secondary button** | `border border-ruling bg-shade … hover:border-edge hover:bg-lift` |
| **Site header** | Public pages: `SiteHeader` (`components/site/site-header.tsx`), sticky `h-14`, `bg-void/85 backdrop-blur-md`; app pages keep their own sticky header |
| **Content width** | Public: `max-w-6xl` (release `max-w-5xl`, upgrade `max-w-4xl`, legal `max-w-3xl`); dashboard feed: `max-w-4xl`; auth/errors: `max-w-md` (`CenteredShell`) |
| **Page intro block** | Public: `PageIntro`. App: kicker → `h1` → `p` with `animate-fade-up` |
| **File card** | Public: `FileCard` — `rounded-xl border-line bg-shade`, `bg-lift` file-path header sticky at `top-14` (`overflow-clip`, not `overflow-hidden`) |
| **Frame panel** | App only: `.frame` + optional `.frame-titlebar` + `win-dots` |
| **Buttons** | `button.primary` / `button.secondary` / `button.ghost` from `components/review/review-ui.tsx`: `min-h-11 rounded-lg font-mono text-[13px]`, lime primary with `text-void` |
| **Destructive** | `bg-rose text-void` or rose text on rose/5 hover bg |

Reuse `<Logo />` in headers; link home with `hover:opacity-80 transition-opacity`.
Expand All @@ -215,7 +230,7 @@ Reuse `<Logo />` in headers; link home with `hover:opacity-80 transition-opacity
| Tool | When |
|------|------|
| `animate-fade-up`, `animate-fade-in`, `stagger-*` | Page enter, lists |
| `motion/react` | Landing hero only (`stack-pulse-hero.tsx`) — avoid sprinkling motion elsewhere |
| `animate-diff-strike`, `animate-diff-land` | Homepage hero only: the removed line strikes through, then the added line lands. Pair with `motion-reduce:animate-none` |
| `PulseLoader` | Loading states (not spinners) |
| `.skeleton` | Placeholders — pulse, not shimmer gradient |

Expand All @@ -231,26 +246,30 @@ Respect `prefers-reduced-motion` (already in `globals.css`).
| `PulseLoader` | `components/ui/pulse-loader.tsx` | Inline/sm/lg loading |
| `Button` | `components/ui/button.tsx` | shadcn/base-ui when variant system helps |
| `LegalShell` | `components/landing/legal-shell.tsx` | Privacy/terms layout |
| `DigestSignupForm` | `components/landing/digest-signup-form.tsx` | Email capture block |
| `DigestSignupForm` | `components/landing/digest-signup-form.tsx` | Email capture block (`framed={false}` inside another card) |
| `SiteHeader`, `SiteFooter` | `components/site/` | Public header (sticky, theme switcher, optional `account` slot) and footer |
| `PageIntro`, `FileCard`, `DiffLines`/`DiffLine`, `ReviewComment`, `MergeBox`, `Checks`, `StatGrid`, `CardNote`, `Badge`, `CenteredShell`, `button` | `components/review/review-ui.tsx` | Diff review building blocks for public pages; server-safe |

New surfaces should compose these before inventing parallel patterns.

### Copy voice

- **Lowercase** for UI chrome: `sign in`, `your feed`, `configure stack`
- **CLI metaphor:** `$ command`, `// comments`, `cd ..`, `exit code 1`, `→ hints`
- **Errors:** plain language, monospace, rose for fault — no corporate alert boxes
- **Lowercase** for small UI chrome: nav links, file paths, badges (`stacks`, `sign in`, `breaking`)
- **Sentence case** for public headings, ledes and buttons: `Review what breaks.`, `Sign in with GitHub`
- **Review metaphor** on public pages: file paths, `+`/`-` lines, `@@` hunks, "stackpulse reviewed";
the app keeps the CLI metaphor (`$ command`, `// comments`, `exit code 1`)
- **Errors:** plain language, monospace, rose for fault — say what happened and the next step
- **Avoid title case** in headings and buttons unless proper nouns (GitHub, StackPulse)

### Page templates

When adding a page, pick the closest template:

1. **Marketing / landing** — `page.tsx`: hero section, `StackPulseHero`, frame blocks, footer with mono links
1. **Marketing / landing** — `page.tsx`: diff hero, checks, files-changed sidebar + `FileCard`s, `MergeBox`, `SiteFooter`
2. **App shell** — sticky header + kicker/title + main (`dashboard`, `onboarding`)
3. **Public content** — stacks layout: header, stats, release cards with signal badges
4. **Auth / empty / error** — centered `frame` with titlebar + terminal copy
5. **Legal** — `LegalShell` + `legal-prose` class
3. **Public content** — `SiteHeader` + `PageIntro` (stats `FileCard` in `aside`) + `FileCard`s + `SiteFooter`
4. **Auth / empty / error** — `CenteredShell` + `FileCard` (sign-in keeps the same look inline)
5. **Legal** — `LegalShell` (`PageIntro` + `FileCard` + `legal-prose`)

---

Expand Down Expand Up @@ -286,8 +305,8 @@ release_fetch_runs (audit)
Before shipping UI or architecture changes:

- [ ] Uses design tokens (`text-dust`, `border-line`, `bg-lime`) — no ad-hoc hex
- [ ] Headings follow mono + lowercase + lime `.` pattern where appropriate
- [ ] Panels use `frame` / sticky header patterns from sibling pages
- [ ] Public pages use `SiteHeader`/`PageIntro`/`FileCard`/`SiteFooter`; app pages keep mono + lowercase + lime `.` headings
- [ ] Panels use `FileCard` (public) or `frame` (app) patterns from sibling pages
- [ ] Badges match semantic tone table above
- [ ] Icons from `hugeicons-react` in product surfaces
- [ ] Loading uses `PulseLoader` or `.skeleton`
Expand Down
57 changes: 25 additions & 32 deletions src/app/(auth)/sign-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,49 +29,43 @@ export default function SignInPage() {
return (
<main className="relative z-10 flex min-h-screen items-center justify-center px-4 py-12">
<div className="w-full max-w-md animate-fade-up">
<div className="mb-6 flex items-center justify-between">
<Link href="/" className="hover:opacity-80 transition-opacity">
<div className="mb-5 flex items-center justify-between">
<Link href="/" aria-label="StackPulse home" className="hover:opacity-80 transition-opacity">
<Logo size="sm" animated />
</Link>
<div className="flex items-center gap-3">
<ThemeSwitcher />
<Link
href="/"
className="inline-flex items-center gap-1.5 font-mono text-[11px] text-fade hover:text-dust transition-colors"
className="inline-flex items-center gap-1.5 font-mono text-[12px] text-dust hover:text-ink transition-colors"
>
<ArrowLeft01Icon className="w-3 h-3" />
cd ..
<ArrowLeft01Icon aria-hidden="true" className="w-3 h-3" />
Back
</Link>
</div>
</div>

<div className="frame overflow-hidden">
<div className="frame-titlebar">
<span className="win-dots">
<span style={{ background: '#fb7185' }} />
<span style={{ background: '#fbbf24' }} />
<span style={{ background: '#34d399' }} />
<div className="overflow-hidden rounded-xl border border-line bg-shade">
<div className="flex items-center justify-between gap-3 border-b border-line bg-lift px-4 py-2.5 font-mono text-[12px] text-dust">
<span className="text-ink">auth/sign-in</span>
<span className="rounded-full border border-emerald/30 bg-emerald/10 px-2 text-emerald">
github only
</span>
<span className="text-dust">~/auth/sign-in.sh</span>
<span className="ml-auto text-mute">secure</span>
</div>

<div className="px-6 py-7">
<div className="font-mono text-[11px] text-fade tracking-[0.2em] uppercase">
<span className="text-lime">$</span> auth --login --provider github
</div>
<h1 className="mt-2 font-mono text-2xl font-bold tracking-tight text-ink lowercase">
welcome back<span className="text-lime caret" />
<h1 className="font-mono text-2xl font-bold tracking-[-0.03em] text-ink">
Welcome back.
</h1>
<p className="mt-1.5 text-[13px] text-dust">
we use github sign-in only — same account you use to ship code.
<p className="mt-2 text-[14px] leading-relaxed text-dust">
We use GitHub sign-in only, the same account you use to ship code.
</p>

<button
type="button"
onClick={handleGithubSignIn}
disabled={loading}
className="mt-7 w-full rounded-md bg-lime px-4 py-3 font-mono text-[13px] font-semibold text-void hover:bg-lime/85 transition-colors disabled:opacity-80 inline-flex items-center justify-center gap-2.5"
className="mt-7 min-h-11 w-full rounded-lg bg-lime px-4 py-3 font-mono text-[13px] font-semibold text-void hover:bg-lime/85 transition-colors disabled:opacity-80 inline-flex items-center justify-center gap-2.5"
>
{loading ? (
<PulseLoader size="inline" tone="dark" label="redirecting…" />
Expand All @@ -85,27 +79,26 @@ export default function SignInPage() {
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
<span>continue with github</span>
<span>Continue with GitHub</span>
</>
)}
</button>

{error && (
<p className="mt-4 font-mono text-[12px] text-rose" role="alert">
<span className="text-fade">→ </span>
<span>error: {error}</span>
<p className="mt-4 font-mono text-[12.5px] text-rose" role="alert">
Could not sign in with GitHub. Try again in a moment.
</p>
)}

<div className="mt-6 font-mono text-[11px] text-fade leading-relaxed">
<p>{'// no email, no password, no spam.'}</p>
<p>{'// we read your public profile and email only.'}</p>
</div>
<ul className="mt-6 grid gap-1 font-mono text-[12px] leading-relaxed text-dust">
<li>No password and no spam.</li>
<li>We read your public profile and email only.</li>
</ul>
</div>

<div className="px-6 py-4 border-t border-line bg-void/40 font-mono text-[12px] text-fade flex items-center justify-between">
<span>new here?</span>
<span className="text-dust">same button works for new accounts.</span>
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-line px-6 py-4 font-mono text-[12px] text-dust">
<span className="text-ink">New here?</span>
<span>The same button creates your account.</span>
</div>
</div>
</div>
Expand Down
Loading
Loading