---
path: /atelier/new
surface: customer
domain: atelier
auth: signed-in
source: apps/oshun/web/src/app/atelier/new/page.tsx
status: walked
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'
---

# Atelier · Start a new scene

## 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`](../10-lilith/lilith-studio-scene-new.md), 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`).

- [x] **Idle (default)** — empty form card: Working title input, Recipient
      input, "Back" + "Start the scene" buttons
- [x] **Pending** — `pending=true`: submit shows "Creating…", `opacity 0.6`,
      cursor `progress`, button disabled
- [x] **Accepted** — `accepted` 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)
- [x] **Error (recoverable)** — non-2xx: `role="alert"` card shows BFF
      `body.message`, falling back to "Atelier could not create the scene
      (<status>)."; inputs retain values for retry
- [x] **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.
- [x] **Validation blocked** — title `required`, `minLength={4}`,
      `maxLength={140}`; browser blocks submit until satisfied
- [x] **Standalone PWA** — `data-responsive-page` wrapper; 390 px standalone
      launch verifies no horizontal overflow and 44 px form/link controls
- [x] **Gated** — sessionless `/atelier/new` redirects to
      `/welcome?redirect=%2Fatelier%2Fnew`; no role gate beyond signed-in

## Interactions

### Form fields

- [x] **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
- [x] **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

- [x] **"Back"** (`LBtn kind="ghost" size="lg"`, `href="/atelier"`)
  - Function: navigates to `/atelier` without submitting
- [x] **"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

- [x] **"Back to Atelier"** (`LBtn kind="ghost"`, `href="/atelier"`)
- [x] **"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

- Atelier index: [`atelier.md`](./atelier.md)
- Render queue: [`atelier-camera-obscura.md`](./atelier-camera-obscura.md)
- Editorial twin (studio scene starter):
  [`../10-lilith/lilith-studio-scene-new.md`](../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`](../../journeys/atelier-image-to-library.md)
- Feature spec: [`V1/features.md`](../../../V1/features.md#atelier)
- Architecture: [`V1/ARCHITECTURE.md`](../../../V1/ARCHITECTURE.md#atelier)
- 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

- [x] 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
