diff --git a/web/src/components/CardView.tsx b/web/src/components/CardView.tsx
index c6fb638..710deda 100644
--- a/web/src/components/CardView.tsx
+++ b/web/src/components/CardView.tsx
@@ -1,3 +1,5 @@
+import { useState } from 'react'
+import { createPortal } from 'react-dom'
import type { Card } from '../types'
import { artFor } from '../petArt'
@@ -11,6 +13,8 @@ interface Props {
bonus?: number
damage?: number
dead?: boolean
+ // preview marks the floating magnified copy so it doesn't magnify itself.
+ preview?: boolean
}
// CardView renders one physical card: pets get a power badge and a colored
@@ -25,7 +29,11 @@ export function CardView({
bonus = 0,
damage = 0,
dead,
+ preview,
}: Props) {
+ // Hover magnifier: show a large floating copy beside the cursor. The
+ // preview copy itself opts out so it can't recurse.
+ const [hover, setHover] = useState<{ x: number; y: number } | null>(null)
const power = (card.power ?? 0) + bonus
const classes = [
'card',
@@ -39,12 +47,33 @@ export function CardView({
.filter(Boolean)
.join(' ')
+ // Place the magnified preview near the cursor, clamped into the viewport.
+ const previewEl =
+ hover && !preview
+ ? createPortal(
+
+
+
,
+ document.body,
+ )
+ : null
+
return (
setHover({ x: e.clientX, y: e.clientY })}
+ onMouseMove={preview ? undefined : (e) => setHover({ x: e.clientX, y: e.clientY })}
+ onMouseLeave={preview ? undefined : () => setHover(null)}
>
+ {previewEl}
{card.tier ? `T${card.tier}` : 'ยท'}
{card.suit && }
diff --git a/web/src/styles.css b/web/src/styles.css
index 43a1937..b0bb3b5 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -1339,3 +1339,41 @@ h3 {
flex-wrap: wrap;
gap: 6px;
}
+
+/* --- hover magnifier --- */
+
+.card-magnify {
+ position: fixed;
+ z-index: 200;
+ pointer-events: none;
+ transform: scale(1.5);
+ transform-origin: top left;
+ filter: drop-shadow(0 12px 24px rgba(0, 0, 0, 0.55));
+ animation: magnify-in 90ms ease-out;
+}
+
+/* The magnifier exists to be read, so let the preview card grow to fit its
+ full effect text instead of clipping like a fixed-height card. */
+.card-magnify .card {
+ height: auto;
+ min-height: 176px;
+ padding-bottom: 10px;
+}
+
+.card-magnify .card-effect {
+ flex: 0 0 auto;
+ overflow: visible;
+ align-items: center;
+ margin-bottom: 6px;
+}
+
+@keyframes magnify-in {
+ from {
+ opacity: 0;
+ transform: scale(1.1);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1.5);
+ }
+}