diff --git a/companion/app.json b/companion/app.json index 01ae433612..33da374fd2 100644 --- a/companion/app.json +++ b/companion/app.json @@ -3,10 +3,10 @@ "name": "Calcom", "slug": "calcom-companion", "scheme": "expo-wxt-app", - "version": "1.0.2", + "version": "1.0.3", "orientation": "portrait", "icon": "./assets/icon.png", - "userInterfaceStyle": "light", + "userInterfaceStyle": "automatic", "newArchEnabled": true, "splash": { "image": "./assets/splash-icon.png", @@ -17,6 +17,7 @@ "supportsTablet": false, "deploymentTarget": "18.0", "bundleIdentifier": "com.cal.companion", + "userInterfaceStyle": "automatic", "infoPlist": { "ITSAppUsesNonExemptEncryption": false }, @@ -33,7 +34,8 @@ "edgeToEdgeEnabled": true, "package": "com.calcom.companion", "softwareKeyboardLayoutMode": "pan", - "usesCleartextTraffic": false + "usesCleartextTraffic": false, + "userInterfaceStyle": "automatic" }, "web": { "favicon": "./assets/favicon.png", diff --git a/companion/app/(tabs)/(availability)/availability-detail.ios.tsx b/companion/app/(tabs)/(availability)/availability-detail.ios.tsx index 6222dd1453..d6ff9ed2a1 100644 --- a/companion/app/(tabs)/(availability)/availability-detail.ios.tsx +++ b/companion/app/(tabs)/(availability)/availability-detail.ios.tsx @@ -1,9 +1,11 @@ +import { useColorScheme } from "react-native"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useRef } from "react"; import { AvailabilityDetailScreen, type AvailabilityDetailScreenHandle, } from "@/components/screens/AvailabilityDetailScreen"; +import { getColors } from "@/constants/colors"; // Type for action handlers exposed by AvailabilityDetailScreen.ios.tsx type ActionHandlers = { @@ -14,6 +16,9 @@ type ActionHandlers = { export default function AvailabilityDetailIOS() { const { id } = useLocalSearchParams<{ id: string }>(); const router = useRouter(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); // Ref to store action handlers from AvailabilityDetailScreen const actionHandlersRef = useRef(null); @@ -67,13 +72,18 @@ export default function AvailabilityDetailIOS() { headerBackButtonDisplayMode: "default", headerTitle: "", headerStyle: { - backgroundColor: "#f2f2f7", + backgroundColor: isDark ? theme.background : theme.backgroundMuted, }, headerShadowVisible: false, }} /> - + {/* Edit Menu */} diff --git a/companion/app/(tabs)/(availability)/availability-detail.tsx b/companion/app/(tabs)/(availability)/availability-detail.tsx index ca62bc228c..81423021eb 100644 --- a/companion/app/(tabs)/(availability)/availability-detail.tsx +++ b/companion/app/(tabs)/(availability)/availability-detail.tsx @@ -1,7 +1,7 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useRef } from "react"; -import { Text, TouchableOpacity } from "react-native"; +import { Text, useColorScheme } from "react-native"; import { AvailabilityDetailScreen, type AvailabilityDetailScreenHandle, @@ -14,6 +14,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { getColors } from "@/constants/colors"; +import { AppPressable } from "@/components/AppPressable"; // Type for action handlers exposed by AvailabilityDetailScreen type ActionHandlers = { @@ -24,6 +26,9 @@ type ActionHandlers = { export default function AvailabilityDetail() { const { id } = useLocalSearchParams<{ id: string }>(); const router = useRouter(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); // Ref to store action handlers from AvailabilityDetailScreen const actionHandlersRef = useRef(null); @@ -72,47 +77,82 @@ export default function AvailabilityDetail() { <> ( + + router.back()} + className="mr-2 h-10 flex-row items-center justify-center rounded-full border border-[#E5E5E5] bg-white px-3 dark:border-[#262626] dark:bg-[#171717]" + > + + + Availability + + + + ), headerRight: () => ( - - - + + + Edit + + - - Name and Timezone + + + Name and Timezone + - - Working Hours + + + Working Hours + - - Date Override + + + Date Override + - - Set as Default + + + Set as Default + - - Delete Schedule + + + Delete Schedule + diff --git a/companion/app/(tabs)/(availability)/index.tsx b/companion/app/(tabs)/(availability)/index.tsx index 80375ddf8c..09ebc96bdb 100644 --- a/companion/app/(tabs)/(availability)/index.tsx +++ b/companion/app/(tabs)/(availability)/index.tsx @@ -2,7 +2,7 @@ import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Image } from "expo-image"; import { Stack, useRouter } from "expo-router"; import { useState } from "react"; -import { Alert, Platform, Pressable } from "react-native"; +import { Alert, Platform, Pressable, useColorScheme } from "react-native"; import { AvailabilityListScreen } from "@/components/screens/AvailabilityListScreen"; import { useCreateSchedule, useUserProfile } from "@/hooks"; import { CalComAPIService } from "@/services/calcom"; @@ -15,6 +15,8 @@ export default function Availability() { const [showCreateModal, setShowCreateModal] = useState(false); const { mutate: createScheduleMutation } = useCreateSchedule(); const { data: userProfile } = useUserProfile(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; const handleCreateNew = () => { // Use native iOS Alert.prompt for a native look @@ -93,7 +95,7 @@ export default function Availability() { <> diff --git a/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx b/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx index 96de100eee..2155ac5c1b 100644 --- a/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx +++ b/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx @@ -1,6 +1,7 @@ import * as Clipboard from "expo-clipboard"; import { Stack, useLocalSearchParams } from "expo-router"; import { useCallback, useMemo, useRef } from "react"; +import { useColorScheme } from "react-native"; import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen"; import { useAuth } from "@/contexts/AuthContext"; import { useBookingByUid } from "@/hooks/useBookings"; @@ -63,6 +64,7 @@ const getMeetingUrl = (booking: Booking | null): string | null => { export default function BookingDetailIOS() { const { uid } = useLocalSearchParams<{ uid: string }>(); const { userInfo } = useAuth(); + const colorScheme = useColorScheme(); // Use React Query hook for booking data - single source of truth const { data: booking, isLoading, error, refetch, isRefetching } = useBookingByUid(uid); @@ -295,7 +297,11 @@ export default function BookingDetailIOS() { {meetingUrl && ( - + Join )} diff --git a/companion/app/(tabs)/(bookings)/booking-detail.tsx b/companion/app/(tabs)/(bookings)/booking-detail.tsx index 570a9791d7..af965911a3 100644 --- a/companion/app/(tabs)/(bookings)/booking-detail.tsx +++ b/companion/app/(tabs)/(bookings)/booking-detail.tsx @@ -1,11 +1,25 @@ -import { Stack, useLocalSearchParams } from "expo-router"; -import { useCallback, useMemo, useRef } from "react"; -import { Platform } from "react-native"; +import * as Clipboard from "expo-clipboard"; +import { Ionicons } from "@expo/vector-icons"; +import { Stack, useLocalSearchParams, useNavigation, useRouter } from "expo-router"; +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { Platform, Text, View, useColorScheme } from "react-native"; +import { AppPressable } from "@/components/AppPressable"; +import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { useAuth } from "@/contexts/AuthContext"; import { useBookingByUid } from "@/hooks/useBookings"; -import { showErrorAlert, showInfoAlert } from "@/utils/alerts"; +import type { Booking } from "@/services/calcom"; +import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts"; import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions"; +import { openInAppBrowser } from "@/utils/browser"; // Empty actions result for when no booking is loaded const EMPTY_ACTIONS: BookingActionsResult = { @@ -19,6 +33,22 @@ const EMPTY_ACTIONS: BookingActionsResult = { markNoShow: { visible: false, enabled: false }, }; +const getMeetingUrl = (booking: Booking | null): string | null => { + if (!booking) return null; + + const videoCallUrl = booking.responses?.videoCallUrl; + if (typeof videoCallUrl === "string" && videoCallUrl.startsWith("http")) { + return videoCallUrl; + } + + const location = booking.location; + if (typeof location === "string" && location.startsWith("http")) { + return location; + } + + return null; +}; + // Type for action handlers exposed by BookingDetailScreen type ActionHandlers = { openRescheduleModal: () => void; @@ -30,6 +60,29 @@ type ActionHandlers = { handleCancelBooking: () => void; }; +const getIconName = (sfSymbol: string): keyof typeof Ionicons.glyphMap => { + switch (sfSymbol) { + case "calendar": + return "calendar"; + case "location": + return "location"; + case "person.badge.plus": + return "person-add"; + case "video": + return "videocam"; + case "info.circle": + return "information-circle"; + case "eye.slash": + return "eye-off"; + case "flag": + return "flag"; + case "xmark.circle": + return "close-circle"; + default: + return "ellipsis-horizontal"; + } +}; + export default function BookingDetail() { "use no memo"; const { uid } = useLocalSearchParams<{ uid: string }>(); @@ -218,11 +271,209 @@ export default function BookingDetail() { handleCancel, ]); + const router = useRouter(); + const navigation = useNavigation(); + const colorScheme = useColorScheme(); + const isDarkMode = colorScheme === "dark"; + + const meetingUrl = useMemo(() => getMeetingUrl(booking ?? null), [booking]); + + const handleJoinMeeting = useCallback(() => { + if (meetingUrl) { + openInAppBrowser(meetingUrl, "meeting link"); + } + }, [meetingUrl]); + + const handleCopyMeetingLink = useCallback(async () => { + if (meetingUrl) { + await Clipboard.setStringAsync(meetingUrl); + showSuccessAlert("Copied", "Meeting link copied to clipboard"); + } + }, [meetingUrl]); + + const renderHeaderLeft = useCallback(() => { + let backButtonLabel = "Back"; + + // Check both start and startTime properties + const startTime = booking?.start || booking?.startTime; + + if (startTime) { + const date = new Date(startTime); + if (!Number.isNaN(date.getTime())) { + backButtonLabel = date.toLocaleDateString("en-US", { month: "long" }); + } + } + + return ( + + router.back()} + className="mr-2 h-10 flex-row items-center justify-center rounded-full border border-[#E5E5E5] bg-white px-3 dark:border-[#262626] dark:bg-[#171717]" + > + + + {backButtonLabel} + + + + ); + }, [booking?.start, booking?.startTime, router, isDarkMode]); + + const renderHeaderRight = useCallback( + () => ( + + + + + + + + + + {meetingUrl && ( + <> + + + + + Copy meeting link + + + + + + )} + + {bookingActionsSections.editEvent.length > 0 && ( + <> + Edit Event + {bookingActionsSections.editEvent.map((action) => ( + + + + + {action.label} + + + + ))} + + + )} + + {bookingActionsSections.afterEvent.length > 0 && ( + <> + After Event + {bookingActionsSections.afterEvent.map((action) => ( + + + + + {action.label} + + + + ))} + + + )} + + Danger Zone + {bookingActionsSections.standalone.map((action) => ( + + + + + {action.label} + + + + ))} + + + {meetingUrl && ( + + + Join + + + )} + + + ), + [isDarkMode, meetingUrl, handleCopyMeetingLink, bookingActionsSections, handleJoinMeeting] + ); + + // Force header update on Android/Web + useEffect(() => { + if (Platform.OS === "android" || Platform.OS === "web") { + navigation.setOptions({ + headerRight: renderHeaderRight, + headerLeft: renderHeaderLeft, + }); + } + }, [navigation, renderHeaderRight, renderHeaderLeft]); + return ( <> diff --git a/companion/app/(tabs)/(bookings)/index.ios.tsx b/companion/app/(tabs)/(bookings)/index.ios.tsx index 36e57d70f0..2953f5dabf 100644 --- a/companion/app/(tabs)/(bookings)/index.ios.tsx +++ b/companion/app/(tabs)/(bookings)/index.ios.tsx @@ -2,6 +2,7 @@ import type { NativeStackHeaderItemMenuAction } from "@react-navigation/native-s import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack } from "expo-router"; import { useState } from "react"; +import { useColorScheme } from "react-native"; import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen"; import { useEventTypes } from "@/hooks"; @@ -22,6 +23,10 @@ export default function Bookings() { } ); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const textColor = isDark ? "#FFFFFF" : "#000000"; + const handleSearch = (query: string) => { setSearchQuery(query); }; @@ -62,7 +67,7 @@ export default function Bookings() { label: currentFilterOption?.label || "Filter", labelStyle: { fontWeight: "600", - color: "#000000", + color: textColor, }, menu: { title: "Filter by Status", @@ -123,7 +128,7 @@ export default function Bookings() { }, labelStyle: { fontWeight: "600", - color: "#000000", + color: textColor, }, menu: { title: menuItems.length > 0 ? "Filter by Event Type" : "No Event Types", @@ -149,7 +154,7 @@ export default function Bookings() { handleSearch(e.nativeEvent.text), obscureBackground: false, - barTintColor: "#fff", + barTintColor: isDark ? "#171717" : "#fff", }, unstable_headerRightItems: () => { const eventTypeFilterMenu = buildEventTypeFilterMenu(); diff --git a/companion/app/(tabs)/(bookings)/index.tsx b/companion/app/(tabs)/(bookings)/index.tsx index d6e8c7ec74..51e20e4d62 100644 --- a/companion/app/(tabs)/(bookings)/index.tsx +++ b/companion/app/(tabs)/(bookings)/index.tsx @@ -1,6 +1,6 @@ import { Ionicons } from "@expo/vector-icons"; import { useState } from "react"; -import { Text, TextInput, View } from "react-native"; +import { Text, TextInput, useColorScheme, View } from "react-native"; import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen"; import { Header } from "@/components/Header"; import { @@ -12,11 +12,15 @@ import { import { AppPressable } from "@/components/AppPressable"; import { useActiveBookingFilter } from "@/hooks/useActiveBookingFilter"; import { useEventTypes } from "@/hooks"; +import { getColors } from "@/constants/colors"; export default function Bookings() { const [searchQuery, setSearchQuery] = useState(""); const [selectedEventTypeId, setSelectedEventTypeId] = useState(null); const [selectedEventTypeLabel, setSelectedEventTypeLabel] = useState(null); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); // Use React Query hook for event types (same as iOS for unified caching) const { data: eventTypes = [], isLoading: eventTypesLoading } = useEventTypes(); @@ -55,7 +59,13 @@ export default function Bookings() { selectedEventTypeId !== null ? selectedEventTypeLabel || "Event Type" : "Filter"; return ( - + {/* Dropdown menu for event type filter */} @@ -63,21 +73,31 @@ export default function Bookings() { - + {filterLabel} - + @@ -92,7 +112,9 @@ export default function Bookings() { checked={selectedEventTypeId === null} onCheckedChange={() => handleEventTypeSelect(null)} > - All Event Types + + All Event Types + {/* Event type options */} @@ -102,7 +124,7 @@ export default function Bookings() { checked={selectedEventTypeId === eventType.id} onCheckedChange={() => handleEventTypeSelect(eventType.id, eventType.title)} > - + {eventType.title} @@ -111,7 +133,7 @@ export default function Bookings() { {/* Loading state */} {eventTypesLoading && eventTypes.length === 0 && ( {}}> - Loading... + Loading... )} @@ -120,8 +142,13 @@ export default function Bookings() { 0; }, [currentFormState, eventTypeData, id]); - const handleSave = async () => { + const handleSave = useCallback(async () => { if (!id) { showErrorAlert("Error", "Event type ID is missing"); return; @@ -1268,108 +1272,159 @@ export default function EventTypeDetail() { showSuccessAlert("Success", "Event type updated successfully"); // No need to manually refresh - cache is updated by the mutation hook } - }; + }, [ + id, + eventTitle, + eventSlug, + eventDuration, + locations, + selectedSchedule?.id, + eventDescription, + isHidden, + createEventType, + router, + currentFormState, + eventTypeData, + updateEventType, + ]); const headerTitle = id === "new" ? "Create Event Type" : truncateTitle(title); const saveButtonText = id === "new" ? "Create" : "Save"; - const renderHeaderLeft = () => ( - - router.back()} className="px-2 py-2"> - - - + const renderHeaderLeft = useCallback( + () => ( + + router.back()} + className="mr-2 h-10 w-10 items-center justify-center rounded-full border border-[#E5E5E5] bg-white dark:border-[#262626] dark:bg-[#171717]" + > + + + + ), + [router, isDarkMode] ); - const renderHeaderRight = () => ( - - - {/* Tab Navigation Dropdown Menu */} - - - ( + + + {/* Tab Navigation Dropdown Menu */} + + + + + {tabs.find((tab) => tab.id === activeTab)?.label ?? "Basics"} + + + + + - - {tabs.find((tab) => tab.id === activeTab)?.label ?? "Basics"} - - - - + {tabs.map((tab) => { + const isSelected = activeTab === tab.id; + return ( + setActiveTab(tab.id)}> + + + + {tab.label} + + + + ); + })} + + - - {tabs.map((tab) => { - const isSelected = activeTab === tab.id; - return ( - setActiveTab(tab.id)}> - - - - {tab.label} - - - - ); - })} - - - - {/* Save Button */} - - - {saveButtonText} - - - - + {isSaving ? ( + + ) : ( + + {saveButtonText} + + )} + + + + ), + [activeTab, isDarkMode, handleSave, isSaving, isDirty, saveButtonText] ); + // Force header update on Android/Web when state changes + const navigation = useNavigation(); + useEffect(() => { + if (Platform.OS === "android" || Platform.OS === "web") { + navigation.setOptions({ + headerRight: renderHeaderRight, + headerLeft: renderHeaderLeft, + }); + } + }, [navigation, renderHeaderRight, renderHeaderLeft]); + return ( <> {Platform.OS === "ios" && ( - + @@ -1403,7 +1458,7 @@ export default function EventTypeDetail() { onPress={handleSave} disabled={isSaving || !isDirty} variant="prominent" - tintColor="#000" + tintColor={isDarkMode ? "#FFF" : "#000"} > {saveButtonText} @@ -1411,7 +1466,7 @@ export default function EventTypeDetail() { )} - + - + Select Available Durations @@ -1476,37 +1531,43 @@ export default function EventTypeDetail() { key={duration} className={`flex-row items-center justify-between py-3.5 ${ index < availableDurations.length - 1 - ? "border-b border-black/10" + ? "border-b border-black/10 dark:border-white/10" : "" }`} onPress={() => toggleDurationSelection(duration)} > {duration} {selectedDurations.includes(duration) ? ( - + ) : ( - + )} ))} setShowDurationDropdown(false)} > - Done + + Done + ) : ( - - + + Select Available Durations @@ -1514,21 +1575,23 @@ export default function EventTypeDetail() { toggleDurationSelection(duration)} > {duration} {selectedDurations.includes(duration) ? ( - + ) : ( - + )} ))} @@ -1561,7 +1624,7 @@ export default function EventTypeDetail() { {isLiquidGlassAvailable() && Platform.OS === "ios" ? ( - + Select Default Duration @@ -1569,7 +1632,9 @@ export default function EventTypeDetail() { { setDefaultDuration(duration); @@ -1577,14 +1642,18 @@ export default function EventTypeDetail() { }} > {duration} {defaultDuration === duration ? ( - + ) : null} ))} @@ -1592,8 +1661,8 @@ export default function EventTypeDetail() { ) : ( - - + + Select Default Duration @@ -1601,7 +1670,9 @@ export default function EventTypeDetail() { { setDefaultDuration(duration); @@ -1609,14 +1680,14 @@ export default function EventTypeDetail() { }} > {duration} {defaultDuration === duration ? ( - + ) : null} ))} @@ -1643,7 +1714,7 @@ export default function EventTypeDetail() { {isLiquidGlassAvailable() && Platform.OS === "ios" ? ( - + Select Schedule @@ -1651,7 +1722,9 @@ export default function EventTypeDetail() { { setSelectedSchedule(schedule); @@ -1661,18 +1734,24 @@ export default function EventTypeDetail() { > {schedule.name} {schedule.isDefault && ( - Default + + Default + )} {selectedSchedule?.id === schedule.id ? ( - + ) : null} ))} @@ -1680,8 +1759,8 @@ export default function EventTypeDetail() { ) : ( - - + + Select Schedule @@ -1689,7 +1768,9 @@ export default function EventTypeDetail() { { setSelectedSchedule(schedule); @@ -1699,18 +1780,20 @@ export default function EventTypeDetail() { > {schedule.name} {schedule.isDefault && ( - Default + + Default + )} {selectedSchedule?.id === schedule.id ? ( - + ) : null} ))} @@ -1733,10 +1816,10 @@ export default function EventTypeDetail() { onPress={() => setShowTimezoneDropdown(false)} > - + Select Timezone @@ -1759,8 +1842,8 @@ export default function EventTypeDetail() { className={`mb-1 flex-row items-center justify-between rounded-lg px-2 py-3 md:px-4 ${ selectedTimezone === tz || (selectedScheduleDetails?.timeZone === tz && !selectedTimezone) - ? "bg-[#F0F0F0]" - : "active:bg-[#F0F0F0]" + ? "bg-[#F0F0F0] dark:bg-[#262626]" + : "active:bg-[#F0F0F0] dark:active:bg-[#262626]" }`} onPress={() => { setSelectedTimezone(tz); @@ -1768,7 +1851,7 @@ export default function EventTypeDetail() { }} > {selectedTimezone === tz || (selectedScheduleDetails?.timeZone === tz && !selectedTimezone) ? ( - + ) : null} ))} @@ -1799,15 +1882,15 @@ export default function EventTypeDetail() { className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]" onPress={() => setShowBeforeBufferDropdown(false)} > - - + + Before event buffer {bufferTimeOptions.map((option) => ( { setBeforeEventBuffer(option); @@ -1815,14 +1898,14 @@ export default function EventTypeDetail() { }} > {option} {beforeEventBuffer === option ? ( - + ) : null} ))} @@ -1841,15 +1924,15 @@ export default function EventTypeDetail() { className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]" onPress={() => setShowAfterBufferDropdown(false)} > - - + + After event buffer {bufferTimeOptions.map((option) => ( { setAfterEventBuffer(option); @@ -1857,14 +1940,14 @@ export default function EventTypeDetail() { }} > {option} {afterEventBuffer === option ? ( - + ) : null} ))} @@ -1883,15 +1966,15 @@ export default function EventTypeDetail() { className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]" onPress={() => setShowMinimumNoticeUnitDropdown(false)} > - - + + Time unit {timeUnitOptions.map((option) => ( { setMinimumNoticeUnit(option); @@ -1899,14 +1982,14 @@ export default function EventTypeDetail() { }} > {option} {minimumNoticeUnit === option ? ( - + ) : null} ))} @@ -1925,8 +2008,8 @@ export default function EventTypeDetail() { className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]" onPress={() => setShowFrequencyUnitDropdown(null)} > - - + + Frequency unit {frequencyUnitOptions.map((option) => { @@ -1938,7 +2021,7 @@ export default function EventTypeDetail() { { if (showFrequencyUnitDropdown) { @@ -1948,11 +2031,11 @@ export default function EventTypeDetail() { }} > {option} - {isSelected && } + {isSelected && } ); })} @@ -1971,8 +2054,8 @@ export default function EventTypeDetail() { className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]" onPress={() => setShowDurationUnitDropdown(null)} > - - + + Duration unit {durationUnitOptions.map((option) => { @@ -1984,7 +2067,7 @@ export default function EventTypeDetail() { { if (showDurationUnitDropdown) { @@ -1994,11 +2077,11 @@ export default function EventTypeDetail() { }} > {option} - {isSelected && } + {isSelected && } ); })} @@ -2017,15 +2100,15 @@ export default function EventTypeDetail() { className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]" onPress={() => setShowSlotIntervalDropdown(false)} > - - + + Slot interval {slotIntervalOptions.map((option) => ( { setSlotInterval(option); @@ -2033,14 +2116,14 @@ export default function EventTypeDetail() { }} > {option} {slotInterval === option ? ( - + ) : null} ))} @@ -2059,15 +2142,15 @@ export default function EventTypeDetail() { className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]" onPress={() => setShowRecurringFrequencyDropdown(false)} > - - + + Repeats every {(["weekly", "monthly", "yearly"] as const).map((option) => ( { setRecurringFrequency(option); @@ -2075,14 +2158,14 @@ export default function EventTypeDetail() { }} > {option === "weekly" ? "week" : option === "monthly" ? "month" : "year"} {recurringFrequency === option ? ( - + ) : null} ))} @@ -2238,16 +2321,16 @@ export default function EventTypeDetail() { {/* Configure on Web */} Configure on Web - + {/* Apps */} - + { if (id === "new") { @@ -2267,12 +2350,12 @@ export default function EventTypeDetail() { activeOpacity={0.5} > - - + + - Apps - Manage integrations + Apps + Manage integrations @@ -2280,9 +2363,9 @@ export default function EventTypeDetail() { {/* Workflows */} - + { if (id === "new") { @@ -2302,12 +2385,12 @@ export default function EventTypeDetail() { activeOpacity={0.5} > - - + + - Workflows - Automated actions + Workflows + Automated actions @@ -2315,7 +2398,7 @@ export default function EventTypeDetail() { {/* Webhooks */} - + - - + + - Webhooks - Event notifications + Webhooks + Event notifications @@ -2358,23 +2441,26 @@ export default function EventTypeDetail() { {/* Visibility */} Visibility - - + + - Hidden + Hidden @@ -2386,33 +2472,33 @@ export default function EventTypeDetail() { {/* Quick Actions */} Quick Actions - - + + {Platform.OS !== "ios" && ( - Preview + Preview )} - + - Copy Link + Copy Link @@ -2421,8 +2507,8 @@ export default function EventTypeDetail() { {/* Danger Zone */} - - + + Event Types + - + Unable to load event types - {error} - refetch()}> - Retry + + {error} + + refetch()} + > + + Retry + ); @@ -374,12 +398,15 @@ export default function EventTypesIOS() { - + Event Types @@ -494,13 +521,13 @@ export default function EventTypesIOS() { placeholder="Search event types" onChangeText={(e) => handleSearch(e.nativeEvent.text)} obscureBackground={false} - barTintColor="#fff" + barTintColor={isDark ? "#171717" : "#fff"} /> {/* Event Types List */} } showsVerticalScrollIndicator={false} @@ -509,7 +536,10 @@ export default function EventTypesIOS() { {refreshing ? ( ) : filteredEventTypes.length === 0 && searchQuery.trim() !== "" ? ( - + ) : filteredEventTypes.length === 0 && activeFilterCount > 0 ? ( - + ) : ( - + {filteredEventTypes.map((item, index) => ( - - - + + + ); diff --git a/companion/app/(tabs)/(event-types)/index.tsx b/companion/app/(tabs)/(event-types)/index.tsx index ec7c486a73..e4cc562682 100644 --- a/companion/app/(tabs)/(event-types)/index.tsx +++ b/companion/app/(tabs)/(event-types)/index.tsx @@ -12,6 +12,7 @@ import { Text, TextInput, TouchableOpacity, + useColorScheme, View, } from "react-native"; import { EmptyScreen } from "@/components/EmptyScreen"; @@ -46,9 +47,14 @@ import { normalizeMarkdown } from "@/utils/normalizeMarkdown"; import { shadows } from "@/utils/shadows"; import { slugify } from "@/utils/slugify"; +import { getColors } from "@/constants/colors"; + export default function EventTypes() { const router = useRouter(); const [searchQuery, setSearchQuery] = useState(""); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); // Modal state for creating new event type const [showCreateModal, setShowCreateModal] = useState(false); @@ -381,10 +387,10 @@ export default function EventTypes() { if (loading) { return ( - + {Platform.OS === "web" &&
} + {Platform.OS === "web" &&
} - - - + + + Unable to load event types - {error} - refetch()}> - Retry + + {error} + + refetch()} + > + + Retry + @@ -415,9 +441,12 @@ export default function EventTypes() { if (eventTypes.length === 0) { return ( - + {Platform.OS === "web" &&
} - + + {Platform.OS === "web" && ( <>
- + - - New + + + New + )} - + - + - - New + + + New + )} } showsVerticalScrollIndicator={false} @@ -534,7 +605,10 @@ export default function EventTypes() { {refreshing ? ( ) : filteredEventTypes.length === 0 && activeFilterCount > 0 ? ( - + ) : ( - + {filteredEventTypes.map((item, index) => ( e.stopPropagation()} - style={shadows.xl()} + style={{ ...shadows.xl(), backgroundColor: theme.backgroundSecondary }} > {/* Header */} - + Add a new event type - + Set up event types to offer different types of meetings. @@ -655,11 +735,21 @@ export default function EventTypes() { {/* Title */} - Title + + Title + {/* Footer */} - + - Close + + Close + - Continue + + Continue + @@ -712,18 +806,18 @@ export default function EventTypes() { }} > e.stopPropagation()} > {selectedEventType ? ( <> - - + + {selectedEventType.title} {selectedEventType.description ? ( - + {normalizeMarkdown(selectedEventType.description)} ) : null} @@ -731,7 +825,7 @@ export default function EventTypes() { { setShowActionModal(false); const eventType = selectedEventType; @@ -739,12 +833,12 @@ export default function EventTypes() { if (eventType) handleEdit(eventType); }} > - - Edit + + Edit { setShowActionModal(false); const eventType = selectedEventType; @@ -752,15 +846,15 @@ export default function EventTypes() { if (eventType) handleDuplicate(eventType); }} > - - Duplicate + + Duplicate {/* Separator before delete button */} - + { setShowActionModal(false); const eventType = selectedEventType; @@ -768,20 +862,24 @@ export default function EventTypes() { if (eventType) handleDelete(eventType); }} > - - Delete + + + Delete + - + { setShowActionModal(false); setSelectedEventType(null); }} > - Cancel + + Cancel + @@ -802,14 +900,16 @@ export default function EventTypes() { onPress={() => setShowNewModal(false)} > e.stopPropagation()} > {/* Header */} - - New - Choose what to create + + New + + Choose what to create + {/* Options List */} @@ -823,20 +923,22 @@ export default function EventTypes() { handleOpenCreateModal(); }, 100); }} - className="flex-row items-center p-2 hover:bg-gray-50 md:p-4" + className="flex-row items-center p-2 hover:bg-gray-50 dark:hover:bg-[#2C2C2E] md:p-4" > - - New Event Type + + New Event Type {/* Cancel button */} - + setShowNewModal(false)} > - Cancel + + Cancel + @@ -855,21 +957,21 @@ export default function EventTypes() { }} > - + {/* Header with icon and title */} {/* Danger icon */} - - + + {/* Title and description */} - + Delete Event Type - + {eventTypeToDelete ? ( <> This will permanently delete the "{eventTypeToDelete.title}" event type. @@ -884,22 +986,26 @@ export default function EventTypes() { {/* Footer with buttons */} - Delete + + Delete + { setShowDeleteModal(false); setEventTypeToDelete(null); }} disabled={isDeleting} > - Cancel + + Cancel + diff --git a/companion/app/(tabs)/(more)/index.ios.tsx b/companion/app/(tabs)/(more)/index.ios.tsx index 043df0f71c..8a64080af8 100644 --- a/companion/app/(tabs)/(more)/index.ios.tsx +++ b/companion/app/(tabs)/(more)/index.ios.tsx @@ -3,7 +3,15 @@ import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Image } from "expo-image"; import { Stack, useRouter } from "expo-router"; import { useState } from "react"; -import { Alert, Pressable, ScrollView, Text, TouchableOpacity, View } from "react-native"; +import { + Alert, + Pressable, + ScrollView, + Text, + TouchableOpacity, + useColorScheme, + View, +} from "react-native"; import { LogoutConfirmModal } from "@/components/LogoutConfirmModal"; import { useAuth } from "@/contexts/AuthContext"; import { useQueryContext } from "@/contexts/QueryContext"; @@ -11,6 +19,7 @@ import { useUserProfile } from "@/hooks"; import { showErrorAlert, showNotAvailableAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { getAvatarUrl } from "@/utils/getAvatarUrl"; +import { getColors } from "@/constants/colors"; interface MoreMenuItem { name: string; @@ -26,6 +35,18 @@ export default function More() { const { clearCache } = useQueryContext(); const [showLogoutModal, setShowLogoutModal] = useState(false); const { data: userProfile } = useUserProfile(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + + const colors = { + background: isDark ? "#000000" : "#FFFFFF", + backgroundSecondary: isDark ? "#171717" : "#FFFFFF", + backgroundActive: isDark ? "#2C2C2E" : "#F8F9FA", + text: isDark ? "#FFFFFF" : "#333333", + textSecondary: isDark ? "#A3A3A3" : "#C7C7CC", + border: isDark ? "#4D4D4D" : "#E5E5EA", + }; const performLogout = async () => { try { @@ -94,7 +115,7 @@ export default function More() { {/* iOS Native Header with Glass UI */} More @@ -117,64 +138,124 @@ export default function More() { {/* Content */} - + {menuItems.map((item, index) => ( - - {item.name} + + + {item.name} + {item.isExternal ? ( - + ) : ( - + )} ))} {/* Delete Account Link */} - + openInAppBrowser("https://app.cal.com/settings/my-account/profile", "Delete Account") } - className="flex-row items-center justify-between bg-white px-5 py-4 active:bg-red-50" + style={{ + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + backgroundColor: colors.backgroundSecondary, + paddingHorizontal: 20, + paddingVertical: 16, + }} + activeOpacity={0.7} > - - Delete Account + + + Delete Account + - + {/* Sign Out Button */} - + - - Sign Out + + + Sign Out + {/* Footer Note */} - + The companion app is an extension of the web application.{"\n"} - For advanced features, visit app.cal.com + For advanced features, visit{" "} + app.cal.com diff --git a/companion/app/(tabs)/(more)/index.tsx b/companion/app/(tabs)/(more)/index.tsx index a819c38ebd..8166e1961e 100644 --- a/companion/app/(tabs)/(more)/index.tsx +++ b/companion/app/(tabs)/(more)/index.tsx @@ -1,12 +1,21 @@ import { Ionicons } from "@expo/vector-icons"; import { useState } from "react"; -import { Alert, Platform, ScrollView, Text, TouchableOpacity, View } from "react-native"; +import { + Alert, + Platform, + ScrollView, + Text, + TouchableOpacity, + useColorScheme, + View, +} from "react-native"; import { Header } from "@/components/Header"; import { LogoutConfirmModal } from "@/components/LogoutConfirmModal"; import { useAuth } from "@/contexts/AuthContext"; import { useQueryContext } from "@/contexts/QueryContext"; import { showErrorAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; +import { getColors } from "@/constants/colors"; interface MoreMenuItem { name: string; @@ -20,6 +29,9 @@ export default function More() { const { logout } = useAuth(); const { clearCache } = useQueryContext(); const [showLogoutModal, setShowLogoutModal] = useState(false); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const performLogout = async () => { try { @@ -88,67 +100,90 @@ export default function More() { ]; return ( - +
- + {menuItems.map((item, index) => ( - - {item.name} + + + {item.name} + {item.isExternal ? ( - + ) : ( - + )} ))} {/* Delete Account Link */} - + openInAppBrowser("https://app.cal.com/settings/my-account/profile", "Delete Account") } className="flex-row items-center justify-between bg-white px-5 py-4 active:bg-red-50" + style={{ backgroundColor: theme.backgroundSecondary }} > - - Delete Account + + + Delete Account + - + {/* Sign Out Button */} - + - - Sign Out + + + Sign Out + {/* Footer Note */} - + The companion app is an extension of the web application.{"\n"} For advanced features, visit{" "} openInAppBrowser("https://app.cal.com", "Cal.com")} > app.cal.com diff --git a/companion/app/(tabs)/_layout.tsx b/companion/app/(tabs)/_layout.tsx index 4c0ff93913..d94c024308 100644 --- a/companion/app/(tabs)/_layout.tsx +++ b/companion/app/(tabs)/_layout.tsx @@ -2,6 +2,7 @@ import { Ionicons } from "@expo/vector-icons"; import type { ColorValue, ImageSourcePropType } from "react-native"; import { Tabs, VectorIcon } from "expo-router"; import { NativeTabs } from "expo-router/unstable-native-tabs"; +import { useColorScheme } from "react-native"; import { Platform } from "react-native"; // Type for vector icon families that support getImageSource @@ -9,22 +10,31 @@ type VectorIconFamily = { getImageSource: (name: string, size: number, color: ColorValue) => Promise; }; -const SELECTED_COLOR = "#000000"; - export default function TabLayout() { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + + const colors = { + selected: isDark ? "#FFFFFF" : "#000000", + inactive: "#A3A3A3", + background: isDark ? "#000000" : "#FFFFFF", + border: isDark ? "#4D4D4D" : "#C6C6C8", + indicator: isDark ? "#FFFFFF15" : "#00000015", + }; + if (Platform.OS === "web") { - return ; + return ; } return ( @@ -34,7 +44,7 @@ export default function TabLayout() { android: ( } - selectedColor={SELECTED_COLOR} + selectedColor={colors.selected} /> ), })} @@ -47,7 +57,7 @@ export default function TabLayout() { android: ( } - selectedColor={SELECTED_COLOR} + selectedColor={colors.selected} /> ), })} @@ -60,7 +70,7 @@ export default function TabLayout() { android: ( } - selectedColor={SELECTED_COLOR} + selectedColor={colors.selected} /> ), })} @@ -73,7 +83,7 @@ export default function TabLayout() { android: ( } - selectedColor={SELECTED_COLOR} + selectedColor={colors.selected} /> ), })} @@ -83,18 +93,26 @@ export default function TabLayout() { ); } +type TabColors = { + selected: string; + inactive: string; + background: string; + border: string; + indicator: string; +}; + // TODO: Remove this once native tabs are supported on web -function WebTabs() { +function WebTabs({ colors }: { colors: TabColors }) { return ( - - + + ); } @@ -53,12 +67,17 @@ function RootLayoutContent() { sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined, contentStyle: { backgroundColor: - Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#FFFFFF", + Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : colors.background, }, headerStyle: { - backgroundColor: Platform.OS === "ios" ? "transparent" : "#FFFFFF", + backgroundColor: Platform.OS === "ios" ? "transparent" : colors.background, }, - headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light", + headerBlurEffect: + Platform.OS === "ios" && isLiquidGlassAvailable() + ? undefined + : isDark + ? "dark" + : "light", }} /> @@ -329,8 +418,8 @@ function RootLayoutContent() { const containerClass = Platform.OS === "web" - ? "w-[400px] flex-1 flex-col self-end border-l border-gray-200 bg-white" - : "bg-white"; + ? "w-[400px] flex-1 flex-col self-end border-l border-gray-200 dark:border-gray-700 bg-white dark:bg-black" + : "bg-white dark:bg-black"; const containerStyle = Platform.OS === "web" @@ -343,15 +432,20 @@ function RootLayoutContent() { : { flex: 1 }; return ( - - - {content} - - - + + + + {content} + + + + ); } diff --git a/companion/app/_webRuntimeGuards.ts b/companion/app/_webRuntimeGuards.ts new file mode 100644 index 0000000000..1df2781bdb --- /dev/null +++ b/companion/app/_webRuntimeGuards.ts @@ -0,0 +1,69 @@ +// Web-only runtime guards for Companion. +// +// This runs as early as possible on web to prevent a noisy (but harmless) +// react-native-css-interop error when darkMode is set to "media". + +declare const window: unknown; +declare const require: undefined | ((id: string) => unknown); + +if (typeof window !== "undefined") { + const w = window as { + addEventListener?: (type: string, listener: (event: unknown) => void) => void; + }; + + const shouldSuppressCssInteropError = (err: unknown) => { + if (!(err instanceof Error)) return false; + if (!err.message.includes("Cannot manually set color scheme")) return false; + + // Some environments omit stack traces; if we have one, ensure it's the expected source. + if (typeof err.stack === "string") { + return err.stack.includes("react-native-css-interop/dist/runtime/web/color-scheme"); + } + + return true; + }; + + const suppressIfMatch = (event: unknown, err: unknown) => { + if (!event || typeof event !== "object") return; + const e = event as { preventDefault?: () => void }; + if (shouldSuppressCssInteropError(err)) e.preventDefault?.(); + }; + + w.addEventListener?.("error", (event) => { + const e = event as { error?: unknown }; + suppressIfMatch(event, e.error); + }); + + w.addEventListener?.("unhandledrejection", (event) => { + const e = event as { reason?: unknown }; + suppressIfMatch(event, e.reason); + }); + + const patchCssInterop = (mod: unknown) => { + const colorScheme = (mod as { colorScheme?: { set?: (value: unknown) => void } }).colorScheme; + if (!colorScheme?.set) return; + + // Completely no-op the set call when darkMode is 'media'. + // Manual color scheme setting is not supported in this mode, + // so we silently ignore attempts to call it. + colorScheme.set = () => { + // No-op: system preference controls the color scheme + }; + }; + + // Patch react-native-css-interop on web to ignore the known error. + // This can happen very early during init when darkMode is "media", so prefer sync require. + try { + if (typeof require === "function") { + patchCssInterop(require("react-native-css-interop/dist/runtime/web/color-scheme")); + } else { + void import("react-native-css-interop/dist/runtime/web/color-scheme") + .then(patchCssInterop) + .catch(() => { + // noop + }); + } + } catch { + // noop + } +} diff --git a/companion/app/add-guests.ios.tsx b/companion/app/add-guests.ios.tsx index f22b7e721a..1832807f47 100644 --- a/companion/app/add-guests.ios.tsx +++ b/companion/app/add-guests.ios.tsx @@ -2,15 +2,15 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { getColors } from "@/constants/colors"; import type { AddGuestsScreenHandle } from "@/components/screens/AddGuestsScreen"; import AddGuestsScreenComponent from "@/components/screens/AddGuestsScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect -const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; function getPresentationStyle(): "formSheet" | "modal" { if (isLiquidGlassAvailable() && osName !== "iPadOS") { @@ -27,6 +27,9 @@ export default function AddGuestsIOS() { const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [guestCount, setGuestCount] = useState(0); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const addGuestsScreenRef = useRef(null); @@ -58,6 +61,9 @@ export default function AddGuestsIOS() { const presentationStyle = getPresentationStyle(); const useGlassEffect = isLiquidGlassAvailable(); + // Semi-transparent background to prevent flashes while preserving glass effect + const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; + const showSaveButton = guestCount > 0; return ( @@ -70,14 +76,22 @@ export default function AddGuestsIOS() { sheetAllowedDetents: [0.7, 1], sheetInitialDetentIndex: 0, contentStyle: { - backgroundColor: useGlassEffect ? GLASS_BACKGROUND : "#F2F2F7", + backgroundColor: useGlassEffect + ? glassBackground + : isDark + ? theme.backgroundSecondary + : theme.background, }, }} /> - router.back()}> + router.back()} + tintColor={theme.backgroundEmphasis} + variant="prominent" + > @@ -90,7 +104,7 @@ export default function AddGuestsIOS() { onPress={handleSave} disabled={isSaving} variant="prominent" - tintColor="#000" + tintColor={theme.text} > @@ -101,14 +115,18 @@ export default function AddGuestsIOS() { {isLoading ? ( - + ) : ( (null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const addGuestsScreenRef = useRef(null); @@ -46,12 +50,20 @@ export default function AddGuests() { const renderHeaderLeft = useCallback( () => ( - router.back()} className="px-2 py-2"> - + router.back()} + className="h-10 w-10 items-center justify-center rounded-full border" + style={{ + borderColor: theme.border, + backgroundColor: theme.background, + marginRight: 8, + }} + > + ), - [router] + [router, theme.border, theme.background, theme.text] ); const renderHeaderRight = useCallback( @@ -60,13 +72,19 @@ export default function AddGuests() { - + ), - [handleSave, isSaving] + [handleSave, isSaving, theme.text, theme.border, theme.background] ); if (isLoading) { @@ -85,8 +103,11 @@ export default function AddGuests() { )} - - + + ); diff --git a/companion/app/edit-availability-day.ios.tsx b/companion/app/edit-availability-day.ios.tsx index b71df2cc28..777d2e9ae7 100644 --- a/companion/app/edit-availability-day.ios.tsx +++ b/companion/app/edit-availability-day.ios.tsx @@ -2,8 +2,9 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { getColors } from "@/constants/colors"; import type { EditAvailabilityDayScreenHandle } from "@/components/screens/EditAvailabilityDayScreen.ios"; import EditAvailabilityDayScreenComponent from "@/components/screens/EditAvailabilityDayScreen.ios"; import { CalComAPIService, type Schedule } from "@/services/calcom"; @@ -12,7 +13,6 @@ import { showErrorAlert } from "@/utils/alerts"; const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; // Semi-transparent background to prevent black flash while preserving glass effect -const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; function getPresentationStyle(): "formSheet" | "modal" { if (isLiquidGlassAvailable() && osName !== "iPadOS") { @@ -25,6 +25,9 @@ export default function EditAvailabilityDayIOS() { const { id, day } = useLocalSearchParams<{ id: string; day: string }>(); const router = useRouter(); const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const [schedule, setSchedule] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); @@ -62,6 +65,9 @@ export default function EditAvailabilityDayIOS() { const presentationStyle = getPresentationStyle(); const useGlassEffect = isLiquidGlassAvailable(); + // Semi-transparent background to prevent flashes while preserving glass effect + const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; + return ( <> - router.back()}> + router.back()} + tintColor={theme.backgroundEmphasis} + variant="prominent" + > @@ -91,7 +105,7 @@ export default function EditAvailabilityDayIOS() { onPress={handleSave} disabled={isSaving} variant="prominent" - tintColor="#000" + tintColor={theme.text} > diff --git a/companion/app/edit-availability-day.tsx b/companion/app/edit-availability-day.tsx index e4df6513d3..b88046ed9b 100644 --- a/companion/app/edit-availability-day.tsx +++ b/companion/app/edit-availability-day.tsx @@ -1,10 +1,13 @@ +import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Text, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { EditAvailabilityDayScreenHandle } from "@/components/screens/EditAvailabilityDayScreen"; import EditAvailabilityDayScreenComponent from "@/components/screens/EditAvailabilityDayScreen"; +import { getColors } from "@/constants/colors"; import { CalComAPIService, type Schedule } from "@/services/calcom"; import { showErrorAlert } from "@/utils/alerts"; @@ -17,6 +20,9 @@ export default function EditAvailabilityDay() { const [schedule, setSchedule] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const screenRef = useRef(null); @@ -49,33 +55,79 @@ export default function EditAvailabilityDay() { router.back(); }, [router]); + const handleClose = useCallback(() => { + router.back(); + }, [router]); + if (isLoading) { return ( - - + + ); } return ( - + ( + + + + + + ), headerRight: () => ( - - Save - + + ), }} diff --git a/companion/app/edit-availability-hours.ios.tsx b/companion/app/edit-availability-hours.ios.tsx index ea9cfea920..f60642bc51 100644 --- a/companion/app/edit-availability-hours.ios.tsx +++ b/companion/app/edit-availability-hours.ios.tsx @@ -2,14 +2,14 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect } from "react"; -import { ActivityIndicator, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { getColors } from "@/constants/colors"; import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen.ios"; import { useScheduleById } from "@/hooks/useSchedules"; import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect -const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; function getPresentationStyle(): "formSheet" | "modal" { if (isLiquidGlassAvailable() && osName !== "iPadOS") { @@ -22,6 +22,9 @@ export default function EditAvailabilityHoursIOS() { const { id } = useLocalSearchParams<{ id: string }>(); const router = useRouter(); const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); // Use React Query hook to read from cache (syncs with mutations) const { data: schedule, isLoading, isError } = useScheduleById(id ? Number(id) : undefined); @@ -47,6 +50,9 @@ export default function EditAvailabilityHoursIOS() { const presentationStyle = getPresentationStyle(); const useGlassEffect = isLiquidGlassAvailable(); + // Semi-transparent background to prevent flashes while preserving glass effect + const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; + return ( <> - router.back()}> + router.back()} + tintColor={theme.backgroundEmphasis} + variant="prominent" + > diff --git a/companion/app/edit-availability-hours.tsx b/companion/app/edit-availability-hours.tsx index de65eb75f6..c16c2d7929 100644 --- a/companion/app/edit-availability-hours.tsx +++ b/companion/app/edit-availability-hours.tsx @@ -1,10 +1,12 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect } from "react"; -import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; +import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen"; +import { getColors } from "@/constants/colors"; import { useScheduleById } from "@/hooks/useSchedules"; import { showErrorAlert } from "@/utils/alerts"; @@ -12,6 +14,9 @@ export default function EditAvailabilityHours() { const { id } = useLocalSearchParams<{ id: string }>(); const router = useRouter(); const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); // Use React Query hook to read from cache (syncs with mutations) const { data: schedule, isLoading, isError } = useScheduleById(id ? Number(id) : undefined); @@ -41,37 +46,57 @@ export default function EditAvailabilityHours() { if (isLoading) { return ( - + ); } return ( - + ( - - - + + + ), }} diff --git a/companion/app/edit-availability-name.ios.tsx b/companion/app/edit-availability-name.ios.tsx index b5f289b88b..adcc2d414c 100644 --- a/companion/app/edit-availability-name.ios.tsx +++ b/companion/app/edit-availability-name.ios.tsx @@ -2,15 +2,15 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { getColors } from "@/constants/colors"; import type { EditAvailabilityNameScreenHandle } from "@/components/screens/EditAvailabilityNameScreen.ios"; import EditAvailabilityNameScreenComponent from "@/components/screens/EditAvailabilityNameScreen.ios"; import { CalComAPIService, type Schedule } from "@/services/calcom"; import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect -const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; function getPresentationStyle(): "formSheet" | "modal" { if (isLiquidGlassAvailable() && osName !== "iPadOS") { @@ -23,6 +23,9 @@ export default function EditAvailabilityNameIOS() { const { id } = useLocalSearchParams<{ id: string }>(); const router = useRouter(); const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const [schedule, setSchedule] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); @@ -57,6 +60,9 @@ export default function EditAvailabilityNameIOS() { const presentationStyle = getPresentationStyle(); const useGlassEffect = isLiquidGlassAvailable(); + // Semi-transparent background to prevent flashes while preserving glass effect + const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; + return ( <> - router.back()}> + router.back()} + tintColor={theme.backgroundEmphasis} + variant="prominent" + > @@ -86,7 +96,7 @@ export default function EditAvailabilityNameIOS() { onPress={handleSave} disabled={isSaving} variant="prominent" - tintColor="#000" + tintColor={theme.text} > diff --git a/companion/app/edit-availability-name.tsx b/companion/app/edit-availability-name.tsx index bc7244c8b3..3acc2e7428 100644 --- a/companion/app/edit-availability-name.tsx +++ b/companion/app/edit-availability-name.tsx @@ -1,11 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { EditAvailabilityNameScreenHandle } from "@/components/screens/EditAvailabilityNameScreen"; import EditAvailabilityNameScreenComponent from "@/components/screens/EditAvailabilityNameScreen"; +import { getColors } from "@/constants/colors"; import { CalComAPIService, type Schedule } from "@/services/calcom"; import { showErrorAlert } from "@/utils/alerts"; @@ -16,6 +18,9 @@ export default function EditAvailabilityName() { const [schedule, setSchedule] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const screenRef = useRef(null); @@ -52,49 +57,74 @@ export default function EditAvailabilityName() { if (isLoading) { return ( - + ); } return ( - + ( - - - + + + ), headerRight: () => ( - - Save - + + ), }} diff --git a/companion/app/edit-availability-override.ios.tsx b/companion/app/edit-availability-override.ios.tsx index 326c8208dd..19a75ed1a2 100644 --- a/companion/app/edit-availability-override.ios.tsx +++ b/companion/app/edit-availability-override.ios.tsx @@ -2,15 +2,15 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { getColors } from "@/constants/colors"; import type { EditAvailabilityOverrideScreenHandle } from "@/components/screens/EditAvailabilityOverrideScreen.ios"; import EditAvailabilityOverrideScreenComponent from "@/components/screens/EditAvailabilityOverrideScreen.ios"; import { CalComAPIService, type Schedule } from "@/services/calcom"; import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect -const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; function getPresentationStyle(): "formSheet" | "modal" { if (isLiquidGlassAvailable() && osName !== "iPadOS") { @@ -26,6 +26,9 @@ export default function EditAvailabilityOverrideIOS() { }>(); const router = useRouter(); const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const [schedule, setSchedule] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); @@ -72,6 +75,9 @@ export default function EditAvailabilityOverrideIOS() { const presentationStyle = getPresentationStyle(); const useGlassEffect = isLiquidGlassAvailable(); + // Semi-transparent background to prevent flashes while preserving glass effect + const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; + return ( <> - router.back()}> + router.back()} + tintColor={theme.backgroundEmphasis} + variant="prominent" + > @@ -101,7 +115,7 @@ export default function EditAvailabilityOverrideIOS() { onPress={handleSave} disabled={isSaving} variant="prominent" - tintColor="#000" + tintColor={theme.text} > diff --git a/companion/app/edit-availability-override.tsx b/companion/app/edit-availability-override.tsx index 79a39c912a..b5f1d59179 100644 --- a/companion/app/edit-availability-override.tsx +++ b/companion/app/edit-availability-override.tsx @@ -1,11 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { EditAvailabilityOverrideScreenHandle } from "@/components/screens/EditAvailabilityOverrideScreen"; import EditAvailabilityOverrideScreenComponent from "@/components/screens/EditAvailabilityOverrideScreen"; +import { getColors } from "@/constants/colors"; import { CalComAPIService, type Schedule } from "@/services/calcom"; import { showErrorAlert } from "@/utils/alerts"; @@ -19,6 +21,9 @@ export default function EditAvailabilityOverride() { const [schedule, setSchedule] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const screenRef = useRef(null); @@ -69,49 +74,74 @@ export default function EditAvailabilityOverride() { if (isLoading) { return ( - + ); } return ( - + ( - - - + + + ), headerRight: () => ( - - Save - + + ), }} diff --git a/companion/app/edit-location.ios.tsx b/companion/app/edit-location.ios.tsx index cf22e26672..ec0b5ab3f9 100644 --- a/companion/app/edit-location.ios.tsx +++ b/companion/app/edit-location.ios.tsx @@ -2,15 +2,15 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { getColors } from "@/constants/colors"; import type { EditLocationScreenHandle } from "@/components/screens/EditLocationScreen"; import EditLocationScreenComponent from "@/components/screens/EditLocationScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect -const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; function getPresentationStyle(): "formSheet" | "modal" { if (isLiquidGlassAvailable() && osName !== "iPadOS") { @@ -26,6 +26,9 @@ export default function EditLocationIOS() { const [booking, setBooking] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const editLocationScreenRef = useRef(null); @@ -57,6 +60,9 @@ export default function EditLocationIOS() { const presentationStyle = getPresentationStyle(); const useGlassEffect = isLiquidGlassAvailable(); + // Semi-transparent background to prevent flashes while preserving glass effect + const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; + return ( <> - router.back()}> + router.back()} + tintColor={theme.backgroundEmphasis} + variant="prominent" + > @@ -86,7 +96,7 @@ export default function EditLocationIOS() { onPress={handleSave} disabled={isSaving} variant="prominent" - tintColor="#000" + tintColor={theme.text} > @@ -96,14 +106,14 @@ export default function EditLocationIOS() { {isLoading ? ( - + ) : ( (null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const editLocationScreenRef = useRef(null); @@ -46,12 +50,20 @@ export default function EditLocation() { const renderHeaderLeft = useCallback( () => ( - router.back()} className="px-2 py-2"> - + router.back()} + className="h-10 w-10 items-center justify-center rounded-full border" + style={{ + borderColor: theme.border, + backgroundColor: theme.background, + marginRight: 8, + }} + > + ), - [router] + [router, theme.border, theme.background, theme.text] ); const renderHeaderRight = useCallback( @@ -60,13 +72,19 @@ export default function EditLocation() { - + ), - [handleSave, isSaving] + [handleSave, isSaving, theme.text, theme.border, theme.background] ); if (isLoading) { @@ -85,8 +103,11 @@ export default function EditLocation() { )} - - + + ); @@ -98,6 +119,15 @@ export default function EditLocation() { options={{ title: "Edit Location", headerBackButtonDisplayMode: "minimal", + contentStyle: { + backgroundColor: theme.background, + }, + headerStyle: { + backgroundColor: theme.background, + }, + headerTitleStyle: { + color: theme.text, + }, headerLeft: Platform.OS !== "ios" ? renderHeaderLeft : undefined, headerRight: Platform.OS !== "ios" ? renderHeaderRight : undefined, }} diff --git a/companion/app/mark-no-show.ios.tsx b/companion/app/mark-no-show.ios.tsx index c4a383568c..5818bab994 100644 --- a/companion/app/mark-no-show.ios.tsx +++ b/companion/app/mark-no-show.ios.tsx @@ -2,8 +2,9 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useEffect, useState } from "react"; -import { ActivityIndicator, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { getColors } from "@/constants/colors"; import MarkNoShowScreenComponent from "@/components/screens/MarkNoShowScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; import { showErrorAlert } from "@/utils/alerts"; @@ -37,6 +38,9 @@ export default function MarkNoShowIOS() { const [booking, setBooking] = useState(null); const [attendees, setAttendees] = useState([]); const [isLoading, setIsLoading] = useState(true); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); useEffect(() => { if (uid) { @@ -82,7 +86,7 @@ export default function MarkNoShowIOS() { sheetAllowedDetents: [0.7, 1], sheetInitialDetentIndex: 0, contentStyle: { - backgroundColor: useGlassEffect ? "transparent" : "#F2F2F7", + backgroundColor: useGlassEffect ? "transparent" : theme.background, }, }} /> @@ -100,14 +104,14 @@ export default function MarkNoShowIOS() { {isLoading ? ( - + ) : ( (null); const [attendees, setAttendees] = useState([]); const [isLoading, setIsLoading] = useState(true); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); useEffect(() => { if (uid) { @@ -65,12 +69,20 @@ export default function MarkNoShow() { const renderHeaderLeft = useCallback( () => ( - router.back()} className="px-2 py-2"> - + router.back()} + className="h-10 w-10 items-center justify-center rounded-full border" + style={{ + borderColor: theme.border, + backgroundColor: theme.background, + marginRight: 8, + }} + > + ), - [router] + [router, theme.border, theme.background, theme.text] ); if (isLoading) { @@ -89,8 +101,11 @@ export default function MarkNoShow() { )} - - + + ); diff --git a/companion/app/meeting-session-details.ios.tsx b/companion/app/meeting-session-details.ios.tsx index 2a959e3525..6380403098 100644 --- a/companion/app/meeting-session-details.ios.tsx +++ b/companion/app/meeting-session-details.ios.tsx @@ -2,8 +2,9 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useEffect, useState } from "react"; -import { ActivityIndicator, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { getColors } from "@/constants/colors"; import MeetingSessionDetailsScreenComponent from "@/components/screens/MeetingSessionDetailsScreen"; import { CalComAPIService } from "@/services/calcom"; import type { ConferencingSession } from "@/services/types/bookings.types"; @@ -27,6 +28,9 @@ export default function MeetingSessionDetailsIOS() { const insets = useSafeAreaInsets(); const [sessions, setSessions] = useState([]); const [isLoading, setIsLoading] = useState(true); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); useEffect(() => { if (uid) { @@ -60,7 +64,7 @@ export default function MeetingSessionDetailsIOS() { sheetAllowedDetents: [0.7, 1], sheetInitialDetentIndex: 0, contentStyle: { - backgroundColor: useGlassEffect ? "transparent" : "#F2F2F7", + backgroundColor: useGlassEffect ? "transparent" : theme.background, }, }} /> @@ -78,14 +82,14 @@ export default function MeetingSessionDetailsIOS() { {isLoading ? ( - + ) : ( ([]); const [isLoading, setIsLoading] = useState(true); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); useEffect(() => { if (uid) { @@ -37,12 +41,20 @@ export default function MeetingSessionDetails() { const renderHeaderLeft = useCallback( () => ( - router.back()} className="px-2 py-2"> - + router.back()} + className="h-10 w-10 items-center justify-center rounded-full border" + style={{ + borderColor: theme.border, + backgroundColor: theme.background, + marginRight: 8, + }} + > + ), - [router] + [router, theme.border, theme.background, theme.text] ); if (isLoading) { @@ -61,8 +73,11 @@ export default function MeetingSessionDetails() { )} - - + + ); diff --git a/companion/app/oauth/callback.tsx b/companion/app/oauth/callback.tsx index 9ef75197e2..d2115ee8d8 100644 --- a/companion/app/oauth/callback.tsx +++ b/companion/app/oauth/callback.tsx @@ -97,10 +97,12 @@ export default function OAuthCallback() { }, [params, router, auth.userInfo]); return ( - + - Processing OAuth callback... - + + Processing OAuth callback... + + Please wait while we complete the authentication. diff --git a/companion/app/profile-sheet.ios.tsx b/companion/app/profile-sheet.ios.tsx index 6ccd9eff9e..0d055d12b5 100644 --- a/companion/app/profile-sheet.ios.tsx +++ b/companion/app/profile-sheet.ios.tsx @@ -4,8 +4,16 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import * as Haptics from "expo-haptics"; import { Stack, useRouter } from "expo-router"; -import { ActivityIndicator, Image, Text, TouchableOpacity, View } from "react-native"; +import { + ActivityIndicator, + Image, + Text, + TouchableOpacity, + useColorScheme, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { getColors } from "@/constants/colors"; import { useUserProfile } from "@/hooks"; import { showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; @@ -35,6 +43,19 @@ export default function ProfileSheet() { const router = useRouter(); const insets = useSafeAreaInsets(); const { data: userProfile, isLoading } = useUserProfile(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + + const colors = { + background: isDark ? "#000000" : "#FFFFFF", + text: isDark ? "#FFFFFF" : "#111827", + textSecondary: isDark ? "#A3A3A3" : "#6B7280", + icon: isDark ? "#FFFFFF" : "#374151", + iconSecondary: isDark ? "#636366" : "#9CA3AF", + avatarBg: isDark ? "#4D4D4D" : "#E5E7EB", + avatarText: isDark ? "#FFFFFF" : "#4B5563", + }; const publicPageUrl = userProfile?.username ? `https://cal.com/${userProfile.username}` : null; @@ -103,6 +124,9 @@ export default function ProfileSheet() { const presentationStyle = getPresentationStyle(); const useGlassEffect = isLiquidGlassAvailable(); + // Semi-transparent background to prevent flashes while preserving glass effect + const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; + return ( <> null, - headerRight: () => ( - - - - ), + headerRight: () => null, }} /> + + You + + + + + + + + @@ -142,7 +185,7 @@ export default function ProfileSheet() { {isLoading ? ( - + ) : ( @@ -154,10 +197,10 @@ export default function ProfileSheet() { /> ) : ( - + {userProfile?.name?.charAt(0).toUpperCase() || userProfile?.email?.charAt(0).toUpperCase() || "?"} @@ -167,11 +210,13 @@ export default function ProfileSheet() { {/* Name and Status */} - + {userProfile?.name || "User"} {userProfile?.email ? ( - {userProfile.email} + + {userProfile.email} + ) : null} @@ -183,19 +228,22 @@ export default function ProfileSheet() { {menuItems.map((item, index) => ( - - {item.label} + + + {item.label} + {item.external ? ( - + ) : ( - + )} ))} diff --git a/companion/app/profile-sheet.tsx b/companion/app/profile-sheet.tsx index 0ad0aa5e69..e341e1ce32 100644 --- a/companion/app/profile-sheet.tsx +++ b/companion/app/profile-sheet.tsx @@ -2,7 +2,15 @@ import { Ionicons } from "@expo/vector-icons"; import * as Clipboard from "expo-clipboard"; import * as Haptics from "expo-haptics"; import { Stack, useRouter } from "expo-router"; -import { ActivityIndicator, Image, ScrollView, Text, TouchableOpacity, View } from "react-native"; +import { + ActivityIndicator, + Image, + ScrollView, + Text, + TouchableOpacity, + useColorScheme, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import { useUserProfile } from "@/hooks"; @@ -22,6 +30,19 @@ export default function ProfileSheet() { const router = useRouter(); const insets = useSafeAreaInsets(); const { data: userProfile, isLoading } = useUserProfile(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + + const colors = { + background: isDark ? "#000000" : "#FFFFFF", + text: isDark ? "#FFFFFF" : "#111827", + textSecondary: isDark ? "#A3A3A3" : "#6B7280", + icon: isDark ? "#FFFFFF" : "#374151", + iconSecondary: isDark ? "#636366" : "#9CA3AF", + avatarBg: isDark ? "#4D4D4D" : "#E5E7EB", + avatarText: isDark ? "#FFFFFF" : "#4B5563", + activeBackground: isDark ? "#171717" : "#F3F4F6", + }; const publicPageUrl = userProfile?.username ? `https://cal.com/${userProfile.username}` : null; @@ -103,16 +124,17 @@ export default function ProfileSheet() { title: "Profile", presentation: "modal", contentStyle: { - backgroundColor: "#FFFFFF", + backgroundColor: colors.background, }, headerStyle: { - backgroundColor: "#FFFFFF", + backgroundColor: colors.background, }, + headerTintColor: colors.text, headerLeft: () => null, headerRight: () => ( - + ), @@ -120,14 +142,14 @@ export default function ProfileSheet() { /> {/* Profile Header */} {isLoading ? ( - + ) : ( @@ -139,10 +161,10 @@ export default function ProfileSheet() { /> ) : ( - + {userProfile?.name?.charAt(0).toUpperCase() || userProfile?.email?.charAt(0).toUpperCase() || "?"} @@ -152,11 +174,13 @@ export default function ProfileSheet() { {/* Name and Email */} - + {userProfile?.name || "User"} {userProfile?.email ? ( - {userProfile.email} + + {userProfile.email} + ) : null} @@ -168,19 +192,22 @@ export default function ProfileSheet() { {menuItems.map((item, index) => ( - - {item.label} + + + {item.label} + {item.external ? ( - + ) : ( - + )} ))} diff --git a/companion/app/reschedule.ios.tsx b/companion/app/reschedule.ios.tsx index 1c5ccfaa7d..d0d6ff54ea 100644 --- a/companion/app/reschedule.ios.tsx +++ b/companion/app/reschedule.ios.tsx @@ -2,15 +2,15 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { getColors } from "@/constants/colors"; import type { RescheduleScreenHandle } from "@/components/screens/RescheduleScreen"; import RescheduleScreenComponent from "@/components/screens/RescheduleScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect -const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; function getPresentationStyle(): "formSheet" | "modal" { if (isLiquidGlassAvailable() && osName !== "iPadOS") { @@ -26,6 +26,9 @@ export default function RescheduleIOS() { const [booking, setBooking] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const rescheduleScreenRef = useRef(null); @@ -57,6 +60,9 @@ export default function RescheduleIOS() { const presentationStyle = getPresentationStyle(); const useGlassEffect = isLiquidGlassAvailable(); + // Semi-transparent background to prevent flashes while preserving glass effect + const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)"; + return ( <> - router.back()}> + router.back()} + tintColor={theme.backgroundEmphasis} + variant="prominent" + > @@ -86,7 +100,7 @@ export default function RescheduleIOS() { onPress={handleSave} disabled={isSaving} variant="prominent" - tintColor="#000" + tintColor={theme.text} > @@ -96,14 +110,18 @@ export default function RescheduleIOS() { {isLoading ? ( - + ) : ( (null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const rescheduleScreenRef = useRef(null); @@ -46,12 +50,20 @@ export default function Reschedule() { const renderHeaderLeft = useCallback( () => ( - router.back()} className="px-2 py-2"> - + router.back()} + className="h-10 w-10 items-center justify-center rounded-full border" + style={{ + borderColor: theme.border, + backgroundColor: theme.background, + marginRight: 8, + }} + > + ), - [router] + [router, theme.border, theme.background, theme.text] ); const renderHeaderRight = useCallback( @@ -60,13 +72,19 @@ export default function Reschedule() { - + ), - [handleSave, isSaving] + [handleSave, isSaving, theme.text, theme.border, theme.background] ); if (isLoading) { @@ -85,8 +103,11 @@ export default function Reschedule() { )} - - + + ); diff --git a/companion/app/view-recordings.ios.tsx b/companion/app/view-recordings.ios.tsx index 3cfa6dd43b..7022ff5814 100644 --- a/companion/app/view-recordings.ios.tsx +++ b/companion/app/view-recordings.ios.tsx @@ -2,8 +2,9 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useEffect, useState } from "react"; -import { ActivityIndicator, View } from "react-native"; +import { ActivityIndicator, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { getColors } from "@/constants/colors"; import ViewRecordingsScreenComponent from "@/components/screens/ViewRecordingsScreen"; import { CalComAPIService } from "@/services/calcom"; import type { BookingRecording } from "@/services/types/bookings.types"; @@ -23,6 +24,9 @@ export default function ViewRecordingsIOS() { const insets = useSafeAreaInsets(); const [recordings, setRecordings] = useState([]); const [isLoading, setIsLoading] = useState(true); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); useEffect(() => { if (uid) { @@ -55,7 +59,7 @@ export default function ViewRecordingsIOS() { sheetAllowedDetents: [0.7, 1], sheetInitialDetentIndex: 0, contentStyle: { - backgroundColor: useGlassEffect ? "transparent" : "#F2F2F7", + backgroundColor: useGlassEffect ? "transparent" : theme.background, }, }} /> @@ -73,14 +77,14 @@ export default function ViewRecordingsIOS() { {isLoading ? ( - + ) : ( ([]); const [isLoading, setIsLoading] = useState(true); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); useEffect(() => { if (uid) { @@ -37,12 +41,20 @@ export default function ViewRecordings() { const renderHeaderLeft = useCallback( () => ( - router.back()} className="px-2 py-2"> - + router.back()} + className="h-10 w-10 items-center justify-center rounded-full border" + style={{ + borderColor: theme.border, + backgroundColor: theme.background, + marginRight: 8, + }} + > + ), - [router] + [router, theme.border, theme.background, theme.text] ); if (isLoading) { @@ -61,8 +73,11 @@ export default function ViewRecordings() { )} - - + + ); diff --git a/companion/components/BookingActionsModal.ios.tsx b/companion/components/BookingActionsModal.ios.tsx index 9a5e6c69c0..ee775f232f 100644 --- a/companion/components/BookingActionsModal.ios.tsx +++ b/companion/components/BookingActionsModal.ios.tsx @@ -6,11 +6,12 @@ */ import { Ionicons } from "@expo/vector-icons"; -import { ScrollView, Text, TouchableOpacity, View } from "react-native"; +import { ScrollView, Text, TouchableOpacity, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { Booking } from "@/services/calcom"; import type { BookingActionsResult } from "@/utils/booking-actions"; import { FullScreenModal } from "./FullScreenModal"; +import { getColors } from "@/constants/colors"; export interface BookingActionsModalProps { visible: boolean; @@ -60,10 +61,14 @@ function ActionButton({ isDanger = false, isLast = false, }: ActionButtonProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + if (!visible) return null; - const iconColor = !enabled ? "#C7C7CC" : isDanger ? "#FF3B30" : "#007AFF"; - const textColor = !enabled ? "#C7C7CC" : isDanger ? "#FF3B30" : "#000"; + const iconColor = !enabled ? "#C7C7CC" : isDanger ? theme.destructive : "#007AFF"; + const textColor = !enabled ? "#C7C7CC" : isDanger ? theme.destructive : theme.text; return ( @@ -87,8 +92,8 @@ function ActionButton({ {label} {!enabled && ( - - Unavailable + + Unavailable )} @@ -101,8 +106,10 @@ interface SectionHeaderProps { function SectionHeader({ title }: SectionHeaderProps) { return ( - - {title} + + + {title} + ); } @@ -248,10 +255,13 @@ export function BookingActionsModal({ style={{ paddingBottom: insets.bottom || 16 }} > {/* Actions Card */} - + {/* Booking Title Header */} - - + + {booking.title} @@ -316,7 +326,7 @@ export function BookingActionsModal({ {/* Cancel Button */} diff --git a/companion/components/BookingActionsModal.tsx b/companion/components/BookingActionsModal.tsx index 4ad27dbba9..e759665220 100644 --- a/companion/components/BookingActionsModal.tsx +++ b/companion/components/BookingActionsModal.tsx @@ -11,7 +11,8 @@ */ import { Ionicons } from "@expo/vector-icons"; -import { ScrollView, Text, TouchableOpacity, View } from "react-native"; +import { ScrollView, Text, TouchableOpacity, useColorScheme, View } from "react-native"; +import { getColors } from "@/constants/colors"; import type { Booking } from "@/services/calcom"; import type { BookingActionsResult } from "@/utils/booking-actions"; import { FullScreenModal } from "./FullScreenModal"; @@ -64,10 +65,13 @@ function ActionButton({ isDanger = false, isLast = false, }: ActionButtonProps) { - if (!visible) return null; + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); - const iconColor = !enabled ? "#D1D5DB" : isDanger ? "#800020" : "#6B7280"; - const textColor = !enabled ? "#D1D5DB" : isDanger ? "#800020" : "#111827"; + if (!visible) return null; + const iconColor = !enabled ? "#D1D5DB" : isDanger ? theme.destructive : "#6B7280"; + const textColor = !enabled ? "#D1D5DB" : isDanger ? theme.destructive : "#111827"; return ( @@ -88,8 +92,8 @@ function ActionButton({ {label} {!enabled && ( - - Unavailable + + Unavailable )} @@ -102,8 +106,8 @@ interface SectionHeaderProps { function SectionHeader({ title }: SectionHeaderProps) { return ( - - + + {title} @@ -248,10 +252,13 @@ export function BookingActionsModal({ onPress={(e) => e.stopPropagation()} > {/* Actions Card */} - + {/* Booking Title Header */} - - + + {booking.title} @@ -316,12 +323,14 @@ export function BookingActionsModal({ {/* Cancel Button */} - Cancel + + Cancel + diff --git a/companion/components/CacheStatusIndicator.tsx b/companion/components/CacheStatusIndicator.tsx index 597732848f..36e2cb4525 100644 --- a/companion/components/CacheStatusIndicator.tsx +++ b/companion/components/CacheStatusIndicator.tsx @@ -92,7 +92,7 @@ export function CacheStatusIndicator({ if (isFetching) { return ( - + {!compact && Updating...} ); @@ -103,14 +103,14 @@ export function CacheStatusIndicator({ if (compact) { return ( - + ); } return ( - + Updated {formatTimeAgo(dataUpdatedAt)} ); @@ -133,7 +133,7 @@ const styles = StyleSheet.create({ }, text: { fontSize: 11, - color: "#8E8E93", + color: "#A3A3A3", }, offlineText: { fontSize: 11, diff --git a/companion/components/EmptyScreen.tsx b/companion/components/EmptyScreen.tsx index fbb41a9558..8610e78315 100644 --- a/companion/components/EmptyScreen.tsx +++ b/companion/components/EmptyScreen.tsx @@ -1,5 +1,5 @@ import { Ionicons } from "@expo/vector-icons"; -import { Text, TouchableOpacity, View } from "react-native"; +import { Text, TouchableOpacity, useColorScheme, View } from "react-native"; type IoniconName = keyof typeof Ionicons.glyphMap; @@ -20,27 +20,71 @@ export function EmptyScreen({ onButtonPress, className, }: EmptyScreenProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + + const colors = { + text: isDark ? "#FFFFFF" : "#111827", + textSecondary: isDark ? "#A3A3A3" : "#6B7280", + background: isDark ? "#171717" : "#FFFFFF", + backgroundSecondary: isDark ? "#2C2C2E" : "#F3F4F6", + border: isDark ? "#4D4D4D" : "#D1D5DB", + icon: isDark ? "#E5E5EA" : "#374151", + buttonBg: isDark ? "#FFFFFF" : "#111827", + buttonText: isDark ? "#000000" : "#FFFFFF", + }; + return ( - - + + - {headline} + + {headline} + - + {description} {buttonText && onButtonPress ? ( - {buttonText} + + {buttonText} + ) : null} diff --git a/companion/components/GlassModalHeader.tsx b/companion/components/GlassModalHeader.tsx index a6ecd9ce63..832b7b5c6d 100644 --- a/companion/components/GlassModalHeader.tsx +++ b/companion/components/GlassModalHeader.tsx @@ -1,5 +1,5 @@ import { Ionicons } from "@expo/vector-icons"; -import { ActivityIndicator, Text, View } from "react-native"; +import { ActivityIndicator, Text, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppPressable } from "./AppPressable"; @@ -24,6 +24,9 @@ export function GlassModalHeader({ actionIcon, }: GlassModalHeaderProps) { const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const iconColor = isDark ? "#fff" : "#000"; const isDisabled = actionDisabled || actionLoading; @@ -31,8 +34,8 @@ export function GlassModalHeader({ {/* Back Button - matching native iOS style */} @@ -41,11 +44,14 @@ export function GlassModalHeader({ onPress={onClose} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > - + {/* Title - centered */} - + {title} @@ -53,7 +59,7 @@ export function GlassModalHeader({ {onAction ? ( {actionLoading ? ( - + ) : actionIcon ? ( // Icon button style (like the three-dot menu) - + ) : ( // Text button style (like "Save", "Done") - - {actionLabel} + + + {actionLabel} + )} diff --git a/companion/components/Header.tsx b/companion/components/Header.tsx index 665b2ac8f3..65a59b0121 100644 --- a/companion/components/Header.tsx +++ b/companion/components/Header.tsx @@ -1,7 +1,15 @@ import { Ionicons } from "@expo/vector-icons"; import { useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Image, Platform, Text, TouchableOpacity, View } from "react-native"; +import { + ActivityIndicator, + Image, + Platform, + Text, + TouchableOpacity, + useColorScheme, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { CalComAPIService, type UserProfile } from "@/services/calcom"; import { getAvatarUrl } from "@/utils/getAvatarUrl"; @@ -53,9 +61,20 @@ export function Header({ }: HeaderProps) { const router = useRouter(); const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; const [userProfile, setUserProfile] = useState(null); const [loading, setLoading] = useState(true); + // iOS system colors for dark mode + const colors = { + text: isDark ? "#FFFFFF" : "#000000", + textSecondary: isDark ? "#A3A3A3" : "#666666", + background: isDark ? "#000000" : "#FFFFFF", + border: isDark ? "#4D4D4D" : "#E5E5EA", + logoColor: isDark ? "#FFFFFF" : "#333333", + }; + const fetchUserProfile = useCallback(async () => { try { const profile = await CalComAPIService.getUserProfile(); @@ -86,12 +105,12 @@ export function Header({ return ( {/* Left: Cal.com Logo */} - + {/* Right: Filter Dropdown + Profile */} @@ -103,14 +122,10 @@ export function Header({ {filterOptions && filterOptions.length > 0 && onFilterChange && ( - - + + {activeFilterLabel} - @@ -146,11 +161,13 @@ export function Header({ {option.label} @@ -168,14 +185,14 @@ export function Header({ - + {/* Badge for active filters */} {eventTypeFilterConfig.activeFilterCount > 0 && ( - + {eventTypeFilterConfig.activeFilterCount} @@ -193,8 +210,8 @@ export function Header({ - - Sort by + + Sort by @@ -209,13 +226,17 @@ export function Header({ : "text-outline" } size={16} - color={eventTypeFilterConfig.sortBy === "alphabetical" ? "#000000" : "#666"} + color={ + eventTypeFilterConfig.sortBy === "alphabetical" + ? colors.text + : colors.textSecondary + } /> Alphabetical @@ -231,13 +252,17 @@ export function Header({ : "calendar-outline" } size={16} - color={eventTypeFilterConfig.sortBy === "newest" ? "#000000" : "#666"} + color={ + eventTypeFilterConfig.sortBy === "newest" + ? colors.text + : colors.textSecondary + } /> Newest First @@ -253,13 +278,17 @@ export function Header({ : "time-outline" } size={16} - color={eventTypeFilterConfig.sortBy === "duration" ? "#000000" : "#666"} + color={ + eventTypeFilterConfig.sortBy === "duration" + ? colors.text + : colors.textSecondary + } /> By Duration @@ -274,8 +303,10 @@ export function Header({ {/* Filter Label */} - - Filters + + + Filters + @@ -289,13 +320,15 @@ export function Header({ : "eye-off-outline" } size={16} - color={eventTypeFilterConfig.filters.hiddenOnly ? "#000000" : "#666"} + color={ + eventTypeFilterConfig.filters.hiddenOnly ? colors.text : colors.textSecondary + } /> Hidden Only @@ -310,13 +343,15 @@ export function Header({ eventTypeFilterConfig.filters.paidOnly ? "checkmark-circle" : "cash-outline" } size={16} - color={eventTypeFilterConfig.filters.paidOnly ? "#000000" : "#666"} + color={ + eventTypeFilterConfig.filters.paidOnly ? colors.text : colors.textSecondary + } /> Paid Events @@ -333,13 +368,15 @@ export function Header({ : "people-outline" } size={16} - color={eventTypeFilterConfig.filters.seatedOnly ? "#000000" : "#666"} + color={ + eventTypeFilterConfig.filters.seatedOnly ? colors.text : colors.textSecondary + } /> Seated Events @@ -359,14 +396,16 @@ export function Header({ } size={16} color={ - eventTypeFilterConfig.filters.requiresConfirmationOnly ? "#000000" : "#666" + eventTypeFilterConfig.filters.requiresConfirmationOnly + ? colors.text + : colors.textSecondary } /> Requires Confirmation @@ -385,13 +424,17 @@ export function Header({ : "repeat-outline" } size={16} - color={eventTypeFilterConfig.filters.recurringOnly ? "#000000" : "#666"} + color={ + eventTypeFilterConfig.filters.recurringOnly + ? colors.text + : colors.textSecondary + } /> Recurring @@ -418,7 +461,7 @@ export function Header({ {/* Profile Picture */} {loading ? ( - + ) : userProfile?.avatarUrl ? ( ) : ( - + )} diff --git a/companion/components/LocationsList.tsx b/companion/components/LocationsList.tsx index 28d4af6651..8fa958b410 100644 --- a/companion/components/LocationsList.tsx +++ b/companion/components/LocationsList.tsx @@ -6,7 +6,16 @@ import { Ionicons } from "@expo/vector-icons"; import type React from "react"; import { useState } from "react"; -import { Modal, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from "react-native"; +import { + Modal, + Platform, + ScrollView, + Text, + TextInput, + TouchableOpacity, + useColorScheme, + View, +} from "react-native"; import type { LocationItem, LocationOptionGroup } from "@/types/locations"; import { createLocationItemFromOption, @@ -69,29 +78,34 @@ export const AddLocationTrigger = ({ loading?: boolean; onPressFallback?: () => void; }) => { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const trigger = isHeader ? ( - + ) : ( {loading ? ( - Loading options... + Loading options... ) : ( <> - Add Location + + Add Location + )} @@ -143,13 +157,13 @@ export const LocationsList: React.FC = ({ const renderLocationIcon = (location: LocationItem) => { if (location.iconUrl) { return ( - + ); } return ( - + ); @@ -183,9 +197,9 @@ export const LocationsList: React.FC = ({ return ( - {label} + {label} onUpdate(location.id, { [fieldKey]: text })} @@ -198,7 +212,7 @@ export const LocationsList: React.FC = ({ }; return ( - + {/* Locations List */} {locations.length > 0 ? ( @@ -206,7 +220,9 @@ export const LocationsList: React.FC = ({ = ({ {renderLocationIcon(location)} - + {location.displayName} @@ -260,10 +279,12 @@ export const LocationsList: React.FC = ({ onRequestClose={() => setShowAddModal(false)} > - + {/* Header */} - - Add Location + + + Add Location + setShowAddModal(false)} className="p-1"> @@ -273,7 +294,7 @@ export const LocationsList: React.FC = ({ {locationOptions.map((group, groupIndex) => ( 0 ? "mt-4" : ""}> - + {group.category} {group.options.map((option) => { @@ -285,7 +306,9 @@ export const LocationsList: React.FC = ({ onPress={() => handleSelectOption(option.value, option.label)} disabled={alreadyAdded} className={`flex-row items-center rounded-lg px-2 py-3 ${ - alreadyAdded ? "opacity-40" : "active:bg-gray-100" + alreadyAdded + ? "opacity-40" + : "active:bg-gray-100 dark:active:bg-[#262626]" }`} > {option.iconUrl ? ( @@ -300,7 +323,9 @@ export const LocationsList: React.FC = ({ )} - {option.label} + + {option.label} + {alreadyAdded && } ); diff --git a/companion/components/LoginScreen.tsx b/companion/components/LoginScreen.tsx index 3febdebae4..cdc9a2a6b8 100644 --- a/companion/components/LoginScreen.tsx +++ b/companion/components/LoginScreen.tsx @@ -1,4 +1,4 @@ -import { Platform, Text, TouchableOpacity, View } from "react-native"; +import { Platform, Text, TouchableOpacity, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useAuth } from "@/contexts/AuthContext"; import { showErrorAlert } from "@/utils/alerts"; @@ -8,6 +8,8 @@ import { CalComLogo } from "./CalComLogo"; export function LoginScreen() { const { loginWithOAuth, loading } = useAuth(); const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; const handleOAuthLogin = async () => { try { @@ -28,10 +30,10 @@ export function LoginScreen() { }; return ( - + {/* Logo centered in the middle */} - + {/* Bottom section with button */} @@ -42,13 +44,13 @@ export function LoginScreen() { disabled={loading} className="flex-row items-center justify-center rounded-2xl py-[18px]" style={[ - { backgroundColor: loading ? "#9CA3AF" : "#000000" }, + { backgroundColor: loading ? "#9CA3AF" : isDark ? "#FFFFFF" : "#000000" }, Platform.select({ web: { boxShadow: loading ? "none" : "0 4px 12px rgba(0, 0, 0, 0.2)", }, default: { - shadowColor: "#000", + shadowColor: isDark ? "#FFF" : "#000", shadowOffset: { width: 0, height: 4 }, shadowOpacity: loading ? 0 : 0.2, shadowRadius: 12, @@ -58,7 +60,12 @@ export function LoginScreen() { ]} activeOpacity={0.9} > - Continue with Cal.com + + Continue with Cal.com + {/* Sign up link - hidden on iOS */} @@ -70,10 +77,11 @@ export function LoginScreen() { activeOpacity={0.7} > - - Don't have an account? Sign up + + Don't have an account?{" "} + Sign up - + )} diff --git a/companion/components/LogoutConfirmModal.tsx b/companion/components/LogoutConfirmModal.tsx index a50db634bb..8ad6b101eb 100644 --- a/companion/components/LogoutConfirmModal.tsx +++ b/companion/components/LogoutConfirmModal.tsx @@ -1,4 +1,4 @@ -import { Platform, Text, TouchableOpacity, View } from "react-native"; +import { Platform, Text, TouchableOpacity, useColorScheme, View } from "react-native"; import { FullScreenModal } from "./FullScreenModal"; interface LogoutConfirmModalProps { @@ -8,27 +8,57 @@ interface LogoutConfirmModalProps { } export function LogoutConfirmModal({ visible, onConfirm, onCancel }: LogoutConfirmModalProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + + const colors = { + background: isDark ? "#171717" : "#FFFFFF", + text: isDark ? "#FFFFFF" : "#111827", + textSecondary: isDark ? "#A3A3A3" : "#4B5563", + border: isDark ? "#4D4D4D" : "#1F2937", + cancelBg: isDark ? "#2C2C2E" : "#FFFFFF", + }; + return ( - - Sign Out - Are you sure you want to sign out? + + + Sign Out + + + Are you sure you want to sign out? + - Cancel + + Cancel + Sign Out diff --git a/companion/components/ScreenWrapper.tsx b/companion/components/ScreenWrapper.tsx index 1e4c7a4797..5e0ee3facc 100644 --- a/companion/components/ScreenWrapper.tsx +++ b/companion/components/ScreenWrapper.tsx @@ -1,9 +1,10 @@ import { Ionicons } from "@expo/vector-icons"; import type { ReactNode } from "react"; -import { Text, TouchableOpacity, View } from "react-native"; +import { Text, TouchableOpacity, useColorScheme, View } from "react-native"; import { EmptyScreen } from "./EmptyScreen"; import { Header } from "./Header"; import { LoadingSpinner } from "./LoadingSpinner"; +import { getColors } from "@/constants/colors"; type IoniconName = keyof typeof Ionicons.glyphMap; @@ -68,11 +69,16 @@ export function ScreenWrapper({ showHeader = true, children, }: ScreenWrapperProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + const errorIconColor = theme.destructive; + if (loading) { return ( - + {showHeader &&
} - + @@ -81,17 +87,22 @@ export function ScreenWrapper({ if (error) { return ( - + {showHeader &&
} - - - + + + {errorTitle} - {error} + + {error} + {onRetry && ( - - Retry + + Retry )} @@ -101,9 +112,9 @@ export function ScreenWrapper({ if (isEmpty && emptyProps) { return ( - + {showHeader &&
} - + @@ -111,7 +122,7 @@ export function ScreenWrapper({ } return ( - + {showHeader &&
} {children} diff --git a/companion/components/SearchHeader.tsx b/companion/components/SearchHeader.tsx index 4ae12e5191..0d189c40ae 100644 --- a/companion/components/SearchHeader.tsx +++ b/companion/components/SearchHeader.tsx @@ -1,5 +1,5 @@ import { Ionicons } from "@expo/vector-icons"; -import { Text, TextInput, TouchableOpacity, View } from "react-native"; +import { Text, TextInput, TouchableOpacity, useColorScheme, View } from "react-native"; interface SearchHeaderProps { /** Current search query value */ @@ -35,10 +35,13 @@ export function SearchHeader({ onNewPress, newButtonText = "New", }: SearchHeaderProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + return ( - + - - {newButtonText} + + {newButtonText} ); diff --git a/companion/components/availability-list-item/AvailabilityListItem.ios.tsx b/companion/components/availability-list-item/AvailabilityListItem.ios.tsx index 5e0debaefb..59982e8f66 100644 --- a/companion/components/availability-list-item/AvailabilityListItem.ios.tsx +++ b/companion/components/availability-list-item/AvailabilityListItem.ios.tsx @@ -1,7 +1,7 @@ import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui"; import { buttonStyle, frame } from "@expo/ui/swift-ui/modifiers"; import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Pressable, View } from "react-native"; +import { Pressable, useColorScheme, View } from "react-native"; import type { SFSymbols7_0 } from "sf-symbols-typescript"; import type { AvailabilityListItemProps } from "./AvailabilityListItem"; import { AvailabilitySlots, ScheduleName, TimeZoneRow } from "./AvailabilityListItemParts"; @@ -43,8 +43,17 @@ export const AvailabilityListItem = ({ }, ]; + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + return ( - + {/* Native iOS Context Menu for long-press */} + - + @@ -122,10 +145,20 @@ export const AvailabilityListItem = ({ - + {action.label} diff --git a/companion/components/availability-list-item/AvailabilityListItemParts.tsx b/companion/components/availability-list-item/AvailabilityListItemParts.tsx index 3dbfd46f7e..fd1759badd 100644 --- a/companion/components/availability-list-item/AvailabilityListItemParts.tsx +++ b/companion/components/availability-list-item/AvailabilityListItemParts.tsx @@ -1,5 +1,5 @@ import { Ionicons } from "@expo/vector-icons"; -import { Text, TouchableOpacity, View } from "react-native"; +import { Text, TouchableOpacity, useColorScheme, View } from "react-native"; import type { Schedule } from "@/hooks"; // Convert 24-hour time to 12-hour format with AM/PM @@ -18,11 +18,19 @@ interface ScheduleNameProps { } export function ScheduleName({ name, isDefault }: ScheduleNameProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + return ( - {name} + + {name} + {isDefault && ( - + Default )} @@ -36,8 +44,16 @@ interface AvailabilitySlotsProps { } export function AvailabilitySlots({ availability, scheduleId }: AvailabilitySlotsProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const textColor = isDark ? "#A3A3A3" : "#666666"; + if (!availability || availability.length === 0) { - return No availability set; + return ( + + No availability set + + ); } const MAX_VISIBLE_SLOTS = 2; @@ -51,14 +67,14 @@ export function AvailabilitySlots({ availability, scheduleId }: AvailabilitySlot key={`${scheduleId}-${slot.days.join("-")}-${slotIndex}`} className={slotIndex > 0 ? "mt-1" : ""} > - + {slot.days.join(", ")} {formatTime12Hour(slot.startTime)} -{" "} {formatTime12Hour(slot.endTime)} ))} {remainingCount > 0 && ( - + +{remainingCount} more {remainingCount === 1 ? "slot" : "slots"} )} @@ -71,10 +87,16 @@ interface TimeZoneRowProps { } export function TimeZoneRow({ timeZone }: TimeZoneRowProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const iconColor = isDark ? "#A3A3A3" : "#666666"; + return ( - - {timeZone} + + + {timeZone} + ); } @@ -90,17 +112,27 @@ export function ScheduleActionsButton({ setSelectedSchedule, setShowActionsModal, }: ScheduleActionsButtonProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + return ( { e.stopPropagation(); setSelectedSchedule(schedule); setShowActionsModal(true); }} > - + ); } diff --git a/companion/components/availability-list-item/AvailabilityListItemSkeleton.tsx b/companion/components/availability-list-item/AvailabilityListItemSkeleton.tsx index 1e6723fa1e..a1e48862d8 100644 --- a/companion/components/availability-list-item/AvailabilityListItemSkeleton.tsx +++ b/companion/components/availability-list-item/AvailabilityListItemSkeleton.tsx @@ -1,5 +1,6 @@ -import { ScrollView, View } from "react-native"; +import { ScrollView, useColorScheme, View } from "react-native"; import { Skeleton } from "@/components/ui/skeleton"; +import { getColors } from "@/constants/colors"; interface AvailabilityListItemSkeletonProps { isLast?: boolean; @@ -8,8 +9,18 @@ interface AvailabilityListItemSkeletonProps { export function AvailabilityListItemSkeleton({ isLast = false, }: AvailabilityListItemSkeletonProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + return ( - + @@ -71,7 +86,7 @@ export function AvailabilityListSkeleton({ flex: 1, borderRadius: 8, borderWidth: 1, - borderColor: "#E5E5EA", + borderColor: theme.border, marginHorizontal: 8, marginVertical: 4, overflow: "hidden", @@ -91,8 +106,15 @@ export function AvailabilityListSkeleton({ return ( {Array.from({ length: count }).map((_, index) => ( = ({ }); }, [booking, userEmail]); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + // Define context menu actions based on booking state type ContextMenuAction = { label: string; @@ -136,7 +139,13 @@ export const BookingListItem: React.FC = ({ .map(({ label, icon, onPress, role }) => ({ label, icon, onPress, role })); return ( - + {/* Native iOS Context Menu for long-press */} = ({ onPress(booking)} - style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }} - className="active:bg-cal-bg-secondary" + style={{ + paddingHorizontal: 16, + paddingTop: 16, + paddingBottom: 12, + }} + className="active:bg-cal-bg-secondary dark:active:bg-[#171717]" > = ({ onReportBooking, onCancelBooking, }) => { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + const [isPressed, setIsPressed] = useState(false); + + const colors = { + icon: isDark ? "#FFFFFF" : "#3C3F44", + iconDestructive: theme.destructive, + iconDefault: isDark ? "#E5E5EA" : "#374151", + }; + const { isUpcoming, isPending, @@ -148,12 +160,24 @@ export const BookingListItem: React.FC = ({ ); return ( - + onPress(booking)} - style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }} - className="active:bg-cal-bg-secondary" - android_ripple={{ color: "rgba(0, 0, 0, 0.1)" }} + onPressIn={() => setIsPressed(true)} + onPressOut={() => setIsPressed(false)} + style={{ + paddingHorizontal: 16, + paddingTop: 16, + paddingBottom: 12, + backgroundColor: isPressed ? (isDark ? "#171717" : "#F3F4F6") : "transparent", + }} + android_ripple={{ color: isDark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)" }} > @@ -182,10 +206,10 @@ export const BookingListItem: React.FC = ({ - + @@ -199,10 +223,23 @@ export const BookingListItem: React.FC = ({ - + {action.label} diff --git a/companion/components/booking-list-item/BookingListItemParts.tsx b/companion/components/booking-list-item/BookingListItemParts.tsx index e23bff3413..971c0bea4d 100644 --- a/companion/components/booking-list-item/BookingListItemParts.tsx +++ b/companion/components/booking-list-item/BookingListItemParts.tsx @@ -1,9 +1,10 @@ import { Ionicons } from "@expo/vector-icons"; -import { Linking, Text, TouchableOpacity, View } from "react-native"; +import { Linking, Text, TouchableOpacity, useColorScheme, View } from "react-native"; import { SvgImage } from "@/components/SvgImage"; import type { Booking } from "@/services/calcom"; import { showErrorAlert } from "@/utils/alerts"; import type { BookingListItemData } from "./useBookingListItemData"; +import { getColors } from "@/constants/colors"; interface TimeAndDateRowProps { formattedDate: string; @@ -13,8 +14,10 @@ interface TimeAndDateRowProps { export function TimeAndDateRow({ formattedDate, formattedTimeRange }: TimeAndDateRowProps) { return ( - {formattedDate} - {formattedTimeRange} + {formattedDate} + + {formattedTimeRange} + ); } @@ -44,7 +47,7 @@ interface BookingTitleProps { export function BookingTitle({ title, isCancelled, isRejected }: BookingTitleProps) { return ( {title} @@ -59,7 +62,10 @@ interface BookingDescriptionProps { export function BookingDescription({ description }: BookingDescriptionProps) { if (!description) return null; return ( - + "{description}" ); @@ -74,13 +80,16 @@ export function HostAndAttendees({ hostAndAttendeesDisplay, hasNoShowAttendee, }: HostAndAttendeesProps) { + const colorScheme = useColorScheme(); + const theme = getColors(colorScheme === "dark"); + if (!hostAndAttendeesDisplay) return null; return ( - {hostAndAttendeesDisplay} + {hostAndAttendeesDisplay} {hasNoShowAttendee && ( - - + + No-show @@ -139,27 +148,19 @@ export function ConfirmRejectButtons({ onConfirm, onReject, }: ConfirmRejectButtonsProps) { - if (!isPending) return null; + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const iconColor = isDark ? "#FFFFFF" : "#3C3F44"; + + // Check both endTime and end (optional) properties + const endDateStr = booking.endTime || booking.end; + const isPast = endDateStr ? new Date(endDateStr) < new Date() : false; + + if (!isPending || isPast) return null; return ( <> { - e.stopPropagation(); - onConfirm(booking); - }} - > - - Confirm - - - - Reject + + + Reject + + + { + e.stopPropagation(); + onConfirm(booking); + }} + > + + Confirm ); diff --git a/companion/components/booking-list-item/BookingListItemSkeleton.tsx b/companion/components/booking-list-item/BookingListItemSkeleton.tsx index a94e16c80d..32007629ff 100644 --- a/companion/components/booking-list-item/BookingListItemSkeleton.tsx +++ b/companion/components/booking-list-item/BookingListItemSkeleton.tsx @@ -1,13 +1,24 @@ -import { ScrollView, View } from "react-native"; +import { ScrollView, View, useColorScheme } from "react-native"; import { Skeleton } from "@/components/ui/skeleton"; +import { getColors } from "@/constants/colors"; interface BookingListItemSkeletonProps { isLast?: boolean; } export function BookingListItemSkeleton({ isLast = false }: BookingListItemSkeletonProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + return ( - + - + = }); }, [booking, userEmail]); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + // Define context menu actions based on booking state type ContextMenuAction = { label: string; @@ -160,16 +165,24 @@ export const RecurringBookingListItem: React.FC = .map(({ label, icon, onPress, role }) => ({ label, icon, onPress, role })); return ( - + onPress(group)} style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }} - className="active:bg-cal-bg-secondary" + className="active:bg-cal-bg-secondary dark:active:bg-[#171717]" > {/* Date and Time */} - {formattedDate} - {formattedTimeRange} + {formattedDate} + + {formattedTimeRange} + {/* Badges Row */} @@ -183,7 +196,14 @@ export const RecurringBookingListItem: React.FC = {/* Unconfirmed Badge */} {group.hasUnconfirmed && ( - + Unconfirmed )} @@ -191,12 +211,14 @@ export const RecurringBookingListItem: React.FC = {/* Recurrence Pattern Text (for unconfirmed recurring) */} {group.hasUnconfirmed && group.recurrenceText && ( - {group.recurrenceText} + + {group.recurrenceText} + )} {/* Title */} {booking.title} @@ -204,7 +226,10 @@ export const RecurringBookingListItem: React.FC = {/* Description */} {booking.description ? ( - + "{booking.description}" ) : null} @@ -212,7 +237,7 @@ export const RecurringBookingListItem: React.FC = {/* Host and Attendees */} {hostAndAttendeesDisplay ? ( - {hostAndAttendeesDisplay} + {hostAndAttendeesDisplay} ) : null} @@ -256,7 +281,7 @@ export const RecurringBookingListItem: React.FC = {/* Confirm All / Reject All for unconfirmed recurring */} {group.hasUnconfirmed && onRejectAll && ( = onRejectAll(group); }} > - - Reject all + + + Reject all + )} {group.hasUnconfirmed && onConfirmAll && ( = onConfirmAll(group); }} > - - Confirm all + + Confirm all )} {/* Cancel All Remaining */} {onCancelAllRemaining && group.remainingCount > 0 && !group.hasUnconfirmed && ( { @@ -308,8 +335,8 @@ export const RecurringBookingListItem: React.FC = onCancelAllRemaining(group); }} > - - + + Cancel all remaining diff --git a/companion/components/booking-list-item/RecurringBookingListItem.tsx b/companion/components/booking-list-item/RecurringBookingListItem.tsx index 904277e87b..16b583141f 100644 --- a/companion/components/booking-list-item/RecurringBookingListItem.tsx +++ b/companion/components/booking-list-item/RecurringBookingListItem.tsx @@ -1,6 +1,6 @@ import { Ionicons } from "@expo/vector-icons"; import React from "react"; -import { Pressable, Text, TouchableOpacity, View } from "react-native"; +import { Pressable, Text, TouchableOpacity, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { DropdownMenu, @@ -10,6 +10,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Text as UIText } from "@/components/ui/text"; +import { getColors } from "@/constants/colors"; import type { Booking } from "@/services/calcom"; import type { RecurringBookingGroup } from "@/utils/bookings-utils"; import { formatDate, formatTime, getHostAndAttendeesDisplay } from "@/utils/bookings-utils"; @@ -58,6 +59,16 @@ export const RecurringBookingListItem: React.FC = onReportBooking, onCancelBooking, }) => { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + + const colors = { + icon: isDark ? "#FFFFFF" : "#3C3F44", + iconDestructive: theme.destructive, + iconDefault: isDark ? "#E5E5EA" : "#374151", + }; + const booking = group.firstUpcoming; const startTime = booking.start || booking.startTime || ""; const endTime = booking.end || booking.endTime || ""; @@ -169,17 +180,25 @@ export const RecurringBookingListItem: React.FC = ); return ( - + onPress(group)} style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }} - className="active:bg-cal-bg-secondary" - android_ripple={{ color: "rgba(0, 0, 0, 0.1)" }} + className="active:bg-cal-bg-secondary dark:active:bg-[#171717]" + android_ripple={{ color: isDark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)" }} > {/* Date and Time */} - {formattedDate} - {formattedTimeRange} + {formattedDate} + + {formattedTimeRange} + {/* Badges Row */} @@ -201,12 +220,14 @@ export const RecurringBookingListItem: React.FC = {/* Recurrence Pattern Text (for unconfirmed recurring) */} {group.hasUnconfirmed && group.recurrenceText && ( - {group.recurrenceText} + + {group.recurrenceText} + )} {/* Title */} {booking.title} @@ -214,7 +235,10 @@ export const RecurringBookingListItem: React.FC = {/* Description */} {booking.description ? ( - + "{booking.description}" ) : null} @@ -222,7 +246,7 @@ export const RecurringBookingListItem: React.FC = {/* Host and Attendees */} {hostAndAttendeesDisplay ? ( - {hostAndAttendeesDisplay} + {hostAndAttendeesDisplay} ) : null} @@ -266,7 +290,7 @@ export const RecurringBookingListItem: React.FC = {/* Confirm All / Reject All for unconfirmed recurring */} {group.hasUnconfirmed && onRejectAll && ( = onRejectAll(group); }} > - - Reject all + + + Reject all + )} {group.hasUnconfirmed && onConfirmAll && ( = onConfirmAll(group); }} > - - Confirm all + + Confirm all )} {/* Cancel All Remaining */} {onCancelAllRemaining && group.remainingCount > 0 && !group.hasUnconfirmed && ( { @@ -318,8 +344,8 @@ export const RecurringBookingListItem: React.FC = onCancelAllRemaining(group); }} > - - + + Cancel all remaining @@ -330,10 +356,10 @@ export const RecurringBookingListItem: React.FC = - + @@ -347,10 +373,23 @@ export const RecurringBookingListItem: React.FC = - + {action.label} diff --git a/companion/components/booking-list-screen/BookingListScreen.tsx b/companion/components/booking-list-screen/BookingListScreen.tsx index 089c9a6d0e..8a6e690c8e 100644 --- a/companion/components/booking-list-screen/BookingListScreen.tsx +++ b/companion/components/booking-list-screen/BookingListScreen.tsx @@ -10,6 +10,7 @@ import { Text, TextInput, TouchableOpacity, + useColorScheme, View, } from "react-native"; import { FullScreenModal } from "@/components/FullScreenModal"; @@ -50,6 +51,7 @@ import { searchBookings, } from "@/utils/bookings-utils"; import { offlineAwareRefresh } from "@/utils/network"; +import { getColors } from "@/constants/colors"; interface BookingListScreenProps { // Platform-specific header rendering @@ -99,6 +101,9 @@ export const BookingListScreen: React.FC = ({ const router = useRouter(); const { userInfo } = useAuth(); const [isManualRefreshing, setIsManualRefreshing] = useState(false); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); // Use React Query hook for fetching bookings const { @@ -598,8 +603,8 @@ export const BookingListScreen: React.FC = ({ const renderListItem = ({ item }: { item: ListItem }) => { if (item.type === "monthHeader") { return ( - - {item.monthYear} + + {item.monthYear} ); } @@ -633,7 +638,7 @@ export const BookingListScreen: React.FC = ({ if (loading) { return ( - + {renderHeader?.()} {renderFilterControls?.()} @@ -643,15 +648,17 @@ export const BookingListScreen: React.FC = ({ if (error) { return ( - + {renderHeader?.()} {renderFilterControls?.()} - - - + + + Unable to load bookings - {error} + + {error} + ); @@ -671,7 +678,7 @@ export const BookingListScreen: React.FC = ({ {/* Empty state - no bookings */} - + = ({ } contentInsetAdjustmentBehavior="automatic" - style={{ backgroundColor: "white" }} + style={{ backgroundColor: isDark ? "#000000" : "white" }} > = ({ {/* Search empty state */} - + = ({ } contentInsetAdjustmentBehavior="automatic" - style={{ backgroundColor: "white" }} + style={{ backgroundColor: isDark ? "#000000" : "white" }} > = ({ refreshControl={} showsVerticalScrollIndicator={false} contentInsetAdjustmentBehavior="automatic" - style={{ backgroundColor: "white" }} + style={{ backgroundColor: isDark ? "#000000" : "white" }} /> - + item.key} @@ -790,31 +797,33 @@ export const BookingListScreen: React.FC = ({ onRequestClose={handleCloseCancelModal} > - + {/* Danger icon */} - - + + {/* Title and description */} - Cancel Event - + + Cancel Event + + Are you sure you want to cancel "{cancelBooking?.title}"? Cancellation reason will be shared with guests. {/* Reason Input */} - + Reason for cancellation = ({ - Cancel Event + + Cancel Event + - Nevermind + + Nevermind + @@ -855,7 +868,9 @@ export const BookingListScreen: React.FC = ({ - Cancel event + + Cancel event + @@ -866,11 +881,13 @@ export const BookingListScreen: React.FC = ({ {/* Reason Input */} - Reason for cancellation + + Reason for cancellation + = ({ - + Cancellation Reason (required) = ({ - + Rejection Reason (optional) {title} @@ -36,11 +47,22 @@ export function SettingsGroup({ headerRight?: React.ReactNode; footer?: string; }) { + const colorScheme = useColorScheme(); + const theme = getColors(colorScheme === "dark"); return ( {header ? : null} - {children} - {footer ? {footer} : null} + + {children} + + {footer ? ( + + {footer} + + ) : null} ); } @@ -63,6 +85,9 @@ export function SettingRow({ isFirst?: boolean; isLast?: boolean; }) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); const height = isFirst || isLast ? 52 : 44; const showDescription = () => { if (!description) return; @@ -84,10 +109,16 @@ export function SettingRow({ }; return ( - + - + {title} {description ? ( @@ -107,8 +138,11 @@ export function SettingRow({ @@ -135,22 +169,26 @@ export function NavigationRow({ onSelect?: (value: string) => void; }) { const height = isFirst || isLast ? 52 : 44; + const colorScheme = useColorScheme(); + const theme = getColors(colorScheme === "dark"); return ( - + - + {title} {Platform.OS === "ios" && options && onSelect ? ( <> {value ? ( - + {value} ) : null} @@ -163,7 +201,7 @@ export function NavigationRow({ activeOpacity={0.5} > {value ? ( - + {value} ) : null} diff --git a/companion/components/event-type-detail/tabs/AdvancedTab.tsx b/companion/components/event-type-detail/tabs/AdvancedTab.tsx index 0c9fb87389..48d675f8cf 100644 --- a/companion/components/event-type-detail/tabs/AdvancedTab.tsx +++ b/companion/components/event-type-detail/tabs/AdvancedTab.tsx @@ -21,6 +21,8 @@ import { import { showInfoAlert, showNotAvailableAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { NavigationRow, SettingRow, SettingsGroup } from "../SettingsUI"; +import { getColors } from "@/constants/colors"; +import { useColorScheme } from "react-native"; // Interface language options matching API V2 enum const interfaceLanguageOptions = [ @@ -122,6 +124,8 @@ interface AdvancedTabProps { export function AdvancedTab(props: AdvancedTabProps) { const [showLanguagePicker, setShowLanguagePicker] = useState(false); + const colorScheme = useColorScheme(); + const theme = getColors(colorScheme === "dark"); const getLanguageLabel = (value: string) => { const option = interfaceLanguageOptions.find((opt) => opt.value === value); @@ -277,15 +281,21 @@ export function AdvancedTab(props: AdvancedTabProps) { {/* Seats Configuration - shown when enabled */} {props.seatsEnabled ? ( - - - Seats per booking + + + + Seats per booking + @@ -336,12 +346,7 @@ export function AdvancedTab(props: AdvancedTabProps) { presentationStyle={Platform.OS === "ios" ? "formSheet" : undefined} onRequestClose={() => setShowLanguagePicker(false)} > - + {Platform.OS !== "ios" ? ( ) : null} {/* Header */} - + setShowLanguagePicker(false)} - className="h-8 w-8 items-center justify-center rounded-full bg-[#E5E5EA]" + className="h-8 w-8 items-center justify-center rounded-full" + style={{ backgroundColor: theme.backgroundEmphasis }} > - + - Select Language + + Select Language + @@ -378,23 +387,32 @@ export function AdvancedTab(props: AdvancedTabProps) { className="flex-1" contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 40, paddingTop: 16 }} > - + {interfaceLanguageOptions.map((option, index) => ( - + { props.setInterfaceLanguage(option.value); setShowLanguagePicker(false); }} > - {option.label} + + {option.label} + {props.interfaceLanguage === option.value ? ( - + ) : null} @@ -417,19 +435,25 @@ export function AdvancedTab(props: AdvancedTabProps) { /> {props.redirectEnabled ? ( <> - - - Redirect URL + + + + Redirect URL + - + Adding a redirect will disable the success page. @@ -471,49 +495,61 @@ export function AdvancedTab(props: AdvancedTabProps) { /> + {/* Event type colors */} {/* Event type colors */} - - - Light Theme + + + + Light Theme + - + - Dark Theme + + Dark Theme + diff --git a/companion/components/event-type-detail/tabs/AvailabilityTab.tsx b/companion/components/event-type-detail/tabs/AvailabilityTab.tsx index 8d828b0539..67eaf6b83e 100644 --- a/companion/components/event-type-detail/tabs/AvailabilityTab.tsx +++ b/companion/components/event-type-detail/tabs/AvailabilityTab.tsx @@ -52,7 +52,7 @@ const formatTime12Hour = (time24: string): string => { function SectionHeader({ title }: { title: string }) { return ( {title} @@ -65,7 +65,7 @@ function SettingsGroup({ children, header }: { children: React.ReactNode; header return ( {header ? : null} - {children} + {children} ); } @@ -89,21 +89,21 @@ function NavigationRow({ onSelect?: (value: string) => void; }) { return ( - + - + {title} {Platform.OS === "ios" && options && onSelect ? ( <> {value ? ( - + {value} ) : null} @@ -121,7 +121,7 @@ function NavigationRow({ disabled={disabled} > {value ? ( - + {value} ) : null} @@ -164,13 +164,13 @@ export function AvailabilityTab(props: AvailabilityTabProps) { {props.selectedSchedule ? ( {/* Header row */} - + - Weekly Schedule - + Weekly Schedule + {enabledDaysCount} {enabledDaysCount === 1 ? "day" : "days"} @@ -178,11 +178,11 @@ export function AvailabilityTab(props: AvailabilityTabProps) { {/* Day rows */} {props.scheduleDetailsLoading ? ( - - Loading schedule details... + + Loading schedule details... ) : props.selectedScheduleDetails ? ( - + {DAYS.map((day, index) => { const dayInfo = daySchedules.find((d) => d.day === day); const isEnabled = dayInfo?.available ?? false; @@ -192,19 +192,19 @@ export function AvailabilityTab(props: AvailabilityTabProps) { return ( {/* Availability indicator */} {/* Day name */} {day} @@ -216,7 +216,7 @@ export function AvailabilityTab(props: AvailabilityTabProps) { {timeSlots.map((slot, slotIndex) => ( 0 ? "mt-1" : ""}`} + className={`text-[15px] text-black dark:text-white ${slotIndex > 0 ? "mt-1" : ""}`} > {slot.startTime && slot.endTime ? `${formatTime12Hour(slot.startTime)} - ${formatTime12Hour(slot.endTime)}` @@ -225,7 +225,7 @@ export function AvailabilityTab(props: AvailabilityTabProps) { ))} ) : ( - + Unavailable )} @@ -234,8 +234,8 @@ export function AvailabilityTab(props: AvailabilityTabProps) { })} ) : ( - - + + Failed to load schedule details @@ -246,8 +246,8 @@ export function AvailabilityTab(props: AvailabilityTabProps) { {/* Timezone */} {props.selectedSchedule ? ( - - + + {props.selectedTimezone || props.selectedScheduleDetails?.timeZone || "No timezone"} diff --git a/companion/components/event-type-detail/tabs/BasicsTab.tsx b/companion/components/event-type-detail/tabs/BasicsTab.tsx index 8c904edf6f..8de3eb85ca 100644 --- a/companion/components/event-type-detail/tabs/BasicsTab.tsx +++ b/companion/components/event-type-detail/tabs/BasicsTab.tsx @@ -12,6 +12,8 @@ import type { LocationItem, LocationOptionGroup } from "@/types/locations"; import { createLocationItemFromOption } from "@/utils/locationHelpers"; import { slugify } from "@/utils/slugify"; import { NavigationRow, SettingRow, SettingsGroup } from "../SettingsUI"; +import { getColors } from "@/constants/colors"; +import { useColorScheme } from "react-native"; interface BasicsTabProps { eventTitle: string; @@ -63,21 +65,31 @@ function InputRow({ isFirst?: boolean; isLast?: boolean; }) { + const colorScheme = useColorScheme(); + const theme = getColors(colorScheme === "dark"); return ( - + - {label} + + {label} + = (props) => { const [showLocationModal, setShowLocationModal] = useState(false); + const colorScheme = useColorScheme(); + const theme = getColors(colorScheme === "dark"); const onSelectLocation = (value: string, label: string) => { const newLocation = createLocationItemFromOption(value, label); @@ -118,11 +132,19 @@ export const BasicsTab: React.FC = (props) => { numberOfLines={4} /> {/* URL Input */} - + - URL - - + + URL + + + {(() => { // Parse bookingUrl to get domain prefix (e.g., "i.cal.com/" or "cal.com/username/") if (props.bookingUrl) { @@ -145,11 +167,12 @@ export const BasicsTab: React.FC = (props) => { })()} props.setEventSlug(slugify(text, true))} placeholder="event-slug" - placeholderTextColor="#8E8E93" + placeholderTextColor="#A3A3A3" /> @@ -159,22 +182,30 @@ export const BasicsTab: React.FC = (props) => { {/* Duration */} {!props.allowMultipleDurations ? ( - + - Duration + + Duration + - minutes + + minutes + diff --git a/companion/components/event-type-detail/tabs/LimitsTab.tsx b/companion/components/event-type-detail/tabs/LimitsTab.tsx index b5c6e80f5f..f233aabbe5 100644 --- a/companion/components/event-type-detail/tabs/LimitsTab.tsx +++ b/companion/components/event-type-detail/tabs/LimitsTab.tsx @@ -11,6 +11,8 @@ import { type Animated, Platform, Text, TextInput, TouchableOpacity, View } from import { NavigationRow, SettingRow, SettingsGroup } from "../SettingsUI"; import { LimitsTabDatePicker } from "./LimitsTabDatePicker"; import { LimitsTabIOSPicker } from "./LimitsTabIOSPicker"; +import { getColors } from "@/constants/colors"; +import { useColorScheme } from "react-native"; interface FrequencyLimit { id: number; @@ -65,15 +67,21 @@ const DURATION_UNIT_OPTIONS = ["Per day", "Per week", "Per month"]; // Local components removed in favor of SettingsUI +// Android/Web picker button (opens modal) // Android/Web picker button (opens modal) function ModalPickerButton({ value, onPress }: { value: string; onPress: () => void }) { + const colorScheme = useColorScheme(); + const theme = getColors(colorScheme === "dark"); return ( - {value} - + + {value} + + ); } @@ -134,6 +142,9 @@ interface LimitsTabProps { } export function LimitsTab(props: LimitsTabProps) { + const colorScheme = useColorScheme(); + const theme = getColors(colorScheme === "dark"); + return ( {/* Buffer & Notice Settings */} @@ -152,27 +163,32 @@ export function LimitsTab(props: LimitsTabProps) { onSelect={props.setAfterEventBuffer} onPress={() => props.setShowAfterBufferDropdown(true)} /> - + - Minimum Notice + + Minimum Notice + { const numericValue = text.replace(/[^0-9]/g, ""); props.setMinimumNoticeValue(numericValue); }} placeholder="1" - placeholderTextColor="#8E8E93" + placeholderTextColor={theme.textMuted} keyboardType="numeric" /> {Platform.OS === "ios" ? ( - {props.minimumNoticeUnit} + + {props.minimumNoticeUnit} + {props.frequencyLimits.map((limit, index) => ( - + { const numericValue = text.replace(/[^0-9]/g, ""); props.updateFrequencyLimit(limit.id, "value", numericValue); }} placeholder="1" - placeholderTextColor="#8E8E93" + placeholderTextColor={theme.textMuted} keyboardType="numeric" /> - per + + per + {Platform.OS === "ios" ? ( - {limit.unit} + + {limit.unit} + {props.frequencyLimits.length > 1 ? ( props.removeFrequencyLimit(limit.id)}> - + ) : null} ))} - - Add Limit + + + Add Limit + ) : null} @@ -296,28 +326,39 @@ export function LimitsTab(props: LimitsTabProps) { {props.limitTotalDuration ? ( {props.durationLimits.map((limit, index) => ( - + { const numericValue = text.replace(/[^0-9]/g, ""); props.updateDurationLimit(limit.id, "value", numericValue); }} placeholder="60" - placeholderTextColor="#8E8E93" + placeholderTextColor={theme.textMuted} keyboardType="numeric" /> - minutes per + + minutes per + {Platform.OS === "ios" ? ( - {limit.unit} + + {limit.unit} + {props.durationLimits.length > 1 ? ( props.removeDurationLimit(limit.id)}> - + ) : null} ))} - - Add Limit + + + Add Limit + ) : null} @@ -354,25 +398,30 @@ export function LimitsTab(props: LimitsTabProps) { {/* Max Active Bookings Configuration */} {props.maxActiveBookingsPerBooker ? ( - + - Max bookings + + Max bookings + { const numericValue = text.replace(/[^0-9]/g, ""); props.setMaxActiveBookingsValue(numericValue); }} placeholder="1" - placeholderTextColor="#8E8E93" + placeholderTextColor={theme.textMuted} keyboardType="numeric" /> - bookings + + bookings + @@ -392,22 +441,32 @@ export function LimitsTab(props: LimitsTabProps) { {/* Rolling option */} props.setFutureBookingType("rolling")} activeOpacity={0.7} > - + {props.futureBookingType === "rolling" ? ( - + ) : null} { const numericValue = text.replace(/[^0-9]/g, ""); @@ -415,21 +474,24 @@ export function LimitsTab(props: LimitsTabProps) { props.setFutureBookingType("rolling"); }} placeholder="30" - placeholderTextColor="#8E8E93" + placeholderTextColor={theme.textMuted} keyboardType="numeric" /> { props.setRollingCalendarDays(!props.rollingCalendarDays); props.setFutureBookingType("rolling"); }} > - + {props.rollingCalendarDays ? "calendar" : "business"} - days ahead + + days ahead + @@ -437,25 +499,35 @@ export function LimitsTab(props: LimitsTabProps) { {/* Date Range option */} props.setFutureBookingType("range")} activeOpacity={0.7} > {props.futureBookingType === "range" ? ( - + ) : null} - Within a date range + + Within a date range + {props.futureBookingType === "range" ? ( - Start date + + Start date + - End date + + End date + - + + {displayText} diff --git a/companion/components/event-type-detail/tabs/LimitsTabDatePicker.tsx b/companion/components/event-type-detail/tabs/LimitsTabDatePicker.tsx index d798b7b75a..a256a7365c 100644 --- a/companion/components/event-type-detail/tabs/LimitsTabDatePicker.tsx +++ b/companion/components/event-type-detail/tabs/LimitsTabDatePicker.tsx @@ -1,4 +1,5 @@ -import { TextInput, View } from "react-native"; +import { TextInput, View, useColorScheme } from "react-native"; +import { getColors } from "@/constants/colors"; interface LimitsTabDatePickerProps { value: string; @@ -7,14 +8,19 @@ interface LimitsTabDatePickerProps { } export function LimitsTabDatePicker({ value, onChange, placeholder }: LimitsTabDatePickerProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + return ( ); diff --git a/companion/components/event-type-detail/tabs/RecurringTab.tsx b/companion/components/event-type-detail/tabs/RecurringTab.tsx index ce0be95264..1344f6b0ef 100644 --- a/companion/components/event-type-detail/tabs/RecurringTab.tsx +++ b/companion/components/event-type-detail/tabs/RecurringTab.tsx @@ -4,10 +4,11 @@ * iOS Settings style with grouped rows and section headers. */ -import { Platform, Text, TextInput, TouchableOpacity, View } from "react-native"; +import { Platform, Text, TextInput, TouchableOpacity, View, useColorScheme } from "react-native"; import { SettingRow, SettingsGroup } from "../SettingsUI"; import { RecurringTabIOSPicker } from "./RecurringTabIOSPicker"; import { Ionicons } from "@expo/vector-icons"; +import { getColors } from "@/constants/colors"; interface RecurringTabProps { recurringEnabled: boolean; @@ -41,6 +42,9 @@ export function RecurringTab({ setRecurringOccurrences, setShowFrequencyDropdown, }: RecurringTabProps) { + const colorScheme = useColorScheme(); + const theme = getColors(colorScheme === "dark"); + return ( {/* Recurring Toggle */} @@ -59,15 +63,22 @@ export function RecurringTab({ {recurringEnabled ? ( {/* Repeats Every */} - + - Repeats every + + Repeats every + { const numericValue = text.replace(/[^0-9]/g, ""); @@ -81,12 +92,12 @@ export function RecurringTab({ } }} placeholder="1" - placeholderTextColor="#8E8E93" + placeholderTextColor={theme.textMuted} keyboardType="numeric" /> {Platform.OS === "ios" ? ( - + {frequencyToLabel[recurringFrequency] || recurringFrequency} ) : ( setShowFrequencyDropdown(true)} > - + {frequencyToLabel[recurringFrequency] || recurringFrequency} @@ -122,12 +134,15 @@ export function RecurringTab({ {/* Maximum occurrences */} - + - Maximum events + + Maximum events + { const numericValue = text.replace(/[^0-9]/g, ""); @@ -141,10 +156,12 @@ export function RecurringTab({ } }} placeholder="12" - placeholderTextColor="#8E8E93" + placeholderTextColor={theme.textMuted} keyboardType="numeric" /> - events + + events + diff --git a/companion/components/event-type-list-item/EventTypeListItem.ios.tsx b/companion/components/event-type-list-item/EventTypeListItem.ios.tsx index a1cf5a7c62..ff77dc58be 100644 --- a/companion/components/event-type-list-item/EventTypeListItem.ios.tsx +++ b/companion/components/event-type-list-item/EventTypeListItem.ios.tsx @@ -2,7 +2,7 @@ import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui"; import { buttonStyle, frame } from "@expo/ui/swift-ui/modifiers"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import type React from "react"; -import { Pressable, View } from "react-native"; +import { Pressable, useColorScheme, View } from "react-native"; import { EventTypeBadges, EventTypeDescription, EventTypeTitle } from "./EventTypeListItemParts"; import type { EventTypeListItemProps } from "./types"; import { useEventTypeListItemData } from "./useEventTypeListItemData"; @@ -20,6 +20,8 @@ export const EventTypeListItem = ({ const { formattedDuration, normalizedDescription, hasPrice, formattedPrice } = useEventTypeListItemData(item); const isLast = index === filteredEventTypes.length - 1; + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; // Calculate badge count to force remount when badges change // This fixes SwiftUI Host caching stale layout measurements @@ -88,7 +90,11 @@ export const EventTypeListItem = ({ // This fixes SwiftUI Host caching stale layout measurements return ( {/* Calculate minHeight based on badge rows to ensure proper spacing */} {/* 1-3 badges = 1 row, 4-5 badges = likely 2 rows, 6 badges = 2 rows */} diff --git a/companion/components/event-type-list-item/EventTypeListItem.tsx b/companion/components/event-type-list-item/EventTypeListItem.tsx index d4f3892196..7e4c86b4d7 100644 --- a/companion/components/event-type-list-item/EventTypeListItem.tsx +++ b/companion/components/event-type-list-item/EventTypeListItem.tsx @@ -1,5 +1,6 @@ import { Ionicons } from "@expo/vector-icons"; -import { Pressable, View } from "react-native"; +import { getColors } from "@/constants/colors"; +import { Pressable, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { DropdownMenu, @@ -28,6 +29,14 @@ export const EventTypeListItem = ({ useEventTypeListItemData(item); const isLast = index === filteredEventTypes.length - 1; const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + + const colors = { + icon: isDark ? "#FFFFFF" : "#3C3F44", + iconMenu: isDark ? "#E5E5EA" : "#374151", + }; const contentInsets = { top: insets.top, @@ -37,7 +46,13 @@ export const EventTypeListItem = ({ }; return ( - + handleEventTypePress(item)} style={{ flex: 1, marginRight: 12 }} - android_ripple={{ color: "rgba(0, 0, 0, 0.1)" }} + android_ripple={{ color: isDark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)" }} > - + handlePreview(item)}> - + Preview handleCopyLink(item)}> - + Copy link @@ -92,22 +117,32 @@ export const EventTypeListItem = ({ Edit onDuplicate?.(item)}> - + Duplicate onDelete?.(item)}> - - Delete + + Delete diff --git a/companion/components/event-type-list-item/EventTypeListItemParts.tsx b/companion/components/event-type-list-item/EventTypeListItemParts.tsx index 26698d76ed..96074ab774 100644 --- a/companion/components/event-type-list-item/EventTypeListItemParts.tsx +++ b/companion/components/event-type-list-item/EventTypeListItemParts.tsx @@ -1,5 +1,5 @@ import { Ionicons } from "@expo/vector-icons"; -import { Text, TouchableOpacity, View } from "react-native"; +import { Text, TouchableOpacity, useColorScheme, View } from "react-native"; import Svg, { Path } from "react-native-svg"; import { Tooltip } from "@/components/Tooltip"; import type { EventType } from "@/services/types/event-types.types"; @@ -32,8 +32,8 @@ export function EventTypeTitle({ title, username, slug, bookingUrl }: EventTypeT const linkText = getDisplayUrl(bookingUrl, username, slug); return ( - {title} - {linkText} + {title} + {linkText} ); } @@ -45,7 +45,10 @@ interface EventTypeDescriptionProps { export function EventTypeDescription({ normalizedDescription }: EventTypeDescriptionProps) { if (!normalizedDescription) return null; return ( - + {normalizedDescription} ); @@ -59,7 +62,11 @@ interface EventTypeLinkProps { export function EventTypeLink({ username, slug, bookingUrl }: EventTypeLinkProps) { const linkText = getDisplayUrl(bookingUrl, username, slug); - return {linkText}; + return ( + + {linkText} + + ); } interface EventTypeBadgesProps { @@ -91,6 +98,10 @@ export function EventTypeBadges({ confirmationPolicy, recurrence, }: EventTypeBadgesProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const badgeIconColor = isDark ? "#FFFFFF" : "#000000"; + const hasSeats = seats && !seats.disabled && seats.seatsPerTimeSlot && seats.seatsPerTimeSlot > 0; const requiresConfirmation = @@ -118,11 +129,11 @@ export function EventTypeBadges({ {/* Duration Badge */} - - + + {formattedDuration} @@ -130,22 +141,24 @@ export function EventTypeBadges({ {/* Hidden Badge */} {hidden ? ( - - Hidden + + + Hidden + ) : null} {/* Seats Badge */} {hasSeats ? ( - - + + {seats.seatsPerTimeSlot} seats @@ -154,11 +167,11 @@ export function EventTypeBadges({ {/* Price Badge */} {hasPrice && formattedPrice ? ( - - + + {formattedPrice} @@ -167,11 +180,11 @@ export function EventTypeBadges({ {/* Repeats Badge */} {hasRecurrence ? ( - - + + {recurrence.occurrences} times @@ -180,11 +193,11 @@ export function EventTypeBadges({ {/* Requires Confirmation Badge */} {requiresConfirmation ? ( - - + + Requires confirmation @@ -208,20 +221,25 @@ export function EventTypeActions({ handlePreview, handleEventTypeLongPress, }: EventTypeActionsProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const iconColor = isDark ? "#FFFFFF" : "#3C3F44"; + const svgStrokeColor = isDark ? "#FFFFFF" : "#3C3F44"; + return ( handlePreview(item)} > - + handleCopyLink(item)} > @@ -233,7 +251,7 @@ export function EventTypeActions({ height="18" viewBox="0 0 24 24" fill="none" - stroke="#3C3F44" + stroke={svgStrokeColor} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" @@ -246,11 +264,11 @@ export function EventTypeActions({ handleEventTypeLongPress(item)} > - + diff --git a/companion/components/event-type-list-item/EventTypeListItemSkeleton.tsx b/companion/components/event-type-list-item/EventTypeListItemSkeleton.tsx index 4d794be0cc..c36469cda6 100644 --- a/companion/components/event-type-list-item/EventTypeListItemSkeleton.tsx +++ b/companion/components/event-type-list-item/EventTypeListItemSkeleton.tsx @@ -1,13 +1,24 @@ -import { View } from "react-native"; +import { View, useColorScheme } from "react-native"; import { Skeleton } from "@/components/ui/skeleton"; +import { getColors } from "@/constants/colors"; interface EventTypeListItemSkeletonProps { isLast?: boolean; } export function EventTypeListItemSkeleton({ isLast = false }: EventTypeListItemSkeletonProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + return ( - + @@ -67,8 +78,8 @@ export function EventTypeListItemSkeleton({ isLast = false }: EventTypeListItemS alignItems: "center", borderRadius: 6, borderWidth: 1, - borderColor: "#E5E5EA", - backgroundColor: "#F5F5F5", + borderColor: theme.border, + backgroundColor: theme.backgroundMuted, }} > @@ -84,8 +95,8 @@ export function EventTypeListItemSkeleton({ isLast = false }: EventTypeListItemS alignItems: "center", borderRadius: 6, borderWidth: 1, - borderColor: "#E5E5EA", - backgroundColor: "#F5F5F5", + borderColor: theme.border, + backgroundColor: theme.backgroundMuted, }} > @@ -106,9 +117,19 @@ interface EventTypeListSkeletonProps { } export function EventTypeListSkeleton({ count = 5 }: EventTypeListSkeletonProps) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + return ( - + {Array.from({ length: count }).map((_, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: Static skeleton items don't reorder diff --git a/companion/components/screens/AddGuestsScreen.tsx b/companion/components/screens/AddGuestsScreen.tsx index c41f45d274..09291e25b0 100644 --- a/companion/components/screens/AddGuestsScreen.tsx +++ b/companion/components/screens/AddGuestsScreen.tsx @@ -15,6 +15,7 @@ import { Text, TextInput, TouchableOpacity, + useColorScheme, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -22,6 +23,7 @@ import { useAddGuests } from "@/hooks/useBookings"; import type { Booking } from "@/services/calcom"; import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; import { safeLogError } from "@/utils/safeLogger"; +import { getColors } from "@/constants/colors"; export interface AddGuestsScreenProps { booking: Booking | null; @@ -47,8 +49,28 @@ export const AddGuestsScreen = forwardRef([]); @@ -160,20 +182,30 @@ export const AddGuestsScreen = forwardRef {/* Email input */} {transparentBackground && ( - Email * + Email * )} {!transparentBackground && ( - Email * + + Email * + )} Name (optional) + + Name (optional) + )} {!transparentBackground && ( - Name (optional) + + Name (optional) + )} 0 && ( - + Guests to add ({guests.length}) {guests.map((guest, index) => ( - + - {guest.email} + + {guest.email} + {guest.name && ( - {guest.name} + + {guest.name} + )} - + ))} diff --git a/companion/components/screens/AvailabilityDetailScreen.ios.tsx b/companion/components/screens/AvailabilityDetailScreen.ios.tsx index b38f2ad2b0..528b5b530e 100644 --- a/companion/components/screens/AvailabilityDetailScreen.ios.tsx +++ b/companion/components/screens/AvailabilityDetailScreen.ios.tsx @@ -9,7 +9,15 @@ import { Ionicons } from "@expo/vector-icons"; import { useRouter } from "expo-router"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo } from "react"; -import { ActivityIndicator, Alert, RefreshControl, ScrollView, Text, View } from "react-native"; +import { + ActivityIndicator, + Alert, + RefreshControl, + ScrollView, + Text, + useColorScheme, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppPressable } from "@/components/AppPressable"; import { useDeleteSchedule, useScheduleById, useSetScheduleAsDefault } from "@/hooks/useSchedules"; @@ -69,6 +77,20 @@ export const AvailabilityDetailScreen = forwardRef< >(function AvailabilityDetailScreen({ id, onActionsReady }, ref) { const router = useRouter(); const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + + const colors = { + background: isDark ? "#000000" : "#f2f2f7", + cardBackground: isDark ? "#171717" : "#FFFFFF", + text: isDark ? "#FFFFFF" : "#000000", + textSecondary: isDark ? "#A3A3A3" : "#A3A3A3", + border: isDark ? "#4D4D4D" : "#E5E5EA", + chevron: isDark ? "#636366" : "#C7C7CC", + enabledDot: "#34C759", + disabledDot: isDark ? "#4D4D4D" : "#E5E5EA", + destructive: isDark ? "#FF453A" : "#FF3B30", + }; // Use React Query hook for data fetching and cache synchronization const { data: schedule, isLoading, error, refetch, isRefetching } = useScheduleById(Number(id)); @@ -227,15 +249,20 @@ export const AvailabilityDetailScreen = forwardRef< if (isLoading) { return ( - - - Loading availability... + + + + Loading availability... + ); } return ( - + {scheduleName || "Untitled Schedule"} @@ -259,7 +286,7 @@ export const AvailabilityDetailScreen = forwardRef< )} @@ -267,20 +294,28 @@ export const AvailabilityDetailScreen = forwardRef< {/* Weekly Schedule Card - Navigable */} - + router.push(`/edit-availability-hours?id=${id}` as never)}> - Weekly Schedule + + Weekly Schedule + - + {enabledDaysCount} {enabledDaysCount === 1 ? "day" : "days"} - + {/* Expanded Day List */} - + {DAYS.map((day, dayIndex) => { const daySlots = availability[dayIndex] || []; const isEnabled = daySlots.length > 0; @@ -288,19 +323,18 @@ export const AvailabilityDetailScreen = forwardRef< return ( 0 ? "border-t border-[#E5E5EA]" : "" - }`} + className="flex-row items-center py-3" + style={dayIndex > 0 ? { borderTopWidth: 1, borderTopColor: colors.border } : {}} > {DAYS[dayIndex]} @@ -310,14 +344,18 @@ export const AvailabilityDetailScreen = forwardRef< {daySlots.map((slot, slotIndex) => ( 0 ? "mt-1" : ""}`} + className={`text-[15px] ${slotIndex > 0 ? "mt-1" : ""}`} + style={{ color: colors.text }} > {formatTime12Hour(slot.startTime)} – {formatTime12Hour(slot.endTime)} ))} ) : ( - + Unavailable )} @@ -329,13 +367,20 @@ export const AvailabilityDetailScreen = forwardRef< {/* Timezone Card - Navigable */} - + router.push(`/edit-availability-name?id=${id}` as never)}> - Timezone + + Timezone + - {timeZone} - + + {timeZone} + + @@ -343,34 +388,45 @@ export const AvailabilityDetailScreen = forwardRef< {/* Date Overrides Card - Navigable */} {overrides.length > 0 && ( - + router.push(`/edit-availability-override?id=${id}` as never)} > - Date Overrides + + Date Overrides + - + {overrides.length} {overrides.length === 1 ? "override" : "overrides"} - + {/* Expanded Overrides List */} - + {overrides.map((override, index) => ( 0 ? "border-t border-[#E5E5EA]" : ""}`} + className="py-2.5" + style={index > 0 ? { borderTopWidth: 1, borderTopColor: colors.border } : {}} > - + {formatDateForDisplay(override.date)} {override.startTime === "00:00" && override.endTime === "00:00" ? ( - Unavailable + + Unavailable + ) : ( - + {formatTime12Hour(`${override.startTime}:00`)} –{" "} {formatTime12Hour(`${override.endTime}:00`)} @@ -384,16 +440,23 @@ export const AvailabilityDetailScreen = forwardRef< {/* No Overrides Message - Still navigable to add overrides */} {overrides.length === 0 && ( - + router.push(`/edit-availability-override?id=${id}` as never)} > - Date Overrides - No date overrides set + + Date Overrides + + + No date overrides set + - + diff --git a/companion/components/screens/AvailabilityDetailScreen.tsx b/companion/components/screens/AvailabilityDetailScreen.tsx index 085247606d..b23e40d6bb 100644 --- a/companion/components/screens/AvailabilityDetailScreen.tsx +++ b/companion/components/screens/AvailabilityDetailScreen.tsx @@ -9,7 +9,15 @@ import { Ionicons } from "@expo/vector-icons"; import { useRouter } from "expo-router"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo } from "react"; -import { ActivityIndicator, Alert, RefreshControl, ScrollView, Text, View } from "react-native"; +import { + ActivityIndicator, + Alert, + RefreshControl, + ScrollView, + Text, + useColorScheme, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppPressable } from "@/components/AppPressable"; import { useDeleteSchedule, useScheduleById, useSetScheduleAsDefault } from "@/hooks/useSchedules"; @@ -69,6 +77,20 @@ export const AvailabilityDetailScreen = forwardRef< >(function AvailabilityDetailScreen({ id, onActionsReady }, ref) { const router = useRouter(); const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + + const colors = { + background: isDark ? "#000000" : "#f2f2f7", + cardBackground: isDark ? "#171717" : "#FFFFFF", + text: isDark ? "#FFFFFF" : "#000000", + textSecondary: isDark ? "#A3A3A3" : "#A3A3A3", + border: isDark ? "#4D4D4D" : "#E5E5EA", + chevron: isDark ? "#636366" : "#C7C7CC", + enabledDot: "#34C759", + disabledDot: isDark ? "#4D4D4D" : "#E5E5EA", + destructive: isDark ? "#FF453A" : "#FF3B30", + }; // Use React Query hook for data fetching and cache synchronization const { data: schedule, isLoading, error, refetch, isRefetching } = useScheduleById(Number(id)); @@ -227,15 +249,20 @@ export const AvailabilityDetailScreen = forwardRef< if (isLoading) { return ( - - - Loading availability... + + + + Loading availability... + ); } return ( - + {scheduleName || "Untitled Schedule"} @@ -259,7 +286,7 @@ export const AvailabilityDetailScreen = forwardRef< )} @@ -267,20 +294,28 @@ export const AvailabilityDetailScreen = forwardRef< {/* Weekly Schedule Card - Navigable */} - + router.push(`/edit-availability-hours?id=${id}` as never)}> - Weekly Schedule + + Weekly Schedule + - + {enabledDaysCount} {enabledDaysCount === 1 ? "day" : "days"} - + {/* Expanded Day List */} - + {DAYS.map((day, dayIndex) => { const daySlots = availability[dayIndex] || []; const isEnabled = daySlots.length > 0; @@ -288,19 +323,18 @@ export const AvailabilityDetailScreen = forwardRef< return ( 0 ? "border-t border-[#E5E5EA]" : "" - }`} + className="flex-row items-center py-3" + style={dayIndex > 0 ? { borderTopWidth: 1, borderTopColor: colors.border } : {}} > {DAYS[dayIndex]} @@ -310,14 +344,18 @@ export const AvailabilityDetailScreen = forwardRef< {daySlots.map((slot, slotIndex) => ( 0 ? "mt-1" : ""}`} + className={`text-[15px] ${slotIndex > 0 ? "mt-1" : ""}`} + style={{ color: colors.text }} > {formatTime12Hour(slot.startTime)} - {formatTime12Hour(slot.endTime)} ))} ) : ( - + Unavailable )} @@ -329,13 +367,20 @@ export const AvailabilityDetailScreen = forwardRef< {/* Timezone Card - Navigable */} - + router.push(`/edit-availability-name?id=${id}` as never)}> - Timezone + + Timezone + - {timeZone} - + + {timeZone} + + @@ -343,34 +388,45 @@ export const AvailabilityDetailScreen = forwardRef< {/* Date Overrides Card - Navigable */} {overrides.length > 0 && ( - + router.push(`/edit-availability-override?id=${id}` as never)} > - Date Overrides + + Date Overrides + - + {overrides.length} {overrides.length === 1 ? "override" : "overrides"} - + {/* Expanded Overrides List */} - + {overrides.map((override, index) => ( 0 ? "border-t border-[#E5E5EA]" : ""}`} + className="py-2.5" + style={index > 0 ? { borderTopWidth: 1, borderTopColor: colors.border } : {}} > - + {formatDateForDisplay(override.date)} {override.startTime === "00:00" && override.endTime === "00:00" ? ( - Unavailable + + Unavailable + ) : ( - + {formatTime12Hour(`${override.startTime}:00`)} -{" "} {formatTime12Hour(`${override.endTime}:00`)} @@ -384,16 +440,23 @@ export const AvailabilityDetailScreen = forwardRef< {/* No Overrides Message - Still navigable to add overrides */} {overrides.length === 0 && ( - + router.push(`/edit-availability-override?id=${id}` as never)} > - Date Overrides - No date overrides set + + Date Overrides + + + No date overrides set + - + diff --git a/companion/components/screens/AvailabilityListScreen.tsx b/companion/components/screens/AvailabilityListScreen.tsx index 2fd424379d..77e7aa0022 100644 --- a/companion/components/screens/AvailabilityListScreen.tsx +++ b/companion/components/screens/AvailabilityListScreen.tsx @@ -10,6 +10,7 @@ import { Text, TextInput, TouchableOpacity, + useColorScheme, View, } from "react-native"; import { AppPressable } from "@/components/AppPressable"; @@ -39,6 +40,7 @@ import { import { CalComAPIService, type Schedule } from "@/services/calcom"; import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; import { offlineAwareRefresh } from "@/utils/network"; +import { getColors } from "@/constants/colors"; export interface AvailabilityListScreenProps { searchQuery: string; @@ -61,6 +63,18 @@ export function AvailabilityListScreen({ const [showDeleteModal, setShowDeleteModal] = useState(false); const [isManualRefreshing, setIsManualRefreshing] = useState(false); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + + const colors = { + background: isDark ? "#000000" : "#FFFFFF", + backgroundSecondary: isDark ? "#171717" : "#f8f9fa", + border: isDark ? "#4D4D4D" : "#E5E5EA", + text: isDark ? "#FFFFFF" : "#333333", + textSecondary: isDark ? "#A3A3A3" : "#666666", + }; + // Use React Query hooks const { data: schedules = [], isLoading: loading, error: queryError, refetch } = useSchedules(); @@ -249,7 +263,7 @@ export function AvailabilityListScreen({ if (loading) { return ( - +
@@ -260,16 +274,21 @@ export function AvailabilityListScreen({ if (error) { return ( - +
- - + + Unable to load availability - {error} - refetch()}> - Retry + + {error} + + refetch()} + > + Retry @@ -287,11 +306,23 @@ export function AvailabilityListScreen({ {/* Non-iOS header with search */}
- + - - New + + New @@ -311,7 +342,7 @@ export function AvailabilityListScreen({ {/* Empty state - no schedules */} ) : ( e.stopPropagation()} > {/* Header */} - + Add a new schedule - Create a new availability schedule. + + Create a new availability schedule. + {/* Content */} - Name + + Name + { onShowCreateModalChange(false); setNewScheduleName(""); }} disabled={creating} > - Cancel + + Cancel + @@ -527,13 +570,13 @@ export function AvailabilityListScreen({ onPress={() => setShowActionsModal(false)} > e.stopPropagation()} > {/* Header */} - - + + Schedule Actions @@ -551,13 +594,15 @@ export function AvailabilityListScreen({ handleSetAsDefault(selectedSchedule); } }} - className="flex-row items-center p-2 hover:bg-gray-50 md:p-4" + className="flex-row items-center p-2 hover:bg-gray-50 dark:hover:bg-[#262626] md:p-4" > - Set as Default + + Set as Default + - + {/* Duplicate */} @@ -570,14 +615,14 @@ export function AvailabilityListScreen({ }, 100); } }} - className="flex-row items-center p-2 hover:bg-gray-50 md:p-4" + className="flex-row items-center p-2 hover:bg-gray-50 dark:hover:bg-[#262626] md:p-4" > - Duplicate + Duplicate {/* Separator */} - + {/* Delete */} - - Delete + + + Delete + {/* Cancel button */} - + setShowActionsModal(false)} > - Cancel + + Cancel + @@ -617,18 +666,20 @@ export function AvailabilityListScreen({ onRequestClose={() => !deleting && setShowDeleteModal(false)} > - + {/* Danger icon */} - - + + {/* Title and description */} - Delete Schedule - + + Delete Schedule + + Are you sure you want to delete "{selectedSchedule?.name}"? This action cannot be undone. @@ -649,11 +700,13 @@ export function AvailabilityListScreen({ setShowDeleteModal(false)} disabled={deleting} > - Cancel + + Cancel + diff --git a/companion/components/screens/BookingDetailScreen.ios.tsx b/companion/components/screens/BookingDetailScreen.ios.tsx index 1fa2bd9c43..11f64883eb 100644 --- a/companion/components/screens/BookingDetailScreen.ios.tsx +++ b/companion/components/screens/BookingDetailScreen.ios.tsx @@ -1,13 +1,45 @@ import { Ionicons } from "@expo/vector-icons"; import { useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Alert, RefreshControl, ScrollView, Text, View } from "react-native"; +import { + ActivityIndicator, + Alert, + RefreshControl, + ScrollView, + Text, + useColorScheme, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppPressable } from "@/components/AppPressable"; +import * as Clipboard from "expo-clipboard"; +import { showSuccessAlert } from "@/utils/alerts"; import { useCancelBooking } from "@/hooks/useBookings"; import type { Booking } from "@/services/calcom"; import { showErrorAlert } from "@/utils/alerts"; +const CopyButton = ({ + text, + color, + size = 16, +}: { + text: string; + color?: string; + size?: number; +}) => ( + { + if (!text) return; + await Clipboard.setStringAsync(text); + showSuccessAlert("Copied", "Copied to clipboard"); + }} + hitSlop={8} + className="ml-2 justify-center" + > + + +); + // Format date for iOS Calendar style: "Thursday, 25 Dec 2025" const formatDateCalendarStyle = (dateString: string): string => { if (!dateString) return ""; @@ -110,6 +142,19 @@ export function BookingDetailScreen({ }: BookingDetailScreenProps): React.JSX.Element { const router = useRouter(); const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + + const colors = { + background: isDark ? "#000000" : "#f2f2f7", + cardBackground: isDark ? "#171717" : "#FFFFFF", + text: isDark ? "#FFFFFF" : "#000000", + textSecondary: isDark ? "#A3A3A3" : "#A3A3A3", + textTertiary: isDark ? "#636366" : "#636366", + border: isDark ? "#4D4D4D" : "#E5E5EA", + destructive: isDark ? "#FF453A" : "#800020", + badge: isDark ? "#4D4D4D" : "#E5E5EA", + }; const [participantsExpanded, setParticipantsExpanded] = useState(true); @@ -253,9 +298,14 @@ export function BookingDetailScreen({ if (isLoading) { return ( - - - Loading booking... + + + + Loading booking... + ); } @@ -263,13 +313,25 @@ export function BookingDetailScreen({ if (error || !booking) { const errorMessage = error?.message || "Booking not found"; return ( - + - + {errorMessage} - router.back()}> - Go Back + router.back()} + > + + Go Back + ); @@ -310,7 +372,7 @@ export function BookingDetailScreen({ if (normalizedStatus === "pending") { return { name: "help-circle" as const, - color: "#8E8E93", + color: "#A3A3A3", label: "Pending", }; } @@ -318,7 +380,7 @@ export function BookingDetailScreen({ if (normalizedStatus === "cancelled" || normalizedStatus === "rejected") { return { name: "close-circle-outline" as const, - color: "#8E8E93", + color: "#A3A3A3", label: null, }; } @@ -339,10 +401,12 @@ export function BookingDetailScreen({ > {/* Title Section - iOS Calendar Style */} + {/* Meeting Title */} {/* Meeting Title */} {booking.title} @@ -350,13 +414,25 @@ export function BookingDetailScreen({ {/* Event Type Slug and Duration Badge */} {(eventTypeSlug || durationFormatted) && ( - {eventTypeSlug && {eventTypeSlug}} + {eventTypeSlug && ( + + {eventTypeSlug} + + )} {eventTypeSlug && durationFormatted && ( - • + + • + )} {durationFormatted && ( - - + + {durationFormatted} @@ -365,58 +441,85 @@ export function BookingDetailScreen({ )} {/* Date */} - {dateFormatted} + + {dateFormatted} + {/* Time */} - {timeFormatted} + + {timeFormatted} + {/* Recurring indicator */} - {isRecurring && Repeats weekly} + {isRecurring && ( + + Repeats weekly + + )} {/* Participants Card - iOS Calendar Style (Expandable) */} - + setParticipantsExpanded(!participantsExpanded)} > - Participants + + Participants + - {totalParticipants} + + {totalParticipants} + {/* Participants list - only show when expanded */} {participantsExpanded && ( - + {/* Hosts */} {booking.hosts && booking.hosts.length > 0 ? booking.hosts.map((host, index) => ( 0 ? "border-t border-[#E5E5EA]" : "" - }`} + className="flex-row items-center py-2" + style={index > 0 ? { borderTopWidth: 1, borderTopColor: colors.border } : {}} > - + {host.name || host.email || "Host"} + (Organizer) - Organizer + )) : booking.user && ( - + {booking.user.name || booking.user.email} + (Organizer) - Organizer + )} @@ -426,21 +529,25 @@ export function BookingDetailScreen({ return ( 0 || booking.hosts?.length || booking.user - ? "border-t border-[#E5E5EA]" - : "" - }`} + ? { borderTopWidth: 1, borderTopColor: colors.border } + : {} + } > - + {attendee.name || attendee.email} + {statusIcon.label ? ( + ({statusIcon.label}) + ) : null} - {statusIcon.label && ( - - {statusIcon.label} - - )} + ); })} @@ -449,17 +556,23 @@ export function BookingDetailScreen({ {booking.guests?.map((guestEmail, index) => ( 0 || booking.attendees?.length || booking.hosts?.length || booking.user - ? "border-t border-[#E5E5EA]" - : "" - }`} + ? { borderTopWidth: 1, borderTopColor: colors.border } + : {} + } > - - + + {guestEmail} + (Guest) - Guest + ))} @@ -503,9 +616,15 @@ export function BookingDetailScreen({ if (displayableEntries.length === 0) return null; return ( - + - + Booking Details {displayableEntries.map(([key, value], index) => { @@ -530,10 +649,18 @@ export function BookingDetailScreen({ return ( 0 ? "border-t border-[#E5E5EA]" : ""}`} + className="py-2" + style={index > 0 ? { borderTopWidth: 1, borderTopColor: colors.border } : {}} > - {displayKey} - {displayValue} + + {displayKey} + + + + {displayValue} + + + ); })} @@ -544,12 +671,23 @@ export function BookingDetailScreen({ {/* Description Card (if available) */} {booking.description && ( - + - + Notes - {booking.description} + + + {booking.description} + + + )} @@ -559,9 +697,15 @@ export function BookingDetailScreen({ booking.rescheduledToUid || booking.reschedulingReason || booking.fromReschedule) && ( - + - + Rescheduling Info @@ -578,7 +722,9 @@ export function BookingDetailScreen({ } }} > - Rescheduled from + + Rescheduled from + View original @@ -588,7 +734,8 @@ export function BookingDetailScreen({ {booking.rescheduledToUid && ( { const uid = booking.rescheduledToUid; if (uid) { @@ -599,7 +746,9 @@ export function BookingDetailScreen({ } }} > - Rescheduled to + + Rescheduled to + View new @@ -609,14 +758,22 @@ export function BookingDetailScreen({ {booking.reschedulingReason && ( - Reason - {booking.reschedulingReason} + + Reason + + + + {booking.reschedulingReason} + + + )} @@ -626,7 +783,10 @@ export function BookingDetailScreen({ {/* Cancellation Info Card (if cancelled) */} {normalizedStatus === "cancelled" && (booking.cancellationReason || booking.cancelledByEmail || booking.absentHost) && ( - + Cancellation Details @@ -634,32 +794,52 @@ export function BookingDetailScreen({ {booking.cancellationReason && ( - Reason - {booking.cancellationReason} + + Reason + + + + {booking.cancellationReason} + + + )} {booking.cancelledByEmail && ( - Cancelled by - {booking.cancelledByEmail} + + Cancelled by + + + + {booking.cancelledByEmail} + + + )} {booking.absentHost && ( - Host was absent + + Host was absent + )} @@ -670,9 +850,14 @@ export function BookingDetailScreen({ {/* Cancelling overlay */} {isCancelling && ( - - - Cancelling booking... + + + + Cancelling booking... + )} diff --git a/companion/components/screens/BookingDetailScreen.tsx b/companion/components/screens/BookingDetailScreen.tsx index ca61bfaead..6d0cce57e0 100644 --- a/companion/components/screens/BookingDetailScreen.tsx +++ b/companion/components/screens/BookingDetailScreen.tsx @@ -11,10 +11,12 @@ import { Text, TextInput, TouchableOpacity, + useColorScheme, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppPressable } from "@/components/AppPressable"; +import * as Clipboard from "expo-clipboard"; import { BookingActionsModal } from "@/components/BookingActionsModal"; import { FullScreenModal } from "@/components/FullScreenModal"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; @@ -42,6 +44,29 @@ import type { Booking } from "@/services/calcom"; import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts"; import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions"; import { openInAppBrowser } from "@/utils/browser"; +import { getColors } from "@/constants/colors"; + +const CopyButton = ({ + text, + color, + size = 16, +}: { + text: string; + color?: string; + size?: number; +}) => ( + { + if (!text) return; + await Clipboard.setStringAsync(text); + showSuccessAlert("Copied", "Copied to clipboard"); + }} + hitSlop={8} + className="ml-2 justify-center" + > + + +); // Empty actions result for when no booking is loaded const EMPTY_ACTIONS: BookingActionsResult = { @@ -174,6 +199,14 @@ export function BookingDetailScreen({ const router = useRouter(); const { userInfo } = useAuth(); const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + + // Background for the main screen (grouped list style: gray in light, black in dark) + const screenBackground = isDark ? theme.background : theme.backgroundMuted; + // Background for cards/items (white in light, dark gray in dark) + const cardBackground = isDark ? theme.backgroundSecondary : theme.background; const [showActionsModal, setShowActionsModal] = useState(false); const [showCancelDialog, setShowCancelDialog] = useState(false); @@ -456,9 +489,14 @@ export function BookingDetailScreen({ if (isLoading) { return ( - - - Loading booking... + + + + Loading booking... + ); } @@ -466,13 +504,25 @@ export function BookingDetailScreen({ if (error || !booking) { const errorMessage = error?.message || "Booking not found"; return ( - - - + + + {errorMessage} - router.back()}> - Go Back + router.back()} + > + + Go Back + ); @@ -514,7 +564,7 @@ export function BookingDetailScreen({ if (normalizedStatus === "pending") { return { name: "help-circle" as const, - color: "#8E8E93", + color: "#A3A3A3", label: "Pending", }; } @@ -522,7 +572,7 @@ export function BookingDetailScreen({ if (normalizedStatus === "cancelled" || normalizedStatus === "rejected") { return { name: "close-circle-outline" as const, - color: "#8E8E93", + color: "#A3A3A3", label: null, }; } @@ -539,18 +589,17 @@ export function BookingDetailScreen({ headerRight: () => ( - {meetingUrl && ( - - - - )} - - + + @@ -569,7 +618,13 @@ export function BookingDetailScreen({ + + {meetingUrl && ( + + + Join + + + )} ), }} /> )} - + {/* Title Section - iOS Calendar Style */} + {/* Meeting Title */} {booking.title} @@ -607,14 +679,28 @@ export function BookingDetailScreen({ {eventTypeSlug || durationFormatted ? ( {eventTypeSlug ? ( - {eventTypeSlug} + + {eventTypeSlug} + ) : null} {eventTypeSlug && durationFormatted ? ( - • + + • + ) : null} {durationFormatted ? ( - - + + {durationFormatted} @@ -622,55 +708,80 @@ export function BookingDetailScreen({ ) : null} - {dateFormatted} - {timeFormatted} + + {dateFormatted} + + + {timeFormatted} + {isRecurring ? ( - Repeats weekly + + Repeats weekly + ) : null} {/* Participants Card - iOS Calendar Style (Expandable) */} - + setParticipantsExpanded(!participantsExpanded)}> - Participants + + Participants + - {totalParticipants} + + {totalParticipants} + {participantsExpanded ? ( - + {/* Hosts */} {booking.hosts && booking.hosts.length > 0 ? ( booking.hosts.map((host, index) => ( 0 ? "border-t border-[#E5E5EA]" : "" - }`} + className="flex-row items-center py-2" + style={index > 0 ? { borderTopWidth: 1, borderTopColor: theme.border } : {}} > - + {host.name || host.email || "Host"} + (Organizer) - Organizer + )) ) : booking.user ? ( - + {booking.user.name || booking.user.email} + (Organizer) - Organizer + ) : null} @@ -680,60 +791,300 @@ export function BookingDetailScreen({ return ( 0 || booking.hosts?.length || booking.user - ? "border-t border-[#E5E5EA]" - : "" - }`} + ? { borderTopWidth: 1, borderTopColor: theme.border } + : {} + } > - + {attendee.name || attendee.email} + {statusIcon.label ? ( + ({statusIcon.label}) + ) : null} - {statusIcon.label ? ( - - {statusIcon.label} - - ) : null} + ); })} + {/* Guests */} {/* Guests */} {(booking as { guests?: string[] }).guests?.map((guestEmail, index) => ( 0 || booking.attendees?.length || booking.hosts?.length || booking.user - ? "border-t border-[#E5E5EA]" - : "" - }`} + ? { borderTopWidth: 1, borderTopColor: theme.border } + : {} + } > - - + + {guestEmail} + (Guest) - Guest + ))} ) : null} + {/* Custom Fields Card (if available) */} + {(() => { + if (!booking.bookingFieldsResponses) return null; + + const excludedKeys = [ + "location", + "guests", + "rescheduledReason", + "rescheduleReason", + "email", + "name", + "notes", + "description", + "additionalNotes", + ]; + + const displayableEntries = Object.entries(booking.bookingFieldsResponses).filter( + ([key, value]) => { + if (excludedKeys.includes(key)) return false; + + if (value === null || value === undefined || value === "") return false; + + if (Array.isArray(value) && value.length === 0) return false; + + if (typeof value === "object" && !Array.isArray(value) && value !== null) { + const obj = value as Record; + if ("value" in obj && "optionValue" in obj) return false; + if (Object.keys(obj).length === 0) return false; + } + + return true; + } + ); + + if (displayableEntries.length === 0) return null; + + return ( + + + + Booking Details + + {displayableEntries.map(([key, value], index) => { + let displayValue: string; + if (typeof value === "string") { + displayValue = value; + } else if (Array.isArray(value)) { + displayValue = value.join(", "); + } else if (typeof value === "boolean") { + displayValue = value ? "Yes" : "No"; + } else if (typeof value === "number") { + displayValue = String(value); + } else { + return null; + } + + const displayKey = key + .replace(/([A-Z])/g, " $1") + .replace(/^./, (str) => str.toUpperCase()) + .trim(); + + return ( + 0 ? { borderTopWidth: 1, borderTopColor: theme.border } : {}} + > + + {displayKey} + + + + {displayValue} + + + + + ); + })} + + + ); + })()} + {/* Notes Card (if available) */} {booking.description ? ( - + - + Notes - {booking.description} + + + {booking.description} + + + ) : null} + + {/* Rescheduling Info Card (if applicable) */} + {(booking.rescheduledFromUid || + booking.rescheduledToUid || + booking.reschedulingReason || + booking.fromReschedule) && ( + + + + Rescheduling Info + + + {(booking.rescheduledFromUid || booking.fromReschedule) && ( + + + Rescheduled from previous booking + + + )} + + {booking.rescheduledToUid && ( + + + Rescheduled to new booking + + + )} + + {booking.reschedulingReason && ( + + + Reason + + + + {booking.reschedulingReason} + + + + + )} + + + )} + + {/* Cancellation Info Card (if cancelled) */} + {normalizedStatus === "cancelled" && + (booking.cancellationReason || booking.cancelledByEmail || booking.absentHost) && ( + + + + Cancellation Details + + + {booking.cancellationReason && ( + + + Reason + + + + {booking.cancellationReason} + + + + + )} + + {booking.cancelledByEmail && ( + + + Cancelled by + + + + {booking.cancelledByEmail} + + + + + )} + + {booking.absentHost && ( + + + + Host was absent + + + )} + + + )} {/* Booking Actions Modal */} @@ -757,9 +1108,9 @@ export function BookingDetailScreen({ {/* Cancelling overlay */} {isCancelling ? ( - - - + + + Cancelling booking... @@ -779,8 +1130,8 @@ export function BookingDetailScreen({ {/* Danger icon */} - - + + {/* Title and description */} diff --git a/companion/components/screens/EditAvailabilityDayScreen.ios.tsx b/companion/components/screens/EditAvailabilityDayScreen.ios.tsx index d6d3591913..8f2d84ad86 100644 --- a/companion/components/screens/EditAvailabilityDayScreen.ios.tsx +++ b/companion/components/screens/EditAvailabilityDayScreen.ios.tsx @@ -1,13 +1,14 @@ import { DatePicker, Host } from "@expo/ui/swift-ui"; import { Ionicons } from "@expo/vector-icons"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react"; -import { Alert, ScrollView, Switch, Text, View } from "react-native"; +import { Alert, ScrollView, Switch, Text, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppPressable } from "@/components/AppPressable"; import { useUpdateSchedule } from "@/hooks/useSchedules"; import type { Schedule } from "@/services/calcom"; import type { ScheduleAvailability } from "@/services/types"; import { showErrorAlert } from "@/utils/alerts"; +import { getColors } from "@/constants/colors"; const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; @@ -167,7 +168,14 @@ export const EditAvailabilityDayScreen = forwardRef< ref ) { const insets = useSafeAreaInsets(); - const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]"; + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + const backgroundColor = transparentBackground + ? "transparent" + : isDark + ? theme.background + : theme.backgroundMuted; // Use mutation hook for cache-synchronized updates const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule(); @@ -304,15 +312,16 @@ export const EditAvailabilityDayScreen = forwardRef< if (!schedule) { return ( - - No schedule data + + No schedule data ); } return ( - Available on {dayName} + + Available on {dayName} + {/* Time Slots */} {isEnabled && ( <> - Time Slots + Time Slots {slots.map((slot, index) => ( - Slot {index + 1} + Slot {index + 1} {slots.length > 1 && ( handleRemoveSlot(index)} className="p-1"> @@ -357,7 +383,7 @@ export const EditAvailabilityDayScreen = forwardRef< - Start + Start - – + – - End + End @@ -406,11 +436,17 @@ export const EditAvailabilityDayScreen = forwardRef< {!isEnabled && ( - - + + You are unavailable on {dayName} diff --git a/companion/components/screens/EditAvailabilityDayScreen.tsx b/companion/components/screens/EditAvailabilityDayScreen.tsx index 167114160d..00f52d0820 100644 --- a/companion/components/screens/EditAvailabilityDayScreen.tsx +++ b/companion/components/screens/EditAvailabilityDayScreen.tsx @@ -1,6 +1,15 @@ import { Ionicons } from "@expo/vector-icons"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react"; -import { Alert, Platform, ScrollView, Switch, Text, TouchableOpacity, View } from "react-native"; +import { + Alert, + Platform, + ScrollView, + Switch, + Text, + TouchableOpacity, + useColorScheme, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppPressable } from "@/components/AppPressable"; import { FullScreenModal } from "@/components/FullScreenModal"; @@ -169,6 +178,18 @@ export const EditAvailabilityDayScreen = forwardRef< EditAvailabilityDayScreenProps >(function EditAvailabilityDayScreen({ schedule, dayIndex, onSuccess, onSavingChange }, ref) { const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + + const colors = { + background: isDark ? "#000000" : "#FFFFFF", + backgroundSecondary: isDark ? "#171717" : "#F9FAFB", + cardBackground: isDark ? "#171717" : "#FFFFFF", + text: isDark ? "#FFFFFF" : "#000000", + textSecondary: isDark ? "#A3A3A3" : "#6B7280", + border: isDark ? "#4D4D4D" : "#E5E5EA", + iconColor: isDark ? "#FFFFFF" : "#A3A3A3", + }; // Use mutation hook for cache-synchronized updates const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule(); @@ -185,12 +206,12 @@ export const EditAvailabilityDayScreen = forwardRef< // Render modal content const renderTimePickerContent = () => ( <> - - + + Select {showTimePicker?.type === "start" ? "Start" : "End"} Time setShowTimePicker(null)}> - + @@ -207,14 +228,16 @@ export const EditAvailabilityDayScreen = forwardRef< {formatTime12Hour(time)} @@ -346,47 +369,54 @@ export const EditAvailabilityDayScreen = forwardRef< if (!schedule) { return ( - - No schedule data + + No schedule data ); } return ( - {/* Enable/Disable Toggle */} - - Available on {dayName} + + + Available on {dayName} + - {/* Time Slots */} {isEnabled && ( <> - Time Slots + + Time Slots + {slots.map((slot, index) => ( - Slot {index + 1} + + Slot {index + 1} + {slots.length > 1 && ( handleRemoveSlot(index)} - className="rounded-full bg-red-50 p-2" + className="rounded-full bg-red-50 p-2 dark:bg-[#FF453A]/20" > @@ -395,8 +425,8 @@ export const EditAvailabilityDayScreen = forwardRef< {Platform.OS === "web" ? ( - - Start + + Start ) : ( setShowTimePicker({ slotIndex: index, type: "start" })} > - Start - + Start + {formatTime12Hour(slot.startTime)} )} - – + – {Platform.OS === "web" ? ( - - End + + End ) : ( setShowTimePicker({ slotIndex: index, type: "end" })} > - End - + End + {formatTime12Hour(slot.endTime)} @@ -469,9 +499,8 @@ export const EditAvailabilityDayScreen = forwardRef< ))} - {/* Add Slot Button */} - + Add Time Slot @@ -479,22 +508,20 @@ export const EditAvailabilityDayScreen = forwardRef< )} - {/* Unavailable Message */} {!isEnabled && ( - - - + + + - + Unavailable - + You are currently unavailable on {dayName}.{"\n"}Toggle the switch above to add hours. )} - {/* Time Picker Modal */} setShowTimePicker(null)} > e.stopPropagation()} style={shadows.xl()} @@ -516,7 +543,7 @@ export const EditAvailabilityDayScreen = forwardRef< ) : ( - {renderTimePickerContent()} + {renderTimePickerContent()} )} diff --git a/companion/components/screens/EditAvailabilityHoursScreen.ios.tsx b/companion/components/screens/EditAvailabilityHoursScreen.ios.tsx index 6f6dde4fc7..1510cb0015 100644 --- a/companion/components/screens/EditAvailabilityHoursScreen.ios.tsx +++ b/companion/components/screens/EditAvailabilityHoursScreen.ios.tsx @@ -1,10 +1,11 @@ import { Ionicons } from "@expo/vector-icons"; import { forwardRef } from "react"; -import { ScrollView, Text, View } from "react-native"; +import { ScrollView, Text, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppPressable } from "@/components/AppPressable"; import type { Schedule } from "@/services/calcom"; import type { ScheduleAvailability } from "@/services/types"; +import { getColors } from "@/constants/colors"; const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; @@ -90,34 +91,48 @@ export const EditAvailabilityHoursScreen = forwardRef - No schedule data + + No schedule data ); } return ( - + Tap a day to edit its hours {DAYS.map((day, dayIndex) => { @@ -128,44 +143,44 @@ export const EditAvailabilityHoursScreen = forwardRef 0 ? "border-t border-[#E5E5EA]" : "" + dayIndex > 0 + ? isDark + ? "border-t border-[#4D4D4D]" + : "border-t border-[#E5E5EA]" + : "" }`} onPress={() => onDayPress(dayIndex)} > - {/* Day status indicator */} - {/* Day name */} {day} - {/* Time slots or unavailable */} {isEnabled ? ( daySlots.map((slot, slotIndex) => ( 0 ? "mt-0.5" : ""}`} + className={`text-[15px] text-[#A3A3A3] ${slotIndex > 0 ? "mt-0.5" : ""}`} > {formatTime12Hour(slot.startTime)} – {formatTime12Hour(slot.endTime)} )) ) : ( - Unavailable + Unavailable )} - {/* Chevron */} - + ); })} diff --git a/companion/components/screens/EditAvailabilityHoursScreen.tsx b/companion/components/screens/EditAvailabilityHoursScreen.tsx index 6aebaf2158..02c00a05ef 100644 --- a/companion/components/screens/EditAvailabilityHoursScreen.tsx +++ b/companion/components/screens/EditAvailabilityHoursScreen.tsx @@ -1,6 +1,6 @@ import { Ionicons } from "@expo/vector-icons"; import { forwardRef } from "react"; -import { ScrollView, Text, View } from "react-native"; +import { ScrollView, Text, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppPressable } from "@/components/AppPressable"; import type { Schedule } from "@/services/calcom"; @@ -87,30 +87,32 @@ export interface EditAvailabilityHoursScreenProps { export const EditAvailabilityHoursScreen = forwardRef( function EditAvailabilityHoursScreen({ schedule, onDayPress }, _ref) { const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; const availability = parseAvailability(schedule); if (!schedule) { return ( - - No schedule data + + No schedule data ); } return ( - + Tap a day to edit its hours - + {DAYS.map((day, dayIndex) => { const daySlots = availability[dayIndex] || []; const isEnabled = daySlots.length > 0; @@ -119,43 +121,45 @@ export const EditAvailabilityHoursScreen = forwardRef onDayPress(dayIndex)}> 0 ? "border-t border-gray-100" : "" + dayIndex > 0 ? "border-t border-gray-100 dark:border-[#4D4D4D]" : "" }`} > - {/* Day status indicator */} - {/* Day name */} {day} - {/* Time slots or unavailable */} {isEnabled ? ( daySlots.map((slot, slotIndex) => ( 0 ? "mt-0.5" : ""}`} + className={`text-[15px] text-gray-600 dark:text-[#A3A3A3] ${slotIndex > 0 ? "mt-0.5" : ""}`} > {formatTime12Hour(slot.startTime)} – {formatTime12Hour(slot.endTime)} )) ) : ( - Unavailable + + Unavailable + )} - {/* Chevron */} - + ); diff --git a/companion/components/screens/EditAvailabilityNameScreen.ios.tsx b/companion/components/screens/EditAvailabilityNameScreen.ios.tsx index 12d697a6e2..a4be373a5f 100644 --- a/companion/components/screens/EditAvailabilityNameScreen.ios.tsx +++ b/companion/components/screens/EditAvailabilityNameScreen.ios.tsx @@ -3,11 +3,20 @@ import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers"; import { Ionicons } from "@expo/vector-icons"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react"; -import { Alert, KeyboardAvoidingView, ScrollView, Text, TextInput, View } from "react-native"; +import { + Alert, + KeyboardAvoidingView, + ScrollView, + Text, + TextInput, + useColorScheme, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { TIMEZONES as ALL_TIMEZONES } from "@/constants/timezones"; import { type Schedule, useUpdateSchedule } from "@/hooks/useSchedules"; import { showErrorAlert } from "@/utils/alerts"; +import { getColors } from "@/constants/colors"; // Format timezones for display const TIMEZONES = ALL_TIMEZONES.map((tz) => ({ @@ -34,7 +43,14 @@ export const EditAvailabilityNameScreen = forwardRef< ref ) { const insets = useSafeAreaInsets(); - const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]"; + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + const backgroundStyle = transparentBackground + ? "bg-transparent" + : isDark + ? "bg-black" + : `bg-[${theme.backgroundMuted}]`; const [name, setName] = useState(""); const [timezone, setTimezone] = useState("UTC"); @@ -103,7 +119,7 @@ export const EditAvailabilityNameScreen = forwardRef< if (!schedule) { return ( - No schedule data + No schedule data ); } @@ -122,12 +138,18 @@ export const EditAvailabilityNameScreen = forwardRef< {transparentBackground ? ( <> {/* Name Input - Glass UI */} - Schedule Name - + Schedule Name + {/* Timezone Selector - Glass UI */} - Timezone - + Timezone + - {selectedTimezoneLabel} - {timezone} + + {selectedTimezoneLabel} + + {timezone} {/* Native iOS Context Menu Button */} @@ -181,15 +211,16 @@ export const EditAvailabilityNameScreen = forwardRef< ) : ( <> - {/* Name Input */} - + Schedule Name - + - {/* Timezone Selector */} - + Timezone - + - {selectedTimezoneLabel} - {timezone} + + {selectedTimezoneLabel} + + {timezone} {/* Native iOS Context Menu Button */} diff --git a/companion/components/screens/EditAvailabilityNameScreen.tsx b/companion/components/screens/EditAvailabilityNameScreen.tsx index ea62c9d58b..d581cb8238 100644 --- a/companion/components/screens/EditAvailabilityNameScreen.tsx +++ b/companion/components/screens/EditAvailabilityNameScreen.tsx @@ -8,6 +8,7 @@ import { Text, TextInput, TouchableOpacity, + useColorScheme, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -40,6 +41,8 @@ export const EditAvailabilityNameScreen = forwardRef< EditAvailabilityNameScreenProps >(function EditAvailabilityNameScreen({ schedule, onSuccess, onSavingChange }, ref) { const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; const [name, setName] = useState(""); const [timezone, setTimezone] = useState("UTC"); @@ -104,10 +107,12 @@ export const EditAvailabilityNameScreen = forwardRef< // Render timezone list content const renderTimezoneContent = () => ( <> - - Select Timezone + + + Select Timezone + setShowTimezoneModal(false)}> - + @@ -122,8 +127,8 @@ export const EditAvailabilityNameScreen = forwardRef< @@ -132,12 +137,14 @@ export const EditAvailabilityNameScreen = forwardRef< className={`text-[17px] ${ tz.id === timezone ? "font-semibold text-[#007AFF]" - : "font-medium text-gray-900" + : "font-medium text-gray-900 dark:text-white" }`} > {tz.label} - {tz.id} + + {tz.id} + {tz.id === timezone && ( @@ -154,14 +161,14 @@ export const EditAvailabilityNameScreen = forwardRef< if (!schedule) { return ( - - No schedule data + + No schedule data ); } return ( - + - {/* Name Input */} - + Schedule Name - {/* Timezone Selector */} - + Timezone setShowTimezoneModal(true)}> - + - {selectedTimezoneLabel} - {timezone} + + {selectedTimezoneLabel} + + + {timezone} + - + - {/* Timezone Modal */} setShowTimezoneModal(false)} > e.stopPropagation()} style={shadows.xl()} @@ -229,7 +237,7 @@ export const EditAvailabilityNameScreen = forwardRef< ) : ( - {renderTimezoneContent()} + {renderTimezoneContent()} )} diff --git a/companion/components/screens/EditAvailabilityOverrideScreen.android.tsx b/companion/components/screens/EditAvailabilityOverrideScreen.android.tsx index 89acca0b1f..c0096bec29 100644 --- a/companion/components/screens/EditAvailabilityOverrideScreen.android.tsx +++ b/companion/components/screens/EditAvailabilityOverrideScreen.android.tsx @@ -1,11 +1,12 @@ import { Ionicons } from "@expo/vector-icons"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react"; -import { Alert, Pressable, ScrollView, Switch, Text, View } from "react-native"; +import { Alert, Pressable, ScrollView, Switch, Text, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { DateTimePickerAndroid } from "@react-native-community/datetimepicker"; import { useUpdateSchedule } from "@/hooks/useSchedules"; import type { Schedule } from "@/services/calcom"; import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; +import { getColors } from "@/constants/colors"; // Convert 24-hour time to 12-hour format with AM/PM const formatTime12Hour = (time24: string): string => { @@ -92,7 +93,14 @@ export const EditAvailabilityOverrideScreen = forwardRef< ref ) { const insets = useSafeAreaInsets(); - const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]"; + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + const backgroundStyle = transparentBackground + ? "bg-transparent" + : isDark + ? "bg-black" + : `bg-[${theme.backgroundMuted}]`; // Use mutation hook for cache-synchronized updates const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule(); @@ -337,7 +345,7 @@ export const EditAvailabilityOverrideScreen = forwardRef< if (!schedule) { return ( - No schedule data + No schedule data ); } @@ -351,12 +359,17 @@ export const EditAvailabilityOverrideScreen = forwardRef< }} showsVerticalScrollIndicator={!transparentBackground} > - {/* Date Picker */} - Date + Date @@ -364,16 +377,23 @@ export const EditAvailabilityOverrideScreen = forwardRef< - {formatDateForDisplay(selectedDate)} + + {formatDateForDisplay(selectedDate)} + - + - {/* Unavailable Toggle */} @@ -381,40 +401,50 @@ export const EditAvailabilityOverrideScreen = forwardRef< - Mark as unavailable - Block this entire day + + Mark as unavailable + + Block this entire day - {/* Time Range (only when available) */} {!isUnavailable && ( <> - Available Hours + Available Hours - Start Time - + Start Time + {formatTime12Hour(dateToTimeString(startTime))} - – + – - End Time - + End Time + {formatTime12Hour(dateToTimeString(endTime))} @@ -423,34 +453,43 @@ export const EditAvailabilityOverrideScreen = forwardRef< )} - {/* Existing Overrides (if any) */} {!isEditing && schedule.overrides && schedule.overrides.length > 0 && ( <> - + Existing Overrides {schedule.overrides.map((override, index) => ( 0 ? "border-t border-[#E5E5EA]" : "" + index > 0 + ? isDark + ? "border-t border-[#4D4D4D]" + : "border-t border-[#E5E5EA]" + : "" }`} onPress={() => onEditOverride?.(index)} > - + {formatDateForDisplay(dateStringToDate(override.date ?? ""))} {normalizeTimeString(override.startTime ?? "00:00") === "00:00" && normalizeTimeString(override.endTime ?? "00:00") === "00:00" ? ( Unavailable ) : ( - + {formatTime12Hour(normalizeTimeString(override.startTime ?? "00:00"))} –{" "} {formatTime12Hour(normalizeTimeString(override.endTime ?? "00:00"))} @@ -464,10 +503,10 @@ export const EditAvailabilityOverrideScreen = forwardRef< onEditOverride?.(index)} > - + diff --git a/companion/components/screens/EditAvailabilityOverrideScreen.ios.tsx b/companion/components/screens/EditAvailabilityOverrideScreen.ios.tsx index 3c3adc2663..5907a46258 100644 --- a/companion/components/screens/EditAvailabilityOverrideScreen.ios.tsx +++ b/companion/components/screens/EditAvailabilityOverrideScreen.ios.tsx @@ -1,11 +1,12 @@ import { DatePicker, Host } from "@expo/ui/swift-ui"; import { Ionicons } from "@expo/vector-icons"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react"; -import { Alert, Pressable, ScrollView, Switch, Text, View } from "react-native"; +import { Alert, Pressable, ScrollView, Switch, Text, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useUpdateSchedule } from "@/hooks/useSchedules"; import type { Schedule } from "@/services/calcom"; import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; +import { getColors } from "@/constants/colors"; // Convert 24-hour time to 12-hour format with AM/PM const formatTime12Hour = (time24: string): string => { @@ -87,7 +88,14 @@ export const EditAvailabilityOverrideScreen = forwardRef< ref ) { const insets = useSafeAreaInsets(); - const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]"; + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + const backgroundColor = transparentBackground + ? "transparent" + : isDark + ? theme.background + : theme.backgroundMuted; // Use mutation hook for cache-synchronized updates const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule(); @@ -275,26 +283,32 @@ export const EditAvailabilityOverrideScreen = forwardRef< if (!schedule) { return ( - - No schedule data + + No schedule data ); } return ( - {/* Date Picker */} - Date + Date @@ -302,7 +316,9 @@ export const EditAvailabilityOverrideScreen = forwardRef< - {formatDateForDisplay(selectedDate)} + + {formatDateForDisplay(selectedDate)} + @@ -316,10 +332,15 @@ export const EditAvailabilityOverrideScreen = forwardRef< - {/* Unavailable Toggle */} @@ -327,30 +348,40 @@ export const EditAvailabilityOverrideScreen = forwardRef< - Mark as unavailable - Block this entire day + + Mark as unavailable + + Block this entire day - {/* Time Range (only when available) */} {!isUnavailable && ( <> - Available Hours + Available Hours - Start Time + Start Time - – + – - End Time + End Time )} - {/* Existing Overrides (if any) */} {!isEditing && schedule.overrides && schedule.overrides.length > 0 && ( <> - + Existing Overrides {schedule.overrides.map((override, index) => ( 0 ? "border-t border-[#E5E5EA]" : "" + index > 0 + ? isDark + ? "border-t border-[#4D4D4D]" + : "border-t border-[#E5E5EA]" + : "" }`} onPress={() => onEditOverride?.(index)} > - + {formatDateForDisplay(dateStringToDate(override.date ?? ""))} {override.startTime === "00:00" && override.endTime === "00:00" ? ( Unavailable ) : ( - + {formatTime12Hour(override.startTime ?? "00:00")} –{" "} {formatTime12Hour(override.endTime ?? "00:00")} @@ -417,10 +457,10 @@ export const EditAvailabilityOverrideScreen = forwardRef< onEditOverride?.(index)} > - + diff --git a/companion/components/screens/EditAvailabilityOverrideScreen.tsx b/companion/components/screens/EditAvailabilityOverrideScreen.tsx index bb76d3bc4d..7c2f0a381f 100644 --- a/companion/components/screens/EditAvailabilityOverrideScreen.tsx +++ b/companion/components/screens/EditAvailabilityOverrideScreen.tsx @@ -8,6 +8,7 @@ import { Text, TextInput, TouchableOpacity, + useColorScheme, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -76,6 +77,8 @@ export const EditAvailabilityOverrideScreen = forwardRef< ref ) { const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; // Use mutation hook for cache-synchronized updates const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule(); @@ -93,12 +96,12 @@ export const EditAvailabilityOverrideScreen = forwardRef< // Render modal content const renderTimePickerContent = () => ( <> - - + + Select {showTimePicker?.type === "start" ? "Start" : "End"} Time setShowTimePicker(null)}> - + @@ -111,14 +114,16 @@ export const EditAvailabilityOverrideScreen = forwardRef< {formatTime12Hour(time)} @@ -320,23 +325,24 @@ export const EditAvailabilityOverrideScreen = forwardRef< if (!schedule) { return ( - - No schedule data + + No schedule data ); } return ( - {/* Date Input */} - Date (YYYY-MM-DD) - + + Date (YYYY-MM-DD) + + {Platform.OS === "web" ? ( @@ -347,7 +353,7 @@ export const EditAvailabilityOverrideScreen = forwardRef< style={{ flex: 1, fontSize: 17, - color: "#000", + color: isDark ? "#FFFFFF" : "#000000", backgroundColor: "transparent", border: "none", outline: "none", @@ -356,54 +362,59 @@ export const EditAvailabilityOverrideScreen = forwardRef< /> ) : ( )} {selectedDate ? ( - + {formatDateForDisplay(selectedDate)} ) : null} - {/* Unavailable Toggle */} - + - Mark as unavailable - Block this entire day + Mark as unavailable + + Block this entire day + - {/* Time Range (only when available) */} {!isUnavailable && ( <> - Available Hours - + + Available Hours + + {Platform.OS === "web" ? ( - - Start Time + + Start Time setStartTime(e.target.value)} style={{ fontSize: 17, - color: "#000", + color: isDark ? "#FFFFFF" : "#000000", backgroundColor: "transparent", border: "none", outline: "none", @@ -414,26 +425,28 @@ export const EditAvailabilityOverrideScreen = forwardRef< ) : ( setShowTimePicker({ type: "start" })} > - Start Time - {formatTime12Hour(startTime)} + Start Time + + {formatTime12Hour(startTime)} + )} - – + – {Platform.OS === "web" ? ( - - End Time + + End Time setEndTime(e.target.value)} style={{ fontSize: 17, - color: "#000", + color: isDark ? "#FFFFFF" : "#000000", backgroundColor: "transparent", border: "none", outline: "none", @@ -444,11 +457,13 @@ export const EditAvailabilityOverrideScreen = forwardRef< ) : ( setShowTimePicker({ type: "end" })} > - End Time - {formatTime12Hour(endTime)} + End Time + + {formatTime12Hour(endTime)} + )} @@ -456,10 +471,9 @@ export const EditAvailabilityOverrideScreen = forwardRef< )} - {/* Existing Overrides */} {!isEditing && schedule.overrides && schedule.overrides.length > 0 && ( <> - + Existing Overrides @@ -469,15 +483,17 @@ export const EditAvailabilityOverrideScreen = forwardRef< className="mb-2.5" onPress={() => onEditOverride?.(index)} > - + - + {formatDateForDisplay(override.date ?? "")} {override.startTime === "00:00" && override.endTime === "00:00" ? ( - Unavailable + + Unavailable + ) : ( - + {formatTime12Hour(override.startTime ?? "00:00")} –{" "} {formatTime12Hour(override.endTime ?? "00:00")} @@ -485,7 +501,7 @@ export const EditAvailabilityOverrideScreen = forwardRef< handleDeleteOverride(index)} > @@ -494,7 +510,7 @@ export const EditAvailabilityOverrideScreen = forwardRef< style={{ marginLeft: 12 }} name="chevron-forward" size={18} - color="#C7C7CC" + color={isDark ? "#A3A3A3" : "#C7C7CC"} /> @@ -504,7 +520,6 @@ export const EditAvailabilityOverrideScreen = forwardRef< )} - {/* Time Picker Modal */} setShowTimePicker(null)} > e.stopPropagation()} style={shadows.xl()} @@ -526,7 +541,7 @@ export const EditAvailabilityOverrideScreen = forwardRef< ) : ( - {renderTimePickerContent()} + {renderTimePickerContent()} )} diff --git a/companion/components/screens/EditLocationScreen.ios.tsx b/companion/components/screens/EditLocationScreen.ios.tsx index 0b38cfc045..45e0535247 100644 --- a/companion/components/screens/EditLocationScreen.ios.tsx +++ b/companion/components/screens/EditLocationScreen.ios.tsx @@ -10,7 +10,14 @@ import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers"; import { Ionicons } from "@expo/vector-icons"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react"; -import { KeyboardAvoidingView, ScrollView, Text, TextInput, View } from "react-native"; +import { + KeyboardAvoidingView, + ScrollView, + Text, + TextInput, + useColorScheme, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useUpdateLocation } from "@/hooks/useBookings"; import type { Booking } from "@/services/calcom"; @@ -94,7 +101,21 @@ export const EditLocationScreen = forwardRef("link"); const [inputValue, setInputValue] = useState(""); @@ -191,14 +212,21 @@ export const EditLocationScreen = forwardRef - No booking data + + No booking data ); } return ( - + {/* Location Type Selector with Native Context Menu - Glass UI */} - - - + + + - + {selectedTypeConfig.label} - + {selectedTypeConfig.description} @@ -265,13 +305,21 @@ export const EditLocationScreen = forwardRef {/* Location Input - Glass UI */} - + {/* Current Location Info */} {booking.location && ( - - - + + + - Current Location - + + Current Location + + {booking.location} @@ -303,18 +366,29 @@ export const EditLocationScreen = forwardRef + Location Type - - - + + + - + {selectedTypeConfig.label} - + {selectedTypeConfig.description} @@ -360,16 +434,23 @@ export const EditLocationScreen = forwardRef {/* Location Input */} - + {selectedTypeConfig.label} - + ("link"); const [inputValue, setInputValue] = useState(""); const [showTypePicker, setShowTypePicker] = useState(false); @@ -194,8 +212,11 @@ export const EditLocationScreen = forwardRef - No booking data + + No booking data ); } @@ -204,7 +225,8 @@ export const EditLocationScreen = forwardRef {/* Current Location Info - hidden in transparent mode */} {!transparentBackground && booking.location && ( - + - + - Current Location - + + Current Location + + {booking.location} @@ -234,57 +266,71 @@ export const EditLocationScreen = forwardRef + Location Type )} setShowTypePicker(true)} disabled={isSaving} activeOpacity={0.7} > - + - + {selectedTypeConfig.label} - + {selectedTypeConfig.description} - + {/* Location Input */} {!transparentBackground && ( - + {selectedTypeConfig.label} )} - - + + Select Location Type @@ -326,7 +379,8 @@ export const EditLocationScreen = forwardRef { setSelectedType(item.id); if (selectedType !== item.id) { @@ -336,36 +390,54 @@ export const EditLocationScreen = forwardRef {item.label} - {item.description} + + {item.description} + - {isSelected && } + {isSelected && } ); }} /> setShowTypePicker(false)} > - Cancel + + Cancel + diff --git a/companion/components/screens/MarkNoShowScreen.tsx b/companion/components/screens/MarkNoShowScreen.tsx index 057aff1788..2c3673ba4f 100644 --- a/companion/components/screens/MarkNoShowScreen.tsx +++ b/companion/components/screens/MarkNoShowScreen.tsx @@ -7,11 +7,20 @@ import { Ionicons } from "@expo/vector-icons"; import { useState } from "react"; -import { ActivityIndicator, Alert, FlatList, Text, TouchableOpacity, View } from "react-native"; +import { + ActivityIndicator, + Alert, + FlatList, + Text, + TouchableOpacity, + useColorScheme, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useMarkNoShow } from "@/hooks/useBookings"; import type { Booking } from "@/services/calcom"; import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; +import { getColors } from "@/constants/colors"; interface Attendee { id?: number | string; @@ -55,11 +64,26 @@ export function MarkNoShowScreen({ transparentBackground = false, }: MarkNoShowScreenProps) { const insets = useSafeAreaInsets(); - const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]"; - const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]"; + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + const backgroundColor = transparentBackground + ? "transparent" + : isDark + ? theme.background + : theme.backgroundMuted; + const pillStyle = transparentBackground + ? "bg-[#E8E8ED]/50" + : isDark + ? "bg-[#4D4D4D]" + : "bg-[#E8E8ED]"; const safeAttendees = Array.isArray(attendees) ? attendees : []; const [processingEmail, setProcessingEmail] = useState(null); + // Dark mode colors + const destructiveColor = theme.destructive; + const successColor = "#16A34A"; + // Mark no-show mutation const markNoShowMutation = useMarkNoShow(); @@ -156,18 +180,26 @@ export function MarkNoShowScreen({ // For transparent mode (iOS glass UI), use consistent styling regardless of no-show state const itemBgStyle = transparentBackground - ? "bg-white/60" + ? isDark + ? "bg-[#171717]/80" + : "bg-white/60" : isNoShow - ? "bg-[#FEF2F2]" - : "bg-white"; + ? isDark + ? "bg-[#FF453A]/10" + : "bg-[#FEF2F2]" + : isDark + ? "bg-[#171717]" + : "bg-white"; return ( handleMarkNoShow(item)} @@ -176,46 +208,64 @@ export function MarkNoShowScreen({ > {getInitials(item.name)} - {item.name || "Unknown"} - {item.email} + + {item.name || "Unknown"} + + + {item.email} + {isNoShow && ( - - Marked as no-show + + + Marked as no-show + )} {isProcessing ? ( - + ) : ( {isNoShow ? "Unmark" : "Mark"} @@ -228,28 +278,36 @@ export function MarkNoShowScreen({ if (!booking) { return ( - - No booking data + + No booking data ); } return ( - + {safeAttendees.length === 0 ? ( - - + + - No attendees found - + + No attendees found + + Attendee information is not available for this booking. ) : ( <> - + Attendees ({safeAttendees.length}) {transparentBackground ? ( @@ -261,7 +319,9 @@ export function MarkNoShowScreen({ scrollEnabled={false} /> ) : ( - + ( - {/* Session header */} - + - + Session {sessions.length > 1 ? index + 1 : ""} {item.duration && ( - + {formatDuration(item.duration)} )} - {/* Session details */} - - + + Started: {formatDate(item.startTime)} {item.endTime && ( - - + + Ended: {formatTime(item.endTime)} )} {item.maxParticipants && ( - - + + Max participants: {item.maxParticipants} )} - {/* Participants */} {item.participants && item.participants.length > 0 && ( - - + + Participants ({item.participants.length}) {item.participants.map((participant, pIndex) => ( - + - + {participant.userName || "Unknown participant"} - + Joined: {formatTime(participant.joinTime)} {participant.duration && ` • ${formatDuration(participant.duration)}`} @@ -146,12 +185,16 @@ export function MeetingSessionDetailsScreen({ return ( - + - + No session details available - + Session details may not be available for all meetings. diff --git a/companion/components/screens/RescheduleScreen.android.tsx b/companion/components/screens/RescheduleScreen.android.tsx index 992b4fd98e..2655e53532 100644 --- a/companion/components/screens/RescheduleScreen.android.tsx +++ b/companion/components/screens/RescheduleScreen.android.tsx @@ -15,6 +15,7 @@ import { ScrollView, Text, TextInput, + useColorScheme, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -49,6 +50,22 @@ export const RescheduleScreen = forwardRef(new Date()); const [reason, setReason] = useState(""); @@ -153,24 +170,31 @@ export const RescheduleScreen = forwardRef - No booking data + + No booking data ); } return ( - + {/* Header - only shown when not using native header */} {!useNativeHeader && ( - + Save @@ -244,25 +268,42 @@ export const RescheduleScreen = forwardRef {/* Booking Title Card */} - - - + + + - Rescheduling - + + Rescheduling + + {booking.title} {/* Form Card */} - + {/* Date picker trigger */} ({ - backgroundColor: pressed ? "#F9F9F9" : "transparent", + backgroundColor: pressed ? colors.pressed : "transparent", + borderBottomWidth: 1, + borderBottomColor: colors.borderLight, })} onPress={() => { safeLogInfo("[RescheduleScreen] Opening date picker"); @@ -271,17 +312,26 @@ export const RescheduleScreen = forwardRef - New Date - {formattedDate} + + New Date + + + {formattedDate} + - + {/* Time picker trigger */} ({ - backgroundColor: pressed ? "#F9F9F9" : "transparent", + backgroundColor: pressed ? colors.pressed : "transparent", + borderBottomWidth: 1, + borderBottomColor: colors.borderLight, })} onPress={() => { safeLogInfo("[RescheduleScreen] Opening time picker"); @@ -290,21 +340,32 @@ export const RescheduleScreen = forwardRef - New Time - {formattedTime} + + New Time + + + {formattedTime} + - + {/* Reason input */} - + Reason (optional) (new Date()); const [reason, setReason] = useState(""); @@ -113,14 +134,21 @@ export const RescheduleScreen = forwardRef - No booking data + + No booking data ); } return ( - + {/* Booking Title */} {booking.title} @@ -149,7 +178,9 @@ export const RescheduleScreen = forwardRef - at + + at + {/* Reason input */} - + Reason (optional) - + {/* Booking Title Card */} - - - + + + - Rescheduling - + + Rescheduling + + {booking.title} {/* Form Card */} - + {/* Date picker */} - - New Date + + + New Date + {/* Time picker */} - - New Time + + + New Time + - + Reason (optional) (new Date()); const [showDatePicker, setShowDatePicker] = useState(false); const [showTimePicker, setShowTimePicker] = useState(false); @@ -182,8 +199,11 @@ export const RescheduleScreen = forwardRef - No booking data + + No booking data ); } @@ -192,7 +212,8 @@ export const RescheduleScreen = forwardRef {/* Booking Title Card */} {!transparentBackground && ( - + - + - Rescheduling - + + Rescheduling + + {booking.title} @@ -226,9 +257,19 @@ export const RescheduleScreen = forwardRef - Date + + Date + { safeLogInfo("[RescheduleScreen] Opening date picker"); setShowDatePicker(true); @@ -236,7 +277,9 @@ export const RescheduleScreen = forwardRef - {formattedDate} + + {formattedDate} + )} @@ -244,9 +287,19 @@ export const RescheduleScreen = forwardRef - Time + + Time + { safeLogInfo("[RescheduleScreen] Opening time picker"); setShowTimePicker(true); @@ -254,17 +307,27 @@ export const RescheduleScreen = forwardRef - {formattedTime} + + {formattedTime} + )} {/* Reason input */} - + ) : ( - + {/* Date picker */} {isWeb ? ( - - New Date + + + New Date + ) : ( - - New Date + + + New Date + { safeLogInfo("[RescheduleScreen] Opening date picker"); setShowDatePicker(true); @@ -316,15 +399,25 @@ export const RescheduleScreen = forwardRef - {formattedDate} + + {formattedDate} + )} {/* Time picker */} {isWeb ? ( - - New Time + + + New Time + ) : ( - - New Time + + + New Time + { safeLogInfo("[RescheduleScreen] Opening time picker"); setShowTimePicker(true); @@ -367,20 +469,26 @@ export const RescheduleScreen = forwardRef - {formattedTime} + + {formattedTime} + )} {/* Reason input */} - + Reason (optional) setShowDatePicker(false)} > - - + + Select Date { const newDate = new Date(option.value); newDate.setHours(selectedDateTime.getHours()); @@ -432,23 +551,33 @@ export const RescheduleScreen = forwardRef {option.label} - {isSelected && } + {isSelected && ( + + )} ); })} setShowDatePicker(false)} > - Done + + Done + @@ -468,8 +597,18 @@ export const RescheduleScreen = forwardRef setShowTimePicker(false)} > - - + + Select Time { const newDate = new Date(selectedDateTime); newDate.setHours(option.value.hour); @@ -495,23 +635,33 @@ export const RescheduleScreen = forwardRef {option.label} - {isSelected && } + {isSelected && ( + + )} ); })} setShowTimePicker(false)} > - Done + + Done + diff --git a/companion/components/screens/ViewRecordingsScreen.tsx b/companion/components/screens/ViewRecordingsScreen.tsx index 953c6d0459..cdc368fbde 100644 --- a/companion/components/screens/ViewRecordingsScreen.tsx +++ b/companion/components/screens/ViewRecordingsScreen.tsx @@ -7,9 +7,10 @@ import { Ionicons } from "@expo/vector-icons"; import * as WebBrowser from "expo-web-browser"; -import { FlatList, Text, TouchableOpacity, View } from "react-native"; +import { FlatList, Text, TouchableOpacity, useColorScheme, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { BookingRecording } from "@/services/types/bookings.types"; +import { getColors } from "@/constants/colors"; export interface ViewRecordingsScreenProps { recordings: BookingRecording[]; @@ -40,8 +41,19 @@ export function ViewRecordingsScreen({ transparentBackground = false, }: ViewRecordingsScreenProps) { const insets = useSafeAreaInsets(); - const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]"; - const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]"; + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const theme = getColors(isDark); + const backgroundStyle = transparentBackground + ? "bg-transparent" + : isDark + ? "bg-black" + : `bg-[${theme.backgroundMuted}]`; + const pillStyle = transparentBackground + ? "bg-[#E8E8ED]/50" + : isDark + ? "bg-[#4D4D4D]" + : "bg-[#E8E8ED]"; const handleOpenRecording = async (recording: BookingRecording) => { try { @@ -56,25 +68,35 @@ export function ViewRecordingsScreen({ const renderRecording = ({ item }: { item: BookingRecording }) => ( handleOpenRecording(item)} activeOpacity={0.7} > - + - Recording - {formatDate(item.startTime)} + + Recording + + + {formatDate(item.startTime)} + {item.duration && ( - + Duration: {formatDuration(item.duration)} )} - + ); @@ -83,12 +105,16 @@ export function ViewRecordingsScreen({ return ( - + - + No recordings available - + Recordings may take some time to process after the meeting ends. diff --git a/companion/components/ui/skeleton.tsx b/companion/components/ui/skeleton.tsx index 5b896dc929..38ce092e77 100644 --- a/companion/components/ui/skeleton.tsx +++ b/companion/components/ui/skeleton.tsx @@ -1,17 +1,18 @@ +import { semanticColors } from "@/constants/colors"; import { cn } from "@/lib/utils"; -import { View } from "react-native"; +import { useColorScheme, View } from "react-native"; function Skeleton({ className, style, ...props }: React.ComponentProps & React.RefAttributes) { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + const backgroundColor = isDark ? semanticColors.skeleton.dark : semanticColors.skeleton.light; + return ( - + ); } diff --git a/companion/components/ui/toast.tsx b/companion/components/ui/toast.tsx index 148bd241f9..1f1c442914 100644 --- a/companion/components/ui/toast.tsx +++ b/companion/components/ui/toast.tsx @@ -41,7 +41,7 @@ export function Toast({ visible, message, type }: ToastProps) { >