@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/README.md CHANGED
@@ -2,12 +2,47 @@
2
2
 
3
3
  Install in a Taskmark board folder (or product root with `./taskmark/`) and open the board in the browser.
4
4
 
5
+ ## Two modes
6
+
7
+ | Mode | When | Behaviour |
8
+ |------|------|-----------|
9
+ | **Bound** | Board found in/near cwd, or `serve` / `dev` | Opens that one board. No setup wizard, no project picker. |
10
+ | **Workspace** | No local board, or `open` / `--workspace` | Standalone setup wizard + app-bar project picker. |
11
+
12
+ ```bash
13
+ # Global / any folder without a board → setup & pick projects
14
+ npx @taskmark/ui
15
+ # or: npx -p @taskmark/ui taskmark
16
+
17
+ # Inside a board folder (or product with ./taskmark/) → that board only
18
+ cd my-app-taskmark && npx taskmark
19
+ # or: npm i @taskmark/ui --save && npx taskmark serve
20
+ ```
21
+
22
+ > **`npx taskmark` only works where `@taskmark/ui` is installed.** `taskmark` is the
23
+ > *bin* name, not a package name — an unrelated `taskmark` package exists on npm, and
24
+ > npx will fetch that one instead. Use `npx @taskmark/ui` when running from a folder
25
+ > with no local install, or install the CLI once (see below).
26
+
27
+ ### Install the CLI globally
28
+
29
+ ```bash
30
+ npm install -g @taskmark/ui # published release
31
+ # from a clone of taskmark-frontend:
32
+ npm run build && npm link # local build on PATH as `taskmark`
33
+ ```
34
+
35
+ Then `taskmark` works anywhere: setup / project picker outside a board, bound board inside one.
36
+
37
+ `taskmark open` always forces the setup / picker, even when a board is nearby. Chosen master folders and the active project persist in cookies.
38
+
5
39
  ## Install and serve
6
40
 
7
41
  ```bash
8
42
  # from nested ./taskmark/ or a dedicated *-taskmark board root
9
43
  npm install @taskmark/ui --save
10
- npx taskmark serve
44
+ npx taskmark
45
+ # or: npx taskmark serve
11
46
  ```
12
47
 
13
48
  Opens [http://localhost:8275](http://localhost:8275) bound to **this** board.
@@ -24,20 +59,23 @@ Runs Next.js in development on port **8275** and refreshes the UI when board `.m
24
59
  Without a local install (once published):
25
60
 
26
61
  ```bash
27
- npx -p @taskmark/ui taskmark serve
62
+ npx @taskmark/ui
63
+ # or: npx -p @taskmark/ui taskmark
28
64
  ```
29
65
 
30
66
  | Option | Meaning |
31
67
  |--------|---------|
32
68
  | `--port` / `-p` | Listen port (default **8275**; also `PORT` / `TASKMARK_PORT`) |
33
69
  | `--board <path>` | Board or product root (sets `TASKMARK_BOARD`) |
70
+ | `--workspace` / `-w` | Force setup / multi-project mode; cannot be combined with `--board` |
34
71
  | `--no-open` | Print the URL only |
35
72
 
36
- Board resolution:
73
+ Board resolution (bound mode only):
37
74
 
38
75
  1. `--board` / `TASKMARK_BOARD`
39
76
  2. `TASKMARK_MASTER`
40
77
  3. `TASKMARK_CWD` → npm `INIT_CWD` → `process.cwd()` — nested `./taskmark/` or flat `*-taskmark` root
78
+ 4. Sibling `<parent>-taskmark` next to a product repo (multi-git workspaces)
41
79
 
42
80
  ### Nested vs flat boards
43
81
 
@@ -87,12 +125,14 @@ Favicon, webmanifest, and Open Graph assets ship in `public/` and are included i
87
125
 
88
126
  ```bash
89
127
  npm install
90
- npm run build # next standalone + dist/ for the CLI
91
- npm run serve # production standalone against cwd/env board
92
- npm run dev # taskmark dev (Next + markdown live reload) on :8275
128
+ npm run build # next standalone + dist/ for the CLI
129
+ npm run serve # production standalone against cwd/env board
130
+ npm run dev # taskmark dev (Next + markdown live reload) on :8275
131
+ npm run dev:workspace # same, but unbound — work on the setup wizard / picker
132
+ npm run open # prebuilt UI in workspace mode
93
133
  ```
94
134
 
95
- Zero-config for `npm run dev` / `serve`: `TASKMARK_BOARD` / `TASKMARK_MASTER` / cwd (see above). Cookie multi-master setup still works when unbound.
135
+ `npm run dev` / `serve` always bind to a board (sibling `taskmark-taskmark` from this repo). Bare `node ./bin/taskmark.js` uses the smart default. Use `open` / `dev:workspace` to work on the setup wizard.
96
136
 
97
137
  ## Stack
98
138
 
@@ -1,49 +1,7 @@
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"
1
+ import { BoardScreen } from "@/components/board/board-screen"
41
2
 
42
3
  export const dynamic = "force-dynamic"
43
4
 
44
- // Keep this import so `taskmark dev` can touch the token module and refresh RSC.
45
- void DEV_RELOAD_TOKEN
46
-
47
5
  type BoardPageProps = {
48
6
  searchParams: Promise<{
49
7
  view?: string | string[]
@@ -53,234 +11,7 @@ type BoardPageProps = {
53
11
  }>
54
12
  }
55
13
 
56
- function paramValue(value: string | string[] | undefined): string | null {
57
- if (typeof value === "string" && value.trim()) return value.trim()
58
- return null
59
- }
60
-
61
- function headingForView(view: ListViewMode): string {
62
- if (view === "overall") return "Epics"
63
- return LIST_VIEW_LABELS[view]
64
- }
65
-
14
+ /** Alias of `/` kept so older links and bookmarks keep working. */
66
15
  export default async function BoardPage({ searchParams }: BoardPageProps) {
67
- if (isStaticRuntime()) {
68
- const snapshot = loadSnapshotFromPublic()
69
- return (
70
- <Suspense fallback={<div className="p-8 text-sm text-muted-foreground">Loading board…</div>}>
71
- <StaticBoardApp snapshot={snapshot} />
72
- </Suspense>
73
- )
74
- }
75
-
76
- const masters = await getMasterFoldersCookie()
77
- const workspace = loadWorkspace(masters)
78
- if (workspace.projects.length === 0) {
79
- redirect("/setup")
80
- }
81
-
82
- const savedActiveId = await getActiveProjectCookie()
83
- const hideCompletedPrefs = await getHideCompletedCookies()
84
- const activeProject = resolveActiveProject(
85
- workspace.projects,
86
- // Autoconfig: prefer the bound board; ignore stale active-project cookies.
87
- workspace.autoconfig ? null : savedActiveId
88
- )
89
- if (!activeProject) {
90
- redirect("/setup")
91
- }
92
-
93
- const params = await searchParams
94
- const activeView = parseListViewMode(params.view)
95
- const selectedEpicId =
96
- activeView === "overall" ? paramValue(params.epic) : null
97
- const selectedStoryId =
98
- activeView === "overall" && selectedEpicId
99
- ? paramValue(params.story)
100
- : null
101
- const selectedItemId = paramValue(params.item)
102
-
103
- const list = parseEpicsForProject(activeProject)
104
- const epicCount = list.epics.length
105
- const errorCount = list.errors.length
106
-
107
- const selectedEpic = selectedEpicId
108
- ? list.epics.find((e) => e.id === selectedEpicId) ?? null
109
- : null
110
-
111
- const epicWorkItems =
112
- activeView === "overall" && selectedEpicId != null
113
- ? parseWorkItemsForEpic(
114
- activeProject,
115
- selectedEpicId,
116
- selectedEpic?.title ?? null
117
- )
118
- : null
119
-
120
- const selectedStory =
121
- epicWorkItems && selectedStoryId
122
- ? (epicWorkItems.rows.find(
123
- (row) => row.kind === "story" && row.id === selectedStoryId
124
- ) ?? null)
125
- : null
126
-
127
- const itemList =
128
- activeView === "overall" &&
129
- selectedEpicId != null &&
130
- selectedStory != null
131
- ? parseItemsForStory(activeProject, selectedEpicId, selectedStory.id)
132
- : null
133
-
134
- const workItemsList =
135
- activeView === "overall" || activeView === "workitems"
136
- ? parseWorkItemsViewForProject(activeProject)
137
- : null
138
-
139
- /** Match Current Speed exactly: completed task/bug leaf points only. */
140
- const countableCompletions = collectCompletedLeafPointSamples(
141
- collectMetricLeaves(activeProject)
142
- )
143
-
144
- const statusMetrics = computeProjectStatusMetrics(activeProject)
145
-
146
- return (
147
- <WorkItemSheetProvider>
148
- <div className="min-h-svh bg-[linear-gradient(180deg,_#fff_0%,_#f7f4ff_100%)]">
149
- <AppBar
150
- projects={workspace.projects}
151
- activeProjectId={activeProject.id}
152
- autoconfig={workspace.autoconfig}
153
- />
154
-
155
- <div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-4 py-8">
156
- <div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
157
- <div>
158
- <h1 className="font-head text-3xl tracking-tight">
159
- {headingForView(activeView)}
160
- </h1>
161
- <p className="mt-1 text-sm text-muted-foreground">
162
- {activeProject.name}
163
- {activeView === "overall"
164
- ? ` · ${epicCount} epic${epicCount === 1 ? "" : "s"}`
165
- : ""}
166
- {activeView === "overall" && errorCount > 0
167
- ? ` · ${errorCount} parse issue${errorCount === 1 ? "" : "s"}`
168
- : ""}
169
- {activeView === "workitems" && workItemsList
170
- ? ` · ${workItemsList.rows.length} item${workItemsList.rows.length === 1 ? "" : "s"}`
171
- : ""}
172
- {activeView === "overall" && selectedEpic
173
- ? ` · selected ${selectedEpic.id}`
174
- : activeView === "overall" && selectedEpicId
175
- ? ` · epic ${selectedEpicId} not in list`
176
- : activeView === "overall"
177
- ? " · select an epic to view work items"
178
- : ""}
179
- {activeView === "overall" && selectedStory
180
- ? ` · story ${selectedStory.id}`
181
- : activeView === "overall" && selectedStoryId
182
- ? ` · story ${selectedStoryId} not in list`
183
- : ""}
184
- </p>
185
- </div>
186
- <ListViewSwitcher
187
- activeView={activeView}
188
- selectedEpicId={selectedEpicId}
189
- selectedStoryId={selectedStory?.id ?? null}
190
- />
191
- </div>
192
-
193
- <ProjectStatusMetricsStrip metrics={statusMetrics} />
194
-
195
- {activeView === "overall" ? (
196
- <>
197
- <EpicList
198
- lists={[list]}
199
- selectedEpicId={selectedEpicId}
200
- countableCompletions={countableCompletions}
201
- initialHideCompleted={hideCompletedPrefs.epics}
202
- />
203
-
204
- {epicWorkItems ? (
205
- <div className="flex flex-col gap-3">
206
- <div className="flex items-baseline justify-between gap-3">
207
- <h2 className="font-head text-2xl tracking-tight">
208
- Work items for{" "}
209
- {epicWorkItems.epicTitle ?? epicWorkItems.epicId}
210
- </h2>
211
- <Link
212
- href={boardHref({
213
- view: "overall",
214
- item: selectedItemId,
215
- })}
216
- className="text-sm text-muted-foreground underline-offset-2 hover:underline"
217
- >
218
- Clear selection
219
- </Link>
220
- </div>
221
- <p className="text-sm text-muted-foreground">
222
- {epicWorkItems.rows.length} work item
223
- {epicWorkItems.rows.length === 1 ? "" : "s"}
224
- {epicWorkItems.errors.length > 0
225
- ? ` · ${epicWorkItems.errors.length} issue${epicWorkItems.errors.length === 1 ? "" : "s"}`
226
- : ""}
227
- {!selectedStory
228
- ? " · select a story to view its sub tasks"
229
- : ""}
230
- </p>
231
- <OverallWorkItemsList
232
- list={epicWorkItems}
233
- selectedStoryId={selectedStory?.id ?? null}
234
- countableCompletions={countableCompletions}
235
- initialHideCompleted={hideCompletedPrefs.overallWorkItems}
236
- />
237
- </div>
238
- ) : null}
239
-
240
- {itemList && selectedStory ? (
241
- <div className="flex flex-col gap-3">
242
- <div className="flex items-baseline justify-between gap-3">
243
- <h2 className="font-head text-2xl tracking-tight">
244
- Sub Tasks for{" "}
245
- {itemList.storyTitle ?? itemList.storyId}
246
- </h2>
247
- <Link
248
- href={boardHref({
249
- view: "overall",
250
- epic: itemList.epicId,
251
- item: selectedItemId,
252
- })}
253
- className="text-sm text-muted-foreground underline-offset-2 hover:underline"
254
- >
255
- Clear story
256
- </Link>
257
- </div>
258
- <p className="text-sm text-muted-foreground">
259
- {itemList.items.length} sub task
260
- {itemList.items.length === 1 ? "" : "s"}
261
- {itemList.errors.length > 0
262
- ? ` · ${itemList.errors.length} issue${itemList.errors.length === 1 ? "" : "s"}`
263
- : ""}
264
- </p>
265
- <TaskList
266
- list={itemList}
267
- countableCompletions={countableCompletions}
268
- initialHideCompleted={hideCompletedPrefs.tasks}
269
- />
270
- </div>
271
- ) : null}
272
- </>
273
- ) : null}
274
-
275
- {activeView === "workitems" && workItemsList ? (
276
- <WorkItemsList
277
- list={workItemsList}
278
- countableCompletions={countableCompletions}
279
- initialHideCompleted={hideCompletedPrefs.workItems}
280
- />
281
- ) : null}
282
- </div>
283
- </div>
284
- </WorkItemSheetProvider>
285
- )
16
+ return <BoardScreen searchParams={searchParams} />
286
17
  }
package/app/page.tsx CHANGED
@@ -1,17 +1,17 @@
1
- import { redirect } from "next/navigation"
1
+ import { BoardScreen } from "@/components/board/board-screen"
2
2
 
3
- import { getMasterFoldersCookie } from "@/lib/taskmark/cookies"
4
- import { isStaticRuntime } from "@/lib/taskmark/static-mode"
5
- import { loadWorkspace } from "@/lib/taskmark/workspace"
3
+ export const dynamic = "force-dynamic"
6
4
 
7
- export default async function HomePage() {
8
- if (isStaticRuntime()) {
9
- redirect("/board")
10
- }
11
- const masters = await getMasterFoldersCookie()
12
- const workspace = loadWorkspace(masters)
13
- if (workspace.projects.length > 0) {
14
- redirect("/board")
15
- }
16
- redirect("/setup")
5
+ type HomePageProps = {
6
+ searchParams: Promise<{
7
+ view?: string | string[]
8
+ epic?: string | string[]
9
+ story?: string | string[]
10
+ item?: string | string[]
11
+ }>
12
+ }
13
+
14
+ /** The board is the default view; BoardScreen sends unbound setups to /setup. */
15
+ export default async function HomePage({ searchParams }: HomePageProps) {
16
+ return <BoardScreen searchParams={searchParams} />
17
17
  }
@@ -64,7 +64,7 @@ export async function saveMasterFolder(
64
64
  await setActiveProjectCookie(preferred.id)
65
65
  }
66
66
 
67
- redirect("/board")
67
+ redirect("/")
68
68
  }
69
69
 
70
70
  export type PickMasterFolderState = {
@@ -89,7 +89,7 @@ export async function openAddProject(): Promise<void> {
89
89
  "@/lib/taskmark/autoconfig"
90
90
  )
91
91
  if (resolveAutoconfigWorkspace()?.projects.length) {
92
- redirect("/board")
92
+ redirect("/")
93
93
  }
94
94
  redirect("/setup?mode=add")
95
95
  }
@@ -100,8 +100,8 @@ export async function selectActiveProject(formData: FormData): Promise<void> {
100
100
  return
101
101
  }
102
102
  await setActiveProjectCookie(projectId)
103
- revalidatePath("/board")
104
- redirect("/board")
103
+ revalidatePath("/")
104
+ redirect("/")
105
105
  }
106
106
 
107
107
  /** Optional: clear everything and start over (not shown in app bar by default). */
@@ -6,26 +6,34 @@ import { SetupWizard } from "@/components/setup/setup-wizard"
6
6
  import { resolveAutoconfigWorkspace } from "@/lib/taskmark/autoconfig"
7
7
  import { getMasterFoldersCookie } from "@/lib/taskmark/cookies"
8
8
  import { isStaticRuntime } from "@/lib/taskmark/static-mode"
9
+ import { workspaceStartPath } from "@/lib/taskmark/workspace-mode"
9
10
 
10
11
  type SetupPageProps = {
11
12
  searchParams?: Promise<{ mode?: string }>
12
13
  }
13
14
 
15
+ // Depends on runtime env (workspace vs bound) and cookies — never prerender in
16
+ // server mode, or a build-time redirect gets baked in and loops against `/`.
17
+ // The static build rewrites this to force-static (see scripts/build-static.mjs).
18
+ export const dynamic = "force-dynamic"
19
+
14
20
  export default async function SetupPage({ searchParams }: SetupPageProps) {
15
21
  if (isStaticRuntime()) {
16
- redirect("/board")
22
+ redirect("/")
17
23
  }
18
24
 
19
25
  const auto = resolveAutoconfigWorkspace()
20
26
  if (auto && auto.projects.length > 0) {
21
27
  // Zero-config bind — setup wizard is not needed.
22
- redirect("/board")
28
+ redirect("/")
23
29
  }
24
30
 
25
31
  const params = searchParams ? await searchParams : {}
26
32
  const masters = await getMasterFoldersCookie()
27
33
  const hasProjects = masters.length > 0
28
34
  const adding = params.mode === "add" && hasProjects
35
+ // First run: offer the directory `taskmark` was launched from as the master.
36
+ const suggestedPath = adding || hasProjects ? "" : workspaceStartPath()
29
37
 
30
38
  return (
31
39
  <div className="relative flex min-h-svh flex-col items-center justify-center px-4 py-16">
@@ -50,13 +58,13 @@ export default async function SetupPage({ searchParams }: SetupPageProps) {
50
58
  </p>
51
59
  </div>
52
60
  <SetupWizard
53
- initialPath=""
61
+ initialPath={suggestedPath}
54
62
  adding={adding}
55
63
  hasExistingProjects={hasProjects}
56
64
  />
57
65
  {adding ? (
58
66
  <p className="text-sm text-muted-foreground">
59
- <Link href="/board" className="underline underline-offset-2">
67
+ <Link href="/" className="underline underline-offset-2">
60
68
  Cancel and return to board
61
69
  </Link>
62
70
  </p>
@@ -48,6 +48,50 @@ export function resolveBoardAtPath(candidatePath) {
48
48
  return null
49
49
  }
50
50
 
51
+ /**
52
+ * Multi-git layout: product repo beside a dedicated `<common>-taskmark` board.
53
+ * Prefer `<parentDirBasename>-taskmark`, else a unique `*-taskmark` sibling,
54
+ * else a sibling whose board prefix matches the product repo name.
55
+ */
56
+ export function resolveSiblingDedicatedBoard(fromDir) {
57
+ const resolved = path.resolve(fromDir)
58
+ const parent = path.dirname(resolved)
59
+ const parentName = path.basename(parent)
60
+ const preferred = path.join(parent, `${parentName}-taskmark`)
61
+ if (
62
+ isDedicatedBoardRepoName(path.basename(preferred)) &&
63
+ hasBoardMarkers(preferred)
64
+ ) {
65
+ return preferred
66
+ }
67
+
68
+ let entries
69
+ try {
70
+ entries = fs.readdirSync(parent, { withFileTypes: true })
71
+ } catch {
72
+ return null
73
+ }
74
+
75
+ const boards = []
76
+ for (const entry of entries) {
77
+ if (!entry.isDirectory() || !isDedicatedBoardRepoName(entry.name)) continue
78
+ const full = path.join(parent, entry.name)
79
+ if (hasBoardMarkers(full)) boards.push(full)
80
+ }
81
+ if (boards.length === 0) return null
82
+ if (boards.length === 1) return boards[0]
83
+
84
+ const cwdBase = path.basename(resolved)
85
+ const parts = cwdBase.split("-").filter(Boolean)
86
+ for (let i = parts.length - 1; i >= 1; i--) {
87
+ const prefix = parts.slice(0, i).join("-")
88
+ const hit = boards.find((b) => path.basename(b) === `${prefix}-taskmark`)
89
+ if (hit) return hit
90
+ }
91
+
92
+ return null
93
+ }
94
+
51
95
  function cwdCandidates() {
52
96
  const out = []
53
97
  const seen = new Set()
@@ -74,7 +118,7 @@ function cwdCandidates() {
74
118
 
75
119
  /**
76
120
  * Same precedence as the Next app autoconfig:
77
- * TASKMARK_BOARD → TASKMARK_MASTER (as board-or-master) → cwd.
121
+ * TASKMARK_BOARD → TASKMARK_MASTER (as board-or-master) → cwd → sibling *-taskmark.
78
122
  * @returns {{ boardPath: string, source: string } | null}
79
123
  */
80
124
  export function resolveServeBoard(explicitBoard) {
@@ -104,5 +148,10 @@ export function resolveServeBoard(explicitBoard) {
104
148
  if (board) return { boardPath: board, source: "cwd" }
105
149
  }
106
150
 
151
+ for (const candidate of cwdCandidates()) {
152
+ const sibling = resolveSiblingDedicatedBoard(candidate)
153
+ if (sibling) return { boardPath: sibling, source: "sibling" }
154
+ }
155
+
107
156
  return null
108
157
  }