From 2288d28e460294ebe824102a2bf2da031cd55238 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:05:31 +0200 Subject: [PATCH] feat: show five or seven days in the multi-day calendar view on wider screens --- locales/rn/de.json | 1 + locales/rn/en.json | 1 + .../calendar/TimeGridScrollView.tsx | 5 ++- src/lib/__tests__/calendar-time-grid.test.ts | 17 ++++++++++ src/lib/calendar-time-grid.ts | 13 +++++-- src/screens/CalendarScreen.tsx | 34 +++++++++++++------ 6 files changed, 57 insertions(+), 14 deletions(-) diff --git a/locales/rn/de.json b/locales/rn/de.json index 2032f8d8..3e9674e2 100644 --- a/locales/rn/de.json +++ b/locales/rn/de.json @@ -134,6 +134,7 @@ "update_error": "Aufgabe konnte nicht aktualisiert werden" }, "views": { + "n_days": "{count} Tage", "schedule": "Terminübersicht", "three_days": "3 Tage" } diff --git a/locales/rn/en.json b/locales/rn/en.json index f0a0113b..00f6f632 100644 --- a/locales/rn/en.json +++ b/locales/rn/en.json @@ -137,6 +137,7 @@ "update_error": "Failed to update task" }, "views": { + "n_days": "{count} days", "schedule": "Schedule", "three_days": "3 days" } diff --git a/src/components/calendar/TimeGridScrollView.tsx b/src/components/calendar/TimeGridScrollView.tsx index fdb658a4..c553c2d6 100644 --- a/src/components/calendar/TimeGridScrollView.tsx +++ b/src/components/calendar/TimeGridScrollView.tsx @@ -61,6 +61,8 @@ type TimeFormat = '12h' | '24h'; interface TimeGridScrollViewProps { mode: TimeGridMode; + /** Days of the multi-day mode (`multiDayCount`). */ + multiDays?: number; /** The day the user navigated to; its week (week view) or itself (day view) is scrolled into view. */ focus: CalendarFocus; /** The loaded window: one column per day. */ @@ -96,6 +98,7 @@ interface TimeGridScrollViewProps { */ function TimeGridScrollViewInner({ mode, + multiDays = 3, focus, window, selectedDate, @@ -120,7 +123,7 @@ function TimeGridScrollViewInner({ [eventsByDay, events], ); - const perScreen = columnsPerScreen(mode); + const perScreen = columnsPerScreen(mode, multiDays); const { width: screenWidth } = useWindowDimensions(); const [rootWidth, setRootWidth] = React.useState(screenWidth); const listWidth = Math.max(perScreen, rootWidth - GUTTER_WIDTH); diff --git a/src/lib/__tests__/calendar-time-grid.test.ts b/src/lib/__tests__/calendar-time-grid.test.ts index dbac0ea0..97bf8ca1 100644 --- a/src/lib/__tests__/calendar-time-grid.test.ts +++ b/src/lib/__tests__/calendar-time-grid.test.ts @@ -8,8 +8,10 @@ import { } from '../calendar-scroll-window'; import { buildAllDaySegments, + columnsPerScreen, headerColumnRange, hourAtOffset, + multiDayCount, timeGridFocusColumn, windowDays, } from '../calendar-time-grid'; @@ -118,3 +120,18 @@ describe('hourAtOffset', () => { expect(hourAtOffset(-4, 48)).toBe(0); }); }); + +describe('multiDayCount', () => { + it('shows three days on a phone, five on a medium window and seven on a wide one', () => { + expect(multiDayCount(411)).toBe(3); + expect(multiDayCount(800)).toBe(5); + expect(multiDayCount(1280)).toBe(7); + }); + + it('only changes the multi-day view', () => { + expect(columnsPerScreen('3day', 7)).toBe(7); + expect(columnsPerScreen('week', 5)).toBe(7); + expect(columnsPerScreen('day', 7)).toBe(1); + expect(columnsPerScreen('3day')).toBe(3); + }); +}); diff --git a/src/lib/calendar-time-grid.ts b/src/lib/calendar-time-grid.ts index 3fff4fb1..09826f77 100644 --- a/src/lib/calendar-time-grid.ts +++ b/src/lib/calendar-time-grid.ts @@ -18,9 +18,18 @@ import type { DayRange, WeekStartsOn } from './calendar-scroll-window'; export type TimeGridMode = 'week' | '3day' | 'day'; +/** + * Days the multi-day view ("3 days" on a phone) shows for a window `width` + * wide: three on a phone, five on a medium window, a full seven-day strip + * on a wide one, so the extra width buys days rather than wider columns. + */ +export function multiDayCount(width: number): number { + return width >= 840 ? 7 : width >= 600 ? 5 : 3; +} + /** Day columns on one screen. */ -export function columnsPerScreen(mode: TimeGridMode): number { - return mode === 'week' ? 7 : mode === '3day' ? 3 : 1; +export function columnsPerScreen(mode: TimeGridMode, multiDays = 3): number { + return mode === 'week' ? 7 : mode === '3day' ? multiDays : 1; } /** Every day of the window, in order. */ diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index 02155bae..52f53bf9 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -8,8 +8,10 @@ import { StyleSheet, Pressable, ActivityIndicator, + useWindowDimensions, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { multiDayCount } from '../lib/calendar-time-grid'; import { Calendar as CalendarIcon, ChevronDown, @@ -110,10 +112,10 @@ function windowMode(mode: ViewMode): ScrollViewMode { return mode === '3day' ? 'day' : mode; } -// A window without free scrolling: one period, three days for the three-day grid. -function fixedWindowFor(mode: ViewMode, anchor: Date): ScrollWindowState { +// A window without free scrolling: one period, the multi-day grid's days. +function fixedWindowFor(mode: ViewMode, anchor: Date, multiDays: number): ScrollWindowState { const state = fixedScrollWindowState(windowMode(mode), anchor); - return mode === '3day' ? { ...state, after: 2 } : state; + return mode === '3day' ? { ...state, after: multiDays - 1 } : state; } type PendingAction = | { @@ -189,6 +191,9 @@ export default function CalendarScreen() { ); // The sideways-scrolling week and day grids assume a left-to-right strip; // right-to-left layouts keep them paged. + // Wider windows show more days in the multi-day view. + const { width: windowWidth } = useWindowDimensions(); + const multiDays = multiDayCount(windowWidth); const freeScroll = calendarFreeScroll && ((viewMode !== 'week' && viewMode !== '3day' && viewMode !== 'day') || !I18nManager.isRTL); @@ -197,8 +202,8 @@ export default function CalendarScreen() { () => freeScroll ? normalizeScrollWindowState(windowState, windowMode(viewMode), parseDayKey(focusKey)) - : fixedWindowFor(viewMode, parseDayKey(focusKey)), - [freeScroll, windowState, viewMode, focusKey], + : fixedWindowFor(viewMode, parseDayKey(focusKey), multiDays), + [freeScroll, windowState, viewMode, focusKey, multiDays], ); React.useEffect(() => { if (freeScroll && activeWindowState !== windowState) setWindowState(activeWindowState); @@ -482,21 +487,21 @@ export default function CalendarScreen() { const base = visibleDate ?? focus.date; jumpTo( viewMode === 'week' ? subWeeks(base, 1) - : viewMode === '3day' ? subDays(base, 3) + : viewMode === '3day' ? subDays(base, multiDays) : viewMode === 'day' ? subDays(base, 1) : subMonths(base, 1), ); - }, [viewMode, visibleDate, focus.date, jumpTo]); + }, [viewMode, visibleDate, focus.date, jumpTo, multiDays]); const goNext = React.useCallback(() => { const base = visibleDate ?? focus.date; jumpTo( viewMode === 'week' ? addWeeks(base, 1) - : viewMode === '3day' ? addDays(base, 3) + : viewMode === '3day' ? addDays(base, multiDays) : viewMode === 'day' ? addDays(base, 1) : addMonths(base, 1), ); - }, [viewMode, visibleDate, focus.date, jumpTo]); + }, [viewMode, visibleDate, focus.date, jumpTo, multiDays]); const goToday = React.useCallback(() => { jumpTo(displayNow()); @@ -520,11 +525,17 @@ export default function CalendarScreen() { () => [ { key: 'agenda', label: t('calendar.views.schedule', 'Schedule'), Icon: Rows2 }, { key: 'day', label: t('calendar.views.day', 'Day'), Icon: Square }, - { key: '3day', label: t('calendar.views.three_days', '3 days'), Icon: Columns3 }, + { + key: '3day', + label: multiDays === 3 + ? t('calendar.views.three_days', '3 days') + : t('calendar.views.n_days', '{count} days', { count: multiDays }), + Icon: Columns3, + }, { key: 'week', label: t('calendar.views.week', 'Week'), Icon: Columns4 }, { key: 'month', label: t('calendar.views.month', 'Month'), Icon: Grid3x3 }, ], - [t], + [t, multiDays], ); // The month that drops down under the title; the title follows the month @@ -1094,6 +1105,7 @@ export default function CalendarScreen() {