beads-kanban-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/.designs/beads-kanban-ui-bj0.md +73 -0
  2. package/.designs/beads-kanban-ui-qxq.md +144 -0
  3. package/.designs/epic-support.md +282 -0
  4. package/.env.local.example +2 -0
  5. package/.eslintrc.json +3 -0
  6. package/.gitattributes +3 -0
  7. package/.github/workflows/release.yml +123 -0
  8. package/.history/README_20260121193710.md +227 -0
  9. package/.history/README_20260121193918.md +227 -0
  10. package/.history/README_20260121193921.md +227 -0
  11. package/.history/README_20260121193933.md +227 -0
  12. package/.history/README_20260121193934.md +227 -0
  13. package/.history/README_20260121193944.md +227 -0
  14. package/.history/README_20260121193953.md +227 -0
  15. package/.history/src/app/page_20260121133429.tsx +134 -0
  16. package/.history/src/app/page_20260121133928.tsx +134 -0
  17. package/.history/src/app/page_20260121144850.tsx +138 -0
  18. package/.history/src/app/page_20260121144854.tsx +138 -0
  19. package/.history/src/app/page_20260121144858.tsx +138 -0
  20. package/.history/src/app/page_20260121144902.tsx +138 -0
  21. package/.history/src/app/page_20260121144906.tsx +138 -0
  22. package/.history/src/app/page_20260121144911.tsx +138 -0
  23. package/.history/src/app/page_20260121144928.tsx +138 -0
  24. package/.playwright-mcp/.playwright-mcp/morphing-dialog-wheel-scroll-fix.png +0 -0
  25. package/.playwright-mcp/beams-test.png +0 -0
  26. package/.playwright-mcp/card-verification.png +0 -0
  27. package/.playwright-mcp/design-doc-dialog-fix-verification.png +0 -0
  28. package/.playwright-mcp/dialog-width-test.png +0 -0
  29. package/.playwright-mcp/homepage.png +0 -0
  30. package/.playwright-mcp/morphing-dialog-expanded.png +0 -0
  31. package/.playwright-mcp/morphing-dialog-fixes-final.png +0 -0
  32. package/.playwright-mcp/morphing-dialog-open.png +0 -0
  33. package/.playwright-mcp/page-2026-01-21T14-08-31-529Z.png +0 -0
  34. package/.playwright-mcp/page-2026-01-21T14-09-23-431Z.png +0 -0
  35. package/.playwright-mcp/page-2026-01-21T14-10-28-773Z.png +0 -0
  36. package/.playwright-mcp/page-2026-01-21T14-10-47-432Z.png +0 -0
  37. package/.playwright-mcp/page-2026-01-21T14-11-12-350Z.png +0 -0
  38. package/.playwright-mcp/screenshot-after-click.png +0 -0
  39. package/.playwright-mcp/screenshot-after-dialog-click.png +0 -0
  40. package/.playwright-mcp/sheet-restored-after-dialog-close.png +0 -0
  41. package/.playwright-mcp/test-1-sheet-open-with-overlay.png +0 -0
  42. package/.playwright-mcp/test-2-morphing-dialog-with-overlay.png +0 -0
  43. package/.playwright-mcp/test-3-sheet-open-dark-overlay.png +0 -0
  44. package/.playwright-mcp/test-4-morphing-dialog-with-dark-overlay.png +0 -0
  45. package/.playwright-mcp/test-5-morphing-dialog-scrolled.png +0 -0
  46. package/.playwright-mcp/test-6-sheet-restored-after-dialog-close.png +0 -0
  47. package/.playwright-mcp/wheel-scroll-fixed.png +0 -0
  48. package/README.md +243 -0
  49. package/Screenshots/bead-detail.png +0 -0
  50. package/Screenshots/dashboard.png +0 -0
  51. package/Screenshots/kanban-board.png +0 -0
  52. package/components.json +27 -0
  53. package/logo/logo.svg +1 -0
  54. package/next.config.js +9 -0
  55. package/npm/README.md +37 -0
  56. package/npm/bin/cli.js +107 -0
  57. package/npm/package.json +20 -0
  58. package/npm/scripts/postinstall.js +132 -0
  59. package/package.json +62 -0
  60. package/postcss.config.js +6 -0
  61. package/public/logo.svg +1 -0
  62. package/restart.sh +5 -0
  63. package/server/Cargo.lock +1685 -0
  64. package/server/Cargo.toml +24 -0
  65. package/server/src/db.rs +570 -0
  66. package/server/src/main.rs +141 -0
  67. package/server/src/routes/beads.rs +413 -0
  68. package/server/src/routes/cli.rs +150 -0
  69. package/server/src/routes/fs.rs +360 -0
  70. package/server/src/routes/git.rs +169 -0
  71. package/server/src/routes/mod.rs +107 -0
  72. package/server/src/routes/projects.rs +177 -0
  73. package/server/src/routes/watch.rs +211 -0
  74. package/src/app/globals.css +101 -0
  75. package/src/app/layout.tsx +36 -0
  76. package/src/app/page.tsx +348 -0
  77. package/src/app/project/kanban-board.tsx +356 -0
  78. package/src/app/project/page.tsx +18 -0
  79. package/src/app/settings/page.tsx +224 -0
  80. package/src/components/Beams.css +5 -0
  81. package/src/components/Beams.jsx +307 -0
  82. package/src/components/Galaxy.css +5 -0
  83. package/src/components/Galaxy.jsx +333 -0
  84. package/src/components/activity-timeline.tsx +172 -0
  85. package/src/components/add-project-dialog.tsx +219 -0
  86. package/src/components/bead-card.tsx +196 -0
  87. package/src/components/bead-detail.tsx +306 -0
  88. package/src/components/color-picker.tsx +101 -0
  89. package/src/components/comment-input.tsx +155 -0
  90. package/src/components/comment-list.tsx +147 -0
  91. package/src/components/dependency-badge.tsx +106 -0
  92. package/src/components/design-doc-dialog.tsx +58 -0
  93. package/src/components/design-doc-preview.tsx +97 -0
  94. package/src/components/design-doc-viewer.tsx +199 -0
  95. package/src/components/editable-project-name.tsx +178 -0
  96. package/src/components/epic-card.tsx +263 -0
  97. package/src/components/folder-browser.tsx +273 -0
  98. package/src/components/footer.tsx +27 -0
  99. package/src/components/kanban/default.tsx +184 -0
  100. package/src/components/kanban-column.tsx +167 -0
  101. package/src/components/project-card.tsx +191 -0
  102. package/src/components/quick-filter-bar.tsx +279 -0
  103. package/src/components/scan-directory-dialog.tsx +368 -0
  104. package/src/components/status-donut.tsx +197 -0
  105. package/src/components/subtask-list.tsx +128 -0
  106. package/src/components/tag-picker.tsx +252 -0
  107. package/src/components/ui/.gitkeep +0 -0
  108. package/src/components/ui/alert-dialog.tsx +141 -0
  109. package/src/components/ui/avatar.tsx +67 -0
  110. package/src/components/ui/badge.tsx +230 -0
  111. package/src/components/ui/button.tsx +433 -0
  112. package/src/components/ui/card/index.tsx +24 -0
  113. package/src/components/ui/card/roiui-card.module.css +197 -0
  114. package/src/components/ui/card/roiui-card.tsx +154 -0
  115. package/src/components/ui/card/shadcn-card.tsx +76 -0
  116. package/src/components/ui/chart.tsx +369 -0
  117. package/src/components/ui/dialog.tsx +122 -0
  118. package/src/components/ui/dropdown-menu.tsx +201 -0
  119. package/src/components/ui/input.tsx +22 -0
  120. package/src/components/ui/kanban.tsx +522 -0
  121. package/src/components/ui/morphing-dialog.tsx +457 -0
  122. package/src/components/ui/popover.tsx +33 -0
  123. package/src/components/ui/progress.tsx +28 -0
  124. package/src/components/ui/scroll-area.tsx +48 -0
  125. package/src/components/ui/select.tsx +159 -0
  126. package/src/components/ui/separator.tsx +31 -0
  127. package/src/components/ui/sheet.tsx +142 -0
  128. package/src/components/ui/skeleton.tsx +15 -0
  129. package/src/components/ui/toast.tsx +129 -0
  130. package/src/components/ui/toaster.tsx +35 -0
  131. package/src/components/ui/tooltip.tsx +30 -0
  132. package/src/hooks/.gitkeep +0 -0
  133. package/src/hooks/use-bead-filters.ts +261 -0
  134. package/src/hooks/use-beads.ts +162 -0
  135. package/src/hooks/use-branch-statuses.ts +161 -0
  136. package/src/hooks/use-epics.ts +173 -0
  137. package/src/hooks/use-file-watcher.ts +111 -0
  138. package/src/hooks/use-keyboard-navigation.ts +282 -0
  139. package/src/hooks/use-project.ts +61 -0
  140. package/src/hooks/use-projects.ts +93 -0
  141. package/src/hooks/use-toast.ts +194 -0
  142. package/src/hooks/useClickOutside.tsx +26 -0
  143. package/src/lib/.gitkeep +0 -0
  144. package/src/lib/api.ts +186 -0
  145. package/src/lib/beads-parser.ts +252 -0
  146. package/src/lib/cli.ts +193 -0
  147. package/src/lib/db.ts +145 -0
  148. package/src/lib/design-doc.ts +74 -0
  149. package/src/lib/epic-parser.ts +242 -0
  150. package/src/lib/git.ts +102 -0
  151. package/src/lib/utils.ts +12 -0
  152. package/src/types/index.ts +107 -0
  153. package/tailwind.config.ts +85 -0
  154. package/tsconfig.json +26 -0
@@ -0,0 +1,177 @@
1
+ //! Project and Tag REST API routes
2
+ //!
3
+ //! Provides CRUD endpoints for projects, tags, and project-tag relationships.
4
+
5
+ use axum::{
6
+ extract::{Path, State},
7
+ http::StatusCode,
8
+ Json,
9
+ };
10
+ use serde::Serialize;
11
+ use std::sync::Arc;
12
+
13
+ use crate::db::{
14
+ CreateProjectInput, CreateTagInput, Database, DbError, ProjectTagInput, ProjectWithTags, Tag,
15
+ UpdateProjectInput,
16
+ };
17
+
18
+ /// Application state containing the database
19
+ pub type AppState = Arc<Database>;
20
+
21
+ /// Error response structure
22
+ #[derive(Serialize)]
23
+ pub struct ErrorResponse {
24
+ pub error: String,
25
+ }
26
+
27
+ /// Success response structure for operations that don't return data
28
+ #[derive(Serialize)]
29
+ pub struct SuccessResponse {
30
+ pub success: bool,
31
+ }
32
+
33
+ impl DbError {
34
+ fn status_code(&self) -> StatusCode {
35
+ match self {
36
+ DbError::ProjectNotFound(_) | DbError::TagNotFound(_) => StatusCode::NOT_FOUND,
37
+ DbError::Sqlite(_) | DbError::PathError => StatusCode::INTERNAL_SERVER_ERROR,
38
+ }
39
+ }
40
+ }
41
+
42
+ fn db_error_response(err: DbError) -> (StatusCode, Json<ErrorResponse>) {
43
+ let status = err.status_code();
44
+ (
45
+ status,
46
+ Json(ErrorResponse {
47
+ error: err.to_string(),
48
+ }),
49
+ )
50
+ }
51
+
52
+ // ===== Project Routes =====
53
+
54
+ /// GET /api/projects - List all projects with their tags
55
+ pub async fn list_projects(
56
+ State(db): State<AppState>,
57
+ ) -> Result<Json<Vec<ProjectWithTags>>, (StatusCode, Json<ErrorResponse>)> {
58
+ db.get_projects_with_tags()
59
+ .map(Json)
60
+ .map_err(db_error_response)
61
+ }
62
+
63
+ /// POST /api/projects - Create a new project
64
+ pub async fn create_project(
65
+ State(db): State<AppState>,
66
+ Json(input): Json<CreateProjectInput>,
67
+ ) -> Result<(StatusCode, Json<ProjectWithTags>), (StatusCode, Json<ErrorResponse>)> {
68
+ let project = db.create_project(input).map_err(db_error_response)?;
69
+
70
+ // Return project with empty tags array
71
+ let project_with_tags = ProjectWithTags {
72
+ id: project.id,
73
+ name: project.name,
74
+ path: project.path,
75
+ tags: vec![],
76
+ last_opened: project.last_opened,
77
+ created_at: project.created_at,
78
+ };
79
+
80
+ Ok((StatusCode::CREATED, Json(project_with_tags)))
81
+ }
82
+
83
+ /// PATCH /api/projects/:id - Update a project
84
+ pub async fn update_project(
85
+ State(db): State<AppState>,
86
+ Path(id): Path<String>,
87
+ Json(input): Json<UpdateProjectInput>,
88
+ ) -> Result<Json<ProjectWithTags>, (StatusCode, Json<ErrorResponse>)> {
89
+ let project = db.update_project(&id, input).map_err(db_error_response)?;
90
+ let tags = db.get_project_tags(&id).map_err(db_error_response)?;
91
+
92
+ Ok(Json(ProjectWithTags {
93
+ id: project.id,
94
+ name: project.name,
95
+ path: project.path,
96
+ tags,
97
+ last_opened: project.last_opened,
98
+ created_at: project.created_at,
99
+ }))
100
+ }
101
+
102
+ /// DELETE /api/projects/:id - Delete a project
103
+ pub async fn delete_project(
104
+ State(db): State<AppState>,
105
+ Path(id): Path<String>,
106
+ ) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
107
+ db.delete_project(&id).map_err(db_error_response)?;
108
+ Ok(StatusCode::NO_CONTENT)
109
+ }
110
+
111
+ // ===== Tag Routes =====
112
+
113
+ /// GET /api/tags - List all tags
114
+ pub async fn list_tags(
115
+ State(db): State<AppState>,
116
+ ) -> Result<Json<Vec<Tag>>, (StatusCode, Json<ErrorResponse>)> {
117
+ db.get_tags().map(Json).map_err(db_error_response)
118
+ }
119
+
120
+ /// POST /api/tags - Create a new tag
121
+ pub async fn create_tag(
122
+ State(db): State<AppState>,
123
+ Json(input): Json<CreateTagInput>,
124
+ ) -> Result<(StatusCode, Json<Tag>), (StatusCode, Json<ErrorResponse>)> {
125
+ let tag = db.create_tag(input).map_err(db_error_response)?;
126
+ Ok((StatusCode::CREATED, Json(tag)))
127
+ }
128
+
129
+ /// DELETE /api/tags/:id - Delete a tag
130
+ pub async fn delete_tag(
131
+ State(db): State<AppState>,
132
+ Path(id): Path<String>,
133
+ ) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
134
+ db.delete_tag(&id).map_err(db_error_response)?;
135
+ Ok(StatusCode::NO_CONTENT)
136
+ }
137
+
138
+ // ===== Project-Tag Relationship Routes =====
139
+
140
+ /// POST /api/project-tags - Add a tag to a project
141
+ pub async fn add_project_tag(
142
+ State(db): State<AppState>,
143
+ Json(input): Json<ProjectTagInput>,
144
+ ) -> Result<(StatusCode, Json<SuccessResponse>), (StatusCode, Json<ErrorResponse>)> {
145
+ db.add_tag_to_project(&input.project_id, &input.tag_id)
146
+ .map_err(db_error_response)?;
147
+ Ok((StatusCode::CREATED, Json(SuccessResponse { success: true })))
148
+ }
149
+
150
+ /// DELETE /api/project-tags/:project_id/:tag_id - Remove a tag from a project
151
+ pub async fn remove_project_tag(
152
+ State(db): State<AppState>,
153
+ Path((project_id, tag_id)): Path<(String, String)>,
154
+ ) -> Result<Json<SuccessResponse>, (StatusCode, Json<ErrorResponse>)> {
155
+ db.remove_tag_from_project(&project_id, &tag_id)
156
+ .map_err(db_error_response)?;
157
+ Ok(Json(SuccessResponse { success: true }))
158
+ }
159
+
160
+ /// Creates the project/tag router with all routes
161
+ pub fn project_routes() -> axum::Router<AppState> {
162
+ use axum::routing::{delete, get, patch, post};
163
+
164
+ axum::Router::new()
165
+ // Project routes
166
+ .route("/projects", get(list_projects).post(create_project))
167
+ .route(
168
+ "/projects/:id",
169
+ patch(update_project).delete(delete_project),
170
+ )
171
+ // Tag routes
172
+ .route("/tags", get(list_tags).post(create_tag))
173
+ .route("/tags/:id", delete(delete_tag))
174
+ // Project-tag relationship routes
175
+ .route("/project-tags", post(add_project_tag))
176
+ .route("/project-tags/:project_id/:tag_id", delete(remove_project_tag))
177
+ }
@@ -0,0 +1,211 @@
1
+ //! File watcher SSE endpoint for real-time file change notifications.
2
+ //!
3
+ //! Provides Server-Sent Events for monitoring changes to beads issue files.
4
+
5
+ use axum::{
6
+ extract::Query,
7
+ response::sse::{Event, Sse},
8
+ };
9
+ use futures::stream::Stream;
10
+ use notify::{
11
+ event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
12
+ };
13
+ use serde::{Deserialize, Serialize};
14
+ use std::{convert::Infallible, path::PathBuf, time::Duration};
15
+ use tokio::sync::mpsc;
16
+ use tokio_stream::wrappers::ReceiverStream;
17
+ use tracing::{error, info, warn};
18
+
19
+ /// Query parameters for the watch endpoint.
20
+ #[derive(Debug, Deserialize)]
21
+ pub struct WatchParams {
22
+ /// The project path to watch for changes.
23
+ pub path: String,
24
+ }
25
+
26
+ /// File change event sent to clients.
27
+ #[derive(Debug, Serialize)]
28
+ pub struct FileChangeEvent {
29
+ /// The path of the changed file.
30
+ pub path: String,
31
+ /// The type of change (modified, created, removed).
32
+ #[serde(rename = "type")]
33
+ pub change_type: String,
34
+ }
35
+
36
+ /// SSE endpoint for watching beads file changes.
37
+ ///
38
+ /// Monitors the `.beads/issues.jsonl` file in the specified project path
39
+ /// and sends SSE events when changes are detected.
40
+ ///
41
+ /// # Query Parameters
42
+ ///
43
+ /// - `path`: The project directory path to monitor
44
+ ///
45
+ /// # Returns
46
+ ///
47
+ /// A Server-Sent Events stream of file change notifications.
48
+ pub async fn watch_beads(
49
+ Query(params): Query<WatchParams>,
50
+ ) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
51
+ let project_path = PathBuf::from(&params.path);
52
+ let beads_file = project_path.join(".beads").join("issues.jsonl");
53
+
54
+ info!("Starting file watcher for: {:?}", beads_file);
55
+
56
+ // Create channel for events with buffer for debouncing
57
+ let (tx, rx) = mpsc::channel::<Result<Event, Infallible>>(100);
58
+
59
+ // Spawn the watcher task
60
+ tokio::spawn(async move {
61
+ if let Err(e) = run_watcher(beads_file, tx).await {
62
+ error!("File watcher error: {}", e);
63
+ }
64
+ });
65
+
66
+ let stream = ReceiverStream::new(rx);
67
+ Sse::new(stream).keep_alive(
68
+ axum::response::sse::KeepAlive::new()
69
+ .interval(Duration::from_secs(30))
70
+ .text("ping"),
71
+ )
72
+ }
73
+
74
+ /// Runs the file watcher and sends events through the channel.
75
+ async fn run_watcher(
76
+ beads_file: PathBuf,
77
+ tx: mpsc::Sender<Result<Event, Infallible>>,
78
+ ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
79
+ // Create a channel for notify events
80
+ let (notify_tx, mut notify_rx) = mpsc::channel(100);
81
+
82
+ // Create the watcher
83
+ let mut watcher = RecommendedWatcher::new(
84
+ move |res: notify::Result<notify::Event>| {
85
+ if let Ok(event) = res {
86
+ // Only forward relevant events
87
+ let _ = notify_tx.blocking_send(event);
88
+ }
89
+ },
90
+ Config::default().with_poll_interval(Duration::from_millis(100)),
91
+ )?;
92
+
93
+ // Watch the parent directory (.beads) since the file might not exist yet
94
+ let watch_path = beads_file
95
+ .parent()
96
+ .map(|p| p.to_path_buf())
97
+ .unwrap_or_else(|| beads_file.clone());
98
+
99
+ // Create the .beads directory if it doesn't exist
100
+ if !watch_path.exists() {
101
+ warn!(
102
+ "Watch path does not exist, waiting for creation: {:?}",
103
+ watch_path
104
+ );
105
+ }
106
+
107
+ // Try to watch the path, or watch parent if it doesn't exist
108
+ let actual_watch_path = if watch_path.exists() {
109
+ watch_path.clone()
110
+ } else if let Some(parent) = watch_path.parent() {
111
+ if parent.exists() {
112
+ parent.to_path_buf()
113
+ } else {
114
+ error!("Neither watch path nor parent exists: {:?}", watch_path);
115
+ return Ok(());
116
+ }
117
+ } else {
118
+ error!("No valid path to watch: {:?}", watch_path);
119
+ return Ok(());
120
+ };
121
+
122
+ watcher.watch(&actual_watch_path, RecursiveMode::Recursive)?;
123
+ info!("File watcher active on: {:?}", actual_watch_path);
124
+
125
+ // Send initial connection event
126
+ let connect_event = Event::default().data(
127
+ serde_json::to_string(&FileChangeEvent {
128
+ path: beads_file.to_string_lossy().to_string(),
129
+ change_type: "connected".to_string(),
130
+ })
131
+ .unwrap_or_default(),
132
+ );
133
+ let _ = tx.send(Ok(connect_event)).await;
134
+
135
+ // Debounce state
136
+ let mut last_event_time = std::time::Instant::now();
137
+ let debounce_duration = Duration::from_millis(100);
138
+
139
+ // Process events
140
+ while let Some(event) = notify_rx.recv().await {
141
+ // Check if the event is for our target file
142
+ let is_relevant = event.paths.iter().any(|p| {
143
+ p.ends_with("issues.jsonl")
144
+ || p.ends_with(".beads")
145
+ || p == &beads_file
146
+ });
147
+
148
+ if !is_relevant {
149
+ continue;
150
+ }
151
+
152
+ // Debounce rapid changes
153
+ let now = std::time::Instant::now();
154
+ if now.duration_since(last_event_time) < debounce_duration {
155
+ continue;
156
+ }
157
+ last_event_time = now;
158
+
159
+ // Determine event type
160
+ let change_type = match event.kind {
161
+ EventKind::Create(_) => "created",
162
+ EventKind::Modify(ModifyKind::Data(_)) => "modified",
163
+ EventKind::Modify(_) => "modified",
164
+ EventKind::Remove(_) => "removed",
165
+ _ => continue, // Ignore other events
166
+ };
167
+
168
+ let file_event = FileChangeEvent {
169
+ path: beads_file.to_string_lossy().to_string(),
170
+ change_type: change_type.to_string(),
171
+ };
172
+
173
+ info!("File change detected: {:?}", file_event);
174
+
175
+ let sse_event = Event::default()
176
+ .data(serde_json::to_string(&file_event).unwrap_or_default());
177
+
178
+ // If send fails, client disconnected
179
+ if tx.send(Ok(sse_event)).await.is_err() {
180
+ info!("Client disconnected, stopping watcher");
181
+ break;
182
+ }
183
+ }
184
+
185
+ // Watcher is automatically dropped and cleaned up here
186
+ info!("File watcher stopped");
187
+ Ok(())
188
+ }
189
+
190
+ #[cfg(test)]
191
+ mod tests {
192
+ use super::*;
193
+
194
+ #[test]
195
+ fn test_file_change_event_serialization() {
196
+ let event = FileChangeEvent {
197
+ path: "/test/path".to_string(),
198
+ change_type: "modified".to_string(),
199
+ };
200
+ let json = serde_json::to_string(&event).unwrap();
201
+ assert!(json.contains("\"path\":\"/test/path\""));
202
+ assert!(json.contains("\"type\":\"modified\""));
203
+ }
204
+
205
+ #[test]
206
+ fn test_watch_params_deserialization() {
207
+ let params: WatchParams =
208
+ serde_json::from_str(r#"{"path": "/test/project"}"#).unwrap();
209
+ assert_eq!(params.path, "/test/project");
210
+ }
211
+ }
@@ -0,0 +1,101 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 240 10% 3.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 240 10% 3.9%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 240 10% 3.9%;
13
+ --primary: 240 5.9% 10%;
14
+ --primary-foreground: 0 0% 98%;
15
+ --secondary: 240 4.8% 95.9%;
16
+ --secondary-foreground: 240 5.9% 10%;
17
+ --muted: 240 4.8% 95.9%;
18
+ --muted-foreground: 240 3.8% 46.1%;
19
+ --accent: 240 4.8% 95.9%;
20
+ --accent-foreground: 240 5.9% 10%;
21
+ --destructive: 0 84.2% 60.2%;
22
+ --destructive-foreground: 0 0% 98%;
23
+ --border: 240 5.9% 90%;
24
+ --input: 240 5.9% 90%;
25
+ --ring: 240 5.9% 10%;
26
+ --chart-1: 12 76% 61%;
27
+ --chart-2: 173 58% 39%;
28
+ --chart-3: 197 37% 24%;
29
+ --chart-4: 43 74% 66%;
30
+ --chart-5: 27 87% 67%;
31
+ --radius: 0.5rem;
32
+
33
+ /* Beads Status colors (HSL) */
34
+ --status-open: 217 91% 60%;
35
+ --status-in_progress: 38 92% 50%;
36
+ --status-inreview: 271 81% 56%;
37
+ --status-closed: 142 71% 45%;
38
+
39
+ /* Beads Priority colors (HSL) */
40
+ --priority-p0: 0 84% 60%;
41
+ --priority-p1: 25 95% 53%;
42
+ --priority-p2: 240 5% 65%;
43
+ --priority-p3: 240 5% 84%;
44
+ --priority-p4: 240 5% 84%;
45
+
46
+ /* Beads Accents (HSL) */
47
+ --blocked: 0 84% 60%;
48
+ --branch: 142 76% 36%;
49
+
50
+ /* roiui Card CSS Variables */
51
+ --shadow-border-stack: 0 1px 2px hsl(var(--foreground) / 0.04);
52
+ --mix-card-33-bg: hsl(var(--card) / 0.33);
53
+ --ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955);
54
+ --background-muted: hsl(var(--muted));
55
+ }
56
+
57
+ .dark {
58
+ --background: 240 10% 3.9%;
59
+ --foreground: 0 0% 98%;
60
+ --card: 240 10% 3.9%;
61
+ --card-foreground: 0 0% 98%;
62
+ --popover: 240 10% 3.9%;
63
+ --popover-foreground: 0 0% 98%;
64
+ --primary: 0 0% 98%;
65
+ --primary-foreground: 240 5.9% 10%;
66
+ --secondary: 240 3.7% 15.9%;
67
+ --secondary-foreground: 0 0% 98%;
68
+ --muted: 240 3.7% 15.9%;
69
+ --muted-foreground: 240 5% 64.9%;
70
+ --accent: 240 3.7% 15.9%;
71
+ --accent-foreground: 0 0% 98%;
72
+ --destructive: 0 62.8% 30.6%;
73
+ --destructive-foreground: 0 0% 98%;
74
+ --border: 240 3.7% 15.9%;
75
+ --input: 240 3.7% 15.9%;
76
+ --ring: 240 4.9% 83.9%;
77
+ --chart-1: 220 70% 50%;
78
+ --chart-2: 160 60% 45%;
79
+ --chart-3: 30 80% 55%;
80
+ --chart-4: 280 65% 60%;
81
+ --chart-5: 340 75% 55%;
82
+ }
83
+ }
84
+
85
+ @layer base {
86
+ * {
87
+ @apply border-border;
88
+ }
89
+ body {
90
+ @apply bg-background text-foreground;
91
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
92
+ }
93
+ }
94
+
95
+ .font-heading {
96
+ font-family: var(--font-space-grotesk), system-ui, sans-serif;
97
+ }
98
+
99
+ .font-project-name {
100
+ font-family: var(--font-plus-jakarta), system-ui, sans-serif;
101
+ }
@@ -0,0 +1,36 @@
1
+ import type { Metadata } from 'next';
2
+ import { Space_Grotesk, Plus_Jakarta_Sans } from 'next/font/google';
3
+ import { Toaster } from '@/components/ui/toaster';
4
+ import './globals.css';
5
+
6
+ const spaceGrotesk = Space_Grotesk({
7
+ subsets: ['latin'],
8
+ display: 'swap',
9
+ variable: '--font-space-grotesk',
10
+ });
11
+
12
+ const plusJakartaSans = Plus_Jakarta_Sans({
13
+ subsets: ['latin'],
14
+ display: 'swap',
15
+ variable: '--font-plus-jakarta',
16
+ });
17
+
18
+ export const metadata: Metadata = {
19
+ title: 'Beads',
20
+ description: 'Kanban interface for beads - git-backed distributed issue tracker',
21
+ };
22
+
23
+ export default function RootLayout({
24
+ children,
25
+ }: {
26
+ children: React.ReactNode;
27
+ }) {
28
+ return (
29
+ <html lang="en" className={`dark ${spaceGrotesk.variable} ${plusJakartaSans.variable}`}>
30
+ <body className="flex min-h-screen flex-col bg-background antialiased">
31
+ <div className="flex-1">{children}</div>
32
+ <Toaster />
33
+ </body>
34
+ </html>
35
+ );
36
+ }