react-native-nitro-tracing 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.
Files changed (233) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/NitroTracing.podspec +32 -0
  4. package/README.md +75 -0
  5. package/android/CMakeLists.txt +49 -0
  6. package/android/build.gradle +133 -0
  7. package/android/fix-prefab.gradle +51 -0
  8. package/android/gradle.properties +5 -0
  9. package/android/proguard-rules.pro +5 -0
  10. package/android/src/main/AndroidManifest.xml +2 -0
  11. package/android/src/main/cpp/cpp-adapter.cpp +47 -0
  12. package/android/src/main/java/com/margelo/nitro/nitrotracing/HermesSamplingGuard.kt +43 -0
  13. package/android/src/main/java/com/margelo/nitro/nitrotracing/NitroTracingFrameSource.kt +36 -0
  14. package/android/src/main/java/com/margelo/nitro/nitrotracing/NitroTracingPackage.kt +31 -0
  15. package/client.ts +15 -0
  16. package/cpp/HermesSampling.cpp +26 -0
  17. package/cpp/HermesSampling.hpp +10 -0
  18. package/cpp/HybridRecording.cpp +66 -0
  19. package/cpp/HybridRecording.hpp +30 -0
  20. package/cpp/HybridTraceSpan.cpp +10 -0
  21. package/cpp/HybridTraceSpan.hpp +17 -0
  22. package/cpp/HybridTracing.cpp +10 -0
  23. package/cpp/HybridTracing.hpp +10 -0
  24. package/cpp/NativeConversions.cpp +77 -0
  25. package/cpp/NativeConversions.hpp +15 -0
  26. package/cpp/core/NativeSampler.cpp +152 -0
  27. package/cpp/core/NativeSampler.hpp +56 -0
  28. package/cpp/core/Recorder.cpp +218 -0
  29. package/cpp/core/Recorder.hpp +60 -0
  30. package/cpp/core/Retention.cpp +26 -0
  31. package/cpp/core/Retention.hpp +7 -0
  32. package/cpp/core/TraceJson.cpp +214 -0
  33. package/cpp/core/TraceJson.hpp +7 -0
  34. package/cpp/core/TraceTypes.hpp +62 -0
  35. package/expo.ts +2 -0
  36. package/ios/NitroTracingFrameSource.mm +35 -0
  37. package/lib/commonjs/client.cjs +767 -0
  38. package/lib/commonjs/client.cjs.map +7 -0
  39. package/lib/commonjs/expo.cjs +88 -0
  40. package/lib/commonjs/expo.cjs.map +7 -0
  41. package/lib/commonjs/index.cjs +28 -0
  42. package/lib/commonjs/index.cjs.map +7 -0
  43. package/lib/commonjs/performance.cjs +112 -0
  44. package/lib/commonjs/performance.cjs.map +7 -0
  45. package/lib/commonjs/plugins.cjs +477 -0
  46. package/lib/commonjs/plugins.cjs.map +7 -0
  47. package/lib/commonjs/react.cjs +3833 -0
  48. package/lib/commonjs/react.cjs.map +7 -0
  49. package/lib/commonjs/release-profiler.cjs +142 -0
  50. package/lib/commonjs/release-profiler.cjs.map +7 -0
  51. package/lib/commonjs/sentry.cjs +188 -0
  52. package/lib/commonjs/sentry.cjs.map +7 -0
  53. package/lib/module/client.mjs +768 -0
  54. package/lib/module/client.mjs.map +7 -0
  55. package/lib/module/expo.mjs +62 -0
  56. package/lib/module/expo.mjs.map +7 -0
  57. package/lib/module/index.mjs +7 -0
  58. package/lib/module/index.mjs.map +7 -0
  59. package/lib/module/performance.mjs +89 -0
  60. package/lib/module/performance.mjs.map +7 -0
  61. package/lib/module/plugins.mjs +461 -0
  62. package/lib/module/plugins.mjs.map +7 -0
  63. package/lib/module/react.mjs +3851 -0
  64. package/lib/module/react.mjs.map +7 -0
  65. package/lib/module/release-profiler.mjs +137 -0
  66. package/lib/module/release-profiler.mjs.map +7 -0
  67. package/lib/module/sentry.mjs +165 -0
  68. package/lib/module/sentry.mjs.map +7 -0
  69. package/lib/typescript/client.d.ts +4 -0
  70. package/lib/typescript/expo.d.ts +2 -0
  71. package/lib/typescript/performance.d.ts +2 -0
  72. package/lib/typescript/plugins.d.ts +12 -0
  73. package/lib/typescript/react.d.ts +5 -0
  74. package/lib/typescript/release-profiler.d.ts +2 -0
  75. package/lib/typescript/sentry.d.ts +2 -0
  76. package/lib/typescript/src/client/TracingSink.d.ts +10 -0
  77. package/lib/typescript/src/client/createTraceClient.d.ts +82 -0
  78. package/lib/typescript/src/client/playground.d.ts +12 -0
  79. package/lib/typescript/src/expo/ExpoAdapter.d.ts +15 -0
  80. package/lib/typescript/src/expo/index.d.ts +2 -0
  81. package/lib/typescript/src/index.d.ts +6 -0
  82. package/lib/typescript/src/plugins/TracePlugin.d.ts +22 -0
  83. package/lib/typescript/src/plugins/errors.d.ts +15 -0
  84. package/lib/typescript/src/plugins/nativeMetrics.d.ts +13 -0
  85. package/lib/typescript/src/plugins/navigation.d.ts +29 -0
  86. package/lib/typescript/src/plugins/network.d.ts +23 -0
  87. package/lib/typescript/src/plugins/orderSpans.d.ts +3 -0
  88. package/lib/typescript/src/plugins/performance.d.ts +13 -0
  89. package/lib/typescript/src/plugins/releaseProfiler.d.ts +25 -0
  90. package/lib/typescript/src/plugins/runtimeMetrics.d.ts +17 -0
  91. package/lib/typescript/src/plugins/sentry.d.ts +15 -0
  92. package/lib/typescript/src/plugins/startPlugins.d.ts +4 -0
  93. package/lib/typescript/src/react/DetailView.d.ts +11 -0
  94. package/lib/typescript/src/react/ExplorerFilters.d.ts +9 -0
  95. package/lib/typescript/src/react/ExplorerView.d.ts +9 -0
  96. package/lib/typescript/src/react/InspectorControls.d.ts +229 -0
  97. package/lib/typescript/src/react/InspectorHeader.d.ts +8 -0
  98. package/lib/typescript/src/react/InspectorSheet.d.ts +16 -0
  99. package/lib/typescript/src/react/MetricsView.d.ts +7 -0
  100. package/lib/typescript/src/react/SummaryView.d.ts +11 -0
  101. package/lib/typescript/src/react/TimelineView.d.ts +8 -0
  102. package/lib/typescript/src/react/TraceInspector.d.ts +11 -0
  103. package/lib/typescript/src/react/TraceOverlay.d.ts +17 -0
  104. package/lib/typescript/src/react/explorerModel.d.ts +38 -0
  105. package/lib/typescript/src/react/labels.d.ts +203 -0
  106. package/lib/typescript/src/react/native.d.ts +42 -0
  107. package/lib/typescript/src/react/readSnapshot.d.ts +4 -0
  108. package/lib/typescript/src/react/store.d.ts +9 -0
  109. package/lib/typescript/src/react/topics.d.ts +80 -0
  110. package/lib/typescript/src/react/useAppReadyMetric.d.ts +3 -0
  111. package/lib/typescript/src/react/useLiveRecording.d.ts +4 -0
  112. package/lib/typescript/src/react/useTraceViewer.d.ts +140 -0
  113. package/lib/typescript/src/react/viewerModel.d.ts +59 -0
  114. package/lib/typescript/src/specs/Recording.nitro.d.ts +31 -0
  115. package/lib/typescript/src/specs/TraceSpan.nitro.d.ts +14 -0
  116. package/lib/typescript/src/specs/Tracing.nitro.d.ts +17 -0
  117. package/lib/typescript/src/types/CompletedSpanOptions.d.ts +15 -0
  118. package/lib/typescript/src/types/EventContext.d.ts +8 -0
  119. package/lib/typescript/src/types/MarkEvent.d.ts +4 -0
  120. package/lib/typescript/src/types/MarkOptions.d.ts +6 -0
  121. package/lib/typescript/src/types/MetricEvent.d.ts +8 -0
  122. package/lib/typescript/src/types/MetricOptions.d.ts +10 -0
  123. package/lib/typescript/src/types/NativeSamplingOptions.d.ts +7 -0
  124. package/lib/typescript/src/types/ReadOptions.d.ts +7 -0
  125. package/lib/typescript/src/types/RecordingOptions.d.ts +11 -0
  126. package/lib/typescript/src/types/RecordingStats.d.ts +25 -0
  127. package/lib/typescript/src/types/SpanEvent.d.ts +13 -0
  128. package/lib/typescript/src/types/SpanOptions.d.ts +6 -0
  129. package/lib/typescript/src/types/SpanOutcome.d.ts +2 -0
  130. package/lib/typescript/src/types/TraceAttribute.d.ts +7 -0
  131. package/lib/typescript/src/types/TraceAttributes.d.ts +3 -0
  132. package/lib/typescript/src/types/TraceContext.d.ts +10 -0
  133. package/lib/typescript/src/types/TracePage.d.ts +18 -0
  134. package/lib/typescript/src/types.d.ts +17 -0
  135. package/nitro.json +20 -0
  136. package/nitrogen/generated/.gitattributes +1 -0
  137. package/nitrogen/generated/android/NitroTracing+autolinking.cmake +83 -0
  138. package/nitrogen/generated/android/NitroTracing+autolinking.gradle +27 -0
  139. package/nitrogen/generated/android/NitroTracingOnLoad.cpp +49 -0
  140. package/nitrogen/generated/android/NitroTracingOnLoad.hpp +34 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrotracing/NitroTracingOnLoad.kt +35 -0
  142. package/nitrogen/generated/ios/NitroTracing+autolinking.rb +62 -0
  143. package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Bridge.cpp +17 -0
  144. package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Bridge.hpp +27 -0
  145. package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Umbrella.hpp +38 -0
  146. package/nitrogen/generated/ios/NitroTracingAutolinking.mm +35 -0
  147. package/nitrogen/generated/ios/NitroTracingAutolinking.swift +16 -0
  148. package/nitrogen/generated/shared/c++/CompletedSpanOptions.hpp +122 -0
  149. package/nitrogen/generated/shared/c++/HybridRecordingSpec.cpp +31 -0
  150. package/nitrogen/generated/shared/c++/HybridRecordingSpec.hpp +100 -0
  151. package/nitrogen/generated/shared/c++/HybridTraceSpanSpec.cpp +23 -0
  152. package/nitrogen/generated/shared/c++/HybridTraceSpanSpec.hpp +65 -0
  153. package/nitrogen/generated/shared/c++/HybridTracingSpec.cpp +22 -0
  154. package/nitrogen/generated/shared/c++/HybridTracingSpec.hpp +68 -0
  155. package/nitrogen/generated/shared/c++/MarkEvent.hpp +102 -0
  156. package/nitrogen/generated/shared/c++/MarkOptions.hpp +99 -0
  157. package/nitrogen/generated/shared/c++/MetricEvent.hpp +110 -0
  158. package/nitrogen/generated/shared/c++/MetricOptions.hpp +107 -0
  159. package/nitrogen/generated/shared/c++/NativeSamplingOptions.hpp +87 -0
  160. package/nitrogen/generated/shared/c++/ReadOptions.hpp +87 -0
  161. package/nitrogen/generated/shared/c++/RecordingOptions.hpp +95 -0
  162. package/nitrogen/generated/shared/c++/RecordingStats.hpp +123 -0
  163. package/nitrogen/generated/shared/c++/SpanEvent.hpp +121 -0
  164. package/nitrogen/generated/shared/c++/SpanOptions.hpp +99 -0
  165. package/nitrogen/generated/shared/c++/SpanOutcome.hpp +84 -0
  166. package/nitrogen/generated/shared/c++/TraceAttribute.hpp +87 -0
  167. package/nitrogen/generated/shared/c++/TracePage.hpp +111 -0
  168. package/package.json +236 -0
  169. package/performance.ts +2 -0
  170. package/plugins.ts +24 -0
  171. package/react-native.config.js +16 -0
  172. package/react.ts +10 -0
  173. package/release-profiler.ts +6 -0
  174. package/sentry.ts +2 -0
  175. package/src/client/TracingSink.ts +25 -0
  176. package/src/client/createTraceClient.ts +439 -0
  177. package/src/client/playground.ts +143 -0
  178. package/src/expo/ExpoAdapter.ts +71 -0
  179. package/src/expo/index.ts +2 -0
  180. package/src/index.ts +8 -0
  181. package/src/plugins/TracePlugin.ts +22 -0
  182. package/src/plugins/errors.ts +69 -0
  183. package/src/plugins/nativeMetrics.ts +26 -0
  184. package/src/plugins/navigation.ts +111 -0
  185. package/src/plugins/network.ts +178 -0
  186. package/src/plugins/orderSpans.ts +22 -0
  187. package/src/plugins/performance.ts +120 -0
  188. package/src/plugins/releaseProfiler.ts +123 -0
  189. package/src/plugins/runtimeMetrics.ts +131 -0
  190. package/src/plugins/sentry.ts +176 -0
  191. package/src/plugins/startPlugins.ts +58 -0
  192. package/src/react/DetailView.tsx +514 -0
  193. package/src/react/ExplorerFilters.tsx +151 -0
  194. package/src/react/ExplorerView.tsx +243 -0
  195. package/src/react/InspectorControls.tsx +651 -0
  196. package/src/react/InspectorHeader.tsx +171 -0
  197. package/src/react/InspectorSheet.tsx +89 -0
  198. package/src/react/MetricsView.tsx +182 -0
  199. package/src/react/SummaryView.tsx +121 -0
  200. package/src/react/TimelineView.tsx +87 -0
  201. package/src/react/TraceInspector.tsx +196 -0
  202. package/src/react/TraceOverlay.tsx +585 -0
  203. package/src/react/explorerModel.ts +285 -0
  204. package/src/react/labels.ts +234 -0
  205. package/src/react/native.tsx +159 -0
  206. package/src/react/readSnapshot.ts +58 -0
  207. package/src/react/store.ts +31 -0
  208. package/src/react/topics.ts +427 -0
  209. package/src/react/useAppReadyMetric.ts +10 -0
  210. package/src/react/useLiveRecording.ts +43 -0
  211. package/src/react/useTraceViewer.ts +333 -0
  212. package/src/react/viewerModel.ts +172 -0
  213. package/src/specs/Recording.nitro.ts +40 -0
  214. package/src/specs/TraceSpan.nitro.ts +14 -0
  215. package/src/specs/Tracing.nitro.ts +14 -0
  216. package/src/types/CompletedSpanOptions.ts +15 -0
  217. package/src/types/EventContext.ts +8 -0
  218. package/src/types/MarkEvent.ts +3 -0
  219. package/src/types/MarkOptions.ts +6 -0
  220. package/src/types/MetricEvent.ts +8 -0
  221. package/src/types/MetricOptions.ts +10 -0
  222. package/src/types/NativeSamplingOptions.ts +7 -0
  223. package/src/types/ReadOptions.ts +7 -0
  224. package/src/types/RecordingOptions.ts +11 -0
  225. package/src/types/RecordingStats.ts +25 -0
  226. package/src/types/SpanEvent.ts +13 -0
  227. package/src/types/SpanOptions.ts +6 -0
  228. package/src/types/SpanOutcome.ts +2 -0
  229. package/src/types/TraceAttribute.ts +7 -0
  230. package/src/types/TraceAttributes.ts +4 -0
  231. package/src/types/TraceContext.ts +10 -0
  232. package/src/types/TracePage.ts +18 -0
  233. package/src/types.ts +17 -0
@@ -0,0 +1,651 @@
1
+ import React from 'react'
2
+ import {
3
+ Modal,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ TextInput,
8
+ View,
9
+ } from 'react-native'
10
+ import { SafeAreaView } from 'react-native-safe-area-context'
11
+ export const palette = {
12
+ // Light system colors, in the spirit of Xcode Instruments.
13
+ bg: '#ffffff',
14
+ panel: '#f2f2f7',
15
+ line: '#d8d8dc',
16
+ text: '#1d1d1f',
17
+ muted: '#6e6e73',
18
+ accent: '#007aff',
19
+ selected: '#e5efff',
20
+ error: '#ff3b30',
21
+ scrim: '#0003',
22
+ }
23
+ export const ui = StyleSheet.create({
24
+ bottomNav: {
25
+ borderTopWidth: StyleSheet.hairlineWidth,
26
+ borderColor: palette.line,
27
+ paddingHorizontal: 10,
28
+ paddingVertical: 8,
29
+ alignItems: 'center',
30
+ },
31
+ root: { flex: 1, backgroundColor: palette.bg },
32
+ header: {
33
+ padding: 14,
34
+ gap: 10,
35
+ borderBottomWidth: StyleSheet.hairlineWidth,
36
+ borderColor: palette.line,
37
+ },
38
+ row: { flexDirection: 'row', alignItems: 'center', gap: 8, flexWrap: 'wrap' },
39
+ spread: {
40
+ flexDirection: 'row',
41
+ alignItems: 'center',
42
+ justifyContent: 'space-between',
43
+ gap: 10,
44
+ },
45
+ content: { padding: 16, gap: 12 },
46
+ title: {
47
+ fontSize: 21,
48
+ fontWeight: '700',
49
+ color: palette.text,
50
+ flexShrink: 1,
51
+ },
52
+ heading: { fontSize: 16, fontWeight: '600', color: palette.text },
53
+ text: { fontSize: 14, color: palette.text },
54
+ muted: { fontSize: 12, lineHeight: 18, color: palette.muted },
55
+ error: { color: palette.error, fontSize: 13 },
56
+ card: {
57
+ padding: 14,
58
+ borderRadius: 10,
59
+ backgroundColor: palette.panel,
60
+ gap: 9,
61
+ },
62
+ item: {
63
+ padding: 15,
64
+ borderBottomWidth: StyleSheet.hairlineWidth,
65
+ borderColor: palette.line,
66
+ gap: 6,
67
+ },
68
+ input: {
69
+ borderWidth: 1,
70
+ borderColor: palette.line,
71
+ borderRadius: 8,
72
+ paddingHorizontal: 12,
73
+ paddingVertical: 10,
74
+ color: palette.text,
75
+ fontSize: 14,
76
+ },
77
+ button: {
78
+ paddingHorizontal: 12,
79
+ paddingVertical: 8,
80
+ borderRadius: 7,
81
+ backgroundColor: palette.panel,
82
+ borderWidth: StyleSheet.hairlineWidth,
83
+ borderColor: palette.line,
84
+ },
85
+ selected: { backgroundColor: palette.selected, borderColor: palette.accent },
86
+ buttonText: { color: palette.text, fontSize: 13, fontWeight: '500' },
87
+ selectedText: { color: palette.accent },
88
+ link: { color: palette.accent, fontSize: 13 },
89
+ chart: { height: 86, flexDirection: 'row', gap: 2, alignItems: 'flex-end' },
90
+ bar: { flex: 1, backgroundColor: palette.accent, minHeight: 2 },
91
+ track: {
92
+ height: 7,
93
+ backgroundColor: palette.panel,
94
+ borderRadius: 3,
95
+ overflow: 'hidden',
96
+ },
97
+ })
98
+ export function Button({
99
+ label,
100
+ onPress,
101
+ selected,
102
+ disabled,
103
+ testID,
104
+ }: {
105
+ label: string
106
+ onPress: () => void
107
+ selected?: boolean
108
+ disabled?: boolean
109
+ testID?: string
110
+ }) {
111
+ return (
112
+ <Pressable
113
+ accessibilityRole="button"
114
+ accessibilityState={{ selected, disabled }}
115
+ testID={testID}
116
+ disabled={disabled}
117
+ onPress={onPress}
118
+ style={[ui.button, selected && ui.selected, disabled && { opacity: 0.4 }]}
119
+ >
120
+ <Text style={[ui.buttonText, selected && ui.selectedText]}>{label}</Text>
121
+ </Pressable>
122
+ )
123
+ }
124
+ export function Field({
125
+ label,
126
+ value,
127
+ onChange,
128
+ numeric = false,
129
+ }: {
130
+ label: string
131
+ value: string
132
+ onChange: (value: string) => void
133
+ numeric?: boolean
134
+ }) {
135
+ return (
136
+ <View style={{ gap: 4 }}>
137
+ <Text style={ui.muted}>{label}</Text>
138
+ <TextInput
139
+ accessibilityLabel={label}
140
+ placeholder={label}
141
+ placeholderTextColor={palette.muted}
142
+ value={value}
143
+ onChangeText={onChange}
144
+ keyboardType={numeric ? 'decimal-pad' : 'default'}
145
+ autoCapitalize="none"
146
+ autoCorrect={false}
147
+ style={ui.input}
148
+ />
149
+ </View>
150
+ )
151
+ }
152
+
153
+ // ── Apple-style primitives: grouped lists, bars and sheets ──────────────────
154
+ export const apple = {
155
+ grouped: '#f2f2f7',
156
+ card: '#ffffff',
157
+ separator: '#c6c6c8',
158
+ secondary: '#8e8e93',
159
+ fill: '#e3e3e8',
160
+ }
161
+ const hairline = StyleSheet.hairlineWidth
162
+ const s = StyleSheet.create({
163
+ navBar: {
164
+ flexDirection: 'row',
165
+ alignItems: 'center',
166
+ paddingHorizontal: 8,
167
+ minHeight: 44,
168
+ backgroundColor: apple.grouped,
169
+ },
170
+ navSide: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 4 },
171
+ navTitle: {
172
+ fontSize: 17,
173
+ fontWeight: '600',
174
+ color: palette.text,
175
+ textAlign: 'center',
176
+ },
177
+ textButton: { paddingHorizontal: 8, paddingVertical: 10 },
178
+ textButtonLabel: { fontSize: 17, color: palette.accent },
179
+ iconLabel: { fontSize: 20, color: palette.accent },
180
+ section: { marginHorizontal: 16, marginTop: 20 },
181
+ sectionHeader: {
182
+ fontSize: 13,
183
+ color: apple.secondary,
184
+ textTransform: 'uppercase',
185
+ marginLeft: 16,
186
+ marginBottom: 6,
187
+ },
188
+ sectionFooter: {
189
+ fontSize: 13,
190
+ color: apple.secondary,
191
+ marginHorizontal: 16,
192
+ marginTop: 6,
193
+ lineHeight: 18,
194
+ },
195
+ sectionBody: {
196
+ backgroundColor: apple.card,
197
+ borderRadius: 10,
198
+ overflow: 'hidden',
199
+ },
200
+ row: {
201
+ flexDirection: 'row',
202
+ alignItems: 'center',
203
+ minHeight: 44,
204
+ paddingVertical: 8,
205
+ paddingLeft: 16,
206
+ paddingRight: 12,
207
+ backgroundColor: apple.card,
208
+ gap: 12,
209
+ },
210
+ rowSeparator: {
211
+ position: 'absolute',
212
+ left: 16,
213
+ right: 0,
214
+ bottom: 0,
215
+ height: hairline,
216
+ backgroundColor: apple.separator,
217
+ },
218
+ rowIcon: { width: 22, textAlign: 'center', fontSize: 17 },
219
+ rowText: { flex: 1, gap: 2 },
220
+ rowTitle: { fontSize: 16, color: palette.text },
221
+ rowSubtitle: { fontSize: 12, color: apple.secondary },
222
+ rowValue: {
223
+ fontSize: 15,
224
+ fontWeight: '500',
225
+ color: apple.secondary,
226
+ fontVariant: ['tabular-nums'],
227
+ },
228
+ chevron: { fontSize: 20, color: '#c4c4c7', marginLeft: -4 },
229
+ check: { width: 20, fontSize: 17, fontWeight: '600', color: palette.accent },
230
+ pressed: { backgroundColor: '#e5e5ea' },
231
+ search: {
232
+ flexDirection: 'row',
233
+ alignItems: 'center',
234
+ marginHorizontal: 16,
235
+ marginTop: 8,
236
+ paddingHorizontal: 8,
237
+ height: 36,
238
+ borderRadius: 10,
239
+ backgroundColor: apple.fill,
240
+ gap: 6,
241
+ },
242
+ searchInput: { flex: 1, fontSize: 16, color: palette.text, padding: 0 },
243
+ segmented: {
244
+ flexDirection: 'row',
245
+ marginHorizontal: 16,
246
+ marginTop: 10,
247
+ padding: 2,
248
+ borderRadius: 9,
249
+ backgroundColor: apple.fill,
250
+ },
251
+ segment: {
252
+ flex: 1,
253
+ height: 28,
254
+ alignItems: 'center',
255
+ justifyContent: 'center',
256
+ paddingHorizontal: 4,
257
+ borderRadius: 7,
258
+ },
259
+ segmentOn: {
260
+ backgroundColor: apple.card,
261
+ shadowColor: '#000',
262
+ shadowOpacity: 0.12,
263
+ shadowRadius: 3,
264
+ shadowOffset: { width: 0, height: 1 },
265
+ elevation: 1,
266
+ },
267
+ segmentText: { fontSize: 13, fontWeight: '500', color: palette.text },
268
+ segmentTextOn: { fontWeight: '600' },
269
+ tabBar: {
270
+ flexDirection: 'row',
271
+ borderTopWidth: hairline,
272
+ borderColor: apple.separator,
273
+ backgroundColor: '#f9f9f9',
274
+ paddingTop: 6,
275
+ paddingBottom: 4,
276
+ },
277
+ tab: { flex: 1, alignItems: 'center', gap: 2 },
278
+ tabIcon: {
279
+ height: 26,
280
+ fontSize: 20,
281
+ lineHeight: 26,
282
+ textAlign: 'center',
283
+ color: apple.secondary,
284
+ },
285
+ tabLabel: { fontSize: 10, fontWeight: '500', color: apple.secondary },
286
+ tabOn: { color: palette.accent },
287
+ banner: {
288
+ flexDirection: 'row',
289
+ justifyContent: 'center',
290
+ alignSelf: 'center',
291
+ marginTop: 8,
292
+ paddingHorizontal: 14,
293
+ paddingVertical: 6,
294
+ borderRadius: 16,
295
+ backgroundColor: palette.accent,
296
+ },
297
+ bannerText: { color: '#fff', fontSize: 13, fontWeight: '600' },
298
+ scrim: {
299
+ flex: 1,
300
+ backgroundColor: palette.scrim,
301
+ justifyContent: 'flex-end',
302
+ },
303
+ sheet: { padding: 8, gap: 8 },
304
+ sheetGroup: {
305
+ backgroundColor: 'rgba(249,249,249,0.97)',
306
+ borderRadius: 14,
307
+ overflow: 'hidden',
308
+ },
309
+ sheetTitle: {
310
+ fontSize: 13,
311
+ color: apple.secondary,
312
+ textAlign: 'center',
313
+ padding: 14,
314
+ borderBottomWidth: hairline,
315
+ borderColor: apple.separator,
316
+ },
317
+ sheetOption: {
318
+ minHeight: 57,
319
+ alignItems: 'center',
320
+ justifyContent: 'center',
321
+ borderBottomWidth: hairline,
322
+ borderColor: apple.separator,
323
+ paddingHorizontal: 16,
324
+ },
325
+ sheetOptionText: { fontSize: 20, color: palette.accent },
326
+ sheetCancel: { fontWeight: '600' },
327
+ })
328
+
329
+ export function NavBar({
330
+ left,
331
+ title,
332
+ right,
333
+ }: {
334
+ left?: React.ReactNode
335
+ title: string
336
+ right?: React.ReactNode
337
+ }) {
338
+ return (
339
+ <View style={s.navBar}>
340
+ <View style={s.navSide}>{left}</View>
341
+ <Text style={s.navTitle} numberOfLines={1} accessibilityRole="header">
342
+ {title}
343
+ </Text>
344
+ <View style={[s.navSide, { justifyContent: 'flex-end' }]}>{right}</View>
345
+ </View>
346
+ )
347
+ }
348
+ /** Blue text or glyph button, as in iOS navigation bars and toolbars. */
349
+ export function TextButton({
350
+ label,
351
+ icon,
352
+ onPress,
353
+ disabled,
354
+ bold,
355
+ accessibilityLabel,
356
+ }: {
357
+ label?: string
358
+ icon?: string
359
+ onPress: () => void
360
+ disabled?: boolean
361
+ bold?: boolean
362
+ accessibilityLabel?: string
363
+ }) {
364
+ return (
365
+ <Pressable
366
+ accessibilityRole="button"
367
+ accessibilityLabel={accessibilityLabel ?? label}
368
+ disabled={disabled}
369
+ onPress={onPress}
370
+ hitSlop={6}
371
+ style={[s.textButton, disabled && { opacity: 0.35 }]}
372
+ >
373
+ <Text
374
+ style={[
375
+ icon ? s.iconLabel : s.textButtonLabel,
376
+ bold && { fontWeight: '600' },
377
+ ]}
378
+ >
379
+ {icon ?? label}
380
+ </Text>
381
+ </Pressable>
382
+ )
383
+ }
384
+ export function Section({
385
+ header,
386
+ footer,
387
+ children,
388
+ }: {
389
+ header?: string
390
+ footer?: string
391
+ children: React.ReactNode
392
+ }) {
393
+ return (
394
+ <View style={s.section}>
395
+ {header && <Text style={s.sectionHeader}>{header}</Text>}
396
+ <View style={s.sectionBody}>{children}</View>
397
+ {footer && <Text style={s.sectionFooter}>{footer}</Text>}
398
+ </View>
399
+ )
400
+ }
401
+ export interface RowProps {
402
+ title: string
403
+ subtitle?: string
404
+ value?: string
405
+ icon?: string
406
+ iconColor?: string
407
+ /** Tints title and value, e.g. errors. */
408
+ tint?: string
409
+ onPress?: () => void
410
+ /** Hide the separator on the last row of a section. */
411
+ last?: boolean
412
+ /** Toggle row: shows a checkmark instead of a disclosure chevron. */
413
+ checked?: boolean
414
+ }
415
+ /** Memoized: unchanged rows skip re-rendering when live data ticks. */
416
+ export const Row = React.memo(function Row({
417
+ title,
418
+ subtitle,
419
+ value,
420
+ icon,
421
+ iconColor,
422
+ tint,
423
+ onPress,
424
+ last,
425
+ checked,
426
+ }: RowProps) {
427
+ return (
428
+ <Pressable
429
+ accessibilityRole={
430
+ checked !== undefined ? 'checkbox' : onPress ? 'button' : undefined
431
+ }
432
+ accessibilityState={checked !== undefined ? { checked } : undefined}
433
+ disabled={!onPress}
434
+ onPress={onPress}
435
+ style={({ pressed }) => [s.row, pressed && s.pressed]}
436
+ >
437
+ {icon !== undefined && (
438
+ <Text style={[s.rowIcon, { color: iconColor ?? palette.accent }]}>
439
+ {icon}
440
+ </Text>
441
+ )}
442
+ <View style={s.rowText}>
443
+ <Text
444
+ style={[s.rowTitle, tint && { color: tint }]}
445
+ numberOfLines={1}
446
+ ellipsizeMode="middle"
447
+ >
448
+ {title}
449
+ </Text>
450
+ {!!subtitle && (
451
+ <Text style={s.rowSubtitle} numberOfLines={2}>
452
+ {subtitle}
453
+ </Text>
454
+ )}
455
+ </View>
456
+ {!!value && (
457
+ <Text style={[s.rowValue, tint && { color: tint }]}>{value}</Text>
458
+ )}
459
+ {checked !== undefined ? (
460
+ <Text style={s.check}>{checked ? '✓' : ''}</Text>
461
+ ) : (
462
+ onPress && <Text style={s.chevron}>›</Text>
463
+ )}
464
+ {!last && <View style={s.rowSeparator} />}
465
+ </Pressable>
466
+ )
467
+ })
468
+ export function SearchField({
469
+ value,
470
+ onChange,
471
+ placeholder,
472
+ }: {
473
+ value: string
474
+ onChange: (value: string) => void
475
+ placeholder: string
476
+ }) {
477
+ return (
478
+ <View style={s.search}>
479
+ <Text style={{ color: apple.secondary, fontSize: 20 }}>⌕</Text>
480
+ <TextInput
481
+ accessibilityLabel={placeholder}
482
+ placeholder={placeholder}
483
+ placeholderTextColor={apple.secondary}
484
+ value={value}
485
+ onChangeText={onChange}
486
+ autoCapitalize="none"
487
+ autoCorrect={false}
488
+ clearButtonMode="while-editing"
489
+ returnKeyType="search"
490
+ style={s.searchInput}
491
+ />
492
+ </View>
493
+ )
494
+ }
495
+ export function Segmented<T extends string>({
496
+ options,
497
+ value,
498
+ onChange,
499
+ }: {
500
+ options: readonly { value: T; label: string }[]
501
+ value: T
502
+ onChange: (value: T) => void
503
+ }) {
504
+ return (
505
+ <View style={s.segmented} accessibilityRole="tablist">
506
+ {options.map((option) => (
507
+ <Pressable
508
+ key={option.value}
509
+ accessibilityRole="tab"
510
+ accessibilityState={{ selected: option.value === value }}
511
+ onPress={() => onChange(option.value)}
512
+ style={[s.segment, option.value === value && s.segmentOn]}
513
+ >
514
+ <Text
515
+ style={[s.segmentText, option.value === value && s.segmentTextOn]}
516
+ numberOfLines={1}
517
+ adjustsFontSizeToFit
518
+ minimumFontScale={0.8}
519
+ >
520
+ {option.label}
521
+ </Text>
522
+ </Pressable>
523
+ ))}
524
+ </View>
525
+ )
526
+ }
527
+ export function TabBar<T extends string>({
528
+ items,
529
+ value,
530
+ onChange,
531
+ }: {
532
+ items: readonly { value: T; label: string; icon: string; iconSize?: number }[]
533
+ value: T
534
+ onChange: (value: T) => void
535
+ }) {
536
+ return (
537
+ <View style={s.tabBar} accessibilityRole="tablist">
538
+ {items.map((item) => {
539
+ const on = item.value === value
540
+ return (
541
+ <Pressable
542
+ key={item.value}
543
+ accessibilityRole="tab"
544
+ accessibilityState={{ selected: on }}
545
+ onPress={() => onChange(item.value)}
546
+ style={s.tab}
547
+ >
548
+ <Text
549
+ style={[
550
+ s.tabIcon,
551
+ item.iconSize ? { fontSize: item.iconSize } : undefined,
552
+ on && s.tabOn,
553
+ ]}
554
+ >
555
+ {item.icon}
556
+ </Text>
557
+ <Text style={[s.tabLabel, on && s.tabOn]}>{item.label}</Text>
558
+ </Pressable>
559
+ )
560
+ })}
561
+ </View>
562
+ )
563
+ }
564
+ export function Banner({
565
+ label,
566
+ onPress,
567
+ }: {
568
+ label: string
569
+ onPress: () => void
570
+ }) {
571
+ return (
572
+ <Pressable accessibilityRole="button" onPress={onPress} style={s.banner}>
573
+ <Text style={s.bannerText}>↑ {label}</Text>
574
+ </Pressable>
575
+ )
576
+ }
577
+ export interface SheetOption {
578
+ label: string
579
+ onPress: () => void
580
+ selected?: boolean
581
+ destructive?: boolean
582
+ disabled?: boolean
583
+ }
584
+ /** iOS-style action sheet fallback (Android, or without @expo/ui). Mount only while open. */
585
+ export function ActionSheet({
586
+ title,
587
+ options,
588
+ cancelLabel,
589
+ close,
590
+ }: {
591
+ title?: string
592
+ options: SheetOption[]
593
+ cancelLabel: string
594
+ close: () => void
595
+ }) {
596
+ return (
597
+ <Modal visible transparent animationType="fade" onRequestClose={close}>
598
+ <Pressable
599
+ style={s.scrim}
600
+ onPress={close}
601
+ accessibilityLabel={cancelLabel}
602
+ >
603
+ <SafeAreaView edges={['bottom']} style={s.sheet}>
604
+ <View style={s.sheetGroup}>
605
+ {title && <Text style={s.sheetTitle}>{title}</Text>}
606
+ {options.map((option) => (
607
+ <Pressable
608
+ key={option.label}
609
+ accessibilityRole="button"
610
+ accessibilityState={{
611
+ selected: option.selected,
612
+ disabled: option.disabled,
613
+ }}
614
+ disabled={option.disabled}
615
+ onPress={() => {
616
+ close()
617
+ option.onPress()
618
+ }}
619
+ style={({ pressed }) => [s.sheetOption, pressed && s.pressed]}
620
+ >
621
+ <Text
622
+ style={[
623
+ s.sheetOptionText,
624
+ option.destructive && { color: palette.error },
625
+ option.disabled && { color: apple.secondary },
626
+ ]}
627
+ >
628
+ {option.selected ? '✓ ' : ''}
629
+ {option.label}
630
+ </Text>
631
+ </Pressable>
632
+ ))}
633
+ </View>
634
+ <Pressable
635
+ accessibilityRole="button"
636
+ onPress={close}
637
+ style={({ pressed }) => [
638
+ s.sheetGroup,
639
+ s.sheetOption,
640
+ pressed && s.pressed,
641
+ ]}
642
+ >
643
+ <Text style={[s.sheetOptionText, s.sheetCancel]}>
644
+ {cancelLabel}
645
+ </Text>
646
+ </Pressable>
647
+ </SafeAreaView>
648
+ </Pressable>
649
+ </Modal>
650
+ )
651
+ }