Card filter: show only Universes Beyond cards

The Universes Beyond toggle now cycles off → hide → only, backed by a new
only_universes_beyond library filter. Choosing "only" counts as a real
query, so Card Search runs it on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 22:03:45 -05:00
parent f1f6591319
commit 04dc4f6e7b
4 changed files with 48 additions and 7 deletions

View File

@@ -53,6 +53,9 @@ fn push_card_filters(
if filter.exclude_universes_beyond {
sql.push_str(" AND oc.universes_beyond = 0");
}
if filter.only_universes_beyond {
sql.push_str(" AND oc.universes_beyond = 1");
}
if filter.exclude_digital_only {
sql.push_str(" AND oc.digital_only = 0");
}
@@ -387,7 +390,7 @@ mod tests {
}
#[test]
fn excluding_universes_beyond_hides_flagged_cards_only_when_asked() {
fn universes_beyond_filters_hide_or_keep_only_flagged_cards() {
let fx = owned_fixture();
// Stand in for what the ingest derives: a card that exists only as
// Universes Beyond.
@@ -410,6 +413,16 @@ mod tests {
// Off unless asked for — this must not quietly change existing searches.
let all = names(&fx.catalog.search_library(&LibraryFilter::default()).unwrap());
assert!(all.contains(&"Serra Angel".to_string()));
let f = LibraryFilter {
only_universes_beyond: true,
..Default::default()
};
assert_eq!(
names(&fx.catalog.search_library(&f).unwrap()),
vec!["Serra Angel"],
"only the flagged card is kept"
);
}
#[test]

View File

@@ -421,6 +421,10 @@ pub struct LibraryFilter {
/// existed, the same way the other reference-schema additions degrade.
#[serde(default)]
pub exclude_universes_beyond: bool,
/// The opposite: only cards that exist only as Universes Beyond. Inert
/// against an older `reference.db` in the same way.
#[serde(default)]
pub only_universes_beyond: bool,
/// Hide cards that exist only on Arena/MTGO (Alchemy rebalances and the
/// like) and so can never be in a physical collection.
#[serde(default)]

View File

@@ -201,6 +201,8 @@ export interface LibraryFilter {
* Warhammer 40,000, Marvel, …). Ordinary cards reprinted into a UB set are
* kept. Needs card data downloaded after this shipped; inert otherwise. */
exclude_universes_beyond?: boolean;
/** The opposite: only cards that exist only as Universes Beyond. */
only_universes_beyond?: boolean;
/** Hide Arena/MTGO-only cards (Alchemy rebalances) — they can't be owned on
* paper. Needs card data downloaded after this shipped; inert otherwise. */
exclude_digital_only?: boolean;

View File

@@ -2391,6 +2391,8 @@ const ICON_PACKAGE = icon(
*/
export type CardCriteria = {
excludeUb: boolean;
/** Never set together with `excludeUb`: the toggle cycles off → hide → only. */
onlyUb: boolean;
excludeDigital: boolean;
excludeFunny: boolean;
priceMin: string;
@@ -2410,6 +2412,7 @@ export type CardCriteria = {
export const EMPTY_CRITERIA: CardCriteria = {
excludeUb: false,
onlyUb: false,
excludeDigital: false,
excludeFunny: false,
priceMin: "",
@@ -2451,6 +2454,7 @@ const num = (s: string): number | null => (s.trim() === "" ? null : Number(s));
export function applyCardCriteria(c: CardCriteria): Partial<LibraryFilter> {
return {
exclude_universes_beyond: c.excludeUb,
only_universes_beyond: c.onlyUb,
exclude_digital_only: c.excludeDigital,
exclude_funny: c.excludeFunny,
price_min: num(c.priceMin),
@@ -2474,10 +2478,12 @@ export function applyCardCriteria(c: CardCriteria): Partial<LibraryFilter> {
* The three "exclude" toggles deliberately don't count: they refine a result
* set rather than select one, so ticking "hide Un-sets" on its own shouldn't
* make Card Search pull every card in the database. Choosing a format, a
* power range, or "not owned" is a real query, and does.
* power range, "not owned" or "only Universes Beyond" is a real query, and
* does.
*/
export function criteriaActive(c: CardCriteria): boolean {
return (
c.onlyUb ||
c.canBeCommander ||
c.vanillaOnly ||
c.notOwned ||
@@ -2611,13 +2617,29 @@ export function CardCriteriaFields({
<div className="icon-toggles">
{/* Exclusions read as "no X" — tinted with the danger colour when
active, so removing things looks different from selecting them. */}
{/* Three states: off → hide → only → off. "Only" looks like the
other "only" toggles rather than an exclusion. */}
<IconToggle
icon={ICON_CROSSOVER}
label="Hide Universes Beyond"
hint="Cards that only exist as another property — Lord of the Rings, Warhammer 40,000, Marvel. Normal cards reprinted in those sets stay."
exclude
on={c.excludeUb}
onToggle={() => onChange({ excludeUb: !c.excludeUb })}
label={
c.onlyUb
? "Only Universes Beyond (click to show all)"
: c.excludeUb
? "Hiding Universes Beyond (click for only them)"
: "Hide Universes Beyond"
}
hint="Cards that only exist as another property — Lord of the Rings, Warhammer 40,000, Marvel. Normal cards reprinted in those sets count as normal cards. Click again to show only these cards, and once more to show everything."
exclude={!c.onlyUb}
on={c.excludeUb || c.onlyUb}
onToggle={() =>
onChange(
c.onlyUb
? { excludeUb: false, onlyUb: false }
: c.excludeUb
? { excludeUb: false, onlyUb: true }
: { excludeUb: true, onlyUb: false },
)
}
/>
<IconToggle
icon={ICON_SCREEN}