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.
- package/README.md +171 -0
- package/dist/admin.cjs +212 -0
- package/dist/admin.cjs.map +1 -0
- package/dist/admin.d.cts +176 -0
- package/dist/admin.d.ts +176 -0
- package/dist/admin.js +207 -0
- package/dist/admin.js.map +1 -0
- package/dist/angular.cjs +13 -3
- package/dist/angular.cjs.map +1 -1
- package/dist/angular.d.cts +4 -0
- package/dist/angular.d.ts +4 -0
- package/dist/angular.js +13 -3
- package/dist/angular.js.map +1 -1
- package/dist/bridges.cjs +422 -0
- package/dist/bridges.cjs.map +1 -0
- package/dist/bridges.d.cts +194 -0
- package/dist/bridges.d.ts +194 -0
- package/dist/bridges.js +395 -0
- package/dist/bridges.js.map +1 -0
- package/dist/ci.cjs +328 -0
- package/dist/ci.cjs.map +1 -0
- package/dist/ci.d.cts +176 -0
- package/dist/ci.d.ts +176 -0
- package/dist/ci.js +324 -0
- package/dist/ci.js.map +1 -0
- package/dist/featuredrop.cjs +162 -20
- package/dist/featuredrop.cjs.map +1 -1
- package/dist/flags.cjs +51 -0
- package/dist/flags.cjs.map +1 -0
- package/dist/flags.d.cts +48 -0
- package/dist/flags.d.ts +48 -0
- package/dist/flags.js +47 -0
- package/dist/flags.js.map +1 -0
- package/dist/index.cjs +2599 -660
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +743 -206
- package/dist/index.d.ts +743 -206
- package/dist/index.js +2555 -668
- package/dist/index.js.map +1 -1
- package/dist/preact.cjs +747 -221
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.d.cts +67 -120
- package/dist/preact.d.ts +67 -120
- package/dist/preact.js +713 -207
- package/dist/preact.js.map +1 -1
- package/dist/react.cjs +747 -221
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +67 -120
- package/dist/react.d.ts +67 -120
- package/dist/react.js +713 -207
- package/dist/react.js.map +1 -1
- package/dist/schema.cjs +78 -1
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +142 -0
- package/dist/schema.d.ts +142 -0
- package/dist/schema.js +78 -1
- package/dist/schema.js.map +1 -1
- package/dist/solid.cjs +13 -3
- package/dist/solid.cjs.map +1 -1
- package/dist/solid.d.cts +4 -0
- package/dist/solid.d.ts +4 -0
- package/dist/solid.js +13 -3
- package/dist/solid.js.map +1 -1
- package/dist/svelte.cjs +13 -3
- package/dist/svelte.cjs.map +1 -1
- package/dist/svelte.js +13 -3
- package/dist/svelte.js.map +1 -1
- package/dist/testing.cjs +136 -15
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +22 -0
- package/dist/testing.d.ts +22 -0
- package/dist/testing.js +136 -15
- package/dist/testing.js.map +1 -1
- package/dist/vue.cjs +36 -5
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +16 -5
- package/dist/vue.js.map +1 -1
- package/dist/web-components.cjs +14 -4
- package/dist/web-components.cjs.map +1 -1
- package/dist/web-components.d.cts +4 -0
- package/dist/web-components.d.ts +4 -0
- package/dist/web-components.js +14 -4
- package/dist/web-components.js.map +1 -1
- package/package.json +59 -1
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
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 =
|
|
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/
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
|
694
|
-
|
|
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
|
-
|
|
697
|
-
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
736
|
+
actions: {
|
|
737
|
+
refresh,
|
|
738
|
+
dismiss,
|
|
739
|
+
dismissAll,
|
|
740
|
+
setManifest,
|
|
741
|
+
setUserContext,
|
|
742
|
+
setAppVersion,
|
|
743
|
+
setAudienceMatcher,
|
|
744
|
+
setFlagBridge,
|
|
745
|
+
setProduct
|
|
711
746
|
},
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
747
|
+
computed: {
|
|
748
|
+
isNew: isNew2,
|
|
749
|
+
getFeature,
|
|
750
|
+
getFeatureById: getFeatureById2,
|
|
751
|
+
getFeaturesByCategory
|
|
715
752
|
},
|
|
716
|
-
|
|
717
|
-
...base.zIndex,
|
|
718
|
-
...overrides.zIndex ?? {}
|
|
719
|
-
}
|
|
753
|
+
subscribe
|
|
720
754
|
};
|
|
721
755
|
}
|
|
722
|
-
|
|
723
|
-
|
|
756
|
+
|
|
757
|
+
// src/rss.ts
|
|
758
|
+
function escape(str) {
|
|
759
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
724
760
|
}
|
|
725
|
-
function
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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, "<").replace(/>/g, ">");
|
|
872
|
+
const safeDescription = (feature.description ?? "").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
1035
|
-
const base =
|
|
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
|
|
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/
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
"
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
1404
|
-
|
|
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
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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
|
|
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
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
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
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
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 =
|
|
3651
|
+
const now = this.now();
|
|
1887
3652
|
if (!force && this.lastManifest && now - this.lastFetchTs < this.fetchInterval) {
|
|
1888
3653
|
return this.lastManifest;
|
|
1889
3654
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
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
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
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
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
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
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
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
|
|
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(
|
|
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:
|
|
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.
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
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(
|
|
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:
|
|
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
|