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.
- package/README.md +626 -186
- package/dist/angular.cjs +286 -0
- package/dist/angular.cjs.map +1 -0
- package/dist/angular.d.cts +229 -0
- package/dist/angular.d.ts +229 -0
- package/dist/angular.js +283 -0
- package/dist/angular.js.map +1 -0
- package/dist/featuredrop.cjs +1256 -0
- package/dist/featuredrop.cjs.map +1 -0
- package/dist/index.cjs +2769 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1020 -9
- package/dist/index.d.ts +1020 -9
- package/dist/index.js +2726 -10
- package/dist/index.js.map +1 -1
- package/dist/preact.cjs +7289 -0
- package/dist/preact.cjs.map +1 -0
- package/dist/preact.d.cts +1266 -0
- package/dist/preact.d.ts +1266 -0
- package/dist/preact.js +7259 -0
- package/dist/preact.js.map +1 -0
- package/dist/react.cjs +7142 -49
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1119 -7
- package/dist/react.d.ts +1119 -7
- package/dist/react.js +7122 -52
- package/dist/react.js.map +1 -1
- package/dist/schema.cjs +215 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.cts +203 -0
- package/dist/schema.d.ts +203 -0
- package/dist/schema.js +209 -0
- package/dist/schema.js.map +1 -0
- package/dist/solid.cjs +373 -0
- package/dist/solid.cjs.map +1 -0
- package/dist/solid.d.cts +242 -0
- package/dist/solid.d.ts +242 -0
- package/dist/solid.js +366 -0
- package/dist/solid.js.map +1 -0
- package/dist/svelte.cjs +329 -0
- package/dist/svelte.cjs.map +1 -0
- package/dist/svelte.js +324 -0
- package/dist/svelte.js.map +1 -0
- package/dist/testing.cjs +1422 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +339 -0
- package/dist/testing.d.ts +339 -0
- package/dist/testing.js +1415 -0
- package/dist/testing.js.map +1 -0
- package/dist/vue.cjs +1084 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.js +1072 -0
- package/dist/vue.js.map +1 -0
- package/dist/web-components.cjs +483 -0
- package/dist/web-components.cjs.map +1 -0
- package/dist/web-components.d.cts +211 -0
- package/dist/web-components.d.ts +211 -0
- package/dist/web-components.js +477 -0
- package/dist/web-components.js.map +1 -0
- package/package.json +126 -3
package/README.md
CHANGED
|
@@ -1,37 +1,76 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="#quick-start">Quick Start</a> •
|
|
19
|
+
<a href="#components">Components</a> •
|
|
20
|
+
<a href="#react">React</a> •
|
|
21
|
+
<a href="https://github.com/GLINCKER/featuredrop/blob/main/docs/API.md">API Docs</a> •
|
|
22
|
+
<a href="https://github.com/GLINCKER/featuredrop/blob/main/docs/ARCHITECTURE.md">Architecture</a>
|
|
23
|
+
</p>
|
|
9
24
|
|
|
10
25
|
---
|
|
11
26
|
|
|
12
|
-
##
|
|
27
|
+
## The Problem
|
|
13
28
|
|
|
14
|
-
Every SaaS
|
|
29
|
+
Every SaaS ships features. Users miss them. You need "New" badges on sidebar items, but:
|
|
15
30
|
|
|
16
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
38
|
+
**featuredrop** is a free, self-hosted alternative to Beamer, Headway, and AnnounceKit. Zero deps, < 10 kB, headless-first.
|
|
25
39
|
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
+
type: 'feature',
|
|
87
|
+
priority: 'critical',
|
|
88
|
+
cta: { label: 'Try it', url: '/journal' },
|
|
48
89
|
},
|
|
49
90
|
{
|
|
50
|
-
id: '
|
|
51
|
-
label: '
|
|
52
|
-
releasedAt: '2026-02-
|
|
53
|
-
showNewUntil: '2026-03-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
281
|
+
## Testing Utilities
|
|
282
|
+
|
|
283
|
+
Use `featuredrop/testing` to speed up unit and component tests.
|
|
117
284
|
|
|
118
285
|
```tsx
|
|
119
|
-
import {
|
|
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
|
-
|
|
122
|
-
|
|
290
|
+
const manifest = createMockManifest([{ label: 'AI Journal', releasedAt: 'today', showNewUntil: '+14d' }])
|
|
291
|
+
const storage = createMockStorage()
|
|
292
|
+
const Wrapper = createTestProvider({ manifest, storage })
|
|
123
293
|
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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 {
|
|
327
|
+
import { ChangelogWidget } from 'featuredrop/react'
|
|
137
328
|
|
|
138
|
-
|
|
139
|
-
|
|
329
|
+
// Default: slide-out panel with all features
|
|
330
|
+
<ChangelogWidget />
|
|
140
331
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
373
|
+
### Toast Notifications
|
|
179
374
|
|
|
180
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
380
|
+
<Toast position="bottom-right" maxVisible={3} />
|
|
191
381
|
|
|
192
|
-
|
|
382
|
+
// Specific features only
|
|
383
|
+
<Toast featureIds={["ai-journal"]} autoDismissMs={5000} />
|
|
384
|
+
```
|
|
193
385
|
|
|
194
|
-
|
|
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
|
-
|
|
388
|
+
Guided, multi-step onboarding with keyboard navigation and persistence.
|
|
202
389
|
|
|
203
|
-
|
|
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
|
-
|
|
393
|
+
<Tour id="onboarding" steps={steps} />
|
|
394
|
+
const { startTour, nextStep, skipTour } = useTour('onboarding')
|
|
395
|
+
```
|
|
210
396
|
|
|
211
|
-
|
|
212
|
-
|---------|-------------|
|
|
213
|
-
| `LocalStorageAdapter` | Browser localStorage + server watermark |
|
|
214
|
-
| `MemoryAdapter` | In-memory (testing, SSR) |
|
|
397
|
+
### Onboarding Checklist
|
|
215
398
|
|
|
216
|
-
|
|
399
|
+
Task-based onboarding that can trigger tours, links, or callbacks.
|
|
217
400
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
408
|
+
### Feedback Widget
|
|
245
409
|
|
|
246
|
-
|
|
410
|
+
Collect lightweight in-app feedback with optional emoji and screenshots.
|
|
247
411
|
|
|
248
|
-
```
|
|
249
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
this.cache.append('dismissed', id)
|
|
262
|
-
}
|
|
543
|
+
**Throttling + quiet mode:**
|
|
263
544
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
|
274
|
-
|
|
275
|
-
|
|
|
276
|
-
|
|
|
277
|
-
|
|
|
278
|
-
|
|
|
279
|
-
|
|
|
280
|
-
|
|
|
281
|
-
|
|
|
282
|
-
|
|
|
283
|
-
|
|
|
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
|
|
725
|
+
MIT © [Glincker](https://glincker.com)
|
|
288
726
|
|
|
289
727
|
---
|
|
290
728
|
|
|
291
729
|
<p align="center">
|
|
292
|
-
<
|
|
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>
|