feat: Add neon esports landing page with WCAG accessibility
- Create HeroSection with animated search bar and stat counters - Add LiveMatchTicker with auto-scrolling recent matches - Build FlashLeaderboard "Wall of Shame" with podium display - Implement FeatureShowcase with scroll-triggered animations - Add NeonCTA call-to-action section with trust badges - Create reusable NeonButton component with glow effects Accessibility improvements: - Add aria-labels, aria-hidden for decorative elements - Implement focus-visible ring styles for keyboard navigation - Support prefers-reduced-motion across all animations - Use semantic HTML (article, nav, dl) for screen readers - Improve color contrast ratios for WCAG compliance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
63
src/lib/components/landing/AnimatedCounter.svelte
Normal file
63
src/lib/components/landing/AnimatedCounter.svelte
Normal file
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import { tweened } from 'svelte/motion';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
duration?: number;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
format?: (value: number) => string;
|
||||
}
|
||||
|
||||
let {
|
||||
value,
|
||||
duration = 2000,
|
||||
prefix = '',
|
||||
suffix = '',
|
||||
format = (val: number) => Math.floor(val).toLocaleString()
|
||||
}: Props = $props();
|
||||
|
||||
const displayValue = tweened(0, {
|
||||
duration,
|
||||
easing: cubicOut
|
||||
});
|
||||
|
||||
let hasAnimated = false;
|
||||
let containerElement: HTMLElement;
|
||||
|
||||
onMount(() => {
|
||||
// Use Intersection Observer to start animation when visible
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting && !hasAnimated) {
|
||||
hasAnimated = true;
|
||||
displayValue.set(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
if (containerElement) {
|
||||
observer.observe(containerElement);
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
});
|
||||
|
||||
// Update the target value if it changes after initial animation
|
||||
$effect(() => {
|
||||
if (hasAnimated) {
|
||||
displayValue.set(value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<span bind:this={containerElement} class="tabular-nums">
|
||||
{prefix}{format($displayValue)}{suffix}
|
||||
</span>
|
||||
Reference in New Issue
Block a user