mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-11 00:05:01 +00:00
site(warmup): calendar-grid ramp + smooth FAQ animation
- Replaced the line chart + bar chart with a 5-week x 7-day calendar grid. Each cell shows the day label, the warmup volume that day, and is shaded by intensity (sky-1 to sky-8). Weekends are skipped visibly. Side panel: 4 stat tiles (Day 1 send, daily ramp, ceiling reached at Day 30, ~550 total sends). - FAQ converted from native <details> to the smooth grid-template-rows animation pattern used on pricing: click expands panel from 0fr to 1fr over 300ms, plus icon rotates 0->45deg.
This commit is contained in:
+122
-156
@@ -86,180 +86,124 @@ const faq = [
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
THE RAMP · chart + week breakdown + day-by-day data
|
||||
THE RAMP · 30-day calendar grid (GitHub-style intensity)
|
||||
============================================================ -->
|
||||
<section class="relative -mt-32 md:-mt-40 pb-16 md:pb-24 z-10">
|
||||
<div class="container-page">
|
||||
<div class="rounded-[18px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden shadow-[0_30px_60px_-20px_rgba(2,32,71,0.25),0_8px_24px_-10px_rgba(15,23,42,0.06)]">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="px-7 py-5 border-b border-[color:var(--border)] grid md:grid-cols-[1fr_auto] gap-3 items-baseline">
|
||||
<div class="px-7 py-5 border-b border-[color:var(--border)] flex flex-wrap items-baseline justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-[10.5px] uppercase tracking-[0.18em] font-mono text-[#0284c7]">The ramp · 30 days</div>
|
||||
<div class="mt-1 text-[18px] font-semibold tracking-[-0.01em] text-heading">One mailbox, day 1 to ceiling.</div>
|
||||
<p class="mt-2 text-[13px] text-foreground/65 max-w-2xl">
|
||||
Each day the mailbox sends one more warmup message than yesterday. After 30 days at +1/day from a 10/day floor, it reaches the 40/day ceiling and holds. Total: 775 warmup sends in 30 days.
|
||||
</p>
|
||||
<div class="text-[10.5px] uppercase tracking-[0.18em] font-mono text-[#0284c7]">First 30 days · one mailbox</div>
|
||||
<div class="mt-1 text-[18px] font-semibold tracking-[-0.01em] text-heading">Day 1 sends 10. Day 30 sends 40. Every day in between is +1.</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-5 text-[11.5px] font-mono text-muted-foreground">
|
||||
<span class="inline-flex items-center gap-1.5"><span class="w-2.5 h-[2px] bg-[#0284c7]"></span>volume / day</span>
|
||||
<span class="inline-flex items-center gap-1.5"><span class="w-2.5 h-[2px] bg-foreground/30" style="border-top:1px dashed"></span>ceiling</span>
|
||||
<!-- Intensity legend -->
|
||||
<div class="flex items-center gap-2 text-[10.5px] font-mono text-muted-foreground">
|
||||
<span>Less</span>
|
||||
<div class="flex gap-0.5">
|
||||
<span class="w-4 h-4 rounded-[3px]" style="background:#e0f2fe"></span>
|
||||
<span class="w-4 h-4 rounded-[3px]" style="background:#7dd3fc"></span>
|
||||
<span class="w-4 h-4 rounded-[3px]" style="background:#38bdf8"></span>
|
||||
<span class="w-4 h-4 rounded-[3px]" style="background:#0284c7"></span>
|
||||
<span class="w-4 h-4 rounded-[3px]" style="background:#075985"></span>
|
||||
</div>
|
||||
<span>More</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Two-column body: chart left, week breakdown right -->
|
||||
<div class="grid lg:grid-cols-[1.6fr_1fr]">
|
||||
<!-- LEFT: bigger chart with milestones -->
|
||||
<div class="px-7 pt-6 pb-5 border-b lg:border-b-0 lg:border-r border-[color:var(--border)]">
|
||||
<svg viewBox="0 0 720 340" class="w-full h-[340px]" preserveAspectRatio="none" aria-label="Ramp curve from 10/day to 40/day over 30 days">
|
||||
<defs>
|
||||
<linearGradient id="rampFill" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0284c7" stop-opacity="0.18"/>
|
||||
<stop offset="100%" stop-color="#0284c7" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- 30-day calendar grid -->
|
||||
<div class="px-7 py-8">
|
||||
<div class="grid lg:grid-cols-[1fr_auto] gap-10 items-start">
|
||||
<!-- LEFT: 5 weeks × 7 days grid (with weekend skipping illustrated by greying out) -->
|
||||
<div>
|
||||
<!-- Day-of-week column headers -->
|
||||
<div class="grid grid-cols-[40px_repeat(7,minmax(0,1fr))] gap-1.5 mb-1.5">
|
||||
<div></div>
|
||||
{['M', 'T', 'W', 'T', 'F', 'S', 'S'].map((d, i) => (
|
||||
<div class={`text-[10px] font-mono uppercase tracking-[0.14em] text-center pb-1 ${i >= 5 ? 'text-muted-foreground/40' : 'text-muted-foreground/70'}`}>{d}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Horizontal grid + y labels -->
|
||||
{[10, 20, 30, 40].map((v) => (
|
||||
<g>
|
||||
<line x1="40" x2="700" y1={300 - (v / 50) * 280} y2={300 - (v / 50) * 280} stroke="#e6ebf2" stroke-width="1"/>
|
||||
<text x="32" y={300 - (v / 50) * 280 + 4} text-anchor="end" fill="#94a3b8" font-size="10" font-family="ui-monospace,monospace">{v}</text>
|
||||
</g>
|
||||
))}
|
||||
<!-- 5 weeks, each row -->
|
||||
{[0, 1, 2, 3, 4].map((wk) => (
|
||||
<div class="grid grid-cols-[40px_repeat(7,minmax(0,1fr))] gap-1.5 mb-1.5">
|
||||
<div class="text-[10.5px] font-mono text-muted-foreground/70 flex items-center">W{wk + 1}</div>
|
||||
{[0, 1, 2, 3, 4, 5, 6].map((dow) => {
|
||||
const calDay = wk * 7 + dow + 1; // 1..35
|
||||
const isWeekend = dow >= 5;
|
||||
// Map calendar day → business day (skip weekends), business day → volume
|
||||
const bizDayCount = Array.from({ length: calDay }, (_, i) => i).filter((d) => {
|
||||
const dDow = d % 7;
|
||||
return dDow < 5;
|
||||
}).length;
|
||||
const volume = Math.min(9 + bizDayCount, 40);
|
||||
const inactive = calDay > 30 || isWeekend;
|
||||
|
||||
<!-- Week dividers (vertical) -->
|
||||
{[7, 14, 21].map((d) => (
|
||||
<line x1={40 + (d / 30) * 660} x2={40 + (d / 30) * 660} y1="20" y2="300" stroke="#e6ebf2" stroke-width="1" stroke-dasharray="2 4"/>
|
||||
))}
|
||||
// Color tier
|
||||
let bg = '#f1f5f9';
|
||||
let textColor = '#94a3b8';
|
||||
if (!inactive) {
|
||||
if (volume >= 40) { bg = '#075985'; textColor = '#ffffff'; }
|
||||
else if (volume >= 30) { bg = '#0284c7'; textColor = '#ffffff'; }
|
||||
else if (volume >= 20) { bg = '#38bdf8'; textColor = '#0c4a6e'; }
|
||||
else if (volume >= 15) { bg = '#7dd3fc'; textColor = '#0c4a6e'; }
|
||||
else { bg = '#e0f2fe'; textColor = '#075985'; }
|
||||
} else if (isWeekend && calDay <= 30) {
|
||||
bg = '#f8fafc';
|
||||
textColor = '#cbd5e1';
|
||||
}
|
||||
|
||||
<!-- Ceiling line (dashed) -->
|
||||
<line x1="40" x2="700" y1={300 - (40 / 50) * 280} y2={300 - (40 / 50) * 280} stroke="#0c4a6e" stroke-width="1" stroke-dasharray="4 4" opacity="0.4"/>
|
||||
<text x="700" y={300 - (40 / 50) * 280 - 6} text-anchor="end" fill="#0c4a6e" font-size="10" font-family="ui-monospace,monospace" opacity="0.6">ceiling · 40 / day</text>
|
||||
|
||||
<!-- Ramp polygon + line -->
|
||||
<polygon
|
||||
points={'40,300 ' + Array.from({ length: 31 }, (_, day) => {
|
||||
const v = Math.min(10 + day, 40);
|
||||
const x = 40 + (day / 30) * 660;
|
||||
const y = 300 - (v / 50) * 280;
|
||||
return x + ',' + y;
|
||||
}).join(' ') + ' 700,300'}
|
||||
fill="url(#rampFill)"
|
||||
/>
|
||||
<polyline
|
||||
points={Array.from({ length: 31 }, (_, day) => {
|
||||
const v = Math.min(10 + day, 40);
|
||||
const x = 40 + (day / 30) * 660;
|
||||
const y = 300 - (v / 50) * 280;
|
||||
return x + ',' + y;
|
||||
}).join(' ')}
|
||||
fill="none"
|
||||
stroke="#0284c7"
|
||||
stroke-width="2"
|
||||
/>
|
||||
|
||||
<!-- Daily dots -->
|
||||
{Array.from({ length: 31 }, (_, day) => {
|
||||
const v = Math.min(10 + day, 40);
|
||||
const x = 40 + (day / 30) * 660;
|
||||
const y = 300 - (v / 50) * 280;
|
||||
return <circle cx={x} cy={y} r="2" fill="#0284c7"/>;
|
||||
})}
|
||||
|
||||
<!-- Milestone markers -->
|
||||
<g>
|
||||
<circle cx="40" cy={300 - (10 / 50) * 280} r="5" fill="white" stroke="#0284c7" stroke-width="2"/>
|
||||
<text x="50" y={300 - (10 / 50) * 280 - 8} fill="#0369a1" font-size="11" font-family="ui-monospace,monospace" font-weight="600">Day 1 · 10/d</text>
|
||||
</g>
|
||||
<g>
|
||||
<circle cx={40 + (7 / 30) * 660} cy={300 - (17 / 50) * 280} r="5" fill="white" stroke="#0284c7" stroke-width="2"/>
|
||||
<text x={40 + (7 / 30) * 660 + 8} y={300 - (17 / 50) * 280 - 8} fill="#0369a1" font-size="11" font-family="ui-monospace,monospace" font-weight="600">Wk 1 · 17/d</text>
|
||||
</g>
|
||||
<g>
|
||||
<circle cx={40 + (30 / 30) * 660} cy={300 - (40 / 50) * 280} r="5" fill="white" stroke="#0284c7" stroke-width="2"/>
|
||||
<text x={40 + (30 / 30) * 660 - 8} y={300 - (40 / 50) * 280 - 8} text-anchor="end" fill="#0369a1" font-size="11" font-family="ui-monospace,monospace" font-weight="600">Day 30 · 40/d</text>
|
||||
</g>
|
||||
|
||||
<!-- X-axis labels -->
|
||||
{[0, 7, 14, 21, 30].map((d) => (
|
||||
<text x={40 + (d / 30) * 660} y="320" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="ui-monospace,monospace">D{d}</text>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: week-by-week breakdown -->
|
||||
<div class="px-7 py-6">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.18em] font-mono text-muted-foreground mb-4">Week by week</div>
|
||||
<div class="space-y-px">
|
||||
{[
|
||||
{ wk: 'Week 1', range: 'D1–D7', start: 10, end: 16, total: 91, status: 'Initial ramp' },
|
||||
{ wk: 'Week 2', range: 'D8–D14', start: 17, end: 23, total: 140, status: 'Building reputation' },
|
||||
{ wk: 'Week 3', range: 'D15–D21', start: 24, end: 30, total: 189, status: 'Provider learning' },
|
||||
{ wk: 'Week 4', range: 'D22–D28', start: 31, end: 37, total: 238, status: 'Approaching ceiling' },
|
||||
{ wk: 'Week 5', range: 'D29–D30', start: 38, end: 40, total: 117, status: 'At ceiling', featured: true },
|
||||
].map((w) => (
|
||||
<div class={`grid grid-cols-[auto_1fr_auto] gap-3 px-3 py-2.5 rounded-[6px] items-baseline ${w.featured ? 'bg-[color:var(--sky-1)]/60' : 'hover:bg-[color:var(--surface-1)]'}`}>
|
||||
<div>
|
||||
<div class={`text-[12.5px] font-semibold ${w.featured ? 'text-[#0369a1]' : 'text-heading'}`}>{w.wk}</div>
|
||||
<div class="text-[10.5px] font-mono text-muted-foreground mt-0.5">{w.range}</div>
|
||||
</div>
|
||||
<div class="text-[11.5px] font-mono text-foreground/65 text-center">
|
||||
{w.start}<span class="text-muted-foreground/60 mx-0.5">→</span>{w.end}<span class="text-muted-foreground/55"> /d</span>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class={`text-[14px] font-semibold font-mono ${w.featured ? 'text-[#0369a1]' : 'text-heading'}`}>{w.total}</div>
|
||||
<div class="text-[10px] font-mono text-muted-foreground">total sends</div>
|
||||
</div>
|
||||
return (
|
||||
<div class="relative aspect-square">
|
||||
<div class="absolute inset-0 rounded-[6px] flex flex-col items-center justify-center" style={`background:${bg}`}>
|
||||
{calDay <= 30 ? (
|
||||
<>
|
||||
<div class="text-[9px] font-mono uppercase tracking-[0.1em]" style={`color:${textColor};opacity:0.7`}>D{calDay}</div>
|
||||
<div class="text-[14px] font-semibold font-mono" style={`color:${textColor}`}>
|
||||
{isWeekend ? '—' : volume}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div class="mt-4 flex flex-wrap items-center gap-x-5 gap-y-1 text-[11px] font-mono text-muted-foreground">
|
||||
<span class="inline-flex items-center gap-1.5"><span class="w-3 h-3 rounded-sm" style="background:#f8fafc"></span>Weekend · no send</span>
|
||||
<span class="inline-flex items-center gap-1.5"><span class="w-3 h-3 rounded-sm" style="background:#0284c7"></span>Active warmup day</span>
|
||||
<span class="inline-flex items-center gap-1.5"><span class="w-3 h-3 rounded-sm" style="background:#075985"></span>At ceiling</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 pt-5 border-t border-[color:var(--border)] grid grid-cols-2 gap-x-4 gap-y-3 text-[11.5px] font-mono">
|
||||
<div>
|
||||
<div class="text-[18px] font-semibold tracking-[-0.02em] text-heading">775</div>
|
||||
<div class="uppercase tracking-[0.16em] text-muted-foreground">Total sends · 30d</div>
|
||||
<!-- RIGHT: stat strip -->
|
||||
<div class="lg:w-[220px] grid grid-cols-2 lg:grid-cols-1 gap-3 lg:gap-2">
|
||||
<div class="rounded-[10px] bg-[color:var(--surface-1)] ring-1 ring-[color:var(--border)] p-4">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.18em] font-mono text-muted-foreground">Day 1 send</div>
|
||||
<div class="mt-1 text-[24px] font-semibold tracking-[-0.02em] text-heading font-mono">10<span class="text-[12px] text-muted-foreground ml-1">/d</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[18px] font-semibold tracking-[-0.02em] text-heading">30%</div>
|
||||
<div class="uppercase tracking-[0.16em] text-muted-foreground">Reply rate target</div>
|
||||
<div class="rounded-[10px] bg-[color:var(--surface-1)] ring-1 ring-[color:var(--border)] p-4">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.18em] font-mono text-muted-foreground">Daily ramp</div>
|
||||
<div class="mt-1 text-[24px] font-semibold tracking-[-0.02em] text-heading font-mono">+1<span class="text-[12px] text-muted-foreground ml-1">/d</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[18px] font-semibold tracking-[-0.02em] text-heading">~10m</div>
|
||||
<div class="uppercase tracking-[0.16em] text-muted-foreground">Avg send gap</div>
|
||||
<div class="rounded-[10px] bg-[color:var(--sky-1)]/70 ring-1 ring-[color:var(--sky-2)] p-4">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.18em] font-mono text-[#0284c7]">Ceiling reached</div>
|
||||
<div class="mt-1 text-[24px] font-semibold tracking-[-0.02em] text-[#0369a1] font-mono">Day 30</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[18px] font-semibold tracking-[-0.02em] text-heading">9–5</div>
|
||||
<div class="uppercase tracking-[0.16em] text-muted-foreground">Window · recipient TZ</div>
|
||||
<div class="rounded-[10px] bg-[color:var(--surface-1)] ring-1 ring-[color:var(--border)] p-4">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.18em] font-mono text-muted-foreground">Total sends</div>
|
||||
<div class="mt-1 text-[24px] font-semibold tracking-[-0.02em] text-heading font-mono">~550</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom: day-by-day mini table -->
|
||||
<div class="border-t border-[color:var(--border)] bg-[color:var(--surface-1)]/40">
|
||||
<div class="px-7 py-3 flex items-center justify-between text-[10.5px] uppercase tracking-[0.18em] font-mono text-muted-foreground">
|
||||
<span>Day-by-day volume</span>
|
||||
<span class="normal-case tracking-normal text-[11px]">Each cell = sends scheduled that day</span>
|
||||
</div>
|
||||
<div class="px-7 pb-5 overflow-x-auto">
|
||||
<div class="grid grid-cols-[repeat(30,minmax(28px,1fr))] gap-1 min-w-[820px]">
|
||||
{Array.from({ length: 30 }, (_, i) => {
|
||||
const day = i + 1;
|
||||
const v = Math.min(9 + day, 40);
|
||||
const pct = (v - 10) / 30;
|
||||
const atCeiling = v >= 40;
|
||||
return (
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<div
|
||||
class="w-full rounded-sm"
|
||||
style={`height: ${6 + pct * 36}px; background: ${atCeiling ? '#0c4a6e' : '#0284c7'}; opacity: ${0.45 + pct * 0.5};`}
|
||||
title={`Day ${day} · ${v}/d`}
|
||||
></div>
|
||||
<div class="text-[9px] font-mono text-muted-foreground">{day}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer rule -->
|
||||
<div class="px-7 py-3 border-t border-[color:var(--border)] bg-[color:var(--surface-1)]/60 text-[11.5px] font-mono text-muted-foreground">
|
||||
Weekends are skipped. Business-hour scheduling by recipient time zone. ~10 minute average gap between sends from the same mailbox.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -622,19 +566,41 @@ const faq = [
|
||||
Want the full mental model? Read the <a class="text-[#0369a1] hover:text-[#075985] font-medium" href="/learn/email-warmup/">warmup guide</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="divide-y divide-[color:var(--border)]">
|
||||
<div class="divide-y divide-[color:var(--border)]" data-faq>
|
||||
{faq.map(([q, a]) => (
|
||||
<details class="group py-5">
|
||||
<summary class="flex items-start justify-between gap-4 cursor-pointer list-none">
|
||||
<div class="faq-row py-2">
|
||||
<button type="button" class="faq-trigger group w-full flex items-start justify-between gap-4 py-3 text-left" aria-expanded="false">
|
||||
<span class="text-[15.5px] font-medium text-heading group-hover:text-[#0369a1] transition-colors">{q}</span>
|
||||
<span class="shrink-0 inline-flex items-center justify-center w-7 h-7 rounded-full bg-white ring-1 ring-[color:var(--border)] text-foreground/60 group-open:rotate-45 transition-transform">
|
||||
<span class="faq-icon shrink-0 inline-flex items-center justify-center w-7 h-7 rounded-full bg-white ring-1 ring-[color:var(--border)] text-foreground/60 transition-transform duration-300 ease-out">
|
||||
<Icon name="plus" size={12} />
|
||||
</span>
|
||||
</summary>
|
||||
<p class="mt-3 text-[14px] text-foreground/75 leading-relaxed max-w-2xl">{a}</p>
|
||||
</details>
|
||||
</button>
|
||||
<div class="faq-panel grid grid-rows-[0fr] transition-[grid-template-rows] duration-300 ease-out">
|
||||
<div class="overflow-hidden">
|
||||
<p class="pb-4 pr-12 text-[14px] text-foreground/75 leading-relaxed max-w-2xl">{a}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
const faq = document.querySelector('[data-faq]');
|
||||
if (!faq) return;
|
||||
faq.querySelectorAll('.faq-trigger').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
const row = btn.closest('.faq-row');
|
||||
const panel = row.querySelector('.faq-panel');
|
||||
const icon = btn.querySelector('.faq-icon');
|
||||
const open = btn.getAttribute('aria-expanded') === 'true';
|
||||
btn.setAttribute('aria-expanded', String(!open));
|
||||
panel.style.gridTemplateRows = open ? '0fr' : '1fr';
|
||||
if (icon) icon.style.transform = open ? 'rotate(0deg)' : 'rotate(45deg)';
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user