V1 Web PWA · Surface walkthrough

The Atelier · index

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

walked
9sections5 minread1table

On this page

Context. surface customer · domain atelier · route /atelier · auth signed-in · source apps/oshun/web/src/app/atelier/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' '2026-06-25 by Codex (room-grid and house-rule row-cell anchors covered), against commit HEAD' '2026-06-26 by Codex (auto-fit room grid and 390 px no-overflow coverage), against commit HEAD

Purpose#

The Atelier index — "a small workshop attached to the reading desk." Five rooms of making (Image / Audio / Video / Forms / Motion) presented as a single broadsheet card row, plus three house rules that govern every generative surface (only from a line; provenance shown; slow by default). Server-rendered by apps/oshun/web/src/app/atelier/page.tsx via getAtelierIndex()<AtelierIndex data>. When opened with ?sceneId=<id>, the route also reads the signed-in member's real Atelier scenes from /v1/atelier/scenes and renders an active-draft handoff only if that scene belongs to the current account.

Entry points#

  • Lilith index (/lilith) — section VIII card 41 "Atelier · index"
  • Customer nav (LCustomerNav active="explore") — the Atelier shell is reachable from the explore tab in the Lilith design system
  • Atelier scene starter (/atelier/new) — accepted state "Open the composer" link lands on /atelier?sceneId=<new scene id>
  • Direct URL / bookmark — yes (signed-in)

Layout regions#

AtelierIndex (apps/oshun/web/src/components/lilith/atelier.tsx, line 1985) renders inside LWebShell with LCustomerNav active="explore".

  • Masthead (LMasthead):
    • left: "The atelier · est. 2026"
    • right: "Tuesday, the sixth of May"
    • kicker: "Five rooms of making"
    • title: The <em>atelier</em>.
  • Lede: centred italic copy about the workshop
  • Active draft band (conditional) — if sceneId is present, a full-width top/bottom-rule band appears below the lede:
    • found: data-atelier-active-scene="found", "Active draft", title, scene id, state, "Clear" (/atelier) and "Start another" (/atelier/new)
    • missing/unowned: data-atelier-active-scene="missing", "Draft unavailable.", explanatory copy, and "Clear"
  • LDoubleRule
  • Rooms grid (auto-fit card shelf, 1px border) — one card per room from data.rooms:
    • X · Camera Obscura · image · href /atelier/image
    • XI · Foundry of Voices · audio · href /atelier/audio
    • XII · Cinematheque · video · href /atelier/video
    • XIII · Forms · three D · href /atelier/forms
    • XIV · Stage of Motion · animated 3 D · href /atelier/motion
  • House rules grid (3 columns): three rules from data.houseRules — "Only from a line.", "Provenance shown.", "Slow by default."

The route root exposes data-atelier-index-page with data-atelier-index-room-count="5" and data-atelier-index-house-rule-count="3". The room grid exposes data-atelier-room-grid / data-atelier-room-grid-count; each room card exposes data-atelier-room-card=<Roman room no>, data-atelier-room-href, and row-cell children for number, name, line, meta, and engine. The house-rule grid exposes data-atelier-house-rules, data-atelier-house-rule-count, and each tile exposes data-atelier-house-rule=<1..3> with title/description cells.

States#

  • Loading — server component awaits getAtelierIndex(); verify whether a loading.tsx skeleton is present
  • Populated — fixture returns 5 rooms and 3 house rules; covered by atelier-image-to-library.spec.ts with exact count attributes and cell-by-cell room / house-rule assertions.
  • Empty rooms / rules — fixture always populated; verify behaviour when arrays are empty (currently no fallback copy)
  • ErrorgetAtelierIndex() synchronously returns fixture; verify error.tsx/global-error.tsx boundary
  • Offline — fully static markup; renders offline from SW cache
  • Standalone PWA — verify safe-area insets at top
  • Reduced motion — no animations declared
  • Mobile / narrow — auto-fit grid stacks without horizontal overflow; covered by atelier-image-to-library at 390 px.
  • Active draft found/atelier?sceneId=<owned id> renders the real owner-scoped title/state from /v1/atelier/scenes and keeps the five-room grid present; covered by atelier-new-scene.spec.ts.
  • Active draft missing / unowned/atelier?sceneId=<unknown id> renders an honest unavailable band and no draft title; covered by atelier-new-scene.spec.ts.

Interactions#

Rooms grid (each card is <a href>)#

  • "Camera Obscura" card — anchor → /atelier/image
    • Function: navigates to the image atelier
    • Keyboard: Tab focuses; Enter activates
    • Screen reader: announces card heading, sub, and engine
  • "Foundry of Voices" card — anchor → /atelier/audio
  • "Cinematheque" card — anchor → /atelier/video
  • "Forms" card — anchor → /atelier/forms
  • "Stage of Motion" card — anchor → /atelier/motion

House rules grid#

  • Each rule tile — read-only (kicker House rule · I/II/III, title, description)

Active draft band#

  • "Clear" — anchor to /atelier, removes the query selection
  • "Start another" — anchor to /atelier/new, starts the new-scene flow
  • Unavailable "Clear" — anchor to /atelier; no retry or fabricated draft controls appear when the scene id is not found for the member

Data & contracts#

  • Reads: getAtelierIndex() from apps/oshun/web/src/lib/lilith-data/atelier-depth.ts — returns AtelierIndexData (fixture today; BFF planned at /atelier/plates/... per the data file header). With a sceneId query param, the server route also calls bffGet('/v1/atelier/scenes'), then selects the matching owner-scoped scene id from the live BFF response. Missing/unowned/BFF-unavailable responses render the unavailable band rather than a draft.
  • Writes: none
  • Realtime: none
  • Caching: fixture index data; authenticated BFF scene reads are no-store
  • Auth/role check: middleware enforces signed-in

Cross-references#

E2E coverage#

Interaction / state Coverage
Five-room index covered: exact room count, room cell text, hrefs, and house-rule rows
Mobile room grid covered at 390 px: no horizontal overflow across index and image room
Active draft composer link covered: create scene through live BFF, click "Open the composer", render title/id/state
Missing active draft covered: signed-in /atelier?sceneId=<unknown> shows unavailable copy and no draft title
Grid remains with query covered: found and missing scene-id states keep the five-room grid rendered

Open questions / known gaps#

  • Confirm responsive behaviour — the rooms grid uses auto-fit tracks and is covered at 390 px by atelier-image-to-library.
  • The masthead's right text is a static "Tuesday, the sixth of May" — confirm whether this is intended fixture copy or expected to be replaced with a live date
  • Document any handoff state shared between rooms (AtelierMotion's "Compose · downstream" panel mentions Cinematheque sh 03 + foundry take 03 + forms cream-north — confirm whether the index reflects project state)