UNPKG

@revenuecat/purchases-ui-js

Version:

Web components for Paywalls. Powered by RevenueCat

415 lines (414 loc) 14.5 kB
import { DEFAULT_COLOR_MODE, DEFAULT_TEXT_COLOR, } from "./constants.js"; import { FontSizes, FontSizeTags, FontWeights, StackAlignment, StackDirection, StackDistribution, TextAlignments, } from "../types.js"; /** * Generates CSS spacing styles for margin or padding * @param spacing - The spacing object containing top, trailing, bottom, and leading values * @param spacingKey - The type of spacing ('margin' or 'padding') * @returns CSS style object with logical properties for spacing */ function getSpacingStyle(spacing, spacingKey) { const styles = { [`--${spacingKey}-block-start`]: "0px", [`--${spacingKey}-inline-end`]: "0px", [`--${spacingKey}-block-end`]: "0px", [`--${spacingKey}-inline-start`]: "0px", }; if (!spacing || !spacingKey) return styles; Object.assign(styles, { [`--${spacingKey}-block-start`]: `${spacing.top ?? 0}px`, [`--${spacingKey}-inline-end`]: `${spacing.trailing ?? 0}px`, [`--${spacingKey}-block-end`]: `${spacing.bottom ?? 0}px`, [`--${spacingKey}-inline-start`]: `${spacing.leading ?? 0}px`, }); return styles; } /** * Maps font size to appropriate HTML heading tag * @param fontSize - Key from FontSizeTags enum * @returns Corresponding HTML heading tag */ export function getTextComponentTag(fontSize) { return FontSizeTags[fontSize]; } /** * Gets color value based on color mode with fallback * @param params - Object containing color map, mode and fallback color * @returns Color value as string */ export function getColor({ colorMap, colorMode = DEFAULT_COLOR_MODE, fallback = "FFFFFF", }) { if (!colorMap) return fallback; const color = colorMap[colorMode] || colorMap[DEFAULT_COLOR_MODE]; let colorPoints = ""; switch (color.type) { case "hex": case "alias": return color.value ?? fallback; case "linear": colorPoints = (color.points || []) .map((point) => `${point.color} ${point.percent}%`) .join(", "); return `linear-gradient(${color.degrees}deg, ${colorPoints})`; case "radial": colorPoints = (color.points || []) .map((point) => `${point.color} ${point.percent}%`) .join(", "); return `radial-gradient(${colorPoints})`; default: return fallback; } } /** * Generates CSS border style string * @param border - Border configuration object * @param colorMode - Color mode (light/dark) * @returns CSS border style string */ export function getBorderStyle(border, colorMode = DEFAULT_COLOR_MODE) { if (!border) return ""; const color = getColor({ colorMap: border.color, colorMode }); return `${border.width}px solid ${color}`; } /** * Generates CSS border radius style for corners * @param corners - Corner radius configuration * @returns CSS border radius string */ export function getCornerRadiusStyle(corners) { return { "--border-start-start-radius": `${corners.top_leading}px`, "--border-start-end-radius": `${corners.top_trailing}px`, "--border-end-start-radius": `${corners.bottom_leading}px`, "--border-end-end-radius": `${corners.bottom_trailing}px`, }; } /** * Generates comprehensive component styles including spacing, colors, borders and shadows * @param params - Component style configuration object * @returns CSS style object with component styles */ export function getComponentStyles({ background_color, border, margin, padding, color, colorMode = DEFAULT_COLOR_MODE, shape, shadow, }) { const stylesObject = { "--margin-block-start": "0px", "--margin-inline-end": "0px", "--margin-block-end": "0px", "--margin-inline-start": "0px", "--padding-block-start": "0px", "--padding-inline-end": "0px", "--padding-block-end": "0px", "--padding-inline-start": "0px", "--background": "initial", "--text-color": "initial", "--border": "none", "--border-end-start-radius": "0px", "--border-end-end-radius": "0px", "--border-start-start-radius": "0px", "--border-start-end-radius": "0px", "--shadow": "none", }; if (padding) { Object.assign(stylesObject, getSpacingStyle(padding, "padding")); } if (margin) { Object.assign(stylesObject, getSpacingStyle(margin, "margin")); } if (background_color) { stylesObject["--background"] = getColor({ colorMap: background_color, colorMode, fallback: "transparent", }); } if (color) { stylesObject["--text-color"] = getColor({ colorMap: color, colorMode, fallback: DEFAULT_TEXT_COLOR, }); } if (border) { stylesObject["--border"] = getBorderStyle(border, colorMode); } if (shape?.type === "rectangle" && shape.corners) { Object.assign(stylesObject, getCornerRadiusStyle(shape.corners)); } if (shape?.type === "pill") { Object.assign(stylesObject, getCornerRadiusStyle({ bottom_leading: 9999, bottom_trailing: 9999, top_leading: 9999, top_trailing: 9999, })); } if (shadow) { stylesObject["--shadow"] = `${shadow.x}px ${shadow.y}px ${shadow.radius}px ${getColor({ colorMap: shadow.color, colorMode, fallback: DEFAULT_TEXT_COLOR })}`; } return stylesObject; } function getSizeValue(size) { if (size.type === "fixed") { return `${size.value}px`; } if (size.type === "fit") { return "fit-content"; } if (size.type === "fill") { const userAgent = navigator.userAgent; const isFirefox = userAgent.match(/firefox|fxios/i); return isFirefox ? "-moz-available" : "-webkit-fill-available"; } return "initial"; } /** * Generates size-related CSS styles for components * @param size - Size configuration object * @returns CSS style object with size properties */ export function getSizeStyle(size) { const styles = { "--width": "initial", "--height": "initial", "--flex": "initial", }; const width = getSizeValue(size.width); const height = getSizeValue(size.height); const isGrow = size.width.type === "fill" || size.height.type === "fill"; Object.assign(styles, { "--width": width, "--height": height, "--flex": isGrow ? "initial" : "0 1 auto", }); return styles; } export function getInsetStyles(dimension) { const defaultStyles = { "--inset": "initial", "--transform": "initial", }; switch (dimension.alignment) { case "top_leading": defaultStyles["--inset"] = "0 auto auto 0"; break; case "top": defaultStyles["--inset"] = "auto auto auto 50%"; defaultStyles["--transform"] = "translate(-50%, 0)"; break; case "top_trailing": defaultStyles["--inset"] = "0 0 auto auto"; break; case "leading": defaultStyles["--inset"] = "50% 0 50% 0"; defaultStyles["--transform"] = "translate(0, -50%)"; break; case "center": defaultStyles["--inset"] = "50% auto auto 50%"; defaultStyles["--transform"] = "translate(-50%, -50%)"; break; case "trailing": defaultStyles["--inset"] = "50% 0 50% auto"; defaultStyles["--transform"] = "translate(0, -50%)"; break; case "bottom_leading": defaultStyles["--inset"] = "auto auto 0 0"; break; case "bottom": defaultStyles["--inset"] = "auto 50% 0 auto"; defaultStyles["--transform"] = "translate(50%, 0)"; break; case "bottom_trailing": defaultStyles["--inset"] = "auto 0 0 auto"; break; } return defaultStyles; } /** * Generates dimension-related styles for stack components * @param dimension - Dimension configuration object * @returns CSS style object with flex layout properties */ export function getDimensionStyle(dimension) { const styles = { "--direction": "initial", "--alignment": "initial", "--distribution": "initial", "--position": "relative", "--inset": "initial", "--transform": "initial", }; if (dimension.type !== "zlayer") { Object.assign(styles, { "--direction": StackDirection[dimension.type], "--alignment": StackAlignment[dimension.alignment], }); if (dimension.distribution) { Object.assign(styles, { "--distribution": StackDistribution[dimension.distribution], }); } } return styles; } /** * Generates text-related styles * @param props - Text component properties * @param colorMode - The currently selected ColorMode (dark/light) * @returns CSS style object with text formatting properties */ export function getTextStyles(props, colorMode = DEFAULT_COLOR_MODE) { const { font_size, horizontal_alignment, font_weight, font_name, color } = props; const styles = { "--text-align": "initial", "--font-weight": "initial", "--font-size": "initial", "--font-family": "sans-serif", "--background-clip": "none", "--text-fill-color": "none", "--background": "unset", }; Object.assign(styles, { "--text-align": TextAlignments[horizontal_alignment] || TextAlignments.leading, "--font-weight": FontWeights[font_weight] || FontWeights.regular, "--font-size": FontSizes[font_size] || FontSizes.body_m, "--font-family": font_name || "sans-serif", }); if (color && (color[colorMode]?.type === "linear" || color[colorMode]?.type === "radial")) { Object.assign(styles, { "--background-clip": "text", "--text-fill-color": "transparent", "--background": getColor({ colorMap: color, colorMode }), }); } return styles; } /** * Converts a style object to a CSS string * @param styles - Object containing CSS properties and values * @returns CSS string */ export function stringifyStyles(styles) { return Object.entries(styles) .map(([key, value]) => `${key}: ${value}`) .join("; "); } /** * Given an instance of PaywallData, returns the id of the first package marked as `is_selected_by_default` if any. * @param paywallData * @returns the id of the first package marked as `is_selected_by_default` or undefined */ export function findSelectedPackageId(paywallData) { const traverseNode = (node) => { if (node.type === "package" && node.is_selected_by_default) { return node; } if (node.components && Array.isArray(node.components)) { for (const c of node.components) { const pkg = traverseNode(c); if (pkg) { return pkg; } } } if (node.stack !== undefined) { const pkg = traverseNode(node.stack); if (pkg) { return pkg; } } return undefined; }; const p = traverseNode(paywallData.components_config.base.stack); if (p === undefined) { return undefined; } return p.package_id; } export const getActiveStateProps = (overrides, componentState) => { if (!componentState) return {}; const activeStateKeys = getComponentActiveStateKeys(componentState); const activeStateProps = activeStateKeys.reduce((props, key) => { if (overrides) { const styles = overrides?.states?.[key] || {}; return { ...props, ...styles }; } return props; }, {}); return activeStateProps; }; const getComponentActiveStateKeys = (componentState) => { if (!componentState) return []; const stateKeys = Object.entries(componentState).reduce((activeStates, [stateKey, stateValue]) => { if (stateValue) activeStates.push(stateKey); return activeStates; }, []); return stateKeys; }; export function prefixObject(object, prefix) { if (!object) return {}; if (!prefix) return object; return Object.entries(object).reduce((acc, [key, value]) => { const replacedKey = key.replace(/^--/, `${prefix}-`); acc[`--${replacedKey}`] = value; return acc; }, {}); } export function getMaskPath(props) { const { mask_shape: maskShape, imageAspectRatio } = props; let maskPath = ""; if (maskShape?.type === "concave") { maskPath = `M 0 0 H 100 V ${imageAspectRatio * 100} Q 50 ${imageAspectRatio * 80} 0 ${imageAspectRatio * 100} Z`; } else if (maskShape?.type === "convex") { maskPath = `M 0 0 H 100 V ${imageAspectRatio * 80} Q 50 ${imageAspectRatio * 120} 0 ${imageAspectRatio * 80} Z`; } else { maskPath = `M 0 0 H 100 V ${imageAspectRatio * 100} H 0 Z`; } return maskPath; } /** * Generates mask styles for images * @param maskShape - Shape configuration for image mask * @returns CSS style object with mask properties */ export const getMaskStyle = (maskShape) => { const maskStyles = { "--border-end-start-radius": "0px", "--border-end-end-radius": "0px", "--border-start-start-radius": "0px", "--border-start-end-radius": "0px", }; if (maskShape?.corners) { Object.assign(maskStyles, getCornerRadiusStyle(maskShape.corners)); } return maskStyles; }; export function getLinearGradientAngle(props) { if (props.color_overlay?.[props.purchaseState.colorMode]?.type !== "linear") { return { x1: "0%", y1: "0%", x2: "0%", y2: "0%" }; } const { color_overlay: colorOverlay } = props; const angle = colorOverlay?.[DEFAULT_COLOR_MODE]?.degrees || 0; const x1 = "50%"; const y1 = "0%"; const x2 = `${Math.round(50 + Math.sin(((angle + 90) * Math.PI) / 90) * 50)}%`; const y2 = `${Math.round(50 - Math.cos(((angle + 90) * Math.PI) / 90) * 50)}%`; return { x1, y1, x2, y2 }; }