---
path: /atelier/image
surface: customer
domain: atelier
auth: signed-in
source: apps/oshun/web/src/app/atelier/image/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 (image room row-cell anchors, save/cast states, and library landing covered), against commit HEAD'
  '2026-06-26 by Codex (aspect/style controls, seed lock, compare panel, live step rail, and 390 px stacking covered), against commit HEAD'
---

# Atelier · X · Camera Obscura

## Purpose

Image atelier — "an image, drawn from a line." Five-step workflow from a written
prompt to a kept plate (contact sheet of twelve specimens → refine chosen
plate). Server-rendered by `apps/oshun/web/src/app/atelier/image/page.tsx` via
`getAtelierImage()` → `<AtelierImage data>`.

## Entry points

- **Atelier index** (`/atelier`) — first room card
- **Lilith index** (`/lilith`) — section VIII card 42 "X · Camera Obscura"
- **Direct URL / bookmark** — yes (signed-in)

## Layout regions

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

The route root exposes `data-atelier-image-page` with
`data-atelier-image-source-count="3"` and
`data-atelier-image-specimen-count="12"`. The auto-fit room body exposes
`data-atelier-image-layout`; the plate head, prompt card, contact sheet, plate
detail, provenance rows, and save/cast action rows now carry stable
`data-atelier-*` anchors used by the Playwright journey.

- **Plate head** (`LAPlateHead no="X"`):
  - kicker: "The atelier · camera obscura"
  - title: `An image, drawn from a <em>line</em>.`
  - right chips: "Specimen 047", "Saved to notebook", "Plate finalised" (accent)
- **Auto-fit room body** (rail / composer / plate detail, stacked on narrow
  viewports):
  - **Left rail** (`AtelierStepRail`, current=3 initially): five steps — "A
    line, written", "Reference, gathered", "Twelve specimens", "Plate kept ·
    refined", "Final · saved to notebook". The live rail exposes
    `data-atelier-step-rail`, `data-atelier-step-current`,
    `data-atelier-step-count="5"`, and each row exposes `data-atelier-step=<n>`
    with `data-atelier-step-state="done|current|pending"`.
  - **Center**:
    - **Prompt card** (`The line · prompt`): editable textarea
      (`data-atelier-prompt-input`) seeded with "A monk, seated on a stone
      bench, lit only by a high north window…" plus a
      `data-atelier-prompt-preview` line and chips Aspect, Plate cream, Steps
      38, Seed 0x4f2a locked/unlocked, Style, Re-cast (accent). Anchors:
      `data-atelier-prompt-card`, `data-atelier-prompt-chips`, and per-chip
      `data-atelier-prompt-chip="aspect|plate|steps|seed|style|recast"`. Aspect
      and style option buttons expose `data-atelier-option="aspect|style"` plus
      pressed/active state.
    - **Contact sheet** (`Contact sheet · twelve`): 4×3 grid of 12 selectable
      specimen buttons (status `kept` for `p.07`, `selected` for the current
      non-kept choice, `draft` for the rest); each contains a procedural SVG
      illustration. The sheet exposes `data-atelier-contact-sheet-count="12"`
      and each tile exposes `data-atelier-specimen`,
      `data-atelier-specimen-status`, and `data-atelier-specimen-kept`; the
      chosen tile also exposes `data-atelier-specimen-selected="true"`.
    - **Action row**: "Cast twelve more", "Lock seed · vary prompt", "Compare
      side-by-side"; Compare opens an inline kept-vs-active panel with
      `data-atelier-compare-panel`.
  - **Right**:
    - **Plate detail** (`Plate · p.07` by default): 4:5 SVG of the selected
      plate + field rows (Engine, Aspect 4:5, Steps 38, Guidance 6.4, Seed
      0x4f2a · locked, Style "Cormorant warmth", Sources "3 · cited")
    - **`LRule`**
    - **Provenance · 3 sources**: list from `data.sources` (Vermeer · light
      study, Hammershøi · interiors, Lilith · plate library). The lane exposes
      `data-atelier-provenance-count="3"` and every source row exposes source
      name and kind cells.
    - **Action stack**: primary "Save plate to notebook", ghost "Refine · vary
      slightly"

## States

- [ ] **Loading** — server component awaits `getAtelierImage()`; verify
      `loading.tsx`
- [x] **Populated** — fixture provides three provenance sources; covered by
      exact source-count and provenance row-cell assertions.
- [x] **Kept plate selected** — `p.07` highlighted; other tiles `draft`; covered
      by `data-atelier-specimen-status="kept"` and
      `data-atelier-selected-plate="p.07"` assertions.
- [x] **Alternate specimen selected** — clicking a draft tile sets
      `data-atelier-specimen-selected="true"` and updates
      `data-atelier-selected-plate` on the right-side plate detail; the rail
      advances to step 4.
- [x] **Final saved** — clicking "Save plate to notebook" advances the rail to
      step 5 and shows the saved state.
- [ ] **Empty sources** — verify if `data.sources` empty (no fallback in code)
- [ ] **Error** — verify error boundary
- [ ] **Offline** — entire view is static markup; renders offline
- [ ] **Reduced motion** — no animations declared

## Interactions

### Prompt card chips and controls

- [x] **Aspect controls** — option buttons update `data-atelier-aspect`, the
      Aspect chip, the right-side Aspect field, and the cast/refine payload.
- [x] **Style controls** — option buttons update `data-atelier-style`, the Style
      chip, the right-side Style field, and the cast/refine payload.
- [x] **Seed chip / lock state** — "Lock seed · vary prompt" toggles
      `data-atelier-seed-state`, the Seed chip, the Seed field, and the
      cast/refine payload.
- [x] **Plate / Steps chips** (`LChip`) — read-only.
- [ ] **"Re-cast" chip** (`LChip accent`) — verify whether interactive
      (component prop `accent` does not necessarily imply click)

### Contact sheet specimens

- [x] **Specimen tile** (`button`, `data-atelier-specimen="p.01"` – `p.12`) —
      all 12 tiles are rendered with stable specimen/status/kept/selected
      attributes; tapping a draft specimen selects it and updates the right
      detail pane.
- [x] **"Kept" tile (p.07)** — accent visual; selected state asserted
- [x] **"Draft" tiles** — muted treatment

### Center actions

- [x] **"Cast twelve more" button** (`AtelierPlateActions mode="cast"`)
  - Function: POSTs `/v1/atelier/cast` with the browser-entered prompt, selected
    specimen id, aspect, style, seed-lock state, and `intent: cast`; in the
    local real-dev stack without Stability credentials it fails closed with
    `data-atelier-cast-result="not-configured"` and
    `data-atelier-cast-state="not-configured"`.
- [x] **"Lock seed · vary prompt" button** — toggles seed locked/unlocked state.
- [x] **"Compare side-by-side" button** — opens/closes an inline compare panel
      with kept reference p.07 and the active selected plate.

### Right column actions

- [x] **"Save plate to notebook" button** (`AtelierPlateActions mode="save"`)
  - Function: writes `atelier-plate-kept` to `oshun.web.library.saved-items.v1`
    as `kind: generated-artifact`, then the Playwright journey opens
    `/library?kind=generated-artifact` and asserts
    `data-library-item="atelier-plate-kept"` with the "Open in Atelier" action.
- [x] **"Refine · vary slightly" button** (`AtelierPlateActions mode="refine"`)
  - Function: POSTs `/v1/atelier/cast` with the browser-entered prompt, selected
    specimen id, aspect, style, seed-lock state, and `intent: refine`; in the
    local real-dev stack it fails closed with
    `data-atelier-refine-result="not-configured"`.

## Data & contracts

- **Reads**: `getAtelierImage()` from
  `apps/oshun/web/src/lib/lilith-data/atelier-depth.ts` — returns
  `AtelierImageData` with `sources` (label + kind tuples; planned BFF at
  `/atelier/plates/image`)
- **Writes**:
  - `Save plate to notebook` calls `saveOshunWebLibraryItem(...)` through
    `AtelierPlateActions`, creating a local web-library generated-artifact row
    with `id: atelier-plate-kept`, `domain: nisaba`,
    `resumePath: /atelier/image`, and `resumeLabel: Open in Atelier`.
  - `Cast twelve more` and `Refine · vary slightly` call
    `api.post('/v1/atelier/cast', ...)` with prompt/specimen/intent plus aspect,
    style, and seed-lock state; the real generation seam fails closed with typed
    503 provider-not-configured when the Stability key is absent.
- **Realtime**: none
- **Caching**: server fetch
- **Auth/role check**: middleware enforces signed-in

## Cross-references

- Atelier index: [`atelier.md`](./atelier.md)
- Sibling rooms: [`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`
  (`AtelierImage`)
- Data: `apps/oshun/web/src/lib/lilith-data/atelier-depth.ts`

## Open questions / known gaps

- [x] Confirm whether the action buttons wire to handlers — Cast, Refine, and
      Save are wired through `AtelierPlateActions`; Lock seed and Compare are
      wired in the composer.
- [x] Document touch behaviour for specimen tiles — draft tiles are buttons;
      tap/click selects the specimen and updates the right detail pane.
- [x] Confirm responsive layout — `atelier-image-to-library` verifies the image
      room at 390 px with no horizontal overflow and stacked rail → composer →
      plate detail.
