rn-backstage 1.0.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 (102) hide show
  1. package/README.md +80 -0
  2. package/lib/commonjs/Backstage.js +130 -0
  3. package/lib/commonjs/Backstage.js.map +1 -0
  4. package/lib/commonjs/components/BackstagePanel.js +192 -0
  5. package/lib/commonjs/components/BackstagePanel.js.map +1 -0
  6. package/lib/commonjs/components/FloatingPill.js +147 -0
  7. package/lib/commonjs/components/FloatingPill.js.map +1 -0
  8. package/lib/commonjs/components/InfoTab.js +260 -0
  9. package/lib/commonjs/components/InfoTab.js.map +1 -0
  10. package/lib/commonjs/components/JsonTreeView.js +245 -0
  11. package/lib/commonjs/components/JsonTreeView.js.map +1 -0
  12. package/lib/commonjs/components/LogItem.js +191 -0
  13. package/lib/commonjs/components/LogItem.js.map +1 -0
  14. package/lib/commonjs/components/LogsTab.js +235 -0
  15. package/lib/commonjs/components/LogsTab.js.map +1 -0
  16. package/lib/commonjs/components/TabBar.js +132 -0
  17. package/lib/commonjs/components/TabBar.js.map +1 -0
  18. package/lib/commonjs/constants.js +89 -0
  19. package/lib/commonjs/constants.js.map +1 -0
  20. package/lib/commonjs/index.js +20 -0
  21. package/lib/commonjs/index.js.map +1 -0
  22. package/lib/commonjs/log-interceptor.js +114 -0
  23. package/lib/commonjs/log-interceptor.js.map +1 -0
  24. package/lib/commonjs/package.json +1 -0
  25. package/lib/commonjs/types.js +21 -0
  26. package/lib/commonjs/types.js.map +1 -0
  27. package/lib/commonjs/utils/formatTimestamp.js +26 -0
  28. package/lib/commonjs/utils/formatTimestamp.js.map +1 -0
  29. package/lib/commonjs/utils/stringify.js +78 -0
  30. package/lib/commonjs/utils/stringify.js.map +1 -0
  31. package/lib/module/Backstage.js +125 -0
  32. package/lib/module/Backstage.js.map +1 -0
  33. package/lib/module/components/BackstagePanel.js +187 -0
  34. package/lib/module/components/BackstagePanel.js.map +1 -0
  35. package/lib/module/components/FloatingPill.js +142 -0
  36. package/lib/module/components/FloatingPill.js.map +1 -0
  37. package/lib/module/components/InfoTab.js +255 -0
  38. package/lib/module/components/InfoTab.js.map +1 -0
  39. package/lib/module/components/JsonTreeView.js +241 -0
  40. package/lib/module/components/JsonTreeView.js.map +1 -0
  41. package/lib/module/components/LogItem.js +186 -0
  42. package/lib/module/components/LogItem.js.map +1 -0
  43. package/lib/module/components/LogsTab.js +230 -0
  44. package/lib/module/components/LogsTab.js.map +1 -0
  45. package/lib/module/components/TabBar.js +127 -0
  46. package/lib/module/components/TabBar.js.map +1 -0
  47. package/lib/module/constants.js +85 -0
  48. package/lib/module/constants.js.map +1 -0
  49. package/lib/module/index.js +5 -0
  50. package/lib/module/index.js.map +1 -0
  51. package/lib/module/log-interceptor.js +108 -0
  52. package/lib/module/log-interceptor.js.map +1 -0
  53. package/lib/module/package.json +1 -0
  54. package/lib/module/types.js +25 -0
  55. package/lib/module/types.js.map +1 -0
  56. package/lib/module/utils/formatTimestamp.js +22 -0
  57. package/lib/module/utils/formatTimestamp.js.map +1 -0
  58. package/lib/module/utils/stringify.js +73 -0
  59. package/lib/module/utils/stringify.js.map +1 -0
  60. package/lib/typescript/Backstage.d.ts +4 -0
  61. package/lib/typescript/Backstage.d.ts.map +1 -0
  62. package/lib/typescript/components/BackstagePanel.d.ts +21 -0
  63. package/lib/typescript/components/BackstagePanel.d.ts.map +1 -0
  64. package/lib/typescript/components/FloatingPill.d.ts +13 -0
  65. package/lib/typescript/components/FloatingPill.d.ts.map +1 -0
  66. package/lib/typescript/components/InfoTab.d.ts +16 -0
  67. package/lib/typescript/components/InfoTab.d.ts.map +1 -0
  68. package/lib/typescript/components/JsonTreeView.d.ts +10 -0
  69. package/lib/typescript/components/JsonTreeView.d.ts.map +1 -0
  70. package/lib/typescript/components/LogItem.d.ts +10 -0
  71. package/lib/typescript/components/LogItem.d.ts.map +1 -0
  72. package/lib/typescript/components/LogsTab.d.ts +11 -0
  73. package/lib/typescript/components/LogsTab.d.ts.map +1 -0
  74. package/lib/typescript/components/TabBar.d.ts +14 -0
  75. package/lib/typescript/components/TabBar.d.ts.map +1 -0
  76. package/lib/typescript/constants.d.ts +43 -0
  77. package/lib/typescript/constants.d.ts.map +1 -0
  78. package/lib/typescript/index.d.ts +4 -0
  79. package/lib/typescript/index.d.ts.map +1 -0
  80. package/lib/typescript/log-interceptor.d.ts +16 -0
  81. package/lib/typescript/log-interceptor.d.ts.map +1 -0
  82. package/lib/typescript/types.d.ts +110 -0
  83. package/lib/typescript/types.d.ts.map +1 -0
  84. package/lib/typescript/utils/formatTimestamp.d.ts +7 -0
  85. package/lib/typescript/utils/formatTimestamp.d.ts.map +1 -0
  86. package/lib/typescript/utils/stringify.d.ts +9 -0
  87. package/lib/typescript/utils/stringify.d.ts.map +1 -0
  88. package/package.json +91 -0
  89. package/src/Backstage.tsx +145 -0
  90. package/src/components/BackstagePanel.tsx +208 -0
  91. package/src/components/FloatingPill.tsx +150 -0
  92. package/src/components/InfoTab.tsx +264 -0
  93. package/src/components/JsonTreeView.tsx +259 -0
  94. package/src/components/LogItem.tsx +189 -0
  95. package/src/components/LogsTab.tsx +239 -0
  96. package/src/components/TabBar.tsx +150 -0
  97. package/src/constants.ts +82 -0
  98. package/src/index.ts +14 -0
  99. package/src/log-interceptor.ts +137 -0
  100. package/src/types.ts +147 -0
  101. package/src/utils/formatTimestamp.ts +22 -0
  102. package/src/utils/stringify.ts +90 -0
@@ -0,0 +1,208 @@
1
+ import React, { useState } from 'react'
2
+ import {
3
+ Modal,
4
+ SafeAreaView,
5
+ StatusBar,
6
+ StyleSheet,
7
+ Text,
8
+ TouchableOpacity,
9
+ View,
10
+ } from 'react-native'
11
+ import { DarkTheme, MonospaceFont, TestIDs } from '../constants'
12
+ import { TabBar } from './TabBar'
13
+ import { InfoTab } from './InfoTab'
14
+ import { LogsTab } from './LogsTab'
15
+ import type {
16
+ AppInfoItem,
17
+ BackstageStyleOverrides,
18
+ BackstageTab,
19
+ LogEntry,
20
+ QuickAction,
21
+ } from '../types'
22
+
23
+ // ─── Types ───────────────────────────────────────────────────────────────────
24
+
25
+ interface BackstagePanelProps {
26
+ visible: boolean
27
+ onClose: () => void
28
+
29
+ // Info tab props
30
+ appVersion?: string
31
+ buildNumber?: string
32
+ bundleId?: string
33
+ deviceInfo?: AppInfoItem[]
34
+ state?: Record<string, unknown>
35
+ quickActions?: QuickAction[]
36
+
37
+ // Logs tab props
38
+ logs: LogEntry[]
39
+ onRefreshLogs: () => void
40
+ onCopyLogs?: (logs: string) => void
41
+
42
+ // Extensibility
43
+ extraTabs?: BackstageTab[]
44
+
45
+ // Styling
46
+ styles?: BackstageStyleOverrides
47
+ children?: React.ReactNode
48
+ }
49
+
50
+ // ─── Built-in Tab Definitions ────────────────────────────────────────────────
51
+
52
+ const BUILT_IN_TABS = [
53
+ { key: 'info', title: 'Info', icon: 'ℹ️' },
54
+ { key: 'logs', title: 'Logs', icon: '📋' },
55
+ ]
56
+
57
+ // ─── Component ───────────────────────────────────────────────────────────────
58
+
59
+ export const BackstagePanel: React.FC<BackstagePanelProps> = ({
60
+ visible,
61
+ onClose,
62
+ appVersion,
63
+ buildNumber,
64
+ bundleId,
65
+ deviceInfo,
66
+ state,
67
+ quickActions,
68
+ logs,
69
+ onRefreshLogs,
70
+ onCopyLogs,
71
+ extraTabs = [],
72
+ styles: propStyles,
73
+ children,
74
+ }) => {
75
+ const [activeTab, setActiveTab] = useState('info')
76
+
77
+ // Compose all tabs: built-in + extra
78
+ const allTabs = [
79
+ ...BUILT_IN_TABS,
80
+ ...extraTabs.map(t => ({ key: t.key, title: t.title, icon: t.icon })),
81
+ ]
82
+
83
+ // Render active tab content
84
+ const renderTabContent = () => {
85
+ switch (activeTab) {
86
+ case 'info':
87
+ return (
88
+ <InfoTab
89
+ appVersion={appVersion}
90
+ buildNumber={buildNumber}
91
+ bundleId={bundleId}
92
+ deviceInfo={deviceInfo}
93
+ state={state}
94
+ quickActions={quickActions}
95
+ onClosePanel={onClose}
96
+ styles={propStyles}
97
+ >
98
+ {children}
99
+ </InfoTab>
100
+ )
101
+ case 'logs':
102
+ return (
103
+ <LogsTab
104
+ logs={logs}
105
+ onRefresh={onRefreshLogs}
106
+ onCopyLogs={onCopyLogs}
107
+ styles={propStyles}
108
+ />
109
+ )
110
+ default: {
111
+ // Render extra tab content
112
+ const extraTab = extraTabs.find(t => t.key === activeTab)
113
+ if (extraTab) {
114
+ return <>{extraTab.render()}</>
115
+ }
116
+ return null
117
+ }
118
+ }
119
+ }
120
+
121
+ return (
122
+ <Modal
123
+ testID={TestIDs.panel}
124
+ animationType="slide"
125
+ transparent={false}
126
+ visible={visible}
127
+ onRequestClose={onClose}
128
+ statusBarTranslucent
129
+ >
130
+ <StatusBar barStyle="light-content" backgroundColor={DarkTheme.background} />
131
+ <SafeAreaView style={[styles.safeArea, propStyles?.panelStyle]}>
132
+ {/* ── Header ──────────────────────────────────────────── */}
133
+ <View style={styles.header}>
134
+ <View style={styles.headerLeft}>
135
+ <Text style={styles.headerBrand}>⚙</Text>
136
+ <Text style={[styles.headerTitle, propStyles?.headerTitleStyle]}>Backstage</Text>
137
+ </View>
138
+ <TouchableOpacity
139
+ testID={TestIDs.header.closeButton}
140
+ style={styles.closeButton}
141
+ onPress={onClose}
142
+ activeOpacity={0.7}
143
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
144
+ >
145
+ <Text style={styles.closeButtonText}>✕</Text>
146
+ </TouchableOpacity>
147
+ </View>
148
+
149
+ {/* ── Tab Bar ─────────────────────────────────────────── */}
150
+ <TabBar tabs={allTabs} activeTab={activeTab} onTabChange={setActiveTab} />
151
+
152
+ {/* ── Tab Content ─────────────────────────────────────── */}
153
+ <View style={styles.content}>{renderTabContent()}</View>
154
+ </SafeAreaView>
155
+ </Modal>
156
+ )
157
+ }
158
+
159
+ // ─── Styles ──────────────────────────────────────────────────────────────────
160
+
161
+ const styles = StyleSheet.create({
162
+ safeArea: {
163
+ flex: 1,
164
+ backgroundColor: DarkTheme.background,
165
+ },
166
+ header: {
167
+ flexDirection: 'row',
168
+ alignItems: 'center',
169
+ justifyContent: 'space-between',
170
+ paddingHorizontal: 16,
171
+ paddingVertical: 12,
172
+ borderBottomWidth: 1,
173
+ borderBottomColor: DarkTheme.border,
174
+ },
175
+ headerLeft: {
176
+ flexDirection: 'row',
177
+ alignItems: 'center',
178
+ },
179
+ headerBrand: {
180
+ fontSize: 20,
181
+ marginRight: 8,
182
+ },
183
+ headerTitle: {
184
+ fontFamily: MonospaceFont,
185
+ fontSize: 18,
186
+ fontWeight: '800',
187
+ color: DarkTheme.text,
188
+ letterSpacing: 1,
189
+ },
190
+ closeButton: {
191
+ width: 32,
192
+ height: 32,
193
+ borderRadius: 16,
194
+ backgroundColor: DarkTheme.surfaceElevated,
195
+ borderWidth: 1,
196
+ borderColor: DarkTheme.border,
197
+ alignItems: 'center',
198
+ justifyContent: 'center',
199
+ },
200
+ closeButtonText: {
201
+ fontSize: 14,
202
+ color: DarkTheme.textSecondary,
203
+ fontWeight: '700',
204
+ },
205
+ content: {
206
+ flex: 1,
207
+ },
208
+ })
@@ -0,0 +1,150 @@
1
+ import React, { useRef } from 'react'
2
+ import { Animated, Dimensions, PanResponder, StyleSheet, Text } from 'react-native'
3
+ import { DarkTheme, Metrics, MonospaceFont } from '../constants'
4
+ import type { BackstageStyleOverrides } from '../types'
5
+
6
+ // ─── Types ───────────────────────────────────────────────────────────────────
7
+
8
+ interface FloatingPillProps {
9
+ text: string
10
+ hasError: boolean
11
+ onPress: () => void
12
+ initialX?: number
13
+ initialY?: number
14
+ styles?: BackstageStyleOverrides
15
+ }
16
+
17
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
18
+
19
+ const PILL_WIDTH = Metrics.pillWidth
20
+ const PILL_HEIGHT = Metrics.pillHeight
21
+ const DRAG_THRESHOLD = 5
22
+
23
+ function clampPosition(x: number, y: number): { x: number; y: number } {
24
+ const { width, height } = Dimensions.get('window')
25
+ return {
26
+ x: Math.max(0, Math.min(x, width - PILL_WIDTH)),
27
+ y: Math.max(0, Math.min(y, height - PILL_HEIGHT)),
28
+ }
29
+ }
30
+
31
+ // ─── Component ───────────────────────────────────────────────────────────────
32
+
33
+ export const FloatingPill: React.FC<FloatingPillProps> = ({
34
+ text,
35
+ hasError,
36
+ onPress,
37
+ initialX,
38
+ initialY,
39
+ styles: propStyles,
40
+ }) => {
41
+ const { width: screenW, height: screenH } = Dimensions.get('window')
42
+ const defaultX = initialX ?? screenW - PILL_WIDTH - 16
43
+ const defaultY = initialY ?? screenH - PILL_HEIGHT - 120
44
+
45
+ const pan = useRef(new Animated.ValueXY({ x: defaultX, y: defaultY })).current
46
+ const lastPosition = useRef({ x: defaultX, y: defaultY })
47
+ const isDragging = useRef(false)
48
+ const dragDistance = useRef(0)
49
+
50
+ const panResponder = useRef(
51
+ PanResponder.create({
52
+ onStartShouldSetPanResponder: () => true,
53
+ onMoveShouldSetPanResponder: (_, gestureState) => {
54
+ return (
55
+ Math.abs(gestureState.dx) > DRAG_THRESHOLD || Math.abs(gestureState.dy) > DRAG_THRESHOLD
56
+ )
57
+ },
58
+ onPanResponderGrant: () => {
59
+ isDragging.current = false
60
+ dragDistance.current = 0
61
+ pan.setOffset({
62
+ x: lastPosition.current.x,
63
+ y: lastPosition.current.y,
64
+ })
65
+ pan.setValue({ x: 0, y: 0 })
66
+ },
67
+ onPanResponderMove: (_, gestureState) => {
68
+ dragDistance.current = Math.sqrt(
69
+ gestureState.dx * gestureState.dx + gestureState.dy * gestureState.dy,
70
+ )
71
+ if (dragDistance.current > DRAG_THRESHOLD) {
72
+ isDragging.current = true
73
+ }
74
+ Animated.event([null, { dx: pan.x, dy: pan.y }], {
75
+ useNativeDriver: false,
76
+ })(_, gestureState)
77
+ },
78
+ onPanResponderRelease: (_, gestureState) => {
79
+ pan.flattenOffset()
80
+
81
+ const newPos = clampPosition(
82
+ lastPosition.current.x + gestureState.dx,
83
+ lastPosition.current.y + gestureState.dy,
84
+ )
85
+
86
+ lastPosition.current = newPos
87
+
88
+ Animated.spring(pan, {
89
+ toValue: newPos,
90
+ useNativeDriver: false,
91
+ friction: 7,
92
+ tension: 40,
93
+ }).start()
94
+
95
+ // Only trigger tap if drag distance was minimal
96
+ if (!isDragging.current) {
97
+ onPress()
98
+ }
99
+ },
100
+ }),
101
+ ).current
102
+
103
+ const backgroundColor = hasError ? DarkTheme.error : DarkTheme.accent
104
+ const shadowColor = hasError ? DarkTheme.error : DarkTheme.accent
105
+
106
+ return (
107
+ <Animated.View
108
+ {...panResponder.panHandlers}
109
+ style={[
110
+ styles.pill,
111
+ {
112
+ backgroundColor,
113
+ transform: pan.getTranslateTransform(),
114
+ shadowColor,
115
+ },
116
+ propStyles?.pillStyle,
117
+ ]}
118
+ >
119
+ <Text style={[styles.pillText, propStyles?.pillTextStyle]} numberOfLines={1}>
120
+ {text}
121
+ </Text>
122
+ </Animated.View>
123
+ )
124
+ }
125
+
126
+ // ─── Styles ──────────────────────────────────────────────────────────────────
127
+
128
+ const styles = StyleSheet.create({
129
+ pill: {
130
+ position: 'absolute',
131
+ minWidth: PILL_WIDTH,
132
+ height: PILL_HEIGHT,
133
+ borderRadius: PILL_HEIGHT / 2,
134
+ paddingHorizontal: 14,
135
+ alignItems: 'center',
136
+ justifyContent: 'center',
137
+ zIndex: 99999,
138
+ elevation: 10,
139
+ shadowOffset: { width: 0, height: 4 },
140
+ shadowOpacity: 0.4,
141
+ shadowRadius: 8,
142
+ },
143
+ pillText: {
144
+ fontFamily: MonospaceFont,
145
+ fontSize: 12,
146
+ fontWeight: '700',
147
+ color: '#FFFFFF',
148
+ letterSpacing: 0.5,
149
+ },
150
+ })
@@ -0,0 +1,264 @@
1
+ import React from 'react'
2
+ import { Platform, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
3
+ import { DarkTheme, MonospaceFont } from '../constants'
4
+ import { JsonTreeView } from './JsonTreeView'
5
+ import type { AppInfoItem, BackstageStyleOverrides, QuickAction } from '../types'
6
+
7
+ // ─── Types ───────────────────────────────────────────────────────────────────
8
+
9
+ interface InfoTabProps {
10
+ appVersion?: string
11
+ buildNumber?: string
12
+ bundleId?: string
13
+ deviceInfo?: AppInfoItem[]
14
+ state?: Record<string, unknown>
15
+ quickActions?: QuickAction[]
16
+ onClosePanel?: () => void
17
+ styles?: BackstageStyleOverrides
18
+ children?: React.ReactNode
19
+ }
20
+
21
+ // ─── Info Row ────────────────────────────────────────────────────────────────
22
+
23
+ const InfoRow: React.FC<{
24
+ label: string
25
+ value: string
26
+ labelStyle?: BackstageStyleOverrides['infoLabelStyle']
27
+ valueStyle?: BackstageStyleOverrides['infoValueStyle']
28
+ }> = ({ label, value, labelStyle, valueStyle }) => (
29
+ <View style={styles.infoRow}>
30
+ <Text style={[styles.infoLabel, labelStyle]} numberOfLines={1}>
31
+ {label}
32
+ </Text>
33
+ <Text style={[styles.infoValue, valueStyle]} numberOfLines={2} selectable>
34
+ {value}
35
+ </Text>
36
+ </View>
37
+ )
38
+
39
+ // ─── Section Header ──────────────────────────────────────────────────────────
40
+
41
+ const SectionHeader: React.FC<{
42
+ title: string
43
+ titleStyle?: BackstageStyleOverrides['sectionTitleStyle']
44
+ }> = ({ title, titleStyle }) => <Text style={[styles.sectionTitle, titleStyle]}>{title}</Text>
45
+
46
+ // ─── Action Button ───────────────────────────────────────────────────────────
47
+
48
+ const ActionButton: React.FC<{
49
+ action: QuickAction
50
+ onClosePanel?: () => void
51
+ buttonStyle?: BackstageStyleOverrides['actionButtonStyle']
52
+ buttonTitleStyle?: BackstageStyleOverrides['actionButtonTitleStyle']
53
+ }> = ({ action, onClosePanel, buttonStyle, buttonTitleStyle }) => {
54
+ const handlePress = () => {
55
+ action.onPress()
56
+ if (action.closeOnPress && onClosePanel) {
57
+ onClosePanel()
58
+ }
59
+ }
60
+
61
+ return (
62
+ <TouchableOpacity
63
+ testID={action.testID}
64
+ style={[
65
+ styles.actionButton,
66
+ action.destructive && styles.actionButtonDestructive,
67
+ buttonStyle,
68
+ ]}
69
+ onPress={handlePress}
70
+ activeOpacity={0.7}
71
+ >
72
+ {action.icon && <Text style={styles.actionIcon}>{action.icon}</Text>}
73
+ <Text
74
+ style={[
75
+ styles.actionButtonTitle,
76
+ action.destructive && styles.actionButtonTitleDestructive,
77
+ buttonTitleStyle,
78
+ ]}
79
+ >
80
+ {action.title}
81
+ </Text>
82
+ </TouchableOpacity>
83
+ )
84
+ }
85
+
86
+ // ─── Main Component ──────────────────────────────────────────────────────────
87
+
88
+ export const InfoTab: React.FC<InfoTabProps> = ({
89
+ appVersion,
90
+ buildNumber,
91
+ bundleId,
92
+ deviceInfo = [],
93
+ state,
94
+ quickActions = [],
95
+ onClosePanel,
96
+ styles: propStyles,
97
+ children,
98
+ }) => {
99
+ // Built-in device info from Platform API
100
+ const builtInInfo: AppInfoItem[] = [
101
+ { label: 'Platform', value: Platform.OS.toUpperCase() },
102
+ { label: 'OS Version', value: String(Platform.Version) },
103
+ ]
104
+
105
+ if (appVersion) {
106
+ builtInInfo.push({ label: 'App Version', value: appVersion })
107
+ }
108
+ if (buildNumber) {
109
+ builtInInfo.push({ label: 'Build Number', value: buildNumber })
110
+ }
111
+ if (bundleId) {
112
+ builtInInfo.push({ label: 'Bundle ID', value: bundleId })
113
+ }
114
+
115
+ const allInfo = [...builtInInfo, ...deviceInfo]
116
+
117
+ return (
118
+ <ScrollView
119
+ style={styles.container}
120
+ contentContainerStyle={styles.scrollContent}
121
+ showsVerticalScrollIndicator={false}
122
+ >
123
+ {/* ── Device Info Section ─────────────────────────────────── */}
124
+ <View style={styles.section}>
125
+ <SectionHeader title="DEVICE INFO" titleStyle={propStyles?.sectionTitleStyle} />
126
+ <View style={styles.card}>
127
+ {allInfo.map((item, index) => (
128
+ <React.Fragment key={`info_${index}`}>
129
+ <InfoRow
130
+ label={item.label}
131
+ value={item.value}
132
+ labelStyle={propStyles?.infoLabelStyle}
133
+ valueStyle={propStyles?.infoValueStyle}
134
+ />
135
+ {index < allInfo.length - 1 && <View style={styles.divider} />}
136
+ </React.Fragment>
137
+ ))}
138
+ </View>
139
+ </View>
140
+
141
+ {/* ── State Tree Section ──────────────────────────────────── */}
142
+ {state && Object.keys(state).length > 0 && (
143
+ <View style={styles.section}>
144
+ <SectionHeader title="STATE TREE" titleStyle={propStyles?.sectionTitleStyle} />
145
+ <View style={styles.card}>
146
+ <JsonTreeView data={state} hideRoot />
147
+ </View>
148
+ </View>
149
+ )}
150
+
151
+ {/* ── Quick Actions Section ───────────────────────────────── */}
152
+ {quickActions.length > 0 && (
153
+ <View style={styles.section}>
154
+ <SectionHeader title="QUICK ACTIONS" titleStyle={propStyles?.sectionTitleStyle} />
155
+ <View style={styles.actionsGrid}>
156
+ {quickActions.map((action, index) => (
157
+ <ActionButton
158
+ key={`action_${index}`}
159
+ action={action}
160
+ onClosePanel={onClosePanel}
161
+ buttonStyle={propStyles?.actionButtonStyle}
162
+ buttonTitleStyle={propStyles?.actionButtonTitleStyle}
163
+ />
164
+ ))}
165
+ </View>
166
+ </View>
167
+ )}
168
+
169
+ {/* ── Custom Children ─────────────────────────────────────── */}
170
+ {children && <View style={styles.section}>{children}</View>}
171
+ </ScrollView>
172
+ )
173
+ }
174
+
175
+ // ─── Styles ──────────────────────────────────────────────────────────────────
176
+
177
+ const styles = StyleSheet.create({
178
+ container: {
179
+ flex: 1,
180
+ },
181
+ scrollContent: {
182
+ padding: 16,
183
+ paddingBottom: 32,
184
+ },
185
+ section: {
186
+ marginBottom: 20,
187
+ },
188
+ sectionTitle: {
189
+ fontFamily: MonospaceFont,
190
+ fontSize: 11,
191
+ fontWeight: '700',
192
+ color: DarkTheme.textMuted,
193
+ letterSpacing: 1.5,
194
+ marginBottom: 8,
195
+ paddingLeft: 4,
196
+ },
197
+ card: {
198
+ backgroundColor: DarkTheme.surfaceElevated,
199
+ borderRadius: 12,
200
+ borderWidth: 1,
201
+ borderColor: DarkTheme.border,
202
+ padding: 12,
203
+ overflow: 'hidden',
204
+ },
205
+ infoRow: {
206
+ flexDirection: 'row',
207
+ justifyContent: 'space-between',
208
+ alignItems: 'center',
209
+ paddingVertical: 8,
210
+ paddingHorizontal: 4,
211
+ },
212
+ infoLabel: {
213
+ fontFamily: MonospaceFont,
214
+ fontSize: 13,
215
+ color: DarkTheme.textSecondary,
216
+ flex: 1,
217
+ },
218
+ infoValue: {
219
+ fontFamily: MonospaceFont,
220
+ fontSize: 13,
221
+ color: DarkTheme.text,
222
+ fontWeight: '600',
223
+ textAlign: 'right',
224
+ flex: 1,
225
+ marginLeft: 12,
226
+ },
227
+ divider: {
228
+ height: 1,
229
+ backgroundColor: DarkTheme.border,
230
+ marginHorizontal: 4,
231
+ },
232
+ actionsGrid: {
233
+ flexDirection: 'row',
234
+ flexWrap: 'wrap',
235
+ gap: 10,
236
+ },
237
+ actionButton: {
238
+ backgroundColor: DarkTheme.accentDim,
239
+ borderRadius: 10,
240
+ borderWidth: 1,
241
+ borderColor: DarkTheme.accent,
242
+ paddingVertical: 10,
243
+ paddingHorizontal: 18,
244
+ flexDirection: 'row',
245
+ alignItems: 'center',
246
+ },
247
+ actionButtonDestructive: {
248
+ backgroundColor: DarkTheme.errorDim,
249
+ borderColor: DarkTheme.error,
250
+ },
251
+ actionIcon: {
252
+ fontSize: 14,
253
+ marginRight: 6,
254
+ },
255
+ actionButtonTitle: {
256
+ fontFamily: MonospaceFont,
257
+ fontSize: 13,
258
+ fontWeight: '600',
259
+ color: DarkTheme.accent,
260
+ },
261
+ actionButtonTitleDestructive: {
262
+ color: DarkTheme.error,
263
+ },
264
+ })