@taskmark/ui 0.3.0 → 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.
Files changed (301) hide show
  1. package/README.md +1 -1
  2. package/app/globals.css +25 -0
  3. package/app/layout.tsx +15 -5
  4. package/app/setup/page.tsx +1 -1
  5. package/bin/lib/resolve-board.mjs +1 -4
  6. package/components/board/app-bar.tsx +3 -0
  7. package/components/board/board-screen.tsx +61 -117
  8. package/components/board/changelog-panel.tsx +26 -0
  9. package/components/board/epic-list.tsx +2 -0
  10. package/components/board/list-view-switcher.tsx +7 -3
  11. package/components/board/markdown-content.tsx +21 -1
  12. package/components/board/mermaid-diagram.tsx +88 -0
  13. package/components/board/metric-stat-card.tsx +4 -4
  14. package/components/board/overall-tree-list.tsx +467 -0
  15. package/components/board/overall-work-items-list.tsx +5 -1
  16. package/components/board/points-heatmap.tsx +183 -0
  17. package/components/board/project-status-metrics-strip.tsx +5 -22
  18. package/components/board/static-board-app.tsx +29 -94
  19. package/components/board/status-badge.tsx +8 -0
  20. package/components/board/story-list.tsx +2 -0
  21. package/components/board/task-list.tsx +5 -1
  22. package/components/board/work-item-detail-body.tsx +123 -8
  23. package/components/board/work-items-list.tsx +2 -0
  24. package/components/theme-provider.tsx +39 -0
  25. package/components/theme-toggle.tsx +27 -0
  26. package/components/ui/sonner.tsx +2 -2
  27. package/dist/prod-next/BUILD_ID +1 -1
  28. package/dist/prod-next/build-manifest.json +6 -6
  29. package/dist/prod-next/react-loadable-manifest.json +391 -1
  30. package/dist/prod-next/server/app/_global-error/page.js +2 -2
  31. package/dist/prod-next/server/app/_global-error/page.js.nft.json +1 -1
  32. package/dist/prod-next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  33. package/dist/prod-next/server/app/_global-error.html +1 -1
  34. package/dist/prod-next/server/app/_global-error.rsc +1 -1
  35. package/dist/prod-next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  36. package/dist/prod-next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  37. package/dist/prod-next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  38. package/dist/prod-next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  39. package/dist/prod-next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  40. package/dist/prod-next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  41. package/dist/prod-next/server/app/_not-found/page.js +2 -2
  42. package/dist/prod-next/server/app/_not-found/page.js.nft.json +1 -1
  43. package/dist/prod-next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  44. package/dist/prod-next/server/app/_not-found.html +1 -1
  45. package/dist/prod-next/server/app/_not-found.rsc +19 -17
  46. package/dist/prod-next/server/app/_not-found.segments/_full.segment.rsc +19 -17
  47. package/dist/prod-next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  48. package/dist/prod-next/server/app/_not-found.segments/_index.segment.rsc +8 -6
  49. package/dist/prod-next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  50. package/dist/prod-next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  51. package/dist/prod-next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  52. package/dist/prod-next/server/app/api/taskmark-reload/route.js +1 -1
  53. package/dist/prod-next/server/app/api/taskmark-reload/route.js.nft.json +1 -1
  54. package/dist/prod-next/server/app/board/page.js +2 -2
  55. package/dist/prod-next/server/app/board/page.js.nft.json +1 -1
  56. package/dist/prod-next/server/app/board/page_client-reference-manifest.js +1 -1
  57. package/dist/prod-next/server/app/favicon.ico/route.js +1 -1
  58. package/dist/prod-next/server/app/favicon.ico/route.js.nft.json +1 -1
  59. package/dist/prod-next/server/app/page.js +2 -2
  60. package/dist/prod-next/server/app/page.js.nft.json +1 -1
  61. package/dist/prod-next/server/app/page_client-reference-manifest.js +1 -1
  62. package/dist/prod-next/server/app/setup/page.js +2 -2
  63. package/dist/prod-next/server/app/setup/page.js.nft.json +1 -1
  64. package/dist/prod-next/server/app/setup/page_client-reference-manifest.js +1 -1
  65. package/dist/prod-next/server/chunks/102.js +90 -0
  66. package/dist/prod-next/server/chunks/11.js +1 -0
  67. package/dist/prod-next/server/chunks/1286.js +1 -0
  68. package/dist/prod-next/server/chunks/1412.js +38 -0
  69. package/dist/prod-next/server/chunks/1605.js +1 -0
  70. package/dist/prod-next/server/chunks/169.js +1 -0
  71. package/dist/prod-next/server/chunks/18.js +86 -0
  72. package/dist/prod-next/server/chunks/2.js +1 -0
  73. package/dist/prod-next/server/chunks/2022.js +1 -0
  74. package/dist/prod-next/server/chunks/2156.js +1 -0
  75. package/dist/prod-next/server/chunks/2157.js +1 -0
  76. package/dist/prod-next/server/chunks/2325.js +82 -0
  77. package/dist/prod-next/server/chunks/2838.js +1 -0
  78. package/dist/prod-next/server/chunks/2897.js +79 -0
  79. package/dist/prod-next/server/chunks/3227.js +1 -0
  80. package/dist/prod-next/server/chunks/3266.js +1 -0
  81. package/dist/prod-next/server/chunks/{192.js → 3307.js} +5 -5
  82. package/dist/prod-next/server/chunks/3387.js +19 -0
  83. package/dist/prod-next/server/chunks/3428.js +32 -0
  84. package/dist/prod-next/server/chunks/3479.js +1 -0
  85. package/dist/prod-next/server/chunks/367.js +115 -0
  86. package/dist/prod-next/server/chunks/3698.js +93 -0
  87. package/dist/prod-next/server/chunks/3709.js +148 -0
  88. package/dist/prod-next/server/chunks/41.js +1 -0
  89. package/dist/prod-next/server/chunks/4107.js +62 -0
  90. package/dist/prod-next/server/chunks/4569.js +34 -0
  91. package/dist/prod-next/server/chunks/{741.js → 4741.js} +1 -1
  92. package/dist/prod-next/server/chunks/4755.js +153 -0
  93. package/dist/prod-next/server/chunks/5002.js +1 -0
  94. package/dist/prod-next/server/chunks/5112.js +1 -0
  95. package/dist/prod-next/server/chunks/5289.js +8 -0
  96. package/dist/prod-next/server/chunks/5318.js +1 -0
  97. package/dist/prod-next/server/chunks/5452.js +23 -0
  98. package/dist/prod-next/server/chunks/5453.js +1 -0
  99. package/dist/prod-next/server/chunks/5749.js +27 -0
  100. package/dist/prod-next/server/chunks/5782.js +36 -0
  101. package/dist/prod-next/server/chunks/6163.js +104 -0
  102. package/dist/prod-next/server/chunks/6169.js +64 -0
  103. package/dist/prod-next/server/chunks/6208.js +172 -0
  104. package/dist/prod-next/server/chunks/6399.js +24 -0
  105. package/dist/prod-next/server/chunks/6496.js +1 -0
  106. package/dist/prod-next/server/chunks/6823.js +215 -0
  107. package/dist/prod-next/server/chunks/6888.js +1 -0
  108. package/dist/prod-next/server/chunks/6980.js +1 -0
  109. package/dist/prod-next/server/chunks/7245.js +77 -0
  110. package/dist/prod-next/server/chunks/7307.js +1 -0
  111. package/dist/prod-next/server/chunks/7372.js +287 -0
  112. package/dist/prod-next/server/chunks/7384.js +226 -0
  113. package/dist/prod-next/server/chunks/7516.js +1 -0
  114. package/dist/prod-next/server/chunks/7531.js +24 -0
  115. package/dist/prod-next/server/chunks/7591.js +1 -0
  116. package/dist/prod-next/server/chunks/7755.js +138 -0
  117. package/dist/prod-next/server/chunks/7916.js +1 -0
  118. package/dist/prod-next/server/chunks/8078.js +94 -0
  119. package/dist/prod-next/server/chunks/8253.js +41 -0
  120. package/dist/prod-next/server/chunks/8635.js +1 -0
  121. package/dist/prod-next/server/chunks/8688.js +1 -0
  122. package/dist/prod-next/server/chunks/8872.js +1 -0
  123. package/dist/prod-next/server/chunks/9006.js +1 -0
  124. package/dist/prod-next/server/chunks/9018.js +1 -0
  125. package/dist/prod-next/server/chunks/9047.js +1 -0
  126. package/dist/prod-next/server/chunks/9108.js +1 -0
  127. package/dist/prod-next/server/chunks/9280.js +256 -0
  128. package/dist/prod-next/server/chunks/9419.js +1 -0
  129. package/dist/prod-next/server/chunks/9430.js +1 -0
  130. package/dist/prod-next/server/chunks/9586.js +1 -0
  131. package/dist/prod-next/server/chunks/9775.js +1 -0
  132. package/dist/prod-next/server/chunks/992.js +1 -0
  133. package/dist/prod-next/server/chunks/9926.js +1 -0
  134. package/dist/prod-next/server/chunks/9984.js +8 -0
  135. package/dist/prod-next/server/middleware-build-manifest.js +1 -1
  136. package/dist/prod-next/server/middleware-react-loadable-manifest.js +1 -1
  137. package/dist/prod-next/server/pages/404.html +1 -1
  138. package/dist/prod-next/server/pages/500.html +1 -1
  139. package/dist/prod-next/server/server-reference-manifest.js +1 -1
  140. package/dist/prod-next/server/server-reference-manifest.json +1 -1
  141. package/dist/prod-next/server/webpack-runtime.js +1 -1
  142. package/dist/prod-next/static/chunks/1213.e5c6e70a79b5f7e6.js +24 -0
  143. package/dist/prod-next/static/chunks/1519.2f596e5f5af9ef57.js +62 -0
  144. package/dist/prod-next/static/chunks/1759-f5b8fb456f8fb420.js +1 -0
  145. package/dist/prod-next/static/chunks/1974.885c9a133b86d8a4.js +32 -0
  146. package/dist/prod-next/static/chunks/2280.6a7f540787704698.js +1 -0
  147. package/dist/prod-next/static/chunks/2521.7f745f50d30076e7.js +77 -0
  148. package/dist/prod-next/static/chunks/2585.4d6aa847c780c666.js +1 -0
  149. package/dist/prod-next/static/chunks/2589.1aad68c09021b1ac.js +1 -0
  150. package/dist/prod-next/static/chunks/2600.45ba17a0f756fc31.js +1 -0
  151. package/dist/prod-next/static/chunks/2619.9aac8983f30c7c8a.js +1 -0
  152. package/dist/prod-next/static/chunks/26d302b2.9f6383e42abdfe16.js +122 -0
  153. package/dist/prod-next/static/chunks/2768.305914f7900745b6.js +1 -0
  154. package/dist/prod-next/static/chunks/2931.c2acdbf72808b646.js +41 -0
  155. package/dist/prod-next/static/chunks/3012-838f69b72a0dc1b3.js +1 -0
  156. package/dist/prod-next/static/chunks/3016.406e568aa95ff867.js +1 -0
  157. package/dist/prod-next/static/chunks/3022.972047558a20553a.js +172 -0
  158. package/dist/prod-next/static/chunks/3064.3a2a1e7cd4fdca5f.js +1 -0
  159. package/dist/prod-next/static/chunks/3174.03fbed7b7d7a8256.js +1 -0
  160. package/dist/prod-next/static/chunks/3189.b048866768087750.js +1 -0
  161. package/dist/prod-next/static/chunks/3217.1a053843c58b70b5.js +24 -0
  162. package/dist/prod-next/static/chunks/3308.30b95d524f9c1daf.js +74 -0
  163. package/dist/prod-next/static/chunks/331.fc5d78e1e91daf43.js +1 -0
  164. package/dist/prod-next/static/chunks/3510.5d4d1c79533e080c.js +1 -0
  165. package/dist/prod-next/static/chunks/364.ce5400a9dd70b9dc.js +1 -0
  166. package/dist/prod-next/static/chunks/3661-1f08d73aaffb011d.js +1 -0
  167. package/dist/prod-next/static/chunks/370.57cfa27da8567918.js +1 -0
  168. package/dist/prod-next/static/chunks/3789.c418b69121761983.js +1 -0
  169. package/dist/prod-next/static/chunks/3794-c66b3d8325973459.js +32 -0
  170. package/dist/prod-next/static/chunks/3831.25ae62d10dccc27a.js +1 -0
  171. package/dist/prod-next/static/chunks/395.b41e9d76613d37b5.js +90 -0
  172. package/dist/prod-next/static/chunks/3a7aa434.93dbd20997cfd24f.js +82 -0
  173. package/dist/prod-next/static/chunks/4020.3cc19c463c49f969.js +23 -0
  174. package/dist/prod-next/static/chunks/4035.0acc428926d5c8dd.js +1 -0
  175. package/dist/prod-next/static/chunks/405.0902e4f6eb7bd9a2.js +215 -0
  176. package/dist/prod-next/static/chunks/4159.49ae537ff687bdc6.js +138 -0
  177. package/dist/prod-next/static/chunks/4208.384b381ca2757643.js +287 -0
  178. package/dist/prod-next/static/chunks/4263.ffa8d9bd8a0faaae.js +1 -0
  179. package/dist/prod-next/static/chunks/4561.d1a3aaca8bae3224.js +34 -0
  180. package/dist/prod-next/static/chunks/4592.d0f37bc216b9d988.js +93 -0
  181. package/dist/prod-next/static/chunks/4799.0dc7aa21bff2fd2a.js +1 -0
  182. package/dist/prod-next/static/chunks/4bd1b696-e356ca5ba0218e27.js +1 -0
  183. package/dist/prod-next/static/chunks/514.b804c51b26f5e126.js +1 -0
  184. package/dist/prod-next/static/chunks/5270.28592864d38e881c.js +1 -0
  185. package/dist/prod-next/static/chunks/52733464.3870f99cc446f9ff.js +1 -0
  186. package/dist/prod-next/static/chunks/5354.b559c717a607dc5c.js +1 -0
  187. package/dist/prod-next/static/chunks/5448.d3972d9fb464b54f.js +38 -0
  188. package/dist/prod-next/static/chunks/587.d52737d7288c11b4.js +1 -0
  189. package/dist/prod-next/static/chunks/5909.9e37f7b495b11995.js +115 -0
  190. package/dist/prod-next/static/chunks/5949.5504b3545fcc3d34.js +104 -0
  191. package/dist/prod-next/static/chunks/6322-4e97920c006f7933.js +1 -0
  192. package/dist/prod-next/static/chunks/6361.893d04ac34fa1b49.js +148 -0
  193. package/dist/prod-next/static/chunks/6774.8cd53427dbf32cac.js +86 -0
  194. package/dist/prod-next/static/chunks/6968.b80277c724dbc96e.js +1 -0
  195. package/dist/prod-next/static/chunks/7113.4b9e7ea870aa300a.js +8 -0
  196. package/dist/prod-next/static/chunks/7236.55a9728d35f5b8a6.js +1 -0
  197. package/dist/prod-next/static/chunks/7390.dde9b5bc2f6e24b8.js +1 -0
  198. package/dist/prod-next/static/chunks/7806.a903780fcfa3a30e.js +1 -0
  199. package/dist/prod-next/static/chunks/7997.be57e92e583fe635.js +1 -0
  200. package/dist/prod-next/static/chunks/8216.64a72d4d35ea7be0.js +1 -0
  201. package/dist/prod-next/static/chunks/830.5d5274d7ad1cb58e.js +1 -0
  202. package/dist/prod-next/static/chunks/8378.fa91de3406f554c9.js +226 -0
  203. package/dist/prod-next/static/chunks/8501.cd06aa1b0c7e1c6a.js +79 -0
  204. package/dist/prod-next/static/chunks/8663.6ec5792fb4952e45.js +27 -0
  205. package/dist/prod-next/static/chunks/872.96937e758ddce860.js +1 -0
  206. package/dist/prod-next/static/chunks/8803.75bde095fe3613f5.js +1 -0
  207. package/dist/prod-next/static/chunks/8863.72f5dbb820a0e4c3.js +1 -0
  208. package/dist/prod-next/static/chunks/90542734.f7a7b2972ef5a749.js +1 -0
  209. package/dist/prod-next/static/chunks/9379.5bc9c9343ec5a16c.js +64 -0
  210. package/dist/prod-next/static/chunks/9389.906fe7aa9cd6a170.js +1 -0
  211. package/dist/prod-next/static/chunks/9472.8818ee52cfa931a4.js +94 -0
  212. package/dist/prod-next/static/chunks/966.d6babbf8119162cc.js +1 -0
  213. package/dist/prod-next/static/chunks/9884.d4ac1a961b3366f1.js +36 -0
  214. package/dist/prod-next/static/chunks/9909.67cf9940b125243b.js +1 -0
  215. package/dist/prod-next/static/chunks/app/_global-error/page-8be5492b3b872a4c.js +1 -0
  216. package/dist/prod-next/static/chunks/app/_not-found/page-799e445a538b0024.js +1 -0
  217. package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-8be5492b3b872a4c.js +1 -0
  218. package/dist/prod-next/static/chunks/app/board/page-5c181f45738daf1c.js +1 -0
  219. package/dist/prod-next/static/chunks/app/layout-21782e6d13a6f275.js +1 -0
  220. package/dist/prod-next/static/chunks/app/page-51bfe4356fe233e3.js +1 -0
  221. package/dist/prod-next/static/chunks/app/setup/page-3ccfba00c10f9b17.js +1 -0
  222. package/dist/prod-next/static/chunks/d3ac728e.7964f816a1ca64e5.js +1 -0
  223. package/dist/prod-next/static/chunks/e413bedd.3a12a8cb02e28fdf.js +153 -0
  224. package/dist/prod-next/static/chunks/ea233666.f00b64a7d99b4950.js +62 -0
  225. package/dist/prod-next/static/chunks/framework-711ef29bc66f648c.js +1 -0
  226. package/dist/prod-next/static/chunks/main-app-dd26d4f9d120646e.js +1 -0
  227. package/dist/prod-next/static/chunks/{main-bb33c46a9c25dee8.js → main-df4bcaed74f274eb.js} +2 -2
  228. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-8be5492b3b872a4c.js +1 -0
  229. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-8be5492b3b872a4c.js +1 -0
  230. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/global-error-45a0cf77e03361ed.js +1 -0
  231. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-8be5492b3b872a4c.js +1 -0
  232. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-8be5492b3b872a4c.js +1 -0
  233. package/dist/prod-next/static/chunks/webpack-713747aaa9716c4c.js +1 -0
  234. package/dist/prod-next/static/css/cb78d4b0062d9a3f.css +1 -0
  235. package/dist/public/taskmark-snapshot.json +1 -1
  236. package/lib/taskmark/board-index.ts +101 -0
  237. package/lib/taskmark/build-snapshot.ts +28 -7
  238. package/lib/taskmark/derive-parents.test.ts +23 -0
  239. package/lib/taskmark/derive-parents.ts +323 -0
  240. package/lib/taskmark/detail-load.ts +55 -7
  241. package/lib/taskmark/detail-types.ts +10 -2
  242. package/lib/taskmark/dev-reload-token.ts +1 -1
  243. package/lib/taskmark/discover.ts +4 -7
  244. package/lib/taskmark/epic-types.ts +2 -1
  245. package/lib/taskmark/flat-work-item-types.ts +1 -4
  246. package/lib/taskmark/identity.ts +3 -3
  247. package/lib/taskmark/item-types.ts +2 -1
  248. package/lib/taskmark/list-filters.test.ts +23 -0
  249. package/lib/taskmark/list-filters.ts +7 -2
  250. package/lib/taskmark/list-view-mode.test.ts +45 -0
  251. package/lib/taskmark/list-view-mode.ts +19 -8
  252. package/lib/taskmark/load-changelog.test.ts +32 -0
  253. package/lib/taskmark/load-changelog.ts +18 -0
  254. package/lib/taskmark/overall-tree.test.ts +196 -0
  255. package/lib/taskmark/overall-tree.ts +194 -0
  256. package/lib/taskmark/parse-detail.ts +5 -5
  257. package/lib/taskmark/parse-epics.ts +19 -16
  258. package/lib/taskmark/parse-flat-lists.ts +32 -28
  259. package/lib/taskmark/parse-items.ts +47 -9
  260. package/lib/taskmark/parse-stories.ts +18 -15
  261. package/lib/taskmark/points-calendar.test.ts +72 -0
  262. package/lib/taskmark/points-calendar.ts +86 -0
  263. package/lib/taskmark/project-metrics.ts +11 -7
  264. package/lib/taskmark/snapshot-types.ts +2 -0
  265. package/lib/taskmark/story-types.ts +2 -1
  266. package/lib/taskmark/table-sort.ts +4 -3
  267. package/lib/taskmark/timing.ts +29 -18
  268. package/lib/taskmark/validate.ts +1 -1
  269. package/lib/theme-cookie.test.ts +27 -0
  270. package/lib/theme-cookie.ts +43 -0
  271. package/package.json +3 -1
  272. package/public/taskmark-snapshot.json +1 -1
  273. package/dist/prod-next/server/chunks/463.js +0 -1
  274. package/dist/prod-next/server/chunks/54.js +0 -19
  275. package/dist/prod-next/server/chunks/57.js +0 -8
  276. package/dist/prod-next/server/chunks/724.js +0 -1
  277. package/dist/prod-next/static/chunks/4bd1b696-deb4a0a1da1923b0.js +0 -1
  278. package/dist/prod-next/static/chunks/613-6f794154d28bb75a.js +0 -1
  279. package/dist/prod-next/static/chunks/636-626c380f021b95c8.js +0 -1
  280. package/dist/prod-next/static/chunks/669-8103441c67b185da.js +0 -1
  281. package/dist/prod-next/static/chunks/701-df3ee5f1cf837b1a.js +0 -1
  282. package/dist/prod-next/static/chunks/794-25668765560cb197.js +0 -32
  283. package/dist/prod-next/static/chunks/909.932f2af3be7f1e89.js +0 -1
  284. package/dist/prod-next/static/chunks/app/_global-error/page-a1e3320be3da9be1.js +0 -1
  285. package/dist/prod-next/static/chunks/app/_not-found/page-0b3785af737e7968.js +0 -1
  286. package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-a1e3320be3da9be1.js +0 -1
  287. package/dist/prod-next/static/chunks/app/board/page-636f60191db62f52.js +0 -1
  288. package/dist/prod-next/static/chunks/app/layout-856e8a282e69e7d0.js +0 -1
  289. package/dist/prod-next/static/chunks/app/page-562a33b50da7665b.js +0 -1
  290. package/dist/prod-next/static/chunks/app/setup/page-4cf527b758b1d51e.js +0 -1
  291. package/dist/prod-next/static/chunks/framework-b29f9083d3cda3b2.js +0 -1
  292. package/dist/prod-next/static/chunks/main-app-bfb36455d24b3e5a.js +0 -1
  293. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-a1e3320be3da9be1.js +0 -1
  294. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-a1e3320be3da9be1.js +0 -1
  295. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/global-error-dc53ab29e0491fe4.js +0 -1
  296. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-a1e3320be3da9be1.js +0 -1
  297. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-a1e3320be3da9be1.js +0 -1
  298. package/dist/prod-next/static/chunks/webpack-d33b8a6298c94a64.js +0 -1
  299. package/dist/prod-next/static/css/2e6ec4c80894e3c0.css +0 -1
  300. /package/dist/prod-next/static/{yyRnYAFQrJjF5_b5ZIeqh → EI1zQaa_kQ7DqUSE4o3Qo}/_buildManifest.js +0 -0
  301. /package/dist/prod-next/static/{yyRnYAFQrJjF5_b5ZIeqh → 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 { readTimingFields } from "@/lib/taskmark/timing"
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
- ...readTimingFields(frontmatter),
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(filePath, project, storyId, epicId)
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(filePath, project, epicId, epicId)
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 { readTimingFields } from "@/lib/taskmark/timing"
18
- import { progressUnderStory } from "@/lib/taskmark/count-leaves"
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 progress = progressUnderStory(project.boardPath, epicId, id)
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: asString(frontmatter.status, "unknown"),
165
+ status: derived.status,
166
166
  priority: asString(frontmatter.priority, "medium"),
167
- size: asString(frontmatter.size, "—"),
168
- points: asNumberOrNull(frontmatter.points),
169
- ...readTimingFields(frontmatter),
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: asContributorList(frontmatter.resolvers),
173
+ resolvers: derived.resolvers,
173
174
  created: asString(frontmatter.created),
174
- completedAt: asString(frontmatter.completed_at),
175
+ updated: asString(frontmatter.updated),
176
+ completedAt: derived.completedAt,
175
177
  parent: asString(frontmatter.parent),
176
178
  epic: asString(frontmatter.epic, epicId),
177
- workItemCount: progress.total,
178
- doneWorkItemCount: progress.done,
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
- /** T-shirt rank: XS < S < M < L < XL. Unknown / empty sorts last. */
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: prefix (E/S/T/B) then numeric suffix.
58
- * Falls back to case-insensitive string compare.
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)
@@ -1,9 +1,13 @@
1
1
  import {
2
- asNumberOrNull,
3
- } from "@/lib/taskmark/frontmatter"
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
- * Read estimate and actual from board frontmatter.
15
- * Est = optional/historical planned minutes (sizing no longer suggests time).
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 readTimingFields(
19
- frontmatter: Record<string, unknown>
20
- ): TimingFields {
21
- const actualMinutes =
22
- asNumberOrNull(frontmatter.actual_minutes) ??
23
- asNumberOrNull(frontmatter.effort_minutes)
24
- const actualMs =
25
- asNumberOrNull(frontmatter.actual_ms) ??
26
- (actualMinutes != null ? actualMinutes * 60_000 : null)
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
- estimateMinutes: asNumberOrNull(frontmatter.estimate_minutes),
29
- actualMinutes,
30
- actualMs,
40
+ actualMinutes: hasClosedInterval ? Math.floor(actualMs / 60_000) : null,
41
+ actualMs: hasClosedInterval ? actualMs : null,
31
42
  }
32
43
  }
@@ -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 (INDEX.md or epics/), or a dedicated *-taskmark folder with those files at its root.",
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.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",