---
path: /library/collections
surface: customer
domain: discovery
auth: signed-in
source: apps/oshun/web/src/app/library/collections/page.tsx
status: walked
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'
---

# Library · collections index

## 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 tab** — `LCustomerNav active="library"`
- **From `/library`** — the collections lane / "Create from filters" affordance
  in `LibraryDashboard` leads here (see [`library.md`](./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`)
- [ ] **Empty** — `data.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 <localeDate>",
      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-2xx** — `bffGet` 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

### Footer actions

- [ ] **"← 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

- Feature spec: [`V1/features.md`](../../../V1/features.md)
- Architecture: [`V1/ARCHITECTURE.md`](../../../V1/ARCHITECTURE.md)
- Sibling routes:
  - [`library.md`](./library.md) — the library dashboard (collection target)
  - [`library-collections-new.md`](./library-collections-new.md) — create form
  - [`library-collection-collectionId.md`](./library-collection-collectionId.md)
    — single-collection entry
- Component sources:
  - `apps/oshun/web/src/lib/server/bff-fetch.ts` (`bffGet`)
- Journeys traversing this view:
  - [`../../journeys/library-save-collection-share.md`](../../journeys/library-save-collection-share.md)

## 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`](./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
