rozenite-sqlite 0.0.7 → 0.0.9
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/package.json +1 -1
- package/CHANGELOG.md +0 -7
- package/dist/assets/panel-3I5ccfMq.js +0 -58
- package/dist/panel.html +0 -30
- package/dist/react-native.cjs +0 -1
- package/dist/react-native.d.ts +0 -28
- package/dist/react-native.js +0 -40
- package/dist/rozenite.json +0 -1
- package/react-native.ts +0 -2
- package/rozenite.config.ts +0 -8
- package/src/components/DataTable.tsx +0 -504
- package/src/components/Dropdown.tsx +0 -139
- package/src/components/Pagination.tsx +0 -266
- package/src/components/RowDetailPanel.tsx +0 -273
- package/src/components/SqlPanel.tsx +0 -220
- package/src/components/Toolbar.tsx +0 -168
- package/src/constants.ts +0 -13
- package/src/hooks/useBridgeSync.ts +0 -146
- package/src/hooks/useExplorerState.ts +0 -160
- package/src/hooks/useRozeniteSQLite.ts +0 -68
- package/src/index.ts +0 -1
- package/src/mockData.ts +0 -210
- package/src/panel.tsx +0 -70
- package/src/theme.ts +0 -17
- package/src/types/react-native-web.d.ts +0 -13
- package/tsconfig.json +0 -25
- package/vite.config.ts +0 -20
package/dist/panel.html
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>panel Panel</title>
|
|
7
|
-
<style>
|
|
8
|
-
html,
|
|
9
|
-
body {
|
|
10
|
-
height: 100%;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
body {
|
|
14
|
-
overflow: hidden;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
#root {
|
|
18
|
-
display: flex;
|
|
19
|
-
height: 100%;
|
|
20
|
-
}
|
|
21
|
-
</style>
|
|
22
|
-
<script>
|
|
23
|
-
var __ROZENITE_PANEL__ = true;
|
|
24
|
-
</script>
|
|
25
|
-
<script type="module" crossorigin src="./assets/panel-3I5ccfMq.js"></script>
|
|
26
|
-
</head>
|
|
27
|
-
<body>
|
|
28
|
-
<div id="root"></div>
|
|
29
|
-
</body>
|
|
30
|
-
</html>
|
package/dist/react-native.cjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
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;
|
package/dist/react-native.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export declare interface RozeniteSQLiteConfig {
|
|
2
|
-
/** List of database names exposed to the devtools panel, e.g. ["app.db", "cache.db"] */
|
|
3
|
-
databases: string[];
|
|
4
|
-
/** Library-agnostic SQL runner — receives the db name and raw query, returns rows */
|
|
5
|
-
sqlExecutor: SQLExecutor;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export declare type SQLExecutor = (dbName: string, query: string) => Promise<Record<string, unknown>[]>;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Connects your React Native app to the Rozenite SQLite devtools panel.
|
|
12
|
-
*
|
|
13
|
-
* Call this once somewhere near the root of your app (or in the component
|
|
14
|
-
* that holds the database instances). It handles all devtools communication —
|
|
15
|
-
* you don't need to touch the plugin-bridge directly.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* useRozeniteSQLite({
|
|
19
|
-
* databases: ['app.db', 'cache.db'],
|
|
20
|
-
* sqlExecutor: async (dbName, query) => {
|
|
21
|
-
* const db = myDatabases[dbName];
|
|
22
|
-
* return db.getAllAsync(query);
|
|
23
|
-
* },
|
|
24
|
-
* });
|
|
25
|
-
*/
|
|
26
|
-
export declare function useRozeniteSQLite(config: RozeniteSQLiteConfig): void;
|
|
27
|
-
|
|
28
|
-
export { }
|
package/dist/react-native.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
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 = {
|
|
4
|
-
GET_DB_LIST: "get-db-list",
|
|
5
|
-
SEND_DB_LIST: "send-db-list",
|
|
6
|
-
SQL_EXECUTE: "sql-execute",
|
|
7
|
-
SQL_EXEC_RESULT: "sql-exec-result"
|
|
8
|
-
};
|
|
9
|
-
function f(o) {
|
|
10
|
-
const e = u({ pluginId: L }), n = i(o);
|
|
11
|
-
r(() => {
|
|
12
|
-
n.current = o;
|
|
13
|
-
}), r(() => {
|
|
14
|
-
if (!e) return;
|
|
15
|
-
const c = [
|
|
16
|
-
e.onMessage(t.GET_DB_LIST, () => {
|
|
17
|
-
e.send(t.SEND_DB_LIST, n.current.databases);
|
|
18
|
-
}),
|
|
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);
|
|
24
|
-
},
|
|
25
|
-
(s) => {
|
|
26
|
-
e.send(t.SQL_EXEC_RESULT, {
|
|
27
|
-
error: s instanceof Error ? s.message : String(s)
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
);
|
|
31
|
-
})
|
|
32
|
-
];
|
|
33
|
-
return () => {
|
|
34
|
-
c.forEach((E) => E.remove());
|
|
35
|
-
};
|
|
36
|
-
}, [e]);
|
|
37
|
-
}
|
|
38
|
-
export {
|
|
39
|
-
f as useRozeniteSQLite
|
|
40
|
-
};
|
package/dist/rozenite.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"name":"rozenite-sqlite","version":"0.0.7","description":"SQLite explorer for RN devtools","panels":[{"name":"SQLite","source":"/panel.html"}]}
|
package/react-native.ts
DELETED
package/rozenite.config.ts
DELETED
|
@@ -1,504 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
View,
|
|
4
|
-
Text,
|
|
5
|
-
StyleSheet,
|
|
6
|
-
ScrollView,
|
|
7
|
-
Pressable,
|
|
8
|
-
TextInput,
|
|
9
|
-
ActivityIndicator,
|
|
10
|
-
} from 'react-native';
|
|
11
|
-
import { C, type RowData } from '../theme';
|
|
12
|
-
import Pagination from './Pagination';
|
|
13
|
-
import type { ExplorerStatus } from '../hooks/useExplorerState';
|
|
14
|
-
|
|
15
|
-
export interface DataTableProps {
|
|
16
|
-
columns: string[];
|
|
17
|
-
rows: RowData[];
|
|
18
|
-
selectedRowIndex: number | null;
|
|
19
|
-
onRowSelect: (originalIndex: number) => void;
|
|
20
|
-
status: ExplorerStatus;
|
|
21
|
-
error: string | null;
|
|
22
|
-
onClearTable?: () => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const COL_WIDTH = 160;
|
|
26
|
-
const IDX_WIDTH = 44;
|
|
27
|
-
const HEADER_HEIGHT = 42;
|
|
28
|
-
|
|
29
|
-
export function DataTable({ columns, rows, selectedRowIndex, onRowSelect, status, error, onClearTable }: DataTableProps) {
|
|
30
|
-
const [sortCol, setSortCol] = useState<string | null>(null);
|
|
31
|
-
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
|
|
32
|
-
const [search, setSearch] = useState<Record<string, string>>({});
|
|
33
|
-
const [filterOpenCol, setFilterOpenCol] = useState<string | null>(null);
|
|
34
|
-
const [scrollX, setScrollX] = useState(0);
|
|
35
|
-
const [page, setPage] = useState(1);
|
|
36
|
-
const [pageSize, setPageSize] = useState(25);
|
|
37
|
-
const [colWidths, setColWidths] = useState<Record<string, number>>({});
|
|
38
|
-
const resizeRef = useRef<{ col: string; startX: number; startW: number } | null>(null);
|
|
39
|
-
const colWidthsRef = useRef<Record<string, number>>({});
|
|
40
|
-
useEffect(() => { colWidthsRef.current = colWidths; }, [colWidths]);
|
|
41
|
-
const getColWidth = useCallback((col: string) => colWidths[col] ?? COL_WIDTH, [colWidths]);
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (typeof document === 'undefined') return;
|
|
45
|
-
const id = 'rozenite-scrollbar-styles';
|
|
46
|
-
if (document.getElementById(id)) return;
|
|
47
|
-
const el = document.createElement('style');
|
|
48
|
-
el.id = id;
|
|
49
|
-
el.textContent = [
|
|
50
|
-
'::-webkit-scrollbar { width: 7px; height: 7px; }',
|
|
51
|
-
'::-webkit-scrollbar-track { background: transparent; }',
|
|
52
|
-
'::-webkit-scrollbar-thumb { background: #30363d; border-radius: 10px; border: 2px solid transparent; background-clip: content-box; }',
|
|
53
|
-
'::-webkit-scrollbar-thumb:hover { background: #484f58; border: 2px solid transparent; background-clip: content-box; }',
|
|
54
|
-
'::-webkit-scrollbar-corner { background: transparent; }',
|
|
55
|
-
].join('\n');
|
|
56
|
-
document.head.appendChild(el);
|
|
57
|
-
}, []);
|
|
58
|
-
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
setSortCol(null);
|
|
61
|
-
setSortDir('asc');
|
|
62
|
-
setSearch({});
|
|
63
|
-
setFilterOpenCol(null);
|
|
64
|
-
setScrollX(0);
|
|
65
|
-
setPage(1);
|
|
66
|
-
setColWidths({});
|
|
67
|
-
}, [columns]);
|
|
68
|
-
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
setPage(1);
|
|
71
|
-
}, [search, sortCol, sortDir]);
|
|
72
|
-
|
|
73
|
-
const handleSortPress = useCallback((col: string) => {
|
|
74
|
-
setSortCol((prev) => {
|
|
75
|
-
if (prev === col) {
|
|
76
|
-
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
|
77
|
-
return prev;
|
|
78
|
-
}
|
|
79
|
-
setSortDir('asc');
|
|
80
|
-
return col;
|
|
81
|
-
});
|
|
82
|
-
}, []);
|
|
83
|
-
|
|
84
|
-
const handleFilterIconPress = useCallback((col: string) => {
|
|
85
|
-
setFilterOpenCol((prev) => (prev === col ? null : col));
|
|
86
|
-
}, []);
|
|
87
|
-
|
|
88
|
-
const handleSearchChange = useCallback((col: string, value: string) => {
|
|
89
|
-
setSearch((prev) => ({ ...prev, [col]: value }));
|
|
90
|
-
}, []);
|
|
91
|
-
|
|
92
|
-
const handleResizeStart = useCallback((col: string, e: { clientX: number; preventDefault: () => void; stopPropagation: () => void }) => {
|
|
93
|
-
e.preventDefault();
|
|
94
|
-
e.stopPropagation();
|
|
95
|
-
const startW = colWidthsRef.current[col] ?? COL_WIDTH;
|
|
96
|
-
resizeRef.current = { col, startX: e.clientX, startW };
|
|
97
|
-
if (typeof document === 'undefined') return;
|
|
98
|
-
document.body.style.userSelect = 'none';
|
|
99
|
-
const onMove = (ev: MouseEvent) => {
|
|
100
|
-
if (!resizeRef.current) return;
|
|
101
|
-
const delta = ev.clientX - resizeRef.current.startX;
|
|
102
|
-
const newW = Math.max(60, resizeRef.current.startW + delta);
|
|
103
|
-
setColWidths((prev) => ({ ...prev, [resizeRef.current!.col]: newW }));
|
|
104
|
-
};
|
|
105
|
-
const onUp = () => {
|
|
106
|
-
resizeRef.current = null;
|
|
107
|
-
document.body.style.userSelect = '';
|
|
108
|
-
document.removeEventListener('mousemove', onMove);
|
|
109
|
-
document.removeEventListener('mouseup', onUp);
|
|
110
|
-
};
|
|
111
|
-
document.addEventListener('mousemove', onMove);
|
|
112
|
-
document.addEventListener('mouseup', onUp);
|
|
113
|
-
}, []);
|
|
114
|
-
|
|
115
|
-
const processedRows = useMemo(() => {
|
|
116
|
-
let result = rows.map((row, originalIndex) => ({ row, originalIndex }));
|
|
117
|
-
|
|
118
|
-
for (const [col, query] of Object.entries(search)) {
|
|
119
|
-
if (!query.trim()) continue;
|
|
120
|
-
const q = query.toLowerCase();
|
|
121
|
-
result = result.filter(({ row }) => {
|
|
122
|
-
const val = row[col];
|
|
123
|
-
return val !== null && val !== undefined && String(val).toLowerCase().includes(q);
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (sortCol) {
|
|
128
|
-
result.sort((a, b) => {
|
|
129
|
-
const av = a.row[sortCol];
|
|
130
|
-
const bv = b.row[sortCol];
|
|
131
|
-
if (av == null) return 1;
|
|
132
|
-
if (bv == null) return -1;
|
|
133
|
-
const cmp = String(av).localeCompare(String(bv), undefined, { numeric: true });
|
|
134
|
-
return sortDir === 'asc' ? cmp : -cmp;
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return result;
|
|
139
|
-
}, [rows, search, sortCol, sortDir]);
|
|
140
|
-
|
|
141
|
-
const hasActiveSearch = Object.values(search).some((v) => v.trim().length > 0);
|
|
142
|
-
|
|
143
|
-
const pagedRows = useMemo(
|
|
144
|
-
() => processedRows.slice((page - 1) * pageSize, page * pageSize),
|
|
145
|
-
[processedRows, page, pageSize],
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
const filterColIdx = filterOpenCol !== null ? columns.indexOf(filterOpenCol) : -1;
|
|
149
|
-
const popoverLeft = filterColIdx >= 0
|
|
150
|
-
? Math.max(0, columns.slice(0, filterColIdx).reduce((acc, c) => acc + (colWidths[c] ?? COL_WIDTH), IDX_WIDTH) - scrollX)
|
|
151
|
-
: 0;
|
|
152
|
-
|
|
153
|
-
if (status === 'connecting') {
|
|
154
|
-
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>
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (status === 'error') {
|
|
166
|
-
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>
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (status === 'loadingTables' || status === 'loadingData') {
|
|
176
|
-
return (
|
|
177
|
-
<View style={s.placeholder}>
|
|
178
|
-
<ActivityIndicator size="large" color={C.accent} />
|
|
179
|
-
<Text style={s.placeholderText}>Loading…</Text>
|
|
180
|
-
</View>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (columns.length === 0) {
|
|
185
|
-
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
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (rows.length === 0) {
|
|
195
|
-
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
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return (
|
|
205
|
-
<View style={s.outer}>
|
|
206
|
-
<View style={s.headerClip}>
|
|
207
|
-
<View style={[s.headerRow, { transform: [{ translateX: -scrollX }] }]}>
|
|
208
|
-
<View style={[s.cell, { width: IDX_WIDTH }]}>
|
|
209
|
-
<Text style={s.headerText}>#</Text>
|
|
210
|
-
</View>
|
|
211
|
-
{columns.map((col) => {
|
|
212
|
-
const isSortActive = sortCol === col;
|
|
213
|
-
const hasFilter = (search[col] ?? '').trim().length > 0;
|
|
214
|
-
const isFilterOpen = filterOpenCol === col;
|
|
215
|
-
return (
|
|
216
|
-
<View key={col} style={[s.headerCellWrapper, { width: getColWidth(col) }]}>
|
|
217
|
-
<Pressable
|
|
218
|
-
style={[s.sortBtn, isSortActive && s.sortBtnActive]}
|
|
219
|
-
onPress={() => handleSortPress(col)}
|
|
220
|
-
>
|
|
221
|
-
<Text
|
|
222
|
-
style={[s.headerText, isSortActive && s.headerTextActive]}
|
|
223
|
-
numberOfLines={1}
|
|
224
|
-
>
|
|
225
|
-
{col}
|
|
226
|
-
</Text>
|
|
227
|
-
<Text style={[s.sortArrow, isSortActive && s.sortArrowActive]}>
|
|
228
|
-
{isSortActive ? (sortDir === 'asc' ? ' ▴' : ' ▾') : ' ⇅'}
|
|
229
|
-
</Text>
|
|
230
|
-
</Pressable>
|
|
231
|
-
<Pressable
|
|
232
|
-
style={[s.filterBtn, (hasFilter || isFilterOpen) && s.filterBtnActive]}
|
|
233
|
-
onPress={() => handleFilterIconPress(col)}
|
|
234
|
-
>
|
|
235
|
-
<Text style={[s.filterBtnText, (hasFilter || isFilterOpen) && s.filterBtnTextActive]}>
|
|
236
|
-
⌕
|
|
237
|
-
</Text>
|
|
238
|
-
</Pressable>
|
|
239
|
-
<View
|
|
240
|
-
style={s.resizeHandle}
|
|
241
|
-
onMouseDown={(e: any) => handleResizeStart(col, e)}
|
|
242
|
-
/>
|
|
243
|
-
</View>
|
|
244
|
-
);
|
|
245
|
-
})}
|
|
246
|
-
</View>
|
|
247
|
-
</View>
|
|
248
|
-
|
|
249
|
-
<ScrollView
|
|
250
|
-
style={s.vScroll}
|
|
251
|
-
showsVerticalScrollIndicator={true}
|
|
252
|
-
indicatorStyle="white"
|
|
253
|
-
>
|
|
254
|
-
<ScrollView
|
|
255
|
-
horizontal
|
|
256
|
-
showsHorizontalScrollIndicator={true}
|
|
257
|
-
indicatorStyle="white"
|
|
258
|
-
onScroll={(e) => setScrollX(e.nativeEvent.contentOffset.x)}
|
|
259
|
-
scrollEventThrottle={16}
|
|
260
|
-
onScrollBeginDrag={() => setFilterOpenCol(null)}
|
|
261
|
-
>
|
|
262
|
-
<View>
|
|
263
|
-
{processedRows.length === 0 && hasActiveSearch && (
|
|
264
|
-
<View style={s.noResults}>
|
|
265
|
-
<Text style={s.noResultsText}>No rows match the current filters</Text>
|
|
266
|
-
</View>
|
|
267
|
-
)}
|
|
268
|
-
|
|
269
|
-
{pagedRows.map(({ row, originalIndex }, visIdx) => (
|
|
270
|
-
<Pressable
|
|
271
|
-
key={originalIndex}
|
|
272
|
-
style={({ pressed }) => [
|
|
273
|
-
s.row,
|
|
274
|
-
visIdx % 2 === 0 ? s.rowEven : s.rowOdd,
|
|
275
|
-
selectedRowIndex === originalIndex && s.rowSelected,
|
|
276
|
-
pressed && s.rowPressed,
|
|
277
|
-
]}
|
|
278
|
-
onPress={() => onRowSelect(originalIndex)}
|
|
279
|
-
>
|
|
280
|
-
<View style={[s.cell, { width: IDX_WIDTH }]}>
|
|
281
|
-
<Text style={s.idxText}>{originalIndex + 1}</Text>
|
|
282
|
-
</View>
|
|
283
|
-
{columns.map((col) => {
|
|
284
|
-
const val = row[col];
|
|
285
|
-
const isNull = val == null;
|
|
286
|
-
const query = (search[col] ?? '').toLowerCase().trim();
|
|
287
|
-
const display = isNull ? 'NULL' : String(val);
|
|
288
|
-
const isJsonVal = !isNull && isJsonString(display);
|
|
289
|
-
return (
|
|
290
|
-
<View key={col} style={[s.cell, { width: getColWidth(col) }]}>
|
|
291
|
-
{isJsonVal ? (
|
|
292
|
-
<View style={s.jsonCell}>
|
|
293
|
-
<Text style={s.jsonBadge}>{display.trimStart()[0] === '[' ? '[]' : '{}'}</Text>
|
|
294
|
-
<Text style={s.cellText} numberOfLines={1}>{display}</Text>
|
|
295
|
-
</View>
|
|
296
|
-
) : query && !isNull ? (
|
|
297
|
-
<HighlightText text={display} query={query} />
|
|
298
|
-
) : (
|
|
299
|
-
<Text style={[s.cellText, isNull && s.cellNull]} numberOfLines={1}>
|
|
300
|
-
{display}
|
|
301
|
-
</Text>
|
|
302
|
-
)}
|
|
303
|
-
</View>
|
|
304
|
-
);
|
|
305
|
-
})}
|
|
306
|
-
</Pressable>
|
|
307
|
-
))}
|
|
308
|
-
</View>
|
|
309
|
-
</ScrollView>
|
|
310
|
-
</ScrollView>
|
|
311
|
-
|
|
312
|
-
<Pagination
|
|
313
|
-
page={page}
|
|
314
|
-
pageSize={pageSize}
|
|
315
|
-
totalFiltered={processedRows.length}
|
|
316
|
-
totalRows={rows.length}
|
|
317
|
-
columnCount={columns.length}
|
|
318
|
-
onPageChange={setPage}
|
|
319
|
-
onPageSizeChange={(sz) => { setPageSize(sz); setPage(1); }}
|
|
320
|
-
onClearTable={onClearTable}
|
|
321
|
-
/>
|
|
322
|
-
|
|
323
|
-
{filterOpenCol !== null && filterColIdx >= 0 && (
|
|
324
|
-
<View style={[s.filterPopover, { left: popoverLeft, width: getColWidth(filterOpenCol) }]}>
|
|
325
|
-
<TextInput
|
|
326
|
-
autoFocus
|
|
327
|
-
style={s.filterInput}
|
|
328
|
-
value={search[filterOpenCol] ?? ''}
|
|
329
|
-
onChangeText={(v) => handleSearchChange(filterOpenCol, v)}
|
|
330
|
-
placeholder={`Filter ${filterOpenCol}…`}
|
|
331
|
-
placeholderTextColor={C.textMuted}
|
|
332
|
-
/>
|
|
333
|
-
{(search[filterOpenCol] ?? '').length > 0 && (
|
|
334
|
-
<Pressable
|
|
335
|
-
onPress={() => handleSearchChange(filterOpenCol, '')}
|
|
336
|
-
style={s.filterClearBtn}
|
|
337
|
-
>
|
|
338
|
-
<Text style={s.filterClearText}>✕</Text>
|
|
339
|
-
</Pressable>
|
|
340
|
-
)}
|
|
341
|
-
</View>
|
|
342
|
-
)}
|
|
343
|
-
</View>
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function isJsonString(val: string): boolean {
|
|
348
|
-
const t = val.trim();
|
|
349
|
-
if (t.length < 2 || (t[0] !== '{' && t[0] !== '[')) return false;
|
|
350
|
-
try { JSON.parse(t); return true; } catch { return false; }
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function HighlightText({ text, query }: { text: string; query: string }) {
|
|
354
|
-
const lower = text.toLowerCase();
|
|
355
|
-
const idx = lower.indexOf(query);
|
|
356
|
-
if (idx === -1) {
|
|
357
|
-
return <Text style={s.cellText} numberOfLines={1}>{text}</Text>;
|
|
358
|
-
}
|
|
359
|
-
return (
|
|
360
|
-
<Text style={s.cellText} numberOfLines={1}>
|
|
361
|
-
{text.slice(0, idx)}
|
|
362
|
-
<Text style={s.cellHighlight}>{text.slice(idx, idx + query.length)}</Text>
|
|
363
|
-
{text.slice(idx + query.length)}
|
|
364
|
-
</Text>
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const s = StyleSheet.create({
|
|
369
|
-
outer: { flex: 1, position: 'relative', flexDirection: 'column' },
|
|
370
|
-
headerClip: {
|
|
371
|
-
overflow: 'hidden',
|
|
372
|
-
height: HEADER_HEIGHT,
|
|
373
|
-
backgroundColor: C.surface2,
|
|
374
|
-
borderBottomWidth: 1,
|
|
375
|
-
borderBottomColor: C.border,
|
|
376
|
-
},
|
|
377
|
-
headerRow: {
|
|
378
|
-
flexDirection: 'row',
|
|
379
|
-
alignItems: 'center',
|
|
380
|
-
backgroundColor: C.surface2,
|
|
381
|
-
height: HEADER_HEIGHT,
|
|
382
|
-
},
|
|
383
|
-
vScroll: { flex: 1, scrollbarWidth: 'thin', scrollbarColor: '#30363d transparent' },
|
|
384
|
-
headerCellWrapper: {
|
|
385
|
-
position: 'relative',
|
|
386
|
-
flexDirection: 'row',
|
|
387
|
-
alignItems: 'center',
|
|
388
|
-
height: HEADER_HEIGHT,
|
|
389
|
-
borderRightWidth: 1,
|
|
390
|
-
borderRightColor: C.borderSubtle,
|
|
391
|
-
},
|
|
392
|
-
sortBtn: {
|
|
393
|
-
flex: 1,
|
|
394
|
-
flexDirection: 'row',
|
|
395
|
-
alignItems: 'center',
|
|
396
|
-
paddingLeft: 12,
|
|
397
|
-
paddingRight: 4,
|
|
398
|
-
height: HEADER_HEIGHT,
|
|
399
|
-
cursor: 'pointer',
|
|
400
|
-
},
|
|
401
|
-
sortBtnActive: {},
|
|
402
|
-
headerText: {
|
|
403
|
-
fontSize: 11,
|
|
404
|
-
fontWeight: '700',
|
|
405
|
-
color: C.textSecondary,
|
|
406
|
-
textTransform: 'uppercase',
|
|
407
|
-
letterSpacing: 0.6,
|
|
408
|
-
flex: 1,
|
|
409
|
-
},
|
|
410
|
-
headerTextActive: { color: C.accent },
|
|
411
|
-
sortArrow: { fontSize: 14, color: C.textMuted },
|
|
412
|
-
sortArrowActive: { color: C.accent },
|
|
413
|
-
cell: { paddingHorizontal: 12, justifyContent: 'center' },
|
|
414
|
-
filterBtn: {
|
|
415
|
-
width: 26,
|
|
416
|
-
height: 26,
|
|
417
|
-
borderRadius: 5,
|
|
418
|
-
alignItems: 'center',
|
|
419
|
-
justifyContent: 'center',
|
|
420
|
-
marginRight: 6,
|
|
421
|
-
cursor: 'pointer',
|
|
422
|
-
},
|
|
423
|
-
filterBtnActive: { backgroundColor: C.accentSubtle },
|
|
424
|
-
filterBtnText: { fontSize: 24, color: C.textMuted },
|
|
425
|
-
filterBtnTextActive: { color: C.accent },
|
|
426
|
-
filterPopover: {
|
|
427
|
-
position: 'absolute',
|
|
428
|
-
top: HEADER_HEIGHT + 1,
|
|
429
|
-
width: COL_WIDTH,
|
|
430
|
-
flexDirection: 'row',
|
|
431
|
-
alignItems: 'center',
|
|
432
|
-
backgroundColor: C.surface2,
|
|
433
|
-
borderWidth: 1,
|
|
434
|
-
borderColor: C.accent,
|
|
435
|
-
borderRadius: 7,
|
|
436
|
-
paddingHorizontal: 10,
|
|
437
|
-
paddingVertical: 7,
|
|
438
|
-
zIndex: 100,
|
|
439
|
-
shadowColor: '#000',
|
|
440
|
-
shadowOffset: { width: 0, height: 6 },
|
|
441
|
-
shadowOpacity: 0.5,
|
|
442
|
-
shadowRadius: 16,
|
|
443
|
-
},
|
|
444
|
-
filterInput: {
|
|
445
|
-
flex: 1,
|
|
446
|
-
fontSize: 13,
|
|
447
|
-
color: C.text,
|
|
448
|
-
paddingVertical: 0,
|
|
449
|
-
outlineStyle: 'none' as any,
|
|
450
|
-
},
|
|
451
|
-
filterClearBtn: {
|
|
452
|
-
width: 18,
|
|
453
|
-
height: 18,
|
|
454
|
-
borderRadius: 9,
|
|
455
|
-
backgroundColor: C.surface,
|
|
456
|
-
alignItems: 'center',
|
|
457
|
-
justifyContent: 'center',
|
|
458
|
-
marginLeft: 6,
|
|
459
|
-
},
|
|
460
|
-
filterClearText: { fontSize: 9, color: C.textSecondary, lineHeight: 9 },
|
|
461
|
-
row: {
|
|
462
|
-
flexDirection: 'row',
|
|
463
|
-
alignItems: 'center',
|
|
464
|
-
paddingVertical: 11,
|
|
465
|
-
borderBottomWidth: 1,
|
|
466
|
-
borderBottomColor: C.borderSubtle,
|
|
467
|
-
cursor: 'pointer',
|
|
468
|
-
},
|
|
469
|
-
rowEven: { backgroundColor: C.bg },
|
|
470
|
-
rowOdd: { backgroundColor: C.surface },
|
|
471
|
-
rowSelected: { backgroundColor: C.rowSelected },
|
|
472
|
-
rowPressed: { opacity: 0.7 },
|
|
473
|
-
cellText: { fontSize: 13, color: C.text },
|
|
474
|
-
cellNull: { color: C.textMuted, fontStyle: 'italic' },
|
|
475
|
-
cellHighlight: { backgroundColor: '#3d3000', color: '#f5c542', fontWeight: '600' },
|
|
476
|
-
idxText: { fontSize: 11, color: C.textMuted, fontWeight: '600', textAlign: 'center' },
|
|
477
|
-
noResults: { paddingVertical: 32, alignItems: 'center' },
|
|
478
|
-
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
|
-
resizeHandle: {
|
|
485
|
-
position: 'absolute',
|
|
486
|
-
right: 0,
|
|
487
|
-
top: 0,
|
|
488
|
-
width: 6,
|
|
489
|
-
height: HEADER_HEIGHT,
|
|
490
|
-
cursor: 'col-resize' as any,
|
|
491
|
-
zIndex: 10,
|
|
492
|
-
},
|
|
493
|
-
jsonCell: { flexDirection: 'row', alignItems: 'center', gap: 5, overflow: 'hidden' },
|
|
494
|
-
jsonBadge: {
|
|
495
|
-
fontSize: 9,
|
|
496
|
-
fontWeight: '700',
|
|
497
|
-
color: '#a78bfa',
|
|
498
|
-
backgroundColor: 'rgba(167,139,250,0.12)',
|
|
499
|
-
paddingHorizontal: 4,
|
|
500
|
-
paddingVertical: 2,
|
|
501
|
-
borderRadius: 3,
|
|
502
|
-
flexShrink: 0,
|
|
503
|
-
},
|
|
504
|
-
});
|