aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/island/calendar
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-13 10:25:59 +0200
committerDanilo M. <danix@danix.xyz>2026-10-13 10:25:59 +0200
commit47a97f0717fc54b5eb33c266d8dbafdc8d3c15ee (patch)
tree9fe90a0fe33e512ba1d2afa87bb5889b52173408 /desktop/island/calendar
parent06b7d7c77c15f24a00cfabb1925a7718cd97210f (diff)
downloadquickshell-47a97f0717fc54b5eb33c266d8dbafdc8d3c15ee.tar.gz
quickshell-47a97f0717fc54b5eb33c266d8dbafdc8d3c15ee.zip
feat(calendar): month grid beside the day's agenda
Six weeks from Monday, a dot per calendar with something that day, today and the selection marked. The agenda lists the selected day, all-day first; a click opens the event in the editor, and while the editor is open the grid picks its dates instead of the selection. Escape closes the editor before the panel. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/island/calendar')
-rw-r--r--desktop/island/calendar/CalendarPanel.qml323
1 files changed, 323 insertions, 0 deletions
diff --git a/desktop/island/calendar/CalendarPanel.qml b/desktop/island/calendar/CalendarPanel.qml
new file mode 100644
index 0000000..9515a9a
--- /dev/null
+++ b/desktop/island/calendar/CalendarPanel.qml
@@ -0,0 +1,323 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import Quickshell
+import QtQuick
+import "../.."
+import "Cal.js" as Cal
+
+// Month grid on the left, the selected day's agenda on the right. Opening
+// an event swaps the agenda for the editor, and the grid becomes its date
+// picker.
+Item {
+ id: panel
+
+ signal closeRequested()
+
+ property int viewY: 2026
+ property int viewM0: 0
+ property string selected: ""
+ property bool editing: false
+ property string agendaError: ""
+
+ readonly property var cells: Cal.monthCells(viewY, viewM0)
+ readonly property real gridWidth: 252
+
+ implicitHeight: Math.max(left.implicitHeight, right.implicitHeight)
+
+ SystemClock { id: clock; precision: SystemClock.Minutes }
+ readonly property string todayIso: Cal.today(clock.date)
+
+ function colorOf(c) {
+ return c.startsWith("#") ? c : Theme[c];
+ }
+
+ function show(y, m0) {
+ panel.viewY = y;
+ panel.viewM0 = m0;
+ const cells = Cal.monthCells(y, m0);
+ Calendar.load(cells[0].iso, Cal.addDays(cells[41].iso, 1));
+ }
+
+ // Each time the panel opens: today, its month, no editor.
+ function reset() {
+ const now = new Date();
+ panel.editing = false;
+ panel.agendaError = "";
+ panel.selected = Cal.today(now);
+ panel.show(now.getFullYear(), now.getMonth());
+ }
+
+ function shift(n) {
+ const m = Cal.shiftMonth(panel.viewY, panel.viewM0, n);
+ panel.show(m.y, m.m0);
+ }
+
+ function clickDay(day, shiftKey) {
+ if (panel.editing) {
+ editor.pick(day, shiftKey);
+ return;
+ }
+ panel.selected = day;
+ if (day.slice(0, 7) !== Cal.monthCells(panel.viewY, panel.viewM0)[10].iso.slice(0, 7))
+ panel.show(+day.slice(0, 4), +day.slice(5, 7) - 1);
+ }
+
+ function openEvent(uid) {
+ Calendar.get(uid, r => {
+ if (!r.ok) { panel.agendaError = r.error; return; }
+ editor.load(Cal.fromEvent(r.data));
+ panel.editing = true;
+ });
+ }
+
+ function newEvent() {
+ editor.load(Cal.newDraft(panel.selected, new Date(), Calendar.defaultCalendar));
+ panel.editing = true;
+ }
+
+ function closeEditor() {
+ panel.editing = false;
+ panel.forceActiveFocus();
+ }
+
+ // Escape: the editor first, then the panel.
+ function escape() {
+ if (panel.editing) { panel.closeEditor(); return true; }
+ return false;
+ }
+
+ Keys.onEscapePressed: if (!panel.escape()) panel.closeRequested()
+
+ Component.onCompleted: panel.reset()
+
+ // --- left: the month ---------------------------------------------------
+
+ Column {
+ id: left
+ width: panel.gridWidth
+ spacing: 6
+
+ Item {
+ width: parent.width
+ height: 22
+
+ Text {
+ anchors.verticalCenter: parent.verticalCenter
+ text: Qt.locale().standaloneMonthName(panel.viewM0) + " " + panel.viewY
+ color: Theme.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true }
+ }
+ Row {
+ anchors { right: parent.right; verticalCenter: parent.verticalCenter }
+ spacing: 12
+ Repeater {
+ model: [{ t: "‹", n: -1 }, { t: "›", n: 1 }]
+ delegate: Text {
+ required property var modelData
+ text: modelData.t
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize + 2 }
+ MouseArea {
+ anchors { fill: parent; margins: -6 }
+ cursorShape: Qt.PointingHandCursor
+ onClicked: panel.shift(parent.modelData.n)
+ }
+ }
+ }
+ }
+ }
+
+ Grid {
+ columns: 7
+ spacing: 2
+
+ Repeater {
+ model: [0, 1, 2, 3, 4, 5, 6]
+ delegate: Text {
+ required property int modelData
+ width: (panel.gridWidth - 12) / 7
+ horizontalAlignment: Text.AlignHCenter
+ // Monday first: Qt.locale().dayName counts from Sunday.
+ text: Qt.locale().dayName((modelData + 1) % 7, Locale.NarrowFormat)
+ color: Theme.overlay
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 }
+ }
+ }
+
+ Repeater {
+ model: panel.cells
+ delegate: Rectangle {
+ id: cell
+ required property var modelData
+ readonly property bool inDraft: panel.editing
+ && modelData.iso >= editor.draft.startDay && modelData.iso <= editor.draft.endDay
+
+ width: (panel.gridWidth - 12) / 7
+ height: 32
+ radius: 8
+ color: inDraft ? Qt.alpha(Theme.accent, 0.35)
+ : hover.containsMouse ? Qt.alpha(Theme.surface, 0.8) : "transparent"
+ border.width: !panel.editing && modelData.iso === panel.selected ? 1 : 0
+ border.color: Theme.accent
+
+ Text {
+ anchors { horizontalCenter: parent.horizontalCenter; top: parent.top; topMargin: 5 }
+ text: cell.modelData.day
+ color: cell.modelData.iso === panel.todayIso ? Theme.accent
+ : cell.modelData.inMonth ? Theme.text : Theme.overlay
+ font {
+ family: Theme.fontFamily
+ pixelSize: Theme.fontSize - 4
+ bold: cell.modelData.iso === panel.todayIso
+ }
+ }
+
+ Row {
+ anchors { horizontalCenter: parent.horizontalCenter; bottom: parent.bottom; bottomMargin: 4 }
+ spacing: 2
+ Repeater {
+ model: Cal.dots(Calendar.events, cell.modelData.iso, Calendar.calendars)
+ delegate: Rectangle {
+ required property var modelData
+ width: 4
+ height: 4
+ radius: 2
+ color: panel.colorOf(modelData.color)
+ }
+ }
+ }
+
+ MouseArea {
+ id: hover
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: mouse => panel.clickDay(cell.modelData.iso,
+ (mouse.modifiers & Qt.ShiftModifier) !== 0)
+ }
+ }
+ }
+ }
+ }
+
+ // --- right: agenda or editor -------------------------------------------
+
+ Item {
+ id: right
+ x: panel.gridWidth + 16
+ width: panel.width - x
+ implicitHeight: panel.editing ? editor.implicitHeight : agenda.implicitHeight
+
+ Column {
+ id: agenda
+ width: parent.width
+ spacing: 6
+ visible: !panel.editing
+
+ Text {
+ text: panel.selected === "" ? ""
+ : Qt.formatDate(new Date(+panel.selected.slice(0, 4), +panel.selected.slice(5, 7) - 1,
+ +panel.selected.slice(8, 10)), "dddd d MMMM")
+ color: Theme.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true }
+ }
+
+ Text {
+ visible: text !== ""
+ width: parent.width
+ wrapMode: Text.WordWrap
+ text: panel.agendaError || Calendar.error
+ color: Theme.red
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 }
+ }
+
+ Repeater {
+ id: dayList
+ model: Cal.dayEvents(Calendar.events, panel.selected)
+ delegate: Rectangle {
+ id: row
+ required property var modelData
+ width: agenda.width
+ height: 30
+ radius: 8
+ color: rowHover.containsMouse ? Theme.surfaceAlt : Theme.surface
+
+ Rectangle {
+ x: 6
+ anchors.verticalCenter: parent.verticalCenter
+ width: 3
+ height: parent.height - 12
+ radius: 2
+ color: panel.colorOf(row.modelData.color)
+ }
+ Text {
+ id: when
+ x: 16
+ width: 82
+ anchors.verticalCenter: parent.verticalCenter
+ text: Cal.timeLabel(row.modelData, panel.selected)
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 }
+ }
+ Text {
+ anchors { left: when.right; right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter }
+ elide: Text.ElideRight
+ text: row.modelData.title
+ color: Theme.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+ MouseArea {
+ id: rowHover
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: panel.openEvent(row.modelData.uid)
+ }
+ }
+ }
+
+ Text {
+ visible: dayList.count === 0
+ text: "Nothing on this day"
+ color: Theme.overlay
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+
+ Rectangle {
+ width: agenda.width
+ height: 30
+ radius: 8
+ color: "transparent"
+ border.width: 1
+ border.color: Theme.surfaceAlt
+ Text {
+ anchors.centerIn: parent
+ text: "+ New event"
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: panel.newEvent()
+ }
+ }
+ }
+
+ EventEditor {
+ id: editor
+ width: parent.width
+ visible: panel.editing
+ onDone: panel.closeEditor()
+ }
+ }
+}