diff --git a/src/app/components/dashboard/ActionsTab.tsx b/src/app/components/dashboard/ActionsTab.tsx index 62be2dea..3a486c88 100644 --- a/src/app/components/dashboard/ActionsTab.tsx +++ b/src/app/components/dashboard/ActionsTab.tsx @@ -1,11 +1,10 @@ import { createMemo, For, Show } from "solid-js"; import { createStore } from "solid-js/store"; -import type { WorkflowRun, ApiError } from "../../services/api"; +import type { WorkflowRun } from "../../services/api"; import { config } from "../../stores/config"; import { viewState, setViewState, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, type ActionsFilterField } from "../../stores/view"; import WorkflowRunRow from "./WorkflowRunRow"; import IgnoreBadge from "./IgnoreBadge"; -import ErrorBannerList from "../shared/ErrorBannerList"; import SkeletonRows from "../shared/SkeletonRows"; import FilterChips from "../shared/FilterChips"; import type { FilterChipGroupDef } from "../shared/FilterChips"; @@ -14,7 +13,6 @@ import ChevronIcon from "../shared/ChevronIcon"; interface ActionsTabProps { workflowRuns: WorkflowRun[]; loading?: boolean; - errors?: ApiError[]; } interface WorkflowGroup { @@ -199,13 +197,10 @@ export default function ActionsTab(props: ActionsTabProps) { - {/* Error */} - ({ source: e.repo, message: e.message, retryable: e.retryable }))} /> - {/* Empty */}
diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index 395e9ee2..7b148e50 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -8,11 +8,9 @@ import IssuesTab from "./IssuesTab"; import PullRequestsTab from "./PullRequestsTab"; import { config } from "../../stores/config"; import { viewState, updateViewState } from "../../stores/view"; -import type { Issue, PullRequest, WorkflowRun, ApiError } from "../../services/api"; +import type { Issue, PullRequest, WorkflowRun } from "../../services/api"; import { createPollCoordinator, fetchAllData, type DashboardData } from "../../services/poll"; import { clearAuth, user, onAuthCleared, DASHBOARD_STORAGE_KEY } from "../../stores/auth"; -import { getErrors, dismissError } from "../../lib/errors"; -import ErrorBannerList from "../shared/ErrorBannerList"; // ── Shared dashboard store (module-level to survive navigation) ───────────── @@ -22,7 +20,6 @@ interface DashboardStore { issues: Issue[]; pullRequests: PullRequest[]; workflowRuns: WorkflowRun[]; - errors: ApiError[]; loading: boolean; lastRefreshedAt: Date | null; } @@ -31,7 +28,6 @@ const initialDashboardState: DashboardStore = { issues: [], pullRequests: [], workflowRuns: [], - errors: [], loading: true, lastRefreshedAt: null, }; @@ -51,7 +47,6 @@ function loadCachedDashboard(): DashboardStore { issues: parsed.issues as Issue[], pullRequests: parsed.pullRequests as PullRequest[], workflowRuns: parsed.workflowRuns as WorkflowRun[], - errors: [], loading: false, lastRefreshedAt: typeof parsed.lastRefreshedAt === "string" ? new Date(parsed.lastRefreshedAt) : null, }; @@ -93,7 +88,6 @@ async function pollFetch(): Promise { issues: data.issues, pullRequests: data.pullRequests, workflowRuns: data.workflowRuns, - errors: data.errors, loading: false, lastRefreshedAt: now, }); @@ -191,19 +185,12 @@ export default function DashboardPage() { onRefresh={() => _coordinator()?.manualRefresh()} /> - {/* Global error banner */} - ({ source: e.source, message: e.message, retryable: e.retryable }))} - onDismiss={(index) => dismissError(getErrors()[index].id)} - /> -
@@ -211,7 +198,6 @@ export default function DashboardPage() { @@ -219,7 +205,6 @@ export default function DashboardPage() { diff --git a/src/app/components/dashboard/IssuesTab.tsx b/src/app/components/dashboard/IssuesTab.tsx index 356ebb70..bcfa1d08 100644 --- a/src/app/components/dashboard/IssuesTab.tsx +++ b/src/app/components/dashboard/IssuesTab.tsx @@ -2,11 +2,10 @@ import { createEffect, createMemo, createSignal, For, Show } from "solid-js"; import { createStore } from "solid-js/store"; import { config } from "../../stores/config"; import { viewState, setSortPreference, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, type IssueFilterField } from "../../stores/view"; -import type { Issue, ApiError } from "../../services/api"; +import type { Issue } from "../../services/api"; import ItemRow from "./ItemRow"; import IgnoreBadge from "./IgnoreBadge"; import SortIcon from "../shared/SortIcon"; -import ErrorBannerList from "../shared/ErrorBannerList"; import PaginationControls from "../shared/PaginationControls"; import FilterChips from "../shared/FilterChips"; import type { FilterChipGroupDef } from "../shared/FilterChips"; @@ -19,7 +18,6 @@ import { groupByRepo, computePageLayout, slicePageGroups } from "../../lib/group export interface IssuesTabProps { issues: Issue[]; loading?: boolean; - errors?: ApiError[]; userLogin: string; } @@ -162,8 +160,6 @@ export default function IssuesTab(props: IssuesTabProps) { return (
- ({ source: e.repo, message: e.message, retryable: e.retryable }))} /> - {/* Column headers */}
- ({ source: e.repo, message: e.message, retryable: e.retryable }))} /> - {/* Column headers */}
getUnreadCount(); + const coreRL = () => getCoreRateLimit(); const searchRL = () => getSearchRateLimit(); @@ -20,95 +35,125 @@ export default function Header() { } return ( -
- - GitHub Tracker - + <> +
+ + GitHub Tracker + -
+
- -
- Rate Limits -
- - {(rl) => ( - - {formatLimit(rl().remaining, 5000, "hr")} - - )} - - - {(rl) => ( - - {formatLimit(rl().remaining, 30, "min")} - - )} - + +
+ Rate Limits +
+ + {(rl) => ( + + {formatLimit(rl().remaining, 5000, "hr")} + + )} + + + {(rl) => ( + + {formatLimit(rl().remaining, 30, "min")} + + )} + +
-
- + - - {(u) => ( -
- {u().login} + {(u) => ( +
+ {u().login} + +
+ )} + + + + -
- )} -
+ + - - - + + 0}> + + {unreadCount() > 9 ? "9+" : unreadCount()} + + + - -
+ + +
+ setDrawerOpen(false)} /> + + ); } diff --git a/src/app/components/shared/ErrorBannerList.tsx b/src/app/components/shared/ErrorBannerList.tsx deleted file mode 100644 index 11b3124e..00000000 --- a/src/app/components/shared/ErrorBannerList.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { For, Show } from "solid-js"; - -export interface ErrorBannerItem { - source: string; - message: string; - retryable?: boolean; -} - -export default function ErrorBannerList(props: { - errors?: ErrorBannerItem[]; - onDismiss?: (index: number) => void; -}) { - return ( - 0}> -
- - {(err, index) => ( - - )} - -
-
- ); -} diff --git a/src/app/components/shared/NotificationDrawer.tsx b/src/app/components/shared/NotificationDrawer.tsx new file mode 100644 index 00000000..0dc847a6 --- /dev/null +++ b/src/app/components/shared/NotificationDrawer.tsx @@ -0,0 +1,200 @@ +import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"; +import { + getNotifications, + markAllAsRead, + clearNotifications, + dismissError, + addMutedSource, +} from "../../lib/errors"; +import { relativeTime } from "../../lib/format"; +import { severityConfig } from "./ToastContainer"; + +interface NotificationDrawerProps { + open: boolean; + onClose: () => void; +} + +export default function NotificationDrawer(props: NotificationDrawerProps) { + const [visible, setVisible] = createSignal(false); + const [closing, setClosing] = createSignal(false); + let closeTimeoutHandle: ReturnType | undefined; + let closeButtonRef: HTMLButtonElement | undefined; + + const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + const animDelay = reducedMotion ? 0 : 300; + + createEffect(() => { + if (props.open) { + // Clear any pending close timeout on re-open + if (closeTimeoutHandle !== undefined) { + clearTimeout(closeTimeoutHandle); + closeTimeoutHandle = undefined; + } + setClosing(false); + setVisible(true); + queueMicrotask(() => closeButtonRef?.focus()); + } else { + if (visible()) { + setClosing(true); + closeTimeoutHandle = setTimeout(() => { + setVisible(false); + closeTimeoutHandle = undefined; + }, animDelay); + } + } + }); + + onCleanup(() => { + if (closeTimeoutHandle !== undefined) clearTimeout(closeTimeoutHandle); + }); + + // Escape key handler + createEffect(() => { + if (!visible()) return; + const handler = (e: KeyboardEvent) => { + if (e.key === "Escape") props.onClose(); + }; + document.addEventListener("keydown", handler); + onCleanup(() => document.removeEventListener("keydown", handler)); + }); + + const sortedNotifications = createMemo(() => getNotifications().slice().reverse()); + + function handleDismissAll() { + const current = getNotifications(); + for (const n of current) addMutedSource(n.source); + clearNotifications(); + } + + return ( + + <> + {/* Overlay */} +