Downloads page polish — implementation plan
Status: In progress · Author: Claude (with Christophe) · Date: 2026-07-23 · Repo: north-os (apps/web)
Related: specs/2026-07-23-downloads-page-polish-design.html (the design), specs/2026-07-22-desktop-release-pipeline-design.html (shipped the MVP page)
Context
Executes the downloads-page-polish design spec: platform-aware ordering + primary/secondary CTAs on /downloads, and a quiet "Download the desktop app" affordance at the bottom of the main app sidebar (browser only). Three files touched, one file created. apps/web/lib/downloads.ts must not change.
Steps
-
Create
apps/web/lib/user-platform.ts— pure UA classifier:export type DesktopPlatform = "mac" | "windows" /** Server-side UA sniff for the /downloads page. Mobile UAs (iPhone/iPad * contain "like Mac OS X") must return null, not "mac". */ export function detectDesktopPlatform( userAgent: string | null, ): DesktopPlatform | null { if (!userAgent) return null if (/iPhone|iPad|Android|Mobile/i.test(userAgent)) return null if (/Windows NT/i.test(userAgent)) return "windows" if (/Macintosh|Mac OS X/i.test(userAgent)) return "mac" return null } -
Create
apps/web/lib/user-platform.test.ts— table-driven cases: mac Chrome, mac Safari, Windows 10/11 Chrome, Edge, iPhone Safari (→null), iPad (→null), Android Chrome (→null), Linux Firefox (→null), Googlebot (→null), empty string andnull(→null). Run withbun testlike the siblinglib/*.test.tsfiles. -
Edit
apps/web/app/(settings)/downloads/page.tsx:- Import
headersfromnext/headersanddetectDesktopPlatform; inDownloadsPage, computeconst platform = detectDesktopPlatform((await headers()).get("user-agent"))and pass it toReleaseCards. - In
ReleaseCards, build the two card descriptors (mac, windows) as data, order them: detected platform first;null→ mac first. Render primary card CTA as defaultButton, secondary card CTA as<Button variant="outline">. When platform isnull, both CTAs stay default (today's look). - Do not touch
FeedUnavailable, the notes section, or any copy in them. No changes tolib/downloads.ts.
- Import
-
Edit
apps/web/components/app-shell/app-sidebar.tsx— in<SidebarFooter>, above<UserMenu />, add the browser-only widget:<SidebarFooter> {/* Browser-only: inside the desktop app (data-desktop-surface="desktop") the existing globals.css rule hides [data-browser-only]. */} <SidebarMenu data-browser-only> <SidebarMenuItem> <SidebarMenuButton asChild className="text-muted-foreground" isActive={pathname === "/downloads"} > <Link href="/downloads" title="Download the desktop app"> <MonitorDown className="size-4" /> <span>Download the desktop app</span> </Link> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> <UserMenu /> </SidebarFooter>Import
MonitorDownfromlucide-react. No new tokens, no new CSS — thedata-browser-onlyrule already exists inpackages/ui/src/styles/globals.css. -
Status flip —
bun docs/superpowers/status.ts set downloads-page-polish implementedin the shipping commit (already set toin-progresswhen work started).
Verification
bun test apps/web/lib/user-platform.test.ts— all UA cases green.bun run typecheckandbun run lintfrom repo root pass.- ui-test (tier 1, browser-use daemon): load
/downloads— macOS card first + solid CTA on a mac UA; sidebar footer shows "Download the desktop app" directly above the account button; click lands on/downloads. - Feed-down check: with
updates.heynorth.devunreachable the page still renders the "Updates feed unavailable" card (behavior unchanged —lib/downloads.tsuntouched). - Desktop check (or CSS-level assertion): with
data-desktop-surface="desktop"set on<html>, the widget computesdisplay:none.