/ src / features / tutorial / TutorialModal.ts
TutorialModal.ts
  1  import { Modal, App } from 'obsidian';
  2  import { createRoot, Root } from 'react-dom/client';
  3  import React from 'react';
  4  import { ManimText } from './ManimText';
  5  import { tutorialService, TutorialStep } from './TutorialService';
  6  
  7  /**
  8   * TutorialModal - Native Obsidian modal for tutorial steps
  9   *
 10   * Uses Obsidian's Modal API with React rendering for ManimText animation
 11   */
 12  export class TutorialModal extends Modal {
 13    private root: Root | null = null;
 14    private currentStep: TutorialStep | null = null;
 15    private autoAdvanceTimer: ReturnType<typeof setTimeout> | null = null;
 16  
 17    constructor(app: App) {
 18      super(app);
 19    }
 20  
 21    onOpen() {
 22      const { contentEl } = this;
 23  
 24      // Style the modal
 25      this.modalEl.addClass('tutorial-modal');
 26      contentEl.empty();
 27  
 28      // Create container for React
 29      const container = contentEl.createDiv({ cls: 'tutorial-modal-content' });
 30  
 31      // Get current step
 32      this.currentStep = tutorialService.getCurrentStep();
 33  
 34      if (!this.currentStep) {
 35        console.warn('🎓 [TutorialModal] No current step available');
 36        this.close();
 37        return;
 38      }
 39  
 40      console.log('🎓 [TutorialModal] Opening with step:', this.currentStep.title);
 41  
 42      // Render React component
 43      this.root = createRoot(container);
 44      this.renderStep();
 45  
 46      // Set up auto-advance
 47      if (this.currentStep.duration) {
 48        this.autoAdvanceTimer = setTimeout(() => {
 49          this.nextStep();
 50        }, this.currentStep.duration);
 51      }
 52    }
 53  
 54    private renderStep() {
 55      if (!this.root || !this.currentStep) return;
 56  
 57      const step = this.currentStep;
 58  
 59      this.root.render(
 60        React.createElement('div', { className: 'tutorial-step' },
 61          // ManimText animation - key forces remount on text change
 62          React.createElement(ManimText, {
 63            key: step.title, // Force remount when title changes
 64            text: step.title,
 65            strokeDuration: 2,
 66            fillDelay: 0.3,
 67            fadeStroke: true,
 68            fontSize: 48
 69          }),
 70  
 71          // Description
 72          React.createElement('div', {
 73            className: 'tutorial-description',
 74            style: {
 75              marginTop: '2rem',
 76              fontSize: '1.2rem',
 77              textAlign: 'center',
 78              opacity: 0,
 79              animation: 'fadeIn 0.5s ease-in 2.5s forwards'
 80            }
 81          }, step.description),
 82  
 83          // Button container
 84          React.createElement('div', {
 85            className: 'tutorial-buttons',
 86            style: {
 87              marginTop: '2rem',
 88              display: 'flex',
 89              gap: '1rem',
 90              justifyContent: 'center'
 91            }
 92          },
 93            // Skip button
 94            React.createElement('button', {
 95              className: 'mod-cta',
 96              onClick: () => this.skipTutorial()
 97            }, 'Skip Tutorial'),
 98  
 99            // Next button (if not auto-advancing)
100            !step.duration && React.createElement('button', {
101              className: 'mod-cta',
102              onClick: () => this.nextStep()
103            }, 'Next')
104          )
105        )
106      );
107    }
108  
109    private nextStep() {
110      if (this.autoAdvanceTimer) {
111        globalThis.clearTimeout(this.autoAdvanceTimer);
112        this.autoAdvanceTimer = null;
113      }
114  
115      tutorialService.next();
116      this.currentStep = tutorialService.getCurrentStep();
117  
118      if (this.currentStep) {
119        // Re-render with new step
120        this.renderStep();
121  
122        // Set up auto-advance for new step
123        if (this.currentStep.duration) {
124          this.autoAdvanceTimer = setTimeout(() => {
125            this.nextStep();
126          }, this.currentStep.duration);
127        }
128      } else {
129        // Tutorial complete
130        this.close();
131      }
132    }
133  
134    private skipTutorial() {
135      if (this.autoAdvanceTimer) {
136        globalThis.clearTimeout(this.autoAdvanceTimer);
137        this.autoAdvanceTimer = null;
138      }
139  
140      tutorialService.skip();
141      this.close();
142    }
143  
144    onClose() {
145      const { contentEl } = this;
146  
147      // Clean up auto-advance timer
148      if (this.autoAdvanceTimer) {
149        globalThis.clearTimeout(this.autoAdvanceTimer);
150        this.autoAdvanceTimer = null;
151      }
152  
153      // Clean up React
154      if (this.root) {
155        this.root.unmount();
156        this.root = null;
157      }
158  
159      contentEl.empty();
160    }
161  }