diff --git a/src/AppRouter.test.ts b/src/AppRouter.test.ts index 2c9ab7a..ac1e614 100644 --- a/src/AppRouter.test.ts +++ b/src/AppRouter.test.ts @@ -199,17 +199,32 @@ describe("AppRouter", () => { }); test("navigates to the tracked org sets page without a document reload", async () => { - vi.stubGlobal( - "fetch", - mockResolvedFetch({ ok: true, status: 200, json: async () => ({ orgSets: [] }) }), - ); + const fetchMock = mockResolvedFetch({ + ok: true, + status: 200, + json: async () => ({ orgSets: [] }), + }); + vi.stubGlobal("fetch", fetchMock); const user = userEvent.setup(); render(AppRouter); - await user.click(screen.getByRole("link", { name: "Tracked orgs" })); + await user.click(await screen.findByRole("link", { name: "Tracking 0 orgs" })); expect(window.location.pathname).toBe("/tracked"); expect(await screen.findByRole("heading", { name: "Tracked org sets" })).toBeInTheDocument(); + expect(await screen.findByRole("link", { name: "Tracking 0 orgs" })).toHaveAttribute( + "aria-current", + "page", + ); + expect( + fetchMock.mock.calls.filter(([input]) => requestUrl(input) === "/api/reports/tracked"), + ).toHaveLength(2); + window.dispatchEvent(new Event("npm.report:tracked-orgs-changed")); + await waitFor(() => + expect( + fetchMock.mock.calls.filter(([input]) => requestUrl(input) === "/api/reports/tracked"), + ).toHaveLength(3), + ); expect(screen.getByRole("main")).toHaveFocus(); }); }); diff --git a/src/TrackedOrgSets.svelte b/src/TrackedOrgSets.svelte index f03260a..51d3ec2 100644 --- a/src/TrackedOrgSets.svelte +++ b/src/TrackedOrgSets.svelte @@ -58,9 +58,7 @@
- -

Daily package trust across tracked npm org sets.

-
+
diff --git a/src/TrackedOrgSets.test.ts b/src/TrackedOrgSets.test.ts index 7a8a68c..5c9ecff 100644 --- a/src/TrackedOrgSets.test.ts +++ b/src/TrackedOrgSets.test.ts @@ -34,6 +34,8 @@ describe("TrackedOrgSets", () => { render(TrackedOrgSets); + expect(screen.getByRole("link", { name: "Tracking … orgs" })).toBeInTheDocument(); + expect(screen.getByRole("heading", { name: "Tracked org sets" })).toBeInTheDocument(); expect(screen.getByRole("status")).toHaveTextContent("Loading tracked orgs…"); expect(screen.getByRole("main")).toHaveAttribute("aria-busy", "true"); }); @@ -47,6 +49,7 @@ describe("TrackedOrgSets", () => { const { container } = render(TrackedOrgSets); expect(await screen.findByText("1 set")).toBeInTheDocument(); + expect(await screen.findByRole("link", { name: "Tracking 2 orgs" })).toBeInTheDocument(); expect(screen.getByRole("main")).toHaveAttribute("aria-busy", "false"); expect(screen.getByRole("link", { name: "acme, example" })).toHaveAttribute( "href", diff --git a/src/components/DailyTrackingButton.svelte b/src/components/DailyTrackingButton.svelte index 4653e27..9bb519d 100644 --- a/src/components/DailyTrackingButton.svelte +++ b/src/components/DailyTrackingButton.svelte @@ -46,6 +46,7 @@ const body = data as ReportRerunScheduleStatus; scheduledFor = body.nextRunAt; status = "done"; + window.dispatchEvent(new Event("npm.report:tracked-orgs-changed")); onToast("Daily tracking enabled"); } catch (reason) { status = "error"; diff --git a/src/components/DailyTrackingButton.test.ts b/src/components/DailyTrackingButton.test.ts index cb67811..cb54327 100644 --- a/src/components/DailyTrackingButton.test.ts +++ b/src/components/DailyTrackingButton.test.ts @@ -34,6 +34,8 @@ describe("DailyTrackingButton", () => { test("replaces the action with schedule status after enabling tracking", async () => { const user = userEvent.setup(); + const trackingChanged = vi.fn<() => void>(); + window.addEventListener("npm.report:tracked-orgs-changed", trackingChanged, { once: true }); vi.stubGlobal( "fetch", mockResolvedFetch({ @@ -57,6 +59,7 @@ describe("DailyTrackingButton", () => { name: `Tracking daily, next run ${formatCompactDateTime("2026-06-28T12:00:00.000Z")}`, }), ).toBeInTheDocument(); + expect(trackingChanged).toHaveBeenCalledOnce(); expect(screen.queryByRole("button", { name: "Track daily" })).not.toBeInTheDocument(); }); diff --git a/src/components/SiteHeader.svelte b/src/components/SiteHeader.svelte index cda44fb..059a748 100644 --- a/src/components/SiteHeader.svelte +++ b/src/components/SiteHeader.svelte @@ -1,3 +1,28 @@ + +
@@ -19,12 +74,23 @@ >npmreport - {#if children} diff --git a/src/styles.css b/src/styles.css index c22eb55..dad186b 100644 --- a/src/styles.css +++ b/src/styles.css @@ -180,26 +180,59 @@ a:hover { gap: var(--space-lg); margin-bottom: var(--space-md); } -.masthead__controls { +.masthead__actions { display: flex; align-items: center; - gap: var(--space-sm); + margin-left: auto; + gap: var(--space-lg); } -.masthead__tracked { +.masthead__nav-link { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-xs); + min-width: 9.75rem; padding: var(--space-xs) var(--space-sm); + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--ink-850); color: var(--fg-dim); font-family: var(--mono); font-size: var(--text-xs); text-decoration: none; white-space: nowrap; } -.masthead__tracked:hover, -.masthead__tracked[aria-current="page"] { +.masthead__nav-link:hover { + color: var(--fg); + border-color: var(--gold-dim); + background: var(--gold-soft); + text-decoration: none; +} +.masthead__nav-link[aria-current="page"] { color: var(--gold); + border-color: var(--gold-dim); + background: var(--gold-soft); +} +.masthead__nav-mark { + width: 1.5rem; + height: 1.5rem; + flex: none; } -.masthead__tracked:focus-visible { +.masthead__nav-count { + min-width: 2ch; + color: var(--gold); + font-variant-numeric: tabular-nums; + font-weight: 600; + text-align: center; +} +.masthead__nav-link:focus-visible { outline: 2px solid var(--gold-dim); - outline-offset: 2px; + outline-offset: 3px; +} +.masthead__controls { + display: flex; + align-items: center; + gap: var(--space-sm); } .glossary__btn { display: inline-flex; @@ -512,7 +545,7 @@ a:hover { align-items: flex-start; flex-wrap: wrap; } - .masthead__controls { + .masthead__actions { margin-left: auto; }