Skip to content

Commit cd326c0

Browse files
thegdsksglinr
andcommitted
feat: complete Phase J — AI-native, engine hooks, framework integrations
AdoptionEngine (J1): BehaviorTracker, TimingOptimizer, FormatSelector, AdoptionScorer with full test coverage (27 tests). Engine React hooks (J1.6): useSmartFeature, useAdoptionScore, useBehaviorProfile with graceful degradation without engine (15 tests). SmartAnnouncement (J1.7): compound component with 6 format renderers (badge, toast, banner, inline, modal→toast, spotlight→badge), render prop support, auto-seen tracking (12 tests). shadcn registry (J3): 5 components (NewBadge, ChangelogWidget, Tour, Checklist, FeedbackWidget), build script, registry.json, docs pages. MCP server (J4): 5 tools + 6 resources, @modelcontextprotocol/sdk. AI setup CLI (J5): `featuredrop ai-setup` auto-detects Claude Code, Cursor, VS Code and creates context files + MCP configs (11 tests). Framework integrations (J6): featuredrop/next (RSC + FeatureDropScript), featuredrop/remix (loader helpers + headers), featuredrop/astro (SSG + manifest script), featuredrop/nuxt (event handler + head script) with 14 tests. All 4 subpaths build as ESM + CJS + DTS. Tailwind plugin (J7): CSS vars, keyframes, utilities, dark mode. Next.js + shadcn example app, upgraded vanilla example, docs site shadcn pages with live demos. Co-Authored-By: Glinr <bot@glincker.com>
1 parent 1a23a5e commit cd326c0

91 files changed

Lines changed: 13079 additions & 262 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ node_modules/
33

44
# Build output
55
dist/
6+
.next/
7+
apps/docs/public/r/
68

79
# Coverage
810
coverage/

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,28 @@ function MyChangelog() {
269269

270270
> **When to use hooks vs components:** If your project uses shadcn/ui, Radix, or any custom design system, use hooks from `featuredrop/react/hooks`. If you want out-of-the-box UI, use components from `featuredrop/react`.
271271
272+
### shadcn/ui Components
273+
274+
Pre-built components that use shadcn primitives for UI + FeatureDrop hooks for logic. Install via the shadcn CLI:
275+
276+
```bash
277+
npx shadcn@latest add https://featuredrop.dev/r/changelog-widget.json
278+
```
279+
280+
| Component | Install |
281+
|-----------|---------|
282+
| New Badge | `npx shadcn@latest add https://featuredrop.dev/r/new-badge.json` |
283+
| Changelog Widget | `npx shadcn@latest add https://featuredrop.dev/r/changelog-widget.json` |
284+
| Tour | `npx shadcn@latest add https://featuredrop.dev/r/tour.json` |
285+
| Checklist | `npx shadcn@latest add https://featuredrop.dev/r/checklist.json` |
286+
| Feedback Widget | `npx shadcn@latest add https://featuredrop.dev/r/feedback-widget.json` |
287+
288+
Components land in `components/featuredrop/` — you own the code. [Full docs &rarr;](https://featuredrop.dev/docs/shadcn)
289+
290+
**Try it now:**
291+
- [Next.js + shadcn example](https://stackblitz.com/github/GLINCKER/featuredrop/tree/main/examples/nextjs-shadcn) — Open in StackBlitz
292+
- [Vanilla JS example](examples/vanilla/index.html) — Zero build step, CDN import
293+
272294
---
273295

274296
## AI-Native
@@ -417,7 +439,7 @@ npx featuredrop migrate --from beamer --input beamer-export.json --out features.
417439

418440
| | Beamer | Pendo | **FeatureDrop** |
419441
|---|---|---|---|
420-
| Price | $59–399/mo | $7k+/yr | **Free forever** |
442+
| Price | $59–399/mo | $7k+/yr | **Free (MIT)** |
421443
| Bundle impact | External script | ~300 kB agent | **< 3 kB core** |
422444
| Vendor lock-in | Yes | Yes | **No** |
423445
| Data ownership | Vendor-hosted | Vendor-hosted | **Your repo** |

apps/docs/components/code-demo-card.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,27 @@ export function CodeDemoCard({ title, description, code, children }: CodeDemoCar
110110
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
111111
</button>
112112
</div>
113-
<div className="flex-1 overflow-auto text-[13px] nice-scrollbar">
114-
<SyntaxHighlighter
115-
language="tsx"
113+
<div className="flex-1 overflow-auto text-[13px] nice-scrollbar" data-code-demo="">
114+
<SyntaxHighlighter
115+
language="tsx"
116116
style={vscDarkPlus}
117-
customStyle={{
117+
wrapLines={false}
118+
wrapLongLines={false}
119+
customStyle={{
118120
margin: 0,
119121
padding: '1.5rem',
120122
backgroundColor: 'transparent',
121123
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
122124
}}
123125
codeTagProps={{
124-
style: { fontFamily: 'inherit', lineHeight: '1.5' }
126+
style: {
127+
fontFamily: 'inherit',
128+
lineHeight: '1.6',
129+
whiteSpace: 'pre',
130+
wordBreak: 'normal',
131+
overflowWrap: 'normal',
132+
display: 'block',
133+
}
125134
}}
126135
>
127136
{code.trim()}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { useMemo, useState } from 'react'
2+
import { MemoryAdapter } from 'featuredrop'
3+
import { FeatureDropProvider } from 'featuredrop/react'
4+
import { useChangelog } from 'featuredrop/react'
5+
import { CodeDemoCard } from '../code-demo-card'
6+
import { demoManifest } from '../demos/demo-manifest'
7+
8+
const FeatureDropProviderView = FeatureDropProvider as any
9+
10+
const demoCode = `import { ChangelogWidget } from "@/components/featuredrop/changelog-widget"
11+
12+
export function Header() {
13+
return (
14+
<header className="flex items-center justify-between p-4">
15+
<h1>My App</h1>
16+
<ChangelogWidget title="Release Notes" side="right" />
17+
</header>
18+
)
19+
}`
20+
21+
function ChangelogPanel() {
22+
const { features, newCount, isNew, dismiss, markAllSeen } = useChangelog()
23+
const [open, setOpen] = useState(false)
24+
25+
const handleClose = () => {
26+
setOpen(false)
27+
markAllSeen()
28+
}
29+
30+
return (
31+
<div className="w-full max-w-sm">
32+
<div className="flex items-center justify-between rounded-lg border border-slate-200 bg-white px-4 py-3 dark:border-slate-700 dark:bg-[#070b14]">
33+
<span className="text-sm font-medium text-slate-900 dark:text-slate-100">My App</span>
34+
<button
35+
type="button"
36+
onClick={() => setOpen(true)}
37+
className="relative inline-flex items-center gap-2 rounded-md border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-900 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700"
38+
>
39+
What&apos;s New
40+
{newCount > 0 && (
41+
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1.5 text-[10px] font-bold text-white">
42+
{newCount}
43+
</span>
44+
)}
45+
</button>
46+
</div>
47+
48+
{open && (
49+
<div className="mt-2 rounded-lg border border-slate-200 bg-white shadow-lg dark:border-slate-700 dark:bg-[#070b14]">
50+
<div className="flex items-center justify-between border-b border-slate-200 px-4 py-3 dark:border-slate-700">
51+
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">Release Notes</p>
52+
<button type="button" onClick={handleClose} className="text-xs text-slate-500 hover:text-slate-700 dark:hover:text-slate-300">
53+
Close
54+
</button>
55+
</div>
56+
<div className="max-h-64 overflow-y-auto">
57+
{features.map((f, i) => (
58+
<div key={f.id}>
59+
<button
60+
type="button"
61+
className="w-full px-4 py-3 text-left transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/50"
62+
onClick={() => dismiss(f.id)}
63+
>
64+
<div className="flex items-start justify-between gap-2">
65+
<div>
66+
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">{f.label}</p>
67+
{f.description && (
68+
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">{f.description}</p>
69+
)}
70+
</div>
71+
{isNew(f.sidebarKey ?? f.id) && (
72+
<span className="shrink-0 rounded-full bg-slate-900 px-2 py-0.5 text-[10px] font-medium text-white dark:bg-slate-100 dark:text-slate-900">
73+
New
74+
</span>
75+
)}
76+
</div>
77+
</button>
78+
{i < features.length - 1 && <hr className="border-slate-100 dark:border-slate-800" />}
79+
</div>
80+
))}
81+
</div>
82+
</div>
83+
)}
84+
</div>
85+
)
86+
}
87+
88+
export function ChangelogWidgetShadcnDemo() {
89+
const storage = useMemo(() => new MemoryAdapter(), [])
90+
91+
return (
92+
<CodeDemoCard
93+
title="Changelog Widget (shadcn/ui)"
94+
description="Slide-out changelog panel with unread count. Uses useChangelog() hook."
95+
code={demoCode}
96+
>
97+
<FeatureDropProviderView manifest={demoManifest} storage={storage}>
98+
<ChangelogPanel />
99+
</FeatureDropProviderView>
100+
</CodeDemoCard>
101+
)
102+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Checklist, type ChecklistTask } from 'featuredrop/react'
2+
import { useChecklist } from 'featuredrop/react'
3+
import { CodeDemoCard } from '../code-demo-card'
4+
5+
const ChecklistView = Checklist as any
6+
7+
const demoCode = `import { Checklist as ChecklistComponent } from "featuredrop/react"
8+
import { Checklist } from "@/components/featuredrop/checklist"
9+
10+
const tasks = [
11+
{ id: "profile", title: "Complete your profile", description: "Add a photo and bio." },
12+
{ id: "invite", title: "Invite a teammate" },
13+
{ id: "first-project", title: "Create your first project" },
14+
]
15+
16+
export function Onboarding() {
17+
return (
18+
<>
19+
<ChecklistComponent id="onboarding" tasks={tasks} position="inline" />
20+
<Checklist checklistId="onboarding" tasks={tasks} title="Getting Started" />
21+
</>
22+
)
23+
}`
24+
25+
const tasks: ChecklistTask[] = [
26+
{ id: 'connect-provider', title: 'Connect provider', description: 'Wrap app shell with FeatureDropProvider.' },
27+
{ id: 'add-widget', title: 'Add changelog widget', description: 'Surface release notes in your nav.' },
28+
{ id: 'enable-ci', title: 'Enable CI checks', description: 'Validate before merge.' }
29+
]
30+
31+
function ChecklistPreview() {
32+
const { completeTask, resetChecklist, dismissChecklist, progress, tasks: taskStates, dismissed } = useChecklist('shadcn-checklist-demo')
33+
34+
if (dismissed) {
35+
return (
36+
<div className="w-full max-w-sm rounded-lg border border-slate-200 bg-white p-6 text-center dark:border-slate-700 dark:bg-[#070b14]">
37+
<p className="text-sm text-slate-500 dark:text-slate-400">Checklist dismissed.</p>
38+
<button type="button" onClick={resetChecklist} className="mt-2 text-xs text-slate-900 underline dark:text-slate-100">
39+
Reset
40+
</button>
41+
</div>
42+
)
43+
}
44+
45+
const getState = (taskId: string) => taskStates.find((t) => t.id === taskId)
46+
47+
return (
48+
<div className="w-full max-w-sm rounded-lg border border-slate-200 bg-white shadow-sm dark:border-slate-700 dark:bg-[#070b14]">
49+
<div className="border-b border-slate-100 p-4 dark:border-slate-800">
50+
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">Getting Started</p>
51+
<div className="mt-2 h-2 w-full rounded-full bg-slate-100 dark:bg-slate-800">
52+
<div
53+
className="h-2 rounded-full bg-slate-900 transition-all dark:bg-slate-100"
54+
style={{ width: `${progress.percent}%` }}
55+
/>
56+
</div>
57+
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">
58+
{progress.completed} of {progress.total} complete
59+
</p>
60+
</div>
61+
<div className="space-y-1 p-2">
62+
{tasks.map((task) => {
63+
const state = getState(task.id)
64+
const completed = state?.completed ?? false
65+
return (
66+
<label
67+
key={task.id}
68+
className={`flex cursor-pointer items-start gap-3 rounded-md p-3 transition-colors ${completed ? 'opacity-60' : 'hover:bg-slate-50 dark:hover:bg-slate-800/50'}`}
69+
>
70+
<input
71+
type="checkbox"
72+
checked={completed}
73+
onChange={() => { if (!completed) completeTask(task.id) }}
74+
disabled={completed}
75+
className="mt-0.5 h-4 w-4 rounded border-slate-300 dark:border-slate-600"
76+
/>
77+
<div>
78+
<p className={`text-sm font-medium text-slate-900 dark:text-slate-100 ${completed ? 'line-through' : ''}`}>
79+
{task.title}
80+
</p>
81+
{task.description && (
82+
<p className="text-xs text-slate-500 dark:text-slate-400">{task.description}</p>
83+
)}
84+
</div>
85+
</label>
86+
)
87+
})}
88+
</div>
89+
<div className="flex justify-between border-t border-slate-100 px-4 py-3 dark:border-slate-800">
90+
<button type="button" onClick={resetChecklist} className="text-xs text-slate-500 hover:text-slate-700 dark:hover:text-slate-300">
91+
Reset
92+
</button>
93+
<button type="button" onClick={dismissChecklist} className="text-xs text-slate-500 hover:text-slate-700 dark:hover:text-slate-300">
94+
Dismiss
95+
</button>
96+
</div>
97+
</div>
98+
)
99+
}
100+
101+
export function ChecklistShadcnDemo() {
102+
return (
103+
<CodeDemoCard
104+
title="Checklist (shadcn/ui)"
105+
description="Onboarding checklist with progress tracking. Uses useChecklist() hook."
106+
code={demoCode}
107+
>
108+
<ChecklistView id="shadcn-checklist-demo" tasks={tasks} position="inline">
109+
{() => <ChecklistPreview />}
110+
</ChecklistView>
111+
</CodeDemoCard>
112+
)
113+
}

0 commit comments

Comments
 (0)