V1 Web PWA · Surface walkthrough

Atelier · X · Camera Obscura

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

walked
8sections6 minread

On this page

Context. surface customer · domain atelier · route /atelier/image · auth signed-in · source apps/oshun/web/src/app/atelier/image/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 (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

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
  • Populated — fixture provides three provenance sources; covered by exact source-count and provenance row-cell assertions.
  • Kept plate selectedp.07 highlighted; other tiles draft; covered by data-atelier-specimen-status="kept" and data-atelier-selected-plate="p.07" assertions.
  • 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.
  • 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#

  • Aspect controls — option buttons update data-atelier-aspect, the Aspect chip, the right-side Aspect field, and the cast/refine payload.
  • Style controls — option buttons update data-atelier-style, the Style chip, the right-side Style field, and the cast/refine payload.
  • Seed chip / lock state — "Lock seed · vary prompt" toggles data-atelier-seed-state, the Seed chip, the Seed field, and the cast/refine payload.
  • 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#

  • 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.
  • "Kept" tile (p.07) — accent visual; selected state asserted
  • "Draft" tiles — muted treatment

Center actions#

  • "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".
  • "Lock seed · vary prompt" button — toggles seed locked/unlocked state.
  • "Compare side-by-side" button — opens/closes an inline compare panel with kept reference p.07 and the active selected plate.

Right column actions#

  • "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.
  • "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#

Open questions / known gaps#

  • 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.
  • Document touch behaviour for specimen tiles — draft tiles are buttons; tap/click selects the specimen and updates the right detail pane.
  • Confirm responsive layout — atelier-image-to-library verifies the image room at 390 px with no horizontal overflow and stacked rail → composer → plate detail.