@hiroleague/taskmanager 0.0.3 → 0.0.4

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 (62) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/index-BpzHnKdP.css +1 -0
  3. package/dist/assets/index-DmNErTAP.js +273 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/skills/hiro-task-manager-cli/SKILL.md +6 -4
  7. package/skills/hiro-task-manager-cli/reference/cli-access-policy.md +1 -0
  8. package/skills/hiro-task-manager-cli/reference/releases.md +14 -0
  9. package/src/cli/commands/query.ts +56 -56
  10. package/src/cli/commands/releases.ts +22 -0
  11. package/src/cli/handlers/boards.test.ts +669 -669
  12. package/src/cli/handlers/cli-wiring.test.ts +38 -1
  13. package/src/cli/handlers/releases.ts +15 -0
  14. package/src/cli/handlers/search.test.ts +374 -374
  15. package/src/cli/handlers/search.ts +17 -17
  16. package/src/cli/lib/cli-http-errors.test.ts +85 -85
  17. package/src/cli/lib/write/releases.ts +64 -1
  18. package/src/cli/lib/write-result.test.ts +3 -0
  19. package/src/cli/lib/write-result.ts +3 -0
  20. package/src/cli/lib/writeCommands.breadth.test.ts +143 -0
  21. package/src/cli/lib/writeCommands.ts +1 -0
  22. package/src/cli/subprocess.real-stack.test.ts +625 -611
  23. package/src/cli/subprocess.smoke.test.ts +954 -954
  24. package/src/client/api/useBoardChangeStream.ts +421 -168
  25. package/src/client/api/useBoardIndexStream.ts +35 -0
  26. package/src/client/components/board/BoardStatsChips.tsx +233 -233
  27. package/src/client/components/board/BoardStatsContext.tsx +41 -41
  28. package/src/client/components/board/boardHeaderButtonStyles.ts +38 -38
  29. package/src/client/components/board/shortcuts/useBoardShortcutKeydown.ts +49 -49
  30. package/src/client/components/board/useBoardCanvasPanScroll.ts +108 -108
  31. package/src/client/components/board/useBoardTaskContainerDroppableReact.ts +33 -33
  32. package/src/client/components/board/useBoardTaskSortableReact.ts +26 -26
  33. package/src/client/components/layout/AppShell.tsx +5 -2
  34. package/src/client/components/layout/NotificationToasts.tsx +38 -1
  35. package/src/client/components/multi-select.tsx +1206 -1206
  36. package/src/client/components/routing/BoardPage.tsx +20 -20
  37. package/src/client/components/routing/NavigationRegistrar.tsx +13 -13
  38. package/src/client/components/task/TaskCard.tsx +643 -643
  39. package/src/client/components/ui/badge.tsx +49 -49
  40. package/src/client/components/ui/button.tsx +65 -65
  41. package/src/client/components/ui/command.tsx +193 -193
  42. package/src/client/components/ui/dialog.tsx +163 -163
  43. package/src/client/components/ui/input-group.tsx +155 -155
  44. package/src/client/components/ui/input.tsx +19 -19
  45. package/src/client/components/ui/popover.tsx +87 -87
  46. package/src/client/components/ui/separator.tsx +28 -28
  47. package/src/client/components/ui/textarea.tsx +18 -18
  48. package/src/client/index.css +248 -248
  49. package/src/client/lib/appNavigate.ts +16 -16
  50. package/src/client/lib/taskCardDate.ts +111 -111
  51. package/src/client/lib/utils.ts +6 -6
  52. package/src/client/store/notificationUi.ts +14 -0
  53. package/src/server/auth.ts +351 -351
  54. package/src/server/events.ts +31 -4
  55. package/src/server/migrations/registry.ts +43 -43
  56. package/src/server/notificationEvents.ts +8 -1
  57. package/src/server/routes/boards.ts +15 -1
  58. package/src/server/routes/trash.ts +6 -1
  59. package/src/shared/boardEvents.ts +6 -0
  60. package/src/shared/runtimeConfig.ts +256 -256
  61. package/dist/assets/index-hMFTu7sr.css +0 -1
  62. package/dist/assets/index-oKG1C41_.js +0 -273
@@ -0,0 +1,35 @@
1
+ import { useEffect } from "react";
2
+ import { useQueryClient } from "@tanstack/react-query";
3
+ import { devDirectApiOrigin } from "./devDirectApiOrigin";
4
+ import { boardKeys } from "./queries";
5
+
6
+ /** Shell-wide SSE: `GET /api/events` without boardId — keep sidebar board list in sync with CLI/agent writes. */
7
+ function boardIndexEventsUrl(): string {
8
+ const path = "/api/events";
9
+ if (import.meta.env.PROD) return path;
10
+ const raw = import.meta.env.VITE_API_ORIGIN as string | undefined;
11
+ const fallbackOrigin = devDirectApiOrigin();
12
+ const origin =
13
+ raw && raw.length > 0 ? raw.replace(/\/$/, "") : fallbackOrigin;
14
+ return `${origin}${path}`;
15
+ }
16
+
17
+ export function useBoardIndexStream(): void {
18
+ const qc = useQueryClient();
19
+
20
+ useEffect(() => {
21
+ const es = new EventSource(boardIndexEventsUrl(), {
22
+ withCredentials: true,
23
+ });
24
+
25
+ const onIndexChanged = () => {
26
+ void qc.invalidateQueries({ queryKey: boardKeys.all, exact: true });
27
+ };
28
+
29
+ es.addEventListener("board-index-changed", onIndexChanged);
30
+ return () => {
31
+ es.removeEventListener("board-index-changed", onIndexChanged);
32
+ es.close();
33
+ };
34
+ }, [qc]);
35
+ }
@@ -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
+ }