featuredrop 2.7.2 → 3.0.1

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.
Files changed (52) hide show
  1. package/README.md +32 -1
  2. package/dist/astro.cjs +333 -0
  3. package/dist/astro.cjs.map +1 -0
  4. package/dist/astro.d.cts +242 -0
  5. package/dist/astro.d.ts +242 -0
  6. package/dist/astro.js +329 -0
  7. package/dist/astro.js.map +1 -0
  8. package/dist/engine.cjs +552 -0
  9. package/dist/engine.cjs.map +1 -0
  10. package/dist/engine.d.cts +422 -0
  11. package/dist/engine.d.ts +422 -0
  12. package/dist/engine.js +545 -0
  13. package/dist/engine.js.map +1 -0
  14. package/dist/featuredrop.cjs +208 -1
  15. package/dist/featuredrop.cjs.map +1 -1
  16. package/dist/next.cjs +336 -0
  17. package/dist/next.cjs.map +1 -0
  18. package/dist/next.d.cts +243 -0
  19. package/dist/next.d.ts +243 -0
  20. package/dist/next.js +332 -0
  21. package/dist/next.js.map +1 -0
  22. package/dist/nuxt.cjs +352 -0
  23. package/dist/nuxt.cjs.map +1 -0
  24. package/dist/nuxt.d.cts +282 -0
  25. package/dist/nuxt.d.ts +282 -0
  26. package/dist/nuxt.js +347 -0
  27. package/dist/nuxt.js.map +1 -0
  28. package/dist/preact.cjs +354 -0
  29. package/dist/preact.cjs.map +1 -1
  30. package/dist/preact.d.cts +170 -1
  31. package/dist/preact.d.ts +170 -1
  32. package/dist/preact.js +350 -1
  33. package/dist/preact.js.map +1 -1
  34. package/dist/react-hooks.cjs +82 -0
  35. package/dist/react-hooks.cjs.map +1 -1
  36. package/dist/react-hooks.d.cts +117 -1
  37. package/dist/react-hooks.d.ts +117 -1
  38. package/dist/react-hooks.js +80 -1
  39. package/dist/react-hooks.js.map +1 -1
  40. package/dist/react.cjs +354 -0
  41. package/dist/react.cjs.map +1 -1
  42. package/dist/react.d.cts +170 -1
  43. package/dist/react.d.ts +170 -1
  44. package/dist/react.js +350 -1
  45. package/dist/react.js.map +1 -1
  46. package/dist/remix.cjs +331 -0
  47. package/dist/remix.cjs.map +1 -0
  48. package/dist/remix.d.cts +305 -0
  49. package/dist/remix.d.ts +305 -0
  50. package/dist/remix.js +327 -0
  51. package/dist/remix.js.map +1 -0
  52. package/package.json +70 -2
@@ -0,0 +1,552 @@
1
+ 'use strict';
2
+
3
+ // src/engine/behavior-tracker.ts
4
+ var STORAGE_KEY = "fd_behavior";
5
+ var MAX_RECENT_DISMISSALS = 20;
6
+ function createEmptyProfile() {
7
+ return {
8
+ version: 1,
9
+ firstSeen: (/* @__PURE__ */ new Date()).toISOString(),
10
+ sessionCount: 0,
11
+ totalInteractions: 0,
12
+ dismissCount: 0,
13
+ clickCount: 0,
14
+ completionCount: 0,
15
+ formatPrefs: { badge: 0, toast: 0, modal: 0, inline: 0, banner: 0, spotlight: 0 },
16
+ hourlyActivity: new Array(24).fill(0),
17
+ recentDismissals: [],
18
+ featureInteractions: {}
19
+ };
20
+ }
21
+ var BehaviorTracker = class {
22
+ profile;
23
+ sessionStartTime;
24
+ constructor() {
25
+ this.profile = this.load();
26
+ this.sessionStartTime = Date.now();
27
+ this.profile.sessionCount++;
28
+ this.save();
29
+ }
30
+ /** Track a feature interaction */
31
+ trackInteraction(featureId, type) {
32
+ const hour = (/* @__PURE__ */ new Date()).getHours();
33
+ this.profile.hourlyActivity[hour]++;
34
+ this.profile.totalInteractions++;
35
+ if (!this.profile.featureInteractions[featureId]) {
36
+ this.profile.featureInteractions[featureId] = {
37
+ seen: 0,
38
+ clicked: 0,
39
+ dismissed: 0,
40
+ completed: 0,
41
+ lastInteraction: (/* @__PURE__ */ new Date()).toISOString()
42
+ };
43
+ }
44
+ const fi = this.profile.featureInteractions[featureId];
45
+ fi.lastInteraction = (/* @__PURE__ */ new Date()).toISOString();
46
+ switch (type) {
47
+ case "seen":
48
+ case "hovered":
49
+ case "expanded":
50
+ fi.seen++;
51
+ break;
52
+ case "clicked":
53
+ fi.clicked++;
54
+ this.profile.clickCount++;
55
+ break;
56
+ case "dismissed":
57
+ case "snoozed":
58
+ fi.dismissed++;
59
+ this.profile.dismissCount++;
60
+ this.profile.recentDismissals.push(featureId);
61
+ if (this.profile.recentDismissals.length > MAX_RECENT_DISMISSALS) {
62
+ this.profile.recentDismissals.shift();
63
+ }
64
+ break;
65
+ case "completed":
66
+ fi.completed++;
67
+ this.profile.completionCount++;
68
+ break;
69
+ }
70
+ this.save();
71
+ }
72
+ /** Record a format engagement (user interacted via this format) */
73
+ trackFormatEngagement(format) {
74
+ if (format in this.profile.formatPrefs) {
75
+ this.profile.formatPrefs[format]++;
76
+ this.save();
77
+ }
78
+ }
79
+ /** Get current profile */
80
+ getProfile() {
81
+ return this.profile;
82
+ }
83
+ /** Session count */
84
+ getSessionCount() {
85
+ return this.profile.sessionCount;
86
+ }
87
+ /** Average session duration (rough estimate based on current session) */
88
+ getSessionAge() {
89
+ return (Date.now() - this.sessionStartTime) / 1e3;
90
+ }
91
+ /** Dismiss rate (0-1) */
92
+ getDismissRate() {
93
+ if (this.profile.totalInteractions === 0) return 0;
94
+ return this.profile.dismissCount / this.profile.totalInteractions;
95
+ }
96
+ /** Engagement rate (clicked or completed / total interactions) */
97
+ getEngagementRate() {
98
+ if (this.profile.totalInteractions === 0) return 0;
99
+ return (this.profile.clickCount + this.profile.completionCount) / this.profile.totalInteractions;
100
+ }
101
+ /** Preferred format based on highest engagement count */
102
+ getPreferredFormat() {
103
+ const prefs = this.profile.formatPrefs;
104
+ let best = "badge";
105
+ let bestCount = 0;
106
+ for (const [format, count] of Object.entries(prefs)) {
107
+ if (count > bestCount) {
108
+ bestCount = count;
109
+ best = format;
110
+ }
111
+ }
112
+ return best;
113
+ }
114
+ /** Top 3 active hours (0-23) */
115
+ getActiveHours() {
116
+ return this.profile.hourlyActivity.map((count, hour) => ({ hour, count })).sort((a, b) => b.count - a.count).slice(0, 3).map((h) => h.hour);
117
+ }
118
+ /** Recent dismissals (last N feature IDs) */
119
+ getRecentDismissals() {
120
+ return this.profile.recentDismissals;
121
+ }
122
+ /** Count of dismissals in last N milliseconds */
123
+ getRecentDismissalCount(windowMs) {
124
+ const now = /* @__PURE__ */ new Date();
125
+ const cutoff = new Date(now.getTime() - windowMs);
126
+ let count = 0;
127
+ for (const featureId of this.profile.recentDismissals) {
128
+ const fi = this.profile.featureInteractions[featureId];
129
+ if (fi && new Date(fi.lastInteraction) >= cutoff) {
130
+ count++;
131
+ }
132
+ }
133
+ return count;
134
+ }
135
+ /** Get interaction data for a specific feature */
136
+ getFeatureInteractions(featureId) {
137
+ return this.profile.featureInteractions[featureId] ?? null;
138
+ }
139
+ /** Clear all behavior data */
140
+ clearProfile() {
141
+ this.profile = createEmptyProfile();
142
+ this.save();
143
+ }
144
+ load() {
145
+ if (typeof localStorage === "undefined") return createEmptyProfile();
146
+ try {
147
+ const raw = localStorage.getItem(STORAGE_KEY);
148
+ if (!raw) return createEmptyProfile();
149
+ const parsed = JSON.parse(raw);
150
+ if (parsed.version !== 1) return createEmptyProfile();
151
+ return parsed;
152
+ } catch {
153
+ return createEmptyProfile();
154
+ }
155
+ }
156
+ save() {
157
+ if (typeof localStorage === "undefined") return;
158
+ try {
159
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.profile));
160
+ } catch {
161
+ }
162
+ }
163
+ };
164
+
165
+ // src/engine/timing-optimizer.ts
166
+ var DEFAULTS = {
167
+ sessionGateMs: 1e4,
168
+ maxDismissalsBeforeBackoff: 2,
169
+ dismissBackoffWindowMs: 18e4,
170
+ cooldownMs: 6e4,
171
+ excludePaths: ["/checkout", "/auth/*", "/error", "/login", "/signup"]
172
+ };
173
+ var TimingOptimizer = class {
174
+ config;
175
+ tracker;
176
+ lastAnnouncementTime = 0;
177
+ constructor(tracker, config) {
178
+ this.tracker = tracker;
179
+ this.config = { ...DEFAULTS, ...config };
180
+ }
181
+ /** Decide whether to show a feature announcement now */
182
+ shouldShowNow(_featureId, context) {
183
+ if (context.sessionAge < this.config.sessionGateMs / 1e3) {
184
+ return {
185
+ show: false,
186
+ reason: "session_too_young",
187
+ delayMs: this.config.sessionGateMs - context.sessionAge * 1e3,
188
+ confidence: 0.9
189
+ };
190
+ }
191
+ const recentDismissals = this.tracker.getRecentDismissalCount(
192
+ this.config.dismissBackoffWindowMs
193
+ );
194
+ if (recentDismissals >= this.config.maxDismissalsBeforeBackoff) {
195
+ return {
196
+ show: false,
197
+ reason: "high_dismiss_rate",
198
+ delayMs: this.config.dismissBackoffWindowMs,
199
+ confidence: 0.85
200
+ };
201
+ }
202
+ if (this.isExcludedPath(context.currentPath)) {
203
+ return {
204
+ show: false,
205
+ reason: "excluded_page",
206
+ confidence: 1
207
+ };
208
+ }
209
+ const now = Date.now();
210
+ if (this.lastAnnouncementTime > 0 && now - this.lastAnnouncementTime < this.config.cooldownMs) {
211
+ return {
212
+ show: false,
213
+ reason: "cooldown",
214
+ delayMs: this.config.cooldownMs - (now - this.lastAnnouncementTime),
215
+ confidence: 0.9
216
+ };
217
+ }
218
+ if (context.featurePriority === "critical") {
219
+ this.lastAnnouncementTime = now;
220
+ return {
221
+ show: true,
222
+ reason: "priority_override",
223
+ confidence: 1
224
+ };
225
+ }
226
+ let confidence = 0.6;
227
+ const currentHour = (/* @__PURE__ */ new Date()).getHours();
228
+ const activeHours = this.tracker.getActiveHours();
229
+ if (activeHours.includes(currentHour)) {
230
+ confidence += 0.2;
231
+ }
232
+ const engagementRate = this.tracker.getEngagementRate();
233
+ if (engagementRate > 0.5) {
234
+ confidence += 0.1;
235
+ }
236
+ if (context.featurePriority === "low" && confidence < 0.7) {
237
+ return {
238
+ show: false,
239
+ reason: "low_priority_insufficient_confidence",
240
+ delayMs: this.config.cooldownMs,
241
+ confidence
242
+ };
243
+ }
244
+ this.lastAnnouncementTime = now;
245
+ return {
246
+ show: true,
247
+ reason: "optimal_window",
248
+ confidence: Math.min(confidence, 1)
249
+ };
250
+ }
251
+ /** Mark that an announcement was shown (for cooldown tracking) */
252
+ recordAnnouncementShown() {
253
+ this.lastAnnouncementTime = Date.now();
254
+ }
255
+ isExcludedPath(path) {
256
+ return this.config.excludePaths.some((pattern) => {
257
+ if (pattern.endsWith("/*")) {
258
+ const prefix = pattern.slice(0, -2);
259
+ return path === prefix || path.startsWith(prefix + "/");
260
+ }
261
+ return path === pattern;
262
+ });
263
+ }
264
+ };
265
+
266
+ // src/engine/format-selector.ts
267
+ var FormatSelector = class {
268
+ tracker;
269
+ constructor(tracker) {
270
+ this.tracker = tracker;
271
+ }
272
+ /** Recommend the best display format for a feature */
273
+ recommendFormat(featureId, priority) {
274
+ const profile = this.tracker.getProfile();
275
+ const sessionCount = this.tracker.getSessionCount();
276
+ const fi = this.tracker.getFeatureInteractions(featureId);
277
+ if (priority === "critical") {
278
+ return {
279
+ primary: "modal",
280
+ fallback: "banner",
281
+ reason: "critical_priority"
282
+ };
283
+ }
284
+ if (priority === "low") {
285
+ return {
286
+ primary: "badge",
287
+ fallback: "inline",
288
+ reason: "low_priority"
289
+ };
290
+ }
291
+ if (sessionCount < 3) {
292
+ return {
293
+ primary: "badge",
294
+ fallback: "toast",
295
+ reason: "new_user_gentle"
296
+ };
297
+ }
298
+ if (sessionCount > 50) {
299
+ return {
300
+ primary: "badge",
301
+ fallback: "inline",
302
+ reason: "power_user_concise"
303
+ };
304
+ }
305
+ const prefs = profile.formatPrefs;
306
+ const totalFormatInteractions = prefs.badge + prefs.toast + prefs.modal + prefs.inline + prefs.banner + prefs.spotlight;
307
+ if (totalFormatInteractions >= 5) {
308
+ const best = this.getBestFormat(prefs);
309
+ const fallback = this.getFallbackFormat(best);
310
+ return {
311
+ primary: best,
312
+ fallback,
313
+ reason: "user_preference"
314
+ };
315
+ }
316
+ if (fi) {
317
+ const seenCount = fi.seen;
318
+ const clicked = fi.clicked;
319
+ if (seenCount >= 3 && clicked === 0) {
320
+ return {
321
+ primary: "toast",
322
+ fallback: "badge",
323
+ reason: "escalation_from_badge"
324
+ };
325
+ }
326
+ if (seenCount >= 6 && clicked === 0) {
327
+ return {
328
+ primary: "modal",
329
+ fallback: "toast",
330
+ reason: "escalation_from_toast"
331
+ };
332
+ }
333
+ }
334
+ const dismissRate = this.tracker.getDismissRate();
335
+ if (dismissRate > 0.7) {
336
+ return {
337
+ primary: "badge",
338
+ fallback: "inline",
339
+ reason: "high_dismiss_rate_gentle"
340
+ };
341
+ }
342
+ return {
343
+ primary: "toast",
344
+ fallback: "badge",
345
+ reason: "default_normal"
346
+ };
347
+ }
348
+ getBestFormat(prefs) {
349
+ let best = "badge";
350
+ let bestCount = 0;
351
+ for (const [format, count] of Object.entries(prefs)) {
352
+ if (count > bestCount) {
353
+ bestCount = count;
354
+ best = format;
355
+ }
356
+ }
357
+ return best;
358
+ }
359
+ getFallbackFormat(primary) {
360
+ const fallbacks = {
361
+ badge: "inline",
362
+ toast: "badge",
363
+ modal: "toast",
364
+ banner: "toast",
365
+ inline: "badge",
366
+ spotlight: "toast"
367
+ };
368
+ return fallbacks[primary];
369
+ }
370
+ };
371
+
372
+ // src/engine/adoption-scorer.ts
373
+ var AdoptionScorer = class {
374
+ tracker;
375
+ constructor(tracker) {
376
+ this.tracker = tracker;
377
+ }
378
+ /** Calculate overall adoption score (0-100) */
379
+ getAdoptionScore(manifest) {
380
+ if (manifest.length === 0) {
381
+ return {
382
+ score: 100,
383
+ grade: "A",
384
+ breakdown: {
385
+ featuresExplored: 1,
386
+ dismissRate: 0,
387
+ completionRate: 1,
388
+ engagementTrend: "stable"
389
+ },
390
+ recommendations: []
391
+ };
392
+ }
393
+ const statuses = manifest.map((f) => this.getFeatureAdoption(f.id));
394
+ const explored = statuses.filter(
395
+ (s) => s.status === "explored" || s.status === "adopted"
396
+ ).length;
397
+ const adopted = statuses.filter((s) => s.status === "adopted").length;
398
+ const dismissed = statuses.filter((s) => s.status === "dismissed").length;
399
+ const explorationRate = explored / manifest.length;
400
+ const adoptionRate = adopted / manifest.length;
401
+ const dismissRate = explored + dismissed > 0 ? dismissed / (explored + dismissed) : 0;
402
+ const rawScore = explorationRate * 30 + adoptionRate * 50 + (1 - dismissRate) * 20;
403
+ const score = Math.round(Math.min(100, Math.max(0, rawScore)));
404
+ const grade = this.toGrade(score);
405
+ const engagementTrend = this.detectTrend();
406
+ const recommendations = [];
407
+ const unseen = statuses.filter((s) => s.status === "unseen");
408
+ if (unseen.length > 0) {
409
+ const first = unseen[0];
410
+ recommendations.push(
411
+ `User hasn't seen "${first.featureId}" \u2014 consider showing as badge or toast.`
412
+ );
413
+ }
414
+ if (dismissRate > 0.5) {
415
+ recommendations.push(
416
+ "High dismiss rate \u2014 try less intrusive formats (badge instead of modal)."
417
+ );
418
+ }
419
+ if (explorationRate < 0.3) {
420
+ recommendations.push(
421
+ "Low feature exploration \u2014 consider a guided tour to highlight key features."
422
+ );
423
+ }
424
+ return {
425
+ score,
426
+ grade,
427
+ breakdown: {
428
+ featuresExplored: explorationRate,
429
+ dismissRate,
430
+ completionRate: adoptionRate,
431
+ engagementTrend
432
+ },
433
+ recommendations
434
+ };
435
+ }
436
+ /** Get adoption status for a specific feature */
437
+ getFeatureAdoption(featureId) {
438
+ const fi = this.tracker.getFeatureInteractions(featureId);
439
+ if (!fi) {
440
+ return {
441
+ featureId,
442
+ status: "unseen",
443
+ interactionCount: 0
444
+ };
445
+ }
446
+ const interactionCount = fi.seen + fi.clicked + fi.dismissed + fi.completed;
447
+ let status;
448
+ if (fi.completed > 0) {
449
+ status = "adopted";
450
+ } else if (fi.dismissed > 0 && fi.clicked === 0) {
451
+ status = "dismissed";
452
+ } else if (fi.clicked > 0) {
453
+ status = "explored";
454
+ } else if (fi.seen > 0) {
455
+ status = "seen";
456
+ } else {
457
+ status = "unseen";
458
+ }
459
+ return {
460
+ featureId,
461
+ status,
462
+ firstSeen: fi.lastInteraction,
463
+ // approximate — we don't store firstSeen separately
464
+ lastInteraction: fi.lastInteraction,
465
+ interactionCount
466
+ };
467
+ }
468
+ toGrade(score) {
469
+ if (score >= 90) return "A";
470
+ if (score >= 75) return "B";
471
+ if (score >= 60) return "C";
472
+ if (score >= 40) return "D";
473
+ return "F";
474
+ }
475
+ detectTrend() {
476
+ const profile = this.tracker.getProfile();
477
+ const hourly = profile.hourlyActivity;
478
+ const recentHalf = hourly.slice(12).reduce((a, b) => a + b, 0);
479
+ const olderHalf = hourly.slice(0, 12).reduce((a, b) => a + b, 0);
480
+ if (recentHalf > olderHalf * 1.2) return "rising";
481
+ if (recentHalf < olderHalf * 0.8) return "declining";
482
+ return "stable";
483
+ }
484
+ };
485
+
486
+ // src/engine/index.ts
487
+ var AdoptionEngine = class {
488
+ tracker;
489
+ timing;
490
+ format;
491
+ scorer;
492
+ manifest;
493
+ constructor(config) {
494
+ this.manifest = config.manifest;
495
+ this.tracker = new BehaviorTracker();
496
+ this.timing = new TimingOptimizer(this.tracker, config.timing);
497
+ this.format = new FormatSelector(this.tracker);
498
+ this.scorer = new AdoptionScorer(this.tracker);
499
+ }
500
+ /** Initialize the engine (called by FeatureDropProvider on mount) */
501
+ initialize() {
502
+ }
503
+ /** Cleanup resources (called by FeatureDropProvider on unmount) */
504
+ destroy() {
505
+ }
506
+ /** Decide whether to show a feature announcement now */
507
+ shouldShow(featureId, context) {
508
+ return this.timing.shouldShowNow(featureId, context);
509
+ }
510
+ /** Recommend the best display format for a feature */
511
+ recommendFormat(featureId) {
512
+ const feature = this.manifest.find((f) => f.id === featureId);
513
+ const priority = feature?.priority ?? "normal";
514
+ return this.format.recommendFormat(featureId, priority);
515
+ }
516
+ /** Get the user's overall adoption score */
517
+ getAdoptionScore() {
518
+ return this.scorer.getAdoptionScore(this.manifest);
519
+ }
520
+ /** Track a user interaction with a feature */
521
+ trackInteraction(featureId, type) {
522
+ this.tracker.trackInteraction(featureId, type);
523
+ }
524
+ /** Get adoption status for a specific feature */
525
+ getFeatureAdoption(featureId) {
526
+ return this.scorer.getFeatureAdoption(featureId);
527
+ }
528
+ /** Access the behavior tracker for advanced usage */
529
+ getBehaviorTracker() {
530
+ return this.tracker;
531
+ }
532
+ /** Clear all behavior data */
533
+ clearProfile() {
534
+ this.tracker.clearProfile();
535
+ }
536
+ /** Update the manifest (e.g., if features change at runtime) */
537
+ updateManifest(manifest) {
538
+ this.manifest = manifest;
539
+ }
540
+ };
541
+ function createAdoptionEngine(config) {
542
+ return new AdoptionEngine(config);
543
+ }
544
+
545
+ exports.AdoptionEngine = AdoptionEngine;
546
+ exports.AdoptionScorer = AdoptionScorer;
547
+ exports.BehaviorTracker = BehaviorTracker;
548
+ exports.FormatSelector = FormatSelector;
549
+ exports.TimingOptimizer = TimingOptimizer;
550
+ exports.createAdoptionEngine = createAdoptionEngine;
551
+ //# sourceMappingURL=engine.cjs.map
552
+ //# sourceMappingURL=engine.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/engine/behavior-tracker.ts","../src/engine/timing-optimizer.ts","../src/engine/format-selector.ts","../src/engine/adoption-scorer.ts","../src/engine/index.ts"],"names":[],"mappings":"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","file":"engine.cjs","sourcesContent":["import type { InteractionType } from \"../types\";\n\n/** Compact behavior profile stored in localStorage */\nexport interface BehaviorProfile {\n version: 1;\n firstSeen: string;\n sessionCount: number;\n totalInteractions: number;\n dismissCount: number;\n clickCount: number;\n completionCount: number;\n formatPrefs: {\n badge: number;\n toast: number;\n modal: number;\n inline: number;\n banner: number;\n spotlight: number;\n };\n /** 24-element array, interaction count per hour */\n hourlyActivity: number[];\n /** Last 20 dismissed feature IDs (circular buffer) */\n recentDismissals: string[];\n /** Per-feature interaction data */\n featureInteractions: Record<\n string,\n {\n seen: number;\n clicked: number;\n dismissed: number;\n completed: number;\n lastInteraction: string;\n }\n >;\n}\n\nconst STORAGE_KEY = \"fd_behavior\";\nconst MAX_RECENT_DISMISSALS = 20;\n\nfunction createEmptyProfile(): BehaviorProfile {\n return {\n version: 1,\n firstSeen: new Date().toISOString(),\n sessionCount: 0,\n totalInteractions: 0,\n dismissCount: 0,\n clickCount: 0,\n completionCount: 0,\n formatPrefs: { badge: 0, toast: 0, modal: 0, inline: 0, banner: 0, spotlight: 0 },\n hourlyActivity: new Array(24).fill(0),\n recentDismissals: [],\n featureInteractions: {},\n };\n}\n\nexport class BehaviorTracker {\n private profile: BehaviorProfile;\n private sessionStartTime: number;\n\n constructor() {\n this.profile = this.load();\n this.sessionStartTime = Date.now();\n this.profile.sessionCount++;\n this.save();\n }\n\n /** Track a feature interaction */\n trackInteraction(featureId: string, type: InteractionType): void {\n const hour = new Date().getHours();\n this.profile.hourlyActivity[hour]++;\n this.profile.totalInteractions++;\n\n if (!this.profile.featureInteractions[featureId]) {\n this.profile.featureInteractions[featureId] = {\n seen: 0,\n clicked: 0,\n dismissed: 0,\n completed: 0,\n lastInteraction: new Date().toISOString(),\n };\n }\n\n const fi = this.profile.featureInteractions[featureId];\n fi.lastInteraction = new Date().toISOString();\n\n switch (type) {\n case \"seen\":\n case \"hovered\":\n case \"expanded\":\n fi.seen++;\n break;\n case \"clicked\":\n fi.clicked++;\n this.profile.clickCount++;\n break;\n case \"dismissed\":\n case \"snoozed\":\n fi.dismissed++;\n this.profile.dismissCount++;\n this.profile.recentDismissals.push(featureId);\n if (this.profile.recentDismissals.length > MAX_RECENT_DISMISSALS) {\n this.profile.recentDismissals.shift();\n }\n break;\n case \"completed\":\n fi.completed++;\n this.profile.completionCount++;\n break;\n }\n\n this.save();\n }\n\n /** Record a format engagement (user interacted via this format) */\n trackFormatEngagement(format: keyof BehaviorProfile[\"formatPrefs\"]): void {\n if (format in this.profile.formatPrefs) {\n this.profile.formatPrefs[format]++;\n this.save();\n }\n }\n\n /** Get current profile */\n getProfile(): Readonly<BehaviorProfile> {\n return this.profile;\n }\n\n /** Session count */\n getSessionCount(): number {\n return this.profile.sessionCount;\n }\n\n /** Average session duration (rough estimate based on current session) */\n getSessionAge(): number {\n return (Date.now() - this.sessionStartTime) / 1000;\n }\n\n /** Dismiss rate (0-1) */\n getDismissRate(): number {\n if (this.profile.totalInteractions === 0) return 0;\n return this.profile.dismissCount / this.profile.totalInteractions;\n }\n\n /** Engagement rate (clicked or completed / total interactions) */\n getEngagementRate(): number {\n if (this.profile.totalInteractions === 0) return 0;\n return (\n (this.profile.clickCount + this.profile.completionCount) /\n this.profile.totalInteractions\n );\n }\n\n /** Preferred format based on highest engagement count */\n getPreferredFormat(): keyof BehaviorProfile[\"formatPrefs\"] {\n const prefs = this.profile.formatPrefs;\n let best: keyof typeof prefs = \"badge\";\n let bestCount = 0;\n for (const [format, count] of Object.entries(prefs)) {\n if (count > bestCount) {\n bestCount = count;\n best = format as keyof typeof prefs;\n }\n }\n return best;\n }\n\n /** Top 3 active hours (0-23) */\n getActiveHours(): number[] {\n return this.profile.hourlyActivity\n .map((count, hour) => ({ hour, count }))\n .sort((a, b) => b.count - a.count)\n .slice(0, 3)\n .map((h) => h.hour);\n }\n\n /** Recent dismissals (last N feature IDs) */\n getRecentDismissals(): string[] {\n return this.profile.recentDismissals;\n }\n\n /** Count of dismissals in last N milliseconds */\n getRecentDismissalCount(windowMs: number): number {\n const now = new Date();\n const cutoff = new Date(now.getTime() - windowMs);\n let count = 0;\n for (const featureId of this.profile.recentDismissals) {\n const fi = this.profile.featureInteractions[featureId];\n if (fi && new Date(fi.lastInteraction) >= cutoff) {\n count++;\n }\n }\n return count;\n }\n\n /** Get interaction data for a specific feature */\n getFeatureInteractions(featureId: string) {\n return this.profile.featureInteractions[featureId] ?? null;\n }\n\n /** Clear all behavior data */\n clearProfile(): void {\n this.profile = createEmptyProfile();\n this.save();\n }\n\n private load(): BehaviorProfile {\n if (typeof localStorage === \"undefined\") return createEmptyProfile();\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return createEmptyProfile();\n const parsed = JSON.parse(raw) as BehaviorProfile;\n if (parsed.version !== 1) return createEmptyProfile();\n return parsed;\n } catch {\n return createEmptyProfile();\n }\n }\n\n private save(): void {\n if (typeof localStorage === \"undefined\") return;\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(this.profile));\n } catch {\n // localStorage full or unavailable — silently fail\n }\n }\n}\n","import type { DeliveryContext, TimingDecision } from \"../types\";\nimport type { BehaviorTracker } from \"./behavior-tracker\";\n\nexport interface TimingOptimizerConfig {\n /** Minimum session age in ms before showing anything (default: 10000) */\n sessionGateMs: number;\n /** Max dismissals in the backoff window before backing off (default: 2) */\n maxDismissalsBeforeBackoff: number;\n /** Backoff window in ms to count recent dismissals (default: 180000 = 3 min) */\n dismissBackoffWindowMs: number;\n /** Cooldown between announcements in ms (default: 60000) */\n cooldownMs: number;\n /** Path patterns to exclude (glob-style simple matching) */\n excludePaths: string[];\n}\n\nconst DEFAULTS: TimingOptimizerConfig = {\n sessionGateMs: 10_000,\n maxDismissalsBeforeBackoff: 2,\n dismissBackoffWindowMs: 180_000,\n cooldownMs: 60_000,\n excludePaths: [\"/checkout\", \"/auth/*\", \"/error\", \"/login\", \"/signup\"],\n};\n\nexport class TimingOptimizer {\n private config: TimingOptimizerConfig;\n private tracker: BehaviorTracker;\n private lastAnnouncementTime = 0;\n\n constructor(tracker: BehaviorTracker, config?: Partial<TimingOptimizerConfig>) {\n this.tracker = tracker;\n this.config = { ...DEFAULTS, ...config };\n }\n\n /** Decide whether to show a feature announcement now */\n shouldShowNow(_featureId: string, context: DeliveryContext): TimingDecision {\n // 1. SESSION GATE — don't interrupt immediately\n if (context.sessionAge < this.config.sessionGateMs / 1000) {\n return {\n show: false,\n reason: \"session_too_young\",\n delayMs: this.config.sessionGateMs - context.sessionAge * 1000,\n confidence: 0.9,\n };\n }\n\n // 2. DISMISS VELOCITY — user is dismissing everything, back off\n const recentDismissals = this.tracker.getRecentDismissalCount(\n this.config.dismissBackoffWindowMs\n );\n if (recentDismissals >= this.config.maxDismissalsBeforeBackoff) {\n return {\n show: false,\n reason: \"high_dismiss_rate\",\n delayMs: this.config.dismissBackoffWindowMs,\n confidence: 0.85,\n };\n }\n\n // 3. PAGE EXCLUSION — don't show on checkout, auth, error pages\n if (this.isExcludedPath(context.currentPath)) {\n return {\n show: false,\n reason: \"excluded_page\",\n confidence: 1.0,\n };\n }\n\n // 4. COOLDOWN — minimum gap between announcements\n const now = Date.now();\n if (this.lastAnnouncementTime > 0 && now - this.lastAnnouncementTime < this.config.cooldownMs) {\n return {\n show: false,\n reason: \"cooldown\",\n delayMs: this.config.cooldownMs - (now - this.lastAnnouncementTime),\n confidence: 0.9,\n };\n }\n\n // 5. PRIORITY OVERRIDE — critical features bypass timing\n if (context.featurePriority === \"critical\") {\n this.lastAnnouncementTime = now;\n return {\n show: true,\n reason: \"priority_override\",\n confidence: 1.0,\n };\n }\n\n // 6. ACTIVE HOURS BOOST — user is typically active at this hour\n let confidence = 0.6;\n const currentHour = new Date().getHours();\n const activeHours = this.tracker.getActiveHours();\n if (activeHours.includes(currentHour)) {\n confidence += 0.2;\n }\n\n // 7. ENGAGEMENT HISTORY — if user engages, confidence goes up\n const engagementRate = this.tracker.getEngagementRate();\n if (engagementRate > 0.5) {\n confidence += 0.1;\n }\n\n // 8. LOW PRIORITY FEATURES — need higher confidence\n if (context.featurePriority === \"low\" && confidence < 0.7) {\n return {\n show: false,\n reason: \"low_priority_insufficient_confidence\",\n delayMs: this.config.cooldownMs,\n confidence,\n };\n }\n\n this.lastAnnouncementTime = now;\n return {\n show: true,\n reason: \"optimal_window\",\n confidence: Math.min(confidence, 1.0),\n };\n }\n\n /** Mark that an announcement was shown (for cooldown tracking) */\n recordAnnouncementShown(): void {\n this.lastAnnouncementTime = Date.now();\n }\n\n private isExcludedPath(path: string): boolean {\n return this.config.excludePaths.some((pattern) => {\n if (pattern.endsWith(\"/*\")) {\n const prefix = pattern.slice(0, -2);\n return path === prefix || path.startsWith(prefix + \"/\");\n }\n return path === pattern;\n });\n }\n}\n","import type { DisplayFormat, FeaturePriority, FormatRecommendation } from \"../types\";\nimport type { BehaviorTracker } from \"./behavior-tracker\";\n\nexport class FormatSelector {\n private tracker: BehaviorTracker;\n\n constructor(tracker: BehaviorTracker) {\n this.tracker = tracker;\n }\n\n /** Recommend the best display format for a feature */\n recommendFormat(\n featureId: string,\n priority: FeaturePriority\n ): FormatRecommendation {\n const profile = this.tracker.getProfile();\n const sessionCount = this.tracker.getSessionCount();\n const fi = this.tracker.getFeatureInteractions(featureId);\n\n // Critical features get modal or banner\n if (priority === \"critical\") {\n return {\n primary: \"modal\",\n fallback: \"banner\",\n reason: \"critical_priority\",\n };\n }\n\n // Low priority features get badge or inline\n if (priority === \"low\") {\n return {\n primary: \"badge\",\n fallback: \"inline\",\n reason: \"low_priority\",\n };\n }\n\n // New users (< 3 sessions) get gentler formats\n if (sessionCount < 3) {\n return {\n primary: \"badge\",\n fallback: \"toast\",\n reason: \"new_user_gentle\",\n };\n }\n\n // Power users (50+ sessions) get concise formats\n if (sessionCount > 50) {\n return {\n primary: \"badge\",\n fallback: \"inline\",\n reason: \"power_user_concise\",\n };\n }\n\n // Format preference from history — if user engages with a format, prefer it\n const prefs = profile.formatPrefs;\n const totalFormatInteractions =\n prefs.badge + prefs.toast + prefs.modal + prefs.inline + prefs.banner + prefs.spotlight;\n\n if (totalFormatInteractions >= 5) {\n // Find the most engaged-with format\n const best = this.getBestFormat(prefs);\n const fallback = this.getFallbackFormat(best);\n return {\n primary: best,\n fallback,\n reason: \"user_preference\",\n };\n }\n\n // Escalation logic — if feature was shown as badge and ignored, escalate\n if (fi) {\n const seenCount = fi.seen;\n const clicked = fi.clicked;\n\n // Shown as badge 3+ times but never clicked → escalate to toast\n if (seenCount >= 3 && clicked === 0) {\n return {\n primary: \"toast\",\n fallback: \"badge\",\n reason: \"escalation_from_badge\",\n };\n }\n\n // Shown 6+ times without click → escalate to modal\n if (seenCount >= 6 && clicked === 0) {\n return {\n primary: \"modal\",\n fallback: \"toast\",\n reason: \"escalation_from_toast\",\n };\n }\n }\n\n // Dismiss rate check — if user dismisses a lot, use less intrusive formats\n const dismissRate = this.tracker.getDismissRate();\n if (dismissRate > 0.7) {\n return {\n primary: \"badge\",\n fallback: \"inline\",\n reason: \"high_dismiss_rate_gentle\",\n };\n }\n\n // Default for normal priority\n return {\n primary: \"toast\",\n fallback: \"badge\",\n reason: \"default_normal\",\n };\n }\n\n private getBestFormat(\n prefs: Record<string, number>\n ): DisplayFormat {\n let best: DisplayFormat = \"badge\";\n let bestCount = 0;\n for (const [format, count] of Object.entries(prefs)) {\n if (count > bestCount) {\n bestCount = count;\n best = format as DisplayFormat;\n }\n }\n return best;\n }\n\n private getFallbackFormat(primary: DisplayFormat): DisplayFormat {\n const fallbacks: Record<DisplayFormat, DisplayFormat> = {\n badge: \"inline\",\n toast: \"badge\",\n modal: \"toast\",\n banner: \"toast\",\n inline: \"badge\",\n spotlight: \"toast\",\n };\n return fallbacks[primary];\n }\n}\n","import type {\n AdoptionScore,\n FeatureAdoptionStatus,\n FeatureEntry,\n} from \"../types\";\nimport type { BehaviorTracker } from \"./behavior-tracker\";\n\nexport class AdoptionScorer {\n private tracker: BehaviorTracker;\n\n constructor(tracker: BehaviorTracker) {\n this.tracker = tracker;\n }\n\n /** Calculate overall adoption score (0-100) */\n getAdoptionScore(manifest: readonly FeatureEntry[]): AdoptionScore {\n if (manifest.length === 0) {\n return {\n score: 100,\n grade: \"A\",\n breakdown: {\n featuresExplored: 1,\n dismissRate: 0,\n completionRate: 1,\n engagementTrend: \"stable\",\n },\n recommendations: [],\n };\n }\n\n const statuses = manifest.map((f) => this.getFeatureAdoption(f.id));\n\n const explored = statuses.filter(\n (s) => s.status === \"explored\" || s.status === \"adopted\"\n ).length;\n const adopted = statuses.filter((s) => s.status === \"adopted\").length;\n const dismissed = statuses.filter((s) => s.status === \"dismissed\").length;\n\n const explorationRate = explored / manifest.length;\n const adoptionRate = adopted / manifest.length;\n const dismissRate =\n explored + dismissed > 0 ? dismissed / (explored + dismissed) : 0;\n\n // Weighted score\n const rawScore =\n explorationRate * 30 + adoptionRate * 50 + (1 - dismissRate) * 20;\n\n const score = Math.round(Math.min(100, Math.max(0, rawScore)));\n\n const grade = this.toGrade(score);\n\n // Trend detection\n const engagementTrend = this.detectTrend();\n\n // Recommendations\n const recommendations: string[] = [];\n\n const unseen = statuses.filter((s) => s.status === \"unseen\");\n if (unseen.length > 0) {\n const first = unseen[0];\n recommendations.push(\n `User hasn't seen \"${first.featureId}\" — consider showing as badge or toast.`\n );\n }\n\n if (dismissRate > 0.5) {\n recommendations.push(\n \"High dismiss rate — try less intrusive formats (badge instead of modal).\"\n );\n }\n\n if (explorationRate < 0.3) {\n recommendations.push(\n \"Low feature exploration — consider a guided tour to highlight key features.\"\n );\n }\n\n return {\n score,\n grade,\n breakdown: {\n featuresExplored: explorationRate,\n dismissRate,\n completionRate: adoptionRate,\n engagementTrend,\n },\n recommendations,\n };\n }\n\n /** Get adoption status for a specific feature */\n getFeatureAdoption(featureId: string): FeatureAdoptionStatus {\n const fi = this.tracker.getFeatureInteractions(featureId);\n\n if (!fi) {\n return {\n featureId,\n status: \"unseen\",\n interactionCount: 0,\n };\n }\n\n const interactionCount = fi.seen + fi.clicked + fi.dismissed + fi.completed;\n\n let status: FeatureAdoptionStatus[\"status\"];\n if (fi.completed > 0) {\n status = \"adopted\";\n } else if (fi.dismissed > 0 && fi.clicked === 0) {\n status = \"dismissed\";\n } else if (fi.clicked > 0) {\n status = \"explored\";\n } else if (fi.seen > 0) {\n status = \"seen\";\n } else {\n status = \"unseen\";\n }\n\n return {\n featureId,\n status,\n firstSeen: fi.lastInteraction, // approximate — we don't store firstSeen separately\n lastInteraction: fi.lastInteraction,\n interactionCount,\n };\n }\n\n private toGrade(score: number): AdoptionScore[\"grade\"] {\n if (score >= 90) return \"A\";\n if (score >= 75) return \"B\";\n if (score >= 60) return \"C\";\n if (score >= 40) return \"D\";\n return \"F\";\n }\n\n private detectTrend(): \"rising\" | \"stable\" | \"declining\" {\n const profile = this.tracker.getProfile();\n\n // Simple heuristic: if there are more recent interactions than older ones\n const hourly = profile.hourlyActivity;\n const recentHalf = hourly.slice(12).reduce((a, b) => a + b, 0);\n const olderHalf = hourly.slice(0, 12).reduce((a, b) => a + b, 0);\n\n if (recentHalf > olderHalf * 1.2) return \"rising\";\n if (recentHalf < olderHalf * 0.8) return \"declining\";\n return \"stable\";\n }\n}\n","import type {\n AdoptionScore,\n DeliveryContext,\n FeatureAdoptionStatus,\n FeatureDropEngine,\n FeatureEntry,\n FormatRecommendation,\n InteractionType,\n TimingDecision,\n} from \"../types\";\nimport { BehaviorTracker } from \"./behavior-tracker\";\nimport type { TimingOptimizerConfig } from \"./timing-optimizer\";\nimport { TimingOptimizer } from \"./timing-optimizer\";\nimport { FormatSelector } from \"./format-selector\";\nimport { AdoptionScorer } from \"./adoption-scorer\";\n\nexport type { BehaviorProfile } from \"./behavior-tracker\";\nexport { BehaviorTracker } from \"./behavior-tracker\";\nexport { TimingOptimizer } from \"./timing-optimizer\";\nexport type { TimingOptimizerConfig } from \"./timing-optimizer\";\nexport { FormatSelector } from \"./format-selector\";\nexport { AdoptionScorer } from \"./adoption-scorer\";\n\n/** Configuration for creating an AdoptionEngine */\nexport interface AdoptionEngineConfig {\n /** Feature manifest (required for adoption scoring) */\n manifest: readonly FeatureEntry[];\n /** Timing optimizer settings */\n timing?: Partial<TimingOptimizerConfig>;\n}\n\n/**\n * Client-side behavioral intelligence engine for FeatureDrop.\n *\n * Tracks user interactions, optimizes announcement timing,\n * recommends display formats, and scores feature adoption —\n * all client-side with zero data transfer.\n *\n * @example\n * ```ts\n * import { createAdoptionEngine } from 'featuredrop/engine'\n *\n * const engine = createAdoptionEngine({\n * manifest: features,\n * timing: { cooldownMs: 30_000 },\n * })\n *\n * <FeatureDropProvider manifest={features} engine={engine}>\n * <App />\n * </FeatureDropProvider>\n * ```\n */\nexport class AdoptionEngine implements FeatureDropEngine {\n private tracker: BehaviorTracker;\n private timing: TimingOptimizer;\n private format: FormatSelector;\n private scorer: AdoptionScorer;\n private manifest: readonly FeatureEntry[];\n\n constructor(config: AdoptionEngineConfig) {\n this.manifest = config.manifest;\n this.tracker = new BehaviorTracker();\n this.timing = new TimingOptimizer(this.tracker, config.timing);\n this.format = new FormatSelector(this.tracker);\n this.scorer = new AdoptionScorer(this.tracker);\n }\n\n /** Initialize the engine (called by FeatureDropProvider on mount) */\n initialize(): void {\n // BehaviorTracker self-initializes from localStorage in constructor\n }\n\n /** Cleanup resources (called by FeatureDropProvider on unmount) */\n destroy(): void {\n // No cleanup needed — data persists in localStorage\n }\n\n /** Decide whether to show a feature announcement now */\n shouldShow(featureId: string, context: DeliveryContext): TimingDecision {\n return this.timing.shouldShowNow(featureId, context);\n }\n\n /** Recommend the best display format for a feature */\n recommendFormat(featureId: string): FormatRecommendation {\n const feature = this.manifest.find((f) => f.id === featureId);\n const priority = feature?.priority ?? \"normal\";\n return this.format.recommendFormat(featureId, priority);\n }\n\n /** Get the user's overall adoption score */\n getAdoptionScore(): AdoptionScore {\n return this.scorer.getAdoptionScore(this.manifest);\n }\n\n /** Track a user interaction with a feature */\n trackInteraction(featureId: string, type: InteractionType): void {\n this.tracker.trackInteraction(featureId, type);\n }\n\n /** Get adoption status for a specific feature */\n getFeatureAdoption(featureId: string): FeatureAdoptionStatus {\n return this.scorer.getFeatureAdoption(featureId);\n }\n\n /** Access the behavior tracker for advanced usage */\n getBehaviorTracker(): BehaviorTracker {\n return this.tracker;\n }\n\n /** Clear all behavior data */\n clearProfile(): void {\n this.tracker.clearProfile();\n }\n\n /** Update the manifest (e.g., if features change at runtime) */\n updateManifest(manifest: readonly FeatureEntry[]): void {\n this.manifest = manifest;\n }\n}\n\n/**\n * Create a new AdoptionEngine instance.\n *\n * @example\n * ```ts\n * import { createAdoptionEngine } from 'featuredrop/engine'\n *\n * const engine = createAdoptionEngine({\n * manifest: features,\n * timing: {\n * cooldownMs: 30_000,\n * excludePaths: ['/checkout', '/login'],\n * },\n * })\n * ```\n */\nexport function createAdoptionEngine(\n config: AdoptionEngineConfig\n): AdoptionEngine {\n return new AdoptionEngine(config);\n}\n"]}