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 \?