559 lines
20 KiB
TypeScript
559 lines
20 KiB
TypeScript
import React, { useState, useEffect, useMemo } from 'react';
|
||
import {
|
||
View, Text, StyleSheet, TouchableOpacity, ScrollView,
|
||
Modal, ActivityIndicator, useWindowDimensions, Platform,
|
||
} from 'react-native';
|
||
import { useConnectionStore } from '../../src/stores/connectionStore';
|
||
import { useDeviceStore } from '../../src/stores/deviceStore';
|
||
import { useDataStore } from '../../src/stores/dataStore';
|
||
import { useDevice } from '../../src/hooks/useDevice';
|
||
import { useWaveform } from '../../src/hooks/useWaveform';
|
||
import { WaveformChart } from '../../src/components/WaveformChart';
|
||
import { ParamForm } from '../../src/components/ParamForm';
|
||
import { CHANNEL_COLORS } from '../../src/protocol/constants';
|
||
import { formatUV, formatUtc, formatCoord } from '../../src/utils/format';
|
||
import { Colors, Spacing, Radius } from '../../src/design/tokens';
|
||
import type { MeasurementFrame } from '../../src/protocol/types';
|
||
|
||
// ── Param sheet ───────────────────────────────────────────────────────────────
|
||
|
||
function ParamSheet({
|
||
visible,
|
||
onClose,
|
||
onSetup,
|
||
busy,
|
||
configDirty,
|
||
}: {
|
||
visible: boolean;
|
||
onClose: () => void;
|
||
onSetup: () => Promise<void>;
|
||
busy: boolean;
|
||
configDirty: boolean;
|
||
}) {
|
||
const { deviceStatus } = useDeviceStore();
|
||
const measuring = deviceStatus !== 'idle';
|
||
|
||
return (
|
||
<Modal
|
||
visible={visible}
|
||
transparent
|
||
animationType="slide"
|
||
onRequestClose={onClose}
|
||
statusBarTranslucent
|
||
>
|
||
<TouchableOpacity style={PS.backdrop} activeOpacity={1} onPress={onClose} />
|
||
<View style={PS.sheet}>
|
||
<View style={PS.handle} />
|
||
|
||
<View style={PS.header}>
|
||
<Text style={PS.title}>采集参数</Text>
|
||
{configDirty && (
|
||
<View style={PS.dirtyBadge}>
|
||
<Text style={PS.dirtyTxt}>待下发</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
<ScrollView style={PS.scroll} keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
|
||
<ParamForm />
|
||
<View style={{ height: Spacing.md }} />
|
||
</ScrollView>
|
||
|
||
<TouchableOpacity
|
||
style={[
|
||
PS.setupBtn,
|
||
configDirty && PS.setupBtnDirty,
|
||
(busy || measuring) && PS.setupBtnDis,
|
||
]}
|
||
onPress={onSetup}
|
||
disabled={busy || measuring}
|
||
activeOpacity={0.8}
|
||
>
|
||
{busy
|
||
? <ActivityIndicator color={Colors.blue.fg} />
|
||
: <Text style={[PS.setupBtnTxt, configDirty && PS.setupBtnTxtDirty]}>
|
||
{measuring ? '采集中,不可下发' : '下 发 配 置'}
|
||
</Text>}
|
||
</TouchableOpacity>
|
||
</View>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
const PS = StyleSheet.create({
|
||
backdrop: { ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0,0,0,0.6)' },
|
||
sheet: {
|
||
position: 'absolute',
|
||
bottom: 0,
|
||
left: 0,
|
||
right: 0,
|
||
maxHeight: '85%',
|
||
backgroundColor: Colors.bg.surface,
|
||
borderTopLeftRadius: Radius.xl,
|
||
borderTopRightRadius: Radius.xl,
|
||
borderTopWidth: 1,
|
||
borderColor: Colors.bg.border,
|
||
paddingBottom: Spacing.xxl,
|
||
},
|
||
handle: {
|
||
width: 36, height: 4, borderRadius: 2,
|
||
backgroundColor: Colors.bg.border,
|
||
alignSelf: 'center',
|
||
marginTop: Spacing.sm, marginBottom: Spacing.sm,
|
||
},
|
||
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: Spacing.lg, paddingBottom: Spacing.sm, gap: Spacing.sm },
|
||
title: { color: Colors.text.secondary, fontSize: 14, fontWeight: '700', flex: 1 },
|
||
dirtyBadge: { backgroundColor: Colors.amber.bg, borderRadius: Radius.sm, paddingHorizontal: 7, paddingVertical: 2, borderWidth: 1, borderColor: Colors.amber.border },
|
||
dirtyTxt: { color: Colors.amber.fg, fontSize: 9, fontWeight: '700' },
|
||
scroll: { flex: 1 },
|
||
setupBtn: {
|
||
margin: Spacing.lg,
|
||
backgroundColor: Colors.bg.raised,
|
||
borderRadius: Radius.lg,
|
||
paddingVertical: 14,
|
||
alignItems: 'center',
|
||
borderWidth: 1,
|
||
borderColor: Colors.bg.border,
|
||
},
|
||
setupBtnDirty: { backgroundColor: Colors.blue.bg, borderColor: Colors.blue.fg },
|
||
setupBtnDis: { opacity: 0.4 },
|
||
setupBtnTxt: { color: Colors.text.muted, fontWeight: '700', fontSize: 14, letterSpacing: 3 },
|
||
setupBtnTxtDirty: { color: Colors.blue.fg },
|
||
});
|
||
|
||
// ── Control row ───────────────────────────────────────────────────────────────
|
||
|
||
function ControlRow({
|
||
deviceStatus,
|
||
busy,
|
||
frameId,
|
||
onContinuous,
|
||
onSingle,
|
||
onStop,
|
||
onSettings,
|
||
configDirty,
|
||
}: {
|
||
deviceStatus: 'idle' | 'running' | 'single';
|
||
busy: boolean;
|
||
frameId: number | undefined;
|
||
onContinuous: () => void;
|
||
onSingle: () => void;
|
||
onStop: () => void;
|
||
onSettings: () => void;
|
||
configDirty: boolean;
|
||
}) {
|
||
const idle = deviceStatus === 'idle';
|
||
const running = deviceStatus === 'running';
|
||
const single = deviceStatus === 'single';
|
||
|
||
return (
|
||
<View style={CR.row}>
|
||
{/* Primary action */}
|
||
{idle && (
|
||
<TouchableOpacity
|
||
style={[CR.primary, CR.green, !busy ? null : CR.dis]}
|
||
onPress={onContinuous}
|
||
disabled={busy}
|
||
activeOpacity={0.85}
|
||
>
|
||
{busy
|
||
? <ActivityIndicator color={Colors.green.fg} />
|
||
: <>
|
||
<Text style={CR.primaryIcon}>▶</Text>
|
||
<Text style={[CR.primaryTxt, { color: Colors.green.fg }]}>连 续 采 集</Text>
|
||
</>}
|
||
</TouchableOpacity>
|
||
)}
|
||
{running && (
|
||
<TouchableOpacity
|
||
style={[CR.primary, CR.red]}
|
||
onPress={onStop}
|
||
activeOpacity={0.85}
|
||
>
|
||
<Text style={CR.primaryIcon}>■</Text>
|
||
<Text style={[CR.primaryTxt, { color: Colors.red.fg }]}>停 止</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
{single && (
|
||
<View style={[CR.primary, CR.teal, CR.dis]}>
|
||
<ActivityIndicator color={Colors.teal.fg} size="small" />
|
||
<Text style={[CR.primaryTxt, { color: Colors.teal.fg }]}>单次采集中…</Text>
|
||
</View>
|
||
)}
|
||
|
||
{/* Secondary: single-shot button (idle only) */}
|
||
{idle && (
|
||
<TouchableOpacity
|
||
style={[CR.secondary, CR.tealBtn, !busy ? null : CR.dis]}
|
||
onPress={onSingle}
|
||
disabled={busy}
|
||
activeOpacity={0.85}
|
||
>
|
||
<Text style={CR.secondaryIcon}>◎</Text>
|
||
<Text style={[CR.secondaryTxt, { color: Colors.teal.fg }]}>单次</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
|
||
{/* Running indicator (running only) */}
|
||
{running && frameId !== undefined && (
|
||
<View style={CR.indicator}>
|
||
<View style={CR.runDot} />
|
||
<Text style={CR.indicatorTxt}>#{frameId}</Text>
|
||
</View>
|
||
)}
|
||
|
||
{/* Settings button — always present */}
|
||
<TouchableOpacity style={CR.settingsBtn} onPress={onSettings} activeOpacity={0.8}>
|
||
<Text style={[CR.settingsTxt, configDirty && CR.settingsDirty]}>⚙</Text>
|
||
{configDirty && <View style={CR.dirtyDot} />}
|
||
</TouchableOpacity>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const CR = StyleSheet.create({
|
||
row: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingHorizontal: Spacing.md,
|
||
paddingVertical: Spacing.sm,
|
||
gap: Spacing.sm,
|
||
borderTopWidth: 1,
|
||
borderTopColor: Colors.bg.border,
|
||
backgroundColor: Colors.bg.surface,
|
||
},
|
||
primary: {
|
||
flex: 2,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 8,
|
||
paddingVertical: 14,
|
||
borderRadius: Radius.lg,
|
||
borderWidth: 1,
|
||
},
|
||
green: { backgroundColor: Colors.green.bg, borderColor: Colors.green.border },
|
||
red: { backgroundColor: Colors.red.bg, borderColor: Colors.red.border },
|
||
teal: { backgroundColor: Colors.teal.bg, borderColor: Colors.teal.border },
|
||
dis: { opacity: 0.35 },
|
||
primaryIcon: { fontSize: 11, color: '#ffffff88' },
|
||
primaryTxt: { fontSize: 13, fontWeight: '700', letterSpacing: 1.5 },
|
||
|
||
secondary: {
|
||
flex: 1,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 5,
|
||
paddingVertical: 14,
|
||
borderRadius: Radius.lg,
|
||
borderWidth: 1,
|
||
},
|
||
tealBtn: { backgroundColor: Colors.teal.bg, borderColor: Colors.teal.border },
|
||
secondaryIcon:{ fontSize: 11, color: '#ffffff66' },
|
||
secondaryTxt: { fontSize: 12, fontWeight: '700' },
|
||
|
||
indicator: {
|
||
flex: 1,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 6,
|
||
paddingVertical: 14,
|
||
borderRadius: Radius.lg,
|
||
backgroundColor: Colors.green.bg,
|
||
borderWidth: 1,
|
||
borderColor: Colors.green.border,
|
||
},
|
||
runDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: Colors.green.fg },
|
||
indicatorTxt: { color: Colors.green.fg, fontSize: 12, fontWeight: '700', fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
|
||
|
||
settingsBtn: {
|
||
width: 44, height: 44,
|
||
borderRadius: Radius.md,
|
||
backgroundColor: Colors.bg.raised,
|
||
borderWidth: 1, borderColor: Colors.bg.border,
|
||
alignItems: 'center', justifyContent: 'center',
|
||
},
|
||
settingsTxt: { fontSize: 18, color: Colors.text.muted },
|
||
settingsDirty: { color: Colors.amber.fg },
|
||
dirtyDot: {
|
||
position: 'absolute', top: 6, right: 6,
|
||
width: 7, height: 7, borderRadius: 4,
|
||
backgroundColor: Colors.amber.fg,
|
||
borderWidth: 1, borderColor: Colors.bg.surface,
|
||
},
|
||
});
|
||
|
||
// ── Peak row ──────────────────────────────────────────────────────────────────
|
||
|
||
function PeakRow({ frame, visible, channelNum }: { frame: MeasurementFrame; visible: boolean[]; channelNum: number }) {
|
||
return (
|
||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={PK.scroll}>
|
||
<View style={PK.row}>
|
||
{frame.adcUV.map((ch, i) => {
|
||
if (i >= channelNum || !visible[i]) return null;
|
||
const pk = ch.reduce((m, v) => Math.max(m, Math.abs(v)), 0);
|
||
return (
|
||
<View key={i} style={PK.cell}>
|
||
<View style={[PK.dot, { backgroundColor: CHANNEL_COLORS[i] }]} />
|
||
<Text style={PK.label}>CH{i + 1}</Text>
|
||
<Text style={[PK.value, { color: CHANNEL_COLORS[i] + 'cc' }]}>{formatUV(pk)}</Text>
|
||
</View>
|
||
);
|
||
})}
|
||
</View>
|
||
</ScrollView>
|
||
);
|
||
}
|
||
|
||
const PK = StyleSheet.create({
|
||
scroll: { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: Colors.bg.divider },
|
||
row: { flexDirection: 'row', paddingHorizontal: Spacing.md, paddingVertical: 7, gap: 10 },
|
||
cell: { flexDirection: 'row', alignItems: 'center', gap: 4, backgroundColor: Colors.bg.surface, borderRadius: Radius.sm, paddingHorizontal: 8, paddingVertical: 4, borderWidth: 1, borderColor: Colors.bg.border },
|
||
dot: { width: 5, height: 5, borderRadius: 3 },
|
||
label: { color: Colors.text.ghost, fontSize: 9, fontWeight: '600' },
|
||
value: { fontSize: 11, fontWeight: '600', fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
|
||
});
|
||
|
||
// ── Frame summary bar ─────────────────────────────────────────────────────────
|
||
|
||
function FrameSummaryBar({ frame }: { frame: MeasurementFrame }) {
|
||
const m = frame.meta;
|
||
return (
|
||
<View style={FS.bar}>
|
||
<Text style={FS.txt}>
|
||
#{frame.frameId}
|
||
{' '}×{frame.accNum}叠
|
||
{m ? ` ${formatUtc(m.utc)}` : ''}
|
||
{m && m.latitude ? ` ${formatCoord(m.latitude, false)} ${formatCoord(m.longitude, true)}` : ''}
|
||
</Text>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const FS = StyleSheet.create({
|
||
bar: { paddingHorizontal: Spacing.md, paddingVertical: 5, backgroundColor: Colors.bg.void, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: Colors.bg.divider },
|
||
txt: { color: Colors.text.ghost, fontSize: 9, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
|
||
});
|
||
|
||
// ── Not connected placeholder ─────────────────────────────────────────────────
|
||
|
||
function NotConnected({ onConnect }: { onConnect: () => void }) {
|
||
return (
|
||
<View style={NC.root}>
|
||
<Text style={NC.icon}>◈</Text>
|
||
<Text style={NC.title}>未连接仪器</Text>
|
||
<Text style={NC.sub}>请先连接 TEM 接收机</Text>
|
||
<TouchableOpacity style={NC.btn} onPress={onConnect} activeOpacity={0.8}>
|
||
<Text style={NC.btnTxt}>连 接 设 备</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const NC = StyleSheet.create({
|
||
root: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 },
|
||
icon: { fontSize: 48, color: Colors.bg.border },
|
||
title: { color: Colors.text.muted, fontSize: 17, fontWeight: '700' },
|
||
sub: { color: Colors.text.ghost, fontSize: 12 },
|
||
btn: {
|
||
marginTop: Spacing.sm,
|
||
backgroundColor: Colors.blue.bg,
|
||
borderRadius: Radius.lg,
|
||
paddingVertical: 13,
|
||
paddingHorizontal: 32,
|
||
borderWidth: 1, borderColor: Colors.blue.fg,
|
||
},
|
||
btnTxt: { color: Colors.blue.fg, fontSize: 14, fontWeight: '700', letterSpacing: 2 },
|
||
});
|
||
|
||
// ── Main screen ───────────────────────────────────────────────────────────────
|
||
|
||
export default function WaveScreen() {
|
||
const { width: sw, height: sh } = useWindowDimensions();
|
||
const { status, showModal } = useConnectionStore();
|
||
const { config, configDirty } = useDeviceStore();
|
||
const { deviceStatus } = useDeviceStore();
|
||
const { busy, setup, startContinuous, startSingle, stop } = useDevice();
|
||
|
||
const frame = useDataStore((s) => s.currentFrame);
|
||
const sessionId = useDataStore((s) => s.sessionId);
|
||
const projectId = useDataStore((s) => s.projectId);
|
||
|
||
const [visible, setVisible] = useState(() => Array(6).fill(true));
|
||
const [logScale, setLogScale] = useState(true);
|
||
const [sheetVisible, setSheetVisible] = useState(false);
|
||
const [projectName, setProjectName] = useState<string | null>(null);
|
||
|
||
const connected = status === 'connected';
|
||
const channelNum = config.channelNum;
|
||
const CHART_H = Math.round(Math.min(280, sh * 0.36));
|
||
|
||
const data = useWaveform(visible);
|
||
|
||
useEffect(() => {
|
||
if (!projectId) { setProjectName(null); return; }
|
||
import('../../src/services/StorageService').then(({ listProjects }) =>
|
||
listProjects().then((ps) => setProjectName(ps.find((p) => p.projectId === projectId)?.name ?? null)),
|
||
);
|
||
}, [projectId]);
|
||
|
||
const toggleChannel = (idx: number) =>
|
||
setVisible((prev) => prev.map((v, i) => (i === idx ? !v : v)));
|
||
|
||
const handleSetup = async () => {
|
||
await setup();
|
||
setSheetVisible(false);
|
||
};
|
||
|
||
if (!connected) {
|
||
return (
|
||
<View style={S.root}>
|
||
<NotConnected onConnect={showModal} />
|
||
</View>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<View style={S.root}>
|
||
|
||
{/* ── Context bar: project · session ── */}
|
||
<View style={S.ctxBar}>
|
||
{projectName
|
||
? <Text style={S.ctxTxt} numberOfLines={1}>{projectName} · {sessionId}</Text>
|
||
: <Text style={S.ctxTxt} numberOfLines={1}>{sessionId}</Text>}
|
||
</View>
|
||
|
||
{/* ── Channel selector + LOG/LIN ── */}
|
||
<View style={S.toolbar}>
|
||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={S.chRow}>
|
||
{Array.from({ length: channelNum }, (_, i) => (
|
||
<TouchableOpacity
|
||
key={i}
|
||
style={[
|
||
S.chBtn,
|
||
visible[i] && { borderColor: CHANNEL_COLORS[i] + '99', backgroundColor: CHANNEL_COLORS[i] + '1a' },
|
||
]}
|
||
onPress={() => toggleChannel(i)}
|
||
activeOpacity={0.7}
|
||
>
|
||
<View style={[S.chDot, { backgroundColor: visible[i] ? CHANNEL_COLORS[i] : Colors.bg.border }]} />
|
||
<Text style={[S.chTxt, visible[i] && { color: CHANNEL_COLORS[i] }]}>CH{i + 1}</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</ScrollView>
|
||
<TouchableOpacity
|
||
style={[S.logBtn, !logScale && S.logBtnOn]}
|
||
onPress={() => setLogScale((v) => !v)}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Text style={[S.logTxt, !logScale && S.logTxtOn]}>{logScale ? 'LOG' : 'LIN'}</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
{/* ── Waveform chart ── */}
|
||
<View style={S.chartWrap}>
|
||
{data ? (
|
||
<WaveformChart
|
||
data={data}
|
||
visibleChannels={visible}
|
||
width={sw}
|
||
height={CHART_H}
|
||
logScale={logScale}
|
||
/>
|
||
) : (
|
||
<View style={[S.chartPlaceholder, { height: CHART_H }]}>
|
||
<Text style={S.placeholderTxt}>等待采样数据…</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* ── Peak values ── */}
|
||
{frame && <PeakRow frame={frame} visible={visible} channelNum={channelNum} />}
|
||
|
||
{/* ── Frame summary ── */}
|
||
{frame && <FrameSummaryBar frame={frame} />}
|
||
|
||
{/* ── Control row ── */}
|
||
<ControlRow
|
||
deviceStatus={deviceStatus}
|
||
busy={busy}
|
||
frameId={frame?.frameId}
|
||
onContinuous={startContinuous}
|
||
onSingle={startSingle}
|
||
onStop={stop}
|
||
onSettings={() => setSheetVisible(true)}
|
||
configDirty={configDirty}
|
||
/>
|
||
|
||
{/* ── Param sheet ── */}
|
||
<ParamSheet
|
||
visible={sheetVisible}
|
||
onClose={() => setSheetVisible(false)}
|
||
onSetup={handleSetup}
|
||
busy={busy}
|
||
configDirty={configDirty}
|
||
/>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const S = StyleSheet.create({
|
||
root: { flex: 1, backgroundColor: Colors.bg.base },
|
||
|
||
ctxBar: {
|
||
paddingHorizontal: Spacing.md,
|
||
paddingVertical: 4,
|
||
backgroundColor: Colors.bg.void,
|
||
borderBottomWidth: 1,
|
||
borderBottomColor: Colors.bg.divider,
|
||
},
|
||
ctxTxt: {
|
||
color: Colors.text.ghost,
|
||
fontSize: 9,
|
||
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
|
||
},
|
||
|
||
toolbar: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingRight: Spacing.sm,
|
||
borderBottomWidth: 1,
|
||
borderBottomColor: Colors.bg.divider,
|
||
},
|
||
chRow: { flexDirection: 'row', paddingHorizontal: Spacing.sm, paddingVertical: 6, gap: 5 },
|
||
chBtn: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 5,
|
||
borderRadius: Radius.md,
|
||
borderWidth: 1,
|
||
borderColor: Colors.bg.border,
|
||
},
|
||
chDot: { width: 6, height: 6, borderRadius: 3 },
|
||
chTxt: { color: Colors.text.ghost, fontSize: 10, fontWeight: '600' },
|
||
logBtn: {
|
||
paddingHorizontal: 9,
|
||
paddingVertical: 5,
|
||
borderRadius: Radius.sm,
|
||
borderWidth: 1,
|
||
borderColor: Colors.bg.border,
|
||
backgroundColor: Colors.bg.raised,
|
||
marginLeft: 4,
|
||
},
|
||
logBtnOn: { borderColor: Colors.blue.border, backgroundColor: Colors.blue.bg },
|
||
logTxt: { color: Colors.text.muted, fontSize: 10, fontWeight: '700', letterSpacing: 0.5 },
|
||
logTxtOn: { color: Colors.blue.fg },
|
||
|
||
chartWrap: { borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: Colors.bg.divider },
|
||
chartPlaceholder: {
|
||
backgroundColor: Colors.bg.surface,
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
},
|
||
placeholderTxt: { color: Colors.text.ghost, fontSize: 13 },
|
||
});
|