You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
602 lines
20 KiB
602 lines
20 KiB
import { Ionicons } from '@expo/vector-icons';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import { useRouter } from 'expo-router';
|
|
import { useEffect, useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
Alert,
|
|
Modal,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Switch,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { buildChecklistText } from '../src/api';
|
|
import { useOnboardingChecklist } from '../src/hooks';
|
|
import { shareText } from '../src/lib/shareFile';
|
|
import {colors, headerGradientShort, spacing} from '../src/theme';
|
|
import type { ChecklistGroup, OnboardingChecklist, OnboardingTip } from '../src/types';
|
|
|
|
/** 分组图标配色轮转(全部来自主题 token) */
|
|
const GROUP_ICON_THEME: Record<string, { bg: string; color: string }> = {
|
|
material: { bg: colors.primaryLight, color: colors.primary },
|
|
procedure: { bg: colors.infoLight, color: colors.info },
|
|
study: { bg: colors.successLight, color: colors.success },
|
|
life: { bg: colors.actionLight, color: colors.action },
|
|
daily: { bg: colors.primaryLight, color: colors.primaryText },
|
|
};
|
|
|
|
/* ===================== 子组件:顶部导航 ===================== */
|
|
|
|
function Header({ onBack, onShare }: { onBack: () => void; onShare: () => void }) {
|
|
return (
|
|
<View style={styles.header}>
|
|
<Pressable style={styles.headerBtn} onPress={onBack} hitSlop={8}>
|
|
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
|
|
</Pressable>
|
|
<Text style={styles.headerTitle}>入职准备清单</Text>
|
|
<Pressable style={styles.headerBtn} onPress={onShare} hitSlop={8}>
|
|
<Ionicons name="share-outline" size={22} color={colors.textPrimary} />
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:倒计时卡片 ===================== */
|
|
|
|
function CountdownCard({
|
|
daysLeft,
|
|
company,
|
|
position,
|
|
onboardingDate,
|
|
completed,
|
|
total,
|
|
}: {
|
|
daysLeft: number;
|
|
company: string;
|
|
position: string;
|
|
onboardingDate: string;
|
|
completed: number;
|
|
total: number;
|
|
}) {
|
|
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
return (
|
|
<LinearGradient
|
|
colors={headerGradientShort}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={styles.countdownCard}
|
|
>
|
|
<View style={styles.countdownTop}>
|
|
<View>
|
|
<Text style={styles.countdownLabel}>入职倒计时</Text>
|
|
<View style={styles.countdownValueRow}>
|
|
<Text style={styles.countdownValue}>{daysLeft}</Text>
|
|
<Text style={styles.countdownUnit}>天</Text>
|
|
</View>
|
|
<Text style={styles.countdownCompany}>
|
|
{company} · {position}
|
|
</Text>
|
|
<Text style={styles.countdownDate}>入职日期 {onboardingDate}</Text>
|
|
</View>
|
|
<View style={styles.countdownIcon}>
|
|
<Ionicons name="briefcase" size={32} color={colors.primary} />
|
|
</View>
|
|
</View>
|
|
<View style={styles.countdownProgress}>
|
|
<View style={styles.countdownProgressHead}>
|
|
<Text style={styles.countdownProgressLabel}>总体进度</Text>
|
|
<Text style={styles.countdownProgressValue}>
|
|
{completed}/{total}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.countdownTrack}>
|
|
<View style={[styles.countdownFill, { width: `${percent}%` }]} />
|
|
</View>
|
|
</View>
|
|
</LinearGradient>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:清单分组 ===================== */
|
|
|
|
function ChecklistGroupCard({
|
|
group,
|
|
onToggleItem,
|
|
onRemoveItem,
|
|
togglingId,
|
|
expanded,
|
|
onToggleExpand,
|
|
}: {
|
|
group: ChecklistGroup;
|
|
onToggleItem: (itemId: string, done: boolean) => void;
|
|
onRemoveItem: (itemId: string) => void;
|
|
togglingId: string | null;
|
|
expanded: boolean;
|
|
onToggleExpand: () => void;
|
|
}) {
|
|
const theme = GROUP_ICON_THEME[group.id] ?? GROUP_ICON_THEME.material;
|
|
const doneCount = group.items.filter((i) => i.done).length;
|
|
// 只有「我的自定义待办」这一组可以删条目:模板待办删了下次进页面还会回来
|
|
const removable = group.id === 'custom';
|
|
return (
|
|
<View style={styles.groupCard}>
|
|
<Pressable style={styles.groupHead} onPress={onToggleExpand}>
|
|
<View style={[styles.groupIcon, { backgroundColor: theme.bg }]}>
|
|
<Ionicons name={group.icon as keyof typeof Ionicons.glyphMap} size={20} color={theme.color} />
|
|
</View>
|
|
<View style={styles.groupTitleWrap}>
|
|
<Text style={styles.groupTitle}>{group.title}</Text>
|
|
<Text style={styles.groupCount}>
|
|
{doneCount}/{group.items.length}
|
|
</Text>
|
|
</View>
|
|
<Ionicons
|
|
name={expanded ? 'chevron-up' : 'chevron-down'}
|
|
size={20}
|
|
color={colors.textHint}
|
|
/>
|
|
</Pressable>
|
|
{expanded && (
|
|
<View style={styles.groupBody}>
|
|
{group.items.map((item) => (
|
|
<Pressable
|
|
key={item.id}
|
|
style={styles.itemRow}
|
|
onPress={() => onToggleItem(item.id, !item.done)}
|
|
disabled={togglingId === item.id}
|
|
>
|
|
<Ionicons
|
|
name={item.done ? 'checkbox' : 'square-outline'}
|
|
size={20}
|
|
color={item.done ? colors.success : colors.borderStrong}
|
|
/>
|
|
<Text style={[styles.itemTitle, item.done && styles.itemTitleDone]}>
|
|
{item.title}
|
|
</Text>
|
|
{item.required && !item.done && (
|
|
<View style={styles.requiredTag}>
|
|
<Text style={styles.requiredText}>必填</Text>
|
|
</View>
|
|
)}
|
|
{removable && (
|
|
<Pressable onPress={() => onRemoveItem(item.id)} hitSlop={8}>
|
|
<Ionicons name="close-circle-outline" size={18} color={colors.textHint} />
|
|
</Pressable>
|
|
)}
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:每日提醒 ===================== */
|
|
|
|
function DailyReminderCard({
|
|
enabled,
|
|
time,
|
|
onToggle,
|
|
}: {
|
|
enabled: boolean;
|
|
time: string;
|
|
onToggle: (v: boolean) => void;
|
|
}) {
|
|
return (
|
|
<View style={styles.card}>
|
|
<View style={styles.reminderRow}>
|
|
<View style={styles.reminderText}>
|
|
<Text style={styles.reminderTitle}>入职每日提醒</Text>
|
|
<Text style={styles.reminderDesc}>每天 {time} 提醒你完成待办</Text>
|
|
</View>
|
|
<Switch
|
|
value={enabled}
|
|
onValueChange={onToggle}
|
|
trackColor={{ false: colors.border, true: colors.primaryBorder }}
|
|
thumbColor={enabled ? colors.primary : colors.white}
|
|
/>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:入职小贴士 ===================== */
|
|
|
|
function TipsCard({ tips }: { tips: OnboardingTip[] }) {
|
|
return (
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>入职小贴士</Text>
|
|
{tips.map((tip) => (
|
|
<View key={tip.id} style={styles.tipRow}>
|
|
<Ionicons name={tip.icon as keyof typeof Ionicons.glyphMap} size={18} color={colors.primary} />
|
|
<View style={styles.tipBody}>
|
|
<Text style={styles.tipTitle}>{tip.title}</Text>
|
|
<Text style={styles.tipDesc}>{tip.desc}</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:四态 ===================== */
|
|
|
|
function Loading() {
|
|
return (
|
|
<View style={styles.center}>
|
|
<ActivityIndicator size="large" color={colors.primary} />
|
|
<Text style={styles.tip}>正在加载清单…</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function ErrorState({ onRetry }: { onRetry: () => void }) {
|
|
return (
|
|
<View style={styles.center}>
|
|
<Ionicons name="list-outline" size={40} color={colors.textHint} />
|
|
<Text style={styles.tip}>清单加载失败</Text>
|
|
<Pressable style={styles.retryBtn} onPress={onRetry} hitSlop={6}>
|
|
<Text style={styles.retryText}>点击重试</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 页面 ===================== */
|
|
|
|
export default function OnboardingScreen() {
|
|
const router = useRouter();
|
|
const { checklist, loading, error, refresh, toggle, togglingId, addItem, removeItem, mutating } =
|
|
useOnboardingChecklist();
|
|
|
|
/** 本地清单状态(勾选后即时更新,避免等待接口往返) */
|
|
const [localChecklist, setLocalChecklist] = useState<OnboardingChecklist | null>(null);
|
|
const [expandedIds, setExpandedIds] = useState<string[]>([]);
|
|
const [reminderEnabled, setReminderEnabled] = useState(false);
|
|
/** 「添加自定义待办」弹窗 */
|
|
const [addVisible, setAddVisible] = useState(false);
|
|
const [draftTitle, setDraftTitle] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (checklist && !localChecklist) {
|
|
setLocalChecklist(checklist);
|
|
setExpandedIds(checklist.groups.filter((g) => g.expanded).map((g) => g.id));
|
|
setReminderEnabled(checklist.dailyReminder.enabled);
|
|
}
|
|
}, [checklist, localChecklist]);
|
|
|
|
const display = localChecklist ?? checklist;
|
|
|
|
const handleToggleItem = async (itemId: string, done: boolean) => {
|
|
setLocalChecklist((prev) => {
|
|
if (!prev) return prev;
|
|
const groups = prev.groups.map((g) => ({
|
|
...g,
|
|
items: g.items.map((item) => (item.id === itemId ? { ...item, done } : item)),
|
|
}));
|
|
const completed = groups.reduce((sum, g) => sum + g.items.filter((i) => i.done).length, 0);
|
|
return { ...prev, groups, completed };
|
|
});
|
|
await toggle(itemId, done);
|
|
};
|
|
|
|
/** 添加自定义待办:接口层负责落库并返回新清单,页面直接换成返回值 */
|
|
const handleAddItem = async () => {
|
|
try {
|
|
const updated = await addItem(draftTitle);
|
|
setLocalChecklist(updated);
|
|
setDraftTitle('');
|
|
setAddVisible(false);
|
|
} catch (e) {
|
|
Alert.alert('添加失败', e instanceof Error ? e.message : '请稍后重试');
|
|
}
|
|
};
|
|
|
|
/** 删除自定义待办(模板条目接口层会拒绝) */
|
|
const handleRemoveItem = async (itemId: string) => {
|
|
try {
|
|
const updated = await removeItem(itemId);
|
|
setLocalChecklist(updated);
|
|
} catch (e) {
|
|
Alert.alert('删除失败', e instanceof Error ? e.message : '请稍后重试');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 导出 / 分享清单:把清单拼成文本交给系统分享面板。
|
|
* `ok` 只代表面板已拉起,不能据此提示「分享成功」(见 lib/shareFile 的说明)。
|
|
*/
|
|
const handleExport = async () => {
|
|
if (!display) return;
|
|
const outcome = await shareText(buildChecklistText(display), '入职准备清单');
|
|
if (!outcome.ok) Alert.alert('无法导出', outcome.message);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onShare={() => void handleExport()} />
|
|
<Loading />
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
if (error || !display) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onShare={() => void handleExport()} />
|
|
<ErrorState onRetry={refresh} />
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onShare={() => void handleExport()} />
|
|
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
|
|
<CountdownCard
|
|
daysLeft={display.daysLeft}
|
|
company={display.company}
|
|
position={display.position}
|
|
onboardingDate={display.onboardingDate}
|
|
completed={display.completed}
|
|
total={display.total}
|
|
/>
|
|
|
|
{display.groups.map((group) => (
|
|
<ChecklistGroupCard
|
|
key={group.id}
|
|
group={group}
|
|
onToggleItem={handleToggleItem}
|
|
onRemoveItem={handleRemoveItem}
|
|
togglingId={togglingId}
|
|
expanded={expandedIds.includes(group.id)}
|
|
onToggleExpand={() =>
|
|
setExpandedIds((prev) =>
|
|
prev.includes(group.id)
|
|
? prev.filter((id) => id !== group.id)
|
|
: [...prev, group.id],
|
|
)
|
|
}
|
|
/>
|
|
))}
|
|
|
|
<DailyReminderCard
|
|
enabled={reminderEnabled}
|
|
time={display.dailyReminder.time}
|
|
onToggle={setReminderEnabled}
|
|
/>
|
|
|
|
<TipsCard tips={display.tips} />
|
|
</ScrollView>
|
|
|
|
<View style={styles.footer}>
|
|
<Pressable
|
|
style={[styles.addBtn, mutating && styles.footerBtnDisabled]}
|
|
onPress={() => setAddVisible(true)}
|
|
disabled={mutating}
|
|
>
|
|
<Ionicons name="add" size={20} color={colors.primary} />
|
|
<Text style={styles.addBtnText}>添加自定义待办</Text>
|
|
</Pressable>
|
|
<Pressable style={styles.exportBtn} onPress={() => void handleExport()}>
|
|
<Text style={styles.exportBtnText}>导出清单</Text>
|
|
</Pressable>
|
|
</View>
|
|
|
|
{/* 添加自定义待办:Alert.prompt 只有 iOS 有,这里用自绘弹窗(与岗位评价回复一致) */}
|
|
<Modal visible={addVisible} transparent animationType="fade" onRequestClose={() => setAddVisible(false)}>
|
|
<View style={styles.modalMask}>
|
|
<View style={styles.modalCard}>
|
|
<Text style={styles.modalTitle}>添加自定义待办</Text>
|
|
<TextInput
|
|
style={styles.modalInput}
|
|
value={draftTitle}
|
|
onChangeText={setDraftTitle}
|
|
placeholder="例如:办理落户材料"
|
|
placeholderTextColor={colors.textHint}
|
|
maxLength={30}
|
|
autoFocus
|
|
/>
|
|
<View style={styles.modalActions}>
|
|
<Pressable style={styles.modalCancel} onPress={() => setAddVisible(false)}>
|
|
<Text style={styles.modalCancelText}>取消</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
style={[styles.modalConfirm, (draftTitle.trim() === '' || mutating) && styles.footerBtnDisabled]}
|
|
onPress={() => void handleAddItem()}
|
|
disabled={draftTitle.trim() === '' || mutating}
|
|
>
|
|
<Text style={styles.modalConfirmText}>{mutating ? '添加中…' : '添加'}</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: colors.pageBg },
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 12,
|
|
backgroundColor: colors.white,
|
|
},
|
|
headerBtn: { width: 40, alignItems: 'center' },
|
|
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
|
|
body: { paddingHorizontal: spacing.card, paddingBottom: 100, paddingTop: 12 },
|
|
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
|
|
tip: { fontSize: 14, color: colors.textSecondary },
|
|
retryBtn: {
|
|
paddingHorizontal: 18,
|
|
paddingVertical: spacing.card,
|
|
borderRadius: 16,
|
|
backgroundColor: colors.primaryLight,
|
|
},
|
|
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
|
|
countdownCard: { borderRadius: spacing.card, padding: spacing.card, marginBottom: 8 },
|
|
countdownTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
|
countdownLabel: { fontSize: 13, color: colors.textInverse, opacity: 0.9 },
|
|
countdownValueRow: { flexDirection: 'row', alignItems: 'baseline', marginTop: 6 },
|
|
countdownValue: { fontSize: 44, fontWeight: '800', color: colors.textInverse },
|
|
countdownUnit: { fontSize: 15, color: colors.textInverse, marginLeft: 4 },
|
|
countdownCompany: { fontSize: 14, color: colors.textInverse, marginTop: 8, fontWeight: '600' },
|
|
countdownDate: { fontSize: 12, color: colors.textInverse, opacity: 0.85, marginTop: 2 },
|
|
countdownIcon: {
|
|
width: 60,
|
|
height: 60,
|
|
borderRadius: 30,
|
|
backgroundColor: colors.white,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
countdownProgress: { marginTop: 18 },
|
|
countdownProgressHead: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
marginBottom: 8,
|
|
},
|
|
countdownProgressLabel: { fontSize: 13, color: colors.textInverse, opacity: 0.9 },
|
|
countdownProgressValue: { fontSize: 13, fontWeight: '700', color: colors.textInverse },
|
|
countdownTrack: {
|
|
height: 8,
|
|
borderRadius: 4,
|
|
backgroundColor: colors.onBrandTrack,
|
|
overflow: 'hidden',
|
|
},
|
|
countdownFill: { height: 8, borderRadius: 4, backgroundColor: colors.white },
|
|
groupCard: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: spacing.card,
|
|
marginBottom: 8,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
overflow: 'hidden',
|
|
},
|
|
groupHead: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
padding: 14,
|
|
},
|
|
groupIcon: {
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 18,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
groupTitleWrap: { flex: 1, marginLeft: 12 },
|
|
groupTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
|
|
groupCount: { fontSize: 12, color: colors.textHint, marginTop: 2 },
|
|
groupBody: { paddingHorizontal: 14, paddingBottom: 12 },
|
|
itemRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: 10,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
itemTitle: { flex: 1, fontSize: 14, color: colors.textPrimary, marginLeft: 10 },
|
|
itemTitleDone: { textDecorationLine: 'line-through', color: colors.textHint },
|
|
requiredTag: {
|
|
backgroundColor: colors.dangerLight,
|
|
borderRadius: spacing.card,
|
|
paddingHorizontal: 6,
|
|
paddingVertical: 2,
|
|
},
|
|
requiredText: { fontSize: 10, color: colors.dangerText, fontWeight: '600' },
|
|
card: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: spacing.card,
|
|
padding: spacing.card,
|
|
marginTop: 8,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
cardTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 12 },
|
|
reminderRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
reminderText: { flex: 1 },
|
|
reminderTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
|
|
reminderDesc: { fontSize: 12, color: colors.textHint, marginTop: 2 },
|
|
tipRow: {
|
|
flexDirection: 'row',
|
|
paddingVertical: 10,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
tipBody: { flex: 1, marginLeft: 10 },
|
|
tipTitle: { fontSize: 14, fontWeight: '600', color: colors.textPrimary },
|
|
tipDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 2, lineHeight: 18 },
|
|
footer: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
flexDirection: 'row',
|
|
paddingHorizontal: spacing.framework,
|
|
paddingVertical: 12,
|
|
backgroundColor: colors.white,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
addBtn: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: colors.white,
|
|
borderRadius: 24,
|
|
paddingVertical: 14,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
marginRight: 12,
|
|
},
|
|
addBtnText: { fontSize: 14, fontWeight: '700', color: colors.primary, marginLeft: 4 },
|
|
exportBtn: {
|
|
flex: 1,
|
|
backgroundColor: colors.primaryBtn,
|
|
borderRadius: 24,
|
|
paddingVertical: 14,
|
|
alignItems: 'center',
|
|
},
|
|
exportBtnText: { fontSize: 15, fontWeight: '700', color: colors.white },
|
|
footerBtnDisabled: { opacity: 0.5 },
|
|
modalMask: {
|
|
flex: 1,
|
|
backgroundColor: colors.scrim,
|
|
justifyContent: 'center',
|
|
paddingHorizontal: 32,
|
|
},
|
|
modalCard: { backgroundColor: colors.white, borderRadius: spacing.card, padding: spacing.card },
|
|
modalTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary },
|
|
modalInput: {
|
|
marginTop: 14,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
borderRadius: 10,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 10,
|
|
fontSize: 14,
|
|
color: colors.textPrimary,
|
|
},
|
|
modalActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: 12, marginTop: 18 },
|
|
modalCancel: { paddingHorizontal: spacing.framework, paddingVertical: spacing.card },
|
|
modalCancelText: { fontSize: 14, color: colors.textSecondary },
|
|
modalConfirm: {
|
|
backgroundColor: colors.primaryBtn,
|
|
borderRadius: 18,
|
|
paddingHorizontal: 20,
|
|
paddingVertical: spacing.card,
|
|
},
|
|
modalConfirmText: { fontSize: 14, fontWeight: '700', color: colors.white },
|
|
});
|
|
|