database-devtools 0.1.0 → 0.1.2

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 (53) hide show
  1. package/README.md +96 -0
  2. package/dist/DatabaseDevTools-CdpQJBqG.d.ts +39 -0
  3. package/dist/DatabaseDevTools-sw1wX5nW.d.cts +39 -0
  4. package/dist/adapter/index.d.cts +3 -3
  5. package/dist/adapter/index.d.ts +3 -3
  6. package/dist/{adapter-BjJm71wW.d.cts → adapter-DQyQ5ycj.d.cts} +1 -1
  7. package/dist/{adapter-DJSjDewg.d.ts → adapter-cIF5P6qI.d.ts} +1 -1
  8. package/dist/adapters/sqlite/index.d.cts +4 -4
  9. package/dist/adapters/sqlite/index.d.ts +4 -4
  10. package/dist/chunk-7UY2LDRW.js +2739 -0
  11. package/dist/chunk-7UY2LDRW.js.map +1 -0
  12. package/dist/chunk-S4Z7A4TW.js +116 -0
  13. package/dist/chunk-S4Z7A4TW.js.map +1 -0
  14. package/dist/{chunk-ZS5MZR75.js → chunk-UNVCJMJK.js} +4 -114
  15. package/dist/chunk-UNVCJMJK.js.map +1 -0
  16. package/dist/client/index.d.cts +4 -3
  17. package/dist/client/index.d.ts +4 -3
  18. package/dist/client/index.js +2 -1
  19. package/dist/client/index.js.map +1 -1
  20. package/dist/{createDevToolsServer-B7NQ_5H4.d.cts → createDevToolsServer-Cqb7xzIv.d.cts} +1 -1
  21. package/dist/{createDevToolsServer-DlNMoCJR.d.ts → createDevToolsServer-D3QnP8Yb.d.ts} +1 -1
  22. package/dist/{handleDeviceSnapshot-cYGuitSc.d.cts → handleDeviceSnapshot-CIZ_00fE.d.cts} +2 -50
  23. package/dist/{handleDeviceSnapshot-CR-4QMdk.d.ts → handleDeviceSnapshot-DF9sS1Pm.d.ts} +2 -50
  24. package/dist/index.d.cts +13 -45
  25. package/dist/index.d.ts +13 -45
  26. package/dist/index.js +8 -2741
  27. package/dist/index.js.map +1 -1
  28. package/dist/inspector/index.d.cts +2 -2
  29. package/dist/inspector/index.d.ts +2 -2
  30. package/dist/inspectors/sqlite/index.d.cts +2 -2
  31. package/dist/inspectors/sqlite/index.d.ts +2 -2
  32. package/dist/native.cjs +3946 -0
  33. package/dist/native.cjs.map +1 -0
  34. package/dist/native.d.cts +37 -0
  35. package/dist/native.d.ts +37 -0
  36. package/dist/native.js +13 -0
  37. package/dist/native.js.map +1 -0
  38. package/dist/{register-BuicFu9L.d.cts → register-DEZ0kK1p.d.cts} +2 -2
  39. package/dist/{register-aITLN9Q9.d.ts → register-Dawue1_L.d.ts} +2 -2
  40. package/dist/resolveDeviceDatabaseUrl-DhRP-h1Y.d.cts +49 -0
  41. package/dist/resolveDeviceDatabaseUrl-DhRP-h1Y.d.ts +49 -0
  42. package/dist/server/index.d.cts +1 -1
  43. package/dist/server/index.d.ts +1 -1
  44. package/dist/{snapshot-Dhyecgo2.d.cts → snapshot-Dknp_qP1.d.cts} +1 -1
  45. package/dist/{snapshot-Dhyecgo2.d.ts → snapshot-Dknp_qP1.d.ts} +1 -1
  46. package/dist/{types-CKVRCX-e.d.ts → types-6nco_RRp.d.ts} +2 -2
  47. package/dist/{types-CIA11diJ.d.cts → types-BQHZtFwx.d.cts} +2 -2
  48. package/dist/{types-CF6qEU-_.d.ts → types-DkVlU6t-.d.ts} +2 -2
  49. package/dist/{types-BRLJdBRY.d.cts → types-gT3gDw_T.d.cts} +2 -2
  50. package/dist/web/assets/{index-DQrPRTOy.js → index-DogMAkgu.js} +1 -1
  51. package/dist/web/index.html +1 -1
  52. package/package.json +22 -12
  53. package/dist/chunk-ZS5MZR75.js.map +0 -1
@@ -0,0 +1,2739 @@
1
+ import { createDevToolsClient, formatRefreshErrorMessage, handleDeviceSnapshotUpload } from './chunk-UNVCJMJK.js';
2
+ import { generateDeviceId } from './chunk-AZECU6D7.js';
3
+ import { DevToolsRole, REFRESH_TIMEOUT_MS, buildDevToolsWsUrl, DEFAULT_DEVTOOLS_PORT } from './chunk-CPVVIH5G.js';
4
+ import { resolveAdapter } from './chunk-BLOSGEQV.js';
5
+ import { detectExpoSqliteInspectable } from './chunk-I65IM47D.js';
6
+ import { __require } from './chunk-DGUM43GV.js';
7
+ import { StyleSheet, View, useWindowDimensions, PanResponder, Pressable, Modal, Text, ActivityIndicator, ScrollView, TextInput, Platform, FlatList } from 'react-native';
8
+ import { createContext, useState, useMemo, useRef, useCallback, useEffect, useContext } from 'react';
9
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
+ import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
11
+
12
+ // src/types/adapter.ts
13
+ function isWritableDatabaseAdapter(adapter) {
14
+ if (!adapter?.capabilities.transactionalWrites) {
15
+ return false;
16
+ }
17
+ const candidate = adapter;
18
+ return typeof candidate.beginTransaction === "function" && typeof candidate.commitTransaction === "function" && typeof candidate.rollbackTransaction === "function" && typeof candidate.executeWrite === "function";
19
+ }
20
+ var isEditableDatabaseAdapter = isWritableDatabaseAdapter;
21
+
22
+ // src/client/handleWriteOperations.ts
23
+ async function handleBeginTransaction(adapter, message, report) {
24
+ if (!isWritableDatabaseAdapter(adapter)) {
25
+ report({
26
+ transactionId: message.transactionId,
27
+ action: "begin",
28
+ ok: false,
29
+ message: "Database adapter does not support writes"
30
+ });
31
+ return;
32
+ }
33
+ try {
34
+ await adapter.beginTransaction();
35
+ report({
36
+ transactionId: message.transactionId,
37
+ action: "begin",
38
+ ok: true
39
+ });
40
+ } catch (error) {
41
+ report({
42
+ transactionId: message.transactionId,
43
+ action: "begin",
44
+ ok: false,
45
+ message: error instanceof Error ? error.message : "Failed to begin transaction"
46
+ });
47
+ }
48
+ }
49
+ async function handleCommitTransaction(adapter, message, report) {
50
+ if (!isWritableDatabaseAdapter(adapter)) {
51
+ report({
52
+ transactionId: message.transactionId,
53
+ action: "commit",
54
+ ok: false,
55
+ message: "Database adapter does not support writes"
56
+ });
57
+ return;
58
+ }
59
+ try {
60
+ await adapter.commitTransaction();
61
+ report({
62
+ transactionId: message.transactionId,
63
+ action: "commit",
64
+ ok: true
65
+ });
66
+ } catch (error) {
67
+ report({
68
+ transactionId: message.transactionId,
69
+ action: "commit",
70
+ ok: false,
71
+ message: error instanceof Error ? error.message : "Failed to commit transaction"
72
+ });
73
+ }
74
+ }
75
+ async function handleRollbackTransaction(adapter, message, report) {
76
+ if (!isWritableDatabaseAdapter(adapter)) {
77
+ report({
78
+ transactionId: message.transactionId,
79
+ action: "rollback",
80
+ ok: false,
81
+ message: "Database adapter does not support writes"
82
+ });
83
+ return;
84
+ }
85
+ try {
86
+ await adapter.rollbackTransaction();
87
+ report({
88
+ transactionId: message.transactionId,
89
+ action: "rollback",
90
+ ok: true
91
+ });
92
+ } catch (error) {
93
+ report({
94
+ transactionId: message.transactionId,
95
+ action: "rollback",
96
+ ok: false,
97
+ message: error instanceof Error ? error.message : "Failed to roll back transaction"
98
+ });
99
+ }
100
+ }
101
+ async function handleExecuteWrite(adapter, message, report) {
102
+ if (!isWritableDatabaseAdapter(adapter)) {
103
+ report({
104
+ writeId: message.writeId,
105
+ transactionId: message.transactionId,
106
+ ok: false,
107
+ message: "Database adapter does not support writes"
108
+ });
109
+ return;
110
+ }
111
+ try {
112
+ const result = await adapter.executeWrite(message.operation);
113
+ report({
114
+ writeId: message.writeId,
115
+ transactionId: message.transactionId,
116
+ ok: true,
117
+ rowsAffected: result.rowsAffected
118
+ });
119
+ } catch (error) {
120
+ report({
121
+ writeId: message.writeId,
122
+ transactionId: message.transactionId,
123
+ ok: false,
124
+ message: error instanceof Error ? error.message : "Write operation failed"
125
+ });
126
+ }
127
+ }
128
+
129
+ // src/utils/sqlSafety.ts
130
+ var BLOCKED_KEYWORDS = /\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|REPLACE|ATTACH|DETACH|REINDEX|VACUUM|TRUNCATE)\b/i;
131
+ function validateReadOnlySql(sql) {
132
+ const trimmed = sql.trim();
133
+ if (!trimmed) {
134
+ throw new Error("SQL query is empty");
135
+ }
136
+ if (BLOCKED_KEYWORDS.test(trimmed)) {
137
+ throw new Error(
138
+ "DevTools is read-only; only SELECT, PRAGMA, EXPLAIN, and WITH queries are allowed"
139
+ );
140
+ }
141
+ const statements = trimmed.split(";").map((statement) => statement.trim()).filter(Boolean);
142
+ for (const statement of statements) {
143
+ const leading = statement.replace(/^\s*(--[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*/g, "").trim();
144
+ const keyword = leading.split(/\s+/)[0]?.toUpperCase();
145
+ if (!keyword) {
146
+ continue;
147
+ }
148
+ if (keyword !== "SELECT" && keyword !== "PRAGMA" && keyword !== "EXPLAIN" && keyword !== "WITH") {
149
+ throw new Error(`DevTools is read-only; "${keyword}" statements are not allowed`);
150
+ }
151
+ }
152
+ }
153
+
154
+ // src/mobile/sqliteTableQuery.ts
155
+ function quoteIdentifier(name) {
156
+ return `"${name.replace(/"/g, '""')}"`;
157
+ }
158
+ function buildSearchClause(columns, search) {
159
+ const term = search?.trim();
160
+ if (!term || columns.length === 0) {
161
+ return { clause: "", params: [] };
162
+ }
163
+ const pattern = `%${term}%`;
164
+ const conditions = columns.map(
165
+ (column) => `CAST(${quoteIdentifier(column.name)} AS TEXT) LIKE ?`
166
+ );
167
+ return {
168
+ clause: ` WHERE (${conditions.join(" OR ")})`,
169
+ params: columns.map(() => pattern)
170
+ };
171
+ }
172
+ function resolveSortColumn(columns, sortColumn) {
173
+ if (!sortColumn) {
174
+ return null;
175
+ }
176
+ return columns.some((column) => column.name === sortColumn) ? sortColumn : null;
177
+ }
178
+
179
+ // src/mobile/createExpoSqliteInspector.ts
180
+ function normalizeCell(cell) {
181
+ if (cell === null || cell === void 0) {
182
+ return null;
183
+ }
184
+ if (typeof cell === "number" || typeof cell === "string") {
185
+ return cell;
186
+ }
187
+ if (typeof cell === "boolean") {
188
+ return cell ? 1 : 0;
189
+ }
190
+ if (cell instanceof Uint8Array) {
191
+ return `[BLOB ${cell.byteLength} bytes]`;
192
+ }
193
+ return String(cell);
194
+ }
195
+ function objectRowsToMatrix(rows) {
196
+ if (rows.length === 0) {
197
+ return { columns: [], matrix: [] };
198
+ }
199
+ const columns = Object.keys(rows[0]);
200
+ return {
201
+ columns,
202
+ matrix: rows.map((row) => columns.map((column) => normalizeCell(row[column])))
203
+ };
204
+ }
205
+ function createExpoSqliteInspector(database) {
206
+ let operationQueue = Promise.resolve();
207
+ const enqueue = async (operation) => {
208
+ const next = operationQueue.then(operation);
209
+ operationQueue = next.then(
210
+ () => void 0,
211
+ () => void 0
212
+ );
213
+ return next;
214
+ };
215
+ const listTablesInternal = async () => {
216
+ const masterRows = await database.getAllAsync(
217
+ `SELECT name FROM sqlite_master
218
+ WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
219
+ ORDER BY name`
220
+ );
221
+ const tables = [];
222
+ for (const row of masterRows) {
223
+ const countRow = await database.getFirstAsync(
224
+ `SELECT COUNT(*) AS count FROM ${quoteIdentifier(row.name)}`
225
+ );
226
+ tables.push({
227
+ name: row.name,
228
+ rowCount: Number(countRow?.count ?? 0)
229
+ });
230
+ }
231
+ return tables;
232
+ };
233
+ const getTableColumnsInternal = async (table) => {
234
+ const rows = await database.getAllAsync(
235
+ `PRAGMA table_info(${quoteIdentifier(table)})`
236
+ );
237
+ return rows.map((row) => ({
238
+ name: row.name,
239
+ type: row.type || "ANY",
240
+ notNull: row.notnull === 1,
241
+ pk: row.pk === 1,
242
+ defaultValue: row.dflt_value === null || row.dflt_value === void 0 ? null : String(row.dflt_value)
243
+ }));
244
+ };
245
+ return {
246
+ listTables: () => enqueue(listTablesInternal),
247
+ getSchema: () => enqueue(async () => {
248
+ const tables = await listTablesInternal();
249
+ return Promise.all(
250
+ tables.map(async (table) => ({
251
+ name: table.name,
252
+ columns: await getTableColumnsInternal(table.name)
253
+ }))
254
+ );
255
+ }),
256
+ getTableColumns: (table) => enqueue(() => getTableColumnsInternal(table)),
257
+ getDatabaseInfo: () => enqueue(async () => {
258
+ const tables = await listTablesInternal();
259
+ const versionRow = await database.getFirstAsync(
260
+ "SELECT sqlite_version() AS version"
261
+ );
262
+ const pageRow = await database.getFirstAsync(
263
+ "SELECT page_size, page_count FROM pragma_page_size(), pragma_page_count()"
264
+ );
265
+ const pageSize = Number(pageRow?.page_size ?? 0);
266
+ const pageCount = Number(pageRow?.page_count ?? 0);
267
+ return {
268
+ name: database.databasePath.split("/").pop() ?? database.databasePath,
269
+ path: database.databasePath,
270
+ tableCount: tables.length,
271
+ sqliteVersion: versionRow?.version ?? "unknown",
272
+ pageSize,
273
+ pageCount,
274
+ estimatedSizeBytes: pageSize * pageCount
275
+ };
276
+ }),
277
+ fetchTablePage: (request) => enqueue(async () => {
278
+ const columns = await getTableColumnsInternal(request.table);
279
+ const { clause, params: searchParams } = buildSearchClause(columns, request.search);
280
+ const quotedTable = quoteIdentifier(request.table);
281
+ const countRow = await database.getFirstAsync(
282
+ `SELECT COUNT(*) AS count FROM ${quotedTable}${clause}`,
283
+ searchParams
284
+ );
285
+ const totalCount = Number(countRow?.count ?? 0);
286
+ const pageSize = Math.max(1, request.pageSize);
287
+ const page = Math.max(1, request.page);
288
+ const offset = (page - 1) * pageSize;
289
+ const sortColumn = resolveSortColumn(columns, request.sortColumn);
290
+ const orderClause = sortColumn ? ` ORDER BY ${quoteIdentifier(sortColumn)} ${request.sortDir === "desc" ? "DESC" : "ASC"}` : "";
291
+ const rows = await database.getAllAsync(
292
+ `SELECT * FROM ${quotedTable}${clause}${orderClause} LIMIT ? OFFSET ?`,
293
+ [...searchParams, pageSize, offset]
294
+ );
295
+ const { columns: resultColumns, matrix } = objectRowsToMatrix(rows);
296
+ return {
297
+ columns: resultColumns,
298
+ rows: matrix,
299
+ totalCount,
300
+ page,
301
+ pageSize
302
+ };
303
+ }),
304
+ executeQuery: (sql) => enqueue(async () => {
305
+ validateReadOnlySql(sql);
306
+ const started = Date.now();
307
+ const rows = await database.getAllAsync(sql);
308
+ const { columns, matrix } = objectRowsToMatrix(rows);
309
+ return {
310
+ columns,
311
+ rows: matrix,
312
+ rowCount: matrix.length,
313
+ durationMs: Date.now() - started
314
+ };
315
+ })
316
+ };
317
+ }
318
+
319
+ // src/utils/isDevToolsEnabled.ts
320
+ function isDevToolsEnabled(enabled) {
321
+ if (enabled === false) {
322
+ return false;
323
+ }
324
+ if (enabled === true) {
325
+ return true;
326
+ }
327
+ return typeof __DEV__ !== "undefined" && __DEV__;
328
+ }
329
+ var DevToolsContext = createContext(null);
330
+ function useDevTools() {
331
+ const context = useContext(DevToolsContext);
332
+ if (!context) {
333
+ throw new Error("useDevTools must be used within DevToolsProvider");
334
+ }
335
+ return context;
336
+ }
337
+
338
+ // src/utils/resolveDevToolsHost.ts
339
+ var ANDROID_LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
340
+ var ANDROID_EMULATOR_HOST = "10.0.2.2";
341
+ function isAndroidLoopbackHost(host) {
342
+ return ANDROID_LOOPBACK_HOSTS.has(host.toLowerCase());
343
+ }
344
+ function readMetroDevHostFromConstants(constants) {
345
+ const hostUri = constants?.expoConfig?.hostUri ?? constants?.expoGoConfig?.debuggerHost ?? constants?.manifest2?.extra?.expoGo?.debuggerHost ?? constants?.manifest?.debuggerHost;
346
+ if (typeof hostUri !== "string" || hostUri.length === 0) {
347
+ return void 0;
348
+ }
349
+ return hostUri.split(":")[0];
350
+ }
351
+ function resolveDevToolsHostFromInputs(platformOs, metroHost) {
352
+ if (platformOs === "android") {
353
+ if (metroHost && !isAndroidLoopbackHost(metroHost)) {
354
+ return metroHost;
355
+ }
356
+ return ANDROID_EMULATOR_HOST;
357
+ }
358
+ if (metroHost) {
359
+ return metroHost;
360
+ }
361
+ return "localhost";
362
+ }
363
+ function getConnectionHintForPlatform(platformOs, serverUrl) {
364
+ try {
365
+ const parsed = new URL(serverUrl);
366
+ const host = parsed.hostname.toLowerCase();
367
+ if (platformOs === "android" && isAndroidLoopbackHost(host)) {
368
+ return `On Android, "${host}" refers to the device itself. Use ${ANDROID_EMULATOR_HOST} (emulator) or your PC's LAN IP (physical device).`;
369
+ }
370
+ if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
371
+ return "Server URL must start with ws:// or wss://";
372
+ }
373
+ } catch {
374
+ return "Invalid WebSocket URL.";
375
+ }
376
+ return null;
377
+ }
378
+ function getPlatformOs() {
379
+ try {
380
+ const { Platform: Platform2 } = __require("react-native");
381
+ return Platform2.OS;
382
+ } catch {
383
+ return "web";
384
+ }
385
+ }
386
+ function readMetroDevHostRuntime() {
387
+ try {
388
+ const mod = __require("expo-constants");
389
+ return readMetroDevHostFromConstants(mod.default);
390
+ } catch {
391
+ return void 0;
392
+ }
393
+ }
394
+ function resolveDevToolsHost() {
395
+ return resolveDevToolsHostFromInputs(getPlatformOs(), readMetroDevHostRuntime());
396
+ }
397
+ function getConnectionHint(serverUrl) {
398
+ return getConnectionHintForPlatform(getPlatformOs(), serverUrl);
399
+ }
400
+ function getExpoConstants() {
401
+ try {
402
+ const mod = __require("expo-constants");
403
+ return mod.default;
404
+ } catch {
405
+ return void 0;
406
+ }
407
+ }
408
+ function resolveDeviceMetadata() {
409
+ const constants = getExpoConstants();
410
+ const config = constants?.expoConfig;
411
+ const metadata = {
412
+ platform: Platform.OS,
413
+ appName: config?.name ?? "React Native App"
414
+ };
415
+ if (config?.version) {
416
+ metadata.appVersion = config.version;
417
+ }
418
+ const bundleId = config?.ios?.bundleIdentifier ?? config?.android?.package;
419
+ if (bundleId) {
420
+ metadata.bundleId = bundleId;
421
+ }
422
+ return metadata;
423
+ }
424
+
425
+ // src/utils/resolveServerUrl.ts
426
+ function normalizeServerUrl(url) {
427
+ const trimmed = url.trim();
428
+ if (trimmed.startsWith("ws://") || trimmed.startsWith("wss://")) {
429
+ return trimmed;
430
+ }
431
+ return buildDevToolsWsUrl(trimmed.replace(/^https?:\/\//, ""), DEFAULT_DEVTOOLS_PORT);
432
+ }
433
+ function resolveServerUrl(serverUrl) {
434
+ if (serverUrl) {
435
+ return normalizeServerUrl(serverUrl);
436
+ }
437
+ const envUrl = typeof process !== "undefined" ? process.env.EXPO_PUBLIC_DATABASE_DEVTOOLS_URL : void 0;
438
+ if (envUrl) {
439
+ return normalizeServerUrl(envUrl);
440
+ }
441
+ return buildDevToolsWsUrl(resolveDevToolsHost(), DEFAULT_DEVTOOLS_PORT);
442
+ }
443
+
444
+ // src/utils/persistDeviceId.ts
445
+ var PERSISTED_DEVICE_ID_STORAGE_KEY = "@database-devtools/device-id";
446
+ var memoryDeviceId = null;
447
+ function createAsyncStorageDeviceIdStore() {
448
+ try {
449
+ const AsyncStorage = __require("@react-native-async-storage/async-storage").default;
450
+ return AsyncStorage;
451
+ } catch {
452
+ return null;
453
+ }
454
+ }
455
+ async function loadOrCreateDeviceId(storage = createAsyncStorageDeviceIdStore()) {
456
+ if (storage) {
457
+ const stored = await storage.getItem(PERSISTED_DEVICE_ID_STORAGE_KEY);
458
+ if (stored) {
459
+ return stored;
460
+ }
461
+ const created = generateDeviceId();
462
+ await storage.setItem(PERSISTED_DEVICE_ID_STORAGE_KEY, created);
463
+ return created;
464
+ }
465
+ if (!memoryDeviceId) {
466
+ memoryDeviceId = generateDeviceId();
467
+ }
468
+ return memoryDeviceId;
469
+ }
470
+ function DevToolsProvider({
471
+ children,
472
+ database,
473
+ type,
474
+ adapter: explicitAdapter,
475
+ serverUrl: initialServerUrl,
476
+ onConnectionStateChange
477
+ }) {
478
+ const [connectionState, setConnectionState] = useState("connecting");
479
+ const [connectionError, setConnectionError] = useState(null);
480
+ const [serverUrl, setServerUrl] = useState(() => resolveServerUrl(initialServerUrl));
481
+ const [deviceId, setDeviceId] = useState();
482
+ const [deviceIdReady, setDeviceIdReady] = useState(false);
483
+ const [settingsVisible, setSettingsVisible] = useState(false);
484
+ const [launcherVisible, setLauncherVisible] = useState(false);
485
+ const [explorerVisible, setExplorerVisible] = useState(false);
486
+ const [mobileInspector, setMobileInspector] = useState(null);
487
+ const [resolvedAdapter, setResolvedAdapter] = useState(explicitAdapter);
488
+ const [adapterError, setAdapterError] = useState(null);
489
+ const [exportState, setExportState] = useState("idle");
490
+ const [exportError, setExportError] = useState(null);
491
+ const metadata = useMemo(() => resolveDeviceMetadata(), []);
492
+ const clientRef = useRef(null);
493
+ const onConnectionStateChangeRef = useRef(onConnectionStateChange);
494
+ const databaseRef = useRef(resolvedAdapter);
495
+ const rollbackOpenTransactionRef = useRef(null);
496
+ const exportWaitersRef = useRef(null);
497
+ const exportTimeoutRef = useRef(null);
498
+ const exportSuccessTimeoutRef = useRef(null);
499
+ const clearExportWaiters = useCallback(() => {
500
+ if (exportTimeoutRef.current) {
501
+ clearTimeout(exportTimeoutRef.current);
502
+ exportTimeoutRef.current = null;
503
+ }
504
+ exportWaitersRef.current = null;
505
+ }, []);
506
+ const resetExportSuccessLater = useCallback(() => {
507
+ if (exportSuccessTimeoutRef.current) {
508
+ clearTimeout(exportSuccessTimeoutRef.current);
509
+ }
510
+ exportSuccessTimeoutRef.current = setTimeout(() => {
511
+ setExportState((current) => current === "success" ? "idle" : current);
512
+ exportSuccessTimeoutRef.current = null;
513
+ }, 3e3);
514
+ }, []);
515
+ const finishExportSuccess = useCallback(() => {
516
+ clearExportWaiters();
517
+ setExportState("success");
518
+ setExportError(null);
519
+ resetExportSuccessLater();
520
+ }, [clearExportWaiters, resetExportSuccessLater]);
521
+ const finishExportFailure = useCallback(
522
+ (message) => {
523
+ clearExportWaiters();
524
+ setExportState("error");
525
+ setExportError(message);
526
+ },
527
+ [clearExportWaiters]
528
+ );
529
+ useEffect(() => {
530
+ onConnectionStateChangeRef.current = onConnectionStateChange;
531
+ }, [onConnectionStateChange]);
532
+ useEffect(() => {
533
+ let cancelled = false;
534
+ async function resolveDatabaseAdapter() {
535
+ if (explicitAdapter) {
536
+ setResolvedAdapter(explicitAdapter);
537
+ setAdapterError(null);
538
+ return;
539
+ }
540
+ if (!database) {
541
+ setResolvedAdapter(void 0);
542
+ setAdapterError(null);
543
+ return;
544
+ }
545
+ try {
546
+ const nextAdapter = await resolveAdapter(database, { type });
547
+ if (!cancelled) {
548
+ setResolvedAdapter(nextAdapter);
549
+ setAdapterError(null);
550
+ }
551
+ } catch (error) {
552
+ if (!cancelled) {
553
+ setResolvedAdapter(void 0);
554
+ setAdapterError(error instanceof Error ? error.message : "Failed to resolve database adapter");
555
+ }
556
+ }
557
+ }
558
+ void resolveDatabaseAdapter();
559
+ return () => {
560
+ cancelled = true;
561
+ };
562
+ }, [database, type, explicitAdapter]);
563
+ useEffect(() => {
564
+ if (detectExpoSqliteInspectable(database)) {
565
+ setMobileInspector(createExpoSqliteInspector(database));
566
+ return;
567
+ }
568
+ setMobileInspector(null);
569
+ }, [database]);
570
+ useEffect(() => {
571
+ databaseRef.current = resolvedAdapter;
572
+ }, [resolvedAdapter]);
573
+ useEffect(() => {
574
+ rollbackOpenTransactionRef.current = async () => {
575
+ const adapter = databaseRef.current;
576
+ if (!isWritableDatabaseAdapter(adapter)) {
577
+ return;
578
+ }
579
+ try {
580
+ await adapter.rollbackTransaction();
581
+ } catch {
582
+ }
583
+ };
584
+ }, [resolvedAdapter]);
585
+ useEffect(() => {
586
+ let cancelled = false;
587
+ void loadOrCreateDeviceId().then((persistedId) => {
588
+ if (!cancelled) {
589
+ setDeviceId(persistedId);
590
+ setDeviceIdReady(true);
591
+ }
592
+ });
593
+ return () => {
594
+ cancelled = true;
595
+ };
596
+ }, []);
597
+ useEffect(() => {
598
+ if (!deviceIdReady || !deviceId) {
599
+ return;
600
+ }
601
+ const client = createDevToolsClient({
602
+ serverUrl,
603
+ role: DevToolsRole.MOBILE,
604
+ deviceId,
605
+ metadata,
606
+ onConnect: () => {
607
+ setConnectionError(null);
608
+ },
609
+ onConnectionStateChange: (state) => {
610
+ setConnectionState(state);
611
+ onConnectionStateChangeRef.current?.(state);
612
+ },
613
+ onError: (error) => {
614
+ setConnectionError(error.message);
615
+ },
616
+ onSnapshotUploadRequested: async (message) => {
617
+ try {
618
+ await handleDeviceSnapshotUpload(databaseRef.current, message, {
619
+ hubServerUrl: serverUrl,
620
+ deviceId: clientRef.current?.getDeviceId()
621
+ });
622
+ if (exportWaitersRef.current) {
623
+ exportWaitersRef.current.resolve();
624
+ clearExportWaiters();
625
+ }
626
+ } catch (error) {
627
+ const errorMessage = error instanceof Error ? error.message : "Database export or upload failed";
628
+ if (exportWaitersRef.current) {
629
+ exportWaitersRef.current.reject(new Error(errorMessage));
630
+ clearExportWaiters();
631
+ return;
632
+ }
633
+ console.error("[database-devtools] Snapshot export or upload failed:", errorMessage);
634
+ }
635
+ },
636
+ onExportSnapshotError: (message) => {
637
+ const errorMessage = formatRefreshErrorMessage(message.code, message.message);
638
+ if (exportWaitersRef.current) {
639
+ exportWaitersRef.current.reject(new Error(errorMessage));
640
+ clearExportWaiters();
641
+ }
642
+ },
643
+ onBeginTransaction: async (message) => {
644
+ const client2 = clientRef.current;
645
+ if (!client2) {
646
+ return;
647
+ }
648
+ await handleBeginTransaction(databaseRef.current, message, (ack) => {
649
+ client2.sendTransactionAck(ack);
650
+ });
651
+ },
652
+ onCommitTransaction: async (message) => {
653
+ const client2 = clientRef.current;
654
+ if (!client2) {
655
+ return;
656
+ }
657
+ await handleCommitTransaction(databaseRef.current, message, (ack) => {
658
+ client2.sendTransactionAck(ack);
659
+ });
660
+ },
661
+ onRollbackTransaction: async (message) => {
662
+ const client2 = clientRef.current;
663
+ if (!client2) {
664
+ return;
665
+ }
666
+ await handleRollbackTransaction(databaseRef.current, message, (ack) => {
667
+ client2.sendTransactionAck(ack);
668
+ });
669
+ },
670
+ onExecuteWrite: async (message) => {
671
+ const client2 = clientRef.current;
672
+ if (!client2) {
673
+ return;
674
+ }
675
+ await handleExecuteWrite(databaseRef.current, message, (ack) => {
676
+ client2.sendWriteAck(ack);
677
+ });
678
+ }
679
+ });
680
+ clientRef.current = client;
681
+ setDeviceId(client.getDeviceId());
682
+ client.connect();
683
+ return () => {
684
+ void rollbackOpenTransactionRef.current?.();
685
+ client.disconnect();
686
+ clientRef.current = null;
687
+ clearExportWaiters();
688
+ if (exportSuccessTimeoutRef.current) {
689
+ clearTimeout(exportSuccessTimeoutRef.current);
690
+ exportSuccessTimeoutRef.current = null;
691
+ }
692
+ };
693
+ }, [serverUrl, metadata, clearExportWaiters, deviceIdReady, deviceId]);
694
+ const openLauncher = useCallback(() => {
695
+ setLauncherVisible(true);
696
+ }, []);
697
+ const closeLauncher = useCallback(() => {
698
+ setLauncherVisible(false);
699
+ }, []);
700
+ const openExplorer = useCallback(() => {
701
+ setLauncherVisible(false);
702
+ setExplorerVisible(true);
703
+ }, []);
704
+ const closeExplorer = useCallback(() => {
705
+ setExplorerVisible(false);
706
+ }, []);
707
+ const openSettings = useCallback(() => {
708
+ setLauncherVisible(false);
709
+ setSettingsVisible(true);
710
+ }, []);
711
+ const closeSettings = useCallback(() => {
712
+ setSettingsVisible(false);
713
+ }, []);
714
+ const reconnect = useCallback((url) => {
715
+ setConnectionError(null);
716
+ setServerUrl(normalizeServerUrl(url));
717
+ }, []);
718
+ const exportDatabase = useCallback(async () => {
719
+ if (exportState === "exporting") {
720
+ return;
721
+ }
722
+ if (connectionState !== "connected") {
723
+ finishExportFailure("Connect to the DevTools hub first");
724
+ return;
725
+ }
726
+ if (!resolvedAdapter) {
727
+ finishExportFailure(adapterError ?? "No database adapter connected");
728
+ return;
729
+ }
730
+ const client = clientRef.current;
731
+ if (!client) {
732
+ finishExportFailure("DevTools client is not ready");
733
+ return;
734
+ }
735
+ setExportState("exporting");
736
+ setExportError(null);
737
+ try {
738
+ await new Promise((resolve, reject) => {
739
+ exportWaitersRef.current = { resolve, reject };
740
+ exportTimeoutRef.current = setTimeout(() => {
741
+ exportWaitersRef.current?.reject(new Error("Export timed out"));
742
+ clearExportWaiters();
743
+ }, REFRESH_TIMEOUT_MS);
744
+ client.requestExportSnapshot();
745
+ });
746
+ finishExportSuccess();
747
+ } catch (error) {
748
+ const message = error instanceof Error ? error.message : "Export failed";
749
+ finishExportFailure(message);
750
+ }
751
+ }, [
752
+ adapterError,
753
+ connectionState,
754
+ exportState,
755
+ finishExportFailure,
756
+ resolvedAdapter
757
+ ]);
758
+ const connectionHint = useMemo(() => getConnectionHint(serverUrl), [serverUrl]);
759
+ const value = useMemo(
760
+ () => ({
761
+ connectionState,
762
+ connectionError,
763
+ connectionHint,
764
+ deviceId,
765
+ serverUrl,
766
+ metadata,
767
+ database: resolvedAdapter,
768
+ adapterError,
769
+ mobileInspector,
770
+ launcherVisible,
771
+ openLauncher,
772
+ closeLauncher,
773
+ explorerVisible,
774
+ openExplorer,
775
+ closeExplorer,
776
+ settingsVisible,
777
+ openSettings,
778
+ closeSettings,
779
+ reconnect,
780
+ exportState,
781
+ exportError,
782
+ exportDatabase
783
+ }),
784
+ [
785
+ connectionState,
786
+ connectionError,
787
+ connectionHint,
788
+ deviceId,
789
+ serverUrl,
790
+ metadata,
791
+ resolvedAdapter,
792
+ adapterError,
793
+ mobileInspector,
794
+ launcherVisible,
795
+ openLauncher,
796
+ closeLauncher,
797
+ explorerVisible,
798
+ openExplorer,
799
+ closeExplorer,
800
+ settingsVisible,
801
+ openSettings,
802
+ closeSettings,
803
+ reconnect,
804
+ exportState,
805
+ exportError,
806
+ exportDatabase
807
+ ]
808
+ );
809
+ return /* @__PURE__ */ jsx(DevToolsContext.Provider, { value, children });
810
+ }
811
+ function getStatusConfig(state) {
812
+ switch (state) {
813
+ case "connected":
814
+ return { label: "Connected", color: "#166534", backgroundColor: "#dcfce7" };
815
+ case "reconnecting":
816
+ return { label: "Reconnecting", color: "#92400e", backgroundColor: "#fef3c7" };
817
+ case "connecting":
818
+ return { label: "Connecting", color: "#92400e", backgroundColor: "#fef3c7" };
819
+ case "disconnected":
820
+ return { label: "Disconnected", color: "#991b1b", backgroundColor: "#fee2e2" };
821
+ }
822
+ }
823
+ function getConnectionDotColor(state) {
824
+ switch (state) {
825
+ case "connected":
826
+ return "#22c55e";
827
+ case "reconnecting":
828
+ case "connecting":
829
+ return "#eab308";
830
+ case "disconnected":
831
+ return "#ef4444";
832
+ }
833
+ }
834
+ function ConnectionStatusBadge({ state }) {
835
+ const config = getStatusConfig(state);
836
+ return /* @__PURE__ */ jsxs(View, { style: [styles.badge, { backgroundColor: config.backgroundColor }], children: [
837
+ /* @__PURE__ */ jsx(View, { style: [styles.dot, { backgroundColor: getConnectionDotColor(state) }] }),
838
+ /* @__PURE__ */ jsx(Text, { style: [styles.label, { color: config.color }], children: config.label })
839
+ ] });
840
+ }
841
+ var styles = StyleSheet.create({
842
+ badge: {
843
+ flexDirection: "row",
844
+ alignItems: "center",
845
+ alignSelf: "flex-start",
846
+ paddingHorizontal: 10,
847
+ paddingVertical: 6,
848
+ borderRadius: 999,
849
+ gap: 6
850
+ },
851
+ dot: {
852
+ width: 8,
853
+ height: 8,
854
+ borderRadius: 4
855
+ },
856
+ label: {
857
+ fontSize: 13,
858
+ fontWeight: "600"
859
+ }
860
+ });
861
+ function DevToolsLauncherModal() {
862
+ const {
863
+ launcherVisible,
864
+ closeLauncher,
865
+ openExplorer,
866
+ openSettings,
867
+ connectionState,
868
+ mobileInspector,
869
+ exportState,
870
+ exportError,
871
+ exportDatabase,
872
+ database,
873
+ adapterError
874
+ } = useDevTools();
875
+ const isExporting = exportState === "exporting";
876
+ const exportDisabled = connectionState !== "connected" || !database || isExporting || Boolean(adapterError);
877
+ return /* @__PURE__ */ jsx(
878
+ Modal,
879
+ {
880
+ animationType: "slide",
881
+ onRequestClose: closeLauncher,
882
+ transparent: true,
883
+ visible: launcherVisible,
884
+ children: /* @__PURE__ */ jsx(View, { style: styles2.overlay, children: /* @__PURE__ */ jsxs(View, { style: styles2.sheet, children: [
885
+ /* @__PURE__ */ jsxs(View, { style: styles2.header, children: [
886
+ /* @__PURE__ */ jsx(Text, { style: styles2.title, children: "Database DevTools" }),
887
+ /* @__PURE__ */ jsx(Pressable, { accessibilityLabel: "Close launcher", onPress: closeLauncher, children: /* @__PURE__ */ jsx(Text, { style: styles2.closeButton, children: "Close" }) })
888
+ ] }),
889
+ /* @__PURE__ */ jsxs(View, { style: styles2.body, children: [
890
+ /* @__PURE__ */ jsx(ConnectionStatusBadge, { state: connectionState }),
891
+ /* @__PURE__ */ jsx(Text, { style: styles2.hint, children: "Browse and query the on-device database, or connect to the web inspector." }),
892
+ /* @__PURE__ */ jsx(
893
+ Pressable,
894
+ {
895
+ accessibilityLabel: "View database on device",
896
+ disabled: !mobileInspector,
897
+ onPress: openExplorer,
898
+ style: [styles2.primaryButton, !mobileInspector && styles2.primaryButtonDisabled],
899
+ children: /* @__PURE__ */ jsx(Text, { style: styles2.primaryLabel, children: "View Database" })
900
+ }
901
+ ),
902
+ !mobileInspector ? /* @__PURE__ */ jsx(Text, { style: styles2.warning, children: "On-device explorer requires an Expo SQLite database instance." }) : null,
903
+ /* @__PURE__ */ jsx(
904
+ Pressable,
905
+ {
906
+ accessibilityLabel: "Open DevTools settings",
907
+ onPress: openSettings,
908
+ style: styles2.secondaryButton,
909
+ children: /* @__PURE__ */ jsx(Text, { style: styles2.secondaryLabel, children: "DevTools Settings" })
910
+ }
911
+ ),
912
+ /* @__PURE__ */ jsx(
913
+ Pressable,
914
+ {
915
+ accessibilityLabel: "Export database to web inspector",
916
+ accessibilityState: { busy: isExporting, disabled: exportDisabled },
917
+ disabled: exportDisabled,
918
+ onPress: () => void exportDatabase(),
919
+ style: [styles2.ghostButton, exportDisabled && !isExporting && styles2.buttonDisabled],
920
+ children: /* @__PURE__ */ jsxs(View, { style: styles2.exportContent, children: [
921
+ isExporting ? /* @__PURE__ */ jsx(ActivityIndicator, { color: "#0f172a", size: "small" }) : null,
922
+ /* @__PURE__ */ jsx(Text, { style: styles2.ghostLabel, children: isExporting ? "Exporting\u2026" : "Export to Web Inspector" })
923
+ ] })
924
+ }
925
+ ),
926
+ connectionState !== "connected" ? /* @__PURE__ */ jsx(Text, { style: styles2.hint, children: "Connect to the hub in settings before exporting." }) : null,
927
+ exportState === "success" ? /* @__PURE__ */ jsx(Text, { style: styles2.success, children: "Sent to web inspector" }) : null,
928
+ exportError ? /* @__PURE__ */ jsx(Text, { style: styles2.error, children: exportError }) : null,
929
+ adapterError ? /* @__PURE__ */ jsx(Text, { style: styles2.error, children: adapterError }) : null
930
+ ] })
931
+ ] }) })
932
+ }
933
+ );
934
+ }
935
+ var styles2 = StyleSheet.create({
936
+ overlay: {
937
+ flex: 1,
938
+ backgroundColor: "rgba(0, 0, 0, 0.45)",
939
+ justifyContent: "flex-end"
940
+ },
941
+ sheet: {
942
+ backgroundColor: "#ffffff",
943
+ borderTopLeftRadius: 16,
944
+ borderTopRightRadius: 16,
945
+ overflow: "hidden"
946
+ },
947
+ header: {
948
+ flexDirection: "row",
949
+ alignItems: "center",
950
+ justifyContent: "space-between",
951
+ paddingHorizontal: 20,
952
+ paddingVertical: 16,
953
+ borderBottomWidth: StyleSheet.hairlineWidth,
954
+ borderBottomColor: "#e2e8f0"
955
+ },
956
+ title: {
957
+ fontSize: 18,
958
+ fontWeight: "700",
959
+ color: "#0f172a"
960
+ },
961
+ closeButton: {
962
+ fontSize: 16,
963
+ color: "#2563eb",
964
+ fontWeight: "600"
965
+ },
966
+ body: {
967
+ padding: 20,
968
+ gap: 12
969
+ },
970
+ hint: {
971
+ fontSize: 13,
972
+ color: "#64748b",
973
+ lineHeight: 20
974
+ },
975
+ warning: {
976
+ fontSize: 12,
977
+ color: "#b45309",
978
+ marginTop: -4
979
+ },
980
+ primaryButton: {
981
+ backgroundColor: "#2563eb",
982
+ borderRadius: 8,
983
+ paddingVertical: 14,
984
+ alignItems: "center"
985
+ },
986
+ primaryButtonDisabled: {
987
+ opacity: 0.5
988
+ },
989
+ primaryLabel: {
990
+ color: "#f8fafc",
991
+ fontSize: 16,
992
+ fontWeight: "700"
993
+ },
994
+ secondaryButton: {
995
+ backgroundColor: "#1e293b",
996
+ borderRadius: 8,
997
+ paddingVertical: 12,
998
+ alignItems: "center"
999
+ },
1000
+ secondaryLabel: {
1001
+ color: "#f8fafc",
1002
+ fontSize: 15,
1003
+ fontWeight: "600"
1004
+ },
1005
+ ghostButton: {
1006
+ borderWidth: 1,
1007
+ borderColor: "#cbd5e1",
1008
+ borderRadius: 8,
1009
+ paddingVertical: 12,
1010
+ alignItems: "center"
1011
+ },
1012
+ buttonDisabled: {
1013
+ opacity: 0.5
1014
+ },
1015
+ ghostLabel: {
1016
+ color: "#0f172a",
1017
+ fontSize: 15,
1018
+ fontWeight: "600"
1019
+ },
1020
+ exportContent: {
1021
+ flexDirection: "row",
1022
+ alignItems: "center",
1023
+ gap: 8
1024
+ },
1025
+ success: {
1026
+ fontSize: 13,
1027
+ color: "#15803d"
1028
+ },
1029
+ error: {
1030
+ fontSize: 13,
1031
+ color: "#dc2626"
1032
+ }
1033
+ });
1034
+ function DevToolsSettingsModal() {
1035
+ const {
1036
+ settingsVisible,
1037
+ closeSettings,
1038
+ connectionState,
1039
+ connectionError,
1040
+ connectionHint,
1041
+ deviceId,
1042
+ serverUrl,
1043
+ metadata,
1044
+ database,
1045
+ adapterError,
1046
+ reconnect,
1047
+ exportState,
1048
+ exportError,
1049
+ exportDatabase
1050
+ } = useDevTools();
1051
+ const [draftUrl, setDraftUrl] = useState(serverUrl);
1052
+ useEffect(() => {
1053
+ if (settingsVisible) {
1054
+ setDraftUrl(serverUrl);
1055
+ }
1056
+ }, [settingsVisible, serverUrl]);
1057
+ const handleReconnect = () => {
1058
+ const trimmed = draftUrl.trim();
1059
+ if (trimmed) {
1060
+ reconnect(trimmed);
1061
+ }
1062
+ };
1063
+ const handleExportDatabase = () => {
1064
+ void exportDatabase();
1065
+ };
1066
+ const isExporting = exportState === "exporting";
1067
+ const exportDisabled = connectionState !== "connected" || !database || isExporting;
1068
+ return /* @__PURE__ */ jsx(
1069
+ Modal,
1070
+ {
1071
+ animationType: "slide",
1072
+ onRequestClose: closeSettings,
1073
+ transparent: true,
1074
+ visible: settingsVisible,
1075
+ children: /* @__PURE__ */ jsx(View, { style: styles3.overlay, children: /* @__PURE__ */ jsxs(View, { style: styles3.sheet, children: [
1076
+ /* @__PURE__ */ jsxs(View, { style: styles3.header, children: [
1077
+ /* @__PURE__ */ jsx(Text, { style: styles3.title, children: "Database DevTools" }),
1078
+ /* @__PURE__ */ jsx(Pressable, { accessibilityLabel: "Close settings", onPress: closeSettings, children: /* @__PURE__ */ jsx(Text, { style: styles3.closeButton, children: "Close" }) })
1079
+ ] }),
1080
+ /* @__PURE__ */ jsxs(
1081
+ ScrollView,
1082
+ {
1083
+ contentContainerStyle: styles3.content,
1084
+ style: styles3.scrollBody,
1085
+ children: [
1086
+ /* @__PURE__ */ jsxs(Section, { title: "Connection", children: [
1087
+ /* @__PURE__ */ jsx(ConnectionStatusBadge, { state: connectionState }),
1088
+ /* @__PURE__ */ jsx(InfoRow, { label: "Server URL", value: serverUrl, mono: true }),
1089
+ connectionError ? /* @__PURE__ */ jsx(Text, { style: styles3.errorText, children: connectionError }) : null,
1090
+ connectionHint ? /* @__PURE__ */ jsx(Text, { style: styles3.hintText, children: connectionHint }) : null,
1091
+ connectionState === "reconnecting" ? /* @__PURE__ */ jsx(Text, { style: styles3.hintText, children: "Ensure the inspector hub is running (npx database-devtools) and the URL reaches your development machine \u2014 not the phone itself." }) : null
1092
+ ] }),
1093
+ /* @__PURE__ */ jsxs(Section, { title: "Device", children: [
1094
+ /* @__PURE__ */ jsx(InfoRow, { label: "Device ID", value: deviceId ?? "\u2014", mono: true }),
1095
+ /* @__PURE__ */ jsx(InfoRow, { label: "Platform", value: metadata.platform ?? "\u2014" }),
1096
+ /* @__PURE__ */ jsx(InfoRow, { label: "App", value: metadata.appName ?? "\u2014" }),
1097
+ metadata.appVersion ? /* @__PURE__ */ jsx(InfoRow, { label: "Version", value: metadata.appVersion }) : null,
1098
+ metadata.bundleId ? /* @__PURE__ */ jsx(InfoRow, { label: "Bundle ID", value: metadata.bundleId, mono: true }) : null
1099
+ ] }),
1100
+ /* @__PURE__ */ jsxs(Section, { title: "Server", children: [
1101
+ /* @__PURE__ */ jsx(Text, { style: styles3.fieldLabel, children: "WebSocket URL" }),
1102
+ /* @__PURE__ */ jsx(
1103
+ TextInput,
1104
+ {
1105
+ autoCapitalize: "none",
1106
+ autoCorrect: false,
1107
+ onChangeText: setDraftUrl,
1108
+ placeholder: "ws://10.0.2.2:3847/ws",
1109
+ style: styles3.input,
1110
+ value: draftUrl
1111
+ }
1112
+ ),
1113
+ /* @__PURE__ */ jsx(Pressable, { onPress: handleReconnect, style: styles3.reconnectButton, children: /* @__PURE__ */ jsx(Text, { style: styles3.reconnectLabel, children: "Reconnect" }) })
1114
+ ] }),
1115
+ /* @__PURE__ */ jsx(Section, { title: "Database", children: database ? /* @__PURE__ */ jsxs(Fragment, { children: [
1116
+ /* @__PURE__ */ jsx(InfoRow, { label: "Kind", value: database.kind }),
1117
+ /* @__PURE__ */ jsx(InfoRow, { label: "Name", value: database.name }),
1118
+ /* @__PURE__ */ jsx(InfoRow, { label: "ID", value: database.id, mono: true })
1119
+ ] }) : /* @__PURE__ */ jsx(Text, { style: styles3.placeholder, children: adapterError ? "Adapter not connected" : "No adapter connected" }) })
1120
+ ]
1121
+ }
1122
+ ),
1123
+ /* @__PURE__ */ jsxs(View, { style: styles3.exportFooter, children: [
1124
+ adapterError ? /* @__PURE__ */ jsx(Text, { style: styles3.errorText, children: adapterError }) : null,
1125
+ database ? /* @__PURE__ */ jsxs(Fragment, { children: [
1126
+ /* @__PURE__ */ jsx(
1127
+ Pressable,
1128
+ {
1129
+ accessibilityLabel: "Export database to web inspector",
1130
+ accessibilityState: { busy: isExporting, disabled: exportDisabled },
1131
+ disabled: exportDisabled,
1132
+ onPress: handleExportDatabase,
1133
+ style: [
1134
+ styles3.exportButton,
1135
+ exportDisabled && !isExporting && styles3.exportButtonDisabled
1136
+ ],
1137
+ children: /* @__PURE__ */ jsxs(View, { style: styles3.exportButtonContent, children: [
1138
+ isExporting ? /* @__PURE__ */ jsx(ActivityIndicator, { color: "#f8fafc", size: "small" }) : null,
1139
+ /* @__PURE__ */ jsx(Text, { style: styles3.exportLabel, children: isExporting ? "Exporting\u2026" : "Export Database" })
1140
+ ] })
1141
+ }
1142
+ ),
1143
+ connectionState !== "connected" ? /* @__PURE__ */ jsx(Text, { style: styles3.hintText, children: "Connect to the hub before exporting." }) : null,
1144
+ exportState === "success" ? /* @__PURE__ */ jsx(Text, { style: styles3.successText, children: "Sent to web inspector" }) : null,
1145
+ exportError ? /* @__PURE__ */ jsx(Text, { style: styles3.errorText, children: exportError }) : null
1146
+ ] }) : /* @__PURE__ */ jsx(Text, { style: styles3.placeholder, children: adapterError ? "Adapter not connected" : "No adapter connected" })
1147
+ ] })
1148
+ ] }) })
1149
+ }
1150
+ );
1151
+ }
1152
+ function Section({ title, children }) {
1153
+ return /* @__PURE__ */ jsxs(View, { style: styles3.section, children: [
1154
+ /* @__PURE__ */ jsx(Text, { style: styles3.sectionTitle, children: title }),
1155
+ children
1156
+ ] });
1157
+ }
1158
+ function InfoRow({
1159
+ label,
1160
+ value,
1161
+ mono = false
1162
+ }) {
1163
+ return /* @__PURE__ */ jsxs(View, { style: styles3.infoRow, children: [
1164
+ /* @__PURE__ */ jsx(Text, { style: styles3.infoLabel, children: label }),
1165
+ /* @__PURE__ */ jsx(Text, { style: [styles3.infoValue, mono && styles3.mono], selectable: true, children: value })
1166
+ ] });
1167
+ }
1168
+ var styles3 = StyleSheet.create({
1169
+ overlay: {
1170
+ flex: 1,
1171
+ backgroundColor: "rgba(0, 0, 0, 0.45)",
1172
+ justifyContent: "flex-end"
1173
+ },
1174
+ sheet: {
1175
+ backgroundColor: "#ffffff",
1176
+ borderTopLeftRadius: 16,
1177
+ borderTopRightRadius: 16,
1178
+ maxHeight: "85%",
1179
+ overflow: "hidden"
1180
+ },
1181
+ scrollBody: {
1182
+ flexGrow: 0,
1183
+ flexShrink: 1
1184
+ },
1185
+ header: {
1186
+ flexDirection: "row",
1187
+ alignItems: "center",
1188
+ justifyContent: "space-between",
1189
+ paddingHorizontal: 20,
1190
+ paddingVertical: 16,
1191
+ borderBottomWidth: StyleSheet.hairlineWidth,
1192
+ borderBottomColor: "#e2e8f0"
1193
+ },
1194
+ title: {
1195
+ fontSize: 18,
1196
+ fontWeight: "700",
1197
+ color: "#0f172a"
1198
+ },
1199
+ closeButton: {
1200
+ fontSize: 16,
1201
+ color: "#2563eb",
1202
+ fontWeight: "600"
1203
+ },
1204
+ exportFooter: {
1205
+ paddingHorizontal: 20,
1206
+ paddingTop: 12,
1207
+ paddingBottom: 20,
1208
+ gap: 8,
1209
+ borderTopWidth: StyleSheet.hairlineWidth,
1210
+ borderTopColor: "#e2e8f0",
1211
+ backgroundColor: "#ffffff"
1212
+ },
1213
+ content: {
1214
+ padding: 20,
1215
+ gap: 20
1216
+ },
1217
+ section: {
1218
+ gap: 10
1219
+ },
1220
+ sectionTitle: {
1221
+ fontSize: 13,
1222
+ fontWeight: "600",
1223
+ color: "#64748b",
1224
+ textTransform: "uppercase",
1225
+ letterSpacing: 0.5
1226
+ },
1227
+ infoRow: {
1228
+ gap: 2
1229
+ },
1230
+ infoLabel: {
1231
+ fontSize: 12,
1232
+ color: "#94a3b8"
1233
+ },
1234
+ infoValue: {
1235
+ fontSize: 14,
1236
+ color: "#0f172a"
1237
+ },
1238
+ mono: {
1239
+ fontFamily: "monospace",
1240
+ fontSize: 12
1241
+ },
1242
+ fieldLabel: {
1243
+ fontSize: 12,
1244
+ color: "#64748b",
1245
+ marginBottom: 4
1246
+ },
1247
+ input: {
1248
+ borderWidth: 1,
1249
+ borderColor: "#cbd5e1",
1250
+ borderRadius: 8,
1251
+ paddingHorizontal: 12,
1252
+ paddingVertical: 10,
1253
+ fontSize: 14,
1254
+ color: "#0f172a",
1255
+ fontFamily: "monospace"
1256
+ },
1257
+ reconnectButton: {
1258
+ marginTop: 8,
1259
+ backgroundColor: "#1e293b",
1260
+ borderRadius: 8,
1261
+ paddingVertical: 12,
1262
+ alignItems: "center"
1263
+ },
1264
+ reconnectLabel: {
1265
+ color: "#f8fafc",
1266
+ fontSize: 15,
1267
+ fontWeight: "600"
1268
+ },
1269
+ exportButton: {
1270
+ backgroundColor: "#2563eb",
1271
+ borderRadius: 8,
1272
+ paddingVertical: 12,
1273
+ paddingHorizontal: 16,
1274
+ alignItems: "center",
1275
+ justifyContent: "center",
1276
+ minHeight: 44
1277
+ },
1278
+ exportButtonContent: {
1279
+ flexDirection: "row",
1280
+ alignItems: "center",
1281
+ justifyContent: "center",
1282
+ gap: 8
1283
+ },
1284
+ exportButtonDisabled: {
1285
+ opacity: 0.5
1286
+ },
1287
+ exportLabel: {
1288
+ color: "#f8fafc",
1289
+ fontSize: 15,
1290
+ fontWeight: "600"
1291
+ },
1292
+ successText: {
1293
+ fontSize: 13,
1294
+ color: "#15803d"
1295
+ },
1296
+ placeholder: {
1297
+ fontSize: 14,
1298
+ color: "#94a3b8",
1299
+ fontStyle: "italic"
1300
+ },
1301
+ errorText: {
1302
+ fontSize: 13,
1303
+ color: "#dc2626",
1304
+ marginBottom: 8
1305
+ },
1306
+ hintText: {
1307
+ fontSize: 12,
1308
+ color: "#b45309",
1309
+ lineHeight: 18
1310
+ }
1311
+ });
1312
+
1313
+ // src/utils/floatingButtonPosition.ts
1314
+ var FLOATING_BUTTON_SIZE = 48;
1315
+ var FLOATING_BUTTON_MARGIN = 16;
1316
+ var FLOATING_BUTTON_BOTTOM = 24;
1317
+ var FLOATING_BUTTON_TAP_THRESHOLD = 8;
1318
+ function getCornerPosition(corner, layout, bottomInset = FLOATING_BUTTON_BOTTOM) {
1319
+ const { windowWidth, windowHeight, buttonSize, margin } = layout;
1320
+ const y = windowHeight - bottomInset - buttonSize;
1321
+ return {
1322
+ x: corner === "bottom-right" ? windowWidth - margin - buttonSize : margin,
1323
+ y
1324
+ };
1325
+ }
1326
+ function clampFloatingPosition(position, layout) {
1327
+ const { windowWidth, windowHeight, buttonSize, margin } = layout;
1328
+ return {
1329
+ x: Math.min(Math.max(margin, position.x), windowWidth - margin - buttonSize),
1330
+ y: Math.min(Math.max(margin, position.y), windowHeight - margin - buttonSize)
1331
+ };
1332
+ }
1333
+ function snapFloatingPositionToEdges(position, layout) {
1334
+ const { windowWidth, windowHeight, buttonSize, margin } = layout;
1335
+ const clamped = clampFloatingPosition(position, layout);
1336
+ const centerX = clamped.x + buttonSize / 2;
1337
+ const centerY = clamped.y + buttonSize / 2;
1338
+ let x = centerX < windowWidth / 2 ? margin : windowWidth - margin - buttonSize;
1339
+ let y = clamped.y;
1340
+ if (centerY < windowHeight * 0.2) {
1341
+ y = margin;
1342
+ } else if (centerY > windowHeight * 0.8) {
1343
+ y = windowHeight - margin - buttonSize;
1344
+ }
1345
+ return { x, y };
1346
+ }
1347
+ function isFloatingButtonTap(totalMovement) {
1348
+ return totalMovement < FLOATING_BUTTON_TAP_THRESHOLD;
1349
+ }
1350
+ var DEFAULT_ICON_SIZE = 22;
1351
+ var DEFAULT_ICON_COLOR = "#f8fafc";
1352
+ function FloatingDevToolsButton({
1353
+ position = "bottom-right",
1354
+ iconStyle,
1355
+ draggable = true,
1356
+ snapToEdges = true,
1357
+ floatingPosition,
1358
+ onFloatingPositionChange
1359
+ }) {
1360
+ const { connectionState, openLauncher } = useDevTools();
1361
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
1362
+ const [internalPosition, setInternalPosition] = useState(null);
1363
+ const [dragPosition, setDragPosition] = useState(null);
1364
+ const dragOrigin = useRef({ x: 0, y: 0 });
1365
+ const hasInitialized = useRef(false);
1366
+ const onFloatingPositionChangeRef = useRef(onFloatingPositionChange);
1367
+ useEffect(() => {
1368
+ onFloatingPositionChangeRef.current = onFloatingPositionChange;
1369
+ }, [onFloatingPositionChange]);
1370
+ const layout = useMemo(
1371
+ () => ({
1372
+ windowWidth,
1373
+ windowHeight,
1374
+ buttonSize: FLOATING_BUTTON_SIZE,
1375
+ margin: FLOATING_BUTTON_MARGIN
1376
+ }),
1377
+ [windowWidth, windowHeight]
1378
+ );
1379
+ const committedPosition = floatingPosition ?? internalPosition;
1380
+ const renderedPosition = dragPosition ?? committedPosition;
1381
+ const commitPosition = useCallback(
1382
+ (next) => {
1383
+ const clamped = clampFloatingPosition(next, layout);
1384
+ if (!floatingPosition) {
1385
+ setInternalPosition(clamped);
1386
+ }
1387
+ onFloatingPositionChangeRef.current?.(clamped);
1388
+ return clamped;
1389
+ },
1390
+ [floatingPosition, layout]
1391
+ );
1392
+ useEffect(() => {
1393
+ if (windowWidth <= 0 || windowHeight <= 0) {
1394
+ return;
1395
+ }
1396
+ if (floatingPosition) {
1397
+ hasInitialized.current = true;
1398
+ return;
1399
+ }
1400
+ if (!hasInitialized.current) {
1401
+ setInternalPosition(getCornerPosition(position, layout));
1402
+ hasInitialized.current = true;
1403
+ }
1404
+ }, [floatingPosition, layout, position, windowHeight, windowWidth]);
1405
+ useEffect(() => {
1406
+ if (!committedPosition || windowWidth <= 0 || windowHeight <= 0) {
1407
+ return;
1408
+ }
1409
+ const clamped = clampFloatingPosition(committedPosition, layout);
1410
+ if (clamped.x !== committedPosition.x || clamped.y !== committedPosition.y) {
1411
+ commitPosition(clamped);
1412
+ }
1413
+ }, [commitPosition, committedPosition, layout, windowHeight, windowWidth]);
1414
+ const panResponder = useMemo(
1415
+ () => PanResponder.create({
1416
+ onStartShouldSetPanResponder: () => draggable,
1417
+ onMoveShouldSetPanResponder: (_, gesture) => draggable && (Math.abs(gesture.dx) > 2 || Math.abs(gesture.dy) > 2),
1418
+ onPanResponderGrant: () => {
1419
+ if (!committedPosition) {
1420
+ return;
1421
+ }
1422
+ dragOrigin.current = committedPosition;
1423
+ },
1424
+ onPanResponderMove: (_, gesture) => {
1425
+ setDragPosition(
1426
+ clampFloatingPosition(
1427
+ {
1428
+ x: dragOrigin.current.x + gesture.dx,
1429
+ y: dragOrigin.current.y + gesture.dy
1430
+ },
1431
+ layout
1432
+ )
1433
+ );
1434
+ },
1435
+ onPanResponderRelease: (_, gesture) => {
1436
+ setDragPosition(null);
1437
+ const movement = Math.hypot(gesture.dx, gesture.dy);
1438
+ if (isFloatingButtonTap(movement)) {
1439
+ openLauncher();
1440
+ return;
1441
+ }
1442
+ const raw = {
1443
+ x: dragOrigin.current.x + gesture.dx,
1444
+ y: dragOrigin.current.y + gesture.dy
1445
+ };
1446
+ const next = snapToEdges ? snapFloatingPositionToEdges(raw, layout) : clampFloatingPosition(raw, layout);
1447
+ commitPosition(next);
1448
+ },
1449
+ onPanResponderTerminate: () => {
1450
+ setDragPosition(null);
1451
+ }
1452
+ }),
1453
+ [commitPosition, committedPosition, draggable, layout, openLauncher, snapToEdges]
1454
+ );
1455
+ if (!renderedPosition) {
1456
+ return null;
1457
+ }
1458
+ const buttonContent = /* @__PURE__ */ jsxs(Fragment, { children: [
1459
+ /* @__PURE__ */ jsx(
1460
+ MaterialCommunityIcons,
1461
+ {
1462
+ color: DEFAULT_ICON_COLOR,
1463
+ name: "database",
1464
+ size: DEFAULT_ICON_SIZE,
1465
+ style: iconStyle
1466
+ }
1467
+ ),
1468
+ /* @__PURE__ */ jsx(
1469
+ View,
1470
+ {
1471
+ style: [styles4.statusDot, { backgroundColor: getConnectionDotColor(connectionState) }]
1472
+ }
1473
+ )
1474
+ ] });
1475
+ if (!draggable) {
1476
+ const positionStyle = position === "bottom-left" ? styles4.bottomLeft : styles4.bottomRight;
1477
+ return /* @__PURE__ */ jsx(View, { style: [styles4.fixedContainer, positionStyle], pointerEvents: "box-none", children: /* @__PURE__ */ jsx(
1478
+ Pressable,
1479
+ {
1480
+ accessibilityLabel: "Open Database DevTools",
1481
+ accessibilityRole: "button",
1482
+ onPress: openLauncher,
1483
+ style: ({ pressed }) => [styles4.button, pressed && styles4.buttonPressed],
1484
+ children: buttonContent
1485
+ }
1486
+ ) });
1487
+ }
1488
+ return /* @__PURE__ */ jsx(
1489
+ View,
1490
+ {
1491
+ pointerEvents: "box-none",
1492
+ style: [
1493
+ styles4.draggableContainer,
1494
+ {
1495
+ transform: [
1496
+ { translateX: renderedPosition.x },
1497
+ { translateY: renderedPosition.y }
1498
+ ]
1499
+ }
1500
+ ],
1501
+ ...panResponder.panHandlers,
1502
+ children: /* @__PURE__ */ jsx(
1503
+ View,
1504
+ {
1505
+ accessibilityHint: "Drag to move. Tap to open DevTools launcher.",
1506
+ accessibilityLabel: "Open Database DevTools",
1507
+ accessibilityRole: "button",
1508
+ style: [styles4.button, dragPosition && styles4.buttonDragging],
1509
+ children: buttonContent
1510
+ }
1511
+ )
1512
+ }
1513
+ );
1514
+ }
1515
+ var styles4 = StyleSheet.create({
1516
+ fixedContainer: {
1517
+ position: "absolute",
1518
+ bottom: 24,
1519
+ zIndex: 9999
1520
+ },
1521
+ bottomRight: {
1522
+ right: 16
1523
+ },
1524
+ bottomLeft: {
1525
+ left: 16
1526
+ },
1527
+ draggableContainer: {
1528
+ position: "absolute",
1529
+ top: 0,
1530
+ left: 0,
1531
+ zIndex: 9999
1532
+ },
1533
+ button: {
1534
+ width: FLOATING_BUTTON_SIZE,
1535
+ height: FLOATING_BUTTON_SIZE,
1536
+ borderRadius: FLOATING_BUTTON_SIZE / 2,
1537
+ backgroundColor: "#1e293b",
1538
+ alignItems: "center",
1539
+ justifyContent: "center",
1540
+ shadowColor: "#000",
1541
+ shadowOffset: { width: 0, height: 2 },
1542
+ shadowOpacity: 0.25,
1543
+ shadowRadius: 4,
1544
+ elevation: 6
1545
+ },
1546
+ buttonPressed: {
1547
+ opacity: 0.85
1548
+ },
1549
+ buttonDragging: {
1550
+ opacity: 0.92,
1551
+ transform: [{ scale: 1.04 }]
1552
+ },
1553
+ statusDot: {
1554
+ position: "absolute",
1555
+ top: 2,
1556
+ right: 2,
1557
+ width: 10,
1558
+ height: 10,
1559
+ borderRadius: 5,
1560
+ borderWidth: 2,
1561
+ borderColor: "#1e293b"
1562
+ }
1563
+ });
1564
+ var explorerColors = {
1565
+ background: "#ffffff",
1566
+ surface: "#f8fafc",
1567
+ border: "#e2e8f0",
1568
+ text: "#0f172a",
1569
+ muted: "#64748b",
1570
+ subtle: "#94a3b8",
1571
+ accent: "#2563eb",
1572
+ accentSoft: "#dbeafe",
1573
+ error: "#dc2626"};
1574
+ var explorerStyles = StyleSheet.create({
1575
+ fullScreen: {
1576
+ flex: 1,
1577
+ backgroundColor: explorerColors.background
1578
+ },
1579
+ header: {
1580
+ flexDirection: "row",
1581
+ alignItems: "center",
1582
+ justifyContent: "space-between",
1583
+ paddingHorizontal: 16,
1584
+ paddingVertical: 14,
1585
+ borderBottomWidth: StyleSheet.hairlineWidth,
1586
+ borderBottomColor: explorerColors.border,
1587
+ backgroundColor: explorerColors.background
1588
+ },
1589
+ headerTitle: {
1590
+ fontSize: 17,
1591
+ fontWeight: "700",
1592
+ color: explorerColors.text
1593
+ },
1594
+ headerAction: {
1595
+ fontSize: 16,
1596
+ color: explorerColors.accent,
1597
+ fontWeight: "600"
1598
+ },
1599
+ tabRow: {
1600
+ flexDirection: "row",
1601
+ borderBottomWidth: StyleSheet.hairlineWidth,
1602
+ borderBottomColor: explorerColors.border,
1603
+ backgroundColor: explorerColors.surface
1604
+ },
1605
+ tab: {
1606
+ flex: 1,
1607
+ paddingVertical: 12,
1608
+ alignItems: "center",
1609
+ borderBottomWidth: 2,
1610
+ borderBottomColor: "transparent"
1611
+ },
1612
+ tabActive: {
1613
+ borderBottomColor: explorerColors.accent
1614
+ },
1615
+ tabLabel: {
1616
+ fontSize: 14,
1617
+ color: explorerColors.muted,
1618
+ fontWeight: "500"
1619
+ },
1620
+ tabLabelActive: {
1621
+ color: explorerColors.accent,
1622
+ fontWeight: "700"
1623
+ },
1624
+ content: {
1625
+ flex: 1,
1626
+ padding: 16
1627
+ },
1628
+ sectionTitle: {
1629
+ fontSize: 12,
1630
+ fontWeight: "600",
1631
+ color: explorerColors.muted,
1632
+ textTransform: "uppercase",
1633
+ letterSpacing: 0.5,
1634
+ marginBottom: 8
1635
+ },
1636
+ infoRow: {
1637
+ marginBottom: 10
1638
+ },
1639
+ infoLabel: {
1640
+ fontSize: 12,
1641
+ color: explorerColors.subtle,
1642
+ marginBottom: 2
1643
+ },
1644
+ infoValue: {
1645
+ fontSize: 14,
1646
+ color: explorerColors.text
1647
+ },
1648
+ mono: {
1649
+ fontFamily: "monospace",
1650
+ fontSize: 12
1651
+ },
1652
+ listItem: {
1653
+ flexDirection: "row",
1654
+ alignItems: "center",
1655
+ justifyContent: "space-between",
1656
+ paddingVertical: 12,
1657
+ paddingHorizontal: 12,
1658
+ borderRadius: 8,
1659
+ backgroundColor: explorerColors.surface,
1660
+ marginBottom: 8,
1661
+ borderWidth: 1,
1662
+ borderColor: explorerColors.border
1663
+ },
1664
+ listItemTitle: {
1665
+ fontSize: 15,
1666
+ fontWeight: "600",
1667
+ color: explorerColors.text,
1668
+ fontFamily: "monospace"
1669
+ },
1670
+ listItemMeta: {
1671
+ fontSize: 12,
1672
+ color: explorerColors.muted
1673
+ },
1674
+ searchInput: {
1675
+ borderWidth: 1,
1676
+ borderColor: explorerColors.border,
1677
+ borderRadius: 8,
1678
+ paddingHorizontal: 12,
1679
+ paddingVertical: 10,
1680
+ fontSize: 14,
1681
+ color: explorerColors.text,
1682
+ backgroundColor: explorerColors.background,
1683
+ marginBottom: 12
1684
+ },
1685
+ primaryButton: {
1686
+ backgroundColor: explorerColors.accent,
1687
+ borderRadius: 8,
1688
+ paddingVertical: 12,
1689
+ alignItems: "center",
1690
+ justifyContent: "center",
1691
+ minHeight: 44
1692
+ },
1693
+ primaryButtonDisabled: {
1694
+ opacity: 0.5
1695
+ },
1696
+ primaryButtonLabel: {
1697
+ color: "#f8fafc",
1698
+ fontSize: 15,
1699
+ fontWeight: "600"
1700
+ },
1701
+ secondaryButton: {
1702
+ backgroundColor: "#1e293b",
1703
+ borderRadius: 8,
1704
+ paddingVertical: 12,
1705
+ alignItems: "center",
1706
+ justifyContent: "center",
1707
+ minHeight: 44
1708
+ },
1709
+ secondaryButtonLabel: {
1710
+ color: "#f8fafc",
1711
+ fontSize: 15,
1712
+ fontWeight: "600"
1713
+ },
1714
+ ghostButton: {
1715
+ borderWidth: 1,
1716
+ borderColor: explorerColors.border,
1717
+ borderRadius: 8,
1718
+ paddingVertical: 12,
1719
+ alignItems: "center",
1720
+ justifyContent: "center",
1721
+ minHeight: 44,
1722
+ backgroundColor: explorerColors.background
1723
+ },
1724
+ ghostButtonLabel: {
1725
+ color: explorerColors.text,
1726
+ fontSize: 15,
1727
+ fontWeight: "600"
1728
+ },
1729
+ errorText: {
1730
+ fontSize: 13,
1731
+ color: explorerColors.error,
1732
+ marginTop: 8
1733
+ },
1734
+ hintText: {
1735
+ fontSize: 12,
1736
+ color: explorerColors.muted,
1737
+ marginTop: 8,
1738
+ lineHeight: 18
1739
+ },
1740
+ placeholder: {
1741
+ fontSize: 14,
1742
+ color: explorerColors.subtle,
1743
+ fontStyle: "italic",
1744
+ textAlign: "center",
1745
+ marginTop: 24
1746
+ },
1747
+ sqlInput: {
1748
+ minHeight: 120,
1749
+ borderWidth: 1,
1750
+ borderColor: explorerColors.border,
1751
+ borderRadius: 8,
1752
+ paddingHorizontal: 12,
1753
+ paddingVertical: 10,
1754
+ fontSize: 13,
1755
+ fontFamily: "monospace",
1756
+ color: explorerColors.text,
1757
+ textAlignVertical: "top",
1758
+ backgroundColor: explorerColors.surface
1759
+ },
1760
+ sqlTabRoot: {
1761
+ flex: 1,
1762
+ minHeight: 0
1763
+ },
1764
+ sqlQueryPane: {
1765
+ flexShrink: 0,
1766
+ paddingHorizontal: 16,
1767
+ paddingTop: 16,
1768
+ paddingBottom: 12,
1769
+ gap: 12,
1770
+ borderBottomWidth: StyleSheet.hairlineWidth,
1771
+ borderBottomColor: explorerColors.border,
1772
+ backgroundColor: explorerColors.background
1773
+ },
1774
+ sqlInputCompact: {
1775
+ minHeight: 96,
1776
+ maxHeight: 120,
1777
+ borderWidth: 1,
1778
+ borderColor: explorerColors.border,
1779
+ borderRadius: 8,
1780
+ paddingHorizontal: 12,
1781
+ paddingVertical: 10,
1782
+ fontSize: 13,
1783
+ fontFamily: "monospace",
1784
+ color: explorerColors.text,
1785
+ textAlignVertical: "top",
1786
+ backgroundColor: explorerColors.surface
1787
+ },
1788
+ sqlResultsPane: {
1789
+ flex: 1,
1790
+ minHeight: 0,
1791
+ paddingHorizontal: 16,
1792
+ paddingTop: 8,
1793
+ paddingBottom: 16
1794
+ },
1795
+ sqlResultsEmpty: {
1796
+ flex: 1,
1797
+ justifyContent: "center",
1798
+ paddingHorizontal: 16
1799
+ },
1800
+ subTabRow: {
1801
+ flexDirection: "row",
1802
+ gap: 8,
1803
+ marginBottom: 12
1804
+ },
1805
+ subTab: {
1806
+ paddingHorizontal: 12,
1807
+ paddingVertical: 6,
1808
+ borderRadius: 999,
1809
+ borderWidth: 1,
1810
+ borderColor: explorerColors.border,
1811
+ backgroundColor: explorerColors.background
1812
+ },
1813
+ subTabActive: {
1814
+ borderColor: explorerColors.accent,
1815
+ backgroundColor: explorerColors.accentSoft
1816
+ },
1817
+ subTabLabel: {
1818
+ fontSize: 13,
1819
+ color: explorerColors.muted
1820
+ },
1821
+ subTabLabelActive: {
1822
+ color: explorerColors.accent,
1823
+ fontWeight: "600"
1824
+ },
1825
+ paginationRow: {
1826
+ flexDirection: "row",
1827
+ alignItems: "center",
1828
+ justifyContent: "space-between",
1829
+ paddingVertical: 12,
1830
+ borderTopWidth: StyleSheet.hairlineWidth,
1831
+ borderTopColor: explorerColors.border
1832
+ },
1833
+ paginationLabel: {
1834
+ fontSize: 12,
1835
+ color: explorerColors.muted
1836
+ }
1837
+ });
1838
+ var tableStyles = StyleSheet.create({
1839
+ viewRoot: {
1840
+ flex: 1,
1841
+ minHeight: 0
1842
+ },
1843
+ viewToolbar: {
1844
+ flexDirection: "row",
1845
+ alignItems: "center",
1846
+ justifyContent: "space-between",
1847
+ gap: 8,
1848
+ marginBottom: 8,
1849
+ flexWrap: "wrap"
1850
+ },
1851
+ viewMeta: {
1852
+ flex: 1,
1853
+ fontSize: 12,
1854
+ color: explorerColors.muted
1855
+ },
1856
+ viewToggle: {
1857
+ flexDirection: "row",
1858
+ borderWidth: 1,
1859
+ borderColor: explorerColors.border,
1860
+ borderRadius: 8,
1861
+ overflow: "hidden"
1862
+ },
1863
+ viewToggleButton: {
1864
+ paddingHorizontal: 10,
1865
+ paddingVertical: 6,
1866
+ backgroundColor: explorerColors.background
1867
+ },
1868
+ viewToggleButtonActive: {
1869
+ backgroundColor: explorerColors.accentSoft
1870
+ },
1871
+ viewToggleLabel: {
1872
+ fontSize: 12,
1873
+ fontWeight: "600",
1874
+ color: explorerColors.muted
1875
+ },
1876
+ viewToggleLabelActive: {
1877
+ color: explorerColors.accent
1878
+ },
1879
+ footerHint: {
1880
+ flexShrink: 0,
1881
+ marginTop: 8,
1882
+ fontSize: 11,
1883
+ color: explorerColors.subtle,
1884
+ textAlign: "center"
1885
+ },
1886
+ tableContainer: {
1887
+ flex: 1,
1888
+ minHeight: 0,
1889
+ borderWidth: 1,
1890
+ borderColor: explorerColors.border,
1891
+ borderRadius: 8,
1892
+ overflow: "hidden",
1893
+ backgroundColor: explorerColors.background
1894
+ },
1895
+ horizontalScroll: {
1896
+ flex: 1
1897
+ },
1898
+ scrollHint: {
1899
+ paddingHorizontal: 10,
1900
+ paddingVertical: 6,
1901
+ fontSize: 11,
1902
+ color: explorerColors.muted,
1903
+ backgroundColor: explorerColors.surface,
1904
+ borderBottomWidth: StyleSheet.hairlineWidth,
1905
+ borderBottomColor: explorerColors.border
1906
+ },
1907
+ headerRow: {
1908
+ flexDirection: "row",
1909
+ backgroundColor: "#f1f5f9",
1910
+ borderBottomWidth: 1,
1911
+ borderBottomColor: explorerColors.border
1912
+ },
1913
+ headerCell: {
1914
+ backgroundColor: "#f1f5f9"
1915
+ },
1916
+ headerText: {
1917
+ fontSize: 12,
1918
+ fontWeight: "700",
1919
+ color: explorerColors.text,
1920
+ fontFamily: "monospace"
1921
+ },
1922
+ bodyList: {
1923
+ flex: 1
1924
+ },
1925
+ dataRow: {
1926
+ flexDirection: "row",
1927
+ backgroundColor: explorerColors.background
1928
+ },
1929
+ dataRowAlt: {
1930
+ backgroundColor: explorerColors.surface
1931
+ },
1932
+ cell: {
1933
+ paddingHorizontal: 10,
1934
+ paddingVertical: 8,
1935
+ borderRightWidth: 1,
1936
+ borderBottomWidth: 1,
1937
+ borderColor: explorerColors.border,
1938
+ justifyContent: "center"
1939
+ },
1940
+ cellText: {
1941
+ fontSize: 13,
1942
+ color: explorerColors.text
1943
+ },
1944
+ cellTextMono: {
1945
+ fontFamily: "monospace",
1946
+ fontSize: 12
1947
+ },
1948
+ cellNull: {
1949
+ color: explorerColors.subtle,
1950
+ fontStyle: "italic"
1951
+ },
1952
+ copiedHint: {
1953
+ marginTop: 2,
1954
+ fontSize: 10,
1955
+ color: explorerColors.accent,
1956
+ fontWeight: "600"
1957
+ },
1958
+ cardList: {
1959
+ paddingBottom: 8,
1960
+ gap: 8
1961
+ },
1962
+ card: {
1963
+ borderWidth: 1,
1964
+ borderColor: explorerColors.border,
1965
+ borderRadius: 8,
1966
+ backgroundColor: explorerColors.surface,
1967
+ padding: 12,
1968
+ gap: 10
1969
+ },
1970
+ cardField: {
1971
+ gap: 2
1972
+ },
1973
+ cardLabel: {
1974
+ fontSize: 11,
1975
+ fontWeight: "600",
1976
+ color: explorerColors.muted,
1977
+ textTransform: "uppercase",
1978
+ letterSpacing: 0.4
1979
+ },
1980
+ cardValue: {
1981
+ fontSize: 14,
1982
+ color: explorerColors.text
1983
+ },
1984
+ cardValueMono: {
1985
+ fontFamily: "monospace",
1986
+ fontSize: 13
1987
+ },
1988
+ detailBody: {
1989
+ flex: 1,
1990
+ minHeight: 0,
1991
+ paddingHorizontal: 16
1992
+ },
1993
+ detailDataPane: {
1994
+ flex: 1,
1995
+ minHeight: 0
1996
+ }
1997
+ });
1998
+ function formatBytes(bytes) {
1999
+ if (bytes < 1024) {
2000
+ return `${bytes} B`;
2001
+ }
2002
+ if (bytes < 1024 * 1024) {
2003
+ return `${(bytes / 1024).toFixed(1)} KB`;
2004
+ }
2005
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2006
+ }
2007
+ function ExplorerOverviewTab({
2008
+ info,
2009
+ loading,
2010
+ error,
2011
+ onOpenTables,
2012
+ onOpenSql
2013
+ }) {
2014
+ if (loading) {
2015
+ return /* @__PURE__ */ jsx(Text, { style: explorerStyles.placeholder, children: "Loading database info\u2026" });
2016
+ }
2017
+ if (error) {
2018
+ return /* @__PURE__ */ jsx(Text, { style: explorerStyles.errorText, children: error });
2019
+ }
2020
+ if (!info) {
2021
+ return /* @__PURE__ */ jsx(Text, { style: explorerStyles.placeholder, children: "No database information available." });
2022
+ }
2023
+ return /* @__PURE__ */ jsxs(ScrollView, { contentContainerStyle: explorerStyles.content, children: [
2024
+ /* @__PURE__ */ jsx(Text, { style: explorerStyles.sectionTitle, children: "Database" }),
2025
+ /* @__PURE__ */ jsx(Info, { label: "Name", value: info.name }),
2026
+ /* @__PURE__ */ jsx(Info, { label: "Path", value: info.path, mono: true }),
2027
+ /* @__PURE__ */ jsx(Info, { label: "SQLite version", value: info.sqliteVersion }),
2028
+ /* @__PURE__ */ jsx(Info, { label: "Tables", value: String(info.tableCount) }),
2029
+ /* @__PURE__ */ jsx(Info, { label: "Estimated size", value: formatBytes(info.estimatedSizeBytes) }),
2030
+ /* @__PURE__ */ jsx(
2031
+ Info,
2032
+ {
2033
+ label: "Pages",
2034
+ value: `${info.pageCount.toLocaleString()} \xD7 ${info.pageSize} bytes`
2035
+ }
2036
+ ),
2037
+ /* @__PURE__ */ jsx(View, { style: { height: 16 } }),
2038
+ /* @__PURE__ */ jsx(Text, { style: explorerStyles.sectionTitle, children: "Quick actions" }),
2039
+ /* @__PURE__ */ jsxs(View, { style: { gap: 8 }, children: [
2040
+ /* @__PURE__ */ jsx(ActionButton, { label: "Browse tables", onPress: onOpenTables }),
2041
+ /* @__PURE__ */ jsx(ActionButton, { label: "Run SQL query", onPress: onOpenSql })
2042
+ ] })
2043
+ ] });
2044
+ }
2045
+ function Info({ label, value, mono = false }) {
2046
+ return /* @__PURE__ */ jsxs(View, { style: explorerStyles.infoRow, children: [
2047
+ /* @__PURE__ */ jsx(Text, { style: explorerStyles.infoLabel, children: label }),
2048
+ /* @__PURE__ */ jsx(Text, { style: [explorerStyles.infoValue, mono && explorerStyles.mono], selectable: true, children: value })
2049
+ ] });
2050
+ }
2051
+ function ActionButton({ label, onPress }) {
2052
+ return /* @__PURE__ */ jsx(Pressable, { onPress, style: explorerStyles.ghostButton, children: /* @__PURE__ */ jsx(Text, { style: explorerStyles.ghostButtonLabel, children: label }) });
2053
+ }
2054
+
2055
+ // src/mobile/constants.ts
2056
+ var DEFAULT_MOBILE_SQL = "";
2057
+ var DEFAULT_PAGE_SIZE = 50;
2058
+
2059
+ // src/mobile/computeColumnWidths.ts
2060
+ var CHAR_WIDTH = 7.5;
2061
+ var CELL_PADDING = 24;
2062
+ var MIN_COLUMN_WIDTH = 72;
2063
+ var MAX_COLUMN_WIDTH = 280;
2064
+ var SAMPLE_ROW_LIMIT = 20;
2065
+ function hintMinWidth(columnName) {
2066
+ const lower = columnName.toLowerCase();
2067
+ if (lower === "id" || lower.endsWith("_id")) {
2068
+ return 80;
2069
+ }
2070
+ if (lower.includes("email")) {
2071
+ return 168;
2072
+ }
2073
+ if (lower.includes("created_at") || lower.includes("updated_at") || lower.endsWith("_at")) {
2074
+ return 188;
2075
+ }
2076
+ if (lower === "name" || lower.includes("title")) {
2077
+ return 128;
2078
+ }
2079
+ if (lower === "status" || lower === "type") {
2080
+ return 96;
2081
+ }
2082
+ return MIN_COLUMN_WIDTH;
2083
+ }
2084
+ function measureText(text) {
2085
+ return Math.ceil(text.length * CHAR_WIDTH) + CELL_PADDING;
2086
+ }
2087
+ function formatCellValue(value) {
2088
+ if (value === null) {
2089
+ return "NULL";
2090
+ }
2091
+ return String(value);
2092
+ }
2093
+ function computeColumnWidths(columns, rows) {
2094
+ const sampleRows = rows.slice(0, SAMPLE_ROW_LIMIT);
2095
+ return columns.map((column, columnIndex) => {
2096
+ let maxWidth = measureText(column);
2097
+ for (const row of sampleRows) {
2098
+ const cell = formatCellValue(row[columnIndex] ?? null);
2099
+ maxWidth = Math.max(maxWidth, measureText(cell));
2100
+ }
2101
+ maxWidth = Math.max(maxWidth, hintMinWidth(column));
2102
+ return Math.min(MAX_COLUMN_WIDTH, Math.max(MIN_COLUMN_WIDTH, maxWidth));
2103
+ });
2104
+ }
2105
+ function totalTableWidth(columnWidths) {
2106
+ return columnWidths.reduce((sum, width) => sum + width, 0);
2107
+ }
2108
+ function isMonospaceColumn(columnName) {
2109
+ const lower = columnName.toLowerCase();
2110
+ return lower === "id" || lower.endsWith("_id") || lower.includes("created_at") || lower.includes("updated_at") || lower.endsWith("_at") || lower.includes("date") || lower.includes("time");
2111
+ }
2112
+
2113
+ // src/mobile/copyToClipboard.ts
2114
+ async function copyToClipboard(_text) {
2115
+ return false;
2116
+ }
2117
+ function formatValue(value) {
2118
+ return value === null ? "NULL" : String(value);
2119
+ }
2120
+ function MobileDataCardList({ columns, rows }) {
2121
+ const [copiedKey, setCopiedKey] = useState(null);
2122
+ const copyValue = async (value, key) => {
2123
+ await copyToClipboard();
2124
+ setCopiedKey(key);
2125
+ setTimeout(() => setCopiedKey(null), 1500);
2126
+ };
2127
+ return /* @__PURE__ */ jsx(
2128
+ FlatList,
2129
+ {
2130
+ data: rows,
2131
+ keyExtractor: (_, index) => String(index),
2132
+ contentContainerStyle: tableStyles.cardList,
2133
+ style: tableStyles.bodyList,
2134
+ renderItem: ({ item: row, index: rowIndex }) => /* @__PURE__ */ jsx(View, { style: tableStyles.card, children: columns.map((column, columnIndex) => {
2135
+ const value = formatValue(row[columnIndex] ?? null);
2136
+ const key = `${rowIndex}-${column}`;
2137
+ const isNull = row[columnIndex] === null;
2138
+ return /* @__PURE__ */ jsxs(
2139
+ Pressable,
2140
+ {
2141
+ onPress: () => void copyValue(value, key),
2142
+ style: tableStyles.cardField,
2143
+ children: [
2144
+ /* @__PURE__ */ jsx(Text, { style: tableStyles.cardLabel, children: column }),
2145
+ /* @__PURE__ */ jsx(
2146
+ Text,
2147
+ {
2148
+ style: [
2149
+ tableStyles.cardValue,
2150
+ isMonospaceColumn(column) && tableStyles.cardValueMono,
2151
+ isNull && tableStyles.cellNull
2152
+ ],
2153
+ children: value
2154
+ }
2155
+ ),
2156
+ copiedKey === key && /* @__PURE__ */ jsx(Text, { style: tableStyles.copiedHint, children: "Copied" })
2157
+ ]
2158
+ },
2159
+ key
2160
+ );
2161
+ }) })
2162
+ }
2163
+ );
2164
+ }
2165
+ function formatValue2(value) {
2166
+ return value === null ? "NULL" : String(value);
2167
+ }
2168
+ function MobileDataTable({ columns, rows, columnWidths: columnWidthsProp }) {
2169
+ const { width: windowWidth } = useWindowDimensions();
2170
+ const [copiedKey, setCopiedKey] = useState(null);
2171
+ const columnWidths = useMemo(
2172
+ () => columnWidthsProp ?? computeColumnWidths(columns, rows),
2173
+ [columnWidthsProp, columns, rows]
2174
+ );
2175
+ const tableWidth = useMemo(() => totalTableWidth(columnWidths), [columnWidths]);
2176
+ const showScrollHint = tableWidth > windowWidth - 32;
2177
+ const copyCell = async (value, key) => {
2178
+ await copyToClipboard();
2179
+ setCopiedKey(key);
2180
+ setTimeout(() => setCopiedKey(null), 1500);
2181
+ };
2182
+ if (columns.length === 0) {
2183
+ return null;
2184
+ }
2185
+ const headerRow = /* @__PURE__ */ jsx(View, { style: tableStyles.headerRow, children: columns.map((column, index) => /* @__PURE__ */ jsx(
2186
+ View,
2187
+ {
2188
+ style: [tableStyles.cell, tableStyles.headerCell, { width: columnWidths[index] }],
2189
+ children: /* @__PURE__ */ jsx(Text, { style: tableStyles.headerText, children: column })
2190
+ },
2191
+ column
2192
+ )) });
2193
+ return /* @__PURE__ */ jsxs(View, { style: tableStyles.tableContainer, children: [
2194
+ showScrollHint && /* @__PURE__ */ jsx(Text, { style: tableStyles.scrollHint, children: "Swipe horizontally for more columns" }),
2195
+ /* @__PURE__ */ jsx(
2196
+ ScrollView,
2197
+ {
2198
+ horizontal: true,
2199
+ bounces: false,
2200
+ nestedScrollEnabled: true,
2201
+ showsHorizontalScrollIndicator: true,
2202
+ style: tableStyles.horizontalScroll,
2203
+ children: /* @__PURE__ */ jsxs(View, { style: { width: tableWidth, flex: 1 }, children: [
2204
+ headerRow,
2205
+ /* @__PURE__ */ jsx(
2206
+ FlatList,
2207
+ {
2208
+ data: rows,
2209
+ keyExtractor: (_, index) => String(index),
2210
+ nestedScrollEnabled: true,
2211
+ style: tableStyles.bodyList,
2212
+ renderItem: ({ item: row, index: rowIndex }) => /* @__PURE__ */ jsx(View, { style: [tableStyles.dataRow, rowIndex % 2 === 1 && tableStyles.dataRowAlt], children: row.map((cell, cellIndex) => {
2213
+ const column = columns[cellIndex];
2214
+ const display = formatValue2(cell);
2215
+ const key = `${rowIndex}-${cellIndex}`;
2216
+ const isNull = cell === null;
2217
+ return /* @__PURE__ */ jsxs(
2218
+ Pressable,
2219
+ {
2220
+ onPress: () => void copyCell(display, key),
2221
+ style: [tableStyles.cell, { width: columnWidths[cellIndex] }],
2222
+ children: [
2223
+ /* @__PURE__ */ jsx(
2224
+ Text,
2225
+ {
2226
+ numberOfLines: 2,
2227
+ selectable: true,
2228
+ style: [
2229
+ tableStyles.cellText,
2230
+ isMonospaceColumn(column) && tableStyles.cellTextMono,
2231
+ isNull && tableStyles.cellNull
2232
+ ],
2233
+ children: display
2234
+ }
2235
+ ),
2236
+ copiedKey === key && /* @__PURE__ */ jsx(Text, { style: tableStyles.copiedHint, children: "Copied" })
2237
+ ]
2238
+ },
2239
+ key
2240
+ );
2241
+ }) })
2242
+ }
2243
+ )
2244
+ ] })
2245
+ }
2246
+ )
2247
+ ] });
2248
+ }
2249
+ function MobileDataView({
2250
+ columns,
2251
+ rows,
2252
+ metaSuffix,
2253
+ defaultMode = "table",
2254
+ dataKey
2255
+ }) {
2256
+ const [mode, setMode] = useState(defaultMode);
2257
+ const columnWidths = useMemo(() => computeColumnWidths(columns, rows), [columns, rows]);
2258
+ const tableWidth = useMemo(() => totalTableWidth(columnWidths), [columnWidths]);
2259
+ const columnsKey = columns.join("|");
2260
+ useEffect(() => {
2261
+ setMode(defaultMode);
2262
+ }, [columnsKey, dataKey, defaultMode, rows.length]);
2263
+ if (columns.length === 0) {
2264
+ return /* @__PURE__ */ jsx(Text, { style: explorerStyles.placeholder, children: "No rows to display." });
2265
+ }
2266
+ const meta = `${rows.length} row${rows.length === 1 ? "" : "s"} \xB7 ${columns.length} column${columns.length === 1 ? "" : "s"}${metaSuffix ? ` \xB7 ${metaSuffix}` : ""}`;
2267
+ return /* @__PURE__ */ jsxs(View, { style: tableStyles.viewRoot, children: [
2268
+ /* @__PURE__ */ jsxs(View, { style: tableStyles.viewToolbar, children: [
2269
+ /* @__PURE__ */ jsx(Text, { style: tableStyles.viewMeta, children: meta }),
2270
+ /* @__PURE__ */ jsxs(View, { style: tableStyles.viewToggle, children: [
2271
+ /* @__PURE__ */ jsx(
2272
+ Pressable,
2273
+ {
2274
+ onPress: () => setMode("cards"),
2275
+ style: [tableStyles.viewToggleButton, mode === "cards" && tableStyles.viewToggleButtonActive],
2276
+ children: /* @__PURE__ */ jsx(
2277
+ Text,
2278
+ {
2279
+ style: [tableStyles.viewToggleLabel, mode === "cards" && tableStyles.viewToggleLabelActive],
2280
+ children: "Cards"
2281
+ }
2282
+ )
2283
+ }
2284
+ ),
2285
+ /* @__PURE__ */ jsx(
2286
+ Pressable,
2287
+ {
2288
+ onPress: () => setMode("table"),
2289
+ style: [tableStyles.viewToggleButton, mode === "table" && tableStyles.viewToggleButtonActive],
2290
+ children: /* @__PURE__ */ jsx(
2291
+ Text,
2292
+ {
2293
+ style: [tableStyles.viewToggleLabel, mode === "table" && tableStyles.viewToggleLabelActive],
2294
+ children: "Table"
2295
+ }
2296
+ )
2297
+ }
2298
+ )
2299
+ ] })
2300
+ ] }),
2301
+ mode === "cards" ? /* @__PURE__ */ jsx(MobileDataCardList, { columns, rows }) : /* @__PURE__ */ jsx(MobileDataTable, { columnWidths, columns, rows }),
2302
+ mode === "table" && tableWidth > 320 && /* @__PURE__ */ jsx(Text, { style: tableStyles.footerHint, children: "Tap a cell to copy its value" }),
2303
+ mode === "cards" && /* @__PURE__ */ jsx(Text, { style: tableStyles.footerHint, children: "Tap a field to copy its value" })
2304
+ ] });
2305
+ }
2306
+ function ExplorerSqlTab({ inspector }) {
2307
+ const [sql, setSql] = useState(DEFAULT_MOBILE_SQL);
2308
+ const [result, setResult] = useState(null);
2309
+ const [error, setError] = useState(null);
2310
+ const [running, setRunning] = useState(false);
2311
+ const [panel, setPanel] = useState("results");
2312
+ const [resultKey, setResultKey] = useState(0);
2313
+ const runQuery = async () => {
2314
+ setRunning(true);
2315
+ setError(null);
2316
+ try {
2317
+ const queryResult = await inspector.executeQuery(sql);
2318
+ setResult(queryResult);
2319
+ setResultKey((current) => current + 1);
2320
+ setPanel("results");
2321
+ } catch (runError) {
2322
+ const message = runError instanceof Error ? runError.message : "Query failed";
2323
+ setError(message);
2324
+ setResult(null);
2325
+ setPanel("output");
2326
+ } finally {
2327
+ setRunning(false);
2328
+ }
2329
+ };
2330
+ const renderResultsContent = () => {
2331
+ if (panel === "output") {
2332
+ if (error) {
2333
+ return /* @__PURE__ */ jsx(Text, { style: explorerStyles.errorText, children: error });
2334
+ }
2335
+ if (result) {
2336
+ return /* @__PURE__ */ jsxs(Text, { style: explorerStyles.infoValue, children: [
2337
+ "Query executed successfully. ",
2338
+ result.rowCount,
2339
+ " row",
2340
+ result.rowCount === 1 ? "" : "s",
2341
+ " in ",
2342
+ result.durationMs.toFixed(1),
2343
+ " ms."
2344
+ ] });
2345
+ }
2346
+ return /* @__PURE__ */ jsx(Text, { style: explorerStyles.placeholder, children: "Messages and errors appear here." });
2347
+ }
2348
+ if (!result) {
2349
+ return /* @__PURE__ */ jsx(View, { style: explorerStyles.sqlResultsEmpty, children: /* @__PURE__ */ jsx(Text, { style: [explorerStyles.placeholder, { marginTop: 0 }], children: "Run a query to see results." }) });
2350
+ }
2351
+ if (result.columns.length === 0) {
2352
+ return /* @__PURE__ */ jsx(View, { style: explorerStyles.sqlResultsEmpty, children: /* @__PURE__ */ jsxs(Text, { style: [explorerStyles.placeholder, { marginTop: 0 }], children: [
2353
+ "Query completed with no result set (",
2354
+ result.durationMs.toFixed(1),
2355
+ " ms)."
2356
+ ] }) });
2357
+ }
2358
+ return /* @__PURE__ */ jsx(
2359
+ MobileDataView,
2360
+ {
2361
+ columns: result.columns,
2362
+ dataKey: String(resultKey),
2363
+ defaultMode: "table",
2364
+ metaSuffix: `${result.durationMs.toFixed(1)} ms`,
2365
+ rows: result.rows
2366
+ }
2367
+ );
2368
+ };
2369
+ return /* @__PURE__ */ jsxs(View, { style: explorerStyles.sqlTabRoot, children: [
2370
+ /* @__PURE__ */ jsxs(View, { style: explorerStyles.sqlQueryPane, children: [
2371
+ /* @__PURE__ */ jsx(Text, { style: explorerStyles.sectionTitle, children: "SQL (read-only)" }),
2372
+ /* @__PURE__ */ jsx(
2373
+ TextInput,
2374
+ {
2375
+ autoCapitalize: "none",
2376
+ autoCorrect: false,
2377
+ multiline: true,
2378
+ onChangeText: setSql,
2379
+ scrollEnabled: true,
2380
+ style: explorerStyles.sqlInputCompact,
2381
+ value: sql
2382
+ }
2383
+ ),
2384
+ /* @__PURE__ */ jsx(
2385
+ Pressable,
2386
+ {
2387
+ disabled: running,
2388
+ onPress: () => void runQuery(),
2389
+ style: [explorerStyles.primaryButton, running && explorerStyles.primaryButtonDisabled],
2390
+ children: running ? /* @__PURE__ */ jsx(ActivityIndicator, { color: "#f8fafc" }) : /* @__PURE__ */ jsx(Text, { style: explorerStyles.primaryButtonLabel, children: "Run query" })
2391
+ }
2392
+ ),
2393
+ /* @__PURE__ */ jsx(Text, { style: [explorerStyles.hintText, { marginTop: 0 }], children: "Allowed: SELECT, PRAGMA, EXPLAIN, WITH. Writes are blocked in DevTools." })
2394
+ ] }),
2395
+ /* @__PURE__ */ jsxs(View, { style: [explorerStyles.tabRow, { flexShrink: 0 }], children: [
2396
+ /* @__PURE__ */ jsx(
2397
+ Pressable,
2398
+ {
2399
+ onPress: () => setPanel("results"),
2400
+ style: [explorerStyles.tab, panel === "results" && explorerStyles.tabActive],
2401
+ children: /* @__PURE__ */ jsxs(
2402
+ Text,
2403
+ {
2404
+ style: [explorerStyles.tabLabel, panel === "results" && explorerStyles.tabLabelActive],
2405
+ children: [
2406
+ "Results",
2407
+ result ? ` (${result.rowCount})` : ""
2408
+ ]
2409
+ }
2410
+ )
2411
+ }
2412
+ ),
2413
+ /* @__PURE__ */ jsx(
2414
+ Pressable,
2415
+ {
2416
+ onPress: () => setPanel("output"),
2417
+ style: [explorerStyles.tab, panel === "output" && explorerStyles.tabActive],
2418
+ children: /* @__PURE__ */ jsx(
2419
+ Text,
2420
+ {
2421
+ style: [explorerStyles.tabLabel, panel === "output" && explorerStyles.tabLabelActive],
2422
+ children: "Output"
2423
+ }
2424
+ )
2425
+ }
2426
+ )
2427
+ ] }),
2428
+ /* @__PURE__ */ jsx(View, { style: explorerStyles.sqlResultsPane, children: renderResultsContent() })
2429
+ ] });
2430
+ }
2431
+ function ExplorerTableDetail({ inspector, tableName, onBack }) {
2432
+ const [view, setView] = useState("data");
2433
+ const [columns, setColumns] = useState([]);
2434
+ const [pageResult, setPageResult] = useState(null);
2435
+ const [search, setSearch] = useState("");
2436
+ const [page, setPage] = useState(1);
2437
+ const [loading, setLoading] = useState(true);
2438
+ const [error, setError] = useState(null);
2439
+ const loadData = useCallback(async () => {
2440
+ setLoading(true);
2441
+ setError(null);
2442
+ try {
2443
+ const nextColumns = await inspector.getTableColumns(tableName);
2444
+ setColumns(nextColumns);
2445
+ const result = await inspector.fetchTablePage({
2446
+ table: tableName,
2447
+ page,
2448
+ pageSize: DEFAULT_PAGE_SIZE,
2449
+ search
2450
+ });
2451
+ setPageResult(result);
2452
+ } catch (loadError) {
2453
+ setError(loadError instanceof Error ? loadError.message : "Failed to load table");
2454
+ } finally {
2455
+ setLoading(false);
2456
+ }
2457
+ }, [inspector, page, search, tableName]);
2458
+ useEffect(() => {
2459
+ void loadData();
2460
+ }, [loadData]);
2461
+ const totalPages = pageResult ? Math.max(1, Math.ceil(pageResult.totalCount / pageResult.pageSize)) : 1;
2462
+ return /* @__PURE__ */ jsxs(View, { style: explorerStyles.fullScreen, children: [
2463
+ /* @__PURE__ */ jsxs(View, { style: explorerStyles.header, children: [
2464
+ /* @__PURE__ */ jsx(Pressable, { onPress: onBack, children: /* @__PURE__ */ jsx(Text, { style: explorerStyles.headerAction, children: "\u2190 Back" }) }),
2465
+ /* @__PURE__ */ jsx(Text, { style: explorerStyles.headerTitle, children: tableName }),
2466
+ /* @__PURE__ */ jsx(View, { style: { width: 48 } })
2467
+ ] }),
2468
+ /* @__PURE__ */ jsxs(View, { style: [explorerStyles.subTabRow, { paddingHorizontal: 16, paddingTop: 12 }], children: [
2469
+ /* @__PURE__ */ jsx(
2470
+ Pressable,
2471
+ {
2472
+ onPress: () => setView("data"),
2473
+ style: [explorerStyles.subTab, view === "data" && explorerStyles.subTabActive],
2474
+ children: /* @__PURE__ */ jsx(Text, { style: [explorerStyles.subTabLabel, view === "data" && explorerStyles.subTabLabelActive], children: "Data" })
2475
+ }
2476
+ ),
2477
+ /* @__PURE__ */ jsx(
2478
+ Pressable,
2479
+ {
2480
+ onPress: () => setView("schema"),
2481
+ style: [explorerStyles.subTab, view === "schema" && explorerStyles.subTabActive],
2482
+ children: /* @__PURE__ */ jsx(
2483
+ Text,
2484
+ {
2485
+ style: [explorerStyles.subTabLabel, view === "schema" && explorerStyles.subTabLabelActive],
2486
+ children: "Schema"
2487
+ }
2488
+ )
2489
+ }
2490
+ )
2491
+ ] }),
2492
+ view === "data" ? /* @__PURE__ */ jsxs(View, { style: tableStyles.detailBody, children: [
2493
+ /* @__PURE__ */ jsx(
2494
+ TextInput,
2495
+ {
2496
+ autoCapitalize: "none",
2497
+ autoCorrect: false,
2498
+ onChangeText: (value) => {
2499
+ setSearch(value);
2500
+ setPage(1);
2501
+ },
2502
+ placeholder: "Search rows\u2026",
2503
+ style: explorerStyles.searchInput,
2504
+ value: search
2505
+ }
2506
+ ),
2507
+ loading ? /* @__PURE__ */ jsx(ActivityIndicator, { style: { marginTop: 24 } }) : error ? /* @__PURE__ */ jsx(Text, { style: explorerStyles.errorText, children: error }) : pageResult ? /* @__PURE__ */ jsxs(Fragment, { children: [
2508
+ /* @__PURE__ */ jsx(View, { style: tableStyles.detailDataPane, children: /* @__PURE__ */ jsx(
2509
+ MobileDataView,
2510
+ {
2511
+ columns: pageResult.columns,
2512
+ dataKey: `${tableName}-${page}-${search}`,
2513
+ defaultMode: "table",
2514
+ metaSuffix: `page ${page} of ${totalPages}`,
2515
+ rows: pageResult.rows
2516
+ }
2517
+ ) }),
2518
+ /* @__PURE__ */ jsxs(View, { style: explorerStyles.paginationRow, children: [
2519
+ /* @__PURE__ */ jsx(Pressable, { disabled: page <= 1, onPress: () => setPage((current) => current - 1), children: /* @__PURE__ */ jsx(Text, { style: explorerStyles.headerAction, children: "Previous" }) }),
2520
+ /* @__PURE__ */ jsxs(Text, { style: explorerStyles.paginationLabel, children: [
2521
+ "Page ",
2522
+ page,
2523
+ " of ",
2524
+ totalPages,
2525
+ " \xB7 ",
2526
+ pageResult.totalCount.toLocaleString(),
2527
+ " rows"
2528
+ ] }),
2529
+ /* @__PURE__ */ jsx(
2530
+ Pressable,
2531
+ {
2532
+ disabled: page >= totalPages,
2533
+ onPress: () => setPage((current) => current + 1),
2534
+ children: /* @__PURE__ */ jsx(Text, { style: explorerStyles.headerAction, children: "Next" })
2535
+ }
2536
+ )
2537
+ ] })
2538
+ ] }) : null
2539
+ ] }) : /* @__PURE__ */ jsx(ScrollView, { contentContainerStyle: explorerStyles.content, children: columns.length === 0 ? /* @__PURE__ */ jsx(Text, { style: explorerStyles.placeholder, children: "No schema information." }) : columns.map((column) => /* @__PURE__ */ jsx(View, { style: explorerStyles.listItem, children: /* @__PURE__ */ jsxs(View, { style: { flex: 1 }, children: [
2540
+ /* @__PURE__ */ jsx(Text, { style: explorerStyles.listItemTitle, children: column.name }),
2541
+ /* @__PURE__ */ jsxs(Text, { style: explorerStyles.listItemMeta, children: [
2542
+ column.type,
2543
+ column.pk ? " \xB7 PK" : "",
2544
+ column.notNull ? " \xB7 NOT NULL" : "",
2545
+ column.defaultValue ? ` \xB7 DEFAULT ${column.defaultValue}` : ""
2546
+ ] })
2547
+ ] }) }, column.name)) })
2548
+ ] });
2549
+ }
2550
+ function ExplorerTablesTab({
2551
+ tables,
2552
+ loading,
2553
+ error,
2554
+ onSelectTable
2555
+ }) {
2556
+ const [search, setSearch] = useState("");
2557
+ const filtered = useMemo(() => {
2558
+ const term = search.trim().toLowerCase();
2559
+ if (!term) {
2560
+ return tables;
2561
+ }
2562
+ return tables.filter((table) => table.name.toLowerCase().includes(term));
2563
+ }, [search, tables]);
2564
+ if (loading) {
2565
+ return /* @__PURE__ */ jsx(Text, { style: explorerStyles.placeholder, children: "Loading tables\u2026" });
2566
+ }
2567
+ if (error) {
2568
+ return /* @__PURE__ */ jsx(Text, { style: explorerStyles.errorText, children: error });
2569
+ }
2570
+ return /* @__PURE__ */ jsxs(View, { style: { flex: 1, padding: 16 }, children: [
2571
+ /* @__PURE__ */ jsx(
2572
+ TextInput,
2573
+ {
2574
+ autoCapitalize: "none",
2575
+ autoCorrect: false,
2576
+ onChangeText: setSearch,
2577
+ placeholder: "Filter tables\u2026",
2578
+ style: explorerStyles.searchInput,
2579
+ value: search
2580
+ }
2581
+ ),
2582
+ filtered.length === 0 ? /* @__PURE__ */ jsx(Text, { style: explorerStyles.placeholder, children: "No tables match your search." }) : /* @__PURE__ */ jsx(
2583
+ FlatList,
2584
+ {
2585
+ data: filtered,
2586
+ keyExtractor: (item) => item.name,
2587
+ style: { flex: 1 },
2588
+ renderItem: ({ item }) => /* @__PURE__ */ jsxs(Pressable, { onPress: () => onSelectTable(item.name), style: explorerStyles.listItem, children: [
2589
+ /* @__PURE__ */ jsxs(View, { children: [
2590
+ /* @__PURE__ */ jsx(Text, { style: explorerStyles.listItemTitle, children: item.name }),
2591
+ /* @__PURE__ */ jsxs(Text, { style: explorerStyles.listItemMeta, children: [
2592
+ item.rowCount.toLocaleString(),
2593
+ " row",
2594
+ item.rowCount === 1 ? "" : "s"
2595
+ ] })
2596
+ ] }),
2597
+ /* @__PURE__ */ jsx(Text, { style: explorerStyles.listItemMeta, children: "\u203A" })
2598
+ ] })
2599
+ }
2600
+ )
2601
+ ] });
2602
+ }
2603
+ function MobileDatabaseExplorer() {
2604
+ const { explorerVisible, closeExplorer, mobileInspector } = useDevTools();
2605
+ const [tab, setTab] = useState("overview");
2606
+ const [screen, setScreen] = useState({ kind: "tabs" });
2607
+ const [info, setInfo] = useState(null);
2608
+ const [tables, setTables] = useState([]);
2609
+ const [loading, setLoading] = useState(false);
2610
+ const [error, setError] = useState(null);
2611
+ const loadOverview = useCallback(async () => {
2612
+ if (!mobileInspector) {
2613
+ return;
2614
+ }
2615
+ setLoading(true);
2616
+ setError(null);
2617
+ try {
2618
+ const [nextInfo, nextTables] = await Promise.all([
2619
+ mobileInspector.getDatabaseInfo(),
2620
+ mobileInspector.listTables()
2621
+ ]);
2622
+ setInfo(nextInfo);
2623
+ setTables(nextTables);
2624
+ } catch (loadError) {
2625
+ setError(loadError instanceof Error ? loadError.message : "Failed to load database");
2626
+ } finally {
2627
+ setLoading(false);
2628
+ }
2629
+ }, [mobileInspector]);
2630
+ useEffect(() => {
2631
+ if (!explorerVisible) {
2632
+ setScreen({ kind: "tabs" });
2633
+ setTab("overview");
2634
+ return;
2635
+ }
2636
+ void loadOverview();
2637
+ }, [explorerVisible, loadOverview]);
2638
+ if (!mobileInspector) {
2639
+ return null;
2640
+ }
2641
+ return /* @__PURE__ */ jsx(Modal, { animationType: "slide", onRequestClose: closeExplorer, visible: explorerVisible, children: screen.kind === "table" ? /* @__PURE__ */ jsx(
2642
+ ExplorerTableDetail,
2643
+ {
2644
+ inspector: mobileInspector,
2645
+ onBack: () => setScreen({ kind: "tabs" }),
2646
+ tableName: screen.tableName
2647
+ }
2648
+ ) : /* @__PURE__ */ jsxs(View, { style: explorerStyles.fullScreen, children: [
2649
+ /* @__PURE__ */ jsxs(View, { style: explorerStyles.header, children: [
2650
+ /* @__PURE__ */ jsx(Text, { style: explorerStyles.headerTitle, children: "Database Explorer" }),
2651
+ /* @__PURE__ */ jsx(Pressable, { accessibilityLabel: "Close database explorer", onPress: closeExplorer, children: /* @__PURE__ */ jsx(Text, { style: explorerStyles.headerAction, children: "Close" }) })
2652
+ ] }),
2653
+ /* @__PURE__ */ jsx(View, { style: explorerStyles.tabRow, children: ["overview", "tables", "sql"].map((item) => /* @__PURE__ */ jsx(
2654
+ Pressable,
2655
+ {
2656
+ onPress: () => setTab(item),
2657
+ style: [explorerStyles.tab, tab === item && explorerStyles.tabActive],
2658
+ children: /* @__PURE__ */ jsx(
2659
+ Text,
2660
+ {
2661
+ style: [explorerStyles.tabLabel, tab === item && explorerStyles.tabLabelActive],
2662
+ children: item === "overview" ? "Overview" : item === "tables" ? "Tables" : "SQL"
2663
+ }
2664
+ )
2665
+ },
2666
+ item
2667
+ )) }),
2668
+ /* @__PURE__ */ jsxs(View, { style: { flex: 1 }, children: [
2669
+ tab === "overview" && /* @__PURE__ */ jsx(
2670
+ ExplorerOverviewTab,
2671
+ {
2672
+ error,
2673
+ info,
2674
+ loading,
2675
+ onOpenSql: () => setTab("sql"),
2676
+ onOpenTables: () => setTab("tables")
2677
+ }
2678
+ ),
2679
+ tab === "tables" && /* @__PURE__ */ jsx(
2680
+ ExplorerTablesTab,
2681
+ {
2682
+ error,
2683
+ loading,
2684
+ onSelectTable: (tableName) => setScreen({ kind: "table", tableName }),
2685
+ tables
2686
+ }
2687
+ ),
2688
+ tab === "sql" && /* @__PURE__ */ jsx(ExplorerSqlTab, { inspector: mobileInspector })
2689
+ ] })
2690
+ ] }) });
2691
+ }
2692
+ function DatabaseDevTools({
2693
+ database,
2694
+ type,
2695
+ adapter,
2696
+ serverUrl,
2697
+ enabled,
2698
+ position = "bottom-right",
2699
+ draggable = true,
2700
+ snapToEdges = true,
2701
+ floatingPosition,
2702
+ onFloatingPositionChange,
2703
+ style,
2704
+ onConnectionStateChange
2705
+ }) {
2706
+ if (!isDevToolsEnabled(enabled)) {
2707
+ return null;
2708
+ }
2709
+ return /* @__PURE__ */ jsx(
2710
+ DevToolsProvider,
2711
+ {
2712
+ adapter,
2713
+ database,
2714
+ onConnectionStateChange,
2715
+ serverUrl,
2716
+ type,
2717
+ children: /* @__PURE__ */ jsxs(View, { pointerEvents: "box-none", style: StyleSheet.absoluteFill, children: [
2718
+ /* @__PURE__ */ jsx(
2719
+ FloatingDevToolsButton,
2720
+ {
2721
+ draggable,
2722
+ floatingPosition,
2723
+ iconStyle: style,
2724
+ onFloatingPositionChange,
2725
+ position,
2726
+ snapToEdges
2727
+ }
2728
+ ),
2729
+ /* @__PURE__ */ jsx(DevToolsLauncherModal, {}),
2730
+ /* @__PURE__ */ jsx(DevToolsSettingsModal, {}),
2731
+ /* @__PURE__ */ jsx(MobileDatabaseExplorer, {})
2732
+ ] })
2733
+ }
2734
+ );
2735
+ }
2736
+
2737
+ export { DatabaseDevTools, createExpoSqliteInspector, handleBeginTransaction, handleCommitTransaction, handleExecuteWrite, handleRollbackTransaction, isEditableDatabaseAdapter, isWritableDatabaseAdapter };
2738
+ //# sourceMappingURL=chunk-7UY2LDRW.js.map
2739
+ //# sourceMappingURL=chunk-7UY2LDRW.js.map