@hiroleague/taskmanager 0.0.1 → 0.0.3

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 (196) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -52
  3. package/dist/assets/architecture-YZFGNWBL-DoE0KxgG.js +1 -0
  4. package/dist/assets/architectureDiagram-Q4EWVU46-DeuBhy7X.js +36 -0
  5. package/dist/assets/{blockDiagram-DXYQGD6D-DfOGNphI.js → blockDiagram-DXYQGD6D-BDBy9ns9.js} +1 -1
  6. package/dist/assets/{c4Diagram-AHTNJAMY-B2Yfcwbo.js → c4Diagram-AHTNJAMY-CpqJj_8a.js} +1 -1
  7. package/dist/assets/channel-PHRyjspt.js +1 -0
  8. package/dist/assets/{chunk-2KRD3SAO-9yt00aGC.js → chunk-2KRD3SAO-DEpUsxdZ.js} +1 -1
  9. package/dist/assets/chunk-336JU56O-BGQvSwLk.js +2 -0
  10. package/dist/assets/chunk-426QAEUC-Cl9nQN9c.js +1 -0
  11. package/dist/assets/{chunk-4TB4RGXK-DF8yJBFl.js → chunk-4TB4RGXK-Dq7aiIrZ.js} +2 -2
  12. package/dist/assets/{chunk-5FUZZQ4R-XEga0hMC.js → chunk-5FUZZQ4R-B_HuuUjf.js} +1 -1
  13. package/dist/assets/{chunk-5PVQY5BW-BrmXs2Gs.js → chunk-5PVQY5BW-cGfZCZGU.js} +2 -2
  14. package/dist/assets/{chunk-67CJDMHE-5wFKo04G.js → chunk-67CJDMHE-BMYAVZfw.js} +1 -1
  15. package/dist/assets/{chunk-7N4EOEYR-BRRGX_NC.js → chunk-7N4EOEYR-Ct-EY7Nc.js} +1 -1
  16. package/dist/assets/{chunk-AA7GKIK3-DUZv_pNI.js → chunk-AA7GKIK3-Bd4HFpeo.js} +1 -1
  17. package/dist/assets/{chunk-CIAEETIT-mA5aM_d7.js → chunk-CIAEETIT-CrFUkPMT.js} +1 -1
  18. package/dist/assets/{chunk-EDXVE4YY-DxUqDyxy.js → chunk-EDXVE4YY-DMDyt0NF.js} +1 -1
  19. package/dist/assets/{chunk-ENJZ2VHE-BgZKYo1l.js → chunk-ENJZ2VHE-DrWzOrpd.js} +1 -1
  20. package/dist/assets/{chunk-FOC6F5B3-B-cqGCPC.js → chunk-FOC6F5B3-Bemzq96j.js} +1 -1
  21. package/dist/assets/{chunk-ICPOFSXX-BNR1V8rT.js → chunk-ICPOFSXX-DkUVjrLw.js} +5 -5
  22. package/dist/assets/{chunk-K5T4RW27-BLRDzioh.js → chunk-K5T4RW27-ALKIf000.js} +5 -5
  23. package/dist/assets/{chunk-KGLVRYIC-CTkQSeKy.js → chunk-KGLVRYIC-Bg6HNTZ-.js} +1 -1
  24. package/dist/assets/{chunk-LIHQZDEY-Cf34Nu3J.js → chunk-LIHQZDEY-DeyGongE.js} +1 -1
  25. package/dist/assets/{chunk-ORNJ4GCN-D3uXgbay.js → chunk-ORNJ4GCN-Bx83s1bJ.js} +1 -1
  26. package/dist/assets/{chunk-OYMX7WX6-syQho5jf.js → chunk-OYMX7WX6-BqRUtRpL.js} +1 -1
  27. package/dist/assets/{chunk-U2HBQHQK-DTJPeU7W.js → chunk-U2HBQHQK-DogcerR6.js} +1 -1
  28. package/dist/assets/{chunk-X2U36JSP-CrTnmMqG.js → chunk-X2U36JSP-CwVWdmZV.js} +1 -1
  29. package/dist/assets/chunk-XPW4576I-DQpNCogT.js +32 -0
  30. package/dist/assets/{chunk-YZCP3GAM-9wq0QKUn.js → chunk-YZCP3GAM-crQSbji9.js} +1 -1
  31. package/dist/assets/{chunk-ZZ45TVLE-D3I1kLlo.js → chunk-ZZ45TVLE-Bk1S1YtS.js} +1 -1
  32. package/dist/assets/classDiagram-6PBFFD2Q-B_TabGaU.js +1 -0
  33. package/dist/assets/classDiagram-v2-HSJHXN6E-CGnZkUWw.js +1 -0
  34. package/dist/assets/clone-D4ka472w.js +1 -0
  35. package/dist/assets/{cose-bilkent-S5V4N54A-BygGvZGW.js → cose-bilkent-S5V4N54A-RBTHUit8.js} +1 -1
  36. package/dist/assets/cytoscape.esm-BGJwlmkf.js +321 -0
  37. package/dist/assets/dagre-B32eYLtm.js +1 -0
  38. package/dist/assets/{dagre-KV5264BT-BBqulDtd.js → dagre-KV5264BT-nX7tuXXn.js} +1 -1
  39. package/dist/assets/diagram-5BDNPKRD-DRxMXlQr.js +10 -0
  40. package/dist/assets/diagram-G4DWMVQ6-CoojevGm.js +24 -0
  41. package/dist/assets/diagram-MMDJMWI5-CWtJyfVW.js +43 -0
  42. package/dist/assets/diagram-TYMM5635-CsDJC4Hq.js +24 -0
  43. package/dist/assets/{erDiagram-SMLLAGMA-BN5eJerP.js → erDiagram-SMLLAGMA-Cf7Xtd9A.js} +2 -2
  44. package/dist/assets/{flatten-C5NL-f24.js → flatten-CYX_pHZ7.js} +1 -1
  45. package/dist/assets/{flowDiagram-DWJPFMVM-CbFskc8S.js → flowDiagram-DWJPFMVM-DQaeR16a.js} +3 -3
  46. package/dist/assets/{ganttDiagram-T4ZO3ILL-OCTvbRxF.js → ganttDiagram-T4ZO3ILL-8EIcztcH.js} +1 -1
  47. package/dist/assets/gitGraph-7Q5UKJZL-BH9A1SAZ.js +1 -0
  48. package/dist/assets/{gitGraphDiagram-UUTBAWPF-wpqI2kyI.js → gitGraphDiagram-UUTBAWPF-DO9ODqYw.js} +1 -1
  49. package/dist/assets/graphlib-bPBqlJKT.js +1 -0
  50. package/dist/assets/identity-Me9aart9.js +1 -0
  51. package/dist/assets/index-oKG1C41_.js +273 -0
  52. package/dist/assets/info-OMHHGYJF-BvKR-zWh.js +1 -0
  53. package/dist/assets/infoDiagram-42DDH7IO-pRTXCm5C.js +2 -0
  54. package/dist/assets/isEmpty-Cu0k-j1j.js +1 -0
  55. package/dist/assets/{ishikawaDiagram-UXIWVN3A-Epc23N_0.js → ishikawaDiagram-UXIWVN3A-BP2YE5QI.js} +2 -2
  56. package/dist/assets/{journeyDiagram-VCZTEJTY-BkMxoaPq.js → journeyDiagram-VCZTEJTY-B3l2juoL.js} +1 -1
  57. package/dist/assets/{kanban-definition-6JOO6SKY-C8dW_26n.js → kanban-definition-6JOO6SKY-BpIpEOZZ.js} +4 -4
  58. package/dist/assets/{line-DNzQATGr.js → line-otOkzGl8.js} +1 -1
  59. package/dist/assets/mermaid-parser.core-xWsW24Gq.js +4 -0
  60. package/dist/assets/{mindmap-definition-QFDTVHPH-CvpNtrKT.js → mindmap-definition-QFDTVHPH-B9khyC7X.js} +3 -3
  61. package/dist/assets/packet-4T2RLAQJ-D8Dw3nmf.js +1 -0
  62. package/dist/assets/pie-ZZUOXDRM-ZghowlAE.js +1 -0
  63. package/dist/assets/{pieDiagram-DEJITSTG-eENymoXZ.js → pieDiagram-DEJITSTG-v32hL3i7.js} +1 -1
  64. package/dist/assets/{quadrantDiagram-34T5L4WZ-c0iZxo2I.js → quadrantDiagram-34T5L4WZ-DIL3GDFt.js} +1 -1
  65. package/dist/assets/radar-PYXPWWZC-D-PK3JOd.js +1 -0
  66. package/dist/assets/reduce-CImcgAcU.js +1 -0
  67. package/dist/assets/{requirementDiagram-MS252O5E-CmRO3hLp.js → requirementDiagram-MS252O5E-D8os2-4y.js} +2 -2
  68. package/dist/assets/{sankeyDiagram-XADWPNL6-woJZoQ58.js → sankeyDiagram-XADWPNL6-BV70D4l5.js} +1 -1
  69. package/dist/assets/{sequenceDiagram-FGHM5R23-B7qNcwNo.js → sequenceDiagram-FGHM5R23-Cwu8hQW1.js} +1 -1
  70. package/dist/assets/stateDiagram-FHFEXIEX-oYUWv7Fb.js +1 -0
  71. package/dist/assets/stateDiagram-v2-QKLJ7IA2-CFUTpFu-.js +1 -0
  72. package/dist/assets/{timeline-definition-GMOUNBTQ-CQWqDPGG.js → timeline-definition-GMOUNBTQ-CxSdKxpL.js} +1 -1
  73. package/dist/assets/treeView-SZITEDCU-uVgaJQzG.js +1 -0
  74. package/dist/assets/treemap-W4RFUUIX-Dcad_9AN.js +1 -0
  75. package/dist/assets/vennDiagram-DHZGUBPP-D4wgD7QI.js +34 -0
  76. package/dist/assets/wardley-RL74JXVD-CFXrK8mx.js +1 -0
  77. package/dist/assets/{wardleyDiagram-NUSXRM2D-DNhPIFCg.js → wardleyDiagram-NUSXRM2D-5Q201ea3.js} +1 -1
  78. package/dist/assets/{xychartDiagram-5P7HB3ND-BDblAZ11.js → xychartDiagram-5P7HB3ND-BPZv_axd.js} +3 -3
  79. package/dist/index.html +16 -12
  80. package/package.json +99 -92
  81. package/scripts/stubs/node-domexception/index.cjs +18 -0
  82. package/scripts/stubs/node-domexception/package.json +7 -0
  83. package/skills/hiro-task-manager-cli/SKILL.md +97 -0
  84. package/skills/hiro-task-manager-cli/reference/boards.md +143 -0
  85. package/skills/hiro-task-manager-cli/reference/cli-access-policy.md +72 -0
  86. package/skills/hiro-task-manager-cli/reference/errors.md +85 -0
  87. package/skills/hiro-task-manager-cli/reference/lists.md +106 -0
  88. package/skills/hiro-task-manager-cli/reference/releases.md +87 -0
  89. package/skills/hiro-task-manager-cli/reference/search.md +38 -0
  90. package/skills/hiro-task-manager-cli/reference/statuses.md +25 -0
  91. package/skills/hiro-task-manager-cli/reference/tasks.md +144 -0
  92. package/skills/hiro-task-manager-cli/reference/trash.md +50 -0
  93. package/src/cli/bootstrap/launcher.test.ts +66 -0
  94. package/src/cli/bootstrap/launcher.ts +389 -35
  95. package/src/cli/bootstrap/program.test.ts +46 -0
  96. package/src/cli/bootstrap/program.ts +54 -1
  97. package/src/cli/bootstrap/runtime.test.ts +15 -0
  98. package/src/cli/bootstrap/runtime.ts +27 -1
  99. package/src/cli/commands/query.ts +56 -56
  100. package/src/cli/commands/server.ts +27 -19
  101. package/src/cli/handlers/boards.test.ts +669 -669
  102. package/src/cli/handlers/cli-wiring.test.ts +1 -1
  103. package/src/cli/handlers/search.test.ts +374 -374
  104. package/src/cli/handlers/search.ts +17 -17
  105. package/src/cli/handlers/server.test.ts +55 -13
  106. package/src/cli/handlers/server.ts +16 -3
  107. package/src/cli/lib/api-client.test.ts +35 -2
  108. package/src/cli/lib/api-client.ts +43 -10
  109. package/src/cli/lib/cli-http-errors.test.ts +85 -85
  110. package/src/cli/lib/command-helpers.ts +161 -154
  111. package/src/cli/lib/config.ts +4 -0
  112. package/src/cli/lib/launcherUi.test.ts +74 -0
  113. package/src/cli/lib/launcherUi.ts +213 -0
  114. package/src/cli/lib/process.test.ts +24 -5
  115. package/src/cli/lib/process.ts +86 -55
  116. package/src/cli/ports/process.ts +8 -2
  117. package/src/cli/subprocess.real-stack.test.ts +611 -598
  118. package/src/cli/subprocess.smoke.test.ts +954 -969
  119. package/src/cli/types/config.ts +2 -6
  120. package/src/client/components/auth/AuthScreen.tsx +3 -3
  121. package/src/client/components/board/BoardStatsChips.tsx +233 -233
  122. package/src/client/components/board/BoardStatsContext.tsx +41 -41
  123. package/src/client/components/board/boardHeaderButtonStyles.ts +38 -38
  124. package/src/client/components/board/shortcuts/useBoardShortcutKeydown.ts +49 -49
  125. package/src/client/components/board/useBoardCanvasPanScroll.ts +108 -108
  126. package/src/client/components/board/useBoardTaskContainerDroppableReact.ts +33 -33
  127. package/src/client/components/board/useBoardTaskSortableReact.ts +26 -26
  128. package/src/client/components/multi-select.tsx +1206 -1206
  129. package/src/client/components/routing/BoardPage.tsx +20 -20
  130. package/src/client/components/routing/NavigationRegistrar.tsx +13 -13
  131. package/src/client/components/settings/SettingsPage.tsx +1 -1
  132. package/src/client/components/task/TaskCard.tsx +643 -643
  133. package/src/client/components/ui/badge.tsx +49 -49
  134. package/src/client/components/ui/button.tsx +65 -65
  135. package/src/client/components/ui/command.tsx +193 -193
  136. package/src/client/components/ui/dialog.tsx +163 -163
  137. package/src/client/components/ui/input-group.tsx +155 -155
  138. package/src/client/components/ui/input.tsx +19 -19
  139. package/src/client/components/ui/popover.tsx +87 -87
  140. package/src/client/components/ui/separator.tsx +28 -28
  141. package/src/client/components/ui/textarea.tsx +18 -18
  142. package/src/client/index.css +248 -248
  143. package/src/client/lib/appNavigate.ts +16 -16
  144. package/src/client/lib/taskCardDate.ts +111 -111
  145. package/src/client/lib/utils.ts +6 -6
  146. package/src/server/auth.ts +351 -302
  147. package/src/server/bootstrapDev.ts +11 -2
  148. package/src/server/bootstrapInstalled.ts +6 -1
  149. package/src/server/index.ts +33 -7
  150. package/src/server/migrations/013_cli_policy_and_provenance.ts +2 -2
  151. package/src/server/migrations/019_cli_global_create_board_default_on.ts +14 -0
  152. package/src/server/migrations/registry.ts +43 -41
  153. package/src/server/parseBootstrapProfile.ts +42 -0
  154. package/src/server/storage/cliPolicy.ts +2 -1
  155. package/src/shared/runtimeConfig.ts +256 -237
  156. package/src/shared/runtimeIdentity.test.ts +47 -0
  157. package/src/shared/runtimeIdentity.ts +35 -0
  158. package/src/shared/serverStatus.ts +21 -0
  159. package/src/shared/skillsInstall.ts +70 -0
  160. package/src/shared/terminalColors.ts +24 -0
  161. package/dist/assets/architecture-YZFGNWBL-3h1eIYfB.js +0 -1
  162. package/dist/assets/architectureDiagram-Q4EWVU46-DSQ1_74_.js +0 -36
  163. package/dist/assets/channel-yBmN_ln0.js +0 -1
  164. package/dist/assets/classDiagram-6PBFFD2Q-CotFZI8-.js +0 -1
  165. package/dist/assets/classDiagram-v2-HSJHXN6E-DAPzeDGn.js +0 -1
  166. package/dist/assets/clone-BRQpYu_n.js +0 -1
  167. package/dist/assets/cytoscape.esm-BIYWHPG0.js +0 -321
  168. package/dist/assets/dagre-rhyPjnsQ.js +0 -1
  169. package/dist/assets/diagram-5BDNPKRD-Ky3EXXj0.js +0 -10
  170. package/dist/assets/diagram-G4DWMVQ6-t7LbT0Uz.js +0 -24
  171. package/dist/assets/diagram-MMDJMWI5-CdnLXEMx.js +0 -43
  172. package/dist/assets/diagram-TYMM5635-CnzTqJBM.js +0 -24
  173. package/dist/assets/gitGraph-7Q5UKJZL-CG8f8JF7.js +0 -1
  174. package/dist/assets/graphlib-COiJG5Qv.js +0 -1
  175. package/dist/assets/identity-D4WOnl_h.js +0 -1
  176. package/dist/assets/index-lyyIVcc_.js +0 -304
  177. package/dist/assets/info-OMHHGYJF-C8_SHoRO.js +0 -1
  178. package/dist/assets/infoDiagram-42DDH7IO-BbvTdpSV.js +0 -2
  179. package/dist/assets/mermaid-parser.core-6Tn8epr_.js +0 -4
  180. package/dist/assets/packet-4T2RLAQJ-BvpAX0kJ.js +0 -1
  181. package/dist/assets/pie-ZZUOXDRM-Ow26Yf-E.js +0 -1
  182. package/dist/assets/radar-PYXPWWZC-e_ron5jQ.js +0 -1
  183. package/dist/assets/reduce-BDOBPIXr.js +0 -1
  184. package/dist/assets/stateDiagram-FHFEXIEX-CYfGMoR8.js +0 -1
  185. package/dist/assets/stateDiagram-v2-QKLJ7IA2-CO1W_n55.js +0 -1
  186. package/dist/assets/treeView-SZITEDCU-DsEr3xeq.js +0 -1
  187. package/dist/assets/treemap-W4RFUUIX-DV7nk2AB.js +0 -1
  188. package/dist/assets/vennDiagram-DHZGUBPP-BjTbuhcb.js +0 -34
  189. package/dist/assets/wardley-RL74JXVD-CrrFU9AE.js +0 -1
  190. /package/dist/assets/{chunk-4BX2VUAB-ean5NKtU.js → chunk-4BX2VUAB-C70mcfQR.js} +0 -0
  191. /package/dist/assets/{chunk-55IACEB6-CvSRyJqy.js → chunk-55IACEB6-CWfnqcLM.js} +0 -0
  192. /package/dist/assets/{chunk-BSJP7CBP-D8kBlJsf.js → chunk-BSJP7CBP-B0LrXV9y.js} +0 -0
  193. /package/dist/assets/{chunk-FMBD7UC4-DrNhFt1N.js → chunk-FMBD7UC4-_mV71Mwu.js} +0 -0
  194. /package/dist/assets/{chunk-QZHKN3VN-Csp3OYJY.js → chunk-QZHKN3VN-t2nrsegL.js} +0 -0
  195. /package/dist/assets/{katex-8mXVa4k3.js → katex-B2dtGfSp.js} +0 -0
  196. /package/dist/assets/{rough.esm-DtEqI08j.js → rough.esm-DEh6Frf9.js} +0 -0
@@ -1,11 +1,7 @@
1
1
  import type { RuntimeConfigOverrides } from "../../shared/runtimeConfig";
2
+ import type { ServerStatus as SharedServerStatus } from "../../shared/serverStatus";
2
3
 
3
4
  export type ConfigOverrides = RuntimeConfigOverrides;
4
5
 
5
6
  /** Snapshot from `readServerStatus` / server lifecycle helpers. */
6
- export interface ServerStatus {
7
- pid?: number;
8
- port?: number;
9
- running: boolean;
10
- url?: string;
11
- }
7
+ export type ServerStatus = SharedServerStatus;
@@ -97,7 +97,7 @@ function SetupForm({
97
97
  {
98
98
  onSuccess: () => {
99
99
  onNoticeChange(
100
- "Recovery key printed to the server console. Save it now, then log in with your new passphrase.",
100
+ "Recovery key shown in the terminal running TaskManager. Save it now, then log in with your new passphrase.",
101
101
  );
102
102
  },
103
103
  },
@@ -132,8 +132,8 @@ function SetupForm({
132
132
  <p className="text-sm text-destructive">{setupAuth.error.message}</p>
133
133
  ) : null}
134
134
  <p className="text-sm text-muted-foreground">
135
- The recovery key will be shown once in the server console. Save it outside
136
- the app before continuing.
135
+ The recovery key will be shown once in the terminal running TaskManager.
136
+ Save it outside the app before continuing.
137
137
  </p>
138
138
  <button
139
139
  type="submit"
@@ -1,233 +1,233 @@
1
- import NumberFlow, {
2
- continuous,
3
- useCanAnimate,
4
- } from "@number-flow/react";
5
- import { useLayoutEffect, useRef, useState } from "react";
6
- import type { TaskCountStat } from "../../../shared/boardStats";
7
- import { cn } from "@/lib/utils";
8
-
9
- /** Low-saturation chip fills so T/O/C read as soft status tints, not full banners. */
10
- const chipBoardT =
11
- "border-border/60 bg-muted/55 text-foreground dark:border-border/50 dark:bg-muted/45";
12
- /** O = non-closed tasks — orange tint with readable opacity (avoid washed-out / red-looking tints). */
13
- const chipBoardO =
14
- "border-orange-500/45 bg-orange-500/68 text-orange-950 dark:border-orange-400/50 dark:text-orange-50";
15
- const chipBoardC =
16
- "border-emerald-600/35 bg-emerald-600/68 text-emerald-950 dark:border-emerald-500/40 dark:text-emerald-100";
17
-
18
- const chipBoardL =
19
- "border-border/60 bg-muted/55 text-foreground dark:border-border/50 dark:bg-muted/45";
20
-
21
- const chipListT =
22
- "border-border/50 bg-muted/35 text-foreground dark:bg-muted/30";
23
- const chipListO =
24
- "border-orange-500/38 bg-orange-500/60 text-orange-950 dark:border-orange-400/42 dark:text-orange-50";
25
- const chipListC =
26
- "border-emerald-600/18 bg-emerald-600/50 text-emerald-950 dark:border-emerald-500/18 dark:text-emerald-100";
27
-
28
- const STATS_FLOW_TIMING = {
29
- spinTiming: {
30
- duration: 450,
31
- easing: "cubic-bezier(0.22, 1, 0.36, 1)",
32
- } as const,
33
- transformTiming: {
34
- duration: 400,
35
- easing: "cubic-bezier(0.22, 1, 0.36, 1)",
36
- } as const,
37
- opacityTiming: {
38
- duration: 220,
39
- easing: "ease-out",
40
- } as const,
41
- };
42
-
43
- function StatChip({
44
- label,
45
- value,
46
- className,
47
- showSpinner,
48
- entryToken,
49
- valueTitle,
50
- }: {
51
- label: "T" | "O" | "C" | "L";
52
- value: number;
53
- className?: string;
54
- showSpinner: boolean;
55
- entryToken: number;
56
- /** Exposed to assistive tech — full word, not shown as chip text. */
57
- valueTitle: string;
58
- }) {
59
- const canAnimate = useCanAnimate();
60
- const [flowValue, setFlowValue] = useState(() => (showSpinner ? 0 : value));
61
- const prevShowSpinner = useRef(showSpinner);
62
- const prevEntryToken = useRef(entryToken);
63
-
64
- // After the loading spinner hides, run 0 → value once so NumberFlow performs an entry count.
65
- // useLayoutEffect avoids one painted frame at the old count before resetting to 0.
66
- // When the stats are merely revealed from hidden state, `entryToken` provides the same one-shot
67
- // 0 → value path even if TanStack Query already has cached numbers and skips the spinner.
68
- // When the spinner is off and `value` changes (filters, refetch without spinner), sync directly.
69
- useLayoutEffect(() => {
70
- if (showSpinner) {
71
- prevShowSpinner.current = true;
72
- return;
73
- }
74
- if (prevShowSpinner.current) {
75
- prevShowSpinner.current = false;
76
- setFlowValue(0);
77
- let raf1 = 0;
78
- let raf2 = 0;
79
- raf1 = requestAnimationFrame(() => {
80
- raf2 = requestAnimationFrame(() => setFlowValue(value));
81
- });
82
- return () => {
83
- cancelAnimationFrame(raf1);
84
- cancelAnimationFrame(raf2);
85
- };
86
- }
87
- if (entryToken !== prevEntryToken.current) {
88
- prevEntryToken.current = entryToken;
89
- setFlowValue(0);
90
- let raf1 = 0;
91
- let raf2 = 0;
92
- raf1 = requestAnimationFrame(() => {
93
- raf2 = requestAnimationFrame(() => setFlowValue(value));
94
- });
95
- return () => {
96
- cancelAnimationFrame(raf1);
97
- cancelAnimationFrame(raf2);
98
- };
99
- }
100
- setFlowValue(value);
101
- }, [entryToken, showSpinner, value]);
102
-
103
- return (
104
- <span
105
- className={cn(
106
- "inline-flex min-w-[2.25rem] items-center justify-center gap-1 rounded-md border px-2 py-0.5 text-xs font-semibold tabular-nums shadow-sm",
107
- className,
108
- )}
109
- title={valueTitle}
110
- >
111
- <span aria-hidden className="opacity-90">
112
- {label}
113
- </span>
114
- {showSpinner ? (
115
- // css-loaders.com/dots — styles: `index.css` → `.board-stats-dots-loader`
116
- <div
117
- className="board-stats-dots-loader shrink-0"
118
- aria-hidden
119
- />
120
- ) : (
121
- // @number-flow/react: flowValue drives both entry (0→n after spinner) and later updates.
122
- <span
123
- className="inline-flex min-w-[1.25rem] justify-end [font-variant-numeric:tabular-nums]"
124
- aria-label={`${valueTitle}: ${value}`}
125
- >
126
- <NumberFlow
127
- value={flowValue}
128
- plugins={[continuous]}
129
- animated={canAnimate}
130
- className="leading-none"
131
- {...STATS_FLOW_TIMING}
132
- willChange
133
- />
134
- </span>
135
- )}
136
- </span>
137
- );
138
- }
139
-
140
- export function BoardStatsChipsRow({
141
- stats,
142
- listCount,
143
- showSpinner,
144
- entryToken,
145
- }: {
146
- stats: TaskCountStat;
147
- /** Structural count of lists on the board (not affected by task filters). */
148
- listCount: number;
149
- showSpinner: boolean;
150
- entryToken: number;
151
- }) {
152
- return (
153
- <div
154
- className="inline-flex flex-wrap items-center gap-1.5"
155
- aria-label="Task counts for current filters"
156
- >
157
- <StatChip
158
- label="L"
159
- value={listCount}
160
- showSpinner={false}
161
- entryToken={entryToken}
162
- valueTitle="Lists on this board"
163
- className={chipBoardL}
164
- />
165
- <StatChip
166
- label="T"
167
- value={stats.total}
168
- showSpinner={showSpinner}
169
- entryToken={entryToken}
170
- valueTitle="Total tasks"
171
- className={chipBoardT}
172
- />
173
- <StatChip
174
- label="O"
175
- value={stats.open}
176
- showSpinner={showSpinner}
177
- entryToken={entryToken}
178
- valueTitle="Open / in-progress tasks"
179
- className={chipBoardO}
180
- />
181
- <StatChip
182
- label="C"
183
- value={stats.closed}
184
- showSpinner={showSpinner}
185
- entryToken={entryToken}
186
- valueTitle="Closed tasks"
187
- className={chipBoardC}
188
- />
189
- </div>
190
- );
191
- }
192
-
193
- export function ListStatsChipsRow({
194
- stats,
195
- showSpinner,
196
- entryToken,
197
- }: {
198
- stats: TaskCountStat;
199
- showSpinner: boolean;
200
- entryToken: number;
201
- }) {
202
- return (
203
- <div
204
- className="flex items-center justify-center gap-1 border-b border-border/60 bg-muted/40 px-2 py-1"
205
- aria-label="List task counts"
206
- >
207
- <StatChip
208
- label="T"
209
- value={stats.total}
210
- showSpinner={showSpinner}
211
- entryToken={entryToken}
212
- valueTitle="Total tasks in this list"
213
- className={chipListT}
214
- />
215
- <StatChip
216
- label="O"
217
- value={stats.open}
218
- showSpinner={showSpinner}
219
- entryToken={entryToken}
220
- valueTitle="Open / in-progress tasks in this list"
221
- className={chipListO}
222
- />
223
- <StatChip
224
- label="C"
225
- value={stats.closed}
226
- showSpinner={showSpinner}
227
- entryToken={entryToken}
228
- valueTitle="Closed tasks in this list"
229
- className={chipListC}
230
- />
231
- </div>
232
- );
233
- }
1
+ import NumberFlow, {
2
+ continuous,
3
+ useCanAnimate,
4
+ } from "@number-flow/react";
5
+ import { useLayoutEffect, useRef, useState } from "react";
6
+ import type { TaskCountStat } from "../../../shared/boardStats";
7
+ import { cn } from "@/lib/utils";
8
+
9
+ /** Low-saturation chip fills so T/O/C read as soft status tints, not full banners. */
10
+ const chipBoardT =
11
+ "border-border/60 bg-muted/55 text-foreground dark:border-border/50 dark:bg-muted/45";
12
+ /** O = non-closed tasks — orange tint with readable opacity (avoid washed-out / red-looking tints). */
13
+ const chipBoardO =
14
+ "border-orange-500/45 bg-orange-500/68 text-orange-950 dark:border-orange-400/50 dark:text-orange-50";
15
+ const chipBoardC =
16
+ "border-emerald-600/35 bg-emerald-600/68 text-emerald-950 dark:border-emerald-500/40 dark:text-emerald-100";
17
+
18
+ const chipBoardL =
19
+ "border-border/60 bg-muted/55 text-foreground dark:border-border/50 dark:bg-muted/45";
20
+
21
+ const chipListT =
22
+ "border-border/50 bg-muted/35 text-foreground dark:bg-muted/30";
23
+ const chipListO =
24
+ "border-orange-500/38 bg-orange-500/60 text-orange-950 dark:border-orange-400/42 dark:text-orange-50";
25
+ const chipListC =
26
+ "border-emerald-600/18 bg-emerald-600/50 text-emerald-950 dark:border-emerald-500/18 dark:text-emerald-100";
27
+
28
+ const STATS_FLOW_TIMING = {
29
+ spinTiming: {
30
+ duration: 450,
31
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)",
32
+ } as const,
33
+ transformTiming: {
34
+ duration: 400,
35
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)",
36
+ } as const,
37
+ opacityTiming: {
38
+ duration: 220,
39
+ easing: "ease-out",
40
+ } as const,
41
+ };
42
+
43
+ function StatChip({
44
+ label,
45
+ value,
46
+ className,
47
+ showSpinner,
48
+ entryToken,
49
+ valueTitle,
50
+ }: {
51
+ label: "T" | "O" | "C" | "L";
52
+ value: number;
53
+ className?: string;
54
+ showSpinner: boolean;
55
+ entryToken: number;
56
+ /** Exposed to assistive tech — full word, not shown as chip text. */
57
+ valueTitle: string;
58
+ }) {
59
+ const canAnimate = useCanAnimate();
60
+ const [flowValue, setFlowValue] = useState(() => (showSpinner ? 0 : value));
61
+ const prevShowSpinner = useRef(showSpinner);
62
+ const prevEntryToken = useRef(entryToken);
63
+
64
+ // After the loading spinner hides, run 0 → value once so NumberFlow performs an entry count.
65
+ // useLayoutEffect avoids one painted frame at the old count before resetting to 0.
66
+ // When the stats are merely revealed from hidden state, `entryToken` provides the same one-shot
67
+ // 0 → value path even if TanStack Query already has cached numbers and skips the spinner.
68
+ // When the spinner is off and `value` changes (filters, refetch without spinner), sync directly.
69
+ useLayoutEffect(() => {
70
+ if (showSpinner) {
71
+ prevShowSpinner.current = true;
72
+ return;
73
+ }
74
+ if (prevShowSpinner.current) {
75
+ prevShowSpinner.current = false;
76
+ setFlowValue(0);
77
+ let raf1 = 0;
78
+ let raf2 = 0;
79
+ raf1 = requestAnimationFrame(() => {
80
+ raf2 = requestAnimationFrame(() => setFlowValue(value));
81
+ });
82
+ return () => {
83
+ cancelAnimationFrame(raf1);
84
+ cancelAnimationFrame(raf2);
85
+ };
86
+ }
87
+ if (entryToken !== prevEntryToken.current) {
88
+ prevEntryToken.current = entryToken;
89
+ setFlowValue(0);
90
+ let raf1 = 0;
91
+ let raf2 = 0;
92
+ raf1 = requestAnimationFrame(() => {
93
+ raf2 = requestAnimationFrame(() => setFlowValue(value));
94
+ });
95
+ return () => {
96
+ cancelAnimationFrame(raf1);
97
+ cancelAnimationFrame(raf2);
98
+ };
99
+ }
100
+ setFlowValue(value);
101
+ }, [entryToken, showSpinner, value]);
102
+
103
+ return (
104
+ <span
105
+ className={cn(
106
+ "inline-flex min-w-[2.25rem] items-center justify-center gap-1 rounded-md border px-2 py-0.5 text-xs font-semibold tabular-nums shadow-sm",
107
+ className,
108
+ )}
109
+ title={valueTitle}
110
+ >
111
+ <span aria-hidden className="opacity-90">
112
+ {label}
113
+ </span>
114
+ {showSpinner ? (
115
+ // css-loaders.com/dots — styles: `index.css` → `.board-stats-dots-loader`
116
+ <div
117
+ className="board-stats-dots-loader shrink-0"
118
+ aria-hidden
119
+ />
120
+ ) : (
121
+ // @number-flow/react: flowValue drives both entry (0→n after spinner) and later updates.
122
+ <span
123
+ className="inline-flex min-w-[1.25rem] justify-end [font-variant-numeric:tabular-nums]"
124
+ aria-label={`${valueTitle}: ${value}`}
125
+ >
126
+ <NumberFlow
127
+ value={flowValue}
128
+ plugins={[continuous]}
129
+ animated={canAnimate}
130
+ className="leading-none"
131
+ {...STATS_FLOW_TIMING}
132
+ willChange
133
+ />
134
+ </span>
135
+ )}
136
+ </span>
137
+ );
138
+ }
139
+
140
+ export function BoardStatsChipsRow({
141
+ stats,
142
+ listCount,
143
+ showSpinner,
144
+ entryToken,
145
+ }: {
146
+ stats: TaskCountStat;
147
+ /** Structural count of lists on the board (not affected by task filters). */
148
+ listCount: number;
149
+ showSpinner: boolean;
150
+ entryToken: number;
151
+ }) {
152
+ return (
153
+ <div
154
+ className="inline-flex flex-wrap items-center gap-1.5"
155
+ aria-label="Task counts for current filters"
156
+ >
157
+ <StatChip
158
+ label="L"
159
+ value={listCount}
160
+ showSpinner={false}
161
+ entryToken={entryToken}
162
+ valueTitle="Lists on this board"
163
+ className={chipBoardL}
164
+ />
165
+ <StatChip
166
+ label="T"
167
+ value={stats.total}
168
+ showSpinner={showSpinner}
169
+ entryToken={entryToken}
170
+ valueTitle="Total tasks"
171
+ className={chipBoardT}
172
+ />
173
+ <StatChip
174
+ label="O"
175
+ value={stats.open}
176
+ showSpinner={showSpinner}
177
+ entryToken={entryToken}
178
+ valueTitle="Open / in-progress tasks"
179
+ className={chipBoardO}
180
+ />
181
+ <StatChip
182
+ label="C"
183
+ value={stats.closed}
184
+ showSpinner={showSpinner}
185
+ entryToken={entryToken}
186
+ valueTitle="Closed tasks"
187
+ className={chipBoardC}
188
+ />
189
+ </div>
190
+ );
191
+ }
192
+
193
+ export function ListStatsChipsRow({
194
+ stats,
195
+ showSpinner,
196
+ entryToken,
197
+ }: {
198
+ stats: TaskCountStat;
199
+ showSpinner: boolean;
200
+ entryToken: number;
201
+ }) {
202
+ return (
203
+ <div
204
+ className="flex items-center justify-center gap-1 border-b border-border/60 bg-muted/40 px-2 py-1"
205
+ aria-label="List task counts"
206
+ >
207
+ <StatChip
208
+ label="T"
209
+ value={stats.total}
210
+ showSpinner={showSpinner}
211
+ entryToken={entryToken}
212
+ valueTitle="Total tasks in this list"
213
+ className={chipListT}
214
+ />
215
+ <StatChip
216
+ label="O"
217
+ value={stats.open}
218
+ showSpinner={showSpinner}
219
+ entryToken={entryToken}
220
+ valueTitle="Open / in-progress tasks in this list"
221
+ className={chipListO}
222
+ />
223
+ <StatChip
224
+ label="C"
225
+ value={stats.closed}
226
+ showSpinner={showSpinner}
227
+ entryToken={entryToken}
228
+ valueTitle="Closed tasks in this list"
229
+ className={chipListC}
230
+ />
231
+ </div>
232
+ );
233
+ }
@@ -1,41 +1,41 @@
1
- import { createContext, useContext, type ReactNode } from "react";
2
- import type { TaskCountStat } from "../../../shared/boardStats";
3
-
4
- export interface BoardStatsDisplayValue {
5
- /** Board-level T/O/C when stats are enabled and loaded (or placeholder). */
6
- board: TaskCountStat | null;
7
- listStat(listId: number): TaskCountStat;
8
- /** Increments when stats visibility turns on so chips can run one-shot entry motion. */
9
- entryToken: number;
10
- /** True while fetching (including background refresh after filter change). */
11
- fetching: boolean;
12
- /** True on first load with no cached placeholder. */
13
- pending: boolean;
14
- /** Show spinner inside chips (initial load or stale placeholder during refetch). */
15
- showChipSpinner: boolean;
16
- /** True when the stats request failed; avoid showing misleading zero chips. */
17
- statsError: boolean;
18
- }
19
-
20
- const BoardStatsDisplayContext = createContext<BoardStatsDisplayValue | null>(
21
- null,
22
- );
23
-
24
- export function BoardStatsDisplayProvider({
25
- value,
26
- children,
27
- }: {
28
- value: BoardStatsDisplayValue;
29
- children: ReactNode;
30
- }) {
31
- return (
32
- <BoardStatsDisplayContext.Provider value={value}>
33
- {children}
34
- </BoardStatsDisplayContext.Provider>
35
- );
36
- }
37
-
38
- /** List columns read per-list stats; returns null when stats are hidden or unavailable. */
39
- export function useBoardStatsDisplayOptional(): BoardStatsDisplayValue | null {
40
- return useContext(BoardStatsDisplayContext);
41
- }
1
+ import { createContext, useContext, type ReactNode } from "react";
2
+ import type { TaskCountStat } from "../../../shared/boardStats";
3
+
4
+ export interface BoardStatsDisplayValue {
5
+ /** Board-level T/O/C when stats are enabled and loaded (or placeholder). */
6
+ board: TaskCountStat | null;
7
+ listStat(listId: number): TaskCountStat;
8
+ /** Increments when stats visibility turns on so chips can run one-shot entry motion. */
9
+ entryToken: number;
10
+ /** True while fetching (including background refresh after filter change). */
11
+ fetching: boolean;
12
+ /** True on first load with no cached placeholder. */
13
+ pending: boolean;
14
+ /** Show spinner inside chips (initial load or stale placeholder during refetch). */
15
+ showChipSpinner: boolean;
16
+ /** True when the stats request failed; avoid showing misleading zero chips. */
17
+ statsError: boolean;
18
+ }
19
+
20
+ const BoardStatsDisplayContext = createContext<BoardStatsDisplayValue | null>(
21
+ null,
22
+ );
23
+
24
+ export function BoardStatsDisplayProvider({
25
+ value,
26
+ children,
27
+ }: {
28
+ value: BoardStatsDisplayValue;
29
+ children: ReactNode;
30
+ }) {
31
+ return (
32
+ <BoardStatsDisplayContext.Provider value={value}>
33
+ {children}
34
+ </BoardStatsDisplayContext.Provider>
35
+ );
36
+ }
37
+
38
+ /** List columns read per-list stats; returns null when stats are hidden or unavailable. */
39
+ export function useBoardStatsDisplayOptional(): BoardStatsDisplayValue | null {
40
+ return useContext(BoardStatsDisplayContext);
41
+ }
@@ -1,38 +1,38 @@
1
- import { cn } from "@/lib/utils";
2
-
3
- /** Labels above filter button rows — foreground-tinted for readable contrast on header surfaces in light and dark themes. */
4
- export const BOARD_HEADER_FILTER_SECTION_LABEL_CLASS =
5
- "text-xs font-semibold uppercase tracking-wide text-foreground/90";
6
-
7
- const BOARD_HEADER_TEXT_BUTTON_BASE_CLASS =
8
- "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors";
9
-
10
- // Keep header text buttons on a single shared visual system so filters and
11
- // board actions stay consistent as more controls are added to the strip.
12
- export function boardHeaderToggleButtonClass(active: boolean) {
13
- return cn(
14
- BOARD_HEADER_TEXT_BUTTON_BASE_CLASS,
15
- active
16
- // Use the app surface token instead of brand color so active buttons stay neutral across board themes.
17
- ? "border-border/80 bg-background/75 text-foreground shadow-sm backdrop-blur-sm"
18
- : "border-border bg-muted/40 text-foreground/60 hover:bg-muted hover:text-foreground",
19
- );
20
- }
21
-
22
- export function boardHeaderActionButtonClass() {
23
- return cn(
24
- BOARD_HEADER_TEXT_BUTTON_BASE_CLASS,
25
- "border-border bg-muted/40 text-foreground hover:bg-muted",
26
- );
27
- }
28
-
29
- /** Reserves space for the section edit icon so filter labels and buttons do not shift on header hover. */
30
- export const BOARD_HEADER_SECTION_EDIT_ICON_SLOT_CLASS =
31
- "inline-flex h-5 w-5 shrink-0 items-center justify-center";
32
-
33
- export function boardHeaderSectionEditIconButtonClass(headerHovered: boolean) {
34
- return cn(
35
- "inline-flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground transition-opacity duration-150 hover:bg-black/[0.06] hover:text-foreground dark:hover:bg-white/[0.06]",
36
- headerHovered ? "opacity-100" : "opacity-0 pointer-events-none",
37
- );
38
- }
1
+ import { cn } from "@/lib/utils";
2
+
3
+ /** Labels above filter button rows — foreground-tinted for readable contrast on header surfaces in light and dark themes. */
4
+ export const BOARD_HEADER_FILTER_SECTION_LABEL_CLASS =
5
+ "text-xs font-semibold uppercase tracking-wide text-foreground/90";
6
+
7
+ const BOARD_HEADER_TEXT_BUTTON_BASE_CLASS =
8
+ "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors";
9
+
10
+ // Keep header text buttons on a single shared visual system so filters and
11
+ // board actions stay consistent as more controls are added to the strip.
12
+ export function boardHeaderToggleButtonClass(active: boolean) {
13
+ return cn(
14
+ BOARD_HEADER_TEXT_BUTTON_BASE_CLASS,
15
+ active
16
+ // Use the app surface token instead of brand color so active buttons stay neutral across board themes.
17
+ ? "border-border/80 bg-background/75 text-foreground shadow-sm backdrop-blur-sm"
18
+ : "border-border bg-muted/40 text-foreground/60 hover:bg-muted hover:text-foreground",
19
+ );
20
+ }
21
+
22
+ export function boardHeaderActionButtonClass() {
23
+ return cn(
24
+ BOARD_HEADER_TEXT_BUTTON_BASE_CLASS,
25
+ "border-border bg-muted/40 text-foreground hover:bg-muted",
26
+ );
27
+ }
28
+
29
+ /** Reserves space for the section edit icon so filter labels and buttons do not shift on header hover. */
30
+ export const BOARD_HEADER_SECTION_EDIT_ICON_SLOT_CLASS =
31
+ "inline-flex h-5 w-5 shrink-0 items-center justify-center";
32
+
33
+ export function boardHeaderSectionEditIconButtonClass(headerHovered: boolean) {
34
+ return cn(
35
+ "inline-flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground transition-opacity duration-150 hover:bg-black/[0.06] hover:text-foreground dark:hover:bg-white/[0.06]",
36
+ headerHovered ? "opacity-100" : "opacity-0 pointer-events-none",
37
+ );
38
+ }