From b3823717fd368b7d9ae308cf890de32ad631b3cf Mon Sep 17 00:00:00 2001 From: Matt Evans Date: Thu, 18 Jun 2026 13:44:42 -0700 Subject: [PATCH] fix(ui): render card preview symbols --- client/src/components/card/CardPreview.tsx | 40 ++++++++++++++----- .../card/__tests__/CardPreview.test.tsx | 39 +++++++++++++++++- .../components/modal/TriggerOrderModal.tsx | 3 +- .../__tests__/TriggerOrderModal.test.tsx | 22 +++++++++- client/src/components/zone/CommandZone.tsx | 8 ++-- 5 files changed, 92 insertions(+), 20 deletions(-) diff --git a/client/src/components/card/CardPreview.tsx b/client/src/components/card/CardPreview.tsx index 61e96d12b5..34cf6bdc9a 100644 --- a/client/src/components/card/CardPreview.tsx +++ b/client/src/components/card/CardPreview.tsx @@ -13,6 +13,7 @@ import type { CardRuling } from "../../services/engineRuntime.ts"; import { useGameStore } from "../../stores/gameStore.ts"; import { useUiStore } from "../../stores/uiStore.ts"; import { ManaCostPips } from "../mana/ManaCostPips.tsx"; +import { RichLabel } from "../mana/RichLabel.tsx"; import { GameplayTooltip } from "../ui/GameplayTooltip.tsx"; import { computePTDisplay, formatCounterType, formatTypeLine, toRoman } from "../../viewmodel/cardProps.ts"; import { @@ -676,7 +677,8 @@ function DetailPills({ details, badgeClass }: { details: [string, string][]; bad
{details.map(([key, value]) => ( - {key}: {value} + {key}:{" "} + ))}
@@ -701,11 +703,19 @@ function ParsedItemRow({ item, depth = 0 }: { item: ParsedItem; depth?: number } {CATEGORY_ABBR[item.category]} - {item.label} + {!item.supported && {t("preview.unsupported")}} {item.source_text && ( -
{item.source_text}
+ )} @@ -880,13 +890,18 @@ function CardInfoPanel({ )} {/* Type line */}
- {formatTypeLine(obj.card_types, obj.keywords)} +
{activateLabels.length > 0 && (
{activateLabels.map((label) => ( -
{t("preview.activateCost", { cost: label })}
+ ))}
)} @@ -906,7 +921,7 @@ function CardInfoPanel({ aria-describedby={tooltipId} className={`group relative cursor-default rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-white/60 ${granted ? "text-indigo-300" : "text-white"}`} > - {getKeywordDisplayText(kw)} + {source && ( {t("preview.fromSource", { source })} @@ -914,7 +929,7 @@ function CardInfoPanel({ )} {reminder && ( - {reminder} + )} @@ -976,12 +991,15 @@ function CardInfoPanel({ {chosenAttributes.map((attribute, index) => { const formatted = formatChosenAttribute(attribute); return ( -
- {t("preview.chosen.entry", { + + size="xs" + className="block" + /> ); })}
@@ -1012,7 +1030,7 @@ function RulingsSection({ rulings }: { rulings: CardRuling[] }) { {visible.map((ruling, i) => (
  • [{ruling.date}] - {ruling.text} +
  • ))} diff --git a/client/src/components/card/__tests__/CardPreview.test.tsx b/client/src/components/card/__tests__/CardPreview.test.tsx index cb6ecfc902..ef046fa439 100644 --- a/client/src/components/card/__tests__/CardPreview.test.tsx +++ b/client/src/components/card/__tests__/CardPreview.test.tsx @@ -90,7 +90,7 @@ afterEach(() => { vi.clearAllMocks(); Object.defineProperty(window, "innerWidth", { configurable: true, writable: true, value: 1280 }); Object.defineProperty(window, "innerHeight", { configurable: true, writable: true, value: 768 }); - useGameStore.setState({ gameState: null, spellCosts: {} }); + useGameStore.setState({ gameState: null, spellCosts: {}, legalActionsByObject: {} }); useUiStore.setState({ inspectedObjectId: null, altHeld: false }); }); @@ -130,11 +130,46 @@ describe("CardPreview chosen attributes", () => { render(); expect(screen.getByText("Flying")).toBeInTheDocument(); - expect(screen.getByText("Ward {2}")).toHaveAttribute("aria-describedby"); + expect(screen.getByText("Ward").closest("[aria-describedby]")).not.toBeNull(); + expect(screen.getAllByAltText("2").length).toBeGreaterThan(0); expect(screen.getByText(/creatures with flying or reach/)).toBeInTheDocument(); expect(screen.getByText(/ward cost/)).toBeInTheDocument(); }); + it("renders mana symbols in battlefield preview ability text", () => { + const object = battlefieldObject({ + abilities: [ + { + description: "{G}, {T}: Add {G}.", + effects: [], + targets: [], + cost: { type: "Tap" }, + timing: "AnyTime", + kind: "Activated", + }, + ], + }); + useGameStore.setState({ + gameState: gameStateWithObject(object), + legalActionsByObject: { + [String(object.id)]: [ + { + type: "ActivateAbility", + data: { source_id: object.id, ability_index: 0 }, + }, + ], + }, + spellCosts: {}, + }); + useUiStore.setState({ inspectedObjectId: object.id, altHeld: false }); + + render(); + + expect(screen.getByText(/Activate/)).toBeInTheDocument(); + expect(screen.getAllByAltText("T").length).toBeGreaterThan(0); + expect(screen.getAllByAltText("G").length).toBeGreaterThan(0); + }); + it("passes token lookup metadata to the mobile preview image hook", () => { Object.defineProperty(window, "innerWidth", { configurable: true, writable: true, value: 500 }); const object = battlefieldObject({ diff --git a/client/src/components/modal/TriggerOrderModal.tsx b/client/src/components/modal/TriggerOrderModal.tsx index d8756abce4..9dbd4490aa 100644 --- a/client/src/components/modal/TriggerOrderModal.tsx +++ b/client/src/components/modal/TriggerOrderModal.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import type { PendingTriggerSummary } from "../../adapter/types.ts"; import { useInspectHoverProps } from "../../hooks/useInspectHoverProps.ts"; import { useGameStore } from "../../stores/gameStore.ts"; +import { RichLabel } from "../mana/RichLabel.tsx"; import { DialogShell } from "./DialogShell.tsx"; const EMPTY_TRIGGER_SUMMARIES: PendingTriggerSummary[] = []; @@ -90,7 +91,7 @@ export function TriggerOrderModal() { {trigger.description && (
    - {trigger.description} +
    )} diff --git a/client/src/components/modal/__tests__/TriggerOrderModal.test.tsx b/client/src/components/modal/__tests__/TriggerOrderModal.test.tsx index 8abd4efdeb..d3d4dff82c 100644 --- a/client/src/components/modal/__tests__/TriggerOrderModal.test.tsx +++ b/client/src/components/modal/__tests__/TriggerOrderModal.test.tsx @@ -6,7 +6,10 @@ import { isWaitingForHandled } from "../../../game/waitingForRegistry.ts"; import { useGameStore } from "../../../stores/gameStore.ts"; import { TriggerOrderModal } from "../TriggerOrderModal.tsx"; -function orderTriggersPrompt(sourceNames: [string, string]): WaitingFor { +function orderTriggersPrompt( + sourceNames: [string, string], + descriptions?: [string, string], +): WaitingFor { return { type: "OrderTriggers", data: { @@ -14,7 +17,7 @@ function orderTriggersPrompt(sourceNames: [string, string]): WaitingFor { triggers: sourceNames.map((sourceName, index) => ({ source_id: index + 1, source_name: sourceName, - description: `${sourceName} triggered ability`, + description: descriptions?.[index] ?? `${sourceName} triggered ability`, })), }, }; @@ -58,4 +61,19 @@ describe("TriggerOrderModal", () => { data: { order: [0, 1] }, }); }); + + it("renders mana symbols in trigger descriptions", () => { + useGameStore.setState({ + waitingFor: orderTriggersPrompt( + ["Llanowar Elves", "Soul Warden"], + ["{T}: Add {G}.", "Whenever another creature enters, gain 1 life."], + ), + dispatch: vi.fn().mockResolvedValue([]), + }); + + render(); + + expect(screen.getAllByAltText("T").length).toBeGreaterThan(0); + expect(screen.getAllByAltText("G").length).toBeGreaterThan(0); + }); }); diff --git a/client/src/components/zone/CommandZone.tsx b/client/src/components/zone/CommandZone.tsx index a6121b6f97..179db439ea 100644 --- a/client/src/components/zone/CommandZone.tsx +++ b/client/src/components/zone/CommandZone.tsx @@ -215,13 +215,13 @@ function EmblemCard({ group, label }: { group: GroupedEmblem; label: string }) { > ✦ -

    - {group.description} -

    + /> )}