mango-lollipop 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +69 -0
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/bin/mango-lollipop.js +385 -0
- package/dist/excel.d.ts +4 -0
- package/dist/excel.js +342 -0
- package/dist/html.d.ts +4 -0
- package/dist/html.js +938 -0
- package/dist/schema.d.ts +120 -0
- package/dist/schema.js +211 -0
- package/lib/excel.ts +433 -0
- package/lib/html.ts +993 -0
- package/lib/schema.ts +394 -0
- package/package.json +44 -0
- package/skills/audit/SKILL.md +248 -0
- package/skills/dev-handoff/SKILL.md +295 -0
- package/skills/generate-dashboard/SKILL.md +195 -0
- package/skills/generate-matrix/SKILL.md +374 -0
- package/skills/generate-messages/SKILL.md +262 -0
- package/skills/iterate/SKILL.md +242 -0
- package/skills/start/SKILL.md +310 -0
- package/templates/copywriting-guide.md +155 -0
- package/templates/dashboard.html +522 -0
- package/templates/events/saas-collaboration.yaml +50 -0
- package/templates/events/saas-document.yaml +44 -0
- package/templates/events/saas-general.yaml +38 -0
- package/templates/events/saas-marketplace.yaml +48 -0
- package/templates/overview.html +598 -0
- package/templates/saas-matrix.json +172 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# SaaS Lifecycle Email Copywriting Guide
|
|
2
|
+
|
|
3
|
+
Reference guide for generating high-quality lifecycle messages. Distilled from real-world SaaS onboarding sequences (Clay, ActiveCampaign, Asana, ClickUp, Typeform, Customer.io, Intercom) and product-led growth best practices.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The Three-Track Model
|
|
8
|
+
|
|
9
|
+
Structure lifecycle messaging around three behavioral tracks, not arbitrary time delays:
|
|
10
|
+
|
|
11
|
+
### Track 1: Quick Win (Acquisition + Activation)
|
|
12
|
+
- **Goal:** Get users to their first "aha moment" as fast as possible
|
|
13
|
+
- **Trigger:** Signup / email verification
|
|
14
|
+
- **End condition:** User achieves first value (not a time limit)
|
|
15
|
+
- **Tone:** Helpful, zero sales pressure
|
|
16
|
+
- **Rule:** No upselling, no promotional content — pure user success
|
|
17
|
+
- Follow up 2-3 times on non-openers (40% miss the first send)
|
|
18
|
+
- Welcome email target open rate: 60%+
|
|
19
|
+
|
|
20
|
+
### Track 2: The Hook (Activation + Retention)
|
|
21
|
+
- **Goal:** Build habit through repeated value delivery
|
|
22
|
+
- **Trigger:** First value achieved
|
|
23
|
+
- **Pattern:** External trigger (email) → Action (user opens product) → Reward (sees result) → Investment (spends time/data)
|
|
24
|
+
- **End condition:** User becomes a Product-Qualified Lead (PQL)
|
|
25
|
+
- Continue until user's behavior shows they don't need external prompts
|
|
26
|
+
|
|
27
|
+
### Track 3: Conversion (Revenue)
|
|
28
|
+
- **Goal:** Convert PQL to paid
|
|
29
|
+
- **Trigger:** User hits PQL threshold (behavioral, not time-based)
|
|
30
|
+
- **Rule:** Reach out within minutes of PQL status — you'll never have a better window
|
|
31
|
+
- Share case studies showing value others experienced
|
|
32
|
+
- Offer trial extensions or surveys if immediate conversion fails
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Proven Sequence Patterns (from real SaaS companies)
|
|
37
|
+
|
|
38
|
+
### Pattern: Guided Training (Clay)
|
|
39
|
+
- 6-part numbered sequence: "Welcome to Clay! (1/6)", "Find your first leads (2/6)", etc.
|
|
40
|
+
- Subject lines telegraph the full sequence length — sets expectations
|
|
41
|
+
- Each email covers exactly ONE actionable step
|
|
42
|
+
- Progressive skill-building: each email builds on the previous
|
|
43
|
+
- Offer a "university" or learning path alongside the drip
|
|
44
|
+
- **Use when:** Product has a clear workflow users need to learn step-by-step
|
|
45
|
+
|
|
46
|
+
### Pattern: Behavior-Driven Nudging (ActiveCampaign)
|
|
47
|
+
- Detect incomplete setup and adapt messaging accordingly
|
|
48
|
+
- Vary the angle each email: benefits, features, integrations, AI capabilities, ROI stats, social proof
|
|
49
|
+
- Always make action feel easy: "just 5 steps", "a few clicks away"
|
|
50
|
+
- Offer live human assistance early and often
|
|
51
|
+
- Keep friction language throughout ("no credit card required")
|
|
52
|
+
- Close with urgency: "Don't lose access to your free trial"
|
|
53
|
+
- **Use when:** Product has a setup/activation step many users don't complete
|
|
54
|
+
|
|
55
|
+
### Pattern: Progressive Feature Education (Asana)
|
|
56
|
+
- Time feature education to match typical user progression across trial
|
|
57
|
+
- Early: simple setup, invite teammates, build first project
|
|
58
|
+
- Mid-trial: higher-value features (AI, automation, goals)
|
|
59
|
+
- Reinforce ROI language consistently (clarity, team impact, time savings)
|
|
60
|
+
- Mid-trial milestone: "You're halfway through your Asana trial"
|
|
61
|
+
- Trial end: reassure they won't lose work, encourage plan selection
|
|
62
|
+
- **Use when:** Product has a trial period and features that unlock value progressively
|
|
63
|
+
|
|
64
|
+
### Pattern: Progress Milestones (Typeform)
|
|
65
|
+
- Track user progress with percentages: "You're 20% of the way there", "40% complete"
|
|
66
|
+
- Each email triggered by completing the previous step (behavioral, not time-based)
|
|
67
|
+
- Introduce upsell early but make it prominent only after value is demonstrated
|
|
68
|
+
- Final email congratulates: "You're pretty much a guru" — makes upsell feel earned
|
|
69
|
+
- **Use when:** Product has a clear onboarding checklist with measurable steps
|
|
70
|
+
|
|
71
|
+
### Pattern: Adaptive Reminders (Customer.io)
|
|
72
|
+
- Start with personalized greeting based on signup data
|
|
73
|
+
- Recommend ONE specific next step (reduce cognitive load)
|
|
74
|
+
- If steps remain incomplete, send follow-up with the same 3 actions
|
|
75
|
+
- Final nudge: friendly, low-pressure — "We hope these help you understand"
|
|
76
|
+
- Surface technical setup (like domain verification) early but frame as helpful
|
|
77
|
+
- **Use when:** Product has critical technical setup steps users might skip
|
|
78
|
+
|
|
79
|
+
### Pattern: Single Activation Email (Intercom)
|
|
80
|
+
- One email: social proof + single CTA to start trial
|
|
81
|
+
- Assumes familiarity — works for well-known products
|
|
82
|
+
- VP of Product persona adds authority without personal outreach
|
|
83
|
+
- Minimal friction for users ready to dive in
|
|
84
|
+
- **Use when:** Product has strong brand recognition; users arrive already educated
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Email Copy Rules
|
|
89
|
+
|
|
90
|
+
### Structure
|
|
91
|
+
- **One action per email.** Every email drives exactly one outcome.
|
|
92
|
+
- **Problem/benefit → specific action → single CTA.** This is the universal structure.
|
|
93
|
+
- **Short paragraphs.** 2-3 sentences max per paragraph.
|
|
94
|
+
- **Numbered steps for instructions.** Break processes into 2-4 digestible steps.
|
|
95
|
+
- **Skimmable.** Use headers, bullets, bold for key phrases. Users scan, not read.
|
|
96
|
+
|
|
97
|
+
### Subject Lines
|
|
98
|
+
- **Be specific about content.** "Find your first leads (2/6)" beats "Tips for getting started"
|
|
99
|
+
- **Show sequence position.** Numbering (1/6, 2/6) sets expectations and builds momentum
|
|
100
|
+
- **Benefit-first.** "Master your agenda in 2 minutes" beats "Agenda feature update"
|
|
101
|
+
- **Use personalization sparingly.** {{first_name}} in subject lines only when it feels natural
|
|
102
|
+
- **Avoid revealing ignorance.** Never send "Did you try X?" if you don't know whether they did — it signals generic, impersonal outreach (the "nagging email" anti-pattern)
|
|
103
|
+
|
|
104
|
+
### CTAs
|
|
105
|
+
- **One primary CTA per email.** Multiple CTAs confuse users.
|
|
106
|
+
- **Action verbs.** "Create your first agenda" not "Learn more"
|
|
107
|
+
- **Link to specific in-app actions.** Deep links to the exact screen, not generic landing pages.
|
|
108
|
+
- **Make effort feel small.** "Takes 2 minutes", "Just 3 steps", "One click setup"
|
|
109
|
+
|
|
110
|
+
### Tone
|
|
111
|
+
- **Benefit-driven, not feature-driven.** "See all your team's work in one place" not "We have a dashboard feature"
|
|
112
|
+
- **Jobs-to-be-done framing.** Anchor around the problem users hired your product to solve
|
|
113
|
+
- **Build confidence, not FOMO.** "You're 40% of the way there" beats "You're missing out"
|
|
114
|
+
- **Low pressure on setup emails.** Friendly final nudges, never aggressive
|
|
115
|
+
- **Plain text for welcome emails.** Reduces spam risk, feels more personal
|
|
116
|
+
|
|
117
|
+
### Anti-Patterns to Avoid
|
|
118
|
+
- **Time-based spam.** Sending emails on a fixed schedule with no behavioral context
|
|
119
|
+
- **Feature lists.** Listing capabilities instead of showing outcomes
|
|
120
|
+
- **Premature selling.** Upselling before the user has experienced value
|
|
121
|
+
- **Repeated requests.** Asking the same thing across multiple emails without new information
|
|
122
|
+
- **Generic subject lines.** "Getting started with [Product]" tells users nothing specific
|
|
123
|
+
- **Multiple CTAs.** Forces users to choose, which often means they choose nothing
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Benchmarks (SaaS Industry)
|
|
128
|
+
|
|
129
|
+
| Metric | Poor | Average | Good | Excellent |
|
|
130
|
+
|--------|------|---------|------|-----------|
|
|
131
|
+
| Welcome email open rate | < 40% | 40-50% | 50-60% | > 60% |
|
|
132
|
+
| Lifecycle open rate | < 15% | 15-25% | 25-35% | > 35% |
|
|
133
|
+
| Click rate | < 1.5% | 1.5-3% | 3-5% | > 5% |
|
|
134
|
+
| Click-to-open rate | < 8% | 8-12% | 12-18% | > 18% |
|
|
135
|
+
| Unsubscribe rate | > 1% | 0.5-1% | 0.2-0.5% | < 0.2% |
|
|
136
|
+
| Trial-to-paid conversion | < 5% | 5-10% | 10-20% | > 20% |
|
|
137
|
+
|
|
138
|
+
### Timing Windows
|
|
139
|
+
- **PQL conversion:** Reach out within minutes (highest conversion probability)
|
|
140
|
+
- **Welcome email follow-up:** Re-send to non-openers after 24h
|
|
141
|
+
- **Activation drip cadence:** 2-3 days between emails (avoid daily bombardment)
|
|
142
|
+
- **Re-engagement:** 3 → 7 → 14 day escalation for inactive users
|
|
143
|
+
- **Trial ending:** First warning 3-7 days before expiry, final nudge on expiry day
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Applying Patterns to AARRR Stages
|
|
148
|
+
|
|
149
|
+
| Stage | Best Pattern | Key Insight |
|
|
150
|
+
|-------|-------------|-------------|
|
|
151
|
+
| **Acquisition (AQ)** | Single Activation / Welcome | Social proof, one CTA, set expectations for what's coming |
|
|
152
|
+
| **Activation (AC)** | Guided Training / Progress Milestones | One feature per email, suppress if already used, show progress |
|
|
153
|
+
| **Revenue (RV)** | Progressive Feature Education | Introduce upsell after value demonstrated, mid-trial milestone reminder |
|
|
154
|
+
| **Retention (RT)** | Behavior-Driven Nudging / Adaptive Reminders | Vary angles, detect inactivity, low-pressure tone |
|
|
155
|
+
| **Referral (RF)** | Progress Milestones | Trigger after success moments, make it feel earned ("You're a guru") |
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{PROJECT_NAME}} — Mango Lollipop Dashboard</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
|
9
|
+
<script>
|
|
10
|
+
tailwind.config = {
|
|
11
|
+
theme: {
|
|
12
|
+
extend: {
|
|
13
|
+
colors: {
|
|
14
|
+
mango: { 50: '#fff8ed', 100: '#ffefcf', 200: '#ffdb9e', 300: '#ffc162', 400: '#ffa033', 500: '#ff850c', 600: '#f06a02', 700: '#c74f08', 800: '#9e3f0f', 900: '#7f3510' },
|
|
15
|
+
stage: {
|
|
16
|
+
tx: '#6b7280',
|
|
17
|
+
aq: '#22c55e',
|
|
18
|
+
ac: '#3b82f6',
|
|
19
|
+
rv: '#eab308',
|
|
20
|
+
rt: '#f97316',
|
|
21
|
+
rf: '#a855f7',
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
<style>
|
|
29
|
+
.stage-badge-TX { background-color: #f3f4f6; color: #374151; }
|
|
30
|
+
.stage-badge-AQ { background-color: #dcfce7; color: #166534; }
|
|
31
|
+
.stage-badge-AC { background-color: #dbeafe; color: #1e40af; }
|
|
32
|
+
.stage-badge-RV { background-color: #fef9c3; color: #854d0e; }
|
|
33
|
+
.stage-badge-RT { background-color: #ffedd5; color: #9a3412; }
|
|
34
|
+
.stage-badge-RF { background-color: #f3e8ff; color: #6b21a8; }
|
|
35
|
+
|
|
36
|
+
.channel-icon { display: inline-block; margin-right: 2px; font-size: 0.75rem; }
|
|
37
|
+
|
|
38
|
+
.tag-pill { cursor: pointer; transition: all 0.15s; }
|
|
39
|
+
.tag-pill:hover { opacity: 0.8; transform: scale(1.05); }
|
|
40
|
+
.tag-pill.active { ring: 2px; box-shadow: 0 0 0 2px #ff850c; }
|
|
41
|
+
|
|
42
|
+
.matrix-row { cursor: pointer; transition: background-color 0.1s; }
|
|
43
|
+
.matrix-row:hover { background-color: #f9fafb; }
|
|
44
|
+
.matrix-row.expanded { background-color: #fffbeb; }
|
|
45
|
+
|
|
46
|
+
.expand-panel { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
|
|
47
|
+
.expand-panel.open { max-height: 600px; }
|
|
48
|
+
|
|
49
|
+
.sort-header { cursor: pointer; user-select: none; }
|
|
50
|
+
.sort-header:hover { color: #ff850c; }
|
|
51
|
+
.sort-indicator { font-size: 0.6rem; margin-left: 4px; }
|
|
52
|
+
|
|
53
|
+
.view-toggle button { transition: all 0.15s; }
|
|
54
|
+
.view-toggle button.active { background-color: #1f2937; color: white; }
|
|
55
|
+
.view-toggle button:not(.active) { background-color: #f3f4f6; color: #374151; }
|
|
56
|
+
|
|
57
|
+
.mermaid { max-width: 100%; overflow-x: auto; }
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body class="bg-gray-50 text-gray-900 min-h-screen">
|
|
61
|
+
|
|
62
|
+
<!-- ===== HEADER ===== -->
|
|
63
|
+
<header class="bg-gray-900 text-white">
|
|
64
|
+
<div class="max-w-screen-2xl mx-auto px-6 py-4 flex items-center justify-between">
|
|
65
|
+
<div class="flex items-center gap-4">
|
|
66
|
+
<h1 class="text-xl font-bold tracking-tight">{{PROJECT_NAME}}</h1>
|
|
67
|
+
<span class="text-gray-400 text-sm">Mango Lollipop</span>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="flex items-center gap-6 text-sm">
|
|
70
|
+
<div class="text-center">
|
|
71
|
+
<div class="text-2xl font-bold text-white">{{TOTAL_MESSAGES}}</div>
|
|
72
|
+
<div class="text-gray-400">Messages</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="text-center">
|
|
75
|
+
<div class="text-2xl font-bold text-white">{{TOTAL_CHANNELS}}</div>
|
|
76
|
+
<div class="text-gray-400">Channels</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="text-center">
|
|
79
|
+
<div class="text-2xl font-bold text-white">{{TOTAL_TAGS}}</div>
|
|
80
|
+
<div class="text-gray-400">Tags</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<!-- Stage stat bar -->
|
|
86
|
+
<div class="bg-gray-800">
|
|
87
|
+
<div class="max-w-screen-2xl mx-auto px-6 py-2 flex items-center gap-4 text-sm overflow-x-auto">
|
|
88
|
+
<button onclick="filterStage('all')" class="stage-filter px-3 py-1 rounded-full bg-gray-700 text-gray-300 hover:bg-gray-600 whitespace-nowrap" data-stage="all">
|
|
89
|
+
All
|
|
90
|
+
</button>
|
|
91
|
+
<button onclick="filterStage('TX')" class="stage-filter px-3 py-1 rounded-full stage-badge-TX hover:opacity-80 whitespace-nowrap" data-stage="TX">
|
|
92
|
+
TX: {{COUNT_TX}}
|
|
93
|
+
</button>
|
|
94
|
+
<button onclick="filterStage('AQ')" class="stage-filter px-3 py-1 rounded-full stage-badge-AQ hover:opacity-80 whitespace-nowrap" data-stage="AQ">
|
|
95
|
+
Acquisition: {{COUNT_AQ}}
|
|
96
|
+
</button>
|
|
97
|
+
<button onclick="filterStage('AC')" class="stage-filter px-3 py-1 rounded-full stage-badge-AC hover:opacity-80 whitespace-nowrap" data-stage="AC">
|
|
98
|
+
Activation: {{COUNT_AC}}
|
|
99
|
+
</button>
|
|
100
|
+
<button onclick="filterStage('RV')" class="stage-filter px-3 py-1 rounded-full stage-badge-RV hover:opacity-80 whitespace-nowrap" data-stage="RV">
|
|
101
|
+
Revenue: {{COUNT_RV}}
|
|
102
|
+
</button>
|
|
103
|
+
<button onclick="filterStage('RT')" class="stage-filter px-3 py-1 rounded-full stage-badge-RT hover:opacity-80 whitespace-nowrap" data-stage="RT">
|
|
104
|
+
Retention: {{COUNT_RT}}
|
|
105
|
+
</button>
|
|
106
|
+
<button onclick="filterStage('RF')" class="stage-filter px-3 py-1 rounded-full stage-badge-RF hover:opacity-80 whitespace-nowrap" data-stage="RF">
|
|
107
|
+
Referral: {{COUNT_RF}}
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</header>
|
|
112
|
+
|
|
113
|
+
<!-- ===== MAIN LAYOUT ===== -->
|
|
114
|
+
<div class="max-w-screen-2xl mx-auto flex">
|
|
115
|
+
|
|
116
|
+
<!-- ===== SIDEBAR: Tags ===== -->
|
|
117
|
+
<aside class="w-64 shrink-0 bg-white border-r border-gray-200 p-4 min-h-screen hidden lg:block">
|
|
118
|
+
<h2 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Filter by Tag</h2>
|
|
119
|
+
<div class="space-y-1" id="tag-sidebar">
|
|
120
|
+
{{TAG_FILTERS}}
|
|
121
|
+
<!-- Example tag items (replaced by lib/html.ts):
|
|
122
|
+
<button class="tag-pill w-full text-left text-sm px-3 py-1.5 rounded hover:bg-gray-100 flex justify-between items-center" data-tag="type:educational" onclick="toggleTag(this)">
|
|
123
|
+
<span>type:educational</span>
|
|
124
|
+
<span class="text-gray-400 text-xs">8</span>
|
|
125
|
+
</button>
|
|
126
|
+
-->
|
|
127
|
+
</div>
|
|
128
|
+
<hr class="my-4 border-gray-200">
|
|
129
|
+
<h2 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Channels</h2>
|
|
130
|
+
<div class="space-y-1" id="channel-sidebar">
|
|
131
|
+
{{CHANNEL_FILTERS}}
|
|
132
|
+
<!-- Example channel items (replaced by lib/html.ts):
|
|
133
|
+
<button class="tag-pill w-full text-left text-sm px-3 py-1.5 rounded hover:bg-gray-100 flex justify-between items-center" data-channel="email" onclick="toggleChannel(this)">
|
|
134
|
+
<span>email</span>
|
|
135
|
+
<span class="text-gray-400 text-xs">18</span>
|
|
136
|
+
</button>
|
|
137
|
+
-->
|
|
138
|
+
</div>
|
|
139
|
+
<hr class="my-4 border-gray-200">
|
|
140
|
+
<button onclick="clearFilters()" class="w-full text-sm text-gray-500 hover:text-gray-900 py-1.5">Clear all filters</button>
|
|
141
|
+
</aside>
|
|
142
|
+
|
|
143
|
+
<!-- ===== CONTENT ===== -->
|
|
144
|
+
<main class="flex-1 p-6 space-y-8 min-w-0">
|
|
145
|
+
|
|
146
|
+
<!-- View Toggle -->
|
|
147
|
+
<div class="flex items-center justify-between">
|
|
148
|
+
<div class="view-toggle flex gap-1 rounded-lg overflow-hidden border border-gray-200">
|
|
149
|
+
<button class="active px-4 py-2 text-sm font-medium" onclick="setView('lifecycle', this)">Lifecycle</button>
|
|
150
|
+
<button class="px-4 py-2 text-sm font-medium" onclick="setView('transactional', this)">Transactional</button>
|
|
151
|
+
<button class="px-4 py-2 text-sm font-medium" onclick="setView('all', this)">All</button>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="text-sm text-gray-500">
|
|
154
|
+
Showing <span id="visible-count">{{TOTAL_MESSAGES}}</span> of {{TOTAL_MESSAGES}} messages
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<!-- ===== JOURNEY MAP ===== -->
|
|
159
|
+
<section>
|
|
160
|
+
<h2 class="text-lg font-semibold text-gray-900 mb-3">Customer Journey</h2>
|
|
161
|
+
<div class="bg-white rounded-xl border border-gray-200 p-6 overflow-x-auto">
|
|
162
|
+
<div class="mermaid">
|
|
163
|
+
{{MERMAID_DIAGRAM}}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</section>
|
|
167
|
+
|
|
168
|
+
<!-- ===== MATRIX TABLE ===== -->
|
|
169
|
+
<section>
|
|
170
|
+
<h2 class="text-lg font-semibold text-gray-900 mb-3">Message Matrix</h2>
|
|
171
|
+
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
172
|
+
<div class="overflow-x-auto">
|
|
173
|
+
<table class="w-full text-sm" id="matrix-table">
|
|
174
|
+
<thead>
|
|
175
|
+
<tr class="bg-gray-50 border-b border-gray-200 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
|
176
|
+
<th class="sort-header px-4 py-3" data-sort="id" onclick="sortTable('id', this)">
|
|
177
|
+
ID<span class="sort-indicator"></span>
|
|
178
|
+
</th>
|
|
179
|
+
<th class="sort-header px-4 py-3" data-sort="stage" onclick="sortTable('stage', this)">
|
|
180
|
+
Stage<span class="sort-indicator"></span>
|
|
181
|
+
</th>
|
|
182
|
+
<th class="sort-header px-4 py-3" data-sort="name" onclick="sortTable('name', this)">
|
|
183
|
+
Name<span class="sort-indicator"></span>
|
|
184
|
+
</th>
|
|
185
|
+
<th class="sort-header px-4 py-3" data-sort="trigger" onclick="sortTable('trigger', this)">
|
|
186
|
+
Trigger<span class="sort-indicator"></span>
|
|
187
|
+
</th>
|
|
188
|
+
<th class="sort-header px-4 py-3" data-sort="wait" onclick="sortTable('wait', this)">
|
|
189
|
+
Wait<span class="sort-indicator"></span>
|
|
190
|
+
</th>
|
|
191
|
+
<th class="px-4 py-3">Channels</th>
|
|
192
|
+
<th class="px-4 py-3">CTA</th>
|
|
193
|
+
<th class="px-4 py-3">Tags</th>
|
|
194
|
+
</tr>
|
|
195
|
+
</thead>
|
|
196
|
+
<tbody id="matrix-body">
|
|
197
|
+
{{MATRIX_ROWS}}
|
|
198
|
+
<!-- Example row (replaced by lib/html.ts):
|
|
199
|
+
<tr class="matrix-row border-b border-gray-100" data-id="AQ-01" data-stage="AQ" data-classification="lifecycle" data-tags="type:educational" data-channels="email" onclick="toggleExpand(this)">
|
|
200
|
+
<td class="px-4 py-3 font-mono text-xs font-medium">AQ-01</td>
|
|
201
|
+
<td class="px-4 py-3"><span class="stage-badge-AQ text-xs font-medium px-2 py-0.5 rounded-full">AQ</span></td>
|
|
202
|
+
<td class="px-4 py-3 font-medium">Welcome to {product}</td>
|
|
203
|
+
<td class="px-4 py-3 text-gray-600 font-mono text-xs">user.email_verified</td>
|
|
204
|
+
<td class="px-4 py-3 text-gray-600 font-mono text-xs">PT5M</td>
|
|
205
|
+
<td class="px-4 py-3 text-gray-600">email</td>
|
|
206
|
+
<td class="px-4 py-3 text-gray-600">Try {feature}</td>
|
|
207
|
+
<td class="px-4 py-3"><span class="inline-block text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">type:educational</span></td>
|
|
208
|
+
</tr>
|
|
209
|
+
<tr class="expand-row hidden" data-expand-for="AQ-01">
|
|
210
|
+
<td colspan="8" class="px-4 py-0">
|
|
211
|
+
<div class="expand-panel">
|
|
212
|
+
<div class="py-4 grid grid-cols-2 gap-4 text-sm">
|
|
213
|
+
<div><span class="font-semibold text-gray-500">Subject:</span> Welcome!</div>
|
|
214
|
+
<div><span class="font-semibold text-gray-500">From:</span> CEO</div>
|
|
215
|
+
<div><span class="font-semibold text-gray-500">Segment:</span> Everyone</div>
|
|
216
|
+
<div><span class="font-semibold text-gray-500">Format:</span> rich</div>
|
|
217
|
+
<div class="col-span-2"><span class="font-semibold text-gray-500">Goal:</span> Onboarding start</div>
|
|
218
|
+
<div class="col-span-2"><span class="font-semibold text-gray-500">Guards:</span> None</div>
|
|
219
|
+
<div class="col-span-2"><span class="font-semibold text-gray-500">Suppressions:</span> None</div>
|
|
220
|
+
<div class="col-span-2"><span class="font-semibold text-gray-500">Body Preview:</span><p class="mt-1 text-gray-600 bg-gray-50 p-3 rounded">...</p></div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</td>
|
|
224
|
+
</tr>
|
|
225
|
+
-->
|
|
226
|
+
</tbody>
|
|
227
|
+
</table>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</section>
|
|
231
|
+
|
|
232
|
+
<!-- ===== STATS ===== -->
|
|
233
|
+
<section>
|
|
234
|
+
<h2 class="text-lg font-semibold text-gray-900 mb-3">Statistics</h2>
|
|
235
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
236
|
+
|
|
237
|
+
<!-- Messages per Stage -->
|
|
238
|
+
<div class="bg-white rounded-xl border border-gray-200 p-5">
|
|
239
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Messages per Stage</h3>
|
|
240
|
+
<div class="space-y-2">
|
|
241
|
+
<div class="flex items-center gap-2">
|
|
242
|
+
<span class="w-20 text-xs font-medium text-gray-500">TX</span>
|
|
243
|
+
<div class="flex-1 bg-gray-100 rounded-full h-4 overflow-hidden">
|
|
244
|
+
<div class="bg-stage-tx h-full rounded-full" style="width: {{PERCENT_TX}}%"></div>
|
|
245
|
+
</div>
|
|
246
|
+
<span class="text-xs text-gray-500 w-6 text-right">{{COUNT_TX}}</span>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="flex items-center gap-2">
|
|
249
|
+
<span class="w-20 text-xs font-medium text-gray-500">Acquisition</span>
|
|
250
|
+
<div class="flex-1 bg-gray-100 rounded-full h-4 overflow-hidden">
|
|
251
|
+
<div class="bg-stage-aq h-full rounded-full" style="width: {{PERCENT_AQ}}%"></div>
|
|
252
|
+
</div>
|
|
253
|
+
<span class="text-xs text-gray-500 w-6 text-right">{{COUNT_AQ}}</span>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="flex items-center gap-2">
|
|
256
|
+
<span class="w-20 text-xs font-medium text-gray-500">Activation</span>
|
|
257
|
+
<div class="flex-1 bg-gray-100 rounded-full h-4 overflow-hidden">
|
|
258
|
+
<div class="bg-stage-ac h-full rounded-full" style="width: {{PERCENT_AC}}%"></div>
|
|
259
|
+
</div>
|
|
260
|
+
<span class="text-xs text-gray-500 w-6 text-right">{{COUNT_AC}}</span>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="flex items-center gap-2">
|
|
263
|
+
<span class="w-20 text-xs font-medium text-gray-500">Revenue</span>
|
|
264
|
+
<div class="flex-1 bg-gray-100 rounded-full h-4 overflow-hidden">
|
|
265
|
+
<div class="bg-stage-rv h-full rounded-full" style="width: {{PERCENT_RV}}%"></div>
|
|
266
|
+
</div>
|
|
267
|
+
<span class="text-xs text-gray-500 w-6 text-right">{{COUNT_RV}}</span>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="flex items-center gap-2">
|
|
270
|
+
<span class="w-20 text-xs font-medium text-gray-500">Retention</span>
|
|
271
|
+
<div class="flex-1 bg-gray-100 rounded-full h-4 overflow-hidden">
|
|
272
|
+
<div class="bg-stage-rt h-full rounded-full" style="width: {{PERCENT_RT}}%"></div>
|
|
273
|
+
</div>
|
|
274
|
+
<span class="text-xs text-gray-500 w-6 text-right">{{COUNT_RT}}</span>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="flex items-center gap-2">
|
|
277
|
+
<span class="w-20 text-xs font-medium text-gray-500">Referral</span>
|
|
278
|
+
<div class="flex-1 bg-gray-100 rounded-full h-4 overflow-hidden">
|
|
279
|
+
<div class="bg-stage-rf h-full rounded-full" style="width: {{PERCENT_RF}}%"></div>
|
|
280
|
+
</div>
|
|
281
|
+
<span class="text-xs text-gray-500 w-6 text-right">{{COUNT_RF}}</span>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<!-- Channel Distribution -->
|
|
287
|
+
<div class="bg-white rounded-xl border border-gray-200 p-5">
|
|
288
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Channel Distribution</h3>
|
|
289
|
+
<div class="space-y-3" id="channel-stats">
|
|
290
|
+
{{CHANNEL_STATS}}
|
|
291
|
+
<!-- Example (replaced by lib/html.ts):
|
|
292
|
+
<div class="flex items-center justify-between">
|
|
293
|
+
<span class="text-sm text-gray-700">email</span>
|
|
294
|
+
<span class="text-sm font-semibold text-gray-900">18</span>
|
|
295
|
+
</div>
|
|
296
|
+
-->
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<!-- Tag Cloud -->
|
|
301
|
+
<div class="bg-white rounded-xl border border-gray-200 p-5">
|
|
302
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Tag Cloud</h3>
|
|
303
|
+
<div class="flex flex-wrap gap-1.5" id="tag-cloud">
|
|
304
|
+
{{TAG_CLOUD}}
|
|
305
|
+
<!-- Example (replaced by lib/html.ts):
|
|
306
|
+
<span class="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full">type:educational (8)</span>
|
|
307
|
+
-->
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
</div>
|
|
312
|
+
</section>
|
|
313
|
+
|
|
314
|
+
</main>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<!-- ===== FOOTER ===== -->
|
|
318
|
+
<footer class="bg-gray-100 border-t border-gray-200 text-center py-4 text-xs text-gray-500">
|
|
319
|
+
<a href="https://github.com/sr-kai/mango-lollipop" class="text-gray-600 hover:text-gray-900 font-medium">Mango Lollipop</a> — AI-powered lifecycle messaging for SaaS<br>
|
|
320
|
+
Made by Sasha Kai with probably too much coffee. · {{GENERATED_AT}}
|
|
321
|
+
</footer>
|
|
322
|
+
|
|
323
|
+
<!-- ===== PROJECT DATA (injected by lib/html.ts) ===== -->
|
|
324
|
+
<script id="project-data" type="application/json">
|
|
325
|
+
{{PROJECT_DATA}}
|
|
326
|
+
</script>
|
|
327
|
+
|
|
328
|
+
<!-- ===== INTERACTIVE JS ===== -->
|
|
329
|
+
<script>
|
|
330
|
+
// Initialize Mermaid
|
|
331
|
+
mermaid.initialize({ startOnLoad: true, theme: 'default', securityLevel: 'loose' });
|
|
332
|
+
|
|
333
|
+
// State
|
|
334
|
+
let currentView = 'lifecycle'; // 'lifecycle' | 'transactional' | 'all'
|
|
335
|
+
let activeStage = 'all';
|
|
336
|
+
let activeTags = new Set();
|
|
337
|
+
let activeChannels = new Set();
|
|
338
|
+
let sortColumn = null;
|
|
339
|
+
let sortAsc = true;
|
|
340
|
+
|
|
341
|
+
// Load project data
|
|
342
|
+
let projectData = {};
|
|
343
|
+
try {
|
|
344
|
+
const dataEl = document.getElementById('project-data');
|
|
345
|
+
if (dataEl && dataEl.textContent.trim()) {
|
|
346
|
+
projectData = JSON.parse(dataEl.textContent);
|
|
347
|
+
}
|
|
348
|
+
} catch (e) {
|
|
349
|
+
console.warn('Could not parse project data:', e);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// --- View Toggle ---
|
|
353
|
+
function setView(view, btn) {
|
|
354
|
+
currentView = view;
|
|
355
|
+
document.querySelectorAll('.view-toggle button').forEach(b => b.classList.remove('active'));
|
|
356
|
+
btn.classList.add('active');
|
|
357
|
+
applyFilters();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// --- Stage Filter ---
|
|
361
|
+
function filterStage(stage) {
|
|
362
|
+
activeStage = stage;
|
|
363
|
+
document.querySelectorAll('.stage-filter').forEach(btn => {
|
|
364
|
+
btn.classList.toggle('ring-2', btn.dataset.stage === stage);
|
|
365
|
+
btn.classList.toggle('ring-mango-500', btn.dataset.stage === stage);
|
|
366
|
+
});
|
|
367
|
+
applyFilters();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// --- Tag Filter ---
|
|
371
|
+
function toggleTag(el) {
|
|
372
|
+
const tag = el.dataset.tag;
|
|
373
|
+
if (activeTags.has(tag)) {
|
|
374
|
+
activeTags.delete(tag);
|
|
375
|
+
el.classList.remove('active', 'bg-mango-50');
|
|
376
|
+
} else {
|
|
377
|
+
activeTags.add(tag);
|
|
378
|
+
el.classList.add('active', 'bg-mango-50');
|
|
379
|
+
}
|
|
380
|
+
applyFilters();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// --- Channel Filter ---
|
|
384
|
+
function toggleChannel(el) {
|
|
385
|
+
const channel = el.dataset.channel;
|
|
386
|
+
if (activeChannels.has(channel)) {
|
|
387
|
+
activeChannels.delete(channel);
|
|
388
|
+
el.classList.remove('active', 'bg-mango-50');
|
|
389
|
+
} else {
|
|
390
|
+
activeChannels.add(channel);
|
|
391
|
+
el.classList.add('active', 'bg-mango-50');
|
|
392
|
+
}
|
|
393
|
+
applyFilters();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// --- Clear Filters ---
|
|
397
|
+
function clearFilters() {
|
|
398
|
+
activeTags.clear();
|
|
399
|
+
activeChannels.clear();
|
|
400
|
+
activeStage = 'all';
|
|
401
|
+
currentView = 'lifecycle';
|
|
402
|
+
|
|
403
|
+
document.querySelectorAll('.tag-pill').forEach(el => el.classList.remove('active', 'bg-mango-50'));
|
|
404
|
+
document.querySelectorAll('.stage-filter').forEach(el => {
|
|
405
|
+
el.classList.remove('ring-2', 'ring-mango-500');
|
|
406
|
+
});
|
|
407
|
+
document.querySelectorAll('.view-toggle button').forEach((btn, i) => {
|
|
408
|
+
btn.classList.toggle('active', i === 0);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
applyFilters();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// --- Apply All Filters ---
|
|
415
|
+
function applyFilters() {
|
|
416
|
+
const rows = document.querySelectorAll('#matrix-body .matrix-row');
|
|
417
|
+
let visibleCount = 0;
|
|
418
|
+
|
|
419
|
+
rows.forEach(row => {
|
|
420
|
+
const classification = row.dataset.classification;
|
|
421
|
+
const stage = row.dataset.stage;
|
|
422
|
+
const rowTags = (row.dataset.tags || '').split(',').filter(Boolean);
|
|
423
|
+
const rowChannels = (row.dataset.channels || '').split(',').filter(Boolean);
|
|
424
|
+
const expandRow = row.nextElementSibling;
|
|
425
|
+
|
|
426
|
+
let visible = true;
|
|
427
|
+
|
|
428
|
+
// View filter
|
|
429
|
+
if (currentView === 'lifecycle' && classification === 'transactional') visible = false;
|
|
430
|
+
if (currentView === 'transactional' && classification === 'lifecycle') visible = false;
|
|
431
|
+
|
|
432
|
+
// Stage filter
|
|
433
|
+
if (activeStage !== 'all' && stage !== activeStage) visible = false;
|
|
434
|
+
|
|
435
|
+
// Tag filter (must match ALL active tags)
|
|
436
|
+
if (activeTags.size > 0) {
|
|
437
|
+
for (const tag of activeTags) {
|
|
438
|
+
if (!rowTags.includes(tag)) { visible = false; break; }
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Channel filter (must match ANY active channel)
|
|
443
|
+
if (activeChannels.size > 0) {
|
|
444
|
+
const hasChannel = rowChannels.some(ch => activeChannels.has(ch));
|
|
445
|
+
if (!hasChannel) visible = false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
row.style.display = visible ? '' : 'none';
|
|
449
|
+
if (expandRow && expandRow.classList.contains('expand-row')) {
|
|
450
|
+
expandRow.style.display = visible ? '' : 'none';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (visible) visibleCount++;
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const countEl = document.getElementById('visible-count');
|
|
457
|
+
if (countEl) countEl.textContent = visibleCount;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// --- Expand/Collapse Row ---
|
|
461
|
+
function toggleExpand(row) {
|
|
462
|
+
const expandRow = row.nextElementSibling;
|
|
463
|
+
if (!expandRow || !expandRow.classList.contains('expand-row')) return;
|
|
464
|
+
|
|
465
|
+
const panel = expandRow.querySelector('.expand-panel');
|
|
466
|
+
const isOpen = panel.classList.contains('open');
|
|
467
|
+
|
|
468
|
+
// Close all
|
|
469
|
+
document.querySelectorAll('.expand-panel.open').forEach(p => p.classList.remove('open'));
|
|
470
|
+
document.querySelectorAll('.matrix-row.expanded').forEach(r => r.classList.remove('expanded'));
|
|
471
|
+
|
|
472
|
+
// Toggle this one
|
|
473
|
+
if (!isOpen) {
|
|
474
|
+
panel.classList.add('open');
|
|
475
|
+
row.classList.add('expanded');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// --- Sort Table ---
|
|
480
|
+
function sortTable(column, headerEl) {
|
|
481
|
+
if (sortColumn === column) {
|
|
482
|
+
sortAsc = !sortAsc;
|
|
483
|
+
} else {
|
|
484
|
+
sortColumn = column;
|
|
485
|
+
sortAsc = true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Update indicators
|
|
489
|
+
document.querySelectorAll('.sort-indicator').forEach(el => el.textContent = '');
|
|
490
|
+
const indicator = headerEl.querySelector('.sort-indicator');
|
|
491
|
+
indicator.textContent = sortAsc ? ' \u25B2' : ' \u25BC';
|
|
492
|
+
|
|
493
|
+
// Sort rows
|
|
494
|
+
const tbody = document.getElementById('matrix-body');
|
|
495
|
+
const rowPairs = [];
|
|
496
|
+
const rows = Array.from(tbody.children);
|
|
497
|
+
|
|
498
|
+
for (let i = 0; i < rows.length; i++) {
|
|
499
|
+
if (rows[i].classList.contains('matrix-row')) {
|
|
500
|
+
rowPairs.push({ main: rows[i], expand: rows[i + 1]?.classList.contains('expand-row') ? rows[i + 1] : null });
|
|
501
|
+
if (rows[i + 1]?.classList.contains('expand-row')) i++;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
rowPairs.sort((a, b) => {
|
|
506
|
+
const colMap = { id: 0, stage: 1, name: 2, trigger: 3, wait: 4 };
|
|
507
|
+
const idx = colMap[column] || 0;
|
|
508
|
+
const aVal = a.main.children[idx]?.textContent?.trim() || '';
|
|
509
|
+
const bVal = b.main.children[idx]?.textContent?.trim() || '';
|
|
510
|
+
const cmp = aVal.localeCompare(bVal, undefined, { numeric: true });
|
|
511
|
+
return sortAsc ? cmp : -cmp;
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
rowPairs.forEach(pair => {
|
|
515
|
+
tbody.appendChild(pair.main);
|
|
516
|
+
if (pair.expand) tbody.appendChild(pair.expand);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
</script>
|
|
520
|
+
|
|
521
|
+
</body>
|
|
522
|
+
</html>
|