/* 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 ? ( {design.name ) : (
Upload artwork
)}
{/* 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} ))} ); }