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 tab —
LCustomerNav active="library" - From
/library/collections— the "Start a collection" primary button (seelibrary-collections.md) - From
/library— "Create from filters" affordances may route here (verify the exact target vs. an inline clone inLibraryDashboard) Backbutton 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"
- Pending —
pending === true; submit button shows "Creating…",disabled,aria-disabled="true",opacity 0.6,cursor: progress; the form root exposesdata-state="pending"andaria-busy="true" - Accepted (success) — POST
response.ok; component swaps to arole="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 BFFmessagefromRejectedResponse, else the generic "Library could not create the collection ()." - Offline / network throw —
fetchthrows;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
labelstate; 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
- Function: sets
- "Seed item IDs (optional, space or comma separated)"
(
textarea#col-seed, 3 rows)- Function: sets
seedItemIdsstring; 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>
- Function: sets
Form actions#
- "Back" (
LBtnghost, link) →/library/collections(flex:1) - "Create the collection" / "Creating…" (submit
<button type="submit">)- Function:
submit()POSTs{ label, itemIds }tobuildOshunBffUrl('/v1/library/collections')withcredentials: 'include',content-type: application/json, and CSRF headers fromcsrfHeaders() - 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
- Function:
Success card actions#
- "All collections" (
LBtnghost, link) →/library/collections - "Open it" (
LBtnprimary, 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/collectionsviafetch(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— lazyGET /v1/csrfhandshake, in-memory token cache, header name from the handshake (defaultx-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 BFF422validation, 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 - Architecture:
V1/ARCHITECTURE.md - Sibling routes:
library-collections.md— collections indexlibrary.md— library dashboard (open target)
- Component sources:
apps/oshun/web/src/app/library/collections/new/LibraryNewCollectionForm.tsxapps/oshun/web/src/lib/csrf.ts(csrfHeaders)apps/oshun/web/src/lib/bff-endpoints.ts(buildOshunBffUrl)
- Journeys traversing this view:
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