UNPKG

@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
# @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.