V1 Web PWA · Surface walkthrough

Atelier · Start a new scene

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

walked
9sections5 minread1table

On this page

Context. surface customer · domain atelier · route /atelier/new · auth signed-in · source apps/oshun/web/src/app/atelier/new/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#

The personal-offering entry point to the Atelier: name a working title and an optional recipient, and the BFF opens a fresh draft scene in the composer. This is the user-facing scene starter (a scene made for someone in particular) — distinct from /lilith-studio/scene/new, which opens an editorial authoring scene. Server shell wraps the client AtelierNewSceneForm, which POSTs to /v1/atelier/scenes.

Entry points#

  • Atelier index (/atelier) — "new draft" CTA (verify the exact link)
  • /atelier/camera-obscura — "Start a new scene" primary button
  • AtelierNewSceneForm "Back" buttons return to /atelier
  • Direct URL / bookmark — yes (signed-in)

Layout regions#

page.tsx (sync server component) renders LWebShell with LCustomerNav active="library". Max width 640, centered.

  • Header: Lilith customer nav (Library tab active)
  • Masthead (LMasthead): left "Atelier · scenes", right "new", kicker "A new draft", title "Start a scene."
  • Intro block (italic serif, muted): LEyebrow "How this opens" + "Name the working title. If the scene is for someone in particular, name them too — Atelier will route the share invite when the scene is ready."
  • AtelierNewSceneForm (client; below) — the bordered form card

States#

States below belong to the client AtelierNewSceneForm (apps/oshun/web/src/app/atelier/new/AtelierNewSceneForm.tsx).

  • Idle (default) — empty form card: Working title input, Recipient input, "Back" + "Start the scene" buttons
  • Pendingpending=true: submit shows "Creating…", opacity 0.6, cursor progress, button disabled
  • Acceptedaccepted set: form replaced by a role="status" aria-live="polite" card — LEyebrow "Drafting", "<title>" — open in the scene composer., mono sceneId, and two buttons (Back to Atelier / Open the composer)
  • Error (recoverable) — non-2xx: role="alert" card shows BFF body.message, falling back to "Atelier could not create the scene ()."; inputs retain values for retry
  • Offline — fetch throws → alert "Atelier is offline. Try again in a moment."; no write queued; Playwright drives Chromium offline and reads the live BFF list afterward to prove the offline title was not persisted.
  • Validation blocked — title required, minLength={4}, maxLength={140}; browser blocks submit until satisfied
  • Standalone PWAdata-responsive-page wrapper; 390 px standalone launch verifies no horizontal overflow and 44 px form/link controls
  • Gated — sessionless /atelier/new redirects to /welcome?redirect=%2Fatelier%2Fnew; no role gate beyond signed-in

Interactions#

Form fields#

  • Working title (<input id="scene-title">, label "Working title")
    • Function: controlled title state
    • Constraints: required, minLength={4}, maxLength={140}
    • Placeholder: "e.g. For J · 14 May · kitchen, walk, breakfast"
    • Keyboard: first field; tab order 1
    • Screen reader: <label htmlFor> ties caption to input
  • Recipient (optional) (<input id="scene-recipient">, label "Recipient (optional)")
    • Function: controlled recipient state; sent as recipient in the POST body
    • Constraints: maxLength={140}, not required
    • Placeholder: "A name or initial, if this is for someone."

Buttons#

  • "Back" (LBtn kind="ghost" size="lg", href="/atelier")
    • Function: navigates to /atelier without submitting
  • "Start the scene" / "Creating…" (<button type="submit">)
    • Function: submit() POSTs { title, recipient } to /v1/atelier/scenes; on response.ok sets accepted and clears both inputs
    • Label: "Start the scene" idle → "Creating…" while pending
    • Keyboard: Enter in any field submits the form
    • Disabled when: pending (does NOT disable on empty — relies on HTML required/minLength validation)
    • Touch target: height 52, pill radius — meets ≥ 44×44 px
    • Offline behavior: caught → error alert; no queue
    • Telemetry: none coded

Accepted-state buttons#

  • "Back to Atelier" (LBtn kind="ghost", href="/atelier")
  • "Open the composer" (LBtn kind="primary", href="/atelier?sceneId=<accepted.sceneId>")
    • Function: opens the composer at /atelier with the new scene id as a query param (URL-encoded)

Data & contracts#

  • Reads: none (page is static; form holds local state)
  • Writes: POST /v1/atelier/scenes via fetch(buildOshunBffUrl(...)), credentials: 'include', content-type: application/json, CSRF headers from csrfHeaders(); body { title, recipient }. Success → AcceptedResponse { sceneId, title, recipient: string | null }; failure → RejectedResponse { code, message }
  • Realtime: none
  • Caching: none — client fetch is not cached
  • Auth/role check: proxy enforces signed-in; the BFF authenticates the POST via the forwarded session cookie + CSRF token

Cross-references#

E2E coverage#

Interaction / state Coverage
Idle form + Back link covered: signed-in route load, field labels, masthead copy, Back href
Real browser scene creation covered: CSRF/auth headers, POST body, 202 response, accepted status, BFF list read-back
Composer handoff covered: accepted "Open composer" link lands on /atelier?sceneId=<id> and renders the owner-scoped active draft
Pending submit semantics covered: first POST held, submit disabled, Creating label, progress cursor
Server validation covered: whitespace title reaches BFF 422, alert uses BFF message, inputs retained
Native title validation covered: empty and 3-character title block POST before BFF
Offline failure covered: Chromium offline submit, offline alert, fields retained, BFF list unchanged
Mobile standalone containment covered at 390 px: standalone launch, no horizontal overflow, 44 px controls
Signed-in gate covered: anonymous route load redirects to /welcome?redirect=%2Fatelier%2Fnew

Open questions / known gaps#

  • The composer destination /atelier?sceneId=<id>/atelier now reads sceneId, resolves the member's real BFF scene list, and renders the active draft title/id/state when it belongs to the signed-in member; unknown or unowned IDs show an unavailable band. Covered by atelier-new-scene.spec.ts.
  • Recipient is collected but the intro promises Atelier "will route the share invite when the scene is ready" — confirm where that invite is sent and add/read a BFF recipient projection (GET /v1/atelier/scenes does not currently return recipient).
  • Submit is not disabled on empty input (relies on native validation) — verify the validation message UX in the running app
  • No telemetry events fired on submit/accept — confirm whether atelier scene creation should emit an analytics event