import { Platform, Keyboard } from 'react-native'; import { useEffect, useState } from 'react'; export const isIOS = Platform.OS === 'ios'; export const isAndroid = Platform.OS === 'android'; export function pick( keys: T[], obj: O ): Pick { return keys .filter((key) => key in obj) .reduce>( (result, cur) => ({ ...result, [cur]: obj[cur], }), {} ) as Pick; } export function omit(keys: T[], obj: O): Omit { const result = obj; keys.forEach((key) => { delete result[key]; }); return result; } export function hexToRgba(hex: string, a: number) { // Validate inputs if (typeof hex !== 'string' || typeof a !== 'number') { throw new Error( 'hexToRgba: hex must be a string and alpha must be a number' ); } if (a < 0 || a > 1) { throw new Error('hexToRgba: alpha must be between 0 and 1'); } // Remove # if present and validate hex format const cleanHex = hex.replace(/^#/, ''); if (!/^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(cleanHex)) { throw new Error( 'hexToRgba: hex must be a valid 3 or 6 character hex color' ); } // Handle 3-character hex codes (e.g., #fff -> #ffffff) const normalizedHex = cleanHex.length === 3 ? cleanHex .split('') .map((char) => char + char) .join('') : cleanHex; // Parse hex values const r = parseInt(normalizedHex.substring(0, 2), 16); const g = parseInt(normalizedHex.substring(2, 4), 16); const b = parseInt(normalizedHex.substring(4, 6), 16); return `rgba(${r},${g},${b},${a})`; } /** * Dim a hex color by blending it with a surface color * @param hex - The hex color to dim (3 or 6 characters, with or without #) * @param surface - The surface color to blend with. * @param amount - The amount of dimming (0 = original color, 1 = fully surface color) * @returns The dimmed hex color */ const DEFAULT_DIM_AMOUNT = 0.6; export function dimHex( hex: string, surface: string, amount = DEFAULT_DIM_AMOUNT ): string { // Validate inputs if ( typeof hex !== 'string' || typeof surface !== 'string' || typeof amount !== 'number' ) { throw new Error( 'dimHex: hex and surface must be strings and amount must be a number' ); } if (amount < 0 || amount > 1) { throw new Error('dimHex: amount must be between 0 and 1'); } // Remove # if present and validate hex format const cleanHex = hex.replace(/^#/, ''); const cleanSurface = surface.replace(/^#/, ''); if (!/^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(cleanHex)) { throw new Error('dimHex: hex must be a valid 3 or 6 character hex color'); } if (!/^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(cleanSurface)) { throw new Error( 'dimHex: surface must be a valid 3 or 6 character hex color' ); } // Handle 3-character hex codes (e.g., #fff -> #ffffff) const normalizedHex = cleanHex.length === 3 ? cleanHex .split('') .map((char) => char + char) .join('') : cleanHex; const normalizedSurface = cleanSurface.length === 3 ? cleanSurface .split('') .map((char) => char + char) .join('') : cleanSurface; // Parse hex values const r = parseInt(normalizedHex.substring(0, 2), 16); const g = parseInt(normalizedHex.substring(2, 4), 16); const b = parseInt(normalizedHex.substring(4, 6), 16); const sr = parseInt(normalizedSurface.substring(0, 2), 16); const sg = parseInt(normalizedSurface.substring(2, 4), 16); const sb = parseInt(normalizedSurface.substring(4, 6), 16); // Linear interpolation between colors const lerp = (c: number, s: number): number => Math.round(c * (1 - amount) + s * amount); const rr = lerp(r, sr).toString(16).padStart(2, '0'); const gg = lerp(g, sg).toString(16).padStart(2, '0'); const bb = lerp(b, sb).toString(16).padStart(2, '0'); return `#${rr}${gg}${bb}`; } export const deepCompareValue = (a: V, b: V): boolean => { // Handle strict equality first (handles primitives, null, undefined) if (a === b) return true; // Special handling for NaN (NaN !== NaN in JS) if ( typeof a === 'number' && typeof b === 'number' && Number.isNaN(a) && Number.isNaN(b) ) { return false; } // If either is null or undefined (but they are not strictly equal), return false if (a == null || b == null) return false; // If types don't match, they can't be equal if (typeof a !== typeof b) return false; // Handle array comparison if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; return a.every((val, index) => deepCompareValue(val, b[index])); } // If one is array and the other isn't, return false if (Array.isArray(a) !== Array.isArray(b)) return false; // Handle object comparison if (typeof a === 'object' && typeof b === 'object') { const keysA = Object.keys(a) as (keyof V)[]; const keysB = Object.keys(b) as (keyof V)[]; if (keysA.length !== keysB.length) return false; return keysA.every( (key) => keysB.includes(key) && deepCompareValue(a[key], b[key]) ); } // If none of the above conditions matched, they're not equal return false; }; export const useKeyboard = () => { const [isKeyboardVisible, setKeyboardVisible] = useState(false); const [keyboardHeight, setKeyboardHeight] = useState(0); useEffect(() => { const keyboardWillShowListener = Keyboard.addListener( 'keyboardWillShow', (e) => { setKeyboardVisible(true); setKeyboardHeight(e.endCoordinates.height); } ); const keyboardWillHideListener = Keyboard.addListener( 'keyboardWillHide', () => { setKeyboardVisible(false); } ); return () => { keyboardWillShowListener.remove(); keyboardWillHideListener.remove(); }; }, []); return { isKeyboardVisible, keyboardHeight }; }; export type CamelCase = S extends `${infer F}-${infer R}` ? R extends `${string}-${string}` ? `${F}${Capitalize>}` : `${F}${Capitalize}` : never; export const transformKebabCaseToCamelCase = ( string: T ): CamelCase => { return string.replace(/-([a-z0-9])/g, (_, char: string) => /[a-z]/.test(char) ? char.toUpperCase() : char ) as CamelCase; }; export function assert(condition: unknown, message: string): asserts condition { if (!condition) { throw new Error(message); } }