rozenite-sqlite 0.0.9 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
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-BKWy6irM.js"></script>
26
+ </head>
27
+ <body>
28
+ <div id="root"></div>
29
+ </body>
30
+ </html>
@@ -0,0 +1 @@
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;
@@ -0,0 +1,28 @@
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 SQLighter 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 { }
@@ -0,0 +1,77 @@
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
+ 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
+ SAVE_ROW: "save-row",
9
+ DELETE_ROW: "delete-row",
10
+ CLEAR_TABLE: "clear-table",
11
+ MUTATION_RESULT: "mutation-result"
12
+ };
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(() => {
21
+ if (!e) return;
22
+ const _ = [
23
+ e.onMessage(s.GET_DB_LIST, () => {
24
+ e.send(s.SEND_DB_LIST, T.current.databases);
25
+ }),
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);
31
+ },
32
+ (t) => {
33
+ e.send(s.SQL_EXEC_RESULT, {
34
+ error: t instanceof Error ? t.message : String(t)
35
+ });
36
+ }
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
+ );
68
+ })
69
+ ];
70
+ return () => {
71
+ _.forEach((r) => r.remove());
72
+ };
73
+ }, [e]);
74
+ }
75
+ export {
76
+ N as useRozeniteSQLite
77
+ };
@@ -0,0 +1 @@
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.9",
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",
@@ -0,0 +1,2 @@
1
+ export { useRozeniteSQLite } from './src/hooks/useRozeniteSQLite';
2
+ export type { RozeniteSQLiteConfig, SQLExecutor } from './src/hooks/useRozeniteSQLite';
@@ -0,0 +1,8 @@
1
+ export default {
2
+ panels: [
3
+ {
4
+ name: 'SQLighter',
5
+ source: './src/panel.tsx',
6
+ },
7
+ ],
8
+ };
@@ -0,0 +1,501 @@
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
+ } from 'react-native';
10
+ import { C, type RowData } from '../theme';
11
+ import Pagination from './Pagination';
12
+ import { Placeholder } from './Placeholder';
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
+ onReconnect?: () => void;
24
+ }
25
+
26
+ const COL_WIDTH = 160;
27
+ const IDX_WIDTH = 44;
28
+ const HEADER_HEIGHT = 42;
29
+
30
+ export function DataTable({ columns, rows, selectedRowIndex, onRowSelect, status, error, onClearTable, onReconnect }: DataTableProps) {
31
+ const [sortCol, setSortCol] = useState<string | null>(null);
32
+ const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
33
+ const [search, setSearch] = useState<Record<string, string>>({});
34
+ const [filterOpenCol, setFilterOpenCol] = useState<string | null>(null);
35
+ const [scrollX, setScrollX] = useState(0);
36
+ const [page, setPage] = useState(1);
37
+ const [pageSize, setPageSize] = useState(25);
38
+ const [colWidths, setColWidths] = useState<Record<string, number>>({});
39
+ const resizeRef = useRef<{ col: string; startX: number; startW: number } | null>(null);
40
+ const colWidthsRef = useRef<Record<string, number>>({});
41
+ useEffect(() => { colWidthsRef.current = colWidths; }, [colWidths]);
42
+ const getColWidth = useCallback((col: string) => colWidths[col] ?? COL_WIDTH, [colWidths]);
43
+
44
+ useEffect(() => {
45
+ if (typeof document === 'undefined') return;
46
+ const id = 'rozenite-scrollbar-styles';
47
+ if (document.getElementById(id)) return;
48
+ const el = document.createElement('style');
49
+ el.id = id;
50
+ el.textContent = [
51
+ '::-webkit-scrollbar { width: 7px; height: 7px; }',
52
+ '::-webkit-scrollbar-track { background: transparent; }',
53
+ '::-webkit-scrollbar-thumb { background: #30363d; border-radius: 10px; border: 2px solid transparent; background-clip: content-box; }',
54
+ '::-webkit-scrollbar-thumb:hover { background: #484f58; border: 2px solid transparent; background-clip: content-box; }',
55
+ '::-webkit-scrollbar-corner { background: transparent; }',
56
+ ].join('\n');
57
+ document.head.appendChild(el);
58
+ }, []);
59
+
60
+ useEffect(() => {
61
+ setSortCol(null);
62
+ setSortDir('asc');
63
+ setSearch({});
64
+ setFilterOpenCol(null);
65
+ setScrollX(0);
66
+ setPage(1);
67
+ setColWidths({});
68
+ }, [columns]);
69
+
70
+ useEffect(() => {
71
+ setPage(1);
72
+ }, [search, sortCol, sortDir]);
73
+
74
+ const handleSortPress = useCallback((col: string) => {
75
+ setSortCol((prev) => {
76
+ if (prev === col) {
77
+ setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
78
+ return prev;
79
+ }
80
+ setSortDir('asc');
81
+ return col;
82
+ });
83
+ }, []);
84
+
85
+ const handleFilterIconPress = useCallback((col: string) => {
86
+ setFilterOpenCol((prev) => (prev === col ? null : col));
87
+ }, []);
88
+
89
+ const handleSearchChange = useCallback((col: string, value: string) => {
90
+ setSearch((prev) => ({ ...prev, [col]: value }));
91
+ }, []);
92
+
93
+ const handleResizeStart = useCallback((col: string, e: { clientX: number; preventDefault: () => void; stopPropagation: () => void }) => {
94
+ e.preventDefault();
95
+ e.stopPropagation();
96
+ const startW = colWidthsRef.current[col] ?? COL_WIDTH;
97
+ resizeRef.current = { col, startX: e.clientX, startW };
98
+ if (typeof document === 'undefined') return;
99
+ document.body.style.userSelect = 'none';
100
+ const onMove = (ev: MouseEvent) => {
101
+ if (!resizeRef.current) return;
102
+ const delta = ev.clientX - resizeRef.current.startX;
103
+ const newW = Math.max(60, resizeRef.current.startW + delta);
104
+ setColWidths((prev) => ({ ...prev, [resizeRef.current!.col]: newW }));
105
+ };
106
+ const onUp = () => {
107
+ resizeRef.current = null;
108
+ document.body.style.userSelect = '';
109
+ document.removeEventListener('mousemove', onMove);
110
+ document.removeEventListener('mouseup', onUp);
111
+ };
112
+ document.addEventListener('mousemove', onMove);
113
+ document.addEventListener('mouseup', onUp);
114
+ }, []);
115
+
116
+ const processedRows = useMemo(() => {
117
+ let result = rows.map((row, originalIndex) => ({ row, originalIndex }));
118
+
119
+ for (const [col, query] of Object.entries(search)) {
120
+ if (!query.trim()) continue;
121
+ const q = query.toLowerCase();
122
+ result = result.filter(({ row }) => {
123
+ const val = row[col];
124
+ return val !== null && val !== undefined && String(val).toLowerCase().includes(q);
125
+ });
126
+ }
127
+
128
+ if (sortCol) {
129
+ result.sort((a, b) => {
130
+ const av = a.row[sortCol];
131
+ const bv = b.row[sortCol];
132
+ if (av == null) return 1;
133
+ if (bv == null) return -1;
134
+ const cmp = String(av).localeCompare(String(bv), undefined, { numeric: true });
135
+ return sortDir === 'asc' ? cmp : -cmp;
136
+ });
137
+ }
138
+
139
+ return result;
140
+ }, [rows, search, sortCol, sortDir]);
141
+
142
+ const hasActiveSearch = Object.values(search).some((v) => v.trim().length > 0);
143
+
144
+ const pagedRows = useMemo(
145
+ () => processedRows.slice((page - 1) * pageSize, page * pageSize),
146
+ [processedRows, page, pageSize],
147
+ );
148
+
149
+ const filterColIdx = filterOpenCol !== null ? columns.indexOf(filterOpenCol) : -1;
150
+ const popoverLeft = filterColIdx >= 0
151
+ ? Math.max(0, columns.slice(0, filterColIdx).reduce((acc, c) => acc + (colWidths[c] ?? COL_WIDTH), IDX_WIDTH) - scrollX)
152
+ : 0;
153
+
154
+ if (status === 'connecting') {
155
+ return (
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
+ />
163
+ );
164
+ }
165
+
166
+ if (status === 'error') {
167
+ return (
168
+ <Placeholder
169
+ emoji="⚠️"
170
+ title="Query failed"
171
+ subtitle={error ?? undefined}
172
+ buttonText={onReconnect ? "↻ Try again" : undefined}
173
+ onButtonPress={onReconnect}
174
+ />
175
+ );
176
+ }
177
+
178
+ if (status === 'loadingTables' || status === 'loadingData') {
179
+ return (
180
+ <Placeholder
181
+ loading
182
+ title="Loading…"
183
+ buttonText={onReconnect ? "↻ Refresh" : undefined}
184
+ onButtonPress={onReconnect}
185
+ />
186
+ );
187
+ }
188
+
189
+ if (columns.length === 0) {
190
+ return (
191
+ <Placeholder
192
+ emoji="🗄️"
193
+ title="No data to display"
194
+ subtitle="Select a database and table above"
195
+ />
196
+ );
197
+ }
198
+
199
+ if (rows.length === 0) {
200
+ return (
201
+ <Placeholder
202
+ emoji="📭"
203
+ title="Table is empty"
204
+ subtitle="This table has no rows yet"
205
+ />
206
+ );
207
+ }
208
+
209
+ return (
210
+ <View style={s.outer}>
211
+ <View style={s.headerClip}>
212
+ <View style={[s.headerRow, { transform: [{ translateX: -scrollX }] }]}>
213
+ <View style={[s.cell, { width: IDX_WIDTH }]}>
214
+ <Text style={s.headerText}>#</Text>
215
+ </View>
216
+ {columns.map((col) => {
217
+ const isSortActive = sortCol === col;
218
+ const hasFilter = (search[col] ?? '').trim().length > 0;
219
+ const isFilterOpen = filterOpenCol === col;
220
+ return (
221
+ <View key={col} style={[s.headerCellWrapper, { width: getColWidth(col) }]}>
222
+ <Pressable
223
+ style={[s.sortBtn, isSortActive && s.sortBtnActive]}
224
+ onPress={() => handleSortPress(col)}
225
+ >
226
+ <Text
227
+ style={[s.headerText, isSortActive && s.headerTextActive]}
228
+ numberOfLines={1}
229
+ >
230
+ {col}
231
+ </Text>
232
+ <Text style={[s.sortArrow, isSortActive && s.sortArrowActive]}>
233
+ {isSortActive ? (sortDir === 'asc' ? ' ▴' : ' ▾') : ' ⇅'}
234
+ </Text>
235
+ </Pressable>
236
+ <Pressable
237
+ style={[s.filterBtn, (hasFilter || isFilterOpen) && s.filterBtnActive]}
238
+ onPress={() => handleFilterIconPress(col)}
239
+ >
240
+ <Text style={[s.filterBtnText, (hasFilter || isFilterOpen) && s.filterBtnTextActive]}>
241
+
242
+ </Text>
243
+ </Pressable>
244
+ <View
245
+ style={s.resizeHandle}
246
+ onMouseDown={(e: any) => handleResizeStart(col, e)}
247
+ />
248
+ </View>
249
+ );
250
+ })}
251
+ </View>
252
+ </View>
253
+
254
+ <ScrollView
255
+ style={s.vScroll}
256
+ showsVerticalScrollIndicator={true}
257
+ indicatorStyle="white"
258
+ >
259
+ <ScrollView
260
+ horizontal
261
+ showsHorizontalScrollIndicator={true}
262
+ indicatorStyle="white"
263
+ onScroll={(e) => setScrollX(e.nativeEvent.contentOffset.x)}
264
+ scrollEventThrottle={16}
265
+ onScrollBeginDrag={() => setFilterOpenCol(null)}
266
+ >
267
+ <View>
268
+ {processedRows.length === 0 && hasActiveSearch && (
269
+ <View style={s.noResults}>
270
+ <Text style={s.noResultsText}>No rows match the current filters</Text>
271
+ </View>
272
+ )}
273
+
274
+ {pagedRows.map(({ row, originalIndex }, visIdx) => (
275
+ <Pressable
276
+ key={originalIndex}
277
+ style={({ pressed }) => [
278
+ s.row,
279
+ visIdx % 2 === 0 ? s.rowEven : s.rowOdd,
280
+ selectedRowIndex === originalIndex && s.rowSelected,
281
+ pressed && s.rowPressed,
282
+ ]}
283
+ onPress={() => onRowSelect(originalIndex)}
284
+ >
285
+ <View style={[s.cell, { width: IDX_WIDTH }]}>
286
+ <Text style={s.idxText}>{originalIndex + 1}</Text>
287
+ </View>
288
+ {columns.map((col) => {
289
+ const val = row[col];
290
+ const isNull = val == null;
291
+ const query = (search[col] ?? '').toLowerCase().trim();
292
+ const display = isNull ? 'NULL' : String(val);
293
+ const isJsonVal = !isNull && isJsonString(display);
294
+ return (
295
+ <View key={col} style={[s.cell, { width: getColWidth(col) }]}>
296
+ {isJsonVal ? (
297
+ <View style={s.jsonCell}>
298
+ <Text style={s.jsonBadge}>{display.trimStart()[0] === '[' ? '[]' : '{}'}</Text>
299
+ <Text style={s.cellText} numberOfLines={1}>{display}</Text>
300
+ </View>
301
+ ) : query && !isNull ? (
302
+ <HighlightText text={display} query={query} />
303
+ ) : (
304
+ <Text style={[s.cellText, isNull && s.cellNull]} numberOfLines={1}>
305
+ {display}
306
+ </Text>
307
+ )}
308
+ </View>
309
+ );
310
+ })}
311
+ </Pressable>
312
+ ))}
313
+ </View>
314
+ </ScrollView>
315
+ </ScrollView>
316
+
317
+ <Pagination
318
+ page={page}
319
+ pageSize={pageSize}
320
+ totalFiltered={processedRows.length}
321
+ totalRows={rows.length}
322
+ columnCount={columns.length}
323
+ onPageChange={setPage}
324
+ onPageSizeChange={(sz) => { setPageSize(sz); setPage(1); }}
325
+ onClearTable={onClearTable}
326
+ />
327
+
328
+ {filterOpenCol !== null && filterColIdx >= 0 && (
329
+ <View style={[s.filterPopover, { left: popoverLeft, width: getColWidth(filterOpenCol) }]}>
330
+ <TextInput
331
+ autoFocus
332
+ style={s.filterInput}
333
+ value={search[filterOpenCol] ?? ''}
334
+ onChangeText={(v) => handleSearchChange(filterOpenCol, v)}
335
+ placeholder={`Filter ${filterOpenCol}…`}
336
+ placeholderTextColor={C.textMuted}
337
+ />
338
+ {(search[filterOpenCol] ?? '').length > 0 && (
339
+ <Pressable
340
+ onPress={() => handleSearchChange(filterOpenCol, '')}
341
+ style={s.filterClearBtn}
342
+ >
343
+ <Text style={s.filterClearText}>✕</Text>
344
+ </Pressable>
345
+ )}
346
+ </View>
347
+ )}
348
+ </View>
349
+ );
350
+ }
351
+
352
+ function isJsonString(val: string): boolean {
353
+ const t = val.trim();
354
+ if (t.length < 2 || (t[0] !== '{' && t[0] !== '[')) return false;
355
+ try { JSON.parse(t); return true; } catch { return false; }
356
+ }
357
+
358
+ function HighlightText({ text, query }: { text: string; query: string }) {
359
+ const lower = text.toLowerCase();
360
+ const idx = lower.indexOf(query);
361
+ if (idx === -1) {
362
+ return <Text style={s.cellText} numberOfLines={1}>{text}</Text>;
363
+ }
364
+ return (
365
+ <Text style={s.cellText} numberOfLines={1}>
366
+ {text.slice(0, idx)}
367
+ <Text style={s.cellHighlight}>{text.slice(idx, idx + query.length)}</Text>
368
+ {text.slice(idx + query.length)}
369
+ </Text>
370
+ );
371
+ }
372
+
373
+ const s = StyleSheet.create({
374
+ outer: { flex: 1, position: 'relative', flexDirection: 'column' },
375
+ headerClip: {
376
+ overflow: 'hidden',
377
+ height: HEADER_HEIGHT,
378
+ backgroundColor: C.surface2,
379
+ borderBottomWidth: 1,
380
+ borderBottomColor: C.border,
381
+ },
382
+ headerRow: {
383
+ flexDirection: 'row',
384
+ alignItems: 'center',
385
+ backgroundColor: C.surface2,
386
+ height: HEADER_HEIGHT,
387
+ },
388
+ vScroll: { flex: 1, scrollbarWidth: 'thin', scrollbarColor: '#30363d transparent' },
389
+ headerCellWrapper: {
390
+ position: 'relative',
391
+ flexDirection: 'row',
392
+ alignItems: 'center',
393
+ height: HEADER_HEIGHT,
394
+ borderRightWidth: 1,
395
+ borderRightColor: C.borderSubtle,
396
+ },
397
+ sortBtn: {
398
+ flex: 1,
399
+ flexDirection: 'row',
400
+ alignItems: 'center',
401
+ paddingLeft: 12,
402
+ paddingRight: 4,
403
+ height: HEADER_HEIGHT,
404
+ cursor: 'pointer',
405
+ },
406
+ sortBtnActive: {},
407
+ headerText: {
408
+ fontSize: 11,
409
+ fontWeight: '700',
410
+ color: C.textSecondary,
411
+ textTransform: 'uppercase',
412
+ letterSpacing: 0.6,
413
+ flex: 1,
414
+ },
415
+ headerTextActive: { color: C.accent },
416
+ sortArrow: { fontSize: 14, color: C.textMuted },
417
+ sortArrowActive: { color: C.accent },
418
+ cell: { paddingHorizontal: 12, justifyContent: 'center' },
419
+ filterBtn: {
420
+ width: 26,
421
+ height: 26,
422
+ borderRadius: 5,
423
+ alignItems: 'center',
424
+ justifyContent: 'center',
425
+ marginRight: 6,
426
+ cursor: 'pointer',
427
+ },
428
+ filterBtnActive: { backgroundColor: C.accentSubtle },
429
+ filterBtnText: { fontSize: 24, color: C.textMuted },
430
+ filterBtnTextActive: { color: C.accent },
431
+ filterPopover: {
432
+ position: 'absolute',
433
+ top: HEADER_HEIGHT + 1,
434
+ width: COL_WIDTH,
435
+ flexDirection: 'row',
436
+ alignItems: 'center',
437
+ backgroundColor: C.surface2,
438
+ borderWidth: 1,
439
+ borderColor: C.accent,
440
+ borderRadius: 7,
441
+ paddingHorizontal: 10,
442
+ paddingVertical: 7,
443
+ zIndex: 100,
444
+ boxShadow: '0 6px 16px rgba(0,0,0,0.5)',
445
+ },
446
+ filterInput: {
447
+ flex: 1,
448
+ fontSize: 13,
449
+ color: C.text,
450
+ paddingVertical: 0,
451
+ outlineStyle: 'none' as any,
452
+ },
453
+ filterClearBtn: {
454
+ width: 18,
455
+ height: 18,
456
+ borderRadius: 9,
457
+ backgroundColor: C.surface,
458
+ alignItems: 'center',
459
+ justifyContent: 'center',
460
+ marginLeft: 6,
461
+ },
462
+ filterClearText: { fontSize: 9, color: C.textSecondary, lineHeight: 9 },
463
+ row: {
464
+ flexDirection: 'row',
465
+ alignItems: 'center',
466
+ paddingVertical: 11,
467
+ borderBottomWidth: 1,
468
+ borderBottomColor: C.borderSubtle,
469
+ cursor: 'pointer',
470
+ },
471
+ rowEven: { backgroundColor: C.bg },
472
+ rowOdd: { backgroundColor: C.surface },
473
+ rowSelected: { backgroundColor: C.rowSelected },
474
+ rowPressed: { opacity: 0.7 },
475
+ cellText: { fontSize: 13, color: C.text },
476
+ cellNull: { color: C.textMuted, fontStyle: 'italic' },
477
+ cellHighlight: { backgroundColor: '#3d3000', color: '#f5c542', fontWeight: '600' },
478
+ idxText: { fontSize: 11, color: C.textMuted, fontWeight: '600', textAlign: 'center' },
479
+ noResults: { paddingVertical: 32, alignItems: 'center' },
480
+ noResultsText: { fontSize: 13, color: C.textMuted, fontStyle: 'italic' },
481
+ resizeHandle: {
482
+ position: 'absolute',
483
+ right: 0,
484
+ top: 0,
485
+ width: 6,
486
+ height: HEADER_HEIGHT,
487
+ cursor: 'col-resize' as any,
488
+ zIndex: 10,
489
+ },
490
+ jsonCell: { flexDirection: 'row', alignItems: 'center', gap: 5, overflow: 'hidden' },
491
+ jsonBadge: {
492
+ fontSize: 9,
493
+ fontWeight: '700',
494
+ color: '#a78bfa',
495
+ backgroundColor: 'rgba(167,139,250,0.12)',
496
+ paddingHorizontal: 4,
497
+ paddingVertical: 2,
498
+ borderRadius: 3,
499
+ flexShrink: 0,
500
+ },
501
+ });