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

# The Atelier · index

## 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
- [x] **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)
- [ ] **Error** — `getAtelierIndex()` 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
- [x] **Mobile / narrow** — auto-fit grid stacks without horizontal overflow;
      covered by `atelier-image-to-library` at 390 px.
- [x] **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`.
- [x] **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>`)

- [x] **"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
- [x] **"Foundry of Voices" card** — anchor → `/atelier/audio`
- [x] **"Cinematheque" card** — anchor → `/atelier/video`
- [x] **"Forms" card** — anchor → `/atelier/forms`
- [x] **"Stage of Motion" card** — anchor → `/atelier/motion`

### House rules grid

- [x] **Each rule tile** — read-only (kicker `House rule · I/II/III`, title,
      description)

### Active draft band

- [x] **"Clear"** — anchor to `/atelier`, removes the query selection
- [x] **"Start another"** — anchor to `/atelier/new`, starts the new-scene flow
- [x] **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

- Lilith index: [`../10-lilith/lilith.md`](../10-lilith/lilith.md) — section
  VIII lists all five rooms
- Five rooms (sibling files in this folder):
  - [`atelier-image.md`](./atelier-image.md)
  - [`atelier-audio.md`](./atelier-audio.md)
  - [`atelier-video.md`](./atelier-video.md)
  - [`atelier-forms.md`](./atelier-forms.md)
  - [`atelier-motion.md`](./atelier-motion.md)
- Component source: `apps/oshun/web/src/components/lilith/atelier.tsx`
  (`AtelierIndex`)
- Data: `apps/oshun/web/src/lib/lilith-data/atelier-depth.ts`
- E2E: `apps/oshun/web/e2e/atelier-image-to-library.spec.ts`
- E2E: `apps/oshun/web/e2e/atelier-new-scene.spec.ts`

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

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