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.
@@ -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> &mdash; AI-powered lifecycle messaging for SaaS<br>
320
+ Made by Sasha Kai with probably too much coffee. &middot; {{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>