feat(dashboard): unified workspace hub — cookie auth, 9-state projects, planning chat
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>
This commit is contained in:
317
dashboard/DESIGN.md
Normal file
317
dashboard/DESIGN.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# 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-toggle` button — persisted in `localStorage`.
|
||||
|
||||
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 precomputed `rgba(...)`).
|
||||
- **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).
|
||||
|
||||
```css
|
||||
.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: flex` centered.
|
||||
- **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.css` via 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 `E`** rendered in Inter 700 inside a
|
||||
square with `--accent` background. 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>`:
|
||||
|
||||
```html
|
||||
<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.
|
||||
Reference in New Issue
Block a user