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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +766 -0
  3. package/dist/blocks/background.d.ts +101 -0
  4. package/dist/blocks/background.js +165 -0
  5. package/dist/blocks/color.d.ts +88 -0
  6. package/dist/blocks/color.js +329 -0
  7. package/dist/blocks/locale.d.ts +42 -0
  8. package/dist/blocks/locale.js +263 -0
  9. package/dist/blocks/selection.d.ts +49 -0
  10. package/dist/blocks/selection.js +109 -0
  11. package/dist/blocks/style.d.ts +93 -0
  12. package/dist/blocks/style.js +365 -0
  13. package/dist/blocks/tags.d.ts +12 -0
  14. package/dist/blocks/tags.js +121 -0
  15. package/dist/blocks/types.d.ts +301 -0
  16. package/dist/blocks/types.js +44 -0
  17. package/dist/client.d.ts +402 -0
  18. package/dist/client.js +1653 -0
  19. package/dist/device.d.ts +29 -0
  20. package/dist/device.js +137 -0
  21. package/dist/errors.d.ts +39 -0
  22. package/dist/errors.js +68 -0
  23. package/dist/esm/blocks/background.js +160 -0
  24. package/dist/esm/blocks/color.js +318 -0
  25. package/dist/esm/blocks/locale.js +255 -0
  26. package/dist/esm/blocks/selection.js +100 -0
  27. package/dist/esm/blocks/style.js +356 -0
  28. package/dist/esm/blocks/tags.js +117 -0
  29. package/dist/esm/blocks/types.js +40 -0
  30. package/dist/esm/client.js +1649 -0
  31. package/dist/esm/device.js +130 -0
  32. package/dist/esm/errors.js +59 -0
  33. package/dist/esm/iap.js +101 -0
  34. package/dist/esm/implicit.js +63 -0
  35. package/dist/esm/index.js +29 -0
  36. package/dist/esm/package.json +1 -0
  37. package/dist/esm/storage.js +78 -0
  38. package/dist/esm/types.js +4 -0
  39. package/dist/esm/ui/RevnixBlockPaywall.js +706 -0
  40. package/dist/esm/ui/RevnixPaywall.js +890 -0
  41. package/dist/esm/ui/index.js +6 -0
  42. package/dist/esm/version.js +4 -0
  43. package/dist/iap.d.ts +20 -0
  44. package/dist/iap.js +104 -0
  45. package/dist/implicit.d.ts +69 -0
  46. package/dist/implicit.js +67 -0
  47. package/dist/index.d.ts +18 -0
  48. package/dist/index.js +50 -0
  49. package/dist/storage.d.ts +14 -0
  50. package/dist/storage.js +85 -0
  51. package/dist/types.d.ts +463 -0
  52. package/dist/types.js +5 -0
  53. package/dist/ui/RevnixBlockPaywall.d.ts +96 -0
  54. package/dist/ui/RevnixBlockPaywall.js +742 -0
  55. package/dist/ui/RevnixPaywall.d.ts +148 -0
  56. package/dist/ui/RevnixPaywall.js +927 -0
  57. package/dist/ui/index.d.ts +7 -0
  58. package/dist/ui/index.js +25 -0
  59. package/dist/version.d.ts +1 -0
  60. package/dist/version.js +7 -0
  61. package/package.json +79 -0
  62. package/src/blocks/background.ts +232 -0
  63. package/src/blocks/color.ts +352 -0
  64. package/src/blocks/locale.ts +271 -0
  65. package/src/blocks/paywall-background-wire.json +12 -0
  66. package/src/blocks/paywall-selection-wire.json +149 -0
  67. package/src/blocks/selection.ts +161 -0
  68. package/src/blocks/style.ts +411 -0
  69. package/src/blocks/tags.ts +125 -0
  70. package/src/blocks/types.ts +352 -0
  71. package/src/client.ts +1993 -0
  72. package/src/device.ts +146 -0
  73. package/src/errors.ts +69 -0
  74. package/src/iap.ts +137 -0
  75. package/src/implicit.ts +129 -0
  76. package/src/index.ts +82 -0
  77. package/src/storage.ts +92 -0
  78. package/src/types.ts +528 -0
  79. package/src/ui/RevnixBlockPaywall.tsx +1423 -0
  80. package/src/ui/RevnixPaywall.tsx +1542 -0
  81. package/src/ui/index.ts +43 -0
  82. package/src/version.ts +4 -0
  83. package/ui.d.ts +1 -0
  84. 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
+ }