Build the Prism calendar Daybook

This commit is contained in:
Gabriel Brown
2026-08-17 11:38:24 -04:00
parent 6a1bec849d
commit 03914e23af
9 changed files with 724 additions and 165 deletions
@@ -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()
}
}
}
@@ -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()
}
}
}
@@ -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]
@@ -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()
}
}
}
@@ -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
@@ -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()
}
}
@@ -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
@@ -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 \
@@ -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'