@revenuecat/purchases-ui-js 0.0.19 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,6 +64,38 @@
64
64
  mask_shape: { type: "rectangle" },
65
65
  }}
66
66
  />
67
+
68
+ <!-- Rounded Rectangle story -->
69
+ <Story
70
+ name="Rounded Rectangle"
71
+ args={{
72
+ purchaseState,
73
+ id: "example-id",
74
+ fit_mode: "fit",
75
+ size: {
76
+ width: { type: "fill" },
77
+ height: { type: "fill" },
78
+ },
79
+ source: {
80
+ light: {
81
+ original: "https://placehold.co/600x400",
82
+ heic: "https://placehold.co/600x400",
83
+ heic_low_res: "https://placehold.co/600x400",
84
+ webp: "https://placehold.co/600x400",
85
+ webp_low_res: "https://placehold.co/600x400",
86
+ },
87
+ },
88
+ mask_shape: {
89
+ type: "rectangle",
90
+ corners: {
91
+ top_leading: 32,
92
+ top_trailing: 32,
93
+ bottom_leading: 32,
94
+ bottom_trailing: 32,
95
+ },
96
+ },
97
+ }}
98
+ />
67
99
  <!-- Circle story -->
68
100
  <Story
69
101
  name="Circle"
@@ -133,9 +165,50 @@
133
165
  mask_shape: { type: "convex" },
134
166
  }}
135
167
  />
136
- <!-- Gradient story -->
168
+ <!-- Radial Gradient story -->
169
+ <Story
170
+ name="Overlay Radial Gradient"
171
+ args={{
172
+ purchaseState,
173
+ id: "example-id",
174
+ fit_mode: "fit",
175
+ size: {
176
+ width: { type: "fill" },
177
+ height: { type: "fill" },
178
+ },
179
+ source: {
180
+ light: {
181
+ original: "https://placehold.co/600x400",
182
+ heic: "https://placehold.co/600x400",
183
+ heic_low_res: "https://placehold.co/600x400",
184
+ webp: "https://placehold.co/600x400",
185
+ webp_low_res: "https://placehold.co/600x400",
186
+ },
187
+ },
188
+ color_overlay: {
189
+ dark: {
190
+ type: "hex",
191
+ value: "#000000FF",
192
+ },
193
+ light: {
194
+ points: [
195
+ {
196
+ color: "#020024ff",
197
+ percent: 0,
198
+ },
199
+ {
200
+ color: "#00d4ff00",
201
+ percent: 100,
202
+ },
203
+ ],
204
+ type: "radial",
205
+ },
206
+ },
207
+ }}
208
+ />
209
+ <!-- Linear Gradient story -->
137
210
  <Story
138
- name="Background Gradient"
211
+ name="Overlay Linear Gradient"
139
212
  args={{
140
213
  purchaseState,
141
214
  id: "example-id",
@@ -153,19 +226,57 @@
153
226
  webp_low_res: "https://placehold.co/600x400",
154
227
  },
155
228
  },
156
- gradient_colors: [
157
- {
158
- dark: { type: "hex", value: "#FFFFFF80" },
159
- light: { type: "hex", value: "#FFFFFF80" },
229
+ color_overlay: {
230
+ dark: {
231
+ type: "hex",
232
+ value: "#000000FF",
233
+ },
234
+ light: {
235
+ degrees: 180,
236
+ points: [
237
+ {
238
+ color: "#020024ff",
239
+ percent: 0,
240
+ },
241
+ {
242
+ color: "#00d4ff00",
243
+ percent: 100,
244
+ },
245
+ ],
246
+ type: "linear",
247
+ },
248
+ },
249
+ }}
250
+ />
251
+ <!-- Solid overlay -->
252
+ <Story
253
+ name="Overlay Solid"
254
+ args={{
255
+ purchaseState,
256
+ id: "example-id",
257
+ fit_mode: "fit",
258
+ size: {
259
+ width: { type: "fill" },
260
+ height: { type: "fill" },
261
+ },
262
+ source: {
263
+ light: {
264
+ original: "https://placehold.co/600x400",
265
+ heic: "https://placehold.co/600x400",
266
+ heic_low_res: "https://placehold.co/600x400",
267
+ webp: "https://placehold.co/600x400",
268
+ webp_low_res: "https://placehold.co/600x400",
160
269
  },
161
- {
162
- dark: { type: "hex", value: "#FFFFFF80" },
163
- light: { type: "hex", value: "#FFFFFF80" },
270
+ },
271
+ color_overlay: {
272
+ dark: {
273
+ type: "hex",
274
+ value: "#000000FF",
164
275
  },
165
- {
166
- dark: { type: "hex", value: "#000000" },
167
- light: { type: "hex", value: "#000000" },
276
+ light: {
277
+ type: "hex",
278
+ value: "#e7c00069",
168
279
  },
169
- ],
280
+ },
170
281
  }}
171
282
  />
@@ -4,13 +4,23 @@
4
4
 
5
5
  const { id, source, purchaseState, ...restProps }: ImageProps = $props();
6
6
 
7
- const { gradientStyles, imageStyles } = $derived(
7
+ let imageAspectRatio = $state(0);
8
+ let imageElement: HTMLImageElement | null;
9
+
10
+ // Calculate aspect ratio once image loads
11
+ function onImageLoad() {
12
+ if (imageElement) {
13
+ imageAspectRatio = imageElement.naturalHeight / imageElement.naturalWidth;
14
+ }
15
+ }
16
+ const { imageStyles, maskPath, linearGradientAngle } = $derived(
8
17
  getImageComponentStyles({
9
18
  id,
10
19
  colorMode: purchaseState.colorMode,
11
20
  source,
12
21
  purchaseState,
13
22
  ...restProps,
23
+ imageAspectRatio,
14
24
  }),
15
25
  );
16
26
 
@@ -24,24 +34,110 @@
24
34
  });
25
35
  </script>
26
36
 
27
- <div
37
+ <img
38
+ src={imageSource}
39
+ bind:this={imageElement}
40
+ onload={onImageLoad}
41
+ style="display: none;"
42
+ alt=""
43
+ />
44
+
45
+ <svg
28
46
  class="rc-pw-image-container"
29
47
  id={`rc-pw-image-container-${id}`}
30
48
  style={imageStyles}
49
+ preserveAspectRatio="xMidYMid slice"
50
+ viewBox={`0 0 100 ${imageAspectRatio * 100}`}
31
51
  >
32
- <img class="rc-pw-image" src={imageSource} alt="" {id} />
33
- <span class="rc-pw-image-overlay" style={gradientStyles}></span>
34
- </div>
52
+ <defs>
53
+ <clipPath id={`clip-path-${id}`}>
54
+ {#if restProps.mask_shape?.type === "circle"}
55
+ <ellipse
56
+ cx="50"
57
+ cy={imageAspectRatio * 50}
58
+ rx="50"
59
+ ry={imageAspectRatio * 50}
60
+ />
61
+ {:else}
62
+ <path d={maskPath} />
63
+ {/if}
64
+ </clipPath>
65
+
66
+ {#if restProps.color_overlay?.[purchaseState.colorMode]?.type === "linear"}
67
+ <linearGradient
68
+ id={`gradient-${id}`}
69
+ x1={linearGradientAngle.x1}
70
+ y1={linearGradientAngle.y1}
71
+ x2={linearGradientAngle.x2}
72
+ y2={linearGradientAngle.y2}
73
+ >
74
+ {#each restProps.color_overlay?.[purchaseState.colorMode]?.points || restProps.color_overlay?.["light"]?.points || [] as stop}
75
+ <stop
76
+ offset={`${stop.percent}%`}
77
+ style={`stop-color: ${stop.color}`}
78
+ />
79
+ {/each}
80
+ </linearGradient>
81
+ {:else if restProps.color_overlay?.[purchaseState.colorMode]?.type === "radial"}
82
+ <radialGradient
83
+ id={`gradient-${id}`}
84
+ cx="50%"
85
+ cy="50%"
86
+ r="50%"
87
+ fx="50%"
88
+ fy="50%"
89
+ >
90
+ {#each restProps.color_overlay?.[purchaseState.colorMode]?.points || restProps.color_overlay?.["light"]?.points || [] as stop}
91
+ <stop
92
+ offset={`${stop.percent}%`}
93
+ style={`stop-color: ${stop.color}`}
94
+ />
95
+ {/each}
96
+ </radialGradient>
97
+ {:else if restProps.color_overlay?.[purchaseState.colorMode]?.type === "hex"}
98
+ <linearGradient id={`gradient-${id}`}>
99
+ <stop
100
+ offset="0%"
101
+ style={`stop-color: ${
102
+ restProps.color_overlay[purchaseState.colorMode]?.value ||
103
+ restProps.color_overlay?.["light"]?.value
104
+ }`}
105
+ />
106
+ </linearGradient>
107
+ {/if}
108
+ </defs>
109
+
110
+ <image
111
+ class="rc-pw-image"
112
+ href={imageSource}
113
+ x="0"
114
+ y="0"
115
+ width="100"
116
+ height={imageAspectRatio * 100}
117
+ clip-path={`url(#clip-path-${id})`}
118
+ preserveAspectRatio="xMidYMid slice"
119
+ {id}
120
+ />
121
+
122
+ <rect
123
+ class="rc-pw-image-overlay"
124
+ x="0"
125
+ y="0"
126
+ width="100"
127
+ height={imageAspectRatio * 100}
128
+ clip-path={`url(#clip-path-${id})`}
129
+ fill={`url(#gradient-${id})`}
130
+ />
131
+ </svg>
35
132
 
36
133
  <style>
37
134
  .rc-pw-image-container {
38
- position: relative;
39
- overflow: hidden;
40
135
  border-end-start-radius: var(--image-border-end-start-radius, 0px);
41
136
  border-end-end-radius: var(--image-border-end-end-radius, 0px);
42
137
  border-start-start-radius: var(--image-border-start-start-radius, 0px);
43
138
  border-start-end-radius: var(--image-border-start-end-radius, 0px);
44
- clip-path: var(--image-clip-path, initial);
139
+ position: relative;
140
+ overflow: hidden;
45
141
  display: flex;
46
142
  flex: var(--image-flex, 1 1 auto);
47
143
  position: var(--image-position, relative);
@@ -61,6 +157,5 @@
61
157
  .rc-pw-image-overlay {
62
158
  position: absolute;
63
159
  inset: 0;
64
- background: var(--image-background, none);
65
160
  }
66
161
  </style>
@@ -4,7 +4,15 @@ import type { ImageProps } from "../../data/entities";
4
4
  * @param props - Image component properties including gradient, mask and size
5
5
  * @returns Object containing image style variables and gradient style variables
6
6
  */
7
- export declare const getImageComponentStyles: (props: ImageProps) => {
7
+ export declare const getImageComponentStyles: (props: ImageProps & {
8
+ imageAspectRatio: number;
9
+ }) => {
8
10
  imageStyles: string;
9
- gradientStyles: string;
11
+ maskPath: string;
12
+ linearGradientAngle: {
13
+ x1: string;
14
+ y1: string;
15
+ x2: string;
16
+ y2: string;
17
+ };
10
18
  };
@@ -1,14 +1,11 @@
1
- import { getActiveStateProps, getGradientStyle, getMaskStyle, getSizeStyle, prefixObject, stringifyStyles, } from "../../utils/style-utils";
1
+ import { getActiveStateProps, getLinearGradientAngle, getMaskPath, getMaskStyle, getSizeStyle, prefixObject, stringifyStyles, } from "../../utils/style-utils";
2
2
  /**
3
3
  * Generates comprehensive styles for image components by combining gradient and size styles
4
4
  * @param props - Image component properties including gradient, mask and size
5
5
  * @returns Object containing image style variables and gradient style variables
6
6
  */
7
7
  export const getImageComponentStyles = (props) => {
8
- const { gradient_colors, mask_shape, size,
9
- // max_height, // TODO: implement this. still waiting on spec
10
- // override_source_lid, // TODO: Implement this once structure is defined
11
- overrides, componentState, purchaseState, zStackChildStyles, } = props;
8
+ const { size, overrides, componentState, zStackChildStyles } = props;
12
9
  const imageStyles = {
13
10
  "--height": "unset",
14
11
  "--width": "unset",
@@ -21,21 +18,16 @@ export const getImageComponentStyles = (props) => {
21
18
  "--inset": "0",
22
19
  "--transform": "initial",
23
20
  };
24
- Object.assign(imageStyles, zStackChildStyles);
25
- const backgroundStyles = {
26
- "--background": "none",
27
- };
28
21
  const activeStateProps = getActiveStateProps(overrides, componentState);
29
- Object.assign(backgroundStyles, getGradientStyle(purchaseState.colorMode, activeStateProps?.gradient_colors || gradient_colors));
22
+ Object.assign(imageStyles, zStackChildStyles);
30
23
  Object.assign(imageStyles, getSizeStyle({ ...size, ...activeStateProps }));
31
- Object.assign(imageStyles, getMaskStyle({
32
- ...mask_shape,
33
- ...activeStateProps,
34
- }));
24
+ Object.assign(imageStyles, getMaskStyle(props.mask_shape));
35
25
  const prefixedImageStyles = prefixObject(imageStyles, "image");
36
- const prefixedGradientStyles = prefixObject(backgroundStyles, "image");
26
+ const maskPath = getMaskPath(props);
27
+ const linearGradientAngle = getLinearGradientAngle(props);
37
28
  return {
38
29
  imageStyles: stringifyStyles(prefixedImageStyles),
39
- gradientStyles: stringifyStyles(prefixedGradientStyles),
30
+ maskPath,
31
+ linearGradientAngle,
40
32
  };
41
33
  };
@@ -194,7 +194,7 @@ export interface ImageProps extends SharedComponentProps {
194
194
  fit_mode: FitTypes;
195
195
  size: SizeType;
196
196
  source: ImageSourceType;
197
- gradient_colors?: ColorType[];
197
+ color_overlay?: ColorType;
198
198
  mask_shape?: ImageMaskShapeType;
199
199
  max_height?: number;
200
200
  override_source_lid?: string;
@@ -1,4 +1,4 @@
1
- import type { ComponentState, ImageMaskShapeType, PaywallData, TextNodeProps } from "../data/entities.js";
1
+ import type { ComponentState, ImageMaskShapeType, ImageProps, PaywallData, TextNodeProps } from "../data/entities.js";
2
2
  import { type BorderType, type ColorMode, type ColorType, type CornerRadiusType, type DimensionType, FontSizeTags, type ShadowType, type ShapeType, type SizeType, type Spacing } from "../types.js";
3
3
  type MarginVariables = {
4
4
  "--margin-block-start": string;
@@ -81,29 +81,6 @@ type SizeStyleVariables = {
81
81
  * @returns CSS style object with size properties
82
82
  */
83
83
  export declare function getSizeStyle(size: SizeType): SizeStyleVariables;
84
- type GradientStyleVariables = {
85
- "--background": string;
86
- };
87
- /**
88
- * Generates gradient background styles
89
- * @param colorMode - Color mode (light/dark)
90
- * @param gradientColors - Array of colors for gradient
91
- * @returns CSS style object with gradient background
92
- */
93
- export declare function getGradientStyle(colorMode: ColorMode, gradientColors?: ColorType[]): GradientStyleVariables;
94
- type MaskStyleVariables = {
95
- "--border-start-start-radius": string;
96
- "--border-start-end-radius": string;
97
- "--border-end-start-radius": string;
98
- "--border-end-end-radius": string;
99
- "--clip-path": string;
100
- };
101
- /**
102
- * Generates mask styles for images
103
- * @param maskShape - Shape configuration for image mask
104
- * @returns CSS style object with mask properties
105
- */
106
- export declare const getMaskStyle: (maskShape?: ImageMaskShapeType) => MaskStyleVariables;
107
84
  type InsetStyleVariables = {
108
85
  "--inset": string;
109
86
  "--transform": string;
@@ -155,4 +132,27 @@ export declare const getActiveStateProps: <T>(overrides?: {
155
132
  states?: Record<string, T>;
156
133
  }, componentState?: ComponentState) => T;
157
134
  export declare function prefixObject(object?: Record<string, string | number>, prefix?: string): Record<string, string | number>;
135
+ export declare function getMaskPath(props: ImageProps & {
136
+ imageAspectRatio: number;
137
+ }): string;
138
+ type MaskStyleVariables = {
139
+ "--border-start-start-radius": string;
140
+ "--border-start-end-radius": string;
141
+ "--border-end-start-radius": string;
142
+ "--border-end-end-radius": string;
143
+ };
144
+ /**
145
+ * Generates mask styles for images
146
+ * @param maskShape - Shape configuration for image mask
147
+ * @returns CSS style object with mask properties
148
+ */
149
+ export declare const getMaskStyle: (maskShape?: ImageMaskShapeType) => MaskStyleVariables;
150
+ export declare function getLinearGradientAngle(props: ImageProps & {
151
+ imageAspectRatio: number;
152
+ }): {
153
+ x1: string;
154
+ y1: string;
155
+ x2: string;
156
+ y2: string;
157
+ };
158
158
  export {};
@@ -182,59 +182,6 @@ export function getSizeStyle(size) {
182
182
  });
183
183
  return styles;
184
184
  }
185
- /**
186
- * Generates gradient background styles
187
- * @param colorMode - Color mode (light/dark)
188
- * @param gradientColors - Array of colors for gradient
189
- * @returns CSS style object with gradient background
190
- */
191
- export function getGradientStyle(colorMode, gradientColors) {
192
- if (!gradientColors)
193
- return { "--background": "none" };
194
- return {
195
- "--background": `linear-gradient(${gradientColors.map((color) => color[colorMode]?.value || color["light"].value).join(", ")})`,
196
- };
197
- }
198
- /**
199
- * Generates mask styles for images
200
- * @param maskShape - Shape configuration for image mask
201
- * @returns CSS style object with mask properties
202
- */
203
- export const getMaskStyle = (maskShape) => {
204
- const maskStyles = {
205
- "--border-end-start-radius": "0px",
206
- "--border-end-end-radius": "0px",
207
- "--border-start-start-radius": "0px",
208
- "--border-start-end-radius": "0px",
209
- "--clip-path": "none",
210
- };
211
- if (maskShape?.type === "circle") {
212
- Object.assign(maskStyles, {
213
- "--border-end-start-radius": "50%",
214
- "--border-end-end-radius": "50%",
215
- "--border-start-start-radius": "50%",
216
- "--border-start-end-radius": "50%",
217
- });
218
- }
219
- if (maskShape?.type === "rectangle" && maskShape.corners) {
220
- Object.assign(maskStyles, getCornerRadiusStyle(maskShape.corners));
221
- }
222
- // TODO: rework this implementation
223
- if (maskShape?.type === "convex") {
224
- Object.assign(maskStyles, {
225
- "--border-start-start-radius": "0%",
226
- "--border-start-end-radius": "0%",
227
- "--border-end-start-radius": "50%",
228
- "--border-end-end-radius": "50%",
229
- });
230
- }
231
- if (maskShape?.type === "concave") {
232
- Object.assign(maskStyles, {
233
- "--clip-path": "polygon(0 0, 100% 0, 100% 100%, 50% 110%, 0 100%);",
234
- });
235
- }
236
- return maskStyles;
237
- };
238
185
  export function getInsetStyles(dimension) {
239
186
  const defaultStyles = {
240
187
  "--inset": "initial",
@@ -413,3 +360,54 @@ export function prefixObject(object, prefix) {
413
360
  return acc;
414
361
  }, {});
415
362
  }
363
+ export function getMaskPath(props) {
364
+ const { mask_shape: maskShape, imageAspectRatio } = props;
365
+ let maskPath = "";
366
+ if (maskShape?.type === "concave") {
367
+ maskPath = `M 0 0
368
+ H 100
369
+ V ${imageAspectRatio * 100}
370
+ Q 50 ${imageAspectRatio * 80} 0 ${imageAspectRatio * 100}
371
+ Z`;
372
+ }
373
+ else if (maskShape?.type === "convex") {
374
+ maskPath = `M 0 0
375
+ H 100
376
+ V ${imageAspectRatio * 80}
377
+ Q 50 ${imageAspectRatio * 120} 0 ${imageAspectRatio * 80}
378
+ Z`;
379
+ }
380
+ else {
381
+ maskPath = `M 0 0 H 100 V ${imageAspectRatio * 100} H 0 Z`;
382
+ }
383
+ return maskPath;
384
+ }
385
+ /**
386
+ * Generates mask styles for images
387
+ * @param maskShape - Shape configuration for image mask
388
+ * @returns CSS style object with mask properties
389
+ */
390
+ export const getMaskStyle = (maskShape) => {
391
+ const maskStyles = {
392
+ "--border-end-start-radius": "0px",
393
+ "--border-end-end-radius": "0px",
394
+ "--border-start-start-radius": "0px",
395
+ "--border-start-end-radius": "0px",
396
+ };
397
+ if (maskShape?.corners) {
398
+ Object.assign(maskStyles, getCornerRadiusStyle(maskShape.corners));
399
+ }
400
+ return maskStyles;
401
+ };
402
+ export function getLinearGradientAngle(props) {
403
+ if (props.color_overlay?.[props.purchaseState.colorMode]?.type !== "linear") {
404
+ return { x1: "0%", y1: "0%", x2: "0%", y2: "0%" };
405
+ }
406
+ const { color_overlay: colorOverlay } = props;
407
+ const angle = colorOverlay?.["light"]?.degrees || 0;
408
+ const x1 = "50%";
409
+ const y1 = "0%";
410
+ const x2 = `${Math.round(50 + Math.sin(((angle + 90) * Math.PI) / 90) * 50)}%`;
411
+ const y2 = `${Math.round(50 - Math.cos(((angle + 90) * Math.PI) / 90) * 50)}%`;
412
+ return { x1, y1, x2, y2 };
413
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@revenuecat/purchases-ui-js",
3
3
  "description": "Web components for Paywalls. Powered by RevenueCat",
4
4
  "private": false,
5
- "version": "0.0.19",
5
+ "version": "0.0.20",
6
6
  "author": {
7
7
  "name": "RevenueCat, Inc."
8
8
  },