Mini-sized cards show the rank and corner suit only, keeping the guide's example rows quiet at small sizes. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
123 lines
3.4 KiB
TypeScript
123 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import type { Card, Rank, Suit } from "@/lib/minigames/poker";
|
|
import { mapCardToSvg, useCardStyle } from "./cardStyle";
|
|
|
|
const RANK_LABELS: Record<Rank, string> = {
|
|
2: "2",
|
|
3: "3",
|
|
4: "4",
|
|
5: "5",
|
|
6: "6",
|
|
7: "7",
|
|
8: "8",
|
|
9: "9",
|
|
10: "10",
|
|
11: "J",
|
|
12: "Q",
|
|
13: "K",
|
|
14: "A",
|
|
};
|
|
|
|
const SUIT_SYMBOLS: Record<Suit, string> = {
|
|
s: "♠",
|
|
h: "♥",
|
|
d: "♦",
|
|
c: "♣",
|
|
};
|
|
|
|
const RED_SUITS = new Set<Suit>(["h", "d"]);
|
|
|
|
interface CardViewProps {
|
|
readonly card: Card | null;
|
|
readonly faceUp?: boolean;
|
|
readonly size?: "mini" | "sm" | "md";
|
|
readonly dealIndex?: number;
|
|
readonly className?: string;
|
|
readonly highlighted?: boolean;
|
|
readonly dimmed?: boolean;
|
|
}
|
|
|
|
export function CardView({
|
|
card,
|
|
faceUp = true,
|
|
size = "md",
|
|
dealIndex = 0,
|
|
className,
|
|
highlighted = false,
|
|
dimmed = false,
|
|
}: CardViewProps) {
|
|
const { style: cardStyle } = useCardStyle();
|
|
const lightFace = cardStyle === "light";
|
|
const classicFace = cardStyle === "classic";
|
|
const revealed = card !== null && faceUp;
|
|
const redSuitClass = lightFace ? "text-rose-600" : "text-rose-400";
|
|
const label = card
|
|
? `${RANK_LABELS[card.rank]} of ${
|
|
card.suit === "s"
|
|
? "spades"
|
|
: card.suit === "h"
|
|
? "hearts"
|
|
: card.suit === "d"
|
|
? "diamonds"
|
|
: "clubs"
|
|
}`
|
|
: "Face-down card";
|
|
|
|
return (
|
|
<span
|
|
className={cn(
|
|
"poker-card inline-block shrink-0",
|
|
size === "mini" && "h-10 w-7",
|
|
size === "sm" && "h-14 w-10",
|
|
size === "md" && "h-16 w-12 sm:h-20 sm:w-14",
|
|
dealIndex > 0 && "poker-card-deal",
|
|
highlighted &&
|
|
"relative z-10 brightness-110 ring-2 ring-amber-300 shadow-lg shadow-amber-300/35",
|
|
dimmed && "opacity-40",
|
|
className,
|
|
)}
|
|
style={dealIndex > 0 ? { animationDelay: `${dealIndex * 60}ms` } : undefined}
|
|
aria-label={label}
|
|
>
|
|
<span className="poker-card-motion">
|
|
<span className={cn("poker-card-inner", revealed && "poker-card-is-face-up")}>
|
|
<span
|
|
className={cn(
|
|
"poker-card-face poker-card-front gap-1",
|
|
lightFace && "poker-card-face--light",
|
|
classicFace && "poker-card-front--svg",
|
|
)}
|
|
aria-hidden={!revealed}
|
|
>
|
|
{card && classicFace && (
|
|
<img src={mapCardToSvg(card)} alt="" draggable={false} className="poker-card-svg" />
|
|
)}
|
|
{card && !classicFace && (
|
|
<>
|
|
<span
|
|
className={cn(
|
|
"flex flex-col items-center leading-none",
|
|
RED_SUITS.has(card.suit) && redSuitClass,
|
|
)}
|
|
>
|
|
<span className="font-semibold">{RANK_LABELS[card.rank]}</span>
|
|
<span>{SUIT_SYMBOLS[card.suit]}</span>
|
|
</span>
|
|
{size !== "mini" && (
|
|
<span
|
|
className={cn("poker-card-center", RED_SUITS.has(card.suit) && redSuitClass)}
|
|
>
|
|
{SUIT_SYMBOLS[card.suit]}
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
</span>
|
|
<span className="poker-card-face poker-card-back" aria-hidden={revealed} />
|
|
</span>
|
|
</span>
|
|
</span>
|
|
);
|
|
}
|