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 buttonAtelierNewSceneForm"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
- Pending —
pending=true: submit shows "Creating…",opacity 0.6, cursorprogress, button disabled - Accepted —
acceptedset: form replaced by arole="status" aria-live="polite"card —LEyebrow"Drafting","<title>" — open in the scene composer., monosceneId, and two buttons (Back to Atelier / Open the composer) - Error (recoverable) — non-2xx:
role="alert"card shows BFFbody.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 PWA —
data-responsive-pagewrapper; 390 px standalone launch verifies no horizontal overflow and 44 px form/link controls - Gated — sessionless
/atelier/newredirects 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
titlestate - 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
- Function: controlled
- Recipient (optional) (
<input id="scene-recipient">, label "Recipient (optional)")- Function: controlled
recipientstate; sent asrecipientin the POST body - Constraints:
maxLength={140}, not required - Placeholder: "A name or initial, if this is for someone."
- Function: controlled
Buttons#
- "Back" (
LBtn kind="ghost" size="lg",href="/atelier")- Function: navigates to
/atelierwithout submitting
- Function: navigates to
- "Start the scene" / "Creating…" (
<button type="submit">)- Function:
submit()POSTs{ title, recipient }to/v1/atelier/scenes; onresponse.oksetsacceptedand 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 HTMLrequired/minLengthvalidation) - Touch target: height 52, pill radius — meets ≥ 44×44 px
- Offline behavior: caught → error alert; no queue
- Telemetry: none coded
- Function:
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
/atelierwith the new scene id as a query param (URL-encoded)
- Function: opens the composer at
Data & contracts#
- Reads: none (page is static; form holds local state)
- Writes:
POST /v1/atelier/scenesviafetch(buildOshunBffUrl(...)),credentials: 'include',content-type: application/json, CSRF headers fromcsrfHeaders(); 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#
- Atelier index:
atelier.md - Render queue:
atelier-camera-obscura.md - Editorial twin (studio scene starter):
../10-lilith/lilith-studio-scene-new.md - Component source:
apps/oshun/web/src/app/atelier/new/AtelierNewSceneForm.tsx - Journey:
../../journeys/atelier-image-to-library.md - Feature spec:
V1/features.md - Architecture:
V1/ARCHITECTURE.md - E2E:
apps/oshun/web/e2e/atelier-new-scene.spec.ts
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>—/ateliernow readssceneId, 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 byatelier-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/scenesdoes not currently returnrecipient). - 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