JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 34
  • Score
    100M100P100Q85708F
  • License MIT

Portable tutorial player component for React applications with interactive step-by-step walkthroughs, screenshots, and annotations

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-player

Quick 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

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

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 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
// 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 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:

npm install @tutorial-maker/react-player

2. 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:

  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:

// 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": 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.