rozenite-sqlite 0.0.9 → 0.0.12

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.
@@ -0,0 +1,324 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { View, Text, StyleSheet, ScrollView, Pressable, TextInput, ActivityIndicator } from 'react-native';
3
+ import { C, type RowData } from '../theme';
4
+ import type { MutationResult } from '../hooks/useBridgeSync';
5
+
6
+ function isJsonString(val: string): boolean {
7
+ const t = val.trim();
8
+ if (t.length < 2 || (t[0] !== '{' && t[0] !== '[')) return false;
9
+ try { JSON.parse(t); return true; } catch { return false; }
10
+ }
11
+
12
+ export interface RowDetailPanelProps {
13
+ row: RowData | null;
14
+ rowIndex: number | null;
15
+ onClose: () => void;
16
+ onSave: (updated: RowData) => Promise<MutationResult>;
17
+ onDelete: () => Promise<MutationResult>;
18
+ mutating?: boolean;
19
+ mutationError?: string | null;
20
+ onClearError?: () => void;
21
+ }
22
+
23
+ export function RowDetailPanel({ row, rowIndex, onClose, onSave, onDelete, mutating, mutationError, onClearError }: RowDetailPanelProps) {
24
+ const [draft, setDraft] = useState<RowData>({});
25
+ const [dirty, setDirty] = useState(false);
26
+ const [confirmDelete, setConfirmDelete] = useState(false);
27
+
28
+ useEffect(() => {
29
+ if (row) {
30
+ const formatted = Object.fromEntries(
31
+ Object.entries(row).map(([k, v]) => {
32
+ const s = v == null ? v : String(v);
33
+ if (s != null && isJsonString(s)) {
34
+ try { return [k, JSON.stringify(JSON.parse(s), null, 2)]; } catch {}
35
+ }
36
+ return [k, v];
37
+ })
38
+ );
39
+ setDraft(formatted);
40
+ setDirty(false);
41
+ setConfirmDelete(false);
42
+ }
43
+ }, [row]);
44
+
45
+ if (!row) return null;
46
+
47
+ const handleChange = (key: string, value: string) => {
48
+ setDraft((prev) => ({ ...prev, [key]: value }));
49
+ setDirty(true);
50
+ };
51
+
52
+ const handleSave = async () => {
53
+ onClearError?.();
54
+ const result = await onSave(draft);
55
+ if (result.success) {
56
+ setDirty(false);
57
+ }
58
+ };
59
+
60
+ const handleDelete = async () => {
61
+ onClearError?.();
62
+ const result = await onDelete();
63
+ if (result.success) {
64
+ setConfirmDelete(false);
65
+ }
66
+ };
67
+
68
+ const handleCancel = () => {
69
+ setDraft({ ...row });
70
+ setDirty(false);
71
+ setConfirmDelete(false);
72
+ onClose();
73
+ };
74
+
75
+ return (
76
+ <View style={s.panel}>
77
+ <View style={s.header}>
78
+ <View>
79
+ <Text style={s.eyebrow}>ROW DETAIL</Text>
80
+ <View style={s.titleRow}>
81
+ <Text style={s.title}>Row #{(rowIndex ?? 0) + 1}</Text>
82
+ {dirty && <View style={s.dirtyDot} />}
83
+ </View>
84
+ </View>
85
+ <Pressable
86
+ onPress={onClose}
87
+ style={({ pressed }) => [s.closeBtn, pressed && { opacity: 0.6 }]}
88
+ >
89
+ <Text style={s.closeBtnText}>✕</Text>
90
+ </Pressable>
91
+ </View>
92
+
93
+ <ScrollView style={s.scroll} showsVerticalScrollIndicator={false}>
94
+ {Object.entries(draft).map(([key, value]) => {
95
+ const strVal = value === null || value === undefined ? '' : String(value);
96
+ const isJson = isJsonString(strVal);
97
+ return (
98
+ <View key={key} style={s.field}>
99
+ <View style={s.fieldKeyRow}>
100
+ <Text style={s.fieldKey}>{key}</Text>
101
+ {isJson && <Text style={s.fieldJsonBadge}>JSON</Text>}
102
+ </View>
103
+ <TextInput
104
+ style={[s.fieldInput, isJson && s.fieldInputJson]}
105
+ value={strVal}
106
+ onChangeText={(text) => handleChange(key, text)}
107
+ placeholder="NULL"
108
+ placeholderTextColor={C.textMuted}
109
+ multiline={isJson || strVal.length > 50}
110
+ />
111
+ </View>
112
+ );
113
+ })}
114
+ <View style={{ height: 20 }} />
115
+ </ScrollView>
116
+
117
+ <View style={s.primaryActions}>
118
+ {mutationError && (
119
+ <View style={s.errorBox}>
120
+ <Text style={s.errorText}>{mutationError}</Text>
121
+ <Pressable onPress={onClearError} style={s.errorClose}>
122
+ <Text style={s.errorCloseText}>✕</Text>
123
+ </Pressable>
124
+ </View>
125
+ )}
126
+ <View style={s.primaryActionsRow}>
127
+ <Pressable
128
+ style={({ pressed }) => [s.btn, s.btnSave, (!dirty || mutating) && s.btnSaveDisabled, pressed && !dirty ? {} : pressed && s.btnPressed]}
129
+ onPress={dirty && !mutating ? handleSave : undefined}
130
+ disabled={!dirty || mutating}
131
+ >
132
+ {mutating ? (
133
+ <ActivityIndicator size="small" color={C.bg} />
134
+ ) : (
135
+ <Text style={[s.btnSaveText, !dirty && s.btnSaveTextDisabled]}>Save</Text>
136
+ )}
137
+ </Pressable>
138
+ <Pressable
139
+ style={({ pressed }) => [s.btn, s.btnCancel, pressed && s.btnPressed]}
140
+ onPress={handleCancel}
141
+ >
142
+ <Text style={s.btnCancelText}>Cancel</Text>
143
+ </Pressable>
144
+ </View>
145
+ </View>
146
+
147
+ <View style={s.deleteZone}>
148
+ {!confirmDelete ? (
149
+ <Pressable
150
+ style={({ pressed }) => [s.btnDeleteSmall, mutating && s.btnSaveDisabled, pressed && s.btnPressed]}
151
+ onPress={() => !mutating && setConfirmDelete(true)}
152
+ disabled={mutating}
153
+ >
154
+ <Text style={s.btnDeleteSmallText}>⊘ Delete this row</Text>
155
+ </Pressable>
156
+ ) : (
157
+ <View style={s.confirmBox}>
158
+ <Text style={s.confirmLabel}>This action cannot be undone.</Text>
159
+ <View style={s.confirmRow}>
160
+ <Pressable
161
+ style={({ pressed }) => [s.btn, s.btnConfirm, { flex: 1 }, mutating && s.btnSaveDisabled, pressed && s.btnPressed]}
162
+ onPress={!mutating ? handleDelete : undefined}
163
+ disabled={mutating}
164
+ >
165
+ {mutating ? (
166
+ <ActivityIndicator size="small" color="#fff" />
167
+ ) : (
168
+ <Text style={s.btnConfirmText}>Yes, delete</Text>
169
+ )}
170
+ </Pressable>
171
+ <Pressable
172
+ style={({ pressed }) => [s.btn, s.btnCancelSmall, { flex: 1 }, pressed && s.btnPressed]}
173
+ onPress={() => setConfirmDelete(false)}
174
+ >
175
+ <Text style={s.btnCancelText}>Cancel</Text>
176
+ </Pressable>
177
+ </View>
178
+ </View>
179
+ )}
180
+ </View>
181
+ </View>
182
+ );
183
+ }
184
+
185
+ const s = StyleSheet.create({
186
+ panel: {
187
+ width: 300,
188
+ backgroundColor: C.surface,
189
+ borderLeftWidth: 1,
190
+ borderLeftColor: C.border,
191
+ flexDirection: 'column',
192
+ boxShadow: '-8px 0 24px rgba(0,0,0,0.45)',
193
+ },
194
+ header: {
195
+ flexDirection: 'row',
196
+ alignItems: 'center',
197
+ justifyContent: 'space-between',
198
+ paddingHorizontal: 16,
199
+ paddingVertical: 14,
200
+ borderBottomWidth: 1,
201
+ borderBottomColor: C.border,
202
+ },
203
+ eyebrow: {
204
+ fontSize: 10,
205
+ fontWeight: '700',
206
+ color: C.textMuted,
207
+ textTransform: 'uppercase',
208
+ letterSpacing: 0.8,
209
+ },
210
+ titleRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 2 },
211
+ title: { fontSize: 16, fontWeight: '700', color: C.text },
212
+ dirtyDot: { width: 7, height: 7, borderRadius: 4, backgroundColor: C.accent },
213
+ closeBtn: {
214
+ width: 28,
215
+ height: 28,
216
+ borderRadius: 6,
217
+ backgroundColor: C.surface2,
218
+ alignItems: 'center',
219
+ justifyContent: 'center',
220
+ },
221
+ closeBtnText: { fontSize: 12, color: C.textSecondary },
222
+ scroll: { flex: 1, paddingHorizontal: 16, paddingTop: 16 },
223
+ field: { marginBottom: 16 },
224
+ fieldKeyRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 5 },
225
+ fieldKey: {
226
+ fontSize: 10,
227
+ fontWeight: '700',
228
+ color: C.textMuted,
229
+ textTransform: 'uppercase',
230
+ letterSpacing: 0.7,
231
+ },
232
+ fieldJsonBadge: {
233
+ fontSize: 9,
234
+ fontWeight: '700',
235
+ color: '#a78bfa',
236
+ backgroundColor: 'rgba(167,139,250,0.12)',
237
+ paddingHorizontal: 4,
238
+ paddingVertical: 2,
239
+ borderRadius: 3,
240
+ },
241
+ fieldInput: {
242
+ backgroundColor: C.surface2,
243
+ borderWidth: 1,
244
+ borderColor: C.border,
245
+ borderRadius: 6,
246
+ paddingHorizontal: 10,
247
+ paddingVertical: 8,
248
+ fontSize: 13,
249
+ color: C.text,
250
+ outlineStyle: 'none' as any,
251
+ },
252
+ fieldInputJson: {
253
+ fontFamily: 'monospace',
254
+ fontSize: 12,
255
+ lineHeight: 18,
256
+ borderColor: 'rgba(167,139,250,0.3)',
257
+ minHeight: 180,
258
+ textAlignVertical: 'top',
259
+ paddingTop: 10,
260
+ },
261
+ primaryActions: {
262
+ flexDirection: 'column',
263
+ gap: 8,
264
+ paddingHorizontal: 16,
265
+ paddingTop: 14,
266
+ paddingBottom: 10,
267
+ borderTopWidth: 1,
268
+ borderTopColor: C.border,
269
+ },
270
+ primaryActionsRow: {
271
+ flexDirection: 'row',
272
+ gap: 8,
273
+ },
274
+ deleteZone: {
275
+ paddingHorizontal: 16,
276
+ paddingBottom: 14,
277
+ paddingTop: 2,
278
+ },
279
+ btn: {
280
+ flex: 1,
281
+ borderRadius: 6,
282
+ paddingVertical: 9,
283
+ alignItems: 'center',
284
+ justifyContent: 'center',
285
+ },
286
+ btnPressed: { opacity: 0.72 },
287
+ btnSave: { backgroundColor: C.accent },
288
+ btnSaveDisabled: { backgroundColor: C.surface2 },
289
+ btnSaveText: { fontSize: 13, fontWeight: '600', color: '#fff' },
290
+ btnSaveTextDisabled: { color: C.textMuted },
291
+ btnCancel: { backgroundColor: C.surface2, borderWidth: 1, borderColor: C.border },
292
+ btnCancelText: { fontSize: 13, fontWeight: '500', color: C.textSecondary },
293
+ btnDeleteSmall: {
294
+ paddingVertical: 7,
295
+ alignItems: 'center',
296
+ justifyContent: 'center',
297
+ borderRadius: 6,
298
+ borderWidth: 1,
299
+ borderColor: 'transparent',
300
+ },
301
+ btnDeleteSmallText: { fontSize: 12, color: C.danger, fontWeight: '500' },
302
+ confirmBox: { gap: 8 },
303
+ confirmLabel: { fontSize: 11, color: C.textMuted, textAlign: 'center' },
304
+ confirmRow: { flexDirection: 'row', gap: 8 },
305
+ btnConfirm: { backgroundColor: C.danger },
306
+ btnConfirmText: { fontSize: 12, fontWeight: '600', color: '#fff' },
307
+ btnCancelSmall: { backgroundColor: C.surface2, borderWidth: 1, borderColor: C.border },
308
+ errorBox: {
309
+ flexDirection: 'row',
310
+ alignItems: 'center',
311
+ justifyContent: 'space-between',
312
+ backgroundColor: 'rgba(239,68,68,0.15)',
313
+ borderWidth: 1,
314
+ borderColor: 'rgba(239,68,68,0.3)',
315
+ borderRadius: 6,
316
+ paddingHorizontal: 10,
317
+ paddingVertical: 8,
318
+ marginBottom: 8,
319
+ width: '100%',
320
+ },
321
+ errorText: { flex: 1, fontSize: 12, color: C.danger, fontWeight: '500' },
322
+ errorClose: { marginLeft: 8, padding: 4 },
323
+ errorCloseText: { fontSize: 12, color: C.danger },
324
+ });
@@ -0,0 +1,220 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ Pressable,
7
+ TextInput,
8
+ ActivityIndicator,
9
+ ScrollView,
10
+ } from 'react-native';
11
+ import { C } from '../theme';
12
+ import type { RowData } from '../theme';
13
+
14
+ interface SqlPanelProps {
15
+ selectedDB: string | null;
16
+ runCustomQuery: (sql: string) => Promise<{ rows: RowData[]; columns: string[]; error?: string }>;
17
+ }
18
+
19
+ export function SqlPanel({ selectedDB, runCustomQuery }: SqlPanelProps) {
20
+ const [sql, setSql] = useState('');
21
+ const [queryRunning, setQueryRunning] = useState(false);
22
+ const [queryResult, setQueryResult] = useState<{ rows: RowData[]; columns: string[] } | null>(null);
23
+ const [queryError, setQueryError] = useState<string | null>(null);
24
+
25
+ const handleRun = () => {
26
+ if (!sql.trim() || queryRunning || !selectedDB) return;
27
+ setQueryRunning(true);
28
+ setQueryResult(null);
29
+ setQueryError(null);
30
+ runCustomQuery(sql.trim()).then(
31
+ (result) => {
32
+ setQueryRunning(false);
33
+ if (result.error) {
34
+ setQueryError(result.error);
35
+ } else {
36
+ setQueryResult({ rows: result.rows, columns: result.columns });
37
+ }
38
+ },
39
+ (err: unknown) => {
40
+ setQueryRunning(false);
41
+ setQueryError(err instanceof Error ? err.message : String(err));
42
+ },
43
+ );
44
+ };
45
+
46
+ return (
47
+ <View style={s.panel}>
48
+ <View style={s.inputRow}>
49
+ <TextInput
50
+ style={s.sqlInput}
51
+ value={sql}
52
+ onChangeText={setSql}
53
+ placeholder={'SELECT * FROM "table_name"'}
54
+ placeholderTextColor={C.textMuted}
55
+ multiline
56
+ blurOnSubmit={false}
57
+ />
58
+ <View style={s.actions}>
59
+ <Pressable
60
+ style={[s.runBtn, (queryRunning || !sql.trim() || !selectedDB) && s.runBtnDisabled]}
61
+ onPress={handleRun}
62
+ disabled={queryRunning || !sql.trim() || !selectedDB}
63
+ >
64
+ {queryRunning
65
+ ? <ActivityIndicator size="small" color="#0d1117" />
66
+ : <Text style={s.runBtnText}>▶ Run</Text>
67
+ }
68
+ </Pressable>
69
+ {(queryResult !== null || queryError !== null) && (
70
+ <Pressable
71
+ style={s.clearBtn}
72
+ onPress={() => { setQueryResult(null); setQueryError(null); }}
73
+ >
74
+ <Text style={s.clearBtnText}>Clear</Text>
75
+ </Pressable>
76
+ )}
77
+ </View>
78
+ </View>
79
+
80
+ {queryError !== null && (
81
+ <View style={s.errorRow}>
82
+ <Text style={s.errorIcon}>⚠</Text>
83
+ <Text style={s.errorText}>{queryError}</Text>
84
+ </View>
85
+ )}
86
+
87
+ {queryResult !== null && (
88
+ <View style={s.resultArea}>
89
+ <Text style={s.resultMeta}>
90
+ {queryResult.rows.length === 0
91
+ ? 'Query executed · 0 rows'
92
+ : `${queryResult.rows.length} row${queryResult.rows.length !== 1 ? 's' : ''} · ${queryResult.columns.length} col${queryResult.columns.length !== 1 ? 's' : ''}`}
93
+ </Text>
94
+ {queryResult.rows.length > 0 && (
95
+ <ScrollView horizontal showsHorizontalScrollIndicator style={s.resultScroll}>
96
+ <View>
97
+ <View style={s.miniHeaderRow}>
98
+ {queryResult.columns.map((col) => (
99
+ <Text key={col} style={s.miniHeaderCell} numberOfLines={1}>{col}</Text>
100
+ ))}
101
+ </View>
102
+ {queryResult.rows.slice(0, 200).map((row, i) => (
103
+ <View key={i} style={[s.miniRow, i % 2 === 0 ? s.miniRowEven : s.miniRowOdd]}>
104
+ {queryResult.columns.map((col) => {
105
+ const val = row[col];
106
+ return (
107
+ <Text key={col} style={[s.miniCell, val == null && s.miniCellNull]} numberOfLines={1}>
108
+ {val == null ? 'NULL' : String(val)}
109
+ </Text>
110
+ );
111
+ })}
112
+ </View>
113
+ ))}
114
+ </View>
115
+ </ScrollView>
116
+ )}
117
+ </View>
118
+ )}
119
+ </View>
120
+ );
121
+ }
122
+
123
+ const s = StyleSheet.create({
124
+ panel: {
125
+ backgroundColor: C.surface,
126
+ borderBottomWidth: 1,
127
+ borderBottomColor: C.border,
128
+ paddingHorizontal: 20,
129
+ paddingTop: 12,
130
+ paddingBottom: 14,
131
+ gap: 10,
132
+ },
133
+ inputRow: { flexDirection: 'row', gap: 10, alignItems: 'flex-start' },
134
+ sqlInput: {
135
+ flex: 1,
136
+ backgroundColor: C.bg,
137
+ borderWidth: 1,
138
+ borderColor: C.border,
139
+ borderRadius: 8,
140
+ paddingHorizontal: 12,
141
+ paddingVertical: 10,
142
+ fontSize: 13,
143
+ color: C.text,
144
+ fontFamily: 'monospace',
145
+ outlineStyle: 'none' as any,
146
+ minHeight: 64,
147
+ textAlignVertical: 'top',
148
+ lineHeight: 20,
149
+ },
150
+ actions: { gap: 6 },
151
+ runBtn: {
152
+ height: 34,
153
+ paddingHorizontal: 14,
154
+ borderRadius: 7,
155
+ backgroundColor: C.accent,
156
+ alignItems: 'center',
157
+ justifyContent: 'center',
158
+ cursor: 'pointer',
159
+ minWidth: 72,
160
+ },
161
+ runBtnDisabled: { opacity: 0.4 },
162
+ runBtnText: { fontSize: 12, fontWeight: '700', color: '#0d1117' },
163
+ clearBtn: {
164
+ height: 28,
165
+ paddingHorizontal: 10,
166
+ borderRadius: 6,
167
+ backgroundColor: C.surface2,
168
+ borderWidth: 1,
169
+ borderColor: C.border,
170
+ alignItems: 'center',
171
+ justifyContent: 'center',
172
+ cursor: 'pointer',
173
+ },
174
+ clearBtnText: { fontSize: 11, color: C.textSecondary },
175
+ errorRow: {
176
+ flexDirection: 'row',
177
+ alignItems: 'flex-start',
178
+ gap: 8,
179
+ backgroundColor: 'rgba(248,81,73,0.08)',
180
+ borderWidth: 1,
181
+ borderColor: 'rgba(248,81,73,0.25)',
182
+ borderRadius: 7,
183
+ padding: 10,
184
+ },
185
+ errorIcon: { fontSize: 13, color: C.danger, marginTop: 1 },
186
+ errorText: { flex: 1, fontSize: 12, color: C.danger, lineHeight: 18, fontFamily: 'monospace' },
187
+ resultArea: { gap: 6 },
188
+ resultMeta: { fontSize: 11, color: C.textMuted },
189
+ resultScroll: { maxHeight: 240, borderRadius: 7, borderWidth: 1, borderColor: C.border },
190
+ miniHeaderRow: {
191
+ flexDirection: 'row',
192
+ backgroundColor: C.surface2,
193
+ borderBottomWidth: 1,
194
+ borderBottomColor: C.border,
195
+ },
196
+ miniHeaderCell: {
197
+ width: 130,
198
+ paddingHorizontal: 10,
199
+ paddingVertical: 6,
200
+ fontSize: 10,
201
+ fontWeight: '700',
202
+ color: C.textSecondary,
203
+ textTransform: 'uppercase',
204
+ borderRightWidth: 1,
205
+ borderRightColor: C.borderSubtle,
206
+ },
207
+ miniRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: C.borderSubtle },
208
+ miniRowEven: { backgroundColor: C.bg },
209
+ miniRowOdd: { backgroundColor: C.surface },
210
+ miniCell: {
211
+ width: 130,
212
+ paddingHorizontal: 10,
213
+ paddingVertical: 7,
214
+ fontSize: 12,
215
+ color: C.text,
216
+ borderRightWidth: 1,
217
+ borderRightColor: C.borderSubtle,
218
+ },
219
+ miniCellNull: { color: C.textMuted, fontStyle: 'italic' },
220
+ });
@@ -0,0 +1,168 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet, Pressable } from 'react-native';
3
+ import { C } from '../theme';
4
+ import { Dropdown } from './Dropdown';
5
+
6
+ interface ToolbarProps {
7
+ databases: string[];
8
+ selectedDB: string | null;
9
+ tables: string[];
10
+ selectedTable: string | null;
11
+ loadingTables: boolean;
12
+ loadingData: boolean;
13
+ rowCount: number;
14
+ columnCount: number;
15
+ onSelectDB: (db: string) => void;
16
+ onSelectTable: (table: string) => void;
17
+ onRefresh: () => void;
18
+ sqlOpen: boolean;
19
+ onToggleSql: () => void;
20
+ }
21
+
22
+ export function Toolbar({
23
+ databases,
24
+ selectedDB,
25
+ tables,
26
+ selectedTable,
27
+ loadingTables,
28
+ loadingData,
29
+ rowCount,
30
+ columnCount,
31
+ onSelectDB,
32
+ onSelectTable,
33
+ onRefresh,
34
+ sqlOpen,
35
+ onToggleSql,
36
+ }: ToolbarProps) {
37
+ return (
38
+ <View style={s.toolbar}>
39
+ <View style={s.topRow}>
40
+ <View style={s.selectors}>
41
+ <Dropdown
42
+ label="Database"
43
+ value={selectedDB}
44
+ options={databases}
45
+ onSelect={onSelectDB}
46
+ loading={loadingTables && !selectedTable}
47
+ zIndex={20}
48
+ />
49
+ <View style={s.divider} />
50
+ <Dropdown
51
+ label="Table"
52
+ value={selectedTable}
53
+ options={tables}
54
+ onSelect={onSelectTable}
55
+ loading={loadingTables}
56
+ disabled={!selectedDB}
57
+ zIndex={10}
58
+ />
59
+ <View style={s.divider} />
60
+ <Pressable
61
+ style={({ pressed }) => [s.refreshBtn, pressed && s.refreshBtnPressed]}
62
+ onPress={onRefresh}
63
+ disabled={!selectedDB}
64
+ >
65
+ <Text style={[s.refreshIcon, !selectedDB && s.refreshIconDisabled]}>↻</Text>
66
+ </Pressable>
67
+ <View style={s.divider} />
68
+ <Pressable
69
+ style={({ pressed }) => [s.sqlBtn, sqlOpen && s.sqlBtnActive, !selectedDB && s.sqlBtnDisabled, pressed && { opacity: 0.8 }]}
70
+ onPress={onToggleSql}
71
+ disabled={!selectedDB}
72
+ >
73
+ <Text style={[s.sqlBtnText, sqlOpen && s.sqlBtnTextActive]}>›_ SQL</Text>
74
+ </Pressable>
75
+ </View>
76
+
77
+ <View style={s.badges}>
78
+ {selectedTable && !loadingData && (
79
+ <View style={s.badge}>
80
+ <Text style={s.badgeText}>{rowCount} rows</Text>
81
+ </View>
82
+ )}
83
+ {columnCount > 0 && !loadingData && (
84
+ <View style={[s.badge, s.badgeMuted]}>
85
+ <Text style={s.badgeText}>{columnCount} cols</Text>
86
+ </View>
87
+ )}
88
+ </View>
89
+ </View>
90
+ </View>
91
+ );
92
+ }
93
+
94
+ const s = StyleSheet.create({
95
+ toolbar: {
96
+ flexDirection: 'column',
97
+ paddingHorizontal: 20,
98
+ paddingTop: 16,
99
+ backgroundColor: C.surface,
100
+ borderBottomWidth: 1,
101
+ borderBottomColor: C.border,
102
+ zIndex: 50,
103
+ overflow: 'visible',
104
+ },
105
+ topRow: {
106
+ flexDirection: 'row',
107
+ alignItems: 'flex-end',
108
+ paddingBottom: 14,
109
+ gap: 20,
110
+ },
111
+ brand: {
112
+ flexDirection: 'row',
113
+ alignItems: 'center',
114
+ gap: 8,
115
+ paddingBottom: 2,
116
+ marginRight: 4,
117
+ },
118
+ brandMark: { fontSize: 18, color: C.accent },
119
+ brandName: { fontSize: 14, fontWeight: '700', color: C.text, letterSpacing: 0.2 },
120
+ selectors: {
121
+ flexDirection: 'row',
122
+ alignItems: 'flex-end',
123
+ flex: 1,
124
+ overflow: 'visible',
125
+ },
126
+ divider: { width: 12 },
127
+ refreshBtn: {
128
+ width: 32,
129
+ height: 32,
130
+ borderRadius: 7,
131
+ backgroundColor: C.surface2,
132
+ borderWidth: 1,
133
+ borderColor: C.border,
134
+ alignItems: 'center',
135
+ justifyContent: 'center',
136
+ marginBottom: 1,
137
+ cursor: 'pointer',
138
+ },
139
+ refreshBtnPressed: { backgroundColor: C.accentSubtle, borderColor: C.accent },
140
+ refreshIcon: { fontSize: 16, color: C.textSecondary, lineHeight: 18 },
141
+ refreshIconDisabled: { color: C.textMuted, opacity: 0.4 },
142
+ badges: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingBottom: 3 },
143
+ badge: {
144
+ paddingHorizontal: 10,
145
+ paddingVertical: 4,
146
+ backgroundColor: C.accentSubtle,
147
+ borderRadius: 20,
148
+ },
149
+ badgeMuted: { backgroundColor: C.surface2 },
150
+ badgeText: { fontSize: 11, fontWeight: '600', color: C.textSecondary },
151
+ sqlBtn: {
152
+ height: 32,
153
+ paddingHorizontal: 10,
154
+ borderRadius: 7,
155
+ backgroundColor: C.surface2,
156
+ borderWidth: 1,
157
+ borderColor: C.border,
158
+ alignItems: 'center',
159
+ justifyContent: 'center',
160
+ marginBottom: 1,
161
+ cursor: 'pointer',
162
+ marginLeft: 40
163
+ },
164
+ sqlBtnActive: { backgroundColor: C.accentSubtle, borderColor: C.accent },
165
+ sqlBtnDisabled: { opacity: 0.4 },
166
+ sqlBtnText: { fontSize: 12, fontWeight: '700', color: C.textSecondary, fontFamily: 'monospace' },
167
+ sqlBtnTextActive: { color: C.accent },
168
+ });