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
46 changes: 39 additions & 7 deletions docs/features/foreign-order-resilience.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,12 +255,33 @@ already offered exactly as it always was for any slot.
- **Per-surface consent ruling** (owner's second security-review round):
self-import/own saved decks allowed by default; shared decks viewed by
others deny-by-default behind an explicit per-deck recipient opt-in with
a reversible "Hide" control. **Only the editor/self-import surface is
wired up in this pass** — `SharedDeckViewer.tsx` and any other read-only
viewer were not touched, so they simply don't synthesize orphan
CardDocuments at all yet (safe-by-omission: deny-by-default is the
correct posture there, just not yet built as an explicit opt-in flow).
Building that opt-in UI is future work, not a regression.
a reversible "Hide" control. **SHIPPED (editor-polish round, item 11,
2026-07-24)** — `SharedDeckViewer.tsx` now synthesizes orphan awareness
for a shared-deck recipient (still NOT a full `synthesizeOrphanCardDocument`
merge into Redux — this component stays local-state-only, per its own
module comment — just enough to detect a face whose `selectedImage`
passes `isLikelyDriveFileId` but wasn't resolved by `APIGetCards`, the
same "unindexed by this catalog" orphan definition Phase 1 already
uses): a `useConsentToast` prompt keyed `shared-deck-orphans:${shareId}`
(per-DECK, not per-identifier or global — a second shared deck asks
independently even in the same session), deny-by-default (decline or
dismiss both leave every orphan face behind a `🔒 External image hidden` placeholder, and NOTHING is fetched — not even the direct-Google
URL is built — until the recipient opts in), and a persistent "N
external images hidden — Review"/"Hide" banner for the reversibility
the base `useConsentToast` `Promise<boolean>` contract doesn't natively
offer (the banner's own local `imagesRevealed` boolean is independent
of the toast's one-shot stored decision — flipping it back and forth
never re-prompts or touches `sessionStorage`). The revealed image uses
`getOrphanSmallImageURL` directly (orphanCard.ts) — still never routed
through the image-CDN Worker/R2 bucket, same posture as the editor's own
orphan rendering. Test coverage: `SharedDeckViewer.test.tsx` (jest/RTL,
not Playwright — this is a plain local-state component with no
`PagePreview` sheet/rail chrome to drive through a page-load E2E flow).
**Still not built**: any OTHER read-only viewer this catalog might grow
later inherits nothing automatically — this is `SharedDeckViewer.tsx`
specifically, not a shared hook/component other future recipient
surfaces can mount directly (a real gap if a second such surface is
ever added, flagged here rather than silently assumed-covered).
- **Bleed normalization for orphans**: `PDF.tsx`'s
`isBleedNormalizationEligible` still gates on `sourceType === GoogleDrive || sourceType === LocalFile`, which an orphan (no
`sourceType`) never matches — an orphan's PDF embed uses the plain
Expand Down Expand Up @@ -310,6 +331,14 @@ already offered exactly as it always was for any slot.
`ParsedXmlImport`'s new `cardback` field and `parseXMLFile`'s gated
`setSelectedCardback` dispatch, fixing the Common Cardback panel bug (see
"Rendering surfaces & acceptance" above).
- `frontend/src/features/savedDecks/SharedDeckViewer.tsx` — (editor-polish
round, item 11, 2026-07-24) the shared-deck recipient's own orphan
detection + consent gate (`isOrphanFace`, the `useConsentToast` mount,
the `HiddenOrphanBadge`/`ExtBanner` presentation) — see "Per-surface
consent ruling" above for the full behaviour.
- `frontend/src/features/savedDecks/SharedDeckPage.tsx` — threads the
route's own `shareId` query param into `SharedDeckViewer`'s `shareId`
prop (the per-deck consent-key scope).
- Tests: `orphanCard.test.ts`, `processing.test.ts` (bracket-token cases),
`listenerMiddleware.test.ts` (both the per-slot AND, as of 2026-07-23, the
project-cardback listener), `ImportXML.test.ts` (front + the b:null
Expand All @@ -321,4 +350,7 @@ already offered exactly as it always was for any slot.
(both reported symptoms, end to end, on the unified `/display` page as of
the 2026-07-23 acceptance-surface correction, with screenshots, plus the
sheet's own `orphan-badge` assertions and a dedicated narrow-viewport case
added in the same follow-up).
added in the same follow-up), and (editor-polish round, item 11)
`SharedDeckViewer.test.tsx` (jest/RTL — consent prompt, decline-hides,
accept-reveals, the reversible banner toggle, and the per-deck-id
independence case).
52 changes: 52 additions & 0 deletions docs/features/grid-selector.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,58 @@ described above.
`onSlotContextMenu` (every other `PagePreview` caller, e.g.
`PDFGenerator`'s fast preview), renders with zero behavior change —
no cue, no long-press handlers, the browser's native menu untouched.
**Editor-polish round (EPcue, SPEC-editor-polish.md §D.8, 2026-07-24)**:
the cue grows `20×20` → `26×26` (glyph `13px` → `17px`), higher-contrast
(`rgba(11,21,32,.92)` bg, `1.5px #abb6c2` border, `#fff` glyph,
drop-shadow) so it reads over card art, and its render gate tightens
from "a context menu is wired" alone to "the slot holds a card **and**
a context menu is wired" — an empty slot now shows no cue at all. The
same round also ships the `⟲` flip button this bullet's own "future
selection-checkbox/flip button" note anticipated: top-right corner,
same `26×26` sizing/reveal behaviour as the cue, an additive
`onSlotFlip?(index)` prop plus a SEPARATE `content.flippable` flag
(deliberately independent of `content.imageUrl` — gating the flip
button on the CURRENTLY-effective face's own image, the same way the
cue is gated, would strand a user the moment they flip to a face with
no art of its own, since the very button that let them flip would
vanish along with the image). `DisplayPage.tsx` tracks a per-slot
`flippedPreviewSlots` set (sheet-local, preview-only — never touches
`activeFace`/selection state) so flipping one slot never affects any
other slot or the project's own Fronts/Backs view setting.
- <a id="ghost-tile-thumbnail"></a>**Ghost tile gains a thumbnail + `+N`
(EP1, SPEC-editor-polish.md §D.4, 2026-07-24)**: the "+N more
copies"/"Show fewer" ghost tile (the "already-link-styled" control the
bullet above this one references) used to be a plain dashed empty box
with text. It now renders the first hidden copy's own
`smallThumbnailUrl`, dimmed (`rgba(11,21,32,.62)` overlay), with a
centred `+N` and a "more copies" caption — a real preview of what's
being compressed, not just a bare count. Only the EXPAND ("+N") ghost
gets this treatment (`GhostThumb`/`GhostDim`/`GhostPlus`/`GhostCap`,
`SelectVersionResults.tsx`); the COLLAPSE ("−") ghost stays plain text
(nothing to preview there). Border REV: `1px rgba(235,235,235,.15)`
(was `1px dashed #abb6c2`).
- <a id="data-driven-sort"></a>**Data-driven Sort (EP7, SPEC-editor-polish.md
§D.4, REVISES RD2, 2026-07-24)**: the `.sortsel` `Form.Select` on the
`layout="stacked"` (funnel/rail) surface stops being the backend-driven
6-option `SortByOptions` list (`search.sortBy`/`dateCreatedDescending`
etc. — that select is untouched on the OTHER, `layout="sidebar"`/modal
path, which never had a funnel to begin with) and becomes a
client-side comparator over fields the response already carries: **
Confirmation status** (`canonicalCard` → `suggestedCanonicalCard` →
neither), **Resolution (DPI) high→low**, **File size low→high**,
**Pinned sources first** (reads the SAME `getLocalStoragePinnedSourcePks`
helper `SourcesAccordion.tsx` writes, re-read fresh on every Sort
change — not reactively synced mid-render if a pin is toggled
elsewhere in the rail without reselecting the ordering), and **Name
(A→Z)**. Only reorders the TOP-LEVEL canonical/non-canonical/unknown
groups — `selectVersionGrouping.ts`'s own section ordering and each
group's internal representative/rest ordering are untouched. "Community
vote weight" (the dispatch's original seventh ordering) needs a
per-card numeric weight the response doesn't carry yet
(`suggestedCanonicalCardConfidence` is a currently-always-`undefined`
seam) — owner-ruled (amendment 2, the same round): ship the five now,
render NOTHING for vote-weight until that seam lands (no disabled
placeholder).
- **Open items, not resolved here (owner call needed)**: (1) group 2's
sub-order beyond "frame type first" — this build picked
`altered-frame > custom-art > ai-art`
Expand Down
24 changes: 22 additions & 2 deletions frontend/src/components/AutofillCollapse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ interface AutofillCollapseProps {
* JumpToVersion/CardResultSet/GridSelectorFilters) renders with Bootstrap's own stock padding,
* byte-for-byte unchanged. */
headerPadding?: string;
/** Additive, optional (SPEC-editor-polish.md §D.3, EP3 - "the grey #4E5D6B header/pins/body
* band is killed -> dark #22303f throughout"). Overrides the header's own hardcoded `#4E5D6B`
* ONLY for the caller that supplies this - see the header's own comment for why that value is
* otherwise deliberately locked (owner ruling, 2026-07-23) and must not be edited in place.
* `undefined` (every caller except `SourcesAccordion.tsx`, EP3's own revision target) keeps
* the shared `#4E5D6B` default, byte-for-byte unchanged. */
headerBackground?: string;
/** Additive, optional (SPEC-editor-polish.md §D.3, same EP3 de-grey) - the body's own
* background; `undefined` (every non-Sources caller) keeps Bootstrap Card's own stock
* default, unchanged. */
bodyBackground?: string;
}

/**
Expand All @@ -55,6 +66,8 @@ export function AutofillCollapse({
pad = 0,
id,
headerPadding,
headerBackground,
bodyBackground,
}: AutofillCollapseProps) {
return (
<>
Expand All @@ -79,7 +92,7 @@ export function AutofillCollapse({
// the two tokens are one hex digit apart by design, not by accident. Do not "fix"
// this back to `#4e5d6c` again; see SPEC-display-left-rail.md §D.0 for the explicit
// note distinguishing them.
backgroundColor: "#4E5D6B",
backgroundColor: headerBackground ?? "#4E5D6B",
zIndex: zIndex + 1,
cursor: "pointer",
...(headerPadding != null ? { padding: headerPadding } : {}),
Expand All @@ -100,7 +113,14 @@ export function AutofillCollapse({
</button>
</Stack>
</Card.Header>
<Card.Body className={`p-0 m-0`}>
<Card.Body
className={`p-0 m-0`}
style={
bodyBackground != null
? { backgroundColor: bodyBackground }
: undefined
}
>
<Collapse in={expanded}>
{/* https://react-bootstrap.netlify.app/docs/utilities/transitions/#collapse */}
<div id={id != null ? `${id}-body` : undefined}>
Expand Down
Loading
Loading