UI interaction contract

Primary tasks

Cards is read-only. Update cards contains quantity edits, removals and restore. Add cards appends confirmed cards. In-store saves purchase candidates separately from ownership. Decks are independent selections. Play reads one saved deck. Never favorite the collection itself. Never imply a photo, search result, or evolution card has been added before explicit confirmation.

Scan flow

  1. Upload: show file selection and privacy note. Crop and Review are unavailable until their prerequisites exist.
  2. Crop & rotate: hide upload and match results. Preserve photo and per-card saved rotations. Read performs OCR only on captured crops.
  3. Review: hide crop editor. Show candidates or a manual fallback, never silently select and add a printing. Back restores the same crop editor. Another photo clears only unconfirmed photo state, never inventory.

An OCR error must retain the image for retry and offer manual entry. Batch cards retain separate confirmations. Switching input tabs must not destroy scan progress. Confirmation stays in Add cards and shows a toast. Only its View in collection action switches to Cards, resets filters and highlights the printing.

Use one primary Find matches action with the number of cards. Grid layouts capture the selected grid and read it in that action; hide the optional Add card to batch control in grid mode. Single-card mode can optionally build an independently rotated batch using Add card to batch. Clearly label its thumbnails Cards to check, and explain that Find matches reads only those batch cards until the batch is cleared. Do not expose competing Read current / Read saved action labels.

The crop selection is a freeform rectangle in both single-card and grid layouts: edges and corners resize independently, with no fixed aspect ratio. Start with a sensible card/grid-shaped outline, but do not enforce that shape during drag, layout reset or rotation. Drag inside to move the box. Export still normalizes each card to 1008 × 1408 pixels. This is rectangular cropping, not four-point perspective correction; separately crop cards at different angles.

Card reader and overlays

Artwork previews open after 350 ms of mouse hover on the thumbnail image only. Rows, names, stats, buttons and keyboard focus do not trigger them. Moving away from the image before the delay shows nothing; moving onto other row content dismisses the preview. Use larger source art when available, with thumbnail fallback on failure. A fixed, pointer-transparent manual popover sits in the top layer and is clamped to the viewport, never changing list scroll height or stealing focus. Click, scroll, Escape, window blur, resize and anchor removal dismiss it. Touch and keyboard users can open the full card reader. Suppress previews while a modal is open. Browser placement checks remain outstanding.

Content and visual hierarchy

Show dynamic collection counts, not the fixed 60-card rule as a dashboard statistic. One primary action per task; secondary administrative actions belong in overflow. Avoid repeated or jokey instructional copy. Pip's optional animation is user-triggered. Its original eyes stay in the artwork; floating duplicate eyes are hidden. Count with Pip can speak an equation using the browser voice, only after a deliberate Count or Next step action. Read aloud turns speech off. Close and pause stop speech.

Card search sources

Bundled Pokémon TCG Data supplies immediate and offline fallback matches; explicit full-catalog search uses TCGdex. TCGplayer is a marketplace, not the official Pokémon database. Its developer documentation currently states that new API access is not being granted (checked October 11, 2026): https://docs.tcgplayer.com/docs/getting-started

Typing filters local matches only. Search or Enter explicitly submits to the full TCGdex catalog. Query changes abort outstanding catalog requests and invalidate their responses. Disable Search and Load more while one request is in flight; ignore repeated submissions until it finishes. Cache up to 40 result pages in memory for ten minutes; do not persist search history. Empty or one-letter names do not issue broad requests (printed numbers and set references are allowed). On network failure clearly label Local fallback and preserve usable local results. Never automatically retry. Honor a 429 Retry-After with a session-wide cooldown (30 seconds if unspecified; bounded to 1–300 seconds). These are client-side protections, not server-enforced limits. The source label, Search button and TCGplayer external-website link sit next to the search controls, above results. There are no provider toggles: TCGplayer is not queried by this app. Only a deliberate submission sends search text to TCGdex; images are not sent.

Offer a user-initiated TCGplayer search link for reference, not scraping or automatic import. External pages cannot add an inventory entry. Compare exact printing, number and effects before confirming. Data coverage and OCR remain fallible.

Local and online search page through printings in groups of 24, with an explicit Load more action. Accept a name, a printed number/fraction, a known set code, full set name, or provider set ID. Keep name, number, total and set constraints separate: never silently broaden a failed exact lookup. Unknown set codes require correction or deliberate removal. Resolve catalog set aliases to TCGdex IDs by full set name, not guessed ID numbering. Online results load exact details before review; no API response updates inventory without Add confirmation. Editing a query invalidates pending results.

Every scan candidate group offers None of these match. Single-card recovery opens manual fields; batch recovery keeps its crop and confirmation callback while searching online. No Google photo upload, headless Lens scraping, Scrydex integration, paid account, or billing setup is included.

Pasted-list review uses inline visual printing choices, not select menus or floating comboboxes. Each entry shows quantity, card artwork, exact set/number, type and printed stats when available. Native radio inputs provide one choice per entry; even a single exact match must be explicitly selected. Details opens the read-only card reader without selecting or adding. Clear selection restores the unconfirmed state. A sticky Add selected summary shows selected line/copy counts and is disabled until a valid choice exists. Partial confirmation preserves unresolved input and shows added rows as completed, preventing duplicate additions. Search all printings is explicit and preserves the original quantity in its confirmation dialog; a successful confirmation also marks that list entry completed.

Interaction checks

Top search opens a drawer from the header or Cmd/Ctrl+K. Search scopes are owned cards, the selected playing deck and saved decks. Matching while typing uses local data only. Recent terms last for the current session; no terms are persisted or sent online. Result selection opens a read-only card or a saved deck. Existing Add-card online search remains explicit.

Play defaults to an artwork grid. The sticky controls join the actual measured app header, and the selected-card Read action remains accessible while scrolling on mobile. Reader mode displays complete effects inline, including Basic Energy information, without an extra Read button. Double-tapping the image opens a full image; wrappers and text do not. A dragged touch or single tap cannot enlarge. Desktop double-click and explicit Full image buttons provide alternatives. Full image inside an existing reader reuses that dialog and preserves its trail, confirmation quantity and scroll when returning. Image failure retains Close/Back access.

The card reader stacks large artwork above text on mobile, with no image wider than its grid column. Camera and Choose photo are two equal panels on wider screens and stack narrowly; both are hidden after Upload. Cards workspace actions become labeled icons on small screens rather than wrapping across rows.

Full table opens a viewport-filling collection dialog with local search, kind filtering and sortable name, copies, kind, type, HP, printed damage, added and updated columns. Expanded rows show card artwork and effects inline; the full card reader remains available. No table sorting/filtering calls online endpoints. Missing historical timestamps display Not recorded and sort after recorded dates. Track dates on future additions and quantity changes; never invent prior history. The backend proposal is documented separately at /backend-plan.html and has not been deployed.

Automated interaction tests must cover state guards, single and batch review, preserving crops on Back, explicit additions, combobox selection/cancel, separating removal, and closing Pip's animation. DOM tests are not browser layout or accessibility certification.

Manual browser gate: keyboard-only Tab/Shift+Tab and Escape with nested dialogs; trigger focus restoration; screen-reader labels; opening menus at all four viewport edges; no list scroll-height increase; 360px and 1024px layouts; reduced motion and mute; repeat scan/crop/Back/confirm with mixed rotations.

Browser verification is required before claiming those visual/focus checks pass. The current environment has no supported browser-control skill; those checks are explicitly outstanding.

Reading aloud

Speaker icons next to owned cards, deck cards, Favorites, expanded rows and the card reader read the exact printing's HP, types, attacks, Energy costs, effects, abilities and rules. Speech starts only after a tap; tapping again or Stop reading cancels it. A slow English voice prefers local device voices. Variable printed damage is described as requiring calculation; missing data is called out. Navigating, closing the reader or hiding the tab stops reading. The player stays inside an open modal so its stop button is accessible. No microphone, recording, paid TTS service or speech endpoint is used. Browser/device voices and pronunciation vary; its speech service may use its own network connection.

Account entry and navigation

Initial entry is a loading/sign-in gate. The sidebar and workspace are hidden and inert until both session and account data load. Registration and recovery remain available. Sign-in cannot be dismissed into a guest workspace. Empty accounts show Add cards to start; returning accounts load online data without replacing it from an empty cache. Log out returns to the gate. Failed session/data requests offer Retry.

The desktop sidebar holds search, card tools, useful deck summaries with direct Play, and an account footer. Its body scrolls independently of the fixed header/footer. Header actions use compact icons; Log out remains visible. Mobile keeps search and account actions in the header.

Mobile Cards and duplicate review

On mobile the header and bottom navigation remain outside one scrolling workspace. Card rows do not use a second scroll area. Cards shows twelve filtered printings per page; filtering resets pagination and never changes inventory. Help & info opens rules, privacy and supporting links in a dialog and restores those sections when closed. Large thumbnails remain. Camera and Photo use matching input cards and the same button treatment. Camera permission still requires a tap.

Adding an exact printing already owned shows the owned, added and resulting counts. A single-card review requires extra-copy consent; changing the quantity clears it. Paste/list, JSON Add and purchased-candidate transfers review duplicate totals before saving. Cancel changes nothing. Different printings sharing a name remain distinct. Energy copies are allowed and receive the same confirmation. Duplicate JSON rows remain invalid, and Replace/Restore operations retain their explicit replacement review rather than treating their counts as additions.

React checkpoint

Cards and Update cards share TanStack Table row models and shadcn primitives. Mobile navigation offers numbered pages and a direct page selector rather than only Previous/Next. Default pages contain six printings; 12 and 24 are optional. Search filters every owned printing locally. Filters and sort sit in a compact Popover; favorites is a separate toggle. Changing query/filter/sort returns to the first page. Shrinking the collection clamps the page to the remaining range. Quantity edits do not reset the current page. Expanded rows contain the actual card text. Cards never offers ownership inputs or removal.

Reader list navigation also uses numbered positions and a direct selector, independently of the evolution trail. Text size is an Aa radio menu beside read-aloud and Close, with Small / Medium / Large options (14 / 16 / 18 px card copy); it does not consume a full control row above the image. Formik + Yup handles migrated form drafts using shadcn fields. Online catalog lookup remains explicitly submitted.

History separates the protected Current card list from deletable snapshots. One modal owns browsing, preview, naming and deletion confirmation; no nested confirmation modal is added. Deletes affect snapshots only and provide guarded session Undo. Online deletions remain deleted after reload.

Play navigation uses one selected printing ID for the spread highlight, readable content and position selector. Previous/next or direct selection reveals the corresponding spread card without stealing keyboard focus. Clicking a spread card updates the controls. Mobile controls use one compact row with view/search/card actions in the overflow menu. Deck Settings, Save changes and overflow stay in a three-column mobile row.

Account forms show the original Pip artwork. Pointer motion is decorative and does not displace controls. Password/recovery focus raises a privacy shield; animation never reads those values. Form validation, submission, recovery-code saving and keyboard focus remain independent. No autoplay audio; reduced-motion removes movement.

Reader text size changes all card data and evolution copy, including previously fixed energy/retreat text, headings and stats; the Image tab hides Aa because artwork text cannot be resized. Navigation keeps its shadcn sizes. The evolution selector uses one horizontal Radix tab row with three visible image tabs on mobile, scrolling for longer chains. Printings appear directly below the selection; the current Pokémon is selectable too. Rules sit in a separate collapsed accordion below printings. Choosing a printing drills into the existing evolution trail; selecting the current printing returns to Card data.

Play sticky toolbar uses the nearest scroll container: top 0 inside mobile main (header and status are already outside it), measured header offset for desktop document scrolling. A ResizeObserver measures the React toolbar height for card reveal margins instead of a fixed 120 px estimate, with listener cleanup when Play is left.

Evolution image tabs override the primitive's horizontal h-9 constraint to keep full image/label height. Stage selection only scrolls the horizontal row. Chrono history uses natural timeline height and visible inner overflow so the shadcn modal body owns vertical scrolling; mobile hides redundant axis titles.

Scan uses Stepperize's guarded controlled steps and shadcn cards/controls. Camera permission is requested only by Open camera; Choose photo uses Formik/Yup file checks before decode. Cropper/video/preview ports stay mounted when steps hide, without React owning their internal nodes. Crop controls disable while processing. Manual recovery is a Formik/Yup form with explicit Find matches / Search all printings, and result-image buttons open the existing confirmation reader. Review never silently adds a guessed match. Photos remain temporary memory and are excluded from workspace/export/API payloads.

Nested Select/Dropdown portals use z-index 180, above popovers at 160 and dialogs at 150 (overlay 149). Rules/help HTML inherits foreground text; secondary help text uses muted-foreground. This avoids retaining legacy text colors that resolve to a shadcn background color.

React navigation and deck editing

Desktop navigation uses shadcn Sidebar with icon collapse. Secondary mobile navigation uses Sheet, closes on a chosen destination and supports Escape. Cards/Decks/Play remain three bottom destinations with safe-area padding. Header, sync status, main scroll origin and mobile navigation are separate layout regions; the Play toolbar sticks within the existing main scroller. Sidebar/header actions remain authenticated and call the established controller.

Saved-deck cards use overflow menus for rename, duplicate, export and deletion, with favorite as a quick action. The editor's Settings/Save/menu controls stay in a three-column mobile row. Deleting a saved deck asks for confirmation and removes that deck's game notes; deleting a different deck must preserve the current draft. Current ownership is never altered by deck editing.

Deck cards use TanStack Table search/kind filtering/sorting and six printings per numbered page. Card artwork occupies the first column; quantities and actions stay in the second. Inline expansion includes the card data. Clicking a name opens the shared reader with the filtered card order. Quantities retain blank input until blur/Enter or an explicit increment/decrement, validate whole numbers 0–60, and still pass the domain ownership/copy/60-card checks. New builds and rebuild previews are explicit Formik submissions; changing a field makes no network request.

The Chrono decoration is clipped by the natural-height timeline boundary, never the heading/action row above it. Menu content is portalled so timeline clipping does not clip the snapshot actions.

Entry and shared management overlays

Find/Scan/Paste/JSON share Radix Tabs. Panels remain mounted while inactive, preserving drafts and native crop/camera lifecycles. Find and Paste printing choices use TanStack numbered pagination and image-only delayed magnification. Text entry performs no online request; online lookup requires Search/Enter or an explicit load-more action.

Reviewed paste text must still match the draft before Add. Choosing an image sets a selection, never ownership. JSON validation and row preview are separate from Save; destructive replacements require confirmation, while additive duplicates use the existing duplicate review. Changes to text/mode/workspace invalidate approval.

Removal, swaps, conflict/import decisions, deck rename/delete and JSON replacement use the shared management overlay owner at runtime. Account recovery takes priority and queues management notices. Removal/swap callbacks reject changed source data. Cancel/Escape writes nothing, and pending network actions display loaders.

Section-top cross-navigation helpers are removed. Bottom/side navigation targets the selected saved deck for Play from Decks; unsaved deck edits are handled before changing to Play. Actual Build/Save/New deck/selection actions remain.

The evolution selector traverses the connected printed-parent chain immediately, including later stages and owned-only printings. Goomy exposes Sliggoo and Goodra on the first visit; selecting a printing is not required to discover another stage. Regional branches remain distinct and cycles stop.

Play includes a Coin flip action, available even without a focused card. It opens one full-screen dialog with Pip heads and star tails. Each toss samples one cryptographically random bit independently of the animation. Three.js renders a Cannon rigid-body trajectory with gravity, collisions and settling; the symmetric coin faces are labeled before replay, without an end snap. The scene loads only when opened, renders only during tosses or resize, and releases graphics resources on close. Reduced motion shows the outcome directly, and unavailable WebGL uses a CSS toss. Results are announced and recent flips stay in memory; no workspace changes or API requests are made.

In-store recommendation Find printings is an explicit catalog search. It collapses the suggestion accordion, clears stale set/number filters, opens Find, and focuses/reveals the matching-printings heading without opening the mobile keyboard. It reuses the catalog loader, cancellation, throttling and local fallback. Repeated taps while searching cannot start duplicate lookups. Searching never saves a candidate or changes ownership; exact printing review and confirmation remain required.

Play options includes optional Group cards: Pokémon, Trainers & Energy (default) or No grouping. The grouped spread has headings and copy counts for nonempty kinds, with the same responsive artwork grid within each section. Filters apply first and the existing sort is kept within each kind. The card selector shows the same sections and positions; Previous/Next follows visible order while switching grouping preserves the focused printing. Reader mode uses the grouped selector without extra content panels. Grouping is an account-scoped, per-deck display preference restored on reload and in group=kind links; it never changes the saved deck, inventory, Prize notes or makes a catalog request.

Grouped Play sections are independent native details/summary accordions, initially expanded. Their collapsed IDs and grouping choice use the existing account-scoped per-deck browser preferences, with group=none and closed=pokemon,energy URL parameters for overrides and deep links. Selecting a hidden card through Previous/Next, the card selector or keyboard navigation expands its group and records that new state. Filtering keeps collapsed choices, including kinds temporarily absent from results. Browser preference format 2 migrates the old implicit flat default to grouped while retaining navigation, filters and scroll; new explicit No grouping choices remain saved. These display preferences never enter the cloud workspace payload.

Full-screen coin dialogs use the shared DialogContent fullscreen layout rather than overriding a centered dialog with inline transform:none. This variant omits all 50% offsets, independent Tailwind translate centering and zoom transforms; it uses inset:0, full viewport width and dynamic viewport height on desktop and mobile. Header, Close target (44 px), content sides and footer respect safe-area insets. Other management dialogs retain the centered layout and constrained scroll body.