diff --git a/config/dot/quickshell/modules/datemenu/AgendaEvent.qml b/config/dot/quickshell/modules/datemenu/AgendaEvent.qml new file mode 100644 index 0000000..cca0d69 --- /dev/null +++ b/config/dot/quickshell/modules/datemenu/AgendaEvent.qml @@ -0,0 +1,128 @@ +import QtQuick +import qs.config +import qs.services + +Item { + id: root + + required property var eventData + property bool past: false + + signal activated + signal joinRequested + + readonly property var sourceData: CalendarAgenda.sourceForId(eventData.sourceId) + readonly property color eventColor: sourceData?.color ?? Theme.accent + readonly property string timeText: eventData.allDay + ? "All day" + : Qt.formatTime(new Date(Number(eventData.start) * 1000), Settings.use24Hour ? "HH:mm" : "h:mm AP") + readonly property string detailText: { + const source = root.sourceData?.name ?? "Calendar"; + const location = String(root.eventData.location ?? "").trim(); + return location === "" ? source : source + " · " + location; + } + + implicitHeight: 58 + opacity: past ? 0.52 : 1 + + Rectangle { + anchors.fill: parent + radius: 9 + border.width: 0 + color: rowMouse.containsMouse ? Theme.alpha(Theme.fg, 0.07) : "transparent" + + Behavior on color { ColorAnimation { duration: Theme.durFast } } + } + + MouseArea { + id: rowMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.activated() + } + + Text { + id: timeLabel + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: 56 + text: root.timeText + color: Theme.fgDim + horizontalAlignment: Text.AlignRight + font.family: Theme.fontFamily + font.features: Theme.tabularFigures + font.pixelSize: Theme.fontSizeSmall + } + + Rectangle { + id: sourceRail + anchors.left: timeLabel.right + anchors.leftMargin: 10 + anchors.verticalCenter: parent.verticalCenter + width: 3 + height: 34 + radius: 2 + border.width: 0 + color: root.eventColor + } + + Column { + anchors.left: sourceRail.right + anchors.leftMargin: 10 + anchors.right: joinPill.visible ? joinPill.left : parent.right + anchors.rightMargin: joinPill.visible ? 10 : 4 + anchors.verticalCenter: parent.verticalCenter + spacing: 3 + + Text { + width: parent.width + text: root.eventData.summary || "Untitled event" + color: Theme.fg + elide: Text.ElideRight + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + font.weight: Font.DemiBold + } + + Text { + width: parent.width + text: root.detailText + color: Theme.fgDim + elide: Text.ElideRight + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + } + } + + Rectangle { + id: joinPill + anchors.right: parent.right + anchors.rightMargin: 4 + anchors.verticalCenter: parent.verticalCenter + visible: String(root.eventData.joinUrl ?? "").startsWith("https://") + width: joinLabel.implicitWidth + 18 + height: 26 + radius: Theme.pillRadius + border.width: 0 + color: joinMouse.containsMouse ? Theme.alpha(Theme.accent, 0.28) : Theme.alpha(Theme.accent, 0.14) + + Text { + id: joinLabel + anchors.centerIn: parent + text: "Join" + color: Theme.accent + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.DemiBold + } + + MouseArea { + id: joinMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.joinRequested() + } + } +} diff --git a/config/dot/quickshell/modules/datemenu/AgendaPane.qml b/config/dot/quickshell/modules/datemenu/AgendaPane.qml new file mode 100644 index 0000000..301dbb5 --- /dev/null +++ b/config/dot/quickshell/modules/datemenu/AgendaPane.qml @@ -0,0 +1,298 @@ +import QtQuick +import Quickshell +import qs.config +import qs.services +import qs.modules.quicksettings + +Item { + id: root + + signal requestNotifications + + readonly property bool isToday: CalendarAgenda.selectedDateKey === CalendarAgenda.dateKey(clock.date) + readonly property var upNext: CalendarAgenda.nextEventForSelectedDate + readonly property var listedEvents: CalendarAgenda.selectedEvents.filter(event => !root.upNext || event.id !== root.upNext.id) + readonly property string selectedLabel: root.isToday + ? "Today" + : Qt.formatDate(CalendarAgenda.selectedDate, "dddd, MMMM d") + + implicitHeight: content.implicitHeight + + SystemClock { + id: clock + precision: SystemClock.Minutes + } + + Column { + id: content + width: parent.width + spacing: 12 + + Column { + width: parent.width + spacing: 3 + visible: CalendarAgenda.phase === "loading" + + Text { + text: "CALENDAR" + color: Theme.fgMuted + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.DemiBold + font.letterSpacing: 1.1 + } + Text { + text: "Loading calendars…" + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.DemiBold + } + } + + Column { + width: parent.width + spacing: 10 + visible: CalendarAgenda.phase === "unavailable" + + Text { + text: "Calendar data is unavailable" + color: Theme.fg + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.DemiBold + } + Text { + width: parent.width + wrapMode: Text.WordWrap + text: "Notifications, weather, and the month grid are still available. GNOME Calendar may have more detail." + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + } + ActionPill { + text: "Open Calendar" + onClicked: CalendarAgenda.openCalendar(CalendarAgenda.selectedDate) + } + } + + Rectangle { + id: nextCard + width: parent.width + height: visible ? 88 : 0 + visible: CalendarAgenda.available && root.isToday && root.upNext !== null + radius: Theme.cardRadius + border.width: 1 + border.color: Theme.alpha(Theme.accent, 0.18) + color: Theme.alpha(Theme.accent, 0.105) + clip: true + + Rectangle { + anchors.right: parent.right + anchors.rightMargin: -24 + anchors.bottom: parent.bottom + anchors.bottomMargin: -34 + width: 110 + height: 110 + radius: 55 + border.width: 0 + color: Theme.alpha(Theme.accentSecondary, 0.08) + } + + Column { + anchors.left: parent.left + anchors.leftMargin: 14 + anchors.right: nextJoin.visible ? nextJoin.left : parent.right + anchors.rightMargin: nextJoin.visible ? 10 : 14 + anchors.verticalCenter: parent.verticalCenter + spacing: 5 + + Text { + text: "UP NEXT · " + root.relativeStart(root.upNext) + color: Theme.accent + font.family: Theme.fontFamily + font.features: Theme.tabularFigures + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + font.letterSpacing: 0.8 + } + Text { + width: parent.width + text: root.upNext?.summary ?? "" + color: Theme.fg + elide: Text.ElideRight + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.DemiBold + } + Text { + width: parent.width + text: root.eventDetail(root.upNext) + color: Theme.fgDim + elide: Text.ElideRight + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + } + } + + ActionPill { + id: nextJoin + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.top: parent.top + anchors.topMargin: 12 + visible: String(root.upNext?.joinUrl ?? "").startsWith("https://") + text: "Join" + accent: true + onClicked: CalendarAgenda.join(root.upNext) + } + + MouseArea { + anchors.fill: parent + z: -1 + cursorShape: Qt.PointingHandCursor + onClicked: CalendarAgenda.openEvent(root.upNext) + } + } + + Item { + width: parent.width + height: 34 + visible: CalendarAgenda.available + + Column { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + Text { + text: root.selectedLabel.toUpperCase() + color: Theme.fgMuted + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.DemiBold + font.letterSpacing: 0.9 + } + Text { + text: "Your schedule" + color: Theme.fg + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.DemiBold + } + } + + Text { + anchors.right: parent.right + anchors.bottom: parent.bottom + text: CalendarAgenda.selectedEvents.length + (CalendarAgenda.selectedEvents.length === 1 ? " event" : " events") + color: Theme.fgDim + font.family: Theme.fontFamily + font.features: Theme.tabularFigures + font.pixelSize: Theme.fontSizeSmall + } + } + + Item { + width: parent.width + height: visible ? 92 : 0 + visible: CalendarAgenda.available && CalendarAgenda.selectedEvents.length === 0 + + Column { + anchors.centerIn: parent + spacing: 8 + + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "Nothing scheduled" + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + } + ActionPill { + anchors.horizontalCenter: parent.horizontalCenter + text: "Open Calendar" + onClicked: CalendarAgenda.openCalendar(CalendarAgenda.selectedDate) + } + } + } + + ScrollColumn { + width: parent.width + maxHeight: 286 + spacing: 0 + visible: CalendarAgenda.available && root.listedEvents.length > 0 + + Repeater { + model: root.listedEvents + + AgendaEvent { + required property var modelData + + width: parent.width + eventData: modelData + past: Number(modelData.end) <= CalendarAgenda.nowEpoch + onActivated: CalendarAgenda.openEvent(modelData) + onJoinRequested: CalendarAgenda.join(modelData) + } + } + } + + NotificationSummary { + width: parent.width + onActivated: root.requestNotifications() + } + } + + function relativeStart(event: var): string { + if (!event) + return ""; + const seconds = Number(event.start) - CalendarAgenda.nowEpoch; + if (seconds <= 0) + return "NOW"; + if (seconds < 3600) + return "IN " + Math.ceil(seconds / 60) + " MINUTES"; + return Qt.formatTime(new Date(Number(event.start) * 1000), Settings.use24Hour ? "HH:mm" : "h:mm AP"); + } + + function eventDetail(event: var): string { + if (!event) + return ""; + const source = CalendarAgenda.sourceForId(event.sourceId)?.name ?? "Calendar"; + const location = String(event.location ?? "").trim(); + return location === "" ? source : source + " · " + location; + } + + component ActionPill: Rectangle { + id: action + + required property string text + property bool accent: false + signal clicked + + width: actionLabel.implicitWidth + 20 + height: 28 + radius: Theme.pillRadius + border.width: 0 + color: actionMouse.containsMouse + ? Theme.alpha(action.accent ? Theme.accent : Theme.fg, 0.24) + : Theme.alpha(action.accent ? Theme.accent : Theme.fg, action.accent ? 0.14 : 0.07) + + Text { + id: actionLabel + anchors.centerIn: parent + text: action.text + color: action.accent ? Theme.accent : Theme.fg + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.DemiBold + } + MouseArea { + id: actionMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: action.clicked() + } + } +} diff --git a/config/dot/quickshell/modules/datemenu/DateMenu.qml b/config/dot/quickshell/modules/datemenu/DateMenu.qml index b33b44a..f353dd9 100644 --- a/config/dot/quickshell/modules/datemenu/DateMenu.qml +++ b/config/dot/quickshell/modules/datemenu/DateMenu.qml @@ -29,7 +29,7 @@ PanelWindow { margins.top: Theme.barHeight + Theme.barGap * 2 exclusiveZone: 0 - implicitWidth: panel.implicitWidth + implicitWidth: 760 implicitHeight: panel.implicitHeight // The `qs-popover` prefix is matched by a layerrule in hypr/rules.lua. @@ -41,15 +41,24 @@ PanelWindow { onVisibleChanged: { if (root.visible) { - // Opening the tray is what marks everything read, same as GNOME. - Notifs.markAllRead(); - panel.prepare(); + CalendarAgenda.refresh(); + if (ShellState.dateMenuPage === "notifications") + Notifs.markAllRead(); + panel.prepare(ShellState.dateMenuPage); openAnim.restart(); } else { panel.reset(); } } + Connections { + target: ShellState + function onDateMenuPageChanged(): void { + if (root.visible && ShellState.dateMenuPage === "notifications") + Notifs.markAllRead(); + } + } + // Closes the panel when a click lands anywhere else on the desktop. HyprlandFocusGrab { windows: [root] diff --git a/config/dot/quickshell/modules/datemenu/DateMenuPanel.qml b/config/dot/quickshell/modules/datemenu/DateMenuPanel.qml index 25feb20..585511a 100644 --- a/config/dot/quickshell/modules/datemenu/DateMenuPanel.qml +++ b/config/dot/quickshell/modules/datemenu/DateMenuPanel.qml @@ -1,9 +1,5 @@ -// The contents of the date menu, in GNOME's order: date header, notifications, -// calendar, weather. -// -// Kept separate from DateMenu.qml (the PanelWindow) so it can be dropped into a -// FloatingWindow for testing — layer-shell surfaces cannot map outside a -// wlroots compositor. +// The approved A · Daybook: month context on the left, the selected day's +// schedule or grouped notification history on the right. import QtQuick import Quickshell @@ -14,38 +10,38 @@ import qs.widgets Item { id: root - implicitWidth: 480 - implicitHeight: content.implicitHeight + Theme.popoverPadding * 2 + implicitWidth: 760 + implicitHeight: frame.implicitHeight + Theme.popoverPadding * 2 - // Called by the window when it opens and closes, so the panel is always in - // a known state rather than wherever the last visit left it. - function prepare(): void { + function prepare(page: string): void { calendar.showToday(); + CalendarAgenda.selectDate(clock.date); + CalendarAgenda.setVisibleMonth(clock.date.getFullYear(), clock.date.getMonth()); + if (page === "notifications") + Notifs.markAllRead(); } function reset(): void { notifications.reset(); } - // Minutes precision: the header only shows a date, so this exists purely to - // roll it over at midnight without a per-second wake. SystemClock { id: clock precision: SystemClock.Minutes } Column { - id: content + id: frame anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: Theme.popoverPadding - spacing: Theme.sectionSpacing + spacing: 0 - // ── Header ────────────────────────────────────────────────────────── Item { + id: header width: parent.width - height: 34 + height: 42 Text { anchors.left: parent.left @@ -57,7 +53,6 @@ Item { color: Theme.fg elide: Text.ElideRight font.family: Theme.fontFamily - // The day number changes in place at midnight. font.features: Theme.tabularFigures font.pixelSize: Theme.fontSizeTitle font.weight: Font.DemiBold @@ -69,153 +64,210 @@ Item { anchors.verticalCenter: parent.verticalCenter spacing: 6 - // Labelled rather than icon-only: an unexplained crossed-out - // bell is exactly the kind of thing a normal person has to - // click to find out about. - Rectangle { - id: dndPill - anchors.verticalCenter: parent.verticalCenter - width: dndRow.implicitWidth + 20 - height: 28 - radius: Theme.pillRadius - border.width: 0 - - color: { - if (Notifs.doNotDisturb) - return dndMouse.containsMouse ? Theme.alpha(Theme.accent, 0.42) : Theme.alpha(Theme.accent, 0.28); - return dndMouse.containsMouse ? Theme.alpha(Theme.fg, 0.16) : Theme.alpha(Theme.fg, 0.08); - } - - Behavior on color { - ColorAnimation { - duration: dndMouse.containsMouse ? Theme.durFast : Theme.durNormal - } - } - - Row { - id: dndRow - anchors.centerIn: parent - spacing: 6 - - ThemedIcon { - anchors.verticalCenter: parent.verticalCenter - size: 14 - icon: "notifications-disabled-symbolic" - iconFallback: "user-invisible-symbolic" - tint: Notifs.doNotDisturb ? Theme.accent : Theme.fgDim - } - - Text { - anchors.verticalCenter: parent.verticalCenter - text: "Do Not Disturb" - color: Notifs.doNotDisturb ? Theme.accent : Theme.fg - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - } - } - - MouseArea { - id: dndMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb - } + HeaderPill { + visible: ShellState.dateMenuPage === "notifications" + text: "← Back to agenda" + onClicked: ShellState.dateMenuPage = "agenda" } - Rectangle { - id: clearPill - anchors.verticalCenter: parent.verticalCenter - width: clearLabel.implicitWidth + 20 - height: 28 - radius: Theme.pillRadius - border.width: 0 - // Hidden rather than disabled: there is nothing ambiguous - // about an empty tray, so the control has no job. - visible: Notifs.hasNotifications - color: clearMouse.containsMouse ? Theme.alpha(Theme.fg, 0.16) : Theme.alpha(Theme.fg, 0.08) + HeaderPill { + text: "Do Not Disturb" + active: Notifs.doNotDisturb + icon: "notifications-disabled-symbolic" + onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb + } - Behavior on color { - ColorAnimation { - duration: clearMouse.containsMouse ? Theme.durFast : Theme.durNormal - } - } + HeaderPill { + visible: ShellState.dateMenuPage === "agenda" + text: "Open Calendar ↗" + onClicked: CalendarAgenda.openCalendar(CalendarAgenda.selectedDate) + } - Text { - id: clearLabel - anchors.centerIn: parent - text: "Clear all" - color: Theme.fg - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - } - - MouseArea { - id: clearMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: Notifs.dismissAll() - } + HeaderPill { + visible: ShellState.dateMenuPage === "notifications" && Notifs.hasNotifications + text: "Clear all" + onClicked: Notifs.dismissAll() } } } - // ── Notifications ─────────────────────────────────────────────────── Rectangle { width: parent.width height: 1 border.width: 0 - color: Theme.alpha(Theme.fg, 0.1) + color: Theme.alpha(Theme.fg, 0.10) } - MediaCard { - id: media + Row { + id: body width: parent.width + height: Math.min(620, Math.max(438, leftContent.implicitHeight + 28, rightContentHeight + 28)) + + Item { + id: leftColumn + width: 310 + height: body.height + + Column { + id: leftContent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 14 + spacing: 12 + + CalendarGrid { + id: calendar + width: parent.width + selectedDate: CalendarAgenda.selectedDate + onDateActivated: date => CalendarAgenda.selectDate(date) + onVisibleMonthChanged: (year, month) => CalendarAgenda.setVisibleMonth(year, month) + } + + Rectangle { + width: parent.width + height: 1 + border.width: 0 + visible: media.visible || Weather.available + color: Theme.alpha(Theme.fg, 0.08) + } + + MediaCard { + id: media + width: parent.width + } + + WeatherCard { + width: parent.width + visible: Weather.available + } + } + } + + Rectangle { + width: 1 + height: body.height + border.width: 0 + color: Theme.alpha(Theme.fg, 0.10) + } + + Item { + id: rightColumn + width: body.width - leftColumn.width - 1 + height: body.height + + readonly property real availableWidth: width - 28 + + AgendaPane { + id: agenda + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 14 + visible: ShellState.dateMenuPage === "agenda" + onRequestNotifications: { + ShellState.dateMenuPage = "notifications"; + Notifs.markAllRead(); + } + } + + Column { + id: notificationPane + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 14 + spacing: 10 + visible: ShellState.dateMenuPage === "notifications" + + Item { + width: parent.width + height: 28 + + Text { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: "Notifications" + color: Theme.fg + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.DemiBold + } + + Text { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + text: Notifs.history.length + (Notifs.history.length === 1 ? " item" : " items") + color: Theme.fgDim + font.family: Theme.fontFamily + font.features: Theme.tabularFigures + font.pixelSize: Theme.fontSizeSmall + } + } + + NotificationList { + id: notifications + width: parent.width + maxHeight: body.height - 76 + } + } + } + } + } + + readonly property real rightContentHeight: ShellState.dateMenuPage === "notifications" + ? notificationPane.implicitHeight + : agenda.implicitHeight + + component HeaderPill: Rectangle { + id: pill + + required property string text + property string icon: "" + property bool active: false + signal clicked + + width: pillRow.implicitWidth + 20 + height: 28 + radius: Theme.pillRadius + border.width: 0 + color: { + if (pill.active) + return pillMouse.containsMouse ? Theme.alpha(Theme.accent, 0.38) : Theme.alpha(Theme.accent, 0.24); + return pillMouse.containsMouse ? Theme.alpha(Theme.fg, 0.15) : Theme.alpha(Theme.fg, 0.07); } - Rectangle { - width: parent.width - height: 1 - border.width: 0 - visible: media.visible - color: Theme.alpha(Theme.fg, 0.1) + Behavior on color { ColorAnimation { duration: Theme.durFast } } + + Row { + id: pillRow + anchors.centerIn: parent + spacing: 6 + + ThemedIcon { + anchors.verticalCenter: parent.verticalCenter + visible: pill.icon !== "" + size: visible ? 14 : 0 + icon: pill.icon + tint: pill.active ? Theme.accent : Theme.fgDim + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: pill.text + color: pill.active ? Theme.accent : Theme.fg + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + } } - NotificationList { - id: notifications - width: parent.width - } - - // ── Calendar ──────────────────────────────────────────────────────── - Rectangle { - width: parent.width - height: 1 - border.width: 0 - color: Theme.alpha(Theme.fg, 0.1) - } - - CalendarGrid { - id: calendar - width: parent.width - } - - // ── Weather ───────────────────────────────────────────────────────── - // Separator and card share one condition so the panel does not end on a - // rule with nothing under it. - Rectangle { - width: parent.width - height: 1 - border.width: 0 - visible: Weather.available - color: Theme.alpha(Theme.fg, 0.1) - } - - WeatherCard { - width: parent.width - visible: Weather.available + MouseArea { + id: pillMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: pill.clicked() } } } diff --git a/config/dot/quickshell/modules/datemenu/MediaCard.qml b/config/dot/quickshell/modules/datemenu/MediaCard.qml index bafd84c..d2a8e87 100644 --- a/config/dot/quickshell/modules/datemenu/MediaCard.qml +++ b/config/dot/quickshell/modules/datemenu/MediaCard.qml @@ -9,13 +9,15 @@ import qs.widgets Rectangle { id: root + readonly property bool compact: width < 360 + readonly property MprisPlayer player: { const players = Mpris.players ? Mpris.players.values : []; return players.find(p => p.isPlaying) ?? players.find(p => p.trackTitle) ?? null; } visible: root.player !== null - implicitHeight: visible ? 76 : 0 + implicitHeight: visible ? (compact ? 64 : 76) : 0 radius: Theme.cardRadius border.width: 0 color: Theme.alpha(Theme.fg, 0.06) @@ -24,11 +26,11 @@ Rectangle { Rectangle { id: artwork anchors.left: parent.left - anchors.leftMargin: 10 + anchors.leftMargin: root.compact ? 8 : 10 anchors.verticalCenter: parent.verticalCenter - width: 56 - height: 56 - radius: 10 + width: root.compact ? 46 : 56 + height: width + radius: root.compact ? 9 : 10 border.width: 0 color: Theme.alpha(Theme.accentAlt, 0.16) clip: true @@ -54,9 +56,9 @@ Rectangle { Column { anchors.left: artwork.right - anchors.leftMargin: 12 + anchors.leftMargin: root.compact ? 9 : 12 anchors.right: controls.left - anchors.rightMargin: 10 + anchors.rightMargin: root.compact ? 6 : 10 anchors.verticalCenter: parent.verticalCenter spacing: 3 @@ -83,7 +85,7 @@ Rectangle { Row { id: controls anchors.right: parent.right - anchors.rightMargin: 10 + anchors.rightMargin: root.compact ? 7 : 10 anchors.verticalCenter: parent.verticalCenter spacing: 2 @@ -114,7 +116,7 @@ Rectangle { property bool emphasized: false signal clicked - width: emphasized ? 34 : 30 + width: root.compact ? (emphasized ? 30 : 27) : (emphasized ? 34 : 30) height: width radius: width / 2 border.width: 0 diff --git a/config/dot/quickshell/modules/datemenu/NotificationSummary.qml b/config/dot/quickshell/modules/datemenu/NotificationSummary.qml new file mode 100644 index 0000000..083bbb1 --- /dev/null +++ b/config/dot/quickshell/modules/datemenu/NotificationSummary.qml @@ -0,0 +1,58 @@ +import QtQuick +import qs.config +import qs.services + +Rectangle { + id: root + + signal activated + + visible: Notifs.hasNotifications + implicitHeight: visible ? 62 : 0 + radius: Theme.cardRadius + border.width: 0 + color: summaryMouse.containsMouse ? Theme.alpha(Theme.fg, 0.10) : Theme.alpha(Theme.fg, 0.055) + + Behavior on color { ColorAnimation { duration: Theme.durFast } } + + Column { + anchors.left: parent.left + anchors.leftMargin: 14 + anchors.verticalCenter: parent.verticalCenter + spacing: 3 + + Text { + text: Notifs.history.length + (Notifs.history.length === 1 ? " notification" : " notifications") + color: Theme.fg + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + font.weight: Font.DemiBold + } + + Text { + text: "Grouped quietly until you open them" + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + } + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 14 + anchors.verticalCenter: parent.verticalCenter + text: "View →" + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + } + + MouseArea { + id: summaryMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.activated() + } +} diff --git a/config/dot/quickshell/modules/datemenu/qmldir b/config/dot/quickshell/modules/datemenu/qmldir index 1ef257a..b7967a4 100644 --- a/config/dot/quickshell/modules/datemenu/qmldir +++ b/config/dot/quickshell/modules/datemenu/qmldir @@ -1,4 +1,6 @@ module qs.modules.datemenu +AgendaEvent 1.0 AgendaEvent.qml +AgendaPane 1.0 AgendaPane.qml CalendarGrid 1.0 CalendarGrid.qml DateMenu 1.0 DateMenu.qml DateMenuPanel 1.0 DateMenuPanel.qml @@ -6,4 +8,5 @@ MediaCard 1.0 MediaCard.qml MonthArrow 1.0 MonthArrow.qml NotificationGroup 1.0 NotificationGroup.qml NotificationList 1.0 NotificationList.qml +NotificationSummary 1.0 NotificationSummary.qml WeatherCard 1.0 WeatherCard.qml diff --git a/docs/superpowers/plans/2026-08-17-calendar-agenda.md b/docs/superpowers/plans/2026-08-17-calendar-agenda.md index a83e566..00d2ef4 100644 --- a/docs/superpowers/plans/2026-08-17-calendar-agenda.md +++ b/docs/superpowers/plans/2026-08-17-calendar-agenda.md @@ -526,7 +526,7 @@ git commit -m "Route the interactive Panama date menu" - Consumes: `ShellState.dateMenuPage`, `Notifs.hasNotifications`, and existing `NotificationList`. - Produces: a 760-pixel two-column date menu with left width 310 and independently bounded right content. -- [ ] **Step 1: Add failing structural and live-surface assertions** +- [x] **Step 1: Add failing structural and live-surface assertions** Extend the contract to assert the loaded layer and approved components: @@ -542,7 +542,7 @@ done Run the contract and expect failure on width or missing components. -- [ ] **Step 2: Implement focused event and summary components** +- [x] **Step 2: Implement focused event and summary components** `AgendaEvent.qml` accepts `property var event`, `property bool past`, and signals `activated` and `joinRequested`. It renders a 48-pixel tabular time column, 3-pixel source-color rail, elided summary, source/location metadata, and optional Join pill. All-day rows replace time with `All day`. @@ -557,7 +557,7 @@ Run the contract and expect failure on width or missing components. The Up Next card is omitted for selected dates other than today and when no future timed event exists. Use `ScrollColumn` with a maximum content height derived from the panel's available height. Event clicks call `CalendarAgenda.openEvent(event)`; Join calls `CalendarAgenda.join(event)`. -- [ ] **Step 3: Recompose DateMenuPanel as Daybook** +- [x] **Step 3: Recompose DateMenuPanel as Daybook** Keep the existing header styling and Prism container, but replace the single `Column` with this concrete structure: @@ -601,7 +601,7 @@ When Agenda's notification summary activates, set `ShellState.dateMenuPage = "no Resize MediaCard typography/controls responsively for the 310-pixel column; do not duplicate its playback logic. -- [ ] **Step 4: Update DateMenu lifecycle and bounds** +- [x] **Step 4: Update DateMenu lifecycle and bounds** Set `DateMenu.implicitWidth: 760`. Bound height to the active screen's available geometry, with the panel content choosing a maximum right-column scroll height instead of overflowing the monitor. @@ -616,7 +616,7 @@ panel.prepare(ShellState.dateMenuPage); Add a connection that marks notifications read when an already-open panel switches into Notifications. Opening from the clock must not mark them read. -- [ ] **Step 5: Verify fixture visuals on the real layer surface** +- [x] **Step 5: Verify fixture visuals on the real layer surface** Run: @@ -637,7 +637,7 @@ Capture a temporary full-screen screenshot with `grim`, inspect the Daybook at o Close the panel and reset fixture mode after inspection. Do not add screenshots to git. -- [ ] **Step 6: Commit the Daybook** +- [x] **Step 6: Commit the Daybook** ```bash git add config/dot/quickshell/modules/datemenu \ diff --git a/tests/quickshell/calendar-agenda-contract.sh b/tests/quickshell/calendar-agenda-contract.sh index d46a9b4..e540367 100755 --- a/tests/quickshell/calendar-agenda-contract.sh +++ b/tests/quickshell/calendar-agenda-contract.sh @@ -40,6 +40,15 @@ jq -e '.open == true and .page == "notifications"' \ <<<"$(qs ipc call calendar-agenda status)" >/dev/null \ || fail 'notification open did not route to Notifications' +hyprctl layers | rg -q 'namespace: qs-popover-datemenu' \ + || fail 'Daybook layer did not map' +rg -q 'implicitWidth: 760' config/dot/quickshell/modules/datemenu/DateMenu.qml \ + || fail 'Daybook is not the approved width' +for component in AgendaPane AgendaEvent NotificationSummary; do + rg -q "${component} 1.0 ${component}.qml" config/dot/quickshell/modules/datemenu/qmldir \ + || fail "$component is not registered" +done + 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'