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
+771 -541

No files matched your search

+27
View File
@@ -61,6 +61,33 @@ notifications are silenced.
The centre module named by `centerAnchor` cannot be dragged out of the centre. The centre module named by `centerAnchor` cannot be dragged out of the centre.
Every other widget still reorders. Every other widget still reorders.
## Calendar
The clock's calendar popup and the Quick Settings card share one component,
`CalendarView` in `shell/Ui`, with its date math in `Ui/CalendarModel.js`.
- Today is filled with the accent color; days from the neighbouring months are
dimmed.
- Clicking a day selects it, and the line under the grid gives its weekday,
day of the year, and how far it is from today. Clicking a day from the
previous or next month moves the view there.
- Clicking the month label opens a month picker; the chevrons then step whole
years, and clicking a month jumps to it.
- The scroll wheel steps months, or years while the picker is open.
Keys in the clock popup:
| Key | Action |
| --- | --- |
| arrows / `hjkl` | move the selected day (a week up or down), or the month cursor in the picker |
| `[` `]` | previous or next month |
| `{` `}` | previous or next year |
| `m` | open or close the month picker |
| `t` | back to today |
| `w` | toggle the week start between Monday and Sunday |
| Enter | back to today, or pick the highlighted month |
| Esc | close the month picker, then the popup |
## Differences from upstream ## Differences from upstream
This shell was forked from Omarchy 4.0.4 and is not tracked against it. The This shell was forked from Omarchy 4.0.4 and is not tracked against it. The
+176
View File
@@ -0,0 +1,176 @@
import QtQuick
import qs.Commons
import "CalendarModel.js" as CalendarModel
Item {
id: root
required property var calendar
readonly property var weeks: CalendarModel.monthGrid(calendar.viewYear, calendar.viewMonth,
calendar.weekStart, calendar.todayKey, calendar.selectedKey)
readonly property var weekdays: CalendarModel.weekdayOrder(calendar.weekStart)
readonly property color foreground: calendar.foreground
readonly property real headerHeight: Style.space(16)
width: grid.width
height: grid.height
Column {
id: grid
spacing: Style.space(3)
Row {
id: headerRow
spacing: root.calendar.cellSpacing
Rectangle {
visible: root.calendar.showWeekNumbers
width: root.calendar.weekColumnWidth
height: root.headerHeight
radius: Style.cornerRadius
color: weekHeaderMouse.containsMouse ? Style.hoverFillFor(root.foreground, Color.accent) : "transparent"
Text {
anchors.centerIn: parent
text: "W"
color: weekHeaderMouse.containsMouse
? Style.hoverStateColor(root.foreground, Color.accent)
: Qt.darker(root.foreground, 1.9)
font.family: root.calendar.fontFamily
font.pixelSize: Style.font.caption
font.letterSpacing: 1
font.bold: true
}
MouseArea {
id: weekHeaderMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.calendar.weekHeaderClicked()
}
PanelToolTip {
visible: weekHeaderMouse.containsMouse && root.calendar.weekHeaderTooltip !== ""
text: root.calendar.weekHeaderTooltip
fontFamily: root.calendar.fontFamily
}
}
Item {
visible: root.calendar.showWeekNumbers
width: root.calendar.gutterWidth
height: root.headerHeight
}
Repeater {
model: root.weekdays
Text {
required property var modelData
width: root.calendar.cellWidth
height: root.headerHeight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
textFormat: Text.PlainText
text: root.calendar.weekdayLabel(modelData)
color: root.calendar.mutedForeground
font.family: root.calendar.fontFamily
font.pixelSize: Style.font.caption
font.letterSpacing: 1
font.bold: true
}
}
}
Repeater {
model: root.weeks
Row {
required property var modelData
spacing: root.calendar.cellSpacing
Text {
visible: root.calendar.showWeekNumbers
width: root.calendar.weekColumnWidth
height: root.calendar.cellHeight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
textFormat: Text.PlainText
text: modelData.week
color: Qt.darker(root.foreground, 1.9)
font.family: root.calendar.fontFamily
font.pixelSize: Style.font.caption
}
Item {
visible: root.calendar.showWeekNumbers
width: root.calendar.gutterWidth
height: root.calendar.cellHeight
}
Repeater {
model: modelData.days
DayCell {}
}
}
}
}
Rectangle {
visible: root.calendar.showWeekNumbers
x: root.calendar.weekColumnWidth + root.calendar.cellSpacing
+ Math.round((root.calendar.gutterWidth - width) / 2)
y: headerRow.height + grid.spacing
width: Style.spacing.hairline
height: grid.height - headerRow.height - grid.spacing
color: root.foreground
opacity: 0.1
}
component DayCell: Rectangle {
id: cell
required property var modelData
readonly property color todayFill: Style.selectedStateColor(root.foreground, Color.accent)
readonly property color dayColor: {
if (modelData.today) return Color.background
if (!modelData.inMonth) return root.foreground
return modelData.weekend ? Qt.darker(root.foreground, 1.45) : root.foreground
}
width: root.calendar.cellWidth
height: root.calendar.cellHeight
radius: Style.cornerRadius
color: {
if (modelData.today) return todayFill
if (dayMouse.containsMouse) return Style.hoverFillFor(root.foreground, Color.accent)
if (modelData.selected) return Style.selectedFillFor(root.foreground, Color.accent)
return "transparent"
}
border.width: modelData.selected && !modelData.today ? Math.max(1, Style.spacing.hairline) : 0
border.color: Style.selectedBorderFor(root.foreground, Color.accent)
Text {
anchors.centerIn: parent
textFormat: Text.PlainText
text: cell.modelData.day
color: cell.dayColor
opacity: cell.modelData.inMonth || cell.modelData.today ? 1 : 0.3
font.family: root.calendar.fontFamily
font.pixelSize: Style.font.body
font.bold: cell.modelData.today || cell.modelData.selected
}
MouseArea {
id: dayMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.calendar.selectDay(cell.modelData.year, cell.modelData.month, cell.modelData.day)
}
}
}
+180
View File
@@ -0,0 +1,180 @@
.pragma library
var MS_PER_DAY = 86400000
var WEEKDAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]
function pad2(value) {
var n = Number(value)
return (n < 10 ? "0" : "") + n
}
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
}
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)]
}
function toggledWeekStart(index) {
return normalizedWeekStart(index, 1) === 1 ? 0 : 1
}
function weekdayOrder(weekStart) {
var start = normalizedWeekStart(weekStart, 1)
var order = []
for (var i = 0; i < 7; i++) order.push((start + i) % 7)
return order
}
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 isoWeekLiteral(year, month, day) {
return pad2(isoWeek(year, month, day))
}
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)
}
function daysInMonth(year, month) {
return new Date(year, Number(month) + 1, 0).getDate()
}
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)
}
function stepMonth(year, month, delta) {
var target = new Date(year, Number(month) + Number(delta), 1)
return { year: target.getFullYear(), month: target.getMonth() }
}
function stepDay(year, month, day, delta) {
var target = new Date(year, month, Number(day) + Number(delta))
return { year: target.getFullYear(), month: target.getMonth(), day: target.getDate() }
}
function clampDay(year, month, day) {
return Math.max(1, Math.min(Number(day), daysInMonth(year, month)))
}
function daysBetween(fromYear, fromMonth, fromDay, toYear, toMonth, toDay) {
return Math.round((Date.UTC(toYear, toMonth, toDay) - Date.UTC(fromYear, fromMonth, fromDay)) / MS_PER_DAY)
}
function relativeDayLabel(offset) {
if (offset === 0) return "Today"
if (offset === 1) return "Tomorrow"
if (offset === -1) return "Yesterday"
if (offset > 0) return "In " + offset + " days"
return (-offset) + " days ago"
}
function monthGrid(year, month, weekStart, todayKey, selectedKey) {
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 selected = String(selectedKey || "")
var weeks = []
for (var row = 0; row < 6; row++) {
var days = []
var thursday = null
for (var column = 0; column < 7; column++) {
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,
selected: key === selected
})
cursor.setDate(cursor.getDate() + 1)
}
var anchor = thursday || days[0]
weeks.push({
week: isoWeek(anchor.year, anchor.month, anchor.day),
days: days
})
}
return weeks
}
if (typeof module !== "undefined") {
module.exports = {
pad2: pad2,
dateKey: dateKey,
keyForDate: keyForDate,
normalizedWeekStart: normalizedWeekStart,
weekStartSettingName: weekStartSettingName,
toggledWeekStart: toggledWeekStart,
weekdayOrder: weekdayOrder,
isoWeek: isoWeek,
isoWeekLiteral: isoWeekLiteral,
dayOfYear: dayOfYear,
daysInYear: daysInYear,
daysInMonth: daysInMonth,
yearProgress: yearProgress,
yearProgressPercent: yearProgressPercent,
stepMonth: stepMonth,
stepDay: stepDay,
clampDay: clampDay,
daysBetween: daysBetween,
relativeDayLabel: relativeDayLabel,
monthGrid: monthGrid
}
}
+65
View File
@@ -0,0 +1,65 @@
import QtQuick
import qs.Commons
Item {
id: root
required property var calendar
readonly property int columns: 4
readonly property int rows: 3
readonly property real gap: calendar.cellSpacing * 2
readonly property color foreground: calendar.foreground
Grid {
anchors.fill: parent
columns: root.columns
spacing: root.gap
Repeater {
model: 12
Rectangle {
id: monthCell
required property int index
readonly property bool isCurrentMonth: root.calendar.today.getFullYear() === root.calendar.pickerYear
&& root.calendar.today.getMonth() === index
readonly property bool isViewedMonth: root.calendar.viewYear === root.calendar.pickerYear
&& root.calendar.viewMonth === index
readonly property bool isCursor: root.calendar.pickerMonth === index
width: (root.width - root.gap * (root.columns - 1)) / root.columns
height: (root.height - root.gap * (root.rows - 1)) / root.rows
radius: Style.cornerRadius
color: {
if (isCurrentMonth) return Style.selectedStateColor(root.foreground, Color.accent)
if (isCursor) return Style.hoverFillFor(root.foreground, Color.accent)
return "transparent"
}
border.width: isViewedMonth && !isCurrentMonth ? Math.max(1, Style.spacing.hairline) : 0
border.color: Style.selectedBorderFor(root.foreground, Color.accent)
Text {
anchors.centerIn: parent
textFormat: Text.PlainText
text: root.calendar.monthLabel(monthCell.index)
color: monthCell.isCurrentMonth ? Color.background : root.foreground
font.family: root.calendar.fontFamily
font.pixelSize: Style.font.body
font.letterSpacing: 1
font.bold: monthCell.isCurrentMonth || monthCell.isViewedMonth
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onContainsMouseChanged: if (containsMouse) root.calendar.pickerMonth = monthCell.index
onClicked: root.calendar.choosePickerMonth(root.calendar.pickerYear, monthCell.index)
}
}
}
}
}
+251
View File
@@ -0,0 +1,251 @@
import QtQuick
import qs.Commons
import "CalendarModel.js" as CalendarModel
Column {
id: root
property date today: new Date()
property int weekStart: CalendarModel.normalizedWeekStart(null, Qt.locale().firstDayOfWeek)
property color foreground: Color.foreground
property string fontFamily: Style.font.family
property bool showWeekNumbers: false
property bool navigationAtBottom: false
property string weekHeaderTooltip: ""
property real cellWidth: Style.space(36)
property real cellHeight: Style.space(28)
property real cellSpacing: Style.space(2)
property real weekColumnWidth: Style.space(32)
property real gutterWidth: Style.space(14)
property int viewYear: today.getFullYear()
property int viewMonth: today.getMonth()
property int selectedYear: today.getFullYear()
property int selectedMonth: today.getMonth()
property int selectedDay: today.getDate()
property bool pickingMonth: false
property int pickerYear: viewYear
property int pickerMonth: viewMonth
readonly property string todayKey: CalendarModel.keyForDate(today)
readonly property string selectedKey: CalendarModel.dateKey(selectedYear, selectedMonth, selectedDay)
readonly property bool selectionIsToday: selectedKey === todayKey
readonly property bool viewingCurrentMonth: viewYear === today.getFullYear() && viewMonth === today.getMonth()
readonly property date viewDate: new Date(viewYear, viewMonth, 1)
readonly property date selectedDate: new Date(selectedYear, selectedMonth, selectedDay)
readonly property int selectedOffset: CalendarModel.daysBetween(
today.getFullYear(), today.getMonth(), today.getDate(), selectedYear, selectedMonth, selectedDay)
readonly property var labelLocale: Qt.locale("en_US")
readonly property real gridWidth: body.width
readonly property color mutedForeground: Qt.darker(foreground, 1.5)
signal weekHeaderClicked()
spacing: Style.space(8)
function weekdayLabel(weekday) {
return String(labelLocale.dayName(weekday, Locale.ShortFormat)).toUpperCase()
}
function monthLabel(month) {
return String(labelLocale.monthName(month, Locale.ShortFormat)).toUpperCase()
}
function selectDay(year, month, day) {
selectedYear = year
selectedMonth = month
selectedDay = day
viewYear = year
viewMonth = month
pickingMonth = false
}
function goToToday() {
selectDay(today.getFullYear(), today.getMonth(), today.getDate())
}
function moveMonth(delta) {
var next = CalendarModel.stepMonth(viewYear, viewMonth, delta)
selectDay(next.year, next.month, CalendarModel.clampDay(next.year, next.month, selectedDay))
}
function moveYear(delta) {
if (pickingMonth) pickerYear += delta
else moveMonth(delta * 12)
}
function moveSelection(days) {
var next = CalendarModel.stepDay(selectedYear, selectedMonth, selectedDay, days)
selectDay(next.year, next.month, next.day)
}
function movePickerCursor(months) {
var next = CalendarModel.stepMonth(pickerYear, pickerMonth, months)
pickerYear = next.year
pickerMonth = next.month
}
function handleMove(dx, dy) {
if (pickingMonth) movePickerCursor(dx + dy * 4)
else moveSelection(dx + dy * 7)
}
function openPicker() {
pickerYear = viewYear
pickerMonth = viewMonth
pickingMonth = true
}
function togglePicker() {
if (pickingMonth) pickingMonth = false
else openPicker()
}
function choosePickerMonth(year, month) {
selectDay(year, month, CalendarModel.clampDay(year, month, selectedDay))
}
function activate() {
if (pickingMonth) choosePickerMonth(pickerYear, pickerMonth)
else goToToday()
}
function dismissPicker() {
if (!pickingMonth) return false
pickingMonth = false
return true
}
function selectedSummary() {
var format = selectedYear === today.getFullYear() ? "dddd, MMMM d" : "dddd, MMMM d, yyyy"
return Qt.formatDate(selectedDate, format)
+ " · Day " + CalendarModel.dayOfYear(selectedYear, selectedMonth, selectedDay)
+ " · " + CalendarModel.relativeDayLabel(selectedOffset)
}
MonthNavigation {
visible: !root.navigationAtBottom
}
Item {
width: root.width
height: body.height
Item {
id: body
anchors.horizontalCenter: parent.horizontalCenter
width: days.width
height: days.height
CalendarDays {
id: days
calendar: root
visible: !root.pickingMonth
}
CalendarMonthPicker {
anchors.fill: parent
calendar: root
visible: root.pickingMonth
}
WheelHandler {
onWheel: function(event) {
if (event.angleDelta.y === 0) return
var step = event.angleDelta.y > 0 ? -1 : 1
if (root.pickingMonth) root.pickerYear += step
else root.moveMonth(step)
}
}
}
}
Text {
width: root.width
horizontalAlignment: Text.AlignHCenter
textFormat: Text.PlainText
elide: Text.ElideRight
text: root.selectedSummary()
color: root.mutedForeground
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
}
MonthNavigation {
visible: root.navigationAtBottom
}
component MonthNavigation: Item {
width: root.width
height: visible ? navigationLabel.implicitHeight + Style.space(10) : 0
Item {
anchors.horizontalCenter: parent.horizontalCenter
width: body.width
height: parent.height
Rectangle {
anchors.centerIn: navigationLabel
width: navigationLabel.width
height: navigationLabel.implicitHeight + Style.space(6)
radius: Style.cornerRadius
color: navigationMouse.containsMouse ? Style.hoverFillFor(root.foreground, Color.accent) : "transparent"
}
Text {
id: navigationLabel
anchors.centerIn: parent
width: Style.space(150)
horizontalAlignment: Text.AlignHCenter
textFormat: Text.PlainText
text: root.pickingMonth
? String(root.pickerYear)
: Qt.formatDate(root.viewDate, "MMMM yyyy").toUpperCase()
color: navigationMouse.containsMouse
? Style.hoverStateColor(root.foreground, Color.accent)
: Qt.darker(root.foreground, 1.4)
font.family: root.fontFamily
font.pixelSize: Style.font.body
font.letterSpacing: 1
}
MouseArea {
id: navigationMouse
anchors.fill: navigationLabel
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.togglePicker()
PanelToolTip {
visible: navigationMouse.containsMouse
text: root.pickingMonth ? "Back to the days" : "Pick a month"
fontFamily: root.fontFamily
}
}
PanelActionButton {
anchors.left: parent.left
anchors.leftMargin: -Style.space(8)
anchors.verticalCenter: parent.verticalCenter
iconText: "󰅁"
tooltipText: root.pickingMonth ? "Previous year" : "Previous month"
foreground: root.foreground
fontFamily: root.fontFamily
onClicked: root.pickingMonth ? root.moveYear(-1) : root.moveMonth(-1)
}
PanelActionButton {
anchors.right: parent.right
anchors.rightMargin: -Style.space(8)
anchors.verticalCenter: parent.verticalCenter
iconText: "󰅂"
tooltipText: root.pickingMonth ? "Next year" : "Next month"
foreground: root.foreground
fontFamily: root.fontFamily
onClicked: root.pickingMonth ? root.moveYear(1) : root.moveMonth(1)
}
}
}
}
+4
View File
@@ -38,3 +38,7 @@ CardHeader 1.0 CardHeader.qml
StatRow 1.0 StatRow.qml StatRow 1.0 StatRow.qml
CardIconButton 1.0 CardIconButton.qml CardIconButton 1.0 CardIconButton.qml
Chip 1.0 Chip.qml Chip 1.0 Chip.qml
CalendarModel 1.0 CalendarModel.js
CalendarView 1.0 CalendarView.qml
CalendarDays 1.0 CalendarDays.qml
CalendarMonthPicker 1.0 CalendarMonthPicker.qml
+1 -1
View File
@@ -53,7 +53,7 @@ BarWidget {
} }
function formatted(date) { 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 // ---- 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. // Clock label formats and the memento mori bar. Calendar date math lives in
// Everything here is locale- and Qt-free so it can be unit tested under node // qs.Ui's CalendarModel.js, shared with the Quick Settings calendar.
// (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"]
// ---- Bar label formats. Right-clicking the clock walks these in order and // ---- 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 // 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] 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 // 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 // an actuarial table: the point of the bar is the reminder, not the
// arithmetic, and whoever wants a different number can say so. // 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) 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") { if (typeof module !== "undefined") {
module.exports = { 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, parseAge: parseAge,
parseBirthYear: parseBirthYear, parseBirthYear: parseBirthYear,
ageFromBirthYear: ageFromBirthYear, ageFromBirthYear: ageFromBirthYear,
parseLifeExpectancy: parseLifeExpectancy, parseLifeExpectancy: parseLifeExpectancy,
lifeProgress: lifeProgress, lifeProgress: lifeProgress,
lifeProgressPercent: lifeProgressPercent, lifeProgressPercent: lifeProgressPercent,
monthGrid: monthGrid,
stepMonth: stepMonth,
clockFormats: clockFormats, clockFormats: clockFormats,
clockFormatRing: clockFormatRing, clockFormatRing: clockFormatRing,
nextClockFormat: nextClockFormat, nextClockFormat: nextClockFormat
isoWeekLiteral: isoWeekLiteral
} }
} }
+44 -279
View File
@@ -8,9 +8,9 @@ import "Model.js" as Model
// sit beside the weather panel — same hero-over-detail composition, same // sit beside the weather panel — same hero-over-detail composition, same
// spacing scale, same small-caps labels. // spacing scale, same small-caps labels.
// //
// The grid is a read-out rather than a picker: today is the only marked // The grid itself is the shared CalendarView from qs.Ui: days can be
// day, and the only thing that moves is which month is on screen — // selected with the pointer or the arrow keys, the month label opens a
// chevrons, the scroll wheel, and the arrow keys all step it. // month picker, and today is filled with the accent color.
// //
// BarWidget.qml owns the bar label and hands this panel the button to // BarWidget.qml owns the bar label and hands this panel the button to
// anchor against. // anchor against.
@@ -33,20 +33,11 @@ Panel {
// ---- Today. SystemClock keeps this honest across midnight so the // ---- Today. SystemClock keeps this honest across midnight so the
// highlight rolls over without the panel being reopened. // highlight rolls over without the panel being reopened.
property date today: new Date() 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 // Pinned to today, not to the month being browsed — stepping through the
// calendar does not change how much of the year is gone. // 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 real yearDone: CalendarModel.yearProgress(today.getFullYear(), today.getMonth(), today.getDate())
readonly property int yearDonePercent: Model.yearProgressPercent(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 // 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 // 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 // starts out matching the rest of the desktop rather than a hardcoded
// convention. Clicking the grid's "W" heading writes the choice back to // convention. Clicking the grid's "W" heading writes the choice back to
// shell.json. // 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 // 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 // system locale. Where the week starts still is: that is a regional
// convention rather than a translation, and it stays overridable above. // convention rather than a translation, and it stays overridable above.
readonly property var labelLocale: Qt.locale("en_US") readonly property var labelLocale: Qt.locale("en_US")
readonly property string nextWeekStartLabel: labelLocale.dayName(Model.toggledWeekStart(weekStart), Locale.LongFormat) readonly property string nextWeekStartLabel: labelLocale.dayName(CalendarModel.toggledWeekStart(weekStart), Locale.LongFormat)
readonly property var weekdays: Model.weekdayOrder(weekStart)
readonly property var weeks: Model.monthGrid(viewYear, viewMonth, weekStart, todayKey)
// Guarded so the widget renders before the bar is injected (the bar-widget // Guarded so the widget renders before the bar is injected (the bar-widget
// contract instantiates it bare). // contract instantiates it bare).
readonly property color contentForeground: bar ? bar.foreground : Color.foreground readonly property color contentForeground: bar ? bar.foreground : Color.foreground
readonly property string contentFontFamily: bar ? bar.fontFamily : Style.font.family 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() { function open() {
refresh() refresh()
root.controller.show() root.controller.show()
@@ -127,22 +109,7 @@ Panel {
function refresh() { function refresh() {
root.today = new Date() root.today = new Date()
root.goToToday() calendar.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)
} }
// Applied locally first so the panel redraws on the click itself; the // Applied locally first so the panel redraws on the click itself; the
@@ -163,9 +130,9 @@ Panel {
} }
function setWeekStart(day) { function setWeekStart(day) {
var next = Model.normalizedWeekStart(day, root.weekStart) var next = CalendarModel.normalizedWeekStart(day, root.weekStart)
if (next === root.weekStart) return if (next === root.weekStart) return
persistSettings({ weekStartDay: Model.weekStartSettingName(next) }) persistSettings({ weekStartDay: CalendarModel.weekStartSettingName(next) })
} }
function startEditingLife() { function startEditingLife() {
@@ -216,22 +183,21 @@ Panel {
} }
function toggleWeekStart() { function toggleWeekStart() {
setWeekStart(Model.toggledWeekStart(root.weekStart)) setWeekStart(CalendarModel.toggledWeekStart(root.weekStart))
} }
// English short day names, matching the rest of the interface. function closeOrDismissPicker() {
function weekdayLabel(weekday) { if (!calendar.dismissPicker()) root.close()
return String(labelLocale.dayName(weekday, Locale.ShortFormat)).toUpperCase()
} }
SystemClock { SystemClock {
id: clock id: clock
precision: SystemClock.Minutes precision: SystemClock.Minutes
onDateChanged: { onDateChanged: {
if (Model.keyForDate(clock.date) === String(root.todayKey)) return if (CalendarModel.keyForDate(clock.date) === CalendarModel.keyForDate(root.today)) return
var followToday = root.viewingCurrentMonth var followToday = calendar.selectionIsToday
root.today = clock.date root.today = clock.date
if (followToday) root.goToToday() if (followToday) calendar.goToToday()
} }
} }
@@ -250,19 +216,17 @@ Panel {
id: keyCatcher id: keyCatcher
anchors.fill: parent anchors.fill: parent
blocked: root.editingLife blocked: root.editingLife
onMoveRequested: function(dx, dy) { onMoveRequested: function(dx, dy) { calendar.handleMove(dx, dy) }
if (dx !== 0) root.moveMonth(dx) onActivateRequested: calendar.activate()
if (dy !== 0) root.moveYear(dy) onCloseRequested: root.closeOrDismissPicker()
}
onActivateRequested: root.goToToday()
onCloseRequested: root.close()
onTabRequested: function(direction) { root.switchPanel(direction) } onTabRequested: function(direction) { root.switchPanel(direction) }
onTextKey: function(t) { onTextKey: function(t) {
if (t === "[") root.moveMonth(-1) if (t === "[") calendar.moveMonth(-1)
else if (t === "]") root.moveMonth(1) else if (t === "]") calendar.moveMonth(1)
else if (t === "{") root.moveYear(-1) else if (t === "{") calendar.moveYear(-1)
else if (t === "}") root.moveYear(1) else if (t === "}") calendar.moveYear(1)
else if (t === "t" || t === "T") root.goToToday() else if (t === "t" || t === "T") calendar.goToToday()
else if (t === "m" || t === "M") calendar.togglePicker()
else if (t === "w" || t === "W") root.toggleWeekStart() 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 // Never narrower than the grid. The popup width is capped to what
// the screen allows, and a fixed seven-column grid would otherwise // the screen allows, and a fixed seven-column grid would otherwise
// lose its last days off the edge instead of scrolling. // 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) spacing: Style.space(8)
// ---- Hero: today, centered. Once the view has stepped back // ---- Hero: today, centered. Once the view has stepped back
@@ -331,10 +295,10 @@ Panel {
y: heroRow.y y: heroRow.y
width: heroRow.width width: heroRow.width
height: heroRow.height height: heroRow.height
enabled: !root.viewingCurrentMonth enabled: !calendar.selectionIsToday || calendar.pickingMonth
hoverEnabled: enabled hoverEnabled: enabled
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: root.goToToday() onClicked: calendar.goToToday()
PanelToolTip { PanelToolTip {
visible: heroMouse.containsMouse visible: heroMouse.containsMouse
@@ -355,7 +319,7 @@ Panel {
id: yearBlock id: yearBlock
y: Style.space(6) y: Style.space(6)
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
width: gridColumn.width width: calendar.gridWidth
height: Math.max(yearLabel.implicitHeight, Style.space(10)) height: Math.max(yearLabel.implicitHeight, Style.space(10))
TapHandler { TapHandler {
@@ -474,7 +438,7 @@ Panel {
Item { Item {
id: lifeBlock id: lifeBlock
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
width: gridColumn.width width: calendar.gridWidth
height: Math.max(lifeLabel.implicitHeight, Style.space(10)) height: Math.max(lifeLabel.implicitHeight, Style.space(10))
Text { Text {
@@ -538,222 +502,23 @@ Panel {
} }
} }
// ---- Month grid: week numbers down a gutter on the left, then CalendarView {
// the seven day columns. Always six rows, so the popup is id: calendar
// exactly as tall in February as it is in August.
Item {
width: parent.width width: parent.width
height: gridColumn.y + gridColumn.height topPadding: Style.space(10)
today: root.today
WheelHandler { weekStart: root.weekStart
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 foreground: root.contentForeground
fontFamily: root.contentFontFamily fontFamily: root.contentFontFamily
onClicked: root.moveMonth(-1) showWeekNumbers: true
} navigationAtBottom: true
weekHeaderTooltip: "Start weeks on " + root.nextWeekStartLabel
PanelActionButton { cellWidth: Style.space(52)
anchors.right: parent.right cellHeight: Style.space(34)
anchors.rightMargin: -Style.space(8) cellSpacing: Style.space(2)
anchors.verticalCenter: parent.verticalCenter weekColumnWidth: Style.space(32)
iconText: "󰅂" gutterWidth: Style.space(14)
tooltipText: "Next month" onWeekHeaderClicked: root.toggleWeekStart()
foreground: root.contentForeground
fontFamily: root.contentFontFamily
onClicked: root.moveMonth(1)
}
}
} }
} }
} }
@@ -1,115 +1,42 @@
import QtQuick import QtQuick
import Quickshell.Io
import qs.Commons import qs.Commons
import qs.Ui import qs.Ui
Item { Item {
id: root id: root
property int monthOffset: 0
property string monthName: ""
property string monthGrid: ""
property bool polling: false property bool polling: false
implicitHeight: layout.implicitHeight implicitHeight: calendar.implicitHeight
function loadMonth() { function refreshToday() {
if (monthProbe.running) return var now = new Date()
monthProbe.running = true if (CalendarModel.keyForDate(now) === CalendarModel.keyForDate(calendar.today)) return
var followToday = calendar.selectionIsToday
calendar.today = now
if (followToday) calendar.goToToday()
} }
function shiftMonth(delta) { onPollingChanged: {
root.monthOffset += delta if (!polling) return
root.loadMonth() calendar.today = new Date()
} calendar.goToToday()
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")
}
}
} }
Timer { Timer {
interval: 3600000 interval: 60000
running: root.polling running: root.polling
repeat: true repeat: true
onTriggered: if (root.monthOffset === 0) root.loadMonth() onTriggered: root.refreshToday()
} }
Component.onCompleted: root.loadMonth() CalendarView {
id: calendar
WheelHandler {
onWheel: function(event) {
root.shiftMonth(event.angleDelta.y > 0 ? -1 : 1)
}
}
Column {
id: layout
width: parent.width width: parent.width
spacing: Style.spaceReal(6) foreground: Color.foreground
fontFamily: Style.font.family
Item { cellSpacing: Style.space(2)
width: parent.width cellWidth: Math.floor((width - cellSpacing * 6) / 7)
implicitHeight: Math.max(monthLabel.implicitHeight, navigation.implicitHeight) cellHeight: Style.space(24)
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
}
} }
} }