Package Exports
- @tutorial-maker/react-player
- @tutorial-maker/react-player/dist/react-player.css
- @tutorial-maker/react-player/styles.css
Readme
@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
npm install @tutorial-maker/react-player
# or
yarn add @tutorial-maker/react-player
# or
pnpm add @tutorial-maker/react-playerQuick Start
The simplest way to get started is using the embedded format (recommended):
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
- Complete Integration Examples
- Props API
- Tutorial Data Format
- Image Loading
- Styling
- TypeScript Support
- 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
{
"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:
{
"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.jsonOr 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.jsonComplete 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:
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 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.
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.
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.
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:
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
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
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:
{
"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:
import { TutorialPlayer, defaultWebImageLoader } from '@tutorial-maker/react-player';
<TutorialPlayer
tutorials={tutorials}
imageLoader={defaultWebImageLoader}
resolveImagePath={(path) => `/tutorials/my-project/${path}`}
/>Custom Image Loader with Authentication
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:
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:
import '@tutorial-maker/react-player/styles.css';Theme Customization
The package uses CSS variables for theming. Override them in your 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
<TutorialPlayer
tutorials={tutorials}
className="my-custom-player"
/>.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
import type {
Tutorial,
Project,
Step,
SubStep,
Annotation,
ArrowAnnotation,
TextBalloonAnnotation,
HighlightAnnotation,
NumberedBadgeAnnotation,
TutorialPlayerProps,
Position,
Size,
TailPosition,
} from '@tutorial-maker/react-player';Type-Safe Tutorial Data
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
# Using sharp-cli
npx sharp-cli -i input.png -o output.webp --quality 852. 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
// 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:
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:
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:
<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
onCloseis provided)
Troubleshooting
Images Not Loading
- Check that
imageLoaderis returning data URLs - Verify
resolveImagePathis generating correct paths - Check browser console for error messages
- Ensure screenshot paths in JSON match actual file paths
TypeScript Errors
- Ensure you're importing types from the package
- Check that your
tsconfig.jsonincludes the package - Verify tutorial data matches the expected type structure
Styling Issues
- Ensure you've imported the CSS:
import '@tutorial-maker/react-player/styles.css' - Check for CSS specificity conflicts
- 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:
npm install @tutorial-maker/react-player2. Embedded Format Integration (Recommended - Zero Config):
If the developer has a .tutorial.json file (embedded format):
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:
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:
- Always use embedded format (
.tutorial.json) when possible - it's simpler and requires zero configuration - Pass
projectData.tutorialsto the player, NOT the entire project object - Always import the CSS - the component requires it to function properly
- Use proper container sizing - wrap in a div with explicit width/height
- Embedded format includes base64 images - no imageLoader needed for embedded format
- 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:
// Example for common UI patterns
<MenuItem onClick={() => navigate('/tutorials')}>
<HelpCircle />
Tutorials
</MenuItem>Integration with routing:
// 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": truefield - 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.