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/dist/excel.js ADDED
@@ -0,0 +1,342 @@
1
+ // =============================================================================
2
+ // Mango Lollipop — Excel Generation (xlsx-js-style)
3
+ // =============================================================================
4
+ import XLSX from "xlsx-js-style";
5
+ import { writeFileSync } from "fs";
6
+ // -----------------------------------------------------------------------------
7
+ // Stage display names and colors
8
+ // -----------------------------------------------------------------------------
9
+ const STAGE_LABELS = {
10
+ TX: "Transactional",
11
+ AQ: "Acquisition",
12
+ AC: "Activation",
13
+ RV: "Revenue",
14
+ RT: "Retention",
15
+ RF: "Referral",
16
+ };
17
+ const STAGE_COLORS = {
18
+ TX: { fill: "F0F0F0" },
19
+ AQ: { fill: "D4EDDA" },
20
+ AC: { fill: "CCE5FF" },
21
+ RV: { fill: "FFF3CD" },
22
+ RT: { fill: "FFE5CC" },
23
+ RF: { fill: "E8D5F5" },
24
+ };
25
+ const HEADER_STYLE = {
26
+ fill: { fgColor: { rgb: "374151" } },
27
+ font: { bold: true, color: { rgb: "FFFFFF" }, sz: 11 },
28
+ alignment: { vertical: "center" },
29
+ };
30
+ // -----------------------------------------------------------------------------
31
+ // Helpers
32
+ // -----------------------------------------------------------------------------
33
+ function guardsToString(msg) {
34
+ if (!msg.guards.length)
35
+ return "\u2014";
36
+ return msg.guards.map((g) => g.condition).join("; ");
37
+ }
38
+ function suppressionsToString(msg) {
39
+ if (!msg.suppressions.length)
40
+ return "\u2014";
41
+ return msg.suppressions.map((s) => s.condition).join("; ");
42
+ }
43
+ function channelToString(msg) {
44
+ if (msg.channel)
45
+ return msg.channel;
46
+ if (msg.channels)
47
+ return msg.channels.join(", ");
48
+ return "\u2014";
49
+ }
50
+ function tagsToString(tags) {
51
+ return tags.join(", ");
52
+ }
53
+ // Apply header style to first row of a worksheet
54
+ function applyHeaderStyle(ws) {
55
+ const range = XLSX.utils.decode_range(ws["!ref"] ?? "A1");
56
+ for (let c = range.s.c; c <= range.e.c; c++) {
57
+ const addr = XLSX.utils.encode_cell({ r: 0, c });
58
+ if (ws[addr]) {
59
+ ws[addr].s = HEADER_STYLE;
60
+ }
61
+ }
62
+ }
63
+ // Apply stage-based row fills to data rows
64
+ function applyStageColors(ws, stageColIndex) {
65
+ const range = XLSX.utils.decode_range(ws["!ref"] ?? "A1");
66
+ for (let r = 1; r <= range.e.r; r++) {
67
+ const stageCell = ws[XLSX.utils.encode_cell({ r, c: stageColIndex })];
68
+ if (!stageCell)
69
+ continue;
70
+ // Resolve stage code from label or raw value
71
+ const val = String(stageCell.v ?? "");
72
+ let stageCode = val;
73
+ for (const [code, label] of Object.entries(STAGE_LABELS)) {
74
+ if (label === val) {
75
+ stageCode = code;
76
+ break;
77
+ }
78
+ }
79
+ const color = STAGE_COLORS[stageCode];
80
+ if (!color)
81
+ continue;
82
+ const fillStyle = {
83
+ fill: { fgColor: { rgb: color.fill } },
84
+ };
85
+ for (let c = range.s.c; c <= range.e.c; c++) {
86
+ const addr = XLSX.utils.encode_cell({ r, c });
87
+ if (ws[addr]) {
88
+ ws[addr].s = fillStyle;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ // -----------------------------------------------------------------------------
94
+ // Sheet builders
95
+ // -----------------------------------------------------------------------------
96
+ function buildWelcomeSheet(analysis, messageCount) {
97
+ const pathLabel = analysis.path === "fresh" ? "Fresh" : "Improving Existing";
98
+ const channels = analysis.channels.join(", ");
99
+ const generated = new Date().toLocaleDateString("en-US", {
100
+ year: "numeric",
101
+ month: "long",
102
+ day: "numeric",
103
+ });
104
+ const data = [
105
+ ["Mango Lollipop \u2014 Lifecycle Messaging Matrix"],
106
+ [],
107
+ ["Company", analysis.company.name],
108
+ ["Product", analysis.company.product_type],
109
+ ["Channels", channels],
110
+ ["Generated", generated],
111
+ ["Total Messages", messageCount],
112
+ ["Path", pathLabel],
113
+ [],
114
+ ["How to Use This Spreadsheet"],
115
+ [
116
+ "Transactional Messages",
117
+ "Mandatory system messages (email verification, password reset, receipts)",
118
+ ],
119
+ [
120
+ "Lifecycle Matrix",
121
+ "Your AARRR messaging strategy with triggers, guards, and suppressions",
122
+ ],
123
+ [
124
+ "Event Taxonomy",
125
+ "All product events and which messages they trigger",
126
+ ],
127
+ ["Tags", "Tag inventory with message counts"],
128
+ [
129
+ "Channel Strategy",
130
+ "Message distribution by channel and stage",
131
+ ],
132
+ [],
133
+ [],
134
+ [
135
+ "Generated by Mango Lollipop \u2014 https://github.com/sr-kai/mango-lollipop",
136
+ ],
137
+ ["Made by Sasha Kai with probably too much coffee."],
138
+ ];
139
+ const ws = XLSX.utils.aoa_to_sheet(data);
140
+ // Style the title row
141
+ if (ws["A1"]) {
142
+ ws["A1"].s = {
143
+ font: { bold: true, sz: 16, color: { rgb: "1F2937" } },
144
+ };
145
+ }
146
+ // Style "How to Use" header
147
+ if (ws["A10"]) {
148
+ ws["A10"].s = {
149
+ font: { bold: true, sz: 13, color: { rgb: "1F2937" } },
150
+ };
151
+ }
152
+ // Style info labels (rows 3-8, column A)
153
+ for (let r = 2; r <= 7; r++) {
154
+ const addr = XLSX.utils.encode_cell({ r, c: 0 });
155
+ if (ws[addr]) {
156
+ ws[addr].s = { font: { bold: true, color: { rgb: "6B7280" } } };
157
+ }
158
+ }
159
+ // Style sheet tab names (rows 11-15, column A)
160
+ for (let r = 10; r <= 14; r++) {
161
+ const addr = XLSX.utils.encode_cell({ r, c: 0 });
162
+ if (ws[addr]) {
163
+ ws[addr].s = { font: { bold: true, color: { rgb: "374151" } } };
164
+ }
165
+ }
166
+ // Style footer
167
+ if (ws["A18"]) {
168
+ ws["A18"].s = { font: { color: { rgb: "9CA3AF" }, sz: 10 } };
169
+ }
170
+ if (ws["A19"]) {
171
+ ws["A19"].s = { font: { color: { rgb: "9CA3AF" }, sz: 10 } };
172
+ }
173
+ // Column widths
174
+ ws["!cols"] = [{ wch: 30 }, { wch: 70 }];
175
+ return ws;
176
+ }
177
+ function buildTransactionalSheet(messages) {
178
+ const tx = messages.filter((m) => m.classification === "transactional");
179
+ const rows = tx.map((m) => ({
180
+ ID: m.id,
181
+ Name: m.name,
182
+ "Trigger Event": m.trigger.event,
183
+ "Trigger Type": m.trigger.type,
184
+ Wait: m.wait,
185
+ Channel: channelToString(m),
186
+ CTA: m.cta.text,
187
+ From: m.from,
188
+ Tags: tagsToString(m.tags),
189
+ }));
190
+ const ws = XLSX.utils.json_to_sheet(rows);
191
+ autoFitColumns(ws, rows);
192
+ applyHeaderStyle(ws);
193
+ // TX rows all get the TX color
194
+ applyStageColors(ws, -1); // special handling below
195
+ // For TX sheet, all rows are TX — apply fill to all data rows
196
+ const range = XLSX.utils.decode_range(ws["!ref"] ?? "A1");
197
+ const txFill = {
198
+ fill: { fgColor: { rgb: STAGE_COLORS.TX.fill } },
199
+ };
200
+ for (let r = 1; r <= range.e.r; r++) {
201
+ for (let c = range.s.c; c <= range.e.c; c++) {
202
+ const addr = XLSX.utils.encode_cell({ r, c });
203
+ if (ws[addr])
204
+ ws[addr].s = txFill;
205
+ }
206
+ }
207
+ return ws;
208
+ }
209
+ function buildLifecycleSheet(messages) {
210
+ const lc = messages.filter((m) => m.classification === "lifecycle");
211
+ const rows = lc.map((m) => ({
212
+ ID: m.id,
213
+ Stage: STAGE_LABELS[m.stage] ?? m.stage,
214
+ Name: m.name,
215
+ "Trigger Event": m.trigger.event,
216
+ "Trigger Type": m.trigger.type,
217
+ Wait: m.wait,
218
+ Guards: guardsToString(m),
219
+ Suppressions: suppressionsToString(m),
220
+ Channel: channelToString(m),
221
+ CTA: m.cta.text,
222
+ Goal: m.goal,
223
+ Segment: m.segment,
224
+ Tags: tagsToString(m.tags),
225
+ Format: m.format,
226
+ From: m.from,
227
+ }));
228
+ const ws = XLSX.utils.json_to_sheet(rows);
229
+ autoFitColumns(ws, rows);
230
+ applyHeaderStyle(ws);
231
+ applyStageColors(ws, 1); // Stage is column B (index 1)
232
+ return ws;
233
+ }
234
+ function buildEventTaxonomySheet(events, messages) {
235
+ const rows = [];
236
+ const categories = [
237
+ "identity",
238
+ "activation",
239
+ "engagement",
240
+ "conversion",
241
+ "retention",
242
+ ];
243
+ for (const category of categories) {
244
+ const eventList = events[category] ?? [];
245
+ for (const event of eventList) {
246
+ const usedBy = messages
247
+ .filter((m) => m.trigger.event === event)
248
+ .map((m) => m.id)
249
+ .join(", ");
250
+ rows.push({
251
+ Category: category.charAt(0).toUpperCase() + category.slice(1),
252
+ Event: event,
253
+ "Used By": usedBy || "\u2014",
254
+ });
255
+ }
256
+ }
257
+ const ws = XLSX.utils.json_to_sheet(rows);
258
+ autoFitColumns(ws, rows);
259
+ applyHeaderStyle(ws);
260
+ return ws;
261
+ }
262
+ function buildTagsSheet(messages, tagDefinitions) {
263
+ const allTags = new Set([
264
+ ...tagDefinitions,
265
+ ...messages.flatMap((m) => m.tags),
266
+ ]);
267
+ const rows = Array.from(allTags)
268
+ .sort()
269
+ .map((tag) => {
270
+ const msgIds = messages
271
+ .filter((m) => m.tags.includes(tag))
272
+ .map((m) => m.id);
273
+ return {
274
+ Tag: tag,
275
+ "Message Count": msgIds.length,
276
+ "Used By": msgIds.join(", ") || "\u2014",
277
+ };
278
+ });
279
+ const ws = XLSX.utils.json_to_sheet(rows);
280
+ autoFitColumns(ws, rows);
281
+ applyHeaderStyle(ws);
282
+ return ws;
283
+ }
284
+ function buildChannelStrategySheet(messages) {
285
+ const channels = ["email", "sms", "in-app", "push"];
286
+ const stages = ["TX", "AQ", "AC", "RV", "RT", "RF"];
287
+ const rows = [];
288
+ for (const ch of channels) {
289
+ const msgsWithChannel = messages.filter((m) => m.channel === ch ||
290
+ (m.channels && m.channels.includes(ch)));
291
+ if (msgsWithChannel.length === 0)
292
+ continue;
293
+ const row = {
294
+ Channel: ch,
295
+ "Total Messages": msgsWithChannel.length,
296
+ };
297
+ for (const stage of stages) {
298
+ const count = msgsWithChannel.filter((m) => m.stage === stage).length;
299
+ row[STAGE_LABELS[stage]] = count;
300
+ }
301
+ rows.push(row);
302
+ }
303
+ const ws = XLSX.utils.json_to_sheet(rows);
304
+ autoFitColumns(ws, rows);
305
+ applyHeaderStyle(ws);
306
+ return ws;
307
+ }
308
+ // Auto-fit column widths based on content
309
+ function autoFitColumns(ws, rows) {
310
+ if (!rows.length)
311
+ return;
312
+ const headers = Object.keys(rows[0]);
313
+ ws["!cols"] = headers.map((h) => {
314
+ let maxWidth = h.length;
315
+ for (const row of rows) {
316
+ const val = String(row[h] ?? "");
317
+ if (val.length > maxWidth)
318
+ maxWidth = val.length;
319
+ }
320
+ return { wch: Math.min(maxWidth + 2, 60) };
321
+ });
322
+ }
323
+ // -----------------------------------------------------------------------------
324
+ // Public API
325
+ // -----------------------------------------------------------------------------
326
+ export function generateMatrixWorkbook(messages, events, tags, analysis) {
327
+ const wb = XLSX.utils.book_new();
328
+ // Welcome sheet first (if analysis data available)
329
+ if (analysis) {
330
+ XLSX.utils.book_append_sheet(wb, buildWelcomeSheet(analysis, messages.length), "Welcome");
331
+ }
332
+ XLSX.utils.book_append_sheet(wb, buildTransactionalSheet(messages), "Transactional Messages");
333
+ XLSX.utils.book_append_sheet(wb, buildLifecycleSheet(messages), "Lifecycle Matrix");
334
+ XLSX.utils.book_append_sheet(wb, buildEventTaxonomySheet(events, messages), "Event Taxonomy");
335
+ XLSX.utils.book_append_sheet(wb, buildTagsSheet(messages, tags), "Tags");
336
+ XLSX.utils.book_append_sheet(wb, buildChannelStrategySheet(messages), "Channel Strategy");
337
+ return wb;
338
+ }
339
+ export function writeWorkbook(workbook, filePath) {
340
+ const buf = XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
341
+ writeFileSync(filePath, buf);
342
+ }
package/dist/html.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { Message, Analysis } from "./schema.js";
2
+ export declare function generateDashboard(messages: Message[], analysis: Analysis): string;
3
+ export declare function generateOverview(messages: Message[], analysis: Analysis): string;
4
+ export declare function generateMessageViewer(messages: Message[], analysis: Analysis, messageContent: Record<string, string>): string;