From 6a1bec849dadd0cb82eb3f579225d0bb60c80780 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Mon, 17 Aug 2026 11:32:57 -0400 Subject: [PATCH] Route the interactive Panama date menu --- config/dot/quickshell/modules/bar/Clock.qml | 2 +- .../modules/datemenu/CalendarGrid.qml | 94 ++++++++++++++----- config/dot/quickshell/services/ShellState.qml | 19 ++++ config/dot/quickshell/shell.qml | 7 +- .../plans/2026-08-17-calendar-agenda.md | 12 +-- tests/quickshell/calendar-agenda-contract.sh | 10 ++ 6 files changed, 112 insertions(+), 32 deletions(-) diff --git a/config/dot/quickshell/modules/bar/Clock.qml b/config/dot/quickshell/modules/bar/Clock.qml index 488deeb..59a0fff 100644 --- a/config/dot/quickshell/modules/bar/Clock.qml +++ b/config/dot/quickshell/modules/bar/Clock.qml @@ -24,7 +24,7 @@ Pill { return date + time; } - onActivated: ShellState.toggle("notifications") + onActivated: ShellState.toggleDateMenu("agenda") Text { anchors.verticalCenter: parent.verticalCenter diff --git a/config/dot/quickshell/modules/datemenu/CalendarGrid.qml b/config/dot/quickshell/modules/datemenu/CalendarGrid.qml index dd61801..b9c4cad 100644 --- a/config/dot/quickshell/modules/datemenu/CalendarGrid.qml +++ b/config/dot/quickshell/modules/datemenu/CalendarGrid.qml @@ -4,17 +4,20 @@ // months and the panel below it never jumps. Days belonging to the neighbouring // months fill the edges, dimmed. Today wears the accent. // -// Deliberately non-interactive apart from the header: there is no calendar -// backend behind this, so a day cell that highlighted on hover would be -// promising a click that does nothing. +// Days become real navigation targets now that Evolution Data Server backs the +// panel. Up to three source-coloured markers show whether a date has events. import Quickshell import QtQuick import qs.config +import qs.services Item { id: root + signal dateActivated(date value) + signal visibleMonthChanged(int year, int month) + implicitHeight: body.implicitHeight // Cells divide the available width evenly; the height is fixed so the grid @@ -37,6 +40,7 @@ Item { // panel opens, so it never comes back showing wherever you paged off to. property int viewYear: root.todayYear property int viewMonth: root.todayMonth + property date selectedDate: CalendarAgenda.selectedDate readonly property bool onCurrentMonth: root.viewYear === root.todayYear && root.viewMonth === root.todayMonth @@ -53,30 +57,29 @@ Item { root.viewMonth = d.getMonth(); } + function activateCell(model: var): void { + if (!model.current) { + root.viewYear = model.date.getFullYear(); + root.viewMonth = model.date.getMonth(); + } + root.dateActivated(model.date); + } + + onViewYearChanged: root.visibleMonthChanged(root.viewYear, root.viewMonth) + onViewMonthChanged: root.visibleMonthChanged(root.viewYear, root.viewMonth) + readonly property var cells: { const first = new Date(root.viewYear, root.viewMonth, 1); const offset = first.getDay(); // 0 = Sunday, matching the header row - const inThisMonth = new Date(root.viewYear, root.viewMonth + 1, 0).getDate(); - const inPrevMonth = new Date(root.viewYear, root.viewMonth, 0).getDate(); const out = []; for (let i = 0; i < 42; i++) { - const n = i - offset + 1; - if (n < 1) - out.push({ - day: inPrevMonth + n, - current: false - }); - else if (n > inThisMonth) - out.push({ - day: n - inThisMonth, - current: false - }); - else - out.push({ - day: n, - current: true - }); + const date = new Date(root.viewYear, root.viewMonth, i - offset + 1); + out.push({ + date: date, + day: date.getDate(), + current: date.getMonth() === root.viewMonth && date.getFullYear() === root.viewYear + }); } return out; } @@ -198,6 +201,8 @@ Item { required property var modelData readonly property bool isToday: cell.modelData.current && cell.modelData.day === root.todayDay && root.onCurrentMonth + readonly property bool isSelected: CalendarAgenda.dateKey(cell.modelData.date) === CalendarAgenda.dateKey(root.selectedDate) + readonly property var markers: CalendarAgenda.markersForDate(cell.modelData.date).slice(0, 3) width: root.cellWidth height: root.cellHeight @@ -208,8 +213,18 @@ Item { height: root.cellHeight - 4 radius: width / 2 border.width: 0 - visible: cell.isToday - color: Theme.accent + visible: cell.isToday || cell.isSelected || cellMouse.containsMouse + color: { + if (cell.isToday) + return Theme.accent; + if (cell.isSelected) + return Theme.alpha(Theme.accent, 0.20); + return Theme.alpha(Theme.fg, 0.08); + } + + Behavior on color { + ColorAnimation { duration: Theme.durFast } + } } Text { @@ -223,11 +238,44 @@ Item { color: { if (cell.isToday) return Theme.bgDark; + if (cell.isSelected) + return Theme.accent; return cell.modelData.current ? Theme.fg : Theme.fgMuted; } } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 1 + spacing: 2 + + Repeater { + model: cell.markers + + Rectangle { + required property var modelData + + width: 3 + height: 3 + radius: 2 + border.width: 0 + color: cell.isToday ? Theme.bgDark : modelData.color + } + } + } + + MouseArea { + id: cellMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.activateCell(cell.modelData) + } } } } } + + Component.onCompleted: root.visibleMonthChanged(root.viewYear, root.viewMonth) } diff --git a/config/dot/quickshell/services/ShellState.qml b/config/dot/quickshell/services/ShellState.qml index f0fb240..ec96d55 100644 --- a/config/dot/quickshell/services/ShellState.qml +++ b/config/dot/quickshell/services/ShellState.qml @@ -35,6 +35,10 @@ Singleton { readonly property bool anyOverlayOpen: activeOverlay !== "" + // The date menu is one surface with two deliberate entry points. The clock + // opens the Daybook; Super+B and notification actions open message history. + property string dateMenuPage: "agenda" + // 0 means "use the currently focused workspace". A positive value lets a // contextual surface, such as Focus, open Mission Control at its target. property int overviewWorkspaceId: 0 @@ -48,6 +52,21 @@ Singleton { root.activeOverlay = name; } + function openDateMenu(page: string): void { + root.dateMenuPage = page === "notifications" ? "notifications" : "agenda"; + root.activeOverlay = "notifications"; + } + + function toggleDateMenu(page: string): void { + const target = page === "notifications" ? "notifications" : "agenda"; + if (root.activeOverlay === "notifications" && root.dateMenuPage === target) { + root.activeOverlay = ""; + return; + } + root.dateMenuPage = target; + root.activeOverlay = "notifications"; + } + function openOverview(workspaceId: int): void { root.overviewWorkspaceId = Math.max(0, workspaceId); root.overviewQuery = ""; diff --git a/config/dot/quickshell/shell.qml b/config/dot/quickshell/shell.qml index 42757db..8b870a9 100644 --- a/config/dot/quickshell/shell.qml +++ b/config/dot/quickshell/shell.qml @@ -253,7 +253,8 @@ ShellRoot { IpcHandler { target: "notifications" - function toggle(): void { ShellState.toggle("notifications"); } + function toggle(): void { ShellState.toggleDateMenu("notifications"); } + function open(): void { ShellState.openDateMenu("notifications"); } function clear(): void { Notifs.dismissAll(); } function dnd(): bool { Notifs.doNotDisturb = !Notifs.doNotDisturb; @@ -266,13 +267,15 @@ ShellRoot { function fixture(name: string): void { CalendarAgenda.applyFixture(name); } function reset(): void { CalendarAgenda.clearFixture(); } function refresh(): void { CalendarAgenda.refresh(); } - function open(): void { ShellState.open("notifications"); } + function open(): void { ShellState.openDateMenu("agenda"); } function close(): void { ShellState.close(); } function select(date: string): void { CalendarAgenda.selectIsoDate(date); } function status(): string { return JSON.stringify({ phase: CalendarAgenda.phase, fixture: CalendarAgenda.fixtureMode, + open: ShellState.notificationsOpen, + page: ShellState.dateMenuPage, sourceCount: CalendarAgenda.sources.length, eventCount: CalendarAgenda.events.length, selectedDate: CalendarAgenda.selectedDateKey, diff --git a/docs/superpowers/plans/2026-08-17-calendar-agenda.md b/docs/superpowers/plans/2026-08-17-calendar-agenda.md index 591cbc7..a83e566 100644 --- a/docs/superpowers/plans/2026-08-17-calendar-agenda.md +++ b/docs/superpowers/plans/2026-08-17-calendar-agenda.md @@ -417,7 +417,7 @@ git commit -m "Add live calendar state to Quickshell" - Produces: `ShellState.dateMenuPage`, `ShellState.toggleDateMenu(page)`, and `ShellState.openDateMenu(page)`. - Produces from CalendarGrid: signals `dateActivated(date)` and `visibleMonthChanged(year, month)`. -- [ ] **Step 1: Extend the failing contract with page-routing assertions** +- [x] **Step 1: Extend the failing contract with page-routing assertions** Append fixture assertions: @@ -433,11 +433,11 @@ jq -e '.open == true and .page == "notifications"' <<<"$(qs ipc call calendar-ag Update the calendar status response to include only `open: ShellState.notificationsOpen` and `page: ShellState.dateMenuPage` in addition to the non-sensitive fields from Task 2. -- [ ] **Step 2: Run the contract and verify RED** +- [x] **Step 2: Run the contract and verify RED** Expected: FAIL because `dateMenuPage` and notification `open()` do not exist. -- [ ] **Step 3: Implement explicit ShellState routing** +- [x] **Step 3: Implement explicit ShellState routing** Add: @@ -462,7 +462,7 @@ function toggleDateMenu(page: string): void { Change Clock activation to `ShellState.toggleDateMenu("agenda")`. Change notification IPC `toggle()` to `ShellState.toggleDateMenu("notifications")`, add `open()` using `openDateMenu("notifications")`, and leave `Super+B` routed through that IPC target. -- [ ] **Step 4: Make CalendarGrid date-aware and event-aware** +- [x] **Step 4: Make CalendarGrid date-aware and event-aware** Replace numeric-only cell objects with full local `Date` values and add: @@ -484,7 +484,7 @@ Every cell receives `markers: CalendarAgenda.markersForDate(cell.date).slice(0, Month changes call `visibleMonthChanged(viewYear, viewMonth)`. Adjacent-month date activation changes month and selection in one click. -- [ ] **Step 5: Verify routing and grid contracts** +- [x] **Step 5: Verify routing and grid contracts** Run: @@ -496,7 +496,7 @@ rg -q 'dateActivated' config/dot/quickshell/modules/datemenu/CalendarGrid.qml Expected: contract PASS and both interaction hooks present. -- [ ] **Step 6: Commit routing and grid behavior** +- [x] **Step 6: Commit routing and grid behavior** ```bash git add config/dot/quickshell/services/ShellState.qml \ diff --git a/tests/quickshell/calendar-agenda-contract.sh b/tests/quickshell/calendar-agenda-contract.sh index ceef10f..d46a9b4 100755 --- a/tests/quickshell/calendar-agenda-contract.sh +++ b/tests/quickshell/calendar-agenda-contract.sh @@ -30,6 +30,16 @@ jq -e '.selectedDate == "2026-08-18" and .selectedEventCount == 1' \ <<<"$(qs ipc call calendar-agenda status)" >/dev/null \ || fail 'date selection did not update events' +qs ipc call calendar-agenda open >/dev/null +jq -e '.open == true and .page == "agenda"' \ + <<<"$(qs ipc call calendar-agenda status)" >/dev/null \ + || fail 'calendar open did not route to Agenda' + +qs ipc call notifications open >/dev/null +jq -e '.open == true and .page == "notifications"' \ + <<<"$(qs ipc call calendar-agenda status)" >/dev/null \ + || fail 'notification open did not route to Notifications' + qs ipc call calendar-agenda reset >/dev/null jq -e '.fixture == false' <<<"$(qs ipc call calendar-agenda status)" >/dev/null \ || fail 'fixture reset did not restore live mode'