Share one calendar with day selection and a month picker

This commit is contained in:
SirBlob committed 2026-09-24 12:37:27 -04:00
1 parent d59725f193
commit 3164e50b36
10 files changed
+773 -543

No files matched your search

+1 -1
View File
@@ -53,7 +53,7 @@ BarWidget {
}
function formatted(date) {
return Qt.formatDateTime(date, activeFormat.replace(/ww/g, Model.isoWeekLiteral(date.getFullYear(), date.getMonth(), date.getDate())))
return Qt.formatDateTime(date, activeFormat.replace(/ww/g, CalendarModel.isoWeekLiteral(date.getFullYear(), date.getMonth(), date.getDate())))
}
// ---- Calendar popup. Shape contract for shell.summon/hide/toggle
+3 -168
View File
@@ -1,13 +1,5 @@
// Pure date and format math for the clock widget and its calendar panel.
// Everything here is locale- and Qt-free so it can be unit tested under node
// (test/shell.d/clock-test.sh); the QML owns month/weekday naming through
// Qt.locale().
var MS_PER_DAY = 86400000
// Weekday indices match both JS Date.getDay() and QML's Locale.Sunday…
// Locale.Saturday, so a locale's firstDayOfWeek can be passed straight in.
var WEEKDAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]
// Clock label formats and the memento mori bar. Calendar date math lives in
// qs.Ui's CalendarModel.js, shared with the Quick Settings calendar.
// ---- Bar label formats. Right-clicking the clock walks these in order and
// writes the result back to shell.json, so the label the bar shows and
@@ -67,99 +59,6 @@ function nextClockFormat(ring, current) {
return ring[(index + 1) % ring.length]
}
// Two-digit ISO week, substituted into a format's 'ww' token before Qt
// formats it -- Qt has no ISO week specifier of its own.
function isoWeekLiteral(year, month, day) {
return pad2(isoWeek(year, month, day))
}
function pad2(value) {
var n = Number(value)
return (n < 10 ? "0" : "") + n
}
// Stable "yyyy-MM-dd" identity for a day, so a grid cell can be compared
// against today without dragging Date objects through bindings.
function dateKey(year, month, day) {
return year + "-" + pad2(Number(month) + 1) + "-" + pad2(day)
}
function keyForDate(date) {
return dateKey(date.getFullYear(), date.getMonth(), date.getDate())
}
function coerceWeekStart(value) {
if (value === undefined || value === null) return null
if (typeof value === "number")
return isFinite(value) ? ((Math.round(value) % 7) + 7) % 7 : null
var text = String(value).replace(/^\s+|\s+$/g, "").toLowerCase()
if (text === "") return null
for (var i = 0; i < WEEKDAY_NAMES.length; i++)
if (WEEKDAY_NAMES[i] === text || WEEKDAY_NAMES[i].substr(0, 3) === text) return i
var parsed = parseInt(text, 10)
return isFinite(parsed) ? ((parsed % 7) + 7) % 7 : null
}
// Configured week start, falling back to the locale's own first day when
// the setting is missing or nonsense.
function normalizedWeekStart(value, fallback) {
var configured = coerceWeekStart(value)
if (configured !== null) return configured
var fallbackStart = coerceWeekStart(fallback)
return fallbackStart === null ? 1 : fallbackStart
}
function weekStartSettingName(index) {
return WEEKDAY_NAMES[normalizedWeekStart(index, 1)]
}
// The toggle flips between the two conventions people actually switch
// between. A calendar configured to any other start (Saturday, say) is
// shown as-is and lands on Monday the first time it is toggled.
function toggledWeekStart(index) {
return normalizedWeekStart(index, 1) === 1 ? 0 : 1
}
function weekdayOrder(weekStart) {
var start = normalizedWeekStart(weekStart, 1)
var out = []
for (var i = 0; i < 7; i++) out.push((start + i) % 7)
return out
}
// ISO-8601 week number: the week owning the Thursday of that date's
// Monday-based week. Mirrors the clock widget's 'ww' format token.
function isoWeek(year, month, day) {
var date = new Date(Date.UTC(year, month, day))
var weekday = date.getUTCDay() || 7
date.setUTCDate(date.getUTCDate() + 4 - weekday)
var yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1))
return Math.ceil(((date.getTime() - yearStart.getTime()) / MS_PER_DAY + 1) / 7)
}
function dayOfYear(year, month, day) {
return Math.round((Date.UTC(year, month, day) - Date.UTC(year, 0, 1)) / MS_PER_DAY) + 1
}
function daysInYear(year) {
return dayOfYear(year, 11, 31)
}
// Share of the year already behind you: whole days completed over days in
// the year, so January 1 reads 0% and December 31 reads 100%.
function yearProgress(year, month, day) {
var total = daysInYear(year)
if (total <= 0) return 0
return Math.max(0, Math.min(1, (dayOfYear(year, month, day) - 1) / total))
}
function yearProgressPercent(year, month, day) {
return Math.round(yearProgress(year, month, day) * 100)
}
// Memento mori. The default span is a round number rather than anything from
// an actuarial table: the point of the bar is the reminder, not the
// arithmetic, and whoever wants a different number can say so.
@@ -217,80 +116,16 @@ function lifeProgressPercent(age, expectancy) {
return Math.round(lifeProgress(age, expectancy) * 100)
}
// Always six rows of seven days. A fixed grid keeps the popup exactly the
// same height in every month, so stepping through the year never makes the
// panel jump under the pointer.
function monthGrid(year, month, weekStart, todayKey) {
var start = normalizedWeekStart(weekStart, 1)
var leading = (new Date(year, month, 1).getDay() - start + 7) % 7
var cursor = new Date(year, month, 1 - leading)
var today = String(todayKey || "")
var weeks = []
for (var w = 0; w < 6; w++) {
var days = []
var thursday = null
for (var d = 0; d < 7; d++) {
var cellYear = cursor.getFullYear()
var cellMonth = cursor.getMonth()
var cellDay = cursor.getDate()
var weekday = cursor.getDay()
var key = dateKey(cellYear, cellMonth, cellDay)
if (weekday === 4) thursday = { year: cellYear, month: cellMonth, day: cellDay }
days.push({
key: key,
year: cellYear,
month: cellMonth,
day: cellDay,
weekday: weekday,
inMonth: cellMonth === month && cellYear === year,
weekend: weekday === 0 || weekday === 6,
today: key === today
})
cursor.setDate(cursor.getDate() + 1)
}
// Number every row by the ISO week owning its Thursday. That is the
// definition itself for Monday-start weeks, and the only answer that
// stays stable for the other starts, where a row straddles two ISO
// weeks but shares all of Monday through Thursday with one of them.
var anchor = thursday || days[0]
weeks.push({
week: isoWeek(anchor.year, anchor.month, anchor.day),
days: days
})
}
return weeks
}
function stepMonth(year, month, delta) {
var target = new Date(year, Number(month) + Number(delta), 1)
return { year: target.getFullYear(), month: target.getMonth() }
}
if (typeof module !== "undefined") {
module.exports = {
dateKey: dateKey,
keyForDate: keyForDate,
normalizedWeekStart: normalizedWeekStart,
weekStartSettingName: weekStartSettingName,
toggledWeekStart: toggledWeekStart,
weekdayOrder: weekdayOrder,
isoWeek: isoWeek,
dayOfYear: dayOfYear,
daysInYear: daysInYear,
yearProgress: yearProgress,
yearProgressPercent: yearProgressPercent,
parseAge: parseAge,
parseBirthYear: parseBirthYear,
ageFromBirthYear: ageFromBirthYear,
parseLifeExpectancy: parseLifeExpectancy,
lifeProgress: lifeProgress,
lifeProgressPercent: lifeProgressPercent,
monthGrid: monthGrid,
stepMonth: stepMonth,
clockFormats: clockFormats,
clockFormatRing: clockFormatRing,
nextClockFormat: nextClockFormat,
isoWeekLiteral: isoWeekLiteral
nextClockFormat: nextClockFormat
}
}
+46 -281
View File
@@ -8,9 +8,9 @@ import "Model.js" as Model
// sit beside the weather panel — same hero-over-detail composition, same
// spacing scale, same small-caps labels.
//
// The grid is a read-out rather than a picker: today is the only marked
// day, and the only thing that moves is which month is on screen —
// chevrons, the scroll wheel, and the arrow keys all step it.
// The grid itself is the shared CalendarView from qs.Ui: days can be
// selected with the pointer or the arrow keys, the month label opens a
// month picker, and today is filled with the accent color.
//
// BarWidget.qml owns the bar label and hands this panel the button to
// anchor against.
@@ -33,20 +33,11 @@ Panel {
// ---- Today. SystemClock keeps this honest across midnight so the
// highlight rolls over without the panel being reopened.
property date today: new Date()
readonly property string todayKey: Model.keyForDate(today)
// The month on screen. Stepping moves this and nothing else: the grid is
// a read-out, not a picker, so there is no per-day cursor to keep in sync.
property int viewYear: today.getFullYear()
property int viewMonth: today.getMonth()
readonly property date viewDate: new Date(viewYear, viewMonth, 1)
readonly property bool viewingCurrentMonth: viewYear === today.getFullYear() && viewMonth === today.getMonth()
// Pinned to today, not to the month being browsed — stepping through the
// calendar does not change how much of the year is gone.
readonly property real yearDone: Model.yearProgress(today.getFullYear(), today.getMonth(), today.getDate())
readonly property int yearDonePercent: Model.yearProgressPercent(today.getFullYear(), today.getMonth(), today.getDate())
readonly property real yearDone: CalendarModel.yearProgress(today.getFullYear(), today.getMonth(), today.getDate())
readonly property int yearDonePercent: CalendarModel.yearProgressPercent(today.getFullYear(), today.getMonth(), today.getDate())
// Memento mori, for anyone who goes looking: double-tapping the year bar
// asks for a birth year and a life expectancy, and a second bar tracks one
@@ -63,27 +54,18 @@ Panel {
// starts out matching the rest of the desktop rather than a hardcoded
// convention. Clicking the grid's "W" heading writes the choice back to
// shell.json.
readonly property int weekStart: Model.normalizedWeekStart(setting("weekStartDay", null), Qt.locale().firstDayOfWeek)
readonly property int weekStart: CalendarModel.normalizedWeekStart(setting("weekStartDay", null), Qt.locale().firstDayOfWeek)
// The interface is English throughout, so day names are not taken from the
// system locale. Where the week starts still is: that is a regional
// convention rather than a translation, and it stays overridable above.
readonly property var labelLocale: Qt.locale("en_US")
readonly property string nextWeekStartLabel: labelLocale.dayName(Model.toggledWeekStart(weekStart), Locale.LongFormat)
readonly property var weekdays: Model.weekdayOrder(weekStart)
readonly property var weeks: Model.monthGrid(viewYear, viewMonth, weekStart, todayKey)
readonly property string nextWeekStartLabel: labelLocale.dayName(CalendarModel.toggledWeekStart(weekStart), Locale.LongFormat)
// Guarded so the widget renders before the bar is injected (the bar-widget
// contract instantiates it bare).
readonly property color contentForeground: bar ? bar.foreground : Color.foreground
readonly property string contentFontFamily: bar ? bar.fontFamily : Style.font.family
readonly property int cellWidth: Style.space(52)
readonly property int cellHeight: Style.space(34)
readonly property int cellSpacing: Style.space(2)
readonly property int weekColumnWidth: Style.space(32)
readonly property int gutterWidth: Style.space(14)
function open() {
refresh()
root.controller.show()
@@ -127,22 +109,7 @@ Panel {
function refresh() {
root.today = new Date()
root.goToToday()
}
function goToToday() {
root.viewYear = today.getFullYear()
root.viewMonth = today.getMonth()
}
function moveMonth(delta) {
var next = Model.stepMonth(viewYear, viewMonth, delta)
root.viewYear = next.year
root.viewMonth = next.month
}
function moveYear(delta) {
moveMonth(delta * 12)
calendar.goToToday()
}
// Applied locally first so the panel redraws on the click itself; the
@@ -163,9 +130,9 @@ Panel {
}
function setWeekStart(day) {
var next = Model.normalizedWeekStart(day, root.weekStart)
var next = CalendarModel.normalizedWeekStart(day, root.weekStart)
if (next === root.weekStart) return
persistSettings({ weekStartDay: Model.weekStartSettingName(next) })
persistSettings({ weekStartDay: CalendarModel.weekStartSettingName(next) })
}
function startEditingLife() {
@@ -216,22 +183,21 @@ Panel {
}
function toggleWeekStart() {
setWeekStart(Model.toggledWeekStart(root.weekStart))
setWeekStart(CalendarModel.toggledWeekStart(root.weekStart))
}
// English short day names, matching the rest of the interface.
function weekdayLabel(weekday) {
return String(labelLocale.dayName(weekday, Locale.ShortFormat)).toUpperCase()
function closeOrDismissPicker() {
if (!calendar.dismissPicker()) root.close()
}
SystemClock {
id: clock
precision: SystemClock.Minutes
onDateChanged: {
if (Model.keyForDate(clock.date) === String(root.todayKey)) return
var followToday = root.viewingCurrentMonth
if (CalendarModel.keyForDate(clock.date) === CalendarModel.keyForDate(root.today)) return
var followToday = calendar.selectionIsToday
root.today = clock.date
if (followToday) root.goToToday()
if (followToday) calendar.goToToday()
}
}
@@ -250,19 +216,17 @@ Panel {
id: keyCatcher
anchors.fill: parent
blocked: root.editingLife
onMoveRequested: function(dx, dy) {
if (dx !== 0) root.moveMonth(dx)
if (dy !== 0) root.moveYear(dy)
}
onActivateRequested: root.goToToday()
onCloseRequested: root.close()
onMoveRequested: function(dx, dy) { calendar.handleMove(dx, dy) }
onActivateRequested: calendar.activate()
onCloseRequested: root.closeOrDismissPicker()
onTabRequested: function(direction) { root.switchPanel(direction) }
onTextKey: function(t) {
if (t === "[") root.moveMonth(-1)
else if (t === "]") root.moveMonth(1)
else if (t === "{") root.moveYear(-1)
else if (t === "}") root.moveYear(1)
else if (t === "t" || t === "T") root.goToToday()
if (t === "[") calendar.moveMonth(-1)
else if (t === "]") calendar.moveMonth(1)
else if (t === "{") calendar.moveYear(-1)
else if (t === "}") calendar.moveYear(1)
else if (t === "t" || t === "T") calendar.goToToday()
else if (t === "m" || t === "M") calendar.togglePicker()
else if (t === "w" || t === "W") root.toggleWeekStart()
}
@@ -280,7 +244,7 @@ Panel {
// Never narrower than the grid. The popup width is capped to what
// the screen allows, and a fixed seven-column grid would otherwise
// lose its last days off the edge instead of scrolling.
width: Math.max(calendarScroll.width, gridColumn.width)
width: Math.max(calendarScroll.width, calendar.gridWidth)
spacing: Style.space(8)
// ---- Hero: today, centered. Once the view has stepped back
@@ -331,10 +295,10 @@ Panel {
y: heroRow.y
width: heroRow.width
height: heroRow.height
enabled: !root.viewingCurrentMonth
enabled: !calendar.selectionIsToday || calendar.pickingMonth
hoverEnabled: enabled
cursorShape: Qt.PointingHandCursor
onClicked: root.goToToday()
onClicked: calendar.goToToday()
PanelToolTip {
visible: heroMouse.containsMouse
@@ -355,7 +319,7 @@ Panel {
id: yearBlock
y: Style.space(6)
anchors.horizontalCenter: parent.horizontalCenter
width: gridColumn.width
width: calendar.gridWidth
height: Math.max(yearLabel.implicitHeight, Style.space(10))
TapHandler {
@@ -474,7 +438,7 @@ Panel {
Item {
id: lifeBlock
anchors.horizontalCenter: parent.horizontalCenter
width: gridColumn.width
width: calendar.gridWidth
height: Math.max(lifeLabel.implicitHeight, Style.space(10))
Text {
@@ -538,222 +502,23 @@ Panel {
}
}
// ---- Month grid: week numbers down a gutter on the left, then
// the seven day columns. Always six rows, so the popup is
// exactly as tall in February as it is in August.
Item {
CalendarView {
id: calendar
width: parent.width
height: gridColumn.y + gridColumn.height
WheelHandler {
onWheel: function(event) {
// Horizontal wheels and touchpad side-scrolls report y === 0;
// without this they would every one read as "next month".
if (event.angleDelta.y === 0) return
root.moveMonth(event.angleDelta.y > 0 ? -1 : 1)
}
}
Column {
id: gridColumn
// The meter above is a solid rule; the grid needs room to
// read as its own block rather than hanging off it.
y: Style.space(18)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.space(3)
Row {
id: headerRow
spacing: root.cellSpacing
// The week-number heading doubles as the week-start toggle.
// It is the one control in the panel whose meaning is not
// self-evident, so it carries a tooltip naming the day the
// click will switch to.
Rectangle {
width: root.weekColumnWidth
height: Style.space(16)
radius: Style.cornerRadius
color: weekStartMouse.containsMouse
? Style.hoverFillFor(root.contentForeground, Color.accent)
: "transparent"
Text {
anchors.centerIn: parent
text: "W"
color: weekStartMouse.containsMouse
? Style.hoverStateColor(root.contentForeground, Color.accent)
: Qt.darker(root.contentForeground, 1.9)
font.family: root.contentFontFamily
font.pixelSize: Style.font.caption
font.letterSpacing: 1
font.bold: true
}
MouseArea {
id: weekStartMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleWeekStart()
}
PanelToolTip {
visible: weekStartMouse.containsMouse
text: "Start weeks on " + root.nextWeekStartLabel
fontFamily: root.contentFontFamily
}
}
Item {
width: root.gutterWidth
height: Style.space(16)
}
Repeater {
model: root.weekdays
Text {
textFormat: Text.PlainText
required property var modelData
width: root.cellWidth
height: Style.space(16)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: root.weekdayLabel(modelData)
color: Qt.darker(root.contentForeground, 1.5)
font.family: root.contentFontFamily
font.pixelSize: Style.font.caption
font.letterSpacing: 1
font.bold: true
}
}
}
Repeater {
model: root.weeks
Row {
required property var modelData
spacing: root.cellSpacing
Text {
textFormat: Text.PlainText
width: root.weekColumnWidth
height: root.cellHeight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: modelData.week
color: Qt.darker(root.contentForeground, 1.9)
font.family: root.contentFontFamily
font.pixelSize: Style.font.caption
}
Item {
width: root.gutterWidth
height: root.cellHeight
}
Repeater {
model: modelData.days
Rectangle {
required property var modelData
width: root.cellWidth
height: root.cellHeight
radius: Style.cornerRadius
// Today is outlined, not filled: a lit-up block shouts
// over a grid this quiet.
color: "transparent"
border.width: modelData.today ? Style.spacing.hairline : 0
border.color: Style.normalBorderFor(root.contentForeground, Color.accent)
Text {
textFormat: Text.PlainText
anchors.centerIn: parent
text: modelData.day
color: modelData.inMonth
? (modelData.weekend ? Qt.darker(root.contentForeground, 1.45) : root.contentForeground)
: Qt.darker(root.contentForeground, 2.2)
font.family: root.contentFontFamily
font.pixelSize: Style.font.body
font.bold: modelData.today
}
}
}
}
}
}
// Hairline down the week-number gutter, drawn only beside the
// day rows so it does not cut through the header band.
Rectangle {
x: gridColumn.x + root.weekColumnWidth + root.cellSpacing + Math.round((root.gutterWidth - width) / 2)
y: gridColumn.y + headerRow.height + gridColumn.spacing
width: Style.spacing.hairline
height: gridColumn.height - headerRow.height - gridColumn.spacing
color: root.contentForeground
opacity: 0.1
}
}
// ---- Month stepping, spanning the grid it drives. The chevrons
// sit on the grid's outer bounds, the same edges the year
// rail above uses, so the row reads as the panel's other
// full-width rail instead of a cluster floating in space.
// The label is centered and fixed-width, so it holds still
// from "MAY" to "SEPTEMBER".
Item {
width: parent.width
height: monthNav.height
Item {
id: monthNav
anchors.horizontalCenter: parent.horizontalCenter
width: gridColumn.width
height: monthLabel.implicitHeight + Style.space(10)
Text {
id: monthLabel
textFormat: Text.PlainText
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
// Fixed width so the chevrons hold still between a
// "MAY 2026" and a "SEPTEMBER 2026".
width: Style.space(130)
horizontalAlignment: Text.AlignHCenter
text: Qt.formatDate(root.viewDate, "MMMM yyyy").toUpperCase()
color: Qt.darker(root.contentForeground, 1.4)
font.family: root.contentFontFamily
font.pixelSize: Style.font.body
font.letterSpacing: 1
}
PanelActionButton {
// Pulled out by the button's own padding so the glyph, not
// its hit box, lines up with the "2026" on the year rail.
anchors.left: parent.left
anchors.leftMargin: -Style.space(8)
anchors.verticalCenter: parent.verticalCenter
iconText: "󰅁"
tooltipText: "Previous month"
foreground: root.contentForeground
fontFamily: root.contentFontFamily
onClicked: root.moveMonth(-1)
}
PanelActionButton {
anchors.right: parent.right
anchors.rightMargin: -Style.space(8)
anchors.verticalCenter: parent.verticalCenter
iconText: "󰅂"
tooltipText: "Next month"
foreground: root.contentForeground
fontFamily: root.contentFontFamily
onClicked: root.moveMonth(1)
}
}
topPadding: Style.space(10)
today: root.today
weekStart: root.weekStart
foreground: root.contentForeground
fontFamily: root.contentFontFamily
showWeekNumbers: true
navigationAtBottom: true
weekHeaderTooltip: "Start weeks on " + root.nextWeekStartLabel
cellWidth: Style.space(52)
cellHeight: Style.space(34)
cellSpacing: Style.space(2)
weekColumnWidth: Style.space(32)
gutterWidth: Style.space(14)
onWeekHeaderClicked: root.toggleWeekStart()
}
}
}
@@ -1,115 +1,42 @@
import QtQuick
import Quickshell.Io
import qs.Commons
import qs.Ui
Item {
id: root
property int monthOffset: 0
property string monthName: ""
property string monthGrid: ""
property bool polling: false
implicitHeight: layout.implicitHeight
implicitHeight: calendar.implicitHeight
function loadMonth() {
if (monthProbe.running) return
monthProbe.running = true
function refreshToday() {
var now = new Date()
if (CalendarModel.keyForDate(now) === CalendarModel.keyForDate(calendar.today)) return
var followToday = calendar.selectionIsToday
calendar.today = now
if (followToday) calendar.goToToday()
}
function shiftMonth(delta) {
root.monthOffset += delta
root.loadMonth()
}
function resetMonth() {
root.monthOffset = 0
root.loadMonth()
}
Process {
id: monthProbe
command: ["bash", "-c",
'first="$(date +%Y-%m-01) ' + root.monthOffset + ' months"; ' +
'date -d "$first" "+%B %Y"; cal $(date -d "$first" "+%m %Y") | sed "1d"']
stdout: StdioCollector {
onStreamFinished: {
var lines = String(text).replace(/\s+$/, "").split("\n")
root.monthName = (lines[0] || "").trim()
root.monthGrid = lines.slice(1).join("\n")
}
}
onPollingChanged: {
if (!polling) return
calendar.today = new Date()
calendar.goToToday()
}
Timer {
interval: 3600000
interval: 60000
running: root.polling
repeat: true
onTriggered: if (root.monthOffset === 0) root.loadMonth()
onTriggered: root.refreshToday()
}
Component.onCompleted: root.loadMonth()
WheelHandler {
onWheel: function(event) {
root.shiftMonth(event.angleDelta.y > 0 ? -1 : 1)
}
}
Column {
id: layout
CalendarView {
id: calendar
width: parent.width
spacing: Style.spaceReal(6)
Item {
width: parent.width
implicitHeight: Math.max(monthLabel.implicitHeight, navigation.implicitHeight)
Text {
id: monthLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.monthName
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.body
font.bold: true
}
Row {
id: navigation
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Style.spaceReal(4)
CardIconButton {
icon: "<"
onActivated: root.shiftMonth(-1)
}
CardIconButton {
icon: "Today"
visible: root.monthOffset !== 0
implicitWidth: Style.spaceReal(44)
onActivated: root.resetMonth()
}
CardIconButton {
icon: ">"
onActivated: root.shiftMonth(1)
}
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: root.monthGrid
textFormat: Text.PlainText
color: Util.alpha(Color.foreground, 0.85)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
foreground: Color.foreground
fontFamily: Style.font.family
cellSpacing: Style.space(2)
cellWidth: Math.floor((width - cellSpacing * 6) / 7)
cellHeight: Style.space(24)
}
}