polish: Priority 2 improvements - smooth scroll, badges, back to top
🎯 Smooth Scroll Behavior: - Added smooth scroll CSS for seamless navigation - Scroll indicator now links to #tools section - Added hover states for scroll indicator - Accessibility: respects prefers-reduced-motion 🏷️ Tool Badges: - Added feature badges to each tool card - Vert: Privacy, Open Source, Free - Paint: Browser-Based, Free - Pastel: Open Source, WCAG, Free - Subtle glassmorphic badge design ⬆️ Back to Top Button: - Animated button appears after scrolling 300px - Smooth scroll to top on click - Hover tooltip with "Back to top" label - Scale animations on hover/tap - Progress bar at top showing scroll position - Gradient progress indicator ♿ Accessibility: - Added prefers-reduced-motion support - Proper ARIA labels - Keyboard accessible - Smooth animations respect user preferences 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--color-foreground);
|
color: var(--color-foreground);
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
@@ -50,6 +54,20 @@ body {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@utility text-balance {
|
@utility text-balance {
|
||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import Hero from '@/components/Hero';
|
|||||||
import Stats from '@/components/Stats';
|
import Stats from '@/components/Stats';
|
||||||
import ToolsGrid from '@/components/ToolsGrid';
|
import ToolsGrid from '@/components/ToolsGrid';
|
||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
|
import BackToTop from '@/components/BackToTop';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main className="relative min-h-screen">
|
<main className="relative min-h-screen">
|
||||||
<AnimatedBackground />
|
<AnimatedBackground />
|
||||||
|
<BackToTop />
|
||||||
<Hero />
|
<Hero />
|
||||||
<Stats />
|
<Stats />
|
||||||
<ToolsGrid />
|
<ToolsGrid />
|
||||||
|
|||||||
77
components/BackToTop.tsx
Normal file
77
components/BackToTop.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion, useScroll, useSpring } from 'framer-motion';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function BackToTop() {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const { scrollYProgress } = useScroll();
|
||||||
|
const scaleX = useSpring(scrollYProgress, {
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 30,
|
||||||
|
restDelta: 0.001,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const toggleVisibility = () => {
|
||||||
|
if (window.pageYOffset > 300) {
|
||||||
|
setIsVisible(true);
|
||||||
|
} else {
|
||||||
|
setIsVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', toggleVisibility);
|
||||||
|
return () => window.removeEventListener('scroll', toggleVisibility);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollToTop = () => {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Progress bar */}
|
||||||
|
<motion.div
|
||||||
|
className="fixed top-0 left-0 right-0 h-1 bg-gradient-to-r from-purple-500 to-cyan-500 transform origin-left z-50"
|
||||||
|
style={{ scaleX }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Back to top button */}
|
||||||
|
{isVisible && (
|
||||||
|
<motion.button
|
||||||
|
onClick={scrollToTop}
|
||||||
|
className="fixed bottom-8 right-8 p-4 rounded-full glass hover:bg-white/10 text-purple-400 hover:text-purple-300 transition-colors shadow-lg z-40 group"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
aria-label="Back to top"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Tooltip */}
|
||||||
|
<span className="absolute bottom-full right-0 mb-2 px-3 py-1 text-xs text-white bg-gray-900 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
|
||||||
|
Back to top
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -49,21 +49,22 @@ export default function Hero() {
|
|||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
{/* Scroll indicator */}
|
{/* Scroll indicator */}
|
||||||
<motion.div
|
<motion.a
|
||||||
className="flex flex-col items-center gap-2"
|
href="#tools"
|
||||||
|
className="flex flex-col items-center gap-2 cursor-pointer group"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.8, delay: 1 }}
|
transition={{ duration: 0.8, delay: 1 }}
|
||||||
>
|
>
|
||||||
<span className="text-sm text-gray-500">Explore Tools</span>
|
<span className="text-sm text-gray-500 group-hover:text-gray-400 transition-colors">Explore Tools</span>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-6 h-10 border-2 border-gray-600 rounded-full p-1"
|
className="w-6 h-10 border-2 border-gray-600 group-hover:border-purple-400 rounded-full p-1 transition-colors"
|
||||||
animate={{ y: [0, 10, 0] }}
|
animate={{ y: [0, 10, 0] }}
|
||||||
transition={{ duration: 1.5, repeat: Infinity }}
|
transition={{ duration: 1.5, repeat: Infinity }}
|
||||||
>
|
>
|
||||||
<div className="w-1 h-2 bg-gradient-to-b from-purple-400 to-cyan-400 rounded-full mx-auto" />
|
<div className="w-1 h-2 bg-gradient-to-b from-purple-400 to-cyan-400 rounded-full mx-auto" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ interface ToolCardProps {
|
|||||||
url: string;
|
url: string;
|
||||||
gradient: string;
|
gradient: string;
|
||||||
index: number;
|
index: number;
|
||||||
|
badges?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ToolCard({ title, description, icon, url, gradient, index }: ToolCardProps) {
|
export default function ToolCard({ title, description, icon, url, gradient, index, badges }: ToolCardProps) {
|
||||||
return (
|
return (
|
||||||
<motion.a
|
<motion.a
|
||||||
href={url}
|
href={url}
|
||||||
@@ -52,6 +53,20 @@ export default function ToolCard({ title, description, icon, url, gradient, inde
|
|||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
{/* Badges */}
|
||||||
|
{badges && badges.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
{badges.map((badge) => (
|
||||||
|
<span
|
||||||
|
key={badge}
|
||||||
|
className="text-xs px-2 py-1 rounded-full bg-white/5 border border-white/10 text-gray-400"
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<p className="text-gray-400 group-hover:text-gray-300 transition-colors duration-300">
|
<p className="text-gray-400 group-hover:text-gray-300 transition-colors duration-300">
|
||||||
{description}
|
{description}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const tools = [
|
|||||||
description: 'Privacy-focused file converter that processes images, audio, and documents locally on your device. No file size limits, completely open source.',
|
description: 'Privacy-focused file converter that processes images, audio, and documents locally on your device. No file size limits, completely open source.',
|
||||||
url: 'https://vert.kit.pivoine.art',
|
url: 'https://vert.kit.pivoine.art',
|
||||||
gradient: 'gradient-green-teal',
|
gradient: 'gradient-green-teal',
|
||||||
|
badges: ['Privacy', 'Open Source', 'Free'],
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
@@ -20,6 +21,7 @@ const tools = [
|
|||||||
description: 'An advanced image editor running in your browser. Edit photos, create graphics, and more.',
|
description: 'An advanced image editor running in your browser. Edit photos, create graphics, and more.',
|
||||||
url: 'https://paint.kit.pivoine.art',
|
url: 'https://paint.kit.pivoine.art',
|
||||||
gradient: 'gradient-orange-pink',
|
gradient: 'gradient-orange-pink',
|
||||||
|
badges: ['Browser-Based', 'Free'],
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
||||||
@@ -31,6 +33,7 @@ const tools = [
|
|||||||
description: 'Modern color manipulation toolkit with palette generation, accessibility testing, and format conversion. Supports hex, RGB, HSL, Lab, and more.',
|
description: 'Modern color manipulation toolkit with palette generation, accessibility testing, and format conversion. Supports hex, RGB, HSL, Lab, and more.',
|
||||||
url: 'https://pastel.kit.pivoine.art',
|
url: 'https://pastel.kit.pivoine.art',
|
||||||
gradient: 'gradient-indigo-purple',
|
gradient: 'gradient-indigo-purple',
|
||||||
|
badges: ['Open Source', 'WCAG', 'Free'],
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
||||||
@@ -44,7 +47,7 @@ const tools = [
|
|||||||
|
|
||||||
export default function ToolsGrid() {
|
export default function ToolsGrid() {
|
||||||
return (
|
return (
|
||||||
<section className="relative py-20 px-4">
|
<section id="tools" className="relative py-20 px-4">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
{/* Section heading */}
|
{/* Section heading */}
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -72,6 +75,7 @@ export default function ToolsGrid() {
|
|||||||
icon={tool.icon}
|
icon={tool.icon}
|
||||||
url={tool.url}
|
url={tool.url}
|
||||||
gradient={tool.gradient}
|
gradient={tool.gradient}
|
||||||
|
badges={tool.badges}
|
||||||
index={index}
|
index={index}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user