featuredrop 1.2.0 → 1.4.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 +422 -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 +395 -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 +162 -20
  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 +2599 -660
  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 +2555 -668
  39. package/dist/index.js.map +1 -1
  40. package/dist/preact.cjs +747 -221
  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 +713 -207
  45. package/dist/preact.js.map +1 -1
  46. package/dist/react.cjs +747 -221
  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 +713 -207
  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 +36 -5
  75. package/dist/vue.cjs.map +1 -1
  76. package/dist/vue.js +16 -5
  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/index.cjs CHANGED
@@ -1,9 +1,33 @@
1
1
  'use strict';
2
2
 
3
- var module$1 = require('module');
3
+ var moduleApi = require('module');
4
4
  var zod = require('zod');
5
+ var react = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+ var promises = require('fs/promises');
8
+ var path = require('path');
5
9
 
6
10
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
11
+ function _interopNamespace(e) {
12
+ if (e && e.__esModule) return e;
13
+ var n = Object.create(null);
14
+ if (e) {
15
+ Object.keys(e).forEach(function (k) {
16
+ if (k !== 'default') {
17
+ var d = Object.getOwnPropertyDescriptor(e, k);
18
+ Object.defineProperty(n, k, d.get ? d : {
19
+ enumerable: true,
20
+ get: function () { return e[k]; }
21
+ });
22
+ }
23
+ });
24
+ }
25
+ n.default = e;
26
+ return Object.freeze(n);
27
+ }
28
+
29
+ var moduleApi__namespace = /*#__PURE__*/_interopNamespace(moduleApi);
30
+
7
31
  // src/semver.ts
8
32
  var SEMVER_REGEX = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
9
33
  function parseSemver(input) {
@@ -225,6 +249,20 @@ function isVersionMatch(feature, appVersion) {
225
249
  if (v.showNewUntil && compareSemver(appVersion, v.showNewUntil) >= 0) return false;
226
250
  return true;
227
251
  }
252
+ function isFlagMatch(feature, flagBridge, userContext) {
253
+ if (!feature.flagKey) return true;
254
+ if (!flagBridge) return false;
255
+ try {
256
+ return flagBridge.isEnabled(feature.flagKey, userContext);
257
+ } catch {
258
+ return false;
259
+ }
260
+ }
261
+ function isProductMatch(feature, product) {
262
+ if (!feature.product || feature.product === "*") return true;
263
+ if (!product) return false;
264
+ return feature.product === product;
265
+ }
228
266
  function isDependencyMatch(feature, dismissedIds, dependencyState) {
229
267
  const dependsOn = feature.dependsOn;
230
268
  if (!dependsOn) return true;
@@ -249,11 +287,13 @@ function isDependencyMatch(feature, dismissedIds, dependencyState) {
249
287
  }
250
288
  return true;
251
289
  }
252
- function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
290
+ function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext, flagBridge, product) {
253
291
  if (dismissedIds.has(feature.id)) return false;
254
292
  if (!isAudienceMatch(feature, userContext, matchAudience)) return false;
255
293
  if (!isDependencyMatch(feature, dismissedIds, dependencyState)) return false;
256
294
  if (!isVersionMatch(feature, appVersion)) return false;
295
+ if (!isFlagMatch(feature, flagBridge, userContext)) return false;
296
+ if (!isProductMatch(feature, product)) return false;
257
297
  if (!isTriggerMatch(feature.trigger, triggerContext)) return false;
258
298
  const nowMs = now.getTime();
259
299
  if (feature.publishAt) {
@@ -269,7 +309,7 @@ function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date(
269
309
  }
270
310
  return true;
271
311
  }
272
- function getNewFeatures(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
312
+ function getNewFeatures(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext, flagBridge, product) {
273
313
  const watermark = storage.getWatermark();
274
314
  const dismissedIds = storage.getDismissedIds();
275
315
  return manifest.filter(
@@ -282,11 +322,13 @@ function getNewFeatures(manifest, storage, now = /* @__PURE__ */ new Date(), use
282
322
  matchAudience,
283
323
  appVersion,
284
324
  dependencyState,
285
- triggerContext
325
+ triggerContext,
326
+ flagBridge,
327
+ product
286
328
  )
287
329
  );
288
330
  }
289
- function getNewFeatureCount(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
331
+ function getNewFeatureCount(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext, flagBridge, product) {
290
332
  return getNewFeatures(
291
333
  manifest,
292
334
  storage,
@@ -295,10 +337,12 @@ function getNewFeatureCount(manifest, storage, now = /* @__PURE__ */ new Date(),
295
337
  matchAudience,
296
338
  appVersion,
297
339
  dependencyState,
298
- triggerContext
340
+ triggerContext,
341
+ flagBridge,
342
+ product
299
343
  ).length;
300
344
  }
301
- function hasNewFeature(manifest, sidebarKey, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
345
+ function hasNewFeature(manifest, sidebarKey, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext, flagBridge, product) {
302
346
  const watermark = storage.getWatermark();
303
347
  const dismissedIds = storage.getDismissedIds();
304
348
  return manifest.some(
@@ -311,11 +355,13 @@ function hasNewFeature(manifest, sidebarKey, storage, now = /* @__PURE__ */ new
311
355
  matchAudience,
312
356
  appVersion,
313
357
  dependencyState,
314
- triggerContext
358
+ triggerContext,
359
+ flagBridge,
360
+ product
315
361
  )
316
362
  );
317
363
  }
318
- function getNewFeaturesSorted(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
364
+ function getNewFeaturesSorted(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext, flagBridge, product) {
319
365
  const priorityOrder = { critical: 0, normal: 1, low: 2 };
320
366
  return getNewFeatures(
321
367
  manifest,
@@ -325,7 +371,9 @@ function getNewFeaturesSorted(manifest, storage, now = /* @__PURE__ */ new Date(
325
371
  matchAudience,
326
372
  appVersion,
327
373
  dependencyState,
328
- triggerContext
374
+ triggerContext,
375
+ flagBridge,
376
+ product
329
377
  ).sort(
330
378
  (a, b) => {
331
379
  const pa = priorityOrder[a.priority ?? "normal"];
@@ -350,10 +398,11 @@ function getNewFeaturesByCategory(manifest, category, storage, now = /* @__PURE_
350
398
  (f) => f.category === category && isNew(f, watermark, dismissedIds, now, userContext, matchAudience, appVersion)
351
399
  );
352
400
  }
353
- var dynamicRequire = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
401
+ var dynamicRequire = typeof moduleApi__namespace.createRequire === "function" ? moduleApi__namespace.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))) : null;
354
402
  var cachedMarked = null;
355
403
  var cachedShiki = null;
356
404
  function optionalRequire(name) {
405
+ if (!dynamicRequire) return null;
357
406
  try {
358
407
  return dynamicRequire(name);
359
408
  } catch (error) {
@@ -577,209 +626,1709 @@ function parseDescription(markdown) {
577
626
  return sanitizeHtml(decoded);
578
627
  }
579
628
 
580
- // src/theme.ts
581
- var LIGHT_THEME = {
582
- colors: {
583
- primary: "#2563eb",
584
- background: "#ffffff",
585
- text: "#111827",
586
- textMuted: "#6b7280",
587
- border: "#e5e7eb",
588
- success: "#16a34a",
589
- warning: "#f59e0b",
590
- error: "#dc2626"
591
- },
592
- fonts: {
593
- family: "system-ui, -apple-system, Segoe UI, sans-serif",
594
- sizeBase: "14px",
595
- sizeSm: "12px",
596
- sizeLg: "16px"
597
- },
598
- spacing: {
599
- xs: "4px",
600
- sm: "8px",
601
- md: "12px",
602
- lg: "16px",
603
- xl: "24px"
604
- },
605
- radii: {
606
- sm: "6px",
607
- md: "8px",
608
- lg: "12px",
609
- full: "999px"
610
- },
611
- shadows: {
612
- sm: "0 2px 8px rgba(0, 0, 0, 0.08)",
613
- md: "0 8px 24px rgba(0, 0, 0, 0.12)",
614
- lg: "0 20px 60px rgba(0, 0, 0, 0.16)"
615
- },
616
- zIndex: {
617
- base: 9998,
618
- tooltip: 1e4,
619
- modal: 10001,
620
- overlay: 9997
621
- }
622
- };
623
- var DARK_THEME = {
624
- ...LIGHT_THEME,
625
- colors: {
626
- primary: "#60a5fa",
627
- background: "#0b1220",
628
- text: "#f3f4f6",
629
- textMuted: "#9ca3af",
630
- border: "#1f2937",
631
- success: "#4ade80",
632
- warning: "#fbbf24",
633
- error: "#f87171"
634
- },
635
- shadows: {
636
- sm: "0 2px 8px rgba(0, 0, 0, 0.35)",
637
- md: "0 8px 24px rgba(0, 0, 0, 0.42)",
638
- lg: "0 20px 60px rgba(0, 0, 0, 0.52)"
639
- }
640
- };
641
- var MINIMAL_THEME = {
642
- ...LIGHT_THEME,
643
- colors: {
644
- ...LIGHT_THEME.colors,
645
- primary: "#111827",
646
- background: "#ffffff",
647
- text: "#111827",
648
- textMuted: "#6b7280",
649
- border: "#d1d5db",
650
- success: "#111827",
651
- warning: "#111827",
652
- error: "#111827"
653
- },
654
- shadows: {
655
- sm: "none",
656
- md: "none",
657
- lg: "none"
658
- },
659
- radii: {
660
- sm: "0",
661
- md: "0",
662
- lg: "0",
663
- full: "0"
664
- }
665
- };
666
- var VIBRANT_THEME = {
667
- ...LIGHT_THEME,
668
- colors: {
669
- primary: "#ec4899",
670
- background: "#fff7ed",
671
- text: "#3f1d57",
672
- textMuted: "#6d4c84",
673
- border: "#fdba74",
674
- success: "#10b981",
675
- warning: "#f59e0b",
676
- error: "#ef4444"
677
- },
678
- shadows: {
679
- sm: "0 2px 10px rgba(236, 72, 153, 0.15)",
680
- md: "0 10px 26px rgba(236, 72, 153, 0.22)",
681
- lg: "0 22px 58px rgba(236, 72, 153, 0.28)"
682
- }
683
- };
684
- var FEATUREDROP_THEMES = {
685
- light: LIGHT_THEME,
686
- dark: DARK_THEME,
687
- minimal: MINIMAL_THEME,
688
- vibrant: VIBRANT_THEME
689
- };
690
- function isThemePreset(value) {
691
- return value === "light" || value === "dark" || value === "auto" || value === "minimal" || value === "vibrant";
629
+ // src/renderer.ts
630
+ function sortFeatures(features) {
631
+ const priorityOrder = { critical: 0, normal: 1, low: 2 };
632
+ return [...features].sort((a, b) => {
633
+ const pa = priorityOrder[a.priority ?? "normal"];
634
+ const pb = priorityOrder[b.priority ?? "normal"];
635
+ if (pa !== pb) return pa - pb;
636
+ return new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime();
637
+ });
692
638
  }
693
- function mergeTheme(base, overrides) {
694
- if (!overrides) return base;
639
+ function createChangelogRenderer({
640
+ manifest: initialManifest,
641
+ storage,
642
+ userContext: initialUserContext,
643
+ matchAudience: initialMatchAudience,
644
+ appVersion: initialAppVersion,
645
+ flagBridge: initialFlagBridge,
646
+ product: initialProduct,
647
+ now = () => /* @__PURE__ */ new Date()
648
+ }) {
649
+ let manifest = initialManifest;
650
+ let userContext = initialUserContext;
651
+ let matchAudience = initialMatchAudience;
652
+ let appVersion = initialAppVersion;
653
+ let flagBridge = initialFlagBridge;
654
+ let product = initialProduct;
655
+ const listeners = /* @__PURE__ */ new Set();
656
+ let state = {
657
+ manifest,
658
+ newFeatures: [],
659
+ newFeaturesSorted: [],
660
+ newCount: 0,
661
+ watermark: storage.getWatermark(),
662
+ dismissedIds: new Set(storage.getDismissedIds())
663
+ };
664
+ const refresh = () => {
665
+ const features = getNewFeatures(
666
+ manifest,
667
+ storage,
668
+ now(),
669
+ userContext,
670
+ matchAudience,
671
+ appVersion,
672
+ void 0,
673
+ void 0,
674
+ flagBridge,
675
+ product
676
+ );
677
+ state = {
678
+ manifest,
679
+ newFeatures: features,
680
+ newFeaturesSorted: sortFeatures(features),
681
+ newCount: features.length,
682
+ watermark: storage.getWatermark(),
683
+ dismissedIds: new Set(storage.getDismissedIds())
684
+ };
685
+ listeners.forEach((listener) => listener(state));
686
+ };
687
+ const dismiss = (id) => {
688
+ if (!id) return;
689
+ storage.dismiss(id);
690
+ refresh();
691
+ };
692
+ const dismissAll = async () => {
693
+ await storage.dismissAll(now());
694
+ refresh();
695
+ };
696
+ const setManifest = (nextManifest) => {
697
+ manifest = nextManifest;
698
+ refresh();
699
+ };
700
+ const setUserContext = (nextUserContext) => {
701
+ userContext = nextUserContext;
702
+ refresh();
703
+ };
704
+ const setAppVersion = (nextAppVersion) => {
705
+ appVersion = nextAppVersion;
706
+ refresh();
707
+ };
708
+ const setAudienceMatcher = (nextMatchAudience) => {
709
+ matchAudience = nextMatchAudience;
710
+ refresh();
711
+ };
712
+ const setFlagBridge = (nextFlagBridge) => {
713
+ flagBridge = nextFlagBridge;
714
+ refresh();
715
+ };
716
+ const setProduct = (nextProduct) => {
717
+ product = nextProduct;
718
+ refresh();
719
+ };
720
+ const isNew2 = (sidebarKey) => state.newFeatures.some((feature) => feature.sidebarKey === sidebarKey);
721
+ const getFeature = (sidebarKey) => state.newFeatures.find((feature) => feature.sidebarKey === sidebarKey);
722
+ const getFeatureById2 = (id) => state.newFeatures.find((feature) => feature.id === id);
723
+ const getFeaturesByCategory = (category) => state.newFeatures.filter((feature) => feature.category === category);
724
+ const subscribe = (listener) => {
725
+ listeners.add(listener);
726
+ listener(state);
727
+ return () => {
728
+ listeners.delete(listener);
729
+ };
730
+ };
731
+ refresh();
695
732
  return {
696
- colors: {
697
- ...base.colors,
698
- ...overrides.colors ?? {}
699
- },
700
- fonts: {
701
- ...base.fonts,
702
- ...overrides.fonts ?? {}
703
- },
704
- spacing: {
705
- ...base.spacing,
706
- ...overrides.spacing ?? {}
733
+ get state() {
734
+ return state;
707
735
  },
708
- radii: {
709
- ...base.radii,
710
- ...overrides.radii ?? {}
736
+ actions: {
737
+ refresh,
738
+ dismiss,
739
+ dismissAll,
740
+ setManifest,
741
+ setUserContext,
742
+ setAppVersion,
743
+ setAudienceMatcher,
744
+ setFlagBridge,
745
+ setProduct
711
746
  },
712
- shadows: {
713
- ...base.shadows,
714
- ...overrides.shadows ?? {}
747
+ computed: {
748
+ isNew: isNew2,
749
+ getFeature,
750
+ getFeatureById: getFeatureById2,
751
+ getFeaturesByCategory
715
752
  },
716
- zIndex: {
717
- ...base.zIndex,
718
- ...overrides.zIndex ?? {}
719
- }
753
+ subscribe
720
754
  };
721
755
  }
722
- function createTheme(overrides, base = LIGHT_THEME) {
723
- return mergeTheme(base, overrides);
756
+
757
+ // src/rss.ts
758
+ function escape(str) {
759
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
724
760
  }
725
- function resolveTheme(input = "light", options = {}) {
726
- if (isThemePreset(input)) {
727
- if (input === "auto") {
728
- return options.prefersDark ? DARK_THEME : LIGHT_THEME;
729
- }
730
- return FEATUREDROP_THEMES[input];
731
- }
732
- return mergeTheme(LIGHT_THEME, input);
761
+ function generateRSS(manifest, options) {
762
+ const title = escape(options?.title ?? "Featuredrop Changelog");
763
+ const link = escape(options?.link ?? "");
764
+ const desc = escape(options?.description ?? "Product updates");
765
+ const items = manifest.slice().sort((a, b) => new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime()).map((item) => {
766
+ const descriptionHtml = item.description ? parseDescription(item.description) : "";
767
+ const itemLink = item.url ? escape(item.url) : "";
768
+ return [
769
+ "<item>",
770
+ `<title>${escape(item.label)}</title>`,
771
+ itemLink ? `<link>${itemLink}</link>` : "",
772
+ `<guid isPermaLink="false">${escape(item.id)}</guid>`,
773
+ `<pubDate>${new Date(item.releasedAt).toUTCString()}</pubDate>`,
774
+ `<description><![CDATA[${descriptionHtml}]]></description>`,
775
+ "</item>"
776
+ ].join("");
777
+ }).join("");
778
+ return [
779
+ '<?xml version="1.0" encoding="UTF-8"?>',
780
+ '<rss version="2.0">',
781
+ "<channel>",
782
+ `<title>${title}</title>`,
783
+ link ? `<link>${link}</link>` : "",
784
+ `<description>${desc}</description>`,
785
+ items,
786
+ "</channel>",
787
+ "</rss>"
788
+ ].join("");
733
789
  }
734
- function applyThemeSection(vars, key, values) {
735
- for (const [token, value] of Object.entries(values)) {
736
- vars[`--featuredrop-${key}-${token}`] = value;
790
+
791
+ // src/bridges.ts
792
+ async function postJson(url, payload, headers) {
793
+ const response = await fetch(url, {
794
+ method: "POST",
795
+ headers: {
796
+ "Content-Type": "application/json",
797
+ ...headers ?? {}
798
+ },
799
+ body: JSON.stringify(payload)
800
+ });
801
+ if (!response.ok) {
802
+ throw new Error(`[featuredrop] Bridge request failed (${response.status}) for ${url}`);
737
803
  }
738
804
  }
739
- function themeToCSSVariables(theme) {
740
- const vars = {};
741
- applyThemeSection(vars, "color", theme.colors);
742
- applyThemeSection(vars, "font", theme.fonts);
743
- applyThemeSection(vars, "space", theme.spacing);
744
- applyThemeSection(vars, "radius", theme.radii);
745
- applyThemeSection(vars, "shadow", theme.shadows);
746
- applyThemeSection(vars, "z", theme.zIndex);
747
- vars["--featuredrop-font-family"] = theme.fonts.family;
748
- vars["--featuredrop-widget-bg"] = theme.colors.background;
749
- vars["--featuredrop-trigger-bg"] = theme.colors.background;
750
- vars["--featuredrop-trigger-color"] = theme.colors.text;
751
- vars["--featuredrop-entry-title-color"] = theme.colors.text;
752
- vars["--featuredrop-entry-desc-color"] = theme.colors.textMuted;
753
- vars["--featuredrop-title-color"] = theme.colors.text;
754
- vars["--featuredrop-border-color"] = theme.colors.border;
755
- vars["--featuredrop-cta-bg"] = theme.colors.primary;
756
- vars["--featuredrop-cta-color"] = theme.colors.background;
757
- vars["--featuredrop-mark-all-color"] = theme.colors.primary;
758
- vars["--featuredrop-widget-shadow"] = theme.shadows.md;
759
- vars["--featuredrop-widget-radius"] = theme.radii.lg;
760
- vars["--featuredrop-trigger-radius"] = theme.radii.md;
761
- vars["--featuredrop-badge-bg"] = theme.colors.warning;
762
- vars["--featuredrop-z-index"] = theme.zIndex.base;
763
- vars["--featuredrop-toast-z-index"] = theme.zIndex.tooltip;
764
- vars["--featuredrop-tour-z-index"] = theme.zIndex.modal;
765
- vars["--featuredrop-tour-overlay-z-index"] = theme.zIndex.overlay;
766
- return vars;
805
+ function formatFeatureLine(feature) {
806
+ const released = new Date(feature.releasedAt).toLocaleDateString("en-US", {
807
+ year: "numeric",
808
+ month: "short",
809
+ day: "numeric"
810
+ });
811
+ return `${feature.label} (${released})`;
767
812
  }
768
-
769
- // src/i18n.ts
770
- var EN_TRANSLATIONS = {
771
- newBadge: "New",
772
- whatsNewTitle: "What's New",
773
- markAllRead: "Mark all as read",
774
- allCaughtUp: "You're all caught up!",
775
- close: "Close",
776
- changelogTitle: "Changelog",
813
+ var SlackBridge = {
814
+ async notify(feature, options) {
815
+ const payload = options.formatter ? options.formatter(feature) : {
816
+ username: options.username,
817
+ icon_emoji: options.iconEmoji,
818
+ channel: options.channel,
819
+ text: `New feature published: *${feature.label}*`,
820
+ attachments: [
821
+ {
822
+ color: "#2563eb",
823
+ title: feature.label,
824
+ text: feature.description ?? "No description provided.",
825
+ title_link: feature.url,
826
+ footer: `featuredrop | ${feature.id}`
827
+ }
828
+ ]
829
+ };
830
+ await postJson(options.webhookUrl, payload);
831
+ }
832
+ };
833
+ var DiscordBridge = {
834
+ async notify(feature, options) {
835
+ const payload = options.formatter ? options.formatter(feature) : {
836
+ username: options.username ?? "featuredrop",
837
+ avatar_url: options.avatarUrl,
838
+ embeds: [
839
+ {
840
+ title: feature.label,
841
+ description: feature.description ?? "No description provided.",
842
+ url: feature.url,
843
+ color: 2450411,
844
+ footer: {
845
+ text: `featuredrop | ${feature.id}`
846
+ }
847
+ }
848
+ ]
849
+ };
850
+ await postJson(options.webhookUrl, payload);
851
+ }
852
+ };
853
+ var WebhookBridge = {
854
+ async post(feature, options) {
855
+ const payload = {
856
+ event: options.event ?? "feature.published",
857
+ feature,
858
+ sentAt: (/* @__PURE__ */ new Date()).toISOString(),
859
+ ...options.body ?? {}
860
+ };
861
+ await postJson(options.url, payload, options.headers);
862
+ }
863
+ };
864
+ var EmailDigestGenerator = {
865
+ generate(features, options = {}) {
866
+ const title = options.title ?? "Product Updates";
867
+ const intro = options.intro ?? "Here are the latest updates:";
868
+ const productName = options.productName ?? "Your Product";
869
+ const template = options.template ?? "default";
870
+ const listItems = features.map((feature) => {
871
+ const safeLabel = feature.label.replace(/</g, "&lt;").replace(/>/g, "&gt;");
872
+ const safeDescription = (feature.description ?? "").replace(/</g, "&lt;").replace(/>/g, "&gt;");
873
+ const link = feature.url ? `<a href="${feature.url}" style="color:#2563eb;text-decoration:none;">Read more</a>` : "";
874
+ if (template === "minimal") {
875
+ return `<li><strong>${safeLabel}</strong>${safeDescription ? ` - ${safeDescription}` : ""}</li>`;
876
+ }
877
+ return [
878
+ '<li style="margin:0 0 14px;">',
879
+ `<p style="margin:0 0 4px;font-weight:600;color:#111827;">${safeLabel}</p>`,
880
+ safeDescription ? `<p style="margin:0 0 6px;color:#4b5563;line-height:1.45;">${safeDescription}</p>` : "",
881
+ link ? `<p style="margin:0;">${link}</p>` : "",
882
+ "</li>"
883
+ ].join("");
884
+ }).join("");
885
+ if (template === "minimal") {
886
+ return [
887
+ "<!doctype html>",
888
+ "<html><body>",
889
+ `<h2>${title}</h2>`,
890
+ `<p>${intro}</p>`,
891
+ `<ul>${listItems}</ul>`,
892
+ "</body></html>"
893
+ ].join("");
894
+ }
895
+ const summary = features.map((feature) => formatFeatureLine(feature)).join(" | ");
896
+ return [
897
+ "<!doctype html>",
898
+ "<html>",
899
+ `<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f8fafc;padding:20px;">`,
900
+ '<div style="max-width:640px;margin:0 auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;">',
901
+ `<p style="margin:0 0 12px;color:#6b7280;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;">${productName}</p>`,
902
+ `<h1 style="margin:0 0 8px;font-size:22px;color:#111827;">${title}</h1>`,
903
+ `<p style="margin:0 0 14px;color:#374151;">${intro}</p>`,
904
+ `<p style="margin:0 0 18px;color:#6b7280;font-size:13px;">${summary}</p>`,
905
+ `<ul style="padding-left:18px;margin:0;">${listItems}</ul>`,
906
+ "</div>",
907
+ "</body>",
908
+ "</html>"
909
+ ].join("");
910
+ }
911
+ };
912
+ var RSSFeedGenerator = {
913
+ generate(manifest, options) {
914
+ return generateRSS(manifest, options);
915
+ }
916
+ };
917
+
918
+ // src/dependencies.ts
919
+ function getDirectDependencies(feature) {
920
+ const dependsOn = feature.dependsOn;
921
+ if (!dependsOn) return [];
922
+ const seen = dependsOn.seen ?? [];
923
+ const clicked = dependsOn.clicked ?? [];
924
+ const dismissed = dependsOn.dismissed ?? [];
925
+ const unique = /* @__PURE__ */ new Set();
926
+ for (const id of [...seen, ...clicked, ...dismissed]) {
927
+ if (id) unique.add(id);
928
+ }
929
+ return Array.from(unique);
930
+ }
931
+ function resolveDependencyOrder(manifest) {
932
+ const ids = new Set(manifest.map((feature) => feature.id));
933
+ const outgoing = /* @__PURE__ */ new Map();
934
+ const indegree = /* @__PURE__ */ new Map();
935
+ for (const feature of manifest) {
936
+ outgoing.set(feature.id, /* @__PURE__ */ new Set());
937
+ indegree.set(feature.id, 0);
938
+ }
939
+ for (const feature of manifest) {
940
+ for (const dependencyId of getDirectDependencies(feature)) {
941
+ if (!ids.has(dependencyId)) continue;
942
+ const edges = outgoing.get(dependencyId);
943
+ if (!edges || edges.has(feature.id)) continue;
944
+ edges.add(feature.id);
945
+ indegree.set(feature.id, (indegree.get(feature.id) ?? 0) + 1);
946
+ }
947
+ }
948
+ const queue = [];
949
+ for (const feature of manifest) {
950
+ if ((indegree.get(feature.id) ?? 0) === 0) queue.push(feature.id);
951
+ }
952
+ const ordered = [];
953
+ while (queue.length > 0) {
954
+ const id = queue.shift();
955
+ if (!id) continue;
956
+ ordered.push(id);
957
+ const edges = outgoing.get(id);
958
+ if (!edges) continue;
959
+ for (const nextId of edges) {
960
+ const nextDegree = (indegree.get(nextId) ?? 0) - 1;
961
+ indegree.set(nextId, nextDegree);
962
+ if (nextDegree === 0) queue.push(nextId);
963
+ }
964
+ }
965
+ if (ordered.length < manifest.length) {
966
+ const included = new Set(ordered);
967
+ for (const feature of manifest) {
968
+ if (included.has(feature.id)) continue;
969
+ ordered.push(feature.id);
970
+ }
971
+ }
972
+ return ordered;
973
+ }
974
+ function hasDependencyCycle(manifest) {
975
+ const ids = new Set(manifest.map((feature) => feature.id));
976
+ const outgoing = /* @__PURE__ */ new Map();
977
+ const indegree = /* @__PURE__ */ new Map();
978
+ for (const feature of manifest) {
979
+ outgoing.set(feature.id, /* @__PURE__ */ new Set());
980
+ indegree.set(feature.id, 0);
981
+ }
982
+ for (const feature of manifest) {
983
+ for (const dependencyId of getDirectDependencies(feature)) {
984
+ if (!ids.has(dependencyId)) continue;
985
+ const edges = outgoing.get(dependencyId);
986
+ if (!edges || edges.has(feature.id)) continue;
987
+ edges.add(feature.id);
988
+ indegree.set(feature.id, (indegree.get(feature.id) ?? 0) + 1);
989
+ }
990
+ }
991
+ const queue = [];
992
+ for (const feature of manifest) {
993
+ if ((indegree.get(feature.id) ?? 0) === 0) queue.push(feature.id);
994
+ }
995
+ let visited = 0;
996
+ while (queue.length > 0) {
997
+ const id = queue.shift();
998
+ if (!id) continue;
999
+ visited += 1;
1000
+ const edges = outgoing.get(id);
1001
+ if (!edges) continue;
1002
+ for (const nextId of edges) {
1003
+ const nextDegree = (indegree.get(nextId) ?? 0) - 1;
1004
+ indegree.set(nextId, nextDegree);
1005
+ if (nextDegree === 0) queue.push(nextId);
1006
+ }
1007
+ }
1008
+ return visited !== manifest.length;
1009
+ }
1010
+ function sortFeaturesByDependencies(features) {
1011
+ if (features.length <= 1) return [...features];
1012
+ const order = resolveDependencyOrder(features);
1013
+ const rank = new Map(order.map((id, index) => [id, index]));
1014
+ return [...features].sort((a, b) => {
1015
+ const ra = rank.get(a.id);
1016
+ const rb = rank.get(b.id);
1017
+ if (ra === void 0 || rb === void 0) return 0;
1018
+ return ra - rb;
1019
+ });
1020
+ }
1021
+ var featureEntryJsonSchema = {
1022
+ type: "object",
1023
+ required: ["id", "label", "releasedAt", "showNewUntil"],
1024
+ properties: {
1025
+ id: { type: "string" },
1026
+ label: { type: "string" },
1027
+ description: { type: "string" },
1028
+ releasedAt: { type: "string", format: "date-time" },
1029
+ showNewUntil: { type: "string", format: "date-time" },
1030
+ flagKey: { type: "string" },
1031
+ product: { type: "string" },
1032
+ url: { type: "string" },
1033
+ image: { type: "string" },
1034
+ type: { enum: ["feature", "improvement", "fix", "breaking"] },
1035
+ priority: { enum: ["critical", "normal", "low"] },
1036
+ cta: {
1037
+ type: "object",
1038
+ properties: {
1039
+ label: { type: "string" },
1040
+ url: { type: "string" }
1041
+ }
1042
+ },
1043
+ meta: { type: "object" }
1044
+ }
1045
+ };
1046
+ var featureManifestJsonSchema = {
1047
+ type: "array",
1048
+ items: featureEntryJsonSchema
1049
+ };
1050
+ function isRecord(value) {
1051
+ return !!value && typeof value === "object" && !Array.isArray(value);
1052
+ }
1053
+ function isValidDate(value) {
1054
+ return Number.isFinite(new Date(value).getTime());
1055
+ }
1056
+ var nonEmptyString = zod.z.string().trim().min(1, "must be a non-empty string");
1057
+ var isoDateString = nonEmptyString.refine(isValidDate, {
1058
+ message: "must be a valid date",
1059
+ params: { featuredropCode: "invalid_date" }
1060
+ });
1061
+ var dependsOnSchema = zod.z.object({
1062
+ seen: zod.z.array(zod.z.string()).optional(),
1063
+ clicked: zod.z.array(zod.z.string()).optional(),
1064
+ dismissed: zod.z.array(zod.z.string()).optional()
1065
+ }).optional();
1066
+ var ctaSchema = zod.z.object({
1067
+ label: nonEmptyString,
1068
+ url: nonEmptyString
1069
+ }).optional();
1070
+ var featureEntrySchema = zod.z.object({
1071
+ id: nonEmptyString,
1072
+ label: nonEmptyString,
1073
+ releasedAt: isoDateString,
1074
+ showNewUntil: isoDateString,
1075
+ description: zod.z.string().optional(),
1076
+ flagKey: zod.z.string().optional(),
1077
+ product: zod.z.string().optional(),
1078
+ url: zod.z.string().optional(),
1079
+ image: zod.z.string().optional(),
1080
+ type: zod.z.enum(["feature", "improvement", "fix", "breaking"]).optional(),
1081
+ priority: zod.z.enum(["critical", "normal", "low"]).optional(),
1082
+ cta: ctaSchema,
1083
+ meta: zod.z.record(zod.z.unknown()).optional(),
1084
+ dependsOn: dependsOnSchema
1085
+ }).passthrough();
1086
+ var featureManifestSchema = zod.z.array(featureEntrySchema);
1087
+ function toIssuePath(path) {
1088
+ if (path.length === 0) return "$";
1089
+ let output = "";
1090
+ for (const part of path) {
1091
+ if (typeof part === "number") output += `[${part}]`;
1092
+ else output += output ? `.${part}` : part;
1093
+ }
1094
+ return output;
1095
+ }
1096
+ function mapZodIssue(issue) {
1097
+ const codeParam = issue.params?.featuredropCode;
1098
+ if (codeParam === "invalid_date") {
1099
+ return {
1100
+ path: toIssuePath(issue.path),
1101
+ message: issue.message,
1102
+ code: "invalid_date"
1103
+ };
1104
+ }
1105
+ if (issue.code === "invalid_type") {
1106
+ return {
1107
+ path: toIssuePath(issue.path),
1108
+ message: issue.message,
1109
+ code: issue.received === "undefined" ? "missing_required" : "invalid_type"
1110
+ };
1111
+ }
1112
+ return {
1113
+ path: toIssuePath(issue.path),
1114
+ message: issue.message,
1115
+ code: "invalid_value"
1116
+ };
1117
+ }
1118
+ var UNSAFE_META_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1119
+ function isSafeUrl(value) {
1120
+ const normalized = value.trim();
1121
+ if (!normalized) return false;
1122
+ if (/^(\/|\.\/|\.\.\/|\?|#)/.test(normalized)) return true;
1123
+ if (/^https?:\/\//i.test(normalized)) return true;
1124
+ return false;
1125
+ }
1126
+ function findUnsafeMetaPath(value, path = "meta") {
1127
+ if (Array.isArray(value)) {
1128
+ for (let index = 0; index < value.length; index++) {
1129
+ const nested = findUnsafeMetaPath(value[index], `${path}[${index}]`);
1130
+ if (nested) return nested;
1131
+ }
1132
+ return null;
1133
+ }
1134
+ if (!isRecord(value)) return null;
1135
+ for (const [key, nestedValue] of Object.entries(value)) {
1136
+ if (UNSAFE_META_KEYS.has(key)) {
1137
+ return `${path}.${key}`;
1138
+ }
1139
+ const nested = findUnsafeMetaPath(nestedValue, `${path}.${key}`);
1140
+ if (nested) return nested;
1141
+ }
1142
+ return null;
1143
+ }
1144
+ function validateFeatureEntry(raw, index) {
1145
+ if (!isRecord(raw)) {
1146
+ return {
1147
+ issues: [
1148
+ {
1149
+ path: `[${index}]`,
1150
+ message: "Feature entry must be an object",
1151
+ code: "invalid_type"
1152
+ }
1153
+ ]
1154
+ };
1155
+ }
1156
+ const parsed = featureEntrySchema.safeParse(raw);
1157
+ if (!parsed.success) {
1158
+ return {
1159
+ issues: parsed.error.issues.map((issue) => ({
1160
+ ...mapZodIssue(issue),
1161
+ path: `[${index}]${issue.path.length > 0 ? `.${toIssuePath(issue.path)}` : ""}`
1162
+ }))
1163
+ };
1164
+ }
1165
+ return {
1166
+ issues: [],
1167
+ entry: parsed.data
1168
+ };
1169
+ }
1170
+ function validateManifest(data) {
1171
+ const errors = [];
1172
+ if (!Array.isArray(data)) {
1173
+ return {
1174
+ valid: false,
1175
+ errors: [
1176
+ {
1177
+ path: "$",
1178
+ message: "Manifest must be an array",
1179
+ code: "invalid_type"
1180
+ }
1181
+ ]
1182
+ };
1183
+ }
1184
+ const entries = [];
1185
+ const seenIds = /* @__PURE__ */ new Set();
1186
+ data.forEach((item, index) => {
1187
+ const result = validateFeatureEntry(item, index);
1188
+ errors.push(...result.issues);
1189
+ if (!result.entry) return;
1190
+ if (seenIds.has(result.entry.id)) {
1191
+ errors.push({
1192
+ path: `[${index}].id`,
1193
+ message: `Duplicate feature id "${result.entry.id}"`,
1194
+ code: "duplicate_id"
1195
+ });
1196
+ return;
1197
+ }
1198
+ seenIds.add(result.entry.id);
1199
+ entries.push(result.entry);
1200
+ });
1201
+ if (entries.length > 0 && hasDependencyCycle(entries)) {
1202
+ errors.push({
1203
+ path: "$",
1204
+ message: "Circular dependsOn relationship detected",
1205
+ code: "circular_dependency"
1206
+ });
1207
+ }
1208
+ for (let index = 0; index < entries.length; index++) {
1209
+ const entry = entries[index];
1210
+ if (new Date(entry.showNewUntil).getTime() <= new Date(entry.releasedAt).getTime()) {
1211
+ errors.push({
1212
+ path: `[${index}].showNewUntil`,
1213
+ message: "showNewUntil must be after releasedAt",
1214
+ code: "invalid_value"
1215
+ });
1216
+ }
1217
+ if (entry.url && !isSafeUrl(entry.url)) {
1218
+ errors.push({
1219
+ path: `[${index}].url`,
1220
+ message: "url must be http, https, or relative",
1221
+ code: "invalid_value"
1222
+ });
1223
+ }
1224
+ if (entry.image && !isSafeUrl(entry.image)) {
1225
+ errors.push({
1226
+ path: `[${index}].image`,
1227
+ message: "image must be http, https, or relative",
1228
+ code: "invalid_value"
1229
+ });
1230
+ }
1231
+ if (entry.cta?.url && !isSafeUrl(entry.cta.url)) {
1232
+ errors.push({
1233
+ path: `[${index}].cta.url`,
1234
+ message: "cta.url must be http, https, or relative",
1235
+ code: "invalid_value"
1236
+ });
1237
+ }
1238
+ const unsafeMetaPath = findUnsafeMetaPath(entry.meta);
1239
+ if (unsafeMetaPath) {
1240
+ errors.push({
1241
+ path: `[${index}].${unsafeMetaPath}`,
1242
+ message: `meta contains unsafe key "${unsafeMetaPath.split(".").pop()}"`,
1243
+ code: "invalid_value"
1244
+ });
1245
+ }
1246
+ }
1247
+ return {
1248
+ valid: errors.length === 0,
1249
+ errors
1250
+ };
1251
+ }
1252
+
1253
+ // src/ci.ts
1254
+ function isRecord2(value) {
1255
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1256
+ }
1257
+ function collectChangedFields(beforeValue, afterValue, path, output) {
1258
+ if (beforeValue === afterValue) return;
1259
+ if (Array.isArray(beforeValue) && Array.isArray(afterValue)) {
1260
+ if (beforeValue.length !== afterValue.length) {
1261
+ output.push(path);
1262
+ return;
1263
+ }
1264
+ for (let i = 0; i < beforeValue.length; i += 1) {
1265
+ collectChangedFields(beforeValue[i], afterValue[i], `${path}[${i}]`, output);
1266
+ }
1267
+ return;
1268
+ }
1269
+ if (isRecord2(beforeValue) && isRecord2(afterValue)) {
1270
+ const keys = /* @__PURE__ */ new Set([...Object.keys(beforeValue), ...Object.keys(afterValue)]);
1271
+ keys.forEach((key) => {
1272
+ const nextPath = path ? `${path}.${key}` : key;
1273
+ collectChangedFields(beforeValue[key], afterValue[key], nextPath, output);
1274
+ });
1275
+ return;
1276
+ }
1277
+ output.push(path);
1278
+ }
1279
+ function diffManifest(before, after) {
1280
+ const beforeById = new Map(before.map((feature) => [feature.id, feature]));
1281
+ const afterById = new Map(after.map((feature) => [feature.id, feature]));
1282
+ const added = after.filter((feature) => !beforeById.has(feature.id));
1283
+ const removed = before.filter((feature) => !afterById.has(feature.id));
1284
+ const changed = after.filter((feature) => beforeById.has(feature.id)).map((feature) => {
1285
+ const previous = beforeById.get(feature.id);
1286
+ if (!previous) return null;
1287
+ const changedFields = [];
1288
+ collectChangedFields(previous, feature, "", changedFields);
1289
+ if (changedFields.length === 0) return null;
1290
+ return {
1291
+ id: feature.id,
1292
+ before: previous,
1293
+ after: feature,
1294
+ changedFields
1295
+ };
1296
+ }).filter((item) => item !== null);
1297
+ return { added, removed, changed };
1298
+ }
1299
+ function generateChangelogDiff(diff, options = {}) {
1300
+ const parts = [];
1301
+ if (diff.added.length > 0) {
1302
+ parts.push(`Added: ${diff.added.map((feature) => feature.label).join(", ")}`);
1303
+ }
1304
+ if (diff.changed.length > 0) {
1305
+ const changedText = diff.changed.map((item) => {
1306
+ if (!options.includeFieldChanges) return item.after.label;
1307
+ return `${item.after.label} [${item.changedFields.join(", ")}]`;
1308
+ });
1309
+ parts.push(`Changed: ${changedText.join(", ")}`);
1310
+ }
1311
+ if (diff.removed.length > 0) {
1312
+ parts.push(`Removed: ${diff.removed.map((feature) => feature.label).join(", ")}`);
1313
+ }
1314
+ return parts.length > 0 ? parts.join(". ") : "No manifest changes.";
1315
+ }
1316
+ function validateManifestForCI(manifest) {
1317
+ return validateManifest(manifest);
1318
+ }
1319
+
1320
+ // src/flags.ts
1321
+ function createFlagBridge(options) {
1322
+ return {
1323
+ isEnabled: (flagKey, userContext) => {
1324
+ if (!flagKey) return false;
1325
+ return options.isEnabled(flagKey, userContext);
1326
+ }
1327
+ };
1328
+ }
1329
+ var LaunchDarklyBridge = class {
1330
+ client;
1331
+ options;
1332
+ constructor(client, options = {}) {
1333
+ this.client = client;
1334
+ this.options = options;
1335
+ }
1336
+ isEnabled(flagKey, userContext) {
1337
+ const defaultUser = {
1338
+ key: userContext?.traits?.id ?? userContext?.role ?? "anonymous",
1339
+ custom: {
1340
+ plan: userContext?.plan,
1341
+ role: userContext?.role,
1342
+ region: userContext?.region,
1343
+ ...userContext?.traits ?? {}
1344
+ }
1345
+ };
1346
+ const user = this.options.userResolver ? this.options.userResolver(userContext) : defaultUser;
1347
+ return this.client.variation(flagKey, user, this.options.defaultValue ?? false);
1348
+ }
1349
+ };
1350
+ var PostHogBridge = class {
1351
+ client;
1352
+ options;
1353
+ constructor(client, options = {}) {
1354
+ this.client = client;
1355
+ this.options = options;
1356
+ }
1357
+ isEnabled(flagKey, userContext) {
1358
+ const distinctId = this.options.distinctIdResolver ? this.options.distinctIdResolver(userContext) : typeof userContext?.traits?.id === "string" ? userContext.traits.id : void 0;
1359
+ const groups = this.options.groupsResolver ? this.options.groupsResolver(userContext) : void 0;
1360
+ return this.client.isFeatureEnabled(flagKey, distinctId, groups, userContext?.traits);
1361
+ }
1362
+ };
1363
+ var panelStyles = {
1364
+ border: "1px solid #e5e7eb",
1365
+ borderRadius: "10px",
1366
+ padding: "12px",
1367
+ background: "#ffffff"
1368
+ };
1369
+ var headingStyles = {
1370
+ margin: "0 0 8px",
1371
+ fontSize: "15px",
1372
+ fontWeight: 700
1373
+ };
1374
+ function ManifestEditor({
1375
+ features,
1376
+ onSave,
1377
+ readOnly = false,
1378
+ children
1379
+ }) {
1380
+ const [draft, setDraft] = react.useState(() => JSON.stringify(features, null, 2));
1381
+ const [status, setStatus] = react.useState("idle");
1382
+ const [error, setError] = react.useState("");
1383
+ const parsed = react.useMemo(() => {
1384
+ try {
1385
+ const next = JSON.parse(draft);
1386
+ if (!Array.isArray(next)) throw new Error("Manifest must be an array");
1387
+ return next;
1388
+ } catch {
1389
+ return null;
1390
+ }
1391
+ }, [draft]);
1392
+ const save = async () => {
1393
+ if (readOnly || !parsed) return;
1394
+ setStatus("saving");
1395
+ setError("");
1396
+ try {
1397
+ await onSave(parsed);
1398
+ setStatus("saved");
1399
+ } catch (cause) {
1400
+ setStatus("error");
1401
+ setError(cause instanceof Error ? cause.message : "Failed to save manifest");
1402
+ }
1403
+ };
1404
+ if (children) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
1405
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "data-featuredrop-admin-manifest-editor": true, style: panelStyles, children: [
1406
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: headingStyles, children: "Manifest Editor" }),
1407
+ /* @__PURE__ */ jsxRuntime.jsx(
1408
+ "textarea",
1409
+ {
1410
+ "aria-label": "Manifest JSON",
1411
+ value: draft,
1412
+ onChange: (event) => setDraft(event.target.value),
1413
+ readOnly,
1414
+ style: {
1415
+ width: "100%",
1416
+ minHeight: "180px",
1417
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
1418
+ fontSize: "12px",
1419
+ lineHeight: 1.45,
1420
+ border: "1px solid #d1d5db",
1421
+ borderRadius: "8px",
1422
+ padding: "10px"
1423
+ }
1424
+ }
1425
+ ),
1426
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px", marginTop: "8px" }, children: [
1427
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: save, disabled: readOnly || !parsed, children: "Save" }),
1428
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-live": "polite", children: status }),
1429
+ !parsed && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#dc2626" }, children: "Invalid JSON" }),
1430
+ error && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#dc2626" }, children: error })
1431
+ ] })
1432
+ ] });
1433
+ }
1434
+ function ScheduleCalendar({ features, onSchedule }) {
1435
+ const [values, setValues] = react.useState({});
1436
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "data-featuredrop-admin-schedule-calendar": true, style: panelStyles, children: [
1437
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: headingStyles, children: "Schedule Calendar" }),
1438
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { style: { margin: 0, padding: 0, listStyle: "none", display: "grid", gap: "10px" }, children: features.map((feature) => /* @__PURE__ */ jsxRuntime.jsxs(
1439
+ "li",
1440
+ {
1441
+ style: {
1442
+ border: "1px solid #e5e7eb",
1443
+ borderRadius: "8px",
1444
+ padding: "10px",
1445
+ display: "grid",
1446
+ gap: "6px"
1447
+ },
1448
+ children: [
1449
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: feature.label }),
1450
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: { display: "grid", gap: "4px" }, children: [
1451
+ "Publish at",
1452
+ /* @__PURE__ */ jsxRuntime.jsx(
1453
+ "input",
1454
+ {
1455
+ type: "datetime-local",
1456
+ value: values[feature.id] ?? "",
1457
+ onChange: (event) => {
1458
+ const value = event.target.value;
1459
+ setValues((previous) => ({ ...previous, [feature.id]: value }));
1460
+ }
1461
+ }
1462
+ )
1463
+ ] }),
1464
+ /* @__PURE__ */ jsxRuntime.jsx(
1465
+ "button",
1466
+ {
1467
+ type: "button",
1468
+ onClick: () => {
1469
+ const value = values[feature.id];
1470
+ if (!value) return;
1471
+ void onSchedule(feature.id, new Date(value).toISOString());
1472
+ },
1473
+ children: "Schedule"
1474
+ }
1475
+ )
1476
+ ]
1477
+ },
1478
+ feature.id
1479
+ )) })
1480
+ ] });
1481
+ }
1482
+ function PreviewPanel({ feature, components = ["badge", "changelog"] }) {
1483
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "data-featuredrop-admin-preview-panel": true, style: panelStyles, children: [
1484
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: headingStyles, children: "Preview Panel" }),
1485
+ !feature ? /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0, color: "#6b7280" }, children: "Select a feature to preview." }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1486
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 6px", fontWeight: 600 }, children: feature.label }),
1487
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 8px", color: "#6b7280" }, children: feature.description ?? "No description" }),
1488
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: components.map((component) => /* @__PURE__ */ jsxRuntime.jsx(
1489
+ "span",
1490
+ {
1491
+ style: {
1492
+ border: "1px solid #d1d5db",
1493
+ borderRadius: "999px",
1494
+ padding: "2px 8px",
1495
+ fontSize: "12px"
1496
+ },
1497
+ children: component
1498
+ },
1499
+ component
1500
+ )) })
1501
+ ] })
1502
+ ] });
1503
+ }
1504
+ function toggle(list, value) {
1505
+ const items = new Set(list ?? []);
1506
+ if (items.has(value)) items.delete(value);
1507
+ else items.add(value);
1508
+ return Array.from(items);
1509
+ }
1510
+ function AudienceBuilder({
1511
+ segments = [],
1512
+ roles = [],
1513
+ regions = [],
1514
+ value,
1515
+ onChange,
1516
+ onSave
1517
+ }) {
1518
+ const [audience, setAudience] = react.useState({
1519
+ plan: value?.plan ?? [],
1520
+ role: value?.role ?? [],
1521
+ region: value?.region ?? []
1522
+ });
1523
+ const updateAudience = (next) => {
1524
+ setAudience(next);
1525
+ onChange?.(next);
1526
+ };
1527
+ const section = (title, values, selected, onToggle) => /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { style: { border: "none", margin: 0, padding: 0 }, children: [
1528
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { style: { fontWeight: 600, marginBottom: "4px" }, children: title }),
1529
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: values.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("label", { style: { display: "inline-flex", alignItems: "center", gap: "6px" }, children: [
1530
+ /* @__PURE__ */ jsxRuntime.jsx(
1531
+ "input",
1532
+ {
1533
+ type: "checkbox",
1534
+ checked: Boolean(selected?.includes(item)),
1535
+ onChange: () => onToggle(item)
1536
+ }
1537
+ ),
1538
+ item
1539
+ ] }, item)) })
1540
+ ] });
1541
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "data-featuredrop-admin-audience-builder": true, style: panelStyles, children: [
1542
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: headingStyles, children: "Audience Builder" }),
1543
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "grid", gap: "10px" }, children: [
1544
+ section("Plans", segments, audience.plan, (item) => updateAudience({ ...audience, plan: toggle(audience.plan, item) })),
1545
+ section("Roles", roles, audience.role, (item) => updateAudience({ ...audience, role: toggle(audience.role, item) })),
1546
+ section("Regions", regions, audience.region, (item) => updateAudience({ ...audience, region: toggle(audience.region, item) }))
1547
+ ] }),
1548
+ onSave && /* @__PURE__ */ jsxRuntime.jsx(
1549
+ "button",
1550
+ {
1551
+ type: "button",
1552
+ style: { marginTop: "10px" },
1553
+ onClick: () => {
1554
+ void onSave(audience);
1555
+ },
1556
+ children: "Save audience"
1557
+ }
1558
+ )
1559
+ ] });
1560
+ }
1561
+ function parseScalar(raw) {
1562
+ const value = raw.trim();
1563
+ if (!value) return "";
1564
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1565
+ return value.slice(1, -1);
1566
+ }
1567
+ if (value === "true") return true;
1568
+ if (value === "false") return false;
1569
+ if (value === "null") return null;
1570
+ if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
1571
+ if (value.startsWith("[") && value.endsWith("]")) {
1572
+ const inner = value.slice(1, -1).trim();
1573
+ if (!inner) return [];
1574
+ return inner.split(",").map((part) => String(parseScalar(part.trim())));
1575
+ }
1576
+ return value;
1577
+ }
1578
+ function parseFrontmatter(raw) {
1579
+ const lines = raw.split(/\r?\n/);
1580
+ const root = {};
1581
+ const stack = [
1582
+ { indent: -1, value: root }
1583
+ ];
1584
+ const isArrayContext = (idx) => {
1585
+ for (let i = idx + 1; i < lines.length; i++) {
1586
+ const line = lines[i];
1587
+ if (!line.trim()) continue;
1588
+ const indent = line.length - line.trimStart().length;
1589
+ if (indent <= lines[idx].length - lines[idx].trimStart().length) return false;
1590
+ return line.trimStart().startsWith("- ");
1591
+ }
1592
+ return false;
1593
+ };
1594
+ for (let i = 0; i < lines.length; i++) {
1595
+ const line = lines[i];
1596
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
1597
+ const indent = line.length - line.trimStart().length;
1598
+ const trimmed = line.trim();
1599
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
1600
+ stack.pop();
1601
+ }
1602
+ const current = stack[stack.length - 1].value;
1603
+ if (trimmed.startsWith("- ")) {
1604
+ if (!Array.isArray(current)) {
1605
+ throw new Error(`Invalid frontmatter list at line ${i + 1}`);
1606
+ }
1607
+ const item = trimmed.slice(2).trim();
1608
+ current.push(parseScalar(item));
1609
+ continue;
1610
+ }
1611
+ const colon = trimmed.indexOf(":");
1612
+ if (colon === -1) {
1613
+ throw new Error(`Invalid frontmatter line ${i + 1}: ${trimmed}`);
1614
+ }
1615
+ const key = trimmed.slice(0, colon).trim();
1616
+ const rest = trimmed.slice(colon + 1).trim();
1617
+ if (Array.isArray(current)) {
1618
+ throw new Error(`Unexpected key in list at line ${i + 1}`);
1619
+ }
1620
+ if (!rest) {
1621
+ const container = isArrayContext(i) ? [] : {};
1622
+ current[key] = container;
1623
+ stack.push({ indent, value: container });
1624
+ continue;
1625
+ }
1626
+ current[key] = parseScalar(rest);
1627
+ }
1628
+ return root;
1629
+ }
1630
+ function splitFrontmatter(markdown) {
1631
+ const normalized = markdown.replace(/\r\n/g, "\n");
1632
+ if (!normalized.startsWith("---\n")) {
1633
+ return { frontmatter: {}, body: normalized.trim() };
1634
+ }
1635
+ const end = normalized.indexOf("\n---\n", 4);
1636
+ if (end === -1) {
1637
+ throw new Error("Frontmatter block is not closed with ---");
1638
+ }
1639
+ const fmRaw = normalized.slice(4, end);
1640
+ const body = normalized.slice(end + 5).trim();
1641
+ return {
1642
+ frontmatter: parseFrontmatter(fmRaw),
1643
+ body
1644
+ };
1645
+ }
1646
+ function asString(value, field, source) {
1647
+ if (typeof value !== "string" || !value.trim()) {
1648
+ throw new Error(`${source}: "${field}" must be a non-empty string`);
1649
+ }
1650
+ return value;
1651
+ }
1652
+ function asOptionalObject(value, field, source) {
1653
+ if (value === void 0) return void 0;
1654
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1655
+ throw new Error(`${source}: "${field}" must be an object`);
1656
+ }
1657
+ return value;
1658
+ }
1659
+ function parseFeatureFile(markdown, source = "feature.md") {
1660
+ const { frontmatter, body } = splitFrontmatter(markdown);
1661
+ const entry = {
1662
+ id: asString(frontmatter.id, "id", source),
1663
+ label: asString(frontmatter.label, "label", source),
1664
+ releasedAt: asString(frontmatter.releasedAt, "releasedAt", source),
1665
+ showNewUntil: asString(frontmatter.showNewUntil, "showNewUntil", source),
1666
+ description: body || void 0
1667
+ };
1668
+ if (frontmatter.sidebarKey !== void 0) entry.sidebarKey = asString(frontmatter.sidebarKey, "sidebarKey", source);
1669
+ if (frontmatter.category !== void 0) entry.category = asString(frontmatter.category, "category", source);
1670
+ if (frontmatter.product !== void 0) entry.product = asString(frontmatter.product, "product", source);
1671
+ if (frontmatter.url !== void 0) entry.url = asString(frontmatter.url, "url", source);
1672
+ if (frontmatter.flagKey !== void 0) entry.flagKey = asString(frontmatter.flagKey, "flagKey", source);
1673
+ if (frontmatter.image !== void 0) entry.image = asString(frontmatter.image, "image", source);
1674
+ if (frontmatter.publishAt !== void 0) entry.publishAt = asString(frontmatter.publishAt, "publishAt", source);
1675
+ if (frontmatter.version !== void 0) {
1676
+ if (typeof frontmatter.version === "string" || typeof frontmatter.version === "object") {
1677
+ entry.version = frontmatter.version;
1678
+ } else {
1679
+ throw new Error(`${source}: "version" must be a string or object`);
1680
+ }
1681
+ }
1682
+ if (frontmatter.type !== void 0) {
1683
+ const type = asString(frontmatter.type, "type", source);
1684
+ if (!["feature", "improvement", "fix", "breaking"].includes(type)) {
1685
+ throw new Error(`${source}: invalid "type" value "${type}"`);
1686
+ }
1687
+ entry.type = type;
1688
+ }
1689
+ if (frontmatter.priority !== void 0) {
1690
+ const priority = asString(frontmatter.priority, "priority", source);
1691
+ if (!["critical", "normal", "low"].includes(priority)) {
1692
+ throw new Error(`${source}: invalid "priority" value "${priority}"`);
1693
+ }
1694
+ entry.priority = priority;
1695
+ }
1696
+ const cta = asOptionalObject(frontmatter.cta, "cta", source);
1697
+ if (cta) {
1698
+ entry.cta = {
1699
+ label: asString(cta.label, "cta.label", source),
1700
+ url: asString(cta.url, "cta.url", source)
1701
+ };
1702
+ }
1703
+ const audience = asOptionalObject(frontmatter.audience, "audience", source);
1704
+ if (audience) {
1705
+ const parsedAudience = {};
1706
+ for (const field of ["plan", "role", "region"]) {
1707
+ const value = audience[field];
1708
+ if (value !== void 0) {
1709
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1710
+ throw new Error(`${source}: "audience.${field}" must be string[]`);
1711
+ }
1712
+ parsedAudience[field] = value;
1713
+ }
1714
+ }
1715
+ if (audience.custom !== void 0) {
1716
+ if (!audience.custom || typeof audience.custom !== "object" || Array.isArray(audience.custom)) {
1717
+ throw new Error(`${source}: "audience.custom" must be an object`);
1718
+ }
1719
+ parsedAudience.custom = audience.custom;
1720
+ }
1721
+ entry.audience = parsedAudience;
1722
+ }
1723
+ return entry;
1724
+ }
1725
+ function normalizePattern(pattern) {
1726
+ const normalized = pattern.replaceAll("\\", "/");
1727
+ if (normalized.endsWith("/**/*.md")) {
1728
+ return {
1729
+ baseDir: normalized.slice(0, -"/**/*.md".length),
1730
+ ext: ".md"
1731
+ };
1732
+ }
1733
+ throw new Error(`Unsupported pattern "${pattern}". Use "features/**/*.md" style patterns.`);
1734
+ }
1735
+ async function collectFiles(dir, ext) {
1736
+ const out = [];
1737
+ async function walk(current) {
1738
+ let entries;
1739
+ try {
1740
+ entries = await promises.readdir(current, { withFileTypes: true });
1741
+ } catch {
1742
+ return;
1743
+ }
1744
+ for (const entry of entries) {
1745
+ const fullPath = path.join(current, entry.name);
1746
+ if (entry.isDirectory()) {
1747
+ await walk(fullPath);
1748
+ continue;
1749
+ }
1750
+ if (entry.isFile() && entry.name.endsWith(ext)) {
1751
+ out.push(fullPath);
1752
+ }
1753
+ }
1754
+ }
1755
+ await walk(dir);
1756
+ return out.sort();
1757
+ }
1758
+ async function buildManifestFromPattern(options = {}) {
1759
+ const cwd = options.cwd ?? process.cwd();
1760
+ const pattern = options.pattern ?? "features/**/*.md";
1761
+ const { baseDir, ext } = normalizePattern(pattern);
1762
+ const baseAbs = path.join(cwd, baseDir);
1763
+ const stats = await promises.stat(baseAbs).catch(() => null);
1764
+ if (!stats || !stats.isDirectory()) {
1765
+ throw new Error(`Pattern base directory does not exist: ${baseDir}`);
1766
+ }
1767
+ const files = await collectFiles(baseAbs, ext);
1768
+ const entries = [];
1769
+ const seenIds = /* @__PURE__ */ new Set();
1770
+ for (const file of files) {
1771
+ const content = await promises.readFile(file, "utf8");
1772
+ const source = path.relative(cwd, file).split(path.sep).join("/");
1773
+ const entry = parseFeatureFile(content, source);
1774
+ if (seenIds.has(entry.id)) {
1775
+ throw new Error(`Duplicate feature id "${entry.id}" found at ${source}`);
1776
+ }
1777
+ seenIds.add(entry.id);
1778
+ entries.push(entry);
1779
+ }
1780
+ if (options.outFile) {
1781
+ const outPath = path.join(cwd, options.outFile);
1782
+ await promises.writeFile(outPath, `${JSON.stringify(entries, null, 2)}
1783
+ `, "utf8");
1784
+ }
1785
+ return entries;
1786
+ }
1787
+
1788
+ // src/cms.ts
1789
+ var DEFAULT_FIELDS = {
1790
+ id: "id",
1791
+ label: "label",
1792
+ releasedAt: "releasedAt",
1793
+ showNewUntil: "showNewUntil",
1794
+ description: "description",
1795
+ sidebarKey: "sidebarKey",
1796
+ category: "category",
1797
+ product: "product",
1798
+ flagKey: "flagKey",
1799
+ url: "url",
1800
+ image: "image",
1801
+ publishAt: "publishAt",
1802
+ type: "type",
1803
+ priority: "priority",
1804
+ ctaLabel: "cta.label",
1805
+ ctaUrl: "cta.url"
1806
+ };
1807
+ function getByPath(record, path) {
1808
+ const parts = path.split(".").filter(Boolean);
1809
+ let cursor = record;
1810
+ for (const part of parts) {
1811
+ if (!cursor || typeof cursor !== "object") return void 0;
1812
+ cursor = cursor[part];
1813
+ }
1814
+ return cursor;
1815
+ }
1816
+ function normalizeLocalizedValue(value) {
1817
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1818
+ const objectValue = value;
1819
+ const keys = Object.keys(objectValue);
1820
+ if (keys.length === 1) {
1821
+ const nested = objectValue[keys[0]];
1822
+ if (typeof nested === "string" || typeof nested === "number" || typeof nested === "boolean" || nested == null) {
1823
+ return nested;
1824
+ }
1825
+ }
1826
+ }
1827
+ return value;
1828
+ }
1829
+ function resolveField(record, resolver) {
1830
+ if (!resolver) return void 0;
1831
+ if (typeof resolver === "function") return resolver(record);
1832
+ return normalizeLocalizedValue(getByPath(record, resolver));
1833
+ }
1834
+ function asString2(value) {
1835
+ if (typeof value === "string") {
1836
+ const trimmed = value.trim();
1837
+ return trimmed ? trimmed : void 0;
1838
+ }
1839
+ if (typeof value === "number" || typeof value === "boolean") {
1840
+ return String(value);
1841
+ }
1842
+ return void 0;
1843
+ }
1844
+ function normalizeFieldMapping(defaults, overrides) {
1845
+ return {
1846
+ ...defaults,
1847
+ ...overrides
1848
+ };
1849
+ }
1850
+ function validateMappedEntries(entries, options) {
1851
+ const strictValidation = options?.strictValidation ?? false;
1852
+ const validEntries = [];
1853
+ const seenIds = /* @__PURE__ */ new Set();
1854
+ const errors = [];
1855
+ for (const entry of entries) {
1856
+ if (seenIds.has(entry.id)) {
1857
+ errors.push(`${entry.id}: duplicate id`);
1858
+ continue;
1859
+ }
1860
+ seenIds.add(entry.id);
1861
+ const validation = validateManifest([entry]);
1862
+ if (validation.valid) {
1863
+ validEntries.push(entry);
1864
+ continue;
1865
+ }
1866
+ const reason = validation.errors.map((error) => `${error.path} ${error.message}`).join("; ");
1867
+ errors.push(`${entry.id}: ${reason}`);
1868
+ }
1869
+ if (errors.length > 0) {
1870
+ if (strictValidation) {
1871
+ throw new Error(`[featuredrop] CMS mapping validation failed: ${errors.join(" | ")}`);
1872
+ }
1873
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
1874
+ console.warn(`[featuredrop] Skipped ${errors.length} invalid CMS entries.`);
1875
+ }
1876
+ }
1877
+ return validEntries;
1878
+ }
1879
+ function mapRecordToFeatureEntry(record, mapping) {
1880
+ const id = asString2(resolveField(record, mapping.id));
1881
+ const label = asString2(resolveField(record, mapping.label));
1882
+ const releasedAt = asString2(resolveField(record, mapping.releasedAt));
1883
+ const showNewUntil = asString2(resolveField(record, mapping.showNewUntil));
1884
+ if (!id || !label || !releasedAt || !showNewUntil) return null;
1885
+ const entry = {
1886
+ id,
1887
+ label,
1888
+ releasedAt,
1889
+ showNewUntil
1890
+ };
1891
+ const description = asString2(resolveField(record, mapping.description));
1892
+ if (description) entry.description = description;
1893
+ const sidebarKey = asString2(resolveField(record, mapping.sidebarKey));
1894
+ if (sidebarKey) entry.sidebarKey = sidebarKey;
1895
+ const category = asString2(resolveField(record, mapping.category));
1896
+ if (category) entry.category = category;
1897
+ const product = asString2(resolveField(record, mapping.product));
1898
+ if (product) entry.product = product;
1899
+ const flagKey = asString2(resolveField(record, mapping.flagKey));
1900
+ if (flagKey) entry.flagKey = flagKey;
1901
+ const url = asString2(resolveField(record, mapping.url));
1902
+ if (url) entry.url = url;
1903
+ const image = asString2(resolveField(record, mapping.image));
1904
+ if (image) entry.image = image;
1905
+ const publishAt = asString2(resolveField(record, mapping.publishAt));
1906
+ if (publishAt) entry.publishAt = publishAt;
1907
+ const type = asString2(resolveField(record, mapping.type));
1908
+ if (type && ["feature", "improvement", "fix", "breaking"].includes(type)) {
1909
+ entry.type = type;
1910
+ }
1911
+ const priority = asString2(resolveField(record, mapping.priority));
1912
+ if (priority && ["critical", "normal", "low"].includes(priority)) {
1913
+ entry.priority = priority;
1914
+ }
1915
+ const ctaLabel = asString2(resolveField(record, mapping.ctaLabel));
1916
+ const ctaUrl = asString2(resolveField(record, mapping.ctaUrl));
1917
+ if (ctaLabel && ctaUrl) {
1918
+ entry.cta = { label: ctaLabel, url: ctaUrl };
1919
+ }
1920
+ return entry;
1921
+ }
1922
+ async function fetchJson(input, init) {
1923
+ const response = await fetch(input, init);
1924
+ if (!response.ok) {
1925
+ throw new Error(`[featuredrop] CMS request failed (${response.status}) for ${input}`);
1926
+ }
1927
+ return response.json();
1928
+ }
1929
+ var ContentfulAdapter = class {
1930
+ options;
1931
+ constructor(options) {
1932
+ this.options = options;
1933
+ }
1934
+ async load() {
1935
+ const environment = this.options.environment ?? "master";
1936
+ const params = new URLSearchParams({
1937
+ content_type: this.options.contentType,
1938
+ limit: String(this.options.limit ?? 1e3)
1939
+ });
1940
+ if (this.options.locale) {
1941
+ params.set("locale", this.options.locale);
1942
+ }
1943
+ const url = `https://cdn.contentful.com/spaces/${encodeURIComponent(this.options.spaceId)}/environments/${encodeURIComponent(environment)}/entries?${params.toString()}`;
1944
+ const payload = await fetchJson(url, {
1945
+ headers: {
1946
+ Authorization: `Bearer ${this.options.accessToken}`
1947
+ }
1948
+ });
1949
+ const mapping = normalizeFieldMapping(
1950
+ {
1951
+ ...DEFAULT_FIELDS,
1952
+ id: "sys.id",
1953
+ label: "fields.label",
1954
+ description: "fields.description",
1955
+ releasedAt: "fields.releasedAt",
1956
+ showNewUntil: "fields.showNewUntil",
1957
+ sidebarKey: "fields.sidebarKey",
1958
+ category: "fields.category",
1959
+ product: "fields.product",
1960
+ flagKey: "fields.flagKey",
1961
+ url: "fields.url",
1962
+ image: "fields.image",
1963
+ publishAt: "fields.publishAt",
1964
+ type: "fields.type",
1965
+ priority: "fields.priority",
1966
+ ctaLabel: "fields.ctaLabel",
1967
+ ctaUrl: "fields.ctaUrl"
1968
+ },
1969
+ this.options.fieldMapping
1970
+ );
1971
+ const entries = (payload.items ?? []).map((item) => mapRecordToFeatureEntry(item, mapping)).filter((entry) => entry !== null);
1972
+ return validateMappedEntries(entries, this.options);
1973
+ }
1974
+ };
1975
+ var SanityAdapter = class {
1976
+ options;
1977
+ constructor(options) {
1978
+ this.options = options;
1979
+ }
1980
+ async load() {
1981
+ const version = this.options.apiVersion ?? "v2023-10-01";
1982
+ const queryParam = encodeURIComponent(this.options.query);
1983
+ const url = `https://${encodeURIComponent(this.options.projectId)}.api.sanity.io/${version}/data/query/${encodeURIComponent(this.options.dataset)}?query=${queryParam}`;
1984
+ const headers = {};
1985
+ if (this.options.token) {
1986
+ headers.Authorization = `Bearer ${this.options.token}`;
1987
+ }
1988
+ const payload = await fetchJson(url, {
1989
+ headers
1990
+ });
1991
+ const mapping = normalizeFieldMapping(
1992
+ {
1993
+ ...DEFAULT_FIELDS,
1994
+ id: "_id"
1995
+ },
1996
+ this.options.fieldMapping
1997
+ );
1998
+ const entries = (payload.result ?? []).map((item) => mapRecordToFeatureEntry(item, mapping)).filter((entry) => entry !== null);
1999
+ return validateMappedEntries(entries, this.options);
2000
+ }
2001
+ };
2002
+ var StrapiAdapter = class {
2003
+ options;
2004
+ constructor(options) {
2005
+ this.options = options;
2006
+ }
2007
+ async load() {
2008
+ const endpoint = this.options.endpoint ?? "/api/features";
2009
+ const base = this.options.baseUrl.replace(/\/+$/, "");
2010
+ const query = this.options.query ? `?${this.options.query}` : "";
2011
+ const url = `${base}${endpoint}${query}`;
2012
+ const headers = {};
2013
+ if (this.options.token) {
2014
+ headers.Authorization = `Bearer ${this.options.token}`;
2015
+ }
2016
+ const payload = await fetchJson(url, { headers });
2017
+ const mapping = normalizeFieldMapping(DEFAULT_FIELDS, this.options.fieldMapping);
2018
+ const entries = (payload.data ?? []).map((item) => {
2019
+ if (!item || typeof item !== "object") return null;
2020
+ const record = item;
2021
+ const attributes = record.attributes;
2022
+ if (attributes && typeof attributes === "object") {
2023
+ return mapRecordToFeatureEntry(
2024
+ { id: record.id, ...attributes },
2025
+ mapping
2026
+ );
2027
+ }
2028
+ return mapRecordToFeatureEntry(record, mapping);
2029
+ }).filter((entry) => entry !== null);
2030
+ return validateMappedEntries(entries, this.options);
2031
+ }
2032
+ };
2033
+ function notionPropertyToValue(property) {
2034
+ if (!property || typeof property !== "object") return void 0;
2035
+ const typed = property;
2036
+ const type = typed.type;
2037
+ if (typeof type !== "string") return void 0;
2038
+ const value = typed[type];
2039
+ if (type === "title" || type === "rich_text") {
2040
+ if (!Array.isArray(value)) return void 0;
2041
+ return value.map((item) => {
2042
+ if (!item || typeof item !== "object") return "";
2043
+ return asString2(item.plain_text) ?? "";
2044
+ }).join("").trim();
2045
+ }
2046
+ if (type === "select") {
2047
+ if (!value || typeof value !== "object") return void 0;
2048
+ return asString2(value.name);
2049
+ }
2050
+ if (type === "multi_select") {
2051
+ if (!Array.isArray(value)) return void 0;
2052
+ return value.map((item) => item && typeof item === "object" ? asString2(item.name) : void 0).filter((item) => Boolean(item));
2053
+ }
2054
+ if (type === "date") {
2055
+ if (!value || typeof value !== "object") return void 0;
2056
+ return asString2(value.start);
2057
+ }
2058
+ if (type === "number" || type === "url" || type === "email" || type === "phone_number") {
2059
+ return value;
2060
+ }
2061
+ if (type === "checkbox") {
2062
+ return value === true ? "true" : value === false ? "false" : void 0;
2063
+ }
2064
+ return void 0;
2065
+ }
2066
+ function flattenNotionPage(page) {
2067
+ if (!page || typeof page !== "object") return {};
2068
+ const record = page;
2069
+ const properties = record.properties;
2070
+ const flattened = {
2071
+ id: record.id
2072
+ };
2073
+ if (properties && typeof properties === "object") {
2074
+ Object.entries(properties).forEach(([key, value]) => {
2075
+ flattened[key] = notionPropertyToValue(value);
2076
+ });
2077
+ }
2078
+ return flattened;
2079
+ }
2080
+ var NotionAdapter = class {
2081
+ options;
2082
+ constructor(options) {
2083
+ this.options = options;
2084
+ }
2085
+ async load() {
2086
+ const body = {};
2087
+ if (this.options.filter) body.filter = this.options.filter;
2088
+ if (this.options.sorts) body.sorts = this.options.sorts;
2089
+ const payload = await fetchJson(
2090
+ `https://api.notion.com/v1/databases/${encodeURIComponent(this.options.databaseId)}/query`,
2091
+ {
2092
+ method: "POST",
2093
+ headers: {
2094
+ "Content-Type": "application/json",
2095
+ Authorization: `Bearer ${this.options.token}`,
2096
+ "Notion-Version": this.options.notionVersion ?? "2022-06-28"
2097
+ },
2098
+ body: JSON.stringify(body)
2099
+ }
2100
+ );
2101
+ const mapping = normalizeFieldMapping(DEFAULT_FIELDS, this.options.fieldMapping);
2102
+ const entries = (payload.results ?? []).map((page) => mapRecordToFeatureEntry(flattenNotionPage(page), mapping)).filter((entry) => entry !== null);
2103
+ return validateMappedEntries(entries, this.options);
2104
+ }
2105
+ };
2106
+ var MarkdownAdapter = class {
2107
+ options;
2108
+ constructor(options = {}) {
2109
+ this.options = options;
2110
+ }
2111
+ async load() {
2112
+ if (this.options.pattern) {
2113
+ const entries2 = await buildManifestFromPattern({
2114
+ cwd: this.options.cwd,
2115
+ pattern: this.options.pattern
2116
+ });
2117
+ return validateMappedEntries(entries2, this.options);
2118
+ }
2119
+ const entries = this.options.entries ?? [];
2120
+ const mapped = entries.map((entry, index) => {
2121
+ const source = entry.source ?? `feature-${index + 1}.md`;
2122
+ return parseFeatureFile(entry.markdown, source);
2123
+ });
2124
+ return validateMappedEntries(mapped, this.options);
2125
+ }
2126
+ };
2127
+
2128
+ // src/theme.ts
2129
+ var LIGHT_THEME = {
2130
+ colors: {
2131
+ primary: "#2563eb",
2132
+ background: "#ffffff",
2133
+ text: "#111827",
2134
+ textMuted: "#6b7280",
2135
+ border: "#e5e7eb",
2136
+ success: "#16a34a",
2137
+ warning: "#f59e0b",
2138
+ error: "#dc2626"
2139
+ },
2140
+ fonts: {
2141
+ family: "system-ui, -apple-system, Segoe UI, sans-serif",
2142
+ sizeBase: "14px",
2143
+ sizeSm: "12px",
2144
+ sizeLg: "16px"
2145
+ },
2146
+ spacing: {
2147
+ xs: "4px",
2148
+ sm: "8px",
2149
+ md: "12px",
2150
+ lg: "16px",
2151
+ xl: "24px"
2152
+ },
2153
+ radii: {
2154
+ sm: "6px",
2155
+ md: "8px",
2156
+ lg: "12px",
2157
+ full: "999px"
2158
+ },
2159
+ shadows: {
2160
+ sm: "0 2px 8px rgba(0, 0, 0, 0.08)",
2161
+ md: "0 8px 24px rgba(0, 0, 0, 0.12)",
2162
+ lg: "0 20px 60px rgba(0, 0, 0, 0.16)"
2163
+ },
2164
+ zIndex: {
2165
+ base: 9998,
2166
+ tooltip: 1e4,
2167
+ modal: 10001,
2168
+ overlay: 9997
2169
+ }
2170
+ };
2171
+ var DARK_THEME = {
2172
+ ...LIGHT_THEME,
2173
+ colors: {
2174
+ primary: "#60a5fa",
2175
+ background: "#0b1220",
2176
+ text: "#f3f4f6",
2177
+ textMuted: "#9ca3af",
2178
+ border: "#1f2937",
2179
+ success: "#4ade80",
2180
+ warning: "#fbbf24",
2181
+ error: "#f87171"
2182
+ },
2183
+ shadows: {
2184
+ sm: "0 2px 8px rgba(0, 0, 0, 0.35)",
2185
+ md: "0 8px 24px rgba(0, 0, 0, 0.42)",
2186
+ lg: "0 20px 60px rgba(0, 0, 0, 0.52)"
2187
+ }
2188
+ };
2189
+ var MINIMAL_THEME = {
2190
+ ...LIGHT_THEME,
2191
+ colors: {
2192
+ ...LIGHT_THEME.colors,
2193
+ primary: "#111827",
2194
+ background: "#ffffff",
2195
+ text: "#111827",
2196
+ textMuted: "#6b7280",
2197
+ border: "#d1d5db",
2198
+ success: "#111827",
2199
+ warning: "#111827",
2200
+ error: "#111827"
2201
+ },
2202
+ shadows: {
2203
+ sm: "none",
2204
+ md: "none",
2205
+ lg: "none"
2206
+ },
2207
+ radii: {
2208
+ sm: "0",
2209
+ md: "0",
2210
+ lg: "0",
2211
+ full: "0"
2212
+ }
2213
+ };
2214
+ var VIBRANT_THEME = {
2215
+ ...LIGHT_THEME,
2216
+ colors: {
2217
+ primary: "#ec4899",
2218
+ background: "#fff7ed",
2219
+ text: "#3f1d57",
2220
+ textMuted: "#6d4c84",
2221
+ border: "#fdba74",
2222
+ success: "#10b981",
2223
+ warning: "#f59e0b",
2224
+ error: "#ef4444"
2225
+ },
2226
+ shadows: {
2227
+ sm: "0 2px 10px rgba(236, 72, 153, 0.15)",
2228
+ md: "0 10px 26px rgba(236, 72, 153, 0.22)",
2229
+ lg: "0 22px 58px rgba(236, 72, 153, 0.28)"
2230
+ }
2231
+ };
2232
+ var FEATUREDROP_THEMES = {
2233
+ light: LIGHT_THEME,
2234
+ dark: DARK_THEME,
2235
+ minimal: MINIMAL_THEME,
2236
+ vibrant: VIBRANT_THEME
2237
+ };
2238
+ function isThemePreset(value) {
2239
+ return value === "light" || value === "dark" || value === "auto" || value === "minimal" || value === "vibrant";
2240
+ }
2241
+ function mergeTheme(base, overrides) {
2242
+ if (!overrides) return base;
2243
+ return {
2244
+ colors: {
2245
+ ...base.colors,
2246
+ ...overrides.colors ?? {}
2247
+ },
2248
+ fonts: {
2249
+ ...base.fonts,
2250
+ ...overrides.fonts ?? {}
2251
+ },
2252
+ spacing: {
2253
+ ...base.spacing,
2254
+ ...overrides.spacing ?? {}
2255
+ },
2256
+ radii: {
2257
+ ...base.radii,
2258
+ ...overrides.radii ?? {}
2259
+ },
2260
+ shadows: {
2261
+ ...base.shadows,
2262
+ ...overrides.shadows ?? {}
2263
+ },
2264
+ zIndex: {
2265
+ ...base.zIndex,
2266
+ ...overrides.zIndex ?? {}
2267
+ }
2268
+ };
2269
+ }
2270
+ function createTheme(overrides, base = LIGHT_THEME) {
2271
+ return mergeTheme(base, overrides);
2272
+ }
2273
+ function resolveTheme(input = "light", options = {}) {
2274
+ if (isThemePreset(input)) {
2275
+ if (input === "auto") {
2276
+ return options.prefersDark ? DARK_THEME : LIGHT_THEME;
2277
+ }
2278
+ return FEATUREDROP_THEMES[input];
2279
+ }
2280
+ return mergeTheme(LIGHT_THEME, input);
2281
+ }
2282
+ function applyThemeSection(vars, key, values) {
2283
+ for (const [token, value] of Object.entries(values)) {
2284
+ vars[`--featuredrop-${key}-${token}`] = value;
2285
+ }
2286
+ }
2287
+ function themeToCSSVariables(theme) {
2288
+ const vars = {};
2289
+ applyThemeSection(vars, "color", theme.colors);
2290
+ applyThemeSection(vars, "font", theme.fonts);
2291
+ applyThemeSection(vars, "space", theme.spacing);
2292
+ applyThemeSection(vars, "radius", theme.radii);
2293
+ applyThemeSection(vars, "shadow", theme.shadows);
2294
+ applyThemeSection(vars, "z", theme.zIndex);
2295
+ vars["--featuredrop-font-family"] = theme.fonts.family;
2296
+ vars["--featuredrop-widget-bg"] = theme.colors.background;
2297
+ vars["--featuredrop-trigger-bg"] = theme.colors.background;
2298
+ vars["--featuredrop-trigger-color"] = theme.colors.text;
2299
+ vars["--featuredrop-entry-title-color"] = theme.colors.text;
2300
+ vars["--featuredrop-entry-desc-color"] = theme.colors.textMuted;
2301
+ vars["--featuredrop-title-color"] = theme.colors.text;
2302
+ vars["--featuredrop-border-color"] = theme.colors.border;
2303
+ vars["--featuredrop-cta-bg"] = theme.colors.primary;
2304
+ vars["--featuredrop-cta-color"] = theme.colors.background;
2305
+ vars["--featuredrop-mark-all-color"] = theme.colors.primary;
2306
+ vars["--featuredrop-widget-shadow"] = theme.shadows.md;
2307
+ vars["--featuredrop-widget-radius"] = theme.radii.lg;
2308
+ vars["--featuredrop-trigger-radius"] = theme.radii.md;
2309
+ vars["--featuredrop-badge-bg"] = theme.colors.warning;
2310
+ vars["--featuredrop-z-index"] = theme.zIndex.base;
2311
+ vars["--featuredrop-toast-z-index"] = theme.zIndex.tooltip;
2312
+ vars["--featuredrop-tour-z-index"] = theme.zIndex.modal;
2313
+ vars["--featuredrop-tour-overlay-z-index"] = theme.zIndex.overlay;
2314
+ return vars;
2315
+ }
2316
+
2317
+ // src/i18n.ts
2318
+ var EN_TRANSLATIONS = {
2319
+ newBadge: "New",
2320
+ whatsNewTitle: "What's New",
2321
+ markAllRead: "Mark all as read",
2322
+ allCaughtUp: "You're all caught up!",
2323
+ close: "Close",
2324
+ changelogTitle: "Changelog",
777
2325
  searchPlaceholder: "Search updates",
778
2326
  allCategories: "All categories",
779
2327
  noUpdatesYet: "No updates yet",
780
2328
  loadMore: "Load more",
781
2329
  share: "Share",
782
2330
  skipToEntries: "Skip to changelog entries",
2331
+ newFeatureCount: (count) => count === 0 ? "No new features" : `${count} new feature${count === 1 ? "" : "s"}`,
783
2332
  stepOf: (current, total) => `Step ${current} of ${total}`,
784
2333
  back: "Back",
785
2334
  next: "Next",
@@ -1030,14 +2579,128 @@ var SIMPLE_TRANSLATIONS = {
1030
2579
  askLater: "\u092C\u093E\u0926 \u092E\u0947\u0902 \u092A\u0942\u091B\u0947\u0902"
1031
2580
  }
1032
2581
  };
2582
+ var RTL_LANGUAGES = /* @__PURE__ */ new Set(["ar", "fa", "he", "ur"]);
2583
+ var STEP_OF_TRANSLATIONS = {
2584
+ en: EN_TRANSLATIONS.stepOf,
2585
+ es: (current, total) => `Paso ${current} de ${total}`,
2586
+ fr: (current, total) => `Etape ${current} sur ${total}`,
2587
+ de: (current, total) => `Schritt ${current} von ${total}`,
2588
+ pt: (current, total) => `Etapa ${current} de ${total}`,
2589
+ "zh-cn": (current, total) => `\u7B2C ${current} / ${total} \u6B65`,
2590
+ ja: (current, total) => `${total}\u4E2D${current}\u756A\u76EE`,
2591
+ ko: (current, total) => `${total}\uB2E8\uACC4 \uC911 ${current}\uB2E8\uACC4`,
2592
+ ar: (current, total) => `\u0627\u0644\u062E\u0637\u0648\u0629 ${current} \u0645\u0646 ${total}`,
2593
+ hi: (current, total) => `${total} \u092E\u0947\u0902 \u0938\u0947 \u091A\u0930\u0923 ${current}`
2594
+ };
2595
+ var NEW_FEATURE_COUNT_TRANSLATIONS = {
2596
+ en: EN_TRANSLATIONS.newFeatureCount,
2597
+ es: (count) => count === 0 ? "Sin novedades" : `${count} novedad${count === 1 ? "" : "es"}`,
2598
+ fr: (count) => count === 0 ? "Aucune nouveaute" : `${count} nouveaute${count === 1 ? "" : "s"}`,
2599
+ de: (count) => count === 0 ? "Keine neuen Features" : `${count} ${count === 1 ? "neues Feature" : "neue Features"}`,
2600
+ pt: (count) => count === 0 ? "Sem novidades" : `${count} novidade${count === 1 ? "" : "s"}`,
2601
+ "zh-cn": (count) => count === 0 ? "\u6682\u65E0\u66F4\u65B0" : `${count} \u6761\u65B0\u66F4\u65B0`,
2602
+ ja: (count) => count === 0 ? "\u65B0\u7740\u306F\u3042\u308A\u307E\u305B\u3093" : `\u65B0\u7740 ${count} \u4EF6`,
2603
+ ko: (count) => count === 0 ? "\uC0C8 \uC18C\uC2DD \uC5C6\uC74C" : `\uC0C8 \uC18C\uC2DD ${count}\uAC1C`,
2604
+ ar: (count) => {
2605
+ if (count === 0) return "\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u064A\u0632\u0627\u062A \u062C\u062F\u064A\u062F\u0629";
2606
+ const category = new Intl.PluralRules("ar").select(count);
2607
+ if (category === "one") return "\u0645\u064A\u0632\u0629 \u062C\u062F\u064A\u062F\u0629 \u0648\u0627\u062D\u062F\u0629";
2608
+ if (category === "two") return "\u0645\u064A\u0632\u062A\u0627\u0646 \u062C\u062F\u064A\u062F\u062A\u0627\u0646";
2609
+ return `${count} \u0645\u064A\u0632\u0627\u062A \u062C\u062F\u064A\u062F\u0629`;
2610
+ },
2611
+ 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"}`
2612
+ };
2613
+ function resolveLocale(locale) {
2614
+ const normalized = (locale ?? "en").toLowerCase();
2615
+ if (normalized === "en" || normalized.startsWith("en-")) return "en";
2616
+ if (Object.prototype.hasOwnProperty.call(SIMPLE_TRANSLATIONS, normalized)) {
2617
+ return normalized;
2618
+ }
2619
+ const base = normalized.split("-")[0];
2620
+ if (base === "en") return "en";
2621
+ if (Object.prototype.hasOwnProperty.call(SIMPLE_TRANSLATIONS, base)) {
2622
+ return base;
2623
+ }
2624
+ return "en";
2625
+ }
2626
+ function getLocaleDirection(locale) {
2627
+ const resolved = resolveLocale(locale);
2628
+ const base = resolved.split("-")[0];
2629
+ return RTL_LANGUAGES.has(base) ? "rtl" : "ltr";
2630
+ }
2631
+ function formatDateForLocale(value, locale, options = {
2632
+ month: "short",
2633
+ day: "numeric",
2634
+ year: "numeric"
2635
+ }) {
2636
+ const date = value instanceof Date ? value : new Date(value);
2637
+ if (Number.isNaN(date.getTime())) return "";
2638
+ const resolved = resolveLocale(locale);
2639
+ try {
2640
+ return new Intl.DateTimeFormat(resolved, options).format(date);
2641
+ } catch {
2642
+ return date.toLocaleDateString(void 0, options);
2643
+ }
2644
+ }
2645
+ function formatRelativeTimeForLocale(value, locale, options) {
2646
+ const target = value instanceof Date ? value : new Date(value);
2647
+ if (Number.isNaN(target.getTime())) return "";
2648
+ const nowInput = options?.now;
2649
+ const nowDate = nowInput instanceof Date ? nowInput : typeof nowInput !== "undefined" ? new Date(nowInput) : /* @__PURE__ */ new Date();
2650
+ if (Number.isNaN(nowDate.getTime())) return "";
2651
+ const diffMs = target.getTime() - nowDate.getTime();
2652
+ const absDiff = Math.abs(diffMs);
2653
+ const minute = 6e4;
2654
+ const hour = 60 * minute;
2655
+ const day = 24 * hour;
2656
+ const week = 7 * day;
2657
+ const month = 30 * day;
2658
+ const year = 365 * day;
2659
+ let unit = "second";
2660
+ let divisor = 1e3;
2661
+ if (absDiff >= year) {
2662
+ unit = "year";
2663
+ divisor = year;
2664
+ } else if (absDiff >= month) {
2665
+ unit = "month";
2666
+ divisor = month;
2667
+ } else if (absDiff >= week) {
2668
+ unit = "week";
2669
+ divisor = week;
2670
+ } else if (absDiff >= day) {
2671
+ unit = "day";
2672
+ divisor = day;
2673
+ } else if (absDiff >= hour) {
2674
+ unit = "hour";
2675
+ divisor = hour;
2676
+ } else if (absDiff >= minute) {
2677
+ unit = "minute";
2678
+ divisor = minute;
2679
+ }
2680
+ const relativeValue = Math.round(diffMs / divisor);
2681
+ const resolvedLocale = resolveLocale(locale);
2682
+ try {
2683
+ const formatter = new Intl.RelativeTimeFormat(resolvedLocale, {
2684
+ numeric: options?.numeric ?? "auto",
2685
+ style: options?.style ?? "long"
2686
+ });
2687
+ return formatter.format(relativeValue, unit);
2688
+ } catch {
2689
+ const fallback = formatDateForLocale(target, resolvedLocale);
2690
+ return fallback || target.toISOString();
2691
+ }
2692
+ }
1033
2693
  function resolveTranslations(locale, overrides) {
1034
- const normalizedLocale = (locale ?? "en").toLowerCase();
1035
- const base = SIMPLE_TRANSLATIONS[normalizedLocale] ?? SIMPLE_TRANSLATIONS[normalizedLocale.split("-")[0]] ?? {};
2694
+ const resolvedLocale = resolveLocale(locale);
2695
+ const base = resolvedLocale === "en" ? {} : SIMPLE_TRANSLATIONS[resolvedLocale] ?? {};
2696
+ const stepOf = overrides?.stepOf ?? STEP_OF_TRANSLATIONS[resolvedLocale] ?? STEP_OF_TRANSLATIONS.en;
2697
+ const newFeatureCount = overrides?.newFeatureCount ?? NEW_FEATURE_COUNT_TRANSLATIONS[resolvedLocale] ?? NEW_FEATURE_COUNT_TRANSLATIONS.en;
1036
2698
  return {
1037
2699
  ...EN_TRANSLATIONS,
1038
2700
  ...base,
1039
2701
  ...overrides ?? {},
1040
- stepOf: overrides?.stepOf ?? EN_TRANSLATIONS.stepOf
2702
+ stepOf,
2703
+ newFeatureCount
1041
2704
  };
1042
2705
  }
1043
2706
  var FEATUREDROP_TRANSLATIONS = {
@@ -1045,38 +2708,71 @@ var FEATUREDROP_TRANSLATIONS = {
1045
2708
  ...SIMPLE_TRANSLATIONS
1046
2709
  };
1047
2710
 
1048
- // src/rss.ts
1049
- function escape(str) {
1050
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2711
+ // src/animation.ts
2712
+ var FEATUREDROP_ANIMATION_PRESETS = [
2713
+ "none",
2714
+ "subtle",
2715
+ "normal",
2716
+ "playful"
2717
+ ];
2718
+ function resolveAnimationPreset(preset = "normal", options) {
2719
+ if (options?.reducedMotion) return "none";
2720
+ return preset;
1051
2721
  }
1052
- function generateRSS(manifest, options) {
1053
- const title = escape(options?.title ?? "Featuredrop Changelog");
1054
- const link = escape(options?.link ?? "");
1055
- const desc = escape(options?.description ?? "Product updates");
1056
- const items = manifest.slice().sort((a, b) => new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime()).map((item) => {
1057
- const descriptionHtml = item.description ? parseDescription(item.description) : "";
1058
- const itemLink = item.url ? escape(item.url) : "";
1059
- return [
1060
- "<item>",
1061
- `<title>${escape(item.label)}</title>`,
1062
- itemLink ? `<link>${itemLink}</link>` : "",
1063
- `<guid isPermaLink="false">${escape(item.id)}</guid>`,
1064
- `<pubDate>${new Date(item.releasedAt).toUTCString()}</pubDate>`,
1065
- `<description><![CDATA[${descriptionHtml}]]></description>`,
1066
- "</item>"
1067
- ].join("");
1068
- }).join("");
1069
- return [
1070
- '<?xml version="1.0" encoding="UTF-8"?>',
1071
- '<rss version="2.0">',
1072
- "<channel>",
1073
- `<title>${title}</title>`,
1074
- link ? `<link>${link}</link>` : "",
1075
- `<description>${desc}</description>`,
1076
- items,
1077
- "</channel>",
1078
- "</rss>"
1079
- ].join("");
2722
+ function getEnterAnimation(preset, surface) {
2723
+ if (preset === "none") return void 0;
2724
+ if (preset === "subtle") {
2725
+ if (surface === "panel") return "featuredrop-enter-panel 180ms ease-out";
2726
+ if (surface === "modal") return "featuredrop-enter-scale 180ms ease-out";
2727
+ return "featuredrop-enter-fade-up 170ms ease-out";
2728
+ }
2729
+ if (preset === "playful") {
2730
+ if (surface === "panel") return "featuredrop-enter-panel 320ms cubic-bezier(0.2, 0.9, 0.2, 1)";
2731
+ return "featuredrop-enter-pop 300ms cubic-bezier(0.22, 1.4, 0.36, 1)";
2732
+ }
2733
+ if (surface === "panel") return "featuredrop-enter-panel 240ms cubic-bezier(0.2, 0.9, 0.2, 1)";
2734
+ if (surface === "modal") return "featuredrop-enter-scale 220ms cubic-bezier(0.2, 0.9, 0.2, 1)";
2735
+ return "featuredrop-enter-fade-up 210ms cubic-bezier(0.2, 0.9, 0.2, 1)";
2736
+ }
2737
+ function getExitAnimation(preset, surface) {
2738
+ if (preset === "none") return void 0;
2739
+ if (preset === "subtle") {
2740
+ if (surface === "panel") return "featuredrop-exit-panel 150ms ease-in forwards";
2741
+ if (surface === "modal") return "featuredrop-exit-scale 150ms ease-in forwards";
2742
+ return "featuredrop-exit-fade-down 140ms ease-in forwards";
2743
+ }
2744
+ if (preset === "playful") {
2745
+ if (surface === "panel") return "featuredrop-exit-panel 260ms ease-in forwards";
2746
+ return "featuredrop-exit-pop 240ms ease-in forwards";
2747
+ }
2748
+ if (surface === "panel") return "featuredrop-exit-panel 200ms ease-in forwards";
2749
+ if (surface === "modal") return "featuredrop-exit-scale 190ms ease-in forwards";
2750
+ return "featuredrop-exit-fade-down 180ms ease-in forwards";
2751
+ }
2752
+ function getPulseAnimation(preset, surface = "beacon") {
2753
+ if (preset === "none") return void 0;
2754
+ if (surface === "dot") {
2755
+ if (preset === "subtle") return "featuredrop-pulse 2.6s ease-in-out infinite";
2756
+ if (preset === "playful") {
2757
+ return "featuredrop-pulse-playful 1.8s cubic-bezier(0.22, 1.4, 0.36, 1) infinite";
2758
+ }
2759
+ return "featuredrop-pulse 2s ease-in-out infinite";
2760
+ }
2761
+ if (preset === "subtle") return "featuredrop-beacon-pulse 2.6s ease-in-out infinite";
2762
+ if (preset === "playful") {
2763
+ return "featuredrop-beacon-pop-pulse 1.8s cubic-bezier(0.22, 1.4, 0.36, 1) infinite";
2764
+ }
2765
+ return "featuredrop-beacon-pulse 2s ease-in-out infinite";
2766
+ }
2767
+ function getAnimationDurationMs(preset, surface, phase) {
2768
+ if (preset === "none") return 0;
2769
+ const animation = phase === "enter" ? getEnterAnimation(preset, surface) : getExitAnimation(preset, surface);
2770
+ if (!animation) return 0;
2771
+ const msMatch = animation.match(/(\d+)ms/);
2772
+ if (msMatch?.[1]) return Number(msMatch[1]);
2773
+ const sMatch = animation.match(/(\d+(?:\.\d+)?)s/);
2774
+ if (sMatch?.[1]) return Math.round(Number(sMatch[1]) * 1e3);
2775
+ return 0;
1080
2776
  }
1081
2777
 
1082
2778
  // src/throttle.ts
@@ -1261,170 +2957,66 @@ var SegmentAdapter = class {
1261
2957
  ...event.metadata
1262
2958
  });
1263
2959
  }
1264
- };
1265
- var CustomAdapter = class {
1266
- constructor(handler) {
1267
- this.handler = handler;
1268
- }
1269
- track(event) {
1270
- return this.handler(event);
1271
- }
1272
- };
1273
- function createAdoptionMetrics(events) {
1274
- const getAdoptionRate = (featureId) => {
1275
- const seen = events.filter((event) => event.type === "feature_seen" && event.featureId === featureId).length;
1276
- if (seen === 0) return 0;
1277
- const clicked = events.filter((event) => event.type === "feature_clicked" && event.featureId === featureId).length;
1278
- return clicked / seen;
1279
- };
1280
- const getTourCompletionRate = (tourId) => {
1281
- const started = events.filter((event) => event.type === "tour_started" && event.tourId === tourId).length;
1282
- if (started === 0) return 0;
1283
- const completed = events.filter((event) => event.type === "tour_completed" && event.tourId === tourId).length;
1284
- return completed / started;
1285
- };
1286
- const getChecklistCompletionRate = (checklistId) => {
1287
- const taskCompleted = events.filter(
1288
- (event) => event.type === "checklist_task_completed" && event.metadata?.checklistId === checklistId
1289
- ).length;
1290
- if (taskCompleted === 0) return 0;
1291
- const completed = events.filter(
1292
- (event) => event.type === "checklist_completed" && event.metadata?.checklistId === checklistId
1293
- ).length;
1294
- return completed / taskCompleted;
1295
- };
1296
- const getFeatureEngagement = (featureId) => ({
1297
- seen: events.filter((event) => event.type === "feature_seen" && event.featureId === featureId).length,
1298
- clicked: events.filter((event) => event.type === "feature_clicked" && event.featureId === featureId).length,
1299
- dismissed: events.filter((event) => event.type === "feature_dismissed" && event.featureId === featureId).length
1300
- });
1301
- const getVariantPerformance = (featureId) => {
1302
- const byVariant = /* @__PURE__ */ new Map();
1303
- for (const event of events) {
1304
- if (event.featureId !== featureId) continue;
1305
- const variant = event.variant ?? "control";
1306
- const bucket = byVariant.get(variant) ?? { seen: 0, clicked: 0 };
1307
- if (event.type === "feature_seen") bucket.seen += 1;
1308
- if (event.type === "feature_clicked") bucket.clicked += 1;
1309
- byVariant.set(variant, bucket);
1310
- }
1311
- const output = {};
1312
- for (const [variant, bucket] of byVariant.entries()) {
1313
- output[variant] = bucket.seen === 0 ? 0 : bucket.clicked / bucket.seen;
1314
- }
1315
- return output;
1316
- };
1317
- return {
1318
- getAdoptionRate,
1319
- getTourCompletionRate,
1320
- getChecklistCompletionRate,
1321
- getFeatureEngagement,
1322
- getVariantPerformance
1323
- };
1324
- }
1325
-
1326
- // src/dependencies.ts
1327
- function getDirectDependencies(feature) {
1328
- const dependsOn = feature.dependsOn;
1329
- if (!dependsOn) return [];
1330
- const seen = dependsOn.seen ?? [];
1331
- const clicked = dependsOn.clicked ?? [];
1332
- const dismissed = dependsOn.dismissed ?? [];
1333
- const unique = /* @__PURE__ */ new Set();
1334
- for (const id of [...seen, ...clicked, ...dismissed]) {
1335
- if (id) unique.add(id);
1336
- }
1337
- return Array.from(unique);
1338
- }
1339
- function resolveDependencyOrder(manifest) {
1340
- const ids = new Set(manifest.map((feature) => feature.id));
1341
- const outgoing = /* @__PURE__ */ new Map();
1342
- const indegree = /* @__PURE__ */ new Map();
1343
- for (const feature of manifest) {
1344
- outgoing.set(feature.id, /* @__PURE__ */ new Set());
1345
- indegree.set(feature.id, 0);
1346
- }
1347
- for (const feature of manifest) {
1348
- for (const dependencyId of getDirectDependencies(feature)) {
1349
- if (!ids.has(dependencyId)) continue;
1350
- const edges = outgoing.get(dependencyId);
1351
- if (!edges || edges.has(feature.id)) continue;
1352
- edges.add(feature.id);
1353
- indegree.set(feature.id, (indegree.get(feature.id) ?? 0) + 1);
1354
- }
1355
- }
1356
- const queue = [];
1357
- for (const feature of manifest) {
1358
- if ((indegree.get(feature.id) ?? 0) === 0) queue.push(feature.id);
1359
- }
1360
- const ordered = [];
1361
- while (queue.length > 0) {
1362
- const id = queue.shift();
1363
- if (!id) continue;
1364
- ordered.push(id);
1365
- const edges = outgoing.get(id);
1366
- if (!edges) continue;
1367
- for (const nextId of edges) {
1368
- const nextDegree = (indegree.get(nextId) ?? 0) - 1;
1369
- indegree.set(nextId, nextDegree);
1370
- if (nextDegree === 0) queue.push(nextId);
1371
- }
1372
- }
1373
- if (ordered.length < manifest.length) {
1374
- const included = new Set(ordered);
1375
- for (const feature of manifest) {
1376
- if (included.has(feature.id)) continue;
1377
- ordered.push(feature.id);
1378
- }
1379
- }
1380
- return ordered;
1381
- }
1382
- function hasDependencyCycle(manifest) {
1383
- const ids = new Set(manifest.map((feature) => feature.id));
1384
- const outgoing = /* @__PURE__ */ new Map();
1385
- const indegree = /* @__PURE__ */ new Map();
1386
- for (const feature of manifest) {
1387
- outgoing.set(feature.id, /* @__PURE__ */ new Set());
1388
- indegree.set(feature.id, 0);
1389
- }
1390
- for (const feature of manifest) {
1391
- for (const dependencyId of getDirectDependencies(feature)) {
1392
- if (!ids.has(dependencyId)) continue;
1393
- const edges = outgoing.get(dependencyId);
1394
- if (!edges || edges.has(feature.id)) continue;
1395
- edges.add(feature.id);
1396
- indegree.set(feature.id, (indegree.get(feature.id) ?? 0) + 1);
1397
- }
1398
- }
1399
- const queue = [];
1400
- for (const feature of manifest) {
1401
- if ((indegree.get(feature.id) ?? 0) === 0) queue.push(feature.id);
2960
+ };
2961
+ var CustomAdapter = class {
2962
+ constructor(handler) {
2963
+ this.handler = handler;
1402
2964
  }
1403
- let visited = 0;
1404
- while (queue.length > 0) {
1405
- const id = queue.shift();
1406
- if (!id) continue;
1407
- visited += 1;
1408
- const edges = outgoing.get(id);
1409
- if (!edges) continue;
1410
- for (const nextId of edges) {
1411
- const nextDegree = (indegree.get(nextId) ?? 0) - 1;
1412
- indegree.set(nextId, nextDegree);
1413
- if (nextDegree === 0) queue.push(nextId);
1414
- }
2965
+ track(event) {
2966
+ return this.handler(event);
1415
2967
  }
1416
- return visited !== manifest.length;
1417
- }
1418
- function sortFeaturesByDependencies(features) {
1419
- if (features.length <= 1) return [...features];
1420
- const order = resolveDependencyOrder(features);
1421
- const rank = new Map(order.map((id, index) => [id, index]));
1422
- return [...features].sort((a, b) => {
1423
- const ra = rank.get(a.id);
1424
- const rb = rank.get(b.id);
1425
- if (ra === void 0 || rb === void 0) return 0;
1426
- return ra - rb;
2968
+ };
2969
+ function createAdoptionMetrics(events) {
2970
+ const getAdoptionRate = (featureId) => {
2971
+ const seen = events.filter((event) => event.type === "feature_seen" && event.featureId === featureId).length;
2972
+ if (seen === 0) return 0;
2973
+ const clicked = events.filter((event) => event.type === "feature_clicked" && event.featureId === featureId).length;
2974
+ return clicked / seen;
2975
+ };
2976
+ const getTourCompletionRate = (tourId) => {
2977
+ const started = events.filter((event) => event.type === "tour_started" && event.tourId === tourId).length;
2978
+ if (started === 0) return 0;
2979
+ const completed = events.filter((event) => event.type === "tour_completed" && event.tourId === tourId).length;
2980
+ return completed / started;
2981
+ };
2982
+ const getChecklistCompletionRate = (checklistId) => {
2983
+ const taskCompleted = events.filter(
2984
+ (event) => event.type === "checklist_task_completed" && event.metadata?.checklistId === checklistId
2985
+ ).length;
2986
+ if (taskCompleted === 0) return 0;
2987
+ const completed = events.filter(
2988
+ (event) => event.type === "checklist_completed" && event.metadata?.checklistId === checklistId
2989
+ ).length;
2990
+ return completed / taskCompleted;
2991
+ };
2992
+ const getFeatureEngagement = (featureId) => ({
2993
+ seen: events.filter((event) => event.type === "feature_seen" && event.featureId === featureId).length,
2994
+ clicked: events.filter((event) => event.type === "feature_clicked" && event.featureId === featureId).length,
2995
+ dismissed: events.filter((event) => event.type === "feature_dismissed" && event.featureId === featureId).length
1427
2996
  });
2997
+ const getVariantPerformance = (featureId) => {
2998
+ const byVariant = /* @__PURE__ */ new Map();
2999
+ for (const event of events) {
3000
+ if (event.featureId !== featureId) continue;
3001
+ const variant = event.variant ?? "control";
3002
+ const bucket = byVariant.get(variant) ?? { seen: 0, clicked: 0 };
3003
+ if (event.type === "feature_seen") bucket.seen += 1;
3004
+ if (event.type === "feature_clicked") bucket.clicked += 1;
3005
+ byVariant.set(variant, bucket);
3006
+ }
3007
+ const output = {};
3008
+ for (const [variant, bucket] of byVariant.entries()) {
3009
+ output[variant] = bucket.seen === 0 ? 0 : bucket.clicked / bucket.seen;
3010
+ }
3011
+ return output;
3012
+ };
3013
+ return {
3014
+ getAdoptionRate,
3015
+ getTourCompletionRate,
3016
+ getChecklistCompletionRate,
3017
+ getFeatureEngagement,
3018
+ getVariantPerformance
3019
+ };
1428
3020
  }
1429
3021
 
1430
3022
  // src/variants.ts
@@ -1554,233 +3146,79 @@ function generateMarkdownChangelog(entries) {
1554
3146
  if (entry.type) lines.push(`- **Type**: ${entry.type}`);
1555
3147
  if (entry.category) lines.push(`- **Category**: ${entry.category}`);
1556
3148
  if (entry.showNewUntil) lines.push(`- **Show new until**: ${entry.showNewUntil}`);
1557
- if (entry.cta) lines.push(`- **CTA**: [${entry.cta.label}](${entry.cta.url})`);
1558
- if (entry.description) {
1559
- lines.push("", entry.description.trim());
1560
- }
1561
- return lines.join("\n");
1562
- });
1563
- return `# Generated Changelog
1564
-
1565
- ${sections.join("\n\n---\n\n")}
1566
- `;
1567
- }
1568
- function isIsoWithTimezone(value) {
1569
- if (!value.includes("T")) return false;
1570
- if (!(value.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(value))) return false;
1571
- return Number.isFinite(new Date(value).getTime());
1572
- }
1573
- function runDoctor(entries, now = /* @__PURE__ */ new Date()) {
1574
- const checks = [];
1575
- const warnings = [];
1576
- const errors = [];
1577
- checks.push(`Manifest entries loaded: ${entries.length}`);
1578
- const ids = /* @__PURE__ */ new Set();
1579
- let duplicateCount = 0;
1580
- for (const entry of entries) {
1581
- if (ids.has(entry.id)) duplicateCount += 1;
1582
- ids.add(entry.id);
1583
- }
1584
- if (duplicateCount > 0) {
1585
- errors.push(`${duplicateCount} duplicate feature id(s) found`);
1586
- } else {
1587
- checks.push("No duplicate IDs");
1588
- }
1589
- let invalidDateCount = 0;
1590
- let reversedDateCount = 0;
1591
- let expiredCount = 0;
1592
- let scheduledCount = 0;
1593
- let missingDescriptionCount = 0;
1594
- for (const entry of entries) {
1595
- if (!entry.description?.trim()) missingDescriptionCount += 1;
1596
- if (!isIsoWithTimezone(entry.releasedAt) || !isIsoWithTimezone(entry.showNewUntil)) {
1597
- invalidDateCount += 1;
1598
- continue;
1599
- }
1600
- const released = new Date(entry.releasedAt).getTime();
1601
- const showUntil = new Date(entry.showNewUntil).getTime();
1602
- if (showUntil <= released) reversedDateCount += 1;
1603
- if (showUntil < now.getTime()) expiredCount += 1;
1604
- if (entry.publishAt) {
1605
- const publishMs = new Date(entry.publishAt).getTime();
1606
- if (Number.isFinite(publishMs) && publishMs > now.getTime()) scheduledCount += 1;
1607
- }
1608
- }
1609
- if (invalidDateCount > 0) {
1610
- errors.push(`${invalidDateCount} entries have invalid ISO 8601 dates with timezone`);
1611
- } else {
1612
- checks.push("All dates are valid ISO 8601 with timezone");
1613
- }
1614
- if (reversedDateCount > 0) {
1615
- errors.push(`${reversedDateCount} entries have showNewUntil before/at releasedAt`);
1616
- }
1617
- if (expiredCount > 0) warnings.push(`${expiredCount} entries have showNewUntil in the past`);
1618
- if (scheduledCount > 0) warnings.push(`${scheduledCount} entries have publishAt in the future`);
1619
- if (missingDescriptionCount > 0) {
1620
- errors.push(`${missingDescriptionCount} entries have no description`);
1621
- } else {
1622
- checks.push("All entries have descriptions");
1623
- }
1624
- if (hasDependencyCycle(entries)) {
1625
- errors.push("Circular dependsOn relationship detected");
1626
- } else {
1627
- checks.push("No circular dependencies in dependsOn chains");
1628
- }
1629
- return { checks, warnings, errors };
1630
- }
1631
- var featureEntryJsonSchema = {
1632
- type: "object",
1633
- required: ["id", "label", "releasedAt", "showNewUntil"],
1634
- properties: {
1635
- id: { type: "string" },
1636
- label: { type: "string" },
1637
- description: { type: "string" },
1638
- releasedAt: { type: "string", format: "date-time" },
1639
- showNewUntil: { type: "string", format: "date-time" },
1640
- type: { enum: ["feature", "improvement", "fix", "breaking"] },
1641
- priority: { enum: ["critical", "normal", "low"] }
1642
- }
1643
- };
1644
- var featureManifestJsonSchema = {
1645
- type: "array",
1646
- items: featureEntryJsonSchema
1647
- };
1648
- function isRecord(value) {
1649
- return !!value && typeof value === "object" && !Array.isArray(value);
1650
- }
1651
- function isValidDate(value) {
1652
- return Number.isFinite(new Date(value).getTime());
1653
- }
1654
- var nonEmptyString = zod.z.string().trim().min(1, "must be a non-empty string");
1655
- var isoDateString = nonEmptyString.refine(isValidDate, {
1656
- message: "must be a valid date",
1657
- params: { featuredropCode: "invalid_date" }
1658
- });
1659
- var dependsOnSchema = zod.z.object({
1660
- seen: zod.z.array(zod.z.string()).optional(),
1661
- clicked: zod.z.array(zod.z.string()).optional(),
1662
- dismissed: zod.z.array(zod.z.string()).optional()
1663
- }).optional();
1664
- var featureEntrySchema = zod.z.object({
1665
- id: nonEmptyString,
1666
- label: nonEmptyString,
1667
- releasedAt: isoDateString,
1668
- showNewUntil: isoDateString,
1669
- description: zod.z.string().optional(),
1670
- type: zod.z.enum(["feature", "improvement", "fix", "breaking"]).optional(),
1671
- priority: zod.z.enum(["critical", "normal", "low"]).optional(),
1672
- dependsOn: dependsOnSchema
1673
- }).passthrough();
1674
- var featureManifestSchema = zod.z.array(featureEntrySchema);
1675
- function toIssuePath(path) {
1676
- if (path.length === 0) return "$";
1677
- let output = "";
1678
- for (const part of path) {
1679
- if (typeof part === "number") output += `[${part}]`;
1680
- else output += output ? `.${part}` : part;
1681
- }
1682
- return output;
1683
- }
1684
- function mapZodIssue(issue) {
1685
- const codeParam = issue.params?.featuredropCode;
1686
- if (codeParam === "invalid_date") {
1687
- return {
1688
- path: toIssuePath(issue.path),
1689
- message: issue.message,
1690
- code: "invalid_date"
1691
- };
1692
- }
1693
- if (issue.code === "invalid_type") {
1694
- return {
1695
- path: toIssuePath(issue.path),
1696
- message: issue.message,
1697
- code: issue.received === "undefined" ? "missing_required" : "invalid_type"
1698
- };
1699
- }
1700
- return {
1701
- path: toIssuePath(issue.path),
1702
- message: issue.message,
1703
- code: "invalid_value"
1704
- };
1705
- }
1706
- function validateFeatureEntry(raw, index) {
1707
- if (!isRecord(raw)) {
1708
- return {
1709
- issues: [
1710
- {
1711
- path: `[${index}]`,
1712
- message: "Feature entry must be an object",
1713
- code: "invalid_type"
1714
- }
1715
- ]
1716
- };
1717
- }
1718
- const parsed = featureEntrySchema.safeParse(raw);
1719
- if (!parsed.success) {
1720
- return {
1721
- issues: parsed.error.issues.map((issue) => ({
1722
- ...mapZodIssue(issue),
1723
- path: `[${index}]${issue.path.length > 0 ? `.${toIssuePath(issue.path)}` : ""}`
1724
- }))
1725
- };
1726
- }
1727
- return {
1728
- issues: [],
1729
- entry: parsed.data
1730
- };
3149
+ if (entry.cta) lines.push(`- **CTA**: [${entry.cta.label}](${entry.cta.url})`);
3150
+ if (entry.description) {
3151
+ lines.push("", entry.description.trim());
3152
+ }
3153
+ return lines.join("\n");
3154
+ });
3155
+ return `# Generated Changelog
3156
+
3157
+ ${sections.join("\n\n---\n\n")}
3158
+ `;
1731
3159
  }
1732
- function validateManifest(data) {
3160
+ function isIsoWithTimezone(value) {
3161
+ if (!value.includes("T")) return false;
3162
+ if (!(value.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(value))) return false;
3163
+ return Number.isFinite(new Date(value).getTime());
3164
+ }
3165
+ function runDoctor(entries, now = /* @__PURE__ */ new Date()) {
3166
+ const checks = [];
3167
+ const warnings = [];
1733
3168
  const errors = [];
1734
- if (!Array.isArray(data)) {
1735
- return {
1736
- valid: false,
1737
- errors: [
1738
- {
1739
- path: "$",
1740
- message: "Manifest must be an array",
1741
- code: "invalid_type"
1742
- }
1743
- ]
1744
- };
3169
+ checks.push(`Manifest entries loaded: ${entries.length}`);
3170
+ const ids = /* @__PURE__ */ new Set();
3171
+ let duplicateCount = 0;
3172
+ for (const entry of entries) {
3173
+ if (ids.has(entry.id)) duplicateCount += 1;
3174
+ ids.add(entry.id);
1745
3175
  }
1746
- const entries = [];
1747
- const seenIds = /* @__PURE__ */ new Set();
1748
- data.forEach((item, index) => {
1749
- const result = validateFeatureEntry(item, index);
1750
- errors.push(...result.issues);
1751
- if (!result.entry) return;
1752
- if (seenIds.has(result.entry.id)) {
1753
- errors.push({
1754
- path: `[${index}].id`,
1755
- message: `Duplicate feature id "${result.entry.id}"`,
1756
- code: "duplicate_id"
1757
- });
1758
- return;
1759
- }
1760
- seenIds.add(result.entry.id);
1761
- entries.push(result.entry);
1762
- });
1763
- if (entries.length > 0 && hasDependencyCycle(entries)) {
1764
- errors.push({
1765
- path: "$",
1766
- message: "Circular dependsOn relationship detected",
1767
- code: "circular_dependency"
1768
- });
3176
+ if (duplicateCount > 0) {
3177
+ errors.push(`${duplicateCount} duplicate feature id(s) found`);
3178
+ } else {
3179
+ checks.push("No duplicate IDs");
1769
3180
  }
1770
- for (let index = 0; index < entries.length; index++) {
1771
- const entry = entries[index];
1772
- if (new Date(entry.showNewUntil).getTime() <= new Date(entry.releasedAt).getTime()) {
1773
- errors.push({
1774
- path: `[${index}].showNewUntil`,
1775
- message: "showNewUntil must be after releasedAt",
1776
- code: "invalid_value"
1777
- });
3181
+ let invalidDateCount = 0;
3182
+ let reversedDateCount = 0;
3183
+ let expiredCount = 0;
3184
+ let scheduledCount = 0;
3185
+ let missingDescriptionCount = 0;
3186
+ for (const entry of entries) {
3187
+ if (!entry.description?.trim()) missingDescriptionCount += 1;
3188
+ if (!isIsoWithTimezone(entry.releasedAt) || !isIsoWithTimezone(entry.showNewUntil)) {
3189
+ invalidDateCount += 1;
3190
+ continue;
3191
+ }
3192
+ const released = new Date(entry.releasedAt).getTime();
3193
+ const showUntil = new Date(entry.showNewUntil).getTime();
3194
+ if (showUntil <= released) reversedDateCount += 1;
3195
+ if (showUntil < now.getTime()) expiredCount += 1;
3196
+ if (entry.publishAt) {
3197
+ const publishMs = new Date(entry.publishAt).getTime();
3198
+ if (Number.isFinite(publishMs) && publishMs > now.getTime()) scheduledCount += 1;
1778
3199
  }
1779
3200
  }
1780
- return {
1781
- valid: errors.length === 0,
1782
- errors
1783
- };
3201
+ if (invalidDateCount > 0) {
3202
+ errors.push(`${invalidDateCount} entries have invalid ISO 8601 dates with timezone`);
3203
+ } else {
3204
+ checks.push("All dates are valid ISO 8601 with timezone");
3205
+ }
3206
+ if (reversedDateCount > 0) {
3207
+ errors.push(`${reversedDateCount} entries have showNewUntil before/at releasedAt`);
3208
+ }
3209
+ if (expiredCount > 0) warnings.push(`${expiredCount} entries have showNewUntil in the past`);
3210
+ if (scheduledCount > 0) warnings.push(`${scheduledCount} entries have publishAt in the future`);
3211
+ if (missingDescriptionCount > 0) {
3212
+ errors.push(`${missingDescriptionCount} entries have no description`);
3213
+ } else {
3214
+ checks.push("All entries have descriptions");
3215
+ }
3216
+ if (hasDependencyCycle(entries)) {
3217
+ errors.push("Circular dependsOn relationship detected");
3218
+ } else {
3219
+ checks.push("No circular dependencies in dependsOn chains");
3220
+ }
3221
+ return { checks, warnings, errors };
1784
3222
  }
1785
3223
 
1786
3224
  // src/adapters/local-storage.ts
@@ -1836,6 +3274,319 @@ var LocalStorageAdapter = class {
1836
3274
  }
1837
3275
  };
1838
3276
 
3277
+ // src/adapters/indexeddb.ts
3278
+ var DISMISSED_SUFFIX2 = ":dismissed";
3279
+ var WATERMARK_SUFFIX = ":watermark";
3280
+ var QUEUE_SUFFIX = ":queue";
3281
+ function canUseLocalStorage() {
3282
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
3283
+ }
3284
+ function readLocalStorageState(prefix) {
3285
+ if (!canUseLocalStorage()) {
3286
+ return { watermark: null, dismissed: [], queue: [] };
3287
+ }
3288
+ try {
3289
+ const dismissedRaw = localStorage.getItem(`${prefix}${DISMISSED_SUFFIX2}`);
3290
+ const watermarkRaw = localStorage.getItem(`${prefix}${WATERMARK_SUFFIX}`);
3291
+ const queueRaw = localStorage.getItem(`${prefix}${QUEUE_SUFFIX}`);
3292
+ const dismissedParsed = dismissedRaw ? JSON.parse(dismissedRaw) : [];
3293
+ const queueParsed = queueRaw ? JSON.parse(queueRaw) : [];
3294
+ return {
3295
+ watermark: typeof watermarkRaw === "string" ? watermarkRaw : null,
3296
+ dismissed: Array.isArray(dismissedParsed) ? dismissedParsed.filter((value) => typeof value === "string") : [],
3297
+ queue: normalizeQueue(queueParsed)
3298
+ };
3299
+ } catch {
3300
+ return { watermark: null, dismissed: [], queue: [] };
3301
+ }
3302
+ }
3303
+ function writeLocalStorageState(prefix, state) {
3304
+ if (!canUseLocalStorage()) return;
3305
+ try {
3306
+ localStorage.setItem(`${prefix}${DISMISSED_SUFFIX2}`, JSON.stringify(state.dismissed));
3307
+ if (state.watermark) {
3308
+ localStorage.setItem(`${prefix}${WATERMARK_SUFFIX}`, state.watermark);
3309
+ } else {
3310
+ localStorage.removeItem(`${prefix}${WATERMARK_SUFFIX}`);
3311
+ }
3312
+ if (state.queue && state.queue.length > 0) {
3313
+ localStorage.setItem(`${prefix}${QUEUE_SUFFIX}`, JSON.stringify(state.queue));
3314
+ } else {
3315
+ localStorage.removeItem(`${prefix}${QUEUE_SUFFIX}`);
3316
+ }
3317
+ } catch {
3318
+ }
3319
+ }
3320
+ function getIndexedDBFactory() {
3321
+ if (typeof globalThis === "undefined") return null;
3322
+ const candidate = globalThis.indexedDB;
3323
+ return candidate ?? null;
3324
+ }
3325
+ function normalizeQueue(value) {
3326
+ if (!Array.isArray(value)) return [];
3327
+ const queue = [];
3328
+ for (const item of value) {
3329
+ if (!item || typeof item !== "object") continue;
3330
+ const candidate = item;
3331
+ if (candidate.type === "dismiss" && typeof candidate.id === "string") {
3332
+ queue.push({ type: "dismiss", id: candidate.id });
3333
+ continue;
3334
+ }
3335
+ if (candidate.type === "dismissAll" && typeof candidate.watermark === "string") {
3336
+ queue.push({ type: "dismissAll", watermark: candidate.watermark });
3337
+ continue;
3338
+ }
3339
+ }
3340
+ return queue;
3341
+ }
3342
+ function normalizeDismissedIds(value) {
3343
+ if (!Array.isArray(value)) return [];
3344
+ return value.filter((entry) => typeof entry === "string");
3345
+ }
3346
+ function parseIso(value) {
3347
+ if (!value) return Number.NaN;
3348
+ return new Date(value).getTime();
3349
+ }
3350
+ function resolveLatestWatermark(a, b) {
3351
+ if (!a) return b ?? null;
3352
+ if (!b) return a;
3353
+ const aTs = parseIso(a);
3354
+ const bTs = parseIso(b);
3355
+ if (!Number.isFinite(aTs)) return b;
3356
+ if (!Number.isFinite(bTs)) return a;
3357
+ return aTs >= bTs ? a : b;
3358
+ }
3359
+ var IndexedDBAdapter = class {
3360
+ prefix;
3361
+ dbName;
3362
+ storeName;
3363
+ onDismissAllCallback;
3364
+ onSyncStateCallback;
3365
+ onFlushDismissBatchCallback;
3366
+ onFlushDismissAllCallback;
3367
+ flushDebounceMs;
3368
+ autoSyncOnOnline;
3369
+ watermark;
3370
+ dismissed;
3371
+ queue;
3372
+ hydratePromise;
3373
+ flushTimer = null;
3374
+ flushing = false;
3375
+ boundOnlineHandler;
3376
+ boundVisibilityHandler;
3377
+ constructor(options = {}) {
3378
+ this.prefix = options.prefix ?? "featuredrop";
3379
+ this.dbName = options.dbName ?? "featuredrop";
3380
+ this.storeName = options.storeName ?? "state";
3381
+ this.onDismissAllCallback = options.onDismissAll;
3382
+ this.onSyncStateCallback = options.onSyncState;
3383
+ this.onFlushDismissBatchCallback = options.onFlushDismissBatch;
3384
+ this.onFlushDismissAllCallback = options.onFlushDismissAll;
3385
+ this.flushDebounceMs = options.flushDebounceMs ?? 500;
3386
+ this.autoSyncOnOnline = options.autoSyncOnOnline ?? true;
3387
+ const localState = readLocalStorageState(this.prefix);
3388
+ this.watermark = options.watermark ?? localState.watermark;
3389
+ this.dismissed = new Set(localState.dismissed);
3390
+ this.queue = localState.queue ?? [];
3391
+ this.hydratePromise = this.hydrateFromIndexedDB();
3392
+ const canAttachListeners = this.autoSyncOnOnline && typeof window !== "undefined";
3393
+ if (canAttachListeners) {
3394
+ this.boundOnlineHandler = () => {
3395
+ void this.syncFromRemote();
3396
+ };
3397
+ this.boundVisibilityHandler = () => {
3398
+ if (document.visibilityState === "visible") {
3399
+ void this.syncFromRemote();
3400
+ }
3401
+ };
3402
+ window.addEventListener("online", this.boundOnlineHandler);
3403
+ document.addEventListener("visibilitychange", this.boundVisibilityHandler);
3404
+ } else {
3405
+ this.boundOnlineHandler = null;
3406
+ this.boundVisibilityHandler = null;
3407
+ }
3408
+ }
3409
+ getWatermark() {
3410
+ return this.watermark;
3411
+ }
3412
+ getDismissedIds() {
3413
+ return this.dismissed;
3414
+ }
3415
+ dismiss(id) {
3416
+ if (!id || this.dismissed.has(id)) return;
3417
+ this.dismissed = new Set(this.dismissed).add(id);
3418
+ this.queue.push({ type: "dismiss", id });
3419
+ this.persist();
3420
+ this.scheduleFlush();
3421
+ }
3422
+ async dismissAll(now) {
3423
+ this.watermark = now.toISOString();
3424
+ this.dismissed = /* @__PURE__ */ new Set();
3425
+ this.queue = [{ type: "dismissAll", watermark: this.watermark }];
3426
+ this.persist();
3427
+ this.scheduleFlush();
3428
+ await this.onDismissAllCallback?.(now);
3429
+ }
3430
+ /** Flush queued dismiss operations to optional remote callbacks. */
3431
+ async flushQueue() {
3432
+ if (this.flushing || this.queue.length === 0) return;
3433
+ if (!this.onFlushDismissBatchCallback && !this.onFlushDismissAllCallback) return;
3434
+ this.flushing = true;
3435
+ try {
3436
+ const operations = [...this.queue];
3437
+ const lastDismissAll = this.getLastDismissAll(operations);
3438
+ const dismissIds = this.collectDismissBatch(operations, !!lastDismissAll);
3439
+ const hasDismissAll = !!lastDismissAll;
3440
+ const needsDismissBatch = dismissIds.length > 0;
3441
+ if (hasDismissAll && !this.onFlushDismissAllCallback) return;
3442
+ if (needsDismissBatch && !this.onFlushDismissBatchCallback) return;
3443
+ if (lastDismissAll && this.onFlushDismissAllCallback) {
3444
+ await this.onFlushDismissAllCallback(lastDismissAll.watermark);
3445
+ }
3446
+ if (dismissIds.length > 0 && this.onFlushDismissBatchCallback) {
3447
+ await this.onFlushDismissBatchCallback(dismissIds);
3448
+ }
3449
+ if (this.queue.length <= operations.length) {
3450
+ this.queue = [];
3451
+ } else {
3452
+ this.queue = this.queue.slice(operations.length);
3453
+ }
3454
+ this.persist();
3455
+ } catch {
3456
+ } finally {
3457
+ this.flushing = false;
3458
+ }
3459
+ }
3460
+ /** Merge local state with optional remote source, then flush queued writes. */
3461
+ async syncFromRemote() {
3462
+ await this.hydratePromise.catch(() => void 0);
3463
+ if (this.onSyncStateCallback) {
3464
+ try {
3465
+ const remote = await this.onSyncStateCallback();
3466
+ const mergedDismissed = new Set(this.dismissed);
3467
+ for (const id of normalizeDismissedIds(remote.dismissedIds)) {
3468
+ mergedDismissed.add(id);
3469
+ }
3470
+ this.dismissed = mergedDismissed;
3471
+ this.watermark = resolveLatestWatermark(this.watermark, remote.watermark ?? null);
3472
+ this.persist();
3473
+ } catch {
3474
+ }
3475
+ }
3476
+ await this.flushQueue();
3477
+ }
3478
+ /** Cleanup optional browser listeners. */
3479
+ destroy() {
3480
+ if (this.flushTimer) {
3481
+ clearTimeout(this.flushTimer);
3482
+ this.flushTimer = null;
3483
+ }
3484
+ if (this.boundOnlineHandler && typeof window !== "undefined") {
3485
+ window.removeEventListener("online", this.boundOnlineHandler);
3486
+ }
3487
+ if (this.boundVisibilityHandler && typeof document !== "undefined") {
3488
+ document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
3489
+ }
3490
+ }
3491
+ persist() {
3492
+ const snapshot = {
3493
+ watermark: this.watermark,
3494
+ dismissed: Array.from(this.dismissed),
3495
+ queue: this.queue
3496
+ };
3497
+ writeLocalStorageState(this.prefix, snapshot);
3498
+ void this.writeIndexedDBState(snapshot);
3499
+ }
3500
+ async hydrateFromIndexedDB() {
3501
+ const state = await this.readIndexedDBState();
3502
+ if (!state) return;
3503
+ this.watermark = state.watermark;
3504
+ this.dismissed = new Set(state.dismissed);
3505
+ this.queue = state.queue ?? [];
3506
+ writeLocalStorageState(this.prefix, state);
3507
+ }
3508
+ async readIndexedDBState() {
3509
+ const db = await this.openDb();
3510
+ if (!db) return null;
3511
+ return new Promise((resolve) => {
3512
+ const tx = db.transaction(this.storeName, "readonly");
3513
+ const store = tx.objectStore(this.storeName);
3514
+ const request = store.get(this.prefix);
3515
+ request.onsuccess = () => {
3516
+ const value = request.result;
3517
+ if (!value) {
3518
+ resolve(null);
3519
+ return;
3520
+ }
3521
+ resolve({
3522
+ watermark: typeof value.watermark === "string" ? value.watermark : null,
3523
+ dismissed: normalizeDismissedIds(value.dismissed),
3524
+ queue: normalizeQueue(value.queue)
3525
+ });
3526
+ };
3527
+ request.onerror = () => resolve(null);
3528
+ });
3529
+ }
3530
+ async writeIndexedDBState(state) {
3531
+ await this.hydratePromise.catch(() => void 0);
3532
+ const db = await this.openDb();
3533
+ if (!db) return;
3534
+ await new Promise((resolve) => {
3535
+ const tx = db.transaction(this.storeName, "readwrite");
3536
+ const store = tx.objectStore(this.storeName);
3537
+ store.put(state, this.prefix);
3538
+ tx.oncomplete = () => resolve();
3539
+ tx.onerror = () => resolve();
3540
+ tx.onabort = () => resolve();
3541
+ });
3542
+ }
3543
+ async openDb() {
3544
+ const factory = getIndexedDBFactory();
3545
+ if (!factory) return null;
3546
+ return new Promise((resolve) => {
3547
+ const request = factory.open(this.dbName, 1);
3548
+ request.onerror = () => resolve(null);
3549
+ request.onupgradeneeded = () => {
3550
+ const db = request.result;
3551
+ if (!db.objectStoreNames.contains(this.storeName)) {
3552
+ db.createObjectStore(this.storeName);
3553
+ }
3554
+ };
3555
+ request.onsuccess = () => resolve(request.result);
3556
+ });
3557
+ }
3558
+ scheduleFlush() {
3559
+ if (this.flushTimer) return;
3560
+ this.flushTimer = setTimeout(() => {
3561
+ this.flushTimer = null;
3562
+ void this.flushQueue();
3563
+ }, this.flushDebounceMs);
3564
+ }
3565
+ getLastDismissAll(operations) {
3566
+ for (let index = operations.length - 1; index >= 0; index--) {
3567
+ const operation = operations[index];
3568
+ if (operation.type === "dismissAll") {
3569
+ return { watermark: operation.watermark };
3570
+ }
3571
+ }
3572
+ return null;
3573
+ }
3574
+ collectDismissBatch(operations, skipBeforeDismissAll) {
3575
+ const startIndex = skipBeforeDismissAll ? operations.reduce(
3576
+ (lastIndex, operation, index) => operation.type === "dismissAll" ? index : lastIndex,
3577
+ -1
3578
+ ) : -1;
3579
+ const batch = /* @__PURE__ */ new Set();
3580
+ for (let index = startIndex + 1; index < operations.length; index++) {
3581
+ const operation = operations[index];
3582
+ if (operation.type === "dismiss") {
3583
+ batch.add(operation.id);
3584
+ }
3585
+ }
3586
+ return Array.from(batch);
3587
+ }
3588
+ };
3589
+
1839
3590
  // src/adapters/memory.ts
1840
3591
  var MemoryAdapter = class {
1841
3592
  watermark;
@@ -1875,41 +3626,66 @@ var RemoteAdapter = class {
1875
3626
  watermark = null;
1876
3627
  lastManifest = null;
1877
3628
  lastFetchTs = 0;
3629
+ retryAttempts;
3630
+ retryBaseDelayMs;
3631
+ circuitBreakerThreshold;
3632
+ circuitBreakerCooldownMs;
3633
+ sleep;
3634
+ now;
3635
+ consecutiveFailures = 0;
3636
+ circuitOpenUntil = 0;
1878
3637
  constructor(options) {
1879
3638
  this.baseUrl = options.url.replace(/\/$/, "");
1880
3639
  this.headers = options.headers ?? {};
1881
3640
  this.fetchInterval = options.fetchInterval ?? 5 * 60 * 1e3;
1882
3641
  this.userId = options.userId;
3642
+ this.retryAttempts = options.retryAttempts ?? 3;
3643
+ this.retryBaseDelayMs = options.retryBaseDelayMs ?? 250;
3644
+ this.circuitBreakerThreshold = options.circuitBreakerThreshold ?? 5;
3645
+ this.circuitBreakerCooldownMs = options.circuitBreakerCooldownMs ?? 6e4;
3646
+ this.sleep = options.sleep ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
3647
+ this.now = options.now ?? (() => Date.now());
1883
3648
  }
1884
3649
  /** Fetch manifest with stale-while-revalidate */
1885
3650
  async fetchManifest(force = false) {
1886
- const now = Date.now();
3651
+ const now = this.now();
1887
3652
  if (!force && this.lastManifest && now - this.lastFetchTs < this.fetchInterval) {
1888
3653
  return this.lastManifest;
1889
3654
  }
1890
- const fetchImpl = assertFetch();
1891
- const res = await fetchImpl(this.baseUrl, {
1892
- method: "GET",
1893
- headers: this.headers
1894
- });
1895
- if (!res.ok) throw new Error(`RemoteAdapter manifest fetch failed: ${res.status}`);
1896
- const json = await res.json();
1897
- this.lastManifest = json;
1898
- this.lastFetchTs = now;
1899
- return json;
3655
+ try {
3656
+ const json = await this.withRetry(async () => {
3657
+ const fetchImpl = assertFetch();
3658
+ const res = await fetchImpl(this.baseUrl, {
3659
+ method: "GET",
3660
+ headers: this.headers
3661
+ });
3662
+ if (!res.ok) throw new Error(`RemoteAdapter manifest fetch failed: ${res.status}`);
3663
+ return await res.json();
3664
+ });
3665
+ this.lastManifest = json;
3666
+ this.lastFetchTs = now;
3667
+ return json;
3668
+ } catch {
3669
+ return this.lastManifest ?? [];
3670
+ }
1900
3671
  }
1901
3672
  /** Fetch state (watermark + dismissed IDs) */
1902
3673
  async syncState() {
1903
- const fetchImpl = assertFetch();
1904
- const url = this.userId ? `${this.baseUrl}/state?userId=${encodeURIComponent(this.userId)}` : `${this.baseUrl}/state`;
1905
- const res = await fetchImpl(url, {
1906
- method: "GET",
1907
- headers: this.headers
1908
- });
1909
- if (!res.ok) return;
1910
- const json = await res.json();
1911
- if (json.watermark !== void 0) this.watermark = json.watermark;
1912
- if (Array.isArray(json.dismissedIds)) this.dismissedIds = new Set(json.dismissedIds);
3674
+ try {
3675
+ const json = await this.withRetry(async () => {
3676
+ const fetchImpl = assertFetch();
3677
+ const url = this.userId ? `${this.baseUrl}/state?userId=${encodeURIComponent(this.userId)}` : `${this.baseUrl}/state`;
3678
+ const res = await fetchImpl(url, {
3679
+ method: "GET",
3680
+ headers: this.headers
3681
+ });
3682
+ if (!res.ok) throw new Error(`RemoteAdapter state sync failed: ${res.status}`);
3683
+ return await res.json();
3684
+ });
3685
+ if (json.watermark !== void 0) this.watermark = json.watermark;
3686
+ if (Array.isArray(json.dismissedIds)) this.dismissedIds = new Set(json.dismissedIds);
3687
+ } catch {
3688
+ }
1913
3689
  }
1914
3690
  getWatermark() {
1915
3691
  return this.watermark;
@@ -1928,26 +3704,82 @@ var RemoteAdapter = class {
1928
3704
  await this.flushDismissAll(now).catch(() => {
1929
3705
  });
1930
3706
  }
3707
+ /** Returns current adapter health; false while circuit breaker is open. */
3708
+ async isHealthy() {
3709
+ if (this.isCircuitOpen()) return false;
3710
+ try {
3711
+ await this.withRetry(async () => {
3712
+ const fetchImpl = assertFetch();
3713
+ const res = await fetchImpl(this.baseUrl, {
3714
+ method: "GET",
3715
+ headers: this.headers
3716
+ });
3717
+ if (!res.ok) throw new Error(`RemoteAdapter health check failed: ${res.status}`);
3718
+ });
3719
+ return true;
3720
+ } catch {
3721
+ return false;
3722
+ }
3723
+ }
1931
3724
  async flushDismiss(id) {
1932
- const fetchImpl = assertFetch();
1933
- await fetchImpl(`${this.baseUrl}/dismiss`, {
1934
- method: "POST",
1935
- headers: { "Content-Type": "application/json", ...this.headers },
1936
- body: JSON.stringify({ featureId: id })
3725
+ await this.withRetry(async () => {
3726
+ const fetchImpl = assertFetch();
3727
+ const res = await fetchImpl(`${this.baseUrl}/dismiss`, {
3728
+ method: "POST",
3729
+ headers: { "Content-Type": "application/json", ...this.headers },
3730
+ body: JSON.stringify({ featureId: id })
3731
+ });
3732
+ if (!res.ok) throw new Error(`RemoteAdapter dismiss failed: ${res.status}`);
1937
3733
  });
1938
3734
  }
1939
3735
  async flushDismissAll(now) {
1940
- const fetchImpl = assertFetch();
1941
- await fetchImpl(`${this.baseUrl}/dismiss-all`, {
1942
- method: "POST",
1943
- headers: { "Content-Type": "application/json", ...this.headers },
1944
- body: JSON.stringify({ watermark: now.toISOString() })
3736
+ await this.withRetry(async () => {
3737
+ const fetchImpl = assertFetch();
3738
+ const res = await fetchImpl(`${this.baseUrl}/dismiss-all`, {
3739
+ method: "POST",
3740
+ headers: { "Content-Type": "application/json", ...this.headers },
3741
+ body: JSON.stringify({ watermark: now.toISOString() })
3742
+ });
3743
+ if (!res.ok) throw new Error(`RemoteAdapter dismiss-all failed: ${res.status}`);
1945
3744
  });
1946
3745
  }
3746
+ isCircuitOpen() {
3747
+ return this.now() < this.circuitOpenUntil;
3748
+ }
3749
+ markFailure() {
3750
+ this.consecutiveFailures += 1;
3751
+ if (this.consecutiveFailures >= this.circuitBreakerThreshold) {
3752
+ this.circuitOpenUntil = this.now() + this.circuitBreakerCooldownMs;
3753
+ }
3754
+ }
3755
+ markSuccess() {
3756
+ this.consecutiveFailures = 0;
3757
+ this.circuitOpenUntil = 0;
3758
+ }
3759
+ async withRetry(operation) {
3760
+ if (this.isCircuitOpen()) {
3761
+ throw new Error("RemoteAdapter circuit breaker is open");
3762
+ }
3763
+ let lastError = new Error("RemoteAdapter request failed");
3764
+ for (let attempt = 0; attempt <= this.retryAttempts; attempt++) {
3765
+ try {
3766
+ const result = await operation();
3767
+ this.markSuccess();
3768
+ return result;
3769
+ } catch (error) {
3770
+ lastError = error;
3771
+ if (attempt >= this.retryAttempts) break;
3772
+ const delayMs = this.retryBaseDelayMs * 2 ** attempt;
3773
+ await this.sleep(delayMs);
3774
+ }
3775
+ }
3776
+ this.markFailure();
3777
+ throw lastError instanceof Error ? lastError : new Error("RemoteAdapter request failed");
3778
+ }
1947
3779
  };
1948
3780
 
1949
3781
  // src/adapters/postgres.ts
1950
- function normalizeDismissedIds(row) {
3782
+ function normalizeDismissedIds2(row) {
1951
3783
  if (!row) return [];
1952
3784
  const ids = row.dismissed_ids ?? row.dismissedIds;
1953
3785
  if (!Array.isArray(ids)) return [];
@@ -2010,7 +3842,7 @@ var PostgresAdapter = class {
2010
3842
  );
2011
3843
  const row = result.rows[0];
2012
3844
  this.watermark = normalizeWatermark(row);
2013
- this.dismissedIds = new Set(normalizeDismissedIds(row));
3845
+ this.dismissedIds = new Set(normalizeDismissedIds2(row));
2014
3846
  }
2015
3847
  async dismissBatch(ids) {
2016
3848
  if (ids.length === 0) return;
@@ -2057,7 +3889,7 @@ var PostgresAdapter = class {
2057
3889
  for (const row of result.rows) {
2058
3890
  out.set(row.user_id, {
2059
3891
  watermark: normalizeWatermark(row),
2060
- dismissedIds: normalizeDismissedIds(row),
3892
+ dismissedIds: normalizeDismissedIds2(row),
2061
3893
  lastSeen: normalizeLastSeen(row),
2062
3894
  deviceCount: 1
2063
3895
  });
@@ -2199,11 +4031,47 @@ var HybridAdapter = class {
2199
4031
  local;
2200
4032
  remote;
2201
4033
  syncBeforeWrite;
4034
+ dismissBatchWindowMs;
4035
+ syncIntervalMs;
4036
+ syncOnVisibilityChange;
4037
+ syncOnOnline;
4038
+ pendingDismissIds = /* @__PURE__ */ new Set();
4039
+ dismissTimer = null;
4040
+ syncTimer = null;
4041
+ boundVisibilityHandler;
4042
+ boundOnlineHandler;
2202
4043
  constructor(options) {
2203
4044
  this.local = options.local;
2204
4045
  this.remote = options.remote;
2205
4046
  this.userId = options.remote.userId;
2206
4047
  this.syncBeforeWrite = options.syncBeforeWrite ?? false;
4048
+ this.dismissBatchWindowMs = options.dismissBatchWindowMs ?? 500;
4049
+ this.syncIntervalMs = options.syncIntervalMs ?? 0;
4050
+ this.syncOnVisibilityChange = options.syncOnVisibilityChange ?? true;
4051
+ this.syncOnOnline = options.syncOnOnline ?? true;
4052
+ if (typeof window !== "undefined" && this.syncOnOnline) {
4053
+ this.boundOnlineHandler = () => {
4054
+ void this.sync();
4055
+ };
4056
+ window.addEventListener("online", this.boundOnlineHandler);
4057
+ } else {
4058
+ this.boundOnlineHandler = null;
4059
+ }
4060
+ if (typeof document !== "undefined" && this.syncOnVisibilityChange) {
4061
+ this.boundVisibilityHandler = () => {
4062
+ if (document.visibilityState === "visible") {
4063
+ void this.sync();
4064
+ }
4065
+ };
4066
+ document.addEventListener("visibilitychange", this.boundVisibilityHandler);
4067
+ } else {
4068
+ this.boundVisibilityHandler = null;
4069
+ }
4070
+ if (this.syncIntervalMs > 0) {
4071
+ this.syncTimer = setInterval(() => {
4072
+ void this.sync();
4073
+ }, this.syncIntervalMs);
4074
+ }
2207
4075
  }
2208
4076
  getWatermark() {
2209
4077
  return this.local.getWatermark() ?? this.remote.getWatermark();
@@ -2216,9 +4084,12 @@ var HybridAdapter = class {
2216
4084
  }
2217
4085
  dismiss(id) {
2218
4086
  this.local.dismiss(id);
2219
- this.remote.dismiss(id);
4087
+ this.pendingDismissIds.add(id);
4088
+ this.scheduleDismissFlush();
2220
4089
  }
2221
4090
  async dismissAll(now) {
4091
+ await this.flushPendingDismisses();
4092
+ this.pendingDismissIds.clear();
2222
4093
  await Promise.all([
2223
4094
  this.local.dismissAll(now),
2224
4095
  this.remote.dismissAll(now)
@@ -2249,8 +4120,44 @@ var HybridAdapter = class {
2249
4120
  return this.remote.isHealthy();
2250
4121
  }
2251
4122
  async destroy() {
4123
+ if (this.dismissTimer) {
4124
+ clearTimeout(this.dismissTimer);
4125
+ this.dismissTimer = null;
4126
+ }
4127
+ if (this.syncTimer) {
4128
+ clearInterval(this.syncTimer);
4129
+ this.syncTimer = null;
4130
+ }
4131
+ if (this.boundOnlineHandler && typeof window !== "undefined") {
4132
+ window.removeEventListener("online", this.boundOnlineHandler);
4133
+ }
4134
+ if (this.boundVisibilityHandler && typeof document !== "undefined") {
4135
+ document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
4136
+ }
4137
+ await this.flushPendingDismisses();
2252
4138
  await this.remote.destroy();
2253
4139
  }
4140
+ /** Manually flush queued dismiss operations to the remote adapter. */
4141
+ async flushPendingDismisses() {
4142
+ if (this.pendingDismissIds.size === 0) return;
4143
+ const ids = Array.from(this.pendingDismissIds);
4144
+ this.pendingDismissIds.clear();
4145
+ try {
4146
+ if (this.syncBeforeWrite) {
4147
+ await this.remote.sync();
4148
+ }
4149
+ await this.remote.dismissBatch(ids);
4150
+ } catch {
4151
+ for (const id of ids) this.pendingDismissIds.add(id);
4152
+ }
4153
+ }
4154
+ scheduleDismissFlush() {
4155
+ if (this.dismissTimer) return;
4156
+ this.dismissTimer = setTimeout(() => {
4157
+ this.dismissTimer = null;
4158
+ void this.flushPendingDismisses();
4159
+ }, this.dismissBatchWindowMs);
4160
+ }
2254
4161
  };
2255
4162
 
2256
4163
  // src/adapters/mysql.ts
@@ -2270,7 +4177,7 @@ function parseDismissedIds(value) {
2270
4177
  }
2271
4178
  return [];
2272
4179
  }
2273
- function normalizeDismissedIds2(row) {
4180
+ function normalizeDismissedIds3(row) {
2274
4181
  if (!row) return [];
2275
4182
  return parseDismissedIds(row.dismissed_ids ?? row.dismissedIds);
2276
4183
  }
@@ -2328,7 +4235,7 @@ var MySQLAdapter = class {
2328
4235
  );
2329
4236
  const row = result.rows[0];
2330
4237
  this.watermark = normalizeWatermark2(row);
2331
- this.dismissedIds = new Set(normalizeDismissedIds2(row));
4238
+ this.dismissedIds = new Set(normalizeDismissedIds3(row));
2332
4239
  }
2333
4240
  async dismissBatch(ids) {
2334
4241
  const uniqueIds = Array.from(new Set(ids));
@@ -2369,7 +4276,7 @@ var MySQLAdapter = class {
2369
4276
  for (const row of result.rows) {
2370
4277
  out.set(row.user_id, {
2371
4278
  watermark: normalizeWatermark2(row),
2372
- dismissedIds: normalizeDismissedIds2(row),
4279
+ dismissedIds: normalizeDismissedIds3(row),
2373
4280
  lastSeen: normalizeLastSeen2(row),
2374
4281
  deviceCount: 1
2375
4282
  });
@@ -2408,7 +4315,7 @@ var MySQLAdapter = class {
2408
4315
  };
2409
4316
 
2410
4317
  // src/adapters/mongo.ts
2411
- function normalizeDismissedIds3(ids) {
4318
+ function normalizeDismissedIds4(ids) {
2412
4319
  if (!Array.isArray(ids)) return [];
2413
4320
  return ids.filter((id) => typeof id === "string");
2414
4321
  }
@@ -2463,7 +4370,7 @@ var MongoAdapter = class {
2463
4370
  async sync() {
2464
4371
  const doc = await this.collection.findOne({ userId: this.userId });
2465
4372
  this.watermark = doc?.watermark ?? null;
2466
- this.dismissedIds = new Set(normalizeDismissedIds3(doc?.dismissedIds));
4373
+ this.dismissedIds = new Set(normalizeDismissedIds4(doc?.dismissedIds));
2467
4374
  }
2468
4375
  async dismissBatch(ids) {
2469
4376
  const unique = Array.from(new Set(ids));
@@ -2493,7 +4400,7 @@ var MongoAdapter = class {
2493
4400
  for (const row of rows) {
2494
4401
  out.set(row.userId, {
2495
4402
  watermark: row.watermark ?? null,
2496
- dismissedIds: normalizeDismissedIds3(row.dismissedIds),
4403
+ dismissedIds: normalizeDismissedIds4(row.dismissedIds),
2497
4404
  lastSeen: normalizeLastSeen3(row.lastSeen),
2498
4405
  deviceCount: 1
2499
4406
  });
@@ -2506,7 +4413,7 @@ var MongoAdapter = class {
2506
4413
  if (!row) return;
2507
4414
  out.set(userId, {
2508
4415
  watermark: row.watermark ?? null,
2509
- dismissedIds: normalizeDismissedIds3(row.dismissedIds),
4416
+ dismissedIds: normalizeDismissedIds4(row.dismissedIds),
2510
4417
  lastSeen: normalizeLastSeen3(row.lastSeen),
2511
4418
  deviceCount: 1
2512
4419
  });
@@ -2543,7 +4450,7 @@ function parseDismissedIds2(value) {
2543
4450
  }
2544
4451
  return [];
2545
4452
  }
2546
- function normalizeDismissedIds4(row) {
4453
+ function normalizeDismissedIds5(row) {
2547
4454
  if (!row) return [];
2548
4455
  return parseDismissedIds2(row.dismissed_ids ?? row.dismissedIds);
2549
4456
  }
@@ -2602,7 +4509,7 @@ var SQLiteAdapter = class {
2602
4509
  );
2603
4510
  const row = result.rows[0];
2604
4511
  this.watermark = normalizeWatermark3(row);
2605
- this.dismissedIds = new Set(normalizeDismissedIds4(row));
4512
+ this.dismissedIds = new Set(normalizeDismissedIds5(row));
2606
4513
  }
2607
4514
  async dismissBatch(ids) {
2608
4515
  const uniqueIds = Array.from(new Set(ids));
@@ -2645,7 +4552,7 @@ var SQLiteAdapter = class {
2645
4552
  for (const row of result.rows) {
2646
4553
  out.set(row.user_id, {
2647
4554
  watermark: normalizeWatermark3(row),
2648
- dismissedIds: normalizeDismissedIds4(row),
4555
+ dismissedIds: normalizeDismissedIds5(row),
2649
4556
  lastSeen: normalizeLastSeen4(row),
2650
4557
  deviceCount: 1
2651
4558
  });
@@ -2684,7 +4591,7 @@ var SQLiteAdapter = class {
2684
4591
  };
2685
4592
 
2686
4593
  // src/adapters/supabase.ts
2687
- function normalizeDismissedIds5(row) {
4594
+ function normalizeDismissedIds6(row) {
2688
4595
  if (!row || !Array.isArray(row.dismissed_ids)) return [];
2689
4596
  return row.dismissed_ids.filter((id) => typeof id === "string");
2690
4597
  }
@@ -2746,7 +4653,7 @@ var SupabaseAdapter = class {
2746
4653
  try {
2747
4654
  const row = await this.fetchState(this.userId);
2748
4655
  this.watermark = normalizeWatermark4(row);
2749
- this.dismissedIds = new Set(normalizeDismissedIds5(row));
4656
+ this.dismissedIds = new Set(normalizeDismissedIds6(row));
2750
4657
  } finally {
2751
4658
  this.syncing = false;
2752
4659
  }
@@ -2782,7 +4689,7 @@ var SupabaseAdapter = class {
2782
4689
  if (!row) return;
2783
4690
  out.set(userId, {
2784
4691
  watermark: normalizeWatermark4(row),
2785
- dismissedIds: normalizeDismissedIds5(row),
4692
+ dismissedIds: normalizeDismissedIds6(row),
2786
4693
  lastSeen: normalizeLastSeen5(row),
2787
4694
  deviceCount: 1
2788
4695
  });
@@ -2839,55 +4746,87 @@ var SupabaseAdapter = class {
2839
4746
 
2840
4747
  exports.AmplitudeAdapter = AmplitudeAdapter;
2841
4748
  exports.AnalyticsCollector = AnalyticsCollector;
4749
+ exports.AudienceBuilder = AudienceBuilder;
4750
+ exports.ContentfulAdapter = ContentfulAdapter;
2842
4751
  exports.CustomAdapter = CustomAdapter;
4752
+ exports.DiscordBridge = DiscordBridge;
4753
+ exports.EmailDigestGenerator = EmailDigestGenerator;
4754
+ exports.FEATUREDROP_ANIMATION_PRESETS = FEATUREDROP_ANIMATION_PRESETS;
2843
4755
  exports.FEATUREDROP_THEMES = FEATUREDROP_THEMES;
2844
4756
  exports.FEATUREDROP_TRANSLATIONS = FEATUREDROP_TRANSLATIONS;
2845
4757
  exports.HybridAdapter = HybridAdapter;
4758
+ exports.IndexedDBAdapter = IndexedDBAdapter;
4759
+ exports.LaunchDarklyBridge = LaunchDarklyBridge;
2846
4760
  exports.LocalStorageAdapter = LocalStorageAdapter;
4761
+ exports.ManifestEditor = ManifestEditor;
4762
+ exports.MarkdownAdapter = MarkdownAdapter;
2847
4763
  exports.MemoryAdapter = MemoryAdapter;
2848
4764
  exports.MixpanelAdapter = MixpanelAdapter;
2849
4765
  exports.MongoAdapter = MongoAdapter;
2850
4766
  exports.MySQLAdapter = MySQLAdapter;
4767
+ exports.NotionAdapter = NotionAdapter;
2851
4768
  exports.PostHogAdapter = PostHogAdapter;
4769
+ exports.PostHogBridge = PostHogBridge;
2852
4770
  exports.PostgresAdapter = PostgresAdapter;
4771
+ exports.PreviewPanel = PreviewPanel;
4772
+ exports.RSSFeedGenerator = RSSFeedGenerator;
2853
4773
  exports.RedisAdapter = RedisAdapter;
2854
4774
  exports.RemoteAdapter = RemoteAdapter;
2855
4775
  exports.SQLiteAdapter = SQLiteAdapter;
4776
+ exports.SanityAdapter = SanityAdapter;
4777
+ exports.ScheduleCalendar = ScheduleCalendar;
2856
4778
  exports.SegmentAdapter = SegmentAdapter;
4779
+ exports.SlackBridge = SlackBridge;
4780
+ exports.StrapiAdapter = StrapiAdapter;
2857
4781
  exports.SupabaseAdapter = SupabaseAdapter;
2858
4782
  exports.TriggerEngine = TriggerEngine;
4783
+ exports.WebhookBridge = WebhookBridge;
2859
4784
  exports.applyAnnouncementThrottle = applyAnnouncementThrottle;
2860
4785
  exports.applyFeatureVariant = applyFeatureVariant;
2861
4786
  exports.applyFeatureVariants = applyFeatureVariants;
2862
4787
  exports.computeManifestStats = computeManifestStats;
2863
4788
  exports.createAdoptionMetrics = createAdoptionMetrics;
4789
+ exports.createChangelogRenderer = createChangelogRenderer;
4790
+ exports.createFlagBridge = createFlagBridge;
2864
4791
  exports.createManifest = createManifest;
2865
4792
  exports.createTheme = createTheme;
4793
+ exports.diffManifest = diffManifest;
2866
4794
  exports.featureEntryJsonSchema = featureEntryJsonSchema;
2867
4795
  exports.featureEntrySchema = featureEntrySchema;
2868
4796
  exports.featureManifestJsonSchema = featureManifestJsonSchema;
2869
4797
  exports.featureManifestSchema = featureManifestSchema;
4798
+ exports.formatDateForLocale = formatDateForLocale;
4799
+ exports.formatRelativeTimeForLocale = formatRelativeTimeForLocale;
4800
+ exports.generateChangelogDiff = generateChangelogDiff;
2870
4801
  exports.generateMarkdownChangelog = generateMarkdownChangelog;
2871
4802
  exports.generateRSS = generateRSS;
4803
+ exports.getAnimationDurationMs = getAnimationDurationMs;
4804
+ exports.getEnterAnimation = getEnterAnimation;
4805
+ exports.getExitAnimation = getExitAnimation;
2872
4806
  exports.getFeatureById = getFeatureById;
2873
4807
  exports.getFeatureVariantName = getFeatureVariantName;
4808
+ exports.getLocaleDirection = getLocaleDirection;
2874
4809
  exports.getNewFeatureCount = getNewFeatureCount;
2875
4810
  exports.getNewFeatures = getNewFeatures;
2876
4811
  exports.getNewFeaturesByCategory = getNewFeaturesByCategory;
2877
4812
  exports.getNewFeaturesSorted = getNewFeaturesSorted;
2878
4813
  exports.getOrCreateVariantKey = getOrCreateVariantKey;
4814
+ exports.getPulseAnimation = getPulseAnimation;
2879
4815
  exports.hasDependencyCycle = hasDependencyCycle;
2880
4816
  exports.hasNewFeature = hasNewFeature;
2881
4817
  exports.isNew = isNew;
2882
4818
  exports.isTriggerMatch = isTriggerMatch;
2883
4819
  exports.matchesAudience = matchesAudience;
2884
4820
  exports.parseDescription = parseDescription;
4821
+ exports.resolveAnimationPreset = resolveAnimationPreset;
2885
4822
  exports.resolveDependencyOrder = resolveDependencyOrder;
4823
+ exports.resolveLocale = resolveLocale;
2886
4824
  exports.resolveTheme = resolveTheme;
2887
4825
  exports.resolveTranslations = resolveTranslations;
2888
4826
  exports.runDoctor = runDoctor;
2889
4827
  exports.sortFeaturesByDependencies = sortFeaturesByDependencies;
2890
4828
  exports.themeToCSSVariables = themeToCSSVariables;
2891
4829
  exports.validateManifest = validateManifest;
4830
+ exports.validateManifestForCI = validateManifestForCI;
2892
4831
  //# sourceMappingURL=index.cjs.map
2893
4832
  //# sourceMappingURL=index.cjs.map