@tutorial-maker/react-player
Version:
Portable tutorial player component for React applications with interactive step-by-step walkthroughs, screenshots, and annotations
1,108 lines (872 loc) ⢠28.8 kB
Markdown
# @tutorial-maker/react-player
A portable, customizable tutorial player component for React applications. Display interactive step-by-step tutorials with screenshots and annotations in your React app.
## Features
- šÆ **Interactive Tutorial Playback** - Step through tutorials with smooth scrolling and navigation
- š¼ļø **Screenshot Support** - Display screenshots with annotations (arrows, text balloons, highlights, numbered badges)
- š± **Responsive Design** - Works seamlessly across different screen sizes
- šØ **Customizable** - Flexible image loading and path resolution
- š **Universal** - Works in both web and Tauri environments
- ā” **Lightweight** - Minimal dependencies, optimized bundle size
- š§ **TypeScript** - Full TypeScript support with type definitions
## Installation
```bash
npm install @tutorial-maker/react-player
# or
yarn add @tutorial-maker/react-player
# or
pnpm add @tutorial-maker/react-player
```
## Quick Start
The simplest way to get started is using the **embedded format** (recommended):
```tsx
import { TutorialPlayer } from '@tutorial-maker/react-player';
import '@tutorial-maker/react-player/styles.css';
// Import your embedded tutorial file (exported from tutorial-maker app)
import tutorialData from './my-tutorial.tutorial.json';
function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<TutorialPlayer
tutorials={tutorialData.tutorials}
onClose={() => console.log('Tutorial closed')}
/>
</div>
);
}
```
**That's it!** No image loaders, no glob imports, no bundler configuration needed. The embedded format includes all screenshots as base64 data URLs in a single self-contained file.
## Table of Contents
- [Embedding Tutorial Projects](#embedding-tutorial-projects)
- [Complete Integration Examples](#complete-integration-examples)
- [Props API](#props-api)
- [Tutorial Data Format](#tutorial-data-format)
- [Image Loading](#image-loading)
- [Styling](#styling)
- [TypeScript Support](#typescript-support)
- [Best Practices](#best-practices)
## Embedding Tutorial Projects
### Project Structure
When you create tutorials with the tutorial-maker app, it generates a project with this structure:
```
my-project/
āāā project.json # Main project file
āāā screenshots/ # Screenshots folder
āāā step1.png
āāā step2.png
āāā ...
```
### Project JSON Format
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Tutorial Project",
"projectFolder": "my-project",
"tutorials": [
{
"id": "tutorial-1",
"title": "Getting Started",
"description": "Learn the basics",
"steps": [...],
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
```
**Important:** Pass `projectData.tutorials` to the player, not the entire project object.
### Tutorial Formats
The tutorial-maker app supports two formats for distributing tutorials:
#### 1. Folder-Based Format (Default)
The standard format with separate files:
```
my-project/
āāā project.json # Main project file
āāā screenshots/ # Screenshots as separate files
āāā step1.png
āāā step2.png
āāā ...
```
**Best for:** Development, version control, smaller file sizes
#### 2. Embedded Format (Single File)
A self-contained format where screenshots are embedded as base64 data URLs:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Tutorial",
"projectFolder": "my-project",
"embedded": true,
"tutorials": [
{
"steps": [
{
"screenshotPath": "data:image/png;base64,iVBORw0KGgoAAAANS..."
}
]
}
]
}
```
**Best for:** Simple distribution, single-file deployment, no build configuration needed
**Size Note:** Embedded format is ~33% larger than folder-based due to base64 encoding.
**Export Embedded Format:** Use the "Export" button in the tutorial-maker app to create a `.tutorial.json` file.
### Embedding in Your App
Copy the entire tutorial project folder into your React app:
```
your-app/
āāā src/
ā āāā App.tsx
ā āāā tutorials/
ā āāā my-project/ ā Copy tutorial project here
ā āāā project.json
ā āāā screenshots/
ā āāā step1.png
ā āāā step2.png
āāā package.json
```
**Or** use the embedded format by importing a single `.tutorial.json` file:
```
your-app/
āāā src/
ā āāā App.tsx
ā āāā tutorials/
ā āāā my-tutorial.tutorial.json ā Single embedded file
āāā package.json
```
## Complete Integration Examples
### Example 1: Embedded Format - Zero Configuration (Recommended)
The simplest and most portable way to integrate tutorials. Perfect for quick integrations, CDN deployments, and when you want zero build configuration.
**Step 1:** Export your tutorial using the "Export" button in the tutorial-maker app. This creates a `.tutorial.json` file with embedded base64 screenshots.
**Step 2:** Import and use it directly:
```tsx
import React from 'react';
import { TutorialPlayer } from '@tutorial-maker/react-player';
import '@tutorial-maker/react-player/styles.css';
// Import the embedded tutorial file
import tutorialData from './my-tutorial.tutorial.json';
function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<TutorialPlayer
tutorials={tutorialData.tutorials}
onClose={() => console.log('Tutorial closed')}
/>
</div>
);
}
export default App;
```
**Why use embedded format?**
ā
**Zero configuration** - No image loaders, no glob imports, no bundler setup required
ā
**Single file** - Easy to distribute, version, and deploy
ā
**Works everywhere** - Compatible with any bundler (Vite, Webpack, Rollup, etc.)
ā
**Self-contained** - All screenshots included as base64 data URLs
ā
**Type-safe** - Full TypeScript support out of the box
**Trade-off:** File size is ~33% larger due to base64 encoding.
**File Structure:**
```
src/
āāā App.tsx
āāā my-tutorial.tutorial.json ā Single embedded file (~2-3 MB typical)
```
**Real-world example:** The [DBill Delivery Helper](https://github.com/yourusername/dbill-delivery-helper) app uses this approach for its built-in tutorials.
### Example 2: Bundled Tutorial with Vite
This example bundles the tutorial project with your app using Vite's glob import feature.
```tsx
import React from 'react';
import { TutorialPlayer } from '@tutorial-maker/react-player';
import '@tutorial-maker/react-player/styles.css';
// Import the project JSON
import projectData from './tutorials/my-project/project.json';
// Import all screenshots using Vite's glob import
const screenshots = import.meta.glob(
'./tutorials/**/screenshots/*.{png,jpg}',
{ eager: true, as: 'url' }
);
function TutorialApp() {
// Custom loader for bundled images
const imageLoader = async (path: string): Promise<string> => {
// Already a data URL? Return as-is
if (path.startsWith('data:')) {
return path;
}
// Find the image in our imports
const projectFolder = projectData.projectFolder;
const imageUrl = screenshots[`./tutorials/${projectFolder}/${path}`];
if (!imageUrl) {
console.error(`Screenshot not found: ${path}`);
throw new Error(`Screenshot not found: ${path}`);
}
return imageUrl;
};
return (
<div style={{ width: '100vw', height: '100vh' }}>
<TutorialPlayer
tutorials={projectData.tutorials}
imageLoader={imageLoader}
onClose={() => console.log('Tutorial closed')}
/>
</div>
);
}
export default TutorialApp;
```
**File Structure:**
```
src/
āāā App.tsx
āāā tutorials/
āāā onboarding/
ā āāā project.json
ā āāā screenshots/
ā āāā welcome.png
ā āāā step1.png
āāā advanced/
āāā project.json
āāā screenshots/
āāā ...
```
### Example 2: Loading from Server/API (Dynamic Loading)
For dynamic scenarios where tutorials are loaded from a server or API endpoint.
```tsx
import React, { useState, useEffect } from 'react';
import { TutorialPlayer, type Tutorial, defaultWebImageLoader } from '@tutorial-maker/react-player';
import '@tutorial-maker/react-player/styles.css';
function TutorialApp() {
const [tutorials, setTutorials] = useState<Tutorial[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadTutorials() {
try {
// Fetch project JSON from server
const response = await fetch('/api/tutorials/my-project.json');
if (!response.ok) {
throw new Error('Failed to load tutorials');
}
const projectData = await response.json();
setTutorials(projectData.tutorials);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
console.error('Failed to load tutorials:', err);
} finally {
setLoading(false);
}
}
loadTutorials();
}, []);
if (loading) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
Loading tutorials...
</div>
);
}
if (error) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
Error: {error}
</div>
);
}
return (
<div style={{ width: '100vw', height: '100vh' }}>
<TutorialPlayer
tutorials={tutorials}
imageLoader={defaultWebImageLoader}
resolveImagePath={(relativePath) =>
`/api/tutorials/my-project/${relativePath}`
}
onClose={() => window.history.back()}
/>
</div>
);
}
export default TutorialApp;
```
### Example 3: Multiple Tutorial Projects
Switch between different tutorial projects with a selector.
```tsx
import React, { useState } from 'react';
import { TutorialPlayer, type Tutorial, type Project } from '@tutorial-maker/react-player';
import '@tutorial-maker/react-player/styles.css';
// Import multiple projects
import onboardingProject from './tutorials/onboarding/project.json';
import advancedProject from './tutorials/advanced/project.json';
const screenshots = import.meta.glob(
'./tutorials/**/screenshots/*.{png,jpg}',
{ eager: true, as: 'url' }
);
type ProjectKey = 'onboarding' | 'advanced';
function MultiTutorialApp() {
const [currentProject, setCurrentProject] = useState<ProjectKey>('onboarding');
const projectMap: Record<ProjectKey, Project> = {
onboarding: onboardingProject as Project,
advanced: advancedProject as Project,
};
const currentProjectData = projectMap[currentProject];
const tutorials = currentProjectData.tutorials;
const imageLoader = async (path: string): Promise<string> => {
if (path.startsWith('data:')) return path;
const projectFolder = currentProjectData.projectFolder;
const imageUrl = screenshots[`./tutorials/${projectFolder}/${path}`];
if (!imageUrl) {
throw new Error(`Screenshot not found: ${path}`);
}
return imageUrl;
};
return (
<div style={{ width: '100vw', height: '100vh', position: 'relative' }}>
{/* Project Selector */}
<div style={{
position: 'fixed',
top: '10px',
left: '10px',
zIndex: 100,
backgroundColor: 'white',
padding: '8px',
borderRadius: '4px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<select
value={currentProject}
onChange={(e) => setCurrentProject(e.target.value as ProjectKey)}
style={{ padding: '4px 8px' }}
>
<option value="onboarding">Onboarding Tutorial</option>
<option value="advanced">Advanced Tutorial</option>
</select>
</div>
<TutorialPlayer
key={currentProject} // Force re-render on project change
tutorials={tutorials}
imageLoader={imageLoader}
/>
</div>
);
}
export default MultiTutorialApp;
```
### Example 4: Webpack/Create React App
For apps using Webpack or Create React App:
```tsx
import React from 'react';
import { TutorialPlayer } from '@tutorial-maker/react-player';
import '@tutorial-maker/react-player/styles.css';
import projectData from './tutorials/my-project/project.json';
// Import screenshots individually
import welcome from './tutorials/my-project/screenshots/welcome.png';
import step1 from './tutorials/my-project/screenshots/step1.png';
import step2 from './tutorials/my-project/screenshots/step2.png';
function TutorialApp() {
const imageMap: Record<string, string> = {
'screenshots/welcome.png': welcome,
'screenshots/step1.png': step1,
'screenshots/step2.png': step2,
};
const imageLoader = async (path: string): Promise<string> => {
if (path.startsWith('data:')) return path;
const imageUrl = imageMap[path];
if (!imageUrl) {
throw new Error(`Screenshot not found: ${path}`);
}
return imageUrl;
};
return (
<div style={{ width: '100vw', height: '100vh' }}>
<TutorialPlayer
tutorials={projectData.tutorials}
imageLoader={imageLoader}
/>
</div>
);
}
export default TutorialApp;
```
## Props API
### TutorialPlayerProps
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `tutorials` | `Tutorial[]` | Yes | - | Array of tutorial objects (extract from `project.tutorials`) |
| `initialTutorialId` | `string` | No | First tutorial | ID of the initially selected tutorial |
| `imageLoader` | `(path: string) => Promise<string>` | No | `defaultWebImageLoader` | Custom function to load images and return data URLs |
| `resolveImagePath` | `(relativePath: string) => string` | No | Identity function | Function to resolve relative image paths to absolute paths/URLs |
| `onClose` | `() => void` | No | - | Callback when the close button is clicked (if not provided, close button is hidden) |
| `className` | `string` | No | `''` | Additional CSS class names for the root container |
## Tutorial Data Format
### Complete Type Definitions
```typescript
interface Project {
id: string;
name: string;
projectFolder: string; // Folder name only
tutorials: Tutorial[];
createdAt: string; // ISO date string
updatedAt: string; // ISO date string
embedded?: boolean; // True if screenshots are embedded as base64 data URLs
annotationDefaults?: {
arrowColor: string;
highlightColor: string;
balloonBackgroundColor: string;
balloonTextColor: string;
badgeBackgroundColor: string;
badgeTextColor: string;
rectColor: string;
circleColor: string;
};
}
interface Tutorial {
id: string;
title: string;
description: string;
steps: Step[];
createdAt: string;
updatedAt: string;
}
interface Step {
id: string;
title: string;
description: string;
screenshotPath: string | null; // Relative path like "screenshots/step1.png"
// OR data URL like "data:image/png;base64,..." for embedded format
subSteps: SubStep[];
order: number;
}
interface SubStep {
id: string;
title: string;
description: string;
order: number;
// Legacy format (still supported)
annotations?: Annotation[];
// New format (recommended)
annotationActions?: AnnotationAction[];
clearPreviousAnnotations?: boolean;
showAnnotationsSequentially?: boolean;
}
```
### Annotation Types
```typescript
type Annotation =
| ArrowAnnotation
| TextBalloonAnnotation
| HighlightAnnotation
| NumberedBadgeAnnotation
| RectAnnotation
| CircleAnnotation;
interface ArrowAnnotation {
id: string;
type: 'arrow';
position: Position;
startPosition: Position;
endPosition: Position;
controlPoint?: Position; // For curved arrows
color: string;
thickness: number;
doubleHeaded?: boolean;
}
interface TextBalloonAnnotation {
id: string;
type: 'textBalloon';
position: Position;
text: string;
size: Size;
backgroundColor: string;
textColor: string;
tailPosition?: TailPosition;
}
interface HighlightAnnotation {
id: string;
type: 'highlight';
position: Position;
size: Size;
color: string;
opacity: number;
}
interface NumberedBadgeAnnotation {
id: string;
type: 'numberedBadge';
position: Position;
number: number;
size: number;
backgroundColor: string;
textColor: string;
}
```
## Image Loading
### Understanding Screenshot Paths
Screenshots are stored with **relative paths** in the tutorial JSON:
```json
{
"screenshotPath": "screenshots/welcome.png"
}
```
You must resolve these to absolute paths or data URLs using the `imageLoader` and `resolveImagePath` props.
### Default Web Loader
The package includes a default image loader for web environments:
```tsx
import { TutorialPlayer, defaultWebImageLoader } from '@tutorial-maker/react-player';
<TutorialPlayer
tutorials={tutorials}
imageLoader={defaultWebImageLoader}
resolveImagePath={(path) => `/tutorials/my-project/${path}`}
/>
```
### Custom Image Loader with Authentication
```tsx
const customImageLoader = async (path: string): Promise<string> => {
const response = await fetch(path, {
headers: {
Authorization: `Bearer ${getAuthToken()}`,
},
});
if (!response.ok) {
throw new Error(`Failed to load image: ${path}`);
}
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
};
<TutorialPlayer
tutorials={tutorials}
imageLoader={customImageLoader}
resolveImagePath={(path) => `https://cdn.example.com/${path}`}
/>
```
### Tauri/Desktop Applications
For Tauri applications, create a custom loader using Tauri's file system API:
```tsx
import { readFile } from '@tauri-apps/plugin-fs';
import { TutorialPlayer, bytesToBase64 } from '@tutorial-maker/react-player';
const tauriImageLoader = async (absolutePath: string): Promise<string> => {
const bytes = await readFile(absolutePath);
const base64 = bytesToBase64(bytes);
const mimeType = absolutePath.endsWith('.jpg') ? 'image/jpeg' : 'image/png';
return `data:${mimeType};base64,${base64}`;
};
<TutorialPlayer
tutorials={tutorials}
imageLoader={tauriImageLoader}
resolveImagePath={(relativePath) =>
`${projectBasePath}/${relativePath}`
}
/>
```
## Styling
### CSS Import
Always import the styles in your entry component:
```tsx
import '@tutorial-maker/react-player/styles.css';
```
### Theme Customization
The package uses CSS variables for theming. Override them in your CSS:
```css
:root {
/* Background and foreground colors */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* Primary colors */
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* Secondary colors */
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
/* Muted colors */
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
/* Accent colors */
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
/* Card colors */
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
/* Border and input colors */
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
/* Border radius */
--radius: 0.5rem;
}
/* Dark mode */
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* ... */
}
```
### Custom Container Styling
```tsx
<TutorialPlayer
tutorials={tutorials}
className="my-custom-player"
/>
```
```css
.my-custom-player {
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
max-width: 1400px;
margin: 0 auto;
}
```
## TypeScript Support
The package is written in TypeScript and includes full type definitions.
### Importing Types
```tsx
import type {
Tutorial,
Project,
Step,
SubStep,
Annotation,
ArrowAnnotation,
TextBalloonAnnotation,
HighlightAnnotation,
NumberedBadgeAnnotation,
TutorialPlayerProps,
Position,
Size,
TailPosition,
} from '@tutorial-maker/react-player';
```
### Type-Safe Tutorial Data
```tsx
import { TutorialPlayer, type Tutorial } from '@tutorial-maker/react-player';
const tutorials: Tutorial[] = [
{
id: 'tutorial-1',
title: 'My Tutorial',
description: 'A great tutorial',
steps: [/* ... */],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
<TutorialPlayer tutorials={tutorials} />
```
## Best Practices
### 1. Image Optimization
Optimize screenshots before bundling:
- Use WebP format for better compression
- Resize images to appropriate dimensions (1920x1080 max recommended)
- Use image optimization tools in your build pipeline
```bash
# Using sharp-cli
npx sharp-cli -i input.png -o output.webp --quality 85
```
### 2. Bundle Size Considerations
For large tutorial projects:
- Consider loading tutorials on-demand rather than bundling all at once
- Use code splitting to lazy load the player component
- Compress project JSON files
```tsx
// Lazy load the player
const TutorialPlayer = React.lazy(() =>
import('@tutorial-maker/react-player').then(mod => ({
default: mod.TutorialPlayer
}))
);
function App() {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<TutorialPlayer tutorials={tutorials} />
</React.Suspense>
);
}
```
### 3. Error Handling
Always handle image loading errors gracefully:
```tsx
const imageLoader = async (path: string): Promise<string> => {
try {
const imageUrl = screenshots[`./tutorials/${projectFolder}/${path}`];
if (!imageUrl) {
console.error(`Screenshot not found: ${path}`);
// Return a placeholder or throw error
return PLACEHOLDER_IMAGE;
}
return imageUrl;
} catch (error) {
console.error('Failed to load image:', error);
throw error;
}
};
```
### 4. Loading States
Show loading states while tutorials are being fetched:
```tsx
function TutorialApp() {
const [loading, setLoading] = useState(true);
const [tutorials, setTutorials] = useState<Tutorial[]>([]);
useEffect(() => {
loadTutorials().then(data => {
setTutorials(data.tutorials);
setLoading(false);
});
}, []);
if (loading) {
return <LoadingSpinner />;
}
return <TutorialPlayer tutorials={tutorials} />;
}
```
### 5. Responsive Container
Always wrap the player in a properly sized container:
```tsx
<div style={{
width: '100%',
height: '100vh',
display: 'flex',
flexDirection: 'column'
}}>
<TutorialPlayer tutorials={tutorials} />
</div>
```
## Browser Support
- Modern browsers (Chrome 90+, Firefox 88+, Safari 14+, Edge 90+)
- ES2020+ support required
- CSS Grid and Flexbox support required
- CSS custom properties (CSS variables) support required
## Keyboard Shortcuts
The player includes built-in keyboard navigation:
- **Arrow Up/Down** - Navigate between substeps
- **Space** - Next substep
- **Home** - Go to first substep
- **End** - Go to last substep
- **Escape** - Close player (if `onClose` is provided)
## Troubleshooting
### Images Not Loading
1. Check that `imageLoader` is returning data URLs
2. Verify `resolveImagePath` is generating correct paths
3. Check browser console for error messages
4. Ensure screenshot paths in JSON match actual file paths
### TypeScript Errors
1. Ensure you're importing types from the package
2. Check that your `tsconfig.json` includes the package
3. Verify tutorial data matches the expected type structure
### Styling Issues
1. Ensure you've imported the CSS: `import '@tutorial-maker/react-player/styles.css'`
2. Check for CSS specificity conflicts
3. Verify CSS variables are defined in your theme
## AI Agent Integration Guide
This section is for AI coding assistants (like Claude Code, GitHub Copilot, Cursor, etc.) helping developers integrate the tutorial player.
### Quick Integration Recipe for AI Agents
When a developer asks to integrate tutorial player functionality, follow this pattern:
**1. Installation:**
```bash
npm install @tutorial-maker/react-player
```
**2. Embedded Format Integration (Recommended - Zero Config):**
If the developer has a `.tutorial.json` file (embedded format):
```tsx
import { TutorialPlayer } from '@tutorial-maker/react-player';
import '@tutorial-maker/react-player/styles.css';
import tutorialData from './path/to/tutorial-file.tutorial.json';
function TutorialsPage() {
return (
<div style={{ width: '100%', height: '100%' }}>
<TutorialPlayer
tutorials={tutorialData.tutorials}
onClose={() => console.log('Tutorial closed')}
/>
</div>
);
}
```
**That's it! No image loaders or additional configuration needed.**
**3. Folder-Based Format Integration (Advanced):**
Only use this if they have a `project.json` file with separate screenshot files:
```tsx
import { TutorialPlayer } from '@tutorial-maker/react-player';
import '@tutorial-maker/react-player/styles.css';
import projectData from './tutorials/my-project/project.json';
// Use Vite's glob import for screenshots
const screenshots = import.meta.glob(
'./tutorials/**/screenshots/*.{png,jpg}',
{ eager: true, as: 'url' }
);
function TutorialsPage() {
const imageLoader = async (path: string) => {
if (path.startsWith('data:')) return path;
const projectFolder = projectData.projectFolder;
const imageUrl = screenshots[`./tutorials/${projectFolder}/${path}`];
if (!imageUrl) {
throw new Error(`Screenshot not found: ${path}`);
}
return imageUrl;
};
return (
<div style={{ width: '100%', height: '100%' }}>
<TutorialPlayer
tutorials={projectData.tutorials}
imageLoader={imageLoader}
onClose={() => console.log('Tutorial closed')}
/>
</div>
);
}
```
### Key Points for AI Agents:
1. **Always use embedded format** (`.tutorial.json`) when possible - it's simpler and requires zero configuration
2. **Pass `projectData.tutorials`** to the player, NOT the entire project object
3. **Always import the CSS** - the component requires it to function properly
4. **Use proper container sizing** - wrap in a div with explicit width/height
5. **Embedded format includes base64 images** - no imageLoader needed for embedded format
6. **Folder format needs imageLoader** - only use glob imports if they have separate screenshot files
### Common Integration Patterns:
**Adding tutorials to existing React app:**
- Create a new page component (e.g., `TutorialsPage.tsx`)
- Add navigation/routing to that page
- Import and render TutorialPlayer with embedded tutorial file
**Adding to navigation menu:**
```tsx
// Example for common UI patterns
<MenuItem onClick={() => navigate('/tutorials')}>
<HelpCircle />
Tutorials
</MenuItem>
```
**Integration with routing:**
```tsx
// React Router example
<Route path="/tutorials" element={<TutorialsPage />} />
// Or as modal/dialog
<Dialog open={showTutorials}>
<TutorialPlayer tutorials={tutorials} onClose={() => setShowTutorials(false)} />
</Dialog>
```
### Format Detection:
**Embedded format indicators:**
- File name ends with `.tutorial.json`
- JSON has `"embedded": true` field
- Screenshots are `"data:image/png;base64,..."` strings
**Folder format indicators:**
- File named `project.json`
- Has `"projectFolder"` field
- Screenshots are relative paths like `"screenshots/step1.png"`
- Separate `screenshots/` folder exists
### Error Prevention:
Common mistakes to avoid:
- ā Forgetting to import CSS
- ā Passing entire project object instead of `project.tutorials`
- ā Using imageLoader for embedded format (not needed)
- ā Not wrapping in sized container
- ā Using wrong import path for CSS
Correct patterns:
- ā
Import CSS: `import '@tutorial-maker/react-player/styles.css'`
- ā
Pass tutorials: `tutorials={data.tutorials}`
- ā
Sized container: `<div style={{ width: '100%', height: '100%' }}>`
- ā
Embedded format: No imageLoader needed
## License
MIT
## Contributing
Contributions are welcome! Please open an issue or submit a pull request on GitHub.
## Support
For issues, questions, or feature requests, please open an issue on the GitHub repository.