featuredrop 1.2.0 → 1.3.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 (84) hide show
  1. package/README.md +171 -0
  2. package/dist/admin.cjs +212 -0
  3. package/dist/admin.cjs.map +1 -0
  4. package/dist/admin.d.cts +176 -0
  5. package/dist/admin.d.ts +176 -0
  6. package/dist/admin.js +207 -0
  7. package/dist/admin.js.map +1 -0
  8. package/dist/angular.cjs +13 -3
  9. package/dist/angular.cjs.map +1 -1
  10. package/dist/angular.d.cts +4 -0
  11. package/dist/angular.d.ts +4 -0
  12. package/dist/angular.js +13 -3
  13. package/dist/angular.js.map +1 -1
  14. package/dist/bridges.cjs +401 -0
  15. package/dist/bridges.cjs.map +1 -0
  16. package/dist/bridges.d.cts +194 -0
  17. package/dist/bridges.d.ts +194 -0
  18. package/dist/bridges.js +394 -0
  19. package/dist/bridges.js.map +1 -0
  20. package/dist/ci.cjs +328 -0
  21. package/dist/ci.cjs.map +1 -0
  22. package/dist/ci.d.cts +176 -0
  23. package/dist/ci.d.ts +176 -0
  24. package/dist/ci.js +324 -0
  25. package/dist/ci.js.map +1 -0
  26. package/dist/featuredrop.cjs +139 -18
  27. package/dist/featuredrop.cjs.map +1 -1
  28. package/dist/flags.cjs +51 -0
  29. package/dist/flags.cjs.map +1 -0
  30. package/dist/flags.d.cts +48 -0
  31. package/dist/flags.d.ts +48 -0
  32. package/dist/flags.js +47 -0
  33. package/dist/flags.js.map +1 -0
  34. package/dist/index.cjs +2583 -665
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +743 -206
  37. package/dist/index.d.ts +743 -206
  38. package/dist/index.js +2552 -666
  39. package/dist/index.js.map +1 -1
  40. package/dist/preact.cjs +710 -209
  41. package/dist/preact.cjs.map +1 -1
  42. package/dist/preact.d.cts +67 -120
  43. package/dist/preact.d.ts +67 -120
  44. package/dist/preact.js +696 -195
  45. package/dist/preact.js.map +1 -1
  46. package/dist/react.cjs +710 -209
  47. package/dist/react.cjs.map +1 -1
  48. package/dist/react.d.cts +67 -120
  49. package/dist/react.d.ts +67 -120
  50. package/dist/react.js +696 -195
  51. package/dist/react.js.map +1 -1
  52. package/dist/schema.cjs +78 -1
  53. package/dist/schema.cjs.map +1 -1
  54. package/dist/schema.d.cts +142 -0
  55. package/dist/schema.d.ts +142 -0
  56. package/dist/schema.js +78 -1
  57. package/dist/schema.js.map +1 -1
  58. package/dist/solid.cjs +13 -3
  59. package/dist/solid.cjs.map +1 -1
  60. package/dist/solid.d.cts +4 -0
  61. package/dist/solid.d.ts +4 -0
  62. package/dist/solid.js +13 -3
  63. package/dist/solid.js.map +1 -1
  64. package/dist/svelte.cjs +13 -3
  65. package/dist/svelte.cjs.map +1 -1
  66. package/dist/svelte.js +13 -3
  67. package/dist/svelte.js.map +1 -1
  68. package/dist/testing.cjs +136 -15
  69. package/dist/testing.cjs.map +1 -1
  70. package/dist/testing.d.cts +22 -0
  71. package/dist/testing.d.ts +22 -0
  72. package/dist/testing.js +136 -15
  73. package/dist/testing.js.map +1 -1
  74. package/dist/vue.cjs +13 -3
  75. package/dist/vue.cjs.map +1 -1
  76. package/dist/vue.js +13 -3
  77. package/dist/vue.js.map +1 -1
  78. package/dist/web-components.cjs +14 -4
  79. package/dist/web-components.cjs.map +1 -1
  80. package/dist/web-components.d.cts +4 -0
  81. package/dist/web-components.d.ts +4 -0
  82. package/dist/web-components.js +14 -4
  83. package/dist/web-components.js.map +1 -1
  84. package/package.json +59 -1
package/dist/preact.js CHANGED
@@ -1,9 +1,137 @@
1
- import { createContext, useRef, useState, useMemo, useCallback, useEffect, useContext } from 'react';
1
+ import { createContext, useRef, useState, useMemo, useCallback, useEffect, useContext, Component } from 'react';
2
2
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
3
3
  import { createRequire } from 'module';
4
4
 
5
5
  // src/react/provider.tsx
6
6
 
7
+ // src/animation.ts
8
+ var injectedAnimationStyles = false;
9
+ function ensureFeatureDropAnimationStyles() {
10
+ if (injectedAnimationStyles || typeof document === "undefined") return;
11
+ injectedAnimationStyles = true;
12
+ const style = document.createElement("style");
13
+ style.setAttribute("data-featuredrop-animations", "true");
14
+ style.textContent = `
15
+ @keyframes featuredrop-enter-fade-up {
16
+ from { opacity: 0; transform: translateY(8px); }
17
+ to { opacity: 1; transform: translateY(0); }
18
+ }
19
+ @keyframes featuredrop-enter-scale {
20
+ from { opacity: 0; transform: scale(0.96); }
21
+ to { opacity: 1; transform: scale(1); }
22
+ }
23
+ @keyframes featuredrop-enter-panel {
24
+ from { opacity: 0; transform: translateX(14px); }
25
+ to { opacity: 1; transform: translateX(0); }
26
+ }
27
+ @keyframes featuredrop-enter-pop {
28
+ 0% { opacity: 0; transform: translateY(12px) scale(0.94); }
29
+ 70% { opacity: 1; transform: translateY(-2px) scale(1.02); }
30
+ 100% { opacity: 1; transform: translateY(0) scale(1); }
31
+ }
32
+ @keyframes featuredrop-pulse {
33
+ 0%, 100% { opacity: 1; }
34
+ 50% { opacity: 0.55; }
35
+ }
36
+ @keyframes featuredrop-pulse-playful {
37
+ 0%, 100% { opacity: 1; transform: scale(1); }
38
+ 40% { opacity: 0.7; transform: scale(1.08); }
39
+ 70% { opacity: 0.9; transform: scale(0.96); }
40
+ }
41
+ @keyframes featuredrop-beacon-pulse {
42
+ 0%, 100% { transform: scale(1); opacity: 0.65; }
43
+ 50% { transform: scale(1.5); opacity: 0; }
44
+ }
45
+ @keyframes featuredrop-beacon-pop-pulse {
46
+ 0%, 100% { transform: scale(1); opacity: 0.72; }
47
+ 45% { transform: scale(1.65); opacity: 0.08; }
48
+ 75% { transform: scale(0.95); opacity: 0.28; }
49
+ }
50
+ @keyframes featuredrop-exit-fade-down {
51
+ from { opacity: 1; transform: translateY(0); }
52
+ to { opacity: 0; transform: translateY(8px); }
53
+ }
54
+ @keyframes featuredrop-exit-scale {
55
+ from { opacity: 1; transform: scale(1); }
56
+ to { opacity: 0; transform: scale(0.96); }
57
+ }
58
+ @keyframes featuredrop-exit-panel {
59
+ from { opacity: 1; transform: translateX(0); }
60
+ to { opacity: 0; transform: translateX(14px); }
61
+ }
62
+ @keyframes featuredrop-exit-pop {
63
+ from { opacity: 1; transform: translateY(0) scale(1); }
64
+ to { opacity: 0; transform: translateY(10px) scale(0.96); }
65
+ }
66
+ `;
67
+ document.head.appendChild(style);
68
+ }
69
+ function prefersReducedMotion() {
70
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
71
+ return false;
72
+ }
73
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
74
+ }
75
+ function resolveAnimationPreset(preset = "normal", options) {
76
+ if (options?.reducedMotion) return "none";
77
+ return preset;
78
+ }
79
+ function getEnterAnimation(preset, surface) {
80
+ if (preset === "none") return void 0;
81
+ if (preset === "subtle") {
82
+ if (surface === "panel") return "featuredrop-enter-panel 180ms ease-out";
83
+ if (surface === "modal") return "featuredrop-enter-scale 180ms ease-out";
84
+ return "featuredrop-enter-fade-up 170ms ease-out";
85
+ }
86
+ if (preset === "playful") {
87
+ if (surface === "panel") return "featuredrop-enter-panel 320ms cubic-bezier(0.2, 0.9, 0.2, 1)";
88
+ return "featuredrop-enter-pop 300ms cubic-bezier(0.22, 1.4, 0.36, 1)";
89
+ }
90
+ if (surface === "panel") return "featuredrop-enter-panel 240ms cubic-bezier(0.2, 0.9, 0.2, 1)";
91
+ if (surface === "modal") return "featuredrop-enter-scale 220ms cubic-bezier(0.2, 0.9, 0.2, 1)";
92
+ return "featuredrop-enter-fade-up 210ms cubic-bezier(0.2, 0.9, 0.2, 1)";
93
+ }
94
+ function getExitAnimation(preset, surface) {
95
+ if (preset === "none") return void 0;
96
+ if (preset === "subtle") {
97
+ if (surface === "panel") return "featuredrop-exit-panel 150ms ease-in forwards";
98
+ if (surface === "modal") return "featuredrop-exit-scale 150ms ease-in forwards";
99
+ return "featuredrop-exit-fade-down 140ms ease-in forwards";
100
+ }
101
+ if (preset === "playful") {
102
+ if (surface === "panel") return "featuredrop-exit-panel 260ms ease-in forwards";
103
+ return "featuredrop-exit-pop 240ms ease-in forwards";
104
+ }
105
+ if (surface === "panel") return "featuredrop-exit-panel 200ms ease-in forwards";
106
+ if (surface === "modal") return "featuredrop-exit-scale 190ms ease-in forwards";
107
+ return "featuredrop-exit-fade-down 180ms ease-in forwards";
108
+ }
109
+ function getPulseAnimation(preset, surface = "beacon") {
110
+ if (preset === "none") return void 0;
111
+ if (surface === "dot") {
112
+ if (preset === "subtle") return "featuredrop-pulse 2.6s ease-in-out infinite";
113
+ if (preset === "playful") {
114
+ return "featuredrop-pulse-playful 1.8s cubic-bezier(0.22, 1.4, 0.36, 1) infinite";
115
+ }
116
+ return "featuredrop-pulse 2s ease-in-out infinite";
117
+ }
118
+ if (preset === "subtle") return "featuredrop-beacon-pulse 2.6s ease-in-out infinite";
119
+ if (preset === "playful") {
120
+ return "featuredrop-beacon-pop-pulse 1.8s cubic-bezier(0.22, 1.4, 0.36, 1) infinite";
121
+ }
122
+ return "featuredrop-beacon-pulse 2s ease-in-out infinite";
123
+ }
124
+ function getAnimationDurationMs(preset, surface, phase) {
125
+ if (preset === "none") return 0;
126
+ const animation = getExitAnimation(preset, surface);
127
+ if (!animation) return 0;
128
+ const msMatch = animation.match(/(\d+)ms/);
129
+ if (msMatch?.[1]) return Number(msMatch[1]);
130
+ const sMatch = animation.match(/(\d+(?:\.\d+)?)s/);
131
+ if (sMatch?.[1]) return Math.round(Number(sMatch[1]) * 1e3);
132
+ return 0;
133
+ }
134
+
7
135
  // src/semver.ts
8
136
  var SEMVER_REGEX = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
9
137
  function parseSemver(input) {
@@ -225,6 +353,20 @@ function isVersionMatch(feature, appVersion) {
225
353
  if (v.showNewUntil && compareSemver(appVersion, v.showNewUntil) >= 0) return false;
226
354
  return true;
227
355
  }
356
+ function isFlagMatch(feature, flagBridge, userContext) {
357
+ if (!feature.flagKey) return true;
358
+ if (!flagBridge) return false;
359
+ try {
360
+ return flagBridge.isEnabled(feature.flagKey, userContext);
361
+ } catch {
362
+ return false;
363
+ }
364
+ }
365
+ function isProductMatch(feature, product) {
366
+ if (!feature.product || feature.product === "*") return true;
367
+ if (!product) return false;
368
+ return feature.product === product;
369
+ }
228
370
  function isDependencyMatch(feature, dismissedIds, dependencyState) {
229
371
  const dependsOn = feature.dependsOn;
230
372
  if (!dependsOn) return true;
@@ -249,11 +391,13 @@ function isDependencyMatch(feature, dismissedIds, dependencyState) {
249
391
  }
250
392
  return true;
251
393
  }
252
- function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
394
+ function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext, flagBridge, product) {
253
395
  if (dismissedIds.has(feature.id)) return false;
254
396
  if (!isAudienceMatch(feature, userContext, matchAudience)) return false;
255
397
  if (!isDependencyMatch(feature, dismissedIds, dependencyState)) return false;
256
398
  if (!isVersionMatch(feature, appVersion)) return false;
399
+ if (!isFlagMatch(feature, flagBridge, userContext)) return false;
400
+ if (!isProductMatch(feature, product)) return false;
257
401
  if (!isTriggerMatch(feature.trigger, triggerContext)) return false;
258
402
  const nowMs = now.getTime();
259
403
  if (feature.publishAt) {
@@ -269,7 +413,7 @@ function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date(
269
413
  }
270
414
  return true;
271
415
  }
272
- function getNewFeatures(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
416
+ function getNewFeatures(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext, flagBridge, product) {
273
417
  const watermark = storage.getWatermark();
274
418
  const dismissedIds = storage.getDismissedIds();
275
419
  return manifest.filter(
@@ -282,7 +426,9 @@ function getNewFeatures(manifest, storage, now = /* @__PURE__ */ new Date(), use
282
426
  matchAudience,
283
427
  appVersion,
284
428
  dependencyState,
285
- triggerContext
429
+ triggerContext,
430
+ flagBridge,
431
+ product
286
432
  )
287
433
  );
288
434
  }
@@ -342,6 +488,7 @@ var EN_TRANSLATIONS = {
342
488
  loadMore: "Load more",
343
489
  share: "Share",
344
490
  skipToEntries: "Skip to changelog entries",
491
+ newFeatureCount: (count) => count === 0 ? "No new features" : `${count} new feature${count === 1 ? "" : "s"}`,
345
492
  stepOf: (current, total) => `Step ${current} of ${total}`,
346
493
  back: "Back",
347
494
  next: "Next",
@@ -592,16 +739,133 @@ var SIMPLE_TRANSLATIONS = {
592
739
  askLater: "\u092C\u093E\u0926 \u092E\u0947\u0902 \u092A\u0942\u091B\u0947\u0902"
593
740
  }
594
741
  };
742
+ var RTL_LANGUAGES = /* @__PURE__ */ new Set(["ar", "fa", "he", "ur"]);
743
+ var STEP_OF_TRANSLATIONS = {
744
+ en: EN_TRANSLATIONS.stepOf,
745
+ es: (current, total) => `Paso ${current} de ${total}`,
746
+ fr: (current, total) => `Etape ${current} sur ${total}`,
747
+ de: (current, total) => `Schritt ${current} von ${total}`,
748
+ pt: (current, total) => `Etapa ${current} de ${total}`,
749
+ "zh-cn": (current, total) => `\u7B2C ${current} / ${total} \u6B65`,
750
+ ja: (current, total) => `${total}\u4E2D${current}\u756A\u76EE`,
751
+ ko: (current, total) => `${total}\uB2E8\uACC4 \uC911 ${current}\uB2E8\uACC4`,
752
+ ar: (current, total) => `\u0627\u0644\u062E\u0637\u0648\u0629 ${current} \u0645\u0646 ${total}`,
753
+ hi: (current, total) => `${total} \u092E\u0947\u0902 \u0938\u0947 \u091A\u0930\u0923 ${current}`
754
+ };
755
+ var NEW_FEATURE_COUNT_TRANSLATIONS = {
756
+ en: EN_TRANSLATIONS.newFeatureCount,
757
+ es: (count) => count === 0 ? "Sin novedades" : `${count} novedad${count === 1 ? "" : "es"}`,
758
+ fr: (count) => count === 0 ? "Aucune nouveaute" : `${count} nouveaute${count === 1 ? "" : "s"}`,
759
+ de: (count) => count === 0 ? "Keine neuen Features" : `${count} ${count === 1 ? "neues Feature" : "neue Features"}`,
760
+ pt: (count) => count === 0 ? "Sem novidades" : `${count} novidade${count === 1 ? "" : "s"}`,
761
+ "zh-cn": (count) => count === 0 ? "\u6682\u65E0\u66F4\u65B0" : `${count} \u6761\u65B0\u66F4\u65B0`,
762
+ ja: (count) => count === 0 ? "\u65B0\u7740\u306F\u3042\u308A\u307E\u305B\u3093" : `\u65B0\u7740 ${count} \u4EF6`,
763
+ ko: (count) => count === 0 ? "\uC0C8 \uC18C\uC2DD \uC5C6\uC74C" : `\uC0C8 \uC18C\uC2DD ${count}\uAC1C`,
764
+ ar: (count) => {
765
+ if (count === 0) return "\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u064A\u0632\u0627\u062A \u062C\u062F\u064A\u062F\u0629";
766
+ const category = new Intl.PluralRules("ar").select(count);
767
+ if (category === "one") return "\u0645\u064A\u0632\u0629 \u062C\u062F\u064A\u062F\u0629 \u0648\u0627\u062D\u062F\u0629";
768
+ if (category === "two") return "\u0645\u064A\u0632\u062A\u0627\u0646 \u062C\u062F\u064A\u062F\u062A\u0627\u0646";
769
+ return `${count} \u0645\u064A\u0632\u0627\u062A \u062C\u062F\u064A\u062F\u0629`;
770
+ },
771
+ hi: (count) => count === 0 ? "\u0915\u094B\u0908 \u0928\u092F\u093E \u0905\u092A\u0921\u0947\u091F \u0928\u0939\u0940\u0902" : `${count} ${count === 1 ? "\u0928\u092F\u093E \u0905\u092A\u0921\u0947\u091F" : "\u0928\u090F \u0905\u092A\u0921\u0947\u091F"}`
772
+ };
773
+ function resolveLocale(locale) {
774
+ const normalized = (locale ?? "en").toLowerCase();
775
+ if (normalized === "en" || normalized.startsWith("en-")) return "en";
776
+ if (Object.prototype.hasOwnProperty.call(SIMPLE_TRANSLATIONS, normalized)) {
777
+ return normalized;
778
+ }
779
+ const base = normalized.split("-")[0];
780
+ if (base === "en") return "en";
781
+ if (Object.prototype.hasOwnProperty.call(SIMPLE_TRANSLATIONS, base)) {
782
+ return base;
783
+ }
784
+ return "en";
785
+ }
786
+ function getLocaleDirection(locale) {
787
+ const resolved = resolveLocale(locale);
788
+ const base = resolved.split("-")[0];
789
+ return RTL_LANGUAGES.has(base) ? "rtl" : "ltr";
790
+ }
791
+ function formatDateForLocale(value, locale, options = {
792
+ month: "short",
793
+ day: "numeric",
794
+ year: "numeric"
795
+ }) {
796
+ const date = value instanceof Date ? value : new Date(value);
797
+ if (Number.isNaN(date.getTime())) return "";
798
+ const resolved = resolveLocale(locale);
799
+ try {
800
+ return new Intl.DateTimeFormat(resolved, options).format(date);
801
+ } catch {
802
+ return date.toLocaleDateString(void 0, options);
803
+ }
804
+ }
805
+ function formatRelativeTimeForLocale(value, locale, options) {
806
+ const target = value instanceof Date ? value : new Date(value);
807
+ if (Number.isNaN(target.getTime())) return "";
808
+ const nowInput = options?.now;
809
+ const nowDate = nowInput instanceof Date ? nowInput : /* @__PURE__ */ new Date();
810
+ if (Number.isNaN(nowDate.getTime())) return "";
811
+ const diffMs = target.getTime() - nowDate.getTime();
812
+ const absDiff = Math.abs(diffMs);
813
+ const minute = 6e4;
814
+ const hour = 60 * minute;
815
+ const day = 24 * hour;
816
+ const week = 7 * day;
817
+ const month = 30 * day;
818
+ const year = 365 * day;
819
+ let unit = "second";
820
+ let divisor = 1e3;
821
+ if (absDiff >= year) {
822
+ unit = "year";
823
+ divisor = year;
824
+ } else if (absDiff >= month) {
825
+ unit = "month";
826
+ divisor = month;
827
+ } else if (absDiff >= week) {
828
+ unit = "week";
829
+ divisor = week;
830
+ } else if (absDiff >= day) {
831
+ unit = "day";
832
+ divisor = day;
833
+ } else if (absDiff >= hour) {
834
+ unit = "hour";
835
+ divisor = hour;
836
+ } else if (absDiff >= minute) {
837
+ unit = "minute";
838
+ divisor = minute;
839
+ }
840
+ const relativeValue = Math.round(diffMs / divisor);
841
+ const resolvedLocale = resolveLocale(locale);
842
+ try {
843
+ const formatter = new Intl.RelativeTimeFormat(resolvedLocale, {
844
+ numeric: options?.numeric ?? "auto",
845
+ style: options?.style ?? "long"
846
+ });
847
+ return formatter.format(relativeValue, unit);
848
+ } catch {
849
+ const fallback = formatDateForLocale(target, resolvedLocale);
850
+ return fallback || target.toISOString();
851
+ }
852
+ }
595
853
  function resolveTranslations(locale, overrides) {
596
- const normalizedLocale = (locale ?? "en").toLowerCase();
597
- const base = SIMPLE_TRANSLATIONS[normalizedLocale] ?? SIMPLE_TRANSLATIONS[normalizedLocale.split("-")[0]] ?? {};
854
+ const resolvedLocale = resolveLocale(locale);
855
+ const base = resolvedLocale === "en" ? {} : SIMPLE_TRANSLATIONS[resolvedLocale] ?? {};
856
+ const stepOf = overrides?.stepOf ?? STEP_OF_TRANSLATIONS[resolvedLocale] ?? STEP_OF_TRANSLATIONS.en;
857
+ const newFeatureCount = overrides?.newFeatureCount ?? NEW_FEATURE_COUNT_TRANSLATIONS[resolvedLocale] ?? NEW_FEATURE_COUNT_TRANSLATIONS.en;
598
858
  return {
599
859
  ...EN_TRANSLATIONS,
600
860
  ...base,
601
861
  ...overrides ?? {},
602
- stepOf: overrides?.stepOf ?? EN_TRANSLATIONS.stepOf
862
+ stepOf,
863
+ newFeatureCount
603
864
  };
604
865
  }
866
+ ({
867
+ ...SIMPLE_TRANSLATIONS
868
+ });
605
869
 
606
870
  // src/variants.ts
607
871
  var VARIANT_META_KEY = "featuredropVariant";
@@ -755,12 +1019,14 @@ function computeFeatureState({
755
1019
  userContext,
756
1020
  matchAudience,
757
1021
  appVersion,
1022
+ product,
758
1023
  throttle,
759
1024
  sessionStartedAt,
760
1025
  quietMode,
761
1026
  seenFeatureIds,
762
1027
  clickedFeatureIds,
763
- triggerContext
1028
+ triggerContext,
1029
+ flagBridge
764
1030
  }) {
765
1031
  const dismissedIds = storage.getDismissedIds();
766
1032
  const allFeatures = getNewFeatures(
@@ -775,7 +1041,9 @@ function computeFeatureState({
775
1041
  clickedIds: clickedFeatureIds,
776
1042
  dismissedIds
777
1043
  },
778
- triggerContext
1044
+ triggerContext,
1045
+ flagBridge,
1046
+ product
779
1047
  );
780
1048
  const throttled = applyAnnouncementThrottle(
781
1049
  allFeatures,
@@ -796,13 +1064,17 @@ function FeatureDropProvider({
796
1064
  manifest,
797
1065
  storage,
798
1066
  analytics,
1067
+ onError,
799
1068
  userContext,
800
1069
  matchAudience: matchAudienceFn,
801
1070
  appVersion,
1071
+ product,
802
1072
  throttle,
803
1073
  variantKey,
804
1074
  collector,
1075
+ flagBridge,
805
1076
  locale = "en",
1077
+ animation = "normal",
806
1078
  translations: translationOverrides,
807
1079
  children
808
1080
  }) {
@@ -831,9 +1103,15 @@ function FeatureDropProvider({
831
1103
  () => readIdSet(CLICKED_FEATURES_STORAGE_KEY)
832
1104
  );
833
1105
  const resolvedVariantKey = useMemo(() => getOrCreateVariantKey(variantKey), [variantKey]);
1106
+ const resolvedLocale = useMemo(() => resolveLocale(locale), [locale]);
1107
+ const direction = useMemo(() => getLocaleDirection(resolvedLocale), [resolvedLocale]);
1108
+ const resolvedAnimation = useMemo(
1109
+ () => resolveAnimationPreset(animation, { reducedMotion: prefersReducedMotion() }),
1110
+ [animation]
1111
+ );
834
1112
  const translations = useMemo(
835
- () => resolveTranslations(locale, translationOverrides),
836
- [locale, translationOverrides]
1113
+ () => resolveTranslations(resolvedLocale, translationOverrides),
1114
+ [resolvedLocale, translationOverrides]
837
1115
  );
838
1116
  const resolvedManifest = useMemo(
839
1117
  () => applyFeatureVariants(manifest, resolvedVariantKey),
@@ -847,6 +1125,7 @@ function FeatureDropProvider({
847
1125
  userContext,
848
1126
  matchAudience: matchAudienceFn,
849
1127
  appVersion,
1128
+ product,
850
1129
  throttle,
851
1130
  sessionStartedAt: sessionStartedAtRef.current,
852
1131
  quietMode: readQuietMode(),
@@ -857,7 +1136,8 @@ function FeatureDropProvider({
857
1136
  if (!engine) return void 0;
858
1137
  engine.setElapsedMs(Date.now() - sessionStartedAtRef.current);
859
1138
  return engine.getContext();
860
- })()
1139
+ })(),
1140
+ flagBridge
861
1141
  })
862
1142
  );
863
1143
  const recompute = useCallback(() => {
@@ -875,12 +1155,14 @@ function FeatureDropProvider({
875
1155
  userContext,
876
1156
  matchAudience: matchAudienceFn,
877
1157
  appVersion,
1158
+ product,
878
1159
  throttle,
879
1160
  sessionStartedAt: sessionStartedAtRef.current,
880
1161
  quietMode,
881
1162
  seenFeatureIds,
882
1163
  clickedFeatureIds,
883
- triggerContext
1164
+ triggerContext,
1165
+ flagBridge
884
1166
  })
885
1167
  );
886
1168
  }, [
@@ -889,11 +1171,13 @@ function FeatureDropProvider({
889
1171
  userContext,
890
1172
  matchAudienceFn,
891
1173
  appVersion,
1174
+ product,
892
1175
  throttle,
893
1176
  quietMode,
894
1177
  seenFeatureIds,
895
1178
  clickedFeatureIds,
896
- triggerVersion
1179
+ triggerVersion,
1180
+ flagBridge
897
1181
  ]);
898
1182
  useEffect(() => {
899
1183
  recompute();
@@ -1100,6 +1384,15 @@ function FeatureDropProvider({
1100
1384
  variant: event.variant ?? (feature ? getFeatureVariantName(feature) : void 0)
1101
1385
  });
1102
1386
  }, [collector, resolvedManifest]);
1387
+ const reportError = useCallback(
1388
+ (error, context) => {
1389
+ onError?.(error, context);
1390
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
1391
+ console.warn("[featuredrop] component error", context?.component, error);
1392
+ }
1393
+ },
1394
+ [onError]
1395
+ );
1103
1396
  const trackUsageEvent = useCallback((event, delta = 1) => {
1104
1397
  if (!event) return;
1105
1398
  triggerEngineRef.current?.trackUsage(event, delta);
@@ -1163,7 +1456,10 @@ function FeatureDropProvider({
1163
1456
  releaseSpotlightSlot,
1164
1457
  activeSpotlightCount: activeSpotlightIds.size,
1165
1458
  trackAdoptionEvent,
1166
- locale,
1459
+ reportError,
1460
+ locale: resolvedLocale,
1461
+ direction,
1462
+ animation: resolvedAnimation,
1167
1463
  translations,
1168
1464
  trackUsageEvent,
1169
1465
  trackTriggerEvent,
@@ -1194,7 +1490,10 @@ function FeatureDropProvider({
1194
1490
  releaseSpotlightSlot,
1195
1491
  activeSpotlightIds.size,
1196
1492
  trackAdoptionEvent,
1197
- locale,
1493
+ reportError,
1494
+ resolvedLocale,
1495
+ direction,
1496
+ resolvedAnimation,
1198
1497
  translations,
1199
1498
  trackUsageEvent,
1200
1499
  trackTriggerEvent,
@@ -1913,6 +2212,34 @@ function useSurvey(id) {
1913
2212
  [askLater, hide, show, snapshot]
1914
2213
  );
1915
2214
  }
2215
+ var FeatureDropComponentBoundary = class extends Component {
2216
+ state = { hasError: false };
2217
+ componentDidCatch(error, info) {
2218
+ this.setState({ hasError: true });
2219
+ this.props.onError?.(error, info);
2220
+ }
2221
+ render() {
2222
+ if (this.state.hasError) return null;
2223
+ return this.props.children;
2224
+ }
2225
+ };
2226
+ function withFeatureDropBoundary(ComponentImpl, componentName) {
2227
+ function Wrapped(props) {
2228
+ const context = useContext(FeatureDropContext);
2229
+ const onError = useCallback(
2230
+ (error, info) => {
2231
+ context?.reportError(error, {
2232
+ component: componentName,
2233
+ componentStack: info.componentStack ?? void 0
2234
+ });
2235
+ },
2236
+ [context]
2237
+ );
2238
+ return /* @__PURE__ */ jsx(FeatureDropComponentBoundary, { onError, children: /* @__PURE__ */ jsx(ComponentImpl, { ...props }) });
2239
+ }
2240
+ Wrapped.displayName = `FeatureDropBoundary(${componentName})`;
2241
+ return Wrapped;
2242
+ }
1916
2243
  var baseStyles = {
1917
2244
  display: "inline-flex",
1918
2245
  alignItems: "center",
@@ -1963,15 +2290,25 @@ function NewBadge({
1963
2290
  style,
1964
2291
  children
1965
2292
  }) {
2293
+ const context = useContext(FeatureDropContext);
2294
+ const pulsePreset = useMemo(
2295
+ () => resolveAnimationPreset(context?.animation ?? "normal", {
2296
+ reducedMotion: prefersReducedMotion()
2297
+ }),
2298
+ [context?.animation]
2299
+ );
2300
+ const dotPulseAnimation = useMemo(() => getPulseAnimation(pulsePreset, "dot"), [pulsePreset]);
2301
+ useEffect(() => {
2302
+ ensureFeatureDropAnimationStyles();
2303
+ }, []);
1966
2304
  if (children) {
1967
2305
  return /* @__PURE__ */ jsx(Fragment, { children: children({ isNew: show }) });
1968
2306
  }
1969
2307
  if (!show) return null;
1970
2308
  const handleClick = dismissOnClick && onDismiss ? onDismiss : void 0;
1971
- const reduceMotion = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1972
2309
  const variantStyles = variant === "dot" ? {
1973
2310
  ...dotStyles,
1974
- animation: reduceMotion ? "none" : dotStyles.animation
2311
+ animation: dotPulseAnimation ?? "none"
1975
2312
  } : variant === "count" ? countStyles : pillStyles;
1976
2313
  const content = variant === "dot" ? null : variant === "count" ? count ?? 0 : label;
1977
2314
  const ariaLabel = variant === "count" ? `${count ?? 0} new features` : "New feature";
@@ -2524,14 +2861,6 @@ var TYPE_COLORS = {
2524
2861
  fix: { color: "#92400e", bg: "rgba(245, 158, 11, 0.15)" },
2525
2862
  breaking: { color: "#991b1b", bg: "rgba(239, 68, 68, 0.15)" }
2526
2863
  };
2527
- function formatDate(iso) {
2528
- const d = new Date(iso);
2529
- return d.toLocaleDateString(void 0, {
2530
- month: "short",
2531
- day: "numeric",
2532
- year: "numeric"
2533
- });
2534
- }
2535
2864
  function getFocusableElements(container) {
2536
2865
  const nodes = container.querySelectorAll(
2537
2866
  [
@@ -2554,7 +2883,9 @@ function DefaultEntry({
2554
2883
  reactions,
2555
2884
  userReaction,
2556
2885
  canReact,
2557
- react
2886
+ react,
2887
+ locale,
2888
+ dateFormat
2558
2889
  }) {
2559
2890
  const typeStyle = feature.type ? TYPE_COLORS[feature.type] ?? TYPE_COLORS.feature : null;
2560
2891
  const descriptionHtml = feature.description ? parseDescription(feature.description) : null;
@@ -2604,7 +2935,7 @@ function DefaultEntry({
2604
2935
  }
2605
2936
  ),
2606
2937
  feature.category && /* @__PURE__ */ jsx("span", { children: feature.category }),
2607
- /* @__PURE__ */ jsx("span", { children: formatDate(feature.releasedAt) }),
2938
+ /* @__PURE__ */ jsx("span", { children: dateFormat === "relative" ? formatRelativeTimeForLocale(feature.releasedAt, locale) : formatDateForLocale(feature.releasedAt, locale) }),
2608
2939
  versionLabel && /* @__PURE__ */ jsxs("span", { children: [
2609
2940
  "v",
2610
2941
  versionLabel
@@ -2651,6 +2982,7 @@ function ChangelogWidget({
2651
2982
  showMarkAll = true,
2652
2983
  emptyLabel,
2653
2984
  maxHeight = "400px",
2985
+ dateFormat = "absolute",
2654
2986
  analytics,
2655
2987
  className,
2656
2988
  style,
@@ -2670,14 +3002,19 @@ function ChangelogWidget({
2670
3002
  markFeatureSeen,
2671
3003
  markFeatureClicked,
2672
3004
  trackAdoptionEvent,
3005
+ locale,
3006
+ direction,
3007
+ animation,
2673
3008
  translations
2674
3009
  } = useFeatureDrop();
2675
3010
  const [isOpen, setIsOpen] = useState(false);
3011
+ const [isClosing, setIsClosing] = useState(false);
2676
3012
  const [, setReactionVersion] = useState(0);
2677
3013
  const containerRef = useRef(null);
2678
3014
  const dialogRef = useRef(null);
2679
3015
  const triggerRef = useRef(null);
2680
3016
  const lastFocusedElementRef = useRef(null);
3017
+ const closeTimerRef = useRef(null);
2681
3018
  const widgetIdRef = useRef(`featuredrop-widget-${Math.random().toString(36).slice(2, 10)}`);
2682
3019
  const themeVariables = useThemeVariables(theme);
2683
3020
  const resolvedTitle = title ?? translations.whatsNewTitle;
@@ -2686,7 +3023,27 @@ function ChangelogWidget({
2686
3023
  const resolvedEmptyLabel = emptyLabel ?? translations.allCaughtUp;
2687
3024
  const dialogId = `${widgetIdRef.current}-dialog`;
2688
3025
  const titleId = `${widgetIdRef.current}-title`;
3026
+ const countLabel = translations.newFeatureCount(newCount);
3027
+ const dialogEnterAnimation = useMemo(
3028
+ () => getEnterAnimation(animation, variant),
3029
+ [animation, variant]
3030
+ );
3031
+ const dialogExitAnimation = useMemo(
3032
+ () => getExitAnimation(animation, variant),
3033
+ [animation, variant]
3034
+ );
3035
+ useEffect(() => {
3036
+ ensureFeatureDropAnimationStyles();
3037
+ return () => {
3038
+ if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
3039
+ };
3040
+ }, []);
2689
3041
  const open = useCallback(() => {
3042
+ if (closeTimerRef.current) {
3043
+ clearTimeout(closeTimerRef.current);
3044
+ closeTimerRef.current = null;
3045
+ }
3046
+ setIsClosing(false);
2690
3047
  if (typeof document !== "undefined" && document.activeElement instanceof HTMLElement) {
2691
3048
  lastFocusedElementRef.current = document.activeElement;
2692
3049
  }
@@ -2694,7 +3051,21 @@ function ChangelogWidget({
2694
3051
  analytics?.onWidgetOpened?.();
2695
3052
  }, [analytics]);
2696
3053
  const close = useCallback(() => {
3054
+ if (closeTimerRef.current) {
3055
+ clearTimeout(closeTimerRef.current);
3056
+ closeTimerRef.current = null;
3057
+ }
2697
3058
  setIsOpen(false);
3059
+ const exitDuration = getAnimationDurationMs(animation, variant);
3060
+ if (exitDuration > 0) {
3061
+ setIsClosing(true);
3062
+ closeTimerRef.current = setTimeout(() => {
3063
+ setIsClosing(false);
3064
+ closeTimerRef.current = null;
3065
+ }, exitDuration);
3066
+ } else {
3067
+ setIsClosing(false);
3068
+ }
2698
3069
  analytics?.onWidgetClosed?.();
2699
3070
  const returnTarget = triggerRef.current ?? lastFocusedElementRef.current;
2700
3071
  if (returnTarget) {
@@ -2706,7 +3077,7 @@ function ChangelogWidget({
2706
3077
  returnTarget.focus();
2707
3078
  }
2708
3079
  }
2709
- }, [analytics]);
3080
+ }, [animation, analytics, variant]);
2710
3081
  const toggle = useCallback(() => {
2711
3082
  if (isOpen) {
2712
3083
  close();
@@ -2843,6 +3214,7 @@ function ChangelogWidget({
2843
3214
  "data-featuredrop-widget": true,
2844
3215
  className,
2845
3216
  style: widgetRootStyle,
3217
+ dir: direction,
2846
3218
  children: [
2847
3219
  renderTrigger ? renderTrigger({ count: newCount, onClick: toggle }) : /* @__PURE__ */ jsxs(
2848
3220
  "button",
@@ -2851,7 +3223,7 @@ function ChangelogWidget({
2851
3223
  onClick: toggle,
2852
3224
  style: triggerButtonStyles,
2853
3225
  "data-featuredrop-trigger": true,
2854
- "aria-label": `${resolvedTriggerLabel}${newCount > 0 ? ` \u2014 ${newCount} new` : ""}`,
3226
+ "aria-label": `${resolvedTriggerLabel}${newCount > 0 ? ` \u2014 ${countLabel}` : ""}`,
2855
3227
  "aria-haspopup": "dialog",
2856
3228
  "aria-expanded": isOpen,
2857
3229
  "aria-controls": dialogId,
@@ -2861,8 +3233,8 @@ function ChangelogWidget({
2861
3233
  ]
2862
3234
  }
2863
3235
  ),
2864
- /* @__PURE__ */ jsx("span", { style: srOnlyStyles, "aria-live": "polite", "aria-atomic": "true", children: newCount > 0 ? `${newCount} new feature${newCount === 1 ? "" : "s"}` : "No new features" }),
2865
- isOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
3236
+ /* @__PURE__ */ jsx("span", { style: srOnlyStyles, "aria-live": "polite", "aria-atomic": "true", children: countLabel }),
3237
+ (isOpen || isClosing) && /* @__PURE__ */ jsxs(Fragment, { children: [
2866
3238
  variant !== "popover" && /* @__PURE__ */ jsx(
2867
3239
  "div",
2868
3240
  {
@@ -2877,7 +3249,10 @@ function ChangelogWidget({
2877
3249
  {
2878
3250
  id: dialogId,
2879
3251
  ref: dialogRef,
2880
- style: dialogContainerStyles,
3252
+ style: {
3253
+ ...dialogContainerStyles,
3254
+ animation: isClosing ? dialogExitAnimation : dialogEnterAnimation
3255
+ },
2881
3256
  "data-featuredrop-container": variant,
2882
3257
  role: "dialog",
2883
3258
  "aria-labelledby": titleId,
@@ -2916,6 +3291,8 @@ function ChangelogWidget({
2916
3291
  feature,
2917
3292
  dismiss: () => handleDismiss(feature.id),
2918
3293
  onFeatureClick: () => handleFeatureClick(feature),
3294
+ locale,
3295
+ dateFormat,
2919
3296
  reactions: showReactions ? getReactionCounts(feature.id, reactions) : void 0,
2920
3297
  userReaction: showReactions ? getUserReaction(feature.id) : null,
2921
3298
  canReact: showReactions ? !getUserReaction(feature.id) : void 0,
@@ -2953,8 +3330,7 @@ var beaconPulseStyles = {
2953
3330
  inset: "-4px",
2954
3331
  borderRadius: "50%",
2955
3332
  border: "2px solid var(--featuredrop-beacon-color, #f59e0b)",
2956
- opacity: 0.6,
2957
- animation: "featuredrop-spotlight-pulse 2s ease-in-out infinite"
3333
+ opacity: 0.6
2958
3334
  };
2959
3335
  var tooltipStyles = {
2960
3336
  position: "absolute",
@@ -2990,29 +3366,6 @@ var tooltipDismissStyles = {
2990
3366
  backgroundColor: "var(--featuredrop-tooltip-dismiss-bg, #f3f4f6)",
2991
3367
  color: "var(--featuredrop-tooltip-dismiss-color, #374151)"
2992
3368
  };
2993
- var injectedKeyframes = false;
2994
- function injectKeyframes() {
2995
- if (injectedKeyframes || typeof document === "undefined") return;
2996
- injectedKeyframes = true;
2997
- const style = document.createElement("style");
2998
- style.textContent = `
2999
- @keyframes featuredrop-spotlight-pulse {
3000
- 0%, 100% { transform: scale(1); opacity: 0.6; }
3001
- 50% { transform: scale(1.6); opacity: 0; }
3002
- }
3003
- @keyframes featuredrop-pulse {
3004
- 0%, 100% { opacity: 1; }
3005
- 50% { opacity: 0.5; }
3006
- }
3007
- `;
3008
- document.head.appendChild(style);
3009
- }
3010
- function prefersReducedMotion() {
3011
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
3012
- return false;
3013
- }
3014
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3015
- }
3016
3369
  function Spotlight({
3017
3370
  featureId,
3018
3371
  targetRef,
@@ -3032,7 +3385,9 @@ function Spotlight({
3032
3385
  markFeatureSeen,
3033
3386
  acquireSpotlightSlot,
3034
3387
  releaseSpotlightSlot,
3035
- activeSpotlightCount
3388
+ activeSpotlightCount,
3389
+ animation,
3390
+ translations
3036
3391
  } = useFeatureDrop();
3037
3392
  const feature = newFeatures.find((f) => f.id === featureId);
3038
3393
  const [isTooltipOpen, setTooltipOpen] = useState(false);
@@ -3041,13 +3396,20 @@ function Spotlight({
3041
3396
  const timerRef = useRef(null);
3042
3397
  const beaconRef = useRef(null);
3043
3398
  const instanceIdRef = useRef(`featuredrop-spotlight-${Math.random().toString(36).slice(2, 10)}`);
3044
- const reduceMotion = useMemo(() => prefersReducedMotion(), []);
3399
+ const beaconPulseAnimation = useMemo(
3400
+ () => getPulseAnimation(animation, "beacon"),
3401
+ [animation]
3402
+ );
3403
+ const tooltipEnterAnimation = useMemo(
3404
+ () => getEnterAnimation(animation, "popover"),
3405
+ [animation]
3406
+ );
3045
3407
  const isActive = !!feature;
3046
3408
  const tooltipId = `${instanceIdRef.current}-tooltip`;
3047
3409
  const tooltipTitleId = `${instanceIdRef.current}-title`;
3048
3410
  const tooltipDescId = `${instanceIdRef.current}-description`;
3049
3411
  useEffect(() => {
3050
- injectKeyframes();
3412
+ ensureFeatureDropAnimationStyles();
3051
3413
  }, []);
3052
3414
  useEffect(() => {
3053
3415
  if (!isActive) {
@@ -3204,7 +3566,7 @@ function Spotlight({
3204
3566
  {
3205
3567
  style: {
3206
3568
  ...beaconPulseStyles,
3207
- animation: reduceMotion ? "none" : beaconPulseStyles.animation
3569
+ animation: beaconPulseAnimation ?? "none"
3208
3570
  }
3209
3571
  }
3210
3572
  )
@@ -3214,7 +3576,12 @@ function Spotlight({
3214
3576
  "div",
3215
3577
  {
3216
3578
  id: tooltipId,
3217
- style: { ...tooltipStyles, position: "fixed", ...tooltipPosition },
3579
+ style: {
3580
+ ...tooltipStyles,
3581
+ position: "fixed",
3582
+ ...tooltipPosition,
3583
+ animation: tooltipEnterAnimation
3584
+ },
3218
3585
  "data-featuredrop-tooltip": true,
3219
3586
  role: "dialog",
3220
3587
  "aria-modal": "false",
@@ -3236,7 +3603,7 @@ function Spotlight({
3236
3603
  type: "button",
3237
3604
  onClick: handleDismiss,
3238
3605
  style: tooltipDismissStyles,
3239
- children: "Got it"
3606
+ children: translations.gotIt
3240
3607
  }
3241
3608
  )
3242
3609
  ] })
@@ -3437,8 +3804,7 @@ var toastStyles = {
3437
3804
  border: "1px solid var(--featuredrop-toast-border, #e5e7eb)",
3438
3805
  width: "var(--featuredrop-toast-width, 340px)",
3439
3806
  maxWidth: "calc(100vw - 32px)",
3440
- fontFamily: "var(--featuredrop-font-family, inherit)",
3441
- animation: "featuredrop-toast-enter 0.3s ease-out"
3807
+ fontFamily: "var(--featuredrop-font-family, inherit)"
3442
3808
  };
3443
3809
  var toastBodyStyles = {
3444
3810
  flex: 1,
@@ -3485,25 +3851,6 @@ var TYPE_INDICATORS = {
3485
3851
  fix: { color: "#f59e0b", icon: "\u{1F527}" },
3486
3852
  breaking: { color: "#ef4444", icon: "\u26A0\uFE0F" }
3487
3853
  };
3488
- var injectedToastKeyframes = false;
3489
- function injectToastKeyframes() {
3490
- if (injectedToastKeyframes || typeof document === "undefined") return;
3491
- injectedToastKeyframes = true;
3492
- const style = document.createElement("style");
3493
- style.textContent = `
3494
- @keyframes featuredrop-toast-enter {
3495
- from { opacity: 0; transform: translateY(8px) scale(0.96); }
3496
- to { opacity: 1; transform: translateY(0) scale(1); }
3497
- }
3498
- `;
3499
- document.head.appendChild(style);
3500
- }
3501
- function prefersReducedMotion2() {
3502
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
3503
- return false;
3504
- }
3505
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3506
- }
3507
3854
  function Toast({
3508
3855
  featureIds,
3509
3856
  maxVisible = 3,
@@ -3522,14 +3869,21 @@ function Toast({
3522
3869
  markFeatureClicked,
3523
3870
  trackAdoptionEvent,
3524
3871
  getRemainingToastSlots,
3525
- markToastsShown
3872
+ markToastsShown,
3873
+ animation
3526
3874
  } = useFeatureDrop();
3527
3875
  const [localDismissed, setLocalDismissed] = useState(/* @__PURE__ */ new Set());
3876
+ const [closingIds, setClosingIds] = useState(/* @__PURE__ */ new Set());
3528
3877
  const timersRef = useRef(/* @__PURE__ */ new Map());
3529
3878
  const shownIdsRef = useRef(/* @__PURE__ */ new Set());
3530
- const reduceMotion = useMemo(() => prefersReducedMotion2(), []);
3879
+ const toastEnterAnimation = useMemo(() => getEnterAnimation(animation, "toast"), [animation]);
3880
+ const toastExitAnimation = useMemo(() => getExitAnimation(animation, "toast"), [animation]);
3881
+ const toastExitDuration = useMemo(
3882
+ () => getAnimationDurationMs(animation, "toast"),
3883
+ [animation]
3884
+ );
3531
3885
  useEffect(() => {
3532
- injectToastKeyframes();
3886
+ ensureFeatureDropAnimationStyles();
3533
3887
  }, []);
3534
3888
  const toastFeatures = newFeaturesSorted.filter((f) => {
3535
3889
  if (localDismissed.has(f.id)) return false;
@@ -3549,8 +3903,14 @@ function Toast({
3549
3903
  return true;
3550
3904
  });
3551
3905
  }, [baseVisible, remainingSlots]);
3552
- const handleDismiss = useCallback(
3906
+ const finalizeDismiss = useCallback(
3553
3907
  (id) => {
3908
+ setClosingIds((previous) => {
3909
+ if (!previous.has(id)) return previous;
3910
+ const next = new Set(previous);
3911
+ next.delete(id);
3912
+ return next;
3913
+ });
3554
3914
  setLocalDismissed((prev) => new Set(prev).add(id));
3555
3915
  dismiss(id);
3556
3916
  const feature = newFeaturesSorted.find((f) => f.id === id);
@@ -3560,9 +3920,40 @@ function Toast({
3560
3920
  clearTimeout(timer);
3561
3921
  timersRef.current.delete(id);
3562
3922
  }
3923
+ const exitTimer = timersRef.current.get(`exit:${id}`);
3924
+ if (exitTimer) {
3925
+ clearTimeout(exitTimer);
3926
+ timersRef.current.delete(`exit:${id}`);
3927
+ }
3563
3928
  },
3564
3929
  [dismiss, newFeaturesSorted, analytics]
3565
3930
  );
3931
+ const handleDismiss = useCallback((id) => {
3932
+ if (closingIds.has(id)) return;
3933
+ if (toastExitDuration > 0 && toastExitAnimation) {
3934
+ const existingAutoTimer = timersRef.current.get(id);
3935
+ if (existingAutoTimer) {
3936
+ clearTimeout(existingAutoTimer);
3937
+ timersRef.current.delete(id);
3938
+ }
3939
+ setClosingIds((previous) => {
3940
+ if (previous.has(id)) return previous;
3941
+ const next = new Set(previous);
3942
+ next.add(id);
3943
+ return next;
3944
+ });
3945
+ const existing = timersRef.current.get(`exit:${id}`);
3946
+ if (existing) clearTimeout(existing);
3947
+ timersRef.current.set(
3948
+ `exit:${id}`,
3949
+ setTimeout(() => {
3950
+ finalizeDismiss(id);
3951
+ }, toastExitDuration)
3952
+ );
3953
+ return;
3954
+ }
3955
+ finalizeDismiss(id);
3956
+ }, [closingIds, finalizeDismiss, toastExitAnimation, toastExitDuration]);
3566
3957
  const handleDismissAllLocal = useCallback(() => {
3567
3958
  for (const f of visibleToasts) {
3568
3959
  handleDismiss(f.id);
@@ -3571,14 +3962,14 @@ function Toast({
3571
3962
  useEffect(() => {
3572
3963
  if (autoDismissMs <= 0) return;
3573
3964
  for (const f of visibleToasts) {
3574
- if (!timersRef.current.has(f.id)) {
3965
+ if (!timersRef.current.has(f.id) && !closingIds.has(f.id)) {
3575
3966
  timersRef.current.set(
3576
3967
  f.id,
3577
3968
  setTimeout(() => handleDismiss(f.id), autoDismissMs)
3578
3969
  );
3579
3970
  }
3580
3971
  }
3581
- }, [visibleToasts, autoDismissMs, handleDismiss]);
3972
+ }, [visibleToasts, autoDismissMs, closingIds, handleDismiss]);
3582
3973
  useEffect(() => {
3583
3974
  const newlyShownIds = [];
3584
3975
  for (const feature of visibleToasts) {
@@ -3634,7 +4025,7 @@ function Toast({
3634
4025
  "data-featuredrop-toast": feature.id,
3635
4026
  style: {
3636
4027
  ...toastStyles,
3637
- animation: reduceMotion ? "none" : toastStyles.animation
4028
+ animation: closingIds.has(feature.id) ? toastExitAnimation : toastEnterAnimation
3638
4029
  },
3639
4030
  role: "status",
3640
4031
  children: [
@@ -3773,17 +4164,12 @@ var reactionButtonStyles2 = {
3773
4164
  cursor: "pointer",
3774
4165
  fontSize: "12px"
3775
4166
  };
3776
- function defaultFormatDate(date) {
3777
- const d = new Date(date);
3778
- return d.toLocaleDateString(void 0, {
3779
- month: "short",
3780
- day: "numeric",
3781
- year: "numeric"
3782
- });
4167
+ function defaultFormatDate(date, locale) {
4168
+ return formatDateForLocale(date, locale);
3783
4169
  }
3784
4170
  function EntryCard({
3785
4171
  entry,
3786
- formatDate: formatDate2,
4172
+ formatDate,
3787
4173
  basePath,
3788
4174
  shareLabel,
3789
4175
  showReactions,
@@ -3839,7 +4225,7 @@ function EntryCard({
3839
4225
  )
3840
4226
  ] }),
3841
4227
  /* @__PURE__ */ jsxs("div", { style: metaRowStyles, children: [
3842
- /* @__PURE__ */ jsx("span", { children: formatDate2(entry.releasedAt) }),
4228
+ /* @__PURE__ */ jsx("span", { children: formatDate(entry.releasedAt) }),
3843
4229
  entry.version && /* @__PURE__ */ jsxs("span", { children: [
3844
4230
  "v",
3845
4231
  typeof entry.version === "string" ? entry.version : entry.version.introduced ?? entry.version.showNewUntil ?? ""
@@ -3906,7 +4292,8 @@ function ChangelogPage({
3906
4292
  categories,
3907
4293
  emptyState,
3908
4294
  renderEntry,
3909
- formatDate: formatDate2 = defaultFormatDate,
4295
+ formatDate,
4296
+ dateFormat = "absolute",
3910
4297
  basePath,
3911
4298
  manifest: manifestProp,
3912
4299
  className,
@@ -3916,7 +4303,7 @@ function ChangelogPage({
3916
4303
  reactions = [...DEFAULT_REACTIONS],
3917
4304
  onReaction
3918
4305
  }) {
3919
- const { manifest: contextManifest, translations } = useFeatureDrop();
4306
+ const { manifest: contextManifest, locale, direction, translations } = useFeatureDrop();
3920
4307
  const themeVariables = useThemeVariables(theme);
3921
4308
  const manifest = useMemo(
3922
4309
  () => manifestProp ?? contextManifest ?? [],
@@ -3980,6 +4367,10 @@ function ChangelogPage({
3980
4367
  if (typeof window !== "undefined") return window.location.pathname;
3981
4368
  return "";
3982
4369
  }, [basePath]);
4370
+ const resolvedFormatDate = useMemo(
4371
+ () => formatDate ?? ((value) => dateFormat === "relative" ? formatRelativeTimeForLocale(value, locale) : defaultFormatDate(value, locale)),
4372
+ [dateFormat, formatDate, locale]
4373
+ );
3983
4374
  useEffect(() => {
3984
4375
  if (typeof window === "undefined") return;
3985
4376
  const hash = window.location.hash;
@@ -4013,7 +4404,7 @@ function ChangelogPage({
4013
4404
  EntryCard,
4014
4405
  {
4015
4406
  entry,
4016
- formatDate: formatDate2,
4407
+ formatDate: resolvedFormatDate,
4017
4408
  basePath: resolvedBasePath,
4018
4409
  showReactions,
4019
4410
  shareLabel: translations.share,
@@ -4058,60 +4449,69 @@ function ChangelogPage({
4058
4449
  () => ({ ...themeVariables ?? {}, ...pageStyles, ...style ?? {} }),
4059
4450
  [themeVariables, style]
4060
4451
  );
4061
- return /* @__PURE__ */ jsxs("div", { "data-featuredrop-changelog-page": true, className, style: containerStyles2, children: [
4062
- /* @__PURE__ */ jsx("a", { href: `#${listId}`, style: skipLinkStyles, children: translations.skipToEntries }),
4063
- /* @__PURE__ */ jsxs("div", { style: headerStyles2, children: [
4064
- /* @__PURE__ */ jsx("h2", { style: { margin: 0 }, children: translations.changelogTitle }),
4065
- showSearch && /* @__PURE__ */ jsx(
4066
- "input",
4067
- {
4068
- style: searchStyles,
4069
- type: "search",
4070
- placeholder: translations.searchPlaceholder,
4071
- value: query,
4072
- onChange: (e) => setQuery(e.target.value),
4073
- "aria-label": "Search changelog updates"
4074
- }
4075
- )
4076
- ] }),
4077
- showFilters && /* @__PURE__ */ jsxs("div", { style: filterBarStyles, children: [
4078
- allCategories.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
4079
- /* @__PURE__ */ jsx(
4080
- "button",
4081
- {
4082
- type: "button",
4083
- style: activeCategory ? pillStyles2 : pillActiveStyles,
4084
- onClick: () => setActiveCategory(null),
4085
- children: translations.allCategories
4086
- }
4087
- ),
4088
- allCategories.map((cat) => /* @__PURE__ */ jsx(
4089
- "button",
4090
- {
4091
- type: "button",
4092
- style: activeCategory === cat ? pillActiveStyles : pillStyles2,
4093
- onClick: () => setActiveCategory(cat),
4094
- children: cat
4095
- },
4096
- cat
4097
- ))
4098
- ] }),
4099
- ["feature", "improvement", "fix", "breaking"].map((t) => /* @__PURE__ */ jsx(
4100
- "button",
4101
- {
4102
- type: "button",
4103
- style: activeType === t ? pillActiveStyles : pillStyles2,
4104
- onClick: () => setActiveType((prev) => prev === t ? null : t),
4105
- children: t
4106
- },
4107
- t
4108
- ))
4109
- ] }),
4110
- visible.length === 0 ? /* @__PURE__ */ jsx("div", { style: { padding: "32px 0", textAlign: "center", color: "#6b7280" }, children: emptyState ?? translations.noUpdatesYet }) : /* @__PURE__ */ jsxs("div", { id: listId, onKeyDown: handleListKeyDown, children: [
4111
- renderList(),
4112
- paginationControls()
4113
- ] })
4114
- ] });
4452
+ return /* @__PURE__ */ jsxs(
4453
+ "div",
4454
+ {
4455
+ "data-featuredrop-changelog-page": true,
4456
+ className,
4457
+ style: containerStyles2,
4458
+ dir: direction,
4459
+ children: [
4460
+ /* @__PURE__ */ jsx("a", { href: `#${listId}`, style: skipLinkStyles, children: translations.skipToEntries }),
4461
+ /* @__PURE__ */ jsxs("div", { style: headerStyles2, children: [
4462
+ /* @__PURE__ */ jsx("h2", { style: { margin: 0 }, children: translations.changelogTitle }),
4463
+ showSearch && /* @__PURE__ */ jsx(
4464
+ "input",
4465
+ {
4466
+ style: searchStyles,
4467
+ type: "search",
4468
+ placeholder: translations.searchPlaceholder,
4469
+ value: query,
4470
+ onChange: (e) => setQuery(e.target.value),
4471
+ "aria-label": "Search changelog updates"
4472
+ }
4473
+ )
4474
+ ] }),
4475
+ showFilters && /* @__PURE__ */ jsxs("div", { style: filterBarStyles, children: [
4476
+ allCategories.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
4477
+ /* @__PURE__ */ jsx(
4478
+ "button",
4479
+ {
4480
+ type: "button",
4481
+ style: activeCategory ? pillStyles2 : pillActiveStyles,
4482
+ onClick: () => setActiveCategory(null),
4483
+ children: translations.allCategories
4484
+ }
4485
+ ),
4486
+ allCategories.map((cat) => /* @__PURE__ */ jsx(
4487
+ "button",
4488
+ {
4489
+ type: "button",
4490
+ style: activeCategory === cat ? pillActiveStyles : pillStyles2,
4491
+ onClick: () => setActiveCategory(cat),
4492
+ children: cat
4493
+ },
4494
+ cat
4495
+ ))
4496
+ ] }),
4497
+ ["feature", "improvement", "fix", "breaking"].map((t) => /* @__PURE__ */ jsx(
4498
+ "button",
4499
+ {
4500
+ type: "button",
4501
+ style: activeType === t ? pillActiveStyles : pillStyles2,
4502
+ onClick: () => setActiveType((prev) => prev === t ? null : t),
4503
+ children: t
4504
+ },
4505
+ t
4506
+ ))
4507
+ ] }),
4508
+ visible.length === 0 ? /* @__PURE__ */ jsx("div", { style: { padding: "32px 0", textAlign: "center", color: "#6b7280" }, children: emptyState ?? translations.noUpdatesYet }) : /* @__PURE__ */ jsxs("div", { id: listId, onKeyDown: handleListKeyDown, children: [
4509
+ renderList(),
4510
+ paginationControls()
4511
+ ] })
4512
+ ]
4513
+ }
4514
+ );
4115
4515
  }
4116
4516
  var tourBoxStyles = {
4117
4517
  position: "fixed",
@@ -4234,6 +4634,10 @@ function Tour({
4234
4634
  children
4235
4635
  }) {
4236
4636
  const context = useContext(FeatureDropContext);
4637
+ const dialogAnimation = useMemo(
4638
+ () => getEnterAnimation(context?.animation ?? "normal", "popover"),
4639
+ [context?.animation]
4640
+ );
4237
4641
  const [isActive, setIsActive] = useState(false);
4238
4642
  const [stepIndex, setStepIndex] = useState(-1);
4239
4643
  const [targetRect, setTargetRect] = useState(null);
@@ -4346,6 +4750,9 @@ function Tour({
4346
4750
  void openStep(next);
4347
4751
  context?.markTourShown();
4348
4752
  }, [context, findValidStep, id, openStep, persistence]);
4753
+ useEffect(() => {
4754
+ ensureFeatureDropAnimationStyles();
4755
+ }, []);
4349
4756
  useEffect(() => {
4350
4757
  if (!isActive || !step) return;
4351
4758
  const updateRect = () => {
@@ -4499,9 +4906,11 @@ function Tour({
4499
4906
  "aria-labelledby": titleId,
4500
4907
  "aria-describedby": showProgress ? `${contentId} ${progressId}` : contentId,
4501
4908
  "data-featuredrop-tour": id,
4909
+ dir: context?.direction,
4502
4910
  tabIndex: -1,
4503
4911
  style: {
4504
4912
  ...tourBoxStyles,
4913
+ animation: dialogAnimation,
4505
4914
  top: tooltipPos.top,
4506
4915
  left: tooltipPos.left
4507
4916
  },
@@ -4959,6 +5368,7 @@ function Hotspot({
4959
5368
  frequency = "once",
4960
5369
  children
4961
5370
  }) {
5371
+ const featureDrop = useContext(FeatureDropContext);
4962
5372
  const group = useContext(TooltipGroupContext);
4963
5373
  const [open, setOpen] = useState(false);
4964
5374
  const [dismissed, setDismissed] = useState(false);
@@ -4966,6 +5376,23 @@ function Hotspot({
4966
5376
  const hoverTimer = useRef(null);
4967
5377
  const beaconRef = useRef(null);
4968
5378
  const instanceIdRef = useRef(`featuredrop-hotspot-${Math.random().toString(36).slice(2, 10)}`);
5379
+ const animation = useMemo(
5380
+ () => resolveAnimationPreset(featureDrop?.animation ?? "normal", {
5381
+ reducedMotion: prefersReducedMotion()
5382
+ }),
5383
+ [featureDrop?.animation]
5384
+ );
5385
+ const beaconPulseAnimation = useMemo(
5386
+ () => getPulseAnimation(animation, "dot"),
5387
+ [animation]
5388
+ );
5389
+ const tooltipEnterAnimation = useMemo(
5390
+ () => getEnterAnimation(animation, "popover"),
5391
+ [animation]
5392
+ );
5393
+ useEffect(() => {
5394
+ ensureFeatureDropAnimationStyles();
5395
+ }, []);
4969
5396
  useEffect(() => {
4970
5397
  if (frequency === "once" && isDismissedOnce(id)) {
4971
5398
  setDismissed(true);
@@ -5074,6 +5501,7 @@ function Hotspot({
5074
5501
  boxShadow: "0 0 0 2px rgba(17,24,39,0.1)",
5075
5502
  cursor: "pointer",
5076
5503
  zIndex: "var(--featuredrop-hotspot-beacon-z-index, 10000)",
5504
+ animation: beaconPulseAnimation ?? "none",
5077
5505
  ...beaconTypeStyles[type]
5078
5506
  }
5079
5507
  }
@@ -5085,7 +5513,12 @@ function Hotspot({
5085
5513
  role: "dialog",
5086
5514
  "aria-modal": "false",
5087
5515
  "data-featuredrop-hotspot-tooltip": id,
5088
- style: { ...tooltipStyles2, top: tooltipTop, left: tooltipLeft },
5516
+ style: {
5517
+ ...tooltipStyles2,
5518
+ top: tooltipTop,
5519
+ left: tooltipLeft,
5520
+ animation: tooltipEnterAnimation
5521
+ },
5089
5522
  onMouseLeave: closeTooltip,
5090
5523
  children: [
5091
5524
  /* @__PURE__ */ jsx("div", { style: { fontSize: "13px", color: "#374151", lineHeight: 1.5 }, children }),
@@ -5171,7 +5604,7 @@ function FeedbackWidget({
5171
5604
  style,
5172
5605
  children
5173
5606
  }) {
5174
- const { trackAdoptionEvent, translations } = useFeatureDrop();
5607
+ const { trackAdoptionEvent, direction, animation, translations } = useFeatureDrop();
5175
5608
  const [isOpen, setIsOpen] = useState(false);
5176
5609
  const [text, setText] = useState("");
5177
5610
  const [category, setCategory] = useState(categories[0] ?? "other");
@@ -5185,6 +5618,7 @@ function FeedbackWidget({
5185
5618
  const lastFocusedElementRef = useRef(null);
5186
5619
  const instanceIdRef = useRef(`featuredrop-feedback-${Math.random().toString(36).slice(2, 10)}`);
5187
5620
  const rateLimitKey = useMemo(() => getRateLimitKey(featureId), [featureId]);
5621
+ const panelAnimation = useMemo(() => getEnterAnimation(animation, "popover"), [animation]);
5188
5622
  const resolvedTriggerLabel = triggerLabel ?? translations.feedbackTrigger;
5189
5623
  const resolvedTitle = title ?? translations.feedbackTitle;
5190
5624
  const isRateLimited = useMemo(() => {
@@ -5221,6 +5655,9 @@ function FeedbackWidget({
5221
5655
  window.removeEventListener("keydown", onKeyDown);
5222
5656
  };
5223
5657
  }, [close, isOpen]);
5658
+ useEffect(() => {
5659
+ ensureFeatureDropAnimationStyles();
5660
+ }, []);
5224
5661
  const captureScreenshot = useCallback(async () => {
5225
5662
  if (!showScreenshot) return;
5226
5663
  try {
@@ -5340,10 +5777,11 @@ function FeedbackWidget({
5340
5777
  id: `${instanceIdRef.current}-panel`,
5341
5778
  "data-featuredrop-feedback-widget": true,
5342
5779
  className,
5343
- style: { ...panelStyles, ...style },
5780
+ style: { ...panelStyles, animation: panelAnimation, ...style },
5344
5781
  role: "dialog",
5345
5782
  "aria-modal": "false",
5346
5783
  "aria-labelledby": titleId,
5784
+ dir: direction,
5347
5785
  children: [
5348
5786
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: "8px" }, children: [
5349
5787
  /* @__PURE__ */ jsx("strong", { id: titleId, children: resolvedTitle }),
@@ -5353,7 +5791,7 @@ function FeedbackWidget({
5353
5791
  type: "button",
5354
5792
  onClick: close,
5355
5793
  style: { border: "none", background: "transparent", cursor: "pointer" },
5356
- "aria-label": "Close feedback",
5794
+ "aria-label": translations.close,
5357
5795
  children: "x"
5358
5796
  }
5359
5797
  )
@@ -5585,6 +6023,7 @@ function AnnouncementModal({
5585
6023
  }) {
5586
6024
  const context = useContext(FeatureDropContext);
5587
6025
  const [isOpen, setIsOpen] = useState(defaultOpen);
6026
+ const [isClosing, setIsClosing] = useState(false);
5588
6027
  const [slideIndex, setSlideIndex] = useState(0);
5589
6028
  const [blockedByFrequency, setBlockedByFrequency] = useState(false);
5590
6029
  const [isMobile, setIsMobile] = useState(
@@ -5592,6 +6031,7 @@ function AnnouncementModal({
5592
6031
  );
5593
6032
  const modalRef = useRef(null);
5594
6033
  const lastFocusedElementRef = useRef(null);
6034
+ const closeTimerRef = useRef(null);
5595
6035
  const instanceIdRef = useRef(`featuredrop-announcement-${Math.random().toString(36).slice(2, 10)}`);
5596
6036
  const resolvedFeature = useMemo(() => {
5597
6037
  if (feature) return feature;
@@ -5607,8 +6047,15 @@ function AnnouncementModal({
5607
6047
  );
5608
6048
  const currentSlide = resolvedSlides[slideIndex] ?? null;
5609
6049
  const t = context?.translations;
6050
+ const enterAnimation = getEnterAnimation(context?.animation ?? "normal", "modal");
6051
+ const exitAnimation = getExitAnimation(context?.animation ?? "normal", "modal");
5610
6052
  const open = useCallback(() => {
5611
6053
  if (blockedByFrequency || resolvedSlides.length === 0) return;
6054
+ if (closeTimerRef.current) {
6055
+ clearTimeout(closeTimerRef.current);
6056
+ closeTimerRef.current = null;
6057
+ }
6058
+ setIsClosing(false);
5612
6059
  if (typeof document !== "undefined" && document.activeElement instanceof HTMLElement) {
5613
6060
  lastFocusedElementRef.current = document.activeElement;
5614
6061
  }
@@ -5631,7 +6078,22 @@ function AnnouncementModal({
5631
6078
  open();
5632
6079
  }, [defaultOpen, open]);
5633
6080
  const close = useCallback(() => {
6081
+ if (closeTimerRef.current) {
6082
+ clearTimeout(closeTimerRef.current);
6083
+ closeTimerRef.current = null;
6084
+ }
5634
6085
  setIsOpen(false);
6086
+ const animationPreset = context?.animation ?? "normal";
6087
+ const exitDuration = getAnimationDurationMs(animationPreset, "modal");
6088
+ if (exitDuration > 0) {
6089
+ setIsClosing(true);
6090
+ closeTimerRef.current = setTimeout(() => {
6091
+ setIsClosing(false);
6092
+ closeTimerRef.current = null;
6093
+ }, exitDuration);
6094
+ } else {
6095
+ setIsClosing(false);
6096
+ }
5635
6097
  const returnTarget = lastFocusedElementRef.current;
5636
6098
  if (returnTarget) {
5637
6099
  if (typeof requestAnimationFrame === "function") {
@@ -5640,10 +6102,10 @@ function AnnouncementModal({
5640
6102
  returnTarget.focus();
5641
6103
  }
5642
6104
  }
5643
- }, []);
6105
+ }, [context?.animation]);
5644
6106
  const dismiss = useCallback(() => {
5645
- close();
5646
6107
  setBlockedByFrequency(true);
6108
+ close();
5647
6109
  if (frequency === "once") {
5648
6110
  setDismissedOnce2(modalId);
5649
6111
  } else if (frequency === "every-session") {
@@ -5676,12 +6138,17 @@ function AnnouncementModal({
5676
6138
  }, [frequency, modalId]);
5677
6139
  useEffect(() => {
5678
6140
  if (trigger !== "auto") return;
5679
- if (blockedByFrequency || isOpen || resolvedSlides.length === 0) return;
6141
+ if (blockedByFrequency || isOpen || isClosing || resolvedSlides.length === 0) return;
6142
+ if (frequency === "once" && isDismissedOnce2(modalId)) return;
6143
+ if (frequency === "every-session" && sessionDismissed2.has(modalId)) return;
5680
6144
  if (resolvedFeature && resolvedFeature.priority !== "critical") return;
5681
6145
  open();
5682
6146
  }, [
6147
+ frequency,
5683
6148
  blockedByFrequency,
6149
+ isClosing,
5684
6150
  isOpen,
6151
+ modalId,
5685
6152
  open,
5686
6153
  resolvedFeature,
5687
6154
  resolvedSlides.length,
@@ -5698,6 +6165,12 @@ function AnnouncementModal({
5698
6165
  window.removeEventListener("resize", updateViewportMode);
5699
6166
  };
5700
6167
  }, [mobileBreakpoint]);
6168
+ useEffect(() => {
6169
+ ensureFeatureDropAnimationStyles();
6170
+ return () => {
6171
+ if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
6172
+ };
6173
+ }, []);
5701
6174
  useEffect(() => {
5702
6175
  if (!isOpen) return;
5703
6176
  const modalEl = modalRef.current;
@@ -5765,7 +6238,7 @@ function AnnouncementModal({
5765
6238
  if (children) {
5766
6239
  return /* @__PURE__ */ jsx(Fragment, { children: children(renderProps) });
5767
6240
  }
5768
- if (!isOpen || !currentSlide || blockedByFrequency) return null;
6241
+ if (!(isOpen || isClosing) || !currentSlide) return null;
5769
6242
  const embeddedVideo = toEmbedUrl(currentSlide.videoUrl);
5770
6243
  const descriptionHtml = currentSlide.description ? parseDescription(currentSlide.description) : null;
5771
6244
  const totalSlides = resolvedSlides.length;
@@ -5791,9 +6264,11 @@ function AnnouncementModal({
5791
6264
  "aria-describedby": currentSlide.description ? descriptionId : void 0,
5792
6265
  "data-featuredrop-announcement-modal": modalId,
5793
6266
  className,
6267
+ dir: context?.direction,
5794
6268
  tabIndex: -1,
5795
6269
  style: {
5796
6270
  ...isMobile ? mobileModalStyles : desktopModalStyles,
6271
+ animation: isClosing ? exitAnimation : enterAnimation,
5797
6272
  ...style
5798
6273
  },
5799
6274
  children: /* @__PURE__ */ jsxs("div", { style: { padding: isMobile ? "16px" : "20px" }, children: [
@@ -5963,8 +6438,7 @@ var beaconStyles2 = {
5963
6438
  border: "2px solid #fff",
5964
6439
  background: "#f59e0b",
5965
6440
  boxShadow: "0 0 0 2px rgba(17,24,39,0.12)",
5966
- zIndex: "var(--featuredrop-spotlight-chain-beacon-z-index, 10010)",
5967
- animation: "featuredrop-chain-pulse 1.8s ease-in-out infinite"
6441
+ zIndex: "var(--featuredrop-spotlight-chain-beacon-z-index, 10010)"
5968
6442
  };
5969
6443
  var tooltipStyles3 = {
5970
6444
  position: "fixed",
@@ -5976,25 +6450,6 @@ var tooltipStyles3 = {
5976
6450
  boxShadow: "0 10px 30px rgba(0,0,0,0.16)",
5977
6451
  width: "min(90vw, 320px)"
5978
6452
  };
5979
- var injectedKeyframes2 = false;
5980
- function injectKeyframes2() {
5981
- if (injectedKeyframes2 || typeof document === "undefined") return;
5982
- injectedKeyframes2 = true;
5983
- const style = document.createElement("style");
5984
- style.textContent = `
5985
- @keyframes featuredrop-chain-pulse {
5986
- 0%, 100% { transform: scale(1); opacity: 1; }
5987
- 50% { transform: scale(1.35); opacity: 0.65; }
5988
- }
5989
- `;
5990
- document.head.appendChild(style);
5991
- }
5992
- function prefersReducedMotion3() {
5993
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
5994
- return false;
5995
- }
5996
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
5997
- }
5998
6453
  function getFocusableElements4(container) {
5999
6454
  const nodes = container.querySelectorAll(
6000
6455
  [
@@ -6023,12 +6478,26 @@ function SpotlightChain({
6023
6478
  onSkip,
6024
6479
  children
6025
6480
  }) {
6481
+ const featureDrop = useContext(FeatureDropContext);
6026
6482
  const [isActive, setIsActive] = useState(false);
6027
6483
  const [stepIndex, setStepIndex] = useState(-1);
6028
6484
  const [rect, setRect] = useState(null);
6029
6485
  const autoTimerRef = useRef(null);
6030
6486
  const dialogRef = useRef(null);
6031
- const reduceMotion = useMemo(() => prefersReducedMotion3(), []);
6487
+ const animation = useMemo(
6488
+ () => resolveAnimationPreset(featureDrop?.animation ?? "normal", {
6489
+ reducedMotion: prefersReducedMotion()
6490
+ }),
6491
+ [featureDrop?.animation]
6492
+ );
6493
+ const beaconPulseAnimation = useMemo(
6494
+ () => getPulseAnimation(animation, "beacon"),
6495
+ [animation]
6496
+ );
6497
+ const tooltipEnterAnimation = useMemo(
6498
+ () => getEnterAnimation(animation, "popover"),
6499
+ [animation]
6500
+ );
6032
6501
  const instanceIdRef = useRef(`featuredrop-spotlight-chain-${Math.random().toString(36).slice(2, 10)}`);
6033
6502
  const step = stepIndex >= 0 ? steps[stepIndex] ?? null : null;
6034
6503
  const findValidStep = useCallback((start2) => {
@@ -6078,7 +6547,7 @@ function SpotlightChain({
6078
6547
  setRect(null);
6079
6548
  }, [onSkip, step, stepIndex]);
6080
6549
  useEffect(() => {
6081
- injectKeyframes2();
6550
+ ensureFeatureDropAnimationStyles();
6082
6551
  }, []);
6083
6552
  useEffect(() => {
6084
6553
  if (!startOnMount) return;
@@ -6185,7 +6654,7 @@ function SpotlightChain({
6185
6654
  "data-featuredrop-spotlight-chain-beacon": step.id ?? stepIndex,
6186
6655
  style: {
6187
6656
  ...beaconStyles2,
6188
- animation: reduceMotion ? "none" : beaconStyles2.animation,
6657
+ animation: beaconPulseAnimation ?? "none",
6189
6658
  top: rect.top - 7,
6190
6659
  left: rect.right - 7
6191
6660
  }
@@ -6201,7 +6670,12 @@ function SpotlightChain({
6201
6670
  "aria-describedby": contentId,
6202
6671
  "data-featuredrop-spotlight-chain": step.id ?? stepIndex,
6203
6672
  tabIndex: -1,
6204
- style: { ...tooltipStyles3, top: tooltipTop, left: tooltipLeft },
6673
+ style: {
6674
+ ...tooltipStyles3,
6675
+ top: tooltipTop,
6676
+ left: tooltipLeft,
6677
+ animation: tooltipEnterAnimation
6678
+ },
6205
6679
  children: [
6206
6680
  step.title && /* @__PURE__ */ jsx("p", { id: titleId, style: { margin: "0 0 6px", fontWeight: 700, fontSize: "15px" }, children: step.title }),
6207
6681
  /* @__PURE__ */ jsx("div", { id: contentId, style: { margin: "0 0 10px", fontSize: "14px", color: "#4b5563" }, children: step.content }),
@@ -6218,7 +6692,7 @@ function SpotlightChain({
6218
6692
  padding: "6px 10px",
6219
6693
  cursor: "pointer"
6220
6694
  },
6221
- children: "Skip"
6695
+ children: featureDrop?.translations.skip ?? "Skip"
6222
6696
  }
6223
6697
  ),
6224
6698
  /* @__PURE__ */ jsx(
@@ -6234,7 +6708,7 @@ function SpotlightChain({
6234
6708
  padding: "6px 10px",
6235
6709
  cursor: "pointer"
6236
6710
  },
6237
- children: stepIndex >= steps.length - 1 ? "Got it" : "Next"
6711
+ children: stepIndex >= steps.length - 1 ? featureDrop?.translations.gotIt ?? "Got it" : featureDrop?.translations.next ?? "Next"
6238
6712
  }
6239
6713
  )
6240
6714
  ] })
@@ -6421,6 +6895,11 @@ function Survey({
6421
6895
  const askLaterCooldownDays = triggerRules?.askLaterCooldownDays ?? DEFAULT_ASK_LATER_DAYS;
6422
6896
  const scale = useMemo(() => resolveScale(type), [type]);
6423
6897
  const translations = context?.translations;
6898
+ const direction = context?.direction ?? "ltr";
6899
+ const panelAnimation = useMemo(
6900
+ () => getEnterAnimation(context?.animation ?? "normal", "modal"),
6901
+ [context?.animation]
6902
+ );
6424
6903
  const canShow = useMemo(() => {
6425
6904
  const now = Date.now();
6426
6905
  const cooldownUntil = parseIsoTime(readStorageValue3(cooldownStorageKey(id)));
@@ -6510,6 +6989,9 @@ function Survey({
6510
6989
  if (!defaultOpen) return;
6511
6990
  show({ force: true });
6512
6991
  }, [defaultOpen, show]);
6992
+ useEffect(() => {
6993
+ ensureFeatureDropAnimationStyles();
6994
+ }, []);
6513
6995
  useEffect(() => {
6514
6996
  if (!isOpen) return;
6515
6997
  const panel = panelRef.current;
@@ -6685,12 +7167,13 @@ function Survey({
6685
7167
  ref: panelRef,
6686
7168
  "data-featuredrop-survey": id,
6687
7169
  className,
6688
- style: { ...panelStyles2, ...style },
7170
+ style: { ...panelStyles2, animation: panelAnimation, ...style },
6689
7171
  role: "dialog",
6690
7172
  "aria-modal": "false",
6691
7173
  "aria-labelledby": surveyTitleId,
6692
7174
  "aria-describedby": prompt ? surveyDescriptionId : void 0,
6693
7175
  tabIndex: -1,
7176
+ dir: direction,
6694
7177
  children: [
6695
7178
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: "8px" }, children: [
6696
7179
  /* @__PURE__ */ jsx("strong", { id: surveyTitleId, children: surveyTitle }),
@@ -7254,6 +7737,24 @@ function FeatureRequestForm({
7254
7737
  );
7255
7738
  }
7256
7739
 
7257
- export { AnnouncementModal, Banner, ChangelogPage, ChangelogWidget, Checklist, FeatureDropContext, FeatureDropProvider, FeatureRequestButton, FeatureRequestForm, FeedbackWidget, Hotspot, NewBadge, Spotlight, SpotlightChain, Survey, ThemeProvider, Toast, TooltipGroup, Tour, useAdoptionAnalytics, useChecklist, useFeatureDrop, useNewCount, useNewFeature, useSurvey, useTabNotification, useTour, useTourSequencer };
7740
+ // src/react/index.ts
7741
+ var NewBadge2 = withFeatureDropBoundary(NewBadge, "NewBadge");
7742
+ var ChangelogWidget2 = withFeatureDropBoundary(ChangelogWidget, "ChangelogWidget");
7743
+ var Spotlight2 = withFeatureDropBoundary(Spotlight, "Spotlight");
7744
+ var Banner2 = withFeatureDropBoundary(Banner, "Banner");
7745
+ var Toast2 = withFeatureDropBoundary(Toast, "Toast");
7746
+ var ChangelogPage2 = withFeatureDropBoundary(ChangelogPage, "ChangelogPage");
7747
+ var Tour2 = withFeatureDropBoundary(Tour, "Tour");
7748
+ var Checklist2 = withFeatureDropBoundary(Checklist, "Checklist");
7749
+ var Hotspot2 = withFeatureDropBoundary(Hotspot, "Hotspot");
7750
+ var TooltipGroup2 = withFeatureDropBoundary(TooltipGroup, "TooltipGroup");
7751
+ var FeedbackWidget2 = withFeatureDropBoundary(FeedbackWidget, "FeedbackWidget");
7752
+ var AnnouncementModal2 = withFeatureDropBoundary(AnnouncementModal, "AnnouncementModal");
7753
+ var SpotlightChain2 = withFeatureDropBoundary(SpotlightChain, "SpotlightChain");
7754
+ var Survey2 = withFeatureDropBoundary(Survey, "Survey");
7755
+ var FeatureRequestButton2 = withFeatureDropBoundary(FeatureRequestButton, "FeatureRequestButton");
7756
+ var FeatureRequestForm2 = withFeatureDropBoundary(FeatureRequestForm, "FeatureRequestForm");
7757
+
7758
+ export { AnnouncementModal2 as AnnouncementModal, Banner2 as Banner, ChangelogPage2 as ChangelogPage, ChangelogWidget2 as ChangelogWidget, Checklist2 as Checklist, FeatureDropContext, FeatureDropProvider, FeatureRequestButton2 as FeatureRequestButton, FeatureRequestForm2 as FeatureRequestForm, FeedbackWidget2 as FeedbackWidget, Hotspot2 as Hotspot, NewBadge2 as NewBadge, Spotlight2 as Spotlight, SpotlightChain2 as SpotlightChain, Survey2 as Survey, ThemeProvider, Toast2 as Toast, TooltipGroup2 as TooltipGroup, Tour2 as Tour, useAdoptionAnalytics, useChecklist, useFeatureDrop, useNewCount, useNewFeature, useSurvey, useTabNotification, useTour, useTourSequencer };
7258
7759
  //# sourceMappingURL=preact.js.map
7259
7760
  //# sourceMappingURL=preact.js.map