From cf4d02b49bc9c4ac7b6c58fa48e64bd25f13e85c Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:33:34 +0100 Subject: [PATCH] feat: draw calendar, tag and folder colours in a colour-blind-safe palette when that setting is on --- .../calendar/__tests__/event-card.test.tsx | 16 +++ components/calendar/calendar-agenda-view.tsx | 4 +- components/calendar/calendar-day-view.tsx | 4 +- components/calendar/calendar-month-view.tsx | 10 +- components/calendar/calendar-search.tsx | 4 +- .../calendar/calendar-sidebar-panel.tsx | 4 +- components/calendar/calendar-toolbar.tsx | 6 +- components/calendar/calendar-week-view.tsx | 6 +- components/calendar/event-card.tsx | 4 +- components/calendar/event-detail-popover.tsx | 4 +- components/calendar/event-modal.tsx | 4 +- components/calendar/quick-create-popover.tsx | 4 +- components/calendar/task-chip.tsx | 4 +- components/calendar/task-list-view.tsx | 4 +- components/layout/mv-side-panel.tsx | 4 +- components/layout/sidebar.tsx | 21 +-- hooks/__tests__/use-keyword-format.test.tsx | 15 ++ hooks/use-cvd-color.ts | 13 ++ hooks/use-keyword-format.ts | 16 ++- lib/__tests__/cvd-palette.test.ts | 51 +++++++ lib/cvd-palette.ts | 129 ++++++++++++++++++ 21 files changed, 295 insertions(+), 32 deletions(-) create mode 100644 hooks/use-cvd-color.ts create mode 100644 lib/__tests__/cvd-palette.test.ts create mode 100644 lib/cvd-palette.ts diff --git a/components/calendar/__tests__/event-card.test.tsx b/components/calendar/__tests__/event-card.test.tsx index 2d76501b7..e9e7b2616 100644 --- a/components/calendar/__tests__/event-card.test.tsx +++ b/components/calendar/__tests__/event-card.test.tsx @@ -112,3 +112,19 @@ describe('EventCard fill', () => { }); }); }); + +describe('EventCard colour settings', () => { + const calendar = { id: 'cal-1', name: 'Work', color: '#22c55e' } as unknown as Calendar; + afterEach(() => { + cleanup(); + useSettingsStore.setState({ showColorNames: false, colorBlindPalette: false }); + }); + + it("draws the event in the colour-blind-safe colour and keeps the calendar's own", () => { + useSettingsStore.setState({ colorBlindPalette: true }); + render(); + // #009E73, the palette's bluish green + expect(screen.getByRole('button').style.backgroundColor).toBe('rgb(0, 158, 115)'); + expect(calendar.color).toBe('#22c55e'); + }); +}); diff --git a/components/calendar/calendar-agenda-view.tsx b/components/calendar/calendar-agenda-view.tsx index e453d8bac..2300525f9 100644 --- a/components/calendar/calendar-agenda-view.tsx +++ b/components/calendar/calendar-agenda-view.tsx @@ -15,6 +15,7 @@ import { getParticipantCount, isDeclinedByUser } from "@/lib/calendar-participan import { useScrollWindow } from "@/hooks/use-scroll-window"; import type { ScrollWindowViewProps } from "@/lib/calendar-scroll-window"; import type { CalendarEvent, Calendar } from "@/lib/jmap/types"; +import { useCvdColor } from "@/hooks/use-cvd-color"; interface CalendarAgendaViewProps extends ScrollWindowViewProps { events: CalendarEvent[]; @@ -55,6 +56,7 @@ export function CalendarAgendaView({ onCreateEvent, }: CalendarAgendaViewProps) { const t = useTranslations("calendar"); + const paint = useCvdColor(); // Grid days / event dates are display dates (local fields = wall-clock in // the user's zone); the app-wide formatter would shift them again (#755). const intlFormatter = useDisplayDateFormatter(); @@ -240,7 +242,7 @@ export function CalendarAgendaView({ {group.events.map((ev) => { const calId = getPrimaryCalendarId(ev); const calendar = calId ? calendarMap.get(calId) : undefined; - const color = getEventColor(ev, calendar); + const color = paint(getEventColor(ev, calendar)); const start = getEventStartDate(ev); const end = getEventEndDate(ev); // iTIP CANCEL marks the attendee's copy with status "cancelled" diff --git a/components/calendar/calendar-day-view.tsx b/components/calendar/calendar-day-view.tsx index feb5f531c..a1d64f7d0 100644 --- a/components/calendar/calendar-day-view.tsx +++ b/components/calendar/calendar-day-view.tsx @@ -23,6 +23,7 @@ import { dayKey, type ScrollWindowViewProps } from "@/lib/calendar-scroll-window import type { PendingEventPreview } from "./event-modal"; import { useCalendarErrorMessage } from "@/hooks/use-calendar-error-message"; import { PluginDayLabel } from "./plugin-day-label"; +import { useCvdColor } from "@/hooks/use-cvd-color"; interface CalendarDayViewProps extends ScrollWindowViewProps { selectedDate: Date; @@ -93,6 +94,7 @@ export function CalendarDayView({ revealAt, }: CalendarDayViewProps) { const t = useTranslations("calendar"); + const paint = useCvdColor(); const tintedEvents = useSettingsStore((s) => s.calendarTintedEvents); const describeError = useCalendarErrorMessage(); // Grid days / event dates are display dates (local fields = wall-clock in @@ -649,7 +651,7 @@ export function CalendarDayView({ if (startMin + durationMin <= hours.startMinutes || startMin >= hours.endMinutes) return null; const clip = clipToDisplayHours(startMin, startMin + durationMin, hours); const cal = calendars.find(c => c.id === pendingPreview.calendarId); - const color = cal?.color || "#3b82f6"; + const color = paint(cal?.color || "#3b82f6"); // Drawn like a saved event, so the new event shows where // it will land while it is being created. return ( diff --git a/components/calendar/calendar-month-view.tsx b/components/calendar/calendar-month-view.tsx index a29f88daf..e0da8549c 100644 --- a/components/calendar/calendar-month-view.tsx +++ b/components/calendar/calendar-month-view.tsx @@ -23,6 +23,7 @@ import { useCalendarErrorMessage } from "@/hooks/use-calendar-error-message"; import { PluginDayLabel } from "./plugin-day-label"; import { useDisplayDateFormatter } from "@/hooks/use-display-date-formatter"; import type { MoveEventHandler } from "@/hooks/use-time-grid-interactions"; +import { useCvdColor } from "@/hooks/use-cvd-color"; interface CalendarMonthViewProps extends ScrollWindowViewProps { selectedDate: Date; @@ -94,6 +95,7 @@ export function CalendarMonthView({ onMoveEvent, }: CalendarMonthViewProps) { const t = useTranslations("calendar"); + const paint = useCvdColor(); const describeError = useCalendarErrorMessage(); const showTimeInMonthView = useSettingsStore((state) => state.showTimeInMonthView); // On mobile the month view collapses events to dots unless the user opted @@ -469,7 +471,7 @@ export function CalendarMonthView({ {dayEvents.slice(0, 3).map((ev) => { const calId = getPrimaryCalendarId(ev); const cal = calId ? calendarMap.get(calId) : undefined; - const evColor = ev.color || cal?.color || "#3b82f6"; + const evColor = paint(ev.color || cal?.color || "#3b82f6"); const inactive = ev.status === "cancelled" || isDeclinedByUser(ev, currentUserEmails); return ( ); })} @@ -497,7 +499,7 @@ export function CalendarMonthView({ {pendingPreview && checkIsSameDay(pendingPreview.start, day) && ( )} @@ -512,7 +514,7 @@ export function CalendarMonthView({ if (previewDayIdx === -1) return null; const previewRow = Math.max(rowCount, dayEventRows[previewDayIdx] + dayTasks[previewDayIdx].length); const cal = calendarMap.get(pendingPreview.calendarId); - const color = cal?.color || "#3b82f6"; + const color = paint(cal?.color || "#3b82f6"); return (
(null); const calendarMap = useMemo(() => new Map(calendars.map((c) => [c.id, c])), [calendars]); @@ -108,7 +110,7 @@ export function CalendarSearchResults({ const renderRow = (ev: CalendarEvent) => { const calId = getPrimaryCalendarId(ev); const calendar = calId ? calendarMap.get(calId) : undefined; - const color = getEventColor(ev, calendar); + const color = paint(getEventColor(ev, calendar)); const start = getEventStartDate(ev); const end = getEventEndDate(ev); const isInactive = ev.status === "cancelled" || isDeclinedByUser(ev, currentUserEmails); diff --git a/components/calendar/calendar-sidebar-panel.tsx b/components/calendar/calendar-sidebar-panel.tsx index 8f3ba4343..9ebc0c85d 100644 --- a/components/calendar/calendar-sidebar-panel.tsx +++ b/components/calendar/calendar-sidebar-panel.tsx @@ -23,6 +23,7 @@ import type { IJMAPClient } from '@/lib/jmap/client-interface'; import { displayNow } from "@/lib/timezone"; import { readableTextOn } from "@/lib/color-transform"; import { shareRecipientNames, useShareRecipientLabels } from "@/hooks/use-share-recipient-labels"; +import { useCvdColor } from "@/hooks/use-cvd-color"; /** * Split a per-account calendar list into "owned" (the user's own) and @@ -92,6 +93,7 @@ export function CalendarSidebarPanel({ multiAccountMode, }: CalendarSidebarPanelProps) { const t = useTranslations("calendar"); + const paint = useCvdColor(); const tSub = useTranslations("calendar.subscription"); const tMgmt = useTranslations("calendar.management"); const isSubscriptionCalendar = useCalendarStore((s) => s.isSubscriptionCalendar); @@ -277,7 +279,7 @@ export function CalendarSidebarPanel({ const renderCalendarItem = (cal: Calendar) => { const isVisible = selectedCalendarIds.includes(cal.id); - const color = cal.color || "#3b82f6"; + const color = paint(cal.color || "#3b82f6"); const hasMenu = isSubscriptionCalendar(cal.id) ? !!client : true; return ( diff --git a/components/calendar/calendar-toolbar.tsx b/components/calendar/calendar-toolbar.tsx index 7763420d6..69a6ef7ee 100644 --- a/components/calendar/calendar-toolbar.tsx +++ b/components/calendar/calendar-toolbar.tsx @@ -15,6 +15,7 @@ import type { CalendarViewMode } from "@/stores/calendar-store"; import type { Calendar } from "@/lib/jmap/types"; import { useCalendarLocale } from "@/hooks/use-calendar-locale"; import { displayNow } from "@/lib/timezone"; +import { useCvdColor } from "@/hooks/use-cvd-color"; interface CalendarToolbarProps { selectedDate: Date; @@ -102,6 +103,7 @@ export function CalendarToolbar({ onCloseSearch, }: CalendarToolbarProps) { const t = useTranslations("calendar"); + const paint = useCvdColor(); const tCommon = useTranslations("common"); const { weekStartsOn, @@ -316,7 +318,7 @@ export function CalendarToolbar({
{calendars.filter(c => !c.isShared).map((cal) => { const isVisible = selectedCalendarIds.includes(cal.id); - const color = cal.color || "#3b82f6"; + const color = paint(cal.color || "#3b82f6"); return (