V1 Web PWA · Surface walkthrough

Library · collections index

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

walked
8sections3 minread

On this page

Context. surface customer · domain discovery · route /library/collections · auth signed-in · source apps/oshun/web/src/app/library/collections/page.tsx

Last walked. 2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data (2xx), console/page-errors, expected content, screenshot verified; live screen-reader, touch, offline, and telemetry-delivery checks pending a manual AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md

Purpose#

A flat list of every named collection the user has made — themed gatherings of saved items (passages, sits, observations, lessons). Each row links into the library filtered to that collection; the foot CTA opens the new-collection form. Backed by GET /v1/library/collections.

Entry points#

  • Shell nav: Library tabLCustomerNav active="library"
  • From /library — the collections lane / "Create from filters" affordance in LibraryDashboard leads here (see library.md)
  • ← Back to Library ghost button at page foot returns to /library
  • After creating a collection — the success card in library-collections-new offers "All collections" → this page
  • Direct URL / bookmark — yes; canonical = '/library/collections'; auth via shell middleware

Layout regions#

page.tsx is an async server component: await bffGet<CollectionsResponse>( '/v1/library/collections') (falling back to { collections: [] } on null) → <LWebShell top={<LCustomerNav active="library" />}> wrapping a centered column (maxWidth: 880).

  • Header: LCustomerNav (Library active)
  • Masthead: LMasthead — left "Library · collections", right ${data.collections.length} held, kicker "Gatherings", title "The collections."
  • "How this opens" note: LEyebrow + serif-italic copy ("Collections are themed gatherings of saved items. Drag and drop, reorder, share or keep private — they live in your library, never indexed.")
  • Main: a grid (display: grid; gap: 12) of collection rows OR the empty card
  • Footer action row: LBtn ghost "← Back to Library" (href /library) and LBtn primary "Start a collection" (href /library/collections/new)

States#

  • Loading — server-side await bffGet; blocks render (no skeleton observed — verify loading.tsx)
  • Emptydata.collections.length === 0; dashed-border card "No collections yet. Start one to gather your first few saved items." Masthead right reads "0 held"
  • Populated (short) — 1–5 rows; each is a Link showing item count (with singular/plural "item"/"items"), optional "· updated ", the label, and a mono "Open →" affordance
  • Populated (long) — 50+ rows in a single grid; no pagination in code (verify behavior with a large roster)
  • BFF unreachable / non-2xxbffGet returns null; page substitutes { generatedAt, collections: [] } → renders the empty state (no fabricated collections, unlike the events fixture)
  • lastUpdatedAtIso absent — row omits the "· updated …" suffix
  • Offline — RSC fetch (revalidate 60); SW-cached HTML serves last render; rows are plain links
  • Standalone PWA — renders cleanly; safe-area via shell (verify)

Interactions#

Masthead#

  • Masthead labels — non-interactive

Collection row (per col in data.collections)#

  • Collection row (Link)
    • Function: navigates to /library?collectionId=<encodeURIComponent(col.id)> — opens the Library dashboard pre-filtered to this collection
    • Keyboard: Enter activates; tab order top-to-bottom
    • Screen reader: announces the item-count line + label + "Open →" (the whole card is one link; verify the count line reads naturally)
    • Touch target: full card row; verify ≥ 44 px height on coarse pointers
    • Mobile (≤ 640 px): two-column grid (1fr auto) — confirm the "Open →" chip doesn't crowd the label
    • Telemetry: none observed
    • Sub-elements (all non-interactive within the link): item-count + updated line, label, "Open →" accent text
  • "← Back to Library" (LBtn ghost, link) → /library
  • "Start a collection" (LBtn primary, link) → /library/collections/new

Data & contracts#

  • Reads: bffGet<CollectionsResponse>('/v1/library/collections'){ generatedAt: string; collections: Collection[] } where Collection = { id, label, itemCount, lastUpdatedAtIso? }. On null (network failure / non-2xx) substitutes an empty list
  • Writes: None. (creation happens on /library/collections/new)
  • Realtime: None.
  • Caching: RSC fetch, next.revalidate = 60; 5 s abort; SW-cached HTML
  • Auth/role check: shell middleware; bffGet forwards session cookie
  • Note: this index reads the BFF, whereas /library reads a client store + localStorage — confirm the two sources agree on collection membership

Cross-references#

Open questions / known gaps#

  • Reconcile the data source: this page reads GET /v1/library/collections (BFF) while /library reads localStorage['oshun.library.collections.v1'] — document which is authoritative and how they sync
  • Collection rows link to /library?collectionId= but library.md documents /library/collection/<id> — verify which collection-open route is canonical
  • No share / delete / reorder affordance on this index despite the "share or keep private … drag and drop, reorder" copy; confirm those live on the detail view
  • Confirm whether a loading.tsx skeleton exists for this segment