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' },
];
/* ─── Bento Features ─── */
/* ─── Format Cloud Data ─── */
const bentoFeatures = [
{
icon: '\u{1F5BC}',
title: 'Images',
desc: 'Convert between any image format with zero quality loss.',
accent: '#f472b6',
accentLight: 'rgba(244,114,182,0.08)',
formats: ['PNG', 'JPG', 'WebP', 'GIF', 'AVIF', 'SVG', 'PSD', 'HEIC', 'BMP', 'TIFF', 'ICO'],
// Bento: tall left column
gridArea: 'images',
},
{
icon: '\u{1F4C4}',
title: 'Documents',
desc: 'Full formatting preservation across office and web formats.',
accent: '#60a5fa',
accentLight: 'rgba(96,165,250,0.08)',
formats: ['DOCX', 'PDF', 'MD', 'HTML', 'TXT', 'RTF', 'PPTX', 'EPUB'],
gridArea: 'docs',
},
{
icon: '\u{1F3B5}',
title: 'Audio',
desc: 'FFmpeg WebAssembly powered.',
accent: '#a78bfa',
accentLight: 'rgba(167,139,250,0.08)',
formats: ['MP3', 'WAV', 'OGG', 'FLAC', 'AAC', 'M4A'],
gridArea: 'audio',
},
{
icon: '\u{1F3AC}',
title: 'Video',
desc: 'Full transcoding in your browser.',
accent: '#fb923c',
accentLight: 'rgba(251,146,60,0.08)',
formats: ['MP4', 'WebM', 'AVI', 'MOV', 'MKV'],
gridArea: 'video',
},
{
icon: '\u{1F4CA}',
title: 'Data & Fonts',
desc: 'Smart structure preservation for structured data and typography.',
accent: '#34d399',
accentLight: 'rgba(52,211,153,0.08)',
formats: ['CSV', 'JSON', 'XML', 'YAML', 'XLSX', 'TSV', 'TOML', 'TTF', 'OTF', 'WOFF2'],
gridArea: 'data',
},
type FormatCategory = 'image' | 'document' | 'audio' | 'video' | 'data';
const categoryMeta: Record<FormatCategory, { label: string; color: string; colorLight: string }> = {
image: { label: 'Images', color: '#f472b6', colorLight: 'rgba(244,114,182,0.10)' },
document: { label: 'Documents', color: '#60a5fa', colorLight: 'rgba(96,165,250,0.10)' },
audio: { label: 'Audio', color: '#a78bfa', colorLight: 'rgba(167,139,250,0.10)' },
video: { label: 'Video', color: '#fb923c', colorLight: 'rgba(251,146,60,0.10)' },
data: { label: 'Data & Fonts', color: '#34d399', colorLight: 'rgba(52,211,153,0.10)' },
};
const allFormats: { name: string; cat: FormatCategory; popular?: boolean }[] = [
// Images
{ name: 'PNG', cat: 'image', popular: true },
{ name: 'JPG', cat: 'image', popular: true },
{ name: 'WebP', cat: 'image', popular: true },
{ name: 'GIF', cat: 'image' },
{ name: 'AVIF', cat: 'image' },
{ name: 'SVG', cat: 'image', popular: true },
{ name: 'PSD', cat: 'image' },
{ name: 'HEIC', cat: 'image', popular: true },
{ name: 'BMP', cat: 'image' },
{ name: 'TIFF', cat: 'image' },
{ name: 'ICO', cat: 'image' },
// Documents
{ name: 'PDF', cat: 'document', popular: true },
{ name: 'DOCX', cat: 'document', popular: true },
{ name: 'MD', cat: 'document' },
{ name: 'HTML', cat: 'document', popular: true },
{ name: 'TXT', cat: 'document' },
{ name: 'RTF', cat: 'document' },
{ name: 'PPTX', cat: 'document', popular: true },
{ name: 'EPUB', cat: 'document' },
// Audio
{ name: 'MP3', cat: 'audio', popular: true },
{ name: 'WAV', cat: 'audio', popular: true },
{ name: 'OGG', cat: 'audio' },
{ name: 'FLAC', cat: 'audio', popular: true },
{ name: 'AAC', cat: 'audio' },
{ name: 'M4A', cat: 'audio' },
// Video
{ name: 'MP4', cat: 'video', popular: true },
{ name: 'WebM', cat: 'video' },
{ name: 'AVI', cat: 'video' },
{ name: 'MOV', cat: 'video', popular: true },
{ name: 'MKV', cat: 'video', popular: true },
// 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 ─── */
@@ -454,7 +467,7 @@ export default function LandingPage() {
<ConversionFlow />
</section>
{/* ──── FEATURES — BENTO GRID ──── */}
{/* ──── FEATURES — FORMAT CLOUD ──── */}
<section
id="features"
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
</h2>
<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>
</motion.div>
{/* Bento Grid — asymmetric masonry layout */}
<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) => (
{/* Category legend */}
<motion.div
key={feat.title}
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"
style={{
gridArea: feat.gridArea,
borderLeft: `3px solid ${feat.accent}`,
}}
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 }}
className="flex flex-wrap items-center justify-center gap-x-5 gap-y-2"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
{/* Top accent bar */}
<div
className="absolute top-0 left-0 right-0 h-[2px]"
style={{ background: feat.accent, opacity: 0.4 }}
/>
<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}
{(Object.entries(categoryMeta) as [FormatCategory, typeof categoryMeta[FormatCategory]][]).map(([key, meta]) => (
<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>
<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>
))}
</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 */}
<motion.p