@taskmark/ui 0.2.6 → 0.3.1
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 +16 -1
- package/app/board/page.tsx +3 -272
- package/app/globals.css +25 -0
- package/app/layout.tsx +15 -5
- package/app/page.tsx +14 -14
- package/app/setup/actions.ts +4 -4
- package/app/setup/page.tsx +9 -4
- package/bin/lib/resolve-board.mjs +1 -4
- package/bin/taskmark.js +15 -2
- package/components/board/app-bar.tsx +3 -0
- package/components/board/board-screen.tsx +229 -0
- package/components/board/changelog-panel.tsx +26 -0
- package/components/board/epic-list.tsx +4 -2
- package/components/board/list-view-switcher.tsx +7 -3
- package/components/board/markdown-content.tsx +21 -1
- package/components/board/mermaid-diagram.tsx +88 -0
- package/components/board/metric-stat-card.tsx +4 -4
- package/components/board/overall-tree-list.tsx +467 -0
- package/components/board/overall-work-items-list.tsx +6 -2
- package/components/board/points-heatmap.tsx +183 -0
- package/components/board/project-status-metrics-strip.tsx +5 -22
- package/components/board/static-board-app.tsx +29 -94
- package/components/board/status-badge.tsx +8 -0
- package/components/board/story-list.tsx +3 -1
- package/components/board/task-list.tsx +5 -1
- package/components/board/work-item-detail-body.tsx +123 -8
- package/components/board/work-items-list.tsx +2 -0
- package/components/theme-provider.tsx +39 -0
- package/components/theme-toggle.tsx +27 -0
- package/components/ui/sonner.tsx +2 -2
- package/dist/prod-next/BUILD_ID +1 -1
- package/dist/prod-next/app-path-routes-manifest.json +2 -2
- package/dist/prod-next/build-manifest.json +6 -6
- package/dist/prod-next/prerender-manifest.json +3 -27
- package/dist/prod-next/react-loadable-manifest.json +392 -2
- package/dist/prod-next/server/app/_global-error/page.js +2 -2
- package/dist/prod-next/server/app/_global-error/page.js.nft.json +1 -1
- package/dist/prod-next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/prod-next/server/app/_global-error.html +1 -1
- package/dist/prod-next/server/app/_global-error.rsc +1 -1
- package/dist/prod-next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/prod-next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/prod-next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/prod-next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/prod-next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/prod-next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/prod-next/server/app/_not-found/page.js +2 -2
- package/dist/prod-next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/prod-next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/prod-next/server/app/_not-found.html +1 -1
- package/dist/prod-next/server/app/_not-found.rsc +19 -17
- package/dist/prod-next/server/app/_not-found.segments/_full.segment.rsc +19 -17
- package/dist/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 -6
- package/dist/prod-next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/prod-next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/prod-next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/dist/prod-next/server/app/api/taskmark-reload/route.js +1 -1
- package/dist/prod-next/server/app/api/taskmark-reload/route.js.nft.json +1 -1
- package/dist/prod-next/server/app/board/page.js +2 -50
- package/dist/prod-next/server/app/board/page.js.nft.json +1 -1
- package/dist/prod-next/server/app/board/page_client-reference-manifest.js +1 -1
- package/dist/prod-next/server/app/favicon.ico/route.js +1 -1
- package/dist/prod-next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/dist/prod-next/server/app/page.js +2 -2
- package/dist/prod-next/server/app/page.js.nft.json +1 -1
- package/dist/prod-next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/prod-next/server/app/setup/page.js +2 -2
- package/dist/prod-next/server/app/setup/page.js.nft.json +1 -1
- package/dist/prod-next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/dist/prod-next/server/app-paths-manifest.json +2 -2
- package/dist/prod-next/server/chunks/102.js +90 -0
- package/dist/prod-next/server/chunks/11.js +1 -0
- package/dist/prod-next/server/chunks/1286.js +1 -0
- package/dist/prod-next/server/chunks/1412.js +38 -0
- package/dist/prod-next/server/chunks/1605.js +1 -0
- package/dist/prod-next/server/chunks/169.js +1 -0
- package/dist/prod-next/server/chunks/18.js +86 -0
- package/dist/prod-next/server/chunks/2.js +1 -0
- package/dist/prod-next/server/chunks/2022.js +1 -0
- package/dist/prod-next/server/chunks/2156.js +1 -0
- package/dist/prod-next/server/chunks/2157.js +1 -0
- package/dist/prod-next/server/chunks/2325.js +82 -0
- package/dist/prod-next/server/chunks/2838.js +1 -0
- package/dist/prod-next/server/chunks/2897.js +79 -0
- package/dist/prod-next/server/chunks/3227.js +1 -0
- package/dist/prod-next/server/chunks/3266.js +1 -0
- package/dist/prod-next/server/chunks/3307.js +49 -0
- package/dist/prod-next/server/chunks/3387.js +19 -0
- package/dist/prod-next/server/chunks/3428.js +32 -0
- package/dist/prod-next/server/chunks/3479.js +1 -0
- package/dist/prod-next/server/chunks/367.js +115 -0
- package/dist/prod-next/server/chunks/3698.js +93 -0
- package/dist/prod-next/server/chunks/3709.js +148 -0
- package/dist/prod-next/server/chunks/41.js +1 -0
- package/dist/prod-next/server/chunks/4107.js +62 -0
- package/dist/prod-next/server/chunks/4569.js +34 -0
- package/dist/prod-next/server/chunks/{741.js → 4741.js} +1 -1
- package/dist/prod-next/server/chunks/4755.js +153 -0
- package/dist/prod-next/server/chunks/487.js +1 -0
- package/dist/prod-next/server/chunks/5002.js +1 -0
- package/dist/prod-next/server/chunks/5112.js +1 -0
- package/dist/prod-next/server/chunks/5289.js +8 -0
- package/dist/prod-next/server/chunks/5318.js +1 -0
- package/dist/prod-next/server/chunks/5452.js +23 -0
- package/dist/prod-next/server/chunks/5453.js +1 -0
- package/dist/prod-next/server/chunks/5749.js +27 -0
- package/dist/prod-next/server/chunks/5782.js +36 -0
- package/dist/prod-next/server/chunks/6163.js +104 -0
- package/dist/prod-next/server/chunks/6169.js +64 -0
- package/dist/prod-next/server/chunks/6208.js +172 -0
- package/dist/prod-next/server/chunks/6399.js +24 -0
- package/dist/prod-next/server/chunks/6496.js +1 -0
- package/dist/prod-next/server/chunks/6823.js +215 -0
- package/dist/prod-next/server/chunks/6888.js +1 -0
- package/dist/prod-next/server/chunks/6980.js +1 -0
- package/dist/prod-next/server/chunks/7245.js +77 -0
- package/dist/prod-next/server/chunks/7307.js +1 -0
- package/dist/prod-next/server/chunks/7372.js +287 -0
- package/dist/prod-next/server/chunks/7384.js +226 -0
- package/dist/prod-next/server/chunks/7516.js +1 -0
- package/dist/prod-next/server/chunks/7531.js +24 -0
- package/dist/prod-next/server/chunks/7591.js +1 -0
- package/dist/prod-next/server/chunks/7755.js +138 -0
- package/dist/prod-next/server/chunks/7916.js +1 -0
- package/dist/prod-next/server/chunks/8078.js +94 -0
- package/dist/prod-next/server/chunks/8253.js +41 -0
- package/dist/prod-next/server/chunks/8635.js +1 -0
- package/dist/prod-next/server/chunks/8688.js +1 -0
- package/dist/prod-next/server/chunks/8872.js +1 -0
- package/dist/prod-next/server/chunks/9006.js +1 -0
- package/dist/prod-next/server/chunks/9018.js +1 -0
- package/dist/prod-next/server/chunks/9047.js +1 -0
- package/dist/prod-next/server/chunks/9108.js +1 -0
- package/dist/prod-next/server/chunks/9280.js +256 -0
- package/dist/prod-next/server/chunks/9419.js +1 -0
- package/dist/prod-next/server/chunks/9430.js +1 -0
- package/dist/prod-next/server/chunks/9586.js +1 -0
- package/dist/prod-next/server/chunks/9775.js +1 -0
- package/dist/prod-next/server/chunks/992.js +1 -0
- package/dist/prod-next/server/chunks/9926.js +1 -0
- package/dist/prod-next/server/chunks/9984.js +8 -0
- package/dist/prod-next/server/middleware-build-manifest.js +1 -1
- package/dist/prod-next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/prod-next/server/pages/404.html +1 -1
- package/dist/prod-next/server/pages/500.html +1 -1
- package/dist/prod-next/server/server-reference-manifest.js +1 -1
- package/dist/prod-next/server/server-reference-manifest.json +1 -1
- package/dist/prod-next/server/webpack-runtime.js +1 -1
- package/dist/prod-next/static/chunks/1213.e5c6e70a79b5f7e6.js +24 -0
- package/dist/prod-next/static/chunks/1519.2f596e5f5af9ef57.js +62 -0
- package/dist/prod-next/static/chunks/1759-f5b8fb456f8fb420.js +1 -0
- package/dist/prod-next/static/chunks/1974.885c9a133b86d8a4.js +32 -0
- package/dist/prod-next/static/chunks/2280.6a7f540787704698.js +1 -0
- package/dist/prod-next/static/chunks/2521.7f745f50d30076e7.js +77 -0
- package/dist/prod-next/static/chunks/2585.4d6aa847c780c666.js +1 -0
- package/dist/prod-next/static/chunks/2589.1aad68c09021b1ac.js +1 -0
- package/dist/prod-next/static/chunks/2600.45ba17a0f756fc31.js +1 -0
- package/dist/prod-next/static/chunks/2619.9aac8983f30c7c8a.js +1 -0
- package/dist/prod-next/static/chunks/26d302b2.9f6383e42abdfe16.js +122 -0
- package/dist/prod-next/static/chunks/2768.305914f7900745b6.js +1 -0
- package/dist/prod-next/static/chunks/2931.c2acdbf72808b646.js +41 -0
- package/dist/prod-next/static/chunks/3012-838f69b72a0dc1b3.js +1 -0
- package/dist/prod-next/static/chunks/3016.406e568aa95ff867.js +1 -0
- package/dist/prod-next/static/chunks/3022.972047558a20553a.js +172 -0
- package/dist/prod-next/static/chunks/3064.3a2a1e7cd4fdca5f.js +1 -0
- package/dist/prod-next/static/chunks/3174.03fbed7b7d7a8256.js +1 -0
- package/dist/prod-next/static/chunks/3189.b048866768087750.js +1 -0
- package/dist/prod-next/static/chunks/3217.1a053843c58b70b5.js +24 -0
- package/dist/prod-next/static/chunks/3308.30b95d524f9c1daf.js +74 -0
- package/dist/prod-next/static/chunks/331.fc5d78e1e91daf43.js +1 -0
- package/dist/prod-next/static/chunks/3510.5d4d1c79533e080c.js +1 -0
- package/dist/prod-next/static/chunks/364.ce5400a9dd70b9dc.js +1 -0
- package/dist/prod-next/static/chunks/3661-1f08d73aaffb011d.js +1 -0
- package/dist/prod-next/static/chunks/370.57cfa27da8567918.js +1 -0
- package/dist/prod-next/static/chunks/3789.c418b69121761983.js +1 -0
- package/dist/prod-next/static/chunks/3794-c66b3d8325973459.js +32 -0
- package/dist/prod-next/static/chunks/3831.25ae62d10dccc27a.js +1 -0
- package/dist/prod-next/static/chunks/395.b41e9d76613d37b5.js +90 -0
- package/dist/prod-next/static/chunks/3a7aa434.93dbd20997cfd24f.js +82 -0
- package/dist/prod-next/static/chunks/4020.3cc19c463c49f969.js +23 -0
- package/dist/prod-next/static/chunks/4035.0acc428926d5c8dd.js +1 -0
- package/dist/prod-next/static/chunks/405.0902e4f6eb7bd9a2.js +215 -0
- package/dist/prod-next/static/chunks/4159.49ae537ff687bdc6.js +138 -0
- package/dist/prod-next/static/chunks/4208.384b381ca2757643.js +287 -0
- package/dist/prod-next/static/chunks/4263.ffa8d9bd8a0faaae.js +1 -0
- package/dist/prod-next/static/chunks/4561.d1a3aaca8bae3224.js +34 -0
- package/dist/prod-next/static/chunks/4592.d0f37bc216b9d988.js +93 -0
- package/dist/prod-next/static/chunks/4799.0dc7aa21bff2fd2a.js +1 -0
- package/dist/prod-next/static/chunks/4bd1b696-e356ca5ba0218e27.js +1 -0
- package/dist/prod-next/static/chunks/514.b804c51b26f5e126.js +1 -0
- package/dist/prod-next/static/chunks/5270.28592864d38e881c.js +1 -0
- package/dist/prod-next/static/chunks/52733464.3870f99cc446f9ff.js +1 -0
- package/dist/prod-next/static/chunks/5354.b559c717a607dc5c.js +1 -0
- package/dist/prod-next/static/chunks/5448.d3972d9fb464b54f.js +38 -0
- package/dist/prod-next/static/chunks/587.d52737d7288c11b4.js +1 -0
- package/dist/prod-next/static/chunks/5909.9e37f7b495b11995.js +115 -0
- package/dist/prod-next/static/chunks/5949.5504b3545fcc3d34.js +104 -0
- package/dist/prod-next/static/chunks/6322-4e97920c006f7933.js +1 -0
- package/dist/prod-next/static/chunks/6361.893d04ac34fa1b49.js +148 -0
- package/dist/prod-next/static/chunks/6774.8cd53427dbf32cac.js +86 -0
- package/dist/prod-next/static/chunks/6968.b80277c724dbc96e.js +1 -0
- package/dist/prod-next/static/chunks/7113.4b9e7ea870aa300a.js +8 -0
- package/dist/prod-next/static/chunks/7236.55a9728d35f5b8a6.js +1 -0
- package/dist/prod-next/static/chunks/7390.dde9b5bc2f6e24b8.js +1 -0
- package/dist/prod-next/static/chunks/7806.a903780fcfa3a30e.js +1 -0
- package/dist/prod-next/static/chunks/7997.be57e92e583fe635.js +1 -0
- package/dist/prod-next/static/chunks/8216.64a72d4d35ea7be0.js +1 -0
- package/dist/prod-next/static/chunks/830.5d5274d7ad1cb58e.js +1 -0
- package/dist/prod-next/static/chunks/8378.fa91de3406f554c9.js +226 -0
- package/dist/prod-next/static/chunks/8501.cd06aa1b0c7e1c6a.js +79 -0
- package/dist/prod-next/static/chunks/8663.6ec5792fb4952e45.js +27 -0
- package/dist/prod-next/static/chunks/872.96937e758ddce860.js +1 -0
- package/dist/prod-next/static/chunks/8803.75bde095fe3613f5.js +1 -0
- package/dist/prod-next/static/chunks/8863.72f5dbb820a0e4c3.js +1 -0
- package/dist/prod-next/static/chunks/90542734.f7a7b2972ef5a749.js +1 -0
- package/dist/prod-next/static/chunks/9379.5bc9c9343ec5a16c.js +64 -0
- package/dist/prod-next/static/chunks/9389.906fe7aa9cd6a170.js +1 -0
- package/dist/prod-next/static/chunks/9472.8818ee52cfa931a4.js +94 -0
- package/dist/prod-next/static/chunks/966.d6babbf8119162cc.js +1 -0
- package/dist/prod-next/static/chunks/9884.d4ac1a961b3366f1.js +36 -0
- package/dist/prod-next/static/chunks/9909.67cf9940b125243b.js +1 -0
- package/dist/prod-next/static/chunks/app/_global-error/page-8be5492b3b872a4c.js +1 -0
- package/dist/prod-next/static/chunks/app/_not-found/page-799e445a538b0024.js +1 -0
- package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-8be5492b3b872a4c.js +1 -0
- package/dist/prod-next/static/chunks/app/board/page-5c181f45738daf1c.js +1 -0
- package/dist/prod-next/static/chunks/app/layout-21782e6d13a6f275.js +1 -0
- package/dist/prod-next/static/chunks/app/page-51bfe4356fe233e3.js +1 -0
- package/dist/prod-next/static/chunks/app/setup/page-3ccfba00c10f9b17.js +1 -0
- package/dist/prod-next/static/chunks/d3ac728e.7964f816a1ca64e5.js +1 -0
- package/dist/prod-next/static/chunks/e413bedd.3a12a8cb02e28fdf.js +153 -0
- package/dist/prod-next/static/chunks/ea233666.f00b64a7d99b4950.js +62 -0
- package/dist/prod-next/static/chunks/framework-711ef29bc66f648c.js +1 -0
- package/dist/prod-next/static/chunks/main-app-dd26d4f9d120646e.js +1 -0
- package/dist/prod-next/static/chunks/{main-bb33c46a9c25dee8.js → main-df4bcaed74f274eb.js} +2 -2
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-8be5492b3b872a4c.js +1 -0
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-8be5492b3b872a4c.js +1 -0
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/global-error-45a0cf77e03361ed.js +1 -0
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-8be5492b3b872a4c.js +1 -0
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-8be5492b3b872a4c.js +1 -0
- package/dist/prod-next/static/chunks/webpack-713747aaa9716c4c.js +1 -0
- package/dist/prod-next/static/css/cb78d4b0062d9a3f.css +1 -0
- package/dist/public/taskmark-snapshot.json +1 -1
- package/lib/taskmark/board-index.ts +101 -0
- package/lib/taskmark/build-snapshot.ts +28 -7
- package/lib/taskmark/derive-parents.test.ts +23 -0
- package/lib/taskmark/derive-parents.ts +323 -0
- package/lib/taskmark/detail-load.ts +55 -7
- package/lib/taskmark/detail-types.ts +10 -2
- package/lib/taskmark/dev-reload-token.ts +1 -1
- package/lib/taskmark/discover.ts +4 -7
- package/lib/taskmark/epic-types.ts +2 -1
- package/lib/taskmark/flat-work-item-types.ts +1 -4
- package/lib/taskmark/identity.ts +3 -3
- package/lib/taskmark/item-types.ts +2 -1
- package/lib/taskmark/list-filters.test.ts +23 -0
- package/lib/taskmark/list-filters.ts +7 -2
- package/lib/taskmark/list-view-mode.test.ts +45 -0
- package/lib/taskmark/list-view-mode.ts +20 -9
- package/lib/taskmark/load-changelog.test.ts +32 -0
- package/lib/taskmark/load-changelog.ts +18 -0
- package/lib/taskmark/overall-tree.test.ts +196 -0
- package/lib/taskmark/overall-tree.ts +194 -0
- package/lib/taskmark/parse-detail.ts +5 -5
- package/lib/taskmark/parse-epics.ts +19 -16
- package/lib/taskmark/parse-flat-lists.ts +32 -28
- package/lib/taskmark/parse-items.ts +47 -9
- package/lib/taskmark/parse-stories.ts +18 -15
- package/lib/taskmark/points-calendar.test.ts +72 -0
- package/lib/taskmark/points-calendar.ts +86 -0
- package/lib/taskmark/project-metrics.ts +11 -7
- package/lib/taskmark/snapshot-types.ts +2 -0
- package/lib/taskmark/story-types.ts +2 -1
- package/lib/taskmark/table-sort.ts +4 -3
- package/lib/taskmark/timing.ts +29 -18
- package/lib/taskmark/validate.ts +1 -1
- package/lib/theme-cookie.test.ts +27 -0
- package/lib/theme-cookie.ts +43 -0
- package/package.json +3 -1
- package/public/taskmark-snapshot.json +1 -1
- package/scripts/build-static.mjs +61 -3
- package/dist/prod-next/server/app/setup.html +0 -1
- package/dist/prod-next/server/app/setup.meta +0 -17
- package/dist/prod-next/server/app/setup.rsc +0 -22
- package/dist/prod-next/server/app/setup.segments/_full.segment.rsc +0 -22
- package/dist/prod-next/server/app/setup.segments/_head.segment.rsc +0 -6
- package/dist/prod-next/server/app/setup.segments/_index.segment.rsc +0 -8
- package/dist/prod-next/server/app/setup.segments/_tree.segment.rsc +0 -5
- package/dist/prod-next/server/app/setup.segments/setup/__PAGE__.segment.rsc +0 -6
- package/dist/prod-next/server/app/setup.segments/setup.segment.rsc +0 -5
- package/dist/prod-next/server/chunks/318.js +0 -1
- package/dist/prod-next/server/chunks/490.js +0 -8
- package/dist/prod-next/server/chunks/57.js +0 -19
- package/dist/prod-next/server/chunks/616.js +0 -1
- package/dist/prod-next/server/chunks/708.js +0 -1
- package/dist/prod-next/server/chunks/724.js +0 -1
- package/dist/prod-next/static/chunks/4bd1b696-deb4a0a1da1923b0.js +0 -1
- package/dist/prod-next/static/chunks/51.e9139a9ea6eb36b9.js +0 -1
- package/dist/prod-next/static/chunks/613-6f794154d28bb75a.js +0 -1
- package/dist/prod-next/static/chunks/669-8103441c67b185da.js +0 -1
- package/dist/prod-next/static/chunks/701-df3ee5f1cf837b1a.js +0 -1
- package/dist/prod-next/static/chunks/794-25668765560cb197.js +0 -32
- package/dist/prod-next/static/chunks/app/_global-error/page-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/app/_not-found/page-0b3785af737e7968.js +0 -1
- package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/app/board/page-cd1d9be0c0127225.js +0 -1
- package/dist/prod-next/static/chunks/app/layout-856e8a282e69e7d0.js +0 -1
- package/dist/prod-next/static/chunks/app/page-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/app/setup/page-c3003359e8becc84.js +0 -1
- package/dist/prod-next/static/chunks/framework-b29f9083d3cda3b2.js +0 -1
- package/dist/prod-next/static/chunks/main-app-bfb36455d24b3e5a.js +0 -1
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/global-error-dc53ab29e0491fe4.js +0 -1
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/webpack-bfe8992962004e2b.js +0 -1
- package/dist/prod-next/static/css/2e6ec4c80894e3c0.css +0 -1
- /package/dist/prod-next/static/{sAWr5ATVuqk6Wqs18kAWs → EI1zQaa_kQ7DqUSE4o3Qo}/_buildManifest.js +0 -0
- /package/dist/prod-next/static/{sAWr5ATVuqk6Wqs18kAWs → EI1zQaa_kQ7DqUSE4o3Qo}/_ssgManifest.js +0 -0
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
extractFrontmatter,
|
|
16
16
|
} from "@/lib/taskmark/frontmatter"
|
|
17
17
|
import { asContributorList } from "@/lib/taskmark/identity"
|
|
18
|
+
import type { BoardIndex, IndexedLeaf } from "@/lib/taskmark/board-index"
|
|
18
19
|
import { findEpicFolder } from "@/lib/taskmark/parse-stories"
|
|
19
|
-
import {
|
|
20
|
+
import { readActualTimingFromWorkLog } from "@/lib/taskmark/timing"
|
|
20
21
|
|
|
21
22
|
type StoryFolder = {
|
|
22
23
|
dirName: string
|
|
@@ -118,11 +119,12 @@ function parseItemFile(
|
|
|
118
119
|
filePath: string,
|
|
119
120
|
project: DiscoveredProject,
|
|
120
121
|
parentId: string,
|
|
121
|
-
epicId: string
|
|
122
|
+
epicId: string,
|
|
123
|
+
indexed?: IndexedLeaf
|
|
122
124
|
): { item?: ItemSummary; error?: ItemParseError } {
|
|
123
125
|
let raw: string
|
|
124
126
|
try {
|
|
125
|
-
raw = fs.readFileSync(filePath, "utf8")
|
|
127
|
+
raw = indexed?.raw ?? fs.readFileSync(filePath, "utf8")
|
|
126
128
|
} catch (err) {
|
|
127
129
|
return {
|
|
128
130
|
error: {
|
|
@@ -137,7 +139,7 @@ function parseItemFile(
|
|
|
137
139
|
|
|
138
140
|
let frontmatter: Record<string, unknown> | null
|
|
139
141
|
try {
|
|
140
|
-
frontmatter = extractFrontmatter(raw)
|
|
142
|
+
frontmatter = indexed?.frontmatter ?? extractFrontmatter(raw)
|
|
141
143
|
} catch (err) {
|
|
142
144
|
return {
|
|
143
145
|
error: {
|
|
@@ -186,11 +188,17 @@ function parseItemFile(
|
|
|
186
188
|
priority: asString(frontmatter.priority, "medium"),
|
|
187
189
|
size: asString(frontmatter.size, "—"),
|
|
188
190
|
points: asNumberOrNull(frontmatter.points),
|
|
189
|
-
...
|
|
191
|
+
...(indexed
|
|
192
|
+
? {
|
|
193
|
+
actualMinutes: indexed.actualMinutes,
|
|
194
|
+
actualMs: indexed.actualMs,
|
|
195
|
+
}
|
|
196
|
+
: readActualTimingFromWorkLog(raw)),
|
|
190
197
|
tags: asStringArray(frontmatter.tags),
|
|
191
198
|
reporters: asContributorList(frontmatter.reporters),
|
|
192
199
|
resolvers: asContributorList(frontmatter.resolvers),
|
|
193
200
|
created: asString(frontmatter.created),
|
|
201
|
+
updated: asString(frontmatter.updated),
|
|
194
202
|
completedAt: asString(frontmatter.completed_at),
|
|
195
203
|
parent: asString(frontmatter.parent, parentId),
|
|
196
204
|
epic: asString(frontmatter.epic, epicId),
|
|
@@ -211,7 +219,8 @@ function parseItemFile(
|
|
|
211
219
|
export function parseItemsForStory(
|
|
212
220
|
project: DiscoveredProject,
|
|
213
221
|
epicId: string,
|
|
214
|
-
storyId: string
|
|
222
|
+
storyId: string,
|
|
223
|
+
index?: BoardIndex
|
|
215
224
|
): StoryItemList {
|
|
216
225
|
const folder = findStoryFolder(project.boardPath, epicId, storyId)
|
|
217
226
|
if (!folder) {
|
|
@@ -246,11 +255,25 @@ export function parseItemsForStory(
|
|
|
246
255
|
folder.dirName
|
|
247
256
|
)
|
|
248
257
|
const files = listItemMarkdownFiles(storyDir)
|
|
258
|
+
const indexedByPath = index
|
|
259
|
+
? new Map(
|
|
260
|
+
(index.leavesByParent.get(storyId) ?? []).map((leaf) => [
|
|
261
|
+
leaf.filePath,
|
|
262
|
+
leaf,
|
|
263
|
+
])
|
|
264
|
+
)
|
|
265
|
+
: null
|
|
249
266
|
const items: ItemSummary[] = []
|
|
250
267
|
const errors: ItemParseError[] = []
|
|
251
268
|
|
|
252
269
|
for (const filePath of files) {
|
|
253
|
-
const result = parseItemFile(
|
|
270
|
+
const result = parseItemFile(
|
|
271
|
+
filePath,
|
|
272
|
+
project,
|
|
273
|
+
storyId,
|
|
274
|
+
epicId,
|
|
275
|
+
indexedByPath?.get(filePath)
|
|
276
|
+
)
|
|
254
277
|
if (result.item) items.push(result.item)
|
|
255
278
|
if (result.error) errors.push(result.error)
|
|
256
279
|
}
|
|
@@ -272,7 +295,8 @@ export function parseItemsForStory(
|
|
|
272
295
|
*/
|
|
273
296
|
export function parseItemsForEpic(
|
|
274
297
|
project: DiscoveredProject,
|
|
275
|
-
epicId: string
|
|
298
|
+
epicId: string,
|
|
299
|
+
index?: BoardIndex
|
|
276
300
|
): StoryItemList {
|
|
277
301
|
const epic = findEpicFolder(project.boardPath, epicId)
|
|
278
302
|
if (!epic) {
|
|
@@ -296,11 +320,25 @@ export function parseItemsForEpic(
|
|
|
296
320
|
|
|
297
321
|
const epicDir = path.join(project.boardPath, "epics", epic.dirName)
|
|
298
322
|
const files = listItemMarkdownFiles(epicDir)
|
|
323
|
+
const indexedByPath = index
|
|
324
|
+
? new Map(
|
|
325
|
+
(index.leavesByParent.get(epicId) ?? []).map((leaf) => [
|
|
326
|
+
leaf.filePath,
|
|
327
|
+
leaf,
|
|
328
|
+
])
|
|
329
|
+
)
|
|
330
|
+
: null
|
|
299
331
|
const items: ItemSummary[] = []
|
|
300
332
|
const errors: ItemParseError[] = []
|
|
301
333
|
|
|
302
334
|
for (const filePath of files) {
|
|
303
|
-
const result = parseItemFile(
|
|
335
|
+
const result = parseItemFile(
|
|
336
|
+
filePath,
|
|
337
|
+
project,
|
|
338
|
+
epicId,
|
|
339
|
+
epicId,
|
|
340
|
+
indexedByPath?.get(filePath)
|
|
341
|
+
)
|
|
304
342
|
if (result.item) items.push(result.item)
|
|
305
343
|
if (result.error) errors.push(result.error)
|
|
306
344
|
}
|
|
@@ -8,14 +8,13 @@ import type {
|
|
|
8
8
|
StorySummary,
|
|
9
9
|
} from "@/lib/taskmark/story-types"
|
|
10
10
|
import {
|
|
11
|
-
asNumberOrNull,
|
|
12
11
|
asString,
|
|
13
12
|
asStringArray,
|
|
14
13
|
extractFrontmatter,
|
|
15
14
|
} from "@/lib/taskmark/frontmatter"
|
|
16
15
|
import { asContributorList } from "@/lib/taskmark/identity"
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
16
|
+
import { deriveParentRollup } from "@/lib/taskmark/derive-parents"
|
|
17
|
+
import type { BoardIndex } from "@/lib/taskmark/board-index"
|
|
19
18
|
|
|
20
19
|
type EpicFolder = {
|
|
21
20
|
dirName: string
|
|
@@ -97,7 +96,8 @@ function listStoryMarkdownFiles(epicDir: string): string[] {
|
|
|
97
96
|
function parseStoryFile(
|
|
98
97
|
filePath: string,
|
|
99
98
|
project: DiscoveredProject,
|
|
100
|
-
epicId: string
|
|
99
|
+
epicId: string,
|
|
100
|
+
index?: BoardIndex
|
|
101
101
|
): { story?: StorySummary; error?: StoryParseError } {
|
|
102
102
|
let raw: string
|
|
103
103
|
try {
|
|
@@ -156,26 +156,28 @@ function parseStoryFile(
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
const
|
|
159
|
+
const derived = deriveParentRollup(project.boardPath, id, "story", index)
|
|
160
160
|
|
|
161
161
|
return {
|
|
162
162
|
story: {
|
|
163
163
|
id,
|
|
164
164
|
title,
|
|
165
|
-
status:
|
|
165
|
+
status: derived.status,
|
|
166
166
|
priority: asString(frontmatter.priority, "medium"),
|
|
167
|
-
size:
|
|
168
|
-
points:
|
|
169
|
-
|
|
167
|
+
size: derived.size,
|
|
168
|
+
points: derived.points,
|
|
169
|
+
actualMinutes: derived.actualMinutes,
|
|
170
|
+
actualMs: derived.actualMs,
|
|
170
171
|
tags: asStringArray(frontmatter.tags),
|
|
171
172
|
reporters: asContributorList(frontmatter.reporters),
|
|
172
|
-
resolvers:
|
|
173
|
+
resolvers: derived.resolvers,
|
|
173
174
|
created: asString(frontmatter.created),
|
|
174
|
-
|
|
175
|
+
updated: asString(frontmatter.updated),
|
|
176
|
+
completedAt: derived.completedAt,
|
|
175
177
|
parent: asString(frontmatter.parent),
|
|
176
178
|
epic: asString(frontmatter.epic, epicId),
|
|
177
|
-
workItemCount:
|
|
178
|
-
doneWorkItemCount:
|
|
179
|
+
workItemCount: derived.leafCount,
|
|
180
|
+
doneWorkItemCount: derived.doneLeafCount,
|
|
179
181
|
filePath,
|
|
180
182
|
project: {
|
|
181
183
|
id: project.id,
|
|
@@ -193,7 +195,8 @@ function parseStoryFile(
|
|
|
193
195
|
*/
|
|
194
196
|
export function parseStoriesForEpic(
|
|
195
197
|
project: DiscoveredProject,
|
|
196
|
-
epicId: string
|
|
198
|
+
epicId: string,
|
|
199
|
+
index?: BoardIndex
|
|
197
200
|
): EpicStoryList {
|
|
198
201
|
const folder = findEpicFolder(project.boardPath, epicId)
|
|
199
202
|
if (!folder) {
|
|
@@ -220,7 +223,7 @@ export function parseStoriesForEpic(
|
|
|
220
223
|
const errors: StoryParseError[] = []
|
|
221
224
|
|
|
222
225
|
for (const filePath of files) {
|
|
223
|
-
const result = parseStoryFile(filePath, project, epicId)
|
|
226
|
+
const result = parseStoryFile(filePath, project, epicId, index)
|
|
224
227
|
if (result.story) stories.push(result.story)
|
|
225
228
|
if (result.error) errors.push(result.error)
|
|
226
229
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import test from "node:test"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
POINTS_CALENDAR_WEEKS,
|
|
6
|
+
buildPointsCalendar,
|
|
7
|
+
pointsHeatLevel,
|
|
8
|
+
} from "./points-calendar"
|
|
9
|
+
|
|
10
|
+
const NOW = new Date(2026, 7, 29)
|
|
11
|
+
|
|
12
|
+
test("empty boards fill a year of weeks with zero days", () => {
|
|
13
|
+
const calendar = buildPointsCalendar([], NOW)
|
|
14
|
+
assert.equal(calendar.weeks.length, POINTS_CALENDAR_WEEKS)
|
|
15
|
+
assert.ok(calendar.weeks.every((week) => week.days.length === 7))
|
|
16
|
+
assert.equal(calendar.maxPoints, 0)
|
|
17
|
+
assert.equal(calendar.totalPoints, 0)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("a single done leaf lands on its own day", () => {
|
|
21
|
+
const calendar = buildPointsCalendar(
|
|
22
|
+
[{ completedAt: "2026-08-26T12:00:00Z", points: 5 }],
|
|
23
|
+
NOW
|
|
24
|
+
)
|
|
25
|
+
const days = calendar.weeks.flatMap((week) => week.days)
|
|
26
|
+
const filled = days.filter((day) => day.points > 0)
|
|
27
|
+
assert.equal(filled.length, 1)
|
|
28
|
+
assert.equal(filled[0]?.key, "2026-08-26")
|
|
29
|
+
assert.equal(calendar.maxPoints, 5)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("two leaves on the same day sum points", () => {
|
|
33
|
+
const calendar = buildPointsCalendar(
|
|
34
|
+
[
|
|
35
|
+
{ completedAt: "2026-08-26", points: 3 },
|
|
36
|
+
{ completedAt: "2026-08-26T18:00:00Z", points: 5 },
|
|
37
|
+
],
|
|
38
|
+
NOW
|
|
39
|
+
)
|
|
40
|
+
const day = calendar.weeks
|
|
41
|
+
.flatMap((week) => week.days)
|
|
42
|
+
.find((candidate) => candidate.key === "2026-08-26")
|
|
43
|
+
assert.equal(day?.points, 8)
|
|
44
|
+
assert.equal(calendar.totalPoints, 8)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("skips unparseable dates and non-positive points", () => {
|
|
48
|
+
const calendar = buildPointsCalendar(
|
|
49
|
+
[
|
|
50
|
+
{ completedAt: "not-a-date", points: 8 },
|
|
51
|
+
{ completedAt: "2026-08-26", points: 0 },
|
|
52
|
+
{ completedAt: "2026-08-26", points: -2 },
|
|
53
|
+
],
|
|
54
|
+
NOW
|
|
55
|
+
)
|
|
56
|
+
assert.equal(calendar.totalPoints, 0)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("marks days after today as future", () => {
|
|
60
|
+
const calendar = buildPointsCalendar([], NOW)
|
|
61
|
+
const days = calendar.weeks.flatMap((week) => week.days)
|
|
62
|
+
const today = days.find((day) => day.key === "2026-08-29")
|
|
63
|
+
const tomorrow = days.find((day) => day.key === "2026-08-30")
|
|
64
|
+
assert.equal(today?.isFuture, false)
|
|
65
|
+
assert.equal(tomorrow?.isFuture, true)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("scales intensity from empty to the calendar maximum", () => {
|
|
69
|
+
assert.equal(pointsHeatLevel(0, 8), 0)
|
|
70
|
+
assert.equal(pointsHeatLevel(2, 8), 1)
|
|
71
|
+
assert.equal(pointsHeatLevel(8, 8), 4)
|
|
72
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { addDays, endOfISOWeek, startOfDay, startOfISOWeek, subWeeks } from "date-fns"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
sumPointsByDate,
|
|
5
|
+
toDateOnlyString,
|
|
6
|
+
type SolvedCompletionSample,
|
|
7
|
+
} from "@/lib/taskmark/timeframe-filters"
|
|
8
|
+
|
|
9
|
+
/** Columns in the calendar: one year of ISO weeks ending at the current week. */
|
|
10
|
+
export const POINTS_CALENDAR_WEEKS = 53
|
|
11
|
+
|
|
12
|
+
export type PointsCalendarDay = {
|
|
13
|
+
date: Date
|
|
14
|
+
/** YYYY-MM-DD */
|
|
15
|
+
key: string
|
|
16
|
+
points: number
|
|
17
|
+
/** Days after today: rendered as blanks so the grid stays honest. */
|
|
18
|
+
isFuture: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type PointsCalendarWeek = {
|
|
22
|
+
key: string
|
|
23
|
+
days: PointsCalendarDay[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type PointsCalendar = {
|
|
27
|
+
weeks: PointsCalendarWeek[]
|
|
28
|
+
maxPoints: number
|
|
29
|
+
totalPoints: number
|
|
30
|
+
start: Date
|
|
31
|
+
end: Date
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Daily story points completed on done task/bug leaves, laid out as a
|
|
36
|
+
* contribution calendar: every column is an ISO week, every cell one day.
|
|
37
|
+
* Days without completions are zeros, never missing cells.
|
|
38
|
+
*/
|
|
39
|
+
export function buildPointsCalendar(
|
|
40
|
+
samples: readonly SolvedCompletionSample[],
|
|
41
|
+
now: Date = new Date()
|
|
42
|
+
): PointsCalendar {
|
|
43
|
+
const totals = sumPointsByDate(samples)
|
|
44
|
+
const start = startOfISOWeek(subWeeks(now, POINTS_CALENDAR_WEEKS - 1))
|
|
45
|
+
const end = endOfISOWeek(now)
|
|
46
|
+
const today = startOfDay(now)
|
|
47
|
+
|
|
48
|
+
const weeks: PointsCalendarWeek[] = []
|
|
49
|
+
let maxPoints = 0
|
|
50
|
+
let totalPoints = 0
|
|
51
|
+
let cursor = start
|
|
52
|
+
|
|
53
|
+
while (cursor.getTime() <= end.getTime()) {
|
|
54
|
+
const days: PointsCalendarDay[] = []
|
|
55
|
+
for (let offset = 0; offset < 7; offset++) {
|
|
56
|
+
const date = addDays(cursor, offset)
|
|
57
|
+
const key = toDateOnlyString(date)
|
|
58
|
+
const points = totals.get(key) ?? 0
|
|
59
|
+
maxPoints = Math.max(maxPoints, points)
|
|
60
|
+
totalPoints += points
|
|
61
|
+
days.push({
|
|
62
|
+
date,
|
|
63
|
+
key,
|
|
64
|
+
points,
|
|
65
|
+
isFuture: startOfDay(date).getTime() > today.getTime(),
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
weeks.push({ key: toDateOnlyString(cursor), days })
|
|
69
|
+
cursor = addDays(cursor, 7)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { weeks, maxPoints, totalPoints, start, end }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Discrete 0–4 intensity from empty through the calendar maximum. */
|
|
76
|
+
export function pointsHeatLevel(
|
|
77
|
+
points: number,
|
|
78
|
+
maxPoints: number
|
|
79
|
+
): 0 | 1 | 2 | 3 | 4 {
|
|
80
|
+
if (points <= 0 || maxPoints <= 0) return 0
|
|
81
|
+
const ratio = points / maxPoints
|
|
82
|
+
if (ratio <= 0.25) return 1
|
|
83
|
+
if (ratio <= 0.5) return 2
|
|
84
|
+
if (ratio <= 0.75) return 3
|
|
85
|
+
return 4
|
|
86
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { addDays } from "date-fns"
|
|
7
7
|
|
|
8
8
|
import type { ContributorIdentity } from "@/lib/taskmark/identity"
|
|
9
|
+
import type { BoardIndex } from "@/lib/taskmark/board-index"
|
|
9
10
|
import { parseEpicsForProject } from "@/lib/taskmark/parse-epics"
|
|
10
11
|
import { parseItemsForEpic, parseItemsForStory } from "@/lib/taskmark/parse-items"
|
|
11
12
|
import { parseStoriesForEpic } from "@/lib/taskmark/parse-stories"
|
|
@@ -206,9 +207,10 @@ export function computeCurrentWeekPointsDone(
|
|
|
206
207
|
|
|
207
208
|
/** Walk the board and collect metric leaves (stories, tasks, bugs + epic identities for contributors). */
|
|
208
209
|
export function collectMetricLeaves(
|
|
209
|
-
project: DiscoveredProject
|
|
210
|
+
project: DiscoveredProject,
|
|
211
|
+
index?: BoardIndex
|
|
210
212
|
): MetricLeaf[] {
|
|
211
|
-
const epicList = parseEpicsForProject(project)
|
|
213
|
+
const epicList = parseEpicsForProject(project, index)
|
|
212
214
|
const leaves: MetricLeaf[] = []
|
|
213
215
|
|
|
214
216
|
for (const epic of epicList.epics) {
|
|
@@ -222,7 +224,7 @@ export function collectMetricLeaves(
|
|
|
222
224
|
resolvers: epic.resolvers,
|
|
223
225
|
})
|
|
224
226
|
|
|
225
|
-
const storyList = parseStoriesForEpic(project, epic.id)
|
|
227
|
+
const storyList = parseStoriesForEpic(project, epic.id, index)
|
|
226
228
|
for (const story of storyList.stories) {
|
|
227
229
|
leaves.push({
|
|
228
230
|
kind: "story",
|
|
@@ -234,7 +236,7 @@ export function collectMetricLeaves(
|
|
|
234
236
|
resolvers: story.resolvers,
|
|
235
237
|
})
|
|
236
238
|
|
|
237
|
-
const items = parseItemsForStory(project, epic.id, story.id)
|
|
239
|
+
const items = parseItemsForStory(project, epic.id, story.id, index)
|
|
238
240
|
for (const item of items.items) {
|
|
239
241
|
leaves.push({
|
|
240
242
|
kind: item.type,
|
|
@@ -248,7 +250,7 @@ export function collectMetricLeaves(
|
|
|
248
250
|
}
|
|
249
251
|
}
|
|
250
252
|
|
|
251
|
-
const epicItems = parseItemsForEpic(project, epic.id)
|
|
253
|
+
const epicItems = parseItemsForEpic(project, epic.id, index)
|
|
252
254
|
for (const item of epicItems.items) {
|
|
253
255
|
leaves.push({
|
|
254
256
|
kind: item.type,
|
|
@@ -266,9 +268,11 @@ export function collectMetricLeaves(
|
|
|
266
268
|
}
|
|
267
269
|
|
|
268
270
|
export function computeProjectStatusMetrics(
|
|
269
|
-
project: DiscoveredProject
|
|
271
|
+
project: DiscoveredProject,
|
|
272
|
+
index?: BoardIndex,
|
|
273
|
+
collectedLeaves?: readonly MetricLeaf[]
|
|
270
274
|
): ProjectStatusMetrics {
|
|
271
|
-
const leaves = collectMetricLeaves(project)
|
|
275
|
+
const leaves = collectedLeaves ?? collectMetricLeaves(project, index)
|
|
272
276
|
const { total, complete } = aggregateWorkItemCounts(leaves)
|
|
273
277
|
const currentWeekPointsDone = computeCurrentWeekPointsDone(leaves)
|
|
274
278
|
const speed = computeCurrentSpeedPtsPerWeek(leaves)
|
|
@@ -25,6 +25,8 @@ export type BoardSnapshot = {
|
|
|
25
25
|
itemsByStory: Record<string, StoryItemList>
|
|
26
26
|
statusMetrics: ProjectStatusMetrics
|
|
27
27
|
countableCompletions: SolvedCompletionSample[]
|
|
28
|
+
/** Board-root CHANGELOG.md; null when missing or whitespace-only. */
|
|
29
|
+
changelogMarkdown: string | null
|
|
28
30
|
hideCompletedDefaults: Record<HideCompletedCookieKey, boolean>
|
|
29
31
|
/** Absolute file path → detail (children attached) */
|
|
30
32
|
detailsByPath: Record<string, WorkItemDetail>
|
|
@@ -8,7 +8,6 @@ export type StorySummary = {
|
|
|
8
8
|
priority: string
|
|
9
9
|
size: string
|
|
10
10
|
points: number | null
|
|
11
|
-
estimateMinutes: number | null
|
|
12
11
|
actualMinutes: number | null
|
|
13
12
|
actualMs: number | null
|
|
14
13
|
tags: string[]
|
|
@@ -16,6 +15,8 @@ export type StorySummary = {
|
|
|
16
15
|
resolvers: ContributorIdentity[]
|
|
17
16
|
/** Frontmatter created date (YYYY-MM-DD or ISO). */
|
|
18
17
|
created: string
|
|
18
|
+
/** Frontmatter updated date (YYYY-MM-DD or ISO). */
|
|
19
|
+
updated: string
|
|
19
20
|
/** Frontmatter completed_at (ISO) when solved; empty if open. */
|
|
20
21
|
completedAt: string
|
|
21
22
|
parent: string
|
|
@@ -27,13 +27,14 @@ export type TableSortState = {
|
|
|
27
27
|
direction: TableSortDirection
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/**
|
|
30
|
+
/** Static t-shirt rank: XS < S < M < L < XL < XXL. */
|
|
31
31
|
const SIZE_RANK: Record<string, number> = {
|
|
32
32
|
XS: 1,
|
|
33
33
|
S: 2,
|
|
34
34
|
M: 3,
|
|
35
35
|
L: 4,
|
|
36
36
|
XL: 5,
|
|
37
|
+
XXL: 6,
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
const ID_PREFIX_RANK: Record<string, number> = {
|
|
@@ -54,8 +55,8 @@ export function sizeSortRank(size: string | null | undefined): number | null {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
|
-
* Natural Taskmark id compare
|
|
58
|
-
*
|
|
58
|
+
* Natural Taskmark id compare for legacy numeric IDs. Collision-resistant IDs
|
|
59
|
+
* use the general natural fallback and are never constrained to an NNN shape.
|
|
59
60
|
*/
|
|
60
61
|
export function compareTaskmarkIds(a: string, b: string): number {
|
|
61
62
|
const pa = parseTaskmarkId(a)
|
package/lib/taskmark/timing.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
cell,
|
|
3
|
+
extractMarkdownBody,
|
|
4
|
+
extractSections,
|
|
5
|
+
getSection,
|
|
6
|
+
parseMarkdownTable,
|
|
7
|
+
tableRowsAsObjects,
|
|
8
|
+
} from "@/lib/taskmark/parse-sections"
|
|
4
9
|
|
|
5
10
|
export type TimingFields = {
|
|
6
|
-
estimateMinutes: number | null
|
|
7
11
|
/** Billable work-log minutes (floor of actual_ms / 60000). */
|
|
8
12
|
actualMinutes: number | null
|
|
9
13
|
/** Precise billable work-log duration in milliseconds. */
|
|
@@ -11,22 +15,29 @@ export type TimingFields = {
|
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* Actual = session time spent (prefers actual_ms; falls back to minutes / effort).
|
|
18
|
+
* Actual is the sum of closed Work log intervals stored on the leaf.
|
|
19
|
+
* Frontmatter timing fields are intentionally ignored.
|
|
17
20
|
*/
|
|
18
|
-
export function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
export function readActualTimingFromWorkLog(raw: string): TimingFields {
|
|
22
|
+
const sections = extractSections(extractMarkdownBody(raw))
|
|
23
|
+
const rows = tableRowsAsObjects(
|
|
24
|
+
parseMarkdownTable(getSection(sections, "Work log"))
|
|
25
|
+
)
|
|
26
|
+
let actualMs = 0
|
|
27
|
+
let hasClosedInterval = false
|
|
28
|
+
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
const started = Date.parse(cell(row, "started (utc)", "started"))
|
|
31
|
+
const ended = Date.parse(cell(row, "ended (utc)", "ended"))
|
|
32
|
+
if (!Number.isFinite(started) || !Number.isFinite(ended) || ended < started) {
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
hasClosedInterval = true
|
|
36
|
+
actualMs += ended - started
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
return {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
actualMs,
|
|
40
|
+
actualMinutes: hasClosedInterval ? Math.floor(actualMs / 60_000) : null,
|
|
41
|
+
actualMs: hasClosedInterval ? actualMs : null,
|
|
31
42
|
}
|
|
32
43
|
}
|
package/lib/taskmark/validate.ts
CHANGED
|
@@ -64,7 +64,7 @@ export function validateMasterFolder(
|
|
|
64
64
|
ok: false,
|
|
65
65
|
code: "no_projects",
|
|
66
66
|
error:
|
|
67
|
-
"No Taskmark projects found in subfolders. Look for a nested taskmark/ board
|
|
67
|
+
"No Taskmark projects found in subfolders. Look for a nested taskmark/epics/ board, or a dedicated *-taskmark folder with epics/ at its root.",
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import test from "node:test"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
parseThemePreference,
|
|
6
|
+
themeCookieDomain,
|
|
7
|
+
} from "./theme-cookie"
|
|
8
|
+
|
|
9
|
+
test("accepts only light or dark cookie values", () => {
|
|
10
|
+
assert.equal(parseThemePreference("light"), "light")
|
|
11
|
+
assert.equal(parseThemePreference("dark"), "dark")
|
|
12
|
+
assert.equal(parseThemePreference("system"), undefined)
|
|
13
|
+
assert.equal(parseThemePreference(""), undefined)
|
|
14
|
+
assert.equal(parseThemePreference(undefined), undefined)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test("uses the parent domain on hosted Taskmark hosts", () => {
|
|
18
|
+
assert.equal(themeCookieDomain("taskmark.dev"), ".taskmark.dev")
|
|
19
|
+
assert.equal(themeCookieDomain("board.taskmark.dev"), ".taskmark.dev")
|
|
20
|
+
assert.equal(themeCookieDomain("www.taskmark.dev"), ".taskmark.dev")
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("omits Domain on localhost and unrelated hosts", () => {
|
|
24
|
+
assert.equal(themeCookieDomain("localhost"), undefined)
|
|
25
|
+
assert.equal(themeCookieDomain("127.0.0.1"), undefined)
|
|
26
|
+
assert.equal(themeCookieDomain("example.com"), undefined)
|
|
27
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const THEME_COOKIE_NAME = "taskmark_theme"
|
|
2
|
+
export const THEME_STORAGE_KEY = "taskmark_theme"
|
|
3
|
+
export const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365
|
|
4
|
+
export const THEME_COOKIE_PARENT_DOMAIN = ".taskmark.dev"
|
|
5
|
+
|
|
6
|
+
export type ThemePreference = "light" | "dark"
|
|
7
|
+
|
|
8
|
+
export function parseThemePreference(
|
|
9
|
+
value: string | null | undefined
|
|
10
|
+
): ThemePreference | undefined {
|
|
11
|
+
if (value === "light" || value === "dark") return value
|
|
12
|
+
return undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Parent domain for hosted site + board; omit on localhost and other hosts. */
|
|
16
|
+
export function themeCookieDomain(
|
|
17
|
+
hostname: string
|
|
18
|
+
): string | undefined {
|
|
19
|
+
if (hostname === "taskmark.dev" || hostname.endsWith(".taskmark.dev")) {
|
|
20
|
+
return THEME_COOKIE_PARENT_DOMAIN
|
|
21
|
+
}
|
|
22
|
+
return undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function writeThemeCookie(theme: ThemePreference): void {
|
|
26
|
+
if (typeof document === "undefined") return
|
|
27
|
+
const parts = [
|
|
28
|
+
`${THEME_COOKIE_NAME}=${theme}`,
|
|
29
|
+
"path=/",
|
|
30
|
+
`max-age=${THEME_COOKIE_MAX_AGE}`,
|
|
31
|
+
"SameSite=Lax",
|
|
32
|
+
]
|
|
33
|
+
const domain = themeCookieDomain(window.location.hostname)
|
|
34
|
+
if (domain) parts.push(`domain=${domain}`)
|
|
35
|
+
if (window.location.protocol === "https:") parts.push("Secure")
|
|
36
|
+
document.cookie = parts.join("; ")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Runs before paint: cookie wins, else localStorage, else OS.
|
|
41
|
+
* Also copies a cookie preference into localStorage so next-themes agrees.
|
|
42
|
+
*/
|
|
43
|
+
export const THEME_INIT_SCRIPT = `(function(){try{var n=${JSON.stringify(THEME_COOKIE_NAME)};var k=${JSON.stringify(THEME_STORAGE_KEY)};var m=document.cookie.match(new RegExp("(?:^|; )"+n+"=([^;]*)"));var stored=m?decodeURIComponent(m[1]):null;function ok(v){return v==="light"||v==="dark"}if(!ok(stored)){stored=null;try{stored=localStorage.getItem(k);if(!ok(stored))stored=localStorage.getItem("theme")}catch(e){stored=null}if(!ok(stored))stored=null}if(ok(stored)){try{localStorage.setItem(k,stored)}catch(e){}}var dark=stored==="dark"||(stored!=="light"&&window.matchMedia("(prefers-color-scheme: dark)").matches);document.documentElement.classList.toggle("dark",dark);document.documentElement.style.colorScheme=dark?"dark":"light"}catch(e){}})();`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taskmark/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Taskmark board UI — run globally for setup/project picker, or install in a board folder to open that board with `npx taskmark`",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"serve": "node ./bin/taskmark.js serve",
|
|
42
42
|
"preview": "node ./bin/taskmark.js preview",
|
|
43
43
|
"prepack": "npm run build",
|
|
44
|
+
"test": "tsx --test \"lib/**/*.test.ts\"",
|
|
44
45
|
"lint": "eslint"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
"date-fns": "^4.4.0",
|
|
56
57
|
"esbuild": "^0.25.0",
|
|
57
58
|
"lucide-react": "^1.25.0",
|
|
59
|
+
"mermaid": "^11.17.2",
|
|
58
60
|
"next": "16.2.10",
|
|
59
61
|
"next-themes": "^0.4.6",
|
|
60
62
|
"react": "19.2.4",
|