All files / src/utils themeIcon.ts

100% Statements 45/45
100% Branches 14/14
100% Functions 2/2
100% Lines 45/45

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 851x                     1x 1x 1x   1x         1x       1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x   1x             1x 6x 6x 6x 6x 1x 1x 6x   1x                         1x 1x 6x 1x 1x 6x 1x 1x   4x 6x 6x 2x 2x 6x  
/**
 * Theme-aware toolbar (action) icon.
 *
 * The manifest ships the main navy logo as the default action icon; this swaps
 * to the inverted white logo whenever the browser reports a dark color scheme,
 * so the icon stays legible against the toolbar. Detection runs from the
 * extension page context (popup) because a service worker has no
 * `matchMedia`; `chrome.action.setIcon` persists the chosen icon for the
 * browser session and otherwise falls back to the manifest default.
 *
 * @module themeIcon
 */
import { Logger } from '@/utils/Logger';
import { getErrorMessage } from '@/helpers/exceptions';
 
const logger = new Logger('themeIcon');
 
/** The browser color schemes we render distinct toolbar icons for. */
type ColorScheme = 'light' | 'dark';
 
/**
 * Per-scheme sized toolbar icons, resolved relative to the extension root (the
 * same convention as the manifest `icons` paths). `light` uses the navy badge
 * so it reads on a light toolbar; `dark` uses the inverted white badge.
 */
const THEME_ICON_PATHS: Record<ColorScheme, Record<number, string>> = {
  light: {
    16: 'static/images/logo/ChemPal-logo-16.png',
    32: 'static/images/logo/ChemPal-logo-32.png',
    48: 'static/images/logo/ChemPal-logo-48.png',
    128: 'static/images/logo/ChemPal-logo-128.png',
  },
  dark: {
    16: 'static/images/logo/ChemPal-logo-inverted-16.png',
    32: 'static/images/logo/ChemPal-logo-inverted-32.png',
    48: 'static/images/logo/ChemPal-logo-inverted-48.png',
    128: 'static/images/logo/ChemPal-logo-inverted-128.png',
  },
};
 
/**
 * Applies the toolbar icon variant matching the given color scheme.
 * @param scheme - The active browser color scheme.
 * @returns Resolves once the icon has been applied (failures are logged, not thrown).
 * @example
 * await applyToolbarIcon("dark"); // toolbar shows the inverted (white) logo
 * @source
 */
async function applyToolbarIcon(scheme: ColorScheme): Promise<void> {
  try {
    await chrome.action.setIcon({ path: THEME_ICON_PATHS[scheme] });
  } catch (error) {
    logger.warn(`Failed to set the ${scheme} toolbar icon: ${getErrorMessage(error)}`, { error });
  }
}
 
/**
 * Initializes the theme-aware toolbar icon: applies the icon matching the
 * current `prefers-color-scheme` and updates it whenever the scheme changes.
 *
 * Safe to call unconditionally at startup — it no-ops in any context lacking
 * `matchMedia` or `chrome.action.setIcon` (tests, the Vite dev preview, a
 * service worker, or a browser that exposes neither), so it never throws.
 * @category Utils
 * @returns Nothing; the toolbar icon is updated as a side effect.
 * @example
 * // In the app entry point (main.tsx):
 * initThemeAwareToolbarIcon();
 * @source
 */
export function initThemeAwareToolbarIcon(): void {
  if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
    return;
  }
  if (typeof chrome === 'undefined' || chrome.action?.setIcon === undefined) {
    return;
  }
 
  const darkScheme = window.matchMedia('(prefers-color-scheme: dark)');
  void applyToolbarIcon(darkScheme.matches ? 'dark' : 'light');
  darkScheme.addEventListener('change', (event) => {
    void applyToolbarIcon(event.matches ? 'dark' : 'light');
  });
}