Context. surface customer · domain lilith · route /lilith-studio/scene/new · auth signed-in + role:v1_editorial_producer (default) · source apps/oshun/web/src/app/lilith-studio/scene/new/page.tsx
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
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, 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/tararead-only mirror — "New scene" primary button/lilith-studio/scenes— "New scene" link from the scenes editor headerStudioNewSceneForm"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
ShellLayoutheader (from the layout) plus this page'sLCustomerNav(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).
- Idle (default) — form with Working title, Kind radiogroup (default
sit), Path id, Session index (default1) - Kind selected — chosen cell gets accent border,
L.bgbackground, italic label;role="radio" aria-checked="true"with rovingtabIndex - Pending — submit shows "Creating…", disabled, cursor
progress - Accepted —
role="status" aria-live="polite"card:LEyebrow"Authoring","<title>" — held in authoring., monosceneId+kind(and· path …/· session …when present), two buttons - Error (recoverable) — non-2xx →
role="alert"shows BFFbody.messageor "Studio could not create the scene ()." - Offline — fetch throws → "Studio is offline. Try again in a moment."
- Validation blocked — title
required,minLength={4},maxLength={140}; session index exposesmin={1} max={99}and normalizes empty or0input back to1 - Gated (access denied) — non-editorial scope → layout renders
LilithStudioAccessDeniedinstead of this page (admin:* or studio:editorial scope grants access) - Standalone PWA — safe-area + narrow-width rendering verified with no horizontal overflow and 44 px controls
Interactions#
Form fields#
- Working title (
<input id="scene-title">)- Constraints:
required,minLength={4},maxLength={140} - Placeholder: "e.g. On returning — the first breath after a long pause"
- Constraints:
- Kind radiogroup (
role="radiogroup" aria-label="Kind") — three<button type="button" role="radio">cells fromKIND_OPTIONS:- Sit — sub "silence + guided voice" (default selected)
- Ritual — sub "breath, mantra, posture"
- Reading — sub "passage + slow voice"
- Function: each click or keyboard selection sets
kind; selected cell carriesaria-checked="true"andtabIndex={0} - Keyboard: ArrowLeft/ArrowUp move backward, ArrowRight/ArrowDown move forward, Home selects Sit, End selects Reading
- Path id (optional) (
<input id="scene-path">,maxLength={80}, placeholder "e.g. sh-iv") — controlledpathId - Session index (
<input id="scene-index" type="number">,min={1} max={99}) — controlledsessionIndex; non-numeric input and0fall back to1
Buttons#
- "Back" (
LBtn kind="ghost" size="lg",href="/lilith-studio/scenes") - "Open authoring" / "Creating…" (
<button type="submit">)- Function: POSTs
{ title, pathId, sessionIndex, kind }to/v1/lilith-studio/scenes; on success setsacceptedand clearstitle(note: pathId / kind / index are NOT reset) - Disabled when:
pending - Keyboard: Enter submits
- Touch target: height 52 pill — meets ≥ 44×44 px
- Function: POSTs
Accepted-state buttons#
- "All studio scenes" (
LBtn kind="ghost",href="/lilith-studio/scenes") - "Open in authoring" (
LBtn kind="primary",href="/lilith-studio/scenes?id=<accepted.sceneId>")
Data & contracts#
- Reads: none (form-only)
- Writes:
POST /v1/lilith-studio/scenesviafetch,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.tsxreadsreadCustomerSessionClaims()→deriveCustomerRoleFromScopes→hasLilithStudioAccess(role); mirrors the BFF gate (admin:*orstudio:editorialscope). Denied →LilithStudioAccessDenied
Cross-references#
- Studio hub:
lilith-studio.md - Scenes index / authoring:
lilith-studio-scenes.md - Tara read-only mirror:
lilith-studio-tara.md - User-facing twin:
../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 - Feature spec:
V1/features.md - Architecture:
V1/ARCHITECTURE.md
Open questions / known gaps#
- Double shell: the layout mounts
ShellLayoutand this page mountsLWebShelltoo. Confirm whether the page should drop itsLWebShell(the lilith-studio refactor note inlayout.tsxsays pages "now return their inner authoring component directly") - Nav uses
active="explore"while the layout usesactive="studio"— two competing active-tab signals. Confirm the intended highlight - Accepted-state clears only
title;kind/pathId/sessionIndexpersist across a second create. Confirm whether that is intended -
/lilith-studio/scenes?id=<sceneId>— confirm the scenes editor reads theidquery param and opens that draft. Current E2E verifies the accepted link handoff and editor render; it does not prove draft-specific loading by id.