@momo-kits/foundation 0.169.4 → 0.169.5-beta.1

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.
@@ -18,6 +18,7 @@ import Animated, {
18
18
  } from 'react-native-reanimated';
19
19
  import { scheduleOnRN } from 'react-native-worklets';
20
20
  import { DUO_ORIENTATIONS, styles } from './BottomSheet.styles';
21
+ import { AdoptionProbe } from './KitAdoption';
21
22
  import {
22
23
  useBottomSheetLayout,
23
24
  useDuoKeyboardLift,
@@ -173,11 +174,16 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
173
174
  );
174
175
 
175
176
  const screen = (
176
- <Screen
177
- {...props}
178
- {...props.route.params}
179
- onRequestClose={onRequestClose}
180
- />
177
+ <AdoptionProbe
178
+ screenName={Screen?.name || 'Invalid'}
179
+ navigation={props.navigation}
180
+ >
181
+ <Screen
182
+ {...props}
183
+ {...props.route.params}
184
+ onRequestClose={onRequestClose}
185
+ />
186
+ </AdoptionProbe>
181
187
  );
182
188
 
183
189
  return (
@@ -0,0 +1,119 @@
1
+ import React, { useState, useSyncExternalStore } from 'react';
2
+ import { Share, TouchableOpacity, View } from 'react-native';
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
+
5
+ import { Colors, Spacing } from '../../Consts';
6
+ import { Text } from '../../Text';
7
+ import { adoptionStore } from './AdoptionStore';
8
+ import { styles } from './styles';
9
+ import type { ScreenSnapshot } from './types';
10
+
11
+ /** Subscribes a React listener to the adoption store. Stable identity for useSyncExternalStore. */
12
+ const subscribeToStore = (listener: () => void): (() => void) =>
13
+ adoptionStore.subscribe(listener);
14
+
15
+ const getLatestSnapshot = (): ScreenSnapshot | undefined =>
16
+ adoptionStore.getLatest();
17
+
18
+ const getScreenCount = (): number => adoptionStore.getAll().length;
19
+
20
+ /** Share of classified nodes that are app-authored kit: K / (K + R + O). */
21
+ const getAdoptionRate = (snapshot: ScreenSnapshot): number => {
22
+ const { K, R, O } = snapshot.counts;
23
+ const total = K + R + O;
24
+ return total === 0 ? 0 : K / total;
25
+ };
26
+
27
+ const formatSummary = (snapshot: ScreenSnapshot | undefined): string => {
28
+ if (!snapshot) return 'Kit adoption: chưa có dữ liệu';
29
+ const { K, R, O } = snapshot.counts;
30
+ const percent = Math.round(getAdoptionRate(snapshot) * 100);
31
+ return `Kit ${percent}% · K${K} R${R} O${O}`;
32
+ };
33
+
34
+ const handleRemeasure = (): void => {
35
+ adoptionStore.remeasureActive();
36
+ };
37
+
38
+ const handleShare = (): void => {
39
+ Share.share({ message: JSON.stringify(adoptionStore.getAll()) }).catch(
40
+ () => undefined,
41
+ );
42
+ };
43
+
44
+ const handleClear = (): void => {
45
+ adoptionStore.clear();
46
+ };
47
+
48
+ /**
49
+ * Dev-only floating badge showing the latest screen's kit adoption. Tap to
50
+ * expand actions (re-measure, share JSON, clear). Renders nothing in release builds.
51
+ */
52
+ const AdoptionBadge: React.FC = () => {
53
+ const insets = useSafeAreaInsets();
54
+ const [isExpanded, setExpanded] = useState(false);
55
+ const latest = useSyncExternalStore(subscribeToStore, getLatestSnapshot);
56
+ const screenCount = useSyncExternalStore(subscribeToStore, getScreenCount);
57
+
58
+ if (!__DEV__) return null;
59
+
60
+ const handleToggle = (): void => setExpanded(prev => !prev);
61
+
62
+ return (
63
+ <View
64
+ pointerEvents="box-none"
65
+ style={[styles.wrapper, { top: insets.top + Spacing.S }]}
66
+ >
67
+ <TouchableOpacity
68
+ accessibilityRole="button"
69
+ onPress={handleToggle}
70
+ style={styles.badge}
71
+ >
72
+ <Text typography="label_xs_medium" color={Colors.black_01}>
73
+ {formatSummary(latest)}
74
+ </Text>
75
+ {latest ? (
76
+ <Text typography="description_xs_regular" color={Colors.black_01}>
77
+ {latest.screen_name}
78
+ </Text>
79
+ ) : null}
80
+ </TouchableOpacity>
81
+ {isExpanded ? (
82
+ <View style={styles.panel}>
83
+ <Text typography="label_xs_medium" color={Colors.black_17}>
84
+ {`Đã đo ${screenCount} màn hình`}
85
+ </Text>
86
+ <TouchableOpacity
87
+ accessibilityRole="button"
88
+ onPress={handleRemeasure}
89
+ style={styles.action}
90
+ >
91
+ <Text typography="label_xs_medium" color={Colors.blue_03}>
92
+ Đo lại
93
+ </Text>
94
+ </TouchableOpacity>
95
+ <TouchableOpacity
96
+ accessibilityRole="button"
97
+ onPress={handleShare}
98
+ style={styles.action}
99
+ >
100
+ <Text typography="label_xs_medium" color={Colors.blue_03}>
101
+ Share JSON
102
+ </Text>
103
+ </TouchableOpacity>
104
+ <TouchableOpacity
105
+ accessibilityRole="button"
106
+ onPress={handleClear}
107
+ style={styles.actionOutline}
108
+ >
109
+ <Text typography="label_xs_medium" color={Colors.black_17}>
110
+ Xoá
111
+ </Text>
112
+ </TouchableOpacity>
113
+ </View>
114
+ ) : null}
115
+ </View>
116
+ );
117
+ };
118
+
119
+ export { AdoptionBadge };
@@ -0,0 +1,145 @@
1
+ import React from 'react';
2
+ import { InteractionManager, Platform } from 'react-native';
3
+
4
+ import { MiniAppContext } from '../../Context';
5
+ import { adoptionStore } from './AdoptionStore';
6
+ import { SETTLE_MS } from './constants';
7
+ import type {
8
+ FiberLike,
9
+ KitRegistries,
10
+ ScreenSnapshot,
11
+ WalkResult,
12
+ } from './types';
13
+ import {
14
+ createRegistries,
15
+ getAppId,
16
+ getCurrentFiber,
17
+ walkScreen,
18
+ } from './utils';
19
+
20
+ type NavigationLike = {
21
+ addListener?: (event: string, callback: () => void) => () => void;
22
+ };
23
+
24
+ type ProbeProps = {
25
+ screenName: string;
26
+ navigation?: NavigationLike;
27
+ children: React.ReactNode;
28
+ };
29
+
30
+ let registriesCache: KitRegistries | undefined;
31
+
32
+ /**
33
+ * Kit and RN identity sets, built on first use. Lazy require breaks the cycle
34
+ * between the foundation index and the Application screens that render this probe.
35
+ */
36
+ const getRegistries = (): KitRegistries => {
37
+ if (!registriesCache) {
38
+ const kitModule = require('../../index') as Record<string, unknown>;
39
+ const rnModule = require('react-native') as Record<string, unknown>;
40
+ registriesCache = createRegistries([kitModule], rnModule);
41
+ }
42
+ return registriesCache;
43
+ };
44
+
45
+ /** Foundation version from its package.json, or undefined if it cannot be read. */
46
+ const readFoundationVersion = (): string | undefined => {
47
+ try {
48
+ const pkg = require('../../package.json') as { version?: string };
49
+ return pkg.version;
50
+ } catch {
51
+ return undefined;
52
+ }
53
+ };
54
+
55
+ const createErrorResult = (): WalkResult => ({
56
+ counts: { K: 0, R: 0, O: 0 },
57
+ kit: {},
58
+ rn: {},
59
+ other: {},
60
+ top_transparent: [],
61
+ fibers: 0,
62
+ truncated: false,
63
+ status: 'error',
64
+ });
65
+
66
+ /** Stops the walk at nested probes so each screen is measured on its own. */
67
+ const isProbeFiber = (fiber: FiberLike): boolean =>
68
+ fiber.type === AdoptionProbeDev;
69
+
70
+ /** Passes children through with no measurement. Used in release builds. */
71
+ const PassThrough = (props: ProbeProps): React.JSX.Element => (
72
+ <>{props.children}</>
73
+ );
74
+
75
+ /**
76
+ * Dev-only wrapper around a screen. Measures the screen's fiber tree after
77
+ * the first interactions settle, and again on focus. Release builds use PassThrough.
78
+ */
79
+ class AdoptionProbeDev extends React.Component<ProbeProps> {
80
+ static contextType = MiniAppContext;
81
+
82
+ private timer: ReturnType<typeof setTimeout> | undefined;
83
+ private interaction: { cancel: () => void } | undefined;
84
+ private unsubscribeFocus: (() => void) | undefined;
85
+
86
+ componentDidMount(): void {
87
+ adoptionStore.setActive(this.measure);
88
+ this.unsubscribeFocus = this.props.navigation?.addListener?.(
89
+ 'focus',
90
+ this.handleFocus,
91
+ );
92
+ this.interaction = InteractionManager.runAfterInteractions(() => {
93
+ this.timer = setTimeout(this.measure, SETTLE_MS);
94
+ });
95
+ }
96
+
97
+ componentWillUnmount(): void {
98
+ if (this.timer !== undefined) clearTimeout(this.timer);
99
+ this.interaction?.cancel();
100
+ this.unsubscribeFocus?.();
101
+ adoptionStore.clearActive(this.measure);
102
+ }
103
+
104
+ render(): React.ReactNode {
105
+ return this.props.children;
106
+ }
107
+
108
+ private handleFocus = (): void => {
109
+ adoptionStore.setActive(this.measure);
110
+ };
111
+
112
+ private measure = (): void => {
113
+ const startedAt = Date.now();
114
+ let result: WalkResult;
115
+ try {
116
+ const self = getCurrentFiber(
117
+ (this as unknown as { _reactInternals: FiberLike })._reactInternals,
118
+ );
119
+ const root = self.child;
120
+ if (!root) return;
121
+ result = walkScreen(root, getRegistries(), isProbeFiber);
122
+ } catch {
123
+ result = createErrorResult();
124
+ }
125
+ this.save(result, startedAt);
126
+ };
127
+
128
+ private save(result: WalkResult, startedAt: number): void {
129
+ const snapshot: ScreenSnapshot = {
130
+ ...result,
131
+ app_id: getAppId(this.context),
132
+ screen_name: this.props.screenName,
133
+ foundation_version: readFoundationVersion(),
134
+ platform: Platform.OS,
135
+ ts: Date.now(),
136
+ walk_ms: Date.now() - startedAt,
137
+ };
138
+ adoptionStore.save(snapshot);
139
+ }
140
+ }
141
+
142
+ const AdoptionProbe = __DEV__ ? AdoptionProbeDev : PassThrough;
143
+
144
+ export type { NavigationLike, ProbeProps };
145
+ export { AdoptionProbe, AdoptionProbeDev };
@@ -0,0 +1,80 @@
1
+ import type { ScreenSnapshot } from './types';
2
+
3
+ type Listener = () => void;
4
+ type Measure = () => void;
5
+
6
+ /** Total classified nodes in a snapshot, used to keep the richer measurement. */
7
+ const getTotal = (snapshot: ScreenSnapshot): number =>
8
+ snapshot.counts.K + snapshot.counts.R + snapshot.counts.O;
9
+
10
+ const getKey = (snapshot: ScreenSnapshot): string =>
11
+ `${snapshot.app_id}/${snapshot.screen_name}`;
12
+
13
+ /**
14
+ * In-memory, dev-only store of per-screen snapshots. Keeps one snapshot per
15
+ * app and screen (the one with the most nodes) and holds the active measurer.
16
+ */
17
+ class AdoptionStore {
18
+ private snapshots = new Map<string, ScreenSnapshot>();
19
+ private listeners = new Set<Listener>();
20
+ private latestKey: string | undefined;
21
+ private activeMeasure: Measure | undefined;
22
+
23
+ /** Stores a snapshot unless the stored one for the same screen is larger. */
24
+ save(snapshot: ScreenSnapshot): void {
25
+ const key = getKey(snapshot);
26
+ const existing = this.snapshots.get(key);
27
+ if (!existing || getTotal(existing) <= getTotal(snapshot)) {
28
+ this.snapshots.set(key, snapshot);
29
+ }
30
+ this.latestKey = key;
31
+ this.notify();
32
+ }
33
+
34
+ getAll(): ScreenSnapshot[] {
35
+ return Array.from(this.snapshots.values());
36
+ }
37
+
38
+ /** The screen saved most recently, or undefined when nothing is saved. */
39
+ getLatest(): ScreenSnapshot | undefined {
40
+ return this.latestKey === undefined
41
+ ? undefined
42
+ : this.snapshots.get(this.latestKey);
43
+ }
44
+
45
+ clear(): void {
46
+ this.snapshots.clear();
47
+ this.latestKey = undefined;
48
+ this.notify();
49
+ }
50
+
51
+ /** Registers a change listener and returns its unsubscribe function. */
52
+ subscribe(listener: Listener): () => void {
53
+ this.listeners.add(listener);
54
+ return () => {
55
+ this.listeners.delete(listener);
56
+ };
57
+ }
58
+
59
+ setActive(measure: Measure): void {
60
+ this.activeMeasure = measure;
61
+ }
62
+
63
+ /** Clears the active measurer only if it is still the given one. */
64
+ clearActive(measure: Measure): void {
65
+ if (this.activeMeasure === measure) this.activeMeasure = undefined;
66
+ }
67
+
68
+ remeasureActive(): void {
69
+ this.activeMeasure?.();
70
+ }
71
+
72
+ private notify(): void {
73
+ this.listeners.forEach(listener => listener());
74
+ }
75
+ }
76
+
77
+ const adoptionStore = new AdoptionStore();
78
+
79
+ export type { Listener, Measure };
80
+ export { AdoptionStore, adoptionStore };
@@ -0,0 +1,137 @@
1
+ import {
2
+ FIBER_TAG,
3
+ LOCAL_SOURCE_KEY,
4
+ REACT_FACTORY_FRAME_NAMES,
5
+ REACT_STACK_BOTTOM_FRAME,
6
+ } from '../constants';
7
+ import type { FiberLike } from '../types';
8
+
9
+ const HTTP_ORIGIN_PATTERN = /^(https?:\/\/[^/]+)/i;
10
+ const FILE_URL_PATTERN = /^file:\/\//i;
11
+ const LINE_COL_SUFFIX_PATTERN = /:\d+(?::\d+)?$/;
12
+ const PAREN_LOCATION_PATTERN = /\((?:address at )?(.+)\)\s*$/;
13
+ const BARE_LOCATION_PATTERN = /^\s*at\s+(.+?)\s*$/;
14
+ const FRAME_NAME_PATTERN = /^\s*at\s+([^\s(]+)/;
15
+ const NATIVE_FRAME_LOCATION = 'native';
16
+
17
+ const creatorSourceCache = new WeakMap<object, string | null>();
18
+ let hostSource: string | undefined;
19
+
20
+ /**
21
+ * Bundle identity of a frame location: the origin for http(s), the directory
22
+ * for file URLs, and one shared key for bare source paths (jest).
23
+ */
24
+ const getSourceKey = (location: string): string => {
25
+ const origin = HTTP_ORIGIN_PATTERN.exec(location);
26
+ if (origin) return origin[1];
27
+ if (!FILE_URL_PATTERN.test(location)) return LOCAL_SOURCE_KEY;
28
+ const path = location.replace(LINE_COL_SUFFIX_PATTERN, '');
29
+ return path.slice(0, path.lastIndexOf('/'));
30
+ };
31
+
32
+ /** Location (`url:line:col`) of a stack frame line, or undefined when it has none. */
33
+ const parseFrameLocation = (line: string): string | undefined => {
34
+ const match =
35
+ PAREN_LOCATION_PATTERN.exec(line) ?? BARE_LOCATION_PATTERN.exec(line);
36
+ return match?.[1];
37
+ };
38
+
39
+ /** Reads `.stack` of a debug stack value without throwing; undefined if it is not a string. */
40
+ const readStack = (debugStack: unknown): string | undefined => {
41
+ try {
42
+ const stack = (debugStack as { stack?: unknown } | null)?.stack;
43
+ return typeof stack === 'string' ? stack : undefined;
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ };
48
+
49
+ /** Frame lines below the header, cut at React's bottom-frame marker. */
50
+ const getFrameLines = (stack: string): string[] => {
51
+ const lines = stack.split('\n').slice(1);
52
+ const bottom = lines.findIndex(line =>
53
+ line.includes(REACT_STACK_BOTTOM_FRAME),
54
+ );
55
+ return bottom === -1 ? lines : lines.slice(0, bottom);
56
+ };
57
+
58
+ /**
59
+ * True for frames that cannot be the element's creator: React's own factory
60
+ * wrappers and native frames (e.g. `apply (native)`), which carry no bundle.
61
+ */
62
+ const isSkippedFrame = (line: string): boolean => {
63
+ const name = FRAME_NAME_PATTERN.exec(line)?.[1] ?? '';
64
+ const isNative = parseFrameLocation(line) === NATIVE_FRAME_LOCATION;
65
+ return isNative || REACT_FACTORY_FRAME_NAMES.includes(name);
66
+ };
67
+
68
+ /** Source key of the frame that created the element, skipping React's factory frames. */
69
+ const findCreatorSource = (stack: string): string | undefined => {
70
+ const lines = getFrameLines(stack).slice(1);
71
+ const index = lines.findIndex(line => !isSkippedFrame(line));
72
+ const location = index === -1 ? undefined : parseFrameLocation(lines[index]);
73
+ return location === undefined ? undefined : getSourceKey(location);
74
+ };
75
+
76
+ /**
77
+ * Source key of the code that created an element, read from its debug stack.
78
+ * Cached per stack object since fibers and alternates share it.
79
+ */
80
+ const getCreatorSource = (debugStack: unknown): string | undefined => {
81
+ const isCacheable = typeof debugStack === 'object' && debugStack !== null;
82
+ if (isCacheable && creatorSourceCache.has(debugStack)) {
83
+ return creatorSourceCache.get(debugStack) ?? undefined;
84
+ }
85
+ const stack = readStack(debugStack);
86
+ const source = stack === undefined ? undefined : findCreatorSource(stack);
87
+ if (isCacheable) creatorSourceCache.set(debugStack, source ?? null);
88
+ return source;
89
+ };
90
+
91
+ /** Source key of the bundle running this module (the host), cached after the first read. */
92
+ const getHostSource = (): string => {
93
+ if (hostSource !== undefined) return hostSource;
94
+ const line = (new Error().stack ?? '').split('\n')[1] ?? '';
95
+ const location = parseFrameLocation(line);
96
+ hostSource =
97
+ location === undefined ? LOCAL_SOURCE_KEY : getSourceKey(location);
98
+ return hostSource;
99
+ };
100
+
101
+ /**
102
+ * The fiber whose stack decides authorship. React creates the inner fiber of a
103
+ * MemoComponent itself, with no stack, so it inherits its memo parent's.
104
+ */
105
+ const getAuthorFiber = (fiber: FiberLike): FiberLike =>
106
+ fiber.return?.tag === FIBER_TAG.MemoComponent && fiber.return.child === fiber
107
+ ? fiber.return
108
+ : fiber;
109
+
110
+ /**
111
+ * True when the fiber has no dev stack (prod jsx) or its creator frame lives
112
+ * in a bundle other than the host's. Unparseable stacks count as host.
113
+ */
114
+ const isAppAuthored = (fiber: FiberLike): boolean => {
115
+ if (!fiber._debugStack) return true;
116
+ const source = getCreatorSource(fiber._debugStack);
117
+ return source !== undefined && source !== getHostSource();
118
+ };
119
+
120
+ /**
121
+ * True when the fiber's first child was created by host code and is owned by
122
+ * this fiber, which marks a host-internal wrapper.
123
+ */
124
+ const isHostDefined = (fiber: FiberLike): boolean => {
125
+ const child = fiber.child;
126
+ return !!child && !isAppAuthored(child) && child._debugOwner === fiber;
127
+ };
128
+
129
+ export {
130
+ getAuthorFiber,
131
+ getCreatorSource,
132
+ getHostSource,
133
+ getSourceKey,
134
+ isAppAuthored,
135
+ isHostDefined,
136
+ parseFrameLocation,
137
+ };
@@ -0,0 +1,118 @@
1
+ const SETTLE_MS = 3000;
2
+ const MAX_FIBERS = 20000;
3
+ const SLOT_SCAN_DEPTH = 4;
4
+ const TOP_TRANSPARENT_LIMIT = 10;
5
+
6
+ // Generic lib names (Badge, Title, Logo, ...) are left out on purpose so app components with the same name are not counted as kit.
7
+ const LIB_UNIT_NAMES: string[] = [
8
+ 'AutoComplete',
9
+ 'Avatar',
10
+ 'Calendar',
11
+ 'Carousel',
12
+ 'Chip',
13
+ 'Collapse',
14
+ 'DateTimePicker',
15
+ 'ProgressInfo',
16
+ 'Rating',
17
+ 'Slider',
18
+ 'Stepper',
19
+ 'Steps',
20
+ 'SuggestAction',
21
+ 'Swipe',
22
+ 'TabView',
23
+ 'TabBar',
24
+ 'CardTabBar',
25
+ 'ScrollableTabBar',
26
+ 'HeaderSliderBanner',
27
+ 'TrustBanner',
28
+ 'Uploader',
29
+ ];
30
+
31
+ const RN_PRIMITIVE_NAMES: string[] = [
32
+ 'View',
33
+ 'Text',
34
+ 'Image',
35
+ 'ImageBackground',
36
+ 'TextInput',
37
+ 'ScrollView',
38
+ 'FlatList',
39
+ 'SectionList',
40
+ 'VirtualizedList',
41
+ 'TouchableOpacity',
42
+ 'TouchableHighlight',
43
+ 'TouchableWithoutFeedback',
44
+ 'Pressable',
45
+ 'Switch',
46
+ 'ActivityIndicator',
47
+ 'Modal',
48
+ 'KeyboardAvoidingView',
49
+ 'RefreshControl',
50
+ ];
51
+
52
+ /** Foundation exports that come from third-party or RN and must not count as kit. */
53
+ const FOUNDATION_REEXPORT_NAMES: string[] = [
54
+ 'TouchableOpacity',
55
+ 'LinearGradient',
56
+ 'SafeAreaView',
57
+ 'SafeAreaProvider',
58
+ 'SafeAreaConsumer',
59
+ 'SafeAreaInsetsContext',
60
+ 'SafeAreaFrameContext',
61
+ 'withSafeAreaInsets',
62
+ ];
63
+
64
+ const FIBER_TAG = {
65
+ FunctionComponent: 0,
66
+ ClassComponent: 1,
67
+ HostRoot: 3,
68
+ HostComponent: 5,
69
+ HostText: 6,
70
+ Fragment: 7,
71
+ ContextProvider: 10,
72
+ ForwardRef: 11,
73
+ MemoComponent: 14,
74
+ SimpleMemoComponent: 15,
75
+ } as const;
76
+
77
+ /** Fiber tags that are component fibers; every other tag is walked through without counting. */
78
+ const COUNTED_FIBER_TAGS: number[] = [
79
+ FIBER_TAG.FunctionComponent,
80
+ FIBER_TAG.ClassComponent,
81
+ FIBER_TAG.HostComponent,
82
+ FIBER_TAG.ForwardRef,
83
+ FIBER_TAG.SimpleMemoComponent,
84
+ ];
85
+
86
+ const HOST_RN_PREFIXES: string[] = ['RCT', 'Android'];
87
+
88
+ const LOCAL_SOURCE_KEY = 'local';
89
+ const REACT_STACK_BOTTOM_FRAME = 'react-stack-bottom-frame';
90
+ const REACT_FACTORY_FRAME_NAMES: string[] = [
91
+ 'jsx',
92
+ 'jsxs',
93
+ 'jsxDEV',
94
+ 'jsxDEVImpl',
95
+ 'createElement',
96
+ 'jsxProd',
97
+ ];
98
+
99
+ const REACT_MEMO_TYPE = Symbol.for('react.memo');
100
+ const REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref');
101
+
102
+ export {
103
+ COUNTED_FIBER_TAGS,
104
+ FIBER_TAG,
105
+ FOUNDATION_REEXPORT_NAMES,
106
+ HOST_RN_PREFIXES,
107
+ LIB_UNIT_NAMES,
108
+ LOCAL_SOURCE_KEY,
109
+ MAX_FIBERS,
110
+ REACT_FACTORY_FRAME_NAMES,
111
+ REACT_FORWARD_REF_TYPE,
112
+ REACT_MEMO_TYPE,
113
+ REACT_STACK_BOTTOM_FRAME,
114
+ RN_PRIMITIVE_NAMES,
115
+ SETTLE_MS,
116
+ SLOT_SCAN_DEPTH,
117
+ TOP_TRANSPARENT_LIMIT,
118
+ };
@@ -0,0 +1,5 @@
1
+ import { AdoptionBadge } from './AdoptionBadge';
2
+ import { AdoptionProbe } from './AdoptionProbe';
3
+ import { adoptionStore } from './AdoptionStore';
4
+
5
+ export { AdoptionBadge, AdoptionProbe, adoptionStore };
@@ -0,0 +1,55 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ import { Colors, Radius, Spacing } from '../../Consts';
4
+
5
+ // Keeps the summary pill from stretching across a narrow screen.
6
+ const BADGE_MAX_WIDTH = 240;
7
+ // Wide enough for the three action labels without clipping.
8
+ const PANEL_MIN_WIDTH = 180;
9
+ // Floats the dev overlay above screen content.
10
+ const BADGE_Z_INDEX = 9999;
11
+
12
+ const styles = StyleSheet.create({
13
+ wrapper: {
14
+ position: 'absolute',
15
+ left: 0,
16
+ right: 0,
17
+ alignItems: 'flex-end',
18
+ paddingRight: Spacing.L,
19
+ zIndex: BADGE_Z_INDEX,
20
+ },
21
+ badge: {
22
+ maxWidth: BADGE_MAX_WIDTH,
23
+ backgroundColor: Colors.black_17,
24
+ borderRadius: Radius.S,
25
+ paddingHorizontal: Spacing.S,
26
+ paddingVertical: Spacing.XS,
27
+ },
28
+ panel: {
29
+ marginTop: Spacing.XS,
30
+ minWidth: PANEL_MIN_WIDTH,
31
+ gap: Spacing.XS,
32
+ padding: Spacing.S,
33
+ backgroundColor: Colors.black_01,
34
+ borderRadius: Radius.M,
35
+ borderWidth: StyleSheet.hairlineWidth,
36
+ borderColor: Colors.black_05,
37
+ },
38
+ action: {
39
+ alignItems: 'center',
40
+ paddingHorizontal: Spacing.S,
41
+ paddingVertical: Spacing.XS,
42
+ borderRadius: Radius.S,
43
+ backgroundColor: Colors.blue_09,
44
+ },
45
+ actionOutline: {
46
+ alignItems: 'center',
47
+ paddingHorizontal: Spacing.S,
48
+ paddingVertical: Spacing.XS,
49
+ borderRadius: Radius.S,
50
+ borderWidth: StyleSheet.hairlineWidth,
51
+ borderColor: Colors.black_07,
52
+ },
53
+ });
54
+
55
+ export { styles };