featuredrop 1.0.1 → 1.2.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 (60) hide show
  1. package/README.md +626 -186
  2. package/dist/angular.cjs +286 -0
  3. package/dist/angular.cjs.map +1 -0
  4. package/dist/angular.d.cts +229 -0
  5. package/dist/angular.d.ts +229 -0
  6. package/dist/angular.js +283 -0
  7. package/dist/angular.js.map +1 -0
  8. package/dist/featuredrop.cjs +1256 -0
  9. package/dist/featuredrop.cjs.map +1 -0
  10. package/dist/index.cjs +2769 -9
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +1020 -9
  13. package/dist/index.d.ts +1020 -9
  14. package/dist/index.js +2726 -10
  15. package/dist/index.js.map +1 -1
  16. package/dist/preact.cjs +7289 -0
  17. package/dist/preact.cjs.map +1 -0
  18. package/dist/preact.d.cts +1266 -0
  19. package/dist/preact.d.ts +1266 -0
  20. package/dist/preact.js +7259 -0
  21. package/dist/preact.js.map +1 -0
  22. package/dist/react.cjs +7142 -49
  23. package/dist/react.cjs.map +1 -1
  24. package/dist/react.d.cts +1119 -7
  25. package/dist/react.d.ts +1119 -7
  26. package/dist/react.js +7122 -52
  27. package/dist/react.js.map +1 -1
  28. package/dist/schema.cjs +215 -0
  29. package/dist/schema.cjs.map +1 -0
  30. package/dist/schema.d.cts +203 -0
  31. package/dist/schema.d.ts +203 -0
  32. package/dist/schema.js +209 -0
  33. package/dist/schema.js.map +1 -0
  34. package/dist/solid.cjs +373 -0
  35. package/dist/solid.cjs.map +1 -0
  36. package/dist/solid.d.cts +242 -0
  37. package/dist/solid.d.ts +242 -0
  38. package/dist/solid.js +366 -0
  39. package/dist/solid.js.map +1 -0
  40. package/dist/svelte.cjs +329 -0
  41. package/dist/svelte.cjs.map +1 -0
  42. package/dist/svelte.js +324 -0
  43. package/dist/svelte.js.map +1 -0
  44. package/dist/testing.cjs +1422 -0
  45. package/dist/testing.cjs.map +1 -0
  46. package/dist/testing.d.cts +339 -0
  47. package/dist/testing.d.ts +339 -0
  48. package/dist/testing.js +1415 -0
  49. package/dist/testing.js.map +1 -0
  50. package/dist/vue.cjs +1084 -0
  51. package/dist/vue.cjs.map +1 -0
  52. package/dist/vue.js +1072 -0
  53. package/dist/vue.js.map +1 -0
  54. package/dist/web-components.cjs +483 -0
  55. package/dist/web-components.cjs.map +1 -0
  56. package/dist/web-components.d.cts +211 -0
  57. package/dist/web-components.d.ts +211 -0
  58. package/dist/web-components.js +477 -0
  59. package/dist/web-components.js.map +1 -0
  60. package/package.json +126 -3
package/README.md CHANGED
@@ -1,37 +1,76 @@
1
- # featuredrop
1
+ <p align="center">
2
+ <h1 align="center">featuredrop</h1>
3
+ <p align="center">
4
+ <strong>"New" badges that actually expire.</strong>
5
+ <br />
6
+ Lightweight feature discovery for SaaS sidebars, dashboards, and nav menus.
7
+ </p>
8
+ </p>
2
9
 
3
- **Lightweight feature discovery system. Show "New" badges that auto-expire.**
10
+ <p align="center">
11
+ <a href="https://www.npmjs.com/package/featuredrop"><img src="https://img.shields.io/npm/v/featuredrop?color=f59e0b&label=npm" alt="npm version"></a>
12
+ <a href="https://bundlephobia.com/package/featuredrop"><img src="https://img.shields.io/bundlephobia/minzip/featuredrop?color=22c55e&label=size" alt="bundle size"></a>
13
+ <a href="https://github.com/GLINCKER/featuredrop/actions/workflows/ci.yml"><img src="https://github.com/GLINCKER/featuredrop/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
14
+ <a href="https://github.com/GLINCKER/featuredrop/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/featuredrop?color=blue" alt="license"></a>
15
+ </p>
4
16
 
5
- [![npm version](https://img.shields.io/npm/v/featuredrop)](https://www.npmjs.com/package/featuredrop)
6
- [![license](https://img.shields.io/npm/l/featuredrop)](https://github.com/GLINCKER/featuredrop/blob/main/LICENSE)
7
- [![bundle size](https://img.shields.io/bundlephobia/minzip/featuredrop)](https://bundlephobia.com/package/featuredrop)
8
- [![CI](https://github.com/GLINCKER/featuredrop/actions/workflows/ci.yml/badge.svg)](https://github.com/GLINCKER/featuredrop/actions/workflows/ci.yml)
17
+ <p align="center">
18
+ <a href="#quick-start">Quick Start</a> &bull;
19
+ <a href="#components">Components</a> &bull;
20
+ <a href="#react">React</a> &bull;
21
+ <a href="https://github.com/GLINCKER/featuredrop/blob/main/docs/API.md">API Docs</a> &bull;
22
+ <a href="https://github.com/GLINCKER/featuredrop/blob/main/docs/ARCHITECTURE.md">Architecture</a>
23
+ </p>
9
24
 
10
25
  ---
11
26
 
12
- ## Why featuredrop?
27
+ ## The Problem
13
28
 
14
- Every SaaS needs "New" badges on sidebar items when features ship. But most solutions are either too complex (LaunchDarkly), too coupled (Beamer), or don't actually expire.
29
+ Every SaaS ships features. Users miss them. You need "New" badges on sidebar items, but:
15
30
 
16
- **featuredrop** solves this with a dead-simple API:
31
+ - **LaunchDarkly / PostHog** Feature flags, not discovery badges. Overkill.
32
+ - **Beamer / Headway** — External widget, vendor lock-in, $59-299/mo.
33
+ - **Joyride / Shepherd.js** — Product tours, not persistent badges.
34
+ - **DIY** — You build it, forget expiry, badges stay forever. Users stop noticing.
17
35
 
18
- - Define features in a manifest (just an array of objects)
19
- - Badges auto-expire based on time windows
20
- - Users can dismiss individually or "mark all as seen"
21
- - Works with any framework — React bindings included
22
- - Zero dependencies, < 2 kB minzipped
36
+ ## The Solution
23
37
 
24
- ## Quick Start
38
+ **featuredrop** is a free, self-hosted alternative to Beamer, Headway, and AnnounceKit. Zero deps, < 10 kB, headless-first.
25
39
 
26
- ### Install
40
+ ```
41
+ npm install featuredrop # 0 dependencies, < 2 kB core
42
+ ```
27
43
 
28
- ```bash
29
- npm install featuredrop
30
- # or
31
- pnpm add featuredrop
32
44
  ```
45
+ ┌─────────────────────────────────────────┐
46
+ │ ☰ My SaaS App 🔔 │
47
+ ├──────────┬──────────────────────────────┤
48
+ │ │ │
49
+ │ Dashboard│ Welcome back, Sarah! │
50
+ │ │ │
51
+ │ Journal ●│ ┌─────────────────────┐ │
52
+ │ │ │ What's New (2) │ │
53
+ │ Analytics│ │ │ │
54
+ │ NEW │ │ ★ AI Journal │ │
55
+ │ │ │ Track decisions │ │
56
+ │ Billing │ │ with AI insights. │ │
57
+ │ │ │ [Try it →] │ │
58
+ │ Settings │ │ │ │
59
+ │ │ │ ★ Analytics v2 │ │
60
+ │ │ │ Real-time charts │ │
61
+ │ │ │ and CSV export. │ │
62
+ │ │ │ │ │
63
+ │ │ │ [Mark all as read] │ │
64
+ │ │ └─────────────────────┘ │
65
+ │ │ │
66
+ └──────────┴──────────────────────────────┘
67
+
68
+ ● = dot badge NEW = pill badge (2) = count badge
69
+ ```
70
+
71
+ ## Quick Start
33
72
 
34
- ### 1. Define your feature manifest
73
+ **1. Define features** (just an array of objects):
35
74
 
36
75
  ```ts
37
76
  import { createManifest } from 'featuredrop'
@@ -40,254 +79,655 @@ export const FEATURES = createManifest([
40
79
  {
41
80
  id: 'ai-journal',
42
81
  label: 'AI Decision Journal',
43
- description: 'Track decisions with AI-powered insights',
82
+ description: 'Track decisions with AI-powered insights.',
44
83
  releasedAt: '2026-02-20T00:00:00Z',
45
84
  showNewUntil: '2026-03-20T00:00:00Z',
46
85
  sidebarKey: '/journal',
47
- category: 'ai',
86
+ type: 'feature',
87
+ priority: 'critical',
88
+ cta: { label: 'Try it', url: '/journal' },
48
89
  },
49
90
  {
50
- id: 'analytics-v2',
51
- label: 'Analytics Dashboard v2',
52
- releasedAt: '2026-02-25T00:00:00Z',
53
- showNewUntil: '2026-03-25T00:00:00Z',
54
- sidebarKey: '/analytics',
91
+ id: 'scheduled-reports',
92
+ label: 'Scheduled Reports',
93
+ releasedAt: '2026-02-23T00:00:00Z',
94
+ showNewUntil: '2026-03-23T00:00:00Z',
95
+ dependsOn: { clicked: ['ai-journal'] }, // progressive rollout
96
+ trigger: { type: 'page', match: '/reports/*' }, // contextual trigger
55
97
  },
56
98
  ])
57
99
  ```
58
100
 
59
- ### 2. Create a storage adapter
101
+ **2. Create a storage adapter:**
60
102
 
61
103
  ```ts
62
104
  import { LocalStorageAdapter } from 'featuredrop'
63
105
 
64
106
  const storage = new LocalStorageAdapter({
65
- // Server-side watermark from user profile (e.g. user.featuresSeenAt)
66
- watermark: user.featuresSeenAt,
67
- // Optional: callback when user clicks "Mark all as seen"
68
- onDismissAll: async (now) => {
69
- await api.updateUser({ featuresSeenAt: now.toISOString() })
70
- },
107
+ watermark: user.featuresSeenAt, // from your server
108
+ onDismissAll: (now) => api.markFeaturesSeen(now), // optional server sync
71
109
  })
72
110
  ```
73
111
 
74
- ### 3. Check what's new
112
+ **3. Check what's new:**
75
113
 
76
114
  ```ts
77
115
  import { getNewFeatures, hasNewFeature } from 'featuredrop'
78
116
 
79
- // Get all new features
80
117
  const newFeatures = getNewFeatures(FEATURES, storage)
81
- console.log(`${newFeatures.length} new features!`)
118
+ hasNewFeature(FEATURES, '/journal', storage) // true/false
119
+ ```
120
+
121
+ Works with **any framework**. Zero React dependency for vanilla use.
122
+
123
+ ## Adoption Analytics
124
+
125
+ Pipe structured adoption events to PostHog/Amplitude/Mixpanel/Segment or your own endpoint.
126
+
127
+ ```ts
128
+ import { AnalyticsCollector, PostHogAdapter } from 'featuredrop'
129
+
130
+ const collector = new AnalyticsCollector({
131
+ adapter: new PostHogAdapter(posthog),
132
+ batchSize: 20,
133
+ flushInterval: 10_000,
134
+ sampleRate: 1,
135
+ })
136
+ ```
137
+
138
+ ```tsx
139
+ <FeatureDropProvider
140
+ manifest={FEATURES}
141
+ storage={storage}
142
+ collector={collector}
143
+ >
144
+ <App />
145
+ </FeatureDropProvider>
146
+ ```
82
147
 
83
- // Check a specific sidebar item
84
- if (hasNewFeature(FEATURES, '/journal', storage)) {
85
- showBadge('/journal')
148
+ ## A/B Announcement Variants
149
+
150
+ Run deterministic per-user announcement variants with weighted splits.
151
+
152
+ ```ts
153
+ {
154
+ id: 'ai-journal',
155
+ label: 'AI Decision Journal',
156
+ variants: {
157
+ control: { description: 'Track decisions with AI-powered insights.' },
158
+ treatment: { description: 'Never second-guess decisions again.' },
159
+ },
160
+ variantSplit: [50, 50],
86
161
  }
87
162
  ```
88
163
 
89
- ## React Integration
164
+ ```tsx
165
+ <FeatureDropProvider
166
+ manifest={FEATURES}
167
+ storage={storage}
168
+ variantKey={user.id} // stable key for deterministic assignment
169
+ >
170
+ <App />
171
+ </FeatureDropProvider>
172
+ ```
90
173
 
91
- ```bash
92
- # React is an optional peer dependency — only needed if you use featuredrop/react
93
- npm install featuredrop react
174
+ ## Theme Engine
175
+
176
+ Theme all featuredrop components with CSS variables (no CSS-in-JS runtime).
177
+
178
+ ```tsx
179
+ import { ThemeProvider, ChangelogWidget } from 'featuredrop/react'
180
+
181
+ <ThemeProvider theme="dark">
182
+ <ChangelogWidget />
183
+ </ThemeProvider>
94
184
  ```
95
185
 
96
- ### Wrap your app with the provider
186
+ ```tsx
187
+ import { createTheme } from 'featuredrop'
188
+ import { ThemeProvider, ChangelogPage } from 'featuredrop/react'
189
+
190
+ const myTheme = createTheme({
191
+ colors: { primary: '#7c3aed' },
192
+ radii: { lg: '16px' },
193
+ })
194
+
195
+ <ThemeProvider theme={myTheme}>
196
+ <ChangelogPage />
197
+ </ThemeProvider>
198
+ ```
199
+
200
+ Presets: `light`, `dark`, `auto`, `minimal`, `vibrant`.
201
+ You can also pass `theme` directly to `ChangelogWidget` and `ChangelogPage` for component-scoped overrides.
202
+
203
+ ## Internationalization
204
+
205
+ Use built-in locale packs or supply partial overrides:
97
206
 
98
207
  ```tsx
99
- import { FeatureDropProvider } from 'featuredrop/react'
100
- import { LocalStorageAdapter } from 'featuredrop'
208
+ <FeatureDropProvider
209
+ manifest={FEATURES}
210
+ storage={storage}
211
+ locale="fr"
212
+ translations={{
213
+ submit: 'Envoyer maintenant',
214
+ }}
215
+ >
216
+ <App />
217
+ </FeatureDropProvider>
218
+ ```
101
219
 
102
- const storage = new LocalStorageAdapter({
103
- watermark: user.featuresSeenAt,
104
- onDismissAll: (now) => api.markFeaturesSeen(now),
220
+ Built-in locales: `en`, `es`, `fr`, `de`, `pt`, `zh-cn`, `ja`, `ko`, `ar`, `hi`.
221
+
222
+ ## Changelog-as-Code
223
+
224
+ Manage announcements as markdown files in your repo and compile to a manifest:
225
+
226
+ ```bash
227
+ npx featuredrop init --format markdown
228
+ npx featuredrop add --label "AI Journal" --category ai --description "Track decisions with AI."
229
+ npx featuredrop build --pattern "features/**/*.md" --out featuredrop.manifest.json
230
+ npx featuredrop validate --pattern "features/**/*.md"
231
+ npx featuredrop stats --pattern "features/**/*.md"
232
+ npx featuredrop doctor --pattern "features/**/*.md"
233
+ npx featuredrop generate-rss --pattern "features/**/*.md" --out featuredrop.rss.xml
234
+ npx featuredrop generate-changelog --pattern "features/**/*.md" --out CHANGELOG.generated.md
235
+ npx featuredrop migrate --from beamer --input beamer-export.json --out featuredrop.manifest.json
236
+ ```
237
+
238
+ Example feature file:
239
+
240
+ ```md
241
+ ---
242
+ id: ai-journal
243
+ label: AI Journal
244
+ type: feature
245
+ category: ai
246
+ releasedAt: 2026-02-15T00:00:00Z
247
+ showNewUntil: 2026-03-15T00:00:00Z
248
+ cta:
249
+ label: Try it now
250
+ url: /journal
251
+ ---
252
+ Track decisions and outcomes with AI-powered insights.
253
+ ```
254
+
255
+ ## Schema Validation
256
+
257
+ Validate manifest JSON in CI or tooling pipelines.
258
+
259
+ ```ts
260
+ import {
261
+ featureEntrySchema,
262
+ featureEntryJsonSchema,
263
+ validateManifest,
264
+ } from 'featuredrop/schema'
265
+
266
+ featureEntrySchema.parse({
267
+ id: 'ai-journal',
268
+ label: 'AI Journal',
269
+ releasedAt: '2026-02-15T00:00:00Z',
270
+ showNewUntil: '2026-03-15T00:00:00Z',
105
271
  })
106
272
 
107
- function App() {
108
- return (
109
- <FeatureDropProvider manifest={FEATURES} storage={storage}>
110
- <Sidebar />
111
- </FeatureDropProvider>
112
- )
273
+ const result = validateManifest(data)
274
+ if (!result.valid) {
275
+ throw new Error(result.errors.map((e) => `${e.path}: ${e.message}`).join("; "))
113
276
  }
277
+
278
+ console.log(featureEntryJsonSchema.properties.id.type) // "string"
114
279
  ```
115
280
 
116
- ### Use hooks in your components
281
+ ## Testing Utilities
282
+
283
+ Use `featuredrop/testing` to speed up unit and component tests.
117
284
 
118
285
  ```tsx
119
- import { useNewFeature, NewBadge } from 'featuredrop/react'
286
+ import { render, screen } from '@testing-library/react'
287
+ import { useNewCount } from 'featuredrop/react'
288
+ import { createMockManifest, createMockStorage, createTestProvider } from 'featuredrop/testing'
120
289
 
121
- function SidebarItem({ path, label }: { path: string; label: string }) {
122
- const { isNew, dismiss } = useNewFeature(path)
290
+ const manifest = createMockManifest([{ label: 'AI Journal', releasedAt: 'today', showNewUntil: '+14d' }])
291
+ const storage = createMockStorage()
292
+ const Wrapper = createTestProvider({ manifest, storage })
123
293
 
124
- return (
125
- <a href={path} onClick={() => isNew && dismiss()}>
126
- {label}
127
- {isNew && <NewBadge />}
128
- </a>
129
- )
294
+ function Count() {
295
+ return <span>{useNewCount()}</span>
130
296
  }
297
+
298
+ render(<Count />, { wrapper: Wrapper })
299
+ expect(screen.getByText('1')).toBeInTheDocument()
131
300
  ```
132
301
 
133
- ### "What's New" panel
302
+ ## Playground & Online Demos
303
+
304
+ Use the lightweight component playground for quick UI iteration:
305
+
306
+ ```bash
307
+ pnpm --dir examples/sandbox-react install
308
+ pnpm playground
309
+ pnpm playground:build
310
+ ```
311
+
312
+ One-click editable demos:
313
+
314
+ - React sandbox source: `examples/sandbox-react`
315
+ - StackBlitz: https://stackblitz.com/github/GLINCKER/featuredrop/tree/main/examples/sandbox-react
316
+ - CodeSandbox: https://codesandbox.io/p/sandbox/github/GLINCKER/featuredrop/tree/main/examples/sandbox-react
317
+
318
+ ## Components
319
+
320
+ Everything you'd expect from Beamer or Headway — but free, self-hosted, and headless-first.
321
+
322
+ ### Changelog Widget
323
+
324
+ The #1 feature people install these tools for. Trigger button with unread count badge, slide-out panel with rich changelog feed.
134
325
 
135
326
  ```tsx
136
- import { useFeatureDrop } from 'featuredrop/react'
327
+ import { ChangelogWidget } from 'featuredrop/react'
137
328
 
138
- function WhatsNew() {
139
- const { newFeatures, newCount, dismissAll } = useFeatureDrop()
329
+ // Default: slide-out panel with all features
330
+ <ChangelogWidget />
140
331
 
141
- return (
142
- <div>
143
- <h2>What's New ({newCount})</h2>
144
- {newFeatures.map(f => (
145
- <div key={f.id}>
146
- <h3>{f.label}</h3>
147
- <p>{f.description}</p>
148
- </div>
149
- ))}
150
- <button onClick={dismissAll}>Mark all as seen</button>
151
- </div>
152
- )
153
- }
332
+ // Or modal / popover variant
333
+ <ChangelogWidget variant="modal" title="Release Notes" />
334
+
335
+ // Enable emoji reactions on entries
336
+ <ChangelogWidget showReactions />
337
+
338
+ // Fully headless
339
+ <ChangelogWidget>
340
+ {({ isOpen, toggle, features, count, dismissAll }) => (
341
+ <YourCustomUI />
342
+ )}
343
+ </ChangelogWidget>
154
344
  ```
155
345
 
156
- ## How It Works
346
+ ### Spotlight Beacon
347
+
348
+ Pulsing beacon that attaches to any DOM element. Click to see feature tooltip.
349
+
350
+ ```tsx
351
+ import { Spotlight } from 'featuredrop/react'
352
+
353
+ const ref = useRef<HTMLButtonElement>(null)
354
+ <button ref={ref}>Analytics</button>
355
+ <Spotlight featureId="analytics-v2" targetRef={ref} />
157
356
 
357
+ // Or with CSS selector
358
+ <Spotlight featureId="analytics-v2" targetSelector="#analytics-btn" />
158
359
  ```
159
- Feature Manifest (static) Storage Adapter
160
- ┌─────────────────────┐ ┌──────────────────────┐
161
- │ id: "ai-journal" │ │ watermark: server │
162
- releasedAt: Feb 20 │ │ dismissed: localStorage│
163
- │ showNewUntil: Mar 20│ └──────────┬───────────┘
164
- └─────────┬───────────┘ │
165
- │ │
166
- ▼ ▼
167
- ┌─────────────────────────────────────┐
168
- │ isNew(feature) │
169
- │ │
170
- │ 1. Not dismissed? ✓ │
171
- │ 2. Before showNewUntil? ✓ │
172
- │ 3. Released after watermark? ✓ │
173
- │ │
174
- │ → Show "New" badge │
175
- └─────────────────────────────────────┘
360
+
361
+ ### Announcement Banner
362
+
363
+ Top-of-page or inline banner for major announcements. Auto-expires like badges.
364
+
365
+ ```tsx
366
+ import { Banner } from 'featuredrop/react'
367
+
368
+ <Banner featureId="v2-launch" variant="announcement" />
369
+ <Banner featureId="breaking-change" variant="warning" />
370
+ <Banner featureId="security-fix" variant="info" position="fixed" />
176
371
  ```
177
372
 
178
- **Three-check algorithm:**
373
+ ### Toast Notifications
179
374
 
180
- 1. **Dismissed?** Has the user clicked to dismiss this specific feature? (client-side, per-device)
181
- 2. **Expired?** — Is the current time past `showNewUntil`? (automatic, no user action needed)
182
- 3. **After watermark?** — Was the feature released after the user's "features seen at" timestamp? (server-side, cross-device)
375
+ Brief popup notifications for new features. Auto-dismiss, stackable, configurable position.
183
376
 
184
- This hybrid approach means:
185
- - New users see all recent features (no watermark = everything is new)
186
- - Returning users only see features released since their last visit
187
- - Individual dismissals are instant (localStorage, no server call)
188
- - "Mark all as seen" syncs across devices (server watermark update)
377
+ ```tsx
378
+ import { Toast } from 'featuredrop/react'
189
379
 
190
- ## API Reference
380
+ <Toast position="bottom-right" maxVisible={3} />
191
381
 
192
- ### Core Functions
382
+ // Specific features only
383
+ <Toast featureIds={["ai-journal"]} autoDismissMs={5000} />
384
+ ```
193
385
 
194
- | Function | Description |
195
- |----------|-------------|
196
- | `isNew(feature, watermark, dismissedIds, now?)` | Check if a single feature is "new" |
197
- | `getNewFeatures(manifest, storage, now?)` | Get all currently new features |
198
- | `getNewFeatureCount(manifest, storage, now?)` | Get count of new features |
199
- | `hasNewFeature(manifest, sidebarKey, storage, now?)` | Check if a sidebar key has new features |
386
+ ### Product Tours
200
387
 
201
- ### Helpers
388
+ Guided, multi-step onboarding with keyboard navigation and persistence.
202
389
 
203
- | Function | Description |
204
- |----------|-------------|
205
- | `createManifest(entries)` | Create a frozen, typed manifest |
206
- | `getFeatureById(manifest, id)` | Find a feature by ID |
207
- | `getNewFeaturesByCategory(manifest, category, storage, now?)` | Filter new features by category |
390
+ ```tsx
391
+ import { Tour, useTour } from 'featuredrop/react'
208
392
 
209
- ### Adapters
393
+ <Tour id="onboarding" steps={steps} />
394
+ const { startTour, nextStep, skipTour } = useTour('onboarding')
395
+ ```
210
396
 
211
- | Adapter | Description |
212
- |---------|-------------|
213
- | `LocalStorageAdapter` | Browser localStorage + server watermark |
214
- | `MemoryAdapter` | In-memory (testing, SSR) |
397
+ ### Onboarding Checklist
215
398
 
216
- ### React (`featuredrop/react`)
399
+ Task-based onboarding that can trigger tours, links, or callbacks.
217
400
 
218
- | Export | Description |
219
- |--------|-------------|
220
- | `FeatureDropProvider` | Context provider — wraps your app |
221
- | `useFeatureDrop()` | Full context: `{ newFeatures, newCount, isNew, dismiss, dismissAll }` |
222
- | `useNewFeature(key)` | Single item: `{ isNew, feature, dismiss }` |
223
- | `useNewCount()` | Just the count number |
224
- | `NewBadge` | Headless badge: `variant="pill" \| "dot" \| "count"` |
225
-
226
- ### NewBadge Styling
227
-
228
- Zero CSS framework dependency. Style via CSS custom properties:
229
-
230
- ```css
231
- /* In your global CSS or CSS-in-JS */
232
- [data-featuredrop] {
233
- --featuredrop-color: #b45309;
234
- --featuredrop-bg: rgba(245, 158, 11, 0.15);
235
- --featuredrop-font-size: 10px;
236
- --featuredrop-dot-size: 8px;
237
- --featuredrop-glow: rgba(245, 158, 11, 0.6);
238
- --featuredrop-count-size: 18px;
239
- --featuredrop-count-color: white;
240
- --featuredrop-count-bg: #f59e0b;
241
- }
401
+ ```tsx
402
+ import { Checklist, useChecklist } from 'featuredrop/react'
403
+
404
+ <Checklist id="getting-started" tasks={tasks} />
405
+ const { completeTask, progress } = useChecklist('getting-started')
242
406
  ```
243
407
 
244
- ## Custom Storage Adapter
408
+ ### Feedback Widget
245
409
 
246
- Implement the `StorageAdapter` interface for your persistence layer:
410
+ Collect lightweight in-app feedback with optional emoji and screenshots.
247
411
 
248
- ```ts
249
- import type { StorageAdapter } from 'featuredrop'
412
+ ```tsx
413
+ import { FeedbackWidget } from 'featuredrop/react'
414
+
415
+ <FeedbackWidget
416
+ featureId="ai-journal"
417
+ onSubmit={async (payload) => {
418
+ await fetch('/api/feedback', { method: 'POST', body: JSON.stringify(payload) })
419
+ }}
420
+ showEmoji
421
+ showScreenshot
422
+ rateLimit="1-per-feature"
423
+ />
424
+ ```
425
+
426
+ ### Survey (NPS / CSAT / CES / Custom)
427
+
428
+ Run micro-surveys with trigger rules and imperative controls.
429
+
430
+ ```tsx
431
+ import { Survey, useSurvey } from 'featuredrop/react'
432
+
433
+ <Survey
434
+ id="nps-main"
435
+ type="nps"
436
+ prompt="How likely are you to recommend us?"
437
+ triggerRules={{ minDaysSinceSignup: 7, signupAt: user.createdAt }}
438
+ onSubmit={saveSurvey}
439
+ />
440
+
441
+ const { show } = useSurvey('nps-main')
442
+ ```
443
+
444
+ ### Feature Request Voting
445
+
446
+ Capture and rank product requests with local persistence and optional webhook sync.
447
+
448
+ ```tsx
449
+ import { FeatureRequestButton, FeatureRequestForm } from 'featuredrop/react'
450
+
451
+ <FeatureRequestButton featureId="dark-mode" requestTitle="Dark mode" />
452
+
453
+ <FeatureRequestForm
454
+ categories={['UI', 'Performance', 'Integration', 'Other']}
455
+ onSubmit={async (request) => {
456
+ await fetch('/api/requests', { method: 'POST', body: JSON.stringify(request) })
457
+ }}
458
+ />
459
+ ```
460
+
461
+ ### Hotspots & Tooltips
462
+
463
+ Persistent contextual hints attached to specific UI targets.
464
+
465
+ ```tsx
466
+ import { Hotspot, TooltipGroup } from 'featuredrop/react'
467
+
468
+ <TooltipGroup maxVisible={1}>
469
+ <Hotspot id="export-help" target="#export-btn" frequency="once">
470
+ Export supports CSV, PDF, and Excel.
471
+ </Hotspot>
472
+ </TooltipGroup>
473
+ ```
474
+
475
+ ### Announcement Modal
476
+
477
+ Priority-based modal announcements with optional slide carousel.
478
+
479
+ ```tsx
480
+ import { AnnouncementModal } from 'featuredrop/react'
481
+
482
+ <AnnouncementModal
483
+ feature={criticalFeature}
484
+ trigger="auto"
485
+ frequency="once"
486
+ />
487
+ ```
488
+
489
+ ### Spotlight Chain
490
+
491
+ Lightweight chained spotlights for "here are 3 new things" flows.
492
+
493
+ ```tsx
494
+ import { SpotlightChain } from 'featuredrop/react'
495
+
496
+ <SpotlightChain
497
+ steps={[
498
+ { target: '#sidebar', content: 'New navigation' },
499
+ { target: '#search', content: 'Global search' },
500
+ ]}
501
+ />
502
+ ```
503
+
504
+ ### NewBadge
505
+
506
+ Headless badge component with variants. Zero CSS framework dependency.
507
+
508
+ ```tsx
509
+ import { NewBadge } from 'featuredrop/react'
510
+
511
+ <NewBadge /> // "New" pill
512
+ <NewBadge variant="dot" /> // Pulsing dot
513
+ <NewBadge variant="count" count={3} /> // Count badge
514
+ ```
515
+
516
+ ### Tab Title Notification
250
517
 
251
- class RedisAdapter implements StorageAdapter {
252
- getWatermark(): string | null {
253
- return this.cache.get('watermark')
254
- }
518
+ Updates the browser tab title with unread count. Restores when all read.
255
519
 
256
- getDismissedIds(): ReadonlySet<string> {
257
- return new Set(this.cache.get('dismissed') ?? [])
258
- }
520
+ ```tsx
521
+ import { useTabNotification } from 'featuredrop/react'
522
+
523
+ useTabNotification() // "(3) My App"
524
+ useTabNotification({ template: "[{count} new] {title}", flash: true })
525
+ ```
526
+
527
+ ## React
528
+
529
+ ```bash
530
+ npm install featuredrop react # react is an optional peer dep
531
+ ```
532
+
533
+ **Wrap your app:**
534
+
535
+ ```tsx
536
+ import { FeatureDropProvider } from 'featuredrop/react'
537
+
538
+ <FeatureDropProvider manifest={FEATURES} storage={storage} appVersion="2.5.1">
539
+ <App />
540
+ </FeatureDropProvider>
541
+ ```
259
542
 
260
- dismiss(id: string): void {
261
- this.cache.append('dismissed', id)
262
- }
543
+ **Throttling + quiet mode:**
263
544
 
264
- async dismissAll(now: Date): Promise<void> {
265
- await this.cache.set('watermark', now.toISOString())
266
- await this.cache.delete('dismissed')
267
- }
545
+ ```tsx
546
+ <FeatureDropProvider
547
+ manifest={FEATURES}
548
+ storage={storage}
549
+ throttle={{
550
+ maxSimultaneousBadges: 3,
551
+ maxSimultaneousSpotlights: 1,
552
+ maxToastsPerSession: 3,
553
+ minTimeBetweenModals: 30000,
554
+ minTimeBetweenTours: 86400000,
555
+ sessionCooldown: 5000,
556
+ respectDoNotDisturb: true,
557
+ }}
558
+ >
559
+ <App />
560
+ </FeatureDropProvider>
561
+ ```
562
+
563
+ **Add badges to your sidebar:**
564
+
565
+ ```tsx
566
+ import { useNewFeature, NewBadge } from 'featuredrop/react'
567
+
568
+ function SidebarItem({ path, label }: { path: string; label: string }) {
569
+ const { isNew, dismiss } = useNewFeature(path)
570
+ return (
571
+ <a href={path} onClick={() => isNew && dismiss()}>
572
+ {label}
573
+ {isNew && <NewBadge />}
574
+ </a>
575
+ )
268
576
  }
269
577
  ```
270
578
 
579
+ **Or drop in the full changelog widget:**
580
+
581
+ ```tsx
582
+ import { ChangelogWidget } from 'featuredrop/react'
583
+
584
+ <ChangelogWidget variant="panel" />
585
+ ```
586
+
587
+ **Hooks & Components:**
588
+
589
+ | Export | What it does |
590
+ |--------|-------------|
591
+ | `useFeatureDrop()` | Full context: features, count, dismiss, dismissAll |
592
+ | `useNewFeature(key)` | Single nav item: `{ isNew, feature, dismiss }` |
593
+ | `useNewCount()` | Just the badge count |
594
+ | `useTabNotification()` | Updates browser tab title with count |
595
+ | `useTour(id)` | Imperative tour controls and current step snapshot |
596
+ | `useTourSequencer(sequence)` | Ordered multi-tour orchestration by feature readiness |
597
+ | `useChecklist(id)` | Imperative checklist controls + progress |
598
+ | `useSurvey(id)` | Imperative survey controls (`show`, `hide`, `askLater`) + state |
599
+ | `<NewBadge />` | Headless badge: `pill`, `dot`, or `count` variant |
600
+ | `<ChangelogWidget />` | Full changelog feed with trigger button + optional reactions |
601
+ | `<ChangelogPage />` | Full-page changelog with filters/search/pagination |
602
+ | `<Spotlight />` | Pulsing beacon attached to DOM elements |
603
+ | `<Banner />` | Announcement banner with variants |
604
+ | `<Toast />` | Stackable toast notifications |
605
+ | `<Tour />` | Multi-step guided product tour |
606
+ | `<Checklist />` | Onboarding task checklist |
607
+ | `<FeedbackWidget />` | In-app feedback form with category/emoji/screenshot support |
608
+ | `<Survey />` | NPS/CSAT/CES/custom survey engine with trigger rules |
609
+ | `<FeatureRequestButton />` | Per-feature voting button with persisted vote guard |
610
+ | `<FeatureRequestForm />` | Request capture form + sortable request list |
611
+ | `<Hotspot />` / `<TooltipGroup />` | Contextual tooltips with visibility caps |
612
+ | `<AnnouncementModal />` | Priority/frequency-gated modal announcements |
613
+ | `<SpotlightChain />` | Lightweight chained spotlight walkthrough |
614
+
615
+ `useFeatureDrop()` also exposes queue/throttle controls: `queuedFeatures`, `totalNewCount`, `quietMode`, `setQuietMode`, `markFeatureSeen`, `markFeatureClicked`, toast-slot helpers, modal/tour pacing checks, and spotlight slot controls.
616
+ It also exposes trigger runtime helpers: `trackUsageEvent`, `trackTriggerEvent`, `trackMilestone`, and `setTriggerPath`.
617
+
618
+ **Analytics integration:**
619
+
620
+ ```tsx
621
+ <FeatureDropProvider
622
+ manifest={FEATURES}
623
+ storage={storage}
624
+ appVersion="2.5.1" // optional semver for version-pinned features
625
+ analytics={{
626
+ onFeatureSeen: (f) => posthog.capture('feature_seen', { id: f.id }),
627
+ onFeatureDismissed: (f) => posthog.capture('feature_dismissed', { id: f.id }),
628
+ onFeatureClicked: (f) => posthog.capture('feature_clicked', { id: f.id }),
629
+ onWidgetOpened: () => posthog.capture('changelog_opened'),
630
+ }}
631
+ >
632
+ ```
633
+
634
+ All components accept an optional `analytics` prop for component-level tracking too.
635
+
636
+ ## How It Works
637
+
638
+ ```
639
+ Manifest (static) Storage (runtime)
640
+ ┌───────────────────┐ ┌──────────────────────┐
641
+ │ releasedAt: Feb 20 │ │ watermark ← server │
642
+ │ showNewUntil: Mar 20│ │ dismissed ← localStorage│
643
+ └────────┬──────────┘ └──────────┬───────────┘
644
+ │ │
645
+ └──────────┐ ┌────────────────┘
646
+ ▼ ▼
647
+ ┌───────────────┐
648
+ │ isNew() │
649
+ │ │
650
+ │ !dismissed │
651
+ │ !expired │
652
+ │ afterWatermark│
653
+ │ afterPublishAt│
654
+ └───────┬───────┘
655
+
656
+ true / false
657
+ ```
658
+
659
+ New users see everything (no watermark). Returning users only see features shipped since their last visit. Individual dismissals are instant (localStorage). "Mark all seen" syncs across devices (one server write).
660
+
661
+ **Scheduled publishing**: Set `publishAt` to hide entries until a specific date — ship code now, reveal later.
662
+
663
+ **Priority sorting**: Critical features surface first in widgets and toasts. Priority levels: `critical`, `normal`, `low`.
664
+
665
+ **Entry types**: `feature`, `improvement`, `fix`, `breaking` — each with default icon/color in built-in components.
666
+
667
+ Read the full [Architecture doc](docs/ARCHITECTURE.md) for cross-device sync flow and custom adapter patterns.
668
+
271
669
  ## Comparison
272
670
 
273
- | Feature | featuredrop | LaunchDarkly | Beamer | Joyride |
274
- |---------|------------|-------------|--------|---------|
275
- | Auto-expiring badges | Yes | No | No | No |
276
- | Zero dependencies | Yes | No | No | No |
277
- | Framework agnostic | Yes | Yes | No | No |
278
- | React bindings | Yes | Yes | No | Yes |
279
- | Server watermark | Yes | N/A | Yes | No |
280
- | Per-feature dismiss | Yes | N/A | No | No |
281
- | < 2 kB bundle | Yes | No | No | No |
282
- | TypeScript | Yes | Yes | No | Partial |
283
- | Free & OSS | Yes | No | Freemium | Yes |
671
+ | | featuredrop | Beamer | Headway | AnnounceKit | Canny |
672
+ |---|:---:|:---:|:---:|:---:|:---:|
673
+ | **Price** | **Free** | $59-399/mo | $49-249/mo | $79-299/mo | $79+ |
674
+ | Auto-expiring badges | Yes | - | - | - | - |
675
+ | Changelog widget | Yes | Yes | Yes | Yes | Yes |
676
+ | Product tours | Yes | - | - | - | - |
677
+ | Onboarding checklists | Yes | - | - | - | - |
678
+ | Spotlight/beacon | Yes | - | - | - | - |
679
+ | Hotspot tooltips | Yes | - | - | - | - |
680
+ | Announcement modal | Yes | - | - | - | - |
681
+ | Spotlight chaining | Yes | - | - | - | - |
682
+ | Toast notifications | Yes | - | - | - | - |
683
+ | Announcement banner | Yes | - | - | - | - |
684
+ | Tab title notification | Yes | - | - | - | - |
685
+ | Zero dependencies | Yes | - | - | - | - |
686
+ | Framework agnostic | Yes | - | - | - | - |
687
+ | React bindings | Yes | - | - | - | - |
688
+ | Headless mode | Yes | - | - | - | - |
689
+ | Cross-device sync | Yes | Yes | Yes | Yes | Yes |
690
+ | Per-feature dismiss | Yes | - | - | - | - |
691
+ | Scheduled publishing | Yes | Yes | Yes | Yes | - |
692
+ | Priority levels | Yes | - | - | - | - |
693
+ | Analytics callbacks | Yes | Built-in | Built-in | Built-in | Built-in |
694
+ | < 10 kB minzipped | Yes | - | - | - | - |
695
+ | Self-hosted | Yes | - | - | - | - |
696
+ | Open source | Yes | - | - | - | - |
697
+
698
+ ## Framework Support
699
+
700
+ | Framework | Status | Import |
701
+ |-----------|--------|--------|
702
+ | React / Next.js | Stable | `featuredrop/react` |
703
+ | Vanilla JS | Stable | `featuredrop` |
704
+ | SolidJS | Preview | `featuredrop/solid` |
705
+ | Preact (compat) | Preview | `featuredrop/preact` |
706
+ | Web Components | Preview | `featuredrop/web-components` |
707
+ | Angular | Preview | `featuredrop/angular` |
708
+ | Vue 3 | Preview | `featuredrop/vue` |
709
+ | Svelte 5 | Preview (store bindings) | `featuredrop/svelte` |
710
+
711
+ ## Documentation
712
+
713
+ - [API Reference](docs/API.md) — All functions, adapters, hooks, components
714
+ - [Architecture](docs/ARCHITECTURE.md) — Three-check algorithm, cross-device sync, custom adapters
715
+ - [Next.js Example](examples/nextjs/) — Full App Router integration
716
+ - [Vanilla Example](examples/vanilla/) — Plain HTML, zero build step
717
+ - [React Sandbox](examples/sandbox-react/) — Interactive local/online playground
718
+
719
+ ## Contributing
720
+
721
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, commit conventions, and how releases work.
284
722
 
285
723
  ## License
286
724
 
287
- MIT - [Glincker](https://glincker.com)
725
+ MIT &copy; [Glincker](https://glincker.com)
288
726
 
289
727
  ---
290
728
 
291
729
  <p align="center">
292
- <strong>A <a href="https://glincker.com">GLINCKER</a> Open Source Project</strong>
730
+ <sub>Built and battle-tested at <a href="https://askverdict.ai">AskVerdict</a>.</sub>
731
+ <br />
732
+ <strong>A <a href="https://glincker.com">GLINCKER</a> open source project.</strong>
293
733
  </p>