rozenite-sqlite 0.0.9 → 0.0.11

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,139 @@
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
+ shadowColor: '#000',
121
+ shadowOffset: { width: 0, height: 8 },
122
+ shadowOpacity: 0.5,
123
+ shadowRadius: 20,
124
+ },
125
+ menuScroll: { maxHeight: 200 },
126
+ menuEmpty: { padding: 14, fontSize: 12, color: C.textMuted, textAlign: 'center' },
127
+ menuItem: {
128
+ flexDirection: 'row',
129
+ alignItems: 'center',
130
+ justifyContent: 'space-between',
131
+ paddingHorizontal: 12,
132
+ paddingVertical: 10,
133
+ },
134
+ menuItemSelected: { backgroundColor: C.accentSubtle },
135
+ menuItemPressed: { backgroundColor: C.surface },
136
+ menuItemText: { fontSize: 13, color: C.text },
137
+ menuItemTextSelected: { color: C.accent, fontWeight: '600' },
138
+ checkmark: { fontSize: 12, color: C.accent },
139
+ });
@@ -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,273 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { View, Text, StyleSheet, ScrollView, Pressable, TextInput } from 'react-native';
3
+ import { C, type RowData } from '../theme';
4
+
5
+ function isJsonString(val: string): boolean {
6
+ const t = val.trim();
7
+ if (t.length < 2 || (t[0] !== '{' && t[0] !== '[')) return false;
8
+ try { JSON.parse(t); return true; } catch { return false; }
9
+ }
10
+
11
+ export interface RowDetailPanelProps {
12
+ row: RowData | null;
13
+ rowIndex: number | null;
14
+ onClose: () => void;
15
+ onSave: (updated: RowData) => void;
16
+ onDelete: () => void;
17
+ }
18
+
19
+ export function RowDetailPanel({ row, rowIndex, onClose, onSave, onDelete }: RowDetailPanelProps) {
20
+ const [draft, setDraft] = useState<RowData>({});
21
+ const [dirty, setDirty] = useState(false);
22
+ const [confirmDelete, setConfirmDelete] = useState(false);
23
+
24
+ useEffect(() => {
25
+ if (row) {
26
+ const formatted = Object.fromEntries(
27
+ Object.entries(row).map(([k, v]) => {
28
+ const s = v == null ? v : String(v);
29
+ if (s != null && isJsonString(s)) {
30
+ try { return [k, JSON.stringify(JSON.parse(s), null, 2)]; } catch {}
31
+ }
32
+ return [k, v];
33
+ })
34
+ );
35
+ setDraft(formatted);
36
+ setDirty(false);
37
+ setConfirmDelete(false);
38
+ }
39
+ }, [row]);
40
+
41
+ if (!row) return null;
42
+
43
+ const handleChange = (key: string, value: string) => {
44
+ setDraft((prev) => ({ ...prev, [key]: value }));
45
+ setDirty(true);
46
+ };
47
+
48
+ const handleSave = () => {
49
+ onSave(draft);
50
+ setDirty(false);
51
+ };
52
+
53
+ const handleCancel = () => {
54
+ setDraft({ ...row });
55
+ setDirty(false);
56
+ setConfirmDelete(false);
57
+ onClose();
58
+ };
59
+
60
+ return (
61
+ <View style={s.panel}>
62
+ <View style={s.header}>
63
+ <View>
64
+ <Text style={s.eyebrow}>ROW DETAIL</Text>
65
+ <View style={s.titleRow}>
66
+ <Text style={s.title}>Row #{(rowIndex ?? 0) + 1}</Text>
67
+ {dirty && <View style={s.dirtyDot} />}
68
+ </View>
69
+ </View>
70
+ <Pressable
71
+ onPress={onClose}
72
+ style={({ pressed }) => [s.closeBtn, pressed && { opacity: 0.6 }]}
73
+ >
74
+ <Text style={s.closeBtnText}>✕</Text>
75
+ </Pressable>
76
+ </View>
77
+
78
+ <ScrollView style={s.scroll} showsVerticalScrollIndicator={false}>
79
+ {Object.entries(draft).map(([key, value]) => {
80
+ const strVal = value === null || value === undefined ? '' : String(value);
81
+ const isJson = isJsonString(strVal);
82
+ return (
83
+ <View key={key} style={s.field}>
84
+ <View style={s.fieldKeyRow}>
85
+ <Text style={s.fieldKey}>{key}</Text>
86
+ {isJson && <Text style={s.fieldJsonBadge}>JSON</Text>}
87
+ </View>
88
+ <TextInput
89
+ style={[s.fieldInput, isJson && s.fieldInputJson]}
90
+ value={strVal}
91
+ onChangeText={(text) => handleChange(key, text)}
92
+ placeholder="NULL"
93
+ placeholderTextColor={C.textMuted}
94
+ multiline={isJson || strVal.length > 50}
95
+ />
96
+ </View>
97
+ );
98
+ })}
99
+ <View style={{ height: 20 }} />
100
+ </ScrollView>
101
+
102
+ <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>
115
+ </View>
116
+
117
+ <View style={s.deleteZone}>
118
+ {!confirmDelete ? (
119
+ <Pressable
120
+ style={({ pressed }) => [s.btnDeleteSmall, pressed && s.btnPressed]}
121
+ onPress={() => setConfirmDelete(true)}
122
+ >
123
+ <Text style={s.btnDeleteSmallText}>⊘ Delete this row</Text>
124
+ </Pressable>
125
+ ) : (
126
+ <View style={s.confirmBox}>
127
+ <Text style={s.confirmLabel}>This action cannot be undone.</Text>
128
+ <View style={s.confirmRow}>
129
+ <Pressable
130
+ style={({ pressed }) => [s.btn, s.btnConfirm, { flex: 1 }, pressed && s.btnPressed]}
131
+ onPress={onDelete}
132
+ >
133
+ <Text style={s.btnConfirmText}>Yes, delete</Text>
134
+ </Pressable>
135
+ <Pressable
136
+ style={({ pressed }) => [s.btn, s.btnCancelSmall, { flex: 1 }, pressed && s.btnPressed]}
137
+ onPress={() => setConfirmDelete(false)}
138
+ >
139
+ <Text style={s.btnCancelText}>Cancel</Text>
140
+ </Pressable>
141
+ </View>
142
+ </View>
143
+ )}
144
+ </View>
145
+ </View>
146
+ );
147
+ }
148
+
149
+ const s = StyleSheet.create({
150
+ panel: {
151
+ width: 300,
152
+ backgroundColor: C.surface,
153
+ borderLeftWidth: 1,
154
+ borderLeftColor: C.border,
155
+ flexDirection: 'column',
156
+ shadowColor: '#000',
157
+ shadowOffset: { width: -8, height: 0 },
158
+ shadowOpacity: 0.45,
159
+ shadowRadius: 20,
160
+ elevation: 10,
161
+ boxShadow: '-8px 0 24px rgba(0,0,0,0.45)',
162
+ },
163
+ header: {
164
+ flexDirection: 'row',
165
+ alignItems: 'center',
166
+ justifyContent: 'space-between',
167
+ paddingHorizontal: 16,
168
+ paddingVertical: 14,
169
+ borderBottomWidth: 1,
170
+ borderBottomColor: C.border,
171
+ },
172
+ eyebrow: {
173
+ fontSize: 10,
174
+ fontWeight: '700',
175
+ color: C.textMuted,
176
+ textTransform: 'uppercase',
177
+ letterSpacing: 0.8,
178
+ },
179
+ titleRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 2 },
180
+ title: { fontSize: 16, fontWeight: '700', color: C.text },
181
+ dirtyDot: { width: 7, height: 7, borderRadius: 4, backgroundColor: C.accent },
182
+ closeBtn: {
183
+ width: 28,
184
+ height: 28,
185
+ borderRadius: 6,
186
+ backgroundColor: C.surface2,
187
+ alignItems: 'center',
188
+ justifyContent: 'center',
189
+ },
190
+ closeBtnText: { fontSize: 12, color: C.textSecondary },
191
+ scroll: { flex: 1, paddingHorizontal: 16, paddingTop: 16 },
192
+ field: { marginBottom: 16 },
193
+ fieldKeyRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 5 },
194
+ fieldKey: {
195
+ fontSize: 10,
196
+ fontWeight: '700',
197
+ color: C.textMuted,
198
+ textTransform: 'uppercase',
199
+ letterSpacing: 0.7,
200
+ },
201
+ fieldJsonBadge: {
202
+ fontSize: 9,
203
+ fontWeight: '700',
204
+ color: '#a78bfa',
205
+ backgroundColor: 'rgba(167,139,250,0.12)',
206
+ paddingHorizontal: 4,
207
+ paddingVertical: 2,
208
+ borderRadius: 3,
209
+ },
210
+ fieldInput: {
211
+ backgroundColor: C.surface2,
212
+ borderWidth: 1,
213
+ borderColor: C.border,
214
+ borderRadius: 6,
215
+ paddingHorizontal: 10,
216
+ paddingVertical: 8,
217
+ fontSize: 13,
218
+ color: C.text,
219
+ outlineStyle: 'none' as any,
220
+ },
221
+ fieldInputJson: {
222
+ fontFamily: 'monospace',
223
+ fontSize: 12,
224
+ lineHeight: 18,
225
+ borderColor: 'rgba(167,139,250,0.3)',
226
+ minHeight: 180,
227
+ textAlignVertical: 'top',
228
+ paddingTop: 10,
229
+ },
230
+ primaryActions: {
231
+ flexDirection: 'row',
232
+ gap: 8,
233
+ paddingHorizontal: 16,
234
+ paddingTop: 14,
235
+ paddingBottom: 10,
236
+ borderTopWidth: 1,
237
+ borderTopColor: C.border,
238
+ },
239
+ deleteZone: {
240
+ paddingHorizontal: 16,
241
+ paddingBottom: 14,
242
+ paddingTop: 2,
243
+ },
244
+ btn: {
245
+ flex: 1,
246
+ borderRadius: 6,
247
+ paddingVertical: 9,
248
+ alignItems: 'center',
249
+ justifyContent: 'center',
250
+ },
251
+ btnPressed: { opacity: 0.72 },
252
+ btnSave: { backgroundColor: C.accent },
253
+ btnSaveDisabled: { backgroundColor: C.surface2 },
254
+ btnSaveText: { fontSize: 13, fontWeight: '600', color: '#fff' },
255
+ btnSaveTextDisabled: { color: C.textMuted },
256
+ btnCancel: { backgroundColor: C.surface2, borderWidth: 1, borderColor: C.border },
257
+ btnCancelText: { fontSize: 13, fontWeight: '500', color: C.textSecondary },
258
+ btnDeleteSmall: {
259
+ paddingVertical: 7,
260
+ alignItems: 'center',
261
+ justifyContent: 'center',
262
+ borderRadius: 6,
263
+ borderWidth: 1,
264
+ borderColor: 'transparent',
265
+ },
266
+ btnDeleteSmallText: { fontSize: 12, color: C.danger, fontWeight: '500' },
267
+ confirmBox: { gap: 8 },
268
+ confirmLabel: { fontSize: 11, color: C.textMuted, textAlign: 'center' },
269
+ confirmRow: { flexDirection: 'row', gap: 8 },
270
+ btnConfirm: { backgroundColor: C.danger },
271
+ btnConfirmText: { fontSize: 12, fontWeight: '600', color: '#fff' },
272
+ btnCancelSmall: { backgroundColor: C.surface2, borderWidth: 1, borderColor: C.border },
273
+ });