---
path: /library/collections/new
surface: customer
domain: discovery
auth: signed-in
source: apps/oshun/web/src/app/library/collections/new/page.tsx
status: walked + e2e-covered
last_walked:
  '2026-06-29 real-dev-infra Playwright — signed-in shell route, idle/pending/
  accepted/error/offline/native-validation/standalone states, POST body parsing,
  aria-disabled, focus handoff, touch targets, no horizontal overflow, and
  shared axe scan verified against the live BFF. Evidence:
  apps/oshun/web/e2e/library-collections-new.spec.ts and
  WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md#201-2026-06-29-library-collection-create-form-state-coverage'
---

# Library · new collection

## Purpose

A short form for starting a named collection: a label, plus an optional seed of
item IDs. The server shell sets the manuscript chrome; the client form
(`LibraryNewCollectionForm`) POSTs to `/v1/library/collections` and swaps to a
success card on acceptance. The route root exposes
`data-library-collection-create-page` / `data-route="/library/collections/new"`
so automation can distinguish this shell view from sibling collection pages.

## Entry points

- **Shell nav: Library tab** — `LCustomerNav active="library"`
- **From `/library/collections`** — the "Start a collection" primary button (see
  [`library-collections.md`](./library-collections.md))
- **From `/library`** — "Create from filters" affordances may route here (verify
  the exact target vs. an inline clone in `LibraryDashboard`)
- **`Back`** button inside the form returns to `/library/collections`
- **Direct URL / bookmark** — yes; `canonical = '/library/collections/new'`;
  auth via shell middleware

## Layout regions

`page.tsx` is a (non-async) server component rendering static chrome around the
`'use client'` form. Centered column `maxWidth: 640`.

- **Header**: `LCustomerNav` (Library active)
- **Masthead**: `LMasthead` — left "Library · collection", right "new", kicker
  "A gathering, named", title "Start a _collection_."
- **"How this opens" note**: `LEyebrow` + serif-italic copy ("Give the
  collection a name and seed it with a few item ids from /library …")
- **Main**: `<LibraryNewCollectionForm />` — a bordered `<form>` card, OR the
  success/`role="status"` card after a successful POST

## States

- [x] **Idle form** — empty label + seed inputs; submit reads "Create the
      collection"
- [x] **Pending** — `pending === true`; submit button shows "Creating…",
      `disabled`, `aria-disabled="true"`, `opacity 0.6`, `cursor: progress`; the
      form root exposes `data-state="pending"` and `aria-busy="true"`
- [x] **Accepted (success)** — POST `response.ok`; component swaps to a
      `role="status" aria-live="polite"` card: «label» — N item(s) attached, the
      mono collection id, and two buttons ("All collections", "Open it"). Inputs
      reset to empty and the status receives focus for screen-reader users
- [x] **Rejected (server error)** — non-ok response; `role="alert"` box shows
      the BFF `message` from `RejectedResponse`, else the generic "Library could
      not create the collection (<status>)."
- [x] **Offline / network throw** — `fetch` throws; `role="alert"` reads
      "Library is offline. Try again in a moment." (the form is preserved for
      retry)
- [x] **Validation (client)** — label `required`, `minLength={2}`,
      `maxLength={120}`; native HTML validation blocks empty/too-short submit
- [x] **Standalone PWA** — renders cleanly; shell safe-area plus the wrapped
      action row avoid horizontal overflow on a 390 px standalone viewport

## Interactions

### Form fields

- [x] **"The collection, named"** (`input#col-label`)
  - Function: sets `label` state; placeholder "e.g. The night watch"
  - Validation: `required`, `minLength={2}`, `maxLength={120}` (native)
  - Keyboard: first field; Tab order top of form
  - Screen reader: `<label htmlFor="col-label">` caption "The collection, named"
  - Touch target: full-width input, ≥ 44 px tall on the standalone mobile run
- [x] **"Seed item IDs (optional, space or comma separated)"**
      (`textarea#col-seed`, 3 rows)
  - Function: sets `seedItemIds` string; placeholder "lib-aurelius-iv-viii
    lib-hadot-citadel-ch4"; `maxLength={2000}`; resizable vertically
  - On submit: split on `/[\s,]+/`, trimmed, empties filtered → `itemIds[]`
  - Screen reader: caption read via associated `<label>`

### Form actions

- [x] **"Back"** (`LBtn` ghost, link) → `/library/collections` (flex:1)
- [x] **"Create the collection" / "Creating…"** (submit
      `<button type="submit">`)
  - Function: `submit()` POSTs `{ label, itemIds }` to
    `buildOshunBffUrl('/v1/library/collections')` with `credentials: 'include'`,
    `content-type: application/json`, and CSRF headers from `csrfHeaders()`
  - Disabled when: `pending` (label "Creating…", `disabled`,
    `aria-disabled="true"`)
  - Keyboard: Enter submits the form; tab order last
  - Offline behavior: throws → "Library is offline" alert; no queueing
  - Telemetry: none observed

### Success card actions

- [x] **"All collections"** (`LBtn` ghost, link) → `/library/collections`
- [x] **"Open it"** (`LBtn` primary, link) →
      `/library?collectionId=<encodeURIComponent(accepted.collectionId)>`

## Data & contracts

- **Reads**: _None._ (the page renders static chrome; the form holds local state
  only)
- **Writes**: `POST /v1/library/collections` via `fetch(buildOshunBffUrl(...))`
  — body `{ label: string, itemIds: string[] }`; success →
  `AcceptedResponse { collectionId, label, itemCount }` plus BFF metadata
  (`generatedAt`, `state: "created"`); failure →
  `RejectedResponse { code, message }`
- **CSRF**: `csrfHeaders()` from `@/lib/csrf` — lazy `GET /v1/csrf` handshake,
  in-memory token cache, header name from the handshake (default `x-csrf-token`)
- **Realtime**: _None._
- **Caching**: client-side `fetch`, `credentials: 'include'`; no SWR/cache
- **Auth/role check**: shell middleware; cookie sent via
  `credentials: 'include'`

## E2E coverage

The Playwright route spec is the authoritative browser proof for this
walkthrough: it runs against the dev Next/BFF stack, verifies the signed-in
customer shell, proves malformed/native-invalid client payloads do not POST,
exercises live BFF accepted and 422 responses, and covers explicit offline,
pending, focus, standalone mobile, tap-target, overflow, and axe checks.

- [x] `apps/oshun/web/e2e/library-collections-new.spec.ts` — signed-in shell
      route, Library nav active state, idle form attributes, Back href,
      controlled label/seed fields, comma/space/newline seed parsing, delayed
      live-BFF pending state, exact POST body, accepted BFF response,
      success-card focus handoff, success links, real BFF `422` validation,
      explicit offline abort preservation, native required/minLength validation
      with zero POSTs, mobile standalone launch, 44 px touch targets, no
      horizontal overflow, and shared axe scan.

## Cross-references

- Feature spec: [`V1/features.md`](../../../V1/features.md)
- Architecture: [`V1/ARCHITECTURE.md`](../../../V1/ARCHITECTURE.md)
- Sibling routes:
  - [`library-collections.md`](./library-collections.md) — collections index
  - [`library.md`](./library.md) — library dashboard (open target)
- Component sources:
  - `apps/oshun/web/src/app/library/collections/new/LibraryNewCollectionForm.tsx`
  - `apps/oshun/web/src/lib/csrf.ts` (`csrfHeaders`)
  - `apps/oshun/web/src/lib/bff-endpoints.ts` (`buildOshunBffUrl`)
- Journeys traversing this view:
  - [`../../journeys/library-save-collection-share.md`](../../journeys/library-save-collection-share.md)

## Known downstream boundaries

- [ ] Seed IDs are entered as free text (`lib-…` ids the user must already know)
      — verify whether a productive flow lets a user pick saved items visually
      instead of typing raw ids (the copy says "a few item ids from /library")
- [ ] Confirm the success state's persistence vs. `/library`'s localStorage
      collection store — POST writes to the BFF; does the client store refresh?
- [ ] No telemetry on create/success/error; confirm whether collection-creation
      analytics are expected
