# userTourKit v1.0.6 — Generated 2026-06-11T07:31:35Z
# Feature Adoption Tracking
> Track feature usage, measure adoption rates, and nudge users toward underused features with @tour-kit/adoption for React
{/* llm-context-callout */}
## What is @tour-kit/adoption?
The adoption package helps you understand which features users actually use, identify adoption patterns, and intelligently nudge users toward valuable features they haven't discovered.
## Why Track Adoption?
Building features is expensive. Understanding which features drive value and which go unused helps you:
- **Prioritize development** based on actual usage patterns
- **Guide users** to features they'll find valuable
- **Reduce churn** by identifying features users abandon
- **Measure product-market fit** through adoption metrics
## Core Concepts
### Feature
A product capability you want to track. Each feature has:
- **ID**: Unique identifier
- **Trigger**: How usage is detected (click, event, or callback)
- **Criteria**: When it's considered "adopted" (default: 3 uses in 30 days)
```tsx
const feature = {
id: 'dark-mode',
name: 'Dark Mode',
trigger: '#dark-mode-toggle', // CSS selector
adoptionCriteria: { minUses: 3, recencyDays: 30 },
}
```
### Adoption Status
Features progress through four states:
- **not_started**: Never used
- **exploring**: Used, but below adoption threshold
- **adopted**: Meets adoption criteria
- **churned**: Was adopted but hasn't been used recently
### Nudge
A contextual prompt shown to encourage feature discovery. The package handles:
- **Scheduling**: When to show nudges based on cooldowns and session limits
- **Prioritization**: Which features to nudge first
- **Dismissal**: Permanent dismissal or temporary snoozing
## Installation
## Quick Start
```tsx title="app/layout.tsx"
import { AdoptionProvider } from '@tour-kit/adoption'
const features = [
{
id: 'dark-mode',
name: 'Dark Mode',
trigger: '#dark-mode-toggle',
category: 'customization',
},
{
id: 'keyboard-shortcuts',
name: 'Keyboard Shortcuts',
trigger: { event: 'shortcuts:opened' },
category: 'productivity',
},
]
export default function RootLayout({ children }) {
return (
{children}
)
}
```
```tsx title="components/feature-badge.tsx"
import { IfNotAdopted, NewFeatureBadge } from '@tour-kit/adoption'
export function DarkModeToggle() {
return (
Toggle Dark Mode
)
}
```
```tsx title="components/export-button.tsx"
import { useFeature } from '@tour-kit/adoption'
export function ExportButton() {
const { trackUsage, isAdopted } = useFeature('export')
const handleExport = async () => {
// Your export logic
await exportData()
// Track the usage
trackUsage()
}
return (
Export Data {!isAdopted && New }
)
}
```
## Usage Tracking Methods
### Automatic Click Tracking
The simplest approach: provide a CSS selector as the trigger.
```tsx
const feature = {
id: 'search',
name: 'Search',
trigger: '[data-feature="search"]', // Tracks clicks on this element
}
```
### Custom Event Tracking
For complex interactions, dispatch custom events:
```tsx
const feature = {
id: 'export',
name: 'Export',
trigger: { event: 'export:complete' },
}
// In your code
async function handleExport() {
await exportData()
window.dispatchEvent(new CustomEvent('export:complete'))
}
```
### Programmatic Tracking
For maximum control, use the `useFeature` hook:
```tsx
function AIAssistant() {
const { trackUsage } = useFeature('ai-assist')
const handleGenerate = async () => {
const result = await generateWithAI()
if (result.success) {
trackUsage() // Track only on success
}
}
return Generate with AI
}
```
## Adoption Criteria
Define what "adopted" means for each feature:
```tsx
const feature = {
id: 'advanced-search',
name: 'Advanced Search',
trigger: '#advanced-search',
adoptionCriteria: {
minUses: 5, // Must use 5+ times
recencyDays: 30, // Within last 30 days
},
}
```
### Custom Adoption Logic
For complex cases, provide a custom function:
```tsx
const feature = {
id: 'premium-feature',
name: 'Premium Feature',
trigger: '#premium-btn',
adoptionCriteria: {
custom: (usage) => {
// Adopted if used 10+ times OR used in last 7 days
return usage.useCount >= 10 ||
(usage.lastUsed &&
Date.now() - new Date(usage.lastUsed).getTime() < 7 * 24 * 60 * 60 * 1000)
},
},
}
```
## Configuration
Full provider configuration:
```tsx
{
console.log(`Feature adopted: ${feature.name}`)
}}
onChurn={(feature) => {
console.log(`Feature churned: ${feature.name}`)
}}
/>
```
## Next Steps
## Related
- [`` reference](/docs/adoption/providers/adoption-provider) — every config option for the provider above.
- [`useFeature`](/docs/adoption/hooks/use-feature), [`useNudge`](/docs/adoption/hooks/use-nudge), [`useAdoptionStats`](/docs/adoption/hooks/use-adoption-stats), [`useAdoptionContext`](/docs/adoption/hooks/use-adoption-context) — full hook surface.
- [Dashboard components](/docs/adoption/dashboard) — `AdoptionDashboard`, [funnel](/docs/adoption/dashboard/funnel), [stats grid](/docs/adoption/dashboard/stats), [table](/docs/adoption/dashboard/table).
- [Analytics integration](/docs/adoption/analytics) — wire adoption events into `@tour-kit/analytics`.
- [Adoption analytics guide](/docs/guides/adoption-analytics) — funnel and retention patterns.
- [`@tour-kit/adoption` API reference](/docs/api/adoption) — full export surface.
---
# Analytics Integration
> Adoption analytics integration: automatically send feature usage and nudge interaction events to your analytics provider
## Overview
The @tour-kit/adoption package integrates with @tour-kit/analytics to automatically track adoption events. When an analytics provider is configured, events are sent for feature usage, adoption milestones, and nudge interactions.
## Setup
### Install Analytics Package
```bash
pnpm add @tour-kit/analytics
```
### Configure Provider
```tsx
import { AnalyticsProvider } from '@tour-kit/analytics'
import { AdoptionProvider } from '@tour-kit/adoption'
function App() {
return (
{children}
)
}
```
That's it! Adoption events are now tracked automatically.
## Automatic Events
### Feature Adopted
Fired when a feature meets adoption criteria:
```ts
{
event: 'Feature Adopted',
properties: {
featureId: 'dark-mode',
featureName: 'Dark Mode',
category: 'customization',
useCount: 3,
daysSinceFirst: 5,
}
}
```
### Feature Churned
Fired when an adopted feature becomes inactive:
```ts
{
event: 'Feature Churned',
properties: {
featureId: 'export',
featureName: 'Export Data',
category: 'productivity',
useCount: 10,
daysSinceLastUse: 35,
}
}
```
### Feature Used
Fired every time a feature is used:
```ts
{
event: 'Feature Used',
properties: {
featureId: 'search',
featureName: 'Advanced Search',
category: 'productivity',
useCount: 7,
status: 'exploring',
}
}
```
### Nudge Shown
Fired when a nudge is displayed:
```ts
{
event: 'Nudge Shown',
properties: {
featureId: 'shortcuts',
featureName: 'Keyboard Shortcuts',
category: 'productivity',
priority: 10,
}
}
```
### Nudge Clicked
Fired when user clicks a nudge:
```ts
{
event: 'Nudge Clicked',
properties: {
featureId: 'ai-assist',
featureName: 'AI Assistant',
category: 'ai',
}
}
```
### Nudge Dismissed
Fired when user dismisses a nudge:
```ts
{
event: 'Nudge Dismissed',
properties: {
featureId: 'premium',
featureName: 'Premium Export',
category: 'export',
}
}
```
## Manual Event Tracking
### useAdoptionAnalytics Hook
For custom analytics:
```tsx
import { useAdoptionAnalytics } from '@tour-kit/adoption'
function CustomComponent() {
const analytics = useAdoptionAnalytics()
const handleAction = () => {
// Track custom event
analytics.track('Custom Adoption Event', {
featureId: 'custom',
customProperty: 'value',
})
}
return Action
}
```
The hook returns the analytics context from @tour-kit/analytics:
void',
description: 'Track custom event',
},
identify: {
type: '(userId: string, traits?: object) => void',
description: 'Identify user',
},
page: {
type: '(name?: string, properties?: object) => void',
description: 'Track page view',
},
}}
/>
### Event Builder Functions
Build event objects manually:
```tsx
import {
buildFeatureAdoptedEvent,
buildFeatureChurnedEvent,
buildFeatureUsedEvent,
buildNudgeShownEvent,
buildNudgeClickedEvent,
buildNudgeDismissedEvent,
} from '@tour-kit/adoption'
// Build event
const event = buildFeatureAdoptedEvent(feature, usage)
// { event: 'Feature Adopted', properties: { ... } }
// Send to your analytics
analytics.track(event.event, event.properties)
```
## Custom Analytics Implementation
### Without @tour-kit/analytics
Use provider callbacks for custom analytics:
```tsx
{
// Send to your analytics service
window.gtag('event', 'feature_adopted', {
feature_id: feature.id,
feature_name: feature.name,
category: feature.category,
})
}}
onChurn={(feature) => {
window.gtag('event', 'feature_churned', {
feature_id: feature.id,
feature_name: feature.name,
})
}}
onNudge={(feature, action) => {
window.gtag('event', `nudge_${action}`, {
feature_id: feature.id,
feature_name: feature.name,
})
}}
/>
```
### Tracking Usage Events
Listen to usage events manually:
```tsx
import { emitFeatureEvent } from '@tour-kit/adoption'
// Emit feature usage event
emitFeatureEvent('feature-id')
// Listen for events
window.addEventListener('feature-used', (event) => {
const { featureId } = event.detail
analytics.track('Feature Used', { featureId })
})
```
## Analytics Integrations
### Segment
```tsx
import { AnalyticsProvider, segmentPlugin } from '@tour-kit/analytics'
{children}
```
### Google Analytics
```tsx
import { AnalyticsProvider, googleAnalyticsPlugin } from '@tour-kit/analytics'
{children}
```
### Mixpanel
```tsx
import { AnalyticsProvider, mixpanelPlugin } from '@tour-kit/analytics'
{children}
```
### PostHog
```tsx
import { AnalyticsProvider, posthogPlugin } from '@tour-kit/analytics'
{children}
```
## Event Properties Reference
### Common Properties
All events include:
```ts
{
featureId: string
featureName: string
category?: string
timestamp: string // ISO 8601
}
```
### Feature Adopted
Additional properties:
```ts
{
useCount: number
daysSinceFirst: number
adoptionCriteria: {
minUses: number
recencyDays: number
}
}
```
### Feature Churned
Additional properties:
```ts
{
useCount: number
daysSinceLastUse: number
previousStatus: 'adopted'
}
```
### Feature Used
Additional properties:
```ts
{
useCount: number
status: AdoptionStatus
isFirstUse: boolean
}
```
### Nudge Events
Additional properties:
```ts
{
priority: number
description?: string
premium?: boolean
}
```
## Privacy Considerations
### Disable Tracking
```tsx
```
### Anonymize Data
```tsx
onAdoption={(feature) => {
analytics.track('Feature Adopted', {
featureId: hashFeatureId(feature.id), // Hash IDs
// Omit featureName to avoid PII
})
}
```
## Debugging
Enable debug mode to log events:
```tsx
import { AnalyticsProvider } from '@tour-kit/analytics'
{children}
```
## Best Practices
1. **Track meaningful events** - Don't over-track:
```tsx
// Good: Track adoption milestones
onAdoption={(feature) => analytics.track('Feature Adopted', { ... })}
// Bad: Track every render
useEffect(() => {
analytics.track('Component Rendered') // Too noisy
}, [])
```
2. **Include context** in event properties:
```tsx
onAdoption={(feature) => {
analytics.track('Feature Adopted', {
featureId: feature.id,
category: feature.category,
userPlan: user.plan, // Useful context
daysActive: user.daysActive,
})
}
```
3. **Use standard event names** for cross-platform analysis
4. **Test analytics** in development before production
## Next Steps
- [Analytics Package Docs](/docs/analytics) - Full analytics documentation
- [AdoptionProvider](/docs/adoption/providers/adoption-provider) - Provider callbacks
- [useAdoptionStats](/docs/adoption/hooks/use-adoption-stats) - Access metrics for custom tracking
---
# AdoptionNudge
> AdoptionNudge component: auto-showing prompt that appears when a user has not adopted a tracked feature after a threshold
## Overview
`AdoptionNudge` is a component that automatically displays nudges for unadopted features based on the nudge configuration in `AdoptionProvider`. It handles scheduling, display, and user interactions out of the box.
## Basic Usage
```tsx
import { AdoptionNudge } from '@tour-kit/adoption'
export default function App() {
return (
)
}
```
That's it! The component will automatically:
- Wait for the configured delay
- Show the highest-priority pending nudge
- Track when nudges are shown
- Handle dismissal and clicks
## Props
ReactNode',
description: 'Custom render function for complete UI control',
},
delay: {
type: 'number',
default: '5000',
description: 'Delay before showing nudge (milliseconds)',
},
position: {
type: '"bottom-right" | "bottom-left" | "top-right" | "top-left"',
default: '"bottom-right"',
description: 'Position of the nudge on screen',
},
size: {
type: '"sm" | "md" | "lg"',
default: '"md"',
description: 'Size variant',
},
asChild: {
type: 'boolean',
default: 'false',
description: 'Use custom element via Slot pattern',
},
className: {
type: 'string',
description: 'Additional CSS classes',
},
}}
/>
## Examples
### Default Nudge
The default styling provides a clean, shadcn/ui-style nudge:
```tsx
```
Renders a card with:
- Feature name as heading
- Feature description (if provided)
- "Try it" button (tracks usage and dismisses)
- "Dismiss" button (dismisses permanently)
### Custom Position
Control where the nudge appears:
```tsx
```
### Custom Delay
Show nudges immediately or with custom timing:
```tsx
{/* Show immediately */}
{/* Wait 10 seconds */}
```
### Custom Render
Take full control of the UI:
```tsx
(
{feature.name}
{feature.description}
Try Now
onSnooze(3600000)}>Remind me in 1h
Not interested
)}
/>
```
void',
description: 'Tracks usage and dismisses the nudge',
},
onDismiss: {
type: '() => void',
description: 'Dismisses the nudge permanently',
},
onSnooze: {
type: '(durationMs: number) => void',
description: 'Snoozes the nudge for specified duration',
},
}}
/>
### With Snooze Options
Add snooze functionality to default UI:
```tsx
(
{feature.name}
{feature.description}
Try it
Remind me later
onSnooze(3600000)}>1 hour
onSnooze(86400000)}>Tomorrow
onSnooze(604800000)}>Next week
Dismiss
)}
/>
```
### Animated Nudge
Add entrance/exit animations:
```tsx
import { useState, useEffect } from 'react'
function AnimatedNudge() {
const [isVisible, setIsVisible] = useState(false)
return (
{
useEffect(() => {
// Animate in
setIsVisible(true)
}, [])
const handleDismiss = () => {
setIsVisible(false)
setTimeout(onDismiss, 300) // Wait for animation
}
return (
{feature.name}
Try
✕
)
}}
/>
)
}
```
```css
.nudge-toast {
transform: translateY(100px);
opacity: 0;
transition: all 0.3s ease;
}
.nudge-toast.visible {
transform: translateY(0);
opacity: 1;
}
```
### With Tour Integration
Launch a tour when user clicks the nudge:
```tsx
import { useTour } from '@tour-kit/react'
function NudgeWithTour() {
const { startTour } = useTour()
return (
{
const handleTryIt = () => {
onClick()
if (feature.resources?.tourId) {
startTour(feature.resources.tourId)
}
}
return (
{feature.name}
{feature.resources?.tourId ? 'Take Tour' : 'Try Now'}
Dismiss
)
}}
/>
)
}
```
### Rich Content Nudge
Show images, videos, or rich formatting:
```tsx
(
{feature.category === 'video' && (
)}
{feature.name}
{feature.description}
{feature.premium && (
Premium
)}
Try it free
Dismiss
)}
/>
```
### Category-Specific Styling
Style nudges differently based on feature category:
```tsx
(
{getCategoryIcon(feature.category)}
{feature.name}
{feature.description}
Try
✕
)}
/>
```
### Progress Indicator
Show how many uses until adoption:
```tsx
import { useFeature } from '@tour-kit/adoption'
{
const { usage } = useFeature(feature.id)
const criteria = feature.adoptionCriteria || { minUses: 3 }
const progress = (usage.useCount / criteria.minUses) * 100
return (
{feature.name}
{feature.description}
{usage.useCount} / {criteria.minUses} uses
Try Now
Dismiss
)
}}
/>
```
## Styling
### Variant Styling
Use built-in size and position variants:
```tsx
```
### Custom Classes
Extend default styling:
```tsx
```
### CSS Variables
Customize via CSS variables:
```css
.adoption-nudge {
--nudge-bg: hsl(0 0% 100%);
--nudge-border: hsl(0 0% 90%);
--nudge-text: hsl(0 0% 10%);
--nudge-radius: 0.5rem;
}
```
### Using asChild
Compose with your own elements:
```tsx
import { Card } from '@/components/ui/card'
{/* Default nudge content rendered inside Card */}
```
## Behavior
### Automatic Scheduling
The component respects nudge configuration:
```tsx
```
Total delay = `nudge.initialDelay` + `AdoptionNudge.delay`
### Priority Ordering
Shows highest-priority feature first:
```tsx
const features = [
{ id: 'basic', name: 'Basic', trigger: '#basic', priority: 1 },
{ id: 'important', name: 'Important', trigger: '#important', priority: 10 },
{ id: 'critical', name: 'Critical', trigger: '#critical', priority: 100 },
]
// will show "Critical" first
```
### Dismissal
When dismissed:
1. Nudge disappears
2. Feature marked as permanently dismissed
3. Won't appear in future sessions
4. Analytics event fired (if configured)
### Click Behavior
When "Try it" is clicked:
1. Feature usage is tracked
2. Nudge is dismissed
3. `onClick` handler runs (if in custom render)
4. Analytics events fired
## Accessibility
The default nudge includes:
- Semantic HTML (`div` with proper heading hierarchy)
- Focus management (buttons are keyboard accessible)
- Clear dismiss action
- No motion if `prefers-reduced-motion` is set
### Screen Reader Support
Add ARIA attributes in custom renders:
```tsx
(
{feature.name}
{feature.description}
Try Now
Dismiss
)}
/>
```
## TypeScript
Fully typed render props:
```tsx
import { AdoptionNudge, type NudgeRenderProps, type Feature } from '@tour-kit/adoption'
{
const { feature, onClick, onDismiss, onSnooze } = props
// All typed correctly
feature.name // string
feature.description // string | undefined
onClick() // () => void
onSnooze(3600000) // (durationMs: number) => void
return ...
}}
/>
```
## Performance
The component efficiently manages state:
- Only re-renders when nudge state changes
- Automatically cleans up timers
- Memoized callbacks prevent unnecessary re-renders
For optimal performance, define render functions outside the component:
```tsx
const renderNudge = ({ feature, onClick, onDismiss }: NudgeRenderProps) => (
{feature.name}
Try
Dismiss
)
function App() {
return
}
```
## Common Patterns
### Multiple Nudge Instances
Show different nudges in different locations:
```tsx
function App() {
return (
)
}
```
### Conditional Display
Only show nudges in certain contexts:
```tsx
function ConditionalNudge() {
const { user } = useAuth()
// Don't nudge trial users
if (user.plan === 'trial') return null
// Don't nudge during onboarding
if (!user.onboardingComplete) return null
return
}
```
### Persistent Nudges
Keep nudge visible until explicitly dismissed:
```tsx
(
Don't forget to try {feature.name}!
Try
Dismiss
)}
/>
```
## Best Practices
1. **Position strategically** - Don't block critical UI:
```tsx
// Good: Bottom corner, out of the way
// Bad: Could block important content
```
2. **Provide context** in feature descriptions:
```tsx
const features = [
{
id: 'shortcuts',
name: 'Keyboard Shortcuts',
description: 'Work faster with keyboard shortcuts', // Explains value
trigger: '#shortcuts',
},
]
```
3. **Test delays** with real usage patterns
4. **Make dismissal obvious** - always provide a clear close button
5. **Respect user decisions** - dismissed means dismissed
## Next Steps
- [useNudge Hook](/docs/adoption/hooks/use-nudge) - Manual nudge control
- [FeatureButton Component](/docs/adoption/components/feature-button) - Button with tracking
- [Analytics Integration](/docs/adoption/analytics) - Track nudge interactions
---
# Conditional Components
> IfNotAdopted and IfAdopted components: conditionally render content based on whether a user has adopted a specific feature
## Overview
`IfNotAdopted` and `IfAdopted` are conditional rendering components that show/hide content based on whether a feature has been adopted.
## Basic Usage
### IfNotAdopted
Show content only if feature is not yet adopted:
```tsx
import { IfNotAdopted } from '@tour-kit/adoption'
function FeatureToggle() {
return (
Dark Mode
New!
)
}
```
### IfAdopted
Show content only if feature is adopted:
```tsx
import { IfAdopted } from '@tour-kit/adoption'
function FeatureToggle() {
return (
Keyboard Shortcuts
)
}
```
## Props
Both components share the same props:
## Examples
### Show/Hide Badges
```tsx
Feature 1
New
Feature 2
```
### With Fallback Content
```tsx
Mastered!}
>
Try this!
```
### Conditional Help Text
```tsx
Advanced Search
Use filters and operators to find exactly what you need.
Try it now to unlock powerful search capabilities!
You're a search pro! Check out keyboard shortcuts for even faster searches.
```
### Conditional UI Elements
```tsx
function Toolbar() {
return (
Learn Quick Actions
)
}
```
### Promotional Messaging
```tsx
Export to PDF
Save your work as a beautifully formatted PDF
Try Premium Export
```
### Progress Indicators
```tsx
import { useFeature } from '@tour-kit/adoption'
function FeatureCard({ featureId }: { featureId: string }) {
const { feature, usage } = useFeature(featureId)
if (!feature) return null
return (
{feature.name}
{usage.useCount} / {feature.adoptionCriteria?.minUses || 3} uses
Feature adopted! Used {usage.useCount} times
)
}
```
### Contextual Nudges
```tsx
Use Feature
Click here to try this powerful feature!
```
### Multi-Feature Conditions
```tsx
function Dashboard() {
return (
{/* Show onboarding if NO features adopted */}
{/* Show congrats if key features adopted */}
)
}
```
### Animation on State Change
```tsx
function AnimatedBadge({ featureId }: { featureId: string }) {
return (
New
✓
)
}
```
## Comparison with useFeature
These components are syntactic sugar for the `useFeature` hook:
```tsx
// Using IfNotAdopted
New
// Equivalent with useFeature
function FeatureBadge() {
const { isAdopted } = useFeature('feature')
if (isAdopted) return null
return New
}
```
**Use conditional components when:**
- You only need to show/hide content
- The logic is simple (adopted vs. not adopted)
- You want cleaner JSX
**Use `useFeature` when:**
- You need additional data (useCount, status, etc.)
- You need the `trackUsage` function
- You have complex conditional logic
## Performance
Both components are lightweight wrappers around `useFeature`:
```tsx
// ✓ Efficient: Only re-renders when adoption status changes
// ✓ Also efficient: Multiple instances share the same context
...
...
```
## TypeScript
Both components are fully typed:
```tsx
import { IfNotAdopted, IfAdopted } from '@tour-kit/adoption'
This is typed correctly
Fallback is typed}
>
Children is typed
```
## Accessibility
These components render fragments and don't affect accessibility:
```tsx
// The button remains accessible
Export
New!
```
Ensure content changes are announced to screen readers:
```tsx
Try this feature!
You've adopted this feature!
```
## Common Patterns
### Feature Discovery Flow
```tsx
function FeatureFlow({ featureId }: { featureId: string }) {
return (
Discover this feature
Learn More
You've mastered this!
Share your achievement
)
}
```
### Gamification
```tsx
Power User Unlocked!
```
### Contextual Help
```tsx
This feature helps you do X. Click to try it!
```
## Best Practices
1. **Use semantic HTML** inside conditional components:
```tsx
// Good
New
// Bad (creates unnecessary wrapper)
```
2. **Provide fallback for important UI**:
```tsx
}>
```
3. **Avoid deep nesting**:
```tsx
// Good: Use useAdoptionStats for complex conditions
const { byStatus } = useAdoptionStats()
if (byStatus.adopted.length === 0) {
return
}
// Bad: Too deeply nested
```
## Next Steps
- [useFeature Hook](/docs/adoption/hooks/use-feature) - More adoption state control
- [NewFeatureBadge](/docs/adoption/components/new-feature-badge) - Pre-built badge component
- [FeatureButton](/docs/adoption/components/feature-button) - Button with tracking
---
# FeatureButton
> FeatureButton component: button wrapper that automatically records usage events for the tracked feature on each click
## Overview
`FeatureButton` is a button wrapper that automatically tracks feature usage when clicked. It optionally shows a "new" indicator for unadopted features.
## Basic Usage
```tsx
import { FeatureButton } from '@tour-kit/adoption'
function Toolbar() {
return (
exportData()}>
Export
)
}
```
Every click automatically tracks usage - no manual `trackUsage()` calls needed.
## Props
Extends all standard `` props.
## Examples
### Basic Button
```tsx
Search
```
### Without New Indicator
```tsx
Save
```
### Different Variants
```tsx
Delete
Settings
Help
```
### With Custom Element (asChild)
```tsx
import { Button } from '@/components/ui/button'
Export Data
```
### Icon Button
```tsx
import { Download } from 'lucide-react'
```
### Loading State
```tsx
function ExportButton() {
const [loading, setLoading] = useState(false)
const handleExport = async () => {
setLoading(true)
try {
await exportData()
} finally {
setLoading(false)
}
}
return (
{loading ? 'Exporting...' : 'Export'}
)
}
```
### With Tooltip
```tsx
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
Ctrl+K
Keyboard Shortcuts
```
## New Indicator
The indicator appears as a small red dot when the feature is not adopted:
```tsx
// Shows red dot if not adopted
Try New Feature
// Hide indicator
Try New Feature
```
The indicator automatically disappears once the feature is adopted.
## Click Handling
The button tracks usage before calling your `onClick`:
```tsx
{
// Usage already tracked at this point
console.log('Button clicked')
}}
>
View Analytics
```
Tracking happens even if `onClick` is not provided:
```tsx
Submit Form
```
## Accessibility
The component:
- Renders a semantic `` by default
- Includes `aria-label` on the new indicator: "New feature"
- Supports all standard button ARIA attributes
- Works with keyboard navigation
```tsx
Export
```
## TypeScript
Fully typed with standard button props:
```tsx
import { FeatureButton, type FeatureButtonProps } from '@tour-kit/adoption'
const props: FeatureButtonProps = {
featureId: 'my-feature',
onClick: (e: React.MouseEvent) => {
console.log('clicked')
},
disabled: false,
type: 'button',
}
Click
```
## Styling
### CSS Variables
```css
.feature-button {
--button-bg: hsl(0 0% 10%);
--button-text: hsl(0 0% 100%);
--indicator-color: hsl(0 84% 60%);
}
```
### Custom Classes
```tsx
Full Width Button
```
## Best Practices
1. **Use for feature-specific actions**, not generic buttons:
```tsx
// Good
Generate with AI
// Bad (too generic)
Click Me
```
2. **Provide meaningful feature IDs**:
```tsx
// Good
featureId="pdf-export"
// Bad
featureId="btn-1"
```
3. **Don't track non-feature actions**:
```tsx
// Good: Feature-specific
Dark Mode
// Bad: Should use regular button
Close
```
## Next Steps
- [useFeature Hook](/docs/adoption/hooks/use-feature) - Manual tracking control
- [NewFeatureBadge](/docs/adoption/components/new-feature-badge) - Badge component
- [IfNotAdopted](/docs/adoption/components/conditional) - Conditional rendering
---
# NewFeatureBadge
> NewFeatureBadge component: badge overlay that highlights unadopted features and auto-hides after the user engages with them
## Overview
`NewFeatureBadge` displays a badge (default: "New") for features that haven't been adopted yet. It automatically hides once the feature is adopted.
## Basic Usage
```tsx
import { NewFeatureBadge } from '@tour-kit/adoption'
function FeatureButton() {
return (
Toggle Dark Mode
)
}
```
## Props
Extends all standard `` props.
## Examples
### Default Badge
```tsx
// Renders: New
```
### Custom Text
```tsx
```
### Different Variants
```tsx
```
### Different Sizes
```tsx
```
### In Navigation
```tsx
function NavBar() {
return (
Dashboard
Analytics
AI Tools
)
}
```
### In Dropdown Menu
```tsx
import {
DropdownMenu,
DropdownMenuItem,
} from '@/components/ui/dropdown-menu'
Export to PDF
Advanced Filters
```
### In Feature Cards
```tsx
function FeatureCard({ featureId, title, description }) {
return (
)
}
```
### With Icons
```tsx
import { Sparkles } from 'lucide-react'
{/* NewFeatureBadge renders text only. Compose an icon beside it: */}
AI Assistant
```
### Animated Badge
```tsx
```
### Positioned Badge
```tsx
Advanced Search
```
## Styling
### Custom Classes
```tsx
```
### CSS Variables
Customize badge appearance:
```css
.new-feature-badge {
--badge-bg: hsl(142 76% 36%);
--badge-text: hsl(0 0% 100%);
--badge-radius: 0.25rem;
--badge-padding: 0.125rem 0.5rem;
}
```
### Variant Styling
The badge uses class-variance-authority (cva) for variants:
```tsx
// Default: Green background
// Secondary: Gray background
// Outline: Transparent with border
// Destructive: Red background
```
## Behavior
The badge automatically:
1. Renders when feature is not adopted
2. Returns `null` when feature is adopted
3. Re-renders when adoption status changes
```tsx
// Initially shows badge
// After user adopts the feature
// → Badge disappears automatically
```
## Accessibility
The badge renders a semantic ``:
```tsx
```
For screen readers, consider adding context:
```tsx
Export
```
## TypeScript
Fully typed:
```tsx
import { NewFeatureBadge, type NewFeatureBadgeProps } from '@tour-kit/adoption'
const badgeProps: NewFeatureBadgeProps = {
featureId: 'my-feature',
text: 'New',
variant: 'default',
size: 'md',
className: 'custom-class',
}
```
## Comparison with Conditional Components
```tsx
// Using NewFeatureBadge (simpler for badges)
// Using IfNotAdopted (more flexible)
New
```
**Use NewFeatureBadge when:**
- You want a simple badge with minimal code
- You want consistent badge styling
- You don't need custom badge content
**Use IfNotAdopted when:**
- You need fully custom content
- You want to show complex UI
- You need a fallback
## Performance
The badge efficiently re-renders only when adoption status changes:
```tsx
// ✓ Efficient: Only updates when "export" is adopted
// ✓ Also efficient: Multiple badges don't cause extra re-renders
```
## Common Patterns
### Category Badges
```tsx
const CATEGORY_BADGES = {
ai: { text: 'AI', variant: 'default' as const },
premium: { text: 'Pro', variant: 'secondary' as const },
beta: { text: 'Beta', variant: 'outline' as const },
}
function FeatureItem({ featureId, category }) {
const badge = CATEGORY_BADGES[category]
return (
Feature Name
{badge && (
)}
)
}
```
### Pulsing Badge
```tsx
```
### Count-Based Badge
```tsx
import { useFeature } from '@tour-kit/adoption'
function SmartBadge({ featureId }: { featureId: string }) {
const { usage, feature } = useFeature(featureId)
const remaining = (feature?.adoptionCriteria?.minUses || 3) - usage.useCount
return (
)
}
```
## Best Practices
1. **Use consistent text** across your app:
```tsx
// Good: Consistent
// Bad: Inconsistent
```
2. **Don't overuse badges** - they lose impact:
```tsx
// Good: Strategic use
Home
Features
About
// Bad: Too many badges
Home
Features
About
```
3. **Consider badge placement**:
```tsx
// Good: Badge after text
Export Data
// Also good: Badge as superscript
Export Data
```
## Next Steps
- [IfNotAdopted Component](/docs/adoption/components/conditional) - More flexible conditional rendering
- [FeatureButton](/docs/adoption/components/feature-button) - Button with built-in tracking
- [useFeature Hook](/docs/adoption/hooks/use-feature) - Access adoption state programmatically
---
# Dashboard Components
> Pre-built admin dashboard components for visualizing feature adoption metrics, trends, and per-feature engagement rates
## Overview
The @tour-kit/adoption package includes pre-built dashboard components for admin interfaces. These components integrate with `useAdoptionStats` to visualize feature adoption data.
## Available Components
| Component | Description |
|-----------|-------------|
| `AdoptionDashboard` | Full-featured dashboard with all components |
| `AdoptionStatsGrid` | Grid of stat cards showing key metrics |
| `AdoptionStatCard` | Individual metric card |
| `AdoptionTable` | Sortable table of features and their status |
| `AdoptionCategoryChart` | Bar chart showing adoption by category |
| `AdoptionStatusBadge` | Status indicator badge |
| `AdoptionFilters` | Filter controls for the dashboard |
## Quick Start
### Full Dashboard
```tsx
import { AdoptionDashboard } from '@tour-kit/adoption'
export default function AdminPage() {
return (
Feature Adoption Dashboard
)
}
```
### Custom Dashboard
Compose individual components:
```tsx
import {
AdoptionStatsGrid,
AdoptionCategoryChart,
AdoptionTable,
} from '@tour-kit/adoption'
export default function CustomDashboard() {
return (
)
}
```
## Component Overview
### AdoptionStatsGrid
Shows overview metrics in a grid:
```tsx
```
Displays:
- Total features
- Adopted features
- Adoption rate
- Features by status (not started, exploring, adopted, churned)
### AdoptionStatCard
Individual stat card:
```tsx
```
### AdoptionTable
Sortable table of all features:
```tsx
```
Columns:
- Feature name
- Category (optional)
- Status with badge
- Use count
- Last used date
- Priority (optional)
### AdoptionCategoryChart
Bar chart showing adoption by category:
```tsx
```
### AdoptionStatusBadge
Status indicator badge:
```tsx
```
### AdoptionFilters
Filter controls:
```tsx
const [filters, setFilters] = useState({
status: 'all',
category: 'all',
search: '',
})
```
## Styling
All components follow shadcn/ui conventions:
```tsx
// Custom classes
// Size variants
// Color variants
```
## Data Integration
Components automatically use `useAdoptionStats()`:
```tsx
// No props needed - uses context
// Or provide custom data
import { useAdoptionStats } from '@tour-kit/adoption'
function CustomView() {
const stats = useAdoptionStats()
// Filter or transform data
const premiumStats = {
...stats,
features: stats.features.filter(f => f.premium),
}
return
}
```
## Examples
### Admin Dashboard Page
```tsx
import { AdoptionDashboard, AdoptionFilters } from '@tour-kit/adoption'
export default function AdminDashboard() {
const [filters, setFilters] = useState({
status: 'all',
category: 'all',
search: '',
})
return (
)
}
```
### Metrics Overview
```tsx
import { AdoptionStatsGrid, AdoptionCategoryChart } from '@tour-kit/adoption'
export function MetricsPage() {
return (
)
}
```
### Export Dashboard Data
```tsx
import { useAdoptionStats } from '@tour-kit/adoption'
import { AdoptionDashboard } from '@tour-kit/adoption'
export function ExportableDashboard() {
const stats = useAdoptionStats()
const exportCSV = () => {
const csv = [
['Feature', 'Category', 'Status', 'Uses', 'Last Used'],
...stats.features.map(f => [
f.name,
f.category || '-',
f.usage.status,
f.usage.useCount,
f.usage.lastUsed || 'Never',
]),
].map(row => row.join(',')).join('\n')
// Download CSV
const blob = new Blob([csv], { type: 'text/csv' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'adoption-metrics.csv'
a.click()
}
return (
)
}
```
## Accessibility
All dashboard components include:
- Semantic HTML (`table`, `figure`, `dl`, etc.)
- ARIA labels for screen readers
- Keyboard navigation support
- Focus indicators
```tsx
```
## TypeScript
Fully typed props:
```tsx
import type {
AdoptionDashboardProps,
AdoptionStatCardProps,
AdoptionTableProps,
AdoptionFiltersState,
} from '@tour-kit/adoption'
const dashboardProps: AdoptionDashboardProps = {
className: 'my-dashboard',
showFilters: true,
}
const filterState: AdoptionFiltersState = {
status: 'all',
category: 'all',
search: '',
}
```
## Best Practices
1. **Use the full dashboard** for admin pages:
```tsx
```
2. **Compose individual components** for custom layouts:
```tsx
```
3. **Add filtering** for large feature sets:
```tsx
```
4. **Export data** for reporting:
```tsx
const stats = useAdoptionStats()
// Export stats.features as CSV/JSON
```
## Next Steps
---
# AdoptionDashboard
> AdoptionDashboard component: complete admin view combining stats grid, category chart, and feature table in one layout
## Overview
`AdoptionDashboard` is a complete, drop-in dashboard component that displays all adoption metrics. It combines stats grid, charts, tables, and filters into a single component.
## Basic Usage
```tsx
import { AdoptionDashboard } from '@tour-kit/adoption'
export default function AdminPage() {
return (
)
}
```
## Props
## Examples
### Minimal Dashboard
```tsx
```
### Stats Only
```tsx
```
### With Custom Wrapper
```tsx
```
## Next Steps
- [AdoptionStatsGrid](/docs/adoption/dashboard/stats) - Metrics grid
- [AdoptionTable](/docs/adoption/dashboard/table) - Feature table
- [AdoptionCategoryChart](/docs/adoption/dashboard/charts) - Charts
---
# Charts
> AdoptionCategoryChart and AdoptionStatusBadge: visualize adoption distribution by category and individual feature status
## AdoptionCategoryChart
Bar chart showing adoption rate by feature category.
### Basic Usage
```tsx
import { AdoptionCategoryChart } from '@tour-kit/adoption'
```
### Props
### Examples
#### Horizontal Chart
```tsx
```
#### Highlight Category
```tsx
```
#### Without Percentages
```tsx
```
## AdoptionStatusBadge
Status indicator badge for adoption states.
### Basic Usage
```tsx
import { AdoptionStatusBadge } from '@tour-kit/adoption'
```
### Props
### Examples
#### All Statuses
```tsx
```
#### Different Sizes
```tsx
```
#### Without Icon
```tsx
```
### Status Colors
- **not_started**: Gray
- **exploring**: Blue
- **adopted**: Green
- **churned**: Red
## Next Steps
- [AdoptionTable](/docs/adoption/dashboard/table) - Feature table
- [useAdoptionStats](/docs/adoption/hooks/use-adoption-stats) - Access chart data
---
# Adoption Funnel
> Step-by-step adoption funnel with drop-off percentages — Pendo/Userpilot parity, native CSS, no chart peer dependency.
`` renders a vertical funnel chart with retention and
drop-off between adjacent steps. It is **data-first**: hand it pre-computed
`steps` and it works without any provider. Inside an
``, pair it with `useFunnelData({ featureIds })` for a
one-line in-provider integration.
## Provider-less Usage (Recommended)
Compute funnel data from your analytics layer and pass it in directly:
```tsx
import { AdoptionFunnel } from '@tour-kit/adoption'
import '@tour-kit/adoption/styles/funnel.css'
const steps = [
{ id: 'view', label: 'Viewed', entered: 1000, completed: 720 },
{ id: 'click', label: 'Clicked CTA', entered: 720, completed: 380 },
{ id: 'signup', label: 'Signed Up', entered: 380, completed: 240 },
]
console.log('drilldown:', step, index)}
/>
```
This path needs no provider, so the funnel can live in any tree —
admin dashboards, server-rendered reports, etc.
## In-provider Usage with `useFunnelData`
```tsx
import {
AdoptionFunnel,
AdoptionProvider,
useFunnelData,
} from '@tour-kit/adoption'
function OnboardingFunnel() {
const steps = useFunnelData({
featureIds: ['view-pricing', 'start-trial', 'invite-team'],
labels: {
'view-pricing': 'Viewed Pricing',
'start-trial': 'Started Trial',
'invite-team': 'Invited Team',
},
})
return
}
```
## `` Props
void',
description:
'Click/keyboard activation handler. Steps become focusable (role="button", tabIndex=0) when provided; Enter and Space both fire the callback.',
},
emptyState: {
type: 'React.ReactNode',
description:
'Replaces the default "No funnel data yet." message when `steps` is empty.',
},
ariaLabel: {
type: 'string',
description:
'Overrides the auto-generated summary on the `role="img"` chart element.',
},
className: {
type: 'string',
description: 'Extra class merged onto the root.',
},
}}
/>
## `FunnelStep` Shape
## `useFunnelData` Input/Output
>',
description:
'Override the visible label per feature ID. Falls back to `feature.name` then the raw id.',
},
}}
/>
Returns `FunnelStep[]` ready to pass to ``. The
source data is in-memory and synchronous, so the hook returns the steps
directly — no `loading`/`error` envelope.
## Accessibility
- The chart is exposed to assistive tech as a single `role="img"` element with
an auto-generated `aria-label` summarizing the funnel
(`Adoption funnel: 100 → 60 → 30, 30% end-to-end retention`). Override via
the `ariaLabel` prop.
- Each bar carries `aria-hidden="true"` — screen readers receive the same
numbers through a visually-hidden `` mirror placed alongside the
visual list.
- Clickable steps are keyboard-activatable. **Enter** and **Space** both
fire `onStepClick`; tab order matches visual order.
- Honors `prefers-reduced-motion: reduce` — the hover-transition is disabled
under the OS preference. See the
[reduced-motion guide](/docs/guides/reduced-motion).
## Styling
Import the base stylesheet once at your app root:
```ts
import '@tour-kit/adoption/styles/funnel.css'
```
CSS custom properties for theming:
```css
:root {
--tour-funnel-gap: 0.5rem;
--tour-funnel-step-gap: 0.5rem;
--tour-funnel-bar-bg: hsl(220 90% 56%);
--tour-funnel-bar-fg: #fff;
--tour-funnel-retention-fg: hsl(0 0% 40%);
--tour-funnel-step-hover: rgba(0, 0, 0, 0.04);
--tour-funnel-focus-ring: hsl(220 90% 56%);
}
```
## Next Steps
- [AdoptionDashboard](/docs/adoption/dashboard/adoption-dashboard) — full
dashboard component
- [useAdoptionStats](/docs/adoption/hooks/use-adoption-stats) — underlying
per-feature stats hook
- [useFunnelData](/docs/adoption/hooks/use-funnel-data) — in-provider hook
used in the second example above
- [Reduced-motion guide](/docs/guides/reduced-motion) — cross-package
motion contract
---
# Stat Cards
> AdoptionStatCard and AdoptionStatsGrid: display aggregate adoption metrics with trend indicators and percentage changes
## Overview
Display adoption metrics as stat cards. Use `AdoptionStatsGrid` for a full overview or `AdoptionStatCard` for individual metrics.
## AdoptionStatsGrid
Displays all key metrics in a responsive grid.
### Basic Usage
```tsx
import { AdoptionStatsGrid } from '@tour-kit/adoption'
```
Shows:
- Total features
- Adopted features
- Adoption rate percentage
- Features by status (not started, exploring, adopted, churned)
### Props
### Example
```tsx
```
## AdoptionStatCard
Individual metric card.
### Basic Usage
```tsx
import { AdoptionStatCard } from '@tour-kit/adoption'
```
### Props
### Examples
#### With Trend
```tsx
```
#### With Icon
```tsx
import { TrendingUp } from 'lucide-react'
}
variant="success"
/>
```
#### Custom Metrics
```tsx
import { useAdoptionStats } from '@tour-kit/adoption'
function CustomStats() {
const stats = useAdoptionStats()
const churnRate = (
(stats.byStatus.churned.length / stats.totalCount) * 100
).toFixed(1)
return (
)
}
```
## Next Steps
- [AdoptionDashboard](/docs/adoption/dashboard/adoption-dashboard) - Full dashboard
- [useAdoptionStats](/docs/adoption/hooks/use-adoption-stats) - Access raw data
---
# AdoptionTable
> AdoptionTable component: sortable, filterable table of tracked features showing adoption status, usage count, and trends
## Overview
`AdoptionTable` displays all features in a sortable table with status badges, use counts, and timestamps.
## Basic Usage
```tsx
import { AdoptionTable } from '@tour-kit/adoption'
```
## Props
void',
description: 'Row click handler',
},
className: {
type: 'string',
description: 'Additional CSS classes',
},
}}
/>
## Examples
### Sorted Table
```tsx
```
### With Row Click
```tsx
{
console.log('Clicked:', feature.name)
// Navigate to feature details, etc.
}}
/>
```
### Show All Columns
```tsx
```
### Filtered Features
```tsx
import { useAdoptionStats } from '@tour-kit/adoption'
function ChurnedFeaturesTable() {
const { byStatus } = useAdoptionStats()
return (
)
}
```
## Table Columns
Default columns:
- **Name**: Feature name
- **Status**: Adoption status badge
- **Uses**: Total use count
- **Last Used**: Relative time (e.g., "2 days ago")
Optional columns:
- **Category**: Feature category (if `showCategory={true}`)
- **Priority**: Numeric priority (if `showPriority={true}`)
## Accessibility
The table includes:
- Semantic `` element
- `` for screen readers
- Sortable column headers with `aria-sort`
- Row selection with keyboard
```tsx
```
## Next Steps
- [AdoptionStatusBadge](/docs/adoption/dashboard/charts) - Status badges
- [useAdoptionStats](/docs/adoption/hooks/use-adoption-stats) - Filter data
---
# useAdoptionContext
> Low-level hook returning the raw AdoptionProvider context — features, usage map, nudge state, and full action surface
Low-level access to the raw `AdoptionContext` value. Most consumers should use [`useFeature`](/docs/adoption/hooks/use-feature) or [`useNudge`](/docs/adoption/hooks/use-nudge) — `useAdoptionContext` is the escape hatch when you need everything at once (custom dashboards, debugging, headless integrations).
`useAdoptionContext` throws if no `` is mounted above.
## Usage
```tsx
import { useAdoptionContext } from '@tour-kit/adoption';
function FeatureMatrix() {
const { features, usageMap, pendingNudges } = useAdoptionContext();
return (
{features.map((f) => (
{f.name}
{usageMap[f.id]?.count ?? 0}
{pendingNudges.includes(f) ? 'Pending nudge' : 'OK'}
))}
);
}
```
## Return Value
| Field | Type | Description |
|-------|------|-------------|
| `features` | `Feature[]` | All registered features |
| `usageMap` | `Record` | Per-feature usage records |
| `nudgeState` | `NudgeState` | Nudge persistence state (snoozed, dismissed, last shown) |
| `pendingNudges` | `Feature[]` | Features whose nudge is currently due |
| `trackUsage` | `(featureId: string) => void` | Manually record a feature usage event |
| `getFeature` | `(featureId: string) => FeatureWithUsage \| null` | Resolve a feature + its usage |
| `showNudge` | `(featureId: string) => void` | Force-show a nudge |
| `dismissNudge` | `(featureId: string) => void` | Persist a dismissal |
| `snoozeNudge` | `(featureId: string, durationMs: number) => void` | Hide for a duration |
---
# useAdoptionStats
> useAdoptionStats hook: retrieve aggregated adoption metrics across all tracked features for dashboards and reporting
## Overview
`useAdoptionStats` provides aggregated statistics across all features, including adoption rates, feature counts by status, and category breakdowns. Use this hook to build dashboards and analytics views.
## Basic Usage
```tsx
import { useAdoptionStats } from '@tour-kit/adoption'
function AdoptionOverview() {
const stats = useAdoptionStats()
return (
)
}
```
## Return Value
',
description: 'Features grouped by status (not_started, exploring, adopted, churned)',
},
byCategory: {
type: 'Record',
description: 'Features grouped by category with adoption stats',
},
}}
/>
## Examples
### Dashboard Overview
Build a comprehensive dashboard:
```tsx
function AdoptionDashboard() {
const stats = useAdoptionStats()
return (
Feature Adoption
{stats.adoptedCount} of {stats.totalCount} features adopted
({stats.adoptionRate.toFixed(1)}%)
)
}
```
### Category Breakdown
Show adoption by feature category:
```tsx
function CategoryBreakdown() {
const { byCategory } = useAdoptionStats()
return (
Adoption by Category
{Object.entries(byCategory).map(([category, stats]) => (
{category}
{stats.adopted} / {stats.total} adopted ({stats.rate.toFixed(1)}%)
))}
)
}
```
### Feature List by Status
Display features grouped by their adoption status:
```tsx
function FeatureStatusList() {
const { byStatus } = useAdoptionStats()
return (
)
}
function FeatureGroup({ title, features, description }) {
return (
{title}
{description}
{features.map((f) => (
{f.name} - {f.usage.useCount} uses
))}
)
}
```
### Adoption Funnel
Visualize the adoption funnel:
```tsx
function AdoptionFunnel() {
const { totalCount, byStatus } = useAdoptionStats()
const stages = [
{
name: 'Total Features',
count: totalCount,
percentage: 100,
},
{
name: 'Discovered',
count: totalCount - byStatus.not_started.length,
percentage: ((totalCount - byStatus.not_started.length) / totalCount) * 100,
},
{
name: 'Exploring',
count: byStatus.exploring.length,
percentage: (byStatus.exploring.length / totalCount) * 100,
},
{
name: 'Adopted',
count: byStatus.adopted.length,
percentage: (byStatus.adopted.length / totalCount) * 100,
},
]
return (
{stages.map((stage, index) => (
{stage.name}
{stage.count}
))}
)
}
```
### Top Features
Show most/least adopted features:
```tsx
function TopFeatures() {
const { features } = useAdoptionStats()
const sortedByUsage = [...features].sort(
(a, b) => b.usage.useCount - a.usage.useCount
)
const mostUsed = sortedByUsage.slice(0, 5)
const leastUsed = sortedByUsage.slice(-5).reverse()
return (
Most Used Features
{mostUsed.map((f) => (
{f.name}: {f.usage.useCount} uses
))}
Least Used Features
{leastUsed.map((f) => (
{f.name}: {f.usage.useCount} uses
))}
)
}
```
### Adoption Score
Calculate a custom adoption health score:
```tsx
function AdoptionScore() {
const { byStatus, totalCount } = useAdoptionStats()
// Weighted scoring
const score =
(byStatus.adopted.length * 100 +
byStatus.exploring.length * 50 +
byStatus.churned.length * -50) /
totalCount
const getScoreColor = (score: number) => {
if (score >= 75) return 'green'
if (score >= 50) return 'yellow'
if (score >= 25) return 'orange'
return 'red'
}
const getScoreLabel = (score: number) => {
if (score >= 75) return 'Excellent'
if (score >= 50) return 'Good'
if (score >= 25) return 'Fair'
return 'Needs Improvement'
}
return (
{score.toFixed(0)}
{getScoreLabel(score)}
)
}
```
### Time-Based Analysis
Show features by recency:
```tsx
function RecentlyUsedFeatures() {
const { features } = useAdoptionStats()
const withLastUsed = features.filter((f) => f.usage.lastUsed)
const sorted = [...withLastUsed].sort((a, b) => {
const dateA = new Date(a.usage.lastUsed!).getTime()
const dateB = new Date(b.usage.lastUsed!).getTime()
return dateB - dateA
})
const recentlyUsed = sorted.slice(0, 10)
return (
Recently Used Features
{recentlyUsed.map((f) => {
const lastUsed = new Date(f.usage.lastUsed!)
const daysAgo = Math.floor(
(Date.now() - lastUsed.getTime()) / (1000 * 60 * 60 * 24)
)
return (
{f.name}
{daysAgo === 0
? 'Today'
: daysAgo === 1
? 'Yesterday'
: `${daysAgo} days ago`}
)
})}
)
}
```
## Filtering and Grouping
### Filter by Premium Status
```tsx
function PremiumFeatureStats() {
const { features } = useAdoptionStats()
const premiumFeatures = features.filter((f) => f.premium)
const premiumAdopted = premiumFeatures.filter(
(f) => f.usage.status === 'adopted'
).length
const premiumAdoptionRate =
premiumFeatures.length > 0
? (premiumAdopted / premiumFeatures.length) * 100
: 0
return (
Premium Features
{premiumAdopted} of {premiumFeatures.length} adopted
({premiumAdoptionRate.toFixed(1)}%)
)
}
```
### Custom Grouping
Group by priority tiers:
```tsx
function PriorityTiers() {
const { features } = useAdoptionStats()
const byPriority = {
high: features.filter((f) => (f.priority || 0) >= 10),
medium: features.filter((f) => {
const p = f.priority || 0
return p >= 5 && p < 10
}),
low: features.filter((f) => (f.priority || 0) < 5),
}
return (
{Object.entries(byPriority).map(([tier, tierFeatures]) => {
const adopted = tierFeatures.filter(
(f) => f.usage.status === 'adopted'
).length
return (
{tier} Priority
{adopted} / {tierFeatures.length} adopted
)
})}
)
}
```
## Performance
The hook uses `useMemo` to prevent recalculating stats on every render:
```tsx
// ✓ Efficient: Stats recalculated only when features or usage changes
function Dashboard() {
const stats = useAdoptionStats()
return {stats.adoptionRate}%
}
// ✓ Also efficient: Destructure only what you need
function SimpleStats() {
const { adoptionRate, adoptedCount } = useAdoptionStats()
return {adoptionRate}%
}
```
For very large feature sets (100+ features), consider memoizing filtered results:
```tsx
function LargeFeatureSet() {
const stats = useAdoptionStats()
const topFeatures = useMemo(
() =>
[...stats.features]
.sort((a, b) => b.usage.useCount - a.usage.useCount)
.slice(0, 10),
[stats.features]
)
return
}
```
## TypeScript
Fully typed return value:
```tsx
import { useAdoptionStats, type AdoptionStats } from '@tour-kit/adoption'
function TypedDashboard() {
const stats: AdoptionStats = useAdoptionStats()
// ✓ Type-safe
stats.byStatus.adopted.forEach((feature) => {
console.log(feature.name, feature.usage.useCount)
})
// ✓ Type-safe
Object.entries(stats.byCategory).forEach(([category, data]) => {
console.log(category, data.rate)
})
}
```
## Accessibility
When building dashboards with these stats:
```tsx
function AccessibleDashboard() {
const stats = useAdoptionStats()
return (
Adoption Overview
Adoption Rate
{stats.adoptionRate.toFixed(1)}%
Adopted Features
{stats.adoptedCount} of {stats.totalCount}
)
}
```
## Common Patterns
### Empty State
Handle cases with no features:
```tsx
function Dashboard() {
const stats = useAdoptionStats()
if (stats.totalCount === 0) {
return (
)
}
return
}
```
### Comparison View
Compare current vs. previous periods:
```tsx
function AdoptionComparison() {
const currentStats = useAdoptionStats()
const previousStats = usePreviousAdoptionStats() // Your custom hook
const change = currentStats.adoptionRate - previousStats.adoptionRate
return (
Adoption Rate
{currentStats.adoptionRate.toFixed(1)}%
= 0 ? 'positive' : 'negative'}>
{change >= 0 ? '↑' : '↓'} {Math.abs(change).toFixed(1)}%
)
}
```
### Export Data
Allow exporting stats:
```tsx
function ExportButton() {
const stats = useAdoptionStats()
const exportCSV = () => {
const csv = [
['Feature', 'Category', 'Status', 'Use Count', 'Last Used'],
...stats.features.map((f) => [
f.name,
f.category || 'uncategorized',
f.usage.status,
f.usage.useCount,
f.usage.lastUsed || 'Never',
]),
]
.map((row) => row.join(','))
.join('\n')
const blob = new Blob([csv], { type: 'text/csv' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'adoption-stats.csv'
a.click()
}
return Export to CSV
}
```
## Best Practices
1. **Memoize expensive calculations**:
```tsx
const topAdopted = useMemo(
() => stats.features.filter((f) => f.usage.status === 'adopted'),
[stats.features]
)
```
2. **Use semantic HTML for charts**:
```tsx
Adoption by Category
```
3. **Provide context for numbers**:
```tsx
// Good: Shows context
{stats.adoptedCount} of {stats.totalCount} features adopted
// Less helpful: Just a number
{stats.adoptedCount}
```
## Next Steps
- [Dashboard Components](/docs/adoption/dashboard) - Pre-built dashboard UI
- [AdoptionDashboard](/docs/adoption/dashboard/adoption-dashboard) - Full dashboard component
- [Analytics Integration](/docs/adoption/analytics) - Track adoption in your analytics platform
---
# useFeature
> useFeature hook: track individual feature usage, query adoption state, and record interactions for adoption metrics
## Overview
`useFeature` provides access to a single feature's adoption state and a method to manually track usage. Use this hook when you need programmatic control over when usage is tracked.
## Basic Usage
```tsx
import { useFeature } from '@tour-kit/adoption'
function ExportButton() {
const { trackUsage, isAdopted, useCount } = useFeature('export')
const handleExport = async () => {
await exportUserData()
trackUsage() // Track after successful export
}
return (
Export Data {!isAdopted && {useCount}/3 }
)
}
```
## Return Value
void',
description: 'Manually track a feature usage',
},
}}
/>
## Examples
### Conditional UI Based on Adoption
Show different UI based on adoption status:
```tsx
function AIAssistantButton() {
const { isAdopted, status, useCount } = useFeature('ai-assistant')
if (status === 'not_started') {
return (
Try AI Assistant
New!
)
}
if (status === 'exploring') {
return (
AI Assistant
{useCount}/5 uses
)
}
return AI Assistant
}
```
### Track on Success Only
Don't track failed attempts:
```tsx
function SaveButton() {
const { trackUsage } = useFeature('auto-save')
const handleSave = async () => {
try {
await saveDocument()
trackUsage() // Only track successful saves
} catch (error) {
showError('Save failed')
// Don't track failures
}
}
return Save
}
```
### Progress Indicators
Show adoption progress:
```tsx
function FeatureCard({ featureId }: { featureId: string }) {
const { feature, usage, isAdopted } = useFeature(featureId)
if (!feature) return null
const criteria = feature.adoptionCriteria || { minUses: 3 }
const progress = Math.min((usage.useCount / criteria.minUses) * 100, 100)
return (
{feature.name}
{isAdopted ? (
Adopted
) : (
{usage.useCount} / {criteria.minUses} uses
)}
)
}
```
### Complex Tracking Logic
Track based on specific conditions:
```tsx
function AdvancedSearchForm() {
const { trackUsage } = useFeature('advanced-search')
const [filters, setFilters] = useState({})
const handleSearch = () => {
const isAdvancedSearch =
Object.keys(filters).length > 1 || // Multiple filters
filters.dateRange || // Date filtering
filters.regex // Regex search
if (isAdvancedSearch) {
trackUsage() // Only track when using advanced features
}
performSearch(filters)
}
return
}
```
### Gamification
Show achievement-style feedback:
```tsx
function KeyboardShortcuts() {
const { useCount, trackUsage } = useFeature('keyboard-shortcuts')
const [showCelebration, setShowCelebration] = useState(false)
const handleShortcut = (e: KeyboardEvent) => {
if (e.metaKey && e.key === 'k') {
const previousCount = useCount
trackUsage()
// Show celebration on milestones
if ([1, 5, 10, 25, 50].includes(previousCount + 1)) {
setShowCelebration(true)
}
}
}
return (
<>
{/* Your app */}
{showCelebration && (
)}
>
)
}
```
## Adoption Status Flow
Features transition through these states:
```
not_started
↓ (first use)
exploring
↓ (reaches minUses)
adopted
↓ (exceeds recencyDays without use)
churned
```
### Checking Status
```tsx
function FeatureStatus({ featureId }: { featureId: string }) {
const { status, usage, feature } = useFeature(featureId)
const getMessage = () => {
switch (status) {
case 'not_started':
return 'Try this feature to get started!'
case 'exploring':
const remaining = (feature?.adoptionCriteria?.minUses || 3) - usage.useCount
return `${remaining} more uses to unlock`
case 'adopted':
return 'You've mastered this feature!'
case 'churned':
const lastUsed = new Date(usage.lastUsed!)
return `Last used ${formatDistanceToNow(lastUsed)} ago`
}
}
return {getMessage()}
}
```
## Usage Timestamps
Access when a feature was first/last used:
```tsx
function FeatureTimeline({ featureId }: { featureId: string }) {
const { usage } = useFeature(featureId)
if (!usage.firstUsed) {
return Not yet used
}
const daysSinceFirst = Math.floor(
(Date.now() - new Date(usage.firstUsed).getTime()) / (1000 * 60 * 60 * 24)
)
const daysSinceLast = usage.lastUsed
? Math.floor((Date.now() - new Date(usage.lastUsed).getTime()) / (1000 * 60 * 60 * 24))
: null
return (
First used {daysSinceFirst} days ago
{daysSinceLast !== null &&
Last used {daysSinceLast} days ago
}
Total uses: {usage.useCount}
)
}
```
## Feature Not Found
Handle missing features gracefully:
```tsx
function DynamicFeature({ featureId }: { featureId: string }) {
const { feature, trackUsage } = useFeature(featureId)
if (!feature) {
console.warn(`Feature not found: ${featureId}`)
return null
}
return (
{feature.name}
)
}
```
The hook returns `feature: null` if the ID doesn't match any feature in the provider.
## Performance Considerations
The hook uses `useMemo` internally to prevent unnecessary recalculations:
```tsx
// ✓ Efficient: Memoized, only updates when usage changes
function MyComponent() {
const { isAdopted } = useFeature('my-feature')
return {isAdopted ? 'Adopted' : 'Not adopted'}
}
// ✓ Also efficient: trackUsage is a stable callback
function MyButton() {
const { trackUsage } = useFeature('my-feature')
return Click
}
```
## TypeScript
Fully typed return values:
```tsx
import { useFeature, type UseFeatureReturn, type AdoptionStatus } from '@tour-kit/adoption'
function TypedComponent() {
const result: UseFeatureReturn = useFeature('search')
const status: AdoptionStatus = result.status // ✓ Type-safe
const isAdopted: boolean = result.isAdopted // ✓ Type-safe
}
```
## Accessibility
The hook:
- Does not affect DOM or ARIA attributes
- Provides data for you to render accessible UI
- Works with screen readers (usage counts can be announced)
Best practices:
```tsx
function AccessibleFeature() {
const { isAdopted, useCount } = useFeature('feature')
return (
Feature
)
}
```
## Common Patterns
### Loading States
Handle async operations:
```tsx
function AsyncFeature() {
const { trackUsage } = useFeature('async-feature')
const [loading, setLoading] = useState(false)
const handleClick = async () => {
setLoading(true)
try {
await performAsyncOperation()
trackUsage()
} finally {
setLoading(false)
}
}
return (
{loading ? 'Loading...' : 'Perform Action'}
)
}
```
### Debounced Tracking
Avoid tracking rapid repeated uses:
```tsx
import { useDebouncedCallback } from 'use-debounce'
function SearchInput() {
const { trackUsage } = useFeature('search')
const debouncedTrack = useDebouncedCallback(
() => trackUsage(),
2000 // Track once per 2 seconds max
)
const handleSearch = (query: string) => {
performSearch(query)
debouncedTrack()
}
return handleSearch(e.target.value)} />
}
```
### Feature Gates
Combine with feature flags:
```tsx
function FeatureGate({ featureId, children }) {
const { feature } = useFeature(featureId)
const isEnabled = useFeatureFlag(featureId)
if (!isEnabled || !feature) {
return null
}
return children
}
```
## Best Practices
1. **Track meaningful interactions**, not page views:
```tsx
// Good: Tracks actual usage
const { trackUsage } = useFeature('export')
{ exportData(); trackUsage() }}>Export
// Bad: Tracks just rendering
useEffect(() => {
trackUsage() // This tracks every render
}, [])
```
2. **Use stable feature IDs**:
```tsx
// Good: Constant ID
const FEATURE_ID = 'dark-mode'
const { trackUsage } = useFeature(FEATURE_ID)
// Bad: Dynamic ID might cause issues
const { trackUsage } = useFeature(`feature-${Math.random()}`)
```
3. **Don't over-track**:
```tsx
// Good: Track once per meaningful action
onClick={() => {
saveDocument()
trackUsage()
}}
// Bad: Tracking every keystroke
onChange={(e) => {
setInput(e.target.value)
trackUsage() // Too frequent!
}}
```
## Next Steps
- [useAdoptionStats Hook](/docs/adoption/hooks/use-adoption-stats) - Aggregate adoption metrics
- [IfNotAdopted Component](/docs/adoption/components/conditional) - Conditional rendering based on adoption
- [Analytics Integration](/docs/adoption/analytics) - Track adoption events automatically
---
# useFunnelData
> Derive a current-state adoption funnel from useAdoptionStats — feed straight into AdoptionFunnel.
`useFunnelData` is a convenience selector that projects the current user's
adoption state — read via `useAdoptionStats` — into a `FunnelStep[]` ready
to hand to ``. Must be used inside ``.
## Usage
```tsx
import {
AdoptionFunnel,
AdoptionProvider,
useFunnelData,
} from '@tour-kit/adoption'
function ActivationFunnel() {
const steps = useFunnelData({
featureIds: ['view-pricing', 'start-trial', 'invite-team'],
labels: {
'view-pricing': 'Viewed Pricing',
'start-trial': 'Started Trial',
'invite-team': 'Invited Team',
},
})
return
}
```
## Input
>',
description:
'Override the visible label per feature ID. Falls back to `feature.name`, then the raw id.',
},
}}
/>
## Output
Returns `FunnelStep[]` directly — one step per `featureIds` entry, in input
order. Pass straight to ``.
## Mapping Semantics
For each `featureIds[i]`, the hook reads the matching `FeatureWithUsage`
from `useAdoptionStats()` and produces:
- `entered` = `usage.useCount` — number of times the current user has touched
the feature.
- `completed` = `usage.useCount` when `usage.status === 'adopted'`, else `0` —
100% per-user conversion once adopted; 0% before.
Unknown feature IDs (no matching feature in the provider) get
`entered: 0, completed: 0`.
## See Also
- [``](/docs/adoption/dashboard/funnel) — the consumer component
- [`useAdoptionStats`](/docs/adoption/hooks/use-adoption-stats) — underlying
current-state stats hook
---
# useNudge
> useNudge hook: manage nudge visibility, dismissal, snooze, and user interaction state for feature discovery prompts
## Overview
`useNudge` provides access to the nudge system, allowing you to show, dismiss, and snooze nudges for features. The hook integrates with the automatic nudge scheduler configured in `AdoptionProvider`.
## Basic Usage
```tsx
import { useNudge } from '@tour-kit/adoption'
function NudgeManager() {
const { pendingNudges, dismissNudge } = useNudge()
if (pendingNudges.length === 0) return null
const feature = pendingNudges[0]
return (
Try {feature.name}!
dismissNudge(feature.id)}>Dismiss
)
}
```
## Return Value
void',
description: 'Mark a nudge as shown (tracks in analytics)',
},
dismissNudge: {
type: '(featureId: string) => void',
description: 'Permanently dismiss a nudge',
},
snoozeNudge: {
type: '(featureId: string, durationMs: number) => void',
description: 'Temporarily dismiss a nudge for specified duration',
},
handleNudgeClick: {
type: '(featureId: string) => void',
description: 'Handle nudge click (tracks usage and dismisses)',
},
}}
/>
## Examples
### Simple Nudge Display
Show the highest-priority pending nudge:
```tsx
function SimpleNudge() {
const { pendingNudges, handleNudgeClick, dismissNudge } = useNudge()
if (pendingNudges.length === 0) return null
const feature = pendingNudges[0]
return (
{feature.name}
{feature.description || 'Try this new feature!'}
handleNudgeClick(feature.id)}>
Try it now
dismissNudge(feature.id)}>
Dismiss
)
}
```
### Nudge with Snooze
Allow users to postpone nudges:
```tsx
function SnoozeableNudge() {
const { pendingNudges, handleNudgeClick, dismissNudge, snoozeNudge } = useNudge()
if (!pendingNudges[0]) return null
const feature = pendingNudges[0]
const handleSnooze = (hours: number) => {
snoozeNudge(feature.id, hours * 60 * 60 * 1000)
}
return (
Discover {feature.name}
handleNudgeClick(feature.id)}>
Try Now
handleSnooze(1)}>Remind me in 1 hour
handleSnooze(24)}>Remind me tomorrow
dismissNudge(feature.id)}>
Don't show again
)
}
```
### Multi-Nudge Display
Show multiple nudges at once:
```tsx
function MultiNudgeList() {
const { pendingNudges, handleNudgeClick, dismissNudge } = useNudge()
if (pendingNudges.length === 0) {
return You've discovered all features!
}
return (
Features to Try ({pendingNudges.length})
{pendingNudges.map((feature) => (
{feature.name}
{feature.description &&
{feature.description}
}
{feature.category && (
{feature.category}
)}
handleNudgeClick(feature.id)}>Try
dismissNudge(feature.id)}>✕
))}
)
}
```
### Animated Nudge
Show nudges with entrance/exit animations:
```tsx
function AnimatedNudge() {
const { pendingNudges, showNudge, dismissNudge } = useNudge()
const [visible, setVisible] = useState(false)
const [currentFeature, setCurrentFeature] = useState(null)
useEffect(() => {
if (pendingNudges.length > 0 && !currentFeature) {
const feature = pendingNudges[0]
setCurrentFeature(feature)
showNudge(feature.id) // Track nudge shown
// Animate in after slight delay
setTimeout(() => setVisible(true), 100)
}
}, [pendingNudges, currentFeature, showNudge])
const handleDismiss = () => {
if (!currentFeature) return
setVisible(false)
setTimeout(() => {
dismissNudge(currentFeature.id)
setCurrentFeature(null)
}, 300) // Wait for exit animation
}
if (!currentFeature) return null
return (
{currentFeature.name}
Dismiss
)
}
```
### Contextual Nudge Placement
Show nudges near related UI elements:
```tsx
function FeatureButton({ featureId }: { featureId: string }) {
const { pendingNudges, handleNudgeClick, dismissNudge } = useNudge()
const shouldNudge = pendingNudges.some((f) => f.id === featureId)
return (
Feature Button
{shouldNudge && (
Try this feature!
handleNudgeClick(featureId)}>OK
dismissNudge(featureId)}>Dismiss
)}
)
}
```
### Nudge with Tour Integration
Launch a tour when user clicks the nudge:
```tsx
function NudgeWithTour() {
const { pendingNudges, handleNudgeClick, dismissNudge } = useNudge()
const { startTour } = useTour() // From @tour-kit/react
if (pendingNudges.length === 0) return null
const feature = pendingNudges[0]
const handleTryIt = () => {
handleNudgeClick(feature.id)
// Launch tour if available
if (feature.resources?.tourId) {
startTour(feature.resources.tourId)
}
}
return (
Learn about {feature.name}
{feature.resources?.tourId ? 'Take Tour' : 'Try Now'}
dismissNudge(feature.id)}>Dismiss
)
}
```
### Nudge Scheduling UI
Show when nudges will appear:
```tsx
function NudgeSchedule() {
const { pendingNudges } = useNudge()
const nudgeConfig = useNudgeConfig() // Your custom hook to get config
return (
Upcoming Nudges
{pendingNudges.length === 0 ? (
No pending nudges
) : (
{pendingNudges.slice(0, 5).map((feature, index) => (
{feature.name}
Priority: {feature.priority || 0}
))}
)}
Nudge cooldown: {nudgeConfig.cooldown / 3600000}h
Max per session: {nudgeConfig.maxPerSession}
)
}
```
## Nudge Priority
Pending nudges are automatically sorted by priority (highest first):
```tsx
function PriorityNudges() {
const { pendingNudges } = useNudge()
return (
{pendingNudges.map((feature, index) => (
#{index + 1}
{feature.name}
Priority: {feature.priority || 0}
))}
)
}
```
Features with higher `priority` values appear first in `pendingNudges`.
## Tracking Nudge Interactions
### Manual Tracking
Use `showNudge` to track when a nudge is displayed:
```tsx
function TrackedNudge() {
const { pendingNudges, showNudge } = useNudge()
useEffect(() => {
if (pendingNudges.length > 0) {
const feature = pendingNudges[0]
showNudge(feature.id) // Tracks "nudge shown" event
}
}, [pendingNudges, showNudge])
// ... render nudge
}
```
### Click Tracking
`handleNudgeClick` automatically:
1. Tracks feature usage
2. Dismisses the nudge
3. Fires analytics events (if configured)
```tsx
handleNudgeClick(feature.id)}>
Try Feature
```
This is equivalent to:
```tsx
const { trackUsage } = useFeature(feature.id)
const { dismissNudge } = useNudge()
{
trackUsage()
dismissNudge(feature.id)
}}
>
Try Feature
```
## Snooze Duration
Common snooze durations:
```tsx
const SNOOZE_DURATIONS = {
'1 hour': 60 * 60 * 1000,
'4 hours': 4 * 60 * 60 * 1000,
'1 day': 24 * 60 * 60 * 1000,
'1 week': 7 * 24 * 60 * 60 * 1000,
}
function CustomSnooze() {
const { snoozeNudge, pendingNudges } = useNudge()
if (!pendingNudges[0]) return null
return (
Remind me:
{Object.entries(SNOOZE_DURATIONS).map(([label, duration]) => (
snoozeNudge(pendingNudges[0].id, duration)}
>
{label}
))}
)
}
```
## Conditional Nudging
Only show nudges under certain conditions:
```tsx
function ConditionalNudge() {
const { pendingNudges } = useNudge()
const user = useUser()
// Don't nudge trial users
if (user.plan === 'trial') return null
// Don't nudge during onboarding
if (user.onboardingComplete === false) return null
// Don't nudge if user is busy
if (user.isEditing) return null
return
}
```
## Performance
The hook efficiently manages nudge state:
```tsx
// ✓ Efficient: Only re-renders when pendingNudges changes
function OptimizedNudge() {
const { pendingNudges } = useNudge()
return
}
// ✓ Also efficient: Actions are stable callbacks
function StableActions() {
const { dismissNudge, snoozeNudge } = useNudge()
// These callbacks won't cause re-renders
return (
)
}
```
## TypeScript
Fully typed:
```tsx
import { useNudge, type UseNudgeReturn, type Feature } from '@tour-kit/adoption'
function TypedNudge() {
const nudge: UseNudgeReturn = useNudge()
nudge.pendingNudges.forEach((feature: Feature) => {
console.log(feature.name)
})
nudge.dismissNudge('feature-id') // ✓ Type-safe
nudge.snoozeNudge('feature-id', 3600000) // ✓ Type-safe
}
```
## Accessibility
Best practices for accessible nudges:
```tsx
function AccessibleNudge() {
const { pendingNudges, dismissNudge } = useNudge()
if (!pendingNudges[0]) return null
const feature = pendingNudges[0]
return (
Try {feature.name}
{feature.description}
handleNudgeClick(feature.id)}
aria-describedby="nudge-desc"
>
Try Now
dismissNudge(feature.id)}
aria-label={`Dismiss ${feature.name} suggestion`}
>
Dismiss
)
}
```
## Common Patterns
### Nudge Counter
Show how many nudges are pending:
```tsx
function NudgeCounter() {
const { hasNudges, pendingNudges } = useNudge()
if (!hasNudges) return null
return (
New Features
{pendingNudges.length}
)
}
```
### Dismiss All
Allow dismissing all nudges at once:
```tsx
function DismissAllButton() {
const { pendingNudges, dismissNudge } = useNudge()
const handleDismissAll = () => {
pendingNudges.forEach((feature) => {
dismissNudge(feature.id)
})
}
if (pendingNudges.length === 0) return null
return (
Dismiss all ({pendingNudges.length})
)
}
```
### Nudge History
Track which nudges were dismissed:
```tsx
function NudgeHistory() {
const [dismissed, setDismissed] = useState([])
const { dismissNudge } = useNudge()
const handleDismiss = (featureId: string) => {
dismissNudge(featureId)
setDismissed((prev) => [...prev, featureId])
}
return (
Dismissed: {dismissed.length} features
{/* Your nudge UI */}
)
}
```
## Best Practices
1. **Limit nudge frequency** to avoid overwhelming users:
```tsx
nudge: {
cooldown: 86400000, // 24 hours minimum
maxPerSession: 2,
maxFeatures: 1,
}
```
2. **Respect user dismissals** - don't re-show dismissed nudges
3. **Provide context** - explain why the feature is useful
4. **Make dismissal easy** - always provide a clear way to close nudges
5. **Test nudge timing** - ensure nudges appear at appropriate moments
## Next Steps
- [AdoptionNudge Component](/docs/adoption/components/adoption-nudge) - Pre-built nudge UI
- [Analytics Integration](/docs/adoption/analytics) - Track nudge events
- [AdoptionProvider](/docs/adoption/providers/adoption-provider) - Configure nudge behavior
---
# AdoptionProvider
> AdoptionProvider: configure feature definitions, usage thresholds, nudge rules, and storage for the adoption tracking system
## Overview
`AdoptionProvider` is the root context provider that manages feature tracking, usage persistence, and nudge scheduling. Wrap your application with this provider to enable adoption tracking.
## Basic Usage
```tsx title="app/layout.tsx"
import { AdoptionProvider } from '@tour-kit/adoption'
const features = [
{
id: 'dark-mode',
name: 'Dark Mode',
trigger: '#dark-mode-toggle',
},
{
id: 'export',
name: 'Export Data',
trigger: { event: 'export:complete' },
},
]
export default function RootLayout({ children }) {
return (
{children}
)
}
```
## Props
void',
description: 'Callback when a feature becomes adopted',
},
onChurn: {
type: '(feature: Feature) => void',
description: 'Callback when a feature churns (was adopted, now inactive)',
},
onNudge: {
type: '(feature: Feature, action: "shown" | "clicked" | "dismissed") => void',
description: 'Callback for nudge interactions',
},
}}
/>
## Feature Configuration
Each feature in the `features` array defines what to track and when it's considered adopted.
### Basic Feature
```tsx
const feature = {
id: 'search',
name: 'Search',
trigger: '[data-feature="search"]',
}
```
### Complete Feature Definition
```tsx
const feature = {
id: 'advanced-search',
name: 'Advanced Search',
trigger: '#advanced-search-btn',
adoptionCriteria: {
minUses: 5,
recencyDays: 30,
},
category: 'productivity',
description: 'Find exactly what you need with filters and operators',
priority: 10,
premium: true,
resources: {
tourId: 'advanced-search-tour',
hintIds: ['search-filter-hint', 'search-operator-hint'],
},
}
```
## Trigger Types
### CSS Selector (Click Tracking)
Track clicks on elements matching the selector:
```tsx
const feature = {
id: 'sidebar',
name: 'Sidebar',
trigger: '[data-sidebar-toggle]',
}
```
The provider automatically sets up event delegation to track clicks. Elements can be added/removed dynamically.
### Custom Event
Track custom events dispatched in your code:
```tsx
const feature = {
id: 'pdf-export',
name: 'PDF Export',
trigger: { event: 'pdf:exported' },
}
// In your component
async function handleExport() {
await generatePDF()
window.dispatchEvent(new CustomEvent('pdf:exported'))
}
```
### Callback (Polling)
For complex conditions, provide a callback that's polled periodically:
```tsx
const feature = {
id: 'full-screen',
name: 'Full Screen Mode',
trigger: {
callback: () => document.fullscreenElement !== null,
},
}
```
## Storage Configuration
### LocalStorage (Default)
```tsx
```
### Memory Storage (Testing)
For tests or temporary sessions:
```tsx
```
### Custom Storage Adapter
Integrate with your own storage:
```tsx
const customStorage = {
getItem: async (key: string) => {
return await db.settings.get(key)
},
setItem: async (key: string, value: string) => {
await db.settings.set(key, value)
},
removeItem: async (key: string) => {
await db.settings.delete(key)
},
}
```
## Nudge Configuration
Control when and how nudges are shown:
```tsx
```
### Disabling Nudges
To track adoption without showing nudges:
```tsx
```
## Event Callbacks
Track adoption milestones in your analytics:
```tsx
{
analytics.track('Feature Adopted', {
featureId: feature.id,
featureName: feature.name,
category: feature.category,
})
}}
onChurn={(feature) => {
analytics.track('Feature Churned', {
featureId: feature.id,
featureName: feature.name,
})
}}
onNudge={(feature, action) => {
analytics.track('Nudge Interaction', {
featureId: feature.id,
action, // 'shown', 'clicked', or 'dismissed'
})
}}
/>
```
## Multi-User Tracking
Use the `userId` prop to track adoption per user:
```tsx
function App() {
const { userId } = useAuth()
return (
{children}
)
}
```
This is essential for:
- Multi-user applications
- Different adoption states per user
- User-specific nudge history
## TypeScript
The provider is fully typed:
```tsx
import type { Feature, AdoptionProviderProps } from '@tour-kit/adoption'
const features: Feature[] = [
{
id: 'feature-1',
name: 'Feature 1',
trigger: '#btn',
},
]
const config: AdoptionProviderProps = {
features,
storage: { type: 'localStorage' },
nudge: { enabled: true },
onAdoption: (feature) => {
// feature is typed as Feature
},
}
```
## Accessibility
The provider:
- Does not render any UI itself (fully headless)
- Tracks usage without interfering with user interactions
- Supports keyboard navigation when using trigger selectors
- Respects `prefers-reduced-motion` for nudge animations
## Best Practices
1. **Define features at the module level** to prevent re-initialization:
```tsx
// Good: Stable reference
const features = [{ id: 'search', name: 'Search', trigger: '#search' }]
function App() {
return ...
}
// Bad: New array every render
function App() {
return (
...
)
}
```
2. **Use specific selectors** to avoid false positives:
```tsx
// Good
trigger: '[data-feature="export-pdf"]'
// Bad (too generic, might match other buttons)
trigger: 'button'
```
3. **Set appropriate cooldowns** to avoid annoying users:
```tsx
nudge: {
cooldown: 86400000, // 24 hours minimum
maxPerSession: 2, // Don't overwhelm
}
```
4. **Test adoption criteria** with real usage patterns:
```tsx
// Too aggressive: Users will hit this accidentally
adoptionCriteria: { minUses: 1 }
// Better: Indicates deliberate usage
adoptionCriteria: { minUses: 3, recencyDays: 30 }
```
## Next Steps
- [useFeature Hook](/docs/adoption/hooks/use-feature) - Track individual features
- [useNudge Hook](/docs/adoption/hooks/use-nudge) - Manage nudge state
- [Analytics Integration](/docs/adoption/analytics) - Automatic analytics tracking
---
# TypeScript Types
> TypeScript types for FeatureConfig, AdoptionState, NudgeRule, UsageThreshold, and the @tour-kit/adoption API surface
## Core Types
### Feature
```ts
interface Feature {
id: string
name: string
trigger: FeatureTrigger
adoptionCriteria?: AdoptionCriteria
resources?: FeatureResources
priority?: number
category?: string
description?: string
premium?: boolean
}
```
### FeatureTrigger
```ts
type FeatureTrigger =
| string // CSS selector
| { event: string } // Custom event
| { callback: () => boolean } // Programmatic check
```
**Examples:**
```ts
// CSS selector (click tracking)
trigger: '#dark-mode-toggle'
trigger: '[data-feature="export"]'
// Custom event
trigger: { event: 'export:complete' }
trigger: { event: 'ai:generated' }
// Callback (polled every 1s)
trigger: { callback: () => document.fullscreenElement !== null }
```
### AdoptionCriteria
```ts
interface AdoptionCriteria {
minUses?: number // default: 3
recencyDays?: number // default: 30
custom?: (usage: FeatureUsage) => boolean
}
```
boolean',
description: 'Custom adoption logic',
},
}}
/>
### FeatureResources
```ts
interface FeatureResources {
tourId?: string
hintIds?: string[]
}
```
### FeatureUsage
```ts
interface FeatureUsage {
featureId: string
firstUsed: string | null // ISO date string
lastUsed: string | null // ISO date string
useCount: number
status: AdoptionStatus
}
```
### AdoptionStatus
```ts
type AdoptionStatus = 'not_started' | 'exploring' | 'adopted' | 'churned'
```
- **not_started**: Never used
- **exploring**: Used, but below `minUses`
- **adopted**: Meets adoption criteria
- **churned**: Was adopted, now inactive (exceeds `recencyDays`)
### FeatureWithUsage
```ts
interface FeatureWithUsage extends Feature {
usage: FeatureUsage
}
```
Combines feature definition with current usage state.
## Provider Types
### AdoptionProviderProps
```ts
interface AdoptionProviderProps {
children: React.ReactNode
features: Feature[]
storage?: StorageConfig
nudge?: NudgeConfig
userId?: string
onAdoption?: (feature: Feature) => void
onChurn?: (feature: Feature) => void
onNudge?: (feature: Feature, action: 'shown' | 'clicked' | 'dismissed') => void
}
```
### StorageConfig
```ts
type StorageConfig =
| { type: 'localStorage'; key?: string }
| { type: 'memory' }
| { type: 'custom'; adapter: StorageAdapter }
```
### StorageAdapter
```ts
interface StorageAdapter {
getItem(key: string): Promise | string | null
setItem(key: string, value: string): Promise | void
removeItem(key: string): Promise | void
}
```
### NudgeConfig
```ts
interface NudgeConfig {
enabled?: boolean // default: true
initialDelay?: number // default: 5000
cooldown?: number // default: 86400000 (24h)
maxPerSession?: number // default: 3
maxFeatures?: number // default: 1
}
```
## Hook Return Types
### UseFeatureReturn
```ts
interface UseFeatureReturn {
feature: Feature | null
usage: FeatureUsage
isAdopted: boolean
status: AdoptionStatus
useCount: number
trackUsage: () => void
}
```
### AdoptionStats
```ts
interface AdoptionStats {
features: FeatureWithUsage[]
adoptionRate: number
adoptedCount: number
totalCount: number
byStatus: Record
byCategory: Record
}
```
### UseNudgeReturn
```ts
interface UseNudgeReturn {
pendingNudges: Feature[]
hasNudges: boolean
showNudge: (featureId: string) => void
dismissNudge: (featureId: string) => void
snoozeNudge: (featureId: string, durationMs: number) => void
handleNudgeClick: (featureId: string) => void
}
```
## Component Prop Types
### AdoptionNudgeProps
```ts
interface AdoptionNudgeProps extends React.ComponentPropsWithoutRef<'div'> {
render?: (props: NudgeRenderProps) => React.ReactNode
delay?: number
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
size?: 'sm' | 'md' | 'lg'
asChild?: boolean
}
```
### NudgeRenderProps
```ts
interface NudgeRenderProps {
feature: Feature
onDismiss: () => void
onSnooze: (durationMs: number) => void
onClick: () => void
}
```
### FeatureButtonProps
```ts
interface FeatureButtonProps extends React.ComponentPropsWithoutRef<'button'> {
featureId: string
showNewIndicator?: boolean
variant?: 'default' | 'secondary' | 'outline' | 'ghost' | 'destructive'
size?: 'sm' | 'md' | 'lg'
asChild?: boolean
}
```
### NewFeatureBadgeProps
```ts
interface NewFeatureBadgeProps extends React.ComponentPropsWithoutRef<'span'> {
featureId: string
text?: string
variant?: 'default' | 'secondary' | 'outline' | 'destructive'
size?: 'sm' | 'md' | 'lg'
}
```
## Dashboard Component Types
### AdoptionDashboardProps
```ts
interface AdoptionDashboardProps {
showFilters?: boolean
showStats?: boolean
showChart?: boolean
showTable?: boolean
className?: string
}
```
### AdoptionStatCardProps
```ts
interface AdoptionStatCardProps {
title: string
value: string | number
description?: string
trend?: number
trendLabel?: string
icon?: React.ReactNode
variant?: 'default' | 'success' | 'warning' | 'danger'
size?: 'sm' | 'md' | 'lg'
}
```
### AdoptionTableProps
```ts
interface AdoptionTableProps {
sortBy?: 'name' | 'status' | 'useCount' | 'lastUsed' | 'category'
sortOrder?: 'asc' | 'desc'
showCategory?: boolean
showPriority?: boolean
features?: FeatureWithUsage[]
onRowClick?: (feature: FeatureWithUsage) => void
className?: string
}
```
### AdoptionCategoryChartProps
```ts
interface AdoptionCategoryChartProps {
showPercentages?: boolean
highlightCategory?: string
orientation?: 'horizontal' | 'vertical'
className?: string
}
```
### AdoptionStatusBadgeProps
```ts
interface AdoptionStatusBadgeProps {
status: AdoptionStatus
size?: 'sm' | 'md' | 'lg'
showIcon?: boolean
className?: string
}
```
### AdoptionFiltersProps
```ts
interface AdoptionFiltersProps {
filters: AdoptionFiltersState
onChange: (filters: AdoptionFiltersState) => void
categories?: string[]
className?: string
}
```
### AdoptionFiltersState
```ts
interface AdoptionFiltersState {
status: AdoptionStatus | 'all'
category: string | 'all'
search: string
}
```
## Usage Examples
### Typed Feature Array
```ts
import type { Feature } from '@tour-kit/adoption'
const features: Feature[] = [
{
id: 'dark-mode',
name: 'Dark Mode',
trigger: '#dark-mode-toggle',
category: 'customization',
adoptionCriteria: {
minUses: 3,
recencyDays: 30,
},
},
]
```
### Typed Hook Usage
```ts
import { useFeature, type UseFeatureReturn } from '@tour-kit/adoption'
function Component() {
const feature: UseFeatureReturn = useFeature('my-feature')
feature.trackUsage() // ✓ Type-safe
feature.isAdopted // ✓ Type-safe
}
```
### Typed Component Props
```ts
import type { FeatureButtonProps } from '@tour-kit/adoption'
const buttonProps: FeatureButtonProps = {
featureId: 'export',
variant: 'default',
onClick: () => console.log('clicked'),
}
```
## Dashboard Component Types
### AdoptionStatsGridProps
Props for the `` dashboard widget.
```ts
interface AdoptionStatsGridProps
extends React.ComponentPropsWithoutRef<'div'>,
AdoptionStatsGridVariants {
/** Custom stat cards to include alongside defaults */
customStats?: AdoptionStatCardProps[]
/** Hide the built-in default stats */
hideDefaults?: boolean
}
```
## Variants Types
CVA-derived variant prop types. Use these when extending or wrapping the styled components.
| Type | Companion variant fn |
|------|----------------------|
| `AdoptionNudgeVariants` | [`adoptionNudgeVariants`](/docs/adoption/components/adoption-nudge#variants) |
| `FeatureButtonVariants` | [`featureButtonVariants`](/docs/adoption/components/feature-button#variants) |
| `NewFeatureBadgeVariants` | [`newFeatureBadgeVariants`](/docs/adoption/components/new-feature-badge#variants) |
```ts
import type { AdoptionNudgeVariants } from '@tour-kit/adoption'
type Size = AdoptionNudgeVariants['size'] // 'default' | 'sm' | 'lg' | ...
```
## Cross-Cutting Types
| Type | Notes |
|------|-------|
| `UILibrary` | `'radix-ui' \| 'base-ui'` — see [Unified Slot](/docs/guides/unified-slot) |
## Next Steps
- [AdoptionProvider](/docs/adoption/providers/adoption-provider) - Provider setup
- [Hooks](/docs/adoption/hooks/use-feature) - Hook usage
- [Components](/docs/adoption/components/adoption-nudge) - Component props
---
# @tour-kit/ai
> AI-powered chat assistant for product tours — context-aware conversation with RAG and CAG documentation retrieval modes
`@tour-kit/ai` adds a conversational AI layer on top of your product tours. It ships React hooks, drop-in chat components, and a route-handler factory that connects to OpenAI, Anthropic, Google, or any other model exposed through the [Vercel AI SDK](https://sdk.vercel.ai/). Tours, checklists, and announcements that the user is currently looking at are passed to the model automatically, so the assistant can answer questions like "what does this step do?" or "how do I skip ahead?" without any extra wiring.
This is a Pro-tier package. The free `@tour-kit/core` and `@tour-kit/react` packages do not include AI chat.
## Pick a retrieval strategy first
Every `@tour-kit/ai` integration has to answer one question: how does the model know about your product? Three approaches, ranked from simplest to most scalable:
| Strategy | Best for | Setup time | Per-request cost | Scales to |
|----------|----------|------------|-------------------|-----------|
| **No context** — bare `useAiChat` | Generic chat widget, no product knowledge needed | 5 min | ~1–2k tokens | n/a |
| **CAG** — context stuffing via `strategy: 'context-stuffing'` | Tours under ~20 steps, single product, small docs | 15 min | ~3–8k tokens (full context every request) | ~50 docs |
| **RAG** — vector search via `strategy: 'rag'` | Large documentation sets, multi-product, evolving content | 1–2 hours | ~2–4k tokens (retrieved chunks only) | Thousands of docs |
If you are unsure: start with CAG. The migration to RAG only requires swapping the server-side `strategy` field and adding an embedding store — your client code does not change.
## Architecture
The package is split into two entry points to keep server-only secrets (API keys, vector stores, embedding clients) out of the browser bundle:
```text
@tour-kit/ai # Client: React hooks, providers, components
@tour-kit/ai/server # Server: route handler factory, RAG pipeline, embeddings
```
A minimal integration looks like this:
```text
┌────────────────────────┐ ┌──────────────────────────┐
│ AiChatProvider │ │ createChatRouteHandler │
│ · useAiChat │ POST │ · model: openai/... │
│ · useTourAssistant │ ──────► │ · strategy: cag | rag │
│ · AiChatPanel │ │ · instructions: {...} │
└────────────────────────┘ └──────────────────────────┘
React app Next.js / API route
```
## What ships in the package
**Hooks** (client) — `useAiChat`, `useAiChatContext`, `useTourAssistant`, `useSuggestions`, `useOptionalSuggestions`. See the [Hooks reference](/docs/ai/hooks).
**Components** (client) — `AiChatProvider`, `AiChatPanel`, `AiChatToggle`, `AiChatHeader`, `AiChatMessageList`, `AiChatMessage`, `AiChatInput`, `AiChatSuggestions`, `AiChatPortal`. All shadcn/ui-compatible. See [Components](/docs/ai/components).
**Server** (Node.js / Edge) — `createChatRouteHandler`, embedding/retrieval utilities for the RAG pipeline, configurable instructions (tone, boundaries, product name).
**Built-ins** — Client-side and server-side rate limiting, AI-generated follow-up question chips via `useSuggestions`, and automatic tour-state context assembly via `useTourAssistant`.
## When NOT to use this package
- **You only need a "Get help" link.** A plain mailto: link or a Discord invite is free, deterministic, and won't hallucinate. AI chat is worth the cost when self-serve answers are bottlenecking your activation funnel.
- **Your product is regulated (healthcare, finance, legal).** LLMs hallucinate. Use `@tour-kit/ai` for non-binding guidance only; route account-changing actions through a human or a deterministic workflow.
- **You can't run a server route.** `@tour-kit/ai` requires a server entry point to hold the model API key. Pure static sites or client-only React apps need a third-party proxy (Vercel, Cloudflare Workers, your own backend).
## Next steps
- [Quick Start](/docs/ai/quick-start) — install, configure a provider, and ship a working chat widget in five minutes
- [CAG Guide](/docs/ai/cag-guide) — context-augmented generation, with decision criteria vs RAG and token-cost math
- [RAG Guide](/docs/ai/rag-guide) — retrieval-augmented generation, with embedding-model and vector-store options
- [Tour Integration](/docs/ai/tour-integration) — connect the assistant to live tour state with `useTourAssistant`
- [Components](/docs/ai/components) — drop-in shadcn/ui chat UI
- [Hooks](/docs/ai/hooks) — every public hook with signatures and examples
- [API Reference](/docs/ai/api-reference) — complete public API surface
---
# API Reference
> Complete API reference for @tour-kit/ai: client hooks, server utilities, chat components, and configuration types
Complete public API surface for `@tour-kit/ai`, split by entry point. Server-only utilities live under `@tour-kit/ai/server` so they don't leak into the browser bundle. Types referenced here are exported from the same entry point unless otherwise noted.
## Entry points
| Import path | Runs in | Contains |
|-------------|---------|----------|
| `@tour-kit/ai` | Browser + Node | Provider, hooks, components, types, client-side rate limiter |
| `@tour-kit/ai/server` | Node / Edge only | `createChatRouteHandler`, RAG pipeline, embedding utilities, server rate limiter |
| `@tour-kit/ai/headless` | Browser + Node | Headless primitive variants (no styling) |
## Client Exports (`@tour-kit/ai`)
### Provider
#### ``
Root provider. Must wrap any component that uses `useAiChat` or `useTourAssistant`. Connects to the server route specified in `config.endpoint`.
| Config field | Type | Default | Notes |
|--------------|------|---------|-------|
| `endpoint` | `string` | — | Required. URL of the `createChatRouteHandler` route (e.g. `/api/chat`). |
| `tourContext` | `boolean` | `false` | When `true`, includes the active tour's `TourAssistantContext` in every request. Required for `useTourAssistant` server-side context. |
| `initialMessages` | `UIMessage[]` | `[]` | Pre-populate the chat history. |
| `rateLimiter` | `SlidingWindowRateLimiter \| null` | `null` | Optional client-side rate limiter. Returns 429 to `sendMessage` when over budget. |
### Hooks
#### `useAiChat(): UseAiChatReturn`
Core chat hook. Must be used within `AiChatProvider`.
| Property | Type | Description |
|----------|------|-------------|
| `messages` | `UIMessage[]` | Chat message history |
| `sendMessage` | `(input: { text: string }) => void` | Send a user message |
| `status` | `ChatStatus` | `'ready' \| 'submitted' \| 'streaming' \| 'error'` |
| `error` | `Error \| null` | Current error, if any |
| `stop` | `() => void` | Stop the current generation |
| `reload` | `() => void` | Regenerate the last response |
| `setMessages` | `(messages: UIMessage[]) => void` | Replace message history (pass `[]` to clear) |
| `isOpen` | `boolean` | Whether the chat panel is open |
| `open` | `() => void` | Open the chat panel |
| `close` | `() => void` | Close the chat panel |
| `toggle` | `() => void` | Toggle the chat panel |
Throws if called outside an ``.
#### `useAiChatContext(): AiChatContextValue`
Lower-level escape hatch — returns the raw context value. Use `useAiChat` instead unless you're building a custom hook.
#### `useTourAssistant()`
Tour-aware chat hook. Extends `useAiChat` with tour context.
Returns `UseTourAssistantReturn` (extends `UseAiChatReturn`):
| Property | Type | Description |
|----------|------|-------------|
| `tourContext` | `TourAssistantContext` | Current tour state |
| `isLoading` | `boolean` | Whether the assistant is loading |
| `suggestions` | `string[]` | Contextual suggestions based on current step |
| `askAboutStep` | `() => void` | Ask the AI about the current tour step |
| `askForHelp` | `(topic?: string) => void` | Ask for help, optionally on a topic |
| _...all `UseAiChatReturn` properties_ | | |
#### `useSuggestions(): UseSuggestionsReturn`
AI-generated follow-up suggestions based on the current conversation. Throws if no provider is mounted.
| Property | Type | Description |
|----------|------|-------------|
| `suggestions` | `string[]` | Current suggestions |
| `isLoading` | `boolean` | Loading state |
#### `useOptionalSuggestions(): UseSuggestionsReturn | null`
Same as `useSuggestions` but returns `null` instead of throwing when no provider is mounted. Use in optional/conditional UI.
### Components
All components are shadcn/ui-compatible and accept standard `className` overrides.
| Component | Purpose |
|-----------|---------|
| `` | Full chat surface — header, message list, input, suggestions |
| `` | Floating button that toggles the panel |
| `` | Header bar — title, close button, optional menu |
| `` | Renders `messages[]` with autoscroll |
| `` | Single message row (user or assistant) |
| `` | Text input + send button + status indicator |
| `` | Renders suggested follow-up chips |
| `` | Portal wrapper for the panel (used internally by ``) |
See [Components](/docs/ai/components) for prop tables.
### Client utilities
#### `SlidingWindowRateLimiter`
Class implementing sliding-window rate limiting. Pass an instance to `AiChatProvider.config.rateLimiter` to enforce per-user limits before the request leaves the browser.
#### `createRateLimiter(config): SlidingWindowRateLimiter`
Factory function. `config` accepts `{ maxRequests, windowMs }`.
#### `createAnalyticsBridge(config)`
Subscribes to AI chat events (`message_sent`, `message_received`, `error`) and forwards them to the `@tour-kit/analytics` tracker.
## Server Exports (`@tour-kit/ai/server`)
### Route handling
#### `createChatRouteHandler(options): { POST }`
Returns an object with a Next.js / Edge-compatible `POST` handler. The handler:
1. Parses the incoming `{ messages, tourContext? }` payload.
2. Runs the configured rate limiter (if any).
3. Builds the system prompt from `instructions` + retrieved context.
4. Streams the model response back as Server-Sent Events.
| Option | Type | Required | Notes |
|--------|------|----------|-------|
| `model` | `LanguageModelV1` | yes | Any AI SDK model — `openai('gpt-4o-mini')`, `anthropic('claude-sonnet-4-5')`, etc. |
| `context.strategy` | `'context-stuffing' \| 'rag'` | yes | Picks CAG or RAG mode. |
| `context.documents` | `Document[]` | yes for CAG | All docs to inject (CAG) or to index (RAG seed). |
| `context.embedding` | `Embedding` | yes for RAG | Output of `createAiSdkEmbedding`. |
| `context.vectorStore` | `VectorStoreAdapter` | yes for RAG | Output of `createInMemoryVectorStore` or your own adapter. |
| `context.topK` | `number` | no (RAG only) | Chunks to retrieve per query. Default 5. |
| `instructions.productName` | `string` | no | Used in the default system prompt. |
| `instructions.tone` | `'friendly' \| 'professional' \| 'concise'` | no | Hints to the model. |
| `instructions.boundaries` | `string[]` | no | Hard rules ("only answer X", "never reveal Y"). |
| `rateLimiter` | `ServerRateLimiter` | no | Pre-check before model call. Returns 429 when exceeded. |
```ts
// app/api/chat/route.ts
import { createChatRouteHandler } from '@tour-kit/ai/server'
import { openai } from '@ai-sdk/openai'
const { POST } = createChatRouteHandler({
model: openai('gpt-4o-mini'),
context: {
strategy: 'context-stuffing',
documents: [{ id: 'doc-1', content: '...' }],
},
instructions: {
productName: 'My App',
tone: 'friendly',
boundaries: ['Only answer questions about My App onboarding.'],
},
})
export { POST }
```
### RAG pipeline
#### `createInMemoryVectorStore(): VectorStoreAdapter`
In-memory vector store. Re-built on every server restart — for dev and prototypes only. Implements the `VectorStoreAdapter` interface so it's swappable with pgvector, Pinecone, or your own store.
#### `createAiSdkEmbedding(options): Embedding`
Embedding adapter wrapping the AI SDK's embedding API. Options:
| Option | Type | Notes |
|--------|------|-------|
| `model` | `string` | Embedding model id (e.g. `'text-embedding-3-small'`). See the [RAG guide](/docs/ai/rag-guide#pick-an-embedding-model) for recommendations. |
| `provider` | `EmbeddingProvider` | Optional explicit provider; defaults to OpenAI. |
#### `createRetriever(options): Retriever`
Convenience wrapper around `{ vectorStore, embedding, topK }` for use outside `createChatRouteHandler` (e.g. background jobs that surface "you might be interested in" suggestions).
#### `chunkDocument(doc, options)` / `chunkDocuments(docs, options)`
Splits documents into chunks suitable for embedding. Options: `{ chunkSize, overlap }`. Recommended starting values: `chunkSize: 512`, `overlap: 50`.
#### `createRAGMiddleware(options)`
Lower-level building block — produces middleware that injects retrieved context into a chat request. Use when you need to compose RAG with custom request handling outside `createChatRouteHandler`.
### Server utilities
#### `createSystemPrompt(config): string`
Builds the structured system prompt that `createChatRouteHandler` uses internally. Export it if you want to log, audit, or modify the final prompt before forwarding to the model.
#### `createServerRateLimiter(config): ServerRateLimiter`
Server-side rate limiter. Pass an instance to `createChatRouteHandler.rateLimiter`. Pairs with `createInMemoryRateLimitStore` or your own Redis-backed store.
#### `createInMemoryRateLimitStore(): RateLimitStore`
In-memory rate-limit store. Scoped to a single Node process — for production use a Redis-backed store across instances.
#### `generateSuggestions(options)` / `parseSuggestions(text)`
Generate and parse AI follow-up suggestion chips. `generateSuggestions` calls the model with a structured prompt; `parseSuggestions` extracts the suggestion array from a raw response.
## Types
The full type definitions live in [`packages/ai/src/types`](https://github.com/domidex01/tour-kit/tree/main/packages/ai/src/types). The most commonly imported types:
```ts
import type {
// Provider config
AiChatConfig,
// Hook returns
UseAiChatReturn,
UseTourAssistantReturn,
UseSuggestionsReturn,
// Context shapes
AiChatContextValue,
TourAssistantContext,
// Server
ChatRouteHandlerOptions,
VectorStoreAdapter,
Document,
Embedding,
Retriever,
// Status
ChatStatus,
} from '@tour-kit/ai'
```
---
# CAG Guide
> Context-Augmented Generation guide: inject tour documentation directly into AI prompts for fast, deterministic responses
Context-Augmented Generation (CAG) is the simplest way to make an AI assistant tour-aware. Every chat request includes the relevant tour documentation in the system prompt, so the model can answer accurately without a vector database, embedding pipeline, or retrieval step. This page covers when CAG is the right call, how to wire it up, the failure modes, and when to migrate to [RAG](/docs/ai/rag-guide).
## CAG vs RAG — the decision in one table
| Signal | Pick CAG | Pick RAG |
|--------|----------|----------|
| Total documentation size | Under ~50 KB / ~12k tokens | Over 50 KB |
| Document count | < 20 docs / tour steps | > 20 docs |
| Update cadence | Infrequent (every release) | Frequent (daily content updates) |
| Infrastructure budget | Zero — no DB, no embeddings | OK with vector store + embedding job |
| Cost sensitivity | OK with 3–8k tokens per request | Need to cap at retrieved-chunk size |
| Determinism | Important — same context every time | OK with retrieval variance |
**Rule of thumb:** if your entire docs corpus fits inside the model's context window (gpt-4o-mini = 128k, Claude Sonnet = 200k) AND total tokens stay under your per-request cost ceiling, CAG is the right call. Migrate to RAG when either constraint breaks.
## When to use CAG
- A focused onboarding flow under ~20 steps
- A single product with stable documentation
- Quick prototypes — you can ship CAG in 15 minutes and migrate later
- Strict determinism requirements (legal review wants the same context every request)
- Air-gapped environments where you can't run a vector DB
## When NOT to use CAG
- Documentation > 50 KB (token cost dominates per-request bill)
- Multi-product or multi-tenant setups (sending all products' docs leaks context across tenants)
- Frequently-changing knowledge bases — CAG redeploys whenever docs change
- Context-window limits matter for response quality — the model has less room for reasoning when half the window is stuffed
## Setup
### Client configuration
Wrap your tree with `AiChatProvider` and enable `tourContext` so the current tour state is forwarded with every request:
```tsx
import { AiChatProvider } from '@tour-kit/ai'
function App() {
return (
)
}
```
### Server configuration
Use `createChatRouteHandler` with `strategy: 'context-stuffing'`. Provide your documents inline — they are injected into the system prompt on every request:
```ts
// app/api/chat/route.ts
import { createChatRouteHandler } from '@tour-kit/ai/server'
import { openai } from '@ai-sdk/openai'
const { POST } = createChatRouteHandler({
model: openai('gpt-4o-mini'),
context: {
strategy: 'context-stuffing',
documents: [
{
id: 'onboarding-overview',
content:
'The Welcome tour introduces three concepts: workspaces, projects, and the activity feed. ' +
'Workspaces are top-level containers shared with your team. Projects live inside a workspace. ' +
'The activity feed shows realtime updates from every project you have access to.',
},
{
id: 'billing-faq',
content:
'Plans: Free (1 workspace, 3 projects), Pro ($12/user/mo, unlimited), Enterprise (contact sales). ' +
'Upgrading is instant; downgrading takes effect at the next billing cycle.',
},
],
},
instructions: {
productName: 'Acme App',
tone: 'friendly',
boundaries: [
'Only answer questions about Acme App onboarding and billing.',
'If asked about competitors, decline politely.',
'Never invent feature names. If unsure, suggest contacting support.',
],
},
})
export { POST }
```
## How CAG works under the hood
```text
User asks: "What is a workspace?"
│
▼
┌──────────────────────────────────┐
│ AiChatProvider (client) │
│ · collects tour state │
│ · POSTs {messages, tourContext} │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ createChatRouteHandler (server) │
│ 1. Build system prompt: │
│ instructions + ALL docs[] │
│ + tourContext │
│ 2. Forward to model │
│ 3. Stream response back │
└──────────────────────────────────┘
│
▼
Model sees the full corpus in its context window
and answers with grounded responses.
```
The server rebuilds the same system prompt on every request. There is no caching, no retrieval, no chunking — the trade-off for simplicity is that every request pays for every document's tokens.
## Token-cost math
Estimating a CAG bill is straightforward:
```text
per_request_tokens = system_prompt_tokens
+ sum(all documents)
+ user_message_tokens
+ model_response_tokens
monthly_cost ≈ per_request_tokens * requests_per_month * price_per_token
```
Worked example with 4 KB of docs (~1k tokens), a 0.5k system prompt, 100-token user messages, 300-token responses, gpt-4o-mini at $0.15/1M input + $0.60/1M output:
- Per request: 1,600 input + 300 output = ~$0.00042
- 10,000 requests/month = **~$4.20/month**
Now scale to 20 KB of docs (~5k tokens), same traffic:
- Per request: 5,600 input + 300 output = ~$0.00102
- 10,000 requests/month = **~$10.20/month**
That linear scaling is the headline reason to migrate to RAG once your corpus gets large — RAG only sends retrieved chunks (typically 1–3 KB) regardless of total docs.
## Common failure modes
- **Context drift.** When you update a document, redeploy the server route. CAG has no live data source — the docs are baked into the deployed bundle.
- **Token budget exceeded.** Models silently drop earlier context past their window. Check the model's max input tokens against `sum(documents) + system_prompt + conversation_history`.
- **Cross-tenant leakage.** If your route is shared across customers, each request sees every customer's documents. Either scope `documents` per request (pull from a database in the route) or use RAG.
- **Stale answers after a release.** A user on an old client may still ask about removed features. Add a version line to your `documents` array or the `instructions.boundaries`.
## Migration path to RAG
When you outgrow CAG, the migration is mostly server-side:
1. Set up a vector store (Pinecone, pgvector, Chroma — see [RAG Guide](/docs/ai/rag-guide))
2. Embed your existing `documents[]` and load them into the store
3. Swap `strategy: 'context-stuffing'` → `strategy: 'rag'` in `createChatRouteHandler`
4. Provide the retriever / store reference instead of inline documents
The client code (`AiChatProvider`, `useAiChat`, your chat UI) does not change.
## Next steps
- [RAG Guide](/docs/ai/rag-guide) — when CAG hits its ceiling
- [Tour Integration](/docs/ai/tour-integration) — wire tour state into CAG with `useTourAssistant`
- [API Reference](/docs/ai/api-reference) — full `createChatRouteHandler` options
---
# Components
> Pre-built AI chat components: AiChatPanel, AiChatToggle, AiChatMessageList, and AiChatInput with shadcn/ui styling
`@tour-kit/ai` provides headless components that you can style with your own design system.
## AiChatProvider
The root provider that manages chat state and configuration.
```tsx
import { AiChatProvider } from '@tour-kit/ai'
{children}
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `config` | `AiChatConfig` | Chat configuration object |
| `children` | `ReactNode` | Child components |
## AiChatSuggestions
Renders AI-generated follow-up suggestions as clickable chips.
```tsx
import { AiChatSuggestions } from '@tour-kit/ai'
sendMessage({ text: suggestion })}
className="flex gap-2"
/>
```
You can customize rendering with `renderSuggestion`:
```tsx
sendMessage({ text: suggestion })}
renderSuggestion={(suggestion, onSelect) => (
{suggestion}
)}
/>
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `suggestions` | `string[]` | Explicit suggestions list (overrides hook data) |
| `onSelect` | `(suggestion: string) => void` | Called when a suggestion is clicked |
| `renderSuggestion` | `(suggestion: string, onSelect: () => void) => ReactNode` | Custom chip renderer |
| `className` | `string` | Container class name |
## AiChatPanel
Pre-built chat panel. Renders a header, message list, suggestions strip, and input.
```tsx
import { AiChatPanel } from '@tour-kit/ai'
Ask me anything.}
showSuggestions
/>
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `size` | `'default' \| 'sm' \| 'lg'` | Panel size variant |
| `position` | `'bottom-right' \| 'bottom-left'` | Anchor position |
| `title` | `ReactNode` | Header title content |
| `emptyState` | `ReactNode` | Rendered when no messages |
| `showSuggestions` | `boolean` | Show the suggestions chip strip |
| `className` | `string` | Container class name |
| `children` | `ReactNode` | Override the default body |
| `renderMessage` | `(message, index) => ReactNode` | Custom message renderer |
## AiChatToggle
Floating button that opens/closes the chat panel.
```tsx
import { AiChatToggle } from '@tour-kit/ai'
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `size` | `'default' \| 'sm' \| 'lg'` | Button size variant |
| `position` | `'bottom-right' \| 'bottom-left'` | Anchor position |
| `icon` | `ReactNode` | Override the default icon |
| `className` | `string` | Class name override |
## AiChatHeader
Header bar inside ``. Wires up the close button and panel title slot. Use directly when composing your own panel layout.
```tsx
import { AiChatHeader } from '@tour-kit/ai'
/* ... */} />
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `title` | `ReactNode` | Header title |
| `showClose` | `boolean` | Render the close button |
| `onClose` | `() => void` | Close-button handler |
| `className` | `string` | Class name override |
| `children` | `ReactNode` | Replace default content |
## AiChatMessageList
Scrollable list of chat messages. Auto-scrolls to the latest message; respects `prefers-reduced-motion`.
```tsx
import { AiChatMessageList } from '@tour-kit/ai'
Ask a question to get started.}
renderMessage={(message) => }
/>
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `className` | `string` | Class name override |
| `emptyState` | `ReactNode` | Rendered when there are no messages |
| `renderMessage` | `(message, index) => ReactNode` | Custom per-message renderer |
## AiChatMessage
Single chat-message bubble. Pass `role` to switch between user/assistant styling.
```tsx
import { AiChatMessage } from '@tour-kit/ai'
Hi! How can I help with your tour?
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `role` | `'user' \| 'assistant'` | Message author |
| `children` | `ReactNode` | Message content |
| `className` | `string` | Class name override |
## AiChatInput
Message input field with send button. Reads/writes through `useAiChat` so it stays in sync with the rest of the panel.
```tsx
import { AiChatInput } from '@tour-kit/ai'
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `className` | `string` | Class name override |
| `placeholder` | `string` | Input placeholder |
| `disabled` | `boolean` | Disable input (also auto-disables while streaming) |
## AiChatPortal
Portals its `children` to `container` (defaults to `document.body`). Use to escape stacking contexts or to mount the chat panel inside a specific overlay container.
```tsx
import { AiChatPortal, AiChatPanel } from '@tour-kit/ai'
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode` | Subtree to portal |
| `container` | `HTMLElement \| null` | Target node; defaults to `document.body` when omitted |
## Headless Pattern
All components follow the headless pattern. Use the hooks directly for full control:
```tsx
import { useAiChat } from '@tour-kit/ai'
function CustomChat() {
const { messages, sendMessage, status } = useAiChat()
// Build your own UI
return (
{messages.map((msg) => (
{msg.content}
))}
sendMessage({ text: 'Hello' })}>Send
)
}
```
See [Hooks](/docs/ai/hooks) for the full hook surface and [API Reference](/docs/api/ai) for variants/types.
---
# Hooks
> React hooks for @tour-kit/ai — useAiChat, useAiChatContext, useSuggestions/useOptionalSuggestions, useTourAssistant
`@tour-kit/ai` ships its state and actions through React hooks. Components built on top of these hooks (see [Components](/docs/ai/components)) are convenient defaults — every screen the package renders can also be built from scratch with the hooks below.
All hooks must be called inside an `` unless noted otherwise.
## useAiChat
Primary chat-state hook. Returns the message list, send/stop/reload actions, and panel open/close state.
```tsx
import { useAiChat } from '@tour-kit/ai'
function CustomChat() {
const { messages, status, sendMessage, isOpen, toggle } = useAiChat()
return (
<>
{isOpen ? 'Close' : 'Open'} chat
{isOpen && (
{messages.map((m) =>
{m.content}
)}
sendMessage({ text: 'Hello' })}>Send
)}
>
)
}
```
### Return Value
| Return | Type | Description |
|--------|------|-------------|
| `messages` | `UIMessage[]` | Conversation history |
| `status` | `ChatStatus` | `'ready' \| 'submitted' \| 'streaming' \| 'error'` |
| `error` | `Error \| null` | Last error if any |
| `sendMessage` | `(input: { text: string }) => void` | Send a user message |
| `stop` | `() => void` | Cancel the current streaming response |
| `reload` | `() => void` | Re-run the last assistant message |
| `setMessages` | `(messages \| (prev) => messages) => void` | Replace conversation history |
| `isOpen` | `boolean` | Whether the panel is open |
| `open` | `() => void` | Open the panel |
| `close` | `() => void` | Close the panel |
| `toggle` | `() => void` | Toggle open state |
## useAiChatContext
Low-level access to the full provider context. Throws if no `AiChatProvider` is mounted above. Most consumers should reach for `useAiChat` instead — `useAiChatContext` is the unsafe escape hatch when you need raw context state (config, internal state machines, custom integrations).
```tsx
import { useAiChatContext } from '@tour-kit/ai'
function ConfigDebugger() {
const ctx = useAiChatContext()
return {JSON.stringify(ctx.config, null, 2)}
}
```
Returns the full `AiChatContextValue`. See [API Reference](/docs/api/ai) for the complete shape.
## useSuggestions
Returns the merged static + dynamic suggestion list and helpers to refresh/select.
```tsx
import { useSuggestions } from '@tour-kit/ai'
function CustomSuggestions() {
const { suggestions, isLoading, isBusy, select } = useSuggestions()
if (isLoading) return
return suggestions.map((s) => (
select(s)} disabled={isBusy}>
{s}
))
}
```
### Return Value
| Return | Type | Description |
|--------|------|-------------|
| `suggestions` | `string[]` | Combined static + dynamic suggestions, filtered for relevance |
| `isLoading` | `boolean` | True while dynamic suggestions are being fetched |
| `isBusy` | `boolean` | True when chat is busy (`submitted` or `streaming`) and cannot accept new messages |
| `refresh` | `() => void` | Clear cache and regenerate dynamic suggestions |
| `select` | `(suggestion: string) => void` | Send a suggestion as a chat message; no-op when busy |
`useSuggestions` works both inside and outside `AiChatProvider` — returns an empty state when no context.
### useOptionalSuggestions (deprecated)
Alias for `useSuggestions` retained for backward compatibility. Will be removed in the next major version. Prefer `useSuggestions`.
## useTourAssistant
Tour-aware extension of `useAiChat`. Adds the active tour, active step, and checklist progress to the assistant context, so the model can answer questions like "what should I do next?".
```tsx
import { useTourAssistant } from '@tour-kit/ai'
function ContextualChat() {
const { messages, sendMessage, isLoading, activeTourContext } = useTourAssistant()
// activeTourContext.activeTour.currentStep is available for inline UI
return /* ... */
}
```
Returns `UseTourAssistantReturn`, which extends `UseAiChatReturn` with `isLoading: boolean` and tour-context fields. See [API Reference](/docs/api/ai#usetourassistant) for the full shape.
---
# Quick Start
> Set up @tour-kit/ai in under 5 minutes: install the package, configure your AI provider, and add the chat widget to React
Five steps to a working AI chat widget in a React app: install the package, wrap your tree with the provider, drop in a chat UI, mount a server route, and verify the round-trip. Total time ~5 minutes assuming you already have a Next.js app and an OpenAI API key. If you don't have an API key, [grab one from platform.openai.com](https://platform.openai.com/api-keys) first — `@tour-kit/ai` works with any Vercel AI SDK provider but OpenAI's `gpt-4o-mini` is the cheapest path to "working".
This guide ships the no-context variant — the assistant has no product knowledge yet. After you confirm the round-trip works, move on to the [CAG Guide](/docs/ai/cag-guide) (small docs) or [RAG Guide](/docs/ai/rag-guide) (large docs) to give the model real product context.
## Installation
```bash
pnpm add @tour-kit/ai
```
You also need the AI SDK as a peer dependency:
```bash
pnpm add ai @ai-sdk/openai
```
## Client Setup
Wrap your application with `AiChatProvider`:
```tsx
import { AiChatProvider } from '@tour-kit/ai'
export function App() {
return (
)
}
```
## Add a Chat Widget
Use the `useAiChat` hook to build your chat UI:
```tsx
import { useAiChat } from '@tour-kit/ai'
function ChatWidget() {
const { messages, sendMessage, status } = useAiChat()
return (
{messages.map((msg) => (
{msg.role}: {msg.content}
))}
{
if (e.key === 'Enter') {
sendMessage({ text: e.currentTarget.value })
e.currentTarget.value = ''
}
}}
/>
)
}
```
## Server Setup
Create an API route handler. For Next.js:
```ts
// app/api/chat/route.ts
import { createChatRouteHandler } from '@tour-kit/ai/server'
import { openai } from '@ai-sdk/openai'
const { POST } = createChatRouteHandler({
model: openai('gpt-4o-mini'),
context: {
strategy: 'context-stuffing',
documents: [],
},
})
export { POST }
```
Set your API key in `.env.local`:
```
OPENAI_API_KEY=sk-...
```
## Verify Your Setup
Render `ChatWidget` somewhere visible, type a message, and confirm three things:
1. The user message appears in the list immediately (client-side optimistic update).
2. The `status` transitions through `submitted` → `streaming` → `ready`.
3. An assistant reply streams in token-by-token (not all at once at the end).
If all three work, the round-trip is healthy. The most common failures:
| Symptom | Likely cause |
|---------|--------------|
| 401/403 from `/api/chat` | `OPENAI_API_KEY` missing or wrong in the server environment (restart `next dev` after editing `.env.local`) |
| Stuck at `submitted`, no streaming | Route handler returning JSON instead of SSE — make sure you're using `createChatRouteHandler`, not a custom handler |
| `useAiChat must be used within an ` thrown | Component is rendered outside the provider tree |
| Streaming works but assistant says "I don't know" to everything | Expected — you have not configured CAG or RAG yet. Continue to the guides below. |
## Next Steps
- [CAG Guide](/docs/ai/cag-guide) — give the assistant product knowledge by injecting docs into every prompt (small docs)
- [RAG Guide](/docs/ai/rag-guide) — vector-search retrieval for large documentation sets
- [Tour Integration](/docs/ai/tour-integration) — connect the assistant to live tour state via `useTourAssistant`
- [Components](/docs/ai/components) — swap the custom UI for the pre-built `` + `` combo
---
# RAG Guide
> Retrieval-Augmented Generation guide: use vector search over documentation for scalable AI chat with large content sets
Retrieval-Augmented Generation (RAG) is the right pattern when your documentation is too big to stuff into every prompt. Instead of sending the whole corpus, `@tour-kit/ai` embeds your documents into a vector store at index time and retrieves only the most relevant chunks at query time. This keeps token cost roughly constant as your knowledge base grows, makes content updates cheap (re-embed one doc, not redeploy), and avoids leaking unrelated content across tenants.
If you have not read [the CAG guide](/docs/ai/cag-guide), start there — many integrations should ship CAG first and migrate later. RAG is the bigger commitment.
## When to use RAG
- Documentation > 50 KB or > 20 distinct docs
- Multi-product / multi-tenant — retrieval can be filtered per request
- Frequent content updates (daily / weekly) you don't want to redeploy
- Per-request token budget needs to stay roughly constant as docs grow
- You need a citation trail — RAG returns matched chunks, so the response can be linked back to a specific source
## When NOT to use RAG
- Corpus fits in a model context window AND budget is OK — use [CAG](/docs/ai/cag-guide) instead
- No infrastructure budget — you need somewhere to store embeddings (in-memory, pgvector, Pinecone, Chroma)
- Strict determinism requirements — retrieval introduces variance; the same question can return different chunks
- Real-time data — RAG retrieves from a snapshot, not live state. For live data, use tool calling, not RAG.
## Pick an embedding model
The embedding model converts your documents (and the user's question) into vectors so similar text lands near each other in vector space. `@tour-kit/ai` uses the Vercel AI SDK's embedding interface, so any AI-SDK-compatible embedding model works.
| Model | Provider | Dimensions | Cost / 1M tokens | When it fits |
|-------|----------|------------|-------------------|--------------|
| `text-embedding-3-small` | OpenAI | 1536 | $0.02 | Default for most projects — best price/quality balance |
| `text-embedding-3-large` | OpenAI | 3072 | $0.13 | Higher accuracy when retrieval quality matters more than cost |
| `voyage-3-lite` | Voyage AI | 512 | $0.02 | Smaller embeddings, faster query, lower storage |
| `voyage-3` | Voyage AI | 1024 | $0.06 | Recommended for technical docs (often beats OpenAI on code/API content) |
| `embed-english-v3.0` | Cohere | 1024 | $0.10 | Strong on retrieval benchmarks; good if you already use Cohere |
**Default recommendation:** start with `text-embedding-3-small`. Re-embedding the corpus to migrate to another model is cheap and one-time.
## Pick a vector store
| Store | Best for | Notes |
|-------|----------|-------|
| `createInMemoryVectorStore()` (built-in) | Prototypes, < 1k chunks, single-process apps | Recomputed on every server restart. Not for production. |
| pgvector (Postgres extension) | You already use Postgres | Lowest operational overhead. ANN index via HNSW. |
| Pinecone | Managed, no ops | Generous free tier. Pay-per-query past that. |
| Chroma | Self-hosted, open source | Local dev parity with prod. Good for small/medium teams. |
| Weaviate / Qdrant | Self-hosted at scale | More features (filters, hybrid search) when you need them. |
**Default recommendation:** start with `createInMemoryVectorStore()` for local dev, then move to pgvector when you ship to staging (it deploys to any Postgres host with no extra service).
## Setup
### 1. Index your documents
```ts
import {
chunkDocuments,
createAiSdkEmbedding,
createInMemoryVectorStore,
} from '@tour-kit/ai/server'
const vectorStore = createInMemoryVectorStore()
const embedding = createAiSdkEmbedding({ model: 'text-embedding-3-small' })
const documents = [
{
id: 'creating-tours',
content: 'How to create a tour: import Tour and TourStep from @tour-kit/react...',
metadata: { title: 'Creating Tours', section: 'docs' },
},
{
id: 'step-config',
content: 'Tour step configuration: target accepts a CSS selector or React ref...',
metadata: { title: 'Step Config', section: 'docs' },
},
]
// Split long docs into retrievable chunks. 512-token chunks with 50-token overlap
// is the conservative default — bigger chunks preserve context but cost more per
// retrieved hit; smaller chunks improve precision at the cost of fragmentation.
const chunks = chunkDocuments(documents, { chunkSize: 512, overlap: 50 })
await vectorStore.upsert(chunks, embedding)
```
### 2. Wire the route handler
```ts
// app/api/chat/route.ts
import { createChatRouteHandler } from '@tour-kit/ai/server'
import { openai } from '@ai-sdk/openai'
const { POST } = createChatRouteHandler({
model: openai('gpt-4o-mini'),
context: {
strategy: 'rag',
documents,
embedding,
vectorStore,
topK: 5,
},
instructions: {
productName: 'Acme App',
tone: 'friendly',
boundaries: ['Only answer using the retrieved documentation.'],
},
})
export { POST }
```
`topK` controls how many chunks are retrieved per query. 3–5 is the standard range — higher values give the model more context to reason over but increase token cost linearly.
### 3. Client stays unchanged
```tsx
import { AiChatProvider } from '@tour-kit/ai'
```
This is the win of `@tour-kit/ai`'s split design — migrating from CAG to RAG is a server-side change only.
## How RAG works under the hood
```text
Index time (one-time, or whenever content changes):
docs[] ──► chunkDocuments() ──► embedding.embed() ──► vectorStore.upsert()
Query time (every request):
user message
│
▼
┌────────────────────────────────────────┐
│ embed(user message) │
└────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ vectorStore.query(vec, topK) │
│ → returns top-K matched chunks │
└────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ System prompt = │
│ instructions + matched chunks only │
│ Model sees only relevant context │
└────────────────────────────────────────┘
```
## Custom vector store
Implement the `VectorStoreAdapter` interface to plug in any vector DB:
```ts
import type { VectorStoreAdapter } from '@tour-kit/ai'
const customStore: VectorStoreAdapter = {
upsert: async (documents, embedding) => {
/* persist to your vector DB */
},
query: async (query, embedding, topK) => {
/* return top-K matches as { id, content, score, metadata } */
},
delete: async (ids) => {
/* remove by id */
},
}
```
## Tuning checklist
- **Chunk size 256–1024 tokens.** Smaller = precise, more index overhead. Larger = more context per hit, less precision.
- **Chunk overlap 10–20% of chunk size.** Prevents semantically-meaningful sentences from being split across chunk boundaries.
- **topK 3–10.** Start at 5. Higher topK → higher cost + risk of irrelevant chunks polluting context.
- **Pre-embed at build time** when content is static (docs, marketing pages). Saves the embed cost on every server boot.
- **Add metadata filters** (e.g. `tenant_id`, `language`) to your `VectorStoreAdapter.query` call to avoid cross-tenant leakage.
## Cost comparison vs CAG
For a 100 KB corpus (~25k tokens), gpt-4o-mini at $0.15/1M input, 10k requests/month:
- **CAG** would send all 25k tokens per request → 25,000 × 10,000 = 250M tokens = **~$37.50/mo just for context**.
- **RAG** sends only retrieved chunks (~5 × 512 = ~2.5k tokens) → 25M tokens = **~$3.75/mo for context**.
The 10× savings widens further as the corpus grows. Below ~50 KB total docs, the cost gap is too small to justify RAG's operational overhead — that's why we recommend starting with CAG.
## Next steps
- [CAG Guide](/docs/ai/cag-guide) — the simpler alternative; ship CAG first if your corpus is small
- [Tour Integration](/docs/ai/tour-integration) — combine RAG with live tour state via `useTourAssistant`
- [API Reference](/docs/ai/api-reference) — full `createChatRouteHandler`, `chunkDocuments`, `createRetriever` options
---
# Tour Integration
> Connect AI chat to active tour state: context-aware assistance that knows the current step, tour progress, and user actions
Tour integration is the differentiator for `@tour-kit/ai`. A generic chat widget answers "how do I save a file?" — the integrated assistant answers "you are on step 3 of the Welcome tour, the Save button is the blue icon in the top-right, here's why you need to click it now." This page covers the wiring, the three highest-value use cases, and the failure modes when tour state and AI context get out of sync.
## What `useTourAssistant` gives you on top of `useAiChat`
`useTourAssistant` extends `useAiChat` with three additions:
1. **`tourContext`** — a structured snapshot of the active tour (id, name, current step index, total steps, completed tours, checklist progress). Re-assembled on every render.
2. **`askAboutStep()`** — sends a pre-built prompt asking the assistant to explain the current step. No-op when there is no active step.
3. **`askForHelp(topic?)`** — sends a help prompt with the current tour context attached, optionally scoped to a topic.
Everything from `useAiChat` (`messages`, `sendMessage`, `status`, `open`, `close`, `toggle`) is still available on the same return value — `useTourAssistant` is a superset.
## Wiring it up (3-line change from `useAiChat`)
```tsx
import { useTourAssistant } from '@tour-kit/ai'
function HelpButton() {
const { askAboutStep, tourContext } = useTourAssistant()
const stepLabel = tourContext.activeStep?.title
return (
{stepLabel ? `Explain "${stepLabel}"` : 'Pick a tour first'}
)
}
```
This works because the `@tour-kit/react` `TourProvider` exposes the active tour via context, and `useTourAssistant` reads from it automatically — no prop drilling.
## The three use cases that justify tour integration
### 1. "Explain this step" — confused users mid-tour
```tsx
function StepExplainer() {
const { askAboutStep, tourContext, messages } = useTourAssistant()
if (!tourContext.activeTour) return null
return (
Step {tourContext.activeTour.currentStep + 1} of {tourContext.activeTour.totalSteps}
{tourContext.activeStep ? ` — ${tourContext.activeStep.title}` : null}
I don't understand this step
{messages.map((m) => (
{m.content}
))}
)
}
```
The prompt that `askAboutStep` sends includes the step's id, title, and content, so the model can give a grounded answer without the user having to retype anything.
### 2. "What does this button do?" — context-aware UI explainer
Combine `useTourAssistant` with a tooltip or right-click handler so any UI element can be "explained" with the current tour as context.
```tsx
function ExplainableButton({ label, action, children }: Props) {
const { sendMessage, tourContext, open } = useTourAssistant()
const tourId = tourContext.activeTour?.id ?? 'no-tour'
return (
{
e.preventDefault()
open()
sendMessage({
text: `In the context of the ${tourId} tour, what does the "${label}" button do?`,
})
}}
>
{children}
)
}
```
Right-clicking the button opens the chat and pre-populates a context-aware question.
### 3. "Skip to step N" — assistant as a navigation surface
Combine with `useTour` from `@tour-kit/react` to let the assistant drive tour navigation in response to user requests:
```tsx
import { useTour } from '@tour-kit/react'
import { useTourAssistant } from '@tour-kit/ai'
function SkipToStep() {
const { sendMessage, tourContext } = useTourAssistant()
const { goToStep } = useTour(tourContext.activeTour?.id ?? '')
return (
sendMessage({
text: 'Skip me to the most important step in this tour.',
})
}
>
Take me to the important part
)
}
```
(For the model's response to actually drive `goToStep`, you need tool calling — see the [AI SDK tool-calling docs](https://sdk.vercel.ai/docs/foundations/tools).)
## How tour context flows end-to-end
```text
┌──────────────────────────┐
│ TourProvider │ active tour state
│ (@tour-kit/react) │ (id, currentStep, ...)
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ useTourAssistant │ reads via context,
│ │ builds TourAssistantContext
└──────────────────────────┘
│
▼ every sendMessage()
┌──────────────────────────┐
│ AiChatProvider │ serializes tourContext into the
│ config.tourContext=true │ POST body alongside the messages
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ /api/chat route handler │ injects tourContext into the
│ createChatRouteHandler │ system prompt
└──────────────────────────┘
```
If you forget to set `tourContext: true` on the provider, the hook works fine for typing but the server never sees the tour state — the most common failure mode.
## Tour context shape
```ts
interface TourAssistantContext {
activeTour: {
id: string
name: string
currentStep: number // zero-indexed
totalSteps: number
} | null
activeStep: {
id: string
title: string
content: string
} | null
completedTours: string[]
checklistProgress: { completed: number; total: number } | null
}
```
`activeTour` and `activeStep` are `null` when no tour is running. Always guard before using.
## Manual assembly for tests
You don't need a `TourProvider` to build a `TourAssistantContext` — `assembleTourContext` accepts a tour-state object directly. Useful in tests or in custom hosts:
```ts
import { assembleTourContext } from '@tour-kit/ai'
const ctx = assembleTourContext({
isActive: true,
tourId: 'onboarding',
tour: { id: 'onboarding', name: 'Onboarding Tour', steps: [] },
currentStepIndex: 2,
totalSteps: 5,
currentStep: { id: 'step-3', title: 'Add your team', content: 'Invite team members.' },
completedTours: [],
})
```
Returns `null`-shaped context when `isActive` is false or `tourState` is missing.
## Combining with CAG vs RAG
Tour integration works with both retrieval strategies — pick CAG or RAG based on documentation size (see [CAG Guide](/docs/ai/cag-guide) and [RAG Guide](/docs/ai/rag-guide)). The client config is identical in both cases:
```tsx
```
The tour context is sent as structured JSON alongside the messages, so it composes with either retrieval strategy on the server.
## Failure modes
- **`tourContext: true` not set on provider.** Hook returns valid data, but the server never sees it. Symptom: assistant answers generically, ignoring tour state.
- **Step content too large.** If a step's `content` is itself a long-form React tree, the serialized version sent to the model can blow past the context window. Either trim the content sent to the model or store the explainer text separately.
- **Stale context after a step jump.** `useTourAssistant` reads on render. If you advance the tour imperatively (e.g. inside a `setTimeout`), make sure the React tree re-renders before sending the next message, or pull `tourContext` fresh each time.
- **Multi-tour ambiguity.** If two tours are active at once (rare but possible with overlapping providers), `tourContext.activeTour` reflects the innermost provider. Scope explicitly with a separate `useTour(tourId)` call if you need a specific one.
## Next steps
- [Hooks](/docs/ai/hooks) — full `useTourAssistant` signature and the rest of the public hook surface
- [CAG Guide](/docs/ai/cag-guide) — server-side wiring for small docs
- [RAG Guide](/docs/ai/rag-guide) — server-side wiring for large docs
- [API Reference](/docs/ai/api-reference) — `TourAssistantContext`, `assembleTourContext`, and provider config
---
# Analytics
> Plugin-based analytics for tracking tour starts, step views, completions, and hint interactions across any provider
{/* llm-context-callout */}
## Why Analytics Matter
Understanding how users interact with your tours and hints is crucial for optimizing the onboarding experience. Analytics help you answer questions like:
- Which tours are users completing vs. abandoning?
- Where do users get stuck in multi-step flows?
- Are hints actually being seen and clicked?
- How long do users spend on each step?
The `@tour-kit/analytics` package provides a plugin-based system that integrates with popular analytics platforms while maintaining full type safety and privacy controls.
## Installation
## Quick Start
Wrap your application with `AnalyticsProvider` and configure one or more plugins:
```tsx title="app/layout.tsx"
import { AnalyticsProvider, consolePlugin } from '@tour-kit/analytics'
export default function RootLayout({ children }) {
return (
{children}
)
}
```
Now tour events will automatically be logged to the console during development.
## Available Plugins
userTourKit provides official plugins for popular analytics platforms:
| Plugin | Description | Peer Dependency |
|--------|-------------|-----------------|
| `consolePlugin` | Debug logging with colored output | None |
| `posthogPlugin` | PostHog product analytics | `posthog-js` |
| `mixpanelPlugin` | Mixpanel event tracking | `mixpanel-browser` |
| `amplitudePlugin` | Amplitude analytics | `@amplitude/analytics-browser` |
| `googleAnalyticsPlugin` | Google Analytics 4 | gtag.js (script tag) |
## Multiple Plugins
You can use multiple analytics providers simultaneously:
```tsx title="app/providers.tsx"
import { AnalyticsProvider, consolePlugin, posthogPlugin } from '@tour-kit/analytics'
const analytics = (
{children}
)
```
## Tracked Events
The analytics system automatically tracks these event types:
### Tour Lifecycle
- `tour_started` - User begins a tour
- `tour_completed` - User finishes all steps
- `tour_skipped` - User exits tour early
- `tour_abandoned` - User closes page during tour
### Step Lifecycle
- `step_viewed` - User views a step
- `step_completed` - User advances from a step
- `step_skipped` - User skips a step
- `step_interaction` - User interacts with step content
### Hint Events
- `hint_shown` - Hint becomes visible
- `hint_dismissed` - User dismisses hint
- `hint_clicked` - User clicks hint hotspot
### Feature Adoption
- `feature_used` - User uses a feature for the first time
- `feature_adopted` - User reaches adoption threshold
- `feature_churned` - User stops using a feature
- `nudge_shown` - Nudge displayed to user
- `nudge_clicked` - User clicks nudge
- `nudge_dismissed` - User dismisses nudge
## Manual Tracking
For custom tracking needs, use the `useAnalytics` hook:
```tsx
import { useAnalytics } from '@tour-kit/analytics'
function FeatureButton() {
const analytics = useAnalytics()
const handleClick = () => {
analytics.track('feature_used', {
tourId: 'custom-feature',
metadata: {
feature_name: 'advanced-search',
source: 'toolbar'
}
})
}
return Advanced Search
}
```
## Privacy & Performance
### Automatic Flush
The `AnalyticsProvider` automatically flushes queued events when the user navigates away from the page, ensuring no data loss.
### Opt-Out Support
Disable analytics entirely by setting `enabled: false`:
```tsx
{children}
```
### Debug Mode
Enable debug logging to see events in the console without sending to production:
```tsx
{children}
```
## Next Steps
---
# Analytics Hooks
> useAnalytics hook: access the analytics tracker to send custom events and track tour interactions in React components
## Overview
userTourKit provides two hooks for accessing the analytics tracker in your components:
- `useAnalytics()` - Throws error if not in provider (strict)
- `useAnalyticsOptional()` - Returns `null` if not in provider (lenient)
## useAnalytics
Access the analytics tracker with strict provider enforcement.
### Usage
```tsx
import { useAnalytics } from '@tour-kit/analytics'
function MyComponent() {
const analytics = useAnalytics()
const handleClick = () => {
analytics.track('feature_used', {
tourId: 'feature-demo',
metadata: { feature: 'export-data' }
})
}
return Export
}
```
### Error Handling
This hook throws an error if used outside an `AnalyticsProvider`:
```tsx
// This will throw an error
function BadComponent() {
const analytics = useAnalytics() // Error: useAnalytics must be used within AnalyticsProvider
return ...
}
```
### When to Use
Use `useAnalytics()` when:
- Analytics are required for the component to function
- You want to catch configuration errors early
- The component is always rendered within a provider
## useAnalyticsOptional
Access the analytics tracker with optional provider support.
### Usage
```tsx
import { useAnalyticsOptional } from '@tour-kit/analytics'
function OptionalTrackingComponent() {
const analytics = useAnalyticsOptional()
const handleClick = () => {
// Only track if analytics is available
analytics?.track('button_clicked', {
tourId: 'optional-feature',
metadata: { source: 'sidebar' }
})
}
return Click Me
}
```
### Null Safety
This hook returns `null` when not in a provider:
```tsx
function SafeComponent() {
const analytics = useAnalyticsOptional()
if (!analytics) {
// Analytics not configured - component still works
return No tracking
}
return Tracking enabled
}
```
### When to Use
Use `useAnalyticsOptional()` when:
- Analytics are nice-to-have but not required
- The component should work without a provider
- You're building reusable components for multiple apps
## Tracking Custom Events
### Feature Usage
Track when users interact with specific features:
```tsx
import { useAnalytics } from '@tour-kit/analytics'
function AdvancedSearch() {
const analytics = useAnalytics()
const handleSearch = (query: string, filters: string[]) => {
analytics.track('feature_used', {
tourId: 'advanced-search',
metadata: {
query_length: query.length,
filter_count: filters.length,
filters: filters.join(',')
}
})
}
return
}
```
### Step Interactions
Track user interactions within tour steps:
```tsx
import { useAnalytics } from '@tour-kit/analytics'
import { useTour } from '@tour-kit/core'
function InteractiveTourStep() {
const analytics = useAnalytics()
const { tourId, currentStep } = useTour()
const handleVideoPlay = () => {
analytics.stepInteraction(
tourId,
currentStep.id,
'video_played',
{ video_duration: 120 }
)
}
return (
...
)
}
```
### User Identification
Identify users after authentication:
```tsx
'use client'
import { useAnalytics } from '@tour-kit/analytics'
import { useEffect } from 'react'
function UserIdentifier({ user }) {
const analytics = useAnalytics()
useEffect(() => {
if (user) {
analytics.identify(user.id, {
email: user.email,
plan: user.plan,
signup_date: user.createdAt
})
}
}, [user, analytics])
return null
}
```
## TourAnalytics Methods
The analytics object returned by both hooks provides these methods:
### Identification
```tsx
// Identify a user
analytics.identify(userId: string, properties?: Record)
```
### Raw Event Tracking
```tsx
// Track any event type
analytics.track(
eventName: TourEventName,
data: TourEventData
)
```
### Tour Lifecycle
```tsx
// Track tour start
analytics.tourStarted(tourId: string, totalSteps: number, metadata?: Record)
// Track tour completion
analytics.tourCompleted(tourId: string, metadata?: Record)
// Track tour skip
analytics.tourSkipped(
tourId: string,
stepIndex: number,
stepId?: string,
metadata?: Record
)
// Track tour abandonment
analytics.tourAbandoned(
tourId: string,
stepIndex: number,
stepId?: string,
metadata?: Record
)
```
### Step Lifecycle
```tsx
// Track step view
analytics.stepViewed(
tourId: string,
stepId: string,
stepIndex: number,
totalSteps: number,
metadata?: Record
)
// Track step completion
analytics.stepCompleted(
tourId: string,
stepId: string,
stepIndex: number,
metadata?: Record
)
// Track step skip
analytics.stepSkipped(
tourId: string,
stepId: string,
stepIndex: number,
metadata?: Record
)
// Track step interaction
analytics.stepInteraction(
tourId: string,
stepId: string,
interactionType: string,
metadata?: Record
)
```
### Hint Tracking
```tsx
// Track hint shown
analytics.hintShown(hintId: string, metadata?: Record)
// Track hint dismissed
analytics.hintDismissed(hintId: string, metadata?: Record)
// Track hint clicked
analytics.hintClicked(hintId: string, metadata?: Record)
```
### Utility Methods
```tsx
// Flush queued events immediately
await analytics.flush()
// Clean up and destroy tracker
analytics.destroy()
```
## Type Safety
Both hooks return a fully typed `TourAnalytics` instance:
```tsx
import { useAnalytics } from '@tour-kit/analytics'
import type { TourAnalytics } from '@tour-kit/analytics'
function TypedComponent() {
const analytics: TourAnalytics = useAnalytics()
// Full TypeScript autocomplete and type checking
analytics.track('tour_started', {
tourId: 'onboarding',
totalSteps: 5
})
return ...
}
```
## Combining with Tour Hooks
Use analytics hooks alongside tour hooks for rich tracking:
```tsx
import { useAnalytics } from '@tour-kit/analytics'
import { useTour } from '@tour-kit/core'
function TourWithAnalytics() {
const analytics = useAnalytics()
const { tourId, currentStep, next, skip } = useTour()
const handleNext = () => {
analytics.stepCompleted(tourId, currentStep.id, currentStep.index)
next()
}
const handleSkip = () => {
analytics.tourSkipped(tourId, currentStep.index, currentStep.id)
skip()
}
return (
Next
Skip Tour
)
}
```
## Related
- [AnalyticsProvider](/docs/analytics/providers) - Configure analytics
- [Types](/docs/analytics/types) - Event types and interfaces
- [Plugins](/docs/analytics/plugins) - Available analytics plugins
---
# Analytics Plugins
> Analytics plugin system: choose from built-in integrations or create custom plugins with the AnalyticsPlugin interface
## Overview
userTourKit's analytics system is built on a plugin architecture that allows you to send events to any analytics platform. Each plugin implements a simple interface and runs independently.
## How Plugins Work
Plugins receive tour events and forward them to analytics platforms:
```tsx
import { AnalyticsProvider, posthogPlugin } from '@tour-kit/analytics'
{children}
```
When an event occurs (e.g., user views a step), the analytics system:
1. Enriches the event with session data and timestamps
2. Passes the event to each plugin's `track()` method
3. Each plugin transforms and sends the event to its platform
## Plugin Interface
All plugins implement the `AnalyticsPlugin` interface:
```ts
interface AnalyticsPlugin {
/** Unique plugin identifier */
name: string
/** Initialize the plugin (called once on setup) */
init?: () => void | Promise
/** Track an event */
track: (event: TourEvent) => void | Promise
/** Identify a user */
identify?: (userId: string, properties?: Record) => void
/** Flush any queued events */
flush?: () => void | Promise
/** Clean up resources */
destroy?: () => void
}
```
Only `name` and `track` are required. All other methods are optional.
## Built-in Plugins
userTourKit provides official plugins for popular platforms:
### Development
- [Console Plugin](/docs/analytics/plugins/console) - Debug logging with colored output
### Production Analytics
- [PostHog Plugin](/docs/analytics/plugins/posthog) - Product analytics and session replay
- [Mixpanel Plugin](/docs/analytics/plugins/mixpanel) - Event tracking and user analytics
- [Amplitude Plugin](/docs/analytics/plugins/amplitude) - Product intelligence platform
- [Google Analytics Plugin](/docs/analytics/plugins/google-analytics) - GA4 event tracking
## Plugin Order
Plugins execute in the order they're registered:
```tsx
{children}
```
This matters when:
- Debugging - put console plugin first to see events before they're sent
- Error handling - earlier plugins won't be affected by later plugin errors
## Error Handling
If a plugin throws an error, it doesn't affect other plugins:
```tsx
// Even if PostHog fails, Mixpanel still receives events
{children}
```
Enable `debug: true` to see plugin errors in the console.
## Conditional Plugins
Load plugins conditionally based on environment:
```tsx
import {
AnalyticsProvider,
consolePlugin,
posthogPlugin,
mixpanelPlugin
} from '@tour-kit/analytics'
const isDev = process.env.NODE_ENV === 'development'
const isProd = process.env.NODE_ENV === 'production'
{children}
```
## Plugin Lifecycle
### Initialization
Plugins are initialized when `AnalyticsProvider` mounts:
```tsx
const plugin: AnalyticsPlugin = {
name: 'my-plugin',
async init() {
// Load SDK, connect to API, etc.
console.log('Plugin initialized')
},
track(event) {
console.log('Tracking:', event)
}
}
```
### Event Tracking
The `track` method is called for every event:
```tsx
const plugin: AnalyticsPlugin = {
name: 'my-plugin',
track(event: TourEvent) {
// event.eventName: 'tour_started' | 'step_viewed' | etc.
// event.tourId: 'onboarding'
// event.timestamp: 1699564800000
// event.sessionId: '1699564800000-abc123'
// ...other properties
}
}
```
### User Identification
The `identify` method is called when users are identified:
```tsx
const plugin: AnalyticsPlugin = {
name: 'my-plugin',
identify(userId: string, properties?: Record) {
console.log('User:', userId, properties)
},
track(event) {
// Events include userId after identification
console.log(event.userId)
}
}
```
### Cleanup
The `destroy` method is called when the provider unmounts:
```tsx
const plugin: AnalyticsPlugin = {
name: 'my-plugin',
destroy() {
// Clear caches, close connections, etc.
console.log('Plugin destroyed')
},
track(event) {
console.log(event)
}
}
```
## Event Batching
Some plugins support batching events before sending:
```tsx
{children}
```
## Creating Custom Plugins
See [Custom Plugins](/docs/analytics/plugins/custom) for a complete guide on creating your own analytics integrations.
## Plugin Comparison
| Plugin | Peer Dependency | Event Prefix | Auto-Flush | Batching |
|--------|-----------------|--------------|------------|----------|
| Console | None | `🎯 userTourKit` | N/A | No |
| PostHog | `posthog-js` | `tourkit_` | Yes | Yes |
| Mixpanel | `mixpanel-browser` | `userTourKit: ` | Yes | Yes |
| Amplitude | `@amplitude/analytics-browser` | `tourkit_` | Manual | Yes |
| Google Analytics | gtag.js | `tourkit_` | Yes | Yes |
## Related
- [Console Plugin](/docs/analytics/plugins/console) - Debug logging
- [Custom Plugins](/docs/analytics/plugins/custom) - Build your own
- [Types](/docs/analytics/types) - Plugin interface reference
---
# Amplitude Plugin
> Amplitude analytics plugin: send tour and hint events to Amplitude with automatic property mapping and session tracking
## Overview
The Amplitude plugin sends tour and hint events to [Amplitude](https://amplitude.com), a product intelligence and behavioral analytics platform.
## Installation
Install the Amplitude peer dependency:
## Basic Usage
```tsx title="app/providers.tsx"
import { AnalyticsProvider, amplitudePlugin } from '@tour-kit/analytics'
export function Providers({ children }) {
return (
{children}
)
}
```
## Options
## Event Names
Events are prefixed with `tourkit_` by default:
| userTourKit Event | Amplitude Event Name |
|----------------|---------------------|
| `tour_started` | `tourkit_tour_started` |
| `step_viewed` | `tourkit_step_viewed` |
| `hint_clicked` | `tourkit_hint_clicked` |
### Custom Prefix
```tsx
amplitudePlugin({
apiKey: 'xxx',
eventPrefix: 'app_tour_'
})
// Events: app_tour_tour_started, app_tour_step_viewed, etc.
```
### No Prefix
```tsx
amplitudePlugin({
apiKey: 'xxx',
eventPrefix: ''
})
// Events: tour_started, step_viewed, etc.
```
## Event Properties
Events include these properties in Amplitude:
```ts
{
tour_id: string // Tour identifier
step_id?: string // Step identifier
step_index?: number // Current step index (0-based)
total_steps?: number // Total steps in tour
duration_ms?: number // Duration in milliseconds
...metadata // Custom metadata from event
}
```
## EU Data Residency
For EU data residency, set the server URL to Amplitude's EU endpoint:
```tsx
amplitudePlugin({
apiKey: process.env.NEXT_PUBLIC_AMPLITUDE_KEY!,
serverUrl: 'https://api.eu.amplitude.com/2/httpapi'
})
```
## User Identification
Identify users and set user properties:
```tsx title="app/analytics-wrapper.tsx"
'use client'
import { AnalyticsProvider, amplitudePlugin } from '@tour-kit/analytics'
import { useUser } from '@/lib/auth'
export function AnalyticsWrapper({ children }) {
const user = useUser()
return (
{children}
)
}
```
## Complete Example
```tsx title="app/providers.tsx"
'use client'
import { AnalyticsProvider, amplitudePlugin, consolePlugin } from '@tour-kit/analytics'
import { useUser } from '@/lib/auth'
const isDev = process.env.NODE_ENV === 'development'
const isEU = process.env.NEXT_PUBLIC_REGION === 'eu'
export function Providers({ children }) {
const user = useUser()
return (
{children}
)
}
```
## Manual Flush
The Amplitude plugin supports manual flushing of queued events:
```tsx
import { useAnalytics } from '@tour-kit/analytics'
function FlushButton() {
const analytics = useAnalytics()
const handleFlush = async () => {
await analytics.flush()
console.log('Events flushed to Amplitude')
}
return Flush Events
}
```
This is useful when:
- User is about to navigate away
- Completing a critical action
- Handling errors or crashes
## Analytics in Amplitude
### Tour Funnel Analysis
Create a funnel to analyze tour completion:
1. Go to Analytics > Funnels
2. Add steps:
- `tourkit_tour_started`
- `tourkit_step_viewed` (filter: step_index = 0)
- `tourkit_step_viewed` (filter: step_index = 1)
- `tourkit_tour_completed`
3. Break down by: `tour_id`
### Retention by Tour Completion
Compare retention of users who completed tours:
1. Go to Analytics > Retention
2. Entry Event: `tourkit_tour_started`
3. Return Event: Any active event
4. Cohort by: `tourkit_tour_completed` (Yes/No)
### Step Duration Analysis
Analyze time spent on each step:
1. Go to Analytics > Event Segmentation
2. Select: `tourkit_step_completed`
3. Measure: Average `duration_ms`
4. Group by: `step_id`
5. Chart type: Bar chart
### User Journeys
Visualize user paths through tours:
1. Go to Analytics > Journeys
2. Start event: `tourkit_tour_started`
3. End event: `tourkit_tour_completed` or `tourkit_tour_skipped`
4. Filter by: `tour_id`
## User Properties
Amplitude automatically includes user properties in events:
```tsx
{children}
```
These properties can be used to:
- Segment funnels and retention
- Create user cohorts
- Analyze feature adoption by user type
## Behavioral Cohorts
Create cohorts based on tour behavior:
1. Go to Audiences > Create Cohort
2. Definition: User performed `tourkit_tour_completed` where `tour_id` = "onboarding"
3. Use cohort to:
- Compare feature adoption
- Analyze product usage patterns
- Target messaging
## Best Practices
### Use Descriptive Metadata
Add context to events with metadata:
```tsx
analytics.track('step_interaction', {
tourId: 'feature-tour',
stepId: 'video-step',
metadata: {
interaction_type: 'video_played',
video_duration: 120,
completion_percentage: 75
}
})
```
### Track Meaningful Milestones
Beyond basic tour events, track adoption milestones:
```tsx
analytics.track('feature_adopted', {
tourId: 'advanced-search',
metadata: {
feature_name: 'saved-filters',
usage_count: 5,
days_since_tour: 3
}
})
```
### Avoid Over-Tracking
Don't track every micro-interaction:
```tsx
// ✅ Good - Track meaningful events
analytics.stepCompleted(tourId, stepId, stepIndex)
// ❌ Bad - Don't track every hover or mouse move
analytics.track('mouse_moved', { x, y })
```
### Production Only
Keep development events out of production analytics:
```tsx
const plugins = process.env.NODE_ENV === 'production'
? [amplitudePlugin({ apiKey: process.env.NEXT_PUBLIC_AMPLITUDE_KEY! })]
: [consolePlugin()]
```
## Troubleshooting
### Events Not Appearing
Check these common issues:
1. **API key is correct**: Verify your Amplitude API key
2. **Ad blockers**: Some ad blockers may block Amplitude
3. **Network errors**: Check browser console for failed requests
4. **Event batching**: Amplitude batches events - they may take a few seconds to appear
### Events Delayed
Amplitude batches events by default for performance. To see events immediately:
```tsx
import { useAnalytics } from '@tour-kit/analytics'
// Flush events manually
const analytics = useAnalytics()
await analytics.flush()
```
### User Properties Not Set
Ensure user properties are passed to the provider:
```tsx
{children}
```
## Related
- [Amplitude Documentation](https://www.docs.developers.amplitude.com/)
- [Plugin Overview](/docs/analytics/plugins) - Plugin architecture
- [AnalyticsProvider](/docs/analytics/providers) - Provider configuration
---
# Console Plugin
> Console analytics plugin: log all tour events with colored output for debugging analytics integration in development
## Overview
The console plugin outputs analytics events to the browser console with colored formatting. It's designed for development and debugging - not for production use.
## Installation
No additional dependencies required. The plugin is included with `@tour-kit/analytics`.
```tsx
import { consolePlugin } from '@tour-kit/analytics'
```
## Basic Usage
```tsx title="app/providers.tsx"
import { AnalyticsProvider, consolePlugin } from '@tour-kit/analytics'
export function Providers({ children }) {
return (
{children}
)
}
```
## Options
## Custom Prefix
Change the log prefix:
```tsx
consolePlugin({
prefix: '📊 Analytics'
})
// Output:
// 📊 Analytics tour_started
```
## Collapsed Groups
Use collapsed console groups to reduce noise:
```tsx
consolePlugin({
collapsed: true
})
```
Events will be logged in collapsed groups that can be expanded in the console.
## Custom Colors
Customize colors for different event types:
```tsx
consolePlugin({
colors: {
tour: '#3b82f6', // Blue
step: '#22c55e', // Green
hint: '#f97316' // Orange
}
})
```
## Console Output Format
Each event is logged as a group with the following structure:
```
🎯 userTourKit tour_started
Tour: onboarding
Step: welcome (0/5)
Duration: 1234ms
Metadata: { source: 'dashboard' }
Timestamp: 2024-01-20T10:30:00.000Z
```
### Event with Metadata
```tsx
analytics.track('step_completed', {
tourId: 'onboarding',
stepId: 'welcome',
stepIndex: 0,
metadata: {
source: 'dashboard',
user_type: 'free'
}
})
```
Console output:
```
🎯 userTourKit step_completed
Tour: onboarding
Step: welcome (0/5)
Duration: 2340ms
Metadata: { source: 'dashboard', user_type: 'free' }
Timestamp: 2024-01-20T10:30:02.340Z
```
## Development Only
Conditionally load the console plugin in development:
```tsx title="lib/analytics.ts"
import {
consolePlugin,
posthogPlugin,
type AnalyticsConfig
} from '@tour-kit/analytics'
const isDev = process.env.NODE_ENV === 'development'
export const analyticsConfig: AnalyticsConfig = {
plugins: [
...(isDev ? [consolePlugin({ collapsed: false })] : []),
posthogPlugin({
apiKey: process.env.NEXT_PUBLIC_POSTHOG_KEY!
})
]
}
```
## Debugging with Console Plugin
Use the console plugin alongside production plugins to debug:
```tsx
import {
AnalyticsProvider,
consolePlugin,
posthogPlugin
} from '@tour-kit/analytics'
{children}
```
## User Identification
The console plugin also logs user identification:
```tsx
analytics.identify('user-123', {
email: 'user@example.com',
plan: 'pro'
})
```
Console output:
```
🎯 userTourKit User Identified: user-123
{ email: 'user@example.com', plan: 'pro' }
```
## Example Output
### Tour Started
```
🎯 userTourKit tour_started
Tour: product-tour
Duration: 0ms
Metadata: { totalSteps: 5 }
Timestamp: 2024-01-20T10:30:00.000Z
```
### Step Viewed
```
🎯 userTourKit step_viewed
Tour: product-tour
Step: create-project (1/5)
Metadata: { source: 'dashboard' }
Timestamp: 2024-01-20T10:30:01.000Z
```
### Hint Clicked
```
🎯 userTourKit hint_clicked
Tour: keyboard-shortcuts-hint
Metadata: { position: 'bottom-right' }
Timestamp: 2024-01-20T10:30:05.000Z
```
## Best Practices
### Recommended Setup
```tsx
// ✅ Good - Only in development
const plugins = process.env.NODE_ENV === 'development'
? [consolePlugin(), productionPlugin()]
: [productionPlugin()]
// ❌ Bad - Always loaded
const plugins = [consolePlugin(), productionPlugin()]
```
### Debug Alongside Production
```tsx
// ✅ Good - Debug flag controls console plugin
{children}
```
## Related
- [Plugin Overview](/docs/analytics/plugins) - Plugin system architecture
- [AnalyticsProvider](/docs/analytics/providers) - Configure analytics
- [Custom Plugins](/docs/analytics/plugins/custom) - Build your own plugin
---
# Custom Analytics Plugins
> Build custom analytics plugins by implementing the AnalyticsPlugin interface with track, identify, and flush methods
## Overview
userTourKit's plugin system allows you to integrate with any analytics platform by implementing a simple interface. Create custom plugins for internal tracking systems, CRM integrations, or any data pipeline.
## Plugin Interface
All plugins must implement the `AnalyticsPlugin` interface:
```ts
interface AnalyticsPlugin {
/** Unique plugin identifier */
name: string
/** Initialize the plugin (called once on setup) */
init?: () => void | Promise
/** Track an event */
track: (event: TourEvent) => void | Promise
/** Identify a user */
identify?: (userId: string, properties?: Record) => void
/** Flush any queued events */
flush?: () => void | Promise
/** Clean up resources */
destroy?: () => void
}
```
Only `name` and `track` are required. All other methods are optional.
## Minimal Plugin
A basic plugin that logs events:
```ts
import type { AnalyticsPlugin, TourEvent } from '@tour-kit/analytics'
export const simplePlugin = (): AnalyticsPlugin => ({
name: 'simple-logger',
track(event: TourEvent) {
console.log('Tour event:', event.eventName, event)
}
})
```
Usage:
```tsx
{children}
```
## Event Structure
Events passed to `track()` include:
```ts
interface TourEvent {
// Required
eventName: TourEventName // 'tour_started' | 'step_viewed' | etc.
timestamp: number // Unix timestamp in milliseconds
sessionId: string // Unique session identifier
tourId: string // Tour identifier
// Optional
stepId?: string // Current step identifier
stepIndex?: number // Current step index (0-based)
totalSteps?: number // Total steps in tour
userId?: string // User identifier
userProperties?: Record
duration?: number // Duration in milliseconds
interactionCount?: number // Number of interactions
metadata?: Record // Custom metadata
}
```
## Complete Plugin Example
A plugin that sends events to a custom API:
```ts title="lib/analytics/custom-api-plugin.ts"
import type { AnalyticsPlugin, TourEvent } from '@tour-kit/analytics'
interface CustomApiPluginOptions {
apiEndpoint: string
apiKey: string
batchSize?: number
}
export function customApiPlugin(options: CustomApiPluginOptions): AnalyticsPlugin {
const eventQueue: TourEvent[] = []
const batchSize = options.batchSize ?? 10
async function sendEvents(events: TourEvent[]) {
try {
await fetch(options.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${options.apiKey}`
},
body: JSON.stringify({ events })
})
} catch (error) {
console.error('Failed to send events:', error)
}
}
async function flushQueue() {
if (eventQueue.length === 0) return
await sendEvents([...eventQueue])
eventQueue.length = 0
}
return {
name: 'custom-api',
async init() {
// Test API connection
console.log('Custom API plugin initialized')
},
async track(event: TourEvent) {
eventQueue.push(event)
if (eventQueue.length >= batchSize) {
await flushQueue()
}
},
identify(userId: string, properties?: Record) {
// Send user identification to API
fetch(`${options.apiEndpoint}/identify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${options.apiKey}`
},
body: JSON.stringify({ userId, properties })
})
},
async flush() {
await flushQueue()
},
destroy() {
// Clean up
eventQueue.length = 0
}
}
}
```
Usage:
```tsx
{children}
```
## Plugin Patterns
### Event Batching
Batch events to reduce API calls:
```ts
export function batchedPlugin(): AnalyticsPlugin {
const queue: TourEvent[] = []
let timer: NodeJS.Timeout | null = null
const flush = () => {
if (queue.length === 0) return
console.log('Sending batch:', queue.length, 'events')
// Send to API
queue.length = 0
}
return {
name: 'batched-plugin',
track(event: TourEvent) {
queue.push(event)
// Auto-flush every 5 seconds
if (timer) clearTimeout(timer)
timer = setTimeout(flush, 5000)
// Or flush when queue reaches limit
if (queue.length >= 10) {
flush()
}
},
flush,
destroy() {
if (timer) clearTimeout(timer)
flush()
}
}
}
```
### Event Filtering
Filter events before sending:
```ts
export function filteredPlugin(): AnalyticsPlugin {
return {
name: 'filtered-plugin',
track(event: TourEvent) {
// Only track tour completion events
if (event.eventName !== 'tour_completed') {
return
}
// Only track specific tours
if (!event.tourId.startsWith('onboarding-')) {
return
}
console.log('Tracking:', event)
// Send to analytics
}
}
}
```
### Event Transformation
Transform events before sending:
```ts
export function transformPlugin(): AnalyticsPlugin {
return {
name: 'transform-plugin',
track(event: TourEvent) {
// Transform to custom format
const transformed = {
type: event.eventName,
tour: event.tourId,
step: event.stepId,
time: new Date(event.timestamp).toISOString(),
// Flatten metadata
...event.metadata
}
console.log('Transformed:', transformed)
// Send to API
}
}
}
```
### Local Storage Plugin
Persist events to localStorage:
```ts
export function localStoragePlugin(): AnalyticsPlugin {
const STORAGE_KEY = 'tour-analytics-events'
return {
name: 'local-storage',
track(event: TourEvent) {
const stored = localStorage.getItem(STORAGE_KEY)
const events = stored ? JSON.parse(stored) : []
events.push(event)
localStorage.setItem(STORAGE_KEY, JSON.stringify(events))
},
flush() {
// Clear stored events
localStorage.removeItem(STORAGE_KEY)
}
}
}
```
### Webhook Plugin
Send events to a webhook:
```ts
interface WebhookPluginOptions {
url: string
headers?: Record
}
export function webhookPlugin(options: WebhookPluginOptions): AnalyticsPlugin {
return {
name: 'webhook',
async track(event: TourEvent) {
await fetch(options.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options.headers
},
body: JSON.stringify(event)
})
}
}
}
```
### Segment Plugin
Integrate with Segment:
```ts
declare global {
interface Window {
analytics?: {
track: (event: string, properties: Record) => void
identify: (userId: string, traits?: Record) => void
}
}
}
export function segmentPlugin(): AnalyticsPlugin {
return {
name: 'segment',
track(event: TourEvent) {
if (!window.analytics) return
window.analytics.track(event.eventName, {
tour_id: event.tourId,
step_id: event.stepId,
step_index: event.stepIndex,
total_steps: event.totalSteps,
duration_ms: event.duration,
...event.metadata
})
},
identify(userId: string, properties?: Record) {
if (!window.analytics) return
window.analytics.identify(userId, properties)
}
}
}
```
## Testing Plugins
### Unit Testing
Test plugins in isolation:
```ts title="__tests__/custom-plugin.test.ts"
import { describe, it, expect, vi } from 'vitest'
import { customApiPlugin } from '../custom-api-plugin'
import type { TourEvent } from '@tour-kit/analytics'
describe('customApiPlugin', () => {
it('sends events to API', async () => {
const fetchSpy = vi.spyOn(global, 'fetch')
const plugin = customApiPlugin({
apiEndpoint: 'https://api.test.com',
apiKey: 'test-key'
})
const event: TourEvent = {
eventName: 'tour_started',
timestamp: Date.now(),
sessionId: 'session-123',
tourId: 'onboarding',
totalSteps: 5
}
await plugin.track(event)
expect(fetchSpy).toHaveBeenCalledWith(
'https://api.test.com',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Authorization': 'Bearer test-key'
})
})
)
})
})
```
### Integration Testing
Test plugins with the provider:
```tsx title="__tests__/plugin-integration.test.tsx"
import { render } from '@testing-library/react'
import { AnalyticsProvider } from '@tour-kit/analytics'
import { customApiPlugin } from '../custom-api-plugin'
describe('Plugin integration', () => {
it('receives events from provider', async () => {
const trackSpy = vi.fn()
const testPlugin = {
name: 'test',
track: trackSpy
}
render(
)
// Trigger event in component
// ...
expect(trackSpy).toHaveBeenCalled()
})
})
```
## Best Practices
### Error Handling
Always handle errors gracefully:
```ts
export function resilientPlugin(): AnalyticsPlugin {
return {
name: 'resilient',
async track(event: TourEvent) {
try {
await sendToAPI(event)
} catch (error) {
console.error('Analytics error:', error)
// Don't throw - let other plugins continue
}
}
}
}
```
### Type Safety
Use TypeScript for full type safety:
```ts
import type { AnalyticsPlugin, TourEvent, TourEventName } from '@tour-kit/analytics'
// Typed options
interface MyPluginOptions {
apiKey: string
endpoint: string
debug?: boolean
}
// Typed plugin factory
export function myPlugin(options: MyPluginOptions): AnalyticsPlugin {
return {
name: 'my-plugin',
track(event: TourEvent) {
// Full autocomplete for event properties
const eventName: TourEventName = event.eventName
// ...
}
}
}
```
### Async Operations
Support both sync and async tracking:
```ts
export function asyncPlugin(): AnalyticsPlugin {
return {
name: 'async-plugin',
async track(event: TourEvent) {
// Can use await
await fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify(event)
})
}
}
}
```
### Resource Cleanup
Clean up resources in `destroy()`:
```ts
export function cleanPlugin(): AnalyticsPlugin {
let interval: NodeJS.Timeout | null = null
return {
name: 'clean-plugin',
init() {
interval = setInterval(() => {
console.log('Heartbeat')
}, 60000)
},
track(event: TourEvent) {
// ...
},
destroy() {
if (interval) {
clearInterval(interval)
interval = null
}
}
}
}
```
## Publishing Plugins
To share your plugin with others:
1. **Create a package**:
```json title="package.json"
{
"name": "@yourcompany/tourkit-analytics-plugin",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"peerDependencies": {
"@tour-kit/analytics": "^1.0.0"
}
}
```
2. **Export your plugin**:
```ts title="src/index.ts"
export { myPlugin } from './my-plugin'
export type { MyPluginOptions } from './my-plugin'
```
3. **Document usage**:
```md title="README.md"
# My userTourKit Plugin
## Installation
\`\`\`bash
npm install @yourcompany/tourkit-analytics-plugin
\`\`\`
## Usage
\`\`\`tsx
import { myPlugin } from '@yourcompany/tourkit-analytics-plugin'
{children}
\`\`\`
```
## Related
- [Plugin Overview](/docs/analytics/plugins) - Plugin system architecture
- [Types](/docs/analytics/types) - Full type reference
- [AnalyticsProvider](/docs/analytics/providers) - Provider configuration
---
# Google Analytics Plugin
> Google Analytics 4 plugin: send tour events as GA4 custom events with automatic parameter mapping and measurement ID
## Overview
The Google Analytics plugin sends tour and hint events to [Google Analytics 4](https://support.google.com/analytics/answer/10089681) using the gtag.js library.
## Installation
No additional dependencies required - the plugin uses the global `gtag` function that must already be loaded on your page.
## Setup gtag.js
Add the Google Analytics script to your HTML:
```html title="app/layout.tsx"
export default function RootLayout({ children }) {
return (
{/* Google Analytics */}
{children}
)
}
```
## Basic Usage
```tsx title="app/providers.tsx"
import { AnalyticsProvider, googleAnalyticsPlugin } from '@tour-kit/analytics'
export function Providers({ children }) {
return (
{children}
)
}
```
## Options
## Event Names
Events are prefixed with `tourkit_` by default:
| userTourKit Event | GA4 Event Name |
|----------------|----------------|
| `tour_started` | `tourkit_tour_started` |
| `step_viewed` | `tourkit_step_viewed` |
| `hint_clicked` | `tourkit_hint_clicked` |
### Custom Prefix
```tsx
googleAnalyticsPlugin({
measurementId: 'G-XXXXXXXXXX',
eventPrefix: 'app_tour_'
})
// Events: app_tour_tour_started, app_tour_step_viewed, etc.
```
### No Prefix
```tsx
googleAnalyticsPlugin({
measurementId: 'G-XXXXXXXXXX',
eventPrefix: ''
})
// Events: tour_started, step_viewed, etc.
```
## Event Parameters
Events include these parameters in GA4:
```ts
{
tour_id: string // Tour identifier
step_id?: string // Step identifier
step_index?: number // Current step index (0-based)
total_steps?: number // Total steps in tour
duration_ms?: number // Duration in milliseconds
...metadata // Custom metadata from event
}
```
## User Identification
Set user ID in Google Analytics:
```tsx title="app/analytics-wrapper.tsx"
'use client'
import { AnalyticsProvider, googleAnalyticsPlugin } from '@tour-kit/analytics'
import { useUser } from '@/lib/auth'
export function AnalyticsWrapper({ children }) {
const user = useUser()
return (
{children}
)
}
```
## Complete Example with Next.js
```tsx title="app/layout.tsx"
import { AnalyticsProvider, googleAnalyticsPlugin } from '@tour-kit/analytics'
import Script from 'next/script'
const GA_ID = process.env.NEXT_PUBLIC_GA_ID!
export default function RootLayout({ children }) {
return (
{/* Google Analytics */}
{children}
)
}
```
## Analytics in Google Analytics
### Tour Completion Funnel
Create a funnel exploration to analyze tour completion:
1. Go to Explore > Funnel exploration
2. Add steps:
- `tourkit_tour_started`
- `tourkit_step_viewed` (parameter: step_index = 0)
- `tourkit_step_viewed` (parameter: step_index = 1)
- `tourkit_tour_completed`
3. Breakdown by: `tour_id` parameter
### Step Engagement
View which steps get the most interaction:
1. Go to Reports > Engagement > Events
2. Filter by event name contains "tourkit_step_"
3. Add dimension: `step_id` parameter
4. Add metric: Event count
### Custom Report
Create a custom report for tour analytics:
1. Go to Explore > Blank
2. Dimensions: `Event name`, `tour_id`, `step_id`
3. Metrics: `Event count`, `Total users`
4. Filters: Event name contains "tourkit_"
### Conversion Events
Mark tour completion as a conversion:
1. Go to Configure > Events
2. Find `tourkit_tour_completed`
3. Toggle "Mark as conversion"
Now tour completions will appear in conversion reports.
## GA4 Limitations
### Event Parameter Limits
GA4 has strict limits on custom parameters:
- Maximum 25 custom parameters per event
- Parameter names: Up to 40 characters
- Parameter values: Up to 100 characters
userTourKit events stay well within these limits.
### Reserved Names
Avoid these reserved parameter names:
- `user_id`
- `user_properties`
- `timestamp_micros`
- Any name starting with `google_` or `ga_`
userTourKit uses safe parameter names like `tour_id`, `step_id`, etc.
## Privacy & Compliance
### Cookie Consent
Respect user consent before loading Google Analytics:
```tsx title="app/analytics-wrapper.tsx"
'use client'
import { AnalyticsProvider, googleAnalyticsPlugin } from '@tour-kit/analytics'
import { useCookieConsent } from '@/lib/consent'
import Script from 'next/script'
export function AnalyticsWrapper({ children }) {
const consent = useCookieConsent()
return (
<>
{consent.analytics && (
)}
{children}
>
)
}
```
### User ID and PII
Never send personally identifiable information (PII) to Google Analytics:
```tsx
// ❌ Bad - Don't send email or name
userId: user.email
// ✅ Good - Use opaque identifier
userId: user.id
```
## Debugging
### Check gtag is Loaded
The plugin will warn in the console if gtag is not available:
```
Analytics: gtag not found. Make sure Google Analytics is loaded on the page.
```
### Enable Debug Mode
Use GA4 DebugView to see events in real-time:
```tsx
// Add to your gtag config
gtag('config', 'G-XXXXXXXXXX', {
debug_mode: true
})
```
Then view events in GA4:
1. Go to Configure > DebugView
2. Events appear in real-time
### Console Debugging
Use the console plugin alongside GA4:
```tsx
{children}
```
## Best Practices
### Production Only
Load GA4 only in production:
```tsx
const plugins = process.env.NODE_ENV === 'production'
? [googleAnalyticsPlugin({ measurementId: process.env.NEXT_PUBLIC_GA_ID! })]
: [consolePlugin()]
```
### Use Google Tag Manager
For more control, load Google Analytics through [Google Tag Manager](https://tagmanager.google.com):
```html
```
Then use the userTourKit plugin as normal.
### Descriptive Event Names
Keep event names descriptive but concise:
```tsx
// ✅ Good
googleAnalyticsPlugin({
measurementId: 'G-XXXXXXXXXX',
eventPrefix: 'onboarding_'
})
// ❌ Bad - Too verbose
googleAnalyticsPlugin({
measurementId: 'G-XXXXXXXXXX',
eventPrefix: 'tour_kit_user_onboarding_flow_'
})
```
## Troubleshooting
### Events Not Appearing
Common issues:
1. **gtag not loaded**: Check browser console for warnings
2. **Ad blockers**: Users with ad blockers won't send events
3. **Sampling**: GA4 may sample data in high-traffic properties
4. **Processing delay**: Events can take 24-48 hours to appear in reports
### gtag is undefined
Ensure gtag loads before userTourKit:
```tsx
// ✅ Good - gtag loads first
...
// ❌ Bad - Race condition
...
```
## Related
- [Google Analytics 4 Documentation](https://support.google.com/analytics/answer/10089681)
- [Plugin Overview](/docs/analytics/plugins) - Plugin architecture
- [AnalyticsProvider](/docs/analytics/providers) - Provider configuration
---
# Mixpanel Plugin
> Mixpanel analytics plugin: track tour funnel events, step completions, and user engagement with Mixpanel properties
## Overview
The Mixpanel plugin sends tour and hint events to [Mixpanel](https://mixpanel.com), an event tracking and user analytics platform.
## Installation
Install the Mixpanel peer dependency:
## Basic Usage
```tsx title="app/providers.tsx"
import { AnalyticsProvider, mixpanelPlugin } from '@tour-kit/analytics'
export function Providers({ children }) {
return (
{children}
)
}
```
## Options
## Event Names
Events are prefixed with `userTourKit: ` by default:
| userTourKit Event | Mixpanel Event Name |
|----------------|-------------------|
| `tour_started` | `userTourKit: tour_started` |
| `step_viewed` | `userTourKit: step_viewed` |
| `hint_clicked` | `userTourKit: hint_clicked` |
### Custom Prefix
```tsx
mixpanelPlugin({
token: 'xxx',
eventPrefix: 'App Tour - '
})
// Events: App Tour - tour_started, App Tour - step_viewed, etc.
```
### No Prefix
```tsx
mixpanelPlugin({
token: 'xxx',
eventPrefix: ''
})
// Events: tour_started, step_viewed, etc.
```
## Event Properties
Events include these properties in Mixpanel:
```ts
{
tour_id: string // Tour identifier
step_id?: string // Step identifier
step_index?: number // Current step index (0-based)
total_steps?: number // Total steps in tour
duration_ms?: number // Duration in milliseconds
...metadata // Custom metadata from event
}
```
## User Identification
Identify users and set user properties:
```tsx title="app/analytics-wrapper.tsx"
'use client'
import { AnalyticsProvider, mixpanelPlugin } from '@tour-kit/analytics'
import { useUser } from '@/lib/auth'
export function AnalyticsWrapper({ children }) {
const user = useUser()
return (
{children}
)
}
```
## Debug Mode
Enable Mixpanel's debug mode to see events in the console:
```tsx
mixpanelPlugin({
token: process.env.NEXT_PUBLIC_MIXPANEL_TOKEN!,
debug: true
})
```
Debug output shows:
- Events being sent
- Properties included
- API responses
- Error messages
## EU Data Residency
For EU data residency, initialize Mixpanel separately with a custom API host:
```tsx
import mixpanel from 'mixpanel-browser'
// Initialize Mixpanel with EU endpoint
mixpanel.init('YOUR_TOKEN', {
api_host: 'https://api-eu.mixpanel.com'
})
// Then use the userTourKit plugin
{children}
```
## Complete Example
```tsx title="app/providers.tsx"
'use client'
import { AnalyticsProvider, mixpanelPlugin, consolePlugin } from '@tour-kit/analytics'
import { useUser } from '@/lib/auth'
const isDev = process.env.NODE_ENV === 'development'
export function Providers({ children }) {
const user = useUser()
return (
{children}
)
}
```
## Analytics in Mixpanel
### Tour Completion Funnel
Create a funnel to analyze tour completion:
1. Go to Insights > Funnels
2. Add steps:
- `userTourKit: tour_started`
- `userTourKit: step_viewed` (filter: step_index = 0)
- `userTourKit: step_viewed` (filter: step_index = 1)
- `userTourKit: tour_completed`
3. Group by: `tour_id`
### Step Duration Analysis
Analyze how long users spend on each step:
1. Go to Insights > Insights
2. Event: `userTourKit: step_completed`
3. Aggregate: Average of `duration_ms`
4. Group by: `step_id`
### User Cohorts
Create cohorts based on tour behavior:
1. Go to Cohorts > Create Cohort
2. Definition: `userTourKit: tour_completed` where `tour_id` = "onboarding"
3. Name: "Completed Onboarding"
Use this cohort to:
- Compare retention between users who completed tours vs. those who didn't
- Target messaging to users who skipped tours
- Analyze feature adoption after tour completion
## User Profiles
View tour activity on user profiles:
1. Go to Users > User Profile
2. Select a user
3. View Events tab
4. Filter by "userTourKit:" to see all tour events
## Retention Analysis
Track retention based on tour completion:
1. Go to Insights > Retention
2. First event: `userTourKit: tour_started`
3. Return event: Any event
4. Segment by: `tour_id`
## Best Practices
### Use Mixpanel Special Properties
Leverage Mixpanel's special properties for better user profiles:
```tsx
userProperties: {
$email: user.email, // Sets email in People profile
$name: user.name, // Sets name in People profile
$created: user.createdAt, // Sets signup date
plan: user.plan // Custom property
}
```
### Group Properties by Context
Use Mixpanel's group analytics for B2B SaaS:
```tsx
import mixpanel from 'mixpanel-browser'
// Set company context
mixpanel.set_group('company_id', 'acme-corp')
// Track with company context
mixpanelPlugin({
token: 'YOUR_TOKEN'
})
```
### Production Only
Avoid polluting production data with development events:
```tsx
const plugins = process.env.NODE_ENV === 'production'
? [mixpanelPlugin({ token: process.env.NEXT_PUBLIC_MIXPANEL_TOKEN! })]
: [consolePlugin()]
```
## Troubleshooting
### Events Not Appearing
Check these common issues:
1. **Token is correct**: Verify your Mixpanel project token
2. **Ad blockers**: Mixpanel may be blocked by ad blockers
3. **CORS**: Ensure your domain is whitelisted in Mixpanel settings
4. **Debug mode**: Enable debug mode to see console logs
```tsx
mixpanelPlugin({
token: 'YOUR_TOKEN',
debug: true // See events in console
})
```
### User Not Identified
Ensure you're passing `userId` to the provider:
```tsx
{children}
```
## Related
- [Mixpanel Documentation](https://developer.mixpanel.com/docs)
- [Plugin Overview](/docs/analytics/plugins) - Plugin architecture
- [AnalyticsProvider](/docs/analytics/providers) - Provider configuration
---
# PostHog Plugin
> PostHog analytics plugin: capture tour events as PostHog actions with automatic feature flag and person property sync
## Overview
The PostHog plugin sends tour and hint events to [PostHog](https://posthog.com), a product analytics platform with session replay, feature flags, and experimentation.
## Installation
Install the PostHog peer dependency:
## Basic Usage
```tsx title="app/providers.tsx"
import { AnalyticsProvider, posthogPlugin } from '@tour-kit/analytics'
export function Providers({ children }) {
return (
{children}
)
}
```
## Options
## Self-Hosted PostHog
Use a custom API host for self-hosted instances:
```tsx
posthogPlugin({
apiKey: 'phc_xxx',
apiHost: 'https://posthog.yourcompany.com'
})
```
## EU Cloud
Use PostHog's EU cloud:
```tsx
posthogPlugin({
apiKey: 'phc_xxx',
apiHost: 'https://eu.posthog.com'
})
```
## Event Names
Events are prefixed with `tourkit_` by default:
| userTourKit Event | PostHog Event Name |
|----------------|-------------------|
| `tour_started` | `tourkit_tour_started` |
| `step_viewed` | `tourkit_step_viewed` |
| `hint_clicked` | `tourkit_hint_clicked` |
### Custom Prefix
```tsx
posthogPlugin({
apiKey: 'phc_xxx',
eventPrefix: 'app_tour_'
})
// Events: app_tour_tour_started, app_tour_step_viewed, etc.
```
### No Prefix
```tsx
posthogPlugin({
apiKey: 'phc_xxx',
eventPrefix: ''
})
// Events: tour_started, step_viewed, etc.
```
## Event Properties
Events include these properties in PostHog:
```ts
{
tour_id: string // Tour identifier
step_id?: string // Step identifier
step_index?: number // Current step index (0-based)
total_steps?: number // Total steps in tour
duration_ms?: number // Duration in milliseconds
session_id: string // userTourKit session ID
...metadata // Custom metadata from event
}
```
## User Identification
Identify users in PostHog:
```tsx title="app/analytics-wrapper.tsx"
'use client'
import { AnalyticsProvider, posthogPlugin } from '@tour-kit/analytics'
import { useUser } from '@/lib/auth'
export function AnalyticsWrapper({ children }) {
const user = useUser()
return (
{children}
)
}
```
## Session Replay
PostHog's session replay automatically captures tour interactions when enabled:
```tsx
// In your PostHog initialization (separate from userTourKit)
posthog.init('phc_xxx', {
api_host: 'https://app.posthog.com',
session_recording: {
recordCrossOriginIframes: true
}
})
// userTourKit plugin (minimal config)
posthogPlugin({
apiKey: 'phc_xxx'
})
```
## Feature Flags
Use PostHog feature flags to control tour visibility:
```tsx
'use client'
import { usePostHog } from 'posthog-js/react'
import { Tour } from '@tour-kit/react'
export function ConditionalTour() {
const posthog = usePostHog()
const showTour = posthog.isFeatureEnabled('new-user-tour')
if (!showTour) return null
return (
{/* tour steps */}
)
}
```
## Complete Example
```tsx title="app/providers.tsx"
'use client'
import { AnalyticsProvider, posthogPlugin } from '@tour-kit/analytics'
import { PostHogProvider } from 'posthog-js/react'
import posthog from 'posthog-js'
import { useEffect } from 'react'
// Initialize PostHog SDK
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: 'https://app.posthog.com',
autocapture: false,
capture_pageview: false,
session_recording: {
recordCrossOriginIframes: true
}
})
}
export function Providers({ children, user }) {
useEffect(() => {
if (user) {
posthog.identify(user.id, {
email: user.email,
name: user.name
})
}
}, [user])
return (
{children}
)
}
```
## Debugging
Enable debug mode to see events in the console:
```tsx
{children}
```
## Analytics in PostHog
### Tour Funnel Analysis
Create a funnel to see tour completion rates:
1. Go to Insights > Funnels
2. Add steps:
- `tourkit_tour_started`
- `tourkit_step_viewed` (filter: step_index = 0)
- `tourkit_step_viewed` (filter: step_index = 1)
- `tourkit_tour_completed`
### Step Engagement
Track which steps users interact with most:
1. Go to Insights > Trends
2. Add event: `tourkit_step_interaction`
3. Break down by: `step_id`
### Hint Performance
Analyze hint click-through rates:
1. Go to Insights > Trends
2. Add events:
- `tourkit_hint_shown`
- `tourkit_hint_clicked`
3. View as: Conversion rate
## Best Practices
### Avoid Duplicate Initialization
Don't initialize PostHog twice - use the userTourKit plugin instead of loading PostHog separately if you only need tour analytics:
```tsx
// ✅ Good - userTourKit plugin handles PostHog
{children}
// ❌ Bad - Don't do both unless you need PostHog features
posthog.init('phc_xxx')
{children}
```
### Production Only
Load PostHog only in production to avoid polluting analytics:
```tsx
const plugins = process.env.NODE_ENV === 'production'
? [posthogPlugin({ apiKey: process.env.NEXT_PUBLIC_POSTHOG_KEY! })]
: [consolePlugin()]
```
## Related
- [PostHog Documentation](https://posthog.com/docs)
- [Plugin Overview](/docs/analytics/plugins) - Plugin architecture
- [AnalyticsProvider](/docs/analytics/providers) - Provider configuration
---
# AnalyticsProvider
> AnalyticsProvider component: configure one or more analytics plugins and provide the tracker to your React component tree
## Overview
The `AnalyticsProvider` component wraps your application to enable analytics tracking across all tour and hint components. It manages plugin initialization, event batching, and automatic cleanup.
## Basic Usage
```tsx title="app/layout.tsx"
import { AnalyticsProvider, consolePlugin } from '@tour-kit/analytics'
export default function RootLayout({ children }) {
return (
{children}
)
}
```
## Props
## AnalyticsConfig
',
default: 'undefined',
description: 'User properties to include with identification'
},
globalProperties: {
type: 'Record',
default: 'undefined',
description: 'Properties to include with every tracked event'
}
}}
/>
## Single Plugin
Use one analytics provider for simple setups:
```tsx title="app/providers.tsx"
import { AnalyticsProvider, posthogPlugin } from '@tour-kit/analytics'
export function Providers({ children }) {
return (
{children}
)
}
```
## Multiple Plugins
Send events to multiple analytics platforms simultaneously:
```tsx title="app/providers.tsx"
import {
AnalyticsProvider,
consolePlugin,
posthogPlugin,
mixpanelPlugin
} from '@tour-kit/analytics'
const isDev = process.env.NODE_ENV === 'development'
export function Providers({ children }) {
return (
{children}
)
}
```
## User Identification
Associate events with a specific user:
```tsx title="app/analytics-wrapper.tsx"
'use client'
import { AnalyticsProvider, posthogPlugin } from '@tour-kit/analytics'
import { useUser } from '@/lib/auth'
export function AnalyticsWrapper({ children }) {
const user = useUser()
return (
{children}
)
}
```
## Global Properties
Add properties to every tracked event:
```tsx title="app/providers.tsx"
import { AnalyticsProvider, posthogPlugin } from '@tour-kit/analytics'
export function Providers({ children }) {
return (
{children}
)
}
```
## Conditional Analytics
Enable or disable based on user consent:
```tsx title="app/providers.tsx"
'use client'
import { AnalyticsProvider, posthogPlugin } from '@tour-kit/analytics'
import { useCookieConsent } from '@/lib/consent'
export function Providers({ children }) {
const consent = useCookieConsent()
return (
{children}
)
}
```
## Automatic Flush
The provider automatically flushes queued events when the user navigates away from the page:
```tsx
// Automatically handled - no setup required
{children}
```
This ensures no analytics data is lost when users close tabs or navigate away during tours.
## Environment-Based Configuration
Separate development and production configurations:
```tsx title="lib/analytics.ts"
import {
consolePlugin,
posthogPlugin,
type AnalyticsConfig
} from '@tour-kit/analytics'
const isDev = process.env.NODE_ENV === 'development'
export const analyticsConfig: AnalyticsConfig = {
plugins: isDev
? [consolePlugin({ collapsed: false })]
: [
posthogPlugin({
apiKey: process.env.NEXT_PUBLIC_POSTHOG_KEY!,
apiHost: 'https://app.posthog.com'
})
],
debug: isDev,
globalProperties: {
environment: process.env.NODE_ENV,
version: process.env.NEXT_PUBLIC_APP_VERSION
}
}
```
```tsx title="app/layout.tsx"
import { AnalyticsProvider } from '@tour-kit/analytics'
import { analyticsConfig } from '@/lib/analytics'
export default function RootLayout({ children }) {
return (
{children}
)
}
```
## Cleanup
The provider automatically cleans up resources on unmount:
- Calls `destroy()` on all plugins
- Clears event queues
- Removes event listeners
No manual cleanup is required.
## Related
- [Hooks](/docs/analytics/hooks) - Access analytics from components
- [Plugins](/docs/analytics/plugins) - Available analytics plugins
- [Types](/docs/analytics/types) - Full configuration reference
---
# Analytics Types
> TypeScript types for AnalyticsPlugin, AnalyticsEvent, TrackerConfig, and the analytics package public API surface
## Overview
The `@tour-kit/analytics` package is fully typed with TypeScript. This reference documents all exported types for events, plugins, and configuration.
## Event Types
### TourEventName
All possible event names that can be tracked:
```ts
type TourEventName =
// Tour lifecycle
| 'tour_started'
| 'tour_completed'
| 'tour_skipped'
| 'tour_abandoned'
// Step lifecycle
| 'step_viewed'
| 'step_completed'
| 'step_skipped'
| 'step_interaction'
// Hint events
| 'hint_shown'
| 'hint_dismissed'
| 'hint_clicked'
// Feature adoption
| 'feature_used'
| 'feature_adopted'
| 'feature_churned'
| 'nudge_shown'
| 'nudge_clicked'
| 'nudge_dismissed'
```
### TourEvent
Complete event payload sent to plugins:
```ts
interface TourEvent {
/** Event type */
eventName: TourEventName
/** Unix timestamp in milliseconds */
timestamp: number
/** Unique session identifier */
sessionId: string
/** Tour identifier */
tourId: string
/** Current step identifier */
stepId?: string
/** Current step index (0-based) */
stepIndex?: number
/** Total number of steps in tour */
totalSteps?: number
/** User identifier (if known) */
userId?: string
/** Additional user properties */
userProperties?: Record
/** Duration in milliseconds */
duration?: number
/** Number of interactions during step */
interactionCount?: number
/** Custom metadata */
metadata?: Record
}
```
### TourEventData
Event data without auto-generated fields (used when tracking events manually):
```ts
type TourEventData = Omit
```
Usage:
```tsx
import { useAnalytics, type TourEventData } from '@tour-kit/analytics'
function MyComponent() {
const analytics = useAnalytics()
const data: TourEventData = {
tourId: 'onboarding',
stepId: 'welcome',
stepIndex: 0,
totalSteps: 5,
metadata: {
source: 'dashboard'
}
}
analytics.track('step_viewed', data)
}
```
## Plugin Types
### AnalyticsPlugin
Plugin interface that all analytics plugins must implement:
```ts
interface AnalyticsPlugin {
/** Unique plugin identifier */
name: string
/** Initialize the plugin (called once on setup) */
init?: () => void | Promise
/** Track an event */
track: (event: TourEvent) => void | Promise
/** Identify a user */
identify?: (userId: string, properties?: Record) => void
/** Flush any queued events */
flush?: () => void | Promise
/** Clean up resources */
destroy?: () => void
}
```
Only `name` and `track` are required.
Example:
```ts
import type { AnalyticsPlugin, TourEvent } from '@tour-kit/analytics'
const myPlugin: AnalyticsPlugin = {
name: 'my-plugin',
async init() {
console.log('Plugin initialized')
},
track(event: TourEvent) {
console.log('Tracking:', event.eventName)
},
identify(userId: string, properties?: Record) {
console.log('User:', userId, properties)
},
async flush() {
console.log('Flushing events')
},
destroy() {
console.log('Cleaning up')
}
}
```
## Configuration Types
### AnalyticsConfig
Configuration object passed to `AnalyticsProvider`:
```ts
interface AnalyticsConfig {
/** Enable/disable analytics (default: true) */
enabled?: boolean
/** Array of analytics plugins */
plugins: AnalyticsPlugin[]
/** Enable debug logging to console */
debug?: boolean
/** Queue events when offline */
offlineQueue?: boolean
/** Batch events before sending */
batchSize?: number
/** Batch interval in milliseconds */
batchInterval?: number
/** User identification */
userId?: string
/** User properties */
userProperties?: Record
/** Global properties added to all events */
globalProperties?: Record
}
```
Example:
```tsx
import { AnalyticsProvider, type AnalyticsConfig } from '@tour-kit/analytics'
const config: AnalyticsConfig = {
enabled: true,
plugins: [
posthogPlugin({ apiKey: 'phc_xxx' })
],
debug: process.env.NODE_ENV === 'development',
userId: user?.id,
userProperties: {
email: user?.email,
plan: user?.plan
},
globalProperties: {
app_version: '1.0.0',
environment: 'production'
}
}
{children}
```
## Class Types
### TourAnalytics
Main analytics tracker class returned by `createAnalytics()` and hooks:
```ts
class TourAnalytics {
// User identification
identify(userId: string, properties?: Record): void
// Raw event tracking
track(eventName: TourEventName, data: TourEventData): void
// Tour lifecycle methods
tourStarted(tourId: string, totalSteps: number, metadata?: Record): void
tourCompleted(tourId: string, metadata?: Record): void
tourSkipped(tourId: string, stepIndex: number, stepId?: string, metadata?: Record): void
tourAbandoned(tourId: string, stepIndex: number, stepId?: string, metadata?: Record): void
// Step lifecycle methods
stepViewed(tourId: string, stepId: string, stepIndex: number, totalSteps: number, metadata?: Record): void
stepCompleted(tourId: string, stepId: string, stepIndex: number, metadata?: Record): void
stepSkipped(tourId: string, stepId: string, stepIndex: number, metadata?: Record): void
stepInteraction(tourId: string, stepId: string, interactionType: string, metadata?: Record): void
// Hint tracking methods
hintShown(hintId: string, metadata?: Record): void
hintDismissed(hintId: string, metadata?: Record): void
hintClicked(hintId: string, metadata?: Record): void
// Utility methods
flush(): Promise
destroy(): void
}
```
## Hook Return Types
### useAnalytics
Returns `TourAnalytics` or throws error:
```ts
function useAnalytics(): TourAnalytics
```
Usage:
```tsx
import { useAnalytics } from '@tour-kit/analytics'
function MyComponent() {
const analytics = useAnalytics()
// analytics is always defined (throws if not in provider)
}
```
### useAnalyticsOptional
Returns `TourAnalytics | null`:
```ts
function useAnalyticsOptional(): TourAnalytics | null
```
Usage:
```tsx
import { useAnalyticsOptional } from '@tour-kit/analytics'
function MyComponent() {
const analytics = useAnalyticsOptional()
// analytics may be null
analytics?.track('event', { tourId: 'test' })
}
```
## Plugin-Specific Types
### Console Plugin
```ts
interface ConsolePluginOptions {
/** Prefix for log messages */
prefix?: string
/** Use collapsed console groups */
collapsed?: boolean
/** Custom colors for different event types */
colors?: {
tour?: string
step?: string
hint?: string
}
}
function consolePlugin(options?: ConsolePluginOptions): AnalyticsPlugin
```
### PostHog Plugin
```ts
interface PostHogPluginOptions {
/** PostHog API key */
apiKey: string
/** PostHog API host (default: https://app.posthog.com) */
apiHost?: string
/** Enable autocapture (default: false) */
autocapture?: boolean
/** Event name prefix (default: tourkit_) */
eventPrefix?: string
}
function posthogPlugin(options: PostHogPluginOptions): AnalyticsPlugin
```
### Mixpanel Plugin
```ts
interface MixpanelPluginOptions {
/** Mixpanel token */
token: string
/** Enable debug mode */
debug?: boolean
/** Event name prefix (default: userTourKit: ) */
eventPrefix?: string
}
function mixpanelPlugin(options: MixpanelPluginOptions): AnalyticsPlugin
```
### Amplitude Plugin
```ts
interface AmplitudePluginOptions {
/** Amplitude API key */
apiKey: string
/** Event name prefix (default: tourkit_) */
eventPrefix?: string
/** Server URL for EU data residency */
serverUrl?: string
}
function amplitudePlugin(options: AmplitudePluginOptions): AnalyticsPlugin
```
### Google Analytics Plugin
```ts
interface GoogleAnalyticsPluginOptions {
/** GA4 Measurement ID (G-XXXXXXXXXX) */
measurementId: string
/** Event name prefix (default: tourkit_) */
eventPrefix?: string
}
function googleAnalyticsPlugin(options: GoogleAnalyticsPluginOptions): AnalyticsPlugin
```
## Type Imports
Import types from the package:
```ts
// Event types
import type {
TourEvent,
TourEventName,
TourEventData
} from '@tour-kit/analytics'
// Plugin types
import type {
AnalyticsPlugin,
AnalyticsConfig
} from '@tour-kit/analytics'
// Class type
import type { TourAnalytics } from '@tour-kit/analytics'
```
## Generic Types
### Metadata Type
Metadata can be any JSON-serializable object:
```ts
// Valid metadata
const metadata: Record = {
string: 'value',
number: 123,
boolean: true,
array: [1, 2, 3],
nested: {
key: 'value'
}
}
// Invalid metadata (not JSON-serializable)
const bad = {
fn: () => {}, // ❌ Function
date: new Date(), // ❌ Date object
regex: /test/, // ❌ RegExp
symbol: Symbol('test') // ❌ Symbol
}
```
### Properties Type
User and global properties use the same type:
```ts
type Properties = Record
```
Example:
```ts
const userProperties: Record = {
email: 'user@example.com',
plan: 'pro',
features: ['feature-a', 'feature-b']
}
const globalProperties: Record = {
app_version: '1.0.0',
platform: 'web',
environment: 'production'
}
```
## Type Guards
Check event types at runtime:
```ts
import type { TourEvent, TourEventName } from '@tour-kit/analytics'
function isTourEvent(eventName: TourEventName): boolean {
return eventName.startsWith('tour_')
}
function isStepEvent(eventName: TourEventName): boolean {
return eventName.startsWith('step_')
}
function isHintEvent(eventName: TourEventName): boolean {
return eventName.startsWith('hint_')
}
// Usage in plugin
const plugin: AnalyticsPlugin = {
name: 'filtered',
track(event: TourEvent) {
if (isTourEvent(event.eventName)) {
console.log('Tour event:', event)
}
}
}
```
## Related
- [AnalyticsProvider](/docs/analytics/providers) - Provider configuration
- [Hooks](/docs/analytics/hooks) - Access analytics in components
- [Custom Plugins](/docs/analytics/plugins/custom) - Build custom integrations
---
# @tour-kit/announcements
> Product announcements with modal, toast, banner, slideout, and spotlight variants — priority queuing and audience targeting
{/* llm-context-callout */}
[Why userTourKit for product announcements →](/product-announcements)
A flexible library for displaying product announcements and updates to your users. Choose from 5 UI variants, manage announcement queues with priority ordering, and target specific user segments.
## Why Announcements?
Product announcements help you communicate new features, important updates, and critical messages to users at the right time. Unlike tours (which are sequential tutorials), announcements are:
- **Flexible** - Choose from 5 UI variants for different use cases
- **Prioritized** - Critical messages appear before low-priority ones
- **Targeted** - Show announcements only to relevant users
- **Frequency-aware** - Control how often users see each announcement
- **Persistent** - Track views and dismissals across sessions
**Use announcements for:**
- New feature releases
- Product updates and improvements
- Important notices and alerts
- Time-sensitive promotions
- Onboarding messages
**Use tours instead when:**
- You need a sequential, step-by-step walkthrough
- Users need to complete specific actions in order
- You want guided product education
---
## Installation
```bash
pnpm add @tour-kit/announcements
```
```bash
npm install @tour-kit/announcements
```
```bash
yarn add @tour-kit/announcements
```
---
## Quick Start
```tsx
import { AnnouncementsProvider, AnnouncementModal } from '@tour-kit/announcements';
function App() {
return (
console.log('Action clicked'),
},
},
]}
>
);
}
```
---
## UI Variants
Choose the right variant for your use case:
### Modal
Centered dialog with overlay - best for important announcements requiring immediate attention.
```tsx
```
**Best for:** Major features, breaking changes, critical updates
### Slideout
Side panel that slides in from left or right - less intrusive than modals.
```tsx
```
**Best for:** Product updates, changelog entries, detailed announcements
### Banner
Full-width bar at top or bottom - persistent and non-blocking.
```tsx
```
**Best for:** System notices, maintenance alerts, persistent messages
### Toast
Temporary notification that auto-dismisses - minimal and unobtrusive.
```tsx
```
**Best for:** Quick tips, non-critical updates, success messages
### Spotlight
Highlights a specific element with an overlay - draws attention to UI changes.
```tsx
```
**Best for:** New UI elements, feature highlights, contextual help
---
## Priority-Based Queue
Announcements are automatically queued based on priority:
```tsx
const announcements = [
{
id: 'critical-alert',
variant: 'modal',
priority: 'critical', // Shows first
title: 'Security Update Required',
},
{
id: 'new-feature',
variant: 'slideout',
priority: 'high', // Shows second
title: 'New Export Feature',
},
{
id: 'tip',
variant: 'toast',
priority: 'low', // Shows last
title: 'Pro Tip',
},
];
```
**Priority order:** `critical` > `high` > `normal` > `low`
Configure queue behavior:
```tsx
```
---
## Frequency Rules
Control how often users see announcements:
```tsx
{
id: 'promo',
variant: 'banner',
frequency: 'once', // Only ever show once
}
{
id: 'tip',
variant: 'toast',
frequency: 'session', // Once per session
}
{
id: 'survey',
variant: 'modal',
frequency: { type: 'times', count: 3 }, // Show 3 times total
}
{
id: 'reminder',
variant: 'banner',
frequency: { type: 'interval', days: 7 }, // Every 7 days
}
```
---
## Audience Targeting
Show announcements only to specific users:
```tsx
```
---
## Programmatic Control
Use hooks to control announcements programmatically:
```tsx
import { useAnnouncement, useAnnouncements } from '@tour-kit/announcements';
function AnnouncementControls() {
const announcement = useAnnouncement('new-feature');
const { announcements, showNext } = useAnnouncements();
return (
Show Announcement
Dismiss Forever
Viewed {announcement.viewCount} times
Active announcements: {announcements.visible.length}
);
}
```
---
## Persistence
Announcement state (views, dismissals) is automatically persisted:
```tsx
```
Reset all announcement state:
```tsx
const { resetAll } = useAnnouncements();
Reset All Announcements
```
---
## Architecture
The announcements package uses a provider/context pattern:
```
AnnouncementsProvider
├── Context (state management)
│ ├── announcements Map
│ ├── queue (priority-sorted)
│ └── activeId (currently shown)
├── UI Components
│ ├── AnnouncementModal
│ ├── AnnouncementSlideout
│ ├── AnnouncementBanner
│ ├── AnnouncementToast
│ └── AnnouncementSpotlight
└── Hooks
├── useAnnouncement(id)
├── useAnnouncements()
└── useAnnouncementQueue()
```
### State Management
- **Provider-based** - Announcement state managed via React context
- **Priority queuing** - Automatic ordering by priority level
- **Persistence** - State saved to localStorage/sessionStorage
- **Lifecycle callbacks** - onShow, onDismiss, onComplete events
---
## Bundle Size
| Package | Gzipped |
|---------|---------|
| @tour-kit/announcements | < 10KB |
---
## Package Contents
---
## Complete Example
```tsx
import {
AnnouncementsProvider,
AnnouncementModal,
AnnouncementBanner,
AnnouncementToast,
useAnnouncement,
} from '@tour-kit/announcements';
const announcements = [
{
id: 'security-update',
variant: 'modal',
priority: 'critical',
title: 'Security Update Required',
description: 'Please update your password to continue.',
frequency: 'always',
primaryAction: {
label: 'Update Now',
onClick: () => router.push('/settings/security'),
},
},
{
id: 'new-export',
variant: 'slideout',
priority: 'high',
title: 'New Export Feature',
description: 'Export your data to CSV, PDF, or Excel.',
frequency: 'once',
media: {
type: 'image',
src: '/export-preview.png',
alt: 'Export feature preview',
},
audience: [
{ field: 'plan', operator: 'in', value: ['pro', 'enterprise'] },
],
},
{
id: 'maintenance',
variant: 'banner',
priority: 'normal',
title: 'Scheduled maintenance tonight at 2 AM EST',
frequency: 'session',
bannerOptions: {
intent: 'warning',
sticky: true,
},
},
];
function App() {
return (
analytics.track('announcement_shown', { id })}
onDismiss={(id, reason) => analytics.track('announcement_dismissed', { id, reason })}
>
{/* Render UI components for each variant */}
);
}
function MainApp() {
const security = useAnnouncement('security-update');
return (
My App
{security.canShow && (
Important Security Update
)}
);
}
```
---
## Accessibility
All announcement components are built with accessibility in mind:
- **Modal**: Traps focus, closes on Escape, ARIA dialog role
- **Slideout**: ARIA complementary role, keyboard navigation
- **Banner**: ARIA banner/alert role based on intent
- **Toast**: ARIA status/alert role, auto-dismiss announcements
- **Spotlight**: Focus management, keyboard dismissal
---
## Related
- [@tour-kit/hints](/docs/hints) - For persistent, non-blocking feature hints
- [@tour-kit/react](/docs/react) - For sequential product tours
- [@tour-kit/scheduling](/docs/scheduling) - Schedule announcements for future dates
---
# Changelog
> Release history for the @tour-kit/announcements package — version-by-version log of breaking changes, new features, and bug fixes. Autogenerated from Changesets.
Release history for the `@tour-kit/announcements` package. Every entry below is
auto-generated by [Changesets](https://github.com/changesets/changesets) from a
PR-attached changeset file — the same source of truth that drives semver bumps
and npm publishes. The authoritative copy lives at
[`packages/announcements/CHANGELOG.md`](https://github.com/domidex01/tour-kit/blob/main/packages/announcements/CHANGELOG.md)
in the repository.
## How to read this changelog
- **Major** (e.g. `4.0.0` → `5.0.0`) — breaking changes. Read the migration note
in the release entry before upgrading.
- **Minor** (e.g. `4.0.0` → `4.1.0`) — new features, backward compatible.
- **Patch** (e.g. `4.1.0` → `4.1.1`) — bug fixes and internal cleanup, no API
changes.
- **Updated dependencies** lines mean the version bump was triggered by a
dependency (usually `@tour-kit/core`) — the announcement package itself
hasn't shipped behavior changes in that release.
## Where to find the entries
- **GitHub Releases** —
[github.com/domidex01/tour-kit/releases](https://github.com/domidex01/tour-kit/releases)
has every release with the rendered changeset, tag, and diff against the
previous version.
- **Full repo file** —
[`packages/announcements/CHANGELOG.md`](https://github.com/domidex01/tour-kit/blob/main/packages/announcements/CHANGELOG.md)
— full version log, including patch and dependency-only releases.
- **npm versions page** —
[npmjs.com/package/@tour-kit/announcements?activeTab=versions](https://www.npmjs.com/package/@tour-kit/announcements?activeTab=versions)
— install any past version, with publish timestamps.
## Latest highlights
The most recent intentional behavior changes worth knowing about:
### 4.1.0 — Priority queue comparator fix
Custom `priorityWeights` and `priorityOrder: 'fifo' | 'lifo'` from
`QueueConfig` now actually drive auto-show ordering. Before 4.1.0 an inline
comparator hardcoded `{ critical: 0, high: 1, normal: 2, low: 3 }` and silently
ignored both options. This is a **behavior fix** — if you had configured these
options before 4.1.0 and the order looked wrong, this release is why.
New helpers exposed: `createAnnouncementComparator(order, weights, sequenceById)`
and `AnnouncementScheduler.queueConfig` getter.
### 4.0.0 — AnnouncementSpotlight visual breaking + Sonner adapter
- `` cutout switched from soft radial gradient to a 2px
inset stroke + directional arrow. Passes WCAG 2.1 AA contrast on white,
off-white, and light-gray backgrounds. Opt back into the legacy look with
`variant="legacy-spotlight"` (kept until v5).
- New `@tour-kit/announcements/adapters/sonner` peer-optional adapter for
`variant="toast"`. Pass `toastAdapter={sonnerAdapter}` to the provider. No
Sonner bytes ship in the main bundle.
### 3.0.0 — Dashboard QA pass
- New `TourEventName` analytics events:
`announcement_shown`, `announcement_dismissed`, `announcement_completed`.
- `` now forwards `aria-describedby` and renders with
`asDialogContent` so Radix's title/description requirements are satisfied
— eliminates the `DialogTitle is required` console warning.
- Install graph: `@tour-kit/analytics` moved from optional peer to direct
dependency. No more manual install for analytics events.
## Migration guides
When a release introduces a breaking change, the changeset entry itself contains
the migration steps. For larger structural changes, see the dedicated guides:
- [Migration overview](/docs/migration) — global cross-package migration
notes between major versions.
## Need an older release's entry?
The full file is at
[`packages/announcements/CHANGELOG.md`](https://github.com/domidex01/tour-kit/blob/main/packages/announcements/CHANGELOG.md).
Anything older than ~6 months is mirrored to GitHub Releases only and not
re-rendered here.
---
# ChangelogPage Component
> Render a public changelog page in React and serialize the same entries as RSS 2.0 + JSON Feed 1.1.
`@tour-kit/announcements` ships two changelog primitives that share the same `ChangelogEntry` shape:
- **``** — a drop-in React page (category filter + emoji reactions + media support) you mount on any route.
- **`serializeFeed`** — a pure RSS 2.0 + JSON Feed 1.1 serializer for the same entries.
Use them together: render the entries to a public web page with ``, syndicate the same list as an RSS feed with `serializeFeed`. The renderer lives behind the `@tour-kit/announcements/changelog` subpath, so toast/modal/banner-only consumers do not pay the bundle cost.
---
## `` — render
Mount it anywhere. No router context required.
```tsx
import { ChangelogPage } from '@tour-kit/announcements/changelog';
import type { ChangelogEntry } from '@tour-kit/announcements';
const entries: ChangelogEntry[] = [
{
id: 'evt-2026-05-04',
variant: 'modal',
title: 'Bulk export now available',
description: 'Export your data to CSV or PDF directly from the dashboard.',
permalink: 'https://acme.com/changelog/bulk-export',
publishedAt: new Date('2026-05-04T00:00:00Z'),
category: 'Features',
},
// ...
];
export default function ChangelogRoute() {
return ;
}
```
The component derives its category sidebar from `entry.category`, prepends an "All" reset button, and supports `ArrowDown`/`ArrowUp` keyboard navigation with a roving tabindex.
### Reactions
Pass `onReact` to capture clicks on the per-entry emoji row (👍 😐 👎). The component holds **no reaction state** — wire the callback to your own backend or analytics:
```tsx
{
fetch('/api/changelog-reaction', {
method: 'POST',
body: JSON.stringify({ entryId, emoji }),
});
}}
/>
```
### Media
If an entry carries `media` (any URL `` understands — YouTube, Vimeo, Loom, Wistia, native video, GIF, Lottie), it renders above the entry body. See the [media docs](/docs/media) for supported sources.
### Next.js URL-sync recipe (controlled mode)
Omitting `category` / `onCategoryChange` runs the page in uncontrolled mode (internal `useState`). Pass both to lift state into your URL:
```tsx
'use client';
import { ChangelogPage } from '@tour-kit/announcements/changelog';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import type { ChangelogEntry } from '@tour-kit/announcements';
export function MyChangelog({ entries }: { entries: ChangelogEntry[] }) {
const sp = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const category = sp.get('category');
const onCategoryChange = (next: string | null) => {
const params = new URLSearchParams(sp);
if (next) params.set('category', next);
else params.delete('category');
router.replace(`${pathname}?${params.toString()}`);
};
return (
);
}
```
The same pattern works with TanStack Router (`useSearch` + `useNavigate`) or Remix loaders — anywhere you can read and write a URL parameter.
### Internationalization
The page reads from `` (Phase 1 i18n primitives). Default English fallbacks ship in the component, so it works without any provider mounted. Override per-key:
| Key | English fallback |
|---|---|
| `changelog.empty` | `"No changelog entries yet"` |
| `changelog.filter.all` | `"All"` |
| `changelog.filter.label` | `"Filter by category"` |
| `changelog.reactions.label` | `"Reactions"` |
| `changelog.reaction.thumbs_up` | `"Thumbs up"` |
| `changelog.reaction.neutral` | `"Neutral"` |
| `changelog.reaction.thumbs_down` | `"Thumbs down"` |
```tsx
import { LocaleProvider } from '@tour-kit/core';
```
RTL locales (`ar`, `he`, `fa`, `ur`) automatically apply `dir="rtl"` to the page root.
---
## `serializeFeed` — syndicate
`serializeFeed` produces both an RSS 2.0 XML string and a JSON Feed 1.1 string from a list of `ChangelogEntry`s. The output is suitable for serving from `app/changelog/rss.xml/route.ts` and `app/changelog/feed.json/route.ts` in Next.js, or any other framework that returns raw `Response` bodies.
The serializer is a pure function — no DOM, no network I/O, no runtime XML library dependency. Every consumer-supplied string is XML-entity-escaped before reaching the output. Invalid `publishedAt` inputs throw `TypeError` rather than emitting `Invalid Date` strings into a live feed.
---
## When to use
Pair `serializeFeed` with a public changelog page when:
- You publish new in-app announcements regularly and want subscribers to follow updates outside the app.
- You want both formats live (RSS for traditional readers; JSON Feed for modern, JSON-first clients) without two implementations.
- You already drive announcements through `` — the same `AnnouncementConfig` shape extends to `ChangelogEntry`.
---
## Define your entries
```tsx
import type { ChangelogEntry } from '@tour-kit/announcements';
const entries: ChangelogEntry[] = [
{
id: 'evt-2026-05-04',
variant: 'modal',
title: 'Bulk export now available',
description: 'Export your data to CSV or PDF directly from the dashboard.',
permalink: 'https://acme.com/changelog/bulk-export',
publishedAt: new Date('2026-05-04T00:00:00Z'),
category: 'feature',
},
{
id: 'evt-2026-04-22',
variant: 'modal',
title: 'Fixed: dashboard filter persistence',
description: 'Filters now persist across page reloads.',
permalink: 'https://acme.com/changelog/filter-fix',
publishedAt: new Date('2026-04-22T00:00:00Z'),
category: 'fix',
},
];
```
`ChangelogEntry` extends `AnnouncementConfig`, so any config that already drives an in-app announcement can be reused as a changelog entry by adding `publishedAt` and `permalink`.
### Entry shape reference
| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `id` | `string` | yes | Stable identifier — becomes the RSS `` and JSON Feed `id`. **Never rotate** an `id` after first publish — readers will treat it as a new item. |
| `title` | `string \| LocalizedText` | yes | Entry headline. Accepts `{{var \| fallback}}` interpolation; resolves through the surrounding ``. |
| `description` | `string \| LocalizedText` | yes | One- or two-paragraph summary. Same i18n grammar as `title`. Inherits XML-entity escaping in the RSS body. |
| `permalink` | `string` | yes | Absolute URL of the canonical changelog page for this entry. Used as the RSS ` ` and JSON Feed `url`. |
| `publishedAt` | `Date \| string` | yes | Publish timestamp. Accepts a `Date` or any `Date`-parseable string (ISO 8601 recommended). Invalid input causes `serializeFeed` to `throw TypeError`. |
| `category` | `string` | no | Free-form tag — surfaces as `` in RSS, `tags: [category]` in JSON Feed, and as a sidebar filter row in ``. |
| `media` | `MediaSlotProps` | no | Any source `` understands (YouTube, Vimeo, Loom, Wistia, native video, GIF, Lottie). Renders above the entry body. |
| `variant` | `'modal' \| 'toast' \| 'banner' \| 'slideout'` | no | Inherited from `AnnouncementConfig` — only consumed when the same entry is reused as an in-app announcement; ignored on the changelog page. |
---
## Wire the routes (Next.js App Router)
### RSS — `app/changelog/rss.xml/route.ts`
```ts
import { serializeFeed } from '@tour-kit/announcements';
import { getChangelogEntries } from '@/lib/changelog';
export async function GET() {
const entries = await getChangelogEntries();
const { rss } = serializeFeed(entries, {
title: 'Acme Changelog',
description: 'Product updates from Acme',
siteUrl: 'https://acme.com',
feedUrl: 'https://acme.com/changelog',
});
return new Response(rss, {
headers: {
'Content-Type': 'application/xml; charset=utf-8',
'Cache-Control': 'public, max-age=600',
},
});
}
```
### JSON Feed — `app/changelog/feed.json/route.ts`
```ts
import { serializeFeed } from '@tour-kit/announcements';
import { getChangelogEntries } from '@/lib/changelog';
export async function GET() {
const entries = await getChangelogEntries();
const { jsonFeed } = serializeFeed(entries, {
title: 'Acme Changelog',
description: 'Product updates from Acme',
siteUrl: 'https://acme.com',
feedUrl: 'https://acme.com/changelog',
});
return new Response(jsonFeed, {
headers: {
'Content-Type': 'application/feed+json; charset=utf-8',
'Cache-Control': 'public, max-age=600',
},
});
}
```
---
## Options
```ts
interface SerializeFeedOptions {
title: string; // Channel/feed title
description: string; // Channel/feed description
siteUrl: string; // Home page URL (RSS , JSON Feed home_page_url)
feedUrl: string; // Base feed URL — .xml and .json appended automatically
language?: string; // Defaults to 'en' if omitted
copyright?: string; // Optional copyright statement (RSS only)
}
```
Pass `feedUrl` *without* a file extension — the serializer appends `.xml` for the RSS `` and `.json` for the JSON Feed `feed_url`.
---
## Caching
Most consumers cache aggressively. A 10-minute browser cache and a CDN-level revalidation works well for daily-or-slower changelog updates:
```ts
'Cache-Control': 'public, max-age=600, s-maxage=3600, stale-while-revalidate=86400'
```
If you publish more often, drop `max-age` to `60` and rely on `s-maxage` + `stale-while-revalidate` to keep the CDN warm.
---
## Validation
Both outputs are spec-compliant:
- **RSS 2.0** validates against `https://www.rssboard.org/rss-specification`. The serializer round-trips losslessly through `fast-xml-parser` for every emitted field (title, link, guid, pubDate, description, category, atom:link).
- **JSON Feed 1.1** validates against the official schema. The `version` field is exactly `"https://jsonfeed.org/version/1.1"`.
If you make changes to your entry shape and want to verify, paste the output into the [W3C Feed Validator](https://validator.w3.org/feed/) (RSS) or run it through the JSON Feed [validator](https://validator.jsonfeed.org/) (JSON Feed).
---
## Security
Every consumer-supplied string flows through XML entity escaping before reaching the RSS body. The serializer ships with a 1000-case property-based fuzz test that asserts no raw `<`, `>`, `&`, or `]]>` ever appears in output.
For JSON Feed, `JSON.stringify` handles escaping natively — manually escaping JSON content would double-escape and corrupt data.
---
# Components
> Pre-styled announcement components: Modal, Toast, Banner, Slideout, and Spotlight with built-in close and action buttons
Pre-built, accessible announcement components styled with Tailwind CSS. Choose from 5 UI variants for different use cases.
## Available Components
---
## Shared Components
All variants use these shared components:
- **AnnouncementOverlay** - Backdrop overlay for modals and spotlights
- **AnnouncementClose** - Close button with accessibility
- **AnnouncementContent** - Content container with title and description
- **AnnouncementActions** - Action buttons layout
---
## Basic Usage
```tsx
import {
AnnouncementsProvider,
AnnouncementModal,
AnnouncementBanner,
} from '@tour-kit/announcements';
const announcements = [
{
id: 'welcome',
variant: 'modal',
title: 'Welcome!',
description: 'Thanks for joining.',
},
{
id: 'notice',
variant: 'banner',
title: 'Maintenance scheduled for tonight',
},
];
function App() {
return (
{/* Render component for each announcement */}
);
}
```
---
## Common Props
All components share these props:
- `id` (required) - Announcement identifier matching config
- `className` - Additional CSS classes
- `asChild` - Use Slot pattern for custom elements
---
## Choosing a Variant
| Variant | Use When | Blocking | Auto-Dismiss | Position |
|---------|----------|----------|--------------|----------|
| **Modal** | Critical updates, requires attention | Yes | No | Center |
| **Slideout** | Detailed info, less urgent | Partial | No | Side |
| **Banner** | Persistent notices, non-blocking | No | No | Top/Bottom |
| **Toast** | Quick tips, success messages | No | Yes | Corner |
| **Spotlight** | Highlight new UI elements | Partial | No | Target |
---
## Styling
All components use Tailwind CSS with CSS variables for theming:
```css
:root {
--announcement-overlay-bg: rgba(0, 0, 0, 0.5);
--announcement-modal-bg: white;
--announcement-border-radius: 0.5rem;
}
```
See individual component pages for variant-specific styling.
---
## Accessibility
All components are built with accessibility in mind:
- **ARIA roles** - Proper semantic roles (dialog, banner, status, etc.)
- **Focus management** - Trap focus in modals, restore on close
- **Keyboard support** - Escape to close, Tab navigation
- **Screen readers** - Descriptive labels and live regions
- **Reduced motion** - Respects `prefers-reduced-motion`
---
## Next Steps
Explore each component variant for detailed props and examples:
- [Modal](/docs/announcements/components/modal) - Centered dialog
- [Slideout](/docs/announcements/components/slideout) - Side panel
- [Banner](/docs/announcements/components/banner) - Top/bottom bar
- [Toast](/docs/announcements/components/toast) - Corner notification
- [Spotlight](/docs/announcements/components/spotlight) - Element highlight
- [CVA variants reference](/docs/announcements/components/variants) - Variant exports for customization
---
# AnnouncementBanner
> AnnouncementBanner component: full-width persistent bar for system notices with dismiss button and action link support
A full-width bar at the top or bottom of the page. Best for persistent, non-blocking notices.
## When to Use
- System maintenance notices
- Cookie consent
- Important non-urgent updates
- Promotions and offers
- Status messages
---
## Basic Usage
```tsx
import { AnnouncementsProvider, AnnouncementBanner } from '@tour-kit/announcements';
const announcements = [
{
id: 'maintenance',
variant: 'banner',
title: 'Scheduled maintenance tonight at 2 AM EST',
bannerOptions: {
position: 'top',
sticky: true,
intent: 'warning',
},
},
];
```
---
## Banner Options
---
## Intent Colors
```tsx
// Default - neutral gray
bannerOptions: { intent: 'default' }
// Info - blue
bannerOptions: { intent: 'info' }
// Success - green
bannerOptions: { intent: 'success' }
// Warning - yellow/orange
bannerOptions: { intent: 'warning' }
// Error - red
bannerOptions: { intent: 'error' }
```
---
## Complete Example
```tsx
{
id: 'promo',
variant: 'banner',
title: 'Limited Time Offer: 50% off Pro plan',
primaryAction: {
label: 'Upgrade Now',
onClick: () => router.push('/pricing'),
},
secondaryAction: {
label: 'Learn More',
onClick: () => router.push('/pricing#features'),
},
bannerOptions: {
position: 'top',
sticky: true,
dismissable: true,
intent: 'success',
},
frequency: { type: 'interval', days: 7 },
}
```
---
## Sticky Positioning
```tsx
// Stick to top when scrolling
bannerOptions: {
position: 'top',
sticky: true,
}
// Stick to bottom
bannerOptions: {
position: 'bottom',
sticky: true,
}
```
---
## Non-Dismissable
For critical persistent messages:
```tsx
bannerOptions: {
dismissable: false, // No close button
}
```
---
## Related
- [Headless Banner](/docs/announcements/headless/banner)
- [useAnnouncement](/docs/announcements/hooks/use-announcement)
---
# AnnouncementModal
> AnnouncementModal component: centered dialog overlay for important product updates with title, body, and action buttons
A centered modal dialog with overlay backdrop. Best for critical announcements that require user attention.
## When to Use
- Critical security updates
- Breaking changes
- Important feature releases
- User action required
- Major product announcements
---
## Basic Usage
```tsx
import { AnnouncementsProvider, AnnouncementModal } from '@tour-kit/announcements';
const announcements = [
{
id: 'security-update',
variant: 'modal',
title: 'Security Update Required',
description: 'Please update your password to continue using the app.',
primaryAction: {
label: 'Update Now',
onClick: () => router.push('/settings/security'),
},
},
];
function App() {
return (
);
}
```
---
## Props
---
## Modal Options
Configure modal behavior in the announcement config:
```tsx
{
id: 'announcement',
variant: 'modal',
modalOptions: {
size: 'md', // 'sm' | 'md' | 'lg' | 'xl'
closeOnOverlayClick: true,
closeOnEscape: true,
showCloseButton: true,
},
}
```
### ModalOptions
---
## Complete Example
```tsx
const announcements = [
{
id: 'feature-launch',
variant: 'modal',
priority: 'high',
title: 'New Export Feature',
description: 'Export your data to CSV, PDF, or Excel format.',
media: {
type: 'image',
src: '/export-preview.png',
alt: 'Export feature preview',
},
primaryAction: {
label: 'Try It Now',
onClick: () => router.push('/export'),
},
secondaryAction: {
label: 'Learn More',
onClick: () => window.open('/docs/export'),
},
modalOptions: {
size: 'lg',
closeOnOverlayClick: false, // Prevent accidental closing
showCloseButton: true,
},
frequency: 'once',
onShow: () => analytics.track('modal_shown'),
onDismiss: (reason) => analytics.track('modal_dismissed', { reason }),
},
];
```
---
## With Media
Display images or videos in the modal:
```tsx
{
id: 'video-announcement',
variant: 'modal',
title: 'Product Demo',
description: 'Watch how the new feature works.',
media: {
type: 'video',
src: '/demo.mp4',
poster: '/demo-poster.jpg',
},
modalOptions: {
size: 'xl', // Larger for video
},
}
```
---
## Preventing Dismissal
For critical modals that require user action:
```tsx
{
id: 'terms-update',
variant: 'modal',
title: 'Terms of Service Update',
description: 'Please accept the updated terms to continue.',
primaryAction: {
label: 'Accept',
onClick: () => acceptTerms(),
},
modalOptions: {
closeOnOverlayClick: false,
closeOnEscape: false,
showCloseButton: false, // No way to dismiss without action
},
}
```
---
## Size Variants
```tsx
// Small - 400px wide
{
id: 'small',
variant: 'modal',
title: 'Quick Tip',
modalOptions: { size: 'sm' },
}
// Medium (default) - 500px wide
{
id: 'medium',
variant: 'modal',
title: 'Feature Update',
modalOptions: { size: 'md' },
}
// Large - 600px wide
{
id: 'large',
variant: 'modal',
title: 'Detailed Announcement',
modalOptions: { size: 'lg' },
}
// Extra Large - 800px wide
{
id: 'xlarge',
variant: 'modal',
title: 'Full Product Demo',
modalOptions: { size: 'xl' },
}
```
---
## Styling
### Custom Classes
```tsx
```
### CSS Variables
```css
:root {
--announcement-modal-bg: white;
--announcement-modal-text: #1a1a1a;
--announcement-overlay-bg: rgba(0, 0, 0, 0.5);
--announcement-border-radius: 0.5rem;
--announcement-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
.dark {
--announcement-modal-bg: #1a1a1a;
--announcement-modal-text: white;
--announcement-overlay-bg: rgba(0, 0, 0, 0.7);
}
```
---
## Accessibility
The modal component includes:
- **ARIA dialog role** - Proper semantic role
- **Focus trap** - Focus stays within modal
- **Focus restoration** - Returns focus when closed
- **Keyboard support** - Escape to close (if enabled)
- **Screen reader** - Announced as dialog
- **Close button** - Labeled "Close announcement"
```tsx
// Rendered HTML structure
Announcement Title
Description...
×
```
---
## Programmatic Control
```tsx
import { useAnnouncement } from '@tour-kit/announcements';
function ModalController() {
const modal = useAnnouncement('my-modal');
return (
Open Modal
Close Modal
modal.dismiss('programmatic')}>
Dismiss Forever
);
}
```
---
## TypeScript
```tsx
import type { AnnouncementConfig, ModalOptions } from '@tour-kit/announcements';
const modalConfig: AnnouncementConfig = {
id: 'typed-modal',
variant: 'modal',
title: 'Typed Announcement',
modalOptions: {
size: 'lg',
closeOnOverlayClick: false,
} satisfies ModalOptions,
};
```
---
## Related
- [Headless Modal](/docs/announcements/headless/modal) - Build custom modal UI
- [AnnouncementOverlay](/docs/announcements/components) - Shared overlay component
- [useAnnouncement](/docs/announcements/hooks/use-announcement) - Programmatic control
---
# AnnouncementSlideout
> AnnouncementSlideout component: side panel drawer for detailed product updates with rich content and multiple sections
A slide-in panel from the left or right side. Best for detailed announcements that don't require immediate attention.
## When to Use
- Product changelogs
- Detailed feature explanations
- Multi-step announcements
- Less critical updates
---
## Basic Usage
```tsx
import { AnnouncementsProvider, AnnouncementSlideout } from '@tour-kit/announcements';
const announcements = [
{
id: 'changelog',
variant: 'slideout',
title: "What's New in v2.0",
description: 'Check out the latest features and improvements.',
slideoutOptions: {
position: 'right',
size: 'md',
},
},
];
```
---
## Slideout Options
---
## Complete Example
```tsx
{
id: 'product-update',
variant: 'slideout',
title: 'Product Update - March 2024',
media: {
type: 'image',
src: '/update-banner.png',
},
description: `
We've shipped some amazing updates this month:
• New dashboard design
• Advanced filtering
• Mobile app improvements
`,
primaryAction: {
label: 'See All Updates',
onClick: () => router.push('/changelog'),
},
slideoutOptions: {
position: 'right',
size: 'lg',
},
}
```
---
## Position
```tsx
// Slide from right (default)
slideoutOptions: { position: 'right' }
// Slide from left
slideoutOptions: { position: 'left' }
```
---
## Size Variants
```tsx
// Small - 320px
slideoutOptions: { size: 'sm' }
// Medium - 400px
slideoutOptions: { size: 'md' }
// Large - 500px
slideoutOptions: { size: 'lg' }
// Full screen
slideoutOptions: { size: 'full' }
```
---
## Related
- [Headless Slideout](/docs/announcements/headless/slideout)
- [useAnnouncement](/docs/announcements/hooks/use-announcement)
---
# AnnouncementSpotlight
> AnnouncementSpotlight component: element highlight overlay for contextual feature announcements tied to specific UI areas
Highlights a specific element with an overlay and floating announcement. Best for drawing attention to new UI elements.
## When to Use
- New buttons or features
- UI changes and relocations
- Interactive element introductions
- Contextual feature announcements
---
## Basic Usage
```tsx
import { AnnouncementsProvider, AnnouncementSpotlight } from '@tour-kit/announcements';
const announcements = [
{
id: 'new-export',
variant: 'spotlight',
title: 'New Export Button',
description: 'Export your data with one click!',
spotlightOptions: {
targetSelector: '#export-button',
placement: 'bottom',
},
},
];
function App() {
return (
Export
);
}
```
---
## Spotlight Options
---
## Target Selector
```tsx
// By ID
spotlightOptions: {
targetSelector: '#new-feature-button',
}
// By class
spotlightOptions: {
targetSelector: '.export-btn',
}
// By attribute
spotlightOptions: {
targetSelector: '[data-feature="export"]',
}
// Complex selector
spotlightOptions: {
targetSelector: 'nav button[aria-label="Settings"]',
}
```
---
## Placement
```tsx
// Basic placements
spotlightOptions: { placement: 'top' }
spotlightOptions: { placement: 'bottom' }
spotlightOptions: { placement: 'left' }
spotlightOptions: { placement: 'right' }
// With alignment
spotlightOptions: { placement: 'top-start' }
spotlightOptions: { placement: 'top-end' }
spotlightOptions: { placement: 'bottom-start' }
spotlightOptions: { placement: 'bottom-end' }
```
---
## Complete Example
```tsx
{
id: 'new-dashboard',
variant: 'spotlight',
title: 'Redesigned Dashboard',
description: 'Check out the new dashboard with improved analytics and insights.',
media: {
type: 'image',
src: '/dashboard-preview.png',
},
primaryAction: {
label: 'Explore Now',
onClick: () => router.push('/dashboard'),
},
spotlightOptions: {
targetSelector: '#dashboard-nav-link',
placement: 'bottom-start',
offset: 12,
showOverlay: true,
overlayOpacity: 0.7,
padding: 8,
},
frequency: 'once',
audience: [
{ field: 'hasSeenDashboard', operator: 'equals', value: false },
],
}
```
---
## Overlay Customization
```tsx
// Light overlay
spotlightOptions: {
overlayOpacity: 0.3,
}
// Dark overlay
spotlightOptions: {
overlayOpacity: 0.8,
}
// No overlay (just card)
spotlightOptions: {
showOverlay: false,
}
```
---
## Padding
Control spacing around the highlighted element:
```tsx
// Tight (4px)
spotlightOptions: { padding: 4 }
// Default (8px)
spotlightOptions: { padding: 8 }
// Spacious (16px)
spotlightOptions: { padding: 16 }
```
---
## Handling Missing Targets
```tsx
{
id: 'spotlight',
variant: 'spotlight',
title: 'New Feature',
spotlightOptions: {
targetSelector: '#maybe-missing',
},
onShow: () => {
const target = document.querySelector('#maybe-missing');
if (!target) {
console.warn('Target element not found');
announcement.hide();
}
},
}
```
---
## Programmatic Control
```tsx
import { useAnnouncement } from '@tour-kit/announcements';
function FeatureButton() {
const spotlight = useAnnouncement('new-feature');
const handleClick = () => {
// Show spotlight when feature is first used
if (!spotlight.isDismissed && spotlight.canShow) {
spotlight.show();
}
};
return (
<>
Use Feature
>
);
}
```
---
## Accessibility
- **Overlay** - `role="presentation"`, blocks interaction with background
- **Focus trap** - Focus stays within announcement card
- **Keyboard** - Escape closes spotlight
- **Screen reader** - Announces content when shown
---
## Related
- [Headless Spotlight](/docs/announcements/headless/spotlight)
- [useAnnouncement](/docs/announcements/hooks/use-announcement)
---
# AnnouncementToast
> AnnouncementToast component: auto-dismissing corner notification for quick updates with configurable duration and position
A temporary notification that appears in a corner. Best for quick, non-critical messages.
## When to Use
- Quick tips
- Success confirmations
- Non-critical updates
- Ephemeral messages
- Status notifications
---
## Basic Usage
```tsx
import { AnnouncementsProvider, AnnouncementToast } from '@tour-kit/announcements';
const announcements = [
{
id: 'quick-tip',
variant: 'toast',
title: 'Pro Tip: Use keyboard shortcuts!',
toastOptions: {
position: 'bottom-right',
autoDismiss: true,
autoDismissDelay: 5000,
},
},
];
```
---
## Toast Options
---
## Positions
```tsx
// Top positions
toastOptions: { position: 'top-left' }
toastOptions: { position: 'top-center' }
toastOptions: { position: 'top-right' }
// Bottom positions
toastOptions: { position: 'bottom-left' }
toastOptions: { position: 'bottom-center' }
toastOptions: { position: 'bottom-right' } // Default
```
---
## Auto-Dismiss
```tsx
// Auto-dismiss after 3 seconds
toastOptions: {
autoDismiss: true,
autoDismissDelay: 3000,
showProgress: true, // Progress bar countdown
}
// Manual dismiss only
toastOptions: {
autoDismiss: false,
}
```
---
## Complete Example
```tsx
{
id: 'success-message',
variant: 'toast',
title: 'Changes saved successfully!',
toastOptions: {
position: 'bottom-right',
autoDismiss: true,
autoDismissDelay: 4000,
showProgress: true,
intent: 'success',
},
frequency: 'always', // Show every time
onDismiss: (reason) => {
if (reason === 'auto_dismiss') {
console.log('Toast auto-dismissed');
}
},
}
```
---
## Intent Styles
```tsx
toastOptions: { intent: 'default' } // Neutral
toastOptions: { intent: 'info' } // Blue
toastOptions: { intent: 'success' } // Green
toastOptions: { intent: 'warning' } // Yellow
toastOptions: { intent: 'error' } // Red
```
---
## Related
- [Headless Toast](/docs/announcements/headless/toast)
- [useAnnouncement](/docs/announcements/hooks/use-announcement)
---
# Variants
> CVA variant exports for @tour-kit/announcements — bannerVariants, modalContentVariants, modalOverlayVariants, slideoutContentVariants, toastVariants, and the full styling surface
Each styled component is built on a `cva()` call that exposes its slot/size/variant matrix. Import the variant function to extend or compose the classes; import the matching `*Variants` type to get prop-level autocomplete.
```tsx
import { modalContentVariants } from '@tour-kit/announcements'
{/* ... */}
```
```tsx
import type { ModalContentVariants } from '@tour-kit/announcements'
type ModalSize = ModalContentVariants['size'] // 'sm' | 'md' | 'lg' | 'xl' | 'full'
```
## Modal
| Variant fn | Slots |
|------------|-------|
| `modalContentVariants` | `size`: `'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` |
| `modalOverlayVariants` | (compound classes only) |
## Slideout
| Variant fn | Slots |
|------------|-------|
| `slideoutContentVariants` | width / direction (see source) |
| `slideoutOverlayVariants` | (compound classes only) |
## Banner
| Variant fn | Slots |
|------------|-------|
| `bannerVariants` | `position`, `tone` (see source) |
## Toast
| Variant fn | Slots |
|------------|-------|
| `toastVariants` | `variant` (default / success / warning / destructive) |
| `toastContainerVariants` | `position` |
| `toastProgressVariants` | (compound classes only) |
## Spotlight
| Variant fn | Slots |
|------------|-------|
| `spotlightContentVariants` | `placement` (see source) |
| `spotlightOverlayVariants` | (compound classes only) |
## Type aliases
Every variant function above has a matching `VariantProps` type alias (e.g. `ModalContentVariants`, `BannerVariants`, ...) — listed in [Type Reference](/docs/announcements/types#variants-types).
---
# Audience Targeting
> Target announcements to user segments with role-based, attribute, and custom predicate audience filtering rules
Target specific user segments with announcements. Only show messages to users who match your conditions.
## Why Audience Targeting?
Send the right message to the right users:
- **Personalized** - Show relevant announcements to specific user groups
- **Efficient** - Don't waste impressions on irrelevant users
- **Contextual** - Message users based on their plan, role, or behavior
- **Privacy-safe** - All targeting happens client-side
---
## User Context
Provide user information to the provider:
```tsx
```
The `userContext` object can contain any data you need for targeting.
---
## Audience Conditions
Define conditions using the `audience` property:
```tsx
{
id: 'pro-feature',
variant: 'modal',
title: 'New Pro Feature',
audience: [
{ field: 'plan', operator: 'equals', value: 'pro' },
],
}
```
### Operators
---
## Examples
### Plan-Based Targeting
```tsx
// Show to free users only
{
id: 'upgrade-promo',
variant: 'banner',
title: 'Upgrade to Pro',
audience: [
{ field: 'plan', operator: 'equals', value: 'free' },
],
}
// Show to pro or enterprise users
{
id: 'advanced-feature',
variant: 'modal',
title: 'New Advanced Analytics',
audience: [
{ field: 'plan', operator: 'in', value: ['pro', 'enterprise'] },
],
}
```
### Role-Based Targeting
```tsx
// Show to admins only
{
id: 'admin-dashboard',
variant: 'slideout',
title: 'New Admin Dashboard',
audience: [
{ field: 'role', operator: 'equals', value: 'admin' },
],
}
// Show to non-admin users
{
id: 'request-access',
variant: 'toast',
title: 'Need Admin Access?',
audience: [
{ field: 'role', operator: 'notEquals', value: 'admin' },
],
}
```
### Feature-Based Targeting
```tsx
// Show if user has export feature
{
id: 'export-update',
variant: 'banner',
title: 'Export Feature Improved',
audience: [
{ field: 'features', operator: 'contains', value: 'export' },
],
}
// Show if user lacks analytics
{
id: 'analytics-promo',
variant: 'modal',
title: 'Try Analytics',
audience: [
{ field: 'features', operator: 'notContains', value: 'analytics' },
],
}
```
### Date-Based Targeting
```tsx
// Show to users who signed up after a date
{
id: 'new-user-welcome',
variant: 'modal',
title: 'Welcome!',
audience: [
{
field: 'signupDate',
operator: 'greaterThan',
value: '2024-01-01',
},
],
}
```
---
## Multiple Conditions
Combine multiple conditions (AND logic):
```tsx
{
id: 'trial-ending',
variant: 'banner',
title: 'Your Trial Ends Soon',
audience: [
{ field: 'plan', operator: 'equals', value: 'trial' },
{ field: 'daysRemaining', operator: 'lessThan', value: 7 },
],
}
```
All conditions must match for the announcement to show.
---
## Custom Conditions
Use custom functions for complex targeting:
```tsx
{
// Custom logic
const { user } = context;
const isActiveUser = user.lastLogin > Date.now() - 86400000;
const hasNoRecent Purchase = !user.lastPurchase;
return isActiveUser && hasNoRecentPurchase;
},
},
],
},
]}
/>
```
### Custom Condition Type
```tsx
type CustomCondition = {
type: 'custom';
check: (context: T) => boolean;
};
```
---
## OR Conditions
Create OR logic with multiple announcement configs:
```tsx
const announcements = [
// Show to free users
{
id: 'upgrade-promo-free',
variant: 'banner',
title: 'Upgrade to Pro',
audience: [
{ field: 'plan', operator: 'equals', value: 'free' },
],
},
// OR show to trial users
{
id: 'upgrade-promo-trial',
variant: 'banner',
title: 'Upgrade to Pro',
audience: [
{ field: 'plan', operator: 'equals', value: 'trial' },
],
},
];
```
Or use the `in` operator:
```tsx
{
id: 'upgrade-promo',
variant: 'banner',
title: 'Upgrade to Pro',
audience: [
{ field: 'plan', operator: 'in', value: ['free', 'trial'] },
],
}
```
---
## Dynamic User Context
Update user context when user data changes:
```tsx
function App() {
const { user } = useAuth();
return (
);
}
```
---
## Programmatic Checks
Check if an announcement matches a user:
```tsx
import { useAnnouncement } from '@tour-kit/announcements';
function ConditionalTrigger() {
const announcement = useAnnouncement('pro-feature');
// canShow checks both frequency rules AND audience targeting
if (announcement.canShow) {
return (
Learn About Pro Features
);
}
return null;
}
```
---
## Use Cases
### Trial Expiration
```tsx
{
id: 'trial-ending',
variant: 'modal',
title: 'Your Trial Expires in 3 Days',
audience: [
{ field: 'plan', operator: 'equals', value: 'trial' },
{ field: 'trialDaysLeft', operator: 'lessThan', value: 4 },
],
frequency: { type: 'interval', days: 1 },
primaryAction: {
label: 'Upgrade Now',
onClick: () => router.push('/upgrade'),
},
}
```
### Feature Upsell
```tsx
{
id: 'analytics-upsell',
variant: 'slideout',
title: 'Unlock Analytics',
description: 'Get insights into your data with our analytics package.',
audience: [
{ field: 'features', operator: 'notContains', value: 'analytics' },
{ field: 'plan', operator: 'in', value: ['pro', 'enterprise'] },
],
frequency: { type: 'times', count: 2 },
}
```
### Onboarding for New Users
```tsx
{
id: 'welcome-tour',
variant: 'modal',
title: 'Welcome to Our Platform',
audience: [
{
type: 'custom',
check: (context) => {
const signupDate = new Date(context.signupDate);
const daysSinceSignup = (Date.now() - signupDate.getTime()) / 86400000;
return daysSinceSignup < 7;
},
},
],
frequency: 'once',
}
```
### Behavior-Based
```tsx
{
id: 'power-user-feature',
variant: 'spotlight',
title: 'Try Advanced Mode',
audience: [
{
type: 'custom',
check: (context) => {
// Show to users with high activity
return context.actionsPerDay > 50;
},
},
],
spotlightOptions: {
target: '#advanced-toggle',
},
}
```
---
## Privacy Considerations
### Best Practices
- Only include necessary user data in userContext
- Don't store sensitive information in announcement configs
- Use custom conditions for complex privacy requirements
- Consider using hashed or anonymized identifiers
---
## Testing Audiences
### Test Mode
```tsx
```
### Manual Testing
```tsx
// Test as free user
// Test as pro user
// Test as admin
```
---
## TypeScript
Type your user context for safety:
```tsx
interface UserContext {
plan: 'free' | 'pro' | 'enterprise';
role: 'user' | 'admin';
features: string[];
signupDate: string;
customerId: string;
}
const announcements: AnnouncementConfig[] = [
{
id: 'pro-feature',
variant: 'modal',
title: 'New Feature',
audience: [
// TypeScript knows 'plan' exists and its possible values
{ field: 'plan', operator: 'equals', value: 'pro' },
],
},
];
userContext={{
plan: 'free',
role: 'user',
features: [],
signupDate: '2024-01-15',
customerId: '12345',
}}
announcements={announcements}
/>
```
---
## Programmatic helpers
Pure functions exposed for tests, custom orchestrators, and SSR-side checks. The provider invokes the same functions internally — use them when you need to mirror the in-tree gating decision outside React.
### matchesAudience
```ts
import { matchesAudience } from '@tour-kit/announcements'
declare function matchesAudience(
conditions: AudienceCondition[] | undefined,
userContext: Record | undefined,
): boolean
```
Returns `true` if `userContext` satisfies every condition (or when `conditions` is `undefined`).
### validateConditions
```ts
import { validateConditions } from '@tour-kit/announcements'
declare function validateConditions(conditions: AudienceCondition[]): string[]
```
Returns a list of validation errors — useful in tests or admin tools that author audience rules before saving them.
---
## Related
- [Frequency Rules](/docs/announcements/configuration/frequency) - Control how often announcements show
- [Queue Management](/docs/announcements/configuration/queue) - Priority and scheduling
- [useAnnouncement](/docs/announcements/hooks/use-announcement) - Check canShow programmatically
---
# Frequency Rules
> Control announcement display frequency with once, daily, weekly, and session-based rules to avoid notification fatigue
Control how often users see announcements with flexible frequency rules. Rules are enforced automatically and state is persisted across sessions.
## Why Frequency Rules?
Prevent announcement fatigue by controlling when and how often announcements appear:
- **Respect users** - Don't show the same announcement repeatedly
- **Optimize engagement** - Show announcements at the right frequency
- **Track views** - Know how many times users have seen each announcement
- **Persistent state** - Frequency rules persist across sessions
---
## Frequency Types
```tsx
type FrequencyRule =
| 'once' // Only ever show once
| 'session' // Once per session
| 'always' // Every time
| { type: 'times'; count: number } // N times total
| { type: 'interval'; days: number } // Every N days
```
---
## Once
Show the announcement only one time ever:
```tsx
{
id: 'welcome',
variant: 'modal',
title: 'Welcome to Our App',
frequency: 'once', // Shows once, then never again
}
```
**Use for:**
- Welcome messages
- One-time feature introductions
- Privacy policy updates
- Terms acceptance
---
## Session
Show once per browser session:
```tsx
{
id: 'tip-of-day',
variant: 'toast',
title: 'Daily Tip',
frequency: 'session', // Shows once per session
}
```
**Use for:**
- Session-specific tips
- Temporary notices
- Per-visit reminders
A new session starts when:
- User closes all browser tabs
- Session storage is cleared
- Browser is restarted
---
## Always
Show every time the announcement is eligible:
```tsx
{
id: 'critical-alert',
variant: 'banner',
title: 'System Maintenance in Progress',
frequency: 'always', // Shows every time
}
```
**Use for:**
- Critical system alerts
- Real-time status updates
- Urgent notifications
- Temporary announcements
---
## Times
Show N times total, then stop:
```tsx
{
id: 'feature-promo',
variant: 'slideout',
title: 'Try Our New Feature',
frequency: { type: 'times', count: 3 }, // Show 3 times total
}
```
**Use for:**
- Feature promotions (show a few times)
- Gradual onboarding (spread over multiple visits)
- Survey invitations
- Feedback requests
### Example
```tsx
const announcements = [
{
id: 'survey',
variant: 'modal',
title: 'We Value Your Feedback',
description: 'Take our 2-minute survey to help us improve.',
frequency: {
type: 'times',
count: 2, // Show twice, then stop
},
primaryAction: {
label: 'Take Survey',
onClick: () => window.open('/survey'),
},
onShow: () => {
// Track which view this is
const announcement = useAnnouncement('survey');
analytics.track('survey_shown', {
viewNumber: announcement.viewCount,
});
},
},
];
```
---
## Interval
Show every N days:
```tsx
{
id: 'weekly-update',
variant: 'banner',
title: 'Weekly Product Update',
frequency: { type: 'interval', days: 7 }, // Show every 7 days
}
```
**Use for:**
- Periodic updates
- Weekly tips
- Monthly reminders
- Recurring promotions
### Examples
```tsx
// Show every day
frequency: { type: 'interval', days: 1 }
// Show every week
frequency: { type: 'interval', days: 7 }
// Show every 2 weeks
frequency: { type: 'interval', days: 14 }
// Show every month (approx)
frequency: { type: 'interval', days: 30 }
```
### How It Works
The interval starts from the last time the announcement was shown:
```tsx
// First view: Today at 10:00 AM
// Next eligible: 7 days later at 10:00 AM
// If dismissed before 7 days, won't show until interval passes
```
---
## Combining with Dismissal
Frequency rules work with dismissal patterns:
```tsx
{
id: 'promo',
variant: 'toast',
title: 'Limited Time Offer',
frequency: { type: 'interval', days: 3 }, // Show every 3 days
// If user dismisses, they won't see it again
// If user just hides it, it will show again in 3 days
}
```
### Dismiss vs Hide
- **Dismiss** - Permanently removes the announcement (ignores frequency)
- **Hide** - Temporarily hides, respects frequency rules
```tsx
import { useAnnouncement } from '@tour-kit/announcements';
function AnnouncementControls() {
const announcement = useAnnouncement('promo');
return (
{/* Hide - will show again based on frequency */}
Remind Me Later
{/* Dismiss - won't show again */}
announcement.dismiss()}>
Don't Show Again
);
}
```
---
## Checking Eligibility
Use `canShow` to check if an announcement can be shown:
```tsx
import { useAnnouncement } from '@tour-kit/announcements';
function SmartTrigger() {
const announcement = useAnnouncement('feature-tip');
useEffect(() => {
// Only show if frequency rules allow
if (announcement.canShow) {
announcement.show();
}
}, []);
}
```
---
## View Tracking
Track how many times an announcement has been viewed:
```tsx
import { useAnnouncement } from '@tour-kit/announcements';
function ViewCounter() {
const announcement = useAnnouncement('tip');
return (
Viewed {announcement.viewCount} times
Can show: {announcement.canShow ? 'Yes' : 'No'}
{announcement.state.lastViewedAt && (
Last viewed:{' '}
{announcement.state.lastViewedAt.toLocaleDateString()}
)}
);
}
```
---
## Advanced Patterns
### Reset After Completion
```tsx
{
id: 'onboarding',
variant: 'modal',
title: 'Complete Onboarding',
frequency: 'once',
primaryAction: {
label: 'Complete',
onClick: () => {
completeOnboarding();
announcement.complete();
// User completed - won't see again
},
},
secondaryAction: {
label: 'Skip',
onClick: () => {
announcement.hide();
// User skipped - can see again next session
},
},
}
```
### Conditional Frequency
```tsx
const getFrequency = (userPlan: string): FrequencyRule => {
// Free users see promo every 3 days
if (userPlan === 'free') {
return { type: 'interval', days: 3 };
}
// Pro users see once
return 'once';
};
const announcements = [
{
id: 'upgrade-promo',
variant: 'banner',
title: 'Upgrade to Pro',
frequency: getFrequency(user.plan),
},
];
```
### Progressive Disclosure
Show different announcements based on view count:
```tsx
function ProgressiveAnnouncements() {
const welcome = useAnnouncement('welcome');
const advanced = useAnnouncement('advanced-tips');
useEffect(() => {
// Show welcome first 3 times
if (welcome.viewCount < 3 && welcome.canShow) {
welcome.show();
}
// Then show advanced tips
else if (welcome.viewCount >= 3 && advanced.canShow) {
advanced.show();
}
}, [welcome.viewCount]);
}
```
---
## Storage
Frequency state is persisted using the storage option:
```tsx
```
### Stored Data
```json
{
"announcement-id": {
"viewCount": 2,
"lastViewedAt": "2024-01-20T10:30:00Z",
"isDismissed": false
}
}
```
---
## TypeScript
```tsx
import type { FrequencyRule } from '@tour-kit/announcements';
const onceRule: FrequencyRule = 'once';
const sessionRule: FrequencyRule = 'session';
const alwaysRule: FrequencyRule = 'always';
const timesRule: FrequencyRule = {
type: 'times',
count: 5,
};
const intervalRule: FrequencyRule = {
type: 'interval',
days: 7,
};
```
---
## Programmatic helpers
Pure functions for evaluating frequency rules outside React (tests, server-side checks, custom orchestrators).
### canShowByFrequency
```ts
import { canShowByFrequency } from '@tour-kit/announcements'
declare function canShowByFrequency(
state: AnnouncementState,
rule: FrequencyRule | undefined,
): boolean
```
Returns `true` when the rule allows showing right now, given the announcement's view history (`viewCount`, `lastShownAt`, dismissal record). Mirrors the gate the provider uses internally.
### canShowAfterDismissal
```ts
import { canShowAfterDismissal } from '@tour-kit/announcements'
declare function canShowAfterDismissal(rule: FrequencyRule | undefined): boolean
```
Returns `true` when the rule permits re-show after a dismissal (`'always'` or numeric-interval rules); `false` for `'once'` and `'session'`.
### getViewLimit
```ts
import { getViewLimit } from '@tour-kit/announcements'
declare function getViewLimit(rule: FrequencyRule | undefined): number
```
Returns the maximum view count permitted by the rule (`Infinity` for `'always'`, `1` for `'once'` and `'session'`, `n` for `{ type: 'times', count: n }`).
---
## Related
- [AnnouncementsProvider](/docs/announcements/providers/announcements-provider) - Configure storage
- [useAnnouncement](/docs/announcements/hooks/use-announcement) - Access view count and eligibility
- [Audience Targeting](/docs/announcements/configuration/audience) - Target specific users
---
# Queue Management
> Priority queue system: rank announcements by urgency, schedule display windows, and prevent overlapping notifications
Manage multiple announcements with a priority-based queue system. Announcements are automatically ordered and shown based on priority, frequency rules, and audience targeting.
## Why a Queue?
When you have multiple announcements, a queue system helps:
- **Prioritize** - Critical messages shown before routine updates
- **Prevent overwhelm** - Control how many announcements show at once
- **Auto-schedule** - Next announcement shows automatically
- **Respect limits** - Enforce maximum concurrent announcements
---
## How the Queue Works
```
Announcements → Filter (audience + frequency) → Sort (priority) → Queue → Display
```
1. **Filter** - Remove announcements that don't match audience or frequency rules
2. **Sort** - Order by priority: `critical` > `high` > `normal` > `low`
3. **Queue** - Maintain ordered list of eligible announcements
4. **Display** - Show based on queue configuration
---
## Priority Levels
```tsx
type AnnouncementPriority = 'low' | 'normal' | 'high' | 'critical';
```
### Critical
Urgent messages that interrupt everything:
```tsx
{
id: 'security-breach',
variant: 'modal',
priority: 'critical',
title: 'Security Alert',
description: 'Your account may be compromised. Update your password immediately.',
}
```
**Use for:**
- Security alerts
- Service outages
- Account issues
- Breaking changes
### High
Important but not critical:
```tsx
{
id: 'major-feature',
variant: 'slideout',
priority: 'high',
title: 'Major Feature Release',
description: 'Check out our biggest update of the year.',
}
```
**Use for:**
- Major feature releases
- Important updates
- Time-sensitive promotions
- Account notifications
### Normal (Default)
Standard announcements:
```tsx
{
id: 'product-update',
variant: 'banner',
priority: 'normal', // Default if not specified
title: 'Product Update',
}
```
**Use for:**
- Regular updates
- Feature improvements
- General announcements
- Product news
### Low
Non-urgent information:
```tsx
{
id: 'tip',
variant: 'toast',
priority: 'low',
title: 'Pro Tip',
}
```
**Use for:**
- Tips and tricks
- Optional features
- Suggestions
- Non-critical information
---
## Queue Configuration
Configure queue behavior in the provider:
```tsx
```
### Props
---
## Examples
### Single Announcement at a Time
```tsx
```
### Multiple Variants Simultaneously
```tsx
```
This allows a banner, toast, and modal to show simultaneously.
### Delayed Queue
```tsx
```
### Manual Queue Control
```tsx
```
---
## Programmatic Queue Control
Use the `useAnnouncementQueue` hook to control the queue:
```tsx
import { useAnnouncementQueue } from '@tour-kit/announcements';
function QueueControls() {
const { queue, showNext, clearQueue, queueSize } = useAnnouncementQueue();
return (
Queue size: {queueSize}
Show Next
Clear Queue
Queue Contents:
{queue.map((announcement) => (
{announcement.title} ({announcement.priority})
))}
);
}
```
### Hook API
void',
description: 'Show next announcement in queue',
},
clearQueue: {
type: '() => void',
description: 'Clear all queued announcements',
},
activeCount: {
type: 'number',
description: 'Number of currently visible announcements',
},
}}
/>
---
## Priority Interruption
Critical announcements interrupt lower-priority ones:
```tsx
const announcements = [
{
id: 'tip',
variant: 'toast',
priority: 'low',
title: 'Pro Tip',
// User is viewing this...
},
{
id: 'security-alert',
variant: 'modal',
priority: 'critical',
title: 'Security Alert',
// This appears and pushes tip back to queue
},
];
```
When a critical announcement is added while a low-priority one is showing:
1. Low-priority announcement is hidden
2. Critical announcement is shown
3. Low-priority announcement returns to queue
---
## Queue Strategies
### Sequential (Default)
Show announcements one at a time in priority order:
```tsx
queueConfig={{
maxConcurrent: 1,
autoShow: true,
}}
```
### Concurrent by Variant
Show multiple announcements if they use different variants:
```tsx
queueConfig={{
maxConcurrent: 5,
respectVariants: true,
}}
```
This allows:
- One modal
- One slideout
- One banner
- One toast
- One spotlight
All showing simultaneously.
### Manual Control
Developer controls when announcements show:
```tsx
queueConfig={{
autoShow: false,
}}
// In your component
function App() {
const { showNext } = useAnnouncementQueue();
useEffect(() => {
// Show announcement after user action
const timer = setTimeout(() => {
showNext();
}, 5000);
return () => clearTimeout(timer);
}, []);
}
```
---
## Advanced Patterns
### Priority Boost for Time-Sensitive
```tsx
const getAnnouncements = () => {
const now = Date.now();
const deadline = new Date('2024-12-31').getTime();
const daysLeft = (deadline - now) / 86400000;
return [
{
id: 'sale',
variant: 'banner',
title: 'Year-End Sale',
// Boost priority as deadline approaches
priority: daysLeft < 3 ? 'high' : 'normal',
},
];
};
```
### Dynamic Queue Size
```tsx
const [maxConcurrent, setMaxConcurrent] = useState(1);
// Show more announcements on larger screens
useEffect(() => {
const updateMax = () => {
if (window.innerWidth > 1200) {
setMaxConcurrent(3);
} else {
setMaxConcurrent(1);
}
};
updateMax();
window.addEventListener('resize', updateMax);
return () => window.removeEventListener('resize', updateMax);
}, []);
```
### Pause/Resume Queue
```tsx
function QueueManager() {
const [isPaused, setIsPaused] = useState(false);
const { showNext, clearQueue } = useAnnouncementQueue();
useEffect(() => {
if (isPaused) {
// Clear visible announcements
clearQueue();
} else {
// Resume showing
showNext();
}
}, [isPaused]);
return (
setIsPaused(!isPaused)}>
{isPaused ? 'Resume' : 'Pause'} Announcements
);
}
```
---
## Queue Events
Track queue events with callbacks:
```tsx
{
console.log('Queue updated:', queue.length, 'announcements');
}}
onShow={(id) => {
console.log('Showing announcement:', id);
analytics.track('announcement_shown', { id });
}}
onDismiss={(id, reason) => {
console.log('Dismissed:', id, 'Reason:', reason);
analytics.track('announcement_dismissed', { id, reason });
}}
/>
```
---
## Best Practices
### Don't Overwhelm Users
```tsx
// Good
queueConfig={{ maxConcurrent: 1 }}
// Use sparingly
queueConfig={{ maxConcurrent: 3, respectVariants: true }}
```
### Use Priority Appropriately
- **Critical**: Truly urgent (security, outages)
- **High**: Important but not urgent
- **Normal**: Standard updates
- **Low**: Optional tips
### Add Delays
Give users time to read:
```tsx
queueConfig={{
delayBetween: 1000, // 1 second breathing room
}}
```
### Clear Queue on Navigation
```tsx
function App() {
const { clearQueue } = useAnnouncementQueue();
const router = useRouter();
useEffect(() => {
// Clear announcements when user navigates
const handleRouteChange = () => {
clearQueue();
};
router.events.on('routeChangeStart', handleRouteChange);
return () => {
router.events.off('routeChangeStart', handleRouteChange);
};
}, [router]);
}
```
---
## TypeScript
```tsx
import type { QueueConfig, AnnouncementPriority } from '@tour-kit/announcements';
const queueConfig: QueueConfig = {
maxConcurrent: 1,
autoShow: true,
delayBetween: 1000,
respectVariants: false,
};
const priority: AnnouncementPriority = 'high';
```
## Custom Comparator
The queue is a `PriorityQueue`. Order is determined by a comparator built from the configured `priorityOrder` and per-priority `weights`. To inspect or override that ordering, build the same comparator with `createComparator`.
### createComparator
```ts
import { createComparator } from '@tour-kit/announcements'
import type { PriorityOrder, AnnouncementPriority, QueueItem } from '@tour-kit/announcements'
declare function createComparator(
order: PriorityOrder,
weights: Record,
): (a: QueueItem, b: QueueItem) => number
```
Returns a comparator suitable for `Array.prototype.sort`. The provider uses this same factory internally; building a comparator yourself is useful for tests or for sorting a snapshot of `useAnnouncementsContext().queue` outside the queue itself.
```tsx
const compare = createComparator('priority-then-time', {
critical: 1000,
high: 100,
normal: 10,
low: 1,
})
const sorted = [...queueSnapshot].sort(compare)
```
---
## Related
- [Frequency Rules](/docs/announcements/configuration/frequency) - Control how often announcements show
- [Audience Targeting](/docs/announcements/configuration/audience) - Target specific users
- [useAnnouncementQueue](/docs/announcements/hooks/use-announcement-queue) - Programmatic queue control
- [Type Reference → AnnouncementScheduler](/docs/announcements/types#announcementscheduler)
---
# Headless Components
> Headless announcement components: unstyled Modal, Toast, Banner, Slideout, and Spotlight with render props for custom UIs
Completely unstyled components that provide announcement logic and state through render props. Perfect for building custom UI that matches your design system.
## Why Headless?
Headless components give you:
- **Full design control** - No CSS to override
- **Framework flexibility** - Use with any CSS solution
- **Logic reuse** - Announcement behavior without UI opinions
- **Accessibility** - Built-in ARIA and keyboard support
- **Type safety** - Full TypeScript support
---
## Available Components
---
## Basic Pattern
All headless components use a render props pattern:
```tsx
import { HeadlessModal } from '@tour-kit/announcements/headless';
{
if (!isVisible) return null;
return (
{config.title}
{config.description}
Close
Don't Show Again
);
}}
/>
```
---
## Shared Render Props
All headless components provide these props:
```tsx
interface HeadlessRenderProps {
// State
isVisible: boolean;
isActive: boolean;
isDismissed: boolean;
canShow: boolean;
viewCount: number;
// Config & State
config: AnnouncementConfig;
state: AnnouncementState;
// Methods
show: () => void;
hide: () => void;
dismiss: (reason?: DismissalReason) => void;
complete: () => void;
}
```
---
## Quick Example
```tsx
import { HeadlessModal } from '@tour-kit/announcements/headless';
function CustomAnnouncement() {
return (
{
if (!isVisible) return null;
return (
{/* Overlay */}
{/* Content */}
{config.title}
{config.description}
{config.media?.type === 'image' && (
)}
{config.primaryAction && (
{
config.primaryAction?.onClick();
dismiss('primary_action');
}}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
{config.primaryAction.label}
)}
Close
);
}}
/>
);
}
```
---
## With CSS-in-JS
```tsx
import styled from '@emotion/styled';
import { HeadlessModal } from '@tour-kit/announcements/headless';
const Overlay = styled.div`
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 50;
`;
const Modal = styled.div`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 2rem;
border-radius: 0.5rem;
z-index: 51;
`;
function EmotionModal() {
return (
{
if (!isVisible) return null;
return (
<>
{config.title}
{config.description}
Close
>
);
}}
/>
);
}
```
---
## With UI Libraries
### shadcn/ui
```tsx
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { HeadlessModal } from '@tour-kit/announcements/headless';
(
{config.title}
{config.description}
)}
/>
```
### Radix UI
```tsx
import * as Dialog from '@radix-ui/react-dialog';
import { HeadlessModal } from '@tour-kit/announcements/headless';
(
{config.title}
{config.description}
Close
)}
/>
```
---
## Accessibility
Headless components provide accessibility primitives:
```tsx
(
{config.title}
{config.description}
)}
/>
```
---
## TypeScript
Full type safety with render props:
```tsx
import type { HeadlessModalProps, HeadlessRenderProps } from '@tour-kit/announcements/headless';
const renderModal = ({ isVisible, config, hide }: HeadlessRenderProps) => {
// Fully typed render function
};
```
---
## Next Steps
Explore each headless component for variant-specific render props:
- [HeadlessModal](/docs/announcements/headless/modal)
- [HeadlessSlideout](/docs/announcements/headless/slideout)
- [HeadlessBanner](/docs/announcements/headless/banner)
- [HeadlessToast](/docs/announcements/headless/toast)
- [HeadlessSpotlight](/docs/announcements/headless/spotlight)
---
# HeadlessBanner
> HeadlessAnnouncementBanner: unstyled banner primitive with dismiss and action state exposed via render props for theming
Unstyled banner bar that provides state and behavior through render props.
## Additional Render Props
## Example
```tsx
import { HeadlessBanner } from '@tour-kit/announcements/headless';
{
if (!isVisible) return null;
return (
{config.title}
{config.bannerOptions?.dismissable && (
dismiss()}>×
)}
);
}}
/>
```
## Related
- [``](/docs/announcements/components/banner) — styled counterpart with default theming and dismiss UI.
- [``](/docs/announcements/headless/modal), [``](/docs/announcements/headless/slideout), [``](/docs/announcements/headless/toast), [``](/docs/announcements/headless/spotlight) — sibling headless primitives for the other display variants.
- [Headless overview](/docs/announcements/headless) — when to reach for headless vs. styled.
- [`useAnnouncement`](/docs/announcements/hooks/use-announcement) — the hook this primitive exposes via render props.
- [``](/docs/announcements/providers/announcements-provider) — required ancestor.
---
# HeadlessModal
> HeadlessAnnouncementModal: unstyled dialog primitive with focus trap, backdrop click, and Escape key dismiss handling
Unstyled modal component that provides state and behavior through render props.
## Render Props
void', description: 'Hide modal' },
dismiss: { type: '(reason?) => void', description: 'Dismiss forever' },
modalOptions: { type: 'ModalOptions', description: 'Modal-specific options' },
titleId: { type: 'string', description: 'ID for aria-labelledby' },
descriptionId: { type: 'string', description: 'ID for aria-describedby' },
}}
/>
## Example
```tsx
import { HeadlessModal } from '@tour-kit/announcements/headless';
{
if (!isVisible) return null;
return (
{config.title}
{config.description}
Close
dismiss()}>Don't Show Again
);
}}
/>
```
## Related
- [``](/docs/announcements/components/modal) — styled counterpart with default backdrop, focus trap, and Escape handling.
- [``](/docs/announcements/headless/banner), [``](/docs/announcements/headless/slideout), [``](/docs/announcements/headless/toast), [``](/docs/announcements/headless/spotlight) — sibling headless primitives for the other display variants.
- [Headless overview](/docs/announcements/headless) — when to reach for headless vs. styled.
- [`useAnnouncement`](/docs/announcements/hooks/use-announcement) — the hook this primitive exposes via render props.
- [`useFocusTrap`](/docs/core/hooks/use-focus-trap) — mandatory for custom modal implementations.
- [Accessibility guide](/docs/guides/accessibility) — `role="dialog"` and `aria-modal` requirements.
---
# HeadlessSlideout
> HeadlessAnnouncementSlideout: unstyled drawer primitive with slide animation state and content sections via render props
Unstyled slideout panel that provides state and behavior through render props.
## Additional Render Props
## Example
```tsx
import { HeadlessSlideout } from '@tour-kit/announcements/headless';
{
if (!isVisible) return null;
return (
{config.title}
{config.description}
Close
);
}}
/>
```
## Related
- [``](/docs/announcements/components/slideout) — styled counterpart with built-in slide animation.
- [``](/docs/announcements/headless/banner), [``](/docs/announcements/headless/modal), [``](/docs/announcements/headless/toast), [``](/docs/announcements/headless/spotlight) — sibling headless primitives for the other display variants.
- [Headless overview](/docs/announcements/headless) — when to reach for headless vs. styled.
- [`useAnnouncement`](/docs/announcements/hooks/use-announcement) — the hook this primitive exposes via render props.
- [``](/docs/announcements/providers/announcements-provider) — required ancestor.
---
# HeadlessSpotlight
> HeadlessAnnouncementSpotlight: unstyled highlight primitive with target element positioning and overlay via render props
Unstyled element spotlight that provides state and behavior through render props.
## Additional Render Props
## Example
```tsx
import { HeadlessSpotlight } from '@tour-kit/announcements/headless';
{
if (!isVisible || !targetElement || !targetRect) return null;
return (
<>
{/* Overlay with cutout */}
{/* Spotlight highlight */}
{/* Announcement card */}
{config.title}
{config.description}
Got it
>
);
}}
/>
```
## Related
- [``](/docs/announcements/components/spotlight) — styled counterpart with default overlay and cutout.
- [``](/docs/announcements/headless/banner), [``](/docs/announcements/headless/modal), [``](/docs/announcements/headless/slideout), [``](/docs/announcements/headless/toast) — sibling headless primitives for the other display variants.
- [Headless overview](/docs/announcements/headless) — when to reach for headless vs. styled.
- [`useAnnouncement`](/docs/announcements/hooks/use-announcement) — the hook this primitive exposes via render props.
- [`useSpotlight`](/docs/core/hooks/use-spotlight) and [`useElementPosition`](/docs/core/hooks/use-element-position) — sibling hooks for the spotlight positioning math.
- [Tour ``](/docs/react/components/tour-overlay) — the tour-side analog for the same overlay UX.
---
# HeadlessToast
> HeadlessAnnouncementToast: unstyled notification primitive with auto-dismiss timer and position data via render props
Unstyled toast notification that provides state and behavior through render props.
## Additional Render Props
## Example
```tsx
import { HeadlessToast } from '@tour-kit/announcements/headless';
{
if (!isVisible) return null;
return (
{config.title}
{config.toastOptions?.showProgress && (
)}
{Math.ceil(remainingTime / 1000)}s
);
}}
/>
```
## Related
- [``](/docs/announcements/components/toast) — styled counterpart with default auto-dismiss timer and positioning.
- [``](/docs/announcements/headless/banner), [``](/docs/announcements/headless/modal), [``](/docs/announcements/headless/slideout), [``](/docs/announcements/headless/spotlight) — sibling headless primitives for the other display variants.
- [Headless overview](/docs/announcements/headless) — when to reach for headless vs. styled.
- [`useAnnouncement`](/docs/announcements/hooks/use-announcement) and [`useAnnouncementQueue`](/docs/announcements/hooks/use-announcement-queue) — the hooks this primitive exposes via render props.
- [Frequency configuration](/docs/announcements/configuration/frequency) — control how often the toast re-appears.
---
# useAnnouncement
> useAnnouncement hook: control visibility, dismissal, and action tracking for a single announcement instance in React
Hook for accessing and controlling a single announcement. Provides state, configuration, and methods for showing, hiding, and dismissing the announcement.
## Why Use This Hook?
Use `useAnnouncement` when you need to:
- Programmatically show or hide an announcement
- Check if an announcement has been dismissed
- Track how many times an announcement has been viewed
- Determine if an announcement can be shown based on frequency rules
- Manually trigger announcement actions
---
## Basic Usage
```tsx
import { useAnnouncement } from '@tour-kit/announcements';
function WelcomeButton() {
const announcement = useAnnouncement('welcome-tour');
return (
Start Welcome Tour
);
}
```
---
## Parameters
---
## Return Value
```tsx
const {
// State
state,
config,
isVisible,
isActive,
isDismissed,
canShow,
viewCount,
// Methods
show,
hide,
dismiss,
complete,
reset,
} = useAnnouncement('announcement-id');
```
### Properties
### Methods
void',
description: 'Show the announcement (increments view count)',
},
hide: {
type: '() => void',
description: 'Hide the announcement (can be shown again)',
},
dismiss: {
type: '(reason?: DismissalReason) => void',
description: 'Permanently dismiss the announcement',
},
complete: {
type: '() => void',
description: 'Mark the announcement as completed',
},
reset: {
type: '() => void',
description: 'Reset all state (views, dismissal, completion)',
},
}}
/>
---
## Examples
### Show/Hide Announcements
```tsx
import { useAnnouncement } from '@tour-kit/announcements';
function AnnouncementControls() {
const announcement = useAnnouncement('new-feature');
return (
Show Announcement
Hide Announcement
Status: {announcement.isVisible ? 'Visible' : 'Hidden'}
);
}
```
### Check Dismissal State
```tsx
function FeatureHighlight() {
const announcement = useAnnouncement('export-feature');
if (announcement.isDismissed) {
return null; // Don't show UI if user dismissed it
}
return (
New!
Learn More
);
}
```
### Track View Count
```tsx
function ViewTracker() {
const announcement = useAnnouncement('onboarding');
useEffect(() => {
if (announcement.viewCount >= 3 && announcement.canShow) {
// Show a different announcement after 3 views
console.log('User has seen this 3 times');
}
}, [announcement.viewCount]);
return (
You've seen this {announcement.viewCount} times
Show Again
);
}
```
### Respect Frequency Rules
```tsx
function SmartAnnouncementButton() {
const announcement = useAnnouncement('weekly-tip');
if (!announcement.canShow) {
return Check back next week for a new tip!
;
}
return (
Show Weekly Tip
);
}
```
### Complete Announcement
```tsx
function OnboardingFlow() {
const announcement = useAnnouncement('onboarding');
const handleFinishOnboarding = () => {
// Mark as completed (for analytics)
announcement.complete();
// Navigate to dashboard
router.push('/dashboard');
};
return (
Welcome!
Get Started
);
}
```
### Reset Announcement
```tsx
function SettingsPanel() {
const { resetAll } = useAnnouncements();
const welcome = useAnnouncement('welcome');
return (
Announcement Settings
Reset Welcome Announcement
Reset All Announcements
Welcome announcement status:
Views: {welcome.viewCount}
Dismissed: {welcome.isDismissed ? 'Yes' : 'No'}
Can show: {welcome.canShow ? 'Yes' : 'No'}
);
}
```
---
## Dismissal Reasons
When calling `dismiss()`, you can optionally specify a reason:
```tsx
const announcement = useAnnouncement('feature');
// Dismiss with default reason ('programmatic')
announcement.dismiss();
// Dismiss with specific reason
announcement.dismiss('close_button');
announcement.dismiss('overlay_click');
announcement.dismiss('escape_key');
announcement.dismiss('primary_action');
announcement.dismiss('secondary_action');
announcement.dismiss('auto_dismiss');
```
This is tracked in analytics via the `onDismiss` callback:
```tsx
{
analytics.track('announcement_dismissed', { id, reason });
}}
/>
```
---
## State Object
The `state` property contains the full announcement state:
```tsx
const announcement = useAnnouncement('feature');
console.log(announcement.state);
// {
// id: 'feature',
// isActive: false,
// isVisible: false,
// isDismissed: false,
// viewCount: 3,
// lastViewedAt: Date('2024-01-20T10:30:00Z'),
// dismissedAt: null,
// dismissalReason: null,
// completedAt: null,
// }
```
---
## Config Object
The `config` property contains the announcement configuration:
```tsx
const announcement = useAnnouncement('feature');
console.log(announcement.config);
// {
// id: 'feature',
// variant: 'modal',
// priority: 'high',
// title: 'New Feature',
// description: '...',
// frequency: 'once',
// modalOptions: { size: 'md' },
// // ... rest of config
// }
```
---
## Conditional Rendering
Show different UI based on announcement state:
```tsx
function ConditionalAnnouncement() {
const announcement = useAnnouncement('promo');
if (!announcement.config) {
return Announcement not found
;
}
if (announcement.isDismissed) {
return (
You dismissed this announcement
Show Again
);
}
if (!announcement.canShow) {
return This announcement will be available soon
;
}
return (
{announcement.config.title}
);
}
```
---
## TypeScript
The hook is fully typed:
```tsx
import { useAnnouncement } from '@tour-kit/announcements';
import type {
AnnouncementState,
AnnouncementConfig,
DismissalReason,
} from '@tour-kit/announcements';
function TypedComponent() {
const announcement = useAnnouncement('feature');
// All properties are typed
const isVisible: boolean = announcement.isVisible;
const viewCount: number = announcement.viewCount;
const state: AnnouncementState = announcement.state;
const config: AnnouncementConfig = announcement.config;
// Methods are typed
announcement.show(); // () => void
announcement.dismiss('close_button'); // (reason?: DismissalReason) => void
}
```
---
## Common Patterns
### Auto-show on Mount
```tsx
function AutoShowAnnouncement() {
const announcement = useAnnouncement('welcome');
useEffect(() => {
if (announcement.canShow) {
announcement.show();
}
}, []);
return ;
}
```
### Show After User Action
```tsx
function FeatureButton() {
const announcement = useAnnouncement('feature-tip');
const handleClick = () => {
// Perform action
performFeature();
// Show tip after action
if (announcement.canShow) {
announcement.show();
}
};
return Use Feature ;
}
```
### Delay Before Showing
```tsx
function DelayedAnnouncement() {
const announcement = useAnnouncement('tip');
useEffect(() => {
// Show after 5 seconds
const timer = setTimeout(() => {
if (announcement.canShow) {
announcement.show();
}
}, 5000);
return () => clearTimeout(timer);
}, []);
return ;
}
```
---
## Error Handling
If the announcement ID doesn't exist in the provider's configuration:
```tsx
function SafeAnnouncement() {
const announcement = useAnnouncement('non-existent');
// Check if config exists
if (!announcement.config) {
console.warn('Announcement not found');
return null;
}
return Show ;
}
```
---
## Related
- [useAnnouncements](/docs/announcements/hooks/use-announcements) - Access all announcements
- [useAnnouncementQueue](/docs/announcements/hooks/use-announcement-queue) - Control the queue
- [AnnouncementsProvider](/docs/announcements/providers/announcements-provider) - Provider configuration
---
# useAnnouncementQueue
> useAnnouncementQueue hook: manage priority-based display ordering and automatic scheduling of queued announcements
Hook for accessing and controlling the announcement queue. The queue automatically orders announcements by priority and manages which announcements to show based on `queueConfig`.
## Why Use This Hook?
Use `useAnnouncementQueue` when you need to:
- Manually trigger the next announcement in queue
- Check what announcements are queued
- Get an announcement's position in the queue
- Clear the queue
- Build custom queue management UIs
---
## Basic Usage
```tsx
import { useAnnouncementQueue } from '@tour-kit/announcements';
function QueueControls() {
const { queue, showNext } = useAnnouncementQueue();
return (
{queue.length} announcements in queue
Show Next
);
}
```
---
## Return Value
```tsx
const {
// State
queue,
size,
isEmpty,
config,
// Methods
showNext,
clear,
isQueued,
getPosition,
} = useAnnouncementQueue();
```
### Properties
### Methods
void',
description: 'Show the next announcement in the queue',
},
clear: {
type: '() => void',
description: 'Clear all announcements from the queue',
},
isQueued: {
type: '(id: string) => boolean',
description: 'Check if an announcement is in the queue',
},
getPosition: {
type: '(id: string) => number',
description: 'Get position of announcement in queue (0-based, -1 if not queued)',
},
}}
/>
---
## Queue Order
Announcements are automatically ordered by priority:
**Priority order:** `critical` > `high` > `normal` > `low`
```tsx
// Provider configuration
const announcements = [
{ id: 'low-priority', priority: 'low' },
{ id: 'critical-alert', priority: 'critical' },
{ id: 'high-priority', priority: 'high' },
{ id: 'normal-msg', priority: 'normal' },
];
// Queue will be ordered as:
// ['critical-alert', 'high-priority', 'normal-msg', 'low-priority']
```
---
## Examples
### Show Next Announcement
```tsx
import { useAnnouncementQueue } from '@tour-kit/announcements';
function NextAnnouncementButton() {
const { queue, showNext, isEmpty } = useAnnouncementQueue();
if (isEmpty) {
return No announcements to show
;
}
return (
Show Next Announcement ({queue.length} remaining)
);
}
```
### Display Queue Status
```tsx
import { useAnnouncementQueue, useAnnouncements } from '@tour-kit/announcements';
function QueueStatus() {
const { queue, size, isEmpty } = useAnnouncementQueue();
const { announcements } = useAnnouncements();
if (isEmpty) {
return Queue is empty
;
}
return (
Queue ({size})
{queue.map((id, index) => {
const announcement = announcements.byId[id];
return (
{announcement.config.title}
Priority: {announcement.config.priority}
Position: {index + 1}
);
})}
);
}
```
### Check if Queued
```tsx
function AnnouncementBadge({ id }: { id: string }) {
const { isQueued, getPosition } = useAnnouncementQueue();
if (!isQueued(id)) {
return null;
}
const position = getPosition(id);
return (
In queue (#{position + 1})
);
}
```
### Clear Queue
```tsx
function QueueManager() {
const { queue, clear, showNext } = useAnnouncementQueue();
const handleClear = () => {
if (confirm('Clear all queued announcements?')) {
clear();
}
};
return (
Queue Management
{queue.length} announcements queued
Show Next
Clear Queue
);
}
```
### Queue Configuration
```tsx
function QueueConfigDisplay() {
const { config, size } = useAnnouncementQueue();
return (
Queue Configuration
Max Concurrent:
{config.maxConcurrent}
Auto Show:
{config.autoShow ? 'Yes' : 'No'}
Delay Between:
{config.delayBetween}ms
Respect Priority:
{config.respectPriority ? 'Yes' : 'No'}
Current Queue Size:
{size}
);
}
```
---
## Queue Behavior
### Auto-Show
When `autoShow` is enabled (default), the queue automatically shows the next announcement when the current one is dismissed:
```tsx
```
### Manual Control
Disable `autoShow` to manually control when announcements are shown:
```tsx
function ManualQueue() {
const { showNext, queue } = useAnnouncementQueue();
return (
Show Next ({queue.length} queued)
);
}
```
### Max Concurrent
Control how many announcements can be visible at once:
```tsx
```
### Delay Between
Add a delay between showing announcements:
```tsx
```
---
## Advanced Patterns
### Priority-Based Queue Display
```tsx
import { useAnnouncementQueue, useAnnouncement } from '@tour-kit/announcements';
function PriorityQueue() {
const { queue } = useAnnouncementQueue();
const grouped = queue.reduce((acc, id) => {
const announcement = useAnnouncement(id);
const priority = announcement.config.priority || 'normal';
if (!acc[priority]) acc[priority] = [];
acc[priority].push(announcement);
return acc;
}, {} as Record);
return (
{grouped.critical?.length > 0 && (
Critical ({grouped.critical.length})
{/* Render critical announcements */}
)}
{grouped.high?.length > 0 && (
High Priority ({grouped.high.length})
{/* Render high priority announcements */}
)}
);
}
```
### Queue Progress Indicator
```tsx
function QueueProgress() {
const { queue, size } = useAnnouncementQueue();
const { count } = useAnnouncements();
const total = count.total;
const remaining = size;
const shown = total - remaining;
const progress = (shown / total) * 100;
return (
);
}
```
### Conditional Queue Display
```tsx
function ConditionalQueue() {
const { queue, isEmpty, showNext } = useAnnouncementQueue();
const { announcements } = useAnnouncements();
if (isEmpty) {
return All announcements have been shown
;
}
const nextId = queue[0];
const next = announcements.byId[nextId];
return (
Next Announcement
{next.config.title}
Show Now
);
}
```
---
## TypeScript
The hook is fully typed:
```tsx
import { useAnnouncementQueue } from '@tour-kit/announcements';
import type { QueueConfig } from '@tour-kit/announcements';
function TypedComponent() {
const {
queue,
size,
isEmpty,
config,
showNext,
clear,
isQueued,
getPosition,
} = useAnnouncementQueue();
// All properties are typed
const queueIds: string[] = queue;
const queueSize: number = size;
const empty: boolean = isEmpty;
const queueConfig: QueueConfig = config;
// Methods are typed
showNext(); // () => void
clear(); // () => void
const queued: boolean = isQueued('announcement-id');
const position: number = getPosition('announcement-id');
}
```
---
## Common Use Cases
### Dashboard Queue Widget
```tsx
function QueueWidget() {
const { queue, size, showNext, isEmpty } = useAnnouncementQueue();
const { announcements } = useAnnouncements();
if (isEmpty) {
return (
);
}
const next = announcements.byId[queue[0]];
return (
Pending Announcements ({size})
{next.config.title}
Priority: {next.config.priority}
Show Now
);
}
```
### Admin Queue Manager
```tsx
function AdminQueueManager() {
const { queue, clear, showNext, getPosition } = useAnnouncementQueue();
const { announcements } = useAnnouncements();
return (
Announcement Queue
Show Next
Clear Queue
Position
Title
Variant
Priority
{queue.map((id) => {
const a = announcements.byId[id];
const position = getPosition(id);
return (
{position + 1}
{a.config.title}
{a.config.variant}
{a.config.priority}
);
})}
);
}
```
---
## Related
- [useAnnouncement](/docs/announcements/hooks/use-announcement) - Control a single announcement
- [useAnnouncements](/docs/announcements/hooks/use-announcements) - Access all announcements
- [Queue Configuration](/docs/announcements/configuration/queue) - Queue config details
- [AnnouncementsProvider](/docs/announcements/providers/announcements-provider) - Provider setup
---
# useAnnouncements
> useAnnouncements hook: query all announcements, filter by status or variant, and manage the announcement collection state
Hook for accessing all announcements and global announcement state. Use this to query, filter, and manage multiple announcements at once.
## Why Use This Hook?
Use `useAnnouncements` when you need to:
- Access all announcements in the provider
- Filter announcements by variant, priority, or dismissal state
- Get counts of visible or dismissed announcements
- Reset multiple announcements at once
- Build announcement management UIs
---
## Basic Usage
```tsx
import { useAnnouncements } from '@tour-kit/announcements';
function AnnouncementList() {
const { announcements } = useAnnouncements();
return (
All Announcements
{announcements.all.map(a => (
{a.config.title}
))}
);
}
```
---
## Return Value
```tsx
const {
// Collections
announcements,
// State
activeId,
// Arrays
ids,
visible,
dismissed,
// Count
count,
// Methods
getFiltered,
resetAll,
} = useAnnouncements();
```
### Properties
### Methods
Announcement[]',
description: 'Get announcements matching filter criteria',
},
resetAll: {
type: '() => void',
description: 'Reset state for all announcements',
},
}}
/>
---
## Announcements Collection
The `announcements` object provides different ways to access announcements:
```tsx
const { announcements } = useAnnouncements();
// Get all announcements
announcements.all // Announcement[]
// Get by ID
announcements.byId['feature-1'] // Announcement | undefined
// Get by variant
announcements.byVariant.modal // Announcement[]
announcements.byVariant.slideout // Announcement[]
announcements.byVariant.banner // Announcement[]
announcements.byVariant.toast // Announcement[]
announcements.byVariant.spotlight // Announcement[]
```
---
## Examples
### Display All Announcements
```tsx
import { useAnnouncements } from '@tour-kit/announcements';
function AnnouncementsDashboard() {
const { announcements, count } = useAnnouncements();
return (
Announcements ({count.total})
Visible: {count.visible}
Dismissed: {count.dismissed}
Can show: {count.canShow}
{announcements.all.map(a => (
{a.config.title}
Variant: {a.config.variant}
Priority: {a.config.priority}
Views: {a.state.viewCount}
{a.state.isDismissed && Dismissed }
))}
);
}
```
### Filter by Variant
```tsx
function ModalAnnouncements() {
const { announcements } = useAnnouncements();
const modals = announcements.byVariant.modal;
return (
Modal Announcements
{modals.map(a => (
{a.config.title}
))}
);
}
```
### Filter with Custom Criteria
```tsx
function FilteredAnnouncements() {
const { getFiltered } = useAnnouncements();
// Get high priority announcements that haven't been dismissed
const highPriority = getFiltered({
priority: ['high', 'critical'],
dismissed: false,
});
// Get visible modals
const visibleModals = getFiltered({
variant: ['modal'],
visible: true,
});
// Get announcements with specific IDs
const specific = getFiltered({
ids: ['feature-1', 'feature-2'],
});
return (
High Priority
{highPriority.map(a => (
{a.config.title}
))}
Visible Modals
{visibleModals.map(a => (
{a.config.title}
))}
);
}
```
### Active Announcement
```tsx
function ActiveAnnouncementIndicator() {
const { activeId, announcements } = useAnnouncements();
if (!activeId) {
return No active announcement
;
}
const active = announcements.byId[activeId];
return (
Currently showing: {active.config.title}
Variant: {active.config.variant}
);
}
```
### Reset All Announcements
```tsx
function AnnouncementSettings() {
const { resetAll, count } = useAnnouncements();
const handleReset = () => {
if (confirm('Reset all announcement state? This cannot be undone.')) {
resetAll();
}
};
return (
Settings
You have dismissed {count.dismissed} announcements
Reset All Announcements
);
}
```
### Visible/Dismissed Lists
```tsx
function AnnouncementStatus() {
const { visible, dismissed, announcements } = useAnnouncements();
return (
Visible Announcements ({visible.length})
{visible.map(id => (
{announcements.byId[id].config.title}
))}
Dismissed Announcements ({dismissed.length})
{dismissed.map(id => (
{announcements.byId[id].config.title}
))}
);
}
```
---
## Filter Interface
The `getFiltered` method accepts this filter interface:
```tsx
interface AnnouncementsFilter {
ids?: string[];
variant?: AnnouncementVariant[];
priority?: AnnouncementPriority[];
visible?: boolean;
dismissed?: boolean;
canShow?: boolean;
}
```
### Filter Examples
```tsx
const { getFiltered } = useAnnouncements();
// Get all modals and slideouts
const sidePanels = getFiltered({
variant: ['modal', 'slideout'],
});
// Get critical and high priority
const important = getFiltered({
priority: ['critical', 'high'],
});
// Get announcements that can be shown
const available = getFiltered({
canShow: true,
dismissed: false,
});
// Combine filters
const criticalModals = getFiltered({
variant: ['modal'],
priority: ['critical'],
visible: false,
});
```
---
## Count Object
The `count` object provides quick access to announcement statistics:
```tsx
const { count } = useAnnouncements();
console.log(count);
// {
// total: 10,
// visible: 2,
// dismissed: 5,
// canShow: 3,
// }
```
Use in UI:
```tsx
function AnnouncementStats() {
const { count } = useAnnouncements();
return (
Dismissed
{count.dismissed}
Available
{count.canShow}
);
}
```
---
## Advanced Patterns
### Conditional Rendering Based on Counts
```tsx
function ConditionalUI() {
const { count, announcements } = useAnnouncements();
if (count.visible === 0 && count.canShow > 0) {
return (
announcements.all[0].show()}>
Show Announcements ({count.canShow})
);
}
if (count.dismissed === count.total) {
return You've seen all announcements!
;
}
return ;
}
```
### Build Announcement Manager
```tsx
import { useAnnouncements, useAnnouncement } from '@tour-kit/announcements';
function AnnouncementManager() {
const { announcements, count, resetAll } = useAnnouncements();
return (
{announcements.all.map(a => (
))}
);
}
function AnnouncementCard({ id }: { id: string }) {
const announcement = useAnnouncement(id);
return (
{announcement.config.title}
{announcement.config.description}
Variant: {announcement.config.variant}
Priority: {announcement.config.priority}
Views: {announcement.viewCount}
Show
Hide
announcement.dismiss()} disabled={announcement.isDismissed}>
Dismiss
Reset
{announcement.isDismissed && (
Dismissed
)}
);
}
```
### Group by Priority
```tsx
function PriorityGroupedAnnouncements() {
const { getFiltered } = useAnnouncements();
const critical = getFiltered({ priority: ['critical'] });
const high = getFiltered({ priority: ['high'] });
const normal = getFiltered({ priority: ['normal'] });
const low = getFiltered({ priority: ['low'] });
return (
{critical.length > 0 && (
Critical ({critical.length})
{critical.map(a => )}
)}
{high.length > 0 && (
High Priority ({high.length})
{high.map(a => )}
)}
{normal.length > 0 && (
Normal ({normal.length})
{normal.map(a => )}
)}
{low.length > 0 && (
Low Priority ({low.length})
{low.map(a => )}
)}
);
}
```
---
## TypeScript
The hook is fully typed:
```tsx
import { useAnnouncements } from '@tour-kit/announcements';
import type {
Announcement,
AnnouncementsFilter,
AnnouncementCounts,
} from '@tour-kit/announcements';
function TypedComponent() {
const {
announcements,
activeId,
ids,
count,
getFiltered,
} = useAnnouncements();
// All properties are typed
const all: Announcement[] = announcements.all;
const id: string | null = activeId;
const idList: string[] = ids;
const counts: AnnouncementCounts = count;
// Methods are typed
const filter: AnnouncementsFilter = {
variant: ['modal'],
priority: ['high'],
};
const filtered: Announcement[] = getFiltered(filter);
}
```
---
## Performance Considerations
The hook uses memoization to prevent unnecessary re-renders:
```tsx
// These are memoized and only update when state changes
const { announcements, count, visible, dismissed } = useAnnouncements();
```
---
## Related
- [useAnnouncement](/docs/announcements/hooks/use-announcement) - Control a single announcement
- [useAnnouncementQueue](/docs/announcements/hooks/use-announcement-queue) - Control the queue
- [AnnouncementsProvider](/docs/announcements/providers/announcements-provider) - Provider configuration
---
# useAnnouncementsContext
> Low-level hook returning the raw AnnouncementsProvider context — registered announcements, queue, queue config, and the full action surface
Low-level access to the raw `AnnouncementsContext`. Most consumers should use [`useAnnouncement`](/docs/announcements/hooks/use-announcement) or [`useAnnouncements`](/docs/announcements/hooks/use-announcements) — `useAnnouncementsContext` is the escape hatch when you need everything at once (queue dashboards, debug panels, custom orchestration).
`useAnnouncementsContext` throws if no `` is mounted above.
## Usage
```tsx
import { useAnnouncementsContext } from '@tour-kit/announcements';
function QueueDebugger() {
const { announcements, activeAnnouncement, queue, clearQueue } = useAnnouncementsContext();
return (
Active: {activeAnnouncement ?? 'none'}
Queue length: {queue.length}
Clear queue
);
}
```
## Return Value
| Field | Type | Description |
|-------|------|-------------|
| `announcements` | `Map` | All registered announcements |
| `activeAnnouncement` | `string \| null` | Currently visible announcement id |
| `queue` | `string[]` | Pending announcement ids |
| `queueConfig` | `QueueConfig` | Resolved queue configuration |
| `register` | `(config) => void` | Register a new announcement |
| `unregister` | `(id) => void` | Remove an announcement |
| `show` | `(id) => void` | Show or queue an announcement |
| `hide` | `(id) => void` | Temporarily hide |
| `dismiss` | `(id, reason?) => void` | Persist dismissal |
| `complete` | `(id) => void` | Mark primary action taken |
| `reset` | `(id) => void` | Reset a dismissed announcement |
| `resetAll` | `() => void` | Reset every dismissed announcement |
| `getState` | `(id) => AnnouncementState \| undefined` | Read state |
| `getConfig` | `(id) => AnnouncementConfig \| undefined` | Read config |
| `canShow` | `(id) => boolean` | Frequency/schedule/audience evaluation |
| `showNext` | `() => void` | Pop next from the queue |
| `clearQueue` | `() => void` | Drop all queued announcements |
---
# useFilteredAnnouncements
> Filter a list of AnnouncementConfig by their `audience` prop. Bulk segment evaluation that is safe under dynamic lists.
Filter a list of `AnnouncementConfig` objects by their `audience` prop. Returns a new array containing only the announcements whose audience passes the current segmentation context.
## Usage
```tsx
import { useFilteredAnnouncements } from '@tour-kit/announcements';
function ChangelogList({ announcements }: { announcements: AnnouncementConfig[] }) {
const visible = useFilteredAnnouncements(announcements);
return (
{visible.map((a) => (
{a.title}
))}
);
}
```
## Signature
```ts
function useFilteredAnnouncements(
announcements: AnnouncementConfig[]
): AnnouncementConfig[]
```
## Audience evaluation rules
| Audience shape | Behavior |
|----------------|----------|
| `undefined` | Let through (always visible). |
| `AudienceCondition[]` | Let through — forwarded to the scheduler, which re-checks against the provider's `userContext` prop. |
| `{ segment: string }` | Evaluated here via [`useSegments`](/docs/core/hooks/use-segments). Filtered out if the segment is `false`. |
## Why bulk?
The hook reads all segments once at the top via `useSegments()` and reuses that map inside the filter callback. This is safe under dynamic announcement lists — never call `useSegment` (the single-segment hook) inside a `.filter()` body; that violates the rules of hooks.
## Related
- [evaluateAnnouncementAudience](/docs/announcements/types) — underlying single-item evaluator
- [Audience targeting](/docs/announcements/configuration) — audience prop reference
- [Segmentation guide](/docs/guides/segmentation)
---
# useResolvedText
> Re-export of @tour-kit/core's useResolvedText for ergonomic in-package access. Resolves LocalizedText | ReactNode into a ReactNode.
Re-exported from [`@tour-kit/core`](/docs/core/hooks/use-resolved-text) so consumers using `@tour-kit/announcements` can import it from the same package as the rest of the API.
```tsx
import { useResolvedText } from '@tour-kit/announcements';
function AnnouncementBody({ body }: { body: LocalizedText | React.ReactNode }) {
const resolved = useResolvedText(body);
return {resolved}
;
}
```
---
# AnnouncementsProvider
> AnnouncementsProvider: configure announcement storage, queue behavior, frequency rules, and audience targeting at app level
The root provider that manages announcement state, queue, persistence, and configuration. All announcement components and hooks must be used within this provider.
## Why Use AnnouncementsProvider?
The provider centralizes announcement management:
- **State management** - Tracks views, dismissals, and active announcements
- **Queue management** - Automatically orders by priority and shows next in queue
- **Persistence** - Saves state to localStorage/sessionStorage
- **Audience targeting** - Filters announcements based on user context
- **Lifecycle callbacks** - Global event handlers for all announcements
---
## Basic Usage
```tsx
import { AnnouncementsProvider, AnnouncementModal } from '@tour-kit/announcements';
function App() {
return (
);
}
```
---
## Props
',
description: 'User data for audience targeting',
},
onShow: {
type: '(id: string) => void',
description: 'Called when any announcement is shown',
},
onDismiss: {
type: '(id: string, reason: DismissalReason) => void',
description: 'Called when any announcement is dismissed',
},
onComplete: {
type: '(id: string) => void',
description: 'Called when any announcement is completed',
},
}}
/>
---
## Announcement Configuration
Each announcement in the `announcements` array requires:
```tsx
const announcements: AnnouncementConfig[] = [
{
id: 'unique-id', // Required: unique identifier
variant: 'modal', // Required: 'modal' | 'slideout' | 'banner' | 'toast' | 'spotlight'
priority: 'high', // Optional: 'low' | 'normal' | 'high' | 'critical'
title: 'Announcement Title',
description: 'Announcement description',
// Actions
primaryAction: {
label: 'Get Started',
onClick: () => console.log('Primary action'),
},
secondaryAction: {
label: 'Learn More',
onClick: () => console.log('Secondary action'),
},
// Display rules
frequency: 'once', // 'once' | 'session' | 'always' | object
audience: [], // Targeting rules
schedule: {}, // From @tour-kit/scheduling
// Variant-specific options
modalOptions: { size: 'md' },
slideoutOptions: { position: 'right' },
bannerOptions: { sticky: true },
toastOptions: { autoDismiss: true },
spotlightOptions: { targetSelector: '#element' },
// Callbacks
onShow: () => console.log('Shown'),
onDismiss: (reason) => console.log('Dismissed', reason),
onComplete: () => console.log('Completed'),
// Metadata
metadata: { campaign: 'spring-2024' },
},
];
```
---
## Queue Configuration
Control how announcements are queued and displayed:
```tsx
```
### QueueConfig Options
---
## User Context for Targeting
Provide user data to enable audience targeting:
```tsx
```
See [Audience Targeting](/docs/announcements/configuration/audience) for detailed targeting rules.
---
## Storage Options
### localStorage (Default)
Persist across browser sessions:
```tsx
```
### sessionStorage
Persist only for current session:
```tsx
```
### Memory
No persistence (resets on page reload):
```tsx
```
---
## Global Callbacks
Track announcement events across your app:
```tsx
{
console.log(`Announcement shown: ${id}`);
analytics.track('announcement_shown', { announcementId: id });
}}
onDismiss={(id, reason) => {
console.log(`Announcement dismissed: ${id}, reason: ${reason}`);
analytics.track('announcement_dismissed', {
announcementId: id,
dismissalReason: reason,
});
}}
onComplete={(id) => {
console.log(`Announcement completed: ${id}`);
analytics.track('announcement_completed', { announcementId: id });
}}
/>
```
### Dismissal Reasons
The `onDismiss` callback receives one of these reasons:
- `'close_button'` - User clicked close button
- `'overlay_click'` - User clicked overlay (modals/slideouts)
- `'escape_key'` - User pressed Escape key
- `'primary_action'` - User clicked primary action button
- `'secondary_action'` - User clicked secondary action button
- `'auto_dismiss'` - Toast auto-dismissed after delay
- `'programmatic'` - Dismissed via hook/API call
---
## Complete Example
```tsx
import {
AnnouncementsProvider,
AnnouncementModal,
AnnouncementBanner,
AnnouncementToast,
} from '@tour-kit/announcements';
const announcements = [
{
id: 'critical-update',
variant: 'modal',
priority: 'critical',
title: 'Security Update Required',
description: 'Please update your password immediately.',
frequency: 'always',
primaryAction: {
label: 'Update Now',
onClick: () => router.push('/settings/security'),
},
modalOptions: {
size: 'md',
closeOnOverlayClick: false,
closeOnEscape: false,
},
},
{
id: 'new-export',
variant: 'modal',
priority: 'high',
title: 'New Export Feature',
description: 'Export your data to CSV, PDF, or Excel.',
frequency: 'once',
audience: [
{ field: 'plan', operator: 'in', value: ['pro', 'enterprise'] },
],
media: {
type: 'image',
src: '/images/export-feature.png',
alt: 'Export feature preview',
},
primaryAction: {
label: 'Try It Now',
onClick: () => router.push('/export'),
},
secondaryAction: {
label: 'Learn More',
onClick: () => window.open('/docs/export', '_blank'),
},
},
{
id: 'maintenance',
variant: 'banner',
priority: 'normal',
title: 'Scheduled maintenance tonight at 2 AM EST',
frequency: 'session',
bannerOptions: {
position: 'top',
sticky: true,
intent: 'warning',
},
},
{
id: 'tip-of-day',
variant: 'toast',
priority: 'low',
title: 'Pro Tip: Use keyboard shortcuts!',
frequency: { type: 'interval', days: 7 },
toastOptions: {
position: 'bottom-right',
autoDismiss: true,
autoDismissDelay: 5000,
},
},
];
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
// Fetch user data
fetchUser().then(setUser);
}, []);
if (!user) return ;
return (
{
console.log('Shown:', id);
analytics.track('announcement_shown', { id });
}}
onDismiss={(id, reason) => {
console.log('Dismissed:', id, reason);
analytics.track('announcement_dismissed', { id, reason });
}}
onComplete={(id) => {
console.log('Completed:', id);
analytics.track('announcement_completed', { id });
}}
>
{/* Render announcement UI components */}
{/* Your app content */}
);
}
```
---
## Dynamic Announcements
Add or update announcements after initial render:
```tsx
function DynamicAnnouncementsExample() {
const [announcements, setAnnouncements] = useState([]);
useEffect(() => {
// Fetch announcements from API
fetch('/api/announcements')
.then(res => res.json())
.then(data => setAnnouncements(data));
}, []);
return (
);
}
```
---
## Nested Providers
You can nest providers for different sections of your app:
```tsx
function App() {
return (
);
}
```
---
## TypeScript
The provider is fully typed:
```tsx
import type {
AnnouncementConfig,
QueueConfig,
DismissalReason,
} from '@tour-kit/announcements';
const announcements: AnnouncementConfig[] = [
{
id: 'welcome',
variant: 'modal',
title: 'Welcome!',
},
];
const queueConfig: QueueConfig = {
maxConcurrent: 1,
autoShow: true,
};
{
console.log(id, reason);
}}
>
```
---
## Related
- [useAnnouncements](/docs/announcements/hooks/use-announcements) - Access provider state
- [Queue Configuration](/docs/announcements/configuration/queue) - Detailed queue config
- [Audience Targeting](/docs/announcements/configuration/audience) - Targeting rules
- [Frequency Rules](/docs/announcements/configuration/frequency) - Display frequency
---
# Type Reference
> TypeScript types for Announcement, AnnouncementVariant, QueueConfig, AudienceRule, and the full announcements API surface
Complete TypeScript type definitions for the announcements package.
---
## Core Types
### AnnouncementConfig
Main configuration object for an announcement:
```tsx
interface AnnouncementConfig {
// Required
id: string;
variant: AnnouncementVariant;
// Content
title?: string;
description?: string | ReactNode;
media?: AnnouncementMedia;
// Actions
primaryAction?: AnnouncementAction;
secondaryAction?: AnnouncementAction;
// Behavior
priority?: AnnouncementPriority;
frequency?: FrequencyRule;
audience?: AudienceCondition[];
// Variant Options
modalOptions?: ModalOptions;
slideoutOptions?: SlideoutOptions;
bannerOptions?: BannerOptions;
toastOptions?: ToastOptions;
spotlightOptions?: SpotlightOptions;
// Callbacks
onShow?: () => void;
onDismiss?: (reason: DismissalReason) => void;
onComplete?: () => void;
// Custom data
metadata?: TMetadata;
}
```
### AnnouncementVariant
```tsx
type AnnouncementVariant = 'modal' | 'slideout' | 'banner' | 'toast' | 'spotlight';
```
### AnnouncementPriority
```tsx
type AnnouncementPriority = 'low' | 'normal' | 'high' | 'critical';
```
---
## Content Types
### AnnouncementMedia
```tsx
interface AnnouncementMedia {
type: 'image' | 'video' | 'custom';
src?: string;
alt?: string;
poster?: string; // For videos
autoPlay?: boolean; // For videos
component?: ReactNode; // For custom
}
```
**Examples:**
```tsx
// Image
media: {
type: 'image',
src: '/feature.png',
alt: 'New feature screenshot',
}
// Video
media: {
type: 'video',
src: '/demo.mp4',
poster: '/demo-poster.jpg',
autoPlay: true,
}
// Custom component
media: {
type: 'custom',
component: ,
}
```
### AnnouncementAction
```tsx
interface AnnouncementAction {
label: string;
onClick: () => void;
href?: string; // Optional link
variant?: 'primary' | 'secondary' | 'ghost';
disabled?: boolean;
}
```
**Example:**
```tsx
primaryAction: {
label: 'Try It Now',
onClick: () => router.push('/feature'),
variant: 'primary',
}
```
---
## Frequency Types
### FrequencyRule
```tsx
type FrequencyRule =
| 'once' // Show once ever
| 'session' // Once per session
| 'always' // Every time
| { type: 'times'; count: number } // N times total
| { type: 'interval'; days: number }; // Every N days
```
**Examples:**
```tsx
frequency: 'once'
frequency: 'session'
frequency: { type: 'times', count: 3 }
frequency: { type: 'interval', days: 7 }
```
---
## Audience Types
### AudienceCondition
```tsx
type AudienceCondition =
| FieldCondition
| CustomCondition;
```
### FieldCondition
```tsx
interface FieldCondition {
field: keyof T;
operator: ComparisonOperator;
value: any;
}
type ComparisonOperator =
| 'equals'
| 'notEquals'
| 'in'
| 'notIn'
| 'greaterThan'
| 'lessThan'
| 'contains'
| 'notContains';
```
**Examples:**
```tsx
{ field: 'plan', operator: 'equals', value: 'pro' }
{ field: 'role', operator: 'in', value: ['admin', 'owner'] }
{ field: 'credits', operator: 'greaterThan', value: 100 }
```
### CustomCondition
```tsx
interface CustomCondition {
type: 'custom';
check: (context: T) => boolean;
}
```
**Example:**
```tsx
{
type: 'custom',
check: (user) => user.isActive && user.hasFeature('analytics'),
}
```
---
## State Types
### AnnouncementState
Internal state for each announcement:
```tsx
interface AnnouncementState {
id: string;
isVisible: boolean;
isActive: boolean;
isDismissed: boolean;
viewCount: number;
lastViewedAt: Date | null;
dismissedAt: Date | null;
dismissalReason: DismissalReason | null;
}
```
### DismissalReason
```tsx
type DismissalReason =
| 'close_button' // User clicked close button
| 'overlay_click' // User clicked overlay
| 'escape_key' // User pressed Escape
| 'primary_action' // User clicked primary action
| 'secondary_action' // User clicked secondary action
| 'auto_dismiss' // Auto-dismissed (toasts)
| 'programmatic'; // Dismissed via code
```
---
## Variant Options
### ModalOptions
```tsx
interface ModalOptions {
size?: 'sm' | 'md' | 'lg' | 'xl';
closeOnOverlay?: boolean;
closeOnEscape?: boolean;
showCloseButton?: boolean;
centered?: boolean;
}
```
**Defaults:**
```tsx
{
size: 'md',
closeOnOverlay: true,
closeOnEscape: true,
showCloseButton: true,
centered: true,
}
```
### SlideoutOptions
```tsx
interface SlideoutOptions {
position?: 'left' | 'right';
size?: 'sm' | 'md' | 'lg';
width?: number;
closeOnOverlay?: boolean;
closeOnEscape?: boolean;
showCloseButton?: boolean;
}
```
**Defaults:**
```tsx
{
position: 'right',
size: 'md',
closeOnOverlay: true,
closeOnEscape: true,
showCloseButton: true,
}
```
### BannerOptions
```tsx
interface BannerOptions {
position?: 'top' | 'bottom';
sticky?: boolean;
dismissible?: boolean;
intent?: 'info' | 'success' | 'warning' | 'error';
}
```
**Defaults:**
```tsx
{
position: 'top',
sticky: false,
dismissible: true,
intent: 'info',
}
```
### ToastOptions
```tsx
interface ToastOptions {
position?: ToastPosition;
autoDismiss?: boolean;
autoDismissDelay?: number; // milliseconds
showCloseButton?: boolean;
intent?: 'info' | 'success' | 'warning' | 'error';
}
type ToastPosition =
| 'top-left'
| 'top-center'
| 'top-right'
| 'bottom-left'
| 'bottom-center'
| 'bottom-right';
```
**Defaults:**
```tsx
{
position: 'bottom-right',
autoDismiss: true,
autoDismissDelay: 5000,
showCloseButton: true,
intent: 'info',
}
```
### SpotlightOptions
```tsx
interface SpotlightOptions {
target: string; // CSS selector
targetElement?: HTMLElement; // Or direct element ref
placement?: Placement;
offset?: number;
padding?: number;
borderRadius?: number;
closeOnClickOutside?: boolean;
closeOnEscape?: boolean;
showCloseButton?: boolean;
}
type Placement =
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'left-start'
| 'left-end'
| 'right'
| 'right-start'
| 'right-end';
```
**Defaults:**
```tsx
{
placement: 'bottom',
offset: 8,
padding: 8,
borderRadius: 4,
closeOnClickOutside: true,
closeOnEscape: true,
showCloseButton: true,
}
```
---
## Provider Types
### AnnouncementsProviderProps
```tsx
interface AnnouncementsProviderProps {
// Required
announcements: AnnouncementConfig[];
children: ReactNode;
// Optional
userContext?: TUserContext;
queueConfig?: QueueConfig;
storage?: StorageType;
storageKey?: string;
// Callbacks
onShow?: (id: string) => void;
onDismiss?: (id: string, reason: DismissalReason) => void;
onComplete?: (id: string) => void;
onQueueChange?: (queue: AnnouncementConfig[]) => void;
// Debug
debug?: boolean;
}
```
### QueueConfig
```tsx
interface QueueConfig {
maxConcurrent?: number; // Max announcements shown at once
autoShow?: boolean; // Auto-show next in queue
delayBetween?: number; // Delay between announcements (ms)
respectVariants?: boolean; // Allow one per variant
}
```
**Defaults:**
```tsx
{
maxConcurrent: 1,
autoShow: true,
delayBetween: 0,
respectVariants: false,
}
```
### StorageType
```tsx
type StorageType = 'localStorage' | 'sessionStorage' | 'memory';
```
---
## Hook Return Types
### UseAnnouncementReturn
```tsx
interface UseAnnouncementReturn {
// State
state: AnnouncementState;
config: AnnouncementConfig;
isVisible: boolean;
isActive: boolean;
isDismissed: boolean;
canShow: boolean;
viewCount: number;
// Actions
show: () => void;
hide: () => void;
dismiss: (reason?: DismissalReason) => void;
complete: () => void;
reset: () => void;
}
```
### UseAnnouncementsReturn
```tsx
interface UseAnnouncementsReturn {
// All announcements
announcements: {
all: AnnouncementState[];
visible: AnnouncementState[];
queued: AnnouncementState[];
dismissed: AnnouncementState[];
};
// Actions
showNext: () => void;
hideAll: () => void;
resetAll: () => void;
}
```
### UseAnnouncementQueueReturn
```tsx
interface UseAnnouncementQueueReturn {
// Queue state
queue: AnnouncementConfig[];
queueSize: number;
activeCount: number;
// Actions
showNext: () => void;
clearQueue: () => void;
}
```
---
## Component Props
### AnnouncementModal Props
```tsx
interface AnnouncementModalProps {
id?: string; // Announcement ID
config?: AnnouncementConfig; // Or inline config
onDismiss?: (reason: DismissalReason) => void;
}
```
### AnnouncementSlideout Props
```tsx
interface AnnouncementSlideoutProps {
id?: string;
config?: AnnouncementConfig;
onDismiss?: (reason: DismissalReason) => void;
}
```
### AnnouncementBanner Props
```tsx
interface AnnouncementBannerProps {
id?: string;
config?: AnnouncementConfig;
onDismiss?: (reason: DismissalReason) => void;
}
```
### AnnouncementToast Props
```tsx
interface AnnouncementToastProps {
id?: string;
config?: AnnouncementConfig;
onDismiss?: (reason: DismissalReason) => void;
}
```
### AnnouncementSpotlight Props
```tsx
interface AnnouncementSpotlightProps {
id?: string;
config?: AnnouncementConfig;
onDismiss?: (reason: DismissalReason) => void;
}
```
---
## Headless Component Props
### HeadlessModal Props
```tsx
interface HeadlessModalProps {
id?: string;
config?: AnnouncementConfig;
children: (props: HeadlessModalRenderProps) => ReactNode;
}
interface HeadlessModalRenderProps {
state: AnnouncementState;
config: AnnouncementConfig;
isOpen: boolean;
dismiss: (reason: DismissalReason) => void;
complete: () => void;
}
```
### HeadlessSlideout Props
```tsx
interface HeadlessSlideoutProps {
id?: string;
config?: AnnouncementConfig;
children: (props: HeadlessSlideoutRenderProps) => ReactNode;
}
interface HeadlessSlideoutRenderProps {
state: AnnouncementState;
config: AnnouncementConfig;
isOpen: boolean;
dismiss: (reason: DismissalReason) => void;
complete: () => void;
}
```
### HeadlessBanner Props
```tsx
interface HeadlessBannerProps {
id?: string;
config?: AnnouncementConfig;
children: (props: HeadlessBannerRenderProps) => ReactNode;
}
interface HeadlessBannerRenderProps {
state: AnnouncementState;
config: AnnouncementConfig;
isVisible: boolean;
dismiss: (reason: DismissalReason) => void;
complete: () => void;
}
```
### HeadlessToast Props
```tsx
interface HeadlessToastProps {
id?: string;
config?: AnnouncementConfig;
children: (props: HeadlessToastRenderProps) => ReactNode;
}
interface HeadlessToastRenderProps {
state: AnnouncementState;
config: AnnouncementConfig;
isVisible: boolean;
dismiss: (reason: DismissalReason) => void;
complete: () => void;
progress: number; // 0-100 for auto-dismiss
}
```
### HeadlessSpotlight Props
```tsx
interface HeadlessSpotlightProps {
id?: string;
config?: AnnouncementConfig;
children: (props: HeadlessSpotlightRenderProps) => ReactNode;
}
interface HeadlessSpotlightRenderProps {
state: AnnouncementState;
config: AnnouncementConfig;
isVisible: boolean;
targetRect: DOMRect | null;
dismiss: (reason: DismissalReason) => void;
complete: () => void;
}
```
---
## Utility Types
### Generic Metadata
Extend announcement configs with custom metadata:
```tsx
interface CustomMetadata {
campaignId: string;
source: 'email' | 'in-app' | 'push';
experimentId?: string;
}
const config: AnnouncementConfig = {
id: 'promo',
variant: 'modal',
title: 'Special Offer',
metadata: {
campaignId: 'summer-2024',
source: 'in-app',
},
};
```
### Generic User Context
Type-safe user context:
```tsx
interface UserContext {
plan: 'free' | 'pro' | 'enterprise';
role: 'user' | 'admin';
features: string[];
}
userContext={{
plan: 'pro',
role: 'user',
features: ['analytics'],
}}
announcements={announcements}
/>
```
---
## Import Paths
```tsx
// Types
import type {
AnnouncementConfig,
AnnouncementVariant,
AnnouncementPriority,
FrequencyRule,
AudienceCondition,
DismissalReason,
QueueConfig,
UseAnnouncementReturn,
UseAnnouncementsReturn,
UseAnnouncementQueueReturn,
} from '@tour-kit/announcements';
// Components
import {
AnnouncementsProvider,
AnnouncementModal,
AnnouncementSlideout,
AnnouncementBanner,
AnnouncementToast,
AnnouncementSpotlight,
} from '@tour-kit/announcements';
// Headless
import {
HeadlessModal,
HeadlessSlideout,
HeadlessBanner,
HeadlessToast,
HeadlessSpotlight,
} from '@tour-kit/announcements/headless';
// Hooks
import {
useAnnouncement,
useAnnouncements,
useAnnouncementQueue,
} from '@tour-kit/announcements';
```
---
## Component Composition Props
Compound parts used to build the styled variants. Most consumers use the variant components directly (Modal, Slideout, Banner, Toast, Spotlight); these props matter when you compose your own layout.
### AnnouncementOverlayProps
```tsx
interface AnnouncementOverlayProps
extends React.HTMLAttributes,
VariantProps {
/** Whether the overlay is visible */
open?: boolean
/** Callback when overlay is clicked */
onClose?: () => void
}
```
### AnnouncementContentProps
```tsx
interface AnnouncementContentProps extends React.HTMLAttributes {
title?: string
description?: React.ReactNode
media?: AnnouncementMedia
titleProps?: React.HTMLAttributes
descriptionProps?: React.HTMLAttributes
}
```
### AnnouncementActionsProps
```tsx
interface AnnouncementActionsProps extends React.HTMLAttributes {
primaryAction?: AnnouncementAction
secondaryAction?: AnnouncementAction
onAction?: (type: 'primary' | 'secondary') => void
/** Fired when action.dismissOnClick is true */
onDismiss?: () => void
direction?: 'horizontal' | 'vertical'
}
```
### AnnouncementCloseProps
```tsx
interface AnnouncementCloseProps
extends Omit, 'children'> {
asChild?: boolean
children?: React.ReactNode | RenderProp
onClose?: () => void
}
```
## Context Types
### AnnouncementsContext
Raw React context. Prefer [`useAnnouncementsContext`](/docs/announcements/hooks/use-announcements-context) — it throws when used outside ``.
```tsx
declare const AnnouncementsContext: React.Context
```
### AnnouncementsContextValue
```tsx
interface AnnouncementsContextValue {
announcements: Map
activeAnnouncement: string | null
queue: string[]
queueConfig: QueueConfig
register: (config: AnnouncementConfig) => void
unregister: (id: string) => void
show: (id: string) => void
hide: (id: string) => void
dismiss: (id: string, reason?: DismissalReason) => void
complete: (id: string) => void
reset: (id: string) => void
resetAll: () => void
getState: (id: string) => AnnouncementState | undefined
getConfig: (id: string) => AnnouncementConfig | undefined
canShow: (id: string) => boolean
showNext: () => void
clearQueue: () => void
}
```
## Queue Types
### QueueItem
```tsx
interface QueueItem {
id: string
priority: AnnouncementPriority
addedAt: number
weight: number
/** Monotonic sequence number for ordering items with same timestamp */
sequence: number
}
```
### DEFAULT_QUEUE_CONFIG
```tsx
declare const DEFAULT_QUEUE_CONFIG: QueueConfig
```
The default queue configuration the provider falls back to when no `queueConfig` is supplied. Use as a base when building a partial override.
### AnnouncementScheduler
Class that manages the queue and gating decisions internally. Exported for advanced use (custom orchestrators, test fixtures); typical apps never construct it directly.
```tsx
declare class AnnouncementScheduler {
constructor(config: QueueConfig)
canShow(config, state, userContext?): boolean
shouldQueue(config, state, userContext?): boolean
enqueue(config: AnnouncementConfig): number
getNext(): string | undefined
peekNext(): string | undefined
remove(id: string): boolean
isQueued(id: string): boolean
getQueuePosition(id: string): number
getQueuedIds(): string[]
get queueSize(): number
canShowMore(): boolean
markActive(): void
markInactive(): void
get currentActiveCount(): number
clearQueue(): void
resetActive(): void
updateConfig(config: QueueConfig): void
get delayBetween(): number
get autoShow(): boolean
}
```
## Storage
### AnnouncementStorageAdapter
Pluggable persistence layer. Default storage uses `localStorage`; pass an adapter on the provider to swap implementations (memory, cookies, SQLite, ...).
```tsx
interface AnnouncementStorageAdapter {
getItem(key: string): string | null | Promise
setItem(key: string, value: string): void | Promise
removeItem(key: string): void | Promise
}
```
## Analytics Event Types
The provider's `onEvent` callback receives an `AnnouncementEvent`, which is a discriminated union of seven concrete shapes.
### AnnouncementEventType
```tsx
type AnnouncementEventType =
| 'announcement_registered'
| 'announcement_shown'
| 'announcement_dismissed'
| 'announcement_completed'
| 'announcement_action_clicked'
| 'announcement_queued'
| 'announcement_dequeued'
```
### BaseAnnouncementEvent
```tsx
interface BaseAnnouncementEvent {
type: AnnouncementEventType
announcementId: string
variant: AnnouncementVariant
timestamp: number
metadata?: Record
}
```
### Concrete events
```tsx
interface AnnouncementRegisteredEvent extends BaseAnnouncementEvent {
type: 'announcement_registered'
}
interface AnnouncementShownEvent extends BaseAnnouncementEvent {
type: 'announcement_shown'
viewCount: number
fromQueue: boolean
}
interface AnnouncementDismissedEvent extends BaseAnnouncementEvent {
type: 'announcement_dismissed'
reason: DismissalReason
viewDuration: number
}
interface AnnouncementCompletedEvent extends BaseAnnouncementEvent {
type: 'announcement_completed'
viewDuration: number
}
interface AnnouncementActionClickedEvent extends BaseAnnouncementEvent {
type: 'announcement_action_clicked'
actionType: 'primary' | 'secondary'
actionLabel: string
}
interface AnnouncementQueuedEvent extends BaseAnnouncementEvent {
type: 'announcement_queued'
queuePosition: number
}
interface AnnouncementDequeuedEvent extends BaseAnnouncementEvent {
type: 'announcement_dequeued'
reason: 'shown' | 'expired' | 'removed'
}
type AnnouncementEvent =
| AnnouncementRegisteredEvent
| AnnouncementShownEvent
| AnnouncementDismissedEvent
| AnnouncementCompletedEvent
| AnnouncementActionClickedEvent
| AnnouncementQueuedEvent
| AnnouncementDequeuedEvent
```
Discriminate on `event.type` — TypeScript narrows the union to the per-event payload.
## Cross-Cutting
| Type | Notes |
|------|-------|
| `UILibrary` | `'radix-ui' \| 'base-ui'` — see [Unified Slot](/docs/guides/unified-slot) |
| `RenderProp` | Generic render-prop callback shape — see [Unified Slot](/docs/guides/unified-slot#renderprop) |
## Related
- [AnnouncementsProvider](/docs/announcements/providers/announcements-provider) - Provider configuration
- [Components](/docs/announcements/components) - Styled components
- [Hooks](/docs/announcements/hooks/use-announcement) - Hook APIs
- [Headless](/docs/announcements/headless) - Headless components
- [useAnnouncementsContext](/docs/announcements/hooks/use-announcements-context) - Raw context hook
---
# API Reference
> Complete API reference for all userTourKit packages — hooks, components, providers, utilities, and TypeScript type exports
Complete API documentation for all userTourKit packages. Each page contains exports, types, and usage examples.
---
## Core Packages
---
## Extended Packages
---
## Quick Reference
### Most Used Exports
| Export | Package | Description |
|--------|---------|-------------|
| `useTour` | core/react | Main tour control hook |
| `Tour` | react | Declarative tour wrapper |
| `TourStep` | react | Step definition component |
| `TourCard` | react | Tooltip card component |
| `TourOverlay` | react | Spotlight overlay |
| `Hint` | hints | Hint with hotspot and tooltip |
| `useHint` | hints | Single hint control |
| `usePersistence` | core/react | State persistence |
| `useFeature` | adoption | Feature adoption tracking |
| `useAnalytics` | analytics | Analytics tracking |
| `useAnnouncement` | announcements | Announcement control |
| `useChecklist` | checklists | Checklist progress |
| `TourMedia` | media | Embedded media component |
| `useSchedule` | scheduling | Time-based scheduling |
### Package Sizes
| Package | Gzipped |
|---------|---------|
| @tour-kit/core | < 8KB |
| @tour-kit/react | < 12KB |
| @tour-kit/hints | < 5KB |
| @tour-kit/adoption | < 10KB |
| @tour-kit/analytics | < 4KB |
| @tour-kit/announcements | < 15KB |
| @tour-kit/checklists | < 12KB |
| @tour-kit/media | < 8KB |
| @tour-kit/scheduling | < 3KB |
---
## Import Patterns
### React App (Recommended)
```tsx
// Import everything from @tour-kit/react
import {
Tour,
TourStep,
TourCard,
TourOverlay,
useTour,
usePersistence,
} from '@tour-kit/react';
// Headless components
import { TourCardHeadless } from '@tour-kit/react/headless';
```
### With Hints
```tsx
import { Tour, TourStep, TourCard } from '@tour-kit/react';
import { HintsProvider, Hint, useHint } from '@tour-kit/hints';
```
### With Analytics
```tsx
import { AnalyticsProvider, createAnalytics, consolePlugin } from '@tour-kit/analytics';
const analytics = createAnalytics({
plugins: [consolePlugin()],
});
```
### With Adoption Tracking
```tsx
import { AdoptionProvider, useFeature, AdoptionNudge } from '@tour-kit/adoption';
```
### With Announcements
```tsx
import { AnnouncementsProvider, useAnnouncement, AnnouncementModal } from '@tour-kit/announcements';
```
### With Checklists
```tsx
import { ChecklistProvider, useChecklist, ChecklistPanel } from '@tour-kit/checklists';
```
### With Media
```tsx
import { TourMedia, YouTubeEmbed } from '@tour-kit/media';
```
### With Scheduling
```tsx
import { useSchedule, checkSchedule } from '@tour-kit/scheduling';
```
### Core Only (Framework-agnostic)
```tsx
import {
useTour,
usePersistence,
createTour,
createStep,
} from '@tour-kit/core';
```
---
## Type Exports
All packages export TypeScript types:
```tsx
import type {
// Config
TourKitConfig,
KeyboardConfig,
SpotlightConfig,
PersistenceConfig,
// Tour
Tour,
TourStep,
TourState,
Placement,
// Router
RouterAdapter,
MultiPagePersistenceConfig,
// Hook returns
UseTourReturn,
UsePersistenceReturn,
} from '@tour-kit/react';
import type {
HintConfig,
HintState,
HotspotPosition,
} from '@tour-kit/hints';
import type {
Feature,
AdoptionCriteria,
FeatureUsage,
AdoptionStatus,
} from '@tour-kit/adoption';
import type {
TourEvent,
TourEventName,
AnalyticsPlugin,
} from '@tour-kit/analytics';
import type {
AnnouncementConfig,
FrequencyRule,
AudienceCondition,
} from '@tour-kit/announcements';
import type {
ChecklistConfig,
ChecklistTaskConfig,
TaskAction,
} from '@tour-kit/checklists';
import type {
MediaType,
TourMediaConfig,
CaptionTrack,
} from '@tour-kit/media';
import type {
Schedule,
RecurringPattern,
BusinessHours,
} from '@tour-kit/scheduling';
```
---
## See also
Each package's prose-style docs section sits next to its API reference:
- Core: [`@tour-kit/core` overview](/docs/core) ↔ [`/docs/api/core`](/docs/api/core)
- React: [`@tour-kit/react` overview](/docs/react) ↔ [`/docs/api/react`](/docs/api/react)
- Hints: [`@tour-kit/hints` overview](/docs/hints) ↔ [`/docs/api/hints`](/docs/api/hints)
- Adoption: [`@tour-kit/adoption` overview](/docs/adoption) ↔ [`/docs/api/adoption`](/docs/api/adoption)
- Analytics: [`@tour-kit/analytics` overview](/docs/analytics) ↔ [`/docs/api/analytics`](/docs/api/analytics)
- Announcements: [`@tour-kit/announcements` overview](/docs/announcements) ↔ [`/docs/api/announcements`](/docs/api/announcements)
- Checklists: [`@tour-kit/checklists` overview](/docs/checklists) ↔ [`/docs/api/checklists`](/docs/api/checklists)
- Media: [`@tour-kit/media` overview](/docs/media) ↔ [`/docs/api/media`](/docs/api/media)
- Scheduling: [`@tour-kit/scheduling` overview](/docs/scheduling) ↔ [`/docs/api/scheduling`](/docs/api/scheduling)
- Surveys: [`@tour-kit/surveys` overview](/docs/surveys)
- AI: [`@tour-kit/ai` overview](/docs/ai) ↔ [`/docs/api/ai`](/docs/api/ai)
- Licensing: [Licensing](/docs/licensing) ↔ [`/docs/api/license`](/docs/api/license)
---
# @tour-kit/adoption API
> API reference for @tour-kit/adoption: AdoptionProvider, useFeature, useNudge, useAdoptionStats, and dashboard exports
Complete API reference for the adoption package. This package provides feature adoption tracking with automatic nudging.
For prose-style guides and recipes, see the [`@tour-kit/adoption` package overview](/docs/adoption), or jump to a specific [component](/docs/adoption/components), [hook](/docs/adoption/hooks), [provider](/docs/adoption/providers), or [dashboard widget](/docs/adoption/dashboard).
---
## Providers
### [AdoptionProvider](/docs/adoption/providers/adoption-provider)
Provider for feature adoption tracking.
```tsx
import { AdoptionProvider } from '@tour-kit/adoption';
console.log('Adopted:', feature.name)}
onChurn={(feature) => console.log('Churned:', feature.name)}
>
{children}
```
| Prop | Type | Description |
|------|------|-------------|
| `features` | `Feature[]` | Array of feature definitions |
| `storage` | `StorageConfig` | Storage configuration |
| `nudge` | `NudgeConfig` | Nudge configuration |
| `userId` | `string` | User identifier for multi-user support |
| `onAdoption` | `(feature: Feature) => void` | Called when feature is adopted |
| `onChurn` | `(feature: Feature) => void` | Called when feature adoption is lost |
| `onNudge` | `(feature: Feature, action: string) => void` | Called on nudge interaction |
---
## Hooks
### [useFeature](/docs/adoption/hooks/use-feature)
Hook for tracking single feature adoption.
```tsx
import { useFeature } from '@tour-kit/adoption';
const {
feature,
usage,
isAdopted,
status,
useCount,
trackUsage,
} = useFeature('feature-id');
```
| Return | Type | Description |
|--------|------|-------------|
| `feature` | `Feature \| undefined` | Feature configuration |
| `usage` | `FeatureUsage \| undefined` | Usage tracking data |
| `isAdopted` | `boolean` | Whether feature is adopted |
| `status` | `AdoptionStatus` | Current adoption status |
| `useCount` | `number` | Number of times feature was used |
| `trackUsage` | `() => void` | Manually track a usage |
### [useAdoptionStats](/docs/adoption/hooks/use-adoption-stats)
Hook for aggregate adoption statistics.
```tsx
import { useAdoptionStats } from '@tour-kit/adoption';
const {
totalFeatures,
adoptedCount,
adoptionRate,
byCategory,
byStatus,
} = useAdoptionStats();
```
| Return | Type | Description |
|--------|------|-------------|
| `totalFeatures` | `number` | Total number of features |
| `adoptedCount` | `number` | Number of adopted features |
| `adoptionRate` | `number` | Adoption rate (0-100) |
| `byCategory` | `Record` | Stats grouped by category |
| `byStatus` | `Record` | Counts by status |
### [useNudge](/docs/adoption/hooks/use-nudge)
Hook for controlling nudge queue.
```tsx
import { useNudge } from '@tour-kit/adoption';
const {
currentNudge,
queue,
show,
dismiss,
skip,
} = useNudge();
```
| Return | Type | Description |
|--------|------|-------------|
| `currentNudge` | `Feature \| null` | Currently shown nudge |
| `queue` | `Feature[]` | Queued nudges |
| `show` | `(featureId: string) => void` | Show nudge for feature |
| `dismiss` | `() => void` | Dismiss current nudge |
| `skip` | `() => void` | Skip current nudge |
### [useAdoptionAnalytics](/docs/adoption/analytics)
Hook for analytics integration.
```tsx
import { useAdoptionAnalytics } from '@tour-kit/adoption';
const analytics = useAdoptionAnalytics();
```
---
## Components
### [AdoptionNudge](/docs/adoption/components/adoption-nudge)
Auto-shows nudges for unadopted features.
```tsx
import { AdoptionNudge } from '@tour-kit/adoption';
}
/>
```
| Prop | Type | Description |
|------|------|-------------|
| `position` | `string` | Position of nudge |
| `delay` | `number` | Delay before showing (ms) |
| `size` | `'sm' \| 'default' \| 'lg'` | Size variant |
| `render` | `(props) => ReactNode` | Custom render function |
| `asChild` | `boolean` | Merge props to child element |
### [FeatureButton](/docs/adoption/components/feature-button)
Button with automatic usage tracking.
```tsx
import { FeatureButton } from '@tour-kit/adoption';
Export
```
| Prop | Type | Description |
|------|------|-------------|
| `featureId` | `string` | Feature to track |
| `onClick` | `() => void` | Click handler |
| `variant` | `string` | Button variant |
### [IfNotAdopted / IfAdopted](/docs/adoption/components/conditional)
Conditional rendering based on adoption status.
```tsx
import { IfNotAdopted, IfAdopted } from '@tour-kit/adoption';
Power user!
```
### [NewFeatureBadge](/docs/adoption/components/new-feature-badge)
Badge component for new features.
```tsx
import { NewFeatureBadge } from '@tour-kit/adoption';
```
**Props**
| Prop | Type | Default | Description |
| ----------- | ------------------------------------------------------- | -------- | -------------------------------------------- |
| `featureId` | `string` | – | Required. Feature to check adoption against. |
| `text` | `string` | `'New'` | Badge label. |
| `variant` | `'default' \| 'secondary' \| 'outline' \| 'destructive'` | `'default'` | Badge color/style variant. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Badge size. |
---
## Dashboard Components
### [AdoptionDashboard](/docs/adoption/dashboard/adoption-dashboard)
Full admin dashboard for adoption metrics.
```tsx
import { AdoptionDashboard } from '@tour-kit/adoption';
```
### [AdoptionStatsGrid](/docs/adoption/dashboard/stats)
Grid of stat cards.
```tsx
import { AdoptionStatsGrid } from '@tour-kit/adoption';
```
### [AdoptionTable](/docs/adoption/dashboard/table)
Sortable, filterable table of features.
```tsx
import { AdoptionTable } from '@tour-kit/adoption';
{}}
/>
```
### [AdoptionCategoryChart](/docs/adoption/dashboard/charts)
Chart showing adoption by category.
```tsx
import { AdoptionCategoryChart } from '@tour-kit/adoption';
```
---
## [Types](/docs/adoption/types)
### Feature
```tsx
interface Feature {
id: string;
name: string;
trigger: string | { event: string } | (() => void);
adoptionCriteria?: AdoptionCriteria;
resources?: FeatureResources;
priority?: number;
category?: string;
description?: string;
premium?: boolean;
}
```
### AdoptionCriteria
```tsx
interface AdoptionCriteria {
minUses?: number;
recencyDays?: number;
custom?: (usage: FeatureUsage) => boolean;
}
```
### AdoptionStatus
```tsx
type AdoptionStatus = 'not_started' | 'exploring' | 'adopted' | 'churned';
```
### FeatureUsage
```tsx
interface FeatureUsage {
featureId: string;
useCount: number;
firstUsedAt: Date | null;
lastUsedAt: Date | null;
adoptedAt: Date | null;
}
```
### AdoptionFunnelProps
```tsx
interface AdoptionFunnelProps
extends Omit, 'role' | 'aria-label' | 'children' | 'title'> {
/** Pre-computed funnel data, in display order */
steps: readonly FunnelStep[];
/** Optional header rendered above the bars */
title?: React.ReactNode;
/** Click/keyboard activation handler. When omitted, steps are not focusable */
onStepClick?: (step: FunnelStep, index: number) => void;
/** Replaces the default "No funnel data yet." message when steps is empty */
emptyState?: React.ReactNode;
/** Override the auto-generated chart summary on the role="img" element */
ariaLabel?: string;
}
```
### UseFunnelDataInput
```tsx
interface UseFunnelDataInput {
/** Feature IDs in funnel order */
featureIds: readonly string[];
/** Optional label overrides keyed by feature id */
labels?: Partial>;
}
```
Used by `useFunnelData()` to derive a current-state funnel from `useAdoptionStats`. For aggregated, date-ranged funnels, pass pre-computed data directly to ``.
---
## See also
- [`@tour-kit/adoption` package overview](/docs/adoption) — prose docs, recipes, and patterns for every symbol above.
- [API reference index](/docs/api) — browse other package references.
- [Analytics integration](/docs/adoption/analytics) — wire adoption events into `@tour-kit/analytics`.
- [Adoption analytics guide](/docs/guides/adoption-analytics) — funnel and retention patterns built on these primitives.
---
# @tour-kit/ai API
> Complete API reference for @tour-kit/ai — providers, hooks, components, variants, and server-side route + RAG helpers
Complete API reference for the AI chat package. The package has two entry points: the **client** (`@tour-kit/ai`) for React surface, and the **server** (`@tour-kit/ai/server`) for route handlers and RAG/embedding pipelines.
---
## Providers
### AiChatProvider
```tsx
import { AiChatProvider } from '@tour-kit/ai'
{children}
```
| Prop | Type | Description |
|------|------|-------------|
| `config` | `AiChatConfig` | Chat configuration (endpoint, suggestions, rateLimit, ...) |
| `children` | `ReactNode` | App tree |
### AiChatContext
Raw React context value. Prefer `useAiChat` / `useAiChatContext`. Exposed for advanced cases (custom providers, testing).
```tsx
import { AiChatContext } from '@tour-kit/ai'
type Value = React.ContextType // AiChatContextValue | null
```
---
## Components
| Export | Notes |
|--------|-------|
| `AiChatPanel` | Pre-built panel (header + messages + suggestions + input) |
| `AiChatToggle` | Floating open/close button |
| `AiChatHeader` | Title + close-button bar |
| `AiChatMessageList` | Scrollable message list |
| `AiChatMessage` | Single message bubble (role-aware) |
| `AiChatInput` | Input + send button |
| `AiChatSuggestions` | Suggestion chip strip |
| `AiChatPortal` | Portal wrapper |
See [Components](/docs/ai/components) for prop tables and examples.
---
## Hooks
| Hook | Return | Notes |
|------|--------|-------|
| `useAiChat` | `UseAiChatReturn` | Primary chat state + actions |
| `useTourAssistant` | `UseTourAssistantReturn` | Tour-aware extension of `useAiChat` |
| `useSuggestions` | `UseSuggestionsReturn` | Static + dynamic suggestion helpers |
| `useOptionalSuggestions` | `UseSuggestionsReturn` | Deprecated alias for `useSuggestions` |
| `useAiChatContext` | `AiChatContextValue` | Low-level context (unsafe escape hatch) |
See [Hooks](/docs/ai/hooks) for usage and full return types.
---
## Variants
CVA helpers exported for users who want to extend or override styling.
| Export | Variant slots |
|--------|---------------|
| `aiChatPanelVariants` | `size`: `'default' \| 'sm' \| 'lg'` |
| `aiChatToggleVariants` | `size`: `'default' \| 'sm' \| 'lg'` |
| `aiChatHeaderVariants` | (compound classes only) |
| `aiChatMessageVariants` | `role`: `'user' \| 'assistant'` |
| `aiChatSuggestionChipVariants` | (compound classes only) |
```tsx
import { aiChatPanelVariants } from '@tour-kit/ai'
...
```
---
## Utilities
### createRateLimiter / SlidingWindowRateLimiter
Sliding-window client-side rate limiter. The class is exported for advanced patterns; `createRateLimiter` is the convenience factory.
```tsx
import { createRateLimiter, SlidingWindowRateLimiter } from '@tour-kit/ai'
const limiter = createRateLimiter({ maxMessages: 10, windowMs: 60_000 })
if (!limiter.recordMessage()) showToast('Slow down')
const status = limiter.getStatus() // RateLimitStatus
```
### createAnalyticsBridge
Adapter that forwards `AiChatEvent`s to `@tour-kit/analytics`'s `track`.
```tsx
import { createAnalyticsBridge } from '@tour-kit/ai'
import { useAnalytics } from '@tour-kit/analytics'
const { track } = useAnalytics()
const onEvent = createAnalyticsBridge({ track, prefix: 'ai_chat' })
{children}
```
### assembleTourContext
Pure helper that derives the `TourAssistantContext` snapshot from a `useTourContext()` value. Used internally by `useTourAssistant`. Exposed for tests and custom integrations.
```tsx
import { assembleTourContext } from '@tour-kit/ai'
const ctx = assembleTourContext(tourState) // TourAssistantContext
```
---
## Client Types
### AiChatConfig
```tsx
interface AiChatConfig {
endpoint: string
chatId?: string
tourContext?: boolean
suggestions?: SuggestionsConfig
persistence?: PersistenceConfig
rateLimit?: ClientRateLimitConfig
onEvent?(event: AiChatEvent): void
strings?: Partial
errorMessage?: string
}
```
### SuggestionsConfig
```tsx
interface SuggestionsConfig {
static?: string[]
dynamic?: boolean
cacheTtl?: number // default 60_000
}
```
### PersistenceConfig / PersistenceAdapter
```tsx
type PersistenceConfig = 'local' | { adapter: PersistenceAdapter }
interface PersistenceAdapter {
save(chatId: string, messages: UIMessage[]): Promise
load(chatId: string): Promise
clear(chatId: string): Promise
}
```
### ClientRateLimitConfig
```tsx
interface ClientRateLimitConfig {
maxMessages?: number // default 10
windowMs?: number // default 60_000
}
```
### RateLimitStatus
```tsx
interface RateLimitStatus {
canSend: boolean
remaining: number
resetInMs: number
}
```
### AnalyticsBridgeConfig
```tsx
interface AnalyticsBridgeConfig {
track: (eventName: string, properties?: Record) => void
prefix?: string // default 'ai_chat'
}
```
### AiChatStrings
```tsx
interface AiChatStrings {
placeholder: string
send: string
errorMessage: string
emptyState: string
stopGenerating: string
retry: string
title: string
closeLabel: string
ratePositiveLabel: string
rateNegativeLabel: string
}
```
### AiChatState
```tsx
interface AiChatState {
messages: UIMessage[]
status: ChatStatus
error: Error | null
isOpen: boolean
}
```
### ChatStatus
```tsx
type ChatStatus = 'ready' | 'submitted' | 'streaming' | 'error'
```
### AiChatEvent / AiChatEventType
```tsx
type AiChatEventType =
| 'chat_opened'
| 'chat_closed'
| 'message_sent'
| 'response_received'
| 'suggestion_clicked'
| 'message_rated'
| 'error'
interface AiChatEvent {
type: AiChatEventType
data: Record
timestamp: Date
}
```
### AiChatContextValue
The shape returned by `useAiChatContext()`. Combines `AiChatState` with actions and configuration. Refer to source for the full union; the safer accessor is `useAiChat`.
### Component Props
| Type | Component |
|------|-----------|
| `AiChatPanelProps` | `` |
| `AiChatToggleProps` | `` |
| `AiChatHeaderProps` | `` |
| `AiChatMessageListProps` | `` |
| `AiChatMessageProps` | `` |
| `AiChatInputProps` | `