@taskmark/ui 0.2.5 → 0.3.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 (94) hide show
  1. package/README.md +47 -7
  2. package/app/board/page.tsx +3 -272
  3. package/app/page.tsx +14 -14
  4. package/app/setup/actions.ts +4 -4
  5. package/app/setup/page.tsx +12 -4
  6. package/bin/lib/resolve-board.mjs +50 -1
  7. package/bin/taskmark.js +154 -32
  8. package/components/board/board-screen.tsx +285 -0
  9. package/components/board/epic-list.tsx +2 -2
  10. package/components/board/overall-work-items-list.tsx +1 -1
  11. package/components/board/story-list.tsx +1 -1
  12. package/dist/prod-next/BUILD_ID +1 -1
  13. package/dist/prod-next/app-path-routes-manifest.json +2 -2
  14. package/dist/prod-next/build-manifest.json +3 -3
  15. package/dist/prod-next/prerender-manifest.json +3 -3
  16. package/dist/prod-next/react-loadable-manifest.json +2 -2
  17. package/dist/prod-next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  18. package/dist/prod-next/server/app/_global-error.html +1 -1
  19. package/dist/prod-next/server/app/_global-error.rsc +1 -1
  20. package/dist/prod-next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  21. package/dist/prod-next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  22. package/dist/prod-next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  23. package/dist/prod-next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  24. package/dist/prod-next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  25. package/dist/prod-next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  26. package/dist/prod-next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  27. package/dist/prod-next/server/app/_not-found.html +1 -1
  28. package/dist/prod-next/server/app/_not-found.rsc +1 -1
  29. package/dist/prod-next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  30. package/dist/prod-next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  31. package/dist/prod-next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  32. package/dist/prod-next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  33. package/dist/prod-next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  34. package/dist/prod-next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  35. package/dist/prod-next/server/app/api/taskmark-reload/route.js +1 -1
  36. package/dist/prod-next/server/app/board/page.js +2 -50
  37. package/dist/prod-next/server/app/board/page.js.nft.json +1 -1
  38. package/dist/prod-next/server/app/board/page_client-reference-manifest.js +1 -1
  39. package/dist/prod-next/server/app/page.js +2 -2
  40. package/dist/prod-next/server/app/page.js.nft.json +1 -1
  41. package/dist/prod-next/server/app/page_client-reference-manifest.js +1 -1
  42. package/dist/prod-next/server/app/setup/page.js +2 -2
  43. package/dist/prod-next/server/app/setup/page.js.nft.json +1 -1
  44. package/dist/prod-next/server/app/setup/page_client-reference-manifest.js +1 -1
  45. package/dist/prod-next/server/app-paths-manifest.json +2 -2
  46. package/dist/prod-next/server/chunks/192.js +49 -0
  47. package/dist/prod-next/server/chunks/463.js +1 -0
  48. package/dist/prod-next/server/chunks/487.js +1 -0
  49. package/dist/prod-next/server/chunks/54.js +19 -0
  50. package/dist/prod-next/server/chunks/57.js +8 -19
  51. package/dist/prod-next/server/middleware-build-manifest.js +1 -1
  52. package/dist/prod-next/server/middleware-react-loadable-manifest.js +1 -1
  53. package/dist/prod-next/server/pages/404.html +1 -1
  54. package/dist/prod-next/server/pages/500.html +1 -1
  55. package/dist/prod-next/server/server-reference-manifest.js +1 -1
  56. package/dist/prod-next/server/server-reference-manifest.json +1 -1
  57. package/dist/prod-next/static/chunks/636-626c380f021b95c8.js +1 -0
  58. package/dist/prod-next/static/chunks/909.932f2af3be7f1e89.js +1 -0
  59. package/dist/prod-next/static/chunks/app/_global-error/page-a1e3320be3da9be1.js +1 -0
  60. package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-a1e3320be3da9be1.js +1 -0
  61. package/dist/prod-next/static/chunks/app/board/page-636f60191db62f52.js +1 -0
  62. package/dist/prod-next/static/chunks/app/page-562a33b50da7665b.js +1 -0
  63. package/dist/prod-next/static/chunks/app/setup/page-4cf527b758b1d51e.js +1 -0
  64. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-a1e3320be3da9be1.js +1 -0
  65. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-a1e3320be3da9be1.js +1 -0
  66. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-a1e3320be3da9be1.js +1 -0
  67. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-a1e3320be3da9be1.js +1 -0
  68. package/dist/prod-next/static/chunks/webpack-d33b8a6298c94a64.js +1 -0
  69. package/dist/public/taskmark-snapshot.json +1 -1
  70. package/lib/taskmark/autoconfig.ts +54 -3
  71. package/lib/taskmark/discover.ts +2 -1
  72. package/lib/taskmark/list-view-mode.ts +1 -1
  73. package/lib/taskmark/workspace-mode.ts +24 -0
  74. package/lib/taskmark/workspace.ts +5 -2
  75. package/package.json +4 -2
  76. package/public/taskmark-snapshot.json +1 -1
  77. package/scripts/build-static.mjs +61 -3
  78. package/dist/prod-next/server/chunks/318.js +0 -1
  79. package/dist/prod-next/server/chunks/490.js +0 -8
  80. package/dist/prod-next/server/chunks/616.js +0 -1
  81. package/dist/prod-next/server/chunks/708.js +0 -1
  82. package/dist/prod-next/static/chunks/51.e9139a9ea6eb36b9.js +0 -1
  83. package/dist/prod-next/static/chunks/app/_global-error/page-d34451c04262dd7d.js +0 -1
  84. package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-d34451c04262dd7d.js +0 -1
  85. package/dist/prod-next/static/chunks/app/board/page-cd1d9be0c0127225.js +0 -1
  86. package/dist/prod-next/static/chunks/app/page-d34451c04262dd7d.js +0 -1
  87. package/dist/prod-next/static/chunks/app/setup/page-c3003359e8becc84.js +0 -1
  88. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-d34451c04262dd7d.js +0 -1
  89. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-d34451c04262dd7d.js +0 -1
  90. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-d34451c04262dd7d.js +0 -1
  91. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-d34451c04262dd7d.js +0 -1
  92. package/dist/prod-next/static/chunks/webpack-bfe8992962004e2b.js +0 -1
  93. /package/dist/prod-next/static/{wbcgYS3Jw87KqhnA9ttOL → yyRnYAFQrJjF5_b5ZIeqh}/_buildManifest.js +0 -0
  94. /package/dist/prod-next/static/{wbcgYS3Jw87KqhnA9ttOL → yyRnYAFQrJjF5_b5ZIeqh}/_ssgManifest.js +0 -0
package/bin/taskmark.js CHANGED
@@ -17,13 +17,18 @@ const requireFromPackage = createRequire(path.join(packageRoot, "package.json"))
17
17
 
18
18
  function printHelp() {
19
19
  console.log(`Usage:
20
+ taskmark [options] smart default: local board if found, else setup
21
+ taskmark open [options]
20
22
  taskmark serve [options]
21
23
  taskmark dev [options]
22
24
  taskmark build [options]
23
25
  taskmark preview [options]
24
26
 
25
27
  Commands:
26
- serve Start the prebuilt local UI server (default port ${DEFAULT_PORT})
28
+ (default) Bind to a board in/near cwd when one exists; otherwise open
29
+ the standalone setup / project picker (same as global use)
30
+ open Always start in workspace mode (setup / project picker)
31
+ serve Start the prebuilt UI bound to one resolved board (port ${DEFAULT_PORT})
27
32
  dev Next.js development server with board markdown live reload
28
33
  build Production static HTML export for Vercel / static hosting
29
34
  preview Serve an existing static export (default: <board>/out)
@@ -31,24 +36,32 @@ Commands:
31
36
  Options:
32
37
  --port, -p <n> Listen port (default ${DEFAULT_PORT}; env PORT / TASKMARK_PORT)
33
38
  --board <path> Board or product root (sets TASKMARK_BOARD)
39
+ --workspace, -w Force multi-project / setup mode (skip local board binding)
34
40
  --out <dir> Static output directory for build/preview (default: <board>/out)
35
- --no-open Do not open a browser (serve / preview / dev)
41
+ --no-open Do not open a browser (default / open / serve / preview / dev)
36
42
  --help, -h Show help
37
43
 
38
- Board resolution (same as the UI):
44
+ Modes:
45
+ Bound One board from --board / env / cwd / sibling *-taskmark; no project picker.
46
+ Workspace Setup wizard + app-bar project picker (cookies). Used when no local
47
+ board is found, or with open / --workspace.
48
+
49
+ Board resolution for bound mode (same as the UI):
39
50
  1. --board / TASKMARK_BOARD
40
51
  2. TASKMARK_MASTER
41
52
  3. TASKMARK_CWD → npm INIT_CWD → process.cwd()
42
53
  (product root with ./taskmark/, or dedicated *-taskmark root)
54
+ 4. Sibling <parent>-taskmark next to a product repo (multi-git)
43
55
 
44
56
  Examples:
57
+ npx @taskmark/ui # global: setup / pick projects
58
+ cd my-app-taskmark && npx taskmark # local board in this folder
45
59
  npm i @taskmark/ui --save && npx taskmark serve
60
+ npx taskmark open # force setup even inside a board folder
46
61
  npx taskmark dev --board .
47
62
  npx taskmark build --board .
48
63
  npm run preview
49
- npx taskmark preview --port 9000
50
- npx -p @taskmark/ui taskmark serve
51
- TASKMARK_BOARD=/path/to/my-app/taskmark npx taskmark serve
64
+ TASKMARK_BOARD=/path/to/board npx taskmark serve
52
65
  `)
53
66
  }
54
67
 
@@ -59,20 +72,24 @@ function parseArgs(argv) {
59
72
  board: null,
60
73
  out: null,
61
74
  open: true,
75
+ workspace: false,
62
76
  help: false,
63
77
  }
64
78
  const rest = [...argv]
65
- if (rest.length === 0 || rest[0] === "--help" || rest[0] === "-h") {
66
- args.help = true
79
+ if (rest.length === 0) {
80
+ args.command = "auto"
67
81
  return args
68
82
  }
69
- args.command = rest.shift()
83
+ // Bare `taskmark` with only flags → smart default (local board or setup).
84
+ args.command = rest[0].startsWith("-") ? "auto" : rest.shift()
70
85
  while (rest.length) {
71
86
  const token = rest.shift()
72
87
  if (token === "--help" || token === "-h") {
73
88
  args.help = true
74
89
  } else if (token === "--no-open") {
75
90
  args.open = false
91
+ } else if (token === "--workspace" || token === "-w") {
92
+ args.workspace = true
76
93
  } else if (token === "--port" || token === "-p") {
77
94
  args.port = rest.shift()
78
95
  } else if (token === "--board") {
@@ -122,6 +139,19 @@ function resolveNextBin() {
122
139
  }
123
140
  }
124
141
 
142
+ /**
143
+ * A `taskmark build` writes an `output: export` build. It has a server/ dir but
144
+ * serves prerendered HTML with the static runtime baked in, so `serve` must not
145
+ * reuse it.
146
+ */
147
+ function isStaticExportBuild(dir) {
148
+ return fs.existsSync(path.join(dir, "export-detail.json"))
149
+ }
150
+
151
+ function hasServerBuild(dir) {
152
+ return fs.existsSync(path.join(dir, "server")) && !isStaticExportBuild(dir)
153
+ }
154
+
125
155
  /**
126
156
  * Ensure packageRoot/.next points at a usable production build.
127
157
  * Published packages ship dist/prod-next (npm cannot pack nested node_modules
@@ -130,8 +160,8 @@ function resolveNextBin() {
130
160
  function ensureProdNext() {
131
161
  const nextDir = path.join(packageRoot, ".next")
132
162
  const shipped = path.join(packageRoot, "dist", "prod-next")
133
- const hasLocalServer = fs.existsSync(path.join(nextDir, "server"))
134
- const hasShippedServer = fs.existsSync(path.join(shipped, "server"))
163
+ const hasLocalServer = hasServerBuild(nextDir)
164
+ const hasShippedServer = hasServerBuild(shipped)
135
165
 
136
166
  if (hasLocalServer) return nextDir
137
167
  if (!hasShippedServer) {
@@ -219,6 +249,46 @@ function waitForServer(port, timeoutMs = 60_000) {
219
249
  })
220
250
  }
221
251
 
252
+ function callerCwd() {
253
+ return process.env.INIT_CWD || process.cwd()
254
+ }
255
+
256
+ function isWorkspaceRun(args) {
257
+ return args.command === "open" || args.workspace === true
258
+ }
259
+
260
+ /**
261
+ * Decide bound vs workspace for the default (`auto`) command.
262
+ * Returns a serve-compatible args object with workspace forced when unbound.
263
+ */
264
+ function resolveAutoArgs(args) {
265
+ if (args.workspace) {
266
+ return { ...args, command: "open", workspace: true }
267
+ }
268
+ if (args.board) {
269
+ return { ...args, command: "serve", workspace: false }
270
+ }
271
+ const resolved = resolveServeBoard(null)
272
+ if (resolved) {
273
+ return { ...args, command: "serve", workspace: false }
274
+ }
275
+ return { ...args, command: "open", workspace: true }
276
+ }
277
+
278
+ /**
279
+ * Workspace mode must out-rank an inherited board bind, so blank those vars for
280
+ * the child (the UI reads empty as unset) instead of only adding the flag.
281
+ */
282
+ function workspaceEnv() {
283
+ return {
284
+ TASKMARK_WORKSPACE: "1",
285
+ TASKMARK_WORKSPACE_ROOT: callerCwd(),
286
+ TASKMARK_BOARD: "",
287
+ TASKMARK_MASTER: "",
288
+ TASKMARK_CWD: "",
289
+ }
290
+ }
291
+
222
292
  function resolveBoardOrExit(boardArg, command) {
223
293
  const resolved = resolveServeBoard(boardArg)
224
294
  if (!resolved) {
@@ -228,10 +298,13 @@ Looked for:
228
298
  - --board / TASKMARK_BOARD
229
299
  - TASKMARK_MASTER
230
300
  - cwd layouts: nested ./taskmark/ or dedicated *-taskmark root
301
+ - sibling <parent>-taskmark beside a product repo
231
302
 
232
303
  Fix: cd into a product repo (with ./taskmark/) or a board root, or set TASKMARK_BOARD.
233
304
  Then re-run: npx taskmark ${command}
234
- (or: npx -p @taskmark/ui taskmark ${command})`)
305
+ (or: npx -p @taskmark/ui taskmark ${command})
306
+
307
+ To pick a project in the browser instead: npx taskmark`)
235
308
  process.exit(1)
236
309
  }
237
310
  return resolved
@@ -253,7 +326,8 @@ function attachChildLifecycle(child, onShutdown) {
253
326
  }
254
327
 
255
328
  async function serve(args) {
256
- const resolved = resolveBoardOrExit(args.board, "serve")
329
+ const workspace = isWorkspaceRun(args)
330
+ const resolved = workspace ? null : resolveBoardOrExit(args.board, "serve")
257
331
  const port = resolvePort(args.port)
258
332
  const url = `http://localhost:${port}`
259
333
  const nextBin = resolveNextBin()
@@ -266,7 +340,12 @@ async function serve(args) {
266
340
  }
267
341
 
268
342
  console.log(`Taskmark UI`)
269
- console.log(` board: ${resolved.boardPath} (${resolved.source})`)
343
+ if (workspace) {
344
+ console.log(` mode: workspace (choose projects in the browser)`)
345
+ console.log(` from: ${callerCwd()}`)
346
+ } else {
347
+ console.log(` board: ${resolved.boardPath} (${resolved.source})`)
348
+ }
270
349
  console.log(` listen: ${url}`)
271
350
 
272
351
  const child = spawn(
@@ -278,7 +357,9 @@ async function serve(args) {
278
357
  ...process.env,
279
358
  PORT: String(port),
280
359
  HOSTNAME: "0.0.0.0",
281
- TASKMARK_BOARD: resolved.boardPath,
360
+ ...(workspace
361
+ ? workspaceEnv()
362
+ : { TASKMARK_BOARD: resolved.boardPath }),
282
363
  NODE_ENV: "production",
283
364
  },
284
365
  stdio: "inherit",
@@ -308,15 +389,25 @@ async function serve(args) {
308
389
  }
309
390
 
310
391
  async function dev(args) {
311
- const resolved = resolveBoardOrExit(args.board, "dev")
392
+ const workspace = isWorkspaceRun(args)
393
+ const resolved = workspace ? null : resolveBoardOrExit(args.board, "dev")
312
394
  const port = resolvePort(args.port)
313
395
  const url = `http://localhost:${port}`
314
396
  const nextBin = resolveNextBin()
315
397
 
316
398
  console.log(`Taskmark UI (dev)`)
317
- console.log(` board: ${resolved.boardPath} (${resolved.source})`)
399
+ if (workspace) {
400
+ console.log(` mode: workspace (choose projects in the browser)`)
401
+ console.log(` from: ${callerCwd()}`)
402
+ } else {
403
+ console.log(` board: ${resolved.boardPath} (${resolved.source})`)
404
+ }
318
405
  console.log(` listen: ${url}`)
319
- console.log(` watch: **/*.md under board`)
406
+ console.log(
407
+ workspace
408
+ ? ` watch: off (no single board bound)`
409
+ : ` watch: **/*.md under board`
410
+ )
320
411
 
321
412
  const child = spawn(
322
413
  process.execPath,
@@ -326,8 +417,12 @@ async function dev(args) {
326
417
  env: {
327
418
  ...process.env,
328
419
  PORT: String(port),
329
- TASKMARK_BOARD: resolved.boardPath,
330
- TASKMARK_CWD: resolved.boardPath,
420
+ ...(workspace
421
+ ? workspaceEnv()
422
+ : {
423
+ TASKMARK_BOARD: resolved.boardPath,
424
+ TASKMARK_CWD: resolved.boardPath,
425
+ }),
331
426
  NODE_ENV: "development",
332
427
  },
333
428
  stdio: "inherit",
@@ -335,17 +430,21 @@ async function dev(args) {
335
430
  )
336
431
 
337
432
  let debounce = null
338
- const stopWatch = watchBoardMarkdown(resolved.boardPath, () => {
339
- if (debounce) clearTimeout(debounce)
340
- debounce = setTimeout(() => {
341
- try {
342
- touchDevReloadToken()
343
- console.log(`[taskmark] board markdown changed — refreshing`)
344
- } catch (err) {
345
- console.warn(`[taskmark] reload touch failed: ${err?.message || err}`)
346
- }
347
- }, 200)
348
- })
433
+ const stopWatch = workspace
434
+ ? () => {}
435
+ : watchBoardMarkdown(resolved.boardPath, () => {
436
+ if (debounce) clearTimeout(debounce)
437
+ debounce = setTimeout(() => {
438
+ try {
439
+ touchDevReloadToken()
440
+ console.log(`[taskmark] board markdown changed refreshing`)
441
+ } catch (err) {
442
+ console.warn(
443
+ `[taskmark] reload touch failed: ${err?.message || err}`
444
+ )
445
+ }
446
+ }, 200)
447
+ })
349
448
 
350
449
  const shutdown = attachChildLifecycle(child, () => {
351
450
  stopWatch()
@@ -427,7 +526,30 @@ async function main() {
427
526
  printHelp()
428
527
  process.exit(args.help || !args.command ? 0 : 1)
429
528
  }
430
- if (args.command === "serve") {
529
+ const workspaceCommands = new Set(["auto", "open", "serve", "dev"])
530
+ if (args.workspace && !workspaceCommands.has(args.command)) {
531
+ console.error(`--workspace applies to the default command, open, serve, and dev.
532
+
533
+ Use: npx taskmark (or: npx taskmark open)`)
534
+ process.exit(1)
535
+ }
536
+ if ((isWorkspaceRun(args) || (args.command === "auto" && args.workspace)) && args.board) {
537
+ console.error(`--board and workspace mode are mutually exclusive.
538
+
539
+ Bind one board: npx taskmark serve --board ${args.board}
540
+ Pick projects in UI: npx taskmark open`)
541
+ process.exit(1)
542
+ }
543
+ if (args.command === "auto") {
544
+ try {
545
+ await serve(resolveAutoArgs(args))
546
+ } catch (err) {
547
+ console.error(String(err?.message || err))
548
+ process.exit(1)
549
+ }
550
+ return
551
+ }
552
+ if (args.command === "open" || args.command === "serve") {
431
553
  try {
432
554
  await serve(args)
433
555
  } catch (err) {
@@ -0,0 +1,285 @@
1
+ import { Suspense } from "react"
2
+ import Link from "next/link"
3
+ import { redirect } from "next/navigation"
4
+
5
+ import { AppBar } from "@/components/board/app-bar"
6
+ import { EpicList } from "@/components/board/epic-list"
7
+ import { ListViewSwitcher } from "@/components/board/list-view-switcher"
8
+ import { OverallWorkItemsList } from "@/components/board/overall-work-items-list"
9
+ import { ProjectStatusMetricsStrip } from "@/components/board/project-status-metrics-strip"
10
+ import { StaticBoardApp } from "@/components/board/static-board-app"
11
+ import { TaskList } from "@/components/board/task-list"
12
+ import { WorkItemsList } from "@/components/board/work-items-list"
13
+ import { WorkItemSheetProvider } from "@/components/board/work-item-sheet"
14
+ import { resolveActiveProject } from "@/lib/taskmark/active-project"
15
+ import {
16
+ getActiveProjectCookie,
17
+ getHideCompletedCookies,
18
+ getMasterFoldersCookie,
19
+ } from "@/lib/taskmark/cookies"
20
+ import {
21
+ LIST_VIEW_LABELS,
22
+ boardHref,
23
+ parseListViewMode,
24
+ type ListViewMode,
25
+ } from "@/lib/taskmark/list-view-mode"
26
+ import { DEV_RELOAD_TOKEN } from "@/lib/taskmark/dev-reload-token"
27
+ import { loadSnapshotFromPublic } from "@/lib/taskmark/load-snapshot-server"
28
+ import { parseEpicsForProject } from "@/lib/taskmark/parse-epics"
29
+ import {
30
+ parseWorkItemsForEpic,
31
+ parseWorkItemsViewForProject,
32
+ } from "@/lib/taskmark/parse-flat-lists"
33
+ import { parseItemsForStory } from "@/lib/taskmark/parse-items"
34
+ import {
35
+ collectCompletedLeafPointSamples,
36
+ collectMetricLeaves,
37
+ computeProjectStatusMetrics,
38
+ } from "@/lib/taskmark/project-metrics"
39
+ import { isStaticRuntime } from "@/lib/taskmark/static-mode"
40
+ import { loadWorkspace } from "@/lib/taskmark/workspace"
41
+
42
+ // Keep this import so `taskmark dev` can touch the token module and refresh RSC.
43
+ void DEV_RELOAD_TOKEN
44
+
45
+ type BoardScreenProps = {
46
+ searchParams: Promise<{
47
+ view?: string | string[]
48
+ epic?: string | string[]
49
+ story?: string | string[]
50
+ item?: string | string[]
51
+ }>
52
+ }
53
+
54
+
55
+ function paramValue(value: string | string[] | undefined): string | null {
56
+ if (typeof value === "string" && value.trim()) return value.trim()
57
+ return null
58
+ }
59
+
60
+ function headingForView(view: ListViewMode): string {
61
+ if (view === "overall") return "Epics"
62
+ return LIST_VIEW_LABELS[view]
63
+ }
64
+
65
+ export async function BoardScreen({ searchParams }: BoardScreenProps) {
66
+ if (isStaticRuntime()) {
67
+ const snapshot = loadSnapshotFromPublic()
68
+ return (
69
+ <Suspense fallback={<div className="p-8 text-sm text-muted-foreground">Loading board…</div>}>
70
+ <StaticBoardApp snapshot={snapshot} />
71
+ </Suspense>
72
+ )
73
+ }
74
+
75
+ const masters = await getMasterFoldersCookie()
76
+ const workspace = loadWorkspace(masters)
77
+ if (workspace.projects.length === 0) {
78
+ redirect("/setup")
79
+ }
80
+
81
+ const savedActiveId = await getActiveProjectCookie()
82
+ const hideCompletedPrefs = await getHideCompletedCookies()
83
+ const activeProject = resolveActiveProject(
84
+ workspace.projects,
85
+ // Autoconfig: prefer the bound board; ignore stale active-project cookies.
86
+ workspace.autoconfig ? null : savedActiveId
87
+ )
88
+ if (!activeProject) {
89
+ redirect("/setup")
90
+ }
91
+
92
+ const params = await searchParams
93
+ const activeView = parseListViewMode(params.view)
94
+ const selectedEpicId =
95
+ activeView === "overall" ? paramValue(params.epic) : null
96
+ const selectedStoryId =
97
+ activeView === "overall" && selectedEpicId
98
+ ? paramValue(params.story)
99
+ : null
100
+ const selectedItemId = paramValue(params.item)
101
+
102
+ const list = parseEpicsForProject(activeProject)
103
+ const epicCount = list.epics.length
104
+ const errorCount = list.errors.length
105
+
106
+ const selectedEpic = selectedEpicId
107
+ ? list.epics.find((e) => e.id === selectedEpicId) ?? null
108
+ : null
109
+
110
+ const epicWorkItems =
111
+ activeView === "overall" && selectedEpicId != null
112
+ ? parseWorkItemsForEpic(
113
+ activeProject,
114
+ selectedEpicId,
115
+ selectedEpic?.title ?? null
116
+ )
117
+ : null
118
+
119
+ const selectedStory =
120
+ epicWorkItems && selectedStoryId
121
+ ? (epicWorkItems.rows.find(
122
+ (row) => row.kind === "story" && row.id === selectedStoryId
123
+ ) ?? null)
124
+ : null
125
+
126
+ const itemList =
127
+ activeView === "overall" &&
128
+ selectedEpicId != null &&
129
+ selectedStory != null
130
+ ? parseItemsForStory(activeProject, selectedEpicId, selectedStory.id)
131
+ : null
132
+
133
+ const workItemsList =
134
+ activeView === "overall" || activeView === "workitems"
135
+ ? parseWorkItemsViewForProject(activeProject)
136
+ : null
137
+
138
+ /** Match Current Speed exactly: completed task/bug leaf points only. */
139
+ const countableCompletions = collectCompletedLeafPointSamples(
140
+ collectMetricLeaves(activeProject)
141
+ )
142
+
143
+ const statusMetrics = computeProjectStatusMetrics(activeProject)
144
+
145
+ return (
146
+ <WorkItemSheetProvider>
147
+ <div className="min-h-svh bg-[linear-gradient(180deg,_#fff_0%,_#f7f4ff_100%)]">
148
+ <AppBar
149
+ projects={workspace.projects}
150
+ activeProjectId={activeProject.id}
151
+ autoconfig={workspace.autoconfig}
152
+ />
153
+
154
+ <div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-4 py-8">
155
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
156
+ <div>
157
+ <h1 className="font-head text-3xl tracking-tight">
158
+ {headingForView(activeView)}
159
+ </h1>
160
+ <p className="mt-1 text-sm text-muted-foreground">
161
+ {activeProject.name}
162
+ {activeView === "overall"
163
+ ? ` · ${epicCount} epic${epicCount === 1 ? "" : "s"}`
164
+ : ""}
165
+ {activeView === "overall" && errorCount > 0
166
+ ? ` · ${errorCount} parse issue${errorCount === 1 ? "" : "s"}`
167
+ : ""}
168
+ {activeView === "workitems" && workItemsList
169
+ ? ` · ${workItemsList.rows.length} item${workItemsList.rows.length === 1 ? "" : "s"}`
170
+ : ""}
171
+ {activeView === "overall" && selectedEpic
172
+ ? ` · selected ${selectedEpic.id}`
173
+ : activeView === "overall" && selectedEpicId
174
+ ? ` · epic ${selectedEpicId} not in list`
175
+ : activeView === "overall"
176
+ ? " · select an epic to view work items"
177
+ : ""}
178
+ {activeView === "overall" && selectedStory
179
+ ? ` · story ${selectedStory.id}`
180
+ : activeView === "overall" && selectedStoryId
181
+ ? ` · story ${selectedStoryId} not in list`
182
+ : ""}
183
+ </p>
184
+ </div>
185
+ <ListViewSwitcher
186
+ activeView={activeView}
187
+ selectedEpicId={selectedEpicId}
188
+ selectedStoryId={selectedStory?.id ?? null}
189
+ />
190
+ </div>
191
+
192
+ <ProjectStatusMetricsStrip metrics={statusMetrics} />
193
+
194
+ {activeView === "overall" ? (
195
+ <>
196
+ <EpicList
197
+ lists={[list]}
198
+ selectedEpicId={selectedEpicId}
199
+ countableCompletions={countableCompletions}
200
+ initialHideCompleted={hideCompletedPrefs.epics}
201
+ />
202
+
203
+ {epicWorkItems ? (
204
+ <div className="flex flex-col gap-3">
205
+ <div className="flex items-baseline justify-between gap-3">
206
+ <h2 className="font-head text-2xl tracking-tight">
207
+ Work items for{" "}
208
+ {epicWorkItems.epicTitle ?? epicWorkItems.epicId}
209
+ </h2>
210
+ <Link
211
+ href={boardHref({
212
+ view: "overall",
213
+ item: selectedItemId,
214
+ })}
215
+ className="text-sm text-muted-foreground underline-offset-2 hover:underline"
216
+ >
217
+ Clear selection
218
+ </Link>
219
+ </div>
220
+ <p className="text-sm text-muted-foreground">
221
+ {epicWorkItems.rows.length} work item
222
+ {epicWorkItems.rows.length === 1 ? "" : "s"}
223
+ {epicWorkItems.errors.length > 0
224
+ ? ` · ${epicWorkItems.errors.length} issue${epicWorkItems.errors.length === 1 ? "" : "s"}`
225
+ : ""}
226
+ {!selectedStory
227
+ ? " · select a story to view its sub tasks"
228
+ : ""}
229
+ </p>
230
+ <OverallWorkItemsList
231
+ list={epicWorkItems}
232
+ selectedStoryId={selectedStory?.id ?? null}
233
+ countableCompletions={countableCompletions}
234
+ initialHideCompleted={hideCompletedPrefs.overallWorkItems}
235
+ />
236
+ </div>
237
+ ) : null}
238
+
239
+ {itemList && selectedStory ? (
240
+ <div className="flex flex-col gap-3">
241
+ <div className="flex items-baseline justify-between gap-3">
242
+ <h2 className="font-head text-2xl tracking-tight">
243
+ Sub Tasks for{" "}
244
+ {itemList.storyTitle ?? itemList.storyId}
245
+ </h2>
246
+ <Link
247
+ href={boardHref({
248
+ view: "overall",
249
+ epic: itemList.epicId,
250
+ item: selectedItemId,
251
+ })}
252
+ className="text-sm text-muted-foreground underline-offset-2 hover:underline"
253
+ >
254
+ Clear story
255
+ </Link>
256
+ </div>
257
+ <p className="text-sm text-muted-foreground">
258
+ {itemList.items.length} sub task
259
+ {itemList.items.length === 1 ? "" : "s"}
260
+ {itemList.errors.length > 0
261
+ ? ` · ${itemList.errors.length} issue${itemList.errors.length === 1 ? "" : "s"}`
262
+ : ""}
263
+ </p>
264
+ <TaskList
265
+ list={itemList}
266
+ countableCompletions={countableCompletions}
267
+ initialHideCompleted={hideCompletedPrefs.tasks}
268
+ />
269
+ </div>
270
+ ) : null}
271
+ </>
272
+ ) : null}
273
+
274
+ {activeView === "workitems" && workItemsList ? (
275
+ <WorkItemsList
276
+ list={workItemsList}
277
+ countableCompletions={countableCompletions}
278
+ initialHideCompleted={hideCompletedPrefs.workItems}
279
+ />
280
+ ) : null}
281
+ </div>
282
+ </div>
283
+ </WorkItemSheetProvider>
284
+ )
285
+ }
@@ -263,7 +263,7 @@ function ProjectEpicCard({
263
263
  <TableCell className="font-mono text-xs">
264
264
  <IdCreatedTooltip id={epic.id} created={epic.created}>
265
265
  <Link
266
- href={`/board?epic=${encodeURIComponent(epic.id)}`}
266
+ href={`/?epic=${encodeURIComponent(epic.id)}`}
267
267
  className="underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
268
268
  aria-current={selected ? "true" : undefined}
269
269
  >
@@ -273,7 +273,7 @@ function ProjectEpicCard({
273
273
  </TableCell>
274
274
  <TableCell className="max-w-[18rem] whitespace-normal font-medium">
275
275
  <Link
276
- href={`/board?epic=${encodeURIComponent(epic.id)}`}
276
+ href={`/?epic=${encodeURIComponent(epic.id)}`}
277
277
  className="underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
278
278
  >
279
279
  {epic.title}
@@ -243,7 +243,7 @@ export function OverallWorkItemsList({
243
243
  const isStory = row.kind === "story"
244
244
  const selected = isStory && selectedStoryId === row.id
245
245
  const href = isStory
246
- ? `/board?epic=${encodeURIComponent(epicId)}&story=${encodeURIComponent(row.id)}`
246
+ ? `/?epic=${encodeURIComponent(epicId)}&story=${encodeURIComponent(row.id)}`
247
247
  : null
248
248
  const sheetKind = isStory ? "story" : "item"
249
249
  return (
@@ -244,7 +244,7 @@ export function StoryList({
244
244
  <TableBody>
245
245
  {pageRows.map((story) => {
246
246
  const selected = selectedStoryId === story.id
247
- const href = `/board?epic=${encodeURIComponent(epicId)}&story=${encodeURIComponent(story.id)}`
247
+ const href = `/?epic=${encodeURIComponent(epicId)}&story=${encodeURIComponent(story.id)}`
248
248
  return (
249
249
  <TableRow
250
250
  key={`${project.id}:${story.id}:${story.filePath}`}
@@ -1 +1 @@
1
- wbcgYS3Jw87KqhnA9ttOL
1
+ yyRnYAFQrJjF5_b5ZIeqh
@@ -3,7 +3,7 @@
3
3
  "/_global-error/page": "/_global-error",
4
4
  "/favicon.ico/route": "/favicon.ico",
5
5
  "/api/taskmark-reload/route": "/api/taskmark-reload",
6
+ "/board/page": "/board",
6
7
  "/page": "/",
7
- "/setup/page": "/setup",
8
- "/board/page": "/board"
8
+ "/setup/page": "/setup"
9
9
  }
@@ -4,11 +4,11 @@
4
4
  ],
5
5
  "devFiles": [],
6
6
  "lowPriorityFiles": [
7
- "static/wbcgYS3Jw87KqhnA9ttOL/_buildManifest.js",
8
- "static/wbcgYS3Jw87KqhnA9ttOL/_ssgManifest.js"
7
+ "static/yyRnYAFQrJjF5_b5ZIeqh/_buildManifest.js",
8
+ "static/yyRnYAFQrJjF5_b5ZIeqh/_ssgManifest.js"
9
9
  ],
10
10
  "rootMainFiles": [
11
- "static/chunks/webpack-bfe8992962004e2b.js",
11
+ "static/chunks/webpack-d33b8a6298c94a64.js",
12
12
  "static/chunks/4bd1b696-deb4a0a1da1923b0.js",
13
13
  "static/chunks/794-25668765560cb197.js",
14
14
  "static/chunks/main-app-bfb36455d24b3e5a.js"