database-devtools 0.1.1 → 0.1.3

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