Преглед на файлове

Add a professional split Scheduler page with interactive calendar navigation and date-based meeting lists.

This keeps Scheduler integrated with existing meeting data refresh flow so selecting days, navigating months, and loading Zoom meetings stay in sync.

Made-with: Cursor
huzaifahayat12 преди 4 дни
родител
ревизия
4e64fe8cb7
променени са 1 файла, в които са добавени 391 реда и са изтрити 9 реда
  1. 391 9
      zoom_app/ViewController.swift

+ 391 - 9
zoom_app/ViewController.swift

@@ -211,6 +211,16 @@ class ViewController: NSViewController {
211 211
     private weak var homeSearchField: NSTextField?
212 212
     private weak var homeSearchPill: NSView?
213 213
     private weak var homeSettingsView: NSView?
214
+    private weak var schedulerRootView: NSView?
215
+    private weak var schedulerMonthLabel: NSTextField?
216
+    private weak var schedulerCalendarGridStack: NSStackView?
217
+    private weak var schedulerSelectedDateLabel: NSTextField?
218
+    private weak var schedulerMeetingsListStack: NSStackView?
219
+    private weak var schedulerMeetingsEmptyLabel: NSTextField?
220
+    private weak var schedulerTodayButton: NSButton?
221
+    private var schedulerDayButtons: [Date: NSButton] = [:]
222
+    private var schedulerDateByButtonTag: [Int: Date] = [:]
223
+    private var nextSchedulerButtonTag: Int = 1
214 224
     private weak var settingsDarkModeSwitch: NSSwitch?
215 225
     private weak var settingsUpgradeButton: NSButton?
216 226
     private weak var settingsRestoreButton: NSButton?
@@ -244,6 +254,12 @@ class ViewController: NSViewController {
244 254
     private var lastKnownPremiumAccess = false
245 255
     private var allScheduledMeetings: [ScheduledMeeting] = []
246 256
     private var selectedMeetingsDayStart: Date = Calendar.current.startOfDay(for: Date())
257
+    private var schedulerVisibleMonthStart: Date = {
258
+        let calendar = Calendar.current
259
+        let today = Date()
260
+        let components = calendar.dateComponents([.year, .month], from: today)
261
+        return calendar.date(from: components) ?? calendar.startOfDay(for: today)
262
+    }()
247 263
     private var selectedHomeSidebarItem: String = "Home"
248 264
     private var homeSidebarRowViews: [String: NSView] = [:]
249 265
     private var homeSidebarIconViews: [String: NSImageView] = [:]
@@ -1586,6 +1602,8 @@ class ViewController: NSViewController {
1586 1602
                 meetingsStatusLabel?.stringValue = "Upcoming meetings"
1587 1603
                 emptyMeetingLabel?.stringValue = "No meetings match your search."
1588 1604
             }
1605
+            applySchedulerMeetingsForSelectedDate()
1606
+            renderSchedulerCalendarGrid()
1589 1607
             return
1590 1608
         }
1591 1609
 
@@ -1599,11 +1617,27 @@ class ViewController: NSViewController {
1599 1617
                 card.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
1600 1618
             }
1601 1619
         }
1620
+        applySchedulerMeetingsForSelectedDate()
1621
+        renderSchedulerCalendarGrid()
1622
+    }
1623
+
1624
+    private func meetingsForDay(_ dayStart: Date) -> [ScheduledMeeting] {
1625
+        let calendar = Calendar.current
1626
+        let safeDayStart = calendar.startOfDay(for: dayStart)
1627
+        let dayEnd = calendar.date(byAdding: .day, value: 1, to: safeDayStart) ?? safeDayStart.addingTimeInterval(60 * 60 * 24)
1628
+        return allScheduledMeetings
1629
+            .filter { meeting in
1630
+                meeting.start >= safeDayStart && meeting.start < dayEnd
1631
+            }
1632
+            .sorted(by: { $0.start < $1.start })
1602 1633
     }
1603 1634
 
1604 1635
     @MainActor
1605 1636
     private func setSelectedMeetingsDayStart(_ newDayStart: Date) {
1606
-        selectedMeetingsDayStart = Calendar.current.startOfDay(for: newDayStart)
1637
+        let calendar = Calendar.current
1638
+        selectedMeetingsDayStart = calendar.startOfDay(for: newDayStart)
1639
+        let monthComponents = calendar.dateComponents([.year, .month], from: selectedMeetingsDayStart)
1640
+        schedulerVisibleMonthStart = calendar.date(from: monthComponents) ?? selectedMeetingsDayStart
1607 1641
         updateMeetingsDayUI()
1608 1642
         applyFilteredMeetings()
1609 1643
     }
@@ -1618,6 +1652,11 @@ class ViewController: NSViewController {
1618 1652
         let isToday = Calendar.current.isDate(dayStart, inSameDayAs: Date())
1619 1653
         meetingsTodayButton?.isEnabled = isToday == false
1620 1654
         meetingsTodayButton?.alphaValue = isToday ? 0.55 : 1.0
1655
+        schedulerTodayButton?.isEnabled = isToday == false
1656
+        schedulerTodayButton?.alphaValue = isToday ? 0.55 : 1.0
1657
+        updateSchedulerDateHeader()
1658
+        applySchedulerMeetingsForSelectedDate()
1659
+        renderSchedulerCalendarGrid()
1621 1660
     }
1622 1661
 
1623 1662
     private func meetingsDayDisplayName(for dayStart: Date) -> String {
@@ -1654,6 +1693,166 @@ class ViewController: NSViewController {
1654 1693
         }
1655 1694
     }
1656 1695
 
1696
+    private func updateSchedulerDateHeader() {
1697
+        let formatter = DateFormatter()
1698
+        formatter.dateFormat = "MMMM yyyy"
1699
+        schedulerMonthLabel?.stringValue = formatter.string(from: schedulerVisibleMonthStart)
1700
+
1701
+        let selectedFormatter = DateFormatter()
1702
+        selectedFormatter.dateFormat = "EEEE, MMM d"
1703
+        schedulerSelectedDateLabel?.stringValue = selectedFormatter.string(from: selectedMeetingsDayStart)
1704
+    }
1705
+
1706
+    @MainActor
1707
+    private func applySchedulerMeetingsForSelectedDate() {
1708
+        guard let stack = schedulerMeetingsListStack else { return }
1709
+        stack.arrangedSubviews.forEach { view in
1710
+            stack.removeArrangedSubview(view)
1711
+            view.removeFromSuperview()
1712
+        }
1713
+
1714
+        let meetings = meetingsForDay(selectedMeetingsDayStart)
1715
+        if meetings.isEmpty {
1716
+            schedulerMeetingsEmptyLabel?.isHidden = false
1717
+            schedulerMeetingsEmptyLabel?.stringValue = "No meetings scheduled for \(meetingsDayDisplayName(for: selectedMeetingsDayStart))."
1718
+            return
1719
+        }
1720
+
1721
+        schedulerMeetingsEmptyLabel?.isHidden = true
1722
+        for meeting in meetings {
1723
+            let card = makeMeetingRowCard(meeting)
1724
+            stack.addArrangedSubview(card)
1725
+            if card.constraints.contains(where: { $0.firstAttribute == .width }) == false {
1726
+                card.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
1727
+            }
1728
+        }
1729
+    }
1730
+
1731
+    @MainActor
1732
+    private func renderSchedulerCalendarGrid() {
1733
+        guard let gridStack = schedulerCalendarGridStack else { return }
1734
+        schedulerDayButtons = [:]
1735
+        schedulerDateByButtonTag = [:]
1736
+        nextSchedulerButtonTag = 1
1737
+        gridStack.arrangedSubviews.forEach { row in
1738
+            gridStack.removeArrangedSubview(row)
1739
+            row.removeFromSuperview()
1740
+        }
1741
+
1742
+        let calendar = Calendar.current
1743
+        let monthStart = schedulerVisibleMonthStart
1744
+        let monthRange = calendar.range(of: .day, in: .month, for: monthStart) ?? 1..<32
1745
+        let monthWeekday = calendar.component(.weekday, from: monthStart)
1746
+        let leadingSlots = monthWeekday - 1
1747
+        let totalSlots = 42
1748
+
1749
+        for week in 0..<6 {
1750
+            let row = NSStackView()
1751
+            row.orientation = .horizontal
1752
+            row.distribution = .fillEqually
1753
+            row.spacing = 6
1754
+            row.translatesAutoresizingMaskIntoConstraints = false
1755
+            gridStack.addArrangedSubview(row)
1756
+
1757
+            for weekday in 0..<7 {
1758
+                let cellIndex = (week * 7) + weekday
1759
+                let dayOffset = cellIndex - leadingSlots
1760
+                let dayDate = calendar.date(byAdding: .day, value: dayOffset, to: monthStart) ?? monthStart
1761
+                let dayNumber = calendar.component(.day, from: dayDate)
1762
+                let inCurrentMonth = monthRange.contains(dayNumber) && calendar.isDate(dayDate, equalTo: monthStart, toGranularity: .month)
1763
+                let hasMeetings = meetingsForDay(dayDate).isEmpty == false
1764
+                let button = makeSchedulerDayCell(
1765
+                    day: dayNumber,
1766
+                    date: dayDate,
1767
+                    inCurrentMonth: inCurrentMonth,
1768
+                    selected: calendar.isDate(dayDate, inSameDayAs: selectedMeetingsDayStart),
1769
+                    isToday: calendar.isDateInToday(dayDate),
1770
+                    hasMeetings: hasMeetings
1771
+                )
1772
+                row.addArrangedSubview(button)
1773
+                schedulerDayButtons[calendar.startOfDay(for: dayDate)] = button
1774
+            }
1775
+        }
1776
+
1777
+        // Keep a consistent 6x7 grid footprint.
1778
+        if totalSlots == 42 {
1779
+            gridStack.needsLayout = true
1780
+        }
1781
+    }
1782
+
1783
+    private func makeSchedulerDayCell(
1784
+        day: Int,
1785
+        date: Date,
1786
+        inCurrentMonth: Bool,
1787
+        selected: Bool,
1788
+        isToday: Bool,
1789
+        hasMeetings: Bool
1790
+    ) -> NSButton {
1791
+        let button = NSButton(title: "\(day)", target: self, action: #selector(schedulerDayTapped(_:)))
1792
+        button.isBordered = false
1793
+        button.bezelStyle = .shadowlessSquare
1794
+        button.focusRingType = .none
1795
+        button.font = .systemFont(ofSize: 16, weight: selected ? .semibold : .regular)
1796
+        button.alignment = .center
1797
+        button.wantsLayer = true
1798
+        button.layer?.cornerRadius = 16
1799
+        button.layer?.borderWidth = selected ? 1.5 : (hasMeetings ? 1 : 0)
1800
+        button.layer?.backgroundColor = selected
1801
+            ? accentBlue.withAlphaComponent(palette.isDarkMode ? 0.92 : 0.16).cgColor
1802
+            : NSColor.clear.cgColor
1803
+        if selected {
1804
+            button.layer?.borderColor = accentBlue.cgColor
1805
+        } else if isToday {
1806
+            button.layer?.borderColor = accentOrange.withAlphaComponent(0.8).cgColor
1807
+            button.layer?.borderWidth = 1
1808
+        } else if hasMeetings {
1809
+            button.layer?.borderColor = accentBlue.withAlphaComponent(0.45).cgColor
1810
+        } else {
1811
+            button.layer?.borderColor = NSColor.clear.cgColor
1812
+        }
1813
+        button.contentTintColor = selected
1814
+            ? primaryText
1815
+            : (inCurrentMonth ? primaryText : mutedText.withAlphaComponent(0.75))
1816
+        let tag = nextSchedulerButtonTag
1817
+        nextSchedulerButtonTag += 1
1818
+        button.tag = tag
1819
+        schedulerDateByButtonTag[tag] = Calendar.current.startOfDay(for: date)
1820
+        button.translatesAutoresizingMaskIntoConstraints = false
1821
+        button.heightAnchor.constraint(equalToConstant: 34).isActive = true
1822
+        return button
1823
+    }
1824
+
1825
+    @objc private func schedulerDayTapped(_ sender: NSButton) {
1826
+        guard let selectedDate = schedulerDateByButtonTag[sender.tag] else { return }
1827
+        Task { @MainActor in
1828
+            self.setSelectedMeetingsDayStart(selectedDate)
1829
+        }
1830
+    }
1831
+
1832
+    @objc private func schedulerPrevMonthTapped() {
1833
+        let calendar = Calendar.current
1834
+        let prev = calendar.date(byAdding: .month, value: -1, to: schedulerVisibleMonthStart) ?? schedulerVisibleMonthStart
1835
+        let monthComponents = calendar.dateComponents([.year, .month], from: prev)
1836
+        schedulerVisibleMonthStart = calendar.date(from: monthComponents) ?? prev
1837
+        updateSchedulerDateHeader()
1838
+        renderSchedulerCalendarGrid()
1839
+    }
1840
+
1841
+    @objc private func schedulerNextMonthTapped() {
1842
+        let calendar = Calendar.current
1843
+        let next = calendar.date(byAdding: .month, value: 1, to: schedulerVisibleMonthStart) ?? schedulerVisibleMonthStart
1844
+        let monthComponents = calendar.dateComponents([.year, .month], from: next)
1845
+        schedulerVisibleMonthStart = calendar.date(from: monthComponents) ?? next
1846
+        updateSchedulerDateHeader()
1847
+        renderSchedulerCalendarGrid()
1848
+    }
1849
+
1850
+    @objc private func schedulerTodayTapped() {
1851
+        Task { @MainActor in
1852
+            self.setSelectedMeetingsDayStart(Date())
1853
+        }
1854
+    }
1855
+
1657 1856
     private func loadScheduledMeetings() async {
1658 1857
         if isLoadingMeetings { return }
1659 1858
         isLoadingMeetings = true
@@ -1682,7 +1881,7 @@ class ViewController: NSViewController {
1682 1881
                 } else if case ZoomOAuthError.missingClientSecret = error {
1683 1882
                     self.meetingsStatusLabel?.stringValue = "Zoom OAuth app not configured."
1684 1883
                     self.promptForZoomOAuthCredentialsIfNeeded()
1685
-                } else if case ZoomOAuthError.missingRequiredScope(let scopeMessage) = error {
1884
+                } else if case ZoomOAuthError.missingRequiredScope = error {
1686 1885
                     self.zoomOAuth.clearSavedTokens()
1687 1886
                     self.meetingsStatusLabel?.stringValue = "Zoom permissions are missing. Update your Zoom app scopes, then sign in again."
1688 1887
                 } else if case ZoomOAuthError.rateLimited(let retryAfterSeconds) = error {
@@ -3163,6 +3362,108 @@ class ViewController: NSViewController {
3163 3362
         let settingsView = makeSettingsView()
3164 3363
         settingsView.isHidden = selectedHomeSidebarItem != "Settings"
3165 3364
 
3365
+        let schedulerRoot = NSView()
3366
+        schedulerRoot.isHidden = selectedHomeSidebarItem != "Scheduler"
3367
+
3368
+        let schedulerSplit = NSStackView()
3369
+        schedulerSplit.orientation = .horizontal
3370
+        schedulerSplit.spacing = 14
3371
+        schedulerSplit.distribution = .fillEqually
3372
+        schedulerSplit.alignment = .top
3373
+
3374
+        let schedulerCalendarPanel = NSView()
3375
+        schedulerCalendarPanel.wantsLayer = true
3376
+        schedulerCalendarPanel.layer?.backgroundColor = secondaryCardBackground.withAlphaComponent(0.94).cgColor
3377
+        schedulerCalendarPanel.layer?.cornerRadius = 16
3378
+        schedulerCalendarPanel.layer?.borderWidth = 1
3379
+        schedulerCalendarPanel.layer?.borderColor = palette.inputBorder.cgColor
3380
+
3381
+        let schedulerMeetingsPanel = NSView()
3382
+        schedulerMeetingsPanel.wantsLayer = true
3383
+        schedulerMeetingsPanel.layer?.backgroundColor = secondaryCardBackground.withAlphaComponent(0.94).cgColor
3384
+        schedulerMeetingsPanel.layer?.cornerRadius = 16
3385
+        schedulerMeetingsPanel.layer?.borderWidth = 1
3386
+        schedulerMeetingsPanel.layer?.borderColor = palette.inputBorder.cgColor
3387
+
3388
+        let schedulerCalendarTopRow = NSStackView()
3389
+        schedulerCalendarTopRow.orientation = .horizontal
3390
+        schedulerCalendarTopRow.alignment = .centerY
3391
+        schedulerCalendarTopRow.distribution = .fill
3392
+        schedulerCalendarTopRow.spacing = 10
3393
+
3394
+        let schedulerConnectLabel = makeLabel("Connect calendar", size: 18, color: accentBlue, weight: .semibold, centered: false)
3395
+        let schedulerPlusButton = NSButton(title: "", target: nil, action: nil)
3396
+        schedulerPlusButton.isBordered = false
3397
+        schedulerPlusButton.bezelStyle = .shadowlessSquare
3398
+        schedulerPlusButton.focusRingType = .none
3399
+        schedulerPlusButton.wantsLayer = true
3400
+        schedulerPlusButton.layer?.backgroundColor = accentBlue.cgColor
3401
+        schedulerPlusButton.layer?.cornerRadius = 14
3402
+        schedulerPlusButton.contentTintColor = .white
3403
+        schedulerPlusButton.toolTip = "Connect calendar"
3404
+        if let image = NSImage(systemSymbolName: "plus", accessibilityDescription: "Connect calendar") {
3405
+            schedulerPlusButton.image = image
3406
+            schedulerPlusButton.imagePosition = .imageOnly
3407
+        }
3408
+        schedulerPlusButton.translatesAutoresizingMaskIntoConstraints = false
3409
+        schedulerPlusButton.widthAnchor.constraint(equalToConstant: 28).isActive = true
3410
+        schedulerPlusButton.heightAnchor.constraint(equalToConstant: 28).isActive = true
3411
+
3412
+        let schedulerMonthRow = NSStackView()
3413
+        schedulerMonthRow.orientation = .horizontal
3414
+        schedulerMonthRow.alignment = .centerY
3415
+        schedulerMonthRow.spacing = 8
3416
+
3417
+        let schedulerPrevMonthButton = makeNavGlyphButton(symbol: "chevron.left", action: #selector(schedulerPrevMonthTapped), dimension: 18, pointSize: 10, toolTip: "Previous month")
3418
+        let schedulerMonthLabel = makeLabel("-", size: 30, color: primaryText, weight: .semibold, centered: false)
3419
+        let schedulerNextMonthButton = makeNavGlyphButton(symbol: "chevron.right", action: #selector(schedulerNextMonthTapped), dimension: 18, pointSize: 10, toolTip: "Next month")
3420
+        [schedulerPrevMonthButton, schedulerMonthLabel, schedulerNextMonthButton].forEach { schedulerMonthRow.addArrangedSubview($0) }
3421
+
3422
+        let schedulerWeekdaysRow = NSStackView()
3423
+        schedulerWeekdaysRow.orientation = .horizontal
3424
+        schedulerWeekdaysRow.distribution = .fillEqually
3425
+        schedulerWeekdaysRow.spacing = 4
3426
+        let weekdaySymbols = ["S", "M", "T", "W", "T", "F", "S"]
3427
+        for symbol in weekdaySymbols {
3428
+            let weekdayLabel = makeLabel(symbol, size: 12, color: mutedText, weight: .medium, centered: true)
3429
+            schedulerWeekdaysRow.addArrangedSubview(weekdayLabel)
3430
+        }
3431
+
3432
+        let schedulerCalendarGrid = NSStackView()
3433
+        schedulerCalendarGrid.orientation = .vertical
3434
+        schedulerCalendarGrid.distribution = .fillEqually
3435
+        schedulerCalendarGrid.spacing = 6
3436
+
3437
+        let schedulerRightTopRow = NSStackView()
3438
+        schedulerRightTopRow.orientation = .horizontal
3439
+        schedulerRightTopRow.alignment = .centerY
3440
+        schedulerRightTopRow.spacing = 10
3441
+
3442
+        let schedulerSelectedDateLabel = makeLabel("-", size: 30, color: primaryText, weight: .semibold, centered: false)
3443
+        let schedulerTodayButton = makeMeetingsDayChipButton(title: "Today", symbol: "calendar", action: #selector(schedulerTodayTapped))
3444
+        schedulerTodayButton.toolTip = "Jump to today"
3445
+        [schedulerTodayButton].forEach { schedulerRightTopRow.addArrangedSubview($0) }
3446
+
3447
+        let schedulerMeetingsHeader = makeLabel("Scheduled meetings", size: 13, color: secondaryText, weight: .medium, centered: false)
3448
+
3449
+        let schedulerMeetingsScroll = NSScrollView()
3450
+        schedulerMeetingsScroll.drawsBackground = false
3451
+        schedulerMeetingsScroll.hasVerticalScroller = true
3452
+        schedulerMeetingsScroll.hasHorizontalScroller = false
3453
+        schedulerMeetingsScroll.autohidesScrollers = true
3454
+        let schedulerMeetingsDocument = FlippedView()
3455
+        let schedulerMeetingsStack = NSStackView()
3456
+        schedulerMeetingsStack.orientation = .vertical
3457
+        schedulerMeetingsStack.spacing = 12
3458
+        schedulerMeetingsStack.alignment = .leading
3459
+        schedulerMeetingsStack.setContentHuggingPriority(.required, for: .vertical)
3460
+        schedulerMeetingsStack.setContentCompressionResistancePriority(.required, for: .vertical)
3461
+        schedulerMeetingsScroll.documentView = schedulerMeetingsDocument
3462
+        schedulerMeetingsDocument.addSubview(schedulerMeetingsStack)
3463
+
3464
+        let schedulerEmptyLabel = makeLabel("No meetings scheduled for this date.", size: 15, color: secondaryText, weight: .regular, centered: true)
3465
+        schedulerEmptyLabel.isHidden = true
3466
+
3166 3467
         let contentColumn = NSView()
3167 3468
         contentColumn.translatesAutoresizingMaskIntoConstraints = false
3168 3469
         content.addSubview(topBar)
@@ -3181,10 +3482,20 @@ class ViewController: NSViewController {
3181 3482
         [searchIcon, searchField, searchHintLabel].forEach {
3182 3483
             searchPill.addSubview($0)
3183 3484
         }
3184
-        [welcome, timeTitle, dateTitle, actions, panel, panelHeader, meetingsStatus, meetingsDayNav, noMeeting, meetingsScrollView, refreshMeetingsButton, placeholder, settingsView].forEach {
3485
+        [welcome, timeTitle, dateTitle, actions, panel, panelHeader, meetingsStatus, meetingsDayNav, noMeeting, meetingsScrollView, refreshMeetingsButton, placeholder, settingsView, schedulerRoot].forEach {
3185 3486
             $0.translatesAutoresizingMaskIntoConstraints = false
3186 3487
             contentColumn.addSubview($0)
3187 3488
         }
3489
+        [schedulerSplit, schedulerCalendarPanel, schedulerMeetingsPanel, schedulerCalendarTopRow, schedulerConnectLabel, schedulerPlusButton, schedulerMonthRow, schedulerWeekdaysRow, schedulerCalendarGrid, schedulerRightTopRow, schedulerSelectedDateLabel, schedulerTodayButton, schedulerMeetingsHeader, schedulerMeetingsScroll, schedulerEmptyLabel, schedulerMeetingsDocument, schedulerMeetingsStack].forEach {
3490
+            $0.translatesAutoresizingMaskIntoConstraints = false
3491
+        }
3492
+        schedulerRoot.addSubview(schedulerSplit)
3493
+        [schedulerCalendarPanel, schedulerMeetingsPanel].forEach { schedulerSplit.addArrangedSubview($0) }
3494
+        [schedulerCalendarTopRow, schedulerMonthRow, schedulerWeekdaysRow, schedulerCalendarGrid].forEach { schedulerCalendarPanel.addSubview($0) }
3495
+        schedulerCalendarTopRow.addArrangedSubview(schedulerConnectLabel)
3496
+        schedulerCalendarTopRow.addArrangedSubview(NSView())
3497
+        schedulerCalendarTopRow.addArrangedSubview(schedulerPlusButton)
3498
+        [schedulerSelectedDateLabel, schedulerRightTopRow, schedulerMeetingsHeader, schedulerMeetingsScroll, schedulerEmptyLabel].forEach { schedulerMeetingsPanel.addSubview($0) }
3188 3499
         topBar.translatesAutoresizingMaskIntoConstraints = false
3189 3500
         topBarDivider.translatesAutoresizingMaskIntoConstraints = false
3190 3501
         meetingsDocument.translatesAutoresizingMaskIntoConstraints = false
@@ -3292,7 +3603,60 @@ class ViewController: NSViewController {
3292 3603
             settingsView.topAnchor.constraint(equalTo: contentColumn.topAnchor),
3293 3604
             settingsView.leadingAnchor.constraint(equalTo: contentColumn.leadingAnchor),
3294 3605
             settingsView.trailingAnchor.constraint(equalTo: contentColumn.trailingAnchor),
3295
-            settingsView.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor)
3606
+            settingsView.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor),
3607
+
3608
+            schedulerRoot.topAnchor.constraint(equalTo: contentColumn.topAnchor, constant: 8),
3609
+            schedulerRoot.leadingAnchor.constraint(equalTo: contentColumn.leadingAnchor, constant: 8),
3610
+            schedulerRoot.trailingAnchor.constraint(equalTo: contentColumn.trailingAnchor, constant: -8),
3611
+            schedulerRoot.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor, constant: -8),
3612
+
3613
+            schedulerSplit.topAnchor.constraint(equalTo: schedulerRoot.topAnchor),
3614
+            schedulerSplit.leadingAnchor.constraint(equalTo: schedulerRoot.leadingAnchor),
3615
+            schedulerSplit.trailingAnchor.constraint(equalTo: schedulerRoot.trailingAnchor),
3616
+            schedulerSplit.bottomAnchor.constraint(equalTo: schedulerRoot.bottomAnchor),
3617
+
3618
+            schedulerCalendarTopRow.topAnchor.constraint(equalTo: schedulerCalendarPanel.topAnchor, constant: 18),
3619
+            schedulerCalendarTopRow.leadingAnchor.constraint(equalTo: schedulerCalendarPanel.leadingAnchor, constant: 16),
3620
+            schedulerCalendarTopRow.trailingAnchor.constraint(equalTo: schedulerCalendarPanel.trailingAnchor, constant: -16),
3621
+
3622
+            schedulerMonthRow.topAnchor.constraint(equalTo: schedulerCalendarTopRow.bottomAnchor, constant: 18),
3623
+            schedulerMonthRow.leadingAnchor.constraint(equalTo: schedulerCalendarPanel.leadingAnchor, constant: 16),
3624
+            schedulerMonthRow.trailingAnchor.constraint(lessThanOrEqualTo: schedulerCalendarPanel.trailingAnchor, constant: -16),
3625
+
3626
+            schedulerWeekdaysRow.topAnchor.constraint(equalTo: schedulerMonthRow.bottomAnchor, constant: 14),
3627
+            schedulerWeekdaysRow.leadingAnchor.constraint(equalTo: schedulerCalendarPanel.leadingAnchor, constant: 16),
3628
+            schedulerWeekdaysRow.trailingAnchor.constraint(equalTo: schedulerCalendarPanel.trailingAnchor, constant: -16),
3629
+
3630
+            schedulerCalendarGrid.topAnchor.constraint(equalTo: schedulerWeekdaysRow.bottomAnchor, constant: 10),
3631
+            schedulerCalendarGrid.leadingAnchor.constraint(equalTo: schedulerCalendarPanel.leadingAnchor, constant: 16),
3632
+            schedulerCalendarGrid.trailingAnchor.constraint(equalTo: schedulerCalendarPanel.trailingAnchor, constant: -16),
3633
+            schedulerCalendarGrid.bottomAnchor.constraint(equalTo: schedulerCalendarPanel.bottomAnchor, constant: -16),
3634
+            schedulerCalendarGrid.heightAnchor.constraint(greaterThanOrEqualToConstant: 280),
3635
+
3636
+            schedulerSelectedDateLabel.topAnchor.constraint(equalTo: schedulerMeetingsPanel.topAnchor, constant: 18),
3637
+            schedulerSelectedDateLabel.leadingAnchor.constraint(equalTo: schedulerMeetingsPanel.leadingAnchor, constant: 16),
3638
+            schedulerSelectedDateLabel.trailingAnchor.constraint(lessThanOrEqualTo: schedulerRightTopRow.leadingAnchor, constant: -10),
3639
+
3640
+            schedulerRightTopRow.centerYAnchor.constraint(equalTo: schedulerSelectedDateLabel.centerYAnchor),
3641
+            schedulerRightTopRow.trailingAnchor.constraint(equalTo: schedulerMeetingsPanel.trailingAnchor, constant: -16),
3642
+
3643
+            schedulerMeetingsHeader.topAnchor.constraint(equalTo: schedulerSelectedDateLabel.bottomAnchor, constant: 12),
3644
+            schedulerMeetingsHeader.leadingAnchor.constraint(equalTo: schedulerMeetingsPanel.leadingAnchor, constant: 16),
3645
+            schedulerMeetingsHeader.trailingAnchor.constraint(equalTo: schedulerMeetingsPanel.trailingAnchor, constant: -16),
3646
+
3647
+            schedulerMeetingsScroll.topAnchor.constraint(equalTo: schedulerMeetingsHeader.bottomAnchor, constant: 10),
3648
+            schedulerMeetingsScroll.leadingAnchor.constraint(equalTo: schedulerMeetingsPanel.leadingAnchor, constant: 14),
3649
+            schedulerMeetingsScroll.trailingAnchor.constraint(equalTo: schedulerMeetingsPanel.trailingAnchor, constant: -14),
3650
+            schedulerMeetingsScroll.bottomAnchor.constraint(equalTo: schedulerMeetingsPanel.bottomAnchor, constant: -14),
3651
+
3652
+            schedulerMeetingsDocument.widthAnchor.constraint(equalTo: schedulerMeetingsScroll.contentView.widthAnchor),
3653
+            schedulerMeetingsStack.topAnchor.constraint(equalTo: schedulerMeetingsDocument.topAnchor),
3654
+            schedulerMeetingsStack.leadingAnchor.constraint(equalTo: schedulerMeetingsDocument.leadingAnchor),
3655
+            schedulerMeetingsStack.trailingAnchor.constraint(equalTo: schedulerMeetingsDocument.trailingAnchor),
3656
+            schedulerMeetingsStack.bottomAnchor.constraint(lessThanOrEqualTo: schedulerMeetingsDocument.bottomAnchor),
3657
+
3658
+            schedulerEmptyLabel.centerXAnchor.constraint(equalTo: schedulerMeetingsPanel.centerXAnchor),
3659
+            schedulerEmptyLabel.centerYAnchor.constraint(equalTo: schedulerMeetingsPanel.centerYAnchor)
3296 3660
         ])
3297 3661
 
3298 3662
         timeLabel = timeTitle
@@ -3304,6 +3668,13 @@ class ViewController: NSViewController {
3304 3668
         homeMeetingsPanel = panel
3305 3669
         homePlaceholderLabel = placeholder
3306 3670
         homeSettingsView = settingsView
3671
+        schedulerRootView = schedulerRoot
3672
+        self.schedulerMonthLabel = schedulerMonthLabel
3673
+        schedulerCalendarGridStack = schedulerCalendarGrid
3674
+        self.schedulerSelectedDateLabel = schedulerSelectedDateLabel
3675
+        schedulerMeetingsListStack = schedulerMeetingsStack
3676
+        schedulerMeetingsEmptyLabel = schedulerEmptyLabel
3677
+        self.schedulerTodayButton = schedulerTodayButton
3307 3678
         meetingsDayHeaderLabel = panelHeader
3308 3679
         meetingsListStack = meetingsStack
3309 3680
         meetingsStatusLabel = meetingsStatus
@@ -3315,6 +3686,9 @@ class ViewController: NSViewController {
3315 3686
         updateClock()
3316 3687
         updateMeetingsDayUI()
3317 3688
         applyFilteredMeetings()
3689
+        renderSchedulerCalendarGrid()
3690
+        updateSchedulerDateHeader()
3691
+        applySchedulerMeetingsForSelectedDate()
3318 3692
 
3319 3693
         homeSearchField = searchField
3320 3694
         homeSearchPill = searchPill
@@ -3323,7 +3697,9 @@ class ViewController: NSViewController {
3323 3697
             object: searchField,
3324 3698
             queue: .main
3325 3699
         ) { [weak self] _ in
3326
-            self?.applyFilteredMeetings()
3700
+            Task { @MainActor [weak self] in
3701
+                self?.applyFilteredMeetings()
3702
+            }
3327 3703
             updateSearchHintVisibility()
3328 3704
         }
3329 3705
 
@@ -3591,10 +3967,9 @@ class ViewController: NSViewController {
3591 3967
     private func updateSelectedHomeSectionUI() {
3592 3968
         let isHome = selectedHomeSidebarItem == "Home"
3593 3969
         let isSettings = selectedHomeSidebarItem == "Settings"
3594
-        let title = selectedHomeSidebarItem
3595
-
3596
-        homeWelcomeLabel?.stringValue = title
3597
-        homeWelcomeLabel?.isHidden = isSettings
3970
+        let isScheduler = selectedHomeSidebarItem == "Scheduler"
3971
+        homeWelcomeLabel?.stringValue = "Home"
3972
+        homeWelcomeLabel?.isHidden = (isHome == false) || isSettings
3598 3973
 
3599 3974
         let dashboardViews: [NSView?] = [
3600 3975
             homeTimeLabelView,
@@ -3620,6 +3995,7 @@ class ViewController: NSViewController {
3620 3995
             emptyMeetingLabel?.isHidden = hasMeetingCards
3621 3996
         }
3622 3997
         homeSettingsView?.isHidden = isSettings == false
3998
+        schedulerRootView?.isHidden = isScheduler == false
3623 3999
 
3624 4000
         if isHome {
3625 4001
             homePlaceholderLabel?.isHidden = true
@@ -3627,6 +4003,12 @@ class ViewController: NSViewController {
3627 4003
             // Keep non-Home pages empty for now.
3628 4004
             homePlaceholderLabel?.isHidden = true
3629 4005
         }
4006
+
4007
+        if isScheduler {
4008
+            updateSchedulerDateHeader()
4009
+            applySchedulerMeetingsForSelectedDate()
4010
+            renderSchedulerCalendarGrid()
4011
+        }
3630 4012
     }
3631 4013
 
3632 4014
     private func sidebarSymbolName(for item: String, filled: Bool = false) -> String {