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,136 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ ScrollView,
7
+ Pressable,
8
+ ActivityIndicator,
9
+ } from 'react-native';
10
+ import { C } from '../theme';
11
+
12
+ export interface DropdownProps {
13
+ label: string;
14
+ value: string | null;
15
+ options: string[];
16
+ onSelect: (value: string) => void;
17
+ formatLabel?: (v: string) => string;
18
+ loading?: boolean;
19
+ disabled?: boolean;
20
+ zIndex?: number;
21
+ }
22
+
23
+ export function Dropdown({
24
+ label,
25
+ value,
26
+ options,
27
+ onSelect,
28
+ formatLabel,
29
+ loading,
30
+ disabled,
31
+ zIndex = 10,
32
+ }: DropdownProps) {
33
+ const [open, setOpen] = useState(false);
34
+ const fmt = formatLabel ?? ((v: string) => v);
35
+
36
+ return (
37
+ <View style={[s.root, { zIndex }]}>
38
+ <Text style={s.label}>{label}</Text>
39
+ <Pressable
40
+ style={[s.trigger, open && s.triggerOpen, disabled && s.triggerDisabled]}
41
+ onPress={() => !disabled && setOpen((o) => !o)}
42
+ >
43
+ {loading ? <ActivityIndicator size="small" color={C.textSecondary} style={s.spinner} /> : null}
44
+ <Text style={[s.triggerText, !value && s.triggerPlaceholder]} numberOfLines={1}>
45
+ {value ? fmt(value) : '— select —'}
46
+ </Text>
47
+ <Text style={s.chevron}>{open ? '▴' : '▾'}</Text>
48
+ </Pressable>
49
+
50
+ {open && (
51
+ <View style={[s.menu, { zIndex: zIndex + 10 }]}>
52
+ <ScrollView style={s.menuScroll} nestedScrollEnabled>
53
+ {options.length === 0 ? (
54
+ <Text style={s.menuEmpty}>No options</Text>
55
+ ) : (
56
+ options.map((opt) => (
57
+ <Pressable
58
+ key={opt}
59
+ style={({ pressed }) => [
60
+ s.menuItem,
61
+ value === opt && s.menuItemSelected,
62
+ pressed && s.menuItemPressed,
63
+ ]}
64
+ onPress={() => {
65
+ onSelect(opt);
66
+ setOpen(false);
67
+ }}
68
+ >
69
+ <Text style={[s.menuItemText, value === opt && s.menuItemTextSelected]}>
70
+ {fmt(opt)}
71
+ </Text>
72
+ {value === opt && <Text style={s.checkmark}>✓</Text>}
73
+ </Pressable>
74
+ ))
75
+ )}
76
+ </ScrollView>
77
+ </View>
78
+ )}
79
+ </View>
80
+ );
81
+ }
82
+
83
+ const s = StyleSheet.create({
84
+ root: { position: 'relative', minWidth: 180 },
85
+ label: {
86
+ fontSize: 10,
87
+ fontWeight: '600',
88
+ color: C.textMuted,
89
+ textTransform: 'uppercase',
90
+ letterSpacing: 0.8,
91
+ marginBottom: 5,
92
+ },
93
+ trigger: {
94
+ flexDirection: 'row',
95
+ alignItems: 'center',
96
+ paddingHorizontal: 12,
97
+ paddingVertical: 8,
98
+ borderRadius: 7,
99
+ backgroundColor: C.surface2,
100
+ borderWidth: 1,
101
+ borderColor: C.border,
102
+ minWidth: 180,
103
+ },
104
+ triggerOpen: { borderColor: C.accent },
105
+ triggerDisabled: { opacity: 0.35 },
106
+ spinner: { marginRight: 6 },
107
+ triggerText: { fontSize: 13, color: C.text, fontWeight: '500', flex: 1 },
108
+ triggerPlaceholder: { color: C.textMuted },
109
+ chevron: { fontSize: 11, color: C.textSecondary, marginLeft: 8 },
110
+ menu: {
111
+ position: 'absolute',
112
+ top: 62,
113
+ left: 0,
114
+ right: 0,
115
+ backgroundColor: C.surface2,
116
+ borderWidth: 1,
117
+ borderColor: C.border,
118
+ borderRadius: 8,
119
+ overflow: 'hidden',
120
+ boxShadow: '0 8px 20px rgba(0,0,0,0.5)',
121
+ },
122
+ menuScroll: { maxHeight: 200 },
123
+ menuEmpty: { padding: 14, fontSize: 12, color: C.textMuted, textAlign: 'center' },
124
+ menuItem: {
125
+ flexDirection: 'row',
126
+ alignItems: 'center',
127
+ justifyContent: 'space-between',
128
+ paddingHorizontal: 12,
129
+ paddingVertical: 10,
130
+ },
131
+ menuItemSelected: { backgroundColor: C.accentSubtle },
132
+ menuItemPressed: { backgroundColor: C.surface },
133
+ menuItemText: { fontSize: 13, color: C.text },
134
+ menuItemTextSelected: { color: C.accent, fontWeight: '600' },
135
+ checkmark: { fontSize: 12, color: C.accent },
136
+ });
@@ -0,0 +1,266 @@
1
+ import React, { useState } from 'react';
2
+ import { View, Text, Pressable, StyleSheet } from 'react-native';
3
+ import { C } from '../theme';
4
+
5
+ interface PaginationProps {
6
+ page: number;
7
+ pageSize: number;
8
+ totalFiltered: number;
9
+ totalRows: number;
10
+ columnCount: number;
11
+ onPageChange: (page: number) => void;
12
+ onPageSizeChange: (size: number) => void;
13
+ onClearTable?: () => void;
14
+ }
15
+
16
+ const PAGE_SIZES = [10, 25, 50, 100];
17
+ const MAX_PAGE_BUTTONS = 5;
18
+
19
+ export default function Pagination({
20
+ page,
21
+ pageSize,
22
+ totalFiltered,
23
+ totalRows,
24
+ columnCount,
25
+ onPageChange,
26
+ onPageSizeChange,
27
+ onClearTable,
28
+ }: PaginationProps) {
29
+ const [confirmClear, setConfirmClear] = useState(false);
30
+ const totalPages = Math.max(1, Math.ceil(totalFiltered / pageSize));
31
+ const from = totalFiltered === 0 ? 0 : (page - 1) * pageSize + 1;
32
+ const to = Math.min(page * pageSize, totalFiltered);
33
+
34
+ const half = Math.floor(MAX_PAGE_BUTTONS / 2);
35
+ let startPage = Math.max(1, page - half);
36
+ let endPage = startPage + MAX_PAGE_BUTTONS - 1;
37
+ if (endPage > totalPages) {
38
+ endPage = totalPages;
39
+ startPage = Math.max(1, endPage - MAX_PAGE_BUTTONS + 1);
40
+ }
41
+ const pageButtons: number[] = [];
42
+ for (let i = startPage; i <= endPage; i++) pageButtons.push(i);
43
+
44
+ const filterActive = totalFiltered !== totalRows;
45
+
46
+ return (
47
+ <View style={s.bar}>
48
+ <View style={s.left}>
49
+ {onClearTable && !confirmClear && (
50
+ <Pressable style={s.clearBtn} onPress={() => setConfirmClear(true)}>
51
+ <Text style={s.clearBtnText}>⊘ Clear table</Text>
52
+ </Pressable>
53
+ )}
54
+ {onClearTable && confirmClear && (
55
+ <View style={s.confirmRow}>
56
+ <Text style={s.confirmLabel}>Delete all rows?</Text>
57
+ <Pressable
58
+ style={s.confirmYes}
59
+ onPress={() => { setConfirmClear(false); onClearTable(); }}
60
+ >
61
+ <Text style={s.confirmYesText}>Yes, clear</Text>
62
+ </Pressable>
63
+ <Pressable style={s.confirmNo} onPress={() => setConfirmClear(false)}>
64
+ <Text style={s.confirmNoText}>Cancel</Text>
65
+ </Pressable>
66
+ </View>
67
+ )}
68
+ <Text style={s.info}>
69
+ {totalFiltered === 0
70
+ ? 'No rows'
71
+ : `${from}–${to} of ${totalFiltered}${filterActive ? ` (filtered from ${totalRows})` : ''} rows · ${columnCount} col${columnCount !== 1 ? 's' : ''}`}
72
+ </Text>
73
+ </View>
74
+
75
+ <View style={s.controls}>
76
+ <NavBtn label="«" disabled={page <= 1} onPress={() => onPageChange(1)} />
77
+ <NavBtn label="‹" disabled={page <= 1} onPress={() => onPageChange(page - 1)} />
78
+
79
+ {startPage > 1 && (
80
+ <>
81
+ <PageBtn num={1} active={false} onPress={() => onPageChange(1)} />
82
+ {startPage > 2 && <Text style={s.ellipsis}>…</Text>}
83
+ </>
84
+ )}
85
+
86
+ {pageButtons.map((n) => (
87
+ <PageBtn key={n} num={n} active={n === page} onPress={() => onPageChange(n)} />
88
+ ))}
89
+
90
+ {endPage < totalPages && (
91
+ <>
92
+ {endPage < totalPages - 1 && <Text style={s.ellipsis}>…</Text>}
93
+ <PageBtn num={totalPages} active={false} onPress={() => onPageChange(totalPages)} />
94
+ </>
95
+ )}
96
+
97
+ <NavBtn label="›" disabled={page >= totalPages} onPress={() => onPageChange(page + 1)} />
98
+ <NavBtn label="»" disabled={page >= totalPages} onPress={() => onPageChange(totalPages)} />
99
+ </View>
100
+
101
+ <View style={s.sizeRow}>
102
+ {PAGE_SIZES.map((sz) => (
103
+ <Pressable
104
+ key={sz}
105
+ onPress={() => onPageSizeChange(sz)}
106
+ style={[s.sizeBtn, sz === pageSize && s.sizeBtnActive]}
107
+ >
108
+ <Text style={[s.sizeBtnText, sz === pageSize && s.sizeBtnTextActive]}>{sz}</Text>
109
+ </Pressable>
110
+ ))}
111
+ </View>
112
+ </View>
113
+ );
114
+ }
115
+
116
+ function PageBtn({ num, active, onPress }: { num: number; active: boolean; onPress: () => void }) {
117
+ return (
118
+ <Pressable onPress={onPress} style={[s.pageBtn, active && s.pageBtnActive]}>
119
+ <Text style={[s.pageBtnText, active && s.pageBtnTextActive]}>{num}</Text>
120
+ </Pressable>
121
+ );
122
+ }
123
+
124
+ function NavBtn({ label, disabled, onPress }: { label: string; disabled: boolean; onPress: () => void }) {
125
+ return (
126
+ <Pressable onPress={disabled ? undefined : onPress} style={[s.navBtn, disabled && s.navBtnDisabled]}>
127
+ <Text style={[s.navBtnText, disabled && s.navBtnTextDisabled]}>{label}</Text>
128
+ </Pressable>
129
+ );
130
+ }
131
+
132
+ const s = StyleSheet.create({
133
+ bar: {
134
+ flexDirection: 'row',
135
+ alignItems: 'center',
136
+ justifyContent: 'space-between',
137
+ paddingHorizontal: 12,
138
+ paddingVertical: 8,
139
+ backgroundColor: C.surface,
140
+ borderTopWidth: 1,
141
+ borderTopColor: C.border,
142
+ flexWrap: 'wrap',
143
+ gap: 8,
144
+ },
145
+ left: {
146
+ flexDirection: 'row',
147
+ alignItems: 'center',
148
+ minWidth: 180,
149
+ flexShrink: 1,
150
+ },
151
+ info: {
152
+ color: C.textMuted,
153
+ fontSize: 12,
154
+ },
155
+ clearBtn: {
156
+ paddingHorizontal: 10,
157
+ paddingVertical: 5,
158
+ borderRadius: 5,
159
+ borderWidth: 1,
160
+ borderColor: 'transparent',
161
+ cursor: 'pointer',
162
+ },
163
+ clearBtnText: { fontSize: 12, color: C.danger, fontWeight: '500' },
164
+ confirmRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
165
+ confirmLabel: { fontSize: 11, color: C.textMuted },
166
+ confirmYes: {
167
+ paddingHorizontal: 9,
168
+ paddingVertical: 4,
169
+ borderRadius: 5,
170
+ backgroundColor: C.danger,
171
+ cursor: 'pointer',
172
+ },
173
+ confirmYesText: { fontSize: 11, fontWeight: '600', color: '#fff' },
174
+ confirmNo: {
175
+ paddingHorizontal: 9,
176
+ paddingVertical: 4,
177
+ borderRadius: 5,
178
+ backgroundColor: C.surface2,
179
+ borderWidth: 1,
180
+ borderColor: C.border,
181
+ cursor: 'pointer',
182
+ },
183
+ confirmNoText: { fontSize: 11, color: C.textSecondary },
184
+ controls: {
185
+ flexDirection: 'row',
186
+ alignItems: 'center',
187
+ gap: 2,
188
+ },
189
+ ellipsis: {
190
+ color: C.textMuted,
191
+ fontSize: 13,
192
+ paddingHorizontal: 4,
193
+ lineHeight: 28,
194
+ },
195
+ pageBtn: {
196
+ minWidth: 28,
197
+ height: 28,
198
+ borderRadius: 4,
199
+ alignItems: 'center',
200
+ justifyContent: 'center',
201
+ paddingHorizontal: 6,
202
+ backgroundColor: C.surface2,
203
+ borderWidth: 1,
204
+ borderColor: C.border,
205
+ },
206
+ pageBtnActive: {
207
+ backgroundColor: C.accent,
208
+ borderColor: C.accent,
209
+ },
210
+ pageBtnText: {
211
+ color: C.text,
212
+ fontSize: 12,
213
+ },
214
+ pageBtnTextActive: {
215
+ color: '#fff',
216
+ fontWeight: '600',
217
+ },
218
+ navBtn: {
219
+ width: 28,
220
+ height: 28,
221
+ borderRadius: 4,
222
+ alignItems: 'center',
223
+ justifyContent: 'center',
224
+ backgroundColor: C.surface2,
225
+ borderWidth: 1,
226
+ borderColor: C.border,
227
+ },
228
+ navBtnDisabled: {
229
+ opacity: 0.3,
230
+ },
231
+ navBtnText: {
232
+ color: C.text,
233
+ fontSize: 14,
234
+ lineHeight: 18,
235
+ },
236
+ navBtnTextDisabled: {
237
+ color: C.textMuted,
238
+ },
239
+ sizeRow: {
240
+ flexDirection: 'row',
241
+ gap: 2,
242
+ alignItems: 'center',
243
+ },
244
+ sizeBtn: {
245
+ height: 26,
246
+ paddingHorizontal: 8,
247
+ borderRadius: 4,
248
+ alignItems: 'center',
249
+ justifyContent: 'center',
250
+ backgroundColor: C.surface2,
251
+ borderWidth: 1,
252
+ borderColor: C.border,
253
+ },
254
+ sizeBtnActive: {
255
+ backgroundColor: C.surface2,
256
+ borderColor: C.accent,
257
+ },
258
+ sizeBtnText: {
259
+ color: C.textMuted,
260
+ fontSize: 11,
261
+ },
262
+ sizeBtnTextActive: {
263
+ color: C.accent,
264
+ fontWeight: '600',
265
+ },
266
+ });
@@ -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
+ });