diff --git a/src/components/calendar/EventBlock.tsx b/src/components/calendar/EventBlock.tsx index 57cce7c5..b1422085 100644 --- a/src/components/calendar/EventBlock.tsx +++ b/src/components/calendar/EventBlock.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native'; -import type { EventBlockColors } from '../../lib/event-colors'; +import { TINTED_BAR_WIDTH, type EventBlockColors } from '../../lib/event-colors'; // Events in the week and day grids, shared by the paged WeekView and the // scrolling TimeGridScrollView. Every event is a solid block of its calendar @@ -80,9 +80,11 @@ export function TimedEventBlock({ styles.block, corners('vertical', continuesBefore, continuesAfter, EVENT_RADIUS), { backgroundColor: colors.fill, borderColor: ringColor }, + colors.bar ? styles.blockWithBar : null, style, ]} > + {colors.bar ? : null} {inactive && ( // The calendar-coloured outline sits inside the ring. + {colors.bar ? : null} {continuesBefore ? '… ' : ''} {title} @@ -164,6 +168,11 @@ const styles = StyleSheet.create({ overflow: 'hidden', }, outline: { ...StyleSheet.absoluteFillObject, borderWidth: 1 }, + // The title clears the light style's bar. + blockWithBar: { paddingStart: 3 + TINTED_BAR_WIDTH }, + barWithBar: { paddingStart: 4 + TINTED_BAR_WIDTH }, + // "Light event colors": the calendar colour along the start edge. + startBar: { position: 'absolute', top: 0, bottom: 0, start: 0, width: TINTED_BAR_WIDTH }, blockTitle: { fontSize: 12, lineHeight: 15, fontWeight: '500' }, blockTime: { fontSize: 10.5, lineHeight: 13, fontWeight: '400', opacity: 0.85 }, struck: { textDecorationLine: 'line-through' }, diff --git a/src/components/calendar/MonthView.tsx b/src/components/calendar/MonthView.tsx index f3f99655..3dc2f630 100644 --- a/src/components/calendar/MonthView.tsx +++ b/src/components/calendar/MonthView.tsx @@ -14,6 +14,7 @@ import { import type { Calendar, CalendarEvent } from '../../api/types'; import { componentSizes, spacing, typography, type ThemePalette } from '../../theme/tokens'; import { useColors } from '../../theme/colors'; +import { useSettingsStore } from '../../stores/settings-store'; import { buildEventDayIndex, eventsOnDayFromIndex, @@ -26,7 +27,7 @@ import { } from '../../lib/calendar-utils'; import { dayIndexIn, monthChipShowsTime, monthKeyOf, monthMask } from '../../lib/calendar-month-scroll'; import { isInactiveEvent } from '../../lib/calendar-participants'; -import { eventBlockColors } from '../../lib/event-colors'; +import { TINTED_BAR_WIDTH, eventBlockColors } from '../../lib/event-colors'; import { useCalendarLocale } from '../../lib/calendar-locale'; import { dayCellAccessibilityLabel } from '../../lib/calendar-a11y'; import { displayNow } from '../../lib/calendar-timezone'; @@ -156,6 +157,8 @@ function MonthWeekRowInner({ const c = useColors(); const { t } = useCalendarLocale(); const { width } = useWindowDimensions(); + // "Light event colors" (webmail calendarTintedEvents). + const tinted = useSettingsStore((s) => s.calendarTintedEvents); const chipTime = monthChipShowsTime(true, width / 7); if (variant === 'full') { return ( @@ -254,13 +257,14 @@ function MonthWeekRowInner({ // Solid bars in the calendar colour; declined and // cancelled events outlined (repos/branding/APP.md). const inactive = isInactiveEvent(event, currentUserEmails); - const colors = eventBlockColors(getEventColor(event, calendars), inactive, c); + const colors = eventBlockColors(getEventColor(event, calendars), inactive, c, tinted); return ( @@ -327,6 +331,8 @@ function FullWeekRow({ }) { const { width } = useWindowDimensions(); const { t } = useCalendarLocale(); + // "Light event colors" (webmail calendarTintedEvents). + const tinted = useSettingsStore((s) => s.calendarTintedEvents); const showTime = monthChipShowsTime(showTimeInMonthView, width / 7); return ( @@ -365,7 +371,7 @@ function FullWeekRow({ {dayEvents.slice(0, shown).map((event, idx) => { const inactive = isInactiveEvent(event, currentUserEmails); - const colors = eventBlockColors(getEventColor(event, calendars), inactive, c); + const colors = eventBlockColors(getEventColor(event, calendars), inactive, c, tinted); // No time on events that fill the whole day. const time = showTime && !event.showWithoutTime && !isTimedEventFullDayOnDate(event, d) ? `${format(getEventStartDate(event), timePattern(timeFormat), { locale })} ` @@ -376,6 +382,7 @@ function FullWeekRow({ style={[ styles.fullChip, { backgroundColor: colors.fill }, + colors.bar ? { borderStartWidth: TINTED_BAR_WIDTH, borderStartColor: colors.bar } : null, colors.border !== null && [styles.chipInactive, { borderColor: colors.border }], ]} > diff --git a/src/components/calendar/TimeGridScrollView.tsx b/src/components/calendar/TimeGridScrollView.tsx index 9ad9210d..65f7503e 100644 --- a/src/components/calendar/TimeGridScrollView.tsx +++ b/src/components/calendar/TimeGridScrollView.tsx @@ -19,6 +19,7 @@ import { differenceInCalendarDays, format, type Locale } from 'date-fns'; import type { Calendar, CalendarEvent } from '../../api/types'; import { spacing, typography, type ThemePalette } from '../../theme/tokens'; import { useColors } from '../../theme/colors'; +import { useSettingsStore } from '../../stores/settings-store'; import { useCalendarLocale } from '../../lib/calendar-locale'; import { eventAccessibilityLabel } from '../../lib/calendar-a11y'; import { @@ -120,6 +121,8 @@ function TimeGridScrollViewInner({ onCreateAtTime, }: TimeGridScrollViewProps) { const c = useColors(); + // "Light event colors" (webmail calendarTintedEvents). + const tinted = useSettingsStore((s) => s.calendarTintedEvents); const styles = React.useMemo(() => makeStyles(c), [c]); const { locale, t } = useCalendarLocale(); const index = React.useMemo( @@ -448,6 +451,7 @@ function TimeGridScrollViewInner({ getEventColor(segment.event, calendars), isInactiveEvent(segment.event, currentUserEmails), c, + tinted, )} continuesBefore={segment.continuesBefore} continuesAfter={segment.continuesAfter} @@ -606,6 +610,8 @@ const DayColumn = React.memo(function DayColumn({ draftHour: number; }) { const { locale, t } = useCalendarLocale(); + // "Light event colors" (webmail calendarTintedEvents). + const tinted = useSettingsStore((s) => s.calendarTintedEvents); const handleLongPress = React.useCallback( (e: GestureResponderEvent) => { onLongPressAt?.(day, hourAtOffset(e.nativeEvent.locationY, HOUR_HEIGHT)); @@ -639,6 +645,7 @@ const DayColumn = React.memo(function DayColumn({ getEventColor(event, calendars), isInactiveEvent(event, currentUserEmails), colors, + tinted, )} ringColor={colors.background} continuesBefore={continuesBefore} diff --git a/src/components/calendar/WeekView.tsx b/src/components/calendar/WeekView.tsx index b1327fd9..cfa39e0f 100644 --- a/src/components/calendar/WeekView.tsx +++ b/src/components/calendar/WeekView.tsx @@ -11,6 +11,7 @@ import { displayNow, displayNowMinutes, isDisplayToday } from '../../lib/calenda import type { Calendar, CalendarEvent } from '../../api/types'; import { spacing, typography, type ThemePalette } from '../../theme/tokens'; import { useColors } from '../../theme/colors'; +import { useSettingsStore } from '../../stores/settings-store'; import { useCalendarLocale } from '../../lib/calendar-locale'; import { eventAccessibilityLabel } from '../../lib/calendar-a11y'; import { @@ -66,6 +67,8 @@ function WeekViewInner({ currentUserEmails, }: WeekViewProps) { const c = useColors(); + // "Light event colors" (webmail calendarTintedEvents). + const tinted = useSettingsStore((s) => s.calendarTintedEvents); const styles = React.useMemo(() => makeStyles(c), [c]); const { locale, t } = useCalendarLocale(); const scrollRef = React.useRef(null); @@ -212,6 +215,7 @@ function WeekViewInner({ getEventColor(segment.event, calendars), isInactiveEvent(segment.event, currentUserEmails), c, + tinted, )} continuesBefore={segment.continuesBefore} continuesAfter={segment.continuesAfter} @@ -287,6 +291,7 @@ function WeekViewInner({ getEventColor(event, calendars), isInactiveEvent(event, currentUserEmails), c, + tinted, )} ringColor={c.background} continuesBefore={continuesBefore} diff --git a/src/components/settings/CalendarSettings.tsx b/src/components/settings/CalendarSettings.tsx index dab6f860..5d575c54 100644 --- a/src/components/settings/CalendarSettings.tsx +++ b/src/components/settings/CalendarSettings.tsx @@ -44,6 +44,7 @@ export function CalendarSettings() { const timeZone = useSettingsStore((s) => s.calendarTimeZone); const showTimeInMonth = useSettingsStore((s) => s.calendarShowTimeInMonth); const showWeekNumbers = useSettingsStore((s) => s.calendarShowWeekNumbers); + const tintedEvents = useSettingsStore((s) => s.calendarTintedEvents); const freeScroll = useSettingsStore((s) => s.calendarFreeScroll); const birthdayCal = useSettingsStore((s) => s.showBirthdayCalendar); const tasksEnabled = useSettingsStore((s) => s.enableCalendarTasks); @@ -135,6 +136,19 @@ export function CalendarSettings() { /> + + update('calendarTintedEvents', v)} + /> + + fs.readFileSync(path.resolve(__dirname, '../..', rel), 'utf8'); + +describe('light event colours', () => { + it('tints active events and adds the start bar', () => { + expect(eventBlockColors('#3b82f6', false, page, true)).toEqual({ + fill: mixColors('#3b82f6', '#ffffff', 0.2), + text: '#111111', + border: null, + bar: '#3b82f6', + }); + expect(mixColors('#3b82f6', '#ffffff', 0.2)).toBe('#d8e6fd'); + // Solid blocks stay the default. + expect(eventBlockColors('#3b82f6', false, page)).toMatchObject({ fill: '#3b82f6' }); + expect(eventBlockColors('#3b82f6', false, page).bar).toBeUndefined(); + }); + + it('keeps declined and cancelled events outlined', () => { + const colors = eventBlockColors('#3b82f6', true, page, true); + expect(colors.border).toBe('#3b82f6'); + expect(colors.fill).toBe('#ffffff'); + expect(colors.bar).toBeUndefined(); + }); + + it('is a setting, off by default, under the webmail key', () => { + expect(mergeWithDefaults({}).calendarTintedEvents).toBe(false); + expect(mergeWithDefaults({ calendarTintedEvents: true }).calendarTintedEvents).toBe(true); + }); + + // No RN renderer in the test env: check the wiring. + it('is offered in the calendar settings and read by every grid', () => { + expect(read('components/settings/CalendarSettings.tsx')).toMatch(/onChange=\{\(v\) => update\('calendarTintedEvents', v\)\}/); + for (const view of ['MonthView', 'TimeGridScrollView', 'WeekView']) { + const src = read(`components/calendar/${view}.tsx`); + expect(src).toMatch(/const tinted = useSettingsStore\(\(s\) => s\.calendarTintedEvents\);/); + expect(src).toMatch(/eventBlockColors\([\s\S]*?tinted,?\s*\)/); + } + expect(read('components/calendar/EventBlock.tsx').match(/colors\.bar \?