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
3 changes: 2 additions & 1 deletion client/src/components/card/GameCardPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function GameCardPreview() {

const inspectedObjectId = useUiStore((s) => s.inspectedObjectId);
const inspectedFaceIndex = useUiStore((s) => s.inspectedFaceIndex);
const previewPlacement = useUiStore((s) => s.previewPlacement);
const isDragging = useUiStore((s) => s.isDragging);
const shiftHeld = useUiStore((s) => s.shiftHeld);
// Card-preview behavior preference. In "shift" mode the preview only renders
Expand Down Expand Up @@ -59,7 +60,7 @@ export function GameCardPreview() {
cardName={previewSuppressed ? null : inspectedCardName}
objectId={inspectedObj?.id ?? null}
backFaceName={previewSuppressed ? null : inspectedOtherFaceName}
dockSide={cardPreviewMode === "side"}
dockSide={cardPreviewMode === "side" || previewPlacement === "side"}
handSourceObjectId={inspectedObj?.zone === "Hand" ? inspectedObj.id : null}
/>
);
Expand Down
12 changes: 12 additions & 0 deletions client/src/components/card/__tests__/GameCardPreview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ afterEach(() => {
useUiStore.setState({
inspectedObjectId: null,
inspectedFaceIndex: 0,
previewPlacement: "cursor",
isDragging: false,
mobileHandGesture: null,
shiftHeld: false,
Expand All @@ -81,6 +82,17 @@ describe("GameCardPreview", () => {
expect(screen.getAllByAltText("Pithing Needle").length).toBeGreaterThan(0);
});

it("docks a preview opened from a modal even when cursor-follow is preferred", () => {
inspect(battlefieldObject());
useUiStore.setState({ previewPlacement: "side" });

const { container } = render(<GameCardPreview />);

expect(container.querySelector<HTMLElement>("[data-card-preview]")).toHaveStyle({
right: "calc(env(safe-area-inset-right) + 1rem + var(--game-right-rail-offset, 0px))",
});
});

it("anchors the preview to the hand card hovered through PlayerHand", async () => {
const firstCard = gameObjectFactory
.withId(201)
Expand Down
9 changes: 5 additions & 4 deletions client/src/components/deck-builder/CardEntryRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { DeckEntry } from "../../services/deckParser";
import type { ParsedItem, UnsupportedCard } from "../../services/deckCompatibility";
import { hasAlternatePrintingsSync, resolveOracleIdSync } from "../../services/scryfall";
import { usePrintingsLoaded } from "../../hooks/usePrintingsLoaded";
import { mouseHoverPreview } from "./hoverPreview";
import { mouseHoverPreview, type CardHoverHandler } from "./hoverPreview";

const CATEGORY_COLORS: Record<string, string> = {
keyword: "text-sky-400",
Expand Down Expand Up @@ -57,7 +57,7 @@ export interface CardEntryRowProps {
* permitting the increment so a not-yet-loaded limit never blocks a legal
* add. */
canIncrement?: (name: string) => boolean;
onCardHover?: (cardName: string | null) => void;
onCardHover?: CardHoverHandler;
unsupported?: UnsupportedCard;
onChooseArt?: (cardName: string, x: number, y: number) => void;
/** When defined and the card is commander-eligible in the current format,
Expand Down Expand Up @@ -117,6 +117,7 @@ export function CardEntryRow({
const printingsLoaded = usePrintingsLoaded();
const oracleId = printingsLoaded ? resolveOracleIdSync(entry.name) : null;
const hasAlternates = oracleId ? hasAlternatePrintingsSync(oracleId) : false;
const hoverInfo = { name: entry.name, sourcePrinting: entry.sourcePrinting };
// Labelled "→ {target}" pill when the destination name is known (deck
// builder); bare directional arrow otherwise (BO3 sideboard modal). The
// labelled variant widens to fit text, so it overrides the square sizing.
Expand Down Expand Up @@ -147,8 +148,8 @@ export function CardEntryRow({
own actions. */}
<span
className={`${unsupported ? "text-amber-200/80" : "text-gray-300"} ${onCardHover ? "cursor-pointer" : ""}`}
onClick={() => onCardHover?.(entry.name)}
{...mouseHoverPreview(onCardHover, entry.name)}
onClick={() => onCardHover?.(hoverInfo)}
{...mouseHoverPreview(onCardHover, hoverInfo)}
>
<span className="mr-1 text-gray-500">{entry.count}x</span>
{entry.name}
Expand Down
11 changes: 6 additions & 5 deletions client/src/components/deck-builder/CardGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { scryfallLegalityKey, type ScryfallCard } from "../../services/scryfall"
import { useLongPress } from "../../hooks/useLongPress";
import type { BrowserLegalityFilter } from "./CardSearch";
import { LegalityBadge } from "./LegalityBadge";
import { mouseHoverPreview } from "./hoverPreview";
import { mouseHoverPreview, type CardHoverHandler } from "./hoverPreview";

interface CardGridProps {
cards: ScryfallCard[];
onAddCard: (card: ScryfallCard) => void;
onCardHover?: (cardName: string | null) => void;
onCardHover?: CardHoverHandler;
cardCounts?: Map<string, number>;
legalityFormat?: BrowserLegalityFilter;
}
Expand Down Expand Up @@ -61,7 +61,7 @@ interface CardGridTileProps {
count: number | undefined;
legalityFormat: BrowserLegalityFilter;
onAddCard: (card: ScryfallCard) => void;
onCardHover?: (cardName: string | null) => void;
onCardHover?: CardHoverHandler;
}

function CardGridTile({
Expand All @@ -77,12 +77,13 @@ function CardGridTile({
const formatLabel = legalityFormat === "all"
? t("grid.allFormats")
: legalityFormat.charAt(0).toUpperCase() + legalityFormat.slice(1);
const hoverInfo = { name: card.name, scryfallId: card.id };

// Touch model (mirrors MobileHandDrawer's DrawerCard): tap adds the card,
// long-press opens the preview. firedRef suppresses the click that follows a
// long-press so a long-press never also adds the card. Desktop is unaffected
// (handlers are touch-only; hover still drives the preview).
const { handlers, firedRef } = useLongPress(() => onCardHover?.(card.name));
const { handlers, firedRef } = useLongPress(() => onCardHover?.(hoverInfo));

const handleClick = () => {
if (firedRef.current) {
Expand All @@ -101,7 +102,7 @@ function CardGridTile({
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.15 }}
onClick={handleClick}
{...mouseHoverPreview(onCardHover, card.name)}
{...mouseHoverPreview(onCardHover, hoverInfo)}
{...handlers}
disabled={!legal}
title={legal ? t("grid.addCard", { name: card.name }) : t("grid.notLegal", { name: card.name, format: formatLabel })}
Expand Down
19 changes: 12 additions & 7 deletions client/src/components/deck-builder/CommanderPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getCombinedColorIdentity,
} from "./commanderUtils";
import { mouseHoverPreview } from "./hoverPreview";
import type { CardHoverHandler } from "./hoverPreview";

const WUBRG_COLORS = ["W", "U", "B", "R", "G"] as const;

Expand Down Expand Up @@ -35,7 +36,7 @@ interface CommanderPanelProps {
companionCandidates?: string[] | null;
onSetCompanion?: (cardName: string) => void;
onRemoveCompanion?: () => void;
onCardHover?: (cardName: string | null) => void;
onCardHover?: CardHoverHandler;
/** Engine evaluateDeckCompatibility reasons for the active format. */
formatValidationReasons?: string[];
}
Expand All @@ -62,6 +63,10 @@ export function CommanderPanel({
}: CommanderPanelProps) {
const { t } = useTranslation("deck-builder");
const identity = getCombinedColorIdentity(commanders, cardDataCache);
const hoverInfo = (name: string) => ({
name,
scryfallId: cardDataCache.get(name)?.id,
});
const totalCards = deck.reduce((sum, e) => sum + e.count, 0)
+ commanders.length
+ (signatureSpell ? 1 : 0);
Expand Down Expand Up @@ -92,7 +97,7 @@ export function CommanderPanel({
return (
<div
key={name}
{...mouseHoverPreview(onCardHover, name)}
{...mouseHoverPreview(onCardHover, hoverInfo(name))}
className="flex items-center justify-between rounded bg-purple-900/30 px-2 py-1.5"
>
<span className="text-sm font-medium text-purple-300">
Expand Down Expand Up @@ -136,7 +141,7 @@ export function CommanderPanel({
<button
key={name}
onClick={() => onSetCommander(name)}
{...mouseHoverPreview(onCardHover, name)}
{...mouseHoverPreview(onCardHover, hoverInfo(name))}
className="block w-full truncate rounded bg-purple-800/40 px-2 py-1 text-left text-xs text-purple-300 hover:bg-purple-700/40"
>
{name}
Expand All @@ -152,7 +157,7 @@ export function CommanderPanel({
</h5>
{signatureSpell ? (
<div
{...mouseHoverPreview(onCardHover, signatureSpell)}
{...mouseHoverPreview(onCardHover, hoverInfo(signatureSpell))}
className="flex items-center justify-between rounded bg-purple-900/30 px-2 py-1.5"
>
<span className="text-sm font-medium text-purple-300">{signatureSpell}</span>
Expand All @@ -172,7 +177,7 @@ export function CommanderPanel({
<button
key={name}
onClick={() => onSetSignatureSpell(name)}
{...mouseHoverPreview(onCardHover, name)}
{...mouseHoverPreview(onCardHover, hoverInfo(name))}
className="block min-h-11 w-full truncate rounded bg-purple-800/40 px-2 py-1 text-left text-xs text-purple-300 hover:bg-purple-700/40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-purple-300"
>
{name}
Expand All @@ -188,7 +193,7 @@ export function CommanderPanel({
</h5>
{companion ? (
<div
{...mouseHoverPreview(onCardHover, companion)}
{...mouseHoverPreview(onCardHover, hoverInfo(companion))}
className="flex items-center justify-between rounded bg-blue-900/30 px-2 py-1.5"
>
<span className="text-sm font-medium text-blue-300">{companion}</span>
Expand All @@ -206,7 +211,7 @@ export function CommanderPanel({
<button
key={name}
onClick={() => onSetCompanion(name)}
{...mouseHoverPreview(onCardHover, name)}
{...mouseHoverPreview(onCardHover, hoverInfo(name))}
className="block min-h-11 w-full truncate rounded bg-blue-800/40 px-2 py-1 text-left text-xs text-blue-300 hover:bg-blue-700/40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-300"
>
{name}
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/deck-builder/DeckBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import { DeckBuilderToolbar } from "./DeckBuilderToolbar";
import { DeckBuilderTabBar } from "./DeckBuilderTabBar";
import { panelId, tabId } from "./deckBuilderTabs";
import { useDeckBuilder } from "./useDeckBuilder";
import type { CardHoverInfo } from "../card/CardPreview";

interface DeckBuilderProps {
onCardHover?: (cardName: string | null, scryfallId?: string) => void;
onCardHover?: (card: CardHoverInfo | null) => void;
format: GameFormat;
onFormatChange: (format: GameFormat) => void;
initialDeckName?: string | null;
Expand Down
12 changes: 9 additions & 3 deletions client/src/components/deck-builder/DeckList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { ScryfallCard } from "../../services/scryfall";

import { MoveList } from "./MoveList";
import { mouseHoverPreview } from "./hoverPreview";
import type { CardHoverHandler } from "./hoverPreview";
import { groupAccent, groupKey, groupOrder, groupTitleKey, type GroupMode } from "./deckGrouping";
import { isMaybeboardPolicy, useSideboardPolicy } from "./useSideboardPolicy";

Expand All @@ -27,7 +28,7 @@ interface DeckListProps {
canIncrementCard: (name: string) => boolean;
onMoveCard: (name: string, from: "main" | "sideboard") => void;
onImport: (deck: ParsedDeck) => void;
onCardHover?: (cardName: string | null) => void;
onCardHover?: CardHoverHandler;
format?: string;
compatibility?: DeckCompatibilityResult | null;
onChooseArt?: (cardName: string, x: number, y: number) => void;
Expand Down Expand Up @@ -252,8 +253,13 @@ export function DeckList({
>
<span
className={`text-fuchsia-50 ${onCardHover ? "cursor-pointer" : ""}`}
onClick={() => onCardHover?.(name)}
{...mouseHoverPreview(onCardHover, name)}
onClick={() =>
onCardHover?.({ name, scryfallId: cardDataCache.get(name)?.id })
}
{...mouseHoverPreview(onCardHover, {
name,
scryfallId: cardDataCache.get(name)?.id,
})}
>
{name}
</span>
Expand Down
21 changes: 15 additions & 6 deletions client/src/components/deck-builder/DeckStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { usePreferencesStore } from "../../stores/preferencesStore";
import type { GameFormat } from "../../adapter/types";
import { DeckCardContextMenu } from "./DeckCardContextMenu";
import { PrintingPickerModal } from "./PrintingPickerModal";
import { mouseHoverPreview } from "./hoverPreview";
import { mouseHoverPreview, type CardHoverHandler } from "./hoverPreview";
import { groupAccent, groupKey, groupRank, groupTitleKey, type GroupMode } from "./deckGrouping";
import { isMaybeboardPolicy, useSideboardPolicy } from "./useSideboardPolicy";

Expand All @@ -26,7 +26,7 @@ interface DeckStackProps {
onRemoveCard: (name: string, section: "main" | "sideboard") => void;
onMoveCard: (name: string, from: "main" | "sideboard") => void;
onRemoveCommander: (cardName: string) => void;
onCardHover?: (cardName: string | null, scryfallId?: string) => void;
onCardHover?: CardHoverHandler;
/** Deck format — resolves the sideboard policy so the second section
* is labelled "Sideboard" or "Maybeboard" consistently with the list view. */
format?: GameFormat;
Expand All @@ -42,6 +42,7 @@ interface DeckStackItem {
section: DeckStackSection;
groupTitle: string;
sortKey: [number, number, string];
scryfallId?: string;
sourcePrinting?: SourcePrinting;
}

Expand Down Expand Up @@ -78,6 +79,7 @@ function createDeckStackItems(
commandersItems.push({
count: 1,
name,
scryfallId: card?.id,
section: "commander",
groupTitle: "",
sortKey: [0, card?.cmc ?? 0, name.toLowerCase()],
Expand All @@ -90,6 +92,7 @@ function createDeckStackItems(
mainItems.push({
count: entry.count,
name: entry.name,
scryfallId: card?.id,
sourcePrinting: entry.sourcePrinting,
section: "main",
groupTitle: groupTitleKey(mode, groupKey(mode, card)),
Expand All @@ -103,6 +106,7 @@ function createDeckStackItems(
sideboardItems.push({
count: entry.count,
name: entry.name,
scryfallId: card?.id,
sourcePrinting: entry.sourcePrinting,
section: "sideboard",
groupTitle: groupTitleKey(mode, groupKey(mode, card)),
Expand Down Expand Up @@ -173,7 +177,7 @@ function DeckStackCard({
onRemoveCard: (name: string, section: "main" | "sideboard") => void;
onMoveCard: (name: string, from: "main" | "sideboard") => void;
onRemoveCommander: (cardName: string) => void;
onCardHover?: (cardName: string | null, scryfallId?: string) => void;
onCardHover?: CardHoverHandler;
onContextMenu?: (cardName: string, x: number, y: number) => void;
}) {
const { t } = useTranslation("deck-builder");
Expand All @@ -182,6 +186,11 @@ function DeckStackCard({
const oracleId = printingsLoaded ? resolveOracleIdSync(item.name) : null;
const hasAlternates = oracleId ? hasAlternatePrintingsSync(oracleId) : false;
const isCommander = item.section === "commander";
const hoverInfo = {
name: item.name,
scryfallId: item.scryfallId,
sourcePrinting: item.sourcePrinting,
};
const showAddButton = item.section === "main";
// The commander isn't part of the main/maybeboard partition, so it has no
// move target. Main cards move out to the sideboard/maybeboard; second-section
Expand Down Expand Up @@ -224,8 +233,8 @@ function DeckStackCard({
style={{ zIndex, width: CARD_WIDTH }}
// Tap previews the card on touch; hover previews on mouse (guarded so the
// touch-compat mouseleave can't tear down the overlay the tap just opened).
onClick={() => onCardHover?.(item.name)}
{...mouseHoverPreview(onCardHover, item.name)}
onClick={() => onCardHover?.(hoverInfo)}
{...mouseHoverPreview(onCardHover, hoverInfo)}
onContextMenu={(e) => {
if (onContextMenu) {
e.preventDefault();
Expand Down Expand Up @@ -349,7 +358,7 @@ function DeckStackSectionLane({
onRemoveCard: (name: string, section: "main" | "sideboard") => void;
onMoveCard: (name: string, from: "main" | "sideboard") => void;
onRemoveCommander: (cardName: string) => void;
onCardHover?: (cardName: string | null, scryfallId?: string) => void;
onCardHover?: CardHoverHandler;
onContextMenu?: (cardName: string, x: number, y: number) => void;
}) {
const { t } = useTranslation("deck-builder");
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/deck-builder/MoveList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { UnsupportedCard } from "../../services/deckCompatibility";
import type { GroupAccent } from "./deckGrouping";

import { CardEntryRow } from "./CardEntryRow";
import type { CardHoverHandler } from "./hoverPreview";

function totalCards(entries: DeckEntry[]): number {
return entries.reduce((sum, e) => sum + e.count, 0);
Expand All @@ -22,7 +23,7 @@ export interface MoveListProps {
onIncrement?: (name: string, section: "main" | "sideboard") => void;
/** Forwarded to each row. See `CardEntryRowProps.canIncrement`. */
canIncrement?: (name: string) => boolean;
onCardHover?: (name: string | null) => void;
onCardHover?: CardHoverHandler;
unsupportedMap?: Map<string, UnsupportedCard>;
/** Render the section even when it has zero entries, showing `emptyHint`.
* Used for the always-visible sideboard target in the deck editor. */
Expand Down
5 changes: 3 additions & 2 deletions client/src/components/deck-builder/PrintingPickerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import { getCardPrintings } from "../../services/scryfall.ts";
import type { PrintingEntry } from "../../services/scryfall.ts";
import { usePreferencesStore } from "../../stores/preferencesStore.ts";
import { ModalPanelShell } from "../ui/ModalPanelShell";
import type { CardHoverHandler } from "./hoverPreview";

interface PrintingPickerModalProps {
cardName: string;
oracleId: string;
onCardHover?: (cardName: string | null, scryfallId?: string) => void;
onCardHover?: CardHoverHandler;
onClose: () => void;
}

Expand Down Expand Up @@ -140,7 +141,7 @@ export function PrintingPickerModal({
key={printing.id}
type="button"
onClick={() => handleSelect(printing)}
onMouseEnter={() => onCardHover?.(cardName, printing.id)}
onMouseEnter={() => onCardHover?.({ name: cardName, scryfallId: printing.id })}
onMouseLeave={() => onCardHover?.(null)}
className={`group relative overflow-hidden rounded-xl border transition-all ${
isSelected
Expand Down
Loading
Loading