From f9feb4bbf8dce88f4a690b2e83f65b678c585fd0 Mon Sep 17 00:00:00 2001 From: Ashish Huddar Date: Sat, 21 Mar 2026 11:01:31 +0530 Subject: [PATCH 01/15] =?UTF-8?q?chore(web):=20kanban=20dashboard=20redesi?= =?UTF-8?q?gn=20=E2=80=94=20Linear-inspired=20design=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redesign the kanban dashboard with a Linear-inspired aesthetic: - New design tokens (neutral charcoal darks, white-alpha borders, muted status colors, indigo accent) - Tighter spacing, visible column backgrounds, dark mode weight reduction - Redesigned SessionCard, ProjectSidebar, and AttentionZone components - Removed list view toggle, reordered columns (respond → merge → done) - Added ThemeToggle, Skeleton/EmptyState components, and showcase page - Exported CI_STATUS and getSizeLabel for component consumption Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/app/globals.css | 1396 +++++++++++++++-- packages/web/src/app/showcase/page.tsx | 990 ++++++++++++ packages/web/src/components/AttentionZone.tsx | 124 +- packages/web/src/components/Dashboard.tsx | 129 +- packages/web/src/components/PRStatus.tsx | 2 +- .../web/src/components/ProjectSidebar.tsx | 280 +++- packages/web/src/components/SessionCard.tsx | 628 ++++++-- packages/web/src/components/Skeleton.tsx | 185 +++ packages/web/src/components/ThemeToggle.tsx | 35 + .../__tests__/Dashboard.emptyState.test.tsx | 57 + packages/web/src/lib/types.ts | 2 +- 11 files changed, 3400 insertions(+), 428 deletions(-) create mode 100644 packages/web/src/app/showcase/page.tsx create mode 100644 packages/web/src/components/Skeleton.tsx create mode 100644 packages/web/src/components/ThemeToggle.tsx create mode 100644 packages/web/src/components/__tests__/Dashboard.emptyState.test.tsx diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css index 211c3f38f..9b7bcc75d 100644 --- a/packages/web/src/app/globals.css +++ b/packages/web/src/app/globals.css @@ -1,83 +1,421 @@ +/** + * Design System Tokens — Agent Orchestrator Dashboard + * ===================================================== + * All tokens are defined in @theme using Tailwind v4 CSS custom properties. + * Reference tokens in components via: bg-[var(--color-bg-surface)], etc. + * + * Token categories: + * --color-bg-* Surface / background colors + * --color-border-* Border colors + * --color-text-* Text colors + * --color-accent-* Interactive and semantic accent colors + * --color-status-* Status indicator colors + * --font-* Font family stacks (in @theme — generates font-family utilities) + * --radius-* Border-radius scale (in @theme — generates rounded-* utilities) + * --transition-* Easing / duration tokens (in @theme) + * + * Tokens in :root (not @theme, to avoid Tailwind utility namespace collisions): + * --font-size-* Font-size scale (xs → xl); avoids --font-* font-family collision + * --box-shadow-* Box-shadow levels (sm → xl); avoids --shadow-* utility collision + * --space-* Spacing reference tokens; avoids potential --space-* utility collision + * --z-* Z-index scale; Tailwind v4 z-index namespace is --z-index-*, not --z-* + * --gradient-* Named gradient surfaces for cards and buttons + */ + @import "tailwindcss"; -@theme { - /* ── Base surfaces ────────────────────────────────────────────────── */ - --color-bg-base: #0d1117; - --color-bg-surface: rgba(22, 27, 34, 0.8); - --color-bg-elevated: rgba(28, 33, 40, 0.9); - --color-bg-subtle: rgba(33, 38, 45, 0.7); +/* ── Light mode (default) ─────────────────────────────────────────── */ +:root { + /* Base surfaces — Linear-standard light palette */ + --color-bg-base: #ffffff; + --color-bg-surface: #ffffff; + --color-bg-elevated: #ffffff; + --color-bg-elevated-hover: #f7f7f8; + --color-bg-subtle: #f2f2f2; /* Backward-compat aliases */ - --color-bg-primary: var(--color-bg-base); + --color-bg-primary: var(--color-bg-base); --color-bg-secondary: var(--color-bg-surface); - --color-bg-tertiary: var(--color-bg-elevated); + --color-bg-tertiary: var(--color-bg-elevated); - /* ── Borders ─────────────────────────────────────────────────────── */ - --color-border-subtle: rgba(48, 54, 61, 0.6); - --color-border-default: rgba(48, 54, 61, 1); - --color-border-strong: rgba(72, 79, 88, 1); + /* Borders */ + --color-border-subtle: #e8e8ec; + --color-border-default: #d9d9de; + --color-border-strong: #c1c1c6; /* Backward-compat aliases */ - --color-border-muted: var(--color-border-subtle); + --color-border-muted: var(--color-border-subtle); --color-border-emphasis: var(--color-border-strong); - /* ── Text ─────────────────────────────────────────────────────────── */ - --color-text-primary: #e6edf3; - --color-text-secondary: #7d8590; - --color-text-tertiary: #484f58; - --color-text-muted: #484f58; - --color-text-inverse: #0d1117; + /* Text */ + --color-text-primary: #1b1b1f; + --color-text-secondary: #5e5e66; + --color-text-tertiary: #8b8b93; + --color-text-muted: #8b8b93; + --color-text-inverse: #ffffff; - /* ── Interactive accent ──────────────────────────────────────────── */ - --color-accent: #58a6ff; - --color-accent-hover: #79b8ff; - --color-accent-subtle: rgba(88, 166, 255, 0.12); + /* Interactive accent — Linear indigo */ + --color-accent: #5e6ad2; + --color-accent-hover: #4850b8; + --color-accent-subtle: rgba(94, 106, 210, 0.08); - /* ── Status / semantic colors ────────────────────────────────────── */ - --color-status-working: #58a6ff; - --color-status-ready: #3fb950; - --color-status-attention: #d29922; - --color-status-idle: #484f58; - --color-status-done: #30363d; - --color-status-error: #f85149; + /* Status / semantic colors */ + --color-status-working: #5e6ad2; + --color-status-ready: #1a7f37; + --color-status-attention: #9a6700; + --color-status-idle: #8b949e; + --color-status-done: #d0d7de; + --color-status-error: #cf222e; /* Semantic aliases */ - --color-accent-blue: #58a6ff; - --color-accent-green: #3fb950; - --color-accent-yellow: #d29922; - --color-accent-orange: #d18616; - --color-accent-red: #f85149; - --color-accent-violet: #a371f7; - --color-accent-purple: #bc8cff; + --color-accent-blue: #5e6ad2; + --color-accent-green: #1a7f37; + --color-accent-yellow: #9a6700; + --color-accent-orange: #bc4c00; + --color-accent-red: #cf222e; + --color-accent-violet: #8250df; + --color-accent-purple: #8250df; + /* Theme-adaptive tints (bg tints behind status pills) */ + --color-tint-blue: rgba(94, 106, 210, 0.08); + --color-tint-green: rgba(26, 127, 55, 0.08); + --color-tint-yellow: rgba(154, 103, 0, 0.08); + --color-tint-red: rgba(207, 34, 46, 0.08); + --color-tint-violet: rgba(130, 80, 223, 0.08); + --color-tint-orange: rgba(188, 76, 0, 0.08); + --color-tint-neutral: rgba(0, 0, 0, 0.04); + + /* Chip/badge background */ + --color-chip-bg: #f2f2f2; + + /* Hover overlay */ + --color-hover-overlay: #f7f7f8; + + /* Scrollbar */ + --color-scrollbar: rgba(0, 0, 0, 0.08); + --color-scrollbar-hover: rgba(0, 0, 0, 0.15); + --color-scrollbar-active: rgba(0, 0, 0, 0.25); + + /* Body radial gradient tints — none in light mode */ + --color-body-gradient-blue: transparent; + --color-body-gradient-violet: transparent; + + /* Nav */ + --color-nav-bg: #ffffff; + + /* Card surfaces — clean white on light gray */ + --card-bg: #ffffff; + --card-merge-bg: #f6fbf8; + --card-expanded-bg: #f7f7f8; + --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04); + --card-shadow-hover: 0 2px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.06); + --card-inset: none; + + /* Done card surfaces */ + --card-done-bg: #fafafa; + --card-done-border: #e8e8ec; + + --done-pill-exited-bg: #f2f2f2; + --done-pill-exited-color: #8b8b93; + --done-pill-merged-bg: rgba(26, 127, 55, 0.08); + --done-pill-merged-color: #1a7f37; + --done-pill-killed-bg: rgba(207, 34, 46, 0.06); + --done-pill-killed-color: #cf222e; + --done-meta-chip-bg: #f2f2f2; + --done-meta-chip-border: #e8e8ec; + --done-section-border: #ececf0; + --done-title-color: #5e5e66; + --done-restore-bg: rgba(9, 105, 218, 0.06); + --done-restore-border: rgba(9, 105, 218, 0.2); + --done-restore-hover-bg: rgba(9, 105, 218, 0.12); + + /* Detail card */ + --detail-card-bg: #ffffff; + --detail-card-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04); + + /* Orchestrator button */ + --btn-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); + --btn-shadow-hover: 0 2px 4px rgba(0, 0, 0, 0.1); + --btn-inset: none; + + /* Kanban column */ + --color-column-bg: #f2f2f2; + --color-column-header: transparent; + + /* Alert colors */ + --color-alert-ci: #6366f1; + --color-alert-ci-bg: #4f46e5; + --color-alert-ci-unknown: #9a6700; + --color-alert-review: #1a7f37; + --color-alert-review-bg: #1a7f37; + --color-alert-changes: #8250df; + --color-alert-changes-bg: #7c3aed; + --color-alert-conflict: #9a6700; + --color-alert-conflict-bg: #92400e; + --color-alert-comment: #bc4c00; + --color-alert-comment-bg: #c2410c; + + /* Activity pulse glow */ + --color-activity-pulse: rgba(94, 106, 210, 0.45); + --color-activity-pulse-end: rgba(94, 106, 210, 0); +} + +/* ── Dark mode ────────────────────────────────────────────────────── */ +.dark { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /* Base surfaces — graphite dashboard */ + --color-bg-base: #0a0d12; + --color-bg-surface: #11161d; + --color-bg-elevated: #171d26; + --color-bg-elevated-hover: #1c2430; + --color-bg-subtle: rgba(177, 206, 255, 0.05); + + /* Borders */ + --color-border-subtle: rgba(160, 190, 255, 0.08); + --color-border-default: rgba(160, 190, 255, 0.14); + --color-border-strong: rgba(185, 214, 255, 0.24); + + /* Text */ + --color-text-primary: #eef3ff; + --color-text-secondary: #a5afc4; + --color-text-tertiary: #6f7c94; + --color-text-muted: #6f7c94; + --color-text-inverse: #0a0d12; + + /* Interactive accent */ + --color-accent: #8fb4ff; + --color-accent-hover: #b4ccff; + --color-accent-subtle: rgba(143, 180, 255, 0.16); + + /* Status */ + --color-status-working: #6e8fff; + --color-status-ready: #73e0aa; + --color-status-attention: #f1be64; + --color-status-idle: #293142; + --color-status-done: #202838; + --color-status-error: #ff7b72; + + /* Semantic aliases */ + --color-accent-blue: #8fb4ff; + --color-accent-green: #5fd39a; + --color-accent-yellow: #f1be64; + --color-accent-orange: #ff9d57; + --color-accent-red: #ff7b72; + --color-accent-violet: #b59cff; + --color-accent-purple: #b59cff; + + /* Theme-adaptive tints */ + --color-tint-blue: rgba(143, 180, 255, 0.12); + --color-tint-green: rgba(95, 211, 154, 0.12); + --color-tint-yellow: rgba(241, 190, 100, 0.12); + --color-tint-red: rgba(255, 123, 114, 0.12); + --color-tint-violet: rgba(181, 156, 255, 0.12); + --color-tint-orange: rgba(255, 157, 87, 0.12); + --color-tint-neutral: rgba(255, 255, 255, 0.05); + + /* Chip/badge background */ + --color-chip-bg: rgba(255, 255, 255, 0.06); + + /* Hover overlay */ + --color-hover-overlay: rgba(143, 180, 255, 0.05); + + /* Scrollbar */ + --color-scrollbar: rgba(255, 255, 255, 0.08); + --color-scrollbar-hover: rgba(255, 255, 255, 0.15); + --color-scrollbar-active: rgba(255, 255, 255, 0.25); + + /* Body */ + --color-body-gradient-blue: rgba(110, 143, 255, 0.2); + --color-body-gradient-violet: rgba(91, 208, 177, 0.08); + + /* Nav glass */ + --color-nav-bg: rgba(10, 13, 18, 0.82); + + /* Card surfaces */ + --card-bg: linear-gradient(180deg, rgba(22, 28, 37, 0.98) 0%, rgba(16, 21, 29, 0.98) 100%); + --card-merge-bg: linear-gradient( + 180deg, + rgba(22, 29, 38, 0.98) 0%, + rgba(17, 23, 31, 0.98) 58%, + rgba(14, 19, 27, 0.98) 100% + ); + --card-expanded-bg: linear-gradient( + 180deg, + rgba(26, 34, 45, 0.98) 0%, + rgba(18, 24, 33, 0.98) 100% + ); + --card-shadow: 0 18px 36px rgba(2, 6, 12, 0.24); + --card-shadow-hover: 0 24px 54px rgba(2, 6, 12, 0.34); + --card-inset: inset 0 1px 0 rgba(255, 255, 255, 0.04); + + /* Done card surfaces */ + --card-done-bg: linear-gradient(180deg, rgba(18, 24, 33, 0.72) 0%, rgba(14, 19, 27, 0.72) 100%); + --card-done-border: rgba(160, 190, 255, 0.1); + + --done-pill-exited-bg: rgba(143, 180, 255, 0.08); + --done-pill-exited-color: #8a9ab6; + --done-pill-merged-bg: rgba(95, 211, 154, 0.14); + --done-pill-merged-color: #5fd39a; + --done-pill-killed-bg: rgba(255, 123, 114, 0.12); + --done-pill-killed-color: #ff7b72; + --done-meta-chip-bg: rgba(143, 180, 255, 0.05); + --done-meta-chip-border: rgba(160, 190, 255, 0.1); + --done-section-border: rgba(160, 190, 255, 0.08); + --done-title-color: #9ba8bf; + --done-restore-bg: rgba(143, 180, 255, 0.1); + --done-restore-border: rgba(143, 180, 255, 0.24); + --done-restore-hover-bg: rgba(143, 180, 255, 0.18); + + /* Detail card */ + --detail-card-bg: linear-gradient(180deg, rgba(20, 26, 35, 0.98) 0%, rgba(14, 19, 27, 0.98) 100%); + --detail-card-shadow: 0 18px 42px rgba(0, 0, 0, 0.28); + + /* Orchestrator button */ + --btn-shadow: 0 10px 24px rgba(34, 63, 116, 0.16); + --btn-shadow-hover: 0 16px 34px rgba(34, 63, 116, 0.22); + --btn-inset: inset 0 1px 0 rgba(255, 255, 255, 0.08); + + /* Kanban column */ + --color-column-bg: linear-gradient(180deg, rgba(18, 24, 33, 0.88) 0%, rgba(12, 16, 23, 0.9) 100%); + --color-column-header: rgba(255, 255, 255, 0.02); + + /* Alert colors — Linear-muted */ + --color-alert-ci: #7b85e0; + --color-alert-ci-bg: rgba(94, 106, 210, 0.25); + --color-alert-ci-unknown: #d4a72c; + --color-alert-review: #4dab6e; + --color-alert-review-bg: rgba(77, 171, 110, 0.25); + --color-alert-changes: #9b8afb; + --color-alert-changes-bg: rgba(155, 138, 251, 0.25); + --color-alert-conflict: #d4a72c; + --color-alert-conflict-bg: rgba(212, 167, 44, 0.25); + --color-alert-comment: #c88a2e; + --color-alert-comment-bg: rgba(200, 138, 46, 0.25); + + /* Activity pulse glow */ + --color-activity-pulse: rgba(94, 106, 210, 0.45); + --color-activity-pulse-end: rgba(94, 106, 210, 0); +} + +@theme { /* ── Typography ──────────────────────────────────────────────────── */ --font-sans: var(--font-ibm-plex-sans), "SF Pro Text", -apple-system, system-ui, sans-serif; --font-mono: var(--font-ibm-plex-mono), "SF Mono", "Menlo", "Consolas", monospace; - /* ── Border radius ────────────────────────────────────────────────── */ + /* ── Border radius ───────────────────────────────────────────────── */ --radius-sm: 4px; --radius-md: 6px; --radius-lg: 8px; --radius-xl: 12px; /* ── Transitions ──────────────────────────────────────────────────── */ - --transition-quick: 0.1s; + --transition-quick: 0.1s; --transition-regular: 0.25s; } +/* ── Reference tokens (outside @theme to avoid Tailwind namespace collisions) ── */ + +:root { + /* Spacing reference tokens — in :root to avoid potential Tailwind utility conflicts */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 32px; + --space-10: 40px; + --space-12: 48px; + --space-16: 64px; + + /* Z-index scale — in :root; Tailwind v4 z-index namespace is --z-index-*, not --z-* */ + --z-base: 0; + --z-raised: 10; + --z-nav: 100; + --z-modal: 200; + --z-overlay: 300; + --z-toast: 400; + + /* Font-size scale — in :root to avoid --font-* font-family namespace in @theme */ + --font-size-xs: 10px; + --font-size-sm: 11px; + --font-size-base: 13px; + --font-size-lg: 15px; + --font-size-xl: 17px; + + /* Shadow levels — in :root to avoid --shadow-* utility namespace in @theme */ + --box-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.6); + --box-shadow-md: 0 1px 3px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.05); + --box-shadow-lg: + 0 1px 2px rgba(0, 0, 0, 0.9), 0 3px 10px rgba(0, 0, 0, 0.55), + inset 0 1px 0 rgba(255, 255, 255, 0.07); + --box-shadow-xl: + 0 4px 8px rgba(0, 0, 0, 0.85), 0 14px 36px rgba(0, 0, 0, 0.55), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + + /* Gradient surfaces */ + --gradient-session-card: linear-gradient( + 175deg, + rgba(30, 30, 35, 1) 0%, + rgba(23, 23, 27, 1) 100% + ); + --gradient-session-card-expanded: linear-gradient( + 175deg, + rgba(38, 38, 44, 1) 0%, + rgba(30, 30, 35, 1) 100% + ); + --gradient-session-card-merge: linear-gradient( + 175deg, + rgba(23, 29, 25, 1) 0%, + rgba(17, 22, 19, 1) 100% + ); + --gradient-detail-card: linear-gradient(175deg, rgba(30, 30, 35, 1) 0%, rgba(23, 23, 27, 1) 100%); + --gradient-orchestrator-btn: linear-gradient( + 175deg, + rgba(94, 106, 210, 0.12) 0%, + rgba(94, 106, 210, 0.06) 100% + ); + --gradient-orchestrator-btn-hover: linear-gradient( + 175deg, + rgba(94, 106, 210, 0.18) 0%, + rgba(94, 106, 210, 0.1) 100% + ); + --gradient-status-strip: linear-gradient( + to bottom, + rgba(94, 106, 210, 0.04) 0%, + transparent 100% + ); +} + /* ── Base styles ─────────────────────────────────────────────────────── */ body { font-family: var(--font-sans); - background: - radial-gradient(ellipse 70% 40% at 15% 0%, rgba(88, 166, 255, 0.05) 0%, transparent 60%), - radial-gradient(ellipse 50% 30% at 85% 90%, rgba(163, 113, 247, 0.04) 0%, transparent 60%), - var(--color-bg-base); + background: var(--color-bg-base); color: var(--color-text-primary); min-height: 100vh; letter-spacing: -0.011em; } +/* Light mode: solid white, NO gradient */ +html:not(.dark) body { + background: #ffffff !important; +} + +/* Radial gradient only for dark mode */ +.dark body { + background: + radial-gradient(ellipse 70% 40% at 15% 0%, var(--color-body-gradient-blue) 0%, transparent 60%), + radial-gradient( + ellipse 50% 30% at 85% 90%, + var(--color-body-gradient-violet) 0%, + transparent 60% + ), + linear-gradient(180deg, rgba(255, 255, 255, 0.015) 0%, transparent 28%), var(--color-bg-base); +} + a { color: var(--color-accent); text-decoration: none; @@ -88,119 +426,963 @@ a:hover { /* ── Scrollbar ────────────────────────────────────────────────────────── */ -::-webkit-scrollbar { width: 6px; height: 6px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 3px; } -::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.15); } -::-webkit-scrollbar-thumb:active { background: rgba(255,255,255,0.25); } +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: var(--color-scrollbar); + border-radius: 3px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--color-scrollbar-hover); +} +::-webkit-scrollbar-thumb:active { + background: var(--color-scrollbar-active); +} + +/* ── xterm.js terminal viewport scrollbar ────────────────────────────── */ + +.xterm .xterm-viewport::-webkit-scrollbar { + width: 6px; +} +.xterm .xterm-viewport::-webkit-scrollbar-track { + background: transparent; +} +.xterm .xterm-viewport::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.08); + border-radius: 3px; +} +.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.15); +} +.xterm .xterm-viewport::-webkit-scrollbar-thumb:active { + background: rgba(255, 255, 255, 0.25); +} + +/* Light-theme variant: darker thumb on light background */ +html.light .xterm .xterm-viewport::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.12); +} +html.light .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.22); +} +html.light .xterm .xterm-viewport::-webkit-scrollbar-thumb:active { + background: rgba(0, 0, 0, 0.35); +} /* ── Animations ──────────────────────────────────────────────────────── */ +@keyframes progress-shimmer { + 0% { + transform: translateX(-200%); + } + 100% { + transform: translateX(500%); + } +} + @keyframes activity-pulse { - 0%, 100% { box-shadow: 0 0 0 0 rgba(88, 166, 255, 0.45); } - 50% { box-shadow: 0 0 0 4px rgba(88, 166, 255, 0); } + 0%, + 100% { + box-shadow: 0 0 0 0 var(--color-activity-pulse); + } + 50% { + box-shadow: 0 0 0 4px var(--color-activity-pulse-end); + } } @keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } } @keyframes slide-up { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ── Kanban card entrance ─────────────────────────────────────────────── */ + +.kanban-card-enter { + animation: slide-up 0.18s ease forwards; } /* ── Orchestrator button ──────────────────────────────────────────────── */ .orchestrator-btn { color: var(--color-accent); - background: linear-gradient(175deg, rgba(88,166,255,0.12) 0%, rgba(88,166,255,0.06) 100%); - border: 1px solid rgba(88,166,255,0.25); - box-shadow: - 0 1px 2px rgba(0,0,0,0.6), - 0 3px 8px rgba(0,0,0,0.3), - inset 0 1px 0 rgba(255,255,255,0.08); - transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease, border-color 0.12s ease; + background: linear-gradient( + 175deg, + color-mix(in srgb, var(--color-accent) 12%, transparent) 0%, + color-mix(in srgb, var(--color-accent) 6%, transparent) 100% + ); + border: 1px solid color-mix(in srgb, var(--color-accent) 25%, transparent); + box-shadow: var(--btn-shadow); + transition: + transform 0.12s ease, + box-shadow 0.12s ease, + background 0.12s ease, + border-color 0.12s ease; } .orchestrator-btn:hover { transform: translateY(-1px); - background: linear-gradient(175deg, rgba(88,166,255,0.18) 0%, rgba(88,166,255,0.1) 100%); - border-color: rgba(88,166,255,0.45); - box-shadow: - 0 2px 6px rgba(0,0,0,0.7), - 0 8px 20px rgba(0,0,0,0.35), - 0 0 20px rgba(88,166,255,0.12), - inset 0 1px 0 rgba(255,255,255,0.12); + background: linear-gradient( + 175deg, + color-mix(in srgb, var(--color-accent) 18%, transparent) 0%, + color-mix(in srgb, var(--color-accent) 10%, transparent) 100% + ); + border-color: color-mix(in srgb, var(--color-accent) 45%, transparent); + box-shadow: var(--btn-shadow-hover); } /* ── Glass nav ────────────────────────────────────────────────────────── */ .nav-glass { - background: rgba(13, 17, 23, 0.88); + background: var(--color-nav-bg); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); } +/* ── Dashboard shell ───────────────────────────────────────────────── */ + +.dashboard-shell { + position: relative; +} + +.dashboard-main { + position: relative; +} + +.dashboard-main::before { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 280px; + pointer-events: none; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.02) 0%, transparent 100%); +} + +.dashboard-hero { + position: relative; + overflow: hidden; + border: 1px solid var(--color-border-default); + background: + radial-gradient( + circle at top left, + color-mix(in srgb, var(--color-accent) 18%, transparent) 0%, + transparent 34% + ), + linear-gradient( + 180deg, + color-mix(in srgb, var(--color-bg-elevated) 86%, black 14%) 0%, + color-mix(in srgb, var(--color-bg-surface) 96%, black 4%) 100% + ); + box-shadow: 0 18px 42px rgba(2, 6, 12, 0.12); +} + +.dashboard-hero__backdrop { + position: absolute; + inset: 0; + pointer-events: none; + opacity: 0.6; + background-image: + linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px); + background-size: 28px 28px; + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.85), transparent); +} + +.dashboard-hero__content { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 16px 18px; +} + +.dashboard-hero__heading { + display: flex; + flex-direction: column; + gap: 8px; + max-width: 720px; +} + +.dashboard-eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + width: fit-content; + padding: 4px 8px; + border: 1px solid color-mix(in srgb, var(--color-accent) 18%, var(--color-border-default)); + background: color-mix(in srgb, var(--color-accent) 8%, transparent); + color: var(--color-accent); + font-size: 9px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.dashboard-eyebrow__dot { + width: 6px; + height: 6px; + border-radius: 999px; + background: currentColor; + box-shadow: 0 0 18px currentColor; +} + +.dashboard-title { + font-size: clamp(20px, 2.6vw, 28px); + line-height: 1; + letter-spacing: -0.035em; + font-weight: 600; + color: var(--color-text-primary); +} + +.dashboard-subtitle { + margin-top: 4px; + max-width: 52ch; + font-size: 11px; + line-height: 1.45; + color: var(--color-text-muted); +} + +.dashboard-hero__meta { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; +} + +.dashboard-stats { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.dashboard-stat { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 9px; + border: 1px solid var(--color-border-subtle); + background: color-mix(in srgb, var(--color-bg-base) 40%, transparent); + backdrop-filter: blur(10px); +} + +.dashboard-stat__value { + font-size: 16px; + line-height: 1; + font-weight: 600; + letter-spacing: -0.035em; + font-variant-numeric: tabular-nums; +} + +.dashboard-stat__label { + font-size: 10px; + color: var(--color-text-muted); +} + +.dashboard-stat__divider { + display: none; +} + +.dashboard-alert { + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08); +} + /* ── Detail page cards — subtle depth, no hover lift ─────────────────── */ .detail-card { - background: linear-gradient(175deg, rgba(24,31,40,1) 0%, rgba(17,22,29,1) 100%); - box-shadow: - 0 1px 3px rgba(0,0,0,0.6), - inset 0 1px 0 rgba(255,255,255,0.05); - --color-text-secondary: #8b949e; - --color-text-muted: #656d76; - --color-text-tertiary: #656d76; + background: var(--detail-card-bg); + box-shadow: var(--detail-card-shadow); } -/* ── Session cards ────────────────────────────────────────────────────── */ +.dark .detail-card { + --color-text-secondary: #9898a0; + --color-text-muted: #5c5c66; + --color-text-tertiary: #5c5c66; +} + +/* ── Session cards — flat, Linear-style ──────────────────────────────── */ .session-card { - background: linear-gradient(175deg, rgba(28,36,47,1) 0%, rgba(18,23,31,1) 100%); - box-shadow: - 0 1px 2px rgba(0,0,0,0.9), - 0 3px 10px rgba(0,0,0,0.55), - inset 0 1px 0 rgba(255,255,255,0.07); - transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease; + background: var(--card-bg); + box-shadow: var(--card-shadow); + box-shadow: var(--card-shadow), var(--card-inset); + transition: + border-color 0.12s ease, + background 0.12s ease, + box-shadow 0.12s ease; +} - /* Bump dim text colors inside the card — the elevated surface is darker - than the transparent bg these values were calibrated for */ - --color-text-secondary: #8b949e; - --color-text-muted: #656d76; - --color-text-tertiary: #656d76; +.dark .session-card { + --color-text-secondary: #9898a0; + --color-text-muted: #5c5c66; + --color-text-tertiary: #5c5c66; +} + +/* ── Dark mode weight reduction ──────────────────────────────────────── + Light text on dark backgrounds appears visually heavier due to + subpixel antialiasing. Step down one weight level. */ + +.dark .kanban-column [class*="font-semibold"] { + font-weight: 500; +} +.dark .kanban-column [class*="font-bold"] { + font-weight: 600; +} +.dark .session-card-done .done-status-pill { + font-weight: 500; +} +.dark .session-card-done .done-detail-heading { + font-weight: 600; } .session-card:hover { - transform: translateY(-2px); - box-shadow: - 0 4px 8px rgba(0,0,0,0.85), - 0 14px 36px rgba(0,0,0,0.55), - inset 0 1px 0 rgba(255,255,255,0.1); + border-color: var(--color-border-strong); + box-shadow: var(--card-shadow-hover); +} + +.session-card__header, +.session-card__title-wrap, +.session-card__meta, +.session-card__actions { + position: relative; + z-index: 1; +} + +.session-card__title-wrap::after { + content: ""; + position: absolute; + left: 16px; + right: 16px; + bottom: 0; + height: 1px; + background: linear-gradient(90deg, var(--color-border-subtle), transparent); + opacity: 0.6; } .session-card.card-merge-ready { - background: linear-gradient(175deg, rgba(16,40,24,1) 0%, rgba(10,26,15,1) 100%); + position: relative; + background: var(--card-merge-bg); + border-color: color-mix(in srgb, var(--color-status-ready) 24%, var(--color-border-default)); box-shadow: - 0 0 0 1px rgba(63,185,80,0.22), - 0 1px 2px rgba(0,0,0,0.9), - 0 3px 10px rgba(0,0,0,0.55), - 0 0 40px rgba(63,185,80,0.13), - inset 0 1px 0 rgba(255,255,255,0.07); + var(--card-shadow), + inset 0 1px 0 rgba(255, 255, 255, 0.04), + inset 3px 0 0 color-mix(in srgb, var(--color-status-ready) 24%, transparent); +} + +.session-card.card-merge-ready::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background: + radial-gradient( + circle at top right, + color-mix(in srgb, var(--color-status-ready) 10%, transparent) 0%, + transparent 32% + ), + radial-gradient(circle at 18% 10%, rgba(255, 255, 255, 0.035) 0%, transparent 24%), + linear-gradient(180deg, rgba(255, 255, 255, 0.02) 0%, transparent 44%); + opacity: 0.75; } .session-card.card-merge-ready:hover { - transform: translateY(-2px); + border-color: color-mix(in srgb, var(--color-status-ready) 34%, var(--color-border-strong)); box-shadow: - 0 0 0 1px rgba(63,185,80,0.35), - 0 4px 8px rgba(0,0,0,0.85), - 0 14px 36px rgba(0,0,0,0.55), - 0 0 60px rgba(63,185,80,0.2), - inset 0 1px 0 rgba(255,255,255,0.1); + var(--card-shadow-hover), + 0 0 0 1px color-mix(in srgb, var(--color-status-ready) 10%, transparent), + 0 0 24px color-mix(in srgb, var(--color-status-ready) 7%, transparent); +} + +.merge-ready-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 9px; + border: 1px solid color-mix(in srgb, var(--color-status-ready) 26%, var(--color-border-subtle)); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--color-status-ready) 10%, transparent) 0%, + color-mix(in srgb, var(--color-status-ready) 5%, transparent) 100% + ); + color: var(--color-status-ready); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.merge-ready-pill__dot { + width: 6px; + height: 6px; + border-radius: 999px; + background: currentColor; + box-shadow: 0 0 14px currentColor; +} + +.merge-ready-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border: 1px solid color-mix(in srgb, var(--color-status-ready) 14%, var(--color-border-subtle)); + background: color-mix(in srgb, var(--color-status-ready) 5%, transparent); + color: var(--color-text-secondary); + font-size: 10px; + font-weight: 600; +} + +.merge-ready-action { + border-color: color-mix(in srgb, var(--color-status-ready) 34%, transparent); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--color-status-ready) 14%, transparent) 0%, + color-mix(in srgb, var(--color-status-ready) 8%, transparent) 100% + ); + color: var(--color-status-ready); + box-shadow: + inset 0 1px 0 color-mix(in srgb, white 10%, transparent), + 0 8px 18px color-mix(in srgb, var(--color-status-ready) 8%, transparent); +} + +.merge-ready-action:hover { + border-color: color-mix(in srgb, var(--color-status-ready) 48%, transparent); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--color-status-ready) 18%, transparent) 0%, + color-mix(in srgb, var(--color-status-ready) 10%, transparent) 100% + ); + box-shadow: + inset 0 1px 0 color-mix(in srgb, white 12%, transparent), + 0 10px 22px color-mix(in srgb, var(--color-status-ready) 10%, transparent); +} + +/* ── Done cards — compact, de-emphasized ─────────────────────────────── */ + +.session-card-done { + background: var(--card-done-bg); + border: 1px solid var(--card-done-border); + border-radius: 0; + transition: + border-color 0.15s ease, + background 0.15s ease, + box-shadow 0.15s ease; + cursor: pointer; + overflow: hidden; +} + +.session-card-done:hover { + border-color: var(--color-border-strong); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); +} + +.dark .session-card-done:hover { + box-shadow: none; +} + +.session-card-done.done-expanded { + border-color: var(--color-border-strong); +} + +/* Status pill inside done cards */ +.done-status-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 99px; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.02em; + line-height: 1.4; +} + +.done-status-pill--exited { + background: var(--done-pill-exited-bg); + color: var(--done-pill-exited-color); +} + +.done-status-pill--merged { + background: var(--done-pill-merged-bg); + color: var(--done-pill-merged-color); +} + +.done-status-pill--killed { + background: var(--done-pill-killed-bg); + color: var(--done-pill-killed-color); +} + +/* Meta chip inside done cards */ +.done-meta-chip { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 2px 7px; + border-radius: 0; + background: var(--done-meta-chip-bg); + border: 1px solid var(--done-meta-chip-border); + font-size: 10px; + font-weight: 500; + color: var(--color-text-muted); +} + +/* Restore button inside done cards */ +.done-restore-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 0; + border: 1px solid var(--done-restore-border); + background: var(--done-restore-bg); + color: var(--color-accent); + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: + background 0.12s ease, + border-color 0.12s ease; +} + +.done-restore-btn:hover { + background: var(--done-restore-hover-bg); + border-color: color-mix(in srgb, var(--color-accent) 40%, transparent); +} + +/* Done card expand section */ +.done-expand-section { + border-top: 1px solid var(--done-section-border); + animation: done-slide-in 0.15s ease-out; +} + +@keyframes done-slide-in { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Done detail section heading */ +.done-detail-heading { + display: flex; + align-items: center; + gap: 5px; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--color-text-tertiary); + margin-bottom: 6px; +} + +.done-detail-heading svg { + width: 11px; + height: 11px; + opacity: 0.6; +} + +/* ── Project sidebar ────────────────────────────────────────────────── */ + +.project-sidebar nav::-webkit-scrollbar { + width: 4px; +} +.project-sidebar nav::-webkit-scrollbar-track { + background: transparent; +} +.project-sidebar nav::-webkit-scrollbar-thumb { + background: var(--color-scrollbar); + border-radius: 2px; +} + +.project-sidebar { + border-right: 1px solid var(--color-border-subtle); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--color-bg-elevated) 92%, black 8%) 0%, + color-mix(in srgb, var(--color-bg-base) 90%, black 10%) 100% + ); +} + +.project-sidebar__header { + border-bottom: 1px solid var(--color-border-subtle); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--color-accent) 4%, transparent) 0%, + transparent 100% + ); +} + +.project-sidebar__eyebrow { + font-size: 10px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--color-accent); +} + +.project-sidebar__title-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-top: 6px; +} + +.project-sidebar__title { + font-size: 13px; + line-height: 1.1; + letter-spacing: -0.02em; + font-weight: 600; + color: var(--color-text-primary); +} + +.project-sidebar__subtitle { + margin-top: 3px; + max-width: 18ch; + font-size: 10px; + line-height: 1.4; + color: var(--color-text-muted); +} + +.project-sidebar__badge { + display: inline-flex; + min-width: 22px; + height: 22px; + align-items: center; + justify-content: center; + border: 1px solid var(--color-border-default); + background: color-mix(in srgb, var(--color-bg-base) 42%, transparent); + font-size: 10px; + font-weight: 600; + color: var(--color-text-primary); + font-variant-numeric: tabular-nums; +} + +.project-sidebar__summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; + margin-top: 10px; +} + +.project-sidebar__metric { + display: flex; + flex-direction: column; + gap: 2px; + padding: 6px 6px 5px; + border: 1px solid var(--color-border-subtle); + background: color-mix(in srgb, var(--color-bg-base) 40%, transparent); +} + +.project-sidebar__metric-value { + font-size: 13px; + line-height: 1; + letter-spacing: -0.03em; + font-weight: 600; + color: var(--color-text-primary); + font-variant-numeric: tabular-nums; +} + +.project-sidebar__metric-label { + font-size: 9px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-text-muted); +} + +.project-sidebar__divider { + border-top: 1px solid var(--color-border-subtle); +} + +.project-sidebar__item { + position: relative; + border: 1px solid transparent; + background: transparent; +} + +.project-sidebar__item:hover { + background: var(--color-hover-overlay); + border-color: var(--color-border-subtle); +} + +.project-sidebar__item--active { + border-color: color-mix(in srgb, var(--color-accent) 24%, var(--color-border-default)); + background: linear-gradient( + 90deg, + color-mix(in srgb, var(--color-accent) 16%, transparent), + transparent 90% + ); +} + +.project-sidebar__count { + border: 1px solid var(--color-border-subtle); + background: color-mix(in srgb, var(--color-bg-base) 40%, transparent); +} + +.project-sidebar__children { + position: relative; + margin-top: 4px; + border-left: 1px solid color-mix(in srgb, var(--color-border-subtle) 82%, transparent); +} + +.project-sidebar__session { + position: relative; + margin: 2px 0 2px 8px; + border: 1px solid transparent; + background: transparent; +} + +.project-sidebar__session:hover { + border-color: var(--color-border-subtle); + background: linear-gradient( + 90deg, + color-mix(in srgb, var(--color-hover-overlay) 90%, transparent), + transparent 100% + ); +} + +.project-sidebar__session--active { + border-color: color-mix(in srgb, var(--color-accent) 18%, var(--color-border-subtle)); + background: linear-gradient( + 90deg, + color-mix(in srgb, var(--color-accent) 12%, transparent), + transparent 88% + ); + box-shadow: inset 2px 0 0 var(--color-accent); +} + +.project-sidebar__session::before { + content: ""; + position: absolute; + left: -9px; + top: 50%; + width: 8px; + height: 1px; + background: var(--color-border-subtle); + transform: translateY(-50%); +} + +.project-sidebar__session-tone { + flex-shrink: 0; + padding: 2px 6px; + border: 1px solid var(--color-border-subtle); + background: color-mix(in srgb, var(--color-bg-base) 40%, transparent); + font-size: 9px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-text-muted); +} + +.project-sidebar__session-id { + opacity: 0; + color: var(--color-text-tertiary); + transition: + opacity 0.15s ease, + color 0.15s ease; +} + +.project-sidebar__session:hover .project-sidebar__session-id, +.project-sidebar__session--active .project-sidebar__session-id { + opacity: 0.75; +} + +.project-sidebar__session--active .project-sidebar__session-tone { + border-color: color-mix(in srgb, var(--color-accent) 24%, var(--color-border-subtle)); + background: color-mix(in srgb, var(--color-accent) 10%, transparent); + color: var(--color-accent); +} + +/* ── Kanban board ────────────────────────────────────────────────────── */ + +.kanban-board { + display: flex; + gap: 14px; + height: calc(100vh - 198px); + overflow-x: auto; + padding-bottom: 8px; +} + +.kanban-board-wrap { + position: relative; +} + +.kanban-column { + flex: 1 0 280px; + min-width: 280px; + max-width: 392px; + display: flex; + flex-direction: column; + border: 1px solid var(--color-border-default); + background: var(--color-column-bg); + box-shadow: 0 22px 44px rgba(0, 0, 0, 0.14); + padding: 10px; +} + +.kanban-column__header { + margin-bottom: 10px; + padding: 8px; + border: 1px solid var(--color-border-subtle); + background: var(--color-column-header); +} + +.kanban-column__title-row { + display: flex; + align-items: center; + gap: 10px; +} + +.kanban-column__dot { + width: 10px; + height: 10px; + border-radius: 999px; + box-shadow: 0 0 22px color-mix(in srgb, currentColor 50%, transparent); +} + +.kanban-column__title { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--color-text-primary); +} + +.kanban-column__count { + margin-left: auto; + padding: 2px 8px; + border: 1px solid var(--color-border-subtle); + background: color-mix(in srgb, var(--color-bg-base) 45%, transparent); + font-size: 11px; + font-weight: 600; + line-height: 1.3; + color: var(--color-text-secondary); + font-variant-numeric: tabular-nums; +} + +.kanban-column__caption { + margin-top: 8px; + font-size: 11px; + line-height: 1.5; + color: var(--color-text-muted); +} + +.kanban-column-body { + flex: 1; + overflow-y: auto; + padding: 2px 0; +} + +.kanban-column-body::-webkit-scrollbar { + width: 4px; +} +.kanban-column-body::-webkit-scrollbar-track { + background: transparent; +} +.kanban-column-body::-webkit-scrollbar-thumb { + background: var(--color-scrollbar); + border-radius: 2px; +} + +/* ── Session card zone glow (hover) ──────────────────────────────────── */ + +.kanban-column__empty { + display: flex; + height: 112px; + align-items: center; + justify-content: center; + border: 1px dashed var(--color-border-default); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.015) 0%, transparent 100%); +} + +.kanban-column__empty-label { + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-text-muted); +} + +.session-card.card-glow-respond:hover { + box-shadow: + 0 0 0 1px rgba(255, 123, 114, 0.16), + 0 16px 32px rgba(2, 6, 12, 0.24), + 0 0 16px rgba(255, 123, 114, 0.06); +} + +.session-card.card-glow-review:hover { + box-shadow: + 0 0 0 1px rgba(255, 157, 87, 0.16), + 0 16px 32px rgba(2, 6, 12, 0.24), + 0 0 16px rgba(255, 157, 87, 0.06); +} + +.session-card.card-glow-pending:hover { + box-shadow: + 0 0 0 1px rgba(241, 190, 100, 0.15), + 0 16px 32px rgba(2, 6, 12, 0.24), + 0 0 16px rgba(241, 190, 100, 0.05); +} + +.session-card.card-glow-working:hover { + box-shadow: + 0 0 0 1px rgba(143, 180, 255, 0.15), + 0 16px 32px rgba(2, 6, 12, 0.24), + 0 0 16px rgba(143, 180, 255, 0.05); +} + +.session-card.card-glow-merge:hover { + box-shadow: + 0 0 0 1px rgba(115, 224, 170, 0.16), + 0 16px 32px rgba(2, 6, 12, 0.24), + 0 0 16px rgba(115, 224, 170, 0.06); +} + +@media (max-width: 960px) { + .dashboard-hero__content { + padding: 18px; + } + + .dashboard-hero__meta { + width: 100%; + } + + .dashboard-stats { + width: 100%; + } + + .kanban-board { + height: auto; + min-height: calc(100vh - 220px); + } } diff --git a/packages/web/src/app/showcase/page.tsx b/packages/web/src/app/showcase/page.tsx new file mode 100644 index 000000000..21100b7bb --- /dev/null +++ b/packages/web/src/app/showcase/page.tsx @@ -0,0 +1,990 @@ +"use client"; + +import { useState } from "react"; +import type { DashboardSession, DashboardPR, AttentionLevel, DashboardCICheck } from "@/lib/types"; +import { AttentionZone } from "@/components/AttentionZone"; +import { SessionCard } from "@/components/SessionCard"; +import { ActivityDot } from "@/components/ActivityDot"; +import { CIBadge } from "@/components/CIBadge"; +import { ThemeToggle } from "@/components/ThemeToggle"; +import { ProjectSidebar } from "@/components/ProjectSidebar"; +import type { ProjectInfo } from "@/lib/project-name"; + +// ── Mock Data ──────────────────────────────────────────────────────────── + +function makePR(overrides: Partial = {}): DashboardPR { + return { + number: 142, + url: "#", + title: "Add user authentication flow with OAuth2", + owner: "acme", + repo: "agent-orchestrator", + branch: "feat/auth-flow", + baseBranch: "main", + isDraft: false, + state: "open", + additions: 234, + deletions: 48, + ciStatus: "passing", + ciChecks: [ + { name: "build", status: "passed" }, + { name: "lint", status: "passed" }, + { name: "test", status: "passed" }, + ], + reviewDecision: "approved", + mergeability: { + mergeable: true, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + unresolvedThreads: 0, + unresolvedComments: [], + ...overrides, + }; +} + +function makeSession(overrides: Partial = {}): DashboardSession { + return { + id: `ses_${Math.random().toString(36).slice(2, 8)}`, + projectId: "proj_alpha", + status: "working", + activity: "active", + branch: "feat/add-feature", + issueId: null, + issueUrl: null, + issueLabel: null, + issueTitle: null, + summary: null, + summaryIsFallback: false, + createdAt: new Date().toISOString(), + lastActivityAt: new Date().toISOString(), + pr: null, + metadata: {}, + ...overrides, + }; +} + +// ── Merge Ready sessions +const mergeSessions: DashboardSession[] = [ + makeSession({ + id: "ses_m1", + status: "approved", + activity: "ready", + branch: "feat/auth-flow", + pr: makePR({ + number: 142, + title: "Add user authentication flow with OAuth2", + additions: 234, + deletions: 48, + }), + }), + makeSession({ + id: "ses_m2", + status: "approved", + activity: "ready", + branch: "fix/rate-limiter", + pr: makePR({ + number: 156, + title: "Fix rate limiter edge case on burst traffic", + additions: 18, + deletions: 5, + }), + }), +]; + +// ── Respond sessions +const respondSessions: DashboardSession[] = [ + makeSession({ + id: "ses_r1", + status: "needs_input", + activity: "waiting_input", + branch: "feat/billing-integration", + summary: "Waiting for Stripe API keys to proceed with billing integration", + pr: makePR({ + number: 160, + title: "Integrate Stripe billing with subscription tiers", + ciStatus: "passing", + reviewDecision: "pending", + mergeability: { + mergeable: false, + ciPassing: true, + approved: false, + noConflicts: true, + blockers: [], + }, + additions: 890, + deletions: 120, + }), + }), +]; + +// ── Review sessions +const reviewSessions: DashboardSession[] = [ + makeSession({ + id: "ses_v1", + status: "ci_failed", + activity: "idle", + branch: "feat/search-index", + pr: makePR({ + number: 151, + title: "Implement full-text search with Elasticsearch", + ciStatus: "failing", + ciChecks: [ + { name: "build", status: "passed" }, + { name: "lint", status: "passed" }, + { name: "test", status: "failed" }, + { name: "e2e", status: "failed" }, + ], + reviewDecision: "pending", + mergeability: { + mergeable: false, + ciPassing: false, + approved: false, + noConflicts: true, + blockers: [], + }, + additions: 542, + deletions: 89, + }), + }), + makeSession({ + id: "ses_v2", + status: "changes_requested", + activity: "idle", + branch: "refactor/api-layer", + pr: makePR({ + number: 148, + title: "Refactor API layer to use tRPC", + ciStatus: "passing", + reviewDecision: "changes_requested", + mergeability: { + mergeable: false, + ciPassing: true, + approved: false, + noConflicts: false, + blockers: [], + }, + unresolvedThreads: 3, + unresolvedComments: [ + { + url: "#", + path: "src/api/router.ts", + author: "reviewer1", + body: "This breaks backward compat", + }, + { + url: "#", + path: "src/api/client.ts", + author: "reviewer1", + body: "Missing error handling", + }, + { url: "#", path: "src/api/types.ts", author: "reviewer2", body: "Use zod schema instead" }, + ], + additions: 1240, + deletions: 680, + }), + }), +]; + +// ── Pending sessions +const pendingSessions: DashboardSession[] = [ + makeSession({ + id: "ses_p1", + status: "review_pending", + activity: "idle", + branch: "feat/dark-mode", + pr: makePR({ + number: 155, + title: "Add comprehensive dark mode support", + ciStatus: "pending", + ciChecks: [ + { name: "build", status: "running" }, + { name: "test", status: "pending" }, + ], + reviewDecision: "pending", + mergeability: { + mergeable: false, + ciPassing: false, + approved: false, + noConflicts: true, + blockers: [], + }, + additions: 380, + deletions: 95, + }), + }), +]; + +// ── Working sessions +const workingSessions: DashboardSession[] = [ + makeSession({ + id: "ses_w1", + status: "working", + activity: "active", + branch: "feat/kanban-filters", + summary: "Implementing column filters for kanban board views", + pr: makePR({ + number: 162, + title: "Add column filter controls to kanban board", + ciStatus: "pending", + reviewDecision: "none", + mergeability: { + mergeable: false, + ciPassing: false, + approved: false, + noConflicts: true, + blockers: [], + }, + additions: 156, + deletions: 22, + }), + }), + makeSession({ + id: "ses_w2", + status: "working", + activity: "active", + branch: "feat/sidebar-tree", + summary: "Building collapsible project sidebar with health indicators", + }), + makeSession({ + id: "ses_w3", + status: "working", + activity: "active", + branch: "feat/sse-events", + summary: "Wiring real-time SSE event streaming for live updates", + }), +]; + +// ── Done sessions +const doneSessions: DashboardSession[] = [ + makeSession({ + id: "ses_d1", + status: "merged", + activity: "exited", + branch: "feat/theme-toggle", + pr: makePR({ + number: 138, + title: "Light/dark theme toggle with CSS custom properties", + state: "merged", + additions: 420, + deletions: 180, + }), + }), + makeSession({ + id: "ses_d2", + status: "killed", + activity: "exited", + branch: "experiment/webgl-chart", + summary: "Experimental WebGL chart rendering (abandoned)", + }), + makeSession({ + id: "ses_d3", + status: "done", + activity: "exited", + branch: "fix/ssr-hydration", + pr: makePR({ + number: 135, + title: "Fix SSR hydration mismatch in theme provider", + state: "merged", + additions: 24, + deletions: 8, + }), + }), +]; + +const allSessions = [ + ...mergeSessions, + ...respondSessions, + ...reviewSessions, + ...pendingSessions, + ...workingSessions, + ...doneSessions, +]; + +const mockProjects: ProjectInfo[] = [ + { id: "proj_alpha", name: "Alpha" }, + { id: "proj_beta", name: "Beta" }, + { id: "proj_gamma", name: "Gamma" }, +]; + +// ── Showcase Page ──────────────────────────────────────────────────────── + +const ZONE_ORDER: AttentionLevel[] = ["respond", "review", "pending", "working", "merge", "done"]; +const zoneMap: Record = { + merge: mergeSessions, + respond: respondSessions, + review: reviewSessions, + pending: pendingSessions, + working: workingSessions, + done: doneSessions, +}; + +const ACTIVITY_STATES = ["active", "ready", "idle", "waiting_input", "blocked", "exited"] as const; +const CI_STATUSES: Array<{ + status: "passing" | "failing" | "pending" | "none"; + checks?: DashboardCICheck[]; +}> = [ + { + status: "passing", + checks: [ + { name: "build", status: "passed" }, + { name: "test", status: "passed" }, + ], + }, + { + status: "failing", + checks: [ + { name: "build", status: "passed" }, + { name: "test", status: "failed" }, + ], + }, + { + status: "pending", + checks: [ + { name: "build", status: "running" }, + { name: "test", status: "pending" }, + ], + }, + { status: "none" }, +]; + +export default function ShowcasePage() { + const [sidebarVisible, setSidebarVisible] = useState(true); + const noop = () => {}; + const totalSessions = allSessions.length; + const workingCount = workingSessions.length; + const reviewLoad = respondSessions.length + reviewSessions.length + pendingSessions.length; + const mergeReadyCount = mergeSessions.length; + + return ( +
+
+
+
+
+
+
+ + Showcase reference +
+
+

Dashboard UI Overhaul

+

+ Reference surface for the redesigned dark-mode kanban dashboard, including the + mission-control header, project rail, lane treatment, and card hierarchy. +

+
+
+ {[ + "Mission Control Hero", + "Dark Kanban Lanes", + "Project Rail", + "Session Card States", + "CI / PR Signals", + "Theme Tokens", + ].map((tag) => ( + + {tag} + + ))} +
+
+ +
+
+ + {totalSessions} + sessions + + + + {workingCount} + + working + + + + {reviewLoad} + + attention load + + + + {mergeReadyCount} + + merge ready + +
+
+ + +
+
+
+
+ + {/* ── Section: Kanban Board ──────────────────────────── */} +
+
+
+
+
+ Live board shell +
+
+ Session lanes, sidebar, and card hierarchy as they should look in the app. +
+
+
+ + Graphite surface + + + Bright status accents + + + Independent lane scroll + +
+
+ +
+ {sidebarVisible && ( +
+ +
+ )} + +
+
+
+ {ZONE_ORDER.map((level) => ( + + ))} +
+
+
+
+
+
+ + {/* ── Section: Attention Zones ───────────────────────── */} +
+
+ {( + [ + { + level: "merge", + label: "Merge Ready", + color: "var(--color-status-ready)", + desc: "PR approved + CI green", + }, + { + level: "respond", + label: "Respond", + color: "var(--color-status-error)", + desc: "Agent waiting for input", + }, + { + level: "review", + label: "Review", + color: "var(--color-accent-orange)", + desc: "CI failed / changes requested", + }, + { + level: "pending", + label: "Pending", + color: "var(--color-status-attention)", + desc: "Waiting on reviewer / CI", + }, + { + level: "working", + label: "Working", + color: "var(--color-status-working)", + desc: "Agent actively coding", + }, + { + level: "done", + label: "Done", + color: "var(--color-text-tertiary)", + desc: "Merged or terminated", + }, + ] as const + ).map((zone) => ( +
+
+
+ + {zone.label} + +
+
+ Lane rule +
+

+ {zone.desc} +

+
+ ))} +
+
+ + {/* ── Section: Activity States ───────────────────────── */} +
+
+ {ACTIVITY_STATES.map((activity) => ( +
+ + +
+ ))} +
+
+ + {/* ── Section: CI Badges ─────────────────────────────── */} +
+
+ {CI_STATUSES.map((ci) => ( + + ))} +
+
+ + {/* ── Section: Card Variants ─────────────────────────── */} +
+
+ {/* Merge-ready card */} +
+ + +
+ + {/* Respond card */} +
+ + +
+ + {/* Review card with CI failures */} +
+ + +
+ + {/* Review card with unresolved comments */} +
+ + +
+ + {/* Working card */} +
+ + +
+ + {/* Done card (merged) */} +
+ + +
+
+
+ + {/* ── Section: Design Tokens ─────────────────────────── */} +
+ {/* Colors */} +
+ +
+ {[ + { name: "Working", var: "--color-status-working" }, + { name: "Ready", var: "--color-status-ready" }, + { name: "Attention", var: "--color-status-attention" }, + { name: "Error", var: "--color-status-error" }, + { name: "Done", var: "--color-status-done" }, + { name: "Idle", var: "--color-status-idle" }, + ].map((c) => ( +
+
+
+
+ {c.name} +
+
+ {c.var} +
+
+
+ ))} +
+
+ +
+ +
+ {[ + { name: "Blue", var: "--color-accent-blue" }, + { name: "Green", var: "--color-accent-green" }, + { name: "Yellow", var: "--color-accent-yellow" }, + { name: "Orange", var: "--color-accent-orange" }, + { name: "Red", var: "--color-accent-red" }, + { name: "Violet", var: "--color-accent-violet" }, + ].map((c) => ( +
+
+
+
+ {c.name} +
+
+ {c.var} +
+
+
+ ))} +
+
+ +
+ +
+ {[ + { name: "Base", var: "--color-bg-base" }, + { name: "Surface", var: "--color-bg-surface" }, + { name: "Elevated", var: "--color-bg-elevated" }, + { name: "Subtle", var: "--color-bg-subtle" }, + ].map((c) => ( +
+
+
+
+ {c.name} +
+
+ {c.var} +
+
+
+ ))} +
+
+ +
+ +
+ {[ + { name: "Blue", var: "--color-tint-blue" }, + { name: "Green", var: "--color-tint-green" }, + { name: "Yellow", var: "--color-tint-yellow" }, + { name: "Red", var: "--color-tint-red" }, + { name: "Violet", var: "--color-tint-violet" }, + { name: "Orange", var: "--color-tint-orange" }, + { name: "Neutral", var: "--color-tint-neutral" }, + ].map((c) => ( +
+
+
+
+ {c.name} +
+
+ {c.var} +
+
+
+ ))} +
+
+ + {/* Typography */} +
+ +
+
+ + Heading XL — IBM Plex Sans 17px/700 + +
+
+ + Heading — IBM Plex Sans 14px/600 + +
+
+ + Body — IBM Plex Sans 13px/400 + +
+
+ + Label — IBM Plex Sans 11px/600 uppercase + +
+
+ + Monospace — IBM Plex Mono 11px + +
+
+
+
+ + {/* ── Section: Animations ────────────────────────────── */} +
+
+ {/* Activity pulse */} +
+ +

+ Active agents show a pulsing glow effect +

+
+
+
+
+
+
+ + {/* Page enter */} +
+ +

+ Pages slide up on entry for smooth navigation +

+
+ Content slides in from below +
+
+ + {/* Skeleton loading */} +
+ +

+ Shimmer effect for loading states +

+
+
+
+
+
+
+
+
+ + {/* ── Section: Alert Banners ─────────────────────────── */} +
+
+
+ + + + + + Orchestrator paused: Rate limit exceeded. Resume after 2:30 PM + +
+ +
+ + + + + + GitHub API rate limited — PR data (CI status, review state, sizes) may be stale. + +
+
+
+ + {/* ── Section: Theme Comparison ──────────────────────── */} +
+
+
+ +
+

+ Full Light / Dark Mode +

+

+ 50+ CSS custom property tokens with smooth 0.25s transitions. Theme persists via + localStorage. FOUC prevention with beforeInteractive script. +

+
+
+
+
+ + {/* ── Footer ─────────────────────────────────────────── */} +
+

+ Agent Orchestrator — Dashboard UI Overhaul Showcase +

+
+
+
+ ); +} + +// ── Helper Components ──────────────────────────────────────────────────── + +function Section({ + title, + subtitle, + children, +}: { + title: string; + subtitle: string; + children: React.ReactNode; +}) { + return ( +
+
+

+ {title} +

+

{subtitle}

+
+ {children} +
+ ); +} + +function Label({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/packages/web/src/components/AttentionZone.tsx b/packages/web/src/components/AttentionZone.tsx index 26ac03726..52d2a57cb 100644 --- a/packages/web/src/components/AttentionZone.tsx +++ b/packages/web/src/components/AttentionZone.tsx @@ -1,13 +1,12 @@ "use client"; -import { memo, useState } from "react"; +import { memo } from "react"; import type { DashboardSession, AttentionLevel } from "@/lib/types"; import { SessionCard } from "./SessionCard"; interface AttentionZoneProps { level: AttentionLevel; sessions: DashboardSession[]; - variant?: "column" | "grid"; onSend?: (sessionId: string, message: string) => void; onKill?: (sessionId: string) => void; onMerge?: (prNumber: number) => void; @@ -19,87 +18,68 @@ const zoneConfig: Record< { label: string; color: string; - defaultCollapsed: boolean; + caption: string; } > = { merge: { - label: "Merge", + label: "Merge Ready", color: "var(--color-status-ready)", - defaultCollapsed: false, + caption: "Cleared to land", }, respond: { label: "Respond", color: "var(--color-status-error)", - defaultCollapsed: false, + caption: "Human judgment needed", }, review: { label: "Review", color: "var(--color-accent-orange)", - defaultCollapsed: false, + caption: "Code waiting on eyes", }, pending: { label: "Pending", color: "var(--color-status-attention)", - defaultCollapsed: false, + caption: "Blocked on system state", }, working: { label: "Working", color: "var(--color-status-working)", - defaultCollapsed: false, + caption: "Agents are actively moving", }, done: { label: "Done", color: "var(--color-text-tertiary)", - defaultCollapsed: true, + caption: "Completed or exited", }, }; +/** + * Kanban column — always renders (even when empty) to preserve + * the board shape. Cards scroll independently within each column. + */ function AttentionZoneView({ level, sessions, - variant = "grid", onSend, onKill, onMerge, onRestore, }: AttentionZoneProps) { const config = zoneConfig[level]; - const [collapsed, setCollapsed] = useState(config.defaultCollapsed); - if (sessions.length === 0) return null; + return ( +
+
+
+
+ {config.label} + {sessions.length} +
+

{config.caption}

+
- if (variant === "column") { - return ( -
- {/* Column header */} - - - {!collapsed && ( +
+ {sessions.length > 0 ? (
{sessions.map((session) => ( ))}
+ ) : ( +
+ No sessions +
)}
- ); - } - - return ( -
- {/* Zone header: [●] LABEL ──────────────────────────────── count [▾] */} - - - {!collapsed && ( -
- {sessions.map((session) => ( - - ))} -
- )}
); } @@ -170,7 +105,6 @@ function AttentionZoneView({ function areAttentionZonePropsEqual(prev: AttentionZoneProps, next: AttentionZoneProps): boolean { return ( prev.level === next.level && - prev.variant === next.variant && prev.onSend === next.onSend && prev.onKill === next.onKill && prev.onMerge === next.onMerge && diff --git a/packages/web/src/components/Dashboard.tsx b/packages/web/src/components/Dashboard.tsx index 80cd66835..d1031e954 100644 --- a/packages/web/src/components/Dashboard.tsx +++ b/packages/web/src/components/Dashboard.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useSearchParams } from "next/navigation"; import { type DashboardSession, type DashboardStats, @@ -10,14 +11,16 @@ import { type DashboardOrchestratorLink, getAttentionLevel, isPRRateLimited, + CI_STATUS, } from "@/lib/types"; -import { CI_STATUS } from "@composio/ao-core/types"; import { AttentionZone } from "./AttentionZone"; import { PRTableRow } from "./PRStatus"; import { DynamicFavicon } from "./DynamicFavicon"; import { useSessionEvents } from "@/hooks/useSessionEvents"; import { ProjectSidebar } from "./ProjectSidebar"; +import { ThemeToggle } from "./ThemeToggle"; import type { ProjectInfo } from "@/lib/project-name"; +import { EmptyState } from "./Skeleton"; interface DashboardProps { initialSessions: DashboardSession[]; @@ -58,6 +61,8 @@ export function Dashboard({ initialGlobalPause, projectId, ); + const searchParams = useSearchParams(); + const activeSessionId = searchParams.get("session") ?? undefined; const [rateLimitDismissed, setRateLimitDismissed] = useState(false); const [globalPauseDismissed, setGlobalPauseDismissed] = useState(false); const [activeOrchestrators, setActiveOrchestrators] = @@ -67,6 +72,11 @@ export function Dashboard({ const showSidebar = projects.length > 1; const allProjectsView = showSidebar && projectId === undefined; + const displaySessions = useMemo(() => { + if (allProjectsView || !activeSessionId) return sessions; + return sessions.filter((s) => s.id === activeSessionId); + }, [sessions, allProjectsView, activeSessionId]); + useEffect(() => { setActiveOrchestrators((current) => mergeOrchestrators(current, orchestratorLinks)); }, [orchestratorLinks]); @@ -80,11 +90,11 @@ export function Dashboard({ working: [], done: [], }; - for (const session of sessions) { + for (const session of displaySessions) { zones[getAttentionLevel(session)].push(session); } return zones; - }, [sessions]); + }, [displaySessions]); const sessionsByProject = useMemo(() => { const groupedSessions = new Map(); @@ -100,14 +110,14 @@ export function Dashboard({ }, [sessions]); const openPRs = useMemo(() => { - return sessions + return displaySessions .filter( (session): session is DashboardSession & { pr: DashboardPR } => session.pr?.state === "open", ) .map((session) => session.pr) .sort((a, b) => mergeScore(a) - mergeScore(b)); - }, [sessions]); + }, [displaySessions]); const projectOverviews = useMemo(() => { if (!allProjectsView) return []; @@ -214,7 +224,7 @@ export function Dashboard({ } }; - const hasKanbanSessions = KANBAN_LEVELS.some((level) => grouped[level].length > 0); + const hasAnySessions = sessions.length > 0; const anyRateLimited = useMemo( () => sessions.some((session) => session.pr && isPRRateLimited(session.pr)), @@ -245,22 +255,45 @@ export function Dashboard({ }, [globalPause?.pausedUntil, globalPause?.reason, globalPause?.sourceSessionId]); return ( -
- {showSidebar && } -
+
+ {showSidebar && ( + + )} +
-
-
-

- {projectName ?? "Orchestrator"} -

- +
+
+
+
+
+ + Overview +
+
+

{projectName ?? "Orchestrator"}

+

+ Live sessions, review pressure, and merge readiness. +

+
+
+ +
+ +
+ {!allProjectsView && } + +
+
- {!allProjectsView && } -
+
{globalPause && !globalPauseDismissed && ( -
+
+
)} - {!allProjectsView && hasKanbanSessions && ( -
- {KANBAN_LEVELS.map((level) => - grouped[level].length > 0 ? ( -
- -
- ) : null, - )} + {!allProjectsView && hasAnySessions && ( +
+
+ {KANBAN_LEVELS.map((level) => ( + + ))} +
)} - {!allProjectsView && grouped.done.length > 0 && ( -
- -
- )} + {!allProjectsView && !hasAnySessions && } {openPRs.length > 0 && (
@@ -615,19 +634,17 @@ function StatusLine({ stats }: { stats: DashboardStats }) { ]; return ( -
+
{parts.map((part, index) => ( - - {index > 0 && ( - · - )} + {part.value} - {part.label} + {part.label} + {index < parts.length - 1 && } ))}
diff --git a/packages/web/src/components/PRStatus.tsx b/packages/web/src/components/PRStatus.tsx index 3d0a38de3..1ead53803 100644 --- a/packages/web/src/components/PRStatus.tsx +++ b/packages/web/src/components/PRStatus.tsx @@ -3,7 +3,7 @@ import { type DashboardPR, isPRRateLimited } from "@/lib/types"; import { CIBadge } from "./CIBadge"; -function getSizeLabel(additions: number, deletions: number): string { +export function getSizeLabel(additions: number, deletions: number): string { const size = additions + deletions; return size > 1000 ? "XL" : size > 500 ? "L" : size > 200 ? "M" : size > 50 ? "S" : "XS"; } diff --git a/packages/web/src/components/ProjectSidebar.tsx b/packages/web/src/components/ProjectSidebar.tsx index 76d400145..85695e4f4 100644 --- a/packages/web/src/components/ProjectSidebar.tsx +++ b/packages/web/src/components/ProjectSidebar.tsx @@ -1,63 +1,283 @@ "use client"; +import { useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import { cn } from "@/lib/cn"; import type { ProjectInfo } from "@/lib/project-name"; +import { getAttentionLevel, type DashboardSession, type AttentionLevel } from "@/lib/types"; +import { isOrchestratorSession } from "@composio/ao-core/types"; +import { getSessionTitle } from "@/lib/format"; interface ProjectSidebarProps { projects: ProjectInfo[]; + sessions: DashboardSession[]; activeProjectId: string | undefined; + activeSessionId: string | undefined; } -export function ProjectSidebar({ projects, activeProjectId }: ProjectSidebarProps) { +type ProjectHealth = "red" | "yellow" | "green" | "gray"; + +function computeProjectHealth(sessions: DashboardSession[]): ProjectHealth { + const workers = sessions.filter((s) => !isOrchestratorSession(s)); + if (workers.length === 0) return "gray"; + for (const s of workers) { + if (getAttentionLevel(s) === "respond") return "red"; + } + for (const s of workers) { + const lvl = getAttentionLevel(s); + if (lvl === "review" || lvl === "pending") return "yellow"; + } + return "green"; +} + +const healthDotColor: Record = { + red: "var(--color-status-error)", + yellow: "var(--color-status-attention)", + green: "var(--color-status-ready)", + gray: "var(--color-text-tertiary)", +}; + +const sessionDotColor: Record = { + merge: "var(--color-status-ready)", + respond: "var(--color-status-error)", + review: "var(--color-accent-orange)", + pending: "var(--color-status-attention)", + working: "var(--color-status-working)", + done: "var(--color-text-tertiary)", +}; + +const sessionToneLabel: Record = { + merge: "merge", + respond: "reply", + review: "review", + pending: "wait", + working: "live", + done: "done", +}; + +function SessionDot({ level }: { level: AttentionLevel }) { + return ( +
+ ); +} + +function HealthDot({ health }: { health: ProjectHealth }) { + return ( +
+ ); +} + +export function ProjectSidebar({ + projects, + sessions, + activeProjectId, + activeSessionId, +}: ProjectSidebarProps) { const router = useRouter(); const pathname = usePathname(); - const handleProjectClick = (projectId: string | null) => { - if (projectId === null) { - router.push(pathname + "?project=all"); - } else { - router.push(pathname + `?project=${encodeURIComponent(projectId)}`); + const [expandedProjects, setExpandedProjects] = useState>( + () => new Set(activeProjectId && activeProjectId !== "all" ? [activeProjectId] : []), + ); + + useEffect(() => { + if (activeProjectId && activeProjectId !== "all") { + setExpandedProjects((prev) => new Set([...prev, activeProjectId])); } + }, [activeProjectId]); + + const toggleExpand = (projectId: string) => { + setExpandedProjects((prev) => { + const next = new Set(prev); + if (next.has(projectId)) { + next.delete(projectId); + } else { + next.add(projectId); + } + return next; + }); + }; + + const handleProjectHeaderClick = (projectId: string) => { + toggleExpand(projectId); + router.push(pathname + `?project=${encodeURIComponent(projectId)}`); }; if (projects.length <= 1) { return null; } + const totalWorkerSessions = sessions.filter((s) => !isOrchestratorSession(s)).length; + const needsInputCount = sessions.filter((s) => getAttentionLevel(s) === "respond").length; + const reviewLoadCount = sessions.filter((s) => { + const level = getAttentionLevel(s); + return level === "review" || level === "pending"; + }).length; + return ( -