Sets: capture and publish
A set is a directory of files with a share URL. vitrinka board is how one gets made, filled and sent — each shot uploads once, and whatever did not land while offline goes with the next push.
Start a set
Creates a .vitrinka set config in the repository so pushes know where they are going.
vitrinka board initCapture a shot
Captures a screenshot straight into the set and uploads it in the background — only the new file, which joins the set's board within seconds. It drives an iOS simulator, an Android emulator or macOS, or adopts an image you already have. A web shot is always adopted: take it with your own browser tooling — the browser MCP, a Playwright script — and hand the file over with --file. capture safari --url <page> drives Mobile Safari on the iOS Simulator through Appium instead, in one of six --states (default, keyboard, scrolled, scroll-end, landscape, overscroll) — the keyboard really opens, the page really scrolls — and the measured safe-area insets travel beside the shot.
vitrinka board capture web \
--file checkout.png \
--viewport 1440x900 \
--route /checkout \
--note "discount applies twice" \
--url https://localhost:3000/checkout--note and --route are required: a shot with neither is a picture nobody can place.
Capture flags
| Flag | What it does |
|---|---|
--route | Required. The app route the shot was taken on. |
--note | Required. The caption. |
--title | Human title — also names the file. |
--label | A stable shot label. Journey wiring targets it. |
--next | Journey branch: the target shot's label. Repeatable. |
--action | What was done on this screen; after --next, that branch's wire label. |
--target | {x,y,w,h} JSON bbox of the clicked element — follows a --next. |
--open | A deep link to open before capturing — simulators, emulators and macOS; never with --file. |
--settle | Seconds to wait before capturing. Default 4 with --open, otherwise 0. |
--viewport | WxH or WxH@scale, e.g. 393x852@3. For a web shot, the page's CSS viewport. |
--hidpi | Web: hold the file to 2× when --viewport is known. Default: the repository's capture.hidpi, on; --hidpi=false excuses this one shot — below. |
--device | Device name. |
--udid | iOS simulator UDID. |
--region | macOS capture region, x,y,w,h. |
--file | Adopt an existing image instead of capturing one. |
--hq | Full-fidelity asset: no lossy re-encode, no resize. |
--boxes | Runner text map in source-image pixels (default: the adopted image's .boxes.json) — review toolkit. |
--src | The implementing source file. Repeatable or comma-separated. |
--state | Notable app or auth state. |
--url | Provenance URL — beats --open in the manifest. |
--root | Capture root. Default .vitrinka/screenshots. |
Web captures at 2×
Web shots are 2× device scale — Playwright's deviceScaleFactor: 2 — so a board read on a retina screen stays sharp. With --viewport WxH given, capture web refuses a file narrower than twice that width with quality_gate, and the reply names the recapture and both ways out; without --viewport the scale cannot be judged and the shot passes. The rule belongs to the repository: capture.hidpi in the committed vitrinka.config.json, on unless set. Simulator, emulator and macOS captures are already native resolution and are never checked or downscaled.
vitrinka config hidpi # show the repo's setting
vitrinka config hidpi off # let 1× web files through for everyone in this repoPush the set
Syncs the capture root with the set, file by file: only what the deployment does not already hold is uploaded. A push only adds — shots the set holds but the folder lacks (a fresh worktree, a cleaned folder) are kept, and the push says how many. Idempotent — re-running sends nothing new. A background upload that failed, while offline or refused, leaves its diagnosis and fix in the capture root; the next capture prints it once, and the next push sends only what is missing. Artifact sets (a doc.json) still go as one archive, and so does every push to a deployment without the per-file door — which replaces the whole set with the folder.
To delete shots, remove them locally and push with --prune: the set then mirrors the folder. When the prune would delete shots that board cards still show, the deployment refuses it and names board pull; --confirm deletes them anyway.
A push to an artifact someone deleted is refused (artifact_deleted) — deleting it was a decision, and re-running a push must not undo it. The refusal links the artifact's page, where Restore brings it back; or push with --revive to publish it again on the same page. After its 30 days in the trash it is purged, and the key is free again.
vitrinka board push --title "Checkout — pass 3"| Flag | What it does |
|---|---|
--root | Capture root. Default .vitrinka/screenshots. |
--title | Human title, re-sent on every push. |
--source | Cross-link a source set: project/branchSlug/key. |
--no-fix | Skip the doc.json preflight autofix (JSON damage; a legacy {meta, blocks} file rewritten as the doc element). |
--no-render | Skip the server's post-publish smoke render and its render.png download. |
--no-design | Skip sending the repo's DESIGN.md with an artifact push. |
--prune | Mirror the folder: delete the set's files it no longer holds. |
--revive | Publish onto an artifact that was deleted: its page comes back, same address, instead of the push being refused. |
Pull the set
A capture root lives in one worktree; the set outlives it. pull restores the set's files into the root, so a fresh worktree continues the set instead of starting from an empty folder. Files already in the root are never overwritten, and manifest.json is merged — the set's shots first, then the folder's own. Without a descriptor in the root, --project, --branch and --key name the set, as board init does.
vitrinka board pull --branch feature/checkout --key 2026-09-26Name and frame it
meta sets the journey header — kicker, title, accent, intro and chips. rename gives the set a stable custom name so a readable URL works alongside the numeric one.
vitrinka board meta --title "Checkout" --kicker "pass 3"
vitrinka board rename myapp/main/7 payout-flowDocuments and pages
A set can carry a document instead of a gallery: a doc.json of elements at the set root, with sibling pages under pages/<slug>.json. Push handles the whole tree in one request:
- Preflight —
doc.jsonis validated against this deployment's element vocabulary before upload; JSON damage is autofixed and a legacy{meta, blocks}file is rewritten as the doc element unless you pass--no-fix, and a real contract error stops the push with the offending element named. - Pages become cards — each
pages/<slug>.jsonis minted as a sibling document card,page:references insiderefelements are rewritten to card ids, and a re-push keeps the ids, so links stay stable. - Smoke render — an artifact push answers with a
render.pngof the result, so an agent can look at what it shipped without opening a browser.
The vocabulary itself — every element kind with its payload — is the elements reference; how documents nest and lay out is composing boards.
Artifacts
An artifact is a self-contained interactive page rather than a gallery. init scaffolds one with its set config; create builds a report from an existing screenshot set.
vitrinka board artifact init --slug checkout-report
vitrinka board artifact create --slug checkout-report --from ./captures