V1 Web PWA · Surface walkthrough

Lilith Studio · New scene (authoring)

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

walked
8sections4 minread

On this page

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/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).

  • Idle (default) — form with Working title, Kind radiogroup (default sit), Path id, Session index (default 1)
  • Kind selected — chosen cell gets accent border, L.bg background, italic label; role="radio" aria-checked="true" with roving tabIndex
  • Pending — submit shows "Creating…", disabled, cursor progress
  • Acceptedrole="status" aria-live="polite" card: LEyebrow "Authoring", "<title>" — held in authoring., mono sceneId + kind (and · path … / · session … when present), two buttons
  • Error (recoverable) — non-2xx → role="alert" shows BFF body.message or "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 exposes min={1} max={99} and normalizes empty or 0 input back to 1
  • Gated (access denied) — non-editorial scope → layout renders LilithStudioAccessDenied instead 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"
  • Kind radiogroup (role="radiogroup" aria-label="Kind") — three <button type="button" role="radio"> cells from KIND_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 carries aria-checked="true" and tabIndex={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") — controlled pathId
  • Session index (<input id="scene-index" type="number">, min={1} max={99}) — controlled sessionIndex; non-numeric input and 0 fall back to 1

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 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#

  • "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/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()deriveCustomerRoleFromScopeshasLilithStudioAccess(role); mirrors the BFF gate (admin:* or studio:editorial scope). Denied → LilithStudioAccessDenied

Cross-references#

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.