mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-30 00:15:48 +00:00
Merge pull request #46 from warmbly/feature/marketing-automation
feat: add automations marketing experience
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
---
|
||||
/**
|
||||
* Live demo of the Warmbly automation builder (/app/automations/:id).
|
||||
*
|
||||
* Pixel-faithful to web/src (PageTopbar + React-Flow canvas: TriggerNode /
|
||||
* ConditionNode / ActionNode, sky-600 chrome, dot grid at 24px), authored at a
|
||||
* fixed 1280px design width and uniformly scaled to fit its container.
|
||||
*
|
||||
* The demo is a 15s master timeline, all CSS keyframes on one shared duration:
|
||||
* a cursor presses Test, the insights panel slides in, and the run travels the
|
||||
* flow node by node: edges draw in, nodes flash as the run passes, result rows
|
||||
* check off in the panel, a Slack notification pops, the summary lands. The
|
||||
* "no" branch dims while the run takes the "yes" path. Clicking the real Test
|
||||
* button restarts the run from zero.
|
||||
*/
|
||||
const zapIcon = '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>';
|
||||
const gitBranchIcon = '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>';
|
||||
const briefcaseIcon = '<path d="M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/><rect width="20" height="14" x="2" y="6" rx="2"/>';
|
||||
const tagIcon = '<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/>';
|
||||
const sendIcon = '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.5.5 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/>';
|
||||
const historyIcon = '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>';
|
||||
const playIcon = '<polygon points="6 3 20 12 6 21 6 3"/>';
|
||||
const wandIcon = '<path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/>';
|
||||
const checkIcon = '<path d="M20 6 9 17l-5-5"/>';
|
||||
const checkCircleIcon = '<path d="M21.801 10A10 10 0 1 1 17 3.335"/><path d="m9 11 3 3L22 4"/>';
|
||||
const arrowLeftIcon = '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>';
|
||||
const plusIcon = '<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>';
|
||||
const xIcon = '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>';
|
||||
---
|
||||
<div class="ad-root relative rounded-[14px] overflow-hidden bg-white ring-1 ring-slate-900/[0.07]" style="box-shadow: 0 50px 110px -32px rgba(2,132,199,0.42), 0 14px 36px -16px rgba(15,23,42,0.18);">
|
||||
<!-- browser chrome -->
|
||||
<div class="flex items-center gap-2.5 px-3 h-11 border-b border-slate-200/80 bg-gradient-to-b from-[#f7f8fa] to-[#eceef1]">
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<span class="w-3 h-3 rounded-full bg-[#ff5f57]"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-[#febc2e]"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-[#28c840]"></span>
|
||||
</div>
|
||||
<div class="hidden sm:flex items-center gap-1 shrink-0 text-slate-400">
|
||||
<span class="grid place-items-center w-6 h-6 rounded-md"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg></span>
|
||||
<span class="grid place-items-center w-6 h-6 rounded-md text-slate-300"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></span>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 flex items-center gap-2 h-7 px-3 rounded-lg bg-white ring-1 ring-slate-200/90 text-[12px] shadow-[inset_0_1px_2px_rgba(15,23,42,0.04)]">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" class="text-emerald-500 shrink-0"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
<span class="truncate font-medium text-slate-700">app.warmbly.com<span class="text-slate-400 font-normal">/automations/reply-to-revenue</span></span>
|
||||
</div>
|
||||
<div class="hidden sm:flex items-center shrink-0 text-slate-300">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- scale-to-fit screen: authored at 1280px design width -->
|
||||
<div class="ad-fit relative w-full overflow-hidden bg-white min-h-[240px]">
|
||||
<div class="ad-screen absolute top-0 left-0 origin-top-left" style="width:1280px;">
|
||||
|
||||
<!-- ── Builder top bar ───────────────────────────────────────── -->
|
||||
<div class="h-12 px-3 flex items-center gap-2 border-b border-slate-200 bg-white">
|
||||
<span class="inline-flex h-7 w-7 items-center justify-center rounded-md text-slate-500"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={arrowLeftIcon}></svg></span>
|
||||
<span class="h-7 px-2 inline-flex items-center rounded-md text-[13px] font-medium text-slate-900">Reply to revenue</span>
|
||||
<span class="inline-flex h-7 items-center gap-2 ml-1">
|
||||
<span class="relative inline-flex h-[18px] w-8 shrink-0 items-center rounded-full bg-sky-600"><span class="inline-block size-3.5 translate-x-[16px] rounded-full bg-white shadow-sm"></span></span>
|
||||
<span class="text-[12px] font-medium text-slate-700">Active</span>
|
||||
</span>
|
||||
<div class="ml-auto flex items-center gap-1.5">
|
||||
<span class="inline-flex h-7 px-2.5 items-center gap-1.5 rounded-md border border-slate-200 text-slate-700 text-[12px]"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={historyIcon}></svg>History</span>
|
||||
<button type="button" class="ad-testbtn ad-anim relative inline-flex h-7 px-2.5 items-center gap-1.5 rounded-md border border-slate-200 text-slate-700 text-[12px] cursor-pointer select-none" aria-label="Replay the demo run">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={playIcon}></svg>
|
||||
<span class="relative inline-grid">
|
||||
<span class="ad-testlabel-a ad-anim col-start-1 row-start-1">Test</span>
|
||||
<span class="ad-testlabel-b ad-anim col-start-1 row-start-1 inline-flex items-center gap-1.5 text-sky-700">Running<span class="ad-spin inline-block size-2.5 rounded-full border-[1.5px] border-sky-300 border-t-sky-600"></span></span>
|
||||
</span>
|
||||
</button>
|
||||
<span class="inline-flex h-7 px-2.5 items-center gap-1.5 rounded-md border border-slate-200 text-slate-700 text-[12px]"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={wandIcon}></svg>Tidy up</span>
|
||||
<span class="inline-flex h-7 px-3 items-center gap-1.5 rounded-md bg-slate-100 text-slate-400 text-[12px] font-medium"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={checkIcon}></svg>Saved</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Canvas ────────────────────────────────────────────────── -->
|
||||
<div class="relative h-[560px] overflow-hidden bg-slate-50/40" style="background-image: radial-gradient(#e9eef5 1px, transparent 1px); background-size: 24px 24px;">
|
||||
|
||||
<!-- toolbar -->
|
||||
<div class="absolute left-3 top-3 z-20 flex items-center gap-1.5">
|
||||
<span class="inline-flex h-8 items-center gap-1.5 rounded-md bg-sky-600 px-2.5 text-[12px] font-medium text-white shadow-sm"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" set:html={plusIcon}></svg>Add action</span>
|
||||
<span class="inline-flex h-8 items-center gap-1.5 rounded-md border border-slate-200 bg-white px-2.5 text-[12px] font-medium text-slate-700 shadow-sm"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={gitBranchIcon}></svg>Add condition</span>
|
||||
</div>
|
||||
|
||||
<!-- canvas hint -->
|
||||
<div class="absolute bottom-3 left-1/2 -translate-x-1/2 z-0 rounded-md bg-white/95 px-3 py-1.5 shadow-sm text-[11px] text-slate-500 whitespace-nowrap">drag a node's dot to connect · IF block: right dot = yes, bottom dot = no</div>
|
||||
|
||||
<!-- edges -->
|
||||
<svg width="1280" height="560" viewBox="0 0 1280 560" class="absolute inset-0 pointer-events-none" aria-hidden="true">
|
||||
<defs>
|
||||
<marker id="ad-arrow-slate" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"><path d="M1 1 L9 5 L1 9 z" fill="#94a3b8"/></marker>
|
||||
<marker id="ad-arrow-sky" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"><path d="M1 1 L9 5 L1 9 z" fill="#0ea5e9"/></marker>
|
||||
</defs>
|
||||
<!-- base edges -->
|
||||
<path d="M 444 98 L 444 134" fill="none" stroke="#cbd5e1" stroke-width="1.5" marker-end="url(#ad-arrow-slate)"/>
|
||||
<path d="M 558 155 C 670 155, 764 168, 764 214" fill="none" stroke="#0ea5e9" stroke-width="1.5" marker-end="url(#ad-arrow-sky)"/>
|
||||
<path class="ad-dim ad-anim" d="M 444 176 C 444 215, 220 208, 220 254" fill="none" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#ad-arrow-slate)"/>
|
||||
<path d="M 764 302 L 764 338" fill="none" stroke="#cbd5e1" stroke-width="1.5" marker-end="url(#ad-arrow-slate)"/>
|
||||
<path d="M 764 422 L 764 458" fill="none" stroke="#cbd5e1" stroke-width="1.5" marker-end="url(#ad-arrow-slate)"/>
|
||||
<path class="ad-dim ad-anim" d="M 220 338 L 220 386" fill="none" stroke="#cbd5e1" stroke-width="1.5" marker-end="url(#ad-arrow-slate)"/>
|
||||
<!-- lit overlays: drawn in as the run travels -->
|
||||
<path class="ad-lit ad-anim" style="--len:60; animation-delay:0s" d="M 444 98 L 444 134" fill="none" stroke="#38bdf8" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path class="ad-lit ad-anim" style="--len:280; animation-delay:0.95s" d="M 558 155 C 670 155, 764 168, 764 214" fill="none" stroke="#7dd3fc" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path class="ad-lit ad-anim" style="--len:60; animation-delay:2.25s" d="M 764 302 L 764 338" fill="none" stroke="#38bdf8" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path class="ad-lit ad-anim" style="--len:60; animation-delay:3.45s" d="M 764 422 L 764 458" fill="none" stroke="#38bdf8" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
<!-- edge labels -->
|
||||
<span class="absolute z-10 inline-flex items-center rounded border border-sky-200 bg-white px-1.5 py-px text-[10px] font-medium text-sky-600 shadow-sm" style="left:632px; top:142px;">yes</span>
|
||||
<span class="ad-dim ad-anim absolute z-10 inline-flex items-center rounded border border-slate-200 bg-white px-1.5 py-px text-[10px] text-slate-500 shadow-sm" style="left:304px; top:192px;">no</span>
|
||||
|
||||
<!-- TriggerNode -->
|
||||
<div class="absolute" style="left:320px; top:22px; width:248px;">
|
||||
<div class="ad-flash ad-anim relative rounded-xl border border-slate-200 bg-white shadow-sm" style="animation-delay:0s">
|
||||
<div class="flex items-center gap-2 rounded-t-xl border-b border-slate-200/70 bg-gradient-to-r from-sky-50/80 to-white px-2.5 py-1.5">
|
||||
<span class="inline-flex size-5 shrink-0 items-center justify-center rounded-md bg-sky-100 text-sky-600 ring-1 ring-sky-200/70"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={zapIcon}></svg></span>
|
||||
<span class="min-w-0 flex-1 truncate text-[12.5px] font-semibold text-slate-800">Prospect replies</span>
|
||||
<span class="shrink-0 rounded bg-sky-600 px-1.5 py-px text-[9px] font-semibold uppercase tracking-[0.12em] text-white">Trigger</span>
|
||||
</div>
|
||||
<div class="px-2.5 py-2">
|
||||
<div class="text-[9.5px] font-semibold uppercase tracking-[0.12em] text-slate-300">When</div>
|
||||
<div class="mt-0.5 truncate font-mono text-[11px] text-slate-500">campaign.reply_received</div>
|
||||
</div>
|
||||
<span class="absolute -bottom-1.5 left-1/2 -translate-x-1/2 size-3 rounded-full bg-sky-500 border-2 border-white shadow-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ConditionNode -->
|
||||
<div class="absolute" style="left:334px; top:140px; width:220px;">
|
||||
<div class="ad-flash ad-anim relative rounded-lg border border-sky-200 bg-gradient-to-b from-sky-50 to-white px-2.5 py-1.5 shadow-sm" style="animation-delay:0.9s">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 text-sky-600" set:html={gitBranchIcon}></svg>
|
||||
<span class="text-[9.5px] font-semibold uppercase tracking-[0.12em] text-sky-500">if</span>
|
||||
<span class="min-w-0 flex-1 truncate text-[11px] font-medium text-sky-800">intent is positive · ≥ 80%</span>
|
||||
</div>
|
||||
<span class="absolute -right-1.5 top-1/2 -translate-y-1/2 size-3 rounded-full bg-sky-500 border-2 border-white shadow-sm"></span>
|
||||
<span class="absolute -bottom-1.5 left-1/2 -translate-x-1/2 size-3 rounded-full bg-slate-400 border-2 border-white shadow-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ActionNode: Slack -->
|
||||
<div class="absolute" style="left:640px; top:222px; width:248px;">
|
||||
<div class="ad-flash ad-anim relative rounded-xl border border-slate-200 bg-white shadow-sm" style="animation-delay:1.95s">
|
||||
<div class="flex items-center gap-2 rounded-t-xl border-b border-slate-200/70 bg-gradient-to-r from-slate-50 to-white px-2.5 py-1.5">
|
||||
<span class="inline-flex size-5 shrink-0 items-center justify-center rounded-md bg-fuchsia-50 text-fuchsia-600 ring-1 ring-fuchsia-200 text-[10px] font-bold">S</span>
|
||||
<span class="min-w-0 flex-1 truncate text-[12.5px] font-semibold text-slate-800">Send a Slack message</span>
|
||||
</div>
|
||||
<div class="px-2.5 py-2">
|
||||
<div class="text-[9.5px] font-semibold uppercase tracking-[0.12em] text-slate-300">Then</div>
|
||||
<div class="mt-0.5 truncate text-[11.5px] text-slate-500">#sales · reply from {`{{.contact_email}}`}</div>
|
||||
</div>
|
||||
<span class="absolute -top-1 left-1/2 -translate-x-1/2 size-2 rounded-full bg-slate-300 border-2 border-white"></span>
|
||||
<span class="absolute -bottom-1.5 left-1/2 -translate-x-1/2 size-3 rounded-full bg-sky-500 border-2 border-white shadow-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ActionNode: Create a deal -->
|
||||
<div class="absolute" style="left:640px; top:342px; width:248px;">
|
||||
<div class="ad-flash ad-anim relative rounded-xl border border-slate-200 bg-white shadow-sm" style="animation-delay:3.2s">
|
||||
<div class="flex items-center gap-2 rounded-t-xl border-b border-slate-200/70 bg-gradient-to-r from-slate-50 to-white px-2.5 py-1.5">
|
||||
<span class="inline-flex size-5 shrink-0 items-center justify-center rounded-md bg-sky-50 text-sky-600 ring-1 ring-sky-200/70"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={briefcaseIcon}></svg></span>
|
||||
<span class="min-w-0 flex-1 truncate text-[12.5px] font-semibold text-slate-800">Create a deal</span>
|
||||
</div>
|
||||
<div class="px-2.5 py-2">
|
||||
<div class="text-[9.5px] font-semibold uppercase tracking-[0.12em] text-slate-300">Then</div>
|
||||
<div class="mt-0.5 truncate text-[11.5px] text-slate-500">Discovery · $4,800</div>
|
||||
</div>
|
||||
<span class="absolute -top-1 left-1/2 -translate-x-1/2 size-2 rounded-full bg-slate-300 border-2 border-white"></span>
|
||||
<span class="absolute -bottom-1.5 left-1/2 -translate-x-1/2 size-3 rounded-full bg-sky-500 border-2 border-white shadow-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ActionNode: HubSpot -->
|
||||
<div class="absolute" style="left:640px; top:462px; width:248px;">
|
||||
<div class="ad-flash ad-anim relative rounded-xl border border-slate-200 bg-white shadow-sm" style="animation-delay:4.4s">
|
||||
<div class="flex items-center gap-2 rounded-t-xl border-b border-slate-200/70 bg-gradient-to-r from-slate-50 to-white px-2.5 py-1.5">
|
||||
<span class="inline-flex size-5 shrink-0 items-center justify-center rounded-md bg-orange-50 text-orange-600 ring-1 ring-orange-200 text-[10px] font-bold">H</span>
|
||||
<span class="min-w-0 flex-1 truncate text-[12.5px] font-semibold text-slate-800">Update HubSpot contact</span>
|
||||
</div>
|
||||
<div class="px-2.5 py-2">
|
||||
<div class="text-[9.5px] font-semibold uppercase tracking-[0.12em] text-slate-300">Then</div>
|
||||
<div class="mt-0.5 truncate text-[11.5px] text-slate-500">lifecycle stage · opportunity</div>
|
||||
</div>
|
||||
<span class="absolute -top-1 left-1/2 -translate-x-1/2 size-2 rounded-full bg-slate-300 border-2 border-white"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ActionNode: Add a tag (no branch, dims during the run) -->
|
||||
<div class="ad-dim ad-anim absolute" style="left:96px; top:258px; width:248px;">
|
||||
<div class="relative rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div class="flex items-center gap-2 rounded-t-xl border-b border-slate-200/70 bg-gradient-to-r from-slate-50 to-white px-2.5 py-1.5">
|
||||
<span class="inline-flex size-5 shrink-0 items-center justify-center rounded-md bg-emerald-50 text-emerald-600 ring-1 ring-emerald-200"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={tagIcon}></svg></span>
|
||||
<span class="min-w-0 flex-1 truncate text-[12.5px] font-semibold text-slate-800">Add a tag</span>
|
||||
</div>
|
||||
<div class="px-2.5 py-2">
|
||||
<div class="text-[9.5px] font-semibold uppercase tracking-[0.12em] text-slate-300">Then</div>
|
||||
<div class="mt-0.5 truncate text-[11.5px] text-slate-500">nurture</div>
|
||||
</div>
|
||||
<span class="absolute -top-1 left-1/2 -translate-x-1/2 size-2 rounded-full bg-slate-300 border-2 border-white"></span>
|
||||
<span class="absolute -bottom-1.5 left-1/2 -translate-x-1/2 size-3 rounded-full bg-sky-500 border-2 border-white shadow-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ActionNode: Webhook (no branch) -->
|
||||
<div class="ad-dim ad-anim absolute" style="left:96px; top:390px; width:248px;">
|
||||
<div class="relative rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div class="flex items-center gap-2 rounded-t-xl border-b border-slate-200/70 bg-gradient-to-r from-slate-50 to-white px-2.5 py-1.5">
|
||||
<span class="inline-flex size-5 shrink-0 items-center justify-center rounded-md bg-sky-50 text-sky-600 ring-1 ring-sky-200/70"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={sendIcon}></svg></span>
|
||||
<span class="min-w-0 flex-1 truncate text-[12.5px] font-semibold text-slate-800">Send a webhook</span>
|
||||
</div>
|
||||
<div class="px-2.5 py-2">
|
||||
<div class="text-[9.5px] font-semibold uppercase tracking-[0.12em] text-slate-300">Then</div>
|
||||
<div class="mt-0.5 truncate font-mono text-[11px] text-slate-500">hooks.yourapp.com/replies</div>
|
||||
</div>
|
||||
<span class="absolute -top-1 left-1/2 -translate-x-1/2 size-2 rounded-full bg-slate-300 border-2 border-white"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slack notification toast -->
|
||||
<div class="ad-toast ad-anim absolute left-5 bottom-5 z-30 w-[300px] rounded-[10px] bg-white shadow-[0_14px_36px_-12px_rgba(15,23,42,0.4)] ring-1 ring-slate-200 p-3" aria-hidden="true">
|
||||
<div class="flex items-start gap-2.5">
|
||||
<span class="inline-flex size-8 shrink-0 items-center justify-center rounded-md bg-fuchsia-50 text-fuchsia-600 ring-1 ring-fuchsia-200 text-[13px] font-bold">S</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[11.5px] font-semibold text-slate-900">#sales</span>
|
||||
<span class="text-[10px] text-slate-400 font-mono">now</span>
|
||||
</div>
|
||||
<p class="text-[11.5px] text-slate-500 mt-0.5 leading-snug"><span class="font-medium text-slate-700">Warmbly</span> maya@northwind.dev replied to Q1 outreach: “Thursday works for me”</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Insights panel: the test run, step by step ──────────── -->
|
||||
<div class="ad-panel ad-anim absolute inset-y-0 right-0 z-40 w-[340px] bg-white border-l border-slate-200 shadow-[-10px_0_34px_-14px_rgba(15,23,42,0.3)] flex flex-col" aria-hidden="true">
|
||||
<div class="h-11 px-3 flex items-center border-b border-slate-200 shrink-0">
|
||||
<span class="text-[12.5px] font-medium text-slate-900">Test run</span>
|
||||
<span class="ml-auto inline-flex h-7 w-7 items-center justify-center rounded-md text-slate-400"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" set:html={xIcon}></svg></span>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0 p-3 space-y-2 overflow-hidden">
|
||||
<p class="text-[11px] text-slate-400 leading-relaxed">Dry run with a sample reply event.</p>
|
||||
|
||||
<div class="ad-row ad-anim rounded-md border border-slate-200 px-2.5 py-2" style="animation-delay:0s">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-emerald-500 shrink-0" set:html={checkCircleIcon}></svg>
|
||||
<span class="text-[11.5px] font-medium text-slate-700 truncate">Trigger · Prospect replies</span>
|
||||
</div>
|
||||
<div class="mt-1 pl-[21px] font-mono text-[10.5px] text-slate-400 truncate">maya@northwind.dev · Re: Quick question</div>
|
||||
</div>
|
||||
|
||||
<div class="ad-row ad-anim rounded-md border border-slate-200 px-2.5 py-2" style="animation-delay:0.9s">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-sky-500 shrink-0" set:html={gitBranchIcon}></svg>
|
||||
<span class="text-[11.5px] font-medium text-slate-700 truncate">intent is positive · ≥ 80%</span>
|
||||
<span class="ml-auto shrink-0 text-[10.5px] font-semibold text-emerald-600">yes · 92%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ad-row ad-anim rounded-md border border-slate-200 px-2.5 py-2" style="animation-delay:1.95s">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-emerald-500 shrink-0" set:html={checkCircleIcon}></svg>
|
||||
<span class="text-[11.5px] font-medium text-slate-700 truncate">Slack message · #sales</span>
|
||||
<span class="ml-auto shrink-0 font-mono text-[10.5px] text-slate-400">212 ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ad-row ad-anim rounded-md border border-slate-200 px-2.5 py-2" style="animation-delay:3.2s">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-emerald-500 shrink-0" set:html={checkCircleIcon}></svg>
|
||||
<span class="text-[11.5px] font-medium text-slate-700 truncate">Deal created · Discovery</span>
|
||||
<span class="ml-auto shrink-0 font-mono text-[10.5px] text-slate-400">$4,800</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ad-row ad-anim rounded-md border border-slate-200 px-2.5 py-2" style="animation-delay:4.4s">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-emerald-500 shrink-0" set:html={checkCircleIcon}></svg>
|
||||
<span class="text-[11.5px] font-medium text-slate-700 truncate">HubSpot contact updated</span>
|
||||
<span class="ml-auto shrink-0 font-mono text-[10.5px] text-slate-400">2 fields</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ad-row ad-anim rounded-md border border-emerald-200 bg-emerald-50 px-2.5 py-2" style="animation-delay:5.2s">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-emerald-600 shrink-0" set:html={checkCircleIcon}></svg>
|
||||
<span class="text-[11.5px] font-semibold text-emerald-700">Run passed · 5 steps · 1.9 s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="ad-row ad-anim text-[11px] text-slate-400 leading-relaxed pt-1" style="animation-delay:5.2s">Flip it on and this fires on every real reply.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Demo cursor: positioned against the whole 1280x608 screen so it can
|
||||
reach the Test button in the top bar -->
|
||||
<div class="ad-cursor ad-anim z-50" aria-hidden="true"><span class="ad-ripple ad-anim"></span><svg class="ad-anim" width="23" height="27" viewBox="0 0 23 27" fill="none"><path d="M3 2.2 L3 20.4 L7.7 16 L10.9 23.2 L14 21.8 L10.9 14.8 L17.2 14.6 Z" fill="#ffffff" stroke="#0f172a" stroke-width="1.4" stroke-linejoin="round"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ── Master timeline: every animation runs on the same 15s loop ── */
|
||||
|
||||
.ad-cursor{position:absolute;width:23px;height:27px;pointer-events:none;filter:drop-shadow(0 4px 6px rgba(15,23,42,.4));animation:ad-move 15s cubic-bezier(.5,0,.2,1) infinite;}
|
||||
.ad-cursor svg{display:block;transform-origin:3px 2px;animation:ad-press 15s ease-in-out infinite;}
|
||||
.ad-ripple{position:absolute;left:-6px;top:-6px;width:32px;height:32px;border-radius:9999px;background:rgba(2,132,199,.5);transform:scale(0);opacity:0;animation:ad-rip 15s ease-out infinite;}
|
||||
@keyframes ad-move{0%{left:46%;top:64%}8%,12%{left:80.5%;top:5.6%}22%{left:58%;top:48%}74%{left:60%;top:52%}88%,100%{left:46%;top:64%}}
|
||||
@keyframes ad-press{0%,8.5%{transform:scale(1)}9.5%{transform:scale(.82)}11%,100%{transform:scale(1)}}
|
||||
@keyframes ad-rip{0%,9%{transform:scale(0);opacity:0}10.5%{transform:scale(.3);opacity:.7}16%{transform:scale(1.9);opacity:0}100%{transform:scale(0);opacity:0}}
|
||||
|
||||
/* Test button: press flash, label crossfades to a spinner while running */
|
||||
.ad-testbtn{animation:ad-btn 15s ease-out infinite;}
|
||||
@keyframes ad-btn{0%,9%{background:#fff;border-color:#e2e8f0}10.5%,48%{background:#f0f9ff;border-color:#7dd3fc}53%,100%{background:#fff;border-color:#e2e8f0}}
|
||||
.ad-testlabel-a{animation:ad-lbl-a 15s ease-out infinite;}
|
||||
.ad-testlabel-b{opacity:0;animation:ad-lbl-b 15s ease-out infinite;}
|
||||
@keyframes ad-lbl-a{0%,10%{opacity:1}11%,49%{opacity:0}52%,100%{opacity:1}}
|
||||
@keyframes ad-lbl-b{0%,10%{opacity:0}11%,49%{opacity:1}52%,100%{opacity:0}}
|
||||
.ad-spin{animation:ad-rot .8s linear infinite;}
|
||||
@keyframes ad-rot{to{transform:rotate(360deg)}}
|
||||
|
||||
/* Insights panel slides in while the run executes */
|
||||
.ad-panel{transform:translateX(102%);animation:ad-pane 15s cubic-bezier(.22,1,.36,1) infinite;}
|
||||
@keyframes ad-pane{0%,11%{transform:translateX(102%)}13.5%,79%{transform:translateX(0)}82%,100%{transform:translateX(102%)}}
|
||||
|
||||
/* Result rows pop in one by one (per-row phase via animation-delay) */
|
||||
.ad-row{opacity:0;animation:ad-pop 15s cubic-bezier(.22,1,.36,1) infinite;}
|
||||
@keyframes ad-pop{0%,16%{opacity:0;transform:translateY(6px)}18%,79%{opacity:1;transform:translateY(0)}82%,100%{opacity:0;transform:translateY(6px)}}
|
||||
|
||||
/* Node flash as the run passes through (shared pulse, shifted per node) */
|
||||
.ad-flash{animation:ad-fl 15s ease-in-out infinite;}
|
||||
@keyframes ad-fl{0%,13.5%{box-shadow:0 1px 2px 0 rgb(0 0 0 / 0.05)}15%{box-shadow:0 0 0 4px rgba(125,211,252,.85)}18%,100%{box-shadow:0 1px 2px 0 rgb(0 0 0 / 0.05)}}
|
||||
|
||||
/* Lit edge overlays draw in along the path */
|
||||
.ad-lit{stroke-dasharray:var(--len);stroke-dashoffset:var(--len);animation:ad-dr 15s ease-in-out infinite;}
|
||||
@keyframes ad-dr{0%,17%{stroke-dashoffset:var(--len);opacity:1}19.5%,76%{stroke-dashoffset:0;opacity:1}78%,100%{stroke-dashoffset:var(--len);opacity:0}}
|
||||
|
||||
/* The path not taken dims while the run is live */
|
||||
.ad-dim{animation:ad-dm 15s ease-in-out infinite;}
|
||||
@keyframes ad-dm{0%,13%{opacity:1}17%,75%{opacity:.4}80%,100%{opacity:1}}
|
||||
|
||||
/* Slack toast pops while the Slack step delivers */
|
||||
.ad-toast{opacity:0;animation:ad-to 15s cubic-bezier(.22,1,.36,1) infinite;}
|
||||
@keyframes ad-to{0%,29%{opacity:0;transform:translateY(14px) scale(.96)}31.5%,58%{opacity:1;transform:translateY(0) scale(1)}61%,100%{opacity:0;transform:translateY(14px) scale(.96)}}
|
||||
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
.ad-cursor,.ad-panel,.ad-toast{display:none}
|
||||
.ad-anim{animation:none !important;opacity:1}
|
||||
.ad-testlabel-b{opacity:0 !important}
|
||||
.ad-lit{stroke-dashoffset:0}
|
||||
.ad-row{opacity:1;transform:none}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Scale-to-fit (1280px design width; below md, show the left 860px like the
|
||||
// hero mock does) + click Test to restart the run from zero.
|
||||
const DESIGN = 1280;
|
||||
const MOBILE_VISIBLE = 860;
|
||||
function fit(screen: HTMLElement) {
|
||||
const box = screen.parentElement as HTMLElement | null;
|
||||
if (!box) return;
|
||||
const visible = window.innerWidth >= 768 ? DESIGN : MOBILE_VISIBLE;
|
||||
const s = box.clientWidth / visible;
|
||||
screen.style.transform = `scale(${s})`;
|
||||
box.style.height = `${screen.offsetHeight * s}px`;
|
||||
}
|
||||
document.querySelectorAll<HTMLElement>('.ad-screen').forEach((screen) => {
|
||||
const run = () => fit(screen);
|
||||
run();
|
||||
requestAnimationFrame(run);
|
||||
if (screen.parentElement) new ResizeObserver(run).observe(screen.parentElement);
|
||||
window.addEventListener('load', run);
|
||||
window.addEventListener('resize', run);
|
||||
});
|
||||
document.querySelectorAll<HTMLElement>('.ad-root').forEach((root) => {
|
||||
const btn = root.querySelector<HTMLElement>('.ad-testbtn');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', () => {
|
||||
// Toggle animation-name only: the per-element animation-delay lives in
|
||||
// the style attribute and the animation shorthand would wipe it.
|
||||
const els = root.querySelectorAll<HTMLElement | SVGElement>('.ad-anim');
|
||||
els.forEach((el) => { (el as HTMLElement).style.animationName = 'none'; });
|
||||
void root.offsetWidth;
|
||||
els.forEach((el) => { (el as HTMLElement).style.animationName = ''; });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
import Icon from './Icon.astro';
|
||||
import AutomationsDemo from './AutomationsDemo.astro';
|
||||
|
||||
/**
|
||||
* Landing-page centerpiece for Automations. Breaks the copy-beside-window
|
||||
* rhythm of the feature showcase on purpose: the whole section is drawn as a
|
||||
* builder canvas (edge-to-edge dot grid + faint flow strokes + floating node
|
||||
* fragments), with the live demo window running a test end to end at
|
||||
* near-full container width.
|
||||
*/
|
||||
const callouts = [
|
||||
{
|
||||
icon: 'gitBranch',
|
||||
label: 'Branches like you think',
|
||||
text: 'One IF chip routes on intent, confidence, source, or a random split. Yes goes right, no goes down, and the no path is still yours to wire.',
|
||||
},
|
||||
{
|
||||
icon: 'layers',
|
||||
label: 'Acts across your stack',
|
||||
text: 'Seven built-in actions plus Slack, Discord, HubSpot, Salesforce, Pipedrive, Close and HMAC-signed webhooks. No keys to paste for automation tools.',
|
||||
},
|
||||
{
|
||||
icon: 'flask',
|
||||
label: 'Test before it is on',
|
||||
text: 'Dry-run any automation against a sample event and read every node verdict, yes or no, before the switch ever flips.',
|
||||
},
|
||||
];
|
||||
---
|
||||
<section class="auto-showcase relative isolate overflow-hidden border-y border-[color:var(--border)]">
|
||||
<!-- The section IS the canvas: dot grid + giant faint flow strokes -->
|
||||
<div class="absolute inset-0 pointer-events-none" aria-hidden="true" style="background: linear-gradient(180deg, #f6f9fd 0%, #ffffff 38%, #f2f7fd 100%);"></div>
|
||||
<div class="absolute inset-0 pointer-events-none" aria-hidden="true" style="background-image: radial-gradient(#dbe6f3 1px, transparent 1px); background-size: 24px 24px; mask-image: radial-gradient(ellipse 90% 80% at 50% 40%, black 30%, transparent 100%); -webkit-mask-image: radial-gradient(ellipse 90% 80% at 50% 40%, black 30%, transparent 100%);"></div>
|
||||
<svg class="absolute inset-0 w-full h-full pointer-events-none" aria-hidden="true" viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" fill="none">
|
||||
<path d="M -40 240 C 280 240, 300 560, 720 560 S 1180 300, 1500 300" stroke="#bae6fd" stroke-width="1.5" opacity="0.55"/>
|
||||
<path d="M -40 700 C 360 700, 420 420, 760 420 S 1220 660, 1500 660" stroke="#e0eaf6" stroke-width="1.5"/>
|
||||
<circle cx="720" cy="560" r="4" fill="#7dd3fc" opacity="0.7"/>
|
||||
<circle cx="760" cy="420" r="4" fill="#cbdcf0"/>
|
||||
</svg>
|
||||
|
||||
<!-- Floating node fragments at the far edges (xl+) -->
|
||||
<div class="hidden xl:block absolute inset-0 pointer-events-none select-none" aria-hidden="true">
|
||||
<div class="auto-float absolute top-[16%] left-[3%]" style="--drift:-9px; animation-duration:7.4s;">
|
||||
<div class="rotate-[-2deg] inline-flex items-center gap-1.5 rounded-lg border border-sky-200 bg-gradient-to-b from-sky-50 to-white px-2.5 py-1.5 shadow-[0_14px_30px_-14px_rgba(2,32,71,0.4)]">
|
||||
<Icon name="gitBranch" size={12} class="text-sky-600" />
|
||||
<span class="text-[9.5px] font-semibold uppercase tracking-[0.12em] text-sky-500">if</span>
|
||||
<span class="text-[12px] font-medium text-sky-800">meeting.booked</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auto-float absolute top-[62%] left-[5%]" style="--drift:-7px; animation-duration:8.6s; animation-delay:-2.2s;">
|
||||
<div class="rotate-[2deg] inline-flex items-center gap-1.5 rounded-md border border-emerald-200 bg-emerald-50 px-2.5 py-1.5 shadow-[0_12px_26px_-14px_rgba(2,32,71,0.35)]">
|
||||
<Icon name="check" size={11} class="text-emerald-600" />
|
||||
<span class="text-[11.5px] font-medium text-emerald-700">Run passed · 1.9 s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auto-float absolute top-[22%] right-[3%]" style="--drift:-11px; animation-duration:7.0s; animation-delay:-1.1s;">
|
||||
<div class="rotate-[2deg] w-[200px] rounded-[12px] bg-white ring-1 ring-slate-900/5 shadow-[0_18px_36px_-14px_rgba(2,32,71,0.4)] overflow-hidden">
|
||||
<div class="flex items-center gap-2 border-b border-slate-200/70 bg-gradient-to-r from-slate-50 to-white px-2.5 py-1.5">
|
||||
<span class="inline-flex size-5 items-center justify-center rounded-md bg-violet-50 text-violet-600 ring-1 ring-violet-200"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg></span>
|
||||
<span class="flex-1 truncate text-[12px] font-semibold text-slate-800">Create a task</span>
|
||||
</div>
|
||||
<div class="px-2.5 py-1.5">
|
||||
<div class="text-[8.5px] font-semibold uppercase tracking-[0.14em] text-slate-300">Then</div>
|
||||
<div class="mt-0.5 truncate text-[11px] text-slate-500">Call back · due in 2 days</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auto-float absolute top-[66%] right-[4%]" style="--drift:-8px; animation-duration:8.0s; animation-delay:-3s;">
|
||||
<div class="rotate-[-2deg] inline-flex items-center gap-1.5 h-7 px-2.5 rounded-full bg-white/95 ring-1 ring-slate-900/5 shadow-[0_10px_22px_-12px_rgba(2,32,71,0.4)]">
|
||||
<Icon name="lock" size={11} class="text-slate-400" />
|
||||
<span class="font-mono text-[10.5px] font-semibold text-slate-600">X-Warmbly-Signature: sha256=…</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-page relative py-16 md:py-24">
|
||||
<div class="text-center max-w-3xl mx-auto">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] font-mono text-[#0284c7] mb-4">Automations</div>
|
||||
<h2 class="text-[34px] md:text-[52px] font-semibold tracking-[-0.03em] leading-[1.04] text-heading">
|
||||
When this happens, do that.
|
||||
</h2>
|
||||
<p class="mt-5 text-[15.5px] md:text-[16.5px] text-foreground/70 leading-relaxed max-w-2xl mx-auto">
|
||||
Every reply, booked meeting, bounce and warmup dip is a trigger. Wire it to your stack on a canvas: branch on intent, post to Slack, open the deal, update the CRM. The one below is running its test right now. Press Test in the window to fire it again.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- The live demo window -->
|
||||
<div class="relative mt-10 md:mt-14 max-w-[1180px] mx-auto">
|
||||
<AutomationsDemo />
|
||||
</div>
|
||||
|
||||
<!-- Callouts -->
|
||||
<div class="mt-12 md:mt-16 grid sm:grid-cols-3 gap-8 lg:gap-12 max-w-5xl mx-auto">
|
||||
{callouts.map((c) => (
|
||||
<div class="sm:border-l sm:border-[color:var(--border)] sm:pl-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name={c.icon} size={14} class="text-[#0284c7]" />
|
||||
<div class="text-[13.5px] font-semibold text-heading">{c.label}</div>
|
||||
</div>
|
||||
<p class="mt-2 text-[13px] text-foreground/65 leading-relaxed">{c.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="mt-12 flex flex-wrap items-center justify-center gap-3">
|
||||
<a href="/automations/" class="inline-flex h-10 px-5 items-center gap-2 rounded-[10px] text-[14px] font-medium bg-[color:var(--brand)] text-white hover:bg-[color:var(--brand-strong)] transition-colors shadow-[0_10px_24px_-10px_rgba(2,132,199,0.65)]">
|
||||
Explore automations
|
||||
<Icon name="arrowRight" size={13} />
|
||||
</a>
|
||||
<a href="https://app.warmbly.com/register" class="inline-flex h-10 px-4 items-center gap-1.5 text-[13.5px] font-medium text-foreground/75 hover:text-foreground transition-colors">
|
||||
or start free
|
||||
<Icon name="arrowUpRight" size={12} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.auto-float { animation-name: autoFloat; animation-timing-function: ease-in-out; animation-iteration-count: infinite; animation-direction: alternate; will-change: transform; }
|
||||
@keyframes autoFloat { from { transform: translateY(0); } to { transform: translateY(var(--drift, -8px)); } }
|
||||
@media (prefers-reduced-motion: reduce) { .auto-float { animation: none; } }
|
||||
</style>
|
||||
</section>
|
||||
@@ -9,6 +9,7 @@ const groups: { title: string; links: { label: string; href: string }[] }[] = [
|
||||
{ label: 'Sending engine', href: '/sending/' },
|
||||
{ label: 'Unified inbox', href: '/inbox/' },
|
||||
{ label: 'Campaigns', href: '/campaigns/' },
|
||||
{ label: 'Automations', href: '/automations/' },
|
||||
{ label: 'Analytics', href: '/analytics/' },
|
||||
{ label: 'CRM', href: '/crm/' },
|
||||
{ label: 'API & Webhooks', href: '/developers/' },
|
||||
|
||||
@@ -10,6 +10,16 @@ const { onSky = false } = Astro.props;
|
||||
|
||||
const path = Astro.url.pathname;
|
||||
|
||||
// Featured product: rendered as a full-width row above the grid, with a
|
||||
// miniature trigger -> if -> action flow as its visual.
|
||||
const productFeatured = {
|
||||
icon: 'zap',
|
||||
href: '/automations/',
|
||||
title: 'Automations',
|
||||
desc: 'When a reply lands: open the deal, ping Slack, update the CRM. A visual builder for every Warmbly event.',
|
||||
tag: 'Orchestration',
|
||||
};
|
||||
|
||||
// Product mega-menu items (6 meaningful products)
|
||||
const products = [
|
||||
{ icon: 'flame', href: '/warmup/', title: 'Warmup', desc: 'Reputation-safe pools and gradual ramps that earn placement, not just inflate it.', tag: 'Deliverability' },
|
||||
@@ -38,17 +48,19 @@ type NavItem = {
|
||||
megaTitle?: string;
|
||||
megaTagline?: string;
|
||||
megaItems?: MegaItem[];
|
||||
megaFeatured?: MegaItem;
|
||||
};
|
||||
|
||||
const nav: NavItem[] = [
|
||||
{
|
||||
label: 'Product',
|
||||
href: '/warmup/',
|
||||
match: '/warmup|/developers|/inbox|/campaigns|/analytics|/crm',
|
||||
match: '/warmup|/developers|/inbox|/campaigns|/analytics|/crm|/automations',
|
||||
mega: true,
|
||||
megaTitle: 'The cold-email platform.',
|
||||
megaTagline: 'Six systems designed to work together. Warmup that earns reputation, sending that respects it.',
|
||||
megaTagline: 'Seven systems designed to work together. Warmup that earns reputation, sending that respects it.',
|
||||
megaItems: products,
|
||||
megaFeatured: productFeatured,
|
||||
},
|
||||
{
|
||||
label: 'Use cases',
|
||||
@@ -124,8 +136,59 @@ function active(match?: string) {
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: 3-column product grid -->
|
||||
<div class="grid grid-cols-2 lg:grid-cols-3">
|
||||
<!-- RIGHT: featured row + 3-column product grid -->
|
||||
<div class="flex flex-col">
|
||||
{n.megaFeatured && (() => {
|
||||
const f = n.megaFeatured;
|
||||
const isCurrent = path === f.href || path.startsWith(f.href);
|
||||
return (
|
||||
<a
|
||||
href={f.href}
|
||||
aria-current={isCurrent ? 'page' : undefined}
|
||||
class={`mega-card group flex items-center gap-6 px-7 py-5 border-b border-[color:var(--border)] ${isCurrent ? 'bg-[color:var(--sky-1)]/60' : 'hover:bg-[color:var(--surface-1)]'}`}
|
||||
style="transition: background-color 140ms ease-out;"
|
||||
>
|
||||
<span class="min-w-0 shrink">
|
||||
<span class="block text-[10px] uppercase tracking-[0.22em] font-mono text-foreground/45 mb-2">{f.tag}</span>
|
||||
<span class={`text-[18px] font-semibold tracking-[-0.015em] flex items-center gap-2 ${isCurrent ? 'text-[#0369a1]' : 'text-heading group-hover:text-[#0369a1]'}`} style="transition: color 140ms ease-out;">
|
||||
{f.title}
|
||||
{isCurrent && <span class="w-1.5 h-1.5 rounded-full bg-[#0284c7]"></span>}
|
||||
</span>
|
||||
<span class="mt-1 block text-[13px] text-foreground/60 leading-relaxed">{f.desc}</span>
|
||||
</span>
|
||||
|
||||
{/* miniature flow: trigger -> if -> actions, same node language as the app */}
|
||||
<span class="hidden xl:flex items-center gap-1.5 ml-auto shrink-0" aria-hidden="true">
|
||||
<span class="inline-flex items-center gap-1.5 h-8 px-2.5 rounded-lg border border-slate-200 bg-white shadow-sm text-[11px] font-medium text-slate-700 whitespace-nowrap">
|
||||
<Icon name="zap" size={11} class="text-sky-600" />
|
||||
Reply lands
|
||||
</span>
|
||||
<svg width="14" height="10" viewBox="0 0 14 10" fill="none" class="text-slate-300 shrink-0"><path d="M0 5h10" stroke="currentColor" stroke-width="1.5"/><path d="m8 1.5 3.5 3.5L8 8.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<span class="inline-flex items-center gap-1.5 h-8 px-2.5 rounded-lg border border-sky-200 bg-gradient-to-b from-sky-50 to-white shadow-sm whitespace-nowrap">
|
||||
<Icon name="gitBranch" size={11} class="text-sky-600" />
|
||||
<span class="text-[9px] font-semibold uppercase tracking-[0.12em] text-sky-500">if</span>
|
||||
<span class="text-[11px] font-medium text-sky-800">positive</span>
|
||||
</span>
|
||||
<svg width="14" height="10" viewBox="0 0 14 10" fill="none" class="text-slate-300 shrink-0"><path d="M0 5h10" stroke="currentColor" stroke-width="1.5"/><path d="m8 1.5 3.5 3.5L8 8.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<span class="inline-flex items-center gap-1.5 h-8 px-2.5 rounded-lg border border-slate-200 bg-white shadow-sm text-[11px] font-medium text-slate-700 whitespace-nowrap">
|
||||
<span class="inline-flex size-4 items-center justify-center rounded bg-fuchsia-50 text-fuchsia-600 ring-1 ring-fuchsia-200 text-[8.5px] font-bold">S</span>
|
||||
#sales
|
||||
</span>
|
||||
<svg width="14" height="10" viewBox="0 0 14 10" fill="none" class="text-slate-300 shrink-0"><path d="M0 5h10" stroke="currentColor" stroke-width="1.5"/><path d="m8 1.5 3.5 3.5L8 8.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<span class="inline-flex items-center gap-1.5 h-8 px-2.5 rounded-lg border border-slate-200 bg-white shadow-sm text-[11px] font-medium text-slate-700 whitespace-nowrap">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-sky-600"><path d="M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/><rect width="20" height="14" x="2" y="6" rx="2"/></svg>
|
||||
Deal opened
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="shrink-0 ml-auto xl:ml-0 inline-flex items-center gap-1 text-[11.5px] font-medium text-foreground/45 group-hover:text-[#0369a1]" style="transition: color 140ms ease-out;">
|
||||
Open
|
||||
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M3.5 3l3 3-3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
})()}
|
||||
<div class="grid grid-cols-2 lg:grid-cols-3">
|
||||
{(n.megaItems ?? []).map((it, i) => {
|
||||
const isCurrent = path === it.href || path.startsWith(it.href);
|
||||
const colBreak = (i + 1) % 3 !== 0;
|
||||
@@ -150,6 +213,7 @@ function active(match?: string) {
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,7 +319,7 @@ function active(match?: string) {
|
||||
<div class="acc-panel">
|
||||
<div>
|
||||
<div class="pb-3 flex flex-col">
|
||||
{(n.megaItems ?? []).map((it) => (
|
||||
{[...(n.megaFeatured ? [n.megaFeatured] : []), ...(n.megaItems ?? [])].map((it) => (
|
||||
<a href={it.href} class="flex flex-col py-2 pl-1 pr-2 rounded-md hover:bg-slate-50">
|
||||
<span class="text-[15px] font-medium text-slate-800">{it.title}</span>
|
||||
<span class="text-[12.5px] text-slate-400 leading-snug">{it.desc.split('.')[0]}.</span>
|
||||
|
||||
@@ -52,6 +52,7 @@ const icons: Record<string, string> = {
|
||||
warning: '<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/><line x1="12" x2="12" y1="9" y2="13"/><line x1="12" x2="12.01" y1="17" y2="17"/>',
|
||||
star: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',
|
||||
rocket: '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>',
|
||||
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
|
||||
};
|
||||
|
||||
const path = icons[name] ?? '';
|
||||
|
||||
@@ -0,0 +1,624 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import HeroAtmosphere from '../components/HeroAtmosphere.astro';
|
||||
import Icon from '../components/Icon.astro';
|
||||
import CTA from '../components/CTA.astro';
|
||||
import AutomationsDemo from '../components/AutomationsDemo.astro';
|
||||
|
||||
// ===========================================================================
|
||||
// Automations page. The centrepiece is the live builder demo (AutomationsDemo,
|
||||
// shared with the landing page): the real /app/automations/:id canvas with a
|
||||
// test run firing end to end. Catalog content mirrors the product:
|
||||
// web/src automations builder (trigger events, native + integration
|
||||
// actions, condition fields, test runs)
|
||||
// migrations 000033..000035 (automations, branching graph, history)
|
||||
//
|
||||
// Astro JSX caveat: template tokens are written as {`{{.Field}}`}.
|
||||
// ===========================================================================
|
||||
|
||||
// --- Trigger events ----------------------------------------------------------
|
||||
const triggers = [
|
||||
{ icon: 'reply', label: 'Prospect replies', key: 'campaign.reply_received', desc: 'The classifier labels intent and confidence before the trigger fires, so you can branch on both.' },
|
||||
{ icon: 'calendarCheck', label: 'Meeting booked', key: 'meeting.booked', desc: 'Calendly and Cal.com invitees, matched back to the contact that booked.' },
|
||||
{ icon: 'calendarClock', label: 'Meeting rescheduled', key: 'meeting.rescheduled', desc: 'Same payload, new time, so downstream reminders stay honest.' },
|
||||
{ icon: 'calendarX', label: 'Meeting canceled', key: 'meeting.canceled', desc: 'Catch the cancellation and put the contact back to work.' },
|
||||
{ icon: 'mailWarning', label: 'Email bounces', key: 'campaign.email_bounced', desc: 'Hard or soft, with the provider and reason attached to the event.' },
|
||||
{ icon: 'userMinus', label: 'Contact unsubscribes', key: 'campaign.unsubscribed', desc: 'Suppression already happened. This is your cue to sync it everywhere else.' },
|
||||
{ icon: 'activity', label: 'Warmup health changes', key: 'warmup.health_changed', desc: 'A mailbox moves bands: healthy, watch, quarantined. React before placement slips.' },
|
||||
{ icon: 'shieldAlert', label: 'Spam complaint', key: 'deliverability.complaint', desc: 'The signal worth reacting to fastest. Page the channel, pause the risk.' },
|
||||
{ icon: 'workflow', label: 'Launched by a campaign step', key: 'campaign.action', desc: 'A sequence action node can fire any automation, per contact, mid-flow.' },
|
||||
];
|
||||
|
||||
const tIcons: Record<string, string> = {
|
||||
reply: '<polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/>',
|
||||
calendarCheck: '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="m9 16 2 2 4-4"/>',
|
||||
calendarClock: '<path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5"/><path d="M16 2v4"/><path d="M8 2v4"/><path d="M3 10h5"/><path d="M17.5 17.5 16 16.3V14"/><circle cx="16" cy="16" r="6"/>',
|
||||
calendarX: '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="m14 14-4 4"/><path d="m10 14 4 4"/>',
|
||||
mailWarning: '<path d="M22 10.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h12.5"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/><path d="M20 14v4"/><path d="M20 22v.01"/>',
|
||||
userMinus: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="22" x2="16" y1="11" y2="11"/>',
|
||||
activity: '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>',
|
||||
shieldAlert: '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="M12 8v4"/><path d="M12 16h.01"/>',
|
||||
workflow: '<rect x="3" y="3" width="6" height="6" rx="1"/><rect x="15" y="15" width="6" height="6" rx="1"/><path d="M6 9v4a2 2 0 0 0 2 2h7"/>',
|
||||
};
|
||||
|
||||
const aIcons: Record<string, string> = {
|
||||
tag: '<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/>',
|
||||
checkSquare: '<polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>',
|
||||
briefcase: '<path d="M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/><rect width="20" height="14" x="2" y="6" rx="2"/>',
|
||||
userMinus: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="22" x2="16" y1="11" y2="11"/>',
|
||||
zap: '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>',
|
||||
send: '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.5.5 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/>',
|
||||
gitBranch: '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
|
||||
checkCircle: '<path d="M21.801 10A10 10 0 1 1 17 3.335"/><path d="m9 11 3 3L22 4"/>',
|
||||
xCircle: '<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>',
|
||||
};
|
||||
|
||||
// --- Actions: built in -------------------------------------------------------
|
||||
const nativeActions = [
|
||||
{ icon: 'tag', tint: 'bg-emerald-50 text-emerald-600 ring-emerald-200', name: 'Add a tag', key: 'warmbly.add_tag', desc: 'Label the contact for the next segment or suppression list.' },
|
||||
{ icon: 'tag', tint: 'bg-amber-50 text-amber-600 ring-amber-200', name: 'Remove a tag', key: 'warmbly.remove_tag', desc: 'Pull them out of a segment the moment the reason expires.' },
|
||||
{ icon: 'checkSquare', tint: 'bg-violet-50 text-violet-600 ring-violet-200', name: 'Create a task', key: 'warmbly.create_task', desc: 'Typed, owned, with a due date: call back, review, follow up.' },
|
||||
{ icon: 'briefcase', tint: 'bg-sky-50 text-sky-600 ring-sky-200', name: 'Create a deal', key: 'warmbly.create_deal', desc: 'Open the pipeline record with a stage, value and currency.' },
|
||||
{ icon: 'briefcase', tint: 'bg-sky-50 text-sky-600 ring-sky-200', name: 'Move the deal stage', key: 'warmbly.move_deal_stage', desc: 'Advance or park the deal as the signal comes in.' },
|
||||
{ icon: 'userMinus', tint: 'bg-rose-50 text-rose-600 ring-rose-200', name: 'Unsubscribe the contact', key: 'warmbly.unsubscribe', desc: 'Workspace-wide suppression, applied before anything else queues.' },
|
||||
{ icon: 'zap', tint: 'bg-indigo-50 text-indigo-600 ring-indigo-200', name: 'Run another automation', key: 'warmbly.run_automation', desc: 'Compose playbooks. Self-reference is blocked, so no loops.' },
|
||||
];
|
||||
|
||||
// --- Actions: across the stack ----------------------------------------------
|
||||
const stackActions = [
|
||||
{ initial: 'S', tile: 'bg-fuchsia-50 text-fuchsia-600 ring-fuchsia-200', name: 'Slack', action: 'Send a channel message, templated per event.' },
|
||||
{ initial: 'D', tile: 'bg-indigo-50 text-indigo-600 ring-indigo-200', name: 'Discord', action: 'Same message action, pointed at a Discord channel.' },
|
||||
{ initial: 'H', tile: 'bg-orange-50 text-orange-600 ring-orange-200', name: 'HubSpot', action: 'Create or update the contact as events land.' },
|
||||
{ initial: 'Sf', tile: 'bg-sky-50 text-sky-600 ring-sky-200', name: 'Salesforce', action: 'Upsert the contact into your org.' },
|
||||
{ initial: 'P', tile: 'bg-slate-100 text-slate-700 ring-slate-300', name: 'Pipedrive', action: 'Create or update the person record.' },
|
||||
{ initial: 'C', tile: 'bg-indigo-50 text-indigo-600 ring-indigo-200', name: 'Close', action: 'Upsert the lead, ready for the calling queue.' },
|
||||
{ initial: '{}', tile: 'bg-sky-50 text-sky-600 ring-sky-200', name: 'Webhook', action: 'HMAC-signed POST to any URL you run.' },
|
||||
];
|
||||
|
||||
// --- Condition surface --------------------------------------------------------
|
||||
const conditionFields = [
|
||||
{ field: 'intent', on: 'replies', type: 'enum' },
|
||||
{ field: 'confidence', on: 'replies', type: 'number' },
|
||||
{ field: 'subject', on: 'replies', type: 'string' },
|
||||
{ field: 'source', on: 'meetings', type: 'enum' },
|
||||
{ field: 'event_name', on: 'meetings', type: 'string' },
|
||||
{ field: 'provider', on: 'deliverability', type: 'enum' },
|
||||
{ field: 'reason', on: 'deliverability', type: 'string' },
|
||||
{ field: 'new_state', on: 'warmup health', type: 'enum' },
|
||||
{ field: 'contact_email', on: 'everything', type: 'string' },
|
||||
];
|
||||
|
||||
const operators = [
|
||||
{ type: 'string', ops: 'equals · not equals · contains · exists' },
|
||||
{ type: 'number', ops: 'greater or equal · less or equal · equals' },
|
||||
{ type: 'enum', ops: 'equals · not equals · exists' },
|
||||
{ type: 'random', ops: 'chance, a weighted split for testing playbooks' },
|
||||
];
|
||||
|
||||
// --- Template variables -------------------------------------------------------
|
||||
const variables = ['{{.contact_email}}', '{{.intent}}', '{{.confidence}}', '{{.subject}}', '{{.snippet}}', '{{.campaign_id}}'];
|
||||
|
||||
// --- Trust strip ---------------------------------------------------------------
|
||||
const trust = [
|
||||
{ icon: 'lock', t: 'Signed deliveries', d: 'Every webhook a run sends carries an HMAC SHA-256 signature header. Verify it in four lines and drop anything that does not match.' },
|
||||
{ icon: 'shield', t: 'Throttled, fail-open', d: 'Dispatch is rate-limited per organization and event with generous plan-based limits. If the limiter itself has a bad day, events still flow.' },
|
||||
{ icon: 'layers', t: 'Safe to compose', d: 'Automations can run automations, and campaign steps can launch them per contact. Anything referenced by a live campaign step cannot be deleted out from under it.' },
|
||||
{ icon: 'key', t: 'No keys to paste', d: 'Automation tools connect in one click. OAuth where the provider has a per-user identity, a minted webhook URL for the rest.' },
|
||||
];
|
||||
|
||||
// --- FAQ -----------------------------------------------------------------------
|
||||
const faq = [
|
||||
['What can start an automation?',
|
||||
'Nine triggers: a classified reply, a booked, rescheduled or canceled meeting, a bounce, an unsubscribe, a warmup health change, a spam complaint, and a campaign step that launches the automation per contact mid-sequence.'],
|
||||
['Do I still need Zapier?',
|
||||
'For the common paths, no: Slack, Discord, HubSpot, Salesforce, Pipedrive, Close and the built-in CRM actions are direct. Zapier, Make and n8n still connect with a scoped key when you want them, and the signed webhook action reaches anything else.'],
|
||||
['What happens when a step fails?',
|
||||
'Each run records a per-node result. A failed step keeps the response it got, the run shows up in history with exactly which node broke and why, and the verdicts of the other nodes stay readable. Test runs exist so most failures never reach production.'],
|
||||
['Can one automation run another?',
|
||||
'Yes. The run-another-automation action composes playbooks, and a campaign sequence step can launch an automation for each contact that reaches it. Self-reference is blocked in the picker, and an automation referenced by a campaign step cannot be deleted.'],
|
||||
['Does the test run send anything?',
|
||||
'No. Test is a dry run against a sample event for your trigger. Every node reports what it would do, conditions report yes or no, and nothing leaves Warmbly until the automation is switched on.'],
|
||||
];
|
||||
---
|
||||
<Layout
|
||||
title="Outbound Automations: Visual Workflow Builder | Warmbly"
|
||||
description="When a reply lands, do everything else automatically: branch on intent, post to Slack, open the deal, update HubSpot or Salesforce, fire a signed webhook. A visual builder for every Warmbly event, with dry runs and full run history."
|
||||
>
|
||||
<!-- ============================================================
|
||||
HERO · sky, with floating builder fragments (xl+)
|
||||
============================================================ -->
|
||||
<section class="relative isolate overflow-hidden">
|
||||
<HeroAtmosphere />
|
||||
|
||||
<div class="hidden xl:block absolute inset-0 z-0 pointer-events-none select-none" aria-hidden="true">
|
||||
<!-- TOP-LEFT · trigger node, above the headline band -->
|
||||
<div class="hero-float absolute top-[11%] left-[calc(50%_-_500px)] z-30" style="--drift:-10px;animation-duration:7.6s;animation-delay:-1.4s">
|
||||
<div class="rotate-[-2deg] w-[212px] rounded-[12px] bg-white ring-1 ring-slate-900/5 shadow-[0_2px_4px_-2px_rgba(2,32,71,0.25),0_18px_36px_-12px_rgba(2,32,71,0.5)] overflow-hidden">
|
||||
<div class="flex items-center gap-2 border-b border-slate-200/70 bg-gradient-to-r from-sky-50/80 to-white px-2.5 py-1.5">
|
||||
<span class="inline-flex size-5 items-center justify-center rounded-md bg-sky-100 text-sky-600 ring-1 ring-sky-200/70"><Icon name="zap" size={11} /></span>
|
||||
<span class="flex-1 truncate text-[12px] font-semibold text-slate-800">Prospect replies</span>
|
||||
<span class="rounded bg-sky-600 px-1.5 py-px text-[8.5px] font-semibold uppercase tracking-[0.12em] text-white">Trigger</span>
|
||||
</div>
|
||||
<div class="px-2.5 py-1.5">
|
||||
<div class="text-[8.5px] font-semibold uppercase tracking-[0.14em] text-slate-300">When</div>
|
||||
<div class="mt-0.5 truncate font-mono text-[10.5px] text-slate-500">campaign.reply_received</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOP-RIGHT · IF chip -->
|
||||
<div class="hero-float absolute top-[9%] right-[calc(50%_-_452px)] z-30" style="--drift:-12px;animation-duration:7.2s;animation-delay:-2.1s">
|
||||
<div class="rotate-[2deg] inline-flex items-center gap-1.5 rounded-lg border border-sky-200 bg-gradient-to-b from-sky-50 to-white px-2.5 py-1.5 shadow-[0_2px_4px_-2px_rgba(2,32,71,0.25),0_16px_32px_-12px_rgba(2,32,71,0.5)]">
|
||||
<Icon name="gitBranch" size={12} class="text-sky-600" />
|
||||
<span class="text-[9px] font-semibold uppercase tracking-[0.14em] text-sky-500">if</span>
|
||||
<span class="text-[12px] font-medium text-sky-800">intent is positive</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOP-RIGHT · run passed pill, staggered under the IF chip but still
|
||||
above the headline so it never touches the title -->
|
||||
<div class="hero-float absolute top-[20%] right-[calc(50%_-_496px)] z-20" style="--drift:-7px;animation-duration:8.2s;animation-delay:-0.7s">
|
||||
<div class="rotate-[-2deg] inline-flex items-center gap-1.5 h-7 px-2.5 rounded-full bg-white/95 ring-1 ring-slate-900/5 shadow-[0_8px_20px_-10px_rgba(2,32,71,0.4)]">
|
||||
<Icon name="check" size={12} class="text-emerald-600" />
|
||||
<span class="font-mono text-[11px] font-semibold text-slate-700">run passed · 1.9 s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOTTOM-LEFT · slack action, below the CTA row -->
|
||||
<div class="hero-float absolute top-[80%] left-[calc(50%_-_478px)] z-20" style="--drift:-9px;animation-duration:7.9s;animation-delay:-2.6s">
|
||||
<div class="rotate-[2deg] inline-flex items-center gap-2 rounded-[12px] bg-white ring-1 ring-slate-900/5 shadow-[0_2px_3px_-2px_rgba(2,32,71,0.2),0_14px_30px_-14px_rgba(2,32,71,0.45)] px-3 py-2">
|
||||
<span class="inline-flex size-5 items-center justify-center rounded-md bg-fuchsia-50 text-fuchsia-600 ring-1 ring-fuchsia-200 text-[10px] font-bold">S</span>
|
||||
<span class="text-[12px] font-semibold text-slate-800">Notify #sales</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOTTOM-RIGHT · signature pill, below the CTA row -->
|
||||
<div class="hero-float absolute top-[83%] right-[calc(50%_-_466px)] z-10" style="--drift:-8px;animation-duration:7.0s;animation-delay:-3.2s">
|
||||
<div class="rotate-[-2deg] inline-flex items-center gap-1.5 h-7 px-2.5 rounded-full bg-white/95 ring-1 ring-slate-900/5 shadow-[0_10px_24px_-12px_rgba(2,32,71,0.4)]">
|
||||
<Icon name="lock" size={11} class="text-slate-400" />
|
||||
<span class="font-mono text-[10.5px] font-semibold text-slate-600">X-Warmbly-Signature: sha256=…</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-page relative z-10 pt-16 md:pt-24 pb-24 md:pb-32 text-center">
|
||||
<div class="inline-flex items-center gap-2 h-7 pl-1 pr-3 rounded-full bg-white/15 backdrop-blur ring-1 ring-white/25 text-[12px] text-white/95 shadow-[0_4px_14px_-4px_rgba(0,0,0,0.25)]">
|
||||
<span class="inline-flex items-center h-5 px-1.5 rounded-full text-[10.5px] font-semibold uppercase tracking-[0.06em] bg-white" style="color:#0369a1;">
|
||||
Automations
|
||||
</span>
|
||||
A visual builder for every Warmbly event
|
||||
</div>
|
||||
|
||||
<h1 class="mt-8 md:mt-10 text-[44px] sm:text-6xl md:text-[72px] lg:text-[80px] font-semibold tracking-[-0.04em] leading-[0.98] text-white max-w-4xl mx-auto">
|
||||
When this happens,<br/>do that.
|
||||
</h1>
|
||||
<p class="mt-6 text-[17px] md:text-[19px] text-white/80 max-w-2xl mx-auto leading-relaxed">
|
||||
A reply lands, a meeting books, a mailbox slips. Wire each moment to your playbook on a canvas: branch on intent, post to Slack, open the deal, update the CRM, fire a signed webhook. Then dry-run it before it ever touches production.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||
<a href="https://app.warmbly.com/register" class="inline-flex h-11 px-5 items-center gap-2 rounded-[10px] text-[14.5px] font-semibold bg-white hover:bg-white hover:-translate-y-0.5 hover:shadow-[0_12px_28px_-6px_rgba(0,0,0,0.3)] transition-all duration-200 ease-out shadow-[0_4px_14px_-2px_rgba(0,0,0,0.18)]">
|
||||
<span style="color:#075985;">Build your first automation</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#075985" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#demo" class="inline-flex h-11 px-5 items-center rounded-[10px] text-[14.5px] font-medium bg-white/10 backdrop-blur ring-1 ring-white/40 text-white hover:bg-white/20 hover:-translate-y-0.5 transition-all duration-200 ease-out">
|
||||
Watch it run
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.hero-float { animation-name: heroFloat; animation-timing-function: ease-in-out; animation-iteration-count: infinite; animation-direction: alternate; will-change: transform; }
|
||||
@keyframes heroFloat { from { transform: translateY(0); } to { transform: translateY(var(--drift, -10px)); } }
|
||||
@media (prefers-reduced-motion: reduce) { .hero-float { animation: none; } }
|
||||
</style>
|
||||
|
||||
<!-- ============================================================
|
||||
LIVE DEMO · the real builder, running a test end to end
|
||||
============================================================ -->
|
||||
<section id="demo" class="relative border-b border-[color:var(--border)] py-20 md:py-28 scroll-mt-4 overflow-hidden">
|
||||
<div class="absolute inset-0 pointer-events-none" aria-hidden="true" style="background: linear-gradient(180deg, #f6f9fd 0%, #ffffff 45%, #f2f7fd 100%);"></div>
|
||||
<div class="absolute inset-0 pointer-events-none" aria-hidden="true" style="background-image: radial-gradient(#dbe6f3 1px, transparent 1px); background-size: 24px 24px; mask-image: radial-gradient(ellipse 90% 80% at 50% 35%, black 25%, transparent 100%); -webkit-mask-image: radial-gradient(ellipse 90% 80% at 50% 35%, black 25%, transparent 100%);"></div>
|
||||
|
||||
<div class="container-page relative">
|
||||
<div class="max-w-2xl mb-10 md:mb-12">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] font-mono text-[#0284c7] mb-3">Live demo</div>
|
||||
<h2 class="text-[32px] md:text-[44px] font-semibold tracking-[-0.03em] leading-[1.05] text-heading">
|
||||
Watch a run, end to end.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15.5px] text-foreground/70 leading-relaxed">
|
||||
This is the builder as it ships. A positive reply comes in, the IF chip routes it, and the yes path posts to Slack, opens the deal and updates HubSpot while the test panel checks off every node. The no path waits its turn. Press Test in the window to fire it again.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="max-w-[1180px]">
|
||||
<AutomationsDemo />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
TRIGGERS · nine moments it can fire
|
||||
============================================================ -->
|
||||
<section class="border-b border-[color:var(--border)] py-20 md:py-28">
|
||||
<div class="container-page">
|
||||
<div class="max-w-2xl mb-12">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Triggers</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
Nine moments it can fire.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed">
|
||||
Every automation starts from one event. Replies arrive already classified, meetings arrive matched to the contact, and a campaign step can launch an automation for each contact that reaches it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-px bg-[color:var(--border)] rounded-[14px] overflow-hidden ring-1 ring-[color:var(--border)]">
|
||||
{triggers.map((t) => (
|
||||
<div class="bg-white p-6">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="inline-flex size-8 items-center justify-center rounded-md bg-sky-100 text-sky-600 ring-1 ring-sky-200/70">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" set:html={tIcons[t.icon]}></svg>
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="text-[14.5px] font-semibold text-heading leading-tight">{t.label}</div>
|
||||
<div class="mt-0.5 font-mono text-[10.5px] text-muted-foreground truncate">{t.key}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3.5 text-[13px] text-foreground/65 leading-relaxed">{t.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
ACTIONS · built-in + across the stack
|
||||
============================================================ -->
|
||||
<section class="border-b border-[color:var(--border)] bg-[color:var(--surface-1)]/40 py-20 md:py-28">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_1.5fr] gap-12 lg:gap-20 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] font-mono text-[#0284c7] mb-3">Actions</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
Do it in Warmbly. Do it everywhere else.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed max-w-md">
|
||||
Seven actions ship built in and need no connection at all. The rest reach your stack through the integrations you have already connected, with message templates rendered per event.
|
||||
</p>
|
||||
<ul class="mt-6 space-y-2.5 text-[14px] text-foreground/85">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0" /><span>Built-in actions run inside Warmbly: tags, tasks, deals, suppression.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0" /><span>Stack actions reuse the connection: pick it from a dropdown, done.</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden">
|
||||
<div class="px-5 py-3 bg-[color:var(--surface-1)] border-b border-[color:var(--border)] text-[10px] uppercase tracking-[0.18em] font-mono text-muted-foreground">Built in · no connection needed</div>
|
||||
{nativeActions.map((a) => (
|
||||
<div class="flex items-center gap-3 px-5 py-3 border-b border-[color:var(--border)] last:border-b-0">
|
||||
<span class={`inline-flex size-7 shrink-0 items-center justify-center rounded-md ring-1 ${a.tint}`}>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={aIcons[a.icon]}></svg>
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="text-[13px] font-medium text-heading">{a.name}</span>
|
||||
<span class="hidden md:inline text-[12.5px] text-foreground/55"> · {a.desc}</span>
|
||||
</div>
|
||||
<code class="hidden sm:inline font-mono text-[10.5px] text-muted-foreground shrink-0">{a.key}</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden">
|
||||
<div class="px-5 py-3 bg-[color:var(--surface-1)] border-b border-[color:var(--border)] text-[10px] uppercase tracking-[0.18em] font-mono text-muted-foreground">Across your stack</div>
|
||||
{stackActions.map((s) => (
|
||||
<div class="flex items-center gap-3 px-5 py-3 border-b border-[color:var(--border)] last:border-b-0">
|
||||
<span class={`inline-flex size-7 shrink-0 items-center justify-center rounded-md ring-1 text-[11px] font-bold ${s.tile}`}>{s.initial}</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="text-[13px] font-medium text-heading">{s.name}</span>
|
||||
<span class="text-[12.5px] text-foreground/55"> · {s.action}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
BRANCHING · the IF surface
|
||||
============================================================ -->
|
||||
<section class="border-b border-[color:var(--border)] py-20 md:py-28">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_1.3fr] gap-12 lg:gap-16 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Branching</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
One chip, two paths, full graph.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed max-w-md">
|
||||
A condition reads one field off the event and routes it: yes out the right dot, no out the bottom. Chain conditions for AND logic, point both paths anywhere, and converge them back when the playbooks rejoin.
|
||||
</p>
|
||||
<ul class="mt-6 space-y-2.5 text-[14px] text-foreground/85">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0" /><span>Fields follow the trigger: intent and confidence on replies, source on meetings, provider and reason on bounces.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0" /><span>A random split routes a weighted share of events down a different path.</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- the IF editor, drawn out -->
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden">
|
||||
<div class="flex items-center gap-2 border-b border-[color:var(--border)] px-4 py-3">
|
||||
<Icon name="gitBranch" size={14} class="text-sky-600" />
|
||||
<div>
|
||||
<div class="text-[10px] uppercase tracking-[0.14em] text-muted-foreground font-mono">Condition</div>
|
||||
<div class="text-[11.5px] text-foreground/55">on campaign.reply_received</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 grid sm:grid-cols-3 gap-2.5">
|
||||
<div>
|
||||
<div class="text-[10px] uppercase tracking-[0.14em] text-slate-400 font-medium mb-1.5">Field</div>
|
||||
<div class="h-8 px-2.5 rounded-md border border-slate-200 bg-white text-[12.5px] text-slate-900 inline-flex w-full items-center justify-between">intent <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="text-slate-400"><path d="m6 9 6 6 6-6"/></svg></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[10px] uppercase tracking-[0.14em] text-slate-400 font-medium mb-1.5">Operator</div>
|
||||
<div class="h-8 px-2.5 rounded-md border border-slate-200 bg-white text-[12.5px] text-slate-900 inline-flex w-full items-center justify-between">equals <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="text-slate-400"><path d="m6 9 6 6 6-6"/></svg></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[10px] uppercase tracking-[0.14em] text-slate-400 font-medium mb-1.5">Value</div>
|
||||
<div class="h-8 px-2.5 rounded-md border border-sky-300 ring-2 ring-sky-100 bg-white text-[12.5px] text-slate-900 inline-flex w-full items-center">positive</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 pb-4 flex flex-wrap items-center gap-1.5">
|
||||
<span class="text-[10px] uppercase tracking-[0.14em] text-slate-400 font-medium mr-1">Fields by trigger</span>
|
||||
{conditionFields.map((f) => (
|
||||
<span class="inline-flex items-center gap-1.5 px-1.5 py-0.5 rounded border border-slate-200 bg-slate-50 font-mono text-[10.5px] text-slate-600">
|
||||
{f.field}
|
||||
<span class="text-slate-400">· {f.on}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- operators per type -->
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden">
|
||||
<div class="grid grid-cols-[110px_minmax(0,1fr)] px-5 py-3 bg-[color:var(--surface-1)] border-b border-[color:var(--border)] text-[10px] uppercase tracking-[0.18em] font-mono text-muted-foreground">
|
||||
<div>Field type</div>
|
||||
<div>Operators</div>
|
||||
</div>
|
||||
{operators.map((o) => (
|
||||
<div class="grid grid-cols-[110px_minmax(0,1fr)] px-5 py-3 border-b border-[color:var(--border)] last:border-b-0 items-center">
|
||||
<span class="font-mono text-[12px] text-heading">{o.type}</span>
|
||||
<span class="text-[12.5px] text-foreground/70">{o.ops}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
TEMPLATING · per-event Go templates, rendered
|
||||
============================================================ -->
|
||||
<section class="border-b border-[color:var(--border)] bg-[color:var(--surface-1)]/40 py-20 md:py-28">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_1.3fr] gap-12 lg:gap-16 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Templating</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
Messages written from the event.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed max-w-md">
|
||||
Slack messages, Discord messages and webhook payloads are Go templates, the same syntax as the rest of Warmbly. Pull any field the trigger carries and the message reads like a teammate wrote it.
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-muted-foreground mb-2">Reply event fields</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{variables.map((v) => (
|
||||
<code class="inline-flex items-center h-6 px-2 rounded-md bg-[#0c1224] text-[11.5px] font-mono text-sky-300 ring-1 ring-white/10">{v}</code>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden">
|
||||
<!-- template source -->
|
||||
<div class="bg-[#0c1224]">
|
||||
<div class="px-4 py-2.5 border-b border-white/10 flex items-center gap-2">
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-[#ff5f56]/80"></span>
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-[#ffbd2e]/80"></span>
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-[#27c93f]/80"></span>
|
||||
<span class="ml-2 text-[11px] font-mono text-white/55">slack message · template</span>
|
||||
</div>
|
||||
<div class="p-5 font-mono text-[12.5px] leading-[1.85] text-white/85">
|
||||
<div>:fire: <span class="text-sky-300">{`{{.contact_email}}`}</span> replied to <span class="text-sky-300">{`{{.subject}}`}</span></div>
|
||||
<div>intent <span class="text-sky-300">{`{{.intent}}`}</span> at <span class="text-sky-300">{`{{.confidence}}`}</span>% · “<span class="text-sky-300">{`{{.snippet}}`}</span>”</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- rendered slack message -->
|
||||
<div class="p-5">
|
||||
<div class="flex items-center gap-2 mb-3 text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground">
|
||||
<Icon name="arrowRight" size={12} class="text-[#0284c7]" />
|
||||
Lands in #sales as
|
||||
</div>
|
||||
<div class="rounded-[10px] bg-[color:var(--surface-1)]/60 ring-1 ring-[color:var(--border)] p-4">
|
||||
<div class="flex items-start gap-2.5">
|
||||
<span class="inline-flex size-8 shrink-0 items-center justify-center rounded-md bg-fuchsia-50 text-fuchsia-600 ring-1 ring-fuchsia-200 text-[13px] font-bold">S</span>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="text-[13px] font-semibold text-heading">Warmbly</span>
|
||||
<span class="inline-flex items-center h-4 px-1 rounded-sm bg-slate-200/80 text-[9px] font-semibold uppercase tracking-wide text-slate-500">app</span>
|
||||
<span class="text-[11px] text-muted-foreground font-mono">2:14 pm</span>
|
||||
</div>
|
||||
<p class="mt-1 text-[13.5px] leading-relaxed text-foreground/85">
|
||||
🔥 <span class="text-[#0369a1] font-medium">maya@northwind.dev</span> replied to <span class="font-medium text-heading">Re: Quick question</span><br/>
|
||||
intent <span class="text-emerald-700 font-medium">positive</span> at 92% · “Thursday works for me”
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 text-[12px] text-foreground/60 leading-relaxed">
|
||||
Rendered with the event that fired the run. A blank field never breaks the message, it just renders empty.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
TEST RUNS & HISTORY
|
||||
============================================================ -->
|
||||
<section class="border-b border-[color:var(--border)] py-20 md:py-28">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_1.3fr] gap-12 lg:gap-16 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] font-mono text-[#0284c7] mb-3">Test runs & history</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
Know what it did, every time.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed max-w-md">
|
||||
Test fires a dry run against a sample event: every node reports what it would do, every condition reports yes or no, and nothing leaves Warmbly. Once live, every real run lands in history with per-node results, so a broken step is a read, not an investigation.
|
||||
</p>
|
||||
<ul class="mt-6 space-y-2.5 text-[14px] text-foreground/85">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0" /><span>Per-node verdicts, with the provider response kept on failures.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0" /><span>Branch decisions are recorded too: you can see which path a contact took.</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden">
|
||||
<div class="flex items-center gap-2 border-b border-[color:var(--border)] px-4 py-3">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse-soft"></span>
|
||||
<div class="text-[10px] uppercase tracking-[0.14em] text-muted-foreground font-mono">Run history · Reply to revenue</div>
|
||||
</div>
|
||||
|
||||
<!-- run 1: expanded -->
|
||||
<div class="border-b border-[color:var(--border)]">
|
||||
<div class="flex items-center gap-2.5 px-4 py-3">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-emerald-500 shrink-0" set:html={aIcons.checkCircle}></svg>
|
||||
<span class="text-[13px] font-medium text-heading">Run passed</span>
|
||||
<span class="text-[11.5px] text-muted-foreground font-mono">2 min ago · 5 steps · 1.9 s</span>
|
||||
</div>
|
||||
<div class="px-4 pb-3 space-y-1.5">
|
||||
{[
|
||||
{ label: 'Trigger · Prospect replies', right: 'maya@northwind.dev', ok: true },
|
||||
{ label: 'intent is positive · ≥ 80%', right: 'yes · 92%', ok: true },
|
||||
{ label: 'Slack message · #sales', right: '212 ms', ok: true },
|
||||
{ label: 'Deal created · Discovery', right: '$4,800', ok: true },
|
||||
{ label: 'HubSpot contact updated', right: '2 fields', ok: true },
|
||||
].map((r) => (
|
||||
<div class="flex items-center gap-2 rounded-md border border-slate-200 px-2.5 py-1.5">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-emerald-500 shrink-0" set:html={aIcons.checkCircle}></svg>
|
||||
<span class="text-[11.5px] font-medium text-slate-700 truncate">{r.label}</span>
|
||||
<span class="ml-auto shrink-0 font-mono text-[10.5px] text-slate-400">{r.right}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- run 2: took the no branch -->
|
||||
<div class="flex items-center gap-2.5 px-4 py-3 border-b border-[color:var(--border)]">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-slate-400 shrink-0" set:html={aIcons.gitBranch}></svg>
|
||||
<span class="text-[13px] font-medium text-heading">Run passed</span>
|
||||
<span class="text-[11.5px] text-muted-foreground font-mono">1 h ago · took the no path · 2 steps</span>
|
||||
<span class="ml-auto inline-flex items-center h-5 px-1.5 rounded text-[10px] font-medium bg-slate-100 text-slate-500 border border-slate-200">no · 41%</span>
|
||||
</div>
|
||||
|
||||
<!-- run 3: failed -->
|
||||
<div class="px-4 py-3">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-rose-500 shrink-0" set:html={aIcons.xCircle}></svg>
|
||||
<span class="text-[13px] font-medium text-heading">Run failed</span>
|
||||
<span class="text-[11.5px] text-muted-foreground font-mono">yesterday · step 3 of 5</span>
|
||||
<span class="ml-auto inline-flex items-center h-5 px-1.5 rounded text-[10px] font-medium bg-rose-50 text-rose-600 border border-rose-200">failed</span>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-2 rounded-md border border-rose-200 bg-rose-50/50 px-2.5 py-1.5">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-rose-500 shrink-0" set:html={aIcons.xCircle}></svg>
|
||||
<span class="text-[11.5px] font-medium text-slate-700 truncate">Slack message · #sales</span>
|
||||
<span class="ml-auto shrink-0 font-mono text-[10.5px] text-rose-600">404 · channel_not_found</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
TRUST · how it behaves under load and failure
|
||||
============================================================ -->
|
||||
<section class="border-b border-[color:var(--border)] bg-[color:var(--surface-1)]/40 py-20 md:py-24">
|
||||
<div class="container-page">
|
||||
<div class="max-w-2xl mb-12">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Plumbing</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
Built to not wake you up.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-px bg-[color:var(--border)] rounded-[12px] overflow-hidden ring-1 ring-[color:var(--border)]">
|
||||
{trust.map((c) => (
|
||||
<div class="bg-white p-6">
|
||||
<Icon name={c.icon} size={16} class="text-[#0284c7]" />
|
||||
<div class="mt-3.5 text-[15px] font-semibold text-heading">{c.t}</div>
|
||||
<p class="mt-1.5 text-[13px] text-foreground/65 leading-relaxed">{c.d}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
FAQ
|
||||
============================================================ -->
|
||||
<section class="border-b border-[color:var(--border)] py-20 md:py-24">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_1.8fr] gap-12 lg:gap-20 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Automations FAQ</div>
|
||||
<h2 class="text-[28px] md:text-[36px] font-semibold tracking-[-0.025em] leading-[1.08] text-heading">
|
||||
Five questions about how runs behave.
|
||||
</h2>
|
||||
</div>
|
||||
<div class="divide-y divide-[color:var(--border)]" data-faq>
|
||||
{faq.map(([q, a]) => (
|
||||
<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="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>
|
||||
</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>
|
||||
|
||||
<CTA
|
||||
title="Wire your first trigger in two minutes."
|
||||
description="Pick the reply trigger, point it at Slack, press Test. Then flip it on."
|
||||
primaryLabel="Start free trial"
|
||||
primaryHref="https://app.warmbly.com/register"
|
||||
secondaryLabel="Browse the integrations"
|
||||
secondaryHref="/integrations/"
|
||||
/>
|
||||
</Layout>
|
||||
@@ -15,6 +15,7 @@ import CRMMock from '../components/CRMMock.astro';
|
||||
import TemplatingShowcase from '../components/TemplatingShowcase.astro';
|
||||
import ChangelogShowcase from '../components/ChangelogShowcase.astro';
|
||||
import OpenSourceShowcase from '../components/OpenSourceShowcase.astro';
|
||||
import AutomationsShowcase from '../components/AutomationsShowcase.astro';
|
||||
|
||||
const compare = [
|
||||
['Per-mailbox cold cap', '50 / day default', '200 / day if you ask for it'],
|
||||
@@ -343,6 +344,13 @@ const softwareJsonLd = {
|
||||
<IntegrationsMock />
|
||||
</FeatureSection>
|
||||
|
||||
<!-- ============================================================
|
||||
AUTOMATIONS — full-width centerpiece: the live builder demo
|
||||
running a test end to end. Deliberately breaks the side-by-side
|
||||
rhythm so it reads as the headline act of the showcase.
|
||||
============================================================ -->
|
||||
<AutomationsShowcase />
|
||||
|
||||
<FeatureSection
|
||||
eyebrow="Developers"
|
||||
title="An API you can build a product on."
|
||||
|
||||
Reference in New Issue
Block a user