← plans · desktop-release-pipeline · DESIGN.md23 juil. 2026 à 13:00

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

  1. 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
    }
  2. 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 and null (→ null). Run with bun test like the sibling lib/*.test.ts files.

  3. Edit apps/web/app/(settings)/downloads/page.tsx:

    • Import headers from next/headers and detectDesktopPlatform; in DownloadsPage, compute const platform = detectDesktopPlatform((await headers()).get("user-agent")) and pass it to ReleaseCards.
    • In ReleaseCards, build the two card descriptors (mac, windows) as data, order them: detected platform first; null → mac first. Render primary card CTA as default Button, secondary card CTA as <Button variant="outline">. When platform is null, both CTAs stay default (today's look).
    • Do not touch FeedUnavailable, the notes section, or any copy in them. No changes to lib/downloads.ts.
  4. 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 MonitorDown from lucide-react. No new tokens, no new CSS — the data-browser-only rule already exists in packages/ui/src/styles/globals.css.

  5. Status flipbun docs/superpowers/status.ts set downloads-page-polish implemented in the shipping commit (already set to in-progress when work started).

Verification