@userflow-tech/react-native 0.5.0 → 0.6.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/README.md +24 -0
- package/docs/custom-fonts.md +36 -0
- package/docs/troubleshooting.md +7 -6
- package/lib/commonjs/anchors/fiber/FiberScanRootStore.js +8 -0
- package/lib/commonjs/anchors/fiber/FiberScanRootStore.js.map +1 -1
- package/lib/commonjs/anchors/fiber/detectAppFontFamily.js +341 -0
- package/lib/commonjs/anchors/fiber/detectAppFontFamily.js.map +1 -0
- package/lib/commonjs/anchors/fiber/visibleScreens.js +6 -0
- package/lib/commonjs/anchors/fiber/visibleScreens.js.map +1 -1
- package/lib/commonjs/content/BannerStepButton.js +2 -1
- package/lib/commonjs/content/BannerStepButton.js.map +1 -1
- package/lib/commonjs/content/RichContentRenderer.js +17 -11
- package/lib/commonjs/content/RichContentRenderer.js.map +1 -1
- package/lib/commonjs/content/StepButton.js +2 -1
- package/lib/commonjs/content/StepButton.js.map +1 -1
- package/lib/commonjs/content/resolveContentFontFamily.js +96 -0
- package/lib/commonjs/content/resolveContentFontFamily.js.map +1 -0
- package/lib/commonjs/index.js +2 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/runtime/BannerHost.js +3 -3
- package/lib/commonjs/runtime/BannerHost.js.map +1 -1
- package/lib/commonjs/runtime/pushBannerLayoutContext.js +11 -8
- package/lib/commonjs/runtime/pushBannerLayoutContext.js.map +1 -1
- package/lib/commonjs/surfaces/BannerSurface.js +2 -6
- package/lib/commonjs/surfaces/BannerSurface.js.map +1 -1
- package/lib/commonjs/surfaces/ProgressBar.js +2 -1
- package/lib/commonjs/surfaces/ProgressBar.js.map +1 -1
- package/lib/commonjs/surfaces/TooltipSurface.js +5 -0
- package/lib/commonjs/surfaces/TooltipSurface.js.map +1 -1
- package/lib/commonjs/surfaces/bannerWebView/BannerWebViewBar.js +6 -4
- package/lib/commonjs/surfaces/bannerWebView/BannerWebViewBar.js.map +1 -1
- package/lib/commonjs/surfaces/bannerWebView/bannerNeedsWebView.js +3 -33
- package/lib/commonjs/surfaces/bannerWebView/bannerNeedsWebView.js.map +1 -1
- package/lib/commonjs/surfaces/bannerWebView/bannerThemeStyles.js +2 -1
- package/lib/commonjs/surfaces/bannerWebView/bannerThemeStyles.js.map +1 -1
- package/lib/commonjs/surfaces/bannerWebView/bannerWebViewHeight.js +33 -0
- package/lib/commonjs/surfaces/bannerWebView/bannerWebViewHeight.js.map +1 -0
- package/lib/commonjs/surfaces/bannerWebView/buildCustomBannerHtml.js +45 -12
- package/lib/commonjs/surfaces/bannerWebView/buildCustomBannerHtml.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/anchors/fiber/FiberScanRootStore.js +8 -0
- package/lib/module/anchors/fiber/FiberScanRootStore.js.map +1 -1
- package/lib/module/anchors/fiber/detectAppFontFamily.js +330 -0
- package/lib/module/anchors/fiber/detectAppFontFamily.js.map +1 -0
- package/lib/module/anchors/fiber/visibleScreens.js +5 -0
- package/lib/module/anchors/fiber/visibleScreens.js.map +1 -1
- package/lib/module/content/BannerStepButton.js +2 -1
- package/lib/module/content/BannerStepButton.js.map +1 -1
- package/lib/module/content/RichContentRenderer.js +17 -11
- package/lib/module/content/RichContentRenderer.js.map +1 -1
- package/lib/module/content/StepButton.js +2 -1
- package/lib/module/content/StepButton.js.map +1 -1
- package/lib/module/content/resolveContentFontFamily.js +89 -0
- package/lib/module/content/resolveContentFontFamily.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/runtime/BannerHost.js +3 -3
- package/lib/module/runtime/BannerHost.js.map +1 -1
- package/lib/module/runtime/pushBannerLayoutContext.js +11 -8
- package/lib/module/runtime/pushBannerLayoutContext.js.map +1 -1
- package/lib/module/surfaces/BannerSurface.js +2 -6
- package/lib/module/surfaces/BannerSurface.js.map +1 -1
- package/lib/module/surfaces/ProgressBar.js +2 -1
- package/lib/module/surfaces/ProgressBar.js.map +1 -1
- package/lib/module/surfaces/TooltipSurface.js +5 -0
- package/lib/module/surfaces/TooltipSurface.js.map +1 -1
- package/lib/module/surfaces/bannerWebView/BannerWebViewBar.js +6 -4
- package/lib/module/surfaces/bannerWebView/BannerWebViewBar.js.map +1 -1
- package/lib/module/surfaces/bannerWebView/bannerNeedsWebView.js +3 -31
- package/lib/module/surfaces/bannerWebView/bannerNeedsWebView.js.map +1 -1
- package/lib/module/surfaces/bannerWebView/bannerThemeStyles.js +2 -1
- package/lib/module/surfaces/bannerWebView/bannerThemeStyles.js.map +1 -1
- package/lib/module/surfaces/bannerWebView/bannerWebViewHeight.js +29 -0
- package/lib/module/surfaces/bannerWebView/bannerWebViewHeight.js.map +1 -0
- package/lib/module/surfaces/bannerWebView/buildCustomBannerHtml.js +45 -12
- package/lib/module/surfaces/bannerWebView/buildCustomBannerHtml.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/commonjs/src/anchors/fiber/FiberScanRootStore.d.ts +5 -0
- package/lib/typescript/commonjs/src/anchors/fiber/FiberScanRootStore.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/anchors/fiber/detectAppFontFamily.d.ts +30 -0
- package/lib/typescript/commonjs/src/anchors/fiber/detectAppFontFamily.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/anchors/fiber/visibleScreens.d.ts +2 -0
- package/lib/typescript/commonjs/src/anchors/fiber/visibleScreens.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/content/BannerStepButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/content/RichContentRenderer.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/content/StepButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/content/resolveContentFontFamily.d.ts +35 -0
- package/lib/typescript/commonjs/src/content/resolveContentFontFamily.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/runtime/pushBannerLayoutContext.d.ts +5 -0
- package/lib/typescript/commonjs/src/runtime/pushBannerLayoutContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/surfaces/BannerSurface.d.ts +1 -4
- package/lib/typescript/commonjs/src/surfaces/BannerSurface.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/surfaces/ProgressBar.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/surfaces/TooltipSurface.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/surfaces/bannerWebView/BannerWebViewBar.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/surfaces/bannerWebView/bannerNeedsWebView.d.ts +3 -12
- package/lib/typescript/commonjs/src/surfaces/bannerWebView/bannerNeedsWebView.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/surfaces/bannerWebView/bannerThemeStyles.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/surfaces/bannerWebView/bannerWebViewHeight.d.ts +13 -0
- package/lib/typescript/commonjs/src/surfaces/bannerWebView/bannerWebViewHeight.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/surfaces/bannerWebView/buildCustomBannerHtml.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/version.d.ts +1 -1
- package/lib/typescript/module/src/anchors/fiber/FiberScanRootStore.d.ts +5 -0
- package/lib/typescript/module/src/anchors/fiber/FiberScanRootStore.d.ts.map +1 -1
- package/lib/typescript/module/src/anchors/fiber/detectAppFontFamily.d.ts +30 -0
- package/lib/typescript/module/src/anchors/fiber/detectAppFontFamily.d.ts.map +1 -0
- package/lib/typescript/module/src/anchors/fiber/visibleScreens.d.ts +2 -0
- package/lib/typescript/module/src/anchors/fiber/visibleScreens.d.ts.map +1 -1
- package/lib/typescript/module/src/content/BannerStepButton.d.ts.map +1 -1
- package/lib/typescript/module/src/content/RichContentRenderer.d.ts.map +1 -1
- package/lib/typescript/module/src/content/StepButton.d.ts.map +1 -1
- package/lib/typescript/module/src/content/resolveContentFontFamily.d.ts +35 -0
- package/lib/typescript/module/src/content/resolveContentFontFamily.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/runtime/pushBannerLayoutContext.d.ts +5 -0
- package/lib/typescript/module/src/runtime/pushBannerLayoutContext.d.ts.map +1 -1
- package/lib/typescript/module/src/surfaces/BannerSurface.d.ts +1 -4
- package/lib/typescript/module/src/surfaces/BannerSurface.d.ts.map +1 -1
- package/lib/typescript/module/src/surfaces/ProgressBar.d.ts.map +1 -1
- package/lib/typescript/module/src/surfaces/TooltipSurface.d.ts.map +1 -1
- package/lib/typescript/module/src/surfaces/bannerWebView/BannerWebViewBar.d.ts.map +1 -1
- package/lib/typescript/module/src/surfaces/bannerWebView/bannerNeedsWebView.d.ts +3 -12
- package/lib/typescript/module/src/surfaces/bannerWebView/bannerNeedsWebView.d.ts.map +1 -1
- package/lib/typescript/module/src/surfaces/bannerWebView/bannerThemeStyles.d.ts.map +1 -1
- package/lib/typescript/module/src/surfaces/bannerWebView/bannerWebViewHeight.d.ts +13 -0
- package/lib/typescript/module/src/surfaces/bannerWebView/bannerWebViewHeight.d.ts.map +1 -0
- package/lib/typescript/module/src/surfaces/bannerWebView/buildCustomBannerHtml.d.ts.map +1 -1
- package/lib/typescript/module/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/anchors/fiber/FiberScanRootStore.ts +8 -0
- package/src/anchors/fiber/detectAppFontFamily.ts +400 -0
- package/src/anchors/fiber/visibleScreens.ts +5 -0
- package/src/content/BannerStepButton.tsx +2 -1
- package/src/content/RichContentRenderer.tsx +15 -10
- package/src/content/StepButton.tsx +2 -1
- package/src/content/resolveContentFontFamily.ts +119 -0
- package/src/index.ts +2 -0
- package/src/runtime/BannerHost.tsx +3 -3
- package/src/runtime/pushBannerLayoutContext.tsx +14 -9
- package/src/surfaces/BannerSurface.tsx +2 -6
- package/src/surfaces/ProgressBar.tsx +2 -1
- package/src/surfaces/TooltipSurface.tsx +15 -2
- package/src/surfaces/bannerWebView/BannerWebViewBar.tsx +6 -4
- package/src/surfaces/bannerWebView/bannerNeedsWebView.ts +4 -36
- package/src/surfaces/bannerWebView/bannerThemeStyles.ts +2 -1
- package/src/surfaces/bannerWebView/bannerWebViewHeight.ts +32 -0
- package/src/surfaces/bannerWebView/buildCustomBannerHtml.ts +48 -12
- package/src/version.ts +1 -1
- package/vendor/core/package.json +1 -1
- package/vendor/protocol/dist/flows.d.ts +2 -0
- package/vendor/protocol/dist/flows.d.ts.map +1 -1
- package/vendor/protocol/package.json +1 -1
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-app font used when a theme is still on the default CSS stack.
|
|
3
|
+
*
|
|
4
|
+
* React Native does not inherit `fontFamily` from a parent view, so the family
|
|
5
|
+
* actually in use is the one set on `Text`. The primary font is whichever
|
|
6
|
+
* family appears on the most visible host text under the scan root (the SDK
|
|
7
|
+
* overlay is a sibling and is not walked). Embedded banners are inside that
|
|
8
|
+
* root; their hosts use a `userflow-` testID and are not votes, or the bar's
|
|
9
|
+
* own text would win. Text with no family counts as the platform font. Icon
|
|
10
|
+
* faces are not content. Nested text with no family does not vote: the
|
|
11
|
+
* ancestor text already does, and a child run must not add another vote for
|
|
12
|
+
* that family or for the platform font.
|
|
13
|
+
*
|
|
14
|
+
* A finished walk is cached until the visible screen changes, the reported
|
|
15
|
+
* screen changes, or `Userflow.reset()`. A walk that hits the node cap is not
|
|
16
|
+
* cached. Failures return null.
|
|
17
|
+
*/
|
|
18
|
+
import { getCurrentScreenLocation } from '../../screenTracker';
|
|
19
|
+
|
|
20
|
+
import { fiberScanRoot } from './FiberScanRootStore';
|
|
21
|
+
import {
|
|
22
|
+
type AnyFiber,
|
|
23
|
+
MAX_ANCESTOR_WALK,
|
|
24
|
+
getHostTypeName,
|
|
25
|
+
getLiveHostProps,
|
|
26
|
+
getReturn,
|
|
27
|
+
getStateNode,
|
|
28
|
+
} from './fiberInternals';
|
|
29
|
+
import { isFiberOnVisibleScreen, isScreenHostFiber } from './visibleScreens';
|
|
30
|
+
import { walkVisibleFibers } from './walkVisibleFibers';
|
|
31
|
+
|
|
32
|
+
const TEXT_HOST_NAMES = new Set(['Text', 'RCTText', 'RCTVirtualText']);
|
|
33
|
+
|
|
34
|
+
/** SDK hosts inside the scan root (embedded banners). Not the app's font. */
|
|
35
|
+
const SDK_HOST_TEST_ID_PREFIX = 'userflow-';
|
|
36
|
+
|
|
37
|
+
/** Cycle guard for nested-text inheritance. Real trees are only a few levels deep. */
|
|
38
|
+
const MAX_FONT_ANCESTORS = 50;
|
|
39
|
+
|
|
40
|
+
const CSS_GENERIC_FAMILIES = new Set([
|
|
41
|
+
'sans-serif',
|
|
42
|
+
'serif',
|
|
43
|
+
'monospace',
|
|
44
|
+
'system-ui',
|
|
45
|
+
'ui-sans-serif',
|
|
46
|
+
'ui-serif',
|
|
47
|
+
'ui-monospace',
|
|
48
|
+
'ui-rounded',
|
|
49
|
+
'cursive',
|
|
50
|
+
'fantasy',
|
|
51
|
+
'emoji',
|
|
52
|
+
'math',
|
|
53
|
+
'fangsong',
|
|
54
|
+
'inherit',
|
|
55
|
+
'initial',
|
|
56
|
+
'unset',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/** Short vector-icon families. Matched after stripping spaces and punctuation. */
|
|
60
|
+
const ICON_FONT_EXACT = new Set([
|
|
61
|
+
'feather',
|
|
62
|
+
'antdesign',
|
|
63
|
+
'entypo',
|
|
64
|
+
'fontisto',
|
|
65
|
+
'simplelineicons',
|
|
66
|
+
'octicons',
|
|
67
|
+
'zocial',
|
|
68
|
+
'foundation',
|
|
69
|
+
'evilicons',
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
interface FontCache {
|
|
73
|
+
instance: object;
|
|
74
|
+
/** null: countable text voted for the platform font, or nothing voted. */
|
|
75
|
+
family: string | null;
|
|
76
|
+
/** Visible screens this tally belongs to. Empty when the tree has none. */
|
|
77
|
+
screens: AnyFiber[];
|
|
78
|
+
/** `updateScreen` / router identity at tally time. Null before the first report. */
|
|
79
|
+
screenKey: string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let cache: FontCache | null = null;
|
|
83
|
+
|
|
84
|
+
/** Drop the cached app font. Called from `Userflow.reset()`. */
|
|
85
|
+
export function resetDetectedAppFont(): void {
|
|
86
|
+
cache = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Family to apply, or null when the app font is the platform face or could
|
|
91
|
+
* not be read. Never throws.
|
|
92
|
+
*/
|
|
93
|
+
export function detectAppFontFamily(): string | null {
|
|
94
|
+
try {
|
|
95
|
+
const instance = fiberScanRoot.getInstance();
|
|
96
|
+
if (!instance || typeof instance !== 'object') return null;
|
|
97
|
+
|
|
98
|
+
const root = fiberScanRoot.getFiber();
|
|
99
|
+
if (!root) return null;
|
|
100
|
+
if (cache?.instance === instance && cacheStillCurrent(cache, root)) return cache.family;
|
|
101
|
+
|
|
102
|
+
const tally = tallyHostText(root);
|
|
103
|
+
if (tally.truncated) {
|
|
104
|
+
if (!tally.voted) return null;
|
|
105
|
+
return pickMajority(tally.counts);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const family = tally.voted ? pickMajority(tally.counts) : null;
|
|
109
|
+
cache = { instance, family, screens: tally.screens, screenKey: reportedScreenKey() };
|
|
110
|
+
return family;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Faces that appear in the builder's default system stack. A comma-separated
|
|
118
|
+
* list made only of these and generic keywords is that default, not a custom
|
|
119
|
+
* font. A single concrete name is still kept, including `Roboto`.
|
|
120
|
+
*/
|
|
121
|
+
const DEFAULT_STACK_FACES = new Set([
|
|
122
|
+
'blinkmacsystemfont',
|
|
123
|
+
'segoe ui',
|
|
124
|
+
'roboto',
|
|
125
|
+
'helvetica',
|
|
126
|
+
'arial',
|
|
127
|
+
'apple color emoji',
|
|
128
|
+
'segoe ui emoji',
|
|
129
|
+
'segoe ui symbol',
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A single React Native family name. CSS stacks and generic keywords are not
|
|
134
|
+
* usable as a host `fontFamily` — RN treats the whole string as one face.
|
|
135
|
+
* Theme stacks are parsed by {@link themeConcreteFontFamily}.
|
|
136
|
+
*/
|
|
137
|
+
export function concreteFontFamily(value: unknown): string | undefined {
|
|
138
|
+
if (typeof value !== 'string') return undefined;
|
|
139
|
+
const name = unwrapFontName(value);
|
|
140
|
+
if (!name || name.includes(',') || name.startsWith('-')) return undefined;
|
|
141
|
+
if (CSS_GENERIC_FAMILIES.has(name.toLowerCase())) return undefined;
|
|
142
|
+
return name;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Family a theme author chose. A single concrete name is kept. A comma-separated
|
|
147
|
+
* list keeps the first family that is not a generic or a default-stack face,
|
|
148
|
+
* so `"Source Sans Pro", sans-serif` stays Source Sans Pro and the system
|
|
149
|
+
* stack still resolves to undefined.
|
|
150
|
+
*/
|
|
151
|
+
export function themeConcreteFontFamily(value: unknown): string | undefined {
|
|
152
|
+
if (typeof value !== 'string') return undefined;
|
|
153
|
+
const parts = splitCssFamilies(value);
|
|
154
|
+
if (parts.length === 0) return undefined;
|
|
155
|
+
if (parts.length === 1) return concreteFontFamily(parts[0]);
|
|
156
|
+
for (const part of parts) {
|
|
157
|
+
const name = concreteFontFamily(part);
|
|
158
|
+
if (!name || DEFAULT_STACK_FACES.has(name.toLowerCase())) continue;
|
|
159
|
+
return name;
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Split a CSS `font-family` list. A quote that opens a family is wrapping and
|
|
166
|
+
* is removed; a quote inside a name stays, so `Foo"Bar` is one family.
|
|
167
|
+
* Commas inside wrapping quotes are not separators.
|
|
168
|
+
*/
|
|
169
|
+
export function splitCssFamilies(value: string): string[] {
|
|
170
|
+
const parts: string[] = [];
|
|
171
|
+
let current = '';
|
|
172
|
+
let quote: '"' | "'" | null = null;
|
|
173
|
+
for (const char of value) {
|
|
174
|
+
if (quote) {
|
|
175
|
+
if (char === quote) quote = null;
|
|
176
|
+
else current += char;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if ((char === '"' || char === "'") && current.trim() === '') {
|
|
180
|
+
quote = char;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (char === ',') {
|
|
184
|
+
const name = current.trim();
|
|
185
|
+
if (name) parts.push(name);
|
|
186
|
+
current = '';
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
current += char;
|
|
190
|
+
}
|
|
191
|
+
const name = current.trim();
|
|
192
|
+
if (name) parts.push(name);
|
|
193
|
+
return parts;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Generic keywords and vendor-prefixed faces stay unquoted in CSS. */
|
|
197
|
+
export function isBareCssFamily(name: string): boolean {
|
|
198
|
+
return name.startsWith('-') || CSS_GENERIC_FAMILIES.has(name.toLowerCase());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function isIconFontFamily(name: string): boolean {
|
|
202
|
+
const key = name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
203
|
+
if (ICON_FONT_EXACT.has(key)) return true;
|
|
204
|
+
return (
|
|
205
|
+
key.startsWith('fontawesome') ||
|
|
206
|
+
key.startsWith('ionicons') ||
|
|
207
|
+
key.startsWith('materialcommunityicons') ||
|
|
208
|
+
key.startsWith('materialicons') ||
|
|
209
|
+
key.startsWith('materialsymbols')
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface Tally {
|
|
214
|
+
voted: boolean;
|
|
215
|
+
truncated: boolean;
|
|
216
|
+
screens: AnyFiber[];
|
|
217
|
+
counts: Map<string | null, number>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function tallyHostText(root: AnyFiber): Tally {
|
|
221
|
+
const counts = new Map<string | null, number>();
|
|
222
|
+
const screens: AnyFiber[] = [];
|
|
223
|
+
let voted = false;
|
|
224
|
+
const { truncated } = walkVisibleFibers(root, (fiber) => {
|
|
225
|
+
try {
|
|
226
|
+
if (isScreenHostFiber(fiber)) screens.push(fiber);
|
|
227
|
+
if (!isTextHost(fiber)) return;
|
|
228
|
+
const vote = voteFor(fiber, root);
|
|
229
|
+
if (vote === 'skip') return;
|
|
230
|
+
voted = true;
|
|
231
|
+
counts.set(vote, (counts.get(vote) ?? 0) + 1);
|
|
232
|
+
} catch {
|
|
233
|
+
// One unreadable node is not a vote.
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
return { voted, truncated, screens, counts };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Usable family, null for the platform font, or skip for an icon face. */
|
|
240
|
+
function voteFor(fiber: AnyFiber, root: AnyFiber): string | null | 'skip' {
|
|
241
|
+
if (isUnderSdkHost(fiber, root)) return 'skip';
|
|
242
|
+
const own = classifyFamily(readFontFamily(fiber));
|
|
243
|
+
if (own.kind === 'icon') return 'skip';
|
|
244
|
+
if (own.kind === 'family') return own.name;
|
|
245
|
+
if (own.kind === 'unusable') return null;
|
|
246
|
+
// No family of its own. The ancestor text node casts the vote for this run.
|
|
247
|
+
if (hasAncestorText(fiber, root)) return 'skip';
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function hasAncestorText(fiber: AnyFiber, root: AnyFiber): boolean {
|
|
252
|
+
let parent = getReturn(fiber);
|
|
253
|
+
for (let depth = 0; parent && parent !== root && depth < MAX_FONT_ANCESTORS; depth += 1) {
|
|
254
|
+
if (isTextHost(parent)) return true;
|
|
255
|
+
parent = getReturn(parent);
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function cacheStillCurrent(cached: FontCache, root: AnyFiber): boolean {
|
|
261
|
+
if (cached.screenKey !== reportedScreenKey()) return false;
|
|
262
|
+
if (cached.screens.length === 0) return true;
|
|
263
|
+
return cached.screens.every((screen) => screenStillCurrent(screen, root));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function screenStillCurrent(screen: AnyFiber, root: AnyFiber): boolean {
|
|
267
|
+
if (!isFiberOnVisibleScreen(screen)) return false;
|
|
268
|
+
let current: AnyFiber = screen;
|
|
269
|
+
for (let depth = 0; current && depth < MAX_ANCESTOR_WALK; depth += 1) {
|
|
270
|
+
if (current === root) return true;
|
|
271
|
+
current = getReturn(current);
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function reportedScreenKey(): string | null {
|
|
277
|
+
const location = getCurrentScreenLocation();
|
|
278
|
+
if (!location) return null;
|
|
279
|
+
const key = 'path' in location ? location.path : location.screen;
|
|
280
|
+
return key || null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
type Classified =
|
|
284
|
+
| { kind: 'absent' }
|
|
285
|
+
| { kind: 'icon' }
|
|
286
|
+
| { kind: 'family'; name: string }
|
|
287
|
+
| { kind: 'unusable' };
|
|
288
|
+
|
|
289
|
+
function classifyFamily(raw: string | undefined): Classified {
|
|
290
|
+
if (!raw) return { kind: 'absent' };
|
|
291
|
+
if (isIconFontFamily(raw)) return { kind: 'icon' };
|
|
292
|
+
const concrete = concreteFontFamily(raw);
|
|
293
|
+
if (!concrete) return { kind: 'unusable' };
|
|
294
|
+
return { kind: 'family', name: concrete };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function pickMajority(counts: Map<string | null, number>): string | null {
|
|
298
|
+
const platform = counts.get(null) ?? 0;
|
|
299
|
+
let bestName: string | null = null;
|
|
300
|
+
let bestCount = 0;
|
|
301
|
+
for (const [name, count] of counts) {
|
|
302
|
+
if (name == null) continue;
|
|
303
|
+
if (bestName == null || count > bestCount || (count === bestCount && name < bestName)) {
|
|
304
|
+
bestName = name;
|
|
305
|
+
bestCount = count;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (bestName == null || platform > bestCount) return null;
|
|
309
|
+
return bestName;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isUnderSdkHost(fiber: AnyFiber, root: AnyFiber): boolean {
|
|
313
|
+
let parent = getReturn(fiber);
|
|
314
|
+
for (let depth = 0; parent && parent !== root && depth < MAX_FONT_ANCESTORS; depth += 1) {
|
|
315
|
+
try {
|
|
316
|
+
const testID = getLiveHostProps(parent)?.testID;
|
|
317
|
+
if (typeof testID === 'string' && testID.startsWith(SDK_HOST_TEST_ID_PREFIX)) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
// An unreadable ancestor is not an SDK host.
|
|
322
|
+
}
|
|
323
|
+
parent = getReturn(parent);
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function isTextHost(fiber: AnyFiber): boolean {
|
|
329
|
+
const typeName = getHostTypeName(fiber);
|
|
330
|
+
if (typeName && TEXT_HOST_NAMES.has(typeName)) return true;
|
|
331
|
+
const elementType = fiber?.elementType;
|
|
332
|
+
if (typeof elementType === 'string' && TEXT_HOST_NAMES.has(elementType)) return true;
|
|
333
|
+
try {
|
|
334
|
+
const node = getStateNode(fiber) as {
|
|
335
|
+
viewConfig?: { uiViewClassName?: unknown };
|
|
336
|
+
__viewConfig?: { uiViewClassName?: unknown };
|
|
337
|
+
} | null;
|
|
338
|
+
const className = node?.viewConfig?.uiViewClassName ?? node?.__viewConfig?.uiViewClassName;
|
|
339
|
+
return typeof className === 'string' && TEXT_HOST_NAMES.has(className);
|
|
340
|
+
} catch {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function readFontFamily(fiber: AnyFiber): string | undefined {
|
|
346
|
+
try {
|
|
347
|
+
const flat = flattenFontStyle(getLiveHostProps(fiber)?.style);
|
|
348
|
+
const family = flat?.fontFamily;
|
|
349
|
+
if (typeof family !== 'string') return undefined;
|
|
350
|
+
const trimmed = family.trim();
|
|
351
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
352
|
+
} catch {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function flattenFontStyle(style: unknown): { fontFamily?: unknown } | null {
|
|
358
|
+
if (style == null || style === false) return null;
|
|
359
|
+
if (typeof style === 'number') return flattenRegisteredStyle(style);
|
|
360
|
+
if (Array.isArray(style)) {
|
|
361
|
+
let family: unknown;
|
|
362
|
+
for (const entry of style) {
|
|
363
|
+
const flat = flattenFontStyle(entry);
|
|
364
|
+
if (flat && flat.fontFamily != null) family = flat.fontFamily;
|
|
365
|
+
}
|
|
366
|
+
return { fontFamily: family };
|
|
367
|
+
}
|
|
368
|
+
if (typeof style === 'object') return style as { fontFamily?: unknown };
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* StyleSheet ids are numbers. Resolve them only when one shows up so tests
|
|
374
|
+
* that pass plain style objects do not load react-native.
|
|
375
|
+
*/
|
|
376
|
+
function flattenRegisteredStyle(styleId: number): { fontFamily?: unknown } | null {
|
|
377
|
+
try {
|
|
378
|
+
// Lazy require so unit tests that pass plain style objects do not load RN.
|
|
379
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
380
|
+
const { StyleSheet } = require('react-native') as {
|
|
381
|
+
StyleSheet?: { flatten?: (style: number) => { fontFamily?: unknown } | null | undefined };
|
|
382
|
+
};
|
|
383
|
+
const flat = StyleSheet?.flatten?.(styleId);
|
|
384
|
+
return flat ?? null;
|
|
385
|
+
} catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function unwrapFontName(value: string): string {
|
|
391
|
+
const trimmed = value.trim();
|
|
392
|
+
if (trimmed.length >= 2) {
|
|
393
|
+
const first = trimmed[0];
|
|
394
|
+
const last = trimmed[trimmed.length - 1];
|
|
395
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
396
|
+
return trimmed.slice(1, -1).trim();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return trimmed;
|
|
400
|
+
}
|
|
@@ -64,6 +64,11 @@ function isScreenFiber(fiber: AnyFiber): boolean {
|
|
|
64
64
|
return type !== undefined && SCREEN_TYPES.has(type);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Host screen the walk can key a cache on (`RNSScreen`, `RNSModalScreen`). */
|
|
68
|
+
export function isScreenHostFiber(fiber: AnyFiber): boolean {
|
|
69
|
+
return isScreenFiber(fiber);
|
|
70
|
+
}
|
|
71
|
+
|
|
67
72
|
function isStackContainer(fiber: AnyFiber): boolean {
|
|
68
73
|
const type = getHostTypeName(fiber);
|
|
69
74
|
return type !== undefined && STACK_CONTAINER_TYPES.has(type);
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '../surfaces/resolveBannerTheme';
|
|
15
15
|
|
|
16
16
|
import { resolveBannerButtonChrome } from './resolveBannerButtonChrome';
|
|
17
|
+
import { resolveContentFontFamily } from './resolveContentFontFamily';
|
|
17
18
|
import { stepButtonContainerStyle } from './resolveStepButtonChrome';
|
|
18
19
|
|
|
19
20
|
export interface BannerStepButtonProps {
|
|
@@ -57,7 +58,7 @@ export function BannerStepButton({
|
|
|
57
58
|
styles.label,
|
|
58
59
|
{
|
|
59
60
|
color: chrome.fontColor,
|
|
60
|
-
fontFamily: theme
|
|
61
|
+
fontFamily: resolveContentFontFamily(theme),
|
|
61
62
|
fontSize: chrome.fontSize,
|
|
62
63
|
fontWeight: chrome.fontWeight,
|
|
63
64
|
...labelMetrics(chrome.lineHeight),
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from './deserialize';
|
|
20
20
|
import { resolveImageFrame } from './imageLayout';
|
|
21
21
|
import { migrateFlowBlocksIntoContent } from './migrateFlowBlocks';
|
|
22
|
+
import { resolveContentFontFamily } from './resolveContentFontFamily';
|
|
22
23
|
import {
|
|
23
24
|
ROOT_CONTENT_GAP,
|
|
24
25
|
WEB_CODE_BG_ALPHA,
|
|
@@ -563,23 +564,27 @@ function RenderedButton({
|
|
|
563
564
|
}
|
|
564
565
|
|
|
565
566
|
function baseText(theme: ThemeFragment): TextStyle {
|
|
567
|
+
const fontFamily = resolveContentFontFamily(theme);
|
|
566
568
|
return {
|
|
567
569
|
color: theme.fontColor,
|
|
568
|
-
fontFamily: theme.fontFamily || undefined,
|
|
569
570
|
fontSize: theme.fontSize,
|
|
571
|
+
// Omit the key when unset. An explicit `undefined` is flattened to null,
|
|
572
|
+
// and RN treats that as "use the default face" rather than keeping a
|
|
573
|
+
// family set earlier on the same run.
|
|
574
|
+
...(fontFamily != null ? { fontFamily } : {}),
|
|
570
575
|
};
|
|
571
576
|
}
|
|
572
577
|
|
|
573
578
|
function markStyle(node: AbstractRichContentTextFormatting): TextStyle {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
579
|
+
const style: TextStyle = {};
|
|
580
|
+
if (node.bold) style.fontWeight = '700';
|
|
581
|
+
if (node.italic) style.fontStyle = 'italic';
|
|
582
|
+
const decoration = textDecoration(node);
|
|
583
|
+
if (decoration) style.textDecorationLine = decoration;
|
|
584
|
+
if (node.small) style.fontSize = 12;
|
|
585
|
+
else if (node.sup) style.fontSize = 10;
|
|
586
|
+
if (node.color) style.color = node.color;
|
|
587
|
+
return style;
|
|
583
588
|
}
|
|
584
589
|
|
|
585
590
|
/** View wrapper so inline code padding/radius work reliably on Android. */
|
|
@@ -9,6 +9,7 @@ import type { TextStyle } from 'react-native';
|
|
|
9
9
|
import { Platform, Pressable, StyleSheet, Text } from 'react-native';
|
|
10
10
|
|
|
11
11
|
import { isPrimaryButtonAppearance } from './buttonAppearance';
|
|
12
|
+
import { resolveContentFontFamily } from './resolveContentFontFamily';
|
|
12
13
|
import { resolveStepButtonChrome, stepButtonContainerStyle } from './resolveStepButtonChrome';
|
|
13
14
|
|
|
14
15
|
export interface StepButtonProps {
|
|
@@ -45,7 +46,7 @@ export function StepButton({
|
|
|
45
46
|
styles.label,
|
|
46
47
|
{
|
|
47
48
|
color: chrome.fontColor,
|
|
48
|
-
fontFamily: theme
|
|
49
|
+
fontFamily: resolveContentFontFamily(theme),
|
|
49
50
|
fontSize: chrome.fontSize,
|
|
50
51
|
fontWeight: chrome.fontWeight,
|
|
51
52
|
...labelMetrics(chrome.lineHeight),
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font family for SDK content text.
|
|
3
|
+
*
|
|
4
|
+
* A single concrete theme family is kept. A comma-separated custom list keeps
|
|
5
|
+
* the first family that is not part of the default system stack. That stack,
|
|
6
|
+
* generics, and empty values use the detected app font, or omit `fontFamily`
|
|
7
|
+
* so the platform face renders with no warning.
|
|
8
|
+
*/
|
|
9
|
+
import type { ThemeFragment } from '@userflow-tech/protocol';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
concreteFontFamily,
|
|
13
|
+
detectAppFontFamily,
|
|
14
|
+
isBareCssFamily,
|
|
15
|
+
isIconFontFamily,
|
|
16
|
+
splitCssFamilies,
|
|
17
|
+
themeConcreteFontFamily,
|
|
18
|
+
} from '../anchors/fiber/detectAppFontFamily';
|
|
19
|
+
|
|
20
|
+
export type AppFontDetector = () => string | null;
|
|
21
|
+
|
|
22
|
+
const CSS_FONT_FALLBACK = 'system-ui, sans-serif';
|
|
23
|
+
|
|
24
|
+
export function resolveContentFontFamily(
|
|
25
|
+
theme: ThemeFragment,
|
|
26
|
+
detect: AppFontDetector = detectAppFontFamily,
|
|
27
|
+
): string | undefined {
|
|
28
|
+
return explicitThemeFamily(theme) ?? detectedFamily(detect);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* CSS `font-family` for banner WebViews. Names are quoted so a multi-word
|
|
33
|
+
* family stays one face. A substituted app face keeps a platform fallback for
|
|
34
|
+
* when the WebView cannot resolve it.
|
|
35
|
+
*/
|
|
36
|
+
export function contentFontFamilyCss(
|
|
37
|
+
theme: ThemeFragment,
|
|
38
|
+
detect: AppFontDetector = detectAppFontFamily,
|
|
39
|
+
): string {
|
|
40
|
+
const authored = explicitThemeCss(theme);
|
|
41
|
+
if (authored) return authored;
|
|
42
|
+
const family = detectedFamily(detect);
|
|
43
|
+
if (!family) return CSS_FONT_FALLBACK;
|
|
44
|
+
return `${quoteCssFamily(family)}, ${CSS_FONT_FALLBACK}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `@font-face` rule that publishes the native family name inside the WebView.
|
|
49
|
+
*
|
|
50
|
+
* One regular face only. A weight range would make that static file match
|
|
51
|
+
* bold, and an italic rule with the same upright source would match italic,
|
|
52
|
+
* so the WebView would not synthesize either. `local()` is the family native
|
|
53
|
+
* text already uses. A `file://` source is not included: iOS treats that URL
|
|
54
|
+
* as a navigation and replaces the banner page.
|
|
55
|
+
*/
|
|
56
|
+
export function contentFontFaceCss(
|
|
57
|
+
theme: ThemeFragment,
|
|
58
|
+
detect: AppFontDetector = detectAppFontFamily,
|
|
59
|
+
): string {
|
|
60
|
+
const family = resolvedFamily(theme, detect);
|
|
61
|
+
if (!family) return '';
|
|
62
|
+
const quoted = quoteCssFamily(family);
|
|
63
|
+
return `@font-face{font-family:${quoted};src:local(${quoted});font-weight:400;font-style:normal;font-display:swap;}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Re-applies the banner face after theme custom.css. Equal-specificity rules
|
|
68
|
+
* in that stylesheet would otherwise replace it with a web font the WebView
|
|
69
|
+
* has not loaded. Code stays mono, including `<strong>`, `<em>`, and `<u>`
|
|
70
|
+
* wrapped inside `<code>`.
|
|
71
|
+
*/
|
|
72
|
+
export function bannerContentFontLockCss(
|
|
73
|
+
theme: ThemeFragment,
|
|
74
|
+
detect: AppFontDetector = detectAppFontFamily,
|
|
75
|
+
): string {
|
|
76
|
+
const family = contentFontFamilyCss(theme, detect);
|
|
77
|
+
return (
|
|
78
|
+
`.userflowjs-banner-root,.userflowjs-banner-root *:not(code):not(pre){font-family:${family};}` +
|
|
79
|
+
`.userflowjs-banner-root code,.userflowjs-banner-root pre,` +
|
|
80
|
+
`.userflowjs-banner-root code *,.userflowjs-banner-root pre *{font-family:ui-monospace,Menlo,monospace;}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolvedFamily(theme: ThemeFragment, detect: AppFontDetector): string | undefined {
|
|
85
|
+
return explicitThemeFamily(theme) ?? detectedFamily(detect);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function explicitThemeFamily(theme: ThemeFragment): string | undefined {
|
|
89
|
+
return themeConcreteFontFamily(theme.fontFamily);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Authored theme list, quoted so a multi-word family stays one face. */
|
|
93
|
+
function explicitThemeCss(theme: ThemeFragment): string | undefined {
|
|
94
|
+
const native = explicitThemeFamily(theme);
|
|
95
|
+
if (!native) return undefined;
|
|
96
|
+
const parts = splitCssFamilies(theme.fontFamily);
|
|
97
|
+
if (parts.length <= 1) return quoteCssFamily(native);
|
|
98
|
+
return parts.map((part) => (isBareCssFamily(part) ? part : quoteCssFamily(part))).join(', ');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function detectedFamily(detect: AppFontDetector): string | undefined {
|
|
102
|
+
try {
|
|
103
|
+
const detected = detect();
|
|
104
|
+
if (typeof detected !== 'string') return undefined;
|
|
105
|
+
const concrete = concreteFontFamily(detected);
|
|
106
|
+
if (!concrete || isIconFontFamily(concrete)) return undefined;
|
|
107
|
+
return concrete;
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function quoteCssFamily(name: string): string {
|
|
114
|
+
return `"${escapeCssFamily(name)}"`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function escapeCssFamily(name: string): string {
|
|
118
|
+
return name.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
119
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
|
|
23
23
|
import './anchors/autoAnchor';
|
|
24
24
|
import { anchorChangeTicker } from './anchors/anchorChangeTicker';
|
|
25
|
+
import { resetDetectedAppFont } from './anchors/fiber/detectAppFontFamily';
|
|
25
26
|
import { notifyAnchorScreenChange } from './anchors/resolveAnchorHost';
|
|
26
27
|
import {
|
|
27
28
|
setCustomScrollIntoView as setCustomScrollIntoViewConfig,
|
|
@@ -493,6 +494,7 @@ export const Userflow = {
|
|
|
493
494
|
},
|
|
494
495
|
/** Clears the current user identity and disconnects from Userflow. */
|
|
495
496
|
async reset(): Promise<void> {
|
|
497
|
+
resetDetectedAppFont();
|
|
496
498
|
resetClientConditionEngine();
|
|
497
499
|
resetFlowSessionEngine();
|
|
498
500
|
resetBannerSessionEngine();
|
|
@@ -49,15 +49,15 @@ export function BannerHost({
|
|
|
49
49
|
if (edgeFilter && edge !== edgeFilter) return null;
|
|
50
50
|
|
|
51
51
|
const floats = banner.overlay !== false;
|
|
52
|
-
const usesWebView = bannerNeedsWebView(banner
|
|
53
|
-
// Custom
|
|
52
|
+
const usesWebView = bannerNeedsWebView(banner);
|
|
53
|
+
// Custom banner WebViews paint in the overlay host (same as userflow.js). A second
|
|
54
54
|
// WKWebView in the flow slot often fails to load after the first banner dismisses.
|
|
55
55
|
const hostAsOverlay = floats || usesWebView;
|
|
56
56
|
if ((placement === 'overlay') !== hostAsOverlay) return null;
|
|
57
57
|
|
|
58
58
|
const pushLayoutEdge = placement === 'flow' && !hostAsOverlay ? edge : null;
|
|
59
59
|
// Builder overlay:true floats below the status bar. Push-layout WebViews in the overlay
|
|
60
|
-
// host still paint safe-area chrome (overlay:false + custom
|
|
60
|
+
// host still paint safe-area chrome (overlay:false + custom banner).
|
|
61
61
|
const overlaySafeEdge = hostAsOverlay && !floats ? edge : null;
|
|
62
62
|
|
|
63
63
|
const surface = (
|