rozenite-sqlite 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/panel.html CHANGED
@@ -22,7 +22,7 @@
22
22
  <script>
23
23
  var __ROZENITE_PANEL__ = true;
24
24
  </script>
25
- <script type="module" crossorigin src="./assets/panel-3I5ccfMq.js"></script>
25
+ <script type="module" crossorigin src="./assets/panel-BKWy6irM.js"></script>
26
26
  </head>
27
27
  <body>
28
28
  <div id="root"></div>
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const E=require("react"),S=require("@rozenite/plugin-bridge"),_="rozenite-sqlite",s={GET_DB_LIST:"get-db-list",SEND_DB_LIST:"send-db-list",SQL_EXECUTE:"sql-execute",SQL_EXEC_RESULT:"sql-exec-result"};function l(o){const e=S.useRozeniteDevToolsClient({pluginId:_}),n=E.useRef(o);E.useEffect(()=>{n.current=o}),E.useEffect(()=>{if(!e)return;const u=[e.onMessage(s.GET_DB_LIST,()=>{e.send(s.SEND_DB_LIST,n.current.databases)}),e.onMessage(s.SQL_EXECUTE,r=>{const{dbName:c,query:i}=r;n.current.sqlExecutor(c,i).then(t=>{e.send(s.SQL_EXEC_RESULT,t)},t=>{e.send(s.SQL_EXEC_RESULT,{error:t instanceof Error?t.message:String(t)})})})];return()=>{u.forEach(r=>r.remove())}},[e])}exports.useRozeniteSQLite=l;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const S=require("react"),f=require("@rozenite/plugin-bridge"),d="rozenite-sqlite",s={GET_DB_LIST:"get-db-list",SEND_DB_LIST:"send-db-list",SQL_EXECUTE:"sql-execute",SQL_EXEC_RESULT:"sql-exec-result",SAVE_ROW:"save-row",DELETE_ROW:"delete-row",CLEAR_TABLE:"clear-table",MUTATION_RESULT:"mutation-result"};function l(n){return n==null?"NULL":typeof n=="number"?String(n):typeof n=="boolean"?n?"1":"0":`'${String(n).replace(/'/g,"''")}'`}function g(n){const e=f.useRozeniteDevToolsClient({pluginId:d}),T=S.useRef(n);S.useEffect(()=>{T.current=n}),S.useEffect(()=>{if(!e)return;const L=[e.onMessage(s.GET_DB_LIST,()=>{e.send(s.SEND_DB_LIST,T.current.databases)}),e.onMessage(s.SQL_EXECUTE,r=>{const{dbName:u,query:o}=r;T.current.sqlExecutor(u,o).then(t=>{e.send(s.SQL_EXEC_RESULT,t)},t=>{e.send(s.SQL_EXEC_RESULT,{error:t instanceof Error?t.message:String(t)})})}),e.onMessage(s.SAVE_ROW,r=>{const{dbName:u,table:o,row:t,primaryKey:E}=r,a=t[E],_=Object.keys(t).filter(c=>c!==E).map(c=>`"${c}" = ${l(t[c])}`).join(", "),R=`UPDATE "${o}" SET ${_} WHERE "${E}" = ${l(a)}`;T.current.sqlExecutor(u,R).then(()=>e.send(s.MUTATION_RESULT,{success:!0}),c=>e.send(s.MUTATION_RESULT,{success:!1,error:c instanceof Error?c.message:String(c)}))}),e.onMessage(s.DELETE_ROW,r=>{const{dbName:u,table:o,primaryKey:t,primaryKeyValue:E}=r,a=`DELETE FROM "${o}" WHERE "${t}" = ${l(E)}`;T.current.sqlExecutor(u,a).then(()=>e.send(s.MUTATION_RESULT,{success:!0}),i=>e.send(s.MUTATION_RESULT,{success:!1,error:i instanceof Error?i.message:String(i)}))}),e.onMessage(s.CLEAR_TABLE,r=>{const{dbName:u,table:o}=r,t=`DELETE FROM "${o}"`;T.current.sqlExecutor(u,t).then(()=>e.send(s.MUTATION_RESULT,{success:!0}),E=>e.send(s.MUTATION_RESULT,{success:!1,error:E instanceof Error?E.message:String(E)}))})];return()=>{L.forEach(r=>r.remove())}},[e])}exports.useRozeniteSQLite=g;
@@ -8,7 +8,7 @@ export declare interface RozeniteSQLiteConfig {
8
8
  export declare type SQLExecutor = (dbName: string, query: string) => Promise<Record<string, unknown>[]>;
9
9
 
10
10
  /**
11
- * Connects your React Native app to the Rozenite SQLite devtools panel.
11
+ * Connects your React Native app to the SQLighter devtools panel.
12
12
  *
13
13
  * Call this once somewhere near the root of your app (or in the component
14
14
  * that holds the database instances). It handles all devtools communication —
@@ -1,40 +1,77 @@
1
- import { useRef as i, useEffect as r } from "react";
2
- import { useRozeniteDevToolsClient as u } from "@rozenite/plugin-bridge";
3
- const L = "rozenite-sqlite", t = {
1
+ import { useRef as f, useEffect as L } from "react";
2
+ import { useRozeniteDevToolsClient as U } from "@rozenite/plugin-bridge";
3
+ const m = "rozenite-sqlite", s = {
4
4
  GET_DB_LIST: "get-db-list",
5
5
  SEND_DB_LIST: "send-db-list",
6
6
  SQL_EXECUTE: "sql-execute",
7
- SQL_EXEC_RESULT: "sql-exec-result"
7
+ SQL_EXEC_RESULT: "sql-exec-result",
8
+ SAVE_ROW: "save-row",
9
+ DELETE_ROW: "delete-row",
10
+ CLEAR_TABLE: "clear-table",
11
+ MUTATION_RESULT: "mutation-result"
8
12
  };
9
- function f(o) {
10
- const e = u({ pluginId: L }), n = i(o);
11
- r(() => {
12
- n.current = o;
13
- }), r(() => {
13
+ function S(n) {
14
+ return n == null ? "NULL" : typeof n == "number" ? String(n) : typeof n == "boolean" ? n ? "1" : "0" : `'${String(n).replace(/'/g, "''")}'`;
15
+ }
16
+ function N(n) {
17
+ const e = U({ pluginId: m }), T = f(n);
18
+ L(() => {
19
+ T.current = n;
20
+ }), L(() => {
14
21
  if (!e) return;
15
- const c = [
16
- e.onMessage(t.GET_DB_LIST, () => {
17
- e.send(t.SEND_DB_LIST, n.current.databases);
22
+ const _ = [
23
+ e.onMessage(s.GET_DB_LIST, () => {
24
+ e.send(s.SEND_DB_LIST, T.current.databases);
18
25
  }),
19
- e.onMessage(t.SQL_EXECUTE, (E) => {
20
- const { dbName: S, query: _ } = E;
21
- n.current.sqlExecutor(S, _).then(
22
- (s) => {
23
- e.send(t.SQL_EXEC_RESULT, s);
26
+ e.onMessage(s.SQL_EXECUTE, (r) => {
27
+ const { dbName: o, query: u } = r;
28
+ T.current.sqlExecutor(o, u).then(
29
+ (t) => {
30
+ e.send(s.SQL_EXEC_RESULT, t);
24
31
  },
25
- (s) => {
26
- e.send(t.SQL_EXEC_RESULT, {
27
- error: s instanceof Error ? s.message : String(s)
32
+ (t) => {
33
+ e.send(s.SQL_EXEC_RESULT, {
34
+ error: t instanceof Error ? t.message : String(t)
28
35
  });
29
36
  }
30
37
  );
38
+ }),
39
+ e.onMessage(s.SAVE_ROW, (r) => {
40
+ const { dbName: o, table: u, row: t, primaryKey: E } = r, i = t[E], l = Object.keys(t).filter((c) => c !== E).map((c) => `"${c}" = ${S(t[c])}`).join(", "), R = `UPDATE "${u}" SET ${l} WHERE "${E}" = ${S(i)}`;
41
+ T.current.sqlExecutor(o, R).then(
42
+ () => e.send(s.MUTATION_RESULT, { success: !0 }),
43
+ (c) => e.send(s.MUTATION_RESULT, {
44
+ success: !1,
45
+ error: c instanceof Error ? c.message : String(c)
46
+ })
47
+ );
48
+ }),
49
+ e.onMessage(s.DELETE_ROW, (r) => {
50
+ const { dbName: o, table: u, primaryKey: t, primaryKeyValue: E } = r, i = `DELETE FROM "${u}" WHERE "${t}" = ${S(E)}`;
51
+ T.current.sqlExecutor(o, i).then(
52
+ () => e.send(s.MUTATION_RESULT, { success: !0 }),
53
+ (a) => e.send(s.MUTATION_RESULT, {
54
+ success: !1,
55
+ error: a instanceof Error ? a.message : String(a)
56
+ })
57
+ );
58
+ }),
59
+ e.onMessage(s.CLEAR_TABLE, (r) => {
60
+ const { dbName: o, table: u } = r, t = `DELETE FROM "${u}"`;
61
+ T.current.sqlExecutor(o, t).then(
62
+ () => e.send(s.MUTATION_RESULT, { success: !0 }),
63
+ (E) => e.send(s.MUTATION_RESULT, {
64
+ success: !1,
65
+ error: E instanceof Error ? E.message : String(E)
66
+ })
67
+ );
31
68
  })
32
69
  ];
33
70
  return () => {
34
- c.forEach((E) => E.remove());
71
+ _.forEach((r) => r.remove());
35
72
  };
36
73
  }, [e]);
37
74
  }
38
75
  export {
39
- f as useRozeniteSQLite
76
+ N as useRozeniteSQLite
40
77
  };
@@ -1 +1 @@
1
- {"name":"rozenite-sqlite","version":"0.0.11","description":"SQLite explorer for RN devtools","panels":[{"name":"SQLite","source":"/panel.html"}]}
1
+ {"name":"rozenite-sqlite","version":"0.0.12","description":"SQLighter - SQLite explorer for React Native devtools","panels":[{"name":"SQLighter","source":"/panel.html"}]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rozenite-sqlite",
3
- "version": "0.0.11",
4
- "description": "SQLite explorer for RN devtools",
3
+ "version": "0.0.12",
4
+ "description": "SQLighter - SQLite explorer for React Native devtools",
5
5
  "type": "module",
6
6
  "main": "./dist/react-native.js",
7
7
  "module": "./dist/react-native.js",
@@ -1,7 +1,7 @@
1
1
  export default {
2
2
  panels: [
3
3
  {
4
- name: 'SQLite',
4
+ name: 'SQLighter',
5
5
  source: './src/panel.tsx',
6
6
  },
7
7
  ],
@@ -6,10 +6,10 @@ import {
6
6
  ScrollView,
7
7
  Pressable,
8
8
  TextInput,
9
- ActivityIndicator,
10
9
  } from 'react-native';
11
10
  import { C, type RowData } from '../theme';
12
11
  import Pagination from './Pagination';
12
+ import { Placeholder } from './Placeholder';
13
13
  import type { ExplorerStatus } from '../hooks/useExplorerState';
14
14
 
15
15
  export interface DataTableProps {
@@ -20,13 +20,14 @@ export interface DataTableProps {
20
20
  status: ExplorerStatus;
21
21
  error: string | null;
22
22
  onClearTable?: () => void;
23
+ onReconnect?: () => void;
23
24
  }
24
25
 
25
26
  const COL_WIDTH = 160;
26
27
  const IDX_WIDTH = 44;
27
28
  const HEADER_HEIGHT = 42;
28
29
 
29
- export function DataTable({ columns, rows, selectedRowIndex, onRowSelect, status, error, onClearTable }: DataTableProps) {
30
+ export function DataTable({ columns, rows, selectedRowIndex, onRowSelect, status, error, onClearTable, onReconnect }: DataTableProps) {
30
31
  const [sortCol, setSortCol] = useState<string | null>(null);
31
32
  const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
32
33
  const [search, setSearch] = useState<Record<string, string>>({});
@@ -152,52 +153,56 @@ export function DataTable({ columns, rows, selectedRowIndex, onRowSelect, status
152
153
 
153
154
  if (status === 'connecting') {
154
155
  return (
155
- <View style={s.placeholder}>
156
- <Text style={s.placeholderEmoji}>📡</Text>
157
- <Text style={s.placeholderTitle}>Waiting for app…</Text>
158
- <Text style={s.placeholderSub}>
159
- Make sure your app is running and useRozeniteSQLite is initialized
160
- </Text>
161
- </View>
156
+ <Placeholder
157
+ emoji="📡"
158
+ title="Waiting for app…"
159
+ subtitle="Make sure your app is running and useSQLighter is initialized"
160
+ buttonText={onReconnect ? "↻ Reconnect" : undefined}
161
+ onButtonPress={onReconnect}
162
+ />
162
163
  );
163
164
  }
164
165
 
165
166
  if (status === 'error') {
166
167
  return (
167
- <View style={s.placeholder}>
168
- <Text style={s.placeholderEmoji}>⚠️</Text>
169
- <Text style={s.placeholderTitle}>Query failed</Text>
170
- <Text style={s.placeholderSub}>{error}</Text>
171
- </View>
168
+ <Placeholder
169
+ emoji="⚠️"
170
+ title="Query failed"
171
+ subtitle={error ?? undefined}
172
+ buttonText={onReconnect ? "↻ Try again" : undefined}
173
+ onButtonPress={onReconnect}
174
+ />
172
175
  );
173
176
  }
174
177
 
175
178
  if (status === 'loadingTables' || status === 'loadingData') {
176
179
  return (
177
- <View style={s.placeholder}>
178
- <ActivityIndicator size="large" color={C.accent} />
179
- <Text style={s.placeholderText}>Loading…</Text>
180
- </View>
180
+ <Placeholder
181
+ loading
182
+ title="Loading…"
183
+ buttonText={onReconnect ? "↻ Refresh" : undefined}
184
+ onButtonPress={onReconnect}
185
+ />
181
186
  );
182
187
  }
183
188
 
184
189
  if (columns.length === 0) {
185
190
  return (
186
- <View style={s.placeholder}>
187
- <Text style={s.placeholderEmoji}>🗄️</Text>
188
- <Text style={s.placeholderTitle}>No data to display</Text>
189
- <Text style={s.placeholderSub}>Select a database and table above</Text>
190
- </View>
191
+ <Placeholder
192
+ emoji="🗄️"
193
+ title="No data to display"
194
+ subtitle="Select a database and table above"
195
+ />
191
196
  );
192
197
  }
193
198
 
194
199
  if (rows.length === 0) {
195
200
  return (
196
- <View style={s.placeholder}>
197
- <Text style={s.placeholderEmoji}>📭</Text>
198
- <Text style={s.placeholderTitle}>Table is empty</Text>
199
- <Text style={s.placeholderSub}>This table has no rows yet</Text>
200
- </View>
201
+ <Placeholder
202
+ emoji="📭"
203
+ title="Table is empty"
204
+ subtitle="This table has no rows yet"
205
+ />
201
206
  );
202
207
  }
203
208
 
@@ -436,10 +441,7 @@ const s = StyleSheet.create({
436
441
  paddingHorizontal: 10,
437
442
  paddingVertical: 7,
438
443
  zIndex: 100,
439
- shadowColor: '#000',
440
- shadowOffset: { width: 0, height: 6 },
441
- shadowOpacity: 0.5,
442
- shadowRadius: 16,
444
+ boxShadow: '0 6px 16px rgba(0,0,0,0.5)',
443
445
  },
444
446
  filterInput: {
445
447
  flex: 1,
@@ -476,11 +478,6 @@ const s = StyleSheet.create({
476
478
  idxText: { fontSize: 11, color: C.textMuted, fontWeight: '600', textAlign: 'center' },
477
479
  noResults: { paddingVertical: 32, alignItems: 'center' },
478
480
  noResultsText: { fontSize: 13, color: C.textMuted, fontStyle: 'italic' },
479
- placeholder: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 60 },
480
- placeholderEmoji: { fontSize: 36, marginBottom: 16 },
481
- placeholderTitle: { fontSize: 15, fontWeight: '600', color: C.textSecondary, marginBottom: 6 },
482
- placeholderSub: { fontSize: 13, color: C.textMuted },
483
- placeholderText: { fontSize: 14, color: C.textSecondary, marginTop: 14 },
484
481
  resizeHandle: {
485
482
  position: 'absolute',
486
483
  right: 0,
@@ -117,10 +117,7 @@ const s = StyleSheet.create({
117
117
  borderColor: C.border,
118
118
  borderRadius: 8,
119
119
  overflow: 'hidden',
120
- shadowColor: '#000',
121
- shadowOffset: { width: 0, height: 8 },
122
- shadowOpacity: 0.5,
123
- shadowRadius: 20,
120
+ boxShadow: '0 8px 20px rgba(0,0,0,0.5)',
124
121
  },
125
122
  menuScroll: { maxHeight: 200 },
126
123
  menuEmpty: { padding: 14, fontSize: 12, color: C.textMuted, textAlign: 'center' },
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet, Pressable, ActivityIndicator } from 'react-native';
3
+ import { C } from '../theme';
4
+
5
+ export interface PlaceholderProps {
6
+ emoji?: string;
7
+ title?: string;
8
+ subtitle?: string;
9
+ loading?: boolean;
10
+ buttonText?: string;
11
+ onButtonPress?: () => void;
12
+ }
13
+
14
+ export function Placeholder({
15
+ emoji,
16
+ title,
17
+ subtitle,
18
+ loading,
19
+ buttonText,
20
+ onButtonPress,
21
+ }: PlaceholderProps) {
22
+ return (
23
+ <View style={s.container}>
24
+ {loading ? (
25
+ <ActivityIndicator size="large" color={C.accent} />
26
+ ) : emoji ? (
27
+ <Text style={s.emoji}>{emoji}</Text>
28
+ ) : null}
29
+ {title && <Text style={s.title}>{title}</Text>}
30
+ {subtitle && <Text style={s.subtitle}>{subtitle}</Text>}
31
+ {buttonText && onButtonPress && (
32
+ <Pressable
33
+ style={({ pressed }) => [s.button, pressed && s.buttonPressed]}
34
+ onPress={onButtonPress}
35
+ >
36
+ <Text style={s.buttonText}>{buttonText}</Text>
37
+ </Pressable>
38
+ )}
39
+ </View>
40
+ );
41
+ }
42
+
43
+ const s = StyleSheet.create({
44
+ container: {
45
+ flex: 1,
46
+ alignItems: 'center',
47
+ justifyContent: 'center',
48
+ padding: 60,
49
+ },
50
+ emoji: {
51
+ fontSize: 36,
52
+ marginBottom: 16,
53
+ },
54
+ title: {
55
+ fontSize: 15,
56
+ fontWeight: '600',
57
+ color: C.textSecondary,
58
+ marginBottom: 6,
59
+ marginTop: 14,
60
+ },
61
+ subtitle: {
62
+ fontSize: 13,
63
+ color: C.textMuted,
64
+ textAlign: 'center',
65
+ },
66
+ button: {
67
+ marginTop: 20,
68
+ paddingHorizontal: 16,
69
+ paddingVertical: 10,
70
+ backgroundColor: C.accent,
71
+ borderRadius: 6,
72
+ },
73
+ buttonPressed: {
74
+ opacity: 0.7,
75
+ },
76
+ buttonText: {
77
+ fontSize: 13,
78
+ fontWeight: '600',
79
+ color: '#fff',
80
+ },
81
+ });
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { View, Text, StyleSheet, ScrollView, Pressable, TextInput } from 'react-native';
2
+ import { View, Text, StyleSheet, ScrollView, Pressable, TextInput, ActivityIndicator } from 'react-native';
3
3
  import { C, type RowData } from '../theme';
4
+ import type { MutationResult } from '../hooks/useBridgeSync';
4
5
 
5
6
  function isJsonString(val: string): boolean {
6
7
  const t = val.trim();
@@ -12,11 +13,14 @@ export interface RowDetailPanelProps {
12
13
  row: RowData | null;
13
14
  rowIndex: number | null;
14
15
  onClose: () => void;
15
- onSave: (updated: RowData) => void;
16
- onDelete: () => void;
16
+ onSave: (updated: RowData) => Promise<MutationResult>;
17
+ onDelete: () => Promise<MutationResult>;
18
+ mutating?: boolean;
19
+ mutationError?: string | null;
20
+ onClearError?: () => void;
17
21
  }
18
22
 
19
- export function RowDetailPanel({ row, rowIndex, onClose, onSave, onDelete }: RowDetailPanelProps) {
23
+ export function RowDetailPanel({ row, rowIndex, onClose, onSave, onDelete, mutating, mutationError, onClearError }: RowDetailPanelProps) {
20
24
  const [draft, setDraft] = useState<RowData>({});
21
25
  const [dirty, setDirty] = useState(false);
22
26
  const [confirmDelete, setConfirmDelete] = useState(false);
@@ -45,9 +49,20 @@ export function RowDetailPanel({ row, rowIndex, onClose, onSave, onDelete }: Row
45
49
  setDirty(true);
46
50
  };
47
51
 
48
- const handleSave = () => {
49
- onSave(draft);
50
- setDirty(false);
52
+ const handleSave = async () => {
53
+ onClearError?.();
54
+ const result = await onSave(draft);
55
+ if (result.success) {
56
+ setDirty(false);
57
+ }
58
+ };
59
+
60
+ const handleDelete = async () => {
61
+ onClearError?.();
62
+ const result = await onDelete();
63
+ if (result.success) {
64
+ setConfirmDelete(false);
65
+ }
51
66
  };
52
67
 
53
68
  const handleCancel = () => {
@@ -100,25 +115,41 @@ export function RowDetailPanel({ row, rowIndex, onClose, onSave, onDelete }: Row
100
115
  </ScrollView>
101
116
 
102
117
  <View style={s.primaryActions}>
103
- <Pressable
104
- style={({ pressed }) => [s.btn, s.btnSave, !dirty && s.btnSaveDisabled, pressed && !dirty ? {} : pressed && s.btnPressed]}
105
- onPress={dirty ? handleSave : undefined}
106
- >
107
- <Text style={[s.btnSaveText, !dirty && s.btnSaveTextDisabled]}>Save</Text>
108
- </Pressable>
109
- <Pressable
110
- style={({ pressed }) => [s.btn, s.btnCancel, pressed && s.btnPressed]}
111
- onPress={handleCancel}
112
- >
113
- <Text style={s.btnCancelText}>Cancel</Text>
114
- </Pressable>
118
+ {mutationError && (
119
+ <View style={s.errorBox}>
120
+ <Text style={s.errorText}>{mutationError}</Text>
121
+ <Pressable onPress={onClearError} style={s.errorClose}>
122
+ <Text style={s.errorCloseText}>✕</Text>
123
+ </Pressable>
124
+ </View>
125
+ )}
126
+ <View style={s.primaryActionsRow}>
127
+ <Pressable
128
+ style={({ pressed }) => [s.btn, s.btnSave, (!dirty || mutating) && s.btnSaveDisabled, pressed && !dirty ? {} : pressed && s.btnPressed]}
129
+ onPress={dirty && !mutating ? handleSave : undefined}
130
+ disabled={!dirty || mutating}
131
+ >
132
+ {mutating ? (
133
+ <ActivityIndicator size="small" color={C.bg} />
134
+ ) : (
135
+ <Text style={[s.btnSaveText, !dirty && s.btnSaveTextDisabled]}>Save</Text>
136
+ )}
137
+ </Pressable>
138
+ <Pressable
139
+ style={({ pressed }) => [s.btn, s.btnCancel, pressed && s.btnPressed]}
140
+ onPress={handleCancel}
141
+ >
142
+ <Text style={s.btnCancelText}>Cancel</Text>
143
+ </Pressable>
144
+ </View>
115
145
  </View>
116
146
 
117
147
  <View style={s.deleteZone}>
118
148
  {!confirmDelete ? (
119
149
  <Pressable
120
- style={({ pressed }) => [s.btnDeleteSmall, pressed && s.btnPressed]}
121
- onPress={() => setConfirmDelete(true)}
150
+ style={({ pressed }) => [s.btnDeleteSmall, mutating && s.btnSaveDisabled, pressed && s.btnPressed]}
151
+ onPress={() => !mutating && setConfirmDelete(true)}
152
+ disabled={mutating}
122
153
  >
123
154
  <Text style={s.btnDeleteSmallText}>⊘ Delete this row</Text>
124
155
  </Pressable>
@@ -127,10 +158,15 @@ export function RowDetailPanel({ row, rowIndex, onClose, onSave, onDelete }: Row
127
158
  <Text style={s.confirmLabel}>This action cannot be undone.</Text>
128
159
  <View style={s.confirmRow}>
129
160
  <Pressable
130
- style={({ pressed }) => [s.btn, s.btnConfirm, { flex: 1 }, pressed && s.btnPressed]}
131
- onPress={onDelete}
161
+ style={({ pressed }) => [s.btn, s.btnConfirm, { flex: 1 }, mutating && s.btnSaveDisabled, pressed && s.btnPressed]}
162
+ onPress={!mutating ? handleDelete : undefined}
163
+ disabled={mutating}
132
164
  >
133
- <Text style={s.btnConfirmText}>Yes, delete</Text>
165
+ {mutating ? (
166
+ <ActivityIndicator size="small" color="#fff" />
167
+ ) : (
168
+ <Text style={s.btnConfirmText}>Yes, delete</Text>
169
+ )}
134
170
  </Pressable>
135
171
  <Pressable
136
172
  style={({ pressed }) => [s.btn, s.btnCancelSmall, { flex: 1 }, pressed && s.btnPressed]}
@@ -153,11 +189,6 @@ const s = StyleSheet.create({
153
189
  borderLeftWidth: 1,
154
190
  borderLeftColor: C.border,
155
191
  flexDirection: 'column',
156
- shadowColor: '#000',
157
- shadowOffset: { width: -8, height: 0 },
158
- shadowOpacity: 0.45,
159
- shadowRadius: 20,
160
- elevation: 10,
161
192
  boxShadow: '-8px 0 24px rgba(0,0,0,0.45)',
162
193
  },
163
194
  header: {
@@ -228,7 +259,7 @@ const s = StyleSheet.create({
228
259
  paddingTop: 10,
229
260
  },
230
261
  primaryActions: {
231
- flexDirection: 'row',
262
+ flexDirection: 'column',
232
263
  gap: 8,
233
264
  paddingHorizontal: 16,
234
265
  paddingTop: 14,
@@ -236,6 +267,10 @@ const s = StyleSheet.create({
236
267
  borderTopWidth: 1,
237
268
  borderTopColor: C.border,
238
269
  },
270
+ primaryActionsRow: {
271
+ flexDirection: 'row',
272
+ gap: 8,
273
+ },
239
274
  deleteZone: {
240
275
  paddingHorizontal: 16,
241
276
  paddingBottom: 14,
@@ -270,4 +305,20 @@ const s = StyleSheet.create({
270
305
  btnConfirm: { backgroundColor: C.danger },
271
306
  btnConfirmText: { fontSize: 12, fontWeight: '600', color: '#fff' },
272
307
  btnCancelSmall: { backgroundColor: C.surface2, borderWidth: 1, borderColor: C.border },
308
+ errorBox: {
309
+ flexDirection: 'row',
310
+ alignItems: 'center',
311
+ justifyContent: 'space-between',
312
+ backgroundColor: 'rgba(239,68,68,0.15)',
313
+ borderWidth: 1,
314
+ borderColor: 'rgba(239,68,68,0.3)',
315
+ borderRadius: 6,
316
+ paddingHorizontal: 10,
317
+ paddingVertical: 8,
318
+ marginBottom: 8,
319
+ width: '100%',
320
+ },
321
+ errorText: { flex: 1, fontSize: 12, color: C.danger, fontWeight: '500' },
322
+ errorClose: { marginLeft: 8, padding: 4 },
323
+ errorCloseText: { fontSize: 12, color: C.danger },
273
324
  });
package/src/constants.ts CHANGED
@@ -5,6 +5,10 @@ export const EVENTS = {
5
5
  SEND_DB_LIST: 'send-db-list',
6
6
  SQL_EXECUTE: 'sql-execute',
7
7
  SQL_EXEC_RESULT: 'sql-exec-result',
8
+ SAVE_ROW: 'save-row',
9
+ DELETE_ROW: 'delete-row',
10
+ CLEAR_TABLE: 'clear-table',
11
+ MUTATION_RESULT: 'mutation-result',
8
12
  } as const;
9
13
 
10
14
  export const QUERIES = {