feat: establish tokenized kanban design foundation
This commit is contained in:
parent
d82452b89c
commit
ce2010fd92
18 changed files with 1544 additions and 162 deletions
|
|
@ -4,8 +4,8 @@
|
|||
{"id":"bb-29x.3","title":"Record parser and realtime performance baseline against PRD targets","description":"Measure parse latency and update propagation using realistic sample sizes and document outcomes.","acceptance_criteria":"Performance report exists with methodology and observed timings.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:18.3210495-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:18.3210495-08:00","labels":["benchmark","perf"],"dependencies":[{"issue_id":"bb-29x.3","depends_on_id":"bb-29x","type":"parent-child","created_at":"2026-02-11T17:12:18.3220949-08:00","created_by":"zenchantlive"},{"issue_id":"bb-29x.3","depends_on_id":"bb-29x.2","type":"blocks","created_at":"2026-02-11T17:12:39.4534943-08:00","created_by":"zenchantlive"}]}
|
||||
{"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"}]}
|
||||
{"id":"bb-6aj","title":"Project Registry and Multi-Project Scanner","description":"Support multiple Windows project roots using profile-scoped registry storage and safe discovery scanning tuned for developer machines.","acceptance_criteria":"Projects can be added/removed/listed and discovered via scanner with deterministic normalization.","status":"open","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-11T17:11:47.7205517-08:00","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":"in_progress","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:43:31.2075726-08:00","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.2","title":"Implement registry API for add/remove/list operations","description":"Expose robust API endpoints with path validation and normalized identity checks to prevent duplicates.","acceptance_criteria":"API supports add, remove, list and returns clear validation errors.","status":"open","priority":0,"issue_type":"task","assignee":"agent-a","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:49.3542564-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:31.7174748-08:00","labels":["api","registry"],"dependencies":[{"issue_id":"bb-6aj.2","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:49.3558158-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.2","depends_on_id":"bb-6aj.1","type":"blocks","created_at":"2026-02-11T17:12:26.7117348-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.2","title":"Implement registry API for add/remove/list operations","description":"Expose robust API endpoints with path validation and normalized identity checks to prevent duplicates.","acceptance_criteria":"API supports add, remove, list and returns clear validation errors.","status":"closed","priority":0,"issue_type":"task","assignee":"agent-a","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:49.3542564-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:53:23.9298353-08:00","closed_at":"2026-02-11T17:53:23.9298353-08:00","close_reason":"Implemented /api/projects GET/POST/DELETE with validation, normalization, and registry integration.","labels":["api","registry"],"dependencies":[{"issue_id":"bb-6aj.2","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:49.3558158-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.2","depends_on_id":"bb-6aj.1","type":"blocks","created_at":"2026-02-11T17:12:26.7117348-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-6aj.3","title":"Build scanner with profile-root default and depth/ignore controls","description":"Scan %USERPROFILE% and user-defined roots for .beads directories with bounded recursion and ignore patterns to protect performance.","acceptance_criteria":"Scanner discovers projects without traversing entire drives by default.","status":"open","priority":0,"issue_type":"task","assignee":"agent-c","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:50.1925005-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:32.4095636-08:00","labels":["performance","scanner"],"dependencies":[{"issue_id":"bb-6aj.3","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:50.1940841-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.3","depends_on_id":"bb-6aj.1","type":"blocks","created_at":"2026-02-11T17:12:27.2225981-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-6aj.3.1","title":"Add explicit full-drive scan mode for C:/D: by user action","description":"Provide an opt-in scan mode for full drive enumeration while retaining safe defaults and progress reporting expectations.","acceptance_criteria":"Full-drive scan is only activated explicitly, never by default startup logic.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:51.0244174-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:51.0244174-08:00","labels":["optional","scanner"],"dependencies":[{"issue_id":"bb-6aj.3.1","depends_on_id":"bb-6aj.3","type":"parent-child","created_at":"2026-02-11T17:11:51.0259617-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-6aj.4","title":"Implement aggregate project issue context model","description":"Define normalized project identity payload attached to every issue for cross-project Kanban, timeline, and session views.","acceptance_criteria":"Aggregated read output always includes stable project metadata.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:51.8518922-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:51.8518922-08:00","labels":["aggregation","data-model"],"dependencies":[{"issue_id":"bb-6aj.4","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:51.8534893-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.4","depends_on_id":"bb-6aj.2","type":"blocks","created_at":"2026-02-11T17:12:27.7270195-08:00","created_by":"zenchantlive"}]}
|
||||
|
|
@ -27,11 +27,11 @@
|
|||
{"id":"bb-tpc.2","title":"Add debounce/coalescing and transient lock handling for file change bursts","description":"Coalesce rapid updates from agent activity and handle temporary read lock contention without surfacing noisy errors.","acceptance_criteria":"Burst writes produce stable event cadence and no hard failures from temporary locks.","status":"open","priority":0,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:54.315119-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:54.315119-08:00","labels":["stability","watcher"],"dependencies":[{"issue_id":"bb-tpc.2","depends_on_id":"bb-tpc","type":"parent-child","created_at":"2026-02-11T17:11:54.3172104-08:00","created_by":"zenchantlive"},{"issue_id":"bb-tpc.2","depends_on_id":"bb-tpc.1","type":"blocks","created_at":"2026-02-11T17:12:28.7308524-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-tpc.3","title":"Implement SSE events API endpoint with heartbeat and event IDs","description":"Create SSE route supporting keepalive heartbeats and resumable event consumption patterns for browser clients.","acceptance_criteria":"SSE stream remains alive and clients can reconnect automatically.","status":"open","priority":0,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:55.1518352-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:55.1518352-08:00","labels":["api","sse"],"dependencies":[{"issue_id":"bb-tpc.3","depends_on_id":"bb-tpc","type":"parent-child","created_at":"2026-02-11T17:11:55.1533991-08:00","created_by":"zenchantlive"},{"issue_id":"bb-tpc.3","depends_on_id":"bb-tpc.2","type":"blocks","created_at":"2026-02-11T17:12:29.2599782-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-tpc.4","title":"Build frontend SSE client with scoped React Query invalidation","description":"Consume server events and invalidate only affected query keys, limiting unnecessary re-fetches in multi-project mode.","acceptance_criteria":"Changed project views refresh while unrelated views remain stable.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:56.0008015-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:56.0008015-08:00","labels":["frontend","react-query"],"dependencies":[{"issue_id":"bb-tpc.4","depends_on_id":"bb-tpc","type":"parent-child","created_at":"2026-02-11T17:11:56.0024218-08:00","created_by":"zenchantlive"},{"issue_id":"bb-tpc.4","depends_on_id":"bb-tpc.3","type":"blocks","created_at":"2026-02-11T17:12:29.768818-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-trz","title":"Kanban Experience (Baseline Dashboard)","description":"Ship a production-ready Kanban baseline inspired by prototype behavior but backed by real Beads project data and strict typing.","acceptance_criteria":"Users can inspect and filter live Beads issues through stable Kanban workflows.","status":"open","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:56.8115491-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:56.8115491-08:00","labels":["kanban","ui"],"dependencies":[{"issue_id":"bb-trz","depends_on_id":"bb-92d","type":"blocks","created_at":"2026-02-11T17:12:20.6480287-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-trz.1","title":"Implement Kanban column layout for Beads statuses","description":"Render columns for open, in_progress, blocked, deferred, and closed with responsive behavior and clear status counts.","acceptance_criteria":"All statuses map correctly and render with stable ordering.","status":"in_progress","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:57.6278082-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:33.1065408-08:00","labels":["columns","kanban"],"dependencies":[{"issue_id":"bb-trz.1","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:57.6288535-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.1","depends_on_id":"bb-92d.4","type":"blocks","created_at":"2026-02-11T17:12:30.2796473-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-trz.2","title":"Build bead cards with priority/type/labels/assignee/dependency metadata","description":"Design compact cards exposing the most actionable issue metadata while preserving readability at high board density.","acceptance_criteria":"Cards show id, priority, type, labels, assignee, and dependency indicators.","status":"in_progress","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:58.4435327-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:33.8093251-08:00","labels":["cards","kanban"],"dependencies":[{"issue_id":"bb-trz.2","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:58.4450798-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.2","depends_on_id":"bb-trz.1","type":"blocks","created_at":"2026-02-11T17:12:30.7837277-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-trz.3","title":"Implement detail slide-out panel with full issue metadata","description":"Add focused issue detail panel showing description, timestamps, dependencies, and lifecycle fields used by power users.","acceptance_criteria":"Selecting a card opens detail panel with complete issue context.","status":"in_progress","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:59.2746013-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:34.4946163-08:00","labels":["details","kanban"],"dependencies":[{"issue_id":"bb-trz.3","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:59.2756402-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.3","depends_on_id":"bb-trz.2","type":"blocks","created_at":"2026-02-11T17:12:31.2944-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-trz.4","title":"Add search/filter/stats controls for status/type/priority/labels","description":"Provide fast filtering and at-a-glance counts, including critical issue indicators, for daily planning and triage workflows.","acceptance_criteria":"Search and filters apply consistently across board and counts.","status":"in_progress","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:00.0927161-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:35.1849967-08:00","labels":["filters","stats"],"dependencies":[{"issue_id":"bb-trz.4","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:12:00.0942721-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.4","depends_on_id":"bb-trz.2","type":"blocks","created_at":"2026-02-11T17:12:31.798413-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-trz","title":"Kanban Experience (Baseline Dashboard)","description":"Ship a production-ready Kanban baseline inspired by prototype behavior but backed by real Beads project data and strict typing.","acceptance_criteria":"Users can inspect and filter live Beads issues through stable Kanban workflows.","status":"closed","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:56.8115491-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:51.4226568-08:00","closed_at":"2026-02-11T17:56:51.4226568-08:00","close_reason":"Kanban epic complete for tracer bullet 1","labels":["kanban","ui"],"dependencies":[{"issue_id":"bb-trz","depends_on_id":"bb-92d","type":"blocks","created_at":"2026-02-11T17:12:20.6480287-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-trz.1","title":"Implement Kanban column layout for Beads statuses","description":"Render columns for open, in_progress, blocked, deferred, and closed with responsive behavior and clear status counts.","acceptance_criteria":"All statuses map correctly and render with stable ordering.","status":"closed","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:57.6278082-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:50.8105288-08:00","closed_at":"2026-02-11T17:56:50.8105288-08:00","close_reason":"Tracer bullet 1 Kanban baseline implemented and verified","labels":["columns","kanban"],"dependencies":[{"issue_id":"bb-trz.1","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:57.6288535-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.1","depends_on_id":"bb-92d.4","type":"blocks","created_at":"2026-02-11T17:12:30.2796473-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-trz.2","title":"Build bead cards with priority/type/labels/assignee/dependency metadata","description":"Design compact cards exposing the most actionable issue metadata while preserving readability at high board density.","acceptance_criteria":"Cards show id, priority, type, labels, assignee, and dependency indicators.","status":"closed","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:58.4435327-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:50.8141656-08:00","closed_at":"2026-02-11T17:56:50.8141656-08:00","close_reason":"Tracer bullet 1 Kanban baseline implemented and verified","labels":["cards","kanban"],"dependencies":[{"issue_id":"bb-trz.2","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:58.4450798-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.2","depends_on_id":"bb-trz.1","type":"blocks","created_at":"2026-02-11T17:12:30.7837277-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-trz.3","title":"Implement detail slide-out panel with full issue metadata","description":"Add focused issue detail panel showing description, timestamps, dependencies, and lifecycle fields used by power users.","acceptance_criteria":"Selecting a card opens detail panel with complete issue context.","status":"closed","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:59.2746013-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:50.8161639-08:00","closed_at":"2026-02-11T17:56:50.8161639-08:00","close_reason":"Tracer bullet 1 Kanban baseline implemented and verified","labels":["details","kanban"],"dependencies":[{"issue_id":"bb-trz.3","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:59.2756402-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.3","depends_on_id":"bb-trz.2","type":"blocks","created_at":"2026-02-11T17:12:31.2944-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-trz.4","title":"Add search/filter/stats controls for status/type/priority/labels","description":"Provide fast filtering and at-a-glance counts, including critical issue indicators, for daily planning and triage workflows.","acceptance_criteria":"Search and filters apply consistently across board and counts.","status":"closed","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:00.0927161-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:50.8186688-08:00","closed_at":"2026-02-11T17:56:50.8186688-08:00","close_reason":"Tracer bullet 1 Kanban baseline implemented and verified","labels":["filters","stats"],"dependencies":[{"issue_id":"bb-trz.4","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:12:00.0942721-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.4","depends_on_id":"bb-trz.2","type":"blocks","created_at":"2026-02-11T17:12:31.798413-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-u6f","title":"Agent Session Views and Metrics","description":"Group work by agent session and actor fields to provide auditability and practical productivity insights for asynchronous coding workflows.","acceptance_criteria":"Session-based summaries and detail views are available per project and aggregate contexts.","status":"open","priority":2,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:12.5083912-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:12.5083912-08:00","labels":["agents","sessions"],"dependencies":[{"issue_id":"bb-u6f","depends_on_id":"bb-tpc","type":"blocks","created_at":"2026-02-11T17:12:23.1727361-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-u6f.1","title":"Extract and normalize session identity fields from issue data","description":"Derive session grouping from closed_by_session, assignee, and created_by with robust fallback semantics.","acceptance_criteria":"Issues are consistently assigned to session buckets when data exists.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:13.3239834-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:13.3239834-08:00","labels":["agents","data"],"dependencies":[{"issue_id":"bb-u6f.1","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-11T17:12:13.3255058-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-u6f.2","title":"Build session list and detail views for claimed/completed/open outcomes","description":"Present session-level issue outcomes and navigation for operational review and accountability.","acceptance_criteria":"Users can inspect session summaries and drill into individual session issue sets.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:14.1559358-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:14.1559358-08:00","labels":["agents","ui"],"dependencies":[{"issue_id":"bb-u6f.2","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-11T17:12:14.157502-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.2","depends_on_id":"bb-u6f.1","type":"blocks","created_at":"2026-02-11T17:12:37.9045555-08:00","created_by":"zenchantlive"}]}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@ import path from 'node:path';
|
|||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
outputFileTracingRoot: path.join(process.cwd()),
|
||||
webpack(config, { dev }) {
|
||||
if (dev) {
|
||||
// Avoid intermittent Windows ENOENT errors from webpack filesystem pack cache.
|
||||
config.cache = false;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
1170
package-lock.json
generated
1170
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,9 +9,10 @@
|
|||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"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/kanban.test.ts && node --import tsx --test tests/lib/read-issues.test.ts && node --test tests/guards/no-direct-jsonl-write.test.mjs"
|
||||
"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/kanban.test.ts && node --import tsx --test tests/lib/read-issues.test.ts && node --test tests/guards/no-direct-jsonl-write.test.mjs && node --test tests/guards/no-inline-style-in-kanban.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^11.18.2",
|
||||
"next": "15.5.7",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1"
|
||||
|
|
@ -20,6 +21,9 @@
|
|||
"@types/node": "^22.10.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
|
|
|
|||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
46
src/app/globals.css
Normal file
46
src/app/globals.css
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--color-bg: #0a111f;
|
||||
--color-surface: #111c31;
|
||||
--color-surface-muted: #17263f;
|
||||
--color-surface-raised: #1b2e4a;
|
||||
--color-text-strong: #f5f8ff;
|
||||
--color-text-body: #cfdae8;
|
||||
--color-text-muted: #8da1bd;
|
||||
--color-border-soft: rgba(130, 152, 185, 0.24);
|
||||
--color-border-strong: rgba(167, 188, 218, 0.44);
|
||||
|
||||
--status-open: #60a5fa;
|
||||
--status-progress: #fbbf24;
|
||||
--status-blocked: #fb7185;
|
||||
--status-deferred: #94a3b8;
|
||||
--status-closed: #34d399;
|
||||
|
||||
--priority-p0: #f43f5e;
|
||||
--priority-p1: #f59e0b;
|
||||
--priority-p2: #38bdf8;
|
||||
--priority-p3: #94a3b8;
|
||||
--priority-p4: #64748b;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
radial-gradient(circle at 15% 8%, rgba(56, 189, 248, 0.2), transparent 32%),
|
||||
radial-gradient(circle at 80% 12%, rgba(167, 139, 250, 0.16), transparent 38%),
|
||||
linear-gradient(180deg, #060c17 0%, #0a111f 40%, #0d1525 100%);
|
||||
color: var(--color-text-body);
|
||||
font-family: 'Segoe UI', Inter, system-ui, sans-serif;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Metadata } from 'next';
|
||||
import type { ReactNode } from 'react';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'BeadBoard',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
|
||||
import { KANBAN_STATUSES } from '../../lib/kanban';
|
||||
import type { BeadIssue } from '../../lib/types';
|
||||
|
||||
|
|
@ -11,26 +13,32 @@ interface KanbanBoardProps {
|
|||
onSelect: (issue: BeadIssue) => void;
|
||||
}
|
||||
|
||||
const STATUS_META: Record<(typeof KANBAN_STATUSES)[number], { label: string; dot: string }> = {
|
||||
open: { label: 'Open', dot: 'bg-sky-300' },
|
||||
in_progress: { label: 'In Progress', dot: 'bg-amber-300' },
|
||||
blocked: { label: 'Blocked', dot: 'bg-rose-300' },
|
||||
deferred: { label: 'Deferred', dot: 'bg-slate-300' },
|
||||
closed: { label: 'Done', dot: 'bg-emerald-300' },
|
||||
};
|
||||
|
||||
export function KanbanBoard({ columns, selectedIssueId, onSelect }: KanbanBoardProps) {
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(5, minmax(220px, 1fr))',
|
||||
gap: '0.8rem',
|
||||
overflowX: 'auto',
|
||||
}}
|
||||
>
|
||||
<section className="grid min-w-[980px] grid-cols-5 gap-3 xl:min-w-0 xl:grid-cols-5">
|
||||
{KANBAN_STATUSES.map((status) => (
|
||||
<div key={status} style={{ background: '#edf2f7', borderRadius: 14, padding: '0.65rem', minHeight: 320 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.55rem' }}>
|
||||
<strong style={{ fontSize: '0.85rem', color: '#1f2937' }}>{status}</strong>
|
||||
<span style={{ fontSize: '0.8rem', color: '#475569' }}>{columns[status].length}</span>
|
||||
<div key={status} className="rounded-2xl border border-border-soft bg-surface-muted/55 p-2.5">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<strong className="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>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: '0.55rem' }}>
|
||||
{columns[status].map((issue) => (
|
||||
<KanbanCard key={issue.id} issue={issue} selected={selectedIssueId === issue.id} onSelect={onSelect} />
|
||||
))}
|
||||
<div className="grid gap-2">
|
||||
<AnimatePresence initial={false}>
|
||||
{columns[status].map((issue) => (
|
||||
<KanbanCard key={issue.id} issue={issue} selected={selectedIssueId === issue.id} onSelect={onSelect} />
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import type { BeadIssue } from '../../lib/types';
|
||||
|
||||
import { Chip } from '../shared/chip';
|
||||
|
|
@ -10,38 +12,53 @@ interface KanbanCardProps {
|
|||
onSelect: (issue: BeadIssue) => void;
|
||||
}
|
||||
|
||||
function priorityClass(priority: number): string {
|
||||
switch (priority) {
|
||||
case 0:
|
||||
return 'border-rose-300/50 bg-rose-400/20 text-rose-100';
|
||||
case 1:
|
||||
return 'border-amber-300/40 bg-amber-400/20 text-amber-100';
|
||||
case 2:
|
||||
return 'border-sky-300/40 bg-sky-400/20 text-sky-100';
|
||||
case 3:
|
||||
return 'border-slate-300/35 bg-slate-400/20 text-slate-100';
|
||||
default:
|
||||
return 'border-slate-400/35 bg-slate-500/20 text-slate-100';
|
||||
}
|
||||
}
|
||||
|
||||
export function KanbanCard({ issue, selected, onSelect }: KanbanCardProps) {
|
||||
const selectedClass = selected
|
||||
? 'border-cyan-300/70 bg-surface-raised/95 shadow-card'
|
||||
: 'border-border-soft bg-surface/90 hover:border-border-strong hover:bg-surface-raised/90';
|
||||
|
||||
return (
|
||||
<button
|
||||
<motion.button
|
||||
layout
|
||||
transition={{ duration: 0.18, ease: 'easeOut' }}
|
||||
type="button"
|
||||
onClick={() => onSelect(issue)}
|
||||
style={{
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
border: selected ? '2px solid #0f766e' : '1px solid #d7dee8',
|
||||
borderRadius: 12,
|
||||
padding: '0.7rem',
|
||||
background: '#ffffff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
className={`w-full cursor-pointer rounded-2xl border px-3 py-2.5 text-left transition ${selectedClass}`}
|
||||
>
|
||||
<div style={{ fontSize: '0.74rem', color: '#5e6b7a' }}>{issue.id}</div>
|
||||
<div style={{ fontWeight: 700, color: '#0f1720', margin: '0.15rem 0 0.5rem' }}>{issue.title}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem', marginBottom: '0.45rem' }}>
|
||||
<Chip>P{issue.priority}</Chip>
|
||||
<div className="font-mono text-[11px] text-text-muted">{issue.id}</div>
|
||||
<div className="mt-1 text-sm font-semibold leading-5 text-text-strong">{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>deps {issue.dependencies.length}</Chip>
|
||||
<Chip tone="status">deps {issue.dependencies.length}</Chip>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.8rem', color: '#314152' }}>
|
||||
<div className="mt-2 truncate font-mono text-xs text-cyan-100/80">
|
||||
{issue.assignee ? `@${issue.assignee}` : 'unassigned'}
|
||||
</div>
|
||||
{issue.labels.length > 0 ? (
|
||||
<div style={{ marginTop: '0.45rem', display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
|
||||
<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}
|
||||
</button>
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import type { KanbanFilterOptions, KanbanStats } from '../../lib/kanban';
|
||||
|
||||
import { StatPill } from '../shared/stat-pill';
|
||||
|
|
@ -11,49 +13,63 @@ interface KanbanControlsProps {
|
|||
}
|
||||
|
||||
export function KanbanControls({ filters, stats, onFiltersChange }: KanbanControlsProps) {
|
||||
const inputClass =
|
||||
'rounded-xl border border-border-soft bg-surface-muted/65 px-3 py-2 text-sm text-text-strong outline-none transition placeholder:text-text-muted focus:border-cyan-300/60 focus:ring-2 focus:ring-cyan-300/25';
|
||||
|
||||
return (
|
||||
<section style={{ display: 'grid', gap: '0.75rem' }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.6rem' }}>
|
||||
<section className="grid gap-3">
|
||||
<motion.div layout className="flex flex-wrap gap-2.5">
|
||||
<input
|
||||
type="search"
|
||||
value={filters.query ?? ''}
|
||||
onChange={(event) => onFiltersChange({ ...filters, query: event.target.value })}
|
||||
placeholder="Search by id/title/labels"
|
||||
style={{ flex: 1, minWidth: 260, borderRadius: 10, border: '1px solid #cbd5e1', padding: '0.5rem 0.6rem' }}
|
||||
className={`${inputClass} min-w-60 flex-1`}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
<select
|
||||
value={filters.type ?? ''}
|
||||
onChange={(event) => onFiltersChange({ ...filters, type: event.target.value })}
|
||||
placeholder="Type (task/bug/feature)"
|
||||
style={{ width: 190, borderRadius: 10, border: '1px solid #cbd5e1', padding: '0.5rem 0.6rem' }}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={4}
|
||||
className={`${inputClass} w-40`}
|
||||
aria-label="Type filter"
|
||||
>
|
||||
<option value="">All types</option>
|
||||
<option value="task">Task</option>
|
||||
<option value="bug">Bug</option>
|
||||
<option value="feature">Feature</option>
|
||||
<option value="epic">Epic</option>
|
||||
<option value="chore">Chore</option>
|
||||
</select>
|
||||
<select
|
||||
value={filters.priority ?? ''}
|
||||
onChange={(event) => onFiltersChange({ ...filters, priority: event.target.value })}
|
||||
placeholder="Priority"
|
||||
style={{ width: 110, borderRadius: 10, border: '1px solid #cbd5e1', padding: '0.5rem 0.6rem' }}
|
||||
/>
|
||||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: '0.45rem', color: '#334155', fontSize: '0.9rem' }}>
|
||||
className={`${inputClass} w-32`}
|
||||
aria-label="Priority filter"
|
||||
>
|
||||
<option value="">All priorities</option>
|
||||
<option value="0">P0</option>
|
||||
<option value="1">P1</option>
|
||||
<option value="2">P2</option>
|
||||
<option value="3">P3</option>
|
||||
<option value="4">P4</option>
|
||||
</select>
|
||||
<label className="inline-flex items-center gap-2 rounded-xl border border-border-soft bg-surface-muted/60 px-3 py-2 text-sm text-text-body">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.showClosed ?? false}
|
||||
onChange={(event) => onFiltersChange({ ...filters, showClosed: event.target.checked })}
|
||||
className="h-4 w-4 accent-cyan-400"
|
||||
/>
|
||||
Show closed
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.6rem' }}>
|
||||
</motion.div>
|
||||
<motion.div layout className="flex flex-wrap gap-2">
|
||||
<StatPill label="Total" value={stats.total} />
|
||||
<StatPill label="Open" value={stats.open} />
|
||||
<StatPill label="Active" value={stats.active} />
|
||||
<StatPill label="Blocked" value={stats.blocked} />
|
||||
<StatPill label="Done" value={stats.done} />
|
||||
<StatPill label="P0" value={stats.p0} />
|
||||
</div>
|
||||
<StatPill label="P0" value={stats.p0} tone={stats.p0 > 0 ? 'critical' : 'default'} />
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
import type { BeadIssue } from '../../lib/types';
|
||||
|
||||
import { Chip } from '../shared/chip';
|
||||
|
|
@ -9,42 +11,66 @@ interface KanbanDetailProps {
|
|||
}
|
||||
|
||||
export function KanbanDetail({ issue }: KanbanDetailProps) {
|
||||
if (!issue) {
|
||||
return (
|
||||
<aside style={{ border: '1px solid #d7dee8', borderRadius: 14, padding: '0.9rem', background: '#ffffff' }}>
|
||||
<strong style={{ color: '#0f1720' }}>Details</strong>
|
||||
<p style={{ color: '#475569' }}>Select a card to inspect full issue details.</p>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside style={{ border: '1px solid #d7dee8', borderRadius: 14, padding: '0.9rem', background: '#ffffff' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.5rem', alignItems: 'start' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.76rem', color: '#475569' }}>{issue.id}</div>
|
||||
<h2 style={{ margin: '0.1rem 0 0.3rem', fontSize: '1.2rem', color: '#0f1720' }}>{issue.title}</h2>
|
||||
</div>
|
||||
<Chip>{issue.status}</Chip>
|
||||
</div>
|
||||
{issue.description ? <p style={{ color: '#334155' }}>{issue.description}</p> : null}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.4rem', marginBottom: '0.5rem' }}>
|
||||
<Chip>priority {issue.priority}</Chip>
|
||||
<Chip>{issue.issue_type}</Chip>
|
||||
<Chip>{issue.assignee ? `@${issue.assignee}` : 'unassigned'}</Chip>
|
||||
<Chip>{issue.dependencies.length} dependencies</Chip>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.84rem', color: '#334155' }}>
|
||||
<div><strong>Created:</strong> {issue.created_at || '-'}</div>
|
||||
<div><strong>Updated:</strong> {issue.updated_at || '-'}</div>
|
||||
</div>
|
||||
{issue.labels.length > 0 ? (
|
||||
<div style={{ marginTop: '0.6rem', display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
|
||||
{issue.labels.map((label) => (
|
||||
<Chip key={`${issue.id}-${label}`}>#{label}</Chip>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</aside>
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{issue ? (
|
||||
<motion.aside
|
||||
key={issue.id}
|
||||
initial={{ opacity: 0, x: 24 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 24 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
className="rounded-2xl border border-border-soft bg-surface/90 p-4 shadow-panel"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<div className="font-mono text-xs text-text-muted">{issue.id}</div>
|
||||
<h2 className="mt-1 text-xl font-semibold text-text-strong">{issue.title}</h2>
|
||||
</div>
|
||||
<Chip tone="status">{issue.status}</Chip>
|
||||
</div>
|
||||
{issue.description ? <p className="mt-3 text-sm leading-6 text-text-body">{issue.description}</p> : null}
|
||||
<div className="mt-3 flex flex-wrap gap-1.5">
|
||||
<Chip tone="priority">priority {issue.priority}</Chip>
|
||||
<Chip>{issue.issue_type}</Chip>
|
||||
<Chip>{issue.assignee ? `@${issue.assignee}` : 'unassigned'}</Chip>
|
||||
<Chip>{issue.dependencies.length} dependencies</Chip>
|
||||
</div>
|
||||
<dl className="mt-4 grid gap-1.5 text-sm text-text-body">
|
||||
<div>
|
||||
<dt className="inline font-semibold text-text-strong">Created:</dt>{' '}
|
||||
<dd className="inline">{issue.created_at || '-'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="inline font-semibold text-text-strong">Updated:</dt>{' '}
|
||||
<dd className="inline">{issue.updated_at || '-'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="inline font-semibold text-text-strong">Closed:</dt>{' '}
|
||||
<dd className="inline">{issue.closed_at || '-'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{issue.labels.length > 0 ? (
|
||||
<div className="mt-4 flex flex-wrap gap-1.5">
|
||||
{issue.labels.map((label) => (
|
||||
<Chip key={`${issue.id}-${label}`}>#{label}</Chip>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</motion.aside>
|
||||
) : (
|
||||
<motion.aside
|
||||
key="empty-detail"
|
||||
initial={{ opacity: 0, x: 12 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 12 }}
|
||||
transition={{ duration: 0.18, ease: 'easeOut' }}
|
||||
className="rounded-2xl border border-border-soft bg-surface/80 p-4"
|
||||
>
|
||||
<strong className="text-text-strong">Details</strong>
|
||||
<p className="mt-1 text-sm text-text-muted">Select a card to inspect full issue details.</p>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type { KanbanFilterOptions } from '../../lib/kanban';
|
||||
|
|
@ -33,27 +34,24 @@ export function KanbanPage({ issues }: KanbanPageProps) {
|
|||
);
|
||||
|
||||
return (
|
||||
<main
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
padding: '1.2rem',
|
||||
background: 'linear-gradient(140deg, #f8fafc, #edf3f7 50%, #f8fbf9)',
|
||||
color: '#0f1720',
|
||||
fontFamily: 'Segoe UI, system-ui, sans-serif',
|
||||
}}
|
||||
>
|
||||
<header style={{ marginBottom: '0.9rem' }}>
|
||||
<h1 style={{ margin: 0, fontSize: '1.9rem' }}>BeadBoard Kanban</h1>
|
||||
<p style={{ margin: '0.35rem 0 0', color: '#475569' }}>Tracer Bullet 1 baseline from live `.beads/issues.jsonl`</p>
|
||||
<main className="mx-auto min-h-screen max-w-[1680px] px-4 py-4 sm:px-6 sm:py-6">
|
||||
<header className="mb-4 rounded-2xl border border-border-soft bg-surface/70 px-4 py-4 backdrop-blur md:px-5">
|
||||
<p className="font-mono text-xs uppercase tracking-[0.14em] text-cyan-200/80">BeadBoard</p>
|
||||
<h1 className="mt-1 text-2xl font-semibold text-text-strong sm:text-3xl">Kanban Dashboard</h1>
|
||||
<p className="mt-2 text-sm text-text-muted">Tracer Bullet 1 from live `.beads/issues.jsonl` on Windows-native paths.</p>
|
||||
</header>
|
||||
<KanbanControls filters={filters} stats={stats} onFiltersChange={setFilters} />
|
||||
<section style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 320px', gap: '0.9rem', marginTop: '0.9rem' }}>
|
||||
<KanbanBoard
|
||||
columns={columns}
|
||||
selectedIssueId={selectedIssue?.id ?? null}
|
||||
onSelect={(issue) => setSelectedIssueId(issue.id)}
|
||||
/>
|
||||
<KanbanDetail issue={selectedIssue} />
|
||||
<section className="mt-3 grid grid-cols-1 gap-3 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<motion.div layout className="overflow-x-auto rounded-2xl border border-border-soft bg-surface/55 p-2">
|
||||
<KanbanBoard
|
||||
columns={columns}
|
||||
selectedIssueId={selectedIssue?.id ?? null}
|
||||
onSelect={(issue) => setSelectedIssueId(issue.id)}
|
||||
/>
|
||||
</motion.div>
|
||||
<div className="xl:sticky xl:top-4 xl:self-start">
|
||||
<KanbanDetail issue={selectedIssue} />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,21 +2,18 @@ import type { ReactNode } from 'react';
|
|||
|
||||
interface ChipProps {
|
||||
children: ReactNode;
|
||||
tone?: 'default' | 'status' | 'priority';
|
||||
}
|
||||
|
||||
export function Chip({ children }: ChipProps) {
|
||||
const CHIP_TONE_CLASS: Record<NonNullable<ChipProps['tone']>, string> = {
|
||||
default: 'border-border-soft bg-surface-muted/70 text-text-body',
|
||||
status: 'border-cyan-300/25 bg-cyan-400/15 text-cyan-100',
|
||||
priority: 'border-amber-300/25 bg-amber-400/15 text-amber-100',
|
||||
};
|
||||
|
||||
export function Chip({ children, tone = 'default' }: ChipProps) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
border: '1px solid #d7dee8',
|
||||
borderRadius: 999,
|
||||
padding: '0.2rem 0.55rem',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
color: '#1f2a38',
|
||||
background: '#f7fafc',
|
||||
}}
|
||||
>
|
||||
<span className={`inline-flex items-center rounded-full border px-2 py-1 text-[11px] font-semibold ${CHIP_TONE_CLASS[tone]}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,16 @@
|
|||
interface StatPillProps {
|
||||
label: string;
|
||||
value: number;
|
||||
tone?: 'default' | 'critical';
|
||||
}
|
||||
|
||||
export function StatPill({ label, value }: StatPillProps) {
|
||||
export function StatPill({ label, value, tone = 'default' }: StatPillProps) {
|
||||
const valueToneClass = tone === 'critical' ? 'text-rose-300' : 'text-text-strong';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #d7dee8',
|
||||
borderRadius: 12,
|
||||
padding: '0.6rem 0.8rem',
|
||||
background: '#ffffff',
|
||||
minWidth: 90,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '0.7rem', textTransform: 'uppercase', letterSpacing: '0.05em', color: '#5e6b7a' }}>{label}</div>
|
||||
<div style={{ fontSize: '1.05rem', fontWeight: 700, color: '#0f1720' }}>{value}</div>
|
||||
<div className="min-w-20 rounded-xl border border-border-soft bg-surface-muted/65 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,24 +10,34 @@ export interface ReadIssuesOptions {
|
|||
includeTombstones?: boolean;
|
||||
}
|
||||
|
||||
export function resolveIssuesJsonlPathCandidates(projectRoot: string = process.cwd()): string[] {
|
||||
const baseDir = path.resolve(projectRoot, '.beads');
|
||||
const primary = canonicalizeWindowsPath(path.join(baseDir, 'issues.jsonl'));
|
||||
const fallback = canonicalizeWindowsPath(path.join(baseDir, 'issues.jsonl.new'));
|
||||
return [primary, fallback];
|
||||
}
|
||||
|
||||
export function resolveIssuesJsonlPath(projectRoot: string = process.cwd()): string {
|
||||
const absolute = path.resolve(projectRoot, '.beads', 'issues.jsonl');
|
||||
return canonicalizeWindowsPath(absolute);
|
||||
return resolveIssuesJsonlPathCandidates(projectRoot)[0];
|
||||
}
|
||||
|
||||
export async function readIssuesFromDisk(options: ReadIssuesOptions = {}): Promise<BeadIssue[]> {
|
||||
const issuesPath = resolveIssuesJsonlPath(options.projectRoot);
|
||||
const candidates = resolveIssuesJsonlPathCandidates(options.projectRoot);
|
||||
|
||||
try {
|
||||
const jsonl = await fs.readFile(issuesPath, 'utf8');
|
||||
return parseIssuesJsonl(jsonl, {
|
||||
includeTombstones: options.includeTombstones ?? false,
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return [];
|
||||
for (const issuesPath of candidates) {
|
||||
try {
|
||||
const jsonl = await fs.readFile(issuesPath, 'utf8');
|
||||
return parseIssuesJsonl(jsonl, {
|
||||
includeTombstones: options.includeTombstones ?? false,
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
39
tailwind.config.ts
Normal file
39
tailwind.config.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
content: ['./src/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
ui: ['Segoe UI', 'Inter', 'system-ui', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'Consolas', 'monospace'],
|
||||
},
|
||||
colors: {
|
||||
bg: 'var(--color-bg)',
|
||||
surface: {
|
||||
DEFAULT: 'var(--color-surface)',
|
||||
muted: 'var(--color-surface-muted)',
|
||||
raised: 'var(--color-surface-raised)',
|
||||
},
|
||||
text: {
|
||||
strong: 'var(--color-text-strong)',
|
||||
body: 'var(--color-text-body)',
|
||||
muted: 'var(--color-text-muted)',
|
||||
},
|
||||
border: {
|
||||
soft: 'var(--color-border-soft)',
|
||||
strong: 'var(--color-border-strong)',
|
||||
},
|
||||
},
|
||||
boxShadow: {
|
||||
card: '0 6px 18px rgba(15, 23, 42, 0.08)',
|
||||
panel: '0 18px 42px rgba(15, 23, 42, 0.2)',
|
||||
},
|
||||
borderRadius: {
|
||||
xl2: '1rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
34
tests/guards/no-inline-style-in-kanban.test.mjs
Normal file
34
tests/guards/no-inline-style-in-kanban.test.mjs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
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();
|
||||
const TARGET_DIRS = ['src/components/kanban', 'src/components/shared'];
|
||||
const INLINE_STYLE_PATTERN = /\bstyle\s*=\s*\{\s*\{/m;
|
||||
|
||||
async function collectTsxFiles(relativeDir) {
|
||||
const absoluteDir = path.join(ROOT, relativeDir);
|
||||
const entries = await fs.readdir(absoluteDir, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.tsx'))
|
||||
.map((entry) => path.join(absoluteDir, entry.name));
|
||||
}
|
||||
|
||||
test('kanban and shared components do not use inline style objects', async () => {
|
||||
const files = (await Promise.all(TARGET_DIRS.map(collectTsxFiles))).flat();
|
||||
const offenders = [];
|
||||
|
||||
for (const filePath of files) {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
if (INLINE_STYLE_PATTERN.test(content)) {
|
||||
offenders.push(path.relative(ROOT, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
offenders,
|
||||
[],
|
||||
`Inline style objects found in: ${offenders.join(', ')}`,
|
||||
);
|
||||
});
|
||||
|
|
@ -4,7 +4,7 @@ import fs from 'node:fs/promises';
|
|||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { readIssuesFromDisk, resolveIssuesJsonlPath } from '../../src/lib/read-issues';
|
||||
import { readIssuesFromDisk, resolveIssuesJsonlPath, resolveIssuesJsonlPathCandidates } from '../../src/lib/read-issues';
|
||||
import { sameWindowsPath } from '../../src/lib/pathing';
|
||||
|
||||
test('resolveIssuesJsonlPath appends .beads/issues.jsonl using windows-safe pathing', () => {
|
||||
|
|
@ -12,6 +12,12 @@ test('resolveIssuesJsonlPath appends .beads/issues.jsonl using windows-safe path
|
|||
assert.equal(sameWindowsPath(resolved, 'C:/Repo/Project/.beads/issues.jsonl'), true);
|
||||
});
|
||||
|
||||
test('resolveIssuesJsonlPathCandidates includes .jsonl and .jsonl.new fallback paths', () => {
|
||||
const [primary, fallback] = resolveIssuesJsonlPathCandidates('C:/Repo/Project');
|
||||
assert.equal(sameWindowsPath(primary, 'C:/Repo/Project/.beads/issues.jsonl'), true);
|
||||
assert.equal(sameWindowsPath(fallback, 'C:/Repo/Project/.beads/issues.jsonl.new'), true);
|
||||
});
|
||||
|
||||
test('readIssuesFromDisk parses JSONL issues from disk', async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-read-'));
|
||||
const beadsDir = path.join(root, '.beads');
|
||||
|
|
@ -39,3 +45,19 @@ test('readIssuesFromDisk returns empty list when issues file does not exist', as
|
|||
const issues = await readIssuesFromDisk({ projectRoot: root });
|
||||
assert.deepEqual(issues, []);
|
||||
});
|
||||
|
||||
test('readIssuesFromDisk falls back to issues.jsonl.new when issues.jsonl is missing', async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-read-fallback-'));
|
||||
const beadsDir = path.join(root, '.beads');
|
||||
const fallbackPath = path.join(beadsDir, 'issues.jsonl.new');
|
||||
await fs.mkdir(beadsDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
fallbackPath,
|
||||
JSON.stringify({ id: 'bb-fallback', title: 'From fallback', status: 'open', priority: 2, issue_type: 'task' }),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const issues = await readIssuesFromDisk({ projectRoot: root });
|
||||
assert.equal(issues.length, 1);
|
||||
assert.equal(issues[0].id, 'bb-fallback');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue