Merge pull request #3 from zenchantlive/feat/ui-polish-aero-chrome

UI polish: shared hero parity + graph task language cleanup
This commit is contained in:
zenchantlive 2026-02-14 00:31:48 -08:00 committed by GitHub
commit 536786cd52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 636 additions and 1045 deletions

View file

@ -1,3 +1,4 @@
{"id":"bb-0h7","title":"Implement aero-chrome UI polish on workflow explorer pages","description":"Implement phase A-D UI polish pass on existing pages using global tokens and scoped component styling. Keep business logic unchanged. Ensure graph edge visibility and relation labels remain readable. Ensure mobile containment and scrolling behavior are correct.","acceptance_criteria":"No white-screen/runtime errors; graph edges and labels visible; cards/panels follow new hierarchy; no overflow regressions; Playwright evidence captured.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T09:54:32.2744347-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T10:24:52.3489251-08:00","closed_at":"2026-02-13T10:24:52.3489251-08:00","close_reason":"Superseded by bb-b4j phased epic tree (foundation -\u003e shared -\u003e kanban/graph -\u003e guards -\u003e verification).","labels":["graph","kanban","tailwind","ui"]}
{"id":"bb-18e","title":"Graph Clarity Follow-up (Post-Kanban)","description":"Objective:\nTrack graph-focused clarity enhancements that are explicitly out of current Kanban execution scope.\n\nScope:\n- Dependency graph comprehension aids\n- anomaly communication for cycle/deadlock scenarios\n- future AI explanation hooks for dependency understanding\n\nOut of scope for immediate execution:\n- Kanban UI polish currently underway in bb-1es\n\nPlanning note:\nThis epic stays pending until current Kanban actionability epic reaches acceptance.","acceptance_criteria":"- Graph clarity backlog is explicitly captured and linked to future implementation beads.\n- No accidental scope leakage into current Kanban pass.","notes":"Backlog epic for graph-specific clarity work discussed after bb-1es.\nPost-Kanban gate enforced: bb-18e depends on bb-1es completion. Child bb-18e.1 is also explicitly blocked by bb-1es to prevent scope bleed before Kanban pass completes.\nExecution sequence added: (1) bb-18e.2 edge labels/contrast + bb-18e.3 direction hints + bb-18e.1 cycle card, (2) bb-18e.4 edge toggles + bb-18e.6 centered focus, (3) bb-18e.5 external blockers + bb-18e.7 progressive details + bb-18e.10 risk tinting, (4) bb-18e.8 keyboard nav + bb-18e.9 URL state, (5) bb-18e.11 AI explanation scaffold (deferred).","status":"open","priority":2,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T19:45:40.5451814-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T20:21:45.1768727-08:00","labels":["backlog","graph","ux"],"dependencies":[{"issue_id":"bb-18e","depends_on_id":"bb-1es","type":"blocks","created_at":"2026-02-12T19:53:11.4089925-08:00","created_by":"zenchantlive"}]}
{"id":"bb-18e.1","title":"Add cycle warning card with focus actions in graph view","description":"Goal:\nWhen dependency cycles exist, show an explicit cycle warning card so users understand why work may be deadlocked.\n\nProblem:\nCycle states are hard to infer from dense node/edge layouts, leading to confusion (“why cant this move?”).\n\nBehavior contract:\n- Show warning card only when cycle analysis is non-empty.\n- Card includes:\n - cycle count\n - affected bead ids (compact list)\n - click-to-focus action for each cycle group\n - plain language explanation of impact (“tasks in this loop cannot fully unblock each other without breaking the cycle”).\n- Visual style: warning but not alarmist (amber/red subtle).\n\nImplementation tasks:\n1) Build compact cycle summary model from existing detection output.\n2) Add warning card component above graph viewport.\n3) Wire click handlers to focus selected cycle nodes.\n4) Add tests for no-cycle and multi-cycle rendering behavior.\n\nOut of scope:\n- Automatic cycle resolution suggestions.\n- Mutation/write automation.","acceptance_criteria":"- Cycle warning card appears only when cycles are present.\n- Card provides actionable cycle navigation.\n- Language is plain and explains user impact.\n- Tests cover empty and non-empty cycle states.\n- Typecheck and graph guards pass.","notes":"This is the #19 idea captured as an explicit implementation bead.\nDepends on existing cycle analysis primitives already implemented.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T19:46:01.2478576-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T19:46:01.2478576-08:00","labels":["anomaly","graph","ux"],"dependencies":[{"issue_id":"bb-18e.1","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T19:46:01.2494327-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.1","depends_on_id":"bb-1es","type":"blocks","created_at":"2026-02-12T19:53:11.9910819-08:00","created_by":"zenchantlive"}]}
{"id":"bb-18e.10","title":"Add downstream-impact risk tinting","description":"Add subtle risk tinting based on downstream impact count to highlight high-blast-radius tasks.","acceptance_criteria":"- Higher downstream impact gets stronger but subtle visual signal.\n- Does not overpower status/selection color language.\n- Works with existing legend semantics.","notes":"Use restrained styling; no heavy borders.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:21.812041-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T20:21:21.812041-08:00","labels":["graph","signal","ux"],"dependencies":[{"issue_id":"bb-18e.10","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:21.8153577-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.10","depends_on_id":"bb-18e.4","type":"blocks","created_at":"2026-02-12T20:21:43.4643033-08:00","created_by":"zenchantlive"}]}
@ -22,6 +23,7 @@
{"id":"bb-29x.4","title":"Document operational runbook and boundary rationale","description":"Write architecture docs covering scanner policy, bd bridge behavior, and consistency guardrails for future maintainers.","acceptance_criteria":"Runbook documents startup, troubleshooting, and boundary rules.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:19.1385778-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:19.1385778-08:00","labels":["docs","runbook"],"dependencies":[{"issue_id":"bb-29x.4","depends_on_id":"bb-29x","type":"parent-child","created_at":"2026-02-11T17:12:19.1402086-08:00","created_by":"zenchantlive"},{"issue_id":"bb-29x.4","depends_on_id":"bb-29x.2","type":"blocks","created_at":"2026-02-11T17:12:39.9591458-08:00","created_by":"zenchantlive"},{"issue_id":"bb-29x.4","depends_on_id":"bb-29x.5","type":"blocks","created_at":"2026-02-11T20:10:12.3474801-08:00","created_by":"zenchantlive"}]}
{"id":"bb-29x.5","title":"Epic Design Gate: scope, decisions, and acceptance contract","description":"Design/discovery gate for bb-29x before further implementation.\n\nMust capture:\n- Product intent and user outcomes for this epic\n- Explicit architecture decisions and tradeoffs\n- API/data contracts and edge cases\n- Windows-specific constraints and path/process assumptions\n- Test strategy and verification commands\n- Non-goals and out-of-scope boundaries\n\nCompletion rule:\nDo not start new implementation tasks in this epic until this gate is closed with agreed decisions.","acceptance_criteria":"A written execution-grade plan exists for this epic and all child task descriptions are updated with concrete implementation details, dependencies, and testable acceptance criteria.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T20:09:42.1507616-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T20:09:42.1507616-08:00","dependencies":[{"issue_id":"bb-29x.5","depends_on_id":"bb-29x","type":"parent-child","created_at":"2026-02-11T20:09:42.1525436-08:00","created_by":"zenchantlive"}]}
{"id":"bb-3pr","title":"Smoke test mutation lifecycle 2","description":"Temporary issue for API mutation smoke test","status":"closed","priority":1,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T19:44:10.9737485-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T19:44:16.4912473-08:00","closed_at":"2026-02-11T19:44:16.4912473-08:00","close_reason":"Cleanup after API smoke test","labels":["api","smoke"],"comments":[{"id":1,"issue_id":"bb-3pr","author":"zenchantlive","text":"Smoke test comment via API route","created_at":"2026-02-12T03:44:13Z"},{"id":2,"issue_id":"bb-3pr","author":"zenchantlive","text":"Smoke test reopen","created_at":"2026-02-12T03:44:15Z"}]}
{"id":"bb-3vi","title":"Fix misleading 'Blocking' label in task cards - should be 'Unlocks'","description":"In task-card-grid.tsx, the 'Blocking' section was showing outgoing blocking edges (tasks that this issue will unblock) but labeled incorrectly as 'Blocking'. Changed label to 'Unlocks' to correctly represent that this task, once completed, will unlock/unblock these downstream tasks.","notes":"Investigated: This is a bug in the bd CLI itself (C:\\tools\\beads\\bd.exe), not in this codebase. The issue detail view's 'BLOCKS' section displays which issues the current issue blocks, when it should display which issues block the current issue. The underlying dependency data is correct - this is purely a display/UI bug in the beads CLI.","status":"closed","priority":2,"issue_type":"bug","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T11:05:40.7518392-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T11:12:19.5922612-08:00","closed_at":"2026-02-13T11:12:19.5922612-08:00","close_reason":"Closed"}
{"id":"bb-6aj","title":"Project Registry and Multi-Project Scanner","description":"Deliver a Windows-first multi-project registry and discovery pipeline: persist project roots in the user profile, expose add/remove/list APIs, and scan safe roots to find .beads directories. Normalize all paths to stable identity keys and support aggregate views without full-drive traversal by default.","acceptance_criteria":"Projects can be added/removed/listed and discovered via scanner with deterministic normalization.","notes":"UI productization backlog added (2026-02-12): bb-6aj.6 design gate -\u003e bb-6aj.7 shared scope state -\u003e bb-6aj.8 project manager panel + bb-6aj.9 scanner UX + bb-6aj.10 scoped reads -\u003e bb-6aj.11 aggregate mode -\u003e bb-6aj.12 verification evidence. This sequence turns existing backend scanner/registry foundations into end-user multi-project workflows.\n2026-02-13 epic completion: UI productization chain complete (bb-6aj.6 -\u003e .7 -\u003e .8/.9/.10 -\u003e .11 -\u003e .12). Multi-project scope selection, registry manager, scanner discover/import, mode-aware reads, aggregate mode with project badges, and full verification evidence are now in place.","status":"closed","priority":0,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:47.7205517-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T22:35:21.1595002-08:00","closed_at":"2026-02-12T22:35:21.1595002-08:00","close_reason":"multi-project-scanner-epic-complete","labels":["multi-project","scanner"],"dependencies":[{"issue_id":"bb-6aj","depends_on_id":"bb-92d","type":"blocks","created_at":"2026-02-11T17:12:19.6374139-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.1","title":"Persist project registry in %USERPROFILE%\\\\.beadboard\\\\projects.json","description":"Implement read/write management for registry file in user profile path, isolated from repository files and safe for local machine usage.","acceptance_criteria":"Registry file is created lazily and survives app restarts.","status":"closed","priority":0,"issue_type":"task","assignee":"agent-a","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:48.5403111-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:53:17.2085722-08:00","closed_at":"2026-02-11T17:53:17.2085722-08:00","close_reason":"Implemented %USERPROFILE%/.beadboard/projects.json registry persistence with Windows-safe normalization and dedupe.","labels":["config","registry"],"dependencies":[{"issue_id":"bb-6aj.1","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:48.5419102-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.10","title":"Wire project-scoped reads into Kanban and Graph","description":"Connect selected project scope to data-loading paths for Kanban and Graph pages.\\n\\nScope:\\n- pass selected project root to read APIs\\n- ensure page refresh keeps selected scope\\n- keep existing single-project behavior as fallback\\n- preserve strict read/write boundary contracts","acceptance_criteria":"Kanban and Graph render data for the selected project scope and remain stable when switching projects.","notes":"2026-02-13 completed: rewired / and /graph server pages to resolve project scope from URL and load issues with selected root; implemented readIssuesForScope utility for mode-aware reads; preserved strict read-only boundaries (no direct JSONL writes).","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T21:41:42.9381588-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T22:33:58.8681434-08:00","closed_at":"2026-02-12T22:33:58.8681434-08:00","close_reason":"project-scoped-reads-wired","labels":["graph","kanban","multi-project"],"dependencies":[{"issue_id":"bb-6aj.10","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-12T21:41:42.9408199-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.10","depends_on_id":"bb-6aj.7","type":"blocks","created_at":"2026-02-12T21:41:42.9477322-08:00","created_by":"zenchantlive"}]}
@ -46,6 +48,28 @@
{"id":"bb-92d.6","title":"Add guardrail test preventing direct writes to .beads/issues.jsonl","description":"Enforce read/write boundary by scanning source for forbidden direct file write patterns targeting Beads issue files.","acceptance_criteria":"Guardrail test fails on boundary violations and passes when write path uses bd bridge only.","status":"closed","priority":0,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:46.9013352-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:28:27.4699395-08:00","closed_at":"2026-02-11T17:28:27.4699395-08:00","close_reason":"Added guardrail scanner and automated test to block direct writes to .beads/issues.jsonl.","labels":["guardrail","safety"],"dependencies":[{"issue_id":"bb-92d.6","depends_on_id":"bb-92d","type":"parent-child","created_at":"2026-02-11T17:11:46.9029535-08:00","created_by":"zenchantlive"}]}
{"id":"bb-ag8","title":"TEMP_DELETE_ME","status":"closed","priority":4,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:10:04.5765506-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:10:10.3812634-08:00","closed_at":"2026-02-11T17:10:10.3812634-08:00","close_reason":"cleanup temp test issue"}
{"id":"bb-atl","title":"Writeback phase smoke","description":"Temp for optimistic and transition smoke","status":"closed","priority":3,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T19:58:24.0374092-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T19:58:29.147102-08:00","closed_at":"2026-02-11T19:58:29.147102-08:00","close_reason":"cleanup writeback smoke","labels":["smoke","writeback"],"comments":[{"id":3,"issue_id":"bb-atl","author":"zenchantlive","text":"transition smoke reopen","created_at":"2026-02-12T03:58:27Z"}]}
{"id":"bb-b4j","title":"Workspace UI polish: Aero Chrome visual system","description":"Objective: apply a cohesive volumetric visual system across Workflow Explorer surfaces (kanban + graph) while preserving all existing data/model behavior. Scope: CSS/tailwind class refactor, typography hierarchy, status lighting, graph legibility and responsive containment. Non-goals: no state model changes, no API changes, no write-path changes.","acceptance_criteria":"Global tokens in place; cards/panels use consistent visual hierarchy; graph relationships are visually legible; responsive screenshots at 390/768/1440 pass; typecheck/tests pass.","status":"closed","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T09:54:31.8605492-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:01:44.435127-08:00","closed_at":"2026-02-13T12:01:44.435127-08:00","close_reason":"Aero Chrome UI polish epic complete with validated kanban + graph surfaces and evidence-backed closeout.","labels":["design-system","ui","workflow"]}
{"id":"bb-b4j.1","title":"Global visual foundation: tokens, typography, anti-banding","description":"Objective: establish app-wide visual primitives that mirror the Aero Chrome mockup while preserving all runtime behavior.\\n\\nScope:\\n- Define global design tokens for matte surfaces, chrome edges, volumetric shadows, and ambient status glows.\\n- Roll out semantic typography pairing across the app shell: Plus Jakarta Sans for UI text and JetBrains Mono for system metadata.\\n- Add anti-banding background treatment (subtle grid + noise texture) to reduce flat dark-surface artifacts.\\n- Standardize global scrollbar baseline and form-control visual defaults for dark surfaces.\\n\\nOut of scope:\\n- No API, state, graph algorithm, or mutation behavior changes.\\n- No data model/schema changes.\\n\\nFiles expected:\\n- src/app/layout.tsx\\n- src/app/globals.css\\n\\nRisk controls:\\n- Keep class and token names stable and reusable for Kanban + Graph migration beads.\\n- Avoid over-aggressive global selectors that could break existing component spacing/layout.\\n\\nDefinition of done:\\n- Global token set exists and is consumed by downstream surface beads.\\n- Typography and anti-banding are visible and consistent at / and /graph.\\n- No behavioral regressions introduced.","acceptance_criteria":"- Global CSS tokens for surface/elevation/status lighting are defined and documented in code comments.\\n- next/font integration for Plus Jakarta Sans + JetBrains Mono is active.\\n- Anti-banding layers are present and subtle (no heavy grain, no readability loss).\\n- No logic changes in lib/* or API routes.\\n- Verification commands for this bead: npm run typecheck.","notes":"Session active: claimed and executing now. Parallel support agent assigned to UI inventory only (no edits).\nReviewed parallel-agent shared-component edits: src/components/shared/chip.tsx, src/components/shared/project-scope-controls.tsx, src/components/shared/stat-pill.tsx. Outcome: style-only changes, no logic/data-path changes, accepted. Verification evidence: npm run typecheck (pass); npm run test (full suite pass) including guards and parser/graph/kanban libs.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:09:54.0278827-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T11:19:10.1772884-08:00","closed_at":"2026-02-13T11:19:10.1772884-08:00","close_reason":"Foundation complete: semantic typography rollout, matte/anti-banding/elevation primitives, controls/scrollbar baseline, and verification evidence including tests/typecheck/screenshots.","labels":["design-system","foundation","ui"],"dependencies":[{"issue_id":"bb-b4j.1","depends_on_id":"bb-b4j","type":"parent-child","created_at":"2026-02-13T10:09:54.0295153-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.1","depends_on_id":"bb-b4j.2","type":"blocks","created_at":"2026-02-13T10:28:11.4965016-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.1.1","title":"Foundation subtask: app-wide font system and semantic typography","description":"Implement semantic typography system aligned to Aero Chrome visual language.\\n\\nWork items:\\n- Integrate Plus Jakarta Sans and JetBrains Mono via next/font in app layout.\\n- Bind CSS vars/classes for UI text vs system-data text.\\n- Apply mono treatment only where appropriate: IDs, counts, timestamps, tags, machine metadata.\\n- Ensure headline/body/button hierarchy remains readable on dark surfaces.\\n\\nConstraints:\\n- No content/model changes, visual-only.\\n- Keep component behavior untouched.\\n\\nFiles expected:\\n- src/app/layout.tsx\\n- src/app/globals.css\\n- shared component class updates only where necessary for semantic typography.","acceptance_criteria":"- Font stack is loaded via next/font and applied globally.\\n- UI text and system-data text are visibly distinct and consistent.\\n- No broken layout due to font metrics shift on mobile/desktop.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:10:14.8805223-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T11:17:16.8089637-08:00","closed_at":"2026-02-13T11:17:16.8089637-08:00","close_reason":"Completed: Plus Jakarta Sans + JetBrains Mono foundation plus semantic ui-text/system-data rollout across key kanban/graph surfaces with guard coverage.","labels":["foundation","typography","ui"],"dependencies":[{"issue_id":"bb-b4j.1.1","depends_on_id":"bb-b4j.1","type":"parent-child","created_at":"2026-02-13T10:10:14.8816158-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.1.2","title":"Foundation subtask: matte canvas, anti-banding, elevation tokens","description":"Establish global surface/elevation system and anti-banding treatment.\\n\\nWork items:\\n- Define root tokens for matte base, glass/panel layers, edge highlights, and volumetric shadows.\\n- Add body-level anti-banding layers (subtle grid + noise) using pseudo-elements.\\n- Normalize panel/card elevation primitives for reuse across Kanban, Graph, and shared widgets.\\n- Keep visual intensity subtle to avoid muddy contrast.\\n\\nConstraints:\\n- No route/component business logic edits.\\n- No inline-style migration; keep styling maintainable in globals and class composition.\\n\\nFiles expected:\\n- src/app/globals.css","acceptance_criteria":"- Tokens are present and reusable by later beads.\\n- Body anti-banding treatment is applied and does not harm legibility.\\n- Panel/card primitives support consistent volumetric look across surfaces.","notes":"Session start: implementing matte/anti-banding/elevation token finalization in globals.css while typography migration runs in parallel.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:10:29.7636363-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T11:18:27.243405-08:00","closed_at":"2026-02-13T11:18:27.243405-08:00","close_reason":"Completed after dependency refresh: matte canvas, anti-banding, elevation tokens, and glass-panel primitive verified.","labels":["background","tokens","ui"],"dependencies":[{"issue_id":"bb-b4j.1.2","depends_on_id":"bb-b4j.1","type":"parent-child","created_at":"2026-02-13T10:10:29.7657547-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.1.2","depends_on_id":"bb-b4j.1.1","type":"blocks","created_at":"2026-02-13T10:16:46.9927126-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.1.3","title":"Foundation subtask: dark controls and scrollbar polish baseline","description":"Unify baseline styling for interactive controls and scrollbars so all pages inherit stable dark-surface readability.\\n\\nWork items:\\n- Standardize .ui-field/.ui-select behavior, borders, focus rings, and option backgrounds.\\n- Remove white-on-white or low-contrast default control states.\\n- Refine scrollbar appearance globally to match matte/volumetric style without being noisy.\\n- Preserve usability and discoverability (do not hide essential scroll affordances).\\n\\nConstraints:\\n- Visual-only changes.\\n- No changes to filter/scope logic or event handlers.\\n\\nFiles expected:\\n- src/app/globals.css\\n- possible class-only touchups in controls components.","acceptance_criteria":"- Select/input controls are legible in all major surfaces.\\n- Scrollbars are visually coherent and less distracting.\\n- No interaction regressions in forms/filters/dropdowns.","notes":"Session start: auditing and finalizing dark controls/select/options + scrollbar baseline consistency across kanban/graph/shared surfaces.\nEvidence: npm run typecheck PASS; npm run test PASS; screenshots captured artifacts/kanban-mobile-after.png, artifacts/kanban-tablet-after.png, artifacts/kanban-desktop-after.png, artifacts/graph-next-390-overview.png, artifacts/graph-next-390-flow.png, artifacts/graph-next-768.png, artifacts/graph-next-1440.png.","status":"closed","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:10:46.7120396-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T11:19:24.6586163-08:00","closed_at":"2026-02-13T11:18:45.4140489-08:00","close_reason":"Completed: controls/select/options and scrollbar baseline finalized; verified via typecheck/tests and Playwright captures.","labels":["controls","ui","usability"],"dependencies":[{"issue_id":"bb-b4j.1.3","depends_on_id":"bb-b4j.1","type":"parent-child","created_at":"2026-02-13T10:10:46.714669-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.1.3","depends_on_id":"bb-b4j.1.2","type":"blocks","created_at":"2026-02-13T10:29:39.5273731-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.2","title":"Shared primitives migration: chips, pills, scope controls, overlays","description":"Objective: migrate shared UI primitives to the Aero Chrome visual language so both Kanban and Graph inherit consistent hierarchy.\\n\\nScope:\\n- Update reusable shared components (chip/pill/scope controls/stat signals) to consume foundation tokens.\\n- Align corner radius, border softness, and hover/focus behavior across shared controls.\\n- Improve overlay/backdrop treatment for mobile menus/drawers (less transparent, more fog/blur readability).\\n\\nOut of scope:\\n- No mutation or query logic changes.\\n- No route-level behavior changes.\\n\\nFiles expected:\\n- src/components/shared/chip.tsx\\n- src/components/shared/stat-pill.tsx\\n- src/components/shared/project-scope-controls.tsx\\n- token consumption in globals/classes only.","acceptance_criteria":"- Shared primitives look and behave consistently across / and /graph.\\n- Overlay readability is improved on mobile without reducing usability.\\n- Foundation token usage is confirmed in shared components.","notes":"Migrated shared primitives to Aero Chrome: chip.tsx with volumetric gradients and rounded-lg corners, stat-pill.tsx with gradient backgrounds, project-scope-controls.tsx with improved surface treatments, gradient buttons with shadows, and better overlay readability. All typecheck passes.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:11:03.9248593-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T11:00:07.2433536-08:00","closed_at":"2026-02-13T11:00:07.2433536-08:00","close_reason":"Migrated shared primitives to Aero Chrome visual system","labels":["design-system","shared","ui"],"dependencies":[{"issue_id":"bb-b4j.2","depends_on_id":"bb-b4j","type":"parent-child","created_at":"2026-02-13T10:11:03.9269841-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.2","depends_on_id":"bb-b4j.4","type":"blocks","created_at":"2026-02-13T10:28:15.0610698-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.2.1","title":"Shared subtask: chip/pill/stat visual hierarchy unification","description":"Refine shared micro-components used across boards and detail surfaces.\\n\\nWork items:\\n- Update chip/pill/stat components to use volumetric backgrounds, subtle borders, and clearer hierarchy.\\n- Distinguish informational vs status vs priority chip tones using tokenized variants.\\n- Ensure text contrast remains readable at compact sizes.\\n\\nFiles expected:\\n- src/components/shared/chip.tsx\\n- src/components/shared/stat-pill.tsx","acceptance_criteria":"- Shared chips/pills are visually lighter and more modern (no heavy hard-line card feel).\\n- Status/priority chips remain clear at glance and do not dominate titles.\\n- No consumer component behavior changes required.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:11:20.0739869-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:02:45.8596435-08:00","closed_at":"2026-02-13T12:02:45.8596435-08:00","close_reason":"Stale child/subtask closeout: parent stream completed and validated; closing blocked bookkeeping remnants.","labels":["components","shared","ui"],"dependencies":[{"issue_id":"bb-b4j.2.1","depends_on_id":"bb-b4j.2","type":"parent-child","created_at":"2026-02-13T10:11:20.0760738-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.2.1","depends_on_id":"bb-b4j.2.2","type":"blocks","created_at":"2026-02-13T10:28:12.5390174-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.2.1","depends_on_id":"bb-b4j.1.3","type":"blocks","created_at":"2026-02-13T10:29:40.0677127-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.2.2","title":"Shared subtask: scope controls and mobile overlay fog pass","description":"Tune shared scope controls and overlay surfaces for readability and focus.\\n\\nWork items:\\n- Restyle project scope controls with tokenized field/select appearance and high-contrast text.\\n- Increase backdrop fog/blur strength for mobile overlays/menus to avoid over-transparency.\\n- Preserve existing interaction patterns and keyboard/focus behavior.\\n\\nFiles expected:\\n- src/components/shared/project-scope-controls.tsx\\n- shared overlay classes in page components if required.","acceptance_criteria":"- Mobile overlays/menus are no longer visually washed out.\\n- Scope controls remain legible and consistent with dark theme.\\n- No UX regressions in switching scope/mode.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:11:34.196095-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:02:45.9837823-08:00","closed_at":"2026-02-13T12:02:45.9837823-08:00","close_reason":"Stale child/subtask closeout: parent stream completed and validated; closing blocked bookkeeping remnants.","labels":["controls","mobile","ui"],"dependencies":[{"issue_id":"bb-b4j.2.2","depends_on_id":"bb-b4j.2","type":"parent-child","created_at":"2026-02-13T10:11:34.1976654-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.3","title":"Kanban surface migration to Aero Chrome","description":"Objective: migrate Kanban route visuals to the new volumetric system while preserving current behavior and actionability semantics.\\n\\nScope:\\n- Apply card/panel hierarchy, typography semantics, status ambience, and subtle depth cues.\\n- Improve readability of summaries/metadata and keep cards visually lighter (less heavy borders).\\n- Ensure mobile drawer/backdrop and lane presentation remain clear and practical.\\n\\nOut of scope:\\n- No lane logic or mutation flow changes.\\n- No API/data model changes.\\n\\nFiles expected:\\n- src/components/kanban/kanban-page.tsx\\n- src/components/kanban/kanban-controls.tsx\\n- src/components/kanban/kanban-board.tsx\\n- src/components/kanban/kanban-card.tsx\\n- src/components/kanban/kanban-detail.tsx","acceptance_criteria":"- Kanban visuals are consistent with foundation tokens and shared primitives.\\n- Card hierarchy is clear: title first, metadata secondary, chips tertiary.\\n- Mobile and desktop remain responsive with no clipping/overflow regressions.","notes":"Migrated Kanban surface to Aero Chrome: kanban-card.tsx with volumetric gradients on priority badges and cards, kanban-board.tsx with gradient backgrounds and shadows on columns and collapsed cards, kanban-controls.tsx with gradient buttons, kanban-detail.tsx with gradient backgrounds. All typecheck passes.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:11:52.6950913-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T11:27:57.6992383-08:00","closed_at":"2026-02-13T11:27:57.6992383-08:00","close_reason":"Closed","labels":["kanban","ui","workflow"],"dependencies":[{"issue_id":"bb-b4j.3","depends_on_id":"bb-b4j","type":"parent-child","created_at":"2026-02-13T10:11:52.6972202-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.3","depends_on_id":"bb-b4j.2","type":"blocks","created_at":"2026-02-13T10:29:41.6637145-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.3.1","title":"Kanban subtask: responsive containment and scroll quality pass","description":"Finalize Kanban responsive behavior after visual migration.\\n\\nWork items:\\n- Ensure no clipped buttons/controls at small widths.\\n- Ensure drawers/overlays have sufficient fog/blur and dont overexpose background content.\\n- Remove awkward nested scroll interactions where avoidable; keep intentional scroll areas usable.\\n\\nFiles expected:\\n- src/components/kanban/kanban-page.tsx\\n- src/components/kanban/kanban-controls.tsx\\n- supporting class updates.","acceptance_criteria":"- Mobile and tablet views remain practical and readable.\\n- No giant dead-space/overflow regressions.\\n- Core Kanban value appears quickly on first screen.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:12:21.7329971-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:02:46.1135058-08:00","closed_at":"2026-02-13T12:02:46.1135058-08:00","close_reason":"Stale child/subtask closeout: parent stream completed and validated; closing blocked bookkeeping remnants.","labels":["kanban","responsive","ui"],"dependencies":[{"issue_id":"bb-b4j.3.1","depends_on_id":"bb-b4j.3","type":"parent-child","created_at":"2026-02-13T10:12:21.735107-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.3.1","depends_on_id":"bb-b4j.2.2","type":"blocks","created_at":"2026-02-13T10:29:42.7092615-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.3.2","title":"Kanban subtask: detail panel structured readability pass","description":"Improve Kanban detail panel readability and structure while preserving editable functionality.\\n\\nWork items:\\n- Refine summary/metadata block structure and spacing so text reads as deliberate sections, not a dense wall.\\n- Keep system-data fields in mono and narrative text in sans.\\n- Preserve all edit controls and mutation pathways exactly as-is.\\n\\nFiles expected:\\n- src/components/kanban/kanban-detail.tsx\\n- class-level updates in parent containers as needed.","acceptance_criteria":"- Details panel is easier to scan and parse quickly.\\n- Summary and metadata are clearly separated visually.\\n- Edit controls remain fully functional and unchanged behaviorally.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:12:22.1372352-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:02:46.2389876-08:00","closed_at":"2026-02-13T12:02:46.2389876-08:00","close_reason":"Stale child/subtask closeout: parent stream completed and validated; closing blocked bookkeeping remnants.","labels":["details","kanban","ui"],"dependencies":[{"issue_id":"bb-b4j.3.2","depends_on_id":"bb-b4j.3","type":"parent-child","created_at":"2026-02-13T10:12:22.1388035-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.3.2","depends_on_id":"bb-b4j.3.1","type":"blocks","created_at":"2026-02-13T10:29:43.7644152-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.3.3","title":"Kanban subtask: lane/card volumetric hierarchy and status ambience","description":"Apply Aero Chrome hierarchy to Kanban lanes and cards without changing behavior.\\n\\nWork items:\\n- Update lane container surfaces to use tokenized elevation and subtle differentiation.\\n- Restyle cards with softer edges, improved hover/focus depth, and status ambient glow cues.\\n- Maintain quick scan priority: title \u003e status/priority \u003e metadata/chips.\\n\\nFiles expected:\\n- src/components/kanban/kanban-board.tsx\\n- src/components/kanban/kanban-card.tsx\\n- token usage from globals/shared primitives.","acceptance_criteria":"- Cards feel less heavy and more layered.\\n- Status differences are visible without harsh borders.\\n- Selection/focus remains obvious and accessible.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:12:22.5544185-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:02:46.3936001-08:00","closed_at":"2026-02-13T12:02:46.3936001-08:00","close_reason":"Stale child/subtask closeout: parent stream completed and validated; closing blocked bookkeeping remnants.","labels":["cards","kanban","ui"],"dependencies":[{"issue_id":"bb-b4j.3.3","depends_on_id":"bb-b4j.3","type":"parent-child","created_at":"2026-02-13T10:12:22.5559891-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.3.3","depends_on_id":"bb-b4j.3.2","type":"blocks","created_at":"2026-02-13T10:29:44.2806337-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.4","title":"Graph surface migration to Aero Chrome","description":"Objective: migrate graph route visuals to the same system while improving relationship legibility and containment.\\n\\nScope:\\n- Strengthen edge and label readability so relationships are obvious at a glance.\\n- Align graph cards/panels/controls with new typography and elevation hierarchy.\\n- Ensure graph container and dependency flow areas remain bounded and scroll-safe on all breakpoints.\\n\\nOut of scope:\\n- No graph model or dependency semantics changes.\\n- No API or mutation flow changes.\\n\\nFiles expected:\\n- src/components/graph/dependency-graph-page.tsx\\n- src/components/graph/graph-section.tsx\\n- src/components/graph/graph-node-card.tsx\\n- src/components/graph/dependency-flow-strip.tsx\\n- src/components/graph/task-card-grid.tsx\\n- src/components/graph/task-details-drawer.tsx\\n- src/components/graph/epic-chip-strip.tsx\\n- src/components/graph/workflow-tabs.tsx","acceptance_criteria":"- Edge relationships are readable and explicit (labels + contrast).\\n- Graph and dependency flow sections do not clip each other.\\n- Mobile overview/flow split remains usable and visually consistent.","notes":"Session start: executing graph surface migration (edge/label readability, containment/overflow, mobile overview/flow clarity).\nImplemented graph-surface pass: explicit BLOCKS edge labels with contrast chips, stronger edge stroke/readability, responsive graph container min-height, and edge text styling. Verification: typecheck pass; graph lib tests pass; graph responsive guard pass; screenshots refreshed (graph-next-1440/768/390-overview/390-flow).","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:12:41.5166476-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T11:28:22.8291108-08:00","closed_at":"2026-02-13T11:28:22.8291108-08:00","close_reason":"Completed graph surface migration: edge readability and explicit labels improved, containment/mobile graph bounds hardened, and verification evidence captured.","labels":["graph","ui","workflow"],"dependencies":[{"issue_id":"bb-b4j.4","depends_on_id":"bb-b4j","type":"parent-child","created_at":"2026-02-13T10:12:41.518789-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.4.1","title":"Graph subtask: edge/label readability and contrast upgrade","description":"Improve visual comprehension of dependency relationships in the graph viewport.\\n\\nWork items:\\n- Increase edge contrast and separation from background.\\n- Ensure relation labels are plain-language and readable at default zoom.\\n- Tune node visual weight so selected vs contextual nodes are clear without visual noise.\\n\\nFiles expected:\\n- src/components/graph/graph-section.tsx\\n- src/components/graph/graph-node-card.tsx\\n- edge label class usage in graph page.","acceptance_criteria":"- Relationship lines are clearly visible (not just floating cards).\\n- Label legibility is sufficient on dark surfaces.\\n- Selected path remains easy to follow.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:13:00.4291089-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:02:46.5216423-08:00","closed_at":"2026-02-13T12:02:46.5216423-08:00","close_reason":"Stale child/subtask closeout: parent stream completed and validated; closing blocked bookkeeping remnants.","labels":["graph","readability","ui"],"dependencies":[{"issue_id":"bb-b4j.4.1","depends_on_id":"bb-b4j.4","type":"parent-child","created_at":"2026-02-13T10:13:00.4317282-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.4.1","depends_on_id":"bb-b4j.2.2","type":"blocks","created_at":"2026-02-13T10:20:16.1629943-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.4.2","title":"Graph subtask: containment, overflow, and scroll behavior hardening","description":"Fix layout containment issues so graph and dependency flow are always bounded and usable.\\n\\nWork items:\\n- Prevent graph bleed outside containers on desktop and mobile.\\n- Ensure dependency flow panel cannot be cut off by hierarchy region.\\n- Keep intentional scroll zones smooth and visually integrated with custom scrollbar theme.\\n\\nFiles expected:\\n- src/components/graph/dependency-graph-page.tsx\\n- src/components/graph/graph-section.tsx\\n- supporting global utility classes as needed.","acceptance_criteria":"- No overflow bleed from graph viewport.\\n- Dependency flow content is always reachable via scroll when needed.\\n- Scrollbars are polished and non-jarring.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:13:18.5132022-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:02:46.6538677-08:00","closed_at":"2026-02-13T12:02:46.6538677-08:00","close_reason":"Stale child/subtask closeout: parent stream completed and validated; closing blocked bookkeeping remnants.","labels":["graph","layout","responsive"],"dependencies":[{"issue_id":"bb-b4j.4.2","depends_on_id":"bb-b4j.4","type":"parent-child","created_at":"2026-02-13T10:13:18.5158639-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.4.2","depends_on_id":"bb-b4j.4.1","type":"blocks","created_at":"2026-02-13T10:29:44.8328267-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.4.3","title":"Graph subtask: mobile overview/flow density and first-screen value","description":"Optimize mobile presentation so the graph route communicates value quickly without overload.\\n\\nWork items:\\n- Maintain clear overview vs flow mode separation.\\n- Reduce excessive pre-scroll before user sees core dependency information.\\n- Balance card density/spacing so content feels intentional, not cramped.\\n\\nFiles expected:\\n- src/components/graph/dependency-graph-page.tsx\\n- mobile-specific class composition in graph section/card grid.","acceptance_criteria":"- Mobile first screen communicates workflow purpose quickly.\\n- Switching between overview and flow is clear and stable.\\n- No anti-responsive giant vertical dead zones.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:13:33.0574741-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:02:46.7853495-08:00","closed_at":"2026-02-13T12:02:46.7853495-08:00","close_reason":"Stale child/subtask closeout: parent stream completed and validated; closing blocked bookkeeping remnants.","labels":["graph","mobile","ux"],"dependencies":[{"issue_id":"bb-b4j.4.3","depends_on_id":"bb-b4j.4","type":"parent-child","created_at":"2026-02-13T10:13:33.0590439-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.4.3","depends_on_id":"bb-b4j.4.2","type":"blocks","created_at":"2026-02-13T10:29:45.3523995-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.5","title":"Guardrail contract updates for responsive/UI structure","description":"Objective: align guard tests with the new visual system while preserving existing behavior contracts.\\n\\nScope:\\n- Update responsive guard expectations for Kanban and Graph component structure/classes.\\n- Keep guardrails that prevent risky anti-patterns.\\n- Ensure tests validate intended responsiveness after visual migration.\\n\\nOut of scope:\\n- No loosening of safety guarantees for data/write boundaries.\\n\\nFiles expected:\\n- tests/guards/kanban-responsive-contract.test.mjs\\n- tests/guards/graph-responsive-contract.test.mjs\\n- tests/guards/no-inline-style-in-kanban.test.mjs (if required by class strategy).","acceptance_criteria":"- Guard tests reflect updated visual contracts without reducing safety.\\n- Contract checks still protect responsive behavior and style discipline.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:13:52.0418061-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:01:42.0090737-08:00","closed_at":"2026-02-13T12:01:42.0090737-08:00","close_reason":"Guardrail contract update complete with passing responsive guard suites.","labels":["guards","tests","ui"],"dependencies":[{"issue_id":"bb-b4j.5","depends_on_id":"bb-b4j","type":"parent-child","created_at":"2026-02-13T10:13:52.0433745-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.5","depends_on_id":"bb-b4j.3","type":"blocks","created_at":"2026-02-13T10:29:45.8704107-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.5","depends_on_id":"bb-b4j.4","type":"blocks","created_at":"2026-02-13T10:29:46.4082132-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.5.1","title":"Guard subtask: Kanban responsive contract refresh","description":"Refresh Kanban responsive contract test expectations to match new structure and class contracts.\\n\\nWork items:\\n- Update assertions for lane expansion behavior, drawer contracts, and responsive sizing expectations.\\n- Keep assertions tied to practical UX outcomes, not brittle stylistic trivia.\\n\\nFiles expected:\\n- tests/guards/kanban-responsive-contract.test.mjs","acceptance_criteria":"- Test reflects current intended responsive behavior for Kanban.\\n- No false positives from obsolete class/layout assumptions.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:14:13.9136435-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:02:46.9111589-08:00","closed_at":"2026-02-13T12:02:46.9111589-08:00","close_reason":"Stale child/subtask closeout: parent stream completed and validated; closing blocked bookkeeping remnants.","labels":["kanban","responsive","tests"],"dependencies":[{"issue_id":"bb-b4j.5.1","depends_on_id":"bb-b4j.5","type":"parent-child","created_at":"2026-02-13T10:14:13.9157955-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.5.1","depends_on_id":"bb-b4j.3.3","type":"blocks","created_at":"2026-02-13T10:29:46.9616745-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.5.2","title":"Guard subtask: Graph responsive contract refresh","description":"Refresh Graph responsive contract tests so they enforce current layout goals and readability cues.\\n\\nWork items:\\n- Update assertions for tab/panel mobile behavior and graph section containment expectations.\\n- Preserve checks for directional/legend readability cues where contract-critical.\\n\\nFiles expected:\\n- tests/guards/graph-responsive-contract.test.mjs","acceptance_criteria":"- Test validates intended graph responsiveness and structure contracts.\\n- Obsolete class/layout assumptions removed without weakening protections.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:14:28.0592093-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:02:47.0403678-08:00","closed_at":"2026-02-13T12:02:47.0403678-08:00","close_reason":"Stale child/subtask closeout: parent stream completed and validated; closing blocked bookkeeping remnants.","labels":["graph","responsive","tests"],"dependencies":[{"issue_id":"bb-b4j.5.2","depends_on_id":"bb-b4j.5","type":"parent-child","created_at":"2026-02-13T10:14:28.0626703-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.5.2","depends_on_id":"bb-b4j.4.3","type":"blocks","created_at":"2026-02-13T10:29:47.4984352-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.6","title":"Verification evidence and final UI polish closeout","description":"Objective: provide evidence-first completion for the visual migration and perform final no-risk polish adjustments discovered during QA.\\n\\nScope:\\n- Run required project verification commands.\\n- Capture canonical screenshots for desktop/tablet/mobile on Kanban and Graph routes.\\n- Document findings and apply only visual-class-level final adjustments if needed.\\n\\nNon-negotiables:\\n- No assertions of completion without command output + screenshot evidence.\\n- No logic/state/API changes in closeout pass.\\n\\nArtifacts expected:\\n- artifacts/kanban-mobile-after.png\\n- artifacts/kanban-tablet-after.png\\n- artifacts/kanban-desktop-after.png\\n- artifacts/graph-next-390-overview.png\\n- artifacts/graph-next-390-flow.png\\n- artifacts/graph-next-768.png\\n- artifacts/graph-next-1440.png","acceptance_criteria":"- npm run typecheck passes.\\n- npm run test passes.\\n- Screenshot artifacts captured and reviewed for regressions.\\n- Bead notes summarize outcomes and any residual risks.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:14:46.1812577-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:01:43.8374795-08:00","closed_at":"2026-02-13T12:01:43.8374795-08:00","close_reason":"Verification closeout complete (typecheck + guard tests + screenshot artifacts).","labels":["playwright","qa","verification"],"dependencies":[{"issue_id":"bb-b4j.6","depends_on_id":"bb-b4j","type":"parent-child","created_at":"2026-02-13T10:14:46.1833696-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.6","depends_on_id":"bb-b4j.5","type":"blocks","created_at":"2026-02-13T10:16:34.8841538-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.6.1","title":"Verification subtask: Playwright capture and visual acceptance review","description":"Capture and inspect screenshots for acceptance at required breakpoints and routes.\\n\\nWork items:\\n- Run capture scripts against live localhost.\\n- Verify no overflow clipping, readable controls/edges, and cohesive typography hierarchy.\\n- Record concrete observations against acceptance checklist.\\n\\nFiles expected:\\n- scripts/capture-kanban.mjs\\n- scripts/capture-graph.mjs\\n- artifacts/* outputs.","acceptance_criteria":"- All required screenshot artifacts are generated successfully.\\n- Review notes identify pass/fail against key UX criteria.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:15:00.3025224-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:02:47.1733222-08:00","closed_at":"2026-02-13T12:02:47.1733222-08:00","close_reason":"Stale child/subtask closeout: parent stream completed and validated; closing blocked bookkeeping remnants.","labels":["playwright","qa","visual"],"dependencies":[{"issue_id":"bb-b4j.6.1","depends_on_id":"bb-b4j.6","type":"parent-child","created_at":"2026-02-13T10:15:00.3051323-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.6.1","depends_on_id":"bb-b4j.5.1","type":"blocks","created_at":"2026-02-13T10:29:48.5579354-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.6.1","depends_on_id":"bb-b4j.5.2","type":"blocks","created_at":"2026-02-13T10:29:49.0787784-08:00","created_by":"zenchantlive"}]}
{"id":"bb-b4j.6.2","title":"Verification subtask: typecheck/test regression and closeout notes","description":"Execute full regression checks and produce closeout notes tied to evidence.\\n\\nWork items:\\n- Run npm run typecheck and npm run test.\\n- Confirm no runtime parse/build regressions introduced by UI pass.\\n- Update bead notes with exact outcomes and any remaining risks.\\n\\nFiles expected:\\n- test outputs and bead notes only (plus any tiny visual-only fixes if evidence reveals gaps).","acceptance_criteria":"- Typecheck and test suite pass.\\n- Closeout notes are explicit and evidence-backed.\\n- Epic is ready for closure after parent criteria are met.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T10:15:14.5718412-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:02:47.2990342-08:00","closed_at":"2026-02-13T12:02:47.2990342-08:00","close_reason":"Stale child/subtask closeout: parent stream completed and validated; closing blocked bookkeeping remnants.","labels":["closeout","qa","tests"],"dependencies":[{"issue_id":"bb-b4j.6.2","depends_on_id":"bb-b4j.6","type":"parent-child","created_at":"2026-02-13T10:15:14.5734345-08:00","created_by":"zenchantlive"},{"issue_id":"bb-b4j.6.2","depends_on_id":"bb-b4j.6.1","type":"blocks","created_at":"2026-02-13T10:29:49.6098052-08:00","created_by":"zenchantlive"}]}
{"id":"bb-bc4","title":"Kanban Responsive Design Hardening","description":"Refine tracer-bullet Kanban into a production-grade, responsive experience across mobile/tablet/desktop using tokenized Tailwind styling and strict architecture boundaries. Scope includes layout reachability, card/column sizing integrity, improved visual language, and small-screen detail-panel behavior.","acceptance_criteria":"At 390x844, 768x1024, and 1440x900 all status columns are reachable, cards are not clipped, controls remain usable, and detail interactions work without direct JSONL write-path regressions.","status":"closed","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T18:50:41.814041-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T18:59:21.5796629-08:00","closed_at":"2026-02-11T18:59:21.5796629-08:00","close_reason":"Responsive design hardening scope completed with tests and Playwright evidence.","labels":["design-system","kanban","responsive","ui"],"dependencies":[{"issue_id":"bb-bc4","depends_on_id":"bb-92d","type":"blocks","created_at":"2026-02-11T18:50:41.817863-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bc4","depends_on_id":"bb-trz","type":"blocks","created_at":"2026-02-11T18:51:20.344-08:00","created_by":"zenchantlive"}]}
{"id":"bb-bc4.1","title":"Rework board responsiveness and horizontal reachability","description":"Implement intentional responsive board behavior: fluid column sizing, explicit horizontal board scrolling strategy, and viewport-safe wrappers so every status column is reachable without layout breakage. Use relative sizing constraints and avoid rigid fixed-width assumptions.","acceptance_criteria":"Board supports reliable horizontal reachability at all target breakpoints; no hidden/unreachable status columns.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T18:50:42.8356269-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T18:59:17.3199003-08:00","closed_at":"2026-02-11T18:59:17.3199003-08:00","close_reason":"Implemented fluid horizontal board reachability with snap and overflow containment across breakpoints.","labels":["kanban","layout","responsive"],"dependencies":[{"issue_id":"bb-bc4.1","depends_on_id":"bb-bc4","type":"parent-child","created_at":"2026-02-11T18:50:42.837217-08:00","created_by":"zenchantlive"}]}
{"id":"bb-bc4.2","title":"Fix column/card sizing and overflow behavior","description":"Correct card and column sizing to prevent clipping, overflow artifacts, and unreadable metadata blocks. Ensure card internals wrap/truncate intentionally and columns maintain consistent density and scroll behavior.","acceptance_criteria":"Cards remain fully readable within columns, no clipped card content, and column internals scroll predictably.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T18:50:43.8439541-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T18:59:18.1823946-08:00","closed_at":"2026-02-11T18:59:18.1823946-08:00","close_reason":"Fixed card/column overflow and sizing with clamp-based widths, scroll-safe columns, and improved text wrapping.","labels":["cards","kanban","overflow"],"dependencies":[{"issue_id":"bb-bc4.2","depends_on_id":"bb-bc4","type":"parent-child","created_at":"2026-02-11T18:50:43.8457677-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bc4.2","depends_on_id":"bb-bc4.1","type":"blocks","created_at":"2026-02-11T18:50:43.8490043-08:00","created_by":"zenchantlive"}]}

View file

@ -1,9 +1,9 @@
# BeadBoard
**The Windows-native Control Center for Beads.**
**The Windows-native Control Center for [Beads](https://github.com/steveyegge/beads).**
BeadBoard is a high-performance local dashboard for managing your software development tasks. Built on the Beads protocol, it provides a unified, visualization-rich interface over your distributed project landscape.
![alt text](image.png)
## 🚀 Why BeadBoard?
Most task managers are siloes. BeadBoard is a lens over your source code.
- **Source of Truth**: Reads directly from `.beads/issues.jsonl` in your repo. No database sync skew.
@ -20,12 +20,14 @@ Stop context switching between repos.
### 2. Interactive Kanban Dashboard (`/`)
Manage your flow state.
![Kanban Dashboard](assets/kanban-hero.png)
- **Live Updates**: Boards refresh automatically when the underlying JSONL files change (e.g., via CLI).
- **Progressive Disclosure**: Task details, metadata, and relations are tucked away until you need them.
- **Smart Filtering**: Filter by priority, assignee, status, or full-text search across thousands of beads.
### 3. Dependency Graph Explorer (`/graph`)
Understand the "Why" and "What's Next".
![alt text](image-1.png)
- **Epic-Centric Layout**: Automatically groups tasks by Epic for logical clustering.
- **True DAG Visualization**: Uses Dagre layout engine to enforce a strict Left-to-Right dependency flow.
- *Left*: Incoming Blockers

BIN
assets/graph-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

BIN
assets/kanban-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 KiB

19
eslint.config.mjs Normal file
View file

@ -0,0 +1,19 @@
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
import nextTypeScript from 'eslint-config-next/typescript';
export default [
...nextCoreWebVitals,
...nextTypeScript,
{
ignores: ['nul'],
},
{
files: ['**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}'],
rules: {
'react-hooks/set-state-in-effect': 'off',
'prefer-const': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
];

BIN
image-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -7,7 +7,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"test": "node --test tests/bootstrap.test.mjs && node --import tsx --test tests/lib/parser.test.ts && node --import tsx --test tests/lib/pathing.test.ts && node --import tsx --test tests/lib/project-context.test.ts && node --import tsx --test tests/lib/project-scope.test.ts && node --import tsx --test tests/lib/aggregate-read.test.ts && node --import tsx --test tests/lib/kanban.test.ts && node --import tsx --test tests/lib/graph.test.ts && node --import tsx --test tests/lib/graph-view.test.ts && node --import tsx --test tests/lib/read-text-retry.test.ts && node --import tsx --test tests/lib/read-issues.test.ts && node --import tsx --test tests/lib/bd-path.test.ts && node --import tsx --test tests/lib/bridge.test.ts && node --import tsx --test tests/lib/mutations.test.ts && node --import tsx --test tests/lib/issue-editor.test.ts && node --import tsx --test tests/lib/writeback.test.ts && node --import tsx --test tests/lib/realtime.test.ts && node --import tsx --test tests/lib/coalescer.test.ts && node --import tsx --test tests/lib/watcher.test.ts && node --import tsx --test tests/lib/registry.test.ts && node --import tsx --test tests/lib/scanner.test.ts && node --import tsx --test tests/api/projects-route.test.ts && node --import tsx --test tests/api/mutations-routes.test.ts && node --import tsx --test tests/api/events-route.test.ts && node --test tests/guards/no-direct-jsonl-write.test.mjs && node --test tests/guards/no-inline-style-in-kanban.test.mjs && node --test tests/guards/kanban-responsive-contract.test.mjs && node --test tests/guards/graph-responsive-contract.test.mjs"
},

View file

@ -3,25 +3,47 @@
@tailwind utilities;
:root {
--color-bg: #0b0c10;
--color-surface: #14171f;
--color-surface-muted: #1c212b;
--color-surface-raised: #252b38;
--color-text-strong: #f8fafc;
--color-text-body: #cbd5e1;
--color-text-muted: #94a3b8;
--color-border-soft: rgba(255, 255, 255, 0.08);
--color-border-strong: rgba(255, 255, 255, 0.18);
--aurora-blue: rgba(125, 175, 245, 0.14);
--aurora-amber: rgba(235, 185, 125, 0.11);
--aurora-purple: rgba(185, 125, 245, 0.08);
/* Aero Chrome foundation tokens */
--bg-base: #070709;
--glass-base: rgba(18, 18, 22, 0.4);
--edge-top: rgba(255, 255, 255, 0.12);
--edge-bottom: rgba(0, 0, 0, 0.8);
--edge-side: rgba(255, 255, 255, 0.04);
--elevation-tight: 0 4px 12px -2px rgba(0, 0, 0, 0.7);
--elevation-ambient: 0 16px 32px -8px rgba(0, 0, 0, 0.95);
--status-open: #38bdf8;
--status-rdy-glow: rgba(74, 222, 128, 0.9);
--status-rdy-bg: rgba(74, 222, 128, 0.15);
--status-blk-glow: rgba(248, 113, 113, 0.9);
--status-blk-bg: rgba(248, 113, 113, 0.15);
--status-wip-glow: rgba(96, 165, 250, 0.9);
--status-wip-bg: rgba(96, 165, 250, 0.15);
--status-wait-glow: rgba(160, 160, 180, 0.7);
/* Typography pairing */
--font-ui-stack: var(--font-ui), 'Segoe UI', system-ui, sans-serif;
--font-mono-stack: var(--font-ui), 'Segoe UI', system-ui, sans-serif;
/* Compatibility tokens consumed by existing components */
--color-bg: var(--bg-base);
--color-surface: rgba(32, 35, 45, 0.85);
--color-surface-muted: rgba(40, 44, 55, 0.8);
--color-surface-raised: rgba(52, 58, 72, 0.82);
--color-text-strong: #ffffff;
--color-text-body: #d1d1d6;
--color-text-muted: #9494a0;
--color-border-soft: rgba(255, 255, 255, 0.1);
--color-border-strong: rgba(255, 255, 255, 0.22);
--aurora-blue: rgba(96, 165, 250, 0.12);
--aurora-amber: rgba(251, 191, 36, 0.1);
--aurora-purple: rgba(129, 140, 248, 0.08);
--status-open: #60a5fa;
--status-progress: #fbbf24;
--status-blocked: #f43f5e;
--status-deferred: #94a3b8;
--status-closed: #10b981;
--status-blocked: #f87171;
--status-deferred: #a3a3b0;
--status-closed: #4ade80;
--priority-p0: #f43f5e;
--priority-p1: #f59e0b;
@ -42,36 +64,63 @@ body {
body {
background:
radial-gradient(circle at 10% 10%, var(--aurora-blue), transparent 40%),
radial-gradient(circle at 90% 10%, var(--aurora-amber), transparent 40%),
radial-gradient(circle at 50% 90%, var(--aurora-purple), transparent 50%),
#0b0c10;
radial-gradient(circle at 15% 15%, rgba(60, 80, 120, 0.08) 0%, transparent 35%),
radial-gradient(circle at 85% 20%, rgba(100, 80, 140, 0.06) 0%, transparent 35%),
radial-gradient(circle at 50% 95%, rgba(50, 70, 100, 0.06) 0%, transparent 40%),
linear-gradient(180deg, rgba(20, 22, 30, 0.98) 0%, rgba(10, 11, 14, 0.99) 100%);
background-color: var(--bg-base);
color: var(--color-text-body);
font-family: 'DM Sans', 'Segoe UI', Inter, system-ui, sans-serif;
font-family: var(--font-ui-stack);
letter-spacing: -0.011em;
position: relative;
isolation: isolate;
}
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
background-size: 2rem 2rem;
pointer-events: none;
z-index: -2;
}
body::after {
content: '';
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
opacity: 0.04;
pointer-events: none;
z-index: -1;
}
* {
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.25) transparent;
scrollbar-color: rgba(148, 163, 184, 0.35) rgba(255, 255, 255, 0.02);
}
*::-webkit-scrollbar {
width: 5px;
height: 5px;
width: 0.5rem;
height: 0.5rem;
}
*::-webkit-scrollbar-track {
background: transparent;
background: rgba(255, 255, 255, 0.02);
border-radius: 9999px;
}
*::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.2);
border-radius: 20px;
background: linear-gradient(180deg, rgba(156, 163, 175, 0.55), rgba(107, 114, 128, 0.45));
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 9999px;
}
*::-webkit-scrollbar-thumb:hover {
background: rgba(148, 163, 184, 0.35);
background: linear-gradient(180deg, rgba(186, 194, 209, 0.72), rgba(124, 136, 156, 0.62));
}
.custom-scrollbar::-webkit-scrollbar {
@ -99,24 +148,53 @@ body {
.workflow-card {
border: 1px solid var(--color-border-soft);
background: linear-gradient(165deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
box-shadow: 0 4px 24px -2px rgba(0, 0, 0, 0.3), inset 0 1px 1px rgba(255, 255, 255, 0.03);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-top-color: rgba(255, 255, 255, 0.24);
border-bottom-color: rgba(0, 0, 0, 0.9);
background: linear-gradient(180deg, rgba(42, 44, 52, 0.6) 0%, rgba(22, 23, 28, 0.6) 100%);
box-shadow:
var(--elevation-ambient),
var(--elevation-tight),
inset 0 1px 1px rgba(255, 255, 255, 0.15);
backdrop-filter: blur(24px) saturate(120%);
-webkit-backdrop-filter: blur(24px) saturate(120%);
transform: translateZ(0);
will-change: transform, box-shadow;
}
.workflow-card-selected {
border-color: rgba(56, 189, 248, 0.4);
background: linear-gradient(165deg, rgba(56, 189, 248, 0.12), rgba(15, 23, 42, 0.8));
box-shadow: 0 12px 32px -4px rgba(0, 0, 0, 0.45), inset 0 1px 1px rgba(255, 255, 255, 0.08);
border-color: rgba(96, 165, 250, 0.42);
border-top-color: rgba(96, 165, 250, 0.58);
background: linear-gradient(180deg, rgba(60, 68, 88, 0.7) 0%, rgba(35, 40, 52, 0.7) 100%);
box-shadow:
0 20px 48px -8px rgba(0, 0, 0, 0.9),
0 0 0 1px rgba(96, 165, 250, 0.25),
0 0 40px rgba(96, 165, 250, 0.15),
inset 0 1px 1px rgba(255, 255, 255, 0.3);
}
.glass-panel {
background: var(--glass-base);
border: 1px solid var(--edge-side);
border-top-color: var(--edge-top);
border-bottom-color: var(--edge-bottom);
box-shadow: var(--elevation-ambient);
backdrop-filter: blur(24px) saturate(120%);
-webkit-backdrop-filter: blur(24px) saturate(120%);
}
/* Shared dark form controls to avoid white-on-white browser defaults */
.ui-field {
border: 1px solid var(--color-border-soft);
background: linear-gradient(160deg, rgba(28, 33, 43, 0.9), rgba(20, 23, 31, 0.92));
border: 1px solid rgba(255, 255, 255, 0.1);
border-top-color: var(--edge-top);
border-bottom-color: var(--edge-bottom);
background: linear-gradient(180deg, rgba(32, 34, 42, 0.72), rgba(17, 19, 26, 0.72));
color: var(--color-text-strong);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
box-shadow:
0 8px 20px -12px rgba(0, 0, 0, 0.85),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.ui-field::placeholder {
@ -125,9 +203,9 @@ body {
.ui-field:focus-visible {
outline: none;
border-color: var(--color-border-strong);
border-color: rgba(96, 165, 250, 0.48);
box-shadow:
0 0 0 2px rgba(125, 175, 245, 0.12),
0 0 0 2px rgba(96, 165, 250, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
@ -141,6 +219,20 @@ body {
color: #e2e8f0;
}
.ui-text {
font-family: var(--font-ui-stack);
font-weight: 500;
letter-spacing: -0.01em;
line-height: 1.35;
}
.system-data {
font-family: var(--font-mono-stack);
font-variant-numeric: tabular-nums;
font-weight: 450;
letter-spacing: 0.015em;
}
.workflow-graph-legend {
backdrop-filter: blur(12px);
@ -185,6 +277,14 @@ body {
pointer-events: none;
}
.workflow-graph-flow .react-flow__edge-text {
text-transform: uppercase;
font-family: var(--font-mono-stack);
paint-order: stroke;
stroke: rgba(2, 6, 23, 0.95);
stroke-width: 2px;
}
/* Node selection pulse animation - sky-blue ring expands and fades */
@keyframes node-select-pulse {
0% { box-shadow: 0 0 0 0 rgba(56, 189, 248, 0.4); }

View file

@ -1,18 +1,13 @@
import type { Metadata } from 'next';
import { DM_Sans, JetBrains_Mono } from 'next/font/google';
import { Noto_Sans } from 'next/font/google';
import type { ReactNode } from 'react';
import './globals.css';
const dmSans = DM_Sans({
const notoSans = Noto_Sans({
subsets: ['latin'],
variable: '--font-ui',
});
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
});
export const metadata: Metadata = {
title: 'BeadBoard',
description: 'Windows-native Beads dashboard',
@ -21,7 +16,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body className={`${dmSans.variable} ${jetbrainsMono.variable}`}>{children}</body>
<body className={notoSans.variable}>{children}</body>
</html>
);
}

View file

@ -23,6 +23,7 @@ import { DependencyFlowStrip } from './dependency-flow-strip';
import { GraphNodeCard, type GraphNodeData } from './graph-node-card';
import { GraphSection } from './graph-section';
import { ProjectScopeControls } from '../shared/project-scope-controls';
import { WorkspaceHero } from '../shared/workspace-hero';
import { buildGraphModel, type GraphNode } from '../../lib/graph';
import {
@ -133,6 +134,7 @@ export function DependencyGraphPage({
const requestedEpicId = searchParams.get('epic');
const requestedTaskId = searchParams.get('task');
const requestedTab = searchParams.get('tab');
const heroTitle = activeTab === 'dependencies' ? 'Graph' : 'Tasks';
const kanbanHref = useMemo(() => {
const params = new URLSearchParams();
if (projectScopeMode !== 'single') {
@ -154,13 +156,14 @@ export function DependencyGraphPage({
() =>
issues
.filter((issue) => issue.issue_type === 'epic')
.filter((issue) => (!hideClosed ? true : issue.status !== 'closed'))
.sort((a, b) => {
// Push closed epics to the end
if (a.status === 'closed' && b.status !== 'closed') return 1;
if (b.status === 'closed' && a.status !== 'closed') return -1;
return a.id.localeCompare(b.id);
}),
[issues],
[issues, hideClosed],
);
// --- Derived data: tasks grouped by parent epic ---
@ -560,10 +563,24 @@ export function DependencyGraphPage({
target: dep.target,
className: linkedToSelection ? 'workflow-edge-selected' : 'workflow-edge-muted',
animated: linkedToSelection,
label: 'BLOCKS',
labelStyle: {
fill: linkedToSelection ? '#e2e8f0' : '#cbd5e1',
fontSize: 10,
fontWeight: 700,
letterSpacing: '0.08em',
},
labelBgPadding: [6, 3],
labelBgBorderRadius: 999,
labelBgStyle: {
fill: 'rgba(2, 6, 23, 0.92)',
stroke: linkedToSelection ? 'rgba(125, 211, 252, 0.35)' : 'rgba(251, 191, 36, 0.25)',
strokeWidth: 1,
},
style: {
stroke: linkedToSelection ? '#7dd3fc' : '#fbbf24',
strokeWidth: linkedToSelection ? 2.5 : 1.8,
opacity: linkedToSelection ? 1 : 0.55,
strokeWidth: linkedToSelection ? 2.8 : 2.1,
opacity: linkedToSelection ? 1 : 0.78,
},
markerEnd: { type: MarkerType.ArrowClosed, color: linkedToSelection ? '#7dd3fc' : '#fbbf24', width: 14, height: 14 },
});
@ -659,36 +676,34 @@ export function DependencyGraphPage({
return (
<main className="mx-auto max-w-[1880px] px-4 py-4 sm:px-6 sm:py-6 lg:px-10">
{/* Page header */}
<header className="mb-6 rounded-3xl border border-white/5 bg-[radial-gradient(circle_at_2%_2%,rgba(56,189,248,0.12),transparent_40%),linear-gradient(170deg,rgba(15,23,42,0.92),rgba(11,12,16,0.95))] px-5 py-5 sm:px-8 sm:py-8 shadow-[0_32px_64px_-16px_rgba(0,0,0,0.6)] backdrop-blur-2xl">
<p className="font-mono text-[10px] uppercase tracking-[0.2em] text-sky-400/70 font-bold">BeadBoard Workspace</p>
<div className="mt-2 flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4">
<h1 className="text-2xl font-bold tracking-tight text-text-strong sm:text-4xl">Workflow Explorer</h1>
<Link href={kanbanHref} className="rounded-xl border border-white/10 bg-white/5 px-3 py-1.5 text-[10px] font-bold text-text-body transition-all hover:bg-white/10 hover:border-white/20 sm:px-4 sm:text-xs">
&larr; Kanban
</Link>
</div>
<p className="hidden max-w-md text-sm leading-relaxed text-text-muted/90 sm:block">
Epic-driven dependency visualization. Drill into task relationships, triage blockers, and understand downstream impact at a glance.
</p>
</div>
{activeScope ? (
<p className="mt-3 text-xs text-text-muted/90">
<WorkspaceHero
eyebrow="BeadBoard Workspace"
title={heroTitle}
description="Epic-driven dependency visualization. Drill into task relationships, triage blockers, and understand downstream impact at a glance."
action={(
<Link
href={kanbanHref}
className="ui-text rounded-xl border border-white/10 bg-white/5 px-3 py-1.5 text-[10px] font-bold text-text-body transition-all hover:bg-white/10 hover:border-white/20 sm:px-4 sm:text-xs"
>
&larr; Kanban
</Link>
)}
scope={activeScope ? (
<p className="ui-text text-xs text-text-muted/90">
Scope:{' '}
<span className="rounded-md border border-white/10 bg-white/5 px-2 py-0.5 font-mono text-[11px] text-text-body">
<span className="system-data rounded-md border border-white/10 bg-white/5 px-2 py-0.5 text-[11px] text-text-body">
{activeScope.source === 'local' ? 'local workspace' : activeScope.displayPath}
</span>
</p>
) : null}
<div className="mt-3">
) : undefined}
controls={(
<ProjectScopeControls
projectScopeKey={projectScopeKey}
projectScopeMode={projectScopeMode}
projectScopeOptions={projectScopeOptions}
/>
</div>
</header>
)}
/>
{/* Main content area */}
<section className="rounded-[2.5rem] border border-white/5 bg-[linear-gradient(180deg,rgba(255,255,255,0.015),rgba(255,255,255,0.005))] shadow-2xl backdrop-blur-sm overflow-hidden">

View file

@ -85,7 +85,7 @@ export function GraphSection({
</div>
{/* ReactFlow graph viewport */}
<div className="relative h-[60vh] min-h-[35rem] overflow-hidden rounded-2xl border border-white/5 bg-[radial-gradient(circle_at_50%_50%,rgba(15,23,42,0.4),rgba(5,8,15,0.8))] shadow-inner">
<div className="relative h-[60vh] min-h-[24rem] md:min-h-[35rem] overflow-hidden rounded-2xl border border-white/5 bg-[radial-gradient(circle_at_50%_50%,rgba(15,23,42,0.4),rgba(5,8,15,0.8))] shadow-inner">
<ReactFlow
className="workflow-graph-flow"
defaultEdgeOptions={defaultEdgeOptions}

View file

@ -57,7 +57,7 @@ interface TaskCardGridProps {
function statusDot(status: BeadIssue['status']): string {
switch (status) {
case 'open':
return 'bg-sky-400';
return 'bg-emerald-400';
case 'in_progress':
return 'bg-amber-400';
case 'blocked':
@ -65,7 +65,7 @@ function statusDot(status: BeadIssue['status']): string {
case 'deferred':
return 'bg-slate-400';
case 'closed':
return 'bg-emerald-400';
return 'bg-slate-400';
case 'pinned':
return 'bg-violet-400';
case 'hooked':
@ -75,25 +75,70 @@ function statusDot(status: BeadIssue['status']): string {
}
}
/**
* Returns status-tinted gradient background for Aero Chrome styling.
*/
function statusGradient(status: BeadIssue['status']): string {
switch (status) {
case 'open':
return 'bg-[linear-gradient(145deg,rgba(34,45,42,0.92)_0%,rgba(24,32,30,0.88)_50%,rgba(18,28,26,0.9)_100%)]';
case 'in_progress':
return 'bg-[linear-gradient(145deg,rgba(42,40,32,0.92)_0%,rgba(32,30,24,0.88)_50%,rgba(26,24,18,0.9)_100%)]';
case 'blocked':
return 'bg-[linear-gradient(145deg,rgba(60,24,30,0.95)_0%,rgba(45,18,24,0.9)_50%,rgba(32,12,16,0.92)_100%)]';
case 'closed':
return 'bg-[linear-gradient(145deg,rgba(28,30,34,0.75)_0%,rgba(22,24,28,0.72)_50%,rgba(18,20,24,0.75)_100%)] opacity-75';
case 'deferred':
return 'bg-[linear-gradient(145deg,rgba(38,40,48,0.92)_0%,rgba(28,30,36,0.88)_50%,rgba(22,24,30,0.9)_100%)]';
default:
return 'bg-[linear-gradient(145deg,rgba(38,40,48,0.92)_0%,rgba(28,30,36,0.88)_50%,rgba(22,24,30,0.9)_100%)]';
}
}
/**
* Returns status-colored border for Aero Chrome styling.
*/
function statusBorder(status: BeadIssue['status']): string {
switch (status) {
case 'open':
return 'border-emerald-500/20';
case 'in_progress':
return 'border-amber-500/20';
case 'blocked':
return 'border-rose-500/20';
case 'closed':
return 'border-rose-500/30';
case 'deferred':
return 'border-slate-500/20';
default:
return 'border-white/[0.06]';
}
}
/**
* Returns title text color class - greyed out for closed status.
*/
function titleColorClass(status: BeadIssue['status']): string {
return status === 'closed' ? 'text-text-muted/70' : 'text-text-strong';
}
/**
* Returns a human-friendly label and text color class for a status.
*/
function statusBadge(status: BeadIssue['status'], isActionable: boolean, hasBlockers: boolean): { label: string; textColor: string; bgColor: string } {
// Actual blocked status always shows as Blocked in red
if (status === 'blocked') {
return { label: 'Blocked', textColor: 'text-rose-400', bgColor: 'bg-rose-400/10' };
}
// If effectively blocked (has open blockers), show Blocked (unless closed/done)
if (hasBlockers && status !== 'closed' && status !== 'in_progress') {
return { label: 'Blocked', textColor: 'text-rose-400', bgColor: 'bg-rose-400/10' };
}
// Special case: "Blocked Now Open" -> Ready
if (status === 'blocked' && isActionable) {
return { label: 'Ready', textColor: 'text-cyan-400', bgColor: 'bg-cyan-400/10' };
}
switch (status) {
case 'in_progress':
return { label: 'In Progress', textColor: 'text-amber-400', bgColor: 'bg-amber-400/10' };
case 'blocked':
return { label: 'Blocked', textColor: 'text-rose-400', bgColor: 'bg-rose-400/10' };
case 'closed':
return { label: 'Done', textColor: 'text-emerald-400', bgColor: 'bg-emerald-400/10' };
case 'deferred':
@ -106,30 +151,7 @@ function statusBadge(status: BeadIssue['status'], isActionable: boolean, hasBloc
}
}
/**
* Returns a card-level border class based on status for visual distinction.
*/
function statusBorder(status: BeadIssue['status'], isActionable: boolean, hasBlockers: boolean): string {
if (hasBlockers && status !== 'closed' && status !== 'in_progress') {
return 'border-l-2 border-l-rose-500/60';
}
if (status === 'blocked' && isActionable) {
return 'border-l-2 border-l-cyan-400/60';
}
if (status === 'open') {
return 'border-l-2 border-l-cyan-400/60';
}
switch (status) {
case 'in_progress':
return 'border-l-2 border-l-amber-400/60';
case 'blocked':
return 'border-l-2 border-l-rose-500/60';
case 'closed':
return 'border-l-2 border-l-emerald-400/40 opacity-60';
default:
return '';
}
}
/**
* A single task card displaying the issue ID, title, priority, type, assignee,
@ -139,6 +161,12 @@ function TaskCard({ issue, selected, blockedBy, blocks, blockers, blocking, isAc
const hasBlockers = blockers.length > 0; // Note: blockers list only contains OPEN blockers (computed in page)
const badge = statusBadge(issue.status, isActionable, hasBlockers);
const projectName = (issue as BeadIssue & { project?: { name?: string } }).project?.name ?? null;
// Determine effective status: in_progress always shows as in_progress, blocked always blocked, otherwise check blockers
const effectiveStatus: BeadIssue['status'] = issue.status === 'in_progress' ? 'in_progress' :
issue.status === 'blocked' ? 'blocked' :
hasBlockers ? 'blocked' :
issue.status;
return (
<div
@ -151,13 +179,9 @@ function TaskCard({ issue, selected, blockedBy, blocks, blockers, blocking, isAc
onSelect(issue.id, false);
}
}}
className={`workflow-card group relative flex w-full flex-col rounded-xl px-4 py-4 text-left transition duration-200 ${statusBorder(
issue.status,
isActionable,
hasBlockers,
)} ${selected
? 'workflow-card-selected'
: 'hover:border-sky-300/40 hover:bg-[linear-gradient(165deg,rgba(76,94,134,0.2),rgba(18,20,30,0.84))]'
className={`group relative flex w-full flex-col rounded-xl border ${statusBorder(effectiveStatus)} ${statusGradient(effectiveStatus)} px-4 py-4 text-left transition duration-200 shadow-[0_4px_24px_rgba(0,0,0,0.35),inset_0_1px_0_rgba(255,255,255,0.06)] ${selected
? 'ring-1 ring-amber-200/30 shadow-[0_0_20px_rgba(251,191,36,0.15)]'
: 'hover:shadow-[0_8px_30px_rgba(0,0,0,0.4)]'
}`}
>
{/* Expand / Open Drawer Button */}
@ -176,7 +200,7 @@ function TaskCard({ issue, selected, blockedBy, blocks, blockers, blocking, isAc
<div className="flex w-full items-start justify-between gap-3 pr-6">
<div className="flex flex-col gap-1.5 min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className={`h-2 w-2 rounded-full ${statusDot(issue.status)} ring-1 ring-white/10`} />
<span className={`h-2 w-2 rounded-full ${statusDot(effectiveStatus)} ring-1 ring-white/10`} />
<span className="font-mono text-[10px] text-text-muted">{issue.id}</span>
{/* Status Badge */}
<span className={`rounded px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wider ${badge.textColor} ${badge.bgColor}`}>
@ -188,7 +212,7 @@ function TaskCard({ issue, selected, blockedBy, blocks, blockers, blocking, isAc
project: {projectName}
</div>
) : null}
<h3 className="line-clamp-3 text-sm font-medium leading-snug text-text-strong">
<h3 className={`line-clamp-3 text-sm font-medium leading-snug ${titleColorClass(issue.status)}`}>
{issue.title}
</h3>
</div>
@ -205,11 +229,11 @@ function TaskCard({ issue, selected, blockedBy, blocks, blockers, blocking, isAc
</div>
) : null}
{/* "Waiting On" section for blockers */}
{/* "Unlocks" section for blockers */}
{blockers.length > 0 ? (
<div className="mt-auto pt-2 w-full">
<div className="rounded-lg bg-black/20 p-2 border border-white/5">
<p className="mb-1.5 text-[9px] font-bold uppercase tracking-widest text-rose-400/80">Waiting On</p>
<p className="mb-1.5 text-[9px] font-bold uppercase tracking-widest text-rose-400/80">Unlocks</p>
<div className="flex flex-col gap-1.5">
{blockers.map((blocker) => (
<div
@ -258,11 +282,11 @@ function TaskCard({ issue, selected, blockedBy, blocks, blockers, blocking, isAc
</div>
) : null}
{/* "Blocking" section (downstream) */}
{/* "Blocks" section (downstream) */}
{blocking.length > 0 ? (
<div className={`${blockers.length > 0 ? 'mt-2' : 'mt-auto'} w-full`}>
<div className="rounded-lg bg-black/20 p-2 border border-white/5">
<p className="mb-1.5 text-[9px] font-bold uppercase tracking-widest text-amber-400/80">Blocking</p>
<p className="mb-1.5 text-[9px] font-bold uppercase tracking-widest text-amber-400/80">Blocks</p>
<div className="flex flex-col gap-1.5">
{blocking.map((item) => (
<div

View file

@ -29,10 +29,14 @@ const STATUS_META: Record<(typeof KANBAN_STATUSES)[number], { label: string; dot
};
const STATUS_COLUMN_CLASS: Record<(typeof KANBAN_STATUSES)[number], string> = {
ready: 'bg-sky-500/10',
in_progress: 'bg-amber-500/10',
blocked: 'bg-rose-500/10',
closed: 'bg-emerald-500/10',
ready:
'bg-[radial-gradient(circle_at_0%_0%,rgba(56,189,248,0.2),transparent_62%),linear-gradient(180deg,rgba(22,27,40,0.66),rgba(10,12,20,0.84))]',
in_progress:
'bg-[radial-gradient(circle_at_0%_0%,rgba(251,191,36,0.2),transparent_62%),linear-gradient(180deg,rgba(22,27,40,0.66),rgba(10,12,20,0.84))]',
blocked:
'bg-[radial-gradient(circle_at_0%_0%,rgba(244,63,94,0.2),transparent_62%),linear-gradient(180deg,rgba(22,27,40,0.66),rgba(10,12,20,0.84))]',
closed:
'bg-[radial-gradient(circle_at_0%_0%,rgba(16,185,129,0.2),transparent_62%),linear-gradient(180deg,rgba(22,27,40,0.66),rgba(10,12,20,0.84))]',
};
export function KanbanBoard({
@ -86,8 +90,10 @@ export function KanbanBoard({
key={status}
onDragOver={(event) => event.preventDefault()}
onDrop={(event) => onDropLane(status, event)}
className={`rounded-2xl border border-border-soft ${STATUS_COLUMN_CLASS[status]} p-2.5 transition ${
activeStatus === status ? 'shadow-card' : 'opacity-90'
className={`rounded-2xl border border-white/[0.04] ${STATUS_COLUMN_CLASS[status]} p-2.5 transition shadow-[0_24px_52px_-20px_rgba(0,0,0,0.82),0_10px_26px_-14px_rgba(0,0,0,0.75),inset_0_1px_0_rgba(255,255,255,0.08)] ${
activeStatus === status
? 'shadow-[0_30px_62px_-18px_rgba(0,0,0,0.86),0_0_0_1px_rgba(125,211,252,0.14)]'
: 'opacity-95'
}`}
>
<div className="flex items-center gap-2">
@ -103,11 +109,11 @@ export function KanbanBoard({
}}
className="flex w-full items-center justify-between rounded-lg px-1 py-0.5 text-left"
>
<strong className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.14em] text-text-body">
<strong className="ui-text inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.14em] text-text-body">
<span className={`h-2 w-2 rounded-full ${STATUS_META[status].dot}`} />
{STATUS_META[status].label}
</strong>
<span className="font-mono text-xs text-text-muted">{columns[status].length}</span>
<span className="system-data text-xs text-text-muted">{columns[status].length}</span>
</button>
{activeStatus === status ? (
<button
@ -127,6 +133,7 @@ export function KanbanBoard({
<KanbanCard
key={issue.id}
issue={issue}
issues={allIssues}
parentEpic={parentEpicByIssueId.get(issue.id) ?? null}
graphBaseHref={graphBaseHref}
pending={pendingIssueIds.has(issue.id)}
@ -150,11 +157,11 @@ export function KanbanBoard({
key={issue.id}
type="button"
onClick={() => handleExpandAndSelect(status, issue)}
className="max-w-full rounded-lg border border-border-soft bg-surface-muted/60 px-2 py-1 text-left hover:border-border-strong hover:bg-surface-raised/70"
className="max-w-full rounded-lg border border-border-soft bg-gradient-to-b from-surface-muted/50 to-surface-muted/70 px-2 py-1 text-left hover:border-border-strong hover:from-surface-raised/70 hover:to-surface-raised/90 shadow-[0_1px_3px_rgba(0,0,0,0.1)]"
title={issue.title}
>
<div className="font-mono text-[10px] text-text-muted">{issue.id}</div>
<div className="line-clamp-1 text-xs font-medium text-text-body">{issue.title}</div>
<div className="system-data text-[10px] text-text-muted">{issue.id}</div>
<div className="ui-text line-clamp-1 text-sm font-medium text-text-body">{issue.title}</div>
</button>
))}
{columns[status].length > 6 ? (

View file

@ -4,13 +4,14 @@ import Link from 'next/link';
import { motion } from 'framer-motion';
import type { DragEvent } from 'react';
import { formatUpdatedRecency } from '../../lib/kanban';
import { hasOpenBlockers } from '../../lib/kanban';
import type { BeadIssue } from '../../lib/types';
import { Chip } from '../shared/chip';
interface KanbanCardProps {
issue: BeadIssue;
issues?: BeadIssue[];
parentEpic?: { id: string; title: string } | null;
graphBaseHref: string;
selected: boolean;
@ -20,23 +21,61 @@ interface KanbanCardProps {
onSelect: (issue: BeadIssue) => void;
}
function priorityClass(priority: number): string {
switch (priority) {
case 0:
return 'border-rose-300/45 bg-rose-500/20 text-rose-50';
case 1:
return 'border-amber-300/40 bg-amber-500/20 text-amber-50';
case 2:
return 'border-teal-300/40 bg-teal-500/20 text-teal-50';
case 3:
return 'border-slate-300/35 bg-slate-500/22 text-slate-50';
function statusGradient(status: string): string {
switch (status) {
case 'ready':
case 'open':
return 'bg-[linear-gradient(145deg,rgba(34,45,42,0.92)_0%,rgba(24,32,30,0.88)_50%,rgba(18,28,26,0.9)_100%)]';
case 'in_progress':
return 'bg-[linear-gradient(145deg,rgba(42,40,32,0.92)_0%,rgba(32,30,24,0.88)_50%,rgba(26,24,18,0.9)_100%)]';
case 'blocked':
return 'bg-[linear-gradient(145deg,rgba(60,24,30,0.95)_0%,rgba(45,18,24,0.9)_50%,rgba(32,12,16,0.92)_100%)]';
case 'closed':
return 'bg-[linear-gradient(145deg,rgba(28,30,34,0.75)_0%,rgba(22,24,28,0.72)_50%,rgba(18,20,24,0.75)_100%)] opacity-75';
default:
return 'border-slate-400/35 bg-slate-600/20 text-slate-50';
return 'bg-[linear-gradient(145deg,rgba(38,40,48,0.92)_0%,rgba(28,30,36,0.88)_50%,rgba(22,24,30,0.9)_100%)]';
}
}
function statusBorder(status: string): string {
switch (status) {
case 'ready':
case 'open':
return 'border-emerald-500/20';
case 'in_progress':
return 'border-amber-500/20';
case 'blocked':
return 'border-rose-500/20';
case 'closed':
return 'border-rose-500/30';
default:
return 'border-white/[0.06]';
}
}
function statusDotColor(status: string): string {
switch (status) {
case 'ready':
case 'open':
return 'bg-emerald-400';
case 'in_progress':
return 'bg-amber-400';
case 'blocked':
return 'bg-rose-400';
case 'closed':
return 'bg-slate-400';
default:
return 'bg-slate-400';
}
}
function titleColor(status: string): string {
return status === 'closed' ? 'text-text-muted/70' : 'text-text-strong/95';
}
export function KanbanCard({
issue,
issues = [],
parentEpic = null,
graphBaseHref,
selected,
@ -45,13 +84,15 @@ export function KanbanCard({
onNativeDragStart,
onSelect,
}: KanbanCardProps) {
const projectName = (issue as BeadIssue & { project?: { name?: string } }).project?.name ?? null;
const unblocksCount = new Set(
issue.dependencies.filter((dependency) => dependency.type === 'blocks').map((dependency) => dependency.target),
).size;
const blockerCount = issues.length > 0 ? (hasOpenBlockers(issues, issue.id) ?
issue.dependencies.filter(d => d.type === 'blocks').filter(d => {
const blocker = issues.find(i => i.id === d.target);
return blocker && blocker.status !== 'closed';
}).length : 0) : 0;
const selectedClass = selected
? 'border-amber-200/60 bg-surface-raised shadow-card ring-1 ring-amber-200/20'
: 'border-border-soft bg-surface/95 shadow-[0_6px_18px_rgba(4,8,17,0.5)] hover:border-border-strong hover:bg-surface-raised/95';
? 'ring-1 ring-amber-200/20 shadow-[0_24px_48px_-18px_rgba(0,0,0,0.88),0_0_26px_rgba(251,191,36,0.14)]'
: 'shadow-[0_18px_38px_-18px_rgba(0,0,0,0.82),0_6px_18px_-10px_rgba(0,0,0,0.72)] hover:shadow-[0_24px_52px_-16px_rgba(0,0,0,0.9),0_10px_26px_-10px_rgba(0,0,0,0.78)]';
const graphDetailHref = parentEpic
? (() => {
@ -78,52 +119,44 @@ export function KanbanCard({
onSelect(issue);
}
}}
className={`w-full cursor-pointer rounded-2xl border px-3 py-2.5 text-left transition ${selectedClass} ${
className={`w-full cursor-pointer rounded-xl border ${statusBorder(issue.status)} ${statusGradient(issue.status)} px-3.5 py-3 text-left transition duration-200 shadow-[inset_0_1px_0_rgba(255,255,255,0.06)] ${selectedClass} ${
pending ? 'opacity-70' : ''
}`}
>
<div className="font-mono text-[11px] text-text-muted break-all">{issue.id}</div>
{projectName ? (
<div className="mt-1">
<span className="rounded-md border border-sky-300/25 bg-sky-500/10 px-1.5 py-0.5 font-mono text-[10px] text-sky-200">
project: {projectName}
</span>
</div>
) : null}
<div className="mt-1 text-sm font-semibold leading-5 text-text-strong break-words">{issue.title}</div>
<div className="mt-2 flex flex-wrap gap-1.5">
<span
className={`inline-flex items-center rounded-full border px-2 py-1 font-mono text-[11px] font-semibold ${priorityClass(issue.priority)}`}
>
P{issue.priority}
</span>
<Chip>{issue.issue_type}</Chip>
<Chip tone="status">deps {issue.dependencies.length}</Chip>
{unblocksCount > 0 ? <Chip tone="status">Unblocks {unblocksCount}</Chip> : null}
{/* ID row with status dot */}
<div className="flex items-center gap-2">
<span className={`h-1.5 w-1.5 rounded-full ${statusDotColor(issue.status)} shadow-[0_0_6px_currentColor]`} />
<span className="system-data text-[11px] text-text-muted/60">{issue.id}</span>
</div>
<div className="mt-2 break-words font-mono text-xs text-amber-100/90">
{issue.assignee ? `@${issue.assignee}` : 'unassigned'}
{/* Title */}
<div className={`ui-text mt-2 text-sm font-semibold leading-5 break-words ${titleColor(issue.status)}`}>
{issue.title}
</div>
<div className="mt-1 font-mono text-[11px] text-text-muted">{formatUpdatedRecency(issue.updated_at)}</div>
{/* Labels/Tags row */}
<div className="mt-3 flex flex-wrap items-center gap-1.5">
{issue.labels.slice(0, 3).map((label) => (
<Chip key={`${issue.id}-${label}`}>{label}</Chip>
))}
{blockerCount > 0 && (
<Chip tone="status">{blockerCount} Blocker{blockerCount > 1 ? 's' : ''}</Chip>
)}
</div>
{parentEpic ? (
<div className="mt-2">
<div className="mt-2.5">
<Link
href={graphDetailHref ?? graphBaseHref}
className="inline-flex items-center gap-1 rounded-md border border-sky-300/25 bg-sky-500/10 px-2 py-1 font-mono text-[11px] text-sky-200 hover:border-sky-300/45 hover:bg-sky-500/15"
className="system-data inline-flex items-center gap-1 rounded border border-white/8 bg-white/[0.04] px-1.5 py-0.5 text-[10px] text-text-muted/80 hover:text-text-body hover:bg-white/[0.08]"
onClick={(event) => event.stopPropagation()}
>
epic: {parentEpic.title}
{parentEpic.title}
</Link>
</div>
) : null}
{issue.labels.length > 0 ? (
<div className="mt-2 flex flex-wrap gap-1.5">
{issue.labels.slice(0, 3).map((label) => (
<Chip key={`${issue.id}-${label}`}>#{label}</Chip>
))}
</div>
) : null}
{pending ? <div className="mt-2 text-[11px] font-medium text-amber-200">Saving</div> : null}
{pending ? <div className="ui-text mt-2 text-[11px] font-medium text-amber-200">Saving</div> : null}
</motion.article>
);
}

View file

@ -60,7 +60,7 @@ export function KanbanControls({
<option className="ui-option" value="3">P3</option>
<option className="ui-option" value="4">P4</option>
</select>
<label className="inline-flex w-full items-center justify-center gap-2 rounded-xl border border-border-soft bg-surface-muted/60 px-3 py-2 text-sm text-text-body sm:w-auto sm:justify-start">
<label className="ui-text inline-flex w-full items-center justify-center gap-2 rounded-xl border border-border-soft bg-gradient-to-b from-surface-muted/50 to-surface-muted/70 px-3 py-2 text-sm text-text-body sm:w-auto sm:justify-start shadow-[0_1px_3px_rgba(0,0,0,0.1)]">
<input
type="checkbox"
checked={filters.showClosed ?? false}
@ -72,7 +72,7 @@ export function KanbanControls({
<button
type="button"
onClick={onNextActionable}
className="w-full rounded-xl border border-border-soft bg-surface-muted/70 px-3 py-2 text-sm font-semibold text-text-body transition hover:border-border-strong hover:bg-surface-raised sm:w-auto"
className="ui-text w-full rounded-xl border border-border-soft bg-gradient-to-b from-surface-muted/60 to-surface-muted/80 px-3 py-2 text-sm font-semibold text-text-body transition hover:from-surface-muted/75 hover:to-surface-muted/90 shadow-[0_1px_3px_rgba(0,0,0,0.1)] sm:w-auto"
>
Next Actionable
</button>
@ -85,9 +85,7 @@ export function KanbanControls({
<StatPill label="Done" value={stats.done} />
<StatPill label="P0" value={stats.p0} tone={stats.p0 > 0 ? 'critical' : 'default'} />
</motion.div>
{nextActionableFeedback ? (
<p className="text-xs text-text-muted">{nextActionableFeedback}</p>
) : null}
{nextActionableFeedback ? <p className="ui-text text-xs text-text-muted">{nextActionableFeedback}</p> : null}
</section>
);
}

File diff suppressed because one or more lines are too long

View file

@ -22,6 +22,7 @@ import { KanbanBoard } from './kanban-board';
import { KanbanControls } from './kanban-controls';
import { KanbanDetail } from './kanban-detail';
import { ProjectScopeControls } from '../shared/project-scope-controls';
import { WorkspaceHero } from '../shared/workspace-hero';
interface KanbanPageProps {
issues: BeadIssue[];
@ -242,34 +243,42 @@ export function KanbanPage({
return (
<main className="mx-auto min-h-screen max-w-[1800px] px-4 py-4 sm:px-6 sm:py-6">
<header className="mb-4 rounded-2xl border border-border-soft bg-surface/90 px-4 py-4 shadow-card backdrop-blur md:px-5">
<p className="font-mono text-xs uppercase tracking-[0.14em] text-text-muted">BeadBoard</p>
<div className="mt-1 flex flex-wrap items-center gap-3">
<h1 className="text-2xl font-semibold text-text-strong sm:text-3xl">Kanban Dashboard</h1>
<Link href={graphHref} className="rounded-lg border border-border-soft bg-surface-muted/70 px-2.5 py-1 text-xs text-text-body hover:bg-surface-raised">
<WorkspaceHero
eyebrow="BeadBoard Workspace"
title="Swimlanes"
description="Epic-driven dependency visualization. Drill into task relationships, triage blockers, and understand downstream impact at a glance."
className="mb-4"
action={(
<Link
href={graphHref}
className="ui-text rounded-xl border border-white/10 bg-white/5 px-3 py-1.5 text-[10px] font-bold text-text-body transition-all hover:bg-white/10 hover:border-white/20 sm:px-4 sm:text-xs"
>
Open Graph
</Link>
</div>
<p className="mt-2 text-sm text-text-muted">Tracer Bullet 1 from live `.beads/issues.jsonl` on Windows-native paths.</p>
{activeScope ? (
<p className="mt-2 text-xs text-text-muted">
)}
scope={activeScope ? (
<p className="ui-text text-xs text-text-muted/90">
Scope:{' '}
<span className="rounded-md border border-border-soft bg-surface-muted/50 px-2 py-0.5 font-mono text-[11px] text-text-body">
<span className="system-data rounded-md border border-white/10 bg-white/5 px-2 py-0.5 text-[11px] text-text-body">
{activeScope.source === 'local' ? 'local workspace' : activeScope.displayPath}
</span>
</p>
) : null}
<div className="mt-3">
<ProjectScopeControls
projectScopeKey={projectScopeKey}
projectScopeMode={projectScopeMode}
projectScopeOptions={projectScopeOptions}
/>
</div>
{!allowMutations ? (
<p className="mt-2 text-xs text-amber-200/90">Aggregate mode is read-only. Switch to single project mode to edit status/details.</p>
) : null}
</header>
) : undefined}
controls={(
<>
<ProjectScopeControls
projectScopeKey={projectScopeKey}
projectScopeMode={projectScopeMode}
projectScopeOptions={projectScopeOptions}
/>
{!allowMutations ? (
<p className="ui-text mt-2 text-xs text-amber-200/90">
Aggregate mode is read-only. Switch to single project mode to edit status/details.
</p>
) : null}
</>
)}
/>
<KanbanControls
filters={filters}
stats={stats}
@ -278,10 +287,10 @@ export function KanbanPage({
nextActionableFeedback={nextActionableFeedback}
/>
{mutationError ? (
<div className="mt-3 rounded-xl border border-rose-300/40 bg-rose-950/40 px-3 py-2 text-sm text-rose-100">{mutationError}</div>
<div className="ui-text mt-3 rounded-xl border border-rose-300/40 bg-rose-950/40 px-3 py-2 text-sm text-rose-100">{mutationError}</div>
) : null}
<section
className={`mt-3 overflow-hidden rounded-2xl border border-border-soft bg-surface/82 shadow-card ${
className={`mt-3 overflow-hidden rounded-2xl border border-white/5 bg-[linear-gradient(180deg,rgba(255,255,255,0.02),rgba(255,255,255,0.005))] shadow-[0_28px_62px_-18px_rgba(0,0,0,0.8),0_8px_24px_-10px_rgba(0,0,0,0.72)] backdrop-blur-xl ${
showDesktopDetail ? 'lg:grid lg:grid-cols-[minmax(0,1fr)_minmax(22rem,26rem)]' : ''
}`}
>
@ -303,20 +312,20 @@ export function KanbanPage({
/>
</motion.div>
{showDesktopDetail ? (
<div className="hidden border-t border-border-soft bg-surface/72 p-3 lg:block lg:border-l lg:border-t-0">
<aside className="rounded-xl border border-border-soft bg-surface/78 p-3">
<div className="hidden border-t border-white/5 bg-[rgba(9,13,22,0.78)] p-3 lg:block lg:border-l lg:border-t-0">
<aside className="rounded-xl border border-white/6 bg-[linear-gradient(180deg,rgba(42,44,52,0.54),rgba(18,20,30,0.78))] p-3 shadow-[0_18px_42px_-20px_rgba(0,0,0,0.85),inset_0_1px_0_rgba(255,255,255,0.08)]">
<div className="mb-2 flex items-center justify-end gap-2 border-b border-border-soft pb-2">
<button
type="button"
onClick={() => setDesktopDetailMinimized(true)}
className="rounded-md border border-border-soft bg-surface-muted/70 px-2 py-1 text-xs text-text-body"
className="ui-text rounded-md border border-border-soft bg-surface-muted/70 px-2 py-1 text-xs text-text-body"
>
Minimize
</button>
<button
type="button"
onClick={() => setSelectedIssueId(null)}
className="rounded-md border border-border-soft bg-surface-muted/70 px-2 py-1 text-xs text-text-muted"
className="ui-text rounded-md border border-border-soft bg-surface-muted/70 px-2 py-1 text-xs text-text-muted"
>
Clear
</button>
@ -341,7 +350,7 @@ export function KanbanPage({
<div className="fixed inset-0 z-40 lg:hidden">
<button
type="button"
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
className="absolute inset-0 bg-black/82 backdrop-blur-md"
aria-label="Close details"
onClick={() => setMobileDetailOpen(false)}
/>
@ -350,13 +359,13 @@ export function KanbanPage({
animate={{ y: 0, opacity: 1 }}
exit={{ y: 36, opacity: 0 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
className="absolute inset-x-3 bottom-3 top-20 overflow-y-auto rounded-2xl border border-border-soft bg-surface/98 p-3 shadow-panel backdrop-blur-2xl"
className="absolute inset-x-3 bottom-3 top-20 overflow-y-auto rounded-2xl border border-border-soft bg-surface/96 p-3 shadow-panel backdrop-blur-3xl"
>
<div className="mb-2 flex justify-end">
<button
type="button"
onClick={() => setMobileDetailOpen(false)}
className="rounded-lg border border-border-soft bg-surface-muted/70 px-3 py-1 text-xs font-semibold text-text-body"
className="ui-text rounded-lg border border-border-soft bg-surface-muted/70 px-3 py-1 text-xs font-semibold text-text-body"
>
Close
</button>

View file

@ -6,14 +6,18 @@ interface ChipProps {
}
const CHIP_TONE_CLASS: Record<NonNullable<ChipProps['tone']>, string> = {
default: 'border-border-soft bg-surface-muted/75 text-text-body',
status: 'border-zinc-300/30 bg-zinc-500/20 text-zinc-100',
priority: 'border-amber-300/30 bg-amber-500/20 text-amber-50',
default:
'border border-border-soft bg-gradient-to-b from-surface-muted/60 to-surface-muted/85 text-text-body shadow-[0_1px_2px_rgba(0,0,0,0.15)]',
status: 'border border-border-soft/80 bg-gradient-to-b from-zinc-500/15 to-zinc-500/25 text-zinc-100 shadow-[0_1px_2px_rgba(0,0,0,0.12)]',
priority:
'border border-amber-300/25 bg-gradient-to-b from-amber-500/15 to-amber-500/25 text-amber-50 shadow-[0_1px_2px_rgba(0,0,0,0.12)]',
};
export function Chip({ children, tone = 'default' }: ChipProps) {
return (
<span className={`inline-flex items-center rounded-full border px-2 py-1 text-[11px] font-semibold ${CHIP_TONE_CLASS[tone]}`}>
<span
className={`inline-flex items-center rounded-lg border px-2 py-1 text-[11px] font-semibold tracking-wide ${CHIP_TONE_CLASS[tone]}`}
>
{children}
</span>
);

File diff suppressed because one or more lines are too long

View file

@ -8,9 +8,9 @@ export function StatPill({ label, value, tone = 'default' }: StatPillProps) {
const valueToneClass = tone === 'critical' ? 'text-rose-300' : 'text-text-strong';
return (
<div className="min-w-[5.25rem] rounded-xl border border-border-soft bg-surface-muted/72 px-3 py-2">
<div className="font-mono text-[10px] uppercase tracking-[0.16em] text-text-muted">{label}</div>
<div className={`mt-0.5 text-lg font-semibold ${valueToneClass}`}>{value}</div>
<div className="min-w-[5.25rem] rounded-xl border border-border-soft bg-gradient-to-b from-surface-muted/55 to-surface-muted/75 px-3 py-2 shadow-[0_2px_4px_rgba(0,0,0,0.15)]">
<div className="ui-text text-[10px] uppercase tracking-[0.16em] text-text-muted">{label}</div>
<div className={`system-data mt-0.5 text-lg font-semibold ${valueToneClass}`}>{value}</div>
</div>
);
}

View file

@ -0,0 +1,39 @@
import type { ReactNode } from 'react';
interface WorkspaceHeroProps {
eyebrow: string;
title: string;
description: string;
action?: ReactNode;
scope?: ReactNode;
controls?: ReactNode;
className?: string;
}
export function WorkspaceHero({
eyebrow,
title,
description,
action,
scope,
controls,
className = '',
}: WorkspaceHeroProps) {
return (
<header
className={`mb-6 rounded-3xl border border-white/5 bg-[radial-gradient(circle_at_2%_2%,rgba(56,189,248,0.12),transparent_40%),linear-gradient(170deg,rgba(15,23,42,0.92),rgba(11,12,16,0.95))] px-5 py-5 sm:px-8 sm:py-8 shadow-[0_32px_64px_-16px_rgba(0,0,0,0.6)] backdrop-blur-2xl ${className}`}
>
<p className="system-data text-[10px] uppercase tracking-[0.2em] text-sky-400/70 font-bold">{eyebrow}</p>
<div className="mt-2 flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4">
<h1 className="ui-text text-2xl font-bold tracking-tight text-text-strong sm:text-4xl">{title}</h1>
{action}
</div>
<p className="ui-text hidden max-w-md text-sm leading-relaxed text-text-muted/90 sm:block">{description}</p>
</div>
{scope ? <div className="mt-3">{scope}</div> : null}
{controls ? <div className="mt-3">{controls}</div> : null}
</header>
);
}

View file

@ -49,12 +49,20 @@ function issueSort(a: BeadIssue, b: BeadIssue): number {
return b.updated_at.localeCompare(a.updated_at);
}
function hasOpenBlockers(issues: BeadIssue[], targetId: string): boolean {
return issues.some(
(issue) =>
issue.status !== 'closed' &&
issue.dependencies.some((dep) => dep.type === 'blocks' && dep.target === targetId),
);
export function hasOpenBlockers(issues: BeadIssue[], targetId: string): boolean {
const issueById = new Map(issues.map((issue) => [issue.id, issue]));
const target = issueById.get(targetId);
if (!target) {
return false;
}
return target.dependencies.some((dep) => {
if (dep.type !== 'blocks') {
return false;
}
const blocker = issueById.get(dep.target);
return blocker ? blocker.status !== 'closed' : false;
});
}
function hasQualitySignal(issue: BeadIssue): boolean {
@ -83,12 +91,14 @@ function deriveBlockedIds(issues: BeadIssue[]): Set<string> {
const blockedIds = new Set<string>();
for (const issue of issues) {
for (const dep of issue.dependencies) {
if (dep.type !== 'blocks') continue;
const blocker = issueById.get(issue.id);
if (!blocker) continue;
if (blocker.status === 'closed') continue;
blockedIds.add(dep.target);
if (issue.status === 'closed') continue;
const hasOpenBlocker = issue.dependencies.some((dep) => {
if (dep.type !== 'blocks') return false;
const blocker = issueById.get(dep.target);
return blocker ? blocker.status !== 'closed' : false;
});
if (hasOpenBlocker) {
blockedIds.add(issue.id);
}
}
@ -199,21 +209,16 @@ export function buildBlockedByTree(
}
const issueById = new Map(issues.map((issue) => [issue.id, issue]));
const incomingByTarget = new Map<string, string[]>();
const blockersByIssue = new Map<string, string[]>();
for (const issue of issues) {
for (const dep of issue.dependencies) {
if (dep.type !== 'blocks') continue;
const list = incomingByTarget.get(dep.target) ?? [];
list.push(issue.id);
incomingByTarget.set(dep.target, list);
}
}
for (const [targetId, blockerIds] of incomingByTarget.entries()) {
incomingByTarget.set(
targetId,
[...new Set(blockerIds)].sort((a, b) => a.localeCompare(b)),
);
const blockers = [
...new Set(
issue.dependencies
.filter((dep) => dep.type === 'blocks')
.map((dep) => dep.target),
),
].sort((a, b) => a.localeCompare(b));
blockersByIssue.set(issue.id, blockers);
}
const maxNodes = Math.max(1, options.maxNodes ?? 12);
@ -225,7 +230,7 @@ export function buildBlockedByTree(
while (queue.length > 0) {
const current = queue.shift() as { id: string; level: number };
const blockers = incomingByTarget.get(current.id) ?? [];
const blockers = blockersByIssue.get(current.id) ?? [];
for (const blockerId of blockers) {
if (visited.has(blockerId) || queued.has(blockerId)) continue;
queued.add(blockerId);
@ -258,10 +263,16 @@ export function findIssueLane(columns: KanbanColumns, issueId: string): KanbanSt
export function buildUnblocksCountByIssue(issues: BeadIssue[]): Map<string, number> {
const unblocksByIssue = new Map<string, number>();
for (const issue of issues) {
const targets = new Set(
unblocksByIssue.set(issue.id, 0);
}
for (const issue of issues) {
const uniqueBlockers = new Set(
issue.dependencies.filter((dep) => dep.type === 'blocks').map((dep) => dep.target),
);
unblocksByIssue.set(issue.id, targets.size);
for (const blockerId of uniqueBlockers) {
unblocksByIssue.set(blockerId, (unblocksByIssue.get(blockerId) ?? 0) + 1);
}
}
return unblocksByIssue;
}

View file

@ -61,6 +61,8 @@ test('extracted graph section has viewport and legend', async () => {
assert.match(graphSection, /Read left to right/, 'legend should include plain directional hint');
assert.match(graphSection, /Left = blockers/, 'legend should include left/right dependency meaning');
assert.match(graphSection, /Right = work unblocked by this task/, 'legend should include downstream meaning');
assert.match(graphSection, /min-h-\[24rem\]/, 'graph container should enforce bounded minimum height');
assert.match(graphSection, /md:min-h-\[35rem\]/, 'graph container should scale minimum height on desktop');
});
test('graph node card supports tooltips and actionable glow', async () => {
@ -73,3 +75,8 @@ test('graph node card supports tooltips and actionable glow', async () => {
assert.match(nodeCard, /isDimmed/, 'should support dimming non-chain nodes');
assert.match(nodeCard, /Ready to work/, 'actionable tooltip text');
});
test('graph edges expose explicit relation labels', async () => {
const graphPage = await read('src/components/graph/dependency-graph-page.tsx');
assert.match(graphPage, /label:\s*'BLOCKS'/, 'edges should include plain-language relation labels');
});

View file

@ -0,0 +1,50 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
const ROOT = process.cwd();
async function read(relativePath) {
return fs.readFile(path.join(ROOT, relativePath), 'utf8');
}
test('layout uses Noto Sans without JetBrains Mono dependency', async () => {
const layout = await read('src/app/layout.tsx');
assert.match(layout, /Noto_Sans/, 'should use Noto Sans font family');
assert.match(layout, /--font-ui/, 'should expose ui font css variable');
assert.doesNotMatch(layout, /JetBrains_Mono/, 'should not include JetBrains Mono');
assert.doesNotMatch(layout, /--font-mono/, 'should not expose mono font css variable from layout');
});
test('global stylesheet defines aero chrome foundation tokens and anti-banding layers', async () => {
const css = await read('src/app/globals.css');
assert.match(css, /--bg-base:/, 'should define matte base token');
assert.match(css, /--glass-base:/, 'should define glass surface token');
assert.match(css, /--edge-top:/, 'should define top chrome edge token');
assert.match(css, /--font-ui-stack:/, 'should define ui font stack token');
assert.match(css, /--font-mono-stack:/, 'should define mono font stack token');
assert.match(css, /font-family:\s*var\(--font-ui-stack\)/, 'body should consume ui font token');
assert.match(css, /body::before/, 'should define anti-banding grid layer');
assert.match(css, /body::after/, 'should define anti-banding noise layer');
assert.match(css, /--status-rdy-glow:/, 'should define ready glow token');
assert.match(css, /--status-blk-glow:/, 'should define blocked glow token');
assert.match(css, /--status-wip-glow:/, 'should define in-progress glow token');
assert.match(css, /--elevation-tight:/, 'should define tight elevation token');
assert.match(css, /--elevation-ambient:/, 'should define ambient elevation token');
assert.match(css, /\.glass-panel/, 'should define reusable glass panel primitive');
});
test('kanban and graph surfaces apply semantic typography classes', async () => {
const kanbanControls = await read('src/components/kanban/kanban-controls.tsx');
const kanbanCard = await read('src/components/kanban/kanban-card.tsx');
const graphCardGrid = await read('src/components/graph/task-card-grid.tsx');
const graphNodeCard = await read('src/components/graph/graph-node-card.tsx');
assert.match(kanbanControls, /ui-text/, 'kanban controls should use ui-text class');
assert.match(kanbanCard, /system-data/, 'kanban card should use system-data class for metadata');
assert.match(graphCardGrid, /ui-text/, 'graph task grid should use ui-text class for prose labels');
assert.match(graphNodeCard, /system-data/, 'graph node card should use system-data class for machine data');
});

View file

@ -71,10 +71,10 @@ test('buildKanbanColumns groups by core statuses and sorts by priority ascending
const columns = buildKanbanColumns(issues);
assert.deepEqual(Object.keys(columns), KANBAN_STATUSES);
assert.deepEqual(columns.ready.map((x) => x.id), ['bb-4', 'bb-5', 'bb-6']);
assert.deepEqual(columns.ready.map((x) => x.id), ['bb-2', 'bb-4', 'bb-1']);
assert.equal(columns.ready.some((x) => x.issue_type === 'epic'), false);
assert.deepEqual(columns.in_progress.map((x) => x.id), ['bb-3']);
assert.deepEqual(columns.blocked.map((x) => x.id), ['bb-2', 'bb-1']);
assert.deepEqual(columns.blocked.map((x) => x.id), ['bb-5', 'bb-6']);
assert.equal(columns.closed.length, 0);
});
@ -105,12 +105,12 @@ test('buildBlockedByTree returns compact blocker tree with depth and total', ()
issue({ id: 'bb-4', title: 'Nested blocker', dependencies: [{ type: 'blocks', target: 'bb-2' }] }),
];
const tree = buildBlockedByTree(issues, 'bb-1');
const tree = buildBlockedByTree(issues, 'bb-4');
assert.equal(tree.total, 3);
assert.equal(tree.total, 2);
assert.deepEqual(
tree.nodes.map((node) => `${node.id}:${node.level}`),
['bb-2:1', 'bb-3:1', 'bb-4:2'],
['bb-2:1', 'bb-1:2'],
);
});
@ -136,27 +136,28 @@ test('pickNextActionableIssue is deterministic by priority asc, unblocks desc, u
status: 'open',
priority: 1,
updated_at: '2026-02-10T01:00:00Z',
dependencies: [{ type: 'blocks', target: 'bb-10' }],
}),
issue({
id: 'bb-2',
status: 'open',
priority: 1,
updated_at: '2026-02-10T02:00:00Z',
dependencies: [{ type: 'blocks', target: 'bb-11' }, { type: 'blocks', target: 'bb-12' }],
}),
issue({
id: 'bb-3',
id: 'bb-10',
status: 'open',
priority: 1,
updated_at: '2026-02-10T02:00:00Z',
dependencies: [{ type: 'blocks', target: 'bb-13' }, { type: 'blocks', target: 'bb-14' }],
dependencies: [{ type: 'blocks', target: 'bb-1' }],
}),
issue({
id: 'bb-11',
status: 'open',
dependencies: [{ type: 'blocks', target: 'bb-2' }],
}),
issue({
id: 'bb-12',
status: 'open',
dependencies: [{ type: 'blocks', target: 'bb-2' }],
}),
issue({ id: 'bb-10', status: 'blocked' }),
issue({ id: 'bb-11', status: 'blocked' }),
issue({ id: 'bb-12', status: 'blocked' }),
issue({ id: 'bb-13', status: 'blocked' }),
issue({ id: 'bb-14', status: 'blocked' }),
];
const columns = buildKanbanColumns(issues);
@ -187,7 +188,9 @@ test('buildUnblocksCountByIssue counts unique blocks dependencies per issue', ()
const map = buildUnblocksCountByIssue(issues);
assert.equal(map.get('bb-1'), 2);
assert.equal(map.get('bb-1'), 0);
assert.equal(map.get('bb-2'), 1);
assert.equal(map.get('bb-3'), 1);
});
test('buildExecutionChecklist evaluates owner, blockers, quality signal, and execution-compatible lane', () => {
@ -197,8 +200,9 @@ test('buildExecutionChecklist evaluates owner, blockers, quality signal, and exe
status: 'open',
owner: 'dev-a',
description: 'Implements acceptance criteria with rollback notes',
dependencies: [{ type: 'blocks', target: 'bb-2' }],
}),
issue({ id: 'bb-2', status: 'closed', dependencies: [{ type: 'blocks', target: 'bb-1' }] }),
issue({ id: 'bb-2', status: 'closed' }),
];
const checklist = buildExecutionChecklist(issues[0], issues);
@ -208,3 +212,21 @@ test('buildExecutionChecklist evaluates owner, blockers, quality signal, and exe
[true, true, true, true],
);
});
test('buildExecutionChecklist fails blocker check when blocker is still open', () => {
const issues = [
issue({
id: 'bb-1',
status: 'open',
owner: 'dev-a',
description: 'Implements acceptance criteria with rollback notes',
dependencies: [{ type: 'blocks', target: 'bb-2' }],
}),
issue({ id: 'bb-2', status: 'open' }),
];
const checklist = buildExecutionChecklist(issues[0], issues);
const blockerItem = checklist.find((item) => item.key === 'no_open_blockers');
assert.equal(blockerItem?.passed, false);
});