/* eslint-disable @next/next/no-img-element -- Artwork comes from MinIO CDN / data URLs; next/image is out of scope for the designer canvas. */
import { useEffect, useRef, useState } from 'react';
import { getFrame, getRarity } from './frames';
/** Natural render size of the card (5:7). Everything inside is px-based
* so screen preview and PNG export are pixel-identical. */
export const CARD_W = 420;
export const CARD_H = 588;
/**
* Live card renderer for the designer. Renders at CARD_W x CARD_H and is
* scaled to fit its container by CardPreview below. Fully inline-styled
* so html-to-image can rasterize it 1:1 during PNG export.
*/
export default function CardFrame({ design, innerRef }) {
const frame = getFrame(design.frame_id);
const rarity = getRarity(design.rarity);
const p = frame.palette;
const showPt = Boolean(design.power || design.toughness);
return (
{/* Title bar */}
{design.name || 'Untitled Card'}
{/* Artwork window */}
{design.artwork_url ? (

) : (
)}
{/* Type line + rarity gem */}
{design.card_type || '— Type —'}
{/* Rarity gem straddles art/type boundary */}
{/* Rules text box */}
{design.rules_text && (
{design.rules_text}
)}
{design.rules_text && design.actions && (
)}
{design.actions && (
{design.actions}
)}
{!design.rules_text && !design.actions && (
Description & actions appear here
)}
{showPt && (
{design.power || '0'} / {design.toughness || '0'}
)}
);
}
/**
* Scales CardFrame to fit the available width while preserving the
* natural 420x588 layout (transform keeps export coordinates intact).
*/
export function CardPreview({ design, innerRef, maxWidth = 420 }) {
const containerRef = useRef(null);
const [scale, setScale] = useState(1);
useEffect(() => {
const el = containerRef.current;
if (!el) return undefined;
const update = () => {
const available = Math.min(el.clientWidth, maxWidth);
setScale(Math.min(1, available / CARD_W));
};
update();
const observer = new ResizeObserver(update);
observer.observe(el);
return () => observer.disconnect();
}, [maxWidth]);
return (
);
}
/**
* Renders a mana cost string as pip circles. Accepts both scryfall
* braces ("{2}{R}{R}") and plain notation ("2RR"), plus arbitrary
* symbols for custom games.
*/
export function ManaPips({ cost, accent }) {
if (!cost || !cost.trim()) return null;
const tokens = /\{/.test(cost)
? (cost.match(/\{[^}]+\}/g) || []).map((t) => t.slice(1, -1))
: cost.split(/\s+/).flatMap((chunk) => chunk.split(''));
if (tokens.length === 0) return null;
return (
{tokens.slice(0, 8).map((token, i) => (
{token}
))}
);
}