featuredrop 1.0.1 → 1.2.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.
Files changed (60) hide show
  1. package/README.md +626 -186
  2. package/dist/angular.cjs +286 -0
  3. package/dist/angular.cjs.map +1 -0
  4. package/dist/angular.d.cts +229 -0
  5. package/dist/angular.d.ts +229 -0
  6. package/dist/angular.js +283 -0
  7. package/dist/angular.js.map +1 -0
  8. package/dist/featuredrop.cjs +1256 -0
  9. package/dist/featuredrop.cjs.map +1 -0
  10. package/dist/index.cjs +2769 -9
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +1020 -9
  13. package/dist/index.d.ts +1020 -9
  14. package/dist/index.js +2726 -10
  15. package/dist/index.js.map +1 -1
  16. package/dist/preact.cjs +7289 -0
  17. package/dist/preact.cjs.map +1 -0
  18. package/dist/preact.d.cts +1266 -0
  19. package/dist/preact.d.ts +1266 -0
  20. package/dist/preact.js +7259 -0
  21. package/dist/preact.js.map +1 -0
  22. package/dist/react.cjs +7142 -49
  23. package/dist/react.cjs.map +1 -1
  24. package/dist/react.d.cts +1119 -7
  25. package/dist/react.d.ts +1119 -7
  26. package/dist/react.js +7122 -52
  27. package/dist/react.js.map +1 -1
  28. package/dist/schema.cjs +215 -0
  29. package/dist/schema.cjs.map +1 -0
  30. package/dist/schema.d.cts +203 -0
  31. package/dist/schema.d.ts +203 -0
  32. package/dist/schema.js +209 -0
  33. package/dist/schema.js.map +1 -0
  34. package/dist/solid.cjs +373 -0
  35. package/dist/solid.cjs.map +1 -0
  36. package/dist/solid.d.cts +242 -0
  37. package/dist/solid.d.ts +242 -0
  38. package/dist/solid.js +366 -0
  39. package/dist/solid.js.map +1 -0
  40. package/dist/svelte.cjs +329 -0
  41. package/dist/svelte.cjs.map +1 -0
  42. package/dist/svelte.js +324 -0
  43. package/dist/svelte.js.map +1 -0
  44. package/dist/testing.cjs +1422 -0
  45. package/dist/testing.cjs.map +1 -0
  46. package/dist/testing.d.cts +339 -0
  47. package/dist/testing.d.ts +339 -0
  48. package/dist/testing.js +1415 -0
  49. package/dist/testing.js.map +1 -0
  50. package/dist/vue.cjs +1084 -0
  51. package/dist/vue.cjs.map +1 -0
  52. package/dist/vue.js +1072 -0
  53. package/dist/vue.js.map +1 -0
  54. package/dist/web-components.cjs +483 -0
  55. package/dist/web-components.cjs.map +1 -0
  56. package/dist/web-components.d.cts +211 -0
  57. package/dist/web-components.d.ts +211 -0
  58. package/dist/web-components.js +477 -0
  59. package/dist/web-components.js.map +1 -0
  60. package/package.json +126 -3
@@ -0,0 +1,1415 @@
1
+ import { createContext, useRef, useState, useMemo, useCallback, useEffect } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/analytics.ts
5
+ var AnalyticsCollector = class {
6
+ adapter;
7
+ queue = [];
8
+ batchSize;
9
+ flushInterval;
10
+ sampleRate;
11
+ enabled;
12
+ now;
13
+ random;
14
+ sessionId;
15
+ userId;
16
+ timer = null;
17
+ flushing = false;
18
+ constructor(options) {
19
+ this.adapter = options.adapter;
20
+ this.batchSize = options.batchSize ?? 20;
21
+ this.flushInterval = options.flushInterval ?? 1e4;
22
+ this.sampleRate = options.sampleRate ?? 1;
23
+ this.enabled = options.enabled ?? true;
24
+ this.sessionId = options.sessionId;
25
+ this.userId = options.userId;
26
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
27
+ this.random = options.random ?? Math.random;
28
+ this.startTimer();
29
+ }
30
+ setEnabled(enabled) {
31
+ this.enabled = enabled;
32
+ }
33
+ setContext(context) {
34
+ if (context.sessionId !== void 0) this.sessionId = context.sessionId;
35
+ if (context.userId !== void 0) this.userId = context.userId;
36
+ }
37
+ getQueueSize() {
38
+ return this.queue.length;
39
+ }
40
+ track(event) {
41
+ if (!this.enabled) return;
42
+ if (this.sampleRate < 1 && this.random() > this.sampleRate) return;
43
+ const normalized = {
44
+ ...event,
45
+ timestamp: event.timestamp ?? this.now().toISOString(),
46
+ sessionId: event.sessionId ?? this.sessionId,
47
+ userId: event.userId ?? this.userId
48
+ };
49
+ this.queue.push(normalized);
50
+ if (this.queue.length >= this.batchSize) {
51
+ void this.flush();
52
+ }
53
+ }
54
+ async flush() {
55
+ if (this.flushing) return;
56
+ if (this.queue.length === 0) return;
57
+ this.flushing = true;
58
+ const batch = this.queue.splice(0, this.queue.length);
59
+ try {
60
+ if (this.adapter.trackBatch) {
61
+ await this.adapter.trackBatch(batch);
62
+ } else {
63
+ for (const event of batch) {
64
+ await this.adapter.track(event);
65
+ }
66
+ }
67
+ } catch {
68
+ this.queue = [...batch, ...this.queue];
69
+ } finally {
70
+ this.flushing = false;
71
+ }
72
+ }
73
+ async destroy() {
74
+ if (this.timer) {
75
+ clearInterval(this.timer);
76
+ this.timer = null;
77
+ }
78
+ await this.flush();
79
+ }
80
+ startTimer() {
81
+ if (this.flushInterval <= 0) return;
82
+ this.timer = setInterval(() => {
83
+ void this.flush();
84
+ }, this.flushInterval);
85
+ }
86
+ };
87
+
88
+ // src/semver.ts
89
+ var SEMVER_REGEX = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
90
+ function parseSemver(input) {
91
+ const match = input.trim().match(SEMVER_REGEX);
92
+ if (!match) return null;
93
+ return {
94
+ major: Number(match[1]),
95
+ minor: Number(match[2]),
96
+ patch: Number(match[3]),
97
+ prerelease: match[4] ? match[4].split(".") : []
98
+ };
99
+ }
100
+ function compareSemver(a, b) {
101
+ const pa = parseSemver(a);
102
+ const pb = parseSemver(b);
103
+ if (!pa || !pb) return 0;
104
+ for (const key of ["major", "minor", "patch"]) {
105
+ if (pa[key] !== pb[key]) return pa[key] - pb[key];
106
+ }
107
+ const aPre = pa.prerelease;
108
+ const bPre = pb.prerelease;
109
+ if (aPre.length === 0 && bPre.length === 0) return 0;
110
+ if (aPre.length === 0) return 1;
111
+ if (bPre.length === 0) return -1;
112
+ const len = Math.max(aPre.length, bPre.length);
113
+ for (let i = 0; i < len; i++) {
114
+ const ai = aPre[i];
115
+ const bi = bPre[i];
116
+ if (ai === void 0) return -1;
117
+ if (bi === void 0) return 1;
118
+ const aNum = Number(ai);
119
+ const bNum = Number(bi);
120
+ const aIsNum = Number.isInteger(aNum);
121
+ const bIsNum = Number.isInteger(bNum);
122
+ if (aIsNum && bIsNum && aNum !== bNum) return aNum - bNum;
123
+ if (aIsNum !== bIsNum) return aIsNum ? -1 : 1;
124
+ if (ai !== bi) return ai < bi ? -1 : 1;
125
+ }
126
+ return 0;
127
+ }
128
+ function parseComparator(comp) {
129
+ const match = comp.trim().match(/^(>=|<=|>|<|=)?\\s*(.+)$/);
130
+ if (!match) return null;
131
+ const op = match[1] || ">=";
132
+ const version = match[2];
133
+ if (!parseSemver(version)) return null;
134
+ return { op, version };
135
+ }
136
+ function satisfiesComparator(version, comp) {
137
+ const diff = compareSemver(version, comp.version);
138
+ switch (comp.op) {
139
+ case ">":
140
+ return diff > 0;
141
+ case ">=":
142
+ return diff >= 0;
143
+ case "<":
144
+ return diff < 0;
145
+ case "<=":
146
+ return diff <= 0;
147
+ case "=":
148
+ return diff === 0;
149
+ default:
150
+ return false;
151
+ }
152
+ }
153
+ function satisfiesRange(version, range) {
154
+ const parts = range.split(/\s+/).filter(Boolean);
155
+ if (parts.length === 0) return true;
156
+ for (const part of parts) {
157
+ const comp = parseComparator(part);
158
+ if (!comp) return false;
159
+ if (!satisfiesComparator(version, comp)) return false;
160
+ }
161
+ return true;
162
+ }
163
+
164
+ // src/triggers.ts
165
+ function wildcardToRegExp(value) {
166
+ const escaped = value.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
167
+ const pattern = `^${escaped.replace(/\*/g, ".*")}$`;
168
+ return new RegExp(pattern);
169
+ }
170
+ function matchPath(path, pattern) {
171
+ if (pattern instanceof RegExp) return pattern.test(path);
172
+ if (!pattern) return false;
173
+ if (pattern.includes("*")) return wildcardToRegExp(pattern).test(path);
174
+ return path === pattern || path.startsWith(pattern);
175
+ }
176
+ function isTriggerMatch(trigger, context) {
177
+ if (!trigger) return true;
178
+ if (!context) return false;
179
+ if (trigger.type === "page") {
180
+ const path = context.path;
181
+ if (!path) return false;
182
+ return matchPath(path, trigger.match);
183
+ }
184
+ if (trigger.type === "usage") {
185
+ const usage = context.usage ?? {};
186
+ const count = usage[trigger.event] ?? 0;
187
+ return count >= (trigger.minActions ?? 1);
188
+ }
189
+ if (trigger.type === "time") {
190
+ const elapsedMs = context.elapsedMs ?? 0;
191
+ return elapsedMs >= trigger.minSeconds * 1e3;
192
+ }
193
+ if (trigger.type === "milestone") {
194
+ return context.milestones?.has(trigger.event) ?? false;
195
+ }
196
+ if (trigger.type === "frustration") {
197
+ const usage = context.usage ?? {};
198
+ const count = usage[trigger.pattern] ?? 0;
199
+ return count >= (trigger.threshold ?? 1);
200
+ }
201
+ if (trigger.type === "scroll") {
202
+ return (context.scrollPercent ?? 0) >= (trigger.minPercent ?? 50);
203
+ }
204
+ try {
205
+ return trigger.evaluate(context);
206
+ } catch {
207
+ return false;
208
+ }
209
+ }
210
+ var TriggerEngine = class {
211
+ context;
212
+ constructor(initial) {
213
+ this.context = {
214
+ path: initial?.path,
215
+ events: new Set(initial?.events ?? []),
216
+ milestones: new Set(initial?.milestones ?? []),
217
+ usage: { ...initial?.usage ?? {} },
218
+ elapsedMs: initial?.elapsedMs ?? 0,
219
+ scrollPercent: initial?.scrollPercent ?? 0,
220
+ metadata: { ...initial?.metadata ?? {} }
221
+ };
222
+ }
223
+ setPath(path) {
224
+ this.context.path = path;
225
+ }
226
+ trackEvent(event) {
227
+ const next = new Set(this.context.events ?? /* @__PURE__ */ new Set());
228
+ next.add(event);
229
+ this.context.events = next;
230
+ }
231
+ trackUsage(event, delta = 1) {
232
+ const usage = { ...this.context.usage ?? {} };
233
+ usage[event] = (usage[event] ?? 0) + Math.max(1, delta);
234
+ this.context.usage = usage;
235
+ }
236
+ trackMilestone(event) {
237
+ const next = new Set(this.context.milestones ?? /* @__PURE__ */ new Set());
238
+ next.add(event);
239
+ this.context.milestones = next;
240
+ }
241
+ setElapsedMs(elapsedMs) {
242
+ this.context.elapsedMs = Math.max(0, elapsedMs);
243
+ }
244
+ setScrollPercent(scrollPercent) {
245
+ const clamped = Math.max(0, Math.min(100, scrollPercent));
246
+ this.context.scrollPercent = clamped;
247
+ }
248
+ setMetadata(next) {
249
+ this.context.metadata = { ...next };
250
+ }
251
+ getContext() {
252
+ return {
253
+ path: this.context.path,
254
+ events: new Set(this.context.events ?? []),
255
+ milestones: new Set(this.context.milestones ?? []),
256
+ usage: { ...this.context.usage ?? {} },
257
+ elapsedMs: this.context.elapsedMs,
258
+ scrollPercent: this.context.scrollPercent,
259
+ metadata: { ...this.context.metadata ?? {} }
260
+ };
261
+ }
262
+ evaluate(trigger) {
263
+ return isTriggerMatch(trigger, this.context);
264
+ }
265
+ evaluateFeature(feature) {
266
+ return this.evaluate(feature.trigger);
267
+ }
268
+ };
269
+
270
+ // src/core.ts
271
+ function matchesAudience(audience, userContext) {
272
+ if (audience.plan && audience.plan.length > 0) {
273
+ if (!userContext.plan || !audience.plan.includes(userContext.plan)) {
274
+ return false;
275
+ }
276
+ }
277
+ if (audience.role && audience.role.length > 0) {
278
+ if (!userContext.role || !audience.role.includes(userContext.role)) {
279
+ return false;
280
+ }
281
+ }
282
+ if (audience.region && audience.region.length > 0) {
283
+ if (!userContext.region || !audience.region.includes(userContext.region)) {
284
+ return false;
285
+ }
286
+ }
287
+ return true;
288
+ }
289
+ function isAudienceMatch(feature, userContext, matchFn) {
290
+ if (!feature.audience) return true;
291
+ const { plan, role, region, custom } = feature.audience;
292
+ const hasRules = plan && plan.length > 0 || role && role.length > 0 || region && region.length > 0 || custom && Object.keys(custom).length > 0;
293
+ if (!hasRules) return true;
294
+ if (!userContext) return false;
295
+ if (matchFn) return matchFn(feature.audience, userContext);
296
+ return matchesAudience(feature.audience, userContext);
297
+ }
298
+ function isVersionMatch(feature, appVersion) {
299
+ const v = feature.version;
300
+ if (!v || typeof v === "string") return true;
301
+ if (!appVersion) return false;
302
+ if (!v.introduced && !v.showNewUntil && !v.deprecatedAt && !v.showIn) return true;
303
+ if (v.showIn && !satisfiesRange(appVersion, v.showIn)) return false;
304
+ if (v.introduced && compareSemver(appVersion, v.introduced) < 0) return false;
305
+ if (v.deprecatedAt && compareSemver(appVersion, v.deprecatedAt) >= 0) return false;
306
+ if (v.showNewUntil && compareSemver(appVersion, v.showNewUntil) >= 0) return false;
307
+ return true;
308
+ }
309
+ function isDependencyMatch(feature, dismissedIds, dependencyState) {
310
+ const dependsOn = feature.dependsOn;
311
+ if (!dependsOn) return true;
312
+ const seenIds = dependencyState?.seenIds;
313
+ const clickedIds = dependencyState?.clickedIds;
314
+ const dismissedDependencyIds = dependencyState?.dismissedIds ?? dismissedIds;
315
+ if (dependsOn.seen && dependsOn.seen.length > 0) {
316
+ for (const id of dependsOn.seen) {
317
+ const seen = seenIds?.has(id) ?? false;
318
+ if (!seen && !dismissedDependencyIds.has(id)) return false;
319
+ }
320
+ }
321
+ if (dependsOn.clicked && dependsOn.clicked.length > 0) {
322
+ for (const id of dependsOn.clicked) {
323
+ if (!(clickedIds?.has(id) ?? false)) return false;
324
+ }
325
+ }
326
+ if (dependsOn.dismissed && dependsOn.dismissed.length > 0) {
327
+ for (const id of dependsOn.dismissed) {
328
+ if (!dismissedDependencyIds.has(id)) return false;
329
+ }
330
+ }
331
+ return true;
332
+ }
333
+ function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
334
+ if (dismissedIds.has(feature.id)) return false;
335
+ if (!isAudienceMatch(feature, userContext, matchAudience)) return false;
336
+ if (!isDependencyMatch(feature, dismissedIds, dependencyState)) return false;
337
+ if (!isVersionMatch(feature, appVersion)) return false;
338
+ if (!isTriggerMatch(feature.trigger, triggerContext)) return false;
339
+ const nowMs = now.getTime();
340
+ if (feature.publishAt) {
341
+ const publishMs = new Date(feature.publishAt).getTime();
342
+ if (nowMs < publishMs) return false;
343
+ }
344
+ const showUntilMs = new Date(feature.showNewUntil).getTime();
345
+ if (nowMs >= showUntilMs) return false;
346
+ if (watermark) {
347
+ const watermarkMs = new Date(watermark).getTime();
348
+ const releasedMs = new Date(feature.releasedAt).getTime();
349
+ if (releasedMs <= watermarkMs) return false;
350
+ }
351
+ return true;
352
+ }
353
+ function getNewFeatures(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
354
+ const watermark = storage.getWatermark();
355
+ const dismissedIds = storage.getDismissedIds();
356
+ return manifest.filter(
357
+ (f) => isNew(
358
+ f,
359
+ watermark,
360
+ dismissedIds,
361
+ now,
362
+ userContext,
363
+ matchAudience,
364
+ appVersion,
365
+ dependencyState,
366
+ triggerContext
367
+ )
368
+ );
369
+ }
370
+
371
+ // src/helpers.ts
372
+ function createManifest(entries) {
373
+ return Object.freeze([...entries]);
374
+ }
375
+ function getFeatureById(manifest, id) {
376
+ return manifest.find((f) => f.id === id);
377
+ }
378
+
379
+ // src/throttle.ts
380
+ function sortByPriorityAndRecency(features) {
381
+ const priorityWeight = { critical: 3, normal: 2, low: 1 };
382
+ return [...features].sort((a, b) => {
383
+ const scoreA = priorityWeight[a.priority ?? "normal"];
384
+ const scoreB = priorityWeight[b.priority ?? "normal"];
385
+ if (scoreA !== scoreB) return scoreB - scoreA;
386
+ return new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime();
387
+ });
388
+ }
389
+ function applyAnnouncementThrottle(features, options, state, now = Date.now()) {
390
+ const sorted = sortByPriorityAndRecency(features);
391
+ if (sorted.length === 0) {
392
+ return { visible: [], queued: [] };
393
+ }
394
+ if (options?.sessionCooldown && options.sessionCooldown > 0) {
395
+ const elapsed = now - state.sessionStartedAt;
396
+ if (elapsed < options.sessionCooldown) {
397
+ return { visible: [], queued: sorted };
398
+ }
399
+ }
400
+ if (options?.respectDoNotDisturb && state.quietMode) {
401
+ const visible = sorted.filter((feature) => feature.priority === "critical");
402
+ const queued = sorted.filter((feature) => feature.priority !== "critical");
403
+ return { visible, queued };
404
+ }
405
+ const maxVisible = options?.maxSimultaneousBadges;
406
+ if (!maxVisible || !Number.isFinite(maxVisible) || maxVisible < 1) {
407
+ return { visible: sorted, queued: [] };
408
+ }
409
+ return {
410
+ visible: sorted.slice(0, maxVisible),
411
+ queued: sorted.slice(maxVisible)
412
+ };
413
+ }
414
+
415
+ // src/i18n.ts
416
+ var EN_TRANSLATIONS = {
417
+ newBadge: "New",
418
+ whatsNewTitle: "What's New",
419
+ markAllRead: "Mark all as read",
420
+ allCaughtUp: "You're all caught up!",
421
+ close: "Close",
422
+ changelogTitle: "Changelog",
423
+ searchPlaceholder: "Search updates",
424
+ allCategories: "All categories",
425
+ noUpdatesYet: "No updates yet",
426
+ loadMore: "Load more",
427
+ share: "Share",
428
+ skipToEntries: "Skip to changelog entries",
429
+ stepOf: (current, total) => `Step ${current} of ${total}`,
430
+ back: "Back",
431
+ next: "Next",
432
+ skip: "Skip",
433
+ finish: "Finish",
434
+ gotIt: "Got it",
435
+ announcement: "Announcement",
436
+ feedbackTitle: "Share feedback",
437
+ feedbackTrigger: "Feedback",
438
+ feedbackSubmitted: "Thanks for the feedback.",
439
+ submit: "Submit",
440
+ cancel: "Cancel",
441
+ askLater: "Ask me later"
442
+ };
443
+ var SIMPLE_TRANSLATIONS = {
444
+ es: {
445
+ newBadge: "Nuevo",
446
+ whatsNewTitle: "Novedades",
447
+ markAllRead: "Marcar todo como le\xEDdo",
448
+ allCaughtUp: "Est\xE1s al d\xEDa.",
449
+ close: "Cerrar",
450
+ changelogTitle: "Registro de cambios",
451
+ searchPlaceholder: "Buscar actualizaciones",
452
+ allCategories: "Todas las categor\xEDas",
453
+ noUpdatesYet: "A\xFAn no hay actualizaciones",
454
+ loadMore: "Cargar m\xE1s",
455
+ share: "Compartir",
456
+ skipToEntries: "Saltar a las entradas del changelog",
457
+ back: "Atr\xE1s",
458
+ next: "Siguiente",
459
+ skip: "Saltar",
460
+ finish: "Finalizar",
461
+ gotIt: "Entendido",
462
+ announcement: "Anuncio",
463
+ feedbackTitle: "Enviar comentarios",
464
+ feedbackTrigger: "Comentarios",
465
+ feedbackSubmitted: "Gracias por tus comentarios.",
466
+ submit: "Enviar",
467
+ cancel: "Cancelar",
468
+ askLater: "Preguntar m\xE1s tarde"
469
+ },
470
+ fr: {
471
+ newBadge: "Nouveau",
472
+ whatsNewTitle: "Nouveaut\xE9s",
473
+ markAllRead: "Tout marquer comme lu",
474
+ allCaughtUp: "Vous \xEAtes \xE0 jour.",
475
+ close: "Fermer",
476
+ changelogTitle: "Journal des changements",
477
+ searchPlaceholder: "Rechercher des mises \xE0 jour",
478
+ allCategories: "Toutes les cat\xE9gories",
479
+ noUpdatesYet: "Aucune mise \xE0 jour",
480
+ loadMore: "Charger plus",
481
+ share: "Partager",
482
+ skipToEntries: "Aller aux entr\xE9es du changelog",
483
+ back: "Retour",
484
+ next: "Suivant",
485
+ skip: "Passer",
486
+ finish: "Terminer",
487
+ gotIt: "Compris",
488
+ announcement: "Annonce",
489
+ feedbackTitle: "Partager un avis",
490
+ feedbackTrigger: "Avis",
491
+ feedbackSubmitted: "Merci pour votre avis.",
492
+ submit: "Envoyer",
493
+ cancel: "Annuler",
494
+ askLater: "Demander plus tard"
495
+ },
496
+ de: {
497
+ newBadge: "Neu",
498
+ whatsNewTitle: "Neuigkeiten",
499
+ markAllRead: "Alles als gelesen markieren",
500
+ allCaughtUp: "Alles erledigt.",
501
+ close: "Schlie\xDFen",
502
+ changelogTitle: "\xC4nderungsprotokoll",
503
+ searchPlaceholder: "Updates suchen",
504
+ allCategories: "Alle Kategorien",
505
+ noUpdatesYet: "Noch keine Updates",
506
+ loadMore: "Mehr laden",
507
+ share: "Teilen",
508
+ skipToEntries: "Zu den Eintr\xE4gen springen",
509
+ back: "Zur\xFCck",
510
+ next: "Weiter",
511
+ skip: "\xDCberspringen",
512
+ finish: "Fertig",
513
+ gotIt: "Verstanden",
514
+ announcement: "Ank\xFCndigung",
515
+ feedbackTitle: "Feedback teilen",
516
+ feedbackTrigger: "Feedback",
517
+ feedbackSubmitted: "Danke f\xFCr dein Feedback.",
518
+ submit: "Senden",
519
+ cancel: "Abbrechen",
520
+ askLater: "Sp\xE4ter fragen"
521
+ },
522
+ pt: {
523
+ newBadge: "Novo",
524
+ whatsNewTitle: "Novidades",
525
+ markAllRead: "Marcar tudo como lido",
526
+ allCaughtUp: "Tudo em dia.",
527
+ close: "Fechar",
528
+ changelogTitle: "Hist\xF3rico de mudan\xE7as",
529
+ searchPlaceholder: "Buscar atualiza\xE7\xF5es",
530
+ allCategories: "Todas as categorias",
531
+ noUpdatesYet: "Sem atualiza\xE7\xF5es ainda",
532
+ loadMore: "Carregar mais",
533
+ share: "Compartilhar",
534
+ skipToEntries: "Ir para entradas do changelog",
535
+ back: "Voltar",
536
+ next: "Pr\xF3ximo",
537
+ skip: "Pular",
538
+ finish: "Concluir",
539
+ gotIt: "Entendi",
540
+ announcement: "An\xFAncio",
541
+ feedbackTitle: "Enviar feedback",
542
+ feedbackTrigger: "Feedback",
543
+ feedbackSubmitted: "Obrigado pelo feedback.",
544
+ submit: "Enviar",
545
+ cancel: "Cancelar",
546
+ askLater: "Perguntar depois"
547
+ },
548
+ "zh-cn": {
549
+ newBadge: "\u65B0",
550
+ whatsNewTitle: "\u6700\u65B0\u52A8\u6001",
551
+ markAllRead: "\u5168\u90E8\u6807\u8BB0\u4E3A\u5DF2\u8BFB",
552
+ allCaughtUp: "\u4F60\u5DF2\u67E5\u770B\u5168\u90E8\u66F4\u65B0\u3002",
553
+ close: "\u5173\u95ED",
554
+ changelogTitle: "\u66F4\u65B0\u65E5\u5FD7",
555
+ searchPlaceholder: "\u641C\u7D22\u66F4\u65B0",
556
+ allCategories: "\u5168\u90E8\u5206\u7C7B",
557
+ noUpdatesYet: "\u6682\u65E0\u66F4\u65B0",
558
+ loadMore: "\u52A0\u8F7D\u66F4\u591A",
559
+ share: "\u5206\u4EAB",
560
+ skipToEntries: "\u8DF3\u8F6C\u5230\u66F4\u65B0\u6761\u76EE",
561
+ back: "\u8FD4\u56DE",
562
+ next: "\u4E0B\u4E00\u6B65",
563
+ skip: "\u8DF3\u8FC7",
564
+ finish: "\u5B8C\u6210",
565
+ gotIt: "\u77E5\u9053\u4E86",
566
+ announcement: "\u516C\u544A",
567
+ feedbackTitle: "\u63D0\u4EA4\u53CD\u9988",
568
+ feedbackTrigger: "\u53CD\u9988",
569
+ feedbackSubmitted: "\u611F\u8C22\u4F60\u7684\u53CD\u9988\u3002",
570
+ submit: "\u63D0\u4EA4",
571
+ cancel: "\u53D6\u6D88",
572
+ askLater: "\u7A0D\u540E\u8BE2\u95EE"
573
+ },
574
+ ja: {
575
+ newBadge: "\u65B0\u7740",
576
+ whatsNewTitle: "\u65B0\u6A5F\u80FD",
577
+ markAllRead: "\u3059\u3079\u3066\u65E2\u8AAD\u306B\u3059\u308B",
578
+ allCaughtUp: "\u3059\u3079\u3066\u78BA\u8A8D\u6E08\u307F\u3067\u3059\u3002",
579
+ close: "\u9589\u3058\u308B",
580
+ changelogTitle: "\u5909\u66F4\u5C65\u6B74",
581
+ searchPlaceholder: "\u66F4\u65B0\u3092\u691C\u7D22",
582
+ allCategories: "\u3059\u3079\u3066\u306E\u30AB\u30C6\u30B4\u30EA",
583
+ noUpdatesYet: "\u66F4\u65B0\u306F\u3042\u308A\u307E\u305B\u3093",
584
+ loadMore: "\u3055\u3089\u306B\u8868\u793A",
585
+ share: "\u5171\u6709",
586
+ skipToEntries: "\u5909\u66F4\u5C65\u6B74\u3078\u79FB\u52D5",
587
+ back: "\u623B\u308B",
588
+ next: "\u6B21\u3078",
589
+ skip: "\u30B9\u30AD\u30C3\u30D7",
590
+ finish: "\u5B8C\u4E86",
591
+ gotIt: "\u4E86\u89E3",
592
+ announcement: "\u304A\u77E5\u3089\u305B",
593
+ feedbackTitle: "\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u3092\u9001\u4FE1",
594
+ feedbackTrigger: "\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF",
595
+ feedbackSubmitted: "\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u3042\u308A\u304C\u3068\u3046\u3054\u3056\u3044\u307E\u3059\u3002",
596
+ submit: "\u9001\u4FE1",
597
+ cancel: "\u30AD\u30E3\u30F3\u30BB\u30EB",
598
+ askLater: "\u5F8C\u3067\u805E\u304F"
599
+ },
600
+ ko: {
601
+ newBadge: "\uC0C8\uB85C\uC6C0",
602
+ whatsNewTitle: "\uC0C8 \uC18C\uC2DD",
603
+ markAllRead: "\uBAA8\uB450 \uC77D\uC74C \uCC98\uB9AC",
604
+ allCaughtUp: "\uBAA8\uB4E0 \uC5C5\uB370\uC774\uD2B8\uB97C \uD655\uC778\uD588\uC2B5\uB2C8\uB2E4.",
605
+ close: "\uB2EB\uAE30",
606
+ changelogTitle: "\uBCC0\uACBD \uB85C\uADF8",
607
+ searchPlaceholder: "\uC5C5\uB370\uC774\uD2B8 \uAC80\uC0C9",
608
+ allCategories: "\uC804\uCCB4 \uCE74\uD14C\uACE0\uB9AC",
609
+ noUpdatesYet: "\uC5C5\uB370\uC774\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
610
+ loadMore: "\uB354 \uBCF4\uAE30",
611
+ share: "\uACF5\uC720",
612
+ skipToEntries: "\uBCC0\uACBD \uD56D\uBAA9\uC73C\uB85C \uC774\uB3D9",
613
+ back: "\uB4A4\uB85C",
614
+ next: "\uB2E4\uC74C",
615
+ skip: "\uAC74\uB108\uB6F0\uAE30",
616
+ finish: "\uC644\uB8CC",
617
+ gotIt: "\uD655\uC778",
618
+ announcement: "\uACF5\uC9C0",
619
+ feedbackTitle: "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
620
+ feedbackTrigger: "\uD53C\uB4DC\uBC31",
621
+ feedbackSubmitted: "\uD53C\uB4DC\uBC31 \uAC10\uC0AC\uD569\uB2C8\uB2E4.",
622
+ submit: "\uC81C\uCD9C",
623
+ cancel: "\uCDE8\uC18C",
624
+ askLater: "\uB098\uC911\uC5D0 \uBB3B\uAE30"
625
+ },
626
+ ar: {
627
+ newBadge: "\u062C\u062F\u064A\u062F",
628
+ whatsNewTitle: "\u0645\u0627 \u0627\u0644\u062C\u062F\u064A\u062F",
629
+ markAllRead: "\u062A\u062D\u062F\u064A\u062F \u0627\u0644\u0643\u0644 \u0643\u0645\u0642\u0631\u0648\u0621",
630
+ allCaughtUp: "\u062A\u0645\u062A \u0645\u062A\u0627\u0628\u0639\u0629 \u0643\u0644 \u0627\u0644\u062A\u062D\u062F\u064A\u062B\u0627\u062A.",
631
+ close: "\u0625\u063A\u0644\u0627\u0642",
632
+ changelogTitle: "\u0633\u062C\u0644 \u0627\u0644\u062A\u063A\u064A\u064A\u0631\u0627\u062A",
633
+ searchPlaceholder: "\u0627\u0628\u062D\u062B \u0641\u064A \u0627\u0644\u062A\u062D\u062F\u064A\u062B\u0627\u062A",
634
+ allCategories: "\u0643\u0644 \u0627\u0644\u0641\u0626\u0627\u062A",
635
+ noUpdatesYet: "\u0644\u0627 \u062A\u0648\u062C\u062F \u062A\u062D\u062F\u064A\u062B\u0627\u062A \u0628\u0639\u062F",
636
+ loadMore: "\u062A\u062D\u0645\u064A\u0644 \u0627\u0644\u0645\u0632\u064A\u062F",
637
+ share: "\u0645\u0634\u0627\u0631\u0643\u0629",
638
+ skipToEntries: "\u062A\u062E\u0637\u064A \u0625\u0644\u0649 \u0639\u0646\u0627\u0635\u0631 \u0627\u0644\u0633\u062C\u0644",
639
+ back: "\u0631\u062C\u0648\u0639",
640
+ next: "\u0627\u0644\u062A\u0627\u0644\u064A",
641
+ skip: "\u062A\u062E\u0637\u064A",
642
+ finish: "\u0625\u0646\u0647\u0627\u0621",
643
+ gotIt: "\u062A\u0645",
644
+ announcement: "\u0625\u0639\u0644\u0627\u0646",
645
+ feedbackTitle: "\u0634\u0627\u0631\u0643 \u0645\u0644\u0627\u062D\u0638\u0627\u062A\u0643",
646
+ feedbackTrigger: "\u0645\u0644\u0627\u062D\u0638\u0627\u062A",
647
+ feedbackSubmitted: "\u0634\u0643\u0631\u064B\u0627 \u0639\u0644\u0649 \u0645\u0644\u0627\u062D\u0638\u0627\u062A\u0643.",
648
+ submit: "\u0625\u0631\u0633\u0627\u0644",
649
+ cancel: "\u0625\u0644\u063A\u0627\u0621",
650
+ askLater: "\u0627\u0633\u0623\u0644\u0646\u064A \u0644\u0627\u062D\u0642\u064B\u0627"
651
+ },
652
+ hi: {
653
+ newBadge: "\u0928\u092F\u093E",
654
+ whatsNewTitle: "\u0928\u092F\u093E \u0915\u094D\u092F\u093E \u0939\u0948",
655
+ markAllRead: "\u0938\u092D\u0940 \u0915\u094B \u092A\u0922\u093C\u093E \u0939\u0941\u0906 \u091A\u093F\u0939\u094D\u0928\u093F\u0924 \u0915\u0930\u0947\u0902",
656
+ allCaughtUp: "\u0906\u092A\u0928\u0947 \u0938\u092D\u0940 \u0905\u092A\u0921\u0947\u091F \u0926\u0947\u0916 \u0932\u093F\u090F \u0939\u0948\u0902\u0964",
657
+ close: "\u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
658
+ changelogTitle: "\u092A\u0930\u093F\u0935\u0930\u094D\u0924\u0928 \u0938\u0942\u091A\u0940",
659
+ searchPlaceholder: "\u0905\u092A\u0921\u0947\u091F \u0916\u094B\u091C\u0947\u0902",
660
+ allCategories: "\u0938\u092D\u0940 \u0936\u094D\u0930\u0947\u0923\u093F\u092F\u093E\u0902",
661
+ noUpdatesYet: "\u0905\u092D\u0940 \u0915\u094B\u0908 \u0905\u092A\u0921\u0947\u091F \u0928\u0939\u0940\u0902",
662
+ loadMore: "\u0914\u0930 \u0932\u094B\u0921 \u0915\u0930\u0947\u0902",
663
+ share: "\u0938\u093E\u091D\u093E \u0915\u0930\u0947\u0902",
664
+ skipToEntries: "\u091A\u0947\u0902\u091C\u0932\u0949\u0917 \u092A\u094D\u0930\u0935\u093F\u0937\u094D\u091F\u093F\u092F\u094B\u0902 \u092A\u0930 \u091C\u093E\u090F\u0902",
665
+ back: "\u0935\u093E\u092A\u0938",
666
+ next: "\u0905\u0917\u0932\u093E",
667
+ skip: "\u091B\u094B\u0921\u093C\u0947\u0902",
668
+ finish: "\u0938\u092E\u093E\u092A\u094D\u0924",
669
+ gotIt: "\u0920\u0940\u0915 \u0939\u0948",
670
+ announcement: "\u0918\u094B\u0937\u0923\u093E",
671
+ feedbackTitle: "\u092B\u0940\u0921\u092C\u0948\u0915 \u0938\u093E\u091D\u093E \u0915\u0930\u0947\u0902",
672
+ feedbackTrigger: "\u092B\u0940\u0921\u092C\u0948\u0915",
673
+ feedbackSubmitted: "\u092B\u0940\u0921\u092C\u0948\u0915 \u0915\u0947 \u0932\u093F\u090F \u0927\u0928\u094D\u092F\u0935\u093E\u0926\u0964",
674
+ submit: "\u091C\u092E\u093E \u0915\u0930\u0947\u0902",
675
+ cancel: "\u0930\u0926\u094D\u0926 \u0915\u0930\u0947\u0902",
676
+ askLater: "\u092C\u093E\u0926 \u092E\u0947\u0902 \u092A\u0942\u091B\u0947\u0902"
677
+ }
678
+ };
679
+ function resolveTranslations(locale, overrides) {
680
+ const normalizedLocale = (locale ?? "en").toLowerCase();
681
+ const base = SIMPLE_TRANSLATIONS[normalizedLocale] ?? SIMPLE_TRANSLATIONS[normalizedLocale.split("-")[0]] ?? {};
682
+ return {
683
+ ...EN_TRANSLATIONS,
684
+ ...base,
685
+ ...overrides ?? {},
686
+ stepOf: overrides?.stepOf ?? EN_TRANSLATIONS.stepOf
687
+ };
688
+ }
689
+
690
+ // src/variants.ts
691
+ var VARIANT_META_KEY = "featuredropVariant";
692
+ var VARIANT_KEY_STORAGE = "featuredrop:variant-key";
693
+ function readStorageValue(key) {
694
+ const storage = globalThis.localStorage;
695
+ if (!storage || typeof storage.getItem !== "function") return null;
696
+ try {
697
+ return storage.getItem(key);
698
+ } catch {
699
+ return null;
700
+ }
701
+ }
702
+ function writeStorageValue(key, value) {
703
+ const storage = globalThis.localStorage;
704
+ if (!storage || typeof storage.setItem !== "function") return;
705
+ try {
706
+ storage.setItem(key, value);
707
+ } catch {
708
+ }
709
+ }
710
+ function hashToPercent(value) {
711
+ let hash = 2166136261;
712
+ for (let i = 0; i < value.length; i += 1) {
713
+ hash ^= value.charCodeAt(i);
714
+ hash = Math.imul(hash, 16777619);
715
+ }
716
+ return (hash >>> 0) % 100;
717
+ }
718
+ function normalizeSplit(count, split) {
719
+ if (!split || split.length !== count) {
720
+ return Array.from({ length: count }, () => 100 / count);
721
+ }
722
+ const cleaned = split.map((value) => Number.isFinite(value) && value > 0 ? value : 0);
723
+ const total = cleaned.reduce((sum, value) => sum + value, 0);
724
+ if (total <= 0) {
725
+ return Array.from({ length: count }, () => 100 / count);
726
+ }
727
+ return cleaned.map((value) => value / total * 100);
728
+ }
729
+ function pickVariantName(feature, variantKey) {
730
+ const variants = feature.variants;
731
+ if (!variants) return null;
732
+ const names = Object.keys(variants);
733
+ if (names.length === 0) return null;
734
+ if (names.length === 1) return names[0];
735
+ const split = normalizeSplit(names.length, feature.variantSplit);
736
+ const bucket = hashToPercent(`${feature.id}:${variantKey}`);
737
+ let cumulative = 0;
738
+ for (let i = 0; i < names.length; i += 1) {
739
+ cumulative += split[i];
740
+ if (bucket < cumulative) return names[i];
741
+ }
742
+ return names[names.length - 1];
743
+ }
744
+ function getFeatureVariantName(feature) {
745
+ const raw = feature.meta?.[VARIANT_META_KEY];
746
+ return typeof raw === "string" ? raw : void 0;
747
+ }
748
+ function applyFeatureVariant(feature, variantKey) {
749
+ const variantName = pickVariantName(feature, variantKey);
750
+ if (!variantName) return feature;
751
+ const variant = feature.variants?.[variantName];
752
+ if (!variant) return feature;
753
+ return {
754
+ ...feature,
755
+ label: variant.label ?? feature.label,
756
+ description: variant.description ?? feature.description,
757
+ image: variant.image ?? feature.image,
758
+ cta: variant.cta ?? feature.cta,
759
+ meta: {
760
+ ...feature.meta ?? {},
761
+ ...variant.meta ?? {},
762
+ [VARIANT_META_KEY]: variantName
763
+ }
764
+ };
765
+ }
766
+ function applyFeatureVariants(manifest, variantKey) {
767
+ return manifest.map((feature) => applyFeatureVariant(feature, variantKey));
768
+ }
769
+ function createRandomKey() {
770
+ return Math.random().toString(36).slice(2, 12);
771
+ }
772
+ function getOrCreateVariantKey(explicitKey) {
773
+ if (explicitKey) return explicitKey;
774
+ const existing = readStorageValue(VARIANT_KEY_STORAGE);
775
+ if (existing) return existing;
776
+ const next = createRandomKey();
777
+ writeStorageValue(VARIANT_KEY_STORAGE, next);
778
+ return next;
779
+ }
780
+ var FeatureDropContext = createContext(
781
+ null
782
+ );
783
+ var QUIET_MODE_STORAGE_KEY = "featuredrop:quiet-mode";
784
+ var SEEN_FEATURES_STORAGE_KEY = "featuredrop:seen-features";
785
+ var CLICKED_FEATURES_STORAGE_KEY = "featuredrop:clicked-features";
786
+ function getCurrentPath() {
787
+ if (typeof window === "undefined") return "";
788
+ return `${window.location.pathname}${window.location.search}${window.location.hash}`;
789
+ }
790
+ function getScrollPercent() {
791
+ if (typeof window === "undefined" || typeof document === "undefined") return 0;
792
+ const root = document.documentElement;
793
+ const max = root.scrollHeight - window.innerHeight;
794
+ if (max <= 0) return 100;
795
+ return Math.max(0, Math.min(100, Math.round(window.scrollY / max * 100)));
796
+ }
797
+ function readQuietMode() {
798
+ const storage = globalThis.localStorage;
799
+ if (!storage || typeof storage.getItem !== "function") return false;
800
+ try {
801
+ return storage.getItem(QUIET_MODE_STORAGE_KEY) === "1";
802
+ } catch {
803
+ return false;
804
+ }
805
+ }
806
+ function writeQuietMode(enabled) {
807
+ const storage = globalThis.localStorage;
808
+ if (!storage || typeof storage.setItem !== "function") return;
809
+ try {
810
+ storage.setItem(QUIET_MODE_STORAGE_KEY, enabled ? "1" : "0");
811
+ } catch {
812
+ }
813
+ }
814
+ function readIdSet(key) {
815
+ const storage = globalThis.localStorage;
816
+ if (!storage || typeof storage.getItem !== "function") return /* @__PURE__ */ new Set();
817
+ try {
818
+ const raw = storage.getItem(key);
819
+ if (!raw) return /* @__PURE__ */ new Set();
820
+ const parsed = JSON.parse(raw);
821
+ if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
822
+ return new Set(parsed.filter((value) => typeof value === "string"));
823
+ } catch {
824
+ return /* @__PURE__ */ new Set();
825
+ }
826
+ }
827
+ function writeIdSet(key, values) {
828
+ const storage = globalThis.localStorage;
829
+ if (!storage || typeof storage.setItem !== "function") return;
830
+ try {
831
+ storage.setItem(key, JSON.stringify(Array.from(values)));
832
+ } catch {
833
+ }
834
+ }
835
+ function computeFeatureState({
836
+ manifest,
837
+ storage,
838
+ now,
839
+ userContext,
840
+ matchAudience,
841
+ appVersion,
842
+ throttle,
843
+ sessionStartedAt,
844
+ quietMode,
845
+ seenFeatureIds,
846
+ clickedFeatureIds,
847
+ triggerContext
848
+ }) {
849
+ const dismissedIds = storage.getDismissedIds();
850
+ const allFeatures = getNewFeatures(
851
+ manifest,
852
+ storage,
853
+ now,
854
+ userContext,
855
+ matchAudience,
856
+ appVersion,
857
+ {
858
+ seenIds: seenFeatureIds,
859
+ clickedIds: clickedFeatureIds,
860
+ dismissedIds
861
+ },
862
+ triggerContext
863
+ );
864
+ const throttled = applyAnnouncementThrottle(
865
+ allFeatures,
866
+ throttle,
867
+ {
868
+ sessionStartedAt,
869
+ quietMode
870
+ },
871
+ now.getTime()
872
+ );
873
+ return {
874
+ allFeatures,
875
+ visibleFeatures: throttled.visible,
876
+ queuedFeatures: throttled.queued
877
+ };
878
+ }
879
+ function FeatureDropProvider({
880
+ manifest,
881
+ storage,
882
+ analytics,
883
+ userContext,
884
+ matchAudience: matchAudienceFn,
885
+ appVersion,
886
+ throttle,
887
+ variantKey,
888
+ collector,
889
+ locale = "en",
890
+ translations: translationOverrides,
891
+ children
892
+ }) {
893
+ const analyticsRef = useRef(analytics);
894
+ analyticsRef.current = analytics;
895
+ const sessionStartedAtRef = useRef(Date.now());
896
+ const lastModalAtRef = useRef(null);
897
+ const lastTourAtRef = useRef(null);
898
+ const activeSpotlightIdsRef = useRef(/* @__PURE__ */ new Set());
899
+ const triggerEngineRef = useRef(null);
900
+ if (!triggerEngineRef.current) {
901
+ triggerEngineRef.current = new TriggerEngine({
902
+ path: getCurrentPath(),
903
+ scrollPercent: getScrollPercent()
904
+ });
905
+ }
906
+ const lastScrollPercentRef = useRef(getScrollPercent());
907
+ const [triggerVersion, setTriggerVersion] = useState(0);
908
+ const [quietMode, setQuietModeState] = useState(() => readQuietMode());
909
+ const [toastShownIds, setToastShownIds] = useState(/* @__PURE__ */ new Set());
910
+ const [activeSpotlightIds, setActiveSpotlightIds] = useState(/* @__PURE__ */ new Set());
911
+ const [seenFeatureIds, setSeenFeatureIds] = useState(
912
+ () => readIdSet(SEEN_FEATURES_STORAGE_KEY)
913
+ );
914
+ const [clickedFeatureIds, setClickedFeatureIds] = useState(
915
+ () => readIdSet(CLICKED_FEATURES_STORAGE_KEY)
916
+ );
917
+ const resolvedVariantKey = useMemo(() => getOrCreateVariantKey(variantKey), [variantKey]);
918
+ const translations = useMemo(
919
+ () => resolveTranslations(locale, translationOverrides),
920
+ [locale, translationOverrides]
921
+ );
922
+ const resolvedManifest = useMemo(
923
+ () => applyFeatureVariants(manifest, resolvedVariantKey),
924
+ [manifest, resolvedVariantKey]
925
+ );
926
+ const [featureState, setFeatureState] = useState(
927
+ () => computeFeatureState({
928
+ manifest: resolvedManifest,
929
+ storage,
930
+ now: /* @__PURE__ */ new Date(),
931
+ userContext,
932
+ matchAudience: matchAudienceFn,
933
+ appVersion,
934
+ throttle,
935
+ sessionStartedAt: sessionStartedAtRef.current,
936
+ quietMode: readQuietMode(),
937
+ seenFeatureIds: readIdSet(SEEN_FEATURES_STORAGE_KEY),
938
+ clickedFeatureIds: readIdSet(CLICKED_FEATURES_STORAGE_KEY),
939
+ triggerContext: (() => {
940
+ const engine = triggerEngineRef.current;
941
+ if (!engine) return void 0;
942
+ engine.setElapsedMs(Date.now() - sessionStartedAtRef.current);
943
+ return engine.getContext();
944
+ })()
945
+ })
946
+ );
947
+ const recompute = useCallback(() => {
948
+ const engine = triggerEngineRef.current;
949
+ let triggerContext;
950
+ if (engine) {
951
+ engine.setElapsedMs(Date.now() - sessionStartedAtRef.current);
952
+ triggerContext = engine.getContext();
953
+ }
954
+ setFeatureState(
955
+ computeFeatureState({
956
+ manifest: resolvedManifest,
957
+ storage,
958
+ now: /* @__PURE__ */ new Date(),
959
+ userContext,
960
+ matchAudience: matchAudienceFn,
961
+ appVersion,
962
+ throttle,
963
+ sessionStartedAt: sessionStartedAtRef.current,
964
+ quietMode,
965
+ seenFeatureIds,
966
+ clickedFeatureIds,
967
+ triggerContext
968
+ })
969
+ );
970
+ }, [
971
+ resolvedManifest,
972
+ storage,
973
+ userContext,
974
+ matchAudienceFn,
975
+ appVersion,
976
+ throttle,
977
+ quietMode,
978
+ seenFeatureIds,
979
+ clickedFeatureIds,
980
+ triggerVersion
981
+ ]);
982
+ useEffect(() => {
983
+ recompute();
984
+ }, [recompute]);
985
+ const hasTimeTriggers = useMemo(
986
+ () => resolvedManifest.some((feature) => feature.trigger?.type === "time"),
987
+ [resolvedManifest]
988
+ );
989
+ useEffect(() => {
990
+ if (typeof window === "undefined") return;
991
+ const updatePath = () => {
992
+ triggerEngineRef.current?.setPath(getCurrentPath());
993
+ setTriggerVersion((value2) => value2 + 1);
994
+ };
995
+ window.addEventListener("popstate", updatePath);
996
+ window.addEventListener("hashchange", updatePath);
997
+ return () => {
998
+ window.removeEventListener("popstate", updatePath);
999
+ window.removeEventListener("hashchange", updatePath);
1000
+ };
1001
+ }, []);
1002
+ useEffect(() => {
1003
+ if (typeof window === "undefined") return;
1004
+ const updateScroll = () => {
1005
+ const percent = getScrollPercent();
1006
+ if (percent === lastScrollPercentRef.current) return;
1007
+ lastScrollPercentRef.current = percent;
1008
+ triggerEngineRef.current?.setScrollPercent(percent);
1009
+ setTriggerVersion((value2) => value2 + 1);
1010
+ };
1011
+ updateScroll();
1012
+ window.addEventListener("scroll", updateScroll, { passive: true });
1013
+ window.addEventListener("resize", updateScroll);
1014
+ return () => {
1015
+ window.removeEventListener("scroll", updateScroll);
1016
+ window.removeEventListener("resize", updateScroll);
1017
+ };
1018
+ }, []);
1019
+ useEffect(() => {
1020
+ if (!hasTimeTriggers) return;
1021
+ const timer = setInterval(() => {
1022
+ setTriggerVersion((value2) => value2 + 1);
1023
+ }, 1e3);
1024
+ return () => {
1025
+ clearInterval(timer);
1026
+ };
1027
+ }, [hasTimeTriggers]);
1028
+ useEffect(() => {
1029
+ const sessionCooldown = throttle?.sessionCooldown;
1030
+ if (!sessionCooldown || sessionCooldown <= 0) return;
1031
+ const elapsed = Date.now() - sessionStartedAtRef.current;
1032
+ const remaining = sessionCooldown - elapsed;
1033
+ if (remaining <= 0) return;
1034
+ const timer = setTimeout(() => {
1035
+ recompute();
1036
+ }, remaining + 5);
1037
+ return () => clearTimeout(timer);
1038
+ }, [recompute, throttle?.sessionCooldown]);
1039
+ useEffect(() => {
1040
+ let changed = false;
1041
+ const next = new Set(seenFeatureIds);
1042
+ for (const feature of featureState.visibleFeatures) {
1043
+ if (next.has(feature.id)) continue;
1044
+ next.add(feature.id);
1045
+ changed = true;
1046
+ }
1047
+ if (!changed) return;
1048
+ setSeenFeatureIds(next);
1049
+ writeIdSet(SEEN_FEATURES_STORAGE_KEY, next);
1050
+ }, [featureState.visibleFeatures, seenFeatureIds]);
1051
+ const dismiss = useCallback(
1052
+ (id) => {
1053
+ const feature = getFeatureById(resolvedManifest, id);
1054
+ storage.dismiss(id);
1055
+ if (feature) {
1056
+ analyticsRef.current?.onFeatureDismissed?.(feature);
1057
+ }
1058
+ collector?.track({
1059
+ type: "feature_dismissed",
1060
+ featureId: id,
1061
+ variant: feature ? getFeatureVariantName(feature) : void 0
1062
+ });
1063
+ recompute();
1064
+ },
1065
+ [collector, resolvedManifest, storage, recompute]
1066
+ );
1067
+ const dismissAll = useCallback(async () => {
1068
+ await storage.dismissAll(/* @__PURE__ */ new Date());
1069
+ analyticsRef.current?.onAllDismissed?.();
1070
+ recompute();
1071
+ }, [recompute, storage]);
1072
+ const inSessionCooldown = useCallback((now) => {
1073
+ const sessionCooldown = throttle?.sessionCooldown ?? 0;
1074
+ if (!sessionCooldown || sessionCooldown <= 0) return false;
1075
+ return now - sessionStartedAtRef.current < sessionCooldown;
1076
+ }, [throttle?.sessionCooldown]);
1077
+ const shouldSuppressForQuietMode = useCallback((priority) => {
1078
+ if (!throttle?.respectDoNotDisturb || !quietMode) return false;
1079
+ return priority !== "critical";
1080
+ }, [quietMode, throttle?.respectDoNotDisturb]);
1081
+ const getRemainingToastSlots = useCallback(() => {
1082
+ const maxToastsPerSession = throttle?.maxToastsPerSession;
1083
+ if (!maxToastsPerSession || maxToastsPerSession <= 0) return Number.POSITIVE_INFINITY;
1084
+ return Math.max(0, maxToastsPerSession - toastShownIds.size);
1085
+ }, [throttle?.maxToastsPerSession, toastShownIds.size]);
1086
+ const markToastsShown = useCallback((featureIds) => {
1087
+ if (featureIds.length === 0) return;
1088
+ setToastShownIds((previous) => {
1089
+ let changed = false;
1090
+ const next = new Set(previous);
1091
+ for (const id of featureIds) {
1092
+ if (!id || next.has(id)) continue;
1093
+ next.add(id);
1094
+ changed = true;
1095
+ }
1096
+ return changed ? next : previous;
1097
+ });
1098
+ }, []);
1099
+ const canShowModal = useCallback((priority) => {
1100
+ const now = Date.now();
1101
+ if (inSessionCooldown(now)) return false;
1102
+ if (shouldSuppressForQuietMode(priority)) return false;
1103
+ const minTime = throttle?.minTimeBetweenModals ?? 0;
1104
+ const lastShown = lastModalAtRef.current;
1105
+ if (minTime > 0 && lastShown && now - lastShown < minTime) return false;
1106
+ return true;
1107
+ }, [inSessionCooldown, shouldSuppressForQuietMode, throttle?.minTimeBetweenModals]);
1108
+ const markModalShown = useCallback(() => {
1109
+ lastModalAtRef.current = Date.now();
1110
+ }, []);
1111
+ const canShowTour = useCallback(() => {
1112
+ const now = Date.now();
1113
+ if (inSessionCooldown(now)) return false;
1114
+ if (shouldSuppressForQuietMode(void 0)) return false;
1115
+ const minTime = throttle?.minTimeBetweenTours ?? 0;
1116
+ const lastShown = lastTourAtRef.current;
1117
+ if (minTime > 0 && lastShown && now - lastShown < minTime) return false;
1118
+ return true;
1119
+ }, [inSessionCooldown, shouldSuppressForQuietMode, throttle?.minTimeBetweenTours]);
1120
+ const markTourShown = useCallback(() => {
1121
+ lastTourAtRef.current = Date.now();
1122
+ }, []);
1123
+ const acquireSpotlightSlot = useCallback((id, priority) => {
1124
+ if (!id) return false;
1125
+ if (shouldSuppressForQuietMode(priority)) return false;
1126
+ const current = activeSpotlightIdsRef.current;
1127
+ if (current.has(id)) return true;
1128
+ const maxSpotlights = throttle?.maxSimultaneousSpotlights;
1129
+ if (maxSpotlights && maxSpotlights > 0 && current.size >= maxSpotlights) return false;
1130
+ const next = new Set(current);
1131
+ next.add(id);
1132
+ activeSpotlightIdsRef.current = next;
1133
+ setActiveSpotlightIds(next);
1134
+ return true;
1135
+ }, [shouldSuppressForQuietMode, throttle?.maxSimultaneousSpotlights]);
1136
+ const releaseSpotlightSlot = useCallback((id) => {
1137
+ if (!id) return;
1138
+ const current = activeSpotlightIdsRef.current;
1139
+ if (!current.has(id)) return;
1140
+ const next = new Set(current);
1141
+ next.delete(id);
1142
+ activeSpotlightIdsRef.current = next;
1143
+ setActiveSpotlightIds(next);
1144
+ }, []);
1145
+ const setQuietMode = useCallback((enabled) => {
1146
+ setQuietModeState(enabled);
1147
+ writeQuietMode(enabled);
1148
+ }, []);
1149
+ const markFeatureSeen = useCallback((featureId) => {
1150
+ setSeenFeatureIds((previous) => {
1151
+ if (previous.has(featureId)) return previous;
1152
+ const next = new Set(previous);
1153
+ next.add(featureId);
1154
+ writeIdSet(SEEN_FEATURES_STORAGE_KEY, next);
1155
+ const feature = getFeatureById(resolvedManifest, featureId);
1156
+ collector?.track({
1157
+ type: "feature_seen",
1158
+ featureId,
1159
+ variant: feature ? getFeatureVariantName(feature) : void 0
1160
+ });
1161
+ return next;
1162
+ });
1163
+ }, [collector, resolvedManifest]);
1164
+ const markFeatureClicked = useCallback((featureId) => {
1165
+ setClickedFeatureIds((previous) => {
1166
+ if (previous.has(featureId)) return previous;
1167
+ const next = new Set(previous);
1168
+ next.add(featureId);
1169
+ writeIdSet(CLICKED_FEATURES_STORAGE_KEY, next);
1170
+ const feature = getFeatureById(resolvedManifest, featureId);
1171
+ collector?.track({
1172
+ type: "feature_clicked",
1173
+ featureId,
1174
+ variant: feature ? getFeatureVariantName(feature) : void 0
1175
+ });
1176
+ return next;
1177
+ });
1178
+ }, [collector, resolvedManifest]);
1179
+ const trackAdoptionEvent = useCallback((event) => {
1180
+ if (!collector) return;
1181
+ const feature = event.featureId ? getFeatureById(resolvedManifest, event.featureId) : void 0;
1182
+ collector.track({
1183
+ ...event,
1184
+ variant: event.variant ?? (feature ? getFeatureVariantName(feature) : void 0)
1185
+ });
1186
+ }, [collector, resolvedManifest]);
1187
+ const trackUsageEvent = useCallback((event, delta = 1) => {
1188
+ if (!event) return;
1189
+ triggerEngineRef.current?.trackUsage(event, delta);
1190
+ setTriggerVersion((value2) => value2 + 1);
1191
+ }, []);
1192
+ const trackTriggerEvent = useCallback((event) => {
1193
+ if (!event) return;
1194
+ triggerEngineRef.current?.trackEvent(event);
1195
+ setTriggerVersion((value2) => value2 + 1);
1196
+ }, []);
1197
+ const trackMilestone = useCallback((event) => {
1198
+ if (!event) return;
1199
+ triggerEngineRef.current?.trackMilestone(event);
1200
+ setTriggerVersion((value2) => value2 + 1);
1201
+ }, []);
1202
+ const setTriggerPath = useCallback((path) => {
1203
+ if (!path) return;
1204
+ triggerEngineRef.current?.setPath(path);
1205
+ setTriggerVersion((value2) => value2 + 1);
1206
+ }, []);
1207
+ const isNewFn = useCallback(
1208
+ (sidebarKey) => featureState.visibleFeatures.some((feature) => feature.sidebarKey === sidebarKey),
1209
+ [featureState.visibleFeatures]
1210
+ );
1211
+ const getFeature = useCallback(
1212
+ (sidebarKey) => featureState.visibleFeatures.find((feature) => feature.sidebarKey === sidebarKey),
1213
+ [featureState.visibleFeatures]
1214
+ );
1215
+ const newFeaturesSorted = useMemo(() => {
1216
+ const priorityOrder = { critical: 0, normal: 1, low: 2 };
1217
+ return [...featureState.visibleFeatures].sort((a, b) => {
1218
+ const pa = priorityOrder[a.priority ?? "normal"];
1219
+ const pb = priorityOrder[b.priority ?? "normal"];
1220
+ if (pa !== pb) return pa - pb;
1221
+ return new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime();
1222
+ });
1223
+ }, [featureState.visibleFeatures]);
1224
+ const value = useMemo(
1225
+ () => ({
1226
+ manifest: resolvedManifest,
1227
+ newFeatures: featureState.visibleFeatures,
1228
+ queuedFeatures: featureState.queuedFeatures,
1229
+ newCount: featureState.visibleFeatures.length,
1230
+ totalNewCount: featureState.allFeatures.length,
1231
+ newFeaturesSorted,
1232
+ isNew: isNewFn,
1233
+ dismiss,
1234
+ dismissAll,
1235
+ getFeature,
1236
+ quietMode,
1237
+ setQuietMode,
1238
+ markFeatureSeen,
1239
+ markFeatureClicked,
1240
+ getRemainingToastSlots,
1241
+ markToastsShown,
1242
+ canShowModal,
1243
+ markModalShown,
1244
+ canShowTour,
1245
+ markTourShown,
1246
+ acquireSpotlightSlot,
1247
+ releaseSpotlightSlot,
1248
+ activeSpotlightCount: activeSpotlightIds.size,
1249
+ trackAdoptionEvent,
1250
+ locale,
1251
+ translations,
1252
+ trackUsageEvent,
1253
+ trackTriggerEvent,
1254
+ trackMilestone,
1255
+ setTriggerPath
1256
+ }),
1257
+ [
1258
+ resolvedManifest,
1259
+ featureState.visibleFeatures,
1260
+ featureState.queuedFeatures,
1261
+ featureState.allFeatures.length,
1262
+ newFeaturesSorted,
1263
+ isNewFn,
1264
+ dismiss,
1265
+ dismissAll,
1266
+ getFeature,
1267
+ quietMode,
1268
+ setQuietMode,
1269
+ markFeatureSeen,
1270
+ markFeatureClicked,
1271
+ getRemainingToastSlots,
1272
+ markToastsShown,
1273
+ canShowModal,
1274
+ markModalShown,
1275
+ canShowTour,
1276
+ markTourShown,
1277
+ acquireSpotlightSlot,
1278
+ releaseSpotlightSlot,
1279
+ activeSpotlightIds.size,
1280
+ trackAdoptionEvent,
1281
+ locale,
1282
+ translations,
1283
+ trackUsageEvent,
1284
+ trackTriggerEvent,
1285
+ trackMilestone,
1286
+ setTriggerPath
1287
+ ]
1288
+ );
1289
+ return /* @__PURE__ */ jsx(FeatureDropContext.Provider, { value, children });
1290
+ }
1291
+ function toSlug(value) {
1292
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
1293
+ }
1294
+ function toIso(value, now, fallbackDays = 0) {
1295
+ const trimmed = value.trim();
1296
+ if (!trimmed || trimmed === "today") return new Date(now).toISOString();
1297
+ const rel = trimmed.match(/^([+-]\d+)d$/);
1298
+ if (rel) {
1299
+ const days = Number(rel[1]);
1300
+ return new Date(now.getTime() + days * 24 * 60 * 60 * 1e3).toISOString();
1301
+ }
1302
+ const parsed = new Date(trimmed).getTime();
1303
+ if (Number.isFinite(parsed)) return new Date(parsed).toISOString();
1304
+ return new Date(now.getTime() + fallbackDays * 24 * 60 * 60 * 1e3).toISOString();
1305
+ }
1306
+ function createMockManifest(entries, now = /* @__PURE__ */ new Date()) {
1307
+ const normalized = entries.map((entry, index) => {
1308
+ const label = entry.label?.trim() || `Feature ${index + 1}`;
1309
+ const id = entry.id?.trim() || toSlug(label) || `feature-${index + 1}`;
1310
+ const releasedAt = toIso(entry.releasedAt ?? "today", now);
1311
+ const showNewUntil = toIso(entry.showNewUntil ?? "+14d", now, 14);
1312
+ const normalizedEntry = {
1313
+ ...entry,
1314
+ id,
1315
+ label,
1316
+ releasedAt,
1317
+ showNewUntil,
1318
+ type: entry.type ?? "feature"
1319
+ };
1320
+ return normalizedEntry;
1321
+ });
1322
+ return createManifest(normalized);
1323
+ }
1324
+ var MockStorageAdapter = class {
1325
+ watermark;
1326
+ dismissed = /* @__PURE__ */ new Set();
1327
+ constructor(initial) {
1328
+ this.watermark = initial?.watermark ?? null;
1329
+ if (initial?.dismissedIds) {
1330
+ for (const id of initial.dismissedIds) this.dismissed.add(id);
1331
+ }
1332
+ }
1333
+ getWatermark() {
1334
+ return this.watermark;
1335
+ }
1336
+ getDismissedIds() {
1337
+ return this.dismissed;
1338
+ }
1339
+ dismiss(id) {
1340
+ this.dismissed.add(id);
1341
+ }
1342
+ async dismissAll(now) {
1343
+ this.watermark = now.toISOString();
1344
+ this.dismissed.clear();
1345
+ }
1346
+ setWatermark(watermark) {
1347
+ this.watermark = watermark;
1348
+ }
1349
+ setDismissedIds(ids) {
1350
+ this.dismissed = new Set(ids);
1351
+ }
1352
+ reset() {
1353
+ this.watermark = null;
1354
+ this.dismissed.clear();
1355
+ }
1356
+ };
1357
+ function createMockStorage(initial) {
1358
+ return new MockStorageAdapter(initial);
1359
+ }
1360
+ function createTestProvider(props) {
1361
+ return function TestProvider({ children }) {
1362
+ return /* @__PURE__ */ jsx(FeatureDropProvider, { ...props, children });
1363
+ };
1364
+ }
1365
+ function getTimerController() {
1366
+ const viController = globalThis.vi;
1367
+ if (viController && typeof viController.advanceTimersByTime === "function") return viController;
1368
+ const jestController = globalThis.jest;
1369
+ if (jestController && typeof jestController.advanceTimersByTime === "function") return jestController;
1370
+ return null;
1371
+ }
1372
+ function advanceTime(ms) {
1373
+ const controller = getTimerController();
1374
+ if (!controller) throw new Error("No fake timer controller found (vi/jest).");
1375
+ controller.advanceTimersByTime(ms);
1376
+ }
1377
+ function setMockTime(now) {
1378
+ const controller = getTimerController();
1379
+ if (!controller || typeof controller.setSystemTime !== "function") {
1380
+ throw new Error("Timer controller does not support setSystemTime.");
1381
+ }
1382
+ controller.setSystemTime(now);
1383
+ }
1384
+ var MockAnalyticsCollector = class extends AnalyticsCollector {
1385
+ events = [];
1386
+ constructor(options = {}) {
1387
+ const events = [];
1388
+ super({
1389
+ adapter: {
1390
+ track: (event) => {
1391
+ events.push(event);
1392
+ },
1393
+ trackBatch: (batch) => {
1394
+ events.push(...batch);
1395
+ }
1396
+ },
1397
+ batchSize: options.batchSize ?? 1,
1398
+ flushInterval: options.flushInterval ?? 0,
1399
+ sampleRate: options.sampleRate,
1400
+ enabled: options.enabled,
1401
+ sessionId: options.sessionId,
1402
+ userId: options.userId,
1403
+ now: options.now,
1404
+ random: options.random
1405
+ });
1406
+ this.events = events;
1407
+ }
1408
+ clear() {
1409
+ this.events.splice(0, this.events.length);
1410
+ }
1411
+ };
1412
+
1413
+ export { MockAnalyticsCollector, advanceTime, createMockManifest, createMockStorage, createTestProvider, setMockTime };
1414
+ //# sourceMappingURL=testing.js.map
1415
+ //# sourceMappingURL=testing.js.map