All files / packages/theme-selector/src index.ts

71.42% Statements 30/42
55.55% Branches 10/18
71.42% Functions 5/7
74.35% Lines 29/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170                                                                    15x     15x 15x     15x                 15x                     17x     17x 17x 2x     15x 15x 15x     15x             15x       17x     17x                 1x         17x 1x       15x     15x     15x 15x   15x                                                                 11x 11x 1x 1x                         1x        
// SPDX-License-Identifier: MIT
/**
 * Theme selector package - main entry point
 */
 
import { migrateLegacyStorage, getSavedTheme, DEFAULT_THEME } from './storage.js';
import { getBaseUrl, getCurrentThemeFromClasses } from './theme-loader.js';
import {
  applyTheme as applyThemeToDocument,
  getCurrentTheme as getCurrentThemeFromDocument,
} from './apply-theme.js';
import { applyTheme as applyThemeById } from './integration.js';
import { THEME_FAMILIES } from './constants.js';
import { getThemes, getValidThemeIds } from './theme-resolver.js';
import { createDropdownStateManager, type DropdownState } from './dropdown/state.js';
import { wireDropdownEventHandlers } from './dropdown/events.js';
import { populateDropdownMenu, wireNativeSelect, getDropdownElements } from './dropdown/ui.js';
import { initNavbar } from './navbar.js';
import { enhanceAccessibility } from './accessibility.js';
import { ThemeErrors, logThemeError } from './errors.js';
 
// Extend Window interface to include our custom property
declare global {
  interface Window {
    __INITIAL_THEME__?: string;
    initNavbar: typeof initNavbar;
  }
}
 
/**
 * Initializes theme system
 */
export async function initTheme(documentObj: Document, windowObj: Window): Promise<void> {
  // Migrate legacy storage keys
  migrateLegacyStorage(windowObj);
 
  // Check if theme was already applied by blocking script
  const initialTheme = windowObj.__INITIAL_THEME__;
  const savedTheme = getSavedTheme(windowObj, getValidThemeIds());
 
  // If blocking script already applied theme and it matches saved, synchronize UI and return
  Iif (initialTheme && initialTheme === savedTheme) {
    if (getCurrentThemeFromClasses(documentObj.documentElement) === savedTheme) {
      // Always call applyTheme so the trigger label/icon stay in sync with the active theme
      await applyThemeToDocument(documentObj, savedTheme);
      return;
    }
  }
 
  // Otherwise, apply theme normally
  await applyThemeToDocument(documentObj, savedTheme);
}
 
/**
 * Initializes and wires up the theme flavor selector dropdown.
 * Returns a Promise for API consistency with initTheme.
 */
export async function wireFlavorSelector(
  documentObj: Document,
  windowObj: Window
): Promise<{ cleanup: () => void }> {
  const abortController = new AbortController();
 
  // Get required DOM elements
  const elements = getDropdownElements(documentObj);
  if (!elements) {
    return { cleanup: () => abortController.abort() };
  }
 
  const { dropdownMenu, selectEl } = elements;
  const baseUrl = getBaseUrl(documentObj);
  const themes = getThemes();
 
  // Initialize state
  const state: DropdownState = {
    currentIndex: -1,
    menuItems: [],
  };
 
  // Get current theme (validated against available themes)
  const currentThemeId =
    getSavedTheme(windowObj, getValidThemeIds()) ||
    getCurrentThemeFromDocument(documentObj, DEFAULT_THEME);
 
  // Create state manager (needs elements and state)
  const stateManager = createDropdownStateManager(elements, state);
 
  // Create context for UI creation
  const ctx = {
    documentObj,
    windowObj,
    baseUrl,
    currentThemeId,
    selectEl,
    menuItems: state.menuItems,
    closeDropdown: stateManager.closeDropdown,
    onThemeSelect: async (themeId: string) => {
      await applyThemeById(themeId, documentObj, windowObj);
    },
  };
 
  // Wire native select if present
  if (selectEl) {
    wireNativeSelect(ctx, selectEl, themes, DEFAULT_THEME);
  }
 
  // Populate dropdown menu
  populateDropdownMenu(ctx, dropdownMenu, themes, THEME_FAMILIES);
 
  // Wire event handlers
  wireDropdownEventHandlers(documentObj, elements, state, stateManager, abortController);
 
  // Initialize state
  stateManager.updateAriaExpanded(false);
  elements.dropdown.classList.remove('is-active');
 
  return { cleanup: () => abortController.abort() };
}
 
// Re-export public integration API for driving theming from site pages
export {
  applyTheme,
  getCurrentTheme,
  subscribeToThemeChanges,
  THEME_CHANGE_EVENT,
  type ApplyThemeResult,
  type ThemeChangeDetail,
  type ThemeChangeListener,
  type Unsubscribe,
} from './integration.js';
 
// Re-export lazy theme-CSS loading helpers for showcase-style pages
export {
  isThemeCSSLoaded,
  loadThemeCSSOnDemand,
  prefetchThemeCSS,
  shouldLoadThemeCSS,
  wireHoverPrefetch,
  PREFETCH_TRIGGER_ATTRIBUTE,
} from './lazy-css.js';
 
// Re-export navbar, accessibility, and blocking script functions
export { initNavbar, enhanceAccessibility };
export { generateBlockingScript, type BlockingScriptOptions } from './blocking-script.js';
 
// Re-export types
export type { ThemeMode, ThemeAppearance, ThemeFamily } from './types.js';
 
// Auto-initialize on DOMContentLoaded
Eif (typeof document !== 'undefined' && typeof window !== 'undefined') {
  document.addEventListener('DOMContentLoaded', async () => {
    try {
      await initTheme(document, window);
 
      const { cleanup } = await wireFlavorSelector(document, window);
      enhanceAccessibility(document);
 
      // Register cleanup on full page unload; skip for bfcache restores
      const pagehideHandler = (event: PageTransitionEvent) => {
        if (event.persisted) return;
        cleanup();
        window.removeEventListener('pagehide', pagehideHandler);
      };
      window.addEventListener('pagehide', pagehideHandler);
    } catch (error) {
      logThemeError(ThemeErrors.INIT_FAILED(error));
    }
  });
}