UNPKG

native-variants

Version:

A library for handling variants in React Native components with theme support.

514 lines 17.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.styled = styled; exports.createNVA = createNVA; exports.clearStyleCache = clearStyleCache; const default_tokens_1 = require("../tokens/default-tokens.js"); /** * High-performance memoization cache using WeakMap for object keys * and Map for primitive keys. Optimized for React Native runtime. */ const styleCache = new WeakMap(); const primitiveCache = new Map(); /** * Creates a stable cache key from variant props. * Optimized for performance by avoiding JSON.stringify on simple cases. * * @param props - The variant props to create a key from * @returns A stable string key for caching */ function createCacheKey(props) { if (!props) return "{}"; const keys = Object.keys(props); if (keys.length === 0) return "{}"; // Sort keys for consistent ordering keys.sort(); let key = ""; for (let i = 0; i < keys.length; i++) { const k = keys[i]; const v = props[k]; if (v !== undefined) { key += `${k}:${String(v)};`; } } return key || "{}"; } /** * Normalizes a variant value to string for comparison. * Handles boolean-to-string conversion for true/false variant keys. * * @param value - The value to normalize * @returns The normalized string value */ function normalizeVariantValue(value) { if (typeof value === "boolean") { return value ? "true" : "false"; } return String(value); } /** * Applies variant styles to a specific slot. * Iterates through variant definitions and applies matching styles. * * @template S - Union type of slot names * @template V - Variants configuration type * * @param slot - The slot name to apply styles to * @param variants - The variants configuration object * @param props - The current variant props * @returns The merged styles for the slot */ function applyVariant(slot, variants, props) { let style = {}; for (const variantKey in variants) { if (!Object.prototype.hasOwnProperty.call(props, variantKey)) continue; const value = props[variantKey]; if (value === undefined) continue; const variantConfig = variants[variantKey]; if (!variantConfig) continue; // Normalize boolean values to string keys const normalizedValue = normalizeVariantValue(value); const styleForValue = variantConfig[normalizedValue]?.[slot]; if (styleForValue) { style = { ...style, ...styleForValue }; } } return style; } /** * Applies compound variant styles to a specific slot. * Evaluates compound conditions and applies matching styles. * * @template S - Union type of slot names * @template V - Variants configuration type * * @param slot - The slot name to apply styles to * @param compoundVariants - Array of compound variant configurations * @param props - The current variant props * @returns The merged compound styles for the slot */ function applyCompound(slot, compoundVariants, props) { let style = {}; for (let i = 0; i < compoundVariants.length; i++) { const cv = compoundVariants[i]; const { css, ...conditions } = cv; // Check if all conditions match let isMatch = true; for (const condKey in conditions) { if (condKey === "css") continue; const condValue = conditions[condKey]; const propValue = props[condKey]; // Normalize both values for comparison const normalizedCond = normalizeVariantValue(condValue); const normalizedProp = normalizeVariantValue(propValue); if (normalizedCond !== normalizedProp) { isMatch = false; break; } } if (isMatch && css?.[slot]) { style = { ...style, ...css[slot] }; } } return style; } /** * Computes the final styles for a slot by merging base, variant, and compound styles. * * @template S - Union type of slot names * @template V - Variants configuration type * * @param slot - The slot name to compute styles for * @param base - The base styles configuration * @param variants - The variants configuration * @param compoundVariants - The compound variants array * @param props - The resolved variant props * @returns The fully merged styles for the slot */ function computeSlotStyles(slot, base, variants, compoundVariants, props) { const baseStyle = base?.[slot] ?? {}; const variantStyle = applyVariant(slot, variants, props); const compoundStyle = applyCompound(slot, compoundVariants, props); return { ...baseStyle, ...variantStyle, ...compoundStyle, }; } /** * Creates a styled component function with variant support. * Provides caching for optimal performance in React Native. * * @template S - Union type of slot names * @template V - Variants configuration type * * @param config - The styled component configuration * @returns A function that computes styles based on variant props * * @example * ```ts * const buttonStyles = styled({ * slots: ["root", "text"], * base: { * root: { padding: 16 }, * text: { fontSize: 14 } * }, * variants: { * size: { * small: { root: { padding: 8 } }, * large: { root: { padding: 24 } } * } * }, * defaultVariants: { * size: "small" * } * }); * * // Usage * const styles = buttonStyles({ size: "large" }); * ``` */ function styled(config) { const { slots, base = {}, variants = {}, defaultVariants = {}, compoundVariants = [], } = config; // Pre-freeze arrays for performance const frozenSlots = Object.freeze([...slots]); const frozenCompoundVariants = Object.freeze([...compoundVariants]); // Create a stable config reference for caching const configRef = { config }; return function computeStyles(props) { // Create cache key from props const cacheKey = createCacheKey(props); // Check cache first let configCache = styleCache.get(configRef); if (configCache?.has(cacheKey)) { return configCache.get(cacheKey); } // Resolve props with defaults const resolvedProps = { ...defaultVariants }; if (props) { for (const key in props) { const value = props[key]; if (value !== undefined) { resolvedProps[key] = value; } } } // Compute styles for each slot const result = {}; for (let i = 0; i < frozenSlots.length; i++) { const slot = frozenSlots[i]; result[slot] = computeSlotStyles(slot, base, variants, frozenCompoundVariants, resolvedProps); } // Store in cache if (!configCache) { configCache = new Map(); styleCache.set(configRef, configCache); } configCache.set(cacheKey, result); return result; }; } /** * Default tokens from Tailwind CSS. * These are included by default in every theme. */ const defaultTokens = { /** @see https://tailwindcss.com/docs/customizing-colors */ palette: default_tokens_1.tailwindColors, /** @see https://tailwindcss.com/docs/customizing-spacing */ spacing: default_tokens_1.tailwindSpacing, /** @see https://tailwindcss.com/docs/font-size */ fontSizes: default_tokens_1.tailwindFontSizes, /** @see https://tailwindcss.com/docs/border-radius */ radii: default_tokens_1.tailwindRadii, /** @see https://tailwindcss.com/docs/box-shadow */ shadows: default_tokens_1.tailwindShadows, /** @see https://tailwindcss.com/docs/z-index */ zIndex: default_tokens_1.tailwindZIndex, /** @see https://tailwindcss.com/docs/opacity */ opacity: default_tokens_1.tailwindOpacity, /** @see https://tailwindcss.com/docs/line-height */ lineHeights: default_tokens_1.tailwindLineHeights, /** @see https://tailwindcss.com/docs/font-weight */ fontWeights: default_tokens_1.tailwindFontWeights, /** @see https://tailwindcss.com/docs/letter-spacing */ letterSpacing: default_tokens_1.tailwindLetterSpacing, /** @see https://tailwindcss.com/docs/border-width */ borderWidths: default_tokens_1.tailwindBorderWidths, /** @see https://tailwindcss.com/docs/transition-duration */ durations: default_tokens_1.tailwindDurations, }; /** * Expands utils in a style object. * Takes a style with potential util keys and expands them to their actual styles. * * @param style - The style object potentially containing util keys * @param utils - The utils configuration * @returns The expanded style object */ function expandUtils(style, utils) { if (!style) return {}; const result = {}; for (const key in style) { const value = style[key]; if (key in utils) { // This is a util - expand it const utilFn = utils[key]; const expandedStyles = utilFn(value); Object.assign(result, expandedStyles); } else { // Regular style property result[key] = value; } } return result; } /** * Expands utils in a base styles object for all slots. * * @param base - The base styles with potential utils * @param utils - The utils configuration * @returns The expanded base styles */ function expandBaseUtils(base, utils) { if (!base) return {}; const result = {}; for (const slot in base) { result[slot] = expandUtils(base[slot], utils); } return result; } /** * Expands utils in variants configuration. * * @param variants - The variants with potential utils * @param utils - The utils configuration * @returns The expanded variants */ function expandVariantsUtils(variants, utils) { if (!variants) return {}; const result = {}; for (const variantKey in variants) { const variantValues = variants[variantKey]; if (!variantValues) continue; result[variantKey] = {}; for (const valueKey in variantValues) { const slots = variantValues[valueKey]; if (!slots) continue; result[variantKey][valueKey] = {}; for (const slot in slots) { result[variantKey][valueKey][slot] = expandUtils(slots[slot], utils); } } } return result; } /** * Expands utils in compound variants. * * @param compoundVariants - The compound variants with potential utils * @param utils - The utils configuration * @returns The expanded compound variants */ function expandCompoundUtils(compoundVariants, utils) { if (!compoundVariants) return []; return compoundVariants.map((cv) => { const { css, ...conditions } = cv; const expandedCss = {}; if (css) { for (const slot in css) { expandedCss[slot] = expandUtils(css[slot], utils); } } return { ...conditions, css: expandedCss, }; }); } /** * Creates a themed NVA (Native Variants API) instance. * Provides a styled function with access to theme tokens and custom utils. * * Colors support light/dark mode via `default` and `dark` keys. * Both must have the same color keys for type safety - TypeScript will * error if dark is missing any keys from default or vice versa. * * Utils are style shortcuts that expand to multiple CSS properties. * They work like Stitches utils - you define them once and use them * throughout your styles. * * Tailwind CSS tokens (spacing, fontSizes, radii, etc.) are included by default. * * @template D - Default colors type (inferred from colors.default) * @template K - Dark colors type (must have same keys as D) * @template U - Utils configuration type * * @param options - Configuration options * @param options.theme - Theme configuration with colors (default/dark) * @param options.utils - Custom style utilities (like Stitches) * @returns An object containing the flattened theme, styled function, and utils * * @example * ```ts * const { styled, theme, colorScheme, utils } = createNVA({ * theme: { * colors: { * default: { * primary: "#007AFF", * background: "#FFFFFF", * }, * dark: { * primary: "#0A84FF", * background: "#000000", * }, * }, * }, * utils: { * // Margin shortcuts * mx: (value) => ({ marginLeft: value, marginRight: value }), * my: (value) => ({ marginTop: value, marginBottom: value }), * // Padding shortcuts * px: (value) => ({ paddingLeft: value, paddingRight: value }), * py: (value) => ({ paddingTop: value, paddingBottom: value }), * // Size shortcut * size: (value) => ({ width: value, height: value }), * }, * }); * * // Use utils in your styles! * const buttonStyles = styled((ctx, t) => ctx({ * slots: ["root"], * base: { * root: { * backgroundColor: t.colors.primary, * px: 16, // → paddingLeft: 16, paddingRight: 16 * py: 12, // → paddingTop: 12, paddingBottom: 12 * }, * }, * })); * ``` */ function createNVA(options) { const inputTheme = options?.theme; const inputUtils = (options?.utils ?? {}); // Extract colors from default scheme (light mode) const userColors = (inputTheme?.colors?.default ?? {}); // Merge user colors with Tailwind colors (user colors override defaults) const mergedColors = { ...defaultTokens.palette, ...userColors, }; const resolvedTheme = { colors: mergedColors, spacing: defaultTokens.spacing, fontSizes: defaultTokens.fontSizes, radii: defaultTokens.radii, shadows: defaultTokens.shadows, zIndex: defaultTokens.zIndex, opacity: defaultTokens.opacity, lineHeights: defaultTokens.lineHeights, fontWeights: defaultTokens.fontWeights, letterSpacing: defaultTokens.letterSpacing, borderWidths: defaultTokens.borderWidths, durations: defaultTokens.durations, }; // Store the color scheme for ThemeProvider access const colorScheme = inputTheme?.colors; // Create a stable theme cache per createNVA instance const instanceCache = new Map(); function styled(configOrFactory) { const defineConfig = (config) => config; const configWithUtils = typeof configOrFactory === "function" ? configOrFactory(defineConfig, resolvedTheme) : configOrFactory; // Expand utils in all style configurations const base = expandBaseUtils(configWithUtils.base, inputUtils); const variants = expandVariantsUtils(configWithUtils.variants, inputUtils); const compoundVariants = expandCompoundUtils(configWithUtils.compoundVariants, inputUtils); const { slots, defaultVariants = {} } = configWithUtils; // Pre-freeze for performance const frozenSlots = Object.freeze([...slots]); const frozenCompoundVariants = Object.freeze([...compoundVariants]); // Create stable reference for this specific styled call const configRef = { id: Symbol() }; return function computeStyles(props) { const cacheKey = createCacheKey(props); // Check instance cache let configCache = instanceCache.get(configRef); if (configCache?.has(cacheKey)) { return configCache.get(cacheKey); } // Resolve props with defaults const resolvedProps = { ...defaultVariants, }; if (props) { for (const key in props) { const value = props[key]; if (value !== undefined) { resolvedProps[key] = value; } } } // Compute styles for each slot const result = {}; for (let i = 0; i < frozenSlots.length; i++) { const slot = frozenSlots[i]; result[slot] = computeSlotStyles(slot, base, variants, frozenCompoundVariants, resolvedProps); } // Store in cache if (!configCache) { configCache = new Map(); instanceCache.set(configRef, configCache); } configCache.set(cacheKey, result); return result; }; } return { /** * The resolved theme object with flattened colors. * Colors use the default (light) scheme. * Use ThemeProvider to access dark mode colors. */ theme: resolvedTheme, /** * The color scheme configuration with both default and dark colors. * Pass this to ThemeProvider for dark mode support. */ colorScheme, /** * Creates styled components with variant support and theme access. * Utils defined in createNVA are automatically expanded in styles. */ styled, /** * The utils configuration for use outside of styled. * Useful for applying utils to inline styles. */ utils: inputUtils, }; } /** * Clears all style caches. Useful for testing or hot reloading scenarios. * Note: This only clears the primitive cache. WeakMap entries are * automatically garbage collected when their keys are no longer referenced. */ function clearStyleCache() { primitiveCache.clear(); } //# sourceMappingURL=create-nva.js.map