@rocapine/react-native-onboarding-ui 1.32.0 → 1.34.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/dist/UI/Pages/ComposableScreen/Renderer.js.map +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/ImageElement.d.ts.map +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/ImageElement.js +72 -32
- package/dist/UI/Pages/ComposableScreen/elements/ImageElement.js.map +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/RichTextElement.d.ts +305 -0
- package/dist/UI/Pages/ComposableScreen/elements/RichTextElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/RichTextElement.js +142 -0
- package/dist/UI/Pages/ComposableScreen/elements/RichTextElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/ScrollViewElement.d.ts +16 -0
- package/dist/UI/Pages/ComposableScreen/elements/ScrollViewElement.d.ts.map +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/ScrollViewElement.js +13 -2
- package/dist/UI/Pages/ComposableScreen/elements/ScrollViewElement.js.map +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/StackElement.d.ts +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/StackElement.d.ts.map +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/TextElement.d.ts +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/TextElement.d.ts.map +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/TextElement.js +26 -12
- package/dist/UI/Pages/ComposableScreen/elements/TextElement.js.map +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/renderElement.d.ts +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/renderElement.d.ts.map +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/renderElement.js +4 -0
- package/dist/UI/Pages/ComposableScreen/elements/renderElement.js.map +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/shared.d.ts +12 -1
- package/dist/UI/Pages/ComposableScreen/elements/shared.d.ts.map +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/shared.js +6 -1
- package/dist/UI/Pages/ComposableScreen/elements/shared.js.map +1 -1
- package/dist/UI/Pages/ComposableScreen/types.d.ts +11 -0
- package/dist/UI/Pages/ComposableScreen/types.d.ts.map +1 -1
- package/dist/UI/Pages/ComposableScreen/types.js +15 -2
- package/dist/UI/Pages/ComposableScreen/types.js.map +1 -1
- package/package.json +5 -1
- package/src/UI/Pages/ComposableScreen/Renderer.tsx +1 -1
- package/src/UI/Pages/ComposableScreen/elements/ImageElement.tsx +102 -42
- package/src/UI/Pages/ComposableScreen/elements/RichTextElement.tsx +188 -0
- package/src/UI/Pages/ComposableScreen/elements/ScrollViewElement.tsx +15 -2
- package/src/UI/Pages/ComposableScreen/elements/StackElement.tsx +1 -1
- package/src/UI/Pages/ComposableScreen/elements/TextElement.tsx +21 -11
- package/src/UI/Pages/ComposableScreen/elements/renderElement.tsx +6 -1
- package/src/UI/Pages/ComposableScreen/elements/shared.ts +19 -1
- package/src/UI/Pages/ComposableScreen/types.ts +25 -2
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { Image, View } from "react-native";
|
|
3
|
+
import { Image as RNImage, View } from "react-native";
|
|
4
|
+
import { SvgUri } from "react-native-svg";
|
|
4
5
|
import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
|
|
5
6
|
import { UIElement } from "../types";
|
|
6
7
|
import { RenderContext, buildShadowStyle, dim } from "./shared";
|
|
7
8
|
import { GradientBox } from "./GradientBox";
|
|
8
9
|
|
|
10
|
+
// expo-image decodes webp/avif reliably across platforms (RN's built-in Image is
|
|
11
|
+
// flaky for webp on iOS). Optional peer dep — fall back to RN Image when absent,
|
|
12
|
+
// mirroring GradientBox's expo-linear-gradient handling.
|
|
13
|
+
let ExpoImage: React.ComponentType<any> | null = null;
|
|
14
|
+
try {
|
|
15
|
+
ExpoImage = require("expo-image").Image;
|
|
16
|
+
} catch {
|
|
17
|
+
// expo-image not installed — RN Image fallback below
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
export type ImageElementProps = BaseBoxProps & {
|
|
10
21
|
url: string;
|
|
11
22
|
aspectRatio?: number;
|
|
@@ -18,6 +29,39 @@ export const ImageElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
|
18
29
|
resizeMode: z.enum(["cover", "contain", "stretch", "center"]).optional(),
|
|
19
30
|
});
|
|
20
31
|
|
|
32
|
+
type ResizeMode = "cover" | "contain" | "stretch" | "center";
|
|
33
|
+
|
|
34
|
+
// RN resizeMode → expo-image contentFit.
|
|
35
|
+
const CONTENT_FIT: Record<ResizeMode, "cover" | "contain" | "fill" | "none"> = {
|
|
36
|
+
cover: "cover",
|
|
37
|
+
contain: "contain",
|
|
38
|
+
stretch: "fill",
|
|
39
|
+
center: "none",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// RN resizeMode → SVG preserveAspectRatio (SvgUri has no resizeMode).
|
|
43
|
+
const SVG_ASPECT: Record<ResizeMode, string> = {
|
|
44
|
+
cover: "xMidYMid slice",
|
|
45
|
+
contain: "xMidYMid meet",
|
|
46
|
+
center: "xMidYMid meet",
|
|
47
|
+
stretch: "none",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// SVGs need react-native-svg's SvgUri — RN/expo Image can't decode SVG XML. Auto
|
|
51
|
+
// -detected by file extension (query-string / hash tolerant) so existing payloads
|
|
52
|
+
// with `.svg` URLs just work, no schema change.
|
|
53
|
+
const isSvgUrl = (url: string): boolean =>
|
|
54
|
+
url.split(/[?#]/)[0].toLowerCase().endsWith(".svg");
|
|
55
|
+
|
|
56
|
+
// Pick expo-image when installed (better webp/avif), else RN Image. resizeMode
|
|
57
|
+
// passes through unchanged on RN; maps to contentFit on expo-image.
|
|
58
|
+
const renderRaster = (url: string, resizeMode: ResizeMode | undefined, style: any): React.ReactElement =>
|
|
59
|
+
ExpoImage ? (
|
|
60
|
+
<ExpoImage source={url} contentFit={CONTENT_FIT[resizeMode ?? "cover"]} style={style} />
|
|
61
|
+
) : (
|
|
62
|
+
<RNImage source={{ uri: url }} resizeMode={resizeMode} style={style} />
|
|
63
|
+
);
|
|
64
|
+
|
|
21
65
|
type ImageUIElement = Extract<UIElement, { type: "Image" }>;
|
|
22
66
|
|
|
23
67
|
type Props = {
|
|
@@ -27,6 +71,7 @@ type Props = {
|
|
|
27
71
|
|
|
28
72
|
export const ImageElementComponent = ({ element }: Props): React.ReactElement => {
|
|
29
73
|
const p = element.props;
|
|
74
|
+
const isSvg = isSvgUrl(p.url);
|
|
30
75
|
const hasShadow = p.shadowColor != null || p.elevation != null;
|
|
31
76
|
// iOS clips shadows when overflow:hidden, so a shadow-bearing Image needs a
|
|
32
77
|
// wrapper View carrying the shadow (no overflow clip) and the Image inside
|
|
@@ -58,17 +103,21 @@ export const ImageElementComponent = ({ element }: Props): React.ReactElement =>
|
|
|
58
103
|
paddingVertical: p.paddingVertical,
|
|
59
104
|
...(shadowStyle ?? {}),
|
|
60
105
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
borderRadius: p.borderRadius,
|
|
69
|
-
overflow: (p.overflow ?? "hidden") as any,
|
|
70
|
-
}}
|
|
106
|
+
// Inner content fills the wrapper (which carries layout + corner clip).
|
|
107
|
+
const innerImage = isSvg ? (
|
|
108
|
+
<SvgUri
|
|
109
|
+
uri={p.url}
|
|
110
|
+
width="100%"
|
|
111
|
+
height="100%"
|
|
112
|
+
preserveAspectRatio={SVG_ASPECT[p.resizeMode ?? "contain"]}
|
|
71
113
|
/>
|
|
114
|
+
) : (
|
|
115
|
+
renderRaster(p.url, p.resizeMode, {
|
|
116
|
+
width: "100%",
|
|
117
|
+
height: "100%",
|
|
118
|
+
borderRadius: p.borderRadius,
|
|
119
|
+
overflow: (p.overflow ?? "hidden") as any,
|
|
120
|
+
})
|
|
72
121
|
);
|
|
73
122
|
if (p.backgroundGradient) {
|
|
74
123
|
return (
|
|
@@ -80,35 +129,46 @@ export const ImageElementComponent = ({ element }: Props): React.ReactElement =>
|
|
|
80
129
|
return <View style={wrapperStyle as any}>{innerImage}</View>;
|
|
81
130
|
}
|
|
82
131
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
const simpleStyle = {
|
|
133
|
+
flex: p.flex,
|
|
134
|
+
flexShrink: p.flexShrink,
|
|
135
|
+
flexGrow: p.flexGrow,
|
|
136
|
+
alignSelf: p.alignSelf,
|
|
137
|
+
aspectRatio: p.aspectRatio,
|
|
138
|
+
width: dim(p.width),
|
|
139
|
+
height: dim(p.height),
|
|
140
|
+
minWidth: p.minWidth,
|
|
141
|
+
maxWidth: p.maxWidth,
|
|
142
|
+
minHeight: p.minHeight,
|
|
143
|
+
maxHeight: p.maxHeight,
|
|
144
|
+
backgroundColor: p.backgroundColor,
|
|
145
|
+
overflow: p.overflow,
|
|
146
|
+
borderRadius: p.borderRadius,
|
|
147
|
+
borderWidth: p.borderWidth,
|
|
148
|
+
borderColor: p.borderColor,
|
|
149
|
+
opacity: p.opacity,
|
|
150
|
+
margin: p.margin,
|
|
151
|
+
marginHorizontal: p.marginHorizontal,
|
|
152
|
+
marginVertical: p.marginVertical,
|
|
153
|
+
padding: p.padding,
|
|
154
|
+
paddingHorizontal: p.paddingHorizontal,
|
|
155
|
+
paddingVertical: p.paddingVertical,
|
|
156
|
+
} as any;
|
|
157
|
+
|
|
158
|
+
// SvgUri can't carry the full RN layout style itself, so wrap it in a View that
|
|
159
|
+
// does, and let the SVG fill it.
|
|
160
|
+
if (isSvg) {
|
|
161
|
+
return (
|
|
162
|
+
<View style={simpleStyle}>
|
|
163
|
+
<SvgUri
|
|
164
|
+
uri={p.url}
|
|
165
|
+
width="100%"
|
|
166
|
+
height="100%"
|
|
167
|
+
preserveAspectRatio={SVG_ASPECT[p.resizeMode ?? "contain"]}
|
|
168
|
+
/>
|
|
169
|
+
</View>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return renderRaster(p.url, p.resizeMode, simpleStyle);
|
|
114
174
|
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { evaluateCondition } from "@rocapine/react-native-onboarding";
|
|
4
|
+
import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
|
|
5
|
+
import { UIElement } from "../types";
|
|
6
|
+
import { RenderContext, dim, interpolate, RichTextStyleContext, InheritedTextStyle } from "./shared";
|
|
7
|
+
import { GradientBox } from "./GradientBox";
|
|
8
|
+
|
|
9
|
+
// Mirror of the headless RichTextElement schema. Kept in lockstep with
|
|
10
|
+
// packages/onboarding/src/steps/ComposableScreen/elements/RichTextElement.ts —
|
|
11
|
+
// TS won't catch drift because this re-declares its own type.
|
|
12
|
+
export type RichTextElementProps = BaseBoxProps & {
|
|
13
|
+
gap?: number;
|
|
14
|
+
alignItems?: "flex-start" | "center" | "flex-end" | "baseline" | "stretch";
|
|
15
|
+
justifyContent?: "flex-start" | "center" | "flex-end" | "space-between" | "space-around";
|
|
16
|
+
flexWrap?: "wrap" | "nowrap";
|
|
17
|
+
fontSize?: number;
|
|
18
|
+
fontWeight?: string;
|
|
19
|
+
fontFamily?: string | "inherit";
|
|
20
|
+
fontStyle?: "normal" | "italic";
|
|
21
|
+
color?: string;
|
|
22
|
+
textAlign?: "left" | "center" | "right";
|
|
23
|
+
letterSpacing?: number;
|
|
24
|
+
lineHeight?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const RichTextElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
28
|
+
gap: z.number().optional(),
|
|
29
|
+
alignItems: z.enum(["flex-start", "center", "flex-end", "baseline", "stretch"]).optional(),
|
|
30
|
+
justifyContent: z.enum(["flex-start", "center", "flex-end", "space-between", "space-around"]).optional(),
|
|
31
|
+
flexWrap: z.enum(["wrap", "nowrap"]).optional(),
|
|
32
|
+
fontSize: z.number().optional(),
|
|
33
|
+
fontWeight: z.string().optional(),
|
|
34
|
+
fontFamily: z.string().optional(),
|
|
35
|
+
fontStyle: z.enum(["normal", "italic"]).optional(),
|
|
36
|
+
color: z.string().optional(),
|
|
37
|
+
textAlign: z.enum(["left", "center", "right"]).optional(),
|
|
38
|
+
letterSpacing: z.number().optional(),
|
|
39
|
+
lineHeight: z.number().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
type RichTextUIElement = Extract<UIElement, { type: "RichText" }>;
|
|
43
|
+
type TextChild = Extract<UIElement, { type: "Text" }>;
|
|
44
|
+
|
|
45
|
+
type Props = {
|
|
46
|
+
element: RichTextUIElement;
|
|
47
|
+
ctx: RenderContext;
|
|
48
|
+
parentType?: "XStack" | "YStack" | "ZStack" | "RichText" | "XScroll";
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// `textAlign` aligns text *inside* a Text box, but RichText splits each word into
|
|
52
|
+
// its own shrink-wrapped flex item, so textAlign is a no-op on the row. The row's
|
|
53
|
+
// horizontal distribution is governed by `justifyContent` — map textAlign onto it
|
|
54
|
+
// (explicit `justifyContent` still wins) so authors get the alignment they expect.
|
|
55
|
+
const ALIGN_TO_JUSTIFY = {
|
|
56
|
+
left: "flex-start",
|
|
57
|
+
center: "center",
|
|
58
|
+
right: "flex-end",
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
61
|
+
// A plain-text child (no box styling, no motion) is split into one flex item per
|
|
62
|
+
// word so the row wraps word-by-word like real text — the chip pattern from
|
|
63
|
+
// host apps (parseTitleWithChips). A child carrying box styling (backgroundColor
|
|
64
|
+
// / borderRadius / border / padding) or motion is a "chip" and stays atomic so
|
|
65
|
+
// its box renders as a single rounded unit.
|
|
66
|
+
const isFlowingText = (child: TextChild): boolean => {
|
|
67
|
+
const p = child.props;
|
|
68
|
+
return (
|
|
69
|
+
typeof p.content === "string" &&
|
|
70
|
+
p.backgroundColor == null &&
|
|
71
|
+
p.backgroundGradient == null &&
|
|
72
|
+
p.borderRadius == null &&
|
|
73
|
+
p.borderWidth == null &&
|
|
74
|
+
p.padding == null &&
|
|
75
|
+
p.paddingHorizontal == null &&
|
|
76
|
+
p.paddingVertical == null &&
|
|
77
|
+
// margin / explicit size would be applied to *every* word if split, so a
|
|
78
|
+
// child carrying any of them is treated as an atomic chip too.
|
|
79
|
+
p.margin == null &&
|
|
80
|
+
p.marginHorizontal == null &&
|
|
81
|
+
p.marginVertical == null &&
|
|
82
|
+
p.width == null &&
|
|
83
|
+
p.height == null &&
|
|
84
|
+
p.minWidth == null &&
|
|
85
|
+
p.maxWidth == null &&
|
|
86
|
+
p.minHeight == null &&
|
|
87
|
+
p.maxHeight == null &&
|
|
88
|
+
p.animation == null &&
|
|
89
|
+
p.transform == null
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Wrapping flex row of child `Text` elements. Plain-text children are split into
|
|
95
|
+
* per-word flex items so text wraps word-by-word (like a paragraph); box-styled
|
|
96
|
+
* children render as atomic "chips" (padded/rounded/rotated pills) that honor
|
|
97
|
+
* their own box props, `renderWhen`, `expression`, and motion. The result is a
|
|
98
|
+
* title where chips sit inline with naturally-wrapping words.
|
|
99
|
+
*
|
|
100
|
+
* The container's text-style props are published via `RichTextStyleContext` so
|
|
101
|
+
* every child `Text` inherits them as defaults (child overrides win).
|
|
102
|
+
*/
|
|
103
|
+
export const RichTextElementComponent = ({ element, ctx, parentType }: Props): React.ReactElement => {
|
|
104
|
+
const p = element.props;
|
|
105
|
+
|
|
106
|
+
const inheritedTextStyle: InheritedTextStyle = {
|
|
107
|
+
fontSize: p.fontSize,
|
|
108
|
+
fontWeight: p.fontWeight,
|
|
109
|
+
fontFamily: p.fontFamily,
|
|
110
|
+
fontStyle: p.fontStyle,
|
|
111
|
+
color: p.color,
|
|
112
|
+
textAlign: p.textAlign,
|
|
113
|
+
letterSpacing: p.letterSpacing,
|
|
114
|
+
lineHeight: p.lineHeight,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Expand children into the actual flex items to render: split flowing text into
|
|
118
|
+
// words, keep chips whole. renderWhen is evaluated once per source child here
|
|
119
|
+
// (flowing-text words then render unconditionally); chips keep their renderWhen
|
|
120
|
+
// and are gated by renderElement.
|
|
121
|
+
const flatVars = Object.fromEntries(
|
|
122
|
+
Object.entries(ctx.variables).map(([k, v]) => [k, v?.value])
|
|
123
|
+
);
|
|
124
|
+
const expanded: UIElement[] = [];
|
|
125
|
+
for (const child of element.children) {
|
|
126
|
+
if (child.renderWhen && !evaluateCondition(child.renderWhen, flatVars)) continue;
|
|
127
|
+
|
|
128
|
+
if (isFlowingText(child)) {
|
|
129
|
+
const raw = child.props.content as string;
|
|
130
|
+
const text = child.props.mode === "expression" ? interpolate(raw, ctx.variables) : raw;
|
|
131
|
+
const tokens = text.split(/(\s+)/).filter((t) => t.length > 0);
|
|
132
|
+
tokens.forEach((tok, i) => {
|
|
133
|
+
expanded.push({
|
|
134
|
+
...child,
|
|
135
|
+
id: `${child.id}-w${i}`,
|
|
136
|
+
renderWhen: undefined,
|
|
137
|
+
// mode dropped (undefined): content is already interpolated above, and
|
|
138
|
+
// undefined is truer to the schema than the non-enum "plain".
|
|
139
|
+
props: { ...child.props, content: tok, mode: undefined },
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
// Chip: renderWhen already passed above; null it so renderElement doesn't
|
|
144
|
+
// re-evaluate the same condition (symmetry with the word path).
|
|
145
|
+
expanded.push({ ...child, renderWhen: undefined });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<GradientBox
|
|
151
|
+
gradient={p.backgroundGradient}
|
|
152
|
+
style={{
|
|
153
|
+
flexDirection: "row",
|
|
154
|
+
flexWrap: p.flexWrap ?? "wrap",
|
|
155
|
+
gap: p.gap,
|
|
156
|
+
alignItems: p.alignItems ?? "center",
|
|
157
|
+
justifyContent:
|
|
158
|
+
p.justifyContent ?? (p.textAlign ? ALIGN_TO_JUSTIFY[p.textAlign] : "center"),
|
|
159
|
+
flex: p.flex,
|
|
160
|
+
flexShrink: p.flexShrink ?? (parentType === "XStack" ? 1 : undefined),
|
|
161
|
+
flexGrow: p.flexGrow,
|
|
162
|
+
alignSelf: p.alignSelf,
|
|
163
|
+
width: dim(p.width),
|
|
164
|
+
height: dim(p.height),
|
|
165
|
+
minWidth: p.minWidth,
|
|
166
|
+
maxWidth: p.maxWidth,
|
|
167
|
+
minHeight: p.minHeight,
|
|
168
|
+
maxHeight: p.maxHeight,
|
|
169
|
+
padding: p.padding,
|
|
170
|
+
paddingHorizontal: p.paddingHorizontal,
|
|
171
|
+
paddingVertical: p.paddingVertical,
|
|
172
|
+
margin: p.margin,
|
|
173
|
+
marginHorizontal: p.marginHorizontal,
|
|
174
|
+
marginVertical: p.marginVertical,
|
|
175
|
+
backgroundColor: p.backgroundGradient ? undefined : p.backgroundColor,
|
|
176
|
+
borderWidth: p.borderWidth,
|
|
177
|
+
borderRadius: p.borderRadius,
|
|
178
|
+
borderColor: p.borderColor,
|
|
179
|
+
overflow: p.overflow,
|
|
180
|
+
opacity: p.opacity,
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
<RichTextStyleContext.Provider value={inheritedTextStyle}>
|
|
184
|
+
{ctx.renderChildren(expanded, "RichText")}
|
|
185
|
+
</RichTextStyleContext.Provider>
|
|
186
|
+
</GradientBox>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
@@ -23,6 +23,8 @@ export type ScrollViewElementProps = BaseBoxProps & {
|
|
|
23
23
|
contentInset?: ScrollViewContentInset;
|
|
24
24
|
contentContainerPadding?: number;
|
|
25
25
|
keyboardShouldPersistTaps?: "always" | "never" | "handled";
|
|
26
|
+
alignItems?: "flex-start" | "center" | "flex-end" | "stretch" | "baseline";
|
|
27
|
+
justifyContent?: "flex-start" | "center" | "flex-end" | "space-between" | "space-around";
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
const ContentInsetSchema = z.object({
|
|
@@ -42,6 +44,8 @@ export const ScrollViewElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
|
42
44
|
contentInset: ContentInsetSchema.optional(),
|
|
43
45
|
contentContainerPadding: z.number().min(0).optional(),
|
|
44
46
|
keyboardShouldPersistTaps: z.enum(["always", "never", "handled"]).optional(),
|
|
47
|
+
alignItems: z.enum(["flex-start", "center", "flex-end", "stretch", "baseline"]).optional(),
|
|
48
|
+
justifyContent: z.enum(["flex-start", "center", "flex-end", "space-between", "space-around"]).optional(),
|
|
45
49
|
});
|
|
46
50
|
|
|
47
51
|
type ScrollViewUIElement = Extract<UIElement, { type: "ScrollView" }>;
|
|
@@ -81,8 +85,17 @@ export const ScrollViewElementComponent = ({ element, ctx }: Props): React.React
|
|
|
81
85
|
opacity: p.opacity,
|
|
82
86
|
};
|
|
83
87
|
|
|
88
|
+
// Horizontal: children must keep their intrinsic width and overflow so the row
|
|
89
|
+
// can scroll — so NO flexGrow (which would pin the content to the viewport
|
|
90
|
+
// width) and children render with parentType "XScroll" (row layout, no
|
|
91
|
+
// flexShrink default). Vertical keeps flexGrow:1 so a short payload still fills
|
|
92
|
+
// the scroll viewport. alignItems/justifyContent let authors control cross-axis
|
|
93
|
+
// alignment + distribution along the scroll axis.
|
|
84
94
|
const contentContainerStyle = {
|
|
85
|
-
|
|
95
|
+
flexDirection: horizontal ? ("row" as const) : ("column" as const),
|
|
96
|
+
flexGrow: horizontal ? undefined : 1,
|
|
97
|
+
alignItems: p.alignItems,
|
|
98
|
+
justifyContent: p.justifyContent,
|
|
86
99
|
padding: p.contentContainerPadding,
|
|
87
100
|
};
|
|
88
101
|
|
|
@@ -99,7 +112,7 @@ export const ScrollViewElementComponent = ({ element, ctx }: Props): React.React
|
|
|
99
112
|
style={hasGradient ? { flex: 1 } : containerStyle}
|
|
100
113
|
contentContainerStyle={contentContainerStyle}
|
|
101
114
|
>
|
|
102
|
-
{ctx.renderChildren(element.children, horizontal ? "
|
|
115
|
+
{ctx.renderChildren(element.children, horizontal ? "XScroll" : "YStack")}
|
|
103
116
|
</ScrollView>
|
|
104
117
|
);
|
|
105
118
|
|
|
@@ -24,7 +24,7 @@ type StackUIElement = Extract<UIElement, { type: "YStack" | "XStack" }>;
|
|
|
24
24
|
type Props = {
|
|
25
25
|
element: StackUIElement;
|
|
26
26
|
ctx: RenderContext;
|
|
27
|
-
parentType?: "XStack" | "YStack" | "ZStack";
|
|
27
|
+
parentType?: "XStack" | "YStack" | "ZStack" | "RichText" | "XScroll";
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
export const StackElementComponent = ({ element, ctx, parentType }: Props): React.ReactElement => {
|
|
@@ -4,7 +4,7 @@ import { Text } from "react-native";
|
|
|
4
4
|
import { useResolvedFontStyle } from "@rocapine/react-native-onboarding";
|
|
5
5
|
import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
|
|
6
6
|
import { UIElement } from "../types";
|
|
7
|
-
import { RenderContext, interpolate, dim, resolveInheritedFontFamily } from "./shared";
|
|
7
|
+
import { RenderContext, interpolate, dim, resolveInheritedFontFamily, RichTextStyleContext } from "./shared";
|
|
8
8
|
import { GradientBox } from "./GradientBox";
|
|
9
9
|
|
|
10
10
|
export type TextSpan = {
|
|
@@ -118,12 +118,22 @@ type TextUIElement = Extract<UIElement, { type: "Text" }>;
|
|
|
118
118
|
type Props = {
|
|
119
119
|
element: TextUIElement;
|
|
120
120
|
ctx: RenderContext;
|
|
121
|
-
parentType?: "XStack" | "YStack" | "ZStack";
|
|
121
|
+
parentType?: "XStack" | "YStack" | "ZStack" | "RichText" | "XScroll";
|
|
122
122
|
};
|
|
123
123
|
|
|
124
124
|
export const TextElementComponent = ({ element, ctx, parentType }: Props): React.ReactElement => {
|
|
125
125
|
const { theme, variables } = ctx;
|
|
126
126
|
const p = element.props;
|
|
127
|
+
// Text-style defaults inherited from a parent `RichText` container (empty when
|
|
128
|
+
// this Text isn't inside one). Element props always win over inherited values.
|
|
129
|
+
const inherited = React.useContext(RichTextStyleContext);
|
|
130
|
+
const fontSize = p.fontSize ?? inherited.fontSize;
|
|
131
|
+
const fontWeight = p.fontWeight ?? inherited.fontWeight;
|
|
132
|
+
const fontStyle = p.fontStyle ?? inherited.fontStyle;
|
|
133
|
+
const color = p.color ?? inherited.color;
|
|
134
|
+
const textAlign = p.textAlign ?? inherited.textAlign;
|
|
135
|
+
const letterSpacing = p.letterSpacing ?? inherited.letterSpacing;
|
|
136
|
+
const lineHeight = p.lineHeight ?? inherited.lineHeight;
|
|
127
137
|
const isExpression = p.mode === "expression";
|
|
128
138
|
const content: string | TextSpan[] = Array.isArray(p.content)
|
|
129
139
|
? isExpression
|
|
@@ -133,10 +143,10 @@ export const TextElementComponent = ({ element, ctx, parentType }: Props): React
|
|
|
133
143
|
? interpolate(p.content, variables)
|
|
134
144
|
: p.content;
|
|
135
145
|
const inheritedFontFamily = resolveInheritedFontFamily(
|
|
136
|
-
p.fontFamily,
|
|
146
|
+
p.fontFamily ?? inherited.fontFamily,
|
|
137
147
|
theme.typography.defaultFontFamily
|
|
138
148
|
);
|
|
139
|
-
const resolvedFont = useResolvedFontStyle(inheritedFontFamily,
|
|
149
|
+
const resolvedFont = useResolvedFontStyle(inheritedFontFamily, fontWeight);
|
|
140
150
|
|
|
141
151
|
const textNode = (
|
|
142
152
|
<Text
|
|
@@ -151,14 +161,14 @@ export const TextElementComponent = ({ element, ctx, parentType }: Props): React
|
|
|
151
161
|
maxWidth: p.backgroundGradient ? undefined : p.maxWidth,
|
|
152
162
|
minHeight: p.backgroundGradient ? undefined : p.minHeight,
|
|
153
163
|
maxHeight: p.backgroundGradient ? undefined : p.maxHeight,
|
|
154
|
-
fontSize
|
|
155
|
-
fontWeight: resolvedFont.resolvedToVariant ? undefined : (
|
|
164
|
+
fontSize,
|
|
165
|
+
fontWeight: resolvedFont.resolvedToVariant ? undefined : (fontWeight as any),
|
|
156
166
|
fontFamily: resolvedFont.fontFamily,
|
|
157
|
-
fontStyle
|
|
158
|
-
color:
|
|
159
|
-
textAlign
|
|
160
|
-
letterSpacing
|
|
161
|
-
lineHeight
|
|
167
|
+
fontStyle,
|
|
168
|
+
color: color ?? theme.colors.text.primary,
|
|
169
|
+
textAlign,
|
|
170
|
+
letterSpacing,
|
|
171
|
+
lineHeight,
|
|
162
172
|
backgroundColor: p.backgroundGradient ? undefined : p.backgroundColor,
|
|
163
173
|
padding: p.backgroundGradient ? undefined : p.padding,
|
|
164
174
|
paddingHorizontal: p.backgroundGradient ? undefined : p.paddingHorizontal,
|
|
@@ -5,6 +5,7 @@ import { BaseBoxProps } from "./BaseBoxProps";
|
|
|
5
5
|
import { RenderContext } from "./shared";
|
|
6
6
|
import { StackElementComponent } from "./StackElement";
|
|
7
7
|
import { TextElementComponent } from "./TextElement";
|
|
8
|
+
import { RichTextElementComponent } from "./RichTextElement";
|
|
8
9
|
import { ImageElementComponent } from "./ImageElement";
|
|
9
10
|
import { LottieElementComponent } from "./LottieElement";
|
|
10
11
|
import { RiveElementRenderer } from "./RiveElement";
|
|
@@ -27,7 +28,7 @@ import { AnimatedBox } from "./AnimatedBox";
|
|
|
27
28
|
export const renderElement = (
|
|
28
29
|
element: UIElement,
|
|
29
30
|
ctx: RenderContext,
|
|
30
|
-
parentType?: "XStack" | "YStack" | "ZStack"
|
|
31
|
+
parentType?: "XStack" | "YStack" | "ZStack" | "RichText" | "XScroll"
|
|
31
32
|
): React.ReactNode => {
|
|
32
33
|
if (element.renderWhen) {
|
|
33
34
|
const flatVars = Object.fromEntries(
|
|
@@ -47,6 +48,10 @@ export const renderElement = (
|
|
|
47
48
|
return <TextElementComponent key={element.id} element={element} ctx={ctx} parentType={parentType} />;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
if (element.type === "RichText") {
|
|
52
|
+
return <RichTextElementComponent key={element.id} element={element} ctx={ctx} parentType={parentType} />;
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
if (element.type === "Image") {
|
|
51
56
|
return <ImageElementComponent key={element.id} element={element} ctx={ctx} />;
|
|
52
57
|
}
|
|
@@ -11,9 +11,27 @@ export type RenderContext = {
|
|
|
11
11
|
setVariable: (key: string, entry: ComposableVariableEntry) => void;
|
|
12
12
|
onContinue: () => void;
|
|
13
13
|
customActions: CustomActions;
|
|
14
|
-
renderChildren: (elements: UIElement[], parentType: "XStack" | "YStack" | "ZStack") => React.ReactNode;
|
|
14
|
+
renderChildren: (elements: UIElement[], parentType: "XStack" | "YStack" | "ZStack" | "RichText" | "XScroll") => React.ReactNode;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// Text-style defaults a `RichText` container hands down to its child `Text`
|
|
18
|
+
// elements. A `<View>` doesn't propagate text style to nested `<Text>`, so the
|
|
19
|
+
// RichText renderer publishes these via context and `TextElementComponent`
|
|
20
|
+
// merges them under its own props (child always wins). Empty default ({}) means
|
|
21
|
+
// Text elements outside a RichText behave unchanged.
|
|
22
|
+
export type InheritedTextStyle = {
|
|
23
|
+
fontSize?: number;
|
|
24
|
+
fontWeight?: string;
|
|
25
|
+
fontFamily?: string | "inherit";
|
|
26
|
+
fontStyle?: "normal" | "italic";
|
|
27
|
+
color?: string;
|
|
28
|
+
textAlign?: "left" | "center" | "right";
|
|
29
|
+
letterSpacing?: number;
|
|
30
|
+
lineHeight?: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const RichTextStyleContext = React.createContext<InheritedTextStyle>({});
|
|
34
|
+
|
|
17
35
|
export const interpolate = (template: string, variables: Record<string, ComposableVariableEntry>): string =>
|
|
18
36
|
template.replace(/\{\{([^}]+?)\}\}/g, (_, key) => variables[key]?.label ?? variables[key]?.value ?? "");
|
|
19
37
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { CustomPayloadSchema } from "../types";
|
|
11
11
|
import { type StackElementProps, StackElementPropsSchema } from "./elements/StackElement";
|
|
12
12
|
import { type TextElementProps, TextElementPropsSchema } from "./elements/TextElement";
|
|
13
|
+
import { type RichTextElementProps, RichTextElementPropsSchema } from "./elements/RichTextElement";
|
|
13
14
|
import { type ImageElementProps, ImageElementPropsSchema } from "./elements/ImageElement";
|
|
14
15
|
import { type LottieElementProps, LottieElementPropsSchema } from "./elements/LottieElement";
|
|
15
16
|
import { type RiveElementProps, RiveElementPropsSchema } from "./elements/RiveElement";
|
|
@@ -37,6 +38,7 @@ export type { BaseBoxProps } from "./elements/BaseBoxProps";
|
|
|
37
38
|
export { BaseBoxPropsSchema } from "./elements/BaseBoxProps";
|
|
38
39
|
export type { StackElementProps } from "./elements/StackElement";
|
|
39
40
|
export type { TextElementProps } from "./elements/TextElement";
|
|
41
|
+
export type { RichTextElementProps } from "./elements/RichTextElement";
|
|
40
42
|
export type { ImageElementProps } from "./elements/ImageElement";
|
|
41
43
|
export type { LottieElementProps } from "./elements/LottieElement";
|
|
42
44
|
export type { RiveElementProps } from "./elements/RiveElement";
|
|
@@ -76,6 +78,14 @@ export type UIElement =
|
|
|
76
78
|
type: "Text";
|
|
77
79
|
props: TextElementProps;
|
|
78
80
|
}
|
|
81
|
+
| {
|
|
82
|
+
id: string;
|
|
83
|
+
name?: string;
|
|
84
|
+
renderWhen?: LeafCondition | ConditionGroup;
|
|
85
|
+
type: "RichText";
|
|
86
|
+
props: RichTextElementProps;
|
|
87
|
+
children: Array<Extract<UIElement, { type: "Text" }>>;
|
|
88
|
+
}
|
|
79
89
|
| {
|
|
80
90
|
id: string;
|
|
81
91
|
name?: string;
|
|
@@ -201,6 +211,17 @@ export type UIElement =
|
|
|
201
211
|
props: ProgressIndicatorElementProps;
|
|
202
212
|
};
|
|
203
213
|
|
|
214
|
+
// The `Text` variant, extracted so `RichText` can restrict its children to
|
|
215
|
+
// Text-only (children: z.array(TextUIElementSchema)) while the union references
|
|
216
|
+
// the same object.
|
|
217
|
+
const TextUIElementSchema = z.object({
|
|
218
|
+
id: z.string(),
|
|
219
|
+
name: z.string().optional(),
|
|
220
|
+
renderWhen: z.union([LeafConditionSchema, ConditionGroupSchema]).optional(),
|
|
221
|
+
type: z.literal("Text"),
|
|
222
|
+
props: TextElementPropsSchema,
|
|
223
|
+
});
|
|
224
|
+
|
|
204
225
|
export const UIElementSchema: z.ZodType<UIElement> = z.lazy(() =>
|
|
205
226
|
z.union([
|
|
206
227
|
z.object({
|
|
@@ -211,12 +232,14 @@ export const UIElementSchema: z.ZodType<UIElement> = z.lazy(() =>
|
|
|
211
232
|
props: StackElementPropsSchema,
|
|
212
233
|
children: z.array(UIElementSchema),
|
|
213
234
|
}),
|
|
235
|
+
TextUIElementSchema,
|
|
214
236
|
z.object({
|
|
215
237
|
id: z.string(),
|
|
216
238
|
name: z.string().optional(),
|
|
217
239
|
renderWhen: z.union([LeafConditionSchema, ConditionGroupSchema]).optional(),
|
|
218
|
-
type: z.literal("
|
|
219
|
-
props:
|
|
240
|
+
type: z.literal("RichText"),
|
|
241
|
+
props: RichTextElementPropsSchema,
|
|
242
|
+
children: z.array(TextUIElementSchema),
|
|
220
243
|
}),
|
|
221
244
|
z.object({
|
|
222
245
|
id: z.string(),
|