Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/features/card-dom-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,19 @@ Documented in `frontend/docs/dom-api.md`. Test coverage in
`QuestionFeed*.spec.ts` suite (unified vote queue, successor to the old
standalone `PrintingTagQueue.tsx` this API originally shipped against)
— real Playwright runs against the mocked backend, not just typecheck.

**Known gap: unimplemented on the unified `/editor` page's sheet.**
`Card.tsx`/`CardSlot.tsx`/`CardDetailedViewModal.tsx` (plus the non-slot
`orphanCard.ts` helper) are the only callers of `getCardDataAttributes`/
`getCardSelectedEventDetail`/`CardSelectedEventName` — none of them render
the _placed_ card for a project slot on the unified page.
`PagePreview.tsx` (the sheet's own per-slot renderer,
`frontend/src/features/pdf/PagePreview.tsx`) renders a plain, unwrapped
`<img>` with no `data-card-*` attributes and dispatches no
`mpc:card-selected` event at all. Any external tooling built against this
contract (userscripts, browser extensions) that read a project's placed
cards off the classic grid gets nothing on the unified page. The one
classic test exercising this ("selecting an image in a CardSlot via the
grid selector") was dropped in the issue #272 wave-3 port rather than
weakened; wiring `PagePreview.tsx`'s slot `<img>` into
`getCardDataAttributes` is a contained, mechanical fix once prioritized.
64 changes: 53 additions & 11 deletions docs/features/grid-selector.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ matched image for a card slot, seeing them all at once in a grid
image-loading/error states, since those apply to every card render
across the app, not just inside this modal.

**Post-route-swap reachability (2026-07-24, issue #272 parity wave 3):**
per-slot picking on the unified `/editor` page goes entirely through the
rail's own Select Version section below (a different component,
`SelectVersionResults.tsx` — no modal, no grouping/filters-sidebar/Jump-to-
Version UI of its own). `GridSelectorModal.tsx` itself has exactly one
surviving mount post-swap: `CardbackToolbarButton`/`CommonCardback.tsx`'s
project-wide cardback picker (testid `cardback-grid-selector`, title
"Select Cardback"), reachable from the right rail's Cardback button once
the project is non-empty. It's otherwise unchanged and fully generic (a
bare `imageIdentifiers` array + `onClick` callback) — every grouping/
filter/keyboard/mobile-viewport behavior below applies identically
regardless of which caller's identifiers feed it. `GridSelectorModal.spec.ts`/
`GridSelectorModalVariants.spec.ts`/`CardSlot.visual.spec.ts`'s own two
grid-selector snapshot tests were re-ported onto this cardback mount in
that wave — see `openDisplayCardbackGridSelector` (`frontend/tests/ test-utils.ts`) for the helper and its own comment for the full rationale.

## Frontend-polish UX pass (PR-B, 2026-07-17)

Presentation/interaction fixes from the frontend-polish package's
Expand Down Expand Up @@ -480,17 +496,28 @@ described above.
half of PR #325's backend contract)
- `frontend/src/features/card/Card.tsx` (+ new `Card.test.tsx`),
`CardSlot.tsx`
- Tests: `frontend/tests/GridSelectorModalVariants.spec.ts` (keyboard nav
- a large-grid focus-perf check, autofocus fallback, mobile filters
default — merged from the former `GridSelectorModalAccessibility.spec.ts`
and `GridSelectorModalMobile.spec.ts`), `frontend/tests/CardImageStates.spec.ts`
(error placeholder + slow-load hint), `frontend/tests/SelectVersionSection.spec.ts`
(grouping/ordering, moment (a)/(b)/(c) behavior on the sidebar layout — issue
#167 — plus the funnel's implicit-cast/reset/ack and retract-on-reselect
end-to-end flows), `frontend/src/features/gridSelector/SelectVersionResults.test.tsx`
(axis exclusivity, membership-driven axis rendering, disclosure tiers,
SUGGESTED-chip rendering, F5 votes-off completeness), `frontend/tests/ DisplayPage.spec.ts` (F6: right-click + the `⋯` cue opening the shared
context menu on the center sheet)
- Tests: `frontend/tests/GridSelectorModal.spec.ts` (23 tests) +
`GridSelectorModalVariants.spec.ts` (7 tests: keyboard nav, a large-grid
focus-perf check, autofocus fallback, mobile filters default — merged
from the former `GridSelectorModalAccessibility.spec.ts` and
`GridSelectorModalMobile.spec.ts`) — parity wave 3 (2026-07-24, issue
#272) ported both onto the cardback mount (see the "Post-route-swap
reachability" note above); `frontend/tests/CardSlot.spec.ts` (15 of 25
tests ported the same wave — delete/duplicate/change-query/context-menu/
auto-select coverage against the sheet's own slots, `page-preview-slot` +
`page-preview-slot-menu-cue`; see this doc's own "Known gaps" for what
wasn't ported) and its `visual/CardSlot.visual.spec.ts` companion (2 of 6
aria-snapshot tests ported, retargeted onto the cardback mount, regex-
tolerant on a handful of pre-existing third-party icon-font/tree-select
rendering leaves — see that file's own module comment),
`frontend/tests/CardImageStates.spec.ts`
(error placeholder + slow-load hint), `frontend/tests/SelectVersionSection.spec.ts`
(grouping/ordering, moment (a)/(b)/(c) behavior on the sidebar layout — issue
#167 — plus the funnel's implicit-cast/reset/ack and retract-on-reselect
end-to-end flows), `frontend/src/features/gridSelector/SelectVersionResults.test.tsx`
(axis exclusivity, membership-driven axis rendering, disclosure tiers,
SUGGESTED-chip rendering, F5 votes-off completeness), `frontend/tests/ DisplayPage.spec.ts` (F6: right-click + the `⋯` cue opening the shared
context menu on the center sheet)

## Known gaps

Expand All @@ -509,3 +536,18 @@ described above.
caller specifically; `GridSelectorModal.tsx`'s own sidebar layout is
unchanged, so this is a rail-only fix, not a change to the shared
column-breakpoint default itself).
- **Per-slot next/prev image-cycling has no unified-page equivalent**
(found 2026-07-24, issue #272 parity wave 3). The classic grid's inline
❯/❮ arrows (`CardSlot.tsx`) let a user cycle a slot's selected image one
step at a time, with wrap-around; the sheet's Select Version section is
a browse-and-click surface only — no "next"/"previous" concept at all.
`CardSlot.spec.ts`'s 3 cycling tests were dropped, not ported, for this
reason (see that file's own module comment). Not tracked against any
numbered gap in issue #272's own checklist — a new finding, flagged for
the owner alongside this same wave's `card-dom-api.md` gap below.
- **`docs/features/card-dom-api.md`'s DOM API contract (`data-card-*`/
`mpc:card-selected`) is unimplemented on the sheet's own placed-card
slots** (found 2026-07-24, same wave) — see that doc's own "Known gap"
entry for the full detail; cross-referenced here since the dropped test
that surfaced it ("selecting an image in a CardSlot via the grid
selector") lives in this feature's own `CardSlot.spec.ts`.
19 changes: 12 additions & 7 deletions docs/features/saved-decks.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,18 @@ from the other without any extra plumbing:
The **load** side (`MyDecksPage.tsx`'s "Open in editor") still always
navigates to `/editor`, unchanged — that's the spec's own explicit binding
text (§4: "an **'Open in editor'** action per row ... navigates to
`/editor`"), not an oversight of this integration. A user working entirely
on `/display` therefore still has to detour through `/editor` once to load
a previously-saved deck (client-side nav between the two pages preserves
the in-memory project — see `DisplayPage.tsx`'s own comment — so this is a
one-time hop, not a full reload); giving `/display` its own load entry
point is unbuilt, tracked as an open item on issue #165, not silently
assumed out of scope.
`/editor`"), not an oversight of this integration. Before the 2026-07-23
Proposal H switchover (issues #231/#272 — `/editor` now serves the unified
page directly, `/display` redirects there), this meant a detour through
the classic grid page for a user working on `/display`; that detour no
longer exists post-switchover, since `/editor` and the page a `/display`
visitor was already on are now the same route. `DisplayPage.tsx`'s own
`isProjectEmpty` landing (issue #268's `SavedDecksLandingPanel`) remains
the more direct in-place load path for an empty project — the open item
this paragraph used to describe (giving the unified page its own load
entry point) is effectively resolved by the route swap for the navigation
case, though the landing panel's own scope (only shown when the project is
empty) is unchanged.

## The mental model

Expand Down
30 changes: 24 additions & 6 deletions docs/proposals/proposal-h-display-layout-spec.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
# /display responsive layout spec — issues #266 / #267 / #268

> **Routing note (2026-07-23, Proposal H switchover):** this doc's "/display" shorthand
> throughout refers to the unified page's DESIGN, not its live URL — the switchover this
> file's own §A5 tracked as still-open is now done: the unified page (component unchanged)
> now serves at **`/editor`**, and `/display` is a client-side redirect there. See §A5's
> updated entry for the full routing history and `frontend/src/pages/editor.tsx`'s own
> comment for the implementation.

Design target for the unified display page (`frontend/src/features/display/DisplayPage.tsx`,
route `frontend/src/pages/display.tsx`). Companion mockup: `display-mockup.html` (same
route `frontend/src/pages/editor.tsx`, formerly `frontend/src/pages/display.tsx` prior to the
2026-07-23 switchover noted above). Companion mockup: `display-mockup.html` (same
directory; open standalone via file://, use its top demo strip to force any breakpoint's
view at any window width) — synced (this PR) with the committed mockup under
`docs/proposals/mockups/proposal-h/responsive-layout-2026-07-21.html`, which previously
Expand Down Expand Up @@ -1117,11 +1125,21 @@ with the historical marking:
bulk selection should work across page boundaries in particular has no
answer yet.
- **Switchover / retire old routes** (the historical doc's migration
steps 5–6): flipping the default nav entry point to `/display` and
eventually retiring `/editor`'s grid-only view + the standalone PDF tab
is still not done — `/display` and `/editor` both remain live, reachable
routes today. Tracked here as the one still-open piece of that doc's
migration plan; no target date set.
steps 5–6): **DONE (2026-07-23), superseding the "still not done" status
this bullet originally carried.** Per an explicit owner directive that a
nav-label swap (PR #313, which pointed the "Editor" nav link at `/display`
while leaving the classic grid page live at `/editor`) fell short of the
intended switchover, the unified page (component unchanged) now serves
directly at **`/editor`**; `/display` is a client-side redirect there,
preserving query params/fragment. The classic grid `ProjectEditor` this
replaces is fully unrouted (its component files are left in-tree, not
deleted — that remains its own later cleanup decision, i.e. the doc's
migration step 6's "retiring" half is now only a code-deletion task, not
a routing one). See `frontend/src/pages/editor.tsx` and
`frontend/src/pages/display.tsx`'s own comments for the implementation,
and this switchover's own PR for the parity-gap tradeoffs it accepted
(issue #272's checklist was not fully closed at the time of this swap —
the owner's directive was to proceed regardless).

Everything else in the historical doc — its original single-toolbar/
single-rail layout (§2–§3), its component-mapping table (§5), and its
Expand Down
31 changes: 31 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -1555,6 +1555,37 @@ deliberately (recreate the container, watch whether the session
survives) and confirm which layer is actually responsible, then replace
this entry's cause with the confirmed one.

## Any `?server=...` link that also carries another query param or a URL fragment silently loses everything but `server` on load

**Symptom**: visiting e.g. `/editor?server=http://host:8000&foo=bar#frag`
(or any other page) ends up at plain `/editor` in the address bar once
the page finishes mounting — not just `foo`/`frag` dropped, `server`
itself vanishes too, along with a console warning: `Unknown key passed via urlObject into url.format: server`. Confirmed to reproduce on a
**direct** page load with no redirect involved at all — this is not
specific to any one route.

**Cause**: `useBackendSetter.ts`'s own `?server=` clean-up step calls
`router.replace({ server }, undefined, { shallow: true })` — passing a
bare `{ server }` object with no `pathname` as the `url` argument.
Next.js's `url.format` doesn't recognize `server` as a valid key on an
object missing `pathname`/`query`, silently drops it, and the resulting
URL is just the bare pathname. Reproduced during the Proposal H
route-swap task (2026-07-23) while verifying `/display`'s new
query-param-preserving redirect to `/editor` — the redirect itself
(`pages/display.tsx`, reading `window.location.search`/`hash` directly)
forwards params/fragment correctly on its own (verified with a
`?server=`-free URL, which never triggers this code path); this bug
fires independently, on `/editor` directly, once `useBackendSetter`'s
effect runs.

**Fix**: not fixed as part of the route-swap task (unrelated, pre-existing,
and in practice harmless for real traffic — no code in the app builds a
`/display` or `/editor` link with extra query params/a fragment today,
per a repo-wide grep). The real fix is straightforward whenever someone
picks it up: pass a proper `{ pathname: router.pathname, query: { ...router.query, server } }` shape (or drop the `shallow` URL-object
call entirely in favor of `router.replace(router.asPath.split("?")[0] + buildQueryString(...))`) so existing query params and the fragment
survive the same clean-up step.

## `chunkErrorRecovery.spec.ts`'s `expect.poll(() => reloadRequests).toBe(1)` fails with "Received: 0" in CI, passes locally

**Symptom**: one of the `chunkErrorRecovery.spec.ts` tests (the ones
Expand Down
14 changes: 8 additions & 6 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ it. See [`documentation-process.md`](documentation-process.md).
How to search the catalog, read search results, and pick a printing for
a slot in your decklist.

The **Editor** page (the redesigned `/display` route - nav+footer redesign,
2026-07-22, renamed the nav link from "Display (beta)" to plain "Editor";
the classic `/editor` page is still reachable directly by URL but no longer
has a nav link of its own) search bar is dual-mode: an **Add / Browse**
The **Editor** page (the unified sheet+rail page, now served directly at
the `/editor` route as of the 2026-07-23 Proposal H switchover — the
2026-07-22 nav+footer redesign had only renamed the nav link to "Editor"
while it still pointed at a separate `/display` route; `/display` now
redirects to `/editor`, and the classic grid editor this replaces is no
longer reachable by any URL) search bar is dual-mode: an **Add / Browse**
toggle next to the input switches between adding cards to your project
(the usual decklist-line paste/search box) and browsing the whole
catalog without touching your project — browsing renders a grid of
Expand Down Expand Up @@ -150,8 +152,8 @@ landing screen, the homepage's own CTA, or `/myDecks` directly).

## Saved decks, export, and the standalone decrypt tool

Signed in with Discord? The classic editor and the Editor page's Save
button persists your deck to your account, and the **My Decks** page lists
Signed in with Discord? The Editor page's Save button persists your deck
to your account, and the **My Decks** page lists
everything you've saved, decrypted right there in your browser — the
server only ever stores encrypted, opaque bytes it can't read.

Expand Down
9 changes: 9 additions & 0 deletions frontend/src/components/ProjectEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
/**
* This component represents the complete MPC Autofill project editor, ready to
* drop into a page (as the only component). Must be wrapped with a Redux provider.
*
* UNROUTED as of the Proposal H switchover (2026-07-23, issues #231/#272) - `pages/editor.tsx`
* now mounts `DisplayPage` instead of this component; nothing in the app routes here anymore.
* Left in-tree deliberately (not deleted) - that's a separate later cleanup decision the
* switchover's own report calls out as still open, pending usage data/owner sign-off per the
* design doc's own migration-plan step 6. Several `beforeunload`/chunk-error-recovery comments
* elsewhere in the codebase still narrate this component's pre-swap behavior for historical
* context - see `DisplayPage.tsx`'s own ported copy of the beforeunload guard below for where
* that specific piece of behavior actually lives now.
*/

import React, { useEffect, useState } from "react";
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/features/display/DisplayPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ import Row from "react-bootstrap/Row";
import ToggleButton from "react-bootstrap/ToggleButton";
import ToggleButtonGroup from "react-bootstrap/ToggleButtonGroup";

import { isRecoveryReloadInFlight } from "@/common/chunkErrorRecovery";
import { Back, CardHeightMM, CardWidthMM, Front } from "@/common/constants";
import { getOrCreateAnonymousId } from "@/common/cookies";
import { doesSearchQueryFilterOnPrinting } from "@/common/processing";
Expand Down Expand Up @@ -1388,6 +1389,28 @@ export function DisplayPage() {
notifyPromoteDraftPrePrint: draftBackup.notifyPromoteDraftPrePrint,
});

// Proposal H switchover (2026-07-23, issues #231/#272) - ported verbatim from
// `ProjectEditor.tsx`'s own beforeunload guard, which this page replaces. That guard lived only
// in the classic component's function body, never extracted to a shared hook, so it did NOT
// "naturally inherit" onto this page the way most reused instruments did - without this block,
// the unrouted classic page taking the beforeunload warning with it would have been a silent,
// real safety-net regression (closing/reloading a tab with unsaved cards would warn no one).
// Must NOT fire for the app's own chunk-load-error recovery reload (chunkErrorRecovery.ts) -
// see ProjectEditor.tsx's own comment (still present there, component unrouted but left
// in-tree) for the full diagnosis this mirrors.
useEffect(() => {
const handler = (event: BeforeUnloadEvent) => {
if (!isProjectEmpty && !isRecoveryReloadInFlight()) {
event.preventDefault();
return false;
}
};
window.addEventListener("beforeunload", handler);
return () => {
window.removeEventListener("beforeunload", handler);
};
}, [isProjectEmpty]);

const [settings, setSettings] = useState<DisplaySheetSettings>(
DEFAULT_SHEET_SETTINGS
);
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/features/ui/Navbar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe("Navbar - nav+footer redesign (N1-N7)", () => {

expect(await screen.findByRole("link", { name: "Editor" })).toHaveAttribute(
"href",
"/display"
"/editor"
);
expect(
screen.getByRole("link", { name: "What's That Card?" })
Expand Down Expand Up @@ -96,7 +96,8 @@ describe("Navbar - nav+footer redesign (N1-N7)", () => {
expect(
screen.queryByRole("link", { name: "Download" })
).not.toBeInTheDocument();
// The old classic-editor label/route - "Editor" now names /display instead (N2).
// The old classic-editor label/route - "Editor" now names the unified page at /editor
// itself, post Proposal H switchover (2026-07-23) (N2).
expect(screen.queryByText("Display (beta)")).not.toBeInTheDocument();
});

Expand Down
Loading
Loading