Merges workspace.html + ralph.html into a single unified project hub with: - Cookie-based auth (DASHBOARD_TOKEN, HttpOnly, SameSite=Strict) - 9-state project badge system (running-ralph/manual, planning, approved, pending, blocked, failed, complete, idle) with BUTTONS_FOR_STATE matrix - SSE realtime + polling fallback, version-based optimistic concurrency (If-Match) - Planning chat modal (phase stepper, markdown bubbles, 50s+ wait state, auto-resume) - Propose modal (Variant B: inline Plan-with-Echo checkbox) - 5-type toast taxonomy (success/info/warning/busy/error, 3px colored left-bar) - Inter font self-hosted + shared tokens.css design system + DESIGN.md - src/jsonlock.py (flock helper, sidecar .lock for stable inode) - src/approved_tasks_cli.py (shell-safe wrapper for cron/ralph.sh) - 55 new tests (T#1–T#30) + real jsonlock bug fix caught by T#16/T#28 - No emoji anywhere (enforced by test_dashboard_no_emoji.py) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
10 KiB
Echo Dashboard — Design System
This document is the source of truth for visual decisions across the Echo
Dashboard (port 8088, served at /echo/). Tokens live in
dashboard/static/tokens.css. Page-level CSS is in common.css and per-page
<style> blocks. Pages must include tokens.css before common.css.
Theme
- Default: dark. Background
--bg-base: #13131a(near-black neutral). - Light theme: opt-in via
<html data-theme="light">. Light tokens override the dark palette in the same:root-equivalent block. - Toggle: header
.theme-togglebutton — persisted inlocalStorage.
Surfaces are translucent overlays on --bg-base, never solid greys, so
elevation reads consistently against future backgrounds.
Color tokens
Surfaces (dark)
| Token | Value | Use |
|---|---|---|
--bg-base |
#13131a |
App background |
--bg-surface |
rgba(255,255,255,0.12) |
Cards, panels, inputs |
--bg-surface-hover |
rgba(255,255,255,0.16) |
Hover state on surfaces |
--bg-surface-active |
rgba(255,255,255,0.20) |
Pressed / active surfaces |
--bg-elevated |
rgba(255,255,255,0.14) |
Selects, popovers |
--header-bg |
rgba(19,19,26,0.95) |
Sticky header backdrop |
Text
| Token | Value | Use |
|---|---|---|
--text-primary |
#ffffff |
Headings, key labels |
--text-secondary |
#f5f5f5 |
Body copy |
--text-muted |
#e5e5e5 |
Meta, timestamps, captions |
Accent + borders
| Token | Value | Use |
|---|---|---|
--accent |
#3b82f6 |
Primary buttons, focus, links |
--accent-hover |
#2563eb |
Hover on --accent |
--accent-subtle |
rgba(59,130,246,0.2) |
Active nav background |
--border |
rgba(255,255,255,0.3) |
Card / input outline |
--border-focus |
rgba(59,130,246,0.7) |
Card hover, input focus |
Semantic state
| Token | Value | Meaning |
|---|---|---|
--success |
#22c55e |
OK, saved, healthy |
--warning |
#eab308 |
Caution, soft fail |
--error |
#ef4444 |
Hard fail, destructive |
Status palette (workflow states)
These drive the .status-pill[data-status] system on workspace cards.
| Token | Value | State name |
|---|---|---|
--status-running |
rgb(34, 197, 94) |
running-ralph, running-manual |
--status-blocked |
rgb(245, 158, 11) |
blocked |
--status-failed |
rgb(239, 68, 68) |
failed |
--status-complete |
rgb(156, 163, 175) |
complete |
--status-idle |
var(--text-muted) |
idle |
--status-planning |
rgb(167, 139, 250) |
planning (new) |
--status-pending |
rgb(96, 165, 250) |
pending (new) |
--status-approved |
rgb(234, 179, 8) |
approved (new) |
Typography
- Sans:
'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif— self-hosted woff2 at/echo/static/fonts/inter-{400,500,600,700}.woff2. - Mono:
'JetBrains Mono', 'Fira Code', ui-monospace, monospace— for logs, code blocks, slugs, IDs. Loaded by browser if present (not bundled).
Size scale
| Token | rem | px @ 16px |
|---|---|---|
--text-xs |
0.75 | 12 |
--text-sm |
0.875 | 14 |
--text-base |
1 | 16 |
--text-lg |
1.125 | 18 |
--text-xl |
1.25 | 20 |
Weights
400 (body), 500 (medium emphasis), 600 (headings, button labels), 700 (rare — page titles only). No 800/900.
Spacing — 8px grid
All padding, margin, and gap values use these tokens. No hard-coded pixels.
| Token | px |
|---|---|
--space-1 |
4 |
--space-2 |
8 |
--space-3 |
12 |
--space-4 |
16 |
--space-5 |
20 |
--space-6 |
24 |
--space-8 |
32 |
--space-10 |
40 |
Border radius
| Token | px | Use |
|---|---|---|
--radius-sm |
4 | Tags, micro-pills |
--radius-md |
8 | Buttons, inputs |
--radius-lg |
12 | Cards, modals, panels |
--radius-full |
9999 | Status pills, badges, avatars |
Buttons
All buttons share .btn (8px radius, 14px font, 8/16 padding,
--transition-fast).
| Variant | Class | Surface | Text | Use |
|---|---|---|---|---|
| Primary | .btn-primary |
--accent |
white | The one CTA per row |
| Secondary | .btn-secondary |
--bg-surface + --border |
--text-secondary |
Side actions |
| Ghost | .btn-ghost |
transparent | --text-secondary |
Tertiary, destructive-soft |
| Danger | .btn-danger |
--error |
white | Stop, delete, irreversible |
Disabled state: opacity: 0.5; cursor: not-allowed;. Never grey out by
swapping colors — keep variant identity.
Card component (.project-card)
border-radius: var(--radius-lg)(12px)background: var(--bg-surface)border: 1px solid var(--border)padding: var(--space-5)transition: border-color var(--transition-base)- Hover:
border-color: var(--border-focus)(blue glow). No surface brightening — border-only hover keeps the grid calm.
Status pill system
A .status-pill is a --radius-full chip placed on every project card. It
encodes the current workflow state via data-status="<state>".
Visual recipe
- Background: state color at 18% alpha (
color-mix(in srgb, var(--status-X) 18%, transparent)or precomputedrgba(...)). - Text: solid state color (full alpha).
- Border: 1px state color at 30% alpha.
- Padding:
var(--space-1) var(--space-3)— slim. - Font:
var(--text-xs), weight 500.
Pulse-dot
Active states render a 6px CSS-shape circle that pulses (no SVG, no emoji).
.status-pill::before {
content: ""; width: 6px; height: 6px; border-radius: 50%;
background: currentColor; margin-right: var(--space-2);
}
.status-pill[data-status="running-ralph"]::before,
.status-pill[data-status="running-manual"]::before,
.status-pill[data-status="planning"]::before {
animation: pulse-dot 1.6s ease-in-out infinite;
}
@keyframes pulse-dot { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
State matrix
data-status |
Color token | Pulse | Label |
|---|---|---|---|
running-ralph |
--status-running |
yes | Ralph running |
running-manual |
--status-running |
yes | Manual run |
planning |
--status-planning |
yes | Planning |
approved |
--status-approved |
no | Approved |
pending |
--status-pending |
no | Pending |
blocked |
--status-blocked |
no | Blocked |
failed |
--status-failed |
no | Failed |
complete |
--status-complete |
no | Complete |
idle |
--status-idle |
no | Idle |
BUTTONS_FOR_STATE matrix
Each project card surfaces ≤3 actions, ordered Primary / Secondary / Ghost.
The renderer picks the row matching data-status.
| State | Primary | Secondary | Ghost |
|---|---|---|---|
running-ralph |
Stop Ralph (danger) | Logs | PRD |
running-manual |
Stop (danger) | Open server | Logs |
planning |
Continue chat | — | Cancel |
approved |
— | Unapprove | Plan |
pending |
Approve | Plan with Echo | Cancel |
blocked |
View logs | Resume | — |
failed |
View logs | Retry | Rollback |
complete |
View plan | — | Run again |
idle |
Run Ralph | — | Delete |
Rules:
- Stop / Delete are always
.btn-danger, never primary blue. - A dash (
—) means render nothing (no placeholder, no greyed-out slot). - The Primary slot is the default action when the card is keyboard-focused and Enter is pressed.
Toast taxonomy
Toasts appear top-right, stack vertically, dismiss after 4s (errors: 8s).
Five types, distinguished by a 3px colored left bar — no emoji, no icon
fill. Body uses --text-primary on --bg-surface.
| Type | Bar color | Use |
|---|---|---|
success |
--success |
Saved, approved, deployed |
info |
--accent |
Neutral confirmation |
warning |
--warning |
Soft fail, retried |
busy |
--status-planning |
Long-running op started |
error |
--error |
Hard fail, action required |
Toast renderer is shared across pages and reads from a single global
window.showToast(type, msg) helper.
SSE indicator
Top-right of pages with a live stream (workspace, ralph). Three states indicated via a CSS-shape pulse-dot — never an emoji.
| State | Dot color | Label | Pulse |
|---|---|---|---|
| Live | --success |
"Live" | yes |
| Polling | --warning |
"Polling" | no |
| Offline | --error |
"Offline" | no |
Uses the same .pulse-dot 6px CSS shape as .status-pill::before. The dot
sits before the label, both inside a tiny .sse-indicator chip on
--bg-surface.
Modal pattern
Used for the planning chat, PRD viewer, log tail, propose-feature form.
- Overlay: full viewport,
background: rgba(0,0,0,0.6),backdrop-filter: blur(4px),display: flexcentered. - Container (
.modal):--radius-lg,--bg-base,--border, max-width 720px, max-height 80vh, scroll on overflow. - Header / Footer: 1px border separators using
--border. - Focus trap: first focusable element gets focus on open; Tab cycles inside the modal.
- ESC: closes — but if the modal has unsaved input, prompt "Discard changes?" before closing. Click on overlay = same behavior.
- Mobile (≤640px): full-screen takeover. Header / footer stick; body
scrolls. Implemented in
tokens.cssvia the shared@media (max-width:640px)block.
No-emoji rule
No emoji anywhere in the dashboard. This is a hard rule, not a preference.
- Buttons are text-only. No leading/trailing emoji decoration.
- Status indicators use CSS-shape colored dots (
.pulse-dot,.status-pill::before) — never🟢 ⏱ 🛑 ✅etc. - The login monogram is the letter
Erendered in Inter 700 inside a square with--accentbackground. Not an emoji, not an SVG logo. - Where icons are needed (nav, action buttons), use Lucide-style stroke
SVGs inlined —
stroke: currentColor,fill: none,stroke-width: 2,stroke-linecap: round,stroke-linejoin: round. Never use emoji as a substitute for an icon.
This rule keeps the UI legible across themes, scales correctly at all sizes, and avoids OS-dependent rendering (Apple, Twemoji, Noto all draw the same emoji differently).
Pages that include this system
Every dashboard page (index.html, workspace.html, ralph.html, notes.html,
habits.html, files.html, login.html) must include in <head>:
<link rel="stylesheet" href="/echo/static/tokens.css">
<link rel="stylesheet" href="/echo/common.css">
In that order — tokens first so common.css and per-page styles can resolve
the variables.