소스 검색

Polish Schedule page layout, filters, and card grid behavior.

Schedule content now pins with geometric anchors and LTR stacks, uses overlay scrollers and asymmetric insets, left-aligns the title row, and sizes card rows without empty columns so one or two meetings fill the width. Filter spacing, control widths, and card dimensions were increased for a fuller main panel.

Made-with: Cursor
huzaifahayat12 3 달 전
부모
커밋
767faa4f31
1개의 변경된 파일114개의 추가작업 그리고 50개의 파일을 삭제
  1. 114 50
      meetings_app/ViewController.swift

+ 114 - 50
meetings_app/ViewController.swift

@@ -330,8 +330,12 @@ final class ViewController: NSViewController {
     private var schedulePageVisibleCount: Int = 0
     private let schedulePageBatchSize: Int = 20
     private let schedulePageCardsPerRow: Int = 3
-    private let schedulePageCardWidth: CGFloat = 240
-    private let schedulePageCardSpacing: CGFloat = 12
+    private let schedulePageCardSpacing: CGFloat = 20
+    private let schedulePageCardHeight: CGFloat = 182
+    private let schedulePageStackSpacing: CGFloat = 16
+    private let schedulePageLeadingInset: CGFloat = 28
+    /// Tighter than leading so cards/filters use full width; overlay scrollbar avoids a dead right gutter.
+    private let schedulePageTrailingInset: CGFloat = 12
     private var schedulePageScrollObservation: NSObjectProtocol?
     private weak var schedulePageDateHeadingLabel: NSTextField?
     private weak var schedulePageFilterDropdown: NSPopUpButton?
@@ -1736,20 +1740,25 @@ private extension ViewController {
     func makeSchedulePageContent() -> NSView {
         let panel = NSView()
         panel.translatesAutoresizingMaskIntoConstraints = false
+        panel.userInterfaceLayoutDirection = .leftToRight
 
         let contentStack = NSStackView()
         contentStack.translatesAutoresizingMaskIntoConstraints = false
+        contentStack.userInterfaceLayoutDirection = .leftToRight
         contentStack.orientation = .vertical
-        contentStack.spacing = 12
-        contentStack.alignment = .leading
+        contentStack.spacing = schedulePageStackSpacing
+        contentStack.alignment = .width
 
-        contentStack.addArrangedSubview(schedulePageHeader())
+        let header = schedulePageHeader()
+        contentStack.addArrangedSubview(header)
 
         let heading = textLabel(schedulePageInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
+        heading.alignment = .left
         schedulePageDateHeadingLabel = heading
         contentStack.addArrangedSubview(heading)
 
         let rangeError = textLabel("", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .systemRed)
+        rangeError.alignment = .left
         rangeError.isHidden = true
         schedulePageRangeErrorLabel = rangeError
         contentStack.addArrangedSubview(rangeError)
@@ -1760,11 +1769,14 @@ private extension ViewController {
         panel.addSubview(contentStack)
 
         NSLayoutConstraint.activate([
-            contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
-            contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
+            contentStack.leftAnchor.constraint(equalTo: panel.leftAnchor, constant: schedulePageLeadingInset),
+            contentStack.rightAnchor.constraint(equalTo: panel.rightAnchor, constant: -schedulePageTrailingInset),
             contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 6),
-            cardsContainer.leadingAnchor.constraint(equalTo: contentStack.leadingAnchor),
-            cardsContainer.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -16)
+            contentStack.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -16),
+            header.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
+            heading.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
+            rangeError.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
+            cardsContainer.widthAnchor.constraint(equalTo: contentStack.widthAnchor)
         ])
 
         Task { [weak self] in
@@ -2658,37 +2670,81 @@ private extension ViewController {
     private func schedulePageHeader() -> NSView {
         let container = NSStackView()
         container.translatesAutoresizingMaskIntoConstraints = false
+        container.userInterfaceLayoutDirection = .leftToRight
         container.orientation = .vertical
-        container.spacing = 10
-        container.alignment = .leading
+        container.spacing = 14
+        container.alignment = .width
+
+        let titleRow = NSStackView()
+        titleRow.translatesAutoresizingMaskIntoConstraints = false
+        titleRow.userInterfaceLayoutDirection = .leftToRight
+        titleRow.orientation = .horizontal
+        titleRow.alignment = .centerY
+        titleRow.distribution = .fill
+        titleRow.spacing = 0
 
         let titleLabel = textLabel("Schedule", font: typography.pageTitle, color: palette.textPrimary)
-        container.addArrangedSubview(titleLabel)
+        titleLabel.alignment = .left
+        titleLabel.userInterfaceLayoutDirection = .leftToRight
+        titleLabel.maximumNumberOfLines = 1
+        titleLabel.lineBreakMode = .byTruncatingTail
+        titleLabel.setContentHuggingPriority(.required, for: .horizontal)
+        titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+        let titleRowSpacer = NSView()
+        titleRowSpacer.translatesAutoresizingMaskIntoConstraints = false
+        titleRowSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        titleRowSpacer.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+
+        titleRow.addArrangedSubview(titleLabel)
+        titleRow.addArrangedSubview(titleRowSpacer)
+        container.addArrangedSubview(titleRow)
 
         let filterRow = NSStackView()
         filterRow.translatesAutoresizingMaskIntoConstraints = false
+        filterRow.userInterfaceLayoutDirection = .leftToRight
         filterRow.orientation = .horizontal
         filterRow.alignment = .centerY
-        filterRow.spacing = 10
+        filterRow.spacing = 18
+        filterRow.distribution = .fill
 
         let filterDropdown = makeSchedulePageFilterDropdown()
         schedulePageFilterDropdown = filterDropdown
         filterRow.addArrangedSubview(filterDropdown)
+        filterRow.setCustomSpacing(20, after: filterDropdown)
 
         let fromPicker = makeScheduleDatePicker(date: schedulePageFromDate)
         schedulePageFromDatePicker = fromPicker
         filterRow.addArrangedSubview(fromPicker)
+        filterRow.setCustomSpacing(16, after: fromPicker)
 
         let toPicker = makeScheduleDatePicker(date: schedulePageToDate)
         schedulePageToDatePicker = toPicker
         filterRow.addArrangedSubview(toPicker)
+        NSLayoutConstraint.activate([
+            fromPicker.widthAnchor.constraint(equalTo: toPicker.widthAnchor),
+            fromPicker.widthAnchor.constraint(greaterThanOrEqualToConstant: 152)
+        ])
 
-        filterRow.addArrangedSubview(makeSchedulePagePillButton(title: "Apply", action: #selector(schedulePageApplyDateRangePressed(_:))))
-        filterRow.addArrangedSubview(makeSchedulePagePillButton(title: "Reset", action: #selector(schedulePageResetFiltersPressed(_:))))
+        let filterRowSpacer = NSView()
+        filterRowSpacer.translatesAutoresizingMaskIntoConstraints = false
+        filterRowSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        filterRowSpacer.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+        filterRow.addArrangedSubview(filterRowSpacer)
+
+        let applyButton = makeSchedulePagePillButton(title: "Apply", action: #selector(schedulePageApplyDateRangePressed(_:)))
+        filterRow.addArrangedSubview(applyButton)
+        filterRow.setCustomSpacing(22, after: applyButton)
+        let resetButton = makeSchedulePagePillButton(title: "Reset", action: #selector(schedulePageResetFiltersPressed(_:)))
+        filterRow.addArrangedSubview(resetButton)
+        filterRow.setCustomSpacing(22, after: resetButton)
         filterRow.addArrangedSubview(makeScheduleRefreshButton())
 
         container.addArrangedSubview(filterRow)
-        container.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
+        NSLayoutConstraint.activate([
+            titleRow.widthAnchor.constraint(equalTo: container.widthAnchor),
+            filterRow.widthAnchor.constraint(equalTo: container.widthAnchor)
+        ])
         refreshSchedulePageDateFilterUI()
         return container
     }
@@ -2710,7 +2766,7 @@ private extension ViewController {
         button.target = self
         button.action = #selector(schedulePageFilterDropdownChanged(_:))
         button.heightAnchor.constraint(equalToConstant: 34).isActive = true
-        button.widthAnchor.constraint(equalToConstant: 190).isActive = true
+        button.widthAnchor.constraint(equalToConstant: 228).isActive = true
 
         button.removeAllItems()
         button.addItems(withTitles: ["All", "Today", "This week", "This month", "Custom range"])
@@ -2742,7 +2798,7 @@ private extension ViewController {
         picker.dateValue = date
         picker.font = typography.filterText
         picker.heightAnchor.constraint(equalToConstant: 34).isActive = true
-        picker.widthAnchor.constraint(equalToConstant: 156).isActive = true
+        picker.setContentHuggingPriority(.defaultLow, for: .horizontal)
         picker.target = self
         picker.action = #selector(schedulePageDatePickerChanged(_:))
         return picker
@@ -2752,7 +2808,7 @@ private extension ViewController {
         let button = makeSchedulePillButton(title: title)
         button.target = self
         button.action = action
-        button.widthAnchor.constraint(equalToConstant: 92).isActive = true
+        button.widthAnchor.constraint(equalToConstant: 100).isActive = true
         return button
     }
 
@@ -2764,24 +2820,27 @@ private extension ViewController {
 
         let wrapper = NSView()
         wrapper.translatesAutoresizingMaskIntoConstraints = false
+        wrapper.userInterfaceLayoutDirection = .leftToRight
 
         let scroll = NSScrollView()
         scroll.translatesAutoresizingMaskIntoConstraints = false
+        scroll.userInterfaceLayoutDirection = .leftToRight
         scroll.drawsBackground = false
         scroll.hasHorizontalScroller = false
         scroll.hasVerticalScroller = true
         scroll.autohidesScrollers = true
         scroll.borderType = .noBorder
-        let viewportWidth = (schedulePageCardWidth * CGFloat(schedulePageCardsPerRow)) + (schedulePageCardSpacing * CGFloat(schedulePageCardsPerRow - 1))
-        scroll.widthAnchor.constraint(equalToConstant: viewportWidth).isActive = true
+        scroll.scrollerStyle = .overlay
+        scroll.automaticallyAdjustsContentInsets = false
         schedulePageCardsScrollView = scroll
         wrapper.addSubview(scroll)
 
         let stack = NSStackView()
         stack.translatesAutoresizingMaskIntoConstraints = false
+        stack.userInterfaceLayoutDirection = .leftToRight
         stack.orientation = .vertical
-        stack.spacing = 12
-        stack.alignment = .leading
+        stack.spacing = schedulePageCardSpacing
+        stack.alignment = .width
         schedulePageCardsStack = stack
         scroll.documentView = stack
 
@@ -3035,16 +3094,21 @@ private extension ViewController {
         return wrapper
     }
 
-    func scheduleCard(meeting: ScheduledMeeting) -> NSView {
+    func scheduleCard(meeting: ScheduledMeeting, useFlexibleWidth: Bool = false, contentHeight: CGFloat = 150) -> NSView {
         let cardWidth: CGFloat = 240
 
         let card = roundedContainer(cornerRadius: 12, color: palette.sectionCard)
         styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: true)
         card.translatesAutoresizingMaskIntoConstraints = false
-        card.widthAnchor.constraint(equalToConstant: cardWidth).isActive = true
-        card.heightAnchor.constraint(equalToConstant: 150).isActive = true
-        card.setContentHuggingPriority(.required, for: .horizontal)
-        card.setContentCompressionResistancePriority(.required, for: .horizontal)
+        card.heightAnchor.constraint(equalToConstant: contentHeight).isActive = true
+        if useFlexibleWidth {
+            card.setContentHuggingPriority(.defaultLow, for: .horizontal)
+            card.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+        } else {
+            card.widthAnchor.constraint(equalToConstant: cardWidth).isActive = true
+            card.setContentHuggingPriority(.required, for: .horizontal)
+            card.setContentCompressionResistancePriority(.required, for: .horizontal)
+        }
 
         let icon = roundedContainer(cornerRadius: 8, color: palette.meetingBadge)
         icon.translatesAutoresizingMaskIntoConstraints = false
@@ -3089,7 +3153,7 @@ private extension ViewController {
         card.addSubview(time)
         card.addSubview(duration)
 
-        NSLayoutConstraint.activate([
+        var titleConstraints: [NSLayoutConstraint] = [
             icon.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10),
             icon.topAnchor.constraint(equalTo: card.topAnchor, constant: 10),
 
@@ -3098,9 +3162,12 @@ private extension ViewController {
 
             title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 6),
             title.centerYAnchor.constraint(equalTo: icon.centerYAnchor),
-            title.trailingAnchor.constraint(lessThanOrEqualTo: dayChip.leadingAnchor, constant: -8),
-            title.widthAnchor.constraint(lessThanOrEqualToConstant: 130),
-
+            title.trailingAnchor.constraint(lessThanOrEqualTo: dayChip.leadingAnchor, constant: -8)
+        ]
+        if !useFlexibleWidth {
+            titleConstraints.append(title.widthAnchor.constraint(lessThanOrEqualToConstant: 130))
+        }
+        NSLayoutConstraint.activate(titleConstraints + [
             subtitle.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10),
             subtitle.topAnchor.constraint(equalTo: icon.bottomAnchor, constant: 10),
             subtitle.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -10),
@@ -3119,10 +3186,15 @@ private extension ViewController {
         hit.isBordered = false
         hit.bezelStyle = .regularSquare
         hit.identifier = NSUserInterfaceItemIdentifier(meeting.meetURL.absoluteString)
-        hit.widthAnchor.constraint(equalToConstant: cardWidth).isActive = true
-        hit.heightAnchor.constraint(equalToConstant: 150).isActive = true
-        hit.setContentHuggingPriority(.required, for: .horizontal)
-        hit.setContentCompressionResistancePriority(.required, for: .horizontal)
+        hit.heightAnchor.constraint(equalToConstant: contentHeight).isActive = true
+        if useFlexibleWidth {
+            hit.setContentHuggingPriority(.defaultLow, for: .horizontal)
+            hit.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+        } else {
+            hit.widthAnchor.constraint(equalToConstant: cardWidth).isActive = true
+            hit.setContentHuggingPriority(.required, for: .horizontal)
+            hit.setContentCompressionResistancePriority(.required, for: .horizontal)
+        }
         hit.addSubview(card)
         NSLayoutConstraint.activate([
             card.leadingAnchor.constraint(equalTo: hit.leadingAnchor),
@@ -4437,9 +4509,8 @@ private extension ViewController {
         if visibleMeetings.isEmpty {
             let empty = roundedContainer(cornerRadius: 10, color: palette.sectionCard)
             empty.translatesAutoresizingMaskIntoConstraints = false
-            empty.heightAnchor.constraint(equalToConstant: 120).isActive = true
-            let viewportWidth = (schedulePageCardWidth * CGFloat(schedulePageCardsPerRow)) + (schedulePageCardSpacing * CGFloat(schedulePageCardsPerRow - 1))
-            empty.widthAnchor.constraint(equalToConstant: viewportWidth).isActive = true
+            empty.heightAnchor.constraint(equalToConstant: 140).isActive = true
+            empty.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
             styleSurface(empty, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
             let label = textLabel(googleOAuth.loadTokens() == nil ? "Connect to load schedule" : "No meetings for selected filters", font: typography.cardSubtitle, color: palette.textSecondary)
             label.translatesAutoresizingMaskIntoConstraints = false
@@ -4456,24 +4527,17 @@ private extension ViewController {
         while index < visibleMeetings.count {
             let row = NSStackView()
             row.translatesAutoresizingMaskIntoConstraints = false
+            row.userInterfaceLayoutDirection = .leftToRight
             row.orientation = .horizontal
             row.alignment = .top
             row.spacing = schedulePageCardSpacing
-            row.distribution = .fill
+            row.distribution = .fillEqually
             let rowEnd = min(index + schedulePageCardsPerRow, visibleMeetings.count)
             for meeting in visibleMeetings[index..<rowEnd] {
-                row.addArrangedSubview(scheduleCard(meeting: meeting))
-            }
-            if rowEnd - index < schedulePageCardsPerRow {
-                for _ in 0..<(schedulePageCardsPerRow - (rowEnd - index)) {
-                    let spacer = NSView()
-                    spacer.translatesAutoresizingMaskIntoConstraints = false
-                    spacer.widthAnchor.constraint(equalToConstant: schedulePageCardWidth).isActive = true
-                    spacer.heightAnchor.constraint(equalToConstant: 150).isActive = true
-                    row.addArrangedSubview(spacer)
-                }
+                row.addArrangedSubview(scheduleCard(meeting: meeting, useFlexibleWidth: true, contentHeight: schedulePageCardHeight))
             }
             stack.addArrangedSubview(row)
+            row.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
             index = rowEnd
         }