---
path: /lilith-studio/scene/new
surface: customer
domain: lilith
auth: signed-in + role:v1_editorial_producer (default)
source: apps/oshun/web/src/app/lilith-studio/scene/new/page.tsx
status: walked
last_walked:
  '2026-07-03 real-infra Playwright walk — editorial gating, direct and linked
  entry points, radio keyboard semantics, native validation, session-index
  normalization, pending lockout, live BFF 202/422 responses, auth/CSRF headers,
  accepted authoring handoff, offline fail-closed behavior, and mobile
  standalone containment verified in Chromium. Evidence:
  apps/oshun/web/e2e/lilith-studio-new-scene.spec.ts and
  WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md'
---

# Lilith Studio · New scene (authoring)

## Purpose

The editorial scene starter: pick a kind (sit / ritual / reading), name a
working title, and optionally bind it to a path id and session index. The BFF
returns an authoring-stage placeholder for the studio shell to open. Distinct
from the user-facing [`/atelier/new`](../11-atelier/atelier-new.md), which makes
a personal-offering scene. Server shell wraps the client `StudioNewSceneForm`,
which POSTs to `/v1/lilith-studio/scenes`.

## Entry points

- **Lilith Studio hub** (`/lilith-studio`) — "New scene" CTA when the active
  surface is scene authoring
- **`/lilith-studio/tara`** read-only mirror — "New scene" primary button
- **`/lilith-studio/scenes`** — "New scene" link from the scenes editor header
- **`StudioNewSceneForm` "Back" buttons** return to `/lilith-studio/scenes`
- **Direct URL / bookmark** — yes (signed-in + editorial scope)

## Layout regions

`/lilith-studio/layout.tsx` already mounts `<ShellLayout active="studio">` and
applies the editorial access gate. **This page additionally mounts its own
`LWebShell` with `LCustomerNav active="explore"`** (see Open questions — likely
a double-shell). Max width 640, centered.

- **Header**: the studio `ShellLayout` header (from the layout) plus this page's
  `LCustomerNav` (Explore tab active)
- **Masthead** (`LMasthead`): left "Lilith Studio · scenes", right "authoring",
  kicker "A scene, started", title "New _scene_."
- **Intro block** (italic serif, muted): `LEyebrow` "How this opens" + "Pick the
  kind, name the working title, and (if known) the path and session index. The
  scene opens in authoring with the editing surfaces already wired."
- **`StudioNewSceneForm`** (client; below) — the bordered form card

## States

States below belong to `StudioNewSceneForm`
(`apps/oshun/web/src/app/lilith-studio/scene/new/StudioNewSceneForm.tsx`).

- [x] **Idle (default)** — form with Working title, Kind radiogroup (default
      `sit`), Path id, Session index (default `1`)
- [x] **Kind selected** — chosen cell gets accent border, `L.bg` background,
      italic label; `role="radio" aria-checked="true"` with roving `tabIndex`
- [x] **Pending** — submit shows "Creating…", disabled, cursor `progress`
- [x] **Accepted** — `role="status" aria-live="polite"` card: `LEyebrow`
      "Authoring", `"<title>" — held in authoring.`, mono `sceneId` + `kind`
      (and `· path …` / `· session …` when present), two buttons
- [x] **Error (recoverable)** — non-2xx → `role="alert"` shows BFF
      `body.message` or "Studio could not create the scene (<status>)."
- [x] **Offline** — fetch throws → "Studio is offline. Try again in a moment."
- [x] **Validation blocked** — title `required`, `minLength={4}`,
      `maxLength={140}`; session index exposes `min={1} max={99}` and normalizes
      empty or `0` input back to `1`
- [x] **Gated (access denied)** — non-editorial scope → layout renders
      `LilithStudioAccessDenied` instead of this page (admin:\* or
      studio:editorial scope grants access)
- [x] **Standalone PWA** — safe-area + narrow-width rendering verified with no
      horizontal overflow and 44 px controls

## Interactions

### Form fields

- [x] **Working title** (`<input id="scene-title">`)
  - Constraints: `required`, `minLength={4}`, `maxLength={140}`
  - Placeholder: "e.g. On returning — the first breath after a long pause"
- [x] **Kind radiogroup** (`role="radiogroup" aria-label="Kind"`) — three
      `<button type="button" role="radio">` cells from `KIND_OPTIONS`:
  - [x] **Sit** — sub "silence + guided voice" (default selected)
  - [x] **Ritual** — sub "breath, mantra, posture"
  - [x] **Reading** — sub "passage + slow voice"
  - Function: each click or keyboard selection sets `kind`; selected cell
    carries `aria-checked="true"` and `tabIndex={0}`
  - Keyboard: ArrowLeft/ArrowUp move backward, ArrowRight/ArrowDown move
    forward, Home selects Sit, End selects Reading
- [x] **Path id (optional)** (`<input id="scene-path">`, `maxLength={80}`,
      placeholder "e.g. sh-iv") — controlled `pathId`
- [x] **Session index** (`<input id="scene-index" type="number">`,
      `min={1}     max={99}`) — controlled `sessionIndex`; non-numeric input and
      `0` fall back to `1`

### Buttons

- [x] **"Back"** (`LBtn kind="ghost" size="lg"`, `href="/lilith-studio/scenes"`)
- [x] **"Open authoring" / "Creating…"** (`<button type="submit">`)
  - Function: POSTs `{ title, pathId, sessionIndex, kind }` to
    `/v1/lilith-studio/scenes`; on success sets `accepted` and clears `title`
    (note: pathId / kind / index are NOT reset)
  - Disabled when: `pending`
  - Keyboard: Enter submits
  - Touch target: height 52 pill — meets ≥ 44×44 px

### Accepted-state buttons

- [x] **"All studio scenes"** (`LBtn kind="ghost"`,
      `href="/lilith-studio/scenes"`)
- [x] **"Open in authoring"** (`LBtn kind="primary"`,
      `href="/lilith-studio/scenes?id=<accepted.sceneId>"`)

## Data & contracts

- **Reads**: none (form-only)
- **Writes**: `POST /v1/lilith-studio/scenes` via `fetch`,
  `credentials: 'include'`, JSON + `csrfHeaders()`; body
  `{ title, pathId, sessionIndex, kind }`. Success →
  `AcceptedResponse { sceneId, title, kind, pathId: string | null, sessionIndex: number | null }`;
  failure → `{ code, message }`
- **Realtime**: none
- **Caching**: none (client fetch)
- **Auth/role check**: `/lilith-studio/layout.tsx` reads
  `readCustomerSessionClaims()` → `deriveCustomerRoleFromScopes` →
  `hasLilithStudioAccess(role)`; mirrors the BFF gate (`admin:*` or
  `studio:editorial` scope). Denied → `LilithStudioAccessDenied`

## Cross-references

- Studio hub: [`lilith-studio.md`](./lilith-studio.md)
- Scenes index / authoring:
  [`lilith-studio-scenes.md`](./lilith-studio-scenes.md)
- Tara read-only mirror: [`lilith-studio-tara.md`](./lilith-studio-tara.md)
- User-facing twin:
  [`../11-atelier/atelier-new.md`](../11-atelier/atelier-new.md)
- Component source:
  `apps/oshun/web/src/app/lilith-studio/scene/new/StudioNewSceneForm.tsx`
- Journey:
  [`../../journeys/lilith-studio-tara-scene-publish.md`](../../journeys/lilith-studio-tara-scene-publish.md)
- Feature spec:
  [`V1/features.md`](../../../V1/features.md#lilith-persona-policy)
- Architecture: [`V1/ARCHITECTURE.md`](../../../V1/ARCHITECTURE.md#lilith)

## Open questions / known gaps

- [ ] **Double shell**: the layout mounts `ShellLayout` and this page mounts
      `LWebShell` too. Confirm whether the page should drop its `LWebShell` (the
      lilith-studio refactor note in `layout.tsx` says pages "now return their
      inner authoring component directly")
- [ ] Nav uses `active="explore"` while the layout uses `active="studio"` — two
      competing active-tab signals. Confirm the intended highlight
- [ ] Accepted-state clears only `title`; `kind`/`pathId`/`sessionIndex` persist
      across a second create. Confirm whether that is intended
- [ ] `/lilith-studio/scenes?id=<sceneId>` — confirm the scenes editor reads the
      `id` query param and opens that draft. Current E2E verifies the accepted
      link handoff and editor render; it does not prove draft-specific loading
      by id.
