UNPKG

@whitemordred/react-native-bootstrap5

Version:

A complete React Native library that replicates Bootstrap 5.3 with 100% feature parity, full theming support, CSS variables, and dark/light mode

480 lines (479 loc) 15.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ThemeManager = exports.ThemeAnimationUtils = exports.ColorUtils = exports.SystemTheme = exports.ThemeStorage = void 0; const react_native_1 = require("react-native"); const defaultTheme_1 = require("./defaultTheme"); // Global reference checks for web environments const isWeb = react_native_1.Platform.OS === 'web'; const hasWindow = isWeb && typeof globalThis !== 'undefined' && 'window' in globalThis; const hasLocalStorage = hasWindow && typeof localStorage !== 'undefined'; const hasDocument = isWeb && typeof globalThis !== 'undefined' && 'document' in globalThis; // Optional AsyncStorage import (only for React Native) let AsyncStorage = null; try { if (!isWeb) { AsyncStorage = require('@react-native-async-storage/async-storage').default; } } catch (error) { // AsyncStorage not available } // Storage keys const THEME_STORAGE_KEY = '@bootstrap5_theme_preferences'; const THEME_MODE_KEY = '@bootstrap5_theme_mode'; /** * Theme storage utilities for persisting user preferences */ class ThemeStorage { /** * Save theme preferences to storage */ static async savePreferences(preferences) { try { if (isWeb && hasLocalStorage) { localStorage.setItem(THEME_STORAGE_KEY, JSON.stringify(preferences)); } else if (AsyncStorage) { await AsyncStorage.setItem(THEME_STORAGE_KEY, JSON.stringify(preferences)); } } catch (error) { console.warn('Failed to save theme preferences:', error); } } /** * Load theme preferences from storage */ static async loadPreferences() { try { let stored = null; if (isWeb && hasLocalStorage) { stored = localStorage.getItem(THEME_STORAGE_KEY); } else if (AsyncStorage) { stored = await AsyncStorage.getItem(THEME_STORAGE_KEY); } if (stored) { const parsed = JSON.parse(stored); return Object.assign(Object.assign({}, defaultTheme_1.defaultThemePreferences), parsed); } } catch (error) { console.warn('Failed to load theme preferences:', error); } return defaultTheme_1.defaultThemePreferences; } /** * Save current theme mode */ static async saveThemeMode(mode) { try { if (isWeb && hasLocalStorage) { localStorage.setItem(THEME_MODE_KEY, mode); } else if (AsyncStorage) { await AsyncStorage.setItem(THEME_MODE_KEY, mode); } } catch (error) { console.warn('Failed to save theme mode:', error); } } /** * Load saved theme mode */ static async loadThemeMode() { try { if (isWeb && hasLocalStorage) { return localStorage.getItem(THEME_MODE_KEY); } else if (AsyncStorage) { return await AsyncStorage.getItem(THEME_MODE_KEY); } } catch (error) { console.warn('Failed to load theme mode:', error); } return null; } /** * Clear all theme data */ static async clearThemeData() { try { if (isWeb && hasLocalStorage) { localStorage.removeItem(THEME_STORAGE_KEY); localStorage.removeItem(THEME_MODE_KEY); } else if (AsyncStorage) { await AsyncStorage.multiRemove([THEME_STORAGE_KEY, THEME_MODE_KEY]); } } catch (error) { console.warn('Failed to clear theme data:', error); } } } exports.ThemeStorage = ThemeStorage; /** * System theme detection utilities */ class SystemTheme { /** * Get system preferred color scheme */ static getSystemTheme() { if (hasWindow) { try { const win = globalThis; if (win.window && win.window.matchMedia) { const mediaQuery = win.window.matchMedia('(prefers-color-scheme: dark)'); return mediaQuery.matches ? 'dark' : 'light'; } } catch (error) { // Fallback } } return 'light'; } /** * Listen to system theme changes on web */ static addSystemThemeListener(callback) { if (hasWindow) { try { const win = globalThis; if (win.window && win.window.matchMedia) { const mediaQuery = win.window.matchMedia('(prefers-color-scheme: dark)'); const handler = (e) => { callback(e.matches ? 'dark' : 'light'); }; if (mediaQuery.addListener) { mediaQuery.addListener(handler); return () => { var _a; return (_a = mediaQuery.removeListener) === null || _a === void 0 ? void 0 : _a.call(mediaQuery, handler); }; } else if (mediaQuery.addEventListener) { mediaQuery.addEventListener('change', handler); return () => { var _a; return (_a = mediaQuery.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(mediaQuery, 'change', handler); }; } } } catch (error) { // Fallback } } return null; } /** * Check if user prefers reduced motion */ static prefersReducedMotion() { if (hasWindow) { try { const win = globalThis; if (win.window && win.window.matchMedia) { const mediaQuery = win.window.matchMedia('(prefers-reduced-motion: reduce)'); return mediaQuery.matches; } } catch (error) { // Fallback } } return false; } /** * Check if user prefers high contrast */ static prefersHighContrast() { if (hasWindow) { try { const win = globalThis; if (win.window && win.window.matchMedia) { const mediaQuery = win.window.matchMedia('(prefers-contrast: high)'); return mediaQuery.matches; } } catch (error) { // Fallback } } return false; } } exports.SystemTheme = SystemTheme; /** * Color utilities for theme manipulation */ class ColorUtils { /** * Convert hex color to rgba */ static hexToRgba(hex, alpha = 1) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } /** * Lighten a hex color by a percentage */ static lighten(hex, percent) { const num = parseInt(hex.replace('#', ''), 16); const amt = Math.round(2.55 * percent); const R = (num >> 16) + amt; const G = (num >> 8 & 0x00FF) + amt; const B = (num & 0x0000FF) + amt; return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)) .toString(16) .slice(1); } /** * Darken a hex color by a percentage */ static darken(hex, percent) { const num = parseInt(hex.replace('#', ''), 16); const amt = Math.round(2.55 * percent); const R = (num >> 16) - amt; const G = (num >> 8 & 0x00FF) - amt; const B = (num & 0x0000FF) - amt; return '#' + (0x1000000 + (R > 255 ? 255 : R < 0 ? 0 : R) * 0x10000 + (G > 255 ? 255 : G < 0 ? 0 : G) * 0x100 + (B > 255 ? 255 : B < 0 ? 0 : B)) .toString(16) .slice(1); } /** * Get contrast color (black or white) for a given background */ static getContrastColor(hex) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); // Calculate luminance const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance > 0.5 ? '#000000' : '#ffffff'; } /** * Check if a color is dark */ static isDark(hex) { return ColorUtils.getContrastColor(hex) === '#ffffff'; } /** * Generate color variations (lighter and darker shades) */ static generateColorVariations(hex) { return { 50: ColorUtils.lighten(hex, 45), 100: ColorUtils.lighten(hex, 40), 200: ColorUtils.lighten(hex, 30), 300: ColorUtils.lighten(hex, 20), 400: ColorUtils.lighten(hex, 10), 500: hex, 600: ColorUtils.darken(hex, 10), 700: ColorUtils.darken(hex, 20), 800: ColorUtils.darken(hex, 30), 900: ColorUtils.darken(hex, 40), 950: ColorUtils.darken(hex, 50), }; } } exports.ColorUtils = ColorUtils; /** * Animation utilities for theme transitions */ class ThemeAnimationUtils { /** * Create smooth theme transition */ static createThemeTransition(duration = 300) { if (hasDocument) { try { const doc = globalThis; if (doc.document) { const style = doc.document.createElement('style'); style.textContent = ` *, *::before, *::after { transition: background-color ${duration}ms ease-in-out, border-color ${duration}ms ease-in-out, color ${duration}ms ease-in-out, opacity ${duration}ms ease-in-out, box-shadow ${duration}ms ease-in-out !important; } `; doc.document.head.appendChild(style); // Remove transition after animation completes setTimeout(() => { try { doc.document.head.removeChild(style); } catch (error) { // Style already removed } }, duration + 100); } } catch (error) { // Fallback } } } /** * Disable animations if user prefers reduced motion */ static respectReducedMotion() { if (hasDocument && SystemTheme.prefersReducedMotion()) { try { const doc = globalThis; if (doc.document) { const style = doc.document.createElement('style'); style.textContent = ` *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } `; doc.document.head.appendChild(style); } } catch (error) { // Fallback } } } } exports.ThemeAnimationUtils = ThemeAnimationUtils; /** * Theme manager for advanced theme handling */ class ThemeManager { /** * Initialize theme manager */ static async initialize(followSystemTheme = true) { this.followSystem = followSystemTheme; // Load saved theme mode first const savedMode = await ThemeStorage.loadThemeMode(); if (savedMode) { this.currentMode = savedMode; } else if (followSystemTheme) { this.currentMode = SystemTheme.getSystemTheme(); } // Set up system theme listener if following system if (followSystemTheme) { this.setupSystemListener(); } // Apply reduced motion preferences ThemeAnimationUtils.respectReducedMotion(); return this.currentMode; } /** * Set up system theme listener */ static setupSystemListener() { if (this.systemListener) { this.systemListener(); this.systemListener = null; } this.systemListener = SystemTheme.addSystemThemeListener((theme) => { if (this.followSystem) { this.setMode(theme, false); // Don't save when following system } }); } /** * Set theme mode */ static async setMode(mode, saveToStorage = true) { if (this.currentMode === mode) return; // Create smooth transition ThemeAnimationUtils.createThemeTransition(); this.currentMode = mode; // Save to storage if requested if (saveToStorage) { await ThemeStorage.saveThemeMode(mode); // Stop following system when user manually sets mode this.followSystem = false; if (this.systemListener) { this.systemListener(); this.systemListener = null; } } // Notify all listeners this.listeners.forEach(listener => listener(mode)); } /** * Toggle between light and dark mode */ static async toggleMode() { const newMode = this.currentMode === 'light' ? 'dark' : 'light'; await this.setMode(newMode, true); } /** * Get current theme mode */ static getCurrentMode() { return this.currentMode; } /** * Check if following system theme */ static isFollowingSystem() { return this.followSystem; } /** * Start following system theme */ static async followSystemTheme() { this.followSystem = true; this.setupSystemListener(); // Immediately switch to system theme const systemTheme = SystemTheme.getSystemTheme(); await this.setMode(systemTheme, false); } /** * Stop following system theme */ static stopFollowingSystem() { this.followSystem = false; if (this.systemListener) { this.systemListener(); this.systemListener = null; } } /** * Add theme change listener */ static addListener(listener) { this.listeners.push(listener); return () => { const index = this.listeners.indexOf(listener); if (index > -1) { this.listeners.splice(index, 1); } }; } /** * Clean up all listeners */ static cleanup() { this.listeners = []; if (this.systemListener) { this.systemListener(); this.systemListener = null; } } } exports.ThemeManager = ThemeManager; ThemeManager.listeners = []; ThemeManager.systemListener = null; ThemeManager.currentMode = 'light'; ThemeManager.followSystem = false;