From 2a87dd62e6eeea18eac944a641d9ed9f2a0ee4a8 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 26 Oct 2026 12:47:36 -0500 Subject: [PATCH 1/4] feat(mobile): the circle opens the Calendar, with or without a note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dave (Discord, 2026-10-26) asked for a calendar on the phone like the desktop sidebar: the month on top, tasks by date below, and a tap on a date that opens or creates that day's note. The iPhone app has had it since its first build, but only from a note's ••• sheet and only with daily or weekly notes on, so he never found it. The circle's menu gets a Calendar row between More and Browse, listed always. It opens core's date calendar (ZenNotes/zennotes 95f48cde, `@zennotes/app-core/calendar`) as a full-screen sheet that needs no open note, with the Done pill every right-hand panel has. A tap selects a day and the week of tasks below follows it; a tap on the selected day opens its daily note, or offers to create it. Today is selected when it opens, so today's note is one tap away. Once a day or a task opens the sheet closes, so the note is what shows, even when it was the note already open. The ••• row opens the same sheet now. An iPad keeps the panel beside the note, the only place its ••• row offers it. With daily and weekly notes both off there is nothing for the calendar to open, so instead of disappearing the row explains that and offers "Turn on daily notes…", which opens Settings on Vault, Periodic notes. The Tasks calendar's Week mode now leaves the date calendar's grid alone: opened over Tasks in Week mode, the sheet would have lost all but one week of days. This needs core 2.77.0 (the calendar subpath, DateCalendar's onOpen, showSettingsPage) and does not build on the vendored 2.76.0; the core adoption is a separate commit. Checked in the release web build at 375x812, dark and light, with touch presses, on a rehearsal core built from 95f48cde: daily notes off, the switch, select, create, open an existing day, the same note already open, the ••• row, and Done. The Android shell carries the same change and was checked with real taps on an emulator. A layout contract test pins the sheet's full-screen and day-cell rules; 290 tests pass. --- src/ui-mobile/MobileShell.tsx | 95 ++++++++++++++++++++++++++--- src/ui-mobile/mobile-layout.test.ts | 7 +++ src/ui-mobile/mobile.css | 10 ++- 3 files changed, 100 insertions(+), 12 deletions(-) diff --git a/src/ui-mobile/MobileShell.tsx b/src/ui-mobile/MobileShell.tsx index 5011489..7b382b9 100644 --- a/src/ui-mobile/MobileShell.tsx +++ b/src/ui-mobile/MobileShell.tsx @@ -6,7 +6,7 @@ * full-screen flow. Rendered into its own React root so app-core stays * untouched; state is driven through the shared Zustand store. */ -import React, { useEffect, useMemo, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import ReactDOM from 'react-dom/client' import { Haptics, ImpactStyle } from '@capacitor/haptics' import { Keyboard } from '@capacitor/keyboard' @@ -15,7 +15,9 @@ import { getShellSnapshot, useShellSnapshot, subscribeShell, getAdjacentNotePath import { getBrowseSnapshot, requestCreateBrowseFolder, requestDeleteBrowseDirectory } from '@zennotes/app-core/browse' import { getWorkspaceSnapshot, useWorkspaceSnapshot, subscribeWorkspace, readPersistedHomeState, configureWorkspacePresentation, pickLocalVault, openLocalVault, connectRemoteProfile, refreshRemoteProfiles } from '@zennotes/app-core/workspace' -import { getSettingsSnapshot, useSettingsSnapshot, subscribeSettings, setSettingsVisible, setEditorFontSize } from '@zennotes/app-core/settings' +import { getSettingsSnapshot, useSettingsSnapshot, subscribeSettings, setSettingsVisible, setEditorFontSize, + showSettingsPage } from '@zennotes/app-core/settings' +import { DateCalendar } from '@zennotes/app-core/calendar' import { useEditorPresentation, setEditorMode, hasEditorSelection, runEditorCommand } from '@zennotes/app-core/editor' import { getTasksSnapshot, moveTaskToColumn, type KanbanGroupBy } from '@zennotes/app-core/tasks' import { runAppCommand, showCommandPalette, showSearch, showTemplates, showOutline } from '@zennotes/app-core/commands' @@ -251,10 +253,7 @@ function ActionSheet({ onClose }: { onClose: () => void }): React.JSX.Element { className="zn-mobile-sheet-row" onClick={() => { onClose() - window.setTimeout( - () => window.dispatchEvent(new Event('zen:toggle-calendar')), - 30 - ) + window.setTimeout(showCalendar, 30) }} > @@ -401,6 +400,71 @@ function CreateSheet({ onClose }: { onClose: () => void }): React.JSX.Element { ) } +const SHOW_CALENDAR_EVENT = 'zn:show-calendar' + +/** + * Open the Calendar. A phone has no room beside a note, and Home has no note + * to sit beside, so there it is a sheet of its own (CalendarSheet). An iPad + * keeps the desktop's panel beside the open note, which is the only place + * its ••• row offers it. + */ +function showCalendar(): void { + if (isPhoneWidth()) window.dispatchEvent(new Event(SHOW_CALENDAR_EVENT)) + else window.dispatchEvent(new Event('zen:toggle-calendar')) +} + +/** + * The Calendar (Discord, 2026-10-26: "I don't see anywhere to bring the + * calendar up"). The only way in used to be a note's ••• sheet, and only + * with daily or weekly notes on, so nobody on Home ever met it. This is + * app-core's date calendar (month grid with writing dots and task marks, + * the selected day's week of tasks below) as a sheet that needs no open + * note, opened from the circle's Calendar row and from •••. A tap selects a + * day and a tap on the selected day opens its daily note or offers to + * create it (core); once a day or a task opens, the sheet closes so the note + * is what shows. Done reaches it as a right-hand panel. + * + * With daily and weekly notes both off there is nothing for it to open, so + * the row says how to turn daily notes on instead of disappearing. + */ +function CalendarSheet({ onClose }: { onClose: () => void }): React.JSX.Element { + const { calendarAvailable } = useSettingsSnapshot() + useEffect(() => { + window.addEventListener('zen:close-right-panel', onClose) + return () => window.removeEventListener('zen:close-right-panel', onClose) + }, [onClose]) + + if (calendarAvailable) { + return ( +
+ +
+ ) + } + const turnOn = (): void => { + onClose() + window.setTimeout(() => showSettingsPage('periodic-notes'), 30) + } + return ( + <> +
+
+
Calendar
+

+ The calendar opens your daily notes: tap a day for its note, with that week's tasks + below. Daily notes are off in this vault. +

+
+ +
+
+ + ) +} + function MobileNav(): React.JSX.Element | null { const vault = useShellSnapshot().vault const { hasOpenNote, mode } = useEditorPresentation() @@ -410,6 +474,8 @@ function MobileNav(): React.JSX.Element | null { const [sheetOpen, setSheetOpen] = useState(false) const [createOpen, setCreateOpen] = useState(false) const [fabOpen, setFabOpen] = useState(false) + const [calendarOpen, setCalendarOpen] = useState(false) + const closeCalendar = useCallback(() => setCalendarOpen(false), []) // First-run coach mark: the unlabeled ensō reads as a loading spinner to // new users (real-user tested). Set by onboarding, cleared on first tap. const [fabHint, setFabHint] = useState( @@ -417,11 +483,17 @@ function MobileNav(): React.JSX.Element | null { ) // The pull-down quick action (usePullDownAction) can be set to "New - // note"; the sheet is this component's, so it is summoned by event. + // note"; the sheet is this component's, so it is summoned by event. So is + // the Calendar, which the ••• sheet opens too. useEffect(() => { const onCreate = (): void => setCreateOpen(true) + const onCalendar = (): void => setCalendarOpen(true) window.addEventListener('zn:quick-create', onCreate) - return () => window.removeEventListener('zn:quick-create', onCreate) + window.addEventListener(SHOW_CALENDAR_EVENT, onCalendar) + return () => { + window.removeEventListener('zn:quick-create', onCreate) + window.removeEventListener(SHOW_CALENDAR_EVENT, onCalendar) + } }, []) if (!vault) return null @@ -466,6 +538,8 @@ function MobileNav(): React.JSX.Element | null { ] : []), { label: 'More', icon: ICONS.more, run: () => setSheetOpen(true) }, + // Always listed, daily notes on or off: off, it says how to turn them on. + { label: 'Calendar', icon: ICONS.calendar, run: showCalendar }, { label: 'Browse', icon: ICONS.sidebar, run: () => setDrawerOpen(true) }, { label: 'Search', icon: ICONS.search, run: showSearch } ] @@ -544,6 +618,7 @@ function MobileNav(): React.JSX.Element | null { {sheetOpen && setSheetOpen(false)} />} {createOpen && setCreateOpen(false)} />} + {calendarOpen && } ) } @@ -1369,9 +1444,11 @@ function useCalendarWeekMode(): void { localStorage.getItem(CAL_SCOPE_KEY) === 'month' ? 'month' : 'week' const apply = (): void => { + // The date calendar (a note's panel, or the Calendar sheet over Tasks) + // marks its days the same way; its grid keeps the whole month. const cells = Array.from( document.querySelectorAll('button[data-cal-day]') - ) + ).filter((cell) => !cell.closest('[data-calendar-panel]')) const todayBtn = document.querySelector("button[title='Today (gt)']") if (cells.length === 0 || !todayBtn) { control?.remove() diff --git a/src/ui-mobile/mobile-layout.test.ts b/src/ui-mobile/mobile-layout.test.ts index 6705dea..17eeb2c 100644 --- a/src/ui-mobile/mobile-layout.test.ts +++ b/src/ui-mobile/mobile-layout.test.ts @@ -83,3 +83,10 @@ test('the date calendar hides its key help on phones and keeps Today on the righ assert.equal(declaration(hint, 'display'), 'none') assert.equal(declaration(`${hint} + button`, 'margin-left'), 'auto') }) + +test("the Calendar sheet is the date calendar's full-screen phone sheet, with finger-sized days", () => { + const panel = '.zn-mobile.zn-phone .zn-calendar-sheet > [data-calendar-panel]' + assert.equal(declaration(panel, 'position'), 'fixed') + assert.ok(declaration(panel, 'width')?.includes('100vw')) + assert.equal(declaration('.zn-mobile.zn-phone .zn-calendar-sheet button[data-cal-day]', 'min-height'), '42px') +}) diff --git a/src/ui-mobile/mobile.css b/src/ui-mobile/mobile.css index da3a327..1909f1b 100644 --- a/src/ui-mobile/mobile.css +++ b/src/ui-mobile/mobile.css @@ -1222,12 +1222,15 @@ /* Right-hand side panels (calendar/outline/connections/comments/reference) would split a 390pt screen in half — they become full-screen sheets, with a shell-injected close button (zen:close-right-panel). Panel roots vary: -
,
(calendar), and