@wireai/activation 0.2.0 → 0.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/dist/analytics/index.d.mts +69 -2
- package/dist/analytics/index.d.ts +69 -2
- package/dist/analytics/index.js +123 -2
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +122 -3
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/coachmarks/index.js +6 -1
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs +6 -1
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/{eventQueue-CxKi7Qd5.d.mts → eventQueue-CA1d8Fmn.d.mts} +7 -3
- package/dist/{eventQueue-rV1dtJJR.d.ts → eventQueue-CrNB9gzH.d.ts} +7 -3
- package/dist/index.d.mts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +220 -151
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +221 -152
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +1 -1
- package/dist/questionnaire/index.d.ts +1 -1
- package/dist/questionnaire/index.js +29 -5
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +30 -6
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +1 -1
- package/dist/reviews/index.d.ts +1 -1
- package/dist/reviews/index.js +40 -9
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +42 -11
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.d.mts +1 -14
- package/dist/showcase/index.d.ts +1 -14
- package/dist/showcase/index.js +8 -2
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs +8 -2
- package/dist/showcase/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/OnboardingFlow.tsx +8 -4
- package/src/WireOnboarding.tsx +8 -3
- package/src/analytics/analyticsFacade.ts +184 -0
- package/src/analytics/eventQueue.ts +5 -0
- package/src/analytics/index.ts +12 -0
- package/src/analytics/reportClientEvent.ts +11 -2
- package/src/analytics/useAnalytics.ts +42 -0
- package/src/analytics/useScreenTracking.ts +23 -1
- package/src/cards/InterstitialCard.tsx +1 -1
- package/src/cards/NumberStepperCard.tsx +12 -7
- package/src/cards/StatusCard.tsx +13 -8
- package/src/cards/TextInputCard.tsx +10 -5
- package/src/components/AnimatedSparkle.tsx +20 -3
- package/src/components/Button.tsx +10 -1
- package/src/components/CardHandoff.tsx +2 -6
- package/src/components/CardLayout.tsx +16 -10
- package/src/components/Illustration.tsx +9 -5
- package/src/components/LoadingBlock.tsx +44 -29
- package/src/components/LoadingScreen.tsx +3 -2
- package/src/components/OnboardingScaffold.tsx +21 -23
- package/src/features/WireFeaturesProvider.tsx +7 -2
- package/src/questionnaire/QuestionnaireGate.tsx +15 -3
- package/src/reviews/ReviewGate.tsx +27 -7
- package/src/reviews/ReviewModal.tsx +4 -0
- package/src/showcase/FeatureShowcase.tsx +2 -1
- package/src/components/loaderChrome.ts +0 -28
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
scaledMs,
|
|
23
23
|
} from "../motion/motionSpec";
|
|
24
24
|
import { useReducedMotion } from "../motion/useReducedMotion";
|
|
25
|
-
import { useLoaderChrome } from "./loaderChrome";
|
|
26
25
|
|
|
27
26
|
const STAGE_SIZE = 96;
|
|
28
27
|
const ORBIT_DOT = 9;
|
|
@@ -104,11 +103,12 @@ const _LoadingBlock: React.FC<LoadingBlockProps> = ({
|
|
|
104
103
|
}) => {
|
|
105
104
|
const t = useOnboardingTheme();
|
|
106
105
|
const reduced = useReducedMotion();
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
106
|
+
// The loader fills its parent (flex:1) and centers in it. In the flow that parent is the
|
|
107
|
+
// scaffold BODY (the same content region the cards render into), so the loader lands at the
|
|
108
|
+
// content-area center — visually consistent with the cards it hands off to. Standalone
|
|
109
|
+
// (LoadingScreen / a playground CardFrame) it centers in whatever bounds it. No screen-inset
|
|
110
|
+
// math and no transform: it inherits the content area it is dropped into, so it is centered
|
|
111
|
+
// in every context by construction.
|
|
112
112
|
|
|
113
113
|
// Skeleton lines are gated behind the 300ms window: a fast fetch never shows them.
|
|
114
114
|
const [showSkeleton, setShowSkeleton] = useState(false);
|
|
@@ -118,34 +118,44 @@ const _LoadingBlock: React.FC<LoadingBlockProps> = ({
|
|
|
118
118
|
}, []);
|
|
119
119
|
|
|
120
120
|
return (
|
|
121
|
-
<View
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
<View style={[styles.center, { padding: t.spacing.lg }]}>
|
|
122
|
+
{/*
|
|
123
|
+
* The stage is the ANCHOR: it's the only in-flow child, so `justifyContent:
|
|
124
|
+
* center` lands the icon at the true center of the flow area. Title, hint and
|
|
125
|
+
* the (300ms-gated) skeleton hang BELOW it in an absolutely-positioned column,
|
|
126
|
+
* so they never displace the icon — previously they lived in the same centered
|
|
127
|
+
* column, which re-balanced (and jumped the icon UP ~½ its height) the moment
|
|
128
|
+
* the skeleton lines mounted at 300ms.
|
|
129
|
+
*/}
|
|
128
130
|
<View style={styles.stage}>
|
|
129
131
|
<View style={[styles.ring, { borderColor: t.colors.border, borderRadius: t.radius.full }]} />
|
|
130
132
|
<OrbitDot color={t.colors.primary} reduced={reduced} />
|
|
131
133
|
<AnimatedSparkle size={40} variant="pulse" />
|
|
132
134
|
</View>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
135
|
+
<View
|
|
136
|
+
style={[
|
|
137
|
+
styles.labels,
|
|
138
|
+
{ marginTop: STAGE_SIZE / 2 + t.spacing.lg, gap: t.spacing.md, paddingHorizontal: t.spacing.lg },
|
|
139
|
+
]}
|
|
140
|
+
pointerEvents="none"
|
|
141
|
+
>
|
|
142
|
+
{title ? (
|
|
143
|
+
<Text style={[headingStyle(t.fonts), { color: t.colors.text, textAlign: "center" }]}>
|
|
144
|
+
{title}
|
|
145
|
+
</Text>
|
|
146
|
+
) : null}
|
|
147
|
+
{hint ? (
|
|
148
|
+
<Text style={[bodyStyle(t.fonts), { color: t.colors.textMuted, textAlign: "center" }]}>
|
|
149
|
+
{hint}
|
|
150
|
+
</Text>
|
|
151
|
+
) : null}
|
|
152
|
+
{showSkeleton ? (
|
|
153
|
+
<View style={[styles.skeleton, { gap: t.spacing.sm, marginTop: t.spacing.sm }]}>
|
|
154
|
+
<SkeletonLine width="80%" delay={0} color={t.colors.surface} reduced={reduced} />
|
|
155
|
+
<SkeletonLine width="60%" delay={200} color={t.colors.surface} reduced={reduced} />
|
|
156
|
+
</View>
|
|
157
|
+
) : null}
|
|
158
|
+
</View>
|
|
149
159
|
</View>
|
|
150
160
|
);
|
|
151
161
|
};
|
|
@@ -154,6 +164,11 @@ export const LoadingBlock = React.memo(_LoadingBlock);
|
|
|
154
164
|
|
|
155
165
|
const styles = StyleSheet.create({
|
|
156
166
|
center: { flex: 1, alignItems: "center", justifyContent: "center" },
|
|
167
|
+
// Absolutely-positioned label column, pinned just under the centered stage: its top
|
|
168
|
+
// edge starts at the flow center (top: 50%) and the inline marginTop (STAGE_SIZE/2 +
|
|
169
|
+
// a gap) drops it below the stage. Out of flow → it never moves the stage, so the
|
|
170
|
+
// 300ms skeleton reveal grows DOWNWARD only and the icon stays centered.
|
|
171
|
+
labels: { position: "absolute", top: "50%", left: 0, right: 0, alignItems: "center" },
|
|
157
172
|
stage: {
|
|
158
173
|
width: STAGE_SIZE,
|
|
159
174
|
height: STAGE_SIZE,
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* WHY a dedicated frame: those slots used to return a bare <LoadingBlock/>, whose flex:1 only
|
|
8
8
|
* fills whatever the host happened to give it — with no safe area and, if the host didn't
|
|
9
9
|
* bound it, no height at all (top-anchored). This wraps them in the same SafeAreaView the flow
|
|
10
|
-
* uses, so the standalone loaders center exactly like the between-turns loader (
|
|
11
|
-
*
|
|
10
|
+
* uses, so the standalone loaders center exactly like the between-turns loader (both fill their
|
|
11
|
+
* parent flex:1 region and center in it — here the SafeAreaView, in the flow the scaffold body).
|
|
12
|
+
* One consistent centered look.
|
|
12
13
|
*/
|
|
13
14
|
import React from "react";
|
|
14
15
|
import { StyleSheet } from "react-native";
|
|
@@ -15,16 +15,13 @@
|
|
|
15
15
|
* RN core SafeAreaView is deprecated and iOS-only, so the kit takes the standard
|
|
16
16
|
* Expo/RN peer instead of shipping cropped layouts on Android + notched devices.
|
|
17
17
|
*/
|
|
18
|
-
import React from "react";
|
|
18
|
+
import React, { useMemo } from "react";
|
|
19
19
|
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
20
20
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
21
21
|
import { useOnboardingTheme } from "../theme/ThemeContext";
|
|
22
|
+
import type { OnboardingTheme } from "../theme/types";
|
|
22
23
|
import { bodyStyle } from "../theme/typography";
|
|
23
24
|
import { StepProgress } from "./StepProgress";
|
|
24
|
-
import { LoaderChromeProvider } from "./loaderChrome";
|
|
25
|
-
|
|
26
|
-
// The header's fixed StepProgress bar height (design bar = 6px; see StepProgress styles).
|
|
27
|
-
const PROGRESS_TRACK = 6;
|
|
28
25
|
|
|
29
26
|
export type OnboardingScaffoldProps = {
|
|
30
27
|
/** 1-based index of the current step; drives the (countless) progress bar. */
|
|
@@ -53,15 +50,9 @@ const _OnboardingScaffold: React.FC<OnboardingScaffoldProps> = ({
|
|
|
53
50
|
children,
|
|
54
51
|
}) => {
|
|
55
52
|
const t = useOnboardingTheme();
|
|
53
|
+
const styles = useMemo(() => makeStyles(t), [t]);
|
|
56
54
|
const hit = { top: 12, bottom: 12, left: 12, right: 12 };
|
|
57
55
|
|
|
58
|
-
// Top chrome above the body content = header (progress bar + its paddingTop) + the body's
|
|
59
|
-
// own paddingTop. A loader that fills the body centers half that distance too low; it reads
|
|
60
|
-
// this offset (via LoaderChromeProvider) and shifts UP by half the chrome to land at the true
|
|
61
|
-
// screen center. Cards ignore it, so their layout — and the card↔loader handoff — is untouched.
|
|
62
|
-
const topChrome = t.spacing.md + PROGRESS_TRACK + t.spacing.md;
|
|
63
|
-
const loaderOffsetY = -Math.round(topChrome / 2);
|
|
64
|
-
|
|
65
56
|
return (
|
|
66
57
|
<SafeAreaView
|
|
67
58
|
edges={["top", "bottom"]}
|
|
@@ -72,15 +63,19 @@ const _OnboardingScaffold: React.FC<OnboardingScaffoldProps> = ({
|
|
|
72
63
|
<StepProgress step={step} complete={complete} approxScreens={approxScreens} />
|
|
73
64
|
</View>
|
|
74
65
|
{onSkip ? (
|
|
75
|
-
<TouchableOpacity onPress={onSkip} hitSlop={hit} style={
|
|
66
|
+
<TouchableOpacity onPress={onSkip} hitSlop={hit} style={styles.skip}>
|
|
76
67
|
<Text style={[bodyStyle(t.fonts), { color: t.colors.textMuted }]}>{skipLabel}</Text>
|
|
77
68
|
</TouchableOpacity>
|
|
78
69
|
) : null}
|
|
79
70
|
</View>
|
|
80
71
|
|
|
81
|
-
{/* Full-screen body: the card (via CardLayout) fills this and pins its own CTA
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
{/* Full-screen body (flex:1): the card (via CardLayout) fills this and pins its own CTA,
|
|
73
|
+
and the between-turns LoadingBlock fills+centers in this SAME region — so the loader
|
|
74
|
+
lands at the content-area center, consistent with the cards, not the full-screen center. */}
|
|
75
|
+
<View
|
|
76
|
+
style={[styles.body, { paddingHorizontal: t.spacing.md, paddingTop: t.spacing.md }]}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
84
79
|
</View>
|
|
85
80
|
|
|
86
81
|
{onBack ? (
|
|
@@ -96,10 +91,13 @@ const _OnboardingScaffold: React.FC<OnboardingScaffoldProps> = ({
|
|
|
96
91
|
|
|
97
92
|
export const OnboardingScaffold = React.memo(_OnboardingScaffold);
|
|
98
93
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
94
|
+
function makeStyles(t: OnboardingTheme) {
|
|
95
|
+
return StyleSheet.create({
|
|
96
|
+
flex: { flex: 1 },
|
|
97
|
+
header: { flexDirection: "row", alignItems: "center" },
|
|
98
|
+
progressWrap: { flex: 1 },
|
|
99
|
+
body: { flex: 1 },
|
|
100
|
+
footer: { flexDirection: "row", justifyContent: "flex-start" },
|
|
101
|
+
skip: { marginLeft: t.spacing.md },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -18,8 +18,13 @@ import { defaultWireFeatures } from "./defaults";
|
|
|
18
18
|
import { useWireFeatures } from "./useWireFeatures";
|
|
19
19
|
import type { WireFeatures, WireFeaturesConfig } from "./types";
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
const
|
|
21
|
+
const CONTEXT_SYMBOL = Symbol.for("wireai.features.context");
|
|
22
|
+
const globalObj = global as any;
|
|
23
|
+
|
|
24
|
+
if (!globalObj[CONTEXT_SYMBOL]) {
|
|
25
|
+
globalObj[CONTEXT_SYMBOL] = createContext<WireFeatures | null>(null);
|
|
26
|
+
}
|
|
27
|
+
const WireFeaturesContext = globalObj[CONTEXT_SYMBOL];
|
|
23
28
|
|
|
24
29
|
export interface WireFeaturesProviderProps {
|
|
25
30
|
/** Tenant creds (+ optional storage) to fetch the flags once for the whole tree. */
|
|
@@ -43,7 +43,7 @@ const mergeThemeOver = (
|
|
|
43
43
|
|
|
44
44
|
type Phase = "form" | "thanks";
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
const _QuestionnaireGate: React.FC<QuestionnaireGateProps> = ({
|
|
47
47
|
questionnaire,
|
|
48
48
|
target,
|
|
49
49
|
sessionId,
|
|
@@ -81,6 +81,14 @@ export const QuestionnaireGate: React.FC<QuestionnaireGateProps> = ({
|
|
|
81
81
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
82
|
}, []);
|
|
83
83
|
|
|
84
|
+
const resolveTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
85
|
+
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
return () => {
|
|
88
|
+
if (resolveTimer.current) clearTimeout(resolveTimer.current);
|
|
89
|
+
};
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
84
92
|
const resolve = useCallback(() => {
|
|
85
93
|
onResolved?.();
|
|
86
94
|
}, [onResolved]);
|
|
@@ -100,8 +108,11 @@ export const QuestionnaireGate: React.FC<QuestionnaireGateProps> = ({
|
|
|
100
108
|
submitQuestionnaireResponse(target, id, submission);
|
|
101
109
|
onEvent?.({ name: "questionnaire_submitted", id });
|
|
102
110
|
setPhase("thanks");
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
if (resolveTimer.current) clearTimeout(resolveTimer.current);
|
|
112
|
+
resolveTimer.current = setTimeout(() => {
|
|
113
|
+
modalRef.current?.close();
|
|
114
|
+
}, 1500);
|
|
115
|
+
}, [canSend, opinion, improve, suggestions, sessionId, meta, target, id, onEvent]);
|
|
105
116
|
|
|
106
117
|
// The in-card "Not now"/"Skip" link plays the popup's exit animation (which resolves on
|
|
107
118
|
// completion, the same path a backdrop tap takes). Emit the dismissed moment first.
|
|
@@ -225,6 +236,7 @@ export const QuestionnaireGate: React.FC<QuestionnaireGateProps> = ({
|
|
|
225
236
|
);
|
|
226
237
|
};
|
|
227
238
|
|
|
239
|
+
export const QuestionnaireGate = React.memo(_QuestionnaireGate);
|
|
228
240
|
QuestionnaireGate.displayName = "QuestionnaireGate";
|
|
229
241
|
|
|
230
242
|
const styles = StyleSheet.create({
|
|
@@ -52,7 +52,7 @@ const mergeThemeOver = (
|
|
|
52
52
|
|
|
53
53
|
type Phase = "rating" | "feedback" | "thanks";
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
const _ReviewGate: React.FC<ReviewGateProps> = ({
|
|
56
56
|
appName,
|
|
57
57
|
store,
|
|
58
58
|
target,
|
|
@@ -94,10 +94,29 @@ export const ReviewGate: React.FC<ReviewGateProps> = ({
|
|
|
94
94
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
95
95
|
}, []);
|
|
96
96
|
|
|
97
|
+
const resolveTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
98
|
+
|
|
99
|
+
React.useEffect(() => {
|
|
100
|
+
return () => {
|
|
101
|
+
if (resolveTimer.current) clearTimeout(resolveTimer.current);
|
|
102
|
+
};
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
97
105
|
const resolve = useCallback(() => {
|
|
98
106
|
onResolved?.();
|
|
99
107
|
}, [onResolved]);
|
|
100
108
|
|
|
109
|
+
const resolveWithDelay = useCallback(() => {
|
|
110
|
+
if (resolveTimer.current) clearTimeout(resolveTimer.current);
|
|
111
|
+
resolveTimer.current = setTimeout(() => {
|
|
112
|
+
if (mode === "modal") {
|
|
113
|
+
modalRef.current?.close();
|
|
114
|
+
} else {
|
|
115
|
+
onResolved?.();
|
|
116
|
+
}
|
|
117
|
+
}, 1500);
|
|
118
|
+
}, [mode, onResolved]);
|
|
119
|
+
|
|
101
120
|
const pickRating = useCallback(
|
|
102
121
|
async (value: number) => {
|
|
103
122
|
setStars(value);
|
|
@@ -106,12 +125,12 @@ export const ReviewGate: React.FC<ReviewGateProps> = ({
|
|
|
106
125
|
onEvent?.({ name: "store_review_requested", id, stars: value });
|
|
107
126
|
await requestStoreReview(store);
|
|
108
127
|
setPhase("thanks");
|
|
109
|
-
|
|
128
|
+
resolveWithDelay();
|
|
110
129
|
} else {
|
|
111
130
|
setPhase("feedback");
|
|
112
131
|
}
|
|
113
132
|
},
|
|
114
|
-
[onEvent, id, store,
|
|
133
|
+
[onEvent, id, store, resolveWithDelay],
|
|
115
134
|
);
|
|
116
135
|
|
|
117
136
|
const sendFeedback = useCallback(() => {
|
|
@@ -127,13 +146,13 @@ export const ReviewGate: React.FC<ReviewGateProps> = ({
|
|
|
127
146
|
submitReview(target, body);
|
|
128
147
|
onEvent?.({ name: "review_feedback_submitted", id, stars });
|
|
129
148
|
setPhase("thanks");
|
|
130
|
-
|
|
131
|
-
}, [stars, feedback, suggestion, sessionId, meta, target, onEvent, id,
|
|
149
|
+
resolveWithDelay();
|
|
150
|
+
}, [stars, feedback, suggestion, sessionId, meta, target, onEvent, id, resolveWithDelay]);
|
|
132
151
|
|
|
133
152
|
const dismiss = useCallback(() => {
|
|
134
153
|
setPhase("thanks");
|
|
135
|
-
|
|
136
|
-
}, [
|
|
154
|
+
resolveWithDelay();
|
|
155
|
+
}, [resolveWithDelay]);
|
|
137
156
|
|
|
138
157
|
// The in-card "Not now"/"Skip" link. In modal mode it plays the popup's exit animation
|
|
139
158
|
// (which resolves on completion, the same path a backdrop tap takes); inline keeps the
|
|
@@ -303,6 +322,7 @@ export const ReviewGate: React.FC<ReviewGateProps> = ({
|
|
|
303
322
|
return <View style={[styles.card, surface]}>{content}</View>;
|
|
304
323
|
};
|
|
305
324
|
|
|
325
|
+
export const ReviewGate = React.memo(_ReviewGate);
|
|
306
326
|
ReviewGate.displayName = "ReviewGate";
|
|
307
327
|
|
|
308
328
|
const styles = StyleSheet.create({
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* questionnaire module can reuse the same shell without importing reviews/. This file keeps the
|
|
6
6
|
* reviews public API (`ReviewModal`, `ReviewModalHandle`, `ReviewModalProps`) pointing at the
|
|
7
7
|
* exact same runtime object, so the reviews canary and every existing import stay valid.
|
|
8
|
+
*
|
|
9
|
+
* The exported shell is already `React.memo`-wrapped at its source (`components/CenteredModal.tsx`
|
|
10
|
+
* wraps `_CenteredModal` in `React.memo`), so `ReviewModal` satisfies the shell-memoization rule
|
|
11
|
+
* without a second wrapper here. Re-wrapping would change the runtime identity the canary asserts.
|
|
8
12
|
*/
|
|
9
13
|
export { CenteredModal as ReviewModal, default } from "../components/CenteredModal";
|
|
10
14
|
export type {
|
|
@@ -48,7 +48,7 @@ const mergeThemeOver = (
|
|
|
48
48
|
* When the gate says "seen", it renders nothing and calls `onDone` from an
|
|
49
49
|
* effect (never during render).
|
|
50
50
|
*/
|
|
51
|
-
|
|
51
|
+
const _FeatureShowcase: React.FC<FeatureShowcaseProps> = ({
|
|
52
52
|
config,
|
|
53
53
|
onDone,
|
|
54
54
|
theme: themeOverride,
|
|
@@ -211,6 +211,7 @@ export const FeatureShowcase: React.FC<FeatureShowcaseProps> = ({
|
|
|
211
211
|
);
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
+
export const FeatureShowcase = React.memo(_FeatureShowcase);
|
|
214
215
|
FeatureShowcase.displayName = "FeatureShowcase";
|
|
215
216
|
|
|
216
217
|
const styles = StyleSheet.create({
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* loaderChrome — the seam that lets a loader center in the FULL flow area instead of
|
|
3
|
-
* the header-offset body it happens to render into.
|
|
4
|
-
*
|
|
5
|
-
* The between-turns loader lives inside the OnboardingScaffold body (so the card↔loader
|
|
6
|
-
* handoff stays one coordinated CardHandoff — see OnboardingFlow). But the body sits
|
|
7
|
-
* BELOW the header (the StepProgress bar), so a loader that only fills the body centers
|
|
8
|
-
* a little low — the "not centralized in the screen" device finding. The scaffold knows
|
|
9
|
-
* its own top chrome, so it publishes a vertical offset here; LoadingBlock reads it and
|
|
10
|
-
* shifts its centered content up by that much, landing it at the true screen center
|
|
11
|
-
* WITHOUT moving the body (the card underneath is untouched, so the handoff never jumps).
|
|
12
|
-
*
|
|
13
|
-
* Default 0: a LoadingBlock rendered OUTSIDE a scaffold (the standalone LoadingScreen used
|
|
14
|
-
* for cold-start / restoring / error) has no header above it, so it already centers in the
|
|
15
|
-
* full area and needs no shift.
|
|
16
|
-
*/
|
|
17
|
-
import { createContext, useContext } from "react";
|
|
18
|
-
|
|
19
|
-
export type LoaderChrome = {
|
|
20
|
-
/** Vertical shift (px, usually negative = up) that re-centers a loader against the full flow area. */
|
|
21
|
-
offsetY: number;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const LoaderChromeContext = createContext<LoaderChrome>({ offsetY: 0 });
|
|
25
|
-
|
|
26
|
-
export const LoaderChromeProvider = LoaderChromeContext.Provider;
|
|
27
|
-
|
|
28
|
-
export const useLoaderChrome = (): LoaderChrome => useContext(LoaderChromeContext);
|