summaryrefslogtreecommitdiff
path: root/web/style.css
diff options
context:
space:
mode:
authorClaude Sonnet 5 <noreply@anthropic.com>2026-07-04 05:31:42 +0000
committerClaude Sonnet 5 <noreply@anthropic.com>2026-07-04 05:31:42 +0000
commitd105eca610b0e737c3313e4978d6a917b4f55d10 (patch)
tree2ad75a33abd45ebb8225dcbee01a2d0a8d7a21ed /web/style.css
parent8cb291ad7cdb317ff80947278ee055b1a4925b41 (diff)
feat(web): add Stories tab -- Kanban board, epic swimlanes, story-detail DAG (Phase 9a)
First UI work on top of the harness's 8 backend phases -- pure web/* frontend, no backend changes, keeping the existing embedded vanilla-JS convention (no build step, no framework). - Kanban board: columns spanning the full story lifecycle (DISCOVERY/FRAMING -> BACKLOG -> PRIORITIZED -> IN_PROGRESS -> VALIDATING/REVIEW_READY -> DONE), cards showing priority/acceptance-criteria count/eval-verdict progress. Drag-and-drop reorders priority within a column (persisted via a partial PUT /api/stories/{id}); cross-column drag is disabled outright (a dragover handler only preventDefault()s when the dragged card's own status already maps to that column) rather than allowed-then-snapped-back, since the latter would visibly misplace a card for several seconds until the next poll -- reads as a bug, not a feature. - Epic swimlanes: stories grouped client-side by epic_id (incl. an "Unassigned" lane) with a DONE/total progress meter per epic, built from data the board already fetches (avoids N+1 GET /api/epics/{id}/stories calls for a result that's a strict subset of the already-loaded list). - Story detail modal: metadata/spec/acceptance criteria, a BFS-layered plain- SVG DAG of the story's task-tree (rows = BFS depth from root_task_id over both parent_task_id and depends_on edges, solid vs. dashed strokes distinguishing the two edge kinds), an event timeline rendering eval_verdict/arbitration_decided/human_accepted/retro_captured/ epic_proposed with human-readable text, and an Accept button wired to POST /api/stories/{id}/accept when status is REVIEW_READY. - DAG node coloring reuses the app's existing --state-* CSS custom properties (the dataviz skill's "reserved status palette" pattern, already used elsewhere in the app) rather than introducing a new palette. Running the skill's own palette validator against that pre-existing 9-color set fails its CVD/lightness checks (a pre-existing condition, out of scope to fix here since those colors are used elsewhere in the app) -- mitigated per the skill's documented remedy for a failing palette: every node always carries a text label (name + role + state) and a full legend, so identity never depends on color alone; edge kind is encoded by stroke style, not color. Verified with a real running server and a real headless-browser session (Chromium via playwright-core, offline-cached), not just code review: seeded real data (an epic, 11 stories across every lifecycle status, a 6-node Builder->4-Evaluator->Arbitration task tree, 4 story events) via the live REST API plus two throwaway fixture scripts for the two write paths the API doesn't expose (task depends_on, direct event writes), then confirmed via screenshots/DOM assertions: correct column placement for every status, working swimlane grouping/progress meter, correct 6-node/8-edge DAG render, working event timeline, a real Accept-button API call flipping REVIEW_READY->DONE, real drag-and-drop priority persistence (re-fetched to confirm), and a genuine cross-column-drag no-op. Caught and fixed a real bug during this process: DAG node-label truncation now measures getComputedTextLength() and binary-searches the longest fit, instead of a fixed character-count budget that collapsed every "Eval: evaluator_X" label to "Eval:..." (only one space in the string). go build ./... clean; node --test web/test/*.mjs -- 275 tests pass (19 new). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
Diffstat (limited to 'web/style.css')
-rw-r--r--web/style.css365
1 files changed, 365 insertions, 0 deletions
diff --git a/web/style.css b/web/style.css
index abbf3b9..b11faf6 100644
--- a/web/style.css
+++ b/web/style.css
@@ -1868,12 +1868,18 @@ dialog label select:focus {
}
.story-status-badge[data-status="PENDING"] { background: var(--state-pending); }
+.story-status-badge[data-status="DISCOVERY"] { background: var(--state-pending); }
+.story-status-badge[data-status="FRAMING"] { background: var(--state-pending); }
+.story-status-badge[data-status="BACKLOG"] { background: var(--state-pending); }
+.story-status-badge[data-status="PRIORITIZED"] { background: var(--state-queued); }
.story-status-badge[data-status="IN_PROGRESS"] { background: var(--state-running); }
.story-status-badge[data-status="SHIPPABLE"] { background: var(--state-completed); }
.story-status-badge[data-status="DEPLOYED"] { background: #60a5fa; }
.story-status-badge[data-status="VALIDATING"] { background: #c084fc; }
.story-status-badge[data-status="REVIEW_READY"] { background: var(--state-completed); }
.story-status-badge[data-status="NEEDS_FIX"] { background: var(--state-failed); color: #fff; }
+.story-status-badge[data-status="DONE"] { background: var(--state-completed); }
+.story-status-badge[data-status="CANCELLED"] { background: var(--state-cancelled); }
.story-meta {
display: flex;
@@ -2005,6 +2011,12 @@ dialog label select:focus {
.event-timeline__item--subtask_spawned { border-left-color: var(--accent); }
.event-timeline__item--execution_started,
.event-timeline__item--execution_ended { border-left-color: var(--state-running); }
+.event-timeline__item--eval_verdict { border-left-color: var(--state-completed); }
+.event-timeline__item--arbitration_decided { border-left-color: var(--accent); }
+.event-timeline__item--human_accepted { border-left-color: var(--state-completed); }
+.event-timeline__item--retro_captured { border-left-color: var(--state-timed-out); }
+.event-timeline__item--epic_proposed { border-left-color: var(--accent); }
+.event-timeline__item--escalated { border-left-color: var(--state-budget-exceeded); }
.event-timeline__actor {
flex: 0 0 auto;
text-transform: uppercase;
@@ -2043,3 +2055,356 @@ dialog label select:focus {
border-color: var(--state-budget-exceeded);
color: var(--state-budget-exceeded);
}
+
+/* ── Stories tab: Kanban board ────────────────────────────────────────────── */
+/* main{max-width:640px} stays untouched for every other tab; the board opts
+ out locally (negative margins) and scrolls horizontally within itself so
+ a 7-column board never forces the whole page to scroll sideways. */
+
+[data-panel="stories"] {
+ max-width: none;
+ margin: 0 -1rem;
+ padding: 0 1rem;
+}
+
+.stories-toolbar {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 0.75rem;
+}
+
+.view-toggle {
+ display: inline-flex;
+ border: 1px solid var(--border);
+ border-radius: 0.375rem;
+ overflow: hidden;
+}
+
+.view-toggle-btn {
+ font-size: 0.8rem;
+ font-weight: 600;
+ padding: 0.35em 0.9em;
+ border: none;
+ background: transparent;
+ color: var(--text-muted);
+ cursor: pointer;
+}
+
+.view-toggle-btn + .view-toggle-btn {
+ border-left: 1px solid var(--border);
+}
+
+.view-toggle-btn.active {
+ background: var(--accent);
+ color: #0f172a;
+}
+
+.stories-board {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.75rem;
+ overflow-x: auto;
+ padding-bottom: 0.5rem;
+}
+
+.stories-column {
+ flex: 0 0 200px;
+ width: 200px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 0.5rem;
+ padding: 0.625rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ transition: border-color 0.15s, background 0.15s;
+}
+
+.stories-column.drag-over {
+ border-color: var(--accent);
+ background: var(--surface);
+}
+
+.stories-column-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+ color: var(--text-muted);
+ padding-bottom: 0.4rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.stories-column-count {
+ font-weight: 700;
+ color: var(--text);
+ background: var(--surface);
+ border-radius: 999px;
+ padding: 0 0.45em;
+ font-size: 0.7rem;
+}
+
+.stories-column-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ min-height: 40px;
+ max-height: 60vh;
+ overflow-y: auto;
+}
+
+.story-card {
+ cursor: grab;
+}
+
+.story-card.dragging {
+ opacity: 0.4;
+}
+
+.story-card-header {
+ flex-wrap: wrap;
+}
+
+.story-eval-indicator {
+ font-size: 0.72rem;
+ color: var(--text-muted);
+ margin-top: 0.1rem;
+}
+
+.story-modal-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 0.5rem;
+}
+
+/* ── Stories tab: Epic swimlanes ──────────────────────────────────────────── */
+
+.epic-swimlanes {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.epic-swimlane {
+ border: 1px solid var(--border);
+ border-radius: 0.5rem;
+ background: var(--bg);
+ padding: 0.75rem;
+}
+
+.epic-swimlane-header {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 0.625rem;
+}
+
+.epic-swimlane-name {
+ font-weight: 700;
+ font-size: 0.92rem;
+ flex-shrink: 0;
+}
+
+.epic-progress {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex: 1;
+ min-width: 0;
+}
+
+/* Meter: fill is a single hue (success), track is a lighter neutral step —
+ same-ramp read, not a multi-color segmented bar. */
+.epic-progress-track {
+ flex: 1;
+ height: 6px;
+ border-radius: 999px;
+ background: var(--border);
+ overflow: hidden;
+}
+
+.epic-progress-fill {
+ height: 100%;
+ background: var(--state-completed);
+ border-radius: 999px;
+ transition: width 0.2s;
+}
+
+.epic-progress-label {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.epic-swimlane-stories {
+ display: flex;
+ gap: 0.625rem;
+ overflow-x: auto;
+ padding-bottom: 0.25rem;
+}
+
+.epic-swimlane-stories .story-card {
+ flex: 0 0 220px;
+ width: 220px;
+ cursor: pointer;
+}
+
+/* ── Story detail modal ───────────────────────────────────────────────────── */
+
+#story-modal {
+ max-width: 900px;
+ width: 92vw;
+ max-height: 88vh;
+ overflow-y: auto;
+}
+
+.story-modal-body {
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+}
+
+.ac-list {
+ margin: 0.25rem 0 0;
+ padding-left: 1.25rem;
+}
+
+.ac-list li {
+ padding: 0.15rem 0;
+ font-size: 0.875rem;
+}
+
+.ac-list.empty {
+ list-style: none;
+ padding-left: 0;
+ color: var(--text-muted);
+ font-size: 0.85rem;
+}
+
+/* ── Story task-tree DAG ──────────────────────────────────────────────────── */
+
+.dag-container {
+ overflow: auto;
+ max-height: 440px;
+ border: 1px solid var(--border);
+ border-radius: 0.5rem;
+ background: var(--bg);
+ padding: 0.5rem;
+}
+
+.dag-svg {
+ display: block;
+}
+
+.dag-node-rect {
+ stroke: rgba(15, 23, 42, 0.35);
+ stroke-width: 1;
+}
+
+.dag-node-rect[data-state="PENDING"] { fill: var(--state-pending); }
+.dag-node-rect[data-state="QUEUED"] { fill: var(--state-queued); }
+.dag-node-rect[data-state="RUNNING"] { fill: var(--state-running); }
+.dag-node-rect[data-state="READY"] { fill: var(--state-running); }
+.dag-node-rect[data-state="COMPLETED"] { fill: var(--state-completed); }
+.dag-node-rect[data-state="FAILED"] { fill: var(--state-failed); }
+.dag-node-rect[data-state="TIMED_OUT"] { fill: var(--state-timed-out); }
+.dag-node-rect[data-state="CANCELLED"] { fill: var(--state-cancelled); }
+.dag-node-rect[data-state="BUDGET_EXCEEDED"] { fill: var(--state-budget-exceeded); }
+.dag-node-rect[data-state="BLOCKED"] { fill: var(--state-blocked); }
+
+.dag-node-name, .dag-node-sub {
+ font-family: inherit;
+ fill: #0f172a;
+}
+
+.dag-node-name {
+ font-size: 11px;
+ font-weight: 700;
+}
+
+.dag-node-sub {
+ font-size: 9px;
+ opacity: 0.85;
+}
+
+.dag-edge {
+ stroke: var(--text-muted);
+ stroke-width: 1.5;
+ fill: none;
+}
+
+.dag-edge--depends {
+ stroke-dasharray: 4 3;
+}
+
+.dag-arrow-head {
+ fill: var(--text-muted);
+}
+
+.dag-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem 0.9rem;
+ margin-top: 0.75rem;
+ font-size: 0.72rem;
+ color: var(--text-muted);
+}
+
+.dag-legend-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+}
+
+.dag-legend-swatch {
+ width: 10px;
+ height: 10px;
+ border-radius: 2px;
+ display: inline-block;
+}
+
+.dag-legend-swatch[data-state="PENDING"] { background: var(--state-pending); }
+.dag-legend-swatch[data-state="QUEUED"] { background: var(--state-queued); }
+.dag-legend-swatch[data-state="RUNNING"] { background: var(--state-running); }
+.dag-legend-swatch[data-state="READY"] { background: var(--state-running); }
+.dag-legend-swatch[data-state="COMPLETED"] { background: var(--state-completed); }
+.dag-legend-swatch[data-state="FAILED"] { background: var(--state-failed); }
+.dag-legend-swatch[data-state="TIMED_OUT"] { background: var(--state-timed-out); }
+.dag-legend-swatch[data-state="CANCELLED"] { background: var(--state-cancelled); }
+.dag-legend-swatch[data-state="BUDGET_EXCEEDED"] { background: var(--state-budget-exceeded); }
+.dag-legend-swatch[data-state="BLOCKED"] { background: var(--state-blocked); }
+
+.dag-edge-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem 1.2rem;
+ margin-top: 0.35rem;
+ font-size: 0.72rem;
+ color: var(--text-muted);
+}
+
+.dag-edge-legend-swatch {
+ display: inline-block;
+ width: 18px;
+ border-top: 2px solid var(--text-muted);
+ margin-right: 0.35rem;
+ vertical-align: middle;
+}
+
+.dag-edge-legend-swatch--depends {
+ border-top-style: dashed;
+}
+
+@media (max-width: 640px) {
+ #story-modal {
+ max-width: 100vw;
+ width: 100vw;
+ max-height: 100vh;
+ border-radius: 0;
+ }
+}