react-native-revnix 1.4.0
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.
- package/LICENSE +21 -0
- package/README.md +766 -0
- package/dist/blocks/background.d.ts +101 -0
- package/dist/blocks/background.js +165 -0
- package/dist/blocks/color.d.ts +88 -0
- package/dist/blocks/color.js +329 -0
- package/dist/blocks/locale.d.ts +42 -0
- package/dist/blocks/locale.js +263 -0
- package/dist/blocks/selection.d.ts +49 -0
- package/dist/blocks/selection.js +109 -0
- package/dist/blocks/style.d.ts +93 -0
- package/dist/blocks/style.js +365 -0
- package/dist/blocks/tags.d.ts +12 -0
- package/dist/blocks/tags.js +121 -0
- package/dist/blocks/types.d.ts +301 -0
- package/dist/blocks/types.js +44 -0
- package/dist/client.d.ts +402 -0
- package/dist/client.js +1653 -0
- package/dist/device.d.ts +29 -0
- package/dist/device.js +137 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.js +68 -0
- package/dist/esm/blocks/background.js +160 -0
- package/dist/esm/blocks/color.js +318 -0
- package/dist/esm/blocks/locale.js +255 -0
- package/dist/esm/blocks/selection.js +100 -0
- package/dist/esm/blocks/style.js +356 -0
- package/dist/esm/blocks/tags.js +117 -0
- package/dist/esm/blocks/types.js +40 -0
- package/dist/esm/client.js +1649 -0
- package/dist/esm/device.js +130 -0
- package/dist/esm/errors.js +59 -0
- package/dist/esm/iap.js +101 -0
- package/dist/esm/implicit.js +63 -0
- package/dist/esm/index.js +29 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/storage.js +78 -0
- package/dist/esm/types.js +4 -0
- package/dist/esm/ui/RevnixBlockPaywall.js +706 -0
- package/dist/esm/ui/RevnixPaywall.js +890 -0
- package/dist/esm/ui/index.js +6 -0
- package/dist/esm/version.js +4 -0
- package/dist/iap.d.ts +20 -0
- package/dist/iap.js +104 -0
- package/dist/implicit.d.ts +69 -0
- package/dist/implicit.js +67 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +50 -0
- package/dist/storage.d.ts +14 -0
- package/dist/storage.js +85 -0
- package/dist/types.d.ts +463 -0
- package/dist/types.js +5 -0
- package/dist/ui/RevnixBlockPaywall.d.ts +96 -0
- package/dist/ui/RevnixBlockPaywall.js +742 -0
- package/dist/ui/RevnixPaywall.d.ts +148 -0
- package/dist/ui/RevnixPaywall.js +927 -0
- package/dist/ui/index.d.ts +7 -0
- package/dist/ui/index.js +25 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/package.json +79 -0
- package/src/blocks/background.ts +232 -0
- package/src/blocks/color.ts +352 -0
- package/src/blocks/locale.ts +271 -0
- package/src/blocks/paywall-background-wire.json +12 -0
- package/src/blocks/paywall-selection-wire.json +149 -0
- package/src/blocks/selection.ts +161 -0
- package/src/blocks/style.ts +411 -0
- package/src/blocks/tags.ts +125 -0
- package/src/blocks/types.ts +352 -0
- package/src/client.ts +1993 -0
- package/src/device.ts +146 -0
- package/src/errors.ts +69 -0
- package/src/iap.ts +137 -0
- package/src/implicit.ts +129 -0
- package/src/index.ts +82 -0
- package/src/storage.ts +92 -0
- package/src/types.ts +528 -0
- package/src/ui/RevnixBlockPaywall.tsx +1423 -0
- package/src/ui/RevnixPaywall.tsx +1542 -0
- package/src/ui/index.ts +43 -0
- package/src/version.ts +4 -0
- package/ui.d.ts +1 -0
- package/ui.js +5 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export type BackgroundFit = "cover" | "contain";
|
|
2
|
+
export interface BackgroundImage {
|
|
3
|
+
/** An https URL the device can load — Convex storage or any CDN. */
|
|
4
|
+
url: string;
|
|
5
|
+
/** Default "cover". */
|
|
6
|
+
fit?: BackgroundFit;
|
|
7
|
+
/** Focal point in percent (0–100, 50/50 = centered): the part of the
|
|
8
|
+
* photo that must survive a cover-crop. */
|
|
9
|
+
focalX?: number;
|
|
10
|
+
focalY?: number;
|
|
11
|
+
/** 0–100, default 100. */
|
|
12
|
+
opacity?: number;
|
|
13
|
+
/** Gaussian blur radius in px. */
|
|
14
|
+
blur?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface BackgroundOverlay {
|
|
17
|
+
/** Any CSS color or gradient; palette tokens resolve as in block fills. */
|
|
18
|
+
fill: string;
|
|
19
|
+
/** 0–100, default 100. */
|
|
20
|
+
opacity?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface PaywallBackgroundSpec {
|
|
23
|
+
/** Ground paint under the image — a solid color or a CSS gradient. */
|
|
24
|
+
color?: string;
|
|
25
|
+
image?: BackgroundImage;
|
|
26
|
+
overlay?: BackgroundOverlay;
|
|
27
|
+
/** Builder bookkeeping (library preset id); ignored on device. */
|
|
28
|
+
presetId?: string;
|
|
29
|
+
/** Builder bookkeeping ("reset to design default"); ignored on device. */
|
|
30
|
+
original?: PaywallBackground;
|
|
31
|
+
}
|
|
32
|
+
export type PaywallBackground = string | PaywallBackgroundSpec;
|
|
33
|
+
/**
|
|
34
|
+
* The flat color a ground stands in for — what `@bg` resolves to, and what
|
|
35
|
+
* paints under a gradient this build cannot draw.
|
|
36
|
+
*
|
|
37
|
+
* It reads the LAST comma-separated layer, because in CSS the first-listed
|
|
38
|
+
* layer paints on TOP: taking the first color would answer with the
|
|
39
|
+
* translucent accent glow the library's Spotlight and Corner halo presets
|
|
40
|
+
* stack over their base, not with the base itself. Fully transparent stops are
|
|
41
|
+
* skipped for the same reason — a scrim that fades to `…00` says nothing about
|
|
42
|
+
* the ground under it.
|
|
43
|
+
*
|
|
44
|
+
* Returning the raw gradient here instead is what put this SDK and the
|
|
45
|
+
* dashboard out of step with the other five renderers, which have always
|
|
46
|
+
* collapsed it: `@bg` is fed to `color-mix()` (and, inside a gradient, to
|
|
47
|
+
* another gradient's stop list), and neither can take a gradient — so the raw
|
|
48
|
+
* form resolved to something invalid and painted NOTHING. Of the 250 shipped
|
|
49
|
+
* gallery presets only 7 have a gradient ground and none of those use `@bg`,
|
|
50
|
+
* so no design that ships today changes; what changes is a background a
|
|
51
|
+
* customer edits to a gradient, which used to silently drop every `@bg` tint
|
|
52
|
+
* on the screen.
|
|
53
|
+
*/
|
|
54
|
+
export declare function backgroundBaseColor(bg: PaywallBackground | undefined): string;
|
|
55
|
+
export interface ResolvedBackgroundImage {
|
|
56
|
+
url: string;
|
|
57
|
+
fit: BackgroundFit;
|
|
58
|
+
focalX: number;
|
|
59
|
+
focalY: number;
|
|
60
|
+
/** 0–1. */
|
|
61
|
+
opacity: number;
|
|
62
|
+
/** px; absent when no blur was asked for. */
|
|
63
|
+
blur?: number;
|
|
64
|
+
}
|
|
65
|
+
export interface ResolvedBackgroundOverlay {
|
|
66
|
+
fill: string;
|
|
67
|
+
/** 0–1. */
|
|
68
|
+
opacity: number;
|
|
69
|
+
}
|
|
70
|
+
export interface BackgroundLayers {
|
|
71
|
+
/** The ground fill (color or gradient string), or undefined for none. */
|
|
72
|
+
ground?: string;
|
|
73
|
+
image?: ResolvedBackgroundImage;
|
|
74
|
+
overlay?: ResolvedBackgroundOverlay;
|
|
75
|
+
}
|
|
76
|
+
/** The paint list, bottom layer first. Layers that would draw nothing (no
|
|
77
|
+
* url, zero opacity) are dropped, so a legacy string resolves to exactly
|
|
78
|
+
* one ground layer. */
|
|
79
|
+
export declare function backgroundLayers(bg: PaywallBackground | undefined,
|
|
80
|
+
/** Resolves palette tokens in the ground and overlay fills. */
|
|
81
|
+
resolve?: (value: string) => string): BackgroundLayers;
|
|
82
|
+
/**
|
|
83
|
+
* Cover-fit geometry with a focal point — the SDK's stand-in for CSS
|
|
84
|
+
* `object-position`, which React Native's <Image> lacks. Given the box and
|
|
85
|
+
* the source dimensions it returns the drawn size and the offset that puts
|
|
86
|
+
* the focal point as close to the box's matching anchor as the image edges
|
|
87
|
+
* allow (exactly CSS's `object-position: X% Y%` rule: the X% point of the
|
|
88
|
+
* image aligns to the X% point of the box, clamped so no edge shows).
|
|
89
|
+
*/
|
|
90
|
+
export declare function coverPlacement(box: {
|
|
91
|
+
width: number;
|
|
92
|
+
height: number;
|
|
93
|
+
}, source: {
|
|
94
|
+
width: number;
|
|
95
|
+
height: number;
|
|
96
|
+
}, focalX: number, focalY: number): {
|
|
97
|
+
width: number;
|
|
98
|
+
height: number;
|
|
99
|
+
left: number;
|
|
100
|
+
top: number;
|
|
101
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The paywall screen background — the SDK half of the dashboard's model.
|
|
3
|
+
//
|
|
4
|
+
// A background is either the original CSS string (color or gradient) or a
|
|
5
|
+
// layered spec: ground color/gradient, then a photo (fit, focal point,
|
|
6
|
+
// opacity, blur), then a scrim. `backgroundLayers()` resolves either form to
|
|
7
|
+
// one paint list, mirroring the dashboard renderer exactly — the same
|
|
8
|
+
// function name, the same defaults, the same clamping — so a background can
|
|
9
|
+
// never look different on device than in the builder for reasons of
|
|
10
|
+
// interpretation. (Type parity with revnix-app/src/lib/paywall-blocks/
|
|
11
|
+
// background.ts; keep the two in step.)
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.backgroundBaseColor = backgroundBaseColor;
|
|
14
|
+
exports.backgroundLayers = backgroundLayers;
|
|
15
|
+
exports.coverPlacement = coverPlacement;
|
|
16
|
+
const DEFAULT_GROUND = "#000000";
|
|
17
|
+
/**
|
|
18
|
+
* The flat color a ground stands in for — what `@bg` resolves to, and what
|
|
19
|
+
* paints under a gradient this build cannot draw.
|
|
20
|
+
*
|
|
21
|
+
* It reads the LAST comma-separated layer, because in CSS the first-listed
|
|
22
|
+
* layer paints on TOP: taking the first color would answer with the
|
|
23
|
+
* translucent accent glow the library's Spotlight and Corner halo presets
|
|
24
|
+
* stack over their base, not with the base itself. Fully transparent stops are
|
|
25
|
+
* skipped for the same reason — a scrim that fades to `…00` says nothing about
|
|
26
|
+
* the ground under it.
|
|
27
|
+
*
|
|
28
|
+
* Returning the raw gradient here instead is what put this SDK and the
|
|
29
|
+
* dashboard out of step with the other five renderers, which have always
|
|
30
|
+
* collapsed it: `@bg` is fed to `color-mix()` (and, inside a gradient, to
|
|
31
|
+
* another gradient's stop list), and neither can take a gradient — so the raw
|
|
32
|
+
* form resolved to something invalid and painted NOTHING. Of the 250 shipped
|
|
33
|
+
* gallery presets only 7 have a gradient ground and none of those use `@bg`,
|
|
34
|
+
* so no design that ships today changes; what changes is a background a
|
|
35
|
+
* customer edits to a gradient, which used to silently drop every `@bg` tint
|
|
36
|
+
* on the screen.
|
|
37
|
+
*/
|
|
38
|
+
function backgroundBaseColor(bg) {
|
|
39
|
+
const ground = typeof bg === "string" ? bg : bg?.color;
|
|
40
|
+
if (ground === undefined)
|
|
41
|
+
return DEFAULT_GROUND;
|
|
42
|
+
const layers = splitLayers(ground);
|
|
43
|
+
const bottom = layers.length > 0 ? layers[layers.length - 1] : ground;
|
|
44
|
+
const colors = bottom.match(/#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)/gi);
|
|
45
|
+
if (!colors)
|
|
46
|
+
return ground;
|
|
47
|
+
return colors.find((color) => !isFullyTransparent(color)) ?? colors[0];
|
|
48
|
+
}
|
|
49
|
+
/** Whether a color literal is fully transparent. Kept here rather than routed
|
|
50
|
+
* through `color.ts`, which imports THIS module — only the two forms the
|
|
51
|
+
* dashboard emits are recognized, and anything else counts as opaque, which
|
|
52
|
+
* is the safe answer for picking a ground. */
|
|
53
|
+
function isFullyTransparent(color) {
|
|
54
|
+
const s = color.trim();
|
|
55
|
+
if (s.startsWith("#")) {
|
|
56
|
+
const hex = s.slice(1);
|
|
57
|
+
if (hex.length === 8)
|
|
58
|
+
return parseInt(hex.slice(6), 16) === 0;
|
|
59
|
+
if (hex.length === 4)
|
|
60
|
+
return parseInt(hex.slice(3).repeat(2), 16) === 0;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const open = s.indexOf("(");
|
|
64
|
+
const close = s.indexOf(")");
|
|
65
|
+
if (open < 0 || close < 0)
|
|
66
|
+
return false;
|
|
67
|
+
const parts = s
|
|
68
|
+
.slice(open + 1, close)
|
|
69
|
+
.split(/[,/\s]+/)
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
return parts.length >= 4 && parseFloat(parts[3]) === 0;
|
|
72
|
+
}
|
|
73
|
+
/** Splits on top-level commas only, so the commas inside `rgba(...)` and
|
|
74
|
+
* inside a gradient's argument list do not tear a layer in half. */
|
|
75
|
+
function splitLayers(input) {
|
|
76
|
+
const out = [];
|
|
77
|
+
let depth = 0;
|
|
78
|
+
let current = "";
|
|
79
|
+
for (const ch of input) {
|
|
80
|
+
if (ch === "(")
|
|
81
|
+
depth++;
|
|
82
|
+
else if (ch === ")" && depth > 0)
|
|
83
|
+
depth--;
|
|
84
|
+
if (ch === "," && depth === 0) {
|
|
85
|
+
out.push(current.trim());
|
|
86
|
+
current = "";
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
current += ch;
|
|
90
|
+
}
|
|
91
|
+
const tail = current.trim();
|
|
92
|
+
if (tail)
|
|
93
|
+
out.push(tail);
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
function pct(value, fallback) {
|
|
97
|
+
if (value === undefined || !Number.isFinite(value))
|
|
98
|
+
return fallback;
|
|
99
|
+
return Math.min(1, Math.max(0, value / 100));
|
|
100
|
+
}
|
|
101
|
+
function coord(value) {
|
|
102
|
+
if (value === undefined || !Number.isFinite(value))
|
|
103
|
+
return 50;
|
|
104
|
+
return Math.min(100, Math.max(0, value));
|
|
105
|
+
}
|
|
106
|
+
/** The paint list, bottom layer first. Layers that would draw nothing (no
|
|
107
|
+
* url, zero opacity) are dropped, so a legacy string resolves to exactly
|
|
108
|
+
* one ground layer. */
|
|
109
|
+
function backgroundLayers(bg,
|
|
110
|
+
/** Resolves palette tokens in the ground and overlay fills. */
|
|
111
|
+
resolve = (v) => v) {
|
|
112
|
+
const spec = bg === undefined ? {} : typeof bg === "string" ? { color: bg } : bg;
|
|
113
|
+
const layers = {};
|
|
114
|
+
if (spec.color)
|
|
115
|
+
layers.ground = resolve(spec.color);
|
|
116
|
+
if (spec.image?.url) {
|
|
117
|
+
const opacity = pct(spec.image.opacity, 1);
|
|
118
|
+
if (opacity > 0) {
|
|
119
|
+
layers.image = {
|
|
120
|
+
url: spec.image.url,
|
|
121
|
+
fit: spec.image.fit === "contain" ? "contain" : "cover",
|
|
122
|
+
focalX: coord(spec.image.focalX),
|
|
123
|
+
focalY: coord(spec.image.focalY),
|
|
124
|
+
opacity,
|
|
125
|
+
...(spec.image.blur ? { blur: spec.image.blur } : {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (spec.overlay?.fill) {
|
|
130
|
+
const opacity = pct(spec.overlay.opacity, 1);
|
|
131
|
+
if (opacity > 0) {
|
|
132
|
+
layers.overlay = { fill: resolve(spec.overlay.fill), opacity };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return layers;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Cover-fit geometry with a focal point — the SDK's stand-in for CSS
|
|
139
|
+
* `object-position`, which React Native's <Image> lacks. Given the box and
|
|
140
|
+
* the source dimensions it returns the drawn size and the offset that puts
|
|
141
|
+
* the focal point as close to the box's matching anchor as the image edges
|
|
142
|
+
* allow (exactly CSS's `object-position: X% Y%` rule: the X% point of the
|
|
143
|
+
* image aligns to the X% point of the box, clamped so no edge shows).
|
|
144
|
+
*/
|
|
145
|
+
function coverPlacement(box, source, focalX, focalY) {
|
|
146
|
+
if (box.width <= 0 || box.height <= 0 || source.width <= 0 || source.height <= 0) {
|
|
147
|
+
return { width: box.width, height: box.height, left: 0, top: 0 };
|
|
148
|
+
}
|
|
149
|
+
const scale = Math.max(box.width / source.width, box.height / source.height);
|
|
150
|
+
const width = source.width * scale;
|
|
151
|
+
const height = source.height * scale;
|
|
152
|
+
const place = (boxLen, drawnLen, pctPoint) => {
|
|
153
|
+
const offset = (boxLen - drawnLen) * (pctPoint / 100);
|
|
154
|
+
// Clamp: never pull an image edge inside the box. `|| 0` normalises the
|
|
155
|
+
// -0 that falls out of the arithmetic at the edges — harmless to RN, but
|
|
156
|
+
// it makes the returned geometry compare cleanly.
|
|
157
|
+
return Math.min(0, Math.max(boxLen - drawnLen, offset)) || 0;
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
width,
|
|
161
|
+
height,
|
|
162
|
+
left: place(box.width, width, focalX),
|
|
163
|
+
top: place(box.height, height, focalY),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { PaywallBlockDoc } from "./types.js";
|
|
2
|
+
/** Expands palette tokens in a color string and applies any `/pct` alpha. */
|
|
3
|
+
export declare function resolveColor(value: string | undefined, doc: PaywallBlockDoc): string | undefined;
|
|
4
|
+
/** Multiplies a color's alpha. Understands #rgb/#rgba/#rrggbb/#rrggbbaa and
|
|
5
|
+
* rgb()/rgba(); anything else (a named color, a gradient) is returned as-is
|
|
6
|
+
* rather than guessed at. */
|
|
7
|
+
export declare function withAlpha(color: string, alpha: number): string;
|
|
8
|
+
interface Rgb {
|
|
9
|
+
r: number;
|
|
10
|
+
g: number;
|
|
11
|
+
b: number;
|
|
12
|
+
a: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function toRgb(color: string): Rgb | null;
|
|
15
|
+
/** One parsed background layer, in the order CSS paints them (first =
|
|
16
|
+
* topmost). A background may end in a plain color — the design's ground —
|
|
17
|
+
* which is a layer like any other and must not be dropped. */
|
|
18
|
+
export interface GradientLayer {
|
|
19
|
+
kind: "linear" | "radial" | "solid";
|
|
20
|
+
/** Degrees, CSS convention (0 = to top, 180 = to bottom). Linear only. */
|
|
21
|
+
angle: number;
|
|
22
|
+
colors: string[];
|
|
23
|
+
/** 0–1 positions, one per color, when the source stated them. */
|
|
24
|
+
locations?: number[];
|
|
25
|
+
/** Radial only: the `at X% Y%` centre, 0–1. Kept because it decides which
|
|
26
|
+
* way the linear stand-in fades — a glow anchored to the top of the screen
|
|
27
|
+
* must not end up washing the bottom. */
|
|
28
|
+
center?: {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether a paint string's BOTTOM layer is a repeating pattern.
|
|
35
|
+
*
|
|
36
|
+
* A `repeating-*` gradient is a TEXTURE, and the colors inside it are stripe
|
|
37
|
+
* colors rather than the surface's. React Native cannot draw one, and painting
|
|
38
|
+
* a color lifted out of its arguments across the whole box is a WRONG answer
|
|
39
|
+
* rather than a degraded one — the library's hairline grid is one color every
|
|
40
|
+
* 26px, and as a solid fill it is a slab. Such a fill paints nothing instead.
|
|
41
|
+
*
|
|
42
|
+
* The bottom layer decides, so a pattern stacked over a real ground
|
|
43
|
+
* (`repeating-…(…), #FBF3E4`) still falls back to that ground.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isRepeatingPattern(value: string | undefined): boolean;
|
|
46
|
+
/** True when the string is a CSS gradient rather than a plain color. */
|
|
47
|
+
export declare function isGradient(value: string): boolean;
|
|
48
|
+
/** Splits a CSS background into its gradient layers. Layers CSS paints last
|
|
49
|
+
* (the base) come last in the returned array, matching source order. */
|
|
50
|
+
export declare function parseGradient(value: string): GradientLayer[];
|
|
51
|
+
/**
|
|
52
|
+
* A field that can only ever be ONE color — a border, text, an icon, a shadow.
|
|
53
|
+
*
|
|
54
|
+
* A gradient there has no native form (nor a CSS one: `border-color` takes no
|
|
55
|
+
* gradient, so the dashboard drops the declaration outright). Collapsing it to
|
|
56
|
+
* the color it stands for keeps the stroke or the glyph visible, which is
|
|
57
|
+
* nearer the design's intent than losing it — and, on React Native, avoids
|
|
58
|
+
* handing the platform a color string it cannot parse.
|
|
59
|
+
*/
|
|
60
|
+
export declare function flatColor(value: string | undefined, doc: PaywallBlockDoc, onDiagnostic?: (message: string) => void): string | undefined;
|
|
61
|
+
/** The solid to paint when no gradient component is available: the base
|
|
62
|
+
* layer's first stop, which is the design's ground color.
|
|
63
|
+
*
|
|
64
|
+
* A value with no paintable layer at all — a `repeating-linear-gradient`, or
|
|
65
|
+
* any other form this build does not read — falls through to the same colour
|
|
66
|
+
* extraction the six SDKs share, so a box the design coloured still gets a
|
|
67
|
+
* colour from the design rather than nothing. */
|
|
68
|
+
export declare function gradientFallbackColor(value: string): string | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The solid a surface paints UNDER a fill — or nothing.
|
|
71
|
+
*
|
|
72
|
+
* The solid exists for one reason: an app that passes no gradient component
|
|
73
|
+
* still gets a colour instead of a hole. It was painted unconditionally, and
|
|
74
|
+
* for an opaque fill that never showed, because the real gradient covered it.
|
|
75
|
+
*
|
|
76
|
+
* A scrim showed it. A scrim is a gradient that is transparent at one end ON
|
|
77
|
+
* PURPOSE — that is how a photo stays visible above the copy. Its first stop
|
|
78
|
+
* is the dark end, so the "fallback" under it was a near-black sheet across
|
|
79
|
+
* the whole box, and the gradient's transparent half revealed that sheet
|
|
80
|
+
* instead of the photograph behind it. Every photo background looked like a
|
|
81
|
+
* black screen on React Native, while the dashboard's CSS painted it fine.
|
|
82
|
+
*
|
|
83
|
+
* So: when the gradient will actually be drawn, nothing goes under it. The
|
|
84
|
+
* solid is only for the degraded case — no gradient renderer, or a fill this
|
|
85
|
+
* build cannot read as a gradient at all.
|
|
86
|
+
*/
|
|
87
|
+
export declare function surfaceSolid(resolved: string | undefined, canPaintGradient: boolean): string | undefined;
|
|
88
|
+
export {};
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Color handling for block paywalls.
|
|
3
|
+
//
|
|
4
|
+
// Two jobs the web renderer gets from CSS for free and React Native does not:
|
|
5
|
+
//
|
|
6
|
+
// 1. Palette TOKENS. Converted designs write `@accent`, `@text`, `@bg`,
|
|
7
|
+
// `@accentInk` (optionally with an alpha percentage: `@text/12`) so the
|
|
8
|
+
// four screen colors stay a single source of truth. The web renderer
|
|
9
|
+
// expands the percentage form with `color-mix()`; here we compute the
|
|
10
|
+
// rgba ourselves.
|
|
11
|
+
//
|
|
12
|
+
// 2. GRADIENTS. `background` is a CSS background string and may carry
|
|
13
|
+
// several gradient layers. React Native has no gradient primitive, so we
|
|
14
|
+
// parse the layers out: a host that supplies a gradient component renders
|
|
15
|
+
// them properly, and without one we fall back to the base layer's first
|
|
16
|
+
// color — a solid that keeps the screen on-palette instead of blank.
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.resolveColor = resolveColor;
|
|
19
|
+
exports.withAlpha = withAlpha;
|
|
20
|
+
exports.toRgb = toRgb;
|
|
21
|
+
exports.isRepeatingPattern = isRepeatingPattern;
|
|
22
|
+
exports.isGradient = isGradient;
|
|
23
|
+
exports.parseGradient = parseGradient;
|
|
24
|
+
exports.flatColor = flatColor;
|
|
25
|
+
exports.gradientFallbackColor = gradientFallbackColor;
|
|
26
|
+
exports.surfaceSolid = surfaceSolid;
|
|
27
|
+
const background_js_1 = require("./background.js");
|
|
28
|
+
const TOKEN = /@(accentInk|accent|bg|text)(?:\/(\d{1,3}))?/g;
|
|
29
|
+
/** Expands palette tokens in a color string and applies any `/pct` alpha. */
|
|
30
|
+
function resolveColor(value, doc) {
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (!value.includes("@"))
|
|
34
|
+
return value;
|
|
35
|
+
return value.replace(TOKEN, (_match, name, pct) => {
|
|
36
|
+
const base = name === "accentInk"
|
|
37
|
+
? doc.accentInk
|
|
38
|
+
: name === "accent"
|
|
39
|
+
? doc.accent
|
|
40
|
+
: name === "bg"
|
|
41
|
+
? (0, background_js_1.backgroundBaseColor)(doc.background)
|
|
42
|
+
: doc.textColor;
|
|
43
|
+
return pct === undefined ? base : withAlpha(base, Number(pct) / 100);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Multiplies a color's alpha. Understands #rgb/#rgba/#rrggbb/#rrggbbaa and
|
|
47
|
+
* rgb()/rgba(); anything else (a named color, a gradient) is returned as-is
|
|
48
|
+
* rather than guessed at. */
|
|
49
|
+
function withAlpha(color, alpha) {
|
|
50
|
+
const rgb = toRgb(color);
|
|
51
|
+
if (!rgb)
|
|
52
|
+
return color;
|
|
53
|
+
const a = Math.max(0, Math.min(1, rgb.a * alpha));
|
|
54
|
+
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${round(a)})`;
|
|
55
|
+
}
|
|
56
|
+
function round(n) {
|
|
57
|
+
return Math.round(n * 1000) / 1000;
|
|
58
|
+
}
|
|
59
|
+
function toRgb(color) {
|
|
60
|
+
const value = color.trim();
|
|
61
|
+
if (value.startsWith("#")) {
|
|
62
|
+
const hex = value.slice(1);
|
|
63
|
+
const expand = (c) => parseInt(c + c, 16);
|
|
64
|
+
if (!/^[0-9a-fA-F]+$/.test(hex))
|
|
65
|
+
return null;
|
|
66
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
67
|
+
return {
|
|
68
|
+
r: expand(hex[0]),
|
|
69
|
+
g: expand(hex[1]),
|
|
70
|
+
b: expand(hex[2]),
|
|
71
|
+
a: hex.length === 4 ? expand(hex[3]) / 255 : 1,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
75
|
+
const byte = (i) => parseInt(hex.slice(i, i + 2), 16);
|
|
76
|
+
return {
|
|
77
|
+
r: byte(0),
|
|
78
|
+
g: byte(2),
|
|
79
|
+
b: byte(4),
|
|
80
|
+
a: hex.length === 8 ? byte(6) / 255 : 1,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const fn = value.match(/^rgba?\(([^)]+)\)$/i);
|
|
86
|
+
if (fn) {
|
|
87
|
+
const parts = fn[1].split(/[,/\s]+/).filter(Boolean).map(Number);
|
|
88
|
+
if (parts.length < 3 || parts.slice(0, 3).some(Number.isNaN))
|
|
89
|
+
return null;
|
|
90
|
+
return {
|
|
91
|
+
r: parts[0],
|
|
92
|
+
g: parts[1],
|
|
93
|
+
b: parts[2],
|
|
94
|
+
a: parts.length > 3 && !Number.isNaN(parts[3]) ? parts[3] : 1,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Whether a paint string's BOTTOM layer is a repeating pattern.
|
|
101
|
+
*
|
|
102
|
+
* A `repeating-*` gradient is a TEXTURE, and the colors inside it are stripe
|
|
103
|
+
* colors rather than the surface's. React Native cannot draw one, and painting
|
|
104
|
+
* a color lifted out of its arguments across the whole box is a WRONG answer
|
|
105
|
+
* rather than a degraded one — the library's hairline grid is one color every
|
|
106
|
+
* 26px, and as a solid fill it is a slab. Such a fill paints nothing instead.
|
|
107
|
+
*
|
|
108
|
+
* The bottom layer decides, so a pattern stacked over a real ground
|
|
109
|
+
* (`repeating-…(…), #FBF3E4`) still falls back to that ground.
|
|
110
|
+
*/
|
|
111
|
+
function isRepeatingPattern(value) {
|
|
112
|
+
const s = value?.trim() ?? "";
|
|
113
|
+
if (!s)
|
|
114
|
+
return false;
|
|
115
|
+
const layers = splitLayers(s);
|
|
116
|
+
const bottom = layers.length > 0 ? layers[layers.length - 1] : s;
|
|
117
|
+
return /^repeating-/i.test(bottom);
|
|
118
|
+
}
|
|
119
|
+
/** True when the string is a CSS gradient rather than a plain color. */
|
|
120
|
+
function isGradient(value) {
|
|
121
|
+
return /gradient\s*\(/i.test(value);
|
|
122
|
+
}
|
|
123
|
+
/** Splits a CSS background into its gradient layers. Layers CSS paints last
|
|
124
|
+
* (the base) come last in the returned array, matching source order. */
|
|
125
|
+
function parseGradient(value) {
|
|
126
|
+
const layers = [];
|
|
127
|
+
for (const raw of splitLayers(value)) {
|
|
128
|
+
const match = raw.match(/^(linear|radial)-gradient\s*\(([\s\S]*)\)$/i);
|
|
129
|
+
if (!match) {
|
|
130
|
+
// A plain color among the layers is the design's ground. Dropping it
|
|
131
|
+
// would leave the screen painted by a translucent glow instead.
|
|
132
|
+
//
|
|
133
|
+
// It has to LOOK like a color, though: any other layer — a
|
|
134
|
+
// `repeating-linear-gradient`, a `url()` — used to be accepted here and
|
|
135
|
+
// handed to the host's gradient component as a color string, which is not
|
|
136
|
+
// one. Dropping it instead is what lets the caller see there is nothing
|
|
137
|
+
// paintable and degrade to a flat color.
|
|
138
|
+
if (raw && hasColor(raw)) {
|
|
139
|
+
layers.push({ kind: "solid", angle: 180, colors: [raw] });
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const kind = match[1].toLowerCase();
|
|
144
|
+
const args = splitTop(match[2]);
|
|
145
|
+
// A radial's center is dropped (RN has no radial gradient), so it is drawn
|
|
146
|
+
// as a top-down fade — 180deg, like every other default.
|
|
147
|
+
let angle = 180;
|
|
148
|
+
let start = 0;
|
|
149
|
+
let center;
|
|
150
|
+
const first = args[0]?.trim() ?? "";
|
|
151
|
+
if (kind === "linear" && /^-?[\d.]+deg$/i.test(first)) {
|
|
152
|
+
angle = parseFloat(first);
|
|
153
|
+
start = 1;
|
|
154
|
+
}
|
|
155
|
+
else if (kind === "linear" && /^to\s+/i.test(first)) {
|
|
156
|
+
angle = TO_ANGLE[first.trim().toLowerCase()] ?? 180;
|
|
157
|
+
start = 1;
|
|
158
|
+
}
|
|
159
|
+
else if (kind === "radial" && !hasColor(first)) {
|
|
160
|
+
// Radial shape/position prelude ("120% 90% at 85% 5%"). RN has no
|
|
161
|
+
// radial gradient, but the centre decides which way the stand-in fades,
|
|
162
|
+
// so it is kept rather than dropped with the rest.
|
|
163
|
+
center = readCenter(first);
|
|
164
|
+
start = 1;
|
|
165
|
+
}
|
|
166
|
+
const colors = [];
|
|
167
|
+
const locations = [];
|
|
168
|
+
let positioned = 0;
|
|
169
|
+
for (const arg of args.slice(start)) {
|
|
170
|
+
// CSS allows TWO positions on one stop — `@accent 0 22%` is the same
|
|
171
|
+
// color at both, the hard edge the library's progress bars and split
|
|
172
|
+
// panels are drawn with — and a unitless `0` is a valid position. Reading
|
|
173
|
+
// only the last one left those designs a smooth fade instead of an edge.
|
|
174
|
+
let body = arg.trim();
|
|
175
|
+
const positions = [];
|
|
176
|
+
while (positions.length < 2) {
|
|
177
|
+
const found = body.match(/\s(-?[\d.]+%|0)$/);
|
|
178
|
+
if (!found)
|
|
179
|
+
break;
|
|
180
|
+
const text = found[1];
|
|
181
|
+
const value = parseFloat(text);
|
|
182
|
+
// The token comes off `body` either way. Leaving a position this build
|
|
183
|
+
// could not read attached to the color would make the color itself
|
|
184
|
+
// unparseable, dropping the whole stop — and a gradient left with one
|
|
185
|
+
// stop does not parse at all. A malformed position is worth losing; the
|
|
186
|
+
// stop is not, so it falls through to the interpolated position.
|
|
187
|
+
body = body.slice(0, body.length - found[0].length).trim();
|
|
188
|
+
if (Number.isNaN(value))
|
|
189
|
+
break;
|
|
190
|
+
positions.unshift(Math.max(0, Math.min(1, text.endsWith("%") ? value / 100 : value)));
|
|
191
|
+
}
|
|
192
|
+
if (!body)
|
|
193
|
+
continue;
|
|
194
|
+
if (positions.length === 0) {
|
|
195
|
+
colors.push(body);
|
|
196
|
+
locations.push(colors.length === 1 ? 0 : 1);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
for (const position of positions) {
|
|
200
|
+
colors.push(body);
|
|
201
|
+
locations.push(position);
|
|
202
|
+
positioned++;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (colors.length === 0)
|
|
206
|
+
continue;
|
|
207
|
+
layers.push({
|
|
208
|
+
kind,
|
|
209
|
+
angle,
|
|
210
|
+
colors,
|
|
211
|
+
// Partial position data would place stops wrongly; only pass locations
|
|
212
|
+
// through when the source positioned every stop.
|
|
213
|
+
locations: positioned === colors.length ? locations : undefined,
|
|
214
|
+
center,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return layers;
|
|
218
|
+
}
|
|
219
|
+
const TO_ANGLE = {
|
|
220
|
+
"to top": 0,
|
|
221
|
+
"to right": 90,
|
|
222
|
+
"to bottom": 180,
|
|
223
|
+
"to left": 270,
|
|
224
|
+
"to top right": 45,
|
|
225
|
+
"to right top": 45,
|
|
226
|
+
"to bottom right": 135,
|
|
227
|
+
"to right bottom": 135,
|
|
228
|
+
"to bottom left": 225,
|
|
229
|
+
"to left bottom": 225,
|
|
230
|
+
"to top left": 315,
|
|
231
|
+
"to left top": 315,
|
|
232
|
+
};
|
|
233
|
+
/** The `at X% Y%` part of a radial's prelude, as 0–1. Defaults to the CSS
|
|
234
|
+
* default centre when the prelude names none. */
|
|
235
|
+
function readCenter(prelude) {
|
|
236
|
+
const at = prelude.split(/\bat\b/i)[1];
|
|
237
|
+
const pcts = at ? [...at.matchAll(/(-?[\d.]+)%/g)].map((m) => parseFloat(m[1])) : [];
|
|
238
|
+
return {
|
|
239
|
+
x: (pcts[0] ?? 50) / 100,
|
|
240
|
+
y: (pcts[1] ?? 50) / 100,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function hasColor(value) {
|
|
244
|
+
const s = value.trim();
|
|
245
|
+
return /^(#|rgb|hsl|@)/i.test(s) || s.toLowerCase() === "transparent";
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* A field that can only ever be ONE color — a border, text, an icon, a shadow.
|
|
249
|
+
*
|
|
250
|
+
* A gradient there has no native form (nor a CSS one: `border-color` takes no
|
|
251
|
+
* gradient, so the dashboard drops the declaration outright). Collapsing it to
|
|
252
|
+
* the color it stands for keeps the stroke or the glyph visible, which is
|
|
253
|
+
* nearer the design's intent than losing it — and, on React Native, avoids
|
|
254
|
+
* handing the platform a color string it cannot parse.
|
|
255
|
+
*/
|
|
256
|
+
function flatColor(value, doc, onDiagnostic) {
|
|
257
|
+
const resolved = resolveColor(value, doc);
|
|
258
|
+
if (resolved === undefined || !isGradient(resolved))
|
|
259
|
+
return resolved;
|
|
260
|
+
onDiagnostic?.(`gradient flattened in a colour-only field: ${resolved}`);
|
|
261
|
+
return gradientFallbackColor(resolved);
|
|
262
|
+
}
|
|
263
|
+
/** The solid to paint when no gradient component is available: the base
|
|
264
|
+
* layer's first stop, which is the design's ground color.
|
|
265
|
+
*
|
|
266
|
+
* A value with no paintable layer at all — a `repeating-linear-gradient`, or
|
|
267
|
+
* any other form this build does not read — falls through to the same colour
|
|
268
|
+
* extraction the six SDKs share, so a box the design coloured still gets a
|
|
269
|
+
* colour from the design rather than nothing. */
|
|
270
|
+
function gradientFallbackColor(value) {
|
|
271
|
+
// A pattern's colors are STRIPE colors; spreading one over the whole box is a
|
|
272
|
+
// wrong answer rather than a degraded one, so it paints nothing.
|
|
273
|
+
if (isRepeatingPattern(value))
|
|
274
|
+
return undefined;
|
|
275
|
+
const layers = parseGradient(value);
|
|
276
|
+
const base = layers[layers.length - 1];
|
|
277
|
+
return base?.colors[0] ?? (0, background_js_1.backgroundBaseColor)(value);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The solid a surface paints UNDER a fill — or nothing.
|
|
281
|
+
*
|
|
282
|
+
* The solid exists for one reason: an app that passes no gradient component
|
|
283
|
+
* still gets a colour instead of a hole. It was painted unconditionally, and
|
|
284
|
+
* for an opaque fill that never showed, because the real gradient covered it.
|
|
285
|
+
*
|
|
286
|
+
* A scrim showed it. A scrim is a gradient that is transparent at one end ON
|
|
287
|
+
* PURPOSE — that is how a photo stays visible above the copy. Its first stop
|
|
288
|
+
* is the dark end, so the "fallback" under it was a near-black sheet across
|
|
289
|
+
* the whole box, and the gradient's transparent half revealed that sheet
|
|
290
|
+
* instead of the photograph behind it. Every photo background looked like a
|
|
291
|
+
* black screen on React Native, while the dashboard's CSS painted it fine.
|
|
292
|
+
*
|
|
293
|
+
* So: when the gradient will actually be drawn, nothing goes under it. The
|
|
294
|
+
* solid is only for the degraded case — no gradient renderer, or a fill this
|
|
295
|
+
* build cannot read as a gradient at all.
|
|
296
|
+
*/
|
|
297
|
+
function surfaceSolid(resolved, canPaintGradient) {
|
|
298
|
+
if (!resolved)
|
|
299
|
+
return undefined;
|
|
300
|
+
if (!isGradient(resolved))
|
|
301
|
+
return resolved;
|
|
302
|
+
if (canPaintGradient && parseGradient(resolved).length > 0)
|
|
303
|
+
return undefined;
|
|
304
|
+
return gradientFallbackColor(resolved);
|
|
305
|
+
}
|
|
306
|
+
/** Splits comma-separated background layers, ignoring commas inside parens. */
|
|
307
|
+
function splitLayers(value) {
|
|
308
|
+
return splitTop(value).map((s) => s.trim());
|
|
309
|
+
}
|
|
310
|
+
function splitTop(value) {
|
|
311
|
+
const out = [];
|
|
312
|
+
let depth = 0;
|
|
313
|
+
let current = "";
|
|
314
|
+
for (const ch of value) {
|
|
315
|
+
if (ch === "(")
|
|
316
|
+
depth++;
|
|
317
|
+
else if (ch === ")")
|
|
318
|
+
depth--;
|
|
319
|
+
if (ch === "," && depth === 0) {
|
|
320
|
+
out.push(current);
|
|
321
|
+
current = "";
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
current += ch;
|
|
325
|
+
}
|
|
326
|
+
if (current.trim())
|
|
327
|
+
out.push(current);
|
|
328
|
+
return out;
|
|
329
|
+
}
|