rn-backstage 1.2.0 → 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 (110) hide show
  1. package/README.md +84 -2
  2. package/lib/commonjs/Backstage.js +46 -33
  3. package/lib/commonjs/Backstage.js.map +1 -1
  4. package/lib/commonjs/ThemeContext.js +43 -0
  5. package/lib/commonjs/ThemeContext.js.map +1 -0
  6. package/lib/commonjs/components/BackstagePanel.js +35 -12
  7. package/lib/commonjs/components/BackstagePanel.js.map +1 -1
  8. package/lib/commonjs/components/FlagsTab.js +60 -57
  9. package/lib/commonjs/components/FlagsTab.js.map +1 -1
  10. package/lib/commonjs/components/FloatingPill.js +86 -24
  11. package/lib/commonjs/components/FloatingPill.js.map +1 -1
  12. package/lib/commonjs/components/InfoTab.js +77 -107
  13. package/lib/commonjs/components/InfoTab.js.map +1 -1
  14. package/lib/commonjs/components/JsonTreeView.js +39 -34
  15. package/lib/commonjs/components/JsonTreeView.js.map +1 -1
  16. package/lib/commonjs/components/LogItem.js +39 -29
  17. package/lib/commonjs/components/LogItem.js.map +1 -1
  18. package/lib/commonjs/components/LogsTab.js +39 -42
  19. package/lib/commonjs/components/LogsTab.js.map +1 -1
  20. package/lib/commonjs/components/NetworkItem.js +111 -135
  21. package/lib/commonjs/components/NetworkItem.js.map +1 -1
  22. package/lib/commonjs/components/NetworkTab.js +50 -49
  23. package/lib/commonjs/components/NetworkTab.js.map +1 -1
  24. package/lib/commonjs/components/StorageTab.js +655 -0
  25. package/lib/commonjs/components/StorageTab.js.map +1 -0
  26. package/lib/commonjs/components/TabBar.js +8 -5
  27. package/lib/commonjs/components/TabBar.js.map +1 -1
  28. package/lib/commonjs/constants.js +96 -7
  29. package/lib/commonjs/constants.js.map +1 -1
  30. package/lib/commonjs/index.js +96 -0
  31. package/lib/commonjs/index.js.map +1 -1
  32. package/lib/commonjs/types.js +1 -0
  33. package/lib/commonjs/types.js.map +1 -1
  34. package/lib/module/Backstage.js +46 -33
  35. package/lib/module/Backstage.js.map +1 -1
  36. package/lib/module/ThemeContext.js +37 -0
  37. package/lib/module/ThemeContext.js.map +1 -0
  38. package/lib/module/components/BackstagePanel.js +37 -14
  39. package/lib/module/components/BackstagePanel.js.map +1 -1
  40. package/lib/module/components/FlagsTab.js +61 -58
  41. package/lib/module/components/FlagsTab.js.map +1 -1
  42. package/lib/module/components/FloatingPill.js +90 -28
  43. package/lib/module/components/FloatingPill.js.map +1 -1
  44. package/lib/module/components/InfoTab.js +77 -107
  45. package/lib/module/components/InfoTab.js.map +1 -1
  46. package/lib/module/components/JsonTreeView.js +42 -38
  47. package/lib/module/components/JsonTreeView.js.map +1 -1
  48. package/lib/module/components/LogItem.js +41 -31
  49. package/lib/module/components/LogItem.js.map +1 -1
  50. package/lib/module/components/LogsTab.js +40 -43
  51. package/lib/module/components/LogsTab.js.map +1 -1
  52. package/lib/module/components/NetworkItem.js +113 -137
  53. package/lib/module/components/NetworkItem.js.map +1 -1
  54. package/lib/module/components/NetworkTab.js +51 -50
  55. package/lib/module/components/NetworkTab.js.map +1 -1
  56. package/lib/module/components/StorageTab.js +650 -0
  57. package/lib/module/components/StorageTab.js.map +1 -0
  58. package/lib/module/components/TabBar.js +10 -7
  59. package/lib/module/components/TabBar.js.map +1 -1
  60. package/lib/module/constants.js +95 -6
  61. package/lib/module/constants.js.map +1 -1
  62. package/lib/module/index.js +14 -0
  63. package/lib/module/index.js.map +1 -1
  64. package/lib/module/types.js +2 -0
  65. package/lib/module/types.js.map +1 -1
  66. package/lib/typescript/Backstage.d.ts.map +1 -1
  67. package/lib/typescript/ThemeContext.d.ts +11 -0
  68. package/lib/typescript/ThemeContext.d.ts.map +1 -0
  69. package/lib/typescript/components/BackstagePanel.d.ts +3 -1
  70. package/lib/typescript/components/BackstagePanel.d.ts.map +1 -1
  71. package/lib/typescript/components/FlagsTab.d.ts.map +1 -1
  72. package/lib/typescript/components/FloatingPill.d.ts +2 -0
  73. package/lib/typescript/components/FloatingPill.d.ts.map +1 -1
  74. package/lib/typescript/components/InfoTab.d.ts +1 -0
  75. package/lib/typescript/components/InfoTab.d.ts.map +1 -1
  76. package/lib/typescript/components/JsonTreeView.d.ts.map +1 -1
  77. package/lib/typescript/components/LogItem.d.ts +1 -0
  78. package/lib/typescript/components/LogItem.d.ts.map +1 -1
  79. package/lib/typescript/components/LogsTab.d.ts +1 -0
  80. package/lib/typescript/components/LogsTab.d.ts.map +1 -1
  81. package/lib/typescript/components/NetworkItem.d.ts +1 -0
  82. package/lib/typescript/components/NetworkItem.d.ts.map +1 -1
  83. package/lib/typescript/components/NetworkTab.d.ts +1 -0
  84. package/lib/typescript/components/NetworkTab.d.ts.map +1 -1
  85. package/lib/typescript/components/StorageTab.d.ts +9 -0
  86. package/lib/typescript/components/StorageTab.d.ts.map +1 -0
  87. package/lib/typescript/components/TabBar.d.ts.map +1 -1
  88. package/lib/typescript/constants.d.ts +60 -4
  89. package/lib/typescript/constants.d.ts.map +1 -1
  90. package/lib/typescript/index.d.ts +14 -1
  91. package/lib/typescript/index.d.ts.map +1 -1
  92. package/lib/typescript/types.d.ts +20 -0
  93. package/lib/typescript/types.d.ts.map +1 -1
  94. package/package.json +6 -4
  95. package/src/Backstage.tsx +45 -33
  96. package/src/ThemeContext.tsx +38 -0
  97. package/src/components/BackstagePanel.tsx +89 -61
  98. package/src/components/FlagsTab.tsx +125 -174
  99. package/src/components/FloatingPill.tsx +127 -30
  100. package/src/components/InfoTab.tsx +153 -186
  101. package/src/components/JsonTreeView.tsx +53 -73
  102. package/src/components/LogItem.tsx +64 -100
  103. package/src/components/LogsTab.tsx +110 -132
  104. package/src/components/NetworkItem.tsx +306 -417
  105. package/src/components/NetworkTab.tsx +129 -163
  106. package/src/components/StorageTab.tsx +607 -0
  107. package/src/components/TabBar.tsx +51 -47
  108. package/src/constants.ts +103 -4
  109. package/src/index.ts +17 -0
  110. package/src/types.ts +28 -0
@@ -0,0 +1,607 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react'
2
+ import { Alert, FlatList, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'
3
+ import { MonospaceFont } from '../constants'
4
+ import { useBackstageTheme } from '../ThemeContext'
5
+ import { JsonTreeView } from './JsonTreeView'
6
+ import type { StorageAdapter, BackstageTheme } from '../types'
7
+
8
+ // ─── Types ───────────────────────────────────────────────────────────────────
9
+
10
+ interface StorageTabProps {
11
+ adapter: StorageAdapter
12
+ jsonMaxDepth?: number
13
+ }
14
+
15
+ interface StorageEntry {
16
+ key: string
17
+ value: string
18
+ }
19
+
20
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
21
+
22
+ function tryParseJSON(text: string): unknown | null {
23
+ try {
24
+ const parsed = JSON.parse(text)
25
+ return typeof parsed === 'object' && parsed !== null ? parsed : null
26
+ } catch {
27
+ return null
28
+ }
29
+ }
30
+
31
+ function truncateValue(value: string, max = 80): string {
32
+ if (value.length <= max) return value
33
+ return value.substring(0, max) + '…'
34
+ }
35
+
36
+ // ─── Component ───────────────────────────────────────────────────────────────
37
+
38
+ export const StorageTab: React.FC<StorageTabProps> = ({ adapter, jsonMaxDepth }) => {
39
+ const theme = useBackstageTheme()
40
+ const s = useMemo(() => createStyles(theme), [theme])
41
+
42
+ const [entries, setEntries] = useState<StorageEntry[]>([])
43
+ const [loading, setLoading] = useState(true)
44
+ const [searchText, setSearchText] = useState('')
45
+
46
+ // Edit state
47
+ const [editingKey, setEditingKey] = useState<string | null>(null)
48
+ const [editValue, setEditValue] = useState('')
49
+
50
+ // Add new entry state
51
+ const [showAddForm, setShowAddForm] = useState(false)
52
+ const [newKey, setNewKey] = useState('')
53
+ const [newValue, setNewValue] = useState('')
54
+
55
+ // Expanded entries (for JSON preview)
56
+ const [expandedKeys, setExpandedKeys] = useState<Set<string>>(new Set())
57
+
58
+ // ─── Load Entries ────────────────────────────────────────
59
+ const loadEntries = useCallback(async () => {
60
+ setLoading(true)
61
+ try {
62
+ const keys = await adapter.getAllKeys()
63
+ const sorted = [...keys].sort((a, b) => a.localeCompare(b))
64
+ const results: StorageEntry[] = []
65
+ for (const key of sorted) {
66
+ const value = await adapter.getItem(key)
67
+ results.push({ key, value: value ?? '' })
68
+ }
69
+ setEntries(results)
70
+ } catch (err) {
71
+ console.error('[Backstage] StorageTab load error:', err)
72
+ } finally {
73
+ setLoading(false)
74
+ }
75
+ }, [adapter])
76
+
77
+ useEffect(() => {
78
+ loadEntries()
79
+ }, [loadEntries])
80
+
81
+ // ─── Filter ──────────────────────────────────────────────
82
+ const filteredEntries = useMemo(() => {
83
+ if (!searchText.trim()) return entries
84
+ const query = searchText.toLowerCase()
85
+ return entries.filter(
86
+ e => e.key.toLowerCase().includes(query) || e.value.toLowerCase().includes(query),
87
+ )
88
+ }, [entries, searchText])
89
+
90
+ // ─── Toggle Expand ───────────────────────────────────────
91
+ const toggleExpand = useCallback((key: string) => {
92
+ setExpandedKeys(prev => {
93
+ const next = new Set(prev)
94
+ if (next.has(key)) {
95
+ next.delete(key)
96
+ } else {
97
+ next.add(key)
98
+ }
99
+ return next
100
+ })
101
+ }, [])
102
+
103
+ // ─── Edit ────────────────────────────────────────────────
104
+ const startEditing = useCallback((entry: StorageEntry) => {
105
+ setEditingKey(entry.key)
106
+ setEditValue(entry.value)
107
+ }, [])
108
+
109
+ const cancelEditing = useCallback(() => {
110
+ setEditingKey(null)
111
+ setEditValue('')
112
+ }, [])
113
+
114
+ const saveEdit = useCallback(async () => {
115
+ if (editingKey === null) return
116
+ try {
117
+ await adapter.setItem(editingKey, editValue)
118
+ setEntries(prev => prev.map(e => (e.key === editingKey ? { ...e, value: editValue } : e)))
119
+ setEditingKey(null)
120
+ setEditValue('')
121
+ } catch (err) {
122
+ Alert.alert('Error', `Failed to save: ${err}`)
123
+ }
124
+ }, [adapter, editingKey, editValue])
125
+
126
+ // ─── Delete ──────────────────────────────────────────────
127
+ const confirmDelete = useCallback(
128
+ (key: string) => {
129
+ Alert.alert('Delete Entry', `Remove "${key}" from storage?`, [
130
+ { text: 'Cancel', style: 'cancel' },
131
+ {
132
+ text: 'Delete',
133
+ style: 'destructive',
134
+ onPress: async () => {
135
+ try {
136
+ await adapter.removeItem(key)
137
+ setEntries(prev => prev.filter(e => e.key !== key))
138
+ setExpandedKeys(prev => {
139
+ const next = new Set(prev)
140
+ next.delete(key)
141
+ return next
142
+ })
143
+ } catch (err) {
144
+ Alert.alert('Error', `Failed to delete: ${err}`)
145
+ }
146
+ },
147
+ },
148
+ ])
149
+ },
150
+ [adapter],
151
+ )
152
+
153
+ // ─── Add New Entry ───────────────────────────────────────
154
+ const handleAddEntry = useCallback(async () => {
155
+ const trimmedKey = newKey.trim()
156
+ if (!trimmedKey) {
157
+ Alert.alert('Error', 'Key cannot be empty')
158
+ return
159
+ }
160
+ try {
161
+ await adapter.setItem(trimmedKey, newValue)
162
+ setEntries(prev => {
163
+ const exists = prev.findIndex(e => e.key === trimmedKey)
164
+ if (exists >= 0) {
165
+ const updated = [...prev]
166
+ updated[exists] = { key: trimmedKey, value: newValue }
167
+ return updated
168
+ }
169
+ return [...prev, { key: trimmedKey, value: newValue }].sort((a, b) =>
170
+ a.key.localeCompare(b.key),
171
+ )
172
+ })
173
+ setNewKey('')
174
+ setNewValue('')
175
+ setShowAddForm(false)
176
+ } catch (err) {
177
+ Alert.alert('Error', `Failed to save: ${err}`)
178
+ }
179
+ }, [adapter, newKey, newValue])
180
+
181
+ // ─── Render Entry ────────────────────────────────────────
182
+ const renderItem = useCallback(
183
+ ({ item }: { item: StorageEntry }) => {
184
+ const isEditing = editingKey === item.key
185
+ const isExpanded = expandedKeys.has(item.key)
186
+ const jsonValue = tryParseJSON(item.value)
187
+ const isJSON = jsonValue !== null
188
+
189
+ return (
190
+ <View style={s.entryContainer}>
191
+ {/* Key row */}
192
+ <View style={s.entryHeader}>
193
+ <TouchableOpacity
194
+ style={s.entryKeyContainer}
195
+ onPress={() => toggleExpand(item.key)}
196
+ activeOpacity={0.7}
197
+ >
198
+ <Text style={s.entryChevron}>{isExpanded ? '▼' : '▶'}</Text>
199
+ <Text style={s.entryKey} numberOfLines={1}>
200
+ {item.key}
201
+ </Text>
202
+ {isJSON && <Text style={s.jsonBadge}>JSON</Text>}
203
+ </TouchableOpacity>
204
+ <View style={s.entryActions}>
205
+ <TouchableOpacity
206
+ onPress={() => startEditing(item)}
207
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
208
+ >
209
+ <Text style={s.actionIcon}>✎</Text>
210
+ </TouchableOpacity>
211
+ <TouchableOpacity
212
+ onPress={() => confirmDelete(item.key)}
213
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
214
+ >
215
+ <Text style={[s.actionIcon, s.deleteIcon]}>✕</Text>
216
+ </TouchableOpacity>
217
+ </View>
218
+ </View>
219
+
220
+ {/* Value */}
221
+ {isEditing ? (
222
+ <View style={s.editContainer}>
223
+ <TextInput
224
+ style={s.editInput}
225
+ value={editValue}
226
+ onChangeText={setEditValue}
227
+ multiline
228
+ autoFocus
229
+ textAlignVertical="top"
230
+ placeholderTextColor={theme.textMuted}
231
+ placeholder="Value"
232
+ />
233
+ <View style={s.editActions}>
234
+ <TouchableOpacity style={s.cancelButton} onPress={cancelEditing}>
235
+ <Text style={s.cancelButtonText}>Cancel</Text>
236
+ </TouchableOpacity>
237
+ <TouchableOpacity style={s.saveButton} onPress={saveEdit}>
238
+ <Text style={s.saveButtonText}>Save</Text>
239
+ </TouchableOpacity>
240
+ </View>
241
+ </View>
242
+ ) : isExpanded ? (
243
+ <View style={s.expandedValue}>
244
+ {isJSON ? (
245
+ <View style={s.jsonContainer}>
246
+ <JsonTreeView data={jsonValue} hideRoot maxDepth={jsonMaxDepth} />
247
+ </View>
248
+ ) : (
249
+ <Text style={s.valueText} selectable>
250
+ {item.value || '(empty)'}
251
+ </Text>
252
+ )}
253
+ </View>
254
+ ) : (
255
+ <TouchableOpacity onPress={() => toggleExpand(item.key)} activeOpacity={0.7}>
256
+ <Text style={s.valuePreview} numberOfLines={1}>
257
+ {truncateValue(item.value) || '(empty)'}
258
+ </Text>
259
+ </TouchableOpacity>
260
+ )}
261
+ </View>
262
+ )
263
+ },
264
+ [
265
+ editingKey,
266
+ editValue,
267
+ expandedKeys,
268
+ jsonMaxDepth,
269
+ s,
270
+ theme.textMuted,
271
+ toggleExpand,
272
+ startEditing,
273
+ cancelEditing,
274
+ saveEdit,
275
+ confirmDelete,
276
+ ],
277
+ )
278
+
279
+ const keyExtractor = useCallback((item: StorageEntry) => item.key, [])
280
+
281
+ return (
282
+ <View style={s.container}>
283
+ {/* Search + Add bar */}
284
+ <View style={s.searchContainer}>
285
+ <View style={s.searchInputWrapper}>
286
+ <Text style={s.searchIcon}>⌕</Text>
287
+ <TextInput
288
+ style={s.searchInput}
289
+ placeholder="Filter by key or value..."
290
+ placeholderTextColor={theme.textMuted}
291
+ value={searchText}
292
+ onChangeText={setSearchText}
293
+ autoCapitalize="none"
294
+ autoCorrect={false}
295
+ clearButtonMode="while-editing"
296
+ returnKeyType="search"
297
+ />
298
+ </View>
299
+ <TouchableOpacity
300
+ style={s.addButton}
301
+ onPress={() => setShowAddForm(prev => !prev)}
302
+ activeOpacity={0.7}
303
+ >
304
+ <Text style={s.addButtonText}>{showAddForm ? '✕' : '+'}</Text>
305
+ </TouchableOpacity>
306
+ </View>
307
+
308
+ {/* Add form */}
309
+ {showAddForm && (
310
+ <View style={s.addForm}>
311
+ <TextInput
312
+ style={s.addInput}
313
+ placeholder="Key"
314
+ placeholderTextColor={theme.textMuted}
315
+ value={newKey}
316
+ onChangeText={setNewKey}
317
+ autoCapitalize="none"
318
+ autoCorrect={false}
319
+ />
320
+ <TextInput
321
+ style={[s.addInput, s.addValueInput]}
322
+ placeholder="Value"
323
+ placeholderTextColor={theme.textMuted}
324
+ value={newValue}
325
+ onChangeText={setNewValue}
326
+ multiline
327
+ textAlignVertical="top"
328
+ />
329
+ <TouchableOpacity style={s.addSubmitButton} onPress={handleAddEntry} activeOpacity={0.7}>
330
+ <Text style={s.addSubmitText}>Add Entry</Text>
331
+ </TouchableOpacity>
332
+ </View>
333
+ )}
334
+
335
+ {/* Stats */}
336
+ <View style={s.statsBar}>
337
+ <Text style={s.statText}>
338
+ {filteredEntries.length === entries.length
339
+ ? `${entries.length} entr${entries.length !== 1 ? 'ies' : 'y'}`
340
+ : `${filteredEntries.length} of ${entries.length}`}
341
+ </Text>
342
+ <Text style={s.pullHint}>↓ pull to refresh</Text>
343
+ </View>
344
+
345
+ {/* List */}
346
+ <FlatList
347
+ data={filteredEntries}
348
+ renderItem={renderItem}
349
+ keyExtractor={keyExtractor}
350
+ refreshing={loading}
351
+ onRefresh={loadEntries}
352
+ style={s.list}
353
+ contentContainerStyle={filteredEntries.length === 0 ? s.emptyContainer : undefined}
354
+ ListEmptyComponent={
355
+ <View style={s.emptyState}>
356
+ <Text style={s.emptyIcon}>🗄</Text>
357
+ <Text style={s.emptyTitle}>{loading ? 'Loading…' : 'No entries'}</Text>
358
+ <Text style={s.emptySubtitle}>
359
+ {loading ? 'Reading storage keys' : 'Storage is empty or no entries match'}
360
+ </Text>
361
+ </View>
362
+ }
363
+ maxToRenderPerBatch={20}
364
+ windowSize={10}
365
+ initialNumToRender={20}
366
+ removeClippedSubviews={true}
367
+ />
368
+ </View>
369
+ )
370
+ }
371
+
372
+ // ─── Styles ──────────────────────────────────────────────────────────────────
373
+
374
+ const createStyles = (t: BackstageTheme) =>
375
+ StyleSheet.create({
376
+ container: { flex: 1 },
377
+
378
+ // ── Search ─────────────────────
379
+ searchContainer: {
380
+ flexDirection: 'row',
381
+ alignItems: 'center',
382
+ paddingHorizontal: 14,
383
+ paddingVertical: 10,
384
+ gap: 10,
385
+ borderBottomWidth: 1,
386
+ borderBottomColor: t.border,
387
+ },
388
+ searchInputWrapper: {
389
+ flex: 1,
390
+ flexDirection: 'row',
391
+ alignItems: 'center',
392
+ backgroundColor: t.surfaceElevated,
393
+ borderRadius: 10,
394
+ borderWidth: 1,
395
+ borderColor: t.border,
396
+ paddingHorizontal: 12,
397
+ height: 38,
398
+ },
399
+ searchIcon: { fontSize: 16, color: t.textMuted, marginRight: 8 },
400
+ searchInput: {
401
+ flex: 1,
402
+ fontFamily: MonospaceFont,
403
+ fontSize: 13,
404
+ color: t.text,
405
+ padding: 0,
406
+ },
407
+ addButton: {
408
+ width: 38,
409
+ height: 38,
410
+ borderRadius: 10,
411
+ backgroundColor: t.accentDim,
412
+ borderWidth: 1,
413
+ borderColor: t.accent,
414
+ alignItems: 'center',
415
+ justifyContent: 'center',
416
+ },
417
+ addButtonText: { fontSize: 18, color: t.accent, fontWeight: '700' },
418
+
419
+ // ── Add Form ───────────────────
420
+ addForm: {
421
+ padding: 14,
422
+ borderBottomWidth: 1,
423
+ borderBottomColor: t.border,
424
+ backgroundColor: t.surface,
425
+ gap: 8,
426
+ },
427
+ addInput: {
428
+ fontFamily: MonospaceFont,
429
+ fontSize: 13,
430
+ color: t.text,
431
+ backgroundColor: t.surfaceElevated,
432
+ borderRadius: 8,
433
+ borderWidth: 1,
434
+ borderColor: t.border,
435
+ paddingHorizontal: 12,
436
+ paddingVertical: 8,
437
+ },
438
+ addValueInput: { minHeight: 60 },
439
+ addSubmitButton: {
440
+ backgroundColor: t.accent,
441
+ borderRadius: 8,
442
+ paddingVertical: 10,
443
+ alignItems: 'center',
444
+ },
445
+ addSubmitText: {
446
+ fontFamily: MonospaceFont,
447
+ fontSize: 13,
448
+ fontWeight: '700',
449
+ color: '#FFFFFF',
450
+ },
451
+
452
+ // ── Stats ──────────────────────
453
+ statsBar: {
454
+ flexDirection: 'row',
455
+ justifyContent: 'space-between',
456
+ alignItems: 'center',
457
+ paddingHorizontal: 16,
458
+ paddingVertical: 6,
459
+ backgroundColor: t.surface,
460
+ },
461
+ statText: { fontFamily: MonospaceFont, fontSize: 11, color: t.textMuted },
462
+ pullHint: {
463
+ fontFamily: MonospaceFont,
464
+ fontSize: 10,
465
+ color: t.textMuted,
466
+ fontStyle: 'italic',
467
+ },
468
+
469
+ // ── List ───────────────────────
470
+ list: { flex: 1 },
471
+ emptyContainer: { flex: 1, justifyContent: 'center' },
472
+ emptyState: { alignItems: 'center', justifyContent: 'center', padding: 40 },
473
+ emptyIcon: { fontSize: 48, marginBottom: 16, opacity: 0.5 },
474
+ emptyTitle: {
475
+ fontFamily: MonospaceFont,
476
+ fontSize: 16,
477
+ fontWeight: '700',
478
+ color: t.textSecondary,
479
+ marginBottom: 8,
480
+ },
481
+ emptySubtitle: {
482
+ fontFamily: MonospaceFont,
483
+ fontSize: 13,
484
+ color: t.textMuted,
485
+ textAlign: 'center',
486
+ },
487
+
488
+ // ── Entry ──────────────────────
489
+ entryContainer: {
490
+ paddingHorizontal: 14,
491
+ paddingVertical: 10,
492
+ borderBottomWidth: 1,
493
+ borderBottomColor: t.border,
494
+ },
495
+ entryHeader: {
496
+ flexDirection: 'row',
497
+ alignItems: 'center',
498
+ justifyContent: 'space-between',
499
+ marginBottom: 4,
500
+ },
501
+ entryKeyContainer: {
502
+ flex: 1,
503
+ flexDirection: 'row',
504
+ alignItems: 'center',
505
+ },
506
+ entryChevron: {
507
+ fontFamily: MonospaceFont,
508
+ fontSize: 10,
509
+ color: t.textMuted,
510
+ marginRight: 6,
511
+ width: 12,
512
+ },
513
+ entryKey: {
514
+ fontFamily: MonospaceFont,
515
+ fontSize: 13,
516
+ fontWeight: '700',
517
+ color: t.text,
518
+ flex: 1,
519
+ },
520
+ jsonBadge: {
521
+ fontFamily: MonospaceFont,
522
+ fontSize: 9,
523
+ fontWeight: '800',
524
+ color: t.info,
525
+ backgroundColor: t.infoDim,
526
+ borderRadius: 3,
527
+ paddingHorizontal: 5,
528
+ paddingVertical: 1,
529
+ marginLeft: 6,
530
+ overflow: 'hidden',
531
+ },
532
+ entryActions: { flexDirection: 'row', alignItems: 'center', gap: 12, marginLeft: 8 },
533
+ actionIcon: {
534
+ fontSize: 16,
535
+ color: t.textMuted,
536
+ },
537
+ deleteIcon: { color: t.error },
538
+
539
+ // ── Value Preview ──────────────
540
+ valuePreview: {
541
+ fontFamily: MonospaceFont,
542
+ fontSize: 12,
543
+ color: t.textSecondary,
544
+ paddingLeft: 18,
545
+ lineHeight: 18,
546
+ },
547
+ expandedValue: { marginTop: 4, paddingLeft: 18 },
548
+ valueText: {
549
+ fontFamily: MonospaceFont,
550
+ fontSize: 12,
551
+ color: t.text,
552
+ lineHeight: 18,
553
+ },
554
+ jsonContainer: {
555
+ backgroundColor: t.surfaceElevated,
556
+ borderRadius: 8,
557
+ borderWidth: 1,
558
+ borderColor: t.border,
559
+ padding: 8,
560
+ },
561
+
562
+ // ── Edit ───────────────────────
563
+ editContainer: {
564
+ marginTop: 6,
565
+ paddingLeft: 18,
566
+ gap: 8,
567
+ },
568
+ editInput: {
569
+ fontFamily: MonospaceFont,
570
+ fontSize: 12,
571
+ color: t.text,
572
+ backgroundColor: t.surfaceElevated,
573
+ borderRadius: 8,
574
+ borderWidth: 1,
575
+ borderColor: t.accent,
576
+ paddingHorizontal: 10,
577
+ paddingVertical: 8,
578
+ minHeight: 60,
579
+ lineHeight: 18,
580
+ },
581
+ editActions: { flexDirection: 'row', gap: 8, justifyContent: 'flex-end' },
582
+ cancelButton: {
583
+ borderRadius: 6,
584
+ borderWidth: 1,
585
+ borderColor: t.border,
586
+ paddingHorizontal: 14,
587
+ paddingVertical: 6,
588
+ },
589
+ cancelButtonText: {
590
+ fontFamily: MonospaceFont,
591
+ fontSize: 12,
592
+ fontWeight: '600',
593
+ color: t.textSecondary,
594
+ },
595
+ saveButton: {
596
+ borderRadius: 6,
597
+ backgroundColor: t.accent,
598
+ paddingHorizontal: 14,
599
+ paddingVertical: 6,
600
+ },
601
+ saveButtonText: {
602
+ fontFamily: MonospaceFont,
603
+ fontSize: 12,
604
+ fontWeight: '700',
605
+ color: '#FFFFFF',
606
+ },
607
+ })