feat: replace bento grid with format cloud / tag wall in features section
All 70+ formats displayed as color-coded badges in a flowing cloud layout. Popular formats are larger, all badges animate in with staggered entrance. Category legend at top with colored dots. Hover effects on each badge.
This commit is contained in:
+123
-115
@@ -51,55 +51,68 @@ const flowSteps = [
|
|||||||
{ inputIcon: '\u{1F3B5}', inputLabel: '.FLAC', outputIcon: '\u{1F3A7}', outputLabel: '.MP3' },
|
{ inputIcon: '\u{1F3B5}', inputLabel: '.FLAC', outputIcon: '\u{1F3A7}', outputLabel: '.MP3' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ─── Bento Features ─── */
|
/* ─── Format Cloud Data ─── */
|
||||||
|
|
||||||
const bentoFeatures = [
|
type FormatCategory = 'image' | 'document' | 'audio' | 'video' | 'data';
|
||||||
{
|
|
||||||
icon: '\u{1F5BC}',
|
const categoryMeta: Record<FormatCategory, { label: string; color: string; colorLight: string }> = {
|
||||||
title: 'Images',
|
image: { label: 'Images', color: '#f472b6', colorLight: 'rgba(244,114,182,0.10)' },
|
||||||
desc: 'Convert between any image format with zero quality loss.',
|
document: { label: 'Documents', color: '#60a5fa', colorLight: 'rgba(96,165,250,0.10)' },
|
||||||
accent: '#f472b6',
|
audio: { label: 'Audio', color: '#a78bfa', colorLight: 'rgba(167,139,250,0.10)' },
|
||||||
accentLight: 'rgba(244,114,182,0.08)',
|
video: { label: 'Video', color: '#fb923c', colorLight: 'rgba(251,146,60,0.10)' },
|
||||||
formats: ['PNG', 'JPG', 'WebP', 'GIF', 'AVIF', 'SVG', 'PSD', 'HEIC', 'BMP', 'TIFF', 'ICO'],
|
data: { label: 'Data & Fonts', color: '#34d399', colorLight: 'rgba(52,211,153,0.10)' },
|
||||||
// Bento: tall left column
|
};
|
||||||
gridArea: 'images',
|
|
||||||
},
|
const allFormats: { name: string; cat: FormatCategory; popular?: boolean }[] = [
|
||||||
{
|
// Images
|
||||||
icon: '\u{1F4C4}',
|
{ name: 'PNG', cat: 'image', popular: true },
|
||||||
title: 'Documents',
|
{ name: 'JPG', cat: 'image', popular: true },
|
||||||
desc: 'Full formatting preservation across office and web formats.',
|
{ name: 'WebP', cat: 'image', popular: true },
|
||||||
accent: '#60a5fa',
|
{ name: 'GIF', cat: 'image' },
|
||||||
accentLight: 'rgba(96,165,250,0.08)',
|
{ name: 'AVIF', cat: 'image' },
|
||||||
formats: ['DOCX', 'PDF', 'MD', 'HTML', 'TXT', 'RTF', 'PPTX', 'EPUB'],
|
{ name: 'SVG', cat: 'image', popular: true },
|
||||||
gridArea: 'docs',
|
{ name: 'PSD', cat: 'image' },
|
||||||
},
|
{ name: 'HEIC', cat: 'image', popular: true },
|
||||||
{
|
{ name: 'BMP', cat: 'image' },
|
||||||
icon: '\u{1F3B5}',
|
{ name: 'TIFF', cat: 'image' },
|
||||||
title: 'Audio',
|
{ name: 'ICO', cat: 'image' },
|
||||||
desc: 'FFmpeg WebAssembly powered.',
|
// Documents
|
||||||
accent: '#a78bfa',
|
{ name: 'PDF', cat: 'document', popular: true },
|
||||||
accentLight: 'rgba(167,139,250,0.08)',
|
{ name: 'DOCX', cat: 'document', popular: true },
|
||||||
formats: ['MP3', 'WAV', 'OGG', 'FLAC', 'AAC', 'M4A'],
|
{ name: 'MD', cat: 'document' },
|
||||||
gridArea: 'audio',
|
{ name: 'HTML', cat: 'document', popular: true },
|
||||||
},
|
{ name: 'TXT', cat: 'document' },
|
||||||
{
|
{ name: 'RTF', cat: 'document' },
|
||||||
icon: '\u{1F3AC}',
|
{ name: 'PPTX', cat: 'document', popular: true },
|
||||||
title: 'Video',
|
{ name: 'EPUB', cat: 'document' },
|
||||||
desc: 'Full transcoding in your browser.',
|
// Audio
|
||||||
accent: '#fb923c',
|
{ name: 'MP3', cat: 'audio', popular: true },
|
||||||
accentLight: 'rgba(251,146,60,0.08)',
|
{ name: 'WAV', cat: 'audio', popular: true },
|
||||||
formats: ['MP4', 'WebM', 'AVI', 'MOV', 'MKV'],
|
{ name: 'OGG', cat: 'audio' },
|
||||||
gridArea: 'video',
|
{ name: 'FLAC', cat: 'audio', popular: true },
|
||||||
},
|
{ name: 'AAC', cat: 'audio' },
|
||||||
{
|
{ name: 'M4A', cat: 'audio' },
|
||||||
icon: '\u{1F4CA}',
|
// Video
|
||||||
title: 'Data & Fonts',
|
{ name: 'MP4', cat: 'video', popular: true },
|
||||||
desc: 'Smart structure preservation for structured data and typography.',
|
{ name: 'WebM', cat: 'video' },
|
||||||
accent: '#34d399',
|
{ name: 'AVI', cat: 'video' },
|
||||||
accentLight: 'rgba(52,211,153,0.08)',
|
{ name: 'MOV', cat: 'video', popular: true },
|
||||||
formats: ['CSV', 'JSON', 'XML', 'YAML', 'XLSX', 'TSV', 'TOML', 'TTF', 'OTF', 'WOFF2'],
|
{ name: 'MKV', cat: 'video', popular: true },
|
||||||
gridArea: 'data',
|
// Data & Fonts
|
||||||
},
|
{ name: 'CSV', cat: 'data', popular: true },
|
||||||
|
{ name: 'JSON', cat: 'data', popular: true },
|
||||||
|
{ name: 'XML', cat: 'data' },
|
||||||
|
{ name: 'YAML', cat: 'data', popular: true },
|
||||||
|
{ name: 'XLSX', cat: 'data', popular: true },
|
||||||
|
{ name: 'TSV', cat: 'data' },
|
||||||
|
{ name: 'TOML', cat: 'data' },
|
||||||
|
{ name: 'INI', cat: 'data' },
|
||||||
|
{ name: 'NDJSON', cat: 'data' },
|
||||||
|
{ name: 'SQL', cat: 'data' },
|
||||||
|
{ name: 'TTF', cat: 'data' },
|
||||||
|
{ name: 'OTF', cat: 'data' },
|
||||||
|
{ name: 'WOFF', cat: 'data' },
|
||||||
|
{ name: 'WOFF2', cat: 'data' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ─── Animation Variants ─── */
|
/* ─── Animation Variants ─── */
|
||||||
@@ -454,7 +467,7 @@ export default function LandingPage() {
|
|||||||
<ConversionFlow />
|
<ConversionFlow />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ──── FEATURES — BENTO GRID ──── */}
|
{/* ──── FEATURES — FORMAT CLOUD ──── */}
|
||||||
<section
|
<section
|
||||||
id="features"
|
id="features"
|
||||||
className="relative z-10 flex flex-col items-center gap-10 px-6 py-20"
|
className="relative z-10 flex flex-col items-center gap-10 px-6 py-20"
|
||||||
@@ -473,77 +486,72 @@ export default function LandingPage() {
|
|||||||
Every format you need
|
Every format you need
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[17px] text-text-mid leading-relaxed max-w-[520px]">
|
<p className="text-[17px] text-text-mid leading-relaxed max-w-[520px]">
|
||||||
70+ file formats across 5 categories, all converted instantly with zero quality loss.
|
70+ file formats across 5 categories, all converted instantly in your browser.
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Bento Grid — asymmetric masonry layout */}
|
{/* Category legend */}
|
||||||
<div
|
<motion.div
|
||||||
className="w-full max-w-[1060px] grid gap-4 md:gap-5"
|
className="flex flex-wrap items-center justify-center gap-x-5 gap-y-2"
|
||||||
style={{
|
initial={{ opacity: 0 }}
|
||||||
gridTemplateColumns: 'repeat(12, 1fr)',
|
whileInView={{ opacity: 1 }}
|
||||||
gridTemplateRows: 'auto auto auto',
|
viewport={{ once: true }}
|
||||||
gridTemplateAreas: `
|
transition={{ delay: 0.2 }}
|
||||||
"images images images images images docs docs docs docs docs docs docs"
|
>
|
||||||
"audio audio audio audio video video video video data data data data"
|
{(Object.entries(categoryMeta) as [FormatCategory, typeof categoryMeta[FormatCategory]][]).map(([key, meta]) => (
|
||||||
"audio audio audio audio video video video video data data data data"
|
<div key={key} className="flex items-center gap-2">
|
||||||
`,
|
<div className="w-2.5 h-2.5 rounded-full" style={{ background: meta.color }} />
|
||||||
|
<span className="font-mono text-[11px] font-semibold text-text-mid tracking-wide">{meta.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Format cloud */}
|
||||||
|
<motion.div
|
||||||
|
className="w-full max-w-[820px] flex flex-wrap items-center justify-center gap-2.5"
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="visible"
|
||||||
|
viewport={{ once: true, margin: '-40px' }}
|
||||||
|
variants={{
|
||||||
|
hidden: {},
|
||||||
|
visible: { transition: { staggerChildren: 0.02 } },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{bentoFeatures.map((feat, i) => (
|
{allFormats.map((fmt) => {
|
||||||
<motion.div
|
const meta = categoryMeta[fmt.cat];
|
||||||
key={feat.title}
|
return (
|
||||||
className="relative bg-white rounded-[20px] overflow-hidden shadow-[0_1px_3px_rgba(160,120,80,0.06)] hover:shadow-[0_12px_40px_rgba(160,120,80,0.12)] hover:-translate-y-0.5 transition-all duration-300 group"
|
<motion.span
|
||||||
style={{
|
key={fmt.name}
|
||||||
gridArea: feat.gridArea,
|
className={`inline-flex items-center font-mono font-bold rounded-xl border cursor-default select-none transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md ${
|
||||||
borderLeft: `3px solid ${feat.accent}`,
|
fmt.popular
|
||||||
}}
|
? 'px-4 py-2 text-[13px] tracking-wide'
|
||||||
initial={{ opacity: 0, y: 28 }}
|
: 'px-3 py-1.5 text-[11px] tracking-wider'
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
}`}
|
||||||
viewport={{ once: true, margin: '-40px' }}
|
style={{
|
||||||
transition={{ duration: 0.5, delay: i * 0.08, ease: [0.16, 1, 0.3, 1] as const }}
|
color: meta.color,
|
||||||
>
|
background: meta.colorLight,
|
||||||
{/* Top accent bar */}
|
borderColor: `${meta.color}20`,
|
||||||
<div
|
}}
|
||||||
className="absolute top-0 left-0 right-0 h-[2px]"
|
variants={{
|
||||||
style={{ background: feat.accent, opacity: 0.4 }}
|
hidden: { opacity: 0, scale: 0.7, y: 12 },
|
||||||
/>
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
<div className="p-6 md:p-7 h-full flex flex-col">
|
scale: 1,
|
||||||
{/* Header row */}
|
y: 0,
|
||||||
<div className="flex items-start gap-3.5 mb-3">
|
transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] as const },
|
||||||
<div
|
},
|
||||||
className="w-11 h-11 rounded-xl flex items-center justify-center text-[20px] flex-shrink-0"
|
}}
|
||||||
style={{ background: feat.accentLight }}
|
whileHover={{
|
||||||
>
|
scale: 1.08,
|
||||||
{feat.icon}
|
background: `${meta.color}20`,
|
||||||
</div>
|
borderColor: `${meta.color}50`,
|
||||||
<div>
|
}}
|
||||||
<h3 className="font-serif font-bold text-[18px] text-text-dark leading-tight">{feat.title}</h3>
|
>
|
||||||
<p className="text-[13px] text-text-mid leading-relaxed mt-0.5">{feat.desc}</p>
|
.{fmt.name}
|
||||||
</div>
|
</motion.span>
|
||||||
</div>
|
);
|
||||||
|
})}
|
||||||
{/* Format badges — grow to fill space */}
|
</motion.div>
|
||||||
<div className="flex flex-wrap gap-1.5 mt-auto pt-3">
|
|
||||||
{feat.formats.map((f) => (
|
|
||||||
<span
|
|
||||||
key={f}
|
|
||||||
className="px-2.5 py-[5px] font-mono text-[10px] font-bold rounded-lg border transition-colors duration-200"
|
|
||||||
style={{
|
|
||||||
borderColor: `${feat.accent}25`,
|
|
||||||
color: feat.accent,
|
|
||||||
background: feat.accentLight,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
.{f}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Total count callout */}
|
{/* Total count callout */}
|
||||||
<motion.p
|
<motion.p
|
||||||
|
|||||||
Reference in New Issue
Block a user