native-variants
Version:
A library for handling variants in React Native components with theme support.
514 lines • 17.8 kB
JavaScript
"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