@buoy-gg/route-events 2.1.3 → 2.1.4-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.
Files changed (36) hide show
  1. package/lib/commonjs/RouteTracker.js +32 -5
  2. package/lib/commonjs/components/NavigationStack.js +24 -33
  3. package/lib/commonjs/components/ReactNavigationRoutes.js +829 -0
  4. package/lib/commonjs/components/RouteEventsModalWithTabs.js +83 -6
  5. package/lib/commonjs/components/RoutesSitemap.js +1 -2
  6. package/lib/commonjs/index.js +7 -0
  7. package/lib/commonjs/useNavigationStack.js +201 -104
  8. package/lib/commonjs/useRouteObserver.js +4 -4
  9. package/lib/commonjs/useRouteObserverReactNavigation.js +109 -0
  10. package/lib/module/RouteTracker.js +32 -5
  11. package/lib/module/components/NavigationStack.js +24 -33
  12. package/lib/module/components/ReactNavigationRoutes.js +825 -0
  13. package/lib/module/components/RouteEventsModalWithTabs.js +85 -7
  14. package/lib/module/components/RoutesSitemap.js +2 -2
  15. package/lib/module/index.js +1 -0
  16. package/lib/module/useNavigationStack.js +203 -106
  17. package/lib/module/useRouteObserver.js +4 -4
  18. package/lib/module/useRouteObserverReactNavigation.js +106 -0
  19. package/lib/typescript/RouteTracker.d.ts +18 -5
  20. package/lib/typescript/RouteTracker.d.ts.map +1 -1
  21. package/lib/typescript/components/NavigationStack.d.ts.map +1 -1
  22. package/lib/typescript/components/ReactNavigationRoutes.d.ts +14 -0
  23. package/lib/typescript/components/ReactNavigationRoutes.d.ts.map +1 -0
  24. package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -1
  25. package/lib/typescript/index.d.ts +1 -0
  26. package/lib/typescript/index.d.ts.map +1 -1
  27. package/lib/typescript/useNavigationStack.d.ts +9 -3
  28. package/lib/typescript/useNavigationStack.d.ts.map +1 -1
  29. package/lib/typescript/useRouteObserver.d.ts.map +1 -1
  30. package/lib/typescript/useRouteObserverReactNavigation.d.ts +19 -0
  31. package/lib/typescript/useRouteObserverReactNavigation.d.ts.map +1 -0
  32. package/package.json +6 -6
  33. package/lib/commonjs/utils/safeExpoRouter.js +0 -129
  34. package/lib/module/utils/safeExpoRouter.js +0 -120
  35. package/lib/typescript/utils/safeExpoRouter.d.ts +0 -13
  36. package/lib/typescript/utils/safeExpoRouter.d.ts.map +0 -1
@@ -0,0 +1,825 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ReactNavigationRoutes - Visual sitemap of all screens in a React Navigation app
5
+ *
6
+ * Discovers all registered screens by walking the navigation state tree.
7
+ * Each navigator's state includes a `routeNames` array listing ALL screens
8
+ * registered with it — even ones not currently mounted in the stack.
9
+ *
10
+ * Provides navigation to any discovered screen using nested screen/params actions.
11
+ */
12
+
13
+ import { useState, useCallback, useMemo, useEffect, useContext } from "react";
14
+ import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, Alert } from "react-native";
15
+ import { CommonActions } from "@react-navigation/native";
16
+ import { NavigationContainerRefContext } from "@react-navigation/core";
17
+ import { Search, ChevronDown, ChevronRight, InlineCopyButton, ToolbarCopyButton, RefreshCw, ProUpgradeModal, buoyColors } from "@buoy-gg/shared-ui";
18
+ import { useIsPro } from "@buoy-gg/license";
19
+
20
+ // ============================================================================
21
+ // Types
22
+ // ============================================================================
23
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
24
+ // ============================================================================
25
+ // State Tree Walking
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Walk the navigation state tree and collect all navigators with their registered screens.
30
+ */
31
+ function collectNavigatorGroups(state, parentPath = "Root") {
32
+ if (!state) return [];
33
+ const groups = [];
34
+
35
+ // routeNames lists ALL screens registered with this navigator
36
+ const routeNames = state.routeNames || [];
37
+ const mountedRouteNames = new Set((state.routes || []).map(r => r.name));
38
+ const focusedIndex = state.index ?? 0;
39
+ const focusedRouteName = state.routes?.[focusedIndex]?.name;
40
+ if (routeNames.length > 0) {
41
+ const screens = routeNames.map(name => ({
42
+ name,
43
+ isMounted: mountedRouteNames.has(name),
44
+ isFocused: name === focusedRouteName
45
+ }));
46
+ groups.push({
47
+ name: parentPath === "Root" ? "Root" : parentPath.split(" > ").pop(),
48
+ path: parentPath,
49
+ screens,
50
+ totalScreens: screens.length
51
+ });
52
+ }
53
+
54
+ // Recurse into mounted routes that have nested navigator state
55
+ for (const route of state.routes || []) {
56
+ if (route.state) {
57
+ const childPath = `${parentPath} > ${route.name}`;
58
+ const nestedGroups = collectNavigatorGroups(route.state, childPath);
59
+ groups.push(...nestedGroups);
60
+ }
61
+ }
62
+ return groups;
63
+ }
64
+
65
+ /**
66
+ * Find the path from root navigation state to a screen by name.
67
+ */
68
+ function findScreenPath(state, screenName) {
69
+ if (!state || !state.routes) return null;
70
+
71
+ // Check routeNames (all registered screens) not just mounted routes
72
+ const routeNames = state.routeNames || [];
73
+ if (routeNames.includes(screenName)) {
74
+ return [screenName];
75
+ }
76
+ for (const route of state.routes) {
77
+ if (route.state) {
78
+ const nested = findScreenPath(route.state, screenName);
79
+ if (nested) {
80
+ return [route.name, ...nested];
81
+ }
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Build a nested CommonActions.navigate() from a path of screen names.
89
+ */
90
+ function buildNestedNavigateAction(path) {
91
+ if (path.length === 0) return null;
92
+ if (path.length === 1) {
93
+ return CommonActions.navigate({
94
+ name: path[0]
95
+ });
96
+ }
97
+ let params = {
98
+ screen: path[path.length - 1]
99
+ };
100
+ for (let i = path.length - 2; i >= 1; i--) {
101
+ params = {
102
+ screen: path[i],
103
+ params
104
+ };
105
+ }
106
+ return CommonActions.navigate({
107
+ name: path[0],
108
+ params
109
+ });
110
+ }
111
+
112
+ // ============================================================================
113
+ // Main Component
114
+ // ============================================================================
115
+
116
+ export function ReactNavigationRoutes({
117
+ style
118
+ }) {
119
+ const containerRef = useContext(NavigationContainerRefContext);
120
+ const [navigationState, setNavigationState] = useState(() => containerRef?.getRootState?.() ?? null);
121
+ const [searchQuery, setSearchQuery] = useState("");
122
+ const [isSearching, setIsSearching] = useState(false);
123
+ const [expandedGroup, setExpandedGroup] = useState(null);
124
+ const [isRefreshing, setIsRefreshing] = useState(false);
125
+ const [showUpgradeModal, setShowUpgradeModal] = useState(false);
126
+ const isPro = useIsPro();
127
+ useEffect(() => {
128
+ if (showUpgradeModal && isPro) {
129
+ setShowUpgradeModal(false);
130
+ }
131
+ }, [showUpgradeModal, isPro]);
132
+
133
+ // Subscribe to navigation state changes
134
+ useEffect(() => {
135
+ if (!containerRef) return;
136
+ const initialState = containerRef.getRootState?.();
137
+ if (initialState) {
138
+ setNavigationState(initialState);
139
+ }
140
+ const unsubscribe = containerRef.addListener?.("state", () => {
141
+ const state = containerRef.getRootState?.();
142
+ if (state) {
143
+ setNavigationState(state);
144
+ }
145
+ });
146
+ return () => {
147
+ unsubscribe?.();
148
+ };
149
+ }, [containerRef]);
150
+
151
+ // Build navigator groups from state
152
+ const groups = useMemo(() => collectNavigatorGroups(navigationState), [navigationState]);
153
+
154
+ // Auto-expand first group on first load
155
+ useEffect(() => {
156
+ if (groups.length > 0 && expandedGroup === null) {
157
+ setExpandedGroup(groups[0].path);
158
+ }
159
+ }, [groups.length]);
160
+
161
+ // Flatten all screens for stats and search
162
+ const allScreens = useMemo(() => {
163
+ const screens = [];
164
+ for (const group of groups) {
165
+ for (const screen of group.screens) {
166
+ screens.push({
167
+ name: screen.name,
168
+ navigatorPath: group.path,
169
+ isMounted: screen.isMounted,
170
+ isFocused: screen.isFocused
171
+ });
172
+ }
173
+ }
174
+ return screens;
175
+ }, [groups]);
176
+
177
+ // Filter groups by search
178
+ const filteredGroups = useMemo(() => {
179
+ if (!searchQuery.trim()) return groups;
180
+ const query = searchQuery.toLowerCase();
181
+ return groups.map(group => ({
182
+ ...group,
183
+ screens: group.screens.filter(s => s.name.toLowerCase().includes(query))
184
+ })).filter(group => group.screens.length > 0);
185
+ }, [groups, searchQuery]);
186
+
187
+ // Stats
188
+ const stats = useMemo(() => {
189
+ const total = allScreens.length;
190
+ const mounted = allScreens.filter(s => s.isMounted).length;
191
+ const available = total - mounted;
192
+ const navigators = groups.length;
193
+ return {
194
+ total,
195
+ mounted,
196
+ available,
197
+ navigators
198
+ };
199
+ }, [allScreens, groups]);
200
+
201
+ // Copy data
202
+ const copyAllData = useMemo(() => {
203
+ return {
204
+ summary: {
205
+ totalScreens: stats.total,
206
+ mounted: stats.mounted,
207
+ available: stats.available,
208
+ navigators: stats.navigators,
209
+ timestamp: new Date().toISOString()
210
+ },
211
+ navigators: groups.map(group => ({
212
+ path: group.path,
213
+ screens: group.screens.map(s => ({
214
+ name: s.name,
215
+ isMounted: s.isMounted,
216
+ isFocused: s.isFocused
217
+ }))
218
+ }))
219
+ };
220
+ }, [groups, stats]);
221
+ const handleToggleGroup = useCallback(groupPath => {
222
+ setExpandedGroup(prev => prev === groupPath ? null : groupPath);
223
+ }, []);
224
+ const handleNavigate = useCallback(screenName => {
225
+ if (!isPro) {
226
+ setShowUpgradeModal(true);
227
+ return;
228
+ }
229
+ if (!containerRef || !navigationState) return;
230
+ try {
231
+ const path = findScreenPath(navigationState, screenName);
232
+ if (path) {
233
+ const action = buildNestedNavigateAction(path);
234
+ if (action) {
235
+ containerRef.dispatch(action);
236
+ return;
237
+ }
238
+ }
239
+ // Fallback
240
+ containerRef.dispatch(CommonActions.navigate({
241
+ name: screenName
242
+ }));
243
+ } catch (error) {
244
+ Alert.alert("Navigation Error", String(error));
245
+ }
246
+ }, [containerRef, navigationState, isPro]);
247
+ const handleRefresh = useCallback(() => {
248
+ if (isRefreshing || !containerRef) return;
249
+ setIsRefreshing(true);
250
+ const state = containerRef.getRootState?.();
251
+ if (state) {
252
+ setNavigationState(state);
253
+ }
254
+ setTimeout(() => setIsRefreshing(false), 400);
255
+ }, [isRefreshing, containerRef]);
256
+ if (!navigationState) {
257
+ return /*#__PURE__*/_jsx(View, {
258
+ style: [styles.container, style],
259
+ children: /*#__PURE__*/_jsxs(View, {
260
+ style: styles.emptyState,
261
+ children: [/*#__PURE__*/_jsx(Text, {
262
+ style: styles.emptyText,
263
+ children: "No navigation state available"
264
+ }), /*#__PURE__*/_jsx(Text, {
265
+ style: styles.emptySubtext,
266
+ children: "Ensure you are inside a NavigationContainer"
267
+ })]
268
+ })
269
+ });
270
+ }
271
+ return /*#__PURE__*/_jsxs(View, {
272
+ style: [styles.container, style],
273
+ children: [!isSearching ? /*#__PURE__*/_jsxs(View, {
274
+ style: styles.header,
275
+ children: [/*#__PURE__*/_jsxs(View, {
276
+ style: styles.actionsRow,
277
+ children: [/*#__PURE__*/_jsxs(View, {
278
+ style: styles.actionWrapper,
279
+ children: [/*#__PURE__*/_jsx(ToolbarCopyButton, {
280
+ value: copyAllData,
281
+ buttonStyle: styles.actionButtonHeader
282
+ }), /*#__PURE__*/_jsx(Text, {
283
+ style: styles.actionLabel,
284
+ children: "Copy"
285
+ })]
286
+ }), /*#__PURE__*/_jsxs(View, {
287
+ style: styles.actionWrapper,
288
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
289
+ style: [styles.iconButton, isRefreshing && styles.refreshButtonDisabled],
290
+ onPress: handleRefresh,
291
+ disabled: isRefreshing,
292
+ children: /*#__PURE__*/_jsx(RefreshCw, {
293
+ size: 16,
294
+ color: isRefreshing ? buoyColors.textMuted : buoyColors.textSecondary
295
+ })
296
+ }), /*#__PURE__*/_jsx(Text, {
297
+ style: styles.actionLabel,
298
+ children: "Refresh"
299
+ })]
300
+ }), /*#__PURE__*/_jsxs(View, {
301
+ style: styles.actionWrapper,
302
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
303
+ style: styles.iconButton,
304
+ onPress: () => setIsSearching(true),
305
+ children: /*#__PURE__*/_jsx(Search, {
306
+ size: 16,
307
+ color: buoyColors.textSecondary
308
+ })
309
+ }), /*#__PURE__*/_jsx(Text, {
310
+ style: styles.actionLabel,
311
+ children: "Search"
312
+ })]
313
+ })]
314
+ }), /*#__PURE__*/_jsxs(View, {
315
+ style: styles.statsGrid,
316
+ children: [/*#__PURE__*/_jsx(StatItem, {
317
+ value: stats.total,
318
+ label: "Screens"
319
+ }), /*#__PURE__*/_jsx(StatItem, {
320
+ value: stats.mounted,
321
+ label: "Mounted"
322
+ }), /*#__PURE__*/_jsx(StatItem, {
323
+ value: stats.available,
324
+ label: "Available"
325
+ }), /*#__PURE__*/_jsx(StatItem, {
326
+ value: stats.navigators,
327
+ label: "Navigators"
328
+ })]
329
+ })]
330
+ }) : /*#__PURE__*/_jsxs(View, {
331
+ style: styles.searchContainer,
332
+ children: [/*#__PURE__*/_jsx(Search, {
333
+ size: 16,
334
+ color: buoyColors.textMuted
335
+ }), /*#__PURE__*/_jsx(TextInput, {
336
+ style: styles.searchInput,
337
+ placeholder: "Search screens...",
338
+ placeholderTextColor: buoyColors.textMuted,
339
+ value: searchQuery,
340
+ onChangeText: setSearchQuery,
341
+ autoCapitalize: "none",
342
+ autoCorrect: false,
343
+ autoFocus: true
344
+ }), searchQuery.length > 0 && /*#__PURE__*/_jsx(TouchableOpacity, {
345
+ onPress: () => setSearchQuery(""),
346
+ style: styles.clearButton,
347
+ children: /*#__PURE__*/_jsx(Text, {
348
+ style: styles.clearButtonText,
349
+ children: "\u2715"
350
+ })
351
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
352
+ style: styles.closeSearchButton,
353
+ onPress: () => {
354
+ setIsSearching(false);
355
+ setSearchQuery("");
356
+ },
357
+ children: /*#__PURE__*/_jsx(Text, {
358
+ style: styles.closeSearchText,
359
+ children: "Done"
360
+ })
361
+ })]
362
+ }), /*#__PURE__*/_jsxs(ScrollView, {
363
+ style: styles.scrollView,
364
+ contentContainerStyle: styles.scrollContent,
365
+ children: [filteredGroups.length === 0 ? /*#__PURE__*/_jsx(View, {
366
+ style: styles.emptyState,
367
+ children: /*#__PURE__*/_jsx(Text, {
368
+ style: styles.emptyText,
369
+ children: searchQuery ? "No matching screens" : "No screens found"
370
+ })
371
+ }) : filteredGroups.map(group => /*#__PURE__*/_jsx(NavigatorGroupView, {
372
+ group: group,
373
+ isExpanded: searchQuery.length > 0 || expandedGroup === group.path,
374
+ onToggleExpand: () => handleToggleGroup(group.path),
375
+ onNavigate: handleNavigate
376
+ }, group.path)), /*#__PURE__*/_jsxs(View, {
377
+ style: styles.metaFooter,
378
+ children: [/*#__PURE__*/_jsx(Text, {
379
+ style: styles.metaText,
380
+ children: "Source: navigation state"
381
+ }), /*#__PURE__*/_jsx(View, {
382
+ style: styles.sourceBadge,
383
+ children: /*#__PURE__*/_jsx(Text, {
384
+ style: styles.sourceText,
385
+ children: "React Navigation"
386
+ })
387
+ })]
388
+ })]
389
+ }), /*#__PURE__*/_jsx(ProUpgradeModal, {
390
+ visible: showUpgradeModal,
391
+ onClose: () => setShowUpgradeModal(false),
392
+ featureName: "Route Navigation"
393
+ })]
394
+ });
395
+ }
396
+
397
+ // ============================================================================
398
+ // Sub-components
399
+ // ============================================================================
400
+
401
+ function StatItem({
402
+ value,
403
+ label
404
+ }) {
405
+ return /*#__PURE__*/_jsxs(View, {
406
+ style: styles.statItem,
407
+ children: [/*#__PURE__*/_jsx(Text, {
408
+ style: styles.statValue,
409
+ children: value
410
+ }), /*#__PURE__*/_jsx(Text, {
411
+ style: styles.statLabel,
412
+ children: label
413
+ })]
414
+ });
415
+ }
416
+ function NavigatorGroupView({
417
+ group,
418
+ isExpanded,
419
+ onToggleExpand,
420
+ onNavigate
421
+ }) {
422
+ const [expandedScreen, setExpandedScreen] = useState(null);
423
+ const handleToggleScreen = useCallback(screenName => {
424
+ setExpandedScreen(prev => prev === screenName ? null : screenName);
425
+ }, []);
426
+ return /*#__PURE__*/_jsxs(View, {
427
+ style: styles.groupContainer,
428
+ children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
429
+ style: styles.groupHeader,
430
+ onPress: onToggleExpand,
431
+ activeOpacity: 0.7,
432
+ children: [/*#__PURE__*/_jsxs(View, {
433
+ style: styles.groupHeaderLeft,
434
+ children: [isExpanded ? /*#__PURE__*/_jsx(ChevronDown, {
435
+ size: 14,
436
+ color: buoyColors.textSecondary
437
+ }) : /*#__PURE__*/_jsx(ChevronRight, {
438
+ size: 14,
439
+ color: buoyColors.textSecondary
440
+ }), /*#__PURE__*/_jsx(Text, {
441
+ style: styles.groupTitle,
442
+ numberOfLines: 1,
443
+ children: group.name
444
+ })]
445
+ }), /*#__PURE__*/_jsx(View, {
446
+ style: styles.groupBadge,
447
+ children: /*#__PURE__*/_jsx(Text, {
448
+ style: styles.groupCount,
449
+ children: group.screens.length
450
+ })
451
+ })]
452
+ }), isExpanded && /*#__PURE__*/_jsxs(View, {
453
+ style: styles.screensList,
454
+ children: [group.path !== "Root" && group.path.includes(" > ") && /*#__PURE__*/_jsx(View, {
455
+ style: styles.groupDescription,
456
+ children: /*#__PURE__*/_jsx(Text, {
457
+ style: styles.groupDescriptionText,
458
+ children: group.path
459
+ })
460
+ }), group.screens.map(screen => /*#__PURE__*/_jsx(ScreenItemView, {
461
+ screen: screen,
462
+ isExpanded: expandedScreen === screen.name,
463
+ onToggleExpand: () => handleToggleScreen(screen.name),
464
+ onNavigate: onNavigate
465
+ }, screen.name))]
466
+ })]
467
+ });
468
+ }
469
+ function ScreenItemView({
470
+ screen,
471
+ isExpanded,
472
+ onToggleExpand,
473
+ onNavigate
474
+ }) {
475
+ const statusColor = screen.isFocused ? buoyColors.success : screen.isMounted ? "#3B82F6" : buoyColors.textMuted;
476
+ const statusLabel = screen.isFocused ? "FOCUSED" : screen.isMounted ? "MOUNTED" : "AVAILABLE";
477
+ return /*#__PURE__*/_jsx(View, {
478
+ style: styles.screenItem,
479
+ children: /*#__PURE__*/_jsxs(View, {
480
+ style: styles.screenCard,
481
+ children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
482
+ style: styles.screenHeaderLeft,
483
+ onPress: onToggleExpand,
484
+ activeOpacity: 0.7,
485
+ children: [/*#__PURE__*/_jsx(View, {
486
+ style: styles.expandIndicator,
487
+ children: isExpanded ? /*#__PURE__*/_jsx(ChevronDown, {
488
+ size: 14,
489
+ color: buoyColors.textSecondary
490
+ }) : /*#__PURE__*/_jsx(ChevronRight, {
491
+ size: 14,
492
+ color: buoyColors.textSecondary
493
+ })
494
+ }), /*#__PURE__*/_jsx(Text, {
495
+ style: styles.screenName,
496
+ numberOfLines: 1,
497
+ children: screen.name
498
+ })]
499
+ }), /*#__PURE__*/_jsxs(View, {
500
+ style: styles.screenHeaderActions,
501
+ children: [/*#__PURE__*/_jsx(InlineCopyButton, {
502
+ value: screen.name,
503
+ buttonStyle: styles.headerActionButton
504
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
505
+ style: [styles.headerActionButton, styles.navigateButton],
506
+ onPress: () => onNavigate(screen.name),
507
+ children: /*#__PURE__*/_jsx(Text, {
508
+ style: styles.navigateButtonText,
509
+ children: "Go"
510
+ })
511
+ }), /*#__PURE__*/_jsx(View, {
512
+ style: [styles.statusBadge, {
513
+ backgroundColor: `${statusColor}15`,
514
+ borderColor: `${statusColor}40`
515
+ }],
516
+ children: /*#__PURE__*/_jsx(Text, {
517
+ style: [styles.statusText, {
518
+ color: statusColor
519
+ }],
520
+ children: statusLabel
521
+ })
522
+ })]
523
+ })]
524
+ })
525
+ });
526
+ }
527
+
528
+ // ============================================================================
529
+ // Styles
530
+ // ============================================================================
531
+
532
+ const styles = StyleSheet.create({
533
+ container: {
534
+ flex: 1,
535
+ backgroundColor: buoyColors.base
536
+ },
537
+ emptyState: {
538
+ padding: 32,
539
+ alignItems: "center"
540
+ },
541
+ emptyText: {
542
+ color: buoyColors.textSecondary,
543
+ fontSize: 14,
544
+ fontFamily: "monospace"
545
+ },
546
+ emptySubtext: {
547
+ color: buoyColors.textMuted,
548
+ fontSize: 12,
549
+ fontFamily: "monospace",
550
+ marginTop: 8
551
+ },
552
+ header: {
553
+ flexDirection: "column",
554
+ padding: 8,
555
+ borderBottomWidth: 1,
556
+ borderBottomColor: buoyColors.border,
557
+ gap: 12
558
+ },
559
+ actionsRow: {
560
+ flexDirection: "row",
561
+ alignItems: "center",
562
+ justifyContent: "flex-end",
563
+ gap: 12,
564
+ paddingBottom: 4
565
+ },
566
+ actionWrapper: {
567
+ alignItems: "center",
568
+ justifyContent: "center",
569
+ gap: 4,
570
+ minWidth: 48
571
+ },
572
+ actionButtonHeader: {
573
+ padding: 6,
574
+ borderRadius: 4,
575
+ backgroundColor: buoyColors.input
576
+ },
577
+ iconButton: {
578
+ padding: 6,
579
+ borderRadius: 4,
580
+ alignItems: "center",
581
+ justifyContent: "center"
582
+ },
583
+ actionLabel: {
584
+ fontSize: 8,
585
+ color: buoyColors.textMuted,
586
+ fontFamily: "monospace",
587
+ textTransform: "uppercase",
588
+ letterSpacing: 0.5
589
+ },
590
+ refreshButtonDisabled: {
591
+ opacity: 0.5
592
+ },
593
+ statsGrid: {
594
+ flexDirection: "row",
595
+ flexWrap: "wrap",
596
+ gap: 8
597
+ },
598
+ statItem: {
599
+ flexGrow: 1,
600
+ flexShrink: 1,
601
+ flexBasis: "20%",
602
+ minWidth: 70,
603
+ backgroundColor: buoyColors.card,
604
+ borderRadius: 8,
605
+ paddingVertical: 10,
606
+ paddingHorizontal: 12,
607
+ alignItems: "center"
608
+ },
609
+ statValue: {
610
+ fontSize: 18,
611
+ fontWeight: "800",
612
+ color: buoyColors.text,
613
+ fontFamily: "monospace"
614
+ },
615
+ statLabel: {
616
+ fontSize: 10,
617
+ color: buoyColors.textMuted,
618
+ marginTop: 4,
619
+ fontFamily: "monospace",
620
+ textTransform: "uppercase",
621
+ letterSpacing: 0.5
622
+ },
623
+ searchContainer: {
624
+ flexDirection: "row",
625
+ alignItems: "center",
626
+ padding: 8,
627
+ gap: 8,
628
+ borderBottomWidth: 1,
629
+ borderBottomColor: buoyColors.border
630
+ },
631
+ searchInput: {
632
+ flex: 1,
633
+ color: buoyColors.text,
634
+ fontSize: 14,
635
+ fontFamily: "monospace"
636
+ },
637
+ clearButton: {
638
+ padding: 4
639
+ },
640
+ clearButtonText: {
641
+ color: buoyColors.textMuted,
642
+ fontSize: 18
643
+ },
644
+ closeSearchButton: {
645
+ paddingHorizontal: 8,
646
+ paddingVertical: 4
647
+ },
648
+ closeSearchText: {
649
+ color: buoyColors.primary,
650
+ fontSize: 14,
651
+ fontWeight: "600",
652
+ fontFamily: "monospace"
653
+ },
654
+ scrollView: {
655
+ flex: 1
656
+ },
657
+ scrollContent: {
658
+ paddingVertical: 8
659
+ },
660
+ metaFooter: {
661
+ flexDirection: "row",
662
+ alignItems: "center",
663
+ justifyContent: "space-between",
664
+ gap: 8,
665
+ paddingHorizontal: 16,
666
+ paddingVertical: 16,
667
+ marginTop: 16,
668
+ borderTopWidth: 1,
669
+ borderTopColor: buoyColors.border
670
+ },
671
+ metaText: {
672
+ fontSize: 12,
673
+ color: buoyColors.textSecondary,
674
+ fontFamily: "monospace"
675
+ },
676
+ sourceBadge: {
677
+ paddingHorizontal: 8,
678
+ paddingVertical: 2,
679
+ borderRadius: 999,
680
+ borderWidth: 1,
681
+ borderColor: buoyColors.border,
682
+ backgroundColor: buoyColors.card
683
+ },
684
+ sourceText: {
685
+ fontSize: 11,
686
+ color: buoyColors.textSecondary,
687
+ textTransform: "capitalize",
688
+ fontFamily: "monospace"
689
+ },
690
+ groupContainer: {
691
+ marginVertical: 4
692
+ },
693
+ groupHeader: {
694
+ flexDirection: "row",
695
+ alignItems: "center",
696
+ justifyContent: "space-between",
697
+ padding: 12,
698
+ paddingHorizontal: 16,
699
+ backgroundColor: buoyColors.card,
700
+ borderLeftWidth: 3,
701
+ borderLeftColor: buoyColors.textSecondary
702
+ },
703
+ groupHeaderLeft: {
704
+ flexDirection: "row",
705
+ alignItems: "center",
706
+ gap: 8,
707
+ flex: 1
708
+ },
709
+ groupTitle: {
710
+ fontSize: 12,
711
+ fontWeight: "700",
712
+ color: buoyColors.text,
713
+ fontFamily: "monospace",
714
+ textTransform: "uppercase",
715
+ letterSpacing: 0.8
716
+ },
717
+ groupBadge: {
718
+ backgroundColor: "#3B82F615",
719
+ borderColor: "#3B82F640",
720
+ borderWidth: 1,
721
+ borderRadius: 12,
722
+ paddingHorizontal: 8,
723
+ paddingVertical: 3,
724
+ minWidth: 24,
725
+ alignItems: "center",
726
+ justifyContent: "center"
727
+ },
728
+ groupCount: {
729
+ fontSize: 11,
730
+ fontWeight: "600",
731
+ color: "#3B82F6",
732
+ fontFamily: "monospace"
733
+ },
734
+ screensList: {
735
+ backgroundColor: buoyColors.base
736
+ },
737
+ groupDescription: {
738
+ paddingHorizontal: 16,
739
+ paddingVertical: 8,
740
+ marginBottom: 8,
741
+ backgroundColor: buoyColors.input,
742
+ borderLeftWidth: 3,
743
+ borderLeftColor: buoyColors.textSecondary
744
+ },
745
+ groupDescriptionText: {
746
+ fontSize: 11,
747
+ color: buoyColors.textSecondary,
748
+ fontFamily: "monospace",
749
+ lineHeight: 16
750
+ },
751
+ screenItem: {
752
+ marginBottom: 6
753
+ },
754
+ screenCard: {
755
+ flexDirection: "row",
756
+ alignItems: "center",
757
+ justifyContent: "space-between",
758
+ paddingVertical: 10,
759
+ paddingHorizontal: 12,
760
+ backgroundColor: buoyColors.card,
761
+ borderRadius: 6,
762
+ borderWidth: 1,
763
+ borderColor: buoyColors.border,
764
+ shadowColor: "#000",
765
+ shadowOffset: {
766
+ width: 0,
767
+ height: 1
768
+ },
769
+ shadowOpacity: 0.05,
770
+ shadowRadius: 2,
771
+ elevation: 1
772
+ },
773
+ screenHeaderLeft: {
774
+ flexDirection: "row",
775
+ alignItems: "center",
776
+ gap: 8,
777
+ flex: 1
778
+ },
779
+ expandIndicator: {
780
+ width: 20,
781
+ alignItems: "center"
782
+ },
783
+ screenName: {
784
+ fontSize: 13,
785
+ fontWeight: "600",
786
+ color: buoyColors.text,
787
+ fontFamily: "monospace",
788
+ flex: 1
789
+ },
790
+ screenHeaderActions: {
791
+ flexDirection: "row",
792
+ alignItems: "center",
793
+ gap: 6
794
+ },
795
+ statusBadge: {
796
+ borderRadius: 4,
797
+ paddingHorizontal: 6,
798
+ paddingVertical: 2,
799
+ borderWidth: 1
800
+ },
801
+ statusText: {
802
+ fontSize: 10,
803
+ fontWeight: "600",
804
+ fontFamily: "monospace"
805
+ },
806
+ headerActionButton: {
807
+ padding: 6,
808
+ borderRadius: 4,
809
+ backgroundColor: buoyColors.input,
810
+ borderWidth: 1,
811
+ borderColor: buoyColors.border,
812
+ alignItems: "center",
813
+ justifyContent: "center"
814
+ },
815
+ navigateButton: {
816
+ backgroundColor: buoyColors.primary + "15",
817
+ borderColor: buoyColors.primary + "40"
818
+ },
819
+ navigateButtonText: {
820
+ fontSize: 12,
821
+ color: buoyColors.primary,
822
+ fontFamily: "monospace",
823
+ fontWeight: "600"
824
+ }
825
+ });