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}
-
+ />
)}