@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
@@ -0,0 +1,467 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronRightIcon } from "lucide-react"
5
+
6
+ import { AttributionAvatarGroup } from "@/components/board/attribution-avatars"
7
+ import { ChildProgressBar } from "@/components/board/child-progress-bar"
8
+ import {
9
+ IdCreatedTooltip,
10
+ SizeWithPointsTooltip,
11
+ StatusWithSolvedTooltip,
12
+ } from "@/components/board/date-tooltip"
13
+ import { ListFiltersBar } from "@/components/board/list-filters-bar"
14
+ import { ListPagination } from "@/components/board/list-pagination"
15
+ import {
16
+ statusRowClass,
17
+ TypeBadge,
18
+ } from "@/components/board/status-badge"
19
+ import { TimeframeFilter } from "@/components/board/timeframe-filter"
20
+ import { ViewWorkItemButton } from "@/components/board/work-item-sheet"
21
+ import {
22
+ Card,
23
+ CardAction,
24
+ CardContent,
25
+ CardHeader,
26
+ CardTitle,
27
+ } from "@/components/ui/card"
28
+ import { Button } from "@/components/ui/button"
29
+ import { usePaginatedRows } from "@/hooks/use-paginated-rows"
30
+ import { usePersistedHideCompleted } from "@/hooks/use-persisted-hide-completed"
31
+ import { displayFileName } from "@/lib/display-path"
32
+ import type { ProjectEpicList } from "@/lib/taskmark/epic-types"
33
+ import type { EpicWorkItemsList } from "@/lib/taskmark/flat-work-item-types"
34
+ import type { StoryItemList } from "@/lib/taskmark/item-types"
35
+ import {
36
+ DEFAULT_TIMEFRAME_FILTER,
37
+ type TimeframeFilterState,
38
+ } from "@/lib/taskmark/list-filters"
39
+ import {
40
+ buildOverallTree,
41
+ filterOverallTree,
42
+ searchRevealIds,
43
+ type OverallTreeNode,
44
+ } from "@/lib/taskmark/overall-tree"
45
+ import type { SolvedCompletionSample } from "@/lib/taskmark/timeframe-filters"
46
+ import { cn } from "@/lib/utils"
47
+
48
+ type OverallTreeListProps = {
49
+ list: ProjectEpicList
50
+ workItemsByEpic: Record<string, EpicWorkItemsList>
51
+ itemsByStory: Record<string, StoryItemList>
52
+ selectedEpicId?: string | null
53
+ selectedStoryId?: string | null
54
+ countableCompletions?: readonly SolvedCompletionSample[]
55
+ initialHideCompleted?: boolean
56
+ }
57
+
58
+ function allNodes(nodes: readonly OverallTreeNode[]): OverallTreeNode[] {
59
+ return nodes.flatMap((node) => [node, ...allNodes(node.children)])
60
+ }
61
+
62
+ function expansionStorageKey(projectId: string): string {
63
+ return `taskmark:overall-expanded:${projectId}`
64
+ }
65
+
66
+ function readExpanded(projectId: string): string[] {
67
+ if (typeof window === "undefined") return []
68
+ try {
69
+ const parsed = JSON.parse(
70
+ window.sessionStorage.getItem(expansionStorageKey(projectId)) ?? "[]"
71
+ )
72
+ return Array.isArray(parsed)
73
+ ? parsed.filter((value): value is string => typeof value === "string")
74
+ : []
75
+ } catch {
76
+ return []
77
+ }
78
+ }
79
+
80
+ function TreeNodeRow({
81
+ node,
82
+ level,
83
+ expanded,
84
+ selectedEpicId,
85
+ selectedStoryId,
86
+ onToggle,
87
+ }: {
88
+ node: OverallTreeNode
89
+ level: number
90
+ expanded: ReadonlySet<string>
91
+ selectedEpicId: string | null
92
+ selectedStoryId: string | null
93
+ onToggle: (id: string) => void
94
+ }) {
95
+ const expandable = node.kind === "epic" || node.kind === "story"
96
+ const isExpanded = expandable && expanded.has(node.id)
97
+ const selected =
98
+ (node.kind === "epic" && node.id === selectedEpicId) ||
99
+ (node.kind === "story" && node.id === selectedStoryId)
100
+ const emptyMessage =
101
+ node.kind === "epic"
102
+ ? "No stories, tasks, or bugs under this epic yet."
103
+ : "No tasks or bugs under this story yet."
104
+
105
+ const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
106
+ if (!expandable || (event.key !== "Enter" && event.key !== " ")) return
107
+ if ((event.target as HTMLElement).closest("button")) return
108
+ event.preventDefault()
109
+ onToggle(node.id)
110
+ }
111
+
112
+ return (
113
+ <div role="none">
114
+ <div
115
+ role="treeitem"
116
+ aria-level={level}
117
+ aria-expanded={expandable ? isExpanded : undefined}
118
+ aria-selected={selected}
119
+ tabIndex={0}
120
+ data-state={selected ? "selected" : undefined}
121
+ onKeyDown={onKeyDown}
122
+ className={cn(
123
+ "grid min-h-12 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-2 border-b px-2 py-1.5 outline-none transition-colors hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring sm:grid-cols-[minmax(0,1fr)_5rem_5rem_4rem_5.5rem_3rem]",
124
+ statusRowClass(node.status),
125
+ selected && "bg-muted/60"
126
+ )}
127
+ >
128
+ <div
129
+ className="flex min-w-0 items-center gap-1.5"
130
+ style={{ paddingInlineStart: `${Math.max(0, level - 1) * 12}px` }}
131
+ >
132
+ {expandable ? (
133
+ <Button
134
+ type="button"
135
+ variant="ghost"
136
+ size="icon"
137
+ className="size-11 shrink-0 sm:size-9"
138
+ aria-label={`${isExpanded ? "Collapse" : "Expand"} ${node.id}`}
139
+ aria-expanded={isExpanded}
140
+ onClick={() => onToggle(node.id)}
141
+ >
142
+ <ChevronRightIcon
143
+ className={cn(
144
+ "size-4 transition-transform",
145
+ isExpanded && "rotate-90"
146
+ )}
147
+ />
148
+ </Button>
149
+ ) : (
150
+ <span className="w-11 shrink-0 sm:w-9" aria-hidden />
151
+ )}
152
+ <TypeBadge type={node.kind} />
153
+ <div className="min-w-0">
154
+ <div className="flex min-w-0 flex-wrap items-baseline gap-x-2">
155
+ <IdCreatedTooltip id={node.id} created={node.created} />
156
+ <span className="min-w-0 break-words font-medium leading-snug">
157
+ {node.title}
158
+ </span>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ <div className="sm:hidden">
163
+ {node.children.length > 0 ? (
164
+ <ChildProgressBar
165
+ done={node.doneWorkItemCount}
166
+ total={node.workItemCount}
167
+ />
168
+ ) : (
169
+ <StatusWithSolvedTooltip
170
+ status={node.status}
171
+ solvedAt={node.completedAt}
172
+ />
173
+ )}
174
+ </div>
175
+ <div className="hidden sm:block">
176
+ <SizeWithPointsTooltip size={node.size} points={node.points} />
177
+ </div>
178
+ <div className="hidden justify-center sm:flex">
179
+ <AttributionAvatarGroup
180
+ reporters={node.reporters}
181
+ resolvers={node.resolvers}
182
+ />
183
+ </div>
184
+ <div className="hidden tabular-nums sm:block">
185
+ {node.children.length > 0 ? node.workItemCount : "—"}
186
+ </div>
187
+ <div className="hidden justify-center sm:flex">
188
+ {node.children.length > 0 ? (
189
+ <ChildProgressBar
190
+ done={node.doneWorkItemCount}
191
+ total={node.workItemCount}
192
+ />
193
+ ) : (
194
+ <StatusWithSolvedTooltip
195
+ status={node.status}
196
+ solvedAt={node.completedAt}
197
+ />
198
+ )}
199
+ </div>
200
+ <div className="hidden justify-end sm:flex pl-2">
201
+ <ViewWorkItemButton
202
+ label={`View details for ${node.id}`}
203
+ itemRef={{
204
+ kind:
205
+ node.kind === "epic"
206
+ ? "epic"
207
+ : node.kind === "story"
208
+ ? "story"
209
+ : "item",
210
+ id: node.id,
211
+ title: node.title,
212
+ filePath: node.filePath,
213
+ itemType:
214
+ node.kind === "task" || node.kind === "bug"
215
+ ? node.kind
216
+ : undefined,
217
+ }}
218
+ />
219
+ </div>
220
+ <div className="col-span-2 flex justify-end sm:hidden pt-0.5">
221
+ <ViewWorkItemButton
222
+ label={`View details for ${node.id}`}
223
+ itemRef={{
224
+ kind:
225
+ node.kind === "epic"
226
+ ? "epic"
227
+ : node.kind === "story"
228
+ ? "story"
229
+ : "item",
230
+ id: node.id,
231
+ title: node.title,
232
+ filePath: node.filePath,
233
+ itemType:
234
+ node.kind === "task" || node.kind === "bug"
235
+ ? node.kind
236
+ : undefined,
237
+ }}
238
+ />
239
+ </div>
240
+ </div>
241
+ {isExpanded ? (
242
+ <div role="group">
243
+ {node.children.length > 0 ? (
244
+ node.children.map((child) => (
245
+ <TreeNodeRow
246
+ key={`${child.kind}:${child.id}:${child.filePath}`}
247
+ node={child}
248
+ level={level + 1}
249
+ expanded={expanded}
250
+ selectedEpicId={selectedEpicId}
251
+ selectedStoryId={selectedStoryId}
252
+ onToggle={onToggle}
253
+ />
254
+ ))
255
+ ) : (
256
+ <p
257
+ className="border-b py-3 pr-3 text-sm text-muted-foreground"
258
+ style={{ paddingInlineStart: `${level * 12 + 44}px` }}
259
+ >
260
+ {emptyMessage}
261
+ </p>
262
+ )}
263
+ </div>
264
+ ) : null}
265
+ </div>
266
+ )
267
+ }
268
+
269
+ export function OverallTreeList({
270
+ list,
271
+ workItemsByEpic,
272
+ itemsByStory,
273
+ selectedEpicId = null,
274
+ selectedStoryId = null,
275
+ countableCompletions = [],
276
+ initialHideCompleted = false,
277
+ }: OverallTreeListProps) {
278
+ const { project, errors } = list
279
+ const tree = React.useMemo(
280
+ () => buildOverallTree(list, workItemsByEpic, itemsByStory),
281
+ [list, workItemsByEpic, itemsByStory]
282
+ )
283
+ const [query, setQuery] = React.useState("")
284
+ const [hideCompleted, setHideCompleted] = usePersistedHideCompleted(
285
+ "epics",
286
+ initialHideCompleted
287
+ )
288
+ const [timeframe, setTimeframe] = React.useState<TimeframeFilterState>(
289
+ DEFAULT_TIMEFRAME_FILTER
290
+ )
291
+ const [expandedIds, setExpandedIds] = React.useState<string[]>(() => [
292
+ ...(selectedEpicId ? [selectedEpicId] : []),
293
+ ...(selectedStoryId ? [selectedStoryId] : []),
294
+ ])
295
+
296
+ React.useEffect(() => {
297
+ let active = true
298
+ queueMicrotask(() => {
299
+ if (!active) return
300
+ setExpandedIds((current) => [
301
+ ...new Set([
302
+ ...current,
303
+ ...readExpanded(project.id),
304
+ ...(selectedEpicId ? [selectedEpicId] : []),
305
+ ...(selectedStoryId ? [selectedStoryId] : []),
306
+ ]),
307
+ ])
308
+ })
309
+ return () => {
310
+ active = false
311
+ }
312
+ }, [project.id, selectedEpicId, selectedStoryId])
313
+
314
+ const filtered = React.useMemo(
315
+ () => filterOverallTree(tree, { query, hideCompleted, timeframe }),
316
+ [tree, query, hideCompleted, timeframe]
317
+ )
318
+ const effectiveExpanded = React.useMemo(
319
+ () => new Set([...expandedIds, ...searchRevealIds(filtered, query)]),
320
+ [expandedIds, filtered, query]
321
+ )
322
+ const completedAts = React.useMemo(
323
+ () => allNodes(tree).map((node) => node.completedAt),
324
+ [tree]
325
+ )
326
+ const resetKey = `${project.id}|${query}|${hideCompleted}|${JSON.stringify(timeframe)}`
327
+ const {
328
+ pageRows,
329
+ page,
330
+ pageSize,
331
+ totalCount,
332
+ totalPages,
333
+ setPage,
334
+ setPageSize,
335
+ } = usePaginatedRows(filtered, resetKey)
336
+
337
+ React.useEffect(() => {
338
+ if (!selectedEpicId) return
339
+ const index = filtered.findIndex((node) => node.id === selectedEpicId)
340
+ if (index >= 0) setPage(Math.floor(index / pageSize) + 1)
341
+ }, [selectedEpicId, filtered, pageSize, setPage])
342
+
343
+ const toggle = React.useCallback(
344
+ (id: string) => {
345
+ setExpandedIds((current) => {
346
+ const next = new Set(current)
347
+ if (next.has(id)) next.delete(id)
348
+ else next.add(id)
349
+ const values = [...next]
350
+ try {
351
+ window.sessionStorage.setItem(
352
+ expansionStorageKey(project.id),
353
+ JSON.stringify(values)
354
+ )
355
+ } catch {
356
+ // Session storage may be unavailable in privacy-restricted contexts.
357
+ }
358
+ return values
359
+ })
360
+ },
361
+ [project.id]
362
+ )
363
+
364
+ const nestedErrors = [
365
+ ...Object.values(workItemsByEpic).flatMap((value) => value.errors),
366
+ ...Object.values(itemsByStory).flatMap((value) => value.errors),
367
+ ]
368
+ const allErrors = [
369
+ ...errors.map((error) => ({
370
+ filePath: error.filePath,
371
+ message: error.message,
372
+ })),
373
+ ...nestedErrors,
374
+ ]
375
+
376
+ return (
377
+ <Card>
378
+ <CardHeader>
379
+ <CardTitle className="font-head text-xl">{project.name}</CardTitle>
380
+ {tree.length > 0 ? (
381
+ <CardAction>
382
+ <TimeframeFilter
383
+ id={`overall-tree-timeframe-${project.id}`}
384
+ value={timeframe}
385
+ onChange={setTimeframe}
386
+ completedAts={completedAts}
387
+ countableCompletions={countableCompletions}
388
+ />
389
+ </CardAction>
390
+ ) : null}
391
+ </CardHeader>
392
+ <CardContent className="flex flex-col gap-4">
393
+ {allErrors.length > 0 ? (
394
+ <div
395
+ role="status"
396
+ className="rounded border-2 border-destructive/40 bg-destructive/10 px-3 py-2 text-sm"
397
+ >
398
+ <p className="font-medium text-destructive">
399
+ {allErrors.length} file{allErrors.length === 1 ? "" : "s"} could
400
+ not be parsed
401
+ </p>
402
+ <ul className="mt-2 flex flex-col gap-1 font-mono text-xs text-muted-foreground">
403
+ {allErrors.map((error, index) => (
404
+ <li key={`${error.filePath}:${error.message}:${index}`}>
405
+ {displayFileName(error.filePath)}: {error.message}
406
+ </li>
407
+ ))}
408
+ </ul>
409
+ </div>
410
+ ) : null}
411
+
412
+ {tree.length === 0 ? (
413
+ <p className="text-sm text-muted-foreground">
414
+ No epics in this board yet.
415
+ </p>
416
+ ) : (
417
+ <>
418
+ <ListFiltersBar
419
+ searchId={`overall-tree-search-${project.id}`}
420
+ query={query}
421
+ onQueryChange={setQuery}
422
+ hideCompleted={hideCompleted}
423
+ onHideCompletedChange={setHideCompleted}
424
+ />
425
+ {filtered.length === 0 ? (
426
+ <p className="text-sm text-muted-foreground">
427
+ No work matches the current search or filters.
428
+ </p>
429
+ ) : (
430
+ <>
431
+ <div className="overflow-hidden rounded-md border" role="tree">
432
+ <div className="hidden grid-cols-[minmax(0,1fr)_5rem_5rem_4rem_5.5rem_3rem] gap-x-3 border-b bg-muted/30 px-2 py-2 text-xs font-medium text-muted-foreground sm:grid">
433
+ <span>Work</span>
434
+ <span>Size</span>
435
+ <span className="text-center">People</span>
436
+ <span>Items</span>
437
+ <span className="text-center">Status</span>
438
+ <span className="sr-only pl-2">Actions</span>
439
+ </div>
440
+ {pageRows.map((node) => (
441
+ <TreeNodeRow
442
+ key={`${node.id}:${node.filePath}`}
443
+ node={node}
444
+ level={1}
445
+ expanded={effectiveExpanded}
446
+ selectedEpicId={selectedEpicId}
447
+ selectedStoryId={selectedStoryId}
448
+ onToggle={toggle}
449
+ />
450
+ ))}
451
+ </div>
452
+ <ListPagination
453
+ page={page}
454
+ totalPages={totalPages}
455
+ totalCount={totalCount}
456
+ pageSize={pageSize}
457
+ onPageChange={setPage}
458
+ onPageSizeChange={setPageSize}
459
+ />
460
+ </>
461
+ )}
462
+ </>
463
+ )}
464
+ </CardContent>
465
+ </Card>
466
+ )
467
+ }
@@ -28,7 +28,10 @@ import {
28
28
  import { ListFiltersBar } from "@/components/board/list-filters-bar"
29
29
  import { ListPagination } from "@/components/board/list-pagination"
30
30
  import { SortableTableHead } from "@/components/board/sortable-table-head"
31
- import { TypeBadge } from "@/components/board/status-badge"
31
+ import {
32
+ statusRowClass,
33
+ TypeBadge,
34
+ } from "@/components/board/status-badge"
32
35
  import { TimeframeFilter } from "@/components/board/timeframe-filter"
33
36
  import { ViewWorkItemButton } from "@/components/board/work-item-sheet"
34
37
  import { AttributionAvatarGroup } from "@/components/board/attribution-avatars"
@@ -251,6 +254,7 @@ export function OverallWorkItemsList({
251
254
  key={`${project.id}:${row.kind}:${row.id}:${row.filePath}`}
252
255
  data-state={selected ? "selected" : undefined}
253
256
  className={cn(
257
+ statusRowClass(row.status),
254
258
  selected && "bg-muted/60",
255
259
  "hover:bg-muted/40"
256
260
  )}
@@ -0,0 +1,183 @@
1
+ "use client"
2
+
3
+ import { format } from "date-fns"
4
+
5
+ import {
6
+ buildPointsCalendar,
7
+ pointsHeatLevel,
8
+ type PointsCalendarDay,
9
+ type PointsCalendarWeek,
10
+ } from "@/lib/taskmark/points-calendar"
11
+ import type { SolvedCompletionSample } from "@/lib/taskmark/timeframe-filters"
12
+ import { cn } from "@/lib/utils"
13
+
14
+ /** White through #C4A1FF: darker means more story points completed. */
15
+ const LEVEL_CLASS: Record<0 | 1 | 2 | 3 | 4, string> = {
16
+ 0: "bg-white dark:bg-neutral-900",
17
+ 1: "bg-[#f0e8ff] dark:bg-[#2f2542]",
18
+ 2: "bg-[#e2d0ff] dark:bg-[#4c3a73]",
19
+ 3: "bg-[#d3b9ff] dark:bg-[#8368c0]",
20
+ 4: "bg-[#c4a1ff]",
21
+ }
22
+
23
+ const WEEKDAY_LABELS = ["Mon", "", "Wed", "", "Fri", "", ""]
24
+ const CELL_SIZE = "0.75rem"
25
+ const CELL_GAP = "3px"
26
+
27
+ function pointsLabel(points: number): string {
28
+ return points === 1 ? "1 story point" : `${points} story points`
29
+ }
30
+
31
+ function dayLabel(day: PointsCalendarDay): string {
32
+ return `${format(day.date, "MMM d, yyyy")} · ${pointsLabel(day.points)}`
33
+ }
34
+
35
+ function monthLabels(weeks: readonly PointsCalendarWeek[]): string[] {
36
+ let previous: number | null = null
37
+ return weeks.map((week, index) => {
38
+ const first = week.days[0]!.date
39
+ const month = first.getMonth()
40
+ const changed = previous !== null && month !== previous
41
+ previous = month
42
+ // Skip the last column so a label never runs past the grid.
43
+ if (!changed || index >= weeks.length - 1) return ""
44
+ return format(first, "MMM")
45
+ })
46
+ }
47
+
48
+ function HeatmapCell({ day, maxPoints }: { day: PointsCalendarDay; maxPoints: number }) {
49
+ if (day.isFuture) return <span aria-hidden className="size-3" />
50
+
51
+ const level = pointsHeatLevel(day.points, maxPoints)
52
+ const className = cn(
53
+ "size-3 rounded-[2px] border border-black/20 dark:border-white/15",
54
+ LEVEL_CLASS[level]
55
+ )
56
+ const label = dayLabel(day)
57
+
58
+ if (day.points <= 0) {
59
+ return <span aria-hidden title={label} className={className} />
60
+ }
61
+
62
+ return (
63
+ <button
64
+ type="button"
65
+ title={label}
66
+ aria-label={label}
67
+ className={cn(
68
+ className,
69
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
70
+ )}
71
+ />
72
+ )
73
+ }
74
+
75
+ type PointsHeatmapProps = {
76
+ samples: readonly SolvedCompletionSample[]
77
+ now?: Date
78
+ }
79
+
80
+ export function PointsHeatmap({ samples, now }: PointsHeatmapProps) {
81
+ const calendar = buildPointsCalendar(samples, now)
82
+ const { weeks, maxPoints, totalPoints, start, end } = calendar
83
+ const labels = monthLabels(weeks)
84
+ const summary = `Story points completed per day between ${format(start, "MMM d, yyyy")} and ${format(end, "MMM d, yyyy")}: ${pointsLabel(totalPoints)} in total.`
85
+
86
+ return (
87
+ <section
88
+ className="border-2 border-black bg-card p-4 shadow-[4px_4px_0_0_var(--shadow-color)]"
89
+ aria-label="Completed story points calendar"
90
+ >
91
+ <div className="mb-3 flex flex-col gap-1 sm:flex-row sm:items-end sm:justify-between">
92
+ <div>
93
+ <h2 className="font-head text-base tracking-tight">Story points</h2>
94
+ <p className="text-xs text-muted-foreground">
95
+ Done tasks and bugs over the last year
96
+ </p>
97
+ </div>
98
+ <p className="text-xs text-muted-foreground">
99
+ {maxPoints > 0
100
+ ? `${pointsLabel(totalPoints)} completed`
101
+ : "No completed story points in this window yet."}
102
+ </p>
103
+ </div>
104
+
105
+ <p className="sr-only">{summary}</p>
106
+
107
+ <div className="overflow-x-auto pb-1">
108
+ <div className="mx-auto flex w-max gap-1.5">
109
+ <div
110
+ className="grid shrink-0"
111
+ style={{
112
+ gridTemplateRows: `repeat(7, ${CELL_SIZE})`,
113
+ rowGap: CELL_GAP,
114
+ // Clear the month-label row so weekday labels line up with their cells.
115
+ paddingTop: `calc(${CELL_SIZE} + ${CELL_GAP})`,
116
+ }}
117
+ aria-hidden
118
+ >
119
+ {WEEKDAY_LABELS.map((label, index) => (
120
+ <span
121
+ key={`weekday-${index}`}
122
+ className="flex items-center pr-1 text-[9px] leading-none text-muted-foreground"
123
+ >
124
+ {label}
125
+ </span>
126
+ ))}
127
+ </div>
128
+
129
+ <div className="flex flex-col" style={{ rowGap: CELL_GAP }}>
130
+ <div
131
+ className="grid"
132
+ style={{
133
+ gridTemplateColumns: `repeat(${weeks.length}, ${CELL_SIZE})`,
134
+ columnGap: CELL_GAP,
135
+ }}
136
+ aria-hidden
137
+ >
138
+ {labels.map((label, index) => (
139
+ <span
140
+ key={`month-${weeks[index]?.key ?? index}`}
141
+ className="h-3 whitespace-nowrap text-[9px] leading-3 text-muted-foreground"
142
+ >
143
+ {label}
144
+ </span>
145
+ ))}
146
+ </div>
147
+
148
+ <div
149
+ className="grid grid-flow-col"
150
+ style={{
151
+ gridTemplateRows: `repeat(7, ${CELL_SIZE})`,
152
+ gridAutoColumns: CELL_SIZE,
153
+ gap: CELL_GAP,
154
+ }}
155
+ >
156
+ {weeks.flatMap((week) =>
157
+ week.days.map((day) => (
158
+ <HeatmapCell key={day.key} day={day} maxPoints={maxPoints} />
159
+ ))
160
+ )}
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <div className="mt-3 flex items-center justify-center gap-2 text-[10px] text-muted-foreground">
167
+ <span>Less</span>
168
+ <div className="flex items-center gap-1" aria-hidden>
169
+ {([0, 1, 2, 3, 4] as const).map((level) => (
170
+ <span
171
+ key={level}
172
+ className={cn(
173
+ "size-3 rounded-[2px] border border-black/20 dark:border-white/15",
174
+ LEVEL_CLASS[level]
175
+ )}
176
+ />
177
+ ))}
178
+ </div>
179
+ <span>More</span>
180
+ </div>
181
+ </section>
182
+ )
183
+ }