From bd0515db975dcda01295050e9da07ffe9502db85 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Fri, 13 Nov 2026 09:24:39 -0500 Subject: [PATCH 01/64] docs(specs): draft spec 082, scanner capture always in view (#344) Decision 1 ruled by the maintainer on 2026-11-13 (option C): no cards soon, so the live re-measure is the last gate and runs on a new corpus. Co-Authored-By: Claude Opus 5.5 --- .../spec.md | 353 ++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 354 insertions(+) create mode 100644 docs/specs/082-scanner-capture-always-in-view/spec.md diff --git a/docs/specs/082-scanner-capture-always-in-view/spec.md b/docs/specs/082-scanner-capture-always-in-view/spec.md new file mode 100644 index 00000000..0f083839 --- /dev/null +++ b/docs/specs/082-scanner-capture-always-in-view/spec.md @@ -0,0 +1,353 @@ +# Feature 082: Scanner: Capture Always in View + +**Status:** Draft for Gate A +**Version:** 0.1.0 +**Created:** 2026-11-13 +**Last Updated:** 2026-11-13 +**Depends on:** [spec 080](../080-phone-first-scanner-components/spec.md) (the shell, the sheet, the lines over the camera, `c-shell--focused`; frozen into the [design system](../../design-system/README.md)), [the phone-first scanner design](../../superpowers/specs/2026-11-12-scanner-phone-first-design.md) (approved 2026-11-12), the scanner as built ([scanner](../../architecture/scanner.md) area doc; specs 007, 009, 011, 040, 042, 047, 065) +**Branch:** `082-scanner-capture-always-in-view` +**Issue:** [#344](https://github.com/plainprogrammer/Collector/issues/344) (roadmap Theme D, the phone-first layout's second item) + +--- + +## Changelog + +| Version | Date | Change | +|---------|------|--------| +| 0.1.0 | 2026-11-13 | Draft from issue #344, spec 080 and the design. Decision 1 ruled by the maintainer on 2026-11-13 (option C: no cards soon; the live re-measure is the last gate, on a new corpus). Decisions 2 to 21 are my calls for Gate A; the re-measure's bar (decision 4) is a scope question for the maintainer | + +--- + +## Problem Statement + +The scanner is one scrolling column: the page heading, a 3:4 camera stage, the controls, "What the scanner read", the candidates, then the sitting's panel with its Condition and Location form. On a phone the candidates land below the camera, so every card is capture, scroll down, add, scroll back up; spec 009 measured a median of 23 s a card. The design of 2026-11-12 makes the scanner one screen, and spec 080 drew its parts in the design system. Nothing renders them yet. + +This spec builds the page: the shell of spec 080 with the sitting's destination in the top bar, the camera filling everything between the bars, and the capture button in the bottom bar, always in view. The sitting's choices and its list move into sheets, so opening them never releases the camera. A result opens in the selection pane's place holding today's candidates; after an add the camera is live again with a line confirming it and Undo. The guide grows with the stage, so the reading is measured again on live captures at the new framing before this ships. The pane's own layout (the band, the swipe row, the details block, the add per finish) is [#345](https://github.com/plainprogrammer/Collector/issues/345); the sitting's language is [#58](https://github.com/plainprogrammer/Collector/issues/58). + +> **Terms.** As spec 080's: the *shell*, the *stage*, the *pane*, a *sheet*, the *sitting*. The *destination* is the top bar's button showing the sitting's place and condition. The *main button* is the bottom bar's middle button ("Capture", or "Scan again" while a result shows). The *status line* and the *after-add line* are the two lines over the camera (spec 080 decision 17). The *full framing* is this spec's guide in the full-height stage; the *shipped framing* is the guide of specs 007 to 011 (the stage 3:4, the guide 80% of its height and at most 90% of its width). A *stage notice* is a message centred on the stage when the camera or the engine can't be used. + +## Goals + +- On a phone the scanner is one screen that never scrolls, and the main button is in view in every state. +- The camera is as large as the screen allows; the guide grows with it, and the page's guide and the capture's crop are one geometry. +- The sitting's destination is always visible in the top bar, and the destination and the sitting's list open as sheets that keep the camera running. +- A result is reached and dismissed without scrolling the page; an add returns to the live camera with a line that confirms it and offers Undo. +- The reading is no worse at the full framing than at the shipped framing, measured on live captures of the same cards, before this ships. + +## Non-Goals + +- The pane's own layout and behaviour: the capture strip, the shrink and slide, the status band, the swipe row with Previous and Next, the details block, one add button per finish under it, "What the scanner read" leaving the page, the Other printings frame's replacement: #345. Today's result partial, candidates grid and Other printings frame move into the pane unchanged. +- Any change to how a card is read or ranked: the strips, the OCR settings, the detector, the art fingerprint, the ranking. Only the live guide's size changes (decision 3). +- The photo path's geometry: a picked photo keeps the shipped framing (decision 3). +- A sitting's language (#58), look-up mode (#262), continuous capture (#54), detection on live frames (#53), Japanese cards (#55). +- New collector-owned data, a migration, a new route or request parameter (decision 20). +- A new ADR (decision 21). + +## Decisions + +Decision 1 is the maintainer's, ruled on 2026-11-13. Decisions 2 to 21 are my calls for Gate A. Decisions of the design are cited as "design decision N", of spec 080 as "spec 080 decision N". + +1. **The re-measure is the last gate and runs on a new corpus** (ruled 2026-11-13, option C). The cards are not at hand soon; the maintainer may still have the 35 cards of specs 010 and 011 but expects to build a new corpus. So the re-measure (Story 7) names no earlier card: it runs on a corpus the maintainer gathers, verified as `.claude/memory/verify-corpus-manifests.md` lists, and the plan orders it last, with nothing else waiting on it. The pull request stays a draft until it has run. Alternatives: the 35 cards of spec 011 (not at hand); no re-measure (refused by the issue). +2. **The guide grows with the stage, and the shipped framing stays measurable** *(my call)*. The live guide is centred in the stage, 80% of the stage's height and at most 97.5% of its width, keeping 63:88. At 390 × 844 the stage is 390 × 692 and the guide 380 × 531 (the design's "about 380 × 536"); at 360 × 640 the stage is 360 × 488 and the guide 279 × 390. On a phone whose camera delivers 1080 × 1920, a card filling the guide is about 1,050 px wide in the frame, against about 825 px at the shipped framing. `scanner/geometry.js` holds the shares; the camera controller draws the guide from them and maps it into the frame with them, so the page and the crop are one geometry (AC-3.1). Measurement mode can draw the shipped framing instead (decision 5), so both are measured on the same cards. Alternatives: the shipped framing's card size in the tall stage (no re-measure needed, but a smaller card on a larger screen, against design decision 2); the guide as wide as the stage (no margin to aim with). +3. **The photo path keeps the shipped framing** *(my call)*. A picked photo is placed in a 3:4 view with the shipped guide, by detection or by guide placement, as today (spec 009 AC-7.1, AC-7.2). The photo has no stage; its geometry was tuned on photos and the detector's strips assume it. `geometry.js` keeps the shipped shares under their own name for the photo path and measurement mode, and the live guide's shares under another. +4. **The re-measure's bar** *(my recommendation; a scope question for the maintainer at Gate A)*. A paired run on the same cards in one sitting: each card is captured once at the shipped framing and once at the full framing, in alternating order per card. The full framing passes when its right card first is at most one card below the shipped framing's on the same cards, and its exact printing (cards whose collector line prints a set code) at most two below. If it fails, the full framing's guide becomes the shipped framing's card size inside the tall stage (decision 2's first alternative), which the same run has measured, and no further sitting is needed. The corpus: about 40 cards the maintainer owns, none of the 12 tuning cards of spec 007, at least 10 sets, at least 8 foils, at least 5 cards whose frame prints no set code, a few with a light name on a dark bar; every row resolves to one printing (`scanner:ground_truth`), overlap with earlier corpora is reported, and each foil value is checked against the card. Alternatives: (B) one run at the full framing with an absolute bar, at least 31 of every 35 right card first (spec 009's first captures, text only) or 34 of 35 with art on (spec 011), which can't be compared fairly across corpora and phones; (C) report the figures with no bar, the maintainer judging them. +5. **Measurement mode is the same page** *(my call)*. The re-measure needs the stage the collector will use, so measurement mode renders the same shell; its run panel (the manifest row, the counts, Skip, Store again, its notices) opens as a third sheet, "Measured run", from a button in the top bar that only measurement mode renders. The framing it draws and crops with is a development setting read once at boot (`COLLECTOR_SCANNER_FRAMING`, `full` by default, `shipped` for the paired run's other half), recorded with each capture, and run directories differ per framing (`COLLECTOR_SCANNER_RUN_DIR`), so the findings tooling scores each framing as a run of its own. The setting is ignored, and cannot be set, outside measurement mode. Alternatives: a framing switch on the page (a run would mix framings within one manifest row); the old page kept for measurement (it measures nothing the collector will use). +6. **The page is a focused page** *(spec 080 decision 8, built here)*. The scanner view asks the layout for a focused page; the layout then gives the `` `c-shell--focused` beside `c-shell--untabbed`, renders no app header, no tab bar, no footer and no `#status` region of its own (the scanner page places its own, decision 11). Every other page's layout output is unchanged. +7. **The top bar** *(design decisions 3 and 4; spec 080 decision 11's copy)*. Back is a link to the collection page, named "Back", with the back icon. The destination is a button, "Adding to" above the value ("Atraxa › Main · NM", "Blue binder", "No location · NM", "No location"; the place is its label, a zone as "Deck › Zone", and the condition its short label); it opens the destination sheet. "Done" is the link to today's confirm page and shows only while a sitting is open (spec 009 AC-3.7); with none the destination takes its room. Every answer that changes the sitting (an add, an Undo, a "Set") redraws the destination and Done. +8. **The bottom bar** *(design decision 5; spec 080 decision 11)*. The count button reads the sitting's count over "added" ("0 added" with no sitting), with the accessible name "12 added. Show this sitting's cards.", and opens the list sheet. The main button is "Capture" (primary) while the camera is live and no result shows, disabled while the engine loads, while a reading runs and while the camera is unavailable; it is "Scan again" (secondary) while a result shows (decision 13). The torch shows only where the camera's track reports one, as today, and its accessible name is "Torch" with `aria-pressed`. Every answer that changes the sitting redraws the count. +9. **The two sheets hold today's panel, divided** *(my call; spec 080 decision 2)*. Each is a `dialog.c-sheet` opened with `showModal()` by a small Stimulus controller, closed by Close, Escape or a tap on its backdrop. The destination sheet, "Adding to", holds today's Condition and Location form (`_picker`, its fieldset, legend, selects and "Set", unchanged), then the art line (when art matching is on) and the photo hint. The list sheet's title is today's heading ("This sitting: 12 cards, Near mint, in Blue binder"), focusable as today (spec 047), and its body holds today's entries with Details, Undo and "Show all", or today's empty-state sentences. Neither sheet holds "Done" (the top bar does). Both end with the content notice's foot (decision 16). A form in a sheet answers with a Turbo Stream, as today; nothing in a sheet navigates but "Details" (Edit copy, which ends the scanning, as today). +10. **A sheet's announcements are inside it** *(my call)*. While a modal dialog is open the rest of the page is inert, and a live region outside it is not announced. So each sheet holds its own `role="status"` region at the top of its body: "Set"'s sentence (accepted or refused) goes into the destination sheet's, and an Undo's sentence into the list sheet's. The same answer also writes into the after-add line (decision 12): of the two, only the one in a shown, non-inert place is announced, so a collector hears each sentence once whichever control sent it. +11. **A result opens in the pane's place** *(design decision 9's place, #344's interim content)*. On capture the live picture pauses on the captured frame (the track keeps running) while "Reading the card…" shows; the reading's answer sets `c-scan--result` on the shell, and the pane (`c-scanpane`) fills the stage over the frozen frame. The pane holds, from the top: the page's `#status` region (the add's refusals and failures, Other printings' announcements), the send failure with its name and collector line, "Send again" and "Sign in" (today's), and `#scanner_result` with today's result partial unchanged ("What the scanner read", the art overrule sentence, the candidates grid with their add buttons, Other printings). The pane's content scrolls inside it; the bars never move. "Nothing could be read", "No card matches" and "The card catalog isn't ready yet" stay in the result, as today. The capture strip is #345's: no strip is rendered here. The reading's spoken line becomes "Done. What the scanner read and its candidates are showing." (today's "…are below."). +12. **After an add the camera is live and the after-add line confirms it** *(design decision 11; spec 080 decisions 11 and 17)*. A successful add clears the result, which removes `c-scan--result` and resumes the live picture; the after-add line's text becomes "Added Lightning Bolt MOM · 123, foil" ("Added Lightning Bolt MOM · 123" for a printing with no finish), which is what is announced, with Undo for that entry beside it, outside the live region; the status line is hidden while the after-add line shows, and the next capture empties and hides the after-add line and shows the status line. Undo from the line removes the copy as today and the line's text becomes today's "Removed 1 × ‹name› (‹SET› · ‹number›, ‹finish›) from your collection." with no Undo. A replay of an undone or changed add, a refused add and a full lot keep the result showing and say so in the pane, as today. The after-add line names no condition and no place: the top bar shows them. +13. **"Scan again" is built here** *(my call; design decision 12)*. With the result filling the stage the collector can't aim, and without this button only an add would bring the camera back. While a result shows, the main button reads "Scan again" in the secondary style at the same place and size; it discards the reading (empties the result and the send failure), removes `c-scan--result`, resumes the live picture and gives the status line "Ready. Line the card up with the guide, then capture.". The reading's key is dropped with it, so nothing about it is sent. #345 keeps the button and its behaviour. Alternative: a Close button in the pane (one more control, and a second way to do the same thing once #345 lands). +14. **Where the other controls and lines sit** *(my call; the design's suggestions)*. "Use a photo" is a secondary button at the stage's bottom-left corner, `space-4` in from its edges, over the camera, 44px high, with today's file input inside its label, and is shown whenever the engine is ready, the camera live or not. The art line and the photo hint are in the destination sheet (decision 9). A stage notice is centred on the stage, on `surface-raised` with a `line-strong` border, for: the camera unavailable (today's reason sentences, "Try the camera again" except for the insecure context, and "Use a photo" as the way in); the engine failed ("The scanner couldn't load…", "Load the scanner again"). Today's `