Files
yt2ai/docs/ui-architecture.md
Marius Mutu 064899eb95
Some checks failed
Build and Test YT2AI Bookmarklet / build-and-test (16.x) (push) Has been cancelled
Build and Test YT2AI Bookmarklet / build-and-test (18.x) (push) Has been cancelled
Build and Test YT2AI Bookmarklet / build-and-test (20.x) (push) Has been cancelled
Build and Test YT2AI Bookmarklet / release (push) Has been cancelled
Build and Test YT2AI Bookmarklet / security-scan (push) Has been cancelled
Initial project setup
Add project structure with package.json, source code, tests, documentation, and GitHub workflows.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-08 15:51:19 +03:00

68 KiB
Raw Blame History

yt2ai Frontend Architecture Document

Change Log

Date Version Description Author
2025-09-06 v1.0 Initial frontend architecture creation for bookmarklet Winston (Architect)

Template and Framework Selection

Architecture Decision

Based on the PRD analysis, this is a YouTube Subtitle Extraction & AI Summarization Bookmarklet project with specific constraints:

  • Platform: Android Chrome browser exclusively
  • Technology: Vanilla JavaScript ES6+ (no frameworks or dependencies)
  • Architecture: Client-side only bookmarklet with external API integrations
  • No Traditional UI Framework Needed: This is a bookmarklet (single JavaScript file)

Key Constraint Analysis

The PRD specifically states "Vanilla JavaScript ES6+ pentru maximum mobile browser compatibility without build dependencies" and "No frameworks or libraries to maintain bookmarklet simplicity and avoid CSP restrictions."

This is NOT a traditional frontend application requiring React/Vue/Angular. It's a bookmarklet - essentially a JavaScript snippet executed in the browser with minimal DOM manipulation overlays on existing YouTube pages.

Architectural Approach

Since this is a bookmarklet rather than a traditional frontend application, the architecture focuses on:

  1. Bookmarklet structure and organization
  2. Mobile-optimized overlay/modal patterns for YouTube integration
  3. JavaScript module organization within constraint of single-file distribution

Frontend Tech Stack

Technology Stack Table

Category Technology Version Purpose Rationale
Framework Vanilla JavaScript ES6+ Core bookmarklet logic and DOM manipulation Maximum browser compatibility, zero build dependencies, CSP compliance
UI Library Native DOM APIs Browser Standard Overlay creation and mobile touch interactions No external dependencies, optimized for bookmarklet constraints
State Management Module Pattern ES6+ Encapsulated state within bookmarklet scope Simple, lightweight, no persistence needed for stateless operation
Routing N/A - No routing needed Bookmarklet operates on current page context
Build Tool GitHub Actions Latest Minification and production bundling Automated minification, zero-cost CI/CD
Styling Inline CSS + CSS-in-JS Browser Standard Mobile-optimized overlays and responsive feedback UI Avoid external stylesheets, ensure mobile touch targets
Testing Jest + jsdom ^29.0.0 Unit testing for core functions Standard JS testing with DOM simulation
Component Library Custom Overlay Components ES6+ Reusable modal, loading, and error UI patterns Lightweight, mobile-first, YouTube-integrated design
Form Handling Native FormData + Fetch Browser Standard API integration with downloadyoutubesubtitles.com No form libraries needed for simple API calls
Animation CSS Transitions + Web Animations API Browser Standard Loading states and mobile-friendly transitions Smooth mobile performance without heavy libraries
Dev Tools ESLint + Prettier Latest Code quality and consistency Standard development tooling

Project Structure

yt2ai-bookmarklet/
├── src/                           # Development source (modular)
│   ├── core/
│   │   ├── videoExtractor.js      # YouTube video ID extraction
│   │   ├── subtitleFetcher.js     # API integration with downloadyoutubesubtitles.com
│   │   └── claudeIntegration.js   # Claude.ai tab management & clipboard
│   ├── ui/
│   │   ├── overlayManager.js      # Mobile overlay creation and management
│   │   ├── loadingStates.js       # Progress indicators and mobile feedback
│   │   ├── errorHandlers.js       # Error display and recovery UX
│   │   └── styles/
│   │       ├── overlay.css        # Mobile-optimized overlay styles
│   │       └── animations.css     # Touch-friendly transitions
│   ├── utils/
│   │   ├── domUtils.js           # DOM manipulation utilities
│   │   ├── mobileUtils.js        # Mobile-specific helpers (touch, viewport)
│   │   └── errorUtils.js         # Error formatting and logging
│   └── bookmarklet.js            # Main development entry point
├── dist/                          # Production builds
│   ├── bookmarklet.min.js        # Minified single-file bookmarklet
│   └── bookmarklet-debug.js      # Development version with logging
├── tests/
│   ├── unit/
│   │   ├── core/                 # Core function tests
│   │   ├── ui/                   # UI component tests
│   │   └── utils/                # Utility function tests
│   ├── integration/
│   │   ├── youtube-urls.test.js  # Real YouTube URL parsing tests
│   │   └── api-integration.test.js # External API integration tests
│   └── manual/
│       ├── mobile-test-cases.md  # Manual mobile testing scenarios
│       └── captcha-scenarios.md  # CAPTCHA handling test cases
├── docs/
│   ├── installation.md           # User installation instructions
│   ├── development.md            # Developer setup guide
│   └── mobile-considerations.md  # Mobile-specific implementation notes
├── build/
│   ├── webpack.config.js         # Build configuration for minification
│   ├── mobile-optimize.js        # Mobile-specific build optimizations
│   └── github-actions.yml        # CI/CD pipeline configuration
└── README.md                     # Project overview and quick start

Component Standards

Component Template

/**
 * Mobile Overlay Component Template
 * Optimized for Android Chrome bookmarklet environment
 */

interface OverlayComponent {
  element: HTMLElement;
  isVisible: boolean;
  destroy(): void;
}

class MobileOverlayComponent implements OverlayComponent {
  public element: HTMLElement;
  public isVisible: boolean = false;
  private readonly containerId: string;
  private readonly mobileBreakpoint: number = 768;

  constructor(
    private readonly config: {
      id: string;
      className?: string;
      content: string | HTMLElement;
      type: 'loading' | 'error' | 'success' | 'info';
      autoClose?: number;
      position?: 'top' | 'center' | 'bottom';
    }
  ) {
    this.containerId = `yt2ai-${config.id}`;
    this.createElement();
    this.attachStyles();
    this.bindMobileEvents();
  }

  private createElement(): void {
    this.element = document.createElement('div');
    this.element.id = this.containerId;
    this.element.className = `yt2ai-overlay yt2ai-${this.config.type} ${this.config.className || ''}`;
    
    // Mobile-first responsive design
    this.element.innerHTML = `
      <div class="yt2ai-overlay-backdrop" role="dialog" aria-modal="true">
        <div class="yt2ai-overlay-content">
          <div class="yt2ai-overlay-body">
            ${typeof this.config.content === 'string' ? this.config.content : ''}
          </div>
          <button class="yt2ai-close-btn" aria-label="Close">×</button>
        </div>
      </div>
    `;

    if (typeof this.config.content !== 'string') {
      const bodyEl = this.element.querySelector('.yt2ai-overlay-body');
      bodyEl?.appendChild(this.config.content);
    }
  }

  private attachStyles(): void {
    const styleId = 'yt2ai-overlay-styles';
    if (document.getElementById(styleId)) return;

    const style = document.createElement('style');
    style.id = styleId;
    style.textContent = `
      .yt2ai-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        z-index: 999999;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      }
      .yt2ai-overlay-backdrop {
        background: rgba(0, 0, 0, 0.75);
        width: 100%;
        height: 100%;
        display: flex;
        align-items: ${this.config.position === 'top' ? 'flex-start' : this.config.position === 'bottom' ? 'flex-end' : 'center'};
        justify-content: center;
        padding: 20px;
        box-sizing: border-box;
      }
      .yt2ai-overlay-content {
        background: white;
        border-radius: 12px;
        max-width: 90vw;
        max-height: 80vh;
        position: relative;
        overflow: hidden;
        box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
      }
      .yt2ai-overlay-body {
        padding: 24px;
        font-size: 16px;
        line-height: 1.5;
        color: #333;
      }
      .yt2ai-close-btn {
        position: absolute;
        top: 12px;
        right: 12px;
        background: none;
        border: none;
        font-size: 24px;
        cursor: pointer;
        padding: 8px;
        min-width: 44px;
        min-height: 44px;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      @media (max-width: 768px) {
        .yt2ai-overlay-content {
          max-width: 95vw;
          margin-top: ${this.config.position === 'top' ? '20px' : '0'};
        }
        .yt2ai-overlay-body {
          padding: 20px;
          font-size: 14px;
        }
      }
    `;
    document.head.appendChild(style);
  }

  private bindMobileEvents(): void {
    // Touch-optimized close handling
    const closeBtn = this.element.querySelector('.yt2ai-close-btn');
    const backdrop = this.element.querySelector('.yt2ai-overlay-backdrop');
    
    closeBtn?.addEventListener('click', () => this.hide());
    backdrop?.addEventListener('click', (e) => {
      if (e.target === backdrop) this.hide();
    });

    // Auto-close timer
    if (this.config.autoClose) {
      setTimeout(() => this.hide(), this.config.autoClose);
    }
  }

  public show(): void {
    if (this.isVisible) return;
    document.body.appendChild(this.element);
    this.isVisible = true;
    
    // Prevent body scroll on mobile
    document.body.style.overflow = 'hidden';
  }

  public hide(): void {
    if (!this.isVisible) return;
    this.element.remove();
    this.isVisible = false;
    
    // Restore body scroll
    document.body.style.overflow = '';
  }

  public destroy(): void {
    this.hide();
    // Cleanup styles if no other overlays exist
    if (!document.querySelector('.yt2ai-overlay')) {
      document.getElementById('yt2ai-overlay-styles')?.remove();
    }
  }
}

// Export for modular development
export { MobileOverlayComponent, type OverlayComponent };

Naming Conventions

File Naming:

  • Modules: camelCase.js (e.g., videoExtractor.js, mobileUtils.js)
  • Classes: PascalCase classes in camelCase files (e.g., MobileOverlayComponent in overlayManager.js)
  • Constants: SCREAMING_SNAKE_CASE (e.g., API_ENDPOINTS, MOBILE_BREAKPOINTS)

CSS Naming:

  • Prefix: All CSS classes prefixed with yt2ai- to avoid YouTube conflicts
  • BEM-inspired: yt2ai-block__element--modifier pattern
  • Mobile States: yt2ai-mobile-* for mobile-specific classes

JavaScript Naming:

  • Functions: camelCase with descriptive verbs (e.g., extractVideoId, showMobileError)
  • Private Methods: Leading underscore _privateMethods convention
  • Event Handlers: handle prefix (e.g., handleTouchEnd, handleAPIError)

Mobile-Specific Conventions:

  • Touch Targets: Minimum 44px touch targets for mobile accessibility
  • Viewport Units: Use vw/vh sparingly, prefer rem for consistent scaling
  • Z-index Ranges: 999999+ for bookmarklet overlays to ensure YouTube override

State Management

Store Structure

src/state/
├── BookmarkletState.js          # Main state container
├── WorkflowState.js            # Processing workflow status
├── ErrorState.js               # Error handling and recovery
└── types/
    ├── WorkflowTypes.js        # Workflow step definitions
    └── ErrorTypes.js           # Error classification types

State Management Template

/**
 * Bookmarklet State Management
 * Lightweight module pattern for stateless bookmarklet operation
 */

interface WorkflowStep {
  id: string;
  name: string;
  status: 'pending' | 'active' | 'completed' | 'failed';
  startTime?: number;
  endTime?: number;
  error?: BookmarkletError;
}

interface BookmarkletError {
  type: 'network' | 'api' | 'parsing' | 'captcha' | 'unknown';
  message: string;
  recoverable: boolean;
  retryable: boolean;
  userAction?: string;
}

interface BookmarkletState {
  // Workflow tracking
  currentStep: string | null;
  steps: WorkflowStep[];
  
  // Data state
  videoId: string | null;
  subtitleText: string | null;
  
  // UI state
  isProcessing: boolean;
  showOverlay: boolean;
  
  // Error state
  currentError: BookmarkletError | null;
  retryCount: number;
}

class BookmarkletStateManager {
  private state: BookmarkletState;
  private listeners: Set<(state: BookmarkletState) => void> = new Set();
  
  // Workflow steps definition
  private readonly WORKFLOW_STEPS: Omit<WorkflowStep, 'status' | 'startTime' | 'endTime'>[] = [
    { id: 'video-detection', name: 'Detecting YouTube Video' },
    { id: 'subtitle-extraction', name: 'Extracting Subtitles' },
    { id: 'claude-preparation', name: 'Preparing Claude.ai' },
    { id: 'completion', name: 'Ready for Analysis' }
  ];

  constructor() {
    this.state = this.createInitialState();
  }

  private createInitialState(): BookmarkletState {
    return {
      currentStep: null,
      steps: this.WORKFLOW_STEPS.map(step => ({
        ...step,
        status: 'pending'
      })),
      videoId: null,
      subtitleText: null,
      isProcessing: false,
      showOverlay: false,
      currentError: null,
      retryCount: 0
    };
  }

  // State getters
  public getState(): Readonly<BookmarkletState> {
    return { ...this.state };
  }

  public getCurrentStep(): WorkflowStep | null {
    return this.state.steps.find(step => step.id === this.state.currentStep) || null;
  }

  public getProgress(): { completed: number; total: number; percentage: number } {
    const completed = this.state.steps.filter(step => step.status === 'completed').length;
    const total = this.state.steps.length;
    return { completed, total, percentage: Math.round((completed / total) * 100) };
  }

  // State mutations
  public startWorkflow(): void {
    this.updateState({
      isProcessing: true,
      showOverlay: true,
      currentError: null,
      retryCount: 0
    });
    this.advanceToStep('video-detection');
  }

  public advanceToStep(stepId: string): void {
    const stepIndex = this.state.steps.findIndex(step => step.id === stepId);
    if (stepIndex === -1) return;

    // Complete previous step
    if (this.state.currentStep) {
      this.completeStep(this.state.currentStep);
    }

    // Start new step
    const updatedSteps = [...this.state.steps];
    updatedSteps[stepIndex] = {
      ...updatedSteps[stepIndex],
      status: 'active',
      startTime: Date.now()
    };

    this.updateState({
      currentStep: stepId,
      steps: updatedSteps
    });
  }

  public completeStep(stepId: string): void {
    const updatedSteps = this.state.steps.map(step => 
      step.id === stepId
        ? { ...step, status: 'completed' as const, endTime: Date.now() }
        : step
    );

    this.updateState({ steps: updatedSteps });
  }

  public failStep(stepId: string, error: BookmarkletError): void {
    const updatedSteps = this.state.steps.map(step => 
      step.id === stepId
        ? { ...step, status: 'failed' as const, endTime: Date.now(), error }
        : step
    );

    this.updateState({
      steps: updatedSteps,
      currentError: error,
      isProcessing: false
    });
  }

  public setVideoData(videoId: string): void {
    this.updateState({ videoId });
  }

  public setSubtitleData(subtitleText: string): void {
    this.updateState({ subtitleText });
  }

  public completeWorkflow(): void {
    this.completeStep(this.state.currentStep!);
    this.updateState({
      isProcessing: false,
      currentStep: null
    });
  }

  public retry(): void {
    if (!this.state.currentError?.retryable) return;
    
    this.updateState({
      currentError: null,
      retryCount: this.state.retryCount + 1,
      isProcessing: true
    });

    // Restart from failed step
    const failedStep = this.state.steps.find(step => step.status === 'failed');
    if (failedStep) {
      this.advanceToStep(failedStep.id);
    }
  }

  public reset(): void {
    this.state = this.createInitialState();
    this.notifyListeners();
  }

  // State subscription
  public subscribe(listener: (state: BookmarkletState) => void): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  private updateState(updates: Partial<BookmarkletState>): void {
    this.state = { ...this.state, ...updates };
    this.notifyListeners();
  }

  private notifyListeners(): void {
    this.listeners.forEach(listener => listener(this.getState()));
  }

  // Utility methods
  public getDuration(stepId: string): number | null {
    const step = this.state.steps.find(s => s.id === stepId);
    if (!step?.startTime) return null;
    const endTime = step.endTime || Date.now();
    return endTime - step.startTime;
  }

  public canRetry(): boolean {
    return !!(this.state.currentError?.retryable && this.state.retryCount < 3);
  }

  // Mobile-specific state helpers
  public shouldShowMobileOptimizedError(): boolean {
    return !!(this.state.currentError && window.innerWidth <= 768);
  }

  public getEstimatedTimeRemaining(): number {
    // Simple estimation based on current step and average durations
    const averageDurations = {
      'video-detection': 2000,
      'subtitle-extraction': 15000,
      'claude-preparation': 3000,
      'completion': 1000
    };

    let remaining = 0;
    let foundCurrent = false;

    for (const step of this.state.steps) {
      if (step.id === this.state.currentStep) {
        foundCurrent = true;
        const elapsed = step.startTime ? Date.now() - step.startTime : 0;
        remaining += Math.max(0, averageDurations[step.id as keyof typeof averageDurations] - elapsed);
      } else if (foundCurrent && step.status === 'pending') {
        remaining += averageDurations[step.id as keyof typeof averageDurations] || 5000;
      }
    }

    return remaining;
  }
}

// Singleton instance for bookmarklet use
const stateManager = new BookmarkletStateManager();

// Export for modular development
export { stateManager, type BookmarkletState, type WorkflowStep, type BookmarkletError };

API Integration

Service Template

/**
 * API Service Template for Bookmarklet
 * Mobile-optimized with comprehensive error handling
 */

interface APIResponse<T> {
  success: boolean;
  data?: T;
  error?: BookmarkletError;
}

interface SubtitleData {
  videoId: string;
  subtitles: string;
  language: string;
  duration: number;
}

interface APIRequestConfig {
  timeout: number;
  retries: number;
  mobile: boolean;
}

class BookmarkletAPIService {
  private readonly BASE_ENDPOINTS = {
    subtitles: 'https://downloadyoutubesubtitles.com/api/subtitles',
    health: 'https://downloadyoutubesubtitles.com/api/health'
  };

  private readonly DEFAULT_CONFIG: APIRequestConfig = {
    timeout: 30000, // 30s for mobile networks
    retries: 2,
    mobile: true
  };

  private readonly MOBILE_HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 Chrome/91.0',
    'Accept': 'application/json, text/plain, */*',
    'Accept-Language': 'en-US,en;q=0.9',
    'Cache-Control': 'no-cache'
  };

  /**
   * Extract subtitles for YouTube video
   */
  public async extractSubtitles(videoId: string): Promise<APIResponse<SubtitleData>> {
    try {
      stateManager.advanceToStep('subtitle-extraction');
      
      const response = await this.makeRequest<SubtitleData>(
        `${this.BASE_ENDPOINTS.subtitles}/${videoId}`,
        {
          method: 'GET',
          params: {
            lang: 'en',
            auto: 'true', // Auto-generated subtitles only
            format: 'txt'
          }
        }
      );

      if (!response.success || !response.data) {
        return {
          success: false,
          error: {
            type: 'api',
            message: 'Failed to extract subtitles',
            recoverable: true,
            retryable: true,
            userAction: 'Check if video has English auto-generated subtitles'
          }
        };
      }

      // Validate subtitle data quality
      const subtitleData = response.data;
      if (!subtitleData.subtitles || subtitleData.subtitles.length < 50) {
        return {
          success: false,
          error: {
            type: 'parsing',
            message: 'Subtitle content too short or empty',
            recoverable: false,
            retryable: false,
            userAction: 'Try a different video with longer content'
          }
        };
      }

      stateManager.setSubtitleData(subtitleData.subtitles);
      stateManager.completeStep('subtitle-extraction');

      return { success: true, data: subtitleData };

    } catch (error) {
      return this.handleAPIError(error as Error, 'extractSubtitles');
    }
  }

  /**
   * Check API health and availability
   */
  public async checkAPIHealth(): Promise<APIResponse<{ status: string; responseTime: number }>> {
    const startTime = Date.now();
    
    try {
      const response = await this.makeRequest<{ status: string }>(
        this.BASE_ENDPOINTS.health,
        { method: 'GET' },
        { ...this.DEFAULT_CONFIG, timeout: 5000 } // Quick health check
      );

      return {
        success: true,
        data: {
          status: response.data?.status || 'unknown',
          responseTime: Date.now() - startTime
        }
      };
    } catch (error) {
      return {
        success: false,
        error: {
          type: 'network',
          message: 'API service unavailable',
          recoverable: true,
          retryable: true,
          userAction: 'Try again in a few minutes'
        }
      };
    }
  }

  /**
   * Generic HTTP request handler with mobile optimizations
   */
  private async makeRequest<T>(
    url: string,
    options: {
      method: 'GET' | 'POST';
      body?: string;
      params?: Record<string, string>;
    },
    config: APIRequestConfig = this.DEFAULT_CONFIG
  ): Promise<APIResponse<T>> {
    
    // Build URL with parameters
    const urlWithParams = this.buildURL(url, options.params);
    
    // Create AbortController for timeout handling
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), config.timeout);

    try {
      const response = await fetch(urlWithParams, {
        method: options.method,
        headers: {
          ...this.MOBILE_HEADERS,
          ...(options.body ? { 'Content-Type': 'application/json' } : {})
        },
        body: options.body,
        signal: controller.signal,
        mode: 'cors', // Handle CORS for bookmarklet
        credentials: 'omit' // No cookies needed
      });

      clearTimeout(timeoutId);

      // Handle specific HTTP status codes
      if (response.status === 429) {
        throw new Error('Rate limit exceeded - too many requests');
      }

      if (response.status === 503) {
        throw new Error('Service temporarily unavailable');
      }

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      // Handle potential CAPTCHA response
      const contentType = response.headers.get('content-type');
      if (contentType?.includes('text/html')) {
        // Likely CAPTCHA challenge
        throw new Error('CAPTCHA verification required');
      }

      const data = await response.json();
      return { success: true, data };

    } catch (error) {
      clearTimeout(timeoutId);
      throw error;
    }
  }

  private buildURL(baseUrl: string, params?: Record<string, string>): string {
    if (!params) return baseUrl;
    
    const url = new URL(baseUrl);
    Object.entries(params).forEach(([key, value]) => {
      url.searchParams.set(key, value);
    });
    
    return url.toString();
  }

  private handleAPIError(error: Error, context: string): APIResponse<never> {
    console.error(`[BookmarkletAPI] Error in ${context}:`, error);

    // Network/timeout errors
    if (error.name === 'AbortError') {
      return {
        success: false,
        error: {
          type: 'network',
          message: 'Request timeout - slow network connection',
          recoverable: true,
          retryable: true,
          userAction: 'Check your internet connection and try again'
        }
      };
    }

    // CAPTCHA detection
    if (error.message.includes('CAPTCHA')) {
      return {
        success: false,
        error: {
          type: 'captcha',
          message: 'CAPTCHA verification required',
          recoverable: true,
          retryable: true,
          userAction: 'Complete CAPTCHA verification and try again'
        }
      };
    }

    // Rate limiting
    if (error.message.includes('Rate limit') || error.message.includes('429')) {
      return {
        success: false,
        error: {
          type: 'api',
          message: 'Too many requests - please wait',
          recoverable: true,
          retryable: true,
          userAction: 'Wait a few minutes before trying again'
        }
      };
    }

    // Generic network error
    return {
      success: false,
      error: {
        type: 'network',
        message: `Network error: ${error.message}`,
        recoverable: true,
        retryable: true,
        userAction: 'Check your connection and try again'
      }
    };
  }

  /**
   * Mobile-specific retry logic with exponential backoff
   */
  public async withRetry<T>(
    operation: () => Promise<APIResponse<T>>,
    maxRetries: number = 2
  ): Promise<APIResponse<T>> {
    let lastError: BookmarkletError | null = null;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      if (attempt > 0) {
        // Exponential backoff: 1s, 2s, 4s
        const delay = Math.pow(2, attempt - 1) * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
      }

      const result = await operation();
      
      if (result.success) {
        return result;
      }

      lastError = result.error!;
      
      // Don't retry non-retryable errors
      if (!result.error?.retryable) {
        break;
      }
    }

    return {
      success: false,
      error: {
        ...lastError!,
        message: `${lastError!.message} (after ${maxRetries + 1} attempts)`
      }
    };
  }
}

// Singleton instance
const apiService = new BookmarkletAPIService();

export { apiService, type APIResponse, type SubtitleData };

API Client Configuration

/**
 * Claude.ai Integration Configuration
 * Handles tab management and clipboard operations
 */

interface ClaudeConfig {
  baseUrl: string;
  promptTemplate: string;
  tabManagement: {
    openInNewTab: boolean;
    focusTab: boolean;
    closeOnComplete: boolean;
  };
}

class ClaudeIntegrationService {
  private readonly config: ClaudeConfig = {
    baseUrl: 'https://claude.ai/chat',
    promptTemplate: this.buildPromptTemplate(),
    tabManagement: {
      openInNewTab: true,
      focusTab: false, // Keep YouTube tab active on mobile
      closeOnComplete: false
    }
  };

  /**
   * Open Claude.ai with formatted subtitle content
   */
  public async openClaudeWithSubtitles(videoId: string, subtitles: string): Promise<APIResponse<{ tabId?: string }>> {
    try {
      stateManager.advanceToStep('claude-preparation');

      const formattedPrompt = this.formatPrompt(videoId, subtitles);
      
      // Copy to clipboard first (primary method)
      await this.copyToClipboard(formattedPrompt);
      
      // Open Claude.ai tab
      const claudeTab = this.openClaudeTab();
      
      stateManager.completeStep('claude-preparation');
      stateManager.completeWorkflow();

      return {
        success: true,
        data: { tabId: claudeTab?.id }
      };

    } catch (error) {
      return {
        success: false,
        error: {
          type: 'unknown',
          message: `Claude integration failed: ${(error as Error).message}`,
          recoverable: true,
          retryable: true,
          userAction: 'Try manually opening Claude.ai and pasting the content'
        }
      };
    }
  }

  private async copyToClipboard(text: string): Promise<void> {
    try {
      // Modern clipboard API (preferred)
      if (navigator.clipboard && navigator.clipboard.writeText) {
        await navigator.clipboard.writeText(text);
        return;
      }

      // Fallback for older browsers
      const textArea = document.createElement('textarea');
      textArea.value = text;
      textArea.style.position = 'fixed';
      textArea.style.left = '-999999px';
      textArea.style.top = '-999999px';
      document.body.appendChild(textArea);
      textArea.focus();
      textArea.select();
      
      const successful = document.execCommand('copy');
      document.body.removeChild(textArea);
      
      if (!successful) {
        throw new Error('Clipboard copy failed');
      }
    } catch (error) {
      throw new Error(`Clipboard operation failed: ${(error as Error).message}`);
    }
  }

  private openClaudeTab(): Window | null {
    return window.open(
      this.config.baseUrl,
      '_blank',
      'noopener,noreferrer'
    );
  }

  private formatPrompt(videoId: string, subtitles: string): string {
    const template = this.config.promptTemplate;
    return template
      .replace('{VIDEO_ID}', videoId)
      .replace('{SUBTITLES}', subtitles)
      .replace('{TIMESTAMP}', new Date().toISOString());
  }

  private buildPromptTemplate(): string {
    return `YouTube Video Analysis Request

Video ID: {VIDEO_ID}
Generated: {TIMESTAMP}

Please analyze the following YouTube video subtitles and provide a structured summary in exactly these four sections:

## Overview
Brief summary of what this video covers and main topic

## Essential Points  
Key insights, facts, or concepts presented (bullet points)

## Value Proposition
Why someone should watch this video - what they'll gain

## Beginner Summary
Simplified explanation suitable for someone new to the topic

---

SUBTITLES:
{SUBTITLES}

---

Please focus on actionable insights and practical value for educational content consumption decisions.`;
  }
}

// Singleton instance
const claudeService = new ClaudeIntegrationService();

export { claudeService, type ClaudeConfig };

Routing

Route Configuration

/**
 * Bookmarklet Routing Configuration
 * Context-aware navigation for YouTube integration
 */

interface RouteContext {
  youtubeUrl: string;
  videoId: string | null;
  pageType: 'watch' | 'shorts' | 'embed' | 'playlist' | 'unknown';
  isMobile: boolean;
}

interface RouteHandler {
  canHandle: (context: RouteContext) => boolean;
  execute: (context: RouteContext) => Promise<void>;
  errorFallback?: (error: Error, context: RouteContext) => Promise<void>;
}

class BookmarkletRouter {
  private handlers: Map<string, RouteHandler> = new Map();
  private readonly YOUTUBE_PATTERNS = {
    watch: /(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/,
    shorts: /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/,
    embed: /youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/,
    playlist: /youtube\.com\/playlist\?list=([a-zA-Z0-9_-]+)/
  };

  constructor() {
    this.registerDefaultHandlers();
  }

  /**
   * Main routing entry point - analyzes current page and executes appropriate handler
   */
  public async route(): Promise<void> {
    try {
      const context = this.analyzeCurrentContext();
      
      // Find matching handler
      const handler = this.findHandler(context);
      if (!handler) {
        throw new Error(`No handler found for context: ${context.pageType}`);
      }

      // Execute handler
      await handler.execute(context);

    } catch (error) {
      await this.handleRoutingError(error as Error);
    }
  }

  /**
   * Analyze current page context
   */
  private analyzeCurrentContext(): RouteContext {
    const currentUrl = window.location.href;
    const isMobile = window.innerWidth <= 768 || /Mobile|Android/i.test(navigator.userAgent);

    // Determine page type and extract video ID
    let pageType: RouteContext['pageType'] = 'unknown';
    let videoId: string | null = null;

    for (const [type, pattern] of Object.entries(this.YOUTUBE_PATTERNS)) {
      const match = currentUrl.match(pattern);
      if (match) {
        pageType = type as RouteContext['pageType'];
        if (type !== 'playlist') {
          videoId = match[1];
        }
        break;
      }
    }

    return {
      youtubeUrl: currentUrl,
      videoId,
      pageType,
      isMobile
    };
  }

  /**
   * Find appropriate handler for context
   */
  private findHandler(context: RouteContext): RouteHandler | null {
    for (const [name, handler] of this.handlers) {
      if (handler.canHandle(context)) {
        console.log(`[Router] Using handler: ${name}`);
        return handler;
      }
    }
    return null;
  }

  /**
   * Register default handlers for different YouTube contexts
   */
  private registerDefaultHandlers(): void {
    
    // Standard YouTube video watch page
    this.registerHandler('youtube-watch', {
      canHandle: (context) => context.pageType === 'watch' && !!context.videoId,
      execute: async (context) => {
        stateManager.startWorkflow();
        stateManager.setVideoData(context.videoId!);
        
        // Standard workflow: extract → claude
        const subtitleResult = await apiService.extractSubtitles(context.videoId!);
        if (!subtitleResult.success) {
          throw new Error(subtitleResult.error!.message);
        }
        
        await claudeService.openClaudeWithSubtitles(context.videoId!, subtitleResult.data!.subtitles);
      }
    });

    // YouTube Shorts
    this.registerHandler('youtube-shorts', {
      canHandle: (context) => context.pageType === 'shorts' && !!context.videoId,
      execute: async (context) => {
        // Shorts often have limited or no subtitles
        const confirmShorts = confirm(
          'YouTube Shorts videos often have limited subtitles. Continue anyway?'
        );
        
        if (!confirmShorts) return;
        
        // Same workflow as regular videos
        await this.handlers.get('youtube-watch')!.execute(context);
      }
    });

    // Embedded YouTube videos
    this.registerHandler('youtube-embed', {
      canHandle: (context) => context.pageType === 'embed' && !!context.videoId,
      execute: async (context) => {
        // For embedded videos, we might need different handling
        stateManager.startWorkflow();
        stateManager.setVideoData(context.videoId!);
        
        const subtitleResult = await apiService.extractSubtitles(context.videoId!);
        if (!subtitleResult.success) {
          throw new Error(subtitleResult.error!.message);
        }
        
        await claudeService.openClaudeWithSubtitles(context.videoId!, subtitleResult.data!.subtitles);
      }
    });

    // Playlist pages (no specific video)
    this.registerHandler('youtube-playlist', {
      canHandle: (context) => context.pageType === 'playlist',
      execute: async (context) => {
        throw new Error('Please navigate to a specific video to analyze. Playlist analysis is not supported.');
      }
    });

    // Mobile-optimized fallback
    this.registerHandler('mobile-fallback', {
      canHandle: (context) => context.isMobile && !context.videoId,
      execute: async (context) => {
        throw new Error('Unable to detect video on this mobile page. Please ensure you\'re on a YouTube video page.');
      }
    });

    // Generic error handler
    this.registerHandler('error-fallback', {
      canHandle: () => true, // Always matches as last resort
      execute: async (context) => {
        throw new Error(`Unsupported YouTube page: ${context.pageType}. Please navigate to a video page.`);
      }
    });
  }

  /**
   * Register a new route handler
   */
  public registerHandler(name: string, handler: RouteHandler): void {
    this.handlers.set(name, handler);
  }

  /**
   * Handle routing errors with user-friendly messages
   */
  private async handleRoutingError(error: Error): Promise<void> {
    console.error('[Router] Routing error:', error);

    const errorOverlay = new MobileOverlayComponent({
      id: 'routing-error',
      type: 'error',
      content: `
        <h3>Unable to Process Video</h3>
        <p>${error.message}</p>
        <div class="yt2ai-error-actions">
          <button class="yt2ai-btn-primary" onclick="window.location.reload()">
            Refresh Page
          </button>
          <button class="yt2ai-btn-secondary" onclick="this.closest('.yt2ai-overlay').remove()">
            Close
          </button>
        </div>
        <style>
          .yt2ai-error-actions {
            margin-top: 20px;
            display: flex;
            gap: 12px;
            flex-wrap: wrap;
          }
          .yt2ai-btn-primary, .yt2ai-btn-secondary {
            padding: 12px 20px;
            border: none;
            border-radius: 8px;
            font-size: 14px;
            cursor: pointer;
            min-height: 44px;
            flex: 1;
            min-width: 120px;
          }
          .yt2ai-btn-primary {
            background: #1976d2;
            color: white;
          }
          .yt2ai-btn-secondary {
            background: #f5f5f5;
            color: #333;
          }
          @media (max-width: 480px) {
            .yt2ai-error-actions {
              flex-direction: column;
            }
          }
        </style>
      `,
      autoClose: 10000
    });

    errorOverlay.show();
    stateManager.reset();
  }

  /**
   * Utility: Extract video ID from various YouTube URL formats
   */
  public static extractVideoId(url: string): string | null {
    const patterns = [
      /(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/,
      /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/,
      /youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/
    ];

    for (const pattern of patterns) {
      const match = url.match(pattern);
      if (match) return match[1];
    }

    return null;
  }

  /**
   * Utility: Check if current page is a valid YouTube video page
   */
  public static isValidYouTubeVideoPage(): boolean {
    const videoId = BookmarkletRouter.extractVideoId(window.location.href);
    return !!videoId;
  }
}

// Singleton router instance
const router = new BookmarkletRouter();

// Main bookmarklet entry point
async function initializeBookmarklet(): Promise<void> {
  try {
    // Verify we're on YouTube
    if (!window.location.hostname.includes('youtube.com')) {
      throw new Error('This bookmarklet only works on YouTube pages');
    }

    // Route based on current context
    await router.route();

  } catch (error) {
    console.error('[Bookmarklet] Initialization failed:', error);
    
    // Show user-friendly error
    alert(`YT2AI Error: ${(error as Error).message}`);
  }
}

export { router, initializeBookmarklet, BookmarkletRouter };

Styling Guidelines

Styling Approach

The bookmarklet uses Inline CSS-in-JS approach with mobile-first responsive design principles. This ensures zero external dependencies while providing YouTube-compatible styling that works across different mobile Chrome versions.

Key Methodologies:

  • Mobile-First Progressive Enhancement: Base styles for mobile (320px+), enhanced for larger screens
  • CSS-in-JS with Template Literals: Dynamic styling based on context and state
  • YouTube Integration-Safe: High z-index values and namespaced classes to avoid conflicts
  • Touch-Optimized: Minimum 44px touch targets, gesture-friendly interactions

Global Theme Variables

:root {
  /* === COLOR SYSTEM === */
  
  /* Primary Colors - YouTube Compatible */
  --yt2ai-primary: #1976d2;
  --yt2ai-primary-hover: #1565c0;
  --yt2ai-primary-light: #e3f2fd;
  --yt2ai-primary-dark: #0d47a1;

  /* Semantic Colors */
  --yt2ai-success: #4caf50;
  --yt2ai-success-light: #e8f5e8;
  --yt2ai-warning: #ff9800;
  --yt2ai-warning-light: #fff3e0;
  --yt2ai-error: #f44336;
  --yt2ai-error-light: #ffebee;
  --yt2ai-info: #2196f3;
  --yt2ai-info-light: #e3f2fd;

  /* Neutral Colors */
  --yt2ai-white: #ffffff;
  --yt2ai-gray-50: #fafafa;
  --yt2ai-gray-100: #f5f5f5;
  --yt2ai-gray-200: #eeeeee;
  --yt2ai-gray-300: #e0e0e0;
  --yt2ai-gray-400: #bdbdbd;
  --yt2ai-gray-500: #9e9e9e;
  --yt2ai-gray-600: #757575;
  --yt2ai-gray-700: #616161;
  --yt2ai-gray-800: #424242;
  --yt2ai-gray-900: #212121;
  --yt2ai-black: #000000;

  /* === SPACING SYSTEM === */
  
  /* Base spacing unit: 4px */
  --yt2ai-space-1: 4px;   /* xs */
  --yt2ai-space-2: 8px;   /* sm */
  --yt2ai-space-3: 12px;  /* md */
  --yt2ai-space-4: 16px;  /* lg */
  --yt2ai-space-5: 20px;  /* xl */
  --yt2ai-space-6: 24px;  /* 2xl */
  --yt2ai-space-8: 32px;  /* 3xl */
  --yt2ai-space-10: 40px; /* 4xl */
  --yt2ai-space-12: 48px; /* 5xl */
  --yt2ai-space-16: 64px; /* 6xl */

  /* Mobile-specific spacing */
  --yt2ai-mobile-padding: var(--yt2ai-space-4);
  --yt2ai-mobile-margin: var(--yt2ai-space-3);
  --yt2ai-touch-target: 44px; /* Minimum touch target */

  /* === TYPOGRAPHY === */
  
  /* Font families */
  --yt2ai-font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', 
                       Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
  --yt2ai-font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', 
                     Consolas, 'Courier New', monospace;

  /* Font sizes - Mobile first */
  --yt2ai-text-xs: 12px;   /* Small text, captions */
  --yt2ai-text-sm: 14px;   /* Body text mobile */
  --yt2ai-text-base: 16px; /* Body text desktop */
  --yt2ai-text-lg: 18px;   /* Large body text */
  --yt2ai-text-xl: 20px;   /* Small headings */
  --yt2ai-text-2xl: 24px;  /* Medium headings */
  --yt2ai-text-3xl: 30px;  /* Large headings */

  /* Line heights */
  --yt2ai-leading-tight: 1.25;
  --yt2ai-leading-normal: 1.5;
  --yt2ai-leading-relaxed: 1.625;

  /* Font weights */
  --yt2ai-font-normal: 400;
  --yt2ai-font-medium: 500;
  --yt2ai-font-semibold: 600;
  --yt2ai-font-bold: 700;

  /* === SHADOWS === */
  
  /* Elevation system */
  --yt2ai-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --yt2ai-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
  --yt2ai-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
  --yt2ai-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
  --yt2ai-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);

  /* Mobile-optimized shadows */
  --yt2ai-shadow-mobile: 0 4px 12px rgb(0 0 0 / 0.15);
  --yt2ai-shadow-modal: 0 25px 50px -12px rgb(0 0 0 / 0.25);

  /* === BORDERS === */
  
  --yt2ai-border-width: 1px;
  --yt2ai-border-color: var(--yt2ai-gray-300);
  --yt2ai-border-radius-sm: 4px;
  --yt2ai-border-radius: 8px;
  --yt2ai-border-radius-md: 12px;
  --yt2ai-border-radius-lg: 16px;
  --yt2ai-border-radius-full: 9999px;

  /* Mobile-optimized border radius */
  --yt2ai-border-radius-mobile: var(--yt2ai-border-radius-md);

  /* === TRANSITIONS === */
  
  --yt2ai-transition-fast: 150ms ease;
  --yt2ai-transition: 200ms ease;
  --yt2ai-transition-slow: 300ms ease;
  
  /* Mobile-optimized transitions */
  --yt2ai-transition-mobile: 200ms cubic-bezier(0.4, 0, 0.2, 1);
  --yt2ai-transition-spring: 300ms cubic-bezier(0.68, -0.55, 0.265, 1.55);

  /* === Z-INDEX LAYERS === */
  
  --yt2ai-z-dropdown: 1000;
  --yt2ai-z-sticky: 1020;
  --yt2ai-z-fixed: 1030;
  --yt2ai-z-modal-backdrop: 1040;
  --yt2ai-z-modal: 1050;
  --yt2ai-z-popover: 1060;
  --yt2ai-z-tooltip: 1070;
  --yt2ai-z-bookmarklet: 999999; /* Override YouTube */

  /* === DARK MODE SUPPORT === */
  
  /* Auto dark mode based on system preference */
  --yt2ai-bg-primary: var(--yt2ai-white);
  --yt2ai-bg-secondary: var(--yt2ai-gray-50);
  --yt2ai-text-primary: var(--yt2ai-gray-900);
  --yt2ai-text-secondary: var(--yt2ai-gray-600);
  --yt2ai-border-primary: var(--yt2ai-gray-300);
}

/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
  :root {
    --yt2ai-bg-primary: var(--yt2ai-gray-900);
    --yt2ai-bg-secondary: var(--yt2ai-gray-800);
    --yt2ai-text-primary: var(--yt2ai-gray-100);
    --yt2ai-text-secondary: var(--yt2ai-gray-400);
    --yt2ai-border-primary: var(--yt2ai-gray-600);
    
    /* Adjust shadows for dark mode */
    --yt2ai-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
    --yt2ai-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3);
    --yt2ai-shadow-mobile: 0 4px 12px rgb(0 0 0 / 0.4);
    --yt2ai-shadow-modal: 0 25px 50px -12px rgb(0 0 0 / 0.5);
  }
}

/* === MOBILE BREAKPOINTS === */

/* Extra small devices */
@media (max-width: 374px) {
  :root {
    --yt2ai-text-sm: 13px;
    --yt2ai-text-base: 15px;
    --yt2ai-mobile-padding: var(--yt2ai-space-3);
  }
}

/* Small devices (landscape phones) */
@media (min-width: 375px) and (max-width: 667px) {
  :root {
    --yt2ai-mobile-padding: var(--yt2ai-space-4);
  }
}

/* Medium devices (tablets) */
@media (min-width: 668px) and (max-width: 1024px) {
  :root {
    --yt2ai-text-base: 16px;
    --yt2ai-mobile-padding: var(--yt2ai-space-5);
  }
}

/* Large devices (desktop) */
@media (min-width: 1025px) {
  :root {
    --yt2ai-text-base: 16px;
    --yt2ai-mobile-padding: var(--yt2ai-space-6);
  }
}

/* === UTILITY CLASSES === */

/* Mobile-first responsive utilities */
.yt2ai-mobile-only { display: block; }
.yt2ai-desktop-only { display: none; }

@media (min-width: 768px) {
  .yt2ai-mobile-only { display: none; }
  .yt2ai-desktop-only { display: block; }
}

/* Touch-optimized utilities */
.yt2ai-touch-target {
  min-height: var(--yt2ai-touch-target);
  min-width: var(--yt2ai-touch-target);
  display: flex;
  align-items: center;
  justify-content: center;
}

.yt2ai-touch-friendly {
  padding: var(--yt2ai-space-3) var(--yt2ai-space-4);
  margin: var(--yt2ai-space-2);
  border-radius: var(--yt2ai-border-radius-mobile);
}

/* Accessibility utilities */
.yt2ai-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.yt2ai-focus-visible:focus-visible {
  outline: 2px solid var(--yt2ai-primary);
  outline-offset: 2px;
}

/* High contrast mode support */
@media (prefers-contrast: high) {
  :root {
    --yt2ai-border-width: 2px;
  }
  
  .yt2ai-overlay {
    border: var(--yt2ai-border-width) solid var(--yt2ai-border-primary);
  }
}

/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
  :root {
    --yt2ai-transition-fast: 0ms;
    --yt2ai-transition: 0ms;
    --yt2ai-transition-slow: 0ms;
    --yt2ai-transition-mobile: 0ms;
    --yt2ai-transition-spring: 0ms;
  }
}

Testing Requirements

Component Test Template

/**
 * Mobile-Optimized Component Testing Template
 * Jest + jsdom for bookmarklet testing
 */

import { jest } from '@jest/globals';
import { MobileOverlayComponent } from '../src/ui/overlayManager.js';
import { stateManager } from '../src/state/BookmarkletState.js';
import { apiService } from '../src/core/subtitleFetcher.js';

// Mock mobile environment
const mockMobileEnvironment = () => {
  // Mock mobile user agent
  Object.defineProperty(navigator, 'userAgent', {
    writable: true,
    value: 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36'
  });

  // Mock mobile viewport
  Object.defineProperty(window, 'innerWidth', { writable: true, value: 375 });
  Object.defineProperty(window, 'innerHeight', { writable: true, value: 667 });

  // Mock touch events
  window.TouchEvent = class TouchEvent extends Event {
    constructor(type: string, options: any) {
      super(type, options);
    }
  };

  // Mock clipboard API
  Object.defineProperty(navigator, 'clipboard', {
    value: {
      writeText: jest.fn().mockResolvedValue(undefined),
      readText: jest.fn().mockResolvedValue('test content')
    }
  });
};

// Mock YouTube environment
const mockYouTubeEnvironment = () => {
  // Mock YouTube URL
  Object.defineProperty(window, 'location', {
    value: {
      href: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
      hostname: 'youtube.com'
    }
  });

  // Mock YouTube DOM elements
  document.body.innerHTML = `
    <div id="movie_player"></div>
    <div class="ytd-watch-flexy"></div>
  `;
};

describe('MobileOverlayComponent', () => {
  beforeEach(() => {
    mockMobileEnvironment();
    document.body.innerHTML = '';
    jest.clearAllMocks();
  });

  afterEach(() => {
    // Cleanup any created overlays
    document.querySelectorAll('.yt2ai-overlay').forEach(el => el.remove());
  });

  describe('Component Creation', () => {
    test('creates mobile-optimized overlay with correct structure', () => {
      const overlay = new MobileOverlayComponent({
        id: 'test-overlay',
        type: 'loading',
        content: 'Test content'
      });

      expect(overlay.element).toBeDefined();
      expect(overlay.element.className).toContain('yt2ai-overlay');
      expect(overlay.element.className).toContain('yt2ai-loading');
      expect(overlay.isVisible).toBe(false);
    });

    test('applies mobile-specific styling', () => {
      const overlay = new MobileOverlayComponent({
        id: 'mobile-test',
        type: 'info',
        content: 'Mobile test'
      });

      overlay.show();
      
      const content = overlay.element.querySelector('.yt2ai-overlay-content');
      const styles = window.getComputedStyle(content);
      
      // Should have mobile-optimized max-width
      expect(styles.maxWidth).toBe('95vw');
    });

    test('handles touch events correctly', async () => {
      const overlay = new MobileOverlayComponent({
        id: 'touch-test',
        type: 'error',
        content: 'Touch test'
      });

      overlay.show();
      
      const closeBtn = overlay.element.querySelector('.yt2ai-close-btn');
      expect(closeBtn).toBeDefined();
      
      // Mock touch event
      const touchEvent = new TouchEvent('touchend', {
        bubbles: true,
        cancelable: true,
        touches: []
      });

      closeBtn.dispatchEvent(touchEvent);
      
      // Should close on touch
      expect(overlay.isVisible).toBe(false);
    });
  });

  describe('Mobile Responsiveness', () => {
    test('adapts to small mobile screens', () => {
      // Mock very small screen
      Object.defineProperty(window, 'innerWidth', { value: 320 });
      
      const overlay = new MobileOverlayComponent({
        id: 'small-screen',
        type: 'success',
        content: 'Small screen test'
      });

      overlay.show();
      
      const content = overlay.element.querySelector('.yt2ai-overlay-content');
      const bodyEl = overlay.element.querySelector('.yt2ai-overlay-body');
      
      // Should adjust padding for small screens
      expect(window.getComputedStyle(bodyEl).padding).toBe('20px');
    });

    test('provides adequate touch targets', () => {
      const overlay = new MobileOverlayComponent({
        id: 'touch-target-test',
        type: 'warning',
        content: 'Touch target test'
      });

      overlay.show();
      
      const closeBtn = overlay.element.querySelector('.yt2ai-close-btn');
      const styles = window.getComputedStyle(closeBtn);
      
      // Should meet minimum touch target size (44px)
      expect(parseInt(styles.minWidth)).toBeGreaterThanOrEqual(44);
      expect(parseInt(styles.minHeight)).toBeGreaterThanOrEqual(44);
    });
  });

  describe('Error Handling', () => {
    test('handles invalid content gracefully', () => {
      expect(() => {
        new MobileOverlayComponent({
          id: 'invalid-test',
          type: 'error',
          content: null as any
        });
      }).not.toThrow();
    });

    test('cleans up properly on destroy', () => {
      const overlay = new MobileOverlayComponent({
        id: 'cleanup-test',
        type: 'info',
        content: 'Cleanup test'
      });

      overlay.show();
      expect(document.querySelector('.yt2ai-overlay')).toBeDefined();
      
      overlay.destroy();
      expect(document.querySelector('.yt2ai-overlay')).toBeNull();
      expect(document.body.style.overflow).toBe('');
    });
  });
});

Testing Best Practices

  1. Unit Tests: Test individual components in isolation with mobile-specific mocking
  2. Integration Tests: Test component interactions with realistic mobile environment simulation
  3. E2E Tests: Test critical user flows using manual testing protocols for mobile Chrome
  4. Coverage Goals: Aim for 80% code coverage with focus on mobile-critical paths
  5. Test Structure: Arrange-Act-Assert pattern with mobile environment setup
  6. Mock External Dependencies: API calls, routing, state management with mobile considerations

Environment Configuration

Runtime Environment Configuration

/**
 * Bookmarklet Environment Configuration
 * Runtime detection and feature flags for different contexts
 */

interface EnvironmentConfig {
  // Runtime environment detection
  isDevelopment: boolean;
  isProduction: boolean;
  isMobile: boolean;
  isAndroid: boolean;
  isChrome: boolean;
  
  // Feature flags
  features: {
    enableDebugLogging: boolean;
    enablePerformanceTracking: boolean;
    enableErrorReporting: boolean;
    enableRPAAutomation: boolean;
    enableCAPTCHAHandling: boolean;
    enableRetryLogic: boolean;
  };
  
  // API configuration
  api: {
    subtitleService: {
      baseUrl: string;
      timeout: number;
      retries: number;
    };
    claude: {
      baseUrl: string;
      features: string[];
    };
  };
  
  // Mobile-specific settings
  mobile: {
    touchTargetSize: number;
    maxOverlayWidth: string;
    animationDuration: number;
    networkTimeout: number;
  };
  
  // Debug configuration
  debug: {
    logLevel: 'error' | 'warn' | 'info' | 'debug';
    enableStateLogging: boolean;
    enableAPILogging: boolean;
    enableUILogging: boolean;
  };
}

class EnvironmentManager {
  private config: EnvironmentConfig;
  
  constructor() {
    this.config = this.detectEnvironment();
  }

  /**
   * Detect runtime environment and configure accordingly
   */
  private detectEnvironment(): EnvironmentConfig {
    // Development detection (presence of specific debug markers)
    const isDevelopment = this.isDevelopmentMode();
    const isProduction = !isDevelopment;
    
    // Mobile/device detection
    const isMobile = this.isMobileDevice();
    const isAndroid = this.isAndroidDevice();
    const isChrome = this.isChromeeBrowser();
    
    return {
      isDevelopment,
      isProduction,
      isMobile,
      isAndroid,
      isChrome,
      
      features: {
        enableDebugLogging: isDevelopment,
        enablePerformanceTracking: isDevelopment || this.hasURLFlag('perf'),
        enableErrorReporting: isProduction,
        enableRPAAutomation: false, // Phase 2 feature
        enableCAPTCHAHandling: true,
        enableRetryLogic: true
      },
      
      api: {
        subtitleService: {
          baseUrl: 'https://downloadyoutubesubtitles.com/api',
          timeout: isMobile ? 30000 : 15000,
          retries: isMobile ? 3 : 2
        },
        claude: {
          baseUrl: 'https://claude.ai/chat',
          features: ['new-tab', 'clipboard-fallback']
        }
      },
      
      mobile: {
        touchTargetSize: 44, // WCAG AA compliance
        maxOverlayWidth: isMobile ? '95vw' : '80vw',
        animationDuration: this.hasReducedMotion() ? 0 : 200,
        networkTimeout: isAndroid ? 35000 : 30000 // Android can be slower
      },
      
      debug: {
        logLevel: isDevelopment ? 'debug' : 'error',
        enableStateLogging: isDevelopment,
        enableAPILogging: isDevelopment || this.hasURLFlag('api-debug'),
        enableUILogging: isDevelopment || this.hasURLFlag('ui-debug')
      }
    };
  }

  /**
   * Environment detection methods
   */
  private isDevelopmentMode(): boolean {
    // Check for development indicators
    return !!(
      // Debug query parameter
      this.hasURLFlag('debug') ||
      // Development console exists
      (window as any).__YT2AI_DEBUG__ ||
      // Local file protocol (for local development)
      window.location.protocol === 'file:' ||
      // Localhost detection
      window.location.hostname === 'localhost' ||
      window.location.hostname === '127.0.0.1'
    );
  }

  private isMobileDevice(): boolean {
    return !!(
      window.innerWidth <= 768 ||
      /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
      ('ontouchstart' in window) ||
      (navigator.maxTouchPoints > 0)
    );
  }

  private isAndroidDevice(): boolean {
    return /Android/i.test(navigator.userAgent);
  }

  private isChromeeBrowser(): boolean {
    return /Chrome/i.test(navigator.userAgent) && !!/Google Inc/.test(navigator.vendor);
  }

  private hasReducedMotion(): boolean {
    return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  }

  private hasURLFlag(flag: string): boolean {
    const url = new URL(window.location.href);
    return url.searchParams.has(flag) || url.hash.includes(`#${flag}`);
  }

  /**
   * Public API for accessing configuration
   */
  public getConfig(): Readonly<EnvironmentConfig> {
    return { ...this.config };
  }

  public isFeatureEnabled(feature: keyof EnvironmentConfig['features']): boolean {
    return this.config.features[feature];
  }

  public getAPIConfig(service: 'subtitleService' | 'claude') {
    return this.config.api[service];
  }

  public getMobileConfig() {
    return this.config.mobile;
  }

  public getDebugConfig() {
    return this.config.debug;
  }

  /**
   * Runtime feature flag updates (for testing/debugging)
   */
  public updateFeature(feature: keyof EnvironmentConfig['features'], enabled: boolean): void {
    if (this.config.isDevelopment) {
      this.config.features[feature] = enabled;
      console.log(`[Environment] Feature ${feature} ${enabled ? 'enabled' : 'disabled'}`);
    }
  }

  /**
   * Compatibility checks
   */
  public checkCompatibility(): { compatible: boolean; issues: string[] } {
    const issues: string[] = [];
    
    // Check required browser features
    if (!window.fetch) {
      issues.push('Fetch API not supported - bookmarklet requires modern browser');
    }
    
    if (!navigator.clipboard) {
      issues.push('Clipboard API not available - fallback methods will be used');
    }
    
    if (!this.config.isChrome && this.config.isMobile) {
      issues.push('Non-Chrome mobile browser detected - functionality may be limited');
    }
    
    if (!this.config.isAndroid && this.config.isMobile) {
      issues.push('Non-Android mobile device - bookmarklet optimized for Android Chrome');
    }
    
    // Check YouTube context
    if (!window.location.hostname.includes('youtube.com')) {
      issues.push('Not on YouTube - bookmarklet only works on YouTube pages');
    }
    
    // Check network connectivity (basic)
    if (!navigator.onLine) {
      issues.push('No internet connection detected');
    }
    
    return {
      compatible: issues.length === 0,
      issues
    };
  }

  /**
   * Performance monitoring setup
   */
  public setupPerformanceTracking(): void {
    if (!this.config.features.enablePerformanceTracking) return;
    
    // Track bookmarklet initialization time
    performance.mark('yt2ai-start');
    
    // Track critical user metrics
    if (window.performance && window.performance.observer) {
      const observer = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          if (entry.name.startsWith('yt2ai-')) {
            console.log(`[Performance] ${entry.name}: ${entry.duration}ms`);
          }
        });
      });
      
      observer.observe({ entryTypes: ['measure', 'navigation'] });
    }
  }

  /**
   * Debug logging setup
   */
  public setupLogging(): void {
    if (!this.config.isDevelopment) return;
    
    // Enhanced console logging for development
    (window as any).__YT2AI_LOG__ = {
      state: (message: string, data?: any) => {
        if (this.config.debug.enableStateLogging) {
          console.log(`[YT2AI:State] ${message}`, data);
        }
      },
      api: (message: string, data?: any) => {
        if (this.config.debug.enableAPILogging) {
          console.log(`[YT2AI:API] ${message}`, data);
        }
      },
      ui: (message: string, data?: any) => {
        if (this.config.debug.enableUILogging) {
          console.log(`[YT2AI:UI] ${message}`, data);
        }
      }
    };
  }
}

// Singleton environment manager
const environment = new EnvironmentManager();

// Initialize environment features
environment.setupPerformanceTracking();
environment.setupLogging();

// Export for use throughout bookmarklet
export { environment, type EnvironmentConfig };

// Global debug access in development
if (environment.getConfig().isDevelopment) {
  (window as any).__YT2AI_ENV__ = environment;
}

Frontend Developer Standards

Critical Coding Rules

Universal JavaScript Rules:

  1. Always use const by default, let when reassignment needed - Prevents accidental mutations in bookmarklet scope
  2. Prefix all CSS classes with yt2ai- - Absolutely critical to avoid YouTube style conflicts
  3. Use z-index: 999999 or higher for all overlays - Must render above YouTube's interface
  4. Handle all async operations with try/catch - Network calls and API failures are common in mobile environment
  5. Always cleanup event listeners and DOM elements - Memory leaks are critical in bookmarklet context
  6. Never use document.write() or innerHTML with user content - XSS prevention in third-party context
  7. Always check for feature availability before use - Mobile Chrome versions may vary in API support

Mobile-Specific Rules: 8. All touch targets must be minimum 44px - WCAG AA compliance for mobile accessibility 9. Use touchend events, not click for mobile optimization - Better mobile responsiveness 10. Always prevent body scroll when showing modals - Essential mobile UX pattern 11. Test viewport changes during overlay display - Android keyboards and orientation changes 12. Use rem units for scalable mobile design - Better cross-device consistency than px 13. Implement loading states for all network operations - Mobile networks are unpredictable 14. Always provide retry mechanisms for failed API calls - Essential for mobile network reliability

Bookmarklet-Specific Rules: 15. Never rely on external dependencies in production - Must be completely self-contained 16. Always namespace global variables with __YT2AI_ - Prevent conflicts with YouTube/page JavaScript 17. Use feature detection, never user agent sniffing - More reliable than parsing user agent strings 18. Always cleanup on error or completion - Bookmarklet should leave no traces when finished 19. Never store sensitive data in localStorage/sessionStorage - Privacy and security in third-party context 20. Always validate YouTube context before execution - Ensure running on valid YouTube pages

Anti-Patterns to Avoid: Never modify YouTube's existing DOM elements - Can break their functionality Never use alert(), confirm(), prompt() - Poor mobile UX and blocks execution
Never use setTimeout without clearTimeout - Memory leaks in long-running bookmarklets Never assume APIs will always be available - External services can fail Never use position: fixed without z-index consideration - Will render behind YouTube elements Never use eval() or Function() constructor - Security risk and CSP violations Never rely on specific YouTube DOM structure - YouTube frequently updates their interface

Quick Reference

Common Commands:

# Development server (if using local development)
npm run dev              # Start development server with hot reload
npm run build            # Build minified production bookmarklet
npm run test             # Run Jest test suite
npm run test:mobile      # Run mobile-specific test suite
npm run lint             # Run ESLint with mobile-first rules
npm run format           # Format code with Prettier

Key Import Patterns:

// Modular development imports (build process inlines these)
import { stateManager } from './state/BookmarkletState.js';
import { apiService } from './core/subtitleFetcher.js';
import { MobileOverlayComponent } from './ui/overlayManager.js';
import { environment } from './utils/environment.js';

// Always use dynamic imports for optional features
const rpaModule = await import('./features/rpaAutomation.js').catch(() => null);

File Naming Conventions:

✅ Good:
- videoExtractor.js
- mobileUtils.js  
- overlayManager.js
- BookmarkletState.js

❌ Avoid:
- VideoExtractor.js (PascalCase files)
- mobile_utils.js (snake_case)
- overlay-manager.js (kebab-case)
- bookmarkletstate.js (no capitals)

Project-Specific Patterns:

Error Handling Pattern:

// Always use this error handling pattern
try {
  stateManager.advanceToStep('subtitle-extraction');
  const result = await apiService.extractSubtitles(videoId);
  
  if (!result.success) {
    throw new BookmarkletError(result.error);
  }
  
  stateManager.completeStep('subtitle-extraction');
  return result.data;
  
} catch (error) {
  stateManager.failStep('subtitle-extraction', {
    type: 'api',
    message: error.message,
    recoverable: true,
    retryable: true
  });
  
  throw error;
}

Mobile Overlay Creation Pattern:

// Standard mobile overlay creation
const overlay = new MobileOverlayComponent({
  id: 'unique-identifier',
  type: 'loading' | 'error' | 'success' | 'info',
  content: 'User-friendly message',
  position: 'center', // 'top' | 'center' | 'bottom'
  autoClose: 5000 // Optional auto-close timer
});

overlay.show();

// Always cleanup
setTimeout(() => {
  overlay.destroy();
}, 10000);

API Call Pattern:

// Standard API call with mobile optimization
const makeAPICall = async (endpoint, options = {}) => {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 30000);
  
  try {
    const response = await fetch(endpoint, {
      ...options,
      signal: controller.signal,
      headers: {
        'User-Agent': 'Mozilla/5.0 (Linux; Android 10; Mobile) Chrome/91.0',
        ...options.headers
      }
    });
    
    clearTimeout(timeoutId);
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return await response.json();
    
  } catch (error) {
    clearTimeout(timeoutId);
    
    if (error.name === 'AbortError') {
      throw new Error('Request timeout - mobile network too slow');
    }
    
    throw error;
  }
};

State Management Pattern:

// Always update state through stateManager
const updateWorkflowData = (videoId, subtitles) => {
  stateManager.setVideoData(videoId);
  stateManager.setSubtitleData(subtitles);
  
  // Subscribe to state changes for UI updates
  const unsubscribe = stateManager.subscribe((state) => {
    if (state.isProcessing) {
      showLoadingOverlay();
    } else {
      hideLoadingOverlay();
    }
  });
  
  // Always cleanup subscriptions
  setTimeout(unsubscribe, 60000); // Max bookmarklet lifetime
};

Mobile-Optimized CSS Pattern:

// CSS-in-JS with mobile-first approach
const mobileStyles = `
  .yt2ai-component {
    /* Mobile base styles */
    font-size: 14px;
    padding: 12px;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
  }
  
  @media (min-width: 768px) {
    .yt2ai-component {
      /* Desktop enhancements */
      font-size: 16px;
      padding: 16px;
    }
  }
  
  @media (max-width: 374px) {
    .yt2ai-component {
      /* Very small mobile adjustments */
      font-size: 13px;
      padding: 10px;
    }
  }
`;

Performance Monitoring Pattern:

// Track critical performance metrics
const trackPerformance = (operationName, fn) => {
  if (environment.isFeatureEnabled('enablePerformanceTracking')) {
    performance.mark(`yt2ai-${operationName}-start`);
  }
  
  const result = await fn();
  
  if (environment.isFeatureEnabled('enablePerformanceTracking')) {
    performance.mark(`yt2ai-${operationName}-end`);
    performance.measure(
      `yt2ai-${operationName}`,
      `yt2ai-${operationName}-start`,
      `yt2ai-${operationName}-end`
    );
  }
  
  return result;
};

🏗️ Frontend Architecture Complete

This comprehensive frontend architecture document provides the complete technical foundation for the YouTube Subtitle Extraction & AI Summarization Bookmarklet, optimized for Android Chrome mobile environment with robust error handling, state management, and user experience considerations.