@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.
- package/README.md +23 -7
- package/app/api/taskmark-reload/route.ts +103 -0
- package/app/board/page.tsx +6 -4
- package/app/globals.css +17 -1
- package/app/layout.tsx +2 -0
- package/bin/lib/static-preview.mjs +110 -0
- package/bin/lib/watch-board-markdown.mjs +65 -0
- package/bin/taskmark.js +254 -46
- package/components/board/app-bar.tsx +31 -34
- package/components/board/board-dev-reloader.tsx +49 -0
- package/components/board/epic-list.tsx +2 -4
- package/components/board/markdown-content.tsx +1 -2
- package/components/board/overall-work-items-list.tsx +2 -1
- package/components/board/static-board-app.tsx +0 -1
- package/components/board/story-list.tsx +2 -1
- package/components/board/task-list.tsx +2 -1
- package/components/board/work-item-sheet.tsx +15 -2
- package/components/board/work-items-list.tsx +2 -2
- package/components/setup/setup-wizard.tsx +2 -6
- package/dist/prod-next/BUILD_ID +1 -0
- package/dist/{standalone/.next → prod-next}/app-path-routes-manifest.json +3 -2
- package/dist/{standalone/.next → prod-next}/build-manifest.json +3 -3
- package/dist/prod-next/export-marker.json +6 -0
- package/dist/prod-next/images-manifest.json +68 -0
- package/dist/prod-next/next-minimal-server.js.nft.json +1 -0
- package/dist/prod-next/next-server.js.nft.json +1 -0
- package/dist/{standalone/.next → prod-next}/prerender-manifest.json +3 -3
- package/dist/{standalone/.next → prod-next}/react-loadable-manifest.json +2 -2
- package/dist/prod-next/required-server-files.js +337 -0
- package/dist/{standalone/.next → prod-next}/required-server-files.json +0 -7
- package/dist/{standalone/.next → prod-next}/routes-manifest.json +6 -0
- package/dist/prod-next/server/app/_global-error/page.js +32 -0
- package/dist/prod-next/server/app/_global-error/page.js.nft.json +1 -0
- package/dist/prod-next/server/app/_global-error/page_client-reference-manifest.js +1 -0
- package/dist/{standalone/.next → prod-next}/server/app/_global-error.html +1 -1
- package/dist/{standalone/.next → prod-next}/server/app/_global-error.rsc +1 -1
- package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/{standalone/.next → prod-next}/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/prod-next/server/app/_not-found/page.js +2 -0
- package/dist/prod-next/server/app/_not-found/page.js.nft.json +1 -0
- package/dist/prod-next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/dist/prod-next/server/app/_not-found.html +1 -0
- package/dist/prod-next/server/app/_not-found.rsc +21 -0
- package/dist/prod-next/server/app/_not-found.segments/_full.segment.rsc +21 -0
- package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/prod-next/server/app/_not-found.segments/_index.segment.rsc +8 -0
- package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/{standalone/.next → prod-next}/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/dist/prod-next/server/app/api/taskmark-reload/route.js +1 -0
- package/dist/prod-next/server/app/api/taskmark-reload/route.js.nft.json +1 -0
- package/dist/prod-next/server/app/api/taskmark-reload/route_client-reference-manifest.js +1 -0
- package/dist/prod-next/server/app/board/page.js +50 -0
- package/dist/prod-next/server/app/board/page.js.nft.json +1 -0
- package/dist/prod-next/server/app/board/page_client-reference-manifest.js +1 -0
- package/dist/{standalone/.next → prod-next}/server/app/favicon.ico/route.js +1 -1
- package/dist/prod-next/server/app/favicon.ico/route.js.nft.json +1 -0
- package/dist/prod-next/server/app/page.js +2 -0
- package/dist/prod-next/server/app/page.js.nft.json +1 -0
- package/dist/prod-next/server/app/page_client-reference-manifest.js +1 -0
- package/dist/prod-next/server/app/setup/page.js +2 -0
- package/dist/prod-next/server/app/setup/page.js.nft.json +1 -0
- package/dist/prod-next/server/app/setup/page_client-reference-manifest.js +1 -0
- package/dist/{standalone/.next → prod-next}/server/app-paths-manifest.json +3 -2
- package/dist/{standalone/.next → prod-next}/server/chunks/319.js +1 -1
- package/dist/{standalone/.next → prod-next}/server/chunks/490.js +1 -1
- package/dist/prod-next/server/chunks/57.js +19 -0
- package/dist/prod-next/server/chunks/616.js +1 -0
- package/dist/prod-next/server/chunks/708.js +1 -0
- package/dist/prod-next/server/chunks/724.js +1 -0
- package/dist/{standalone/.next/server/chunks/445.js → prod-next/server/chunks/741.js} +2 -2
- package/dist/prod-next/server/functions-config-manifest.json +6 -0
- package/dist/prod-next/server/interception-route-rewrite-manifest.js +1 -0
- package/dist/{standalone/.next → prod-next}/server/middleware-build-manifest.js +1 -1
- package/dist/prod-next/server/middleware-react-loadable-manifest.js +1 -0
- package/dist/prod-next/server/pages/404.html +1 -0
- package/dist/{standalone/.next → prod-next}/server/pages/500.html +1 -1
- package/dist/prod-next/server/server-reference-manifest.js +1 -0
- package/dist/prod-next/server/server-reference-manifest.json +1 -0
- package/dist/prod-next/static/chunks/51.e9139a9ea6eb36b9.js +1 -0
- package/dist/{standalone/.next/static/chunks/781-f9f201ddc8c8591a.js → prod-next/static/chunks/613-6f794154d28bb75a.js} +1 -1
- package/dist/prod-next/static/chunks/app/_global-error/page-d34451c04262dd7d.js +1 -0
- package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-d34451c04262dd7d.js +1 -0
- package/dist/prod-next/static/chunks/app/board/page-cd1d9be0c0127225.js +1 -0
- package/dist/prod-next/static/chunks/app/layout-856e8a282e69e7d0.js +1 -0
- package/dist/prod-next/static/chunks/app/page-d34451c04262dd7d.js +1 -0
- package/dist/prod-next/static/chunks/app/setup/page-c3003359e8becc84.js +1 -0
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-d34451c04262dd7d.js +1 -0
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-d34451c04262dd7d.js +1 -0
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-d34451c04262dd7d.js +1 -0
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-d34451c04262dd7d.js +1 -0
- package/dist/prod-next/static/chunks/webpack-bfe8992962004e2b.js +1 -0
- package/dist/prod-next/static/css/2e6ec4c80894e3c0.css +1 -0
- package/dist/prod-next/static/wbcgYS3Jw87KqhnA9ttOL/_buildManifest.js +1 -0
- package/dist/public/taskmark-snapshot.json +1 -0
- package/lib/display-path.ts +6 -0
- package/lib/taskmark/dev-reload-token.ts +5 -0
- package/lib/taskmark/project-label.ts +3 -2
- package/lib/taskmark/validate.ts +4 -4
- package/next.config.ts +3 -8
- package/package.json +6 -4
- package/public/taskmark-snapshot.json +1 -1
- package/scripts/build-static.mjs +10 -1
- package/scripts/prepare-standalone.mjs +53 -20
- package/dist/standalone/.next/BUILD_ID +0 -1
- package/dist/standalone/.next/server/app/_global-error/page.js +0 -32
- package/dist/standalone/.next/server/app/_global-error/page.js.nft.json +0 -1
- package/dist/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +0 -1
- package/dist/standalone/.next/server/app/_not-found/page.js +0 -2
- package/dist/standalone/.next/server/app/_not-found/page.js.nft.json +0 -1
- package/dist/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
- package/dist/standalone/.next/server/app/_not-found.html +0 -1
- package/dist/standalone/.next/server/app/_not-found.rsc +0 -20
- package/dist/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +0 -20
- package/dist/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +0 -7
- package/dist/standalone/.next/server/app/board/page.js +0 -50
- package/dist/standalone/.next/server/app/board/page.js.nft.json +0 -1
- package/dist/standalone/.next/server/app/board/page_client-reference-manifest.js +0 -1
- package/dist/standalone/.next/server/app/favicon.ico/route.js.nft.json +0 -1
- package/dist/standalone/.next/server/app/page.js +0 -2
- package/dist/standalone/.next/server/app/page.js.nft.json +0 -1
- package/dist/standalone/.next/server/app/page_client-reference-manifest.js +0 -1
- package/dist/standalone/.next/server/app/setup/page.js +0 -2
- package/dist/standalone/.next/server/app/setup/page.js.nft.json +0 -1
- package/dist/standalone/.next/server/app/setup/page_client-reference-manifest.js +0 -1
- package/dist/standalone/.next/server/chunks/57.js +0 -19
- package/dist/standalone/.next/server/chunks/570.js +0 -1
- package/dist/standalone/.next/server/chunks/600.js +0 -1
- package/dist/standalone/.next/server/chunks/666.js +0 -1
- package/dist/standalone/.next/server/functions-config-manifest.json +0 -4
- package/dist/standalone/.next/server/middleware-react-loadable-manifest.js +0 -1
- package/dist/standalone/.next/server/pages/404.html +0 -1
- package/dist/standalone/.next/server/server-reference-manifest.js +0 -1
- package/dist/standalone/.next/server/server-reference-manifest.json +0 -1
- package/dist/standalone/.next/static/O83kNzYUmJlkfw7SS-SsU/_buildManifest.js +0 -1
- package/dist/standalone/.next/static/chunks/826.92750d8ed3a55429.js +0 -1
- package/dist/standalone/.next/static/chunks/app/_global-error/page-82d456ade9f47021.js +0 -1
- package/dist/standalone/.next/static/chunks/app/board/page-f609a854d76933c2.js +0 -1
- package/dist/standalone/.next/static/chunks/app/layout-3745e06d87691845.js +0 -1
- package/dist/standalone/.next/static/chunks/app/page-82d456ade9f47021.js +0 -1
- package/dist/standalone/.next/static/chunks/app/setup/page-acaffda2083bf4d0.js +0 -1
- package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/app-error-82d456ade9f47021.js +0 -1
- package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/forbidden-82d456ade9f47021.js +0 -1
- package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/not-found-82d456ade9f47021.js +0 -1
- package/dist/standalone/.next/static/chunks/next/dist/client/components/builtin/unauthorized-82d456ade9f47021.js +0 -1
- package/dist/standalone/.next/static/chunks/webpack-f181688592aeb1f6.js +0 -1
- package/dist/standalone/.next/static/css/7af268deac5c5edb.css +0 -1
- package/dist/standalone/node_modules/next/dist/compiled/jest-worker/processChild.js +0 -1
- package/dist/standalone/node_modules/next/dist/compiled/jest-worker/threadChild.js +0 -1
- package/dist/standalone/node_modules/next/dist/server/lib/start-server.js +0 -494
- package/dist/standalone/node_modules/next/dist/server/next-server.js +0 -1429
- package/dist/standalone/node_modules/next/dist/server/next.js +0 -455
- package/dist/standalone/node_modules/next/dist/server/require-hook.js +0 -82
- package/dist/standalone/node_modules/next/dist/server/route-modules/app-page/module.compiled.js +0 -36
- package/dist/standalone/node_modules/next/dist/server/route-modules/pages/module.compiled.js +0 -20
- package/dist/standalone/node_modules/styled-jsx/index.js +0 -1
- package/dist/standalone/node_modules/styled-jsx/package.json +0 -132
- package/dist/standalone/node_modules/styled-jsx/style.js +0 -1
- package/dist/standalone/package.json +0 -76
- package/dist/standalone/public/taskmark-snapshot.json +0 -1
- package/dist/standalone/server.js +0 -44
- /package/dist/{standalone/.next → prod-next}/package.json +0 -0
- /package/dist/{standalone/.next → prod-next}/server/app/_global-error.meta +0 -0
- /package/dist/{standalone/.next → prod-next}/server/app/_not-found.meta +0 -0
- /package/dist/{standalone/.next → prod-next}/server/app/favicon.ico.body +0 -0
- /package/dist/{standalone/.next → prod-next}/server/app/favicon.ico.meta +0 -0
- /package/dist/{standalone/.next → prod-next}/server/chunks/318.js +0 -0
- /package/dist/{standalone/.next → prod-next}/server/middleware-manifest.json +0 -0
- /package/dist/{standalone/.next → prod-next}/server/next-font-manifest.js +0 -0
- /package/dist/{standalone/.next → prod-next}/server/next-font-manifest.json +0 -0
- /package/dist/{standalone/.next → prod-next}/server/pages-manifest.json +0 -0
- /package/dist/{standalone/.next → prod-next}/server/prefetch-hints.json +0 -0
- /package/dist/{standalone/.next → prod-next}/server/webpack-runtime.js +0 -0
- /package/dist/{standalone/.next → prod-next}/static/chunks/4bd1b696-deb4a0a1da1923b0.js +0 -0
- /package/dist/{standalone/.next → prod-next}/static/chunks/669-8103441c67b185da.js +0 -0
- /package/dist/{standalone/.next → prod-next}/static/chunks/701-df3ee5f1cf837b1a.js +0 -0
- /package/dist/{standalone/.next → prod-next}/static/chunks/794-25668765560cb197.js +0 -0
- /package/dist/{standalone/.next → prod-next}/static/chunks/app/_not-found/page-0b3785af737e7968.js +0 -0
- /package/dist/{standalone/.next → prod-next}/static/chunks/framework-b29f9083d3cda3b2.js +0 -0
- /package/dist/{standalone/.next → prod-next}/static/chunks/main-app-bfb36455d24b3e5a.js +0 -0
- /package/dist/{standalone/.next → prod-next}/static/chunks/main-bb33c46a9c25dee8.js +0 -0
- /package/dist/{standalone/.next → prod-next}/static/chunks/next/dist/client/components/builtin/global-error-dc53ab29e0491fe4.js +0 -0
- /package/dist/{standalone/.next → prod-next}/static/chunks/polyfills-42372ed130431b0a.js +0 -0
- /package/dist/{standalone/.next → prod-next}/static/css/c593627ceef17a4b.css +0 -0
- /package/dist/{standalone/.next → prod-next}/static/media/36966cca54120369-s.p.woff2 +0 -0
- /package/dist/{standalone/.next → prod-next}/static/media/982ceffe7b733b3b-s.p.woff2 +0 -0
- /package/dist/{standalone/.next → prod-next}/static/media/b7387a63dd068245-s.woff2 +0 -0
- /package/dist/{standalone/.next → prod-next}/static/media/b8bfb68ab9ab617c-s.woff2 +0 -0
- /package/dist/{standalone/.next → prod-next}/static/media/e1aab0933260df4d-s.woff2 +0 -0
- /package/dist/{standalone/.next/static/O83kNzYUmJlkfw7SS-SsU → prod-next/static/wbcgYS3Jw87KqhnA9ttOL}/_ssgManifest.js +0 -0
- /package/dist/{standalone/public → public}/apple-touch-icon.png +0 -0
- /package/dist/{standalone/public → public}/favicon-96x96.png +0 -0
- /package/dist/{standalone/public → public}/favicon.ico +0 -0
- /package/dist/{standalone/public → public}/favicon.svg +0 -0
- /package/dist/{standalone/public → public}/file.svg +0 -0
- /package/dist/{standalone/public → public}/globe.svg +0 -0
- /package/dist/{standalone/public → public}/next.svg +0 -0
- /package/dist/{standalone/public → public}/og/og-default.png +0 -0
- /package/dist/{standalone/public → public}/site.webmanifest +0 -0
- /package/dist/{standalone/public → public}/tm_dark.png +0 -0
- /package/dist/{standalone/public → public}/tm_light.png +0 -0
- /package/dist/{standalone/public → public}/vercel.svg +0 -0
- /package/dist/{standalone/public → public}/web-app-manifest-192x192.png +0 -0
- /package/dist/{standalone/public → public}/web-app-manifest-512x512.png +0 -0
- /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
|
-
"
|
|
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.
|
|
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 #
|
|
77
|
-
npm run dev #
|
|
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
|
|
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
|
+
}
|
package/app/board/page.tsx
CHANGED
|
@@ -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
|
+
}
|