@revenuecat/purchases-ui-js
Version:
Web components for Paywalls. Powered by RevenueCat
265 lines (264 loc) • 10.5 kB
JavaScript
import { getTextComponentStyles } from "../text/text-utils";
import { getActiveStateProps, getComponentStyles, getDimensionStyle, getInsetStyles, getSizeStyle, prefixObject, stringifyStyles, } from "../../utils/style-utils";
/**
* Generates comprehensive styles for stack components by combining component, dimension and size styles
* @param props - Stack component properties including background, spacing, size, border etc.
* @returns CSS style variables object with all stack-specific styles
*/
export const getStackComponentStyles = (props) => {
const { background_color, margin, padding, spacing, size, border, shape, shadow, dimension, componentState, overrides, purchaseState, zStackChildStyles, } = props;
const stackStyles = {
"--flex": "0 1 auto",
"--height": "initial",
"--width": "initial",
"--spacing": `${spacing || 0}px`,
"--direction": "unset",
"--alignment": "unset",
"--distribution": "flex-start",
"--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": "unset",
"--text-color": "inherit",
"--border": "none",
"--border-end-start-radius": "0px",
"--border-end-end-radius": "0px",
"--border-start-start-radius": "0px",
"--border-start-end-radius": "0px",
"--shadow": "none",
"--position": "relative",
"--transform": "initial",
"--inset": "initial",
};
const activeStateProps = getActiveStateProps(overrides, componentState);
Object.assign(stackStyles, getComponentStyles({
background_color,
margin,
padding,
border,
colorMode: purchaseState.colorMode,
shape,
shadow,
...activeStateProps,
}), getDimensionStyle({
...dimension,
...activeStateProps,
}), getSizeStyle({ ...size, ...activeStateProps }));
Object.assign(stackStyles, zStackChildStyles);
const prefixedStyles = prefixObject(stackStyles, "stack");
return stringifyStyles(prefixedStyles);
};
/**
* Generates styles for badge component within a stack
* @param props - Stack component properties containing badge configuration
* @returns CSS style object with badge-specific styles including positioning, dimensions and appearance
*/
export function getBadgeStyles(props) {
const { badge } = props;
if (!badge)
return "";
const styles = {
"--inset": "unset",
"--transform": "unset",
"--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": "unset",
"--text-color": "inherit",
"--border": "none",
"--border-end-start-radius": "0px",
"--border-end-end-radius": "0px",
"--border-start-start-radius": "0px",
"--border-start-end-radius": "0px",
"--shadow": "none",
"--badge-width": "unset",
"--z-index": "unset",
};
// Calculating this as a zIndex alignment since it behaves in the same way
// except there's no center alignment
Object.assign(styles, getInsetStyles({
type: "zlayer",
alignment: badge.alignment,
}), getComponentStyles({
...badge,
background_color: badge.background_color,
color: badge.color,
colorMode: props.purchaseState.colorMode,
}));
if (badge.style === "overlay") {
// Disable vertical margin for nested & center aligned badges
styles["--margin-block-start"] = "0px";
styles["--margin-block-end"] = "0px";
// Vertically center the badge to the edge of the stack
styles["--transform"] = getBadgeTransformStyles(badge, 0, -(props.border?.width || 0));
}
if (badge.style === "edge_to_edge") {
styles["--margin-block-start"] = "0px";
styles["--margin-block-end"] = "0px";
styles["--margin-inline-start"] = "0px";
styles["--margin-inline-end"] = "0px";
// Shared props between top and bottom
if (badge.alignment === "top" || badge.alignment === "bottom") {
styles["--badge-width"] =
`calc(100% + ${(props.border?.width || 0) * 2}px)`;
styles["--z-index"] = "-1";
const highestRadius = Math.max(props.shape?.corners?.top_leading || 0, props.shape?.corners?.top_trailing || 0);
// one offs for top and bottom
if (badge.alignment === "top") {
styles["--padding-block-end"] =
`${highestRadius + badge.padding.bottom}px`;
styles["--transform"] = getBadgeTransformStyles(badge, highestRadius);
}
if (badge.alignment === "bottom") {
styles["--padding-block-start"] =
`${highestRadius + badge.padding.top}px`;
styles["--transform"] = getBadgeTransformStyles(badge, -highestRadius);
}
}
else {
styles["--transform"] = getBadgeTransformStyles(badge);
Object.assign(styles, getBadgeBorderRadiusStyles(props));
}
}
const prefixedStyles = prefixObject(styles, "stack-badge");
return stringifyStyles(prefixedStyles);
}
const oppositeCornerDictionary = {
top_leading: "bottom_trailing",
top_trailing: "bottom_leading",
bottom_leading: "top_trailing",
bottom_trailing: "top_leading",
};
const propToCssPropDictionary = {
top_leading: "--border-start-start-radius",
top_trailing: "--border-start-end-radius",
bottom_leading: "--border-end-start-radius",
bottom_trailing: "--border-end-end-radius",
};
function getBadgeBorderRadiusStyles(props) {
const styles = {
"--border-end-start-radius": "0px",
"--border-end-end-radius": "0px",
"--border-start-start-radius": "0px",
"--border-start-end-radius": "0px",
};
const { badge } = props;
if (!badge)
return styles;
const borderWidth = props.border?.width || 0;
const badgeAlignment = badge.alignment;
// badge inner radius
styles[propToCssPropDictionary[oppositeCornerDictionary[badgeAlignment]]] =
`${badge.shape?.corners[oppositeCornerDictionary[badgeAlignment]]}px`;
// badge outer radius
styles[propToCssPropDictionary[badgeAlignment]] =
`${props.shape?.corners[badgeAlignment] - borderWidth}px`;
return styles;
}
/**
* Generates CSS transform styles for badge positioning
* @param badge - Badge configuration from StackProps
* @param verticalOffset - Optional vertical offset in pixels for edge-to-edge badges
* @returns CSS transform string for badge positioning
*/
function getBadgeTransformStyles(badge, verticalOffset = 0, horizontalOffset = 0) {
if (!badge)
return "";
if (badge.style === "overlay") {
if (badge.alignment === "top_leading") {
return `translate(${horizontalOffset}px, calc(-50% + ${verticalOffset}px))`;
}
if (badge.alignment === "top_trailing") {
return `translate(calc(0% - ${horizontalOffset}px), calc(-50% + ${verticalOffset}px))`;
}
if (badge.alignment === "bottom_leading") {
return `translate(${horizontalOffset}px, calc(50% + ${verticalOffset}px))`;
}
if (badge.alignment === "bottom_trailing") {
return `translate(calc(0% - ${horizontalOffset}px), calc(50% + ${verticalOffset}px))`;
}
if (badge.alignment === "top") {
return `translate(calc(-50% + ${horizontalOffset}px), calc(-50% + ${verticalOffset}px))`;
}
if (badge.alignment === "bottom") {
return `translate(calc(50% + ${horizontalOffset}px), calc(50% + ${verticalOffset}px))`;
}
}
if (badge.style === "edge_to_edge") {
if (badge.alignment === "top") {
return `translate(calc(-50% + ${horizontalOffset}px), calc(-100% - ${verticalOffset}px + 16px))`;
}
else if (badge.alignment === "bottom") {
return `translate(calc(50% + ${horizontalOffset}px), calc(100% + ${verticalOffset}px))`;
}
}
return "";
}
/**
* Generates text styles and HTML tag for a stack's badge component
* @param props - Stack component properties containing badge configuration
* @returns Object containing:
* - tagToRender: HTML tag to use for the badge text
* - textStyles: CSS styles string for the badge text
*/
export function getStackBadgeTextStyles(props) {
const { badge } = props;
if (!badge)
return { tagToRender: "", textStyles: "" };
const { tagToRender, textStyles } = getTextComponentStyles({
id: props.id,
labels: props.labels,
purchaseState: props.purchaseState,
...badge,
components: [],
type: "text",
size: {
width: { type: "fit" },
height: { type: "fit" },
},
color: badge?.color,
name: props.name,
});
const resetSpacing = {
"--text-margin-block-start": "0px",
"--text-margin-inline-end": "0px",
"--text-margin-block-end": "0px",
"--text-margin-inline-start": "0px",
"--text-padding-block-start": "0px",
"--text-padding-inline-end": "0px",
"--text-padding-block-end": "0px",
"--text-padding-inline-start": "0px",
};
const stylesObject = {
...textStyles,
...resetSpacing,
};
const stringifiedStyles = stringifyStyles(stylesObject);
return {
tagToRender,
textStyles: stringifiedStyles,
};
}
export function getZStackChildStyles(props) {
const { dimension } = props;
const baseStyles = {
"--inset": "initial",
"--transform": "initial",
"--position": "relative",
};
if (dimension.type !== "zlayer")
return;
const insetStyles = getInsetStyles({ ...dimension, type: "zlayer" });
Object.assign(baseStyles, { ...insetStyles, "--position": "absolute" });
return baseStyles;
}