rn-backstage 1.2.1 → 1.3.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 (77) hide show
  1. package/README.md +82 -2
  2. package/lib/commonjs/Backstage.js +6 -0
  3. package/lib/commonjs/Backstage.js.map +1 -1
  4. package/lib/commonjs/components/BackstagePanel.js +18 -2
  5. package/lib/commonjs/components/BackstagePanel.js.map +1 -1
  6. package/lib/commonjs/components/FlagsTab.js +11 -9
  7. package/lib/commonjs/components/FlagsTab.js.map +1 -1
  8. package/lib/commonjs/components/FloatingPill.js +82 -22
  9. package/lib/commonjs/components/FloatingPill.js.map +1 -1
  10. package/lib/commonjs/components/InfoTab.js +5 -1
  11. package/lib/commonjs/components/InfoTab.js.map +1 -1
  12. package/lib/commonjs/components/LogItem.js +6 -0
  13. package/lib/commonjs/components/LogItem.js.map +1 -1
  14. package/lib/commonjs/components/LogsTab.js +2 -0
  15. package/lib/commonjs/components/LogsTab.js.map +1 -1
  16. package/lib/commonjs/components/NetworkTab.js +2 -0
  17. package/lib/commonjs/components/NetworkTab.js.map +1 -1
  18. package/lib/commonjs/components/StorageTab.js +655 -0
  19. package/lib/commonjs/components/StorageTab.js.map +1 -0
  20. package/lib/commonjs/constants.js +72 -6
  21. package/lib/commonjs/constants.js.map +1 -1
  22. package/lib/commonjs/index.js +77 -0
  23. package/lib/commonjs/index.js.map +1 -1
  24. package/lib/commonjs/types.js +1 -0
  25. package/lib/commonjs/types.js.map +1 -1
  26. package/lib/module/Backstage.js +6 -0
  27. package/lib/module/Backstage.js.map +1 -1
  28. package/lib/module/components/BackstagePanel.js +18 -2
  29. package/lib/module/components/BackstagePanel.js.map +1 -1
  30. package/lib/module/components/FlagsTab.js +11 -9
  31. package/lib/module/components/FlagsTab.js.map +1 -1
  32. package/lib/module/components/FloatingPill.js +86 -26
  33. package/lib/module/components/FloatingPill.js.map +1 -1
  34. package/lib/module/components/InfoTab.js +6 -2
  35. package/lib/module/components/InfoTab.js.map +1 -1
  36. package/lib/module/components/LogItem.js +7 -1
  37. package/lib/module/components/LogItem.js.map +1 -1
  38. package/lib/module/components/LogsTab.js +2 -0
  39. package/lib/module/components/LogsTab.js.map +1 -1
  40. package/lib/module/components/NetworkTab.js +2 -0
  41. package/lib/module/components/NetworkTab.js.map +1 -1
  42. package/lib/module/components/StorageTab.js +650 -0
  43. package/lib/module/components/StorageTab.js.map +1 -0
  44. package/lib/module/constants.js +72 -6
  45. package/lib/module/constants.js.map +1 -1
  46. package/lib/module/index.js +13 -0
  47. package/lib/module/index.js.map +1 -1
  48. package/lib/module/types.js +2 -0
  49. package/lib/module/types.js.map +1 -1
  50. package/lib/typescript/Backstage.d.ts.map +1 -1
  51. package/lib/typescript/components/BackstagePanel.d.ts +2 -1
  52. package/lib/typescript/components/BackstagePanel.d.ts.map +1 -1
  53. package/lib/typescript/components/FloatingPill.d.ts +2 -0
  54. package/lib/typescript/components/FloatingPill.d.ts.map +1 -1
  55. package/lib/typescript/components/InfoTab.d.ts.map +1 -1
  56. package/lib/typescript/components/LogItem.d.ts.map +1 -1
  57. package/lib/typescript/components/StorageTab.d.ts +9 -0
  58. package/lib/typescript/components/StorageTab.d.ts.map +1 -0
  59. package/lib/typescript/constants.d.ts +59 -4
  60. package/lib/typescript/constants.d.ts.map +1 -1
  61. package/lib/typescript/index.d.ts +12 -1
  62. package/lib/typescript/index.d.ts.map +1 -1
  63. package/lib/typescript/types.d.ts +16 -0
  64. package/lib/typescript/types.d.ts.map +1 -1
  65. package/package.json +6 -4
  66. package/src/Backstage.tsx +6 -0
  67. package/src/components/BackstagePanel.tsx +23 -4
  68. package/src/components/FlagsTab.tsx +6 -4
  69. package/src/components/FloatingPill.tsx +122 -28
  70. package/src/components/InfoTab.tsx +6 -5
  71. package/src/components/LogItem.tsx +13 -4
  72. package/src/components/LogsTab.tsx +2 -2
  73. package/src/components/NetworkTab.tsx +2 -2
  74. package/src/components/StorageTab.tsx +607 -0
  75. package/src/constants.ts +80 -4
  76. package/src/index.ts +15 -0
  77. package/src/types.ts +22 -0
@@ -1,6 +1,15 @@
1
- import React, { useRef } from 'react'
2
- import { Animated, Dimensions, PanResponder, StyleSheet, Text } from 'react-native'
3
- import { Metrics, MonospaceFont } from '../constants'
1
+ import React, { useCallback, useRef, useState } from 'react'
2
+ import {
3
+ Animated,
4
+ Dimensions,
5
+ LayoutChangeEvent,
6
+ PanResponder,
7
+ SafeAreaView,
8
+ StyleSheet,
9
+ Text,
10
+ View,
11
+ } from 'react-native'
12
+ import { Metrics, MonospaceFont, TestIDs } from '../constants'
4
13
  import { useBackstageTheme } from '../ThemeContext'
5
14
  import type { BackstageStyleOverrides } from '../types'
6
15
 
@@ -12,6 +21,8 @@ interface FloatingPillProps {
12
21
  onPress: () => void
13
22
  initialX?: number
14
23
  initialY?: number
24
+ pillWidth?: number
25
+ pillHeight?: number
15
26
  styles?: BackstageStyleOverrides
16
27
  }
17
28
 
@@ -20,12 +31,32 @@ interface FloatingPillProps {
20
31
  const PILL_WIDTH = Metrics.pillWidth
21
32
  const PILL_HEIGHT = Metrics.pillHeight
22
33
  const DRAG_THRESHOLD = 5
34
+ const INSET_PADDING = 8 // extra padding from safe area edges
23
35
 
24
- function clampPosition(x: number, y: number): { x: number; y: number } {
36
+ interface SafeAreaInsets {
37
+ top: number
38
+ bottom: number
39
+ left: number
40
+ right: number
41
+ }
42
+
43
+ function clampPosition(
44
+ x: number,
45
+ y: number,
46
+ insets: SafeAreaInsets,
47
+ pillW: number,
48
+ pillH: number,
49
+ ): { x: number; y: number } {
25
50
  const { width, height } = Dimensions.get('window')
26
51
  return {
27
- x: Math.max(0, Math.min(x, width - PILL_WIDTH)),
28
- y: Math.max(0, Math.min(y, height - PILL_HEIGHT)),
52
+ x: Math.max(
53
+ insets.left + INSET_PADDING,
54
+ Math.min(x, width - pillW - insets.right - INSET_PADDING),
55
+ ),
56
+ y: Math.max(
57
+ insets.top + INSET_PADDING,
58
+ Math.min(y, height - pillH - insets.bottom - INSET_PADDING),
59
+ ),
29
60
  }
30
61
  }
31
62
 
@@ -37,17 +68,55 @@ export const FloatingPill: React.FC<FloatingPillProps> = ({
37
68
  onPress,
38
69
  initialX,
39
70
  initialY,
71
+ pillWidth: propWidth,
72
+ pillHeight: propHeight,
40
73
  styles: propStyles,
41
74
  }) => {
75
+ const pillW = propWidth ?? PILL_WIDTH
76
+ const pillH = propHeight ?? PILL_HEIGHT
77
+
42
78
  const { width: screenW, height: screenH } = Dimensions.get('window')
43
- const defaultX = initialX ?? screenW - PILL_WIDTH - 16
44
- const defaultY = initialY ?? screenH - PILL_HEIGHT - 120
79
+ const defaultX = initialX ?? screenW - pillW - 16
80
+ const defaultY = initialY ?? screenH - pillH - 120
45
81
 
46
82
  const pan = useRef(new Animated.ValueXY({ x: defaultX, y: defaultY })).current
47
83
  const lastPosition = useRef({ x: defaultX, y: defaultY })
48
84
  const isDragging = useRef(false)
49
85
  const dragDistance = useRef(0)
50
86
 
87
+ // Safe area insets measured from invisible SafeAreaView
88
+ const [insets, setInsets] = useState<SafeAreaInsets>({
89
+ top: 0,
90
+ bottom: 0,
91
+ left: 0,
92
+ right: 0,
93
+ })
94
+
95
+ const handleSafeAreaLayout = useCallback(
96
+ (event: LayoutChangeEvent) => {
97
+ const { x, y, width, height } = event.nativeEvent.layout
98
+ const screen = Dimensions.get('window')
99
+ const newInsets: SafeAreaInsets = {
100
+ top: y,
101
+ left: x,
102
+ bottom: screen.height - y - height,
103
+ right: screen.width - x - width,
104
+ }
105
+ setInsets(newInsets)
106
+
107
+ // Re-clamp current position to new safe area
108
+ const clamped = clampPosition(lastPosition.current.x, lastPosition.current.y, newInsets, pillW, pillH)
109
+ if (clamped.x !== lastPosition.current.x || clamped.y !== lastPosition.current.y) {
110
+ lastPosition.current = clamped
111
+ pan.setValue(clamped)
112
+ }
113
+ },
114
+ [pan],
115
+ )
116
+
117
+ const insetsRef = useRef(insets)
118
+ insetsRef.current = insets
119
+
51
120
  const panResponder = useRef(
52
121
  PanResponder.create({
53
122
  onStartShouldSetPanResponder: () => true,
@@ -72,6 +141,8 @@ export const FloatingPill: React.FC<FloatingPillProps> = ({
72
141
  if (dragDistance.current > DRAG_THRESHOLD) {
73
142
  isDragging.current = true
74
143
  }
144
+
145
+ // Allow free dragging — bounce back happens on release
75
146
  Animated.event([null, { dx: pan.x, dy: pan.y }], {
76
147
  useNativeDriver: false,
77
148
  })(_, gestureState)
@@ -82,6 +153,9 @@ export const FloatingPill: React.FC<FloatingPillProps> = ({
82
153
  const newPos = clampPosition(
83
154
  lastPosition.current.x + gestureState.dx,
84
155
  lastPosition.current.y + gestureState.dy,
156
+ insetsRef.current,
157
+ pillW,
158
+ pillH,
85
159
  )
86
160
 
87
161
  lastPosition.current = newPos
@@ -107,33 +181,45 @@ export const FloatingPill: React.FC<FloatingPillProps> = ({
107
181
  const shadowColor = hasError ? theme.error : theme.accent
108
182
 
109
183
  return (
110
- <Animated.View
111
- {...panResponder.panHandlers}
112
- style={[
113
- styles.pill,
114
- {
115
- backgroundColor,
116
- transform: pan.getTranslateTransform(),
117
- shadowColor,
118
- },
119
- propStyles?.pillStyle,
120
- ]}
121
- >
122
- <Text style={[styles.pillText, propStyles?.pillTextStyle]} numberOfLines={1}>
123
- {text}
124
- </Text>
125
- </Animated.View>
184
+ <>
185
+ {/* Invisible SafeAreaView to measure insets */}
186
+ <SafeAreaView style={componentStyles.measurer} pointerEvents="none">
187
+ <View style={componentStyles.measurerInner} onLayout={handleSafeAreaLayout} />
188
+ </SafeAreaView>
189
+
190
+ <Animated.View
191
+ testID={TestIDs.floatingPill}
192
+ {...panResponder.panHandlers}
193
+ style={[
194
+ componentStyles.pill,
195
+ {
196
+ minWidth: pillW,
197
+ height: pillH,
198
+ borderRadius: pillH / 2,
199
+ backgroundColor,
200
+ transform: pan.getTranslateTransform(),
201
+ shadowColor,
202
+ },
203
+ propStyles?.pillStyle,
204
+ ]}
205
+ >
206
+ <Text
207
+ testID={TestIDs.floatingPillText}
208
+ style={[componentStyles.pillText, propStyles?.pillTextStyle]}
209
+ numberOfLines={1}
210
+ >
211
+ {text}
212
+ </Text>
213
+ </Animated.View>
214
+ </>
126
215
  )
127
216
  }
128
217
 
129
218
  // ─── Styles ──────────────────────────────────────────────────────────────────
130
219
 
131
- const styles = StyleSheet.create({
220
+ const componentStyles = StyleSheet.create({
132
221
  pill: {
133
222
  position: 'absolute',
134
- minWidth: PILL_WIDTH,
135
- height: PILL_HEIGHT,
136
- borderRadius: PILL_HEIGHT / 2,
137
223
  paddingHorizontal: 14,
138
224
  alignItems: 'center',
139
225
  justifyContent: 'center',
@@ -150,4 +236,12 @@ const styles = StyleSheet.create({
150
236
  color: '#FFFFFF',
151
237
  letterSpacing: 0.5,
152
238
  },
239
+ measurer: {
240
+ ...StyleSheet.absoluteFillObject,
241
+ zIndex: -1,
242
+ opacity: 0,
243
+ },
244
+ measurerInner: {
245
+ flex: 1,
246
+ },
153
247
  })
@@ -1,6 +1,6 @@
1
1
  import React, { useMemo } from 'react'
2
2
  import { Platform, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
3
- import { MonospaceFont } from '../constants'
3
+ import { MonospaceFont, TestIDs } from '../constants'
4
4
  import { useBackstageTheme } from '../ThemeContext'
5
5
  import { JsonTreeView } from './JsonTreeView'
6
6
  import type { AppInfoItem, BackstageStyleOverrides, BackstageTheme, QuickAction } from '../types'
@@ -57,12 +57,13 @@ export const InfoTab: React.FC<InfoTabProps> = ({
57
57
 
58
58
  return (
59
59
  <ScrollView
60
+ testID={TestIDs.infoTab.container}
60
61
  style={s.container}
61
62
  contentContainerStyle={s.scrollContent}
62
63
  showsVerticalScrollIndicator={false}
63
64
  >
64
65
  {/* ── Device Info Section ─────────────────────────────────── */}
65
- <View style={s.section}>
66
+ <View testID={TestIDs.infoTab.deviceInfo} style={s.section}>
66
67
  <Text style={[s.sectionTitle, propStyles?.sectionTitleStyle]}>DEVICE INFO</Text>
67
68
  <View style={s.card}>
68
69
  {allInfo.map((item, index) => (
@@ -83,7 +84,7 @@ export const InfoTab: React.FC<InfoTabProps> = ({
83
84
 
84
85
  {/* ── State Tree Section ──────────────────────────────────── */}
85
86
  {state && Object.keys(state).length > 0 && (
86
- <View style={s.section}>
87
+ <View testID={TestIDs.infoTab.stateTree} style={s.section}>
87
88
  <Text style={[s.sectionTitle, propStyles?.sectionTitleStyle]}>STATE TREE</Text>
88
89
  <View style={s.card}>
89
90
  <JsonTreeView data={state} hideRoot maxDepth={jsonMaxDepth} />
@@ -93,7 +94,7 @@ export const InfoTab: React.FC<InfoTabProps> = ({
93
94
 
94
95
  {/* ── Quick Actions Section ─────────────────────────────────── */}
95
96
  {quickActions.length > 0 && (
96
- <View style={s.section}>
97
+ <View testID={TestIDs.infoTab.quickActions} style={s.section}>
97
98
  <Text style={[s.sectionTitle, propStyles?.sectionTitleStyle]}>QUICK ACTIONS</Text>
98
99
  <View style={s.actionsGrid}>
99
100
  {quickActions.map((action, index) => {
@@ -106,7 +107,7 @@ export const InfoTab: React.FC<InfoTabProps> = ({
106
107
  return (
107
108
  <TouchableOpacity
108
109
  key={`action_${index}`}
109
- testID={action.testID}
110
+ testID={action.testID || TestIDs.infoTab.actionButton(index)}
110
111
  style={[
111
112
  s.actionButton,
112
113
  action.destructive && s.actionButtonDestructive,
@@ -1,6 +1,6 @@
1
1
  import React, { useCallback, useMemo, useState } from 'react'
2
2
  import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'
3
- import { MonospaceFont } from '../constants'
3
+ import { MonospaceFont, TestIDs } from '../constants'
4
4
  import { useBackstageTheme } from '../ThemeContext'
5
5
  import { LogLevel } from '../types'
6
6
  import type { LogEntry, BackstageStyleOverrides, BackstageTheme } from '../types'
@@ -74,20 +74,28 @@ export const LogItem: React.FC<LogItemProps> = React.memo(
74
74
 
75
75
  return (
76
76
  <TouchableOpacity
77
+ testID={TestIDs.logItem.container(item.id)}
77
78
  style={[s.container, { backgroundColor: badge.rowBg }]}
78
79
  onPress={hasData ? toggleExpand : undefined}
79
80
  onLongPress={handleCopy}
80
81
  activeOpacity={hasData ? 0.7 : 1}
81
82
  >
82
83
  <View style={s.headerRow}>
83
- <View style={[s.badge, { backgroundColor: badge.bgColor }]}>
84
+ <View
85
+ testID={TestIDs.logItem.badge(item.id)}
86
+ style={[s.badge, { backgroundColor: badge.bgColor }]}
87
+ >
84
88
  <Text style={[s.badgeText, { color: badge.color }]}>{badge.label}</Text>
85
89
  </View>
86
- <Text style={[s.timestamp, propStyles?.logTimestampStyle]}>
90
+ <Text
91
+ testID={TestIDs.logItem.timestamp(item.id)}
92
+ style={[s.timestamp, propStyles?.logTimestampStyle]}
93
+ >
87
94
  {formatTimestamp(item.timestamp)}
88
95
  </Text>
89
96
  {onCopy && (
90
97
  <TouchableOpacity
98
+ testID={TestIDs.logItem.copyButton(item.id)}
91
99
  style={s.copyButton}
92
100
  onPress={handleCopy}
93
101
  hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
@@ -98,6 +106,7 @@ export const LogItem: React.FC<LogItemProps> = React.memo(
98
106
  </View>
99
107
 
100
108
  <Text
109
+ testID={TestIDs.logItem.message(item.id)}
101
110
  style={[s.message, propStyles?.logMessageStyle]}
102
111
  numberOfLines={expanded ? undefined : 3}
103
112
  >
@@ -105,7 +114,7 @@ export const LogItem: React.FC<LogItemProps> = React.memo(
105
114
  </Text>
106
115
 
107
116
  {expanded && hasData && (
108
- <View style={s.dataContainer}>
117
+ <View testID={TestIDs.logItem.dataContainer(item.id)} style={s.dataContainer}>
109
118
  <JsonTreeView data={item.data} hideRoot maxDepth={jsonMaxDepth} />
110
119
  </View>
111
120
  )}
@@ -69,7 +69,7 @@ export const LogsTab: React.FC<LogsTabProps> = ({
69
69
  const keyExtractor = useCallback((item: LogEntry) => item.id, [])
70
70
 
71
71
  return (
72
- <View style={s.container}>
72
+ <View testID={TestIDs.logsTab.container} style={s.container}>
73
73
  <View style={s.searchContainer}>
74
74
  <View style={s.searchInputWrapper}>
75
75
  <Text style={s.searchIcon}>⌕</Text>
@@ -98,7 +98,7 @@ export const LogsTab: React.FC<LogsTabProps> = ({
98
98
  )}
99
99
  </View>
100
100
 
101
- <View style={s.countBar}>
101
+ <View testID={TestIDs.logsTab.statsBar} style={s.countBar}>
102
102
  <Text style={s.countText}>
103
103
  {filteredLogs.length === logs.length
104
104
  ? `${logs.length} log${logs.length !== 1 ? 's' : ''}`
@@ -68,7 +68,7 @@ export const NetworkTab: React.FC<NetworkTabProps> = ({
68
68
  const keyExtractor = useCallback((item: NetworkEntry) => item.id, [])
69
69
 
70
70
  return (
71
- <View style={s.container}>
71
+ <View testID={TestIDs.networkTab.container} style={s.container}>
72
72
  <View style={s.searchContainer}>
73
73
  <View style={s.searchInputWrapper}>
74
74
  <Text style={s.searchIcon}>⌕</Text>
@@ -95,7 +95,7 @@ export const NetworkTab: React.FC<NetworkTabProps> = ({
95
95
  </TouchableOpacity>
96
96
  </View>
97
97
 
98
- <View style={s.statsBar}>
98
+ <View testID={TestIDs.networkTab.statsBar} style={s.statsBar}>
99
99
  <View style={s.statsRow}>
100
100
  <Text style={s.statText}>
101
101
  {filteredEntries.length === entries.length