From e7484e6caf2a47f677fba552bcf6ad249e5d984d Mon Sep 17 00:00:00 2001 From: heyradcode Date: Sat, 20 Jun 2026 21:54:26 -0400 Subject: [PATCH 01/73] feat: add redesign animation keyframes and design tokens --- frontend/src/index.css | 1 + frontend/src/styles/animations.css | 348 +++++++++++++++++++++++++++++ frontend/src/styles/variables.css | 44 ++++ 3 files changed, 393 insertions(+) create mode 100644 frontend/src/styles/animations.css diff --git a/frontend/src/index.css b/frontend/src/index.css index a729ca90..35315641 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,4 +1,5 @@ @import './styles/variables.css'; +@import './styles/animations.css'; @import './styles/messages.css'; :root { diff --git a/frontend/src/styles/animations.css b/frontend/src/styles/animations.css new file mode 100644 index 00000000..5df784f5 --- /dev/null +++ b/frontend/src/styles/animations.css @@ -0,0 +1,348 @@ +/* ============================================================================= + Keyframes — ported from the Crypto Pets redesign mock. + Shared vocabulary for the shell, gallery, and interaction views. + Names are kept 1:1 with the design so per-view styles can reference them. + ============================================================================= */ + +/* Ambient / shell */ +@keyframes cp-scanline { + from { + transform: translateY(-100%); + } + to { + transform: translateY(200vh); + } +} +@keyframes cp-orb-float { + 0%, + 100% { + transform: translateY(0) rotate(0deg); + } + 50% { + transform: translateY(-12px) rotate(4deg); + } +} +@keyframes cp-spin-slow { + to { + transform: rotate(360deg); + } +} +@keyframes cp-ray-spin { + to { + transform: rotate(360deg); + } +} + +/* Badges / glows */ +@keyframes cp-glow-pulse { + 0%, + 100% { + box-shadow: 0 0 14px rgb(255 215 0 / 25%); + } + 50% { + box-shadow: 0 0 32px rgb(255 215 0 / 55%); + } +} +@keyframes cp-streak-fire { + 0%, + 100% { + box-shadow: 0 0 10px rgb(255 207 112 / 25%); + } + 50% { + box-shadow: 0 0 22px rgb(255 123 203 / 45%); + } +} + +/* Cards / entrances */ +@keyframes cp-card-enter { + from { + transform: translateY(14px) scale(0.97); + } + to { + transform: none; + } +} +@keyframes cp-battle-enter { + from { + opacity: 0; + transform: scale(0.94); + } + to { + opacity: 1; + transform: none; + } +} +@keyframes cp-fade-up { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: none; + } +} +@keyframes cp-modal-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* Pets / avatars */ +@keyframes cp-float { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-9px); + } +} + +/* Battle */ +@keyframes cp-vs-pulse { + 0%, + 100% { + transform: scale(1); + filter: drop-shadow(0 0 18px #ff7bcb); + } + 50% { + transform: scale(1.14); + filter: drop-shadow(0 0 42px #ff7bcb); + } +} +@keyframes cp-attack-left { + 0% { + opacity: 0; + transform: translateX(-24px) scale(0.6); + } + 40% { + opacity: 1; + transform: translateX(8px) scale(1.4); + } + 100% { + opacity: 0; + transform: translateX(70px) scale(0.3); + } +} +@keyframes cp-attack-right { + 0% { + opacity: 0; + transform: translateX(24px) scale(0.6); + } + 40% { + opacity: 1; + transform: translateX(-8px) scale(1.4); + } + 100% { + opacity: 0; + transform: translateX(-70px) scale(0.3); + } +} +@keyframes cp-shake { + 0%, + 100% { + transform: translateX(0); + } + 20% { + transform: translateX(-8px); + } + 40% { + transform: translateX(8px); + } + 60% { + transform: translateX(-5px); + } + 80% { + transform: translateX(5px); + } +} +@keyframes cp-victory-burst { + 0% { + opacity: 0; + transform: scale(0.4) rotate(-8deg); + } + 65% { + opacity: 1; + transform: scale(1.08) rotate(3deg); + } + 100% { + opacity: 1; + transform: none; + } +} +@keyframes cp-defeat-drop { + from { + opacity: 0; + transform: scale(1.3) translateY(-30px); + } + to { + opacity: 1; + transform: none; + } +} + +/* Progress / bars */ +@keyframes cp-xp-fill { + from { + width: 0; + } +} +@keyframes cp-bar-grow { + from { + width: 0; + } +} + +/* Level up / forge */ +@keyframes cp-power-ring { + 0% { + transform: scale(0.8); + opacity: 0.9; + } + 100% { + transform: scale(2.6); + opacity: 0; + } +} + +/* Breeding — DNA helix */ +@keyframes cp-strand-glow { + 0%, + 100% { + filter: drop-shadow(0 0 3px rgb(125 214 255 / 40%)); + } + 50% { + filter: drop-shadow(0 0 9px rgb(125 214 255 / 90%)); + } +} +@keyframes cp-node-blink { + 0%, + 100% { + opacity: 0.65; + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(1.4); + } +} +@keyframes cp-compat-draw { + from { + stroke-dasharray: 0 251; + } +} + +/* Marriage */ +@keyframes cp-heart-float { + 0%, + 100% { + transform: translateY(0) scale(1); + } + 50% { + transform: translateY(-14px) scale(1.2); + } +} + +/* Training */ +@keyframes cp-intensity-bg { + 0%, + 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } +} + +/* Achievements */ +@keyframes cp-ach-pop { + 0% { + transform: translateX(140px); + opacity: 0; + } + 60% { + transform: translateX(-6px); + opacity: 1; + } + 100% { + transform: none; + opacity: 1; + } +} +@keyframes cp-ach-dismiss { + 0% { + transform: none; + opacity: 1; + } + 100% { + transform: translateX(140px); + opacity: 0; + } +} + +/* Gacha chest */ +@keyframes cp-chest-shake { + 0%, + 100% { + transform: rotate(0) scale(1); + } + 15% { + transform: rotate(-6deg) scale(1.04); + } + 30% { + transform: rotate(6deg) scale(1.04); + } + 45% { + transform: rotate(-5deg) scale(1.05); + } + 60% { + transform: rotate(5deg) scale(1.05); + } + 75% { + transform: rotate(-3deg) scale(1.06); + } +} +@keyframes cp-chest-burst { + 0% { + transform: scale(0.3); + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + transform: scale(2.6); + opacity: 0; + } +} +@keyframes cp-reward-rise { + 0% { + transform: translateY(30px) scale(0.5); + opacity: 0; + } + 60% { + transform: translateY(-6px) scale(1.12); + opacity: 1; + } + 100% { + transform: translateY(0) scale(1); + opacity: 1; + } +} + +/* ============================================================================= + Accessibility — respect reduced-motion for the heavy ambient/looping effects. + Entrance animations are allowed to run once; infinite loops are stilled. + ============================================================================= */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + } +} diff --git a/frontend/src/styles/variables.css b/frontend/src/styles/variables.css index e504548f..bc92fd88 100644 --- a/frontend/src/styles/variables.css +++ b/frontend/src/styles/variables.css @@ -52,6 +52,50 @@ ), radial-gradient(circle at 80% 20%, rgb(32 163 255 / 14%) 0%, transparent 44%), var(--neon-base-bg); + + /* Accent tokens introduced by the redesign (badges, streak, emerald HP, gold rank) */ + --neon-gold: #ffd700; + --neon-amber: #ffcf70; + --neon-emerald: #0fffae; + + /* Rarity ladder (common → legendary) */ + --rarity-common: #9ca3af; + --rarity-rare: #7dd6ff; + --rarity-epic: #b58cff; + --rarity-legendary: #ff7bcb; + + /* Panel surfaces used across shell + cards */ + --neon-surface: linear-gradient(160deg, rgb(10 16 34 / 97%), rgb(6 10 24 / 99%)); + --neon-surface-flat: rgb(10 16 32 / 88%); + --neon-inset: rgb(5 13 30 / 80%); + + /* Per-view radial background washes (content area accents by feature) */ + --wash-battle: radial-gradient(circle at 50% 110%, rgb(255 123 203 / 14%), transparent 55%), + var(--neon-base-bg); + --wash-breed: radial-gradient(circle at 50% 0%, rgb(255 207 112 / 13%), transparent 50%), + radial-gradient(circle at 20% 100%, rgb(181 140 255 / 8%), transparent 40%), + var(--neon-base-bg); + --wash-levelup: radial-gradient(circle at 50% 0%, rgb(181 140 255 / 16%), transparent 50%), + radial-gradient(circle at 50% 100%, rgb(255 215 0 / 10%), transparent 50%), + var(--neon-base-bg); + --wash-train: radial-gradient(circle at 20% 0%, rgb(125 214 255 / 12%), transparent 45%), + radial-gradient(circle at 80% 100%, rgb(15 255 174 / 8%), transparent 45%), + var(--neon-base-bg); + --wash-marriage: radial-gradient(circle at 50% 0%, rgb(255 123 203 / 15%), transparent 50%), + var(--neon-base-bg); + --wash-rename: radial-gradient(circle at 50% 0%, rgb(125 214 255 / 12%), transparent 45%), + var(--neon-base-bg); +} + +/* ============================================================================= + App shell dimensions (sidebar + header) + ============================================================================= */ +:root { + --shell-sidebar-collapsed: 72px; + --shell-sidebar-expanded: 236px; + --shell-header-height: 64px; + --shell-nav-item-height: 50px; + --shell-transition: 0.32s cubic-bezier(0.4, 0, 0.2, 1); } /* ============================================================================= From cdea83d3127e87b69955e90507e3cc6a2ed2204e Mon Sep 17 00:00:00 2001 From: heyradcode Date: Sat, 20 Jun 2026 21:55:51 -0400 Subject: [PATCH 02/73] feat: add ambient shell background layer --- .../src/components/layout/ambient/index.css | 39 ++++++ .../src/components/layout/ambient/index.tsx | 115 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 frontend/src/components/layout/ambient/index.css create mode 100644 frontend/src/components/layout/ambient/index.tsx diff --git a/frontend/src/components/layout/ambient/index.css b/frontend/src/components/layout/ambient/index.css new file mode 100644 index 00000000..f686f57f --- /dev/null +++ b/frontend/src/components/layout/ambient/index.css @@ -0,0 +1,39 @@ +/* Ambient shell background: particle canvas + scanline sweep + grid overlay. + Fills its positioned parent, sits behind content, never captures pointer. */ +.cp-ambient { + position: absolute; + inset: 0; + z-index: 0; + overflow: hidden; + pointer-events: none; + background: var(--neon-base-bg); +} + +.cp-ambient__canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + display: block; +} + +.cp-ambient__scanline { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100px; + background: linear-gradient(transparent, rgb(125 214 255 / 2.2%), transparent); + animation: cp-scanline 9s linear infinite; +} + +.cp-ambient__grid { + position: absolute; + inset: 0; + background: repeating-linear-gradient( + 0deg, + var(--neon-grid-line) 0 1px, + transparent 1px 48px + ), + repeating-linear-gradient(90deg, var(--neon-grid-line) 0 1px, transparent 1px 48px); +} diff --git a/frontend/src/components/layout/ambient/index.tsx b/frontend/src/components/layout/ambient/index.tsx new file mode 100644 index 00000000..c070fc0a --- /dev/null +++ b/frontend/src/components/layout/ambient/index.tsx @@ -0,0 +1,115 @@ +import React, { useEffect, useRef } from 'react'; + +import './index.css'; + +/** Tri-color neon particles rising through the shell background. */ +const PARTICLE_COLORS = ['#7dd6ff', '#b58cff', '#ff7bcb'] as const; +const PARTICLE_COUNT = 65; + +type Particle = { + x: number; + y: number; + vx: number; + vy: number; + r: number; + color: string; + alpha: number; + life: number; + maxLife: number; +}; + +const prefersReducedMotion = (): boolean => + typeof window !== 'undefined' && + window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true; + +const makeParticle = (width: number, height: number): Particle => ({ + x: Math.random() * width, + y: Math.random() * height, + vx: (Math.random() - 0.5) * 0.35, + vy: -(Math.random() * 0.55 + 0.15), + r: Math.random() * 1.6 + 0.4, + color: PARTICLE_COLORS[Math.floor(Math.random() * PARTICLE_COLORS.length)], + alpha: Math.random() * 0.55 + 0.12, + life: Math.random(), + maxLife: Math.random() * 0.75 + 0.45, +}); + +/** + * Ambient background for the app shell: a particle canvas plus a scanline sweep + * and a faint grid overlay (both pure CSS). Purely decorative and + * pointer-events:none, so it never intercepts interaction. Honors + * prefers-reduced-motion by rendering a single static frame instead of looping. + */ +const Ambient: React.FC = () => { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + let particles: Particle[] = []; + + const resize = () => { + const w = canvas.offsetWidth || window.innerWidth; + const h = canvas.offsetHeight || window.innerHeight; + canvas.width = w; + canvas.height = h; + particles = Array.from({ length: PARTICLE_COUNT }, () => makeParticle(w, h)); + }; + resize(); + + const draw = (advance: boolean) => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + for (const p of particles) { + if (advance) { + p.life -= 0.0025; + if (p.life <= 0) { + p.x = Math.random() * canvas.width; + p.y = canvas.height + 5; + p.life = p.maxLife; + p.alpha = Math.random() * 0.55 + 0.12; + } + p.x += p.vx; + p.y += p.vy; + } + ctx.beginPath(); + ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); + ctx.fillStyle = p.color; + ctx.globalAlpha = p.alpha * (p.life / p.maxLife); + ctx.fill(); + } + ctx.globalAlpha = 1; + }; + + if (prefersReducedMotion()) { + draw(false); + window.addEventListener('resize', resize); + return () => window.removeEventListener('resize', resize); + } + + let raf = 0; + const tick = () => { + draw(true); + raf = requestAnimationFrame(tick); + }; + tick(); + window.addEventListener('resize', resize); + + return () => { + cancelAnimationFrame(raf); + window.removeEventListener('resize', resize); + }; + }, []); + + return ( +