featuredrop 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +547 -4
  2. package/dist/admin.cjs +212 -0
  3. package/dist/admin.cjs.map +1 -0
  4. package/dist/admin.d.cts +176 -0
  5. package/dist/admin.d.ts +176 -0
  6. package/dist/admin.js +207 -0
  7. package/dist/admin.js.map +1 -0
  8. package/dist/angular.cjs +296 -0
  9. package/dist/angular.cjs.map +1 -0
  10. package/dist/angular.d.cts +233 -0
  11. package/dist/angular.d.ts +233 -0
  12. package/dist/angular.js +293 -0
  13. package/dist/angular.js.map +1 -0
  14. package/dist/bridges.cjs +401 -0
  15. package/dist/bridges.cjs.map +1 -0
  16. package/dist/bridges.d.cts +194 -0
  17. package/dist/bridges.d.ts +194 -0
  18. package/dist/bridges.js +394 -0
  19. package/dist/bridges.js.map +1 -0
  20. package/dist/ci.cjs +328 -0
  21. package/dist/ci.cjs.map +1 -0
  22. package/dist/ci.d.cts +176 -0
  23. package/dist/ci.d.ts +176 -0
  24. package/dist/ci.js +324 -0
  25. package/dist/ci.js.map +1 -0
  26. package/dist/featuredrop.cjs +1377 -0
  27. package/dist/featuredrop.cjs.map +1 -0
  28. package/dist/flags.cjs +51 -0
  29. package/dist/flags.cjs.map +1 -0
  30. package/dist/flags.d.cts +48 -0
  31. package/dist/flags.d.ts +48 -0
  32. package/dist/flags.js +47 -0
  33. package/dist/flags.js.map +1 -0
  34. package/dist/index.cjs +4734 -70
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +1516 -9
  37. package/dist/index.d.ts +1516 -9
  38. package/dist/index.js +4660 -71
  39. package/dist/index.js.map +1 -1
  40. package/dist/preact.cjs +7790 -0
  41. package/dist/preact.cjs.map +1 -0
  42. package/dist/preact.d.cts +1213 -0
  43. package/dist/preact.d.ts +1213 -0
  44. package/dist/preact.js +7760 -0
  45. package/dist/preact.js.map +1 -0
  46. package/dist/react.cjs +6678 -159
  47. package/dist/react.cjs.map +1 -1
  48. package/dist/react.d.cts +852 -112
  49. package/dist/react.d.ts +852 -112
  50. package/dist/react.js +6657 -156
  51. package/dist/react.js.map +1 -1
  52. package/dist/schema.cjs +292 -0
  53. package/dist/schema.cjs.map +1 -0
  54. package/dist/schema.d.cts +345 -0
  55. package/dist/schema.d.ts +345 -0
  56. package/dist/schema.js +286 -0
  57. package/dist/schema.js.map +1 -0
  58. package/dist/solid.cjs +383 -0
  59. package/dist/solid.cjs.map +1 -0
  60. package/dist/solid.d.cts +246 -0
  61. package/dist/solid.d.ts +246 -0
  62. package/dist/solid.js +376 -0
  63. package/dist/solid.js.map +1 -0
  64. package/dist/svelte.cjs +339 -0
  65. package/dist/svelte.cjs.map +1 -0
  66. package/dist/svelte.js +334 -0
  67. package/dist/svelte.js.map +1 -0
  68. package/dist/testing.cjs +1543 -0
  69. package/dist/testing.cjs.map +1 -0
  70. package/dist/testing.d.cts +361 -0
  71. package/dist/testing.d.ts +361 -0
  72. package/dist/testing.js +1536 -0
  73. package/dist/testing.js.map +1 -0
  74. package/dist/vue.cjs +1094 -0
  75. package/dist/vue.cjs.map +1 -0
  76. package/dist/vue.js +1082 -0
  77. package/dist/vue.js.map +1 -0
  78. package/dist/web-components.cjs +493 -0
  79. package/dist/web-components.cjs.map +1 -0
  80. package/dist/web-components.d.cts +215 -0
  81. package/dist/web-components.d.ts +215 -0
  82. package/dist/web-components.js +487 -0
  83. package/dist/web-components.js.map +1 -0
  84. package/package.json +184 -3
package/dist/index.js CHANGED
@@ -1,6 +1,277 @@
1
+ import { createRequire } from 'module';
2
+ import { z } from 'zod';
3
+ import { useState, useMemo } from 'react';
4
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
5
+ import { stat, readFile, writeFile, readdir } from 'fs/promises';
6
+ import { join, relative, sep } from 'path';
7
+
8
+ // src/semver.ts
9
+ var SEMVER_REGEX = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
10
+ function parseSemver(input) {
11
+ const match = input.trim().match(SEMVER_REGEX);
12
+ if (!match) return null;
13
+ return {
14
+ major: Number(match[1]),
15
+ minor: Number(match[2]),
16
+ patch: Number(match[3]),
17
+ prerelease: match[4] ? match[4].split(".") : []
18
+ };
19
+ }
20
+ function compareSemver(a, b) {
21
+ const pa = parseSemver(a);
22
+ const pb = parseSemver(b);
23
+ if (!pa || !pb) return 0;
24
+ for (const key of ["major", "minor", "patch"]) {
25
+ if (pa[key] !== pb[key]) return pa[key] - pb[key];
26
+ }
27
+ const aPre = pa.prerelease;
28
+ const bPre = pb.prerelease;
29
+ if (aPre.length === 0 && bPre.length === 0) return 0;
30
+ if (aPre.length === 0) return 1;
31
+ if (bPre.length === 0) return -1;
32
+ const len = Math.max(aPre.length, bPre.length);
33
+ for (let i = 0; i < len; i++) {
34
+ const ai = aPre[i];
35
+ const bi = bPre[i];
36
+ if (ai === void 0) return -1;
37
+ if (bi === void 0) return 1;
38
+ const aNum = Number(ai);
39
+ const bNum = Number(bi);
40
+ const aIsNum = Number.isInteger(aNum);
41
+ const bIsNum = Number.isInteger(bNum);
42
+ if (aIsNum && bIsNum && aNum !== bNum) return aNum - bNum;
43
+ if (aIsNum !== bIsNum) return aIsNum ? -1 : 1;
44
+ if (ai !== bi) return ai < bi ? -1 : 1;
45
+ }
46
+ return 0;
47
+ }
48
+ function parseComparator(comp) {
49
+ const match = comp.trim().match(/^(>=|<=|>|<|=)?\\s*(.+)$/);
50
+ if (!match) return null;
51
+ const op = match[1] || ">=";
52
+ const version = match[2];
53
+ if (!parseSemver(version)) return null;
54
+ return { op, version };
55
+ }
56
+ function satisfiesComparator(version, comp) {
57
+ const diff = compareSemver(version, comp.version);
58
+ switch (comp.op) {
59
+ case ">":
60
+ return diff > 0;
61
+ case ">=":
62
+ return diff >= 0;
63
+ case "<":
64
+ return diff < 0;
65
+ case "<=":
66
+ return diff <= 0;
67
+ case "=":
68
+ return diff === 0;
69
+ default:
70
+ return false;
71
+ }
72
+ }
73
+ function satisfiesRange(version, range) {
74
+ const parts = range.split(/\s+/).filter(Boolean);
75
+ if (parts.length === 0) return true;
76
+ for (const part of parts) {
77
+ const comp = parseComparator(part);
78
+ if (!comp) return false;
79
+ if (!satisfiesComparator(version, comp)) return false;
80
+ }
81
+ return true;
82
+ }
83
+
84
+ // src/triggers.ts
85
+ function wildcardToRegExp(value) {
86
+ const escaped = value.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
87
+ const pattern = `^${escaped.replace(/\*/g, ".*")}$`;
88
+ return new RegExp(pattern);
89
+ }
90
+ function matchPath(path, pattern) {
91
+ if (pattern instanceof RegExp) return pattern.test(path);
92
+ if (!pattern) return false;
93
+ if (pattern.includes("*")) return wildcardToRegExp(pattern).test(path);
94
+ return path === pattern || path.startsWith(pattern);
95
+ }
96
+ function isTriggerMatch(trigger, context) {
97
+ if (!trigger) return true;
98
+ if (!context) return false;
99
+ if (trigger.type === "page") {
100
+ const path = context.path;
101
+ if (!path) return false;
102
+ return matchPath(path, trigger.match);
103
+ }
104
+ if (trigger.type === "usage") {
105
+ const usage = context.usage ?? {};
106
+ const count = usage[trigger.event] ?? 0;
107
+ return count >= (trigger.minActions ?? 1);
108
+ }
109
+ if (trigger.type === "time") {
110
+ const elapsedMs = context.elapsedMs ?? 0;
111
+ return elapsedMs >= trigger.minSeconds * 1e3;
112
+ }
113
+ if (trigger.type === "milestone") {
114
+ return context.milestones?.has(trigger.event) ?? false;
115
+ }
116
+ if (trigger.type === "frustration") {
117
+ const usage = context.usage ?? {};
118
+ const count = usage[trigger.pattern] ?? 0;
119
+ return count >= (trigger.threshold ?? 1);
120
+ }
121
+ if (trigger.type === "scroll") {
122
+ return (context.scrollPercent ?? 0) >= (trigger.minPercent ?? 50);
123
+ }
124
+ try {
125
+ return trigger.evaluate(context);
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+ var TriggerEngine = class {
131
+ context;
132
+ constructor(initial) {
133
+ this.context = {
134
+ path: initial?.path,
135
+ events: new Set(initial?.events ?? []),
136
+ milestones: new Set(initial?.milestones ?? []),
137
+ usage: { ...initial?.usage ?? {} },
138
+ elapsedMs: initial?.elapsedMs ?? 0,
139
+ scrollPercent: initial?.scrollPercent ?? 0,
140
+ metadata: { ...initial?.metadata ?? {} }
141
+ };
142
+ }
143
+ setPath(path) {
144
+ this.context.path = path;
145
+ }
146
+ trackEvent(event) {
147
+ const next = new Set(this.context.events ?? /* @__PURE__ */ new Set());
148
+ next.add(event);
149
+ this.context.events = next;
150
+ }
151
+ trackUsage(event, delta = 1) {
152
+ const usage = { ...this.context.usage ?? {} };
153
+ usage[event] = (usage[event] ?? 0) + Math.max(1, delta);
154
+ this.context.usage = usage;
155
+ }
156
+ trackMilestone(event) {
157
+ const next = new Set(this.context.milestones ?? /* @__PURE__ */ new Set());
158
+ next.add(event);
159
+ this.context.milestones = next;
160
+ }
161
+ setElapsedMs(elapsedMs) {
162
+ this.context.elapsedMs = Math.max(0, elapsedMs);
163
+ }
164
+ setScrollPercent(scrollPercent) {
165
+ const clamped = Math.max(0, Math.min(100, scrollPercent));
166
+ this.context.scrollPercent = clamped;
167
+ }
168
+ setMetadata(next) {
169
+ this.context.metadata = { ...next };
170
+ }
171
+ getContext() {
172
+ return {
173
+ path: this.context.path,
174
+ events: new Set(this.context.events ?? []),
175
+ milestones: new Set(this.context.milestones ?? []),
176
+ usage: { ...this.context.usage ?? {} },
177
+ elapsedMs: this.context.elapsedMs,
178
+ scrollPercent: this.context.scrollPercent,
179
+ metadata: { ...this.context.metadata ?? {} }
180
+ };
181
+ }
182
+ evaluate(trigger) {
183
+ return isTriggerMatch(trigger, this.context);
184
+ }
185
+ evaluateFeature(feature) {
186
+ return this.evaluate(feature.trigger);
187
+ }
188
+ };
189
+
1
190
  // src/core.ts
2
- function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date()) {
191
+ function matchesAudience(audience, userContext) {
192
+ if (audience.plan && audience.plan.length > 0) {
193
+ if (!userContext.plan || !audience.plan.includes(userContext.plan)) {
194
+ return false;
195
+ }
196
+ }
197
+ if (audience.role && audience.role.length > 0) {
198
+ if (!userContext.role || !audience.role.includes(userContext.role)) {
199
+ return false;
200
+ }
201
+ }
202
+ if (audience.region && audience.region.length > 0) {
203
+ if (!userContext.region || !audience.region.includes(userContext.region)) {
204
+ return false;
205
+ }
206
+ }
207
+ return true;
208
+ }
209
+ function isAudienceMatch(feature, userContext, matchFn) {
210
+ if (!feature.audience) return true;
211
+ const { plan, role, region, custom } = feature.audience;
212
+ const hasRules = plan && plan.length > 0 || role && role.length > 0 || region && region.length > 0 || custom && Object.keys(custom).length > 0;
213
+ if (!hasRules) return true;
214
+ if (!userContext) return false;
215
+ if (matchFn) return matchFn(feature.audience, userContext);
216
+ return matchesAudience(feature.audience, userContext);
217
+ }
218
+ function isVersionMatch(feature, appVersion) {
219
+ const v = feature.version;
220
+ if (!v || typeof v === "string") return true;
221
+ if (!appVersion) return false;
222
+ if (!v.introduced && !v.showNewUntil && !v.deprecatedAt && !v.showIn) return true;
223
+ if (v.showIn && !satisfiesRange(appVersion, v.showIn)) return false;
224
+ if (v.introduced && compareSemver(appVersion, v.introduced) < 0) return false;
225
+ if (v.deprecatedAt && compareSemver(appVersion, v.deprecatedAt) >= 0) return false;
226
+ if (v.showNewUntil && compareSemver(appVersion, v.showNewUntil) >= 0) return false;
227
+ return true;
228
+ }
229
+ function isFlagMatch(feature, flagBridge, userContext) {
230
+ if (!feature.flagKey) return true;
231
+ if (!flagBridge) return false;
232
+ try {
233
+ return flagBridge.isEnabled(feature.flagKey, userContext);
234
+ } catch {
235
+ return false;
236
+ }
237
+ }
238
+ function isProductMatch(feature, product) {
239
+ if (!feature.product || feature.product === "*") return true;
240
+ if (!product) return false;
241
+ return feature.product === product;
242
+ }
243
+ function isDependencyMatch(feature, dismissedIds, dependencyState) {
244
+ const dependsOn = feature.dependsOn;
245
+ if (!dependsOn) return true;
246
+ const seenIds = dependencyState?.seenIds;
247
+ const clickedIds = dependencyState?.clickedIds;
248
+ const dismissedDependencyIds = dependencyState?.dismissedIds ?? dismissedIds;
249
+ if (dependsOn.seen && dependsOn.seen.length > 0) {
250
+ for (const id of dependsOn.seen) {
251
+ const seen = seenIds?.has(id) ?? false;
252
+ if (!seen && !dismissedDependencyIds.has(id)) return false;
253
+ }
254
+ }
255
+ if (dependsOn.clicked && dependsOn.clicked.length > 0) {
256
+ for (const id of dependsOn.clicked) {
257
+ if (!(clickedIds?.has(id) ?? false)) return false;
258
+ }
259
+ }
260
+ if (dependsOn.dismissed && dependsOn.dismissed.length > 0) {
261
+ for (const id of dependsOn.dismissed) {
262
+ if (!dismissedDependencyIds.has(id)) return false;
263
+ }
264
+ }
265
+ return true;
266
+ }
267
+ function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext, flagBridge, product) {
3
268
  if (dismissedIds.has(feature.id)) return false;
269
+ if (!isAudienceMatch(feature, userContext, matchAudience)) return false;
270
+ if (!isDependencyMatch(feature, dismissedIds, dependencyState)) return false;
271
+ if (!isVersionMatch(feature, appVersion)) return false;
272
+ if (!isFlagMatch(feature, flagBridge, userContext)) return false;
273
+ if (!isProductMatch(feature, product)) return false;
274
+ if (!isTriggerMatch(feature.trigger, triggerContext)) return false;
4
275
  const nowMs = now.getTime();
5
276
  if (feature.publishAt) {
6
277
  const publishMs = new Date(feature.publishAt).getTime();
@@ -15,29 +286,79 @@ function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date(
15
286
  }
16
287
  return true;
17
288
  }
18
- function getNewFeatures(manifest, storage, now = /* @__PURE__ */ new Date()) {
289
+ function getNewFeatures(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext, flagBridge, product) {
19
290
  const watermark = storage.getWatermark();
20
291
  const dismissedIds = storage.getDismissedIds();
21
- return manifest.filter((f) => isNew(f, watermark, dismissedIds, now));
292
+ return manifest.filter(
293
+ (f) => isNew(
294
+ f,
295
+ watermark,
296
+ dismissedIds,
297
+ now,
298
+ userContext,
299
+ matchAudience,
300
+ appVersion,
301
+ dependencyState,
302
+ triggerContext,
303
+ flagBridge,
304
+ product
305
+ )
306
+ );
22
307
  }
23
- function getNewFeatureCount(manifest, storage, now = /* @__PURE__ */ new Date()) {
24
- return getNewFeatures(manifest, storage, now).length;
308
+ function getNewFeatureCount(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext, flagBridge, product) {
309
+ return getNewFeatures(
310
+ manifest,
311
+ storage,
312
+ now,
313
+ userContext,
314
+ matchAudience,
315
+ appVersion,
316
+ dependencyState,
317
+ triggerContext,
318
+ flagBridge,
319
+ product
320
+ ).length;
25
321
  }
26
- function hasNewFeature(manifest, sidebarKey, storage, now = /* @__PURE__ */ new Date()) {
322
+ function hasNewFeature(manifest, sidebarKey, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext, flagBridge, product) {
27
323
  const watermark = storage.getWatermark();
28
324
  const dismissedIds = storage.getDismissedIds();
29
325
  return manifest.some(
30
- (f) => f.sidebarKey === sidebarKey && isNew(f, watermark, dismissedIds, now)
326
+ (f) => f.sidebarKey === sidebarKey && isNew(
327
+ f,
328
+ watermark,
329
+ dismissedIds,
330
+ now,
331
+ userContext,
332
+ matchAudience,
333
+ appVersion,
334
+ dependencyState,
335
+ triggerContext,
336
+ flagBridge,
337
+ product
338
+ )
31
339
  );
32
340
  }
33
- function getNewFeaturesSorted(manifest, storage, now = /* @__PURE__ */ new Date()) {
341
+ function getNewFeaturesSorted(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext, flagBridge, product) {
34
342
  const priorityOrder = { critical: 0, normal: 1, low: 2 };
35
- return getNewFeatures(manifest, storage, now).sort((a, b) => {
36
- const pa = priorityOrder[a.priority ?? "normal"];
37
- const pb = priorityOrder[b.priority ?? "normal"];
38
- if (pa !== pb) return pa - pb;
39
- return new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime();
40
- });
343
+ return getNewFeatures(
344
+ manifest,
345
+ storage,
346
+ now,
347
+ userContext,
348
+ matchAudience,
349
+ appVersion,
350
+ dependencyState,
351
+ triggerContext,
352
+ flagBridge,
353
+ product
354
+ ).sort(
355
+ (a, b) => {
356
+ const pa = priorityOrder[a.priority ?? "normal"];
357
+ const pb = priorityOrder[b.priority ?? "normal"];
358
+ if (pa !== pb) return pa - pb;
359
+ return new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime();
360
+ }
361
+ );
41
362
  }
42
363
 
43
364
  // src/helpers.ts
@@ -47,90 +368,4358 @@ function createManifest(entries) {
47
368
  function getFeatureById(manifest, id) {
48
369
  return manifest.find((f) => f.id === id);
49
370
  }
50
- function getNewFeaturesByCategory(manifest, category, storage, now = /* @__PURE__ */ new Date()) {
371
+ function getNewFeaturesByCategory(manifest, category, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion) {
51
372
  const watermark = storage.getWatermark();
52
373
  const dismissedIds = storage.getDismissedIds();
53
374
  return manifest.filter(
54
- (f) => f.category === category && isNew(f, watermark, dismissedIds, now)
375
+ (f) => f.category === category && isNew(f, watermark, dismissedIds, now, userContext, matchAudience, appVersion)
55
376
  );
56
377
  }
57
-
58
- // src/adapters/local-storage.ts
59
- var DISMISSED_SUFFIX = ":dismissed";
60
- var LocalStorageAdapter = class {
61
- prefix;
62
- watermarkValue;
63
- onDismissAllCallback;
64
- dismissedKey;
65
- constructor(options = {}) {
66
- this.prefix = options.prefix ?? "featuredrop";
67
- this.watermarkValue = options.watermark ?? null;
68
- this.onDismissAllCallback = options.onDismissAll;
69
- this.dismissedKey = `${this.prefix}${DISMISSED_SUFFIX}`;
70
- }
71
- getWatermark() {
72
- return this.watermarkValue;
378
+ var dynamicRequire = createRequire(import.meta.url);
379
+ var cachedMarked = null;
380
+ var cachedShiki = null;
381
+ function optionalRequire(name) {
382
+ try {
383
+ return dynamicRequire(name);
384
+ } catch (error) {
385
+ if (error && typeof error === "object" && "code" in error && error.code === "MODULE_NOT_FOUND") {
386
+ return null;
387
+ }
388
+ return null;
73
389
  }
74
- getDismissedIds() {
390
+ }
391
+ function getMarked() {
392
+ if (cachedMarked !== null) return cachedMarked || null;
393
+ cachedMarked = optionalRequire("marked") ?? false;
394
+ return cachedMarked || null;
395
+ }
396
+ function getShiki() {
397
+ if (cachedShiki !== null) return cachedShiki || null;
398
+ cachedShiki = optionalRequire("shiki") ?? false;
399
+ return cachedShiki || null;
400
+ }
401
+ function escapeHtml(value) {
402
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
403
+ }
404
+ function sanitizeUrl(url) {
405
+ if (!url) return null;
406
+ const trimmed = url.trim();
407
+ if (!trimmed) return null;
408
+ const lower = trimmed.toLowerCase();
409
+ if (lower.startsWith("javascript:")) return null;
410
+ if (lower.startsWith("data:")) return null;
411
+ if (lower.startsWith("vbscript:")) return null;
412
+ if (/['"<>\s]/.test(trimmed)) return null;
413
+ return trimmed;
414
+ }
415
+ function sanitizeHtml(html) {
416
+ return html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?>[\s\S]*?<\/style>/gi, "").replace(/\s+on[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/\s+(?:href|src|xlink:href)\s*=\s*("|')(?:javascript:|data:)[^"']*\1/gi, "");
417
+ }
418
+ function decodeAllowedEntities(html) {
419
+ const allowTags = [
420
+ "p",
421
+ "strong",
422
+ "em",
423
+ "a",
424
+ "code",
425
+ "pre",
426
+ "img",
427
+ "ul",
428
+ "ol",
429
+ "li",
430
+ "blockquote",
431
+ "h1",
432
+ "h2",
433
+ "h3",
434
+ "h4",
435
+ "h5",
436
+ "h6",
437
+ "br"
438
+ ];
439
+ return html.replace(/&lt;(\/?)([a-z0-9]+)([^>]*)&gt;/gi, (match, slash, tag, rest) => {
440
+ if (!allowTags.includes(tag.toLowerCase())) return match;
441
+ const decodedRest = rest.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
442
+ return `<${slash}${tag}${decodedRest}>`;
443
+ });
444
+ }
445
+ function renderCodeBlock(code, language) {
446
+ const shiki = getShiki();
447
+ if (shiki?.codeToHtml) {
75
448
  try {
76
- if (typeof window === "undefined") return /* @__PURE__ */ new Set();
77
- const raw = localStorage.getItem(this.dismissedKey);
78
- if (!raw) return /* @__PURE__ */ new Set();
79
- const parsed = JSON.parse(raw);
80
- if (Array.isArray(parsed)) return new Set(parsed);
81
- return /* @__PURE__ */ new Set();
449
+ const rendered = shiki.codeToHtml(code, { lang: language || "text", theme: "github-dark" });
450
+ if (typeof rendered === "string") return rendered;
82
451
  } catch {
83
- return /* @__PURE__ */ new Set();
84
452
  }
85
453
  }
86
- dismiss(id) {
87
- try {
88
- if (typeof window === "undefined") return;
89
- const raw = localStorage.getItem(this.dismissedKey);
90
- const existing = raw ? JSON.parse(raw) : [];
91
- if (!existing.includes(id)) {
92
- existing.push(id);
93
- localStorage.setItem(this.dismissedKey, JSON.stringify(existing));
454
+ const langAttr = language ? ` class="language-${escapeHtml(language)}"` : "";
455
+ return `<pre><code${langAttr}>${escapeHtml(code)}</code></pre>`;
456
+ }
457
+ function inlineMarkdown(text) {
458
+ let result = escapeHtml(text);
459
+ const codeSpans = [];
460
+ result = result.replace(/`([^`]+)`/g, (_match, code) => {
461
+ const idx = codeSpans.length;
462
+ codeSpans.push(`<code>${escapeHtml(code)}</code>`);
463
+ return `\xA7\xA7CODE${idx}\xA7\xA7`;
464
+ });
465
+ result = result.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, url) => {
466
+ const safeUrl = sanitizeUrl(url);
467
+ const safeAlt = escapeHtml(alt ?? "");
468
+ if (!safeUrl) return safeAlt;
469
+ return `<img src="${escapeHtml(safeUrl)}" alt="${safeAlt}" />`;
470
+ });
471
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, url) => {
472
+ const safeUrl = sanitizeUrl(url);
473
+ const safeLabel = escapeHtml(label ?? "");
474
+ if (!safeUrl) return safeLabel;
475
+ return `<a href="${escapeHtml(safeUrl)}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`;
476
+ });
477
+ result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
478
+ result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
479
+ result = result.replace(/§§CODE(\d+)§§/g, (_m, idx) => codeSpans[Number(idx)] ?? "");
480
+ return result;
481
+ }
482
+ function fallbackParse(markdown) {
483
+ const lines = markdown.split(/\r?\n/);
484
+ const blocks = [];
485
+ let listBuffer = null;
486
+ let quoteBuffer = null;
487
+ let inCodeBlock = false;
488
+ let codeLang;
489
+ let codeLines = [];
490
+ const flushList = () => {
491
+ if (!listBuffer) return;
492
+ blocks.push(`<ul>${listBuffer.map((item) => `<li>${item}</li>`).join("")}</ul>`);
493
+ listBuffer = null;
494
+ };
495
+ const flushQuote = () => {
496
+ if (!quoteBuffer) return;
497
+ const content = quoteBuffer.map((line) => inlineMarkdown(line.trim())).join("<br>");
498
+ blocks.push(`<blockquote>${content}</blockquote>`);
499
+ quoteBuffer = null;
500
+ };
501
+ const flushCode = () => {
502
+ if (!inCodeBlock) return;
503
+ blocks.push(renderCodeBlock(codeLines.join("\n"), codeLang));
504
+ codeLines = [];
505
+ codeLang = void 0;
506
+ inCodeBlock = false;
507
+ };
508
+ for (const rawLine of lines) {
509
+ const line = rawLine.replace(/\s+$/, "");
510
+ const codeFence = line.match(/^```(.*)$/);
511
+ if (codeFence) {
512
+ if (inCodeBlock) {
513
+ flushCode();
514
+ } else {
515
+ flushList();
516
+ flushQuote();
517
+ inCodeBlock = true;
518
+ codeLang = codeFence[1]?.trim() || void 0;
519
+ codeLines = [];
94
520
  }
95
- } catch {
521
+ continue;
522
+ }
523
+ if (inCodeBlock) {
524
+ codeLines.push(rawLine);
525
+ continue;
526
+ }
527
+ const listMatch = line.match(/^\s*[-*+]\s+(.*)$/);
528
+ if (listMatch) {
529
+ flushQuote();
530
+ listBuffer = listBuffer ?? [];
531
+ listBuffer.push(inlineMarkdown(listMatch[1].trim()));
532
+ continue;
533
+ }
534
+ if (listBuffer) flushList();
535
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
536
+ if (headingMatch) {
537
+ flushQuote();
538
+ const level = headingMatch[1].length;
539
+ const content = inlineMarkdown(headingMatch[2].trim());
540
+ blocks.push(`<h${level}>${content}</h${level}>`);
541
+ continue;
96
542
  }
543
+ const quoteMatch = line.match(/^>\s?(.*)$/);
544
+ if (quoteMatch) {
545
+ quoteBuffer = quoteBuffer ?? [];
546
+ quoteBuffer.push(quoteMatch[1]);
547
+ continue;
548
+ }
549
+ if (quoteBuffer) flushQuote();
550
+ if (!line.trim()) {
551
+ continue;
552
+ }
553
+ blocks.push(`<p>${inlineMarkdown(line.trim())}</p>`);
97
554
  }
98
- async dismissAll(now) {
555
+ flushList();
556
+ flushQuote();
557
+ flushCode();
558
+ return blocks.join("\n");
559
+ }
560
+ function renderWithMarked(markdown, marked) {
561
+ if (!marked.parse) return null;
562
+ const renderer = marked.Renderer ? new marked.Renderer() : void 0;
563
+ if (renderer) {
564
+ renderer.link = (href, _title, text) => {
565
+ const safeUrl = sanitizeUrl(href);
566
+ if (!safeUrl) return escapeHtml(text);
567
+ return `<a href="${escapeHtml(safeUrl)}" target="_blank" rel="noopener noreferrer">${text}</a>`;
568
+ };
569
+ renderer.image = (href, _title, text) => {
570
+ const safeUrl = sanitizeUrl(href);
571
+ const safeAlt = escapeHtml(text ?? "");
572
+ if (!safeUrl) return safeAlt;
573
+ return `<img src="${escapeHtml(safeUrl)}" alt="${safeAlt}" />`;
574
+ };
575
+ }
576
+ const output = marked.parse(markdown, renderer ? { renderer } : void 0);
577
+ if (typeof output === "string") return output;
578
+ return output ? String(output) : null;
579
+ }
580
+ function parseDescription(markdown) {
581
+ if (!markdown) return "";
582
+ const marked = getMarked();
583
+ if (marked) {
99
584
  try {
100
- if (typeof window !== "undefined") {
101
- localStorage.removeItem(this.dismissedKey);
585
+ const rendered = renderWithMarked(markdown, marked);
586
+ if (rendered) {
587
+ const sanitized2 = sanitizeHtml(rendered);
588
+ const decoded2 = decodeAllowedEntities(sanitized2);
589
+ return sanitizeHtml(decoded2);
102
590
  }
103
591
  } catch {
104
592
  }
105
- if (this.onDismissAllCallback) {
106
- await this.onDismissAllCallback(now);
593
+ }
594
+ if (/<[^>]+>/.test(markdown)) {
595
+ const sanitized2 = sanitizeHtml(markdown);
596
+ const decoded2 = decodeAllowedEntities(sanitized2);
597
+ return sanitizeHtml(decoded2);
598
+ }
599
+ const fallback = fallbackParse(markdown);
600
+ const sanitized = sanitizeHtml(fallback);
601
+ const decoded = decodeAllowedEntities(sanitized);
602
+ return sanitizeHtml(decoded);
603
+ }
604
+
605
+ // src/renderer.ts
606
+ function sortFeatures(features) {
607
+ const priorityOrder = { critical: 0, normal: 1, low: 2 };
608
+ return [...features].sort((a, b) => {
609
+ const pa = priorityOrder[a.priority ?? "normal"];
610
+ const pb = priorityOrder[b.priority ?? "normal"];
611
+ if (pa !== pb) return pa - pb;
612
+ return new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime();
613
+ });
614
+ }
615
+ function createChangelogRenderer({
616
+ manifest: initialManifest,
617
+ storage,
618
+ userContext: initialUserContext,
619
+ matchAudience: initialMatchAudience,
620
+ appVersion: initialAppVersion,
621
+ flagBridge: initialFlagBridge,
622
+ product: initialProduct,
623
+ now = () => /* @__PURE__ */ new Date()
624
+ }) {
625
+ let manifest = initialManifest;
626
+ let userContext = initialUserContext;
627
+ let matchAudience = initialMatchAudience;
628
+ let appVersion = initialAppVersion;
629
+ let flagBridge = initialFlagBridge;
630
+ let product = initialProduct;
631
+ const listeners = /* @__PURE__ */ new Set();
632
+ let state = {
633
+ manifest,
634
+ newFeatures: [],
635
+ newFeaturesSorted: [],
636
+ newCount: 0,
637
+ watermark: storage.getWatermark(),
638
+ dismissedIds: new Set(storage.getDismissedIds())
639
+ };
640
+ const refresh = () => {
641
+ const features = getNewFeatures(
642
+ manifest,
643
+ storage,
644
+ now(),
645
+ userContext,
646
+ matchAudience,
647
+ appVersion,
648
+ void 0,
649
+ void 0,
650
+ flagBridge,
651
+ product
652
+ );
653
+ state = {
654
+ manifest,
655
+ newFeatures: features,
656
+ newFeaturesSorted: sortFeatures(features),
657
+ newCount: features.length,
658
+ watermark: storage.getWatermark(),
659
+ dismissedIds: new Set(storage.getDismissedIds())
660
+ };
661
+ listeners.forEach((listener) => listener(state));
662
+ };
663
+ const dismiss = (id) => {
664
+ if (!id) return;
665
+ storage.dismiss(id);
666
+ refresh();
667
+ };
668
+ const dismissAll = async () => {
669
+ await storage.dismissAll(now());
670
+ refresh();
671
+ };
672
+ const setManifest = (nextManifest) => {
673
+ manifest = nextManifest;
674
+ refresh();
675
+ };
676
+ const setUserContext = (nextUserContext) => {
677
+ userContext = nextUserContext;
678
+ refresh();
679
+ };
680
+ const setAppVersion = (nextAppVersion) => {
681
+ appVersion = nextAppVersion;
682
+ refresh();
683
+ };
684
+ const setAudienceMatcher = (nextMatchAudience) => {
685
+ matchAudience = nextMatchAudience;
686
+ refresh();
687
+ };
688
+ const setFlagBridge = (nextFlagBridge) => {
689
+ flagBridge = nextFlagBridge;
690
+ refresh();
691
+ };
692
+ const setProduct = (nextProduct) => {
693
+ product = nextProduct;
694
+ refresh();
695
+ };
696
+ const isNew2 = (sidebarKey) => state.newFeatures.some((feature) => feature.sidebarKey === sidebarKey);
697
+ const getFeature = (sidebarKey) => state.newFeatures.find((feature) => feature.sidebarKey === sidebarKey);
698
+ const getFeatureById2 = (id) => state.newFeatures.find((feature) => feature.id === id);
699
+ const getFeaturesByCategory = (category) => state.newFeatures.filter((feature) => feature.category === category);
700
+ const subscribe = (listener) => {
701
+ listeners.add(listener);
702
+ listener(state);
703
+ return () => {
704
+ listeners.delete(listener);
705
+ };
706
+ };
707
+ refresh();
708
+ return {
709
+ get state() {
710
+ return state;
711
+ },
712
+ actions: {
713
+ refresh,
714
+ dismiss,
715
+ dismissAll,
716
+ setManifest,
717
+ setUserContext,
718
+ setAppVersion,
719
+ setAudienceMatcher,
720
+ setFlagBridge,
721
+ setProduct
722
+ },
723
+ computed: {
724
+ isNew: isNew2,
725
+ getFeature,
726
+ getFeatureById: getFeatureById2,
727
+ getFeaturesByCategory
728
+ },
729
+ subscribe
730
+ };
731
+ }
732
+
733
+ // src/rss.ts
734
+ function escape(str) {
735
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
736
+ }
737
+ function generateRSS(manifest, options) {
738
+ const title = escape(options?.title ?? "Featuredrop Changelog");
739
+ const link = escape(options?.link ?? "");
740
+ const desc = escape(options?.description ?? "Product updates");
741
+ const items = manifest.slice().sort((a, b) => new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime()).map((item) => {
742
+ const descriptionHtml = item.description ? parseDescription(item.description) : "";
743
+ const itemLink = item.url ? escape(item.url) : "";
744
+ return [
745
+ "<item>",
746
+ `<title>${escape(item.label)}</title>`,
747
+ itemLink ? `<link>${itemLink}</link>` : "",
748
+ `<guid isPermaLink="false">${escape(item.id)}</guid>`,
749
+ `<pubDate>${new Date(item.releasedAt).toUTCString()}</pubDate>`,
750
+ `<description><![CDATA[${descriptionHtml}]]></description>`,
751
+ "</item>"
752
+ ].join("");
753
+ }).join("");
754
+ return [
755
+ '<?xml version="1.0" encoding="UTF-8"?>',
756
+ '<rss version="2.0">',
757
+ "<channel>",
758
+ `<title>${title}</title>`,
759
+ link ? `<link>${link}</link>` : "",
760
+ `<description>${desc}</description>`,
761
+ items,
762
+ "</channel>",
763
+ "</rss>"
764
+ ].join("");
765
+ }
766
+
767
+ // src/bridges.ts
768
+ async function postJson(url, payload, headers) {
769
+ const response = await fetch(url, {
770
+ method: "POST",
771
+ headers: {
772
+ "Content-Type": "application/json",
773
+ ...headers ?? {}
774
+ },
775
+ body: JSON.stringify(payload)
776
+ });
777
+ if (!response.ok) {
778
+ throw new Error(`[featuredrop] Bridge request failed (${response.status}) for ${url}`);
779
+ }
780
+ }
781
+ function formatFeatureLine(feature) {
782
+ const released = new Date(feature.releasedAt).toLocaleDateString("en-US", {
783
+ year: "numeric",
784
+ month: "short",
785
+ day: "numeric"
786
+ });
787
+ return `${feature.label} (${released})`;
788
+ }
789
+ var SlackBridge = {
790
+ async notify(feature, options) {
791
+ const payload = options.formatter ? options.formatter(feature) : {
792
+ username: options.username,
793
+ icon_emoji: options.iconEmoji,
794
+ channel: options.channel,
795
+ text: `New feature published: *${feature.label}*`,
796
+ attachments: [
797
+ {
798
+ color: "#2563eb",
799
+ title: feature.label,
800
+ text: feature.description ?? "No description provided.",
801
+ title_link: feature.url,
802
+ footer: `featuredrop | ${feature.id}`
803
+ }
804
+ ]
805
+ };
806
+ await postJson(options.webhookUrl, payload);
807
+ }
808
+ };
809
+ var DiscordBridge = {
810
+ async notify(feature, options) {
811
+ const payload = options.formatter ? options.formatter(feature) : {
812
+ username: options.username ?? "featuredrop",
813
+ avatar_url: options.avatarUrl,
814
+ embeds: [
815
+ {
816
+ title: feature.label,
817
+ description: feature.description ?? "No description provided.",
818
+ url: feature.url,
819
+ color: 2450411,
820
+ footer: {
821
+ text: `featuredrop | ${feature.id}`
822
+ }
823
+ }
824
+ ]
825
+ };
826
+ await postJson(options.webhookUrl, payload);
827
+ }
828
+ };
829
+ var WebhookBridge = {
830
+ async post(feature, options) {
831
+ const payload = {
832
+ event: options.event ?? "feature.published",
833
+ feature,
834
+ sentAt: (/* @__PURE__ */ new Date()).toISOString(),
835
+ ...options.body ?? {}
836
+ };
837
+ await postJson(options.url, payload, options.headers);
838
+ }
839
+ };
840
+ var EmailDigestGenerator = {
841
+ generate(features, options = {}) {
842
+ const title = options.title ?? "Product Updates";
843
+ const intro = options.intro ?? "Here are the latest updates:";
844
+ const productName = options.productName ?? "Your Product";
845
+ const template = options.template ?? "default";
846
+ const listItems = features.map((feature) => {
847
+ const safeLabel = feature.label.replace(/</g, "&lt;").replace(/>/g, "&gt;");
848
+ const safeDescription = (feature.description ?? "").replace(/</g, "&lt;").replace(/>/g, "&gt;");
849
+ const link = feature.url ? `<a href="${feature.url}" style="color:#2563eb;text-decoration:none;">Read more</a>` : "";
850
+ if (template === "minimal") {
851
+ return `<li><strong>${safeLabel}</strong>${safeDescription ? ` - ${safeDescription}` : ""}</li>`;
852
+ }
853
+ return [
854
+ '<li style="margin:0 0 14px;">',
855
+ `<p style="margin:0 0 4px;font-weight:600;color:#111827;">${safeLabel}</p>`,
856
+ safeDescription ? `<p style="margin:0 0 6px;color:#4b5563;line-height:1.45;">${safeDescription}</p>` : "",
857
+ link ? `<p style="margin:0;">${link}</p>` : "",
858
+ "</li>"
859
+ ].join("");
860
+ }).join("");
861
+ if (template === "minimal") {
862
+ return [
863
+ "<!doctype html>",
864
+ "<html><body>",
865
+ `<h2>${title}</h2>`,
866
+ `<p>${intro}</p>`,
867
+ `<ul>${listItems}</ul>`,
868
+ "</body></html>"
869
+ ].join("");
107
870
  }
871
+ const summary = features.map((feature) => formatFeatureLine(feature)).join(" | ");
872
+ return [
873
+ "<!doctype html>",
874
+ "<html>",
875
+ `<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f8fafc;padding:20px;">`,
876
+ '<div style="max-width:640px;margin:0 auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;">',
877
+ `<p style="margin:0 0 12px;color:#6b7280;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;">${productName}</p>`,
878
+ `<h1 style="margin:0 0 8px;font-size:22px;color:#111827;">${title}</h1>`,
879
+ `<p style="margin:0 0 14px;color:#374151;">${intro}</p>`,
880
+ `<p style="margin:0 0 18px;color:#6b7280;font-size:13px;">${summary}</p>`,
881
+ `<ul style="padding-left:18px;margin:0;">${listItems}</ul>`,
882
+ "</div>",
883
+ "</body>",
884
+ "</html>"
885
+ ].join("");
886
+ }
887
+ };
888
+ var RSSFeedGenerator = {
889
+ generate(manifest, options) {
890
+ return generateRSS(manifest, options);
108
891
  }
109
892
  };
110
893
 
111
- // src/adapters/memory.ts
112
- var MemoryAdapter = class {
113
- watermark;
114
- dismissed;
115
- constructor(options = {}) {
116
- this.watermark = options.watermark ?? null;
117
- this.dismissed = /* @__PURE__ */ new Set();
894
+ // src/dependencies.ts
895
+ function getDirectDependencies(feature) {
896
+ const dependsOn = feature.dependsOn;
897
+ if (!dependsOn) return [];
898
+ const seen = dependsOn.seen ?? [];
899
+ const clicked = dependsOn.clicked ?? [];
900
+ const dismissed = dependsOn.dismissed ?? [];
901
+ const unique = /* @__PURE__ */ new Set();
902
+ for (const id of [...seen, ...clicked, ...dismissed]) {
903
+ if (id) unique.add(id);
118
904
  }
119
- getWatermark() {
120
- return this.watermark;
905
+ return Array.from(unique);
906
+ }
907
+ function resolveDependencyOrder(manifest) {
908
+ const ids = new Set(manifest.map((feature) => feature.id));
909
+ const outgoing = /* @__PURE__ */ new Map();
910
+ const indegree = /* @__PURE__ */ new Map();
911
+ for (const feature of manifest) {
912
+ outgoing.set(feature.id, /* @__PURE__ */ new Set());
913
+ indegree.set(feature.id, 0);
121
914
  }
122
- getDismissedIds() {
123
- return this.dismissed;
915
+ for (const feature of manifest) {
916
+ for (const dependencyId of getDirectDependencies(feature)) {
917
+ if (!ids.has(dependencyId)) continue;
918
+ const edges = outgoing.get(dependencyId);
919
+ if (!edges || edges.has(feature.id)) continue;
920
+ edges.add(feature.id);
921
+ indegree.set(feature.id, (indegree.get(feature.id) ?? 0) + 1);
922
+ }
124
923
  }
125
- dismiss(id) {
126
- this.dismissed.add(id);
924
+ const queue = [];
925
+ for (const feature of manifest) {
926
+ if ((indegree.get(feature.id) ?? 0) === 0) queue.push(feature.id);
127
927
  }
128
- async dismissAll(now) {
129
- this.watermark = now.toISOString();
130
- this.dismissed.clear();
928
+ const ordered = [];
929
+ while (queue.length > 0) {
930
+ const id = queue.shift();
931
+ if (!id) continue;
932
+ ordered.push(id);
933
+ const edges = outgoing.get(id);
934
+ if (!edges) continue;
935
+ for (const nextId of edges) {
936
+ const nextDegree = (indegree.get(nextId) ?? 0) - 1;
937
+ indegree.set(nextId, nextDegree);
938
+ if (nextDegree === 0) queue.push(nextId);
939
+ }
940
+ }
941
+ if (ordered.length < manifest.length) {
942
+ const included = new Set(ordered);
943
+ for (const feature of manifest) {
944
+ if (included.has(feature.id)) continue;
945
+ ordered.push(feature.id);
946
+ }
947
+ }
948
+ return ordered;
949
+ }
950
+ function hasDependencyCycle(manifest) {
951
+ const ids = new Set(manifest.map((feature) => feature.id));
952
+ const outgoing = /* @__PURE__ */ new Map();
953
+ const indegree = /* @__PURE__ */ new Map();
954
+ for (const feature of manifest) {
955
+ outgoing.set(feature.id, /* @__PURE__ */ new Set());
956
+ indegree.set(feature.id, 0);
957
+ }
958
+ for (const feature of manifest) {
959
+ for (const dependencyId of getDirectDependencies(feature)) {
960
+ if (!ids.has(dependencyId)) continue;
961
+ const edges = outgoing.get(dependencyId);
962
+ if (!edges || edges.has(feature.id)) continue;
963
+ edges.add(feature.id);
964
+ indegree.set(feature.id, (indegree.get(feature.id) ?? 0) + 1);
965
+ }
966
+ }
967
+ const queue = [];
968
+ for (const feature of manifest) {
969
+ if ((indegree.get(feature.id) ?? 0) === 0) queue.push(feature.id);
970
+ }
971
+ let visited = 0;
972
+ while (queue.length > 0) {
973
+ const id = queue.shift();
974
+ if (!id) continue;
975
+ visited += 1;
976
+ const edges = outgoing.get(id);
977
+ if (!edges) continue;
978
+ for (const nextId of edges) {
979
+ const nextDegree = (indegree.get(nextId) ?? 0) - 1;
980
+ indegree.set(nextId, nextDegree);
981
+ if (nextDegree === 0) queue.push(nextId);
982
+ }
983
+ }
984
+ return visited !== manifest.length;
985
+ }
986
+ function sortFeaturesByDependencies(features) {
987
+ if (features.length <= 1) return [...features];
988
+ const order = resolveDependencyOrder(features);
989
+ const rank = new Map(order.map((id, index) => [id, index]));
990
+ return [...features].sort((a, b) => {
991
+ const ra = rank.get(a.id);
992
+ const rb = rank.get(b.id);
993
+ if (ra === void 0 || rb === void 0) return 0;
994
+ return ra - rb;
995
+ });
996
+ }
997
+ var featureEntryJsonSchema = {
998
+ type: "object",
999
+ required: ["id", "label", "releasedAt", "showNewUntil"],
1000
+ properties: {
1001
+ id: { type: "string" },
1002
+ label: { type: "string" },
1003
+ description: { type: "string" },
1004
+ releasedAt: { type: "string", format: "date-time" },
1005
+ showNewUntil: { type: "string", format: "date-time" },
1006
+ flagKey: { type: "string" },
1007
+ product: { type: "string" },
1008
+ url: { type: "string" },
1009
+ image: { type: "string" },
1010
+ type: { enum: ["feature", "improvement", "fix", "breaking"] },
1011
+ priority: { enum: ["critical", "normal", "low"] },
1012
+ cta: {
1013
+ type: "object",
1014
+ properties: {
1015
+ label: { type: "string" },
1016
+ url: { type: "string" }
1017
+ }
1018
+ },
1019
+ meta: { type: "object" }
1020
+ }
1021
+ };
1022
+ var featureManifestJsonSchema = {
1023
+ type: "array",
1024
+ items: featureEntryJsonSchema
1025
+ };
1026
+ function isRecord(value) {
1027
+ return !!value && typeof value === "object" && !Array.isArray(value);
1028
+ }
1029
+ function isValidDate(value) {
1030
+ return Number.isFinite(new Date(value).getTime());
1031
+ }
1032
+ var nonEmptyString = z.string().trim().min(1, "must be a non-empty string");
1033
+ var isoDateString = nonEmptyString.refine(isValidDate, {
1034
+ message: "must be a valid date",
1035
+ params: { featuredropCode: "invalid_date" }
1036
+ });
1037
+ var dependsOnSchema = z.object({
1038
+ seen: z.array(z.string()).optional(),
1039
+ clicked: z.array(z.string()).optional(),
1040
+ dismissed: z.array(z.string()).optional()
1041
+ }).optional();
1042
+ var ctaSchema = z.object({
1043
+ label: nonEmptyString,
1044
+ url: nonEmptyString
1045
+ }).optional();
1046
+ var featureEntrySchema = z.object({
1047
+ id: nonEmptyString,
1048
+ label: nonEmptyString,
1049
+ releasedAt: isoDateString,
1050
+ showNewUntil: isoDateString,
1051
+ description: z.string().optional(),
1052
+ flagKey: z.string().optional(),
1053
+ product: z.string().optional(),
1054
+ url: z.string().optional(),
1055
+ image: z.string().optional(),
1056
+ type: z.enum(["feature", "improvement", "fix", "breaking"]).optional(),
1057
+ priority: z.enum(["critical", "normal", "low"]).optional(),
1058
+ cta: ctaSchema,
1059
+ meta: z.record(z.unknown()).optional(),
1060
+ dependsOn: dependsOnSchema
1061
+ }).passthrough();
1062
+ var featureManifestSchema = z.array(featureEntrySchema);
1063
+ function toIssuePath(path) {
1064
+ if (path.length === 0) return "$";
1065
+ let output = "";
1066
+ for (const part of path) {
1067
+ if (typeof part === "number") output += `[${part}]`;
1068
+ else output += output ? `.${part}` : part;
1069
+ }
1070
+ return output;
1071
+ }
1072
+ function mapZodIssue(issue) {
1073
+ const codeParam = issue.params?.featuredropCode;
1074
+ if (codeParam === "invalid_date") {
1075
+ return {
1076
+ path: toIssuePath(issue.path),
1077
+ message: issue.message,
1078
+ code: "invalid_date"
1079
+ };
1080
+ }
1081
+ if (issue.code === "invalid_type") {
1082
+ return {
1083
+ path: toIssuePath(issue.path),
1084
+ message: issue.message,
1085
+ code: issue.received === "undefined" ? "missing_required" : "invalid_type"
1086
+ };
1087
+ }
1088
+ return {
1089
+ path: toIssuePath(issue.path),
1090
+ message: issue.message,
1091
+ code: "invalid_value"
1092
+ };
1093
+ }
1094
+ var UNSAFE_META_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1095
+ function isSafeUrl(value) {
1096
+ const normalized = value.trim();
1097
+ if (!normalized) return false;
1098
+ if (/^(\/|\.\/|\.\.\/|\?|#)/.test(normalized)) return true;
1099
+ if (/^https?:\/\//i.test(normalized)) return true;
1100
+ return false;
1101
+ }
1102
+ function findUnsafeMetaPath(value, path = "meta") {
1103
+ if (Array.isArray(value)) {
1104
+ for (let index = 0; index < value.length; index++) {
1105
+ const nested = findUnsafeMetaPath(value[index], `${path}[${index}]`);
1106
+ if (nested) return nested;
1107
+ }
1108
+ return null;
1109
+ }
1110
+ if (!isRecord(value)) return null;
1111
+ for (const [key, nestedValue] of Object.entries(value)) {
1112
+ if (UNSAFE_META_KEYS.has(key)) {
1113
+ return `${path}.${key}`;
1114
+ }
1115
+ const nested = findUnsafeMetaPath(nestedValue, `${path}.${key}`);
1116
+ if (nested) return nested;
1117
+ }
1118
+ return null;
1119
+ }
1120
+ function validateFeatureEntry(raw, index) {
1121
+ if (!isRecord(raw)) {
1122
+ return {
1123
+ issues: [
1124
+ {
1125
+ path: `[${index}]`,
1126
+ message: "Feature entry must be an object",
1127
+ code: "invalid_type"
1128
+ }
1129
+ ]
1130
+ };
1131
+ }
1132
+ const parsed = featureEntrySchema.safeParse(raw);
1133
+ if (!parsed.success) {
1134
+ return {
1135
+ issues: parsed.error.issues.map((issue) => ({
1136
+ ...mapZodIssue(issue),
1137
+ path: `[${index}]${issue.path.length > 0 ? `.${toIssuePath(issue.path)}` : ""}`
1138
+ }))
1139
+ };
1140
+ }
1141
+ return {
1142
+ issues: [],
1143
+ entry: parsed.data
1144
+ };
1145
+ }
1146
+ function validateManifest(data) {
1147
+ const errors = [];
1148
+ if (!Array.isArray(data)) {
1149
+ return {
1150
+ valid: false,
1151
+ errors: [
1152
+ {
1153
+ path: "$",
1154
+ message: "Manifest must be an array",
1155
+ code: "invalid_type"
1156
+ }
1157
+ ]
1158
+ };
1159
+ }
1160
+ const entries = [];
1161
+ const seenIds = /* @__PURE__ */ new Set();
1162
+ data.forEach((item, index) => {
1163
+ const result = validateFeatureEntry(item, index);
1164
+ errors.push(...result.issues);
1165
+ if (!result.entry) return;
1166
+ if (seenIds.has(result.entry.id)) {
1167
+ errors.push({
1168
+ path: `[${index}].id`,
1169
+ message: `Duplicate feature id "${result.entry.id}"`,
1170
+ code: "duplicate_id"
1171
+ });
1172
+ return;
1173
+ }
1174
+ seenIds.add(result.entry.id);
1175
+ entries.push(result.entry);
1176
+ });
1177
+ if (entries.length > 0 && hasDependencyCycle(entries)) {
1178
+ errors.push({
1179
+ path: "$",
1180
+ message: "Circular dependsOn relationship detected",
1181
+ code: "circular_dependency"
1182
+ });
1183
+ }
1184
+ for (let index = 0; index < entries.length; index++) {
1185
+ const entry = entries[index];
1186
+ if (new Date(entry.showNewUntil).getTime() <= new Date(entry.releasedAt).getTime()) {
1187
+ errors.push({
1188
+ path: `[${index}].showNewUntil`,
1189
+ message: "showNewUntil must be after releasedAt",
1190
+ code: "invalid_value"
1191
+ });
1192
+ }
1193
+ if (entry.url && !isSafeUrl(entry.url)) {
1194
+ errors.push({
1195
+ path: `[${index}].url`,
1196
+ message: "url must be http, https, or relative",
1197
+ code: "invalid_value"
1198
+ });
1199
+ }
1200
+ if (entry.image && !isSafeUrl(entry.image)) {
1201
+ errors.push({
1202
+ path: `[${index}].image`,
1203
+ message: "image must be http, https, or relative",
1204
+ code: "invalid_value"
1205
+ });
1206
+ }
1207
+ if (entry.cta?.url && !isSafeUrl(entry.cta.url)) {
1208
+ errors.push({
1209
+ path: `[${index}].cta.url`,
1210
+ message: "cta.url must be http, https, or relative",
1211
+ code: "invalid_value"
1212
+ });
1213
+ }
1214
+ const unsafeMetaPath = findUnsafeMetaPath(entry.meta);
1215
+ if (unsafeMetaPath) {
1216
+ errors.push({
1217
+ path: `[${index}].${unsafeMetaPath}`,
1218
+ message: `meta contains unsafe key "${unsafeMetaPath.split(".").pop()}"`,
1219
+ code: "invalid_value"
1220
+ });
1221
+ }
1222
+ }
1223
+ return {
1224
+ valid: errors.length === 0,
1225
+ errors
1226
+ };
1227
+ }
1228
+
1229
+ // src/ci.ts
1230
+ function isRecord2(value) {
1231
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1232
+ }
1233
+ function collectChangedFields(beforeValue, afterValue, path, output) {
1234
+ if (beforeValue === afterValue) return;
1235
+ if (Array.isArray(beforeValue) && Array.isArray(afterValue)) {
1236
+ if (beforeValue.length !== afterValue.length) {
1237
+ output.push(path);
1238
+ return;
1239
+ }
1240
+ for (let i = 0; i < beforeValue.length; i += 1) {
1241
+ collectChangedFields(beforeValue[i], afterValue[i], `${path}[${i}]`, output);
1242
+ }
1243
+ return;
1244
+ }
1245
+ if (isRecord2(beforeValue) && isRecord2(afterValue)) {
1246
+ const keys = /* @__PURE__ */ new Set([...Object.keys(beforeValue), ...Object.keys(afterValue)]);
1247
+ keys.forEach((key) => {
1248
+ const nextPath = path ? `${path}.${key}` : key;
1249
+ collectChangedFields(beforeValue[key], afterValue[key], nextPath, output);
1250
+ });
1251
+ return;
1252
+ }
1253
+ output.push(path);
1254
+ }
1255
+ function diffManifest(before, after) {
1256
+ const beforeById = new Map(before.map((feature) => [feature.id, feature]));
1257
+ const afterById = new Map(after.map((feature) => [feature.id, feature]));
1258
+ const added = after.filter((feature) => !beforeById.has(feature.id));
1259
+ const removed = before.filter((feature) => !afterById.has(feature.id));
1260
+ const changed = after.filter((feature) => beforeById.has(feature.id)).map((feature) => {
1261
+ const previous = beforeById.get(feature.id);
1262
+ if (!previous) return null;
1263
+ const changedFields = [];
1264
+ collectChangedFields(previous, feature, "", changedFields);
1265
+ if (changedFields.length === 0) return null;
1266
+ return {
1267
+ id: feature.id,
1268
+ before: previous,
1269
+ after: feature,
1270
+ changedFields
1271
+ };
1272
+ }).filter((item) => item !== null);
1273
+ return { added, removed, changed };
1274
+ }
1275
+ function generateChangelogDiff(diff, options = {}) {
1276
+ const parts = [];
1277
+ if (diff.added.length > 0) {
1278
+ parts.push(`Added: ${diff.added.map((feature) => feature.label).join(", ")}`);
1279
+ }
1280
+ if (diff.changed.length > 0) {
1281
+ const changedText = diff.changed.map((item) => {
1282
+ if (!options.includeFieldChanges) return item.after.label;
1283
+ return `${item.after.label} [${item.changedFields.join(", ")}]`;
1284
+ });
1285
+ parts.push(`Changed: ${changedText.join(", ")}`);
1286
+ }
1287
+ if (diff.removed.length > 0) {
1288
+ parts.push(`Removed: ${diff.removed.map((feature) => feature.label).join(", ")}`);
1289
+ }
1290
+ return parts.length > 0 ? parts.join(". ") : "No manifest changes.";
1291
+ }
1292
+ function validateManifestForCI(manifest) {
1293
+ return validateManifest(manifest);
1294
+ }
1295
+
1296
+ // src/flags.ts
1297
+ function createFlagBridge(options) {
1298
+ return {
1299
+ isEnabled: (flagKey, userContext) => {
1300
+ if (!flagKey) return false;
1301
+ return options.isEnabled(flagKey, userContext);
1302
+ }
1303
+ };
1304
+ }
1305
+ var LaunchDarklyBridge = class {
1306
+ client;
1307
+ options;
1308
+ constructor(client, options = {}) {
1309
+ this.client = client;
1310
+ this.options = options;
1311
+ }
1312
+ isEnabled(flagKey, userContext) {
1313
+ const defaultUser = {
1314
+ key: userContext?.traits?.id ?? userContext?.role ?? "anonymous",
1315
+ custom: {
1316
+ plan: userContext?.plan,
1317
+ role: userContext?.role,
1318
+ region: userContext?.region,
1319
+ ...userContext?.traits ?? {}
1320
+ }
1321
+ };
1322
+ const user = this.options.userResolver ? this.options.userResolver(userContext) : defaultUser;
1323
+ return this.client.variation(flagKey, user, this.options.defaultValue ?? false);
1324
+ }
1325
+ };
1326
+ var PostHogBridge = class {
1327
+ client;
1328
+ options;
1329
+ constructor(client, options = {}) {
1330
+ this.client = client;
1331
+ this.options = options;
1332
+ }
1333
+ isEnabled(flagKey, userContext) {
1334
+ const distinctId = this.options.distinctIdResolver ? this.options.distinctIdResolver(userContext) : typeof userContext?.traits?.id === "string" ? userContext.traits.id : void 0;
1335
+ const groups = this.options.groupsResolver ? this.options.groupsResolver(userContext) : void 0;
1336
+ return this.client.isFeatureEnabled(flagKey, distinctId, groups, userContext?.traits);
1337
+ }
1338
+ };
1339
+ var panelStyles = {
1340
+ border: "1px solid #e5e7eb",
1341
+ borderRadius: "10px",
1342
+ padding: "12px",
1343
+ background: "#ffffff"
1344
+ };
1345
+ var headingStyles = {
1346
+ margin: "0 0 8px",
1347
+ fontSize: "15px",
1348
+ fontWeight: 700
1349
+ };
1350
+ function ManifestEditor({
1351
+ features,
1352
+ onSave,
1353
+ readOnly = false,
1354
+ children
1355
+ }) {
1356
+ const [draft, setDraft] = useState(() => JSON.stringify(features, null, 2));
1357
+ const [status, setStatus] = useState("idle");
1358
+ const [error, setError] = useState("");
1359
+ const parsed = useMemo(() => {
1360
+ try {
1361
+ const next = JSON.parse(draft);
1362
+ if (!Array.isArray(next)) throw new Error("Manifest must be an array");
1363
+ return next;
1364
+ } catch {
1365
+ return null;
1366
+ }
1367
+ }, [draft]);
1368
+ const save = async () => {
1369
+ if (readOnly || !parsed) return;
1370
+ setStatus("saving");
1371
+ setError("");
1372
+ try {
1373
+ await onSave(parsed);
1374
+ setStatus("saved");
1375
+ } catch (cause) {
1376
+ setStatus("error");
1377
+ setError(cause instanceof Error ? cause.message : "Failed to save manifest");
1378
+ }
1379
+ };
1380
+ if (children) return /* @__PURE__ */ jsx(Fragment, { children });
1381
+ return /* @__PURE__ */ jsxs("section", { "data-featuredrop-admin-manifest-editor": true, style: panelStyles, children: [
1382
+ /* @__PURE__ */ jsx("p", { style: headingStyles, children: "Manifest Editor" }),
1383
+ /* @__PURE__ */ jsx(
1384
+ "textarea",
1385
+ {
1386
+ "aria-label": "Manifest JSON",
1387
+ value: draft,
1388
+ onChange: (event) => setDraft(event.target.value),
1389
+ readOnly,
1390
+ style: {
1391
+ width: "100%",
1392
+ minHeight: "180px",
1393
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
1394
+ fontSize: "12px",
1395
+ lineHeight: 1.45,
1396
+ border: "1px solid #d1d5db",
1397
+ borderRadius: "8px",
1398
+ padding: "10px"
1399
+ }
1400
+ }
1401
+ ),
1402
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px", marginTop: "8px" }, children: [
1403
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: save, disabled: readOnly || !parsed, children: "Save" }),
1404
+ /* @__PURE__ */ jsx("span", { "aria-live": "polite", children: status }),
1405
+ !parsed && /* @__PURE__ */ jsx("span", { style: { color: "#dc2626" }, children: "Invalid JSON" }),
1406
+ error && /* @__PURE__ */ jsx("span", { style: { color: "#dc2626" }, children: error })
1407
+ ] })
1408
+ ] });
1409
+ }
1410
+ function ScheduleCalendar({ features, onSchedule }) {
1411
+ const [values, setValues] = useState({});
1412
+ return /* @__PURE__ */ jsxs("section", { "data-featuredrop-admin-schedule-calendar": true, style: panelStyles, children: [
1413
+ /* @__PURE__ */ jsx("p", { style: headingStyles, children: "Schedule Calendar" }),
1414
+ /* @__PURE__ */ jsx("ul", { style: { margin: 0, padding: 0, listStyle: "none", display: "grid", gap: "10px" }, children: features.map((feature) => /* @__PURE__ */ jsxs(
1415
+ "li",
1416
+ {
1417
+ style: {
1418
+ border: "1px solid #e5e7eb",
1419
+ borderRadius: "8px",
1420
+ padding: "10px",
1421
+ display: "grid",
1422
+ gap: "6px"
1423
+ },
1424
+ children: [
1425
+ /* @__PURE__ */ jsx("strong", { children: feature.label }),
1426
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: "4px" }, children: [
1427
+ "Publish at",
1428
+ /* @__PURE__ */ jsx(
1429
+ "input",
1430
+ {
1431
+ type: "datetime-local",
1432
+ value: values[feature.id] ?? "",
1433
+ onChange: (event) => {
1434
+ const value = event.target.value;
1435
+ setValues((previous) => ({ ...previous, [feature.id]: value }));
1436
+ }
1437
+ }
1438
+ )
1439
+ ] }),
1440
+ /* @__PURE__ */ jsx(
1441
+ "button",
1442
+ {
1443
+ type: "button",
1444
+ onClick: () => {
1445
+ const value = values[feature.id];
1446
+ if (!value) return;
1447
+ void onSchedule(feature.id, new Date(value).toISOString());
1448
+ },
1449
+ children: "Schedule"
1450
+ }
1451
+ )
1452
+ ]
1453
+ },
1454
+ feature.id
1455
+ )) })
1456
+ ] });
1457
+ }
1458
+ function PreviewPanel({ feature, components = ["badge", "changelog"] }) {
1459
+ return /* @__PURE__ */ jsxs("section", { "data-featuredrop-admin-preview-panel": true, style: panelStyles, children: [
1460
+ /* @__PURE__ */ jsx("p", { style: headingStyles, children: "Preview Panel" }),
1461
+ !feature ? /* @__PURE__ */ jsx("p", { style: { margin: 0, color: "#6b7280" }, children: "Select a feature to preview." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1462
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 6px", fontWeight: 600 }, children: feature.label }),
1463
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 8px", color: "#6b7280" }, children: feature.description ?? "No description" }),
1464
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: components.map((component) => /* @__PURE__ */ jsx(
1465
+ "span",
1466
+ {
1467
+ style: {
1468
+ border: "1px solid #d1d5db",
1469
+ borderRadius: "999px",
1470
+ padding: "2px 8px",
1471
+ fontSize: "12px"
1472
+ },
1473
+ children: component
1474
+ },
1475
+ component
1476
+ )) })
1477
+ ] })
1478
+ ] });
1479
+ }
1480
+ function toggle(list, value) {
1481
+ const items = new Set(list ?? []);
1482
+ if (items.has(value)) items.delete(value);
1483
+ else items.add(value);
1484
+ return Array.from(items);
1485
+ }
1486
+ function AudienceBuilder({
1487
+ segments = [],
1488
+ roles = [],
1489
+ regions = [],
1490
+ value,
1491
+ onChange,
1492
+ onSave
1493
+ }) {
1494
+ const [audience, setAudience] = useState({
1495
+ plan: value?.plan ?? [],
1496
+ role: value?.role ?? [],
1497
+ region: value?.region ?? []
1498
+ });
1499
+ const updateAudience = (next) => {
1500
+ setAudience(next);
1501
+ onChange?.(next);
1502
+ };
1503
+ const section = (title, values, selected, onToggle) => /* @__PURE__ */ jsxs("fieldset", { style: { border: "none", margin: 0, padding: 0 }, children: [
1504
+ /* @__PURE__ */ jsx("legend", { style: { fontWeight: 600, marginBottom: "4px" }, children: title }),
1505
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: values.map((item) => /* @__PURE__ */ jsxs("label", { style: { display: "inline-flex", alignItems: "center", gap: "6px" }, children: [
1506
+ /* @__PURE__ */ jsx(
1507
+ "input",
1508
+ {
1509
+ type: "checkbox",
1510
+ checked: Boolean(selected?.includes(item)),
1511
+ onChange: () => onToggle(item)
1512
+ }
1513
+ ),
1514
+ item
1515
+ ] }, item)) })
1516
+ ] });
1517
+ return /* @__PURE__ */ jsxs("section", { "data-featuredrop-admin-audience-builder": true, style: panelStyles, children: [
1518
+ /* @__PURE__ */ jsx("p", { style: headingStyles, children: "Audience Builder" }),
1519
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: "10px" }, children: [
1520
+ section("Plans", segments, audience.plan, (item) => updateAudience({ ...audience, plan: toggle(audience.plan, item) })),
1521
+ section("Roles", roles, audience.role, (item) => updateAudience({ ...audience, role: toggle(audience.role, item) })),
1522
+ section("Regions", regions, audience.region, (item) => updateAudience({ ...audience, region: toggle(audience.region, item) }))
1523
+ ] }),
1524
+ onSave && /* @__PURE__ */ jsx(
1525
+ "button",
1526
+ {
1527
+ type: "button",
1528
+ style: { marginTop: "10px" },
1529
+ onClick: () => {
1530
+ void onSave(audience);
1531
+ },
1532
+ children: "Save audience"
1533
+ }
1534
+ )
1535
+ ] });
1536
+ }
1537
+ function parseScalar(raw) {
1538
+ const value = raw.trim();
1539
+ if (!value) return "";
1540
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1541
+ return value.slice(1, -1);
1542
+ }
1543
+ if (value === "true") return true;
1544
+ if (value === "false") return false;
1545
+ if (value === "null") return null;
1546
+ if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
1547
+ if (value.startsWith("[") && value.endsWith("]")) {
1548
+ const inner = value.slice(1, -1).trim();
1549
+ if (!inner) return [];
1550
+ return inner.split(",").map((part) => String(parseScalar(part.trim())));
1551
+ }
1552
+ return value;
1553
+ }
1554
+ function parseFrontmatter(raw) {
1555
+ const lines = raw.split(/\r?\n/);
1556
+ const root = {};
1557
+ const stack = [
1558
+ { indent: -1, value: root }
1559
+ ];
1560
+ const isArrayContext = (idx) => {
1561
+ for (let i = idx + 1; i < lines.length; i++) {
1562
+ const line = lines[i];
1563
+ if (!line.trim()) continue;
1564
+ const indent = line.length - line.trimStart().length;
1565
+ if (indent <= lines[idx].length - lines[idx].trimStart().length) return false;
1566
+ return line.trimStart().startsWith("- ");
1567
+ }
1568
+ return false;
1569
+ };
1570
+ for (let i = 0; i < lines.length; i++) {
1571
+ const line = lines[i];
1572
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
1573
+ const indent = line.length - line.trimStart().length;
1574
+ const trimmed = line.trim();
1575
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
1576
+ stack.pop();
1577
+ }
1578
+ const current = stack[stack.length - 1].value;
1579
+ if (trimmed.startsWith("- ")) {
1580
+ if (!Array.isArray(current)) {
1581
+ throw new Error(`Invalid frontmatter list at line ${i + 1}`);
1582
+ }
1583
+ const item = trimmed.slice(2).trim();
1584
+ current.push(parseScalar(item));
1585
+ continue;
1586
+ }
1587
+ const colon = trimmed.indexOf(":");
1588
+ if (colon === -1) {
1589
+ throw new Error(`Invalid frontmatter line ${i + 1}: ${trimmed}`);
1590
+ }
1591
+ const key = trimmed.slice(0, colon).trim();
1592
+ const rest = trimmed.slice(colon + 1).trim();
1593
+ if (Array.isArray(current)) {
1594
+ throw new Error(`Unexpected key in list at line ${i + 1}`);
1595
+ }
1596
+ if (!rest) {
1597
+ const container = isArrayContext(i) ? [] : {};
1598
+ current[key] = container;
1599
+ stack.push({ indent, value: container });
1600
+ continue;
1601
+ }
1602
+ current[key] = parseScalar(rest);
1603
+ }
1604
+ return root;
1605
+ }
1606
+ function splitFrontmatter(markdown) {
1607
+ const normalized = markdown.replace(/\r\n/g, "\n");
1608
+ if (!normalized.startsWith("---\n")) {
1609
+ return { frontmatter: {}, body: normalized.trim() };
1610
+ }
1611
+ const end = normalized.indexOf("\n---\n", 4);
1612
+ if (end === -1) {
1613
+ throw new Error("Frontmatter block is not closed with ---");
1614
+ }
1615
+ const fmRaw = normalized.slice(4, end);
1616
+ const body = normalized.slice(end + 5).trim();
1617
+ return {
1618
+ frontmatter: parseFrontmatter(fmRaw),
1619
+ body
1620
+ };
1621
+ }
1622
+ function asString(value, field, source) {
1623
+ if (typeof value !== "string" || !value.trim()) {
1624
+ throw new Error(`${source}: "${field}" must be a non-empty string`);
1625
+ }
1626
+ return value;
1627
+ }
1628
+ function asOptionalObject(value, field, source) {
1629
+ if (value === void 0) return void 0;
1630
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1631
+ throw new Error(`${source}: "${field}" must be an object`);
1632
+ }
1633
+ return value;
1634
+ }
1635
+ function parseFeatureFile(markdown, source = "feature.md") {
1636
+ const { frontmatter, body } = splitFrontmatter(markdown);
1637
+ const entry = {
1638
+ id: asString(frontmatter.id, "id", source),
1639
+ label: asString(frontmatter.label, "label", source),
1640
+ releasedAt: asString(frontmatter.releasedAt, "releasedAt", source),
1641
+ showNewUntil: asString(frontmatter.showNewUntil, "showNewUntil", source),
1642
+ description: body || void 0
1643
+ };
1644
+ if (frontmatter.sidebarKey !== void 0) entry.sidebarKey = asString(frontmatter.sidebarKey, "sidebarKey", source);
1645
+ if (frontmatter.category !== void 0) entry.category = asString(frontmatter.category, "category", source);
1646
+ if (frontmatter.product !== void 0) entry.product = asString(frontmatter.product, "product", source);
1647
+ if (frontmatter.url !== void 0) entry.url = asString(frontmatter.url, "url", source);
1648
+ if (frontmatter.flagKey !== void 0) entry.flagKey = asString(frontmatter.flagKey, "flagKey", source);
1649
+ if (frontmatter.image !== void 0) entry.image = asString(frontmatter.image, "image", source);
1650
+ if (frontmatter.publishAt !== void 0) entry.publishAt = asString(frontmatter.publishAt, "publishAt", source);
1651
+ if (frontmatter.version !== void 0) {
1652
+ if (typeof frontmatter.version === "string" || typeof frontmatter.version === "object") {
1653
+ entry.version = frontmatter.version;
1654
+ } else {
1655
+ throw new Error(`${source}: "version" must be a string or object`);
1656
+ }
1657
+ }
1658
+ if (frontmatter.type !== void 0) {
1659
+ const type = asString(frontmatter.type, "type", source);
1660
+ if (!["feature", "improvement", "fix", "breaking"].includes(type)) {
1661
+ throw new Error(`${source}: invalid "type" value "${type}"`);
1662
+ }
1663
+ entry.type = type;
1664
+ }
1665
+ if (frontmatter.priority !== void 0) {
1666
+ const priority = asString(frontmatter.priority, "priority", source);
1667
+ if (!["critical", "normal", "low"].includes(priority)) {
1668
+ throw new Error(`${source}: invalid "priority" value "${priority}"`);
1669
+ }
1670
+ entry.priority = priority;
1671
+ }
1672
+ const cta = asOptionalObject(frontmatter.cta, "cta", source);
1673
+ if (cta) {
1674
+ entry.cta = {
1675
+ label: asString(cta.label, "cta.label", source),
1676
+ url: asString(cta.url, "cta.url", source)
1677
+ };
1678
+ }
1679
+ const audience = asOptionalObject(frontmatter.audience, "audience", source);
1680
+ if (audience) {
1681
+ const parsedAudience = {};
1682
+ for (const field of ["plan", "role", "region"]) {
1683
+ const value = audience[field];
1684
+ if (value !== void 0) {
1685
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1686
+ throw new Error(`${source}: "audience.${field}" must be string[]`);
1687
+ }
1688
+ parsedAudience[field] = value;
1689
+ }
1690
+ }
1691
+ if (audience.custom !== void 0) {
1692
+ if (!audience.custom || typeof audience.custom !== "object" || Array.isArray(audience.custom)) {
1693
+ throw new Error(`${source}: "audience.custom" must be an object`);
1694
+ }
1695
+ parsedAudience.custom = audience.custom;
1696
+ }
1697
+ entry.audience = parsedAudience;
1698
+ }
1699
+ return entry;
1700
+ }
1701
+ function normalizePattern(pattern) {
1702
+ const normalized = pattern.replaceAll("\\", "/");
1703
+ if (normalized.endsWith("/**/*.md")) {
1704
+ return {
1705
+ baseDir: normalized.slice(0, -"/**/*.md".length),
1706
+ ext: ".md"
1707
+ };
1708
+ }
1709
+ throw new Error(`Unsupported pattern "${pattern}". Use "features/**/*.md" style patterns.`);
1710
+ }
1711
+ async function collectFiles(dir, ext) {
1712
+ const out = [];
1713
+ async function walk(current) {
1714
+ let entries;
1715
+ try {
1716
+ entries = await readdir(current, { withFileTypes: true });
1717
+ } catch {
1718
+ return;
1719
+ }
1720
+ for (const entry of entries) {
1721
+ const fullPath = join(current, entry.name);
1722
+ if (entry.isDirectory()) {
1723
+ await walk(fullPath);
1724
+ continue;
1725
+ }
1726
+ if (entry.isFile() && entry.name.endsWith(ext)) {
1727
+ out.push(fullPath);
1728
+ }
1729
+ }
1730
+ }
1731
+ await walk(dir);
1732
+ return out.sort();
1733
+ }
1734
+ async function buildManifestFromPattern(options = {}) {
1735
+ const cwd = options.cwd ?? process.cwd();
1736
+ const pattern = options.pattern ?? "features/**/*.md";
1737
+ const { baseDir, ext } = normalizePattern(pattern);
1738
+ const baseAbs = join(cwd, baseDir);
1739
+ const stats = await stat(baseAbs).catch(() => null);
1740
+ if (!stats || !stats.isDirectory()) {
1741
+ throw new Error(`Pattern base directory does not exist: ${baseDir}`);
1742
+ }
1743
+ const files = await collectFiles(baseAbs, ext);
1744
+ const entries = [];
1745
+ const seenIds = /* @__PURE__ */ new Set();
1746
+ for (const file of files) {
1747
+ const content = await readFile(file, "utf8");
1748
+ const source = relative(cwd, file).split(sep).join("/");
1749
+ const entry = parseFeatureFile(content, source);
1750
+ if (seenIds.has(entry.id)) {
1751
+ throw new Error(`Duplicate feature id "${entry.id}" found at ${source}`);
1752
+ }
1753
+ seenIds.add(entry.id);
1754
+ entries.push(entry);
1755
+ }
1756
+ if (options.outFile) {
1757
+ const outPath = join(cwd, options.outFile);
1758
+ await writeFile(outPath, `${JSON.stringify(entries, null, 2)}
1759
+ `, "utf8");
1760
+ }
1761
+ return entries;
1762
+ }
1763
+
1764
+ // src/cms.ts
1765
+ var DEFAULT_FIELDS = {
1766
+ id: "id",
1767
+ label: "label",
1768
+ releasedAt: "releasedAt",
1769
+ showNewUntil: "showNewUntil",
1770
+ description: "description",
1771
+ sidebarKey: "sidebarKey",
1772
+ category: "category",
1773
+ product: "product",
1774
+ flagKey: "flagKey",
1775
+ url: "url",
1776
+ image: "image",
1777
+ publishAt: "publishAt",
1778
+ type: "type",
1779
+ priority: "priority",
1780
+ ctaLabel: "cta.label",
1781
+ ctaUrl: "cta.url"
1782
+ };
1783
+ function getByPath(record, path) {
1784
+ const parts = path.split(".").filter(Boolean);
1785
+ let cursor = record;
1786
+ for (const part of parts) {
1787
+ if (!cursor || typeof cursor !== "object") return void 0;
1788
+ cursor = cursor[part];
1789
+ }
1790
+ return cursor;
1791
+ }
1792
+ function normalizeLocalizedValue(value) {
1793
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1794
+ const objectValue = value;
1795
+ const keys = Object.keys(objectValue);
1796
+ if (keys.length === 1) {
1797
+ const nested = objectValue[keys[0]];
1798
+ if (typeof nested === "string" || typeof nested === "number" || typeof nested === "boolean" || nested == null) {
1799
+ return nested;
1800
+ }
1801
+ }
1802
+ }
1803
+ return value;
1804
+ }
1805
+ function resolveField(record, resolver) {
1806
+ if (!resolver) return void 0;
1807
+ if (typeof resolver === "function") return resolver(record);
1808
+ return normalizeLocalizedValue(getByPath(record, resolver));
1809
+ }
1810
+ function asString2(value) {
1811
+ if (typeof value === "string") {
1812
+ const trimmed = value.trim();
1813
+ return trimmed ? trimmed : void 0;
1814
+ }
1815
+ if (typeof value === "number" || typeof value === "boolean") {
1816
+ return String(value);
1817
+ }
1818
+ return void 0;
1819
+ }
1820
+ function normalizeFieldMapping(defaults, overrides) {
1821
+ return {
1822
+ ...defaults,
1823
+ ...overrides
1824
+ };
1825
+ }
1826
+ function validateMappedEntries(entries, options) {
1827
+ const strictValidation = options?.strictValidation ?? false;
1828
+ const validEntries = [];
1829
+ const seenIds = /* @__PURE__ */ new Set();
1830
+ const errors = [];
1831
+ for (const entry of entries) {
1832
+ if (seenIds.has(entry.id)) {
1833
+ errors.push(`${entry.id}: duplicate id`);
1834
+ continue;
1835
+ }
1836
+ seenIds.add(entry.id);
1837
+ const validation = validateManifest([entry]);
1838
+ if (validation.valid) {
1839
+ validEntries.push(entry);
1840
+ continue;
1841
+ }
1842
+ const reason = validation.errors.map((error) => `${error.path} ${error.message}`).join("; ");
1843
+ errors.push(`${entry.id}: ${reason}`);
1844
+ }
1845
+ if (errors.length > 0) {
1846
+ if (strictValidation) {
1847
+ throw new Error(`[featuredrop] CMS mapping validation failed: ${errors.join(" | ")}`);
1848
+ }
1849
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
1850
+ console.warn(`[featuredrop] Skipped ${errors.length} invalid CMS entries.`);
1851
+ }
1852
+ }
1853
+ return validEntries;
1854
+ }
1855
+ function mapRecordToFeatureEntry(record, mapping) {
1856
+ const id = asString2(resolveField(record, mapping.id));
1857
+ const label = asString2(resolveField(record, mapping.label));
1858
+ const releasedAt = asString2(resolveField(record, mapping.releasedAt));
1859
+ const showNewUntil = asString2(resolveField(record, mapping.showNewUntil));
1860
+ if (!id || !label || !releasedAt || !showNewUntil) return null;
1861
+ const entry = {
1862
+ id,
1863
+ label,
1864
+ releasedAt,
1865
+ showNewUntil
1866
+ };
1867
+ const description = asString2(resolveField(record, mapping.description));
1868
+ if (description) entry.description = description;
1869
+ const sidebarKey = asString2(resolveField(record, mapping.sidebarKey));
1870
+ if (sidebarKey) entry.sidebarKey = sidebarKey;
1871
+ const category = asString2(resolveField(record, mapping.category));
1872
+ if (category) entry.category = category;
1873
+ const product = asString2(resolveField(record, mapping.product));
1874
+ if (product) entry.product = product;
1875
+ const flagKey = asString2(resolveField(record, mapping.flagKey));
1876
+ if (flagKey) entry.flagKey = flagKey;
1877
+ const url = asString2(resolveField(record, mapping.url));
1878
+ if (url) entry.url = url;
1879
+ const image = asString2(resolveField(record, mapping.image));
1880
+ if (image) entry.image = image;
1881
+ const publishAt = asString2(resolveField(record, mapping.publishAt));
1882
+ if (publishAt) entry.publishAt = publishAt;
1883
+ const type = asString2(resolveField(record, mapping.type));
1884
+ if (type && ["feature", "improvement", "fix", "breaking"].includes(type)) {
1885
+ entry.type = type;
1886
+ }
1887
+ const priority = asString2(resolveField(record, mapping.priority));
1888
+ if (priority && ["critical", "normal", "low"].includes(priority)) {
1889
+ entry.priority = priority;
1890
+ }
1891
+ const ctaLabel = asString2(resolveField(record, mapping.ctaLabel));
1892
+ const ctaUrl = asString2(resolveField(record, mapping.ctaUrl));
1893
+ if (ctaLabel && ctaUrl) {
1894
+ entry.cta = { label: ctaLabel, url: ctaUrl };
1895
+ }
1896
+ return entry;
1897
+ }
1898
+ async function fetchJson(input, init) {
1899
+ const response = await fetch(input, init);
1900
+ if (!response.ok) {
1901
+ throw new Error(`[featuredrop] CMS request failed (${response.status}) for ${input}`);
1902
+ }
1903
+ return response.json();
1904
+ }
1905
+ var ContentfulAdapter = class {
1906
+ options;
1907
+ constructor(options) {
1908
+ this.options = options;
1909
+ }
1910
+ async load() {
1911
+ const environment = this.options.environment ?? "master";
1912
+ const params = new URLSearchParams({
1913
+ content_type: this.options.contentType,
1914
+ limit: String(this.options.limit ?? 1e3)
1915
+ });
1916
+ if (this.options.locale) {
1917
+ params.set("locale", this.options.locale);
1918
+ }
1919
+ const url = `https://cdn.contentful.com/spaces/${encodeURIComponent(this.options.spaceId)}/environments/${encodeURIComponent(environment)}/entries?${params.toString()}`;
1920
+ const payload = await fetchJson(url, {
1921
+ headers: {
1922
+ Authorization: `Bearer ${this.options.accessToken}`
1923
+ }
1924
+ });
1925
+ const mapping = normalizeFieldMapping(
1926
+ {
1927
+ ...DEFAULT_FIELDS,
1928
+ id: "sys.id",
1929
+ label: "fields.label",
1930
+ description: "fields.description",
1931
+ releasedAt: "fields.releasedAt",
1932
+ showNewUntil: "fields.showNewUntil",
1933
+ sidebarKey: "fields.sidebarKey",
1934
+ category: "fields.category",
1935
+ product: "fields.product",
1936
+ flagKey: "fields.flagKey",
1937
+ url: "fields.url",
1938
+ image: "fields.image",
1939
+ publishAt: "fields.publishAt",
1940
+ type: "fields.type",
1941
+ priority: "fields.priority",
1942
+ ctaLabel: "fields.ctaLabel",
1943
+ ctaUrl: "fields.ctaUrl"
1944
+ },
1945
+ this.options.fieldMapping
1946
+ );
1947
+ const entries = (payload.items ?? []).map((item) => mapRecordToFeatureEntry(item, mapping)).filter((entry) => entry !== null);
1948
+ return validateMappedEntries(entries, this.options);
1949
+ }
1950
+ };
1951
+ var SanityAdapter = class {
1952
+ options;
1953
+ constructor(options) {
1954
+ this.options = options;
1955
+ }
1956
+ async load() {
1957
+ const version = this.options.apiVersion ?? "v2023-10-01";
1958
+ const queryParam = encodeURIComponent(this.options.query);
1959
+ const url = `https://${encodeURIComponent(this.options.projectId)}.api.sanity.io/${version}/data/query/${encodeURIComponent(this.options.dataset)}?query=${queryParam}`;
1960
+ const headers = {};
1961
+ if (this.options.token) {
1962
+ headers.Authorization = `Bearer ${this.options.token}`;
1963
+ }
1964
+ const payload = await fetchJson(url, {
1965
+ headers
1966
+ });
1967
+ const mapping = normalizeFieldMapping(
1968
+ {
1969
+ ...DEFAULT_FIELDS,
1970
+ id: "_id"
1971
+ },
1972
+ this.options.fieldMapping
1973
+ );
1974
+ const entries = (payload.result ?? []).map((item) => mapRecordToFeatureEntry(item, mapping)).filter((entry) => entry !== null);
1975
+ return validateMappedEntries(entries, this.options);
1976
+ }
1977
+ };
1978
+ var StrapiAdapter = class {
1979
+ options;
1980
+ constructor(options) {
1981
+ this.options = options;
1982
+ }
1983
+ async load() {
1984
+ const endpoint = this.options.endpoint ?? "/api/features";
1985
+ const base = this.options.baseUrl.replace(/\/+$/, "");
1986
+ const query = this.options.query ? `?${this.options.query}` : "";
1987
+ const url = `${base}${endpoint}${query}`;
1988
+ const headers = {};
1989
+ if (this.options.token) {
1990
+ headers.Authorization = `Bearer ${this.options.token}`;
1991
+ }
1992
+ const payload = await fetchJson(url, { headers });
1993
+ const mapping = normalizeFieldMapping(DEFAULT_FIELDS, this.options.fieldMapping);
1994
+ const entries = (payload.data ?? []).map((item) => {
1995
+ if (!item || typeof item !== "object") return null;
1996
+ const record = item;
1997
+ const attributes = record.attributes;
1998
+ if (attributes && typeof attributes === "object") {
1999
+ return mapRecordToFeatureEntry(
2000
+ { id: record.id, ...attributes },
2001
+ mapping
2002
+ );
2003
+ }
2004
+ return mapRecordToFeatureEntry(record, mapping);
2005
+ }).filter((entry) => entry !== null);
2006
+ return validateMappedEntries(entries, this.options);
2007
+ }
2008
+ };
2009
+ function notionPropertyToValue(property) {
2010
+ if (!property || typeof property !== "object") return void 0;
2011
+ const typed = property;
2012
+ const type = typed.type;
2013
+ if (typeof type !== "string") return void 0;
2014
+ const value = typed[type];
2015
+ if (type === "title" || type === "rich_text") {
2016
+ if (!Array.isArray(value)) return void 0;
2017
+ return value.map((item) => {
2018
+ if (!item || typeof item !== "object") return "";
2019
+ return asString2(item.plain_text) ?? "";
2020
+ }).join("").trim();
2021
+ }
2022
+ if (type === "select") {
2023
+ if (!value || typeof value !== "object") return void 0;
2024
+ return asString2(value.name);
2025
+ }
2026
+ if (type === "multi_select") {
2027
+ if (!Array.isArray(value)) return void 0;
2028
+ return value.map((item) => item && typeof item === "object" ? asString2(item.name) : void 0).filter((item) => Boolean(item));
2029
+ }
2030
+ if (type === "date") {
2031
+ if (!value || typeof value !== "object") return void 0;
2032
+ return asString2(value.start);
2033
+ }
2034
+ if (type === "number" || type === "url" || type === "email" || type === "phone_number") {
2035
+ return value;
2036
+ }
2037
+ if (type === "checkbox") {
2038
+ return value === true ? "true" : value === false ? "false" : void 0;
2039
+ }
2040
+ return void 0;
2041
+ }
2042
+ function flattenNotionPage(page) {
2043
+ if (!page || typeof page !== "object") return {};
2044
+ const record = page;
2045
+ const properties = record.properties;
2046
+ const flattened = {
2047
+ id: record.id
2048
+ };
2049
+ if (properties && typeof properties === "object") {
2050
+ Object.entries(properties).forEach(([key, value]) => {
2051
+ flattened[key] = notionPropertyToValue(value);
2052
+ });
2053
+ }
2054
+ return flattened;
2055
+ }
2056
+ var NotionAdapter = class {
2057
+ options;
2058
+ constructor(options) {
2059
+ this.options = options;
2060
+ }
2061
+ async load() {
2062
+ const body = {};
2063
+ if (this.options.filter) body.filter = this.options.filter;
2064
+ if (this.options.sorts) body.sorts = this.options.sorts;
2065
+ const payload = await fetchJson(
2066
+ `https://api.notion.com/v1/databases/${encodeURIComponent(this.options.databaseId)}/query`,
2067
+ {
2068
+ method: "POST",
2069
+ headers: {
2070
+ "Content-Type": "application/json",
2071
+ Authorization: `Bearer ${this.options.token}`,
2072
+ "Notion-Version": this.options.notionVersion ?? "2022-06-28"
2073
+ },
2074
+ body: JSON.stringify(body)
2075
+ }
2076
+ );
2077
+ const mapping = normalizeFieldMapping(DEFAULT_FIELDS, this.options.fieldMapping);
2078
+ const entries = (payload.results ?? []).map((page) => mapRecordToFeatureEntry(flattenNotionPage(page), mapping)).filter((entry) => entry !== null);
2079
+ return validateMappedEntries(entries, this.options);
2080
+ }
2081
+ };
2082
+ var MarkdownAdapter = class {
2083
+ options;
2084
+ constructor(options = {}) {
2085
+ this.options = options;
2086
+ }
2087
+ async load() {
2088
+ if (this.options.pattern) {
2089
+ const entries2 = await buildManifestFromPattern({
2090
+ cwd: this.options.cwd,
2091
+ pattern: this.options.pattern
2092
+ });
2093
+ return validateMappedEntries(entries2, this.options);
2094
+ }
2095
+ const entries = this.options.entries ?? [];
2096
+ const mapped = entries.map((entry, index) => {
2097
+ const source = entry.source ?? `feature-${index + 1}.md`;
2098
+ return parseFeatureFile(entry.markdown, source);
2099
+ });
2100
+ return validateMappedEntries(mapped, this.options);
2101
+ }
2102
+ };
2103
+
2104
+ // src/theme.ts
2105
+ var LIGHT_THEME = {
2106
+ colors: {
2107
+ primary: "#2563eb",
2108
+ background: "#ffffff",
2109
+ text: "#111827",
2110
+ textMuted: "#6b7280",
2111
+ border: "#e5e7eb",
2112
+ success: "#16a34a",
2113
+ warning: "#f59e0b",
2114
+ error: "#dc2626"
2115
+ },
2116
+ fonts: {
2117
+ family: "system-ui, -apple-system, Segoe UI, sans-serif",
2118
+ sizeBase: "14px",
2119
+ sizeSm: "12px",
2120
+ sizeLg: "16px"
2121
+ },
2122
+ spacing: {
2123
+ xs: "4px",
2124
+ sm: "8px",
2125
+ md: "12px",
2126
+ lg: "16px",
2127
+ xl: "24px"
2128
+ },
2129
+ radii: {
2130
+ sm: "6px",
2131
+ md: "8px",
2132
+ lg: "12px",
2133
+ full: "999px"
2134
+ },
2135
+ shadows: {
2136
+ sm: "0 2px 8px rgba(0, 0, 0, 0.08)",
2137
+ md: "0 8px 24px rgba(0, 0, 0, 0.12)",
2138
+ lg: "0 20px 60px rgba(0, 0, 0, 0.16)"
2139
+ },
2140
+ zIndex: {
2141
+ base: 9998,
2142
+ tooltip: 1e4,
2143
+ modal: 10001,
2144
+ overlay: 9997
2145
+ }
2146
+ };
2147
+ var DARK_THEME = {
2148
+ ...LIGHT_THEME,
2149
+ colors: {
2150
+ primary: "#60a5fa",
2151
+ background: "#0b1220",
2152
+ text: "#f3f4f6",
2153
+ textMuted: "#9ca3af",
2154
+ border: "#1f2937",
2155
+ success: "#4ade80",
2156
+ warning: "#fbbf24",
2157
+ error: "#f87171"
2158
+ },
2159
+ shadows: {
2160
+ sm: "0 2px 8px rgba(0, 0, 0, 0.35)",
2161
+ md: "0 8px 24px rgba(0, 0, 0, 0.42)",
2162
+ lg: "0 20px 60px rgba(0, 0, 0, 0.52)"
2163
+ }
2164
+ };
2165
+ var MINIMAL_THEME = {
2166
+ ...LIGHT_THEME,
2167
+ colors: {
2168
+ ...LIGHT_THEME.colors,
2169
+ primary: "#111827",
2170
+ background: "#ffffff",
2171
+ text: "#111827",
2172
+ textMuted: "#6b7280",
2173
+ border: "#d1d5db",
2174
+ success: "#111827",
2175
+ warning: "#111827",
2176
+ error: "#111827"
2177
+ },
2178
+ shadows: {
2179
+ sm: "none",
2180
+ md: "none",
2181
+ lg: "none"
2182
+ },
2183
+ radii: {
2184
+ sm: "0",
2185
+ md: "0",
2186
+ lg: "0",
2187
+ full: "0"
2188
+ }
2189
+ };
2190
+ var VIBRANT_THEME = {
2191
+ ...LIGHT_THEME,
2192
+ colors: {
2193
+ primary: "#ec4899",
2194
+ background: "#fff7ed",
2195
+ text: "#3f1d57",
2196
+ textMuted: "#6d4c84",
2197
+ border: "#fdba74",
2198
+ success: "#10b981",
2199
+ warning: "#f59e0b",
2200
+ error: "#ef4444"
2201
+ },
2202
+ shadows: {
2203
+ sm: "0 2px 10px rgba(236, 72, 153, 0.15)",
2204
+ md: "0 10px 26px rgba(236, 72, 153, 0.22)",
2205
+ lg: "0 22px 58px rgba(236, 72, 153, 0.28)"
2206
+ }
2207
+ };
2208
+ var FEATUREDROP_THEMES = {
2209
+ light: LIGHT_THEME,
2210
+ dark: DARK_THEME,
2211
+ minimal: MINIMAL_THEME,
2212
+ vibrant: VIBRANT_THEME
2213
+ };
2214
+ function isThemePreset(value) {
2215
+ return value === "light" || value === "dark" || value === "auto" || value === "minimal" || value === "vibrant";
2216
+ }
2217
+ function mergeTheme(base, overrides) {
2218
+ if (!overrides) return base;
2219
+ return {
2220
+ colors: {
2221
+ ...base.colors,
2222
+ ...overrides.colors ?? {}
2223
+ },
2224
+ fonts: {
2225
+ ...base.fonts,
2226
+ ...overrides.fonts ?? {}
2227
+ },
2228
+ spacing: {
2229
+ ...base.spacing,
2230
+ ...overrides.spacing ?? {}
2231
+ },
2232
+ radii: {
2233
+ ...base.radii,
2234
+ ...overrides.radii ?? {}
2235
+ },
2236
+ shadows: {
2237
+ ...base.shadows,
2238
+ ...overrides.shadows ?? {}
2239
+ },
2240
+ zIndex: {
2241
+ ...base.zIndex,
2242
+ ...overrides.zIndex ?? {}
2243
+ }
2244
+ };
2245
+ }
2246
+ function createTheme(overrides, base = LIGHT_THEME) {
2247
+ return mergeTheme(base, overrides);
2248
+ }
2249
+ function resolveTheme(input = "light", options = {}) {
2250
+ if (isThemePreset(input)) {
2251
+ if (input === "auto") {
2252
+ return options.prefersDark ? DARK_THEME : LIGHT_THEME;
2253
+ }
2254
+ return FEATUREDROP_THEMES[input];
2255
+ }
2256
+ return mergeTheme(LIGHT_THEME, input);
2257
+ }
2258
+ function applyThemeSection(vars, key, values) {
2259
+ for (const [token, value] of Object.entries(values)) {
2260
+ vars[`--featuredrop-${key}-${token}`] = value;
2261
+ }
2262
+ }
2263
+ function themeToCSSVariables(theme) {
2264
+ const vars = {};
2265
+ applyThemeSection(vars, "color", theme.colors);
2266
+ applyThemeSection(vars, "font", theme.fonts);
2267
+ applyThemeSection(vars, "space", theme.spacing);
2268
+ applyThemeSection(vars, "radius", theme.radii);
2269
+ applyThemeSection(vars, "shadow", theme.shadows);
2270
+ applyThemeSection(vars, "z", theme.zIndex);
2271
+ vars["--featuredrop-font-family"] = theme.fonts.family;
2272
+ vars["--featuredrop-widget-bg"] = theme.colors.background;
2273
+ vars["--featuredrop-trigger-bg"] = theme.colors.background;
2274
+ vars["--featuredrop-trigger-color"] = theme.colors.text;
2275
+ vars["--featuredrop-entry-title-color"] = theme.colors.text;
2276
+ vars["--featuredrop-entry-desc-color"] = theme.colors.textMuted;
2277
+ vars["--featuredrop-title-color"] = theme.colors.text;
2278
+ vars["--featuredrop-border-color"] = theme.colors.border;
2279
+ vars["--featuredrop-cta-bg"] = theme.colors.primary;
2280
+ vars["--featuredrop-cta-color"] = theme.colors.background;
2281
+ vars["--featuredrop-mark-all-color"] = theme.colors.primary;
2282
+ vars["--featuredrop-widget-shadow"] = theme.shadows.md;
2283
+ vars["--featuredrop-widget-radius"] = theme.radii.lg;
2284
+ vars["--featuredrop-trigger-radius"] = theme.radii.md;
2285
+ vars["--featuredrop-badge-bg"] = theme.colors.warning;
2286
+ vars["--featuredrop-z-index"] = theme.zIndex.base;
2287
+ vars["--featuredrop-toast-z-index"] = theme.zIndex.tooltip;
2288
+ vars["--featuredrop-tour-z-index"] = theme.zIndex.modal;
2289
+ vars["--featuredrop-tour-overlay-z-index"] = theme.zIndex.overlay;
2290
+ return vars;
2291
+ }
2292
+
2293
+ // src/i18n.ts
2294
+ var EN_TRANSLATIONS = {
2295
+ newBadge: "New",
2296
+ whatsNewTitle: "What's New",
2297
+ markAllRead: "Mark all as read",
2298
+ allCaughtUp: "You're all caught up!",
2299
+ close: "Close",
2300
+ changelogTitle: "Changelog",
2301
+ searchPlaceholder: "Search updates",
2302
+ allCategories: "All categories",
2303
+ noUpdatesYet: "No updates yet",
2304
+ loadMore: "Load more",
2305
+ share: "Share",
2306
+ skipToEntries: "Skip to changelog entries",
2307
+ newFeatureCount: (count) => count === 0 ? "No new features" : `${count} new feature${count === 1 ? "" : "s"}`,
2308
+ stepOf: (current, total) => `Step ${current} of ${total}`,
2309
+ back: "Back",
2310
+ next: "Next",
2311
+ skip: "Skip",
2312
+ finish: "Finish",
2313
+ gotIt: "Got it",
2314
+ announcement: "Announcement",
2315
+ feedbackTitle: "Share feedback",
2316
+ feedbackTrigger: "Feedback",
2317
+ feedbackSubmitted: "Thanks for the feedback.",
2318
+ submit: "Submit",
2319
+ cancel: "Cancel",
2320
+ askLater: "Ask me later"
2321
+ };
2322
+ var SIMPLE_TRANSLATIONS = {
2323
+ es: {
2324
+ newBadge: "Nuevo",
2325
+ whatsNewTitle: "Novedades",
2326
+ markAllRead: "Marcar todo como le\xEDdo",
2327
+ allCaughtUp: "Est\xE1s al d\xEDa.",
2328
+ close: "Cerrar",
2329
+ changelogTitle: "Registro de cambios",
2330
+ searchPlaceholder: "Buscar actualizaciones",
2331
+ allCategories: "Todas las categor\xEDas",
2332
+ noUpdatesYet: "A\xFAn no hay actualizaciones",
2333
+ loadMore: "Cargar m\xE1s",
2334
+ share: "Compartir",
2335
+ skipToEntries: "Saltar a las entradas del changelog",
2336
+ back: "Atr\xE1s",
2337
+ next: "Siguiente",
2338
+ skip: "Saltar",
2339
+ finish: "Finalizar",
2340
+ gotIt: "Entendido",
2341
+ announcement: "Anuncio",
2342
+ feedbackTitle: "Enviar comentarios",
2343
+ feedbackTrigger: "Comentarios",
2344
+ feedbackSubmitted: "Gracias por tus comentarios.",
2345
+ submit: "Enviar",
2346
+ cancel: "Cancelar",
2347
+ askLater: "Preguntar m\xE1s tarde"
2348
+ },
2349
+ fr: {
2350
+ newBadge: "Nouveau",
2351
+ whatsNewTitle: "Nouveaut\xE9s",
2352
+ markAllRead: "Tout marquer comme lu",
2353
+ allCaughtUp: "Vous \xEAtes \xE0 jour.",
2354
+ close: "Fermer",
2355
+ changelogTitle: "Journal des changements",
2356
+ searchPlaceholder: "Rechercher des mises \xE0 jour",
2357
+ allCategories: "Toutes les cat\xE9gories",
2358
+ noUpdatesYet: "Aucune mise \xE0 jour",
2359
+ loadMore: "Charger plus",
2360
+ share: "Partager",
2361
+ skipToEntries: "Aller aux entr\xE9es du changelog",
2362
+ back: "Retour",
2363
+ next: "Suivant",
2364
+ skip: "Passer",
2365
+ finish: "Terminer",
2366
+ gotIt: "Compris",
2367
+ announcement: "Annonce",
2368
+ feedbackTitle: "Partager un avis",
2369
+ feedbackTrigger: "Avis",
2370
+ feedbackSubmitted: "Merci pour votre avis.",
2371
+ submit: "Envoyer",
2372
+ cancel: "Annuler",
2373
+ askLater: "Demander plus tard"
2374
+ },
2375
+ de: {
2376
+ newBadge: "Neu",
2377
+ whatsNewTitle: "Neuigkeiten",
2378
+ markAllRead: "Alles als gelesen markieren",
2379
+ allCaughtUp: "Alles erledigt.",
2380
+ close: "Schlie\xDFen",
2381
+ changelogTitle: "\xC4nderungsprotokoll",
2382
+ searchPlaceholder: "Updates suchen",
2383
+ allCategories: "Alle Kategorien",
2384
+ noUpdatesYet: "Noch keine Updates",
2385
+ loadMore: "Mehr laden",
2386
+ share: "Teilen",
2387
+ skipToEntries: "Zu den Eintr\xE4gen springen",
2388
+ back: "Zur\xFCck",
2389
+ next: "Weiter",
2390
+ skip: "\xDCberspringen",
2391
+ finish: "Fertig",
2392
+ gotIt: "Verstanden",
2393
+ announcement: "Ank\xFCndigung",
2394
+ feedbackTitle: "Feedback teilen",
2395
+ feedbackTrigger: "Feedback",
2396
+ feedbackSubmitted: "Danke f\xFCr dein Feedback.",
2397
+ submit: "Senden",
2398
+ cancel: "Abbrechen",
2399
+ askLater: "Sp\xE4ter fragen"
2400
+ },
2401
+ pt: {
2402
+ newBadge: "Novo",
2403
+ whatsNewTitle: "Novidades",
2404
+ markAllRead: "Marcar tudo como lido",
2405
+ allCaughtUp: "Tudo em dia.",
2406
+ close: "Fechar",
2407
+ changelogTitle: "Hist\xF3rico de mudan\xE7as",
2408
+ searchPlaceholder: "Buscar atualiza\xE7\xF5es",
2409
+ allCategories: "Todas as categorias",
2410
+ noUpdatesYet: "Sem atualiza\xE7\xF5es ainda",
2411
+ loadMore: "Carregar mais",
2412
+ share: "Compartilhar",
2413
+ skipToEntries: "Ir para entradas do changelog",
2414
+ back: "Voltar",
2415
+ next: "Pr\xF3ximo",
2416
+ skip: "Pular",
2417
+ finish: "Concluir",
2418
+ gotIt: "Entendi",
2419
+ announcement: "An\xFAncio",
2420
+ feedbackTitle: "Enviar feedback",
2421
+ feedbackTrigger: "Feedback",
2422
+ feedbackSubmitted: "Obrigado pelo feedback.",
2423
+ submit: "Enviar",
2424
+ cancel: "Cancelar",
2425
+ askLater: "Perguntar depois"
2426
+ },
2427
+ "zh-cn": {
2428
+ newBadge: "\u65B0",
2429
+ whatsNewTitle: "\u6700\u65B0\u52A8\u6001",
2430
+ markAllRead: "\u5168\u90E8\u6807\u8BB0\u4E3A\u5DF2\u8BFB",
2431
+ allCaughtUp: "\u4F60\u5DF2\u67E5\u770B\u5168\u90E8\u66F4\u65B0\u3002",
2432
+ close: "\u5173\u95ED",
2433
+ changelogTitle: "\u66F4\u65B0\u65E5\u5FD7",
2434
+ searchPlaceholder: "\u641C\u7D22\u66F4\u65B0",
2435
+ allCategories: "\u5168\u90E8\u5206\u7C7B",
2436
+ noUpdatesYet: "\u6682\u65E0\u66F4\u65B0",
2437
+ loadMore: "\u52A0\u8F7D\u66F4\u591A",
2438
+ share: "\u5206\u4EAB",
2439
+ skipToEntries: "\u8DF3\u8F6C\u5230\u66F4\u65B0\u6761\u76EE",
2440
+ back: "\u8FD4\u56DE",
2441
+ next: "\u4E0B\u4E00\u6B65",
2442
+ skip: "\u8DF3\u8FC7",
2443
+ finish: "\u5B8C\u6210",
2444
+ gotIt: "\u77E5\u9053\u4E86",
2445
+ announcement: "\u516C\u544A",
2446
+ feedbackTitle: "\u63D0\u4EA4\u53CD\u9988",
2447
+ feedbackTrigger: "\u53CD\u9988",
2448
+ feedbackSubmitted: "\u611F\u8C22\u4F60\u7684\u53CD\u9988\u3002",
2449
+ submit: "\u63D0\u4EA4",
2450
+ cancel: "\u53D6\u6D88",
2451
+ askLater: "\u7A0D\u540E\u8BE2\u95EE"
2452
+ },
2453
+ ja: {
2454
+ newBadge: "\u65B0\u7740",
2455
+ whatsNewTitle: "\u65B0\u6A5F\u80FD",
2456
+ markAllRead: "\u3059\u3079\u3066\u65E2\u8AAD\u306B\u3059\u308B",
2457
+ allCaughtUp: "\u3059\u3079\u3066\u78BA\u8A8D\u6E08\u307F\u3067\u3059\u3002",
2458
+ close: "\u9589\u3058\u308B",
2459
+ changelogTitle: "\u5909\u66F4\u5C65\u6B74",
2460
+ searchPlaceholder: "\u66F4\u65B0\u3092\u691C\u7D22",
2461
+ allCategories: "\u3059\u3079\u3066\u306E\u30AB\u30C6\u30B4\u30EA",
2462
+ noUpdatesYet: "\u66F4\u65B0\u306F\u3042\u308A\u307E\u305B\u3093",
2463
+ loadMore: "\u3055\u3089\u306B\u8868\u793A",
2464
+ share: "\u5171\u6709",
2465
+ skipToEntries: "\u5909\u66F4\u5C65\u6B74\u3078\u79FB\u52D5",
2466
+ back: "\u623B\u308B",
2467
+ next: "\u6B21\u3078",
2468
+ skip: "\u30B9\u30AD\u30C3\u30D7",
2469
+ finish: "\u5B8C\u4E86",
2470
+ gotIt: "\u4E86\u89E3",
2471
+ announcement: "\u304A\u77E5\u3089\u305B",
2472
+ feedbackTitle: "\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u3092\u9001\u4FE1",
2473
+ feedbackTrigger: "\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF",
2474
+ feedbackSubmitted: "\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u3042\u308A\u304C\u3068\u3046\u3054\u3056\u3044\u307E\u3059\u3002",
2475
+ submit: "\u9001\u4FE1",
2476
+ cancel: "\u30AD\u30E3\u30F3\u30BB\u30EB",
2477
+ askLater: "\u5F8C\u3067\u805E\u304F"
2478
+ },
2479
+ ko: {
2480
+ newBadge: "\uC0C8\uB85C\uC6C0",
2481
+ whatsNewTitle: "\uC0C8 \uC18C\uC2DD",
2482
+ markAllRead: "\uBAA8\uB450 \uC77D\uC74C \uCC98\uB9AC",
2483
+ allCaughtUp: "\uBAA8\uB4E0 \uC5C5\uB370\uC774\uD2B8\uB97C \uD655\uC778\uD588\uC2B5\uB2C8\uB2E4.",
2484
+ close: "\uB2EB\uAE30",
2485
+ changelogTitle: "\uBCC0\uACBD \uB85C\uADF8",
2486
+ searchPlaceholder: "\uC5C5\uB370\uC774\uD2B8 \uAC80\uC0C9",
2487
+ allCategories: "\uC804\uCCB4 \uCE74\uD14C\uACE0\uB9AC",
2488
+ noUpdatesYet: "\uC5C5\uB370\uC774\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
2489
+ loadMore: "\uB354 \uBCF4\uAE30",
2490
+ share: "\uACF5\uC720",
2491
+ skipToEntries: "\uBCC0\uACBD \uD56D\uBAA9\uC73C\uB85C \uC774\uB3D9",
2492
+ back: "\uB4A4\uB85C",
2493
+ next: "\uB2E4\uC74C",
2494
+ skip: "\uAC74\uB108\uB6F0\uAE30",
2495
+ finish: "\uC644\uB8CC",
2496
+ gotIt: "\uD655\uC778",
2497
+ announcement: "\uACF5\uC9C0",
2498
+ feedbackTitle: "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
2499
+ feedbackTrigger: "\uD53C\uB4DC\uBC31",
2500
+ feedbackSubmitted: "\uD53C\uB4DC\uBC31 \uAC10\uC0AC\uD569\uB2C8\uB2E4.",
2501
+ submit: "\uC81C\uCD9C",
2502
+ cancel: "\uCDE8\uC18C",
2503
+ askLater: "\uB098\uC911\uC5D0 \uBB3B\uAE30"
2504
+ },
2505
+ ar: {
2506
+ newBadge: "\u062C\u062F\u064A\u062F",
2507
+ whatsNewTitle: "\u0645\u0627 \u0627\u0644\u062C\u062F\u064A\u062F",
2508
+ markAllRead: "\u062A\u062D\u062F\u064A\u062F \u0627\u0644\u0643\u0644 \u0643\u0645\u0642\u0631\u0648\u0621",
2509
+ allCaughtUp: "\u062A\u0645\u062A \u0645\u062A\u0627\u0628\u0639\u0629 \u0643\u0644 \u0627\u0644\u062A\u062D\u062F\u064A\u062B\u0627\u062A.",
2510
+ close: "\u0625\u063A\u0644\u0627\u0642",
2511
+ changelogTitle: "\u0633\u062C\u0644 \u0627\u0644\u062A\u063A\u064A\u064A\u0631\u0627\u062A",
2512
+ searchPlaceholder: "\u0627\u0628\u062D\u062B \u0641\u064A \u0627\u0644\u062A\u062D\u062F\u064A\u062B\u0627\u062A",
2513
+ allCategories: "\u0643\u0644 \u0627\u0644\u0641\u0626\u0627\u062A",
2514
+ noUpdatesYet: "\u0644\u0627 \u062A\u0648\u062C\u062F \u062A\u062D\u062F\u064A\u062B\u0627\u062A \u0628\u0639\u062F",
2515
+ loadMore: "\u062A\u062D\u0645\u064A\u0644 \u0627\u0644\u0645\u0632\u064A\u062F",
2516
+ share: "\u0645\u0634\u0627\u0631\u0643\u0629",
2517
+ skipToEntries: "\u062A\u062E\u0637\u064A \u0625\u0644\u0649 \u0639\u0646\u0627\u0635\u0631 \u0627\u0644\u0633\u062C\u0644",
2518
+ back: "\u0631\u062C\u0648\u0639",
2519
+ next: "\u0627\u0644\u062A\u0627\u0644\u064A",
2520
+ skip: "\u062A\u062E\u0637\u064A",
2521
+ finish: "\u0625\u0646\u0647\u0627\u0621",
2522
+ gotIt: "\u062A\u0645",
2523
+ announcement: "\u0625\u0639\u0644\u0627\u0646",
2524
+ feedbackTitle: "\u0634\u0627\u0631\u0643 \u0645\u0644\u0627\u062D\u0638\u0627\u062A\u0643",
2525
+ feedbackTrigger: "\u0645\u0644\u0627\u062D\u0638\u0627\u062A",
2526
+ feedbackSubmitted: "\u0634\u0643\u0631\u064B\u0627 \u0639\u0644\u0649 \u0645\u0644\u0627\u062D\u0638\u0627\u062A\u0643.",
2527
+ submit: "\u0625\u0631\u0633\u0627\u0644",
2528
+ cancel: "\u0625\u0644\u063A\u0627\u0621",
2529
+ askLater: "\u0627\u0633\u0623\u0644\u0646\u064A \u0644\u0627\u062D\u0642\u064B\u0627"
2530
+ },
2531
+ hi: {
2532
+ newBadge: "\u0928\u092F\u093E",
2533
+ whatsNewTitle: "\u0928\u092F\u093E \u0915\u094D\u092F\u093E \u0939\u0948",
2534
+ markAllRead: "\u0938\u092D\u0940 \u0915\u094B \u092A\u0922\u093C\u093E \u0939\u0941\u0906 \u091A\u093F\u0939\u094D\u0928\u093F\u0924 \u0915\u0930\u0947\u0902",
2535
+ allCaughtUp: "\u0906\u092A\u0928\u0947 \u0938\u092D\u0940 \u0905\u092A\u0921\u0947\u091F \u0926\u0947\u0916 \u0932\u093F\u090F \u0939\u0948\u0902\u0964",
2536
+ close: "\u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
2537
+ changelogTitle: "\u092A\u0930\u093F\u0935\u0930\u094D\u0924\u0928 \u0938\u0942\u091A\u0940",
2538
+ searchPlaceholder: "\u0905\u092A\u0921\u0947\u091F \u0916\u094B\u091C\u0947\u0902",
2539
+ allCategories: "\u0938\u092D\u0940 \u0936\u094D\u0930\u0947\u0923\u093F\u092F\u093E\u0902",
2540
+ noUpdatesYet: "\u0905\u092D\u0940 \u0915\u094B\u0908 \u0905\u092A\u0921\u0947\u091F \u0928\u0939\u0940\u0902",
2541
+ loadMore: "\u0914\u0930 \u0932\u094B\u0921 \u0915\u0930\u0947\u0902",
2542
+ share: "\u0938\u093E\u091D\u093E \u0915\u0930\u0947\u0902",
2543
+ skipToEntries: "\u091A\u0947\u0902\u091C\u0932\u0949\u0917 \u092A\u094D\u0930\u0935\u093F\u0937\u094D\u091F\u093F\u092F\u094B\u0902 \u092A\u0930 \u091C\u093E\u090F\u0902",
2544
+ back: "\u0935\u093E\u092A\u0938",
2545
+ next: "\u0905\u0917\u0932\u093E",
2546
+ skip: "\u091B\u094B\u0921\u093C\u0947\u0902",
2547
+ finish: "\u0938\u092E\u093E\u092A\u094D\u0924",
2548
+ gotIt: "\u0920\u0940\u0915 \u0939\u0948",
2549
+ announcement: "\u0918\u094B\u0937\u0923\u093E",
2550
+ feedbackTitle: "\u092B\u0940\u0921\u092C\u0948\u0915 \u0938\u093E\u091D\u093E \u0915\u0930\u0947\u0902",
2551
+ feedbackTrigger: "\u092B\u0940\u0921\u092C\u0948\u0915",
2552
+ feedbackSubmitted: "\u092B\u0940\u0921\u092C\u0948\u0915 \u0915\u0947 \u0932\u093F\u090F \u0927\u0928\u094D\u092F\u0935\u093E\u0926\u0964",
2553
+ submit: "\u091C\u092E\u093E \u0915\u0930\u0947\u0902",
2554
+ cancel: "\u0930\u0926\u094D\u0926 \u0915\u0930\u0947\u0902",
2555
+ askLater: "\u092C\u093E\u0926 \u092E\u0947\u0902 \u092A\u0942\u091B\u0947\u0902"
2556
+ }
2557
+ };
2558
+ var RTL_LANGUAGES = /* @__PURE__ */ new Set(["ar", "fa", "he", "ur"]);
2559
+ var STEP_OF_TRANSLATIONS = {
2560
+ en: EN_TRANSLATIONS.stepOf,
2561
+ es: (current, total) => `Paso ${current} de ${total}`,
2562
+ fr: (current, total) => `Etape ${current} sur ${total}`,
2563
+ de: (current, total) => `Schritt ${current} von ${total}`,
2564
+ pt: (current, total) => `Etapa ${current} de ${total}`,
2565
+ "zh-cn": (current, total) => `\u7B2C ${current} / ${total} \u6B65`,
2566
+ ja: (current, total) => `${total}\u4E2D${current}\u756A\u76EE`,
2567
+ ko: (current, total) => `${total}\uB2E8\uACC4 \uC911 ${current}\uB2E8\uACC4`,
2568
+ ar: (current, total) => `\u0627\u0644\u062E\u0637\u0648\u0629 ${current} \u0645\u0646 ${total}`,
2569
+ hi: (current, total) => `${total} \u092E\u0947\u0902 \u0938\u0947 \u091A\u0930\u0923 ${current}`
2570
+ };
2571
+ var NEW_FEATURE_COUNT_TRANSLATIONS = {
2572
+ en: EN_TRANSLATIONS.newFeatureCount,
2573
+ es: (count) => count === 0 ? "Sin novedades" : `${count} novedad${count === 1 ? "" : "es"}`,
2574
+ fr: (count) => count === 0 ? "Aucune nouveaute" : `${count} nouveaute${count === 1 ? "" : "s"}`,
2575
+ de: (count) => count === 0 ? "Keine neuen Features" : `${count} ${count === 1 ? "neues Feature" : "neue Features"}`,
2576
+ pt: (count) => count === 0 ? "Sem novidades" : `${count} novidade${count === 1 ? "" : "s"}`,
2577
+ "zh-cn": (count) => count === 0 ? "\u6682\u65E0\u66F4\u65B0" : `${count} \u6761\u65B0\u66F4\u65B0`,
2578
+ ja: (count) => count === 0 ? "\u65B0\u7740\u306F\u3042\u308A\u307E\u305B\u3093" : `\u65B0\u7740 ${count} \u4EF6`,
2579
+ ko: (count) => count === 0 ? "\uC0C8 \uC18C\uC2DD \uC5C6\uC74C" : `\uC0C8 \uC18C\uC2DD ${count}\uAC1C`,
2580
+ ar: (count) => {
2581
+ if (count === 0) return "\u0644\u0627 \u062A\u0648\u062C\u062F \u0645\u064A\u0632\u0627\u062A \u062C\u062F\u064A\u062F\u0629";
2582
+ const category = new Intl.PluralRules("ar").select(count);
2583
+ if (category === "one") return "\u0645\u064A\u0632\u0629 \u062C\u062F\u064A\u062F\u0629 \u0648\u0627\u062D\u062F\u0629";
2584
+ if (category === "two") return "\u0645\u064A\u0632\u062A\u0627\u0646 \u062C\u062F\u064A\u062F\u062A\u0627\u0646";
2585
+ return `${count} \u0645\u064A\u0632\u0627\u062A \u062C\u062F\u064A\u062F\u0629`;
2586
+ },
2587
+ 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"}`
2588
+ };
2589
+ function resolveLocale(locale) {
2590
+ const normalized = (locale ?? "en").toLowerCase();
2591
+ if (normalized === "en" || normalized.startsWith("en-")) return "en";
2592
+ if (Object.prototype.hasOwnProperty.call(SIMPLE_TRANSLATIONS, normalized)) {
2593
+ return normalized;
2594
+ }
2595
+ const base = normalized.split("-")[0];
2596
+ if (base === "en") return "en";
2597
+ if (Object.prototype.hasOwnProperty.call(SIMPLE_TRANSLATIONS, base)) {
2598
+ return base;
2599
+ }
2600
+ return "en";
2601
+ }
2602
+ function getLocaleDirection(locale) {
2603
+ const resolved = resolveLocale(locale);
2604
+ const base = resolved.split("-")[0];
2605
+ return RTL_LANGUAGES.has(base) ? "rtl" : "ltr";
2606
+ }
2607
+ function formatDateForLocale(value, locale, options = {
2608
+ month: "short",
2609
+ day: "numeric",
2610
+ year: "numeric"
2611
+ }) {
2612
+ const date = value instanceof Date ? value : new Date(value);
2613
+ if (Number.isNaN(date.getTime())) return "";
2614
+ const resolved = resolveLocale(locale);
2615
+ try {
2616
+ return new Intl.DateTimeFormat(resolved, options).format(date);
2617
+ } catch {
2618
+ return date.toLocaleDateString(void 0, options);
2619
+ }
2620
+ }
2621
+ function formatRelativeTimeForLocale(value, locale, options) {
2622
+ const target = value instanceof Date ? value : new Date(value);
2623
+ if (Number.isNaN(target.getTime())) return "";
2624
+ const nowInput = options?.now;
2625
+ const nowDate = nowInput instanceof Date ? nowInput : typeof nowInput !== "undefined" ? new Date(nowInput) : /* @__PURE__ */ new Date();
2626
+ if (Number.isNaN(nowDate.getTime())) return "";
2627
+ const diffMs = target.getTime() - nowDate.getTime();
2628
+ const absDiff = Math.abs(diffMs);
2629
+ const minute = 6e4;
2630
+ const hour = 60 * minute;
2631
+ const day = 24 * hour;
2632
+ const week = 7 * day;
2633
+ const month = 30 * day;
2634
+ const year = 365 * day;
2635
+ let unit = "second";
2636
+ let divisor = 1e3;
2637
+ if (absDiff >= year) {
2638
+ unit = "year";
2639
+ divisor = year;
2640
+ } else if (absDiff >= month) {
2641
+ unit = "month";
2642
+ divisor = month;
2643
+ } else if (absDiff >= week) {
2644
+ unit = "week";
2645
+ divisor = week;
2646
+ } else if (absDiff >= day) {
2647
+ unit = "day";
2648
+ divisor = day;
2649
+ } else if (absDiff >= hour) {
2650
+ unit = "hour";
2651
+ divisor = hour;
2652
+ } else if (absDiff >= minute) {
2653
+ unit = "minute";
2654
+ divisor = minute;
2655
+ }
2656
+ const relativeValue = Math.round(diffMs / divisor);
2657
+ const resolvedLocale = resolveLocale(locale);
2658
+ try {
2659
+ const formatter = new Intl.RelativeTimeFormat(resolvedLocale, {
2660
+ numeric: options?.numeric ?? "auto",
2661
+ style: options?.style ?? "long"
2662
+ });
2663
+ return formatter.format(relativeValue, unit);
2664
+ } catch {
2665
+ const fallback = formatDateForLocale(target, resolvedLocale);
2666
+ return fallback || target.toISOString();
2667
+ }
2668
+ }
2669
+ function resolveTranslations(locale, overrides) {
2670
+ const resolvedLocale = resolveLocale(locale);
2671
+ const base = resolvedLocale === "en" ? {} : SIMPLE_TRANSLATIONS[resolvedLocale] ?? {};
2672
+ const stepOf = overrides?.stepOf ?? STEP_OF_TRANSLATIONS[resolvedLocale] ?? STEP_OF_TRANSLATIONS.en;
2673
+ const newFeatureCount = overrides?.newFeatureCount ?? NEW_FEATURE_COUNT_TRANSLATIONS[resolvedLocale] ?? NEW_FEATURE_COUNT_TRANSLATIONS.en;
2674
+ return {
2675
+ ...EN_TRANSLATIONS,
2676
+ ...base,
2677
+ ...overrides ?? {},
2678
+ stepOf,
2679
+ newFeatureCount
2680
+ };
2681
+ }
2682
+ var FEATUREDROP_TRANSLATIONS = {
2683
+ en: EN_TRANSLATIONS,
2684
+ ...SIMPLE_TRANSLATIONS
2685
+ };
2686
+
2687
+ // src/animation.ts
2688
+ var FEATUREDROP_ANIMATION_PRESETS = [
2689
+ "none",
2690
+ "subtle",
2691
+ "normal",
2692
+ "playful"
2693
+ ];
2694
+ function resolveAnimationPreset(preset = "normal", options) {
2695
+ if (options?.reducedMotion) return "none";
2696
+ return preset;
2697
+ }
2698
+ function getEnterAnimation(preset, surface) {
2699
+ if (preset === "none") return void 0;
2700
+ if (preset === "subtle") {
2701
+ if (surface === "panel") return "featuredrop-enter-panel 180ms ease-out";
2702
+ if (surface === "modal") return "featuredrop-enter-scale 180ms ease-out";
2703
+ return "featuredrop-enter-fade-up 170ms ease-out";
2704
+ }
2705
+ if (preset === "playful") {
2706
+ if (surface === "panel") return "featuredrop-enter-panel 320ms cubic-bezier(0.2, 0.9, 0.2, 1)";
2707
+ return "featuredrop-enter-pop 300ms cubic-bezier(0.22, 1.4, 0.36, 1)";
2708
+ }
2709
+ if (surface === "panel") return "featuredrop-enter-panel 240ms cubic-bezier(0.2, 0.9, 0.2, 1)";
2710
+ if (surface === "modal") return "featuredrop-enter-scale 220ms cubic-bezier(0.2, 0.9, 0.2, 1)";
2711
+ return "featuredrop-enter-fade-up 210ms cubic-bezier(0.2, 0.9, 0.2, 1)";
2712
+ }
2713
+ function getExitAnimation(preset, surface) {
2714
+ if (preset === "none") return void 0;
2715
+ if (preset === "subtle") {
2716
+ if (surface === "panel") return "featuredrop-exit-panel 150ms ease-in forwards";
2717
+ if (surface === "modal") return "featuredrop-exit-scale 150ms ease-in forwards";
2718
+ return "featuredrop-exit-fade-down 140ms ease-in forwards";
2719
+ }
2720
+ if (preset === "playful") {
2721
+ if (surface === "panel") return "featuredrop-exit-panel 260ms ease-in forwards";
2722
+ return "featuredrop-exit-pop 240ms ease-in forwards";
2723
+ }
2724
+ if (surface === "panel") return "featuredrop-exit-panel 200ms ease-in forwards";
2725
+ if (surface === "modal") return "featuredrop-exit-scale 190ms ease-in forwards";
2726
+ return "featuredrop-exit-fade-down 180ms ease-in forwards";
2727
+ }
2728
+ function getPulseAnimation(preset, surface = "beacon") {
2729
+ if (preset === "none") return void 0;
2730
+ if (surface === "dot") {
2731
+ if (preset === "subtle") return "featuredrop-pulse 2.6s ease-in-out infinite";
2732
+ if (preset === "playful") {
2733
+ return "featuredrop-pulse-playful 1.8s cubic-bezier(0.22, 1.4, 0.36, 1) infinite";
2734
+ }
2735
+ return "featuredrop-pulse 2s ease-in-out infinite";
2736
+ }
2737
+ if (preset === "subtle") return "featuredrop-beacon-pulse 2.6s ease-in-out infinite";
2738
+ if (preset === "playful") {
2739
+ return "featuredrop-beacon-pop-pulse 1.8s cubic-bezier(0.22, 1.4, 0.36, 1) infinite";
2740
+ }
2741
+ return "featuredrop-beacon-pulse 2s ease-in-out infinite";
2742
+ }
2743
+ function getAnimationDurationMs(preset, surface, phase) {
2744
+ if (preset === "none") return 0;
2745
+ const animation = phase === "enter" ? getEnterAnimation(preset, surface) : getExitAnimation(preset, surface);
2746
+ if (!animation) return 0;
2747
+ const msMatch = animation.match(/(\d+)ms/);
2748
+ if (msMatch?.[1]) return Number(msMatch[1]);
2749
+ const sMatch = animation.match(/(\d+(?:\.\d+)?)s/);
2750
+ if (sMatch?.[1]) return Math.round(Number(sMatch[1]) * 1e3);
2751
+ return 0;
2752
+ }
2753
+
2754
+ // src/throttle.ts
2755
+ function sortByPriorityAndRecency(features) {
2756
+ const priorityWeight = { critical: 3, normal: 2, low: 1 };
2757
+ return [...features].sort((a, b) => {
2758
+ const scoreA = priorityWeight[a.priority ?? "normal"];
2759
+ const scoreB = priorityWeight[b.priority ?? "normal"];
2760
+ if (scoreA !== scoreB) return scoreB - scoreA;
2761
+ return new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime();
2762
+ });
2763
+ }
2764
+ function applyAnnouncementThrottle(features, options, state, now = Date.now()) {
2765
+ const sorted = sortByPriorityAndRecency(features);
2766
+ if (sorted.length === 0) {
2767
+ return { visible: [], queued: [] };
2768
+ }
2769
+ if (options?.sessionCooldown && options.sessionCooldown > 0) {
2770
+ const elapsed = now - state.sessionStartedAt;
2771
+ if (elapsed < options.sessionCooldown) {
2772
+ return { visible: [], queued: sorted };
2773
+ }
2774
+ }
2775
+ if (options?.respectDoNotDisturb && state.quietMode) {
2776
+ const visible = sorted.filter((feature) => feature.priority === "critical");
2777
+ const queued = sorted.filter((feature) => feature.priority !== "critical");
2778
+ return { visible, queued };
2779
+ }
2780
+ const maxVisible = options?.maxSimultaneousBadges;
2781
+ if (!maxVisible || !Number.isFinite(maxVisible) || maxVisible < 1) {
2782
+ return { visible: sorted, queued: [] };
2783
+ }
2784
+ return {
2785
+ visible: sorted.slice(0, maxVisible),
2786
+ queued: sorted.slice(maxVisible)
2787
+ };
2788
+ }
2789
+
2790
+ // src/analytics.ts
2791
+ var AnalyticsCollector = class {
2792
+ adapter;
2793
+ queue = [];
2794
+ batchSize;
2795
+ flushInterval;
2796
+ sampleRate;
2797
+ enabled;
2798
+ now;
2799
+ random;
2800
+ sessionId;
2801
+ userId;
2802
+ timer = null;
2803
+ flushing = false;
2804
+ constructor(options) {
2805
+ this.adapter = options.adapter;
2806
+ this.batchSize = options.batchSize ?? 20;
2807
+ this.flushInterval = options.flushInterval ?? 1e4;
2808
+ this.sampleRate = options.sampleRate ?? 1;
2809
+ this.enabled = options.enabled ?? true;
2810
+ this.sessionId = options.sessionId;
2811
+ this.userId = options.userId;
2812
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
2813
+ this.random = options.random ?? Math.random;
2814
+ this.startTimer();
2815
+ }
2816
+ setEnabled(enabled) {
2817
+ this.enabled = enabled;
2818
+ }
2819
+ setContext(context) {
2820
+ if (context.sessionId !== void 0) this.sessionId = context.sessionId;
2821
+ if (context.userId !== void 0) this.userId = context.userId;
2822
+ }
2823
+ getQueueSize() {
2824
+ return this.queue.length;
2825
+ }
2826
+ track(event) {
2827
+ if (!this.enabled) return;
2828
+ if (this.sampleRate < 1 && this.random() > this.sampleRate) return;
2829
+ const normalized = {
2830
+ ...event,
2831
+ timestamp: event.timestamp ?? this.now().toISOString(),
2832
+ sessionId: event.sessionId ?? this.sessionId,
2833
+ userId: event.userId ?? this.userId
2834
+ };
2835
+ this.queue.push(normalized);
2836
+ if (this.queue.length >= this.batchSize) {
2837
+ void this.flush();
2838
+ }
2839
+ }
2840
+ async flush() {
2841
+ if (this.flushing) return;
2842
+ if (this.queue.length === 0) return;
2843
+ this.flushing = true;
2844
+ const batch = this.queue.splice(0, this.queue.length);
2845
+ try {
2846
+ if (this.adapter.trackBatch) {
2847
+ await this.adapter.trackBatch(batch);
2848
+ } else {
2849
+ for (const event of batch) {
2850
+ await this.adapter.track(event);
2851
+ }
2852
+ }
2853
+ } catch {
2854
+ this.queue = [...batch, ...this.queue];
2855
+ } finally {
2856
+ this.flushing = false;
2857
+ }
2858
+ }
2859
+ async destroy() {
2860
+ if (this.timer) {
2861
+ clearInterval(this.timer);
2862
+ this.timer = null;
2863
+ }
2864
+ await this.flush();
2865
+ }
2866
+ startTimer() {
2867
+ if (this.flushInterval <= 0) return;
2868
+ this.timer = setInterval(() => {
2869
+ void this.flush();
2870
+ }, this.flushInterval);
2871
+ }
2872
+ };
2873
+ var PostHogAdapter = class {
2874
+ constructor(client) {
2875
+ this.client = client;
2876
+ }
2877
+ track(event) {
2878
+ this.client.capture(event.type, {
2879
+ featureId: event.featureId,
2880
+ tourId: event.tourId,
2881
+ variant: event.variant,
2882
+ timestamp: event.timestamp,
2883
+ sessionId: event.sessionId,
2884
+ userId: event.userId,
2885
+ ...event.metadata
2886
+ });
2887
+ }
2888
+ };
2889
+ var AmplitudeAdapter = class {
2890
+ constructor(client) {
2891
+ this.client = client;
2892
+ }
2893
+ track(event) {
2894
+ this.client.track(event.type, {
2895
+ featureId: event.featureId,
2896
+ tourId: event.tourId,
2897
+ variant: event.variant,
2898
+ timestamp: event.timestamp,
2899
+ sessionId: event.sessionId,
2900
+ userId: event.userId,
2901
+ ...event.metadata
2902
+ });
2903
+ }
2904
+ };
2905
+ var MixpanelAdapter = class {
2906
+ constructor(client) {
2907
+ this.client = client;
2908
+ }
2909
+ track(event) {
2910
+ this.client.track(event.type, {
2911
+ featureId: event.featureId,
2912
+ tourId: event.tourId,
2913
+ variant: event.variant,
2914
+ timestamp: event.timestamp,
2915
+ sessionId: event.sessionId,
2916
+ userId: event.userId,
2917
+ ...event.metadata
2918
+ });
2919
+ }
2920
+ };
2921
+ var SegmentAdapter = class {
2922
+ constructor(client) {
2923
+ this.client = client;
2924
+ }
2925
+ track(event) {
2926
+ this.client.track(event.type, {
2927
+ featureId: event.featureId,
2928
+ tourId: event.tourId,
2929
+ variant: event.variant,
2930
+ timestamp: event.timestamp,
2931
+ sessionId: event.sessionId,
2932
+ userId: event.userId,
2933
+ ...event.metadata
2934
+ });
2935
+ }
2936
+ };
2937
+ var CustomAdapter = class {
2938
+ constructor(handler) {
2939
+ this.handler = handler;
2940
+ }
2941
+ track(event) {
2942
+ return this.handler(event);
2943
+ }
2944
+ };
2945
+ function createAdoptionMetrics(events) {
2946
+ const getAdoptionRate = (featureId) => {
2947
+ const seen = events.filter((event) => event.type === "feature_seen" && event.featureId === featureId).length;
2948
+ if (seen === 0) return 0;
2949
+ const clicked = events.filter((event) => event.type === "feature_clicked" && event.featureId === featureId).length;
2950
+ return clicked / seen;
2951
+ };
2952
+ const getTourCompletionRate = (tourId) => {
2953
+ const started = events.filter((event) => event.type === "tour_started" && event.tourId === tourId).length;
2954
+ if (started === 0) return 0;
2955
+ const completed = events.filter((event) => event.type === "tour_completed" && event.tourId === tourId).length;
2956
+ return completed / started;
2957
+ };
2958
+ const getChecklistCompletionRate = (checklistId) => {
2959
+ const taskCompleted = events.filter(
2960
+ (event) => event.type === "checklist_task_completed" && event.metadata?.checklistId === checklistId
2961
+ ).length;
2962
+ if (taskCompleted === 0) return 0;
2963
+ const completed = events.filter(
2964
+ (event) => event.type === "checklist_completed" && event.metadata?.checklistId === checklistId
2965
+ ).length;
2966
+ return completed / taskCompleted;
2967
+ };
2968
+ const getFeatureEngagement = (featureId) => ({
2969
+ seen: events.filter((event) => event.type === "feature_seen" && event.featureId === featureId).length,
2970
+ clicked: events.filter((event) => event.type === "feature_clicked" && event.featureId === featureId).length,
2971
+ dismissed: events.filter((event) => event.type === "feature_dismissed" && event.featureId === featureId).length
2972
+ });
2973
+ const getVariantPerformance = (featureId) => {
2974
+ const byVariant = /* @__PURE__ */ new Map();
2975
+ for (const event of events) {
2976
+ if (event.featureId !== featureId) continue;
2977
+ const variant = event.variant ?? "control";
2978
+ const bucket = byVariant.get(variant) ?? { seen: 0, clicked: 0 };
2979
+ if (event.type === "feature_seen") bucket.seen += 1;
2980
+ if (event.type === "feature_clicked") bucket.clicked += 1;
2981
+ byVariant.set(variant, bucket);
2982
+ }
2983
+ const output = {};
2984
+ for (const [variant, bucket] of byVariant.entries()) {
2985
+ output[variant] = bucket.seen === 0 ? 0 : bucket.clicked / bucket.seen;
2986
+ }
2987
+ return output;
2988
+ };
2989
+ return {
2990
+ getAdoptionRate,
2991
+ getTourCompletionRate,
2992
+ getChecklistCompletionRate,
2993
+ getFeatureEngagement,
2994
+ getVariantPerformance
2995
+ };
2996
+ }
2997
+
2998
+ // src/variants.ts
2999
+ var VARIANT_META_KEY = "featuredropVariant";
3000
+ var VARIANT_KEY_STORAGE = "featuredrop:variant-key";
3001
+ function readStorageValue(key) {
3002
+ const storage = globalThis.localStorage;
3003
+ if (!storage || typeof storage.getItem !== "function") return null;
3004
+ try {
3005
+ return storage.getItem(key);
3006
+ } catch {
3007
+ return null;
3008
+ }
3009
+ }
3010
+ function writeStorageValue(key, value) {
3011
+ const storage = globalThis.localStorage;
3012
+ if (!storage || typeof storage.setItem !== "function") return;
3013
+ try {
3014
+ storage.setItem(key, value);
3015
+ } catch {
3016
+ }
3017
+ }
3018
+ function hashToPercent(value) {
3019
+ let hash = 2166136261;
3020
+ for (let i = 0; i < value.length; i += 1) {
3021
+ hash ^= value.charCodeAt(i);
3022
+ hash = Math.imul(hash, 16777619);
3023
+ }
3024
+ return (hash >>> 0) % 100;
3025
+ }
3026
+ function normalizeSplit(count, split) {
3027
+ if (!split || split.length !== count) {
3028
+ return Array.from({ length: count }, () => 100 / count);
3029
+ }
3030
+ const cleaned = split.map((value) => Number.isFinite(value) && value > 0 ? value : 0);
3031
+ const total = cleaned.reduce((sum, value) => sum + value, 0);
3032
+ if (total <= 0) {
3033
+ return Array.from({ length: count }, () => 100 / count);
3034
+ }
3035
+ return cleaned.map((value) => value / total * 100);
3036
+ }
3037
+ function pickVariantName(feature, variantKey) {
3038
+ const variants = feature.variants;
3039
+ if (!variants) return null;
3040
+ const names = Object.keys(variants);
3041
+ if (names.length === 0) return null;
3042
+ if (names.length === 1) return names[0];
3043
+ const split = normalizeSplit(names.length, feature.variantSplit);
3044
+ const bucket = hashToPercent(`${feature.id}:${variantKey}`);
3045
+ let cumulative = 0;
3046
+ for (let i = 0; i < names.length; i += 1) {
3047
+ cumulative += split[i];
3048
+ if (bucket < cumulative) return names[i];
3049
+ }
3050
+ return names[names.length - 1];
3051
+ }
3052
+ function getFeatureVariantName(feature) {
3053
+ const raw = feature.meta?.[VARIANT_META_KEY];
3054
+ return typeof raw === "string" ? raw : void 0;
3055
+ }
3056
+ function applyFeatureVariant(feature, variantKey) {
3057
+ const variantName = pickVariantName(feature, variantKey);
3058
+ if (!variantName) return feature;
3059
+ const variant = feature.variants?.[variantName];
3060
+ if (!variant) return feature;
3061
+ return {
3062
+ ...feature,
3063
+ label: variant.label ?? feature.label,
3064
+ description: variant.description ?? feature.description,
3065
+ image: variant.image ?? feature.image,
3066
+ cta: variant.cta ?? feature.cta,
3067
+ meta: {
3068
+ ...feature.meta ?? {},
3069
+ ...variant.meta ?? {},
3070
+ [VARIANT_META_KEY]: variantName
3071
+ }
3072
+ };
3073
+ }
3074
+ function applyFeatureVariants(manifest, variantKey) {
3075
+ return manifest.map((feature) => applyFeatureVariant(feature, variantKey));
3076
+ }
3077
+ function createRandomKey() {
3078
+ return Math.random().toString(36).slice(2, 12);
3079
+ }
3080
+ function getOrCreateVariantKey(explicitKey) {
3081
+ if (explicitKey) return explicitKey;
3082
+ const existing = readStorageValue(VARIANT_KEY_STORAGE);
3083
+ if (existing) return existing;
3084
+ const next = createRandomKey();
3085
+ writeStorageValue(VARIANT_KEY_STORAGE, next);
3086
+ return next;
3087
+ }
3088
+
3089
+ // src/cli-utils.ts
3090
+ function computeManifestStats(entries) {
3091
+ const byType = {};
3092
+ const byCategory = {};
3093
+ for (const entry of entries) {
3094
+ const type = entry.type ?? "feature";
3095
+ byType[type] = (byType[type] ?? 0) + 1;
3096
+ if (entry.category) {
3097
+ byCategory[entry.category] = (byCategory[entry.category] ?? 0) + 1;
3098
+ }
3099
+ }
3100
+ const sortedByDate = [...entries].sort(
3101
+ (a, b) => new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime()
3102
+ );
3103
+ return {
3104
+ total: entries.length,
3105
+ byType,
3106
+ byCategory,
3107
+ newestRelease: sortedByDate[0]?.releasedAt ?? null,
3108
+ oldestRelease: sortedByDate[sortedByDate.length - 1]?.releasedAt ?? null
3109
+ };
3110
+ }
3111
+ function generateMarkdownChangelog(entries) {
3112
+ const sorted = [...entries].sort(
3113
+ (a, b) => new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime()
3114
+ );
3115
+ const sections = sorted.map((entry) => {
3116
+ const lines = [
3117
+ `## ${entry.label}`,
3118
+ "",
3119
+ `- **ID**: \`${entry.id}\``,
3120
+ `- **Released**: ${entry.releasedAt}`
3121
+ ];
3122
+ if (entry.type) lines.push(`- **Type**: ${entry.type}`);
3123
+ if (entry.category) lines.push(`- **Category**: ${entry.category}`);
3124
+ if (entry.showNewUntil) lines.push(`- **Show new until**: ${entry.showNewUntil}`);
3125
+ if (entry.cta) lines.push(`- **CTA**: [${entry.cta.label}](${entry.cta.url})`);
3126
+ if (entry.description) {
3127
+ lines.push("", entry.description.trim());
3128
+ }
3129
+ return lines.join("\n");
3130
+ });
3131
+ return `# Generated Changelog
3132
+
3133
+ ${sections.join("\n\n---\n\n")}
3134
+ `;
3135
+ }
3136
+ function isIsoWithTimezone(value) {
3137
+ if (!value.includes("T")) return false;
3138
+ if (!(value.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(value))) return false;
3139
+ return Number.isFinite(new Date(value).getTime());
3140
+ }
3141
+ function runDoctor(entries, now = /* @__PURE__ */ new Date()) {
3142
+ const checks = [];
3143
+ const warnings = [];
3144
+ const errors = [];
3145
+ checks.push(`Manifest entries loaded: ${entries.length}`);
3146
+ const ids = /* @__PURE__ */ new Set();
3147
+ let duplicateCount = 0;
3148
+ for (const entry of entries) {
3149
+ if (ids.has(entry.id)) duplicateCount += 1;
3150
+ ids.add(entry.id);
3151
+ }
3152
+ if (duplicateCount > 0) {
3153
+ errors.push(`${duplicateCount} duplicate feature id(s) found`);
3154
+ } else {
3155
+ checks.push("No duplicate IDs");
3156
+ }
3157
+ let invalidDateCount = 0;
3158
+ let reversedDateCount = 0;
3159
+ let expiredCount = 0;
3160
+ let scheduledCount = 0;
3161
+ let missingDescriptionCount = 0;
3162
+ for (const entry of entries) {
3163
+ if (!entry.description?.trim()) missingDescriptionCount += 1;
3164
+ if (!isIsoWithTimezone(entry.releasedAt) || !isIsoWithTimezone(entry.showNewUntil)) {
3165
+ invalidDateCount += 1;
3166
+ continue;
3167
+ }
3168
+ const released = new Date(entry.releasedAt).getTime();
3169
+ const showUntil = new Date(entry.showNewUntil).getTime();
3170
+ if (showUntil <= released) reversedDateCount += 1;
3171
+ if (showUntil < now.getTime()) expiredCount += 1;
3172
+ if (entry.publishAt) {
3173
+ const publishMs = new Date(entry.publishAt).getTime();
3174
+ if (Number.isFinite(publishMs) && publishMs > now.getTime()) scheduledCount += 1;
3175
+ }
3176
+ }
3177
+ if (invalidDateCount > 0) {
3178
+ errors.push(`${invalidDateCount} entries have invalid ISO 8601 dates with timezone`);
3179
+ } else {
3180
+ checks.push("All dates are valid ISO 8601 with timezone");
3181
+ }
3182
+ if (reversedDateCount > 0) {
3183
+ errors.push(`${reversedDateCount} entries have showNewUntil before/at releasedAt`);
3184
+ }
3185
+ if (expiredCount > 0) warnings.push(`${expiredCount} entries have showNewUntil in the past`);
3186
+ if (scheduledCount > 0) warnings.push(`${scheduledCount} entries have publishAt in the future`);
3187
+ if (missingDescriptionCount > 0) {
3188
+ errors.push(`${missingDescriptionCount} entries have no description`);
3189
+ } else {
3190
+ checks.push("All entries have descriptions");
3191
+ }
3192
+ if (hasDependencyCycle(entries)) {
3193
+ errors.push("Circular dependsOn relationship detected");
3194
+ } else {
3195
+ checks.push("No circular dependencies in dependsOn chains");
3196
+ }
3197
+ return { checks, warnings, errors };
3198
+ }
3199
+
3200
+ // src/adapters/local-storage.ts
3201
+ var DISMISSED_SUFFIX = ":dismissed";
3202
+ var LocalStorageAdapter = class {
3203
+ prefix;
3204
+ watermarkValue;
3205
+ onDismissAllCallback;
3206
+ dismissedKey;
3207
+ constructor(options = {}) {
3208
+ this.prefix = options.prefix ?? "featuredrop";
3209
+ this.watermarkValue = options.watermark ?? null;
3210
+ this.onDismissAllCallback = options.onDismissAll;
3211
+ this.dismissedKey = `${this.prefix}${DISMISSED_SUFFIX}`;
3212
+ }
3213
+ getWatermark() {
3214
+ return this.watermarkValue;
3215
+ }
3216
+ getDismissedIds() {
3217
+ try {
3218
+ if (typeof window === "undefined") return /* @__PURE__ */ new Set();
3219
+ const raw = localStorage.getItem(this.dismissedKey);
3220
+ if (!raw) return /* @__PURE__ */ new Set();
3221
+ const parsed = JSON.parse(raw);
3222
+ if (Array.isArray(parsed)) return new Set(parsed);
3223
+ return /* @__PURE__ */ new Set();
3224
+ } catch {
3225
+ return /* @__PURE__ */ new Set();
3226
+ }
3227
+ }
3228
+ dismiss(id) {
3229
+ try {
3230
+ if (typeof window === "undefined") return;
3231
+ const raw = localStorage.getItem(this.dismissedKey);
3232
+ const existing = raw ? JSON.parse(raw) : [];
3233
+ if (!existing.includes(id)) {
3234
+ existing.push(id);
3235
+ localStorage.setItem(this.dismissedKey, JSON.stringify(existing));
3236
+ }
3237
+ } catch {
3238
+ }
3239
+ }
3240
+ async dismissAll(now) {
3241
+ try {
3242
+ if (typeof window !== "undefined") {
3243
+ localStorage.removeItem(this.dismissedKey);
3244
+ }
3245
+ } catch {
3246
+ }
3247
+ if (this.onDismissAllCallback) {
3248
+ await this.onDismissAllCallback(now);
3249
+ }
3250
+ }
3251
+ };
3252
+
3253
+ // src/adapters/indexeddb.ts
3254
+ var DISMISSED_SUFFIX2 = ":dismissed";
3255
+ var WATERMARK_SUFFIX = ":watermark";
3256
+ var QUEUE_SUFFIX = ":queue";
3257
+ function canUseLocalStorage() {
3258
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
3259
+ }
3260
+ function readLocalStorageState(prefix) {
3261
+ if (!canUseLocalStorage()) {
3262
+ return { watermark: null, dismissed: [], queue: [] };
3263
+ }
3264
+ try {
3265
+ const dismissedRaw = localStorage.getItem(`${prefix}${DISMISSED_SUFFIX2}`);
3266
+ const watermarkRaw = localStorage.getItem(`${prefix}${WATERMARK_SUFFIX}`);
3267
+ const queueRaw = localStorage.getItem(`${prefix}${QUEUE_SUFFIX}`);
3268
+ const dismissedParsed = dismissedRaw ? JSON.parse(dismissedRaw) : [];
3269
+ const queueParsed = queueRaw ? JSON.parse(queueRaw) : [];
3270
+ return {
3271
+ watermark: typeof watermarkRaw === "string" ? watermarkRaw : null,
3272
+ dismissed: Array.isArray(dismissedParsed) ? dismissedParsed.filter((value) => typeof value === "string") : [],
3273
+ queue: normalizeQueue(queueParsed)
3274
+ };
3275
+ } catch {
3276
+ return { watermark: null, dismissed: [], queue: [] };
3277
+ }
3278
+ }
3279
+ function writeLocalStorageState(prefix, state) {
3280
+ if (!canUseLocalStorage()) return;
3281
+ try {
3282
+ localStorage.setItem(`${prefix}${DISMISSED_SUFFIX2}`, JSON.stringify(state.dismissed));
3283
+ if (state.watermark) {
3284
+ localStorage.setItem(`${prefix}${WATERMARK_SUFFIX}`, state.watermark);
3285
+ } else {
3286
+ localStorage.removeItem(`${prefix}${WATERMARK_SUFFIX}`);
3287
+ }
3288
+ if (state.queue && state.queue.length > 0) {
3289
+ localStorage.setItem(`${prefix}${QUEUE_SUFFIX}`, JSON.stringify(state.queue));
3290
+ } else {
3291
+ localStorage.removeItem(`${prefix}${QUEUE_SUFFIX}`);
3292
+ }
3293
+ } catch {
3294
+ }
3295
+ }
3296
+ function getIndexedDBFactory() {
3297
+ if (typeof globalThis === "undefined") return null;
3298
+ const candidate = globalThis.indexedDB;
3299
+ return candidate ?? null;
3300
+ }
3301
+ function normalizeQueue(value) {
3302
+ if (!Array.isArray(value)) return [];
3303
+ const queue = [];
3304
+ for (const item of value) {
3305
+ if (!item || typeof item !== "object") continue;
3306
+ const candidate = item;
3307
+ if (candidate.type === "dismiss" && typeof candidate.id === "string") {
3308
+ queue.push({ type: "dismiss", id: candidate.id });
3309
+ continue;
3310
+ }
3311
+ if (candidate.type === "dismissAll" && typeof candidate.watermark === "string") {
3312
+ queue.push({ type: "dismissAll", watermark: candidate.watermark });
3313
+ continue;
3314
+ }
3315
+ }
3316
+ return queue;
3317
+ }
3318
+ function normalizeDismissedIds(value) {
3319
+ if (!Array.isArray(value)) return [];
3320
+ return value.filter((entry) => typeof entry === "string");
3321
+ }
3322
+ function parseIso(value) {
3323
+ if (!value) return Number.NaN;
3324
+ return new Date(value).getTime();
3325
+ }
3326
+ function resolveLatestWatermark(a, b) {
3327
+ if (!a) return b ?? null;
3328
+ if (!b) return a;
3329
+ const aTs = parseIso(a);
3330
+ const bTs = parseIso(b);
3331
+ if (!Number.isFinite(aTs)) return b;
3332
+ if (!Number.isFinite(bTs)) return a;
3333
+ return aTs >= bTs ? a : b;
3334
+ }
3335
+ var IndexedDBAdapter = class {
3336
+ prefix;
3337
+ dbName;
3338
+ storeName;
3339
+ onDismissAllCallback;
3340
+ onSyncStateCallback;
3341
+ onFlushDismissBatchCallback;
3342
+ onFlushDismissAllCallback;
3343
+ flushDebounceMs;
3344
+ autoSyncOnOnline;
3345
+ watermark;
3346
+ dismissed;
3347
+ queue;
3348
+ hydratePromise;
3349
+ flushTimer = null;
3350
+ flushing = false;
3351
+ boundOnlineHandler;
3352
+ boundVisibilityHandler;
3353
+ constructor(options = {}) {
3354
+ this.prefix = options.prefix ?? "featuredrop";
3355
+ this.dbName = options.dbName ?? "featuredrop";
3356
+ this.storeName = options.storeName ?? "state";
3357
+ this.onDismissAllCallback = options.onDismissAll;
3358
+ this.onSyncStateCallback = options.onSyncState;
3359
+ this.onFlushDismissBatchCallback = options.onFlushDismissBatch;
3360
+ this.onFlushDismissAllCallback = options.onFlushDismissAll;
3361
+ this.flushDebounceMs = options.flushDebounceMs ?? 500;
3362
+ this.autoSyncOnOnline = options.autoSyncOnOnline ?? true;
3363
+ const localState = readLocalStorageState(this.prefix);
3364
+ this.watermark = options.watermark ?? localState.watermark;
3365
+ this.dismissed = new Set(localState.dismissed);
3366
+ this.queue = localState.queue ?? [];
3367
+ this.hydratePromise = this.hydrateFromIndexedDB();
3368
+ const canAttachListeners = this.autoSyncOnOnline && typeof window !== "undefined";
3369
+ if (canAttachListeners) {
3370
+ this.boundOnlineHandler = () => {
3371
+ void this.syncFromRemote();
3372
+ };
3373
+ this.boundVisibilityHandler = () => {
3374
+ if (document.visibilityState === "visible") {
3375
+ void this.syncFromRemote();
3376
+ }
3377
+ };
3378
+ window.addEventListener("online", this.boundOnlineHandler);
3379
+ document.addEventListener("visibilitychange", this.boundVisibilityHandler);
3380
+ } else {
3381
+ this.boundOnlineHandler = null;
3382
+ this.boundVisibilityHandler = null;
3383
+ }
3384
+ }
3385
+ getWatermark() {
3386
+ return this.watermark;
3387
+ }
3388
+ getDismissedIds() {
3389
+ return this.dismissed;
3390
+ }
3391
+ dismiss(id) {
3392
+ if (!id || this.dismissed.has(id)) return;
3393
+ this.dismissed = new Set(this.dismissed).add(id);
3394
+ this.queue.push({ type: "dismiss", id });
3395
+ this.persist();
3396
+ this.scheduleFlush();
3397
+ }
3398
+ async dismissAll(now) {
3399
+ this.watermark = now.toISOString();
3400
+ this.dismissed = /* @__PURE__ */ new Set();
3401
+ this.queue = [{ type: "dismissAll", watermark: this.watermark }];
3402
+ this.persist();
3403
+ this.scheduleFlush();
3404
+ await this.onDismissAllCallback?.(now);
3405
+ }
3406
+ /** Flush queued dismiss operations to optional remote callbacks. */
3407
+ async flushQueue() {
3408
+ if (this.flushing || this.queue.length === 0) return;
3409
+ if (!this.onFlushDismissBatchCallback && !this.onFlushDismissAllCallback) return;
3410
+ this.flushing = true;
3411
+ try {
3412
+ const operations = [...this.queue];
3413
+ const lastDismissAll = this.getLastDismissAll(operations);
3414
+ const dismissIds = this.collectDismissBatch(operations, !!lastDismissAll);
3415
+ const hasDismissAll = !!lastDismissAll;
3416
+ const needsDismissBatch = dismissIds.length > 0;
3417
+ if (hasDismissAll && !this.onFlushDismissAllCallback) return;
3418
+ if (needsDismissBatch && !this.onFlushDismissBatchCallback) return;
3419
+ if (lastDismissAll && this.onFlushDismissAllCallback) {
3420
+ await this.onFlushDismissAllCallback(lastDismissAll.watermark);
3421
+ }
3422
+ if (dismissIds.length > 0 && this.onFlushDismissBatchCallback) {
3423
+ await this.onFlushDismissBatchCallback(dismissIds);
3424
+ }
3425
+ if (this.queue.length <= operations.length) {
3426
+ this.queue = [];
3427
+ } else {
3428
+ this.queue = this.queue.slice(operations.length);
3429
+ }
3430
+ this.persist();
3431
+ } catch {
3432
+ } finally {
3433
+ this.flushing = false;
3434
+ }
3435
+ }
3436
+ /** Merge local state with optional remote source, then flush queued writes. */
3437
+ async syncFromRemote() {
3438
+ await this.hydratePromise.catch(() => void 0);
3439
+ if (this.onSyncStateCallback) {
3440
+ try {
3441
+ const remote = await this.onSyncStateCallback();
3442
+ const mergedDismissed = new Set(this.dismissed);
3443
+ for (const id of normalizeDismissedIds(remote.dismissedIds)) {
3444
+ mergedDismissed.add(id);
3445
+ }
3446
+ this.dismissed = mergedDismissed;
3447
+ this.watermark = resolveLatestWatermark(this.watermark, remote.watermark ?? null);
3448
+ this.persist();
3449
+ } catch {
3450
+ }
3451
+ }
3452
+ await this.flushQueue();
3453
+ }
3454
+ /** Cleanup optional browser listeners. */
3455
+ destroy() {
3456
+ if (this.flushTimer) {
3457
+ clearTimeout(this.flushTimer);
3458
+ this.flushTimer = null;
3459
+ }
3460
+ if (this.boundOnlineHandler && typeof window !== "undefined") {
3461
+ window.removeEventListener("online", this.boundOnlineHandler);
3462
+ }
3463
+ if (this.boundVisibilityHandler && typeof document !== "undefined") {
3464
+ document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
3465
+ }
3466
+ }
3467
+ persist() {
3468
+ const snapshot = {
3469
+ watermark: this.watermark,
3470
+ dismissed: Array.from(this.dismissed),
3471
+ queue: this.queue
3472
+ };
3473
+ writeLocalStorageState(this.prefix, snapshot);
3474
+ void this.writeIndexedDBState(snapshot);
3475
+ }
3476
+ async hydrateFromIndexedDB() {
3477
+ const state = await this.readIndexedDBState();
3478
+ if (!state) return;
3479
+ this.watermark = state.watermark;
3480
+ this.dismissed = new Set(state.dismissed);
3481
+ this.queue = state.queue ?? [];
3482
+ writeLocalStorageState(this.prefix, state);
3483
+ }
3484
+ async readIndexedDBState() {
3485
+ const db = await this.openDb();
3486
+ if (!db) return null;
3487
+ return new Promise((resolve) => {
3488
+ const tx = db.transaction(this.storeName, "readonly");
3489
+ const store = tx.objectStore(this.storeName);
3490
+ const request = store.get(this.prefix);
3491
+ request.onsuccess = () => {
3492
+ const value = request.result;
3493
+ if (!value) {
3494
+ resolve(null);
3495
+ return;
3496
+ }
3497
+ resolve({
3498
+ watermark: typeof value.watermark === "string" ? value.watermark : null,
3499
+ dismissed: normalizeDismissedIds(value.dismissed),
3500
+ queue: normalizeQueue(value.queue)
3501
+ });
3502
+ };
3503
+ request.onerror = () => resolve(null);
3504
+ });
3505
+ }
3506
+ async writeIndexedDBState(state) {
3507
+ await this.hydratePromise.catch(() => void 0);
3508
+ const db = await this.openDb();
3509
+ if (!db) return;
3510
+ await new Promise((resolve) => {
3511
+ const tx = db.transaction(this.storeName, "readwrite");
3512
+ const store = tx.objectStore(this.storeName);
3513
+ store.put(state, this.prefix);
3514
+ tx.oncomplete = () => resolve();
3515
+ tx.onerror = () => resolve();
3516
+ tx.onabort = () => resolve();
3517
+ });
3518
+ }
3519
+ async openDb() {
3520
+ const factory = getIndexedDBFactory();
3521
+ if (!factory) return null;
3522
+ return new Promise((resolve) => {
3523
+ const request = factory.open(this.dbName, 1);
3524
+ request.onerror = () => resolve(null);
3525
+ request.onupgradeneeded = () => {
3526
+ const db = request.result;
3527
+ if (!db.objectStoreNames.contains(this.storeName)) {
3528
+ db.createObjectStore(this.storeName);
3529
+ }
3530
+ };
3531
+ request.onsuccess = () => resolve(request.result);
3532
+ });
3533
+ }
3534
+ scheduleFlush() {
3535
+ if (this.flushTimer) return;
3536
+ this.flushTimer = setTimeout(() => {
3537
+ this.flushTimer = null;
3538
+ void this.flushQueue();
3539
+ }, this.flushDebounceMs);
3540
+ }
3541
+ getLastDismissAll(operations) {
3542
+ for (let index = operations.length - 1; index >= 0; index--) {
3543
+ const operation = operations[index];
3544
+ if (operation.type === "dismissAll") {
3545
+ return { watermark: operation.watermark };
3546
+ }
3547
+ }
3548
+ return null;
3549
+ }
3550
+ collectDismissBatch(operations, skipBeforeDismissAll) {
3551
+ const startIndex = skipBeforeDismissAll ? operations.reduce(
3552
+ (lastIndex, operation, index) => operation.type === "dismissAll" ? index : lastIndex,
3553
+ -1
3554
+ ) : -1;
3555
+ const batch = /* @__PURE__ */ new Set();
3556
+ for (let index = startIndex + 1; index < operations.length; index++) {
3557
+ const operation = operations[index];
3558
+ if (operation.type === "dismiss") {
3559
+ batch.add(operation.id);
3560
+ }
3561
+ }
3562
+ return Array.from(batch);
3563
+ }
3564
+ };
3565
+
3566
+ // src/adapters/memory.ts
3567
+ var MemoryAdapter = class {
3568
+ watermark;
3569
+ dismissed;
3570
+ constructor(options = {}) {
3571
+ this.watermark = options.watermark ?? null;
3572
+ this.dismissed = /* @__PURE__ */ new Set();
3573
+ }
3574
+ getWatermark() {
3575
+ return this.watermark;
3576
+ }
3577
+ getDismissedIds() {
3578
+ return this.dismissed;
3579
+ }
3580
+ dismiss(id) {
3581
+ this.dismissed.add(id);
3582
+ }
3583
+ async dismissAll(now) {
3584
+ this.watermark = now.toISOString();
3585
+ this.dismissed.clear();
3586
+ }
3587
+ };
3588
+
3589
+ // src/adapters/remote.ts
3590
+ function assertFetch() {
3591
+ if (typeof fetch === "undefined") {
3592
+ throw new Error("RemoteAdapter requires global fetch (Node 18+ or polyfill)");
3593
+ }
3594
+ return fetch;
3595
+ }
3596
+ var RemoteAdapter = class {
3597
+ baseUrl;
3598
+ headers;
3599
+ fetchInterval;
3600
+ userId;
3601
+ dismissedIds = /* @__PURE__ */ new Set();
3602
+ watermark = null;
3603
+ lastManifest = null;
3604
+ lastFetchTs = 0;
3605
+ retryAttempts;
3606
+ retryBaseDelayMs;
3607
+ circuitBreakerThreshold;
3608
+ circuitBreakerCooldownMs;
3609
+ sleep;
3610
+ now;
3611
+ consecutiveFailures = 0;
3612
+ circuitOpenUntil = 0;
3613
+ constructor(options) {
3614
+ this.baseUrl = options.url.replace(/\/$/, "");
3615
+ this.headers = options.headers ?? {};
3616
+ this.fetchInterval = options.fetchInterval ?? 5 * 60 * 1e3;
3617
+ this.userId = options.userId;
3618
+ this.retryAttempts = options.retryAttempts ?? 3;
3619
+ this.retryBaseDelayMs = options.retryBaseDelayMs ?? 250;
3620
+ this.circuitBreakerThreshold = options.circuitBreakerThreshold ?? 5;
3621
+ this.circuitBreakerCooldownMs = options.circuitBreakerCooldownMs ?? 6e4;
3622
+ this.sleep = options.sleep ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
3623
+ this.now = options.now ?? (() => Date.now());
3624
+ }
3625
+ /** Fetch manifest with stale-while-revalidate */
3626
+ async fetchManifest(force = false) {
3627
+ const now = this.now();
3628
+ if (!force && this.lastManifest && now - this.lastFetchTs < this.fetchInterval) {
3629
+ return this.lastManifest;
3630
+ }
3631
+ try {
3632
+ const json = await this.withRetry(async () => {
3633
+ const fetchImpl = assertFetch();
3634
+ const res = await fetchImpl(this.baseUrl, {
3635
+ method: "GET",
3636
+ headers: this.headers
3637
+ });
3638
+ if (!res.ok) throw new Error(`RemoteAdapter manifest fetch failed: ${res.status}`);
3639
+ return await res.json();
3640
+ });
3641
+ this.lastManifest = json;
3642
+ this.lastFetchTs = now;
3643
+ return json;
3644
+ } catch {
3645
+ return this.lastManifest ?? [];
3646
+ }
3647
+ }
3648
+ /** Fetch state (watermark + dismissed IDs) */
3649
+ async syncState() {
3650
+ try {
3651
+ const json = await this.withRetry(async () => {
3652
+ const fetchImpl = assertFetch();
3653
+ const url = this.userId ? `${this.baseUrl}/state?userId=${encodeURIComponent(this.userId)}` : `${this.baseUrl}/state`;
3654
+ const res = await fetchImpl(url, {
3655
+ method: "GET",
3656
+ headers: this.headers
3657
+ });
3658
+ if (!res.ok) throw new Error(`RemoteAdapter state sync failed: ${res.status}`);
3659
+ return await res.json();
3660
+ });
3661
+ if (json.watermark !== void 0) this.watermark = json.watermark;
3662
+ if (Array.isArray(json.dismissedIds)) this.dismissedIds = new Set(json.dismissedIds);
3663
+ } catch {
3664
+ }
3665
+ }
3666
+ getWatermark() {
3667
+ return this.watermark;
3668
+ }
3669
+ getDismissedIds() {
3670
+ return this.dismissedIds;
3671
+ }
3672
+ dismiss(id) {
3673
+ this.dismissedIds.add(id);
3674
+ this.flushDismiss(id).catch(() => {
3675
+ });
3676
+ }
3677
+ async dismissAll(now) {
3678
+ this.watermark = now.toISOString();
3679
+ this.dismissedIds.clear();
3680
+ await this.flushDismissAll(now).catch(() => {
3681
+ });
3682
+ }
3683
+ /** Returns current adapter health; false while circuit breaker is open. */
3684
+ async isHealthy() {
3685
+ if (this.isCircuitOpen()) return false;
3686
+ try {
3687
+ await this.withRetry(async () => {
3688
+ const fetchImpl = assertFetch();
3689
+ const res = await fetchImpl(this.baseUrl, {
3690
+ method: "GET",
3691
+ headers: this.headers
3692
+ });
3693
+ if (!res.ok) throw new Error(`RemoteAdapter health check failed: ${res.status}`);
3694
+ });
3695
+ return true;
3696
+ } catch {
3697
+ return false;
3698
+ }
3699
+ }
3700
+ async flushDismiss(id) {
3701
+ await this.withRetry(async () => {
3702
+ const fetchImpl = assertFetch();
3703
+ const res = await fetchImpl(`${this.baseUrl}/dismiss`, {
3704
+ method: "POST",
3705
+ headers: { "Content-Type": "application/json", ...this.headers },
3706
+ body: JSON.stringify({ featureId: id })
3707
+ });
3708
+ if (!res.ok) throw new Error(`RemoteAdapter dismiss failed: ${res.status}`);
3709
+ });
3710
+ }
3711
+ async flushDismissAll(now) {
3712
+ await this.withRetry(async () => {
3713
+ const fetchImpl = assertFetch();
3714
+ const res = await fetchImpl(`${this.baseUrl}/dismiss-all`, {
3715
+ method: "POST",
3716
+ headers: { "Content-Type": "application/json", ...this.headers },
3717
+ body: JSON.stringify({ watermark: now.toISOString() })
3718
+ });
3719
+ if (!res.ok) throw new Error(`RemoteAdapter dismiss-all failed: ${res.status}`);
3720
+ });
3721
+ }
3722
+ isCircuitOpen() {
3723
+ return this.now() < this.circuitOpenUntil;
3724
+ }
3725
+ markFailure() {
3726
+ this.consecutiveFailures += 1;
3727
+ if (this.consecutiveFailures >= this.circuitBreakerThreshold) {
3728
+ this.circuitOpenUntil = this.now() + this.circuitBreakerCooldownMs;
3729
+ }
3730
+ }
3731
+ markSuccess() {
3732
+ this.consecutiveFailures = 0;
3733
+ this.circuitOpenUntil = 0;
3734
+ }
3735
+ async withRetry(operation) {
3736
+ if (this.isCircuitOpen()) {
3737
+ throw new Error("RemoteAdapter circuit breaker is open");
3738
+ }
3739
+ let lastError = new Error("RemoteAdapter request failed");
3740
+ for (let attempt = 0; attempt <= this.retryAttempts; attempt++) {
3741
+ try {
3742
+ const result = await operation();
3743
+ this.markSuccess();
3744
+ return result;
3745
+ } catch (error) {
3746
+ lastError = error;
3747
+ if (attempt >= this.retryAttempts) break;
3748
+ const delayMs = this.retryBaseDelayMs * 2 ** attempt;
3749
+ await this.sleep(delayMs);
3750
+ }
3751
+ }
3752
+ this.markFailure();
3753
+ throw lastError instanceof Error ? lastError : new Error("RemoteAdapter request failed");
3754
+ }
3755
+ };
3756
+
3757
+ // src/adapters/postgres.ts
3758
+ function normalizeDismissedIds2(row) {
3759
+ if (!row) return [];
3760
+ const ids = row.dismissed_ids ?? row.dismissedIds;
3761
+ if (!Array.isArray(ids)) return [];
3762
+ return ids.filter((id) => typeof id === "string");
3763
+ }
3764
+ function normalizeWatermark(row) {
3765
+ if (!row) return null;
3766
+ return row.watermark ?? null;
3767
+ }
3768
+ function normalizeLastSeen(row) {
3769
+ if (!row) return (/* @__PURE__ */ new Date(0)).toISOString();
3770
+ return row.last_seen ?? row.lastSeen ?? (/* @__PURE__ */ new Date(0)).toISOString();
3771
+ }
3772
+ var PostgresAdapter = class {
3773
+ userId;
3774
+ query;
3775
+ tableName;
3776
+ autoMigrate;
3777
+ watermark = null;
3778
+ dismissedIds = /* @__PURE__ */ new Set();
3779
+ initialized = false;
3780
+ constructor(options) {
3781
+ if (!options.userId) {
3782
+ throw new Error("PostgresAdapter: userId is required");
3783
+ }
3784
+ this.userId = options.userId;
3785
+ this.query = options.query;
3786
+ this.tableName = options.tableName ?? "featuredrop_state";
3787
+ this.autoMigrate = options.autoMigrate ?? true;
3788
+ }
3789
+ getWatermark() {
3790
+ return this.watermark;
3791
+ }
3792
+ getDismissedIds() {
3793
+ return this.dismissedIds;
3794
+ }
3795
+ dismiss(id) {
3796
+ this.dismissedIds.add(id);
3797
+ void this.dismissBatch([id]);
3798
+ }
3799
+ async dismissAll(now) {
3800
+ this.watermark = now.toISOString();
3801
+ this.dismissedIds.clear();
3802
+ await this.ensureReady();
3803
+ await this.query(
3804
+ `INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
3805
+ VALUES ($1, $2, '{}', NOW(), NOW(), NOW())
3806
+ ON CONFLICT (user_id)
3807
+ DO UPDATE SET watermark = EXCLUDED.watermark, dismissed_ids = '{}', last_seen = NOW(), updated_at = NOW()`,
3808
+ [this.userId, this.watermark]
3809
+ );
3810
+ }
3811
+ async sync() {
3812
+ await this.ensureReady();
3813
+ const result = await this.query(
3814
+ `SELECT watermark, dismissed_ids, last_seen
3815
+ FROM ${this.tableName}
3816
+ WHERE user_id = $1`,
3817
+ [this.userId]
3818
+ );
3819
+ const row = result.rows[0];
3820
+ this.watermark = normalizeWatermark(row);
3821
+ this.dismissedIds = new Set(normalizeDismissedIds2(row));
3822
+ }
3823
+ async dismissBatch(ids) {
3824
+ if (ids.length === 0) return;
3825
+ await this.ensureReady();
3826
+ const uniqueIds = Array.from(new Set(ids));
3827
+ await this.query(
3828
+ `INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
3829
+ VALUES ($1, NULL, $2::text[], NOW(), NOW(), NOW())
3830
+ ON CONFLICT (user_id)
3831
+ DO UPDATE SET
3832
+ dismissed_ids = (
3833
+ SELECT ARRAY(
3834
+ SELECT DISTINCT x FROM UNNEST(
3835
+ COALESCE(${this.tableName}.dismissed_ids, '{}') || EXCLUDED.dismissed_ids
3836
+ ) AS x
3837
+ )
3838
+ ),
3839
+ last_seen = NOW(),
3840
+ updated_at = NOW()`,
3841
+ [this.userId, uniqueIds]
3842
+ );
3843
+ }
3844
+ async resetUser(userId) {
3845
+ await this.ensureReady();
3846
+ await this.query(
3847
+ `DELETE FROM ${this.tableName} WHERE user_id = $1`,
3848
+ [userId]
3849
+ );
3850
+ if (userId === this.userId) {
3851
+ this.watermark = null;
3852
+ this.dismissedIds.clear();
3853
+ }
3854
+ }
3855
+ async getBulkState(userIds) {
3856
+ await this.ensureReady();
3857
+ if (userIds.length === 0) return /* @__PURE__ */ new Map();
3858
+ const result = await this.query(
3859
+ `SELECT user_id, watermark, dismissed_ids, last_seen
3860
+ FROM ${this.tableName}
3861
+ WHERE user_id = ANY($1::text[])`,
3862
+ [userIds]
3863
+ );
3864
+ const out = /* @__PURE__ */ new Map();
3865
+ for (const row of result.rows) {
3866
+ out.set(row.user_id, {
3867
+ watermark: normalizeWatermark(row),
3868
+ dismissedIds: normalizeDismissedIds2(row),
3869
+ lastSeen: normalizeLastSeen(row),
3870
+ deviceCount: 1
3871
+ });
3872
+ }
3873
+ return out;
3874
+ }
3875
+ async isHealthy() {
3876
+ try {
3877
+ await this.query("SELECT 1");
3878
+ return true;
3879
+ } catch {
3880
+ return false;
3881
+ }
3882
+ }
3883
+ async destroy() {
3884
+ }
3885
+ async ensureReady() {
3886
+ if (this.initialized) return;
3887
+ if (this.autoMigrate) {
3888
+ await this.query(
3889
+ `CREATE TABLE IF NOT EXISTS ${this.tableName} (
3890
+ user_id TEXT PRIMARY KEY,
3891
+ watermark TIMESTAMPTZ,
3892
+ dismissed_ids TEXT[] DEFAULT '{}',
3893
+ last_seen TIMESTAMPTZ DEFAULT NOW(),
3894
+ created_at TIMESTAMPTZ DEFAULT NOW(),
3895
+ updated_at TIMESTAMPTZ DEFAULT NOW()
3896
+ )`
3897
+ );
3898
+ await this.query(
3899
+ `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_seen ON ${this.tableName}(last_seen)`
3900
+ );
3901
+ }
3902
+ this.initialized = true;
3903
+ }
3904
+ };
3905
+
3906
+ // src/adapters/redis.ts
3907
+ var RedisAdapter = class {
3908
+ userId;
3909
+ client;
3910
+ keyPrefix;
3911
+ watermark = null;
3912
+ dismissedIds = /* @__PURE__ */ new Set();
3913
+ constructor(options) {
3914
+ if (!options.userId) {
3915
+ throw new Error("RedisAdapter: userId is required");
3916
+ }
3917
+ this.userId = options.userId;
3918
+ this.client = options.client;
3919
+ this.keyPrefix = options.keyPrefix ?? "fd:";
3920
+ }
3921
+ getWatermark() {
3922
+ return this.watermark;
3923
+ }
3924
+ getDismissedIds() {
3925
+ return this.dismissedIds;
3926
+ }
3927
+ dismiss(id) {
3928
+ this.dismissedIds.add(id);
3929
+ void this.client.sadd(this.dismissedKey(this.userId), id);
3930
+ void this.client.set(this.lastSeenKey(this.userId), (/* @__PURE__ */ new Date()).toISOString());
3931
+ }
3932
+ async dismissAll(now) {
3933
+ this.watermark = now.toISOString();
3934
+ this.dismissedIds.clear();
3935
+ await this.client.multi().set(this.watermarkKey(this.userId), this.watermark).del(this.dismissedKey(this.userId)).set(this.lastSeenKey(this.userId), now.toISOString()).exec();
3936
+ }
3937
+ async sync() {
3938
+ const [watermark, dismissedIds] = await Promise.all([
3939
+ this.client.get(this.watermarkKey(this.userId)),
3940
+ this.client.smembers(this.dismissedKey(this.userId))
3941
+ ]);
3942
+ this.watermark = watermark;
3943
+ this.dismissedIds = new Set(dismissedIds);
3944
+ }
3945
+ async dismissBatch(ids) {
3946
+ const uniqueIds = Array.from(new Set(ids));
3947
+ if (uniqueIds.length === 0) return;
3948
+ this.dismissedIds = /* @__PURE__ */ new Set([...this.dismissedIds, ...uniqueIds]);
3949
+ await this.client.sadd(this.dismissedKey(this.userId), ...uniqueIds);
3950
+ await this.client.set(this.lastSeenKey(this.userId), (/* @__PURE__ */ new Date()).toISOString());
3951
+ }
3952
+ async resetUser(userId) {
3953
+ await this.client.multi().del(this.watermarkKey(userId)).del(this.dismissedKey(userId)).del(this.lastSeenKey(userId)).exec();
3954
+ if (userId === this.userId) {
3955
+ this.watermark = null;
3956
+ this.dismissedIds.clear();
3957
+ }
3958
+ }
3959
+ async getBulkState(userIds) {
3960
+ const map = /* @__PURE__ */ new Map();
3961
+ await Promise.all(
3962
+ userIds.map(async (userId) => {
3963
+ const [watermark, dismissedIds, lastSeen] = await Promise.all([
3964
+ this.client.get(this.watermarkKey(userId)),
3965
+ this.client.smembers(this.dismissedKey(userId)),
3966
+ this.client.get(this.lastSeenKey(userId))
3967
+ ]);
3968
+ map.set(userId, {
3969
+ watermark,
3970
+ dismissedIds,
3971
+ lastSeen: lastSeen ?? (/* @__PURE__ */ new Date(0)).toISOString(),
3972
+ deviceCount: 1
3973
+ });
3974
+ })
3975
+ );
3976
+ return map;
3977
+ }
3978
+ async isHealthy() {
3979
+ try {
3980
+ const response = await this.client.ping();
3981
+ return response.toUpperCase() === "PONG";
3982
+ } catch {
3983
+ return false;
3984
+ }
3985
+ }
3986
+ async destroy() {
3987
+ if (this.client.quit) {
3988
+ await this.client.quit();
3989
+ return;
3990
+ }
3991
+ this.client.disconnect?.();
3992
+ }
3993
+ watermarkKey(userId) {
3994
+ return `${this.keyPrefix}${userId}:watermark`;
3995
+ }
3996
+ dismissedKey(userId) {
3997
+ return `${this.keyPrefix}${userId}:dismissed`;
3998
+ }
3999
+ lastSeenKey(userId) {
4000
+ return `${this.keyPrefix}${userId}:last_seen`;
4001
+ }
4002
+ };
4003
+
4004
+ // src/adapters/hybrid.ts
4005
+ var HybridAdapter = class {
4006
+ userId;
4007
+ local;
4008
+ remote;
4009
+ syncBeforeWrite;
4010
+ dismissBatchWindowMs;
4011
+ syncIntervalMs;
4012
+ syncOnVisibilityChange;
4013
+ syncOnOnline;
4014
+ pendingDismissIds = /* @__PURE__ */ new Set();
4015
+ dismissTimer = null;
4016
+ syncTimer = null;
4017
+ boundVisibilityHandler;
4018
+ boundOnlineHandler;
4019
+ constructor(options) {
4020
+ this.local = options.local;
4021
+ this.remote = options.remote;
4022
+ this.userId = options.remote.userId;
4023
+ this.syncBeforeWrite = options.syncBeforeWrite ?? false;
4024
+ this.dismissBatchWindowMs = options.dismissBatchWindowMs ?? 500;
4025
+ this.syncIntervalMs = options.syncIntervalMs ?? 0;
4026
+ this.syncOnVisibilityChange = options.syncOnVisibilityChange ?? true;
4027
+ this.syncOnOnline = options.syncOnOnline ?? true;
4028
+ if (typeof window !== "undefined" && this.syncOnOnline) {
4029
+ this.boundOnlineHandler = () => {
4030
+ void this.sync();
4031
+ };
4032
+ window.addEventListener("online", this.boundOnlineHandler);
4033
+ } else {
4034
+ this.boundOnlineHandler = null;
4035
+ }
4036
+ if (typeof document !== "undefined" && this.syncOnVisibilityChange) {
4037
+ this.boundVisibilityHandler = () => {
4038
+ if (document.visibilityState === "visible") {
4039
+ void this.sync();
4040
+ }
4041
+ };
4042
+ document.addEventListener("visibilitychange", this.boundVisibilityHandler);
4043
+ } else {
4044
+ this.boundVisibilityHandler = null;
4045
+ }
4046
+ if (this.syncIntervalMs > 0) {
4047
+ this.syncTimer = setInterval(() => {
4048
+ void this.sync();
4049
+ }, this.syncIntervalMs);
4050
+ }
4051
+ }
4052
+ getWatermark() {
4053
+ return this.local.getWatermark() ?? this.remote.getWatermark();
4054
+ }
4055
+ getDismissedIds() {
4056
+ const merged = /* @__PURE__ */ new Set();
4057
+ for (const id of this.local.getDismissedIds()) merged.add(id);
4058
+ for (const id of this.remote.getDismissedIds()) merged.add(id);
4059
+ return merged;
4060
+ }
4061
+ dismiss(id) {
4062
+ this.local.dismiss(id);
4063
+ this.pendingDismissIds.add(id);
4064
+ this.scheduleDismissFlush();
4065
+ }
4066
+ async dismissAll(now) {
4067
+ await this.flushPendingDismisses();
4068
+ this.pendingDismissIds.clear();
4069
+ await Promise.all([
4070
+ this.local.dismissAll(now),
4071
+ this.remote.dismissAll(now)
4072
+ ]);
4073
+ }
4074
+ async sync() {
4075
+ await this.remote.sync();
4076
+ }
4077
+ async dismissBatch(ids) {
4078
+ if (this.syncBeforeWrite) {
4079
+ await this.remote.sync();
4080
+ }
4081
+ for (const id of ids) {
4082
+ this.local.dismiss(id);
4083
+ }
4084
+ await this.remote.dismissBatch(ids);
4085
+ }
4086
+ async resetUser(userId) {
4087
+ await this.remote.resetUser(userId);
4088
+ if (userId === this.userId) {
4089
+ await this.local.dismissAll(/* @__PURE__ */ new Date(0));
4090
+ }
4091
+ }
4092
+ async getBulkState(userIds) {
4093
+ return this.remote.getBulkState(userIds);
4094
+ }
4095
+ async isHealthy() {
4096
+ return this.remote.isHealthy();
4097
+ }
4098
+ async destroy() {
4099
+ if (this.dismissTimer) {
4100
+ clearTimeout(this.dismissTimer);
4101
+ this.dismissTimer = null;
4102
+ }
4103
+ if (this.syncTimer) {
4104
+ clearInterval(this.syncTimer);
4105
+ this.syncTimer = null;
4106
+ }
4107
+ if (this.boundOnlineHandler && typeof window !== "undefined") {
4108
+ window.removeEventListener("online", this.boundOnlineHandler);
4109
+ }
4110
+ if (this.boundVisibilityHandler && typeof document !== "undefined") {
4111
+ document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
4112
+ }
4113
+ await this.flushPendingDismisses();
4114
+ await this.remote.destroy();
4115
+ }
4116
+ /** Manually flush queued dismiss operations to the remote adapter. */
4117
+ async flushPendingDismisses() {
4118
+ if (this.pendingDismissIds.size === 0) return;
4119
+ const ids = Array.from(this.pendingDismissIds);
4120
+ this.pendingDismissIds.clear();
4121
+ try {
4122
+ if (this.syncBeforeWrite) {
4123
+ await this.remote.sync();
4124
+ }
4125
+ await this.remote.dismissBatch(ids);
4126
+ } catch {
4127
+ for (const id of ids) this.pendingDismissIds.add(id);
4128
+ }
4129
+ }
4130
+ scheduleDismissFlush() {
4131
+ if (this.dismissTimer) return;
4132
+ this.dismissTimer = setTimeout(() => {
4133
+ this.dismissTimer = null;
4134
+ void this.flushPendingDismisses();
4135
+ }, this.dismissBatchWindowMs);
4136
+ }
4137
+ };
4138
+
4139
+ // src/adapters/mysql.ts
4140
+ function parseDismissedIds(value) {
4141
+ if (Array.isArray(value)) {
4142
+ return value.filter((item) => typeof item === "string");
4143
+ }
4144
+ if (typeof value === "string" && value.trim()) {
4145
+ try {
4146
+ const parsed = JSON.parse(value);
4147
+ if (Array.isArray(parsed)) {
4148
+ return parsed.filter((item) => typeof item === "string");
4149
+ }
4150
+ } catch {
4151
+ return [];
4152
+ }
4153
+ }
4154
+ return [];
4155
+ }
4156
+ function normalizeDismissedIds3(row) {
4157
+ if (!row) return [];
4158
+ return parseDismissedIds(row.dismissed_ids ?? row.dismissedIds);
4159
+ }
4160
+ function normalizeWatermark2(row) {
4161
+ if (!row) return null;
4162
+ return row.watermark ?? null;
4163
+ }
4164
+ function normalizeLastSeen2(row) {
4165
+ if (!row) return (/* @__PURE__ */ new Date(0)).toISOString();
4166
+ return row.last_seen ?? row.lastSeen ?? (/* @__PURE__ */ new Date(0)).toISOString();
4167
+ }
4168
+ var MySQLAdapter = class {
4169
+ userId;
4170
+ query;
4171
+ tableName;
4172
+ autoMigrate;
4173
+ watermark = null;
4174
+ dismissedIds = /* @__PURE__ */ new Set();
4175
+ initialized = false;
4176
+ constructor(options) {
4177
+ if (!options.userId) {
4178
+ throw new Error("MySQLAdapter: userId is required");
4179
+ }
4180
+ this.userId = options.userId;
4181
+ this.query = options.query;
4182
+ this.tableName = options.tableName ?? "featuredrop_state";
4183
+ this.autoMigrate = options.autoMigrate ?? true;
4184
+ }
4185
+ getWatermark() {
4186
+ return this.watermark;
4187
+ }
4188
+ getDismissedIds() {
4189
+ return this.dismissedIds;
4190
+ }
4191
+ dismiss(id) {
4192
+ this.dismissedIds.add(id);
4193
+ void this.dismissBatch([id]);
4194
+ }
4195
+ async dismissAll(now) {
4196
+ this.watermark = now.toISOString();
4197
+ this.dismissedIds.clear();
4198
+ await this.ensureReady();
4199
+ await this.query(
4200
+ `INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
4201
+ VALUES (?, ?, ?, NOW(3), NOW(3), NOW(3))
4202
+ ON DUPLICATE KEY UPDATE watermark = VALUES(watermark), dismissed_ids = VALUES(dismissed_ids), last_seen = NOW(3), updated_at = NOW(3)`,
4203
+ [this.userId, this.watermark, JSON.stringify([])]
4204
+ );
4205
+ }
4206
+ async sync() {
4207
+ await this.ensureReady();
4208
+ const result = await this.query(
4209
+ `SELECT watermark, dismissed_ids, last_seen FROM ${this.tableName} WHERE user_id = ? LIMIT 1`,
4210
+ [this.userId]
4211
+ );
4212
+ const row = result.rows[0];
4213
+ this.watermark = normalizeWatermark2(row);
4214
+ this.dismissedIds = new Set(normalizeDismissedIds3(row));
4215
+ }
4216
+ async dismissBatch(ids) {
4217
+ const uniqueIds = Array.from(new Set(ids));
4218
+ if (uniqueIds.length === 0) return;
4219
+ await this.ensureReady();
4220
+ const merged = /* @__PURE__ */ new Set([
4221
+ ...Array.from(this.dismissedIds),
4222
+ ...uniqueIds
4223
+ ]);
4224
+ const mergedArray = Array.from(merged);
4225
+ this.dismissedIds = merged;
4226
+ await this.query(
4227
+ `INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
4228
+ VALUES (?, ?, ?, NOW(3), NOW(3), NOW(3))
4229
+ ON DUPLICATE KEY UPDATE dismissed_ids = VALUES(dismissed_ids), last_seen = NOW(3), updated_at = NOW(3)`,
4230
+ [this.userId, this.watermark, JSON.stringify(mergedArray)]
4231
+ );
4232
+ }
4233
+ async resetUser(userId) {
4234
+ await this.ensureReady();
4235
+ await this.query(`DELETE FROM ${this.tableName} WHERE user_id = ?`, [userId]);
4236
+ if (userId === this.userId) {
4237
+ this.watermark = null;
4238
+ this.dismissedIds.clear();
4239
+ }
4240
+ }
4241
+ async getBulkState(userIds) {
4242
+ await this.ensureReady();
4243
+ if (userIds.length === 0) return /* @__PURE__ */ new Map();
4244
+ const placeholders = userIds.map(() => "?").join(", ");
4245
+ const result = await this.query(
4246
+ `SELECT user_id, watermark, dismissed_ids, last_seen
4247
+ FROM ${this.tableName}
4248
+ WHERE user_id IN (${placeholders})`,
4249
+ userIds
4250
+ );
4251
+ const out = /* @__PURE__ */ new Map();
4252
+ for (const row of result.rows) {
4253
+ out.set(row.user_id, {
4254
+ watermark: normalizeWatermark2(row),
4255
+ dismissedIds: normalizeDismissedIds3(row),
4256
+ lastSeen: normalizeLastSeen2(row),
4257
+ deviceCount: 1
4258
+ });
4259
+ }
4260
+ return out;
4261
+ }
4262
+ async isHealthy() {
4263
+ try {
4264
+ await this.query("SELECT 1");
4265
+ return true;
4266
+ } catch {
4267
+ return false;
4268
+ }
4269
+ }
4270
+ async destroy() {
4271
+ }
4272
+ async ensureReady() {
4273
+ if (this.initialized) return;
4274
+ if (this.autoMigrate) {
4275
+ await this.query(
4276
+ `CREATE TABLE IF NOT EXISTS ${this.tableName} (
4277
+ user_id VARCHAR(255) PRIMARY KEY,
4278
+ watermark DATETIME(3) NULL,
4279
+ dismissed_ids JSON NOT NULL,
4280
+ last_seen DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
4281
+ created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
4282
+ updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
4283
+ )`
4284
+ );
4285
+ await this.query(
4286
+ `CREATE INDEX idx_${this.tableName}_last_seen ON ${this.tableName}(last_seen)`
4287
+ );
4288
+ }
4289
+ this.initialized = true;
4290
+ }
4291
+ };
4292
+
4293
+ // src/adapters/mongo.ts
4294
+ function normalizeDismissedIds4(ids) {
4295
+ if (!Array.isArray(ids)) return [];
4296
+ return ids.filter((id) => typeof id === "string");
4297
+ }
4298
+ function normalizeLastSeen3(value) {
4299
+ return typeof value === "string" && value ? value : (/* @__PURE__ */ new Date(0)).toISOString();
4300
+ }
4301
+ var MongoAdapter = class {
4302
+ userId;
4303
+ collection;
4304
+ watermark = null;
4305
+ dismissedIds = /* @__PURE__ */ new Set();
4306
+ constructor(options) {
4307
+ if (!options.userId) {
4308
+ throw new Error("MongoAdapter: userId is required");
4309
+ }
4310
+ this.userId = options.userId;
4311
+ this.collection = options.collection;
4312
+ }
4313
+ getWatermark() {
4314
+ return this.watermark;
4315
+ }
4316
+ getDismissedIds() {
4317
+ return this.dismissedIds;
4318
+ }
4319
+ dismiss(id) {
4320
+ this.dismissedIds.add(id);
4321
+ void this.collection.updateOne(
4322
+ { userId: this.userId },
4323
+ {
4324
+ $addToSet: { dismissedIds: id },
4325
+ $set: { lastSeen: (/* @__PURE__ */ new Date()).toISOString() }
4326
+ },
4327
+ { upsert: true }
4328
+ );
4329
+ }
4330
+ async dismissAll(now) {
4331
+ this.watermark = now.toISOString();
4332
+ this.dismissedIds.clear();
4333
+ await this.collection.updateOne(
4334
+ { userId: this.userId },
4335
+ {
4336
+ $set: {
4337
+ userId: this.userId,
4338
+ watermark: this.watermark,
4339
+ dismissedIds: [],
4340
+ lastSeen: this.watermark
4341
+ }
4342
+ },
4343
+ { upsert: true }
4344
+ );
4345
+ }
4346
+ async sync() {
4347
+ const doc = await this.collection.findOne({ userId: this.userId });
4348
+ this.watermark = doc?.watermark ?? null;
4349
+ this.dismissedIds = new Set(normalizeDismissedIds4(doc?.dismissedIds));
4350
+ }
4351
+ async dismissBatch(ids) {
4352
+ const unique = Array.from(new Set(ids));
4353
+ if (unique.length === 0) return;
4354
+ this.dismissedIds = /* @__PURE__ */ new Set([...this.dismissedIds, ...unique]);
4355
+ await this.collection.updateOne(
4356
+ { userId: this.userId },
4357
+ {
4358
+ $addToSet: { dismissedIds: { $each: unique } },
4359
+ $set: { lastSeen: (/* @__PURE__ */ new Date()).toISOString() }
4360
+ },
4361
+ { upsert: true }
4362
+ );
4363
+ }
4364
+ async resetUser(userId) {
4365
+ await this.collection.deleteOne({ userId });
4366
+ if (userId === this.userId) {
4367
+ this.watermark = null;
4368
+ this.dismissedIds.clear();
4369
+ }
4370
+ }
4371
+ async getBulkState(userIds) {
4372
+ const out = /* @__PURE__ */ new Map();
4373
+ if (userIds.length === 0) return out;
4374
+ if (this.collection.find) {
4375
+ const rows = await this.collection.find({ userId: { $in: userIds } }).toArray();
4376
+ for (const row of rows) {
4377
+ out.set(row.userId, {
4378
+ watermark: row.watermark ?? null,
4379
+ dismissedIds: normalizeDismissedIds4(row.dismissedIds),
4380
+ lastSeen: normalizeLastSeen3(row.lastSeen),
4381
+ deviceCount: 1
4382
+ });
4383
+ }
4384
+ return out;
4385
+ }
4386
+ await Promise.all(
4387
+ userIds.map(async (userId) => {
4388
+ const row = await this.collection.findOne({ userId });
4389
+ if (!row) return;
4390
+ out.set(userId, {
4391
+ watermark: row.watermark ?? null,
4392
+ dismissedIds: normalizeDismissedIds4(row.dismissedIds),
4393
+ lastSeen: normalizeLastSeen3(row.lastSeen),
4394
+ deviceCount: 1
4395
+ });
4396
+ })
4397
+ );
4398
+ return out;
4399
+ }
4400
+ async isHealthy() {
4401
+ try {
4402
+ await this.collection.findOne({});
4403
+ return true;
4404
+ } catch {
4405
+ return false;
4406
+ }
4407
+ }
4408
+ async destroy() {
4409
+ }
4410
+ };
4411
+
4412
+ // src/adapters/sqlite.ts
4413
+ function parseDismissedIds2(value) {
4414
+ if (Array.isArray(value)) {
4415
+ return value.filter((item) => typeof item === "string");
4416
+ }
4417
+ if (typeof value === "string" && value.trim()) {
4418
+ try {
4419
+ const parsed = JSON.parse(value);
4420
+ if (Array.isArray(parsed)) {
4421
+ return parsed.filter((item) => typeof item === "string");
4422
+ }
4423
+ } catch {
4424
+ return [];
4425
+ }
4426
+ }
4427
+ return [];
4428
+ }
4429
+ function normalizeDismissedIds5(row) {
4430
+ if (!row) return [];
4431
+ return parseDismissedIds2(row.dismissed_ids ?? row.dismissedIds);
4432
+ }
4433
+ function normalizeWatermark3(row) {
4434
+ if (!row) return null;
4435
+ return row.watermark ?? null;
4436
+ }
4437
+ function normalizeLastSeen4(row) {
4438
+ if (!row) return (/* @__PURE__ */ new Date(0)).toISOString();
4439
+ return row.last_seen ?? row.lastSeen ?? (/* @__PURE__ */ new Date(0)).toISOString();
4440
+ }
4441
+ var SQLiteAdapter = class {
4442
+ userId;
4443
+ query;
4444
+ tableName;
4445
+ autoMigrate;
4446
+ watermark = null;
4447
+ dismissedIds = /* @__PURE__ */ new Set();
4448
+ initialized = false;
4449
+ constructor(options) {
4450
+ if (!options.userId) {
4451
+ throw new Error("SQLiteAdapter: userId is required");
4452
+ }
4453
+ this.userId = options.userId;
4454
+ this.query = options.query;
4455
+ this.tableName = options.tableName ?? "featuredrop_state";
4456
+ this.autoMigrate = options.autoMigrate ?? true;
4457
+ }
4458
+ getWatermark() {
4459
+ return this.watermark;
4460
+ }
4461
+ getDismissedIds() {
4462
+ return this.dismissedIds;
4463
+ }
4464
+ dismiss(id) {
4465
+ this.dismissedIds.add(id);
4466
+ void this.dismissBatch([id]);
4467
+ }
4468
+ async dismissAll(now) {
4469
+ this.watermark = now.toISOString();
4470
+ this.dismissedIds.clear();
4471
+ await this.ensureReady();
4472
+ await this.query(
4473
+ `INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
4474
+ VALUES (?, ?, ?, ?, ?, ?)
4475
+ ON CONFLICT(user_id)
4476
+ DO UPDATE SET watermark = excluded.watermark, dismissed_ids = excluded.dismissed_ids, last_seen = excluded.last_seen, updated_at = excluded.updated_at`,
4477
+ [this.userId, this.watermark, JSON.stringify([]), this.watermark, this.watermark, this.watermark]
4478
+ );
4479
+ }
4480
+ async sync() {
4481
+ await this.ensureReady();
4482
+ const result = await this.query(
4483
+ `SELECT watermark, dismissed_ids, last_seen FROM ${this.tableName} WHERE user_id = ? LIMIT 1`,
4484
+ [this.userId]
4485
+ );
4486
+ const row = result.rows[0];
4487
+ this.watermark = normalizeWatermark3(row);
4488
+ this.dismissedIds = new Set(normalizeDismissedIds5(row));
4489
+ }
4490
+ async dismissBatch(ids) {
4491
+ const uniqueIds = Array.from(new Set(ids));
4492
+ if (uniqueIds.length === 0) return;
4493
+ await this.ensureReady();
4494
+ const merged = /* @__PURE__ */ new Set([
4495
+ ...Array.from(this.dismissedIds),
4496
+ ...uniqueIds
4497
+ ]);
4498
+ const mergedArray = Array.from(merged);
4499
+ this.dismissedIds = merged;
4500
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4501
+ await this.query(
4502
+ `INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
4503
+ VALUES (?, ?, ?, ?, ?, ?)
4504
+ ON CONFLICT(user_id)
4505
+ DO UPDATE SET dismissed_ids = excluded.dismissed_ids, last_seen = excluded.last_seen, updated_at = excluded.updated_at`,
4506
+ [this.userId, this.watermark, JSON.stringify(mergedArray), nowIso, nowIso, nowIso]
4507
+ );
4508
+ }
4509
+ async resetUser(userId) {
4510
+ await this.ensureReady();
4511
+ await this.query(`DELETE FROM ${this.tableName} WHERE user_id = ?`, [userId]);
4512
+ if (userId === this.userId) {
4513
+ this.watermark = null;
4514
+ this.dismissedIds.clear();
4515
+ }
4516
+ }
4517
+ async getBulkState(userIds) {
4518
+ await this.ensureReady();
4519
+ if (userIds.length === 0) return /* @__PURE__ */ new Map();
4520
+ const placeholders = userIds.map(() => "?").join(", ");
4521
+ const result = await this.query(
4522
+ `SELECT user_id, watermark, dismissed_ids, last_seen
4523
+ FROM ${this.tableName}
4524
+ WHERE user_id IN (${placeholders})`,
4525
+ userIds
4526
+ );
4527
+ const out = /* @__PURE__ */ new Map();
4528
+ for (const row of result.rows) {
4529
+ out.set(row.user_id, {
4530
+ watermark: normalizeWatermark3(row),
4531
+ dismissedIds: normalizeDismissedIds5(row),
4532
+ lastSeen: normalizeLastSeen4(row),
4533
+ deviceCount: 1
4534
+ });
4535
+ }
4536
+ return out;
4537
+ }
4538
+ async isHealthy() {
4539
+ try {
4540
+ await this.query("SELECT 1");
4541
+ return true;
4542
+ } catch {
4543
+ return false;
4544
+ }
4545
+ }
4546
+ async destroy() {
4547
+ }
4548
+ async ensureReady() {
4549
+ if (this.initialized) return;
4550
+ if (this.autoMigrate) {
4551
+ await this.query(
4552
+ `CREATE TABLE IF NOT EXISTS ${this.tableName} (
4553
+ user_id TEXT PRIMARY KEY,
4554
+ watermark TEXT,
4555
+ dismissed_ids TEXT NOT NULL,
4556
+ last_seen TEXT NOT NULL,
4557
+ created_at TEXT NOT NULL,
4558
+ updated_at TEXT NOT NULL
4559
+ )`
4560
+ );
4561
+ await this.query(
4562
+ `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_seen ON ${this.tableName}(last_seen)`
4563
+ );
4564
+ }
4565
+ this.initialized = true;
4566
+ }
4567
+ };
4568
+
4569
+ // src/adapters/supabase.ts
4570
+ function normalizeDismissedIds6(row) {
4571
+ if (!row || !Array.isArray(row.dismissed_ids)) return [];
4572
+ return row.dismissed_ids.filter((id) => typeof id === "string");
4573
+ }
4574
+ function normalizeWatermark4(row) {
4575
+ if (!row) return null;
4576
+ return row.watermark ?? null;
4577
+ }
4578
+ function normalizeLastSeen5(row) {
4579
+ if (!row) return (/* @__PURE__ */ new Date(0)).toISOString();
4580
+ return row.last_seen ?? (/* @__PURE__ */ new Date(0)).toISOString();
4581
+ }
4582
+ function throwOnError(error) {
4583
+ if (!error) return;
4584
+ throw new Error(`SupabaseAdapter: ${error.message ?? "unknown error"}`);
4585
+ }
4586
+ var SupabaseAdapter = class {
4587
+ userId;
4588
+ client;
4589
+ tableName;
4590
+ realtime;
4591
+ watermark = null;
4592
+ dismissedIds = /* @__PURE__ */ new Set();
4593
+ realtimeChannel = null;
4594
+ syncing = false;
4595
+ constructor(options) {
4596
+ if (!options.userId) {
4597
+ throw new Error("SupabaseAdapter: userId is required");
4598
+ }
4599
+ this.userId = options.userId;
4600
+ this.client = options.client;
4601
+ this.tableName = options.tableName ?? "featuredrop_state";
4602
+ this.realtime = options.realtime ?? false;
4603
+ if (this.realtime && this.client.channel) {
4604
+ this.setupRealtime();
4605
+ }
4606
+ }
4607
+ getWatermark() {
4608
+ return this.watermark;
4609
+ }
4610
+ getDismissedIds() {
4611
+ return this.dismissedIds;
4612
+ }
4613
+ dismiss(id) {
4614
+ this.dismissedIds.add(id);
4615
+ void this.dismissBatch([id]);
4616
+ }
4617
+ async dismissAll(now) {
4618
+ this.watermark = now.toISOString();
4619
+ this.dismissedIds.clear();
4620
+ await this.upsertState({
4621
+ watermark: this.watermark,
4622
+ dismissed_ids: [],
4623
+ last_seen: this.watermark
4624
+ });
4625
+ }
4626
+ async sync() {
4627
+ if (this.syncing) return;
4628
+ this.syncing = true;
4629
+ try {
4630
+ const row = await this.fetchState(this.userId);
4631
+ this.watermark = normalizeWatermark4(row);
4632
+ this.dismissedIds = new Set(normalizeDismissedIds6(row));
4633
+ } finally {
4634
+ this.syncing = false;
4635
+ }
4636
+ }
4637
+ async dismissBatch(ids) {
4638
+ const uniqueIds = Array.from(new Set(ids));
4639
+ if (uniqueIds.length === 0) return;
4640
+ const merged = /* @__PURE__ */ new Set([
4641
+ ...Array.from(this.dismissedIds),
4642
+ ...uniqueIds
4643
+ ]);
4644
+ this.dismissedIds = merged;
4645
+ await this.upsertState({
4646
+ watermark: this.watermark,
4647
+ dismissed_ids: Array.from(merged),
4648
+ last_seen: (/* @__PURE__ */ new Date()).toISOString()
4649
+ });
4650
+ }
4651
+ async resetUser(userId) {
4652
+ const result = await this.client.from(this.tableName).delete().eq("user_id", userId);
4653
+ throwOnError(result.error);
4654
+ if (userId === this.userId) {
4655
+ this.watermark = null;
4656
+ this.dismissedIds.clear();
4657
+ }
4658
+ }
4659
+ async getBulkState(userIds) {
4660
+ const out = /* @__PURE__ */ new Map();
4661
+ if (userIds.length === 0) return out;
4662
+ await Promise.all(
4663
+ userIds.map(async (userId) => {
4664
+ const row = await this.fetchState(userId);
4665
+ if (!row) return;
4666
+ out.set(userId, {
4667
+ watermark: normalizeWatermark4(row),
4668
+ dismissedIds: normalizeDismissedIds6(row),
4669
+ lastSeen: normalizeLastSeen5(row),
4670
+ deviceCount: 1
4671
+ });
4672
+ })
4673
+ );
4674
+ return out;
4675
+ }
4676
+ async isHealthy() {
4677
+ try {
4678
+ await this.client.from(this.tableName).select("user_id").eq("user_id", this.userId).maybeSingle();
4679
+ return true;
4680
+ } catch {
4681
+ return false;
4682
+ }
4683
+ }
4684
+ async destroy() {
4685
+ if (this.realtimeChannel && this.client.removeChannel) {
4686
+ await this.client.removeChannel(this.realtimeChannel);
4687
+ }
4688
+ this.realtimeChannel = null;
4689
+ }
4690
+ async fetchState(userId) {
4691
+ const result = await this.client.from(this.tableName).select("user_id, watermark, dismissed_ids, last_seen").eq("user_id", userId).maybeSingle();
4692
+ throwOnError(result.error);
4693
+ return result.data;
4694
+ }
4695
+ async upsertState(state) {
4696
+ const payload = {
4697
+ user_id: this.userId,
4698
+ watermark: state.watermark,
4699
+ dismissed_ids: state.dismissed_ids,
4700
+ last_seen: state.last_seen
4701
+ };
4702
+ const result = await this.client.from(this.tableName).upsert(payload);
4703
+ throwOnError(result.error);
4704
+ }
4705
+ setupRealtime() {
4706
+ const channelFactory = this.client.channel;
4707
+ if (!channelFactory) return;
4708
+ this.realtimeChannel = channelFactory(`featuredrop:${this.tableName}:${this.userId}`).on(
4709
+ "postgres_changes",
4710
+ {
4711
+ event: "*",
4712
+ schema: "public",
4713
+ table: this.tableName,
4714
+ filter: `user_id=eq.${this.userId}`
4715
+ },
4716
+ () => {
4717
+ void this.sync();
4718
+ }
4719
+ ).subscribe();
131
4720
  }
132
4721
  };
133
4722
 
134
- export { LocalStorageAdapter, MemoryAdapter, createManifest, getFeatureById, getNewFeatureCount, getNewFeatures, getNewFeaturesByCategory, getNewFeaturesSorted, hasNewFeature, isNew };
4723
+ export { AmplitudeAdapter, AnalyticsCollector, AudienceBuilder, ContentfulAdapter, CustomAdapter, DiscordBridge, EmailDigestGenerator, FEATUREDROP_ANIMATION_PRESETS, FEATUREDROP_THEMES, FEATUREDROP_TRANSLATIONS, HybridAdapter, IndexedDBAdapter, LaunchDarklyBridge, LocalStorageAdapter, ManifestEditor, MarkdownAdapter, MemoryAdapter, MixpanelAdapter, MongoAdapter, MySQLAdapter, NotionAdapter, PostHogAdapter, PostHogBridge, PostgresAdapter, PreviewPanel, RSSFeedGenerator, RedisAdapter, RemoteAdapter, SQLiteAdapter, SanityAdapter, ScheduleCalendar, SegmentAdapter, SlackBridge, StrapiAdapter, SupabaseAdapter, TriggerEngine, WebhookBridge, applyAnnouncementThrottle, applyFeatureVariant, applyFeatureVariants, computeManifestStats, createAdoptionMetrics, createChangelogRenderer, createFlagBridge, createManifest, createTheme, diffManifest, featureEntryJsonSchema, featureEntrySchema, featureManifestJsonSchema, featureManifestSchema, formatDateForLocale, formatRelativeTimeForLocale, generateChangelogDiff, generateMarkdownChangelog, generateRSS, getAnimationDurationMs, getEnterAnimation, getExitAnimation, getFeatureById, getFeatureVariantName, getLocaleDirection, getNewFeatureCount, getNewFeatures, getNewFeaturesByCategory, getNewFeaturesSorted, getOrCreateVariantKey, getPulseAnimation, hasDependencyCycle, hasNewFeature, isNew, isTriggerMatch, matchesAudience, parseDescription, resolveAnimationPreset, resolveDependencyOrder, resolveLocale, resolveTheme, resolveTranslations, runDoctor, sortFeaturesByDependencies, themeToCSSVariables, validateManifest, validateManifestForCI };
135
4724
  //# sourceMappingURL=index.js.map
136
4725
  //# sourceMappingURL=index.js.map