V1 Web PWA · Surface walkthrough

Legal · Privacy Policy

A per-surface walkthrough of the V1 Web PWA customer surface: layout, states, interactions, data, and cross-references.

walked
9sections5 minread

On this page

Context. surface customer · domain onboarding-public · route /legal/privacy · auth anon · source apps/oshun/web/src/app/legal/privacy/page.tsx

Last walked. 2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data (2xx), console/page-errors, expected content, screenshot verified; manual screen-reader and real telemetry-delivery checks pending. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md; content re-verified 2026-06-03 against current source; 2026-06-28 public-legal-pages Playwright pass covered anonymous routing, exact section inventory, legal nav matrix/active states, TOC/back-to-top/print shell controls, 390 px no-overflow, 44 px touch targets, offline-after-load stability, no telemetry hooks, and axe. Manual AT pass and real telemetry delivery remain pending.

Purpose#

OSHUN's full privacy policy, rendered in the Lilith manuscript register (cream paper, terracotta accent, Cormorant Garamond serif). Fifteen titled sections delivered as a single <article> inside LegalPageShell, which adds cross-page legal nav, scroll-spy TOC, back-to-top, and a print stylesheet.

Entry points#

  • Footer link "Privacy Policy" on /welcome and /landing
  • Sidebar nav inside any other /legal/* page (the shell renders a fixed LEGAL_NAV list)
  • Direct URL / bookmark — yes (anon)
  • Linked from in-product privacy controls (e.g., profile settings, cookie consent banner)
  • Email footer "Privacy" link

Layout regions#

page.tsx declares a SECTIONS array of 15 anchor ids and wraps the article body in <LegalPageShell sections={SECTIONS}>. All visual styling is injected by the shell as a <style> block; the article itself uses only semantic HTML and data-legal-date / data-legal-table-wrap / data-legal-mono hooks.

  • Header (shell): minimal [data-legal-header] with brand link and a back link (rendered by LegalPageShell)
  • Skip link: [data-legal-skip-link] jumps to main content
  • Sticky sidebar: [data-legal-sidebar] — cross-legal nav (7 entries) plus in-page TOC button list ("On this page")
  • Main article: [data-legal-content] article — h1 "Privacy Policy", data-legal-date "Last updated: May 26, 2026", then 15 <section> anchors (introduction, information-we-collect, how-we-use, data-sharing, data-retention, data-security, your-rights, regional-notices, dsar-companion, data-residency, moderation-reporting, international-transfers, childrens-privacy, changes, contact)
  • Floating control: [data-legal-back-to-top] — 44px circular button fixed bottom-right, fades in after 300px scroll

States#

  • Loading — N/A; static server component, no async data fetched
  • Default (populated) — all 15 sections render with serif body type
  • TOC click / active section — clicking a TOC button scrolls to that section and marks it data-active="true"; passive scroll-spy remains driven by IntersectionObserver in LegalPageShell
  • Back-to-top visible — appears once window.scrollY > 300
  • Narrow viewport (≤768px) — sidebar collapses to a static block above the article; in-page TOC section is hidden via display: none !important
  • Print@media print removes sidebar, header, back-to-top; switches background to white, text to black, expands the article to full width, and prints anchor hrefs after each link
  • Error (unrecoverable) — N/A; no data dependencies, only segment error.tsx would catch
  • Offline after load — once the route is loaded, the URL, heading, section inventory, and active legal nav state remain stable when the browser context goes offline
  • Gated — N/A; anonymous-readable

Interactions#

Seven <Link> entries hardcoded in LegalPageShell's LEGAL_NAV: Privacy Policy, Terms of Service, Cookie Policy, Accessibility, CCPA, DPA, Lilith Terms.

  • Each sidebar nav link (link)
    • Function: navigates to the sibling legal page
    • Active styling: data-active attribute when pathname === href (italicised, terracotta, left-border accent)
    • Hover styling: paper background on onMouseEnter
    • Keyboard: Tab focuses; Enter activates
    • Touch target: ≥ 44 px height and width on the 390 px mobile viewport

Fifteen <button> elements, one per SECTIONS entry.

  • Each TOC button (button)
    • Function: calls scrollToSection(id)el.scrollIntoView({ behavior: 'smooth', block: 'start' })
    • Active styling: terracotta + left border when clicked immediately or when IntersectionObserver flags that section as intersecting
    • Hover: paper background
    • Keyboard: Tab into list, Enter / Space activates
    • Hidden when viewport ≤ 768px
  • "↑" back to top (button, aria-label="Back to top")
    • Function: window.scrollTo({ top: 0, behavior: 'smooth' })
    • Visibility: opacity 0 with pointer-events: none until window.scrollY > 300, then opacity 1
    • Position: fixed; bottom: 28; right: 28; 44×44

Anchors inside the article use the shell's serif/terracotta-underline style. The visible link here is an inline mailto-style reference to privacy@oshun.app rendered as plain text, not an anchor element — verify during walk whether it should be clickable.

Data & contracts#

  • Reads: none — static text only
  • Writes: none
  • Realtime: none
  • Caching: server-rendered static; loaded-route offline stability is asserted. Cold offline navigation / per-legal SW path caching is not asserted, and current sw.js does not path-cache /legal/*.
  • Auth/role check: anonymous; each legal page is listed individually in middleware PUBLIC_PATHS (e.g. /legal/privacy, /legal/terms, /legal/cookies, /legal/accessibility, /legal/ccpa, /legal/dpa) — there is no /legal prefix entry
  • Metadata: title: 'Privacy Policy', description: 'How OSHUN collects, uses, and protects your personal information.'

Cross-references#

E2E coverage#

  • Backed by apps/oshun/web/e2e/public-legal-pages.spec.ts — covers the public legal route matrix with anonymous access, exact section inventory, legal nav inventory/active state, TOC click and active state, back-to-top behavior, print chrome suppression, 390 px no-overflow, 44 px nav/back-to-top targets, offline-after-load stability, no telemetry hooks, and axe.

Open questions / known gaps#

  • Cold offline navigation / per-legal SW cache is not asserted and current sw.js does not path-cache /legal/*; PWA shell cache coverage remains in the offline-first / PWA walkthroughs.
  • Body contains a plain-text email reference (privacy@oshun.app) — confirm intent: should it be a mailto link?
  • LAST_UPDATED string is hard-coded ('May 26, 2026') — confirm the update process when policy changes
  • No "I accept" or consent affordance on this page; consent UX lives on the cookie banner shell — verify scope expectation
  • No localisation; en-US only — confirm whether i18n is in scope