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:
noah
2026-03-09 21:08:11 +01:00
parent dee839964b
commit a1e64225d5
+120 -112
View File
@@ -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
className="w-full max-w-[1060px] grid gap-4 md:gap-5"
style={{
gridTemplateColumns: 'repeat(12, 1fr)',
gridTemplateRows: 'auto auto auto',
gridTemplateAreas: `
"images images images images images docs docs docs docs docs docs docs"
"audio audio audio audio video video video video data data data data"
"audio audio audio audio video video video video data data data data"
`,
}}
>
{bentoFeatures.map((feat, i) => (
<motion.div <motion.div
key={feat.title} className="flex flex-wrap items-center justify-center gap-x-5 gap-y-2"
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" initial={{ opacity: 0 }}
style={{ whileInView={{ opacity: 1 }}
gridArea: feat.gridArea, viewport={{ once: true }}
borderLeft: `3px solid ${feat.accent}`, transition={{ delay: 0.2 }}
}}
initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-40px' }}
transition={{ duration: 0.5, delay: i * 0.08, ease: [0.16, 1, 0.3, 1] as const }}
> >
{/* Top accent bar */} {(Object.entries(categoryMeta) as [FormatCategory, typeof categoryMeta[FormatCategory]][]).map(([key, meta]) => (
<div <div key={key} className="flex items-center gap-2">
className="absolute top-0 left-0 right-0 h-[2px]" <div className="w-2.5 h-2.5 rounded-full" style={{ background: meta.color }} />
style={{ background: feat.accent, opacity: 0.4 }} <span className="font-mono text-[11px] font-semibold text-text-mid tracking-wide">{meta.label}</span>
/>
<div className="p-6 md:p-7 h-full flex flex-col">
{/* Header row */}
<div className="flex items-start gap-3.5 mb-3">
<div
className="w-11 h-11 rounded-xl flex items-center justify-center text-[20px] flex-shrink-0"
style={{ background: feat.accentLight }}
>
{feat.icon}
</div> </div>
<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>
</div>
</div>
{/* Format badges — grow to fill space */}
<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> </motion.div>
))}
</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 } },
}}
>
{allFormats.map((fmt) => {
const meta = categoryMeta[fmt.cat];
return (
<motion.span
key={fmt.name}
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 ${
fmt.popular
? 'px-4 py-2 text-[13px] tracking-wide'
: 'px-3 py-1.5 text-[11px] tracking-wider'
}`}
style={{
color: meta.color,
background: meta.colorLight,
borderColor: `${meta.color}20`,
}}
variants={{
hidden: { opacity: 0, scale: 0.7, y: 12 },
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] as const },
},
}}
whileHover={{
scale: 1.08,
background: `${meta.color}20`,
borderColor: `${meta.color}50`,
}}
>
.{fmt.name}
</motion.span>
);
})}
</motion.div>
{/* Total count callout */} {/* Total count callout */}
<motion.p <motion.p