7093ebaf0b
Settings hatte zwei Probleme:
1) Gedächtnis-Liste scrollte nur runter, nicht hoch. Klassisches Android
nested-Scroll-Problem: aeussere ScrollView + innere FlatList mit
fixer height:600 = nur eine Richtung wird respektiert.
Fix: outer ScrollView mit scrollEnabled=false wenn die Section eine
eigene voll-hoch-scrollende Sub-Liste hat (memory/triggers). Plus
dynamische Hoehe via useWindowDimensions (winHeight - 220 statt
hardcoded 600) damit MemoryBrowser sauber den verfuegbaren Platz
nutzt.
2) Trigger waren bisher nur via Diagnostic-Tab editierbar — keine App-
side CRUD. Stefan wollte das.
Neu: TriggerBrowser-Komponente (analog MemoryBrowser-Struktur)
- Liste aller Trigger mit Filter (alle/aktive/inaktive)
- Toggle aktiv/inaktiv via Switch direkt in der Zeile
- Tap oeffnet TriggerEditModal (Nachricht/Condition/fires_at/intervals
editieren, Loeschen-Knopf mit Confirm)
- "+ Neu"-Knopf oeffnet TriggerNewModal mit Type-Switch (Watcher/Timer),
Watcher zeigt Hinweis auf verfuegbare Funktionen + Variablen
- Live Reload-Button, Meta-Info (fire_count, last_fired_at, ...)
brainApi um Trigger-Endpoints erweitert: listTriggers, getTrigger,
createTimer, createWatcher, updateTrigger (patch), deleteTrigger,
getTriggerConditions, getTriggerLogs. Plus Trigger-Type-Definition.
Settings-Liste hat eine neue Section "⏰ Trigger" zwischen Gedaechtnis
und Protokoll.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
584 lines
20 KiB
TypeScript
584 lines
20 KiB
TypeScript
/**
|
||
* Trigger-Browser — Liste aller Trigger (timer + watcher) mit Toggle,
|
||
* Tap-zum-Bearbeiten und "+ Neu"-Knopf.
|
||
*
|
||
* Eingesetzt von SettingsScreen → Sektion "Trigger".
|
||
*
|
||
* Brain-API ueber brainApi (RVS-Brain-Proxy).
|
||
*/
|
||
|
||
import React, { useCallback, useEffect, useState } from 'react';
|
||
import {
|
||
ActivityIndicator,
|
||
Alert,
|
||
FlatList,
|
||
Modal,
|
||
ScrollView,
|
||
StyleSheet,
|
||
Switch,
|
||
Text,
|
||
TextInput,
|
||
TouchableOpacity,
|
||
View,
|
||
} from 'react-native';
|
||
|
||
import brainApi, { Trigger } from '../services/brainApi';
|
||
|
||
const COL_ACTIVE = '#34C759';
|
||
const COL_INACTIVE = '#555570';
|
||
const COL_TIMER = '#0096FF';
|
||
const COL_WATCHER = '#FFD60A';
|
||
|
||
function relTime(iso: string | null | undefined): string {
|
||
if (!iso) return '—';
|
||
const t = new Date(iso).getTime();
|
||
if (!t) return '—';
|
||
const diffSec = Math.floor((Date.now() - t) / 1000);
|
||
if (diffSec < 60) return `vor ${diffSec}s`;
|
||
if (diffSec < 3600) return `vor ${Math.floor(diffSec / 60)}min`;
|
||
if (diffSec < 86400) return `vor ${Math.floor(diffSec / 3600)}h`;
|
||
return `vor ${Math.floor(diffSec / 86400)}d`;
|
||
}
|
||
|
||
export const TriggerBrowser: React.FC = () => {
|
||
const [items, setItems] = useState<Trigger[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [err, setErr] = useState<string | null>(null);
|
||
const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('all');
|
||
const [editTrigger, setEditTrigger] = useState<Trigger | null>(null);
|
||
const [showNew, setShowNew] = useState(false);
|
||
|
||
const load = useCallback(() => {
|
||
setLoading(true); setErr(null);
|
||
brainApi.listTriggers()
|
||
.then(t => {
|
||
// Sortierung: aktive zuerst, dann nach Name
|
||
t.sort((a, b) => {
|
||
if (a.active !== b.active) return a.active ? -1 : 1;
|
||
return (a.name || '').localeCompare(b.name || '');
|
||
});
|
||
setItems(t);
|
||
})
|
||
.catch(e => setErr(String(e?.message || e)))
|
||
.finally(() => setLoading(false));
|
||
}, []);
|
||
|
||
useEffect(() => { load(); }, [load]);
|
||
|
||
const visible = items.filter(t => {
|
||
if (filter === 'active') return t.active;
|
||
if (filter === 'inactive') return !t.active;
|
||
return true;
|
||
});
|
||
|
||
const toggleActive = (t: Trigger) => {
|
||
brainApi.updateTrigger(t.name, { active: !t.active })
|
||
.then(() => load())
|
||
.catch(e => Alert.alert('Fehler', String(e?.message || e)));
|
||
};
|
||
|
||
const deleteTrigger = (t: Trigger) => {
|
||
Alert.alert(
|
||
'Trigger löschen?',
|
||
`"${t.name}" — diese Aktion ist nicht rückgängig zu machen.`,
|
||
[
|
||
{ text: 'Abbrechen', style: 'cancel' },
|
||
{
|
||
text: 'Löschen',
|
||
style: 'destructive',
|
||
onPress: () => {
|
||
brainApi.deleteTrigger(t.name)
|
||
.then(() => { setEditTrigger(null); load(); })
|
||
.catch(e => Alert.alert('Fehler', String(e?.message || e)));
|
||
},
|
||
},
|
||
],
|
||
);
|
||
};
|
||
|
||
const renderItem = ({ item }: { item: Trigger }) => {
|
||
const typeColor = item.type === 'timer' ? COL_TIMER : COL_WATCHER;
|
||
const typeLabel = item.type === 'timer' ? '⏰ Timer' : '👁 Watcher';
|
||
return (
|
||
<TouchableOpacity style={s.row} onPress={() => setEditTrigger(item)}>
|
||
<View style={{flex: 1, marginRight: 8}}>
|
||
<View style={{flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 4}}>
|
||
<Text style={{color: typeColor, fontSize: 11, fontWeight: '700'}}>{typeLabel}</Text>
|
||
<Text style={{color: '#E0E0F0', fontWeight: '600', flex: 1}} numberOfLines={1}>{item.name}</Text>
|
||
</View>
|
||
<Text style={{color: '#8888AA', fontSize: 12}} numberOfLines={2}>{item.message}</Text>
|
||
{item.type === 'watcher' && item.condition ? (
|
||
<Text style={{color: '#555570', fontSize: 11, marginTop: 4, fontFamily: 'monospace'}} numberOfLines={1}>
|
||
{item.condition}
|
||
</Text>
|
||
) : null}
|
||
{item.type === 'timer' && item.fires_at ? (
|
||
<Text style={{color: '#555570', fontSize: 11, marginTop: 4}}>
|
||
feuert: {new Date(item.fires_at).toLocaleString('de-DE')}
|
||
</Text>
|
||
) : null}
|
||
<Text style={{color: '#444460', fontSize: 10, marginTop: 4}}>
|
||
{item.fire_count || 0}× gefeuert · zuletzt: {relTime(item.last_fired_at)}
|
||
</Text>
|
||
</View>
|
||
<Switch
|
||
value={item.active}
|
||
onValueChange={() => toggleActive(item)}
|
||
trackColor={{ false: '#1E1E2E', true: COL_ACTIVE }}
|
||
thumbColor="#E0E0F0"
|
||
/>
|
||
</TouchableOpacity>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<View style={{flex: 1}}>
|
||
{/* Filter-Leiste + Reload + Neu */}
|
||
<View style={s.toolbar}>
|
||
{(['all', 'active', 'inactive'] as const).map(f => (
|
||
<TouchableOpacity
|
||
key={f}
|
||
style={[s.chip, filter === f && s.chipActive]}
|
||
onPress={() => setFilter(f)}
|
||
>
|
||
<Text style={{color: filter === f ? '#0D0D1A' : '#8888AA', fontSize: 12, fontWeight: '600'}}>
|
||
{f === 'all' ? 'Alle' : f === 'active' ? 'Aktive' : 'Inaktive'}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
<View style={{flex: 1}} />
|
||
<TouchableOpacity onPress={load} style={s.iconBtn}>
|
||
<Text style={{fontSize: 16}}>{'↻'}</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity onPress={() => setShowNew(true)} style={[s.iconBtn, {backgroundColor: '#0096FF'}]}>
|
||
<Text style={{fontSize: 14, color: '#fff', fontWeight: '700'}}>+ Neu</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
{err ? <Text style={s.err}>{err}</Text> : null}
|
||
|
||
{loading && items.length === 0 ? (
|
||
<ActivityIndicator color="#0096FF" style={{marginTop: 20}} />
|
||
) : (
|
||
<FlatList
|
||
data={visible}
|
||
keyExtractor={t => t.name}
|
||
renderItem={renderItem}
|
||
nestedScrollEnabled={true}
|
||
ListEmptyComponent={
|
||
<Text style={{color: '#555570', textAlign: 'center', padding: 20, fontStyle: 'italic'}}>
|
||
{items.length === 0 ? '(keine Trigger angelegt)' : '(keine Treffer für diesen Filter)'}
|
||
</Text>
|
||
}
|
||
contentContainerStyle={{paddingBottom: 20}}
|
||
/>
|
||
)}
|
||
|
||
{editTrigger ? (
|
||
<TriggerEditModal
|
||
trigger={editTrigger}
|
||
onClose={() => setEditTrigger(null)}
|
||
onSaved={() => { setEditTrigger(null); load(); }}
|
||
onDelete={() => deleteTrigger(editTrigger)}
|
||
/>
|
||
) : null}
|
||
|
||
{showNew ? (
|
||
<TriggerNewModal
|
||
onClose={() => setShowNew(false)}
|
||
onCreated={() => { setShowNew(false); load(); }}
|
||
/>
|
||
) : null}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
// ── Edit-Modal ─────────────────────────────────────────────────────────
|
||
|
||
interface EditProps {
|
||
trigger: Trigger;
|
||
onClose: () => void;
|
||
onSaved: () => void;
|
||
onDelete: () => void;
|
||
}
|
||
|
||
const TriggerEditModal: React.FC<EditProps> = ({ trigger, onClose, onSaved, onDelete }) => {
|
||
const [message, setMessage] = useState(trigger.message || '');
|
||
const [condition, setCondition] = useState(trigger.condition || '');
|
||
const [firesAt, setFiresAt] = useState(trigger.fires_at || '');
|
||
const [checkInterval, setCheckInterval] = useState(String(trigger.check_interval_sec || 300));
|
||
const [throttle, setThrottle] = useState(String(trigger.throttle_sec || 3600));
|
||
const [saving, setSaving] = useState(false);
|
||
|
||
const save = () => {
|
||
setSaving(true);
|
||
const patch: any = { message };
|
||
if (trigger.type === 'watcher') {
|
||
patch.condition = condition;
|
||
patch.check_interval_sec = parseInt(checkInterval, 10) || 300;
|
||
patch.throttle_sec = parseInt(throttle, 10) || 3600;
|
||
} else if (trigger.type === 'timer') {
|
||
patch.fires_at = firesAt;
|
||
}
|
||
brainApi.updateTrigger(trigger.name, patch)
|
||
.then(onSaved)
|
||
.catch(e => Alert.alert('Fehler beim Speichern', String(e?.message || e)))
|
||
.finally(() => setSaving(false));
|
||
};
|
||
|
||
return (
|
||
<Modal visible animationType="slide" onRequestClose={onClose} transparent>
|
||
<View style={s.modalBg}>
|
||
<View style={s.modal}>
|
||
<View style={s.modalHeader}>
|
||
<Text style={{color: trigger.type === 'timer' ? COL_TIMER : COL_WATCHER, fontWeight: '700', fontSize: 16, flex: 1}}>
|
||
{trigger.type === 'timer' ? '⏰' : '👁'} {trigger.name}
|
||
</Text>
|
||
<TouchableOpacity onPress={onClose}>
|
||
<Text style={{color: '#8888AA', fontSize: 24}}>×</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
<ScrollView style={{padding: 14}} nestedScrollEnabled>
|
||
<Text style={s.label}>Nachricht</Text>
|
||
<TextInput
|
||
style={s.input}
|
||
value={message}
|
||
onChangeText={setMessage}
|
||
multiline
|
||
placeholder="Was soll ARIA sagen wenn der Trigger feuert?"
|
||
placeholderTextColor="#555570"
|
||
/>
|
||
|
||
{trigger.type === 'watcher' ? (
|
||
<>
|
||
<Text style={s.label}>Condition</Text>
|
||
<TextInput
|
||
style={[s.input, {fontFamily: 'monospace', fontSize: 12}]}
|
||
value={condition}
|
||
onChangeText={setCondition}
|
||
placeholder="z.B. near(53.0, 8.5, 300)"
|
||
placeholderTextColor="#555570"
|
||
autoCapitalize="none"
|
||
/>
|
||
<View style={{flexDirection: 'row', gap: 8}}>
|
||
<View style={{flex: 1}}>
|
||
<Text style={s.label}>Check-Intervall (s)</Text>
|
||
<TextInput
|
||
style={s.input}
|
||
value={checkInterval}
|
||
onChangeText={setCheckInterval}
|
||
keyboardType="number-pad"
|
||
/>
|
||
</View>
|
||
<View style={{flex: 1}}>
|
||
<Text style={s.label}>Throttle (s)</Text>
|
||
<TextInput
|
||
style={s.input}
|
||
value={throttle}
|
||
onChangeText={setThrottle}
|
||
keyboardType="number-pad"
|
||
/>
|
||
</View>
|
||
</View>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Text style={s.label}>Feuert am (ISO, UTC)</Text>
|
||
<TextInput
|
||
style={[s.input, {fontFamily: 'monospace', fontSize: 12}]}
|
||
value={firesAt}
|
||
onChangeText={setFiresAt}
|
||
placeholder="2026-05-15T20:00:00+00:00"
|
||
placeholderTextColor="#555570"
|
||
autoCapitalize="none"
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
<View style={s.metaBox}>
|
||
<Text style={s.meta}>Status: {trigger.active ? '🟢 aktiv' : '⚪ inaktiv'}</Text>
|
||
<Text style={s.meta}>Gefeuert: {trigger.fire_count || 0}×</Text>
|
||
<Text style={s.meta}>Zuletzt gefeuert: {relTime(trigger.last_fired_at)}</Text>
|
||
<Text style={s.meta}>Zuletzt geprüft: {relTime(trigger.last_checked_at)}</Text>
|
||
{trigger.author ? <Text style={s.meta}>Angelegt von: {trigger.author}</Text> : null}
|
||
</View>
|
||
</ScrollView>
|
||
<View style={s.modalFooter}>
|
||
<TouchableOpacity onPress={onDelete} style={[s.btn, {backgroundColor: '#3A1F1F', borderColor: '#FF3B30'}]}>
|
||
<Text style={{color: '#FF3B30', fontWeight: '700'}}>🗑 Löschen</Text>
|
||
</TouchableOpacity>
|
||
<View style={{flex: 1}} />
|
||
<TouchableOpacity onPress={save} disabled={saving} style={[s.btn, {backgroundColor: '#0096FF', opacity: saving ? 0.5 : 1}]}>
|
||
<Text style={{color: '#fff', fontWeight: '700'}}>{saving ? 'Speichert...' : 'Speichern'}</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
);
|
||
};
|
||
|
||
// ── Neu-Modal ──────────────────────────────────────────────────────────
|
||
|
||
interface NewProps {
|
||
onClose: () => void;
|
||
onCreated: () => void;
|
||
}
|
||
|
||
const TriggerNewModal: React.FC<NewProps> = ({ onClose, onCreated }) => {
|
||
const [ttype, setTtype] = useState<'timer' | 'watcher'>('watcher');
|
||
const [name, setName] = useState('');
|
||
const [message, setMessage] = useState('');
|
||
const [condition, setCondition] = useState('');
|
||
const [firesAt, setFiresAt] = useState('');
|
||
const [checkInterval, setCheckInterval] = useState('300');
|
||
const [throttle, setThrottle] = useState('3600');
|
||
const [saving, setSaving] = useState(false);
|
||
|
||
const create = () => {
|
||
if (!name.trim() || !message.trim()) {
|
||
Alert.alert('Name und Nachricht erforderlich');
|
||
return;
|
||
}
|
||
setSaving(true);
|
||
const promise = ttype === 'timer'
|
||
? brainApi.createTimer({
|
||
name: name.trim(),
|
||
fires_at: firesAt.trim(),
|
||
message: message.trim(),
|
||
})
|
||
: brainApi.createWatcher({
|
||
name: name.trim(),
|
||
condition: condition.trim(),
|
||
message: message.trim(),
|
||
check_interval_sec: parseInt(checkInterval, 10) || 300,
|
||
throttle_sec: parseInt(throttle, 10) || 3600,
|
||
});
|
||
promise
|
||
.then(onCreated)
|
||
.catch(e => Alert.alert('Fehler beim Anlegen', String(e?.message || e)))
|
||
.finally(() => setSaving(false));
|
||
};
|
||
|
||
return (
|
||
<Modal visible animationType="slide" onRequestClose={onClose} transparent>
|
||
<View style={s.modalBg}>
|
||
<View style={s.modal}>
|
||
<View style={s.modalHeader}>
|
||
<Text style={{color: '#FFD60A', fontWeight: '700', fontSize: 16, flex: 1}}>+ Neuer Trigger</Text>
|
||
<TouchableOpacity onPress={onClose}>
|
||
<Text style={{color: '#8888AA', fontSize: 24}}>×</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
<ScrollView style={{padding: 14}} nestedScrollEnabled>
|
||
<Text style={s.label}>Typ</Text>
|
||
<View style={{flexDirection: 'row', gap: 8, marginBottom: 12}}>
|
||
{(['watcher', 'timer'] as const).map(t => (
|
||
<TouchableOpacity
|
||
key={t}
|
||
onPress={() => setTtype(t)}
|
||
style={[s.chip, ttype === t && s.chipActive, {flex: 1, paddingVertical: 10}]}
|
||
>
|
||
<Text style={{color: ttype === t ? '#0D0D1A' : '#8888AA', fontWeight: '700', textAlign: 'center'}}>
|
||
{t === 'watcher' ? '👁 Watcher' : '⏰ Timer'}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
|
||
<Text style={s.label}>Name (kebab-case)</Text>
|
||
<TextInput
|
||
style={s.input}
|
||
value={name}
|
||
onChangeText={setName}
|
||
placeholder="z.B. drk-kreyenbrueck-warnung"
|
||
placeholderTextColor="#555570"
|
||
autoCapitalize="none"
|
||
/>
|
||
|
||
<Text style={s.label}>Nachricht</Text>
|
||
<TextInput
|
||
style={s.input}
|
||
value={message}
|
||
onChangeText={setMessage}
|
||
multiline
|
||
placeholder="Was soll ARIA sagen?"
|
||
placeholderTextColor="#555570"
|
||
/>
|
||
|
||
{ttype === 'watcher' ? (
|
||
<>
|
||
<Text style={s.label}>Condition</Text>
|
||
<TextInput
|
||
style={[s.input, {fontFamily: 'monospace', fontSize: 12}]}
|
||
value={condition}
|
||
onChangeText={setCondition}
|
||
placeholder="z.B. entered_near(53.0, 8.5, 300)"
|
||
placeholderTextColor="#555570"
|
||
autoCapitalize="none"
|
||
/>
|
||
<Text style={s.hint}>
|
||
Funktionen: near() / entered_near() / left_near() · Variablen: disk_free_gb, hour_of_day, current_lat, current_lon, last_user_message_ago_sec
|
||
</Text>
|
||
<View style={{flexDirection: 'row', gap: 8}}>
|
||
<View style={{flex: 1}}>
|
||
<Text style={s.label}>Check-Intervall (s)</Text>
|
||
<TextInput
|
||
style={s.input}
|
||
value={checkInterval}
|
||
onChangeText={setCheckInterval}
|
||
keyboardType="number-pad"
|
||
/>
|
||
</View>
|
||
<View style={{flex: 1}}>
|
||
<Text style={s.label}>Throttle (s)</Text>
|
||
<TextInput
|
||
style={s.input}
|
||
value={throttle}
|
||
onChangeText={setThrottle}
|
||
keyboardType="number-pad"
|
||
/>
|
||
</View>
|
||
</View>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Text style={s.label}>Feuert am (ISO, UTC)</Text>
|
||
<TextInput
|
||
style={[s.input, {fontFamily: 'monospace', fontSize: 12}]}
|
||
value={firesAt}
|
||
onChangeText={setFiresAt}
|
||
placeholder="2026-05-15T20:00:00+00:00"
|
||
placeholderTextColor="#555570"
|
||
autoCapitalize="none"
|
||
/>
|
||
<Text style={s.hint}>Beispiel oben: heute 20:00 UTC = 22:00 CEST</Text>
|
||
</>
|
||
)}
|
||
</ScrollView>
|
||
<View style={s.modalFooter}>
|
||
<View style={{flex: 1}} />
|
||
<TouchableOpacity onPress={create} disabled={saving} style={[s.btn, {backgroundColor: '#0096FF', opacity: saving ? 0.5 : 1}]}>
|
||
<Text style={{color: '#fff', fontWeight: '700'}}>{saving ? 'Legt an...' : 'Anlegen'}</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
);
|
||
};
|
||
|
||
const s = StyleSheet.create({
|
||
toolbar: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
marginBottom: 8,
|
||
},
|
||
chip: {
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 6,
|
||
borderRadius: 14,
|
||
backgroundColor: '#1E1E2E',
|
||
},
|
||
chipActive: {
|
||
backgroundColor: '#FFD60A',
|
||
},
|
||
iconBtn: {
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 6,
|
||
borderRadius: 14,
|
||
backgroundColor: '#1E1E2E',
|
||
},
|
||
err: {
|
||
color: '#FF3B30',
|
||
padding: 12,
|
||
fontSize: 12,
|
||
},
|
||
row: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
padding: 12,
|
||
backgroundColor: '#1A1A2E',
|
||
borderRadius: 8,
|
||
marginBottom: 6,
|
||
},
|
||
modalBg: {
|
||
flex: 1,
|
||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
padding: 16,
|
||
},
|
||
modal: {
|
||
backgroundColor: '#0D0D1A',
|
||
borderRadius: 12,
|
||
width: '100%',
|
||
maxWidth: 600,
|
||
maxHeight: '90%',
|
||
borderWidth: 1,
|
||
borderColor: '#1E1E2E',
|
||
},
|
||
modalHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
padding: 14,
|
||
borderBottomWidth: 1,
|
||
borderBottomColor: '#1E1E2E',
|
||
},
|
||
modalFooter: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
padding: 12,
|
||
borderTopWidth: 1,
|
||
borderTopColor: '#1E1E2E',
|
||
gap: 8,
|
||
},
|
||
label: {
|
||
color: '#8888AA',
|
||
fontSize: 11,
|
||
fontWeight: '700',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: 0.5,
|
||
marginTop: 8,
|
||
marginBottom: 4,
|
||
},
|
||
input: {
|
||
backgroundColor: '#1A1A2E',
|
||
borderWidth: 1,
|
||
borderColor: '#1E1E2E',
|
||
borderRadius: 6,
|
||
color: '#E0E0F0',
|
||
padding: 10,
|
||
fontSize: 14,
|
||
marginBottom: 8,
|
||
},
|
||
hint: {
|
||
color: '#555570',
|
||
fontSize: 11,
|
||
fontStyle: 'italic',
|
||
marginTop: -4,
|
||
marginBottom: 10,
|
||
},
|
||
metaBox: {
|
||
backgroundColor: '#1A1A2E',
|
||
borderRadius: 6,
|
||
padding: 10,
|
||
marginTop: 10,
|
||
gap: 4,
|
||
},
|
||
meta: {
|
||
color: '#8888AA',
|
||
fontSize: 12,
|
||
},
|
||
btn: {
|
||
paddingHorizontal: 14,
|
||
paddingVertical: 10,
|
||
borderRadius: 6,
|
||
borderWidth: 1,
|
||
borderColor: 'transparent',
|
||
},
|
||
});
|
||
|
||
export default TriggerBrowser;
|