featuredrop 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +171 -0
  2. package/dist/admin.cjs +212 -0
  3. package/dist/admin.cjs.map +1 -0
  4. package/dist/admin.d.cts +176 -0
  5. package/dist/admin.d.ts +176 -0
  6. package/dist/admin.js +207 -0
  7. package/dist/admin.js.map +1 -0
  8. package/dist/angular.cjs +13 -3
  9. package/dist/angular.cjs.map +1 -1
  10. package/dist/angular.d.cts +4 -0
  11. package/dist/angular.d.ts +4 -0
  12. package/dist/angular.js +13 -3
  13. package/dist/angular.js.map +1 -1
  14. package/dist/bridges.cjs +422 -0
  15. package/dist/bridges.cjs.map +1 -0
  16. package/dist/bridges.d.cts +194 -0
  17. package/dist/bridges.d.ts +194 -0
  18. package/dist/bridges.js +395 -0
  19. package/dist/bridges.js.map +1 -0
  20. package/dist/ci.cjs +328 -0
  21. package/dist/ci.cjs.map +1 -0
  22. package/dist/ci.d.cts +176 -0
  23. package/dist/ci.d.ts +176 -0
  24. package/dist/ci.js +324 -0
  25. package/dist/ci.js.map +1 -0
  26. package/dist/featuredrop.cjs +162 -20
  27. package/dist/featuredrop.cjs.map +1 -1
  28. package/dist/flags.cjs +51 -0
  29. package/dist/flags.cjs.map +1 -0
  30. package/dist/flags.d.cts +48 -0
  31. package/dist/flags.d.ts +48 -0
  32. package/dist/flags.js +47 -0
  33. package/dist/flags.js.map +1 -0
  34. package/dist/index.cjs +2599 -660
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +743 -206
  37. package/dist/index.d.ts +743 -206
  38. package/dist/index.js +2555 -668
  39. package/dist/index.js.map +1 -1
  40. package/dist/preact.cjs +747 -221
  41. package/dist/preact.cjs.map +1 -1
  42. package/dist/preact.d.cts +67 -120
  43. package/dist/preact.d.ts +67 -120
  44. package/dist/preact.js +713 -207
  45. package/dist/preact.js.map +1 -1
  46. package/dist/react.cjs +747 -221
  47. package/dist/react.cjs.map +1 -1
  48. package/dist/react.d.cts +67 -120
  49. package/dist/react.d.ts +67 -120
  50. package/dist/react.js +713 -207
  51. package/dist/react.js.map +1 -1
  52. package/dist/schema.cjs +78 -1
  53. package/dist/schema.cjs.map +1 -1
  54. package/dist/schema.d.cts +142 -0
  55. package/dist/schema.d.ts +142 -0
  56. package/dist/schema.js +78 -1
  57. package/dist/schema.js.map +1 -1
  58. package/dist/solid.cjs +13 -3
  59. package/dist/solid.cjs.map +1 -1
  60. package/dist/solid.d.cts +4 -0
  61. package/dist/solid.d.ts +4 -0
  62. package/dist/solid.js +13 -3
  63. package/dist/solid.js.map +1 -1
  64. package/dist/svelte.cjs +13 -3
  65. package/dist/svelte.cjs.map +1 -1
  66. package/dist/svelte.js +13 -3
  67. package/dist/svelte.js.map +1 -1
  68. package/dist/testing.cjs +136 -15
  69. package/dist/testing.cjs.map +1 -1
  70. package/dist/testing.d.cts +22 -0
  71. package/dist/testing.d.ts +22 -0
  72. package/dist/testing.js +136 -15
  73. package/dist/testing.js.map +1 -1
  74. package/dist/vue.cjs +36 -5
  75. package/dist/vue.cjs.map +1 -1
  76. package/dist/vue.js +16 -5
  77. package/dist/vue.js.map +1 -1
  78. package/dist/web-components.cjs +14 -4
  79. package/dist/web-components.cjs.map +1 -1
  80. package/dist/web-components.d.cts +4 -0
  81. package/dist/web-components.d.ts +4 -0
  82. package/dist/web-components.js +14 -4
  83. package/dist/web-components.js.map +1 -1
  84. package/package.json +59 -1
package/README.md CHANGED
@@ -109,6 +109,10 @@ const storage = new LocalStorageAdapter({
109
109
  })
110
110
  ```
111
111
 
112
+ For offline-heavy apps, use `IndexedDBAdapter` to persist the same state with IndexedDB + local fallback.
113
+ It also supports queued dismiss sync (`flushQueue`) + remote state reconciliation (`syncFromRemote`) for offline-first flows.
114
+ For remote/state sync, `RemoteAdapter` now includes retry + circuit-breaker protection and a monitoring-friendly `isHealthy()` check.
115
+
112
116
  **3. Check what's new:**
113
117
 
114
118
  ```ts
@@ -218,6 +222,40 @@ Use built-in locale packs or supply partial overrides:
218
222
  ```
219
223
 
220
224
  Built-in locales: `en`, `es`, `fr`, `de`, `pt`, `zh-cn`, `ja`, `ko`, `ar`, `hi`.
225
+ Locale helpers include locale-aware date formatting, pluralized count copy, and RTL direction support for Arabic.
226
+ Use `formatRelativeTimeForLocale()` for localized "x days ago" labels, or set `dateFormat="relative"` on `ChangelogWidget` / `ChangelogPage`.
227
+
228
+ ## Animation Presets
229
+
230
+ Choose built-in motion levels globally:
231
+
232
+ ```tsx
233
+ <FeatureDropProvider manifest={FEATURES} storage={storage} animation="subtle">
234
+ <App />
235
+ </FeatureDropProvider>
236
+ ```
237
+
238
+ Available presets: `none`, `subtle`, `normal`, `playful`.
239
+ Reduced-motion users automatically receive `none`.
240
+ Built-in `ChangelogWidget`, `AnnouncementModal`, and `Toast` include enter/exit animations, and `Tour`, `Survey`, `FeedbackWidget`, `Spotlight`, `SpotlightChain`, and `Hotspot` follow the same preset for enter/pulse motion.
241
+
242
+ ## Custom Renderer Protocol
243
+
244
+ Need full UI control? Use a UI-agnostic renderer with state/actions/computed helpers:
245
+
246
+ ```ts
247
+ import { createChangelogRenderer } from 'featuredrop'
248
+
249
+ const renderer = createChangelogRenderer({ manifest: FEATURES, storage })
250
+ renderer.subscribe((state) => console.log(state.newCount))
251
+ renderer.actions.dismiss('ai-journal')
252
+ ```
253
+
254
+ ## Accessibility
255
+
256
+ featuredrop components include keyboard navigation, focus trapping/return for dialogs, ARIA live regions, and reduced-motion support.
257
+
258
+ Automated accessibility regression checks run in CI via `axe-core` + `vitest-axe` (`src/__tests__/accessibility-axe.test.tsx`).
221
259
 
222
260
  ## Changelog-as-Code
223
261
 
@@ -233,6 +271,10 @@ npx featuredrop doctor --pattern "features/**/*.md"
233
271
  npx featuredrop generate-rss --pattern "features/**/*.md" --out featuredrop.rss.xml
234
272
  npx featuredrop generate-changelog --pattern "features/**/*.md" --out CHANGELOG.generated.md
235
273
  npx featuredrop migrate --from beamer --input beamer-export.json --out featuredrop.manifest.json
274
+ npx featuredrop migrate --from headway --input headway-export.json --out featuredrop.manifest.json
275
+ npx featuredrop migrate --from announcekit --input announcekit-export.json --out featuredrop.manifest.json
276
+ npx featuredrop migrate --from canny --input canny-export.json --out featuredrop.manifest.json
277
+ npx featuredrop migrate --from launchnotes --input launchnotes-export.json --out featuredrop.manifest.json
236
278
  ```
237
279
 
238
280
  Example feature file:
@@ -252,6 +294,114 @@ cta:
252
294
  Track decisions and outcomes with AI-powered insights.
253
295
  ```
254
296
 
297
+ ## CMS Adapters
298
+
299
+ Pull releases from your CMS and map them into `FeatureEntry[]`:
300
+
301
+ ```ts
302
+ import { ContentfulAdapter, SanityAdapter, StrapiAdapter, NotionAdapter, MarkdownAdapter } from 'featuredrop'
303
+
304
+ const contentful = new ContentfulAdapter({
305
+ spaceId: process.env.CONTENTFUL_SPACE!,
306
+ accessToken: process.env.CONTENTFUL_TOKEN!,
307
+ contentType: 'featureRelease',
308
+ })
309
+
310
+ const entries = await contentful.load()
311
+ ```
312
+
313
+ Each adapter accepts optional `fieldMapping` overrides so you can map your content model fields without reshaping server responses.
314
+
315
+ ## Notification Bridges
316
+
317
+ Use framework-agnostic bridge helpers to fan out release notifications:
318
+
319
+ ```ts
320
+ import {
321
+ SlackBridge,
322
+ DiscordBridge,
323
+ EmailDigestGenerator,
324
+ WebhookBridge,
325
+ RSSFeedGenerator,
326
+ } from 'featuredrop/bridges'
327
+
328
+ await SlackBridge.notify(feature, { webhookUrl: process.env.SLACK_WEBHOOK! })
329
+ await DiscordBridge.notify(feature, { webhookUrl: process.env.DISCORD_WEBHOOK! })
330
+ await WebhookBridge.post(feature, { url: 'https://api.example.com/hooks/features' })
331
+
332
+ const html = EmailDigestGenerator.generate(features, { title: 'Weekly updates' })
333
+ const rss = RSSFeedGenerator.generate(features, { title: 'Product Updates' })
334
+ ```
335
+
336
+ ## CI Utilities
337
+
338
+ Use `featuredrop/ci` for manifest diffing in pull-request checks:
339
+
340
+ ```ts
341
+ import { diffManifest, generateChangelogDiff, validateManifestForCI } from 'featuredrop/ci'
342
+
343
+ const diff = diffManifest(beforeManifest, afterManifest)
344
+ const summary = generateChangelogDiff(diff, { includeFieldChanges: true })
345
+ const validation = validateManifestForCI(afterManifest)
346
+ ```
347
+
348
+ Run bundle budget checks after `pnpm build`:
349
+
350
+ ```bash
351
+ pnpm size-check
352
+ ```
353
+
354
+ ## Feature Flag Bridges
355
+
356
+ Gate announcement visibility behind flags via `feature.flagKey`:
357
+
358
+ ```ts
359
+ import { createFlagBridge, LaunchDarklyBridge, PostHogBridge } from 'featuredrop/flags'
360
+
361
+ const bridge = createFlagBridge({
362
+ isEnabled: (flagKey) => myFlagService.isOn(flagKey),
363
+ })
364
+
365
+ // Provider + feature entry
366
+ <FeatureDropProvider manifest={FEATURES} storage={storage} flagBridge={bridge} />
367
+ // { id: 'ai-journal', flagKey: 'ai-journal-enabled', ... }
368
+
369
+ // Or vendor bridges
370
+ const ld = new LaunchDarklyBridge(ldClient)
371
+ const ph = new PostHogBridge(posthog)
372
+ ```
373
+
374
+ ## Multi-Product Support
375
+
376
+ Scope visibility by product using `FeatureDropProvider` + `feature.product`:
377
+
378
+ ```tsx
379
+ <FeatureDropProvider manifest={FEATURES} storage={storage} product="askverdict" />
380
+ // feature.product can be "askverdict", "other-product", or "*" for shared entries
381
+ ```
382
+
383
+ ## Admin Components
384
+
385
+ Use optional admin UI primitives via `featuredrop/admin`:
386
+
387
+ ```tsx
388
+ import {
389
+ ManifestEditor,
390
+ ScheduleCalendar,
391
+ PreviewPanel,
392
+ AudienceBuilder,
393
+ } from 'featuredrop/admin'
394
+
395
+ <ManifestEditor features={features} onSave={saveManifest} />
396
+ <ScheduleCalendar features={features} onSchedule={scheduleFeature} />
397
+ <PreviewPanel feature={selectedFeature} components={['badge', 'changelog', 'toast']} />
398
+ <AudienceBuilder segments={['free', 'pro']} roles={['admin', 'viewer']} onSave={saveAudience} />
399
+ ```
400
+
401
+ ## Resilience
402
+
403
+ Built-in React exports are wrapped in internal error boundaries. If a featuredrop component throws, it unmounts itself (no host app crash) and optionally reports through provider `onError`.
404
+
255
405
  ## Schema Validation
256
406
 
257
407
  Validate manifest JSON in CI or tooling pipelines.
@@ -315,6 +465,18 @@ One-click editable demos:
315
465
  - StackBlitz: https://stackblitz.com/github/GLINCKER/featuredrop/tree/main/examples/sandbox-react
316
466
  - CodeSandbox: https://codesandbox.io/p/sandbox/github/GLINCKER/featuredrop/tree/main/examples/sandbox-react
317
467
 
468
+ ## Docs Site (Nextra + Vercel)
469
+
470
+ The repo now includes a docs app scaffold at `apps/docs` for a fast validation launch on Vercel.
471
+
472
+ ```bash
473
+ pnpm docs:install
474
+ pnpm docs:dev
475
+ pnpm docs:build
476
+ ```
477
+
478
+ The docs app is static-export ready, so it can also deploy on Cloudflare Pages or GitHub Pages.
479
+
318
480
  ## Components
319
481
 
320
482
  Everything you'd expect from Beamer or Headway — but free, self-hosted, and headless-first.
@@ -710,7 +872,10 @@ Read the full [Architecture doc](docs/ARCHITECTURE.md) for cross-device sync flo
710
872
 
711
873
  ## Documentation
712
874
 
875
+ - [Docs App Source](apps/docs/) — Next.js + Nextra site scaffold (`/`, `/docs`, `/playground`)
876
+ - [Docs Site Guide](docs/DOCS_SITE.md) — Local commands and Vercel deployment setup
713
877
  - [API Reference](docs/API.md) — All functions, adapters, hooks, components
878
+ - [Recipes](docs/RECIPES.md) — Copy-paste integration patterns (sidebar badge, tours, migration, A/B tests)
714
879
  - [Architecture](docs/ARCHITECTURE.md) — Three-check algorithm, cross-device sync, custom adapters
715
880
  - [Next.js Example](examples/nextjs/) — Full App Router integration
716
881
  - [Vanilla Example](examples/vanilla/) — Plain HTML, zero build step
@@ -720,6 +885,12 @@ Read the full [Architecture doc](docs/ARCHITECTURE.md) for cross-device sync flo
720
885
 
721
886
  See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, commit conventions, and how releases work.
722
887
 
888
+ ## Security
889
+
890
+ - Report vulnerabilities privately via [SECURITY.md](SECURITY.md).
891
+ - CI includes CodeQL static analysis on pull requests and `main`.
892
+ - `pnpm security-check` scans runtime source for unsafe execution/rendering patterns.
893
+
723
894
  ## License
724
895
 
725
896
  MIT &copy; [Glincker](https://glincker.com)
package/dist/admin.cjs ADDED
@@ -0,0 +1,212 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/admin/index.tsx
8
+ var panelStyles = {
9
+ border: "1px solid #e5e7eb",
10
+ borderRadius: "10px",
11
+ padding: "12px",
12
+ background: "#ffffff"
13
+ };
14
+ var headingStyles = {
15
+ margin: "0 0 8px",
16
+ fontSize: "15px",
17
+ fontWeight: 700
18
+ };
19
+ function ManifestEditor({
20
+ features,
21
+ onSave,
22
+ readOnly = false,
23
+ children
24
+ }) {
25
+ const [draft, setDraft] = react.useState(() => JSON.stringify(features, null, 2));
26
+ const [status, setStatus] = react.useState("idle");
27
+ const [error, setError] = react.useState("");
28
+ const parsed = react.useMemo(() => {
29
+ try {
30
+ const next = JSON.parse(draft);
31
+ if (!Array.isArray(next)) throw new Error("Manifest must be an array");
32
+ return next;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }, [draft]);
37
+ const save = async () => {
38
+ if (readOnly || !parsed) return;
39
+ setStatus("saving");
40
+ setError("");
41
+ try {
42
+ await onSave(parsed);
43
+ setStatus("saved");
44
+ } catch (cause) {
45
+ setStatus("error");
46
+ setError(cause instanceof Error ? cause.message : "Failed to save manifest");
47
+ }
48
+ };
49
+ if (children) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
50
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "data-featuredrop-admin-manifest-editor": true, style: panelStyles, children: [
51
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: headingStyles, children: "Manifest Editor" }),
52
+ /* @__PURE__ */ jsxRuntime.jsx(
53
+ "textarea",
54
+ {
55
+ "aria-label": "Manifest JSON",
56
+ value: draft,
57
+ onChange: (event) => setDraft(event.target.value),
58
+ readOnly,
59
+ style: {
60
+ width: "100%",
61
+ minHeight: "180px",
62
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
63
+ fontSize: "12px",
64
+ lineHeight: 1.45,
65
+ border: "1px solid #d1d5db",
66
+ borderRadius: "8px",
67
+ padding: "10px"
68
+ }
69
+ }
70
+ ),
71
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px", marginTop: "8px" }, children: [
72
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: save, disabled: readOnly || !parsed, children: "Save" }),
73
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-live": "polite", children: status }),
74
+ !parsed && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#dc2626" }, children: "Invalid JSON" }),
75
+ error && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#dc2626" }, children: error })
76
+ ] })
77
+ ] });
78
+ }
79
+ function ScheduleCalendar({ features, onSchedule }) {
80
+ const [values, setValues] = react.useState({});
81
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "data-featuredrop-admin-schedule-calendar": true, style: panelStyles, children: [
82
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: headingStyles, children: "Schedule Calendar" }),
83
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { style: { margin: 0, padding: 0, listStyle: "none", display: "grid", gap: "10px" }, children: features.map((feature) => /* @__PURE__ */ jsxRuntime.jsxs(
84
+ "li",
85
+ {
86
+ style: {
87
+ border: "1px solid #e5e7eb",
88
+ borderRadius: "8px",
89
+ padding: "10px",
90
+ display: "grid",
91
+ gap: "6px"
92
+ },
93
+ children: [
94
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: feature.label }),
95
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: { display: "grid", gap: "4px" }, children: [
96
+ "Publish at",
97
+ /* @__PURE__ */ jsxRuntime.jsx(
98
+ "input",
99
+ {
100
+ type: "datetime-local",
101
+ value: values[feature.id] ?? "",
102
+ onChange: (event) => {
103
+ const value = event.target.value;
104
+ setValues((previous) => ({ ...previous, [feature.id]: value }));
105
+ }
106
+ }
107
+ )
108
+ ] }),
109
+ /* @__PURE__ */ jsxRuntime.jsx(
110
+ "button",
111
+ {
112
+ type: "button",
113
+ onClick: () => {
114
+ const value = values[feature.id];
115
+ if (!value) return;
116
+ void onSchedule(feature.id, new Date(value).toISOString());
117
+ },
118
+ children: "Schedule"
119
+ }
120
+ )
121
+ ]
122
+ },
123
+ feature.id
124
+ )) })
125
+ ] });
126
+ }
127
+ function PreviewPanel({ feature, components = ["badge", "changelog"] }) {
128
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "data-featuredrop-admin-preview-panel": true, style: panelStyles, children: [
129
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: headingStyles, children: "Preview Panel" }),
130
+ !feature ? /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0, color: "#6b7280" }, children: "Select a feature to preview." }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
131
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 6px", fontWeight: 600 }, children: feature.label }),
132
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 8px", color: "#6b7280" }, children: feature.description ?? "No description" }),
133
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: components.map((component) => /* @__PURE__ */ jsxRuntime.jsx(
134
+ "span",
135
+ {
136
+ style: {
137
+ border: "1px solid #d1d5db",
138
+ borderRadius: "999px",
139
+ padding: "2px 8px",
140
+ fontSize: "12px"
141
+ },
142
+ children: component
143
+ },
144
+ component
145
+ )) })
146
+ ] })
147
+ ] });
148
+ }
149
+ function toggle(list, value) {
150
+ const items = new Set(list ?? []);
151
+ if (items.has(value)) items.delete(value);
152
+ else items.add(value);
153
+ return Array.from(items);
154
+ }
155
+ function AudienceBuilder({
156
+ segments = [],
157
+ roles = [],
158
+ regions = [],
159
+ value,
160
+ onChange,
161
+ onSave
162
+ }) {
163
+ const [audience, setAudience] = react.useState({
164
+ plan: value?.plan ?? [],
165
+ role: value?.role ?? [],
166
+ region: value?.region ?? []
167
+ });
168
+ const updateAudience = (next) => {
169
+ setAudience(next);
170
+ onChange?.(next);
171
+ };
172
+ const section = (title, values, selected, onToggle) => /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { style: { border: "none", margin: 0, padding: 0 }, children: [
173
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { style: { fontWeight: 600, marginBottom: "4px" }, children: title }),
174
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: values.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("label", { style: { display: "inline-flex", alignItems: "center", gap: "6px" }, children: [
175
+ /* @__PURE__ */ jsxRuntime.jsx(
176
+ "input",
177
+ {
178
+ type: "checkbox",
179
+ checked: Boolean(selected?.includes(item)),
180
+ onChange: () => onToggle(item)
181
+ }
182
+ ),
183
+ item
184
+ ] }, item)) })
185
+ ] });
186
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "data-featuredrop-admin-audience-builder": true, style: panelStyles, children: [
187
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: headingStyles, children: "Audience Builder" }),
188
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "grid", gap: "10px" }, children: [
189
+ section("Plans", segments, audience.plan, (item) => updateAudience({ ...audience, plan: toggle(audience.plan, item) })),
190
+ section("Roles", roles, audience.role, (item) => updateAudience({ ...audience, role: toggle(audience.role, item) })),
191
+ section("Regions", regions, audience.region, (item) => updateAudience({ ...audience, region: toggle(audience.region, item) }))
192
+ ] }),
193
+ onSave && /* @__PURE__ */ jsxRuntime.jsx(
194
+ "button",
195
+ {
196
+ type: "button",
197
+ style: { marginTop: "10px" },
198
+ onClick: () => {
199
+ void onSave(audience);
200
+ },
201
+ children: "Save audience"
202
+ }
203
+ )
204
+ ] });
205
+ }
206
+
207
+ exports.AudienceBuilder = AudienceBuilder;
208
+ exports.ManifestEditor = ManifestEditor;
209
+ exports.PreviewPanel = PreviewPanel;
210
+ exports.ScheduleCalendar = ScheduleCalendar;
211
+ //# sourceMappingURL=admin.cjs.map
212
+ //# sourceMappingURL=admin.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/admin/index.tsx"],"names":["useState","useMemo","jsx","Fragment","jsxs"],"mappings":"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","file":"admin.cjs","sourcesContent":["import { useMemo, useState, type CSSProperties, type ReactNode } from \"react\";\nimport type { AudienceRule, FeatureEntry } from \"../types\";\n\nconst panelStyles: CSSProperties = {\n border: \"1px solid #e5e7eb\",\n borderRadius: \"10px\",\n padding: \"12px\",\n background: \"#ffffff\",\n};\n\nconst headingStyles: CSSProperties = {\n margin: \"0 0 8px\",\n fontSize: \"15px\",\n fontWeight: 700,\n};\n\nexport interface ManifestEditorProps {\n features: readonly FeatureEntry[];\n onSave: (updated: FeatureEntry[]) => Promise<void> | void;\n readOnly?: boolean;\n children?: ReactNode;\n}\n\nexport function ManifestEditor({\n features,\n onSave,\n readOnly = false,\n children,\n}: ManifestEditorProps) {\n const [draft, setDraft] = useState(() => JSON.stringify(features, null, 2));\n const [status, setStatus] = useState<string>(\"idle\");\n const [error, setError] = useState<string>(\"\");\n\n const parsed = useMemo(() => {\n try {\n const next = JSON.parse(draft) as unknown;\n if (!Array.isArray(next)) throw new Error(\"Manifest must be an array\");\n return next as FeatureEntry[];\n } catch {\n return null;\n }\n }, [draft]);\n\n const save = async () => {\n if (readOnly || !parsed) return;\n setStatus(\"saving\");\n setError(\"\");\n try {\n await onSave(parsed);\n setStatus(\"saved\");\n } catch (cause) {\n setStatus(\"error\");\n setError(cause instanceof Error ? cause.message : \"Failed to save manifest\");\n }\n };\n\n if (children) return <>{children}</>;\n\n return (\n <section data-featuredrop-admin-manifest-editor style={panelStyles}>\n <p style={headingStyles}>Manifest Editor</p>\n <textarea\n aria-label=\"Manifest JSON\"\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n readOnly={readOnly}\n style={{\n width: \"100%\",\n minHeight: \"180px\",\n fontFamily: \"ui-monospace, SFMono-Regular, Menlo, monospace\",\n fontSize: \"12px\",\n lineHeight: 1.45,\n border: \"1px solid #d1d5db\",\n borderRadius: \"8px\",\n padding: \"10px\",\n }}\n />\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"8px\", marginTop: \"8px\" }}>\n <button type=\"button\" onClick={save} disabled={readOnly || !parsed}>\n Save\n </button>\n <span aria-live=\"polite\">{status}</span>\n {!parsed && <span style={{ color: \"#dc2626\" }}>Invalid JSON</span>}\n {error && <span style={{ color: \"#dc2626\" }}>{error}</span>}\n </div>\n </section>\n );\n}\n\nexport interface ScheduleCalendarProps {\n features: readonly FeatureEntry[];\n onSchedule: (featureId: string, publishAt: string) => Promise<void> | void;\n}\n\nexport function ScheduleCalendar({ features, onSchedule }: ScheduleCalendarProps) {\n const [values, setValues] = useState<Record<string, string>>({});\n\n return (\n <section data-featuredrop-admin-schedule-calendar style={panelStyles}>\n <p style={headingStyles}>Schedule Calendar</p>\n <ul style={{ margin: 0, padding: 0, listStyle: \"none\", display: \"grid\", gap: \"10px\" }}>\n {features.map((feature) => (\n <li\n key={feature.id}\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: \"8px\",\n padding: \"10px\",\n display: \"grid\",\n gap: \"6px\",\n }}\n >\n <strong>{feature.label}</strong>\n <label style={{ display: \"grid\", gap: \"4px\" }}>\n Publish at\n <input\n type=\"datetime-local\"\n value={values[feature.id] ?? \"\"}\n onChange={(event) => {\n const value = event.target.value;\n setValues((previous) => ({ ...previous, [feature.id]: value }));\n }}\n />\n </label>\n <button\n type=\"button\"\n onClick={() => {\n const value = values[feature.id];\n if (!value) return;\n void onSchedule(feature.id, new Date(value).toISOString());\n }}\n >\n Schedule\n </button>\n </li>\n ))}\n </ul>\n </section>\n );\n}\n\nexport interface PreviewPanelProps {\n feature?: FeatureEntry | null;\n components?: Array<\"badge\" | \"changelog\" | \"spotlight\" | \"banner\" | \"toast\">;\n}\n\nexport function PreviewPanel({ feature, components = [\"badge\", \"changelog\"] }: PreviewPanelProps) {\n return (\n <section data-featuredrop-admin-preview-panel style={panelStyles}>\n <p style={headingStyles}>Preview Panel</p>\n {!feature ? (\n <p style={{ margin: 0, color: \"#6b7280\" }}>Select a feature to preview.</p>\n ) : (\n <>\n <p style={{ margin: \"0 0 6px\", fontWeight: 600 }}>{feature.label}</p>\n <p style={{ margin: \"0 0 8px\", color: \"#6b7280\" }}>{feature.description ?? \"No description\"}</p>\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: \"6px\" }}>\n {components.map((component) => (\n <span\n key={component}\n style={{\n border: \"1px solid #d1d5db\",\n borderRadius: \"999px\",\n padding: \"2px 8px\",\n fontSize: \"12px\",\n }}\n >\n {component}\n </span>\n ))}\n </div>\n </>\n )}\n </section>\n );\n}\n\nexport interface AudienceBuilderProps {\n segments?: string[];\n roles?: string[];\n regions?: string[];\n value?: AudienceRule;\n onChange?: (audience: AudienceRule) => void;\n onSave?: (audience: AudienceRule) => Promise<void> | void;\n}\n\nfunction toggle(list: string[] | undefined, value: string): string[] {\n const items = new Set(list ?? []);\n if (items.has(value)) items.delete(value);\n else items.add(value);\n return Array.from(items);\n}\n\nexport function AudienceBuilder({\n segments = [],\n roles = [],\n regions = [],\n value,\n onChange,\n onSave,\n}: AudienceBuilderProps) {\n const [audience, setAudience] = useState<AudienceRule>({\n plan: value?.plan ?? [],\n role: value?.role ?? [],\n region: value?.region ?? [],\n });\n\n const updateAudience = (next: AudienceRule) => {\n setAudience(next);\n onChange?.(next);\n };\n\n const section = (\n title: string,\n values: string[],\n selected: string[] | undefined,\n onToggle: (value: string) => void,\n ) => (\n <fieldset style={{ border: \"none\", margin: 0, padding: 0 }}>\n <legend style={{ fontWeight: 600, marginBottom: \"4px\" }}>{title}</legend>\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: \"8px\" }}>\n {values.map((item) => (\n <label key={item} style={{ display: \"inline-flex\", alignItems: \"center\", gap: \"6px\" }}>\n <input\n type=\"checkbox\"\n checked={Boolean(selected?.includes(item))}\n onChange={() => onToggle(item)}\n />\n {item}\n </label>\n ))}\n </div>\n </fieldset>\n );\n\n return (\n <section data-featuredrop-admin-audience-builder style={panelStyles}>\n <p style={headingStyles}>Audience Builder</p>\n <div style={{ display: \"grid\", gap: \"10px\" }}>\n {section(\"Plans\", segments, audience.plan, (item) =>\n updateAudience({ ...audience, plan: toggle(audience.plan, item) }))}\n {section(\"Roles\", roles, audience.role, (item) =>\n updateAudience({ ...audience, role: toggle(audience.role, item) }))}\n {section(\"Regions\", regions, audience.region, (item) =>\n updateAudience({ ...audience, region: toggle(audience.region, item) }))}\n </div>\n {onSave && (\n <button\n type=\"button\"\n style={{ marginTop: \"10px\" }}\n onClick={() => {\n void onSave(audience);\n }}\n >\n Save audience\n </button>\n )}\n </section>\n );\n}\n"]}
@@ -0,0 +1,176 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ /** Entry type label — determines default icon/color in UI */
5
+ type FeatureType = "feature" | "improvement" | "fix" | "breaking";
6
+ /** Priority level for announcements */
7
+ type FeaturePriority = "critical" | "normal" | "low";
8
+ /** Call-to-action for a feature entry */
9
+ interface FeatureCTA {
10
+ /** Button/link label */
11
+ label: string;
12
+ /** URL to navigate to */
13
+ url: string;
14
+ }
15
+ /** Variant-level overrides for A/B announcement testing */
16
+ interface FeatureVariant {
17
+ /** Optional variant-specific label override */
18
+ label?: string;
19
+ /** Optional variant-specific description override */
20
+ description?: string;
21
+ /** Optional variant-specific image override */
22
+ image?: string;
23
+ /** Optional variant-specific CTA override */
24
+ cta?: FeatureCTA;
25
+ /** Optional variant-specific metadata overrides */
26
+ meta?: Record<string, unknown>;
27
+ }
28
+ /** Audience targeting rule — determines which user segments see a feature */
29
+ interface AudienceRule {
30
+ /** Plans that should see this feature (e.g. ["pro", "enterprise"]) */
31
+ plan?: string[];
32
+ /** Roles that should see this feature (e.g. ["admin", "editor"]) */
33
+ role?: string[];
34
+ /** Regions that should see this feature (e.g. ["us", "eu"]) */
35
+ region?: string[];
36
+ /** Arbitrary key-value pairs for custom matching logic */
37
+ custom?: Record<string, unknown>;
38
+ }
39
+ /** Dependency gates for progressive feature discovery */
40
+ interface FeatureDependencies {
41
+ /** Features the user must have seen before this one can surface */
42
+ seen?: string[];
43
+ /** Features the user must have clicked before this one can surface */
44
+ clicked?: string[];
45
+ /** Features the user must have dismissed before this one can surface */
46
+ dismissed?: string[];
47
+ }
48
+ /** Runtime context used by trigger evaluation */
49
+ interface TriggerContext {
50
+ /** Current app route/path */
51
+ path?: string;
52
+ /** Named events observed in this session */
53
+ events?: ReadonlySet<string>;
54
+ /** Named milestone flags reached in this session */
55
+ milestones?: ReadonlySet<string>;
56
+ /** Usage counters keyed by event/pattern name */
57
+ usage?: Record<string, number>;
58
+ /** Session elapsed time in milliseconds */
59
+ elapsedMs?: number;
60
+ /** Scroll completion percentage (0-100) */
61
+ scrollPercent?: number;
62
+ /** Optional additional trigger context */
63
+ metadata?: Record<string, unknown>;
64
+ }
65
+ type FeatureTrigger = {
66
+ type: "page";
67
+ match: string | RegExp;
68
+ } | {
69
+ type: "usage";
70
+ event: string;
71
+ minActions?: number;
72
+ } | {
73
+ type: "time";
74
+ minSeconds: number;
75
+ } | {
76
+ type: "milestone";
77
+ event: string;
78
+ } | {
79
+ type: "frustration";
80
+ pattern: string;
81
+ threshold?: number;
82
+ } | {
83
+ type: "scroll";
84
+ minPercent?: number;
85
+ } | {
86
+ type: "custom";
87
+ evaluate: (context: TriggerContext) => boolean;
88
+ };
89
+ /** A single feature entry in the manifest */
90
+ interface FeatureEntry {
91
+ /** Unique identifier for the feature */
92
+ id: string;
93
+ /** Human-readable label (e.g. "Decision Journal") */
94
+ label: string;
95
+ /** Optional longer description (supports markdown in UI components) */
96
+ description?: string;
97
+ /**
98
+ * Semantic version targeting.
99
+ * If provided as an object, requires `appVersion` to be supplied to the provider/helpers.
100
+ * - introduced: earliest app version that includes this feature
101
+ * - showNewUntil: stop showing "new" once appVersion reaches this
102
+ * - deprecatedAt: hide feature for app versions at or above this (optional safety)
103
+ * - showIn: range string, e.g. ">=2.5.0 <3.0.0"
104
+ */
105
+ version?: string | {
106
+ introduced?: string;
107
+ showNewUntil?: string;
108
+ deprecatedAt?: string;
109
+ showIn?: string;
110
+ };
111
+ /** ISO date when this feature was released */
112
+ releasedAt: string;
113
+ /** ISO date after which the "new" badge should stop showing */
114
+ showNewUntil: string;
115
+ /** Optional key to match navigation items (e.g. "/journal", "settings") */
116
+ sidebarKey?: string;
117
+ /** Optional grouping category (e.g. "ai", "billing", "core") */
118
+ category?: string;
119
+ /** Optional product scope (`"*"`, `"askverdict"`, etc.) for multi-product manifests */
120
+ product?: string;
121
+ /** Optional URL to link to (e.g. docs page, changelog entry) */
122
+ url?: string;
123
+ /** Optional feature flag key; requires a flag bridge to evaluate */
124
+ flagKey?: string;
125
+ /** Entry type — determines default icon/color in UI components */
126
+ type?: FeatureType;
127
+ /** Priority level — critical entries get special treatment in UI */
128
+ priority?: FeaturePriority;
129
+ /** Optional image/screenshot URL */
130
+ image?: string;
131
+ /** Optional call-to-action button */
132
+ cta?: FeatureCTA;
133
+ /** ISO date — entry is hidden until this date (scheduled publishing) */
134
+ publishAt?: string;
135
+ /** Optional arbitrary metadata */
136
+ meta?: Record<string, unknown>;
137
+ /** A/B variants keyed by variant name (e.g. control, treatment_a) */
138
+ variants?: Record<string, FeatureVariant>;
139
+ /** Percentage split per variant (same order as variants object keys) */
140
+ variantSplit?: number[];
141
+ /** Audience targeting — if set, only matching users see this feature */
142
+ audience?: AudienceRule;
143
+ /** Dependency requirements (progressive disclosure sequencing) */
144
+ dependsOn?: FeatureDependencies;
145
+ /** Contextual trigger rule */
146
+ trigger?: FeatureTrigger;
147
+ }
148
+
149
+ interface ManifestEditorProps {
150
+ features: readonly FeatureEntry[];
151
+ onSave: (updated: FeatureEntry[]) => Promise<void> | void;
152
+ readOnly?: boolean;
153
+ children?: ReactNode;
154
+ }
155
+ declare function ManifestEditor({ features, onSave, readOnly, children, }: ManifestEditorProps): react_jsx_runtime.JSX.Element;
156
+ interface ScheduleCalendarProps {
157
+ features: readonly FeatureEntry[];
158
+ onSchedule: (featureId: string, publishAt: string) => Promise<void> | void;
159
+ }
160
+ declare function ScheduleCalendar({ features, onSchedule }: ScheduleCalendarProps): react_jsx_runtime.JSX.Element;
161
+ interface PreviewPanelProps {
162
+ feature?: FeatureEntry | null;
163
+ components?: Array<"badge" | "changelog" | "spotlight" | "banner" | "toast">;
164
+ }
165
+ declare function PreviewPanel({ feature, components }: PreviewPanelProps): react_jsx_runtime.JSX.Element;
166
+ interface AudienceBuilderProps {
167
+ segments?: string[];
168
+ roles?: string[];
169
+ regions?: string[];
170
+ value?: AudienceRule;
171
+ onChange?: (audience: AudienceRule) => void;
172
+ onSave?: (audience: AudienceRule) => Promise<void> | void;
173
+ }
174
+ declare function AudienceBuilder({ segments, roles, regions, value, onChange, onSave, }: AudienceBuilderProps): react_jsx_runtime.JSX.Element;
175
+
176
+ export { AudienceBuilder, type AudienceBuilderProps, ManifestEditor, type ManifestEditorProps, PreviewPanel, type PreviewPanelProps, ScheduleCalendar, type ScheduleCalendarProps };