V1 Web PWA · Surface walkthrough

Library · new collection

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

walked + e2e-covered
9sections4 minread

On this page

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

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

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 tabLCustomerNav active="library"
  • From /library/collections — the "Start a collection" primary button (see 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#

  • Idle form — empty label + seed inputs; submit reads "Create the collection"
  • Pendingpending === 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"
  • 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
  • Rejected (server error) — non-ok response; role="alert" box shows the BFF message from RejectedResponse, else the generic "Library could not create the collection ()."
  • Offline / network throwfetch throws; role="alert" reads "Library is offline. Try again in a moment." (the form is preserved for retry)
  • Validation (client) — label required, minLength={2}, maxLength={120}; native HTML validation blocks empty/too-short submit
  • Standalone PWA — renders cleanly; shell safe-area plus the wrapped action row avoid horizontal overflow on a 390 px standalone viewport

Interactions#

Form fields#

  • "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
  • "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#

  • "Back" (LBtn ghost, link) → /library/collections (flex:1)
  • "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#

  • "All collections" (LBtn ghost, link) → /library/collections
  • "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.

  • 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#

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