@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
@@ -1,49 +1,49 @@
1
- import { useEffect, useRef } from "react";
2
- import type { Board } from "../../../../shared/models";
3
- import { boardShortcutRegistry } from "./boardShortcutRegistry";
4
- import type { BoardShortcutActions } from "./boardShortcutTypes";
5
- import { isEditableKeyboardTarget } from "./isEditableKeyboardTarget";
6
- import { useShortcutScope } from "./ShortcutScopeContext";
7
-
8
- interface UseBoardShortcutKeydownOptions {
9
- board: Board | null;
10
- actions: BoardShortcutActions;
11
- }
12
-
13
- /**
14
- * Registers the board shortcut dispatcher with {@link ShortcutScopeProvider}.
15
- * The global listener only invokes it when the scope stack is empty (board is active).
16
- */
17
- export function useBoardShortcutKeydown({
18
- board,
19
- actions,
20
- }: UseBoardShortcutKeydownOptions): void {
21
- const { registerBoardKeyHandler } = useShortcutScope();
22
- const actionsRef = useRef(actions);
23
- actionsRef.current = actions;
24
-
25
- useEffect(() => {
26
- if (!board) {
27
- return registerBoardKeyHandler(() => {});
28
- }
29
-
30
- const onKeyDown = (e: KeyboardEvent) => {
31
- if (!board) return;
32
- if (isEditableKeyboardTarget(e.target)) return;
33
- if (e.ctrlKey || e.metaKey || e.altKey) return;
34
- // Shift+Tab keeps native reverse tab order; unmodified Tab establishes board highlight.
35
- if (e.key === "Tab" && e.shiftKey) return;
36
-
37
- for (const def of boardShortcutRegistry) {
38
- if (def.helpOnly) continue;
39
- if (!def.matchKey(e.key)) continue;
40
- if (def.enabled && !def.enabled(board)) continue;
41
- if (def.preventDefault) e.preventDefault();
42
- def.run(board, actionsRef.current);
43
- return;
44
- }
45
- };
46
-
47
- return registerBoardKeyHandler(onKeyDown);
48
- }, [board, registerBoardKeyHandler]);
49
- }
1
+ import { useEffect, useRef } from "react";
2
+ import type { Board } from "../../../../shared/models";
3
+ import { boardShortcutRegistry } from "./boardShortcutRegistry";
4
+ import type { BoardShortcutActions } from "./boardShortcutTypes";
5
+ import { isEditableKeyboardTarget } from "./isEditableKeyboardTarget";
6
+ import { useShortcutScope } from "./ShortcutScopeContext";
7
+
8
+ interface UseBoardShortcutKeydownOptions {
9
+ board: Board | null;
10
+ actions: BoardShortcutActions;
11
+ }
12
+
13
+ /**
14
+ * Registers the board shortcut dispatcher with {@link ShortcutScopeProvider}.
15
+ * The global listener only invokes it when the scope stack is empty (board is active).
16
+ */
17
+ export function useBoardShortcutKeydown({
18
+ board,
19
+ actions,
20
+ }: UseBoardShortcutKeydownOptions): void {
21
+ const { registerBoardKeyHandler } = useShortcutScope();
22
+ const actionsRef = useRef(actions);
23
+ actionsRef.current = actions;
24
+
25
+ useEffect(() => {
26
+ if (!board) {
27
+ return registerBoardKeyHandler(() => {});
28
+ }
29
+
30
+ const onKeyDown = (e: KeyboardEvent) => {
31
+ if (!board) return;
32
+ if (isEditableKeyboardTarget(e.target)) return;
33
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
34
+ // Shift+Tab keeps native reverse tab order; unmodified Tab establishes board highlight.
35
+ if (e.key === "Tab" && e.shiftKey) return;
36
+
37
+ for (const def of boardShortcutRegistry) {
38
+ if (def.helpOnly) continue;
39
+ if (!def.matchKey(e.key)) continue;
40
+ if (def.enabled && !def.enabled(board)) continue;
41
+ if (def.preventDefault) e.preventDefault();
42
+ def.run(board, actionsRef.current);
43
+ return;
44
+ }
45
+ };
46
+
47
+ return registerBoardKeyHandler(onKeyDown);
48
+ }, [board, registerBoardKeyHandler]);
49
+ }
@@ -1,108 +1,108 @@
1
- import { useCallback, useRef, useState } from "react";
2
-
3
- const ACTIVATION_PX = 6;
4
-
5
- /** Selectors for elements that must not start horizontal board pan (columns, chrome, controls). */
6
- const NO_PAN =
7
- "[data-board-no-pan],button,a[href],input,textarea,select,option,label,[role='button'],[contenteditable='true']";
8
-
9
- function targetStartsPan(target: EventTarget | null): boolean {
10
- if (!(target instanceof Element)) return false;
11
- return target.closest(NO_PAN) == null;
12
- }
13
-
14
- type PanMode = "undecided" | "horizontal" | "none";
15
-
16
- export function useBoardCanvasPanScroll() {
17
- const scrollRef = useRef<HTMLDivElement>(null);
18
- const panRef = useRef<{
19
- pointerId: number;
20
- startX: number;
21
- startY: number;
22
- startScrollLeft: number;
23
- mode: PanMode;
24
- } | null>(null);
25
-
26
- const [panning, setPanning] = useState(false);
27
-
28
- const onPointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
29
- if (e.button !== 0) return;
30
- if (!targetStartsPan(e.target)) return;
31
- const scroller = scrollRef.current;
32
- if (!scroller) return;
33
-
34
- panRef.current = {
35
- pointerId: e.pointerId,
36
- startX: e.clientX,
37
- startY: e.clientY,
38
- startScrollLeft: scroller.scrollLeft,
39
- mode: "undecided",
40
- };
41
- }, []);
42
-
43
- const onPointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
44
- const pan = panRef.current;
45
- const scroller = scrollRef.current;
46
- if (!pan || !scroller || e.pointerId !== pan.pointerId) return;
47
-
48
- if (pan.mode === "none") return;
49
-
50
- if (pan.mode === "undecided") {
51
- const dx = e.clientX - pan.startX;
52
- const dy = e.clientY - pan.startY;
53
- if (
54
- Math.abs(dx) < ACTIVATION_PX &&
55
- Math.abs(dy) < ACTIVATION_PX
56
- ) {
57
- return;
58
- }
59
- if (Math.abs(dx) >= Math.abs(dy)) {
60
- pan.mode = "horizontal";
61
- try {
62
- scroller.setPointerCapture(e.pointerId);
63
- } catch {
64
- /* already captured or unsupported */
65
- }
66
- e.preventDefault();
67
- setPanning(true);
68
- } else {
69
- pan.mode = "none";
70
- panRef.current = null;
71
- return;
72
- }
73
- }
74
-
75
- if (pan.mode === "horizontal") {
76
- const dx = e.clientX - pan.startX;
77
- scroller.scrollLeft = pan.startScrollLeft - dx;
78
- e.preventDefault();
79
- }
80
- }, []);
81
-
82
- const endPan = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
83
- const pan = panRef.current;
84
- const scroller = scrollRef.current;
85
- if (!pan || e.pointerId !== pan.pointerId) return;
86
-
87
- if (pan.mode === "horizontal" && scroller) {
88
- try {
89
- scroller.releasePointerCapture(e.pointerId);
90
- } catch {
91
- /* not captured */
92
- }
93
- setPanning(false);
94
- }
95
- panRef.current = null;
96
- }, []);
97
-
98
- return {
99
- scrollRef,
100
- panning,
101
- boardCanvasPanHandlers: {
102
- onPointerDown,
103
- onPointerMove,
104
- onPointerUp: endPan,
105
- onPointerCancel: endPan,
106
- },
107
- };
108
- }
1
+ import { useCallback, useRef, useState } from "react";
2
+
3
+ const ACTIVATION_PX = 6;
4
+
5
+ /** Selectors for elements that must not start horizontal board pan (columns, chrome, controls). */
6
+ const NO_PAN =
7
+ "[data-board-no-pan],button,a[href],input,textarea,select,option,label,[role='button'],[contenteditable='true']";
8
+
9
+ function targetStartsPan(target: EventTarget | null): boolean {
10
+ if (!(target instanceof Element)) return false;
11
+ return target.closest(NO_PAN) == null;
12
+ }
13
+
14
+ type PanMode = "undecided" | "horizontal" | "none";
15
+
16
+ export function useBoardCanvasPanScroll() {
17
+ const scrollRef = useRef<HTMLDivElement>(null);
18
+ const panRef = useRef<{
19
+ pointerId: number;
20
+ startX: number;
21
+ startY: number;
22
+ startScrollLeft: number;
23
+ mode: PanMode;
24
+ } | null>(null);
25
+
26
+ const [panning, setPanning] = useState(false);
27
+
28
+ const onPointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
29
+ if (e.button !== 0) return;
30
+ if (!targetStartsPan(e.target)) return;
31
+ const scroller = scrollRef.current;
32
+ if (!scroller) return;
33
+
34
+ panRef.current = {
35
+ pointerId: e.pointerId,
36
+ startX: e.clientX,
37
+ startY: e.clientY,
38
+ startScrollLeft: scroller.scrollLeft,
39
+ mode: "undecided",
40
+ };
41
+ }, []);
42
+
43
+ const onPointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
44
+ const pan = panRef.current;
45
+ const scroller = scrollRef.current;
46
+ if (!pan || !scroller || e.pointerId !== pan.pointerId) return;
47
+
48
+ if (pan.mode === "none") return;
49
+
50
+ if (pan.mode === "undecided") {
51
+ const dx = e.clientX - pan.startX;
52
+ const dy = e.clientY - pan.startY;
53
+ if (
54
+ Math.abs(dx) < ACTIVATION_PX &&
55
+ Math.abs(dy) < ACTIVATION_PX
56
+ ) {
57
+ return;
58
+ }
59
+ if (Math.abs(dx) >= Math.abs(dy)) {
60
+ pan.mode = "horizontal";
61
+ try {
62
+ scroller.setPointerCapture(e.pointerId);
63
+ } catch {
64
+ /* already captured or unsupported */
65
+ }
66
+ e.preventDefault();
67
+ setPanning(true);
68
+ } else {
69
+ pan.mode = "none";
70
+ panRef.current = null;
71
+ return;
72
+ }
73
+ }
74
+
75
+ if (pan.mode === "horizontal") {
76
+ const dx = e.clientX - pan.startX;
77
+ scroller.scrollLeft = pan.startScrollLeft - dx;
78
+ e.preventDefault();
79
+ }
80
+ }, []);
81
+
82
+ const endPan = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
83
+ const pan = panRef.current;
84
+ const scroller = scrollRef.current;
85
+ if (!pan || e.pointerId !== pan.pointerId) return;
86
+
87
+ if (pan.mode === "horizontal" && scroller) {
88
+ try {
89
+ scroller.releasePointerCapture(e.pointerId);
90
+ } catch {
91
+ /* not captured */
92
+ }
93
+ setPanning(false);
94
+ }
95
+ panRef.current = null;
96
+ }, []);
97
+
98
+ return {
99
+ scrollRef,
100
+ panning,
101
+ boardCanvasPanHandlers: {
102
+ onPointerDown,
103
+ onPointerMove,
104
+ onPointerUp: endPan,
105
+ onPointerCancel: endPan,
106
+ },
107
+ };
108
+ }
@@ -1,33 +1,33 @@
1
- import { CollisionPriority } from "@dnd-kit/abstract";
2
- import { useDroppable } from "@dnd-kit/react";
3
- import {
4
- BOARD_TASK_CONTAINER_DND_TYPE,
5
- BOARD_TASK_DND_TYPE,
6
- type BoardDndLayout,
7
- boardTaskContainerData,
8
- } from "./dndReactModel";
9
-
10
- /**
11
- * Phase 1 React-first wrapper for task drop containers.
12
- * This encodes the board's container metadata once so stacked lists and lane
13
- * bands can share the same droppable configuration in later phases.
14
- */
15
- export function useBoardTaskContainerDroppableReact({
16
- containerId,
17
- layout,
18
- listId,
19
- status,
20
- }: {
21
- containerId: string;
22
- layout: BoardDndLayout;
23
- listId: number;
24
- status?: string;
25
- }) {
26
- return useDroppable({
27
- id: containerId,
28
- type: BOARD_TASK_CONTAINER_DND_TYPE,
29
- accept: BOARD_TASK_DND_TYPE,
30
- collisionPriority: CollisionPriority.Low,
31
- data: boardTaskContainerData(containerId, layout, listId, status),
32
- });
33
- }
1
+ import { CollisionPriority } from "@dnd-kit/abstract";
2
+ import { useDroppable } from "@dnd-kit/react";
3
+ import {
4
+ BOARD_TASK_CONTAINER_DND_TYPE,
5
+ BOARD_TASK_DND_TYPE,
6
+ type BoardDndLayout,
7
+ boardTaskContainerData,
8
+ } from "./dndReactModel";
9
+
10
+ /**
11
+ * Phase 1 React-first wrapper for task drop containers.
12
+ * This encodes the board's container metadata once so stacked lists and lane
13
+ * bands can share the same droppable configuration in later phases.
14
+ */
15
+ export function useBoardTaskContainerDroppableReact({
16
+ containerId,
17
+ layout,
18
+ listId,
19
+ status,
20
+ }: {
21
+ containerId: string;
22
+ layout: BoardDndLayout;
23
+ listId: number;
24
+ status?: string;
25
+ }) {
26
+ return useDroppable({
27
+ id: containerId,
28
+ type: BOARD_TASK_CONTAINER_DND_TYPE,
29
+ accept: BOARD_TASK_DND_TYPE,
30
+ collisionPriority: CollisionPriority.Low,
31
+ data: boardTaskContainerData(containerId, layout, listId, status),
32
+ });
33
+ }
@@ -1,26 +1,26 @@
1
- import { useSortable } from "@dnd-kit/react/sortable";
2
- import { BOARD_TASK_DND_TYPE, boardTaskDragData } from "./dndReactModel";
3
-
4
- /**
5
- * Phase 1 React-first wrapper for sortable task rows.
6
- * Group and index are explicit so grouped multi-list movement can follow the
7
- * official multiple-sortable-lists approach during later phases.
8
- */
9
- export function useBoardTaskSortableReact(
10
- taskId: number,
11
- sortableId: string,
12
- containerId: string,
13
- index: number,
14
- ) {
15
- return useSortable({
16
- id: sortableId,
17
- index,
18
- group: containerId,
19
- type: BOARD_TASK_DND_TYPE,
20
- accept: BOARD_TASK_DND_TYPE,
21
- // Mirror the working board-list route so task drags use the same
22
- // React-first feedback clone behavior as columns.
23
- feedback: "clone",
24
- data: boardTaskDragData(taskId, containerId),
25
- });
26
- }
1
+ import { useSortable } from "@dnd-kit/react/sortable";
2
+ import { BOARD_TASK_DND_TYPE, boardTaskDragData } from "./dndReactModel";
3
+
4
+ /**
5
+ * Phase 1 React-first wrapper for sortable task rows.
6
+ * Group and index are explicit so grouped multi-list movement can follow the
7
+ * official multiple-sortable-lists approach during later phases.
8
+ */
9
+ export function useBoardTaskSortableReact(
10
+ taskId: number,
11
+ sortableId: string,
12
+ containerId: string,
13
+ index: number,
14
+ ) {
15
+ return useSortable({
16
+ id: sortableId,
17
+ index,
18
+ group: containerId,
19
+ type: BOARD_TASK_DND_TYPE,
20
+ accept: BOARD_TASK_DND_TYPE,
21
+ // Mirror the working board-list route so task drags use the same
22
+ // React-first feedback clone behavior as columns.
23
+ feedback: "clone",
24
+ data: boardTaskDragData(taskId, containerId),
25
+ });
26
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { useNotificationStream } from "@/api/useNotificationStream";
2
+ import { useBoardChangeStream } from "@/api/useBoardChangeStream";
3
3
  import { cn } from "@/lib/utils";
4
4
  import { usePreferencesStore } from "@/store/preferences";
5
5
  import { AppHeader } from "./AppHeader";
@@ -12,7 +12,10 @@ interface AppShellProps {
12
12
 
13
13
  export function AppShell({ sidebar, children }: AppShellProps) {
14
14
  const sidebarCollapsed = usePreferencesStore((s) => s.sidebarCollapsed);
15
- useNotificationStream();
15
+ // Single SSE connection for non-board pages (board-index + notifications).
16
+ // On board pages, BoardView opens its own board-scoped connection that
17
+ // supersedes this one — the effect re-runs with a boardId when navigating.
18
+ useBoardChangeStream(null, null);
16
19
 
17
20
  return (
18
21
  <div className="flex h-dvh min-h-0 flex-col bg-background">
@@ -77,14 +77,51 @@ function ToastCard({ item, onDismiss }: { item: NotificationItem; onDismiss: ()
77
77
  );
78
78
  }
79
79
 
80
+ function SystemToastCard({
81
+ message,
82
+ onDismiss,
83
+ }: {
84
+ message: string;
85
+ onDismiss: () => void;
86
+ }) {
87
+ useEffect(() => {
88
+ const timer = window.setTimeout(() => onDismiss(), 12_000);
89
+ return () => window.clearTimeout(timer);
90
+ }, [onDismiss]);
91
+
92
+ return (
93
+ <div className="pointer-events-auto rounded-xl border border-amber-500/40 bg-amber-950/90 p-3 text-sm text-amber-50 shadow-xl backdrop-blur">
94
+ <div className="flex items-start justify-between gap-3">
95
+ <p className="min-w-0 leading-5">{message}</p>
96
+ <button
97
+ type="button"
98
+ onClick={onDismiss}
99
+ className="shrink-0 rounded-md px-2 py-0.5 text-xs font-medium text-amber-200 hover:bg-amber-500/20"
100
+ >
101
+ Dismiss
102
+ </button>
103
+ </div>
104
+ </div>
105
+ );
106
+ }
107
+
80
108
  export function NotificationToasts() {
81
109
  const toasts = useNotificationUiStore((s) => s.toasts);
82
110
  const dismissToast = useNotificationUiStore((s) => s.dismissToast);
111
+ const systemToast = useNotificationUiStore((s) => s.systemToast);
112
+ const dismissSystemToast = useNotificationUiStore((s) => s.dismissSystemToast);
83
113
 
84
- if (toasts.length === 0) return null;
114
+ if (toasts.length === 0 && !systemToast) return null;
85
115
 
86
116
  return (
87
117
  <div className="pointer-events-none fixed bottom-4 right-4 z-[140] flex w-[min(24rem,calc(100vw-2rem))] flex-col gap-2">
118
+ {systemToast ? (
119
+ <SystemToastCard
120
+ key={systemToast.id}
121
+ message={systemToast.message}
122
+ onDismiss={dismissSystemToast}
123
+ />
124
+ ) : null}
88
125
  {toasts.map((toast) => (
89
126
  <div key={toast.id} className="pointer-events-auto">
90
127
  <ToastCard item={toast.notification} onDismiss={() => dismissToast(toast.id)} />