@taskmark/ui 0.2.2 → 0.2.5

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 (209) hide show
  1. package/README.md +23 -7
  2. package/app/api/taskmark-reload/route.ts +103 -0
  3. package/app/board/page.tsx +6 -4
  4. package/app/globals.css +17 -1
  5. package/app/layout.tsx +2 -0
  6. package/bin/lib/static-preview.mjs +110 -0
  7. package/bin/lib/watch-board-markdown.mjs +65 -0
  8. package/bin/taskmark.js +254 -46
  9. package/components/board/app-bar.tsx +31 -34
  10. package/components/board/board-dev-reloader.tsx +49 -0
  11. package/components/board/epic-list.tsx +2 -4
  12. package/components/board/markdown-content.tsx +1 -2
  13. package/components/board/overall-work-items-list.tsx +2 -1
  14. package/components/board/static-board-app.tsx +0 -1
  15. package/components/board/story-list.tsx +2 -1
  16. package/components/board/task-list.tsx +2 -1
  17. package/components/board/work-item-sheet.tsx +15 -2
  18. package/components/board/work-items-list.tsx +2 -2
  19. package/components/setup/setup-wizard.tsx +2 -6
  20. package/dist/prod-next/BUILD_ID +1 -0
  21. package/dist/{standalone/.next → prod-next}/app-path-routes-manifest.json +3 -2
  22. package/dist/{standalone/.next → prod-next}/build-manifest.json +3 -3
  23. package/dist/prod-next/export-marker.json +6 -0
  24. package/dist/prod-next/images-manifest.json +68 -0
  25. package/dist/prod-next/next-minimal-server.js.nft.json +1 -0
  26. package/dist/prod-next/next-server.js.nft.json +1 -0
  27. package/dist/{standalone/.next → prod-next}/prerender-manifest.json +3 -3
  28. package/dist/{standalone/.next → prod-next}/react-loadable-manifest.json +2 -2
  29. package/dist/prod-next/required-server-files.js +337 -0
  30. package/dist/{standalone/.next → prod-next}/required-server-files.json +0 -7
  31. package/dist/{standalone/.next → prod-next}/routes-manifest.json +6 -0
  32. package/dist/prod-next/server/app/_global-error/page.js +32 -0
  33. package/dist/prod-next/server/app/_global-error/page.js.nft.json +1 -0
  34. package/dist/prod-next/server/app/_global-error/page_client-reference-manifest.js +1 -0
  35. package/dist/{standalone/.next → prod-next}/server/app/_global-error.html +1 -1
  36. package/dist/{standalone/.next → prod-next}/server/app/_global-error.rsc +1 -1
  37. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_full.segment.rsc +1 -1
  38. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  39. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  40. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_head.segment.rsc +1 -1
  41. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_index.segment.rsc +1 -1
  42. package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  43. package/dist/prod-next/server/app/_not-found/page.js +2 -0
  44. package/dist/prod-next/server/app/_not-found/page.js.nft.json +1 -0
  45. package/dist/prod-next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  46. package/dist/prod-next/server/app/_not-found.html +1 -0
  47. package/dist/prod-next/server/app/_not-found.rsc +21 -0
  48. package/dist/prod-next/server/app/_not-found.segments/_full.segment.rsc +21 -0
  49. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_head.segment.rsc +1 -1
  50. package/dist/prod-next/server/app/_not-found.segments/_index.segment.rsc +8 -0
  51. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  52. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  53. package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  54. package/dist/prod-next/server/app/api/taskmark-reload/route.js +1 -0
  55. package/dist/prod-next/server/app/api/taskmark-reload/route.js.nft.json +1 -0
  56. package/dist/prod-next/server/app/api/taskmark-reload/route_client-reference-manifest.js +1 -0
  57. package/dist/prod-next/server/app/board/page.js +50 -0
  58. package/dist/prod-next/server/app/board/page.js.nft.json +1 -0
  59. package/dist/prod-next/server/app/board/page_client-reference-manifest.js +1 -0
  60. package/dist/{standalone/.next → prod-next}/server/app/favicon.ico/route.js +1 -1
  61. package/dist/prod-next/server/app/favicon.ico/route.js.nft.json +1 -0
  62. package/dist/prod-next/server/app/page.js +2 -0
  63. package/dist/prod-next/server/app/page.js.nft.json +1 -0
  64. package/dist/prod-next/server/app/page_client-reference-manifest.js +1 -0
  65. package/dist/prod-next/server/app/setup/page.js +2 -0
  66. package/dist/prod-next/server/app/setup/page.js.nft.json +1 -0
  67. package/dist/prod-next/server/app/setup/page_client-reference-manifest.js +1 -0
  68. package/dist/{standalone/.next → prod-next}/server/app-paths-manifest.json +3 -2
  69. package/dist/{standalone/.next → prod-next}/server/chunks/319.js +1 -1
  70. package/dist/{standalone/.next → prod-next}/server/chunks/490.js +1 -1
  71. package/dist/prod-next/server/chunks/57.js +19 -0
  72. package/dist/prod-next/server/chunks/616.js +1 -0
  73. package/dist/prod-next/server/chunks/708.js +1 -0
  74. package/dist/prod-next/server/chunks/724.js +1 -0
  75. package/dist/{standalone/.next/server/chunks/445.js → prod-next/server/chunks/741.js} +2 -2
  76. package/dist/prod-next/server/functions-config-manifest.json +6 -0
  77. package/dist/prod-next/server/interception-route-rewrite-manifest.js +1 -0
  78. package/dist/{standalone/.next → prod-next}/server/middleware-build-manifest.js +1 -1
  79. package/dist/prod-next/server/middleware-react-loadable-manifest.js +1 -0
  80. package/dist/prod-next/server/pages/404.html +1 -0
  81. package/dist/{standalone/.next → prod-next}/server/pages/500.html +1 -1
  82. package/dist/prod-next/server/server-reference-manifest.js +1 -0
  83. package/dist/prod-next/server/server-reference-manifest.json +1 -0
  84. package/dist/prod-next/static/chunks/51.e9139a9ea6eb36b9.js +1 -0
  85. package/dist/{standalone/.next/static/chunks/781-f9f201ddc8c8591a.js → prod-next/static/chunks/613-6f794154d28bb75a.js} +1 -1
  86. package/dist/prod-next/static/chunks/app/_global-error/page-d34451c04262dd7d.js +1 -0
  87. package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-d34451c04262dd7d.js +1 -0
  88. package/dist/prod-next/static/chunks/app/board/page-cd1d9be0c0127225.js +1 -0
  89. package/dist/prod-next/static/chunks/app/layout-856e8a282e69e7d0.js +1 -0
  90. package/dist/prod-next/static/chunks/app/page-d34451c04262dd7d.js +1 -0
  91. package/dist/prod-next/static/chunks/app/setup/page-c3003359e8becc84.js +1 -0
  92. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-d34451c04262dd7d.js +1 -0
  93. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-d34451c04262dd7d.js +1 -0
  94. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-d34451c04262dd7d.js +1 -0
  95. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-d34451c04262dd7d.js +1 -0
  96. package/dist/prod-next/static/chunks/webpack-bfe8992962004e2b.js +1 -0
  97. package/dist/prod-next/static/css/2e6ec4c80894e3c0.css +1 -0
  98. package/dist/prod-next/static/wbcgYS3Jw87KqhnA9ttOL/_buildManifest.js +1 -0
  99. package/dist/public/taskmark-snapshot.json +1 -0
  100. package/lib/display-path.ts +6 -0
  101. package/lib/taskmark/dev-reload-token.ts +5 -0
  102. package/lib/taskmark/project-label.ts +3 -2
  103. package/lib/taskmark/validate.ts +4 -4
  104. package/next.config.ts +3 -8
  105. package/package.json +6 -4
  106. package/public/taskmark-snapshot.json +1 -1
  107. package/scripts/build-static.mjs +10 -1
  108. package/scripts/prepare-standalone.mjs +53 -20
  109. package/dist/standalone/.next/BUILD_ID +0 -1
  110. package/dist/standalone/.next/server/app/_global-error/page.js +0 -32
  111. package/dist/standalone/.next/server/app/_global-error/page.js.nft.json +0 -1
  112. package/dist/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +0 -1
  113. package/dist/standalone/.next/server/app/_not-found/page.js +0 -2
  114. package/dist/standalone/.next/server/app/_not-found/page.js.nft.json +0 -1
  115. package/dist/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
  116. package/dist/standalone/.next/server/app/_not-found.html +0 -1
  117. package/dist/standalone/.next/server/app/_not-found.rsc +0 -20
  118. package/dist/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +0 -20
  119. package/dist/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +0 -7
  120. package/dist/standalone/.next/server/app/board/page.js +0 -50
  121. package/dist/standalone/.next/server/app/board/page.js.nft.json +0 -1
  122. package/dist/standalone/.next/server/app/board/page_client-reference-manifest.js +0 -1
  123. package/dist/standalone/.next/server/app/favicon.ico/route.js.nft.json +0 -1
  124. package/dist/standalone/.next/server/app/page.js +0 -2
  125. package/dist/standalone/.next/server/app/page.js.nft.json +0 -1
  126. package/dist/standalone/.next/server/app/page_client-reference-manifest.js +0 -1
  127. package/dist/standalone/.next/server/app/setup/page.js +0 -2
  128. package/dist/standalone/.next/server/app/setup/page.js.nft.json +0 -1
  129. package/dist/standalone/.next/server/app/setup/page_client-reference-manifest.js +0 -1
  130. package/dist/standalone/.next/server/chunks/57.js +0 -19
  131. package/dist/standalone/.next/server/chunks/570.js +0 -1
  132. package/dist/standalone/.next/server/chunks/600.js +0 -1
  133. package/dist/standalone/.next/server/chunks/666.js +0 -1
  134. package/dist/standalone/.next/server/functions-config-manifest.json +0 -4
  135. package/dist/standalone/.next/server/middleware-react-loadable-manifest.js +0 -1
  136. package/dist/standalone/.next/server/pages/404.html +0 -1
  137. package/dist/standalone/.next/server/server-reference-manifest.js +0 -1
  138. package/dist/standalone/.next/server/server-reference-manifest.json +0 -1
  139. package/dist/standalone/.next/static/O83kNzYUmJlkfw7SS-SsU/_buildManifest.js +0 -1
  140. package/dist/standalone/.next/static/chunks/826.92750d8ed3a55429.js +0 -1
  141. package/dist/standalone/.next/static/chunks/app/_global-error/page-82d456ade9f47021.js +0 -1
  142. package/dist/standalone/.next/static/chunks/app/board/page-f609a854d76933c2.js +0 -1
  143. package/dist/standalone/.next/static/chunks/app/layout-3745e06d87691845.js +0 -1
  144. package/dist/standalone/.next/static/chunks/app/page-82d456ade9f47021.js +0 -1
  145. package/dist/standalone/.next/static/chunks/app/setup/page-acaffda2083bf4d0.js +0 -1
  146. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/app-error-82d456ade9f47021.js +0 -1
  147. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/forbidden-82d456ade9f47021.js +0 -1
  148. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/not-found-82d456ade9f47021.js +0 -1
  149. package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/unauthorized-82d456ade9f47021.js +0 -1
  150. package/dist/standalone/.next/static/chunks/webpack-f181688592aeb1f6.js +0 -1
  151. package/dist/standalone/.next/static/css/7af268deac5c5edb.css +0 -1
  152. package/dist/standalone/node_modules/next/dist/compiled/jest-worker/processChild.js +0 -1
  153. package/dist/standalone/node_modules/next/dist/compiled/jest-worker/threadChild.js +0 -1
  154. package/dist/standalone/node_modules/next/dist/server/lib/start-server.js +0 -494
  155. package/dist/standalone/node_modules/next/dist/server/next-server.js +0 -1429
  156. package/dist/standalone/node_modules/next/dist/server/next.js +0 -455
  157. package/dist/standalone/node_modules/next/dist/server/require-hook.js +0 -82
  158. package/dist/standalone/node_modules/next/dist/server/route-modules/app-page/module.compiled.js +0 -36
  159. package/dist/standalone/node_modules/next/dist/server/route-modules/pages/module.compiled.js +0 -20
  160. package/dist/standalone/node_modules/styled-jsx/index.js +0 -1
  161. package/dist/standalone/node_modules/styled-jsx/package.json +0 -132
  162. package/dist/standalone/node_modules/styled-jsx/style.js +0 -1
  163. package/dist/standalone/package.json +0 -76
  164. package/dist/standalone/public/taskmark-snapshot.json +0 -1
  165. package/dist/standalone/server.js +0 -44
  166. /package/dist/{standalone/.next → prod-next}/package.json +0 -0
  167. /package/dist/{standalone/.next → prod-next}/server/app/_global-error.meta +0 -0
  168. /package/dist/{standalone/.next → prod-next}/server/app/_not-found.meta +0 -0
  169. /package/dist/{standalone/.next → prod-next}/server/app/favicon.ico.body +0 -0
  170. /package/dist/{standalone/.next → prod-next}/server/app/favicon.ico.meta +0 -0
  171. /package/dist/{standalone/.next → prod-next}/server/chunks/318.js +0 -0
  172. /package/dist/{standalone/.next → prod-next}/server/middleware-manifest.json +0 -0
  173. /package/dist/{standalone/.next → prod-next}/server/next-font-manifest.js +0 -0
  174. /package/dist/{standalone/.next → prod-next}/server/next-font-manifest.json +0 -0
  175. /package/dist/{standalone/.next → prod-next}/server/pages-manifest.json +0 -0
  176. /package/dist/{standalone/.next → prod-next}/server/prefetch-hints.json +0 -0
  177. /package/dist/{standalone/.next → prod-next}/server/webpack-runtime.js +0 -0
  178. /package/dist/{standalone/.next → prod-next}/static/chunks/4bd1b696-deb4a0a1da1923b0.js +0 -0
  179. /package/dist/{standalone/.next → prod-next}/static/chunks/669-8103441c67b185da.js +0 -0
  180. /package/dist/{standalone/.next → prod-next}/static/chunks/701-df3ee5f1cf837b1a.js +0 -0
  181. /package/dist/{standalone/.next → prod-next}/static/chunks/794-25668765560cb197.js +0 -0
  182. /package/dist/{standalone/.next → prod-next}/static/chunks/app/_not-found/page-0b3785af737e7968.js +0 -0
  183. /package/dist/{standalone/.next → prod-next}/static/chunks/framework-b29f9083d3cda3b2.js +0 -0
  184. /package/dist/{standalone/.next → prod-next}/static/chunks/main-app-bfb36455d24b3e5a.js +0 -0
  185. /package/dist/{standalone/.next → prod-next}/static/chunks/main-bb33c46a9c25dee8.js +0 -0
  186. /package/dist/{standalone/.next → prod-next}/static/chunks/next/dist/client/components/builtin/global-error-dc53ab29e0491fe4.js +0 -0
  187. /package/dist/{standalone/.next → prod-next}/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  188. /package/dist/{standalone/.next → prod-next}/static/css/c593627ceef17a4b.css +0 -0
  189. /package/dist/{standalone/.next → prod-next}/static/media/36966cca54120369-s.p.woff2 +0 -0
  190. /package/dist/{standalone/.next → prod-next}/static/media/982ceffe7b733b3b-s.p.woff2 +0 -0
  191. /package/dist/{standalone/.next → prod-next}/static/media/b7387a63dd068245-s.woff2 +0 -0
  192. /package/dist/{standalone/.next → prod-next}/static/media/b8bfb68ab9ab617c-s.woff2 +0 -0
  193. /package/dist/{standalone/.next → prod-next}/static/media/e1aab0933260df4d-s.woff2 +0 -0
  194. /package/dist/{standalone/.next/static/O83kNzYUmJlkfw7SS-SsU → prod-next/static/wbcgYS3Jw87KqhnA9ttOL}/_ssgManifest.js +0 -0
  195. /package/dist/{standalone/public → public}/apple-touch-icon.png +0 -0
  196. /package/dist/{standalone/public → public}/favicon-96x96.png +0 -0
  197. /package/dist/{standalone/public → public}/favicon.ico +0 -0
  198. /package/dist/{standalone/public → public}/favicon.svg +0 -0
  199. /package/dist/{standalone/public → public}/file.svg +0 -0
  200. /package/dist/{standalone/public → public}/globe.svg +0 -0
  201. /package/dist/{standalone/public → public}/next.svg +0 -0
  202. /package/dist/{standalone/public → public}/og/og-default.png +0 -0
  203. /package/dist/{standalone/public → public}/site.webmanifest +0 -0
  204. /package/dist/{standalone/public → public}/tm_dark.png +0 -0
  205. /package/dist/{standalone/public → public}/tm_light.png +0 -0
  206. /package/dist/{standalone/public → public}/vercel.svg +0 -0
  207. /package/dist/{standalone/public → public}/web-app-manifest-192x192.png +0 -0
  208. /package/dist/{standalone/public → public}/web-app-manifest-512x512.png +0 -0
  209. /package/dist/{standalone/public → public}/window.svg +0 -0
package/README.md CHANGED
@@ -12,6 +12,15 @@ npx taskmark serve
12
12
 
13
13
  Opens [http://localhost:8275](http://localhost:8275) bound to **this** board.
14
14
 
15
+ ### Development (live board reload)
16
+
17
+ ```bash
18
+ npx taskmark dev
19
+ # or: npm run dev / yarn dev
20
+ ```
21
+
22
+ Runs Next.js in development on port **8275** and refreshes the UI when board `.md` files change.
23
+
15
24
  Without a local install (once published):
16
25
 
17
26
  ```bash
@@ -49,21 +58,27 @@ You do **not** need to clone `taskmark-frontend`.
49
58
  "scripts": {
50
59
  "start": "taskmark serve --no-open",
51
60
  "serve": "taskmark serve",
52
- "build": "taskmark build --board . --out out"
61
+ "dev": "taskmark dev",
62
+ "build": "taskmark build --board . --out out",
63
+ "preview": "taskmark preview"
53
64
  },
54
65
  "dependencies": {
55
- "@taskmark/ui": "^0.2.0"
66
+ "@taskmark/ui": "^0.2.5"
56
67
  }
57
68
  }
58
69
  ```
59
70
 
60
- Then `npm install && npm run serve` (or `npm start`). For **Vercel**, use `npm run build` (static HTML in `out/`) — see the board-ui-stub `vercel.json`.
71
+ Then `npm install && npm run serve` (or `npm start`). Use `npm run dev` while editing board markdown. For **Vercel**, use `npm run build` (static HTML in `out/`) — see the board-ui-stub `vercel.json`.
61
72
 
62
73
  ### Static production build
63
74
 
64
75
  ```bash
65
76
  npx taskmark build --board .
66
77
  # → <board>/out (HTML + assets + taskmark-snapshot.json)
78
+
79
+ npm run preview
80
+ # or: npx taskmark preview
81
+ # serves <board>/out locally (default port 8275)
67
82
  ```
68
83
 
69
84
  Favicon, webmanifest, and Open Graph assets ship in `public/` and are included in both standalone (`taskmark serve`) and static (`taskmark build`) outputs.
@@ -73,14 +88,15 @@ Favicon, webmanifest, and Open Graph assets ship in `public/` and are included i
73
88
  ```bash
74
89
  npm install
75
90
  npm run build # next standalone + dist/ for the CLI
76
- npm run serve # local bin against cwd/env board
77
- npm run dev # Next dev server on :3000 (setup wizard unless auto-bound)
91
+ npm run serve # production standalone against cwd/env board
92
+ npm run dev # taskmark dev (Next + markdown live reload) on :8275
78
93
  ```
79
94
 
80
- Zero-config for `npm run dev`: `TASKMARK_BOARD` / `TASKMARK_MASTER` / cwd (see above). Cookie multi-master setup still works when unbound.
95
+ Zero-config for `npm run dev` / `serve`: `TASKMARK_BOARD` / `TASKMARK_MASTER` / cwd (see above). Cookie multi-master setup still works when unbound.
81
96
 
82
97
  ## Stack
83
98
 
84
- - Next.js standalone server packaged for `taskmark serve`
99
+ - Next.js production server packaged for `taskmark serve` (`next start` + slim `dist/prod-next`)
100
+ - `taskmark dev` for live board markdown reload
85
101
  - Tailwind CSS v4
86
102
  - Local markdown boards only (no hosted sync)
@@ -0,0 +1,103 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+
4
+ import { cwdCandidates } from "@/lib/taskmark/autoconfig"
5
+ import { resolveBoardAtPath } from "@/lib/taskmark/discover"
6
+
7
+ export const dynamic = "force-dynamic"
8
+ export const runtime = "nodejs"
9
+
10
+ function resolveBoardRoot(): string | null {
11
+ const env = process.env.TASKMARK_BOARD?.trim()
12
+ if (env) {
13
+ const board = resolveBoardAtPath(path.resolve(env))
14
+ if (board) return board
15
+ }
16
+ for (const candidate of cwdCandidates()) {
17
+ const board = resolveBoardAtPath(candidate)
18
+ if (board) return board
19
+ }
20
+ return null
21
+ }
22
+
23
+ function walkMarkdownFiles(root: string, out: string[] = []): string[] {
24
+ let entries: fs.Dirent[]
25
+ try {
26
+ entries = fs.readdirSync(root, { withFileTypes: true })
27
+ } catch {
28
+ return out
29
+ }
30
+ for (const entry of entries) {
31
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "out") {
32
+ continue
33
+ }
34
+ const full = path.join(root, entry.name)
35
+ if (entry.isDirectory()) {
36
+ walkMarkdownFiles(full, out)
37
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
38
+ out.push(full)
39
+ }
40
+ }
41
+ return out
42
+ }
43
+
44
+ function latestMarkdownMtime(boardRoot: string): number {
45
+ let latest = 0
46
+ for (const file of walkMarkdownFiles(boardRoot)) {
47
+ try {
48
+ const mtime = fs.statSync(file).mtimeMs
49
+ if (mtime > latest) latest = mtime
50
+ } catch {
51
+ // ignore transient deletes
52
+ }
53
+ }
54
+ return latest
55
+ }
56
+
57
+ /**
58
+ * SSE stream that emits `reload` when any board `.md` mtime changes.
59
+ * Used only by `taskmark dev` / Next development mode.
60
+ */
61
+ export async function GET() {
62
+ if (process.env.NODE_ENV !== "development") {
63
+ return new Response("Not available", { status: 404 })
64
+ }
65
+
66
+ const boardRoot = resolveBoardRoot()
67
+ if (!boardRoot) {
68
+ return new Response("No board", { status: 404 })
69
+ }
70
+
71
+ const encoder = new TextEncoder()
72
+ let last = latestMarkdownMtime(boardRoot)
73
+ let interval: ReturnType<typeof setInterval> | null = null
74
+
75
+ const stream = new ReadableStream({
76
+ start(controller) {
77
+ const send = (event: string, data: string) => {
78
+ controller.enqueue(encoder.encode(`event: ${event}\ndata: ${data}\n\n`))
79
+ }
80
+ send("hello", String(Date.now()))
81
+ interval = setInterval(() => {
82
+ const next = latestMarkdownMtime(boardRoot)
83
+ if (next > last) {
84
+ last = next
85
+ send("reload", String(Date.now()))
86
+ } else {
87
+ send("ping", String(Date.now()))
88
+ }
89
+ }, 750)
90
+ },
91
+ cancel() {
92
+ if (interval) clearInterval(interval)
93
+ },
94
+ })
95
+
96
+ return new Response(stream, {
97
+ headers: {
98
+ "Content-Type": "text/event-stream; charset=utf-8",
99
+ "Cache-Control": "no-cache, no-transform",
100
+ Connection: "keep-alive",
101
+ },
102
+ })
103
+ }
@@ -23,6 +23,7 @@ import {
23
23
  parseListViewMode,
24
24
  type ListViewMode,
25
25
  } from "@/lib/taskmark/list-view-mode"
26
+ import { DEV_RELOAD_TOKEN } from "@/lib/taskmark/dev-reload-token"
26
27
  import { loadSnapshotFromPublic } from "@/lib/taskmark/load-snapshot-server"
27
28
  import { parseEpicsForProject } from "@/lib/taskmark/parse-epics"
28
29
  import {
@@ -38,6 +39,11 @@ import {
38
39
  import { isStaticRuntime } from "@/lib/taskmark/static-mode"
39
40
  import { loadWorkspace } from "@/lib/taskmark/workspace"
40
41
 
42
+ export const dynamic = "force-dynamic"
43
+
44
+ // Keep this import so `taskmark dev` can touch the token module and refresh RSC.
45
+ void DEV_RELOAD_TOKEN
46
+
41
47
  type BoardPageProps = {
42
48
  searchParams: Promise<{
43
49
  view?: string | string[]
@@ -143,7 +149,6 @@ export default async function BoardPage({ searchParams }: BoardPageProps) {
143
149
  <AppBar
144
150
  projects={workspace.projects}
145
151
  activeProjectId={activeProject.id}
146
- masterCount={workspace.masters.length}
147
152
  autoconfig={workspace.autoconfig}
148
153
  />
149
154
 
@@ -177,9 +182,6 @@ export default async function BoardPage({ searchParams }: BoardPageProps) {
177
182
  ? ` · story ${selectedStoryId} not in list`
178
183
  : ""}
179
184
  </p>
180
- <p className="mt-1 font-mono text-xs text-muted-foreground">
181
- {activeProject.boardPath}
182
- </p>
183
185
  </div>
184
186
  <ListViewSwitcher
185
187
  activeView={activeView}
package/app/globals.css CHANGED
@@ -1,4 +1,11 @@
1
- @import "tailwindcss";
1
+ @import "tailwindcss" source(none);
2
+
3
+ /* Explicit sources only — staged builds live under a board and must not scan board markdown. */
4
+ @source "../app";
5
+ @source "../components";
6
+ @source "../hooks";
7
+ @source "../lib";
8
+ @source "../bin";
2
9
 
3
10
  @custom-variant dark (&:is(.dark *));
4
11
 
@@ -89,6 +96,15 @@
89
96
  --shadow-2xl: 20px 20px 0 2px var(--shadow-color);
90
97
  }
91
98
 
99
+ /* Markdown body: reset outer margins without Tailwind arbitrary :first-child variants
100
+ (those trigger Lightning CSS optimize warnings when scanned from board docs). */
101
+ .tm-md > :first-child {
102
+ margin-top: 0;
103
+ }
104
+ .tm-md > :last-child {
105
+ margin-bottom: 0;
106
+ }
107
+
92
108
  .dark {
93
109
  --background: #0f0f0f;
94
110
  --foreground: #fafafa;
package/app/layout.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Metadata, Viewport } from "next"
2
2
  import { Archivo_Black, Space_Grotesk } from "next/font/google"
3
+ import { BoardDevReloader } from "@/components/board/board-dev-reloader"
3
4
  import { Toaster } from "@/components/ui/sonner"
4
5
  import { SITE } from "@/lib/site"
5
6
  import "./globals.css"
@@ -80,6 +81,7 @@ export default function RootLayout({
80
81
  className={`${archivoBlack.variable} ${spaceGrotesk.variable} font-sans antialiased`}
81
82
  >
82
83
  <Toaster />
84
+ <BoardDevReloader />
83
85
  {children}
84
86
  </body>
85
87
  </html>
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Minimal static file server for Next `output: 'export'` sites (board/out).
3
+ */
4
+ import fs from "node:fs"
5
+ import http from "node:http"
6
+ import path from "node:path"
7
+ import { fileURLToPath } from "node:url"
8
+
9
+ const MIME = {
10
+ ".html": "text/html; charset=utf-8",
11
+ ".js": "text/javascript; charset=utf-8",
12
+ ".css": "text/css; charset=utf-8",
13
+ ".json": "application/json; charset=utf-8",
14
+ ".svg": "image/svg+xml",
15
+ ".png": "image/png",
16
+ ".jpg": "image/jpeg",
17
+ ".jpeg": "image/jpeg",
18
+ ".gif": "image/gif",
19
+ ".webp": "image/webp",
20
+ ".ico": "image/x-icon",
21
+ ".woff": "font/woff",
22
+ ".woff2": "font/woff2",
23
+ ".txt": "text/plain; charset=utf-8",
24
+ ".map": "application/json; charset=utf-8",
25
+ ".webmanifest": "application/manifest+json",
26
+ }
27
+
28
+ function safeJoin(root, urlPath) {
29
+ const decoded = decodeURIComponent((urlPath || "/").split("?")[0].split("#")[0])
30
+ const rel = decoded.replace(/^\/+/, "")
31
+ const full = path.resolve(root, rel)
32
+ const rootReal = path.resolve(root)
33
+ if (full !== rootReal && !full.startsWith(rootReal + path.sep)) {
34
+ return null
35
+ }
36
+ return full
37
+ }
38
+
39
+ function existingFile(...candidates) {
40
+ for (const candidate of candidates) {
41
+ try {
42
+ if (fs.statSync(candidate).isFile()) return candidate
43
+ } catch {
44
+ /* try next */
45
+ }
46
+ }
47
+ return null
48
+ }
49
+
50
+ function pickFile(root, urlPath) {
51
+ const base = safeJoin(root, urlPath)
52
+ if (!base) return { file: null, status: 403 }
53
+
54
+ let file = null
55
+ if (urlPath === "/" || urlPath === "" || urlPath?.startsWith("/?")) {
56
+ file = existingFile(path.join(root, "index.html"))
57
+ } else {
58
+ const candidates = [base]
59
+ if (!path.extname(base)) {
60
+ candidates.push(`${base}.html`, path.join(base, "index.html"))
61
+ }
62
+ file = existingFile(...candidates)
63
+ }
64
+
65
+ if (file) return { file, status: 200 }
66
+
67
+ const notFound = existingFile(path.join(root, "404.html"))
68
+ if (notFound) return { file: notFound, status: 404 }
69
+ return { file: null, status: 404 }
70
+ }
71
+
72
+ /**
73
+ * @param {{ root: string, port: number, host?: string }} opts
74
+ * @returns {Promise<import('node:http').Server>}
75
+ */
76
+ export function startStaticServer({ root, port, host = "0.0.0.0" }) {
77
+ const server = http.createServer((req, res) => {
78
+ const { file, status } = pickFile(root, req.url || "/")
79
+ if (!file) {
80
+ res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" })
81
+ res.end("Not found")
82
+ return
83
+ }
84
+ const ext = path.extname(file).toLowerCase()
85
+ const type = MIME[ext] || "application/octet-stream"
86
+ res.writeHead(status, {
87
+ "Content-Type": type,
88
+ "Cache-Control":
89
+ ext === ".html" ? "no-cache" : "public, max-age=31536000, immutable",
90
+ })
91
+ fs.createReadStream(file).pipe(res)
92
+ })
93
+
94
+ return new Promise((resolve, reject) => {
95
+ server.once("error", reject)
96
+ server.listen(port, host, () => resolve(server))
97
+ })
98
+ }
99
+
100
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
101
+ const root = process.argv[2]
102
+ const port = Number(process.argv[3] || 8275)
103
+ if (!root) {
104
+ console.error("Usage: static-preview.mjs <root> [port]")
105
+ process.exit(1)
106
+ }
107
+ startStaticServer({ root, port }).then(() => {
108
+ console.log(`Static preview: http://localhost:${port} → ${root}`)
109
+ })
110
+ }
@@ -0,0 +1,65 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+
4
+ const IGNORE_DIRS = new Set([
5
+ "node_modules",
6
+ ".git",
7
+ "out",
8
+ ".next",
9
+ "dist",
10
+ ".taskmark-ui-build",
11
+ ])
12
+
13
+ function shouldIgnoreDir(name) {
14
+ return IGNORE_DIRS.has(name) || name.startsWith(".")
15
+ }
16
+
17
+ function latestMarkdownMtime(boardRoot) {
18
+ let latest = 0
19
+ const walk = (dir) => {
20
+ let entries
21
+ try {
22
+ entries = fs.readdirSync(dir, { withFileTypes: true })
23
+ } catch {
24
+ return
25
+ }
26
+ for (const entry of entries) {
27
+ const full = path.join(dir, entry.name)
28
+ if (entry.isDirectory()) {
29
+ if (!shouldIgnoreDir(entry.name)) walk(full)
30
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
31
+ try {
32
+ latest = Math.max(latest, fs.statSync(full).mtimeMs)
33
+ } catch {
34
+ // ignore races
35
+ }
36
+ }
37
+ }
38
+ }
39
+ walk(boardRoot)
40
+ return latest
41
+ }
42
+
43
+ /**
44
+ * Poll a board directory for `.md` mtime changes.
45
+ * Uses polling (not recursive fs.watch) to avoid EMFILE on large boards.
46
+ * Returns a stop function.
47
+ */
48
+ export function watchBoardMarkdown(boardRoot, onChange) {
49
+ let stopped = false
50
+ let lastStamp = latestMarkdownMtime(boardRoot)
51
+
52
+ const interval = setInterval(() => {
53
+ if (stopped) return
54
+ const next = latestMarkdownMtime(boardRoot)
55
+ if (next > lastStamp) {
56
+ lastStamp = next
57
+ onChange()
58
+ }
59
+ }, 750)
60
+
61
+ return () => {
62
+ stopped = true
63
+ clearInterval(interval)
64
+ }
65
+ }