mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-06 00:07:28 +00:00
789 lines
24 KiB
TypeScript
789 lines
24 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
Modal,
|
|
View,
|
|
Text,
|
|
ScrollView,
|
|
TextInput,
|
|
Pressable,
|
|
StyleSheet,
|
|
Switch,
|
|
Platform,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import DateTimePicker from '@react-native-community/datetimepicker';
|
|
import {
|
|
X,
|
|
Calendar as CalendarIcon,
|
|
Clock,
|
|
MapPin,
|
|
Video,
|
|
Repeat,
|
|
Bell,
|
|
Plus,
|
|
Users,
|
|
Trash2,
|
|
AlignLeft,
|
|
} from 'lucide-react-native';
|
|
import { addHours, addMinutes, format } from 'date-fns';
|
|
import type {
|
|
Calendar,
|
|
CalendarEvent,
|
|
Participant,
|
|
EventLocation,
|
|
VirtualLocation,
|
|
} from '../../api/types';
|
|
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
|
import { useColors } from '../../theme/colors';
|
|
import {
|
|
buildAllDayDuration,
|
|
getCalendarColor,
|
|
getEventEndDate,
|
|
getEventStartDate,
|
|
getPrimaryCalendarId,
|
|
} from '../../lib/calendar-utils';
|
|
import {
|
|
alertsToReminders,
|
|
remindersToAlerts,
|
|
formatReminder,
|
|
REMINDER_PRESETS,
|
|
type Reminder,
|
|
} from '../../lib/calendar-alerts';
|
|
import { Button } from '..';
|
|
import { ParticipantInput } from './ParticipantInput';
|
|
|
|
type RecurrenceOption = 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly';
|
|
|
|
const RECURRENCE_OPTIONS: { value: RecurrenceOption; label: string }[] = [
|
|
{ value: 'none', label: 'Does not repeat' },
|
|
{ value: 'daily', label: 'Daily' },
|
|
{ value: 'weekly', label: 'Weekly' },
|
|
{ value: 'monthly', label: 'Monthly' },
|
|
{ value: 'yearly', label: 'Yearly' },
|
|
];
|
|
|
|
interface EventModalProps {
|
|
visible: boolean;
|
|
event?: CalendarEvent | null;
|
|
calendars: Calendar[];
|
|
defaultDate?: Date;
|
|
defaultCalendarId?: string;
|
|
onSave: (data: Partial<CalendarEvent>, calendarId: string) => void | Promise<void>;
|
|
onDelete?: (event: CalendarEvent) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
function nextHalfHour(d: Date): Date {
|
|
const date = new Date(d);
|
|
const min = date.getMinutes();
|
|
const add = min < 30 ? 30 - min : 60 - min;
|
|
date.setMinutes(date.getMinutes() + add, 0, 0);
|
|
return date;
|
|
}
|
|
|
|
function buildDuration(start: Date, end: Date): string {
|
|
const diffMin = Math.max(0, Math.floor((end.getTime() - start.getTime()) / 60000));
|
|
const days = Math.floor(diffMin / (24 * 60));
|
|
const hours = Math.floor((diffMin % (24 * 60)) / 60);
|
|
const mins = diffMin % 60;
|
|
let dur = 'P';
|
|
if (days > 0) dur += `${days}D`;
|
|
if (hours > 0 || mins > 0) {
|
|
dur += 'T';
|
|
if (hours > 0) dur += `${hours}H`;
|
|
if (mins > 0) dur += `${mins}M`;
|
|
}
|
|
return dur === 'P' ? 'PT0M' : dur;
|
|
}
|
|
|
|
function detectRecurrence(event: CalendarEvent | null | undefined): RecurrenceOption {
|
|
const rule = event?.recurrenceRules?.[0];
|
|
if (!rule) return 'none';
|
|
const f = rule.frequency.toLowerCase();
|
|
if (f === 'daily' || f === 'weekly' || f === 'monthly' || f === 'yearly') {
|
|
return f as RecurrenceOption;
|
|
}
|
|
return 'none';
|
|
}
|
|
|
|
function detectLocation(event: CalendarEvent | null | undefined): string {
|
|
const locations = event?.locations;
|
|
if (!locations) return '';
|
|
const first = Object.values(locations)[0];
|
|
return first?.name || '';
|
|
}
|
|
|
|
function detectVideoUrl(event: CalendarEvent | null | undefined): string {
|
|
const v = event?.virtualLocations;
|
|
if (!v) return '';
|
|
const first = Object.values(v)[0];
|
|
return first?.uri || '';
|
|
}
|
|
|
|
function buildLocations(name: string): Record<string, EventLocation> | undefined {
|
|
const trimmed = name.trim();
|
|
if (!trimmed) return undefined;
|
|
return { 'loc-1': { '@type': 'Location', name: trimmed } };
|
|
}
|
|
|
|
function buildVirtualLocations(uri: string): Record<string, VirtualLocation> | undefined {
|
|
const trimmed = uri.trim();
|
|
if (!trimmed) return undefined;
|
|
return { 'vloc-1': { '@type': 'VirtualLocation', name: 'Video call', uri: trimmed } };
|
|
}
|
|
|
|
function recurrenceFromOption(opt: RecurrenceOption): CalendarEvent['recurrenceRules'] | undefined {
|
|
if (opt === 'none') return undefined;
|
|
return [{ frequency: opt, interval: 1 }];
|
|
}
|
|
|
|
export function EventModal({
|
|
visible,
|
|
event,
|
|
calendars,
|
|
defaultDate,
|
|
defaultCalendarId,
|
|
onSave,
|
|
onDelete,
|
|
onClose,
|
|
}: EventModalProps) {
|
|
const c = useColors();
|
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
|
const isEdit = !!event;
|
|
|
|
const [title, setTitle] = React.useState('');
|
|
const [description, setDescription] = React.useState('');
|
|
const [allDay, setAllDay] = React.useState(false);
|
|
const [start, setStart] = React.useState<Date>(() => nextHalfHour(defaultDate ?? new Date()));
|
|
const [end, setEnd] = React.useState<Date>(() => addHours(nextHalfHour(defaultDate ?? new Date()), 1));
|
|
const [calendarId, setCalendarId] = React.useState<string>(defaultCalendarId || calendars[0]?.id || '');
|
|
const [participants, setParticipants] = React.useState<Record<string, Participant>>({});
|
|
const [recurrence, setRecurrence] = React.useState<RecurrenceOption>('none');
|
|
const [reminders, setReminders] = React.useState<Reminder[]>([]);
|
|
const [location, setLocation] = React.useState('');
|
|
const [videoUrl, setVideoUrl] = React.useState('');
|
|
const [recurrenceOpen, setRecurrenceOpen] = React.useState(false);
|
|
const [reminderPickerOpen, setReminderPickerOpen] = React.useState(false);
|
|
const [calendarOpen, setCalendarOpen] = React.useState(false);
|
|
const [showStartDate, setShowStartDate] = React.useState(false);
|
|
const [showStartTime, setShowStartTime] = React.useState(false);
|
|
const [showEndDate, setShowEndDate] = React.useState(false);
|
|
const [showEndTime, setShowEndTime] = React.useState(false);
|
|
const [saving, setSaving] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
if (!visible) return;
|
|
if (event) {
|
|
setTitle(event.title || '');
|
|
setDescription(event.description || '');
|
|
setAllDay(!!event.showWithoutTime);
|
|
setStart(getEventStartDate(event));
|
|
setEnd(getEventEndDate(event));
|
|
setCalendarId(getPrimaryCalendarId(event) || calendars[0]?.id || '');
|
|
setParticipants(event.participants || {});
|
|
setRecurrence(detectRecurrence(event));
|
|
setReminders(alertsToReminders(event.alerts));
|
|
setLocation(detectLocation(event));
|
|
setVideoUrl(detectVideoUrl(event));
|
|
} else {
|
|
const d = defaultDate ? nextHalfHour(defaultDate) : nextHalfHour(new Date());
|
|
setTitle('');
|
|
setDescription('');
|
|
setAllDay(false);
|
|
setStart(d);
|
|
setEnd(addHours(d, 1));
|
|
setCalendarId(defaultCalendarId || calendars[0]?.id || '');
|
|
setParticipants({});
|
|
setRecurrence('none');
|
|
setReminders([]);
|
|
setLocation('');
|
|
setVideoUrl('');
|
|
}
|
|
}, [visible, event, defaultDate, defaultCalendarId, calendars]);
|
|
|
|
React.useEffect(() => {
|
|
if (allDay) {
|
|
const s = new Date(start);
|
|
s.setHours(0, 0, 0, 0);
|
|
const e = new Date(end);
|
|
e.setHours(0, 0, 0, 0);
|
|
if (e <= s) e.setDate(s.getDate() + 1);
|
|
setStart(s);
|
|
setEnd(e);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [allDay]);
|
|
|
|
const updateStart = (next: Date) => {
|
|
const diff = end.getTime() - start.getTime();
|
|
setStart(next);
|
|
setEnd(new Date(next.getTime() + Math.max(diff, 15 * 60_000)));
|
|
};
|
|
|
|
const updateEnd = (next: Date) => {
|
|
if (next <= start) {
|
|
setEnd(addMinutes(start, 15));
|
|
} else {
|
|
setEnd(next);
|
|
}
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!title.trim() || !calendarId) return;
|
|
setSaving(true);
|
|
try {
|
|
const data: Partial<CalendarEvent> = {
|
|
title: title.trim(),
|
|
description: description.trim() || undefined,
|
|
showWithoutTime: allDay || undefined,
|
|
start: allDay
|
|
? format(start, "yyyy-MM-dd'T'00:00:00")
|
|
: format(start, "yyyy-MM-dd'T'HH:mm:ss"),
|
|
duration: allDay ? buildAllDayDuration(start, end) : buildDuration(start, end),
|
|
participants: Object.keys(participants).length > 0 ? participants : undefined,
|
|
recurrenceRules: recurrenceFromOption(recurrence),
|
|
alerts: remindersToAlerts(reminders),
|
|
useDefaultAlerts: reminders.length > 0 ? false : undefined,
|
|
locations: buildLocations(location),
|
|
virtualLocations: buildVirtualLocations(videoUrl),
|
|
};
|
|
await onSave(data, calendarId);
|
|
onClose();
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = () => {
|
|
if (event && onDelete) onDelete(event);
|
|
};
|
|
|
|
const selectedCalendar = calendars.find((c) => c.id === calendarId);
|
|
const recurrenceLabel = RECURRENCE_OPTIONS.find((o) => o.value === recurrence)?.label || 'None';
|
|
|
|
const addReminder = (minutesBefore: number) => {
|
|
setReminderPickerOpen(false);
|
|
setReminders((prev) =>
|
|
prev.some((r) => r.minutesBefore === minutesBefore)
|
|
? prev
|
|
: [...prev, { minutesBefore }].sort((a, b) => b.minutesBefore - a.minutesBefore),
|
|
);
|
|
};
|
|
const removeReminder = (minutesBefore: number) => {
|
|
setReminders((prev) => prev.filter((r) => r.minutesBefore !== minutesBefore));
|
|
};
|
|
const availablePresets = REMINDER_PRESETS.filter(
|
|
(p) => !reminders.some((r) => r.minutesBefore === p),
|
|
);
|
|
|
|
return (
|
|
<Modal
|
|
visible={visible}
|
|
animationType="slide"
|
|
presentationStyle="fullScreen"
|
|
onRequestClose={onClose}
|
|
>
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
<View style={styles.header}>
|
|
<Pressable onPress={onClose} hitSlop={8} style={styles.headerBtn}>
|
|
<X size={20} color={c.text} />
|
|
</Pressable>
|
|
<Text style={styles.headerTitle}>{isEdit ? 'Edit event' : 'New event'}</Text>
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
onPress={handleSave}
|
|
disabled={!title.trim() || !calendarId}
|
|
loading={saving}
|
|
>
|
|
Save
|
|
</Button>
|
|
</View>
|
|
|
|
<ScrollView
|
|
style={styles.body}
|
|
contentContainerStyle={styles.bodyContent}
|
|
keyboardShouldPersistTaps="handled"
|
|
>
|
|
<TextInput
|
|
value={title}
|
|
onChangeText={setTitle}
|
|
placeholder="Title"
|
|
placeholderTextColor={c.textMuted}
|
|
style={styles.titleInput}
|
|
/>
|
|
|
|
<Section
|
|
icon={<CalendarIcon size={16} color={c.textMuted} />}
|
|
label="Calendar"
|
|
>
|
|
<Pressable
|
|
style={styles.fieldButton}
|
|
onPress={() => setCalendarOpen((v) => !v)}
|
|
>
|
|
<View style={styles.calendarRow}>
|
|
{selectedCalendar && (
|
|
<View
|
|
style={[
|
|
styles.calendarSwatch,
|
|
{ backgroundColor: getCalendarColor(selectedCalendar) },
|
|
]}
|
|
/>
|
|
)}
|
|
<Text style={styles.fieldText}>
|
|
{selectedCalendar?.name || 'Pick a calendar'}
|
|
</Text>
|
|
</View>
|
|
</Pressable>
|
|
{calendarOpen && (
|
|
<View style={styles.popover}>
|
|
{calendars.map((c) => (
|
|
<Pressable
|
|
key={c.id}
|
|
style={[
|
|
styles.popoverRow,
|
|
c.id === calendarId && styles.popoverRowActive,
|
|
]}
|
|
onPress={() => {
|
|
setCalendarId(c.id);
|
|
setCalendarOpen(false);
|
|
}}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.calendarSwatch,
|
|
{ backgroundColor: getCalendarColor(c) },
|
|
]}
|
|
/>
|
|
<Text style={styles.popoverRowText}>{c.name}</Text>
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
)}
|
|
</Section>
|
|
|
|
<Section icon={<Clock size={16} color={c.textMuted} />} label="Time">
|
|
<View style={styles.allDayRow}>
|
|
<Text style={styles.fieldText}>All-day</Text>
|
|
<Switch
|
|
value={allDay}
|
|
onValueChange={setAllDay}
|
|
thumbColor={allDay ? c.primary : c.textMuted}
|
|
trackColor={{ false: c.surface, true: c.primaryBg }}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.dateRow}>
|
|
<Text style={styles.dateLabel}>Starts</Text>
|
|
<View style={styles.datePickers}>
|
|
<Pressable
|
|
style={styles.dateBtn}
|
|
onPress={() => setShowStartDate(true)}
|
|
>
|
|
<Text style={styles.dateText}>{format(start, 'EEE, MMM d')}</Text>
|
|
</Pressable>
|
|
{!allDay && (
|
|
<Pressable
|
|
style={styles.dateBtn}
|
|
onPress={() => setShowStartTime(true)}
|
|
>
|
|
<Text style={styles.dateText}>{format(start, 'HH:mm')}</Text>
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.dateRow}>
|
|
<Text style={styles.dateLabel}>Ends</Text>
|
|
<View style={styles.datePickers}>
|
|
<Pressable
|
|
style={styles.dateBtn}
|
|
onPress={() => setShowEndDate(true)}
|
|
>
|
|
<Text style={styles.dateText}>{format(end, 'EEE, MMM d')}</Text>
|
|
</Pressable>
|
|
{!allDay && (
|
|
<Pressable
|
|
style={styles.dateBtn}
|
|
onPress={() => setShowEndTime(true)}
|
|
>
|
|
<Text style={styles.dateText}>{format(end, 'HH:mm')}</Text>
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</Section>
|
|
|
|
<Section icon={<Repeat size={16} color={c.textMuted} />} label="Repeat">
|
|
<Pressable
|
|
style={styles.fieldButton}
|
|
onPress={() => setRecurrenceOpen((v) => !v)}
|
|
>
|
|
<Text style={styles.fieldText}>{recurrenceLabel}</Text>
|
|
</Pressable>
|
|
{recurrenceOpen && (
|
|
<View style={styles.popover}>
|
|
{RECURRENCE_OPTIONS.map((opt) => (
|
|
<Pressable
|
|
key={opt.value}
|
|
style={[
|
|
styles.popoverRow,
|
|
opt.value === recurrence && styles.popoverRowActive,
|
|
]}
|
|
onPress={() => {
|
|
setRecurrence(opt.value);
|
|
setRecurrenceOpen(false);
|
|
}}
|
|
>
|
|
<Text style={styles.popoverRowText}>{opt.label}</Text>
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
)}
|
|
</Section>
|
|
|
|
<Section icon={<Bell size={16} color={c.textMuted} />} label="Reminders">
|
|
{reminders.length > 0 && (
|
|
<View style={{ gap: spacing.xs }}>
|
|
{reminders.map((r) => (
|
|
<View key={r.minutesBefore} style={styles.reminderRow}>
|
|
<Text style={styles.fieldText}>{formatReminder(r.minutesBefore)}</Text>
|
|
<Pressable
|
|
onPress={() => removeReminder(r.minutesBefore)}
|
|
hitSlop={8}
|
|
style={styles.reminderRemove}
|
|
>
|
|
<X size={16} color={c.textMuted} />
|
|
</Pressable>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
{availablePresets.length > 0 && (
|
|
<Pressable
|
|
style={styles.addRow}
|
|
onPress={() => setReminderPickerOpen((v) => !v)}
|
|
>
|
|
<Plus size={16} color={c.primary} />
|
|
<Text style={styles.addRowText}>
|
|
{reminders.length === 0 ? 'Add reminder' : 'Add another reminder'}
|
|
</Text>
|
|
</Pressable>
|
|
)}
|
|
{reminderPickerOpen && (
|
|
<View style={styles.popover}>
|
|
{availablePresets.map((preset) => (
|
|
<Pressable
|
|
key={preset}
|
|
style={styles.popoverRow}
|
|
onPress={() => addReminder(preset)}
|
|
>
|
|
<Text style={styles.popoverRowText}>{formatReminder(preset)}</Text>
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
)}
|
|
</Section>
|
|
|
|
<Section icon={<MapPin size={16} color={c.textMuted} />} label="Location">
|
|
<TextInput
|
|
value={location}
|
|
onChangeText={setLocation}
|
|
placeholder="Place or address"
|
|
placeholderTextColor={c.textMuted}
|
|
style={styles.fieldButton}
|
|
/>
|
|
</Section>
|
|
|
|
<Section icon={<Video size={16} color={c.textMuted} />} label="Video call">
|
|
<TextInput
|
|
value={videoUrl}
|
|
onChangeText={setVideoUrl}
|
|
placeholder="https://meet.example.com/…"
|
|
placeholderTextColor={c.textMuted}
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
keyboardType="url"
|
|
style={styles.fieldButton}
|
|
/>
|
|
</Section>
|
|
|
|
<Section
|
|
icon={<AlignLeft size={16} color={c.textMuted} />}
|
|
label="Description"
|
|
>
|
|
<TextInput
|
|
value={description}
|
|
onChangeText={setDescription}
|
|
placeholder="Notes, agenda…"
|
|
placeholderTextColor={c.textMuted}
|
|
multiline
|
|
style={styles.descriptionInput}
|
|
/>
|
|
</Section>
|
|
|
|
<Section
|
|
icon={<Users size={16} color={c.textMuted} />}
|
|
label="Participants"
|
|
>
|
|
<ParticipantInput
|
|
participants={participants}
|
|
onChange={setParticipants}
|
|
/>
|
|
</Section>
|
|
|
|
{isEdit && onDelete && (
|
|
<Pressable
|
|
style={({ pressed }) => [
|
|
styles.deleteBtn,
|
|
pressed && styles.deleteBtnPressed,
|
|
]}
|
|
onPress={handleDelete}
|
|
>
|
|
<Trash2 size={16} color={c.error} />
|
|
<Text style={styles.deleteBtnText}>Delete event</Text>
|
|
</Pressable>
|
|
)}
|
|
</ScrollView>
|
|
|
|
{showStartDate && (
|
|
<DateTimePicker
|
|
value={start}
|
|
mode="date"
|
|
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
|
onChange={(_, d) => {
|
|
setShowStartDate(false);
|
|
if (d) {
|
|
const next = new Date(start);
|
|
next.setFullYear(d.getFullYear(), d.getMonth(), d.getDate());
|
|
updateStart(next);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
{showStartTime && (
|
|
<DateTimePicker
|
|
value={start}
|
|
mode="time"
|
|
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
|
onChange={(_, d) => {
|
|
setShowStartTime(false);
|
|
if (d) {
|
|
const next = new Date(start);
|
|
next.setHours(d.getHours(), d.getMinutes(), 0, 0);
|
|
updateStart(next);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
{showEndDate && (
|
|
<DateTimePicker
|
|
value={end}
|
|
mode="date"
|
|
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
|
onChange={(_, d) => {
|
|
setShowEndDate(false);
|
|
if (d) {
|
|
const next = new Date(end);
|
|
next.setFullYear(d.getFullYear(), d.getMonth(), d.getDate());
|
|
updateEnd(next);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
{showEndTime && (
|
|
<DateTimePicker
|
|
value={end}
|
|
mode="time"
|
|
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
|
onChange={(_, d) => {
|
|
setShowEndTime(false);
|
|
if (d) {
|
|
const next = new Date(end);
|
|
next.setHours(d.getHours(), d.getMinutes(), 0, 0);
|
|
updateEnd(next);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</SafeAreaView>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
function Section({
|
|
icon,
|
|
label,
|
|
children,
|
|
}: {
|
|
icon: React.ReactNode;
|
|
label: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
const c = useColors();
|
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
|
return (
|
|
<View style={styles.section}>
|
|
<View style={styles.sectionHeader}>
|
|
<View style={styles.sectionIcon}>{icon}</View>
|
|
<Text style={styles.sectionLabel}>{label}</Text>
|
|
</View>
|
|
<View style={styles.sectionBody}>{children}</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function makeStyles(c: ThemePalette) {
|
|
return StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: c.background },
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: c.border,
|
|
gap: spacing.md,
|
|
},
|
|
headerBtn: {
|
|
width: 36,
|
|
height: 36,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderRadius: radius.full,
|
|
},
|
|
headerTitle: { flex: 1, ...typography.h3, color: c.text },
|
|
|
|
body: { flex: 1 },
|
|
bodyContent: { padding: spacing.lg, gap: spacing.lg, paddingBottom: spacing.xxxl },
|
|
|
|
titleInput: {
|
|
...typography.h2,
|
|
color: c.text,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: c.border,
|
|
paddingVertical: spacing.sm,
|
|
},
|
|
|
|
section: { gap: spacing.sm },
|
|
sectionHeader: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm },
|
|
sectionIcon: { width: 16 },
|
|
sectionLabel: { ...typography.bodyMedium, color: c.textSecondary },
|
|
sectionBody: { gap: spacing.sm },
|
|
|
|
fieldButton: {
|
|
minHeight: 40,
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
borderRadius: radius.sm,
|
|
borderWidth: 1,
|
|
borderColor: c.border,
|
|
backgroundColor: c.surface,
|
|
justifyContent: 'center',
|
|
},
|
|
fieldText: { ...typography.body, color: c.text },
|
|
|
|
calendarRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm },
|
|
calendarSwatch: { width: 12, height: 12, borderRadius: 6 },
|
|
|
|
reminderRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
borderRadius: radius.sm,
|
|
borderWidth: 1,
|
|
borderColor: c.border,
|
|
backgroundColor: c.surface,
|
|
},
|
|
reminderRemove: { padding: 2 },
|
|
addRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: spacing.sm,
|
|
paddingVertical: spacing.sm,
|
|
},
|
|
addRowText: { ...typography.body, color: c.primary },
|
|
|
|
popover: {
|
|
borderWidth: 1,
|
|
borderColor: c.border,
|
|
borderRadius: radius.sm,
|
|
backgroundColor: c.surface,
|
|
overflow: 'hidden',
|
|
},
|
|
popoverRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: spacing.sm,
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: c.borderLight,
|
|
},
|
|
popoverRowActive: { backgroundColor: c.primaryBg },
|
|
popoverRowText: { ...typography.body, color: c.text },
|
|
|
|
allDayRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.xs,
|
|
borderRadius: radius.sm,
|
|
backgroundColor: c.surface,
|
|
borderWidth: 1,
|
|
borderColor: c.border,
|
|
},
|
|
dateRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
borderRadius: radius.sm,
|
|
borderWidth: 1,
|
|
borderColor: c.border,
|
|
backgroundColor: c.surface,
|
|
},
|
|
dateLabel: { ...typography.body, color: c.textSecondary, flex: 1 },
|
|
datePickers: { flexDirection: 'row', gap: spacing.sm },
|
|
dateBtn: {
|
|
paddingHorizontal: spacing.sm,
|
|
paddingVertical: 4,
|
|
borderRadius: radius.xs,
|
|
backgroundColor: c.background,
|
|
borderWidth: 1,
|
|
borderColor: c.border,
|
|
},
|
|
dateText: { ...typography.body, color: c.text },
|
|
|
|
descriptionInput: {
|
|
minHeight: 80,
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
borderRadius: radius.sm,
|
|
borderWidth: 1,
|
|
borderColor: c.border,
|
|
backgroundColor: c.surface,
|
|
color: c.text,
|
|
...typography.body,
|
|
textAlignVertical: 'top',
|
|
},
|
|
|
|
deleteBtn: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: spacing.sm,
|
|
paddingVertical: spacing.md,
|
|
borderRadius: radius.sm,
|
|
borderWidth: 1,
|
|
borderColor: c.errorBorder,
|
|
marginTop: spacing.lg,
|
|
},
|
|
deleteBtnPressed: { backgroundColor: c.errorBg },
|
|
deleteBtnText: { ...typography.bodyMedium, color: c.error },
|
|
});
|
|
}
|