feat: draw calendar colours in the colour-blind-safe palette when that setting is on

This commit is contained in:
Linus Rath
2026-10-31 15:05:48 +01:00
parent 6e5980e4e0
commit 21dd5a2405
15 changed files with 110 additions and 35 deletions
@@ -14,7 +14,7 @@ import { format } from 'date-fns';
import { ArrowLeft, MapPin, Repeat, Search, X } from '../icons'; import { ArrowLeft, MapPin, Repeat, Search, X } from '../icons';
import type { Calendar, CalendarEvent } from '../../api/types'; import type { Calendar, CalendarEvent } from '../../api/types';
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens'; import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
import { useCalendarStore, searchCalendarEvents } from '../../stores/calendar-store'; import { useCalendarStore, searchCalendarEvents } from '../../stores/calendar-store';
import { useCalendarLocale } from '../../lib/calendar-locale'; import { useCalendarLocale } from '../../lib/calendar-locale';
import { formatCalendarDay } from '../../lib/calendar-system'; import { formatCalendarDay } from '../../lib/calendar-system';
@@ -62,6 +62,7 @@ export function CalendarSearchSheet({
onClose, onClose,
}: CalendarSearchSheetProps) { }: CalendarSearchSheetProps) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
const { locale, t, system } = useCalendarLocale(); const { locale, t, system } = useCalendarLocale();
const loaded = useCalendarStore((s) => s.events); const loaded = useCalendarStore((s) => s.events);
@@ -134,7 +135,7 @@ export function CalendarSearchSheet({
const start = getEventStartDate(event); const start = getEventStartDate(event);
const end = getEventEndDate(event); const end = getEventEndDate(event);
const calendar = calendars.find((cal) => cal.id === getPrimaryCalendarId(event)); const calendar = calendars.find((cal) => cal.id === getPrimaryCalendarId(event));
const color = getEventColor(event, calendars); const color = paint(getEventColor(event, calendars));
const inactive = isInactiveEvent(event, currentUserEmails); const inactive = isInactiveEvent(event, currentUserEmails);
const place = event.locations ? Object.values(event.locations)[0]?.name : undefined; const place = event.locations ? Object.values(event.locations)[0]?.name : undefined;
const meta = [place, calendar?.name].filter(Boolean).join(' · '); const meta = [place, calendar?.name].filter(Boolean).join(' · ');
@@ -16,7 +16,7 @@ import {
} from '../icons'; } from '../icons';
import type { Calendar } from '../../api/types'; import type { Calendar } from '../../api/types';
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens'; import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
import { useAnimDuration } from '../../theme/dynamic'; import { useAnimDuration } from '../../theme/dynamic';
import { CALENDAR_COLOR_PALETTE, calendarColorName, getCalendarColor } from '../../lib/calendar-utils'; import { CALENDAR_COLOR_PALETTE, calendarColorName, getCalendarColor } from '../../lib/calendar-utils';
import { BIRTHDAY_CALENDAR_ID } from '../../lib/birthday-calendar'; import { BIRTHDAY_CALENDAR_ID } from '../../lib/birthday-calendar';
@@ -309,6 +309,7 @@ function Section({
isSubscriptionCalendar?: (calendarId: string) => boolean; isSubscriptionCalendar?: (calendarId: string) => boolean;
}) { }) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
const t = useLocaleStore((s) => s.t); const t = useLocaleStore((s) => s.t);
// Neighbours a calendar can move past: the server calendars of this section. // Neighbours a calendar can move past: the server calendars of this section.
@@ -352,8 +353,8 @@ function Section({
style={[ style={[
styles.swatch, styles.swatch,
{ {
backgroundColor: visible ? getCalendarColor(cal) : 'transparent', backgroundColor: visible ? paint(getCalendarColor(cal)) : 'transparent',
borderColor: getCalendarColor(cal), borderColor: paint(getCalendarColor(cal)),
}, },
]} ]}
> >
+3 -2
View File
@@ -4,7 +4,7 @@ import { Clock, MapPin, Users } from '../icons';
import { format } from 'date-fns'; import { format } from 'date-fns';
import type { Calendar, CalendarEvent } from '../../api/types'; import type { Calendar, CalendarEvent } from '../../api/types';
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens'; import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
import { import {
eventTimeRange, eventTimeRange,
getEventColor, getEventColor,
@@ -38,9 +38,10 @@ function participantCount(event: CalendarEvent): number {
*/ */
export function EventCard({ event, calendars, timeFormat, currentUserEmails, onPress, onLongPress, day }: EventCardProps) { export function EventCard({ event, calendars, timeFormat, currentUserEmails, onPress, onLongPress, day }: EventCardProps) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
const { locale, t } = useCalendarLocale(); const { locale, t } = useCalendarLocale();
const color = getEventColor(event, calendars); const color = paint(getEventColor(event, calendars));
const range = eventTimeRange(event); const range = eventTimeRange(event);
const { start, end } = range; const { start, end } = range;
const allDay = range.allDay || (!!day && isTimedEventFullDayOnDate(event, day)); const allDay = range.allDay || (!!day && isTimedEventFullDayOnDate(event, day));
+3 -2
View File
@@ -39,7 +39,7 @@ import {
import { format, type Locale } from 'date-fns'; import { format, type Locale } from 'date-fns';
import type { Calendar, CalendarEvent } from '../../api/types'; import type { Calendar, CalendarEvent } from '../../api/types';
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens'; import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
import { import {
eventTimeRange, eventTimeRange,
getEventColor, getEventColor,
@@ -140,6 +140,7 @@ export function EventDetailSheet({
onSaveNote, onSaveNote,
}: EventDetailSheetProps) { }: EventDetailSheetProps) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
const { locale, t } = useCalendarLocale(); const { locale, t } = useCalendarLocale();
const [rsvpBusy, setRsvpBusy] = React.useState(false); const [rsvpBusy, setRsvpBusy] = React.useState(false);
const [noteOpen, setNoteOpen] = React.useState(false); const [noteOpen, setNoteOpen] = React.useState(false);
@@ -200,7 +201,7 @@ export function EventDetailSheet({
if (!event) return null; if (!event) return null;
const color = getEventColor(event, calendars); const color = paint(getEventColor(event, calendars));
const calendarId = getPrimaryCalendarId(event); const calendarId = getPrimaryCalendarId(event);
const calendar = calendars.find((c) => c.id === calendarId); const calendar = calendars.find((c) => c.id === calendarId);
const range = formatRange(event, timeFormat, locale); const range = formatRange(event, timeFormat, locale);
+4 -3
View File
@@ -36,7 +36,7 @@ import type {
VirtualLocation, VirtualLocation,
} from '../../api/types'; } from '../../api/types';
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens'; import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
import { import {
getCalendarColor, getCalendarColor,
getPrimaryCalendarId, getPrimaryCalendarId,
@@ -189,6 +189,7 @@ export function EventModal({
onClose, onClose,
}: EventModalProps) { }: EventModalProps) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
const { locale, t } = useCalendarLocale(); const { locale, t } = useCalendarLocale();
const timeFormat = useSettingsStore((s) => s.calendarTimeFormat); const timeFormat = useSettingsStore((s) => s.calendarTimeFormat);
@@ -588,7 +589,7 @@ export function EventModal({
<View <View
style={[ style={[
styles.calendarSwatch, styles.calendarSwatch,
{ backgroundColor: getCalendarColor(selectedCalendar) }, { backgroundColor: paint(getCalendarColor(selectedCalendar)) },
]} ]}
/> />
)} )}
@@ -614,7 +615,7 @@ export function EventModal({
<View <View
style={[ style={[
styles.calendarSwatch, styles.calendarSwatch,
{ backgroundColor: getCalendarColor(c) }, { backgroundColor: paint(getCalendarColor(c)) },
]} ]}
/> />
<Text style={styles.popoverRowText}>{c.name}</Text> <Text style={styles.popoverRowText}>{c.name}</Text>
+3 -2
View File
@@ -10,7 +10,7 @@ import { uploadBlob } from '../../api/blob';
import { parseCalendarBlob } from '../../api/calendar'; import { parseCalendarBlob } from '../../api/calendar';
import Button from '../Button'; import Button from '../Button';
import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
import { getCalendarColor } from '../../lib/calendar-utils'; import { getCalendarColor } from '../../lib/calendar-utils';
import { isWritableCalendar } from '../../lib/calendar-editability'; import { isWritableCalendar } from '../../lib/calendar-editability';
import { ImportRefusedError, type ImportResult } from '../../stores/calendar-store'; import { ImportRefusedError, type ImportResult } from '../../stores/calendar-store';
@@ -58,6 +58,7 @@ function eventDateLabel(event: Partial<CalendarEvent>, locale: Locale): string |
export function ICalImportSheet({ visible, onClose, calendars, onImport, onImported }: Props) { export function ICalImportSheet({ visible, onClose, calendars, onImport, onImported }: Props) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const t = useLocaleStore((s) => s.t); const t = useLocaleStore((s) => s.t);
@@ -308,7 +309,7 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
onPress={() => setCalendarId(cal.id)} onPress={() => setCalendarId(cal.id)}
style={[styles.calChip, cal.id === calendarId && styles.calChipActive]} style={[styles.calChip, cal.id === calendarId && styles.calChipActive]}
> >
<View style={[styles.calSwatch, { backgroundColor: getCalendarColor(cal) }]} /> <View style={[styles.calSwatch, { backgroundColor: paint(getCalendarColor(cal)) }]} />
<Text style={styles.calChipText} numberOfLines={1}>{cal.name}</Text> <Text style={styles.calChipText} numberOfLines={1}>{cal.name}</Text>
</Pressable> </Pressable>
))} ))}
@@ -7,7 +7,7 @@ import { X, Plus, RefreshCw, Trash2, Rss, AlertTriangle } from '../icons';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import Button from '../Button'; import Button from '../Button';
import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
import { CALENDAR_COLOR_PALETTE, calendarColorName } from '../../lib/calendar-utils'; import { CALENDAR_COLOR_PALETTE, calendarColorName } from '../../lib/calendar-utils';
import { useCalendarLocale } from '../../lib/calendar-locale'; import { useCalendarLocale } from '../../lib/calendar-locale';
import { import {
@@ -37,6 +37,7 @@ interface Props {
export function ICalSubscriptionSheet({ visible, onClose }: Props) { export function ICalSubscriptionSheet({ visible, onClose }: Props) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { locale, t } = useCalendarLocale(); const { locale, t } = useCalendarLocale();
@@ -257,7 +258,7 @@ export function ICalSubscriptionSheet({ visible, onClose }: Props) {
const busy = !!syncing[sub.id]; const busy = !!syncing[sub.id];
return ( return (
<View key={sub.id} style={styles.subRow}> <View key={sub.id} style={styles.subRow}>
<View style={[styles.subSwatch, { backgroundColor: sub.color || c.primary }]} /> <View style={[styles.subSwatch, { backgroundColor: sub.color ? paint(sub.color) : c.primary }]} />
<Pressable style={{ flex: 1, minWidth: 0 }} onPress={() => startEdit(sub)}> <Pressable style={{ flex: 1, minWidth: 0 }} onPress={() => startEdit(sub)}>
<Text style={styles.subName} numberOfLines={1}>{sub.name}</Text> <Text style={styles.subName} numberOfLines={1}>{sub.name}</Text>
<Text style={styles.subMeta} numberOfLines={1}> <Text style={styles.subMeta} numberOfLines={1}>
+6 -4
View File
@@ -10,7 +10,7 @@ import {
} from 'date-fns'; } from 'date-fns';
import type { Calendar, CalendarEvent } from '../../api/types'; import type { Calendar, CalendarEvent } from '../../api/types';
import { componentSizes, spacing, typography, type ThemePalette } from '../../theme/tokens'; import { componentSizes, spacing, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
import { useSettingsStore } from '../../stores/settings-store'; import { useSettingsStore } from '../../stores/settings-store';
import { import {
buildEventDayIndex, buildEventDayIndex,
@@ -157,6 +157,7 @@ function MonthWeekRowInner({
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
// "Light event colors" (webmail calendarTintedEvents). // "Light event colors" (webmail calendarTintedEvents).
const tinted = useSettingsStore((s) => s.calendarTintedEvents); const tinted = useSettingsStore((s) => s.calendarTintedEvents);
const paint = useCvdColor();
const chipTime = monthChipShowsTime(true, width / 7); const chipTime = monthChipShowsTime(true, width / 7);
if (variant === 'full') { if (variant === 'full') {
return ( return (
@@ -241,7 +242,7 @@ function MonthWeekRowInner({
{visible.map((event, idx) => ( {visible.map((event, idx) => (
<View <View
key={`${event.id}-${idx}`} key={`${event.id}-${idx}`}
style={[styles.dot, { backgroundColor: getEventColor(event, calendars) }]} style={[styles.dot, { backgroundColor: paint(getEventColor(event, calendars)) }]}
/> />
))} ))}
{overflow > 0 && ( {overflow > 0 && (
@@ -255,7 +256,7 @@ function MonthWeekRowInner({
// Solid bars in the calendar colour; declined and // Solid bars in the calendar colour; declined and
// cancelled events outlined (repos/branding/APP.md). // cancelled events outlined (repos/branding/APP.md).
const inactive = isInactiveEvent(event, currentUserEmails); const inactive = isInactiveEvent(event, currentUserEmails);
const colors = eventBlockColors(getEventColor(event, calendars), inactive, c, tinted); const colors = eventBlockColors(paint(getEventColor(event, calendars)), inactive, c, tinted);
return ( return (
<View <View
key={`${event.id}-${idx}`} key={`${event.id}-${idx}`}
@@ -331,6 +332,7 @@ function FullWeekRow({
const { t, system } = useCalendarLocale(); const { t, system } = useCalendarLocale();
// "Light event colors" (webmail calendarTintedEvents). // "Light event colors" (webmail calendarTintedEvents).
const tinted = useSettingsStore((s) => s.calendarTintedEvents); const tinted = useSettingsStore((s) => s.calendarTintedEvents);
const paint = useCvdColor();
const showTime = monthChipShowsTime(showTimeInMonthView, width / 7); const showTime = monthChipShowsTime(showTimeInMonthView, width / 7);
return ( return (
<View style={[styles.fullWeekRow, { height }]}> <View style={[styles.fullWeekRow, { height }]}>
@@ -371,7 +373,7 @@ function FullWeekRow({
</View> </View>
{dayEvents.slice(0, shown).map((event, idx) => { {dayEvents.slice(0, shown).map((event, idx) => {
const inactive = isInactiveEvent(event, currentUserEmails); const inactive = isInactiveEvent(event, currentUserEmails);
const colors = eventBlockColors(getEventColor(event, calendars), inactive, c, tinted); const colors = eventBlockColors(paint(getEventColor(event, calendars)), inactive, c, tinted);
// No time on events that fill the whole day. // No time on events that fill the whole day.
const time = showTime && !event.showWithoutTime && !isTimedEventFullDayOnDate(event, d) const time = showTime && !event.showWithoutTime && !isTimedEventFullDayOnDate(event, d)
? `${format(getEventStartDate(event), timePattern(timeFormat), { locale })} ` ? `${format(getEventStartDate(event), timePattern(timeFormat), { locale })} `
+3 -2
View File
@@ -27,7 +27,7 @@ import {
import { format, isBefore, startOfDay } from 'date-fns'; import { format, isBefore, startOfDay } from 'date-fns';
import type { Calendar, CalendarEvent } from '../../api/types'; import type { Calendar, CalendarEvent } from '../../api/types';
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens'; import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
import { getCalendarColor, getTaskDueDisplayDate, timePattern, type TimeFormat } from '../../lib/calendar-utils'; import { getCalendarColor, getTaskDueDisplayDate, timePattern, type TimeFormat } from '../../lib/calendar-utils';
import { displayNow, isDisplayToday } from '../../lib/calendar-timezone'; import { displayNow, isDisplayToday } from '../../lib/calendar-timezone';
import { import {
@@ -109,6 +109,7 @@ export function TasksSheet({
onDelete, onDelete,
}: TasksSheetProps) { }: TasksSheetProps) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
const { locale, t } = useCalendarLocale(); const { locale, t } = useCalendarLocale();
const slideY = React.useRef(new Animated.Value(Dimensions.get('window').height)).current; const slideY = React.useRef(new Animated.Value(Dimensions.get('window').height)).current;
@@ -373,7 +374,7 @@ export function TasksSheet({
onPress={() => setEditor((e) => ({ ...e, calendarId: cal.id }))} onPress={() => setEditor((e) => ({ ...e, calendarId: cal.id }))}
style={[styles.calChip, cal.id === editor.calendarId && styles.calChipActive]} style={[styles.calChip, cal.id === editor.calendarId && styles.calChipActive]}
> >
<View style={[styles.calSwatch, { backgroundColor: getCalendarColor(cal) }]} /> <View style={[styles.calSwatch, { backgroundColor: paint(getCalendarColor(cal)) }]} />
<Text style={styles.calChipText} numberOfLines={1}>{cal.name}</Text> <Text style={styles.calChipText} numberOfLines={1}>{cal.name}</Text>
</Pressable> </Pressable>
))} ))}
@@ -18,7 +18,7 @@ import {
import { differenceInCalendarDays, format, type Locale } from 'date-fns'; import { differenceInCalendarDays, format, type Locale } from 'date-fns';
import type { Calendar, CalendarEvent } from '../../api/types'; import type { Calendar, CalendarEvent } from '../../api/types';
import { spacing, typography, type ThemePalette } from '../../theme/tokens'; import { spacing, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
import { useSettingsStore } from '../../stores/settings-store'; import { useSettingsStore } from '../../stores/settings-store';
import { useCalendarLocale } from '../../lib/calendar-locale'; import { useCalendarLocale } from '../../lib/calendar-locale';
import { dayNumberLabel } from '../../lib/calendar-system'; import { dayNumberLabel } from '../../lib/calendar-system';
@@ -125,6 +125,7 @@ function TimeGridScrollViewInner({
draftResetKey = 0, draftResetKey = 0,
}: TimeGridScrollViewProps) { }: TimeGridScrollViewProps) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
// "Light event colors" (webmail calendarTintedEvents). // "Light event colors" (webmail calendarTintedEvents).
const tinted = useSettingsStore((s) => s.calendarTintedEvents); const tinted = useSettingsStore((s) => s.calendarTintedEvents);
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
@@ -455,7 +456,7 @@ function TimeGridScrollViewInner({
key={`${segment.event.id}:${segment.startIndex}:${segment.row}`} key={`${segment.event.id}:${segment.startIndex}:${segment.row}`}
title={segment.event.title || t('calendar.events.no_title', '(No title)')} title={segment.event.title || t('calendar.events.no_title', '(No title)')}
colors={eventBlockColors( colors={eventBlockColors(
getEventColor(segment.event, calendars), paint(getEventColor(segment.event, calendars)),
isInactiveEvent(segment.event, currentUserEmails), isInactiveEvent(segment.event, currentUserEmails),
c, c,
tinted, tinted,
@@ -620,6 +621,7 @@ const DayColumn = React.memo(function DayColumn({
const { locale, t } = useCalendarLocale(); const { locale, t } = useCalendarLocale();
// "Light event colors" (webmail calendarTintedEvents). // "Light event colors" (webmail calendarTintedEvents).
const tinted = useSettingsStore((s) => s.calendarTintedEvents); const tinted = useSettingsStore((s) => s.calendarTintedEvents);
const paint = useCvdColor();
const handleLongPress = React.useCallback( const handleLongPress = React.useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
onLongPressAt?.(day, hourAtOffset(e.nativeEvent.locationY, HOUR_HEIGHT)); onLongPressAt?.(day, hourAtOffset(e.nativeEvent.locationY, HOUR_HEIGHT));
@@ -650,7 +652,7 @@ const DayColumn = React.memo(function DayColumn({
? minutesToTimeLabel(startMinutes, timeFormat) ? minutesToTimeLabel(startMinutes, timeFormat)
: null} : null}
colors={eventBlockColors( colors={eventBlockColors(
getEventColor(event, calendars), paint(getEventColor(event, calendars)),
isInactiveEvent(event, currentUserEmails), isInactiveEvent(event, currentUserEmails),
colors, colors,
tinted, tinted,
+4 -3
View File
@@ -10,7 +10,7 @@ import { addDays, format, isSameDay, startOfWeek } from 'date-fns';
import { displayNow, displayNowMinutes, isDisplayToday } from '../../lib/calendar-timezone'; import { displayNow, displayNowMinutes, isDisplayToday } from '../../lib/calendar-timezone';
import type { Calendar, CalendarEvent } from '../../api/types'; import type { Calendar, CalendarEvent } from '../../api/types';
import { spacing, typography, type ThemePalette } from '../../theme/tokens'; import { spacing, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
import { useSettingsStore } from '../../stores/settings-store'; import { useSettingsStore } from '../../stores/settings-store';
import { useCalendarLocale } from '../../lib/calendar-locale'; import { useCalendarLocale } from '../../lib/calendar-locale';
import { dayNumberLabel } from '../../lib/calendar-system'; import { dayNumberLabel } from '../../lib/calendar-system';
@@ -68,6 +68,7 @@ function WeekViewInner({
currentUserEmails, currentUserEmails,
}: WeekViewProps) { }: WeekViewProps) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
// "Light event colors" (webmail calendarTintedEvents). // "Light event colors" (webmail calendarTintedEvents).
const tinted = useSettingsStore((s) => s.calendarTintedEvents); const tinted = useSettingsStore((s) => s.calendarTintedEvents);
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
@@ -213,7 +214,7 @@ function WeekViewInner({
key={`${segment.event.id}:${segment.startIndex}:${segment.row}`} key={`${segment.event.id}:${segment.startIndex}:${segment.row}`}
title={segment.event.title || t('calendar.events.no_title', '(No title)')} title={segment.event.title || t('calendar.events.no_title', '(No title)')}
colors={eventBlockColors( colors={eventBlockColors(
getEventColor(segment.event, calendars), paint(getEventColor(segment.event, calendars)),
isInactiveEvent(segment.event, currentUserEmails), isInactiveEvent(segment.event, currentUserEmails),
c, c,
tinted, tinted,
@@ -289,7 +290,7 @@ function WeekViewInner({
? minutesToTimeLabel(startMinutes, timeFormat) ? minutesToTimeLabel(startMinutes, timeFormat)
: null} : null}
colors={eventBlockColors( colors={eventBlockColors(
getEventColor(event, calendars), paint(getEventColor(event, calendars)),
isInactiveEvent(event, currentUserEmails), isInactiveEvent(event, currentUserEmails),
c, c,
tinted, tinted,
@@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { cvdColor } from '../../../lib/cvd-palette';
// Checked on the source: the suite has no React Native renderer. Every place
// that draws a calendar's colour runs it through useCvdColor() first, so the
// colour-blind-safe setting recolours calendars as its description promises
// (webmail cf4d02b49). The colour pickers keep the raw colours.
const src = path.resolve(__dirname, '../../..');
const DRAWERS = [
'components/calendar/EventCard.tsx',
'components/calendar/EventDetailSheet.tsx',
'components/calendar/MonthView.tsx',
'components/calendar/WeekView.tsx',
'components/calendar/TimeGridScrollView.tsx',
'components/calendar/CalendarSearchSheet.tsx',
'components/calendar/CalendarSidebarDrawer.tsx',
'components/calendar/EventModal.tsx',
'components/calendar/TasksSheet.tsx',
'components/calendar/ICalImportSheet.tsx',
'components/contacts/ContactActivity.tsx',
'components/email/CalendarInvitationBanner.tsx',
'components/email/structured-mail/booking-calendar-actions.tsx',
];
function colourCalls(source: string): string[] {
return source
.split(/\r?\n/)
.filter((line) => !/^\s*import\b/.test(line) && /\bget(Event|Calendar)Color\(/.test(line))
.map((line) => line.trim());
}
describe('calendar colours in the colour-blind-safe palette', () => {
it.each(DRAWERS)('%s paints every calendar colour it draws', (file) => {
const source = fs.readFileSync(path.join(src, file), 'utf8');
expect(source).toMatch(/const paint = useCvdColor\(\);/);
const calls = colourCalls(source);
expect(calls.length).toBeGreaterThan(0);
for (const line of calls) {
expect(line).toMatch(/paint\(get(Event|Calendar)Color\(/);
}
});
it('keeps the raw colour in the calendar editor, which is a colour picker', () => {
const source = fs.readFileSync(path.join(src, 'components/calendar/CalendarEditSheet.tsx'), 'utf8');
expect(source).not.toMatch(/useCvdColor/);
});
it('paints a subscription swatch through the hook', () => {
const source = fs.readFileSync(path.join(src, 'components/calendar/ICalSubscriptionSheet.tsx'), 'utf8');
expect(source).toMatch(/paint\(sub\.color\)/);
});
it('maps a calendar green to the palette green only when the setting is on', () => {
expect(cvdColor('#22c55e', false)).toBe('#22c55e');
expect(cvdColor('#22c55e', true).toLowerCase()).not.toBe('#22c55e');
});
});
+3 -2
View File
@@ -17,7 +17,7 @@ import { singleLine } from '../../lib/single-line';
import { prefetchMessage } from '../../lib/email-detail-cache'; import { prefetchMessage } from '../../lib/email-detail-cache';
import SenderAvatar from '../SenderAvatar'; import SenderAvatar from '../SenderAvatar';
import { radius, spacing, typography, componentSizes, type ThemePalette } from '../../theme/tokens'; import { radius, spacing, typography, componentSizes, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
const EMAIL_LIMIT = 5; const EMAIL_LIMIT = 5;
const EVENT_LIMIT = 5; const EVENT_LIMIT = 5;
@@ -92,6 +92,7 @@ interface Props {
export function ContactActivity({ contact }: Props) { export function ContactActivity({ contact }: Props) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
const navigation = useNavigation<Nav>(); const navigation = useNavigation<Nav>();
const t = useLocaleStore((s) => s.t); const t = useLocaleStore((s) => s.t);
@@ -254,7 +255,7 @@ export function ContactActivity({ contact }: Props) {
upcomingEvents.map((event) => ( upcomingEvents.map((event) => (
<View key={event.id} style={styles.eventRow}> <View key={event.id} style={styles.eventRow}>
<View <View
style={[styles.eventDot, { backgroundColor: getEventColor(event, calendars) }]} style={[styles.eventDot, { backgroundColor: paint(getEventColor(event, calendars)) }]}
/> />
<Text style={styles.eventTime}>{formatEventTime(event, t)}</Text> <Text style={styles.eventTime}>{formatEventTime(event, t)}</Text>
<View style={styles.eventInfo}> <View style={styles.eventInfo}>
@@ -8,7 +8,7 @@ import {
import { format, isSameDay } from 'date-fns'; import { format, isSameDay } from 'date-fns';
import type { Calendar, Email, CalendarEvent } from '../../api/types'; import type { Calendar, Email, CalendarEvent } from '../../api/types';
import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors'; import { useColors, useCvdColor } from '../../theme/colors';
import { fetchCalendarBlobText, findEventsByUid, parseCalendarBlob } from '../../api/calendar'; import { fetchCalendarBlobText, findEventsByUid, parseCalendarBlob } from '../../api/calendar';
import { useCalendarStore } from '../../stores/calendar-store'; import { useCalendarStore } from '../../stores/calendar-store';
import { useSettingsStore } from '../../stores/settings-store'; import { useSettingsStore } from '../../stores/settings-store';
@@ -72,6 +72,7 @@ function trustReasonText(
export function CalendarInvitationBanner({ email, jmapAccountId }: Props) { export function CalendarInvitationBanner({ email, jmapAccountId }: Props) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
const t = useLocaleStore((s) => s.t); const t = useLocaleStore((s) => s.t);
const locale = useLocaleStore((s) => s.locale); const locale = useLocaleStore((s) => s.locale);
@@ -390,7 +391,7 @@ export function CalendarInvitationBanner({ email, jmapAccountId }: Props) {
{state !== 'done' && !existing && targetCalendar && candidates.length > 1 && ( {state !== 'done' && !existing && targetCalendar && candidates.length > 1 && (
<View> <View>
<Pressable style={styles.calendarPicker} onPress={() => setPickerOpen((v) => !v)}> <Pressable style={styles.calendarPicker} onPress={() => setPickerOpen((v) => !v)}>
<View style={[styles.calendarSwatch, { backgroundColor: getCalendarColor(targetCalendar) }]} /> <View style={[styles.calendarSwatch, { backgroundColor: paint(getCalendarColor(targetCalendar)) }]} />
<Text style={styles.calendarPickerText} numberOfLines={1}>{targetCalendar.name}</Text> <Text style={styles.calendarPickerText} numberOfLines={1}>{targetCalendar.name}</Text>
<ChevronDown size={14} color={c.textMuted} /> <ChevronDown size={14} color={c.textMuted} />
</Pressable> </Pressable>
@@ -402,7 +403,7 @@ export function CalendarInvitationBanner({ email, jmapAccountId }: Props) {
style={[styles.calendarRow, cal.id === targetCalendar.id && styles.calendarRowActive]} style={[styles.calendarRow, cal.id === targetCalendar.id && styles.calendarRowActive]}
onPress={() => { setCalendarId(cal.id); setPickerOpen(false); }} onPress={() => { setCalendarId(cal.id); setPickerOpen(false); }}
> >
<View style={[styles.calendarSwatch, { backgroundColor: getCalendarColor(cal) }]} /> <View style={[styles.calendarSwatch, { backgroundColor: paint(getCalendarColor(cal)) }]} />
<Text style={styles.calendarPickerText} numberOfLines={1}>{cal.name}</Text> <Text style={styles.calendarPickerText} numberOfLines={1}>{cal.name}</Text>
</Pressable> </Pressable>
))} ))}
@@ -11,7 +11,7 @@ import { getCalendarColor } from '../../../lib/calendar-utils';
import type { StructuredCard } from '../../../lib/structured-mail'; import type { StructuredCard } from '../../../lib/structured-mail';
import type { RootStackParamList } from '../../../navigation/types'; import type { RootStackParamList } from '../../../navigation/types';
import { useLocaleStore } from '../../../stores/locale-store'; import { useLocaleStore } from '../../../stores/locale-store';
import { useColors } from '../../../theme/colors'; import { useColors, useCvdColor } from '../../../theme/colors';
import { radius, spacing, typography, type ThemePalette } from '../../../theme/tokens'; import { radius, spacing, typography, type ThemePalette } from '../../../theme/tokens';
import { ArrowRight, CalendarCheck, CalendarDays, CalendarPlus, CalendarX, ChevronDown } from '../../icons'; import { ArrowRight, CalendarCheck, CalendarDays, CalendarPlus, CalendarX, ChevronDown } from '../../icons';
import { useBookingCalendar } from './use-booking-calendar'; import { useBookingCalendar } from './use-booking-calendar';
@@ -27,6 +27,7 @@ type Nav = NativeStackNavigationProp<RootStackParamList>;
*/ */
export function BookingCalendarActions({ booking, email }: { booking: StructuredCard; email: Pick<Email, 'from' | 'subject'> }) { export function BookingCalendarActions({ booking, email }: { booking: StructuredCard; email: Pick<Email, 'from' | 'subject'> }) {
const c = useColors(); const c = useColors();
const paint = useCvdColor();
const styles = React.useMemo(() => makeStyles(c), [c]); const styles = React.useMemo(() => makeStyles(c), [c]);
const t = useLocaleStore((s) => s.t); const t = useLocaleStore((s) => s.t);
const navigation = useNavigation<Nav>(); const navigation = useNavigation<Nav>();
@@ -144,7 +145,7 @@ export function BookingCalendarActions({ booking, email }: { booking: Structured
accessibilityLabel={cal.name} accessibilityLabel={cal.name}
testID="booking-calendar-choice" testID="booking-calendar-choice"
> >
<View style={[styles.swatch, { backgroundColor: getCalendarColor(cal) }]} /> <View style={[styles.swatch, { backgroundColor: paint(getCalendarColor(cal)) }]} />
<Text style={styles.pickerText} numberOfLines={1}>{cal.name}</Text> <Text style={styles.pickerText} numberOfLines={1}>{cal.name}</Text>
</Pressable> </Pressable>
))} ))}